Paste messy meeting notes → get a structured action board you actually trust.
🔗 Live UI preview: https://bundle-rust.vercel.app · Repo: https://github.com/PugarHuda/action-board
(The live link is a static, SDK-free render of the UI so you can see it instantly. The
functional app — AI extraction, storage, chat — runs inside Anna via anna-app dev.)
Extract → review/approve → organize across To Do / In Progress / Done. Static shots: full board · dark theme · empty state · filtered by owner · mobile
Action Board turns a raw brain-dump or meeting transcript into editable action-item cards (task · owner · deadline · priority) that you approve and drag across To Do / In Progress / Done. The AI does the first pass; the human stays the reviewer. The board persists automatically, and one click pushes a clean summary back into the conversation.
This is the hackathon answer to "what comes after a chatbot?" — the assistant participates inside a workflow (extracting structure, updating the board) while the human keeps final say. It uses four Anna primitives together: App UI window · Executa tool call · persistent storage (APS) · chat write-back.
- You paste notes and hit Extract.
- The UI calls the
action-triageExecuta tool viatools.invoke. - Inside the tool, it borrows the host LLM through reverse sampling
(
sampling/createMessage) — no API key required — to extract a clean, structured JSON list of action items. - Cards render in the UI. You edit, approve, delete, and drag them — human review.
- State is saved to APS (
storage.set) and survives reloads / other devices. - Send summary to chat posts a structured artifact + readable message back into
the conversation (
chat.append_artifact/chat.write_message).
The LLM in the conversation can also open the app directly:
open_app_view(view="board", payload={ notes: "<raw text>" }) → the board
auto-extracts. (See system_prompt_addendum in manifest.json.)
- Source badges — every card shows whether it came from the AI model, the rule-based parser, or was added manually.
- Quick-add — type a task in the toolbar to add one by hand (still parsed for
@owner, dates, and priority). - Filter by owner + sort each column by priority.
- Export the board to Markdown or CSV (one click).
- Bilingual — one-click EN / ID toggle (persisted), covering all UI chrome.
- Dark / light theme — one-click toggle, persisted.
- Smart dates — picks up
Fri,Jun 20,Friday the 20th,in 3 days,end of week, ISO dates, and more. - Keyboard-friendly —
Ctrl/⌘+Enterextracts; focus a card and use←/→to move it across columns,ato approve,Deleteto remove. ARIA roles/labels throughout. - Dedupe — re-extracting the same notes won't create duplicate cards.
A static, SDK-free render of the UI lives at bundle/preview.html (used to produce
the screenshot above) — open it via the dev harness to screenshot without a runtime.
Extraction degrades gracefully across three layers — you always get a board:
| Layer | Where | When it runs |
|---|---|---|
| Host LLM (sampling) | inside the Executa tool | production / real platform |
| Tool heuristic parser | inside the Executa tool | tools.invoke works but no LLM (ACTION_TRIAGE_NO_SAMPLE=1) |
| In-browser parser | bundle/app.js |
a runtime that doesn't implement tools.invoke (resilience fallback) |
The UI label shows which path produced the items.
action-board/
├── app.json # App Store listing metadata
├── manifest.json # schema 2: executas + ui (views, host_api ACL) + dev config
├── bundle/ # static SPA (no bundler needed)
│ ├── index.html
│ ├── app.js # SDK wiring: tools / storage / chat / window + DnD + review
│ ├── parser.js # shared, dependency-free action-item parser (unit-tested)
│ ├── board.js # pure board logic: grouping, summary, dedupe, sort, filter, CSV (unit-tested)
│ ├── i18n.js # EN/ID dictionary + t() (unit-tested)
│ ├── preview.html # static SDK-free render of the UI (for screenshots)
│ ├── style.css
│ └── icon.svg
├── executas/
│ ├── triage-node/ # Tool (DEFAULT for `anna-app dev`) — verified working
│ │ ├── plugin.js
│ │ └── package.json
│ └── triage-python/ # Same contract, Python flavour (publish-ready)
│ ├── plugin.py
│ └── pyproject.toml
├── tests/ # 8 suites, 178 assertions, plain Node (no deps)
│ ├── run-all.mjs # aggregate runner (npm test)
│ ├── parser.test.mjs
│ ├── board.test.mjs # pure board logic
│ ├── i18n.test.mjs # EN/ID dictionary
│ ├── replay.mjs # stdio contract
│ ├── mock-host.test.mjs # LLM / sampling path
│ ├── python-parity.mjs # Python flavour parity (stdio + sampling)
│ ├── e2e-harness.test.mjs # live harness lifecycle + ACL enforcement
│ └── ui-smoke.mjs # real browser drive of app.js (puppeteer) + XSS check
├── fixtures/
│ └── sample-notes.txt # demo input
└── README.md
- Node 22+
- uv (the harness spawns a Python bridge via
uvx, even for a Node executa):- Windows:
irm https://astral.sh/uv/install.ps1 | iex - macOS/Linux:
curl -LsSf https://astral.sh/uv/install.sh | sh
- Windows:
- Anna CLI:
npm i -g @anna-ai/cli, thenanna-app doctor
cd action-board
anna-app dev --no-llm # serves bundle/ + supervises executas/triage-node as a stdio tool
# open http://localhost:5180/ → the board view loads in a sandboxed iframeFirst run downloads the Python bridge (~20 MB, cached afterwards). Use the Python flavour instead with:
anna-app dev --executa dir=./executas/triage-python,type=pythonanna-app validate --strict # ✓ passes (schema + UI ACL + bundle linter)- ✅
anna-app validate --strict→ passes - ✅
anna-app dev→ bridge ready, dashboard athttp://localhost:5180/, bundle served - ✅ Host APIs exercised through the harness:
storage.get/set,chat.append_artifact,chat.write_message,window.set_title,tools.list(listsaction-triage) — andstorage.list/deletecorrectly denied (least-privilege ACL is enforced) - ✅ AI/sampling path verified through the real executa runtime:
anna-app executa dev --invoke extract_actions --mock-sampling fixtures/mock-sampling.jsonlreturns"source":"llm"with model-parsed items (and--no-sampling→"heuristic") - ✅
npm test→ 178/178 assertions across 8 suites (168 locally; the Python-parity suite adds 10 on CI where a Python runtime is present) - ✅
tools.invokeis live in the current runtime (anna-app-runtime-local 0.2.0a9, spawned byanna-app dev): the UI→Executa tool path runs end-to-end locally (E2E test asserts it). Under--no-llmthe tool returns its heuristic; with the host LLM it returns the AI parse. The in-browser parser stays as a resilience fallback for runtimes withouttools.invoke(see Resilience above).
- SUBMISSION.md — copy/paste-ready DoraHacks submission writeup
- ARCHITECTURE.md — data flow, components, why the split, state model
- DEMO.md — 60–90s demo script, shot list, narration, submission blurb
- PUBLISH.md — mint Tool ID → wire it in → publish & submit (real
anna-appcommands) - CHANGELOG.md — what's built, QA fixes, known limitations
- CONTRIBUTING.md — setup, the pure-module rule, PR checklist
- SECURITY.md — trust boundaries, the XSS fix, least-privilege ACL review
- CI —
.github/workflows/ci.ymlruns validate + all tests + the mock-sampling AI check + live-harness E2E on every push - Regenerate screenshots + GIF:
npm run shots(needs a runninganna-app dev)
Eight suites, 178 assertions, all green. No test framework — plain Node, zero deps.
npm test # runs all suites (E2E auto-skips if no harness is up)
npm run test:parser # 50 — in-browser parser: extraction, chatter filter, smart dates
npm run test:board # 47 — pure board logic: grouping, summary, dedupe, sort/filter, CSV
npm run test:i18n # 15 — EN/ID dictionary: key parity, interpolation, fallback
npm run test:contract # 15 — Executa JSON-RPC stdio contract (describe/invoke/errors)
npm run test:sampling # 18 — mock-host drives the tool's LLM/sampling path + fallbacks
npm run test:py # 10 — Python flavour parity (heuristic + sampling + fallback)
npm run test:e2e # 11 — live harness: storage/chat/window/tools + ACL denial
npm run test:ui # 12 — real browser drives app.js (extract/theme/lang/quick-add/XSS)What each suite proves:
| Suite | File | Covers |
|---|---|---|
| parser | tests/parser.test.mjs |
owner/deadline/priority extraction, FYI/chatter skipping, smart date formats, cleanup, CRLF, 2000-line perf, null/empty/HTML-ish input |
| board | tests/board.test.mjs |
status grouping + counts, normalization, dedupe-merge, sort-by-priority, owner filter, CSV export, chat-summary markdown |
| i18n | tests/i18n.test.mjs |
EN/ID key parity (no missing/extra), no empty values, interpolation, language + key fallback |
| contract | tests/replay.mjs |
spawns the real plugin over stdio; describe returns a bare manifest; invoke succeeds; unknown method → -32601; empty notes don't crash |
| sampling | tests/mock-host.test.mjs |
acts as the Anna host and answers the plugin's sampling/createMessage reverse-RPC — real {type:text} shape + string/array shapes, ```json fences, garbage→heuristic, error→heuristic, malformed-item normalization, invoke_id echo |
| python-parity | tests/python-parity.mjs |
drives the Python flavour over stdio — heuristic extraction, the {type:text} sampling path, and garbage→fallback — proving real parity with Node |
| e2e | tests/e2e-harness.test.mjs |
against a running anna-app dev: storage.get/set, chat.append_artifact/write_message, window.set_title, tools.list, and least-privilege ACL (ungranted storage.list/delete → permission_denied) |
| ui-smoke | tests/ui-smoke.mjs |
drives the real app.js in headless Chrome (puppeteer): SDK connects, extract renders cards, theme/lang toggles, quick-add, source badges, chat write-back, zero uncaught JS errors |
cd executas/triage-node
printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"describe"}' \
'{"jsonrpc":"2.0","id":2,"method":"invoke","params":{"tool":"extract_actions","invoke_id":"t","arguments":{"notes":"- @Sara to send the deck by Fri, urgent\n- Tom will fix the login bug tomorrow"}}}' \
| ACTION_TRIAGE_NO_SAMPLE=1 node plugin.jsExpected: a describe manifest, then { success:true, data:{ items:[…], source:"heuristic" } }.
- Open the Action Board window.
- Paste
fixtures/sample-notes.txt. - Hit Extract → cards appear (status updates / FYIs are skipped).
- Fix one owner, bump one priority, approve two cards.
- Drag a card to In Progress, another to Done.
- Reload the window → board is still there (APS persistence).
- Click Send summary to chat → a clean summary lands back in the conversation.
- Mint a Tool ID at
https://anna.partners/executa; rewritetool-dev-action-triageinmanifest.jsonandTOOL_IDinbundle/app.js. - Create the App listing from
app.json. - Create a version with
manifest.json+ upload allbundle/files. anna-app validate --strict, submit for review, publish.
Tool IDs are mint-only — Anna assigns them server-side; you can't type a custom one.
