@enrichlayer/el-linear 1.35.0 → 1.36.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 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. el-linear can record each
253
- fire/override decision to a local JSONL file so you can measure the gate's
254
- **override-rate** and tell whether it's too aggressive. It is **off by default**
255
- and writes nothing unless you opt in (e.g. `export EL_TELEMETRY_DIR=<path>`);
256
- there is no server or database, and `EL_TELEMETRY_DISABLED=1` forces it off.
257
- Full opt-in rules, the event schema, and a `jq` reader are in
258
- [docs/telemetry.md](./docs/telemetry.md).
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
 
@@ -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
@@ -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")
@@ -67,6 +67,21 @@ export interface ElLinearConfig {
67
67
  * `DEFAULT_DUPLICATE_THRESHOLD` (0.35). Lower = more aggressive.
68
68
  */
69
69
  duplicateThreshold?: number;
70
+ /**
71
+ * OPT-IN SOP-label parent gate (DEV-5378). When `true`, `issues create`
72
+ * requires an issue carrying an SOP-type label (see `sopLabels`) to point
73
+ * at a parent SOP via `--parent` or `--related-to`, blocking otherwise.
74
+ * Defaults to `false` (dormant) — el-linear is MIT/open-source and "SOP"
75
+ * is an Enrich-Layer-specific taxonomy, so a fresh install stays silent
76
+ * until a workspace opts in (the EL shared team config flips it on).
77
+ * Bypass a single create with `--allow-unparented-sop`.
78
+ */
79
+ sopLabelParentGate?: boolean;
80
+ /**
81
+ * Label names (matched case-insensitively) that mark an issue as an SOP
82
+ * for the `sopLabelParentGate` check. Defaults to `["SOP"]`.
83
+ */
84
+ sopLabels?: string[];
70
85
  };
71
86
  /**
72
87
  * Optional override for the Linear workspace URL key (the part after
@@ -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
+ }
@@ -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
- outcome: "blocked" | "overridden";
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. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enrichlayer/el-linear",
3
- "version": "1.35.0",
3
+ "version": "1.36.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",