---
name: gantora-plans
description: >
  Read, report on, and carefully extend Gantora project plans (.gantora files).
  Use when the user mentions Gantora, a .gantora file, or a DB folder of plans —
  to summarise status, list overdue or blocked tasks, add new tasks, or move a
  task's status on. Gantora plans are plain pipe-separated text files; this
  skill teaches the exact format and the safety rules for writing them.
---

# 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/<plan>.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. 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 <writer-id> <name>` | 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 <name>;…` | 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|<name>|view=zoom;level=day;px=24;at=2026-04-15
#DEL|<uid>|<stamp>|<name>
#META|format|2
#META|readOnly|yes
#META|writer|<writer-id>
#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|<uid>|<your stamp>|<name>`.
- `#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|<id>` — 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.

## 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:** `<writer-id>-<ms since epoch in base36>-<counter in base36>-<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.

## 5. The rules, in order of importance

1. **Never lose what you don't understand.** Round-trip every cell and every
   `#`-line you are not deliberately changing, byte for byte. Unknown columns
   are someone's data; unknown `#`-lines are someone's settings.
2. **Respect `#META|readOnly|yes`** — read freely, write never; offer a copy.
3. **Read fresh, then write, in one breath.** Never write over a plan you did
   not read *immediately* before: the user may have edited it in the app in
   the meantime, and these folders are often synced (Dropbox, OneDrive), so
   somebody else's save may have landed. Re-read, compare with what you read
   earlier, and if it changed, re-apply your edit to the new content — or stop
   and say so. Write the **whole file in one operation**; where the channel
   lets you, write a temp file in the same folder and rename it over the
   original. Never leave a half-written plan.
4. **Prefer adding over editing.** Safe: a new task row; a status move
   (Status + History append + History By append + Modified). Leave to the app:
   deleting rows, changing UIDs, restructuring groups, rewriting dates that are
   already being worked to (that is what Date History tracks), editing
   `#VIEW` lines.
5. **Keep the file's invariants** — exactly six status words; `yes`/`no`;
   dates as `YYYY-MM-DD`; references to IDs that exist; one group per task;
   nothing on a milestone but a name, a date and a status; `Reported` = `yes` on
   every main task; a group's dates are its members' first start and last end;
   **rows in time order** (by Start, then by Start hour; a group directly above
   its members; events last) and numbered 1, 2, 3… in that order — the app
   renumbers on open anyway, but a file already in order is one it rewrites
   nothing in; the header row first; the final line break.
6. **Stamp your writes:** `Modified` on every row you touched, `Created` on
   every row you made, `#META|writer|claude-skill` and
   `#META|spec|https://gantora.net/format.md` at the end.
7. **Never refuse a newer file.** A column, a `#`-line or a writer you do not
   recognise is not an error — read what you can, say what you skipped.

## 5b. Who owns the plan — two patterns, and the file says which

- **A plan the user owns.** They edit it in the app; you are the advisor —
  review it, add to it, reschedule *on request*. Its name is theirs, agreed in
  the conversation, in their own language and words, with no marker — even if
  you wrote its first version. No `readOnly` line.
- **A plan you keep for them** — a status board, a roadmap you maintain from
  what they tell you; they only look at it in the app. Then **you write
  `#META|readOnly|yes`** (so a stray tap in the app cannot write over your
  next update — the app offers *Save As* for an editable copy), and the file
  name carries an **`__ai` suffix** before the extension: `Kitchen
  renovation__ai.gantora`. The suffix is a beacon for people and programs; the
  truth is the flag and the writer stamp. **And keep it current as you go** —
  a status board that lags the conversation is one the person stops trusting,
  so the plan must never say less than what you have already been told. What
  "as you go" means depends on the channel (§5c): where you can write —
  connector, local folder, MCP — every new task and every status change goes
  into the file the moment it comes up, not batched for later; on a channel
  with no way back (an attachment, a link) hand over the updated file as soon
  as the conversation has changed the plan, without waiting to be asked.
- **Never rename a plan, and never write over a name that is taken.** On a
  clash, ask — or add a date to the name you propose.

## 5c. How you reach the file — the channels

The rules above are the same on every channel; what differs is the mechanics.

| Channel | Read | Write | Mind |
|---|---|---|---|
| **The user attaches a `.gantora` to the chat** | the attachment | hand back a complete file to download — never a fragment, never "paste this row" | say what you changed, row by row |
| **A Dropbox (or similar) connector** — the user's `DB` folder in their Dropbox | read the file through the connector, *immediately* before you write | one call that writes the **whole file**; no temp file is possible, so the fresh-read rule carries the weight | the app's own Dropbox mode writes the same folder — never assume the file is as you left it |
| **A local folder** the user has opened for you | read it | temp file + rename | `DB/_lists.csv` and anything `_*` are the app's |
| **The Gantora MCP server** | its tools | its tools — they enforce these rules in code | prefer it when it is there |
| **A link** — `…/index.html#plan=…` | — | hand the user one link; the app opens it read-only, as a shared copy, with no file and no folder | for *showing* a plan, not for keeping one: nothing comes back |

**Making a plan link.** The whole plan travels after the `#`, so it never
reaches a server. Take the file's text (UTF-8, exactly as you would write it),
**deflate-raw** it, base64url-encode the bytes (`A–Z a–z 0–9 - _`, no padding),
and put a `~` in front: `#plan=~<that>`. Add `&file=<name>` for the plan's
name (the extension is added if missing), and `&view=gantt` / `&lang=hu` if
you want a view or a language. Without the `~` the value is read as plain
UTF-8 in base64url — a shape a person can make by hand. A fifty-row plan is
about 4–8 KB of link; most chat clients carry that, a few truncate very long
messages — if in doubt, send the file as well. The base address is whatever
Gantora page the user opens: the copy on gantora.net once it is published, or
their own `index.html`.

