- 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.
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
- OpenSpec owns the state -
tasks.md, artifacts, and change status live in OpenSpec - Planka is a projection - it reflects OpenSpec state, never the other way around
- Sync is one-directional - OpenSpec -> Planka, never Planka -> OpenSpec
- Sync is idempotent - running it twice produces the same result
- Sync is best-effort - if Planka is down, work continues uninterrupted
- 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
applyRequiresartifacts 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 Progresswhen artifacts are complete and apply beginsIn Progress->Reviewwhen all tasks are marked doneReview->Donewhen 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_TLis empty (no task list exists) -> create one and add all tasks - If
EXISTING_TLis 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
agentlabel -> read-only projection, managed by this sync workflow - No
agentlabel -> regular Planka card, managed directly by humans
ID Discovery
Planka IDs cannot be cached across sessions. Each sync run must discover IDs dynamically:
- Read project and board name from
project.yaml - Find the project by name:
pcli project list | jq ... - Find the board by name within the project
- Find lists on the board:
pcli board get <board-id> | jq ... - Find agent cards:
pcli card list --board <board-id> | jq '.data[] | select(.labels[]?.name == "agent")' - 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