@sayansr26/agent-os 0.6.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/.claude-plugin/marketplace.json +23 -0
  2. package/CHANGELOG.md +39 -1
  3. package/README.md +32 -0
  4. package/package.json +2 -2
  5. package/plugins/agent-os/.claude-plugin/icon.png +0 -0
  6. package/plugins/agent-os/.claude-plugin/plugin.json +2 -0
  7. package/plugins/auto-handoff/.claude-plugin/plugin.json +49 -0
  8. package/plugins/auto-handoff/LICENSE +21 -0
  9. package/plugins/auto-handoff/README.md +131 -0
  10. package/plugins/auto-handoff/UPSTREAM.md +22 -0
  11. package/plugins/auto-handoff/hooks/brief.ts +190 -0
  12. package/plugins/auto-handoff/hooks/config.ts +64 -0
  13. package/plugins/auto-handoff/hooks/hooks.json +1 -0
  14. package/plugins/auto-handoff/hooks/panel.tsx +45 -0
  15. package/plugins/auto-handoff/hooks/register.tsx +615 -0
  16. package/plugins/auto-handoff/hooks/server.ts +32 -0
  17. package/plugins/auto-handoff/hooks/templates.ts +17 -0
  18. package/plugins/auto-handoff/hooks/viewer.ts +173 -0
  19. package/plugins/auto-handoff/statusline/handoff-link.sh +15 -0
  20. package/plugins/auto-handoff/templates/brief.md +34 -0
  21. package/plugins/auto-handoff/templates/instructions.md +10 -0
  22. package/plugins/auto-handoff/tests/auto-handoff.test.tsx +931 -0
  23. package/plugins/auto-handoff/tests/seed-row.test.tsx +58 -0
  24. package/plugins/auto-handoff/tests/viewer.test.ts +111 -0
  25. package/plugins/auto-handoff/tsconfig.json +4 -0
  26. package/plugins/auto-route/.claude-plugin/plugin.json +18 -0
  27. package/plugins/auto-route/hooks/hooks.json +1 -0
  28. package/plugins/auto-route/hooks/register.ts +143 -0
  29. package/plugins/auto-route/tests/route.test.ts +87 -0
  30. package/plugins/auto-route/tsconfig.json +3 -0
  31. package/plugins/auto-route/types/index.d.ts +12 -0
  32. package/src/cli.mjs +25 -15
  33. package/src/plugin.mjs +39 -23
  34. package/src/selftest.mjs +9 -0
@@ -22,6 +22,29 @@
22
22
  "orchestration",
23
23
  "codebase-navigation"
24
24
  ]
25
+ },
26
+ {
27
+ "name": "auto-route",
28
+ "source": "./plugins/auto-route",
29
+ "description": "Picks the model and effort level for each turn from what the prompt asks for. /route pins a tier or turns routing off.",
30
+ "category": "productivity",
31
+ "keywords": [
32
+ "model-routing",
33
+ "effort",
34
+ "mod"
35
+ ]
36
+ },
37
+ {
38
+ "name": "auto-handoff",
39
+ "source": "./plugins/auto-handoff",
40
+ "description": "Hands a long session off to a fresh one instead of auto-compacting: Haiku writes a structured brief, the mod runs /clear and seeds the new session with it. Vendored from alexknowshtml/claude-auto-handoff (MIT).",
41
+ "category": "productivity",
42
+ "keywords": [
43
+ "context",
44
+ "handoff",
45
+ "compaction",
46
+ "mod"
47
+ ]
25
48
  }
26
49
  ]
27
50
  }
package/CHANGELOG.md CHANGED
@@ -5,7 +5,45 @@ All notable changes to this project are documented here.
5
5
  The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
6
6
  this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [0.6.0] — 2026-09-25
8
+ ## [0.6.2] — 2026-10-22
9
+
10
+ agent-os plugin unchanged (0.5.0), auto-route unchanged (0.1.0). New plugin
11
+ auto-handoff 0.8.6 in the same marketplace.
12
+
13
+ ### Added
14
+ - **auto-handoff, a third plugin in the `sayan-plugins` marketplace**, vendored
15
+ from [alexknowshtml/claude-auto-handoff](https://github.com/alexknowshtml/claude-auto-handoff)
16
+ by Alex Hillman (MIT, commit `8d762ce`). In place of auto-compact, Haiku
17
+ writes a structured brief, the mod runs `/clear`, and the fresh session is
18
+ seeded with a pointer to the brief. `/plugin install auto-handoff@sayan-plugins`.
19
+ The brief viewer server is **off** by default here (upstream serves on the
20
+ Tailscale IP); `plugins/auto-handoff/UPSTREAM.md` lists every local change.
21
+ - **`agent-os init` installs and updates auto-handoff** with the other two;
22
+ `--no-auto-handoff` leaves it out.
23
+
24
+ ### Changed
25
+ - `scripts/validate-plugin.mjs` lets a mod carry a listing `icon.png`, as the
26
+ agent-os plugin now does.
27
+
28
+ ## [0.6.1] — 2026-10-21
29
+
30
+ agent-os plugin unchanged (0.5.0). New plugin auto-route 0.1.0 in the same
31
+ marketplace. 0.7.0 stays reserved for the move to Rulegate.
32
+
33
+ ### Added
34
+ - **auto-route, a second plugin in the `sayan-plugins` marketplace.** A Claude
35
+ Code mod (a TypeScript hooks module) that picks the model and effort for each
36
+ turn from what the prompt asks for: lookups to Haiku, regular coding to
37
+ Sonnet, design and debugging to Opus, "ultrathink" and security work to Opus
38
+ at max effort. Subagents keep their own model. `/route` shows the route,
39
+ `/route light|standard|deep|max` pins a tier, `/route off` and `/route auto`
40
+ switch it off and on. `/plugin install auto-route@sayan-plugins`.
41
+ - **`agent-os init` installs and updates auto-route** at project scope next to
42
+ agent-os, adding or refreshing the marketplace once. `--no-auto-route` leaves
43
+ it out; `--no-plugin` still skips both.
44
+ - `scripts/validate-plugin.mjs` checks every other marketplace plugin built
45
+ from hook modules: manifest, name against its marketplace entry, the types
46
+ contract and each module it names.
9
47
 
10
48
  Plugin 0.5.0. Found by running 0.4.1 in a real project: the plugin passed its
11
49
  own audit while Claude ignored it — a whole feature was built without the
package/README.md CHANGED
@@ -140,6 +140,38 @@ Feature directories default to the first of `src/features`, `src/modules`, `app/
140
140
  { "targets": ["claude-code"], "claude": { "features": ["src/features/*", "apps/*"], "cartographerReminder": true } }
141
141
  ```
142
142
 
143
+ ### auto-route
144
+
145
+ A second, independent plugin in the same marketplace: a Claude Code mod (a TypeScript hooks module) that picks the model and effort for each turn from what the prompt asks for — lookups go to Haiku, regular coding to Sonnet, design and debugging to Opus, "ultrathink" and security work to Opus at max effort. Subagents keep their own model.
146
+
147
+ ```
148
+ /plugin install auto-route@sayan-plugins
149
+ ```
150
+
151
+ `agent-os init` installs and updates it alongside agent-os, at project scope; `--no-auto-route` leaves it out.
152
+
153
+ `/route` shows the current route; `/route light|standard|deep|max` pins a tier, `/route off` hands control back to the session's own model, `/route auto` resumes routing. The tier table and the prompt rules sit at the top of `plugins/auto-route/hooks/register.ts`.
154
+
155
+ Working on it:
156
+
157
+ ```sh
158
+ claude --plugin-dir plugins/auto-route # run a session with the working copy, hot-reloaded
159
+ claude plugin validate plugins/auto-route # what the engine would refuse
160
+ claude plugin test plugins/auto-route # tests/route.test.ts
161
+ ```
162
+
163
+ Once the engine loads it, it writes `.claude-plugin/types/` (git-ignored) for the editor; `tsc -p plugins/auto-route` type-checks it after that. Bump `version` in its `plugin.json` when shipping a change so installs update.
164
+
165
+ ### auto-handoff
166
+
167
+ A third plugin, vendored from [alexknowshtml/claude-auto-handoff](https://github.com/alexknowshtml/claude-auto-handoff) by Alex Hillman (MIT). Instead of auto-compacting at the context limit, it has Haiku write a structured brief, runs `/clear`, and seeds the fresh session with a pointer to the brief, so work carries on with a clean context. Settings (threshold, templates, ignored files) are rows in `/config`.
168
+
169
+ ```
170
+ /plugin install auto-handoff@sayan-plugins
171
+ ```
172
+
173
+ `agent-os init` installs it too; `--no-auto-handoff` leaves it out. One change from upstream: the brief viewer server is **off** by default here (upstream serves pages on your Tailscale IP); set `viewer` in `/config` to turn it on. `plugins/auto-handoff/UPSTREAM.md` records the upstream commit, the local changes and how to sync. Develop it the same way as auto-route: `claude --plugin-dir plugins/auto-handoff`, `claude plugin validate|test plugins/auto-handoff`.
174
+
143
175
  ---
144
176
 
145
177
  ## Design rules
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sayansr26/agent-os",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "One source of truth for AI coding agent config. Write your rules once; compile them to Claude Code, Cursor, Cline, Windsurf, Antigravity, Gemini CLI, OpenCode and Kilo.",
5
5
  "keywords": [
6
6
  "ai",
@@ -46,4 +46,4 @@
46
46
  "test": "node scripts/validate-plugin.mjs && node src/selftest.mjs",
47
47
  "prepublishOnly": "npm test"
48
48
  }
49
- }
49
+ }
@@ -8,6 +8,8 @@
8
8
  },
9
9
  "homepage": "https://github.com/sayansr26/agent-os",
10
10
  "repository": "https://github.com/sayansr26/agent-os",
11
+ "privacyPolicyUrl": "https://s3techpulse.com/privacy-policy",
12
+ "supportUrl": "https://s3techpulse.com/support",
11
13
  "license": "MIT",
