@threadcrumbs/common 0.1.10 → 0.1.11

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.
package/README.md CHANGED
@@ -31,7 +31,8 @@ need it.
31
31
  | `@threadcrumbs/common/ui/totals` | The status lamps and the footer: `statusCounts`, `lampsHTML(counts, total, pressed)`, `footerTotals(threads)`, `footerHTML(totals, { short, bars })`, and the output-over-time log, `emptyOutputLog`, `recordOutput`, and `outputBars`. `ui/totals.css` styles them and builds on `ui/rows.css` |
32
32
  | `@threadcrumbs/common/ui/pictures` | The footer's pictures, `FOOT_DRAW` (`drawPie`, `drawWaffle`, `drawCols`) at `FOOT_SIZE`, with `footerNote`; plan ticks, `planTicks` and `planSpec`; `waffleStates` and `binTimes`; and the SVG helpers Threadgauge's other pictures use (`fx`, `clamp`, `polar`, `wideBox`, `wideSVG`, and an `esc` for SVG text). `ui/pictures.css` colors them |
33
33
 
34
- `launch/` is the only part of this package that starts processes, and `license/store.mjs` is the
34
+ `launch/` is the only part of this package that starts processes, apart from `lib/proc-start.mjs`
35
+ asking `ps` or `Get-Process` when a pid started, and `license/store.mjs` is the
35
36
  only part that writes a file. The readers and `orgs/` import neither. `ui/` is plain functions
36
37
  that return markup, plus CSS and fonts, for a page to draw; it touches no DOM and does no I/O.
37
38
 
@@ -132,12 +133,14 @@ pnpm test
132
133
  `test/harness.test.mjs` fakes an install of each harness in a temp folder for the session readers,
133
134
  and each `test/<harness>-telemetry.test.mjs` does the same for that harness's telemetry reader.
134
135
  `test/claude-transcript.test.mjs` covers the shared Claude parser on its own.
136
+ `test/proc-start.test.mjs` covers telling a live Claude Code session from a recycled pid, against
137
+ fake lookups: a match, a recycled pid, a failed or slow lookup, the cache, and the batching.
135
138
  `test/shared-ids.test.mjs` has both readers of each harness scan one fake install and checks they
136
139
  give its sessions the same ids, which Threadgauge's Open depends on.
137
140
  `test/orgs.test.mjs` covers each package manager's name, the git origin, and the combining rule.
138
141
  `test/read-only.test.mjs` reads the package's own source and fails if anything under `harnesses/`,
