Commit Graph
64 Commits
Author SHA1 Message Date
logaritmiskandClaude Opus 5 ecff75d514 fix(daemon): log fatal startup errors to daemon.log
A daemon that fails during startup reported the reason via eprintln! in
main's error arm. Under launchd stderr is discarded, so the failure was
invisible: daemon.log was created and left empty — exactly the case the
log file was added for.

Found by the manual acceptance run: the launchd agent loaded correctly
(RunAtLoad fired, runs=9) but every spawn exited 1 because a previously
started daemon held the pidfile, and nothing recorded why.

Fatal errors on the daemon path now go through tracing::error!, reaching
both the log file and stderr. Other subcommands keep eprintln!, since
their stderr is the user's terminal.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EGntTHCW3sEPy1VBRopNNp
2026-08-01 09:38:04 +02:00
logaritmiskandClaude Opus 5 7bb80803fe fix(service): strip Disabled before load, verify it, and honour the plist
service-manager 0.11 writes Disabled: true into every plist carrying
KeepAlive, and our RestartPolicy::Always guarantees KeepAlive, so every
agent we installed was born disabled. launchctl load honours the key while
still exiting 0, so install reported success on an agent that would never
start — not then and not at the next login.

service::start now removes the Disabled key itself before loading, via a
pure enable_plist() that rewrites nothing when the key is absent and
preserves every other key, including the EnvironmentVariables PATH
snapshot. That makes start self-healing for plists left disabled by an
earlier build. The crate's own start() is still not used, since without
Disabled it degrades to launchctl start, which fails on an unloaded job.

Because launchctl load exits 0 on failure, start also checks a
post-condition: it asks launchd whether the job now exists and reports a
diagnostic if it does not. stop keeps no such check, since a benign unload
of an already-stopped job also prints a failure while exiting 0.

status now treats the plist on disk as the definition of installed, as the
spec says: a plist that exists but is not loaded reports stopped with its
program, PATH and snapshot date intact instead of collapsing to
not-installed with every field cleared. That is precisely the state the
Disabled bug left users in, so it is the state status most needs to
describe.

status also gains the log path the spec always listed, and the daemon's own
log is renamed xy.log -> daemon.log so a supervised server named xy cannot
share a file, and two rotation counters, with the daemon.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EGntTHCW3sEPy1VBRopNNp
2026-08-01 00:27:34 +02:00
logaritmiskandClaude Opus 5 fd842289e3 docs: correct the false Arc<Mutex> MakeWriter claim
Both the spec and the plan asserted that Arc<Mutex<RotatingLogWriter>>
satisfies tracing-subscriber's MakeWriter via impl MakeWriter for Arc<W>.
That impl requires &'a W: io::Write, and &Mutex<W> does not implement
io::Write. The error reached the implementer and cost a fix round before
being caught; the shipped code uses a bare Mutex.

Corrections are marked inline so the mistake stays visible rather than
being silently erased.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EGntTHCW3sEPy1VBRopNNp
2026-08-01 00:13:54 +02:00
logaritmiskandClaude Opus 5 41acc3e21a fix(service): unescape plist XML, surface both launchctl streams, clear status fields when not installed
Three review findings on task 4:
- read_plist_fields now parses the plist properly via the plist crate
  (already a transitive dep of service-manager, promoted to direct) instead
  of slicing raw XML, so a PATH or program path containing & or < no longer
  round-trips as literal &amp;/&lt; through cli::service::render_status.
- launchctl's error path now includes both stdout and stderr, trimmed and
  joined only on non-empty parts, so a failure never surfaces as
  "launchctl load failed: " with nothing after the colon.
- status() now returns identical None fields (program, path_env,
  snapshotted, pid) whether the plist is absent or the plist exists but the
  crate reports NotInstalled (e.g. written but never loaded, or booted out
  of band) - extracted via a shared not_installed_status/build_status split
  so both paths run the same code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EGntTHCW3sEPy1VBRopNNp
2026-08-01 00:09:59 +02:00
logaritmisk 1d73057444 docs(readme): document xy service verbs 2026-07-31 23:41:17 +02:00
logaritmisk f48221b09c feat(cli): xy service install/uninstall/start/stop/status 2026-07-31 23:39:32 +02:00
logaritmisk 999134657c feat(service): agent status plus launchctl load/unload 2026-07-31 23:35:36 +02:00
logaritmisk f267efc967 feat(service): AgentSpec and launchd install/uninstall 2026-07-31 23:32:46 +02:00
logaritmisk ae37060160 fix(daemon): drop unneeded Arc from daemon_writer's Mutex return type 2026-07-31 23:28:53 +02:00
logaritmisk b69426f4df feat(daemon): write rotating xy.log alongside stderr 2026-07-31 22:57:30 +02:00
logaritmisk f70092f1a0 feat(logs): impl io::Write for RotatingLogWriter 2026-07-31 22:49:36 +02:00
logaritmiskandClaude Opus 5 2ab74c992b docs(plan): drop the launchd integration test
Task 6 is documentation-only. A real launchd cycle test would either
hard-code the live label and risk clobbering a working installation, or
need a test-only --label flag; the manual acceptance section covers the
mechanism instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EGntTHCW3sEPy1VBRopNNp
2026-07-31 22:48:12 +02:00
logaritmiskandClaude Opus 5 f74eb1e865 docs(spec): drop the unnecessary MakeWriter newtype
tracing-subscriber already impls MakeWriter for Mutex<W: io::Write> and
Arc<W>, so Arc<Mutex<RotatingLogWriter>> satisfies with_writer directly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EGntTHCW3sEPy1VBRopNNp
2026-07-31 22:24:51 +02:00
logaritmiskandClaude Opus 5 dff798a9be docs(plan): implementation plan for start-on-login
Six TDD tasks: io::Write for RotatingLogWriter, the daemon log file and
main.rs reorder, AgentSpec plus launchd install/uninstall, status and
launchctl load/unload, the CLI verbs, and the ignored end-to-end test
plus README.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EGntTHCW3sEPy1VBRopNNp
2026-07-31 22:21:01 +02:00
logaritmiskandClaude Opus 5 5b088f119c docs(spec): correct launchd behaviour after reading crate source
Reading service-manager-0.11.0/src/launchd.rs invalidated three claims:

- install() always writes Disabled:true alongside KeepAlive, so a freshly
  installed agent does NOT start at login. install must be followed by
  start(), which strips the key and reloads.
- The crate's stop() runs `launchctl stop`, which a KeepAlive service
  survives. start/stop are now implemented as launchctl load/unload on the
  plist path; the crate handles install/uninstall/status only.
- status() returns Stopped(None) unconditionally on macOS, so the reason
  string the spec planned to surface does not exist.

Also corrects the RestartPolicy mapping (OnFailure emits a KeepAlive
dictionary, not a boolean) and records the launchctl-print risk to verify.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EGntTHCW3sEPy1VBRopNNp
2026-07-31 22:15:27 +02:00
logaritmiskandClaude Opus 5 3a434657bc docs(spec): start-on-login via launchd user agent (macOS)
Design for `xy service install|uninstall|start|stop|status`, backed by the
service-manager crate. Covers the launchd environment problem (PATH snapshot
at install time), the daemon's own rotating log file, and the accepted
blind spot for pre-logger launch failures.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EGntTHCW3sEPy1VBRopNNp
2026-07-31 22:12:15 +02:00
logaritmiskandClaude Opus 4.8 40ffd9b06c fix(pidfile): replace stale pidfile after unclean shutdown
PidFile::acquire used create_new(true) with Drop-based cleanup, so a
pidfile surviving power loss or SIGKILL made the daemon refuse to start
until the file was deleted by hand.

On AlreadyExists, read the recorded PID and probe it with kill(pid, 0):
ESRCH (or unparseable content) means stale, so remove the file and
retry the atomic create. A live PID keeps the refusal and now names the
holding process. The retry loop is bounded to stay race-safe against a
concurrent starter.

