Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
c3ec51c
feat(daemon): ai-devkitd coordination daemon — socket, sole-writer st…
codeaholicguy Oct 7, 2026
aecfcf9
test(daemon): deepen coverage + add lifecycle phase docs; fix stale s…
codeaholicguy Oct 8, 2026
014b848
feat(console): event-driven agent list refresh via daemon subscription
codeaholicguy Oct 8, 2026
c554d24
refactor(rust): split ai-devkitd into devkitd + devkit-core, wire car…
codeaholicguy Oct 8, 2026
545625d
feat(daemon): harness-port foundation — agent.enriched RPC, devkit-ha…
codeaholicguy Oct 8, 2026
12b6ded
feat(harness): port claude adapter to devkit-harness + daemon merge path
codeaholicguy Oct 8, 2026
7cfecd9
feat(harness): port codex adapter to devkit-harness
codeaholicguy Oct 8, 2026
7135890
feat(harness): port pi adapter to devkit-harness (I3)
codeaholicguy Oct 8, 2026
48fc9a8
feat(harness): port gemini adapter to devkit-harness (I4)
codeaholicguy Oct 8, 2026
4dc7277
docs(planning): record I4 gemini port commit
codeaholicguy Oct 8, 2026
22d6fd2
feat(harness): port copilot adapter to devkit-harness (I5)
codeaholicguy Oct 8, 2026
412a2a0
docs(planning): record I5 copilot port commit
codeaholicguy Oct 8, 2026
8298101
feat(harness): port grok adapter to devkit-harness (I6)
codeaholicguy Oct 8, 2026
73a7930
docs(planning): record I6 grok port commit
codeaholicguy Oct 8, 2026
cdf1cc6
feat(harness): port opencode adapter to devkit-harness (I7)
codeaholicguy Oct 8, 2026
5299518
docs(planning): record I7 opencode port commit; fix I3 reference
codeaholicguy Oct 8, 2026
d788bf6
feat(harness): port devin adapter to devkit-harness (I8)
codeaholicguy Oct 8, 2026
0d1e823
docs(planning): record I8 devin port commit
codeaholicguy Oct 8, 2026
9c0e9ba
feat(harness): port kiro adapter to devkit-harness (I9)
codeaholicguy Oct 8, 2026
38f3070
docs(planning): record I9 kiro port commit
codeaholicguy Oct 8, 2026
d7de322
feat(harness): port antigravity adapter to devkit-harness (I10)
codeaholicguy Oct 8, 2026
4bc80b1
docs(planning): record I10 antigravity port commit
codeaholicguy Oct 8, 2026
c42b79c
feat(harness): port readiness shared lib to devkit-harness (I11)
codeaholicguy Oct 8, 2026
5299bb5
docs(planning): record I11 readiness port commit
codeaholicguy Oct 8, 2026
b7402c3
feat(harness): daemon-primary cutover + efficiency report (I12)
codeaholicguy Oct 8, 2026
b1c8e6c
docs(planning): record I12 cutover commit — epic complete
codeaholicguy Oct 8, 2026
16e3c4b
perf(cli): lazy-load command modules, fix daemon sweep stalls
codeaholicguy Oct 9, 2026
174e03e
fix(channel-connector): close daemon client per registry RPC
codeaholicguy Oct 9, 2026
a1cec04
perf(devkitd): parallelize readiness probes, add params-keyed TTL cache
codeaholicguy Oct 9, 2026
924696f
revert(channel-connector): keep channel config file-backed
codeaholicguy Oct 9, 2026
a19747f
feat(channel): move bridge registry to daemon
codeaholicguy Oct 9, 2026
11789c8
fix(devkitd): serialize emit ordering so broadcast seqs stay monotonic
codeaholicguy Oct 9, 2026
e78dc0a
fix(devkitd): close the subscribe replay-to-live race
codeaholicguy Oct 9, 2026
34325c4
fix(devkitd): skip agent snapshot when the ps sweep fails
codeaholicguy Oct 9, 2026
f1f7b28
fix(devkitd): time out discovery shell-outs at 10s
codeaholicguy Oct 9, 2026
ead706d
perf(devkitd): O(n) agent snapshot diff via HashSet
codeaholicguy Oct 9, 2026
4500c9d
fix(devkitd): pid-reuse identity + bounded events table
codeaholicguy Oct 9, 2026
ab53abc
fix(devkitd): harden server, db recovery, and readiness parity
codeaholicguy Oct 9, 2026
0900811
chore(daemon-client): add nx project.json matching sibling packages
codeaholicguy Oct 9, 2026
6a26815
fix(daemon-client): drain pending on socket death; harden autostart
codeaholicguy Oct 9, 2026
67c96be
fix(devkitd): cfg-gate install_systemd to Linux
codeaholicguy Oct 9, 2026
1573690
feat(devkitd): liveOnly subscribe + console resubscribe on disconnect
codeaholicguy Oct 9, 2026
f1715bf
refactor(devkitd): consolidate agent API — drop agent.list and ported
codeaholicguy Oct 9, 2026
202b8e0
refactor(devkitd): rename agent.enriched RPC to agent.list
codeaholicguy Oct 9, 2026
409b60e
feat(devkitd): carry attributed agent info in lifecycle events
codeaholicguy Oct 9, 2026
c1d0e36
refactor(devkitd): drop unused event surface
codeaholicguy Oct 9, 2026
3b9a289
refactor(devkitd): remove registry KV table and RPCs
codeaholicguy Oct 9, 2026
a30b28f
refactor(devkitd): move readiness back to the client, drop agent.read…
codeaholicguy Oct 9, 2026
427f782
perf(cli): bundle ai-devkit entry into a single esbuild chunk set
codeaholicguy Oct 9, 2026
d7b88ce
chore(devkitd): PR-prep cleanup — dead code, stale docs, unused deps
codeaholicguy Oct 9, 2026
e382857
chore(cli): drop stale registry references from console comments/tests
codeaholicguy Oct 9, 2026
c288f4d
fix(devkitd): refuse to serve when a live daemon owns the socket
codeaholicguy Oct 9, 2026
a683edf
ci(devkitd): publish per-platform daemon binaries and daemon-client
codeaholicguy Oct 9, 2026
b447d45
Merge branch main into feature-devkitd-monorepo
codeaholicguy Oct 9, 2026
bcddc9f
test(harness): commit synthetic parity + readiness fixture bundles
codeaholicguy Oct 9, 2026
8409bd7
fix: restore files dropped from merge tree by corrupted index
codeaholicguy Oct 9, 2026
8ffc370
fix: commit feature content lost by index corruption during hook runs
codeaholicguy Oct 9, 2026
b3a0b9e
chore(channel-connector): drop comment rewording, not needed for this PR
codeaholicguy Oct 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions .github/workflows/publish-daemon-client.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: Publish Daemon Client Package

