fixit

import "github.com/jbcom/radioactive-ralph/internal/fixit"

Package fixit implements the deliberate plan-creation pipeline for fixit-ralph’s advisor mode.

See docs/design/fixit-plan-pipeline.md for the architectural rationale. Each stage in this package corresponds to a stage in the design doc.

Index

Constants

MinConfidence is the floor below which a plan becomes provisional regardless of other rules passing.

const MinConfidence = 50

type AnalyzeOptions

AnalyzeOptions feeds Analyze.

type AnalyzeOptions struct {
    Intent IntentSpec
    RC     RepoContext
    Scores []VariantScore

    // ClaudeBin overrides the default `claude` binary path. Tests use
    // the cassette replayer or the fake-claude double here.
    ClaudeBin string

    // WorkingDir is the cwd for the spawned subprocess. Defaults to the
    // repo root from RC.GitRoot.
    WorkingDir string

    // Model pins the tier for the planning subprocess. Empty defaults
    // to "opus" — the advisor runs infrequently (once per plan), so
    // the cost is bounded and the quality delta is meaningful.
    Model string

    // Effort pins the reasoning-effort level. Empty defaults to "high"
    // so opus scales up on genuinely hard repos without burning tokens
    // on simple ones.
    Effort string

    // Timeout caps the total Claude analysis time. Default 180s —
    // opus with auto-effort can take longer than the old sonnet/medium
    // defaults, so the cap is lifted from 90s accordingly.
    Timeout time.Duration
}

type DocFile

DocFile records a markdown file’s frontmatter and basic metadata so Stage 4 can reason about doc freshness without re-reading every file.

type DocFile struct {
    Path        string
    Frontmatter map[string]string
    UpdatedISO  string // from frontmatter or file mtime fallback
}

type EmitResult

EmitResult is what EmitToDAG returns.

type EmitResult struct {
    PlanID     string
    Status     PlanStatus
    TaskIDs    []string
    Proposal   PlanProposal
    Validation ValidationResult
}

func EmitToDAG

func EmitToDAG(ctx context.Context, o EmitToDAGOpts) (EmitResult, error)

EmitToDAG writes the fixit output into plandag. Creates:

  • one plans row (status mapped from fixit PlanStatus)

  • one intents row capturing raw operator input

  • one analyses row capturing Claude’s Phase 2 output

  • N tasks rows (one per proposal.Tasks entry)

  • topologically-sorted dependency edges when Task.DependsOn is set

Returns the newly-generated plan UUID plus the slugified task IDs.

type EmitToDAGOpts

EmitToDAGOpts configures EmitToDAG.

type EmitToDAGOpts struct {
    Store      *plandag.Store
    Topic      string
    Proposal   PlanProposal
    Validation ValidationResult
    Status     PlanStatus
    Intent     IntentSpec
    RC         RepoContext
    RawOutput  string // Phase 2 Claude output; stored in analyses.raw_json
}

type EmittedPlan

EmittedPlan describes what Stage 6 wrote.

type EmittedPlan struct {
    Path        string
    Status      PlanStatus
    Proposal    PlanProposal // zero-valued when Status=fallback
    Validation  ValidationResult
    Intent      IntentSpec
    RepoContext RepoContext
}

func Emit

func Emit(plansDir, topic string, proposal PlanProposal, validation ValidationResult, status PlanStatus, intent IntentSpec, rc RepoContext) (EmittedPlan, error)

Emit runs Stage 6 — write the plan to disk and return what happened. The status comes from the caller (Current / Provisional / Fallback) based on Stage 4 + Stage 5 outcomes.

The emitted file ALWAYS has valid plan-format frontmatter so it satisfies the shape the plans-first discipline expects, even when status is fallback or provisional — other variants gate on the status field, not on presence of frontmatter.

func EmitFallback

func EmitFallback(plansDir, topic string, reason string, rawOutput string, intent IntentSpec, rc RepoContext) (EmittedPlan, error)

EmitFallback writes a diagnostic-only plan whose status is `fallback`. Used when Stage 4 fails twice or Stage 5 produces a failure the operator should see directly.

func RunPipeline

func RunPipeline(ctx context.Context, opts RunOptions) (EmittedPlan, error)

RunPipeline orchestrates Stages 1-6 and returns what was emitted. Errors here indicate an unrecoverable pipeline failure (e.g. can’t explore the repo); Stage 4/5 failures become fallback or provisional plans, not errors.

type GHIssue

GHIssue is the subset of `gh pr/issue list –json` fields fixit uses.

type GHIssue struct {
    Number      int
    Title       string
    Draft       bool
    State       string // OPEN, CLOSED, MERGED
    MergeStatus string // CLEAN, DIRTY, BLOCKED, UNKNOWN
    Author      string
    Labels      []string
}

type GitCommit

GitCommit is a single entry from `git log –oneline` enriched with author + date.

type GitCommit struct {
    SHA     string
    Subject string
    Author  string
    DateISO string
}

type IntentOptions

IntentOptions feeds CaptureIntent.

type IntentOptions struct {
    Topic          string
    Description    string
    Constraints    []string
    NonInteractive bool
    RepoRoot       string
    Stdin          io.Reader // defaults to os.Stdin
    Stdout         io.Writer // defaults to os.Stdout
}

type IntentSpec

IntentSpec captures Stage 1 output — what the operator is trying to accomplish and what’s off-limits.

type IntentSpec struct {
    // Topic is the sanitized slug used in the output filename.
    Topic string

    // Description is free-form operator text describing the goal.
    // Either passed via --description, scraped from a TOPIC.md at the
    // repo root, or empty.
    Description string

    // Constraints are operator-declared hard limits. Examples: "no
    // opus", "stay under $5", "main branch only", "weekend only".
    // Stage 4 renders these as inviolable rules in the prompt; Stage
    // 5 rejects proposals that violate them.
    Constraints []string

    // AnswersToQs is the populated record of any interactive
    // questions the operator answered. Empty in non-interactive mode.
    AnswersToQs map[string]string
}

func CaptureIntent

func CaptureIntent(opts IntentOptions) (IntentSpec, error)

CaptureIntent runs Stage 1. In non-interactive mode it returns immediately with whatever the caller supplied. In interactive mode it asks three short questions on stdout and reads answers from stdin. Either way it consults a TOPIC.md at the repo root for an operator-prepared description if –description wasn’t passed.

type InventorySnapshot

InventorySnapshot is a flattened view of the operator capability inventory (helper integrations, MCPs, agents). The full inventory.Inventory has more detail but the advisor only needs names and high-level availability.

type InventorySnapshot struct {
    Skills []string // FullName form, e.g. "coderabbit:review"
    MCPs   []string
    Agents []string
}

type PlanProposal

PlanProposal is Stage 4 output — the structured JSON the constrained Claude subprocess returns.

type PlanProposal struct {
    Primary            string   `json:"primary"`
    PrimaryRationale   string   `json:"primary_rationale"`
    Alternate          string   `json:"alternate,omitempty"`
    AlternateWhen      string   `json:"alternate_when,omitempty"`
    Tasks              []Task   `json:"tasks"`
    AcceptanceCriteria []string `json:"acceptance_criteria"`
    Confidence         int      `json:"confidence"` // 0..100
}

func Analyze

func Analyze(ctx context.Context, opts AnalyzeOptions) (PlanProposal, error)

Analyze runs Stage 4. Returns a parsed PlanProposal on success, or (zero, error) on hard failure. Callers handle fallback emission; this function never returns a half-filled proposal.

One retry on JSON-parse failure: when Claude returns text that doesn’t unmarshal cleanly, we re-spawn with the parse error appended so the model can self-correct. Second failure bubbles up.

type PlanStatus

PlanStatus captures whether the emitted plan satisfies the plans- first discipline gate.

type PlanStatus string

const (
    // StatusCurrent means the plan passed every validation rule and
    // other variants will accept it as a valid durable plan.
    StatusCurrent PlanStatus = "current"

    // StatusProvisional means at least one validation rule failed but
    // the proposal had enough merit to write. Other variants refuse to
    // run on a provisional plan until the operator promotes it.
    StatusProvisional PlanStatus = "provisional"

    // StatusFallback means Stage 4 (Claude analysis) failed twice. The
    // emitted file is a diagnostic, not a plan.
    StatusFallback PlanStatus = "fallback"
)

type RefineIteration

RefineIteration records what happened in one pass.

type RefineIteration struct {
    Iteration  int
    Proposal   PlanProposal
    Validation ValidationResult
    Confidence int
    Accepted   bool
}

type RefineOptions

RefineOptions drives the Stage 4 + Stage 5 refinement loop.

type RefineOptions struct {
    AnalyzeOpts            AnalyzeOptions
    MaxIterations          int  // default 3
    MinConfidenceThreshold int  // default 70
    StopOnValidationPass   bool // default true — stop as soon as validation passes
}

type RefineResult

RefineResult captures each iteration the loop produced, plus the final accepted proposal (or zero value if all iterations failed).

type RefineResult struct {
    Iterations      int          // how many passes we actually ran
    FinalProposal   PlanProposal // the accepted or last-attempt proposal
    FinalValidation ValidationResult
    History         []RefineIteration
    AcceptedAt      int // iteration number of the accepted proposal (1-indexed), 0 if never
}

func Refine

func Refine(ctx context.Context, rc RepoContext, intent IntentSpec, opts RefineOptions) (RefineResult, error)

Refine runs Stage 4 repeatedly, feeding validation failures back into each subsequent call as corrective context. Stops when any of:

  • Proposal validates AND confidence >= MinConfidenceThreshold

  • MaxIterations reached

  • Hard error (context cancel, claude spawn fail)