139
- `telemetry/`, `lib/`, or `orgs/` writes a file, opens a database for writing, starts or signals a process,
140
- or imports `launch/` or the license store. `test/launch.test.mjs` covers how `present()` refuses and names a missing app,
142
+ `telemetry/`, `lib/`, or `orgs/` writes a file, opens a database for writing, starts or signals a process
143
+ (apart from `lib/proc-start.mjs`'s start-time query), or imports `launch/` or the license store. `test/launch.test.mjs` covers how `present()` refuses and names a missing app,
141
144
  starting nothing but a registry query on Windows. `test/license.test.mjs` covers signing and the unlock rule, and
142
145
  `test/license-store.test.mjs` covers the shared file, including two apps starting at once on a
143
146
  fresh machine and a clock set back, and `portalUrl()` against a fake key service.
@@ -1,213 +1,214 @@
1
- # Harness adapters
2
-
3
- A **harness** is whatever runs the agent threads — Claude Code, Codex CLI, OpenCode, and so on.
4
- The Threadcrumbs apps do not care which one you use: each asks every harness present on the
5
- machine for its threads, through these adapters, and shows whatever comes back. Threadnauts
6
- draws them as bots in its colony, which is where most of the wording below comes from.
7
-
8
- Adding one is meant to be **one new file in this directory**, plus one line in `index.mjs`.
9
- Nothing in an app's scanner, its API, or its page should need to change.
10
- If you find yourself editing those to land a harness, that is a bug in this seam — please say so
11
- in the PR, because the next person will hit it too.
12
-
13
- ## The shape of it
14
-
15
- ```js
16
- // harnesses/my-harness.mjs
17
- export default {
18
- id: 'my-harness', // stable, kebab-case, used as a key — never change it later
19
- name: 'My Harness', // what a human sees in the UI
20
- detect, // () => Promise<boolean>
21
- scanThreads, // () => Promise<Thread[]>
22
- app, // { name, download }: the app that answers the deep link
23
- openThread, // (ref) => { ok, url, command?, app } | { ok: false, error }
24
- newSession, // (dir) => { ok, url, command?, app } | { ok: false, error }
25
- }
26
- ```
27
-
28
- Then, in `index.mjs`:
29
-
30
- ```js
31
- import myHarness from './my-harness.mjs'
32
- export const HARNESSES = [claudeCode, myHarness]
33
- ```
34
-
35
- ### `detect()`
36
-
37
- Is this harness on this machine at all? Usually just "does its data directory exist". Cheap —
38
- it runs on every scan, so that installing a harness while the colony is open is noticed on the
39
- next poll. Returning `false` means the harness is skipped entirely, and no bot for it
40
- ever appears.
41
-
42
- ### `scanThreads()`
43
-
44
- The real work: return one `Thread` per session the harness knows about.
45
-
46
- Throwing is survivable — the scanner logs it and carries on with the other harnesses, so one
47
- broken adapter costs you its own threads and nothing else. Prefer that over returning junk.
48
-
49
- ### `openThread(ref)` / `newSession(dir)`
50
-
51
- Return `{ ok: true, url }` and the server hands that URL to the OS opener. `openThread` gets
52
- the `ref` from the thread it belongs to; `newSession` gets an absolute directory that the
53
- server has already checked still exists.
54
-
55
- Add `command: { argv, cwd }` — the harness's own CLI resuming the same thread, with an absolute `argv[0]` —
56
- when the CLI is installed, and the server runs it in a terminal for a machine with no desktop app or a person who asked for one.
57
- Never spawn it yourself.
58
-
59
- Add `app: { name, download }` too, the same object as the adapter's own `app`: the app that answers
60
- the deep link, as a person would call it, and the page to download it from. When nothing on the
61
- machine answers the link, `present()` names that app, so the UI can say what to install instead
62
- of a click that does nothing. `app.also` can list other apps that would do, such as the Claude Code
63
- extension for VS Code.
64
-
65
- When another app can open the thread through links of its own, add `fallback: { app, urls, note }`.
66
- `present()` tries it when nothing answers `url`: it opens each URL in turn, two seconds apart,
67
- and returns `note`. Claude Code uses it to open the thread's folder in VS Code and then the
68
- extension's session link.
69
-
70
- If your harness has no deep link, return `{ ok: false, error: '…' }` and say why — the UI
71
- shows the message rather than pretending the click worked.
72
-
73
- ### There is no `setArchived`, and that is deliberate
74
-
75
- No adapter writes to a harness. Not the transcripts, not the session records, not one
76
- flag. Threadnauts records archiving in its own `colony.json` and nowhere else: the thread leaves
77
- the map and the bot walks back to the ship.
78
-
79
- It used to write one flag — `isArchived` on Claude Code's own session record — and that write
80
- genuinely landed on disk. It just did not *mean* anything: the desktop app serves from the copy it
81
- loaded at launch, so the thread stayed in its list until the app restarted, and the app rewrote the
82
- record from memory the next time it touched the thread. Holding that together took a re-assert on
83
- every scan, a `ps` sweep to guess whether the app had re-read the file, and a *pending* state for
84
- the gap between them. All of that is gone, and the scan no longer starts a subprocess at all.
85
-
86
- Archiving in the harness's own UI still works and is still the right way to do it — your adapter
87
- reports it through the `archived` field and the bot goes home on the next poll.
88
-
89
- ## The `Thread` your adapter returns
90
-
91
- Only `id` is truly required, but the colony gets duller the more you leave out — `project` is
92
- what earns a repo its own zone, and `lastActivityAt` is what sorts the whole map.
93
-
94
- | Field | Type | What it means |
95
- | --- | --- | --- |
96
- | `id` | string | **Unique across every harness.** A UUID is fine; otherwise prefix it, e.g. `my-harness:1234` |
97
- | `title` | string | Thread title. `'Untitled thread'` if the harness has none |
98
- | `preview` | string | First prompt, trimmed — shown on the thread card |
99
- | `project` | string | Repo/folder **name**. This is what claims a hex zone |
100
- | `projectPath` | string | Absolute path to the repo root |
101
- | `worktree` | string | Worktree name, or `''` |
102
- | `cwd` | string | Where the thread is actually working |
103
- | `gitBranch` | string | Branch name, or `''` |
104
- | `model` / `effort` | string | Shown on the thread card |
105
- | `createdAt` | number | Epoch ms |
106
- | `lastActivityAt` | number | Epoch ms. Sorts the colony and drives the "asleep for 3 days" behaviour |
107
- | `lastFocusedAt` | number | Epoch ms, `0` if unknowable |
108
- | `running` | boolean | Working **right now** — the bot hammers away |
109
- | `unread` | boolean | Done: the turn ended and moved on since you last looked — the bot stops and raises a hand |
110
- | `needsInput` | boolean | Needs you: stopped mid-turn on a permission prompt, a question, or an approval only you can answer — the bot holds a `?` and waves until you do. Outranks every other state |
111
- | `awaitingSubagents` | boolean | Waiting on bots: its own turn is over, or its only open calls are subagents, and subagents it sent out are still working, or another thread it asked to hear from is still busy — the bot stands at its site under a `…` until they report back. Outranks `running` and `unread` |
112
- | `hasError` | boolean | Errored — the bot slumps, red eyes |
113
- | `starred` / `routine` / `prState` | | Optional extras; `prState: 'merged'` triggers the confetti |
114
- | `archived` | boolean | Archived in the harness's own records. Read-only — reporting it is all an adapter does |
115
- | `sizeBytes` | number | Transcript size. **This is how finished a building looks**, on a log scale |
116
- | `source` | string | Free-form, for your own bookkeeping (the Claude adapter uses `desktop` / `cli`) |
117
- | `canOpen` | boolean | Whether this thread can be opened. The UI greys the button out |
118
- | `subagents` | array | Optional. Errands this thread has out *right now*: `{ id, task, lastActivityAt }`. Drawn as small companions at the parent's building — no zone, no badge, not counted. Omit it and nothing changes |
119
- | `awaiting` | array | Optional. Other threads it asked to hear from that are still busy: `{ id, title }`, where `id` is that thread's own id. They are bots of their own, so this names them rather than adding companions. Omit it and nothing changes |
120
- | `ref` | object | **Opaque.** Whatever *you* need to find this thread again |
121
-
122
- ### About `ref`
123
-
124
- `ref` is the whole reason the browser does not know what a session id looks like. Your adapter
125
- puts whatever it needs in there, the page hands it straight back on open and archive, and
126
- nothing between the two ever inspects it.
127
-
128
- Keep it small and keep it serialisable — it makes a round trip through JSON on every action.
129
- Do not put a file handle, a class instance, or a secret in it.
130
-
131
- ## Ground rules
132
-
133
- - **Read-only. No exceptions.** An adapter never writes a file and never starts a process;
134
- `test/read-only.test.mjs` fails if one tries. A harness's transcripts and records are
135
- somebody's actual work, and the apps are viewers, not editors. If an adapter seems to need a
136
- write, it does not — say so in an issue. Anything that has to write to a harness belongs to the
137
- app that asks for it, outside this package.
138
- - **Never run anything out of another application's bundle.** Not to read from it, not to
139
- execute it. Only files under the user's own home directory. Opening a thread goes through a
140
- URL the OS resolves, or a command the user already has on `PATH`.
141
- - **Never block the scan.** It runs on a poll. Cache anything expensive against file size and
142
- mtime. Claude Code's transcripts go through `transcriptReader` in
143
- `../lib/claude-transcript.mjs`, which reads a new transcript's head and tail once and after
144
- that only the lines appended since the last scan, so a 12MB transcript is never reparsed.
145
- - **Read heads, not whole files.** `readHead` in `../lib/fsutil.mjs` pulls the first chunk and
146
- drops a trailing partial line, so `JSON.parse` never sees half a record.
147
- - **Expect malformed data.** A session being written *right now* is a normal thing to trip
148
- over. Skip that record and move on; do not throw the pass away.
149
- - **Never widen `id` collisions.** The colony keys its archive list and saved layout on `id`.
150
- Two harnesses handing back the same id would merge two unrelated threads into one bot.
151
-
152
- ## Starting points
153
-
154
- Verified on a real machine:
155
-
156
- - **Claude Code** — desktop records in
157
- `~/Library/Application Support/Claude/claude-code-sessions/<account>/<org>/local_*.json`
158
- (`%APPDATA%\Claude\claude-code-sessions\…` on older Windows installs, and
159
- `%LOCALAPPDATA%\Claude-Data\<install>\claude-code-sessions\…` on current ones, which leave a
160
- `.user-data-left.<install>` marker in the old folder). The app folder is not always called
161
- `Claude`: it is `Claude-3p` on some macOS installs, and each extra desktop profile gets its own
162
- `Claude-<profile>` beside the first, so every `Claude*` folder holding a store is read. In the
163
- same folder sits a
164
- `deleted_<cliSessionId>` marker for every thread deleted in the app, holding the deletion time
165
- in epoch ms — the record goes, the CLI transcript stays, and the marker is all that tells a
166
- deleted thread from one started in a terminal, so the adapter reports it as `archived`; CLI
167
- transcripts in `~/.claude/projects/<encoded-cwd>/<sessionId>.jsonl`; live processes in
168
- `~/.claude/sessions/*.json`, whose `status` is `busy`, `waiting` (a prompt is open, named in
169
- `waitingFor`) or `idle`, and whose `messagingSocketPath` is the address other sessions message
170
- it at. A session that sends `SendMessage` with `notify_when_idle: true` is waiting on the
171
- session at that address until a user record reading `[Cross-session idle notice] "<address>"`
172
- arrives. `CLAUDE_CONFIG_DIR` (the CLI's own override for `~/.claude`) and
173
- `THREADCRUMBS_CLAUDE_DESKTOP_DIR` (the session store) point both roots elsewhere, which is how
174
- `test/harness.test.mjs` fakes an install. Implemented in `claude-code.mjs`.
175
- - **Codex** — the desktop app, the VS Code extension and the CLI share one store. One row per
176
- thread (title, cwd, branch, model, effort, archived) in `~/.codex/state_<n>.sqlite`, and
177
- transcripts in `~/.codex/sessions/YYYY/MM/DD/rollout-<iso>-<uuid>.jsonl`, with records shaped
178
- `{ timestamp, type, payload }`. They join on the session id, and the transcripts are the only
179
- source for a session the database has not indexed yet. Renames are also in
180
- `~/.codex/session_index.jsonl`. `CODEX_HOME` points it elsewhere. Implemented in `codex.mjs`.
181
-
182
- For anything else, the fastest way in is usually to start a throwaway session in that harness
183
- and watch which files change:
184
-
185
- ```bash
186
- find ~ -maxdepth 4 -newermt '-2 minutes' -type f 2>/dev/null | grep -iv Library/Caches
187
- ```
188
-
189
- ## Checking your work
190
-
191
- `pnpm test` in this repo runs `test/harness.test.mjs`, which fakes an install of each harness in a
192
- temp folder. Add yours there. Then, against a real install:
193
-
194
- 1. `node --check harnesses/my-harness.mjs`
195
- 2. With Threadnauts running, `GET /api/harnesses` lists every registered harness and whether
196
- `detect()` found it. If yours is missing or `detected: false`, stop here — nothing else
197
- will work until it shows up:
198
-
199
- ```bash
200
- curl -s localhost:5274/api/harnesses
201
- ```
202
- 3. Scan straight from node and look at the result — the number should match what the harness
203
- itself reports, and no field should be `undefined`:
204
-
205
- ```bash
206
- node -e 'import("./harnesses/my-harness.mjs").then(async m => {
207
- const t = await m.default.scanThreads()
208
- console.log(t.length, "threads"); console.dir(t[0], { depth: 4 })
209
- })'
210
- ```
211
- 4. `pnpm dev` in Threadnauts, then confirm the bots appear on the right plots, the thread card
212
- fills in, and Open does what you expect.
213
- 5. Archive one thread and check it shows as archived **in the harness's own UI**, not just here.
1
+ # Harness adapters
2
+
3
+ A **harness** is whatever runs the agent threads — Claude Code, Codex CLI, OpenCode, and so on.
4
+ The Threadcrumbs apps do not care which one you use: each asks every harness present on the
5
+ machine for its threads, through these adapters, and shows whatever comes back. Threadnauts
6
+ draws them as bots in its colony, which is where most of the wording below comes from.
7
+
8
+ Adding one is meant to be **one new file in this directory**, plus one line in `index.mjs`.
9
+ Nothing in an app's scanner, its API, or its page should need to change.
10
+ If you find yourself editing those to land a harness, that is a bug in this seam — please say so
11
+ in the PR, because the next person will hit it too.
12
+
13
+ ## The shape of it
14
+
15
+ ```js
16
+ // harnesses/my-harness.mjs
17
+ export default {
18
+ id: 'my-harness', // stable, kebab-case, used as a key — never change it later
19
+ name: 'My Harness', // what a human sees in the UI
20
+ detect, // () => Promise<boolean>
21
+ scanThreads, // () => Promise<Thread[]>
22
+ app, // { name, download }: the app that answers the deep link
23
+ openThread, // (ref) => { ok, url, command?, app } | { ok: false, error }
24
+ newSession, // (dir) => { ok, url, command?, app } | { ok: false, error }
25
+ }
26
+ ```
27
+
28
+ Then, in `index.mjs`:
29
+
30
+ ```js
31
+ import myHarness from './my-harness.mjs'
32
+ export const HARNESSES = [claudeCode, myHarness]
33
+ ```
34
+
35
+ ### `detect()`
36
+
37
+ Is this harness on this machine at all? Usually just "does its data directory exist". Cheap —
38
+ it runs on every scan, so that installing a harness while the colony is open is noticed on the
39
+ next poll. Returning `false` means the harness is skipped entirely, and no bot for it
40
+ ever appears.
41
+
42
+ ### `scanThreads()`
43
+
44
+ The real work: return one `Thread` per session the harness knows about.
45
+
46
+ Throwing is survivable — the scanner logs it and carries on with the other harnesses, so one
47
+ broken adapter costs you its own threads and nothing else. Prefer that over returning junk.
48
+
49
+ ### `openThread(ref)` / `newSession(dir)`
50
+
51
+ Return `{ ok: true, url }` and the server hands that URL to the OS opener. `openThread` gets
52
+ the `ref` from the thread it belongs to; `newSession` gets an absolute directory that the
53
+ server has already checked still exists.
54
+
55
+ Add `command: { argv, cwd }` — the harness's own CLI resuming the same thread, with an absolute `argv[0]` —
56
+ when the CLI is installed, and the server runs it in a terminal for a machine with no desktop app or a person who asked for one.
57
+ Never spawn it yourself.
58
+
59
+ Add `app: { name, download }` too, the same object as the adapter's own `app`: the app that answers
60
+ the deep link, as a person would call it, and the page to download it from. When nothing on the
61
+ machine answers the link, `present()` names that app, so the UI can say what to install instead
62
+ of a click that does nothing. `app.also` can list other apps that would do, such as the Claude Code
63
+ extension for VS Code.
64
+
65
+ When another app can open the thread through links of its own, add `fallback: { app, urls, note }`.
66
+ `present()` tries it when nothing answers `url`: it opens each URL in turn, two seconds apart,
67
+ and returns `note`. Claude Code uses it to open the thread's folder in VS Code and then the
68
+ extension's session link.
69
+
70
+ If your harness has no deep link, return `{ ok: false, error: '…' }` and say why — the UI
71
+ shows the message rather than pretending the click worked.
72
+
73
+ ### There is no `setArchived`, and that is deliberate
74
+
75
+ No adapter writes to a harness. Not the transcripts, not the session records, not one
76
+ flag. Threadnauts records archiving in its own `colony.json` and nowhere else: the thread leaves
77
+ the map and the bot walks back to the ship.
78
+
79
+ It used to write one flag — `isArchived` on Claude Code's own session record — and that write
80
+ genuinely landed on disk. It just did not *mean* anything: the desktop app serves from the copy it
81
+ loaded at launch, so the thread stayed in its list until the app restarted, and the app rewrote the
82
+ record from memory the next time it touched the thread. Holding that together took a re-assert on
83
+ every scan, a `ps` sweep to guess whether the app had re-read the file, and a *pending* state for
84
+ the gap between them. All of that is gone, and the scan no longer starts a subprocess at all.
85
+
86
+ Archiving in the harness's own UI still works and is still the right way to do it — your adapter
87
+ reports it through the `archived` field and the bot goes home on the next poll.
88
+
89
+ ## The `Thread` your adapter returns
90
+
91
+ Only `id` is truly required, but the colony gets duller the more you leave out — `project` is
92
+ what earns a repo its own zone, and `lastActivityAt` is what sorts the whole map.
93
+
94
+ | Field | Type | What it means |
95
+ | --- | --- | --- |
96
+ | `id` | string | **Unique across every harness.** A UUID is fine; otherwise prefix it, e.g. `my-harness:1234` |
97
+ | `title` | string | Thread title. `'Untitled thread'` if the harness has none |
98
+ | `preview` | string | First prompt, trimmed — shown on the thread card |
99
+ | `project` | string | Repo/folder **name**. This is what claims a hex zone |
100
+ | `projectPath` | string | Absolute path to the repo root |
101
+ | `worktree` | string | Worktree name, or `''` |
102
+ | `cwd` | string | Where the thread is actually working |
103
+ | `gitBranch` | string | Branch name, or `''` |
104
+ | `model` / `effort` | string | Shown on the thread card |
105
+ | `createdAt` | number | Epoch ms |
106
+ | `lastActivityAt` | number | Epoch ms. Sorts the colony and drives the "asleep for 3 days" behaviour |
107
+ | `lastFocusedAt` | number | Epoch ms, `0` if unknowable |
108
+ | `running` | boolean | Working **right now** — the bot hammers away |
109
+ | `unread` | boolean | Done: the turn ended and moved on since you last looked — the bot stops and raises a hand |
110
+ | `needsInput` | boolean | Needs you: stopped mid-turn on a permission prompt, a question, or an approval only you can answer — the bot holds a `?` and waves until you do. Outranks every other state |
111
+ | `awaitingSubagents` | boolean | Waiting on bots: its own turn is over, or its only open calls are subagents, and subagents it sent out are still working, or another thread it asked to hear from is still busy — the bot stands at its site under a `…` until they report back. Outranks `running` and `unread` |
112
+ | `hasError` | boolean | Errored — the bot slumps, red eyes |
113
+ | `starred` / `routine` / `prState` | | Optional extras; `prState: 'merged'` triggers the confetti |
114
+ | `archived` | boolean | Archived in the harness's own records. Read-only — reporting it is all an adapter does |
115
+ | `sizeBytes` | number | Transcript size. **This is how finished a building looks**, on a log scale |
116
+ | `source` | string | Free-form, for your own bookkeeping (the Claude adapter uses `desktop` / `cli`) |
117
+ | `canOpen` | boolean | Whether this thread can be opened. The UI greys the button out |
118
+ | `subagents` | array | Optional. Errands this thread has out *right now*: `{ id, task, lastActivityAt }`. Drawn as small companions at the parent's building — no zone, no badge, not counted. Omit it and nothing changes |
119
+ | `awaiting` | array | Optional. Other threads it asked to hear from that are still busy: `{ id, title }`, where `id` is that thread's own id. They are bots of their own, so this names them rather than adding companions. Omit it and nothing changes |
120
+ | `ref` | object | **Opaque.** Whatever *you* need to find this thread again |
121
+
122
+ ### About `ref`
123
+
124
+ `ref` is the whole reason the browser does not know what a session id looks like. Your adapter
125
+ puts whatever it needs in there, the page hands it straight back on open and archive, and
126
+ nothing between the two ever inspects it.
127
+
128
+ Keep it small and keep it serialisable — it makes a round trip through JSON on every action.
129
+ Do not put a file handle, a class instance, or a secret in it.
130
+
131
+ ## Ground rules
132
+
133
+ - **Read-only. No exceptions.** An adapter never writes a file and never starts a process;
134
+ `test/read-only.test.mjs` fails if one tries. The one process the readers share is
135
+ `lib/proc-start.mjs` asking the OS when a pid started, and DECISIONS.md says why. A harness's transcripts and records are
136
+ somebody's actual work, and the apps are viewers, not editors. If an adapter seems to need a
137
+ write, it does not — say so in an issue. Anything that has to write to a harness belongs to the
138
+ app that asks for it, outside this package.
139
+ - **Never run anything out of another application's bundle.** Not to read from it, not to
140
+ execute it. Only files under the user's own home directory. Opening a thread goes through a
141
+ URL the OS resolves, or a command the user already has on `PATH`.
142
+ - **Never block the scan.** It runs on a poll. Cache anything expensive against file size and
143
+ mtime. Claude Code's transcripts go through `transcriptReader` in
144
+ `../lib/claude-transcript.mjs`, which reads a new transcript's head and tail once and after
145
+ that only the lines appended since the last scan, so a 12MB transcript is never reparsed.
146
+ - **Read heads, not whole files.** `readHead` in `../lib/fsutil.mjs` pulls the first chunk and
147
+ drops a trailing partial line, so `JSON.parse` never sees half a record.
148
+ - **Expect malformed data.** A session being written *right now* is a normal thing to trip
149
+ over. Skip that record and move on; do not throw the pass away.
150
+ - **Never widen `id` collisions.** The colony keys its archive list and saved layout on `id`.
151
+ Two harnesses handing back the same id would merge two unrelated threads into one bot.
152
+
153
+ ## Starting points
154
+
155
+ Verified on a real machine:
156
+
157
+ - **Claude Code** — desktop records in
158
+ `~/Library/Application Support/Claude/claude-code-sessions/<account>/<org>/local_*.json`
159
+ (`%APPDATA%\Claude\claude-code-sessions\…` on older Windows installs, and
160
+ `%LOCALAPPDATA%\Claude-Data\<install>\claude-code-sessions\…` on current ones, which leave a
161
+ `.user-data-left.<install>` marker in the old folder). The app folder is not always called
162
+ `Claude`: it is `Claude-3p` on some macOS installs, and each extra desktop profile gets its own
163
+ `Claude-<profile>` beside the first, so every `Claude*` folder holding a store is read. In the
164
+ same folder sits a
165
+ `deleted_<cliSessionId>` marker for every thread deleted in the app, holding the deletion time
166
+ in epoch ms — the record goes, the CLI transcript stays, and the marker is all that tells a
167
+ deleted thread from one started in a terminal, so the adapter reports it as `archived`; CLI
168
+ transcripts in `~/.claude/projects/<encoded-cwd>/<sessionId>.jsonl`; live processes in
169
+ `~/.claude/sessions/*.json`, whose `status` is `busy`, `waiting` (a prompt is open, named in
170
+ `waitingFor`) or `idle`, and whose `messagingSocketPath` is the address other sessions message
171
+ it at. A session that sends `SendMessage` with `notify_when_idle: true` is waiting on the
172
+ session at that address until a user record reading `[Cross-session idle notice] "<address>"`
173
+ arrives. `CLAUDE_CONFIG_DIR` (the CLI's own override for `~/.claude`) and
174
+ `THREADCRUMBS_CLAUDE_DESKTOP_DIR` (the session store) point both roots elsewhere, which is how
175
+ `test/harness.test.mjs` fakes an install. Implemented in `claude-code.mjs`.
176
+ - **Codex** — the desktop app, the VS Code extension and the CLI share one store. One row per
177
+ thread (title, cwd, branch, model, effort, archived) in `~/.codex/state_<n>.sqlite`, and
178
+ transcripts in `~/.codex/sessions/YYYY/MM/DD/rollout-<iso>-<uuid>.jsonl`, with records shaped
179
+ `{ timestamp, type, payload }`. They join on the session id, and the transcripts are the only
180
+ source for a session the database has not indexed yet. Renames are also in
181
+ `~/.codex/session_index.jsonl`. `CODEX_HOME` points it elsewhere. Implemented in `codex.mjs`.
182
+
183
+ For anything else, the fastest way in is usually to start a throwaway session in that harness
184
+ and watch which files change:
185
+
186
+ ```bash
187
+ find ~ -maxdepth 4 -newermt '-2 minutes' -type f 2>/dev/null | grep -iv Library/Caches
188
+ ```
189
+
190
+ ## Checking your work
191
+
192
+ `pnpm test` in this repo runs `test/harness.test.mjs`, which fakes an install of each harness in a
193
+ temp folder. Add yours there. Then, against a real install:
194
+
195
+ 1. `node --check harnesses/my-harness.mjs`
196
+ 2. With Threadnauts running, `GET /api/harnesses` lists every registered harness and whether
197
+ `detect()` found it. If yours is missing or `detected: false`, stop here — nothing else
198
+ will work until it shows up:
199
+
200
+ ```bash
201
+ curl -s localhost:5274/api/harnesses
202
+ ```
203
+ 3. Scan straight from node and look at the result — the number should match what the harness
204
+ itself reports, and no field should be `undefined`:
205
+
206
+ ```bash
207
+ node -e 'import("./harnesses/my-harness.mjs").then(async m => {
208
+ const t = await m.default.scanThreads()
209
+ console.log(t.length, "threads"); console.dir(t[0], { depth: 4 })
210
+ })'
211
+ ```
212
+ 4. `pnpm dev` in Threadnauts, then confirm the bots appear on the right plots, the thread card
213
+ fills in, and Open does what you expect.
214
+ 5. Archive one thread and check it shows as archived **in the harness's own UI**, not just here.