yask
by ShayDamir
README.md
# Yet Another Simple Kanban board (yask)
This is a simple Kanban board that can track multiple projects.
The functionality will be extended later.
The MVP definition:
* track multiple projects with completely separate state between them
* every task has a number, starting with 1 and increasing
* regular task types: Story, Task, Bug. Can be easily changed anytime.
* regular task estimate: story points
* compound task type: Epic
* Epics can contain other epics (tree-like structure) without cycles, and also Stories, Tasks and Bugs
* Epics cannot be estimated, they contain the sum of estimations of all contained tasks
* Tasks state: Backlog, Todo, Planning, In progress, Review, Done, Blocked, Archived
* Any task can be archived at any time
* Archived tasks are not listed by default. They can be permanently deleted.
* Blocked is a holding state for a task that cannot proceed until external input arrives (answers, a decision, missing info). It is skipped by the pipeline until the task is moved back to a normal state; moving to Blocked carries no prerequisite pull.
* Tasks are sorted, sorting must be preserved. Order of execution is top-down.
* Tasks can have other tasks as prerequisite
* If task has prerequisites and is moved in the workflow, prerequisites are moved with it unless they're already past the stage
* example: task is in Backlog, and has prereqs in Planning, Backlog and Review stages. If the task is moved from Backlog from Todo, the prerequisite that is also in Backlog is moved too. Others stay at their stages.
* ask confirmation before changing state for multiple tasks in one action
* Each state change is tracked with timestamp
* Tasks can have attachments - markdown or images
Interface:
* web interface (on local machine), configurable port, 4304 (0x10D0)
* MCP interface for agents
Tech stack:
* sqlite backend
* python3
After MVP, the development of yask will dogfood itself to add more features.
## Usage
### Development
Everything is provided by the flake (nixpkgs 26.05). Enter the dev environment
(Python with all dependencies and pytest):
```
nix develop
```
Run the test suite (538 tests covering the domain rules above):
```
python3 -m pytest tests -q
```
Full build + tests, hermetically:
```
nix build
nix flake check
```
### Web UI
```
yask serve # http://127.0.0.1:4304 (0x10D0)
yask serve --port 9000 # or: YASK_PORT=9000 yask serve
yask serve --data DIR # or: YASK_DATA=DIR yask serve
```
The UI is vanilla ES modules (no build step). State lives in a SQLite database
inside the data directory (default `~/.local/share/yask/yask.db`).
The data directory is machine-private: it is created 0700 and the database
file with its WAL sidecars 0600, and these modes are re-applied on every
start — the DB holds the boards, attachment BLOBs, and the Telegram bot's
password hashes, so other local users must not be able to read it.
The server is loopback-only by design and unauthenticated: the loopback bind
is the boundary. As defense in depth it also rejects non-loopback `Host`
headers and cross-origin `Origin` / `Sec-Fetch-Site: cross-site` requests, so
a web page elsewhere cannot read or write it (CSRF, DNS rebinding).
`--allow-remote` disables these checks.
### MCP interface
```
yask mcp
```
Speaks MCP over stdio. Example client config:
```json
{ "mcpServers": { "yask": { "command": "nix", "args": ["run", "/path/to/yask", "--", "mcp"] } } }
```
Tools: list/create projects and tasks, set prerequisites, move/archive/
restore/delete/reorder tasks, task history, task types, attachments (list,
read — images come back as viewable image blocks, markdown as text — plus a
`last_attachment` convenience that returns the task's most recent one). Actions
that would touch several tasks return `requires_confirmation` plus the list of
affected tasks; re-invoke with `confirm: true` to apply.
### Telegram bot
```
TELEGRAM_BOT_TOKEN=123:ABC yask telegram
yask telegram --data DIR # or: YASK_DATA=DIR yask telegram
```
Runs the bot as a separate process: long-polls the Bot API with the token
from `TELEGRAM_BOT_TOKEN` (get one from @BotFather) and answers `/start`,
`/help`, `/projects` (the list of projects with their per-state task
counts, sent as a rich message),
`/tasks [project]` (the tasks in the active states — Todo, Planning,
In progress and Review — grouped by project and state, sent as a rich
message), `/task
<project> <number|title>` (one task's details — state, estimate,
description, prerequisites, attachments and recent history — the task
found by number or by title, sent as a rich message), `/backlog
[project]` (the tasks in the Backlog state, grouped by project, sent as
a rich message), `/blocked
[project]` (the tasks in the Blocked state, grouped by project, sent as
a rich message),
`/attachment <project> <task> <id>` (shows a task's attachment — small
markdown (<16 KB) inline as a rich message (Bot API 10.1
`sendRichMessage`), small plain text inline as a plain message, images
as a photo, larger content as a file) and `/move
<project> <task> <state>` (moves a task to another state; a move that
would pull prerequisites along asks for confirmation via inline buttons
first), `/describe <project> <number|title> <description>` (sets —
replaces — a task's description; a numeric first word resolves the task
by number, otherwise the longest title prefix match does, and the
remaining words become the description), `/type <project>
<number|title> <type>` (changes a task's type — one of the board's task
types, matched case-insensitively; a numeric first word resolves the
task by number, otherwise the longest title prefix match does, and the
remaining words name the type) and `/attach <project>
<number|title>` (attach a file to a task: send a document or a photo to
the bot captioned `/attach <project> <number|title>` — the bot downloads
the file and attaches it; allowed types are markdown/plain text and
png/jpeg/gif/webp/svg images, 10 MB max). The rich surfaces (the
`/projects`, `/tasks`, `/backlog` and `/blocked` list views, the `/task`
detail view and small markdown attachments) degrade to HTML, then plain
text, if the
rich send fails (or the Bot API server has no `sendRichMessage`); set
`YASK_TELEGRAM_RICH=0` to send plain text only — rich is on by default. A chat can
`/subscribe [project]` to receive
notifications about
every task state change in that project, and `/unsubscribe [project]` to
stop them —
subscriptions are per chat and per project and persist across restarts; the
bot detects changes by polling `state_history`, so latency is at most one
poll interval (~30 s). More board commands are on the way. Reads the same
data directory as the other subcommands.
**Main menu.** `/start` opens the main menu — an inline-keyboard hub with
five `h:`-family buttons: **Projects** (the project list — the `/projects`
view), **Tasks** (the active tasks of all projects — the `/tasks` view),
**Subscriptions** (this chat's subscription list), **Add task** (the `/add`
usage) and **Help** (the command reference — the `/help` text). The hub is
one tap away from anywhere: every board view (`/projects`, `/tasks`,
`/task`, the `/add` confirmation and the state-change notifications) and
`/help` carry a "Main menu" button back to it. The menu's board buttons
are auth-gated like every other inline button.
At startup the bot also registers a native command menu (the Telegram app's
menu button and `/` command suggestions) via `setMyCommands`, listing the
same commands.
**Authentication.** Board commands are password-gated. Permitted users — a
Telegram chat id plus a password, stored only as a salted scrypt hash — are
managed in the web UI (topbar → ✈ "Telegram bot users"), not via Telegram.
A password is checked against a strength floor when it is set or rotated:
at least 12 characters, using at least two of the lowercase, uppercase,
digit, and symbol character classes.
A user asks the bot `/whoami` to learn their chat id, the administrator
enters it in the web UI with a password, and the user unlocks the board
with `/login <password>`. A successful login persists across bot restarts
(the session is stored in the database) and has a TTL: the stamp tracks
the chat's last authenticated activity (any board command or inline-button
use refreshes it), and after 24 hours without such activity the session
expires and the chat must `/login` again — state-change notifications are
not delivered while the session is expired. `/logout` ends the session
immediately. A newly added chat must log in once, and a password change or
removal from the web UI requires a fresh `/login` (removal revokes access
immediately). Until a chat is authenticated, the board commands and all
inline buttons answer with an auth-required notice and no board data, and
state-change notifications are not delivered to it; `/start`, `/help`,
`/login`, `/logout` and `/whoami` stay available to everyone.
Each permitted user can additionally be restricted to a list of visible
projects (web UI → ✈ "Telegram bot users" → **Projects** per user). With
no list set, all projects are visible; with a list, non-visible projects
behave as if they do not exist for that user in every bot command, inline
button and state-change notification (the regular not-found wording — a
restricted user cannot tell a hidden project from a nonexistent one), and
state-change notifications for a hidden project are not delivered to it.
Stale subscriptions to projects that later become non-visible are left in
place but inert: hidden from the subscription list, never notified.
## Project layout
- `yask/store.py` — all domain logic (projects, numbering, epic trees,
prerequisite cascade, archiving, ordering, history, attachments)
- `yask/api.py` — REST API; also serves the web UI
- `yask/web/` — web UI (vanilla JS modules, no build step)
- `yask/mcp_server.py` — MCP tool surface
- `yask/telegram_bot.py` — Telegram bot process (Bot API client, poll loop, command dispatch)
- `yask/cli.py` — `yask serve` / `yask mcp` / `yask telegram`
- `tests/` — pytest suite
- `flake.nix` / `package.nix` — packaging and dev environment
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues