memory

package
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 31, 2026 License: Apache-2.0 Imports: 30 Imported by: 0

Documentation

Overview

Package memory implements the agent-facing operations: BuildContextPack (M2 T2.7), and — added later — the structured update pipeline (M3) and staging engine (M5).

fetch.go assembles the Markdown context pack that memory.fetch_context returns. The algorithm follows design doc v0.4.1 §20.5:

  1. Empty query → bootstrap pack (current.<branch>.md + current.shared.md + conventions.md + a compact index.md summary).
  2. Non-empty query → Index.Search + ranking + section-level assembly, with the bootstrap files always prepended.

Budget enforcement is greedy: sections are appended in ranked order until the running character total would exceed Budget, then the remaining sections move to the Omitted list.

Index

Constants

View Source
const (
	StatusRebased      = "rebased"
	StatusSkippedClean = "skipped_clean"
)

Status values for RebaseResult. StatusRejected is reused from the orchestrator's vocabulary (rebase rejection uses the same exit-code semantics as a propose_update rejection).

View Source
const (
	ReasonForceRequired     = "force_required"
	ReasonUnresolvableDrift = "unresolvable_drift"
	ReasonRebasePlanFailed  = "rebase_plan_failed"
	ReasonRebaseSecret      = "rebase_secret_detected"
	ReasonRebasePIIDetected = "rebase_pii_detected"
	ReasonRebaseInvalidMD   = "rebase_invalid_markdown"
)

Reason codes for RebaseResult. Stable wire identifiers — CLI / tests match against them.

View Source
const (
	RejectionReasonUser       = "user_rejected"
	RejectionReasonTTLExpired = "ttl_expired"
)

RejectionReason values are the wire-stable strings written to the audit log's `reason` field. Stable so log scrapers can filter on them.

View Source
const (
	StatusApplied  = "applied"
	StatusStaged   = "staged"
	StatusRejected = "rejected"
)

Status values returned in ProposeResponse.Status.

View Source
const (
	ReasonInvalidIntent          = "invalid_intent"
	ReasonInvalidOperation       = "invalid_operation"
	ReasonNoOperations           = "no_operations"
	ReasonValidationFailed       = "validation_failed"
	ReasonInvalidPath            = "invalid_path"
	ReasonUnknownCategory        = "unknown_category"
	ReasonServerManagedCategory  = "server_managed_category"
	ReasonReadError              = "read_error"
	ReasonPlanFailed             = "plan_failed"
	ReasonSpliceFailed           = "splice_failed"
	ReasonInvalidMarkdown        = "invalid_markdown"
	ReasonAllowlistParseError    = "allowlist_parse_error"
	ReasonSecretDetected         = "secret_detected"
	ReasonProvenanceViolation    = "provenance_violation"
	ReasonServerOnlyCategory     = "server_only_category"
	ReasonLockHeld               = "lock_held"
	ReasonTargetDrift            = "target_drift"
	ReasonStagingNotFound        = "staging_not_found"
	ReasonAllowlistLimitExceeded = "allowlist_limit_exceeded"
	ReasonPIIDetected            = "pii_detected"
	ReasonWriteOnceViolation     = "write_once_violation"
	ReasonArchiveExists          = "archive_exists"
)

Reject reason codes. These are part of the propose_update wire contract: callers (CLI, MCP tool consumers, evals) match against them. Add a new code rather than reusing an existing one when introducing a new failure mode.

View Source
const LatestRef = "--latest"

LatestRef is the sentinel ResolveStagingID accepts to mean "the most recently staged proposal". Staging IDs start with a UTC timestamp, so the lexically-largest id is the newest.

View Source
const PIIPrefix = "pii_"

PIIPrefix is the prefix every PII Finding.Type carries. Classifiers use it to split mixed scan results into "credential present" vs "only PII".

Variables

AllIntents is the canonical list used by Validate / docs.

View Source
var ConfidenceLevels = []string{
	"confirmed",
	"inferred",
	"user-provided",
	"stale",
	"unknown",
}

ConfidenceLevels are the allowed values for the Confidence field on a propose_update input (design doc §23.5). Empty Confidence is also accepted — the orchestrator treats it as "unknown".

View Source
var ErrNoStaged = errors.New("no matching staged proposal")

ErrNoStaged is returned by ResolveStagingID when no staged proposal matches the reference (empty queue, or no prefix match).

View Source
var SourceTypes = []string{
	"file", "test", "user", "session", "inference", "external",
}

SourceTypes are the canonical source-type strings. Used as a hint; the per-category policy in schema.Provenance can narrow this further.

Functions

func AppendRejection

func AppendRejection(memDir string, entry RejectionEntry) error

AppendRejection serialises entry as one JSON object on its own line and appends it to meta/rejection-log.jsonl, creating the file (and the parent meta/ directory) on demand. Append is O_APPEND so concurrent writes within a single process get atomic per-line semantics from the OS; the in-process mutex above keeps the field order stable.

Best-effort: a write failure does NOT cause the calling reject / sweep to fail. The bytes-on-disk reality is the staging dir being gone; the log is a downstream audit channel.

func BuildIndexContent

func BuildIndexContent(memDir string, sch *schema.Schema) ([]byte, error)

BuildIndexContent walks memDir and produces the index.md routing file per design §10.1. The output is DETERMINISTIC for a given tree — no wall-clock timestamps in the body — so regeneration only changes the file when the underlying memory actually changed, avoiding needless git churn and keeping tests stable.

Sections produced:

# Agent Memory Index + @generated comment
## Always include   — local current state + conventions
## Topic map        — decisions (by status), pitfalls (count), modules
## Archive          — archived-context count
## Freshness        — placeholder until per-section freshness lands

func CheckAllowlistLimits

func CheckAllowlistLimits(regions []AllowlistRegion, limits AllowlistLimits) string

CheckAllowlistLimits verifies regions against limits. Returns an empty string when all checks pass; a human-readable description of the first hit limit otherwise. The orchestrator passes this string through as the rejection Message.

Limits are checked in this order (lowest-impact wins for the error message; if both region-count and total-bytes are exceeded, the region-count message is what the user sees first):

  1. MaxRegionsPerFile — count of regions
  2. MaxBytesPerRegion — single largest region
  3. MaxBytesPerFile — sum across all regions

func ClassifyFindings

func ClassifyFindings(findings []Finding) string

