Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
2f7b32d
refactor(mcp): remove dead Server.registry field and WithRegistry option
Koopa0 Jun 24, 2026
3a6468c
refactor: remove zero-reference dead types and sentinels
Koopa0 Jun 24, 2026
0a107df
refactor: remove zero-reference store methods and orphaned queries
Koopa0 Jun 24, 2026
d003d08
fix: catch SIGTERM for graceful shutdown
Koopa0 Jun 24, 2026
2b30778
fix: drain background workers when the listener errors
Koopa0 Jun 24, 2026
7026466
fix: map goal CHECK violation to 400 not 500
Koopa0 Jun 24, 2026
b324f2e
refactor(mcp): remove the never-supported search_knowledge project fi…
Koopa0 Jun 24, 2026
1bc4807
fix: narrow PutPlan to the plan_day state allowlist
Koopa0 Jun 24, 2026
d11f1d8
fix(content): reject control characters in admin Create/Update
Koopa0 Jun 24, 2026
e34d1e8
feat(auth): purge expired refresh tokens periodically
Koopa0 Jun 24, 2026
bf0ea19
feat(content): surface review_note in the admin detail read
Koopa0 Jun 24, 2026
8eabf9d
fix(frontend): SSR hardening — explicit transfer cache, prerender leg…
Koopa0 Jun 24, 2026
e6bbf87
fix(frontend): render an error state on the home page
Koopa0 Jun 24, 2026
c06fec8
fix(frontend): trap focus in the command palette
Koopa0 Jun 24, 2026
f7e84f1
refactor(frontend): prune 33 unused design-system components and the …
Koopa0 Jun 24, 2026
4d94c5a
test(frontend): add markdown XSS sanitization tests; drop hollow stat…
Koopa0 Jun 24, 2026
f2b18a5
test(mcp): cover validateProposeContent rejection branches + fuzz
Koopa0 Jun 24, 2026
7b45070
test(feed): replace fictional handler tests with real integration cov…
Koopa0 Jun 24, 2026
6e16837
test(stats): replace DB-mock handler tests with testcontainers integr…
Koopa0 Jun 24, 2026
e229806
test(todo): cover interval-mode recurrence arithmetic
Koopa0 Jun 24, 2026
04ed8f5
test(goal): assert a blank title returns ErrInvalidInput
Koopa0 Jun 24, 2026
287d826
test(auth): cover the refresh-token consume cycle; drop skipped stub
Koopa0 Jun 24, 2026
e8b940e
test: remove tautological tests in content and api
Koopa0 Jun 24, 2026
c240db4
docs: correct stale and wrong code comments
Koopa0 Jun 24, 2026
f0169e4
test(mcp): drop the obsolete project-filter integration test
Koopa0 Jun 24, 2026
2e3c96c
fix: surface both listener and shutdown errors on exit
Koopa0 Jun 24, 2026
7aca7e2
fix(content): reject control characters in the admin slug field
Koopa0 Jun 24, 2026
850c8a8
refactor: address review nits
Koopa0 Jun 24, 2026
9dc4751
docs(readme): correct the authz model, tool count, and missing tool
Koopa0 Jun 24, 2026
a489272
chore: regenerate sqlc after SQL comment fixes
Koopa0 Jun 24, 2026
1d00864
docs(readme): reframe the agent narrative around the active drivers
Koopa0 Jun 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 10 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,13 @@

**koopa** is a private-by-default personal OS where AI agents share a semantic runtime — so the AI reads your state, not your prompts.

It's 8 a.m. You ask for the day. The planner doesn't ask what's on your plate — it reads yesterday's unfinished daily plan, this week's goal progress, the projects that have gone quiet, and the RSS highlights the ingest pipeline collected overnight, and hands you one briefing. You skim it, set today's plan, and start. Through the day the agents stay in their lane: the planner sets the day and drafts a goal or project proposal, any agent searches the corpus, and a finished article gets pushed into your review queue — all in conversation with you. Nothing high-stakes happens behind your back: every goal, project, milestone, and published article is **your** decision, made in the admin UI. The agents surface structure; you make the call.
Three actors drive it today, and a fourth is what it's built for. **You** make every call — each goal, project, milestone, and published article is committed by you in the admin UI. **Claude Code** runs development sessions in this repo: it searches the corpus, logs what it built, and pushes a finished draft into your review queue — reading the same state any agent sees, never re-explained. **hermes** curates your Obsidian vault on a schedule. The fourth is the **planner**: a daily driver that reads yesterday's unfinished plan, this week's goal progress, the projects that have gone quiet, and the overnight RSS highlights, and hands you one morning briefing — the flow is wired and runs on an external scheduler, the piece still becoming a daily habit. Nothing high-stakes happens behind your back: the agents surface structure; you make the call.

## Why this exists

Most AI integrations are stateless: every conversation starts from zero, every agent is a fresh amnesiac, and you spend your time re-explaining context. The more agents you add — Claude Code in your editor, Cowork agents on schedulers, background summarizers — the worse it gets, because each produces output the others never see.

koopa models the work instead. Areas, goals, projects, milestones, todos, daily plans, content — all first-class entities with precise schemas and their own lifecycles, in one store every agent reads through MCP and writes through bounded workflow steps. When the planner assembles your morning briefing, it reads yesterday's daily plan and surfaces what didn't get done — not because you summarized it, but because the state is there. When it proposes a new goal, the draft lands inert in your triage queue with the milestones already laid out. Understanding is queried, not reconstructed; there is no drift between agents and no "I think you mentioned…".
koopa models the work instead. Areas, goals, projects, milestones, todos, daily plans, content — all first-class entities with precise schemas and their own lifecycles, in one store every agent reads through MCP and writes through bounded workflow steps. When an agent reads your state — Claude Code opening a session, or the planner assembling a briefing — it pulls yesterday's daily plan and goal progress through MCP, not because you summarized it, but because the state is there. When an agent proposes a new goal, the draft lands inert in your triage queue with the milestones already laid out. Understanding is queried, not reconstructed; there is no drift between agents and no "I think you mentioned…".

## How it works

Expand All @@ -46,15 +46,15 @@ The working roster (`internal/agent/registry.go::BuiltinAgents()`):

| Identity | Runs as | Role |
|---|---|---|
| `planner` | Claude Cowork | Morning briefing, candidate day plans, inbox capture, search, PARA proposals |
| `koopa0-dev` / `go-spec` | Claude Code | Development sessions in this repo |
| `codex` | Codex CLI | Dev collaborator — repo work and cross-review sessions |
| `hermes` | Claude Code (scheduled) | Curates the personal Obsidian vault on assigned cron jobs |
| `human` | — | Koopa: the only decision-maker, the only router |
| `koopa0-dev` / `go-spec` | Claude Code | Development sessions in this repo — search, build-logs, content drafts |
| `hermes` | Claude Code (scheduled) | Curates the personal Obsidian vault on assigned cron jobs |
| `planner` | Claude Cowork | The intended daily driver — morning briefing, candidate day plans, inbox capture, PARA proposals |
| `codex` | Codex CLI | Dev collaborator — repo work and cross-review sessions |

Cowork agents run on declared cadences — the planner at 8 a.m., pinned in the registry — but execution is driven by external runners, not by this repo; the backend owns the registry metadata, the schema, and the `process_runs` table that audits each external run.
The active drivers today are you, Claude Code, and hermes. The `planner` is the daily-driver the system is designed around; its cadence (a morning briefing, pinned in the registry) is **declared in the backend but executed by an external runner**, not by this repo the backend owns the registry metadata, the schema, and the `process_runs` table that audits each external run.

Writes are gated by **identity**. Every MCP call self-identifies via an `as` field; the server resolves it against the registry and applies three-axis authorization (`internal/mcp/authz.go`): an **author** allowlist (a human is always permitted), **registration** (a known, non-anonymous caller), and **self** (you may only act on your own rows). An unknown caller fails closed on every mutating tool.
Writes carry an **actor**, not a tool-layer gate. Every MCP call self-identifies via an `as` field; the server (`internal/mcp/server.go::callerIdentity`) records it as attribution — it sets `created_by`, the `activity_events` actor, and the caller-scope of an agent's own rows — but no tool checks it for permission. Access control is the MCP transport itself: the HTTP `/mcp` endpoint sits behind admin-email OAuth and a bearer token, and stdio is an OS process boundary. A fabricated `as` is caught downstream by the `created_by` foreign key to the agent roster, so an unknown caller's writes are attributed to `unknown`, never to you.

Two structural invariants hold:

Expand Down Expand Up @@ -84,7 +84,7 @@ Any agent queries the corpus through MCP via `search_knowledge` — published co

## The agent toolset

Fourteen MCP tools — small on purpose. Everything an agent can do is a workflow step with valid transitions and invariant checks, never raw table access:
Fifteen MCP tools — small on purpose. Everything an agent can do is a workflow step with valid transitions and invariant checks, never raw table access:

| Tool | What it does |
|---|---|
Expand All @@ -96,6 +96,7 @@ Fourteen MCP tools — small on purpose. Everything an agent can do is a workflo
| `plan_day` | Set today's plan as one atomic replacement. No auto-carryover. |
| `propose_area` / `propose_goal` / `propose_project` | Draft an inert PARA proposal (`status=proposed`) for you to activate or reject in admin triage. |
| `list_tasks` / `resolve_task` | Read back the disposition of the todos an agent created, and self-clear the ones it has finished. |
| `set_todo_recurrence` | Make a todo the agent created recurring — by weekday (e.g. Mon–Sat) or interval (every N days/weeks/months) — or clear it; recurring todos resurface in the brief on each matching day, computed on read. |
| `propose_content` | Push a finished content piece into the editorial review queue (`status=review`); you publish it or send it back for revision. |
| `list_content` / `revise_content` | Read back the disposition of the content an agent proposed — including your revision note when you send a draft back — and revise a sent-back draft back into review. |

Expand Down
21 changes: 11 additions & 10 deletions README.zh-TW.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@

**koopa** 是一個預設私有的個人作業系統,讓多個 AI agent 共享同一套語意運行時 — AI 讀取的是你的狀態,不是你的 prompt。

早上 8 點。你問今天怎麼安排。規劃者不會反問你手上有什麼它讀昨天未完成的 daily plan、這週的目標進度、安靜下來的 project,還有夜裡 ingest pipeline 收集的 RSS 重點,然後遞給你一份 briefing。你掃過一遍,定下今天的 plan,開始動工。一整天裡 agent 各守本分:規劃者規劃當天、起草一份 goal 或 project 提案、任何 agent 都能搜尋語料庫,完成的文章被推進你的審核佇列 — 全都在跟你的對話裡。沒有任何高風險的事在你背後發生:每一個 goal、project、milestone、發佈的文章,都是**你**的決定,在 admin UI 裡做的。Agent 浮現結構你下判斷。
今天驅動它的有三個 actor,還有它為之設計的第四個。**你**下每一個判斷每一個 goal、project、milestone、發佈的文章,都是你在 admin UI 裡 commit 的。**Claude Code** 在這個 repo 跑開發 session:搜尋語料庫、記錄它做了什麼、把完成的草稿推進你的審核佇列 — 讀的是任何 agent 都看得到的同一份狀態,不必重講。**hermes** 按排程整理你的 Obsidian vault。第四個是 **planner**:一個日常驅動者,讀昨天未完成的 plan、這週的目標進度、安靜下來的 project、還有夜裡的 RSS 重點,遞給你一份晨間 briefing — 這條流程已經接好、跑在外部 scheduler 上,是還在變成日常習慣的那一塊。沒有任何高風險的事在你背後發生:agent 浮現結構;你下判斷。

## 為什麼存在

大多數 AI 整合是無狀態的:每次對話從零開始、每個 agent 都是新鮮的失憶者,你把時間花在重複解釋脈絡。你加的 agent 越多 — 編輯器裡的 Claude Code、scheduler 上的 Cowork agent、背景跑的 summarizer — 問題就越嚴重,因為每個 agent 產出的東西,別人從來看不到。

koopa 改成把工作本身建模。Area、goal、project、milestone、todo、daily plan、content — 全都是一等公民實體,有精確的 schema 和各自的 lifecycle,存在同一份儲存裡,每個 agent 都透過 MCP 讀取,並透過有界的工作流步驟寫入。規劃者組裝晨間 briefing 時,它讀昨天的 daily plan 並浮現未完成的項目 — 不是因為你總結了,而是因為狀態就在那裡。它提一個新 goal 時,草稿帶著排好的 milestone,惰性地落進你的 triage 佇列。理解是查詢出來的,不是重建出來的;agent 之間沒有漂移,也沒有「我好像記得你提過⋯」。
koopa 改成把工作本身建模。Area、goal、project、milestone、todo、daily plan、content — 全都是一等公民實體,有精確的 schema 和各自的 lifecycle,存在同一份儲存裡,每個 agent 都透過 MCP 讀取,並透過有界的工作流步驟寫入。一個 agent 讀你的狀態時 — Claude Code 開一個 session,或 planner 組裝 briefing它透過 MCP 拉昨天的 daily plan 與目標進度,不是因為你總結了,而是因為狀態就在那裡。一個 agent 提新 goal 時,草稿帶著排好的 milestone,惰性地落進你的 triage 佇列。理解是查詢出來的,不是重建出來的;agent 之間沒有漂移,也沒有「我好像記得你提過⋯」。

## 運作方式

Expand All @@ -45,15 +45,15 @@ actor 的軸線是**流程 vs. 決策**,不是人類 vs. agent:

| 身分 | 執行環境 | 角色 |
|---|---|---|
| `planner` | Claude Cowork | 晨間 briefing、候選日計畫、inbox capture、搜尋、PARA 提案 |
| `koopa0-dev` / `go-spec` | Claude Code | 這個 repo 的開發 session |
| `codex` | Codex CLI | 開發協作者 — repo 工作與 cross-review session |
| `hermes` | Claude Code(排程) | 按指派的 cron job 整理個人 Obsidian vault |
| `human` | — | Koopa:唯一的決策者、唯一的 router |
| `koopa0-dev` / `go-spec` | Claude Code | 這個 repo 的開發 session — 搜尋、build-log、content 草稿 |
| `hermes` | Claude Code(排程) | 按指派的 cron job 整理個人 Obsidian vault |
| `planner` | Claude Cowork | 系統為之設計的日常驅動者 — 晨間 briefing、候選日計畫、inbox capture、PARA 提案 |
| `codex` | Codex CLI | 開發協作者 — repo 工作與 cross-review session |

Cowork agent 跑在宣告好的節奏上 — 規劃者早 8 點,釘在 registry 裡 — 但執行由外部 runner 驅動,不是這個 repo 自己跑的backend 持有的是 registry metadata、schema,以及記錄每次外部執行的 `process_runs` audit 表。
今天實際在驅動的是你、Claude Code 和 hermes。`planner` 是系統設計所圍繞的日常驅動者;它的節奏(一份晨間 briefing,釘在 registry 裡)**在 backend 宣告,但由外部 runner 執行**,不是這個 repo 自己跑的backend 持有的是 registry metadata、schema,以及記錄每次外部執行的 `process_runs` audit 表。

把守寫入的是**身分**。每一次 MCP call 都透過 `as` 欄位自我表明身分server 對著 registry 解析它,套用三軸授權(`internal/mcp/authz.go`):一個 **author** 白名單(人類永遠被允許)、**registration**(已知、非匿名的 caller),以及 **self**(你只能操作自己的 row)。未知的 caller 對每一個會 mutation 的工具都 fail closed
寫入帶的是 **actor**,不是 tool 層的授權閘。每一次 MCP call 都透過 `as` 欄位自我表明身分;server(`internal/mcp/server.go::callerIdentity`)把它當 attribution 記下來 — 設定 `created_by`、`activity_events` 的 actor,以及該 agent 自己 rowcaller-scope — 但沒有任何工具拿它來檢查權限。存取控制是 MCP transport 本身:HTTP `/mcp` 端點走 admin-email OAuth + bearer token,stdio 則是 OS 行程邊界。偽造的 `as` 會在下游被 `created_by` 對 agent roster 的外鍵擋下,所以未知 caller 的寫入被歸給 `unknown`,絕不會算成你

兩個結構性 invariant 成立:

Expand Down Expand Up @@ -83,7 +83,7 @@ Agent 可以把一個 raw todo 丟進你的 inbox、起草一份惰性的 area /

## Agent 工具集

十四個 MCP 工具 — 刻意做得小。agent 能做的每一件事都是一個工作流步驟,帶合法轉換與不變量檢查,絕不是原始的 table 存取:
十五個 MCP 工具 — 刻意做得小。agent 能做的每一件事都是一個工作流步驟,帶合法轉換與不變量檢查,絕不是原始的 table 存取:

| 工具 | 它做什麼 |
|---|---|
Expand All @@ -95,6 +95,7 @@ Agent 可以把一個 raw todo 丟進你的 inbox、起草一份惰性的 area /
| `plan_day` | 把今天的 plan 設定為一次 atomic 的整體替換。沒有 auto-carryover。 |
| `propose_area` / `propose_goal` / `propose_project` | 起草一份惰性的 PARA 提案(`status=proposed`),讓你在 admin triage 啟用或拒絕。 |
| `list_tasks` / `resolve_task` | 讀回 agent 建立的 todo 的處置,並自清它已處理完的。 |
| `set_todo_recurrence` | 把 agent 建立的 todo 設成循環(週幾型如 Mon–Sat,或間隔型每 N 天/週/月)或清掉;循環 todo 每逢符合的日子在 brief 重新浮現,compute-on-read。 |
| `propose_content` | 把完成的內容推進 editorial 審核佇列(`status=review`);由你 publish 或退回要求修改。 |
| `list_content` / `revise_content` | 讀回 agent 提的內容的處置 — 包含你退件時寫的修改原因 — 並把被退回的稿子改好、送回 review。 |

Expand Down Expand Up @@ -123,7 +124,7 @@ Agent 可以把一個 raw todo 丟進你的 inbox、起草一份惰性的 area /
| Embedding | `gemini-embedding-2`(1536d Matryoshka);背景 reconciler 維持搜尋語料庫的 embedding 最新 |
| 排程 | Agent 節奏在 `internal/agent/registry.go` 宣告;執行由外部 Cowork/Desktop runner 驅動;以 `process_runs` 留 audit |
| 前端 | Angular 22(SSR、zoneless、Signal Forms)、Tailwind CSS v4 |
| AI 協作 | Claude(Cowork + Code)、Codex CLI、MCP(14 個工作流工具) |
| AI 協作 | Claude(Cowork + Code)、Codex CLI、MCP(15 個工作流工具) |
| Cache | Ristretto(in-memory,單機) |
| Object 儲存 | Cloudflare R2(S3 相容) |

Expand Down
Loading
Loading