@pome-sh/cli 0.21.5 → 0.21.6

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.
@@ -1,9 +1,14 @@
1
- import { statePath, defineCheck, VACUITY_SENTINEL, VACUITY_SENTINEL_NUMBER, childStatePath } from './chunk-NY55QTVQ.js';
1
+ export { LINEAR_CHECKS } from './chunk-XS66ZRBJ.js';
2
+ import { MCP_PAGE_MAX, defaultSeedState, linearSeedSchema, notFound, badUserInput, MCP_PAGE_DEFAULT, DEFAULT_LINEAR_EMAIL, RELAY_PAGE_MAX, RELAY_PAGE_DEFAULT, STATE_EXPORT_CAP, parseSeed, DEFAULT_LINEAR_CLOCK, LINEAR_PROVIDER_TOKEN_PREFIX, DEFAULT_LINEAR_SID, DEFAULT_SCOPES, linearErrorEnvelope, unauthorizedEnvelope, unsupportedEnvelope, assertWebhookUrl, ACCESS_TOKEN_TTL_SECONDS, OAUTH_CODE_TTL_SECONDS, TITLE_MAX_BYTES, BODY_MAX_BYTES, LinearTwinError, GRAPHQL_QUERY_MAX_BYTES, GRAPHQL_SELECTION_DEPTH_MAX } from './chunk-ZKID2HS3.js';
3
+ export { DEFAULT_LINEAR_CLOCK, DEFAULT_LINEAR_EMAIL, DEFAULT_LINEAR_PORT, DEFAULT_LINEAR_SID, DEFAULT_LINEAR_TOKEN, LINEAR_PROVIDER_TOKEN_PREFIX, LinearTwinError, STATE_EXPORT_CAP, assertWebhookUrl, defaultSeedState, linearErrorEnvelope, linearSeedSchema, loadSeedFromEnv, parseSeed, unauthorizedEnvelope, unsupportedEnvelope, webhookUrlError } from './chunk-ZKID2HS3.js';
4
+ import './chunk-NY55QTVQ.js';
2
5
  import { openTwinDatabase, defineTwin, createApp } from './chunk-LZIPGCQJ.js';
3
- import { z, ZodError } from 'zod';
6
+ import './chunk-VBATFCWR.js';
7
+ import './chunk-SG6ZTIMT.js';
4
8
  import { createHmac, timingSafeEqual, createHash, randomBytes } from 'node:crypto';
5
9
  import { isIP } from 'node:net';
6
10
  import { lookup } from 'node:dns/promises';
11
+ import { z } from 'zod';
7
12
  import { Hono } from 'hono';
8
13
  import { buildSchema, graphql, parse, getOperationAST } from 'graphql';
9
14
 
@@ -367,123 +372,6 @@ function ensureColumn(db, table, column, definition) {
367
372
  db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
368
373
  }
369
374
  }
370
- var LinearTwinError = class extends Error {
371
- status;
372
- code;
373
- extensions;
374
- constructor(status, code, message, extensions = {}) {
375
- super(message);
376
- this.status = status;
377
- this.code = code;
378
- this.extensions = extensions;
379
- this.name = "LinearTwinError";
380
- }
381
- toGraphQLError() {
382
- return {
383
- message: this.message,
384
- extensions: {
385
- code: this.code,
386
- http: { status: this.status },
387
- ...this.extensions
388
- }
389
- };
390
- }
391
- };
392
- function unauthorizedEnvelope(message = "Authentication required") {
393
- return {
394
- status: 401,
395
- body: {
396
- errors: [
397
- {
398
- message,
399
- extensions: { code: "AUTHENTICATION_ERROR", http: { status: 401 } }
400
- }
401
- ]
402
- }
403
- };
404
- }
405
- function unsupportedEnvelope(method, path) {
406
- return {
407
- status: 501,
408
- body: {
409
- message: `Unsupported Linear twin route: ${method} ${path}`,
410
- errors: [
411
- {
412
- message: "Not implemented",
413
- extensions: { code: "UNIMPLEMENTED", http: { status: 501 } }
414
- }
415
- ],
416
- fidelity: "unsupported",
417
- method,
418
- path
419
- }
420
- };
421
- }
422
- function linearErrorEnvelope(error) {
423
- if (error instanceof LinearTwinError) {
424
- return {
425
- status: error.status,
426
- body: { errors: [error.toGraphQLError()] }
427
- };
428
- }
429
- if (error instanceof ZodError || error instanceof Error && error.name === "ZodError") {
430
- const message = error instanceof ZodError ? error.issues[0]?.message ?? "Invalid request" : "Invalid request";
431
- return {
432
- status: 400,
433
- body: {
434
- errors: [
435
- {
436
- message,
437
- extensions: { code: "BAD_USER_INPUT", http: { status: 400 } }
438
- }
439
- ]
440
- }
441
- };
442
- }
443
- if (error instanceof SyntaxError) {
444
- return {
445
- status: 400,
446
- body: {
447
- errors: [
448
- {
449
- message: "Invalid JSON",
450
- extensions: { code: "BAD_USER_INPUT", http: { status: 400 } }
451
- }
452
- ]
453
- }
454
- };
455
- }
456
- if (error instanceof Error && error.name === "UnknownToolError") {
457
- return {
458
- status: 404,
459
- body: {
460
- errors: [
461
- {
462
- message: error.message,
463
- extensions: { code: "NOT_FOUND", http: { status: 404 } }
464
- }
465
- ]
466
- }
467
- };
468
- }
469
- return {
470
- status: 500,
471
- body: {
472
- errors: [
473
- {
474
- message: error instanceof Error ? error.message : "Internal Server Error",
475
- extensions: { code: "INTERNAL_SERVER_ERROR", http: { status: 500 } }
476
- }
477
- ]
478
- }
479
- };
480
- }
481
- function badUserInput(message, extensions = {}) {
482
- throw new LinearTwinError(400, "BAD_USER_INPUT", message, extensions);
483
- }
484
- function notFound(message) {
485
- throw new LinearTwinError(404, "NOT_FOUND", message);
486
- }
487
375
  function linearIdFromCounter(namespace, counter) {
488
376
  const digest = createHash("sha256").update(`${namespace}:${counter}`).digest("hex");
489
377
  return [
@@ -504,514 +392,6 @@ function byteLength(value) {
504
392
  return Buffer.byteLength(value, "utf8");
505
393
  }
506
394
 
507
- // ../packages/twin-linear/dist/src/types.js
508
- var DEFAULT_LINEAR_CLOCK = "2026-07-21T00:00:00.000Z";
509
- var DEFAULT_LINEAR_EMAIL = "admin@pome-twin.test";
510
- var DEFAULT_LINEAR_TOKEN = "lin_test_admin";
511
- var LINEAR_PROVIDER_TOKEN_PREFIX = "lin_pome_";
512
- var DEFAULT_LINEAR_SID = "standalone";
513
- var DEFAULT_LINEAR_PORT = 3337;
514
- var DEFAULT_SCOPES = ["read", "write", "issues:create", "comments:create", "admin"];
515
- var TITLE_MAX_BYTES = 512;
516
- var BODY_MAX_BYTES = 65536;
517
- var GRAPHQL_QUERY_MAX_BYTES = 1e5;
518
- var GRAPHQL_SELECTION_DEPTH_MAX = 20;
519
- var MCP_PAGE_DEFAULT = 50;
520
- var MCP_PAGE_MAX = 250;
521
- var RELAY_PAGE_DEFAULT = 50;
522
- var RELAY_PAGE_MAX = 250;
523
- var STATE_EXPORT_CAP = 2e3;
524
- var OAUTH_CODE_TTL_SECONDS = 600;
525
- var ACCESS_TOKEN_TTL_SECONDS = 3600;
526
-
527
- // ../packages/twin-linear/dist/src/webhook-url.js
528
- function webhookUrlError(url) {
529
- let parsed;
530
- try {
531
- parsed = new URL(url);
532
- } catch {
533
- return `Invalid webhook URL: ${url}`;
534
- }
535
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
536
- return `Webhook URL must use http or https: ${url}`;
537
- }
538
- if (parsed.username || parsed.password) {
539
- return `Webhook URL must not include credentials`;
540
- }
541
- return null;
542
- }
543
- function assertWebhookUrl(url) {
544
- const error = webhookUrlError(url);
545
- if (error)
546
- badUserInput(error);
547
- return url;
548
- }
549
-
550
- // ../packages/twin-linear/dist/src/seed.js
551
- var datetime = z.string().datetime({ offset: true });
552
- var email = z.string().trim().email().transform((v) => v.toLowerCase());
553
- var id = z.string().min(1).max(128);
554
- var scopesField = z.union([z.array(z.string().min(1).max(64)).max(50), z.string().max(500)]).optional();
555
- var stateType = z.enum(["backlog", "unstarted", "started", "completed", "canceled"]);
556
- var linearSeedSchema = z.object({
557
- clock: datetime.default(DEFAULT_LINEAR_CLOCK),
558
- defaultSid: z.string().min(1).max(128).default(DEFAULT_LINEAR_SID),
559
- baseUrl: z.string().url().default("http://127.0.0.1:3337"),
560
- strictScopes: z.boolean().default(false),
561
- organization: z.object({
562
- id: id.optional(),
563
- name: z.string().min(1).max(200).optional(),
564
- urlKey: z.string().min(1).max(100).optional()
565
- }).strict().optional(),
566
- users: z.array(z.object({
567
- id: id.optional(),
568
- email,
569
- name: z.string().min(1).max(200).optional(),
570
- displayName: z.string().min(1).max(200).optional(),
571
- avatarUrl: z.string().url().nullable().optional(),
572
- active: z.boolean().default(true),
573
- admin: z.boolean().default(false),
574
- app: z.boolean().default(false)
575
- }).strict()).max(500).default([]),
576
- teams: z.array(z.object({
577
- id: id.optional(),
578
- key: z.string().min(1).max(20).regex(/^[A-Z][A-Z0-9]*$/),
579
- name: z.string().min(1).max(200),
580
- description: z.string().max(2e3).nullable().optional(),
581
- private: z.boolean().default(false),
582
- states: z.array(z.object({
583
- id: id.optional(),
584
- name: z.string().min(1).max(100),
585
- type: stateType.optional(),
586
- position: z.number().int().nonnegative().optional()
587
- }).strict()).max(50).optional()
588
- }).strict()).max(50).default([]),
589
- labels: z.array(z.object({
590
- id: id.optional(),
591
- name: z.string().min(1).max(100),
592
- color: z.string().max(32).optional(),
593
- description: z.string().max(2e3).nullable().optional(),
594
- team: z.string().min(1).max(128).optional()
595
- }).strict()).max(500).default([]),
596
- projects: z.array(z.object({
597
- id: id.optional(),
598
- name: z.string().min(1).max(200),
599
- description: z.string().max(1e4).nullable().optional(),
600
- state: z.enum(["planned", "started", "completed", "canceled"]).default("planned"),
601
- team: z.string().min(1).max(128).optional()
602
- }).strict()).max(200).default([]),
603
- cycles: z.array(z.object({
604
- id: id.optional(),
605
- team: z.string().min(1).max(128),
606
- name: z.string().min(1).max(200),
607
- number: z.number().int().positive().optional(),
608
- startsAt: datetime.nullable().optional(),
609
- endsAt: datetime.nullable().optional()
610
- }).strict()).max(200).default([]),
611
- issues: z.array(z.object({
612
- id: id.optional(),
613
- team: z.string().min(1).max(128),
614
- title: z.string().min(1).max(512),
615
- description: z.string().max(65536).nullable().optional(),
616
- priority: z.union([z.literal(0), z.literal(1), z.literal(2), z.literal(3), z.literal(4)]).default(0),
617
- state: z.string().min(1).max(128).optional(),
618
- assignee: z.string().min(1).max(200).optional(),
619
- creator: z.string().min(1).max(200).optional(),
620
- delegate: z.string().min(1).max(200).optional(),
621
- project: z.string().min(1).max(200).optional(),
622
- cycle: z.string().min(1).max(200).optional(),
623
- parent: z.string().min(1).max(200).optional(),
624
- estimate: z.number().int().nonnegative().nullable().optional(),
625
- labels: z.array(z.string().min(1).max(100)).max(50).default([]),
626
- dueDate: z.string().max(32).nullable().optional(),
627
- createdAt: datetime.optional(),
628
- updatedAt: datetime.optional()
629
- }).strict()).max(5e3).default([]),
630
- comments: z.array(z.object({
631
- id: id.optional(),
632
- issue: z.string().min(1).max(128),
633
- body: z.string().min(1).max(65536),
634
- parent: z.string().min(1).max(128).optional(),
635
- user: z.string().min(1).max(200).optional(),
636
- createdAt: datetime.optional()
637
- }).strict()).max(2e4).default([]),
638
- documents: z.array(z.object({
639
- id: id.optional(),
640
- title: z.string().min(1).max(512),
641
- content: z.string().max(65536).nullable().optional(),
642
- slug: z.string().min(1).max(200).optional(),
643
- project: z.string().min(1).max(200).optional(),
644
- team: z.string().min(1).max(128).optional(),
645
- issue: z.string().min(1).max(200).optional(),
646
- cycle: z.string().min(1).max(200).optional(),
647
- icon: z.string().max(64).nullable().optional(),
648
- color: z.string().max(32).nullable().optional(),
649
- creator: z.string().min(1).max(200).optional(),
650
- createdAt: datetime.optional(),
651
- updatedAt: datetime.optional()
652
- }).strict()).max(500).default([]),
653
- oauthApps: z.array(z.object({
654
- id: id.optional(),
655
- clientId: z.string().min(1).max(200),
656
- clientSecret: z.string().min(1).max(500),
657
- name: z.string().min(1).max(200),
658
- redirectUris: z.array(z.string().url()).min(1).max(20),
659
- scopes: scopesField,
660
- actor: z.enum(["user", "app"]).default("user"),
661
- assignable: z.boolean().default(false),
662
- mentionable: z.boolean().default(false),
663
- appUserId: z.string().min(1).max(128).nullable().optional()
664
- }).strict()).max(20).default([]),
665
- tokens: z.array(z.object({
666
- token: z.string().min(1).max(500),
667
- type: z.enum(["personal", "oauth_access", "client_credentials"]).default("personal"),
668
- user: z.string().min(1).max(200).optional(),
669
- app: z.string().min(1).max(200).optional(),
670
- scopes: scopesField,
671
- actor: z.enum(["user", "app"]).optional(),
672
- sid: z.string().min(1).max(128).optional(),
673
- expiresAt: datetime.nullable().optional()
674
- }).strict()).max(50).default([]),
675
- webhooks: z.array(z.object({
676
- id: id.optional(),
677
- label: z.string().min(1).max(200).optional(),
678
- url: z.string().url(),
679
- resourceTypes: scopesField,
680
- team: z.string().min(1).max(128).optional(),
681
- allPublicTeams: z.boolean().optional(),
682
- secret: z.string().max(500).nullable().optional(),
683
- enabled: z.boolean().default(true)
684
- }).strict()).max(50).default([])
685
- }).strict().superRefine((seed, ctx) => {
686
- const teamKeys = new Set(seed.teams.map((t) => t.key));
687
- const teamIds = new Set(seed.teams.map((t) => t.id).filter(Boolean));
688
- const userEmails = new Set(seed.users.map((u) => u.email));
689
- const userIds = new Set(seed.users.map((u) => u.id).filter(Boolean));
690
- const labelNames = new Set(seed.labels.map((l) => l.name));
691
- const projectNames = new Set(seed.projects.map((p) => p.name));
692
- const cycleNames = new Set(seed.cycles.map((c) => c.name));
693
- const issueTitlesByTeam = /* @__PURE__ */ new Map();
694
- const resolveTeam = (ref) => teamKeys.has(ref) || teamIds.has(ref);
695
- const resolveUser = (ref) => userEmails.has(ref.toLowerCase()) || userIds.has(ref) || seed.users.some((u) => u.name === ref || u.displayName === ref);
696
- for (const team of seed.teams) {
697
- const stateNames = /* @__PURE__ */ new Set();
698
- for (const state of team.states ?? []) {
699
- if (stateNames.has(state.name)) {
700
- ctx.addIssue({ code: "custom", message: `Duplicate workflow state ${state.name} on team ${team.key}` });
701
- }
702
- stateNames.add(state.name);
703
- }
704
- }
705
- for (const label of seed.labels) {
706
- if (label.team && !resolveTeam(label.team)) {
707
- ctx.addIssue({ code: "custom", message: `Label team not found: ${label.team}` });
708
- }
709
- }
710
- for (const project of seed.projects) {
711
- if (project.team && !resolveTeam(project.team)) {
712
- ctx.addIssue({ code: "custom", message: `Project team not found: ${project.team}` });
713
- }
714
- }
715
- for (const cycle of seed.cycles) {
716
- if (!resolveTeam(cycle.team)) {
717
- ctx.addIssue({ code: "custom", message: `Cycle team not found: ${cycle.team}` });
718
- }
719
- }
720
- for (const issue of seed.issues) {
721
- if (!resolveTeam(issue.team)) {
722
- ctx.addIssue({ code: "custom", message: `Issue team not found: ${issue.team}` });
723
- }
724
- if (issue.assignee && !resolveUser(issue.assignee)) {
725
- ctx.addIssue({ code: "custom", message: `Issue assignee not found: ${issue.assignee}` });
726
- }
727
- if (issue.creator && !resolveUser(issue.creator)) {
728
- ctx.addIssue({ code: "custom", message: `Issue creator not found: ${issue.creator}` });
729
- }
730
- if (issue.delegate && !resolveUser(issue.delegate)) {
731
- ctx.addIssue({ code: "custom", message: `Issue delegate not found: ${issue.delegate}` });
732
- }
733
- if (issue.project && !projectNames.has(issue.project) && !seed.projects.some((p) => p.id === issue.project)) {
734
- ctx.addIssue({ code: "custom", message: `Issue project not found: ${issue.project}` });
735
- }
736
- if (issue.cycle && !cycleNames.has(issue.cycle) && !seed.cycles.some((c) => c.id === issue.cycle || String(c.number) === issue.cycle)) {
737
- ctx.addIssue({ code: "custom", message: `Issue cycle not found: ${issue.cycle}` });
738
- }
739
- for (const label of issue.labels) {
740
- if (!labelNames.has(label) && !seed.labels.some((l) => l.id === label)) {
741
- ctx.addIssue({ code: "custom", message: `Issue label not found: ${label}` });
742
- }
743
- }
744
- if (issue.state) {
745
- const team = seed.teams.find((t) => t.key === issue.team || t.id === issue.team);
746
- const states = team?.states ?? [];
747
- const defaultNames = ["Backlog", "Todo", "In Progress", "Done", "Canceled"];
748
- const ok = states.some((s) => s.name === issue.state || s.id === issue.state) || defaultNames.includes(issue.state);
749
- if (!ok && states.length > 0) {
750
- ctx.addIssue({ code: "custom", message: `Issue state not found: ${issue.state}` });
751
- }
752
- }
753
- const titles = issueTitlesByTeam.get(issue.team) ?? /* @__PURE__ */ new Set();
754
- if (titles.has(issue.title)) {
755
- ctx.addIssue({ code: "custom", message: `Duplicate issue title on team ${issue.team}: ${issue.title}` });
756
- }
757
- titles.add(issue.title);
758
- issueTitlesByTeam.set(issue.team, titles);
759
- }
760
- for (const comment of seed.comments) {
761
- const issueOk = seed.issues.some((i) => i.id === comment.issue || i.title === comment.issue) || /^[A-Z]+-\d+$/.test(comment.issue);
762
- if (!issueOk) {
763
- ctx.addIssue({ code: "custom", message: `Comment issue not found: ${comment.issue}` });
764
- }
765
- if (comment.user && !resolveUser(comment.user)) {
766
- ctx.addIssue({ code: "custom", message: `Comment user not found: ${comment.user}` });
767
- }
768
- }
769
- for (const app of seed.oauthApps) {
770
- for (const uri of app.redirectUris) {
771
- try {
772
- new URL(uri);
773
- } catch {
774
- ctx.addIssue({ code: "custom", message: `Invalid OAuth redirect URI: ${uri}` });
775
- }
776
- }
777
- if (app.appUserId && !resolveUser(app.appUserId)) {
778
- ctx.addIssue({ code: "custom", message: `OAuth app user not found: ${app.appUserId}` });
779
- }
780
- }
781
- for (const token2 of seed.tokens) {
782
- if (token2.user && !resolveUser(token2.user)) {
783
- ctx.addIssue({ code: "custom", message: `Token user not found: ${token2.user}` });
784
- }
785
- if (token2.app && !seed.oauthApps.some((a) => a.clientId === token2.app || a.id === token2.app)) {
786
- ctx.addIssue({ code: "custom", message: `Token app not found: ${token2.app}` });
787
- }
788
- }
789
- for (const webhook of seed.webhooks) {
790
- const urlError = webhookUrlError(webhook.url);
791
- if (urlError) {
792
- ctx.addIssue({ code: "custom", message: urlError });
793
- }
794
- if (webhook.team && !resolveTeam(webhook.team)) {
795
- ctx.addIssue({ code: "custom", message: `Webhook team not found: ${webhook.team}` });
796
- }
797
- }
798
- });
799
- function parseSeed(input) {
800
- return linearSeedSchema.parse(input);
801
- }
802
- function loadSeedFromEnv(env = process.env) {
803
- const raw = env.POME_SEED_JSON;
804
- if (!raw)
805
- return parseSeed(defaultSeedState());
806
- let parsed;
807
- try {
808
- parsed = JSON.parse(raw);
809
- } catch (error) {
810
- throw new Error(`POME_SEED_JSON is not valid JSON: ${error.message}`);
811
- }
812
- return parseSeed(parsed);
813
- }
814
- function defaultSeedState() {
815
- return {
816
- clock: DEFAULT_LINEAR_CLOCK,
817
- defaultSid: DEFAULT_LINEAR_SID,
818
- baseUrl: "http://127.0.0.1:3337",
819
- strictScopes: false,
820
- organization: {
821
- id: "org_pome",
822
- name: "Pome Twin",
823
- urlKey: "pome-twin"
824
- },
825
- users: [
826
- {
827
- id: "user_admin",
828
- email: DEFAULT_LINEAR_EMAIL,
829
- name: "Admin User",
830
- displayName: "Admin",
831
- admin: true,
832
- active: true
833
- },
834
- {
835
- id: "user_dev",
836
- email: "dev@pome-twin.test",
837
- name: "Developer",
838
- displayName: "Dev",
839
- admin: false,
840
- active: true
841
- },
842
- {
843
- id: "user_agent",
844
- email: "agent@pome-twin.test",
845
- name: "Pome Agent",
846
- displayName: "Pome Agent",
847
- admin: false,
848
- active: true,
849
- app: true
850
- }
851
- ],
852
- teams: [
853
- {
854
- id: "team_eng",
855
- key: "ENG",
856
- name: "Engineering",
857
- description: "Default engineering team",
858
- private: false,
859
- states: [
860
- { id: "state_backlog", name: "Backlog", type: "backlog", position: 0 },
861
- { id: "state_todo", name: "Todo", type: "unstarted", position: 1 },
862
- { id: "state_progress", name: "In Progress", type: "started", position: 2 },
863
- { id: "state_done", name: "Done", type: "completed", position: 3 },
864
- { id: "state_canceled", name: "Canceled", type: "canceled", position: 4 }
865
- ]
866
- }
867
- ],
868
- labels: [
869
- { id: "label_bug", name: "Bug", color: "#d92d20", team: "ENG", description: "Defect" },
870
- { id: "label_feature", name: "Feature", color: "#2563eb", team: "ENG", description: "New work" },
871
- { id: "label_agent", name: "Agent", color: "#0f766e", team: "ENG", description: "Agent triage" }
872
- ],
873
- projects: [
874
- {
875
- id: "project_local",
876
- name: "Local Twin",
877
- description: "Agent evaluation workspace",
878
- state: "started",
879
- team: "ENG"
880
- }
881
- ],
882
- cycles: [
883
- {
884
- id: "cycle_1",
885
- team: "ENG",
886
- name: "Cycle 1",
887
- number: 1,
888
- startsAt: "2026-07-14T00:00:00.000Z",
889
- endsAt: "2026-07-28T00:00:00.000Z"
890
- }
891
- ],
892
- issues: [
893
- {
894
- id: "issue_backlog",
895
- team: "ENG",
896
- title: "Triage inbox for agent eval",
897
- description: "Backlog item waiting for triage.",
898
- priority: 2,
899
- state: "Backlog",
900
- assignee: "dev@pome-twin.test",
901
- creator: DEFAULT_LINEAR_EMAIL,
902
- project: "Local Twin",
903
- cycle: "Cycle 1",
904
- labels: ["Agent"],
905
- createdAt: "2026-07-20T10:00:00.000Z",
906
- updatedAt: "2026-07-20T10:00:00.000Z"
907
- },
908
- {
909
- id: "issue_todo",
910
- team: "ENG",
911
- title: "Ship Linear twin GraphQL surface",
912
- description: "Implement issue CRUD + comments for agent testing.",
913
- priority: 3,
914
- state: "Todo",
915
- assignee: "dev@pome-twin.test",
916
- creator: DEFAULT_LINEAR_EMAIL,
917
- project: "Local Twin",
918
- cycle: "Cycle 1",
919
- labels: ["Feature"],
920
- createdAt: "2026-07-20T12:00:00.000Z",
921
- updatedAt: "2026-07-20T14:00:00.000Z"
922
- },
923
- {
924
- id: "issue_progress",
925
- team: "ENG",
926
- title: "Wire MCP tools to commands",
927
- description: "Keep GraphQL and MCP on the same LinearDomain layer.",
928
- priority: 2,
929
- state: "In Progress",
930
- assignee: DEFAULT_LINEAR_EMAIL,
931
- creator: DEFAULT_LINEAR_EMAIL,
932
- project: "Local Twin",
933
- cycle: "Cycle 1",
934
- labels: ["Feature", "Agent"],
935
- createdAt: "2026-07-20T16:00:00.000Z",
936
- updatedAt: "2026-07-21T00:00:00.000Z"
937
- },
938
- {
939
- id: "issue_done",
940
- team: "ENG",
941
- title: "Seed multi-issue agent world",
942
- description: "Default seed covers backlog/todo/progress/done.",
943
- priority: 1,
944
- state: "Done",
945
- assignee: "dev@pome-twin.test",
946
- creator: DEFAULT_LINEAR_EMAIL,
947
- project: "Local Twin",
948
- labels: ["Bug"],
949
- createdAt: "2026-07-19T09:00:00.000Z",
950
- updatedAt: "2026-07-20T18:00:00.000Z"
951
- }
952
- ],
953
- comments: [
954
- {
955
- id: "comment_1",
956
- issue: "Ship Linear twin GraphQL surface",
957
- body: "Starting with viewer + issues queries.",
958
- user: DEFAULT_LINEAR_EMAIL,
959
- createdAt: "2026-07-20T13:00:00.000Z"
960
- },
961
- {
962
- id: "comment_2",
963
- issue: "Wire MCP tools to commands",
964
- body: "MCP list_issues should match GraphQL issues.",
965
- user: "dev@pome-twin.test",
966
- createdAt: "2026-07-20T17:00:00.000Z"
967
- },
968
- {
969
- id: "comment_3",
970
- issue: "Triage inbox for agent eval",
971
- body: "@Pome Agent please prioritize this.",
972
- user: DEFAULT_LINEAR_EMAIL,
973
- createdAt: "2026-07-20T11:00:00.000Z"
974
- }
975
- ],
976
- oauthApps: [
977
- {
978
- id: "oauth_app_1",
979
- clientId: "lin_example_client_id",
980
- clientSecret: "example_client_secret",
981
- name: "Pome Linear App",
982
- redirectUris: ["http://localhost:3000/api/auth/callback/linear"],
983
- scopes: [...DEFAULT_SCOPES],
984
- actor: "user",
985
- assignable: true,
986
- mentionable: true,
987
- appUserId: "user_agent"
988
- }
989
- ],
990
- tokens: [
991
- {
992
- token: DEFAULT_LINEAR_TOKEN,
993
- type: "personal",
994
- user: DEFAULT_LINEAR_EMAIL,
995
- scopes: [...DEFAULT_SCOPES],
996
- actor: "user",
997
- sid: DEFAULT_LINEAR_SID
998
- }
999
- ],
1000
- webhooks: [
1001
- {
1002
- id: "webhook_1",
1003
- label: "Sample webhook",
1004
- url: "http://127.0.0.1:9999/linear-hooks",
1005
- resourceTypes: ["Issue", "Comment"],
1006
- team: "ENG",
1007
- allPublicTeams: false,
1008
- secret: "whsec_test_linear",
1009
- enabled: true
1010
- }
1011
- ]
1012
- };
1013
- }
1014
-
1015
395
  // ../packages/twin-linear/dist/src/domain/issue-relations.js
1016
396
  function listIssueRelations(domain, issueId) {
1017
397
  const rows = domain.db.prepare(`SELECT r.type AS type, i.identifier AS identifier
@@ -1779,7 +1159,7 @@ async function createIssue(domain, input, actor = {}) {
1779
1159
  const viewer = domain.resolveViewer(actor);
1780
1160
  const now = domain.tick();
1781
1161
  const number = nextIssueNumber(domain, team.id);
1782
- const id2 = domain.nextId("issue");
1162
+ const id = domain.nextId("issue");
1783
1163
  const identifier = `${team.key}-${number}`;
1784
1164
  const labelIds = (input.labelIds ?? []).map((ref) => domain.requireLabel(ref, team.id).id);
1785
1165
  const assigneeId = input.assigneeId ? domain.requireUser(input.assigneeId).id : null;
@@ -1787,7 +1167,7 @@ async function createIssue(domain, input, actor = {}) {
1787
1167
  const projectId = input.projectId ? domain.requireProject(input.projectId, team.id).id : null;
1788
1168
  const cycleId = input.cycleId ? domain.requireCycle(input.cycleId, team.id).id : null;
1789
1169
  const parentId = input.parentId ? domain.requireIssue(input.parentId).id : null;
1790
- if (parentId === id2)
1170
+ if (parentId === id)
1791
1171
  badUserInput("Issue cannot be its own parent");
1792
1172
  const estimate = normalizeEstimate(input.estimate);
1793
1173
  const baseUrl = domain.config("base_url") ?? "http://127.0.0.1:3337";
@@ -1796,10 +1176,10 @@ async function createIssue(domain, input, actor = {}) {
1796
1176
  assignee_id, creator_id, delegate_id, project_id, cycle_id, parent_id, url,
1797
1177
  archived_at, canceled_at, completed_at, started_at, due_date,
1798
1178
  create_as_user, display_icon_url, created_at, updated_at
1799
- ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(id2, identifier, number, team.id, input.title, input.description ?? null, normalizePriority(input.priority), estimate, state.id, assigneeId, viewer.id, delegateId, projectId, cycleId, parentId, `${baseUrl}/issue/${identifier}`, null, state.type === "canceled" ? now : null, state.type === "completed" ? now : null, state.type === "started" ? now : null, input.dueDate ?? null, input.createAsUser ?? null, input.displayIconUrl ?? null, now, now);
1800
- setIssueLabels(domain, id2, labelIds);
1801
- appendIssueRelations(domain, id2, input);
1802
- const issue = domain.requireIssue(id2);
1179
+ ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(id, identifier, number, team.id, input.title, input.description ?? null, normalizePriority(input.priority), estimate, state.id, assigneeId, viewer.id, delegateId, projectId, cycleId, parentId, `${baseUrl}/issue/${identifier}`, null, state.type === "canceled" ? now : null, state.type === "completed" ? now : null, state.type === "started" ? now : null, input.dueDate ?? null, input.createAsUser ?? null, input.displayIconUrl ?? null, now, now);
1180
+ setIssueLabels(domain, id, labelIds);
1181
+ appendIssueRelations(domain, id, input);
1182
+ const issue = domain.requireIssue(id);
1803
1183
  await emitWebhook(domain, {
1804
1184
  type: "Issue",
1805
1185
  action: "create",
@@ -1813,9 +1193,9 @@ async function createIssue(domain, input, actor = {}) {
1813
1193
  }
1814
1194
  return issue;
1815
1195
  }
1816
- async function updateIssue(domain, id2, input, actor = {}) {
1196
+ async function updateIssue(domain, id, input, actor = {}) {
1817
1197
  domain.requireScopes(actor, ["write"]);
1818
- const issue = domain.requireIssue(id2);
1198
+ const issue = domain.requireIssue(id);
1819
1199
  const before = issueWebhookPayload(issue);
1820
1200
  const patch = {};
1821
1201
  let changed = false;
@@ -1974,9 +1354,9 @@ async function updateIssue(domain, id2, input, actor = {}) {
1974
1354
  }
1975
1355
  return updated;
1976
1356
  }
1977
- async function deleteIssue(domain, id2, actor = {}) {
1357
+ async function deleteIssue(domain, id, actor = {}) {
1978
1358
  domain.requireScopes(actor, ["write"]);
1979
- const issue = domain.requireIssue(id2);
1359
+ const issue = domain.requireIssue(id);
1980
1360
  const viewer = domain.resolveViewer(actor);
1981
1361
  domain.db.prepare("DELETE FROM agent_activities WHERE session_id IN (SELECT id FROM agent_sessions WHERE issue_id = ?)").run(issue.id);
1982
1362
  domain.db.prepare("DELETE FROM agent_sessions WHERE issue_id = ?").run(issue.id);
@@ -1994,9 +1374,9 @@ async function deleteIssue(domain, id2, actor = {}) {
1994
1374
  });
1995
1375
  return issue;
1996
1376
  }
1997
- async function archiveIssue(domain, id2, actor = {}) {
1377
+ async function archiveIssue(domain, id, actor = {}) {
1998
1378
  domain.requireScopes(actor, ["write"]);
1999
- const issue = domain.requireIssue(id2);
1379
+ const issue = domain.requireIssue(id);
2000
1380
  const now = domain.tick();
2001
1381
  domain.db.prepare("UPDATE issues SET archived_at = ?, updated_at = ? WHERE id = ?").run(now, now, issue.id);
2002
1382
  const updated = domain.requireIssue(issue.id);
@@ -2010,9 +1390,9 @@ async function archiveIssue(domain, id2, actor = {}) {
2010
1390
  });
2011
1391
  return updated;
2012
1392
  }
2013
- async function unarchiveIssue(domain, id2, actor = {}) {
1393
+ async function unarchiveIssue(domain, id, actor = {}) {
2014
1394
  domain.requireScopes(actor, ["write"]);
2015
- const issue = domain.requireIssue(id2);
1395
+ const issue = domain.requireIssue(id);
2016
1396
  const now = domain.tick();
2017
1397
  domain.db.prepare("UPDATE issues SET archived_at = NULL, updated_at = ? WHERE id = ?").run(now, issue.id);
2018
1398
  const updated = domain.requireIssue(issue.id);
@@ -2060,15 +1440,15 @@ function applySeed(domain, seed) {
2060
1440
  const orgId = seed.organization?.id ?? domain.nextId("org");
2061
1441
  domain.db.prepare(`INSERT INTO organizations(id, name, url_key, url, created_at, updated_at) VALUES (?,?,?,?,?,?)`).run(orgId, orgName, urlKey, `https://linear.app/${urlKey}`, now, now);
2062
1442
  for (const user of seed.users) {
2063
- const id2 = user.id ?? domain.nextId("user");
1443
+ const id = user.id ?? domain.nextId("user");
2064
1444
  const name = user.name ?? user.displayName ?? user.email.split("@")[0];
2065
1445
  domain.db.prepare(`INSERT INTO users(id, email, name, display_name, avatar_url, active, admin, app, created_at, updated_at)
2066
- VALUES (?,?,?,?,?,?,?,?,?,?)`).run(id2, user.email, name, user.displayName ?? name, user.avatarUrl ?? null, user.active ? 1 : 0, user.admin ? 1 : 0, user.app ? 1 : 0, now, now);
1446
+ VALUES (?,?,?,?,?,?,?,?,?,?)`).run(id, user.email, name, user.displayName ?? name, user.avatarUrl ?? null, user.active ? 1 : 0, user.admin ? 1 : 0, user.app ? 1 : 0, now, now);
2067
1447
  }
2068
1448
  for (const teamCfg of seed.teams) {
2069
- const id2 = teamCfg.id ?? domain.nextId("team");
1449
+ const id = teamCfg.id ?? domain.nextId("team");
2070
1450
  domain.db.prepare(`INSERT INTO teams(id, key, name, description, private, url, issue_sequence, created_at, updated_at)
2071
- VALUES (?,?,?,?,?,?,0,?,?)`).run(id2, teamCfg.key, teamCfg.name, teamCfg.description ?? null, teamCfg.private ? 1 : 0, `https://linear.app/${urlKey}/team/${teamCfg.key}`, now, now);
1451
+ VALUES (?,?,?,?,?,?,0,?,?)`).run(id, teamCfg.key, teamCfg.name, teamCfg.description ?? null, teamCfg.private ? 1 : 0, `https://linear.app/${urlKey}/team/${teamCfg.key}`, now, now);
2072
1452
  const states = teamCfg.states && teamCfg.states.length > 0 ? teamCfg.states : [
2073
1453
  { name: "Backlog", type: "backlog", position: 0 },
2074
1454
  { name: "Todo", type: "unstarted", position: 1 },
@@ -2078,7 +1458,7 @@ function applySeed(domain, seed) {
2078
1458
  ];
2079
1459
  for (const [index, state] of states.entries()) {
2080
1460
  domain.db.prepare(`INSERT INTO workflow_states(id, team_id, name, type, position, created_at, updated_at)
2081
- VALUES (?,?,?,?,?,?,?)`).run(state.id ?? domain.nextId("state"), id2, state.name, state.type ?? inferStateType(state.name), state.position ?? index, now, now);
1461
+ VALUES (?,?,?,?,?,?,?)`).run(state.id ?? domain.nextId("state"), id, state.name, state.type ?? inferStateType(state.name), state.position ?? index, now, now);
2082
1462
  }
2083
1463
  }
2084
1464
  for (const label of seed.labels) {
@@ -2103,7 +1483,7 @@ function applySeed(domain, seed) {
2103
1483
  if (!state)
2104
1484
  badUserInput(`No workflow state for team ${team.key}`);
2105
1485
  const number = nextIssueNumber(domain, team.id);
2106
- const id2 = issueCfg.id ?? domain.nextId("issue");
1486
+ const id = issueCfg.id ?? domain.nextId("issue");
2107
1487
  const identifier = `${team.key}-${number}`;
2108
1488
  const createdAt = issueCfg.createdAt ?? now;
2109
1489
  const updatedAt = issueCfg.updatedAt ?? createdAt;
@@ -2119,13 +1499,13 @@ function applySeed(domain, seed) {
2119
1499
  assignee_id, creator_id, delegate_id, project_id, cycle_id, parent_id, url,
2120
1500
  archived_at, canceled_at, completed_at, started_at, due_date,
2121
1501
  create_as_user, display_icon_url, created_at, updated_at
2122
- ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(id2, identifier, number, team.id, issueCfg.title, issueCfg.description ?? null, issueCfg.priority, issueCfg.estimate ?? null, state.id, assignee?.id ?? null, creator.id, delegate?.id ?? null, project?.id ?? null, cycle?.id ?? null, parentId, `${baseUrl}/issue/${identifier}`, null, state.type === "canceled" ? updatedAt : null, state.type === "completed" ? updatedAt : null, state.type === "started" ? updatedAt : null, issueCfg.dueDate ?? null, null, null, createdAt, updatedAt);
1502
+ ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(id, identifier, number, team.id, issueCfg.title, issueCfg.description ?? null, issueCfg.priority, issueCfg.estimate ?? null, state.id, assignee?.id ?? null, creator.id, delegate?.id ?? null, project?.id ?? null, cycle?.id ?? null, parentId, `${baseUrl}/issue/${identifier}`, null, state.type === "canceled" ? updatedAt : null, state.type === "completed" ? updatedAt : null, state.type === "started" ? updatedAt : null, issueCfg.dueDate ?? null, null, null, createdAt, updatedAt);
2123
1503
  const labelIds = issueCfg.labels.map((ref) => domain.requireLabel(ref, team.id).id);
2124
- setIssueLabels(domain, id2, labelIds);
2125
- issueIdByTitle.set(issueCfg.title, id2);
1504
+ setIssueLabels(domain, id, labelIds);
1505
+ issueIdByTitle.set(issueCfg.title, id);
2126
1506
  if (issueCfg.id)
2127
- issueIdByTitle.set(issueCfg.id, id2);
2128
- issueIdByTitle.set(identifier, id2);
1507
+ issueIdByTitle.set(issueCfg.id, id);
1508
+ issueIdByTitle.set(identifier, id);
2129
1509
  }
2130
1510
  const commentIdByRef = /* @__PURE__ */ new Map();
2131
1511
  for (const comment of seed.comments) {
@@ -2151,12 +1531,12 @@ function applySeed(domain, seed) {
2151
1531
  const creator = doc.creator ? domain.requireUser(doc.creator) : domain.resolveViewer({});
2152
1532
  const createdAt = doc.createdAt ?? now;
2153
1533
  const updatedAt = doc.updatedAt ?? createdAt;
2154
- const id2 = doc.id ?? domain.nextId("document");
2155
- const slug = doc.slug ?? `${slugify(doc.title) || "document"}-${id2.slice(-6)}`;
1534
+ const id = doc.id ?? domain.nextId("document");
1535
+ const slug = doc.slug ?? `${slugify(doc.title) || "document"}-${id.slice(-6)}`;
2156
1536
  domain.db.prepare(`INSERT INTO documents(
2157
1537
  id, title, content, slug, project_id, team_id, issue_id, cycle_id,
2158
1538
  icon, color, creator_id, created_at, updated_at
2159
- ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(id2, doc.title, doc.content ?? null, slug, project?.id ?? null, team?.id ?? null, issue?.id ?? null, cycle?.id ?? null, doc.icon ?? null, doc.color ?? null, creator.id, createdAt, updatedAt);
1539
+ ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(id, doc.title, doc.content ?? null, slug, project?.id ?? null, team?.id ?? null, issue?.id ?? null, cycle?.id ?? null, doc.icon ?? null, doc.color ?? null, creator.id, createdAt, updatedAt);
2160
1540
  }
2161
1541
  for (const app of seed.oauthApps) {
2162
1542
  domain.db.prepare(`INSERT INTO oauth_apps(
@@ -2372,10 +1752,10 @@ async function createAgentSessionOnIssue(domain, input, actor = {}) {
2372
1752
  const viewer = domain.resolveViewer(actor);
2373
1753
  const agent = (input.appUserId ? domain.requireUser(input.appUserId) : null) ?? domain.listUsers().find((u) => u.app) ?? viewer;
2374
1754
  const now = domain.tick();
2375
- const id2 = domain.nextId("agent_session");
1755
+ const id = domain.nextId("agent_session");
2376
1756
  domain.db.prepare(`INSERT INTO agent_sessions(id, issue_id, comment_id, app_user_id, status, plan, external_urls_json, created_at, updated_at)
2377
- VALUES (?,?,?,?,?,?,?,?,?)`).run(id2, issue.id, null, agent.id, "pending", input.plan ?? null, JSON.stringify(input.externalUrls ?? []), now, now);
2378
- const session = domain.requireAgentSession(id2);
1757
+ VALUES (?,?,?,?,?,?,?,?,?)`).run(id, issue.id, null, agent.id, "pending", input.plan ?? null, JSON.stringify(input.externalUrls ?? []), now, now);
1758
+ const session = domain.requireAgentSession(id);
2379
1759
  await emitWebhook(domain, {
2380
1760
  type: "AgentSessionEvent",
2381
1761
  action: "created",
@@ -2392,14 +1772,14 @@ async function createAgentSessionOnComment(domain, input, actor = {}) {
2392
1772
  const viewer = domain.resolveViewer(actor);
2393
1773
  const agent = (input.appUserId ? domain.requireUser(input.appUserId) : null) ?? domain.listUsers().find((u) => u.app) ?? viewer;
2394
1774
  const now = domain.tick();
2395
- const id2 = domain.nextId("agent_session");
1775
+ const id = domain.nextId("agent_session");
2396
1776
  domain.db.prepare(`INSERT INTO agent_sessions(id, issue_id, comment_id, app_user_id, status, plan, external_urls_json, created_at, updated_at)
2397
- VALUES (?,?,?,?,?,?,?,?,?)`).run(id2, issue.id, comment.id, agent.id, "pending", input.plan ?? null, JSON.stringify(input.externalUrls ?? []), now, now);
2398
- return domain.requireAgentSession(id2);
1777
+ VALUES (?,?,?,?,?,?,?,?,?)`).run(id, issue.id, comment.id, agent.id, "pending", input.plan ?? null, JSON.stringify(input.externalUrls ?? []), now, now);
1778
+ return domain.requireAgentSession(id);
2399
1779
  }
2400
- function updateAgentSession(domain, id2, input, actor = {}) {
1780
+ function updateAgentSession(domain, id, input, actor = {}) {
2401
1781
  domain.requireScopes(actor, ["write"]);
2402
- const session = domain.requireAgentSession(id2);
1782
+ const session = domain.requireAgentSession(id);
2403
1783
  const now = domain.tick();
2404
1784
  domain.db.prepare(`UPDATE agent_sessions SET
2405
1785
  status = COALESCE(?, status),
@@ -2416,10 +1796,10 @@ async function createAgentActivity(domain, input, actor = {}) {
2416
1796
  const viewer = domain.resolveViewer(actor);
2417
1797
  const type = normalizeActivityType(input.type);
2418
1798
  const now = domain.tick();
2419
- const id2 = domain.nextId("agent_activity");
1799
+ const id = domain.nextId("agent_activity");
2420
1800
  const ephemeral = typeof input.ephemeral === "boolean" ? input.ephemeral : type === "thought" || type === "action";
2421
1801
  domain.db.prepare(`INSERT INTO agent_activities(id, session_id, user_id, type, body, ephemeral, created_at, updated_at)
2422
- VALUES (?,?,?,?,?,?,?,?)`).run(id2, session.id, viewer.id, type, input.body, ephemeral ? 1 : 0, now, now);
1802
+ VALUES (?,?,?,?,?,?,?,?)`).run(id, session.id, viewer.id, type, input.body, ephemeral ? 1 : 0, now, now);
2423
1803
  if (type === "prompt") {
2424
1804
  await emitWebhook(domain, {
2425
1805
  type: "AgentSessionEvent",
@@ -2429,9 +1809,9 @@ async function createAgentActivity(domain, input, actor = {}) {
2429
1809
  teamId: session.issueId ? domain.requireIssue(session.issueId).teamId : null
2430
1810
  });
2431
1811
  }
2432
- const activity = domain.getAgentActivity(id2);
1812
+ const activity = domain.getAgentActivity(id);
2433
1813
  if (!activity)
2434
- notFound(`Agent activity not found: ${id2}`);
1814
+ notFound(`Agent activity not found: ${id}`);
2435
1815
  return activity;
2436
1816
  }
2437
1817
  function getAgentActivity(domain, ref) {
@@ -2500,10 +1880,10 @@ async function createComment(domain, input, actor = {}) {
2500
1880
  }
2501
1881
  const viewer = domain.resolveViewer(actor);
2502
1882
  const now = domain.tick();
2503
- const id2 = domain.nextId("comment");
1883
+ const id = domain.nextId("comment");
2504
1884
  domain.db.prepare(`INSERT INTO comments(id, issue_id, parent_id, user_id, body, create_as_user, display_icon_url, created_at, updated_at)
2505
- VALUES (?,?,?,?,?,?,?,?,?)`).run(id2, issue.id, parent?.id ?? null, viewer.id, input.body, input.createAsUser ?? null, input.displayIconUrl ?? null, now, now);
2506
- const comment = domain.getComment(id2);
1885
+ VALUES (?,?,?,?,?,?,?,?,?)`).run(id, issue.id, parent?.id ?? null, viewer.id, input.body, input.createAsUser ?? null, input.displayIconUrl ?? null, now, now);
1886
+ const comment = domain.getComment(id);
2507
1887
  await emitWebhook(domain, {
2508
1888
  type: "Comment",
2509
1889
  action: "create",
@@ -2520,10 +1900,10 @@ async function createComment(domain, input, actor = {}) {
2520
1900
  }
2521
1901
  return comment;
2522
1902
  }
2523
- async function updateComment(domain, id2, body, actor = {}) {
1903
+ async function updateComment(domain, id, body, actor = {}) {
2524
1904
  domain.requireScopes(actor, ["write"]);
2525
1905
  assertBody(body);
2526
- const comment = domain.requireComment(id2);
1906
+ const comment = domain.requireComment(id);
2527
1907
  const before = commentWebhookPayload(comment);
2528
1908
  const now = domain.tick();
2529
1909
  domain.db.prepare("UPDATE comments SET body = ?, updated_at = ? WHERE id = ?").run(body, now, comment.id);
@@ -2540,9 +1920,9 @@ async function updateComment(domain, id2, body, actor = {}) {
2540
1920
  });
2541
1921
  return updated;
2542
1922
  }
2543
- async function deleteComment(domain, id2, actor = {}) {
1923
+ async function deleteComment(domain, id, actor = {}) {
2544
1924
  domain.requireScopes(actor, ["write"]);
2545
- const comment = domain.requireComment(id2);
1925
+ const comment = domain.requireComment(id);
2546
1926
  const issue = domain.requireIssue(comment.issueId);
2547
1927
  const treeIds = domain.db.prepare(`WITH RECURSIVE tree(id) AS (
2548
1928
  SELECT id FROM comments WHERE id = ?
@@ -2611,17 +1991,17 @@ function createDocument(domain, input, actor = {}) {
2611
1991
  }
2612
1992
  const viewer = domain.resolveViewer(actor);
2613
1993
  const now = domain.tick();
2614
- const id2 = domain.nextId("document");
2615
- const slug = uniqueSlug(domain, input.title, id2);
1994
+ const id = domain.nextId("document");
1995
+ const slug = uniqueSlug(domain, input.title, id);
2616
1996
  domain.db.prepare(`INSERT INTO documents(
2617
1997
  id, title, content, slug, project_id, team_id, issue_id, cycle_id,
2618
1998
  icon, color, creator_id, created_at, updated_at
2619
- ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(id2, input.title, input.content ?? null, slug, parents.projectId, parents.teamId, parents.issueId, parents.cycleId, input.icon ?? null, input.color ?? null, viewer.id, now, now);
2620
- return domain.requireDocument(id2);
1999
+ ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(id, input.title, input.content ?? null, slug, parents.projectId, parents.teamId, parents.issueId, parents.cycleId, input.icon ?? null, input.color ?? null, viewer.id, now, now);
2000
+ return domain.requireDocument(id);
2621
2001
  }
2622
- function updateDocument(domain, id2, input, actor = {}) {
2002
+ function updateDocument(domain, id, input, actor = {}) {
2623
2003
  domain.requireScopes(actor, ["write"]);
2624
- const doc = domain.requireDocument(id2);
2004
+ const doc = domain.requireDocument(id);
2625
2005
  if (input.title !== void 0)
2626
2006
  assertTitle(input.title);
2627
2007
  if (input.content != null)
@@ -2660,13 +2040,13 @@ function resolveParents(domain, input) {
2660
2040
  const count = [projectId, teamId, issueId, cycleId].filter(Boolean).length;
2661
2041
  return { count, projectId, teamId, issueId, cycleId };
2662
2042
  }
2663
- function uniqueSlug(domain, title, id2) {
2043
+ function uniqueSlug(domain, title, id) {
2664
2044
  const base = slugify(title) || "document";
2665
- const candidate = `${base}-${id2.slice(-6)}`;
2045
+ const candidate = `${base}-${id.slice(-6)}`;
2666
2046
  const existing = domain.db.prepare("SELECT 1 FROM documents WHERE slug = ?").get(candidate);
2667
2047
  if (!existing)
2668
2048
  return candidate;
2669
- return `${base}-${id2}`;
2049
+ return `${base}-${id}`;
2670
2050
  }
2671
2051
 
2672
2052
  // ../packages/twin-linear/dist/src/domain/labels.js
@@ -2687,10 +2067,10 @@ async function createLabel(domain, input, actor = {}) {
2687
2067
  domain.requireScopes(actor, ["write"]);
2688
2068
  const team = input.teamId ? domain.requireTeam(input.teamId) : null;
2689
2069
  const now = domain.tick();
2690
- const id2 = domain.nextId("label");
2070
+ const id = domain.nextId("label");
2691
2071
  domain.db.prepare(`INSERT INTO issue_labels(id, team_id, name, color, description, created_at, updated_at)
2692
- VALUES (?,?,?,?,?,?,?)`).run(id2, team?.id ?? null, input.name, input.color ?? "#64748b", input.description ?? null, now, now);
2693
- const label = domain.getLabel(id2);
2072
+ VALUES (?,?,?,?,?,?,?)`).run(id, team?.id ?? null, input.name, input.color ?? "#64748b", input.description ?? null, now, now);
2073
+ const label = domain.getLabel(id);
2694
2074
  await emitWebhook(domain, {
2695
2075
  type: "IssueLabel",
2696
2076
  action: "create",
@@ -2700,9 +2080,9 @@ async function createLabel(domain, input, actor = {}) {
2700
2080
  });
2701
2081
  return label;
2702
2082
  }
2703
- async function updateLabel(domain, id2, input, actor = {}) {
2083
+ async function updateLabel(domain, id, input, actor = {}) {
2704
2084
  domain.requireScopes(actor, ["write"]);
2705
- const label = domain.requireLabel(id2);
2085
+ const label = domain.requireLabel(id);
2706
2086
  const now = domain.tick();
2707
2087
  domain.db.prepare(`UPDATE issue_labels SET
2708
2088
  name = COALESCE(?, name),
@@ -2720,9 +2100,9 @@ async function updateLabel(domain, id2, input, actor = {}) {
2720
2100
  });
2721
2101
  return updated;
2722
2102
  }
2723
- async function deleteLabel(domain, id2, actor = {}) {
2103
+ async function deleteLabel(domain, id, actor = {}) {
2724
2104
  domain.requireScopes(actor, ["write"]);
2725
- const label = domain.requireLabel(id2);
2105
+ const label = domain.requireLabel(id);
2726
2106
  domain.db.prepare("DELETE FROM issue_label_links WHERE label_id = ?").run(label.id);
2727
2107
  domain.db.prepare("DELETE FROM issue_labels WHERE id = ?").run(label.id);
2728
2108
  await emitWebhook(domain, {
@@ -2872,15 +2252,15 @@ function createWebhook(domain, input, actor = {}) {
2872
2252
  const team = input.teamId ? domain.requireTeam(input.teamId) : null;
2873
2253
  const viewer = domain.resolveViewer(actor);
2874
2254
  const now = domain.tick();
2875
- const id2 = domain.nextId("webhook");
2255
+ const id = domain.nextId("webhook");
2876
2256
  domain.db.prepare(`INSERT INTO webhooks(
2877
2257
  id, label, url, enabled, resource_types_json, team_id, all_public_teams, secret, creator_id, created_at, updated_at
2878
- ) VALUES (?,?,?,?,?,?,?,?,?,?,?)`).run(id2, input.label ?? "Local webhook", url, input.enabled === false ? 0 : 1, JSON.stringify(input.resourceTypes ?? ["Issue", "Comment"]), team?.id ?? null, input.allPublicTeams ?? !team ? 1 : 0, input.secret ?? null, viewer.id, now, now);
2879
- return domain.getWebhook(id2);
2258
+ ) VALUES (?,?,?,?,?,?,?,?,?,?,?)`).run(id, input.label ?? "Local webhook", url, input.enabled === false ? 0 : 1, JSON.stringify(input.resourceTypes ?? ["Issue", "Comment"]), team?.id ?? null, input.allPublicTeams ?? !team ? 1 : 0, input.secret ?? null, viewer.id, now, now);
2259
+ return domain.getWebhook(id);
2880
2260
  }
2881
- function deleteWebhook(domain, id2, actor = {}) {
2261
+ function deleteWebhook(domain, id, actor = {}) {
2882
2262
  domain.requireScopes(actor, ["write"]);
2883
- const webhook = domain.requireWebhook(id2);
2263
+ const webhook = domain.requireWebhook(id);
2884
2264
  domain.db.prepare("DELETE FROM webhook_deliveries WHERE webhook_id = ?").run(webhook.id);
2885
2265
  domain.db.prepare("DELETE FROM webhooks WHERE id = ?").run(webhook.id);
2886
2266
  return webhook.id;
@@ -2909,14 +2289,14 @@ function createProject(domain, input, actor = {}) {
2909
2289
  domain.requireScopes(actor, ["write"]);
2910
2290
  const team = input.teamId ? domain.requireTeam(input.teamId) : null;
2911
2291
  const now = domain.tick();
2912
- const id2 = domain.nextId("project");
2292
+ const id = domain.nextId("project");
2913
2293
  domain.db.prepare(`INSERT INTO projects(id, team_id, name, description, state, created_at, updated_at)
2914
- VALUES (?,?,?,?,?,?,?)`).run(id2, team?.id ?? null, input.name, input.description ?? null, input.state ?? "planned", now, now);
2915
- return domain.requireProject(id2);
2294
+ VALUES (?,?,?,?,?,?,?)`).run(id, team?.id ?? null, input.name, input.description ?? null, input.state ?? "planned", now, now);
2295
+ return domain.requireProject(id);
2916
2296
  }
2917
- function updateProject(domain, id2, input, actor = {}) {
2297
+ function updateProject(domain, id, input, actor = {}) {
2918
2298
  domain.requireScopes(actor, ["write"]);
2919
- const project = domain.requireProject(id2);
2299
+ const project = domain.requireProject(id);
2920
2300
  const now = domain.tick();
2921
2301
  domain.db.prepare(`UPDATE projects SET
2922
2302
  name = COALESCE(?, name),
@@ -3032,17 +2412,17 @@ var LinearDomain = class {
3032
2412
  createIssue(input, actor = {}) {
3033
2413
  return createIssue(this, input, actor);
3034
2414
  }
3035
- updateIssue(id2, input, actor = {}) {
3036
- return updateIssue(this, id2, input, actor);
2415
+ updateIssue(id, input, actor = {}) {
2416
+ return updateIssue(this, id, input, actor);
3037
2417
  }
3038
- deleteIssue(id2, actor = {}) {
3039
- return deleteIssue(this, id2, actor);
2418
+ deleteIssue(id, actor = {}) {
2419
+ return deleteIssue(this, id, actor);
3040
2420
  }
3041
- archiveIssue(id2, actor = {}) {
3042
- return archiveIssue(this, id2, actor);
2421
+ archiveIssue(id, actor = {}) {
2422
+ return archiveIssue(this, id, actor);
3043
2423
  }
3044
- unarchiveIssue(id2, actor = {}) {
3045
- return unarchiveIssue(this, id2, actor);
2424
+ unarchiveIssue(id, actor = {}) {
2425
+ return unarchiveIssue(this, id, actor);
3046
2426
  }
3047
2427
  listComments(issueRef) {
3048
2428
  return listComments(this, issueRef);
@@ -3053,11 +2433,11 @@ var LinearDomain = class {
3053
2433
  createComment(input, actor = {}) {
3054
2434
  return createComment(this, input, actor);
3055
2435
  }
3056
- updateComment(id2, body, actor = {}) {
3057
- return updateComment(this, id2, body, actor);
2436
+ updateComment(id, body, actor = {}) {
2437
+ return updateComment(this, id, body, actor);
3058
2438
  }
3059
- deleteComment(id2, actor = {}) {
3060
- return deleteComment(this, id2, actor);
2439
+ deleteComment(id, actor = {}) {
2440
+ return deleteComment(this, id, actor);
3061
2441
  }
3062
2442
  listIssueRelations(issueId) {
3063
2443
  return listIssueRelations(this, issueId);
@@ -3071,8 +2451,8 @@ var LinearDomain = class {
3071
2451
  createDocument(input, actor = {}) {
3072
2452
  return createDocument(this, input, actor);
3073
2453
  }
3074
- updateDocument(id2, input, actor = {}) {
3075
- return updateDocument(this, id2, input, actor);
2454
+ updateDocument(id, input, actor = {}) {
2455
+ return updateDocument(this, id, input, actor);
3076
2456
  }
3077
2457
  requireDocument(ref) {
3078
2458
  return this.getDocument(ref) ?? notFound(`Document not found: ${ref}`);
@@ -3086,11 +2466,11 @@ var LinearDomain = class {
3086
2466
  createLabel(input, actor = {}) {
3087
2467
  return createLabel(this, input, actor);
3088
2468
  }
3089
- updateLabel(id2, input, actor = {}) {
3090
- return updateLabel(this, id2, input, actor);
2469
+ updateLabel(id, input, actor = {}) {
2470
+ return updateLabel(this, id, input, actor);
3091
2471
  }
3092
- deleteLabel(id2, actor = {}) {
3093
- return deleteLabel(this, id2, actor);
2472
+ deleteLabel(id, actor = {}) {
2473
+ return deleteLabel(this, id, actor);
3094
2474
  }
3095
2475
  addIssueLabel(issueId, labelId, actor = {}) {
3096
2476
  return addIssueLabel(this, issueId, labelId, actor);
@@ -3107,8 +2487,8 @@ var LinearDomain = class {
3107
2487
  createProject(input, actor = {}) {
3108
2488
  return createProject(this, input, actor);
3109
2489
  }
3110
- updateProject(id2, input, actor = {}) {
3111
- return updateProject(this, id2, input, actor);
2490
+ updateProject(id, input, actor = {}) {
2491
+ return updateProject(this, id, input, actor);
3112
2492
  }
3113
2493
  listCycles(teamRef) {
3114
2494
  return listCycles(this, teamRef);
@@ -3125,8 +2505,8 @@ var LinearDomain = class {
3125
2505
  createWebhook(input, actor = {}) {
3126
2506
  return createWebhook(this, input, actor);
3127
2507
  }
3128
- deleteWebhook(id2, actor = {}) {
3129
- return deleteWebhook(this, id2, actor);
2508
+ deleteWebhook(id, actor = {}) {
2509
+ return deleteWebhook(this, id, actor);
3130
2510
  }
3131
2511
  recordWebhookDelivery(input) {
3132
2512
  recordWebhookDelivery(this, input);
@@ -3143,8 +2523,8 @@ var LinearDomain = class {
3143
2523
  createAgentSessionOnComment(input, actor = {}) {
3144
2524
  return createAgentSessionOnComment(this, input, actor);
3145
2525
  }
3146
- updateAgentSession(id2, input, actor = {}) {
3147
- return updateAgentSession(this, id2, input, actor);
2526
+ updateAgentSession(id, input, actor = {}) {
2527
+ return updateAgentSession(this, id, input, actor);
3148
2528
  }
3149
2529
  createAgentActivity(input, actor = {}) {
3150
2530
  return createAgentActivity(this, input, actor);
@@ -3913,8 +3293,8 @@ var implementations = {
3913
3293
  schema: deleteCommentSchema,
3914
3294
  mutation: true,
3915
3295
  handler: (domain, args, ctx) => mutate(domain, ctx, async () => {
3916
- const id2 = await domain.deleteComment(String(args.id), actorFromToolContext(ctx));
3917
- return { success: true, id: id2 };
3296
+ const id = await domain.deleteComment(String(args.id), actorFromToolContext(ctx));
3297
+ return { success: true, id };
3918
3298
  })
3919
3299
  },
3920
3300
  list_teams: {
@@ -4319,7 +3699,7 @@ function registerOAuthRoutes(app, commands) {
4319
3699
  const state = bodyStr(body.state);
4320
3700
  const scopes = normalizeScopes2(bodyStr(body.scope), ["read"]);
4321
3701
  const requestedActor = normalizeActor(bodyStr(body.actor));
4322
- const userRef2 = bodyStr(body.user_ref);
3702
+ const userRef = bodyStr(body.user_ref);
4323
3703
  const codeChallenge = bodyStr(body.code_challenge);
4324
3704
  const codeChallengeMethod = bodyStr(body.code_challenge_method);
4325
3705
  const oauthApp = requireRegisteredOAuthApp(commands, clientId);
@@ -4341,7 +3721,7 @@ function registerOAuthRoutes(app, commands) {
4341
3721
  if (invalidScopes.length > 0) {
4342
3722
  return c.html(errorPage("Invalid scope", `The app is not registered for scopes: ${invalidScopes.join(", ")}.`), 400);
4343
3723
  }
4344
- const user = userRef2 ? commands.getUser(userRef2) : commands.listUsers().find((u) => !u.app);
3724
+ const user = userRef ? commands.getUser(userRef) : commands.listUsers().find((u) => !u.app);
4345
3725
  const appUser = actor === "app" ? oauthApp.appUserId ? commands.getUser(oauthApp.appUserId) : user : user;
4346
3726
  if (!appUser) {
4347
3727
  return c.html(errorPage("No Linear actor", "No matching user or app actor is available."), 400);
@@ -4783,7 +4163,7 @@ function createFormatters(commands, actor) {
4783
4163
  assignee: () => issue.assigneeId ? formatUser(commands.requireUser(issue.assigneeId)) : null,
4784
4164
  creator: () => issue.creatorId ? formatUser(commands.requireUser(issue.creatorId)) : null,
4785
4165
  delegate: () => issue.delegateId ? formatUser(commands.requireUser(issue.delegateId)) : null,
4786
- labels: (args) => connect(issue.labelIds.map((id2) => commands.requireLabel(id2)), args, formatLabel, `issue:${issue.id}:labels`),
4166
+ labels: (args) => connect(issue.labelIds.map((id) => commands.requireLabel(id)), args, formatLabel, `issue:${issue.id}:labels`),
4787
4167
  comments: (args) => connect(commands.listComments(issue.id), args, formatComment, `issue:${issue.id}:comments`),
4788
4168
  parent: () => issue.parentId ? formatIssue(commands.requireIssue(issue.parentId)) : null,
4789
4169
  project: () => issue.projectId ? formatProject(commands.requireProject(issue.projectId)) : null,
@@ -4882,7 +4262,7 @@ function issueMatches(commands, issue, filter) {
4882
4262
  }
4883
4263
  }
4884
4264
  if (filter.labels) {
4885
- const labels = issue.labelIds.map((id2) => commands.getLabel(id2)).filter(Boolean);
4265
+ const labels = issue.labelIds.map((id) => commands.getLabel(id)).filter(Boolean);
4886
4266
  const anyMatch = labels.some((label) => matchStringComparator(label.name, filter.labels) || matchStringComparator(label.id, filter.labels));
4887
4267
  if (!anyMatch)
4888
4268
  return false;
@@ -5180,122 +4560,122 @@ function createRootValue(ctx) {
5180
4560
  };
5181
4561
  },
5182
4562
  users: (args) => connect(filterUsers(commands.listUsers(), args.filter), args, formatUser, "users"),
5183
- user: ({ id: id2 }) => {
5184
- const user = commands.getUser(id2);
4563
+ user: ({ id }) => {
4564
+ const user = commands.getUser(id);
5185
4565
  return user ? formatUser(user) : null;
5186
4566
  },
5187
4567
  teams: (args) => connect(commands.listTeams(), args, formatTeam, "teams"),
5188
- team: ({ id: id2 }) => {
5189
- const team = commands.getTeam(id2);
4568
+ team: ({ id }) => {
4569
+ const team = commands.getTeam(id);
5190
4570
  return team ? formatTeam(team) : null;
5191
4571
  },
5192
4572
  workflowStates: (args) => connect(commands.listWorkflowStates(), args, formatState, "workflowStates"),
5193
- workflowState: ({ id: id2 }) => {
5194
- const state = commands.getWorkflowState(id2);
4573
+ workflowState: ({ id }) => {
4574
+ const state = commands.getWorkflowState(id);
5195
4575
  return state ? formatState(state) : null;
5196
4576
  },
5197
4577
  issues: (args) => connect(filterIssues(commands, args.filter), args, formatIssue, "issues"),
5198
- issue: ({ id: id2 }) => {
5199
- const issue = commands.getIssue(id2);
4578
+ issue: ({ id }) => {
4579
+ const issue = commands.getIssue(id);
5200
4580
  return issue ? formatIssue(issue) : null;
5201
4581
  },
5202
4582
  comments: (args) => connect(commands.listComments(), args, formatComment, "comments"),
5203
- comment: ({ id: id2 }) => {
5204
- const comment = commands.getComment(id2);
4583
+ comment: ({ id }) => {
4584
+ const comment = commands.getComment(id);
5205
4585
  return comment ? formatComment(comment) : null;
5206
4586
  },
5207
4587
  issueLabels: (args) => connect(commands.listLabels(), args, formatLabel, "issueLabels"),
5208
- issueLabel: ({ id: id2 }) => {
5209
- const label = commands.getLabel(id2);
4588
+ issueLabel: ({ id }) => {
4589
+ const label = commands.getLabel(id);
5210
4590
  return label ? formatLabel(label) : null;
5211
4591
  },
5212
4592
  projects: (args) => connect(commands.listProjects(), args, formatProject, "projects"),
5213
- project: ({ id: id2 }) => {
5214
- const project = commands.getProject(id2);
4593
+ project: ({ id }) => {
4594
+ const project = commands.getProject(id);
5215
4595
  return project ? formatProject(project) : null;
5216
4596
  },
5217
4597
  cycles: (args) => {
5218
4598
  const all = commands.listTeams().flatMap((team) => commands.listCycles(team.id));
5219
4599
  return connect(all, args, formatCycle, "cycles");
5220
4600
  },
5221
- cycle: ({ id: id2 }) => {
5222
- const cycle = commands.getCycle(id2);
4601
+ cycle: ({ id }) => {
4602
+ const cycle = commands.getCycle(id);
5223
4603
  return cycle ? formatCycle(cycle) : null;
5224
4604
  },
5225
4605
  webhooks: (args) => connect(commands.listWebhooks(), args, formatWebhook, "webhooks"),
5226
- webhook: ({ id: id2 }) => {
5227
- const webhook = commands.getWebhook(id2);
4606
+ webhook: ({ id }) => {
4607
+ const webhook = commands.getWebhook(id);
5228
4608
  return webhook ? formatWebhook(webhook) : null;
5229
4609
  },
5230
4610
  agentSessions: (args) => connect(commands.listAgentSessions(), args, formatAgentSession, "agentSessions"),
5231
- agentSession: ({ id: id2 }) => {
5232
- const session = commands.getAgentSession(id2);
4611
+ agentSession: ({ id }) => {
4612
+ const session = commands.getAgentSession(id);
5233
4613
  return session ? formatAgentSession(session) : null;
5234
4614
  },
5235
4615
  issueCreate: async ({ input }) => {
5236
4616
  const issue = await commands.createIssue(parseIssueCreateInput(input), actor);
5237
4617
  return payload({ issue: formatIssue(issue) });
5238
4618
  },
5239
- issueUpdate: async ({ id: id2, input }) => {
4619
+ issueUpdate: async ({ id, input }) => {
5240
4620
  const parsed = parseIssueUpdateInput(input);
5241
- const issue = await commands.updateIssue(String(id2 ?? parsed.id), parsed.patch, actor);
4621
+ const issue = await commands.updateIssue(String(id ?? parsed.id), parsed.patch, actor);
5242
4622
  return payload({ issue: formatIssue(issue) });
5243
4623
  },
5244
- issueDelete: async ({ id: id2 }) => {
5245
- const issue = await commands.deleteIssue(id2, actor);
4624
+ issueDelete: async ({ id }) => {
4625
+ const issue = await commands.deleteIssue(id, actor);
5246
4626
  return payload({ entity: formatIssue(issue) });
5247
4627
  },
5248
- issueArchive: async ({ id: id2 }) => {
5249
- const issue = await commands.archiveIssue(id2, actor);
4628
+ issueArchive: async ({ id }) => {
4629
+ const issue = await commands.archiveIssue(id, actor);
5250
4630
  return payload({ entity: formatIssue(issue) });
5251
4631
  },
5252
- issueUnarchive: async ({ id: id2 }) => {
5253
- const issue = await commands.unarchiveIssue(id2, actor);
4632
+ issueUnarchive: async ({ id }) => {
4633
+ const issue = await commands.unarchiveIssue(id, actor);
5254
4634
  return payload({ entity: formatIssue(issue) });
5255
4635
  },
5256
4636
  commentCreate: async ({ input }) => {
5257
4637
  const comment = await commands.createComment(parseCommentCreateInput(input), actor);
5258
4638
  return payload({ comment: formatComment(comment) });
5259
4639
  },
5260
- commentUpdate: async ({ id: id2, input }) => {
4640
+ commentUpdate: async ({ id, input }) => {
5261
4641
  const parsed = parseCommentUpdateInput(input);
5262
- const comment = await commands.updateComment(String(id2 ?? parsed.id), parsed.body, actor);
4642
+ const comment = await commands.updateComment(String(id ?? parsed.id), parsed.body, actor);
5263
4643
  return payload({ comment: formatComment(comment) });
5264
4644
  },
5265
- commentDelete: async ({ id: id2 }) => {
5266
- const entityId = await commands.deleteComment(id2, actor);
4645
+ commentDelete: async ({ id }) => {
4646
+ const entityId = await commands.deleteComment(id, actor);
5267
4647
  return payload({ entityId });
5268
4648
  },
5269
4649
  issueLabelCreate: async ({ input }) => {
5270
4650
  const label = await commands.createLabel(parseIssueLabelCreateInput(input), actor);
5271
4651
  return payload({ issueLabel: formatLabel(label) });
5272
4652
  },
5273
- issueLabelUpdate: async ({ id: id2, input }) => {
4653
+ issueLabelUpdate: async ({ id, input }) => {
5274
4654
  const parsed = parseIssueLabelUpdateInput(input);
5275
- const label = await commands.updateLabel(String(id2 ?? parsed.id), {
4655
+ const label = await commands.updateLabel(String(id ?? parsed.id), {
5276
4656
  name: parsed.name,
5277
4657
  color: parsed.color,
5278
4658
  description: parsed.description
5279
4659
  }, actor);
5280
4660
  return payload({ issueLabel: formatLabel(label) });
5281
4661
  },
5282
- issueLabelDelete: async ({ id: id2 }) => {
5283
- const entityId = await commands.deleteLabel(id2, actor);
4662
+ issueLabelDelete: async ({ id }) => {
4663
+ const entityId = await commands.deleteLabel(id, actor);
5284
4664
  return payload({ entityId });
5285
4665
  },
5286
- issueAddLabel: async ({ id: id2, labelId }) => {
5287
- const issue = await commands.addIssueLabel(id2, labelId, actor);
4666
+ issueAddLabel: async ({ id, labelId }) => {
4667
+ const issue = await commands.addIssueLabel(id, labelId, actor);
5288
4668
  return payload({ issue: formatIssue(issue) });
5289
4669
  },
5290
- issueRemoveLabel: async ({ id: id2, labelId }) => {
5291
- const issue = await commands.removeIssueLabel(id2, labelId, actor);
4670
+ issueRemoveLabel: async ({ id, labelId }) => {
4671
+ const issue = await commands.removeIssueLabel(id, labelId, actor);
5292
4672
  return payload({ issue: formatIssue(issue) });
5293
4673
  },
5294
4674
  webhookCreate: ({ input }) => {
5295
4675
  const webhook = commands.createWebhook(parseWebhookCreateInput(input), actor);
5296
4676
  return payload({ webhook: formatWebhook(webhook) });
5297
4677
  },
5298
- webhookDelete: ({ id: id2 }) => payload({ entityId: commands.deleteWebhook(id2, actor) }),
4678
+ webhookDelete: ({ id }) => payload({ entityId: commands.deleteWebhook(id, actor) }),
5299
4679
  agentSessionCreateOnIssue: async ({ input }) => {
5300
4680
  const session = await commands.createAgentSessionOnIssue(parseAgentSessionOnIssueInput(input), actor);
5301
4681
  return payload({ agentSession: formatAgentSession(session) });
@@ -5304,9 +4684,9 @@ function createRootValue(ctx) {
5304
4684
  const session = await commands.createAgentSessionOnComment(parseAgentSessionOnCommentInput(input), actor);
5305
4685
  return payload({ agentSession: formatAgentSession(session) });
5306
4686
  },
5307
- agentSessionUpdate: ({ id: id2, input }) => {
4687
+ agentSessionUpdate: ({ id, input }) => {
5308
4688
  const parsed = parseAgentSessionUpdateInput(input);
5309
- const session = commands.updateAgentSession(String(id2 ?? parsed.id), {
4689
+ const session = commands.updateAgentSession(String(id ?? parsed.id), {
5310
4690
  status: parsed.status,
5311
4691
  plan: parsed.plan,
5312
4692
  externalUrls: parsed.externalUrls
@@ -6077,538 +5457,4 @@ function createLinearTwinApp(options = {}) {
6077
5457
  return withPublicOAuth(app, db);
6078
5458
  }
6079
5459
 
6080
- // ../packages/twin-linear/dist/src/check-params.js
6081
- var issueTitle = {
6082
- name: "title",
6083
- pattern: '[^"\\n]+',
6084
- example: "Orders 500 after deploy",
6085
- render: (value) => value,
6086
- parse: (raw) => raw
6087
- };
6088
- var teamKey = {
6089
- name: "team",
6090
- pattern: "[A-Z][A-Z0-9]*",
6091
- example: "ENG",
6092
- render: (value) => value,
6093
- parse: (raw) => raw
6094
- };
6095
- var workflowStateName = {
6096
- name: "state",
6097
- pattern: '[^"\\n]+',
6098
- example: "In Progress",
6099
- render: (value) => value,
6100
- parse: (raw) => raw
6101
- };
6102
- var labelName = {
6103
- name: "label",
6104
- pattern: '[^"\\n]+',
6105
- example: "Agent",
6106
- render: (value) => value,
6107
- parse: (raw) => raw
6108
- };
6109
- var estimatePoints = {
6110
- name: "estimate",
6111
- pattern: "0|[1-9][0-9]*",
6112
- example: "2",
6113
- render: (value) => value,
6114
- parse: (raw) => raw
6115
- };
6116
- var userRef = {
6117
- name: "user",
6118
- pattern: "[^`\\n]+",
6119
- example: "dev@pome-twin.test",
6120
- render: (value) => value,
6121
- parse: (raw) => raw
6122
- };
6123
- var commentNeedle = {
6124
- name: "needle",
6125
- pattern: '[^"\\n]+',
6126
- example: "triage",
6127
- render: (value) => value,
6128
- parse: (raw) => raw
6129
- };
6130
-
6131
- // ../packages/twin-linear/dist/src/check-state.js
6132
- var ISSUES_PATH = statePath("issues");
6133
- var COMMENTS_PATH = statePath("comments");
6134
- var LABELS_PATH = statePath("labels");
6135
- var WORKFLOW_STATES_PATH = statePath("workflowStates");
6136
- var USERS_PATH = statePath("users");
6137
- function fieldPath(base, row, key) {
6138
- return Object.prototype.hasOwnProperty.call(row, key) ? childStatePath(base, key) : base;
6139
- }
6140
- function unresolved(r) {
6141
- const outcome = r.skip ? { passed: false, status: "skipped", reason: r.missing } : { passed: false, reason: r.missing };
6142
- if (r.searched === void 0)
6143
- return outcome;
6144
- return { ...outcome, evidenceStatePaths: [r.searched] };
6145
- }
6146
- function isTruncated(state, collection) {
6147
- return (state.exportBounds?.truncatedCollections ?? []).includes(collection);
6148
- }
6149
- function resolveIssue(state, teamKey2, title) {
6150
- if (state.teams == null)
6151
- return { missing: "state_incomplete", skip: true };
6152
- const team = state.teams.find((t) => t.key === teamKey2);
6153
- if (team?.id == null) {
6154
- return {
6155
- missing: `team \`${teamKey2}\` not found in state_final`,
6156
- skip: false,
6157
- searched: statePath("teams")
6158
- };
6159
- }
6160
- if (state.issues == null)
6161
- return { missing: "state_incomplete", skip: true };
6162
- const indices = state.issues.map((issue, index) => issue.teamId === team.id && issue.title === title && (issue.archivedAt ?? null) === null ? index : -1).filter((index) => index >= 0);
6163
- if (indices.length === 1) {
6164
- const index = indices[0];
6165
- return { found: state.issues[index], path: childStatePath(ISSUES_PATH, index) };
6166
- }
6167
- if (indices.length > 1) {
6168
- return {
6169
- missing: `${indices.length} issues in \`${teamKey2}\` share that title`,
6170
- skip: false,
6171
- searched: ISSUES_PATH
6172
- };
6173
- }
6174
- if (isTruncated(state, "issues")) {
6175
- return { missing: "state_truncated", skip: true, searched: ISSUES_PATH };
6176
- }
6177
- return {
6178
- missing: `no issue with that title in \`${teamKey2}\``,
6179
- skip: false,
6180
- searched: ISSUES_PATH
6181
- };
6182
- }
6183
- function resolveWorkflowStateName(state, issue) {
6184
- if (state.workflowStates == null)
6185
- return { missing: "state_incomplete", skip: true };
6186
- const index = state.workflowStates.findIndex((s) => s.id === issue.stateId && s.teamId === issue.teamId);
6187
- const row = index >= 0 ? state.workflowStates[index] : void 0;
6188
- if (row?.name == null) {
6189
- return { missing: "workflow_state_unresolved", skip: true, searched: WORKFLOW_STATES_PATH };
6190
- }
6191
- return { found: row.name, path: childStatePath(WORKFLOW_STATES_PATH, index, "name") };
6192
- }
6193
- function resolveLabelNames(state, issue) {
6194
- if (state.labels == null)
6195
- return { missing: "state_incomplete", skip: true };
6196
- const byId = new Map(state.labels.filter((l) => l.id != null).map((l) => [l.id, (l.name ?? "").toLowerCase()]));
6197
- const names = /* @__PURE__ */ new Set();
6198
- for (const id2 of issue.labelIds ?? []) {
6199
- const name = byId.get(id2);
6200
- if (name === void 0)
6201
- return { missing: "label_unresolved", skip: true, searched: LABELS_PATH };
6202
- names.add(name);
6203
- }
6204
- return { found: names, path: LABELS_PATH };
6205
- }
6206
- function resolveComments(state, issue) {
6207
- if (state.comments == null)
6208
- return { missing: "state_incomplete", skip: true };
6209
- return { found: state.comments.filter((c) => c.issueId === issue.id), path: COMMENTS_PATH };
6210
- }
6211
- function resolveUserLabels(state, id2) {
6212
- if (id2 == null || state.users == null)
6213
- return [];
6214
- const user = state.users.find((u) => u.id === id2);
6215
- if (user == null)
6216
- return [];
6217
- return [user.email, user.name, user.displayName].filter((v) => typeof v === "string" && v.length > 0);
6218
- }
6219
-
6220
- // ../packages/twin-linear/dist/src/check-worlds.js
6221
- var FIXTURE_TEAM_ID = "team_eng";
6222
- var FIXTURE_TEAM_KEY = "ENG";
6223
- function fixtureStateId(name) {
6224
- return `state_${name.toLowerCase().replace(/[^a-z0-9]+/g, "_")}`;
6225
- }
6226
- function fixtureLabelId(name) {
6227
- return `label_${name.toLowerCase().replace(/[^a-z0-9]+/g, "_")}`;
6228
- }
6229
- var FIXTURE_STATES = [
6230
- { name: "Backlog", type: "backlog" },
6231
- { name: "Todo", type: "unstarted" },
6232
- { name: "In Progress", type: "started" },
6233
- { name: "Done", type: "completed" },
6234
- { name: "Canceled", type: "canceled" }
6235
- ];
6236
- function finalWorld(final) {
6237
- return { seed: null, final, tape: null };
6238
- }
6239
- function deltaWorld(seed, final) {
6240
- return { seed, final, tape: null };
6241
- }
6242
- function tapeWorld(tape) {
6243
- return { seed: null, final: { issues: [] }, tape };
6244
- }
6245
- function linearState(issues, comments = [], labelNames = ["Agent"]) {
6246
- return {
6247
- teams: [{ id: FIXTURE_TEAM_ID, key: FIXTURE_TEAM_KEY, name: "Engineering" }],
6248
- workflowStates: FIXTURE_STATES.map((s) => ({
6249
- id: fixtureStateId(s.name),
6250
- teamId: FIXTURE_TEAM_ID,
6251
- name: s.name,
6252
- type: s.type
6253
- })),
6254
- labels: labelNames.map((name) => ({
6255
- id: fixtureLabelId(name),
6256
- teamId: FIXTURE_TEAM_ID,
6257
- name
6258
- })),
6259
- issues,
6260
- comments,
6261
- users: [
6262
- { id: "user_dev", email: "dev@pome-twin.test", name: "Developer", displayName: "Dev" }
6263
- ],
6264
- exportBounds: { truncatedCollections: [] }
6265
- };
6266
- }
6267
- function issueRow(title, over = {}) {
6268
- return {
6269
- id: over.id ?? "issue_1",
6270
- identifier: "ENG-1",
6271
- number: 1,
6272
- teamId: FIXTURE_TEAM_ID,
6273
- title,
6274
- stateId: fixtureStateId(over.stateName ?? "In Progress"),
6275
- estimate: over.estimate ?? null,
6276
- ...over.assigneeId === void 0 ? {} : { assigneeId: over.assigneeId },
6277
- archivedAt: null,
6278
- labelIds: (over.labelNames ?? []).map(fixtureLabelId)
6279
- };
6280
- }
6281
-
6282
- // ../packages/twin-linear/dist/src/check-comments.js
6283
- var issueCommentContains = defineCheck({
6284
- id: "linear.issue-comment-contains",
6285
- description: 'Resolves the issue and scans the body of every comment on it for this string as a SUBSTRING, case-sensitively. Because the string is hunted inside free prose rather than compared to a field, a redactor that destroys it makes this check unable to fire \u2014 the engine skips it as `subject_redacted` rather than passing it vacuously. Choose the needle every honest phrasing would share, not the whole sentence you imagine the agent writing: a task wanting a GitHub cross-reference is served by "#1", where "GitHub issue #1" fails an agent that wrote "linked from acme/api#1".',
6286
- template: 'A comment on issue "{title}" in `{team}` contains "{needle}"',
6287
- params: { title: issueTitle, team: teamKey, needle: commentNeedle },
6288
- substrate: "final",
6289
- polarity: () => "positive",
6290
- // The needle is SCANNED inside prose, so it is the subject — twin-slack's
6291
- // `messageNeedle` precedent, and the reason a redacted needle becomes an
6292
- // honest skip rather than a silent failure.
6293
- subject: ({ needle }) => needle,
6294
- vacuityMutant: (args) => ({ ...args, needle: VACUITY_SENTINEL }),
6295
- discriminatingWorlds: ({ title, needle }) => ({
6296
- passing: finalWorld(linearState([issueRow(title)], [{ id: "c1", issueId: "issue_1", parentId: null, body: `see ${needle}` }])),
6297
- // The issue AND a comment are present in both worlds; only the body
6298
- // moves. A world with no comments would still fail, but for a reason
6299
- // closer to the empty world's than to the assertion's.
6300
- failing: finalWorld(linearState([issueRow(title)], [{ id: "c1", issueId: "issue_1", parentId: null, body: "nothing relevant here" }]))
6301
- }),
6302
- evaluate({ title, team, needle }, { final }) {
6303
- const issue = resolveIssue(final, team, title);
6304
- if ("missing" in issue)
6305
- return unresolved(issue);
6306
- const comments = resolveComments(final, issue.found);
6307
- if ("missing" in comments)
6308
- return unresolved(comments);
6309
- const hit = comments.found.some((c) => (c.body ?? "").includes(needle));
6310
- return {
6311
- passed: hit,
6312
- reason: hit ? `a comment contains "${needle}"` : `no comment contains "${needle}" (${comments.found.length} comment(s) scanned)`,
6313
- evidenceStatePaths: [comments.path]
6314
- };
6315
- }
6316
- });
6317
- var issueThreadedReply = defineCheck({
6318
- id: "linear.issue-threaded-reply",
6319
- description: "Asserts a comment exists on this issue whose `parentId` names a comment that was ALREADY THERE IN THE SEED. Needs the seed: it is a delta, not a state assertion, and the delta is what separates replying inside an existing thread from posting a comment and replying to yourself. It asserts nothing about what the reply says, or who wrote it.",
6320
- template: 'A threaded reply to a seeded comment exists on issue "{title}" in `{team}`',
6321
- params: { title: issueTitle, team: teamKey },
6322
- substrate: "seed+final",
6323
- polarity: () => "positive",
6324
- // No caller-supplied literal reaches an assertion: title and team only
6325
- // select, and the predicate compares ids it read out of the two substrates.
6326
- subject: () => null,
6327
- // Ledgered. Both slots only SELECT, so there is nothing in the sentence to
6328
- // falsify — the trigger is a parentId relation between two substrates.
6329
- vacuityMutant: () => null,
6330
- discriminatingWorlds: ({ title }) => {
6331
- const seededRoot = { id: "root", issueId: "issue_1", parentId: null, body: "reply here" };
6332
- const seed = linearState([issueRow(title)], [seededRoot]);
6333
- return {
6334
- passing: deltaWorld(seed, linearState([issueRow(title)], [seededRoot, { id: "reply", issueId: "issue_1", parentId: "root", body: "on it" }])),
6335
- // The issue and the seeded root are present in both worlds; only the
6336
- // reply's PARENT moves. This agent commented — just not in the thread.
6337
- failing: deltaWorld(seed, linearState([issueRow(title)], [seededRoot, { id: "own", issueId: "issue_1", parentId: null, body: "on it" }]))
6338
- };
6339
- },
6340
- evaluate({ title, team }, { seed, final }) {
6341
- if (seed === null)
6342
- return { passed: false, reason: "seed_missing", status: "skipped" };
6343
- const seedIssue = resolveIssue(seed, team, title);
6344
- if ("missing" in seedIssue)
6345
- return unresolved({ ...seedIssue, searched: void 0 });
6346
- const seedComments = resolveComments(seed, seedIssue.found);
6347
- if ("missing" in seedComments)
6348
- return unresolved({ ...seedComments, searched: void 0 });
6349
- const seeded = new Set(seedComments.found.map((c) => c.id).filter((id2) => typeof id2 === "string"));
6350
- const finalIssue = resolveIssue(final, team, title);
6351
- if ("missing" in finalIssue)
6352
- return unresolved(finalIssue);
6353
- const finalComments = resolveComments(final, finalIssue.found);
6354
- if ("missing" in finalComments)
6355
- return unresolved(finalComments);
6356
- const replies = finalComments.found.filter((c) => typeof c.parentId === "string" && seeded.has(c.parentId));
6357
- return {
6358
- passed: replies.length > 0,
6359
- reason: `${replies.length} repl${replies.length === 1 ? "y" : "ies"} to a seeded comment (${seeded.size} seeded comment(s), ${finalComments.found.length} at finish)`,
6360
- // The FINAL comment list only. This is a delta over two trees and the
6361
- // reader has one on screen; a pointer into the seed would send them to the
6362
- // tree the report does not render (see the sdk's `check-state-path.ts`).
6363
- // The reason carries the seed side.
6364
- evidenceStatePaths: [COMMENTS_PATH]
6365
- };
6366
- }
6367
- });
6368
-
6369
- // ../packages/twin-linear/dist/src/check-issues.js
6370
- var issueExists = defineCheck({
6371
- id: "linear.issue-exists",
6372
- description: "Asserts an unarchived issue with this exact title exists in the named team. Declared and unused by the shipped corpus on purpose: `linear.issue-state` FAILS on a missing issue and therefore subsumes this one, so task 26 carries the state criterion alone \u2014 twin-github ships the same pair for the same reason. A vocabulary is what an author may pick from, not what the corpus happens to exercise. Title matching is EXACT and archived issues do not count, so an examinee that renames or archives the issue fails this.",
6373
- template: 'An issue titled "{title}" exists in `{team}`',
6374
- params: { title: issueTitle, team: teamKey },
6375
- substrate: "final",
6376
- polarity: () => "positive",
6377
- // The only check where the title IS the assertion rather than the selector,
6378
- // which is why it is the only one that declares the title as its subject.
6379
- subject: ({ title }) => title,
6380
- vacuityMutant: (args) => ({ ...args, title: VACUITY_SENTINEL }),
6381
- discriminatingWorlds: ({ title }) => ({
6382
- // The team is present in both worlds; only the issue moves. `linearState`
6383
- // always fills teams, so the failing world's reason is "no issue with that
6384
- // title in `ENG`" rather than the empty world's "team not found" — which is
6385
- // what arm 3 rejects.
6386
- passing: finalWorld(linearState([issueRow(title)])),
6387
- failing: finalWorld(linearState([issueRow("a different issue entirely")]))
6388
- }),
6389
- evaluate({ title, team }, { final }) {
6390
- const issue = resolveIssue(final, team, title);
6391
- if ("missing" in issue)
6392
- return unresolved(issue);
6393
- return {
6394
- passed: true,
6395
- reason: `an issue titled "${title}" exists in \`${team}\``,
6396
- evidenceStatePaths: [issue.path]
6397
- };
6398
- }
6399
- });
6400
- var issueState = defineCheck({
6401
- id: "linear.issue-state",
6402
- description: "Resolves the issue by title within the named team, follows `stateId` to that team's workflow-state row, and compares its NAME to the one given \u2014 case-insensitively, because a workflow state name is prose an author retypes. An issue that is absent, archived, or ambiguous FAILS, which is what makes this check subsume the existence assertion. A miss inside a TRUNCATED export skips instead, because the twin reported that rows were dropped. Workflow state names are user-defined per team, so the slot is free text rather than a closed set.",
6403
- // "is in state X", not "is X". Two reasons, both of which twin-github's
6404
- // `issue-state` comment states while crediting this very idiom to the Linear
6405
- // tasks: one reading habit spans twins, and the longer literal tail keeps
6406
- // this template from near-missing `... is assigned to \`{user}\``.
6407
- template: 'Issue "{title}" in `{team}` is in state "{state}"',
6408
- params: { title: issueTitle, team: teamKey, state: workflowStateName },
6409
- substrate: "final",
6410
- // Positive on every shipped use. Unlike `github.issue-state` there is no
6411
- // canonical "open" member to read a prohibition off, because the state names
6412
- // belong to the workspace rather than to the API.
6413
- polarity: () => "positive",
6414
- // The state name is the value COMPARED against the state; title and team only
6415
- // select.
6416
- subject: ({ state }) => state,
6417
- vacuityMutant: (args) => ({ ...args, state: VACUITY_SENTINEL }),
6418
- discriminatingWorlds: ({ title, state }) => ({
6419
- passing: finalWorld(linearState([issueRow(title, { stateName: state })])),
6420
- // The team and the issue are PRESENT in both worlds; only the state moves.
6421
- failing: finalWorld(linearState([issueRow(title, { stateName: "Backlog" })]))
6422
- }),
6423
- evaluate({ title, team, state }, { final }) {
6424
- const issue = resolveIssue(final, team, title);
6425
- if ("missing" in issue)
6426
- return unresolved(issue);
6427
- const name = resolveWorkflowStateName(final, issue.found);
6428
- if ("missing" in name)
6429
- return unresolved(name);
6430
- return {
6431
- passed: name.found.toLowerCase() === state.toLowerCase(),
6432
- reason: `issue state is "${name.found}" (wanted "${state}")`,
6433
- // BOTH ends of trap 1's join. An issue has no state string — it has a
6434
- // `stateId` into the team's own workflow catalog — so a reader handed only
6435
- // the issue row would find an opaque id, and one handed only the catalog
6436
- // row would not know which issue pointed at it.
6437
- evidenceStatePaths: [fieldPath(issue.path, issue.found, "stateId"), name.path]
6438
- };
6439
- }
6440
- });
6441
- var issueHasLabel = defineCheck({
6442
- id: "linear.issue-has-label",
6443
- description: "Resolves the issue, joins its `labelIds` to the workspace label catalog, and asserts the named label is among them \u2014 case-insensitively, as the legacy rule's comparison was. The join is the point: this export carries label IDS where the seed writes names and twin-github writes objects, so one concept has three shapes and only this one is exported. A label id with no catalog row is a partial export and SKIPS rather than failing.",
6444
- template: 'Issue "{title}" in `{team}` has label "{label}"',
6445
- params: { title: issueTitle, team: teamKey, label: labelName },
6446
- substrate: "final",
6447
- polarity: () => "positive",
6448
- subject: ({ label }) => label,
6449
- vacuityMutant: (args) => ({ ...args, label: VACUITY_SENTINEL }),
6450
- discriminatingWorlds: ({ title, label }) => ({
6451
- passing: finalWorld(linearState([issueRow(title, { labelNames: [label] })], [], [label])),
6452
- // The issue is present and resolvable in both; only its labels move.
6453
- failing: finalWorld(linearState([issueRow(title, { labelNames: [] })], [], [label]))
6454
- }),
6455
- evaluate({ title, team, label }, { final }) {
6456
- const issue = resolveIssue(final, team, title);
6457
- if ("missing" in issue)
6458
- return unresolved(issue);
6459
- const names = resolveLabelNames(final, issue.found);
6460
- if ("missing" in names)
6461
- return unresolved(names);
6462
- return {
6463
- passed: names.found.has(label.toLowerCase()),
6464
- reason: `issue carries ${names.found.size} label(s) (wanted "${label}")`,
6465
- // Trap 2's join, both ends: `labelIds` on the row, names in the catalog.
6466
- evidenceStatePaths: [fieldPath(issue.path, issue.found, "labelIds"), names.path]
6467
- };
6468
- }
6469
- });
6470
- var issueEstimate = defineCheck({
6471
- id: "linear.issue-estimate",
6472
- description: "Resolves the issue and compares its `estimate` column to the number given. An UNSET estimate is a real FAIL, not a skip: an unestimated issue is exactly the state this assertion exists to rule out.",
6473
- template: 'Issue "{title}" in `{team}` has estimate {estimate}',
6474
- params: { title: issueTitle, team: teamKey, estimate: estimatePoints },
6475
- substrate: "final",
6476
- polarity: () => "positive",
6477
- // The single null in the subject column, and for twin-slack's emoji-name
6478
- // reason rather than an exemption: a bare integer of at most three digits
6479
- // matches no pattern in `redactSecrets` (key prefixes, 13-19-digit runs) or
6480
- // in a team's `PII_PATTERNS` (emails, phones). A value no redactor can eat is
6481
- // not a subject, and declaring one would only narrow the corpus gate's
6482
- // whole-phrase fallback for nothing.
6483
- subject: () => null,
6484
- // D10's second allowlist entry ever, and the argument is stripe's unchanged:
6485
- // here the number IS the scanned value, and the title is the selector.
6486
- vacuityMutant: (args) => ({ ...args, estimate: String(VACUITY_SENTINEL_NUMBER) }),
6487
- discriminatingWorlds: ({ title, estimate }) => ({
6488
- passing: finalWorld(linearState([issueRow(title, { estimate: Number(estimate) })])),
6489
- failing: finalWorld(linearState([issueRow(title, { estimate: null })]))
6490
- }),
6491
- evaluate({ title, team, estimate }, { final }) {
6492
- const issue = resolveIssue(final, team, title);
6493
- if ("missing" in issue)
6494
- return unresolved(issue);
6495
- const actual = issue.found.estimate ?? null;
6496
- return {
6497
- passed: actual === Number(estimate),
6498
- reason: `issue estimate is ${actual === null ? "unset" : actual} (wanted ${estimate})`,
6499
- // `fieldPath`, not a bare `…/estimate`: an UNSET estimate is the verdict
6500
- // this check exists to deliver, and on an export that omits the column
6501
- // entirely a pointer at it would resolve to nothing — stripping the
6502
- // affordance from exactly the row a reader wants to open.
6503
- evidenceStatePaths: [fieldPath(issue.path, issue.found, "estimate")]
6504
- };
6505
- }
6506
- });
6507
- var issueAssignee = defineCheck({
6508
- id: "linear.issue-assignee",
6509
- description: "Resolves the issue, then its assignee, and matches the given reference against that user's email, name OR displayName \u2014 every spelling the legacy rule accepted. An UNASSIGNED issue is a real FAIL. Declared with no shipped corpus user, carrying a legacy capability forward. This is the check whose `subject` earns its keep: an email reference is destroyed by a team's `PII_PATTERNS`, which the twin's own redactor has no equivalent of, so without the declaration the criterion would silently be unable to fire.",
6510
- template: 'Issue "{title}" in `{team}` is assigned to `{user}`',
6511
- params: { title: issueTitle, team: teamKey, user: userRef },
6512
- substrate: "final",
6513
- polarity: () => "positive",
6514
- subject: ({ user }) => user,
6515
- vacuityMutant: (args) => ({ ...args, user: VACUITY_SENTINEL }),
6516
- discriminatingWorlds: ({ title }) => ({
6517
- // The issue resolves in both worlds; only the assignee moves.
6518
- passing: finalWorld(linearState([issueRow(title, { assigneeId: "user_dev" })])),
6519
- failing: finalWorld(linearState([issueRow(title)]))
6520
- }),
6521
- evaluate({ title, team, user }, { final }) {
6522
- const issue = resolveIssue(final, team, title);
6523
- if ("missing" in issue)
6524
- return unresolved(issue);
6525
- const labels = resolveUserLabels(final, issue.found.assigneeId);
6526
- const assignment = [fieldPath(issue.path, issue.found, "assigneeId")];
6527
- if (labels.length === 0) {
6528
- return { passed: false, reason: "issue has no assignee", evidenceStatePaths: assignment };
6529
- }
6530
- if (final.users != null)
6531
- assignment.push(USERS_PATH);
6532
- return {
6533
- passed: labels.some((l) => l.toLowerCase() === user.toLowerCase()),
6534
- // Safe to quote: `user` is this check's declared subject.
6535
- reason: `issue is assigned to \`${labels[0]}\` (wanted \`${user}\`)`,
6536
- evidenceStatePaths: assignment
6537
- };
6538
- }
6539
- });
6540
-
6541
- // ../packages/twin-linear/dist/src/check-tape.js
6542
- var noUnsupportedEndpoint = defineCheck({
6543
- id: "linear.no-unsupported-endpoint",
6544
- description: 'Scans the recorded call tape for any request the twin answered with fidelity "unsupported" \u2014 a route it does not implement, answered 501. It asserts nothing about whether the run SUCCEEDED, and nothing about calls that were merely rejected: a 404 or a 422 from a route the twin does implement is a semantic answer and passes. The tape is scoped to this twin by the engine before the check sees it, so an unsupported call to a different twin in a multi-twin session cannot fail it.',
6545
- template: "No unsupported endpoint was called",
6546
- params: {},
6547
- substrate: "tape",
6548
- // A prohibition. Nothing is required to happen; only the examinee reaching
6549
- // for an unimplemented route can break it.
6550
- polarity: () => "negative",
6551
- // No caller-supplied literal is hunted for in any substrate, so there is
6552
- // nothing a redactor could silently delete out from under this check.
6553
- subject: () => null,
6554
- // Ledgered. No slots, so the sentence carries no literal to falsify — the
6555
- // trigger is a fidelity stamp on the tape, which no mutation of the criterion
6556
- // text can reach.
6557
- vacuityMutant: () => null,
6558
- discriminatingWorlds: () => ({
6559
- passing: tapeWorld([
6560
- {
6561
- twin: "linear",
6562
- method: "POST",
6563
- path: "/graphql",
6564
- status: 200,
6565
- fidelity: "semantic",
6566
- event_id: "evt_ok"
6567
- }
6568
- ]),
6569
- failing: tapeWorld([
6570
- {
6571
- twin: "linear",
6572
- method: "POST",
6573
- path: "/graphql",
6574
- status: 501,
6575
- fidelity: "unsupported",
6576
- event_id: "evt_bad"
6577
- }
6578
- ])
6579
- }),
6580
- evaluate(_args, { tape }) {
6581
- if (tape === null)
6582
- return { passed: false, reason: "tape_missing", status: "skipped" };
6583
- const unsupported = tape.filter((event) => event.fidelity === "unsupported");
6584
- if (unsupported.length === 0) {
6585
- return {
6586
- passed: true,
6587
- reason: `no unsupported endpoint was called (${tape.length} call(s) inspected)`
6588
- };
6589
- }
6590
- const evidenceEventIds = unsupported.map((event) => event.event_id).filter((id2) => typeof id2 === "string" && id2 !== "");
6591
- return {
6592
- passed: false,
6593
- reason: `${unsupported.length} unsupported call(s): ${unsupported.map((e) => `${e.method ?? "?"} ${e.path ?? "?"}`).join(", ")}`,
6594
- ...evidenceEventIds.length > 0 ? { evidenceEventIds } : {}
6595
- };
6596
- }
6597
- });
6598
-
6599
- // ../packages/twin-linear/dist/src/checks.js
6600
- var LINEAR_CHECKS = [
6601
- issueExists,
6602
- issueState,
6603
- issueHasLabel,
6604
- issueEstimate,
6605
- issueAssignee,
6606
- issueCommentContains,
6607
- issueThreadedReply,
6608
- // Last, because the listing order runs from the assertion an author reaches
6609
- // for first to the ones a specialised task needs — and this is the only one
6610
- // that reads the run rather than the world it left behind.
6611
- noUnsupportedEndpoint
6612
- ];
6613
-
6614
- export { DEFAULT_LINEAR_CLOCK, DEFAULT_LINEAR_EMAIL, DEFAULT_LINEAR_PORT, DEFAULT_LINEAR_SID, DEFAULT_LINEAR_TOKEN, LINEAR_CHECKS, LINEAR_MCP_TOOL_COUNT, LINEAR_PROVIDER_TOKEN_PREFIX, LinearDomain, LinearTwinError, STATE_EXPORT_CAP, assertWebhookUrl, createLinearTwinApp, createLinearTwinDefinition, defaultSeedState, exportLinearState, linearErrorEnvelope, linearSeedSchema, linearStateDelta, linearTools, loadSeedFromEnv, looksLikeLinearToken, migrate, openLinearTwinDatabase, parseSeed, projectLinearRecording, registerLinearRoutes, resetDatabase, resolveLinearCredential, unauthorizedEnvelope, unsupportedEnvelope, webhookUrlError, withPublicOAuth };
5460
+ export { LINEAR_MCP_TOOL_COUNT, LinearDomain, createLinearTwinApp, createLinearTwinDefinition, exportLinearState, linearStateDelta, linearTools, looksLikeLinearToken, migrate, openLinearTwinDatabase, projectLinearRecording, registerLinearRoutes, resetDatabase, resolveLinearCredential, withPublicOAuth };