Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Importing a kata ledger

kata keeps issues in its own database, for people and for agents. ikigai-ledger took its model from kata — issues with authors, priorities, labels, comments, closes with a reason, and parent, blocks and related links — so most of an import is one to one. This chapter imports one, runs it again to show that it converges, and says plainly what does not survive the trip.

An export

In a new empty directory, as in every chapter: if the gonk from Filing roborev findings is still running, stop it first, since this one takes the same port.

kata export writes the database out as JSON Lines, one record per line, each with a kind (stop kata’s daemon first, or pass --allow-running-daemon):

$ kata export --output kata.jsonl

Here is a small one: a project called flight, four issues (one closed wontfix, one deleted), three comments, three labels, four links, and an event. Save it as kata.jsonl:

{"kind":"meta","data":{"key":"export_version","value":"27"}}
{"kind":"meta","data":{"key":"instance_uid","value":"01K6Z0000000000000000INST1"}}
{"kind":"meta","data":{"key":"schema_version","value":"27"}}
{"kind":"project","data":{"created_at":"2026-09-01T12:00:00.000Z","id":1,"metadata":{},"name":"flight","revision":1,"uid":"01K6Z00000000000000000PRJ1"}}
{"kind":"issue","data":{"author":"chris","body":"The loop should stop when the queue is empty.\n\nSee the flight log.","closed_at":null,"closed_reason":null,"content_revision":1,"created_at":"2026-09-02T09:15:00.000Z","deleted_at":null,"id":1,"metadata":{},"owner":"chris","priority":1,"project_id":1,"revision":3,"short_id":"0001","status":"open","title":"Stop the loop on an empty queue","uid":"01K6Z000000000000000000001","updated_at":"2026-09-03T10:00:00.000Z"}}
{"kind":"issue","data":{"author":"chris","body":"","closed_at":"2026-09-04T08:00:00.000Z","closed_reason":"wontfix","content_revision":0,"created_at":"2026-09-02T09:20:00.000Z","deleted_at":null,"id":2,"metadata":{},"owner":null,"priority":3,"project_id":1,"revision":2,"short_id":"0002","status":"closed","title":"Rename the flight binary","uid":"01K6Z000000000000000000002","updated_at":"2026-09-04T08:00:00.000Z"}}
{"kind":"issue","data":{"author":"agent-7","body":"Child of 0001.","closed_at":null,"closed_reason":null,"content_revision":0,"created_at":"2026-09-02T11:00:00.000Z","deleted_at":null,"id":3,"metadata":{},"owner":null,"project_id":1,"revision":1,"short_id":"0003","status":"open","title":"Write the empty-queue test","uid":"01K6Z000000000000000000003","updated_at":"2026-09-02T11:00:00.000Z"}}
{"kind":"issue","data":{"author":"chris","body":"Filed by mistake.","closed_at":null,"closed_reason":null,"content_revision":0,"created_at":"2026-09-02T12:00:00.000Z","deleted_at":"2026-09-02T12:05:00.000Z","id":4,"metadata":{},"owner":null,"priority":2,"project_id":1,"revision":2,"short_id":"0004","status":"open","title":"A mistake","uid":"01K6Z000000000000000000004","updated_at":"2026-09-02T12:05:00.000Z"}}
{"kind":"comment","data":{"author":"chris","body":"Seen twice this week.","created_at":"2026-09-02T10:00:00.000Z","id":1,"issue_id":1,"uid":"01K6Z0000000000000000000C1"}}
{"kind":"comment","data":{"author":"chris","body":"Agent 7 is on it.","created_at":"2026-09-03T10:00:00.000Z","id":2,"issue_id":1,"teammate":"agent-7","uid":"01K6Z0000000000000000000C2"}}
{"kind":"comment","data":{"author":"chris","body":"Never mind.","created_at":"2026-09-02T12:04:00.000Z","id":3,"issue_id":4,"uid":"01K6Z0000000000000000000C3"}}
{"kind":"issue_label","data":{"author":"chris","created_at":"2026-09-02T09:15:00.000Z","issue_id":1,"label":"bug"}}
{"kind":"issue_label","data":{"author":"chris","created_at":"2026-09-02T09:15:00.000Z","issue_id":1,"label":"area:loop"}}
{"kind":"issue_label","data":{"author":"chris","created_at":"2026-09-02T09:20:00.000Z","issue_id":2,"label":"chore"}}
{"kind":"link","data":{"author":"agent-7","created_at":"2026-09-02T11:00:00.000Z","from_issue_id":3,"from_issue_uid":"01K6Z000000000000000000003","id":1,"to_issue_id":1,"to_issue_uid":"01K6Z000000000000000000001","type":"parent"}}
{"kind":"link","data":{"author":"agent-7","created_at":"2026-09-02T11:01:00.000Z","from_issue_id":3,"from_issue_uid":"01K6Z000000000000000000003","id":2,"to_issue_id":1,"to_issue_uid":"01K6Z000000000000000000001","type":"blocks"}}
{"kind":"link","data":{"author":"chris","created_at":"2026-09-02T09:21:00.000Z","from_issue_id":1,"from_issue_uid":"01K6Z000000000000000000001","id":3,"to_issue_id":2,"to_issue_uid":"01K6Z000000000000000000002","type":"related"}}
{"kind":"link","data":{"author":"chris","created_at":"2026-09-02T12:01:00.000Z","from_issue_id":1,"from_issue_uid":"01K6Z000000000000000000001","id":4,"to_issue_id":4,"to_issue_uid":"01K6Z000000000000000000004","type":"related"}}
{"kind":"event","data":{"actor":"chris","created_at":"2026-09-02T09:15:00.000Z","id":1,"issue_id":1,"payload":{},"project_id":1,"type":"issue.created","uid":"01K6Z0000000000000000000E1"}}
{"kind":"sqlite_sequence","data":{"name":"issues","seq":4}}

