Library — universal hierarchical workspace
The Library is Prax's (and, more generally, any TeamWork agent's) persistent workspace for the user. Every goal-directed effort lives here: life areas, work initiatives, learning, creative projects, operational boards, personal tracking…
f62d7985
View source ↗
The Library is Prax’s (and, more generally, any TeamWork agent’s) persistent workspace for the user. Every goal-directed effort lives here: life areas, work initiatives, learning, creative projects, operational boards, personal tracking — whatever the user wants to organize. Humans and agents collaborate on the same corpus with explicit authorship provenance so neither steps on the other.
The core shape is Project → Notebook → Note with three cross-cutting features:
- Sequenced notebooks for ordered lessons / steps / chapters with progress tracking and a “current lesson” pointer
- Kanban task boards per project — columns + cards with activity log, assignees, checklist, and due-date reminders wired into the existing scheduler
- Raw / outputs / wikilinks / graph view / schema editor / health check — every Obsidian-style feature you’d expect plus a Karpathy-style audit
Branding note. TeamWork is agent-neutral — any agent running on TeamWork can own a Library — so the panel is simply Library. Alongside Library there is a Home rail showing active projects as cards.
Scope — the wall between Kanban and agent_plan
The Library Kanban is for humans, not for Prax’s working memory. Prax has a separate, private to-do mechanism called
agent_plan(stored atworkspaces/{user}/agent_plan.yaml) that he uses for his own multi-step work. The two systems MUST NOT be mixed.
| System | Whose list? | Storage | Time scale | Visibility |
|---|---|---|---|---|
agent_plan |
Prax’s private working memory | workspaces/{user}/agent_plan.yaml |
Seconds to minutes (per user turn) | Read-only widget in the chat view; cleared at end of turn |
| Library Kanban | The human’s project board | library/projects/{p}/.tasks.yaml |
Days to weeks (real project work) | Full Library UI, drag-and-drop, activity log, reminders |
Why the wall matters:
- Different time scales. Kanban is built for work items that live for days or weeks. Prax’s “tasks” are seconds-long tool calls — a single user turn would generate dozens of create/move/done operations and leave a cluttered board behind.
- Different cognitive objects. Kanban is a human UI for tracking
durable commitments.
agent_planis a compact internal state file Prax uses to remember where he was across a multi-step turn. - Context cost.
agent_plan.yamlcompresses to ~200 tokens for a 10-step plan; a Kanban task with activity log + metadata is 400-800 tokens per task. Prax reasoning over Kanban as working memory would burn significant context per turn. - Semantic collision risk. If Prax used Kanban for his own work, he’d have to constantly disambiguate “my todo column” from “the user’s todo column” when working inside a project that already has a real Kanban. Keeping them in different files with different tool families removes the confusion entirely.
- The research supports this. See §22 and §25 in
research/agentic-todo-flows.md— exposing Prax’s working memory to the user as an editable artifact raises cognitive load and can actually reduce plan quality via miscalibrated trust. The read-only chat widget (below) is the research-supported middle ground.
What Prax may do with the Library Kanban:
- Add tasks when the user explicitly asks for something tracked on the board (“add a card to remind me to ship the spec by Friday”)
- Move / comment on / complete tasks the user asks him to touch
- Read the board for context when the user references it (“what’s blocked on Q2?”)
What Prax must NOT do with the Library Kanban:
- Use it to track his own tool-call sequence inside a single turn
- Create tasks representing ephemeral subgoals that exist only for the duration of a user request
- Mirror
agent_plansteps onto the board
Agent-plan visibility in the chat UI. Even though agent_plan is
private by default, the TeamWork chat view renders a small read-only
card showing Prax’s current goal + current step + progress (3/7 done) whenever an agent_plan is active. This gives the user
situational awareness without the cost of a full Kanban or the risks
of mid-execution oversight. The card is not editable — the CHI 2025
Plan-Then-Execute study (N=248) found that mid-execution user edits
to plans often reduce plan quality when the system’s initial plan
was already correct.
Future possibility (not built, not planned yet). For genuinely
multi-day projects where Prax is the primary driver — e.g., “write
a 40-page report over the next two weeks” — we may eventually give
Prax limited write access to the Library Kanban so his long-horizon
work becomes visible alongside the user’s work. This would be an
explicit opt-in per project (a prax_may_add_tasks project-level
flag), not a default capability, and it would coexist with
agent_plan for ephemeral within-turn state. This is deferred
until there’s a concrete use case demanding it.
Why projects are universal
“Project” in the Library is deliberately broad. A project is anything the user wants to organize toward a goal or outcome:
| Example use case | kind |
Notebook shape | Tasks? |
|---|---|---|---|
| Learn French in 6 months | learning |
sequenced notebooks for Vocab / Grammar / Speaking / Culture | practice reminders, chapter deadlines |
| Ship Feature X | initiative |
free-form notebooks for Spec / Research / Decisions / Log | yes — the Kanban board is the feature tracker |
| Knit an Aran sweater | creative |
free-form notebooks for Pattern / Materials / Progress photos | optional (shopping list, milestones) |
| Run business Ops | ops |
notebooks for Metrics / Meetings / Decisions | yes — recurring tasks and reviews |
| Personal (life area) | life_area |
free-form notebooks for Health / Hobbies / Journal | optional |
| Research X | research |
sequenced notebook for reading list + free-form for notes | optional |
kind is a freeform label — the system never enforces any specific
set, but the UI suggests common ones via an autocomplete datalist.
The same hierarchy handles reading lists, software launches, and
knitting projects. No separate “Courses” surface is needed — a
learning project is just a project whose notebooks are sequenced=true.
Inspiration: Karpathy’s Second Brain
The core shape of the Library is adapted from Andrej Karpathy’s personal knowledge base pattern (widely shared on Twitter in early April 2026). His central idea:
Three folders. One schema file. An AI that maintains everything.
raw/— the junk drawer. Articles, clips, screenshots, meeting notes. Never organized by hand; the agent classifies from here.wiki/— the organized layer. Every topic is a markdown file, linked to related topics. The agent maintains it.outputs/— generated answers, reports, and briefings. Separate from the wiki so agent output never pollutes the source-of-truth.
Plus a schema file (he calls it CLAUDE.md; we call ours LIBRARY.md)
that tells the agent what the knowledge base is about, what the rules
are, and what the human cares about.
We extended Karpathy’s pattern in two ways to make it fit Prax:
- A Project → Notebook hierarchy replaces the single flat
wiki/. Real users want separate life areas that don’t bleed into each other. Projects are the big groupings; notebooks are topics inside a project; notes are individual files inside a notebook. - Explicit author provenance. Every note is tagged
author: humanorauthor: prax, and human-authored notes are read-only to Prax unless the human explicitly opts in. This is the trust primitive that lets a human actually keep their own writing in the Library without worrying about the agent overwriting them.
Layout on disk
Inside each user’s workspace:
workspaces/{user_id}/library/
├── LIBRARY.md # schema / rules / user interests
├── INDEX.md # auto-maintained table of contents
├── .pending_engagements.yaml # queue for prax_may_edit unlocks
├── raw/ # unsorted captures (junk drawer)
│ └── 2026-04-08-140000-article.md
├── outputs/ # generated briefs / reports / answers
│ └── 2026-04-08-140000-health-check.md
└── projects/
└── {project-slug}/
├── .project.yaml # project metadata
├── .tasks.yaml # Kanban columns + tasks (when tasks_enabled)
└── notebooks/
└── {notebook-slug}/
├── .notebook.yaml
└── {note-slug}.md
LIBRARY.md is seeded on first access and is freely editable by both
the human and the agent. It’s the contract between the human’s intent
and the agent’s behavior — the agent reads it at the start of every
library turn so the human’s rules override any generic system prompt.
INDEX.md is rebuilt automatically on every note/notebook/project
write. Humans can read it but shouldn’t edit — edits will be
overwritten on the next mutation.
Data model
Project metadata (.project.yaml)
slug: learn-french
name: Learn French
description: 6 months to fluency
kind: learning # freeform label (learning, initiative, creative, ops, life_area, research, …)
status: active # active | paused | completed | archived
target_date: "2026-10-01"
started_at: "2026-04-08T..."
pinned: true # floats to the top of the Home dashboard
tasks_enabled: true # show Kanban board?
reminder_channel: sms # default channel for task reminders (all/sms/discord/teamwork)
created_at: "..."
updated_at: "..."
The Home dashboard shows pinned projects first, then alphabetical.
Notebook metadata (.notebook.yaml)
slug: lessons
name: Lessons
description: "Sequenced French lessons"
project: learn-french
sequenced: true # ordered notebook with progress tracking
current_slug: vocab-basics # pointer to the active lesson
created_at: "..."
updated_at: "..."
Flip sequenced: true to turn a notebook into an ordered sequence.
Notes gain lesson_order and status (todo/done). Marking a note
done auto-advances current_slug to the next todo.
Note frontmatter
---
title: "Sleep optimization"
slug: sleep-optimization
author: human # "human" or "prax" — set once, never changes
project: personal
notebook: health
prax_may_edit: false # permission gate — see below
last_edited_by: human
tags: [sleep, health, routine] # always stored normalized: lowercase, no leading #
wikilinks: [cbt-i, circadian] # extracted from [[…]] patterns on write
lesson_order: 0 # only meaningful in sequenced notebooks
status: todo # todo | done (only meaningful in sequenced notebooks)
created_at: "..."
updated_at: "..."
---
Task entries (.tasks.yaml)
columns:
- id: todo
name: "To Do"
- id: doing
name: "Doing"
- id: done
name: "Done"
tasks:
- id: tsk-abc123
title: "Ship feature X"
description: "..."
column: doing
author: human # or 'prax'
source: user_request # user_request | agent_derived | tool_output
source_justification: "" # required when source == tool_output
confidence: medium # low | medium | high (self-reported)
assignees: ["prax", "sam"]
due_date: "2026-04-15T17:00:00-07:00"
reminder_enabled: true
reminder_id: "rem-..." # present iff a scheduler reminder is live
reminder_channel: "" # overrides project default when set
checklist:
- text: "Endpoints"
done: true
- text: "Auth"
done: false
activity:
- actor: human
at: "..."
action: created
source: user_request
- actor: prax
at: "..."
action: moved
from: todo
to: doing
comments: []
Permission model: unlike notes, tasks are inherently collaborative.
Both human and Prax can freely create, move, update, and comment on
any task — no prax_may_edit gate. Every mutation appends to the
activity log with the actor, the action, and any relevant fields.
The log is the audit trail — read-only in the UI.
Task provenance (P1): source records where the request to
create a task came from, so the activity trail can explain how a
task got onto the board.
user_request— the human explicitly asked for this task. This is the default when the UI’s “Add task” button is the caller.agent_derived— Prax added the task while executing a user request (e.g., “break this down for me”). Default whenlibrary_task_addis called from an agent tool.tool_output— a tool suggested the task. This is the dangerous case: a scraped webpage or calendar entry could carry instruction- like text that’s really a prompt-injection attempt. Creating atool_outputtask requiressource_justificationor the call errors out; the UI renders an amber warning badge and a dedicated “Review before trusting” callout in the side panel. Prax has a dedicatedlibrary_task_add_from_tool_outputtool that forces him to statesource_tooland explain why the task belongs on the user’s board.
Confidence signal (P2): confidence is Prax’s self-reported
hint (low / medium / high) about how sure he is that a task
is well-scoped or — on agent_plan — that the plan is correct and
complete. It is not calibrated and not a probability; it’s
a situational-awareness cue rendered as a small colored dot (red /
amber / emerald) on cards and the AgentPlanCard widget so the
user knows when to pay extra attention. The UI tooltip explicitly
labels it “self-reported, not calibrated”.
Plan context cap (P5): workspace_service.get_workspace_context
compacts large plans before injecting them into the system prompt.
When a plan has more than 6 steps or more than 800 characters of
step descriptions, the injected view shrinks to: goal, current step
in full, next 2 upcoming steps (brief), and a … N more step(s)
pointer at agent_plan_status as the overflow valve. This
prevents long-running plans from eating turn context while still
keeping Prax aware of his current focus. See
docs/research/agentic-todo-flows.md
§25 for the rationale.
The prax_may_edit permission gate
The core trust primitive for notes. It answers: “is the agent allowed to modify this specific note?”
- Prax-authored notes:
prax_may_editdefaults totrue. Prax created the note and can refine it freely. The human can turn it off to freeze a Prax note as canonical. - Human-authored notes:
prax_may_editdefaults tofalse. Prax cannot edit the note.update_noterefuses the mutation and returns an error that the knowledge spoke surfaces back so the orchestrator can tell the user to unlock the note first. - The UI “Ask Prax to refine” button passes
override_permission=trueon the API call. That’s the human’s explicit per-turn consent, so it bypasses the gate without requiring the persistentprax_may_edit: trueflag.
Proactive engagement on unlock
When the human flips prax_may_edit from false to true on a
human-authored note, the note is queued at
library/.pending_engagements.yaml. At the start of every turn,
workspace_service.get_workspace_context() drains the queue and
injects this into Prax’s system-prompt context:
Proactive engagement — notes just unlocked for you
The user flipped
prax_may_editto true on the following human-authored notes since your last turn. Read each one and proactively offer to refine, expand, fact-check, or add to it. Don’t wait to be asked — the unlock is the ask.
- My thoughts (
personal/journal/my-thoughts) — unlocked at 2026-04-08T14:30:00Z
Prax-authored notes don’t trigger the queue (Prax already has free editing access). Locking a note back removes any pending engagement. The queue drains once per turn so Prax doesn’t nag on subsequent turns.
Storage service (prax/services/library_service.py)
Projects
| Function | Purpose |
|---|---|
ensure_library(user_id) |
Idempotent skeleton creation |
create_project(user_id, name, description?, kind?, status?, target_date?, pinned?, tasks_enabled?, reminder_channel?) |
Create a project |
update_project(user_id, project, ...) |
Update any subset of metadata fields |
get_project(user_id, project) |
Full project detail with progress rollup |
list_projects(user_id) |
All projects, pinned first then alphabetical |
delete_project(user_id, project) |
Delete an empty project |
create_learning_project(user_id, subject, title?, modules?, description?, target_date?, notebook_name?) |
One-call course creation: project + sequenced notebook + ordered lesson notes |
Notebooks
| Function | Purpose |
|---|---|
create_notebook(user_id, project, name, description?, sequenced?) |
Create a notebook inside a project |
update_notebook(user_id, project, notebook, name?, description?, sequenced?, current_slug?) |
Update metadata (auto-backfills lesson_order when turning sequenced on) |
get_notebook(user_id, project, notebook) |
Notebook metadata |
list_notebooks(user_id, project?) |
All notebooks (optionally scoped to one project) |
delete_notebook(user_id, project, notebook) |
Delete an empty notebook |
reorder_notes(user_id, project, notebook, slug_order) |
Batch rewrite lesson_order to match a new order |
Notes
| Function | Purpose |
|---|---|
create_note(user_id, title, content, project, notebook, author?, tags?, prax_may_edit?, lesson_order?, status?) |
Create a note (author defaults to prax) |
get_note(user_id, project, notebook, slug) |
Fetch a single note with metadata + content |
list_notes(user_id, project?, notebook?) |
List notes in scope — sorted by lesson_order in sequenced notebooks |
update_note(user_id, project, notebook, slug, content?, title?, tags?, editor?, override_permission?) |
Update a note — enforces the permission gate |
delete_note(user_id, project, notebook, slug) |
Delete a note |
move_note(user_id, from_project, from_notebook, slug, to_project, to_notebook) |
Move a note between notebooks / projects |
set_note_status(user_id, project, notebook, slug, status) |
Mark a note todo/done (auto-advances current_slug on done) |
set_prax_may_edit(user_id, project, notebook, slug, editable) |
Toggle the permission flag and enqueue/drain a pending engagement |
Wikilinks + backlinks + index
| Function | Purpose |
|---|---|
extract_wikilinks(body) |
Pull [[slug]], [[notebook/slug]], [[project/notebook/slug]], and `[[slug |
get_backlinks(user_id, project, notebook, slug) |
Reverse wikilink lookup — notes that point to this one |
find_dead_wikilinks(user_id) |
Static scan for [[…]] targets that don’t resolve |
rebuild_index(user_id) |
Regenerate INDEX.md from the current tree (auto-called on every mutation) |
read_index(user_id) |
Return current INDEX.md |
read_schema(user_id) / write_schema(user_id, content) |
LIBRARY.md I/O |
Raw / outputs
| Function | Purpose |
|---|---|
raw_capture(user_id, title, content, source_url?) |
Drop an unsorted item into the junk drawer |
list_raw(user_id) / get_raw(user_id, slug) / delete_raw(user_id, slug) |
Raw capture CRUD |
promote_raw(user_id, raw_slug, project, notebook, new_title?) |
Promote a raw item into a real note (adds promoted_from provenance, removes the raw file) |
write_output(user_id, title, content, kind?) |
Save a generated brief/report/answer to outputs/ |
list_outputs(user_id) / get_output(user_id, slug) |
Outputs CRUD |
Tags (normalized)
| Function | Purpose |
|---|---|
list_tag_tree(user_id) |
Nested tag tree with count + total + children |
list_notes_by_tag_prefix(user_id, prefix) |
Notes matching a tag prefix or any descendant |
Every tag is canonicalized on write and read: leading # stripped,
lowercased, whitespace trimmed, // collapsed to /, empty segments
dropped, lists deduped. Queries work case-insensitively —
list_notes_by_tag_prefix(user, "#Math") matches the same notes as
list_notes_by_tag_prefix(user, "math").
Refine
| Function | Purpose |
|---|---|
refine_note(user_id, project, notebook, slug, instructions) |
Low-tier LLM rewrites the body; returns {before, after} without applying |
apply_refine(user_id, project, notebook, slug, new_content) |
Apply an approved refinement (bypasses prax_may_edit because the UI click is consent) |
Health check
| Function | Purpose |
|---|---|
run_health_check(user_id) |
Static + LLM audit of the whole library; writes a full report to outputs/health-check-{date}.md |
schedule_health_check(user_id, cron_expr?, channel?, timezone?) |
Create a recurring health check via scheduler_service.create_schedule |
Default cron is "0 9 * * 1" (Mondays at 09:00). The scheduled prompt
asks Prax to run library_health_check and summarize the findings in
<200 words — full report still lands in outputs/.
Tasks (prax/services/library_tasks.py)
| Function | Purpose |
|---|---|
list_columns(user_id, project) |
Kanban columns |
add_column(user_id, project, name) |
Add a column |
rename_column(user_id, project, column_id, new_name) |
Rename |
remove_column(user_id, project, column_id) |
Remove empty column |
reorder_columns(user_id, project, order) |
Reorder columns |
list_tasks(user_id, project, column?) |
List tasks, optionally filtered by column |
get_task(user_id, project, task_id) |
Full task with activity log + comments |
create_task(user_id, project, title, description?, column?, author?, assignees?, due_date?, reminder_enabled?, reminder_channel?, checklist?) |
Create a task + optional reminder |
update_task(user_id, project, task_id, ...) |
Update (reschedules reminder if due date / channel / enabled changes) |
move_task(user_id, project, task_id, new_column, editor?) |
Move between columns — cancels reminder on move to done |
delete_task(user_id, project, task_id) |
Delete + cancel reminder |
add_comment(user_id, project, task_id, text, actor?) |
Append a comment (also logged in activity) |
task_summary(user_id, project) |
Per-column counts for dashboards |
Reminder lifecycle
When a task is created with a due_date and reminder_enabled: true
(the default), library_tasks automatically calls
scheduler_service.create_reminder over the project’s configured
channel (all by default, overridable per-project or per-task).
The resulting reminder_id is stored on the task.
- Update due_date / reminder_channel / reminder_enabled → old reminder cancelled, new one scheduled
- Move to
donecolumn → reminder cancelled - Delete task → reminder cancelled
This makes the Kanban a real lightweight ticketing system: put a date on a card and you’ll be pinged; mark it done and the ping disappears.
Agent tools (prax/agent/library_tools.py)
30 tools registered in the knowledge spoke (and the course spoke,
alongside the legacy course_* tools).
Projects: library_project_create, library_projects_list,
library_project_update, library_create_learning_project
Notebooks: library_notebook_create, library_notebooks_list,
library_notebook_sequence, library_notebook_reorder
Notes: library_note_create, library_note_read,
library_note_update, library_note_move, library_note_mark,
library_notes_list
Raw / outputs / health: library_raw_capture, library_raw_list,
library_raw_promote, library_outputs_write, library_outputs_list,
library_health_check, library_schedule_health_check
Kanban: library_task_add, library_tasks_list,
library_task_move, library_task_update, library_task_delete,
library_task_comment, library_column_add, library_column_rename,
library_column_remove
Agent-created notes always get author: prax. Human-authored notes
come in through the TeamWork UI (the POST /library/notes endpoint
defaults to author: human).
The library_note_update tool enforces the permission gate — it
refuses to edit human-authored notes unless prax_may_edit: true is
set. When Prax hits this, it stops and asks the user to unlock.
HTTP API
All routes are prefixed /teamwork/library. TeamWork proxies them
through ../teamwork/src/teamwork/routers/library.py at the standard
/api prefix.
Tree + projects
| Method | Path | Purpose |
|---|---|---|
GET |
/library |
Full tree (projects → notebooks → notes metadata) |
POST |
/library/projects |
Create project |
GET |
/library/projects/{p} |
Project meta + progress |
PATCH |
/library/projects/{p} |
Update project meta |
DELETE |
/library/projects/{p} |
Delete empty project |
Notebooks + notes
| Method | Path | Purpose |
|---|---|---|
POST |
/library/projects/{p}/notebooks |
Create notebook |
PATCH |
/library/projects/{p}/notebooks/{n} |
Update notebook meta (sequenced, current_slug) |
POST |
/library/projects/{p}/notebooks/{n}/reorder |
Batch reorder notes |
DELETE |
/library/projects/{p}/notebooks/{n} |
Delete empty notebook |
POST |
/library/notes |
Create note (defaults to author: human — the UI is the caller) |
GET |
/library/notes/{p}/{n}/{slug} |
Read note |
PATCH |
/library/notes/{p}/{n}/{slug} |
Update note |
DELETE |
/library/notes/{p}/{n}/{slug} |
Delete note |
PATCH |
/library/notes/{p}/{n}/{slug}/move |
Move note |
PATCH |
/library/notes/{p}/{n}/{slug}/editable |
Toggle prax_may_edit |
PATCH |
/library/notes/{p}/{n}/{slug}/status |
Mark todo/done |
GET |
/library/notes/{p}/{n}/{slug}/backlinks |
Notes that wikilink to this one |
POST |
/library/notes/{p}/{n}/{slug}/refine |
Quick refine (preview, not applied) |
POST |
/library/notes/{p}/{n}/{slug}/apply-refine |
Apply approved refinement |
POST |
/library/notes/{p}/{n}/{slug}/refine-via-agent |
Full chat-agent refinement with tool access |
Schema + index + tags
| Method | Path | Purpose |
|---|---|---|
GET |
/library/schema |
LIBRARY.md content |
PUT |
/library/schema |
Overwrite LIBRARY.md |
GET |
/library/index |
INDEX.md content |
GET |
/library/tags |
Nested tag tree with counts |
GET |
/library/notes/by-tag?prefix=math/algebra |
Notes matching a tag prefix |
Raw / outputs / health
| Method | Path | Purpose |
|---|---|---|
GET |
/library/raw |
List raw captures |
POST |
/library/raw |
Capture a raw item |
GET |
/library/raw/{slug} |
Fetch a raw capture |
DELETE |
/library/raw/{slug} |
Delete a raw capture |
POST |
/library/raw/{slug}/promote |
Promote to notebook |
GET |
/library/outputs |
List generated outputs |
GET |
/library/outputs/{slug} |
Fetch an output |
POST |
/library/health-check |
Run the audit |
POST |
/library/health-check/schedule |
Schedule the audit on a recurring cron |
Kanban
| Method | Path | Purpose |
|---|---|---|
GET |
/library/projects/{p}/tasks |
List tasks (optionally ?column=...) |
POST |
/library/projects/{p}/tasks |
Create task |
GET |
/library/projects/{p}/tasks/{id} |
Full task detail |
PATCH |
/library/projects/{p}/tasks/{id} |
Update task |
DELETE |
/library/projects/{p}/tasks/{id} |
Delete task |
PATCH |
/library/projects/{p}/tasks/{id}/move |
Move to column |
POST |
/library/projects/{p}/tasks/{id}/comment |
Add comment |
GET |
/library/projects/{p}/tasks/columns |
List columns |
POST |
/library/projects/{p}/tasks/columns |
Add column |
PATCH |
/library/projects/{p}/tasks/columns/{id} |
Rename column |
DELETE |
/library/projects/{p}/tasks/columns/{id} |
Remove empty column |
POST |
/library/projects/{p}/tasks/columns/reorder |
Reorder columns |
Hugo publishing (prax/services/hugo_publishing.py)
Note and course publishing share a single Hugo site at
workspaces/{user}/courses/_site/ (historical — the URL scheme is
/courses/… for courses and /notes/… for notes). The shared
primitives live in hugo_publishing.py:
| Symbol | Purpose |
|---|---|
KATEX_HEAD, THEME_CSS |
KaTeX + Mermaid boot + theme CSS |
courses_dir(root) |
Path to the user’s courses/ directory (creates it) |
hugo_site_dir(root) |
Path to the shared Hugo site inside courses/_site/ |
ensure_hugo_site(root, base_url, site_title?) |
Create the Hugo skeleton (config + layouts) if missing |
run_hugo(site) |
Invoke the local hugo binary to build the site |
get_course_site_public_dir(user_id) |
Return the built public/ dir for a specific user |
find_course_site_public_dir(path) |
Scan all workspaces to find which one contains a given path (used by the public /courses/<path> Flask route) |
note_service.publish_notes and course_service.build_course_site
both import from this module. course_service.generate_hugo_content
stays in course_service because it’s course-specific.
course_service.py keeps # noqa: F401 re-exports of the shared
names for back-compat with code that still does
from prax.services.course_service import hugo_site_dir.
TeamWork UI (LibraryPanel.tsx)
Users reach the Library via the dedicated Library rail icon in the left navigation. The Home rail icon next to it shows active projects as cards with status pills and progress bars — click one to jump into its project view in the Library.
Sidebar
Quick-access pseudo-nodes at the top:
- Schema (LIBRARY.md) — opens the schema editor
- Index — renders the auto-maintained
INDEX.md - Raw — browses raw captures (with a nested list when selected)
- Outputs — browses generated briefings / reports / answers
- Graph — opens the force-directed graph view
- Health check — opens the audit runner
Below those: the project → notebook → note tree. Notes are draggable to move between notebooks; drop targets highlight during dragover. Clicking a project name opens the project detail view (metadata editor + Kanban); clicking a notebook name opens the notebook view (sequenced or free-form); clicking a note opens the note view.
Project detail view
- Header with title + status pill + kind tag + target date + pin toggle + edit button
- Inline metadata editor (status, kind, target date, reminder channel, description)
- Kanban board — columns with drag-and-drop between columns, inline “Add task” at the bottom of each column, column rename / delete / add, task count per column
- Task cards show the author icon, title, due date (red if overdue), reminder bell, assignee chips, comment count
- Click any card to open the task side panel
Task side panel
- Title, author, assignees, due date (with reminder toggle)
- Description (markdown)
- Checklist rendering
- Comment composer
- Full activity log with actor badges (
human/prax)
Notebook view
When a notebook has sequenced: true:
- Progress bar (done / total)
- Ordered lesson list with done checkboxes
- Current-lesson highlight
- Next lesson button
- Drag-to-reorder (updates
lesson_orderin batch) - Set current action on non-current lessons
- Sequenced toggle in the header to switch to free-form
Free-form notebooks show a sorted title list with the same controls minus the ordering.
Note view
- Author badge: emerald/user for human, indigo/sparkle for agent
- Wikilink rendering:
[[slug]],[[notebook/slug]], or[[project/notebook/slug]]patterns render as clickable indigo pills - Backlinks panel below the note body with every note that links here
- Edit / delete / move / create actions in the header
- Human-note action bar: on notes you wrote, shows
Lock / Unlock for PraxandAsk Prax to refinebuttons - Refine flow offers two paths:
- Quick refine — cheap low-tier LLM, diff preview with Apply/Cancel
- Full agent — routes through the chat agent with access to
web search, arxiv lookup, knowledge graph, and all
library_*tools; saves directly (no preview)
Raw captures browser
Clicking “Raw” expands the list of pending captures. Click any item
to view it. The main pane shows a Promote to notebook action
that opens a project + notebook picker — on promote, the raw file is
deleted and a new note is created in the target notebook with a
promoted_from field linking back to the original (and source_url
copied through if present).
Auto-capture from external channels: inbound SMS and Discord
messages containing HTTP(S) URLs are auto-captured into library/raw/
before the agent runs. Prax is told about the captured slug via a
system hint and can offer to promote it naturally. PDFs are excluded
(they have their own flow via pdf_service).
Outputs browser
Shows all generated briefings / reports / answers. Each output has a
kind tag (brief, report, answer, health-check,
news-briefing) and a generation timestamp. The daily morning
briefing and the health-check report both live here, keeping
agent-generated content out of the source-of-truth wiki.
Graph view
Force-directed graph of every note + wikilink edge:
- Forces: Coulomb-like repulsion between every node pair, spring attraction along each wikilink edge (rest length ≈ 90px), weak center gravity, velocity friction at 0.85/tick
- Termination: at most 400 iterations per re-seed, then freezes (no CPU burn on the idle view)
- Pan: drag the empty canvas
- Zoom: mouse wheel, clamped to [0.2×, 3×]
- Drag a node: pins it in place — the simulation keeps running around it but doesn’t move the pinned node itself
- Double-click a pinned node to unpin
- Unpin all button in the top-right when any node is pinned
- Click without drag (3px movement threshold) opens the note
- Project filter and author filter in the header
- Reset view button
- Emerald circles = human-authored, indigo = agent-authored
- Pinned nodes get an amber ring + small amber dot
No new dependencies — the physics sim runs in React state via
requestAnimationFrame.
Health check
Press Run now to execute the audit. The report shows:
- Static checks (no LLM): total notes, dead wikilinks, empty notebooks, orphan notes (no links in or out), short notes (<50 words) — each clickable to jump to the offending note
- LLM analysis (cheap low-tier call): contradictions between notes, unsourced claims, gap topics — each finding links to the relevant note(s)
A full markdown report is also saved to
outputs/health-check-{date}.md so you can review asynchronously.
Press Schedule in the header to open an inline form with preset
cron options (Mondays 09:00, Sundays 09:00, 1st of month 09:00,
Mon/Wed/Fri 09:00, Fridays 18:00) and a channel dropdown. The
recurring job is created via scheduler_service.create_schedule and
shows up in the Scheduler panel — you can pause, edit, or delete it
from there.
Schema editor
Click “Schema (LIBRARY.md)” in the sidebar to open a textarea bound
to LIBRARY.md. Save to overwrite. Prax re-reads LIBRARY.md at
the start of every library-related turn so human edits take effect on
the next interaction.
Why this shape
The design addresses four specific failure modes from Prax’s pre-Library era:
- Hallucinated briefings. A knowledge base that treats raw captures and synthesized wiki entries as the same thing gives the agent nothing to ground on. The raw / outputs split means synthesized content can be checked against raw sources, and the auditor can enforce “if this response cites external facts, at least one raw source must have been read.”
- Spoke over-routing to memory. The flat
notes/dir gave the model no organizing principle, so everything got routed to “memory” as a catch-all. A hierarchy with obvious human affordances makes the boundary concrete. - No sense of ownership. Before author provenance, the whole
notes system felt like it belonged to Prax — humans were reluctant
to add their own writing because they couldn’t tell what was
theirs. The
authorfield and the emerald/indigo badges make it obvious whose writing is whose. - No trust primitive for collaboration. Without
prax_may_edit, the only safe default is “Prax cannot touch anything the human wrote,” which makes the agent useless for refining or expanding the human’s notes. The opt-in flag gives humans a knob to turn on collaboration per-note, and the pending-engagement queue lets Prax proactively help when the human unlocks something.
See also
IDEAS_BACKLOG.md— broader backlog of ideas adapted from Claude customer stories + cookbook recipesinfrastructure/content-publishing.md— Hugo publishing flow for the SMS/Discord note delivery path- Andrej Karpathy’s “Second Brain” post (widely shared April 2026) for the three-folder idea and the schema-file pattern