afk-mode: autonomous agents & ops surface #29

Merged
erik merged 25 commits from feat/afk-mode into main 2026-06-21 18:23:47 +00:00
Owner

Lands the full afk-mode change: agents can run unattended on adaptive cadences with a supervisor daemon, telemetry that survives crashes, per-language container images, and a read-only mobile-first ops dashboard backed by the on-disk telemetry. Spec deltas are folded into openspec/specs/ and the change is archived at openspec/changes/archive/2026-06-20-afk-mode/.

What's new

Supervisor (fjx supervise) — long-running daemon that drives pm/dev/qa ticks across one or more project dirs on adaptive cadences via per-cycle Docker containers. Strict serial execution (one tick in flight, ever), unique per-tick container names, crash recovery, SIGTERM-drains-then-exits, SIGHUP/supervise reload re-reads each project's fjx.json. Control socket exposes status, pause, resume, kill, reload.

Per-project config (fjx.json) — JSON file at each project root declaring images.<role> overrides. Forward-compatible: unknown fields preserved.

Per-language agent imagesfjx-base plus language-specific variants, all multi-stage, run as the host user so writes into the mounted repo land with the right ownership; full Claude state is mounted in.

Mgmt service (fjx-mgmt) — Fresh app + MCP endpoint serving cross-project runs, run-detail with stats and avatars, issue browser, dashboards over the per-project telemetry. Read-only at MVP. Auth deferred — deploy on a Tailscale tailnet or VPN-only host. Image built and published by the existing images.yaml workflow.

Telemetry — JSONL shards per project, optional SQLite index, role/event taxonomy stable enough to drive the mgmt UI. Backfill script for older messages.

Workflow polishpm next (renamed from pm tick), four-guard QA handoff trigger, surfaces comments + context when the planner holds an issue, codex one-shot driver for the agent container.

Docs

  • doc/supervise-setup.md — systemd / launchd / tmux recipes for keeping fjx supervise alive, control-socket notes.
  • doc/fjx-json.md — schema reference for the per-project config.
  • doc/mgmt-deploy.mdfjx-mgmt deployment recipe (env vars, volume mounts, healthcheck, auth-deferral note).
  • doc/cli-reference.md — regenerated for supervise, telemetry *, <role> release.
  • doc/roadmap.md — autonomous-agents and mobile-UI items marked in flight with back-links.
  • doc/setup.md — links to supervisor setup and fjx.json schema from the per-repo install step.

Test plan

  • just validate clean on the branch
  • fjx supervise start <dir> boots, schedules a pm tick, drains cleanly on SIGTERM
  • fjx supervise reload re-reads a modified fjx.json without restart
  • End-to-end pm → dev → qa cycle under the supervisor produces telemetry shards + SQLite rows
  • fjx-mgmt container with a read-only mount of a projects dir renders dashboard, runs list, run detail, issue view
  • MCP endpoint responds to fjx-recent-activity, fjx-failed-runs, fjx-token-usage, fjx-stuck-work
  • Operator setup doc: at least one of systemd / launchd / tmux verified by hand on the host that will run it
Lands the full afk-mode change: agents can run unattended on adaptive cadences with a supervisor daemon, telemetry that survives crashes, per-language container images, and a read-only mobile-first ops dashboard backed by the on-disk telemetry. Spec deltas are folded into `openspec/specs/` and the change is archived at `openspec/changes/archive/2026-06-20-afk-mode/`. ## What's new **Supervisor** (`fjx supervise`) — long-running daemon that drives `pm`/`dev`/`qa` ticks across one or more project dirs on adaptive cadences via per-cycle Docker containers. Strict serial execution (one tick in flight, ever), unique per-tick container names, crash recovery, SIGTERM-drains-then-exits, SIGHUP/`supervise reload` re-reads each project's `fjx.json`. Control socket exposes `status`, `pause`, `resume`, `kill`, `reload`. **Per-project config** (`fjx.json`) — JSON file at each project root declaring `images.<role>` overrides. Forward-compatible: unknown fields preserved. **Per-language agent images** — `fjx-base` plus language-specific variants, all multi-stage, run as the host user so writes into the mounted repo land with the right ownership; full Claude state is mounted in. **Mgmt service** (`fjx-mgmt`) — Fresh app + MCP endpoint serving cross-project runs, run-detail with stats and avatars, issue browser, dashboards over the per-project telemetry. Read-only at MVP. Auth deferred — deploy on a Tailscale tailnet or VPN-only host. Image built and published by the existing `images.yaml` workflow. **Telemetry** — JSONL shards per project, optional SQLite index, role/event taxonomy stable enough to drive the mgmt UI. Backfill script for older messages. **Workflow polish** — `pm next` (renamed from `pm tick`), four-guard QA handoff trigger, surfaces comments + context when the planner holds an issue, codex one-shot driver for the agent container. ## Docs - `doc/supervise-setup.md` — systemd / launchd / tmux recipes for keeping `fjx supervise` alive, control-socket notes. - `doc/fjx-json.md` — schema reference for the per-project config. - `doc/mgmt-deploy.md` — `fjx-mgmt` deployment recipe (env vars, volume mounts, healthcheck, auth-deferral note). - `doc/cli-reference.md` — regenerated for `supervise`, `telemetry *`, `<role> release`. - `doc/roadmap.md` — autonomous-agents and mobile-UI items marked *in flight* with back-links. - `doc/setup.md` — links to supervisor setup and `fjx.json` schema from the per-repo install step. ## Test plan - [ ] `just validate` clean on the branch - [ ] `fjx supervise start <dir>` boots, schedules a `pm` tick, drains cleanly on SIGTERM - [ ] `fjx supervise reload` re-reads a modified `fjx.json` without restart - [ ] End-to-end pm → dev → qa cycle under the supervisor produces telemetry shards + SQLite rows - [ ] `fjx-mgmt` container with a read-only mount of a projects dir renders dashboard, runs list, run detail, issue view - [ ] MCP endpoint responds to `fjx-recent-activity`, `fjx-failed-runs`, `fjx-token-usage`, `fjx-stuck-work` - [ ] Operator setup doc: at least one of systemd / launchd / tmux verified by hand on the host that will run it
erik added 20 commits 2026-06-19 21:17:38 +00:00
Lands the foundation for unattended agent operation: a per-project fjx.json image map, a direct-write JSONL telemetry pipeline with concurrent-safe appends, a derived SQLite index plus rebuild + compaction verbs, lifecycle events on every cycle verb, and crash-recovery release verbs that escalate to $FJX_OWNER instead of silently dropping state.

Scope:

- §1 Project config: `fjx.json` loader (forward-compatible, unknown fields preserved), `fjx.sqlite` gitignored.
- §2 Telemetry ingest: `src/telemetry.ts` direct-write appender (single-write under PIPE_BUF, `Deno.FsFile.lock()` above), `fjx telemetry emit` stdin filter (events → shard, everything else → stdout), `startLifecycle` helper wired into `pm tick` / `dev next` / `qa next`.
- §3 Telemetry index + compaction: `@db/sqlite`-backed `events`/`runs`/`llm_calls`/`tool_calls` schema, `fjx telemetry rebuild`, `peekShardRange` head/tail peek for time-range queries, `fjx telemetry compact` folding per-branch shards into year-month archives.
- §4 Role release: `fjx <role> release <id>` for pm/dev/qa — adds `agent/blocked`, reassigns to `$FJX_OWNER`, appends a crashed ledger entry, clears the per-role worktree cache pointer. Idempotent.

Also introduces a shared `src/cmd.ts` `runCmd` wrapper to eliminate four duplicated `Deno.Command` boilerplates across `config.ts`, `git.ts`, `telemetry.ts`, and `commands/pm.ts`.

Spec updates inline with the implementation: `emit` is now spec'd as a stdin filter (was: argument-based), the `release` verb escalates rather than dropping state (was: drop label + clear assignee), and the qa role replaces the placeholder `test` key in the project-config image map. Tasks.md tracks the cross-references.

