# Planner > Planner is a free, open-source web app that runs on localhost and shows the HTML and Markdown plans coding agents write, side by side in split panes, live as they are written. It reads one or more docs folders (for example one per project), and has a `planner` command line that agents use to configure it and to show the user a plan. What agents most often need: - Install: `npm install -g https://github.com/limyuquan/planner/releases/latest/download/planner.tgz` (Node.js 20.19+), then `planner` starts it on http://localhost:4173. - Show the user a doc in the Planner tab they already have open: `planner open ''`. Links are `?ws=/&plan=` (or `?ws=:/...` for any docs folder but the first), or `?doc=`. - Configure it: `planner config add [--name ]`, `planner config remove `, `planner config show`, `planner config path`, and `planner config check`, which validates the config and prints what each folder holds (exits non-zero while anything is wrong). - The config is `~/.config/planner/config.json`. Only changed keys are needed; any docs folder can override the layout keys (`statusFolders`, `plansFolder`, `phasePattern`, `fileTypes`, `groups`, `collections`) for itself. `planner config schema` prints the JSON schema. - A running Planner picks up config and file changes by itself; never restart it to apply them. ## Docs Each page is also Markdown at its URL with `.md`; the HTML docs are at https://limyuquan.github.io/planner/docs/. - [Everything in one file](https://limyuquan.github.io/planner/llms-full.txt): every page below, the full text of each agent skill, and the config JSON schema - [Introduction](https://limyuquan.github.io/planner/docs/introduction.md): what Planner is and does - [Installation](https://limyuquan.github.io/planner/docs/installation.md): install, start, update, run from source - [Quick tour](https://limyuquan.github.io/planner/docs/quick-tour.md): five minutes on the demo folder - [Reading plans](https://limyuquan.github.io/planner/docs/reading-plans.md): sidebar, workspaces, panes, tabs, live updates, search, settling - [Keyboard shortcuts](https://limyuquan.github.io/planner/docs/keyboard-shortcuts.md): defaults, and the hotkeys config - [Links](https://limyuquan.github.io/planner/docs/links.md): link format, `planner open`, links inside plans, theme hand-off - [Docs folders](https://limyuquan.github.io/planner/docs/docs-folders.md): how a folder is read: tasks, phases, doc types, groups, collections, several folders - [Configuration](https://limyuquan.github.io/planner/docs/configuration.md): the config file and every key - [Working with agents](https://limyuquan.github.io/planner/docs/agents.md): setting Planner up with an agent, showing plans, docs for agents - [Agent skills](https://limyuquan.github.io/planner/docs/skills.md): planner-setup, link-plan and visualise - [Command line](https://limyuquan.github.io/planner/docs/command-line.md): `planner`, `planner open`, `planner config …` - [API overview](https://limyuquan.github.io/planner/docs/http-api.md): the local HTTP API: base URL, names, errors, and every data shape - [API: Tree and docs](https://limyuquan.github.io/planner/docs/api-tree-and-docs.md): `GET /api/tree`, `GET /api/doc`, `GET /docs//` - [API: Search](https://limyuquan.github.io/planner/docs/api-search.md): `GET /api/search` - [API: Open and events](https://limyuquan.github.io/planner/docs/api-open-and-events.md): `GET /api/open`, `GET /api/events` - [API: Tasks](https://limyuquan.github.io/planner/docs/api-tasks.md): `POST /api/move` - [API: Config](https://limyuquan.github.io/planner/docs/api-config.md): `GET`/`PUT /api/config`, `POST /api/config/preview` - [Troubleshooting](https://limyuquan.github.io/planner/docs/troubleshooting.md): common problems - [Config JSON schema](https://raw.githubusercontent.com/limyuquan/planner/main/schema/config.schema.json): every config key with a description ## Agent skills - [planner-setup](https://raw.githubusercontent.com/limyuquan/planner/main/skills/planner-setup/SKILL.md): set Planner up for the user: find their docs folders, match the layout rules to how each is organised, and check the result with `planner config check` - [link-plan](https://raw.githubusercontent.com/limyuquan/planner/main/skills/link-plan/SKILL.md): show the user a plan in Planner with `planner open` instead of pasting a file path - [visualise](https://raw.githubusercontent.com/limyuquan/planner/main/skills/visualise/SKILL.md): have a subagent write a plan, recap, codebase map, explainer, ELI5 picture page or review as one self-contained HTML page in a consistent style - [visualise render guide](https://raw.githubusercontent.com/limyuquan/planner/main/skills/visualise/render/GUIDE.md): the full brief the writing subagent follows - [visualise components](https://raw.githubusercontent.com/limyuquan/planner/main/skills/visualise/render/base.html): every component of the page style once, with the scripts to copy ## Optional - [Code map](https://raw.githubusercontent.com/limyuquan/planner/main/CONTRIBUTING.md): how the source is laid out, for changing Planner itself - [Website](https://limyuquan.github.io/planner/): with a live demo of the app - [Repository](https://github.com/limyuquan/planner) --- # Introduction Planner is a free, open-source web app that runs on your machine and shows the plans your coding agents write, side by side in split panes, live as they are written. Coding agents increasingly write their plans, explainers and recaps as files: an HTML page here, a Markdown file there, one folder per task, one sub-folder per phase. Planner reads those folders and turns them into something you can actually review: every plan of a phase opens at once in its own pane, new plans announce themselves, and edited plans reload in place. ## What it does - **Split panes like an editor.** Split any pane right or down, drag tabs between panes and onto any edge, resize freely. See [Reading plans](reading-plans.md). - **Workspaces.** Each phase of a task is a workspace. Opening one lays out all its plans at once, and your arrangement is remembered per phase. - **Live.** New plans raise a notification, edited plans reload in place, and scroll positions are kept. - **Linkable.** The address bar always names the plan on screen, so a link can go in a chat or another plan, and `planner open ` shows it in the tab you already have open. See [Links](links.md). - **Search inside plans.** The search box looks inside every doc, not only at names. - **Your folders, your rules.** Read one or many docs folders, each organised its own way. Folder names, phase naming, file types, badges, icons and shortcuts are settings, not code. See [Docs folders](docs-folders.md). - **Built for agents.** Agents can set Planner up, show you what they wrote, and read these docs as Markdown. See [Working with agents](agents.md). ## Why it exists I built Planner at work, for myself. Once coding agents were writing most of the code, writing code stopped being the slow part of my day. Reviewing their plans was. Every task produced a plan, then an explainer, then a recap, spread across folders, and I was skimming them in an editor tab, or not reading them at all. When an agent did link me a plan, every link opened another browser tab, one per plan, until I had dozens open and could not tell which one was current. That is how you stop knowing what your own codebase is turning into. I wanted reviewing to be fast enough that I would actually do it, every time. So the agents write each plan as a readable page, Planner shows it the moment it lands, and I read a whole phase side by side instead of opening files one at a time. Agents show me what they wrote with `planner open`, which puts the plan in the tab I already have open instead of opening a new one. ## Next steps - [Install Planner](installation.md) and point it at your docs. - Take the [quick tour](quick-tour.md) on the demo folder. - Let your agent [set it up for you](agents.md#set-planner-up-with-an-agent). # Installation Install Planner with one command, then point it at the folder your agents write plans into. ## Requirements - Node.js 20.19 or newer - macOS, Linux or Windows, and any modern browser ## Install ```sh npm install -g https://github.com/limyuquan/planner/releases/latest/download/planner.tgz ``` This installs the `planner` command from the latest release. To update, run the same line again. ## Start it ```sh planner ``` Planner starts on [http://localhost:4173](http://localhost:4173) and opens it in your browser. The first time, it asks for your docs folder. You can also add folders from a terminal, or have your agent do it: ```sh planner config add ~/code/my-app/docs/plans --name my-app ``` See [Docs folders](docs-folders.md) for how Planner reads a folder, and [Agents](agents.md) for letting an agent set it up. Planner only listens on `localhost`; nothing leaves your machine. ## Run from source To try the demo folder, or to work on Planner itself: ```sh git clone https://github.com/limyuquan/planner cd planner npm install npm run build node dist/cli.js --root examples/demo-docs ``` `npm run dev -- --root examples/demo-docs` runs it with hot reload instead. ## Uninstall ```sh npm uninstall -g planner ``` Your settings stay in `~/.config/planner/` until you delete that folder. # Quick tour Five minutes on the demo folder: open a phase, split panes, catch a new plan, and search. The quickest way to see Planner is the live demo on the [website](https://limyuquan.github.io/planner/), which runs the real app in your browser. To run it yourself, start Planner on the demo folder from a clone (see [Run from source](installation.md#run-from-source)): ```sh node dist/cli.js --root examples/demo-docs ``` ## 1. Open a phase The sidebar lists tasks; click **Planner V1** to see its phases. Click `ws` next to **Phase 2** to make it your workspace: its plan, ELI5 and recap open side by side, one pane each. ## 2. Arrange the panes - Drag the recap's tab onto the **bottom edge** of the ELI5 pane. Only that pane splits; the plan keeps its width. - Drag the divider between panes to resize them. - Click a tab and press `Option` + `→` to send it to the next pane. Planner remembers this arrangement for Phase 2. Click `↺` next to the phase to lay it out from scratch again. ## 3. Catch a new plan Write a file into a phase folder, as an agent would: ```sh echo '# Phase 3 recap' > examples/demo-docs/active/planner-v1/plans/phase-3-settings/recap.md ``` A notification names the task and phase; **Workspace** opens it. Edit the file and the open tab reloads in place. ## 4. Search Type `tokenised` in the search box. Planner finds it inside the Search Rewrite plan; opening the result shows the plan at that word. ## 5. Settle a finished task Hover a task and click `settle`: its folder moves from `active/` to `done/`, and any open tabs follow. **Undo** brings it back. Next: [Reading plans](reading-plans.md) covers everything the panes and sidebar do. # Reading plans How the sidebar, workspaces, panes and tabs work, and how Planner keeps up as agents write. ## The sidebar The sidebar lists your tasks, newest first; drag a task to move it. Click a task to show its phases, and a phase to show its docs. With more than one [docs folder](docs-folders.md), each folder gets its own section. - Each doc shows a **badge** for its type (`plan`, `eli5`, `recap`, `md`, …), set by its file name. - **Click** a doc to open it in the focused pane. **Shift-click** it, or click `⇥`, to open it in the next pane, splitting one off if needed. - **Drag** a doc onto a pane to open it there, or onto a pane's edge to open it in a new pane. - `⧉` copies the absolute path of a file or folder, ready to paste into a terminal or hand to an agent. - `☺` on a task gives it an icon. Doc types and docs folders can have icons too; see [Configuration](configuration.md#icons). - `‹` collapses the sidebar to a thin rail; drag its edge to resize it. ## Workspaces A **workspace** is one folder's docs opened together: a phase, a task's own top-level plans, or an extra folder like `research/`. Each workspace remembers its own pane arrangement. - `ws` next to a task or phase makes it your workspace. The first time, every doc gets its own pane: up to three in a row, more over two rows, six at most. - On the workspace you are in, `ws` becomes `↺`: it lays the workspace out again from its docs (picking up new ones), and **Undo** brings your arrangement back. - Opening a doc from another phase adds it to the workspace on screen; the number on its tab says which phase it came from. - `Ctrl` + `1`…`9` opens the first nine phases of the task you are in. ## Panes and tabs The panes form a tree like an editor's: any pane can be split right or down, and a split only divides that pane. - **Drag a tab** onto a pane's edge to split it there, onto its middle to move the tab in, or along a tab strip to reorder. The highlight always shows exactly the space the tab will take. - **Drag a divider** to resize the panes either side of it. - The **pane buttons** move the shown tab to the neighbouring pane (chevrons), split it off right or below (`⊞` `⊟`), or close the pane (`×`, with **Undo**). Each only shows when it can do something. - **Middle-click** a tab to close it. - A pane keeps every tab you give it; its tab strip scrolls sideways when they overflow. - Each doc keeps its scroll position when you switch tabs, move it, or change theme. All of these have [keyboard shortcuts](keyboard-shortcuts.md). ## Live updates - When a new doc appears, a **notification** names its task and phase. **Workspace** opens it in its own workspace. - If the Planner tab is in the background, its title and icon show a count until you come back. - A doc you have open **reloads in place** when its file changes, keeping your place. ## Search The search box filters tasks, phases and docs by name as you type. From three letters it also searches **inside** every doc: results show the matching line, and opening one shows the doc with the first match selected. Press `/` to jump to the search box. ## Settling tasks When a docs folder has status folders (`active/` and `done/` by default), hover a task and click `settle` to move its folder to `done/`; `restore` moves it back. Open tabs follow the files, and links keep working because they leave the status folder out. **Undo** reverses a settle. ## Light and dark `☀` / `☾` switches the theme. It starts from your system setting and remembers your choice. Markdown follows it; HTML docs keep their own colours unless they listen for Planner's theme (see [Theme hand-off](links.md#theme-hand-off-for-html-docs)). # Keyboard shortcuts The default shortcuts, and how to change them. ## Defaults | Keys | Action | Does | | ------------------------------ | ------------------------- | -------------------------------------------------------------------- | | `Ctrl`/`Option` + arrow | `moveTab…` | Move the shown tab one pane that way, splitting a new pane if needed | | `Option` + `Shift` + `→` / `←` | `nextTab`, `previousTab` | Next / previous tab in the focused pane | | `Option` + `W` | `closeTab` | Close the shown tab | | `Ctrl` + `1`…`9` | `openPhase` | Open the nth phase of the current task | | `Option` + `B` | `toggleSidebar` | Show or hide the sidebar | | `/` | `focusSearch` | Jump to the search box | | _(unbound)_ | `splitRight`, `splitDown` | Split the shown tab into a new pane right / below | | Middle click on a tab | | Close it | Shortcuts also work while a plan has focus, but never while you type in a field. `Option` is there because macOS keeps `Ctrl` + arrows for Mission Control. ## Change a shortcut In **Settings** (⚙ in the sidebar) → **Keyboard shortcuts**, click **record** next to an action and press the keys. `×` removes a combo, and **reset** brings back the default. Or edit `hotkeys` in the [config file](configuration.md): each action takes a list of combos. ```json { "hotkeys": { "closeTab": ["Alt+Q"], "splitRight": ["Alt+\\"], "focusSearch": [] } } ``` - An action you leave out keeps its default; `[]` turns it off. - Combos are modifiers and a key joined by `+`: `Ctrl`, `Alt` (Option on a Mac), `Shift`, `Meta` (Command), then a letter, digit, arrow (`ArrowLeft`), or a symbol like `/`, `[`, `\`. - `openPhase` takes modifiers only, such as `"Ctrl"`; the digit 1–9 picks the phase. - Keys match the physical key, not the character it types, so `Alt+W` works on macOS even though Option+W types `∑`. ## Actions | Action | Does | | --------------------------------------------------------- | -------------------------------------------------------------------------------- | | `moveTabLeft`, `moveTabRight`, `moveTabUp`, `moveTabDown` | Move the shown tab to the next pane that way, splitting one off if there is none | | `nextTab`, `previousTab` | Show the next or previous tab in the focused pane | | `closeTab` | Close the shown tab | | `splitRight`, `splitDown` | Split the shown tab into a new pane | | `openPhase` | Open phase 1–9 of the current task | | `toggleSidebar` | Show or hide the sidebar | | `focusSearch` | Jump to the search box | # Links Every view has a link, and agents can use links to show you what they wrote. ## Link format The address bar always names what is on screen, so it can be copied into a chat, a ticket or another plan: ```text ?ws=[/]&plan= a doc in a workspace, by file name ?ws=[/] just the workspace ?doc= any other file ``` - `plan` is a **file name**, not a title. A missing extension is forgiven: `plan=plan` finds `plan.html` before `plan.md`. - `ws` leaves out the status folder (`active/`, `done/`) and the plans folder, so a link keeps working after the task is settled. - `doc` is a path inside the docs folder, or an absolute path on your machine. - With several [docs folders](docs-folders.md), links into any folder but the first start with its name: `?ws=side:my-task/phase-1-setup`, `?doc=side:notes/a.md`. Links into the first folder never change. For example: ```text http://localhost:4173/?ws=search-rewrite/phase-2-query-api&plan=plan.md http://localhost:4173/?ws=search-rewrite&plan=mother-plan.html ``` ## Show a link in your open tab ```sh planner open 'http://localhost:4173/?ws=search-rewrite&plan=mother-plan.html' ``` `planner open` sends the link to the Planner tab you already have open, instead of opening another one. If Planner is not running it starts it, and if no tab is open it opens one. On macOS it also brings that Chrome tab to the front. The query part alone works too: `planner open '?ws=search-rewrite'`. This is how agents show you their work; the [`link-plan`](skills.md#link-plan) skill teaches them to. ## Links inside plans Links in a doc open as tabs in Planner: - relative links to other docs (`../phase-2-api/plan.md`) - absolute paths on your machine that point into a docs folder, through symlinks too - other Planner links Links to other websites open in a new browser tab. ## Theme hand-off for HTML docs HTML docs can follow Planner's light and dark switch: - A doc is loaded with `?theme=dark` or `?theme=light`. - When the theme changes, Planner posts `{ type: 'plan-theme', theme }` to the doc. - A doc can post the same message to its parent to switch Planner's theme. Pages written with the [`visualise`](skills.md#visualise) skill do all three. # Docs folders How Planner finds tasks, phases and docs in a folder, and how to read several folders at once. ## The default layout Out of the box Planner expects each docs folder to look like this: ```text / ├── active/ tasks in progress ┐ "status folders": settle / restore ├── done/ finished tasks ┘ moves a task between them │ └── / │ ├── plans/ │ │ ├── mother-plan.html task-level docs: the task's own workspace │ │ ├── phase-1-setup/ a phase: its own workspace │ │ │ ├── plan.html │ │ │ └── recap.md │ │ └── phase-2-api/ │ └── research/ an extra folder with its own section └── weekly/ docs outside any task: a collection ``` Every part of this is a setting. If your agents lay things out differently, change the layout in **Settings** or the [config file](configuration.md), for every folder or for one. ## Tasks, phases and docs - **Tasks** are the folders inside each status folder, or directly inside the docs folder when `statusFolders` is `null`. Without status folders there is no settle or restore. - A task's **plans** sit in its `plansFolder` (`plans` by default), or in the task folder itself when it is `""`. - **Phases** are the sub-folders of the plans folder. A folder matching the `phasePattern` gets a number and a name: `phase-1a-auth` is phase 1a, _auth_, sorted after 1 and before 2. A folder not matching still shows, after the numbered ones, if it holds something to read. - **Docs** are the files with a listed extension (`fileTypes`, `html` and `md` by default). HTML shows as written; Markdown is rendered in Planner's theme; other types, such as `pdf`, `txt` or images, show as the browser shows them. Files starting with a dot are ignored. - A doc's label is its ``, or the first `#` heading of a Markdown file. ## Doc types A **doc type** gives a doc a coloured badge, and optionally an icon, by its file name. The first type whose pattern matches wins, and the order of the types is also the order docs are listed and laid out in. Files matching none show as _doc_. | Type | Badge | Matches | | --------- | -------- | ------------------ | | overview | overview | `mother-plan.html` | | plan | plan | `plan.html` | | eli5 | eli5 | `eli5*.html` | | explainer | note | `explainer*.html` | | recap | recap | `recap.html` | | evidence | tests | `evidence.html` | | md | md | `*.md` | In patterns, `*` matches anything and `?` one character. ## Groups and collections - A **group** is an extra folder inside each task with its own section after the phases, like `research/`. By default Planner shows the HTML pages in each task's `research/`. - A **collection** lists docs outside any task by a path pattern inside the docs folder, such as `weekly/*/*.html`, in its own section at the bottom of the sidebar. Collections can notify you of new docs or stay quiet. ## Several docs folders Planner can read any number of docs folders, for example one per project. Each gets a short **name**, its own section in the sidebar, and can be organised its own way: any layout key (`statusFolders`, `plansFolder`, `phasePattern`, `fileTypes`, `groups`, `collections`) set on a folder overrides the shared one for that folder only. ```json { "folders": [ { "name": "work", "path": "~/code/work/.agents/docs" }, { "name": "side", "path": "~/code/side-project/plans", "statusFolders": null, "plansFolder": "" } ] } ``` Add folders in **Settings**, with `planner config add <folder>`, or by asking an agent with the [`planner-setup`](skills.md#planner-setup) skill. Symlinked folders work. ## Check what Planner finds The settings page shows, as you type, what the rules find in each folder: tasks, phases and docs, phase folders the pattern misses, and file types it leaves out. From a terminal: ```sh planner config check ``` ```text ✓ ~/.config/planner/config.json is valid work ~/code/work/.agents/docs 30 tasks, 79 phases, 320 docs (61 plan, 58 eli5, 55 recap, 127 md, …) side ~/code/side-project/plans 4 tasks, 0 phases, 9 docs (9 md) ``` # Configuration Every setting, where it lives, and how changes apply. ## The config file Settings live in `~/.config/planner/config.json` (or `$XDG_CONFIG_HOME/planner/config.json`). The settings page, `planner config` and you can all edit it. - **Only the keys you change are needed.** Everything else is the default. - **Changes apply by themselves.** A running Planner notices the file changed and rescans; never restart it. - **Errors never wipe your setup.** A file with an error is not applied: Planner keeps the last good settings, shows a warning in the sidebar, and `planner config check` says what is wrong. - **It has a schema.** The file names its [JSON schema](https://raw.githubusercontent.com/limyuquan/planner/main/schema/config.schema.json) in `$schema`, so editors and agents can check it as they write. `planner config schema` prints it. - `--root` and `--port` on the command line apply to one run only and are never saved. A typical file: ```json { "$schema": "https://raw.githubusercontent.com/limyuquan/planner/main/schema/config.schema.json", "folders": [ { "name": "work", "path": "~/code/work/.agents/docs", "icon": "🏢" }, { "name": "side", "path": "~/code/side-project/plans", "statusFolders": null, "plansFolder": "" } ], "collections": [{ "label": "Weekly", "path": "weekly/*/*.html" }], "hotkeys": { "closeTab": ["Alt+Q"] }, "taskIcons": { "work:search-rewrite": "🔎" } } ``` ## Reference | Key | Default | Meaning | | --------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------ | | `folders` | `[]` | Docs folders to read; see [below](#folders) | | `statusFolders` | `{"active":"active","done":"done"}` | Folders holding active and finished tasks; `null` if tasks sit directly in the docs folder | | `plansFolder` | `"plans"` | Folder inside each task holding its plans; `""` for the task folder itself | | `phasePattern` | `^phase-(?<num>\d+[a-z]*)-(?<slug>.+)$` | Phase folder names; `(?<num>…)` is the phase number, `(?<slug>…)` its name | | `fileTypes` | `["html","md"]` | Extensions to show, without the dot | | `groups` | Research → `research/`, HTML only | Extra folders in each task with their own section; see [below](#groups) | | `collections` | `[]` | Docs outside any task, by path pattern; see [below](#collections) | | `docTypes` | plan, eli5, recap, md, … | Badges by file name; see [below](#doc-types) | | `hotkeys` | see [Keyboard shortcuts](keyboard-shortcuts.md) | Shortcuts by action; `[]` turns one off | | `taskIcons` | `{}` | Icons for tasks, by `"<folder>:<task>"` | | `port` | `4173` | Port to listen on | ### folders Each folder is `{ "name", "path" }`, with an optional `icon` and any layout key to override the shared one for that folder only (`statusFolders`, `plansFolder`, `phasePattern`, `fileTypes`, `groups`, `collections`). - `name`: lowercase letters, digits and dashes, unique. It shows in the sidebar and in links to the folder's docs. - `path`: absolute, or starting with `~` for your home folder. It must exist. ### groups Each group is `{ "label", "folder" }`, with an optional `fileTypes` list (default: `fileTypes`). ```json { "groups": [{ "label": "Research", "folder": "research", "fileTypes": ["html"] }] } ``` ### collections Each collection is `{ "label", "path" }`, where `path` is a pattern inside the docs folder (`*` matches one name). Also `newestFirst` (default `true`; otherwise sorted by path) and `notify` (default `false`: no notification for new ones). ```json { "collections": [{ "label": "Weekly", "path": "weekly/*/*.html", "notify": false }] } ``` ### doc types Each type is `{ "id", "label", "match", "color" }`, with an optional `icon`. `match` is a file name pattern; `color` is one of `grey`, `violet`, `blue`, `teal`, `green`, `amber`, `orange`, `pink`, `red`. The first match wins, and the order is the reading order. ```json { "docTypes": [ { "id": "plan", "label": "plan", "match": "plan.html", "color": "violet", "icon": "📐" }, { "id": "md", "label": "md", "match": "*.md", "color": "grey" } ] } ``` Giving `docTypes` replaces the whole default list, so include every type you want. ### Icons An icon is an emoji or a short symbol. Set them on doc types (shown in badges), on folders (shown in their sidebar section), and on tasks with `taskIcons` or the `☺` on a task row. ## Older config files Config files from before Planner read several folders have a single `"root"`; it is read as one folder named after the last part of its path, and links stay the same. Files in `~/.config/plan-dashboard/` are still used while there is no `~/.config/planner/config.json`. # Working with agents Planner is built to sit between you and your coding agents: they write plans as files, set Planner up, and show you what they wrote. These docs are written for them as much as for you. ## Set Planner up with an agent Give your agent the [`planner-setup`](skills.md#planner-setup) skill, then ask it to set Planner up. It finds the folders your plans live in, works out how each one is organised, writes the config with `planner config add`, and runs `planner config check` until every folder shows what is really there. Without the skill, an agent can do the same from the [command line](command-line.md#config): every config change it makes is checked, and `planner config check` exits non-zero until the config is right. ## Let agents show you their work When an agent links you a plan, each link you click opens another browser tab. After a day of tasks that is one tab per plan, and you lose track of which one is current. Instead, have agents run `planner open`: ```sh planner open '?ws=my-task/phase-2-api&plan=plan' ``` The plan appears in the Planner tab you already have open, so tabs never pile up. If Planner is not running it starts, and if no tab is open it opens one. The [`link-plan`](skills.md#link-plan) skill tells agents to do this every time, how to build the link, and to put the same link in their message so you can reopen it later. ## Have agents write better plans The [`visualise`](skills.md#visualise) skill has an agent write a plan, recap, codebase map, explainer, ELI5 page or review as one self-contained HTML page in a consistent, readable style, saved where Planner picks it up. ## Docs for agents - **Copy Markdown.** Every page of these docs has a **Copy Markdown** button that copies the page as Markdown, ready to paste into an agent. Agents can also fetch any page as Markdown by adding `.md` to its address, without the trailing slash. - **llms.txt.** [`/llms.txt`](https://limyuquan.github.io/planner/llms.txt) is a short index of Planner for agents, and [`/llms-full.txt`](https://limyuquan.github.io/planner/llms-full.txt) is every page of these docs, every skill and the config schema in one file. - **Schema.** The config's [JSON schema](https://raw.githubusercontent.com/limyuquan/planner/main/schema/config.schema.json) describes every key. - **HTTP API.** A running Planner has a small [local API](http-api.md) for scripts and agents. # Agent skills Three skills for Claude Code, Codex and other agents that read `SKILL.md` folders. Use them as they are, or as a starting point for your own. ## Install a skill Copy the skill's folder from [`skills/`](https://github.com/limyuquan/planner/tree/main/skills) into your agent's skills directory, for example `~/.claude/skills/` for Claude Code or `.agents/skills/` in a project for Codex: ```sh git clone https://github.com/limyuquan/planner /tmp/planner cp -R /tmp/planner/skills/planner-setup ~/.claude/skills/ ``` ## planner-setup Sets Planner up, or changes its config, for you: finds your docs folders, looks at how each is organised, adds them with `planner config add`, matches the layout keys to each folder, and checks the result with `planner config check` until it matches what is really there. Use it when installing Planner, adding a project, or when Planner is not showing your plans. [Read the skill](https://github.com/limyuquan/planner/blob/main/skills/planner-setup/SKILL.md) ## link-plan Tells an agent to show you a plan in Planner with `planner open '<link>'` instead of pasting a file path, and how to build the [link](links.md). [Read the skill](https://github.com/limyuquan/planner/blob/main/skills/link-plan/SKILL.md) ## visualise Has an agent turn a plan, a diff or a codebase into one self-contained HTML page in a consistent style: - **plan**, before implementation: what will change, how it works, decisions, risks - **recap**, after implementation: what landed, behaviour before and after, the diff explained, verification - **explore**, a codebase map; **explain**, one concept; **eli5**, a picture page - **diff-review**, findings you can select Every page states its conclusion up front, backs claims with `file:line` and numbers from commands it ran, and ends each section with a picture-book explanation. The main agent briefs one subagent, which follows [`render/GUIDE.md`](https://github.com/limyuquan/planner/blob/main/skills/visualise/render/GUIDE.md) and the components in [`render/base.html`](https://github.com/limyuquan/planner/blob/main/skills/visualise/render/base.html). Pages follow Planner's light and dark switch. The demo folder's `planner-v1` task, about Planner's own build, was written this way. [Read the skill](https://github.com/limyuquan/planner/blob/main/skills/visualise/SKILL.md) # API overview A running Planner has a small local HTTP API, the same one its web app and command line use. Scripts and agents can read every plan, search them, watch for new ones, show one to the user, settle tasks and change settings. ## Base URL ```text http://localhost:4173 ``` Or your configured `port`. Planner only listens on `localhost`, and there is no authentication: anything that can reach the port can use the API. ## Endpoints | Endpoint | Does | Page | | ---------------------------------------------------------------- | ------------------------------------------------- | ----------------------------------------- | | `GET /api/tree` | Every folder, task, phase and doc | [Tree and docs](api-tree-and-docs.md) | | `GET /api/doc` | Describe one doc, by doc path or absolute path | [Tree and docs](api-tree-and-docs.md) | | `GET /docs/<folder>/<path>` | A doc's file, with Markdown rendered as HTML | [Tree and docs](api-tree-and-docs.md) | | `GET /api/search` | Docs whose title or text holds every word | [Search](api-search.md) | | `GET /api/open` | Show a link in the user's open Planner tab | [Open and events](api-open-and-events.md) | | `GET /api/events` | A live stream of new docs, edits and tree changes | [Open and events](api-open-and-events.md) | | `POST /api/move` | Settle or restore a task | [Tasks](api-tasks.md) | | `GET /api/config`, `PUT /api/config`, `POST /api/config/preview` | Read, save and dry-run the settings | [Config](api-config.md) | ## Names you will see Everything is named by its folder, so several docs folders never clash. A **docs folder name** is the `name` in the config, such as `plans`. | Name | Form | Example | | ------------- | --------------------------- | ------------------------------------------------------------- | | Doc path | `<folder>/<path inside it>` | `plans/active/search-rewrite/plans/phase-2-query-api/plan.md` | | Task key | `<folder>:<task folder>` | `plans:search-rewrite` | | Workspace key | `<task key>[/<sub-folder>]` | `plans:search-rewrite/phase-2-query-api` | A workspace is a task's own top-level plans, a phase, or a group such as `research/`. Every doc's `ws` is the workspace it belongs to, or `""` for docs outside any task. [Links](links.md) use the same names but leave out the first folder's name. ## Requests and responses - Replies are JSON (`Content-Type: application/json`), except `/docs/…` files and the `/api/events` stream. - Request bodies are JSON. - Errors reply with a `4xx` status and `{ "error": "<what is wrong>" }`. - `POST` and `PUT` requests must not carry an `Origin` header from another site: a browser page elsewhere gets `403 { "error": "not from the dashboard" }`. Scripts, `curl` and agents send no `Origin`, so they are fine. ## Data shapes The types below are Planner's own, from [`shared/types.ts`](https://github.com/limyuquan/planner/blob/main/shared/types.ts), and every response is one of them. ```ts import type { Color } from './config' import type { Hotkeys } from './hotkeys' export type Status = 'active' | 'done' // One file the dashboard can show. `path` is "<folder>/<path inside it>" (see // shared/keys.ts) and is also its id. `ws` is the workspace it belongs to // ("" for loose files). export type Doc = { path: string; file: string; kind: string; title: string; ws: string } // A folder whose files open together as one workspace: a phase, a group like // research/, or a task's own top-level plans. export type Folder = { key: string; name: string; label: string; num: string; dir: string; docs: Doc[] } export type Task = { // Also the workspace key of the task's own plans: "<folder>:<task>". key: string folder: string name: string label: string icon?: string status: Status | null dir: string docs: Doc[] phases: Folder[] groups: Folder[] mtime: number } export type Collection = { folder: string; label: string; docs: Doc[] } export type Kind = { id: string; label: string; color: Color; icon?: string } // One configured docs folder, as the browser sees it. export type DocsFolderInfo = { name: string icon?: string // The real path, and the same as shown to people (~ for the home folder). root: string label: string // Whether its tasks can be settled, i.e. it has status folders. settle: boolean // Set when the folder cannot be read, e.g. it does not exist. problem?: string } export type Tree = { // In the order of the config. The first one's links leave out its name. folders: DocsFolderInfo[] tasks: Task[] collections: Collection[] kinds: Kind[] // Every action's combos, defaults filled in. hotkeys: Hotkeys } // Where a doc sits, as the "new plan" toast names it. export type Place = { task: string; phase: string | null } // Pushed from the server over /api/events. export type ServerEvent = | { type: 'tree' } | { type: 'added'; doc: Doc; place: Place } | { type: 'changed'; path: string } | { type: 'open'; ws: string; plan: string; doc: string } // What one folder's layout finds, for the settings page and `config check`. export type FolderPreview = { name: string label: string problem?: string tasks: number phases: number docs: number byKind: Record<string, number> // Phase folders whose names the phase pattern does not match. unnumbered: string[] // Extensions found next to plans but not listed in fileTypes, with counts. skipped: Record<string, number> } // The settings page's dry run: how a draft config reads its folders. export type Preview = { ok: boolean; error?: string; folders: FolderPreview[] } // /api/tree. No tree means no folder is configured yet. configProblem is set // when the config file has an error, and the last good settings are in use. export type TreeResponse = { tree: Tree | null; configProblem?: string } ``` `SearchHit`, from [`shared/search.ts`](https://github.com/limyuquan/planner/blob/main/shared/search.ts): ```ts export type SearchHit = { doc: Doc; snippet: string; hits: number } ``` # Tree and docs Read everything Planner sees: every folder, task, phase and doc, one doc's details, or a doc's file. ## `GET /api/tree` Every configured docs folder with its tasks, phases, groups and docs, plus collections, doc types and shortcuts. This is what the sidebar is drawn from. Tasks come newest first. ```sh curl -s http://localhost:4173/api/tree ``` ### Response ```ts // /api/tree. No tree means no folder is configured yet. configProblem is set // when the config file has an error, and the last good settings are in use. export type TreeResponse = { tree: Tree | null; configProblem?: string } ``` `tree` is `null` when no docs folder is configured yet. `configProblem` is set when the config file has an error and the last good settings are in use. ### Example Trimmed to one task: ```json { "tree": { "folders": [{ "name": "plans", "root": "/home/you/plans", "label": "~/plans", "settle": true }], "tasks": [ { "key": "plans:search-rewrite", "folder": "plans", "name": "search-rewrite", "label": "search rewrite", "status": "active", "dir": "plans/active/search-rewrite", "docs": [ { "path": "plans/active/search-rewrite/plans/mother-plan.html", "file": "mother-plan.html", "kind": "overview", "title": "Search rewrite — overview", "ws": "plans:search-rewrite" } ], "phases": [ { "key": "plans:search-rewrite/phase-2-query-api", "name": "phase-2-query-api", "label": "query api", "num": "2", "dir": "plans/active/search-rewrite/plans/phase-2-query-api", "docs": [ { "path": "plans/active/search-rewrite/plans/phase-2-query-api/plan.md", "file": "plan.md", "kind": "md", "title": "Phase 2 — Query API", "ws": "plans:search-rewrite/phase-2-query-api" } ] } ], "groups": [ { "key": "plans:search-rewrite/research", "name": "research", "label": "Research", "num": "", "dir": "plans/active/search-rewrite/research", "docs": [ { "path": "plans/active/search-rewrite/research/benchmarks.html", "file": "benchmarks.html", "kind": "doc", "title": "Index benchmarks", "ws": "plans:search-rewrite/research" } ] } ], "mtime": 1791279512041.3 } ], "collections": [], "kinds": [{ "id": "overview", "label": "overview", "color": "pink" }], "hotkeys": { "moveTabLeft": ["Ctrl+ArrowLeft", "Alt+ArrowLeft"] } } } ``` ### Recipes The newest plan of every active task, with `jq`: ```sh curl -s http://localhost:4173/api/tree | jq -r '.tree.tasks[] | select(.status == "active") | "\(.label): \([.phases[].docs[]] | last | .title)"' ``` ## `GET /api/doc` Describes one doc, found by its doc path or by an absolute path on the machine (through symlinks too). Use it to turn a path an agent wrote into a doc, for example before [showing it](api-open-and-events.md#get-apiopen). ### Query | Parameter | Required | Meaning | | --------- | -------- | ----------------------------------------------------------------------- | | `path` | yes | A doc path (`plans/active/…`), or an absolute path inside a docs folder | ```sh curl -s 'http://localhost:4173/api/doc?path=/home/you/plans/active/search-rewrite/plans/mother-plan.html' ``` ### Response A [`Doc`](http-api.md#data-shapes). `ws` is `""` for a file outside any task. ```json { "path": "plans/active/search-rewrite/plans/mother-plan.html", "file": "mother-plan.html", "kind": "overview", "title": "Search rewrite — overview", "ws": "plans:search-rewrite" } ``` ### Errors | Status | When | | ------ | ---------------------------------------------------------------------------- | | `404` | Not a file in a docs folder, or its type is not in that folder's `fileTypes` | ```json { "error": "plans/nope.md is not a document in the docs folders" } ``` ## `GET /docs/<folder>/<path>` The doc's file itself, at its doc path: what Planner's panes load. Markdown comes back rendered as a full HTML page; everything else is sent as it is, with its content type. ### Query | Parameter | Required | Meaning | | --------- | -------- | ---------------------------------------------------------------- | | `theme` | no | `dark` (default) or `light`: the colours Markdown is rendered in | ```sh curl -s 'http://localhost:4173/docs/plans/active/search-rewrite/plans/phase-2-query-api/plan.md?theme=light' ``` Replies `404` with the text `Not found in the docs folders` for a path outside every docs folder. # Search Find docs by what they say, not only by their names. ## `GET /api/search` Every listed doc whose title or text holds **every** word of the query. HTML is searched without its tags, scripts and styles. Docs with more of the words in their title come first, then those that mention them most. Each result has a snippet of text around the first match. ### Query | Parameter | Required | Meaning | | --------- | -------- | ------------------------------------------------------------------ | | `q` | yes | Words to find, case-insensitive. Under two characters returns `[]` | ```sh curl -s 'http://localhost:4173/api/search?q=tokenised%20search' ``` ### Response At most 40 [`SearchHit`](http-api.md#data-shapes)s, best first: ```ts export type SearchHit = { doc: Doc; snippet: string; hits: number } ``` ### Example ```json [ { "doc": { "path": "plans/active/search-rewrite/plans/phase-1-index-schema/plan.html", "file": "plan.html", "kind": "plan", "title": "Phase 1 — Index schema", "ws": "plans:search-rewrite/phase-1-index-schema" }, "snippet": "…heading anchor. Fields doc_id , anchor , title body , tokenised for full-text search updated_at , for incremental rebuilds Rebuilds A change to a documen…", "hits": 2 } ] ``` `hits` counts how often the words appear in the doc's text. ### Recipes Titles of every plan that mentions rate limits: ```sh curl -s 'http://localhost:4173/api/search?q=rate%20limit' | jq -r '.[].doc.title' ``` Search, then show the best match to the user: ```sh best=$(curl -s 'http://localhost:4173/api/search?q=rate%20limit' | jq -r '.[0].doc | "?ws=\(.ws)&plan=\(.file)"') planner open "$best" ``` A doc's `ws` works as it is in a link, folder name included, for any docs folder. # Open and events Show the user a doc, and follow what happens in the docs folders as it happens. ## `GET /api/open` Shows a [link](links.md) in the Planner tab the user already has open, as if they had followed it. This is what `planner open` calls. Give `ws` (with an optional `plan`) or `doc`. ### Query | Parameter | Required | Meaning | | --------- | ------------- | ----------------------------------------------------------------------------------------------------------- | | `ws` | `ws` or `doc` | A workspace, as links name it: `my-task/phase-1-setup`, or `side:my-task` for a folder other than the first | | `plan` | no | A file name in that workspace; the extension may be left out | | `doc` | `ws` or `doc` | Any doc: a path inside the first folder, `<folder>:<path>`, or an absolute path | ```sh curl -s 'http://localhost:4173/api/open?ws=search-rewrite&plan=mother-plan' ``` ### Response ```json { "ok": true, "listeners": 1 } ``` `listeners` is how many Planner tabs heard it. `0` means none is open: open the link in a browser instead (`planner open` does this for you). ### Errors | Status | When | Body | | ------ | ---------------------- | ------------------------------- | | `400` | Neither `ws` nor `doc` | `{ "error": "need ws or doc" }` | A name that matches nothing is not an error here: the tab shows the user a notification instead. ## `GET /api/events` A [server-sent events](https://developer.mozilla.org/docs/Web/API/Server-sent_events) stream of what changes. Each message's `data` is one JSON [`ServerEvent`](http-api.md#data-shapes): ```ts // Pushed from the server over /api/events. export type ServerEvent = | { type: 'tree' } | { type: 'added'; doc: Doc; place: Place } | { type: 'changed'; path: string } | { type: 'open'; ws: string; plan: string; doc: string } ``` | Event | When | | --------- | ------------------------------------------------------------------------------------------------ | | `tree` | Anything in a docs folder or the config changed; fetch [`/api/tree`](api-tree-and-docs.md) again | | `added` | A new doc appeared, with where it sits; collections with `notify: false` stay quiet | | `changed` | A listed doc's file was edited | | `open` | Something asked to show a link, through `/api/open` | Writes are batched: events arrive about half a second after files stop changing, so a page written in several chunks is announced once. ```sh curl -sN http://localhost:4173/api/events ``` ### Example An agent writes a recap, edits it, then shows the task: ```text retry: 1000 data: {"type":"tree"} data: {"type":"added","doc":{"path":"plans/active/search-rewrite/plans/phase-2-query-api/recap.md","file":"recap.md","kind":"md","title":"New recap","ws":"plans:search-rewrite/phase-2-query-api"},"place":{"task":"search rewrite","phase":"Phase 2 · query api"}} data: {"type":"tree"} data: {"type":"changed","path":"plans/active/search-rewrite/plans/phase-2-query-api/recap.md"} data: {"type":"open","ws":"search-rewrite","plan":"","doc":""} ``` ### Recipe Print each new plan's title as it lands: ```sh curl -sN http://localhost:4173/api/events | while read -r line; do case "$line" in data:*'"type":"added"'*) echo "${line#data: }" | jq -r '.doc.title' ;; esac done ``` # Tasks Settle a finished task, or restore it. ## `POST /api/move` Moves a task's folder between its docs folder's two status folders: `to: "done"` settles an active task, `to: "active"` restores a settled one. The folder really moves on disk; open tabs follow it, and links keep working because they leave the status folder out. Only for docs folders with `statusFolders`; see [Docs folders](docs-folders.md). ### Body | Field | Required | Meaning | | ------ | -------- | -------------------------------------- | | `task` | yes | The task key, `<folder>:<task folder>` | | `to` | yes | `"done"` or `"active"` | ```sh curl -s -X POST http://localhost:4173/api/move \ -H 'Content-Type: application/json' \ -d '{"task": "plans:onboarding-flow", "to": "done"}' ``` ### Response The task's folder before and after, as doc paths: ```json { "from": "plans/active/onboarding-flow", "to": "plans/done/onboarding-flow" } ``` ### Errors | Status | When | Example body | | ------ | ------------------------------------------------------------ | ---------------------------------------------------- | | `400` | `to` is not `done` or `active`, or the task key is malformed | `{ "error": "bad task or destination" }` | | `400` | The folder has no status folders | `{ "error": "that folder has no status folders" }` | | `403` | Sent by a web page on another site | `{ "error": "not from the dashboard" }` | | `404` | The task is not in the status folder it moves from | `{ "error": "active/onboarding-flow not found" }` | | `409` | A task with that name is already where it would go | `{ "error": "done/onboarding-flow already exists" }` | # Config Read and change Planner's settings, and try a change out before saving it. The [command line](command-line.md#config) does the same from a terminal; this is for scripts and agents that prefer HTTP. ## `GET /api/config` The settings saved in the config file, which flags this run was started with, and where the file is. ```sh curl -s http://localhost:4173/api/config ``` ### Response ```ts type ConfigResponse = { // The whole saved config, defaults filled in. See Configuration for every key. config: Config // --root / --port given to this run; they override `config` but are never saved. overrides: { folders?: DocsFolder[]; port?: number } // The config file, ~ for the home folder. file: string } ``` ### Example Trimmed: ```json { "config": { "folders": [{ "name": "plans", "path": "~/plans" }], "port": 4173, "statusFolders": { "active": "active", "done": "done" }, "plansFolder": "plans", "phasePattern": "^phase-(?<num>\\d+[a-z]*)-(?<slug>.+)$", "fileTypes": ["html", "md"] }, "overrides": {}, "file": "~/.config/planner/config.json" } ``` ## `PUT /api/config` Checks a config and, if it is valid, saves it to the config file. Planner then rescans and every open tab updates. The body is the **whole** config, not just the keys you change: read it with `GET /api/config`, change it, and send it back. Every key is described in [Configuration](configuration.md) and in the [JSON schema](https://raw.githubusercontent.com/limyuquan/planner/main/schema/config.schema.json). ```sh curl -s http://localhost:4173/api/config | jq '.config | .folders += [{"name": "side", "path": "~/code/side/plans", "statusFolders": null, "plansFolder": ""}]' | curl -s -X PUT http://localhost:4173/api/config -H 'Content-Type: application/json' -d @- ``` ### Response ```json { "ok": true } ``` ### Errors | Status | When | | ------ | ----------------------------------------------------------------- | | `400` | The config is invalid, has no folders, or a folder does not exist | | `403` | Sent by a web page on another site | `error` lists every problem, one per line, each starting with the key it is about: ```json { "error": "phasePattern: needs a (?<num>...) group\nfolders.1.path: ~/code/side/plans does not exist" } ``` ## `POST /api/config/preview` What a config would find in each folder, without saving it: the same check as the settings page's preview and `planner config check`. Use it to try a layout until it matches the folders, then `PUT` it. The body is a whole config, as for `PUT /api/config`. ```sh curl -s http://localhost:4173/api/config | jq '.config' | curl -s -X POST http://localhost:4173/api/config/preview -H 'Content-Type: application/json' -d @- ``` ### Response ```ts // The settings page's dry run: how a draft config reads its folders. export type Preview = { ok: boolean; error?: string; folders: FolderPreview[] } ``` ```ts // What one folder's layout finds, for the settings page and `config check`. export type FolderPreview = { name: string label: string problem?: string tasks: number phases: number docs: number byKind: Record<string, number> // Phase folders whose names the phase pattern does not match. unnumbered: string[] // Extensions found next to plans but not listed in fileTypes, with counts. skipped: Record<string, number> } ``` An invalid config is not an error here: it replies `200` with `ok: false`, the problems in `error`, and no folders. ### Example ```json { "ok": true, "folders": [ { "name": "plans", "label": "~/plans", "tasks": 4, "phases": 9, "docs": 18, "byKind": { "plan": 6, "md": 4, "eli5": 2, "recap": 3, "overview": 2, "doc": 1 }, "unnumbered": [], "skipped": {} } ] } ``` A folder with `0` tasks almost always needs a different `statusFolders` or `plansFolder`; see [Troubleshooting](troubleshooting.md#a-folder-shows-no-tasks). # Command line Everything the `planner` command does. ## planner ```text planner [--root <folder>]... [--port <number>] [--config <file>] [--no-open] ``` Starts Planner and opens it in your browser. | Option | Does | | ----------------- | ------------------------------------------------------------------------------------ | | `--root <folder>` | Read this docs folder for this run only, instead of the config's; repeat for several | | `--port <number>` | Listen on this port for this run only (default `4173`) | | `--config <file>` | Use this config file (default `~/.config/planner/config.json`) | | `--no-open` | Do not open a browser tab | | `-h`, `--help` | Show help | ## planner open ```text planner open <link> ``` Shows a [link](links.md) in the Planner tab you already have open. Starts Planner if it is not running, and opens a tab if none is open. `<link>` is a full URL or just its query, like `'?ws=my-task&plan=plan'`. ## config ```text planner config path where the config file is planner config show print it planner config add <folder> add a docs folder; --name <name> to name it planner config remove <name> remove a docs folder planner config check validate, then print what each folder holds planner config schema the JSON schema, with a description of every key ``` - `add` checks the folder exists, names it after its last path segment unless you give `--name`, saves, and prints what the folder holds. - `check` exits non-zero if the file has an error or a folder is missing. For a folder that finds no tasks, it says which layout key to look at. - A running Planner picks up every change by itself. # Troubleshooting Common problems and what to do about them. ## A folder shows no tasks Planner is reading the folder with a layout that does not match it. Run `planner config check`: a folder with no tasks says which key to look at. - Tasks sit directly in the folder, not in `active/` and `done/`: set `"statusFolders": null` on that folder. - A task keeps its plans in its own folder, not in `plans/`: set `"plansFolder": ""`. The settings page shows the effect of each change before you save it. See [Docs folders](docs-folders.md). ## Phases show at the bottom, without numbers Their folder names do not match the `phasePattern`. The settings preview and `planner config check` list them. Change the pattern so `(?<num>…)` captures the number, for example `^(?<num>\d+)-(?<slug>.+)$` for `01-setup`. ## A file does not show up Check its extension is in `fileTypes` (the preview lists the types it left out), and that its name does not start with a dot. Files must sit directly in a phase folder, a task's plans folder, a group folder, or match a collection. ## "The config file has an error" The config file has something Planner cannot use, so it keeps your last good settings. Run `planner config check` to see what and where, fix it, and Planner applies it by itself. ## The port is in use Another program, or another Planner, is on port 4173. Start this one with `--port 4174`, or set `"port"` in the config. ## `planner open` opens a new tab every time It reuses a tab only while one is open and connected. On macOS, bringing the tab to the front works for Google Chrome. ## Still stuck [Open an issue](https://github.com/limyuquan/planner/issues/new/choose) with what you see and the output of `planner config check`. --- # Agent skills, in full Each is a folder with a SKILL.md that Claude Code, Codex and similar agents read. Copy a folder into the agent's skills directory (for example ~/.claude/skills/) to use it. ## skills/link-plan/SKILL.md --- name: link-plan description: Show the user a markdown or HTML plan in Planner instead of giving them a file path. Use whenever you point the user at a plan, recap, ELI5, explainer, or any other doc in the docs folder. --- # link-plan Send the doc to the dashboard tab the user already has open — a clicked link would open yet another tab: ```bash planner open '<url>' ``` It starts the dashboard if it is down, and opens a tab only when no dashboard is running. Put the same URL in your message too, so it can be reopened later. Files sitting directly in a phase folder, a task's plans folder, or an extra folder like `research/` are named by workspace and **file name**, never by title. Leave out the status folder (`active/`, `done/`) and the plans folder, so the link survives the task being settled: ```text http://localhost:4173/?ws=<task-folder>[/<phase-folder>]&plan=<file> http://localhost:4173/?ws=search-rewrite/phase-2-query-api&plan=plan.md http://localhost:4173/?ws=search-rewrite&plan=mother-plan.html ``` Anything else is not listed in the sidebar, so name it by its path under the docs folder (or its absolute path): ```text http://localhost:4173/?doc=<path-under-the-docs-folder> http://localhost:4173/?doc=notes/meeting-2026-01-12.md ``` Use the port from the user's dashboard if it is not the default 4173. ## skills/planner-setup/SKILL.md --- name: planner-setup description: Set up or change Planner's config for the user — add the folders their plans live in and match Planner's layout rules to how those folders are organised. Use when the user asks to install, set up or configure Planner, add or remove a docs folder, or says Planner is not showing their plans. --- # planner-setup Planner reads one or more docs folders and shows the plans in them. Its config is one JSON file; the `planner config` commands read, change and check it, so you never have to guess whether an edit worked. ```bash planner config path # where the config file is planner config show # the whole file planner config add <folder> # add a docs folder (--name <name> to choose its name) planner config remove <name> # remove one planner config check # validate, then print what each folder holds planner config schema # the JSON schema, with a description of every key ``` If `planner` is not installed: `npm install -g https://github.com/limyuquan/planner/releases/latest/download/planner.tgz` (needs Node.js 20.19 or newer). ## Steps 1. **Find the folders.** Ask the user where their agents write plans, or look for them (`docs/`, `plans/`, `.agents/docs/`, `.claude/docs/` in their projects). Each project's folder becomes one docs folder; do not merge projects into one. 2. **Look at how each folder is organised** before configuring it: list two or three levels (`find <folder> -maxdepth 3 -type d | head -40`) and note: - are tasks inside status folders like `active/` and `done/`, or directly in the folder? - does each task keep its plans in a sub-folder like `plans/`, or in the task folder itself? - how are phase folders named (`phase-1-setup`, `01-setup`, `step-1`…)? - which file types are the plans (`html`, `md`, `pdf`…)? 3. **Add each folder** with `planner config add <folder> --name <short-name>`. Names are lowercase with dashes (`my-app`) and appear in the sidebar and in links. 4. **Match the layout.** The defaults expect `active/` and `done/` status folders, a `plans/` folder per task, and `phase-<n>-<name>` phase folders. Where a folder differs, edit the config file and put the differing keys **on that folder's entry** (they override the shared ones for that folder only): ```json { "name": "my-app", "path": "~/code/my-app/docs/plans", "statusFolders": null, "plansFolder": "", "phasePattern": "^(?<num>\\d+)-(?<slug>.+)$" } ``` - `statusFolders`: `{"active": "...", "done": "..."}`, or `null` when tasks sit directly in the folder - `plansFolder`: the sub-folder of a task holding its plans, or `""` for the task folder itself - `phasePattern`: a regular expression with a `(?<num>...)` group, and optionally `(?<slug>...)` - `fileTypes`: extensions without the dot Change a shared key at the top level only when every folder uses the same convention. 5. **Check, and fix until it matches.** Run `planner config check`. For each folder it prints tasks, phases and docs, phase folders the pattern misses, and file types it leaves out. Compare with what you saw in step 2; a folder with `0 tasks` almost always has the wrong `statusFolders` or `plansFolder`. Repeat until every folder finds what is really there. The command exits non-zero while anything is wrong. 6. **Show the user.** A running Planner picks up the change by itself. Start it if needed (`planner`), then show a plan with `planner open '<link>'` (see the `link-plan` skill for link shapes) and tell them which folders you added and what each one holds. ## Rules - Never delete or move the user's plan files; only the config changes. - Keep `~` in paths under the home folder, so the config can move between machines. - Do not change `docTypes` (badge names and colours) unless the user asks. - If `planner config check` reports an error in the file, fix that first; until then Planner keeps using the last valid settings. ## skills/visualise/SKILL.md --- name: visualise description: Produce a self-contained HTML page in the house style — implementation plans, post-implementation recaps, codebase explorations, concept explainers, standalone ELI5 picture pages, and diff/audit reviews. Use when the user types /visualise or /eli5, asks for a plan/recap/explainer/diagram page, or another skill calls for plan.html, eli5.html, or recap.html. --- # visualise The main thread never writes the HTML. It briefs one render subagent and links the result. 1. **Pick the mode**: `plan` (before implementation), `recap` (after), `explore` (codebase map), `explain` (one concept), `eli5` (standalone picture page), `diff-review` (selectable findings). 2. **Resolve the output path.** In a Planner docs folder: `plan.html`, `eli5.html`, `recap.html` in the phase folder; whole-task pages in the task's `plans/` folder; anything else where the user says. Existing files are updated in place. 3. **Spawn one subagent** (a capable model; the page is long and the guide is detailed). Its prompt contains, in this order: - "Read `render/GUIDE.md` in the visualise skill folder first and follow it." (give the absolute path) - the mode, the title, the exact output path - the sources: plan paths, diff range or commands, files to read, and any decisions or context from the conversation the sources do not contain - "Return the output path, the title, and anything you could not verify." 4. **Show it** with the `link-plan` skill (a dashboard link, not a file path), and relay the subagent's unverified items in one line. Do not read `render/GUIDE.md` or the files beside it yourself; they are written for the subagent and are long on purpose. If your agent cannot spawn subagents, read the guide and write the page yourself. --- # Config JSON schema Also printed by `planner config schema`. ```json { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "$schema": { "type": "string" }, "folders": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$", "description": "Short name shown in the sidebar and in links, e.g. \"my-app\"." }, "path": { "type": "string", "minLength": 1, "description": "Absolute path to the docs folder; ~ means the home folder." }, "icon": { "type": "string", "minLength": 1, "maxLength": 8, "description": "An emoji or short symbol shown with it, e.g. \"📐\"." }, "statusFolders": { "anyOf": [ { "type": "object", "properties": { "active": { "type": "string", "pattern": "^[^/\\\\]+$" }, "done": { "type": "string", "pattern": "^[^/\\\\]+$" } }, "required": [ "active", "done" ] }, { "type": "null" } ], "description": "Folders that hold active and finished tasks, e.g. {\"active\":\"active\",\"done\":\"done\"}; null if tasks sit directly in the docs folder." }, "plansFolder": { "anyOf": [ { "type": "string", "pattern": "^[^/\\\\]+$" }, { "type": "string", "const": "" } ], "description": "Folder inside each task that holds its plans, e.g. \"plans\"; \"\" if plans sit in the task folder." }, "phasePattern": { "type": "string", "description": "Regular expression for phase folder names; (?<num>...) captures the number, (?<slug>...) the name." }, "fileTypes": { "minItems": 1, "type": "array", "items": { "type": "string", "pattern": "^[a-z0-9]+$" }, "description": "Extensions to show, without the dot, e.g. [\"html\",\"md\"]." }, "groups": { "type": "array", "items": { "type": "object", "properties": { "label": { "type": "string", "minLength": 1, "description": "Section name in the sidebar, e.g. \"Research\"." }, "folder": { "type": "string", "pattern": "^[^/\\\\]+$", "description": "Folder name inside each task, e.g. \"research\"." }, "fileTypes": { "description": "Extensions to list here; defaults to fileTypes.", "type": "array", "items": { "type": "string", "pattern": "^[a-z0-9]+$" } } }, "required": [ "label", "folder" ] }, "description": "Extra folders inside each task that get their own sidebar section." }, "collections": { "type": "array", "items": { "type": "object", "properties": { "label": { "type": "string", "minLength": 1, "description": "Section name in the sidebar, e.g. \"Weekly\"." }, "path": { "type": "string", "minLength": 1, "description": "Path pattern inside the docs folder; * matches one name, e.g. \"weekly/*/*.html\"." }, "newestFirst": { "default": true, "description": "Newest first; otherwise sorted by path.", "type": "boolean" }, "notify": { "default": false, "description": "Show a notification when a new one appears.", "type": "boolean" } }, "required": [ "label", "path" ] }, "description": "Docs outside any task, listed by a path pattern." } }, "required": [ "name", "path" ], "additionalProperties": false }, "description": "The docs folders to read. Each may override any layout key below for itself." }, "port": { "type": "integer", "minimum": 1, "maximum": 65535, "description": "Port Planner listens on (default 4173)." }, "statusFolders": { "anyOf": [ { "type": "object", "properties": { "active": { "type": "string", "pattern": "^[^/\\\\]+$" }, "done": { "type": "string", "pattern": "^[^/\\\\]+$" } }, "required": [ "active", "done" ] }, { "type": "null" } ], "description": "Folders that hold active and finished tasks, e.g. {\"active\":\"active\",\"done\":\"done\"}; null if tasks sit directly in the docs folder." }, "plansFolder": { "anyOf": [ { "type": "string", "pattern": "^[^/\\\\]+$" }, { "type": "string", "const": "" } ], "description": "Folder inside each task that holds its plans, e.g. \"plans\"; \"\" if plans sit in the task folder." }, "phasePattern": { "type": "string", "description": "Regular expression for phase folder names; (?<num>...) captures the number, (?<slug>...) the name." }, "fileTypes": { "minItems": 1, "type": "array", "items": { "type": "string", "pattern": "^[a-z0-9]+$" }, "description": "Extensions to show, without the dot, e.g. [\"html\",\"md\"]." }, "groups": { "type": "array", "items": { "type": "object", "properties": { "label": { "type": "string", "minLength": 1, "description": "Section name in the sidebar, e.g. \"Research\"." }, "folder": { "type": "string", "pattern": "^[^/\\\\]+$", "description": "Folder name inside each task, e.g. \"research\"." }, "fileTypes": { "description": "Extensions to list here; defaults to fileTypes.", "type": "array", "items": { "type": "string", "pattern": "^[a-z0-9]+$" } } }, "required": [ "label", "folder" ] }, "description": "Extra folders inside each task that get their own sidebar section." }, "collections": { "type": "array", "items": { "type": "object", "properties": { "label": { "type": "string", "minLength": 1, "description": "Section name in the sidebar, e.g. \"Weekly\"." }, "path": { "type": "string", "minLength": 1, "description": "Path pattern inside the docs folder; * matches one name, e.g. \"weekly/*/*.html\"." }, "newestFirst": { "default": true, "description": "Newest first; otherwise sorted by path.", "type": "boolean" }, "notify": { "default": false, "description": "Show a notification when a new one appears.", "type": "boolean" } }, "required": [ "label", "path" ] }, "description": "Docs outside any task, listed by a path pattern." }, "taskIcons": { "type": "object", "propertyNames": { "type": "string" }, "additionalProperties": { "type": "string", "minLength": 1, "maxLength": 8, "description": "An emoji or short symbol shown with it, e.g. \"📐\"." }, "description": "Icons for tasks, by \"<folder name>:<task folder>\", e.g. {\"work:search-rewrite\": \"🔎\"}." }, "hotkeys": { "type": "object", "properties": { "moveTabLeft": { "description": "Move the shown tab to the pane on the left, splitting one off if there is none", "type": "array", "items": { "type": "string", "minLength": 1 } }, "moveTabRight": { "description": "Move the shown tab to the pane on the right, splitting one off if there is none", "type": "array", "items": { "type": "string", "minLength": 1 } }, "moveTabUp": { "description": "Move the shown tab to the pane above, splitting one off if there is none", "type": "array", "items": { "type": "string", "minLength": 1 } }, "moveTabDown": { "description": "Move the shown tab to the pane below, splitting one off if there is none", "type": "array", "items": { "type": "string", "minLength": 1 } }, "nextTab": { "description": "Show the next tab in the focused pane", "type": "array", "items": { "type": "string", "minLength": 1 } }, "previousTab": { "description": "Show the previous tab in the focused pane", "type": "array", "items": { "type": "string", "minLength": 1 } }, "closeTab": { "description": "Close the shown tab", "type": "array", "items": { "type": "string", "minLength": 1 } }, "splitRight": { "description": "Split the shown tab into a new pane on the right", "type": "array", "items": { "type": "string", "minLength": 1 } }, "splitDown": { "description": "Split the shown tab into a new pane below", "type": "array", "items": { "type": "string", "minLength": 1 } }, "openPhase": { "description": "Open phase 1-9 of the current task: modifiers only, e.g. \"Ctrl\" for Ctrl+1", "type": "array", "items": { "type": "string", "minLength": 1 } }, "toggleSidebar": { "description": "Show or hide the sidebar", "type": "array", "items": { "type": "string", "minLength": 1 } }, "focusSearch": { "description": "Jump to the search box", "type": "array", "items": { "type": "string", "minLength": 1 } } }, "additionalProperties": false, "description": "Keyboard shortcuts by action, each a list of combos like \"Alt+W\". Leave an action out to keep its default; [] turns it off." }, "docTypes": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string", "pattern": "^[a-z0-9-]+$", "description": "Stable id, never shown." }, "label": { "type": "string", "minLength": 1, "maxLength": 12, "description": "The badge text, e.g. \"plan\"." }, "match": { "type": "string", "minLength": 1, "description": "File name pattern; * matches anything, e.g. \"eli5*.html\"." }, "color": { "type": "string", "enum": [ "grey", "violet", "blue", "teal", "green", "amber", "orange", "pink", "red" ], "description": "Badge colour." }, "icon": { "type": "string", "minLength": 1, "maxLength": 8, "description": "An emoji or short symbol shown with it, e.g. \"📐\"." } }, "required": [ "id", "label", "match", "color" ] }, "description": "Badges by file name. A file takes the first match; the order is also the reading order." } }, "additionalProperties": false, "title": "Planner config", "description": "Settings for Planner: which docs folders to read, and how they are organised." } ```