Files
pcli/.claude/commands/kanban-sync.md
T
Steve Cliff 7937266262 feat(status): add --project flag for filtering boards by project name
- Implemented the --project flag in the pcli status command to filter boards based on the specified project name.
- Updated the command to resolve project names to IDs using case-insensitive matching.
- Adjusted the totalBoards count in the output to reflect the number of boards matching the project filter.
- Enhanced command help text and README documentation to include usage examples for the new flag.
- Verified functionality through manual testing and ensured default behavior remains unchanged when the flag is omitted.

feat(board): expand GetBoard response to include labels and card associations

- Modified the Board struct to include Labels, CardLabels, and CardMemberships fields.
- Updated the GetBoard method to parse additional fields from the API response.
- Enhanced ListCardsByBoard to include label names for each card based on the enriched board data.
- Ensured backward compatibility by making new fields optional and preserving existing output structure.
2026-02-18 21:38:41 +00:00

9.7 KiB

description, allowed-tools
description allowed-tools
Reconcile Planka board state with OpenSpec changes Bash, Read, Grep, Glob, AskUserQuestion

Planka <-> OpenSpec Reconciliation Sync

OpenSpec is the source of truth for all agentic work. Planka provides a read-only view so humans and other agents can see what's happening. This workflow reads OpenSpec state and reconciles Planka to match.

Prerequisites

Before syncing, verify connectivity:

pcli status

If this fails, skip the Planka sync silently. Planka sync is best-effort - never block agentic work because Planka is unavailable.


Core Principles

  1. OpenSpec owns the state - tasks.md, artifacts, and change status live in OpenSpec
  2. Planka is a projection - it reflects OpenSpec state, never the other way around
  3. Sync is one-directional - OpenSpec -> Planka, never Planka -> OpenSpec
  4. Sync is idempotent - running it twice produces the same result
  5. Sync is best-effort - if Planka is down, work continues uninterrupted
  6. Non-agentic work is managed directly in Planka (no OpenSpec involvement)

When This Runs

This workflow is triggered automatically after any opsx workflow completes (via project-level instruction in CLAUDE.md). It can also be invoked manually via /kanban-sync.


Board Structure

The board lists map to OpenSpec lifecycle stages:

List Position Purpose
Backlog 1 Non-agentic work items (human-managed, read-write)
To Do 2 Non-agentic work items ready to start (human-managed, read-write)
Planning 3 OpenSpec changes with artifacts still being created (opsx:new, opsx:continue, opsx:ff)
In Progress 4 Active implementation — tasks being worked (opsx:apply)
Review 5 All tasks complete, awaiting verification (opsx:verify)
Done 6 Completed and archived (opsx:archive)

Bootstrap: Ensure Project, Board, Lists, and Label Exist

Before reconciling, ensure all required Planka infrastructure exists. This makes the sync self-bootstrapping — running it on a fresh Planka instance will create everything needed.

1. Read project config

PROJECT_NAME=$(yq '.planka.project' project.yaml)
BOARD_NAME=$(yq '.planka.board' project.yaml)

If project.yaml doesn't exist or has no planka section, ask the user for the project and board name, then offer to create the file.

2. Find or create the project

PROJECT_ID=$(pcli project list | jq -r --arg name "$PROJECT_NAME" '.data[] | select(.name == $name) | .id')

If no project found:

PROJECT_ID=$(pcli project create --name "$PROJECT_NAME" --type "public" | jq -r '.data.id')

3. Find or create the board

# Get project details to find boards
BOARD_ID=$(pcli board list --project "$PROJECT_NAME" | jq -r --arg name "$BOARD_NAME" '.data[] | select(.name == $name) | .id')

If no board found:

BOARD_ID=$(pcli board create --project $PROJECT_ID --name "$BOARD_NAME" | jq -r '.data.id')

4. Find or create the lists

After obtaining the board, get its current lists:

EXISTING_LISTS=$(pcli board get $BOARD_ID | jq -r '.data.lists[]? | .name')

Create any missing lists with explicit positions to maintain correct ordering:

# Only create lists that don't already exist
pcli list create --board $BOARD_ID --name "Backlog" --position 65536
pcli list create --board $BOARD_ID --name "To Do" --position 131072
pcli list create --board $BOARD_ID --name "Planning" --position 196608
pcli list create --board $BOARD_ID --name "In Progress" --position 262144
pcli list create --board $BOARD_ID --name "Review" --position 327680
pcli list create --board $BOARD_ID --name "Done" --position 393216

Skip any list that already exists (match by name).

5. Find or create the agent label

LABEL_ID=$(pcli board get $BOARD_ID | jq -r '.data.labels[]? | select(.name == "agent") | .id')

If no agent label found:

LABEL_ID=$(pcli label create --board $BOARD_ID --name "agent" --color "berry-red" | jq -r '.data.id')

Reconciliation Steps

1. Gather OpenSpec state

openspec list --json

This returns all active changes with their names, schemas, and status.

For each active change:

openspec status --change "<name>" --json