on:
release:
types: [created]
workflow_dispatch:
inputs:
tag:
description: 'Tag to publish from (e.g., cli@0.67.0)'
required: false

permissions:
contents: read

jobs:
publish:
name: Publish @ai-devkit/daemon-client to NPM
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.tag || github.sha }}

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
registry-url: 'https://registry.npmjs.org'
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Build all packages
run: npm run build

- name: Pin devkitd platform packages
working-directory: ./packages/daemon-client
# Injected at publish time so dev installs never resolve the
# unpublished platform packages. Version matches this package's —
# publish-devkitd.yml stamps the same version on the binaries.
run: |
V=$(node -p "require('./package.json').version")
for p in darwin-arm64 darwin-x64 linux-x64 linux-arm64; do
npm pkg set "optionalDependencies.@ai-devkit/devkitd-$p=$V"
done

- name: Publish to NPM
working-directory: ./packages/daemon-client
run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
97 changes: 97 additions & 0 deletions .github/workflows/publish-devkitd.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
name: Publish devkitd Platform Packages

on:
release:
types: [created]
workflow_dispatch:
inputs:
tag:
description: 'Tag to build and publish from (e.g., cli@0.67.0)'
required: false

permissions:
contents: read

jobs:
build-publish:
name: Build and publish @ai-devkit/devkitd-${{ matrix.pkg }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
# Linux targets are musl-static so one binary covers glibc and
# Alpine/musl hosts.
- os: macos-latest
target: aarch64-apple-darwin
pkg: darwin-arm64
- os: macos-latest
target: x86_64-apple-darwin
pkg: darwin-x64
- os: ubuntu-latest
target: x86_64-unknown-linux-musl
pkg: linux-x64
musl-tools: true
- os: ubuntu-latest
target: aarch64-unknown-linux-musl
pkg: linux-arm64
cross: true

steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.tag || github.sha }}

- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}

- name: Install musl toolchain
if: matrix.musl-tools
run: sudo apt-get update && sudo apt-get install -y musl-tools

- name: Install cross
if: matrix.cross
uses: taiki-e/install-action@v2
with:
tool: cross

- name: Build devkitd
working-directory: ./rust
run: |
if [ "${{ matrix.cross }}" = "true" ]; then
cross build --release --target ${{ matrix.target }} -p devkitd
else
cargo build --release --target ${{ matrix.target }} -p devkitd
fi
BIN=target/${{ matrix.target }}/release/devkitd
file "$BIN" 2>/dev/null || true
test -x "$BIN"

- name: Stage platform package
run: |
OUT=out/devkitd-${{ matrix.pkg }}
mkdir -p "$OUT"
cp npm/devkitd-${{ matrix.pkg }}/package.json "$OUT/"
cp rust/target/${{ matrix.target }}/release/devkitd "$OUT/devkitd"
chmod +x "$OUT/devkitd"

