Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
358 commits
Select commit Hold shift + click to select a range
1f5b821
release: 0.16.1
AltanS Jul 27, 2026
3baa585
Merge pull request #38 from AltanS/fix/config-endpoint-gate
AltanS Jul 27, 2026
b7cae36
fix(web): never send the submit key until the text is verified in the…
AltanS Jul 27, 2026
0b4f365
Merge pull request #39 from AltanS/fix/34-guard-free-text-send
AltanS Jul 27, 2026
098fa40
docs(herdr-api): record that send_text writes raw bytes, and what an …
AltanS Jul 27, 2026
8c898a0
docs: say that the device gate bounds damage, not disclosure
AltanS Jul 27, 2026
6afaf5b
Merge pull request #29 from Optic00/feat/prompt-binding-server-side
AltanS Jul 28, 2026
1a5972b
feat: reserve /auth/ so a fronting proxy can be reached from an insta…
AltanS Jul 28, 2026
aa7765e
fix(sw): match the passthrough against pathname+search, not pathname
AltanS Jul 28, 2026
3b276cd
release: 0.18.0
AltanS Jul 28, 2026
4682e05
Merge pull request #43 from AltanS/fix/31-proxy-signin-path
AltanS Jul 28, 2026
7e3b2bd
feat(journal): make pane history per-harness, with codex and pi adapters
AltanS Jul 28, 2026
c683888
release: 0.19.0
AltanS Jul 28, 2026
636b7af
fix(web): stop the space and tab chip rows painting over each other
konpyl Jul 28, 2026
59bcfe1
feat(web): light and system theme
konpyl Jul 28, 2026
2f4d691
feat(bridge): keep two timestamps per pane, so the dashboard can sort…
konpyl Jul 28, 2026
78425bd
docs: ADR 0002 — the light terminal mirror is inverted, not re-themed
konpyl Jul 28, 2026
da4f44c
feat(web): triage the dashboard by attention then recency, and stop c…
konpyl Jul 28, 2026
0c589bc
release: 0.19.0
konpyl Jul 28, 2026
6786ca1
docs: ADR 0003 — one shared "seen", and only Collie's own reads count
konpyl Jul 28, 2026
29ede4d
release: 0.20.0
konpyl Jul 28, 2026
f9000cb
fix(bridge): don't let a cross-site GET clear your unseen agents
konpyl Jul 28, 2026
f8064c1
docs: record the CSRF fix in the 0.20.0 entry
konpyl Jul 28, 2026
4cca8db
feat(web): fold the long tails in the "Switch pane" sheet, shells inc…
konpyl Jul 28, 2026
6754742
docs: note the Switch pane folds in 0.20.0
konpyl Jul 28, 2026
8a8a4c9
fix(web): let the tab survive truncation, and stop every row restatin…
konpyl Jul 28, 2026
1a1100d
fix(web): make the card shape mean something, and stop line 2 repeati…
konpyl Jul 28, 2026
5c04453
fix(web): stop the hollow status ring reading as a notch cut out of t…
konpyl Jul 28, 2026
dab7e05
fix(web): light --accent was byte-identical to --background, so "you …
konpyl Jul 28, 2026
50a068f
docs: correct the --accent comment, and record the UX sweep's fixes i…
konpyl Jul 28, 2026
22d4a5f
feat(web): say what's going on inside each tab and space, not just "b…
konpyl Jul 28, 2026
472277f
docs: note the tab/space status dots in 0.20.0
konpyl Jul 28, 2026
0c10829
test(journal): prove containment against a symlink out of the root
AltanS Jul 29, 2026
de8196b
Merge pull request #44 from AltanS/feat/journal-adapters
AltanS Jul 29, 2026
cc38351
feat(web): make URLs in the pane mirror tappable
konpyl Jul 29, 2026
b86216b
docs: correct ADR 0002's false rule, and move the herdr terminal fact…
AltanS Jul 29, 2026
7f0189d
fix(web): one colour spelling in the mirror, splash values that match…
AltanS Jul 29, 2026
812e938
docs: CLAUDE.md's mirror rule restated ADR 0002's false mechanism
AltanS Jul 29, 2026
77f363a
chore: ignore node_modules as a symlink too, not just a directory
AltanS Jul 29, 2026
4b4aead
docs: trim the mirror rule to the half that can actually break
AltanS Jul 29, 2026
df47112
feat(web): appearance lives in Settings only, and follows the phone b…
AltanS Jul 29, 2026
6f84045
docs: point the 0.20.0 changelog at commits that exist
AltanS Jul 29, 2026
ff0cf1d
Merge branch 'main' into feat/light-and-system-theme-takeover
AltanS Jul 29, 2026
0bc4a41
Merge pull request #46 from AltanS/feat/light-and-system-theme-takeover
AltanS Jul 29, 2026
9d87bb0
Merge branch 'main' into feat/dashboard-triage-takeover
AltanS Jul 29, 2026
e024f48
fix(web): one classifier for rows and chips, and clear out the parts …
AltanS Jul 29, 2026
3d5b191
fix(web): stop the settings page rearranging itself, and square off t…
AltanS Jul 29, 2026
e208408
fix(web): give the mirror a top edge so the pane row doesn't bleed in…
AltanS Jul 29, 2026
f7e616b
fix(bridge): only a request that will be served marks a pane seen
AltanS Jul 29, 2026
2999660
Merge pull request #47 from AltanS/feat/dashboard-triage-takeover
AltanS Jul 29, 2026
4558984
Merge branch 'main' into feat/clickable-links-takeover
AltanS Jul 29, 2026
a2febae
fix(web): restore the describe block closers lost merging the two mir…
AltanS Jul 29, 2026
d3cf57e
docs: the link tap target overlaps its neighbours on purpose — say wh…
AltanS Jul 29, 2026
8d59b3e
Merge pull request #48 from AltanS/feat/clickable-links-takeover
AltanS Jul 29, 2026
a347c4b
docs: consolidate the 0.20.0 changelog and credit the contributor
AltanS Jul 29, 2026
e8b1357
fix(bridge): rotation-following was the one journal path that skipped…
AltanS Jul 29, 2026
bf38d45
chore: hold new dependency versions for 7 days before installing them
AltanS Jul 29, 2026
fd6caf1
chore(release): 0.20.1
AltanS Jul 29, 2026
c674af5
docs: AGENTS.md points at CLAUDE.md, and CLAUDE.md catches up with th…
AltanS Jul 29, 2026
08f44f6
fix(scripts): find Bun when Herdr invokes an action
konpyl Jul 29, 2026
4841e37
fix(scripts): only an absolute Bun path may reach PATH, and pin it in…
AltanS Jul 29, 2026
6071de4
Merge PR #52: find Bun when Herdr invokes a plugin action
AltanS Jul 29, 2026
ae8f562
chore(release): 0.20.2
AltanS Jul 29, 2026
fe8e548
fix(web): let the statusline run be as tall as a real statusline
stekman08 Jul 30, 2026
a7d8f9a
fix(test): make the pi journal fixture portable to macOS
stekman08 Jul 30, 2026
36c78c7
docs: ADR 0004 — the statusline-run bound guards less than it looks
stekman08 Jul 30, 2026
a0be73d
feat(scripts): supervise the bridge with launchd on macOS
darieldatoon Jul 30, 2026
b1ebb83
fix(scripts): retry launchd bootstrap across the bootout teardown window
AltanS Jul 30, 2026
8a68416
Merge PR #57: supervise the bridge with launchd on macOS
AltanS Jul 30, 2026
273d886
fix(web): default the mirror to wrap on
AltanS Jul 30, 2026
73cc7da
test(web): pin both wrap branches, not just the new default
AltanS Jul 30, 2026
96df14f
Merge PR #56: let the statusline run be as tall as a real statusline
AltanS Jul 30, 2026
f053f0d
Merge PR #59: default the mirror to wrap on
AltanS Jul 30, 2026
61db7a5
feat(harness): surface the whole statusline run, not just its first row
AltanS Jul 30, 2026
5b5106c
fix(scripts): a Mac that can't bootstrap keeps a bridge, unsupervised
AltanS Jul 30, 2026
ac3c62d
feat(web): the statusline strip keeps the agent's own colour
AltanS Jul 30, 2026
1a6f532
Merge PR #60: surface the whole statusline run, in the agent's own co…
AltanS Jul 30, 2026
0cbf583
chore(release): 0.21.0
AltanS Jul 30, 2026
bdf4c26
feat(web): read the two dialog shapes that fell through to the raw mi…
konpyl Jul 29, 2026
3dc8945
test(web): pin the five dialog fixtures PR #51 added to the chrome co…
AltanS Aug 3, 2026
8b10244
Merge pull request #62 from AltanS/supersede-pr-51
AltanS Aug 3, 2026
bddded3
fix(bridge): ReadSource's unwrapped variant matches the wire (recent_…
AltanS Aug 3, 2026
539cdf4
feat(bridge): OpenCode journal adapter — history out of the sqlite store
AltanS Aug 3, 2026
78cf013
chore(release): 0.22.0
AltanS Aug 3, 2026
3be4934
feat(web): one Controls row, labelled display prefs, and press feedba…
AltanS Aug 3, 2026
e7ada40
feat(web): hold-to-repeat arrows, guarded queue discard, pane-kind qu…
AltanS Aug 3, 2026
397743f
chore(release): 0.23.0
AltanS Aug 3, 2026
a0f880e
Merge branch 'feat/controls-restructure' — one Controls row, press fe…
AltanS Aug 3, 2026
bb36724
docs(adr): 0005 — a composed key queue never outlives its dock
AltanS Aug 3, 2026
aeeddcd
fix(ctl): update the checkout in whatever shape Herdr installed it
AltanS Aug 3, 2026
8c8b98f
chore(release): 0.23.1
AltanS Aug 3, 2026
5f1c3ed
Merge pull request #64 from AltanS/fix/update-in-managed-checkout
AltanS Aug 3, 2026
f5b2eff
docs(changelog): spell out the one-time reinstall 0.23.1 needs
AltanS Aug 3, 2026
ff9b037
docs(release): drop "pulls … and re-links" from the notes boilerplate
AltanS Aug 3, 2026
d51ad6b
docs: the two checkout shapes, and what #63 means for an existing ins…
AltanS Aug 3, 2026
79f30e6
fix(push): send agent alerts at high urgency so Android stops deferri…
AltanS Aug 4, 2026
3135638
chore(release): 0.23.2
AltanS Aug 4, 2026
746ce87
feat(idle-lock): pause an unattended screen instead of gating a retur…
AltanS Aug 4, 2026
4ffce3c
feat(idle-lock): glass cover, and hold it through the catch-up refetch
AltanS Aug 4, 2026
120f106
chore(release): 0.23.3
AltanS Aug 4, 2026
29daaae
docs: version by what the operator has to do, not by what moved
AltanS Aug 4, 2026
b353711
docs(adr): 0008 — Collie does not run a terminal emulator
AltanS Aug 4, 2026
5392ac7
feat(menu): drive an unrecognised modal by the keys its footer names
AltanS Aug 5, 2026
c4ffe45
feat(reply): refuse to type when the agent's input box isn't on screen
AltanS Aug 5, 2026
f16b244
chore(release): 0.24.0
AltanS Aug 5, 2026
9d41411
feat(composer): keep a per-pane draft across navigation
AltanS Aug 5, 2026
374f31b
chore(release): 0.24.1
AltanS Aug 5, 2026
d872490
feat(menu): label the ←/→ cluster with the value it adjusts
AltanS Aug 5, 2026
b88c93f
chore(release): 0.24.0 — fold 0.24.1 into the unreleased 0.24.0
AltanS Aug 5, 2026
0c9dace
refactor(harness): make menus an explicit harness contract, not a Cla…
AltanS Aug 5, 2026
80cc870
docs(changelog): record the menu harness contract under 0.24.0
AltanS Aug 5, 2026
3b5cf7c
refactor(harness): hoist every dialog model out of the Claude grammars
AltanS Aug 5, 2026
79ebc0c
refactor(guard): run every dialog race guard through the pane's adapter
AltanS Aug 5, 2026
b78aa0f
test(conformance): pin the dialog-model contract for every block kind
AltanS Aug 5, 2026
f7800bb
docs(changelog): record the neutral dialog contract + generic race gu…
AltanS Aug 5, 2026
9db4b9b
docs(changelog): tighten two 0.24.0 lines
AltanS Aug 5, 2026
6def208
fix(web): verify wrapped CJK drafts so the reply guard submits them
Aug 6, 2026
e9f1a33
fix(guard): read Claude's paste placeholder as send evidence for long…
AltanS Aug 6, 2026
3c2a9a5
chore(release): 0.24.1
AltanS Aug 6, 2026
b2d2803
docs(readme): refresh demo screenshots for current UI, add AskUserQue…
AltanS Aug 6, 2026
b98bbfd
Merge PR #66: verify wrapped CJK drafts so the reply guard submits them
AltanS Aug 6, 2026
46a85d1
fix(guard): construct the grapheme segmenter only where Intl.Segmente…
AltanS Aug 6, 2026
f76be58
chore(release): 0.24.2
AltanS Aug 6, 2026
ded605c
docs(readme): drop the demo video, keep the screenshot grid
AltanS Aug 6, 2026
2ea3e61
fix(push): evict persistently-failing subscriptions, log the real status
AltanS Aug 7, 2026
d4387e3
chore(release): 0.25.0
AltanS Aug 7, 2026
f7b692b
Merge pull request #69 from AltanS/fix/push-prune-68
AltanS Aug 7, 2026
13919c7
feat(nav-tray): add quick Ctrl+C button in the Esc/Up gap
Jarva Aug 8, 2026
12f1b7f
fix(web): make the reply guard's input-box detection width-independent
tyamanak Aug 9, 2026
1a0c2ae
test(ctl): keep resolve_bun inside the missing-tailscale sandbox
tyamanak Aug 9, 2026
962642b
fix(markdown): render GFM tables instead of collapsing them into a pa…
AltanS Aug 9, 2026
ad65307
fix(web): bundle Nerd Font symbols so PUA glyphs stop rendering as tofu
AltanS Aug 9, 2026
d82ef1b
Merge pull request #81 from AltanS/fix/markdown-tables-72
AltanS Aug 9, 2026
d31d97d
Merge pull request #82 from AltanS/fix/nerd-font-70
AltanS Aug 9, 2026
51fce21
fix(test): stop the ctl suite re-initialising the repository it runs …
AltanS Aug 9, 2026
5c48721
Merge pull request #80 from tyamanak/pr/ctl-test-sandbox
AltanS Aug 9, 2026
d139b1b
Merge pull request #75 from Jarva/feat/nav-tray-ctrl-c
AltanS Aug 9, 2026
023b2b1
polish(nav-tray): label the quick Ctrl+C the way its preset does
AltanS Aug 9, 2026
de88b38
Merge pull request #78 from tyamanak/pr/input-box-detection-upstream
AltanS Aug 9, 2026
1a07af9
feat(mirror): clip terminal rule borders
en-ver Aug 8, 2026
661d6bf
fix(mirror): re-anchor the clip threshold and keep the two border tes…
AltanS Aug 9, 2026
4480019
Merge pull request #83 from AltanS/fix/clip-rule-rows-79
AltanS Aug 9, 2026
0acdc80
feat(composer): send keys straight to the pane, without a trailing Enter
aspiers Aug 9, 2026
7dea503
feat(composer): a "Type" toggle in the Controls row, dying with the p…
AltanS Aug 9, 2026
57080f5
feat(composer): give the row an on-state colour, and put attach insid…
AltanS Aug 9, 2026
4654013
Merge pull request #84 from AltanS/feat/type-into-terminal-77
AltanS Aug 9, 2026
3037179
chore(release): 0.26.0 — type into the terminal, tables, Nerd Font gl…
AltanS Aug 9, 2026
0dc852e
fix(web): mark API requests as XHR so an identity proxy refuses inste…
ojulean Aug 10, 2026
e021ae3
Merge pull request #86 from ojulean/fix/xhr-header-auth-detection
AltanS Aug 10, 2026
dab122e
fix(state): stop the session-name poll from scrolling idle claude panes
OowhitecatoO Aug 10, 2026
8f4276c
docs: a fork PR leaves the version files alone
AltanS Aug 10, 2026
82bbe0e
fix(ctl): say so when tailnet ACLs admit no peer to this node
AltanS Aug 10, 2026
ff84538
feat(ctl): `qr` prints the URL as a scannable code
AltanS Aug 10, 2026
ed13e71
chore(release): 0.27.0 — scan your way in, and an honest tailnet line
AltanS Aug 10, 2026
f615362
docs: lead the README with what Collie actually gives you
AltanS Aug 10, 2026
1274a68
test(fixtures): a 20-pane omp corpus, sanitized in place
qaz74107410 Aug 11, 2026
786b99f
test(harness): hold the reference grammars to raw on a foreign harness
qaz74107410 Aug 11, 2026
8947cb1
feat(web): a slash palette for omp, sourced from its own captures
qaz74107410 Aug 11, 2026
b98b90d
feat(harness): give omp an adapter, so a reply stops confirming its p…
qaz74107410 Aug 11, 2026
0719353
fix(web): a command lookup must not answer with Object.prototype
qaz74107410 Aug 11, 2026
4fc9e5e
fix(harness): pin omp's composer by glyphs, and only while it has the…
qaz74107410 Aug 11, 2026
6c8332f
fix(reply): a destructive pre-clear needs a live read, and a binding …
qaz74107410 Aug 11, 2026
19572d7
fix(push): Apple refuses the update topic — "collie-update" is an imp…
ojulean Aug 11, 2026
cefbed9
docs(reply): the sweep's fail-fast is inert, not stale, for a raw-onl…
qaz74107410 Aug 11, 2026
2e3f79d
Merge pull request #90 from ojulean/fix/apple-push-topic
AltanS Aug 11, 2026
b549101
fix(journal): a harness can keep its logs in more than one root
AltanS Aug 11, 2026
ea21dda
Merge pull request #94 from AltanS/fix/92-multi-root-transcripts
AltanS Aug 11, 2026
8572e49
feat(contrib): Windows lifecycle controller, community-maintained (fr…
Pimpmuckl Aug 11, 2026
d76a4c1
test(composer): await the stall the `Type anyway?` retry ends in
AltanS Aug 11, 2026
2c44255
fix(harness): omp names no region it would put out of the bridge's reach
AltanS Aug 11, 2026
88d329b
Merge pull request #93 from qaz74107410/feat/omp-harness-adapter
AltanS Aug 11, 2026
2910f40
chore(release): 0.28.0 — omp joins the herd, and Apple pushes work again
AltanS Aug 11, 2026
9dbc0fe
feat(herd): name a pane by what its process says it is doing
praneetrohida Aug 15, 2026
6183c89
test(composer): a stall that outlives its test lands in the next one
AltanS Aug 13, 2026
c0ce09e
fix(harness): the plan dialog's row 4 is an input, and focus changes …
navidkashani Aug 12, 2026
967e94d
feat(harness): give the plan dialog's feedback row a route from the p…
AltanS Aug 15, 2026
64de1d4
fix(harness): the plan input wraps, and only the plan input is modelled
AltanS Aug 15, 2026
980dde0
Merge pull request #102 from AltanS/fix/95-plan-feedback
AltanS Aug 15, 2026
982b8e1
fix(herd): a shell's user@host:cwd title is a locator, not a name
AltanS Aug 16, 2026
e110933
Merge pull request #100 from praneetrohida/feat/name-panes-by-termina…
AltanS Aug 16, 2026
0021300
fix(push): a re-subscribe replaces the row it supersedes (#104)
AltanS Aug 16, 2026
452da20
fix(direct-typing): say the mode stopped when the app comes back
enieuwy Aug 16, 2026
c18a6a6
chore(release): 0.29.0 — plan feedback, panes named by their work
AltanS Aug 16, 2026
1334540
feat(composer): a password prompt names itself and offers Type (#103)
AltanS Aug 16, 2026
8a7b9e5
chore(release): 0.30.0 — a password prompt has a route from the phone
AltanS Aug 16, 2026
581f30e
docs(adr): 0017 — recognising a password prompt changes what Collie s…
AltanS Aug 16, 2026
92233ef
Merge pull request #106 from AltanS/fix/103-password-prompt
AltanS Aug 16, 2026
85f0454
feat(push): `push-keys` generates the VAPID keypair and writes the ri…
AltanS Aug 17, 2026
357b86f
feat(mirror): "Tap to type" — the mirror can stop volunteering the ke…
AltanS Aug 17, 2026
30f2f29
Merge remote-tracking branch 'origin/main' into fix/direct-typing-bac…
AltanS Aug 17, 2026
1a2ca49
fix(direct-typing): settle the blur by cancellation, and don't owe th…
AltanS Aug 17, 2026
8295e80
Merge pull request #108 from enieuwy/fix/direct-typing-background-notice
AltanS Aug 17, 2026
7965674
fix(drafts): a paste too big to persist no longer restores an older, …
AltanS Aug 17, 2026
0b46e53
Merge remote-tracking branch 'origin/main' into hn/feedback
AltanS Aug 17, 2026
5dda876
audit: optional COLLIE_AUDIT_CONTENT=none, keeping the event and drop…
Aug 17, 2026
cdad445
audit: redact by field role, not by value type
AltanS Aug 17, 2026
1e83691
Merge pull request #111 from AltanS/audit/content-redaction
AltanS Aug 17, 2026
27f4cdf
fix(paste): a half-arrived long send is no longer send evidence (#110)
AltanS Aug 17, 2026
35da673
feat(palette): COLLIE_COMMANDS — the Agent-commands palette becomes y…
enieuwy Aug 16, 2026
28bdf5a
refactor(palette): operator command rows move from an env var to comm…
AltanS Aug 17, 2026
f03daa4
docs(palette): ADR 0018 + the operator-facing route to commands.toml
AltanS Aug 17, 2026
59df2a9
Merge pull request #112 from AltanS/commands/operator-rows
AltanS Aug 17, 2026
e151813
docs(readme): a Features title, and the palette bullet leads with qui…
AltanS Aug 17, 2026
5a4e6c9
Merge pull request #113 from AltanS/fix/110-partial-arrival
AltanS Aug 17, 2026
9464c14
docs: deployment variants B–E move to DEPLOYMENT.md
AltanS Aug 17, 2026
c52d4af
docs(readme): shorter, how-first, findable
AltanS Aug 17, 2026
12212a8
chore(release): 0.31.0 — your palette, your commands
AltanS Aug 17, 2026
cc810c9
fix(bridge): a long request survives socket backpressure
AltanS Aug 17, 2026
cd2f1f8
fix(docs-in-code): pointers name DEPLOYMENT.md now that variants B–E …
AltanS Aug 17, 2026
ee64069
docs(readme): one spelling for COLLIE_MULTI_SESSION, and the push ver…
AltanS Aug 17, 2026
ff2f82e
chore(release): 0.31.1 — a long request survives backpressure
AltanS Aug 17, 2026
09b0571
feat(nav-tray): F1–F12 behind an F keys disclosure (#119)
martin-tahli Aug 19, 2026
09d5e12
Merge pull request #120 from martin-tahli/feat/nav-tray-f-keys
AltanS Aug 19, 2026
ecbf86a
docs(adr): 0020 — a major upgrade is consented by flag
AltanS Aug 19, 2026
633b2a1
feat(update): a routine update stays inside its major; --major is the…
AltanS Aug 19, 2026
99910cf
fix(update): gate a clone on the commit its pull will actually take
AltanS Aug 19, 2026
a38df8c
feat(web): the banner says WHICH kind of behind you are
AltanS Aug 19, 2026
c02ab19
feat(keys): the Keys tray's presets become yours, in keys.toml
AltanS Aug 19, 2026
a6a6ded
docs(keys): the operator-facing route to keys.toml
AltanS Aug 19, 2026
0931d16
docs(adr): 0020's linked-clone pre-flight reads @{u}, not FETCH_HEAD
AltanS Aug 19, 2026
0f4c651
fix(web): survive a cold reboot offline — render the cached last snap…
AltanS Aug 19, 2026
c473aa0
fix(web): say "disconnected", and when — never "No agents" on a faile…
AltanS Aug 19, 2026
1042fe0
fix(web): date a stale mirror by the PANE's stamp, not the snapshot's
AltanS Aug 19, 2026
adb8064
Merge branch 'keys/operator-rows'
AltanS Aug 19, 2026
a80df6a
Merge branch 'gate/backport-main'
AltanS Aug 19, 2026
df14d4a
docs(readme): what the 0.32.0 gatekeeper protects
AltanS Aug 19, 2026
c345ccd
chore(release): 0.32.0
AltanS Aug 19, 2026
774902b
feat(bridge): windows-native fallbacks for state and config dirs
kimjunny Aug 22, 2026
ae3b67a
fix(bridge): case-aware path comparisons for win32
kimjunny Aug 22, 2026
ff0d9af
fix(bridge): force-kill shutdown note and service-neutral wording
kimjunny Aug 22, 2026
6c5520e
feat(scripts): cross-platform version gate in typescript
kimjunny Aug 22, 2026
929f578
feat(scripts): cross-platform ctl skeleton
kimjunny Aug 22, 2026
384c06f
feat(scripts): windows task scheduler, systemd and launchd backends
kimjunny Aug 22, 2026
9ba1881
feat(scripts): lifecycle verbs for ctl
kimjunny Aug 22, 2026
1923d9b
feat(scripts): info verbs for ctl
kimjunny Aug 22, 2026
8895d66
feat(scripts): build, serve and push verbs for ctl
kimjunny Aug 22, 2026
f7baed9
test(scripts): ctl integration suite over injected fakes
kimjunny Aug 22, 2026
3d1b63d
feat(plugin): declare windows platform with neutral bun actions
kimjunny Aug 22, 2026
4c8415a
ci: add windows job and keep posix ctl coverage
kimjunny Aug 22, 2026
fa9ebd6
docs: windows install, variant-e fallback and adr-0021
kimjunny Aug 22, 2026
f5831d9
test(bridge): windows-portable fixtures and expectations
kimjunny Aug 22, 2026
bd3f5eb
test(web): storage shim for node 22+ gap and locale-proof date
kimjunny Aug 22, 2026
574c9e8
fix(scripts): wire the verb modules into ctl dispatch
kimjunny Aug 23, 2026
c4600c7
fix(scripts): single-writer bridge log under the windows task
kimjunny Aug 23, 2026
f49f0f6
test(web): raise vitest timeout headroom for loaded windows hosts
kimjunny Aug 23, 2026
7d4c0ef
fix(scripts): complete ctl runtime and service wiring
kimjunny Aug 23, 2026
a00f662
docs: align Windows lifecycle and ingress guidance
kimjunny Aug 23, 2026
a3c698d
style(scripts): remove trailing backend whitespace
kimjunny Aug 23, 2026
3f9dec4
fix(scripts): address lifecycle review findings
kimjunny Aug 23, 2026
58bf526
docs: describe the supported TypeScript ctl path
kimjunny Aug 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
81 changes: 81 additions & 0 deletions .adr/0001-one-managed-front-door.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# 0001 — Collie manages exactly one front door

- **Status:** Accepted
- **Date:** 2026-07-27
- **Shipped in:** 0.16.0
- **Trail:** [PR #26](https://github.com/AltanS/collie/pull/26) (declined,
[reasoning](https://github.com/AltanS/collie/pull/26#issuecomment-5085567630)) ·
[PR #36](https://github.com/AltanS/collie/pull/36) (what we kept from it)

## Context

Collie is remote shell access. Whatever sits in front of the loopback bridge is the only thing
between a stranger and a terminal running as you, so "which front door" is a security question
before it is a convenience one.

**The bridge is already tunnel-agnostic.** Its entire Tailscale coupling is *one* line —
`req.headers.get("tailscale-user-login")` in `bridge/server.ts`. Everything else is a convenience in
`scripts/collie-ctl.sh` and the README's voice. The bridge binds loopback, speaks plain HTTP, and
gates on `Host`, `Origin`, and two optional headers, one of which (`COLLIE_DEVICE_HEADER`) is
deliberately vendor-neutral.

PR #26 proposed a **second managed front door**: `COLLIE_FRONT_DOOR=tailscale|netbird|proxy`, a
supervised `netbird expose` sidecar with its own systemd unit and teardown, and config plumbing for
NetBird's auth flags. 1441 additions across 12 files. The work was careful, and it raised a fair
question — is Collie too tied to Tailscale, and are non-Tailscale users being hindered?

Three things settled it, each checked rather than assumed:

1. **Nobody was blocked.** `COLLIE_SKIP_SERVE=1` plus `netbird expose 8787` *is* the whole
integration. What the PR added on top was supervision and teardown of that one command — the same
thing Variant C already, deliberately, declines to do for anyone's Caddy.
2. **We would have been shipping blind.** NetBird isn't installed on the deployment host and there's
no CI for it, so its CLI contract — flag names, the v0.66 floor, what `expose` actually publishes
— would have been maintained by reading a PR description rather than by anything that runs.
3. **Managing someone else's authenticated process means owning their credentials.** The PR
demonstrated the cost rather than hypothesising it: the generated runner passed `--with-pin` and
`--with-password` as command-line arguments, so `ps -eo args` and `/proc/<pid>/cmdline` (mode
`444`) handed them to any local user.

And the precedent doesn't scale. Cloudflare Tunnel, ZeroTier, Twingate and Nebula all have equal
claim, each with a different CLI and auth model, all as `case` branches in one bash script. That is a
plugin-shaped problem being solved in the wrong shape.

## Decision

**Collie manages exactly one front door: `tailscale serve`.**

We own its lifecycle end to end — `collie-ctl.sh` publishes it, records the mapping in
`<config-dir>/tailscale-managed-handler`, and only ever tears down a mapping still matching that
record.

**Every other tunnel is `COLLIE_SKIP_SERVE=1` plus [DEPLOYMENT.md Variant
E](../DEPLOYMENT.md#variant-e--any-other-mesh-or-tunnel-netbird-zerotier-cloudflare-tunnel).** The
operator owns the ingress; Collie publishes nothing, supervises nothing, and tears down nothing.

The criterion is not popularity or quality. It is: **we manage only what we run and can test.**
`tailscale serve` is on the deployment host, so a regression surfaces the same day.

## Consequences

**Accepted costs.** Non-Tailscale operators supervise their own tunnel — no sidecar unit from us, and
`uninstall` won't remove it. That is exactly what Variant C has always promised for a reverse proxy,
so it is a consistency, not a new gap.

**What we keep owning.** The ownership tracking extracted from #26 (0.16.0) makes the one managed
mapping precise in both directions: publishing refuses to replace a root mount we don't own, and
teardown refuses to remove one that was replaced out from under us. A blind
`tailscale serve --https=443 off` could previously unpublish a mapping Collie never created.

**Documentation carries the load instead of code.** Variant E covers NetBird, ZeroTier, Cloudflare
Tunnel and anything not yet invented, for zero runtime surface. When the honest fix for "users feel
excluded" is a doc section, that is the fix.

**The funnel prohibition generalises.** "Never `tailscale funnel`" was never about Tailscale; it is
about reachability. Any tunnel offering a public URL inherits it, and auth in front of a public URL
is not a substitute for not having one. A shared PIN guarding a root shell is a root password.

**What would justify revisiting.** If the deployment host itself moves to another mesh, that mesh
becomes testable and its front door becomes ownable. The rule that survives is *exactly one managed
front door* — not one per vendor. Adding a second, whatever the vendor, means maintaining a CLI
contract we cannot exercise, and this ADR should be superseded rather than quietly ignored.
156 changes: 156 additions & 0 deletions .adr/0002-invert-the-light-terminal-mirror.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
# 0002 — The light terminal mirror is inverted, not re-themed

- **Status:** Accepted
- **Date:** 2026-07-28 (revised 2026-07-29)
- **Shipped in:** _(set at the release commit)_
- **Trail:** every figure is measured. Colour-form counts come from live panes read through the
production path (`pane.read` → `/api/pane/:id`); contrast is rasterized through a canvas to
resolve `oklch()`/`light-dark()` and composited against the real ancestor stack.

## Context

The obvious light mirror is a light ANSI palette: define `--ansi-0…15` twice, emit `var(--ansi-N)`
from `lib/ansi.ts`, let CSS swap them. This work carried that design for most of its life, built
and tested, before anyone measured whether it would matter.

**Only one of the three colour forms is themeable, and it is the rare one:**

| form | meaning | redirectable by a palette? |
| --- | --- | --- |
| `38;2;r;g;b` | an absolute sRGB value | **no** |
| `38;5;n`, n ≥ 16 | the 6×6×6 cube / greyscale ramp | **no** |
| `30–37`, `90–97`, `38;5;n` n < 16 | one of the 16 palette slots | **yes** |

Both spellings in that third row are the same slot — `31m` and `38;5;1` — and must be counted
together. An earlier revision of this ADR tabulated all of `38;5` as "256-colour", hiding palette
slots inside the unthemeable cube and making the measurement blind to the one harness that uses
them.

**What agents emit**, per harness, TUI output only (shell prompt and MOTD excluded):

| harness | truecolor | 256-cube | **themeable** |
| --- | --- | --- | --- |
| opencode | **100%** (508 seqs) | 0% | **0%** |
| pi | 89% | 5% | 5% |
| claude | 79% | 20% | 1% |
| codex | 7% | 34% | **59%** |

For three of four, truecolor dominates — and truecolor names an absolute value with no slot to
redirect. Codex is the exception; see the Decision.

**Absolute values authored for black are unreadable on white.** Of 13 distinct colours in a real
pane, eight fall below 3:1 on white — including a `●` at **1.0:1** and Monokai's foreground
`#f8f8f2` at **1.07:1**. This is faithful — the same agent looks equally bad in a real light
terminal — but the mirror is the app's primary reading surface, so fidelity is not the goal.

Two other exits, both rejected for losing the syntax highlighting that makes output scannable:
keeping the mirror permanently dark (what an IDE does, but it leaves a dark slab in a light app),
and clamping absolute colours to a luminance floor (an arbitrary mapping that misrepresents what
the program emitted).

## Decision

**Render the mirror in dark space under every theme, and invert it in light.** The `<pre>` carries
`filter: invert(1) hue-rotate(180deg)`, reset to `none` under the `dark:` variant. The `hue-rotate`
is what makes this more than a negative: it approximately restores hue after inversion flips
lightness, so green stays green and syntax highlighting survives.

Three rules follow, all load-bearing:

1. **Everything inside the `<pre>` is authored for a dark ground** — palette, find-match highlight,
muted rule glyphs. A `dark:` variant inside the mirror is a bug: it tracks the root theme, which
is backwards in inverted space.

2. **Colours inside the `<pre>` are written as literals — a convention, not a constraint.**
`color-scheme: dark` on the element *does* flip an inherited `light-dark()` token; resolution is
element-scoped, per spec (verified in Chromium: with the root pinned light, `--muted-foreground`
resolves to its light half outside the `<pre>` and its dark half inside). The literals here are
byte-exact matches for those dark halves, so either would render identically. Literals win
because they sit beside truecolor nothing can re-theme, and they say at the point of use that
the value is deliberately theme-independent. What matters is that the mirror does not mix the
two.

3. **The filter is scoped to the `<pre>` alone.** The interactive blocks (prompt-select, wizard,
preview, multi-select) are siblings, not children, so they keep normal app theming.

`--ansi-0…15` therefore has **one** set of values, the dark one. `lib/ansi.ts` still emits
`var(--ansi-N)`: the variables remain the seam where indexed colour is defined once, and both
spellings route through them.

### Why codex does not reopen this

Codex is the one harness drawing its chrome from themeable slots, and the one that asks the
terminal what background it is on (`OSC 10`/`OSC 11`). That looks like an argument for re-theming.
It is not: **herdr answers neither query, so codex falls back to dark** — and Collie could not
answer anyway, since it reads a rendered buffer downstream of the PTY. Measurements and
consequences in [`HERDR_API.md`](../HERDR_API.md).

## Consequences

Same pane, light against dark, sampling rendered pixels:

| | background | min | median | max |
| --- | --- | --- | --- | --- |
| dark (unchanged) | `#0a0a0a` | 1.34 | 7.46 | 21.0 |
| light (inverted) | `#f5f5f5` | 1.43 | 6.73 | 18.69 |

Light tracks dark almost exactly — it inherits whatever readability the agent designed for instead
of fighting it. (Sub-2 values are antialiasing edges, present in both.)

What it costs:

- **Colours are approximations.** `hue-rotate` is a linear matrix, not a true hue rotation, so
saturated colours shift. The mirror shows a palette *interpreted*, not reproduced.
- **Diffs and inverse-video become dark slabs in light.** Legibility survives; only visual weight
inverts. Greying them is worse.
- **Unmeasured scroll cost.** A CSS filter over a `<pre>` running to thousands of lines has not been
profiled on a phone.
- **A trap for contributors.** Every instinct — use the token, add a `dark:` variant — is wrong
inside the `<pre>`, and wrong in a way that type-checks and often passes a computed-style test.
`components/ansi-output.test.tsx` guards rules 1 and 2.

The sharpest edge is **cancelling the filter**, which the find highlight does so its yellow isn't
reinterpreted as brown. Re-applying `invert + hue-rotate` cancels it — but only for colours the
element sets *itself*. The current match survives because `text-black` pins its text (black →
invert → white → invert → black). The same cancellation on a non-current match, which sets no
text colour, sent its *inherited* text light → dark → light: invisible on its own highlight. It
looked symmetrical and was not. **Cancel the filter only on an element that fully specifies both
its foreground and its background.**

### Known limitation: an agent on a light theme

The agent's own theme travels **as content, not metadata**. Claude Code under `theme: light` emits
the same 75 sequences and 16 distinct colours as under `theme: dark`, values inverted (foreground
`#333333` rather than `#f8f8f2`). Nothing in the chain carries a theme *field*, so Collie cannot
detect which is in use — and such a pane is unreadable in **both** Collie themes (1.57:1 dark,
1.47:1 light).

**Pre-existing**: the shipped dark-only mirror fails it identically. Recorded because the fix, if
wanted, is a per-pane "don't invert this one" — so any storage added for mirror preferences should
be keyed to let that layer on later.

### Alternatives closed off

- **A light ANSI palette.** Reaches 0–5% of what three of four harnesses emit. The set was actually
built — VS Code's light terminal palette, verified against upstream (including catching that
`ansiGreen` moved from `#00BC00` to `#107C10`) — and is in the git history if the premise changes.
- **Set the agent to a light theme and don't invert.** Strictly *more* faithful: colours authored
for white rather than negated onto it. Rejected only because Collie cannot detect the agent's
theme, so it cannot know when to suppress the filter. Revisit alongside a per-pane override or a
reliable detection heuristic.
- **Per-harness colour maps** (translate claude's dark palette to its light one). Needs a table per
harness, per theme, per release; breaks silently when a harness retunes a colour; and cannot cover
output from the tools and programs an agent runs, which is in no table. If per-harness knowledge
is ever wired in, the payload should be **one bit** — "authored for dark/light" — not a colour
map. One bit is all the mirror needs.

**What would justify revisiting:**

- **A measured codex real-output profile showing majority palette.** The 59% above is chrome only,
n=29, captured while its auth was stale, so it never rendered a diff or a highlighted code block.
If real output is also palette-dominated, the answer is per-harness rendering — re-theme codex,
keep inverting the rest — not a global change.
- **Herdr answering `OSC 11`, or a palette protocol letting the client supply the ground.** Either
restores the premise that a re-themed palette can work.
- **A measured scroll regression on a mid-range phone** — which reopens it in favour of keeping the
mirror dark, not of re-theming it.
71 changes: 71 additions & 0 deletions .adr/0003-one-shared-seen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# 0003 — "Seen" is one shared fact, and Collie only trusts what happened in Collie

Status: **Accepted** (2026-07-28)

## Context

The dashboard sorts the herd by attention and then by recency, and it surfaces a **Ready · unseen**
section: agents that finished while you weren't looking. Both need to know two things per pane —
when the agent last moved, and when *you* last looked at it.

Herdr supplies neither. Its pane, tab, and workspace records carry **no timestamps of any kind**
(see [`HERDR_API.md`](../HERDR_API.md)), so every notion of "when" in this feature is one Collie
derives and owns. That forced two questions that would otherwise never have been asked out loud.

**Where does "seen" live?** Collie is a phone UI for a herd you also drive from a desk, and the same
person uses both. The bridge already persists exactly this kind of state — `snooze.json`,
`notify-prefs.json` — bridge-wide rather than per-device, on the same reasoning: a notification
fans out to every device, so muting it on one must mute it on all.

**What counts as looking?** Herdr reports a `focused` flag per pane, so the bridge *could* see you
working in a pane at the desk and count that as having seen it. That was considered and rejected
during design.

## Decision

**One shared "seen", recorded bridge-side and persisted to the state dir.** `activity.json` holds
`{activeAt, seenAt}` per pane, keyed by session name (pane ids are session-scoped and collide across
sessions). Not per-device, not in `localStorage`.

**Only what happens in Collie counts as seeing.** `seenAt` is stamped when a request reaches
`/api/pane/:id` — opening the pane, replying, sending keys, reading its history. A Herdr focus at
the desk does not stamp it, and neither does anything else the bridge merely observes.

**"Seen" is a comparison, not a stored flag.** An agent is unread exactly when
`status === "done" && activeAt > seenAt`. There is no read-receipt table and nothing to keep in
sync: opening the pane bumps `seenAt` past `activeAt`, and the row leaves the section by itself.

**A first sighting is seeded as already-seen** (`activeAt = seenAt = now`), so only transitions
observed *after* Collie first saw a pane can mark it unread. This is the same rule the state engine
already applies to notifications — a first sighting never fires a transition, so a fresh start
doesn't notify for agents that were already blocked.

## Consequences

- **A second device agrees with the first.** An alert cleared on the phone is cleared on the laptop.
This is the whole point, and it's why per-device storage was rejected: the failure mode there is
an alert you already dealt with still shouting at you somewhere else.
- **Two people sharing one bridge share one "seen".** Accepted. Collie's threat model is a personal
tailnet with one operator; a bridge is remote shell access, not a multi-tenant service.
- **Working in a pane at the desk does not clear its Collie alert.** This is the deliberate cost.
Counting a Herdr focus would let a pane you merely clicked past silently clear an alert you never
read — a false negative on the one thing the dashboard exists to surface. A false *positive* (an
item still listed as unseen after you dealt with it at the desk) costs one tap; a false negative
costs a missed agent.
- **The ledger writes on a debounce.** An open pane polls about once a second and each poll stamps
`seenAt`; in memory that's free, on disk it would be a write per second forever. Flushes are
capped at one per 10s plus one on shutdown, so an unclean kill can lose up to ten seconds of
precision — imperceptible in a feature whose finest unit is "just now".
- **The state can be thrown away.** Delete `activity.json` and the next poll re-seeds every pane as
seen. Nothing else depends on it.

### What would justify revisiting

- Herdr starts reporting real per-pane activity timestamps — then `activeAt` should come from the
source rather than from Collie's own observation, and a bridge restart would stop being a
re-seed.
- Collie grows genuine multi-user support (distinct identities, not just distinct devices). Then
"seen" becomes per-identity, and this ADR is superseded rather than amended.
- Evidence that clearing-at-the-desk actually matters in practice — i.e. the false positives are
frequent and annoying enough to outweigh the missed-agent risk. That's a usage question, not a
design one, and it should be answered with usage.
Loading