Closes #1

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 18:16:54 +02:00
logaritmiskandClaude Opus 4.7 00c7e7e812 feat(paths): auto-create config dir on daemon startup
ensure_dirs() now creates config_dir alongside state_dir and log_dir,
so first daemon run materializes $XDG_CONFIG_HOME/xy/servers/ — making
it obvious where to drop server .kdl files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 12:44:47 +02:00
logaritmisk 740b8b4c84 Merge feat/mcp-supervisor: HTTP MCP server supervisor MVP
37 planned tasks plus 3 follow-up fixes from final code review.

Architecture:
- Cargo workspace: xy-protocol, xy-supervisor, xy-ipc, xy (single binary)
- Unix socket + newline-delimited JSON-RPC 2.0
- Per-server KDL configs at XDG paths (XDG on macOS via etcetera)
- One supervisor task per managed server, owning all state
- Per-server log capture: rotating disk + ring buffer + broadcast stream

Features:
- Daemon auto-launches all configured servers on boot
- start/stop/restart (single or --all), reload (diff added/removed/changed),
  list/status, logs (--tail / --follow)
- Per-server restart policy (always/on-failure/never) with exponential
  backoff, sliding 60s retry window, and Failed state on cap
- Graceful shutdown via SIGTERM/SIGINT, SIGKILL escalation after grace
- 51 tests: unit (state machine via MockChild, KDL parser, framing) +
  integration (real daemon + helper bins exercising lifecycle/reload/
  restart-cap/logs)

Bugs found and fixed during execution:
- Connection deadlock from single shared read/write mutex (split into
  separate reader/writer halves)
- LOGS response vs notification ordering race (oneshot gate)
- StartAck::Started returned even on spawn failure (added SpawnFailed)
- Backoff sleep blocked the supervisor's command channel (interruptible
  select)
- list/status returned zeroed fields (now publish full Status via watch)
2026-05-25 13:22:28 +02:00
logaritmiskandClaude Sonnet 4.6 4a0b32d90e fix(supervisor): StartAck::SpawnFailed surfaces real failures
Add StartAck::SpawnFailed(String) so callers can distinguish a successful
start from a failed spawn. The Start command arm now sends SpawnFailed on
io::Error rather than the misleading Started. handlers.rs maps the new
variant to an RpcErrorCode::SpawnFailed JSON-RPC error response.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 12:32:07 +02:00
logaritmiskandClaude Sonnet 4.6 b366df0482 fix(supervisor): make backoff sleep interruptible by Stop/Shutdown
Replace the bare sleep(delay).await in the Restart backoff arm with a
tokio::select! over the timer and cmd_rx. Stop/Shutdown are now handled
immediately during backoff (Stop → Stopped, Shutdown → clean exit);
Start/Restart/Reconfigure skip the remaining delay and retry at once.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 12:31:32 +02:00
logaritmiskandClaude Sonnet 4.6 3e4ad79137 fix(supervisor): publish full status (pid, port, uptime, restart_count, last_exit) via watch channel
Replace watch::Receiver<ServerState> on SupervisorHandle with watch::Receiver<Status>,
a richer snapshot type that carries pid, port, uptime_secs, restart_count and last_exit.
SupervisorTask maintains current_pid and publishes a fresh Status on every state
transition; handlers.rs reads the full Status so list/status no longer return
zeroed/None fields.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 12:30:56 +02:00
logaritmiskandClaude Opus 4.7 ae6ed1cf0a chore: remove stray libnull.rlib and gitignore *.rlib
Accidentally committed in 9d5d8f0 during the polish task.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 12:25:13 +02:00
logaritmisk 0261d58d5d docs: README and example KDL config 2026-05-25 12:19:34 +02:00
logaritmisk 9d5d8f04a2 chore: clippy fixes - allow should_implement_trait and collapse nested if 2026-05-25 12:19:24 +02:00
logaritmisk b1e7dea739 test(xy): logs --tail and --follow
Fix a deadlock in the log-stream handler that caused all logs
requests to hang: Connection used a single Mutex<JsonFramed> for
both reads and writes, so the serve loop holding the read lock
blocked the spawned notification task from writing.  Split
Connection into separate reader and writer mutexes.

