I had a thoughts directory that was working. Research docs, implementation plans, session handoffs — all markdown, all in my dev workspace. Deep in some P0 work, I was generating a lot of this: architecture research, design specs, implementation plans, handoffs between sessions. The problem was entropy.
Finished work left files sitting idle. Mid-session ideas got typed into a prompt and disappeared. I had no way to see how the work connected over time. I built tw to fix that without changing how I work.
The practice behind it
I started from open-source Claude skills from the HumanLayer repo. These skills are specifically focused on producing research docs, plans, and session handoffs. My skills (/research_codebase, /create_plan, /create_handoff, /resume_handoff, and others) were updated to fit my use case and workflow.
What those commands mean as practice:
Research codebase — before starting significant work, an agent reads the relevant code and produces a document: what exists, how it works, where things live. You’re not building against a mental model that’s three weeks out of date.
Create plan — you don’t write code until there’s a written spec: what you’re building, in what order, with success criteria per phase.
Create handoff — when a session ends, you produce a document with everything the next session needs: what was completed, what was learned, what comes next. The session ends deliberately. Nothing is lost.
Dex Horthy’s thinking on intentional compartmentalization — treating the end of a session as a preservation act rather than a loss — changed how I think about working with agents. That shift is what the rest of this is built around.
Architecture
Three layers. The workspace is the source of truth. GitLab is the transport. Obsidian is where I read and explore.
flowchart LR
subgraph WS["Dev workspace"]
T[".thoughts/<br>markdown + frontmatter"]
C["Claude CLI<br>triage + suggest"]
W["file watcher<br>push on save · pull every 5m"]
end
subgraph GL["GitLab (private)"]
R["tw-thoughts repo"]
end
subgraph OB["Obsidian (local Mac)"]
V["vault<br>markdown files"]
G["graph view<br>connections over time"]
P["Obsidian Git plugin<br>push edits · pull every 5m"]
end
DS["personal scripts repo<br>dotfiles · tw scripts · commands"]
W -->|git push| R
R -->|git pull every 5m| W
P -->|git push| R
R -->|git pull every 5m| P
DS -.->|symlinked on startup| WSEverything lives under /workspace/.thoughts/ — outside the repo, persisting across sessions. The personal scripts repo (dotfiles) gets pulled into every new workspace on startup, so the tooling travels across instances.
Directory structure
/workspace/.thoughts/
├── _global/
│ ├── _index.md # master status index — written by triage
│ └── todos.md # global rollup across all repos
└── my-service/
├── handoffs/
│ └── general/
│ └── 2026-03-25_14-30-00_feature-impl-handoff.md
├── parked/
│ └── 2026-03-20-rate-limiting-idea.md
├── plans/
│ └── 2026-03-25-supplier-workflow-design.md
├── research/
│ └── 2026-03-18-workflow-engine-architecture.md
├── retrospectives/
└── todos/
└── todos.mdFrontmatter
Every file gets this on creation:
---
date: 2026-03-25T10:00:00Z
type: plan
repo: my-service
status: active
priority: p1
urgency: high
tags: [plan, workflow, supplier-integration]
title: "Supplier workflow design and state machine"
related: []
---status is the field everything else reads and writes. related is where wiki-links go when tw-link runs. Todos carry machine-readable metadata in an HTML comment, invisible in Obsidian’s reading view but parsed by triage and suggest:
- [ ] Wire supplier integration into workflow engine <!-- priority:p1 urgency:high date:2026-03-01 repo:my-service -->Capture
tw "rate limiting edge case — supplier API returns 200 on auth failure" --p1 --highCreates a frontmattered markdown file in .thoughts/my-service/parked/, syncs in the background. Under 3 seconds. From inside a Claude Code session, /save_thought does the same thing.
After every save, a lightweight local similarity check (Jaccard, no API call) compares the new thought against recent parked docs and surfaces a suggestion if something looks related. The deeper link-finding uses Claude and runs separately on demand.
Todos
todo "Review PR: supplier workflow — Wave 3 frontend" --p0 --highAppends to both .thoughts/my-service/todos/todos.md and .thoughts/_global/todos.md. The global list aggregates uncompleted items across all repos — that’s what the session brief reads by default.
Priority scale: p00 > p0 > p1 > p2 > p3. Todos are Obsidian-compatible checkboxes. Check one off in the vault, it syncs back to the workspace on the next pull.
The index file
_global/_index.md is a markdown table of every document in the system: path, type, status, priority, last updated. Triage writes it. The session brief reads it. Sync uses it to exclude stale docs from Obsidian.
| path | type | status | priority | updated |
|------|------|--------|----------|---------|
| my-service/plans/2026-03-25-supplier-workflow-design.md | plan | active | p1 | 2026-03-26 |
| my-service/research/2026-03-18-workflow-engine-architecture.md | research | implemented | p2 | 2026-03-26 |
| my-service/handoffs/general/2026-03-19_..._feature-impl.md | handoff | active | p1 | 2026-03-26 |Without it, every script would have to read every file to know the state of the system. With it, the reads are fast and everything stays coherent.
Triage
The thing that keeps the directory from becoming a graveyard.
tw-triage.sh --repo my-serviceLoops over every doc, calls Claude (claude -p), gets back a status determination. High-confidence changes apply automatically. Low-confidence ones surface for review.
stateDiagram-v2
[*] --> active : document created
active --> stale : triage — no recent activity
active --> implemented : triage — work shipped
stale --> active : manual override
stale --> archived : confirmed irrelevant
implemented --> archived : project wrapped up
archived --> [*]
note right of active : synced to Obsidian
note right of stale : excluded from syncRunning it against a batch of P0 work correctly identified older research docs as implemented, recent plans as active, and handoffs from completed phases as archived. It’s not fast — one Claude API call per document — but it runs weekly so that’s fine.
Session start
tw-start.shReads from _index.md and prints a brief:
─── tw session brief ─── 2026-03-26 ───────────────────────
repo: my-service
last handoff: 2026-03-25
Supplier workflow design — mid-implementation
/resume_handoff my-service/handoffs/general/2026-03-25_...
todos — my-service:
[ ] Review PR: supplier workflow Wave 3 [p0]
[ ] Wire supplier integration [p1]
open plans:
→ Supplier workflow design and state machine
my-service/plans/2026-03-25-supplier-workflow-design.md
commands: tw "idea" | todo "task" | tw-suggest | tw-triage
───────────────────────────────────────────────────────────Hard caps: top 5 todos, 3 recent parked thoughts, 1 open plan per repo. For a more considered view of what to work on:
tw-suggest.sh --repo my-serviceCalls Claude on the full inventory of todos and parked thoughts, returns 3-5 ranked recommendations with one-line rationale each.
End of project
tw-consolidate.sh --repo my-serviceReads all docs for the repo, sends them to Claude, generates a retrospective: what was built, how thinking evolved, key decisions, things to do differently. Archives the source docs. The retrospective is the artifact — something worth reading six months later, not a folder of stale markdown.
Graph
Every doc has a related: [] field. tw-link looks across active docs for genuine connections and proposes wiki-links.
tw-link.sh --repo my-serviceIt surfaced connections like a research doc linking to the plan it directly informed, and a handoff linking back to the plan it was implementing. High-confidence links inject automatically. Medium-confidence surfaces for confirmation. Links write to both the frontmatter related field and a ## Related section at the bottom of the file. Obsidian renders them as graph edges.
Over enough time it becomes a map of how the work connected — which research led to which plans, which plans produced which handoffs.
Sync
The file watcher daemon fires tw-sync on every save:
tw-watch.sh --daemontw-sync does a git pull --rebase first — picks up any Obsidian changes like checked-off todos — then commits and pushes. Obsidian’s Git plugin pulls every 5 minutes. The watcher also runs a periodic pull every 5 minutes independently, so Obsidian changes land in the workspace even when nothing on the Coder side has changed.
The thoughts repo is private GitLab, authenticated via a personal access token stored as a workspace environment variable. Nothing goes to a third-party service. My notes stay on infrastructure I control.
Current state
Working. I migrated existing thoughts from active P0 work into the system this week and triage correctly classified most of them on first pass. There are bugs — general scripting issues, a CLI flag that changed between versions — and I’m still evaluating what earns its place in the daily flow versus what adds friction. The consolidation and link-suggestion features are built but haven’t been tested against a fully completed project yet.
The real work now is integration: does tw-start actually orient me faster than just opening files? Does triage run cleanly enough that I trust it? Does the Obsidian graph start showing something useful after a few weeks of real use?
The repository will be shared once I’ve had time to generalize a few things.