- name: Stamp version
working-directory: ./out/devkitd-${{ matrix.pkg }}
# Platform packages version in lockstep with daemon-client — its
# optionalDependencies pin the same version.
run: npm pkg set version="$(node -p "require('$GITHUB_WORKSPACE/packages/daemon-client/package.json').version")"

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
registry-url: 'https://registry.npmjs.org'

- name: Publish to NPM
working-directory: ./out/devkitd-${{ matrix.pkg }}
run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,6 @@ coverage/
.github/prompts/
.github/skills/
.worktrees/
rust/target/
packages/daemon-client/dist/
fixtures/harness/**/live*.json
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages/daemon-client/src/gen/
166 changes: 166 additions & 0 deletions docs/ai/design/2026-10-07-feature-rust-daemon.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
---
phase: design
title: ai-devkitd design
description: Unix-socket JSON-RPC daemon; SQLite sole writer; append-only event log; single discovery loop; tmux-style autostart; npm binary distribution
---

# Design — ai-devkitd

## Architecture overview

`ai-devkitd` is a single static Rust binary that owns coordination: shared
state, event distribution, process discovery, and IPC. Everything else —
harness-specific detection and parsing, channel message formatting, TUI
rendering — stays in the TypeScript clients. The daemon never needs to know
what a "codex session file" is; it knows pids, cwds, commands, registries, and
events. That boundary is deliberate: it is what lets the daemon be Rust without
dragging the TS adapter layer along, and it keeps the wire schema
harness-agnostic so a later port of parser knowledge doesn't force protocol
changes.

## IPC contract

- Transport: unix domain socket at `~/.ai-devkit/daemon.sock`, mode `0600`,
same-uid enforcement via `SO_PEERCRED` on Linux (permissive cfg fallback on
other platforms, where the 0600 socket still applies).
- Framing: one JSON object per line, both directions. Request
`{id?, method, params?}`; response `{id?, result?}|{id?, error?}` (absent
fields omitted, never `null`); event frame `{"event": {seq, ts, kind,
payload}}`.
- gRPC and localhost HTTP were rejected: no remote caller exists, TCP is a
security regression for a same-user coordination bus, and the
memory-dashboard unauthenticated-bind lesson says don't listen on the
network for local-only state.

### Methods

| Method | Params | Result |
|---|---|---|
| `ping` | — | `{pong, startedAt}` |
| `daemon.status` | — | `{version, startedAt, socket}` |
| `agent.list` | — | array of fully attributed `AgentInfo` rows — authoritative for all harness types |
| `subscribe` | `{afterSeq?, liveOnly?}` | ack `{subscribed}`, then event frames; replays persisted events after `afterSeq` first unless `liveOnly` |
| `shutdown` | — | `{ok}`; daemon exits after the response flushes |

The scoped `registry` KV table was removed: its only consumer
(channel-bridge bookkeeping) already had a complete JSON-file path, and
daemon rows had to be unioned with file rows at read time — the two-store
merge was worse than the file alone. `~/.ai-devkit/channel-bridges.json`
is again the single store; `~/.ai-devkit/channels.json` owns credentials.

## State: SQLite, daemon sole writer

rusqlite (bundled), WAL journal, `synchronous=NORMAL`. Two tables:

- `events(seq, ts, kind, payload)` — append-only log; `seq` is AUTOINCREMENT.
This is the pub/sub substrate: subscribers ack by `seq`, replay survives
daemon restarts, and the log doubles as an audit trail.
- `agents(pid, ppid, tty, command, cwd, session_file, first_seen, last_seen)` —
the discovery snapshot. `session_file` exists in the schema but is
unpopulated: session-file resolution is harness knowledge, client-side by
design.

## Events

Mutations append to `events` and publish to a `tokio::broadcast` channel.
`subscribe` first drains persisted events with `seq > afterSeq` (at-least-once
across reconnects), acks, then streams live frames. Kinds:
`agent.appeared`, `agent.disappeared`. Lifecycle payloads carry
`{pid, agents: EnrichedAgent[]}` — appeared looks up the fresh sweep cache,
disappeared the previous one (last-known info; unattributed procs get `[]`).

## Discovery loop

One `ps -axo pid=,ppid=,tty=,command=` sweep every 2s inside the daemon —
replacing the per-invocation sweeps every client ran before. Filtering is by
executable basename against the harness binary list (`claude`, `codex`,
`gemini`, `copilot`, `grok`, `opencode`, `pi`, `kiro-cli-chat`, `devin`, `kimi`,
`cursor-agent`, `droid`). Cwd comes from `/proc/<pid>/cwd` — one syscall, no
`lsof`/`pwdx` spawn. Each sweep diffs the snapshot and emits appear/vanish
events. Known limitation: wrapper-launched agents (`node codex-linux.js`)
aren't matched by basename — recorded for the phase-2 knowledge port.

