7.2 KiB
Projects
Filesystem-linked projects, each a unit of work tied to a directory on disk. A project gives agents a standing context (path, description, permissions) so they know what they're working on without the user re-explaining it every time.
Two ways to work on a project:
- Tickets — fire-and-forget background tasks (one agent run per ticket).
- Interactive chat — a persistent conversation with the project's coordinator agent, which delegates to specialist sub-agents.
For the database schema (projects, project_tickets) see database.md. This file
documents the subsystem behavior.
Modules
| Path | Role |
|---|---|
src/core/projects/mod.rs |
ProjectManager — CRUD; free fn build_runtime_run_context |
src/core/projects/tickets.rs |
ProjectTicketManager — ticket CRUD + lifecycle |
src/core/db/projects.rs, src/core/db/project_tickets.rs |
DAOs |
src/frontend/api/projects.rs |
REST + chat-session endpoints |
web/components/projects/ |
<projects-page>, <project-list-section>, <project-board-section> |
RunContext: build_runtime_run_context
projects::build_runtime_run_context(project, base) -> RunContext is the single place that turns a
project into a runtime RunContext. It layers project-runtime fields over an
optional base RC (which carries only static config set at creation, e.g. security_group):
working_directory←project.path(always overwritten).allow_fs_writes← extended with the project tree and{skald_cwd}/data(pre-authorizes writes there, so tool calls in those trees skip the approval gate).system_prompt← project-context fragments prepended: a project header (name + description) and a hint pointing at the personal-data dir.
It is shared by both execution paths below, so a ticket job and an interactive chat see an identical project context. Edit it in one place.
Tickets (background)
A ticket is an individual work item with a title, description, agent_id, and optional
run_context (static config only). Lifecycle in ProjectTicketManager:
create/delete/reset— CRUD; every mutation callsdb::projects::touchso the list orders by recency.start(ticket_id)— resolves the base RC (ticket override → project default), callsbuild_runtime_run_context, then spawns a background job viaTaskManager.spawn_async_jobwithorigin_ref = "PROJECT_TASK:{id}". The ticket row records thejob_idand moves to running.- Completion is event-driven:
start_listenersubscribes to the system bus and, onSystemEvent::JobCompletedwhoseorigin_refmatchesPROJECT_TASK:, callson_job_completedto persist the result/error and final status.
The board UI (web/components/projects/project-board.js) renders tickets in a single scrollable
list divided into three sections:
- Running — active tickets (status
pendingorin_progress), in start order. - Todo — pending tickets, sorted by
created_atdescending (newest first). - Completed — done/failed tickets, sorted by
completed_atdescending (most recent first).
The LLM result of a done ticket is rendered as markdown. Failed tickets show raw error text. The view polls every 5 s while any ticket is running. Each ticket links to its session.
Interactive project chat
A persistent conversation about the project, driven by the project-coordinator agent (see
agents.md). A project can be of any kind — software, but also travel, study,
writing, events, personal goals — and the coordinator adapts to its nature (read from the injected
project description). The user talks to one bot that already knows the project; it does everyday
planning and writing itself and delegates specialized work — research via researcher, or code via
tech-lead/software-architect/software-engineer — to sub-agents through execute_task.
Project memory (SKALD.md). The coordinator's meta.json declares "inject_memory": ["$WD/SKALD.md"].
The $WD placeholder expands to the session's working directory (the project path), so a SKALD.md
placed in the project root is auto-loaded into the system prompt as a <memory_file> block — the
per-project analogue of how main loads data/memory/*. If the file doesn't exist yet, a
(file not created yet) placeholder is injected, which the coordinator can fill in via write_file.
See inject_memory in agents.md.
Source. The chat is bound to source id project-{id} in the sources table (hyphen, not :,
so it stays URL-safe in /api/{source}/messages). The session is interactive and
non-ephemeral, so it persists and is resumed on reopen — unlike the disposable per-client
sessions ChatHub normally manages.
Provisioning. api::projects::provisioning_for_source(skald, source) maps a source to its
(agent_id, RunContext):
project-{id}→ (project-coordinator,build_runtime_run_context(project, project.run_context))- anything else → (
main,None)
This single resolver is reused by both endpoints so open and reset never diverge:
| Endpoint | Effect |
|---|---|
POST /api/projects/{id}/session |
ChatHub::provision_session(source, agent, rc, reset=false) — open or resume; returns { source, session_id } |
POST /api/sessions?source=project-{id} |
same with reset=true — recreates the session with the coordinator (not main) |
provision_session is the only entry point for the source→session mapping ChatHub owns; the RC is
persisted at session creation (via ChatSessionManager::create_session) so it's present before the
handler is built. Because the session is interactive, execute_task is auto-injected, giving the
coordinator sub-agent delegation for free.
UI (desktop). The desktop copilot shows browser-style tabs: General (the web source, always
present) plus one tab per open project chat. The board's Open Chat button POSTs the session
endpoint, then dispatches a project-chat-open window event ({source, label}); the copilot
adds/focuses the tab and calls ChatSession._switchSource(source) to swap the live WebSocket.
Closing a project tab is UI-only — the session persists and can be reopened from the board.
UI (mobile). The mobile web app (<mobile-app>) has a Projects bottom-nav entry rendering
<projects-page> (list from GET /api/projects). Tapping a project POSTs the same session
endpoint and emits a project-open event; the shell navigates to #chat/project-{id}, which its
hash router turns into the <chat-page> source prop project-{id} (→ _switchSource) with the
project name in the header. A back button returns to #chat (the main mobile session), and the
hash survives refresh. It reuses the same project-{id} session as the desktop, so a project
chat is continuous across desktop, mobile browser, and — since the native iOS shell renders this web
app over the relay — remote. See frontend.md.
When to update this file
- Changing the project/ticket lifecycle or
build_runtime_run_context. - Changing how project chats are provisioned, sourced, or surfaced in the UI.
- Schema changes go in database.md; the coordinator agent in agents.md.