--- description: Reconcile Planka board state with OpenSpec changes allowed-tools: 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: ```bash 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 ```bash 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 ```bash PROJECT_ID=$(pcli project list | jq -r --arg name "$PROJECT_NAME" '.data[] | select(.name == $name) | .id') ``` If no project found: ```bash PROJECT_ID=$(pcli project create --name "$PROJECT_NAME" --type "public" | jq -r '.data.id') ``` ### 3. Find or create the board ```bash # 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: ```bash 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: ```bash EXISTING_LISTS=$(pcli board get $BOARD_ID | jq -r '.data.lists[]? | .name') ``` Create any missing lists with explicit positions to maintain correct ordering: ```bash # 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 ```bash LABEL_ID=$(pcli board get $BOARD_ID | jq -r '.data.labels[]? | select(.name == "agent") | .id') ``` If no `agent` label found: ```bash LABEL_ID=$(pcli label create --board $BOARD_ID --name "agent" --color "berry-red" | jq -r '.data.id') ``` --- ## Reconciliation Steps ### 1. Gather OpenSpec state ```bash openspec list --json ``` This returns all active changes with their names, schemas, and status. For each active change: ```bash openspec status --change "" --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 ```bash 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: ```bash # Determine the correct list based on change state (see step 2) LIST_ID=$(pcli board get $BOARD_ID | jq -r --arg list "" '.data.lists[] | select(.name == $list) | .id') CARD_ID=$(pcli card create --list $LIST_ID --name "" --description "" | 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): ```bash TARGET_LIST_ID=$(pcli board get $BOARD_ID | jq -r --arg 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`: ```bash # 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 ```bash # 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 "" # For tasks already in Planka, update completion state to match tasks.md: pcli task update --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" ```bash 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 | jq ...` 5. Find agent cards: `pcli card list --board | 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