{"id":"84ed83a0-d2eb-4838-8908-2275418e51c1","slug":"yaiba","kind":"mcp","name":"yaiba","summary":"刃 — vim-flavoured todo & gantt planner. One local binary, embedded UI, peer-to-peer sync over iroh.","owner":"yukimemi","repoUrl":"https://github.com/yukimemi/yaiba","license":"MIT","licenseTier":"permissive","trust":"community","stars":3,"match":null,"fewStars":true,"format":"mcp","createdAt":"2026-07-25T17:49:35.000Z","addedAt":"2026-10-21T09:22:55.695Z","logoUrl":"https://avatars.githubusercontent.com/u/6442108?v=4","linkOnly":false,"collection":null,"skillPath":null,"categories":["productivity","data-analytics","design-media"],"tags":["crdt","gantt-chart","iroh","peer-to-peer","project-management","rust","todo","typescript"],"runtime":"binary","local":true,"cloudCapable":false,"cloudCapability":"unknown","cloudReadyOnPlatform":false,"needsApiKey":false,"reviewed":false,"syncedAt":"2026-10-26T01:47:18.968Z","sample":false,"url":"https://conduits.dev/tools/yaiba","language":"TypeScript","transports":["stdio"],"install":{},"requirements":{"env":[],"upstreamMayCharge":false},"tools":null,"readme":"<p align=\"center\">\n  <img src=\"https://raw.githubusercontent.com/yukimemi/yaiba/main/assets/logo.svg\" alt=\"yaiba — vim-flavoured todo & gantt, peer to peer\" width=\"620\">\n</p>\n\n> 刃 — *the blade*. A vim-flavoured todo and gantt planner that runs as a\n> single local binary and syncs directly between people. No server to\n> host, no account to make.\n\n**Status: released — modal UI, a foldable work breakdown, dependency\nscheduling with a critical path, and peer-to-peer replication over\n[iroh](https://iroh.computer). `cargo install yaiba`.**\n\n<p align=\"center\">\n  <img src=\"https://raw.githubusercontent.com/yukimemi/yaiba/main/assets/demo.gif\" alt=\"a row opened with o and made to wait for another with D, the date columns filled by walking them, a block of dates yanked onto the actuals, then super mode with strikes flying off the caret and a shockwave on a completion, and the same plan in office mode and in Japanese\" width=\"900\">\n</p>\n\n<p align=\"center\">\n  <sub><code>o</code> opens a row · <code>+</code> lengthens it ·\n  <code>D</code> makes it wait for another, and the critical path\n  moves to follow · the divider drags · <code>gd</code> puts the dates\n  in columns, <code>h</code> <code>l</code> walk them and <code>⏎</code>\n  edits one · <code>v</code> <code>y</code> <code>p</code> move a block\n  of cells · <code>co</code> hands the row over ·\n  <code>zm</code> folds an altitude · <code>gs</code> is super mode —\n  the caret throws strikes as you type, a completion sets off a\n  shockwave and a delete shakes the screen · <code>gt</code> is office\n  mode · <code>:lang ja</code> is the whole UI</sub>\n</p>\n\n## Why\n\nTask tools make you choose. Terminal tools are fast to type into but\ncan't draw a timeline. Web planners draw beautiful gantt charts and then\nask you to reach for the mouse — and to put your plans on someone else's\nserver.\n\n`yaiba` refuses the choice:\n\n- **The keyboard is the interface.** Modal editing, `:` commands,\n  operators that wait for a motion. `o` opens a row and you type;\n  `space` completes it; `dd` deletes and `u` brings it back.\n- **Dependencies are real, not decoration.** Finish-to-start edges feed a\n  forward/backward pass that computes each task's earliest start, its\n  slack, and the critical path — so \"when does this actually land\"\n  has an answer rather than a guess.\n- **One outline, every altitude.** Tasks nest, and folding to a level\n  changes who the view is for: level 0 is the project list a manager\n  scans, level 4 is the work an implementer is actually doing. Same\n  data, same keys.\n- **One binary.** The UI is compiled in. Download, run, work.\n- **Peer-to-peer, not client-server.** Everyone runs their own replica.\n  Edits merge; nobody hosts.\n\n## Install\n\nFrom crates.io, or build from source:\n\n```sh\ncargo install yaiba\nyaiba\n```\n\nPrebuilt binaries for Linux / macOS / Windows are on the\n[releases page](https://github.com/yukimemi/yaiba/releases). To build it\nyourself:\n\n```sh\ngit clone https://github.com/yukimemi/yaiba\ncd yaiba\ncargo make release-build     # builds the web bundle, then the binary\n./target/release/yaiba\n```\n\nTo put your build on `PATH` instead of running it out of `target/`:\n\n```sh\ncargo make install           # or: makers install\n```\n\nBoth go through the web bundle first. Plain `cargo install --path .`\ndoes not, and the UI is baked in by `rust-embed` at compile time — a\nmissing bundle installs an empty one silently rather than failing.\n\nThe browser opens on `http://localhost:8188`. The database lives in your\nplatform data directory (`--db` to move it).\n\n```sh\nyaiba --port 9000            # somewhere else\nyaiba --no-open              # don't launch a browser\nyaiba --no-sync              # fully local, no peer endpoint at all\nyaiba --relay-only           # sync without binding a UDP socket — no firewall prompt\nyaiba --host 0.0.0.0         # expose the UI on your LAN (no auth — trusted networks only)\n```\n\n### Staying current\n\n`yaiba` updates itself. On launch it quietly checks GitHub for a newer\nrelease and, if there is one, downloads and swaps its own binary in the\nbackground — the running process keeps the old one, so an update never\npulls the floor out from under an open UI. The new version applies next\ntime you start it.\n\n```sh\nyaiba --update notify        # tell me, don't touch the binary\nyaiba --update off           # never look\nYAIBA_NO_AUTOUPDATE=1 yaiba  # same, from the environment\n\nyaiba self-update            # do it now, interactively\nyaiba self-update --check    # just tell me whether one exists\n```\n\n## Keys\n\n`?` shows the full list in the app. The shape of it:\n\n| | |\n|---|---|\n| `j` `k` `gg` `G` `^d` `^u` | move |\n| `o` `O` | new task below / above — you type immediately, it saves on commit |\n| `i` `I` `a` `A` `cc` | edit the cell under the cursor — on the title, at the head / tail / cleared |\n| `space` | complete (the row gets cut) · `s` cycles todo → doing → done |\n| `x` | clear the cell under the cursor · `dl` is vim's spelling of it |\n| `dd` `yy` `Y` | delete, yank rows |\n| `p` `P` | put the last yank · `P` is rows only · `u` / `^r` undo, redo |\n| `J` `K` | move the row down / up, changing level to suit where it lands |\n| `+` `-` | duration ±1 day · `.` `,` start ±1 day · `gp` `gP` priority · `(` `)` progress |\n| `D` | add a dependency: pick the task this one waits for, `⏎` |\n| `X` | cut a dependency |\n| `v` `V` | select cells / whole rows — every edit above applies to the block |\n| `/` `n` `N` | search |\n| `tab` | split → list → gantt · `[` `]` zoom the timeline |\n| `gd` | plan-vs-actual date columns ⇄ compact |\n| `cs` `ce` `ca` `cA` | calendar on the planned start / end, actual start / end |\n| `co` | hand the row over — the panel lists the names already in use |\n| `gt` | office mode ⇄ neon · `:theme` and `:lang` say it by name |\n| `gs` | super mode ⇄ neon — every effect at maximum · `:super` |\n| `gw` | glass mode ⇄ neon — slow light, soft edges · `:glass` |\n| `gc` | the colours — presets, and every slot by hand · `:colors` |\n| `h` `l` | out / in — a fold, or a cell once `gd` is up · `⏎` `i` `I` `a` `A` `cc` edit it |\n| `>>` `<<` | nest under the row above / move back out |\n| `zm` `zr` | fold one level shallower / deeper · `zM` `zR` all the way |\n| `za` | fold this row · `zf` focus its subtree, `zF` to come back |\n\n## Four modes, and a mouse\n\n`yaiba` ships a second theme for the times a neon HUD is the wrong\nthing to have open — a meeting room, a shared screen, a status deck.\nOffice mode drops the glow, the scanlines and the completion sweep\nentirely, and swaps cyan/magenta for the blue/red/amber a reader already\nknows from every other planning tool. It prints.\n\n```text\ngt              office mode <-> neon mode\n:theme light    or by name\n```\n\nAnd a third for the opposite occasion. **Super mode** is the same HUD\nwith nothing held back: the glow multiplier goes up, an aurora drifts\nbehind the plan, a CRT band rolls down the screen, the wordmark flickers\nlike a sign, the cursor row catches the light, the critical path marches,\nand every stroke the blade draws is answered by the whole screen — a\nshockwave on a completion, a shake on a delete.\n\nTyping draws too. Every character throws short strikes off the caret and\nputs a small recoil through the shell, a run of them keeps a count, and\nthe count makes both louder — the power-mode gag with a blade in place\nof the sparks. It follows the caret through anything you can type into:\na task title, the `:` line, search, the project palette. Japanese draws\nkana by kana as the reading is typed, and the 変換 that commits it lands\nheavier than a single key.\n\n```text\ngs              super mode <-> neon mode\n:super          bare toggles · :super on / :super off\n:theme super    or by name\n```\n\nIt is one axis, not a switch beside the theme: `gt` takes you out of\nsuper the same way it takes you out of neon, because office mode is\nsomewhere you go *to* and it wins. Two things survive being turned up\nthis far — the palette rule, so magenta still means the critical path\nand nothing else, and `prefers-reduced-motion`, which turns the whole\nmode's motion off and leaves nothing frozen on screen.\n\nAnd a fourth, opposite the third. **Glass mode** is the same HUD with\nthe brakes left on and the clock turned down instead: the panels frost\n— a blur behind them rather than a colour under them — light drifts\nbehind the plan the way it does through water, the cursor row breathes\nrather than sweeps, the critical path still marches but at a slow\ntide's pace, and a stroke still answers at the scale of the whole\nscreen, as a soft ripple rather than a shockwave.\n\nTwo things it deliberately does not have. No shake on a delete and no\nstrikes off the caret while typing — both are fast gestures, and fast\nis the one thing this theme refuses to be.\n\n```text\ngw              glass mode <-> neon mode\n:glass          bare toggles · :glass on / :glass off\n:theme glass    or by name\n```\n\nIt rides the same axis super does: `gt` takes you out of glass exactly\nthe way it takes you out of super, and the two loud themes cannot be on\nat once — pressing one always turns the other off first, on the way to\nwherever it is going.\n\nThe choice is remembered, and a fresh install follows your OS\npreference.\n\n### The colours are yours\n\nThe four modes above decide how loud the screen is. What each colour\n*is* is a separate question, and `gc` opens the panel that answers it:\ntwelve named slots, a hex field and a swatch on each, and a handful of\npresets to start from.\n\n```text\ngc              the colour panel · :colors, or ◍ in the bar\nネオン / オフィス   which of the two grounds you are editing — the tab\n                switches the theme with it, since editing colours you\n                cannot see is not editing\nyaiba           the built-in palette: the way back from any preset\ngruvbox nord dracula tokyo          on the neon ground\nsolarized · gruvbox light           on the office one\n```\n\nWhat you are replacing is the *value* of a slot, never what it means.\nCyan is the blade, magenta is the critical path and amber is overdue —\nthat rule is the reason a row can say something in a colour instead of a\nword, so the panel lets you pick which magenta and not which meaning.\nTwo things are deliberately out of reach for the same reason: the glow,\nwhich is the whole of office mode and lives on `gs` / `gw`, and anything that is\nnot a colour.\n\nThe two grounds keep separate palettes, because a fill mixed for a\nnear-black background washes out to nothing over white. `×` puts one slot\nback; the `yaiba` preset puts the ground back. It is remembered next to\nthe theme, so it survives a reload and follows you across projects.\n\nThe UI has a language too, and that one starts in English:\n\n```text\n:lang ja        日本語で表示します\n:lang en        back to English (bare :lang toggles)\n```\n\nIt is the whole surface, not a veneer: the `?` panel, the status line's\nreplies, every refusal the `:` line can give you, the column heads and\nthe project palette. English is the default rather than your browser's\nlocale, because a screenshot of `yaiba` should read the same wherever it\nis opened; `ja` is a choice, and it is remembered. Both toggles sit at\nthe right of the top bar for a mouse.\n\nWhat is *not* translated is anything you type. Key names, command names,\n`todo` / `doing` / `done`, tag names and the words a command accepts\n(`day, week, month`) read the same in either language — a help that\nrenamed the keys would be describing a different program from the one\nunder your fingers.\n\nThe keyboard remains the point, but every common action has a mouse\nequivalent, because handing the screen to someone else should not\nrequire handing over the keybindings too:\n\n| | |\n|---|---|\n| click a row | put the cursor on it |\n| right-click a row or its bar | the menu of what nothing else here reaches |\n| `⇧` right-click | the browser's own menu, untouched |\n| click the `+` on it | a new task below, at the same level — what `o` does |\n| click `[ ]` | complete / reopen |\n| click `▾` | fold a summary |\n| double-click | edit the title |\n| click the owner cell | pick who it belongs to (`gd` shows the column) |\n| drag a row | reorder — a line shows the slot, indented to the level it lands at |\n| drag a bar | move its start date |\n| drag its right edge | change the duration |\n| drag the dot past its end onto another bar | make that task wait for this one |\n| click a date cell | pick that date off a calendar (`gd` shows the columns) |\n| `◀` `▶` beside the date | move the reference date a day |\n| click the date | jump to one, or back to now |\n| drag the divider | give the list more of the width, or less |\n\nThat table used to have a hole in it, and the hole was the shape of a\nrule this project keeps in the other direction: *nothing should be\nreachable only by clicking*. Deleting a row was reachable only by\ntyping. So were its priority, its progress, its nesting, its note — and\n`doing`, which is the one a status meeting is actually about, since\n`[ ]` only ever toggles the two ends of `todo → doing → done`.\n\nRight-click fills it. What is on the menu is decided by a rule rather\nthan by taste — **an item is there exactly when the mouse cannot already\nreach it, and every item names the key it runs**:\n\n```text\n⚑  mark it doing                       s\n◆  priority                    ▲ gp ▼ gP\n%  progress                     + ) − (\n✎  note…                           :note\n⇥  nest / unnest               → >> ← <<\n⌖  focus this subtree                 zf\n✂  yank the row                       yy\n⎘  put it below                        p\n↶  undo                                u\n✖  delete the row                     dd\n```\n\nBoth halves of the rule are load-bearing. The first keeps the menu from\nbecoming fifteen items nobody reads, and shrinks it on its own: add a\ndirect gesture for something and its entry leaves, which\n`check-rowmenu.ts` asserts against the list of gestures rather than\nleaving to memory. The second makes the menu teach — you right-click to\ndelete a row and the menu tells you it was `dd` all along, so the mouse\npath advertises the keyboard one instead of competing with it.\n\nIt is also the one panel here with no keyboard opener, and for the same\nreason: `co` had to have one because it lists the names already in use,\nwhich is something no key can tell you, and this lists keys, which `?`\nalready does. Nothing is behind a click that is not also behind a\nkeystroke — that is the point, not an exception to it.\n\nDeleting from the menu asks nothing first. `dd` does not either, `u`\nbrings the row back — drawing it back, since an undo now replays the\nstroke its own ops describe: a restored task is born again, and a task\nan undone `o` takes away is cut down first — and right-click → move →\nclick is already two acts of intent; the status line says what went.\n\n`⇧` and right-click declines the event and you get the browser's menu,\nbecause there is no API that *opens* it — declining is the only offer a\npage can make. Only rows and bars take the button at all, so anywhere\nelse it was never ours.\n\nThe divider between the list and the timeline is draggable — the line you\nsee is the thing you grab, with a few pixels either side so it is\ncatchable. Double-click puts it back to the default. The width is\nremembered like the theme is, so it survives a reload, and `:split 40`\nsets it by number for anyone who would rather not aim. Grabbing it also\nleaves it holding the keyboard, so `←` and `→` — or `h` and `l` — move it\n2% at a time, and `Home` puts it back. That the mouse is what hands it the\nkeyboard is not an oversight: `tab` is spoken for by the layout cycle, so\nthere is no keying your way over to the divider without touching it first.\nNeither side can be dragged below 15%: a pane squeezed to nothing reads as\na bug, and there is no grip left to pull it back by. `tab` is how you\nactually hide one.\n\nDragging a bar pins its start: a task placed by its dependencies gets an\nexplicit date, which is what makes the gesture survive the next\nrecompute. Summary bars cannot be dragged — their dates are a\nconsequence of what is inside them.\n\nThe `+` shows on the cursor row only. On every row it is a column of plus\nsigns and the eye stops reading the tasks; and the row it sits on is the\none whose level the new task inherits, so the indent it will land at is\nthe indent you are looking at. An empty project has no row to hover, so\nthere the prompt itself — *o to open a new task* — is the button.\n\nThe owner panel is the mouse's way to a field the command line reached\nfirst, and it lists the names already in use rather than offering a bare\nbox. That list is the only thing keeping a team to one spelling, since\nthere is no roster behind the names: `tab` does that job on the `:` line,\nand clicking has no `tab`. `co` opens the same panel from the keyboard —\nnothing in yaiba should be reachable *only* by clicking.\n\n## And a phone\n\nThe same app, reached over the network and laid out for a thumb. Nothing\nis installed on the phone and nothing syncs to it: it is a browser\nlooking at the yaiba already running on your machine, so there is one\nreplica and one store, and what you type on the train is on your desk\nbefore you are.\n\n```sh\nyaiba --host 0.0.0.0        # or YAIBA_HOST=0.0.0.0\n```\n\n**There is no authentication, and this does not add any.** Everything at\nthat address can read and write the plan, so the network has to be the\nthing you trust. A mesh VPN is the cheap way to get that and to keep\nworking away from home — with [Tailscale](https://tailscale.com/), `yaiba\n--host 0.0.0.0` plus the tailnet address is the whole recipe:\n\n```sh\ntailscale ip -4             # 100.x.y.z\n# then http://100.x.y.z:8188 from the phone, with Tailscale on there too\n```\n\nOn a LAN with no VPN it works the same way and stops at the front door.\nDo not put it on the open internet: put a reverse proxy that authenticates\nin front of it, or do not do it.\n\n### Windows: the prompt you already said no to\n\nBinding the address is not the same as being reachable. yaiba's sync\nendpoint raises a firewall dialog on startup — the one wanting an\nadministrator — and answering it with anything but *allow* writes\n**block** rules for that executable, inbound TCP *and* UDP, on the\nprivate and public profiles. `--host 0.0.0.0` then binds perfectly and\nthe phone still gets nothing.\n\nBlock beats allow in Windows Firewall, so the old rules have to go before\na new one means anything. In an **administrator** PowerShell:\n\n```powershell\n# What is there now — Action: Block on your yaiba is the thing to remove.\nGet-NetFirewallRule -Direction Inbound |\n  Where-Object DisplayName -like '*yaiba*' |\n  Select-Object DisplayName, Action, Enabled\n\nGet-NetFirewallRule -Direction Inbound |\n  Where-Object { $_.DisplayName -like '*yaiba*' -and $_.Action -eq 'Block' } |\n  Remove-NetFirewallRule\n\n# Then let the tailnet in, and only the tailnet: 100.64.0.0/10 is the\n# CGNAT range every Tailscale address lives in, so this opens nothing on\n# the LAN or a cafe's wifi.\nNew-NetFirewallRule -DisplayName 'yaiba UI (tailnet)' -Direction Inbound `\n  -Action Allow -Protocol TCP -LocalPort 8188 `\n  -RemoteAddress 100.64.0.0/10\n```\n\n`--relay-only` is worth pairing with this if the dialog is what you were\navoiding: it syncs through relays and binds no UDP socket at all, so\nnothing asks again.\n\n### What changes on a small screen\n\nBelow 720px the layout is one pane at a time — at 390px the split's list\nwould be 180px, which is a column of ellipses — and `tab`, on the bar or\nthe keyboard, swaps between the list and the chart. The stored `:split`\nis left alone, so the desktop is unchanged when you come back to it.\n\nEverything a key does is still done by a key. What the phone gets is a\nroute to the keys it cannot type:\n\n- **The bar along the bottom** is eight global commands — new task,\n  list/chart, zoom out, zoom in, dates, the `:` line, undo, today. Each\n  button *is* its key: it hands the same string to the same command layer\n  the keyboard uses, so nothing on it can drift from what the keyboard\n  does. It stands down while you are typing, because the keys it would\n  run are not the keys that mode is listening for.\n- **A long press on a row** opens the row menu — the same menu the right\n  button opens, which is where everything that acts on *one row* lives:\n  status, priority, progress, a note, nesting, focus, yank, put, undo,\n  delete.\n- **The handle at the left of a row** drags it to a new place. A press\n  that moves anywhere else is a scroll, which is why the handle exists at\n  all.\n- **Bars, edges and dependency arrows** keep their drags, with hit areas\n  a finger can actually land on. The date columns (`gd`) scroll sideways\n  inside the pane rather than pushing the app off the screen.\n\nWhat a phone does not get is the keyboard's speed, and nothing here tries\nto fake it: this is for looking at the plan, ticking things off, and\nfixing a date — the work you do standing up.\n\n## Projects, and the level you look at them from\n\nThere is no separate \"project\" object: a task with no parent *is* a\nproject. That means the same fold commands that collapse a sub-task\ncollapse a whole project, and one gantt spans all of them.\n\n```\nzM              every project on one screen, each showing its own roll-up\nzr              open one level — the phases inside each project\nzf              zoom into just this subtree\nzF              back out to the folds you had before it\n:level 2        jump straight to an altitude\n```\n\nA task with children becomes a **summary**: its bar spans its children's\ndates and its percentage is their duration-weighted roll-up, so a\n9-day task at 100% and a 1-day task at 0% reads 90%, not 50%. You never\ntype a summary's dates — they are a consequence of the work inside it.\n\nDependencies and nesting are separate axes on purpose: a parent\n*contains* its children, a dependency *orders* two tasks. Only leaves\nare scheduled from dependencies; summaries follow.\n\nYou can still link a summary, and it means what it looks like: an edge\n*onto* one holds back everything inside it, and an edge *out of* one waits\nfor the whole bracket to close — the last child, not the first. Under the\nhood the edge is rewritten to run between the leaves, since those are the\nrows that carry dates. The one pairing that is refused is a summary and\nsomething inside it, which would ask a task to finish before itself.\n\nAn edge carries how long after its predecessor the successor may start,\nand `:dep ⟨row⟩ +⟨days⟩` is where you say it:\n\n```text\n:dep 3        row 3 finishes, this one starts the next day — the default\n:dep 3 +0     row 3 finishes, this one may start the same day\n:dep 3 +5     five days after, for parts arriving or paint drying\n```\n\n`+0` is the one worth knowing about. Two half-day jobs done in one\nsitting are a real shape, and until an edge could say so the second one\nwas always pushed to tomorrow. Re-running `:dep` on an edge that\nalready exists is how its spacing changes; there is no need to unlink\nfirst.\n\nYou rarely have to, though. A pinned start is a floor to the scheduler,\nand a pin dropped inside an edge's lag — by dragging the bar onto its\npredecessor's day, by `:start`, from the calendar, by a pasted cell, or\nwith `.` / `,` — adjusts the lag to the spacing the date implies, in\nthe same commit, and says so on the status line. A pin before the\npredecessor's finish would invert the edge, and is refused with the\ndate it does finish.\n\nThe default stays at one day, so no existing plan moves. A negative lag\nis refused rather than clamped quietly: it would mean the two *overlap*,\nwhich \"A finishes before B starts\" cannot carry — that would want a\ndifferent kind of edge, not a smaller number.\n\n`blocked` still means \"a predecessor is not done\", not \"not today\". A\nsame-day successor is drawn alongside its predecessor and is still\nblocked, because the edge is about order rather than dates — a task\nwaiting on unfinished work should not stop saying so just because the\nscheduler placed it conveniently.\n\nCommands take dates the way you'd say them: `:due tom`, `:due mon`,\n`:due +3d`, `:due 8/14`. Filters compose: `:f tag:dev open crit`, and\n`:f late` is the one that answers \"what has actually slipped\" — every\nrow whose finish has gone by with the box still unchecked, due date or\nno due date.\n\n`tab` completes on the `:` line, wildmenu and all: the command name\nfirst, then that command's own vocabulary — `:sort` its keys, `:tag` the\ntags already in use, `:assign` the people already on something, `:due`\nthe words above. `shift-tab` walks back, and either one past the end of\nthe list returns what you had typed.\n\n## Who owns what\n\nOne person per task. In the working view it rides the row as `@name`,\nnext to the tags; switch to the date columns with `gd` and it becomes an\n`owner` column instead — fixed width, so a roster reads straight down the\npage. The chip keeps \"whose is this\" answerable without a mode change;\nthe column is what a progress meeting scans. Never both at once, which\nwould be the same name twice on one row.\n\n```\n:assign yuki      hand it over — the visual block, if one is selected\n:assign           and bare takes it back (`none` / `-` read the same)\n:f @yuki          just theirs\n:f unassigned     the rows nobody has picked up\n:sort owner       one person's work in a block, unowned last\n```\n\nThere is no user table behind the name and there deliberately isn't\none: inventing a roster would mean every replica agreeing on it before\nanybody could be assigned anything. So the people who exist are exactly\nthe people already on something, which is what `tab` completes from —\nand completing rather than retyping is the only thing keeping `Yuki` and\n`yuki` from becoming two names in a report nobody notices are one\nperson. Matching ignores case regardless; the spelling you typed is the\none that is stored, because with no registry it is the only record of\nhow somebody writes their own name.\n\nUnlike tags, the owner is a single last-writer-wins field: two peers\nnaming different people converge on the later write rather than both\nsticking, and every replica picks the same one. The loser's name is\ngone, not merged — which is the point. A task never ends up with two\nowners, and it only ends up with none if clearing it is what won.\n\nA name is one word, the way a tag is: the filter grammar is\nspace-separated, so `mary jane` would be two terms and match neither.\n`:assign` says so rather than storing something you cannot then find.\n\n## Plan vs actual, and the progress line\n\nEvery task carries both sides. The plan is `start` + `duration`; the\nactuals are stamped as work happens — `actual_start` the first time a\ntask leaves `todo`, `actual_end` when it is done. Reopening a task\nclears its finish, because leaving it would quietly corrupt every\ncomparison downstream.\n\n```sh\n:start mon      # the plan: where it begins…\n:dur 5          # …and how long it takes\n:end 8/20       # or say where it lands, and the duration follows\n:astart 8/18    # the actuals, when you are recording after the fact\n:aend 8/22      # `none` on either one clears it\n```\n\nThe two sides are shaped differently on purpose. A plan is `start` +\n`duration` because the scheduler *moves* it: when a dependency slips,\nwhat stays true is how long the work takes, not the day it happens to\nfinish. So there is no stored end date — `:end` is sugar that measures\nback from the date you name and writes the duration. An actual span is\nthe opposite: both ends are recorded, because nothing recomputes what\nalready happened, and the days between them are not \"how long it took\"\nanyway.\n\nThe gantt draws each recorded span as a thin rail under the bar, so the\noffset between plan and actual reads without a legend — flush left\nstarted on time, a rail past the bar's right edge ran long. Work still\nin progress fades out at the reference date rather than claiming a\nfinish it doesn't have.\n\nWhen the four dates are the point — a progress meeting, a status sheet\nsomeone else fills in — the list will show them as columns instead of\nits markers:\n\n```text\ngd              date columns <-> compact\n:dates          the same toggle by name · :cols compact\n```\n\n| | |\n|---|---|\n| `start` `end` | the plan · dim means the scheduler placed it, not you |\n| `began` `ended` | the record, quieter, behind a rule |\n\nA task you have not pinned starts **today** — or at the reference date,\nwhen `:asof` has moved it — and then as late as its predecessors force.\nThe floor is a default, not a clamp: `:start` on a past date is a\nstatement and is kept. It applies to a task with predecessors too, so a\nchain whose earlier links finished last month does not lay the rest of\nitself out behind you. What actually happened belongs in `began` /\n`ended`; the plan is for work still ahead.\n\n> Upgrading to 0.12 moves rows, once. Before it, an unpinned task sat at\n> the *oldest* date anywhere in the plan, so on a project with history\n> every one of them will jump forward on first load. Nothing is written\n> and no `start` appears — they were never pinned and still are not; the\n> scheduler has simply stopped answering \"when can this be done\" with\n> \"whenever the project began\".\n\nClick a cell and a calendar opens over it — `hjkl` or the arrows walk\nthe grid, `[` `]` page months, `t` jumps to the reference date, `x`\nclears, `⏎` commits. The keyboard opens the same panel on the cursor\nrow, without reaching for the mouse or turning the columns on:\n\n```text\ncs              calendar on the planned start · ce on the end\nca              calendar on the actual start · cA on the actual end\n```\n\n`c` is change, the way `cc` changes the title. Every pick runs the\ncommand you could have typed, so it obeys the same rules: picking an\n`end` writes a duration, an actual span is refused if it would run\nbackwards, and a summary's plan is not editable at all — those cells are\nplain text rather than buttons, and `cs` on one says so.\n\nThose five keys each name one field, which is what you want when you are\nsomewhere else in the plan and just need to fix a date. Filling a column\nin is the other job, and for that the cursor walks:\n\n```text\nh l             out / in — the cell to the left, the cell to the right\nj k             the next row, still in this column\n⏎               edit the cell you are standing in\n```\n\n`h` and `l` mean one step out and one step in, and a cell is a smaller\nstep than a subtree — so at the leftmost cell, where there is no cell\nfurther out, they are the fold's again. That is the whole rule. In the\ncompact view there is one column, so they only ever fold, which is what\nthey did before the columns existed.\n\nStanding still in a column while `j` walks down the page is the point:\n`⏎`, pick, `j`, `⏎`, pick — a roster or a set of planned starts, filled\nstraight down without the mouse and without retyping a command per row.\n\nOr copy what is already there. `v` selects a rectangle of cells rather\nthan a run of rows, so the shape you take is the shape you put down:\n\n```text\nv j j y         one column, three rows\nv l j y         two columns, two rows\np               put it down with the cursor on its top-left corner\n```\n\nIt lands by offset, not by column name — that is what makes yanking\n`start` `end` and dropping it on `began` `ended` the two keystrokes it\nshould be, which is the comparison the columns exist for. A column whose\nkind disagrees does not land: a date will not overwrite a title, and the\nstatus line says which cells it skipped rather than half-writing the\nblock and looking like it wrote all of it. Running off the last column\nor the last row reads the same way.\n\nEvery cell is written by running the command you could have typed, so\nthe rules do not move: `end` is a duration measured back from the start,\nso pasting one onto a row that starts elsewhere changes its length; an\nactual span that would finish before it began is refused; a summary's\nplan cells are refused. And a `start` the scheduler placed for you looks\nthe same as one you pinned — copying it pins it on the row it lands on.\n\n`V` takes whole rows instead, which is the selection `v` used to be.\nRows and cells are two registers and two kinds of put: `yy` / `Y` yank\nrows and `p` makes copies of them, `y` yanks cells and `p` writes them\nover what is there. `p` puts down whichever you filled last.\n\n`P` is the exception: it only ever puts rows. A row block makes room\nabove the cursor, which is what \"before\" means for a row; a cell block\noverwrites the cells you point it at, and there is no above to land on.\n\nThe gantt draws a **progress line** (イナズマ線) from the reference date:\neach row steps left or right by how far it deviates from where the plan\nsays it should be. Straight means on schedule, a notch left is behind, a\nbulge right is ahead. You read the project's health from the shape, not\nfrom a column of numbers.\n\n```sh\n:asof 2026-07-20    # the plan as it stood that day\n:asof -3d           # three days ago\n:asof today         # back to now\n```\n\nThe reference date sits in the top bar at all times — it is the number\nevery bar, percentage and overdue flag is measured from, so leaving it\nunstated made the view depend on something it never showed. `◀` and `▶`\nwalk it a day at a time, which is how you watch the progress line bend;\nclicking the date opens a jump list and a date field. It reads as grey\nchrome while you are at now and turns amber the moment you are not,\nbecause moving off now is a mode, and one that refuses edits.\n\nForward is as available as back: a date ahead of now answers \"if nothing\nmoves, how far behind is this by Friday\". What a past date will *not*\nshow you is a task that did not exist yet — the snapshot drops anything\ncreated after the date, so a task entered today is absent from\nyesterday's view even when its planned dates run back a fortnight. The\nfilter is on when the task was written down, not on where its bar sits.\n\nProgress and status are recorded per day, so a past reference date shows\nwhat was known then rather than today's numbers back-projected. The\nrecording is one entry per task per day: several edits on one day\ncollapse to that day's final value, and a day you didn't touch a task\ncosts nothing. A year of active use is a few megabytes.\n\nFields that keep no history — titles, dates, the breakdown — are shown\nas they are now. That is a real limitation rather than a papered-over\none: the CRDT keeps only the latest value for those, and inventing a\npast for them would be worse than admitting there isn't one.\n\n## Working days, and days off\n\nA duration is counted in *something*. By default that something is\ncalendar days: `:dur 5` is five squares on the chart, weekend included.\nSwitch the project over and the same 5 means five days somebody is\nactually at work:\n\n```text\n:cal            what this project counts in, and what it counts as off\n:cal on         count durations in working days · :cal off back to calendar days\n:cal week mon-fri     which weekdays are worked · mon-sat · or a mask: 1111100\n```\n\n**It is off until you ask.** Turning it on moves every finish date later,\nwhich is not something an upgrade is allowed to do to a plan you already\ntrust — so a project that has never heard of this schedules exactly as it\ndid before.\n\nOnce it is on, everything that counts days counts the same way: a\ndependency's lag of one day means *the next working day*, so a task\nfinishing on Friday hands over on Monday, and a slack of `3d` is three\ndays somebody could have worked rather than three squares two of which\nare a weekend. A pinned start stays a floor rather than becoming a\npromise: pin a task to a Sunday and it begins on the Monday, while the\npin you typed is left exactly where you typed it.\n\n### Days off\n\nAny date can be named, and any date can be taken back:\n\n```text\n:cal holiday 12/29 年末休み    # a day the office is closed (the name is optional)\n:cal workday 8/15             # a Saturday everybody is in\n:cal clear 12/29              # no opinion either way — back to the week mask\n```\n\nThat is the whole mechanism, and it is deliberately not tied to any\ncountry: mark the dates you don't work, from wherever you get them. A\ncalendar usually *arrives* as a file rather than as something you type, so\nthe same verbs exist on the command line and take one:\n\n```sh\nyaiba cal                              # what this project counts in\nyaiba cal on                           # ...and switch it over\nyaiba cal week mon-sat\nyaiba cal region jp\nyaiba cal holiday 2026-12-29 年末休み   # one day\nyaiba cal holiday --file days.csv      # or a year of them, in one request\nyaiba cal workday --file open.csv      # the days you are in anyway\nyaiba cal clear   --file days.csv      # and the way back out\n```\n\nThe file is one `date[,name]` per line — `#` starts a comment, a tab works\nas well as a comma, and a name may contain commas because the split is on\nthe first separator only. `--file -` reads stdin, so anything that can\nproduce a list of dates can feed it:\n\n```sh\ncurl -s https://example.test/holidays.csv | yaiba cal holiday --file -\n```\n\nA line that names no date is dropped, and a date named twice keeps the\nlast name it was given — the format is forgiving on purpose. It is not\nquiet about it, though, because a file that lost two lines must not print\nwhat a clean one prints:\n\n```text\n3 days marked off. (2 lines skipped, 1 date repeated)\n```\n\n**One request, so it either all lands or none of it does.** The server\nvalidates every line before writing any of them; a typo on line 40 leaves\nthe calendar exactly as it was and says which date it choked on. Fifty\nseparate requests would leave it halfway.\n\nLike `yaiba mcp` and `yaiba gcal`, this talks to a yaiba that is **already\nrunning** rather than starting one of its own — the server stays the only\nwriter.\n\nWhat ships built in is one table, because the dates it holds move around\nby law every year and typing them in annually would be miserable:\n\n```text\n:cal region jp     # 国民の祝日, with 振替休日 and 国民の休日 derived\n:cal region none   # nothing built in (the default)\n```\n\nIt is a *region*, not a Japan switch — adding another one is a variant in\nan enum and a function beside it, and nothing stored or on the wire\nchanges. Two bounds worth knowing, both of them stated in the code as\nwell: the equinox dates are the standard approximation and hold for\n1980–2099, and because the table lives in the binary rather than in the\ndata, two replicas on different versions can resolve the same project to\ndifferent dates until both are upgraded.\n\nThe chart shades every non-working day, holidays included, and names them\nin the header where they have a name. `:cal` on its own only reports —\nit never writes, so an early `⏎` cannot re-plan the project.\n\nThe calendar belongs to the project and **syncs with your peers**, in the\nsame CRDT log as the tasks. It has to: the schedule is computed by each\nreplica from the same tasks and dependencies, so a calendar that stayed\nlocal would have two people looking at the same plan and reading\ndifferent dates off it.\n\n## Working with peers\n\nEach replica prints a ticket at startup, and `:ticket` copies it from\ninside the app. Hand it to someone:\n\n```sh\n# them\nyaiba join <ticket>\n```\n\nThat's the whole setup. From then on both sides sync automatically —\nimmediately on every edit, and on a timer to catch up after being\noffline.\n\nTheir tasks arrive as a **project of their own** — a separate database,\nlisted under a name you can pick with `--as` — so joining someone never\nmixes their backlog into yours. See [Projects](#projects).\n\n**No port forwarding, no inbound firewall rule.** iroh dials by public\nkey and hole-punches, so outbound UDP is all that's needed. If hole\npunching fails the connection falls back to a relay that forwards\nalready-encrypted QUIC and cannot read what passes through. Peers on the\nsame network connect directly and never touch it.\n\nHole punching does bind a UDP socket on every interface, and Windows\ngreets that with a firewall prompt — which on a locked-down machine asks\nfor an administrator who isn't you, and comes back on every start.\n`--relay-only` binds no UDP socket at all:\n\n```sh\nyaiba --relay-only            # sync over the relay, no UDP socket of our own\nYAIBA_RELAY_ONLY=1 yaiba      # same, permanently, from the environment\n```\n\nThe UI's listener is unaffected — it is on loopback, which no firewall\nasks about, and that is true in either mode. Everything still syncs and\nthe ticket is unchanged: a peer dials your public key and never learns\nwhich path answered. What you give up is the direct connection, so it is\nslower and it needs the relay to be reachable.\n\nThe ticket carries a 32-byte room key. A peer that can't present it is\ndropped before any data moves.\n\n## Projects\n\nA `yaiba` database is one task set, one sync room, one identity — nothing\nin the replication layer scopes tasks to a project *inside* a database. So\na project **is** a database file, and `yaiba` keeps an index of them.\n\n```sh\nyaiba                          # the default project\nyaiba new private              # start one of your own, and open it\nyaiba list                     # what's registered, most recent first\nyaiba open work                # open one by name\nyaiba open                     # fuzzy-pick one\nyaiba join <ticket> --as work  # take a peer's tasks as a new project\nyaiba merge <ticket>           # fuse two replicas into one plan (no undo)\nyaiba forget work              # drop the name; the database stays on disk\n```\n\nKeeping apart what should be apart is what `new` is for — a backlog you\nshare with someone, and one you don't:\n\n```sh\nyaiba new shared               # hand this one's ticket out\nyaiba new private              # never share this one, and nobody can reach it\n```\n\nNothing leaves a project until you hand out *its* ticket. Each has its own\ndatabase, its own sync room and its own identity, so sharing one says\nnothing about the others.\n\nWhatever you open is registered as you go, `--db` paths included, so the\npicker fills itself in without any setup step. Your default database is\nadopted the moment it exists, so `yaiba list` and the picker already know\nabout it before you have opened anything. The index is a TOML file meant\nto survive being hand-edited:\n\n| path | |\n| --- | --- |\n| `<data dir>/yaiba/projects.toml` | the index |\n| `<data dir>/yaiba/yaiba.db` | the default project |\n| `<data dir>/yaiba/projects/<name>.db` | projects you joined |\n\n`YAIBA_DATA_DIR` moves that whole root — one variable for a\nself-contained yaiba on a synced folder or a stick.\n\nLosing the index costs names and ordering, never tasks: every database\nstill opens with `yaiba --db <path>`, registered or not.\n\n**One yaiba holds them all.** Starting it opens every registered project,\nnot only the one you asked for, and each one replicates on its own —\nso a project is up to date when you turn to it, rather than starting to\ncatch up at that moment. There is no second process and no second port.\n\nManage them from inside the app, without restarting. **`:proj`** opens a\nfuzzy picker over what is open — and so does clicking the project name in\nthe top bar, which is the way in for a mouse.\n\n| in the picker | |\n| --- | --- |\n| `^n` / `^p` | move |\n| enter | switch |\n| `^r` | rename the row under the cursor |\n| `^d` | forget it — asks first |\n| type a name nothing matches | offers to create it |\n\n`rename` and `forget` are buttons on that row too. From the command line:\n`:proj <name>` switches, and `:proj new <name>`, `:proj rename <name>`\n(renames the one you are on) and `:proj forget <name>` do the rest, with\n`<tab>` completing names.\n\n**Forget only drops it from the list** — the database stays where it is,\nwhich is what the picker says before it does anything.\n\nA registry entry whose database has gone is skipped with a warning rather\nthan stopping the launch. If opening everything costs more than it is\nworth — a long registry, a slow disk, a metered link — `--only-active`\nopens just the one project.\n\n### `join` keeps them apart, `merge` mixes them together\n\nTwo things you can do with somebody's ticket, and they are opposites:\n\n```sh\nyaiba join <ticket> --as work   # their tasks, as a project of your own\nyaiba merge <ticket>            # both task sets, in both replicas\n```\n\n**`join` is the one you almost always want.** The peer's tasks land in a\ndatabase of their own, so nothing you already have is changed and nothing\nof yours is shared with them. `:join <ticket>` in the UI does the same,\nwithout a restart.\n\n**`merge` fuses two replicas into one plan, and it cannot be undone.**\nBoth task sets end up on both sides, and this project leaves its own sync\nroom for theirs — the old room key is overwritten, so anyone holding your\nprevious ticket is dropped on their next sync. It merges whichever\nproject would open by default; `--project <name>` picks another.\n`:merge <ticket>` in the UI does the same.\n\nBoth were spelled `join` until v0.21 — the `--join` *flag* merged, the\n`join` *subcommand* did not — and since only one of the two can be taken\nback, guessing wrong cost people the separation they had set up. The flag\nnow refuses and names both replacements.\n\n### Sharing a view as a link\n\nThe address bar is always a link to what is on screen — view, zoom,\ncolumns, sort, filter, folds, focus, the `:asof` date, the cursor row and\nthe show-hidden toggle (`#view=split&sort=due&fold=…`). Theme, colours,\nlanguage and the split width are yours and are not in it. It is the URL\n*fragment*, so it never reaches the server; opening one overrides your\nsaved view for that session without rewriting it, until you change\nsomething yourself. Task ids are the same on every replica, so ids the\nrecipient does not have are ignored.\n\n- `:url` (or `gL`) copies the full URL **with this project's ticket**, so\n  the recipient lands on the project — switched to if they already have it\n  (matched by the ticket's room key, not by name), or offered as a join\n  behind a confirmation. `:url view` leaves the ticket out, for someone who\n  already has the project. (`:link` and `:share` were already taken, by\n  dependencies and by `:ticket`.)\n- `:open <link>` (alias `:goto`) takes a pasted URL or a bare `#…` fragment\n  and applies only the fragment to the page you are on — for a recipient\n  on another host or port, since a copied URL carries the sender's origin.\n- **A link with a ticket is the ticket.** Whoever holds it can read and\n  write that project. The fragment is not sent to the server, but it does\n  land in browser history and wherever the link is pasted. The address bar\n  itself never contains the ticket; only `:url` does, when you ask.\n- The fold list is the only part that grows: past 2000 characters it is\n  left out of the link (and `:url` says so) — roughly fifty folded rows.\n\n### `leave` is the way back out\n\n```sh\nyaiba leave [--project <name>]   # forget its peers, move to a room of your own\n```\n\n`:leave` in the UI does the same, and asks first.\n\nIt works after either verb. The project forgets its peers and moves to a\nnew sync room: it stops dialling them, and anyone still holding the old\nticket can reach the endpoint but is turned away, because the room no\nlonger matches. The tasks stay exactly as they are — **including the ones\nthat arrived from the group**, which are yours to keep or to delete now\nthat you are out. Delete them *after* leaving; before, the deletions\nreplicate.\n\nTwo things it does not do:\n\n- **It cannot take back what already reached them.** Their replica is as\n  complete as yours, and nothing here reaches across to remove it.\n  Leaving stops the future, not the past.\n- **It changes this project's ticket**, because a ticket is the endpoint\n  and the room together. So every replica holding the old one is cut off\n  — your own other machines included. Share the new `:ticket` to pair up\n  again.\n\n## Letting an agent read and edit the plan\n\n`yaiba mcp` serves the plan over [MCP](https://modelcontextprotocol.io),\nso a coding agent can answer \"what's blocking the release?\" and break a\ntask down without you retyping any of it. It talks to a yaiba that is\n**already running**, so leave one up and register the server once:\n\n```sh\nyaiba                              # in one window, and leave it\nclaude mcp add yaiba -- yaiba mcp  # once\n```\n\nPoint it elsewhere with `yaiba mcp --url http://127.0.0.1:9000`, or\n`YAIBA_MCP_URL`. `http://` is what you want — this dials your own\nmachine, which has no certificate to present.\n\nEight tools: `plan` reads everything (computed dates, critical path,\nwhat's blocked, what's overdue); `add_task`, `update_task`,\n`delete_task`, `link` and `unlink` edit it; `projects` and\n`switch_project` move between plans. Tasks are named by title or by the\nshort id `plan` prints — an ambiguous name is an error listing the\ncandidates rather than a guess.\n\n**The agent is a client like any other.** It goes through the same HTTP\nAPI the UI does, so every rule holds without being restated: a\ndependency that would close a loop is refused, a summary's dates still\ncome from its children, and a pinned start is still a floor. Every write\nanswers with where the plan then stands, which is usually the point —\ncutting one dependency can move the finish date a week.\n\nThere is no authentication on the API, and the MCP server is a local\nprocess talking to `127.0.0.1`. Keep it that way.\n\n## Putting the plan on a Google Calendar\n\n`yaiba gcal push` writes the schedule to a calendar of its own, so the\nweek you planned sits beside the meetings you actually have — on your\nphone, in whatever your team already looks at.\n\nIt runs when you ask it to. Most tasks have no pinned start, so their\ndates come out of the dependency graph and one new edge moves many bars\nat once; pushing continuously would be a calendar rewriting itself while\nyou think. On demand it is one reconcile: work out the difference, apply\nit, stop. Running it twice does nothing the second time.\n\nAsk for it either way: `:gcal push` on the `:` line, or `yaiba gcal push`\nin a terminal. Both are one `POST /api/gcal/push` to the server holding\nthe plan, so they do the same reconcile and report the same counts —\n`:gcal push` puts them in the status line, and when Google refused an\nevent it names the refusal there rather than counting it as done.\n\n### Setting it up\n\nyaiba has no Google account of its own, so you lend it one. Once, about\nfive minutes, in the [Cloud console](https://console.cloud.google.com/):\n\n1. **New project.** Any name.\n2. **Enable the Calendar API** — *APIs & Services* → *Library* → *Google\n   Calendar API* → *Enable*.\n3. **Configure the consent screen** — *External*. App name, your email\n   for support and for developer contact. Add the scope\n   `https://www.googleapis.com/auth/calendar`.\n4. **Publish it.** On the consent screen, set the publishing status to\n   *In production*. **Do not skip this** — a project left in *Testing*\n   is issued refresh tokens that expire after seven days, and you will\n   be logging in again every week without being told why.\n5. **Create credentials** — *Credentials* → *Create credentials* →\n   *OAuth client ID* → **Desktop app**. Keep the client id and secret.\n\nThen tell yaiba, and log in:\n\n```sh\nexport YAIBA_GCAL_CLIENT_ID=....apps.googleusercontent.com\nexport YAIBA_GCAL_CLIENT_SECRET=GOCSPX-...\n\nyaiba gcal login      # opens your browser; grant access. Once per machine.\n\nyaiba                 # in one window, and leave it\nyaiba gcal push\n```\n\nGoogle will say the app is unverified. It is — it is a client you made\nfor yourself five minutes ago, and nobody reviews those. *Advanced* →\n*Go to … (unsafe)*.\n\nThe URL is printed as well as opened, so this works over SSH or on a\nmachine with no browser; `--no-open` skips the launch and leaves only the\nprint.\n\n**Both** processes read those variables: `yaiba gcal login` in the\ncommand you type, and `yaiba gcal push` in the server that holds your\nplan. So set them somewhere both inherit — a shell profile rather than\nthe one window you happened to be in.\n\n`login` needs no running yaiba — the credential is yours, not a\nproject's, so it writes straight to your credentials file. `push` does\nneed one: it is reading a plan. That asymmetry is why the `:` line has\n`:gcal push` and no `:gcal login`: the consent screen wants a browser\nand a listener on *this* machine, which is the terminal's job rather\nthan the server's. Typing `:gcal login` says so instead of failing.\n\n### What lands on the calendar\n\n- **A calendar of its own**, called `yaiba: <project>`. Nothing is\n  written to your primary one, and getting the plan back off your\n  calendar is deleting that calendar rather than trusting yaiba to tidy\n  up after itself.\n- **All-day events**, one per task, spanning the dates the gantt draws.\n- **Leaves only.** A summary's span is the union of its children's, so\n  an event for it would book the same days twice.\n- **Events yaiba did not create are never touched.** Every event it\n  writes carries a private `yaibaTask` stamp, and anything without one\n  is somebody else's — including anything you add to that calendar\n  yourself.\n- **Removed tasks take their events with them.** A task deleted, or one\n  whose event you dragged somewhere it does not belong, is put back the\n  way the plan says on the next push.\n- **Renaming the project renames the calendar** on the next push — but\n  only while the calendar still carries the name yaiba gave it. Rename\n  it yourself on the Google side and it keeps the name you chose.\n\n`yaiba gcal push` acts on the **active project** — the one `:proj` is\nlooking at — and each project gets its own calendar. You log in once per\nmachine, not once per project: the credential is yours, so it lives in\n`credentials.toml` beside the project registry in your data directory,\nwhile the calendar each project writes to is the project's own business\nand stays with it.\n\nNothing is read back. Google's events carry no dependencies and no\nbreakdown, and a start dragged in a calendar would fight the forward\npass that computed it.\n\n## How it works\n\n### Edits merge instead of colliding\n\nThe data model is a CRDT — a last-writer-wins map keyed by\n`(task, field)`, stamped with a [hybrid logical\nclock](https://cse.buffalo.edu/tech-reports/2014-04.pdf). Granularity at\nthe *field* level is what makes concurrent editing feel unremarkable:\nif you set a due date while someone else raises the priority of the same\ntask, both survive. Tags are one entry per tag rather than one array, so\n`+dev` and `+ui` added at the same moment both stick — where the\nassignee is a single entry precisely so two people naming different\nowners *don't* both stick. Deletes leave a\ntombstone, so a peer that hasn't heard about them can't resurrect the\ntask on its next sync.\n\nThe HLC's node id breaks ties, which means two replicas that receive the\nsame pair of concurrent writes in opposite orders still land on the same\nanswer.\n\nSync is a version-vector exchange: each side says what it has seen, and\nonly the difference crosses the wire.\n\n### The schedule is computed, never stored\n\nBar positions come from a forward pass over the dependency DAG (earliest\nstart, honouring any pinned start date) and a backward pass (latest\nstart, hence slack). Zero slack means critical path — drawn in magenta.\nBlocked, overdue, late and level are derived the same way, so they can't\ndrift from the graph they describe.\n\nThe last two answer different questions and a plan usually has more of\nthe second. **Overdue** is the plan overrunning a date somebody typed:\nit needs a due date, and it compares two *planned* dates. **Late** is\nthe ordinary case — the computed finish has gone by and the box is\nstill unchecked — so it needs no due date at all, it is measured\nagainst the reference date and moves with `:asof`, and a summary is\nlate whenever anything inside it is, which is what keeps a folded\nbranch from hiding it. Both draw amber, on the row's title and on its\nbar; `:f late` filters to them and the HUD counts them beside `crit`.\n\nCycles are rejected when you create them, but the renderer still\ntolerates one: two peers can independently add edges that only close a\nloop once merged, so a cyclic graph is a state the UI has to survive\nrather than an error it can refuse.\n\n## Development\n\n```sh\ncargo make check         # fmt + clippy + test + lock-check — the pre-push gate\ncargo make dev           # server against a scratch db\ncargo make web-dev       # Vite dev server, /api proxied to the above\ncargo make web-build     # rebuild the embedded bundle\ncargo make smoke         # release smoke: sqlite, embedded UI, iroh endpoint\n```\n\nThe SPA lives under `crates/yaiba-server/web/` — inside the crate,\nbecause `rust-embed` bakes `web/dist/` into the binary at compile time.\nA release built without `cargo make web-build` still compiles and still\nstarts; it just serves an empty shell. `cargo make smoke` exists to\ncatch exactly that.\n\n| crate | what it holds |\n|---|---|\n| `yaiba-core` | model, CRDT store, dependency scheduling |\n| `yaiba-sync` | iroh transport and the sync protocol |\n| `yaiba` (in `crates/yaiba-server/`) | HTTP API, embedded SPA, CLI |\n\nThe binary crate is named `yaiba` so that `cargo install yaiba` gives you\n`yaiba`; its directory keeps the `-server` suffix to say what it holds\nnext to `-core` and `-sync`.\n\n## License\n\nMIT — see [LICENSE](./LICENSE).\n"}