Also fix a response/notification ordering race: the log task now
waits for an explicit ready signal sent by serve after writing the
LOGS response, ensuring notifications never arrive at the client
before their initiating response.
2026-05-25 12:17:32 +02:00
logaritmisk 15791c628b test(xy): reload diff 2026-05-25 12:05:58 +02:00
logaritmisk 284b6e7402 test(xy): restart cap escalates to failed 2026-05-25 12:05:45 +02:00
logaritmisk 434828c14e test(xy): auto-start + stop/start lifecycle 2026-05-25 12:05:28 +02:00
logaritmisk 48d63a0549 test(xy): integration test harness 2026-05-25 12:03:38 +02:00
logaritmisk 7107977637 test(xy): helper binaries for integration tests 2026-05-25 12:03:13 +02:00
logaritmiskandClaude Opus 4.7 c1f6225e26 feat(xy): CLI client commands
Replace bail!("not implemented") stubs with real RPC calls over the Unix
socket; add format::list_table for fixed-width list output.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 12:02:09 +02:00
logaritmiskandClaude Sonnet 4.6 b434c636a6 feat(xy): logs streaming via subscription notifications
Implement per-connection ConnState tracking active subscriptions, and the
logs/logs_cancel RPC handlers. Snapshot-only streams terminate with a
log_end notification; follow streams forward broadcast lines until
cancelled or connection close.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 11:59:46 +02:00
logaritmiskandClaude Sonnet 4.6 c679465f12 feat(xy): reload handler with diff
Implements the `reload` JSON-RPC method: diffs the on-disk config dir
against the in-memory registry and reconciles — stops removed servers,
restarts changed servers (shutdown-then-respawn), and starts new ones.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 11:56:45 +02:00
logaritmiskandClaude Sonnet 4.6 736e6d1854 feat(xy): RPC handlers for list/status/start/stop/restart
Per-connection JSON-RPC dispatch in daemon/handlers.rs — list, status,
start, stop, and restart are fully implemented; reload, logs, and
logs_cancel are stubbed with -32601 for later tasks.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 11:54:52 +02:00
logaritmiskandClaude Sonnet 4.6 3ab982aea1 feat(xy): daemon boot + accept loop + graceful shutdown
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 11:52:41 +02:00
logaritmisk d7aa543ac0 feat(xy): daemon Registry with config-hash entries 2026-05-25 11:49:58 +02:00
logaritmisk 71808783c4 feat(xy): clap CLI scaffold 2026-05-25 11:49:47 +02:00
logaritmisk 49c006df10 feat(xy): exclusive pidfile guard 2026-05-25 11:48:38 +02:00
logaritmisk 58c44e0b48 feat(xy): XDG path resolution 2026-05-25 11:48:36 +02:00
logaritmisk b137f85a0c feat(ipc): server bind + Connection wrapper 2026-05-25 11:47:24 +02:00
logaritmisk fbfb1db427 feat(ipc): client with call + notification reader 2026-05-25 11:47:11 +02:00
logaritmisk e58b6866ef feat(ipc): newline-delimited JSON framing 2026-05-25 11:45:50 +02:00
logaritmisk 53f6b82f2b feat(ipc): JSON-RPC envelope types 2026-05-25 11:45:37 +02:00
logaritmiskandClaude Sonnet 4.6 a3c979511e feat(supervisor): supervisor task with state machine
One async task per managed server owns all state transitions via a
tokio::select! loop over cmd_rx and wait_child. Includes RealSpawner
and a smoke test covering the Start → Running → exit → Stopped →
Shutdown happy path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 11:44:12 +02:00
logaritmiskandClaude Sonnet 4.6 f1b2306156 feat(supervisor): RealChild + spawn_with_logs
Append RealChild (real tokio::process::Child wrapper) and spawn_with_logs
to child.rs. Uses nix::unistd::setpgid via tokio's re-exported pre_exec
to create an own process group, and fires per-stream log pump tasks that
drain stdout/stderr into the provided LogSink. terminate/kill signal the
whole process group via kill(-pgid, SIG*).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 11:40:19 +02:00
logaritmisk e121fe28bb feat(supervisor): LogSink fans out to file, ring buffer, broadcast 2026-05-25 11:37:19 +02:00
logaritmisk 7995a53e82 feat(supervisor): ring buffer for recent log lines 2026-05-25 11:36:52 +02:00
logaritmisk d51f25350c feat(supervisor): rotating log writer 2026-05-25 11:36:23 +02:00
logaritmisk d237e980e9 feat(supervisor): sliding retry-window tracker 2026-05-25 11:34:55 +02:00
logaritmisk 54045da2df feat(supervisor): exponential backoff calculator 2026-05-25 11:34:53 +02:00
logaritmisk 4837a73167 feat(supervisor): restart-policy decision logic 2026-05-25 11:34:50 +02:00
logaritmisk 1d2848f03a feat(supervisor): ChildHandle trait + MockChild 2026-05-25 11:33:23 +02:00
logaritmisk bd926061bf feat(protocol): JSON-RPC method param/result types 2026-05-25 11:31:56 +02:00
logaritmisk e8f5846cec feat(protocol): load_all_configs from dir with duplicate port detection 2026-05-25 11:30:38 +02:00
logaritmisk 7e59d7d050 feat(protocol): KDL parser for ServerConfig
Adds kdl_parse module with parse_server_config() that deserialises a
KDL document into ServerConfig, with full validation of name, types,
durations, and restart/stop blocks. Also derives Default on
RestartPolicy to satisfy clippy.
2026-05-25 11:29:05 +02:00
logaritmisk 355d0debda feat(protocol): ServerConfig + ConfigError + RpcErrorCode 2026-05-25 11:23:57 +02:00
logaritmisk 5a0963665d feat(protocol): RestartPolicy/RestartConfig/StopConfig with defaults 2026-05-25 11:22:52 +02:00
logaritmisk 0e49834c93 feat(protocol): ServerState enum 2026-05-25 11:21:43 +02:00
logaritmiskandClaude Opus 4.7 1b76378b37 chore: bump workspace resolver to "3"
cargo 1.95 supports resolver 3; align with plan spec.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 11:20:54 +02:00
logaritmisk 5b1314b0af chore: convert to cargo workspace with four crates 2026-05-25 11:17:24 +02:00
logaritmiskandClaude Opus 4.7 c252bd7716 docs: add xy MCP supervisor implementation plan
37-task TDD-style plan across 7 phases: workspace skeleton,
xy-protocol (config/state/rpc types), xy-supervisor (state machine
with mock-driven unit tests), xy-ipc (JSON-RPC over Unix socket),
xy binary (daemon + CLI), integration tests with test-helper bins,
and polish (fmt/clippy/README).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 11:13:56 +02:00
logaritmiskandClaude Opus 4.7 8f7200aa25 docs: add xy MCP supervisor design spec
Approved design for the MVP: single xy binary with a Cargo workspace
(xy-protocol, xy-supervisor, xy-ipc, xy), Unix socket + newline-delimited
JSON-RPC, per-server KDL configs at XDG paths (XDG on macOS too via
etcetera), supervisor-per-server task model with per-server restart policy,
log capture to disk + ring buffer + broadcast for follow.

MVP commands: daemon, list, status, start/stop/restart (name|--all),
reload, logs. Process-alive supervision only; HTTP/MCP-aware probes,
container isolation, launchd integration, and TUI deferred.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 10:51:57 +02:00
logaritmiskandClaude Opus 4.7 cd2746cc3d chore: initial cargo skeleton
cargo new output as the baseline for the xy MCP supervisor project.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 10:51:49 +02:00