@enrichlayer/el-linear 1.35.0 → 1.37.0
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/README.md +10 -9
- package/claude-skills/linear-operations/SKILL.md +36 -2
- package/dist/commands/config.js +2 -0
- package/dist/commands/issues.js +115 -1
- package/dist/config/config.d.ts +27 -1
- package/dist/config/config.js +12 -1
- package/dist/config/sop-label-validation.d.ts +82 -0
- package/dist/config/sop-label-validation.js +103 -0
- package/dist/queries/common.d.ts +2 -2
- package/dist/queries/common.js +1 -0
- package/dist/queries/issue-tree.d.ts +2 -0
- package/dist/queries/issue-tree.js +5 -0
- package/dist/queries/issues-types.d.ts +1 -0
- package/dist/queries/issues.d.ts +8 -8
- package/dist/queries/project-milestones.d.ts +1 -1
- package/dist/types/linear.d.ts +1 -0
- package/dist/utils/formatters/summary.js +7 -0
- package/dist/utils/gate-telemetry.d.ts +7 -1
- package/dist/utils/graphql-issues-service.js +4 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -249,13 +249,14 @@ itself: `el-linear teams list --raw | jq '.[] | {key, id}'`, etc.
|
|
|
249
249
|
|
|
250
250
|
### Gate telemetry (optional)
|
|
251
251
|
|
|
252
|
-
`issues create` has a duplicate-detection gate
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
252
|
+
`issues create` has a duplicate-detection gate (on by default) and an opt-in
|
|
253
|
+
[SOP-label parent gate](./docs/configuration.md#sop-label-parent-gate-validationsoplabelparentgate).
|
|
254
|
+
el-linear can record each gate's fire/override decision to a local JSONL file so
|
|
255
|
+
you can measure its **override-rate** and tell whether it's too aggressive. It is
|
|
256
|
+
**off by default** and writes nothing unless you opt in (e.g.
|
|
257
|
+
`export EL_TELEMETRY_DIR=<path>`); there is no server or database, and
|
|
258
|
+
`EL_TELEMETRY_DISABLED=1` forces it off. Full opt-in rules, the event schema, and
|
|
259
|
+
a `jq` reader are in [docs/telemetry.md](./docs/telemetry.md).
|
|
259
260
|
|
|
260
261
|
### Networking (IPv4 preference)
|
|
261
262
|
|
|
@@ -555,9 +556,9 @@ Currently wired through:
|
|
|
555
556
|
|
|
556
557
|
| Resource | Defaults | Extras you can request |
|
|
557
558
|
|----------|----------|------------------------|
|
|
558
|
-
| `issues list` | `id, title, state, assignee` | `project, cycle, milestone, labels, url, priority, estimate, createdAt, updatedAt, team` |
|
|
559
|
+
| `issues list` | `id, title, state, assignee` | `project, cycle, milestone, labels, url, priority, estimate, createdAt, updatedAt, completedAt, team` |
|
|
559
560
|
| `projects list` | `name, state, progress, lead` | `teams, target, url, updatedAt` |
|
|
560
|
-
| `issues read` (single) | `state, assignee, project, cycle, milestone, labels, url` | `priority, estimate, created, updated` |
|
|
561
|
+
| `issues read` (single) | `state, assignee, project, cycle, milestone, labels, url` | `priority, estimate, created, updated, completed` |
|
|
561
562
|
| `projects read` (single) | `state, lead, teams, target, progress, url` | (filter only — no extras) |
|
|
562
563
|
|
|
563
564
|
`identifier`, `id`, `title` and `name` are headline-only on single-resource summaries and are filtered out of the labelled block (they remain on the title line above). `status` is accepted as a synonym for `state`, `owner` for `assignee`, `targetDate` for `target`.
|
|
@@ -143,8 +143,8 @@ For tools that aren't `el-linear` (e.g. `gh`, `glab`, `kubectl`), prefer `jq` fo
|
|
|
143
143
|
|
|
144
144
|
`--format summary` is implemented for:
|
|
145
145
|
|
|
146
|
-
- **Single resources:** `issues read`, `comments read`, `projects read`, `cycles read`, `project-milestones read`, `documents read`, `templates read`, releases (`graphql` query results), `users read`
|
|
147
|
-
- **Lists:** `issues list`, `issues search`, `projects list`, `comments list`, `cycles list`, `project-milestones list`, `labels list`, `teams list`, `users list`, `documents list`, `templates list`, `attachments list`, `releases list`, and the cross-resource `search` command
|
|
146
|
+
- **Single resources:** `issues read`, `comments read`, `projects read`, `cycles read`, `project-milestones read`, `project-updates read`, `documents read`, `templates read`, releases (`graphql` query results), `users read`
|
|
147
|
+
- **Lists:** `issues list`, `issues search`, `projects list`, `comments list`, `cycles list`, `project-milestones list`, `project-updates list`, `labels list`, `teams list`, `users list`, `documents list`, `templates list`, `attachments list`, `releases list`, and the cross-resource `search` command
|
|
148
148
|
|
|
149
149
|
Commands without a dedicated formatter (e.g. `config show`, custom `graphql` queries) fall back to a generic key/value rendering of their JSON payload.
|
|
150
150
|
|
|
@@ -200,6 +200,21 @@ and outreach tracked in one place.
|
|
|
200
200
|
> `validation.duplicateDetection: false` (field validation still runs);
|
|
201
201
|
> `validation.enabled: false` turns off all validation.
|
|
202
202
|
|
|
203
|
+
> **SOP-label parent gate ([DEV-5378](https://linear.app/verticalint/issue/DEV-5378/), opt-in).** When enabled, `el-linear issues create` requires an
|
|
204
|
+
> issue carrying an **SOP-type label** (any name in `validation.sopLabels`,
|
|
205
|
+
> default `["SOP"]`, case-insensitive) to point at a **parent SOP** — `--parent`
|
|
206
|
+
> or `--related-to` must resolve to another SOP-labeled issue — and **blocks
|
|
207
|
+
> (exit non-zero)** otherwise, naming the rule. An SOP with no parent SOP is
|
|
208
|
+
> unfindable by SOP tooling and breaks the catalog topology. It is **off by
|
|
209
|
+
> default** (`validation.sopLabelParentGate: true` turns it on; the Enrich Layer
|
|
210
|
+
> shared team config flips it on) because "SOP" is a workspace-specific
|
|
211
|
+
> taxonomy, not something an open-source install should assume. Escape hatch:
|
|
212
|
+
> `--allow-unparented-sop` (narrow, for an intentionally top-level SOP);
|
|
213
|
+
> `--skip-validation` also bypasses it but skips all field validation. A typo'd
|
|
214
|
+
> or nonexistent parent reference **blocks** (naming the ref) so a mistake can't
|
|
215
|
+
> orphan an SOP; a transport/service error **fails open** with a warning (and a
|
|
216
|
+
> `fail-open` gate event); a resolvable non-SOP parent hard-blocks.
|
|
217
|
+
|
|
203
218
|
```bash
|
|
204
219
|
# --include-closed is required so previously-completed duplicates surface.
|
|
205
220
|
# `issues search` defaults to open states (DEV-4478); the duplicate check
|
|
@@ -517,6 +532,25 @@ el-linear projects add-team "Project Name" ENG 2>&1
|
|
|
517
532
|
|
|
518
533
|
**Never use raw `projectUpdate` with `teamIds`** — it replaces the entire team list. Always use `projects add-team` / `remove-team`.
|
|
519
534
|
|
|
535
|
+
### Project Updates (status posts) — mind the naming collision
|
|
536
|
+
|
|
537
|
+
Linear has two unrelated things spelled almost the same:
|
|
538
|
+
|
|
539
|
+
- `projectUpdate(id, input)` — the mutation that **edits a project** (the one warned about just above). Surfaced by the `projects` command.
|
|
540
|
+
- `ProjectUpdate` — a **status post** in a project's Updates feed (progress + a health color). Surfaced by the dedicated `project-updates` command:
|
|
541
|
+
|
|
542
|
+
```bash
|
|
543
|
+
# Post a status update to a project (appears in the Updates feed)
|
|
544
|
+
el-linear project-updates create --project "Auth Refactor" \
|
|
545
|
+
--body "Shipped the session-store migration; rollout at 60%." \
|
|
546
|
+
--health onTrack # onTrack | atRisk | offTrack — omit to leave unset
|
|
547
|
+
|
|
548
|
+
el-linear project-updates list --project "Auth Refactor"
|
|
549
|
+
el-linear project-updates read <updateId>
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
`--body` / `--body-file` are mutually exclusive (one required); `--body-file` avoids shell-quoting for markdown/tables. `--health` is validated against the enum before the API call. `--diff-hidden` hides the auto-generated progress diff on the update. `-q/--quiet` prints `<health> <url>`. A status update is not the same as a project **document** (`documents create --project`) — use a document for durable reference content, a project update for point-in-time progress.
|
|
553
|
+
|
|
520
554
|
### Discovery Before Creation
|
|
521
555
|
|
|
522
556
|
Always check if a project exists before creating: `el-linear projects list`.
|
package/dist/commands/config.js
CHANGED
|
@@ -33,6 +33,8 @@ function sourceLabel(source) {
|
|
|
33
33
|
return "teamConfigPath in personal config";
|
|
34
34
|
case "marker":
|
|
35
35
|
return "auto-discovered via ~/.config/el-tools-root";
|
|
36
|
+
case "disabled":
|
|
37
|
+
return 'disabled for this profile via teamConfigPath: "" in personal config';
|
|
36
38
|
case null:
|
|
37
39
|
return null;
|
|
38
40
|
}
|
package/dist/commands/issues.js
CHANGED
|
@@ -3,6 +3,7 @@ import { loadConfig } from "../config/config.js";
|
|
|
3
3
|
import { enrichProjectResolverError, enrichValidationErrors, } from "../config/error-enrichment.js";
|
|
4
4
|
import { enforceValidation, validateIssueCreation, } from "../config/issue-validation.js";
|
|
5
5
|
import { resolveAssignee, resolveLabels, resolveMemberWithRegistry, resolveTeam, } from "../config/resolver.js";
|
|
6
|
+
import { formatSopParentBlock, getSopLabelGateConfig, hasSopLabel, isUnresolvableReferenceError, } from "../config/sop-label-validation.js";
|
|
6
7
|
import { resolveDefaultStatus } from "../config/status-defaults.js";
|
|
7
8
|
import { enforceTerms } from "../config/term-enforcer.js";
|
|
8
9
|
import { GET_ISSUE_RELATIONS_QUERY, GET_ISSUE_STATE_HISTORY_QUERY, } from "../queries/issues.js";
|
|
@@ -544,6 +545,109 @@ async function enforceNoDuplicateIssue(title, options, issuesService) {
|
|
|
544
545
|
});
|
|
545
546
|
throw new Error(`Issue creation blocked: ${formatDuplicateBlock(matches)}`);
|
|
546
547
|
}
|
|
548
|
+
/**
|
|
549
|
+
* DEV-5378: create-time SOP-label parent gate. When the issue being created
|
|
550
|
+
* carries an SOP-type label, require that `--parent` or `--related-to` resolves
|
|
551
|
+
* to another SOP-labeled issue, and throw otherwise — the deterministic form of
|
|
552
|
+
* the kaizen skill's Stage-4 rule (parent ALL-1028). An SOP with no parent SOP
|
|
553
|
+
* is unfindable by `el-sop landscape` and breaks the catalog topology.
|
|
554
|
+
*
|
|
555
|
+
* OPT-IN: dormant unless `validation.sopLabelParentGate: true` (see
|
|
556
|
+
* {@link getSopLabelGateConfig}) — el-linear is MIT/open-source and "SOP" is an
|
|
557
|
+
* Enrich-Layer-specific taxonomy. Bypassed by `--skip-validation` (blanket) and
|
|
558
|
+
* the narrow `--allow-unparented-sop` (which records an `overridden` gate event,
|
|
559
|
+
* mirroring `--allow-duplicate`). The parent-label fetch is best-effort: when a
|
|
560
|
+
* referenced issue can't be resolved (network / not-found) the gate fails open
|
|
561
|
+
* with a warning rather than blocking legitimate creation on infra trouble — a
|
|
562
|
+
* genuinely resolvable non-SOP parent still hard-blocks.
|
|
563
|
+
*/
|
|
564
|
+
async function enforceSopLabelParent(labels, options, issuesService) {
|
|
565
|
+
if (options.skipValidation) {
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
const { enabled, sopLabels } = getSopLabelGateConfig();
|
|
569
|
+
if (!enabled) {
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
if (!hasSopLabel(labels, sopLabels)) {
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
const parentRefs = [
|
|
576
|
+
...(typeof options.parentTicket === "string" && options.parentTicket
|
|
577
|
+
? [options.parentTicket]
|
|
578
|
+
: []),
|
|
579
|
+
...(options.relatedTo ? splitList(options.relatedTo) : []),
|
|
580
|
+
];
|
|
581
|
+
// Record the decision so `el-telemetry gates` can compute override-rate,
|
|
582
|
+
// mirroring the dup gate (DEV-4834): `overridden` when the caller passed
|
|
583
|
+
// --allow-unparented-sop and we proceed, `blocked` when we stop creation.
|
|
584
|
+
const decide = async (reason, unresolvableRefs) => {
|
|
585
|
+
const gateEvent = { gate: "issues-create-sop-parent" };
|
|
586
|
+
if (options.allowUnparentedSop) {
|
|
587
|
+
await emitGateEvent("el-linear", "issues create", {
|
|
588
|
+
...gateEvent,
|
|
589
|
+
outcome: "overridden",
|
|
590
|
+
});
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
await emitGateEvent("el-linear", "issues create", {
|
|
594
|
+
...gateEvent,
|
|
595
|
+
outcome: "blocked",
|
|
596
|
+
});
|
|
597
|
+
throw new Error(`Issue creation blocked: ${formatSopParentBlock({
|
|
598
|
+
sopLabels,
|
|
599
|
+
reason,
|
|
600
|
+
parentRefs,
|
|
601
|
+
unresolvableRefs,
|
|
602
|
+
})}`);
|
|
603
|
+
};
|
|
604
|
+
// No parent/related at all → deterministic block (no fetch needed).
|
|
605
|
+
if (parentRefs.length === 0) {
|
|
606
|
+
await decide("no-parent");
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
// A referenced issue that resolves AND carries an SOP label satisfies the
|
|
610
|
+
// gate. Fetch per-ref (not batched) so one unresolvable ref doesn't sink a
|
|
611
|
+
// sibling that would have passed. Classify each failure: a clean
|
|
612
|
+
// not-found/malformed ref is an unresolvable reference (it contributes to a
|
|
613
|
+
// block — a typo must not slip through), a transport/service error is infra
|
|
614
|
+
// trouble (fail open so it can't block legitimate creation).
|
|
615
|
+
const unresolvableRefs = [];
|
|
616
|
+
let transportError = false;
|
|
617
|
+
for (const ref of parentRefs) {
|
|
618
|
+
try {
|
|
619
|
+
const issue = await issuesService.getIssueById(ref);
|
|
620
|
+
if (hasSopLabel(issue.labels.map((l) => l.name), sopLabels)) {
|
|
621
|
+
return; // valid SOP parent — pass
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
catch (err) {
|
|
625
|
+
if (isUnresolvableReferenceError(err)) {
|
|
626
|
+
unresolvableRefs.push(ref);
|
|
627
|
+
}
|
|
628
|
+
else {
|
|
629
|
+
transportError = true;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
// No SOP parent was confirmed. A transport failure may have hidden the real
|
|
634
|
+
// SOP parent → fail open (best-effort, like the dup gate on a search
|
|
635
|
+
// failure) and record it so the degradation is measurable (DEV-5378).
|
|
636
|
+
if (transportError) {
|
|
637
|
+
await emitGateEvent("el-linear", "issues create", {
|
|
638
|
+
gate: "issues-create-sop-parent",
|
|
639
|
+
outcome: "fail-open",
|
|
640
|
+
});
|
|
641
|
+
outputWarning(`SOP-parent check could not verify a parent SOP for ${parentRefs.join(", ")} due to a service error; proceeding without blocking.${options.allowUnparentedSop
|
|
642
|
+
? ""
|
|
643
|
+
: " Pass --allow-unparented-sop to silence."}`);
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
// Every reference either resolved to a non-SOP issue or cleanly failed to
|
|
647
|
+
// resolve (typo / nonexistent) — none is a valid SOP parent → block, naming
|
|
648
|
+
// any unresolvable refs so a typo reads differently from a real non-SOP parent.
|
|
649
|
+
await decide("no-sop-parent", unresolvableRefs.length > 0 ? unresolvableRefs : undefined);
|
|
650
|
+
}
|
|
547
651
|
async function handleCreateIssue(title, options, command) {
|
|
548
652
|
const rootOpts = getRootOpts(command);
|
|
549
653
|
const { teamInput, teamId, assigneeId, delegateId, labelIds, status, subscriberIds, priority, } = await resolveCreateInputs(title ?? "", options, rootOpts);
|
|
@@ -572,6 +676,15 @@ async function handleCreateIssue(title, options, command) {
|
|
|
572
676
|
if (title) {
|
|
573
677
|
await enforceNoDuplicateIssue(title, options, issuesService);
|
|
574
678
|
}
|
|
679
|
+
// DEV-5378: deterministic SOP-label parent gate. Opt-in (dormant unless
|
|
680
|
+
// validation.sopLabelParentGate). Runs on the post-normalization labels
|
|
681
|
+
// (resolveCreateInputs rewrote options.labels to the canonical set) and,
|
|
682
|
+
// when the issue is SOP-labeled, blocks unless --parent/--related-to points
|
|
683
|
+
// at another SOP-labeled issue. Independent of title, so it runs even on the
|
|
684
|
+
// --from-template path — though a template that supplies labels server-side
|
|
685
|
+
// leaves options.labels undefined here, in which case the gate sees no SOP
|
|
686
|
+
// label and no-ops (same client-side-visibility gap as the dup gate).
|
|
687
|
+
await enforceSopLabelParent(options.labels ? splitList(options.labels) : [], options, issuesService);
|
|
575
688
|
// Wrap valid issue identifiers as markdown links before creating, so the description
|
|
576
689
|
// saved on Linear has clickable refs from the start. Self-reference can't apply here
|
|
577
690
|
// because the issue doesn't exist yet — pass undefined.
|
|
@@ -1137,8 +1250,9 @@ export function setupIssuesCommands(program) {
|
|
|
1137
1250
|
.option("--due-date <date>", "due date (YYYY-MM-DD)")
|
|
1138
1251
|
.option("--checkout", "create and checkout a git branch named after the issue")
|
|
1139
1252
|
.option("--no-claim", "with --checkout, skip assigning the issue to the current Linear user and moving it to the first started state")
|
|
1140
|
-
.option("--skip-validation", "skip all validation (labels, description, assignee, project, duplicate detection)")
|
|
1253
|
+
.option("--skip-validation", "skip all validation (labels, description, assignee, project, duplicate detection, SOP-parent gate)")
|
|
1141
1254
|
.option("--allow-duplicate", "skip the duplicate-detection gate and create even if a similar issue already exists")
|
|
1255
|
+
.option("--allow-unparented-sop", "skip the SOP-label parent gate and create an SOP-labeled issue without a parent SOP")
|
|
1142
1256
|
.option("--no-auto-link", "skip auto-linking issue references found in the description")
|
|
1143
1257
|
.option("--footer <text>", "text appended to the description (overrides config.messageFooter)")
|
|
1144
1258
|
.option("--no-footer", "skip the configured messageFooter for this issue")
|
package/dist/config/config.d.ts
CHANGED
|
@@ -7,9 +7,11 @@ import type { TermRule } from "./term-enforcer.js";
|
|
|
7
7
|
* - `env`: `EL_LINEAR_TEAM_CONFIG` is set in the environment (highest).
|
|
8
8
|
* - `personal`: `teamConfigPath` field in personal `config.json`.
|
|
9
9
|
* - `marker`: auto-discovered via `~/.config/el-tools-root` (DEV-4258).
|
|
10
|
+
* - `disabled`: explicitly opted out via `teamConfigPath: ""` in personal
|
|
11
|
+
* config — the layer is off even when the marker would otherwise apply.
|
|
10
12
|
* - `null`: no team layer active.
|
|
11
13
|
*/
|
|
12
|
-
export type TeamConfigSource = "env" | "personal" | "marker" | null;
|
|
14
|
+
export type TeamConfigSource = "env" | "personal" | "marker" | "disabled" | null;
|
|
13
15
|
/**
|
|
14
16
|
* Canonical shape of `~/.config/el-linear/config.json`. This is the single source
|
|
15
17
|
* of truth for the on-disk config — the wizard reads/writes the same shape
|
|
@@ -67,6 +69,21 @@ export interface ElLinearConfig {
|
|
|
67
69
|
* `DEFAULT_DUPLICATE_THRESHOLD` (0.35). Lower = more aggressive.
|
|
68
70
|
*/
|
|
69
71
|
duplicateThreshold?: number;
|
|
72
|
+
/**
|
|
73
|
+
* OPT-IN SOP-label parent gate (DEV-5378). When `true`, `issues create`
|
|
74
|
+
* requires an issue carrying an SOP-type label (see `sopLabels`) to point
|
|
75
|
+
* at a parent SOP via `--parent` or `--related-to`, blocking otherwise.
|
|
76
|
+
* Defaults to `false` (dormant) — el-linear is MIT/open-source and "SOP"
|
|
77
|
+
* is an Enrich-Layer-specific taxonomy, so a fresh install stays silent
|
|
78
|
+
* until a workspace opts in (the EL shared team config flips it on).
|
|
79
|
+
* Bypass a single create with `--allow-unparented-sop`.
|
|
80
|
+
*/
|
|
81
|
+
sopLabelParentGate?: boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Label names (matched case-insensitively) that mark an issue as an SOP
|
|
84
|
+
* for the `sopLabelParentGate` check. Defaults to `["SOP"]`.
|
|
85
|
+
*/
|
|
86
|
+
sopLabels?: string[];
|
|
70
87
|
};
|
|
71
88
|
/**
|
|
72
89
|
* Optional override for the Linear workspace URL key (the part after
|
|
@@ -127,6 +144,15 @@ export interface ElLinearConfig {
|
|
|
127
144
|
* maps, and term rules across a team by checking the file into a shared
|
|
128
145
|
* repository. Override at runtime with the `EL_LINEAR_TEAM_CONFIG` env var
|
|
129
146
|
* (env var takes precedence over this field).
|
|
147
|
+
*
|
|
148
|
+
* Set to the empty string (`""`) to explicitly DISABLE the team layer for
|
|
149
|
+
* this profile, including `~/.config/el-tools-root` marker auto-discovery.
|
|
150
|
+
* This is the multi-account escape hatch: a marker-discovered team config
|
|
151
|
+
* carries workspace-specific member/label UUIDs, and applying it to a
|
|
152
|
+
* profile pointed at a different Linear workspace makes `--assignee <name>`
|
|
153
|
+
* resolve to a foreign UUID that the API rejects with an opaque
|
|
154
|
+
* "Entity not found in validateAccess" error. Profiles for secondary
|
|
155
|
+
* workspaces should set `"teamConfigPath": ""`.
|
|
130
156
|
*/
|
|
131
157
|
teamConfigPath?: string;
|
|
132
158
|
}
|
package/dist/config/config.js
CHANGED
|
@@ -112,6 +112,10 @@ export function loadConfig() {
|
|
|
112
112
|
// The marker is the opt-in consent signal — only developers who ran
|
|
113
113
|
// `link-project.sh` get the auto-discovery; OSS users hit the silent
|
|
114
114
|
// no-op path. DEV-4258 / ALL-964.
|
|
115
|
+
//
|
|
116
|
+
// An EMPTY string is a deliberate per-profile opt-out (see the
|
|
117
|
+
// `teamConfigPath` JSDoc): it is defined, so marker discovery is
|
|
118
|
+
// skipped, and falsy, so no team layer is loaded below.
|
|
115
119
|
personalRaw = readRawPersonalConfig(active);
|
|
116
120
|
teamConfigPath = personalRaw.teamConfigPath;
|
|
117
121
|
if (teamConfigPath === undefined) {
|
|
@@ -181,7 +185,14 @@ export function getActiveTeamConfigInfo() {
|
|
|
181
185
|
}
|
|
182
186
|
const personalRaw = readRawPersonalConfig(resolveActiveProfile());
|
|
183
187
|
const personalPath = personalRaw.teamConfigPath;
|
|
184
|
-
if (personalPath
|
|
188
|
+
if (personalPath === "") {
|
|
189
|
+
// Explicit per-profile opt-out — must be attributed as such, and must
|
|
190
|
+
// NOT fall through to the marker: loadConfig skips marker discovery
|
|
191
|
+
// for a defined-but-empty path, so reporting "marker" here would
|
|
192
|
+
// contradict what actually loads.
|
|
193
|
+
return { path: undefined, source: "disabled" };
|
|
194
|
+
}
|
|
195
|
+
if (personalPath !== undefined) {
|
|
185
196
|
return { path: personalPath, source: "personal" };
|
|
186
197
|
}
|
|
187
198
|
const markerPath = discoverTeamConfigViaMarker();
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SOP-label parent validation — DEV-5378.
|
|
3
|
+
*
|
|
4
|
+
* The el-linear side of the SOP-system enforcement (parent ALL-1028). When an
|
|
5
|
+
* issue carries an SOP-type label, it must point at a parent SOP so the
|
|
6
|
+
* `el-sop landscape` catalog topology stays connected — an SOP with no parent
|
|
7
|
+
* SOP is unfindable and breaks the tree. This turns the kaizen skill's Stage-4
|
|
8
|
+
* "always give an SOP issue a parent SOP" prose rule into a deterministic
|
|
9
|
+
* create-time gate, mirroring the DEV-4823 duplicate-detection gate.
|
|
10
|
+
*
|
|
11
|
+
* OPT-IN by design. el-linear is MIT and published on npm; most installs are
|
|
12
|
+
* not Enrich Layer and have no SOP taxonomy. The gate is dormant unless
|
|
13
|
+
* `validation.sopLabelParentGate: true` is set (the EL workspace flips it on in
|
|
14
|
+
* its shared team config). This is the opposite of the duplicate-detection gate,
|
|
15
|
+
* which defaults on — but the dup check is workspace-agnostic, whereas "SOP" is
|
|
16
|
+
* an Enrich-Layer-specific label taxonomy that a fresh OSS install must not be
|
|
17
|
+
* surprised by.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Default label names that mark an issue as an SOP. Overridable via
|
|
21
|
+
* `config.validation.sopLabels`. Matched case-insensitively against the
|
|
22
|
+
* issue's labels.
|
|
23
|
+
*/
|
|
24
|
+
export declare const DEFAULT_SOP_LABELS: string[];
|
|
25
|
+
export interface SopLabelGateConfig {
|
|
26
|
+
/**
|
|
27
|
+
* Whether the gate is active. OPT-IN: only true when validation is not
|
|
28
|
+
* turned off AND `sopLabelParentGate` is explicitly `true`.
|
|
29
|
+
*/
|
|
30
|
+
enabled: boolean;
|
|
31
|
+
/** The SOP label names in effect (config override or {@link DEFAULT_SOP_LABELS}). */
|
|
32
|
+
sopLabels: string[];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Resolve the SOP-label-parent-gate config from the merged el-linear config.
|
|
36
|
+
*
|
|
37
|
+
* The gate is dormant by default. It activates only when validation isn't
|
|
38
|
+
* disabled (`validation.enabled !== false`) AND the operator has explicitly set
|
|
39
|
+
* `validation.sopLabelParentGate: true`. An absent or empty `sopLabels` falls
|
|
40
|
+
* back to {@link DEFAULT_SOP_LABELS}.
|
|
41
|
+
*/
|
|
42
|
+
export declare function getSopLabelGateConfig(): SopLabelGateConfig;
|
|
43
|
+
/**
|
|
44
|
+
* Case-insensitive membership test: does `labels` contain any of the configured
|
|
45
|
+
* SOP label names? Returns false for an empty `labels` set.
|
|
46
|
+
*/
|
|
47
|
+
export declare function hasSopLabel(labels: string[], sopLabels: string[]): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Distinguish a reference that cleanly does NOT resolve to a real issue (a
|
|
50
|
+
* typo'd or nonexistent `--parent` / `--related-to`) from a transport/service
|
|
51
|
+
* failure (network, GraphQL 5xx, timeout).
|
|
52
|
+
*
|
|
53
|
+
* `getIssueById` throws a plain `Error` for both — this codebase has no error
|
|
54
|
+
* `code` or subclass to switch on — so we key on the two message families it
|
|
55
|
+
* produces for an *unresolvable reference*:
|
|
56
|
+
* - `notFoundError` → `… not found.` (well-formed ref, no such issue)
|
|
57
|
+
* - `parseIssueIdentifier` → `Invalid issue identifier format: …` /
|
|
58
|
+
* `Invalid issue number in identifier: …` (malformed ref)
|
|
59
|
+
* Everything else — notably `GraphQL request failed: …` / `GraphQL query
|
|
60
|
+
* failed` from `graphql-service` — is treated as transport and fails open.
|
|
61
|
+
*
|
|
62
|
+
* This is a correctness distinction, not just telemetry: an unresolvable
|
|
63
|
+
* reference must BLOCK. Otherwise a typo'd `--related-to` on an SOP issue fails
|
|
64
|
+
* open, the issue is created, and the *follow-up* `createRelations` throws —
|
|
65
|
+
* leaving an orphan SOP on the board (the exact gap DEV-5378 cycle-1 caught). A
|
|
66
|
+
* transport error must FAIL OPEN so infra trouble can't block legitimate work.
|
|
67
|
+
*/
|
|
68
|
+
export declare function isUnresolvableReferenceError(err: unknown): boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Render the human/agent-facing block thrown when an SOP-labeled issue has no
|
|
71
|
+
* SOP parent. `reason` distinguishes "no parent at all" from "one or more
|
|
72
|
+
* references present but none resolves to an SOP-labeled issue", so the message
|
|
73
|
+
* points at the exact fix. When some references couldn't be resolved at all,
|
|
74
|
+
* `unresolvableRefs` names them (a typo'd ref reads differently from a real but
|
|
75
|
+
* non-SOP parent). Names the rule and the `--allow-unparented-sop` escape hatch.
|
|
76
|
+
*/
|
|
77
|
+
export declare function formatSopParentBlock(opts: {
|
|
78
|
+
sopLabels: string[];
|
|
79
|
+
reason: "no-parent" | "no-sop-parent";
|
|
80
|
+
parentRefs: string[];
|
|
81
|
+
unresolvableRefs?: string[];
|
|
82
|
+
}): string;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SOP-label parent validation — DEV-5378.
|
|
3
|
+
*
|
|
4
|
+
* The el-linear side of the SOP-system enforcement (parent ALL-1028). When an
|
|
5
|
+
* issue carries an SOP-type label, it must point at a parent SOP so the
|
|
6
|
+
* `el-sop landscape` catalog topology stays connected — an SOP with no parent
|
|
7
|
+
* SOP is unfindable and breaks the tree. This turns the kaizen skill's Stage-4
|
|
8
|
+
* "always give an SOP issue a parent SOP" prose rule into a deterministic
|
|
9
|
+
* create-time gate, mirroring the DEV-4823 duplicate-detection gate.
|
|
10
|
+
*
|
|
11
|
+
* OPT-IN by design. el-linear is MIT and published on npm; most installs are
|
|
12
|
+
* not Enrich Layer and have no SOP taxonomy. The gate is dormant unless
|
|
13
|
+
* `validation.sopLabelParentGate: true` is set (the EL workspace flips it on in
|
|
14
|
+
* its shared team config). This is the opposite of the duplicate-detection gate,
|
|
15
|
+
* which defaults on — but the dup check is workspace-agnostic, whereas "SOP" is
|
|
16
|
+
* an Enrich-Layer-specific label taxonomy that a fresh OSS install must not be
|
|
17
|
+
* surprised by.
|
|
18
|
+
*/
|
|
19
|
+
import { loadConfig } from "./config.js";
|
|
20
|
+
/**
|
|
21
|
+
* Default label names that mark an issue as an SOP. Overridable via
|
|
22
|
+
* `config.validation.sopLabels`. Matched case-insensitively against the
|
|
23
|
+
* issue's labels.
|
|
24
|
+
*/
|
|
25
|
+
export const DEFAULT_SOP_LABELS = ["SOP"];
|
|
26
|
+
/**
|
|
27
|
+
* Resolve the SOP-label-parent-gate config from the merged el-linear config.
|
|
28
|
+
*
|
|
29
|
+
* The gate is dormant by default. It activates only when validation isn't
|
|
30
|
+
* disabled (`validation.enabled !== false`) AND the operator has explicitly set
|
|
31
|
+
* `validation.sopLabelParentGate: true`. An absent or empty `sopLabels` falls
|
|
32
|
+
* back to {@link DEFAULT_SOP_LABELS}.
|
|
33
|
+
*/
|
|
34
|
+
export function getSopLabelGateConfig() {
|
|
35
|
+
const validation = loadConfig().validation;
|
|
36
|
+
const enabled = validation?.enabled !== false && validation?.sopLabelParentGate === true;
|
|
37
|
+
const sopLabels = validation?.sopLabels && validation.sopLabels.length > 0
|
|
38
|
+
? validation.sopLabels
|
|
39
|
+
: DEFAULT_SOP_LABELS;
|
|
40
|
+
return { enabled, sopLabels };
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Case-insensitive membership test: does `labels` contain any of the configured
|
|
44
|
+
* SOP label names? Returns false for an empty `labels` set.
|
|
45
|
+
*/
|
|
46
|
+
export function hasSopLabel(labels, sopLabels) {
|
|
47
|
+
const wanted = new Set(sopLabels.map((l) => l.toLowerCase()));
|
|
48
|
+
return labels.some((l) => wanted.has(l.toLowerCase()));
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Distinguish a reference that cleanly does NOT resolve to a real issue (a
|
|
52
|
+
* typo'd or nonexistent `--parent` / `--related-to`) from a transport/service
|
|
53
|
+
* failure (network, GraphQL 5xx, timeout).
|
|
54
|
+
*
|
|
55
|
+
* `getIssueById` throws a plain `Error` for both — this codebase has no error
|
|
56
|
+
* `code` or subclass to switch on — so we key on the two message families it
|
|
57
|
+
* produces for an *unresolvable reference*:
|
|
58
|
+
* - `notFoundError` → `… not found.` (well-formed ref, no such issue)
|
|
59
|
+
* - `parseIssueIdentifier` → `Invalid issue identifier format: …` /
|
|
60
|
+
* `Invalid issue number in identifier: …` (malformed ref)
|
|
61
|
+
* Everything else — notably `GraphQL request failed: …` / `GraphQL query
|
|
62
|
+
* failed` from `graphql-service` — is treated as transport and fails open.
|
|
63
|
+
*
|
|
64
|
+
* This is a correctness distinction, not just telemetry: an unresolvable
|
|
65
|
+
* reference must BLOCK. Otherwise a typo'd `--related-to` on an SOP issue fails
|
|
66
|
+
* open, the issue is created, and the *follow-up* `createRelations` throws —
|
|
67
|
+
* leaving an orphan SOP on the board (the exact gap DEV-5378 cycle-1 caught). A
|
|
68
|
+
* transport error must FAIL OPEN so infra trouble can't block legitimate work.
|
|
69
|
+
*/
|
|
70
|
+
export function isUnresolvableReferenceError(err) {
|
|
71
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
72
|
+
return (message.includes("not found.") ||
|
|
73
|
+
message.includes("Invalid issue identifier format") ||
|
|
74
|
+
message.includes("Invalid issue number in identifier"));
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Render the human/agent-facing block thrown when an SOP-labeled issue has no
|
|
78
|
+
* SOP parent. `reason` distinguishes "no parent at all" from "one or more
|
|
79
|
+
* references present but none resolves to an SOP-labeled issue", so the message
|
|
80
|
+
* points at the exact fix. When some references couldn't be resolved at all,
|
|
81
|
+
* `unresolvableRefs` names them (a typo'd ref reads differently from a real but
|
|
82
|
+
* non-SOP parent). Names the rule and the `--allow-unparented-sop` escape hatch.
|
|
83
|
+
*/
|
|
84
|
+
export function formatSopParentBlock(opts) {
|
|
85
|
+
const sopList = opts.sopLabels.join(", ");
|
|
86
|
+
const head = `SOP-labeled issue must point at a parent SOP (SOP label(s): ${sopList}).\n` +
|
|
87
|
+
" An SOP with no parent SOP is unfindable by `el-sop landscape` and breaks the catalog topology.\n";
|
|
88
|
+
let detail;
|
|
89
|
+
if (opts.reason === "no-parent") {
|
|
90
|
+
detail =
|
|
91
|
+
" This issue has no --parent or --related-to. Add one that points at an SOP-labeled issue.\n";
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
detail = ` No referenced issue resolves to an SOP-labeled issue (referenced: ${opts.parentRefs.join(", ")}).\n`;
|
|
95
|
+
if (opts.unresolvableRefs && opts.unresolvableRefs.length > 0) {
|
|
96
|
+
detail += ` Could not resolve: ${opts.unresolvableRefs.join(", ")} — check the identifier exists.\n`;
|
|
97
|
+
}
|
|
98
|
+
detail +=
|
|
99
|
+
" Point --parent or --related-to at an existing SOP-labeled issue.\n";
|
|
100
|
+
}
|
|
101
|
+
const hatch = " If this SOP is intentionally top-level, re-run with --allow-unparented-sop.";
|
|
102
|
+
return head + detail + hatch;
|
|
103
|
+
}
|
package/dist/queries/common.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const COMPLETE_ISSUE_FRAGMENT = "\n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n\n \n state {\n id\n name\n type\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n";
|
|
2
|
-
export declare const COMPLETE_ISSUE_WITH_COMMENTS_FRAGMENT = "\n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n\n \n state {\n id\n name\n type\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n \n comments {\n nodes {\n id\n body\n createdAt\n updatedAt\n user {\n id\n name\n url\n }\n }\n }\n\n";
|
|
1
|
+
export declare const COMPLETE_ISSUE_FRAGMENT = "\n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n completedAt\n\n \n state {\n id\n name\n type\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n";
|
|
2
|
+
export declare const COMPLETE_ISSUE_WITH_COMMENTS_FRAGMENT = "\n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n completedAt\n\n \n state {\n id\n name\n type\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n \n comments {\n nodes {\n id\n body\n createdAt\n updatedAt\n user {\n id\n name\n url\n }\n }\n }\n\n";
|
package/dist/queries/common.js
CHANGED
|
@@ -23,6 +23,10 @@ export const DEFAULT_TREE_DEPTH = 3;
|
|
|
23
23
|
// Field set per tree node — kept minimal so the depth-N query stays small.
|
|
24
24
|
// `state.type` is required client-side for `--no-include-closed` pruning;
|
|
25
25
|
// `state.name` for the [Done]/[Canceled] suffix on terminal nodes.
|
|
26
|
+
// `completedAt` is the moment a node reached a terminal state (null while
|
|
27
|
+
// open) — consumers that reason about how long a subtree has been finished
|
|
28
|
+
// (e.g. el-sop health's completed-children grace period, DEV-5454) need the
|
|
29
|
+
// per-child timestamp, which no other field carries.
|
|
26
30
|
// `priority` was intentionally dropped after cycle-1: the ASCII formatter
|
|
27
31
|
// doesn't render it and including it inflates the query string by ~N nodes.
|
|
28
32
|
const TREE_NODE_FIELDS = `
|
|
@@ -31,6 +35,7 @@ const TREE_NODE_FIELDS = `
|
|
|
31
35
|
title
|
|
32
36
|
state { id name type }
|
|
33
37
|
assignee { id name }
|
|
38
|
+
completedAt
|
|
34
39
|
`;
|
|
35
40
|
/**
|
|
36
41
|
* Recursive children fragment generator. `depth=0` emits nothing (no
|
package/dist/queries/issues.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export declare const GET_ISSUES_QUERY = "\n query GetIssues($first: Int!, $orderBy: PaginationOrderBy) {\n issues(\n first: $first\n orderBy: $orderBy\n filter: {\n state: { type: { neq: \"completed\" } }\n }\n ) {\n nodes {\n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n\n \n state {\n id\n name\n type\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n }\n }\n }\n";
|
|
2
|
-
export declare const SEARCH_ISSUES_QUERY = "\n query SearchIssues($term: String!, $first: Int!) {\n searchIssues(term: $term, first: $first, includeArchived: false) {\n nodes {\n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n\n \n state {\n id\n name\n type\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n }\n }\n }\n";
|
|
3
|
-
export declare const FILTERED_SEARCH_ISSUES_QUERY = "\n query FilteredSearchIssues(\n $first: Int!\n $filter: IssueFilter\n $orderBy: PaginationOrderBy\n ) {\n issues(\n first: $first\n filter: $filter\n orderBy: $orderBy\n includeArchived: false\n ) {\n nodes {\n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n\n \n state {\n id\n name\n type\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n }\n }\n }\n";
|
|
1
|
+
export declare const GET_ISSUES_QUERY = "\n query GetIssues($first: Int!, $orderBy: PaginationOrderBy) {\n issues(\n first: $first\n orderBy: $orderBy\n filter: {\n state: { type: { neq: \"completed\" } }\n }\n ) {\n nodes {\n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n completedAt\n\n \n state {\n id\n name\n type\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n }\n }\n }\n";
|
|
2
|
+
export declare const SEARCH_ISSUES_QUERY = "\n query SearchIssues($term: String!, $first: Int!) {\n searchIssues(term: $term, first: $first, includeArchived: false) {\n nodes {\n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n completedAt\n\n \n state {\n id\n name\n type\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n }\n }\n }\n";
|
|
3
|
+
export declare const FILTERED_SEARCH_ISSUES_QUERY = "\n query FilteredSearchIssues(\n $first: Int!\n $filter: IssueFilter\n $orderBy: PaginationOrderBy\n ) {\n issues(\n first: $first\n filter: $filter\n orderBy: $orderBy\n includeArchived: false\n ) {\n nodes {\n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n completedAt\n\n \n state {\n id\n name\n type\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n }\n }\n }\n";
|
|
4
4
|
/**
|
|
5
5
|
* Batch-resolves a search's team/project/assignee/delegate filter inputs.
|
|
6
6
|
*
|
|
@@ -11,8 +11,8 @@ export declare const FILTERED_SEARCH_ISSUES_QUERY = "\n query FilteredSearchIss
|
|
|
11
11
|
* name-resolution arm is needed here.)
|
|
12
12
|
*/
|
|
13
13
|
export declare const BATCH_RESOLVE_FOR_SEARCH_QUERY = "\n query BatchResolveForSearch(\n $teamKey: String\n $teamName: String\n $projectName: String\n $hasProjectName: Boolean = false\n $assigneeEmail: String\n $delegateEmail: String\n ) {\n teams(\n filter: {\n or: [\n { key: { eq: $teamKey } }\n { name: { eqIgnoreCase: $teamName } }\n ]\n }\n first: 1\n ) {\n nodes {\n id\n key\n name\n }\n }\n\n projects(\n filter: { name: { eqIgnoreCase: $projectName } }\n first: 1\n ) @include(if: $hasProjectName) {\n nodes {\n id\n name\n }\n }\n\n users(filter: { email: { eq: $assigneeEmail } }, first: 1) {\n nodes {\n id\n name\n email\n }\n }\n\n delegates: users(filter: { email: { eq: $delegateEmail } }, first: 1) {\n nodes {\n id\n name\n email\n }\n }\n }\n";
|
|
14
|
-
export declare const GET_ISSUE_BY_ID_QUERY = "\n query GetIssue($id: String!) {\n issue(id: $id) {\n \n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n\n \n state {\n id\n name\n type\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n \n comments {\n nodes {\n id\n body\n createdAt\n updatedAt\n user {\n id\n name\n url\n }\n }\n }\n\n\n }\n }\n";
|
|
15
|
-
export declare const GET_ISSUE_BY_IDENTIFIER_QUERY = "\n query GetIssueByIdentifier($teamKey: String!, $number: Float!) {\n issues(\n filter: {\n team: { key: { eq: $teamKey } }\n number: { eq: $number }\n }\n first: 1\n ) {\n nodes {\n \n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n\n \n state {\n id\n name\n type\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n \n comments {\n nodes {\n id\n body\n createdAt\n updatedAt\n user {\n id\n name\n url\n }\n }\n }\n\n\n }\n }\n }\n";
|
|
14
|
+
export declare const GET_ISSUE_BY_ID_QUERY = "\n query GetIssue($id: String!) {\n issue(id: $id) {\n \n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n completedAt\n\n \n state {\n id\n name\n type\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n \n comments {\n nodes {\n id\n body\n createdAt\n updatedAt\n user {\n id\n name\n url\n }\n }\n }\n\n\n }\n }\n";
|
|
15
|
+
export declare const GET_ISSUE_BY_IDENTIFIER_QUERY = "\n query GetIssueByIdentifier($teamKey: String!, $number: Float!) {\n issues(\n filter: {\n team: { key: { eq: $teamKey } }\n number: { eq: $number }\n }\n first: 1\n ) {\n nodes {\n \n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n completedAt\n\n \n state {\n id\n name\n type\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n \n comments {\n nodes {\n id\n body\n createdAt\n updatedAt\n user {\n id\n name\n url\n }\n }\n }\n\n\n }\n }\n }\n";
|
|
16
16
|
/**
|
|
17
17
|
* Batch issue fetch. Used by `issues read <id...>` (DEV-4477) to collapse
|
|
18
18
|
* N parallel single-issue queries into one round-trip. The `$filter` is
|
|
@@ -34,7 +34,7 @@ export declare const GET_ISSUE_BY_IDENTIFIER_QUERY = "\n query GetIssueByIdenti
|
|
|
34
34
|
* CLAUDE.md ("Performance-critical loops where the SDK's per-edge
|
|
35
35
|
* resolver promises produce N+1 round-trips") explicitly authorizes this.
|
|
36
36
|
*/
|
|
37
|
-
export declare const BATCH_GET_ISSUES_QUERY = "\n query BatchGetIssues($filter: IssueFilter!, $first: Int!) {\n issues(filter: $filter, first: $first) {\n nodes {\n \n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n\n \n state {\n id\n name\n type\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n \n comments {\n nodes {\n id\n body\n createdAt\n updatedAt\n user {\n id\n name\n url\n }\n }\n }\n\n\n }\n }\n }\n";
|
|
37
|
+
export declare const BATCH_GET_ISSUES_QUERY = "\n query BatchGetIssues($filter: IssueFilter!, $first: Int!) {\n issues(filter: $filter, first: $first) {\n nodes {\n \n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n completedAt\n\n \n state {\n id\n name\n type\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n \n comments {\n nodes {\n id\n body\n createdAt\n updatedAt\n user {\n id\n name\n url\n }\n }\n }\n\n\n }\n }\n }\n";
|
|
38
38
|
/**
|
|
39
39
|
* Batch-resolves an update's project/milestone/issue inputs.
|
|
40
40
|
*
|
|
@@ -45,8 +45,8 @@ export declare const BATCH_GET_ISSUES_QUERY = "\n query BatchGetIssues($filter:
|
|
|
45
45
|
* `--project-milestone` name resolution; a name uses `projectsByName`.
|
|
46
46
|
*/
|
|
47
47
|
export declare const BATCH_RESOLVE_FOR_UPDATE_QUERY = "\n query BatchResolveForUpdate(\n $projectName: String\n $projectId: ID\n $hasProjectName: Boolean = false\n $hasProjectId: Boolean = false\n $teamKey: String\n $issueNumber: Float\n $milestoneName: String\n $hasMilestoneName: Boolean = false\n ) {\n projectsByName: projects(\n filter: { name: { eqIgnoreCase: $projectName } }\n first: 5\n ) @include(if: $hasProjectName) {\n nodes {\n id\n name\n teams {\n nodes { id key }\n }\n projectMilestones {\n nodes {\n id\n name\n }\n }\n }\n }\n\n projectsById: projects(\n filter: { id: { eq: $projectId } }\n first: 1\n ) @include(if: $hasProjectId) {\n nodes {\n id\n name\n projectMilestones {\n nodes {\n id\n name\n }\n }\n }\n }\n\n milestones: projectMilestones(\n filter: { name: { eq: $milestoneName } }\n first: 1\n ) @include(if: $hasMilestoneName) {\n nodes {\n id\n name\n }\n }\n\n issues(\n filter: {\n and: [\n { team: { key: { eq: $teamKey } } }\n { number: { eq: $issueNumber } }\n ]\n }\n first: 1\n ) {\n nodes {\n id\n identifier\n team {\n id\n key\n }\n labels {\n nodes {\n id\n name\n }\n }\n project {\n id\n projectMilestones {\n nodes {\n id\n name\n }\n }\n }\n }\n }\n }\n";
|
|
48
|
-
export declare const CREATE_ISSUE_MUTATION = "\n mutation CreateIssue($input: IssueCreateInput!) {\n issueCreate(input: $input) {\n success\n issue {\n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n\n \n state {\n id\n name\n type\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n }\n }\n }\n";
|
|
49
|
-
export declare const UPDATE_ISSUE_MUTATION = "\n mutation UpdateIssue($id: String!, $input: IssueUpdateInput!) {\n issueUpdate(id: $id, input: $input) {\n success\n issue {\n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n\n \n state {\n id\n name\n type\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n }\n }\n }\n";
|
|
48
|
+
export declare const CREATE_ISSUE_MUTATION = "\n mutation CreateIssue($input: IssueCreateInput!) {\n issueCreate(input: $input) {\n success\n issue {\n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n completedAt\n\n \n state {\n id\n name\n type\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n }\n }\n }\n";
|
|
49
|
+
export declare const UPDATE_ISSUE_MUTATION = "\n mutation UpdateIssue($id: String!, $input: IssueUpdateInput!) {\n issueUpdate(id: $id, input: $input) {\n success\n issue {\n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n completedAt\n\n \n state {\n id\n name\n type\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n }\n }\n }\n";
|
|
50
50
|
export declare const ARCHIVE_ISSUE_MUTATION = "\n mutation ArchiveIssue($id: String!) {\n issueArchive(id: $id) {\n success\n lastSyncId\n entity {\n id\n }\n }\n }\n";
|
|
51
51
|
export declare const DELETE_ISSUE_MUTATION = "\n mutation DeleteIssue($id: String!, $permanentlyDelete: Boolean) {\n issueDelete(id: $id, permanentlyDelete: $permanentlyDelete) {\n success\n lastSyncId\n entity {\n id\n }\n }\n }\n";
|
|
52
52
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export declare const LIST_PROJECT_MILESTONES_QUERY = "\n query ListProjectMilestones($projectId: String!, $first: Int!) {\n project(id: $projectId) {\n id\n name\n projectMilestones(first: $first) {\n nodes {\n id\n name\n description\n targetDate\n sortOrder\n createdAt\n updatedAt\n }\n }\n }\n }\n";
|
|
2
|
-
export declare const GET_PROJECT_MILESTONE_BY_ID_QUERY = "\n query GetProjectMilestone($id: String!, $issuesFirst: Int) {\n projectMilestone(id: $id) {\n id\n name\n description\n targetDate\n sortOrder\n createdAt\n updatedAt\n project {\n id\n name\n }\n issues(first: $issuesFirst) {\n nodes {\n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n\n \n state {\n id\n name\n type\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n }\n }\n }\n }\n";
|
|
2
|
+
export declare const GET_PROJECT_MILESTONE_BY_ID_QUERY = "\n query GetProjectMilestone($id: String!, $issuesFirst: Int) {\n projectMilestone(id: $id) {\n id\n name\n description\n targetDate\n sortOrder\n createdAt\n updatedAt\n project {\n id\n name\n }\n issues(first: $issuesFirst) {\n nodes {\n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n completedAt\n\n \n state {\n id\n name\n type\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n }\n }\n }\n }\n";
|
|
3
3
|
export declare const FIND_PROJECT_MILESTONE_BY_NAME_SCOPED = "\n query FindProjectMilestoneScoped($name: String!, $projectId: String!) {\n project(id: $projectId) {\n projectMilestones(filter: { name: { eq: $name } }, first: 10) {\n nodes {\n id\n name\n targetDate\n sortOrder\n project {\n id\n name\n }\n }\n }\n }\n }\n";
|
|
4
4
|
export declare const FIND_PROJECT_MILESTONE_BY_NAME_GLOBAL = "\n query FindProjectMilestoneGlobal($name: String!) {\n projectMilestones(filter: { name: { eq: $name } }, first: 10) {\n nodes {\n id\n name\n targetDate\n sortOrder\n project {\n id\n name\n }\n }\n }\n }\n";
|
|
5
5
|
export declare const CREATE_PROJECT_MILESTONE_MUTATION = "\n mutation CreateProjectMilestone($projectId: String!, $name: String!, $description: String, $targetDate: TimelessDate) {\n projectMilestoneCreate(input: {\n projectId: $projectId\n name: $name\n description: $description\n targetDate: $targetDate\n }) {\n success\n projectMilestone {\n id\n name\n description\n targetDate\n sortOrder\n createdAt\n updatedAt\n project {\n id\n name\n }\n }\n }\n }\n";
|
package/dist/types/linear.d.ts
CHANGED
|
@@ -301,6 +301,7 @@ const ISSUE_SUMMARY_SYNONYMS = {
|
|
|
301
301
|
owner: "assignee",
|
|
302
302
|
createdat: "created",
|
|
303
303
|
updatedat: "updated",
|
|
304
|
+
completedat: "completed",
|
|
304
305
|
prioritylabel: "priority",
|
|
305
306
|
projectmilestone: "milestone",
|
|
306
307
|
};
|
|
@@ -316,6 +317,7 @@ function buildIssueHeaderFields(issue) {
|
|
|
316
317
|
{ label: "Estimate", value: s(issue.estimate) },
|
|
317
318
|
{ label: "Created", value: s(issue.createdAt) },
|
|
318
319
|
{ label: "Updated", value: s(issue.updatedAt) },
|
|
320
|
+
{ label: "Completed", value: s(issue.completedAt) },
|
|
319
321
|
{ label: "URL", value: s(issue.url) },
|
|
320
322
|
];
|
|
321
323
|
}
|
|
@@ -424,6 +426,11 @@ const ISSUE_LIST_EXTRAS = {
|
|
|
424
426
|
minWidth: 7,
|
|
425
427
|
extract: (i) => s(i.updatedAt).slice(0, 10),
|
|
426
428
|
},
|
|
429
|
+
completedat: {
|
|
430
|
+
header: "COMPLETED",
|
|
431
|
+
minWidth: 9,
|
|
432
|
+
extract: (i) => s(i.completedAt).slice(0, 10),
|
|
433
|
+
},
|
|
427
434
|
team: {
|
|
428
435
|
header: "TEAM",
|
|
429
436
|
minWidth: 4,
|
|
@@ -25,7 +25,13 @@ export declare function decideGateLedger(opts: {
|
|
|
25
25
|
export interface GateEvent {
|
|
26
26
|
/** Stable gate id, e.g. `issues-create-dup`. */
|
|
27
27
|
gate: string;
|
|
28
|
-
|
|
28
|
+
/**
|
|
29
|
+
* `blocked` — the gate stopped creation. `overridden` — a gate-specific
|
|
30
|
+
* override flag let a would-block proceed. `fail-open` — the gate could not
|
|
31
|
+
* evaluate (infra/service error) and let creation proceed rather than block
|
|
32
|
+
* on trouble; tracked so degradation is measurable (DEV-5378).
|
|
33
|
+
*/
|
|
34
|
+
outcome: "blocked" | "overridden" | "fail-open";
|
|
29
35
|
/** Highest candidate similarity that triggered the gate (0–1). */
|
|
30
36
|
topScore?: number;
|
|
31
37
|
/** How many candidates crossed the threshold. */
|
|
@@ -3,7 +3,7 @@ import { resolveUserDisplayName } from "../config/resolver.js";
|
|
|
3
3
|
import { ARCHIVE_ISSUE_MUTATION, BATCH_GET_ISSUES_QUERY, BATCH_RESOLVE_FOR_CREATE_QUERY, BATCH_RESOLVE_FOR_SEARCH_QUERY, BATCH_RESOLVE_FOR_UPDATE_QUERY, buildResolveLabelsByNameQuery, CREATE_ISSUE_MUTATION, DELETE_ISSUE_MUTATION, FILTERED_SEARCH_ISSUES_QUERY, GET_ISSUE_BY_ID_QUERY, GET_ISSUE_BY_IDENTIFIER_QUERY, GET_ISSUE_CLAIM_CONTEXT_QUERY, GET_ISSUE_START_CONTEXT_QUERY, GET_ISSUE_TEAM_QUERY, GET_ISSUES_QUERY, SEARCH_ISSUES_QUERY, TEAM_STARTED_STATUSES_QUERY, UPDATE_ISSUE_MUTATION, } from "../queries/issues.js";
|
|
4
4
|
import { CREATE_LABEL_MUTATION } from "../queries/labels.js";
|
|
5
5
|
import { TERMINAL_STATE_TYPES } from "../types/linear.js";
|
|
6
|
-
import { toISOStringOrNow } from "./date-format.js";
|
|
6
|
+
import { toISOStringOrNow, toISOStringOrUndefined } from "./date-format.js";
|
|
7
7
|
import { extractEmbeds } from "./embed-parser.js";
|
|
8
8
|
import { multipleMatchesError, notFoundError } from "./error-messages.js";
|
|
9
9
|
import { parseIssueIdentifier, tryParseIssueIdentifier, } from "./identifier-parser.js";
|
|
@@ -1270,6 +1270,9 @@ export class GraphQLIssuesService {
|
|
|
1270
1270
|
: [],
|
|
1271
1271
|
createdAt: toISOStringOrNow(issue.createdAt),
|
|
1272
1272
|
updatedAt: toISOStringOrNow(issue.updatedAt),
|
|
1273
|
+
// Nullable, not `-OrNow`: an open issue has no completion, so it must
|
|
1274
|
+
// stay `undefined` rather than be fabricated as the current time.
|
|
1275
|
+
completedAt: toISOStringOrUndefined(issue.completedAt),
|
|
1273
1276
|
};
|
|
1274
1277
|
}
|
|
1275
1278
|
transformIssueCoreFields(issue) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@enrichlayer/el-linear",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.37.0",
|
|
4
4
|
"description": "A pragmatic CLI for Linear.app — deterministic team/label/member resolution, structured issue validation, configurable term enforcement, and a GraphQL escape hatch.",
|
|
5
5
|
"main": "dist/main.js",
|
|
6
6
|
"types": "dist/main.d.ts",
|