## Autostart and lifecycle

tmux-style implicit spawn, implemented in `@ai-devkit/daemon-client`:
`ensureDaemon()` tries the socket; on miss it takes an `O_EXCL` lockfile
(single spawner; locks older than 30s are reclaimed so a crashed starter can't
wedge autostart), spawns `devkitd serve` detached with output appended to
`~/.ai-devkit/daemon.log`, and polls the socket for readiness (≤3s). Socket
existence is liveness — no PID files, so the stale-PID/`kill(pid,0)` hazard
class cannot recur here. `devkitd install` writes a systemd `--user` unit
for boot persistence; the daemon also runs fine with pure implicit spawn.

## Distribution

Rust lives in `rust/` inside the monorepo. npm keeps shipping the JS `ai-devkit`
bin; `@ai-devkit/daemon-client` resolves the daemon binary in order:
`DEVKITD_BIN` env → `@ai-devkit/devkitd-<platform>-<arch>` optional-dep
package → `~/.ai-devkit/bin/devkitd` →
`rust/target/{release,debug}/devkitd` in a dev checkout. Nothing found ⇒
callers degrade to pre-daemon behavior.

Platform packages live as stubs under `npm/devkitd-{darwin,linux}-{arm64,x64}/`
(version `0.0.0` placeholders — stamped from `packages/daemon-client/package.json`
at publish). Linux binaries are musl-static so one build covers glibc and Alpine.
`publish-devkitd.yml` builds a four-target matrix (darwin arm64/x64 on
macos-latest, linux musl x64 native + arm64 via `cross`); `publish-daemon-client.yml`
injects the `optionalDependencies` pins at publish time so dev installs never
resolve the unpublished platform packages. Windows has no daemon (unix-only
sockets) — the resolver returns null and clients fall back.

## The five settled decisions

1. **Absorb vs coexist**: end state is one `ai-devkitd`. Channel bridges become
socket clients now, supervised children in v1.5. pi-session-tracker can't be
absorbed (it runs inside each pi process) — its *store* is absorbed instead:
the extension is a daemon-first writer with JSON fallback. memory-dashboard
keeps its UI but must not hold its own writer connection to the daemon DB.
Never two daemons side by side.
2. **Autostart**: implicit spawn + optional systemd `--user` unit, as above.
3. **Events**: persisted append-only log + at-least-once subscribers. Caveat
kept explicit: this covers "what happened" and replay; it does not subsume
task-manager's *transactional multi-row writes* — that absorption needs a
schema review before it's claimed.
4. **Packaging**: `rust/` workspace + npm wrapper + per-platform binary
optional deps, as above.
5. **Knowledge port**: golden-file corpus extracted from the TS parsers is the
drift-proof contract — captured session files + expected outputs per
harness, run against both implementations, crate-by-crate. The corpus itself
is v1.5+ work; the v1 discipline it enforces (harness-agnostic wire schema)
is already applied.

## Console subscription migration (v1 scope)

`useAgentList` (`cli/src/tui/console/hooks/useAgentList.ts`) currently re-runs
`manager.listAgents` every 3s regardless of whether anything changed. Migrated
design: on mount the hook calls `DaemonClient.tryConnect` / `ensureDaemon`; on
success it `subscribe`s and each `agent.appeared` / `agent.disappeared`
frame triggers an immediate `refresh()`. The interval drops
to a slow fallback (60s) covering attribution drift the daemon can't see
(session-file paths, model, status text — all harness knowledge). On subscribe
failure the hook keeps today's 3s interval — zero behavior change without a
daemon. React specifics: the subscription callback only ever calls the existing
`refresh`, so all existing in-flight/mounted guards still apply; the client is
closed on unmount.

Data flow stays: daemon supplies *invalidation* ("something changed"), the
console still pulls the enriched list from `manager.listAgents`. This is the
honest coordination-only split — daemon events are timing, not content.

## What the daemon does NOT own

Git operations (repo-level locks are the answer there, not a serialization
proxy), releases, skills, agent-side hooks, harness parsing (until the
fixture-contracted port), and any network listener.

## Deviations recorded honestly

- The CLI's `agent list` path was not rewired to the daemon cache at I0:
attribution and session-file knowledge were client-side, so a daemon-backed
fast path needed the knowledge port to pay off. (Post-port the raw
snapshot RPC was removed — the attributed view took over the `agent.list`
name as the single list API; the agents table is an internal diff ledger.)
- Bridge supervision and capacity caching were scoped to v1.5 by explicit
decision; the console's event-driven refresh is in v1 (see above) while
channel-bridge output polling stays client-side (harness parsing).
Loading
Loading