All line numbers are 1-based (matching editor, git, and compiler conventions). Line 1 is the first line of the file.
Tool responses follow a tri-state contract so agents can tell "didn't run" from "ran clean" from "ran but partial":
success: false— the work could not be performed. Always carries acode(e.g.path_not_found,no_lsp_server,project_too_large,invalid_request,ambiguous_match) and amessage.success: truewithcomplete: true— the result is trustworthy. Absence of items in the result means the tool genuinely found nothing.success: truewithcomplete: false— the tool ran but the result is partial. The response will name the gap with one or more of:pending_files,unchecked_files,walk_truncated— files the tool didn't get toskipped_files: [{file, reason}]— files intentionally skipped (parse error, unsupported language)scope_warnings,no_files_matched_scope— paths/globs that resolved to zero files
- Side-effect skips — when the main work succeeded but a non-essential post-step was
skipped, the response carries a
<step>_skipped_reason. Approved values:format_skipped_reason:unsupported_language|no_formatter_configured|formatter_not_installed|formatter_excluded_path|timeout|errorvalidate_skipped_reason:unsupported_language|no_checker_configured|checker_not_installed|timeout|error
Tools that read a background index (indexes.trigram, indexes.semantic, indexes.callgraph)
say so instead of returning an empty result:
grepandglobscan the filesystem while the trigram index is not ready and mark the answerfallback: "filesystem".aft_searchuses whichever lanes are ready and names the ones it left out. With no lane ready it refuses withno_search_lanes_enabled(both indexes configured off) orsearch_lanes_unavailable, with each lane's status and cause. Usegrepmeanwhile.aft_callgraphrefuses withcallgraph_off,callgraph_buildingorcallgraph_unavailable.aft_inspectreports dead code as unavailable rather than zero findings.globandaft_searchleave out indexed paths that no longer exist on disk and count them inmissing_on_disk_dropped.
These replace the host harness's built-ins under the same names. A tool is registered unless
its name is in disabled_tools; disabling a host name (for example "grep") leaves the
host's own tool in place. There are no aft_-prefixed alternatives. The bash_status,
bash_watch, bash_write, and bash_kill companions register independently of bash,
but only while bash.background is on: they act only on background tasks. Apart from that,
index state and runtime settings never remove a registration: a tool whose index is off or
building reports that when called. The bash arguments follow their features the same way
(see shell-tool-surface.md).
The Pi/OMP adapter has no apply_patch or glob implementation, so those two tools are not
registered there.
| Tool | Description | Key Params |
|---|---|---|
read |
File read, directory listing, image/PDF detection | path, startLine, endLine, offset, limit |
write |
Write file with auto-dirs, backup, format, inline diagnostics | path, content |
edit |
Find/replace, symbol replace, batch, glob | path, oldString, newString, symbol, content, edits[] |
apply_patch |
*** Begin Patch multi-file patch format |
patchText |
ast_grep_search |
AST pattern search with meta-variables | pattern, lang, paths[], globs[] |
ast_grep_replace |
AST pattern replace (applies by default) | pattern, rewrite, lang, dryRun |
lsp_diagnostics |
Errors/warnings from language server | path, directory, severity, waitMs |
grep |
Trigram-indexed regex search with compressed output | pattern, path, include, exclude |
glob |
Indexed file discovery with compressed output | pattern, path |
Registered unless listed in disabled_tools.
| Tool | Description | Key Params |
|---|---|---|
aft_outline |
Structural outline of a file, directory, files, or URL; or indexed file tree | target (string or array), files |
aft_zoom |
Inspect symbols (same-file or cross-file); opt-in call-graph annotations | path, symbols (string or array), targets, url, callgraph |
aft_import |
Language-aware import add/remove/organize | op, path, module, names[] |
aft_conflicts |
Show all git merge conflicts with line-numbered regions | path (optional) |
aft_search |
Hybrid semantic + lexical code search by meaning | query, topK, path |
aft_inspect |
Codebase-health snapshot (TODOs, metrics, dead code, unused exports, duplicates) | sections, scope, topK |
aft_safety |
Undo, history, checkpoints, restore, purge | op, path, name |
Default-off tools (in the default disabled_tools; set an explicit list such as [] to
enable them) plus the call graph:
| Tool | Description | Key Params |
|---|---|---|
aft_delete |
Delete one or more files (or directories) with backup | files, recursive |
aft_move |
Move or rename a file with backup | path, destination |
aft_callgraph |
Call graph and data-flow navigation | op, path, symbol, depth |
Plain file reading and directory listing. Pass path to read a file, or a directory path to
list its entries. Paginate large files with startLine/endLine or offset/limit.
With github.read enabled, use pr://N/diff for a live unified diff or
pr://N/diff/<path> for one exact changed path (renames use the new path).
Both accept pr://OWNER/REPO/N/diff forms. Each page names the PR head SHA;
line ranges select the diff body, not the repeated header. Binary changes get
one line, and paging or the 4 MiB fetch ceiling is disclosed. Diffs are read-only
views: use read, not aft_outline or aft_zoom. Diff failures never use cached data.
// Read full file
{ "path": "src/app.ts" }
// Read lines 50-100
{ "path": "src/app.ts", "startLine": 50, "endLine": 100 }
// Read 30 lines from line 200
{ "path": "src/app.ts", "offset": 200, "limit": 30 }
// List directory
{ "path": "src/" }Returns line-numbered content (e.g. 1: const x = 1). Directories return sorted entries with
trailing / for subdirectories. Binary files return a size-only message. Image and PDF files
return metadata suitable for UI preview. Output is capped at 50KB.
For symbol inspection with call-graph annotations, use aft_zoom.
On OpenCode 1 and 2, read rules govern read, aft_outline and aft_zoom.
Outline and zoom check each file whose content they return, including revision reads.
A denied file is refused; in a batch it is left out with a permission error for that entry.
URL targets are not file paths and do not ask for file-read permission.
grep and aft_search use their own permissions, not read rules. They can return
matched lines from files a read rule denies. To keep secrets out of search results,
use path restrictions or deny rules on the search tools too; a read deny alone is
not a search-results filter.
With revision_reads enabled, read, aft_outline and
aft_zoom take an optional ref: any revision git resolves to a commit, such as a branch, a
tag (annotated tags resolve to their commit), a full or short SHA, or forms like HEAD~2. The
file comes from git's object store as it is in that commit; the working tree, the index and
HEAD are never read or changed, so uncommitted work is safe.
// The file as it is on another branch
{ "path": "src/app.ts", "ref": "main" }
// Lines 40-80 of the file two commits ago
{ "path": "src/app.ts", "ref": "HEAD~2", "startLine": 40, "endLine": 80 }- The revision is resolved once per call, and the reply's first line names the commit:
Revision `main` = commit <sha> (read from git's object store, not the working tree). readkeeps its line numbers, ranges, binary detection and size limits.aft_outlineandaft_zoomparse the file with the same parsers, choosing the language from the path. Every file of anaft_zoomcall, including eachtargetsentry, is read at the same revision.- The path is checked exactly like a working-tree read: the project-root restriction, read permission rules and outside-path prompts all apply to the path you pass.
- Only files of the session's own repository can be read, including from a linked worktree of
it.
aft_outlineat a revision takes a single file, not a directory, an array or a URL. There is no search at a revision. - Errors are typed:
revision_path_not_found(the path does not exist at that revision; the message names the revision and commit),revision_not_found(the revision names no commit),revision_path_is_directory,revision_blob_too_large(above 256 MiB) and, whilerevision_readsis off,revision_reads_disabled. While it is off,refis not part of any tool schema or description.
Write the full content of a file. Creates the file (and any missing parent directories) if it doesn't exist. Backs up any existing content before overwriting.
{ "path": "src/config.ts", "content": "export const TIMEOUT = 10000;\n" }Auto-formats using the project's configured formatter (biome, oxfmt, prettier, etc.).
The write returns as soon as the file is written. Diagnostics surface through the AFT status
bar and aft_inspect; set lsp.diagnostics_on_edit: true in aft.jsonc to additionally wait
for and inline fresh LSP diagnostics on every edit.
For partial edits (find/replace), use edit instead.
The main editing tool. Mode is determined by which parameters you pass:
Find and replace — pass path + oldString + newString:
{ "path": "src/config.ts", "oldString": "const TIMEOUT = 5000", "newString": "const TIMEOUT = 10000" }Matching uses a 4-pass fuzzy fallback: exact match first, then trailing-whitespace trim, then
both-ends trim, then Unicode normalization. Returns an error if multiple matches exist — use
occurrence: N (0-indexed) to pick one, or replaceAll: true to replace all.
Symbol replace — pass path + symbol + content:
{
"path": "src/utils.ts",
"symbol": "formatDate",
"content": "export function formatDate(d: Date): string {\n return d.toISOString().split('T')[0];\n}"
}Includes decorators, doc comments, and attributes in the replacement range.
Batch edits — pass path + edits array. Atomic: all edits apply or none do.
{
"path": "src/constants.ts",
"edits": [
{ "oldString": "VERSION = '1.0'", "newString": "VERSION = '2.0'" },
{ "startLine": 5, "endLine": 7, "content": "// updated header\n" }
]
}Set content to "" to delete lines. Per-edit occurrence is supported.
To edit multiple files, make parallel edit calls in one response.
Glob replace — use a glob as path with replaceAll: true:
{ "path": "src/**/*.ts", "oldString": "oldName", "newString": "newName", "replaceAll": true }Append to file — pass path + appendContent:
{ "path": "notes.md", "appendContent": "\n## New section\n..." }Creates the file (and parent directories) if missing. Faster than read+write for adding to logs, notepad files, or large appendable structures.
The edit returns as soon as the write completes. Diagnostics surface through the AFT status bar
and aft_inspect; set lsp.diagnostics_on_edit: true in aft.jsonc to additionally wait for
and inline fresh LSP diagnostics on every edit. Use aft_safety checkpoint / undo for
recovery before risky edits.
Apply a multi-file patch using the *** Begin Patch format. Creates, updates, deletes, and
renames files. Hunks commit per file: successful hunks are kept and failures are reported, so a
partial patch leaves the applied changes in place (use aft_safety to revert if you want to
abort). A move hunk never deletes the source unless the destination write succeeds.
*** Begin Patch
*** Add File: path/to/new-file.ts
+line 1
+line 2
*** Update File: path/to/existing-file.ts
@@ context anchor line
-old line
+new line
*** Delete File: path/to/obsolete-file.ts
*** End Patch
Context anchors (@@) use fuzzy matching to handle whitespace and Unicode differences.
Diagnostics surface through the AFT status bar and aft_inspect (or inline on every edit with lsp.diagnostics_on_edit: true).
With validate_on_edit: "full" (or a binary request's validate: "full" override),
formatting and syntax validation still happen per write, but type checking waits until
all hunks have succeeded. Each distinct checker runs once for the configured project
root against the completed patch. File-scoped checkers receive all touched paths when
their CLI supports it; Go named-file checks across directories fall back to one run per
file. Deleted files are not checker inputs, and repeated writes to a path are checked once.
Checker diagnostics appear on the existing metadata.files entries as validation_errors
(including an empty array for a clean check) and, when necessary, validate_skipped_reason.
Each diagnostic's file retains the checker-reported path, resolved against the checker
root for attribution to the correct touched file.
For a move hunk, diagnostics belong to the destination. The rendered output also includes
a single summary, for example type check: 2 errors in 1 of 3 files (cargo check, tsc);
unchecked files are explicitly named by count and skip reason in that line. There is no
top-level aggregate diagnostic list. Failed or partially applied patches do not run
type checkers, even though their successful writes are kept.
Execute shell commands through AFT's unified bash handler. AFT registers bash in the
recommended tool surface; experimental flags gate advanced behavior, not the tool itself.
Schema:
| Param | Type | Description |
|---|---|---|
command |
string | Shell command to execute |
timeout |
number | Hard-kill cap in milliseconds (positive integer). Default 30 minutes when unset; while a delegated (subagent) session keeps waiting on the command, each wait pushes that default to at least bash.worker_wait_max_ms after the wait. An explicit timeout is never extended. NOT a polling window — see below. |
workdir |
string | Working directory for command execution |
description |
string | Short human-readable summary for harness UI metadata |
background |
boolean | Spawn detached and return a taskId (requires the background flag) |
compressed |
boolean | Opt in/out of output compression for this call (default true; requires compression flag) |
pty |
boolean | Run in a real PTY for interactive programs. Implies background: true. |
ptyRows / ptyCols |
number | PTY dimensions (max 60 rows / 140 cols). Soft-ignored on non-PTY calls. |
runon |
string | Run on a remote build server: linux (2 vCPUs), or linux,Nc for N vCPUs such as linux,8c; add ",net" for outbound internet (offline by default). windows runs the command in Git Bash on a Windows build machine (no vCPU or network options). |
Calls admitted under the worker catalog preset set NEXTEST_TEST_THREADS and RUST_TEST_THREADS for local bash children from ../.cargo/alfonso-test-threads (one decimal integer plus newline), defaulting to 4 when the file is missing, invalid, or above 256; valid values through 256 are preserved. Existing inherited or per-call values take precedence, and shell command prefixes still override them; head sessions and unscoped plugin worker flags receive no defaults, and exec-remote requests never carry these variables.
Command environment:
- Where it comes from. In standalone mode AFT runs as a child of the host, so a local command inherits the host's environment. Under the CortexKit daemon, a command inherits the AFT module's environment, not the host's. Sandboxed commands get a filtered environment.
- Setting a variable for one command. Put it in the command text, for example
SUDO_ASKPASS=/path/to/helper sudo -A make install. On OpenCode 1, AFT also calls the host'sshell.envplugin hook for each bash call and adds the variables it returns. OpenCode 2 has no equivalent hook a plugin can call. - What AFT changes. Before launch, AFT removes the daemon's credentials. When the GitHub shim is enabled it puts its
ghshim first onPATH, and when git co-author attribution is enabled it points Git at its managed hooks. - Remote
runonjobs. The command text, including anyNAME=valueprefix, is sent unchanged. Forwarded variables are filtered by name, case-insensitively: names containingTOKEN,SECRET,PASSWORD,PASSWD,PRIVATE_KEY,API_KEYorCREDENTIAL; names starting withAWS_,GH_,AFT_,CORTEXKIT_,CORTEX_,CK_orSUBC_; andSSH_AUTH_SOCK,NEXTEST_TEST_THREADSandRUST_TEST_THREADSare never sent.
Timeout model: timeout is a hard-kill cap, never a polling parameter, and starts after
process spawn (setup time does not count). Expiry sends SIGTERM to the Unix process group, then
SIGKILL after up to 2 seconds (Windows uses taskkill /T /F) and reports exit 124. Unix processes
that leave the group (setsid, setpgid, Python start_new_session=True) survive; macOS has no
tree kill. AFT cannot signal a process owned by another user, such as one started with sudo; on
Linux and macOS the task's live_descendants and text name any such survivor by pid, owner and executable
name (never its arguments, which can hold credentials; a null list comes with
live_descendants_unavailable, the reason AFT could not check), so bound sudo commands yourself:
sudo -n timeout -k 1 <secs> <cmd> shorter than AFT's limit, or
sudo -n bash -c "trap 'kill -KILL 0' TERM; <cmd> & wait". A bare foreground
bash({ command }) is polled for a short internal wait window (~5s); if the command hasn't
finished it auto-promotes to a background task and returns a taskId while the command keeps
running under the 30-minute (or explicit timeout) kill cap. bash({ timeout: 2000 }) polls
briefly then hard-kills at 2s. background: true skips polling entirely.
Completion previews in notices and wakes have common credential formats and secret-named child environment values masked; stdout/stderr files and bash_status tool results remain unchanged.
After a daemon restart, a masked preview saved before the restart is replayed unchanged. A preview not yet saved is built from the output files only when the task was recorded as having no secret-named environment values; otherwise its preview is withheld because the secret values are no longer in memory to mask it, and bash_status reads the output.
Foreground example:
{ "command": "git status" }Returns combined stdout/stderr plus exit_code, duration_ms, truncation status, and an
output_path when large output spills to disk.
Running on a remote build server (runon) — runon: "linux" sends the whole command
line, exactly as written (pipes, lists, environment prefixes and all), to the remote Linux
build server (ck-motor, reached through the Subconscious daemon's exec-remote/v1). runon: "linux,4c" requests exactly four vCPUs; the runner refuses counts it cannot serve before the
command starts. Without a count, the job gets 2 vCPUs. Add ,net for outbound internet:
linux,net, linux,4c,net and linux,net,4c are accepted; remote jobs are offline by default. A plan's
default_demand may include these options too. runon: "windows" sends the command to a Windows
build machine and runs it in Git Bash; it does not accept vCPU or network options. Duplicate
options, unknown tokens and unsupported platforms are refused by name before dispatch. It runs
there under bash -c in the same working directory, with the same timeout and the same
environment, minus the secret-shaped and AFT/CortexKit control variables AFT strips before any
off-host request (the reply names them). New plans require runon to send a line away. Deployed
worker plans with an enabled commands prefix list retain their existing automatic routing: every
literal command must match an allowed prefix, and unsupported syntax stays local.
Explicit whole-line runon also requires the user-only live safety switch
bash.runon_enabled: true (default false); projects cannot enable it. Turning that switch off
hides and refuses runon without disabling legacy prefix routing.
When remote runs are available, put runon: "linux" on build and test lines (cargo, bun test), including chains and pipes. Keep git, gh, interactive and file-editing commands local, and keep a line local if it needs this machine's own operating system or hardware (its sandbox, code signing, service manager, permission prompts or GUI frameworks) or runs a binary that already exists on this machine; building and testing still go remote, and a remote build leaves no binaries or target/ output here.
The job sees exactly the CPUs it gets, so ask for what the command uses: 2c (the default) for a single test binary or script, 4c for cargo check/clippy on one crate, 8c for a workspace test suite, and 16c for a large release build.
Who is offered runon:
- OpenCode and Pi sessions on macOS or Linux in subc mode, when the user config sets
remote_exec.enabled: true, the user safety switchbash.runon_enabled: true, and the project has not turned remote runs off (see Configuration). The decision is made once when the tool is built; provider discovery happens at call time, without a startup round trip. - Head catalogs follow the same user setting. Broca worker catalogs instead use that session's
persisted plan, and offer
runononly when itsremote_exec.enabledand the user safety switch are true, including after a paramless refetch or restart.
Default schemas and unavailable sessions contain neither the parameter nor remote-build instructions. Standalone NDJSON sessions and Windows never advertise it, whatever the user setting says.
A call that sets runon is refused by name, and runs nowhere, when it cannot run remotely:
the project turned remote runs off (remote runs are off for this project), the session has no
remote runner (this session has no remote runner), the demand is not one AFT knows
(unknown runner demand; the server only runs Linux), an option is malformed (for example,
runon "linux,4" is not valid: options are net and a vCPU count as Nc, for example "linux,4c,net"),
or the call also sets pty: true, a
PowerShell shell, or sandbox: "host". On Windows runon is refused by name: remote dispatch
needs AFT on macOS or Linux. If no exec-remote/v1 provider answers, or daemon discovery fails before dispatch,
the task is refused by name and the command is not run locally. background: true works as
for local commands, and a background remote task re-attaches after a restart.
When the runner refuses an explicit runon job before starting it, the call fails with code
remote_unavailable: runon refused: remote refused: <reason>; command was not run; retry, or omit runon to run locally. The refusal reason is preserved, including reasons from newer
runners. This also applies to pending tasks recovered after a restart. No local process is
spawned. A deliberately backgrounded call still returns its task ID; a later refusal marks
that task failed and its status includes remote_refusal with the same error text.
network_unsupported means the runner cannot grant the requested network mode; it is a
non-transient refusal before start, not a local fallback, and includes the runner's detail
when present. If the runner accepts a net job but does not acknowledge outbound access
(including older runners), the header plainly says OFFLINE: the runner did not grant the requested network access.
Remote results begin with ran remotely on ck-motor. The header names the accepted job's full
ID when available. If a vCPU count was requested, it includes that too; for example
ran remotely on ck-motor (4 vCPUs requested, job <UUID>, network) when net was also
requested. Refusals after acceptance name the job ID too. A runner's Started record marks the job
as running; its timeout is measured from then, not from when it entered the queue. Only
automatic prefix routing (without an explicit runon) retains local fallback with the advisory
ran locally on macOS: remote refused: <reason> (or the local OS name). A job that started
remotely is never automatically resubmitted: if AFT loses track of it, the reply says the
outcome is unknown instead of re-running it. Remote jobs run on a
server-side copy of the workspace, and no files are synced back. For offline jobs the
reply says remote outcome unknown (job <id>); the remote job could not affect this machine or the network, so rerunning is safe; check exec.status <id> first if you need its result. For jobs granted outbound access, the reply instead warns that the job may have had
outside effects through outbound network access, so check before rerunning, and names the
remote job ID. It never says such a rerun is safe. If AFT has no job ID, it omits (job <id>)
and the check exec.status advice. This
applies only to remote jobs; local commands, including a remote refusal's local fallback,
keep their existing unknown-outcome warnings because their side effects may have happened.
After the output, the reply prints what the runner reported about the server's workspace, none of which
is copied back: the files the run changed (These files changed on the server and were NOT copied back:), a changed Git state (HEAD before -> after, the symbolic ref or detached,
whether the index tree changed, and the stash count change), new untracked files (marked when
the runner listed only some of them), and the number of writes under ignored paths with sample
paths. A kind of change the runner reported as empty is not mentioned. A kind it did not report
(an older runner) gets one line, such as git state: not reported by the runner, and is never
shown as "nothing changed".
Rewriter — when experimental.bash.rewrite: true, common shell command shapes route to AFT
tools instead of spawning bash:
| Pattern | Routes to | Example |
|---|---|---|
cat <file> |
read |
cat README.md → read |
grep [-r] PATTERN <path> |
grep |
grep -r TODO src/ → grep |
find <path> -name '<glob>' |
glob |
find src -name '*.ts' → glob |
sed -n 'N,Mp' <file> |
read startLine/endLine |
sed -n '10,20p' src/x.ts → read |
ls [-l] [-R] [<path>] |
read directory mode / glob |
ls src/ → read |
rg PATTERN [<path>] |
grep |
rg foo → grep |
cat >> <file> / echo "X" >> <file> |
edit append op |
cat >> notes.md <<< 'note' → edit appendContent |
Each rewrite returns the AFT tool's result with a footer hint reminding the agent to call the direct tool next time.
Compression — when the compression flag is enabled (default-on once enabled), bash output flows through five tiers in order:
- Specific Rust compressors — stateful parsers keyed by a specific tool token anywhere in
the command (
npx vitest,pnpm exec eslint, etc.). Win first. Currently:git(status / diff / show / log / branch / blame / add / commit / push / pull / fetch / stash),cargo,tsc,pytest,eslint,vitest/jest,biome,prettier,ruff,mypy,go,golangci-lint,playwright,next. - Output-shape sniffers — the same inner-tool parsers recognizing their own summaries even
when invoked through wrappers (
npm test,make test,bun run vitest,./scripts/check.sh). - Package-manager compressors — broad head-token matchers (
npm,pnpm,bun) that compress unclaimed package-manager output. - Built-in TOML filters — declarative strip + truncate + cap + shortcircuit rules covering
the long tail of CLI tools. Ships 22 filters:
make,ls,tree,df,du,find,wc,gradle,xcodebuild,terraform,helm,docker,kubectl,gh,ansible-playbook,aws,curl,wget,deno,pip,uv,psql. User-supplied filters at<storage_dir>/filters/*.tomloverride built-ins; project-supplied filters at<project>/.cortexkit/aft/filters/*.tomloverride both but require explicit trust vianpx @cortexkit/aft@latest doctor filters trust. - Generic fallback — ANSI stripping plus consecutive-line deduplication and middle-truncate.
Use npx @cortexkit/aft@latest doctor filters to inspect what's loaded for the current project. Pass
compressed: false on a bash call to opt out for that invocation.
# ~/.local/share/cortexkit/aft/filters/my-tool.toml
[filter]
matches = ["my-tool"] # program name (after stripping env vars + path)
description = "Compact my-tool output"
[strip]
patterns = [ # regex per line; matching lines are dropped
'^Loading config from',
'^Resolving \d+ dependencies',
]
[truncate]
line_max = 500 # middle-truncate per-line over N chars
[cap]
max_lines = 80 # head|tail|middle
keep = "tail"
[shortcircuit] # if remainder matches `when`, replace whole output
when = '^\s*$'
replacement = "my-tool: ok"
[ansi]
strip = true # default trueProject filters under .cortexkit/aft/filters/ are an attack vector — a malicious repo could ship a filter
that strips real failures and replaces them with tests: ok. AFT therefore only loads project
filters from explicitly trusted projects. Run npx @cortexkit/aft@latest doctor filters trust to
review and approve them. Inspect the active set with npx @cortexkit/aft@latest doctor filters and dump
a single filter's resolved content with --show <name>.
Background — when the background flag is enabled, pass background: true to spawn detached.
The call returns taskId; inspect a snapshot with bash_status({ "taskId": "..." }); kill with
bash_kill({ "taskId": "..." }). Completed-but-unread tasks surface in bg_completions: [...] on
the next foreground tool call, and a completion reminder is delivered automatically (no polling
needed). Output is buffered in memory up to 1MB and spills beyond that to AFT's bash-output cache
(default ~/.cache/aft/bash-output/<taskId>.log, or the harness storage directory when configured).
Background tasks and undelivered completions are persisted to disk and survive AFT restarts.
Once a task has finished for 24 hours and its completion has been delivered, its output files are
deleted; copy evidence you need into your report or a shared file. With project-root restrictions,
only the starting session gets an ownership exception for output outside the project; another
session (such as a reviewer reading a worker's path) may be refused even while the files exist.
Foreground bash also starts through the same task flow. Short commands are polled and return inline
output; commands that exceed the foreground wait window are automatically promoted to background
and return a taskId.
Background task limit — at most 8 background tasks (max_background_bash_tasks) run at once
per project root, shared by all sessions in it. Local and remote (runon) tasks share the same
slots. Only background tasks count: background: true and
pty: true launches, and foreground commands once they are promoted. A background launch at the
limit is refused with background_task_limit_exceeded; the message lists your own session's tasks
holding slots (task id, age, the first 60 characters of the command, up to 8 rows), counts the slots
held by other sessions in one line ("N more held by other sessions in this project", with no task ids
or commands), and says to free a slot by stopping one of your own tasks with bash_kill or to wait
for a task to finish. A foreground command always starts, even at the limit. If it then outlives
its wait window it is still promoted (it is
already running, so refusing it would lose its work) and counts from then on, so the count can
briefly exceed the limit.
bash_status — read-only snapshot of a background or PTY task's current state and output.
Never waits. For PTY tasks, outputMode selects screen (vt100-rendered), raw (byte stream),
or both.
bash_watch — block on or register for a background task's output. In a main session sync
waits are for a short remaining wait on a task (default 30s, max bash.watch_sync_max_ms, 120s
by default); for anything longer end the turn on bash({background:true}) and let the completion
reminder wake you, or use bash({wait:true}) when the result is needed before anything else. In a
delegated (subagent) session, which cannot be woken once its turn ends, a sync wait without
timeoutMs waits up to the worker wait limit (bash.worker_wait_max_ms, 30 minutes by default),
then reports the command is still running with how long it has run and its latest output; the
worker watches again to keep waiting or kills it. An explicit timeoutMs can shorten that wait,
but never extends it beyond the configured worker limit. Every session's blocking bash
call (wait: true, or any foreground call when
bash.subagent_background is false) is bounded the same way: at the limit the command moves to the
background, not killed, and the reply gives its task id, including on the tool-provider/v1 route.
The head preset declares a reply deadline for bash; the worker preset declares it for bash
and bash_watch. Both use the resolved bash.worker_wait_max_ms plus 240 seconds for spawn,
terminal handoff and reply delivery. A watch does not spawn a command, but retains the same
margin for admission, observing terminal state, and delivering its reply under load. Head
catalogs do not serve bash_watch (heads receive completion wakes); reader catalogs serve neither
shell tool and declare no deadline. If the sum exceeds the role contract's 24-hour maximum, the catalog
still serves the tools but omits their reply metadata, leaving callers' fallback in force.
Broca freezes the declared window when a session is admitted: new sessions get the longer
window, while running sessions keep their admitted window. A shorter client deadline can
still shorten a wait so the reply arrives safely before that deadline. No hold_argument
is declared, so the transport window follows the resolved configuration rather than each
call's own wait; a future hold_argument declaration could make it follow each call.
The wait limit is not the task's kill
deadline: every reply that hands a task back (launch, promotion, detach) and every bash_watch
result also names the task's own deadline (for example, "AFT kills this task at 2026-09-10 10:30:00Z, when it has run 30 minutes (its default background limit); about 12 minutes remain", or the timeout you passed), and a task killed by it is reported by
name ("killed by AFT's default background limit of 30 minutes (exit 124)") rather than as a bare
timeout. Sync mode
waits until a pattern matches, the task exits, timeoutMs elapses, a new message arrives, or the
call is aborted. Async mode (background: true)
registers a pattern watcher that fires a notification when matched and suppresses the default
completion reminder. (Wait/watch semantics moved here from bash_status — bash_status is
snapshot-only.)
bash_write — send input to a running PTY task. input is either a literal string or an
array mixing literal strings and { key: "..." } objects for control keys, e.g.
[ "iHello", { key: "esc" }, ":wq", { key: "enter" } ]. Named keys cover enter/tab/esc/arrows/
function keys/ctrl chords. { key: "enter" } emits CR. Expanded input is capped at 1 MiB.
PTY — pass pty: true (implies background: true) to run interactive programs (python,
node, vim, even a nested agent) in a real PTY. Drive it with bash_write and inspect with
bash_status({ outputMode: "screen" }). PTY sessions are session-scoped and do not survive a
bridge restart. Subagents cannot spawn PTY tasks.
Permissions (OpenCode only) — bash uses tree-sitter to parse the command into sub-commands
and asks for permission per sub-command via ctx.ask({ permission: "bash", patterns, always }).
File-touching commands (rm, cp, mv, etc.) also fire
ctx.ask({ permission: "external_directory" }) for paths outside the project root. Pi has no
permission system; bash runs without prompts.
For a file outside the project, OpenCode's "always" rule and Cingulate's folder option stay
file-only for your home folder and the folders above it, drive and volume roots, root-level
folders, and system folders such as /usr, /etc, /System, C:\Windows and
C:\Program Files (macOS per-user temp folders under /var/folders are not system folders).
Other folders keep the folder-wide rule.
Search for structural code patterns using meta-variables. Patterns must be complete AST nodes.
Groovy path filtering covers .groovy, .gvy, .gy, .gsh, *.gradle, and Jenkinsfile when lang is groovy.
{ "pattern": "console.log($MSG)", "lang": "typescript" }$VARmatches a single AST node$$$matches multiple nodes (variadic)
Returns matches with file, line (1-based), column, matched text, and captured variable values.
Add contextLines: 3 to include surrounding lines.
// Find all async functions in JS/TS
{ "pattern": "async function $NAME($$$) { $$$ }", "lang": "typescript" }When the supplied paths or globs resolve to zero files (rather than matching files with no
hits), the response carries no_files_matched_scope: true and scope_warnings: [...] listing
each path/glob that contributed zero files. This is distinct from a successful search that
returned no matches.
Replace structural code patterns across files. Applies changes by default — set dryRun: true to preview.
Groovy replacements use the same lang: "groovy" path rules as ast_grep_search, including *.gradle and Jenkinsfile.
{ "pattern": "console.log($MSG)", "rewrite": "logger.info($MSG)", "lang": "typescript" }Meta-variables captured in pattern are available in rewrite. Returns unified diffs per file
in dry-run mode, or writes changes with backups when applied.
On-demand LSP file/scope check. Lazily spawns the relevant language server, opens the document, prefers LSP 3.17 pull diagnostics where supported (rust-analyzer, gopls, ty), and falls back to push + waitMs for servers that don't support pull (bash-language-server, yaml-language-server, typescript-language-server).
Not a project-wide type checker — for full coverage run tsc --noEmit, cargo check,
pyright src/, etc. AFT's LSP is for fast feedback during edits.
Built-in servers (6 + 1 experimental): TypeScript (.ts/.tsx/.js/.jsx), Pyright (Python),
rust-analyzer (Rust), gopls (Go), bash-language-server (.sh/.bash/.zsh),
yaml-language-server (.yaml/.yml), and ty (Python, gated by experimental.lsp_ty).
User-defined servers go in lsp.servers (see Configuration). Disable any built-in via lsp.disabled.
// Check a single file (pull where supported, push fallback otherwise)
{ "path": "src/api.ts", "severity": "error" }
// Check files under a directory (workspace pull from active servers + 200-file walk for unchecked listing)
{ "directory": "src/", "severity": "all" }
// Wait up to 2s for push diagnostics on push-only servers (bash, yaml, typescript)
{ "path": "deploy.sh", "waitMs": 2000 }Response shape:
Reading honestly: total: 0 with empty lsp_servers_used means nothing was checked —
install the relevant LSP server (see warnings on plugin startup). total: 0 with pull_ok /
push_only means the file is genuinely clean.
When the response looks unhelpful and you can't tell which case applies, run
npx @cortexkit/aft@latest doctor lsp <file> for a per-file triage that names the binary
resolution path, workspace root markers, and spawn outcome for every server registered for
that extension.
Returns all top-level symbols in a file with their kind, name, line range, visibility, and nested
members (methods in classes, sub-headings in Markdown). Takes a single target parameter that
auto-detects what to outline:
- File path → outline that file with signatures
- Directory path → recursively outline all source files (capped at 200)
- Array of paths → batch-outline multiple specific files
- URL (
http:///https://) → fetch and outline a remote HTML/Markdown/JSON document
Pass files: true with a directory target to get a flat indexed file tree instead of a symbol
outline — each entry carries language, top-level symbol count, and byte size, reusing the symbol
cache so it's cheap on large trees.
For Markdown files (.md, .mdx): returns heading hierarchy with section ranges — each
heading becomes a symbol you can read by name.
// Outline a single file
{ "target": "src/server.ts" }
// Outline two files at once
{ "target": ["src/server.ts", "src/router.ts"] }
// Outline all source files in a directory
{ "target": "src/auth" }
// Outline a remote document (OpenCode)
{ "target": "https://docs.example.com/api.md" }In multi-file and directory modes, files that fail to parse or whose language is unsupported
are listed under skipped_files with a per-file reason (e.g. parse_error,
unsupported_language) instead of being silently dropped from the result.
With revision_reads enabled, ref outlines a single file as it is at a git revision; see
Reading at a git revision.
Inspect code symbols or documentation sections using one non-empty batch array.
Each entry has a target and symbols (a string or string array). Targets are auto-detected
as file paths (absolute or relative to the project root), HTTP/HTTPS URLs, or issue://N /
pr://N when GitHub reads are enabled. Files and URLs can share the same batch.
For Markdown and HTML, use the heading text as the symbol name.
// One lookup
{ "batch": [{ "target": "src/app.ts", "symbols": "handleRequest" }] }
// Mix a file and a remote document; options apply to every entry
{ "batch": [
{ "target": "src/app.ts", "symbols": ["Config", "createApp"] },
{ "target": "https://docs.example.com/api.md", "symbols": "Authentication" }
], "contextLines": 3, "callgraph": true }callgraph defaults to false. Enabling it adds calls_out and called_by annotations.
Each failed lookup reports its own error line while other entries still return results.
Use read rather than zoom to read an entire local file.
With revision_reads enabled, ref reads every file of the call at one git revision; see
Reading at a git revision.
Show all git merge conflicts across a repository in a single call. Resolves the git top level,
unions index-unmerged files (git ls-files --unmerged) with a tracked working-tree marker scan
(so staged-but-still-marked files are caught), parses conflict markers, and returns line-numbered
regions with 3 lines of surrounding context — the same format as read output. The output names
the Checked repo root so a false "none" is obvious.
// Inspect the session project repository
{}
// Inspect a different repository or git worktree (e.g. where a rebase is running)
{ "path": "/Users/me/work/some-other-worktree" }path is optional (defaults to the session project root); conflicts are discovered from that
path's git top level, so it works even when the conflict is in a sibling worktree or a subdirectory.
Returns output like:
9 files, 13 conflicts
── src/manager.ts [3 conflicts] ──
15: resolveInheritedPromptTools,
16: createInternalAgentTextPart,
17: } from "../../shared"
18: <<<<<<< HEAD
19: import { normalizeAgentForPrompt } from "../../shared/agent-display-names"
20: =======
21: import { applySessionPromptParams } from "../../shared/session-prompt-params-helpers"
22: >>>>>>> upstream/dev
23: import { setSessionTools } from "../../shared/session-tools-store"
Use edit with the full conflict block (including markers) as oldString to resolve each conflict.
When a git merge or git rebase produces conflicts, the plugin automatically appends a hint
suggesting aft_conflicts to the bash output.
Trigram-indexed regex search that hoists the host harness's built-in grep. Requires
indexes.trigram (on by default). The trigram index is built in a background thread
at session start, persisted to disk for fast cold starts, and kept fresh via file watcher.
Falls back to direct file scanning when the index isn't ready.
For out-of-project paths, shells out to ripgrep with the same flag set the harness's native grep would have used.
{ "pattern": "handleRequest", "include": "*.ts" }Returns matches grouped by file with relative paths, sorted by modification time (newest first). With default paging, a result of at most 100 lines and 16 KiB prints whole:
src/server.ts
42: export async function handleRequest(req: Request) {
89: return handleRequest(retryReq)
src/test/server.test.ts
15: import { handleRequest } from "../server"
Found 3 matches across 2 files
Lockfiles, generated and minified files, logs and command output, and JSON data files are
counted, not printed, unless path names the file or include names it or its kind (bun.lock,
*.log, **/stdout, *.json, *.min.js). The reply lists each counted file or pattern with its
line count and kind, and the value to pass:
Counted only, not shown: 112 lines in 1 file. To show them, pass a file below as path, or a pattern below as include.
tools/common-auth-build/bun.lock: 112 lines (lockfile)
Counted lines are printed anyway when nothing else matched, or when there are at most 10 of them
(2 KiB or less). A folder passed as path does not by itself print the counted files inside
it.
A larger result starts with the matching lines per file (up to 25 files, then a per-folder roll-up
of the rest) and a first page of 40 lines dealt round-robin across files, at most 5 per file when
several files match. Later pages hold up to
100 lines and stop once their rows reach 50 KB. Lines longer than 500 characters are shown as a
window around the match, marked … [line truncated]. Counts are exact: every match in scope is
counted (up to 100,100 stored lines; past that the total is shown as a lower bound and the stored
lines are the first 100,100 by path and line, chosen the same way on every request), and pages are
cut from one fixed order, so while the files do not change, following offset never repeats or
skips a line. A search stopped by the walk's time or file limit says so, and another request may
store different lines. When rows are cut,
the reply ends with the standard trailer, for example
shown 40 of 1187 rows (cap) · narrow: offset, path, include, or
shown 2 of 114 rows (kind) · narrow: path, include when only counted lines were left out.
Parameters: pattern (required), path (optional — scope to subdirectory or absolute path),
include (glob filter, e.g. "*.ts"), offset (zero-based count of printed lines to skip,
default 0). The bridge command also accepts exclude (negate glob), case_sensitive
(default true) and max_results (page size for every page; by default the first page holds 40
lines and later pages 100).
Indexed file discovery that hoists the host harness's built-in glob. Uses
indexes.trigram (on by default) and scans directly while the index is off or building. Returns absolute paths sorted by modification time,
capped at 100 files.
{ "pattern": "**/*.test.ts" }Returns relative paths. For small result sets, a flat list:
3 files matching **/*.test.ts
src/server.test.ts
src/utils.test.ts
src/auth/login.test.ts
For larger result sets (>20 files), groups by directory:
20 files matching **/*.test.ts
src/ (8 files)
server.test.ts, utils.test.ts, config.test.ts, ...
src/auth/ (4 files)
login.test.ts, session.test.ts, token.test.ts, permissions.test.ts
... and 8 more files in 3 directories
Parameters: pattern (required), path (optional — scope to subdirectory or absolute path).
When a filesystem fallback walk reaches its time limit, glob returns matches found so far
with complete: false, reports walk_entries_examined, and includes a time limit trailer.
Narrow the search with a smaller path or a more specific pattern to continue.
The primary code-search tool: concepts, identifiers, error strings, regex, literals, and
filenames are auto-routed to the right engine and returned ranked. Works even when you only
know what the code does, not what it's named ("where is rate limiting handled", "retry
logic", ^export, Cargo.lock). The semantic lane uses indexes.semantic (on by default) and
ONNX Runtime installed on the system when using the default
fastembed backend.
How it works — hybrid retrieval: AFT classifies each query by shape (identifier, path, error-code, mixed, natural-language) and routes through two lanes:
- Semantic lane — local embedding model (all-MiniLM-L6-v2, ~22MB, downloaded on first use) embeds code symbols (functions, classes, methods, structs, file-level summaries for thin files) and matches by cosine similarity. Always runs.
- Lexical lane — trigram-index scoring over the same code files, runs for identifier, path, error-code, and mixed shapes. Disabled for pure natural-language queries to avoid noise.
Each result carries a source tag drawn from a closed set: semantic, lexical, regex, or
literal. A semantic result that the lexical lane also surfaced is not retagged — instead it
carries hybrid_boosted: true plus a lexical_score alongside its semantic_score, so the
provenance of each result stays unambiguous. Lexical-only matches (files the embedding lane
missed but the trigram lane found by exact identifier hit) tag source: lexical and render with
[lexical match — score: <X>] instead of a symbol range. Indexes code extensions only; markdown,
HTML, and config files are intentionally excluded — they crowd out real code matches. Use grep
for prose.
Install ONNX Runtime:
- macOS:
brew install onnxruntime - Linux (Debian/Ubuntu):
apt install libonnxruntime - Linux (other): Download from ONNX Runtime releases
- Windows:
winget install Microsoft.ONNXRuntime
Without ONNX Runtime, all other AFT tools work normally — only aft_search is unavailable.
{ "query": "authentication middleware that validates JWT tokens" }Returns ranked results with relevance scores, provenance tags, and code snippets:
crates/aft/src/commands/configure.rs
handle_configure [function] lines 17-253 score 0.648 source semantic (hybrid_boosted)
pub fn handle_configure(req: &RawRequest, ctx: &AppContext) -> Response {
let root = match req.params.get("project_root")...
...
packages/opencode-plugin/src/bridge.ts
checkVersion [method] lines 150-175 score 0.482 source semantic
private async checkVersion(): Promise<void> {
...
packages/pi-plugin/src/commands/aft-status.ts
aft-status [file-summary] [file summary] score 0.504 source semantic
/**
* /aft-status — show AFT status (version, indexes, LSP, storage).
Found 10 semantic result(s). [index: ready]
The index is built in a background thread at session start, persisted to disk for fast cold start, and uses cAST-style enrichment (file path + kind + name + signature + body snippet) for better embedding quality. Files with ≤2 top-level exports additionally produce a synthetic "file-summary" chunk that captures filename, parent directory, leading doc comment, and export list — this lifts recall for filename-shaped concept queries like "the bridge spawn helper".
Parameters: query (natural language description, or a single auto-routed string), pattern
(a regex for names or text that must appear), topK (optional — default 10), path (optional —
search a different project root, see below). At least one of query and pattern is required;
an empty or whitespace pattern counts as absent.
Query and pattern together. When you know a name the answer must contain but also want the file that explains a concept, give both:
{ "query": "how is the config file loaded", "pattern": "load_config|ConfigLoader" }pattern uses grep's regex syntax and is case-sensitive, like grep. The ranking starts from
exactly the ranking query alone would return, and the pattern only adds bounded evidence to
it. Each top-level alternative of the pattern (a|b, and the branches of a single group such as
^pub struct (A|B)) is judged on its own; one that matches more than 50 files, or that nothing
declares, adds nothing. A leading result moves up a little for its own lines that match a
selective alternative, a little more if it declares the name. An alternative's definition is
placed right after the best-placed leading result that mentions the name (a caller always
does), never above it, and marked with supports. With no such result, the definition is added
only when the query has no semantic lane (identifier-shaped prose, or the semantic index is
still building) or when it is relevant to the query on its own (semantic or lexical score);
otherwise the summary line still names it. The reply
opens with a pattern summary line: how many files matched, how many
the query also found, up to three definition sites (or "no definition found"), and "examined N
of M candidate files" when the pattern's examination hit its bound. Each result carries
matched_by (query, pattern or both). A reply is marked incomplete when either input
was bounded. An invalid regex is refused with grep's invalid_pattern error and position.
pattern alone ranks the files it matches, definitions first, one result per file. pattern
cannot yet be combined with path.
Pass path to search another project or plain folder on the same machine — a sibling
repository, a worktree, or a directory that is not in Git:
{ "query": "how does the sidebar discover the RPC endpoint", "path": "~/Work/other-project" }How it behaves:
- Read-only borrow. The session never mutates the other project's caches: indexes are opened with strict read-only loaders, no rebuild or re-embed is triggered, and the other project's watcher/ownership state is untouched. The owning session keeps exclusive write access.
- Served with drift disclosure. The borrowed index reflects the owner's last persisted
state. If files changed since (or the checkouts differ in untracked ignore files), results
are still served and the response carries a note —
borrowed, drift count, andignore_rules_differ— with a hint to grep the target root when exact line numbers matter. Owners flush their index on clean shutdown, so drift is normally the owner's uncommitted working set since it last ran. - Semantic lane requires a matching embedding backend. A borrowed semantic index is only used when its embedding fingerprint matches your session's backend; otherwise the search degrades to the lexical lane for that query.
- Nonexistent paths fail loud with
path_not_foundinstead of walking up to an unrelated parent. - Unindexed folders. If there is no index to borrow, including for a plain non-Git
directory, search runs the same bounded lexical scan and labels the reply unindexed.
The scan honours
.gitignoreand.aftignorefiles in the tree and reports its coverage; it never builds an index or starts a watcher. Its existing file and time limits still apply, so use grep with path when an exhaustive check is needed. A plain folder can be searched even when Git is not installed; repository metadata that Git cannot read still causes a resolution error rather than silently searching a different scope. - Choose a narrow folder. Filesystem and volume roots, the home directory, and ancestors of home are refused for an unindexed plain-folder scan. Use a narrower path or grep with path instead.
Under restrict_to_project_root: true (and always for untrusted MCP callers), cross-project
search outside the session's root is denied.
aft_search supports three embedding backends. Set them under the semantic block in your
user-level AFT config (~/.config/cortexkit/aft.jsonc).
Trust boundary:
backend,base_url, andapi_key_envare user-only. Project-levelaft.jsoncfiles cannot inject these — a hostile repository cannot point your embeddings at an attacker-controlled endpoint or steal your API keys. Project config can still tunemodel,timeout_ms, andmax_batch_size.
1. fastembed (default) — local ONNX Runtime, no network, no API key. Uses
all-MiniLM-L6-v2 (384 dims, ~22MB downloaded on first use). Works fully offline.
{
"indexes": { "semantic": true }
// No "semantic" block needed — fastembed is the default.
}2. openai_compatible — any OpenAI-compatible /v1/embeddings endpoint. Works with
OpenAI, Together, Voyage, Anyscale, Fireworks, vLLM, LM Studio, etc.
{
"indexes": { "semantic": true },
"semantic": {
"backend": "openai_compatible",
"model": "text-embedding-3-small",
"base_url": "https://api.openai.com/v1",
"api_key_env": "OPENAI_API_KEY", // env var name, not the key itself
"timeout_ms": 25000, // optional, default 25000
"max_batch_size": 64 // optional, default 64
}
}The plugin reads the API key from the environment variable named in api_key_env at request
time. The key itself is never stored in config or logs.
3. ollama — self-hosted Ollama at its /api/embeddings endpoint. No API key required.
{
"indexes": { "semantic": true },
"semantic": {
"backend": "ollama",
"model": "nomic-embed-text",
"base_url": "http://127.0.0.1:11434"
}
}Choosing a backend:
| backend | when |
|---|---|
fastembed |
Default. Offline, free, zero setup beyond ONNX Runtime. Lower recall than larger models but good enough for most code search. |
openai_compatible |
You want higher recall (1536/3072-dim models), already pay for an embeddings API, or your repo is large enough that local CPU embedding is too slow. |
ollama |
You want a local self-hosted model larger than all-MiniLM-L6-v2 without paying per-token. |
Switching backends rebuilds the index. AFT stores a fingerprint
(backend, model, base_url, dimension, plus an internal chunking_version for the
synthetic file-summary chunk format) with every persisted index. Changing any fingerprint
field deletes the cached index on the next session start and rebuilds from scratch in the
background — necessary because different models produce different vector dimensions and
incompatible semantic spaces. For OpenAI-compatible backends on a large repo this can
mean hundreds of API calls and a few minutes of wall-clock time. aft_search returns
[index: building] while the rebuild runs; status is also visible via /aft-status and
the OpenCode TUI sidebar. First launch on AFT v0.23+ triggers a one-time rebuild
because chunking_version bumped to add file-summary chunks.
Switching API keys (rotating OPENAI_API_KEY without changing api_key_env) does not
trigger a rebuild — the key isn't part of the fingerprint.
Constraints:
base_urlmust behttp://orhttps://.- Loopback is allowed.
127.0.0.1,localhost, and*.localhostare accepted so self-hosted backends like Ollama work at their default config (http://127.0.0.1:11434). Loopback is by definition same-machine and not an SSRF target. - Non-loopback private/reserved IPs are rejected at configure time as an SSRF guard
against a malicious config redirecting embeddings to internal services. This includes
10/8, 172.16/12, 192.168/16, 169.254/16 (link-local), and 100.64/10 (CGNAT). mDNS
hostnames (
*.local) are also rejected. Users running self-hosted services on a LAN IP can either bind the service to loopback and use SSH/port-forward, or expose it on a public-routable interface. - The plugin retries failed HTTP requests with exponential backoff before giving up.
- Vector dimension is detected from the first response and validated on every subsequent insert; mismatches abort the build instead of silently corrupting the index.
Codebase-health snapshot in a single call. Returns summary stats for TODOs, file/symbol metrics, dead code, unused exports, and code duplicates. Use it when starting work in unfamiliar code, before a refactor or review, or to verify cleanup completeness.
// Summary across all active categories
{}
// Drill into specific categories with per-category detail
{ "sections": ["todos", "dead_code"], "topK": 20 }
// Restrict to a subtree
{ "sections": "duplicates", "scope": "crates/aft/src/inspect" }Categories run in two tiers:
- Tier 1 (
todos,metrics) — computed synchronously with a ~1s soft deadline. Always present in the response. - Tier 2 (
dead_code,unused_exports,duplicates) — heavier cross-file analyses backed by a callgraph snapshot. They run as background scans triggered on session idle. Anaft_inspectcall reads cached aggregates and returns immediately: a category that hasn't been scanned yet appears inpending_categories, and a category whose inputs changed since the last scan appears instale_categories. Tier 2 never blocks the call on a full scan.
Response shape:
{
"success": true,
"scanner_state": {
"disabled_categories": ["complexity", "circular_deps", "..."], // deferred to a later release
"pending_categories": ["dead_code", "unused_exports", "duplicates"],
"stale_categories": [],
"failed_categories": [],
"tier2_last_run": null
},
"summary": {
"metrics": { "files": 845, "loc": 318236, "symbols": 8490 },
"todos": { "count": 8, "by_kind": { "TODO": 3, "FIXME": 1, "BUG": 2, "HACK": 1, "XXX": 1 } },
"dead_code": { "count": 0, "by_language": {} },
"unused_exports": { "count": 0 },
"duplicates": { "count": 0, "total_groups": 0 }
},
"details": { /* present only for categories named in `sections` */ }
}Parameters: sections (string or array of category names, or "all"; omit for summary-only),
scope (file or directory to restrict results to — applied as a result filter), topK (max
drill-down items per category, default 20).
Every call ends in exactly one terminal result, named by inspect_terminal and rendered as the
first line of the tool output:
inspect_terminal |
Header | Meaning |
|---|---|---|
fresh |
FRESH |
Completed, and every diagnostics producer gave an authoritative answer. Carries wait_stamp (text and phases). |
partial |
PARTIAL — diagnostics unknown: rust-analyzer @ .: cargo check still running (1 file); retry aft_inspect. |
Completed with the same payload and wait_stamp as fresh, but some diagnostics are unknown. The header explains each analyzer's reason and affected file count once. partial_reason holds the header text without the PARTIAL — prefix. Full reasons and per-file gaps remain structured. |
interrupted |
INTERRUPTED — ... |
Cancelled before completion; retry the request. completed_phases remains in structured data. |
phase_failed |
PHASE-FAILED — ... |
Inspection could not finish; address the reported reason and retry or narrow the scope. completed_phases, failed_phase, failure_reason and failure_detail remain structured. |
FRESH never heads a result whose diagnostics summary reads diagnostics: unknown. A missing
language server binary (for example docker-langserver) leaves its files' diagnostics unknown and
the result partial; install the server or disable it with lsp.disabled to get fresh.
Wait stamps and phase lists are retained only in structured data, not agent-visible text.
For example, the structured wait stamp counts phases instead of listing each one:
waited: yes; completed: lsp_start ×9 (typescript 3, python 2, bash 1, ...), lsp_quiescence ×9 (...), tier2_rescan ×5 (...).
Repeated TypeScript runtime notes collapse the same way
(TypeScript 5.9.3: project installation ×3 (first: ...)).
With scope, all findings, counts, worst offenders, and examples are narrowed to those paths.
Duplicate groups can cross the boundary: their summary says how many groups touch the scope,
and their examples show only scoped occurrences. Project-wide duplicate percentages and
suppression totals are omitted because they cannot be narrowed from the cached aggregate.
Cross-boundary import cycles likewise show only scoped members and edges, labeled as cycles
touching the scope rather than claiming a closed cycle entirely within it.
Without scope, the repository-wide summaries are unchanged.
A blocking inspect walks the project (or the scope) and starts one server per (server, workspace
root) its files need, all at once, under one shared startup deadline. The walk honors .gitignore
and .aftignore, and skips dependency and build output directories, test-fixture directories
(fixtures, __fixtures__, testdata, test-data, __mocks__, __snapshots__, corpora) and
spikes/, unless the scope itself names one of them. Roots are shared where one server can serve
several packages: TypeScript packages that see the same installed TypeScript version share one
server, and Bash and YAML use the outermost root marker in the project. Rust uses the owning Cargo
workspace.
Registered on the recommended and all tiers; disable via inspect.enabled: false in config.
Delete one or more files (or directories) with per-file backups. Each file is backed up before
deletion and can be restored via aft_safety undo — one delete call is one undo operation, even
when it removes many files. Single-file callers pass a single-element array.
{ "files": ["src/deprecated/old-utils.ts"] }{ "files": ["dist/foo.js", "dist/bar.js", "dist/baz.js"] }Deleting a directory requires recursive: true. The tree is backed up before it is removed,
and one aft_safety undo restores it exactly:
- directories, including empty ones, with their permissions;
- file contents;
- hard links inside the tree, relinked so they share one file again;
- symlinks: the link itself is deleted and restored with its exact target text (also when dangling or pointing outside the tree); the target is never followed or touched.
Reported as warnings: sockets are deleted but not restored (they hold no data, and a recreated
one would have no process listening), and a file hard-linked to paths outside the tree comes
back as an independent copy. Refused before anything is deleted: a mount point of another
filesystem (removing it would delete that filesystem's contents), named pipes, device nodes,
and symlinks undo cannot recreate exactly (a non-UTF-8 target, or any symlink on Windows).
The delete removes only the entries it backed up, deepest first; if something new appears in
the tree meanwhile, it stops with partial: true, leaves the new entry in place, and undo
restores what was removed.
Paths under the system temp directory are never backed up, and neither is anything when
backups are disabled; such deletes skip these checks and the backup budget, and only a mount
point is refused. A recursive delete whose backup would record more than 2,000 entries (files,
directories and links) or copy 100 MiB in one call is refused; delete it in smaller pieces or
use bash rm -rf when no undo is needed.
{ "files": ["build/cache"], "recursive": true }Returns { success, complete, deleted: [paths], skipped_files: [{file, reason}] }. Partial
success is allowed: files that can be deleted are deleted; files that fail (missing,
permission denied, etc.) are reported in skipped_files and complete: false. If every
file fails the call throws an error.
Move or rename a file. Creates parent directories for the destination automatically. Falls back to copy+delete for cross-filesystem moves. Backs up the original before moving.
{ "path": "src/helpers.ts", "destination": "src/utils/helpers.ts" }Returns { file, destination, moved, backup_id } on success.
The symbol-relocation command moves a TypeScript/JavaScript symbol and rewrites its
consumers. It is separate from aft_move, which moves a whole file. Source, destination
and consumer writes are checkpointed; a write failure or syntax rollback restores the
operation and does not run the type checker.
With validate: "full" or validate_on_edit: "full", type checking runs only after every
file has been written and formatted, once per distinct checker and configured project
root. File arguments are batched where supported (Go named-file checks across directories
fall back to one run per file). Each existing per-file results entry carries its own
validation_errors array and optional validate_skipped_reason. The rendered output
contains one type check: … summary line, not a separate top-level diagnostic list.
Type errors report the completed project's state; they do not roll back an otherwise
successful move. Intermediate errors from a consumer's not-yet-rewritten import are not
reported.
Call graph and data-flow analysis across the workspace.
| Mode | What it does |
|---|---|
call_tree |
What does this function call? (forward, default depth 5) |
callers |
Where is this function called from? (reverse, default depth 1) |
trace_to |
How does execution reach this function from entry points? |
impact |
What callers are affected if this function changes? |
trace_to_symbol |
Shortest call path from one symbol to another. Needs toSymbol (and toPath to disambiguate). |
trace_data |
Follow a value through assignments and parameters. Needs expression. |
// Find everything that would break if processPayment changes
{
"op": "impact",
"path": "src/payments/processor.ts",
"symbol": "processPayment",
"depth": 3
}Language-aware import management for TS, JS, TSX, Python, Rust, Go, Solidity, Java, C#, PHP, Kotlin, Scala, Swift, Ruby, Lua, C, C++, Perl, and Vue.
// Add named imports with auto-grouping and deduplication
{
"op": "add",
"path": "src/api.ts",
"module": "react",
"names": ["useState", "useEffect"]
}
// Remove a single named import
{ "op": "remove", "path": "src/api.ts", "module": "react", "removeName": "useEffect" }
// Re-sort and deduplicate all imports by language convention
{ "op": "organize", "path": "src/api.ts" }Beyond module/names, add accepts language-appropriate fields: defaultImport and
namespace (ES import X, * as NS), alias (whole-module alias, e.g. Solidity import "./X.sol" as X),
typeOnly (TS), modifiers (statement modifiers such as Java/C# static, Swift @testable),
and importKind (kind-specific imports such as PHP function/const, Swift struct/func).
op: "remove" reports removed: false with a reason of module_not_found (the module
was never imported) or name_not_found (the module is imported but the named symbol isn't
in it) instead of pretending the removal succeeded. For languages whose grammar can't be
safely regenerated (wildcard/group/rename forms), organize sorts verbatim and refuses
rather than corrupting syntax; a generated line that fails to parse rolls back and reports
generated_invalid_syntax instead of a false success.
Backup and recovery for risky edits.
| Op | Description |
|---|---|
undo |
Undo the entire last tool call (omit path), or the last edit to one file (pass path) |
history |
List all edit snapshots for a file |
checkpoint |
Save a named snapshot; explicit files may be untracked or gitignored |
restore |
Restore files to a named checkpoint |
list |
List all available checkpoints |
purge |
Permanently delete this session's undo backups and checkpoint copies of path (a file, or everything under a directory, up to the project root); asks edit permission for the path and cannot be undone |
// Checkpoint before a multi-file refactor
{ "op": "checkpoint", "name": "before-auth-refactor" }
// Restore if something goes wrong
{ "op": "restore", "name": "before-auth-refactor" }Note: Backups are persisted to disk (SQLite-backed) and survive bridge and host restarts. Named checkpoints are stored on disk across bridge and daemon restarts, expire after 14 days, and may contain secrets. Undo is operation-scoped: a single multi-file delete, directory delete, file move, symbol move, or AST replace is reverted atomically by one
undowith nopath. Per-file undo stack is capped at 20 entries — oldest snapshots are evicted when exceeded. History, undo, and checkpoints are session-private even when multiple sessions share one project bridge.
{ "diagnostics": [{ "file", "line", "column", "end_line", "end_column", "severity", "message", "code" }], "total": 2, "files_with_errors": 1, "complete": true, // true = trustable absence of diagnostics; false = partial result "lsp_servers_used": [ // per-server status; empty array means nothing was checked { "id": "rust-analyzer", "status": "pull_ok" }, { "id": "bash-language-server", "status": "binary_not_installed" } ], "unchecked_files": [] // directory mode only — files we couldn't get info for }