Forward-looking ideas that aren't on the active roadmap yet. Items here are
exploratory — they should be promoted to TODO.md (or a design doc) before
implementation starts.
A project-level continuous health score, computed from the existing code index, that gives agents a gradient to optimise against during autonomous work (autopilot, swarm). Today aide emits findings as a flat list; agents can trivially "fix" one issue while making the project worse along another axis. A single ungameable aggregate, paired with pointer-quality diagnostics, closes that loop.
Compute a small set of normalised [0,1] graph-shape metrics over the code
index and aggregate them with a geometric mean (Nash Social Welfare).
Geometric mean is the anti-gaming property: improving one dimension while
tanking another cannot lift the aggregate, so an agent cannot "win" by
optimising one number.
All five are computable from data the index already has (symbols, imports, call edges, file/symbol size):
- Modularity — Newman's Q on the import graph. High Q ⇒ clean module boundaries.
- Acyclicity — Tarjan SCC count → sigmoid. Cycles are penalised hard.
- Depth — longest DAG path → sigmoid. Discourages deep dependency chains.
- Equality —
1 − Gini(cyclomatic complexity per symbol). Penalises "god functions" that concentrate complexity. - Redundancy —
1 − (dead + duplicate fraction). Already partially covered byfindings/deadcodeandfindings/clone.
- A
healthMCP tool returning{ score, bottleneck_dimension, dimensions{...}, diagnostics{god_files, hotspots, deep_chains, complexity_outliers, dead_groups, duplicate_groups} }. - Findings of class
architecture/{cycle, god_file, deep_chain, complexity_outlier}with file/symbol pointers, fed through the existing findings/triage flow. - A
health.toml(sibling to blueprints) for hard CI gates:max_cycles=0,min_modularity=0.4, layer/boundary deny rules. Violations become high-severity findings.
The actual feedback loop — and the reason this is worth building:
health_snapshotbefore a task batch / story.health_diffafter.- Block stage completion (or require an explicit
decisionapproving the regression) if the aggregate score dropped. This makes the agent responsible for not silently degrading architecture while chasing a green test suite.
~1–2 weeks on top of the existing code index if import resolution (below) is good enough that fan-in / cycles / modularity numbers aren't dominated by same-name collisions. Most of the data is there; the cost is metric math, normalisation, finding emitters, and the session-baseline plumbing. Without import resolution, modularity (Newman Q on the import graph) and acyclicity (SCCs) are the dimensions that degrade first.
- GUI / treemap visualisation. Run-on-demand + autopilot hook is enough.
- Real-time file-watcher mode. Aide's existing
aide:patternscadence is sufficient.
A natural extension of the current findings analyzers (complexity,
coupling, deadcode, clones, security, secrets, todos).
Heuristic: surface symbols with high fan-in (many callers) that have no covering test — i.e. the index has incoming call edges but no test-file caller in the reverse-call set.
Aide already has the inputs:
- Call edges from the code index (
pkg/code). - Test-file detection (the topology / classification pass already distinguishes test files per language).
Proposed shape:
- New analyzer
pkg/findings/testgap/registered alongside the others (mirrorsdeadcode.go/coupling.go). - New constant
AnalyzerTestGap = "testgap"inpkg/findings/types.go. - Severity scaled by fan-in: high fan-in untested symbol →
warning; exported + high fan-in untested →critical. - Finding metadata carries
fanIn,callers, and the symbol's qualified name so triage tooling can sort by impact.
This is small enough to ship independently of the broader health-signal work above and gives agents an immediate "what should I write a test for next?" signal.
Accuracy depends on import resolution (see next section). Without it, fan-in counts and "is any caller a test file?" both leak across same-named symbols. Acceptable for a first version (Aider's repo map ships with strictly no resolution and is still useful); document the caveat and sharpen as resolvers land per language.
Both items above (and several existing features — survey_graph,
code_references, dead-code accuracy) are bottlenecked on the same problem:
tree-sitter gives us the import statement's text, not what it resolves to.
This is well-trodden ground in the OSS code-intelligence world; the consensus
is that there is no language-agnostic shortcut, and every serious tool either
ships per-language resolvers or accepts acknowledged fuzziness.
Two camps, with one experimental third:
-
Real toolchain per language (precise). SCIP indexers (
scip-goshells togo list;scip-typescriptuses the TS Compiler API;scip-pythonembeds Pyright;scip-javauses SemanticDB). CodeQL, Kythe, Glean wrap real compilers.gopls/go-callvisusego/packages.dependency-cruiserandmadgeuse the real Node + tsconfig resolver. Same-name collisions are a non-issue because the compiler has already disambiguated. Cost: ship a build environment per language. -
Tree-sitter + heuristics (fuzzy, acknowledged). Aider's repo map does no resolution at all — global name match + PageRank + LLM tolerance. Pure AST tools (
pyan,snakefood,findimports) are best-effort. SLang-based Sonar analyzers and Semgrep OSS sit here too. This is where aide is today. -
tree-sitter-stack-graphs(declarative scope rules per language). GitHub's middle path. Production-deployed for "precise code nav" on Python and TS/JS, Java in beta. Solves same-name collisions via scope-stack lookup, but only as faithfully as the per-language binding rules are written.
There is no reusable cross-language import resolver. Even hub architectures (Kythe, Glean) only standardise the fact schema, not the resolution logic.
-
Path-aware import edges (cheapest, biggest win). Parse build files —
go.mod,Cargo.toml,tsconfig.jsonpaths,package.json,pyproject.toml— to map import-string → repo subtree. Resolve a call reference by intersecting the caller's import set (via the resolver) with the candidate target's package directory. ~1 week per language, no runtime toolchain dep, kills the cross-package collision case for fan-in/modularity/cycles. Aide'spkg/surveyalready detects most of these manifests during the topology pass. -
Adopt
tree-sitter-stack-graphsfor Python and TS/JS. Inherit GitHub's binding rules. Heaviest payoff in the languages where build-file parsing is hardest (Python's__init__.pyre-exports, TS'spathsaliases layered on Node resolution). Adds a Rust runtime dependency (the engine is Rust); we'd FFI or shell out. -
Opportunistic toolchain shell-out. When
go list/tsc/pyrightis on PATH and the project uses it, ingest its output (SCIP if available) for precise edges; fall back to options 1+2 otherwise. This is the model GitHub's precise code nav uses.
This work is also TODO.md §5's actual core — "implement import resolution
per language" — restated with field context. Item §5 step 1 (@qualifier
captures in tree-sitter ref queries) is still useful: it eliminates the
remaining ambiguity in the one case path-aware resolution can't solve
(a single file imports two same-named-export packages), and it captures
import aliases.
For both the Architectural Health Signal and Test-Gap Detection, ship on option-1 resolution for the languages it covers (start with Go + Rust), fall back to current name-matching elsewhere with a documented caveat, and progressively replace fallbacks as resolvers land. Don't gate either feature on full multi-language resolution.
Substrate (proposal store, broadcaster, MCP tools, CLI, aide-web page,
Stop hook) is live. Two ship-first parsers (repetition, convergence)
are in pkg/instinct/. The original design called for four; these
three small follow-ups close out the catalogue and surface:
-
same-questionparser. UserPromptSubmit text Jaccard-similar (shingled token n-grams, no embeddings) to N earlier prompts across distinct sessions. Defaults:n_propose=3,jaccard_threshold=0.4,min_distinct_sessions=3,shingle_size=3,min_prompt_tokens=5. Pure structural matching; no per-language reasoning. -
tool-flailingparser. Window of N consecutive read-only tool calls (code_search,Grep,Read, MCP code tools) with token overlap on their query strings, ending without an Edit. Defaults:window_size=5,flail_tolerance=3,reset_on_edit=true,n_propose=3. Promotion suggests a known entry point for the recurring cluster of terms. -
Session-start instinct nudge. Extend
src/hooks/session-start.tsso that if there are open proposals, the injected context carries a one-line line: "N instinct proposals waiting — run/recall instincts, open the aide-web Instincts page, or callinstinct_proposals_list." Bounded; must never block start.
Each is small (parser ~150 LOC + config struct + tests; nudge is a
~20-line addition to session-start). The deferred parsers from the
original design (revert, friction) stay deferred — they need a diff
comparator or a corrective-marker model that's high-false-positive.
design/event-streaming.md listed WatchFindings and WatchDecisions
in its per-domain table but neither shipped because no page needs them
yet. They're cheap (broadcaster instance + RPC + handler + bus call on
each write — ~80 LOC each) and unblock live updates on the Findings and
Decisions pages whenever those grow a live-tail toggle.
The friction instinct detector reads observe.Event.Error, populated when a
tool call fails. On Claude Code this works via the PostToolUseFailure hook,
and Codex mirrors that registration. OpenCode has no equivalent: its plugin
API exposes only tool.execute.after, which fires on success — when a native
tool throws, no hook runs, so the failure is invisible to aide.
There is an open OpenCode feature request to add a tool.execute.error hook
(anomalyco/opencode#10027). Until it lands, native-tool friction is uncapturable
under OpenCode. (MCP-tool failures are unaffected — the Go MCP middleware sets
Error directly, independent of the harness.)
When the upstream hook ships: register it in src/opencode/hooks.ts and route
its error payload through recordToolEvent({ errorText }), mirroring the
Claude Code PostToolUseFailure handler in src/hooks/tool-observe.ts.