Skip to content

t1k:plane

FieldValue
Modulet1k-plane
Version1.2.1
Effortmedium
Tools—

Keywords: bind repo to plane, estimated time, plane, plane cycle, plane issue, plane mcp, plane module, plane project, plane state, todo in progress done, work item, work item status, workitem, workload estimate

/t1k:plane
[bind|status|match|estimate|advance|comment|unbind] [PROJ-N]

Bind work to Plane (The1Studio/plane-mcp-server) and keep the board honest without anyone hand-updating it. Two bindings, two lifetimes: a repo binds to one project AND one module, persistently; a session binds to one or more work items, freshly each session.

This skill is also the SSOT behind the enforced work-item workflow that t1k:plan, t1k:cook, t1k:fix, t1k:test, t1k:review, t1k:git, and t1k:ship invoke. Those skills cite references/workflow-enforcement.md; they do not re-implement any of it.

  • Any /t1k:plan, /t1k:cook, /t1k:fix, /t1k:test, /t1k:review, /t1k:git, /t1k:ship run — the lifecycle gate fires automatically.
  • “Bind this repo to Plane”, “which Plane project is this repo?”, “rebind to a different project”.
  • “What am I working on?”, “show my open work items”, “find the work item for X”.
  • “Move CODEBASE-42 to In Progress”, “set 6 hours on CODEBASE-42”, “comment the PR on the work item”.
  • Creating a work item / module / project when nothing matches the task at hand.
  • A single read you already know the tool for — call mcp__plane__retrieve_work_item_by_identifier directly.
  • Conceptual questions about Plane the product — answer from training data, no tool calls.
  • ClickUp, Linear, Jira, or GitHub Issues work — wrong tracker. GitHub issue claiming is modules/t1k-maintainer/rules/issue-claim-discipline.md; the two systems are independent and both can be active.
  • Bulk import of >100 work items — rate-limited; script it against the API instead.
TriggerStageWhat happens
/t1k:planTodoMatch or create the work item · decompose into a nested sub-task tree, every leaf <= 4h · assign to the current user · write per-leaf estimates · state → unstarted group
/t1k:cook, /t1k:fixIn ProgressResolve the session binding · state → started group · write start_date + target_date · comment the plan path · move each phase sub-task as that phase starts, and to its completed state as it finishes
/t1k:test, /t1k:review(no transition)Comment the result — pass/fail counts, review verdict. Uses an In Review/Testing state only if the project defines one
/t1k:git push, /t1k:ship, PR mergedDoneComment the commit SHA / PR URL · state → completed group

Done fires once, at whichever of these comes last: a push with no PR opened → Done at push; a PR opened → Done at merge, not at push. Config: planeWorkflow.doneTrigger (pr-merge-else-push).

Stages resolve to state UUIDs by state group, never by name — see Gotchas.

Every work item T1K creates carries a scope prefix and the matching labels:

[Discipline][Kind] Title

Kind is required, exactly one. Discipline is optional, zero or one, and comes first.

Tokens
kind (required)feature bug doc chore refactor test polish balance
discipline (optional)2d 3d ui ux vfx anim audio design level liveops build qa
[Feature] Add piggy bank shop tier labels: feature
[2D][Feature] New rarity frame icon set labels: 2d, feature
[Audio][Polish] Rebalance SFX bus levels labels: audio, polish
[Balance] Retune wave 12 HP curve labels: balance

Compose and parse with the script — never hand-assemble a prefix, because the token→prefix casing is data (2d → [2D], liveops → [LiveOps]), not a derivable rule:

Terminal window
node .claude/scripts/t1k-plane-taxonomy.cjs compose --kind feature --discipline 2d --title "…"
node .claude/scripts/t1k-plane-taxonomy.cjs parse --title "[2D][Feature] …"

Tokens are inferred, not asked for — from the invoking skill, the title’s verbs, the branch name, then changed paths. The composed title appears inside the existing create-item confirmation and is editable there; only low confidence or a genuine tie triggers a question.

T1K never retrofits. An item it merely binds to keeps its owner’s title and labels untouched. Vocabulary meanings, inference precedence, label auto-create policy, and the full non-goals list: references/taxonomy.md.

