Imported from eshu-hq/eshu (
go/cmd/eshu/AGENTS.md). Install upstream withnpx skills add eshu-hq/eshu --skill eshu. Copyright stays with the author.
AGENTS.md — cmd/eshu guidance for LLM assistants
Read first
go/cmd/eshu/README.md— binary purpose, subcommand groups, configuration, and evidence markers. Its "Gotchas / invariants" section routes to three sibling docs that hold the per-command contracts:gotchas-onboarding-and-dogfood.md,gotchas-read-surface-commands.md, andgotchas-local-runtime-and-graph.mdgo/cmd/eshu/root.go—rootCmd, persistent flags (--database,--visual), and root subcommand registrationgo/cmd/eshu/service.go—runMCPStart,runAPIStart,procexec.Exec,procexec.Executable; how the binary execs runtime processesgo/cmd/eshu/basic.go— indexing subcommands (index,list,watch,query,stats);runIndexdelegates toeshu-bootstrap-indexviaindexLookPathgo/cmd/eshu/graph.go— thegraphandinstallsubcommand trees and theirRunEs (runGraphStatus,runGraphStart,runGraphStop,runGraphLogs,runGraphUpgrade). The status, stop, logs, and upgrade logic itself is ingo/internal/cli/localsupervisor.go/cmd/eshu/local_host.go— the hiddenlocal-hostsupervisor entry point. Registration and signal handling only; the supervisor isgo/internal/cli/localsupervisor.go/internal/cli/procexec— the shared re-exec seam:procexec.Executable,procexec.Getwd,procexec.LookPath,procexec.Exec,procexec.Environ,procexec.CleanExecutableArg0,procexec.MergeEnvironment.eshu mcp start(both the stdio and the HTTP path),eshu graph start, andeshu watchhand the process over through it, which is what lets their tests substitute the seams instead of losing the test process to a realsyscall.Exec. Three re-exec paths do not, so do not assume you can stub them the same way:eshu api startandeshu servecallsyscall.Execdirectly inservice.gowithexec.LookPathandos.Environ, andeshu indexgoes through its ownindexLookPath/indexExecpair inbasic.go, whichbasic_test.gosubstitutes instead. Route a new re-exec throughprocexec; itsAGENTS.mdcarries the substitution rules tests must followgo/internal/cli/— where command logic that is not process wiring lives, because this directory ispackage main.eshu report's digest and artifact logic is ingo/internal/cli/opdigest; the local Eshu service and everyeshu graphsubcommand's logic is ingo/internal/cli/localsupervisor; the repository-selector matching rules behind theanalyzefamily's--repoandvuln-scan repo's scanned root are ingo/internal/cli/reposelector; thevuln-scan reporun itself — scan, resolution, findings read, guards, output document — isvulnscan.RunRepoingo/internal/cli/vulnscan, andvuln_scan.gokeeps only the flags, the local-runtime decision, the API client, and the*vulnscan.FailuretocommandExitErrormapping. Read the target package'sAGENTS.mdbefore changing the wrapper that calls it.
Invariants this package enforces
-
SilenceUsageandSilenceErrors— both set totrueon therootCmdliteral inroot.go, so Cobra does not print usage on every error. Removing either breaks operator scripts that parsestderr. -
--databasemutates the process environment — thePersistentPreRunEon that samerootCmdliteral callsos.Setenv("ESHU_RUNTIME_DB_TYPE", globalDatabase). This affects every child process exec'd in the same process. -
Service-launch replaces the process image —
eshu mcp start(both paths),eshu api start,eshu serve,eshu graph start,eshu watch, andeshu indexall reachsyscall.Exec, so no Eshu logic runs after the exec point: nothing deferred, no flush, no cleanup. Anything the operator must see has to be written before the call. Which route they take differs and matters when you write a test —procexec.Execformcp start(service.go),graph start(graph.go), andwatch(basic.go); a baresyscall.Execforapi startandserve(service.go); the localindexExecvar forindex(basic.go). -
Flag names printed by an
internal/clipackage are declared there — theeshu componentfamily's--instance,--version,--id,--publisher, and--fact-kindappear in--<flag> is requirederrors raised insidego/internal/cli/component, so that package declares them (clicomponent.InstanceFlagand friends) andcomponent.goregisters those constants instead of its own. The remaining component flag names, which no package outside this binary prints, stay in theconstblock incomponent.go. Re-adding a local constant for one of the five puts the same string under two owners again, which is what the extraction review caught. -
Selector flags are declared per command, read centrally, matched elsewhere — each command file declares its own
--repo/--repo-id(analyze.go:96,analyze.go:315).repository_selector.goowns only the reading:readRepositorySelectorFlagplus the--repo-idshort-circuit that skips resolution when the caller already holds an exact ID. What a selector actually matches — exact on ID/name/slug, canonicalized and symlink-resolved on the path fields — belongs togo/internal/cli/reposelector, which holds no cobra. So a new selector form goes in that package, a new selector flag goes in the command's own file, and neither goes inrepository_selector.go.Not every
--repoin this binary is that flag.map.go:33,trace.go:43anddocs.go:64declare a--repothey hand to the API unresolved, andhosted.go:159takes exactowner/namevalues. Those never reachreposelector; do not "fix" them to route through it without deciding that server-side resolution should move client-side. -
Removed commands use
removedCommandError— deprecated and removed commands (delete,clean,unwatch,add-package,finalize) callremovedCommandErrorincontract.goinstead of silently succeeding or panicking. Any new removal must follow this pattern.
Common changes and how to scope them
-
Add a new
adminsubcommand → put the request shaping (endpoint, request body, validation) ingo/internal/cli/admin, then add acobra.Commandinadmin.go, wire it toadminCmdoradminFactsCmd, and have itsRunEread the flags, callapiClientFromCmd, andprintJSONthe result. Why:admin.goowns the full admin subcommand tree, so scattering admin commands into other files makes auditing harder; the endpoint and body are the decision worth testing and live outsidepackage main(issue #6059, epic #6053). -
Add a new
graphsubcommand → add acobra.Commandtograph.go'sinit()and add itsrun*func in the same file, but put the behaviour it invokes ingo/internal/cli/localsupervisor. Why: thegraphsubcommand tree is fully wired ingraph.goand thegraphCmdvar is defined there, while everything that is not flag reading, printing, or the exit-code contract belongs in a package that can be imported and tested. -
Add a new persistent flag → add it in
root.goand thread it throughPersistentPreRunEif it affects child-process env. Why: persistent flags apply to all subcommands; adding them only in a leaf file makes them invisible to sibling commands. -
Add a new local-host subcommand → add a
cobra.Commandinside theinit()inlocal_host.go; keep the commandHidden: true, and put the supervision behaviour ingo/internal/cli/localsupervisor. Why:local-hostis the internal supervisor entry point, not a public user command, andeshu watchreaches it through asyscall.Execstring argument (basic.go) that no symbol search can see.
Failure modes and how to debug
-
Symptom:
eshu mcp startprintseshu-mcp-server binary not found in PATH→ cause:exec.LookPath("eshu-mcp-server")failed; rebuild withcd go && go build -o bin/ ./cmd/mcp-server/and addgo/bintoPATH. -
Symptom:
eshu indexprintseshu-bootstrap-index binary not found in PATH→ cause:indexLookPath("eshu-bootstrap-index")failed; rebuild./cmd/bootstrap-index/and ensurego/binis onPATH. -
Symptom:
eshu graph startstarts a process but the graph does not come up → cause:eshu-reduceroreshu-ingesterare not onPATH; thelocal-host watchsupervisor discovers them throughPATH. Rebuild all binaries and checkPATHbefore running. -
Symptom:
eshu graph startappears noisy in local foreground mode → cause: child service logs are being routed to the terminal with--verboseor--logs terminal. Default local runs should keepeshu-ingester.logandeshu-reducer.logunder the workspace log directory and leave the terminal to the branded Bubble Tea known-work progress panel. The verdict line is the primary operator signal:Completemeans all known work drained,Indexingmeans pending collector generations or active work remain,Settlingmeans queued work or shared projection backlog remains, andAttentionmeans a failure/dead-letter path is present. The collector row treatsscope_generations.status='active'as the current snapshot, not a running worker; only pending generations should keep the collector waiting. When queue counters are zero but readiness is stillprogressing, check theShared projectionsline before assuming the panel is stale. Use--progress plainwhen testing append-only output and--progress quietonly when another wrapper owns progress display. -
Symptom: the progress table is healthy but stays at
idleafter a local-authoritative restart → first check whethercache/reposwas reset. A stale filesystem selector manifest can make the ingester skip collection against fresh Postgres state, soresetLocalAuthoritativeStatemust remove that directory while preserving embedded Postgres binaries. -
Symptom:
eshu graph statusreportsowner_present=truebut the owner PID is already dead → cause: staleowner.json;eshu graph stopmust acquireowner.lock, stop any recorded embedded Postgres child, and remove the stale metadata for bothlocal_lightweightandlocal_authoritativeprofiles. -
Symptom:
local_authoritativespends minutes on old projector work or stale graph retraction after restart → cause: rebuildable local state was preserved across owner starts;local_host_reset.gomust clear Postgresdata/runtimeand graphnornicdbstate afterowner.lockacquisition and before embedded Postgres starts. -
Symptom: a
eshu admincommand returns a non-200 response → cause: theAPIClienttarget URL is wrong or the API server is down; check the service URL config and thateshu api startis running.
Anti-patterns specific to this package
-
Business logic in subcommand
RunEfunctions —RunEfunctions should callapiClientFromCmd,procexec.Exec, or a delegating helper. Domain logic (graph writes, fact queries, schema checks) belongs in theinternal/*packages that own those surfaces.go/internal/cli/<family>is where a command family's own logic goes when no other package owns it — seeinternal/cli/adminandinternal/cli/mcpsetup. This package ispackage mainand cannot grow subdirectories, so that is the only place it can go. -
Direct driver or Postgres calls in this package — this binary is a CLI dispatcher. It must not open Postgres or graph driver connections except through
internal/runtimehelpers already used here. All data-plane work runs in the launched binaries. -
Reaching
rootCmdfrom a test withoutlockCommandTree(t)— every command here is a package-level singleton wired together byinit(), and cobra has no read-only accessor for the resulting tree:Find,Execute,Help,Commands, andFlagsall memoize state in place.Findmerges persistent flag sets on the way down and writes through to the root even when it starts at a subcommand, so the whole tree is one conflict domain and two parallel tests resolving a path at once is a data race. CalllockCommandTree(t)once at the top of any test that reaches the tree (aftert.Parallel());TestSharedCommandTreeAccessIsGuardedfails the build if a test file skips it. Do not "fix" a race here by droppingt.Parallel()— that hides the shared mutable state for the next caller instead of synchronizing it.command_tree_test.gohas the mechanism. -
Adding a hidden command without tests — hidden
local-hostsubcommands have integration-level tests inlocal_host_supervision_test.goandservice_local_test.go. New hidden commands need coverage before merging.
What NOT to change without an ADR
- The
local-host watchandlocal-host mcp-stdiosubcommand contract — theeshu mcp startandeshu graph startpaths hard-code these subcommand names when callingprocexec.Exec; renaming them silently breaks both flows. - The
--databaseflag name and its effect onESHU_RUNTIME_DB_TYPE— external scripts and the local-authoritative profile depend on this flag; seedocs/public/reference/cli-reference.md.