Parse to get:

  • Change name
  • Schema name
  • Artifact completion status (how many artifacts complete vs total)
  • Whether all applyRequires artifacts are done
  • Whether tasks exist and their completion state

If a tasks.md exists, read it and parse checkbox state (- [ ] vs - [x]).

2. Determine target list for each change

Map each change to the correct board list based on its OpenSpec state:

Condition Target List
Artifacts incomplete (not all applyRequires done) Planning
Artifacts complete, tasks exist with incomplete items In Progress
All tasks complete (all [x]) Review
Change archived (not in active list) Done

3. Gather Planka state

pcli card list --board $BOARD_ID | jq '.data[] | select(.labels[]?.name == "agent")'

Build a map of existing agent-labelled cards by name, including which list they're currently in.

4. Reconcile: create missing cards

For each active OpenSpec change that has no matching Planka card:

# Determine the correct list based on change state (see step 2)
LIST_ID=$(pcli board get $BOARD_ID | jq -r --arg list "<target-list>" '.data.lists[] | select(.name == $list) | .id')
CARD_ID=$(pcli card create --list $LIST_ID --name "<change-name>" --description "<schema: schema-name>" | jq -r '.data.id')

# Add agent label
pcli card add-label $CARD_ID --label $LABEL_ID

5. Reconcile: move cards to correct list

For each agent-labelled card that exists but is in the wrong list (based on current OpenSpec state):

TARGET_LIST_ID=$(pcli board get $BOARD_ID | jq -r --arg list "<target-list>" '.data.lists[] | select(.name == $list) | .id')
pcli card move $CARD_ID --list $TARGET_LIST_ID

This ensures cards move through the board as work progresses:

  • Planning -> In Progress when artifacts are complete and apply begins
  • In Progress -> Review when all tasks are marked done
  • Review -> Done when the change is archived

6. Reconcile: sync task lists

For each OpenSpec change that has a tasks.md:

First, check if the card already has task lists by using pcli card get:

# Get card details including existing task lists and tasks
CARD_DATA=$(pcli card get $CARD_ID)
EXISTING_TL=$(echo "$CARD_DATA" | jq -r '.data.taskLists[0].id // empty')
  • If EXISTING_TL is empty (no task list exists) -> create one and add all tasks
  • If EXISTING_TL is set (task list already exists) -> compare existing tasks by name and update completion state as needed; only create tasks that don't already exist
# Create task list ONLY if none exists
if [ -z "$EXISTING_TL" ]; then
  TL_ID=$(pcli task-list create --card $CARD_ID --name "Implementation" --show-on-front | jq -r '.data.id')
else
  TL_ID="$EXISTING_TL"
fi

# Get existing task names to avoid duplicates
EXISTING_TASKS=$(echo "$CARD_DATA" | jq -r '.data.tasks[] | select(.taskListId == "'$TL_ID'") | .name')

# For each task in tasks.md (in order), assign incrementing positions:
#   position = (index + 1) * 65536   (i.e. 65536, 131072, 196608, ...)
#   - If a task with the same name already exists, update its completion state if needed
#   - If no matching task exists, create it with explicit position
pcli task create --task-list $TL_ID --name "<task description>" --position <pos>

# For tasks already in Planka, update completion state to match tasks.md:
pcli task update <task-id> --completed   # if tasks.md shows [x]

7. Reconcile: move completed/archived changes

For each agent-labelled Planka card that has no matching active OpenSpec change:

  • The change was likely archived -> move the card to "Done"
DONE_LIST_ID=$(pcli board get $BOARD_ID | jq -r '.data.lists[] | select(.name == "Done") | .id')
pcli card move $CARD_ID --list $DONE_LIST_ID

8. Report

After reconciliation, briefly summarise what changed:

  • Infrastructure created (project/board/lists/label): yes/no
  • Cards created: N
  • Cards moved: N (list details)
  • Tasks synced: N
  • Cards moved to Done: N
  • Errors (if any): list them

Non-Agentic Work

Cards without the agent label are human-managed and fully read-write. The kanban skill (/kanban) handles these directly - creating cards, moving them, adding checklists, etc.

The distinction:

  • Has agent label -> read-only projection, managed by this sync workflow
  • No agent label -> regular Planka card, managed directly by humans

ID Discovery

Planka IDs cannot be cached across sessions. Each sync run must discover IDs dynamically:

  1. Read project and board name from project.yaml
  2. Find the project by name: pcli project list | jq ...
  3. Find the board by name within the project
  4. Find lists on the board: pcli board get <board-id> | jq ...
  5. Find agent cards: pcli card list --board <board-id> | jq '.data[] | select(.labels[]?.name == "agent")'
  6. Match cards to changes by name

Guardrails

  • Never modify an agent-labelled card outside of this sync workflow
  • Never read Planka to determine what work to do - query OpenSpec instead
  • Always discover IDs dynamically - never hardcode or cache across sessions
  • Sync failures are silent - log a warning but never block opsx workflows
  • One board per project - if multiple boards exist, ask the user which to sync to
  • Idempotent - safe to run multiple times, will not create duplicates
  • Bootstrap is safe - creating project/board/lists is idempotent; existing resources are reused