164 tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
§5 lands `fjx supervise <projects-dir>`: discovers projects, per-role adaptive cadence (base + doubling-on-noop + ceiling + reset-on-work), one concurrent tick per (project, role), per-cycle Docker spawn with worktree + ~/.claude + ~/.codex bind-mounts, tick.spawned/skipped/timed-out events emitted directly, --no-spawn idle mode.

§5b lands the runtime adapter layer: generic AdapterRunner with per-file offset tracking and warn-once degradation; Claude translator (assistant.usage → llm.call, tool_use/tool_result → tool.call.*); Codex translator (response_item.function_call/output → tool.call.*, event_msg.token_count → llm.call); DefaultProjectResolver with container-correlation (bindActive for -work sessions) and reverse-encoding (registerProject for manual runs). Fixture-based tests per adapter.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Failure paths (timeout, spawn error, non-zero non-3 exit) now emit run.failed and invoke `fjx <role> release <issue>` via a pluggable ReleaseFn (default reads `.fj-cache-<role>.json` from the project worktree and spawns the release verb with forwarded role credentials). Crash-recovery outcome is logged as a `release.invoked` telemetry event.

Adds a Unix-socket control surface (src/supervise-control.ts) with newline-delimited JSON request/response. New CLI subverbs `fjx supervise status|pause|resume|kill|reload` connect to the daemon socket. SIGHUP triggers `control.reload()`, which re-discovers projects and re-loads each `fjx.json` mid-flight without restarting; SIGTERM continues to drain in-flight ticks.

15 new tests cover crash → release on timeout/exit-N, no-release on no-op (code 3), pause/resume scheduling, kill aborting an in-flight tick, reload picking up new projects, control-dispatch unit cases, and socket roundtrip (including malformed JSON and stale-socket replacement).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Ships the fjx-base Docker image (denoland/deno:2.8.1 + git + ripgrep + fd + jq + ast-grep + gitleaks + trivy + claude + codex + fjx), each third-party CLI pulled as a native binary release in its own layer so `docker history` attributes size accurately. ~997 MB total; claude (240 MB) and codex (188 MB) dominate and will go away once the native FJX agent lands.

`.forgejo/workflows/images.yaml` builds, smoke-tests, and pushes the image to the Forgejo registry on `v*` tags. `images/fjx-base/smoke.sh` (also wired via `just images::smoke`) checks every bundled tool boots and `fjx pm/dev/qa --help` resolves.

Tasks 7.1–7.3 closed; 7.4 (full dev tick against a fixture project) deferred to §14 dogfooding because a real tick needs a live Forgejo.

Also lifts the alternatives-considered narrative from the (untracked) doc/plans plan file into wiki/ard/afk.md as a durable ARD and retargets design.md at it. Drops dangling AFK.md references from the openspec docs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Ships fjx-rust, fjx-go, fjx-python — each `FROM fjx-base` with the per-language toolchain pinned by ENV and installed in dedicated layers so `docker history` stays informative.

