Skip to content

feat!: replace implicit ID auto-detection with explicit mutually exclusive --key/--name/--uuid flags #87

@iamfj

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

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions