@enrichlayer/el-linear 1.34.1 → 1.35.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
@@ -445,6 +445,7 @@ el-linear <command> --help # detailed help for one command
445
445
  | Comments | `comments {list, read, create, update, delete}` |
446
446
  | Labels | `labels {list, create, retire, restore}` |
447
447
  | Projects | `projects {list, add-team, remove-team}` |
448
+ | Project updates | `project-updates {create, list, read}` (post a status update to a project's Updates feed) |
448
449
  | Cycles | `cycles {list, read}` |
449
450
  | Documents | `documents {list, read, create, update, delete}` |
450
451
  | Releases | `releases {list, read, create, pipelines}` |
@@ -578,7 +579,7 @@ el-linear projects list --format summary --fields name,state,progress,lead,teams
578
579
  # ...
579
580
  ```
580
581
 
581
- Unrecognized field names are reported as a `_warnings:` line appended after the summary block (`fields_unprojectable: --format summary on issues list does not project foo, bar; ...`) — same signal scripts get on the JSON path. Resources whose summary formatter doesn't yet wire `--fields` (cycles, milestones, comments, teams, labels, users, documents, templates, attachments, releases, search results) emit the same warning and render their default summary.
582
+ Unrecognized field names are reported as a `_warnings:` line appended after the summary block (`fields_unprojectable: --format summary on issues list does not project foo, bar; ...`) — same signal scripts get on the JSON path. Resources whose summary formatter doesn't yet wire `--fields` (cycles, milestones, project updates, comments, teams, labels, users, documents, templates, attachments, releases, search results) emit the same warning and render their default summary.
582
583
 
583
584
  ### Windowed metadata (`WindowedMeta`)
584
585
 
@@ -22,11 +22,12 @@ import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
22
22
  // elsewhere in the workspace. Any addition here is the single point of
23
23
  // truth that all skills rely on.
24
24
  //
25
- // Accepted prefixes (DEV-4777 adds bug/spike; DEV-4660 added codex; both
26
- // mirror tools-repo DEV-4417):
27
- // feature | fix | chore | refactor | dev — SOP-canonical + Linear-CLI direct
25
+ // Accepted prefixes (DEV-4777 adds bug/spike; DEV-4660 added codex; DEV-5342
26
+ // adds the feat short-form alias; all mirror tools-repo DEV-4417/DEV-5334):
27
+ // feature | feat | fix | chore | refactor | dev — SOP-canonical + Linear-CLI direct
28
28
  // bug | spike — sanctioned Linear type labels
29
29
  // codex — Codex-authored branches (codex/<TEAM>-<N>-slug)
30
+ // feat — short-form alias for feature (DEV-5342)
30
31
  // New authoring surfaces (Codex, future agent prefixes) and sanctioned Linear
31
32
  // type labels (bug, spike) get first-class issue detection so commit guards,
32
33
  // MR descriptions, and session handoff don't go dark on a branch a human
@@ -34,7 +35,7 @@ import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
34
35
  //
35
36
  // Mirror of cli/el-git/src/commands/context.ts:BRANCH_RE in the
36
37
  // vertical-int/tools repo. If you change one, change the other.
37
- const BRANCH_RE = /^(?:feature|fix|chore|refactor|bug|spike|dev|codex)[-/]([A-Z]{2,4})-(\d+)(?:[-/](.*))?$/i;
38
+ const BRANCH_RE = /^(?:feature|feat|fix|chore|refactor|bug|spike|dev|codex)[-/]([A-Z]{2,4})-(\d+)(?:[-/](.*))?$/i;
38
39
  export function parseBranchName(branch) {
39
40
  const m = branch.match(BRANCH_RE);
40
41
  if (!m) {
@@ -0,0 +1,2 @@
1
+ import type { Command } from "commander";
2
+ export declare function setupProjectUpdatesCommands(program: Command): void;
@@ -0,0 +1,116 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { CREATE_PROJECT_UPDATE_MUTATION, GET_PROJECT_UPDATE_BY_ID_QUERY, LIST_PROJECT_UPDATES_QUERY, } from "../queries/project-updates.js";
3
+ import { notFoundError } from "../utils/error-messages.js";
4
+ import { createGraphQLService } from "../utils/graphql-service.js";
5
+ import { createLinearService } from "../utils/linear-service.js";
6
+ import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
7
+ import { getRootOpts } from "../utils/root-opts.js";
8
+ import { parsePositiveInt } from "../utils/validators.js";
9
+ const VALID_HEALTH = [
10
+ "onTrack",
11
+ "atRisk",
12
+ "offTrack",
13
+ ];
14
+ /**
15
+ * Resolve the update body from `--body` / `--body-file` (mutually exclusive,
16
+ * exactly one required) — mirrors the comments-command contract so file-sourced
17
+ * bodies sidestep shell-quoting traps for markdown/tables/backticks.
18
+ */
19
+ function resolveBody(options) {
20
+ if (options.body && options.bodyFile) {
21
+ throw new Error("--body and --body-file are mutually exclusive — pass one or the other");
22
+ }
23
+ if (options.bodyFile) {
24
+ return readFileSync(options.bodyFile, "utf-8");
25
+ }
26
+ if (options.body) {
27
+ return options.body;
28
+ }
29
+ throw new Error("Either --body or --body-file is required");
30
+ }
31
+ /**
32
+ * Validate an optional `--health` value against Linear's
33
+ * `ProjectUpdateHealthType` enum. Returns undefined when the flag is omitted
34
+ * (Linear then defaults the health), throws on an unknown value.
35
+ */
36
+ function resolveHealth(value) {
37
+ if (value === undefined) {
38
+ return undefined;
39
+ }
40
+ if (!VALID_HEALTH.includes(value)) {
41
+ throw new Error(`Invalid --health "${value}" — expected one of: ${VALID_HEALTH.join(", ")}`);
42
+ }
43
+ return value;
44
+ }
45
+ async function handleCreateProjectUpdate(options, command) {
46
+ // Validate local input first so bad --body/--health fails with zero network.
47
+ const body = resolveBody(options);
48
+ const health = resolveHealth(options.health);
49
+ const rootOpts = getRootOpts(command);
50
+ const graphQLService = await createGraphQLService(rootOpts);
51
+ const linearService = await createLinearService(rootOpts);
52
+ const projectId = await linearService.resolveProjectId(options.project);
53
+ const input = { projectId, body };
54
+ if (health !== undefined) {
55
+ input.health = health;
56
+ }
57
+ if (options.diffHidden) {
58
+ input.isDiffHidden = true;
59
+ }
60
+ const result = await graphQLService.rawRequest(CREATE_PROJECT_UPDATE_MUTATION, { input });
61
+ if (!result.projectUpdateCreate.success ||
62
+ !result.projectUpdateCreate.projectUpdate) {
63
+ throw new Error(`Failed to create project update on project "${options.project}"`);
64
+ }
65
+ outputSuccess(result.projectUpdateCreate.projectUpdate);
66
+ }
67
+ async function handleListProjectUpdates(options, command) {
68
+ const rootOpts = getRootOpts(command);
69
+ const graphQLService = await createGraphQLService(rootOpts);
70
+ const linearService = await createLinearService(rootOpts);
71
+ const projectId = await linearService.resolveProjectId(options.project);
72
+ const result = await graphQLService.rawRequest(LIST_PROJECT_UPDATES_QUERY, {
73
+ projectId,
74
+ first: parsePositiveInt(options.limit, "--limit"),
75
+ });
76
+ if (!result.project) {
77
+ throw notFoundError("Project", options.project);
78
+ }
79
+ const nodes = result.project.projectUpdates.nodes;
80
+ outputSuccess({ data: nodes, meta: { count: nodes.length } });
81
+ }
82
+ async function handleReadProjectUpdate(updateId, _options, command) {
83
+ const rootOpts = getRootOpts(command);
84
+ const graphQLService = await createGraphQLService(rootOpts);
85
+ const result = await graphQLService.rawRequest(GET_PROJECT_UPDATE_BY_ID_QUERY, { id: updateId });
86
+ if (!result.projectUpdate) {
87
+ throw notFoundError("Project update", updateId);
88
+ }
89
+ outputSuccess(result.projectUpdate);
90
+ }
91
+ export function setupProjectUpdatesCommands(program) {
92
+ const projectUpdates = program
93
+ .command("project-updates")
94
+ .description("Project update (status post) operations");
95
+ projectUpdates.action(() => projectUpdates.help());
96
+ projectUpdates
97
+ .command("create")
98
+ .description("Post a status update to a project (appears in the project's Updates feed).")
99
+ .requiredOption("--project <project>", "project name or ID")
100
+ .option("--body <body>", "update body markdown (inline)")
101
+ .option("--body-file <path>", "read update body from file")
102
+ .option("--health <health>", "project health: onTrack | atRisk | offTrack (omit to leave unset)")
103
+ .option("--diff-hidden", "hide the progress diff on the update")
104
+ .option("-q, --quiet", "print one confirmation line (health url) instead of the full JSON")
105
+ .action(handleAsyncCommand(handleCreateProjectUpdate));
106
+ projectUpdates
107
+ .command("list")
108
+ .description("List status updates posted to a project (newest first).")
109
+ .requiredOption("--project <project>", "project name or ID")
110
+ .option("-l, --limit <number>", "limit results", "50")
111
+ .action(handleAsyncCommand(handleListProjectUpdates));
112
+ projectUpdates
113
+ .command("read <updateId>")
114
+ .description("Get a single project update by its ID.")
115
+ .action(handleAsyncCommand(handleReadProjectUpdate));
116
+ }
package/dist/main.js CHANGED
@@ -20,6 +20,7 @@ import { setupIssuesCommands } from "./commands/issues.js";
20
20
  import { setupLabelsCommands } from "./commands/labels.js";
21
21
  import { setupProfileCommands } from "./commands/profile.js";
22
22
  import { setupProjectMilestonesCommands } from "./commands/project-milestones.js";
23
+ import { setupProjectUpdatesCommands } from "./commands/project-updates.js";
23
24
  import { setupProjectsCommands } from "./commands/projects.js";
24
25
  import { setupReadShortcut } from "./commands/read-shortcut.js";
25
26
  import { setupRefsCommands } from "./commands/refs.js";
@@ -65,9 +66,13 @@ program
65
66
  .option("--profile <name>", "named profile (under ~/.config/el-linear/profiles/<name>/) for this invocation. Overrides EL_LINEAR_PROFILE env + the on-disk active-profile marker.")
66
67
  .option("--json", "output as JSON (default, accepted for compatibility)")
67
68
  .option("--format <kind>", "output format: json (default, structured envelope) or summary (human-readable)", "json")
68
- .option("--raw", "strip { data, meta } wrapper from list output — emit the array directly")
69
+ .option("--raw", "strip { data, meta } wrapper from list output — emit the array directly. " +
70
+ "Warnings (e.g. --fields fields_unresolved) can't ride on a bare array, so " +
71
+ "they are written to stderr prefixed `_warnings: `, keeping stdout a pure JSON array")
69
72
  .option("--jq <filter>", "apply a jq filter to the JSON output")
70
- .option("--fields <fields>", "filter output to specific fields (comma-separated)")
73
+ .option("--fields <fields>", "filter output to specific fields (comma-separated). Unresolved fields are " +
74
+ "emitted as null plus a `fields_unresolved:` warning — in the JSON " +
75
+ "envelope's `_warnings`, or on stderr when output is a bare array (with --raw)")
71
76
  .option("--no-cache", "bypass the on-disk cache for `teams list` / `labels list` / `projects list`");
72
77
  program.hook("preAction", (_thisCommand, actionCommand) => {
73
78
  const rootOpts = actionCommand.optsWithGlobals();
@@ -143,6 +148,7 @@ setupReleasesCommands(program);
143
148
  setupProjectsCommands(program);
144
149
  setupCyclesCommands(program);
145
150
  setupProjectMilestonesCommands(program);
151
+ setupProjectUpdatesCommands(program);
146
152
  setupEmbedsCommands(program);
147
153
  setupTeamsCommands(program);
148
154
  setupTemplatesCommands(program);
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Typed response shapes for the queries in `./project-updates.ts`.
3
+ * See `./issues-types.ts` for the rationale (ALL-937).
4
+ *
5
+ * All three queries (create / list / read) select the same
6
+ * `PROJECT_UPDATE_FRAGMENT`, so one node shape covers every consumer.
7
+ */
8
+ export type ProjectUpdateHealth = "onTrack" | "atRisk" | "offTrack";
9
+ interface ProjectUpdateProjectRef {
10
+ id: string;
11
+ name: string;
12
+ }
13
+ interface ProjectUpdateUserRef {
14
+ id: string;
15
+ name: string;
16
+ displayName: string | null;
17
+ }
18
+ /**
19
+ * Mirrors `PROJECT_UPDATE_FRAGMENT` — the full selection set shared by the
20
+ * create mutation, the project-scoped list, and the by-id read.
21
+ */
22
+ export interface ProjectUpdateNode {
23
+ id: string;
24
+ body: string | null;
25
+ health: ProjectUpdateHealth | null;
26
+ url: string | null;
27
+ slugId: string | null;
28
+ createdAt: string;
29
+ updatedAt: string;
30
+ editedAt: string | null;
31
+ user: ProjectUpdateUserRef | null;
32
+ project: ProjectUpdateProjectRef | null;
33
+ }
34
+ export interface CreateProjectUpdateResponse {
35
+ projectUpdateCreate: {
36
+ success: boolean;
37
+ projectUpdate: ProjectUpdateNode | null;
38
+ };
39
+ }
40
+ export interface ListProjectUpdatesResponse {
41
+ project: {
42
+ id: string;
43
+ name: string;
44
+ projectUpdates: {
45
+ nodes: ProjectUpdateNode[];
46
+ };
47
+ } | null;
48
+ }
49
+ export interface GetProjectUpdateByIdResponse {
50
+ projectUpdate: ProjectUpdateNode | null;
51
+ }
52
+ export {};
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Typed response shapes for the queries in `./project-updates.ts`.
3
+ * See `./issues-types.ts` for the rationale (ALL-937).
4
+ *
5
+ * All three queries (create / list / read) select the same
6
+ * `PROJECT_UPDATE_FRAGMENT`, so one node shape covers every consumer.
7
+ */
8
+ export {};
@@ -0,0 +1,3 @@
1
+ export declare const CREATE_PROJECT_UPDATE_MUTATION = "\n mutation ProjectUpdateCreate($input: ProjectUpdateCreateInput!) {\n projectUpdateCreate(input: $input) {\n success\n projectUpdate {\n \n id\n body\n health\n url\n slugId\n createdAt\n updatedAt\n editedAt\n user {\n id\n name\n displayName\n }\n project {\n id\n name\n }\n\n }\n }\n }\n";
2
+ export declare const LIST_PROJECT_UPDATES_QUERY = "\n query ListProjectUpdates($projectId: String!, $first: Int!) {\n project(id: $projectId) {\n id\n name\n projectUpdates(first: $first, orderBy: createdAt) {\n nodes {\n \n id\n body\n health\n url\n slugId\n createdAt\n updatedAt\n editedAt\n user {\n id\n name\n displayName\n }\n project {\n id\n name\n }\n\n }\n }\n }\n }\n";
3
+ export declare const GET_PROJECT_UPDATE_BY_ID_QUERY = "\n query GetProjectUpdate($id: String!) {\n projectUpdate(id: $id) {\n \n id\n body\n health\n url\n slugId\n createdAt\n updatedAt\n editedAt\n user {\n id\n name\n displayName\n }\n project {\n id\n name\n }\n\n }\n }\n";
@@ -0,0 +1,49 @@
1
+ const PROJECT_UPDATE_FRAGMENT = `
2
+ id
3
+ body
4
+ health
5
+ url
6
+ slugId
7
+ createdAt
8
+ updatedAt
9
+ editedAt
10
+ user {
11
+ id
12
+ name
13
+ displayName
14
+ }
15
+ project {
16
+ id
17
+ name
18
+ }
19
+ `;
20
+ export const CREATE_PROJECT_UPDATE_MUTATION = `
21
+ mutation ProjectUpdateCreate($input: ProjectUpdateCreateInput!) {
22
+ projectUpdateCreate(input: $input) {
23
+ success
24
+ projectUpdate {
25
+ ${PROJECT_UPDATE_FRAGMENT}
26
+ }
27
+ }
28
+ }
29
+ `;
30
+ export const LIST_PROJECT_UPDATES_QUERY = `
31
+ query ListProjectUpdates($projectId: String!, $first: Int!) {
32
+ project(id: $projectId) {
33
+ id
34
+ name
35
+ projectUpdates(first: $first, orderBy: createdAt) {
36
+ nodes {
37
+ ${PROJECT_UPDATE_FRAGMENT}
38
+ }
39
+ }
40
+ }
41
+ }
42
+ `;
43
+ export const GET_PROJECT_UPDATE_BY_ID_QUERY = `
44
+ query GetProjectUpdate($id: String!) {
45
+ projectUpdate(id: $id) {
46
+ ${PROJECT_UPDATE_FRAGMENT}
47
+ }
48
+ }
49
+ `;
@@ -30,6 +30,8 @@ export declare function formatCycleSummary(cycle: Record<string, unknown>): stri
30
30
  export declare function formatCycleList(cycles: unknown[]): string;
31
31
  export declare function formatMilestoneSummary(milestone: Record<string, unknown>): string;
32
32
  export declare function formatMilestoneList(milestones: unknown[]): string;
33
+ export declare function formatProjectUpdateSummary(update: Record<string, unknown>): string;
34
+ export declare function formatProjectUpdateList(updates: unknown[]): string;
33
35
  export declare function formatTeamList(teams: unknown[]): string;
34
36
  export declare function formatLabelList(labels: unknown[]): string;
35
37
  /**
@@ -68,7 +70,7 @@ export declare function formatSearchResultList(results: unknown[]): string;
68
70
  * the full payload. Lists fall back to a simple bulleted list.
69
71
  */
70
72
  export declare function formatGenericSummary(value: unknown): string;
71
- export type ResourceKind = "issue" | "issue-list" | "project" | "project-list" | "comment" | "comment-list" | "cycle" | "cycle-list" | "milestone" | "milestone-list" | "team-list" | "label-list" | "user" | "user-list" | "document" | "document-list" | "template" | "template-list" | "attachment-list" | "release" | "release-list" | "search-result-list" | "relation-list" | "issue-relation-list" | "empty-list" | "generic";
73
+ export type ResourceKind = "issue" | "issue-list" | "project" | "project-list" | "comment" | "comment-list" | "cycle" | "cycle-list" | "milestone" | "milestone-list" | "project-update" | "project-update-list" | "team-list" | "label-list" | "user" | "user-list" | "document" | "document-list" | "template" | "template-list" | "attachment-list" | "release" | "release-list" | "search-result-list" | "relation-list" | "issue-relation-list" | "empty-list" | "generic";
72
74
  /**
73
75
  * Heuristic — used by the central `outputSuccess` path which doesn't know
74
76
  * which command produced the payload. Looks at the shape of the data to
@@ -655,6 +655,35 @@ export function formatMilestoneList(milestones) {
655
655
  { header: "PROJECT", minWidth: 7, extract: (m) => getName(m.project) },
656
656
  ], { emptyText: "(no milestones)", itemNoun: "milestone" });
657
657
  }
658
+ // ── project updates ────────────────────────────────────────────
659
+ export function formatProjectUpdateSummary(update) {
660
+ const headerLine = `project-update ${s(update.id)}`;
661
+ const fields = [
662
+ { label: "Health", value: s(update.health) },
663
+ { label: "Project", value: getName(update.project) },
664
+ { label: "Author", value: getName(update.user) },
665
+ { label: "Created", value: s(update.createdAt) },
666
+ { label: "URL", value: s(update.url) },
667
+ ];
668
+ const header = renderHeader(fields);
669
+ const body = clipDescription(update.body);
670
+ const parts = [headerLine, header];
671
+ if (body)
672
+ parts.push("", body);
673
+ return parts.filter((p) => p !== "").join("\n");
674
+ }
675
+ export function formatProjectUpdateList(updates) {
676
+ return renderTable(updates.map((raw) => asObj(raw) ?? {}), [
677
+ { header: "HEALTH", minWidth: 6, extract: (u) => s(u.health) },
678
+ { header: "AUTHOR", minWidth: 6, extract: (u) => getName(u.user) },
679
+ {
680
+ header: "CREATED",
681
+ minWidth: 7,
682
+ extract: (u) => s(u.createdAt).slice(0, 10),
683
+ },
684
+ { header: "URL", minWidth: 3, maxWidth: 60, extract: (u) => s(u.url) },
685
+ ], { emptyText: "(no project updates)", itemNoun: "project update" });
686
+ }
658
687
  // ── teams ──────────────────────────────────────────────────────
659
688
  export function formatTeamList(teams) {
660
689
  return renderTable(teams.map((raw) => asObj(raw) ?? {}), [
@@ -1036,6 +1065,10 @@ export function inferKindFromPayload(value) {
1036
1065
  "progress" in obj &&
1037
1066
  ("state" in obj || "lead" in obj || "teams" in obj))
1038
1067
  return "project";
1068
+ // Project updates carry body + createdAt + user like comments; `health` is
1069
+ // the distinguishing field, so this must precede the comment check.
1070
+ if ("body" in obj && "health" in obj)
1071
+ return "project-update";
1039
1072
  if ("body" in obj && "createdAt" in obj && "user" in obj)
1040
1073
  return "comment";
1041
1074
  if (("number" in obj || "isActive" in obj) &&
@@ -1079,6 +1112,10 @@ function inferListKind(items) {
1079
1112
  }
1080
1113
  return "issue-list";
1081
1114
  }
1115
+ // Project-update rows carry body + createdAt like comments; `health` is the
1116
+ // distinguishing field, so this must precede the comment-list check.
1117
+ if ("body" in sample && "health" in sample)
1118
+ return "project-update-list";
1082
1119
  if ("body" in sample && "createdAt" in sample)
1083
1120
  return "comment-list";
1084
1121
  if ("progress" in sample && "name" in sample) {
@@ -1168,6 +1205,10 @@ export function dispatch(kind, payload, fields) {
1168
1205
  return formatMilestoneSummary((obj ?? {}));
1169
1206
  case "milestone-list":
1170
1207
  return formatMilestoneList(list ?? []);
1208
+ case "project-update":
1209
+ return formatProjectUpdateSummary((obj ?? {}));
1210
+ case "project-update-list":
1211
+ return formatProjectUpdateList(list ?? []);
1171
1212
  case "team-list":
1172
1213
  return formatTeamList(list ?? []);
1173
1214
  case "label-list":
@@ -1230,6 +1271,11 @@ export function formatLine(payload) {
1230
1271
  if (kind === "comment" && obj) {
1231
1272
  return `comment ${s(obj.id)}`;
1232
1273
  }
1274
+ if (kind === "project-update" && obj) {
1275
+ // create doesn't carry an identifier/title — health + url are the
1276
+ // stable handles a caller needs (matches the summary header style).
1277
+ return `${s(obj.health)} ${s(obj.url)}`;
1278
+ }
1233
1279
  if (kind === "relation-list") {
1234
1280
  return formatRelationLine(payload);
1235
1281
  }
@@ -78,6 +78,12 @@ function filterFields(obj, fields, unresolved) {
78
78
  const perItem = new Set();
79
79
  const projected = filterFields(item, fields, perItem);
80
80
  for (const field of fields) {
81
+ // A primitive item short-circuits: filterFields returns it
82
+ // unchanged and records NOTHING in perItem, so every field counts
83
+ // as resolved here — a mixed array holding one primitive suppresses
84
+ // the unresolved warning for all fields. Acceptable: a primitive
85
+ // item can't meaningfully be projected, and heterogeneous lists
86
+ // legitimately have per-item gaps.
81
87
  if (!perItem.has(field)) {
82
88
  resolvedSomewhere.add(field);
83
89
  }
@@ -208,20 +214,19 @@ export function outputSuccess(data) {
208
214
  }
209
215
  else if (output !== null && typeof output === "object") {
210
216
  const obj = output;
211
- if (Array.isArray(obj.data)) {
212
- output = {
213
- ...obj,
214
- data: filterFields(obj.data, fieldsFilter, unresolved),
215
- };
216
- }
217
- else if (obj.data !== null &&
217
+ if (obj.data !== null &&
218
218
  obj.data !== undefined &&
219
219
  typeof obj.data === "object") {
220
- // Envelope with an OBJECT data payload (e.g. el-git context):
221
- // project inside `data`, same as the array branch. Before
222
- // DEV-5323 this fell through to root filtering, so
223
- // `--fields branch,issueId` on an envelope returned `{}`.
224
- // Paths are relative to `data` for every envelope shape.
220
+ // Envelope with a `data` payload — project inside `data`,
221
+ // whether it's an array (list envelope) or an object (e.g.
222
+ // el-git context). Arrays ARE objects, so this single
223
+ // `typeof === "object"` check subsumes the former separate
224
+ // `Array.isArray(obj.data)` arm (DEV-5339 collapsed the two
225
+ // byte-identical branches); `filterFields` dispatches on
226
+ // array-vs-object internally. Paths are relative to `data` for
227
+ // every envelope shape. Before DEV-5323 the object case fell
228
+ // through to root filtering, so `--fields branch,issueId` on an
229
+ // envelope returned `{}`.
225
230
  output = {
226
231
  ...obj,
227
232
  data: filterFields(obj.data, fieldsFilter, unresolved),
@@ -261,11 +266,31 @@ export function outputSuccess(data) {
261
266
  // fields_unprojectable surfaced from a prior dispatch) and embed
262
267
  // them as `_warnings` on the envelope.
263
268
  const warnings = [...drainWarnings(), ...drainSummaryFieldWarnings()];
264
- if (warnings.length > 0 &&
265
- output !== null &&
266
- typeof output === "object" &&
267
- !Array.isArray(output)) {
268
- output = { ...output, _warnings: warnings };
269
+ if (warnings.length > 0) {
270
+ if (output !== null &&
271
+ typeof output === "object" &&
272
+ !Array.isArray(output)) {
273
+ output = { ...output, _warnings: warnings };
274
+ }
275
+ else {
276
+ // Bare-array (or primitive / null) output has no envelope object to
277
+ // carry `_warnings`. This is the DEV-5339 fix: previously the buffer
278
+ // was drained above but only re-embedded for object output, so a
279
+ // warning was silently dropped on a top-level array payload AND on
280
+ // `--raw` (which unwraps { data: [...] } to a bare array *before*
281
+ // this point) — losing exactly the DEV-5323 `fields_unresolved:`
282
+ // fail-visible signal on the `--raw` form our own CLAUDE.md
283
+ // recommends to agents. Route each warning to STDERR prefixed
284
+ // `_warnings: ` so the signal always reaches the consumer while
285
+ // stdout stays a pure JSON array (safe to pipe to `jq`). Mirrors the
286
+ // summary path's `_warnings:` line convention — that path uses
287
+ // logger.info because its stdout is already human text; here stdout
288
+ // must remain machine-parseable JSON, so we use logger.error
289
+ // (stderr).
290
+ for (const w of warnings) {
291
+ logger.error(`_warnings: ${w}`);
292
+ }
293
+ }
269
294
  }
270
295
  if (jqFilter) {
271
296
  const json = JSON.stringify(output);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enrichlayer/el-linear",
3
- "version": "1.34.1",
3
+ "version": "1.35.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",