12
14
  "keywords": [
13
15
  "context-engineering",
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "auto-handoff",
3
+ "version": "0.8.6",
4
+ "description": "Hands a Claude Code session off to a fresh one before the context fills: a Haiku-written brief, /clear, and a seed that points the new session at the brief.",
5
+ "author": {
6
+ "name": "Alex Hillman",
7
+ "url": "https://github.com/alexknowshtml"
8
+ },
9
+ "repository": "https://github.com/alexknowshtml/claude-auto-handoff",
10
+ "license": "MIT",
11
+ "userConfig": {
12
+ "threshold": {
13
+ "type": "number",
14
+ "title": "Handoff threshold (tokens)",
15
+ "description": "Context size that triggers a handoff. AUTO_HANDOFF_TOKENS overrides it for a test run. A seeded session hands off no sooner than 40k past its starting size; the panel above the prompt says so when this is set lower.",
16
+ "default": 160000
17
+ },
18
+ "maxConsecutiveHandoffs": {
19
+ "type": "number",
20
+ "title": "Handoffs in a row without a message",
21
+ "description": "Handoffs allowed before you type a prompt. Past this the mod pauses until you send one.",
22
+ "default": 2
23
+ },
24
+ "briefTemplate": {
25
+ "type": "string",
26
+ "title": "Brief template file",
27
+ "description": "Markdown file that tells Haiku which sections to write. Created with the defaults on first run; edit it to change the brief.",
28
+ "default": "~/.claude/auto-handoff/brief.md"
29
+ },
30
+ "instructionsTemplate": {
31
+ "type": "string",
32
+ "title": "Instructions template file",
33
+ "description": "Markdown file that tells the fresh session how to read the brief. Created with the defaults on first run.",
34
+ "default": "~/.claude/auto-handoff/instructions.md"
35
+ },
36
+ "ignoreFiles": {
37
+ "type": "string",
38
+ "title": "Ignored files (regex)",
39
+ "description": "Edited files matching this pattern are left out of the brief, such as auto-synced state or caches. Leave blank for none.",
40
+ "default": ""
41
+ },
42
+ "viewer": {
43
+ "type": "string",
44
+ "title": "Viewer address",
45
+ "description": "Where the mod serves a readable page for each brief, as host:port, e.g. 127.0.0.1:3846. \"tailscale\" as the host means this machine's Tailscale IP, so only your tailnet can open it; without Tailscale it serves on 127.0.0.1. Blank (the default here) runs no server; the link is then the local file.",
46
+ "default": ""
47
+ }
48
+ }
49
+ }
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alex Hillman
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,131 @@
1
+ # claude-auto-handoff
2
+
3
+ > Vendored into the `sayan-plugins` marketplace from [alexknowshtml/claude-auto-handoff](https://github.com/alexknowshtml/claude-auto-handoff) (MIT, © 2026 Alex Hillman). See [UPSTREAM.md](UPSTREAM.md) for the commit and what differs here.
4
+
5
+ A Claude Code mod that hands a long session off to a fresh one before the context fills up. It replaces auto-compact.
6
+
7
+ At the threshold, Haiku writes a structured handoff brief to disk. Then the mod runs `/clear` and seeds the new session with one line that points at the brief. The fresh session reads the brief and keeps working.
8
+
9
+ ![auto-handoff in a live session: the tool gate stops a read at the threshold, the panel walks through the brief and /clear, and the fresh session picks the work back up](https://raw.githubusercontent.com/alexknowshtml/claude-auto-handoff/8d762ce726c962268e2dbe24c1c60f0889a2fcde/docs/demo.gif)
10
+
11
+ A live run on Haiku with the threshold at 80k. The mod refuses a read at the threshold, writes the brief, clears, and the fresh session is back at work about 7 seconds later at 31k. ([video](https://github.com/alexknowshtml/claude-auto-handoff/blob/8d762ce726c962268e2dbe24c1c60f0889a2fcde/docs/demo.mp4))
12
+
13
+ ## Why not auto-compact?
14
+
15
+ Auto-compact summarizes in place, and you can't control what it keeps. A handoff brief has a fixed structure that you can edit. It covers work in progress, decisions, assumptions to verify, dead ends, your last request and whether it was answered, and the next step. The files, commits and issues sections come from the transcript in code, so they don't depend on the model's memory.
16
+
17
+ ## What happens
18
+
19
+ 1. **Threshold.** The mod checks the context size after each turn and before each model request, including tool output that hasn't been measured yet. Once it's past the threshold, the mod refuses new tool calls, so one burst of reads can't overflow the window. Unmeasured tool output is an estimate that can run high, so the real size sometimes comes in under the threshold. A refused call still ends in a handoff, at the next request or the end of the turn.
20
+ 2. **Brief.** Haiku writes the brief from the transcript. If Haiku fails, a facts-only brief stands in. Briefs go to `~/.claude/state/auto-handoff/<session-id>.md`.
21
+ 3. **Clear and seed.** The mod runs `/clear` and sends the fresh session one line: read the brief and follow its Instructions section. In the transcript, that line's brief path and viewer URL are drawn as links. Claude Code makes them clickable only when it detects a terminal that supports links. Over plain SSH it usually doesn't, so set `FORCE_HYPERLINK=1` if your terminal handles links, or use the status line link below.
22
+ 4. **A panel above the prompt.** It shows each step with a braille spinner on the one still running: writing the brief, clearing, starting the fresh session. Once the new session is measured it reads `✓ handed off · 162k → 45k` with an `open brief` link, then collapses after 10 seconds. Failures, the loop-guard pause, a facts-only brief and a too-tight threshold stay up until you press Dismiss. Typing `/clear` yourself closes the panel, including one waiting for Dismiss, unless a handoff is running. The panel steps aside while a survey holds that band. The band is drawn on the terminal and desktop only, so on the mobile app or in VS Code the threshold, the result, and anything that stays up also arrive as a toast.
23
+ 5. **Viewer.** Each brief also gets a readable page in `~/.claude/state/auto-handoff/pages/`. The page shows the brief and every handoff in the same run, linked in order. The served link is short, like `http://100.x.y.z:3846/1a2b3c4d`, so it fits on one line on a phone. In this copy the server is off by default and the link is the local file; set `viewer` in `/config` (for example `tailscale:3846`) to serve the pages. With `tailscale:3846` the mod serves them on your Tailscale IP at port 3846, so you can open them from any device on your tailnet. Devices off your tailnet can't reach them. The server starts with the first session that loads the mod and runs while that session is open; if it stops, including when the mod reloads, the next session to finish a turn starts it again. A session that finds the port already taken logs one line and leaves the running server alone, since it serves the same pages. Without Tailscale, the mod serves on `127.0.0.1` instead, so the link opens only on this machine. If Tailscale comes up later, a session already serving on localhost keeps using it; the next new session can serve on the Tailscale IP.
24
+ 6. **Status line link (optional).** `statusline/handoff-link.sh` wraps your status line command and adds a `↪ <link>` line when the session came from a handoff. Set it as the `statusLine` command in `~/.claude/settings.json`, with your existing command after it:
25
+
26
+ ```json
27
+ "statusLine": { "type": "command", "command": "~/claude-auto-handoff/statusline/handoff-link.sh ~/.claude/my-statusline.sh" }
28
+ ```
29
+
30
+ It needs `jq`. It finds the link in the previous brief's header, which names this session in `to:` and the page in `viewer:`.
31
+
32
+ Loop guards stop a fresh session that starts large from handing off again right away. They also cap how many handoffs run in a row before you type something.
33
+
34
+ ## Install
35
+
36
+ Requires a Claude Code build with mods (function-hook plugins). From the `sayan-plugins` marketplace (`agent-os init` installs it too; `--no-auto-handoff` skips it):
37
+
38
+ ```
39
+ /plugin marketplace add sayansr26/agent-os
40
+ /plugin install auto-handoff@sayan-plugins
41
+ ```
42
+
43
+ Or from upstream, as a plugin folder:
44
+
45
+ ```sh
46
+ git clone https://github.com/alexknowshtml/claude-auto-handoff.git ~/claude-auto-handoff
47
+ claude --plugin-dir ~/claude-auto-handoff
48
+ ```
49
+
50
+ To load it in every session, set `CLAUDE_CODE_PLUGIN_DIRS` to the folder in your shell environment, or in the `env` block of `~/.claude/settings.json`:
51
+
52
+ ```json
53
+ { "env": { "CLAUDE_CODE_PLUGIN_DIRS": "~/claude-auto-handoff" } }
54
+ ```
55
+
56
+ ## Configure
57
+
58
+ Every setting is a row in `/config` under auto-handoff. They're stored in `~/.claude/settings.json` under `pluginConfigs`.
59
+
60
+ | Setting | Default | What it does |
61
+ |---|---|---|
62
+ | `threshold` | `160000` | Context tokens that trigger a handoff. Sized for a 200k window: it leaves room for the brief and the turn in flight. A seeded session hands off no sooner than 40k past its own starting size, whatever this says; set it lower than that and the panel tells you where the line actually is |
63
+ | `maxConsecutiveHandoffs` | `2` | Handoffs allowed before you type a prompt; past this, the mod pauses until you do |
64
+ | `briefTemplate` | `~/.claude/auto-handoff/brief.md` | Your copy of the sections Haiku writes |
65
+ | `instructionsTemplate` | `~/.claude/auto-handoff/instructions.md` | Your copy of what the fresh session is told to do |
66
+ | `ignoreFiles` | blank | Regex for edited files to leave out of the brief, such as caches or synced state |
67
+ | `viewer` | blank | Where to serve the brief pages, as `host:port`. `tailscale` as the host means this machine's Tailscale IP, or `127.0.0.1` when Tailscale isn't set up. Leave blank for no server; the link is then the local file |
68
+
69
+ Environment variables:
70
+
71
+ - `AUTO_HANDOFF_TOKENS=60000` overrides the threshold for one run, so you can watch a handoff without filling 160k first. It stays set in that shell after the test. Seeded sessions start near 45k, so a value under about 85k leaves them less than 40k of room: the mod then hands off at start + 40k instead and the panel shows `threshold 60k (AUTO_HANDOFF_TOKENS) leaves 15k ...` so you know the override is still live.
72
+ - `AUTO_HANDOFF_DISABLE=1` turns the mod off for one session, viewer server included.
73
+ - `DISABLE_AUTO_COMPACT` also turns it off. When something else manages the context limit, such as a wrapper that pipes the session, `/clear` would break that pipe. The viewer server still runs there.
74
+
75
+ ## Change the brief's structure and rules
76
+
77
+ The brief is shaped by two markdown files. The defaults live in this repo's [`templates/`](templates/) folder:
78
+
79
+ - **[`templates/brief.md`](templates/brief.md)** is the prompt Haiku gets after the transcript. Each `## ` heading is a section of the brief.
80
+ - **[`templates/instructions.md`](templates/instructions.md)** goes at the top of the brief and tells the fresh session what to do with it.
81
+
82
+ On a session's first start, the mod copies both files to `~/.claude/auto-handoff/` if they aren't there yet. Edit those copies, not the ones in the repo, so a `git pull` never overwrites your changes. The next handoff uses your version.
83
+
84
+ To get the current default back, delete your copy. The next start copies it fresh. To keep your files somewhere else, point `briefTemplate` or `instructionsTemplate` in `/config` at them.
85
+
86
+ ### Editing `brief.md`
87
+
88
+ Add, remove, rename or reorder `## ` sections. The text under each heading tells Haiku what to put there. A Haiku reply counts as valid if it contains at least one of your headings. Otherwise the mod falls back to a facts-only brief.
89
+
90
+ Leave out files and commits sections. The mod adds them from the transcript in code.
91
+
92
+ ### Editing `instructions.md`
93
+
94
+ It has one switch:
95
+
96
+ ```md
97
+ {{#priority}}Shown when the last request is not fully answered.{{/priority}}
98
+ {{^priority}}Shown when it is.{{/priority}}
99
+ ```
100
+
101
+ The switch reads the brief's `## Last Request from the User` section and its `Status:` line. Keep both in `brief.md` if you want it to work.
102
+
103
+ ## Logs
104
+
105
+ Everything the mod does is logged to `~/.claude/state/auto-handoff/auto-handoff.log`.
106
+
107
+ ## Develop
108
+
109
+ ```sh
110
+ claude plugin validate .
111
+ claude plugin test .
112
+ ```
113
+
114
+ The mod hot-reloads when you save while it's loaded with `--plugin-dir`.
115
+
116
+ ## Changelog
117
+
118
+ - **0.8.6** On a machine without `sh` (Windows), the brief page is still written, and the link opens it as a local file instead of a server that never started. The viewer no longer shells out to `mkdir`.
119
+ - **0.8.5** Windows support, from [@davidboomcycle](https://github.com/davidboomcycle) (#3). The mod falls back to `USERPROFILE` when `HOME` is unset, so briefs no longer land in `<project>/undefined/`. Where there is no `sh`, the log is written through `$.fs`. The tests pass on Windows. The viewer server still needs a POSIX shell.
120
+ - **0.8.4** Any token figure in Haiku's brief that isn't in Handoff Numbers is marked `[unverified: not in Handoff Numbers]` and logged. The figure is marked, not removed.
121
+ - **0.8.3** The brief gets the real numbers: tokens at handoff, the threshold and where it came from, the session's starting size, and how many handoffs ran with no message from you. Haiku must copy them or write "unknown", so a brief can no longer invent a figure like "burned its 200k budget".
122
+ - **0.8.2** A refused tool call always ends in a handoff, even when the real size measures under the threshold. Your own `/clear` closes the panel. A second session that finds the viewer port taken exits quietly instead of logging a stack trace.
123
+ - **0.8.1** On the mobile app and in VS Code, which don't draw the panel, the threshold, the result and anything that stays up also arrive as toasts.
124
+ - **0.8.0** A panel above the prompt replaces the toasts, with a spinner on each step and an `open brief` link.
125
+ - **0.7.0** A seeded session hands off no sooner than 40k past its starting size. When the threshold is set tighter than that, the panel says so and names the setting. The viewer serves on `127.0.0.1` when Tailscale isn't available.
126
+ - **0.6.0** A viewer page for each brief, served on your Tailscale IP with short links. The pages in a chain link to each other. The seed row's links are clickable, and the status line script adds a handoff link.
127
+ - **0.5.0** First release: a Haiku brief, `/clear` and a seed prompt. It includes the tool gate, the check before each request, auto-compact replaced by a handoff, and the loop guards.
128
+
129
+ ## License
130
+
131
+ MIT
@@ -0,0 +1,22 @@
1
+ # Upstream
2
+
3
+ This plugin is a vendored copy of [alexknowshtml/claude-auto-handoff](https://github.com/alexknowshtml/claude-auto-handoff)
4
+ by Alex Hillman, MIT licensed (see [LICENSE](LICENSE)); the copyright and author stay his.
5
+
6
+ - Source commit: `8d762ce726c962268e2dbe24c1c60f0889a2fcde` (upstream version 0.8.6), taken 2026-10-22.
7
+ - Left out: `docs/demo.gif` and `docs/demo.mp4` (1.5 MB); the README links to them upstream.
8
+
9
+ ## Local changes
10
+
11
+ - `viewer` defaults to blank (no server) in `.claude-plugin/plugin.json` and `hooks/config.ts`.
12
+ Upstream defaults to `tailscale:3846`, which starts a detached HTTP server that outlives the
13
+ session. `agent-os init` installs this plugin into projects by default, so it must not start a
14
+ network listener nobody asked for. Set `viewer` in `/config` to turn it on.
15
+ - Tests that exercise the server name `viewer: 'tailscale:3846'` explicitly.
16
+ - README: provenance note, install from `sayan-plugins`, viewer default.
17
+
18
+ ## Syncing from upstream
19
+
20
+ Fetch the new commit into a scratch folder, diff it against this folder, re-apply the local
21
+ changes above, update the commit here, and bump `version` in `.claude-plugin/plugin.json` so
22
+ installs update. Run `claude plugin validate` and `claude plugin test` on this folder after.
@@ -0,0 +1,190 @@
1
+ import type { SessionMessage } from 'claude-code'
2
+ import { renderTemplate, sectionHeadings } from './templates.ts'
3
+
4
+ // Brief building.
5
+ // Files, commits, issues and the last real request come from the transcript in code;
6
+ // Haiku only writes the judgment sections. Its reply is checked, and the facts alone
7
+ // stand in when it fails.
8
+
9
+ const MAX_MESSAGES = 120
10
+ const MAX_MSG_CHARS = 2_000
11
+ const MAX_TRANSCRIPT_CHARS = 150_000
12
+
13
+ // Harness signals that arrive as user messages but are never the user's words.
14
+ const META_PREFIXES = ['Stop hook feedback', '[Automatic handoff]', '[auto-handoff]', '[Image', '<system-reminder>', '<command-name>', '<local-command']
15
+
16
+ const EDIT_TOOLS = new Set(['Edit', 'Write', 'NotebookEdit'])
17
+ const GIT_COMMIT_OUTPUT = /^\[[\w./-]+(?: \(root-commit\))? ([0-9a-f]{7,})\] (.+)$/m
18
+
19
+ export type Facts = {
20
+ filesModified: string[]
21
+ commits: string[]
22
+ issues: string[]
23
+ lastUserMessage?: string
24
+ handoffTokens?: number
25
+ threshold?: number
26
+ /** Where the base threshold came from: the env override or the /config setting. */
27
+ thresholdSource?: string
28
+ seededSessionStartSize?: number
29
+ /** Handoffs since the user last typed, this one included. */
30
+ unattendedCount?: number
31
+ /** Which handoff this is in its chain (1 for the first, 2 for the second, etc.). */
32
+ depth?: number
33
+ }
34
+
35
+ /** The user's own words, or undefined for a harness signal or a tool-result-only message. */
36
+ export function userText(m: SessionMessage): string | undefined {
37
+ if (m.role !== 'user') return undefined
38
+ const raw = m.text.trim()
39
+ if (!raw) return undefined
40
+ return isMeta(raw) ? undefined : raw
41
+ }
42
+
43
+ function isMeta(text: string): boolean {
44
+ return META_PREFIXES.some(p => text.startsWith(p))
45
+ }
46
+
47
+ function commitSubject(command: string): string | undefined {
48
+ if (!/\bgit\b[^\n|;&]*\bcommit\b/.test(command)) return undefined
49
+ const heredoc = command.match(/<<\s*'?(\w+)'?\n([\s\S]*?)\n\1/)
50
+ if (heredoc) return heredoc[2]?.trim().split('\n')[0]
51
+ return command.match(/-m\s+(["'])(.+?)\1/)?.[2]
52
+ }
53
+
54
+ /** ignoreFiles: paths that never count as edits (auto-synced state, caches); the ignoreFiles setting. */
55
+ export function extractFacts(messages: readonly SessionMessage[], ignoreFiles?: RegExp): Facts {
56
+ const files = new Set<string>()
57
+ const commits: string[] = []
58
+ const issues = new Set<string>()
59
+ let lastUserMessage: string | undefined
60
+ for (const m of messages) {
61
+ const said = userText(m)
62
+ if (said) lastUserMessage = said
63
+ for (const n of `${said ?? (m.role === 'assistant' ? m.text : '')}`.matchAll(/(?<![\w&/])#(\d{2,5})\b/g)) issues.add(`#${n[1]}`)
64
+ for (const t of m.toolUses) {
65
+ const path = t.input.file_path ?? t.input.notebook_path
66
+ if (EDIT_TOOLS.has(t.tool) && typeof path === 'string' && !t.isError && !ignoreFiles?.test(path)) files.add(path)
67
+ if (t.tool !== 'Bash' || typeof t.input.command !== 'string' || t.isError) continue
68
+ // git's own "[main abc1234] subject" line is the proof a commit landed; the command is the fallback
69
+ const out = t.text?.match(GIT_COMMIT_OUTPUT)
70
+ const subject = out ? `${out[2]} (${out[1]})` : t.text === undefined ? commitSubject(t.input.command) : undefined
71
+ if (subject && !commits.includes(subject)) commits.push(subject)
72
+ }
73
+ }
74
+ return { filesModified: [...files].slice(-20), commits: commits.slice(-10), issues: [...issues].slice(-15), lastUserMessage }
75
+ }
76
+
77
+ /** The transcript as Haiku reads it. Harness signals are labelled so they are not taken for the user. */
78
+ export function renderTranscript(messages: readonly SessionMessage[]): string {
79
+ const lines = messages.slice(-MAX_MESSAGES).map(m => {
80
+ const role = m.role === 'user' && m.text.trim() && !userText(m) ? 'system signal (not the user)' : m.role
81
+ const text = m.text.length > MAX_MSG_CHARS ? m.text.slice(0, MAX_MSG_CHARS) + ' [truncated]' : m.text
82
+ const tools = m.toolUses.map(t => ` [tool ${t.tool}${t.isError ? ' ERROR' : ''}] ${JSON.stringify(t.input).slice(0, 300)}`)
83
+ return [`### ${role}`, text, ...tools].filter(Boolean).join('\n')
84
+ })
85
+ const joined = lines.join('\n\n')
86
+ return joined.length > MAX_TRANSCRIPT_CHARS ? joined.slice(-MAX_TRANSCRIPT_CHARS) : joined
87
+ }
88
+
89
+ function list(items: string[], empty: string): string {
90
+ return items.length ? items.map(i => `- ${i}`).join('\n') : empty
91
+ }
92
+
93
+ // The only token figures the brief may use. Haiku once wrote "burned its 200k budget" for a
94
+ // session at 93k because the prompt held no numbers at all.
95
+ function handoffNumbersBlock(f: Facts): string {
96
+ const lines = []
97
+ if (f.depth !== undefined) lines.push(`- **Handoff depth:** ${f.depth}`)
98
+ if (f.handoffTokens !== undefined) lines.push(`- **Tokens at handoff:** ${f.handoffTokens} (${k(f.handoffTokens)})`)
99
+ if (f.threshold !== undefined) lines.push(`- **Threshold:** ${f.threshold} (${k(f.threshold)})${f.thresholdSource ? `, from ${f.thresholdSource}` : ''}`)
100
+ if (f.seededSessionStartSize !== undefined) lines.push(`- **This session's starting size (seeded from a handoff):** ${f.seededSessionStartSize} (${k(f.seededSessionStartSize)})`)
101
+ if (f.unattendedCount !== undefined) lines.push(`- **Handoffs in a row with no user message:** ${f.unattendedCount}`)
102
+ return lines.length ? `## Handoff Numbers\n${lines.join('\n')}\n` : ''
103
+ }
104
+
105
+ const k = (n: number) => `${Math.round(n / 1000)}k`
106
+
107
+ export function factsBlock(f: Facts, withLastMessage = true): string {
108
+ const numbers = handoffNumbersBlock(f)
109
+ const files = `## Files Modified (from Edit/Write calls)
110
+ ${list(f.filesModified, 'None.')}
111
+
112
+ ## Commits This Session
113
+ ${list(f.commits, 'None.')}
114
+
115
+ ## GitHub Issues Mentioned
116
+ ${f.issues.length ? f.issues.join(', ') : 'None.'}`
117
+ const all = [numbers, files].filter(Boolean).join('\n')
118
+ return withLastMessage ? `${all}\n\n## Last Real User Message (verbatim)\n${f.lastUserMessage ?? 'None found.'}` : all
119
+ }
120
+
121
+ export function briefPrompt(messages: readonly SessionMessage[], facts: Facts, template: string): string {
122
+ // Data first, instructions last.
123
+ return `## Extracted Facts\n${factsBlock(facts)}\n\n## Conversation\n${renderTranscript(messages)}\n\n---\n\n${template.trim()}`
124
+ }
125
+
126
+ /** A reply that holds none of the template's sections is a dialogue fragment, not a brief. */
127
+ export function isValidBrief(text: string, template: string): boolean {
128
+ return sectionHeadings(template).some(h => text.includes(h))
129
+ }
130
+
131
+ // Whether the brief's last-request section
132
+ // says the request is not (fully) answered. A chain with no user message is not a real request.
133
+ export function hasUnansweredLastRequest(text: string): boolean {
134
+ const heading = /##\s*Last Request from the User/i.exec(text)
135
+ if (!heading) return false
136
+ const rest = text.slice(heading.index + heading[0].length)
137
+ const next = /^#{1,6}\s/m.exec(rest)
138
+ const section = next ? rest.slice(0, next.index) : rest
139
+ if (/No user message found|^\s*\[auto-handoff\]/i.test(section)) return false
140
+ return /\bStatus[\s*_`]*:[\s*_`"]*(Not (?:yet |fully )?answered|Partially answered|Unanswered)/i.test(section)
141
+ }
142
+
143
+ export type BriefContext = {
144
+ sessionId: string
145
+ /** The session's transcript file. */
146
+ transcript: string
147
+ /** The instructions template, rendered at the top of the brief. */
148
+ instructions: string
149
+ }
150
+
151
+ export function assembleBrief(ctx: BriefContext, facts: Facts, haiku: string | undefined): string {
152
+ const header = `${renderTemplate(ctx.instructions, { priority: haiku ? hasUnansweredLastRequest(haiku) : false })}
153
+
154
+ ## Session Handoff Brief
155
+
156
+ - **Previous Session:** ${ctx.sessionId}
157
+ - **Transcript:** \`${ctx.transcript}\`
158
+
159
+ ## How to Use This Brief
160
+ ${haiku ? 'Haiku wrote the judgment sections from conversation text with tool output abbreviated. The facts sections came from tool calls in code.' : 'Haiku did not return a usable brief, so this holds only facts extracted in code. Read the transcript for the rest.'} Treat every line as a starting point, not a fact. Before acting on anything here, spawn a subagent to verify: run git status, gh pr view, or Read the file directly. If a fact is missing, grep the transcript before asking the user.`
161
+ // A valid Haiku brief already quotes the last request in its own section.
162
+ return [header, haiku?.trim(), factsBlock(facts, !haiku)].filter(Boolean).join('\n\n')
163
+ }
164
+
165
+ // A token figure: "93k", "93.1k", "93,105 tokens", "93105 tokens".
166
+ const TOKEN_FIGURE = /\b(\d{1,4}(?:\.\d+)?)k\b|\b(\d{1,3}(?:,\d{3})+|\d{4,7})(?= tokens\b)/gi
167
+
168
+ const figureValue = (k?: string, whole?: string) => k !== undefined ? Number(k) * 1000 : Number(whole!.replace(/,/g, ''))
169
+
170
+ /**
171
+ * Marks every token figure in Haiku's text that the Handoff Numbers block does not hold, within
172
+ * rounding to the nearest thousand. The template asks Haiku to copy those numbers; this makes it a
173
+ * rule. Figures are marked, not removed, so the next session sees what was claimed and that it is
174
+ * unchecked. With no numbers block, every figure is marked.
175
+ */
176
+ export function markUnverifiedFigures(text: string, f: Facts): { text: string; flagged: string[] } {
177
+ const block = handoffNumbersBlock(f)
178
+ const allowed = [
179
+ ...[...block.matchAll(/\b(\d{1,4}(?:\.\d+)?)k\b/g)].map(m => figureValue(m[1])),
180
+ ...[...block.matchAll(/\b\d{4,7}\b/g)].map(m => Number(m[0])),
181
+ ]
182
+ const flagged: string[] = []
183
+ const marked = text.replace(TOKEN_FIGURE, (match: string, k?: string, whole?: string) => {
184
+ const value = figureValue(k, whole)
185
+ if (allowed.some(a => Math.abs(a - value) < 1000)) return match
186
+ flagged.push(match)
187
+ return `${match} [unverified: not in Handoff Numbers]`
188
+ })
189
+ return { text: marked, flagged }
190
+ }
@@ -0,0 +1,64 @@
1
+ // Settings from /config and the constants built on them. Engine-free: the host follows $ only
2
+ // into functions declared in register.tsx, so anything taking $ stays there.
3
+
4
+ export const BRIEF_DIR = '.claude/state/auto-handoff'
5
+ // The two numbers are userConfig fields (plugin.json), set in /config. Defaults match the manifest.
6
+ // threshold: matches HANDOFF_ARM_TOKENS in context-warning.ts.
7
+ // MIN_HEADROOM and maxUnattended are loop guards. A seeded session must grow MIN_HEADROOM past its
8
+ // first-turn size (its floor) before it can hand off again, and at most maxUnattended handoffs may
9
+ // run before the user types a prompt. MIN_HEADROOM is fixed: seeded sessions start near 45k, so at
10
+ // the default threshold it never moves the line; it only matters when the threshold is set low.
11
+ // 40k is the empirical line where a seeded session can read its brief and still do real work. The
12
+ // 2026-10-03 live run (threshold 20k, seeded sessions start at ~31k) chained six times with no
13
+ // guard; the 2026-10-04 run (AUTO_HANDOFF_TOKENS=80000 left in a shell, floor ~45k) chained eight
14
+ // times with a guard of a quarter of the threshold, because max(80k, 45k + 20k) is still 80k, and
15
+ // 35k of room goes in reading the brief. Progress, not time: a 15-minute chain cap could block a
16
+ // real session that fills fast.
17
+ // The rest shape the brief: two template files and a pattern for files
18
+ // that never count as edits.
19
+ // viewer: where the mod serves the brief pages, "host:port"; "tailscale" as the host means this
20
+ // machine's Tailscale IP, or 127.0.0.1 without Tailscale. Blank: no server, and the link is the local file.
21
+ // sayan-plugins ships it blank (upstream: 'tailscale:3846'), so installing the mod starts no server.
22
+ export type Config = { threshold: number; maxUnattended: number; briefTemplate: string; instructionsTemplate: string; ignoreFiles?: RegExp; viewer: string }
23
+ export const DEFAULTS: Config = { threshold: 160_000, maxUnattended: 2, briefTemplate: '~/.claude/auto-handoff/brief.md', instructionsTemplate: '~/.claude/auto-handoff/instructions.md', viewer: '' }
24
+ export const MIN_HEADROOM = 40_000
25
+ // Each template's default, a file in the mod's templates/ folder.
26
+ export const TEMPLATES = [['briefTemplate', 'brief.md'], ['instructionsTemplate', 'instructions.md']] as const
27
+ export type TemplateKey = typeof TEMPLATES[number][0]
28
+ // Used only when the shipped default is unreadable too, so the fresh session still knows what to do.
29
+ export const LAST_RESORT_INSTRUCTIONS = '## Instructions\n\nThis turn was triggered by the system, not by a user. Read this brief and continue the work it describes.'
30
+
31
+ export const k = (n: number) => `${Math.round(n / 1000)}k`
32
+ export const short = (sessionId: string) => sessionId.slice(0, 8)
33
+ export const expand = (path: string, home: string) => path.replace(/^~(?=\/|$)/, home)
34
+
35
+ // A non-positive or non-numeric value falls back to the default rather than handing off at 0.
36
+ const num = (v: unknown, fallback: number) => typeof v === 'number' && Number.isFinite(v) && v > 0 ? v : fallback
37
+ const str = (v: unknown, fallback: string) => typeof v === 'string' && v.trim() ? v.trim() : fallback
38
+ // A bad pattern is dropped, not fatal: a typo in /config should not stop handoffs.
39
+ function pattern(v: unknown): RegExp | undefined {
40
+ if (typeof v !== 'string' || !v.trim()) return undefined
41
+ try { return new RegExp(v) } catch { return undefined }
42
+ }
43
+
44
+ /** The /config values, each checked, with the manifest's defaults for anything missing or bad. */
45
+ export function parseConfig(options: Record<string, unknown>): Config {
46
+ return {
47
+ threshold: num(options.threshold, DEFAULTS.threshold),
48
+ maxUnattended: num(options.maxConsecutiveHandoffs, DEFAULTS.maxUnattended),
49
+ briefTemplate: str(options.briefTemplate, DEFAULTS.briefTemplate),
50
+ instructionsTemplate: str(options.instructionsTemplate, DEFAULTS.instructionsTemplate),
51
+ ignoreFiles: pattern(options.ignoreFiles),
52
+ viewer: typeof options.viewer === 'string' ? options.viewer.trim() : DEFAULTS.viewer,
53
+ }
54
+ }
55
+
56
+ // The seed prompt's first characters; the render hook knows the seed row by them.
57
+ export const SEED_PREFIX = '[auto-handoff] ↪ Handoff from session'
58
+ // The seed text with its brief path and viewer URL as markdown links. The path becomes a
59
+ // file: link labelled by its file name; the URL links to itself. Exported for the test.
60
+ export function linkify(text: string): string {
61
+ return text
62
+ .replace(/(?<=\bat )(\/\S+\.md)(?=[\s)]|$)/, (p) => `[${p.slice(p.lastIndexOf('/') + 1)}](file://${p})`)
63
+ .replace(/(https?:\/\/[^\s)]+)/, (u) => `[${u}](${u})`)
64
+ }
@@ -0,0 +1 @@
1
+ { "modules": ["./register.tsx"] }