Into a ledger of its own

The importer files over gonk’s HTTP door, like roborev file, as its anonymous loopback caller, so the ledger it files into must be one that door grants. Start gonk granting kata as well as default (--http-ledger replaces the configured list, so name both; in a config file it is one gonk.http.ledger line each):

$ ikigai-gonk --config-home "$PWD/.config/ikigai" --data-home "$PWD/.ikigai" --port 1070 --no-quic --no-backup --http-ledger default --http-ledger kata
ikigai-gonk 0.1.0 — holding the store at …/.ikigai/store
  http    http://localhost:1070/ — loopback (127.0.0.1:1070); anonymous read+write: default, kata; 0 passkey(s); anonymous SPARQL budget 1000 ms
…

--dry-run reads the export, asks the ledger for each issue’s key exactly as the real run does, and prints the plan; it writes nothing. So it says already for an issue that is there, reports the comments, closes and links a real run would add, and is refused, like the real run, on a ledger the door does not grant. (roborev file --dry-run is the other way round: it asks gonk nothing.) ⚠ The one thing a read cannot tell: an issue whose item was deleted in the ledger answers like one never filed, so the dry run says would file where the real run leaves it alone. Into an empty ledger, the plan is every issue:

$ ikigai-gonk kata import kata.jsonl --gonk http://127.0.0.1:1070 --ledger kata --dry-run
kata export version 27: 4 issue(s), 4 link(s) (dry run: reads only)
would file  kata 0001  Stop the loop on an empty queue
    key=urn:kata:issue:01K6Z000000000000000000001
    about=urn:kata:issue:01K6Z000000000000000000001
    author=chris
    priority=1
    labels=bug,area:loop
    then 2 comment(s)
would file  kata 0002  Rename the flight binary
    key=urn:kata:issue:01K6Z000000000000000000002
    about=urn:kata:issue:01K6Z000000000000000000002
    author=chris
    priority=3
    labels=chore
    then 0 comment(s), close
would file  kata 0003  Write the empty-queue test
    key=urn:kata:issue:01K6Z000000000000000000003
    about=urn:kata:issue:01K6Z000000000000000000003
    author=agent-7
    then 0 comment(s)
deleted   kata 0004  not imported
would linked    01K6Z000000000000000000003 parent 01K6Z000000000000000000001
would linked    01K6Z000000000000000000003 blocks 01K6Z000000000000000000001
would linked    01K6Z000000000000000000001 related 01K6Z000000000000000000002
skipped   link 01K6Z000000000000000000001 related 01K6Z000000000000000000004: an end was not imported, or the type is not one the ledger has
dry run, nothing written: 3 would be filed, 0 already there; 2 comment(s), 1 close(s), 3 link(s) would be added; 1 deleted issue(s) and 1 link(s) skipped; not read: event 1, sqlite_sequence 1

and without it, the import:

$ ikigai-gonk kata import kata.jsonl --gonk http://127.0.0.1:1070 --ledger kata
kata export version 27: 4 issue(s), 4 link(s)
filed     kata#1   kata 0001  Stop the loop on an empty queue
commented kata#1   kata comment 01K6Z0000000000000000000C1
commented kata#1   kata comment 01K6Z0000000000000000000C2
filed     kata#2   kata 0002  Rename the flight binary
closed    kata#2   kata 0002
filed     kata#3   kata 0003  Write the empty-queue test
deleted   kata 0004  not imported
linked    kata#3 parent kata#1
linked    kata#3 blocks kata#1
linked    kata#1 related kata#2
skipped   link 01K6Z000000000000000000001 related 01K6Z000000000000000000004: an end was not imported, or the type is not one the ledger has

3 filed, 0 already there; 2 comment(s), 1 close(s), 3 link(s) added; 1 deleted issue(s) and 1 link(s) skipped; not read: event 1, sqlite_sequence 1

Issues first, so that every link has two items to join, then the links. The soft-deleted issue is not imported, and neither is anything that hangs from it: its comment, and the link to it, which is reported as skipped. The last line counts everything the importer did not read, by kind, so nothing in the export is silently dropped.

The links came across as links, which means next already knows the order of the work:

