{"id":51468415,"url":"https://github.com/smorinlabs/agent2linear","last_synced_at":"2026-07-06T14:01:05.456Z","repository":{"id":322376821,"uuid":"1083409777","full_name":"smorinlabs/agent2linear","owner":"smorinlabs","description":"agent2linear is a command-line interface for Linear designed to work seamlessly with both humans and AI agents. Unlike Linear's web interface or standard APIs, agent2linear is built to minimize token usage and context window waste - critical for AI workflows where every token counts.","archived":false,"fork":false,"pushed_at":"2026-06-26T16:34:51.000Z","size":1815,"stargazers_count":6,"open_issues_count":0,"forks_count":2,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-26T17:24:26.857Z","etag":null,"topics":["agent","agent-orchestration","agent-skills","agentic-workflow","agents","cli","linear","linearapp","project-management"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/smorinlabs.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-10-26T00:28:42.000Z","updated_at":"2026-06-26T16:34:56.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/smorinlabs/agent2linear","commit_stats":null,"previous_names":["smorin/agent2linear","smorinlabs/agent2linear"],"tags_count":22,"template":false,"template_full_name":null,"purl":"pkg:github/smorinlabs/agent2linear","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smorinlabs%2Fagent2linear","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smorinlabs%2Fagent2linear/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smorinlabs%2Fagent2linear/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smorinlabs%2Fagent2linear/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/smorinlabs","download_url":"https://codeload.github.com/smorinlabs/agent2linear/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smorinlabs%2Fagent2linear/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35193679,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-06T02:00:07.184Z","response_time":106,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["agent","agent-orchestration","agent-skills","agentic-workflow","agents","cli","linear","linearapp","project-management"],"created_at":"2026-07-06T14:01:04.596Z","updated_at":"2026-07-06T14:01:05.426Z","avatar_url":"https://github.com/smorinlabs.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# agent2linear\n\n**agent2linear** is a command-line interface for Linear designed to work seamlessly with both humans and AI agents. Unlike Linear's web interface or standard APIs, agent2linear is built to **minimize token usage and context window waste** - critical for AI workflows where every token counts.\n\n**Why it exists**: Linear uses long UUIDs (`team_9b2e5f8a-c3d1-4e6f-8a9b-2c3d4e5f6a7b`) that consume tokens and confuse agents. agent2linear replaces them with memorable aliases (`--team backend`), persistent config defaults (global or project-local), and natural date formats (`Q1 2025`). Set your `defaultTeam`, `defaultInitiative`, and other preferences once, then create issues and projects without repeating the same parameters. The result: **10x fewer tokens** for the same operations, cleaner agent prompts, and workflows that just work.\n\n**Use `agent2linear` or the short `a2l` alias - both commands work identically.**\n\n---\n\n## 🎯 Why Aliases? (The Killer Feature)\n\n**The Problem**: Linear uses UUIDs everywhere. They're impossible to remember and painful for both humans and AI agents.\n\n**The Solution**: agent2linear lets you use friendly aliases instead of UUIDs.\n\n### Before vs After\n\n```bash\n# ❌ Without aliases (UUID Hell)\na2l project create \\\n  --title \"Mobile Redesign\" \\\n  --team team_9b2e5f8a-c3d1-4e6f-8a9b-2c3d4e5f6a7b \\\n  --initiative init_4d5e6f7a-8b9c-0d1e-2f3a-4b5c6d7e8f9a \\\n  --status status_3c4d5e6f-7a8b-9c0d-1e2f-3a4b5c6d7e8f\n\n# ✅ With aliases (Clean \u0026 Readable)\na2l project create \\\n  --title \"Mobile Redesign\" \\\n  --team mobile \\\n  --initiative q2-product \\\n  --status planned\n```\n\n### Benefits\n\n**For Humans:**\n- 🧠 **Memorable**: `--team backend` vs `--team team_9b2e5f8a...`\n- 📖 **Self-documenting**: Commands are readable without looking up IDs\n- ⚡ **Faster**: Type less, work faster\n\n**For AI Agents (Why this tool exists!):**\n- 💭 **Context persistence**: AI remembers \"backend\" across conversations, not UUIDs\n- 🎯 **Fewer errors**: Less likely to corrupt \"backend\" than a 36-character UUID\n- 🪙 **Token efficient**: Aliases save tokens in AI context windows\n- 🗣️ **Natural language**: Maps to how humans describe things\n\n### What You Can Alias\n\nCreate aliases for **11 entity types**: `team`, `initiative`, `project`, `member`, `workflow-state`, `project-status`, `issue-label`, `project-label`, `issue-template`, `project-template`, `cycle`\n\n### Quick Start with Aliases\n\nThe setup wizard automatically creates helpful aliases:\n\n```bash\na2l setup\n# Auto-creates aliases for:\n#   - Workflow states (todo, in-progress, done, canceled)\n#   - Project statuses (planned, started, completed, paused)\n#   - Team members (john-smith, jane-doe)\n```\n\n**Manual alias management:**\n\n```bash\n# Add aliases\na2l alias add team backend team_abc123\n\n# Bulk sync (auto-generates from names)\na2l teams sync-aliases\n\n# List aliases\na2l alias list\na2l alias list teams\n```\n\n**💡 Learn more**: See the [Aliases](#aliases) section for full documentation.\n\n---\n\n## ⚡ Quick Start (5 minutes)\n\nGet up and running with agent2linear in 3 easy steps:\n\n### Step 1: Install (or use with npx)\n\n**Option A: Global Install** (recommended for frequent use)\n```bash\nnpm install -g agent2linear\n```\n\n**Option B: Use with npx** (no installation needed - great for trying it out!)\n```bash\nnpx agent2linear --version\n```\n\nBoth methods work identically. Examples below use the short alias `a2l` for convenience.\n\n### Step 2: Run the Setup Wizard\n\n```bash\nagent2linear setup\n```\n\n**Or with npx:**\n```bash\nnpx agent2linear setup\n```\n\nThe interactive setup wizard will:\n- ✅ Guide you to get your Linear API key → https://linear.app/settings/api\n- ✅ Validate your connection to Linear\n- ✅ Help you select your default team\n- ✅ Optionally create helpful aliases (workflow states, members, project statuses)\n- ✅ Walk you through key features with a 7-screen tutorial\n\n**That's it!** The wizard configures everything for you.\n\n### Step 3: Start Working\n\n```bash\n# Create your first issue (auto-assigned to you!)\na2l issue create --title \"My first issue\"\n\n# List YOUR assigned issues (the default - most common command!)\na2l issue list\n\n# List all issues in your team\na2l issue list --all\n\n# Create a project\na2l project create --title \"Q1 Goals\"\n\n# Get help anytime\na2l issue create --help\n```\n\n**🎉 You're now productive!** Explore the full documentation below for advanced features.\n\n💡 **Tip:** The setup wizard created helpful aliases for you. Try `a2l alias list` to see them.\n\n---\n\n## Installation\n\n### For End Users\n\n**Global Install** (recommended for regular use):\n\n```bash\nnpm install -g agent2linear\n\n# Verify installation\nagent2linear --version\na2l --version\n```\n\n**Use with npx** (no installation needed):\n\n```bash\n# Try it out without installing\nnpx agent2linear --help\nnpx agent2linear setup\n\n# Both commands work: agent2linear and a2l\nnpx agent2linear issue list\n```\n\n**Which should you choose?**\n- Install globally if you'll use agent2linear frequently\n- Use npx for trying it out or one-off usage\n- Both methods provide identical functionality\n\n### For Development\n\n```bash\ngit clone https://github.com/smorin/agent2linear.git\ncd agent2linear\nnpm install\nnpm run build\n```\n\n## Configuration\n\n### Recommended: Use the Setup Wizard\n\nThe easiest way to configure agent2linear is with the interactive setup wizard:\n\n```bash\nagent2linear setup\n```\n\nThe wizard will:\n- Guide you to get your Linear API key from https://linear.app/settings/api\n- Let you choose between saving to config file or using an environment variable\n- Validate your API key by connecting to Linear\n- Help you select your default team interactively\n- Optionally create helpful aliases for workflow states, members, and project statuses\n- Provide a guided tour of features\n\n### Alternative: Manual Configuration\n\nIf you prefer manual setup, you can set your Linear API key as an environment variable:\n\n```bash\nexport LINEAR_API_KEY=lin_api_xxxxxxxxxxxx\n```\n\nOr use the interactive config editor:\n\n```bash\nagent2linear config edit\n```\n\n**Configuration files:**\n- Global: `$XDG_CONFIG_HOME/agent2linear/config.json` (default: `~/.config/agent2linear/config.json`)\n- Project: `.agent2linear/config.json`\n\nProject configuration is discovered by walking up from the current directory toward `$HOME` for the nearest `.agent2linear/` directory. Caches are stored separately under `$XDG_CACHE_HOME/agent2linear/\u003cworkspace-key\u003e/` (default: `~/.cache/agent2linear/\u003cworkspace-key\u003e/`), keyed per workspace so they never mix between accounts.\n\n**See also:** Run `agent2linear config --help` for all configuration options.\n\n### Context-Aware Overrides (`overrides[]`)\n\nAdd an optional `overrides` array to any `config.json` (global or repo) to resolve **defaults by context** — filesystem location, repo identity, or branch — instead of one flat value per scope. Each rule is `{ \"when\": { … }, …defaults }`: when the current context matches `when`, that rule's values apply. Resolution is **field-level** (a rule setting only `defaultTeam` leaves `defaultInitiative` to fall back), and `apiKey` is **never** overridable.\n\n```jsonc\n{\n  \"defaultTeam\": \"platform\",                 // catch-all (lowest precedence)\n  \"overrides\": [\n    { \"when\": { \"path\": \"cli/**\" },          // repo-root-anchored path glob\n      \"defaultTeam\": \"cli-team\",\n      \"aliases\": { \"teams\": { \"default\": \"team_cli123\" } } },  // per-rule alias remap\n    { \"when\": { \"owner\": \"acme\" },           // repo identity (reads `origin`)\n      \"defaultTeam\": \"acme-eng\" },\n    { \"when\": { \"branch\": \"release/*\" },     // current branch\n      \"defaultInitiative\": \"hardening\" },\n    { \"when\": { \"anyOf\": [ { \"owner\": \"acme\" },\n                           { \"remote\": \"upstream\", \"owner\": \"acme\" } ] },  // fork: base OR upstream\n      \"defaultTeam\": \"acme-eng\" }\n  ]\n}\n```\n\n**`when` matchers:** `path` (relative = repo-root-anchored gitignore-style glob; leading `~/` or `/` = absolute disk match), `repo`/`owner`/`host` (identity, normalized to `host/owner/name` from the git remote; reads `origin` unless a `remote` qualifier is given), `branch`, and the boolean composites `allOf`/`anyOf`/`not`. The `remote` qualifier picks which remote(s) identity reads (a name, a list, or `\"*\"`); a bare `{ \"remote\": \"upstream\" }` matches \"this checkout has an `upstream` remote\".\n\n**Precedence (most → least):** repo scope beats global regardless of specificity; within a scope, most-specific wins (exact `repo` \u003e `repo` glob/`owner`/`host` \u003e `path` \u003e `branch` \u003e catch-all); ties break by declaration order. Configs without `overrides` behave exactly as before.\n\n**Authoring rules from the CLI — `config override` (alias `config ov`):** the full lifecycle without hand-editing JSON. Every rule is addressable by a stable, human-chosen **label** (`\u003clabel\u003e`); legacy unlabeled rules are addressable by `#\u003cindex\u003e`. Each command takes `-g/--global` (default) or `-p/--project`, and supports `--json` (and `--dry-run` for `add`/`edit`).\n\n```bash\n# add — one or more `when` criteria + one or more values, named \u003clabel\u003e\na2l config ov add cli-team --when-path 'cli/**' --set defaultTeam=frontend --project\na2l config ov add release  --when-branch 'release/*,main' --set defaultTeam=ship --global   # comma/repeat = OR within a facet\na2l config ov add web-alias --when-repo acme/web --alias team.default=team_cli123 --project  # per-rule alias remap\na2l config ov add nested   --when-json '{\"anyOf\":[{\"path\":\"cli/**\"},{\"branch\":\"main\"}]}' --set defaultTeam=cli --global\na2l config ov add fallback --when-json '{}' --set defaultTeam=platform --global              # explicit catch-all\n\n# list / get — context-independent inventory; each row carries a static specificity tag\na2l config ov list                    # both scopes (project + global)\na2l config ov get cli-team --project  # one rule in full (by label or #\u003cindex\u003e)\n\n# edit / remove / move — manage an existing rule by selector\na2l config ov edit cli-team --set defaultInitiative=q3 --unset defaultProject --project  # field-by-field value merge\na2l config ov edit '#2' --id release --project                                           # label a legacy #\u003cindex\u003e rule\na2l config ov move release --before cli-team --project                                   # reorder (controls equal-specificity tie-break)\na2l config ov remove release --project                                                   # alias: rm\n```\n\n`--set` accepts only the overridable defaults (`defaultTeam`, `defaultInitiative`, `defaultProject`, the templates, `defaultPrompt`, `defaultAutoAssignLead`) — `apiKey` can never be set via an override. Flag-sugar (`--when-*`) and the `--when-json` escape hatch are mutually exclusive; comma-lists or repeated `--when-\u003cfacet\u003e` express OR **within** a facet, and `--when-json` is the escape hatch for arbitrary nested trees. Globs and label uniqueness are validated **before** writing, so a malformed or never-matching rule is rejected up front.\n\n**Targeting another directory — `-C, --cwd`:** a global, git-style flag makes *any* command resolve as if launched in `\u003cdir\u003e` (config discovery, override matching, and relative path args). Falls back to `$AGENT2LINEAR_CWD`, then the current directory.\n\n```bash\na2l -C ~/work/acme/web/apps/mobile issue create --title \"Bug\"   # resolves defaults as if in apps/mobile\nAGENT2LINEAR_CWD=~/work/acme/web a2l issue create --title \"Bug\" # same, via env\n```\n\n**Debugging routing — `config explain`:** prints the resolved context (contextDir, repoRoot, remotes, branch) and the winning rule per field — named by its **label** (`config ov` selector), so you can jump straight to `config ov edit \u003clabel\u003e`. A `rules:` section then annotates **every** rule (both scopes) with ✓/✗ for this context (the ✓/✗ comes from the same matcher that drives resolution, so the audit can never disagree) and echoes each rule's `when`. `config get \u003ckey\u003e [dir]` returns a single override-resolved field for scripting.\n\n```bash\na2l config explain ~/work/acme/web/apps/mobile        # positional dir = sugar for -C\na2l config explain --json                             # machine-readable (resolved map + a rules[] audit array), for agents\na2l config get defaultTeam apps/web                   # one override-resolved field (names the winning rule's label)\n```\n\n## Prompt Templates\n\nAsk `a2l` for the **right markdown prompt to follow before creating a Linear issue**, selected by context. This is read-side only today: prompts are **hand-authored** in a committable `prompts.json` (CRUD and interpolation are planned). It is fully additive — a config with no prompts behaves exactly as before, and `apiKey`/workspace selection is never affected.\n\nThe `prompt` command is also aliased as **`skill`**: `a2l skill get` returns the right **skill** (prompt) to call — the context-appropriate guidance an agent should follow before creating an issue. `skill` and `prompt` are interchangeable everywhere (`skill get`/`skill list`/`skill explain` = `prompt get`/`prompt list`/`prompt explain`).\n\n### Authoring prompts (`prompts.json`)\n\nPrompts live in a `prompts.json` next to your config:\n\n- **Global:** `$XDG_CONFIG_HOME/agent2linear/prompts.json` (default: `~/.config/agent2linear/prompts.json`)\n- **Project:** `.agent2linear/prompts.json` (nearest, walking up from the current directory)\n\nA project prompt **overwrites** a global one of the same name. Each prompt sets **exactly one** of an inline `body` or a `bodyFile`:\n\n```jsonc\n{\n  \"prompts\": {\n    \"general\":         { \"description\": \"Default issue prompt\", \"body\": \"## Title\\nWrite a clear, scoped title.\\n\" },\n    \"payments-issue\":  { \"description\": \"Payments convention\",  \"bodyFile\": \"prompts/payments.md\" }\n  },\n  \"promptRules\": [\n    { \"when\": { \"team\": \"payments\" }, \"prompt\": \"payments-issue\" }   // team layer (see below)\n  ]\n}\n```\n\nA **relative** `bodyFile` is anchored to the directory of the `prompts.json` that declares it (not your current directory), so a committed project `prompts.json` referencing `prompts/payments.md` resolves portably regardless of where you run `a2l`. Absolute and `~/`-prefixed paths are used as-is.\n\n### Selecting a prompt\n\n```bash\na2l prompt get                       # the prompt that applies for the current directory (raw markdown)\na2l prompt get payments-issue        # an exact prompt by unique name (highest precedence)\na2l prompt get --team payments       # the team-layer prompt for the payments team\na2l prompt get --json                # a { name, source, selection, body, context } envelope (for agents)\na2l skill get                        # `skill` is an alias for `prompt` — the right skill to call here\na2l issue prompt                     # alias for `prompt get` (same flags) — handy before `issue create`\n```\n\n`defaultPrompt` is a first-class config key — a local prompt **name**. Set it like any other default, and it is **wired into `overrides[]`** so it can be resolved by `path`/`repo`/`branch`:\n\n```jsonc\n{\n  \"defaultPrompt\": \"general\",                                      // general default (a name in prompts.json)\n  \"overrides\": [\n    { \"when\": { \"path\": \"apps/mobile/**\" }, \"defaultPrompt\": \"mobile-issue\" }  // location-specific prompt\n  ]\n}\n```\n\n```bash\na2l config set defaultPrompt general                   # validated: the name must exist in prompts.json\na2l -C apps/mobile prompt get                          # → the location prompt (path override)\n```\n\n**Team layer (`promptRules`).** Authored **nested**, exactly like `overrides[]`: `{ \"when\": { \"team\": \"\u003cid|alias\u003e\", …location matchers }, \"prompt\": \"\u003cname\u003e\" }`. The resolved team is `--team` (if given) or your `defaultTeam`; both the rule's `team` and your team are normalized through team aliases, so `payments` matches whether expressed as the alias or the raw `team_*` id. (A `team` key is honored **only** here — it is ignored inside config `overrides[]`.)\n\n**Selection precedence:** explicit name → specific location override (`path`/`repo`/`owner`/`host`) → team (`promptRules`) → general `defaultPrompt` (top-level, or a branch-only / catch-all override) → error. An explicit `--team X` with no matching `promptRule` is a **hard error** (exit 1); a *derived* `defaultTeam` with no rule simply falls through to the general prompt. A branch-only override is general-tier (team outranks branch).\n\n**`--force` (team-first).** Scoped to an explicit `--team`: evaluate the team layer **first**, so a matching `promptRule` outranks any location override; if no rule matches it is a hard error even when a location override or general default would otherwise resolve. `--force` without `--team` is a no-op.\n\n```bash\na2l prompt get --team payments --force                 # force the payments team prompt (beats a location override)\n```\n\n### Listing and explaining\n\n```bash\na2l prompt list                      # available prompt names, grouped by source (names only)\na2l prompt list --descriptions       # include each prompt's description\na2l prompt list pay                  # filter to names containing \"pay\" (case-insensitive)\na2l prompt list --format json        # complete records (name, description, source); the filter still applies\n```\n\n`prompt explain` mirrors `config explain` and adds the team layer (which `config explain` alone can't show): the context, the resolved `defaultPrompt` + provenance, the team (input/resolved id), the **matched `promptRule`** (shown even when a location override outranks it), and the final selection + tier. Unlike `prompt get`, it never exits 1 — an unresolved selection is reported in the trace.\n\n```bash\na2l prompt explain                   # what would be selected here, and why\na2l prompt explain apps/mobile       # explain as if in apps/mobile (positional dir = sugar for -C)\na2l prompt explain --team payments --json   # machine-readable decision trace\n```\n\n## Multi-Workspace \u0026 Profiles\n\nIf you work across **multiple Linear workspaces** (e.g. personal + several companies), agent2linear can hold several keys at once and automatically target the right workspace per repository — without naming the workspace in every command.\n\n\u003e **The simple case is unchanged.** If you use a single key (`LINEAR_API_KEY` env var or `apiKey` in config), everything works exactly as before. Profiles and workspaces are strictly opt-in.\n\n### Concepts: `global \u003c profile \u003c repo`\n\n- A **workspace** is a Linear workspace, addressed by one `lin_api_…` key. Keys live in a **secrets registry** that is never committed.\n- A **profile** is a named bundle of *settings* — which workspace to use, optional defaults (`defaultTeam`, `defaultInitiative`, …), and **detection rules**. Profiles live in committable config and **never contain a raw key**.\n- Resolved configuration merges **`global \u003c profile \u003c repo`**, so a repo override beats a profile default beats a global default.\n\n### Register workspaces and profiles\n\n```bash\n# 1. Register a workspace's key in the secrets registry (piped, never in shell history)\necho \"$ACME_KEY\" | a2l workspace add acme --api-key -\na2l workspace list                    # names + masked keys\na2l workspace current                 # the resolved active workspace + source (offline)\n\n# 2. Define a profile that points at it, with optional defaults\na2l profile add acme --workspace acme --default-team backend --default-initiative q3\na2l config set defaultProfile acme    # persisted fallback default\n\n# 3. Auto-detect: route any repo under a GitHub org to this profile\na2l profile match add acme --git-remote-owner acme-co --git-remote-owner acme-labs\n#   (accepts a bare owner OR a full repo URL — the owner is extracted)\n\n# Match on host and/or repo name too — all accept glob patterns:\na2l profile match add acme --git-remote-host '*.gitlab.example.com' --git-remote-owner acme\na2l profile match add acme --git-remote-repo 'my-org/secret-*'        # owner/name glob\na2l profile match add acme --git-remote-owner Acme-Co --case-sensitive  # opt out of the case-insensitive default\n```\n\nNow, inside any repo whose `origin` owner is `acme-co`/`acme-labs`, commands automatically use the `acme` workspace and its defaults.\n\n### Common multi-repo setup: two workspaces, routed by repo owner\n\nThis is the usual setup when you have a work Linear workspace and a personal\nLinear workspace, and you want `a2l` to pick the right one from the current\nrepo's GitHub owner.\n\n```bash\n# 1. Store both keys without putting secrets in config.json.\necho \"$WORK_LINEAR_API_KEY\" | a2l workspace add work --api-key -\necho \"$PERSONAL_LINEAR_API_KEY\" | a2l workspace add personal --api-key -\n\n# 2. Create one profile per Linear workspace.\na2l profile add work --workspace work --default-team platform\na2l profile add personal --workspace personal --default-team inbox\n\n# 3. Route repo owners to profiles. Owners are matched case-insensitively.\na2l profile match add work \\\n  --git-remote-owner acme-co \\\n  --git-remote-owner acme-labs\n\na2l profile match add personal \\\n  --git-remote-owner alice \\\n  --git-remote-owner alice-labs\n\n# 4. Refuse to guess in repos that do not match either profile.\na2l config set noMatchPolicy match-only\n```\n\nAfter that:\n\n- `github.com/acme-co/*` and `github.com/acme-labs/*` resolve to `work`.\n- `github.com/alice/*` and `github.com/alice-labs/*` resolve to `personal`.\n- Any other repo fails with a no-match error instead of silently using the wrong key.\n- `--workspace \u003cname\u003e` or `--api-key` still forces a one-off override.\n\nVerify the routing from inside representative repos:\n\n```bash\na2l workspace current   # offline: selected workspace + source\na2l whoami              # API-backed identity check\na2l doctor              # config, workspace, and secret hygiene checks\n```\n\nIf you run agents or CI non-interactively and you trust your match rules, disable\nthe write confirmation for auto-detected multi-workspace writes:\n\n```bash\na2l config set confirmAutoDetectedWrites false\n```\n\nOtherwise, mutating commands in auto-detected multi-workspace repos require an\ninteractive confirmation, `-y`, or an explicit `--workspace`.\n\n#### Dotfiles/env-file variant\n\nIf you manage secrets outside agent2linear, profiles can point at per-workspace\nenv files instead of using `a2l workspace add`. The files are dotenv-style,\nuntracked, and only need to contain the key for that workspace:\n\n```text\n~/.config/agent2linear/work.env      # LINEAR_API_KEY_WORK=lin_api_...\n~/.config/agent2linear/personal.env  # LINEAR_API_KEY_PERSONAL=lin_api_...\n```\n\nThen hand-author or generate this global config at\n`~/.config/agent2linear/config.json`:\n\n```json\n{\n  \"noMatchPolicy\": \"match-only\",\n  \"confirmAutoDetectedWrites\": false,\n  \"profiles\": {\n    \"work\": {\n      \"workspace\": \"work\",\n      \"defaultTeam\": \"platform\",\n      \"match\": {\n        \"remote\": [\"origin\", \"upstream\"],\n        \"gitRemoteOwner\": [\"acme-co\", \"acme-labs\"]\n      },\n      \"envFile\": \"~/.config/agent2linear/work.env\"\n    },\n    \"personal\": {\n      \"workspace\": \"personal\",\n      \"defaultTeam\": \"inbox\",\n      \"match\": {\n        \"gitRemoteOwner\": [\"alice\", \"alice-labs\"]\n      },\n      \"envFile\": \"~/.config/agent2linear/personal.env\"\n    }\n  }\n}\n```\n\nThat `remote: [\"origin\", \"upstream\"]` on the work profile handles the common fork\ncase: if `origin` is your personal fork but `upstream` points at the work org, the\nwork profile can still win. Keep the work profile before the personal profile so\nfirst-positive-wins picks it for those forks.\n\n### Matching on host, repo name, and which remote\n\nA `match` rule can read **host**, **owner**, **repo** (`owner/name`), and choose **which remote** its identity criteria read:\n\n- All identity fields accept **glob patterns** — `github.com`, `*.gitlab.example.com`, `acme-*`, `my-org/secret-*`.\n- Within one rule, **present fields AND**; **each repeated field's list ORs**; a `linear: false` exclusion still wins. A rule matches only when *some selected remote* satisfies *every* present field.\n- Matching is **case-insensitive by default** (forge-correct). Pass `--case-sensitive` for the rare GitLab `Foo` ≠ `foo` case; it is per-rule, so it does not flip your other (GitHub) rules. (Turning it back off is a hand-edit of `config.json` for now.)\n- `--remote \u003cname\u003e` selects which remote(s) the identity reads: default `origin`, a name (`upstream`), `\"*\"` (any remote), or *bare* `--remote upstream` with no identity fields = \"a remote named `upstream` exists\" (the fork predicate).\n\n**Worked example — two orgs to one workspace, two users to another, refuse everything else:**\n\n```bash\na2l config set noMatchPolicy match-only          # recognized repos only; refuse everywhere else\n\n# Org A (both its GitHub orgs) → the work workspace\na2l profile match add work --git-remote-owner acme-co --git-remote-owner acme-labs\n\n# Two personal users → the personal workspace\na2l profile match add personal --git-remote-owner alice --git-remote-owner bob\n```\n\nNow `acme-co/*` and `acme-labs/*` route to `work`; `alice/*` and `bob/*` route to `personal`; **any other repo hard-errors** (exit 1) — no default, no guessing.\n\n**Fork example — route by `upstream`, not your personal fork:**\n\n```bash\n# You forked acme/widgets to alice/widgets, so origin = your fork and upstream = the org.\na2l profile match add acme --remote upstream --git-remote-owner acme   # read the upstream owner\na2l profile match add personal --git-remote-owner alice                # plain origin-owner rule\n```\n\nDeclare the `upstream` rule **first** so it wins for a fork (origin = `alice/widgets`, upstream = `acme/widgets`) — the `acme` profile resolves via the upstream owner even though `origin` is `alice`. In a non-forked `alice/*` repo (no `upstream`), the `personal` rule wins. When more than one profile matches the same repo, `a2l doctor` and `a2l profile match list` print an informational ambiguity warning (`⚠️ N profiles match this repo (first-positive-wins; order profiles to control which)`) — resolution is unchanged; ordering profiles is the lever.\n\n`a2l profile match list \u003cprofile\u003e` prints each rule's `remote` (when not the default `origin`), `git-remote-host` / `git-remote-owner` / `git-remote-repo` lines, and `case-sensitive: true` when set.\n\n\u003e **Release note (v0.31.0) — nested-group owners.** Profile detection now uses the same git parser as `config` overrides, so for **nested** self-hosted groups (`git@gitlab.com:group/sub/repo.git`) the **owner** is now the all-but-last path (`group/sub`), not just the first segment (`group`). A rule written as `--git-remote-owner group` no longer matches such a repo — use `--git-remote-owner group/sub` or the glob `--git-remote-owner 'group/*'`. Flat `host/owner/name` repos (e.g. plain GitHub) are unaffected.\n\n### Selection precedence (which workspace?)\n\nHighest to lowest:\n\n| # | Source | Example |\n|---|---|---|\n| 1 | `--workspace \u003cname\u003e` / `--api-key \u003ckey\u003e` (per-invocation) | `a2l --workspace acme issue create …` |\n| 2 | `AGENT2LINEAR_WORKSPACE` env declarator | `AGENT2LINEAR_WORKSPACE=acme a2l …` |\n| 3 | Repo config `profile`/`workspace` in `.agent2linear/config.json` | `{ \"profile\": \"acme\" }` |\n| 4 | Git-remote auto-detect (remote identity → `profile.match` host/owner/repo + `remote`) | (automatic) |\n| 5 | Global `defaultProfile` | `a2l config set defaultProfile acme` |\n| 6 | Legacy single key (`LINEAR_API_KEY` / config `apiKey`) | (the simple case) |\n\n### Key-source precedence (commit-safe — where does the key come from?)\n\nCommittable config **never** holds a raw key. For the resolved workspace `\u003cNAME\u003e`, the key is sourced (highest → lowest):\n\n1. `--api-key \u003ckey\u003e` / `--api-key -` (stdin)\n2. **Named env var** — `apiKeyEnv` override, else the default `LINEAR_API_KEY_\u003cNAME\u003e` (e.g. `acme` → `LINEAR_API_KEY_ACME`, `acme-co` → `LINEAR_API_KEY_ACME_CO`)\n3. **Per-profile env-file** — `profiles.\u003cname\u003e.envFile` (dotenv; `~` and `$VAR` expanded; never mutates `process.env`)\n4. **Secrets registry** — `workspaces.json` (global, mode `0600`) or `.agent2linear/workspaces.local.json` (project, auto-gitignored)\n5. Legacy plain `LINEAR_API_KEY` — used only when unambiguous\n\nIf a non-explicitly-selected workspace would fall back to the bare `LINEAR_API_KEY` while ≥2 workspaces are configured, the tool **refuses** (ambiguous) and tells you to set `LINEAR_API_KEY_\u003cNAME\u003e` or pass `--workspace`.\n\n### Refuse to guess: `noMatchPolicy` and exclusion\n\nWhen ≥2 profiles exist and nothing matches a repo:\n\n- **`deny`** (default) — refuse, with guidance. The simple single-key case never denies.\n- **`default`** — fall back to `defaultProfile`.\n- **`match-only`** — operate only in recognized repos; refuse everywhere else.\n\n```bash\na2l config set noMatchPolicy deny\na2l profile exclude acme               # mark a profile/org off-limits (linear: false)\n# repo opt-out: .agent2linear/config.json -\u003e { \"linear\": false }\n```\n\nAn explicit `--workspace`/`--api-key` always forces through exclusion **and** no-match.\n\n### Safety on writes (R11)\n\nMutating commands (`issue create`, `issue update`, `project create`) print a workspace/source banner and never let you write to the wrong place by accident:\n\n```bash\na2l issue create --title \"Fix bug\"                     # → banner shows the active workspace + source\na2l issue create --title \"Fix bug\" --json              # { \"ok\": true, \"workspace\": { \"name\": \"acme\", \"source\": \"auto-detect\" }, \"issue\": { … } }\na2l issue create --title \"Fix bug\" -y                  # skip the auto-detected-workspace confirmation\n```\n\n- In an **auto-detected, multi-workspace** repo, a mutating command **confirms** on an interactive terminal and **fail-safe errors** (never hangs) when run non-interactively without `--workspace`/`-y`/`confirmAutoDetectedWrites=false`.\n- `--json` emits `workspace.source` so an agent can verify *which* workspace it wrote to.\n- `a2l whoami` and `a2l doctor` show the active workspace + source; `doctor` also warns if a secrets file is tracked by git or a raw `apiKey` sits in a project `config.json`.\n\n## Usage\n\nThe CLI provides two command names that work identically:\n- `agent2linear` - Full command name\n- `a2l` - Short alias (used in most examples for brevity)\n\n### Common Commands\n\n```bash\n# Get help\na2l --help\na2l issue --help\na2l issue create --help\n\n# Work with issues (most common workflows)\na2l issue list                           # List YOUR assigned issues\na2l issue list --all                     # List all team issues\na2l issue create --title \"Fix bug\"       # Create issue (auto-assigned to you)\na2l issue view ENG-123                   # View issue details\na2l issue update ENG-123 --state done    # Update issue\n\n# Work with projects\na2l project create --title \"Q1 Goals\"    # Create project\na2l project list                         # List all projects\na2l project view \"My Project\"            # View project by name\n\n# Configuration\na2l config list                          # Show current config\na2l config edit                          # Interactive config editor\na2l setup                                # Run setup wizard again\n```\n\n**💡 Tip:** Most examples in this README use the short `a2l` alias for convenience. You can use `agent2linear` anywhere you see `a2l`.\n\n## Issue Commands\n\nCreate and manage Linear issues with comprehensive options and smart defaults.\n\n### Issue Create\n\nCreate issues with auto-assignment, full field support, and intelligent validation.\n\n**Basic Examples:**\n```bash\n# Minimal (auto-assigns to you, uses defaultTeam if configured)\nagent2linear issue create --title \"Fix login bug\"\n\n# Standard creation\nagent2linear issue create \\\n  --title \"Add OAuth support\" \\\n  --team backend \\\n  --priority 2 \\\n  --estimate 8\n\n# Full-featured\nagent2linear issue create \\\n  --title \"Implement auth\" \\\n  --team backend \\\n  --description \"Add OAuth2 providers\" \\\n  --priority 1 \\\n  --assignee john@company.com \\\n  --labels \"feature,security\" \\\n  --project \"Q1 Goals\" \\\n  --due-date 2025-02-15\n```\n\n**Key Features:**\n- **Auto-assignment**: Issues are assigned to you by default (use `--no-assignee` to override)\n- **Member resolution**: Supports ID, alias, email, or display name\n- **Project resolution**: Supports ID, alias, or name lookup\n- **Config defaults**: Use `defaultTeam` and `defaultProject` to simplify creation\n- **Validation**: Team-aware validation for states and projects\n\nFor full documentation: `agent2linear issue create --help`\n\n### Issue View\n\nView comprehensive issue details in terminal or browser.\n\n```bash\n# View by identifier\nagent2linear issue view ENG-123\n\n# View with JSON output\nagent2linear issue view ENG-123 --json\n\n# Open in browser\nagent2linear issue view ENG-123 --web\n```\n\n### Issue Update\n\nUpdate existing issues with comprehensive field support and smart validation.\n\n**Basic Examples:**\n```bash\n# Update single field\nagent2linear issue update ENG-123 --title \"New title\"\nagent2linear issue update ENG-123 --priority 1\nagent2linear issue update ENG-123 --state done\n\n# Update multiple fields\nagent2linear issue update ENG-123 \\\n  --title \"Updated title\" \\\n  --priority 2 \\\n  --estimate 5 \\\n  --due-date 2025-12-31\n```\n\n**Advanced Examples:**\n```bash\n# Change assignment\nagent2linear issue update ENG-123 --assignee john@company.com\nagent2linear issue update ENG-123 --no-assignee\n\n# Label management (3 modes)\nagent2linear issue update ENG-123 --labels \"bug,urgent\"           # Replace all\nagent2linear issue update ENG-123 --add-labels \"feature\"          # Add to existing\nagent2linear issue update ENG-123 --remove-labels \"wontfix\"       # Remove specific\nagent2linear issue update ENG-123 --add-labels \"new\" --remove-labels \"old\"  # Both\n\n# Subscriber management (3 modes)\nagent2linear issue update ENG-123 --subscribers \"user1,user2\"     # Replace all\nagent2linear issue update ENG-123 --add-subscribers \"user3\"       # Add to existing\nagent2linear issue update ENG-123 --remove-subscribers \"user1\"    # Remove specific\n\n# Clear fields\nagent2linear issue update ENG-123 --no-assignee --no-due-date --no-estimate\nagent2linear issue update ENG-123 --no-project --no-cycle --no-parent\n\n# Parent relationship\nagent2linear issue update ENG-123 --parent ENG-100     # Make sub-issue\nagent2linear issue update ENG-123 --no-parent          # Make root issue\n\n# Move between teams\nagent2linear issue update ENG-123 --team frontend --state todo\n\n# Lifecycle operations\nagent2linear issue update ENG-123 --trash              # Move to trash\nagent2linear issue update ENG-123 --untrash            # Restore from trash\n```\n\n**Key Features:**\n- **33+ update options**: Comprehensive field coverage including add/remove patterns\n- **Smart validation**: Team-aware state validation, compatibility checks\n- **Flexible updates**: Update one field or many at once\n- **Clearing operations**: Use `--no-*` flags to clear fields (assignee, dates, etc.)\n- **Label/subscriber patterns**: Replace, add, or remove items with distinct flags\n- **Mutual exclusivity**: Prevents conflicting flag combinations with helpful errors\n\nFor full documentation: `agent2linear issue update --help`\n\n### Issue List\n\nList and filter issues with smart defaults, extensive filtering, sorting, and multiple output formats.\n\n**Basic Examples:**\n```bash\n# Smart defaults: your assigned issues\nagent2linear issue list\n\n# Limit results\nagent2linear issue list --limit 10\n\n# Filter by team\nagent2linear issue list --team backend\n\n# Filter by state\nagent2linear issue list --state \"in progress\"\n```\n\n**Advanced Filtering:**\n```bash\n# Multiple filters\nagent2linear issue list \\\n  --team backend \\\n  --priority 1 \\\n  --state \"in progress\" \\\n  --assignee steve@company.com\n\n# Project and labels\nagent2linear issue list \\\n  --project \"Q1 Goals\" \\\n  --labels \"bug,urgent\"\n\n# Date filters\nagent2linear issue list --due-before 2025-12-31\nagent2linear issue list --created-after 2025-01-01\n\n# Parent/sub-issue filters\nagent2linear issue list --has-parent        # Only sub-issues\nagent2linear issue list --no-parent         # Only root issues\n```\n\n**Sorting and Output:**\n```bash\n# Sort options\nagent2linear issue list --sort priority     # By priority (high to low)\nagent2linear issue list --sort created      # By creation date (newest first)\nagent2linear issue list --sort updated      # By update date\nagent2linear issue list --sort identifier   # By identifier (ENG-1, ENG-2...)\n\n# Output formats\nagent2linear issue list --format table      # Table view (default)\nagent2linear issue list --format compact    # Compact view\nagent2linear issue list --format json       # JSON output\nagent2linear issue list --format urls       # URLs only (for scripting)\n```\n\n**Key Features:**\n- **Smart defaults**: Shows your assigned issues by default\n- **Extensive filters**: Team, state, priority, assignee, labels, project, dates, parent, and more\n- **Flexible sorting**: By priority, dates, identifier, or other fields\n- **Multiple formats**: Table, compact, JSON, or URLs for scripting\n- **Performance optimized**: Batch fetching eliminates N+1 queries (11x+ API call reduction)\n\nFor full documentation: `agent2linear issue list --help`\n\n## Project List \u0026 Search\n\nList and search projects with smart defaults and extensive filtering. The `project list` command provides intelligent defaults for common workflows while supporting comprehensive filtering options.\n\n### Smart Defaults\n\nBy default, `project list` filters to show projects where **you are the lead**, in your **default team and initiative** (if configured):\n\n```bash\n# Smart defaults: projects I lead in my default team/initiative\nagent2linear project list\n\n# Equivalent to (if you have defaults configured):\n# --lead \u003ccurrent-user-id\u003e --team \u003cdefault-team\u003e --initiative \u003cdefault-initiative\u003e\n```\n\n### Override Flags\n\nUse these flags to bypass smart defaults and see more projects:\n\n```bash\n# Show ALL projects (any lead) in default team/initiative\nagent2linear project list --all-leads\n\n# Show projects I lead across ALL teams\nagent2linear project list --all-teams\n\n# Show projects I lead across ALL initiatives\nagent2linear project list --all-initiatives\n\n# Override everything - show ALL projects everywhere\nagent2linear project list --all-leads --all-teams --all-initiatives\n```\n\n### Filter Options\n\n**Core Filters:**\n```bash\n# Filter by team\nagent2linear project list --team backend\nagent2linear project list -t backend\n\n# Filter by initiative\nagent2linear project list --initiative q1-goals\nagent2linear project list -i q1-goals\n\n# Filter by project status\nagent2linear project list --status planned\nagent2linear project list -s started\n\n# Filter by priority (0-4)\nagent2linear project list --priority 1\nagent2linear project list -p 2\n\n# Filter by specific project lead\nagent2linear project list --lead alice@company.com\nagent2linear project list -l alice\n\n# Filter by member (projects where someone is assigned)\nagent2linear project list --member bob\nagent2linear project list -m alice,bob  # Multiple members\n\n# Filter by label\nagent2linear project list --label urgent\nagent2linear project list --label urgent,critical  # Multiple labels\n\n# Search in project name, description, or content\nagent2linear project list --search \"API\"\nagent2linear project list --search \"mobile redesign\"\n```\n\n**Date Range Filters:**\n```bash\n# Projects starting in Q1 2025\nagent2linear project list --start-after 2025-01-01 --start-before 2025-03-31\n\n# Projects targeting after June 2025\nagent2linear project list --target-after 2025-06-01\n\n# Projects targeting before end of year\nagent2linear project list --target-before 2025-12-31\n```\n\n### Output Formats\n\n**Table Format (default):**\n```bash\nagent2linear project list\n```\nOutput:\n```\nID           Title                          Status      Team           Lead                 Preview\n-----------------------------------------------------------------------------------------------------------------------\nbf2e1a8a9b   Mobile App Redesign            Started     Mobile         Alice Johnson        Complete redesign of iOS...\na9c3d4e5f6   API v2 Migration               Planned     Backend        Bob Smith            Migrate all endpoints...\nc1d2e3f4g5   Customer Dashboard             Completed   Frontend       Carol Davis          New dashboard for customer...\n\nTotal: 3 projects\n```\n\n**JSON Format:**\n```bash\n# Machine-readable format for scripting\nagent2linear project list --format json\nagent2linear project list -f json\n\n# Example with filtering\nagent2linear project list --team backend --status started --format json\n```\n\n**TSV Format:**\n```bash\n# Tab-separated values for data processing\nagent2linear project list --format tsv\nagent2linear project list -f tsv \u003e projects.tsv\n```\n\n**Interactive Mode:**\n```bash\n# Ink UI with rich formatting\nagent2linear project list --interactive\nagent2linear project list -I\n```\n\n### Complex Filter Examples\n\n```bash\n# Backend team projects, started status, high priority\nagent2linear project list --team backend --status started --priority 1\n\n# Projects led by specific person in any team\nagent2linear project list --lead alice@company.com --all-teams\n\n# Projects where Bob is assigned (as member)\nagent2linear project list --member bob --all-leads\n\n# Search for \"API\" projects in backend team (any lead)\nagent2linear project list --search \"API\" --team backend --all-leads\n\n# Urgent projects targeting Q1 2025\nagent2linear project list --label urgent --target-after 2025-01-01 --target-before 2025-03-31\n\n# All projects with multiple filters\nagent2linear project list \\\n  --team backend \\\n  --status started \\\n  --priority 1 \\\n  --lead alice \\\n  --label critical\n\n# Export all projects to JSON\nagent2linear project list --all-teams --all-leads --all-initiatives --format json \u003e all-projects.json\n```\n\n### Alias Support\n\nAll entity filters support aliases:\n\n```bash\n# Use team alias instead of ID\nagent2linear project list --team backend\n\n# Use initiative alias\nagent2linear project list --initiative q1-goals\n\n# Use member alias\nagent2linear project list --lead alice\n\n# Use label alias\nagent2linear project list --label urgent,critical\n```\n\n### Setting Defaults\n\nConfigure default values to streamline your workflow:\n\n```bash\n# Set default team\nagent2linear config set defaultTeam backend\n\n# Set default initiative\nagent2linear config set defaultInitiative q1-goals\n\n# Now simple list uses your defaults:\nagent2linear project list\n# Shows: projects you lead in 'backend' team within 'q1-goals' initiative\n```\n\n## Milestone Templates\n\nMilestone templates allow you to quickly set up project milestones using predefined templates. Templates are stored locally in JSON files and can be customized for your workflows.\n\n### Creating Templates\n\nYou can create milestone templates using the CLI (recommended) or by manually editing JSON files.\n\n**Using the CLI (Interactive):**\n```bash\n# Interactive mode - guided wizard\nagent2linear milestone-templates create --interactive\nagent2linear mtmpl create -I\n\n# Interactive mode with project scope\nagent2linear milestone-templates create --project --interactive\n```\n\n**Using the CLI (Non-Interactive):**\n```bash\n# Create a template with milestones\nagent2linear milestone-templates create my-sprint \\\n  --description \"Custom 2-week sprint\" \\\n  --milestone \"Planning:+1d:Define sprint goals and tasks\" \\\n  --milestone \"Development:+10d:Implementation phase\" \\\n  --milestone \"Review:+14d:Code review and deployment\"\n\n# Create in project scope\nagent2linear milestone-templates create team-workflow \\\n  --project \\\n  --milestone \"Kickoff::Team alignment meeting\" \\\n  --milestone \"Execution:+7d:Complete assigned tasks\" \\\n  --milestone \"Retrospective:+14d:Review and improve\"\n```\n\n**Milestone Spec Format:** `name:targetDate:description`\n- `name` - Required\n- `targetDate` - Optional (+7d, +2w, +1m, or ISO date)\n- `description` - Optional (supports markdown)\n\n**Manual Template File Creation:**\n\nTemplates are stored at:\n- **Global**: `$XDG_CONFIG_HOME/agent2linear/milestone-templates.json` (default: `~/.config/agent2linear/milestone-templates.json`) - Available across all projects\n- **Project**: `.agent2linear/milestone-templates.json` - Project-specific templates\n\n**Example Template File:**\n```json\n{\n  \"templates\": {\n    \"basic-sprint\": {\n      \"name\": \"Basic Sprint Template\",\n      \"description\": \"Simple 2-week sprint structure\",\n      \"milestones\": [\n        {\n          \"name\": \"Sprint Planning\",\n          \"description\": \"Define sprint goals and tasks\",\n          \"targetDate\": \"+1d\"\n        },\n        {\n          \"name\": \"Development\",\n          \"description\": \"Implementation phase\",\n          \"targetDate\": \"+10d\"\n        },\n        {\n          \"name\": \"Review \u0026 Deploy\",\n          \"description\": \"Code review and deployment\",\n          \"targetDate\": \"+14d\"\n        }\n      ]\n    }\n  }\n}\n```\n\n**Managing Templates:**\n```bash\n# Edit a template (interactive)\nagent2linear milestone-templates edit basic-sprint\n\n# Remove a template\nagent2linear milestone-templates remove basic-sprint\nagent2linear mtmpl rm old-template --yes  # Skip confirmation\n```\n\n### Using Milestone Templates\n\n```bash\n# List all templates\nagent2linear milestone-templates list\nagent2linear mtmpl ls                # Short alias\n\n# View template details\nagent2linear milestone-templates view basic-sprint\n\n# Add milestones to a project\nagent2linear project add-milestones PRJ-123 --template basic-sprint\n\n# Set default template\nagent2linear config set defaultMilestoneTemplate basic-sprint\n\n# Use default when creating milestones\nagent2linear project add-milestones PRJ-123  # Uses default template\n```\n\n### Date Offset Format\n\nTarget dates support relative formats:\n- `+7d` - 7 days from now\n- `+2w` - 2 weeks from now\n- `+1m` - 1 month from now\n- `2025-12-31` - Absolute ISO date\n\n## Aliases\n\nAliases allow you to use simple, memorable names instead of long Linear IDs. For example, use \"backend\" instead of \"init_abc123xyz\". This is especially useful for AI assistants that have difficulty tracking long IDs.\n\n### Managing Aliases\n\n```bash\n# Add an alias\nagent2linear alias add initiative backend init_abc123xyz\nagent2linear alias add team frontend team_def456uvw --project\nagent2linear alias add project api proj_ghi789rst\n\n# List all aliases\nagent2linear alias list\n\n# List aliases for a specific type\nagent2linear alias list initiatives\nagent2linear alias list teams\n\n# Get the ID for an alias\nagent2linear alias get initiative backend\n\n# Edit aliases interactively\nagent2linear alias edit           # Interactive mode - select scope, type, and alias to edit\nagent2linear alias edit --global  # Edit global aliases\nagent2linear alias edit --project # Edit project aliases\n\n# Remove an alias\nagent2linear alias remove initiative backend\nagent2linear alias rm team frontend --project\n\n# Validate all aliases\nagent2linear alias list --validate\n```\n\n### Using Aliases\n\nOnce configured, aliases can be used anywhere an ID is accepted:\n\n```bash\n# Use initiative alias\nagent2linear initiatives set backend\nagent2linear initiatives view backend\n\n# Use team and initiative aliases in project creation\nagent2linear project create --title \"New API\" --team backend --initiative backend-init\n\n# Use team alias in selection\nagent2linear teams select --id frontend\n```\n\n### Storage Locations\n\n- **Global aliases**: `$XDG_CONFIG_HOME/agent2linear/aliases.json` (default: `~/.config/agent2linear/aliases.json`) - Available across all projects\n- **Project aliases**: `.agent2linear/aliases.json` - Project-specific, can be version controlled\n\nProject aliases take precedence over global aliases, allowing you to override global settings per-project. The project `.agent2linear/` directory is discovered by walking up from the current directory toward `$HOME`.\n\n### Alias Scope\n\nAliases are scoped by entity type, meaning you can use the same alias name for different types:\n\n```bash\n# \"backend\" can refer to both an initiative and a team\nagent2linear alias add initiative backend init_abc123\nagent2linear alias add team backend team_xyz789\n```\n\n## Icon Usage\n\n### Supported Icons\n\nagent2linear supports Linear's standard icon catalog for projects. Icons can be specified by name (e.g., \"Checklist\", \"Tree\", \"Joystick\") and are validated by Linear's API.\n\n**Note on Icon Validation**: This tool does NOT validate icons client-side. Icons are passed directly to Linear's API for server-side validation. This design decision was made after investigation revealed:\n\n1. **No API catalog endpoint**: Linear's GraphQL API does not expose an endpoint to fetch the complete standard icon catalog\n2. **Emojis query limitation**: The `emojis` query only returns custom organization emojis (user-uploaded), not Linear's built-in icons\n3. **Maintenance burden**: Maintaining a hardcoded list would be incomplete and quickly outdated\n\n### Icon Discovery\n\n```bash\n# View curated icon suggestions (for discovery only, not exhaustive)\nagent2linear icons list\n\n# Search for specific icons\nagent2linear icons list --search rocket\n\n# View icons by category\nagent2linear icons list --category status\n\n# Extract icons currently used in your workspace\nagent2linear icons extract --type projects\n```\n\n### Using Icons\n\n```bash\n# Icon names are capitalized (Linear's format)\nagent2linear project create --title \"My Project\" --team eng --icon \"Checklist\"\nagent2linear project create --title \"API\" --team backend --icon \"Joystick\"\nagent2linear project create --title \"Design\" --team frontend --icon \"Tree\"\n\n# If an invalid icon is provided, Linear API will return a helpful error\nagent2linear project create --title \"Test\" --team eng --icon \"InvalidIcon\"\n# Error: Icon not found (from Linear API)\n```\n\n### Icon Resources\n\n- The `agent2linear icons list` command shows ~67 curated icons for discovery\n- Linear supports hundreds of standard icons beyond this curated list\n- Invalid icons will be rejected by Linear's API with clear error messages\n\n## Date Formats\n\nagent2linear supports flexible date formats for project `--start-date` and `--target-date` options, making it easy to specify dates naturally without manually calculating start-of-quarter or start-of-month dates.\n\n### Supported Formats\n\n**Quarters:**\n```bash\nagent2linear project create --title \"Q1 Initiative\" --start-date \"2025-Q1\"\nagent2linear project create --title \"Q2 Goals\" --start-date \"Q2 2025\"\nagent2linear project create --title \"Q3 Project\" --start-date \"q3-2025\"  # Case-insensitive\n```\n\n**Half-Years:**\n```bash\nagent2linear project create --title \"H1 Strategy\" --start-date \"2025-H1\"\nagent2linear project create --title \"H2 Plan\" --start-date \"H2 2025\"\n```\n\n**Months:**\n```bash\n# Numeric format\nagent2linear project create --title \"January Sprint\" --start-date \"2025-01\"\n\n# Short month names\nagent2linear project create --title \"Feb Release\" --start-date \"Feb 2025\"\n\n# Full month names\nagent2linear project create --title \"March Update\" --start-date \"March 2025\"\n```\n\n**Years:**\n```bash\nagent2linear project create --title \"2025 Roadmap\" --start-date \"2025\"\n```\n\n**ISO Dates (specific dates):**\n```bash\nagent2linear project create --title \"Sprint 1\" --start-date \"2025-01-15\"\n```\n\n### How It Works\n\nThe date parser automatically:\n- Converts flexible formats to ISO dates (YYYY-MM-DD)\n- Detects and sets the appropriate resolution (quarter, month, year)\n- Shows confirmation messages with the parsed format\n\n**Example output:**\n```bash\n$ agent2linear project create --title \"Q1 Initiative\" --start-date \"2025-Q1\"\n📅 Start date: Q1 2025 (2025-01-01, resolution: quarter)\n✅ Created project: Q1 Initiative\n```\n\n### Date Resolution\n\nLinear projects support date resolutions to indicate time granularity:\n- **quarter**: Project spans a quarter (Q1-Q4)\n- **month**: Project spans a month\n- **halfYear**: Project spans half a year (H1 or H2)\n- **year**: Project spans an entire year\n- *(none)*: Specific date without resolution\n\n#### Auto-Detection (Recommended)\n\nThe parser **automatically sets the resolution** based on your input format:\n```bash\n# ✅ Recommended: Let the parser auto-detect resolution\nagent2linear project create --start-date \"2025-Q1\"      # Auto: resolution = quarter\nagent2linear project create --start-date \"Jan 2025\"     # Auto: resolution = month\nagent2linear project create --start-date \"2025\"         # Auto: resolution = year\nagent2linear project create --start-date \"2025-01-15\"   # Auto: no resolution (specific date)\n```\n\n#### Explicit Override (Advanced)\n\nFor advanced use cases, you can explicitly override the resolution with `--start-date-resolution` or `--target-date-resolution`:\n\n**When to use explicit override:**\n- Mid-period dates with specific resolution (e.g., mid-month representing a quarter)\n- Resolution-only updates (update command only)\n\n```bash\n# ⚙️ Advanced: Override auto-detection\n# Mid-month date representing Q1\nagent2linear project create --start-date \"2025-01-15\" --start-date-resolution quarter\n\n# Resolution-only update (update command)\nagent2linear project update myproject --start-date-resolution quarter\n```\n\n**Validation warnings:**\n```bash\n# ⚠️ Conflicting format and explicit flag\n$ agent2linear project create --start-date \"2025-Q1\" --start-date-resolution month\n⚠️  Warning: Date format '2025-Q1' implies quarter resolution, but --start-date-resolution\n    is set to 'month'. Using explicit value (month).\n```\n\n**Best practice:** Use auto-detection for 95% of cases. Only use explicit flags when the date format doesn't match your intent.\n\n### Error Handling\n\nInvalid dates are caught with helpful error messages:\n\n```bash\n$ agent2linear project create --title \"Test\" --start-date \"2025-Q5\"\n❌ Invalid start date: Invalid quarter: Q5\n\nQuarter must be Q1, Q2, Q3, or Q4. Examples:\n  --start-date \"2025-Q1\"     → Q1 2025 (Jan 1 - Mar 31)\n  --start-date \"Q2 2025\"     → Q2 2025 (Apr 1 - Jun 30)\n  --start-date \"Q3 2025\"     → Q3 2025 (Jul 1 - Sep 30)\n  --start-date \"Q4 2025\"     → Q4 2025 (Oct 1 - Dec 31)\n```\n\n## Development\n\n```bash\n# Build the project\nnpm run build\n\n# Run in development mode (watch)\nnpm run dev\n\n# Lint code\nnpm run lint\n\n# Format code\nnpm run format\n\n# Type check\nnpm run typecheck\n\n# Run unit tests\nnpm run test\n\n# Run tests in watch mode\nnpm run test:watch\n\n# Run tests with web UI\nnpm run test:ui\n\n# Run tests with coverage report\nnpm run test:coverage\n```\n\n### Testing\n\nThis project uses [Vitest](https://vitest.dev/) for unit testing with comprehensive coverage of core utilities.\n\n**Two test suites — different runners, different concurrency models:**\n\n| Suite | Location | Runner | Concurrency | Isolation |\n|-------|----------|--------|-------------|-----------|\n| **Unit tests** | `src/**/*.test.ts` | Vitest (`npm run test`) | **Parallel** — files run concurrently in a worker pool | Each file owns its own `mktemp` sandbox + `vi.stubEnv('XDG_CONFIG_HOME', …)` per `beforeEach`, with `afterEach` cleanup |\n| **Integration / E2E** | `tests/scripts/*.sh` | Bash (`./run-all-tests.sh`) | **Sequential** — one process, no backgrounding | Each script uses a fresh `mktemp` sandbox (`HOME`/`XDG`); the override-CLI suite is a stateful lifecycle (see below) |\n\n**What must stay sequential — and why:**\n- **`tests/scripts/*.sh` are sequential by design.** `run-all-tests.sh` runs each suite as a single blocking foreground process (no `\u0026`/`wait`), and the bash suites shell out to the built `dist/index.js`. They are **not** run by Vitest and must not be parallelized.\n- **`tests/scripts/test-config-override-cli.sh` is a stateful lifecycle suite**: its numbered cases (`add → list → get → edit → move → remove → explain`) execute in source order against **one shared sandbox** — case 1 creates the rule that later cases build on. Run the **full** suite, or use `--range` **starting at 1** (e.g. `--range 1-7`) to stop early. A non-prefix slice (`--test 14`, `--range 10-14`) reports false failures because the earlier fixtures never ran — expected, not a bug.\n\n**Parallelism is safe for the Vitest suite** because every test file is self-isolated (its own temp `XDG_CONFIG_HOME`) and there are no `.concurrent` tests. **New unit tests must follow that pattern** (own `mktemp` sandbox + `stubEnv` in `beforeEach`) so they stay parallel-safe.\n\n**CI coverage:** `ci.yml` runs on every PR/push — typecheck, lint, the full Vitest suite (Node 18 + 20), build, and the offline `test-config-override-cli.sh` E2E (no secret). The **live** API suites run separately in `live.yml` — only on **push-to-`main`** and manual `workflow_dispatch` (never on PRs, so `LINEAR_API_KEY` is never exposed to a fork) — and they create real `TEST_*` entities in a throwaway Linear workspace.\n\n**Running Tests:**\n```bash\n# Run all unit tests once (recommended for CI/CD and verification)\nnpm run test\n\n# Watch mode - auto-run tests on file changes (best for active development)\nnpm run test:watch\n\n# Interactive web UI for test exploration (debugging and visual analysis)\nnpm run test:ui\n\n# Generate coverage report (check test completeness)\nnpm run test:coverage\n```\n\n**Running Specific Tests:**\n```bash\n# Run only date parser tests (104 tests)\nnpx vitest run src/lib/date-parser.test.ts\n\n# Run tests matching a pattern\nnpx vitest run -t \"Quarter formats\"\n\n# Run with verbose output\nnpx vitest run --reporter=verbose\n```\n\n**Test Files:**\n- `src/lib/smoke.test.ts` - 4 basic sanity tests\n- `src/lib/date-parser.test.ts` - 104 comprehensive date parser tests\n\n**Date Parser Test Coverage (104 tests):**\n- Quarter formats (15 tests) - All Q1-Q4 variants, case sensitivity, validation\n- Half-year formats (10 tests) - H1/H2 variants, edge cases\n- Month formats - Numeric (10 tests) - YYYY-MM format with validation\n- Month formats - Named (20 tests) - Jan/January, all 12 months, case sensitivity\n- Year formats (5 tests) - 4-digit years with range validation\n- ISO date formats (10 tests) - YYYY-MM-DD with leap year validation\n- Resolution detection (8 tests) - Auto-detection verification\n- Parser priority (12 tests) - Format precedence rules\n- Error messages (10 tests) - User-friendly error handling\n- Edge cases (4 tests) - Whitespace, mixed case, boundaries\n\n**Helper Function Tests (20 tests):**\n- `getQuarterStartDate()` - 6 tests\n- `getHalfYearStartDate()` - 4 tests\n- `getMonthStartDate()` - 5 tests\n- `parseMonthName()` - 5 tests\n\n**Coverage:**\n- Target: 95%+ coverage for core utilities\n- Current: `date-parser.ts` has **99.10%** coverage\n  - Statements: 99.10%\n  - Branches: 98.07%\n  - Functions: 100%\n  - Lines: 99.04%\n- Coverage reports available in `coverage/` directory after running `npm run test:coverage`\n\n## Publishing\n\nThis project uses [np](https://github.com/sindresorhus/np) for automated publishing to npm.\n\n**Prerequisites:**\n- npm account with publish access\n- Logged in: `npm whoami`\n- Clean git working directory\n\n**Release Process:**\n\n```bash\n# Interactive release with np (recommended)\nnpm run release\n\n# np will automatically:\n# 1. Run tests and build (via prepublishOnly)\n# 2. Bump version in package.json\n# 3. Create git tag\n# 4. Push to GitHub\n# 5. Publish to npm\n# 6. Create GitHub release\n```\n\n**Manual Publishing (not recommended):**\n\n```bash\n# Ensure everything is ready\nnpm run typecheck\nnpm run lint\nnpm run build\nnpm run test\n\n# Publish to npm\nnpm publish\n\n# Tag and push\ngit tag v0.24.0\ngit push origin main --tags\n```\n\n**After Publishing:**\n\n```bash\n# Verify package is available\nnpm view agent2linear\n\n# Test installation\nnpx agent2linear --version\nnpx a2l --version\n```\n\n## Design Decisions\n\n### No Delete Commands\n\nDelete commands are **intentionally omitted** for data safety. This is a deliberate design choice — not a missing feature. Destructive operations like deleting projects or permanently removing issues should be done through the Linear web UI where you can visually confirm what you're deleting.\n\nFor issues, you can use the trash/restore workflow:\n```bash\nagent2linear issue update ENG-123 --trash     # Move to trash (reversible)\nagent2linear issue update ENG-123 --untrash   # Restore from trash\n```\n\nTrashed issues can be recovered; deleted entities cannot.\n\n## Project Status\n\nSee [MILESTONES.md](./MILESTONES.md) for detailed project milestones and progress.\n\n**Current Version**: v0.1.0 - Project Foundation\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsmorinlabs%2Fagent2linear","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsmorinlabs%2Fagent2linear","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsmorinlabs%2Fagent2linear/lists"}