Documentation
¶
Overview ¶
Package investigate contains the env-var contract between `entire investigate` (which spawns the agent process) and the lifecycle hook (which adopts the session), plus the persisted run state for resuming an investigation. These names are stable API; renaming any constant is a breaking change.
Design rationale: each spawned agent inherits its own copy of the process environment, so multi-tenant correctness (multiple worktrees, multi-agent runs) holds by construction — one agent's env vars cannot bleed into another agent's session. The lifecycle UserPromptSubmit hook reads these env vars to tag the in-flight session as an investigate session (Kind = "agent_investigate") and records the run id + topic.
Index ¶
- Constants
- Variables
- func AppendInvestigateEnv(base []string, opts AppendOptions) []string
- func ComposeInvestigatePrompt(in ComposeInput) string
- func ConfirmFirstRunSetup(ctx context.Context, out io.Writer) bool
- func DeriveTopicFromSeed(body []byte, fallbackFilename string) string
- func IsInvestigateEnvEntry(kv string) bool
- func IsValidRunID(runID string) bool
- func NewCommand(deps Deps) *cobra.Command
- func PrintInvestigateFindingsListForTest(w io.Writer, manifests []LocalManifest)
- func RunClean(ctx context.Context, in CleanInput, deps CleanDeps) error
- func RunFix(ctx context.Context, in FixInput, deps FixDeps) error
- func RunInvestigateConfigPicker(ctx context.Context, out io.Writer, ...) (*settings.InvestigateConfig, error)
- func RunShow(ctx context.Context, in ShowInput, deps ShowDeps) error
- func SetPickerFormFnForTest(fn pickerFormFn) func()
- func SlugifyTopic(topic string) string
- type AgentChoice
- type AppendOptions
- type BootstrapInput
- type BootstrapResult
- type CleanDeps
- type CleanInput
- type ComposeInput
- type Deps
- type Files
- type FixDeps
- type FixInput
- type IssueLinkResult
- type LocalManifest
- type LocalManifestStore
- func (s *LocalManifestStore) FindByRunID(ctx context.Context, runID string) (LocalManifest, bool, error)
- func (s *LocalManifestStore) Latest(ctx context.Context) (LocalManifest, bool, error)
- func (s *LocalManifestStore) List(ctx context.Context) ([]LocalManifest, error)
- func (s *LocalManifestStore) PathFor(m LocalManifest) string
- func (s *LocalManifestStore) Write(ctx context.Context, m LocalManifest) error
- type LoopDeps
- type LoopInput
- type LoopOutcome
- type LoopResult
- type PendingTurn
- type PickedInvestigate
- type ProgressSink
- type RunState
- type ShowDeps
- type ShowInput
- type StateStore
- func (s *StateStore) Clear(ctx context.Context, runID string) error
- func (s *StateStore) List(ctx context.Context) ([]*RunState, error)
- func (s *StateStore) Load(ctx context.Context, runID string) (*RunState, error)
- func (s *StateStore) RunDir(runID string) string
- func (s *StateStore) Save(ctx context.Context, st *RunState) error
- type TurnStance
Constants ¶
const ( EnvSession = provenance.InvestigateSession EnvAgent = provenance.InvestigateAgent EnvRunID = provenance.InvestigateRunID EnvTopic = provenance.InvestigateTopic EnvFindingsDoc = provenance.InvestigateFindingsDoc EnvStateDoc = provenance.InvestigateStateDoc EnvStartingSHA = provenance.InvestigateStartingSHA )
Investigate env vars. Names live in cmd/entire/cli/provenance; aliased here for the package's call sites.
const InvestigationsDirName = "entire-investigations"
InvestigationsDirName is the directory name (under git common dir) where investigation runs persist their per-run artifacts (findings.md + state.json).
Variables ¶
var ErrGhNotFound = errors.New("gh CLI not found on PATH")
ErrGhNotFound is returned when ResolveIssueLink cannot find the gh CLI on PATH. Callers (and tests) can match on this sentinel via errors.Is.
var ErrInvestigateNoAgentsSelected = errors.New("no agents selected for investigation")
ErrInvestigateNoAgentsSelected is returned when the user unchecks all agents.
var ErrInvestigatePickerCancelled = errors.New("investigate agent picker cancelled")
ErrInvestigatePickerCancelled is returned when the user aborts the multi-select.
Functions ¶
func AppendInvestigateEnv ¶
func AppendInvestigateEnv(base []string, opts AppendOptions) []string
AppendInvestigateEnv adds the ENTIRE_INVESTIGATE_* env vars to base, returning the new slice. Used by the loop driver when spawning each per-turn agent process to propagate the investigate-session contract.
Any pre-existing ENTIRE_INVESTIGATE_* AND ENTIRE_REVIEW_* entries in base are stripped before the new values are appended. Stripping investigate entries handles nested invocations and stale inheritance from a parent shell — duplicate keys would otherwise have implementation-defined precedence. Stripping review entries prevents an outer `entire review` session from mis-tagging a child investigate session if invoked nested.
func ComposeInvestigatePrompt ¶
func ComposeInvestigatePrompt(in ComposeInput) string
ComposeInvestigatePrompt renders the full prompt sent to one agent for one turn of an investigate run.
The findings doc is a shared living document. Each agent appends findings, evidence, and analysis until the team reaches quorum. The agent records its stance by writing the `pending_turn` field of Files.State to a JSON object of the form {"stance":"approve|request-changes|reject","note":"<one-line>"}. The agent must not modify any other field of state.json.
func ConfirmFirstRunSetup ¶
ConfirmFirstRunSetup prints a banner framing the picker as first-run setup (rather than the investigation itself) and waits for the user to confirm.
func DeriveTopicFromSeed ¶
DeriveTopicFromSeed extracts a human-readable topic from a seed-doc body. Order of precedence:
- The first `# Investigation: <topic>` line — the scaffold's own title format. Round-trips a finished findings doc cleanly.
- The first markdown H1 (`# anything`).
- fallbackFilename without its extension.
func IsInvestigateEnvEntry ¶
IsInvestigateEnvEntry reports whether kv is a "KEY=VALUE" entry whose key is one of the ENTIRE_INVESTIGATE_* contract variables.
func IsValidRunID ¶
IsValidRunID reports whether runID matches the 12-lowercase-hex format. Delegates to provenance.IsValidRunID — the canonical validator lives alongside the env-var contract it's most often paired with.
func NewCommand ¶
NewCommand returns the `entire investigate` cobra command wired with the provided deps.
func PrintInvestigateFindingsListForTest ¶
func PrintInvestigateFindingsListForTest(w io.Writer, manifests []LocalManifest)
PrintInvestigateFindingsListForTest exposes printInvestigateFindingsList to tests in package investigate_test.
func RunClean ¶
func RunClean(ctx context.Context, in CleanInput, deps CleanDeps) error
RunClean implements `entire investigate clean`.
func RunFix ¶
RunFix resolves a saved investigation, composes the follow-up prompt, and launches a coding agent session via deps.Launch.
The prompt body says "use these findings as grounded context, do not re-investigate". The composed prompt embeds the findings doc verbatim so the agent has full access without needing to re-read disk.
func RunInvestigateConfigPicker ¶
func RunInvestigateConfigPicker( ctx context.Context, out io.Writer, spawnerFor func(agentName string) spawn.Spawner, getAgentsWithHooksInstalled func(ctx context.Context) []types.AgentName, ) (*settings.InvestigateConfig, error)
RunInvestigateConfigPicker shows a multi-select of eligible agents and prompts for max-turns / quorum. Returns a populated InvestigateConfig the caller can persist via settings.Save.
Eligibility: agent has a non-nil Spawner AND has hooks installed. Non-spawnable agents (cursor, opencode, factoryai-droid, copilot-cli) are filtered out at the SpawnerFor check.
func RunShow ¶
RunShow prints the saved investigation summary + findings for the requested run id. Resolution rules:
- empty RunID + exactly one manifest → use that manifest
- empty RunID + multiple manifests → list candidates, return error
- non-empty RunID: exact match wins; otherwise unique-prefix match; otherwise return an "ambiguous" or "not found" error
Findings come from manifest.FindingsContent when present (terminal outcomes), or by reading manifest.FindingsDoc from disk (paused / cancelled runs whose per-run dir still exists). Both paths missing is a soft state — the header is printed with an explanatory line.
func SetPickerFormFnForTest ¶
func SetPickerFormFnForTest(fn pickerFormFn) func()
SetPickerFormFnForTest swaps the picker form function. Returns a cleanup function the caller must defer to restore the previous value.
func SlugifyTopic ¶
SlugifyTopic converts an arbitrary topic string into a filesystem-safe slug. Result is lowercase, ASCII-alphanumeric with single dashes, no leading or trailing dash, and no longer than 60 characters. Empty/non-mappable input returns "investigation".
Types ¶
type AgentChoice ¶
AgentChoice is one row in the investigate picker. Name is the agent registry key (used for spawning); Label is the picker-visible string.
type AppendOptions ¶
type AppendOptions struct {
AgentName string
RunID string
Topic string
FindingsDoc string
StateDoc string
StartingSHA string
}
AppendOptions carries the data needed to populate the ENTIRE_INVESTIGATE_* env vars on a spawned agent process.
type BootstrapInput ¶
type BootstrapInput struct {
// SeedDoc is the absolute path to a user-provided seed file. Empty
// when no seed was passed.
SeedDoc string
// Topic is the topic-only investigation prompt collected from the
// spawn-time multipicker (set when neither SeedDoc nor IssueLinkSeed
// is supplied). Empty otherwise.
Topic string
// IssueLinkSeed is the markdown bytes produced by ResolveIssueLink.
// Empty when --issue-link was not used.
IssueLinkSeed []byte
// IssueLinkTopic is the topic derived from the resolved issue/PR
// title. Used only when IssueLinkSeed is non-empty.
IssueLinkTopic string
// FindingsDoc is the absolute path the findings doc must be written
// to.
FindingsDoc string
}
BootstrapInput carries the data needed to produce the initial findings doc on disk.
Exactly one of SeedDoc / Topic / IssueLinkSeed must be set:
- SeedDoc: the user passed a positional [seed-doc] path; render the scaffold and embed the seed bytes under the `## Question` section. Topic is derived from the body (or filename).
- Topic only: the user supplied the investigation prompt via the spawn-time multipicker (no seed, no issue link); render the scaffold with the topic printed under `## Question`.
- IssueLinkSeed: the user passed --issue-link; ResolveIssueLink already produced a markdown body — render the scaffold and embed those bytes under `## Question`, using IssueLinkTopic as the topic.
type BootstrapResult ¶
type BootstrapResult struct {
// Topic is the resolved topic — used downstream for slug derivation,
// manifest entries, and prompt rendering.
Topic string
// FindingsDoc is the absolute path the findings doc was written to
// (echoes BootstrapInput.FindingsDoc).
FindingsDoc string
}
BootstrapResult reports what was produced.
func Bootstrap ¶
func Bootstrap(ctx context.Context, in BootstrapInput) (BootstrapResult, error)
Bootstrap writes the initial findings doc to disk.
File-write semantics: creates parent directories as needed and writes the findings file unconditionally. Callers that want "skip if findings doc exists" semantics should stat the path themselves; Bootstrap is idempotent at the byte level (same input → same output) but does not protect existing files — protecting an existing investigation belongs to a layer above this one.
type CleanDeps ¶
type CleanDeps struct {
ManifestStore *LocalManifestStore
// RunDir returns the per-run directory path for a given run id. In
// production this is StateStore.RunDir; tests inject a fake.
RunDir func(runID string) string
// ManifestPath returns the on-disk path for a manifest. In
// production this is LocalManifestStore.PathFor(m).
ManifestPath func(m LocalManifest) string
// Confirm prompts the user with the given message and returns the
// y/N answer. Nil → real huh-backed prompt (use newAccessibleForm).
Confirm func(ctx context.Context, message string) (bool, error)
}
CleanDeps is what RunClean needs that's test-injectable.
type CleanInput ¶
type CleanInput struct {
// RunID, when non-empty, targets one run via exact-match-then-
// unique-prefix. Ignored when All is true.
RunID string
// All targets every investigation found by the manifest store.
All bool
// Force skips the confirmation prompt.
Force bool
// Out / ErrOut sink the operator-facing output.
Out io.Writer
ErrOut io.Writer
}
CleanInput drives RunClean.
type ComposeInput ¶
type ComposeInput struct {
// Topic is the human-readable subject of the investigation. Used in
// the body of the prompt as plain text — never as a section heading,
// since the rendered findings doc owns that.
Topic string
// AgentName is the agent the prompt is being rendered for (e.g.
// "claude-code").
AgentName string
// Round is the 1-indexed round number in the loop.
Round int
// MaxTurns is the per-agent turn budget (the "of N" half of
// "Round X of N").
MaxTurns int
// Turn is the 1-indexed overall turn number across rounds.
Turn int
// AlwaysPrompt, if non-empty, is appended verbatim at the end of the
// rendered prompt. Lets users inject project-specific guardrails into
// every turn via settings.
AlwaysPrompt string
// Files holds the findings + state absolute paths the agent must
// read and edit.
Files Files
}
ComposeInput is the per-turn data needed to render an investigate prompt. Intentionally narrow: the loop driver passes only what the prompt template uses.
type Deps ¶
type Deps struct {
// GetAgentsWithHooksInstalled returns the registry names of all agents
// whose lifecycle hooks are installed in the current repo.
GetAgentsWithHooksInstalled func(ctx context.Context) []types.AgentName
// NewSilentError wraps an error so the cobra root does not double-print
// it.
NewSilentError func(err error) error
// SpawnerFor maps an agent name → Spawner (claude-code, codex,
// gemini-cli). Returns nil for non-launchable agents.
SpawnerFor func(agentName string) spawn.Spawner
// LaunchFix delegates to agentlaunch.LaunchFixAgent in production.
LaunchFix func(ctx context.Context, agentName string, prompt string) error
// LoopRun, when non-nil, replaces RunInvestigateLoop.
LoopRun func(ctx context.Context, in LoopInput, ldeps LoopDeps) (LoopResult, error)
// PromptYN is the interactive y/N prompt used by the settings migration
// and the HEAD-soft-warn. Nil means "use the real huh-backed prompt".
PromptYN func(ctx context.Context, question string, def bool) (bool, error)
// HeadHasInvestigateCheckpoint returns (true, info) when the
// checkpoint at HEAD already has HasInvestigation set. Used to
// soft-warn against running a redundant investigation. Nil means
// "skip the check entirely".
HeadHasInvestigateCheckpoint func(ctx context.Context) (bool, string)
// InvestigateMultipicker overrides the spawn-time agent picker. Nil
// means "use the real PickInvestigateAgents form".
InvestigateMultipicker func(ctx context.Context, choices []AgentChoice, askPrompt bool) (PickedInvestigate, error)
}
Deps collects the runtime-injectable hooks NewCommand needs from the parent cli package. Tests stub fields to drive branches that would otherwise require a real TTY or enabled repo.
type Files ¶
type Files struct {
// Findings is the absolute path to the findings document the agent
// reads, edits, and adds evidence to.
Findings string
// State is the absolute path to the run's state.json file. The agent
// records its stance there via the `pending_turn` field.
State string
}
Files holds the absolute paths to the documents shared across an investigation run.
type FixDeps ¶
type FixDeps struct {
// ManifestStore loads local manifests by run ID.
ManifestStore *LocalManifestStore
// FixAgent is the agent registry name to launch. When empty, RunFix
// falls back to defaultFixAgent.
FixAgent string
// Launch runs the actual coding agent session. Production wires this
// to agentlaunch.LaunchFixAgent.
Launch func(ctx context.Context, agentName string, prompt string) error
// ReadFile, when non-nil, replaces os.ReadFile.
ReadFile func(name string) ([]byte, error)
}
FixDeps collects what RunFix needs that's injectable for tests.
type FixInput ¶
type FixInput struct {
// RunID resolves a specific run; empty means "pick the most recent".
RunID string
// Out is the user-facing stream for the launch banner.
Out io.Writer
// ErrOut is the user-facing stream for warnings (e.g. missing doc).
ErrOut io.Writer
}
FixInput drives RunFix.
type IssueLinkResult ¶
type IssueLinkResult struct {
// SeedDoc is the rendered markdown body — ready to write to a
// findings doc via Bootstrap.IssueLinkSeed.
SeedDoc []byte
// Topic is the human-readable topic. Prefers the issue/PR title; if
// the title is empty, falls back to the URL.
Topic string
}
IssueLinkResult is the output of ResolveIssueLink.
func ResolveIssueLink ¶
func ResolveIssueLink(ctx context.Context, rawURL string) (IssueLinkResult, error)
ResolveIssueLink resolves a GitHub issue or PR URL via the gh CLI and returns a markdown seed-doc body suitable for passing to Bootstrap.IssueLinkSeed.
Supported: GitHub issues and PRs only. Non-GitHub hosts (gitlab, bitbucket, self-hosted forges) and non-issue/PR GitHub paths return an actionable error pointing the user at [seed-doc] instead.
The function intentionally does not follow nested issue/PR cross-references or fetch related sub-issues: keep the seed scope to one resource so agents have a clear starting point.
type LocalManifest ¶
type LocalManifest struct {
// RunID is the 12-hex-char investigation run identifier.
RunID string `json:"run_id"`
// Topic is the human-readable subject of the investigation.
Topic string `json:"topic"`
// Slug is the filesystem-safe form of Topic, derived via SlugifyTopic.
Slug string `json:"slug"`
// StartingSHA is the git commit SHA that was HEAD when the
// investigation started.
StartingSHA string `json:"starting_sha"`
// WorktreePath is the absolute path to the worktree the run executed
// in. Empty when the run was not associated with a specific
// worktree.
WorktreePath string `json:"worktree_path,omitempty"`
// FindingsDoc is the absolute path to the findings document the run
// produced. Always absolute — callers (writeRunManifest in particular)
// must resolve repo-relative paths before populating this field, since
// `entire investigate show` / `fix` read it back via os.ReadFile and
// do not perform their own resolution. The on-disk file is removed for
// terminal outcomes (Quorum/Stalled) once FindingsContent has been
// captured — the path remains here for resumable runs (Paused /
// Cancelled) where the file still lives in the per-run directory.
FindingsDoc string `json:"findings_doc,omitempty"`
// FindingsContent embeds the final findings.md content as of run
// end. Populated on terminal outcomes (Quorum/Stalled) so the
// findings survive after the per-run directory is cleaned up. Empty
// on Paused/Cancelled — those runs are resumable and the file lives
// on disk in the per-run directory at FindingsDoc.
FindingsContent string `json:"findings_content,omitempty"`
// Agents is the ordered list of agent names that participated in
// the run.
Agents []string `json:"agents"`
// Outcome is the terminal outcome of the run. One of: "quorum",
// "stalled", "paused", "cancelled".
Outcome string `json:"outcome"`
// StancesByAgent records the LAST stance each agent expressed in
// the run, keyed by agent name. Empty when the run terminated
// without any stances being recorded.
StancesByAgent map[string]string `json:"stances_by_agent,omitempty"`
// StartedAt is when the run was initiated.
StartedAt time.Time `json:"started_at"`
// EndedAt is when the run terminated.
EndedAt time.Time `json:"ended_at"`
}
LocalManifest is the persisted record of one `entire investigate` run for local findings browsing. Written to <git-common-dir>/entire-investigations/ manifests/<timestamp>-<run-id>.json after each run terminates.
The schema is intentionally narrower than RunState: this file is what `entire investigate --findings` reads to render the picker, so it carries only what a human (or `entire status`) needs to identify a past run, not the state needed to resume one.
func ResolveByRunID ¶
func ResolveByRunID(manifests []LocalManifest, runID string) ([]LocalManifest, error)
ResolveByRunID matches a (possibly partial) run ID against the supplied manifest list. Exact match wins; otherwise unique-prefix wins. Returns a slice (always length 1 on success) so callers handle the not-found and ambiguous cases via the error.
The shape mirrors what show and clean both want: an exact 12-hex match resolves O(1), a unique prefix expands to exactly one manifest, and any other case produces a user-readable error listing the candidates.
type LocalManifestStore ¶
type LocalManifestStore struct {
// contains filtered or unexported fields
}
LocalManifestStore wraps the directory that holds persisted LocalManifest JSON files for one repository.
func NewLocalManifestStore ¶
func NewLocalManifestStore(ctx context.Context) (*LocalManifestStore, error)
NewLocalManifestStore creates a LocalManifestStore rooted at <git-common-dir>/entire-investigations/manifests. Resolves the common dir via session.GetGitCommonDir, so this requires a git repository context.
func NewLocalManifestStoreWithDir ¶
func NewLocalManifestStoreWithDir(dir string) *LocalManifestStore
NewLocalManifestStoreWithDir creates a LocalManifestStore rooted at dir. Useful for tests that do not want to depend on a real git repository.
func (*LocalManifestStore) FindByRunID ¶
func (s *LocalManifestStore) FindByRunID(ctx context.Context, runID string) (LocalManifest, bool, error)
FindByRunID returns the manifest whose RunID equals runID. The bool reports whether a match was found; when false the returned manifest is the zero value. Returns an error only when the underlying directory read itself fails.
Filenames are <timestamp>-<runID>.json, so the lookup is a single Glob + one file read rather than scanning every manifest in the directory.
func (*LocalManifestStore) Latest ¶
func (s *LocalManifestStore) Latest(ctx context.Context) (LocalManifest, bool, error)
Latest returns the most recent manifest in the store, identified by the lexicographically largest filename (filenames are <timestamp>-<runID>.json where the timestamp prefix sorts chronologically). The bool reports whether the store has any manifests; when false the returned manifest is the zero value. Avoids reading every manifest just to pick the newest one.
func (*LocalManifestStore) List ¶
func (s *LocalManifestStore) List(ctx context.Context) ([]LocalManifest, error)
List returns every manifest in the store sorted newest first by StartedAt. A missing directory is treated as an empty list (nil, nil) — useful for callers that want to render `--findings` even when no investigation has ever been run in this repo.
func (*LocalManifestStore) PathFor ¶
func (s *LocalManifestStore) PathFor(m LocalManifest) string
PathFor returns the on-disk path of the manifest file for m. The path is computed deterministically from m.StartedAt + m.RunID (the same inputs Write uses to choose its destination), so callers can use this to delete a manifest record without scanning the directory.
func (*LocalManifestStore) Write ¶
func (s *LocalManifestStore) Write(ctx context.Context, m LocalManifest) error
Write persists m to the manifests directory using a deterministic filename derived from m.StartedAt and m.RunID. Existing files are overwritten — the timestamp+run-id combination is unique by construction (each run has a fresh run ID and a different start time).
type LoopDeps ¶
type LoopDeps struct {
// SpawnerFor maps an agent name → Spawner. Returns nil for an unknown
// agent name, in which case the loop pauses with an error.
SpawnerFor func(agentName string) spawn.Spawner
// States persists/loads RunState across turns. In production this is
// a *StateStore rooted at <git-common-dir>/entire-investigations.
States *StateStore
// Progress receives turn lifecycle events. Production wires either a
// tuiProgressSink (TTY) or textProgressSink (non-TTY). nil is treated
// as nullProgressSink (no-op).
Progress ProgressSink
// Now returns the current time. Defaults to time.Now if nil.
Now func() time.Time
}
LoopDeps collects the runtime-injectable hooks RunInvestigateLoop needs.
type LoopInput ¶
type LoopInput struct {
RunID string // 12-hex
Topic string // human-readable subject of the investigation
Agents []string // ordered, length >= 1
MaxTurns int // per-agent turn budget; 0 → defaultMaxTurns (2)
Quorum int // approvals needed; 0 → len(Agents)
AlwaysPrompt string // optional, appended verbatim to every prompt
FindingsDoc string // absolute path
StartingSHA string // git HEAD when `entire investigate` was invoked
Resume *RunState // when non-nil, resume from this state
}
LoopInput carries everything RunInvestigateLoop needs that isn't a hook.
type LoopOutcome ¶
type LoopOutcome string
LoopOutcome describes how the loop ended.
const ( // OutcomeQuorum means the most recent completed round produced enough // approve stances to meet Quorum. The loop ends successfully. OutcomeQuorum LoopOutcome = "quorum" // OutcomeStalled means the per-agent turn budget was exhausted without // reaching quorum. The investigation produced findings but no // consensus. OutcomeStalled LoopOutcome = "stalled" // OutcomePaused means two consecutive agent invocations failed (process // error, non-zero exit). The loop stops so the user can investigate; // state is preserved for `--continue`. OutcomePaused LoopOutcome = "paused" // OutcomeCancelled means the context was cancelled (Ctrl+C, parent // command shutdown). State is preserved for resume. OutcomeCancelled LoopOutcome = "cancelled" )
type LoopResult ¶
type LoopResult struct {
Outcome LoopOutcome
State *RunState
// Err holds the most recent per-turn spawn error, if any. Informational:
// when Outcome is Quorum/Stalled it is typically nil. When Outcome is
// Paused this surfaces the underlying agent failure.
Err error
}
LoopResult is the loop's final report.
func RunInvestigateLoop ¶
RunInvestigateLoop runs the round-robin investigation loop until it reaches quorum, stalls, gets paused, or the context is cancelled.
On every turn the function persists state via deps.States so a crash mid- turn leaves a recoverable RunState on disk. The returned LoopResult is always populated, even on context cancellation.
The function returns (result, error) where error is non-nil only for programmer errors (invalid input, missing dependencies). Per-turn agent failures are reflected in result.Outcome and result.Err, not the return error.
type PendingTurn ¶
type PendingTurn struct {
Stance string `json:"stance"` // "approve" | "request-changes" | "reject"
Note string `json:"note,omitempty"` // short explanation; optional
}
PendingTurn is the agent-written stance for the most recent turn. The agent populates this before exiting; the loop reads it, appends to Stances[], and clears the field. The `agent` and `turn` fields are unambiguous from context (the loop knows which turn it just ran), so the agent does not include them.
type PickedInvestigate ¶
PickedInvestigate is the result of PickInvestigateAgents: the agents the user selected for this run, and (when no seed/issue input was supplied) the free-form investigation prompt that becomes the topic for this run. Prompt is always empty when askPrompt was false.
func PickInvestigateAgents ¶
func PickInvestigateAgents(ctx context.Context, eligible []AgentChoice, askPrompt bool) (PickedInvestigate, error)
PickInvestigateAgents shows a multi-select form populated from eligible (the agents that are both configured AND have a launchable Spawner), pre-checks all of them, and returns the user's selection.
When askPrompt is true, a second form collects the investigation prompt that will become the topic for this run. When false (e.g. a seed doc or --issue-link was supplied), the prompt form is skipped and Prompt is returned empty.
Requires len(eligible) >= 2.
type ProgressSink ¶
type ProgressSink interface {
// TurnStarted is called immediately before the agent process starts for
// the given turn. perAgentTurn is the 1-indexed count of turns this
// agent has taken (this one included); maxPerAgent is the configured
// per-agent budget.
TurnStarted(agent string, turn, perAgentTurn, maxPerAgent int)
// TurnFinished is called once after the agent process exits AND the
// timeline doc has been parsed for the freshly-added turn block. stance
// is one of "approve", "request-changes", "reject", "unknown". duration
// is the wall-clock duration of the agent process. failed is true when
// the turn was treated as a failure by the loop (spawn error, missing
// heading, etc.); err is the underlying error or nil.
TurnFinished(agent string, turn int, stance string, duration time.Duration, failed bool, err error, preview string)
// RunFinished is called once when the loop terminates (any outcome).
// The TUI uses this to flip rows to a terminal status and freeze the
// dashboard; the text sink may print a final outcome line.
RunFinished(outcome LoopOutcome)
}
ProgressSink consumes turn lifecycle events from RunInvestigateLoop. The loop invokes the methods from a single goroutine — implementations need not synchronize against themselves.
Implementations MUST NOT block. The loop calls these synchronously around the per-turn agent spawn; a slow sink stalls the entire investigation.
type RunState ¶
type RunState struct {
RunID string `json:"run_id"`
Topic string `json:"topic"`
Agents []string `json:"agents"`
MaxTurns int `json:"max_turns"`
Quorum int `json:"quorum"`
CompletedRounds int `json:"completed_rounds"`
Turn int `json:"turn"` // overall turn index across rounds
NextAgentIdx int `json:"next_agent_idx"` // index into Agents for the NEXT turn
Stances []TurnStance `json:"stances,omitempty"`
FindingsDoc string `json:"findings_doc"` // absolute path
StartingSHA string `json:"starting_sha"`
StartedAt time.Time `json:"started_at"`
UpdatedAt time.Time `json:"updated_at"`
// PendingTurn is the agent-writable section. After each agent turn the
// agent sets this to its stance + a short note. The loop reads it
// after the agent process exits, validates it, appends a TurnStance to
// Stances[], clears PendingTurn, advances cursors, persists.
PendingTurn *PendingTurn `json:"pending_turn,omitempty"`
}
RunState is the persisted state of an investigation run, sufficient to resume after a crash, Ctrl+C, or `--continue`.
Round semantics: CompletedRounds counts how many full passes through every agent have finished — it is 0 mid-round-1, increments to 1 once every agent has had its first turn, and so on. By contrast, TurnStance.Round records the 1-indexed round each individual turn belongs to. The two fields look similar but represent different things; readers must pick the one that matches the question they're asking.
type ShowDeps ¶
type ShowDeps struct {
ManifestStore *LocalManifestStore
}
ShowDeps collects what RunShow needs that's test-injectable.
type ShowInput ¶
type ShowInput struct {
// RunID is the run id (or run-id prefix) to display. Empty means
// "show the only manifest, or list options if more than one exists".
RunID string
// Out is the destination writer for the rendered summary + findings.
Out io.Writer
// ErrOut is the destination writer for user-facing error/help messages.
ErrOut io.Writer
}
ShowInput drives RunShow.
type StateStore ¶
type StateStore struct {
// contains filtered or unexported fields
}
StateStore is the runs-state directory wrapper. The root contains one sub-directory per run (named after the run ID), holding findings.md and state.json.
func NewStateStore ¶
func NewStateStore(ctx context.Context) (*StateStore, error)
NewStateStore creates a StateStore rooted at <git-common-dir>/entire-investigations. Resolves the common dir via session.GetGitCommonDir, so this requires a git repository context.
func NewStateStoreWithDir ¶
func NewStateStoreWithDir(dir string) *StateStore
NewStateStoreWithDir creates a StateStore rooted at dir. Useful for tests that don't want to depend on a real git repository.
func (*StateStore) Clear ¶
func (s *StateStore) Clear(ctx context.Context, runID string) error
Clear removes the persisted state for runID. Missing files are treated as a successful clear (no-op).
func (*StateStore) List ¶
func (s *StateStore) List(ctx context.Context) ([]*RunState, error)
List returns all persisted run states. Returns nil (and no error) when the state directory does not exist.
func (*StateStore) Load ¶
Load reads the run state for runID. Returns (nil, nil) when the file does not exist (treat as "no such run").
func (*StateStore) RunDir ¶
func (s *StateStore) RunDir(runID string) string
RunDir returns the absolute path of the per-run directory for runID, where findings.md and state.json both live. The directory may or may not exist on disk; callers that need it materialised should MkdirAll before writing.
Precondition: runID MUST be a validated 12-hex id. RunDir joins it into a path that callers feed to os.RemoveAll (via clean), so an unvalidated id would be a path-traversal sink. Every path that reaches here enforces this: Save/Load validate before calling; manifest List/ResolveByRunID drop manifests whose RunID fails validateRunID before any RunID reaches clean.
type TurnStance ¶
type TurnStance struct {
Round int `json:"round"`
Turn int `json:"turn"` // overall turn number
Agent string `json:"agent"`
Stance string `json:"stance"` // "approve" | "request-changes" | "reject" | "unknown"
PlanChanged bool `json:"plan_changed"`
Note string `json:"note,omitempty"`
}
TurnStance is one agent's recorded stance for a turn.
Round here is the 1-indexed round the turn belongs to (turn 1 of round 1, turn N+1 starts round 2, etc.) — distinct from RunState.CompletedRounds, which counts finished rounds.