This is a company-wide self-hosted Plane (https://plane.the1studio.org, not plane.so cloud) with multiple workspaces. One plane MCP server registration now addresses all of them; the workspace is not fixed at install time.

Get the workspace list from the server, not from this file. list_workspaces() returns every workspace the account belongs to, with each one’s project_count. Call it once per session and cache the result; this skill deliberately hardcodes no slug list, because a copy here is a second source of truth that goes stale the moment someone adds a workspace in Plane.

mcp__plane__list_workspaces() # → { discovery, workspaces: [{slug, name, id, reachable}], … }
mcp__plane__set_workspace(slug) # → session default

Read discovery before trusting the list — "api" means it is complete, "probe" means it is not (see Gotchas).

Resolution order, highest wins: explicit workspace_slug arg on the call → set_workspace(slug) session default → OAuth claim → PLANE_WORKSPACE_SLUG env default.

Picking one is the judgment the API cannot make for you. Match the work, not the repo: marketing and TOFUNNY work goes to the marketing workspace, Cocos-engine work to the Cocos one, Unity-engine work to the Unity one. Engine-agnostic kit or core work has no obvious home — ask rather than defaulting, and bind the answer per t1k-plane-binding.cjs bind --workspace-slug so it is asked once per repo.

Before the first bind in a repo, run this sequence — it is not optional:

  1. list_workspaces() — enumerate. Never assume the set; never reuse a slug from this file.
  2. Search the project across every workspace returned, not just the default. A project name that exists in one workspace may exist in another, and search_work_items / list_projects are workspace-scoped, so a single-workspace search cannot tell “not here” from “not anywhere”.
  3. If more than one workspace could plausibly own the work — or none obviously does — AskUserQuestion. Engine-agnostic core/kit work is exactly this case and has no right answer the API can supply.
  4. Only then bind --workspace-slug <chosen>.

bind now refuses without --workspace-slug (--default-workspace is the deliberate single-workspace opt-out), and unknown flags are a hard error rather than a silent no-op — a misspelled --workspace used to be dropped, leaving the bind silently on the server default while the caller believed it had named one. Neither guard replaces step 3: the script can force you to name a workspace, but only the user knows which one is right.

Terminal window
node .claude/scripts/t1k-plane-binding.cjs status
node .claude/scripts/t1k-plane-binding.cjs bind \
--workspace-slug unity \
--project-id <uuid> --project-identifier CODEBASE \
--project-name "Code Base" --module-id <uuid> --module-name "Core"
node .claude/scripts/t1k-plane-binding.cjs unbind

The script owns local state only; it never calls Plane. Resolution algorithm (workspace → fuzzy project match → module → confirm → auto-learn) is in references/binding.md.

Always pass --workspace-slug — a project UUID is only unique within its workspace, so a binding without it silently resolves against whatever the server default is. Then pass workspace_slug=<binding.workspaceSlug> on every Plane call rather than relying on that default. A binding written before this field existed has no workspaceSlug and means “use the server default” — still valid on a single-workspace instance.

Prefer binding the module over the project when the deliverable convention holds. A module is a child of a project, and a repo may bind at either level — project (parent) or module (child); both are valid bindings, and the gate blocks only when neither is set (no-target). When module is used as one deliverable (product / title / app) rather than Plane’s generic “feature grouping”, binding to the module is the more precise choice: it distinguishes this repo’s work from every other deliverable sharing that team’s project board. planeWorkflow.moduleBinding expresses this as an org-level preference on top of the gate’s floor — "optional" (default) leaves the choice to whoever binds; an org that wants the stricter policy can set "required" deliberately (#1271). Detection rule: references/binding.md § “What each hierarchy level means”.

Terminal window
node .claude/scripts/t1k-plane-binding.cjs session list
node .claude/scripts/t1k-plane-binding.cjs session add \
--work-item-id <uuid> --identifier CODEBASE-42 --name "Add plane skill" --stage todo
node .claude/scripts/t1k-plane-binding.cjs session stage --identifier CODEBASE-42 --stage in_progress
node .claude/scripts/t1k-plane-binding.cjs session clear

session stage returns changed:false when the item is already at that stage — skip the Plane write on false. That guard is what stops a re-run of /t1k:cook from spamming duplicate transitions.

Priority order — stop at the first hit:

  1. Explicit identifier in the user’s prompt (CODEBASE-42) → retrieve_work_item_by_identifier.
  2. Session binding already holds a matching item → reuse, no search.
  3. Repo-bound project → list_work_items(project_id, pql='stateGroup IN openStates() AND assignee = currentUser()'), plus search_work_items(query=<task keywords>) for a workspace-wide sweep.
  4. Score candidates on title/description overlap with the task, then propose — never auto-adopt.

No confident match → AskUserQuestion: bind to a candidate · bind to an identifier you type · create a new work item · proceed unbound (session-level only — does not satisfy the repo-level gate: the repo must still be bound, or recorded tracked:false). Full scoring rules: references/binding.md.

mcp__plane__set_issue_workload_estimate(project_id, work_item_id, hours)

Hours, not points. Derived from the plan’s phase breakdown and surfaced for confirmation before writing (planeWorkflow.estimate.confirmBeforeWrite). A work item that has sub-items rejects the write with PARENT_HAS_CHILDREN — put the estimate on the leaves; the parent rolls up via get_workload_rollups.

Every leaf estimate must be <= 4 hours. A larger number means the item is not decomposed yet, not that it is a big task — split it and re-estimate. Plans are always tracked as a nested tree, never as one item. Full contract: references/workflow-enforcement.md § “Decomposition”.

states = mcp__plane__list_states(project_id) # cache per project per session
target = first state whose `group` == stageMapping[stage].group
(tie-break by `preferredNames`, then lowest `sequence`)
mcp__plane__update_work_item(project_id, work_item_id, state=target.id)

mcp__plane__create_work_item_comment(project_id, work_item_id, comment_html=...) — HTML, not markdown. Used for plan links, test results, review verdicts, commit SHAs, PR URLs.

Full tool map, PQL notes, and the rest of the surface: references/operations.md.

Every time you name a work item, module, or project to the user, give them its URL. A bare identifier (WORLDGRAIN-289) or a UUID is not something a human can click, and neither the MCP tools nor the binding file carries a link — you have to build it. The script does it for you:

Terminal window
node .claude/scripts/t1k-plane-binding.cjs url # narrowest bound scope
node .claude/scripts/t1k-plane-binding.cjs url --scope project # project board
node .claude/scripts/t1k-plane-binding.cjs url --work-item <uuid> # one item
node .claude/scripts/t1k-plane-binding.cjs url --open # launch the browser

status also returns a urls block, so a caller that already ran it needs no second command. Prefer both over hand-assembling a path.

Route shapes ({base} = PLANE_BASE_URL, {ws} = binding.workspaceSlug):

ScopeRoute
Project board{base}/{ws}/projects/{projectId}/issues
Module{base}/{ws}/projects/{projectId}/modules/{moduleId}
Work item{base}/{ws}/projects/{projectId}/issues/{workItemId}
Cycles{base}/{ws}/projects/{projectId}/cycles

The work-item route keys on the UUID, not the identifier. WORLDGRAIN-289 is not addressable — resolve it through retrieve_work_item_by_identifier (or the id you already hold from a list_*) and pass that UUID.

PLANE_BASE_URL is not in your environment. The plane MCP server is stdio-launched with it set inline in its own config block, so it never reaches the agent process. The script resolves it in override order — PLANE_BASE_URL env → binding.baseUrl → project .mcp.json → ~/.claude.json — and reports which one answered as baseUrlSource. When none does, it returns state: "skip" rather than a guessed hostname: say you cannot build the link instead of emitting a plausible wrong one.

  • Confirm before creating or estimating. create_project, create_module, create_work_item, set_issue_workload_estimate, and any delete require explicit user confirmation via AskUserQuestion. Configured in planeWorkflow.guardedWrites.confirmRequired.
  • State transitions and comments are pre-authorized — they are the workflow the user opted into. Do not prompt for each one.
  • Never delete or archive a work item, module, or project as part of a workflow hook. Deletion is only ever an explicit user request.
  • Never invent a UUID. Every id comes from a list_* / retrieve_* call in the same session.
  • Never silently proceed unbound. If matching fails and the user does not choose an option, stop and say so — do not quietly skip the board update.
  • Never write to a project the repo is not bound to without re-confirming the binding.
  • Resolve states by group, not by name. Default projects ship Backlog(backlog) / Todo(unstarted) / In Progress(started) / Done(completed) / Cancelled(cancelled), but any project can rename them. Matching on the literal string "Todo" breaks the moment a team renames it to "Ready". Verified seed table: references/operations.md § States.
  • update_work_item(state=...) needs a state UUID, not a name or a group. Call list_states first.
  • fields does not shrink the returned key set. list_projects(fields="id,name,identifier") still returns every key — the unrequested ones come back null. A null here means not requested, not empty. Same trap on list_work_items: omit description_html and the description reads as null.
  • There is no description field — it is description_html. And project, not project_id, in sparse fieldsets.
  • set_issue_workload_estimate rejects parents (400 PARENT_HAS_CHILDREN). Estimate the leaves.
  • create_work_item takes assignees and parent — both are set on every item T1K creates: assignees=[<get_me().id>], parent=<parent id> for anything below the root. There is no bulk-create; the tree is built one call per node, parent before child.
  • Epics are not a separate tool. An epic is a work item whose type is named Epic — resolve via resolve_work_item_type(project_id, "Epic"), then create_work_item(type_id=...).
  • resolve_work_item_type / list_work_item_types 404 where work item types are unavailable (a paid/Pro feature; absent on community self-hosted deployments). The 404 means the feature is missing, not that the project id is wrong — do not retry it and do not create_work_item_type to work around it. Build the hierarchy from plain work items nested via parent instead; childOf(...) queries and estimate rollup still work, only the type label and type = "<uuid>" filtering are lost. Fallback recipe: references/operations.md § “When work item types are unavailable”.
  • A projectId without a workspaceSlug is structurally incomplete. Project UUIDs are workspace-scoped and every tool falls back to the default workspace, so such a binding looks complete and silently resolves against the wrong workspace. Bind with --workspace-slug; pass workspace_slug on every call.
  • A Plane module here does not mean “feature grouping”. That is Plane’s generic definition; the binding treats module as one deliverable (product / game title / app), which is why binding to the module — a child of the project — is usually the more precise choice over the project alone, not the only valid one. Read the shape of list_modules names to confirm before deciding via planeWorkflow.moduleBinding (default "optional"; an org may set "required", #1271).
  • update_work_item(labels=[…]) replaces the whole label list, silently dropping every label not in the array. To add or remove one, use manage_work_item_label(add_ids=…, remove_ids=…). Detail: references/operations.md § Labels.
  • UUID-valued PQL fields need real UUIDs. assignee, state, module, label, cycle, type in a PQL string must be UUIDs; call the matching list_* first. get_pql_reference has the full grammar.
  • list_projects is heavy in a workspace this size (50+ projects). Call it once per session and cache; the repo binding exists precisely so you rarely need it.
  • Reporting an identifier without a URL is the default failure, not a style choice. The MCP surface returns ids and names and no links at all, so the natural output of every Plane call is unclickable. Build the URL (§ 7) whenever a work item, module, or project reaches the user.
  • The binding file is never kit-shipped. .claude/plane-binding.json is consumer data. Do not move it into t1k-config-plane.json — t1k modules update replaces kit config files wholesale and would destroy every binding.
  • list_workspaces() has two modes, and only one of them gives a complete answer. Check the discovery field. "api" means the server exposed GET /api/v1/users/me/workspaces/ (present since The1Studio/plane#30) and the list is every workspace the account is an active member of — trust it as exhaustive. "probe" means the server predates that endpoint, so the tool fell back to testing candidate slugs one at a time; a workspace missing from a probe result is missing from the probe set, not necessarily from the account. Under probe, reachable: false is also ambiguous by construction: Plane answers a wrong slug and a real slug you lack access to with the same 403. Never report a probe result as “these are your workspaces” — and never write the result into a file, which is how the old hardcoded routing table went stale.

T1K_PLANE_MODE=advisory (warn, never block) · T1K_PLANE_MODE=off (skip entirely) · --no-plane on any hooked skill · planeWorkflow.enabled: false in t1k-config-plane.json · a recorded tracked:false (optout). An unbound repo or an absent plane server blocks the hooked skills until one of these applies — the escape hatches, not a silent warning, are the way through. A registered-but-unreachable server blocks in the skill body.

  • references/workflow-enforcement.md — the enforced lifecycle contract cited by the seven hooked skills
  • references/binding.md — repo→project/module and session→work-item resolution algorithms
  • references/operations.md — full tool map, PQL, estimates, cycles, modules
  • modules/t1k-plane/rules/plane-workitem-workflow.md — the always-loaded rule that makes the workflow enforced
  • t1k-config-plane.json — enforcement mode, stage mapping, guarded-write lists