Files
pcli/.claude/commands/kanban-sync.md
T
Steve Cliff 22d5848e1a feat: Add openspec-sync-specs and openspec-verify-change skills
- Introduced `openspec-sync-specs` skill to sync delta specs to main specs, allowing intelligent merging of requirements.
- Added `openspec-verify-change` skill to verify implementation against change artifacts, ensuring completeness, correctness, and coherence before archiving.

docs: Create CLAUDE.md for project guidance

- Added CLAUDE.md to provide an overview of the PCLI project, including build, test commands, architecture, and resource addition guidelines.

chore: Add new change and design documents for project filter in status command

- Created `.openspec.yaml`, `design.md`, `proposal.md`, and `tasks.md` for the `add-project-filter-to-status` change.
- Updated specs for CLI commands and status command to include project filtering functionality.

feat: Expand board included parsing in API client

- Added parsing for `labels`, `cardLabels`, and `cardMemberships` in the `GetBoard` response.
- Updated `ListCardsByBoard` to enrich card output with label names, enhancing usability in kanban sync workflows.
2026-02-18 21:27:02 +00:00

9.6 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:
#   - If a task with the same name already exists, update its completion state if needed
#   - If no matching task exists, create it
pcli task create --task-list $TL_ID --name "<task description>"

# 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