Documentation
¶
Overview ¶
Package plan enriches agent-generated implementation plans with SageOx team context. ox computes DETERMINISTIC badges locally (zero LLM tokens) and assembles a context bundle; the client agent does any inference. ox NEVER makes an LLM or network call in this path.
Architecture:
- Detectors produce deterministic Annotations from local data (collision, prior-art, expert-routing). They are fail-open: missing/unreadable data returns (nil, nil), never an aborting error.
- Retrievers assemble a context bundle ([]ContextItem) the client agent reasons over to author judgment badges (aligns/conflicts, expert perspective). Also fail-open.
- Enrich() orchestrates registered detectors and retrievers, aggregating results into a Result with a deterministic SignalSummary.
Round 2 agents implement detectors/retrievers in collision.go, expert.go, priorart.go (and a context-bundle assembler) and register them via init().
Index ¶
- Constants
- func AnchorFor(heading, text string) string
- func AppendResolution(planDir string, r Resolution, now time.Time) error
- func ContestedAnchors(items []MergedItem) map[string]bool
- func CountOpenFeedback(planDir string) int
- func FeedbackDigest(items []MergedItem) string
- func Load(gitRoot, slug string) (string, Result, PlanInfo, error)
- func MutatePlanMeta(ctx context.Context, planDir string, mutate func(*Meta) (*Meta, error)) error
- func PlanHTMLPath(dir string) (path string, ref lfs.FileRef, isPointer, exists bool)
- func ReconcileSessionOutcome(gitRoot, slug, sessionID, outcome string) error
- func RecordPlanCraft(rep CraftReport)
- func RecordPlanGenerated(res Result, saved bool)
- func RegisterDetector(d Detector)
- func RegisterRetriever(r Retriever)
- func RenderHTML(in Input, res Result) ([]byte, error)
- func RenderHTMLOpts(in Input, res Result, opts RenderOptions) ([]byte, error)
- func RenderViz(pattern string, data []byte) (string, error)
- func ReviewServiceWorkerJS() ([]byte, error)
- func Save(gitRoot string, in Input, res Result, html []byte, meta Meta) (string, error)
- func SaveFeedback(planDir string, set FeedbackSet, now time.Time) (string, error)
- func SaveReviewServerState(gitRoot, planDirName string, st ReviewServerState) error
- func SetSessionOutcome(gitRoot, slug, outcome string) error
- func SetStatus(gitRoot, slug string, status PlanStatus) error
- func Slugify(topic string) string
- func StableReviewPort(planDirName string) int
- type Annotation
- type BadgeKind
- type BadgeType
- type BrandingFinding
- type CollabSignals
- type ContextItem
- type CraftReport
- type Detector
- type DiagramHint
- type DiagramKind
- type FeedbackItem
- type FeedbackSet
- type FeedbackStatus
- type Finding
- func LintArtifact(htmlBytes []byte) []Finding
- func LintCraft(res Result, htmlBytes []byte) []Finding
- func LintMermaid(htmlBytes []byte) []Finding
- func LintMermaidMarkdown(raw string) []Finding
- func LintRender(htmlBytes []byte, res Result) []Finding
- func LintSessionLink(htmlBytes []byte, sessionID string) []Finding
- type Input
- type MergedItem
- type Meta
- type OpenFeedbackSummary
- type PlanInfo
- type PlanStatus
- type Provenance
- type RemapEntry
- type RenderOptions
- type Resolution
- type ResolutionState
- type Result
- type Retriever
- type ReviewServerState
- type Section
- type SignalSummary
- type VizHint
- type VizPattern
Constants ¶
const ( // NonTrivialMinFiles: a multi-file plan (>= 2 distinct files) is non-trivial. // Exported as the single source of truth: the plan-exit hook mirrors these // for wording and a drift test asserts the copies stay equal. NonTrivialMinFiles = 2 // NonTrivialMinSteps: a ~5+ step plan is non-trivial, matching the prime // "~5+ steps" criterion. H2 sections are the step proxy. NonTrivialMinSteps = 5 )
const ( SessionOutcomeActive = "active" SessionOutcomeStopped = "stopped" SessionOutcomeAborted = "aborted" )
Session-outcome values for Provenance.SessionOutcome. "" == unknown.
const SchemaVersion = "v1"
SchemaVersion is the on-disk schema version stamped into both annotations.json (Result.SchemaVersion) and meta.json (Meta.SchemaVersion) on write. These are long-lived ledger artifacts a future ox may need to migrate; an explicit version lets a reader detect and adapt to an older layout instead of guessing. Bump this when the serialized shape of Result or Meta changes incompatibly.
Variables ¶
This section is empty.
Functions ¶
func AnchorFor ¶ added in v0.11.1
AnchorFor computes the review anchor for an element, given its enclosing section heading and its full text content — matching review.js anchorFor().
func AppendResolution ¶
func AppendResolution(planDir string, r Resolution, now time.Time) error
AppendResolution adds one agent disposition to the append log. The whole read-modify-write runs under an advisory flock: the reviewer's Accept (via the review server) and the agent's `ox plan feedback resolve` are separate processes writing the same file, and an unlocked RMW loses whichever resolution lands first — a sacred-data loss, not a cosmetic race.
func ContestedAnchors ¶ added in v0.11.0
func ContestedAnchors(items []MergedItem) map[string]bool
ContestedAnchors returns the set of anchors where OPEN reviewer verdicts conflict — e.g. one reviewer approves while another requests a change. These are the marks a human authorizer must reconcile before the plan is safe to execute (ADR-026). Comment-only marks never contest; an approve alongside any change/flag does.
func CountOpenFeedback ¶ added in v0.11.0
CountOpenFeedback returns the number of OPEN, actionable review items for a plan dir — approvals don't count (they close, not open, the loop). Fail-open: 0 on any read error, so a discovery surface never breaks on one bad plan.
func FeedbackDigest ¶
func FeedbackDigest(items []MergedItem) string
FeedbackDigest renders a compact, agent-readable summary from the merged view: open/addressed/verified/wontfix counts, then every OPEN actionable item with its anchor (the id to resolve), section, label, and note. Returns "" when there is no feedback.
func Load ¶
Load reads a captured plan by slug. The slug matches either the meta.json slug or the directory's trailing slug segment (the YYYY-MM-DD- prefix is optional in the lookup). Returns the raw plan markdown, the stored Result, and the listing info.
func MutatePlanMeta ¶
MutatePlanMeta runs an exclusive read-modify-write under an advisory flock on a plan's meta.json — the direct mirror of lfs.MutateSessionMeta. Every write to meta.json AFTER the initial Save (status changes, session-outcome reconciliation) MUST go through this so a re-save and a concurrent session-stop/doctor write serialize at the filesystem level instead of clobbering each other.
The mutator receives the on-disk Meta (nil if the file is missing) and returns the Meta to write, or nil to leave the file untouched (an "only if exists" guard). Returning an error aborts the write.
func PlanHTMLPath ¶
PlanHTMLPath returns the absolute path to a captured plan's plan.html, the referenced FileRef when it is an LFS pointer, and whether the file exists. The view path uses this to decide between opening a plain HTML file and hydrating a pointer first.
func ReconcileSessionOutcome ¶
ReconcileSessionOutcome backfills the producing session's canonical id and final outcome onto a plan (by slug) at session-stop, where both are known. sessionID is the ses_<UUIDv7> minted at stop (skipped when ""); outcome is a SessionOutcome* constant. Single flocked write so it can't race a re-save.
func RecordPlanCraft ¶ added in v0.11.0
func RecordPlanCraft(rep CraftReport)
RecordPlanCraft emits the design-craft realization metric for a rendered plan: how many visual craft expectations ox produced (a diagram suggested, a user-facing surface detected) and how many the render actually realized. The hints_emitted vs hints_realized ratio, aggregated across the ledger, is the visual-enrichment "did the agent act on the hint" rate — the tachometer for whether enrichment changes what gets drawn, not just what gets suggested. No plan content is recorded, only counts and the unrealized rule names. A render with no craft expectation is silent (nothing to measure).
func RecordPlanGenerated ¶
RecordPlanGenerated emits a single-line, key=value structured metric for a completed `ox plan` enrichment. It is purely local observability — there is no server LLM in this path, so there is nothing to meter server-side. The counts let us see, in aggregate, how often plans fire collision / prior-art / expert signals and how much context the bundle carried, without recording any plan content.
saved reports whether the enriched plan was captured to the ledger. It is a separate boolean (not derived from res) because auto-save is gated on config and on a configured ledger, independent of the signal summary.
func RegisterDetector ¶
func RegisterDetector(d Detector)
RegisterDetector adds a deterministic detector to the global registry. Call from an init() in the detector's file. Nil detectors are ignored.
func RegisterRetriever ¶
func RegisterRetriever(r Retriever)
RegisterRetriever adds a context-bundle retriever to the global registry. Call from an init() in the retriever's file. Nil retrievers are ignored.
func RenderHTML ¶
RenderHTML renders a resolved plan + its enrichment Result into a single self-contained HTML document. Deterministic and network-free at render time (Mermaid loads from CDN only when the page is viewed).
func RenderHTMLOpts ¶
func RenderHTMLOpts(in Input, res Result, opts RenderOptions) ([]byte, error)
RenderHTMLOpts is RenderHTML with optional render-time context (e.g. the slug for the review layer). RenderHTML delegates here with zero options.
func RenderViz ¶
RenderViz renders one parameterized pattern from its JSON data into an HTML fragment. Returns an error for an unknown pattern or malformed data so the command layer can show an actionable message.
On a render failure (usually a JSON-shape mismatch), the error echoes the pattern's expected `param:` shape from the catalog so the caller — typically an AI coworker driving ox from inside another agent — can self-correct in one shot instead of guessing the schema. (Goose's Auto Visualiser, which has the agent supply the full chart spec, documented exactly this failure mode with no shape hint to recover from; ox supplies data only, and names the shape on a miss.)
func ReviewServiceWorkerJS ¶ added in v0.11.1
ReviewServiceWorkerJS returns the embedded service worker the live review server exposes at /sw.js — the offline shell that keeps a rendered plan readable in the browser after the server exits (review.js registers it).
func Save ¶
Save writes a captured plan into the ledger under data/plans/<dated-slug>/. It writes plan.md (from in.Raw), annotations.json (res), and meta.json as plain git-tracked text. plan.html is written ONLY when html != nil: plain when small, as an LFS pointer when it exceeds htmlLFSThreshold. Save never renders HTML and never commits — it only materializes files in the working tree. Returns the absolute plan directory.
gitRoot is the producing project's git root; the ledger path is resolved from it via ProjectContext. Returns an error if no ledger is configured (the caller decides whether that is fatal — the porcelain path treats it as "nothing to save").
func SaveFeedback ¶
SaveFeedback writes a review round under <planDir>/feedback/. now controls the timestamp (tests stay deterministic). Returns the written path.
func SaveReviewServerState ¶ added in v0.11.1
func SaveReviewServerState(gitRoot, planDirName string, st ReviewServerState) error
SaveReviewServerState persists a plan's server identity (0600 — it carries the review token). Best-effort callers may ignore the error: without state the next run simply mints a fresh identity, which is the pre-existing behavior, not a data loss.
func SetSessionOutcome ¶
SetSessionOutcome reconciles the producing session's lifecycle onto the plan (by slug) under the meta flock — written only by session-stop / `ox doctor`, never by Save. Use the SessionOutcome* constants. No-op if no meta.json.
func SetStatus ¶
func SetStatus(gitRoot, slug string, status PlanStatus) error
SetStatus updates a saved plan's lifecycle status (by slug) under the meta flock. No-op if the plan dir has no meta.json. Use the PlanStatus* constants.
func Slugify ¶
Slugify derives a 2-4 word kebab-case slug from a topic/title. Lowercases, strips punctuation, and keeps the first 2-4 meaningful words. An empty or punctuation-only input yields "untitled-plan" so a directory name is always well-formed.
func StableReviewPort ¶ added in v0.11.1
StableReviewPort maps a plan's dated dir name to its deterministic default port. Same plan → same port on every run, on every machine.
Types ¶
type Annotation ¶
type Annotation struct {
Section string `json:"section,omitempty"`
Kind BadgeKind `json:"kind"`
Type BadgeType `json:"type"`
Why string `json:"why"`
// HumanWhy is the curated, decision-first phrasing used in the HTML render. It
// drops agent-only provenance noise that Why carries for the --json/agent path
// (commit SHAs, relative timestamps like "12h ago", workspace counts). The
// renderer prefers HumanWhy when non-empty; --json keeps the raw Why intact.
// Empty means "use Why" — most annotations need no separate human phrasing.
HumanWhy string `json:"human_why,omitempty"`
SourceURL string `json:"source_url,omitempty"`
Expert string `json:"expert,omitempty"`
Files []string `json:"files,omitempty"`
// Date, Summary, RefKind carry the structured prior-art fields the renderer
// composes into a crisp "person · date · summary" link. Environment-
// independent (no URL), so they're safe to emit in `ox plan enrich --json`;
// the web URL is built only at render time from the local project config.
Date string `json:"date,omitempty"`
Summary string `json:"summary,omitempty"`
RefKind string `json:"ref_kind,omitempty"` // session | plan | murmur
}
Annotation is a single badge attached to a plan section.
type BadgeKind ¶
type BadgeKind string
BadgeKind distinguishes who produces an annotation: ox locally (deterministic, zero tokens) versus the client agent (judgment, reasoned from the bundle).
type BadgeType ¶
type BadgeType string
BadgeType is the specific signal an annotation carries.
const ( // BadgeCollision: plan touches files in an open PR, hotspot, or recent murmur. BadgeCollision BadgeType = "collision" // BadgePriorArt: a teammate already did or planned this. BadgePriorArt BadgeType = "prior-art" // BadgeExpertRoute: who owns this area + their relevant work (deterministic). BadgeExpertRoute BadgeType = "expert-routing" // BadgeAligns: plan aligns with ADRs, decisions, conventions (judgment). BadgeAligns BadgeType = "aligns" // BadgeConflicts: plan conflicts with ADRs, decisions, conventions (judgment). BadgeConflicts BadgeType = "conflicts" // BadgeExpertPersp: synthesized expert stance, cited (judgment). BadgeExpertPersp BadgeType = "expert-perspective" // BadgeRigor: collaboration-rigor stance synthesized from CollabSignals — // how thoughtful the human↔agent path to this plan was (judgment). ox emits // the raw counts (CollabSignals); the agent/cloud authors this badge. BadgeRigor BadgeType = "rigor" )
type BrandingFinding ¶
type BrandingFinding = Finding
BrandingFinding is retained as an alias so existing callers/tests keep compiling; new code should use Finding.
func LintBranding ¶
func LintBranding(html []byte, res Result) []BrandingFinding
LintBranding verifies a rendered plan HTML carries the conditional SageOx attribution the html-plan skill is spec'd to produce. The contract (extensions/claude/skills/ox-plan/SKILL.md, "SageOx attribution — subtle, earned, conditional"):
- EARNED: when the plan carried enrichment — any deterministic badges OR context-bundle items were present — the render MUST credit it: a footer line ("…enriched by SageOx") and, when there are deterministic badges, at least one anchored OX marker.
- NO OVERCLAIM: an un-enriched plan (no badges, empty context) must NOT carry SageOx credit — there is nothing to credit.
- SELF-CONTAINED: the OX marker's avatar must never be a live remote <img src>; it is data:-inlined or an inline-SVG monogram. Always checked.
Returns nil when the page satisfies the contract. Fail-open: callers warn, never block.
type CollabSignals ¶
type CollabSignals struct {
UserPrompts int `json:"user_prompts"` // distinct human turns before the plan
AgentQuestions int `json:"agent_questions"` // AskUserQuestion / clarifying tool calls
ToolCalls int `json:"tool_calls"` // exploration-depth proxy
DurationSeconds int `json:"duration_seconds"` // first user prompt → plan finalized
}
CollabSignals are deterministic, locally-counted facts about the human↔agent collaboration that produced the plan — effort proxies, NOT a score. Scoring (a rigor judgment) is authored by the agent now / a cloud judge later, per ADR-021. Signal COUNTS (collisions/prior-art/expert-routes) deliberately live in annotations.json (Result.Signals), not here, to avoid duplication.
type ContextItem ¶
type ContextItem struct {
Kind string `json:"kind"` // murmur|session|decision|adr|commit|discussion
Title string `json:"title"`
Ref string `json:"ref"`
Snippet string `json:"snippet,omitempty"`
Score float64 `json:"score"`
Author string `json:"author,omitempty"`
When string `json:"when,omitempty"`
}
ContextItem is one ranked, pre-retrieved slice of ledger / team context / code the client agent reasons over to author judgment badges.
type CraftReport ¶ added in v0.11.0
CraftReport is the realization side of the design-craft check: how many craft expectations enrich produced (a diagram suggested, a user-facing surface detected) and how many the rendered page realized. The render path records it as the `plan_craft` metric — hints_emitted vs hints_realized, aggregated across the ledger, is the "did the agent act on the visual hint" rate — and surfaces the unrealized Gaps as advisory nudges.
func CraftRealization ¶ added in v0.11.0
func CraftRealization(res Result, htmlBytes []byte) CraftReport
CraftRealization compares what ox EXPECTED (computed at enrich, surfaced cross-agent in Result.Guidance) against what the page DREW. Detection lives at enrich (DiagramHints / MockupSection); this is the thin, belt-and-suspenders realization check for an agent already in the ox render flow — NOT the primary cross-agent lever. Fail-open: an empty page yields a zero report. Precision over recall: a diagram expectation is met by ANY visual (a chart counts — "show, don't tell" holds even when the form differs from the hint), so a well-visualized plan is never nagged for the wrong diagram shape.
type Detector ¶
type Detector interface {
Name() string
Detect(ctx context.Context, in Input, gitRoot string) ([]Annotation, error)
}
Detector produces deterministic annotations from local data. MUST be fail-open: on missing/unreadable data return (nil, nil), never an error that aborts enrichment.
type DiagramHint ¶
type DiagramHint struct {
Section string `json:"section"` // H2 heading the hint applies to
SuggestedType DiagramKind `json:"suggested_type"` // the diagram form that fits
Reason string `json:"reason"` // what structure was detected, in one clause
}
DiagramHint is a deterministic, per-section suggestion of which diagram form best captures the structure ox detected in that section. Rendering an HTML plan is now deterministic and free, so the only remaining lever on diagram QUALITY is the Mermaid the agent authors into the plan markdown — these hints point any agent (Claude, Codex, Gemini, …) at the right diagram for THIS plan, per section, instead of defaulting every section to a flowchart. Computed locally with zero LLM/network calls, same lane as the badge detectors.
type DiagramKind ¶
type DiagramKind string
DiagramKind is a suggested diagram form for a plan section. The values are the literal Mermaid diagram keyword (or "swimlane-timeline" for the hand-built CSS timeline) so the agent can paste the suggestion straight into a fenced block.
const ( DiagramSequence DiagramKind = "sequenceDiagram" // ordered call/response path DiagramState DiagramKind = "stateDiagram-v2" // states + time-bounded transitions DiagramSwimlane DiagramKind = "swimlane-timeline" // phased/parallel work (CSS, not Mermaid) DiagramTopology DiagramKind = "flowchart-LR" // dependency/topology graph DiagramFlowchart DiagramKind = "flowchart-TB" // branching procedure (hero default) )
type FeedbackItem ¶
type FeedbackItem struct {
Anchor string `json:"anchor"` // stable content-hash id, e.g. "h3f9a1c2"
Section string `json:"section,omitempty"` // section heading the element sits under
Label string `json:"label"` // short text of the element
Status FeedbackStatus `json:"status"` // approve | request-change | flag | comment
Note string `json:"note,omitempty"` // the reviewer's comment
Reviewer string `json:"reviewer,omitempty"` // who left this mark (multi-user); stamped from the round on save
}
FeedbackItem is one anchored review mark. Anchor is a CONTENT hash of the element (section heading + element text), computed page-side, so it survives a re-render and only disappears when the agent rewrites that text — which is itself the signal the item was addressed. Anchor doubles as the item id used by `ox plan feedback resolve`.
type FeedbackSet ¶
type FeedbackSet struct {
Slug string `json:"slug"`
Reviewer string `json:"reviewer,omitempty"`
CreatedAt time.Time `json:"created_at"`
Items []FeedbackItem `json:"items"`
}
FeedbackSet is one review round (one submit from the page).
func LoadAllFeedback ¶
func LoadAllFeedback(planDir string) ([]FeedbackSet, error)
LoadAllFeedback reads every review round under a plan dir, oldest first. A missing feedback/ dir is not an error. resolutions.json is skipped (it is not a round).
func ParseFeedback ¶
func ParseFeedback(raw []byte) (FeedbackSet, error)
ParseFeedback decodes and validates a review-round JSON (the page submit/export). Fail-loud on malformed input, an unknown status, or an unsafe slug.
type FeedbackStatus ¶
type FeedbackStatus string
FeedbackStatus is the reviewer's verdict on one anchored element.
const ( FeedbackApprove FeedbackStatus = "approve" FeedbackRequestChange FeedbackStatus = "request-change" FeedbackFlag FeedbackStatus = "flag" FeedbackComment FeedbackStatus = "comment" )
type Finding ¶
type Finding struct {
Rule string // stable id, e.g. "branding.footer-credit" / "mermaid.arrow-in-label"
Message string // human-readable, actionable
}
Finding is one advisory lint result on a rendered plan HTML — attribution (branding.*) or diagram (mermaid.*). All findings are warn-level: linting NEVER blocks a render or a save (fail-open agent UX). A non-empty slice means the render did not honor the html-plan contract or carries a diagram that will not render.
func LintArtifact ¶ added in v0.11.0
LintArtifact verifies a rendered plan HTML is safe to publish as a Claude Code Artifact: strictly self-contained, with no construct that the artifact CSP would block at view time. It checks resource loads (external script/style/ font/img, CSS url() to a remote host) and the SSE review layer (EventSource). It deliberately does NOT flag <a href="https://…"> — outbound navigation is allowed, and the SageOx enrichment links rely on it. Fail-open: an empty page returns nil; callers warn, never block.
func LintCraft ¶ added in v0.11.0
LintCraft is the advisory view of CraftRealization — the unrealized craft gaps, printed (never blocking) after a render.
func LintMermaid ¶
LintMermaid extracts every Mermaid diagram from a rendered/saved plan HTML and returns one Finding per high-confidence problem. Fail-open: no diagrams (or none broken) returns nil.
func LintMermaidMarkdown ¶
LintMermaidMarkdown is the same check over raw plan markdown (```mermaid fences), for the render-time path where the source is in hand before the page is built.
func LintRender ¶
LintRender runs the full advisory contract over a rendered plan HTML: SageOx attribution (LintBranding) plus diagram validity (LintMermaid). It is the single entrypoint `ox plan lint` / `ox plan save` call. Fail-open: an empty page returns nil.
func LintSessionLink ¶ added in v0.12.0
LintSessionLink warns when a plan whose provenance carries a session identity renders with no /c/ conversation link back to that exact session. The Go renderer injects the footer link deterministically (RenderOptions.SessionURL); this advisory exists for agent-authored renders (the ox-plan skill) where the link is part of the spec but the author is fallible. Exact-ID match only — a link to a different session is as wrong as none. Fail-open and advisory: callers warn, never block. Empty page or empty sessionID returns nil.
type Input ¶
type Input struct {
Path string
Raw string
Sections []Section
// Topic is the pre-draft consult subject (--topic). Empty for a full-document
// Input (the --file/stdin/auto-discovery path). Mirrors decision.Input.Topic
// for cross-command consistency.
Topic string
// Files is the caller-provided file list for a topic-only consult (--files).
// Empty for a full-document Input, where Section.Files (extracted from the
// parsed prose) is the file source instead. decision.Input has no equivalent
// field: plan's collision/expert-routing signals are file-keyed, which
// decision's related-decision signals are not.
Files []string
}
Input is a resolved plan: its source path (if any), raw markdown, and parsed sections. A pre-draft consult (--topic, optionally --files) instead sets Topic/Files and leaves Raw empty — see ResolveInput.
func Parse ¶
Parse splits markdown into Sections on "## " H2 headings and extracts the file references cited in each section. Content before the first H2 becomes a preamble Section with an empty Heading (only emitted when it has content).
func Resolve ¶
Resolve reads a plan from --file if set, otherwise from a piped stdin, and parses it into an Input. When neither is provided it best-effort auto-discovers the active plan-mode file: the newest *.md under ~/.claude/plans/ (the dir Claude Code plan-mode writes to). Precedence is --file > piped stdin > auto-discovery. An empty/unfound source yields an Input with empty Raw and no sections (the enrich path must stay fail-open on empty input); the caller is expected to surface a clear "no plan found" message rather than enrich nothing.
func ResolveInput ¶ added in v0.12.0
ResolveInput builds the enrich Input from --topic (+ --files), --file, or stdin/auto-discovery, in that precedence order: topic beats file beats stdin/auto-discovery. Mirrors decision.ResolveInput's precedence and shape for cross-command consistency — an agent that has learned the --topic pre-draft pattern from `ox decision enrich` finds the identical shape here, which is the fix for the friction this exists to close (agents guessing --topic on `plan enrich` by analogy from `decision enrich`). The full-document path is untouched: topic empty delegates straight to Resolve, so --file/stdin/auto-discovery behavior is byte-for-byte unchanged.
type MergedItem ¶
type MergedItem struct {
FeedbackItem
RaisedAt time.Time
Resolution *Resolution
Open bool
// RemappedFrom is the item's original anchor when a plan update moved its
// content and the save-time remap rebound it (see remap.go). Empty when the
// mark still lives at its original address. Anchor always holds the CURRENT
// address — the one `ox plan feedback resolve` acts on.
RemappedFrom string `json:"remapped_from,omitempty"`
}
MergedItem is a review item joined with its latest resolution and a computed open/closed state. Open = no resolution, or the item was re-raised after the last resolution (CreatedAt newer than the resolution's At).
func AssembleReview ¶
func AssembleReview(planDir string) ([]MergedItem, error)
AssembleReview joins every review item (latest mark per anchor across rounds) with its latest resolution, computing open/closed. This is the single source the digest and the render read. An item is OPEN when it has no resolution, or when it was re-raised after the latest resolution (supporting the verify loop).
Anchors are CANONICALIZED through the remap chain first (see remap.go): a mark whose content moved when the plan was updated is merged, resolved, and rendered at its current address, with the original preserved in RemappedFrom. Rounds on disk are never rewritten — the chain is applied at read time.
type Meta ¶
type Meta struct {
// SchemaVersion stamps the meta.json shape (set to SchemaVersion on write)
// so a future reader can detect and migrate an older layout.
SchemaVersion string `json:"schema_version,omitempty"`
Topic string `json:"topic"`
Slug string `json:"slug"`
Authors []string `json:"authors,omitempty"`
CreatedAt time.Time `json:"created_at"`
SourcePlanPath string `json:"source_plan_path,omitempty"`
// Status is the plan's lifecycle. Missing == "draft" for legacy plans.
Status PlanStatus `json:"status,omitempty"`
// Provenance links the plan to its producing session/agent/repo (forward).
Provenance *Provenance `json:"provenance,omitempty"`
// Collaboration holds the deterministic collaboration-effort counts.
Collaboration *CollabSignals `json:"collaboration,omitempty"`
}
Meta is the git-tracked descriptor written as meta.json alongside a captured plan. It carries the searchable, hydration-free facts about the plan: who authored it, when, where it came from, which session/agent produced it, and how thoughtful the collaboration was.
func LoadMeta ¶ added in v0.11.0
LoadMeta reads and parses a captured plan's meta.json from its plan directory (PlanInfo.Dir). Exported so the cmd layer can read provenance — e.g. the authoring agent id, to notify that coworker when review feedback arrives — without re-deriving the on-disk path. A missing/unreadable meta is an error.
func ReadPlanMeta ¶
ReadPlanMeta returns the stored Meta for a saved plan (by slug), including provenance, collaboration signals, and status. Used by the view path to surface the link without re-deriving it.
type OpenFeedbackSummary ¶ added in v0.11.0
type OpenFeedbackSummary struct {
Slug string `json:"slug"`
Topic string `json:"topic,omitempty"`
Open int `json:"open"` // open, actionable items (approvals excluded)
AgentType string `json:"agent_type,omitempty"` // authoring coworker type, if recorded
Dir string `json:"-"` // absolute plan dir (local only)
}
OpenFeedbackSummary is one saved plan with unaddressed human review feedback waiting — the unit a discovery surface (ox plan list, ox agent prime) lists so the feedback is findable even when the push notification missed.
func OpenFeedbackPlans ¶ added in v0.11.0
func OpenFeedbackPlans(gitRoot string) ([]OpenFeedbackSummary, error)
OpenFeedbackPlans returns every saved plan in the project's ledger that has open review feedback, newest first. It is the PULL half of the feedback loop: the push (a plan-feedback agent-task) can miss — wrong agent type, a failed queue write, an unlinked plan, or simply a human choosing to look — so this reads the ledger directly and never depends on the queue. Fail-open: an unreadable plan dir is skipped, not fatal.
type PlanInfo ¶
type PlanInfo struct {
Slug string
Topic string
Dir string
CreatedAt time.Time
Authors []string
HasHTML bool
}
PlanInfo is the listing-level view of a captured plan, assembled from meta.json. Dir is the absolute path to the plan folder.
type PlanStatus ¶
type PlanStatus string
PlanStatus is the plan's own lifecycle, independent of the producing session. A plan is worth keeping even if never built — it is a decision record and prior-art seed — so status lets UI/search weight rather than discard. v1: a plain writable field; no CLI auto-detection of "implemented" (that correlation is inference and belongs in the cloud judge per ADR-021).
const ( PlanStatusDraft PlanStatus = "draft" PlanStatusApproved PlanStatus = "approved" // reviewer signed off via the review loop PlanStatusImplemented PlanStatus = "implemented" PlanStatusAbandoned PlanStatus = "abandoned" PlanStatusSuperseded PlanStatus = "superseded" )
type Provenance ¶
type Provenance struct {
// Join keys (may dangle if the session was aborted / never uploaded).
//
// Two-phase population, because the canonical ses_ SessionID is minted
// fresh at session-STOP and is NOT knowable mid-recording:
// - SessionName is the durable identifier available at plan-save time
// (the recording's folder name, what `ox session view <name>` resolves).
// It is the primary join key and is always set when a recording is live.
// - SessionID (ses_<UUIDv7>) is BACKFILLED at session-stop, in the same
// reconciliation that sets SessionOutcome=stopped — we have the real id
// and the produced-plan slugs in hand there. Empty for aborted sessions
// (no stop) and for plans saved outside a recording.
SessionName string `json:"session_name,omitempty"`
SessionID string `json:"session_id,omitempty"` // ses_<UUIDv7>, backfilled at stop
AgentID string `json:"agent_id,omitempty"` // Ox#### stable agent instance
RepoID string `json:"repo_id,omitempty"`
// Denormalized snapshot — renders without the session present.
AgentType string `json:"agent_type,omitempty"` // claude-code, codex, ...
Model string `json:"model,omitempty"`
AuthorName string `json:"author_name,omitempty"` // privacy-safe display name at save time
// SessionOutcome is RECONCILED SYSTEM STATE, not authored provenance:
// "" (unknown) | "active" | "stopped" | "aborted". Written only by
// session-stop / `ox doctor` through MutatePlanMeta, never by Save.
SessionOutcome string `json:"session_outcome,omitempty"`
}
Provenance ties a saved plan back to the session/agent/repo that produced it. It is DENORMALIZED on purpose: the join keys (SessionID/AgentID/RepoID) are the precise link, but a session can be aborted, never uploaded, or GC'd, so the snapshot fields (AgentType/Model/AuthorName) let a plan render fully without the session present. Duplication is the feature, not a smell.
type RemapEntry ¶ added in v0.11.1
type RemapEntry struct {
From string `json:"from"` // anchor that vanished from the render
To string `json:"to"` // anchor of the element it was rebound to
Section string `json:"section,omitempty"` // new element's section heading
Label string `json:"label,omitempty"` // new element's label
Method string `json:"method"` // label-exact | label-fuzzy
Score float64 `json:"score"` // similarity that justified the rebind
At time.Time `json:"at"`
}
RemapEntry records one anchor rebind. Append-only, alongside rounds and resolutions, so the full history of where a mark lived stays in the ledger.
func LoadRemaps ¶ added in v0.11.1
func LoadRemaps(planDir string) ([]RemapEntry, error)
LoadRemaps reads the append log of anchor rebinds. Missing file is empty.
func RemapFeedback ¶ added in v0.11.1
RemapFeedback re-anchors open review items onto a freshly rendered plan. html is the new render (the same bytes being saved as plan.html). Returns the rebinds it recorded; an empty slice means every open item still anchors (or nothing was confidently rebindable). Fail-soft by design: called from Save, where feedback durability must never block the plan write itself.
type RenderOptions ¶
type RenderOptions struct {
Slug string
// Review is the merged review state (rounds + resolutions) for this plan, so
// the render can show each item's open/addressed state inline and in a
// summary. Empty for a plan with no review yet.
Review []MergedItem
// ReviewEndpoint + ReviewToken are set ONLY when the page is served by the
// ephemeral `ox plan review` server: the page POSTs marks to the endpoint
// with the token. Empty for a static file:// render (clipboard fallback).
ReviewEndpoint string
ReviewToken string
// PriorArtURL resolves a prior-art source (its kind + ref/slug) to a SageOx
// web URL, opened in a new tab from the enrichment panel. Nil-safe: when nil
// or when it returns "", the prior-art entry renders as crisp text with no
// link. This is the seam that keeps internal/plan config-agnostic — the
// command layer builds the closure from the local project config, and
// `ox plan enrich --json` (no config) never embeds an environment URL.
PriorArtURL func(refKind, ref string) string
// SessionURL is the universal conversation link (/c/<ses_id>) of the
// recording that produced this plan, rendered as a deterministic footer
// link so committed plan artifacts point back to their session. Empty
// (no recording, no start-minted ID, or session attribution disabled)
// omits the link.
SessionURL string
// Artifact renders a strictly self-contained page with NO external resource
// requests, suitable for publishing as a Claude Code Artifact (served under a
// strict CSP that blocks all cross-origin script/style/font/img and all
// fetch/XHR/WebSocket). In this mode the Google-Fonts <link> is dropped (the
// font stacks fall back to system fonts), the SSE review layer is omitted, and
// the Mermaid CDN <script> is replaced by the vendored library inlined in
// place (only when the plan carries a diagram), so diagrams render at full
// parity with zero network. The SageOx enrichment reference links are
// preserved — top-level <a href> navigation is not CSP-blocked, so a published
// artifact stays a hub back into the Ledger.
Artifact bool
}
RenderOptions carries optional render-time context that isn't part of the enrichment Result.
type Resolution ¶
type Resolution struct {
Anchor string `json:"anchor"` // the item it resolves
State ResolutionState `json:"state"` // addressed | wontfix | verified
Commit string `json:"commit,omitempty"` // commit SHA that made the change
Note string `json:"note,omitempty"` // what the agent did / why wontfix
At time.Time `json:"at"`
}
Resolution is one agent disposition of a review item, append-logged.
func LoadResolutions ¶
func LoadResolutions(planDir string) ([]Resolution, error)
LoadResolutions reads the append log (latest entries last). Missing is empty.
type ResolutionState ¶
type ResolutionState string
ResolutionState is the agent's disposition of a review item.
const ( ResolutionAddressed ResolutionState = "addressed" // agent made the change ResolutionWontfix ResolutionState = "wontfix" // agent declined, with reason ResolutionVerified ResolutionState = "verified" // human confirmed the fix )
type Result ¶
type Result struct {
// SchemaVersion stamps the serialized annotations.json shape (set to
// SchemaVersion on write) so a future reader can detect an older layout.
SchemaVersion string `json:"schema_version,omitempty"`
Annotations []Annotation `json:"annotations"`
Context []ContextItem `json:"context"`
Signals SignalSummary `json:"signals"`
// DiagramHints are deterministic per-section diagram suggestions (which
// Mermaid/timeline form fits the structure ox detected). Empty when no
// section had strong enough structure to suggest one.
DiagramHints []DiagramHint `json:"diagram_hints,omitempty"`
// VizHints are deterministic per-section data-visualization suggestions
// (which parameterized catalog pattern fits — risk-matrix, file-impact-map,
// …), each renderable via `ox plan viz render <id> --data`. Empty when no
// section matched a pattern's use: signal strongly enough.
VizHints []VizHint `json:"viz_hints,omitempty"`
// MockupSection is the heading of the section that changes a user-facing
// surface (one the reader sees on screen), or "" when the plan changes nothing
// visible. Detected deterministically at enrich (mockupCues, precision-gated
// like the diagram hints) and surfaced in Guidance so every agent is told to
// show a mockup pre-authoring; the render-time craft lint only checks it was
// realized. Environment-independent, so it's safe in `ox plan enrich --json`.
MockupSection string `json:"mockup_section,omitempty"`
// Guidance is a concise, cross-agent authoring contract for rendering a
// fantastic HTML plan (decision-first, ten-minute reader, diagrams over
// prose). It folds in the DiagramHints so the agent gets plan-specific
// direction, not a generic spec. Empty for a trivial/empty plan.
Guidance string `json:"guidance,omitempty"`
}
Result is the full output of Enrich: deterministic annotations, the context bundle, and the signal summary.
func Enrich ¶
Enrich runs every registered detector and retriever against the plan, FAIL-OPEN: a panic or error in any one detector/retriever is logged and skipped, never aborting the others. It aggregates the annotations and context items, computes a deterministic SignalSummary, and returns a sorted, deduped Result.
ox makes NO network or LLM call here — detectors and retrievers read only local data. Round 2 owns their implementations.
type Retriever ¶
type Retriever interface {
Name() string
Retrieve(ctx context.Context, in Input, gitRoot string) ([]ContextItem, error)
}
Retriever produces context-bundle items. Also fail-open.
type ReviewServerState ¶ added in v0.11.1
type ReviewServerState struct {
Port int `json:"port"` // last port actually served on
Token string `json:"token"` // review token the served pages carry
}
ReviewServerState is the persisted identity of a plan's review server.
func LoadReviewServerState ¶ added in v0.11.1
func LoadReviewServerState(gitRoot, planDirName string) (ReviewServerState, bool)
LoadReviewServerState reads a plan's persisted server identity. ok is false when there is none (first review, no ledger, or unreadable state).
type SignalSummary ¶
type SignalSummary struct {
Collisions int `json:"collisions"`
PriorArt int `json:"prior_art"`
ExpertRoutes int `json:"expert_routes"`
Material bool `json:"material"`
Files int `json:"files"`
Steps int `json:"steps"`
NonTrivial bool `json:"non_trivial"`
}
SignalSummary is the deterministic rollup of which signals fired.
Material is the TEAM-CONTEXT axis: true when the plan warrants surfacing a nudge because team context had something to say (any collision OR expert-route OR at least one strong prior-art hit).
NonTrivial is the STRUCTURAL axis, independent of team context: true when the plan is substantial enough to warrant an enriched HTML render for human review even on greenfield work where zero team-context signals fire — multi-file (Files >= 2) OR many-step (Steps >= 5). Files counts distinct file references cited across all sections; Steps counts H2 sections (excluding the preamble). These mirror the prime non-triviality criteria; hotspot/open-PR is already covered by Material, and "architectural" is left to agent judgment.
type VizHint ¶ added in v0.11.0
type VizHint struct {
Section string `json:"section"` // H2 heading the hint applies to
PatternID string `json:"pattern_id"` // catalog id, e.g. "risk-matrix" (renderable via `ox plan viz render`)
Reason string `json:"reason"` // the trigger terms matched, in one clause
Param string `json:"param,omitempty"` // the pattern's `param:` JSON skeleton to fill and render
}
VizHint is the data-visualization counterpart of DiagramHint: a per-section suggestion of which PARAMETERIZED catalog pattern (one with a deterministic `ox plan viz render` renderer — risk-matrix, file-impact-map, cost-waterfall, stat-cards, flag-rollout-matrix, …) fits a section. It closes the gap that DiagramHint only covers Mermaid/CSS diagram FORMS, leaving the data-viz catalog invisible to content-aware matching. The match signal is DERIVED from each pattern's `use:` line (no separate catalog field) — see computeVizHints.
Param carries the matched pattern's `param:` JSON skeleton so the agent goes straight from section→pattern→fill-in-the-blanks, instead of round-tripping `ox plan viz <id>` to recall the shape. This is the most "auto" an autovisualizer can be from INSIDE another coding agent (Claude, Codex, …): ox can't render into a host surface it doesn't own, but it can pre-stage the exact data shape to fill.
type VizPattern ¶
type VizPattern struct {
ID string `json:"id"` // stable slug, e.g. "sparkline"
Use string `json:"use"` // when to reach for it
Why string `json:"why"` // the cognitive payoff
Param string `json:"param,omitempty"` // data-shape hint when `ox plan viz render <id> --data` is supported
Body string `json:"body"` // copy-paste snippet(s) + any notes
}
VizPattern is one catalog entry.
func VizCatalog ¶
func VizCatalog() []VizPattern
VizCatalog parses and returns every visualization pattern, in document order.
func VizPatternByID ¶
func VizPatternByID(id string) (VizPattern, bool)
VizPatternByID returns the pattern with the given id (case-insensitive), or ok=false when none matches.