Skip to content

feat(projects): add --summary, remap --description to the long field - #69

Open
torarnv wants to merge 4 commits into
joa23:mainfrom
torarnv:torarne/dev-17-projects-summary-description-json-keys
Open

feat(projects): add --summary, remap --description to the long field#69
torarnv wants to merge 4 commits into
joa23:mainfrom
torarnv:torarne/dev-17-projects-summary-description-json-keys

Conversation

@torarnv

Copy link
Copy Markdown

What

Linear gives a project two pieces of prose. In its API the short line under the project title is called description and is capped at 255 characters; the long-form document below it is content, with no limit.

The CLI passed those API names straight through, so --description wrote the short field and the long document had no flag at all. The obvious command failed on anything real:

cat spec.md | linear projects create "Rewrite" -d -
# opaque GraphQL error for any spec over 255 characters

This PR renames the two fields to match what a reader of the Linear UI would call them:

beforeafter
short line under the title--description--summary
full document(unreachable)--description

--summary is length-checked locally against the 255-character cap instead of deferring to the server's error, which named no field and suggested no fix.

Changes

  • projects create / projects update gain --summary; -d/--description now writes the long document. Both commands' help text and examples updated, including -d - for stdin.
  • core.Project and UpdateProjectInput: Summary and Description now carry the description and content struct tags. Those tags are the only place Linear's names survive — everything above them speaks in summary/description terms.
  • CreateProject takes a summary parameter alongside description, through pkg/linear/client.go and the ProjectClientOperations interface.
  • JSON output: .summary is the short line, .description is the long document.
  • Text output: labels are now Summary: and DESCRIPTION (were Description: and CONTENT).
  • README gains a short section naming the two fields.

Breaking changes

Both are documented in the CHANGELOG under Changed.

  1. --description now writes a different field. Scripts passing a short blurb should switch to --summary. Nothing errors if they don't — the text just lands in the description instead of the summary.
  2. Project JSON keys move.jq '.description' on a project now returns the long document; .summary is the short line.

Keeping the JSON keys aligned to Linear's API names was the alternative. It was rejected because it would leave --description writing one field while .description read the other — precisely the confusion this PR exists to remove.

Testing

  • make test green, go build ./... clean.
  • Create and update exercised across the matrix: summary only, description only, both, neither.
  • A 592-character description round-trips, which is what the old code could not do.
  • The summary cap checked at the 255/256 boundary — 255 accepted, 256 rejected locally with a message naming the limit and pointing at --description.

Review notes

  • Stacked on DEV-15 (torarne/dev-15-finish-9-port-stranded-dependency-commands-to-native), which removes the description-embedded metadata store. It needs to land first — retarget this to main once it does. Only the single commit feat(projects): … belongs to this PR.

