From 584f3eb32e937216ebcfa16f88e687d15784f553 Mon Sep 17 00:00:00 2001 From: Jason Irish Date: Tue, 1 Sep 2026 10:46:22 -0500 Subject: [PATCH 1/2] docs(#68): split README into docs/, add Related section and a runnable demo README.md drops from 492 to 231 lines: per-harness install detail moves to docs/INSTALL.md, and Configuration/worktrees/opt-out/housekeeping/layout move to docs/REFERENCE.md (which also documents the handoff-as-executable-input trust boundary). Adds a Related section naming 9 verified alternative tools, and a demo/homelab/ fictional project with a real HANDOFF.md, session log, and unconsumed capture buffer, wired via demo/setup.sh so someone can try throughline in under a minute without installing into a real repo. Prompted by a competitive read of adrrr/persistent-handoff, whose README structure (pitch -> demo -> install, reference material pushed to docs/) and named "Related" section this borrows the shape of. Closes #69 Closes #70 Closes #65 Addresses #68 - docs split and demo are done, the recorded demo GIF is not Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01UXL2ffVDsAp4P7387Nr18G --- README.md | 401 ++++-------------- demo/homelab/.claude/settings.json | 59 +++ demo/homelab/.claude/throughline/HANDOFF.md | 22 + .../throughline/buffer/session-demo-0002.md | 7 + .../logs/handoff-2026-08-27-2140.md | 61 +++ demo/homelab/.gitignore | 21 + demo/homelab/README.md | 15 + demo/homelab/logs/link-check.log | 20 + demo/homelab/scripts/check-links.sh | 21 + demo/homelab/urls.txt | 4 + demo/setup.sh | 41 ++ docs/INSTALL.md | 150 +++++++ docs/REFERENCE.md | 228 ++++++++++ 13 files changed, 728 insertions(+), 322 deletions(-) create mode 100644 demo/homelab/.claude/settings.json create mode 100644 demo/homelab/.claude/throughline/HANDOFF.md create mode 100644 demo/homelab/.claude/throughline/buffer/session-demo-0002.md create mode 100644 demo/homelab/.claude/throughline/logs/handoff-2026-08-27-2140.md create mode 100644 demo/homelab/.gitignore create mode 100644 demo/homelab/README.md create mode 100644 demo/homelab/logs/link-check.log create mode 100755 demo/homelab/scripts/check-links.sh create mode 100644 demo/homelab/urls.txt create mode 100755 demo/setup.sh create mode 100644 docs/INSTALL.md create mode 100644 docs/REFERENCE.md diff --git a/README.md b/README.md index 089b0d8..323a4cc 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,24 @@ Honest scope: the **what** (commands run, files changed) is compaction-proof; th discards. Run a handoff before a long session compacts to preserve the reasoning, and the boundary marker flags where recall stops being trustworthy. +## Try it + +`demo/` is a small fictional project with a real, populated +`.claude/throughline/` already in it - a HANDOFF.md, a prior session log, and +a live capture buffer that never got distilled - so you can see the artifacts +before generating your own: + +```sh +git clone https://github.com/dynamic/throughline && cd throughline +./demo/setup.sh && cd demo/homelab +claude --setting-sources project,local --strict-mcp-config \ + --tools Read,Glob,Grep,Bash,Skill,Write +``` + +Ask `where were we?` and the answer comes from `HANDOFF.md`, injected by the +`SessionStart` hook before your first message. `./demo/setup.sh` prints the +rest of the walkthrough. + ## Choose your harness Every harness reads and writes the same `.claude/throughline/` data format, so a @@ -121,24 +139,8 @@ honest comparison, then jump to the section for your harness. /plugin install throughline@throughline # install plugin@marketplace (same name) ``` -Then reload (`/reload-plugins`) or restart the session. - -**Requirements:** `git` and `jq` on your `PATH`. `jq` parses the hook payloads; if it -is missing, capture cannot run and the SessionStart block says so rather than failing -silently. - -**What you get:** the 4 skills (`handoff`, `onboard`, `consolidate`, -`consolidate-memory`) plus all 5 hooks. The only harness with a native durable -memory system of its own (`/memory`, backed by `MEMORY.md`) - the session-start -injection above complements that auto-load with project-level state, and the -`handoff`/`consolidate-memory` skills promote genuinely durable facts into it. -Codex and OpenCode have no equivalent system to bind into today. - -**Updating.** Installed plugins are snapshots - they do not track this repo. An old -copy keeps running (without newer redaction and activation fixes) until you update it -from the `/plugin` manager (or uninstall and reinstall), then `/reload-plugins`. The -SessionStart block prints the running version (`## throughline vX.Y.Z`) - if it lags -this repo's releases, your install is stale. +Then reload (`/reload-plugins`) or restart the session. Requires `git` and `jq` on +your `PATH`. ### Codex CLI @@ -147,105 +149,20 @@ codex plugin marketplace add dynamic/throughline codex plugin add throughline@throughline ``` -**Requirements:** `git` and `jq` on your `PATH`, same as Claude Code - Codex runs -the identical hook scripts. Without `jq`, capture cannot run. Works in Codex CLI and -Codex Desktop. - -**What you get:** the 4 skills plus all 5 hooks, reading and writing the same -`.claude/throughline/` data format Claude Code and OpenCode use - so a project's -history is readable and continuable from any of the three. - -**The one-time trust step.** Codex gates hook execution behind a one-time trust -decision per machine (Claude Code has no equivalent gate - a plugin's hooks just run -once installed). What that looks like the first time you use a project with -throughline installed: - -- **Codex CLI** shows a native **"Hooks need review"** dialog before your first - message: "5 hooks are new or changed. Hooks can run outside the sandbox after you - trust them." Choose **"Trust all and continue."** That's the whole step - trust is - granted by content hash, not by project path, so it covers every project at once. - A throughline update that changes the hook scripts triggers the dialog again. -- **Codex Desktop** grants trust silently, with no dialog - capture just starts - working on your first message. - -Verify anytime with the in-TUI **`/hooks`** command: it lists every Codex hook event -with an Installed/Active count, and pressing Enter on a row shows that hook's -`Source`, `Command`, `Mode`, `Timeout`, and `Trust` status. A trusted throughline -hook reads `Source: Plugin - throughline@throughline`, `Trust: Trusted`. - -**Updating.** Same story as Claude Code: an installed plugin is a snapshot, not a -live checkout. Update from `codex plugin add throughline@throughline` again (or the -Codex plugin manager) to pick up the latest release - a throughline update also -changes the hook scripts' content hash, so the trust dialog reappears once on Codex -CLI. +Same requirements as Claude Code. Codex gates hooks behind a one-time per-machine +trust dialog on first use - see [docs/INSTALL.md](docs/INSTALL.md#codex-cli). ### OpenCode -throughline is also available as an OpenCode plugin, providing the same session -capture functionality within the OpenCode ecosystem. - ```json { "plugin": ["@dynamicagency/throughline-opencode"] } ``` -OpenCode's plugin config key is `plugin` (singular) in `opencode.json`, and each -entry is either an npm package name or a local path - there is no separate -`plugins/` directory to copy into. Add the line above and restart OpenCode; it -installs the package automatically via Bun at startup. Check -`~/.local/share/opencode/log/opencode.log` for a load error if session capture -doesn't appear to be running. - -**Requirements:** Node.js 18+ (no `jq` required - TypeScript uses native JSON -parsing). - -**What you get:** all 5 hooks, ported to TypeScript against OpenCode's own plugin -API - continuous prompt/action capture with redaction, session-start context -injection (HANDOFF.md pointer + live git state), and compaction survival (a boundary -marker plus buffer-tail re-injection right after). One behavioral difference worth -knowing: OpenCode's `session.idle` event fires after every turn, not once at process -exit the way Claude Code's `SessionEnd` does, so the buffer's end-marker is a "last -known idle point" that gets re-stamped each time the session goes idle, rather than -a one-shot end-of-session stamp - `onboard` reads it the same way either way (has -this buffer seen activity since the marker). This plugin ships **hooks only, -no skills** - OpenCode's own plugin API has no supported way to ship a skill -directory alongside a plugin today. Run `npx skills add dynamic/throughline` -separately for `handoff`/`onboard`/`consolidate`/`consolidate-memory`. In practice -this is often a non-issue: OpenCode discovers `SKILL.md` files from several -locations it shares with Claude Code and Codex (project-local and global -`.claude/skills/`, `.agents/skills/`, and its own `.opencode/skills/` / -`~/.config/opencode/skills/`), so skills installed for another harness on the same -machine are frequently already visible to OpenCode with no extra step. - -By default the OpenCode plugin uses the same `.claude/throughline/` data directory -as Claude Code and Codex, so a project's history stays continuous across harnesses. - -**Updating.** An installed plugin is a versioned snapshot, same as Claude Code and -Codex - bumping the version in `opencode.json` (or letting Bun resolve a new -range) is what picks up a release, not a `git pull`. The running version is -printed in the injected session-start block (`## throughline vX.Y.Z`) the same way -it is on Claude Code and Codex - if it lags this repo's releases, your install is -stale. - -Publishing to npm is tag-triggered: pushing a `vX.Y.Z` tag runs a GitHub Actions -workflow that publishes `@dynamicagency/throughline-opencode` via npm Trusted -Publishing (OIDC), with no long-lived npm token and an automatic provenance -attestation on the published package. - -**Local-path install (testing unreleased changes).** Point `opencode.json` at a -checkout of this repo instead of the package name: - -```json -{ - "plugin": ["/absolute/path/to/throughline/.opencode-plugin"] -} -``` - -The package's `main` field points at compiled `dist/`, which is gitignored, so a -local-path install needs a build first: `cd .opencode-plugin && npm ci && npm run -build`. Unlike the npm install, this stays a live checkout - `git pull` and -rebuild to update it. +Add to `opencode.json` and restart; Bun installs it automatically. Requires +Node.js 18+, no `jq`. Hooks only - see +[docs/INSTALL.md](docs/INSTALL.md#opencode) for skills and the local-path install. ### npx skills @@ -253,179 +170,24 @@ rebuild to update it. npx skills add dynamic/throughline ``` -Installs the 4 skills directly - no plugin system, no marketplace registration. This -is the fallback for any harness that reads `SKILL.md` files from disk but has no -plugin system of its own. **What you get:** the 4 skills, nothing else - no -automatic capture (there's no hook mechanism in this delivery form at all); run -`handoff` manually at the end of a session. - -## Configuration - -By default, state lives in **`.claude/throughline/`** in each project (the universal -Claude Code workspace dir). Override the location with an environment variable: - -```sh -# Opt in to a portable .agent/ handoff convention - e.g. for cross-harness use, -# or a team that has agreed to commit its handoffs (see "Local by default" below): -export THROUGHLINE_DATA_DIR=.agent/handoff -``` - -- Relative values resolve against the project root; absolute values are used as-is. -- throughline auto-activates in every project: the first time any hook fires it - creates its data dir on demand, so capture starts working immediately with no - manual opt-in. To keep it out of a specific project, drop an empty - `.throughlineignore` file at the project root (see "Opting a project out" below). - -### Git worktrees - -In a **linked git worktree** (e.g. Claude Code's `claude/` auto-worktree -workflow, under `/.claude/worktrees//`), "the project root" above -resolves to the **main working tree**, not the worktree itself - so every worktree -of a repo, plus its main checkout, share one `HANDOFF.md`/`logs/`/`buffer/` instead -of each worktree silently accumulating its own. The session-start capture point -prints a note when this redirect is active. Live git state (current branch, -`git status`) and captured file paths still describe the worktree you're actually in. - -Set `THROUGHLINE_WORKTREE_SHARED=0` to opt back into isolated per-worktree data -dirs. Requires git 2.31+; falls back to per-worktree behavior for bare repos, -submodules, and older git. - -### Opting a project out - -throughline activates automatically in every project. To disable it for one -project, add an empty marker file at the project root: - -```sh -touch .throughlineignore -``` - -**In a linked git worktree** (see "Git worktrees" above), place this at the -**main** working tree's root, not the worktree you're sitting in - that's where -the opt-out check now looks by default. (A marker already sitting in a worktree -from before worktree-sharing existed is still honored there too, so upgrading -never silently re-enables a pre-existing opt-out.) - -With that file present, no new data dir is created, and `onboard`/`capture` stop -adding anything new - regardless of `THROUGHLINE_DATA_DIR` or any pre-existing -`.claude/throughline/`. The opt-out wins even over a project that was already -active: existing `HANDOFF.md`/`logs/` are left in place, and no *new* activity is -recorded. One nuance: if a session was already being captured when the file -appears, `flush`/`precompact` still finalize that one session's already-existing -buffer (its end-stamp or compaction marker) rather than leaving it in a permanent -"still live?" limbo - they don't create anything new, they just avoid corrupting -bookkeeping for work that had already legitimately started. Remove the file to -re-enable. Commit it like `.gitignore` so the policy is shared with teammates. - -### Disabling machine-wide - -To turn throughline off everywhere without uninstalling or touching every project, -set the kill switch (e.g. in `~/.claude/settings.json`'s `env` block, or your shell -profile): +Skills only, no automatic capture - the fallback for any harness with no plugin +system of its own. -```sh -export THROUGHLINE_DISABLE=1 -``` +Full per-harness detail - requirements, what each install gives you, updating, +Codex's trust step, OpenCode's local-path install - lives in +[docs/INSTALL.md](docs/INSTALL.md). -Any value other than `0` disables **all five hooks completely** - no capture, no -SessionStart block (not even about existing data), no end-stamps. This is stricter -than `.throughlineignore`, which keeps orienting toward already-existing content. -Unset it (or set `0`) to re-enable; existing data is untouched either way. - -**Cross-harness handoffs.** The data dir is the one knob that makes throughline -portable. Point it at `.agent/handoff` (or any other path) and the durable -`HANDOFF.md` it produces lives in a harness-neutral location any agent can read, -not buried under a Claude-Code-specific path - useful if other tooling also drives -this project. Portability of the *location* is independent of whether you commit -it - see "Local by default" below. - -### Local by default - -throughline's data - `HANDOFF.md`, `logs/`, `buffer/`, everything under the data -dir - is **per-operator working memory, not a shared team artifact**, and stays -local (gitignored) by default. Gitignore the whole data dir for whichever location -you use: - -```gitignore -# default layout -.claude/throughline/ -# or, if you set THROUGHLINE_DATA_DIR=.agent/handoff -.agent/handoff/ -``` +## Configuration -**Team projects.** On a project with multiple developers - especially ones not -using throughline, or already running their own memory/notes tooling - committing -one operator's session artifacts into the shared tree causes real friction: churn -and merge conflicts on the single mutable `HANDOFF.md`, review noise on every PR, -and possible collision with whatever a teammate already relies on. Local-only -avoids all of it: nothing throughline writes reaches a teammate's checkout unless -you deliberately choose to share it. - -**Opting in to tracking.** For a solo repo, or a team that has all adopted -throughline, committing `HANDOFF.md` + `logs/` gives fresh clones and teammates a -shared, readable project record - genuinely useful when everyone is actually -reading it. To opt in, un-ignore just those two paths (keep `buffer/` and -`.capture-errors` ignored always - `buffer/` is scratch and can contain unredacted -command text, and `.capture-errors` is a scratch breadcrumb file): - -```gitignore -.claude/throughline/* -!.claude/throughline/HANDOFF.md -!.claude/throughline/logs/ -``` +State lives in **`.claude/throughline/`** in each project by default, local +(gitignored) and per-operator - not a shared team artifact unless you opt in. +throughline auto-activates on first use; drop an empty `.throughlineignore` at +the project root to opt a project out, or set `THROUGHLINE_DISABLE=1` to kill it +machine-wide. It shares one data dir across a repo's git worktrees. -The `handoff` skill's Phase 4 offers (never auto-runs, and relevant -only once you've opted in as above) to stage exactly `HANDOFF.md` + the new -session log and commit/push them - it checks `git check-ignore` first and skips -the offer entirely when the files aren't actually committable in your layout. - -> **Heads-up for allowlist-style `.gitignore`.** If your repo ignores everything -> by default (a root `/*` then `!/keep` pattern) and you *do* want to opt in to -> tracking, re-including just the two leaf paths does **not** work - git prunes -> an excluded directory before it ever evaluates negation patterns for paths -> inside it, so `.claude` (matched by the root `/*`) is never even descended -> into. The simplest fix is `THROUGHLINE_DATA_DIR=.agent/handoff` so the -> opted-in artifacts sit outside the ignored tree entirely. To keep the default -> location instead, negate **every ancestor directory** on the way down, then -> re-exclude the scratch paths (which the ancestor negations would otherwise -> expose too): -> ```gitignore -> !/.claude/ -> !/.claude/throughline/ -> !/.claude/throughline/HANDOFF.md -> !/.claude/throughline/logs/ -> .claude/throughline/buffer/ -> .claude/throughline/.capture-errors -> ``` - -## Housekeeping - -Everything throughline writes grows without automatic bound: there is no -background cleanup process, deliberately, to keep the plugin's footprint at -"pure POSIX sh + jq, zero infrastructure." What's safe to clean up by hand, -and what isn't: - -**Safe to delete:** -- `buffer/archive/*.md` older than your last `consolidate` pass - - once a consolidation has mined a log for recurring lessons, an archived raw - buffer behind it has nothing left to give. As a simple rule of thumb, an - archived buffer older than ~90 days with no open question against it is safe - to remove. -- `.capture-errors`, once its contents have been surfaced in a session log and - cleared by the handoff skill (Phase 4): it's a breadcrumb meant to be read - once, not a running log. - -**Not safe to delete:** -- `logs/`: these are the evidence trail. `consolidate` explicitly - never prunes them, and HANDOFF.md's own "Recent Session Logs" list only ever - points at the last 5, so older logs are already off the beaten path without - needing to be deleted. -- `HANDOFF.md` itself, obviously - it's the durable record. -- Any buffer still in `buffer/` (not yet archived) - it may be an in-progress or - unconsumed session; run a handoff first, which moves it to `archive/` once - distilled. - -There's no automated retention policy beyond this: clean up by hand on the -cadence above, or leave it, a growing `archive/` costs disk, not correctness. +Git worktrees, the opt-out marker, the kill switch, committing `HANDOFF.md` to +a team repo (and the trust boundary that implies), and cleanup guidance are all +in [docs/REFERENCE.md](docs/REFERENCE.md). ## Auto-handoff at wrap-up (optional reinforcement) @@ -435,51 +197,46 @@ winding down. To reinforce it, add one line to your project or global `CLAUDE.md > When a session reaches a natural stopping point or the user signals they're done, > run the `handoff` skill and report the diff - don't wait to be asked. -## Layout - -``` -throughline/ -├─ .claude-plugin/ -│ ├─ plugin.json -│ └─ marketplace.json -├─ .codex-plugin/ -│ └─ plugin.json # declares skills; hooks found via convention in hooks/ -├─ .agents/plugins/ -│ └─ marketplace.json # Codex marketplace entry, mirrors .claude-plugin's -├─ .opencode-plugin/ -│ ├─ package.json # Node dependencies + plugin entry point (main) -│ ├─ tsconfig.json # TypeScript config -│ ├─ .gitignore # Excludes node_modules/ and dist/ -│ └─ src/ -│ ├─ index.ts # Plugin entry point -│ ├─ lib.ts # Core library (data dir, session ID, buffer) -│ ├─ hooks/ # All 5 hook implementations -│ │ ├─ session-created.ts -│ │ ├─ chat-message.ts -│ │ ├─ tool-execute-after.ts -│ │ ├─ session-compacted.ts -│ │ └─ session-idle.ts -│ ├─ utils/ -│ │ ├─ redaction.ts # Redaction logic ported from jq to TypeScript -│ │ └─ redaction.test.ts -│ └─ integration.test.ts -├─ hooks/ -│ ├─ hooks.json -│ ├─ _lib.sh # data-dir resolution + activation gate + jq/sid/redaction helpers -│ ├─ session-onboard.sh # SessionStart: pointer, git state, compaction recovery -│ ├─ session-prompt.sh # UserPromptSubmit: redacted, truncated user-intent line -│ ├─ session-capture.sh # PostToolUse: structured action buffer (outcome + redaction) -│ ├─ session-precompact.sh # PreCompact: stamp the compaction-boundary marker -│ └─ session-flush.sh # SessionEnd: safety-net stamp -├─ skills/ -│ ├─ onboard/SKILL.md # full orientation -│ ├─ handoff/SKILL.md # judged distillation + memory binding -│ ├─ consolidate/SKILL.md # periodic promotion of recurring lessons -│ └─ consolidate-memory/SKILL.md # native-memory file hygiene -├─ tests/run.sh # fixture-driven hook tests (shellcheck + CI) -├─ docs/ # promo site + review report -└─ CHANGELOG.md -``` +## Related + +Session-memory tools for Claude Code are a crowded field. What's out there, and how +throughline differs, one line each: + +- **[adrrr/persistent-handoff](https://github.com/adrrr/persistent-handoff)** - one + mutable file, rewritten in place at milestones, deleted when nothing is in flight. + Built for a long-running daemon agent (tmux + cron restarts) that needs one state + file, not for a repo you return to over weeks and want a readable history of. +- **[thedotmack/claude-mem](https://github.com/thedotmack/claude-mem)** - AI-compressed + observations in SQLite/FTS5 plus vector search, progressive-disclosure retrieval. + Its retrieval ideas are sound; the runtime (a background worker process) is the + infrastructure weight throughline stays "pure POSIX sh + jq" to avoid. +- **[thepushkarp/handoff](https://github.com/thepushkarp/handoff)** - auto-injects the + latest handoff entry on `SessionStart(compact)`; a `Stop` hook blocks session exit + until the model fills in required summary fields. throughline captures continuously + instead of gating exit, so nothing is lost if a session ends without a summary. +- **[REMvisual/claude-handoff](https://github.com/REMvisual/claude-handoff)** - + sequence-numbered chain links between handoffs, "What We Tried" as the highest-value + section, self-validation gates. Closest in spirit to throughline's emphasis on + concrete failed approaches over narrative summary. +- **[who96/claude-code-context-handoff](https://github.com/who96/claude-code-context-handoff)** + - restores context as `additionalContext` on `SessionStart(compact|clear)`, with an + age-guarded fallback to the latest handoff. No automatic capture between writes. +- **[Sting25/claude-code-handoff](https://github.com/Sting25/claude-code-handoff)** - + auto-snapshots git state and model decisions at context boundaries. One file per + repo, agent-written at the boundary rather than captured continuously. +- **[blader/baton](https://github.com/blader/baton)** - pass in-progress work to the + next agent via a single verified markdown file, manual on both ends, works with + Codex as well as Claude Code. +- **[rupaut98/unforget](https://github.com/rupaut98/unforget)** - a local, + zero-dependency `SessionStart` hook that re-injects working state lost to + compaction, extracted from the transcript rather than written by the agent. +- **[mattpocock/skills](https://github.com/mattpocock/skills)** (`handoff` skill) - + writes once to the OS temp directory on manual invocation. Disposable, and the + right tool for a coding session you'll close today rather than resume tomorrow. + +Where throughline differs from all of the above: capture is mechanical and continuous +(five hooks, no model call, independent of whether the agent remembers to write +anything), and it's the only one of these that also redacts secrets at capture time. ## Support diff --git a/demo/homelab/.claude/settings.json b/demo/homelab/.claude/settings.json new file mode 100644 index 0000000..b2e57e1 --- /dev/null +++ b/demo/homelab/.claude/settings.json @@ -0,0 +1,59 @@ +{ + "hooks": { + "SessionStart": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/session-onboard.sh" + } + ] + } + ], + "UserPromptSubmit": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/session-prompt.sh" + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "Bash|Edit|Write|NotebookEdit|Grep|WebFetch|WebSearch|Task|Agent|mcp__.*", + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/session-capture.sh" + } + ] + } + ], + "PreCompact": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/session-precompact.sh" + } + ] + } + ], + "SessionEnd": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/session-flush.sh" + } + ] + } + ] + } +} diff --git a/demo/homelab/.claude/throughline/HANDOFF.md b/demo/homelab/.claude/throughline/HANDOFF.md new file mode 100644 index 0000000..7a21da3 --- /dev/null +++ b/demo/homelab/.claude/throughline/HANDOFF.md @@ -0,0 +1,22 @@ +# homelab-linkcheck — Handoff +**Last Updated:** 2026-08-29 + +## Resolved Issues +| Issue | Resolution | Date | +|---|---|---| +| `status.example.com` false FAIL every night since Aug 25 | Root cause: its load balancer 405s HEAD requests, GET-only. `check-links.sh` switched from `curl -I` to a GET-based check for all URLs. | 2026-08-27 | + +## Pending Items +| Item | Priority | Tracking | +|---|---|---| +| Confirm the fix holds for a third consecutive clean run before calling this closed | Medium | logs/link-check.log | + +## Current State +- Fix landed 2026-08-27 evening. Clean runs since: Aug 28, Aug 29. One more + clean night (Aug 30) closes this out; anything else means the + load-balancer theory was incomplete. +- `urls.txt` unchanged: 4 URLs, all on `example.com` / `status.example.com`. +- No other work in flight on this project. + +## Recent Session Logs +1. [status.example.com false-FAIL root-caused and fixed](logs/handoff-2026-08-27-2140.md) — 2026-08-27 diff --git a/demo/homelab/.claude/throughline/buffer/session-demo-0002.md b/demo/homelab/.claude/throughline/buffer/session-demo-0002.md new file mode 100644 index 0000000..4401aa9 --- /dev/null +++ b/demo/homelab/.claude/throughline/buffer/session-demo-0002.md @@ -0,0 +1,7 @@ +- `2026-08-29 08:05:12` **prompt** did last night's run pass for status.example.com? +- `2026-08-29 08:05:14` **bash** Tail last night's link-check results - `tail -4 logs/link-check.log` +- `2026-08-29 08:05:19` **grep** `status.example.com` +- `2026-08-29 08:05:31` **prompt** good, that's the second clean night in a row. one more and we can close the handoff item. + + + diff --git a/demo/homelab/.claude/throughline/logs/handoff-2026-08-27-2140.md b/demo/homelab/.claude/throughline/logs/handoff-2026-08-27-2140.md new file mode 100644 index 0000000..ad676fc --- /dev/null +++ b/demo/homelab/.claude/throughline/logs/handoff-2026-08-27-2140.md @@ -0,0 +1,61 @@ +# Handoff: status.example.com nightly link-check false failure + +## Objective +Find why `check-links.sh` reports `status.example.com` as failing every night +when the site is reachable and fine in a browser. + +## What happened +Every run since at least Aug 25 logged `FAIL https://status.example.com/ (405)` +while the other three URLs passed. Loading the site in a browser works. Curling +it by hand with a plain GET also returns 200. Only the script's HEAD request +(`curl -I`) gets a 405. + +## What we tried (including what failed) +- Assumed a transient outage at first - ruled out once the same URL failed + identically three nights running with nothing else changing. +- Compared the script's `curl -I "$url"` against `curl -sI "$url" -L` by hand: + `-L` alone didn't change the result, still 405. +- Checked the site's own status page (ironically, the URL under test) for a + known issue - nothing reported. +- Ran `curl -v -I https://status.example.com/` and read the actual response: + the load balancer in front of that site returns `405 Method Not Allowed` + specifically for `HEAD` requests, and only for that one host - `Allow: GET` + is in the response headers. `example.com`'s other three URLs happily answer + HEAD, this one doesn't. + +## Progress +### Completed +Root cause confirmed: `status.example.com`'s load balancer rejects HEAD, +GET-only. Not a redirect issue, not a TLS issue, not our script's timeout. + +### In progress +Fix: swap the script's `curl -I` for a GET request +(`curl -s -o /dev/null -w '%{http_code}' "$url"` instead of the `-I` variant), +so it never sends a method the target might reject. Verify the new script +against all four URLs, one clean run tonight (Aug 27), then watch two more +nightly runs before calling it closed. + +### Not started +Nothing else queued on this project. + +## Key learnings & gotchas +- `curl -I` sends `HEAD`, not `GET`. A URL that 405s a HEAD request but 200s a + GET request looks identical to "the site is down" in a HEAD-based checker. + `curl -v -I ` on a failing one shows the `Allow:` header naming what the + server actually accepts - check that before assuming an outage. +- Don't add `-L` as the fix here: it was a red herring, this host doesn't + redirect at all, it flat-out rejects the method. + +## Current state +Fix is written (`scripts/check-links.sh` now uses a GET-based check for all +URLs, not just this one - simpler than special-casing one host). Not yet +proven: needs consecutive clean nightly runs, not just one manual test. + +## Next steps +Check `logs/link-check.log` after tomorrow's (Aug 28) and the day after's +(Aug 29) 02:30 run. Two consecutive clean runs for `status.example.com` closes +this out; anything else means the load-balancer theory was incomplete. + +## Files & resources +- `scripts/check-links.sh` - the fix +- `logs/link-check.log` - where the nightly results land diff --git a/demo/homelab/.gitignore b/demo/homelab/.gitignore new file mode 100644 index 0000000..e2d6b88 --- /dev/null +++ b/demo/homelab/.gitignore @@ -0,0 +1,21 @@ +# Populated by ../setup.sh from the canonical copies at the repo root, so the +# demo never drifts from the real hooks/skills. Not tracked here. +.claude/hooks/ +.claude/skills/ + +# The repo root's .gitignore ignores .claude/throughline/ everywhere (this +# project dogfoods "local by default"). The demo is the one deliberate +# exception: its throughline data IS the example, so re-include exactly the +# artifacts a real install would produce and nothing else. +!.claude/throughline/ +!.claude/throughline/HANDOFF.md +!.claude/throughline/logs/ +!.claude/throughline/logs/*.md +!.claude/throughline/buffer/ +!.claude/throughline/buffer/*.md +.claude/throughline/buffer/archive/ +.claude/throughline/.capture-errors + +# The root .gitignore ignores *.log everywhere; this one sample log is part +# of the fixture, not scratch output, so it's tracked deliberately. +!logs/link-check.log diff --git a/demo/homelab/README.md b/demo/homelab/README.md new file mode 100644 index 0000000..06ee774 --- /dev/null +++ b/demo/homelab/README.md @@ -0,0 +1,15 @@ +# homelab-linkcheck + +A small nightly link checker for a personal site. `scripts/check-links.sh` reads +`urls.txt`, hits each one, and appends a result line to `logs/link-check.log`. Run +from cron at 2:30am. + +This is throughline's demo project: a fictional repo with a real, populated +`.claude/throughline/` already in it, so you can see the artifacts before +generating your own. See [`../README.md`](../README.md) for how to run it. + +**One deliberate quirk.** throughline is local-only by default and normally +gitignores the whole data dir - this demo tracks its `HANDOFF.md`, a session +log, and one live capture buffer instead, because those artifacts *are* the +example. That's why a session here prints a "not gitignored yet" warning on +`buffer/` - correct behavior, aimed at a real project, not a bug in the demo. diff --git a/demo/homelab/logs/link-check.log b/demo/homelab/logs/link-check.log new file mode 100644 index 0000000..019c55f --- /dev/null +++ b/demo/homelab/logs/link-check.log @@ -0,0 +1,20 @@ +2026-08-25 02:30:01 OK https://example.com/ (200) +2026-08-25 02:30:02 OK https://example.com/about (200) +2026-08-25 02:30:03 OK https://example.com/blog (200) +2026-08-25 02:30:04 FAIL https://status.example.com/ (405) +2026-08-26 02:30:01 OK https://example.com/ (200) +2026-08-26 02:30:02 OK https://example.com/about (200) +2026-08-26 02:30:03 OK https://example.com/blog (200) +2026-08-26 02:30:04 FAIL https://status.example.com/ (405) +2026-08-27 02:30:01 OK https://example.com/ (200) +2026-08-27 02:30:02 OK https://example.com/about (200) +2026-08-27 02:30:03 OK https://example.com/blog (200) +2026-08-27 02:30:04 FAIL https://status.example.com/ (405) +2026-08-28 02:30:01 OK https://example.com/ (200) +2026-08-28 02:30:02 OK https://example.com/about (200) +2026-08-28 02:30:03 OK https://example.com/blog (200) +2026-08-28 02:30:04 OK https://status.example.com/ (200) +2026-08-29 02:30:01 OK https://example.com/ (200) +2026-08-29 02:30:02 OK https://example.com/about (200) +2026-08-29 02:30:03 OK https://example.com/blog (200) +2026-08-29 02:30:04 OK https://status.example.com/ (200) diff --git a/demo/homelab/scripts/check-links.sh b/demo/homelab/scripts/check-links.sh new file mode 100755 index 0000000..5fb8e21 --- /dev/null +++ b/demo/homelab/scripts/check-links.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# +# Nightly link check. Reads urls.txt, HEAD-requests each URL, appends one +# result line per URL to logs/link-check.log. Run from cron at 2:30am. + +set -uo pipefail + +here=$(cd "$(dirname "$0")/.." && pwd) +urls="$here/urls.txt" +log="$here/logs/link-check.log" + +while IFS= read -r url; do + [ -n "$url" ] || continue + code=$(curl -s -o /dev/null -w '%{http_code}' -I "$url") + ts=$(date '+%Y-%m-%d %H:%M:%S') + if [ "$code" = "200" ]; then + printf '%s OK %s (%s)\n' "$ts" "$url" "$code" >> "$log" + else + printf '%s FAIL %s (%s)\n' "$ts" "$url" "$code" >> "$log" + fi +done < "$urls" diff --git a/demo/homelab/urls.txt b/demo/homelab/urls.txt new file mode 100644 index 0000000..7b7fc55 --- /dev/null +++ b/demo/homelab/urls.txt @@ -0,0 +1,4 @@ +https://example.com/ +https://example.com/about +https://example.com/blog +https://status.example.com/ diff --git a/demo/setup.sh b/demo/setup.sh new file mode 100755 index 0000000..9793cb3 --- /dev/null +++ b/demo/setup.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# +# Puts the hooks and skills into demo/homelab so the demo project is a real +# install, not a mockup. They live once in the repo; these copies are +# gitignored (see demo/homelab/.gitignore) and rebuilt fresh by this script. + +set -euo pipefail + +here=$(cd "$(dirname "$0")" && pwd) +root=$(cd "$here/.." && pwd) +project="$here/homelab" + +rm -rf "$project/.claude/hooks" "$project/.claude/skills" +mkdir -p "$project/.claude/hooks" "$project/.claude/skills" "$project/logs" + +install -m 755 "$root"/hooks/*.sh "$project/.claude/hooks/" +for skill in onboard handoff consolidate consolidate-memory; do + mkdir -p "$project/.claude/skills/$skill" + cp -R "$root/skills/$skill/." "$project/.claude/skills/$skill/" +done + +cat <<'EOF' +demo/homelab is ready. Open a session scoped to just this project: + + cd demo/homelab + claude --setting-sources project,local --strict-mcp-config \ + --tools Read,Glob,Grep,Bash,Skill,Write + +Ask: "where were we?" - the answer comes from HANDOFF.md, injected by the +SessionStart hook before your first message, not from a file you have to open. + +Then try the loop: "check logs/link-check.log for last night's run and update +the handoff if the third clean night landed." Watch the capture buffer grow at +.claude/throughline/buffer/session-.md as you go, then ask for a handoff +and see it distilled into HANDOFF.md and a new session log. + +Claude Code will ask you to trust this folder, because it carries a project +hook in .claude/settings.json. The hooks at .claude/hooks/*.sh are copies of +the real throughline/hooks/*.sh, placed here by this script: read them before +you accept, the same as any repo you clone. +EOF diff --git a/docs/INSTALL.md b/docs/INSTALL.md new file mode 100644 index 0000000..45f702f --- /dev/null +++ b/docs/INSTALL.md @@ -0,0 +1,150 @@ +# Install + +Per-harness requirements, what each install gives you, the Codex trust step, and how +to update or point at a local checkout. The [README](../README.md) has the one-liner +commands and the capability comparison table; this is the detail behind them. + +## Claude Code + +``` +/plugin marketplace add dynamic/throughline # register this repo as a marketplace +/plugin install throughline@throughline # install plugin@marketplace (same name) +``` + +Then reload (`/reload-plugins`) or restart the session. + +**Requirements:** `git` and `jq` on your `PATH`. `jq` parses the hook payloads; if it +is missing, capture cannot run and the SessionStart block says so rather than failing +silently. + +**What you get:** the 4 skills (`handoff`, `onboard`, `consolidate`, +`consolidate-memory`) plus all 5 hooks. The only harness with a native durable +memory system of its own (`/memory`, backed by `MEMORY.md`) - the session-start +injection above complements that auto-load with project-level state, and the +`handoff`/`consolidate-memory` skills promote genuinely durable facts into it. +Codex and OpenCode have no equivalent system to bind into today. + +**Updating.** Installed plugins are snapshots - they do not track this repo. An old +copy keeps running (without newer redaction and activation fixes) until you update it +from the `/plugin` manager (or uninstall and reinstall), then `/reload-plugins`. The +SessionStart block prints the running version (`## throughline vX.Y.Z`) - if it lags +this repo's releases, your install is stale. + +## Codex CLI + +```sh +codex plugin marketplace add dynamic/throughline +codex plugin add throughline@throughline +``` + +**Requirements:** `git` and `jq` on your `PATH`, same as Claude Code - Codex runs +the identical hook scripts. Without `jq`, capture cannot run. Works in Codex CLI and +Codex Desktop. + +**What you get:** the 4 skills plus all 5 hooks, reading and writing the same +`.claude/throughline/` data format Claude Code and OpenCode use - so a project's +history is readable and continuable from any of the three. + +**The one-time trust step.** Codex gates hook execution behind a one-time trust +decision per machine (Claude Code has no equivalent gate - a plugin's hooks just run +once installed). What that looks like the first time you use a project with +throughline installed: + +- **Codex CLI** shows a native **"Hooks need review"** dialog before your first + message: "5 hooks are new or changed. Hooks can run outside the sandbox after you + trust them." Choose **"Trust all and continue."** That's the whole step - trust is + granted by content hash, not by project path, so it covers every project at once. + A throughline update that changes the hook scripts triggers the dialog again. +- **Codex Desktop** grants trust silently, with no dialog - capture just starts + working on your first message. + +Verify anytime with the in-TUI **`/hooks`** command: it lists every Codex hook event +with an Installed/Active count, and pressing Enter on a row shows that hook's +`Source`, `Command`, `Mode`, `Timeout`, and `Trust` status. A trusted throughline +hook reads `Source: Plugin - throughline@throughline`, `Trust: Trusted`. + +**Updating.** Same story as Claude Code: an installed plugin is a snapshot, not a +live checkout. Update from `codex plugin add throughline@throughline` again (or the +Codex plugin manager) to pick up the latest release - a throughline update also +changes the hook scripts' content hash, so the trust dialog reappears once on Codex +CLI. + +## OpenCode + +throughline is also available as an OpenCode plugin, providing the same session +capture functionality within the OpenCode ecosystem. + +```json +{ + "plugin": ["@dynamicagency/throughline-opencode"] +} +``` + +OpenCode's plugin config key is `plugin` (singular) in `opencode.json`, and each +entry is either an npm package name or a local path - there is no separate +`plugins/` directory to copy into. Add the line above and restart OpenCode; it +installs the package automatically via Bun at startup. Check +`~/.local/share/opencode/log/opencode.log` for a load error if session capture +doesn't appear to be running. + +**Requirements:** Node.js 18+ (no `jq` required - TypeScript uses native JSON +parsing). + +**What you get:** all 5 hooks, ported to TypeScript against OpenCode's own plugin +API - continuous prompt/action capture with redaction, session-start context +injection (HANDOFF.md pointer + live git state), and compaction survival (a boundary +marker plus buffer-tail re-injection right after). One behavioral difference worth +knowing: OpenCode's `session.idle` event fires after every turn, not once at process +exit the way Claude Code's `SessionEnd` does, so the buffer's end-marker is a "last +known idle point" that gets re-stamped each time the session goes idle, rather than +a one-shot end-of-session stamp - `onboard` reads it the same way either way (has +this buffer seen activity since the marker). This plugin ships **hooks only, +no skills** - OpenCode's own plugin API has no supported way to ship a skill +directory alongside a plugin today. Run `npx skills add dynamic/throughline` +separately for `handoff`/`onboard`/`consolidate`/`consolidate-memory`. In practice +this is often a non-issue: OpenCode discovers `SKILL.md` files from several +locations it shares with Claude Code and Codex (project-local and global +`.claude/skills/`, `.agents/skills/`, and its own `.opencode/skills/` / +`~/.config/opencode/skills/`), so skills installed for another harness on the same +machine are frequently already visible to OpenCode with no extra step. + +By default the OpenCode plugin uses the same `.claude/throughline/` data directory +as Claude Code and Codex, so a project's history stays continuous across harnesses. + +**Updating.** An installed plugin is a versioned snapshot, same as Claude Code and +Codex - bumping the version in `opencode.json` (or letting Bun resolve a new +range) is what picks up a release, not a `git pull`. The running version is +printed in the injected session-start block (`## throughline vX.Y.Z`) the same way +it is on Claude Code and Codex - if it lags this repo's releases, your install is +stale. + +Publishing to npm is tag-triggered: pushing a `vX.Y.Z` tag runs a GitHub Actions +workflow that publishes `@dynamicagency/throughline-opencode` via npm Trusted +Publishing (OIDC), with no long-lived npm token and an automatic provenance +attestation on the published package. + +**Local-path install (testing unreleased changes).** Point `opencode.json` at a +checkout of this repo instead of the package name: + +```json +{ + "plugin": ["/absolute/path/to/throughline/.opencode-plugin"] +} +``` + +The package's `main` field points at compiled `dist/`, which is gitignored, so a +local-path install needs a build first: `cd .opencode-plugin && npm ci && npm run +build`. Unlike the npm install, this stays a live checkout - `git pull` and +rebuild to update it. + +## npx skills + +```sh +npx skills add dynamic/throughline +``` + +Installs the 4 skills directly - no plugin system, no marketplace registration. This +is the fallback for any harness that reads `SKILL.md` files from disk but has no +plugin system of its own. **What you get:** the 4 skills, nothing else - no +automatic capture (there's no hook mechanism in this delivery form at all); run +`handoff` manually at the end of a session. diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md new file mode 100644 index 0000000..7d40f14 --- /dev/null +++ b/docs/REFERENCE.md @@ -0,0 +1,228 @@ +# Reference + +Configuration, git worktrees, opt-out, and housekeeping detail for throughline. The +[README](../README.md) covers the pitch, how it works, and install; this is where the +knobs and edge cases live. + +## Configuration + +By default, state lives in **`.claude/throughline/`** in each project (the universal +Claude Code workspace dir). Override the location with an environment variable: + +```sh +# Opt in to a portable .agent/ handoff convention - e.g. for cross-harness use, +# or a team that has agreed to commit its handoffs (see "Local by default" below): +export THROUGHLINE_DATA_DIR=.agent/handoff +``` + +- Relative values resolve against the project root; absolute values are used as-is. +- throughline auto-activates in every project: the first time any hook fires it + creates its data dir on demand, so capture starts working immediately with no + manual opt-in. To keep it out of a specific project, drop an empty + `.throughlineignore` file at the project root (see "Opting a project out" below). + +### Git worktrees + +In a **linked git worktree** (e.g. Claude Code's `claude/` auto-worktree +workflow, under `/.claude/worktrees//`), "the project root" above +resolves to the **main working tree**, not the worktree itself - so every worktree +of a repo, plus its main checkout, share one `HANDOFF.md`/`logs/`/`buffer/` instead +of each worktree silently accumulating its own. The session-start capture point +prints a note when this redirect is active. Live git state (current branch, +`git status`) and captured file paths still describe the worktree you're actually in. + +Set `THROUGHLINE_WORKTREE_SHARED=0` to opt back into isolated per-worktree data +dirs. Requires git 2.31+; falls back to per-worktree behavior for bare repos, +submodules, and older git. + +### Opting a project out + +throughline activates automatically in every project. To disable it for one +project, add an empty marker file at the project root: + +```sh +touch .throughlineignore +``` + +**In a linked git worktree** (see "Git worktrees" above), place this at the +**main** working tree's root, not the worktree you're sitting in - that's where +the opt-out check now looks by default. (A marker already sitting in a worktree +from before worktree-sharing existed is still honored there too, so upgrading +never silently re-enables a pre-existing opt-out.) + +With that file present, no new data dir is created, and `onboard`/`capture` stop +adding anything new - regardless of `THROUGHLINE_DATA_DIR` or any pre-existing +`.claude/throughline/`. The opt-out wins even over a project that was already +active: existing `HANDOFF.md`/`logs/` are left in place, and no *new* activity is +recorded. One nuance: if a session was already being captured when the file +appears, `flush`/`precompact` still finalize that one session's already-existing +buffer (its end-stamp or compaction marker) rather than leaving it in a permanent +"still live?" limbo - they don't create anything new, they just avoid corrupting +bookkeeping for work that had already legitimately started. Remove the file to +re-enable. Commit it like `.gitignore` so the policy is shared with teammates. + +### Disabling machine-wide + +To turn throughline off everywhere without uninstalling or touching every project, +set the kill switch (e.g. in `~/.claude/settings.json`'s `env` block, or your shell +profile): + +```sh +export THROUGHLINE_DISABLE=1 +``` + +Any value other than `0` disables **all five hooks completely** - no capture, no +SessionStart block (not even about existing data), no end-stamps. This is stricter +than `.throughlineignore`, which keeps orienting toward already-existing content. +Unset it (or set `0`) to re-enable; existing data is untouched either way. + +**Cross-harness handoffs.** The data dir is the one knob that makes throughline +portable. Point it at `.agent/handoff` (or any other path) and the durable +`HANDOFF.md` it produces lives in a harness-neutral location any agent can read, +not buried under a Claude-Code-specific path - useful if other tooling also drives +this project. Portability of the *location* is independent of whether you commit +it - see "Local by default" below. + +### Local by default + +throughline's data - `HANDOFF.md`, `logs/`, `buffer/`, everything under the data +dir - is **per-operator working memory, not a shared team artifact**, and stays +local (gitignored) by default. Gitignore the whole data dir for whichever location +you use: + +```gitignore +# default layout +.claude/throughline/ +# or, if you set THROUGHLINE_DATA_DIR=.agent/handoff +.agent/handoff/ +``` + +**Team projects.** On a project with multiple developers - especially ones not +using throughline, or already running their own memory/notes tooling - committing +one operator's session artifacts into the shared tree causes real friction: churn +and merge conflicts on the single mutable `HANDOFF.md`, review noise on every PR, +and possible collision with whatever a teammate already relies on. Local-only +avoids all of it: nothing throughline writes reaches a teammate's checkout unless +you deliberately choose to share it. + +**Opting in to tracking.** For a solo repo, or a team that has all adopted +throughline, committing `HANDOFF.md` + `logs/` gives fresh clones and teammates a +shared, readable project record - genuinely useful when everyone is actually +reading it. To opt in, un-ignore just those two paths (keep `buffer/` and +`.capture-errors` ignored always - `buffer/` is scratch and can contain unredacted +command text, and `.capture-errors` is a scratch breadcrumb file): + +```gitignore +.claude/throughline/* +!.claude/throughline/HANDOFF.md +!.claude/throughline/logs/ +``` + +The `handoff` skill's Phase 4 offers (never auto-runs, and relevant +only once you've opted in as above) to stage exactly `HANDOFF.md` + the new +session log and commit/push them - it checks `git check-ignore` first and skips +the offer entirely when the files aren't actually committable in your layout. + +> **Heads-up for allowlist-style `.gitignore`.** If your repo ignores everything +> by default (a root `/*` then `!/keep` pattern) and you *do* want to opt in to +> tracking, re-including just the two leaf paths does **not** work - git prunes +> an excluded directory before it ever evaluates negation patterns for paths +> inside it, so `.claude` (matched by the root `/*`) is never even descended +> into. The simplest fix is `THROUGHLINE_DATA_DIR=.agent/handoff` so the +> opted-in artifacts sit outside the ignored tree entirely. To keep the default +> location instead, negate **every ancestor directory** on the way down, then +> re-exclude the scratch paths (which the ancestor negations would otherwise +> expose too): +> ```gitignore +> !/.claude/ +> !/.claude/throughline/ +> !/.claude/throughline/HANDOFF.md +> !/.claude/throughline/logs/ +> .claude/throughline/buffer/ +> .claude/throughline/.capture-errors +> ``` + +> **A tracked `HANDOFF.md` is input the agent acts on, not passive documentation.** +> The `SessionStart` hook injects it straight into a fresh session's context, and +> the `onboard`/`handoff` skills tell the agent to act on what it says. Tracking it +> means anyone who can push to that repo writes into every future session's +> context - the same caution that applies to a tracked `CLAUDE.md`, but less +> obvious here because the injection is automatic rather than something a human +> opens on purpose. Keep tracking to repos whose writers you'd trust to shape +> agent behavior. + +## Housekeeping + +Everything throughline writes grows without automatic bound: there is no +background cleanup process, deliberately, to keep the plugin's footprint at +"pure POSIX sh + jq, zero infrastructure." What's safe to clean up by hand, +and what isn't: + +**Safe to delete:** +- `buffer/archive/*.md` older than your last `consolidate` pass - + once a consolidation has mined a log for recurring lessons, an archived raw + buffer behind it has nothing left to give. As a simple rule of thumb, an + archived buffer older than ~90 days with no open question against it is safe + to remove. +- `.capture-errors`, once its contents have been surfaced in a session log and + cleared by the handoff skill (Phase 4): it's a breadcrumb meant to be read + once, not a running log. + +**Not safe to delete:** +- `logs/`: these are the evidence trail. `consolidate` explicitly + never prunes them, and HANDOFF.md's own "Recent Session Logs" list only ever + points at the last 5, so older logs are already off the beaten path without + needing to be deleted. +- `HANDOFF.md` itself, obviously - it's the durable record. +- Any buffer still in `buffer/` (not yet archived) - it may be an in-progress or + unconsumed session; run a handoff first, which moves it to `archive/` once + distilled. + +There's no automated retention policy beyond this: clean up by hand on the +cadence above, or leave it, a growing `archive/` costs disk, not correctness. + +## Layout + +``` +throughline/ +├─ .claude-plugin/ +│ ├─ plugin.json +│ └─ marketplace.json +├─ .codex-plugin/ +│ └─ plugin.json # declares skills; hooks found via convention in hooks/ +├─ .agents/plugins/ +│ └─ marketplace.json # Codex marketplace entry, mirrors .claude-plugin's +├─ .opencode-plugin/ +│ ├─ package.json # Node dependencies + plugin entry point (main) +│ ├─ tsconfig.json # TypeScript config +│ ├─ .gitignore # Excludes node_modules/ and dist/ +│ └─ src/ +│ ├─ index.ts # Plugin entry point +│ ├─ lib.ts # Core library (data dir, session ID, buffer) +│ ├─ hooks/ # All 5 hook implementations +│ │ ├─ session-created.ts +│ │ ├─ chat-message.ts +│ │ ├─ tool-execute-after.ts +│ │ ├─ session-compacted.ts +│ │ └─ session-idle.ts +│ ├─ utils/ +│ │ ├─ redaction.ts # Redaction logic ported from jq to TypeScript +│ │ └─ redaction.test.ts +│ └─ integration.test.ts +├─ hooks/ +│ ├─ hooks.json +│ ├─ _lib.sh # data-dir resolution + activation gate + jq/sid/redaction helpers +│ ├─ session-onboard.sh # SessionStart: pointer, git state, compaction recovery +│ ├─ session-prompt.sh # UserPromptSubmit: redacted, truncated user-intent line +│ ├─ session-capture.sh # PostToolUse: structured action buffer (outcome + redaction) +│ ├─ session-precompact.sh # PreCompact: stamp the compaction-boundary marker +│ └─ session-flush.sh # SessionEnd: safety-net stamp +├─ skills/ +│ ├─ onboard/SKILL.md # full orientation +│ ├─ handoff/SKILL.md # judged distillation + memory binding +│ ├─ consolidate/SKILL.md # periodic promotion of recurring lessons +│ └─ consolidate-memory/SKILL.md # native-memory file hygiene +├─ tests/run.sh # fixture-driven hook tests (shellcheck + CI) +├─ docs/ # promo site + reference docs +└─ CHANGELOG.md +``` From 41495a4c0563cd05013d8324b219e23da430de87 Mon Sep 17 00:00:00 2001 From: Jason Irish Date: Tue, 1 Sep 2026 10:54:43 -0500 Subject: [PATCH 2/2] fix(#68): correct demo script, broken link, and inaccurate gitignore comment /review-pr on PR #73 found four real issues: - demo/homelab/scripts/check-links.sh still sent a HEAD request (curl -I), contradicting the demo's own HANDOFF.md and session log, both of which claim the fix (switch to GET) already landed. - demo/homelab/README.md linked ../README.md, which resolves to a nonexistent demo/README.md - needed ../../README.md. - README.md's OpenCode session-end cross-reference pointed at the README's own (now content-free) #opencode section instead of docs/INSTALL.md#opencode, where the session.idle explanation actually moved to. - demo/homelab/.gitignore's negation comment claimed the root .gitignore's .claude/throughline/ pattern matches at every depth; it doesn't (it's repo-relative), so six negation lines were dead weight and the comment taught something untrue about gitignore anchoring. Removed the no-op lines, corrected the comment. Also: the demo's link-check.log ended at Aug 29 with no third clean night logged, so the walkthrough's own scripted prompt ("update the handoff if the third clean night landed") always resolved to "not yet" - a no-op. Added the Aug 30 clean run so trying the demo as documented produces a real distill. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01UXL2ffVDsAp4P7387Nr18G --- README.md | 2 +- demo/homelab/.gitignore | 19 +++++++++---------- demo/homelab/README.md | 2 +- demo/homelab/logs/link-check.log | 4 ++++ demo/homelab/scripts/check-links.sh | 6 ++++-- 5 files changed, 19 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 323a4cc..b9142de 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ does the full orientation pass (open PRs/issues, deep read) on demand. | User intent | `UserPromptSubmit` hook | `chat.message` hook | | Action | `PostToolUse` hook | `tool.execute.after` hook | | Compaction boundary | `PreCompact` hook, then `SessionStart` re-fires with `source=compact` | `session.compacted` event, which queues the recovery block for the next context injection | -| Session end | `SessionEnd` hook | `session.idle` event (fires after every turn, not once at exit - see [OpenCode](#opencode)) | +| Session end | `SessionEnd` hook | `session.idle` event (fires after every turn, not once at exit - see [docs/INSTALL.md#opencode](docs/INSTALL.md#opencode)) | Claude Code and Codex CLI share the identical shell hook scripts; OpenCode's plugin is a TypeScript port of the same logic against OpenCode's own API. diff --git a/demo/homelab/.gitignore b/demo/homelab/.gitignore index e2d6b88..b31808d 100644 --- a/demo/homelab/.gitignore +++ b/demo/homelab/.gitignore @@ -3,16 +3,15 @@ .claude/hooks/ .claude/skills/ -# The repo root's .gitignore ignores .claude/throughline/ everywhere (this -# project dogfoods "local by default"). The demo is the one deliberate -# exception: its throughline data IS the example, so re-include exactly the -# artifacts a real install would produce and nothing else. -!.claude/throughline/ -!.claude/throughline/HANDOFF.md -!.claude/throughline/logs/ -!.claude/throughline/logs/*.md -!.claude/throughline/buffer/ -!.claude/throughline/buffer/*.md +# The repo root's `.claude/throughline/` line is a repo-relative pattern (it +# has a `/` in the middle), so it only ever matches the repo root's own data +# dir - nothing here ignores this subtree by default. throughline's data is +# still per-operator working memory, not something a real project would +# track (see the root README's "Local by default"); the demo is the +# deliberate exception, since its throughline data IS the example. Only the +# scratch paths below need excluding - a user running the demo will see +# their own rewritten HANDOFF.md/session log/buffer as uncommitted changes, +# which is expected. .claude/throughline/buffer/archive/ .claude/throughline/.capture-errors diff --git a/demo/homelab/README.md b/demo/homelab/README.md index 06ee774..f8d424e 100644 --- a/demo/homelab/README.md +++ b/demo/homelab/README.md @@ -6,7 +6,7 @@ from cron at 2:30am. This is throughline's demo project: a fictional repo with a real, populated `.claude/throughline/` already in it, so you can see the artifacts before -generating your own. See [`../README.md`](../README.md) for how to run it. +generating your own. See [`../../README.md`](../../README.md) for how to run it. **One deliberate quirk.** throughline is local-only by default and normally gitignores the whole data dir - this demo tracks its `HANDOFF.md`, a session diff --git a/demo/homelab/logs/link-check.log b/demo/homelab/logs/link-check.log index 019c55f..d295725 100644 --- a/demo/homelab/logs/link-check.log +++ b/demo/homelab/logs/link-check.log @@ -18,3 +18,7 @@ 2026-08-29 02:30:02 OK https://example.com/about (200) 2026-08-29 02:30:03 OK https://example.com/blog (200) 2026-08-29 02:30:04 OK https://status.example.com/ (200) +2026-08-30 02:30:01 OK https://example.com/ (200) +2026-08-30 02:30:02 OK https://example.com/about (200) +2026-08-30 02:30:03 OK https://example.com/blog (200) +2026-08-30 02:30:04 OK https://status.example.com/ (200) diff --git a/demo/homelab/scripts/check-links.sh b/demo/homelab/scripts/check-links.sh index 5fb8e21..44ffc26 100755 --- a/demo/homelab/scripts/check-links.sh +++ b/demo/homelab/scripts/check-links.sh @@ -1,7 +1,9 @@ #!/usr/bin/env bash # -# Nightly link check. Reads urls.txt, HEAD-requests each URL, appends one +# Nightly link check. Reads urls.txt, GET-requests each URL, appends one # result line per URL to logs/link-check.log. Run from cron at 2:30am. +# GET, not HEAD: status.example.com's load balancer 405s HEAD requests +# (see .claude/throughline/HANDOFF.md). set -uo pipefail @@ -11,7 +13,7 @@ log="$here/logs/link-check.log" while IFS= read -r url; do [ -n "$url" ] || continue - code=$(curl -s -o /dev/null -w '%{http_code}' -I "$url") + code=$(curl -s -o /dev/null -w '%{http_code}' "$url") ts=$(date '+%Y-%m-%d %H:%M:%S') if [ "$code" = "200" ]; then printf '%s OK %s (%s)\n' "$ts" "$url" "$code" >> "$log"