@@ -82,10 +82,9 @@ make build # mix escript.build → bin/shuttle (MIX_ENV=dev)
8282make start # nohup detached; logs → ~/Library/Logs/shuttle.log (macOS)
8383make stop # SIGTERM with 5s grace
8484make restart # build + stop + start (the load-bearing daemon target)
85- make cli # go build → ~/go/bin/shuttle-ctl (load-bearing CLI target)
86- make all # restart + cli (everything)
85+ make all # restart (daemon)
8786make logs # tail -f the log
88- make status # shuttle-ctl ps + snapshot summary
87+ make status # felt shuttle ps + snapshot summary
8988make clean # rm _build and stray Elixir.*.beam at project root
9089
9190make install-agent # durable launchd keep-alive (crash + login restart)
@@ -284,10 +283,10 @@ node via a `bash -lc` login-shell fallback, so it works even though the respawn
284283loop sources asdf but not nvm. A host without node/MySTRA fails ` /astra ` cleanly;
285284the board + fibers are unaffected.
286285
287- ** Two artifacts, two languages, two release cadences .** The Elixir daemon
288- ( ` bin/shuttle ` ) and the Go CLI ( ` ~/go/bin/ shuttle-ctl ` ) are independent —
289- rebuilding one never implies rebuilding the other. Editing ` cmd/shuttle/*.go `
290- needs ` make cli ` ; editing ` lib/shuttle/*.ex ` needs ` make restart ` .
286+ ** One artifact in this repo: the Elixir daemon .** ` bin/shuttle ` is the only
287+ buildable thing here — editing ` lib/ shuttle/*.ex ` needs ` make restart ` . The
288+ agent-facing CLI is ` felt shuttle <verb> ` , owned and shipped by felt (the old
289+ standalone shuttle-ctl Go shim is retired); nothing in this repo builds it .
291290
292291** ` bin/shuttle ` is an escript** — it bundles BEAM bytecode at build time and
293292loads it at boot. A restart without ` make build ` is a no-op for picking up
@@ -313,24 +312,24 @@ should never be committed.
313312bin/shuttle snapshot # JSON snapshot of daemon state
314313bin/shuttle dispatch < fiber-id> # one-shot dispatch
315314
316- # shuttle-ctl — agent-facing CLI; offline; schema-validating
317- shuttle-ctl status # all fibers with shuttle: blocks
318- shuttle-ctl status --all # local + every configured remote
319- shuttle-ctl status --remote < name> # single remote
320- shuttle-ctl ps # live tmux workers only
321- shuttle-ctl install < fiber> --project-dir " $PWD " [-m < agent-id> ] [--disabled]
322- shuttle-ctl repeat < fiber> --schedule " 0 9 * * 1-5" --tz Europe/Paris --project-dir " $PWD "
323- shuttle-ctl pause < fiber> # disable + kill live worker; --no-kill preserves it
324- shuttle-ctl resume / accept < fiber>
325- shuttle-ctl set-model < fiber> < agent-id>
326- shuttle-ctl dispatch < fiber>
327- shuttle-ctl handoff < fiber> # worker's clean-exit ritual: stamp
315+ # felt shuttle — agent-facing CLI; offline; schema-validating
316+ felt shuttle status # all fibers with shuttle: blocks
317+ felt shuttle status --all # local + every configured remote
318+ felt shuttle status --remote < name> # single remote
319+ felt shuttle ps # live tmux workers only
320+ felt shuttle install < fiber> --project-dir " $PWD " [-m < agent-id> ] [--disabled]
321+ felt shuttle repeat < fiber> --schedule " 0 9 * * 1-5" --tz Europe/Paris --project-dir " $PWD "
322+ felt shuttle pause < fiber> # disable + kill live worker; --no-kill preserves it
323+ felt shuttle resume / accept < fiber>
324+ felt shuttle set-model < fiber> < agent-id>
325+ felt shuttle dispatch < fiber>
326+ felt shuttle handoff < fiber> # worker's clean-exit ritual: stamp
328327 # shuttle.handed_off_at (→ next dispatch
329328 # is fresh) + end own tmux session. The
330329 # single final action; folds in kill $PPID.
331- shuttle-ctl snapshot
332- shuttle-ctl abort / attach < fiber>
333- shuttle-ctl validate-identity # UID migration/cross-city validation
330+ felt shuttle snapshot
331+ felt shuttle abort / attach < fiber>
332+ felt shuttle validate-identity # UID migration/cross-city validation
334333```
335334
336335## Critical invariants
@@ -355,27 +354,27 @@ shuttle-ctl validate-identity # UID migration/cross-city validati
355354 mirror", because the read was attempted locally instead of being owner-routed.
356355 New endpoints that surface a fiber's host-local content MUST route through
357356 ` OriginRouter ` , not assume the bytes are reachable on this host.
358- - ** Agent records live in one source of truth: ` share/agents.json ` . ** Both
359- runtimes (Elixir daemon, Go CLI) embed it at compile time — Elixir via
360- ` @external_resource ` + ` File.read! ` in ` lib/ shuttle/agents.ex ` , Go via
361- ` //go:embed ` (generated ` pkg/schema/agents_embedded.go ` ). Edit the JSON,
362- then ` make restart ` . There is no ` config/agents.exs ` .
357+ - ** Agent records live in one source of truth: felt's registry. ** Felt owns
358+ the registry for the merge; the daemon reads the already-resolved record off
359+ felt's ` shuttle.resolved.agent ` JSON and shells `felt shuttle agents
360+ [ resolve ] ` for the registry / no-fiber cases. There is no daemon-embedded
361+ ` share/agents.json ` and no ` config/agents.exs ` .
363362- ** ` shuttle.agent ` field drives agent selection.** The ` shuttle: ` block's
364363 ` agent: ` field resolves against the registry. Default agent is
365364 ` claude-sonnet ` .
366365- ** ` shuttle.host ` field drives daemon affinity — strictly.** A daemon
367366 dispatches a block iff ` block.host == own_host_id ` (its ` SHUTTLE_HOST ` or
368367 ` :inet.gethostname() ` ). There is no ` "local" ` default and no ` nil `
369368 wildcard: an absent or empty ` host: ` is unowned and ineligible on * every*
370- daemon. ` shuttle-ctl install ` /` repeat ` stamp ` host ` by default so blocks
369+ daemon. ` felt shuttle install` /` repeat ` stamp ` host ` by default so blocks
371370 are born owned. The same predicate gates the orphan-resurrection path, so
372371 a remote restart can't re-grab another host's fiber.
373- - ** ` shuttle.project_dir ` is required for enabled installs.** `shuttle-ctl
372+ - ** ` shuttle.project_dir ` is required for enabled installs.** `felt shuttle
374373 install` and ` repeat` require ` --project-dir`; workers start there instead
375374 of falling back to the felt store.
376- - ** shuttle-ctl is the agent-facing CLI.** Local write verbs validate before
375+ - ** felt shuttle is the agent-facing CLI.** Local write verbs validate before
377376 write and work offline. Cross-host writes belong to Portolan's kanban/API
378- surface, not to ` shuttle-ctl ` . ` bin/shuttle ` handles daemon lifecycle and
377+ surface, not to ` felt shuttle` . ` bin/shuttle ` handles daemon lifecycle and
379378 dispatch.
380379- ** No tag predicate for dispatch.** The ` shuttle: ` block's ` enabled: true `
381380 field is the dispatch signal. Tags are free-form qualitative noticings;
@@ -398,7 +397,7 @@ shuttle-ctl validate-identity # UID migration/cross-city validati
398397 Scheduled runs dispatch only when ` next_due_at ` is due AND ` review.state `
399398 is ` scheduled ` or ` accepted ` . Manual dispatch is ad-hoc (` adhoc-... `
400399 run id) and preserves ` next_due_at ` ; worker exit flips state to
401- ` awaiting ` , and ` shuttle-ctl accept ` advances ` next_due_at ` only for
400+ ` awaiting ` , and ` felt shuttle accept` advances ` next_due_at ` only for
402401 scheduled runs.
403402
404403## Dispatch prompt structure
@@ -429,23 +428,23 @@ the git log of the constitution.)
429428## Inspecting state
430429
431430``` bash
432- shuttle-ctl status # Go walker view (independent of daemon)
431+ felt shuttle status # offline walker view (independent of daemon)
433432bin/shuttle snapshot # raw JSON snapshot
434433make status # daemon-side view (ps + snapshot)
435434~ /Library/Logs/shuttle.log # daemon stdout/stderr (macOS)
436435tmux ls | grep ' ^shuttle-' # live workers
437436curl -s http://127.0.0.1:4000/api/v1/agents | jq
438437curl -s http://127.0.0.1:4000/api/v1/state | jq
439438curl -s http://127.0.0.1:4000/api/v1/state/composite | jq
440- shuttle-ctl validate-identity # checks :4000/:4001/:4002 by default
439+ felt shuttle validate-identity # checks :4000/:4001/:4002 by default
441440```
442441
443442Dispatch sanity ladder:
444443
445- 1 . ` shuttle-ctl status ` shows ` enabled: true, idle, oneshot ` ? → fiber is
446- well-formed and the Go walker sees it.
444+ 1 . ` felt shuttle status` shows ` enabled: true, idle, oneshot ` ? → fiber is
445+ well-formed and the offline walker sees it.
4474462 . ` bin/shuttle snapshot ` lists it under ` eligible[] ` ? → daemon dispatched.
448- 3 . shuttle-ctl sees it but daemon doesn't → daemon binary is stale.
447+ 3 . ` felt shuttle` sees it but daemon doesn't → daemon binary is stale.
449448 ` make restart ` .
4504494 . Daemon sees it but agent never appears → check ` share/agents.json ` for
451450 the resolved agent's ` cli ` and that the wrapper is on ` PATH ` .
@@ -480,26 +479,21 @@ shuttle/
480479├── lib/ Elixir source
481480│ ├── shuttle/poller.ex discover + eligibility + retry queue
482481│ ├── shuttle/dispatcher.ex agent resolution, tmux launch
483- │ ├── shuttle/agents.ex agent registry — reads share/agents.json at compile time
484482│ └── shuttle_web/ agent-API HTTP endpoints (/api/v1/...)
485- ├── cmd/shuttle/ Go CLI (shuttle-ctl)
486- ├── pkg/schema/ Go schema package (types, validation, YAML I/O)
487- │ ├── agents.go registry loader (//go:embed agents_embedded.go)
488- │ └── agents_embedded.go generated — embeds share/agents.json bytes
489- ├── share/ shared data (canonical for both runtimes)
490- │ ├── agents.json THE agent registry — single source of truth
491- │ └── schema.json shuttle: block frontmatter schema
483+ ├── share/ shared data (plist template, launchd assets)
492484├── config/ Elixir env config (dev/test/prod endpoint settings)
493485├── test/ Mix test suite
494486└── deps/, _build/ Mix-managed; gitignored
495487```
496488
489+ The agent-facing CLI (` felt shuttle <verb> ` ) and the agent registry are owned by
490+ felt, not built here — this repo is pure Elixir + TS.
491+
497492## Tests
498493
499494``` bash
500- mix test # full Elixir suite (110 tests, ~7s)
495+ mix test # full Elixir suite
501496mix test --only focus # tagged subset
502- go test ./pkg/schema/... # Go schema tests
503497
504498# Opt-in real harness smoke. Opens real Claude/Codex/Pi CLIs in tmux,
505499# sends no prompt, captures the idle pane, then kills the smoke sessions.
0 commit comments