`issues dependencies` read a `[robot] Metadata` block embedded in the issue
description. Nothing has written that block since 63fc213, first released in
v1.5.0, moved dependency writes to Linear's native issueRelationCreate mutation;
that commit touched eight files and internal/cli/issues.go was not among them.
The reader has not been modified since the initial commit.
So the command did not fail. It printed `none` — for every issue, including
issues with genuine native relations set through the Linear UI. `none` reads as
an answer, and the answer it reads as is "this issue is unblocked". Anyone
trusting it concludes work is ready to start when it is blocked.
`deps <issue-id>` already reads relations/inverseRelations and has done since
the first commit, so the use case is covered by a command that gets it right.
The link-deps skill listed `issues dependencies` among its verification
commands, directly below the two `deps` invocations that supersede it.
Both commands read a `[robot] Metadata` block embedded in the issue description.
Nothing has written that block since 63fc213, first released in v1.5.0, moved
dependency writes to Linear's native issueRelationCreate mutation, and `blocking`
never had a writer in any version — it has never worked.
`blocked-by` was the worse of the two. With no metadata to parse it fell through
to a hardcoded `check description or Linear UI for blocking issues`, but only
when the description was non-empty; otherwise it printed `none`. Its answer
turned on whether the description happened to be blank and never on the
relations. An issue with two genuine relations set through the Linear UI got
`none` from the commands whose job is reporting relations.
Both now call GetIssueWithRelations and read relations/inverseRelations, the
same source `deps` has read correctly since the first commit. Linear stores a
blocker as "blocker blocks queried", so blockers come off the inverse side; both
connections also carry related/duplicate/similar relations, which say nothing
about blocking and are filtered out.
Output is one line per issue as `ABC-123 [State] Title`, replacing the Go slice
syntax (`[DEV-12 DEV-9]`) that `fmt.Printf("%v")` on a []string would have
printed had the store ever held data. The state is not decoration: a blocker
already in Done does not block anything, and the identifier alone cannot tell
you that. Completed blockers are listed rather than filtered, because silently
dropping a real relation is the failure this commit exists to fix. Empty results
still print `none`.
The line builders are pure functions over IssueWithRelations, so they are
testable without a mock client — the first coverage this area has had since
c449bf2 deleted the metadata tests.
The store kept structured data by appending a block to an issue's description
and to a project's description and content:
<details><summary>🤖 Metadata</summary>
```json
{ "dependencies": ["DEV-1", "DEV-2"] }
```
</details>
pkg/linear/metadata/metadata.go justified this as "a hidden collapsible section
in descriptions to avoid cluttering the UI". It was not hidden. Linear's editor
is rich text and converts recognised Markdown as it is typed; it has no raw-HTML
passthrough, so the <details> and <summary> tags rendered as literal visible
text and the json fence rendered as a code block. Every user looking at the
issue in Linear saw the block sitting at the bottom of the description.
The CLI did write to this store. `issues create` and `issues update` fed
--depends-on and --blocked-by into UpdateIssueMetadataKey under the keys
"dependencies" and "blocked_by", from the first release up to and including
v1.4.1. 63fc213, first released in v1.5.0, replaced those writes with native
issueRelationCreate, and nothing has written to the store since. The project-side
writers were different: UpdateProjectMetadataKey and RemoveProjectMetadataKey
were never reachable from any command, only from the Client passthrough, so no
project description ever received a block from this CLI. The last readers went
with `issues dependencies` and the blocked-by/blocking port in the two preceding
commits.
Removed: the pkg/linear/metadata package; UpdateIssueMetadataKey and
RemoveIssueMetadataKey on the issues client; UpdateProjectMetadataKey and
RemoveProjectMetadataKey on projects; the four Client passthroughs;
validation.IsValidMetadataKey, whose only caller was UpdateIssueMetadataKey; the
Metadata field on core.Issue, core.ParentIssue, core.Project and
core.IssueWithDetails; and the Metadata Management section of pkg/linear/doc.go.
core.Attachment.Metadata stays. That is Linear's own Attachment.metadata
JSONObject, unrelated to this store.
Two consequences beyond the deletion. Extraction ran on every issue and project
read to strip these blocks back out before display, so any issue written by
v1.4.1 or earlier will now show its block in CLI output. That is a smaller change
than it sounds: the block has been plainly visible in Linear's own UI the whole
time, and extraction only ever hid it from the CLI, so this makes the two agree.
And UpdateIssue no longer fetches the issue before every description update just
to re-inject metadata, which drops an API round-trip from each description edit.
BREAKING CHANGE: pkg/linear is a public library as of joa23#47 and doc.go documented
the metadata API as a supported generic key/value feature. Library consumers
using it will not compile. pkg.go.dev reports no known importers for pkg/linear
or any of its subpackages, and a GitHub code search for the removed symbols
returns hits only in this repository.
Linear gives a project two text fields. Its API calls the short one under the
project title "description" and caps it at 255 characters; the long-form
document below it is "content", with no limit. The CLI mirrored those API names
straight through, so `projects create -d` and `projects update -d` wrote the
255-character field and the long document had no flag at all.
That made the obvious command fail on anything real:
cat spec.md | linear projects create "Rewrite" -d -
Any spec longer than 255 characters came back as an opaque GraphQL error, and
the field a user would call the description — the one holding the actual
document in the UI — was unreachable.
--description now writes the long field and a new --summary writes the short
one, so each flag names what a reader of the Linear UI would call it. --summary
is length-checked before the request, so an over-long one is reported against
the flag that carries it instead of arriving as "Argument Validation Error"
naming no field. The measuring is left to validation.ValidateStringLength; only
the suggestion to move the text to --description is added here, since the
library layer knows nothing about flags.
The same swap runs through core.Project and UpdateProjectInput, where Summary
and Description now carry the `description` and `content` struct tags. Those
tags are the only place Linear's names survive; everything above them speaks in
summary/description terms.
JSON output follows the flags: a project's `description` key is now the long
document and `summary` is the short line. Text output labels them `Summary:` and
`DESCRIPTION`. Keeping the keys aligned to Linear's API names instead would have
left `--description` writing one field while `.description` read the other,
which is the confusion this commit exists to remove.
Breaking for callers passing a short blurb to --description: it now lands in the
description rather than the summary. Nothing errors; the text moves.
@torarnv
torarnvforce-pushed the torarne/dev-17-projects-summary-description-json-keys branch from f9185f3 to d142cccCompareAugust 9, 2026 12:35
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@torarnv