ClassifyFindings inspects a mixed Findings slice and returns the appropriate reject reason: ReasonSecretDetected if ANY finding is a credential (Type doesn't start with PIIPrefix), ReasonPIIDetected otherwise. Used by the orchestrator + rebase paths so callers don't have to walk the slice manually.

An empty slice returns "" — caller checks len() first.

func IsAllowlisted

func IsAllowlisted(start, end int, regions []AllowlistRegion) bool

IsAllowlisted reports whether the byte range [start, end) is fully contained in any of regions.

func IsValidConfidence

func IsValidConfidence(c string) bool

IsValidConfidence reports whether c is one of ConfidenceLevels or empty.

func IsValidIntent

func IsValidIntent(i Intent) bool

IsValidIntent reports whether i is a recognised Intent.

func RegenerateIndex

func RegenerateIndex(memDir string, sch *schema.Schema) (bool, error)

RegenerateIndex rebuilds index.md and writes it atomically IF the new content differs from what's on disk. Returns whether the file changed.

Best-effort by contract: callers in the apply path ignore the error (the durable bytes already landed; a stale index can be rebuilt via `agent-memory rebuild-index`). The "changed" return lets the apply path decide whether to include index.md in a git auto-stage batch.

func ResolveStagingID

func ResolveStagingID(memDir, ref string) (string, error)

ResolveStagingID turns a user-supplied reference into a full staging ID. Accepts:

  • LatestRef ("--latest") or "latest" → the newest staged proposal.
  • an exact staging ID → returned as-is (even if it's also a prefix of others).
  • a unique prefix (Git-style) → the single matching ID.

Errors: ErrNoStaged (no match / empty queue), *ErrAmbiguousPrefix (more than one prefix match).

func StagingExists

func StagingExists(memDir, stagingID string) bool

StagingExists reports whether staging/<stagingID>/ is a directory under memDir. Cheap probe used by review/apply/reject before doing heavier work.

func ValidateProvenance

func ValidateProvenance(policy schema.Provenance, ctx ProvenanceContext) []string

ValidateProvenance checks a ProvenanceContext against a category's Provenance policy from the schema. Returns a slice of human-readable violations; an empty slice means the proposal satisfies the policy.

Checks (in order):

  • Confidence is one of ConfidenceLevels (or empty).
  • Every Source.Type is in schema's AllowedSourceTypes (when set).
  • No Source.Type is in schema's ForbiddenSourceTypes.
  • If Required → at least one Source must be present.
  • If RequiredForNewSections AND ctx.IsNewSection → at least one Source.

Types

type AffectedSection

type AffectedSection struct {
	File      string `json:"file"`
	SectionID string `json:"section_id,omitempty"`
}

AffectedSection identifies one (file, section) touched by an applied proposal. Part of the design §15.2 Applied output.

type AllowlistLimits

type AllowlistLimits struct {
	MaxBytesPerFile   int // sum of bytes inside all regions
	MaxRegionsPerFile int // count of regions
	MaxBytesPerRegion int // single largest region
}

AllowlistLimits caps how much of a file's content can sit inside secret-scanner allowlist regions. The allowlist is a per-region escape hatch for documenting token FORMATS (a few dozen bytes per example). Without a cap, a careless or malicious agent could wrap a 5 KB region around a real credential and bypass the scanner.

A field set to 0 means "unlimited for that dimension" (the orchestrator skips the corresponding check). Defaults shipped via manifest.security.allowlist_limits: see config.DefaultManifest.

type AllowlistRegion

type AllowlistRegion struct {
	ByteStart int    `json:"byte_start"`
	ByteEnd   int    `json:"byte_end"`
	Reason    string `json:"reason"`
}

AllowlistRegion is a byte range [ByteStart, ByteEnd) in scanned content that the secret scanner must skip. Produced by ExtractAllowlistRegions from paired HTML-comment markers in Markdown source.

func ExtractAllowlistRegions

func ExtractAllowlistRegions(content []byte) ([]AllowlistRegion, error)

ExtractAllowlistRegions scans content for paired allowlist markers and returns the byte ranges between them.

The marker format (verbatim):

<!-- @secret-scan: allow reason="documentation example" -->
...allowed content...
<!-- @secret-scan: end -->

Region boundaries: ByteStart is the offset right after the opening marker's '>' character; ByteEnd is the offset of the closing marker's '<' character. The markers themselves are NOT inside the allowed range (so a secret that happened to live in the comment text would still be flagged — but markers don't look like secrets in practice).

Errors:

  • open marker without a closing one in scope
  • close marker with no preceding open
  • nested open (a second open before the first has closed)
  • empty reason on an open marker

An empty input or input without any markers returns nil regions and nil error.

type AppendSection

type AppendSection struct {
	FilePath        string
	ParentSectionID string
	Heading         string
	Level           int
	Content         []byte
}

AppendSection adds a new section to the file. If ParentSectionID is set, the new section is inserted at the "first child slot" of that parent — the position of the parent's first existing child (a section inside the parent's byte range with strictly greater HeadingLevel). When the parent has no children, the insert falls back to parent.ByteEnd (just before the parent's next sibling/ancestor heading or EOF).

Without ParentSectionID the new section is appended at EOF.

Why the "first child slot" semantic: a level-1 parent subsumes ALL subsequent sections until the next level-1 heading (or EOF). Inserting at parent.ByteEnd would put the new child AFTER every other section in the file, which is rarely what the agent intends. Inserting before the first existing child keeps the new section visually "at the top of the parent's contents" — matching how a human would add a sub-section to a chapter.

Content must start with the heading line. The orchestrator's AssignMissingIDs pass will inject an @id anchor on the next index pass if Content doesn't carry one.

func (*AppendSection) Kind

func (op *AppendSection) Kind() string

func (*AppendSection) Path

func (op *AppendSection) Path() string

func (*AppendSection) Plan

func (op *AppendSection) Plan(src []byte) (agentmd.SpliceOp, error)

func (*AppendSection) Targets

func (op *AppendSection) Targets() []OperationTarget

func (*AppendSection) Validate

func (op *AppendSection) Validate(sch *schema.Schema) error

type AppendToSection

type AppendToSection struct {
	FilePath   string
	SectionID  string
	Heading    string
	Level      int
	Occurrence int
	Content    []byte
}

AppendToSection appends Content to the END of an existing section (just before the next heading at same/higher level). Heading stays untouched. Used for bullet-level entries (pitfalls, session logs).

func (*AppendToSection) Kind

func (op *AppendToSection) Kind() string

func (*AppendToSection) Path

func (op *AppendToSection) Path() string

func (*AppendToSection) Plan

func (op *AppendToSection) Plan(src []byte) (agentmd.SpliceOp, error)

func (*AppendToSection) Targets

func (op *AppendToSection) Targets() []OperationTarget

func (*AppendToSection) Validate

func (op *AppendToSection) Validate(sch *schema.Schema) error

type ApplyResult

type ApplyResult struct {
	StagingID string        `json:"staging_id"`
	Status    string        `json:"status"`
	Reason    string        `json:"reason,omitempty"`
	Message   string        `json:"message,omitempty"`
	Files     []string      `json:"files,omitempty"`
	Drift     []DriftReport `json:"drift,omitempty"`

	// AutoStage carries git auto-stage / commit outcomes when the apply
	// produced writes AND manifest.git.auto_stage_changes is true. nil
	// on rejection; nil on success when the feature is disabled.
	AutoStage *AutoStageResult `json:"auto_stage,omitempty"`
}

ApplyResult is what ApplyStaged returns. Status is one of StatusApplied / StatusRejected. On rejection, Reason is a stable code:

  • ReasonStagingNotFound — no staging/<id>/ directory.
  • ReasonTargetDrift — the disk state changed since stage.
  • ReasonLockHeld — another writer holds the advisory lock.

func ApplyStaged

func ApplyStaged(ctx context.Context, stagingID string, deps UpdateDeps) (res *ApplyResult, err error)

ApplyStaged applies a staged proposal:

  1. Acquires the cross-process advisory lock (same lock ProposeUpdate uses).
  2. Loads proposal.json + target-checksums.json.
  3. CheckDrift on every target; any drift → ApplyResult{Status: rejected, Reason: target_drift, Drift: [...]} (no I/O).
  4. Reads each staged file under staging/<id>/files/ and WriteAtomics it to its destination under memDir.
  5. Re-indexes touched files (best-effort; failures don't roll back).
  6. Removes staging/<id>/ from disk.

Errors only for infrastructure failures (lock open, JSON parse fatal, destination write). Application-level rejections (drift, missing staging dir, lock held) come back in ApplyResult, NOT as Go errors. This mirrors ProposeUpdate's contract so the MCP wrapper can stay simple.

func RejectStaged

func RejectStaged(memDir, stagingID string) (*ApplyResult, error)

RejectStaged removes staging/<stagingID>/ from disk and appends an audit entry to meta/rejection-log.jsonl with reason="user_rejected". Cheap; no drift checks, no lock acquisition (the proposal isn't touching any other memory file).

Returns ReasonStagingNotFound through the ApplyResult (NOT as a Go error) when the directory doesn't exist — symmetric with ApplyStaged's contract. Real filesystem errors during removal propagate as Go errors.

The rejection log write is best-effort: a logging failure doesn't undo the removal. See sweep.go for the shared rejectStagedWithReason helper that also serves the TTL sweeper.

type ArchiveSection

type ArchiveSection struct {
	FilePath    string
	SectionID   string
	Heading     string
	Level       int
	Occurrence  int
	ArchivePath string
	Replacement []byte // new content for the source section (heading + anchor + stub)
}

ArchiveSection copies a section's current content to a new archive file and replaces the source section (heading included) with a pointer/stub Replacement. The archive file is write-once: it must not already exist. Per design §15.8, archiving never destroys content and always stages.

This is a MULTI-FILE operation: Plan() handles the source-file splice, and ExtraFiles() produces the new archive file. The orchestrator wires both together.

func (*ArchiveSection) ExtraFiles

func (op *ArchiveSection) ExtraFiles(src []byte) ([]ExtraFile, error)

ExtraFiles copies the section's current bytes (heading included) into the archive file. src is the source file's bytes BEFORE this op's splice, so the section is still present.

func (*ArchiveSection) Kind

func (op *ArchiveSection) Kind() string

func (*ArchiveSection) Path

func (op *ArchiveSection) Path() string

func (*ArchiveSection) Plan

func (op *ArchiveSection) Plan(src []byte) (agentmd.SpliceOp, error)

func (*ArchiveSection) Targets

func (op *ArchiveSection) Targets() []OperationTarget

func (*ArchiveSection) Validate

func (op *ArchiveSection) Validate(sch *schema.Schema) error

type AutoStageResult

type AutoStageResult struct {
	// Staged is the list of repo-root-relative paths git add was called
	// with. Forward slashes.
	Staged []string `json:"staged,omitempty"`

	// CommitSHA is non-empty when auto_commit was on AND a commit was
	// actually created (nothing staged ≠ commit).
	CommitSHA string `json:"commit_sha,omitempty"`

	// Errors, if any. Auto-stage NEVER fails an apply — errors are
	// collected for surfacing only. Bytes are durable; git is a
	// best-effort side channel.
	Errors []string `json:"errors,omitempty"`

	// Skipped is true when auto-stage logic ran but produced no work
	// (manifest disabled, not-a-repo, nothing tracked). Distinguishes
	// "ran and did nothing" from "didn't run".
	Skipped bool `json:"skipped,omitempty"`
}

AutoStageResult reports what auto-stage did. Returned through the apply paths (Direct apply and ApplyStaged) so the CLI can surface it in its human + JSON output. Empty / zero-valued when auto-stage is disabled or the project isn't a git repo.

type ContextMetadata

type ContextMetadata struct {
	ActiveBranch    string   `json:"active_branch,omitempty"`
	BudgetUsed      int      `json:"budget_used"`
	BudgetRemaining int      `json:"budget_remaining"`
	StaleWarnings   []string `json:"stale_warnings,omitempty"`
}

ContextMetadata is sidecar info the agent can use to decide follow-up behaviour without re-fetching.

type CreateFile

type CreateFile struct {
	FilePath string
	Content  []byte
	IfExists string
}

CreateFile creates a new file with Content at FilePath. IfExists controls what to do when the file already exists at apply time:

"reject" (default) — fail; the orchestrator emits "target_drift".
"append"           — append Content to the existing file.
"replace"          — overwrite the file with Content.

func (*CreateFile) Kind

func (op *CreateFile) Kind() string

func (*CreateFile) Path

func (op *CreateFile) Path() string

func (*CreateFile) Plan

func (op *CreateFile) Plan(src []byte) (agentmd.SpliceOp, error)

func (*CreateFile) Targets

func (op *CreateFile) Targets() []OperationTarget

func (*CreateFile) Validate

func (op *CreateFile) Validate(sch *schema.Schema) error

type DriftPolicy

type DriftPolicy int

DriftPolicy controls how the staging engine (M5) re-validates an operation's target at apply time. See design doc v0.4.1 §16.4.

const (
	// RequireSectionContentMatch — used by replace/archive/remove/rename
	// where the operation is only meaningful against a specific snapshot
	// of the target section. Drift = section's content hash changed.
	RequireSectionContentMatch DriftPolicy = iota

	// RequireSectionResolvable — used by append_to_section. The section
	// may have grown since staging; we only require it to still resolve
	// by its ID.
	RequireSectionResolvable

	// RequireFileAbsent — used by create_file with if_exists=reject.
	// Drift = file appeared between stage and apply.
	RequireFileAbsent

	// RequireFilePresent — used by create_file with if_exists=append/replace,
	// and by append_section against a parent_section_id.
	RequireFilePresent
)

func (DriftPolicy) MarshalJSON

func (p DriftPolicy) MarshalJSON() ([]byte, error)

MarshalJSON renders DriftPolicy as its String() form, not as the integer iota value. Staged targets must be human-readable on disk.

func (DriftPolicy) String

func (p DriftPolicy) String() string

String renders the DriftPolicy as a stable snake_case identifier used in staged target-checksums.json and CLI output. Keep these values stable — the M5 apply path matches against them.

func (*DriftPolicy) UnmarshalJSON

func (p *DriftPolicy) UnmarshalJSON(b []byte) error

UnmarshalJSON reverses MarshalJSON for the M5 apply path, which round- trips target-checksums.json off disk. An unknown identifier is an error — silently mapping it to the zero value would mask staging-file corruption.

type DriftReport

type DriftReport struct {
	Path      string `json:"path"`
	SectionID string `json:"section_id,omitempty"`
	Policy    string `json:"policy"`
	Expected  string `json:"expected"`
	Found     string `json:"found"`
}

DriftReport describes a single target whose current disk state no longer matches the snapshot recorded at stage time. Surfaced verbatim to humans (review/apply CLI output) and to agents (response JSON) so they can decide between re-staging and abandoning.

func CheckDrift

func CheckDrift(memDir string, t OperationTarget) (*DriftReport, error)

CheckDrift re-validates one OperationTarget against the current disk state of memDir. Returns:

  • (nil, nil) → no drift; safe to apply.
  • (*DriftReport, nil) → drift detected; report describes the diff.
  • (nil, err) → I/O failure unrelated to drift; caller decides.

Drift semantics by policy:

RequireSectionContentMatch: section must resolve by ID and its current
  ContentHash must match the stored Hash. A missing file / missing
  section / changed hash all count as drift.

RequireSectionResolvable: section must resolve by ID. Hash is ignored.
  A missing file / missing section count as drift.

RequireFileAbsent: the file must not exist. A present file is drift.

RequireFilePresent: the file must exist. An absent file is drift.

type ErrAmbiguousPrefix

type ErrAmbiguousPrefix struct {
	Prefix     string
	Candidates []string
}

ErrAmbiguousPrefix is returned when a prefix matches more than one staged proposal. Its message lists the candidates so the caller can disambiguate.

func (*ErrAmbiguousPrefix) Error

func (e *ErrAmbiguousPrefix) Error() string

type ExpiredProposal

type ExpiredProposal struct {
	StagingID  string `json:"staging_id"`
	Intent     string `json:"intent,omitempty"`
	Rationale  string `json:"rationale,omitempty"`
	StagedAt   string `json:"staged_at,omitempty"`
	AgeSeconds int    `json:"age_seconds"`
}

ExpiredProposal is the per-entry shape inside SweepResult.Expired. Keeps just enough metadata for humans to recognise what got removed without re-reading staging/<id>/proposal.json (which is already gone).

type ExtraFile

type ExtraFile struct {
	// Path is the forward-slash, memory-relative destination.
	Path string
	// Content is the full bytes to write. The orchestrator treats the
	// file as new (RequireFileAbsent) and write-once.
	Content []byte
}

ExtraFile is an additional file an operation produces beyond its primary Path() target. archive_section / remove_section use this to copy the archived section content into a brand-new archive/ file.

type ExtraFileProducer

type ExtraFileProducer interface {
	// ExtraFiles computes the additional files this op creates, derived
	// from src — the primary file's bytes at the moment the op runs,
	// BEFORE its own splice is applied. (archive_section reads the
	// section it's about to replace; remove_section reads the section it's
	// about to delete.)
	ExtraFiles(src []byte) ([]ExtraFile, error)
}

ExtraFileProducer is the optional interface an Operation implements when it writes to files beyond its primary Path(). The orchestrator type-asserts for it during the per-file planning loop and, if present, collects the extra files for validation + staging/apply.

Only archive_section and remove_section implement this today; the five original operations don't, so they're untouched.

type FetchDeps

type FetchDeps struct {
	Idx       *index.Index
	Schema    *schema.Schema
	Manifest  *config.Manifest
	MemoryDir string // absolute path to .agent-memory/
	Branch    git.BranchInfo

	// ChangedFiles are repo-relative paths with uncommitted changes,
	// resolved by the caller (CLI / MCP) via git.ChangedFiles. Feeds the
	// "decisions/pitfalls referencing changed files" ranking signal. Empty
	// is fine (no boost) — outside a git repo, or on a clean tree.
	ChangedFiles []string

	// Logger is optional; nil → discard. See UpdateDeps.log().
	Logger *slog.Logger
}

FetchDeps bundles the dependencies BuildContextPack needs. Callers (CLI fetch, MCP server handler) construct this and reuse it for the lifetime of one request.

type FetchRequest

type FetchRequest struct {
	Query          string
	Scope          []string
	Budget         int      // characters; 0 = use manifest default
	Include        []string // categories (advisory in M2; M3 enforces)
	ExcludeArchive bool     // hard exclude vs penalize
}

FetchRequest mirrors the memory.fetch_context MCP tool input.

type FetchResponse

type FetchResponse struct {
	Context              string          `json:"context"`
	IncludedFiles        []IncludedFile  `json:"included_files"`
	Omitted              []OmittedFile   `json:"omitted,omitempty"`
	SuggestedNextQueries []string        `json:"suggested_next_queries,omitempty"`
	ContextMetadata      ContextMetadata `json:"context_metadata"`
}

FetchResponse is the structured shape returned to the caller. The MCP tool serialises this verbatim; the CLI emits the Context field by default and the whole structure under --json.

func BuildContextPack

func BuildContextPack(ctx context.Context, req FetchRequest, deps FetchDeps) (resp *FetchResponse, err error)

BuildContextPack is the entry point. It dispatches between the bootstrap path (empty query) and the search path (non-empty query). All output goes into FetchResponse.

type Finding

type Finding struct {
	Type                string `json:"type"`
	Line                int    `json:"line"`
	ApproximateLocation string `json:"approximate_location"`
}

Finding describes one secret-scan hit. The actual matched bytes are intentionally NOT included — by policy (design doc §13.2 / §23.3) the scanner must not echo the full secret value back to the caller or to logs. Type + Line + ApproximateLocation is enough for the agent to locate and rewrite the offending content.

func Scan

func Scan(content []byte, opts ScanOpts) []Finding

Scan returns every secret-shaped hit in content, ordered by byte position. Returns nil (not an error) when content is empty.

Behaviour:

  • Each pattern in the rule set is checked against content. Matches fully inside an opts.Allowlist region are dropped.
  • If opts.EntropyThreshold and opts.EntropyMinLength are both > 0, a secondary pass flags any alphanumeric-ish token whose Shannon entropy meets the threshold.
  • Findings carry the type, line number (1-based), and a string location — NEVER the matched bytes.

type IncludedFile

type IncludedFile struct {
	Path         string `json:"path"`
	Reason       string `json:"reason"`
	Freshness    string `json:"freshness,omitempty"`
	Confidence   string `json:"confidence,omitempty"`
	SectionCount int    `json:"section_count,omitempty"`
}

IncludedFile describes one file (possibly contributing multiple sections) that ended up in the pack.

type Intent

type Intent string

Intent classifies what KIND of change the agent wants to make. Each intent maps to a specific approval slot in manifest.updates.approval; routing.go holds that mapping in one place so the orchestrator never has to think about category-vs-intent semantics.

The set is closed: an unknown intent is a hard reject ("invalid_intent") rather than a silent stage. This matches the design doc v0.4.1 §22.2 intent enumeration.

const (
	IntentUpdateCurrent     Intent = "update_current"
	IntentUpdateShared      Intent = "update_shared"
	IntentSessionLog        Intent = "session_log"
	IntentAddPitfall        Intent = "add_pitfall"
	IntentRecordDecision    Intent = "record_decision"
	IntentRefreshModule     Intent = "refresh_module"
	IntentUpdateConventions Intent = "update_conventions"
	IntentArchiveStale      Intent = "archive_stale"
)

type MemoryStatus

type MemoryStatus struct {
	MemoryVersion     string              `json:"memory_version"`
	Repo              string              `json:"repo"`
	ActiveBranch      string              `json:"active_branch,omitempty"`
	DurableFiles      int                 `json:"durable_files"`
	ArchiveFiles      int                 `json:"archive_files"`
	LocalSessions     int                 `json:"local_sessions"`
	LocalCurrentFiles int                 `json:"local_current_files"`
	OrphanLocalFiles  []string            `json:"orphan_local_files,omitempty"`
	IndexSizeBytes    int64               `json:"index_size_bytes"`
	CurrentSizeBytes  int64               `json:"current_size_bytes"`
	StagedUpdates     []StagedStatusEntry `json:"staged_updates,omitempty"`

	// StaleNotes lists files flagged "stale" by future per-section
	// freshness tracking. Currently always empty (mechanism unimplemented
	// — see design doc §20.3).
	StaleNotes []string `json:"stale_notes,omitempty"`

	Security MemoryStatusSecurity `json:"security"`
	Git      MemoryStatusGit      `json:"git"`
	Lock     MemoryStatusLock     `json:"lock"`
}

MemoryStatus matches the design-doc §15.11 output shape for the `memory.status` MCP tool. Same type is used by the CLI `status` subcommand so both transports return identical structured data.

Some fields are populated with conservative approximations until the underlying mechanism lands — they're documented inline.

func BuildStatus

func BuildStatus(ctx context.Context, deps StatusDeps) (*MemoryStatus, error)

BuildStatus walks the .agent-memory/ tree and assembles a MemoryStatus matching design §15.11. Read-only; never modifies any file. Acquires the advisory lock briefly to determine `lock.held` (best-effort).

type MemoryStatusGit

type MemoryStatusGit struct {
	TrackLocal           bool `json:"track_local"`
	TrackSessions        bool `json:"track_sessions"`
	IgnoredLocalState    bool `json:"ignored_local_state"`
	MergeDriverInstalled bool `json:"merge_driver_installed"`
}

MemoryStatusGit is the §15.11 `git` sub-block.

type MemoryStatusLock

type MemoryStatusLock struct {
	Held bool `json:"held"`
	// StaleRecoveriesLast24h would count crash-recovered locks in a 24h
	// window. The kernel handles lock release on process death so this
	// is informational and currently always 0 until persisted.
	StaleRecoveriesLast24h int `json:"stale_recoveries_last_24h"`
}

MemoryStatusLock is the §15.11 `lock` sub-block.

type MemoryStatusSecurity

type MemoryStatusSecurity struct {
	// LastSecretScan is one of "passed" | "n/a" | "failed". Currently
	// always "n/a" — there's no per-write scan history persisted. Future
	// work (M8b2) will populate this from a scan log.
	LastSecretScan string `json:"last_secret_scan"`

	// AllowlistedRegions is the total count of secret-scan allowlist
	// regions discovered across all durable .md files at status time.
	AllowlistedRegions int `json:"allowlisted_regions"`

	// UntrustedSources counts proposals with sources of type
	// "external" or "inference" recorded against durable files. Not yet
	// persisted; always 0 until proposal history tracking lands.
	UntrustedSources int `json:"untrusted_sources"`
}

MemoryStatusSecurity is the §15.11 `security` sub-block.

type OmittedFile

type OmittedFile struct {
	Path   string `json:"path"`
	Reason string `json:"reason"`
}

OmittedFile describes a candidate that was dropped (budget exceeded or relevance below threshold).

type Operation

type Operation interface {
	// Kind returns the operation type ("create_file", "replace_section", ...).
	Kind() string

	// Path returns the target memory-relative file path (forward-slash).
	Path() string

	// Validate runs op-specific structural checks: content parses as
	// Markdown, required fields are present, paths are well-formed.
	// The schema is passed so an op can consult per-category policy.
	// Category-level checks (server_managed, agent_writable) are the
	// orchestrator's responsibility.
	Validate(sch *schema.Schema) error

	// Targets returns the drift-check targets the staging engine should
	// verify at apply time. May be empty for ops with no drift concern.
	Targets() []OperationTarget

	// Plan returns the byte-range splice that, applied via
	// markdown.Splice(src, []SpliceOp{plan}), produces the desired
	// post-state. For create_file with if_exists=reject, src is expected
	// to be nil (file doesn't exist) and the splice covers [0, 0).
	Plan(src []byte) (agentmd.SpliceOp, error)
}

Operation is one structured Markdown edit. Concrete types live in this file (T3.2). Construct via ParseOperation from an OperationInput, or directly with a struct literal in tests.

func ParseOperation

func ParseOperation(in OperationInput) (Operation, error)

ParseOperation dispatches on in.Op to construct a concrete Operation. Returns a clear error for unknown op kinds.

type OperationInput

type OperationInput struct {
	Op              string `json:"operation"`
	Path            string `json:"path"`
	SectionID       string `json:"section_id,omitempty"`
	Heading         string `json:"heading,omitempty"`
	HeadingLevel    int    `json:"heading_level,omitempty"`
	Occurrence      int    `json:"occurrence,omitempty"`
	ParentSectionID string `json:"parent_section_id,omitempty"`
	Content         string `json:"content,omitempty"`
	IfExists        string `json:"if_exists,omitempty"`
	IfMissing       string `json:"if_missing,omitempty"`

	// M4 archival/rename fields.
	ArchivePath     string `json:"archive_path,omitempty"`      // archive_section, remove_section
	Replacement     string `json:"replacement,omitempty"`       // archive_section: new source-section body
	Reason          string `json:"reason,omitempty"`            // remove_section: why it's gone
	NewHeading      string `json:"new_heading,omitempty"`       // rename_heading
	NewHeadingLevel int    `json:"new_heading_level,omitempty"` // rename_heading; 0 = keep current
}

OperationInput is the JSON shape every operation deserialises from. Fields not relevant to a given op are omitted (omitempty); ParseOperation validates required fields per op type.

type OperationTarget

type OperationTarget struct {
	Path      string      `json:"path"`
	SectionID string      `json:"section_id,omitempty"`
	Policy    DriftPolicy `json:"policy"`
	Hash      string      `json:"hash,omitempty"`
}

OperationTarget describes a single (file, optional section) the operation depends on for its drift check. The orchestrator (T3.7) materialises Hash from disk at staging time.

func LoadStagedTargets

func LoadStagedTargets(memDir, stagingID string) ([]OperationTarget, error)

LoadStagedTargets reads staging/<stagingID>/target-checksums.json. Used by apply to re-verify drift policies against the current disk state.

type OwnerInfo

type OwnerInfo struct {
	ID   string `json:"id,omitempty"`
	Kind string `json:"kind,omitempty"` // "agent" | "cli" | ...
	OpID string `json:"op_id,omitempty"`
}

OwnerInfo identifies who is proposing the update. Used to populate the lock metadata (which is informational only — see internal/lock).

type ProposeRequest

type ProposeRequest struct {
	Intent     Intent           `json:"intent"`
	Rationale  string           `json:"rationale,omitempty"`
	Operations []OperationInput `json:"operations"`
	Sources    []Source         `json:"sources,omitempty"`
	Confidence string           `json:"confidence,omitempty"`
	Owner      OwnerInfo        `json:"owner,omitempty"`
}

ProposeRequest is the orchestrator's input. Mirrors the propose_update MCP tool input verbatim — see design doc v0.4.1 §22.

type ProposeResponse

type ProposeResponse struct {
	Status               string                    `json:"status"`
	Reason               string                    `json:"reason,omitempty"`
	Message              string                    `json:"message,omitempty"`
	Routing              Routing                   `json:"routing,omitempty"`
	StagingID            string                    `json:"staging_id,omitempty"`
	Files                []string                  `json:"files,omitempty"`
	Findings             []Finding                 `json:"findings,omitempty"`
	Violations           []schema.SectionViolation `json:"violations,omitempty"`
	ProvenanceViolations []string                  `json:"provenance_violations,omitempty"`

	// --- Applied output (design §15.2) ---
	// AppliedAt is the RFC3339 UTC timestamp of the write. Set on apply.
	AppliedAt string `json:"applied_at,omitempty"`
	// AffectedSections lists the (file, section_id) pairs the proposal's
	// operations touched. Set on apply.
	AffectedSections []AffectedSection `json:"affected_sections,omitempty"`
	// IndexUpdated reports whether the FTS shadow was refreshed. Set on
	// apply (true when deps.Idx was present).
	IndexUpdated bool `json:"index_updated,omitempty"`
	// Warnings carries non-fatal advisories. Always present on apply
	// (possibly empty) so consumers can rely on the field.
	Warnings []string `json:"warnings,omitempty"`

	// --- Staged output (design §15.2) ---
	// StagingTTLSeconds is the manifest's staging.ttl_seconds. Set on stage.
	StagingTTLSeconds int `json:"staging_ttl_seconds,omitempty"`
	// HumanApprovalRequired is always true on the staged path (staging
	// means a human must review). Set on stage.
	HumanApprovalRequired bool `json:"human_approval_required,omitempty"`
	// ReviewCommand is the exact CLI invocation to inspect the staged
	// proposal. Set on stage.
	ReviewCommand string `json:"review_command,omitempty"`

	// AutoStage reports git auto-stage / auto-commit outcomes when the
	// applied path produced a write AND manifest.git.auto_stage_changes
	// is true. nil on stage or reject; nil on apply when the feature is
	// off. See internal/memory/autostage.go.
	AutoStage *AutoStageResult `json:"auto_stage,omitempty"`
}

ProposeResponse is what the orchestrator returns to the caller. The MCP tool serialises this verbatim. Field set tracks design §15.2's three output shapes (Applied / Staged / Rejected).

func ProposeUpdate

func ProposeUpdate(ctx context.Context, req ProposeRequest, deps UpdateDeps) (resp *ProposeResponse, err error)

ProposeUpdate runs the full propose_update pipeline:

  1. Validate intent + non-empty operations.
  2. session_log intent: rewrite each op's path to sessions/<UTC-today>.md unless it already lives under sessions/.
  3. Parse + per-op Validate() against the schema.
  4. Validate paths (ValidateMemoryPath) and resolve each op's Category; reject unknown / server_managed categories.
  5. Acquire the .agent-memory/meta/lock advisory lock with WaitTimeout from manifest.Concurrency.
  6. For each unique file in the proposal: a. Read current bytes (empty if file is absent). b. Apply this file's ops SEQUENTIALLY in memory (each op's Plan sees the post-previous-op bytes). c. ValidateMarkdown on the final bytes. d. Per-section schema validation on the final bytes. e. ExtractAllowlistRegions on the final bytes. f. Scan(final, ScanOpts{Allowlist, ...}) — reject on any finding.
  7. Provenance validation against the dominant category's policy.
  8. Routing: combine per-op routings. server_only → reject; stage → stage; apply → apply.
  9. Apply (write atomic + re-index) OR Stage (write proposal artefacts).

Any single step's failure short-circuits to ProposeResponse{Status: rejected} with a stable Reason code.

type ProvenanceContext

type ProvenanceContext struct {
	// Sources from the proposal.
	Sources []Source

	// Confidence from the proposal. May be empty.
	Confidence string

	// IsNewSection is true when the operation creates a new section (e.g.,
	// append_section, replace_section with if_missing=append) rather than
	// modifying an existing one. Used by Provenance.RequiredForNewSections.
	IsNewSection bool
}

ProvenanceContext bundles the runtime facts the validator needs about a specific propose_update operation.

type RebaseResult

type RebaseResult struct {
	StagingID string        `json:"staging_id"`
	Status    string        `json:"status"`
	Reason    string        `json:"reason,omitempty"`
	Message   string        `json:"message,omitempty"`
	Forced    bool          `json:"forced,omitempty"`
	Drift     []DriftReport `json:"drift,omitempty"`
	Files     []string      `json:"files,omitempty"`    // staged files updated by rebase
	Findings  []Finding     `json:"findings,omitempty"` // populated on rebase_secret_detected
}

RebaseResult is what RebaseStaged returns.

func RebaseStaged

func RebaseStaged(ctx context.Context, stagingID string, deps UpdateDeps, force bool) (res *RebaseResult, err error)

RebaseStaged attempts to make a staged proposal applyable again after the target disk state changed since stage time.

Phases:

  1. Acquire the cross-process advisory lock.
  2. Load proposal.json + target-checksums.json.
  3. CheckDrift every target. Classify: - file_present drift (file gone) → hard block - file_absent drift (file appeared) → hard block - section_resolvable drift (section gone) → hard block - section_content_match where section is gone → hard block - section_content_match where only hash differs → SOFT (rebaseable with --force; we accept the new base bytes as the planning input)
  4. No drift at all → return Status=skipped_clean.
  5. Any hard block → return rejected with reason=unresolvable_drift.
  6. All soft but --force not set → return rejected with reason=force_required.
  7. Re-plan: walk proposal.Request.Operations grouped by path; for each file, read current disk bytes, ParseOperation + Validate(schema) + Plan + Splice sequentially. The post-state per file becomes the new staging/<id>/files/<path>.
  8. Re-validate the new staged bytes (ValidateMarkdown + allowlist+Scan). A re-splice that introduces a secret rejects the rebase without touching disk.
  9. WriteAtomic the new staged files. Update target-checksums.json with refreshed hashes for content_match targets.

Provenance is NOT re-checked — sources don't drift. Routing is NOT re-checked — the stored proposal already passed routing at stage time.

Returns a Go error only for infrastructure failures (lock open, I/O). Application-level rejections come back in RebaseResult, NOT as errors, symmetric with ProposeUpdate / ApplyStaged.

type RejectionEntry

type RejectionEntry struct {
	RejectedAt string   `json:"rejected_at"` // RFC3339 UTC
	Reason     string   `json:"reason"`      // one of RejectionReason* constants
	StagingID  string   `json:"staging_id"`
	Intent     string   `json:"intent,omitempty"` // copied from the staged Request
	Rationale  string   `json:"rationale,omitempty"`
	Files      []string `json:"files,omitempty"`
	StagedAt   string   `json:"staged_at,omitempty"`   // when the proposal was originally staged
	AgeSeconds int      `json:"age_seconds,omitempty"` // RejectedAt - StagedAt
}

RejectionEntry is one row in meta/rejection-log.jsonl, recording a staged proposal that was discarded (either by the user or by the TTL sweeper). JSON tags match the on-disk format byte-for-byte.

func ListRejections

func ListRejections(memDir string) ([]RejectionEntry, error)

ListRejections reads meta/rejection-log.jsonl in full and returns the entries in file order (chronological because the log is append-only). A missing log file returns (nil, nil) — that's the normal state for a fresh project.

Malformed JSON lines are silently skipped. This is intentional: an audit log is a forensic tool; a corrupted line shouldn't prevent the rest from being readable.

type RemoveSection

type RemoveSection struct {
	FilePath    string
	SectionID   string
	Heading     string
	Level       int
	Occurrence  int
	ArchivePath string
	Reason      string // why it's being removed; recorded as a comment in the archive
}

RemoveSection archives a section to a new write-once archive file, then splices the section out of the source entirely (heading included). Per design §15.9, removal is archive-first — content is preserved in archive/ before the source loses it — and always stages.

func (*RemoveSection) ExtraFiles

func (op *RemoveSection) ExtraFiles(src []byte) ([]ExtraFile, error)

ExtraFiles archives the section content. When Reason is set, it's prepended as an HTML comment so the archive file records WHY the section was removed without affecting rendered output.

func (*RemoveSection) Kind

func (op *RemoveSection) Kind() string

func (*RemoveSection) Path

func (op *RemoveSection) Path() string

func (*RemoveSection) Plan

func (op *RemoveSection) Plan(src []byte) (agentmd.SpliceOp, error)

func (*RemoveSection) Targets

func (op *RemoveSection) Targets() []OperationTarget

func (*RemoveSection) Validate

func (op *RemoveSection) Validate(sch *schema.Schema) error

type RenameHeading

type RenameHeading struct {
	FilePath        string
	SectionID       string
	Heading         string
	Level           int
	Occurrence      int
	NewHeading      string
	NewHeadingLevel int // 0 = keep current level
}

RenameHeading changes a section's heading text (and optionally its level, constrained to ±1) while preserving the @id anchor and all bytes outside the heading line. Per design §15.10.

func (*RenameHeading) Kind

func (op *RenameHeading) Kind() string

func (*RenameHeading) Path

func (op *RenameHeading) Path() string

func (*RenameHeading) Plan

func (op *RenameHeading) Plan(src []byte) (agentmd.SpliceOp, error)

func (*RenameHeading) Targets

func (op *RenameHeading) Targets() []OperationTarget

func (*RenameHeading) Validate

func (op *RenameHeading) Validate(sch *schema.Schema) error

type ReplaceSection

type ReplaceSection struct {
	FilePath   string
	SectionID  string
	Heading    string
	Level      int
	Occurrence int
	Content    []byte
	IfMissing  string // "reject" (default) | "append" | "create_file"
}

ReplaceSection replaces the entire section identified by SectionID (or Heading+Level+Occurrence) with Content. Content must start with the same heading line and include the @id anchor (if the original had one).

func (*ReplaceSection) Kind

func (op *ReplaceSection) Kind() string

func (*ReplaceSection) Path

func (op *ReplaceSection) Path() string

func (*ReplaceSection) Plan

func (op *ReplaceSection) Plan(src []byte) (agentmd.SpliceOp, error)

func (*ReplaceSection) Targets

func (op *ReplaceSection) Targets() []OperationTarget

func (*ReplaceSection) Validate

func (op *ReplaceSection) Validate(sch *schema.Schema) error

type ReplaceSectionContent

type ReplaceSectionContent struct {
	FilePath   string
	SectionID  string
	Heading    string
	Level      int
	Occurrence int
	Content    []byte // body only; must NOT start with a heading
}

ReplaceSectionContent replaces a section's BODY only — the heading line and the immediately-following @id anchor (if any) and at most one blank line after the anchor are preserved. Useful when the heading/ID must stay stable but the body changes.

func (*ReplaceSectionContent) Kind

func (op *ReplaceSectionContent) Kind() string

func (*ReplaceSectionContent) Path

func (op *ReplaceSectionContent) Path() string

func (*ReplaceSectionContent) Plan

func (op *ReplaceSectionContent) Plan(src []byte) (agentmd.SpliceOp, error)

func (*ReplaceSectionContent) Targets

func (op *ReplaceSectionContent) Targets() []OperationTarget

func (*ReplaceSectionContent) Validate

func (op *ReplaceSectionContent) Validate(sch *schema.Schema) error

type Routing

type Routing struct {
	Mode   schema.ApprovalMode
	Reason string
}

Routing is the resolved approval decision for one operation under one intent. Mode comes from the manifest's per-slot ApprovalPolicy; Reason is a human-readable trace of how we got there (which slot was consulted, any overrides, etc.) — surfaced in propose_update's response so the agent can learn why a proposal was staged vs applied.

func CombineRoutings

func CombineRoutings(routings []Routing) Routing

CombineRoutings merges per-op Routings into a single proposal-level decision. Rules (most restrictive wins):

  • any server_only → reject (the orchestrator translates this to a "server_only_category" rejection; routing itself just reports the mode).
  • any stage → stage.
  • else → apply.

Reason is the concatenation of contributing routings' reasons.

func DecideRouting

func DecideRouting(intent Intent, op Operation, manifest *config.Manifest) (Routing, error)

DecideRouting picks the approval slot for (intent, op) and returns the resolved Mode from the manifest plus a Reason trace.

The intent → slot mapping (manifest.updates.approval.<slot>):

update_current       → current
update_shared        → current_shared
session_log          → sessions
add_pitfall + append_to_section → pitfalls_append
add_pitfall + anything else     → pitfalls_replace
record_decision      → decisions
refresh_module       → modules
update_conventions   → conventions
archive_stale        → archive

add_pitfall is the only intent whose slot depends on the operation kind: append-style updates apply without review (low-risk, additive) while section-level rewrites require staging (high-risk, can drop knowledge). See manifest defaults in config.DefaultManifest.

Returns an error only when the intent is not in the recognised set; callers should map that to a "reject: invalid_intent" response.

type ScanOpts

type ScanOpts struct {
	// Allowlist excludes the listed byte ranges from scanning. Build it
	// with ExtractAllowlistRegions over the same content.
	Allowlist []AllowlistRegion

	// EntropyThreshold and EntropyMinLength enable Shannon-entropy
	// detection over alphanumeric tokens. Tokens shorter than
	// EntropyMinLength are skipped. Tokens whose entropy meets or exceeds
	// EntropyThreshold are flagged as type "high_entropy".
	//
	// Set both to zero to disable entropy scanning. Recommended starting
	// values per design doc §23.2: threshold=4.5, min_length=32. Tighter
	// thresholds reduce false positives at the cost of catching fewer
	// real-but-unrecognised tokens.
	EntropyThreshold float64
	EntropyMinLength int

	// PIIScanSSNAndCC enables high-confidence PII detection (SSN shape +
	// credit card with Luhn validation). Both patterns are extremely rare
	// in legitimate technical content; default-on in DefaultManifest.
	PIIScanSSNAndCC bool

	// PIIScanEmail enables email-address detection. Opt-in because emails
	// appear legitimately in documentation (maintainer addresses, support
	// contacts, example syntax). Use allowlist regions for legitimate
	// occurrences when this is on.
	PIIScanEmail bool
}

ScanOpts configures Scan.

func DefaultScanOpts

func DefaultScanOpts() ScanOpts

DefaultScanOpts returns the recommended scanner configuration per design doc §23.2.

type Source

type Source struct {
	Type string `json:"type"`          // file | test | user | session | inference | external
	Ref  string `json:"ref,omitempty"` // file path, test name, etc.
}

Source describes one provenance entry attached to a propose_update proposal — where the agent claims this knowledge comes from. The fields mirror design doc v0.4.1 §23.5.

type StagedProposal

type StagedProposal struct {
	StagingID string         `json:"staging_id"`
	StagedAt  string         `json:"staged_at"`
	Request   ProposeRequest `json:"request"`
	Routing   Routing        `json:"routing"`
	Files     []string       `json:"files"`
}

StagedProposal is the envelope written to staging/<id>/proposal.json by stageProposal and read back by review/apply. Public so CLI renderers and test fixtures can inspect fields without re-parsing JSON themselves.

func ListStaged

func ListStaged(memDir string) ([]StagedProposal, error)

ListStaged returns every staged proposal under memDir/staging/, sorted ascending by staging-id (which gives chronological order because the IDs start with a UTC timestamp).

A missing staging/ directory returns (nil, nil) — that's the normal state right after `agent-memory init`.

Malformed staging entries (missing proposal.json, unreadable JSON) are silently skipped. The reasoning: review should still surface the rest of the queue; a separate `doctor` command will eventually inspect quarantined staging entries.

func LoadStaged

func LoadStaged(memDir, stagingID string) (*StagedProposal, error)

LoadStaged reads staging/<stagingID>/proposal.json and returns the parsed envelope. Returns a wrapped error when the directory or file is missing.

type StagedStatusEntry

type StagedStatusEntry struct {
	ID                  string   `json:"id"`
	Intent              string   `json:"intent"`
	AgeSeconds          int      `json:"age_seconds"`
	TTLRemainingSeconds int      `json:"ttl_remaining_seconds"`
	TargetFiles         []string `json:"target_files"`
	DriftDetected       bool     `json:"drift_detected"`
}

StagedStatusEntry is the per-proposal summary inside MemoryStatus.StagedUpdates.

type StatusDeps

type StatusDeps struct {
	MemoryDir     string
	Manifest      *config.Manifest
	Schema        *schema.Schema
	Branch        agentgit.BranchInfo
	MemoryVersion string // "0.X.Y" or "dev"
}

StatusDeps bundles the inputs BuildStatus needs. MemoryDir is the absolute path to .agent-memory/; Manifest + Schema are loaded; Branch is optional (zero value treated as "not in a git repo").

type SweepResult

type SweepResult struct {
	DryRun  bool              `json:"dry_run,omitempty"`
	Expired []ExpiredProposal `json:"expired,omitempty"`
	Removed []string          `json:"removed,omitempty"`
}

SweepResult is what SweepStale returns. Expired is every staging entry whose age exceeded TTL at sweep time; Removed is the subset that SweepStale actually deleted (matches Expired unless DryRun was true).

func SweepStale

func SweepStale(memDir string, ttl time.Duration, dryRun bool) (*SweepResult, error)

SweepStale walks .agent-memory/staging/ and removes every proposal whose (now - StagedAt) exceeds ttl. Each removal is also recorded in meta/rejection-log.jsonl with reason="ttl_expired".

  • dryRun=true → no filesystem changes; only Expired is populated.
  • ttl <= 0 → returns SweepResult{} immediately (sweep disabled).
  • Missing / malformed StagedAt on a staged proposal → conservative: the entry is skipped (kept on disk) so corrupted-but-recent proposals don't get nuked.
  • Per-entry failures during removal are collected silently; the remaining proposals still get processed. The user can re-run sweep after fixing the underlying issue.

SweepStale does NOT acquire the cross-process advisory lock — the caller (CLI subcommand) should. A propose_update writing into the same staging directory mid-sweep would be the only race, and the lock is the right primitive for that.

type UpdateDeps

type UpdateDeps struct {
	Manifest  *config.Manifest
	Schema    *schema.Schema
	MemoryDir string       // absolute path to .agent-memory/
	Idx       *index.Index // optional

	// Logger receives structured events from the orchestrator + staging
	// engine. Optional: nil → a discard logger (see log()). NEVER logs
	// matched secret bytes; only Finding.Type / .Line.
	Logger *slog.Logger
}

UpdateDeps bundles the orchestrator's dependencies. Index is optional — nil means "skip re-index after apply" (used in tests that don't care about the FTS shadow).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL