@enrichlayer/el-linear 1.25.0 → 1.26.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
|
@@ -766,6 +766,39 @@ existing markdown or Slack links, angle-bracket autolinks, and bare URLs, so
|
|
|
766
766
|
it's safe to pipe documents that already contain a mix of formatted links
|
|
767
767
|
and bare identifiers.
|
|
768
768
|
|
|
769
|
+
## Relation-candidate confirmation prompt
|
|
770
|
+
|
|
771
|
+
When `el-linear search` or `el-linear issues search` returns results that
|
|
772
|
+
carry issue identifiers (the "I just ran a duplicate/related check" shape),
|
|
773
|
+
the JSON envelope embeds a structured `_warnings` line:
|
|
774
|
+
|
|
775
|
+
```json
|
|
776
|
+
{
|
|
777
|
+
"data": [{ "identifier": "DEV-2134", "title": "…" }, /* … */],
|
|
778
|
+
"meta": { "count": 3, "query": "auth flicker" },
|
|
779
|
+
"_warnings": [
|
|
780
|
+
"relation_candidates: Found 3 candidate related issues (DEV-2134, FIN-77, ALL-672). To link them as related: reply with the IDs you want linked (e.g. \"link DEV-2134 and FIN-77\"). To skip linking: reply \"no links\". (Agent-inferred IDs are blocked by auto-mode; user-named IDs pass — DEV-4494.)"
|
|
781
|
+
]
|
|
782
|
+
}
|
|
783
|
+
```
|
|
784
|
+
|
|
785
|
+
Why it exists: Claude Code's auto-mode permission classifier blocks
|
|
786
|
+
`el-linear issues relate <source> --related-to "<ids>"` calls when the IDs
|
|
787
|
+
were inferred by the agent from its own search rather than typed by the
|
|
788
|
+
user — because each listed peer is a write target. The prompt nudges the
|
|
789
|
+
caller (typically an agent driving the `linear-operations` skill) to surface
|
|
790
|
+
the candidates verbatim and have the user name which IDs to link; any
|
|
791
|
+
subsequent `issues relate` call then carries user-specified IDs and the
|
|
792
|
+
guard passes naturally. **The fix is not to weaken the guard** — see
|
|
793
|
+
[DEV-4494](https://linear.app/verticalint/issue/DEV-4494/) for the original
|
|
794
|
+
incident (PYT-213 triage, 2026-06-04).
|
|
795
|
+
|
|
796
|
+
The `relation_candidates:` prefix is a stable token so a skill or harness
|
|
797
|
+
can grep for it without parsing free-form prose, matching the existing
|
|
798
|
+
`results_truncated:` convention. Non-issue rows (projects, documents,
|
|
799
|
+
initiatives) are ignored; the warning is suppressed when no result carries
|
|
800
|
+
an identifier.
|
|
801
|
+
|
|
769
802
|
## Use with Claude Code
|
|
770
803
|
|
|
771
804
|
el-linear ships a Claude Code skill at `claude-skills/linear-operations/SKILL.md`.
|
|
@@ -181,6 +181,45 @@ el-linear issues search "keywords from proposed title" --include-closed 2>&1
|
|
|
181
181
|
el-linear issues create "Title" --team ENG --related-to "ENG-456,ENG-789" ... 2>&1
|
|
182
182
|
```
|
|
183
183
|
|
|
184
|
+
### Surfacing relation candidates — explicit user reply required ([DEV-4494](https://linear.app/verticalint/issue/DEV-4494/))
|
|
185
|
+
|
|
186
|
+
When `el-linear issues search` (or the cross-resource `search`) returns rows
|
|
187
|
+
carrying issue identifiers, the JSON envelope embeds a `_warnings` line
|
|
188
|
+
starting with `relation_candidates:` that enumerates the candidate IDs and
|
|
189
|
+
asks the user to reply with which ones to link. Treat it as a hard step,
|
|
190
|
+
not a hint:
|
|
191
|
+
|
|
192
|
+
1. **Surface the IDs to the user verbatim.** Show the `relation_candidates:`
|
|
193
|
+
line (or paraphrase it preserving every ID + the example reply). Do **not**
|
|
194
|
+
skip ahead to `issues relate`.
|
|
195
|
+
2. **Wait for an explicit reply naming the IDs to link** (e.g. `link DEV-2134
|
|
196
|
+
and ALL-672`) or a clear skip (`no links`).
|
|
197
|
+
3. **Only the user-named IDs** go into the next `el-linear issues relate
|
|
198
|
+
<source> --related-to "<ids>"` call. Never pass IDs the user did not name,
|
|
199
|
+
even if your earlier search obviously surfaced them.
|
|
200
|
+
|
|
201
|
+
Why this matters: Claude Code's auto-mode permission classifier blocks
|
|
202
|
+
`issues relate --related-to "<ids>"` when the IDs were *agent-inferred*
|
|
203
|
+
(came from your own search) rather than *user-specified* (typed by the human),
|
|
204
|
+
because each listed peer is a write target. Routing the IDs through an
|
|
205
|
+
explicit human reply converts them from agent-inferred → user-specified;
|
|
206
|
+
the existing search step (above) stays intact; auto-mode's guard is not
|
|
207
|
+
weakened. The fix is the loop shape, not the guard.
|
|
208
|
+
|
|
209
|
+
Anti-patterns:
|
|
210
|
+
|
|
211
|
+
- **Calling `issues relate` directly off your own search output** — even if
|
|
212
|
+
the IDs are real and the candidates look obvious, this is the exact path
|
|
213
|
+
the auto-mode guard refuses.
|
|
214
|
+
- **Splitting one relate call into N single-ID calls** to "look smaller" —
|
|
215
|
+
same provenance problem, same block, just multiplied.
|
|
216
|
+
- **Asking the user a yes/no question** ("Should I link these?") instead of
|
|
217
|
+
having them name the IDs — yes answers stay agent-inferred, the reply
|
|
218
|
+
must carry the IDs to convert them to user-specified.
|
|
219
|
+
|
|
220
|
+
If `--include-closed` search returns no matches, no `relation_candidates:`
|
|
221
|
+
warning is emitted (nothing to confirm) and the flow proceeds normally.
|
|
222
|
+
|
|
184
223
|
### Viewing existing relations
|
|
185
224
|
|
|
186
225
|
```bash
|
package/dist/commands/issues.js
CHANGED
|
@@ -14,6 +14,7 @@ import { createIssuesService } from "../utils/issues-service-bootstrap.js";
|
|
|
14
14
|
import { createLinearService, } from "../utils/linear-service.js";
|
|
15
15
|
import { logger } from "../utils/logger.js";
|
|
16
16
|
import { handleAsyncCommand, outputSuccess, outputWarning, warnIfTruncated, } from "../utils/output.js";
|
|
17
|
+
import { buildRelationCandidatePrompt } from "../utils/relation-candidate-prompt.js";
|
|
17
18
|
import { getRootOpts } from "../utils/root-opts.js";
|
|
18
19
|
import { formatCsv, formatMarkdown, formatTable, } from "../utils/table-formatter.js";
|
|
19
20
|
import { parsePositiveInt, parsePriorityFilter, splitList, validatePriority, } from "../utils/validators.js";
|
|
@@ -269,6 +270,15 @@ async function handleSearchIssues(query, options, command) {
|
|
|
269
270
|
outputWarning("excluded terminal states (Done / Canceled) by default; pass --include-closed to include them");
|
|
270
271
|
}
|
|
271
272
|
warnIfTruncated(result.length, limit);
|
|
273
|
+
// DEV-4494: surface the explicit "reply with the IDs to link" prompt
|
|
274
|
+
// whenever an issue search returns candidate identifiers. The
|
|
275
|
+
// `linear-operations` skill consumes this `_warnings` line and shows it
|
|
276
|
+
// to the user so any subsequent `issues relate` call is user-specified
|
|
277
|
+
// rather than agent-inferred (which auto-mode blocks).
|
|
278
|
+
const relationPrompt = buildRelationCandidatePrompt(result);
|
|
279
|
+
if (relationPrompt) {
|
|
280
|
+
outputWarning(relationPrompt);
|
|
281
|
+
}
|
|
272
282
|
outputIssues(result, options.format, options.fields, { query });
|
|
273
283
|
}
|
|
274
284
|
/**
|
package/dist/commands/search.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { resolveTeam, resolveUserDisplayName } from "../config/resolver.js";
|
|
2
2
|
import { SEMANTIC_SEARCH_QUERY } from "../queries/search.js";
|
|
3
3
|
import { createGraphQLService } from "../utils/graphql-service.js";
|
|
4
|
-
import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
|
|
4
|
+
import { handleAsyncCommand, outputSuccess, outputWarning, } from "../utils/output.js";
|
|
5
|
+
import { buildRelationCandidatePrompt } from "../utils/relation-candidate-prompt.js";
|
|
5
6
|
import { getRootOpts } from "../utils/root-opts.js";
|
|
6
7
|
import { parsePositiveInt } from "../utils/validators.js";
|
|
7
8
|
const TEMPLATES_QUERY = `
|
|
@@ -180,9 +181,19 @@ export function setupSearchCommands(program) {
|
|
|
180
181
|
const templates = templateResult.templates ?? [];
|
|
181
182
|
data = [...data, ...searchTemplates(templates, query)];
|
|
182
183
|
}
|
|
184
|
+
const finalData = data.slice(0, limit);
|
|
185
|
+
// DEV-4494: when results carry issue identifiers, nudge the
|
|
186
|
+
// caller to surface them to the user verbatim and have the
|
|
187
|
+
// user name which IDs to link via `issues relate`. Keeps
|
|
188
|
+
// agent-inferred IDs out of relate calls without weakening
|
|
189
|
+
// auto-mode's guard.
|
|
190
|
+
const relationPrompt = buildRelationCandidatePrompt(finalData);
|
|
191
|
+
if (relationPrompt) {
|
|
192
|
+
outputWarning(relationPrompt);
|
|
193
|
+
}
|
|
183
194
|
outputSuccess({
|
|
184
|
-
data:
|
|
185
|
-
meta: { count:
|
|
195
|
+
data: finalData,
|
|
196
|
+
meta: { count: finalData.length, query },
|
|
186
197
|
});
|
|
187
198
|
}));
|
|
188
199
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Relation-candidate confirmation prompt — DEV-4494.
|
|
3
|
+
*
|
|
4
|
+
* When `el-linear search` or `el-linear issues search` returns results that
|
|
5
|
+
* carry issue identifiers (the "I just ran a dup-check" shape), we emit a
|
|
6
|
+
* structured `_warnings` line that nudges the *caller* (typically a Claude
|
|
7
|
+
* agent driving `linear-operations`) to surface the IDs to the user verbatim
|
|
8
|
+
* and wait for an explicit reply naming which ones to link.
|
|
9
|
+
*
|
|
10
|
+
* Why the explicit reply matters
|
|
11
|
+
* ------------------------------
|
|
12
|
+
* Claude Code's auto-mode permission classifier blocks
|
|
13
|
+
* `el-linear issues relate <source> --related-to "<ids>"` when the IDs were
|
|
14
|
+
* inferred by the agent from its own search rather than typed by the user —
|
|
15
|
+
* because creating a relation writes onto *every* listed peer issue, and the
|
|
16
|
+
* IDs must be user-specified, not agent-inferred, to clear the guard.
|
|
17
|
+
*
|
|
18
|
+
* The fix isn't to weaken the guard. It's to tighten the loop: surface the
|
|
19
|
+
* candidates, ask the human to name which IDs to link, and only THEN call
|
|
20
|
+
* `issues relate` — at which point the IDs are user-specified by
|
|
21
|
+
* construction and the guard passes naturally.
|
|
22
|
+
*
|
|
23
|
+
* This module produces the warning. The actual UX is enforced by the
|
|
24
|
+
* `linear-operations` skill (it consumes the warning and shows it to the
|
|
25
|
+
* user) and by the existing auto-mode guard (it continues to block
|
|
26
|
+
* agent-inferred relate calls).
|
|
27
|
+
*
|
|
28
|
+
* Reference: https://linear.app/verticalint/issue/DEV-4494/
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* Extract issue identifiers from a heterogeneous result array.
|
|
32
|
+
*
|
|
33
|
+
* Accepts the union of shapes used across the search commands:
|
|
34
|
+
* - `issues search` rows (`LinearIssue`) carry `identifier` at the top level
|
|
35
|
+
* - cross-resource `search` rows transform to `{ type: "issue", identifier }`
|
|
36
|
+
* for issue rows; non-issue rows (`project`, `document`, …) have no
|
|
37
|
+
* identifier and are skipped.
|
|
38
|
+
*
|
|
39
|
+
* Deduplicates and preserves insertion order so the prompt enumerates IDs
|
|
40
|
+
* in the same order they appear on screen.
|
|
41
|
+
*/
|
|
42
|
+
export declare function extractCandidateIdentifiers(rows: unknown[]): string[];
|
|
43
|
+
/**
|
|
44
|
+
* Build the relation-candidate confirmation warning string, or `null` when
|
|
45
|
+
* the result set has no identifier-bearing rows (nothing to confirm).
|
|
46
|
+
*
|
|
47
|
+
* Shape (single line, structured-prose so a skill can match on the prefix):
|
|
48
|
+
*
|
|
49
|
+
* relation_candidates: Found N candidate related issues (DEV-1, DEV-2, …).
|
|
50
|
+
* To link them as related: reply with the IDs you want linked
|
|
51
|
+
* (e.g. "link DEV-1 and DEV-2"). To skip linking: reply "no links".
|
|
52
|
+
*
|
|
53
|
+
* The `relation_candidates:` prefix matches the existing `results_truncated:`
|
|
54
|
+
* convention in `outputWarning` callers — a stable token a skill / agent
|
|
55
|
+
* harness can grep for without parsing free-form prose.
|
|
56
|
+
*/
|
|
57
|
+
export declare function buildRelationCandidatePrompt(rows: unknown[]): string | null;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Relation-candidate confirmation prompt — DEV-4494.
|
|
3
|
+
*
|
|
4
|
+
* When `el-linear search` or `el-linear issues search` returns results that
|
|
5
|
+
* carry issue identifiers (the "I just ran a dup-check" shape), we emit a
|
|
6
|
+
* structured `_warnings` line that nudges the *caller* (typically a Claude
|
|
7
|
+
* agent driving `linear-operations`) to surface the IDs to the user verbatim
|
|
8
|
+
* and wait for an explicit reply naming which ones to link.
|
|
9
|
+
*
|
|
10
|
+
* Why the explicit reply matters
|
|
11
|
+
* ------------------------------
|
|
12
|
+
* Claude Code's auto-mode permission classifier blocks
|
|
13
|
+
* `el-linear issues relate <source> --related-to "<ids>"` when the IDs were
|
|
14
|
+
* inferred by the agent from its own search rather than typed by the user —
|
|
15
|
+
* because creating a relation writes onto *every* listed peer issue, and the
|
|
16
|
+
* IDs must be user-specified, not agent-inferred, to clear the guard.
|
|
17
|
+
*
|
|
18
|
+
* The fix isn't to weaken the guard. It's to tighten the loop: surface the
|
|
19
|
+
* candidates, ask the human to name which IDs to link, and only THEN call
|
|
20
|
+
* `issues relate` — at which point the IDs are user-specified by
|
|
21
|
+
* construction and the guard passes naturally.
|
|
22
|
+
*
|
|
23
|
+
* This module produces the warning. The actual UX is enforced by the
|
|
24
|
+
* `linear-operations` skill (it consumes the warning and shows it to the
|
|
25
|
+
* user) and by the existing auto-mode guard (it continues to block
|
|
26
|
+
* agent-inferred relate calls).
|
|
27
|
+
*
|
|
28
|
+
* Reference: https://linear.app/verticalint/issue/DEV-4494/
|
|
29
|
+
*/
|
|
30
|
+
/** Cap how many candidate IDs the prompt enumerates inline. */
|
|
31
|
+
const MAX_CANDIDATES_IN_PROMPT = 10;
|
|
32
|
+
/**
|
|
33
|
+
* Extract issue identifiers from a heterogeneous result array.
|
|
34
|
+
*
|
|
35
|
+
* Accepts the union of shapes used across the search commands:
|
|
36
|
+
* - `issues search` rows (`LinearIssue`) carry `identifier` at the top level
|
|
37
|
+
* - cross-resource `search` rows transform to `{ type: "issue", identifier }`
|
|
38
|
+
* for issue rows; non-issue rows (`project`, `document`, …) have no
|
|
39
|
+
* identifier and are skipped.
|
|
40
|
+
*
|
|
41
|
+
* Deduplicates and preserves insertion order so the prompt enumerates IDs
|
|
42
|
+
* in the same order they appear on screen.
|
|
43
|
+
*/
|
|
44
|
+
export function extractCandidateIdentifiers(rows) {
|
|
45
|
+
const seen = new Set();
|
|
46
|
+
const out = [];
|
|
47
|
+
for (const row of rows) {
|
|
48
|
+
if (row === null || typeof row !== "object")
|
|
49
|
+
continue;
|
|
50
|
+
const r = row;
|
|
51
|
+
const id = typeof r.identifier === "string" ? r.identifier : undefined;
|
|
52
|
+
if (!id)
|
|
53
|
+
continue;
|
|
54
|
+
if (seen.has(id))
|
|
55
|
+
continue;
|
|
56
|
+
seen.add(id);
|
|
57
|
+
out.push(id);
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Build the relation-candidate confirmation warning string, or `null` when
|
|
63
|
+
* the result set has no identifier-bearing rows (nothing to confirm).
|
|
64
|
+
*
|
|
65
|
+
* Shape (single line, structured-prose so a skill can match on the prefix):
|
|
66
|
+
*
|
|
67
|
+
* relation_candidates: Found N candidate related issues (DEV-1, DEV-2, …).
|
|
68
|
+
* To link them as related: reply with the IDs you want linked
|
|
69
|
+
* (e.g. "link DEV-1 and DEV-2"). To skip linking: reply "no links".
|
|
70
|
+
*
|
|
71
|
+
* The `relation_candidates:` prefix matches the existing `results_truncated:`
|
|
72
|
+
* convention in `outputWarning` callers — a stable token a skill / agent
|
|
73
|
+
* harness can grep for without parsing free-form prose.
|
|
74
|
+
*/
|
|
75
|
+
export function buildRelationCandidatePrompt(rows) {
|
|
76
|
+
const ids = extractCandidateIdentifiers(rows);
|
|
77
|
+
if (ids.length === 0)
|
|
78
|
+
return null;
|
|
79
|
+
const shown = ids.slice(0, MAX_CANDIDATES_IN_PROMPT);
|
|
80
|
+
const overflow = ids.length - shown.length;
|
|
81
|
+
const idList = overflow > 0
|
|
82
|
+
? `${shown.join(", ")}, … (+${overflow} more)`
|
|
83
|
+
: shown.join(", ");
|
|
84
|
+
// Build two concrete example IDs from the head of the list so the
|
|
85
|
+
// "reply with the IDs you want linked" example is realistic for the
|
|
86
|
+
// caller's actual search rather than a fixed placeholder. Single-result
|
|
87
|
+
// case still reads naturally ("link DEV-1").
|
|
88
|
+
const example = shown.length >= 2 ? `link ${shown[0]} and ${shown[1]}` : `link ${shown[0]}`;
|
|
89
|
+
const noun = ids.length === 1 ? "candidate related issue" : "candidate related issues";
|
|
90
|
+
return (`relation_candidates: Found ${ids.length} ${noun} (${idList}). ` +
|
|
91
|
+
`To link them as related: reply with the IDs you want linked ` +
|
|
92
|
+
`(e.g. "${example}"). To skip linking: reply "no links". ` +
|
|
93
|
+
`(Agent-inferred IDs are blocked by auto-mode; user-named IDs pass — DEV-4494.)`);
|
|
94
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@enrichlayer/el-linear",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.26.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",
|