Summary
Replace implicit ID auto-detection (UUID vs key/name guessing in resolvers) with explicit, mutually exclusive CLI flags for every entity reference. This is a breaking change that makes the CLI contract unambiguous for both humans and LLM agents.
Before
linearis issues read DAT-123 # resolver guesses: identifier
linearis issues read 550e8400-... # resolver guesses: UUID
linearis issues create --team ENG # resolver guesses: key? name?
linearis issues create --team 550e8400-...# resolver guesses: UUID
After
linearis issues read DAT-123 # positional = --key (default)
linearis issues read --key DAT-123 # explicit key
linearis issues read --uuid 550e8400-... # explicit UUID
linearis issues read --team-key DAT --number 123 # decomposed (primary arg only)
linearis issues read --team DAT --number 123 # --team aliases --team-key
linearis issues create --team-key ENG # explicit: team key
linearis issues create --team-uuid 550e8400-... # explicit: team UUID
Motivation
- Ambiguity —
resolveTeamId tries key, then falls back to name. A team name could collide with another team's key.
- Implicit contracts — callers don't declare what they're passing; resolvers guess at runtime via
isUuid().
- Wasted API calls — UUID inputs still enter the resolver, run
isUuid(), and only then short-circuit.
- Agent ergonomics — LLM agents benefit from explicit flag contracts; auto-detection is a source of subtle bugs.
Design
Naming Convention
Entities with a Linear key field use --X-key; entities resolved by name use --X-name. The short alias always maps to the human-friendly variant.
| Entity |
Short alias |
Human flag |
UUID flag |
Repeatable? |
| Team |
--team |
--team-key |
--team-uuid |
No |
| Issue (option) |
--issue |
--issue-key |
--issue-uuid |
No |
| Project |
--project |
--project-name |
--project-uuid |
No |
| Assignee/User |
--assignee |
--assignee-name |
--assignee-uuid |
No |
| Cycle |
--cycle |
--cycle-name |
--cycle-uuid |
No |
| Status |
--status |
--status-name |
--status-uuid |
No |
| Milestone |
--milestone |
--milestone-name |
--milestone-uuid |
No |
| Parent ticket |
--parent-ticket |
--parent-ticket-key |
--parent-ticket-uuid |
No |
| Label |
--label |
--label-name |
--label-uuid |
Yes |
| Blocks |
--blocks |
--blocks-key |
--blocks-uuid |
Yes |
| Blocked-by |
--blocked-by |
--blocked-by-key |
--blocked-by-uuid |
Yes |
| Relates-to |
--relates-to |
--relates-to-key |
--relates-to-uuid |
Yes |
| Duplicate-of |
--duplicate-of |
--duplicate-of-key |
--duplicate-of-uuid |
Yes |
| Remove-relation |
--remove-relation |
--remove-relation-key |
--remove-relation-uuid |
Yes |
Primary Argument (positional)
Commands with a primary entity argument (issues read <issue>, cycles read <cycle>, etc.) support three forms, all mutually exclusive:
- Positional — shorthand for
--key / --name
- Explicit
--key / --name — same as positional
--uuid — direct UUID passthrough
Validation rejects combining positional with --key/--name/--uuid.
Issue Decomposed Form (primary argument only)
Issue commands additionally support decomposed identification:
# These four strategies are mutually exclusive:
linearis issues read DAT-123 # strategy 1: positional (= --key)
linearis issues read --key DAT-123 # strategy 2: explicit key
linearis issues read --uuid 550e8400-... # strategy 3: UUID
linearis issues read --team-key DAT --number 123 # strategy 4a: team key + number
linearis issues read --team-uuid 7a8b... --number 123 # strategy 4b: team UUID + number
Validation:
--number requires exactly one of --team-key or --team-uuid (and vice versa)
--team-key and --team-uuid are mutually exclusive
- Decomposed form only applies to primary issue argument, not option flags like
--parent-ticket
Multi-Value References
Labels and relation flags use repeatable flags instead of comma-separated strings:
# Before (comma-separated, single relation)
linearis issues create --labels "bug,feat" --blocks DAT-456
# After (repeatable, multiple relations)
linearis issues create \
--label bug --label feat --label-uuid 550e8400-... \
--blocks-key DAT-456 --blocks-key DAT-789 \
--relates-to-uuid 660f9500-...
All three forms (--label, --label-name, --label-uuid) accumulate into one merged set. The single-relation-per-command restriction is removed.
Architecture Changes
Resolver Layer
Resolvers simplify from multi-strategy guesswork to single-purpose lookups:
// Before
export async function resolveTeamId(client, keyOrNameOrId: string): Promise<string> {
if (isUuid(keyOrNameOrId)) return keyOrNameOrId;
// try key... try name... throw
}
// After
export async function resolveTeamByKey(client, key: string): Promise<string> {
// key lookup only → UUID or throw
}
Renames:
resolveTeamId → resolveTeamByKey
resolveProjectId → resolveProjectByName
resolveIssueId → resolveIssueByKey
resolveCycleId → resolveCycleByName
resolveLabelId / resolveLabelIds → resolveLabelByName / resolveLabelsByName
resolveMilestoneId → resolveMilestoneByName
resolveUserId → resolveUserByName
resolveStatusId → resolveStatusByName
Command Layer
Commands gain two new responsibilities:
- Mutual exclusion validation — reject conflicting flags before any API call
- Routing —
--uuid skips resolver; --key/--name calls the appropriate resolver
New Common Utilities
- Discriminated union type:
{ kind: 'key' | 'name' | 'uuid', value: string } for entity references
- Validation helper: checks mutual exclusion across flag groups, produces clear error messages
isUuid() remains for input validation on --uuid flags (no longer used for type guessing)
Unchanged
- Service layer — still receives UUIDs only, no changes
- GraphQL layer — no query/mutation changes
createContext() — shape unchanged
Breaking Changes
| Change |
Migration |
--labels "a,b,c" (comma-separated) |
--label a --label b --label c (repeatable, singular) |
--project-milestone <val> |
--milestone-name <val> / --milestone <val> |
| UUID auto-detection removed |
Passing a UUID to --team now does key lookup, not passthrough. Use --team-uuid |
| Single-relation restriction removed |
--blocks, --relates-to, etc. are repeatable and mixable |
| Relation flags renamed |
--blocks X → --blocks-key X / --blocks X (alias) |
Command Migration Map
High impact
issues create — all option flags get typed variants
issues update — primary arg + all option flags
issues read — primary arg with decomposed form
Medium impact
comments list/create — primary issue arg gets typed variants
cycles list/read — --team typed variants, read gets primary arg change
milestones list/read/create/update — --project typed variants, read/update get primary arg change
documents list/create/update — --project, --team, --issue typed variants
labels list — --team typed variants
No impact
teams list, users list, files upload/download — no entity references
documents read/delete, comments reply/edit/delete — UUID-only, no change
- All
usage subcommands — display only (but text content updates)
Testing Impact
- Resolver tests: all rewrite — remove
isUuid() passthrough tests, test single-purpose lookup only
- New command validation tests: mutual exclusion, repeatable flag accumulation, positional + flag conflicts
- Integration tests: all update to use new flag syntax
- Estimated scope: ~30-40 files across resolvers, commands, tests, docs
Documentation Impact
README.md — all usage examples
AGENTS.md — architecture section, anti-patterns, decision tree, resolver contract
docs/architecture.md, docs/development.md, docs/testing.md — pattern references
DomainMeta for every command — argument descriptions
- Help text on every command
USAGE.md — regenerated via npm run generate:usage
Acceptance Criteria
Summary
Replace implicit ID auto-detection (UUID vs key/name guessing in resolvers) with explicit, mutually exclusive CLI flags for every entity reference. This is a breaking change that makes the CLI contract unambiguous for both humans and LLM agents.
Before
After
Motivation
resolveTeamIdtries key, then falls back to name. A team name could collide with another team's key.isUuid().isUuid(), and only then short-circuit.Design
Naming Convention
Entities with a Linear
keyfield use--X-key; entities resolved by name use--X-name. The short alias always maps to the human-friendly variant.--team--team-key--team-uuid--issue--issue-key--issue-uuid--project--project-name--project-uuid--assignee--assignee-name--assignee-uuid--cycle--cycle-name--cycle-uuid--status--status-name--status-uuid--milestone--milestone-name--milestone-uuid--parent-ticket--parent-ticket-key--parent-ticket-uuid--label--label-name--label-uuid--blocks--blocks-key--blocks-uuid--blocked-by--blocked-by-key--blocked-by-uuid--relates-to--relates-to-key--relates-to-uuid--duplicate-of--duplicate-of-key--duplicate-of-uuid--remove-relation--remove-relation-key--remove-relation-uuidPrimary Argument (positional)
Commands with a primary entity argument (
issues read <issue>,cycles read <cycle>, etc.) support three forms, all mutually exclusive:--key/--name--key/--name— same as positional--uuid— direct UUID passthroughValidation rejects combining positional with
--key/--name/--uuid.Issue Decomposed Form (primary argument only)
Issue commands additionally support decomposed identification:
Validation:
--numberrequires exactly one of--team-keyor--team-uuid(and vice versa)--team-keyand--team-uuidare mutually exclusive--parent-ticketMulti-Value References
Labels and relation flags use repeatable flags instead of comma-separated strings:
All three forms (
--label,--label-name,--label-uuid) accumulate into one merged set. The single-relation-per-command restriction is removed.Architecture Changes
Resolver Layer
Resolvers simplify from multi-strategy guesswork to single-purpose lookups:
Renames:
resolveTeamId→resolveTeamByKeyresolveProjectId→resolveProjectByNameresolveIssueId→resolveIssueByKeyresolveCycleId→resolveCycleByNameresolveLabelId/resolveLabelIds→resolveLabelByName/resolveLabelsByNameresolveMilestoneId→resolveMilestoneByNameresolveUserId→resolveUserByNameresolveStatusId→resolveStatusByNameCommand Layer
Commands gain two new responsibilities:
--uuidskips resolver;--key/--namecalls the appropriate resolverNew Common Utilities
{ kind: 'key' | 'name' | 'uuid', value: string }for entity referencesisUuid()remains for input validation on--uuidflags (no longer used for type guessing)Unchanged
createContext()— shape unchangedBreaking Changes
--labels "a,b,c"(comma-separated)--label a --label b --label c(repeatable, singular)--project-milestone <val>--milestone-name <val>/--milestone <val>--teamnow does key lookup, not passthrough. Use--team-uuid--blocks,--relates-to, etc. are repeatable and mixable--blocks X→--blocks-key X/--blocks X(alias)Command Migration Map
High impact
issues create— all option flags get typed variantsissues update— primary arg + all option flagsissues read— primary arg with decomposed formMedium impact
comments list/create— primary issue arg gets typed variantscycles list/read—--teamtyped variants, read gets primary arg changemilestones list/read/create/update—--projecttyped variants, read/update get primary arg changedocuments list/create/update—--project,--team,--issuetyped variantslabels list—--teamtyped variantsNo impact
teams list,users list,files upload/download— no entity referencesdocuments read/delete,comments reply/edit/delete— UUID-only, no changeusagesubcommands — display only (but text content updates)Testing Impact
isUuid()passthrough tests, test single-purpose lookup onlyDocumentation Impact
README.md— all usage examplesAGENTS.md— architecture section, anti-patterns, decision tree, resolver contractdocs/architecture.md,docs/development.md,docs/testing.md— pattern referencesDomainMetafor every command — argument descriptionsUSAGE.md— regenerated vianpm run generate:usageAcceptance Criteria
--X-key/--X-name+--X-uuidmutually exclusive flags--team,--project, etc.) work as before (map to human-friendly variant)--key,--uuid, and decomposed--team-key+--number/--team-uuid+--numberresolveTeamByKey,resolveProjectByName, etc.)npm run check:ci && npx tsc --noEmit && npm test && npm run buildall pass