Skip to content

feat: add explicit --key/--name/--uuid flags without breaking existing CLI behavior #87

Description

@iamfj

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

  1. AmbiguityresolveTeamId tries key, then falls back to name. A team name could collide with another team's key.
  2. Implicit contracts — callers don't declare what they're passing; resolvers guess at runtime via isUuid().
  3. Wasted API calls — UUID inputs still enter the resolver, run isUuid(), and only then short-circuit.
  4. 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:

  1. Positional — shorthand for --key / --name
  2. Explicit --key / --name — same as positional
  3. --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:

  • resolveTeamIdresolveTeamByKey
  • resolveProjectIdresolveProjectByName
  • resolveIssueIdresolveIssueByKey
  • resolveCycleIdresolveCycleByName
  • resolveLabelId / resolveLabelIdsresolveLabelByName / resolveLabelsByName
  • resolveMilestoneIdresolveMilestoneByName
  • resolveUserIdresolveUserByName
  • resolveStatusIdresolveStatusByName

Command Layer

Commands gain two new responsibilities:

  1. Mutual exclusion validation — reject conflicting flags before any API call
  2. 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

  • All entity references use explicit --X-key/--X-name + --X-uuid mutually exclusive flags
  • Short aliases (--team, --project, etc.) work as before (map to human-friendly variant)
  • Issue primary arg supports positional, --key, --uuid, and decomposed --team-key+--number / --team-uuid+--number
  • Labels and relation flags are repeatable
  • Multiple relation types allowed in one command
  • Resolvers renamed to single-purpose (resolveTeamByKey, resolveProjectByName, etc.)
  • Mutual exclusion validation with clear error messages
  • All help texts and documentation updated
  • npm run check:ci && npx tsc --noEmit && npm test && npm run build all pass

Metadata

Metadata

Assignees

No one assigned

    Labels

    priority:P2Important work, but not part of the immediate implementation queue

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions