symbion 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. symbion-0.1.0/LICENSE +21 -0
  2. symbion-0.1.0/MANIFEST.in +2 -0
  3. symbion-0.1.0/PKG-INFO +333 -0
  4. symbion-0.1.0/README.md +313 -0
  5. symbion-0.1.0/pyproject.toml +39 -0
  6. symbion-0.1.0/setup.cfg +4 -0
  7. symbion-0.1.0/src/symbion/__init__.py +0 -0
  8. symbion-0.1.0/src/symbion/api.py +625 -0
  9. symbion-0.1.0/src/symbion/catalog.py +273 -0
  10. symbion-0.1.0/src/symbion/cli.py +1892 -0
  11. symbion-0.1.0/src/symbion/config.py +223 -0
  12. symbion-0.1.0/src/symbion/data/skill/SKILL.md +218 -0
  13. symbion-0.1.0/src/symbion/data/skill/adoption.md +154 -0
  14. symbion-0.1.0/src/symbion/data/skill/catalogs.md +74 -0
  15. symbion-0.1.0/src/symbion/data/skill/session_start.sh +59 -0
  16. symbion-0.1.0/src/symbion/gitref.py +270 -0
  17. symbion-0.1.0/src/symbion/gui/__init__.py +0 -0
  18. symbion-0.1.0/src/symbion/gui/arcs.py +119 -0
  19. symbion-0.1.0/src/symbion/gui/chrome.py +91 -0
  20. symbion-0.1.0/src/symbion/gui/filters.py +84 -0
  21. symbion-0.1.0/src/symbion/gui/notes.py +410 -0
  22. symbion-0.1.0/src/symbion/gui/pages.py +282 -0
  23. symbion-0.1.0/src/symbion/gui/serve.py +75 -0
  24. symbion-0.1.0/src/symbion/gui/theme.py +182 -0
  25. symbion-0.1.0/src/symbion/kinds.py +101 -0
  26. symbion-0.1.0/src/symbion/store.py +1232 -0
  27. symbion-0.1.0/src/symbion/summary.py +540 -0
  28. symbion-0.1.0/src/symbion/term.py +260 -0
  29. symbion-0.1.0/src/symbion.egg-info/PKG-INFO +333 -0
  30. symbion-0.1.0/src/symbion.egg-info/SOURCES.txt +57 -0
  31. symbion-0.1.0/src/symbion.egg-info/dependency_links.txt +1 -0
  32. symbion-0.1.0/src/symbion.egg-info/entry_points.txt +2 -0
  33. symbion-0.1.0/src/symbion.egg-info/requires.txt +9 -0
  34. symbion-0.1.0/src/symbion.egg-info/top_level.txt +1 -0
  35. symbion-0.1.0/tests/_resolvers.py +53 -0
  36. symbion-0.1.0/tests/conftest.py +101 -0
  37. symbion-0.1.0/tests/test_api.py +863 -0
  38. symbion-0.1.0/tests/test_arcs.py +190 -0
  39. symbion-0.1.0/tests/test_author.py +76 -0
  40. symbion-0.1.0/tests/test_catalog.py +291 -0
  41. symbion-0.1.0/tests/test_cli.py +3949 -0
  42. symbion-0.1.0/tests/test_concurrency.py +63 -0
  43. symbion-0.1.0/tests/test_config.py +312 -0
  44. symbion-0.1.0/tests/test_filters.py +103 -0
  45. symbion-0.1.0/tests/test_gitref.py +360 -0
  46. symbion-0.1.0/tests/test_gui_arcs.py +176 -0
  47. symbion-0.1.0/tests/test_gui_notes.py +685 -0
  48. symbion-0.1.0/tests/test_gui_pages.py +438 -0
  49. symbion-0.1.0/tests/test_gui_seam.py +212 -0
  50. symbion-0.1.0/tests/test_gui_serve.py +68 -0
  51. symbion-0.1.0/tests/test_gui_theme.py +80 -0
  52. symbion-0.1.0/tests/test_hook.py +261 -0
  53. symbion-0.1.0/tests/test_init.py +303 -0
  54. symbion-0.1.0/tests/test_kinds.py +95 -0
  55. symbion-0.1.0/tests/test_reconcile.py +173 -0
  56. symbion-0.1.0/tests/test_schema.py +119 -0
  57. symbion-0.1.0/tests/test_store.py +654 -0
  58. symbion-0.1.0/tests/test_summary.py +762 -0
  59. symbion-0.1.0/tests/test_summary_predicates.py +75 -0
symbion-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 phreakocious
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,2 @@
1
+ graft tests
2
+ global-exclude *.py[cod]
symbion-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,333 @@
1
+ Metadata-Version: 2.4
2
+ Name: symbion
3
+ Version: 0.1.0
4
+ Summary: A notebook and ticket registry for agents: dated rows attached to a commit, a file, an item, an arc or the project, kept in a sibling git repo of JSONL.
5
+ Author: phreakocious
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/phreakocious/symbion
8
+ Project-URL: Issues, https://github.com/phreakocious/symbion/issues
9
+ Requires-Python: >=3.11
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: rich>=13
13
+ Requires-Dist: rich-argparse>=1.5
14
+ Provides-Extra: gui
15
+ Requires-Dist: nicegui>=3.0; extra == "gui"
16
+ Provides-Extra: test
17
+ Requires-Dist: pytest; extra == "test"
18
+ Requires-Dist: pytest-asyncio; extra == "test"
19
+ Dynamic: license-file
20
+
21
+ # symbion
22
+
23
+ A per-project notebook and ticket registry for small projects, driven by a
24
+ human and a coding agent through one CLI. The CLI works on its own; the agent
25
+ surface (a skill and a SessionStart hook) is written for Claude Code.
26
+
27
+ A note is dated, attributed, retractable, and attached to something stable: a
28
+ commit, a file, a free-form item, or the project as a whole. Seven kinds by
29
+ default — `check` (a dated verification), `decision` (an ADR), `bug` (known
30
+ broken), `task` (a next action), `question` (needs the owner's answer), `idea`
31
+ (parked), `note` (everything else) — and a project may declare its own labels
32
+ in `symbion.toml` under `[kinds]`; every kind is a label on three bits
33
+ (`status`, `parked`, `verdict`), and `symbion schema` prints the table. An
34
+ **arc** is a named line of work — an epic, if you like — rendered as a
35
+ per-object checklist with its decisions and notes filed under it. The store is
36
+ a private sibling git repo of JSONL, so checking out an old branch never
37
+ time-travels your tickets.
38
+
39
+ Design and rationale: [`docs/superpowers/specs/2026-09-04-symbion-design.md`](https://github.com/phreakocious/symbion/blob/main/docs/superpowers/specs/2026-09-04-symbion-design.md).
40
+ The agent-facing surface, which is also the best command reference:
41
+ `src/symbion/data/skill/SKILL.md` (linked at `~/.claude/skills/symbion`).
42
+
43
+ ## Install
44
+
45
+ Python 3.11+ and git. The core needs two packages, `rich` and `rich-argparse`,
46
+ for a person at a terminal: `list`, `show` and `context` as aligned, coloured
47
+ columns with bodies rendered as markdown, and every other command and `--help`
48
+ in the same colours. A pipe gets plain text. One optional extra, `gui`, adds the web UI
49
+ (`symbion serve`):
50
+
51
+ ```bash
52
+ pipx install 'symbion[gui]'
53
+ # or: uv tool install 'symbion[gui]'
54
+ # drop [gui] for the core alone; for the unreleased main branch:
55
+ # pipx install 'symbion[gui] @ git+https://github.com/phreakocious/symbion'
56
+ ```
57
+
58
+ To work on symbion itself, install a clone editable:
59
+
60
+ ```bash
61
+ pipx install -e /path/to/symbion # or: uv tool install -e /path/to/symbion
62
+ ```
63
+
64
+ Without pipx or uv, install into a venv and put the console script on PATH:
65
+
66
+ ```bash
67
+ cd /path/to/symbion
68
+ python3 -m venv .venv && .venv/bin/pip install -e .
69
+ ln -s "$PWD/.venv/bin/symbion" ~/.local/bin/symbion # or any directory on your PATH
70
+ ```
71
+
72
+ Verify from any other directory:
73
+
74
+ ```bash
75
+ command -v symbion
76
+ ```
77
+
78
+ This matters more than it looks. The SessionStart hook below finds `symbion`
79
+ through PATH. With a venv-only install the hook prints only `symbion: store
80
+ exists at … but 'symbion' is not on PATH` where a store exists, and nothing
81
+ where none does: easy to read past as a project that never adopted symbion.
82
+
83
+ ## Adopt in a fresh repo
84
+
85
+ Run from the repo's root.
86
+
87
+ **1. Create the store and link the agent files.**
88
+
89
+ ```bash
90
+ symbion init
91
+ ```
92
+
93
+ This creates `../<repo>-notes` (a git repo, no remote) with an annotated
94
+ `symbion.toml`; nothing requires the toml, every value has a default. The
95
+ first `init` on a machine also links `~/.claude/skills/symbion` to the skill
96
+ directory inside the installed package (SKILL.md, adoption.md, catalogs.md
97
+ and the SessionStart hook script), so an upgrade reaches every project with nothing
98
+ to refresh, and registers the hook in `~/.claude/settings.json` when that file
99
+ does not exist. If it exists, `init` reads its `hooks.SessionStart` and says
100
+ which of the three it found: `kept hook in ...`, the block to add instead of
101
+ merging, or that the file is not readable as JSON so neither answer holds. A `~/.claude/skills/symbion` that is not that link is left alone and
102
+ named. Nothing is written into the project except a `.symbion` pointer when
103
+ the store is not the default sibling. The hook runs in every project and says
104
+ nothing in one without a store.
105
+
106
+ **2. Check the read side works.** Start a session, or run the hook by hand:
107
+
108
+ ```bash
109
+ CLAUDE_PROJECT_DIR=$PWD bash ~/.claude/skills/symbion/session_start.sh
110
+ ```
111
+
112
+ `init` marks a `.symbion` pointer it writes `(git: ignored)` or `(git: not
113
+ ignored)`; one marked not ignored goes in with the next `git add -A`.
114
+
115
+ | output | meaning |
116
+ |---|---|
117
+ | `symbion: open outside arcs: bug 0, task 0, question 0` (and `; N open in arcs` when arcs hold open rows), then one line per arc (`id done/total age-in-days name`) and the open heads | working |
118
+ | `symbion --dir ../other-notes: open outside arcs: …` | the same, for a store this repo's tree does not name (`--dir`, `SYMBION_DIR`, or the cwd inside a store); the header carries the flag that reaches it, so two summaries read in one session are told apart |
119
+ | the same, then ` N notes not yet in the store's git (symbion commit)` | working; `symbion commit` when the session ends |
120
+ | the same, then ` N per-project symbion copies from an older init: …` | an `init` from before the user-level link left the skill, the hook or its registration in this project; remove them (the link replaces them) |
121
+ | `symbion: store exists at … but 'symbion' is not on PATH` | fix Install |
122
+ | `symbion: no store at …; run \`symbion init\`` | no store yet, or `.symbion`/`SYMBION_DIR` names a path that does not exist |
123
+ | nothing | no store, no `.symbion` pointer and no `SYMBION_DIR` (a project that never adopted symbion), or the hook is not registered in `~/.claude/settings.json` |
124
+
125
+ **3. Tell the agent.** One line in `CLAUDE.md`, for example:
126
+
127
+ ```
128
+ - We use symbion for durable notes and tickets. Surface friction with it so it can be addressed.
129
+ ```
130
+ If that repo's `CLAUDE.md` is gitignored or its owner reviews every change to
131
+ it, propose the line to the owner instead of writing it.
132
+
133
+ ## First session
134
+
135
+ A repo that already holds known issues, TODOs, docs or a handoff file has
136
+ history: read `adoption.md` (installed beside `SKILL.md`) before the first
137
+ row, since it says which catalogs to turn on and what to bring in. Either way:
138
+
139
+ **The first note is a check, and it goes in now.** It is the kind that earns
140
+ its place fastest: dated, re-checkable, and it goes stale visibly. A check
141
+ stamps HEAD and whether the tree was dirty, and untracked files count, so on
142
+ a dirty tree (the CLAUDE.md edit from step 3, or any untracked file) it reads
143
+ `state=unverifiable (dirty tree)`. That is honest, not blocked: commit, write
144
+ it again, and it reads `current`. Files the checked command writes count too:
145
+ `pytest` leaves `__pycache__/`, so git-ignore those first or the second write
146
+ reads dirty again. A check on something outside the repo (DNS, a host)
147
+ takes `--external` and lists its age instead. A bootstrap that ends with no
148
+ check row skipped the step that pays first.
149
+
150
+ ```bash
151
+ symbion add --kind check --type commit --name HEAD --checked "pytest -q" --result "147 passed"
152
+ symbion list --kind check # unverifiable (dirty tree) on a dirty tree; current on a clean one; behind N once HEAD moves
153
+ ```
154
+
155
+ **The first checklist is an arc over free-form items.** Use `add` per
156
+ item, not `seed`. `seed` mints one bodiless task per name and exists for
157
+ fanning out over a catalog; a hand-picked list wants a body on each ticket.
158
+
159
+ ```bash
160
+ aid=$(symbion arc create --name "Adoption" --scope item --desc "Make this usable by someone who was not in the room.")
161
+ symbion add --kind task --type item --name "write the README" --arc-id "$aid" --body "What done looks like: a stranger can install and record a note."
162
+ symbion arc todo "$aid" --json # open items, same rows as `list --json`
163
+ symbion resolve <id> # tick the box
164
+ symbion arc list # done/total per arc
165
+ symbion list --arc "$aid" --json # every item, resolved included
166
+ ```
167
+
168
+ **Commit the store at the end of a session.** Writes never auto-commit, so a
169
+ git failure can never block a note. `symbion summary` shows the uncommitted
170
+ count until you do.
171
+
172
+ ```bash
173
+ symbion commit -m "adoption: README ticket closed"
174
+ ```
175
+
176
+ **Adopting a repo with history?** The store starts empty and the project does
177
+ not. `adoption.md`, installed beside `SKILL.md`, is the procedure, written
178
+ from the adoptions it cites. The rows it produces go in as one `symbion add
179
+ --from-json -`, one JSON object per line, validated together before any is
180
+ written.
181
+
182
+ ## Browse it (optional)
183
+
184
+ ```bash
185
+ symbion serve # needs the gui extra: see Install
186
+ ```
187
+
188
+ A local page at `http://127.0.0.1:43210` (another free port when that one is
189
+ busy; `serve` prints the address, and `--port` picks one): boards, a filtered note list where
190
+ every tag/kind/author/target chip is a link, a search box (`/` focuses it; every
191
+ word, in any case, taken literally, or a pasted note id), a page per note with
192
+ its earlier versions, a tag index, and arc checklists you tick. It writes as **you** — `SYMBION_AUTHOR`, else
193
+ `git user.name` — never as `claude`, and shows the resolved name in the top
194
+ bar. `--author NAME` overrides.
195
+
196
+ `serve` is the only thing that needs the extra.
197
+
198
+ ## Catalogs, when a homogeneous set exists
199
+
200
+ A catalog is a shell command that prints one name per line and nothing else.
201
+ Declare it in `../<repo>-notes/symbion.toml`:
202
+
203
+ ```toml
204
+ [catalogs]
205
+ file = "git ls-files --cached --others --exclude-standard '*.py'"
206
+ test = "git ls-files --cached --others --exclude-standard 'tests/test_*.py'"
207
+
208
+ [renames]
209
+ file = "git" # so reconcile can follow renamed files
210
+ ```
211
+
212
+ Each key becomes a target type: `--type file --name src/x.py`. An arc
213
+ seeded over it gets one task per member, and `arc reconcile` reports
214
+ which are live, renamed, or stale as the tree changes.
215
+
216
+ Scope the command to where notes will attach: no pathspec when docs and
217
+ results matter, a glob when only code does. A broad catalog costs nothing
218
+ except a refusal on an ambiguous substring. `--others --exclude-standard`
219
+ adds files not yet committed: without it, a note on a file made this session
220
+ warns `taken as typed` on a correct name, and that warning is the only thing
221
+ that catches a typo.
222
+
223
+ **Always dry-run the first seed of any catalog type.** A command's output
224
+ depends on the tool's version and on the project's own config; the only way
225
+ to know what it prints is to run it here, today.
226
+
227
+ ```bash
228
+ symbion arc seed --scope file --dry-run # no arc needed; writes nothing
229
+ ```
230
+
231
+ Names resolve exact, then unique substring, else verbatim. `cli.py` is
232
+ ambiguous between `src/symbion/cli.py` and `tests/test_cli.py` and is refused;
233
+ use the full path.
234
+
235
+ A catalog type whose names are **measured** (a reading, a serial) needs a
236
+ resolver too, or a near-miss mints a second target: substring matching cannot
237
+ see that `3.1416` is `3.14159`. So does every **store-derived** catalog
238
+ (`symbion list … --type X | jq …`): it is empty until its first `X` row
239
+ exists, and that row cannot be written against an empty catalog, so the first
240
+ `add` is refused until a resolver decides what a first sighting stores.
241
+ Declare one beside the catalog:
242
+
243
+ ```toml
244
+ [resolvers]
245
+ reading = "python3 tools/resolve_reading.py" # stdin: query, then names; exit 2 = ambiguous
246
+ ```
247
+
248
+ The starter `symbion.toml` carries the full protocol. A failing resolver
249
+ stores nothing; it never falls back to the substring rule.
250
+
251
+ Within one write (a batch `add --from-json`, an `arc seed --name …`), names
252
+ already resolved by earlier rows are candidates for later rows, resolver or
253
+ not — so a batch that names `src/parser.py` and then `p` against a catalog
254
+ holding `src/pipe.py` is refused as ambiguous where two separate adds would
255
+ store both.
256
+
257
+ ## Conventions
258
+
259
+ - **Bodies are markdown.** `list --full`, `show` and `context` print them as
260
+ written on a pipe and render them on a terminal; a plain `list` page and
261
+ `summary` flatten each to one clipped line.
262
+ - **A terminal and a pipe get different text.** A terminal shows each row as
263
+ columns: the id's last 10 characters, its age, a `○`/`✓` status mark, the
264
+ kind, the target, its tags and a count of its refs, then one body line cut to
265
+ the terminal's width. `show` takes that id tail. A pipe, which is what
266
+ scripts and agents read, always gets the plain line.
267
+ - **`idea` is a kind, and parked means parked.** `add --kind idea --type project
268
+ --body …` shelves a thought; it never prints in `arc todo` or `context`, and
269
+ in `summary` only when it carries a `--due` date that is near or past (how a
270
+ thought comes back on a date) or a person other than the reader wrote it
271
+ (`from <author>`). Close it with `resolve <id> --add-tag adopted --body "…"` or
272
+ `--add-tag retired`. `list --kind idea` is the shelf; `list --tag idea` still
273
+ finds rows written before it was a kind.
274
+ - **Kinds are the project's.** Rename or add a label in `symbion.toml`
275
+ `[kinds]`; a label that already has rows also needs a one-line `sed` over
276
+ `notes.jsonl` (`init` writes the recipe into the toml). `status` +
277
+ `verdict` is a pre-registration: `resolve --result` closes it.
278
+ - **`priority` tag.** `symbion supersede <id> --add-tag priority` stars a note,
279
+ whoever writes it, so the next session's summary lists it under `priority`
280
+ until the row is resolved or `--rm-tag priority` takes the star off.
281
+ - **Due dates.** `--due 2026-10-01` (or an ISO datetime) on any status row.
282
+ Past due or due within 7 days, an open row leads the session-start summary
283
+ as `overdue 2d` or `due in 3d`, in an arc or not; `list --overdue` lists
284
+ the ones past due, and `supersede <id> --due ''` clears one.
285
+ - **Author.** Inside a Claude Code session the author is `claude`; otherwise
286
+ git `user.name`. `--author` or `SYMBION_AUTHOR` override. A human typing
287
+ `! symbion add …` inside a session is recorded as `claude` without one.
288
+ An agent's session start lists open rows a person wrote, parked ones
289
+ included, as `from <author>`: the owner's word reaches the next session.
290
+ - **Close done work as a resolved task**, not by deleting the ticket.
291
+ `add --kind task --status resolved --body "done: …"` records work that
292
+ was finished before it was ever ticketed.
293
+
294
+ ## Where things live
295
+
296
+ - **Store:** `../<repo>-notes`, beside the *main* worktree. Linked worktrees
297
+ share it. `SYMBION_DIR` or `--dir PATH` override; `--dir` may sit anywhere
298
+ in the argv. A `<repo>-notes` store named from another repo resolves in
299
+ `<repo>`, and says so on stderr. So does a command run from inside the
300
+ store itself (to edit `symbion.toml`, say): the store is the one you are
301
+ in, and `init` there refuses.
302
+ - **A store that is not named after the repo:** put its path in a
303
+ `.symbion` file in the repo root — one line, relative to the repo or
304
+ absolute, first non-blank line wins. `symbion --dir ../other-notes init`
305
+ writes it for you. Commit it; a relative pointer survives a clone.
306
+ This knob cannot live in `symbion.toml`, because that file is inside the
307
+ store you are trying to find.
308
+
309
+ It is worth setting whenever the two names differ: rename a repo directory
310
+ and every command reports `no store at ../<new-name>-notes` (the old store
311
+ is still beside the old name) until a `.symbion` file names it. A write
312
+ refuses too; only `symbion init` creates a store.
313
+ - **Files:** `notes.jsonl` (append-only; corrections are new rows that
314
+ supersede old ones), `arcs.jsonl`, `symbion.toml`.
315
+ - **Reading:** `symbion summary` (what the hook prints), `symbion context
316
+ --target TYPE:NAME`, `symbion context --branch REF`, `symbion list --json`.
317
+ Every command that prints rows takes `--json` (`tags` prints counts and
318
+ does not).
319
+
320
+ ## Development
321
+
322
+ ```bash
323
+ python3 -m venv .venv && .venv/bin/pip install -e '.[test,gui]'
324
+ .venv/bin/python -m pytest -q
325
+ ```
326
+
327
+ The specs are dated design records: why each decision was made, and the design as of that date. `SKILL.md` is
328
+ what an agent reads; keep it short and keep its footgun list honest. It lives
329
+ at `src/symbion/data/skill/SKILL.md`, beside `adoption.md` and the hook. If
330
+ this clone is your installed symbion (an editable install, then `symbion
331
+ init`), `~/.claude/skills/symbion` links to that directory: an edit reaches
332
+ every session on the machine at once, the same way an edit to `src/` reaches
333
+ every `symbion` call.
@@ -0,0 +1,313 @@
1
+ # symbion
2
+
3
+ A per-project notebook and ticket registry for small projects, driven by a
4
+ human and a coding agent through one CLI. The CLI works on its own; the agent
5
+ surface (a skill and a SessionStart hook) is written for Claude Code.
6
+
7
+ A note is dated, attributed, retractable, and attached to something stable: a
8
+ commit, a file, a free-form item, or the project as a whole. Seven kinds by
9
+ default — `check` (a dated verification), `decision` (an ADR), `bug` (known
10
+ broken), `task` (a next action), `question` (needs the owner's answer), `idea`
11
+ (parked), `note` (everything else) — and a project may declare its own labels
12
+ in `symbion.toml` under `[kinds]`; every kind is a label on three bits
13
+ (`status`, `parked`, `verdict`), and `symbion schema` prints the table. An
14
+ **arc** is a named line of work — an epic, if you like — rendered as a
15
+ per-object checklist with its decisions and notes filed under it. The store is
16
+ a private sibling git repo of JSONL, so checking out an old branch never
17
+ time-travels your tickets.
18
+
19
+ Design and rationale: [`docs/superpowers/specs/2026-09-04-symbion-design.md`](https://github.com/phreakocious/symbion/blob/main/docs/superpowers/specs/2026-09-04-symbion-design.md).
20
+ The agent-facing surface, which is also the best command reference:
21
+ `src/symbion/data/skill/SKILL.md` (linked at `~/.claude/skills/symbion`).
22
+
23
+ ## Install
24
+
25
+ Python 3.11+ and git. The core needs two packages, `rich` and `rich-argparse`,
26
+ for a person at a terminal: `list`, `show` and `context` as aligned, coloured
27
+ columns with bodies rendered as markdown, and every other command and `--help`
28
+ in the same colours. A pipe gets plain text. One optional extra, `gui`, adds the web UI
29
+ (`symbion serve`):
30
+
31
+ ```bash
32
+ pipx install 'symbion[gui]'
33
+ # or: uv tool install 'symbion[gui]'
34
+ # drop [gui] for the core alone; for the unreleased main branch:
35
+ # pipx install 'symbion[gui] @ git+https://github.com/phreakocious/symbion'
36
+ ```
37
+
38
+ To work on symbion itself, install a clone editable:
39
+
40
+ ```bash
41
+ pipx install -e /path/to/symbion # or: uv tool install -e /path/to/symbion
42
+ ```
43
+
44
+ Without pipx or uv, install into a venv and put the console script on PATH:
45
+
46
+ ```bash
47
+ cd /path/to/symbion
48
+ python3 -m venv .venv && .venv/bin/pip install -e .
49
+ ln -s "$PWD/.venv/bin/symbion" ~/.local/bin/symbion # or any directory on your PATH
50
+ ```
51
+
52
+ Verify from any other directory:
53
+
54
+ ```bash
55
+ command -v symbion
56
+ ```
57
+
58
+ This matters more than it looks. The SessionStart hook below finds `symbion`
59
+ through PATH. With a venv-only install the hook prints only `symbion: store
60
+ exists at … but 'symbion' is not on PATH` where a store exists, and nothing
61
+ where none does: easy to read past as a project that never adopted symbion.
62
+
63
+ ## Adopt in a fresh repo
64
+
65
+ Run from the repo's root.
66
+
67
+ **1. Create the store and link the agent files.**
68
+
69
+ ```bash
70
+ symbion init
71
+ ```
72
+
73
+ This creates `../<repo>-notes` (a git repo, no remote) with an annotated
74
+ `symbion.toml`; nothing requires the toml, every value has a default. The
75
+ first `init` on a machine also links `~/.claude/skills/symbion` to the skill
76
+ directory inside the installed package (SKILL.md, adoption.md, catalogs.md
77
+ and the SessionStart hook script), so an upgrade reaches every project with nothing
78
+ to refresh, and registers the hook in `~/.claude/settings.json` when that file
79
+ does not exist. If it exists, `init` reads its `hooks.SessionStart` and says
80
+ which of the three it found: `kept hook in ...`, the block to add instead of
81
+ merging, or that the file is not readable as JSON so neither answer holds. A `~/.claude/skills/symbion` that is not that link is left alone and
82
+ named. Nothing is written into the project except a `.symbion` pointer when
83
+ the store is not the default sibling. The hook runs in every project and says
84
+ nothing in one without a store.
85
+
86
+ **2. Check the read side works.** Start a session, or run the hook by hand:
87
+
88
+ ```bash
89
+ CLAUDE_PROJECT_DIR=$PWD bash ~/.claude/skills/symbion/session_start.sh
90
+ ```
91
+
92
+ `init` marks a `.symbion` pointer it writes `(git: ignored)` or `(git: not
93
+ ignored)`; one marked not ignored goes in with the next `git add -A`.
94
+
95
+ | output | meaning |
96
+ |---|---|
97
+ | `symbion: open outside arcs: bug 0, task 0, question 0` (and `; N open in arcs` when arcs hold open rows), then one line per arc (`id done/total age-in-days name`) and the open heads | working |
98
+ | `symbion --dir ../other-notes: open outside arcs: …` | the same, for a store this repo's tree does not name (`--dir`, `SYMBION_DIR`, or the cwd inside a store); the header carries the flag that reaches it, so two summaries read in one session are told apart |
99
+ | the same, then ` N notes not yet in the store's git (symbion commit)` | working; `symbion commit` when the session ends |
100
+ | the same, then ` N per-project symbion copies from an older init: …` | an `init` from before the user-level link left the skill, the hook or its registration in this project; remove them (the link replaces them) |
101
+ | `symbion: store exists at … but 'symbion' is not on PATH` | fix Install |
102
+ | `symbion: no store at …; run \`symbion init\`` | no store yet, or `.symbion`/`SYMBION_DIR` names a path that does not exist |
103
+ | nothing | no store, no `.symbion` pointer and no `SYMBION_DIR` (a project that never adopted symbion), or the hook is not registered in `~/.claude/settings.json` |
104
+
105
+ **3. Tell the agent.** One line in `CLAUDE.md`, for example:
106
+
107
+ ```
108
+ - We use symbion for durable notes and tickets. Surface friction with it so it can be addressed.
109
+ ```
110
+ If that repo's `CLAUDE.md` is gitignored or its owner reviews every change to
111
+ it, propose the line to the owner instead of writing it.
112
+
113
+ ## First session
114
+
115
+ A repo that already holds known issues, TODOs, docs or a handoff file has
116
+ history: read `adoption.md` (installed beside `SKILL.md`) before the first
117
+ row, since it says which catalogs to turn on and what to bring in. Either way:
118
+
119
+ **The first note is a check, and it goes in now.** It is the kind that earns
120
+ its place fastest: dated, re-checkable, and it goes stale visibly. A check
121
+ stamps HEAD and whether the tree was dirty, and untracked files count, so on
122
+ a dirty tree (the CLAUDE.md edit from step 3, or any untracked file) it reads
123
+ `state=unverifiable (dirty tree)`. That is honest, not blocked: commit, write
124
+ it again, and it reads `current`. Files the checked command writes count too:
125
+ `pytest` leaves `__pycache__/`, so git-ignore those first or the second write
126
+ reads dirty again. A check on something outside the repo (DNS, a host)
127
+ takes `--external` and lists its age instead. A bootstrap that ends with no
128
+ check row skipped the step that pays first.
129
+
130
+ ```bash
131
+ symbion add --kind check --type commit --name HEAD --checked "pytest -q" --result "147 passed"
132
+ symbion list --kind check # unverifiable (dirty tree) on a dirty tree; current on a clean one; behind N once HEAD moves
133
+ ```
134
+
135
+ **The first checklist is an arc over free-form items.** Use `add` per
136
+ item, not `seed`. `seed` mints one bodiless task per name and exists for
137
+ fanning out over a catalog; a hand-picked list wants a body on each ticket.
138
+
139
+ ```bash
140
+ aid=$(symbion arc create --name "Adoption" --scope item --desc "Make this usable by someone who was not in the room.")
141
+ symbion add --kind task --type item --name "write the README" --arc-id "$aid" --body "What done looks like: a stranger can install and record a note."
142
+ symbion arc todo "$aid" --json # open items, same rows as `list --json`
143
+ symbion resolve <id> # tick the box
144
+ symbion arc list # done/total per arc
145
+ symbion list --arc "$aid" --json # every item, resolved included
146
+ ```
147
+
148
+ **Commit the store at the end of a session.** Writes never auto-commit, so a
149
+ git failure can never block a note. `symbion summary` shows the uncommitted
150
+ count until you do.
151
+
152
+ ```bash
153
+ symbion commit -m "adoption: README ticket closed"
154
+ ```
155
+
156
+ **Adopting a repo with history?** The store starts empty and the project does
157
+ not. `adoption.md`, installed beside `SKILL.md`, is the procedure, written
158
+ from the adoptions it cites. The rows it produces go in as one `symbion add
159
+ --from-json -`, one JSON object per line, validated together before any is
160
+ written.
161
+
162
+ ## Browse it (optional)
163
+
164
+ ```bash
165
+ symbion serve # needs the gui extra: see Install
166
+ ```
167
+
168
+ A local page at `http://127.0.0.1:43210` (another free port when that one is
169
+ busy; `serve` prints the address, and `--port` picks one): boards, a filtered note list where
170
+ every tag/kind/author/target chip is a link, a search box (`/` focuses it; every
171
+ word, in any case, taken literally, or a pasted note id), a page per note with
172
+ its earlier versions, a tag index, and arc checklists you tick. It writes as **you** — `SYMBION_AUTHOR`, else
173
+ `git user.name` — never as `claude`, and shows the resolved name in the top
174
+ bar. `--author NAME` overrides.
175
+
176
+ `serve` is the only thing that needs the extra.
177
+
178
+ ## Catalogs, when a homogeneous set exists
179
+
180
+ A catalog is a shell command that prints one name per line and nothing else.
181
+ Declare it in `../<repo>-notes/symbion.toml`:
182
+
183
+ ```toml
184
+ [catalogs]
185
+ file = "git ls-files --cached --others --exclude-standard '*.py'"
186
+ test = "git ls-files --cached --others --exclude-standard 'tests/test_*.py'"
187
+
188
+ [renames]
189
+ file = "git" # so reconcile can follow renamed files
190
+ ```
191
+
192
+ Each key becomes a target type: `--type file --name src/x.py`. An arc
193
+ seeded over it gets one task per member, and `arc reconcile` reports
194
+ which are live, renamed, or stale as the tree changes.
195
+
196
+ Scope the command to where notes will attach: no pathspec when docs and
197
+ results matter, a glob when only code does. A broad catalog costs nothing
198
+ except a refusal on an ambiguous substring. `--others --exclude-standard`
199
+ adds files not yet committed: without it, a note on a file made this session
200
+ warns `taken as typed` on a correct name, and that warning is the only thing
201
+ that catches a typo.
202
+
203
+ **Always dry-run the first seed of any catalog type.** A command's output
204
+ depends on the tool's version and on the project's own config; the only way
205
+ to know what it prints is to run it here, today.
206
+
207
+ ```bash
208
+ symbion arc seed --scope file --dry-run # no arc needed; writes nothing
209
+ ```
210
+
211
+ Names resolve exact, then unique substring, else verbatim. `cli.py` is
212
+ ambiguous between `src/symbion/cli.py` and `tests/test_cli.py` and is refused;
213
+ use the full path.
214
+
215
+ A catalog type whose names are **measured** (a reading, a serial) needs a
216
+ resolver too, or a near-miss mints a second target: substring matching cannot
217
+ see that `3.1416` is `3.14159`. So does every **store-derived** catalog
218
+ (`symbion list … --type X | jq …`): it is empty until its first `X` row
219
+ exists, and that row cannot be written against an empty catalog, so the first
220
+ `add` is refused until a resolver decides what a first sighting stores.
221
+ Declare one beside the catalog:
222
+
223
+ ```toml
224
+ [resolvers]
225
+ reading = "python3 tools/resolve_reading.py" # stdin: query, then names; exit 2 = ambiguous
226
+ ```
227
+
228
+ The starter `symbion.toml` carries the full protocol. A failing resolver
229
+ stores nothing; it never falls back to the substring rule.
230
+
231
+ Within one write (a batch `add --from-json`, an `arc seed --name …`), names
232
+ already resolved by earlier rows are candidates for later rows, resolver or
233
+ not — so a batch that names `src/parser.py` and then `p` against a catalog
234
+ holding `src/pipe.py` is refused as ambiguous where two separate adds would
235
+ store both.
236
+
237
+ ## Conventions
238
+
239
+ - **Bodies are markdown.** `list --full`, `show` and `context` print them as
240
+ written on a pipe and render them on a terminal; a plain `list` page and
241
+ `summary` flatten each to one clipped line.
242
+ - **A terminal and a pipe get different text.** A terminal shows each row as
243
+ columns: the id's last 10 characters, its age, a `○`/`✓` status mark, the
244
+ kind, the target, its tags and a count of its refs, then one body line cut to
245
+ the terminal's width. `show` takes that id tail. A pipe, which is what
246
+ scripts and agents read, always gets the plain line.
247
+ - **`idea` is a kind, and parked means parked.** `add --kind idea --type project
248
+ --body …` shelves a thought; it never prints in `arc todo` or `context`, and
249
+ in `summary` only when it carries a `--due` date that is near or past (how a
250
+ thought comes back on a date) or a person other than the reader wrote it
251
+ (`from <author>`). Close it with `resolve <id> --add-tag adopted --body "…"` or
252
+ `--add-tag retired`. `list --kind idea` is the shelf; `list --tag idea` still
253
+ finds rows written before it was a kind.
254
+ - **Kinds are the project's.** Rename or add a label in `symbion.toml`
255
+ `[kinds]`; a label that already has rows also needs a one-line `sed` over
256
+ `notes.jsonl` (`init` writes the recipe into the toml). `status` +
257
+ `verdict` is a pre-registration: `resolve --result` closes it.
258
+ - **`priority` tag.** `symbion supersede <id> --add-tag priority` stars a note,
259
+ whoever writes it, so the next session's summary lists it under `priority`
260
+ until the row is resolved or `--rm-tag priority` takes the star off.
261
+ - **Due dates.** `--due 2026-10-01` (or an ISO datetime) on any status row.
262
+ Past due or due within 7 days, an open row leads the session-start summary
263
+ as `overdue 2d` or `due in 3d`, in an arc or not; `list --overdue` lists
264
+ the ones past due, and `supersede <id> --due ''` clears one.
265
+ - **Author.** Inside a Claude Code session the author is `claude`; otherwise
266
+ git `user.name`. `--author` or `SYMBION_AUTHOR` override. A human typing
267
+ `! symbion add …` inside a session is recorded as `claude` without one.
268
+ An agent's session start lists open rows a person wrote, parked ones
269
+ included, as `from <author>`: the owner's word reaches the next session.
270
+ - **Close done work as a resolved task**, not by deleting the ticket.
271
+ `add --kind task --status resolved --body "done: …"` records work that
272
+ was finished before it was ever ticketed.
273
+
274
+ ## Where things live
275
+
276
+ - **Store:** `../<repo>-notes`, beside the *main* worktree. Linked worktrees
277
+ share it. `SYMBION_DIR` or `--dir PATH` override; `--dir` may sit anywhere
278
+ in the argv. A `<repo>-notes` store named from another repo resolves in
279
+ `<repo>`, and says so on stderr. So does a command run from inside the
280
+ store itself (to edit `symbion.toml`, say): the store is the one you are
281
+ in, and `init` there refuses.
282
+ - **A store that is not named after the repo:** put its path in a
283
+ `.symbion` file in the repo root — one line, relative to the repo or
284
+ absolute, first non-blank line wins. `symbion --dir ../other-notes init`
285
+ writes it for you. Commit it; a relative pointer survives a clone.
286
+ This knob cannot live in `symbion.toml`, because that file is inside the
287
+ store you are trying to find.
288
+
289
+ It is worth setting whenever the two names differ: rename a repo directory
290
+ and every command reports `no store at ../<new-name>-notes` (the old store
291
+ is still beside the old name) until a `.symbion` file names it. A write
292
+ refuses too; only `symbion init` creates a store.
293
+ - **Files:** `notes.jsonl` (append-only; corrections are new rows that
294
+ supersede old ones), `arcs.jsonl`, `symbion.toml`.
295
+ - **Reading:** `symbion summary` (what the hook prints), `symbion context
296
+ --target TYPE:NAME`, `symbion context --branch REF`, `symbion list --json`.
297
+ Every command that prints rows takes `--json` (`tags` prints counts and
298
+ does not).
299
+
300
+ ## Development
301
+
302
+ ```bash
303
+ python3 -m venv .venv && .venv/bin/pip install -e '.[test,gui]'
304
+ .venv/bin/python -m pytest -q
305
+ ```
306
+
307
+ The specs are dated design records: why each decision was made, and the design as of that date. `SKILL.md` is
308
+ what an agent reads; keep it short and keep its footgun list honest. It lives
309
+ at `src/symbion/data/skill/SKILL.md`, beside `adoption.md` and the hook. If
310
+ this clone is your installed symbion (an editable install, then `symbion
311
+ init`), `~/.claude/skills/symbion` links to that directory: an edit reaches
312
+ every session on the machine at once, the same way an edit to `src/` reaches
313
+ every `symbion` call.