Repo config
A repository can commit a name → GID map at .agents/cyber-asana.json. Agents and MCP
tools then resolve a human-readable project or user name locally, with no API call and no guessing.
{ "schema_version": 2, "projects": [ { "gid": "1215109751173511", "name": "cyber-asana", "aliases": ["cyber-asana", "cli"], "purpose": "CLI, MCP server, and plugin work for this package", "default": true } ], "users": [ { "gid": "1200000000000001", "name": "Alice Anderson", "email": "alice@example.com", "aliases": ["ali"] } ], "defaults": { "assignee": "ali", "section": "1215109751173999" }}A project entry’s aliases are trigger keywords that resolve to it, matched
case-insensitively and ahead of another project’s display name; an alias is unique across the
registry. purpose is one line saying what work belongs in the project. default: true marks
the project commands fall back to when none is given — at most one project can carry it.
users and defaults are both optional; a file with only projects stays
valid. A schema_version: 1 file (project entries with only gid and name) still parses and
is upgraded to schema_version: 2 in place the next time the CLI writes the file.
Workspace GID deliberately stays out of this file — keep it in ASANA_WORKSPACE_GID. There is no
defaults.workspace key; one in a config file is rejected by name.
Commands
Section titled “Commands”cyber-asana config add <project-gid> # seed or update an entrycyber-asana config add <project-gid> --alias api --purpose "Service work" --defaultcyber-asana config resolve-project "Backend" --json # local lookup, no API call (name or alias)cyber-asana config set-default <gid-name-or-alias> # mark the fallback projectcyber-asana config set-default --none # clear the fallback projectcyber-asana config remove-project-alias api,svc # drop aliases, keep the projectcyber-asana config sync # refresh cached names from Asanacyber-asana config show
cyber-asana config add-user <user-gid> --alias ali # store name + email, plus an aliascyber-asana config add-user --search "Ada" --alias ali # find the GID by typeahead firstcyber-asana config remove-alias ali,al # drop aliases, keep the usercyber-asana config resolve-user ali --json # local lookup, no API callcyber-asana config list-users
cyber-asana config set defaults.assignee ali # set a defaults keycyber-asana config unset defaults.assignee # clear it
cyber-asana config discover-fields api # find a project's points fields, save themcyber-asana config set-field story_points <field-gid> --project api # pick a field by handcyber-asana config unset-field story_points --project api| Command | Arguments | Description |
|---|---|---|
show |
— | Print the repo config, including the defaults block |
list |
— | Alias for show |
path |
— | Print the resolved config file path |
resolve-project |
<name> |
Resolve a project name or alias to its GID, no API call |
add |
<project-gid>, [--alias <alias>...], [--purpose <text>], [--default] |
Add or update an entry, fetching the name from Asana. Repeat --alias or pass a comma-separated list (--alias api,svc) |
set-default |
[gid-name-or-alias] or --none |
Mark the project commands fall back to when none is given, or clear it |
remove-project-alias |
<alias...> |
Remove aliases from whichever registered projects own them; pass several or a comma-separated list |
remove |
<gid-or-name> |
Remove an entry by GID or name |
sync |
— | Refresh all cached project names, and user names and emails, from Asana |
add-user |
<user-gid> or --search <query>, [--alias <alias>...] |
Add or update a user, fetching name and email from Asana; aliases accumulate. Repeat --alias or pass a comma-separated list (--alias ali,al). --search needs a workspace (--workspace-gid or ASANA_WORKSPACE_GID) |
remove-alias |
<alias...> |
Remove aliases from whichever users own them; pass several or a comma-separated list. Fails without changing anything if an alias is not registered |
resolve-user |
<query> |
Resolve a GID, alias, email, or name to a user, no API call |
list-users |
— | Print registered users |
remove-user |
<query> |
Remove the user a GID, alias, email, or name resolves to |
set |
<key> <value> |
Set defaults.assignee or defaults.section |
unset |
<key> |
Clear a key set accepts |
discover-fields |
[project] |
Find the project’s custom fields by role and save their GIDs under the entry’s fields. Without a project, uses the default project. Several matches for one role are listed, not saved |
set-field |
<role> <field-gid>, [--project <project>] |
Register a custom field for a role, fetching its name from Asana |
unset-field |
<role>, [--project <project>] |
Clear the field registered for a role |
Every subcommand accepts --config <path>, which overrides the CYBER_ASANA_CONFIG
environment variable.
Defaults
Section titled “Defaults”defaults holds fallbacks a command uses when it was not told a value: assignee (resolved
the same way --assignee is) and section (a section GID in the default project). The default
project itself is not part of defaults — it is whichever project entry carries default: true.
task create and asana_task_create apply defaults.section themselves: a task created in the
default project lands directly in that section, in the same API call. A task created only in other
projects is left alone, because the section belongs to the default project, and with no project
marked default the key does nothing. Skip it per call with --no-default-section (MCP:
default_section: false).
Project custom fields
Section titled “Project custom fields”A convention can name a custom field by the role it plays instead of by GID, because field GIDs differ per workspace and project. The task conventions reference does this for story points. A project entry records the fields found for it:
{ "gid": "1201", "name": "Backend", "aliases": ["api"], "fields": { "story_points": { "gid": "9001", "name": "Story Points" } } }There are two roles: story_points and task_points. The task conventions reference says what
each measures. On a number
or dropdown field, discover-fields fills story_points from “Story Points”, “Story pts”, or “SP”,
and task_points from “Task Points”, “Task pts”, or “TP”. A bare “Points” or “pts” fills
story_points, which a project with a single points field uses for everything. A project with both
a story points and a task points field gets both roles saved. When several fields match one role,
it saves none for that role and lists them; pick one with set-field. On a later run, a saved field that is still on the
project is kept even if its name does not match, and one no longer on the project is dropped.
Task conventions
Section titled “Task conventions”Repo house style lives in the frontmatter of the cyber-asana.task-conventions reference, not in
this file. The CLI and MCP resolve that reference through the
cyber-agentkit layers — the repo copy
(.agents/references/), the user copy (~/.agents/references/), installed plugins, then the copy
cyber-asana ships — and merge the frontmatter key by key, the higher layer winning. Set a
convention in a repo copy:
---merge: merge-sectionstask_name_format: "<area>: <summary>"description_template: | ## Problem
## Plandefault_tags: [agent-created]---A config file that still has a conventions block fails to load; move its keys into the
frontmatter of .agents/references/cyber-asana.task-conventions.md and delete the block. If two plugins ship the reference, the name is
ambiguous: a warning goes to stderr and no conventions apply.
Task creation applies two of the three keys on its own:
| Key | Effect on task create / asana_task_create |
|---|---|
description_template |
Becomes the description when no notes are given. A template opening with <body> is sent as html_notes, anything else as notes |
default_tags |
Tags applied when no tags are given. Override with --tag (MCP: tag_gids) |
task_name_format |
Not applied automatically — a shape such as <area>: <summary> for an agent to follow when it writes the name |
A tag may be written as a GID or as its name. An Asana GID is a long numeric string, so a numeric value is used as-is and a list of only GIDs costs no API call; a name is resolved case-insensitively against the workspace’s tags. A name no tag matches reports how to create it, and a name two tags share is refused rather than guessed.
task create resolves --project as a GID or a registered project name or alias, falling back
to the default project when --project is omitted; --assignee falls back to
defaults.assignee the same way. Every fallback here is create-only — task update with no
--assignee leaves the assignee alone, and never picks up a template or default tags.
Global registry (across repositories)
Section titled “Global registry (across repositories)”.agents/cyber-asana.json is committed and per-repo. For a personal, cross-machine registry that
isn’t committed — or for a repo that has no local file yet — show, list, resolve-project,
add, remove, and sync also take --global:
cyber-asana config add <project-gid> --global # remembered for this repo, not committedcyber-asana config show --global # this repo's entry in the personal registrycyber-asana config path --global # where that file livesIt lives at $XDG_CONFIG_HOME/cyber-asana/config.json (or ~/.config/cyber-asana/config.json),
overridable with CYBER_ASANA_GLOBAL_CONFIG, and pairs a repo key — the normalized git remote
origin URL (github.com/org/repo), or the git root’s path when there’s no remote — with its own
project list, so the same entry applies wherever that repo is cloned. Outside any git checkout, or
to manage a repo you’re not currently in, pass --repo <key> explicitly.
To see everything that applies to the current repo — the committed file and the personal
registry, combined — add --merged instead of --global to show, list, or resolve-project.
The two sources are unioned by GID; the committed repo config wins a name conflict, and either
source may be absent without it being an error.
cyber-asana config show --merged--global and --merged are mutually exclusive.
The global registry also has users
Section titled “The global registry also has users”add-user, resolve-user, remove-user, remove-alias, and list-users take --global too —
but unlike projects, global users are flat, not filed per repo, since the same person doesn’t
change identity when you cd into a different checkout:
cyber-asana config add-user <user-gid> --global --alias ali # personal, no --repocyber-asana config list-users --global--assignee resolution (below) checks the repo config first and only opens the global registry on
a clean miss — so an alias that’s unambiguous in each registry on its own never collides across
scopes. resolve-user --merged / list-users --merged, by contrast, are listing views and do
union the two, so a genuine cross-scope alias collision surfaces there instead.
Finding a user’s GID
Section titled “Finding a user’s GID”add-user --search <query> looks the person up with Asana’s typeahead search (the same
search cyber-asana search objects user uses) and registers the match. Typeahead is fuzzy, so
add-user only picks a user when the result is clear:
- A single hit is registered.
- Among several hits, a hit whose name or email equals the query exactly is registered.
- Otherwise nothing is written. The command lists the candidates, and you re-run it with the right GID.
Searching by email is the most reliable way to get one exact hit.
Assigning by name
Section titled “Assigning by name”task create, task update, and task subtask create take --assignee <user>, and the
asana_task_create, asana_task_update, and asana_task_subtask_create MCP tools take
assignee. The value can be a user GID, me, or anything resolve-user understands:
cyber-asana task create "Review the draft" --project-gid <gid> --assignee ali--assignee-gid (MCP: assignee_gid) still takes a GID as-is and wins when both are set.
A query is matched case-insensitively in four tiers — GID, then alias, then email, then
display name — and the first tier with a match decides. An alias is unique to one user, so
it always wins over someone else’s display name. A query that matches two users in the
deciding tier (two people with the same display name, say) is an error that lists both
candidates; give an alias or a GID instead. If the repo config has no match at all, the personal
global registry is checked next before giving up. A name that matches nobody in either is an error
too, naming both config add-user --search and config add-user --global as the fix.
Keeping names fresh
Section titled “Keeping names fresh”add, add-user, sync, discover-fields, and set-field are the only commands that call Asana. Everything else reads the file.
Beyond those, project get and the asana_project_get MCP tool opportunistically update
cached names whenever a result includes both gid and name, so the map drifts less than
you would expect.
Run config sync after a batch of renames in Asana.
Typical setup
Section titled “Typical setup”cyber-asana config add <project-gid>git add .agents/cyber-asana.jsonCommitting the file is the point — it is what lets a fresh agent session in a clone resolve “the backend project” to a GID on the first try.