diff --git a/README.md b/README.md index 089b0d8..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. @@ -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..b31808d --- /dev/null +++ b/demo/homelab/.gitignore @@ -0,0 +1,20 @@ +# 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 `.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 + +# 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..f8d424e --- /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..d295725 --- /dev/null +++ b/demo/homelab/logs/link-check.log @@ -0,0 +1,24 @@ +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) +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 new file mode 100755 index 0000000..44ffc26 --- /dev/null +++ b/demo/homelab/scripts/check-links.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# +# 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 + +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}' "$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 +```