# The Gantora plan format > Cut from https://gantora.net/skill.md — the full document adds the writing rules. # Gantora plans Gantora is a single-file HTML Gantt / project planner. It keeps its plans as **plain text files** in a `DB/` folder — one file per plan, one row per task, cells separated by `|`, a header row first. There is no server, no lock and no database: **the file on disk is the whole truth**, and the app rewrites the whole file on every save. This document is the format as the app's own parser reads and writes it (checked against the parser on 2026-08-19), plus the rules that keep a plan safe when something other than the app writes it. | File | What it is | |---|---| | `DB/.gantora` (older plans: `.csv`) | one plan — header row, task rows, then a few `#`-lines | | `DB/_lists.csv` | the folder's shared registry of Teams / Systems / People — **read it, never write it**; the app maintains it | | `DB/_*` anything else | the app's own; leave alone | ## 0. Read this part twice — it is where plans actually go wrong These are not style notes. Every rule below is here because an assistant broke it on a real plan, on three separate attempts at the same one (measured 2026-08-20). None of the breakages looked like errors: the file parsed, opened, and was quietly wrong. 1. **Read this whole file before you write anything.** If you cannot read it — the fetch failed, the content came back as binary, you are guessing from the name — **stop and say so.** Ask the person for the file — or, if they do not have one yet because you are about to make their first plan, open . If neither works, say what you could not read and ask them to paste the spec; do not reconstruct the format from a sample — the parts that matter are the ones a sample does not show. *(An assistant wrote this rule for itself, in those words, after failing the other way.)* 2. 🔴 **`Milestone=yes` is a project-level, standalone, zero-day event.** It is NOT a way to mark a task as important, and a milestone is **never a member of a group**. This is the single most common mistake, and the most damaging: a milestone inside a group breaks the group's own dates and the health of everything above it. If you want to say "this one matters", say it in `Notes`. 3. **Never write dates on a group row.** A group row — one that other rows name in their `Parent` cells — is a container, and the app computes its `Start`, `End` and `Duration` from its members every time it loads. Dates you type there are overwritten — and until they are, they disagree with the plan and mislead whoever reads it. 4. **`Status` is exactly one of six words:** `Planned`, `In Progress`, `On Hold`, `Blocked`, `Done`, `Cancelled`. Not a sentence, not a percentage, not a description. Anything the person wants to *say* about a task belongs in `Notes`. `Cancelled` means it will not happen — never use it for "finished", and never delete a row to mean it. 5. **The 28 columns keep their order, and every row has all of them.** Do not skip a column because it is empty — write the empty cell. A single missing `|` shifts every value after it into the wrong field, and the file still opens. **And no trailing `|` after the last cell** — 28 cells is 27 pipes; a pipe at the row's end makes a 29th cell, and the app refuses the file as truncated rather than guess which cell is the ghost. 6. 🔴 **If you are unsure about grouping, leave it out.** A flat list of tasks with correct dependencies is a *valid, good, useful* plan. A wrong `Parent` cell — one naming a UID that is not in the file, or a chain nested past four levels — is worse than no grouping at all, and the person can group rows in the app in seconds. **When in doubt: no groups.** 7. **`Parent` is the child's cell, and it holds a UID.** Membership is written exactly where every assistant expects it: on the member, naming its parent — by the parent's `UID`, never its `Seq` (positions renumber; identity does not). The parent row's own `Parent` cell stays empty unless it too sits inside a larger group. ``` ✓ RIGHT — each child names its parent, by UID 5|Packing|…|2026-03-02|…|2026-03-06|…||no|…|uid-of-nobody-needed 6|Boxes|…|2026-03-02|…|2026-03-03|…|pk4x-…-packing|no|… 7|Labelling|…|2026-03-04|…|2026-03-05|…|pk4x-…-packing|no|… ``` *(In an old file — format 1, no `#META|format` line — membership sits the other way round: a `Group` column on the parent, listing its members' IDs. Read it if you meet it; the app upgrades such files on their next save, and everything you write is format 2.)* Two more that cost real time on the same test: - **Do not invent columns or reorder them** to be helpful. Since format 2 an unknown column does survive the round trip — no Gantora will eat it — but no reader gives it meaning either: the app cannot show it and another assistant will not understand it. What you want to say belongs in `Notes`, or in a `#META` line if it is about the whole plan. - **Read `Predecessors` before you renumber anything.** They point at `ID`s, and `ID`s are positions in time order — see §5. Renumbering without fixing them turns a schedule into a shuffle. ## 1. The file, byte by byte - **UTF-8, no BOM.** The app writes `\r\n` line ends and **always ends the file with a line break**; the reader accepts `\n` too. Write it the way the app does. - **Blank lines are ignored.** - 🔴 **Never touch a file whose name ends in `.trash`.** That is the app's wastebasket: a plan somebody deleted, kept under a name the plan list does not recognise so it can be brought back. Do not read it, do not write it, do not offer to tidy it away. If somebody asks for a deleted plan, tell them the Wastebasket is in the app's DB menu. - **Line 1 is the header** — these 28 names, in this order, separated by `|` (format 2, 2026-08-23; the older names `ID` and `Group` still *read*, but everything you write uses these): ``` Seq|Activity Name|Duration|Start|Hour (Start)|End|Hour (End)|Predecessors|Team|System|Accountable|Responsible|Parent|Milestone|Notes|Status|History|Event|Emoji|Not Finalized|Baseline End|Date History|UID|Created|Modified|History By|Reported|Labels ``` The reader maps columns **by header name**, not by position. A name it does not know is carried through open and save untouched — but carries no meaning to any reader. Do not invent columns. - **A task row is any line whose first cell is a positive integer.** A line whose first cell is not a number is skipped by every reader there has ever been — that is how the `#VIEW` / `#DEL` / `#META` lines below work. - **Truncation is refused, not repaired.** The app will not open a file whose last line is a row narrower than the header, or a row wider than it. Always write complete rows and the final line break. - **Inside a cell**, `|` and line breaks cannot appear as themselves. They are escaped — **`~p` is `|`, `~n` is a line break, `~t` is a literal `~`** — and the reader decodes every cell. Encode `~` first on the way out, or you encode your own output. (A note typed on three lines is stored as `line one~nline two~nline three`.) Do **not** replace a pipe with `/` — that was the app's behaviour until August 2026 and is now simply data loss. - Cells are trimmed on read; a missing trailing cell reads as empty. ## 2. The columns | Column | Format | Notes | |---|---|---| | `Seq` | positive integer | **A position, not a name** — which is why it is no longer called "ID" (the app shows it as `#`). Rows are kept in time order and numbered 1, 2, 3… down the file; a group takes the number directly above its members. The app **renumbers on open**, so a new row with `max+1` is fine — what matters is that `Predecessors` reference Seq values *in this same file*. For identity, use `UID`. | | `Activity Name` | text | | | `Duration` | calendar days, both ends counted (`End − Start + 1`); **`0` on a milestone and on an event** (even a period) | **Recalculated by the app on every save.** Write the arithmetic; never trust it from a hand-edited file. | | `Start`, `End` | `YYYY-MM-DD` | `End ≥ Start`. **A group's dates — and its hours — are derived:** its members' first start (with that member's start hour) and last end (with that end hour); the app recalculates them on every save. | | `Hour (Start)`, `Hour (End)` | `HH:MM` or empty | empty = any time of that day | | `Predecessors` | IDs, `;`-separated | must exist in this file; a task may not precede itself; a task may not start before a predecessor ends | | `Team`, `System` | text | free text; the registry offers suggestions, nothing enforces them | | `Accountable` | **one** `Name` or `Name:hours` | hours a day, `0 < h ≤ 24`, optional; a name without a number costs nothing | | `Responsible` | `Name[:hours]` entries, `;`-separated | same rule | | `Parent` | the parent row's **UID** | on the CHILD row — a row somebody names as their parent **is a group**. Hierarchy is by reference, not indentation, and by identity, not position: the cell survives renumbering untouched. A task belongs to at most one group; groups nest at most **4** levels (group → sub → sub² → sub³ → task). A milestone can neither be nor hold a member. | | `Milestone` | `yes` / `no` | a point in time: `End = Start`, no team/system/people/group | | `Notes` | text, max 500 chars | line breaks escaped as `~n` | | `Status` | exactly one of `Planned` · `In Progress` · `On Hold` · `Blocked` · `Done` · `Cancelled` | **empty on an event**. Not derived from dates — a task past its End is not Done until somebody says so. `Cancelled` is work that will not happen: it counts towards nothing, it can never be late, and the row is kept rather than deleted so the plan still records that somebody meant to do it. Never use it to mean "finished". | | `History` | `YYYY-MM-DD Status;…`, oldest first | **append-only.** Every status change is one entry dated the day it happened; the first entry is the day the row entered the plan. Two entries on one day are allowed. Setting the status a row already has is not a change and writes nothing. Empty on an event. | | `Event` | `yes` / `no` | an event is a milestone *flavour* the plan does not own (a holiday, a freeze): `Milestone` is `yes` on it too, it may have `End > Start` (then it is a period), it carries no status and no history | | `Emoji` | one emoji, events only | empty otherwise | | `Not Finalized` | `yes` / `no` | "these dates are still a guess". Never `yes` on a milestone or an event. | | `Baseline End` | `YYYY-MM-DD` or empty | **only when typed by hand** to overrule the recorded baseline; empty means "work it out from Date History" | | `Date History` | `YYYY-MM-DD YYYY-MM-DD;…` — *the day it was written*, then *the End date that day* | **Written by the app, never by you.** It opens the first time a task's End moves *after the task has left Planned* (first entry = the End it had, and that entry is never rewritten); then one entry per day, last value wins. Empty on an event. | | `UID` | `[A-Za-z0-9_-]{4,40}` | the row's **permanent name** — minted once, never changed, shown nowhere, what a merge matches rows by. See §4 for minting one. | | `Created` | `YYYY-MM-DDTHH:MM:SSZ ` | UTC to the second, then a short writer id, then a display name (optional). Written once. | | `Modified` | same shape | the last change that actually changed something. A renumber is not a change. | | `History By` | `YYYY-MM-DD ;…` | one entry per `History` entry, **same order, same dates**; `-` for an unknown name. If the dates fall out of step with History, the app drops all the names and keeps the history. | | `Reported` | `yes` / `no` | whether the management report speaks about this row. **A main task (top level, not in any group, not a milestone) is always `yes`** — the app forces it. | | `Labels` | words, `;`-separated | free tags; two labels differing only in case are one label | **Reading the 'yes/no' columns:** the parser accepts anything starting with `y`, `i`, `1` or `t` as yes. Always *write* `yes` / `no`. ## 3. The `#`-lines at the end of the file After the last task row, in this order. **Preserve every one of them verbatim when you rewrite a file** — they are somebody's settings and somebody's deletions — except the single `writer` line, which you replace with your own. ``` #VIEW|hiddenColumns|duration;team #VIEW|shownColumns|history #VIEW|columnOrder|id;name;status;start;end;… #VIEW|collapsed|3;7 #VIEW|savedView||view=zoom;level=day;px=24;at=2026-04-15 #DEL||| #META|format|2 #META|readOnly|yes #META|writer| #META|spec|https://gantora.net/format.md ``` - `#VIEW` — how the plan is looked at (folded columns, column order, folded groups, saved views). `~s` escapes a `;` inside a saved view's values. Never generate these; carry them through. - `#DEL` — **a tombstone**: this uid was deleted, when (a `Created`-style stamp) and what it was called. It is what lets a merge tell *deleted here* from *added there*. **Therefore: do not delete rows.** A row that simply goes missing comes back in the next merge with somebody's copy, old uid and all. If the user insists, either point them at the app, or remove the row **and** append `#DEL|||`. - `#META|format|2` — the format's version stamp (2026-08-23). **Write it on every file.** A file without it is format 1 — older column names, membership the other way round (see rule 7) — which the app reads forever and upgrades on its next save. The number moves only on a breaking change: a new column or a new `#META` key is NOT one. - `#META|readOnly|yes` — the plan declares itself read-only (2026-08-19). It is accident-prevention, not protection — but it is the file's own statement, and the app honours it: **when it is present, do not write the file.** Offer to write an editable *copy* under another name, without this line. Absence of the line means writable; never write `readOnly|no`. **And if you set this lock while working on a file, LIFT it when you deliver** — the lock belongs to the writer, and a lock left behind closes the plan to the person it was written for. - `#META|writer|` — which program saved the file last. The app writes `gantora-app`; **you write `claude-skill`**. It is a fact for a support conversation, never a gate: **never refuse a file because of what this line says**, and never refuse one with a `#`-line you do not recognise. - `#META|spec|https://gantora.net/format.md` — where this very format is written up. Write it on every save: it is the file carrying its own manual, so a `.gantora` dropped into any assistant with zero context can explain itself. Never a gate, never fetched by the app. - `#META|summary|` — the plan's **written** executive summary (2026-08-25). Optional: with no such line the report works its own summary out from the state of the plan, and that is the normal case. When the line is present it **replaces** the automatic one rather than being added to it, so write it only when the user has actually said what the summary should be. The text carries a **tiny inline markup**, and nothing outside it — no HTML, ever, because a plan is a file that travels and its summary is drawn in somebody else's app: | | | |---|---| | `*bold*` | bold | | `_italic_` | italic | | `[red]…[/red]` | and `[blue]`, `[orange]`, `[green]` | | anything else | text, including a stray bracket | Emoji need no markup — they are characters. Escape the cell like any other (`~t` `~p` `~n`): a summary sentence normally contains a pipe or a line break. Keep it under 2000 characters; longer is cut. ## 4. Minting identity: UID and stamps Pick yourself a **writer id**: 3–8 lowercase letters/digits. Use `clde` unless the user gives you one. Then: - **UID for a new row:** `---<4 random base36 chars>`, e.g. `clde-mfa3k9x2-0-7hq4`. Check it is not already in the file. **Never change an existing UID** — not on a rename, not on a move, not on a copy; a *duplicate* row is a new row and gets a new uid. - **Stamp** (for `Created`, `Modified`, `#DEL`): `2026-08-19T14:02:11Z clde Claude` — UTC ISO-8601 to the second, no milliseconds, then the writer id, then a name. Name may be the user's name if they gave one.