$ curl -s http://127.0.0.1:1070/iki/ledger/kata/next
 1. kata#3  open    p-  Write the empty-queue test
    unprioritized — no priority set, so it ranks below every item that has one; last updated …

policy: priority-recency (weighs priority, recency, number)
ready: 1   excluded: 1
  not kata#1: blocked by kata#3

Again, and later

Every issue is filed with one request, a keyed append whose key is urn:kata:issue:<uid> (also filed as an about): the ledger checks the key and files the item in the same store update, and a key already taken answers the item that holds it and files nothing. So the same export imported twice files nothing:

$ ikigai-gonk kata import kata.jsonl --gonk http://127.0.0.1:1070 --ledger kata
kata export version 27: 4 issue(s), 4 link(s)
already   kata#1   kata 0001
already   kata#2   kata 0002
already   kata#3   kata 0003
deleted   kata 0004  not imported
skipped   link 01K6Z000000000000000000001 related 01K6Z000000000000000000004: an end was not imported, or the type is not one the ledger has

0 filed, 3 already there; 0 comment(s), 0 close(s), 0 link(s) added; 1 deleted issue(s) and 1 link(s) skipped; not read: event 1, sqlite_sequence 1

It does more than skip. A run converges on the items already there: a comment whose kata id is not on its item is added, an issue closed in kata whose item is open is closed, and a missing link is made. So a later export, with work done in kata since, brings exactly that work across. Here a comment is added to the export, as a later kata export would have it, and the dry run, which asks the ledger, names exactly the one comment the import then adds:

$ cp kata.jsonl later.jsonl
$ printf '%s\n' '{"kind":"comment","data":{"author":"agent-7","body":"The test fails on an empty queue, as it should.","created_at":"2026-09-05T08:30:00.000Z","id":4,"issue_id":3,"uid":"01K6Z0000000000000000000C4"}}' >> later.jsonl
$ ikigai-gonk kata import later.jsonl --gonk http://127.0.0.1:1070 --ledger kata --dry-run
kata export version 27: 4 issue(s), 4 link(s) (dry run: reads only)
already   kata#1   kata 0001
already   kata#2   kata 0002
already   kata#3   kata 0003
would commented kata#3   kata comment 01K6Z0000000000000000000C4
deleted   kata 0004  not imported
skipped   link 01K6Z000000000000000000001 related 01K6Z000000000000000000004: an end was not imported, or the type is not one the ledger has
dry run, nothing written: 0 would be filed, 3 already there; 1 comment(s), 0 close(s), 0 link(s) would be added; 1 deleted issue(s) and 1 link(s) skipped; not read: event 1, sqlite_sequence 1
$ ikigai-gonk kata import later.jsonl --gonk http://127.0.0.1:1070 --ledger kata
kata export version 27: 4 issue(s), 4 link(s)
already   kata#1   kata 0001
already   kata#2   kata 0002
already   kata#3   kata 0003
commented kata#3   kata comment 01K6Z0000000000000000000C4
deleted   kata 0004  not imported
skipped   link 01K6Z000000000000000000001 related 01K6Z000000000000000000004: an end was not imported, or the type is not one the ledger has

0 filed, 3 already there; 1 comment(s), 0 close(s), 0 link(s) added; 1 deleted issue(s) and 1 link(s) skipped; not read: event 1, sqlite_sequence 1

⚠ What does not converge: an issue’s title, body, labels and priority are written once, when it is filed. An edit made in kata later does not reach the item. Two imports of one export at the same instant file each issue once, because the filing is the keyed append; but only the filing is atomic. The convergence reads the item and then writes, and the ledger has no keyed comment, so two imports racing on the same new comment can both add it.

What is lost, and why

$ curl -s http://127.0.0.1:1070/iki/ledger/kata/item/1 | grep -E 'filed:|Imported from'
  filed:    … by chris
  Imported from kata flight#0001 (01K6Z000000000000000000001), filed 2026-09-02T09:15:00.000Z by chris; owner chris.

The two filed times disagree, and that is the loss in one line. The ledger stamps every item with the time it was filed and mints its own number, because those are facts the ledger vouches for, and it has no way today to record an item as filed at some other time by some other system. So kata’s created_at, its short_id (0001) and its uid survive only as text, in the provenance line every imported body ends with, and in each comment’s closing note. The uid is also the item’s key (and an about), which is what makes a re-run find it.

Carried as text only, or not at all:

katain the ledger
created_at, short_id, uidthe provenance line; the uid is also the item’s key and an about
a comment’s timeits closing note (kata comment <uid>, <created_at>)
ownerthe provenance line, not a claim
updated_at, metadata, who added a label or link and whennot carried
the event historynot carried; counted on the last line (event 1)
sync bindings, federation state, claims, purge logsnot carried; counted on the last line

A faithful import, keeping kata’s times and ids as data rather than text and carrying the history, needs the ledger to accept an item’s original times and a foreign id. That is tracked as ledger item 774. Until then the import is lossy in exactly the ways above, and says so.