agf discovers sessions by reading the files an agent already writes locally — no
plugin binaries, no config. Adding a new agent (harness) is a self-contained
change: implement one scanner and register it in a handful of match arms. The
compiler enforces most of the wiring — every match self { Agent::… } becomes
non-exhaustive until you add the new arm — but three registrations live in
plain Vecs the compiler can't check, so they're called out below.
Community integrations are welcome even if the agent isn't a "main" harness; keep them scoped like the existing ones (read-only scan, deletion limited to the validated session).
Say the new agent is Foo, CLI foo, sessions under ~/.foo/sessions/.
-
src/model.rs— theAgentenum. AddFooand fill in every arm the compiler now flags:Display,color(),all()(plain array — add it),cli_name(),resume_cmd(),new_session_cmd(). Addresume_mode_options()only iffoohas permission/approval flags.resume_cmd()must quote the id via the passedshell(shell.quote(session_id)), never raw'{session_id}'— session ids come from parsed files and may contain shell metacharacters.
-
src/config.rs— add afoo_sessions_dir()(or_dir()) helper returning the on-disk location. Usedirs::for platform-correct paths. -
src/scanner/foo.rs— implementpub fn scan() -> Result<Vec<Session>, AgfError>.- Return a
Sessionper resumable session. Settimestamp(Unix ms) to the last-activity time (file mtime or the newest in-file event) — not creation time — so time sort is consistent with the other agents. - Bound reads on large transcripts with the shared
read_head_tail/ bounded-read helpers inscanner/mod.rs; never slurp multi-MB logs whole. - Skip malformed lines, don't panic on bad input.
- Register the module in
src/scanner/mod.rs: addpub mod foo;and athread::spawn(|| foo::scan().unwrap_or_default())line inscan_all()(plainVec— the compiler won't remind you).
- Return a
-
src/plugin.rs— addBox::new(PluginAdapter(Agent::Foo))toall_plugins()(plainVec— not compiler-checked), then fill thename(),scan(), anddata_sources()arms.data_sources()returns the paths whose mtime decides cache freshness — keep it as narrow as possible (a single file/db beats a whole tree; see the perf note inscanner/claude.rs). -
src/cache.rs— add theAgent::Foo => scanner::foo::scan().unwrap_or_default()arm instart_stale_scan(). BumpCACHE_VERSIONonly if the cached payload shape can change within a single released package version (a new agent key alone doesn't require it — theagf_versionstamp forces a rescan on upgrade). -
src/delete.rs— addAgent::Foo => delete_foo_session(session)and implement it. Scope deletion to the one validated session (match by id in file content / a validated dir name); never delete by unvalidated path. Add a test proving a sibling session survives. -
Tests + docs — unit-test the scanner against a fixture session, add a
resume_cmdtest, add a row to the Supported agents and storage tables inREADME.md, and add the CLI name to the Requirements list.
cargo test --locked
cargo clippy --locked --all-targets -- -D warnings
cargo fmt --all -- --check
# Real-data smoke test (scans all agents regardless of install):
AGF_DEBUG=1 cargo run -- list --agent foo --format jsonagf list runs every scanner unconditionally, so you can verify foo against a
fixture $HOME without installing the CLI:
HOME=/tmp/agf-fixture cargo run -- list --agent foo --format json