Kairokuplaybook

Sync

Jira and Confluence, from the app outward.

Kairoku is the system of record for plans and documents; Jira and Confluence are where the rest of the organization reads them. Sync moves work outward from the app — and only outward, in one direction, from one place.

Coming soon on app.kairoku.io. Atlassian ships switched off on the hosted app in this release. Settings → Integrations lists Atlassian as Coming soon, a project's Sync tab reads Jira and Confluence sync is coming soon with a link to the roadmap, and the Jira and Publish controls elsewhere are hidden. The rest of this page describes how Sync behaves once Atlassian is available — connected with Connect with Atlassian, an OAuth sign-in, not an API token.

Jira
← create-or-updatestatus only →

Kairoku

publish / pull →
Confluence

Pushing a plan to Jira

A release's plan is phases of plan items. Pushing it from the Sync tab creates the matching Jira issues and records a sync_mappings row for each — the link between a local item and its remote key. The push is create-or-update: pushing a corrected plan a second time updates the issues that already exist rather than minting a second copy of the plan.

Status flows back the other way. The Jira refresh reads remote status into the app, and a plan re-write never overwrites an item's status or its Jira key — those belong to the sync. That is why upsert_plan will happily rewrite a title, a body, test notes and ordering, and will not touch the two fields the board owns.

Pushing and pulling Confluence

Publishing a document renders it into Confluence storage format — markdown or Mermaid, converted at publish time — and records its page mapping. The Sync tab's bulk push performs the identical write for many documents at once.

The pull reconciles the other direction: it reads the space, builds a page tree, diffs it against local folders, documents and mappings, and produces an ordered list of changes to apply. A remote edit that would overwrite a document the write guard protects — a frozen release box, a reference document, a ledger document without a valid appended row — is recorded as a conflict rather than applied. It stays visible, stays unapplied, and conflicts again on every future pull until the box unfreezes or the document is reclassified. Pages that arrive with no existing mapping land under Unsorted/ instead of at the project root, so an ordinary sync keeps applying exactly as before.

A version divergence — both sides edited since the last sync — is recorded under the same word and behaves differently: it goes to a human picker that shows both bodies and retains whichever side the decision discards, so nothing anyone wrote is lost to a click. Deciding it moves the version gate forward, so that conflict does not resurface on the next pull; taking the Confluence side still runs the write guard, which is why the guard refusal is the only kind that recurs.

The Documents tab's adoption wizard is what brings documents the project already has — including whatever the pull left in Unsorted/ — under the template tree: it lays the tree down, then proposes a template slot for each one, a human confirms or redirects every row, and the run reports before and after counts, failing outright rather than reporting if the document count drifted.

sync_mappings keeps both sides honest

The mapping table is what makes the plan and the board describe the same reality. A local item with a mapping row can be updated remotely and refreshed back. A remote issue with no mapping row is invisible to the plan forever — nothing in the app can adopt it after the fact.

That single fact is why invariant 1 reads the way it does:

The app owns plan structure. Agents never create Jira issues for plan structure — they write the plan with upsert_plan and a human pushes it from the Sync tab. An issue minted outside the app gets no sync_mappings row, so the plan can never see it again.

An agent that creates issues directly produces issues the status refresh never maps. The board looks correct while the Plan tab goes blind — the failure is silent, and it is the exact failure the mapping exists to prevent.

Auto-update, and where its line sits

When a project's auto-sync toggle for a system is on, agents may auto-update remote issues or pages that already have a sync_mappings row, bounded to the rows their call touched and logged as the agent. Creating remote structure — any new issue or page — remains a human push from the Sync tab.

Every clause of that is enforced rather than trusted:

  • Already mapped. An unmapped local is skipped in silence. There is no input to the auto-sync path that reaches a create.
  • Bounded. The caller passes the ids it just wrote; nothing widens the scope to the release or the project.
  • Logged as the agent. Every activity row carries the agent actor and the specific credential. A row that read as a human action nobody performed would be the quietest possible breach of the human-only-gates invariant.
  • Never a status. The Jira writer structurally cannot send one, so auto-sync cannot transition an issue even by accident.

None of that work happens inside the agent's request. The write marks the mapped rows it touched, claims an auto-sync run under the agent actor, and enqueues a durable drain workflow; the remote updates happen there, off the request path. The tool result reports how many updates were queued rather than how many landed. There is no row cap on it, and the reason the old one existed is gone with it — bounding the request only ever bounded the work because there was nowhere durable to run a queue.

On this page