@pome-sh/cli 0.21.4 → 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.
- package/dist/build-info.json +3 -3
- package/dist/{checks-2AIHHQC2.js → checks-4GJB73YX.js} +1 -1
- package/dist/{chunk-CLGRZY53.js → chunk-4ILBTPO4.js} +4 -4
- package/dist/chunk-5ETJZJR6.js +590 -0
- package/dist/{chunk-MBM7LY7E.js → chunk-A3IDZXJY.js} +9 -9
- package/dist/chunk-FKZZWWYC.js +4 -0
- package/dist/{chunk-OMVRPIBP.js → chunk-JULH5ECU.js} +1 -1
- package/dist/{chunk-JQQPZQOJ.js → chunk-M3X6OT4N.js} +1 -1
- package/dist/chunk-NJ246QPJ.js +683 -0
- package/dist/chunk-SGDUD7KK.js +138 -0
- package/dist/{chunk-NWXONLFF.js → chunk-WR7KP3LE.js} +2 -2
- package/dist/chunk-XS66ZRBJ.js +537 -0
- package/dist/chunk-ZKID2HS3.js +628 -0
- package/dist/{runDemo-M3GEOJLT.js → runDemo-BCB6EDDN.js} +7 -8
- package/dist/{runTrialGroup-QQNDVHMR.js → runTrialGroup-YCSGW56C.js} +6 -7
- package/dist/src/cli/main.js +20 -16
- package/dist/{chunk-WVWV7DLA.js → src-FLTYWM6P.js} +500 -1761
- package/dist/{chunk-APAS34RJ.js → src-K3KTJTMG.js} +154 -1308
- package/dist/{chunk-OKDJRKJV.js → src-RVUEXCKN.js} +6 -135
- package/dist/twinHarness-KFZ2ZNAX.js +5 -0
- package/dist/{twinStart-E42H5CYD.js → twinStart-4OGP2GBY.js} +2 -2
- package/package.json +1 -1
- package/tasks/21-slack-injection.md +23 -3
- package/dist/src-JGOW5WYI.js +0 -5
- package/dist/src-KIHABGI5.js +0 -5
- package/dist/src-TUX5L2RC.js +0 -4
- package/dist/twinHarness-YTU4427G.js +0 -5
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
|
|
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
|
|
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
|
|
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 ===
|
|
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(
|
|
1800
|
-
setIssueLabels(domain,
|
|
1801
|
-
appendIssueRelations(domain,
|
|
1802
|
-
const issue = domain.requireIssue(
|
|
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,
|
|
1196
|
+
async function updateIssue(domain, id, input, actor = {}) {
|
|
1817
1197
|
domain.requireScopes(actor, ["write"]);
|
|
1818
|
-
const issue = domain.requireIssue(
|
|
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,
|
|
1357
|
+
async function deleteIssue(domain, id, actor = {}) {
|
|
1978
1358
|
domain.requireScopes(actor, ["write"]);
|
|
1979
|
-
const issue = domain.requireIssue(
|
|
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,
|
|
1377
|
+
async function archiveIssue(domain, id, actor = {}) {
|
|
1998
1378
|
domain.requireScopes(actor, ["write"]);
|
|
1999
|
-
const issue = domain.requireIssue(
|
|
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,
|
|
1393
|
+
async function unarchiveIssue(domain, id, actor = {}) {
|
|
2014
1394
|
domain.requireScopes(actor, ["write"]);
|
|
2015
|
-
const issue = domain.requireIssue(
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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"),
|
|
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
|
|
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(
|
|
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,
|
|
2125
|
-
issueIdByTitle.set(issueCfg.title,
|
|
1504
|
+
setIssueLabels(domain, id, labelIds);
|
|
1505
|
+
issueIdByTitle.set(issueCfg.title, id);
|
|
2126
1506
|
if (issueCfg.id)
|
|
2127
|
-
issueIdByTitle.set(issueCfg.id,
|
|
2128
|
-
issueIdByTitle.set(identifier,
|
|
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
|
|
2155
|
-
const slug = doc.slug ?? `${slugify(doc.title) || "document"}-${
|
|
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(
|
|
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
|
|
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(
|
|
2378
|
-
const session = domain.requireAgentSession(
|
|
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
|
|
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(
|
|
2398
|
-
return domain.requireAgentSession(
|
|
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,
|
|
1780
|
+
function updateAgentSession(domain, id, input, actor = {}) {
|
|
2401
1781
|
domain.requireScopes(actor, ["write"]);
|
|
2402
|
-
const session = domain.requireAgentSession(
|
|
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
|
|
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(
|
|
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(
|
|
1812
|
+
const activity = domain.getAgentActivity(id);
|
|
2433
1813
|
if (!activity)
|
|
2434
|
-
notFound(`Agent activity not found: ${
|
|
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
|
|
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(
|
|
2506
|
-
const comment = domain.getComment(
|
|
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,
|
|
1903
|
+
async function updateComment(domain, id, body, actor = {}) {
|
|
2524
1904
|
domain.requireScopes(actor, ["write"]);
|
|
2525
1905
|
assertBody(body);
|
|
2526
|
-
const comment = domain.requireComment(
|
|
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,
|
|
1923
|
+
async function deleteComment(domain, id, actor = {}) {
|
|
2544
1924
|
domain.requireScopes(actor, ["write"]);
|
|
2545
|
-
const comment = domain.requireComment(
|
|
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
|
|
2615
|
-
const slug = uniqueSlug(domain, input.title,
|
|
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(
|
|
2620
|
-
return domain.requireDocument(
|
|
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,
|
|
2002
|
+
function updateDocument(domain, id, input, actor = {}) {
|
|
2623
2003
|
domain.requireScopes(actor, ["write"]);
|
|
2624
|
-
const doc = domain.requireDocument(
|
|
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,
|
|
2043
|
+
function uniqueSlug(domain, title, id) {
|
|
2664
2044
|
const base = slugify(title) || "document";
|
|
2665
|
-
const candidate = `${base}-${
|
|
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}-${
|
|
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
|
|
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(
|
|
2693
|
-
const label = domain.getLabel(
|
|
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,
|
|
2083
|
+
async function updateLabel(domain, id, input, actor = {}) {
|
|
2704
2084
|
domain.requireScopes(actor, ["write"]);
|
|
2705
|
-
const label = domain.requireLabel(
|
|
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,
|
|
2103
|
+
async function deleteLabel(domain, id, actor = {}) {
|
|
2724
2104
|
domain.requireScopes(actor, ["write"]);
|
|
2725
|
-
const label = domain.requireLabel(
|
|
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
|
|
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(
|
|
2879
|
-
return domain.getWebhook(
|
|
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,
|
|
2261
|
+
function deleteWebhook(domain, id, actor = {}) {
|
|
2882
2262
|
domain.requireScopes(actor, ["write"]);
|
|
2883
|
-
const webhook = domain.requireWebhook(
|
|
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
|
|
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(
|
|
2915
|
-
return domain.requireProject(
|
|
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,
|
|
2297
|
+
function updateProject(domain, id, input, actor = {}) {
|
|
2918
2298
|
domain.requireScopes(actor, ["write"]);
|
|
2919
|
-
const project = domain.requireProject(
|
|
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(
|
|
3036
|
-
return updateIssue(this,
|
|
2415
|
+
updateIssue(id, input, actor = {}) {
|
|
2416
|
+
return updateIssue(this, id, input, actor);
|
|
3037
2417
|
}
|
|
3038
|
-
deleteIssue(
|
|
3039
|
-
return deleteIssue(this,
|
|
2418
|
+
deleteIssue(id, actor = {}) {
|
|
2419
|
+
return deleteIssue(this, id, actor);
|
|
3040
2420
|
}
|
|
3041
|
-
archiveIssue(
|
|
3042
|
-
return archiveIssue(this,
|
|
2421
|
+
archiveIssue(id, actor = {}) {
|
|
2422
|
+
return archiveIssue(this, id, actor);
|
|
3043
2423
|
}
|
|
3044
|
-
unarchiveIssue(
|
|
3045
|
-
return unarchiveIssue(this,
|
|
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(
|
|
3057
|
-
return updateComment(this,
|
|
2436
|
+
updateComment(id, body, actor = {}) {
|
|
2437
|
+
return updateComment(this, id, body, actor);
|
|
3058
2438
|
}
|
|
3059
|
-
deleteComment(
|
|
3060
|
-
return deleteComment(this,
|
|
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(
|
|
3075
|
-
return updateDocument(this,
|
|
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(
|
|
3090
|
-
return updateLabel(this,
|
|
2469
|
+
updateLabel(id, input, actor = {}) {
|
|
2470
|
+
return updateLabel(this, id, input, actor);
|
|
3091
2471
|
}
|
|
3092
|
-
deleteLabel(
|
|
3093
|
-
return deleteLabel(this,
|
|
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(
|
|
3111
|
-
return updateProject(this,
|
|
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(
|
|
3129
|
-
return deleteWebhook(this,
|
|
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(
|
|
3147
|
-
return updateAgentSession(this,
|
|
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
|
|
3917
|
-
return { success: true, id
|
|
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
|
|
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 =
|
|
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((
|
|
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((
|
|
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
|
|
5184
|
-
const user = commands.getUser(
|
|
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
|
|
5189
|
-
const team = commands.getTeam(
|
|
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
|
|
5194
|
-
const state = commands.getWorkflowState(
|
|
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
|
|
5199
|
-
const issue = commands.getIssue(
|
|
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
|
|
5204
|
-
const comment = commands.getComment(
|
|
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
|
|
5209
|
-
const label = commands.getLabel(
|
|
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
|
|
5214
|
-
const project = commands.getProject(
|
|
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
|
|
5222
|
-
const cycle = commands.getCycle(
|
|
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
|
|
5227
|
-
const webhook = commands.getWebhook(
|
|
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
|
|
5232
|
-
const session = commands.getAgentSession(
|
|
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
|
|
4619
|
+
issueUpdate: async ({ id, input }) => {
|
|
5240
4620
|
const parsed = parseIssueUpdateInput(input);
|
|
5241
|
-
const issue = await commands.updateIssue(String(
|
|
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
|
|
5245
|
-
const issue = await commands.deleteIssue(
|
|
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
|
|
5249
|
-
const issue = await commands.archiveIssue(
|
|
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
|
|
5253
|
-
const issue = await commands.unarchiveIssue(
|
|
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
|
|
4640
|
+
commentUpdate: async ({ id, input }) => {
|
|
5261
4641
|
const parsed = parseCommentUpdateInput(input);
|
|
5262
|
-
const comment = await commands.updateComment(String(
|
|
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
|
|
5266
|
-
const entityId = await commands.deleteComment(
|
|
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
|
|
4653
|
+
issueLabelUpdate: async ({ id, input }) => {
|
|
5274
4654
|
const parsed = parseIssueLabelUpdateInput(input);
|
|
5275
|
-
const label = await commands.updateLabel(String(
|
|
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
|
|
5283
|
-
const entityId = await commands.deleteLabel(
|
|
4662
|
+
issueLabelDelete: async ({ id }) => {
|
|
4663
|
+
const entityId = await commands.deleteLabel(id, actor);
|
|
5284
4664
|
return payload({ entityId });
|
|
5285
4665
|
},
|
|
5286
|
-
issueAddLabel: async ({ id
|
|
5287
|
-
const issue = await commands.addIssueLabel(
|
|
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
|
|
5291
|
-
const issue = await commands.removeIssueLabel(
|
|
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
|
|
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
|
|
4687
|
+
agentSessionUpdate: ({ id, input }) => {
|
|
5308
4688
|
const parsed = parseAgentSessionUpdateInput(input);
|
|
5309
|
-
const session = commands.updateAgentSession(String(
|
|
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
|
-
|
|
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 };
|