## 5d. Before you write a plan from scratch — the planner's interview

A good plan is decided before the file: ask, briefly, then write.

1. **What is being done, and in what breakdown** — by room or by trade? by
   phase or by deliverable? That choice *is* the group structure, so it is
   worth one question.
2. **Who does what** — `Team` / `Responsible`, even roughly ("me", "the
   electrician", "the venue").
3. **What has to wait for what** — the real `Predecessors`, not every row
   chained to the one above it.
4. **The waiting times, as rows of their own** — drying, curing, delivery lead
   time, approvals. They are what turn "two weeks of work" into eight weeks on
   the chart, and a plan without them is a plan that will be late.
5. Dates you are unsure of: write them and tick `Not Finalized` = `yes` — the
   app draws those pale and dashed, which is exactly what they are.

Then write it in time order (rule 5), stamp it (rule 6), and tell the user
what the file is called and where it is.

## 6. Reading a plan well

- **Where does the project stand?** Count leaf tasks (not groups — a group's
  status and dates are its members'; not milestones — those get a section of
  their own). Report by top-level group: Done / In Progress / Blocked / On Hold,
  **overdue** (`End < today` and Status not Done/Blocked/On Hold), late starts
  (`Start < today`, still Planned), and what falls in the next week.
- **Drift:** baseline = `Baseline End` if set, else the End of the *first*
  `Date History` entry, else the current End. Drift = End − baseline, in days;
  positive is late. A row still `Not Finalized` has no drift to report.
- **A group's status** is the loudest member: `Blocked > On Hold > In Progress > Planned > Done` — and one member begun makes the group In Progress.
- The `History` / `History By` pair tells you who moved what and when; the
  `Modified` stamp tells you who touched the row last.
- **Remember rows by UID, never by ID.** The ID is a position: the app
  renumbers the plan into time order on every open, so an ID noted earlier in
  the conversation — or in an earlier conversation — may point at a different
  row by now. When you expect to come back to a task, note its UID.
- **Report, don't echo.** When asked where a plan stands, answer with the
  counts and the rows that matter (overdue, blocked, this week) — never by
  reproducing the whole file; a large plan pasted into the chat drowns the
  answer and the context both.

## 7. Typical asks

- *"Add a task: …"* — new row: next free ID, dates as given (`Duration` computed), `Status` = `Planned`, `History` = `<today> Planned`, `History By` = `<today> <name or ->`, fresh UID, `Created` = `Modified` = your stamp, `Reported` = `yes` if top level else `no`, `Not Finalized` = `no`. To put it in a group, **write the parent row's `UID` into the new row's `Parent` cell** — nothing else says membership, and the parent row is not touched at all. Then update `#META|writer`.
- *"Mark X done / in progress / blocked."* — change `Status`, **append** to `History` (`<today> <Status>`) and to `History By` (`<today> <name>`), set `Modified`. Touch nothing else on the row. If the row already has that status, do nothing and say so.
- *"Which tasks are overdue / blocked / due this week?"* — read-only; answer from the rules in §6.

## 8. A minimal example

```
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
1|Design|10|2026-09-01|09:00|2026-09-10||||||||no||In Progress|2026-08-19 Planned;2026-09-01 In Progress|no||no|||clde-mfa3k9x2-0-7hq4|2026-08-19T14:02:11Z clde Claude|2026-09-01T08:10:00Z ak7 Anna Kovacs|2026-08-19 Claude;2026-09-01 Anna Kovacs|yes|
2|Workshop|3|2026-09-01|09:00|2026-09-03|16:00||PMO|ERP|Anna Kovacs:4|Bela Nagy:6|clde-mfa3k9x2-0-7hq4|no|Room booked~nAgenda: scope~p risks|Done|2026-08-19 Planned;2026-09-03 Done|no||no|||clde-mfa3k9x2-1-q2m8|2026-08-19T14:02:11Z clde Claude|2026-09-03T15:30:00Z ak7 Anna Kovacs|2026-08-19 Claude;2026-09-03 Anna Kovacs|no|wave-1
3|Write the spec|7|2026-09-04||2026-09-10||2|PMO|ERP|Anna Kovacs||clde-mfa3k9x2-0-7hq4|no||Planned|2026-08-19 Planned|no||yes|||clde-mfa3k9x2-2-z0p1|2026-08-19T14:02:11Z clde Claude|2026-08-19T14:02:11Z clde Claude|2026-08-19 Claude|no|wave-1;risk
4|Spec approved|0|2026-09-11||2026-09-11||3||||||yes||Planned|2026-08-19 Planned|no||no|||clde-mfa3k9x2-3-8hd3|2026-08-19T14:02:11Z clde Claude|2026-08-19T14:02:11Z clde Claude|2026-08-19 Claude|yes|
5|Christmas freeze|0|2026-12-22||2026-12-29||||||||yes||||yes|🎄|no|||clde-mfa3k9x2-4-k4n7|2026-08-19T14:02:11Z clde Claude|2026-08-19T14:02:11Z clde Claude||yes|
#META|format|2
#META|writer|claude-skill
#META|spec|https://gantora.net/format.md
```

Row 1 is a group (rows 2 and 3 carry its UID in their `Parent` cells); its dates, its `09:00` start
hour and its status are derived from them. Row 2 shows a note with an escaped line break and pipe,
people with hours, and a Done row. Row 4 is a milestone; row 5 an event —
a period with an emoji, no status, no history, and `Duration` 0 like every
milestone-flavoured row.

---

*This format description may be used freely to read and write Gantora plans
from any tool. The Gantora application itself is separately licensed.*