The LLM sees each prior attempt’s failures appended to the system prompt, so it can deliberately address them rather than randomly redrafting.

type RepoContext

RepoContext is Stage 2 output — everything the deterministic exploration discovered about the repo.

type RepoContext struct {
    GitRoot         string
    CurrentBranch   string
    DefaultBranch   string
    OnDefaultBranch bool

    Commits []GitCommit

    DocsPresent []DocFile
    DocsStale   []string
    DocsMissing []string

    PlansDir         string
    PlansIndexExists bool
    PlansIndexFM     map[string]string
    PlansFiles       []string

    GHAuthenticated bool
    OpenPRs         []GHIssue
    OpenIssues      []GHIssue
    AIWelcomeIssues []GHIssue

    Inventory InventorySnapshot

    LangCounts        map[string]int
    GovernanceMissing []string
}

func Explore

func Explore(ctx context.Context, repoRoot string) (RepoContext, error)

Explore runs Stage 2 — deterministic repo exploration. Every field in the returned RepoContext comes from a shell-out (git, gh, file walk) — zero LLM calls.

type RunOptions

RunOptions drives the full six-stage pipeline.

type RunOptions struct {
    RepoRoot       string
    Topic          string
    Description    string
    Constraints    []string
    NonInteractive bool

    // ClaudeBin overrides the default `claude` binary for Stage 4.
    // Tests pass the cassette replayer or fake-claude here.
    ClaudeBin string

    // SkipAnalysis bypasses Stage 4 — useful only for tests that
    // exercise the deterministic stages without spawning a
    // subprocess. When true, the pipeline returns after Stage 3 with
    // a zero PlanProposal.
    SkipAnalysis bool

    // MaxRefinementIterations caps how many rounds of Claude
    // refinement Stage 4 will do before giving up. Default 3.
    // Configurable via CLI flag or config.toml [variants.fixit]
    // max_refinement_iterations.
    MaxRefinementIterations int

    // MinConfidenceThreshold is the confidence floor a proposal must
    // meet before we stop refining. Validate.MinConfidence is the
    // lower absolute floor below which validation fails; this
    // threshold is the refinement-loop bar. Default 70.
    // Configurable via CLI flag or config.toml [variants.fixit]
    // min_confidence_threshold.
    MinConfidenceThreshold int

    // PlanModel pins the Claude model tier for Stage 4 planning.
    // Empty defaults to "opus" — planning benefits from the most
    // capable tier and the advisor runs infrequently. Configurable
    // via CLI (--plan-model) or config.toml [variants.fixit]
    // plan_model.
    PlanModel string

    // PlanEffort pins the reasoning-effort level for Stage 4.
    // Empty defaults to "high". Configurable via CLI (--plan-effort)
    // or config.toml [variants.fixit] plan_effort.
    PlanEffort string
}

type Task

Task is one item in a PlanProposal’s task list. The DAG-oriented fields (VariantHint, ContextBoundary, AcceptanceCriteria, DependsOn) are populated when Stage 4 emits enough structure to build a real DAG. When omitted, EmitToDAG falls back to an implicit linear chain.

type Task struct {
    Title              string   `json:"title"`
    Effort             string   `json:"effort"` // S | M | L
    Impact             string   `json:"impact"` // S | M | L
    VariantHint        string   `json:"variant_hint,omitempty"`
    ContextBoundary    bool     `json:"context_boundary,omitempty"`
    AcceptanceCriteria []string `json:"acceptance_criteria,omitempty"`
    DependsOn          []string `json:"depends_on,omitempty"`
}

type ValidationResult

ValidationResult is Stage 5 output — whether the proposal passed every rule and, if not, what failed.

type ValidationResult struct {
    Passed   bool
    Failures []string
}

func Validate

func Validate(p PlanProposal, rc RepoContext, intent IntentSpec) ValidationResult

Validate runs Stage 5. Returns (passed, failures). A passing validation means the plan gets `status: current`; a failing validation still emits the plan but downgrades status to `provisional` so other variants’ plans-first gate refuses it.

type VariantScore

VariantScore is one entry in Stage 3’s deterministic ranking.

type VariantScore struct {
    Variant       string
    Score         int      // 0..100
    Reasons       []string // human-readable bullets, fed into prompt
    Disqualifying []string // hard exclusions; non-empty means score=0
}

func Score

func Score(rc RepoContext, intent IntentSpec) []VariantScore

Score runs Stage 3 — deterministic variant ranking. Every variant gets a 0..100 score with bullet-justified Reasons. Disqualifying notes set the score to 0 (e.g. world-breaker without –confirm-burn-everything from the CLI, since gated variants can’t be auto-handed-off).

Same input always produces same output. Rules live here so they can be unit-tested independently of Claude.

Generated by gomarkdoc