- fjx-rust  (~1.57 GB total): rustup-managed stable toolchain with the minimal profile + clippy + rustfmt
- fjx-go    (~1.26 GB total): Go 1.26.3 official tarball + golangci-lint 2.12.2 release binary
- fjx-python (~1.62 GB total): uv-installed Python 3.12 + ruff + pyright (libatomic1 added because pyright's bundled Node needs it on arm64)

CI: `.forgejo/workflows/images.yaml` extended with a `fjx-lang` matrix job (rust/go/python) that `needs: fjx-base`, relying on the same-runner docker daemon to provide the locally-tagged `fjx-base` parent.

Per-image `smoke.sh` checks the language `--version` invocations on top of the base toolbelt; same harness as fjx-base.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Customization needs are stack-scoped, not project-scoped — the shipped `fjx-<lang>` family already covers them. Projects whose needs the family doesn't cover (private toolchains, native deps) build and tag their own image out-of-band (typically via their own CI) and reference that tag in `fjx.json.images.<role>`. The supervisor stays a pure `docker run` consumer with no project-side build path.

What changes:

- tasks.md: §9 (9.1–9.4) marked DROPPED with the stack-vs-project rationale inline.
- specs/agent-images/spec.md: the "Project-side custom extension" and "Image rebuilds are cached by Dockerfile hash" requirements are replaced with a single "Customization is stack-scoped, not project-scoped" requirement plus two scenarios.
- proposal.md, design.md, wiki/ard/afk.md: updated descriptions; the rejected alternative is recorded alongside the other "alternatives considered and rejected" entries so it doesn't get re-relitigated.

The dropped feature would have added a `docker build` code path to the long-running supervisor (hung builds, disk fill, dangling tags) for a use case CI already handles well.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
§10 (backend): Fresh 2 scaffold under top-level mgmt/ with env-driven FJX_PROJECTS_DIR; per-project SQLite reads with on-the-fly JSONL fallback so the operator surface stays honest even when fjx telemetry rebuild hasn't been run; pure marker extractors for the pm:brief and agent-role:ledger HTML-comment markers over the existing ForgejoClient; cross-project discovery from FJX_PROJECTS_DIR. 23 unit tests cover both fallback and indexed reads asserting identical projections, plus marker extraction and config validation.

§11 (UI): Tailwind via @fresh/plugin-tailwind + dark-themed Shoelace via CDN. Dashboard rolls active runs, recent failures, stuck runs (telemetry proxy until per-project Forgejo creds land), and tokens-today across discovered projects. Runs list is SSR-only with status/role/since filters; run detail page renders the event timeline and mounts the one justified island — a live-tail that polls an SSE endpoint while the run is still running. Issue view groups briefs and per-role ledgers separately from the other-comments bucket. Four mobile-viewport smoke tests assert width=device-width, the max-width container, no wide fixed-pixel widths, and that the runs list ships no island markers.

Tooling: mgmt.just module (just mgmt::{check,test,dev,build}), scoped root just test::units to src/ + scripts/ so the Fresh tree doesn't leak into the CLI's type check, gitignored mgmt/_fresh/ build artifacts.

Decision capture: wiki/ard/afk.md records why per-project SQLite stays the right primary — fan-out across projects is cheap at this scale and the rebuildable-from-durable-raw invariant lives naturally inside one project; the supervisor is a writer and has no use for a SQLite at all.

Deferred from §11 to follow-ups: stuck-work via canonical agent/working Forgejo label, CI-health panel, mgmt-service auth shape beyond Tailscale + FJX_TOKEN.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds the MCP JSON-RPC surface on `/mcp` (initialize + tools/list + the §12.2 tool quartet) and the production container that ships it: multi-stage Dockerfile, smoke test covering dashboard + MCP handshake, and a deploy guide. Also folds the standalone `mgmt.just` and `images.just` into per-tree justfiles for consistency.

Image build required a few corrections to make Fresh 2 + Tailwind v4 work end-to-end in Deno: switch `static/styles.css` to v4 syntax (`@import "tailwindcss"` + `@theme`), pin tailwindcss/postcss to v4 in `deno.json` and enable `nodeModulesDir: "auto"` so `@tailwindcss/postcss` can resolve the bare `tailwindcss` import during build, and run the prebuilt `_fresh/server.js` via `deno serve` so the production build cache is actually wired up — running `main.ts` directly bypassed `setBuildCache` and returned 404 on every route.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Consolidates the PM cycle verb under `fjx pm next` (the prior `pm next` issue selector is removed; `pm next` now runs one full sweep→plan→apply cycle and exits 3 on an empty sweep). Touch-ups across docs/specs reflect the rename.

Also: supervise CLI flags switch from milliseconds to seconds for ergonomics; label-fetch failures now log the failing endpoint instead of being silently swallowed; spawn stderr is captured and tail-truncated into run.failed telemetry so silent container exits are debuggable.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The PM planner used to auto-route any issue with `agent/review` + no QA brief straight to `agent-qa`. But `agent/review` is also added the moment PM assigns work to `agent-dev`, so the trigger fired in scenarios where dev hadn't actually picked up the work — and silently overwrote unread owner prose comments in the process.

The QA-handoff trigger now requires all four of: (1) a PR is linked to the issue (open or merged), (2) the issue is assigned to `agent-pm` (the canonical `fjx dev done` signal), (3) the `agent/review` label is present, and (4) zero new comments this tick. Any failing guard suppresses the trigger and surfaces a `qa handoff held: …` diagnostic so the PM LLM picks up the issue and applies judgment per `src/prompts/pm-bootstrap.md`.

To support the new guards, `PlannerInput.issue` now carries `assignee`, `PlannerInput` carries `hasLinkedPr` (derived from open + merged PRs referencing the issue), and `slimIssue` exposes `assignees`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The four-guard QA handoff trigger emits a `qa handoff held: …` diagnostic when it suppresses an auto-route, but the sweep summary contained only `home`, `plan`, and `applied` — the LLM PM had nothing to read. So it saw "held: 2 new comment(s)" and did nothing.

When `plan.diagnostics` is non-empty, the summary entry now also carries `title`, `assignee`, `labels`, and `new_comments` so the PM LLM can interpret owner intent in one pass. The bootstrap prompt gains a short pointer to diagnostics-driven entries so the LLM treats them as work, not noise.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Per-tick spawn now runs `sh -c 'claude --dangerously-skip-permissions "$(fjx <role> prompt bootstrap)\nThen run: fjx <role> next"'` inside the container instead of `fjx <role> next` directly. The LLM agent reads the planner's JSON output — including held-handoff diagnostics and surfaced `new_comments` — and applies judgment per the bootstrap prompt. The mounted `~/.claude` volume already gave claude the auth it needs.

Known tradeoff: the cycle verb's `exit 3 = no-op` signal is lost once claude is the entrypoint, so supervise's adaptive backoff treats every clean run as `worked`. Revisit once a proper runtime adapter exists.

`fjx-base` already ships claude v2.1.152 on PATH and all per-language images inherit from it; updated the Dockerfile CMD comment so the documented override matches what supervise actually does.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
`--name fjx-${role}` collided across concurrent projects and on retry when a previous container hadn't `--rm`'d cleanly, causing `exit 125 — Conflict. The container name "/fjx-pm" is already in use`. Names now include the project name and the tick's runId suffix, so each tick gets a unique container while still being readable in `docker ps`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Previously `state.inFlight` only blocked overlapping ticks for the same `(project, role)` pair, so N projects × M roles could fan out to N×M concurrent claude containers. That is how `dropbear`'s `pm` tick collided with another project's `pm` tick on the `fjx-pm` container name — and on a real server it would have meant N×M parallel agents at once.

The scheduler now skips any candidate when `state.inFlight.size > 0`, so at most one tick runs across the entire supervisor at any time. The skip reason distinguishes `already-running` (same key) from `another-tick-running` (different key) for telemetry.

Updated the "spawns one tick per role on first pass" test to assert strict-serial (only one role fires per pass) and added a cross-project test that confirms a second project's tick waits while the first is in flight.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Telemetry shards are still the source of truth, but operators tailing `fjx supervise start` shouldn't need `jq` to see what's happening. Each tick now logs a short one-liner on start, completion, skip, and release:

```
14:23:01  start   pm/dropbear  run=0950a89f img=fjx-pm
14:23:14  done    pm/dropbear  worked  took=13.2s
14:23:14  skip    pm/alpha     another-tick-running
14:23:15  start   pm/alpha     run=abc12345 img=fjx-pm
14:23:18  done    pm/alpha     no-op   took=2.8s
```

Also logs project reloads (SIGHUP) with the new project count.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two failures from yesterday's claude entrypoint:

1. claude needs `~/.claude.json` in addition to `~/.claude/`, but only the directory was bind-mounted.
2. claude refuses `--dangerously-skip-permissions` when uid=0, and the container was running as root by default.

Spawn now passes `--user <host-uid>:<host-gid>` so claude's anti-root guard is satisfied and bind-mounted host files stay writable. Both `~/.claude` and `~/.claude.json` (plus `~/.codex`) are mounted under a synthetic `/agent-home`, and `HOME=/agent-home` is exported so the agent CLIs find them without needing a /etc/passwd entry for the dynamic uid.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Switches the AFK agent runtime from interactive `claude` to one-shot `codex exec`. The container itself is the sandbox boundary, so codex runs with `--dangerously-bypass-approvals-and-sandbox` (skips its bubblewrap layer) + `--skip-git-repo-check`. Auth flows through the existing `~/.codex` bind mount.

Also in this commit, because debugging the rollout surfaced them:

- Role bootstrap prompts (pm/dev/qa) now lead with an "unattended — stop and report, never wait for Erik" line so codex knows to record a blocker rather than ask a question.
- `dockerSpawnFn` dumps the exact `docker run` invocation plus the container's full stdout/stderr to `<project>/telemetry/<container>.{cmd,out,err}` after each tick, so a misbehaving tick is replayable by hand.
- `discoverProjects` resolves `project.path` to an absolute path. Relative paths in `-v <name>:/work` are interpreted by docker as named volumes, not bind mounts — which silently mounted an empty volume and made `/work` look like a non-git directory to the agent.
- Dockerfile CMD comment updated to reflect the new codex invocation.

Adaptive backoff still loses the cycle verb's `code 3 = no-op` signal under the codex wrapper, same caveat as under claude — already documented inline.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- supervise: take an explicit list of project dirs instead of a parent dir; discoverProjects errors on missing dirs, missing fjx.json, or duplicate basenames.
- pm: emit SUPERVISOR_BACKOFF in the tick JSON when nothing real happened (mechanical ledger updates don't count) — needed because the codex wrapper erases dev/qa's exit-3 signal.
- dev/qa: tag "no matching issues" stderr with SUPERVISOR_BACKOFF for the same reason.
- worktree path: move from sibling \`../worktrees/<repo>-<N>\` to in-repo \`.git/fjx/<N>\` so codex's sandbox sees them.
- ledger: accept \`--body-file -\` (stdin) and steer dev-bootstrap toward heredoc usage; writing the body to a tracked file leaves a pending sandbox diff that re-streams every tool call.
- adapter-codex: add sumCodexRunTokens — walk ~/.codex/sessions, pick the last token_count per session file in window, return cumulative totals.
- fjx-base image: add build-essential (cgo for \`go test -race\`) and just (project recipes); fjx-go image: export Go on /etc/profile.d so \`bash -lc\` (codex's shell) keeps it on PATH.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds /projects/<name> overview (active/failed/recent runs + link to issues), /projects/<name>/issues index (state + assignee filters; assignee includes agent-pm/dev/qa and FJX_OWNER), and revamps the issue detail view: sl-breadcrumb nav, sl-details for description / per-role brief & ledger (briefs/ledgers grouped by role, closed by default, content stripped through the Brief/Ledger heading line), markdown-rendered via @deno/gfm, assignee avatar left of titles, colored label pills using Forgejo's cached hex with luminance-picked foreground, and sl-icon state indicator on issue rows. mgmt code speaks fjx-namespaced types (FjxLabel/FjxIssue/...) re-exported from lib/fjx-types.ts so the UI surface doesn't leak the underlying provider. FJX_OWNER plumbed through MgmtConfig (default "erik" to match src/config.ts).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
feat: afk-mode §13–§14 — run-detail stats, avatar support, telemetry fixes
Some checks failed
Validate / validate (pull_request) Failing after 22s
827ec57e21
mgmt UI:
- Run detail page: exec-command timeline with token attribution, llm.message
  interleaving, per-command Top-N breakdown (run-stats.ts, event-summary.ts)
- Agent avatars on run list rows via Forgejo user lookup (avatar.tsx, run-row.tsx)
- Project drill-down run list: shortId label, role badge
- Live tail, issues browser, token usage windows

Telemetry / adapter:
- lifecycle.ts reads FJX_RUN_ID env so run.started/completed share the
  supervise-assigned run ID with adapter-captured llm.call/tool.call events
- Codex adapter: llm.message + llm.reasoning event types, improved session
  attribution, fixture-based tests
- telemetry-db: schema versioning, llm_calls/tool_calls projection tables
- backfill-messages.ts: one-shot script to enrich historical shards from
  codex session JSONL

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
docs: afk-mode operator docs + archive change
Some checks failed
Validate / validate (pull_request) Failing after 19s
7eb9cc7fa8
Adds doc/supervise-setup.md (systemd/launchd/tmux recipes for keeping `fjx supervise` alive), doc/fjx-json.md (per-project config schema), and wires both into doc/setup.md and doc/mgmt-deploy.md. Marks the autonomous-agents and mobile-UI roadmap items as in-flight with links back to the afk-mode change and mgmt-deploy. Archives the afk-mode change into openspec/changes/archive/2026-06-20-afk-mode/ and folds the deltas into openspec/specs/ (new capabilities: agent-images, mgmt-service, project-config, supervisor; updates to fjx-core and fjx-workflows).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
erik changed title from First cut at an AFK mode that utilizes a supervisor to launch agents in a container to afk-mode: autonomous agents & ops surface 2026-06-20 00:08:56 +00:00
fix flaky unit test
Some checks failed
Validate / validate (pull_request) Failing after 24s
9dc3f9e663
fix(adapter-codex): treat epoch-0 birthtime as missing
Some checks failed
Validate / validate (pull_request) Failing after 24s
da096212a1
On overlayfs (Docker, act-based CI runners) `Deno.stat.birthtime` returns
a Date at epoch 0 instead of null, so the `??` fallback to `mtime` never
fires and `CodexResolver.resolve` permanently caches a null binding for
the session file — events never reach the shard regardless of how long
the spawn waits. Treat any non-positive timestamp as missing.

Also silence `runSupervisor`'s operator stdout at the test-module level so
captured-output blocks don't drown out AssertionError detail, and revert
the `--quiet` flag added in 9dc3f9e (it hides failure detail too).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
fix(adapter-codex): drop filesystem-timestamp attribution
Some checks failed
Validate / validate (pull_request) Failing after 32s
21e925e1f4
CodexResolver was filtering session files by birthtime / mtime against the spawn's startedAt. On overlayfs (Docker, act-based CI runners) both fields are reported inconsistently — frequently as a Date at epoch 0, sometimes with coarser resolution than Date.now() — so the check loses, resolve() returns null, the adapter caches that null binding via state.resolved, and the file is silently dropped for the rest of the runner's lifetime. The previous positiveMs fallback only patched birthtime to mtime; mtime can fail the same way.

The check was never load-bearing for the actual contract. The adapter calls resolve() exactly once per file, on first observation of new bytes, so whichever bind is active at that moment is the right owner. Pre-existing files are caught by skipExisting at boot. Switch to active-binding-wins semantics; no FileInfo lookup.

Also restructure the codex date-partitioned test so writingSpawn keeps the bind window open until aborted and the test polls the shard for codex event types specifically — the old events.length >= 3 threshold counted supervisor lifecycle events (tick.spawned, run.failed, release.invoked) too, so a failed spawn could falsely satisfy it. Drop the 50ms pre-abort sleep; the polling loop owns synchronization and runSupervisor's in-flight drain handles teardown.

Replace the two timestamp-based CodexResolver tests with three that exercise the new contract (no bind → null, bind wins, stacked binds).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ignore non-root Dockerfile USER
All checks were successful
Validate / validate (pull_request) Successful in 31s
aa4305c26f
erik merged commit 9788bd7ea1 into main 2026-06-21 18:23:47 +00:00
erik deleted branch feat/afk-mode 2026-06-21 18:23:48 +00:00
Sign in to join this conversation.
No description provided.