store

package module
v0.0.0-...-7097738 Latest Latest
Warning

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

Go to latest
Published: Jul 21, 2026 License: MIT Imports: 13 Imported by: 0

README

projx-store

ProjX's declared-knowledge layer — the second deterministic root (core = facts from code; store = facts you declare). No AI, no UI, no logic beyond records and a read/write API.

Two files, three scopes

Scope File Holds
Global your store (travels with you) recipes, conventions, style — how you work everywhere
Workspace your store this machine's repo list, default gate posture
Project project store (stays with the repo) ADRs, declared architecture, history, this project's gate rules

My file travels with me; the project's file stays with the project. Global recipes live only in your store. Finest gate rule wins.

Interface-first

Store (Put / Get / List / Delete) is the contract context, workflow, graph, and verify depend on. The concrete SQLite schema is deferred until real records teach its shape — define the contract now, learn the implementation later. Mem (in-memory) backs it today; a SQLite Store swaps in without touching callers.

Status

Active — SQLite backend live (modernc.org/sqlite, pure-Go, no CGo), three-migration schema (records table + updated_at + origin for last-write-wins merge), full test coverage for Mem, SQLite, Workspace, merge engine, and export. Wasm (GOARCH=wasm) build uses the Pulp host's storage.sqlite capability via build-tag seam.

go test ./...
# wasm gate
GOOS=wasip1 GOARCH=wasm go build ./...

License

MIT © 2026 SirNiklas9.

Documentation

Overview

Package store is ProjX's declared-knowledge layer — the second deterministic root (core = facts from code; store = facts you declare). Plain data only: no AI, no UI, no logic beyond records and a read/write API.

INTERFACE-FIRST by design: this contract is what context/workflow/graph/verify depend on. The concrete SQLite schema is deferred until real records teach its shape (maximal interface, minimal obligation). An in-memory impl backs it today.

Index

Constants

View Source
const (
	ClaudeBegin = "<!-- PROJX:BEGIN — managed by ProjX from the project store; edits inside are overwritten -->"
	ClaudeEnd   = "<!-- PROJX:END -->"
)
View Source
const (
	EnforcementHard     = "hard"     // gate denies; no override (secrets / off-limits floor)
	EnforcementSoft     = "soft"     // denies by default; a recorded reasoned override may proceed
	EnforcementAdvisory = "advisory" // context-injected only; never gated
)
View Source
const (
	TransportCLI        = "cli"
	TransportHTTPOpenAI = "http-openai"
)

Transport values.

View Source
const (
	// ProvenanceHuman: a person committed or approved the record.
	ProvenanceHuman = "human-confirmed"
	// ProvenanceGate: the verify gate ran a real build/test and it passed.
	ProvenanceGate = "gate-verified"
	// ProvenanceAgent: an AI committed it without an independent check.
	ProvenanceAgent = "agent-asserted"
)
View Source
const (
	SettingRoutePin   = "setting/route-pin"   // Body = class: hard-lock every task to this tier
	SettingRouteFloor = "setting/route-floor" // Body = class: minimum tier; triage may go above

)

Routing-setting record keys. They live in the store as KRoute records under a `setting/` key prefix so they are routing knowledge that travels + is journaled, yet are excluded from context injection (dropSettings).

View Source
const DefaultWorkerAllow = "git go gofmt goimports make npm npx pnpm yarn node projx-engine cat ls grep rg find sed awk head tail wc"

DefaultWorkerAllow is the SEED content and the fallback when the record is absent — so a legacy/un-reseeded project still gets a working worker floor. This is DATA (seeded into the store), not a hardcoded policy: the store record is the source of truth and fully editable; this constant only bootstraps it.

View Source
const DefaultWorkerDirective = "# YOUR ROLE: WORKER (executor) — READ THIS FIRST\n" +
	"You are a spawned worker agent, NOT the trunk. Your job is to COMPLETE this task yourself: " +
	"read, edit files, and run whatever tools are needed, then stop. Editing files is expected and permitted for you.\n" +
	"The project's \"dispatch, don't mutate\" convention below governs the TRUNK session ONLY — it does NOT apply to you. " +
	"Do NOT dispatch, delegate, spawn another agent, or ask to — just do the work directly.\n\n---\n\n"

DefaultWorkerDirective is the SEED content for the worker directive, and the fallback WorkerDirectiveText returns when the store has no record yet (a legacy project that hasn't re-seeded, or the store is briefly unreachable) — so a worker is never left without this reframing just because the record is missing.

View Source
const SessionSuggestText = "ProjX: you were asked to @remember something this session, but nothing was " +
	"committed to the project store. If it's worth keeping, commit it now:\n" +
	"    projx-engine store commit --kind doc --key <area>/<feature> --body \"<the fact>\"\n" +
	"Otherwise, briefly note that it wasn't worth storing and you're done."

SessionSuggestText is the @remember nudge surfaced by SessionSuggest / the Stop hook.

View Source
const SettingCageMode = "setting/cage-mode"

SettingCageMode is the DECLARED, project-level default for OS-level agent confinement. Cage stays opt-in — this setting only lets a project turn it ON by default (seeded "off"); the PROJX_CAGE env var, when set, always overrides it explicitly for one launch. Not the gate/dispatcher-mode axis — orthogonal.

View Source
const SettingDispatcherMode = "setting/dispatcher-mode"

── Trunk-dispatch (dispatcher-mode) ───────────────────────────────────────── The interaction law: the main session is a DISPATCHER, never a worker. When dispatcher-mode is ON, the trunk is denied file-mutating tools so every change is routed to a spawned tier-agent; a projx-spawned worker (PROJX_ROLE=worker) is exempt. This is a policy gate — NOT the cage/sandbox (which stays separately opt-in). Stored as a setting gate-rule (skipped by GatePatterns → never a deny glob).

View Source
const (
	SettingIntegrationActive = "setting/integration-active" // Body = active integration name

)

Integration setting record keys.

View Source
const SettingOverrideAuthority = "setting/override-authority"

SettingOverrideAuthority keys the HUMAN-CONTROLLED delegation flag that decides whether the AI may override a soft rule at all. Default OFF: the AI can REQUEST an override but must not self-grant one. The human delegates by setting this ON (which, like the override itself, only they can do out-of-band — the hook blocks the AI from flipping it). See doc/enforcement-follow-override-plan and the override-authority ADR.

View Source
const SettingWorkerAllow = "setting/worker-allow"

SettingWorkerAllow keys the DECLARED list of shell commands a dispatched worker may run WITHOUT prompting — the "basic permissions" floor. The body is command names separated by commas / spaces / newlines. Anything NOT listed still prompts (the human grants the rest = "reach and ask for more"). Seeded with DefaultWorkerAllow; change it with `store commit --kind convention --key setting/worker-allow --body "git, go, …"` — no recompile. Key starts with "setting/" so it stays out of normal preamble render.

View Source
const SettingWorkerAutonomy = "setting/worker-autonomy"

SettingWorkerAutonomy keys the human-granted "full autonomy" escalation for workers. When its body is affirmative ("full"/"on"/"true"), a dispatched worker runs with ALL permissions — the verbal "you're allowed to do whatever you need" override, expressed as data. Default (absent) = basic permissions (the safe-list). The ProjX gate still blocks secrets/off-limits even under full autonomy.

View Source
const SettingWorkerDirective = "setting/worker-directive"

SettingWorkerDirective keys the EDITABLE worker-role directive: the text prepended to a spawned worker's context (PROJX_ROLE=worker) so it reframes the trunk's "dispatch, don't mutate" law as not-its-own and does the task directly instead of re-dispatching. Seeded at floor time with DefaultWorkerDirective as its body — edit it with `store commit --kind convention --key setting/worker-directive --body "…"`, no recompile needed. Key starts with "setting/" so dropSettings excludes it from normal preamble rendering (only WorkerDirectiveText's explicit fetch surfaces it).

Variables

View Source
var DefaultIntegration = CompletionSpec{
	Name:      "claude-code",
	Transport: TransportCLI,
	Template:  "claude -p {prompt} --model {model}",
	Model:     "haiku",
}

DefaultIntegration is the shipped fallback: drive the harness's own agent CLI in one-shot print mode. It is DATA (a replaceable default), not logic — override it by declaring an integration and marking it active. The `{prompt}`/`{model}` placeholders are substituted as whole argv elements (no shell), so prompts with spaces are safe.

View Source
var DefaultRuleTiers = map[string]string{
	"dispatcher-mode":     EnforcementSoft,
	"confirm-before-push": EnforcementSoft,
	"commit-style":        EnforcementSoft,
}

DefaultRuleTiers is the built-in tier for named POLICY rules when a store carries no explicit Enforcement for them. These are the overridable soft rules the override command and hook understand. A store record with an explicit Enforcement overrides this, so a project can retier a rule (e.g. make dispatcher-mode hard) as data.

View Source
var ErrNoID = errors.New("store: record has no ID")

ErrNoID is returned by Put when a record has no ID.

View Source
var FloorConventions = []SeedRec{
	{"dispatch don't mutate", "The main session is a DISPATCHER, not a worker. Do not edit files directly from the trunk — route each task to its tier and spawn an agent to do the work (`projx-engine dispatch --run \"<task>\"`). The trunk reads, plans, dispatches, and VERIFIES the returned diff; spawned agents do the mutation. Tight iterative work = keep messaging one spawned agent rather than re-spawning. When dispatcher-mode is on this is enforced by a gate, not left to willpower."},
}

FloorConventions are the PROJECT-scope behaviour rules — only the ones tied to per-project mechanics. Universal law (secrets, engineering discipline, off-limits gates) lives at GLOBAL scope and INHERITS down (global → workspace → project), so it is NOT re-seeded here — each scope declares only its own distinct layer and they compound. Per-language rules belong in stacks (profiles.go); per-repo rules in a project seed.toml.

View Source
var FloorGates = []SeedRec{}

FloorGates: NONE at project scope by default. Off-limits gates are universal and are seeded ONCE at GLOBAL scope, inheriting into every workspace/project. A project (or workspace) can still declare its OWN additional gates — they compound on top of global.

View Source
var FloorKeywordSeeds = []SeedRec{
	{"deep-reasoning", strings.Join(deepKeywords, " ")},
	{"cheap-fast", strings.Join(cheapKeywords, " ")},
	{"default", strings.Join(stdKeywords, " ")},
	{"risk", strings.Join(riskKeywords, " ")},
}

FloorKeywordSeeds seed the classifier's built-in vocabulary into the store as EDITABLE setting/route-keywords/<class> records ("make it a rule, not a hardcode"): once seeded, ClassifyStore reads ONLY the store's copy, so removing or adding a word actually changes routing — not just extends it. deepKeywords/cheapKeywords/ stdKeywords above remain the seed DEFAULTS and the fallback for a store that hasn't been (re-)seeded yet (see ClassifyStore).

View Source
var FloorRoutes = []SeedRec{
	{"cheap-fast", "claude --permission-mode acceptEdits --model claude-haiku-4-5-20251001"},
	{"default", "claude --permission-mode acceptEdits --model claude-sonnet-4-6"},
	{"deep-reasoning", "claude --permission-mode acceptEdits --model claude-opus-4-8"},
	{"elevate", "claude --permission-mode acceptEdits --model claude-fable-5"},
}

FloorRoutes are the default class -> launch-command tiers seeded into every project. The model IDs live HERE, once — not hardcoded in the engine binary, so updating a tier is a store edit, not a recompile. The tier commands launch an AUTONOMOUS worker: --permission-mode acceptEdits lets it apply file edits without an interactive approval (a dispatched worker is headless — no one is there to click "allow"). ProjX's own PreToolUse gate (off-limits paths + the optional cage) remains the real guardrail. Model IDs live HERE, once (store-editable).

Functions

func AgentContext

func AgentContext(st Store, sel Filter) string

AgentContext renders the contract with TASK SLICING. The protocol + the LAW sections (gate rules, conventions) are ALWAYS included in full (small, binding); the reference sections (ADRs, declared structure, docs, history) are narrowed by sel.KeyPrefix / sel.Text — typically derived from the task — so only the relevant records load ("query, don't dump"). A zero sel includes everything (== the full AgentPreamble). Deterministic, read-only; an empty store still yields the protocol so the agent always knows the rules.

func AgentContextDelta

func AgentContextDelta(st Store, task string, seen map[string]int64) (string, map[string]int64)

AgentContextDelta renders the PER-MESSAGE contract for a session that has ALREADY been injected the records named in `seen` (recordID -> the UpdatedAt at injection time). Unlike AgentContextForTask it does NOT repeat the protocol lecture — the agent received it at SessionStart and it is still in context. The LAW (gate rules + conventions) is always re-sent in full: it is small, binding, and must not be forgotten between turns. Reference records (ADR / doc / declared-structure / history) are included ONLY when they (a) match the task slice AND (b) are new or changed versus `seen` — so an unchanged record already in the agent's context is not paid for again. Returns the rendered text plus the UPDATED seen map (every task-relevant record now in the agent's context, by id -> UpdatedAt) for the caller to persist as the session checkpoint. A nil `seen` means "fresh session" (everything relevant is new). Deterministic and read-only. Equivalent to AgentContextDeltaSel with nil sel.

func AgentContextDeltaSel

func AgentContextDeltaSel(st Store, task string, seen map[string]int64, sel SelectorFunc, focus string) (string, map[string]int64)

AgentContextDeltaSel is AgentContextDelta with an injectable relevance selector and a focus repo: when sel is non-nil it chooses the relevant reference records semantically (v2); nil uses the deterministic v1 token match. focus (a repo/group) boosts that repo's matched records. An empty/failed selection falls back to v1 so a turn is never starved.

func AgentContextFloor

func AgentContextFloor(st Store) string

AgentContextFloor renders the LEAN session-start floor: the full protocol lecture plus the LAW (gate rules + conventions) in full — and nothing else. The reference knowledge (ADRs, docs, declared structure, history) is deliberately NOT dumped here; it loads per-message via AgentContextDelta as a task makes it relevant ("why load the full context if you don't have to"). This is the SessionStart counterpart to the per-message delta: small + binding at the top of a session, the rest streamed in on demand. Pure and read-only; a nil/empty store still yields the protocol.

func AgentContextForTask

func AgentContextForTask(st Store, task string) string

AgentContextForTask is the task-driven entry point used by the launch hooks: it renders the law + only the reference records relevant to the task, selected by the v1 DETERMINISTIC token match (significant tokens vs each record's Key+Body). Equivalent to AgentContextForTaskSel with a nil selector.

func AgentContextForTaskSel

func AgentContextForTaskSel(st Store, task string, sel SelectorFunc, focus string) string

AgentContextForTaskSel renders the task-sliced contract using sel to choose the relevant reference records (v2 semantic selection) — or, when sel is nil, the deterministic v1 token match. The law (gates+conventions) always loads in full. Falls back to the FULL context when there is no selection signal at all (nil sel AND no usable tokens), so the agent is never starved of context.

func AgentPreamble

func AgentPreamble(st Store) string

AgentPreamble renders the FULL contract (everything in the store). It is AgentContext with a zero selector — kept for callers that want the whole store.

func CageModeOn

func CageModeOn(s Store) bool

CageModeOn reports whether a project has declared cage-mode ON by default (a setting/cage-mode gate-rule with an affirmative body); false (uncaged) if absent.

func CaptureHint

func CaptureHint(task string) string

CaptureHint returns instructions for turning the user's aside into a clean store commit when the task signals capture intent; "" otherwise. Appended to the task context by the launch hooks so a "remember: …" becomes a real, well-formed store.commit instead of a markdown scribble.

func Classify

func Classify(task string) string

Classify is the back-compat single-return classifier (the class only).

func ClassifyConfident

func ClassifyConfident(task string) (class string, matched bool)

ClassifyConfident maps a task to a capability class by keyword and reports whether a keyword actually MATCHED (true) or the task fell through to the default class with no signal (false). The matched flag is what the decider uses to tell a confident keyword route from the ambiguous middle that warrants cheap model triage. Deterministic; no LLM. Priority: deep-reasoning > cheap-fast > default.

func ClassifyStore

func ClassifyStore(s Store, task string) (class string, matched bool)

func Decompose

func Decompose(message string) []string

Decompose splits a multi-task message into discrete task fragments on natural connectors and numbered/bulleted list markers. Returns a single-element slice (the trimmed message) when there is no clear split — so a normal one-task message is untouched. Deterministic and order-preserving.

func DenyRules

func DenyRules(s Store) []string

DenyRules turns the store's gate rules into agent file-tool deny rules — "Read(glob)" / "Edit(glob)" — over the GatePatterns set. This is the SINGLE gate->deny definition shared by the engine, the engine cell, and the Workbench (each previously derived it on its own).

func DispatcherModeOn

func DispatcherModeOn(s Store) bool

DispatcherModeOn reports whether the trunk-dispatch discipline is enabled (a setting/dispatcher-mode gate-rule with an affirmative body).

func Export

func Export(project Store) string

Export renders the project store's declared knowledge as an architecture.md-style markdown document. It is strictly READ-ONLY: it pulls records via List and never writes. Records are grouped into fixed sections (Architecture Decisions, Declared Structure, Conventions, Docs, History); empty sections are omitted. Within a section records are sorted by Key (then ID) and rendered as "### {Key}" followed by the record Body.

func ExportJSON

func ExportJSON(recs []Record) ([]byte, error)

ExportJSON serializes records (typically one scope) to portable JSON — the durable form of a "your store" for git-backing, new-brain import, and promotion carry. (Distinct from Export, which renders the project store as read-only markdown.)

func GateDenied

func GateDenied(s Store, path string) (pattern string, denied bool)

GateDenied reports whether path is denied by any of the store's gate patterns, returning the matching pattern. Matching is doublestar glob semantics (** crosses path segments, * does not) — the same semantics the gate's deny globs use — so secret/**, **/*.key, .env*, and **/.ssh/** all match correctly (a prefix matcher would not). One definition for native gate checks and the cell's gate endpoint.

func GatePatterns

func GatePatterns(s Store) []string

GatePatterns returns the normalized off-limits glob patterns declared by the store's gate rules: each rule's Body (falling back to its Key), with a trailing "/" expanded to a recursive "/**". setting/* rules are skipped — config/secrets are never a project gate. This is the SINGLE source of the gate pattern set: DenyRules renders these as agent Read()/Edit() denies, and a path-matching gate check tests file paths against them (so the deny globs and the check can never drift).

func IntegrationNames

func IntegrationNames(s Store) []string

IntegrationNames lists the names of every declared integration (sorted for stable output), read from the `setting/integration/<name>` records.

func IsMutatingTool

func IsMutatingTool(name string) bool

IsMutatingTool reports whether a tool name writes to files.

func IsRiskyTask

func IsRiskyTask(s Store, task string) bool

IsRiskyTask reports whether a task is correctness-critical (concurrency, serialization, auth/crypto, money, migrations, destructive ops). The decider raises the tier FLOOR to deep-reasoning for these so triage can't send subtle work to a cheap model. The keyword set is editable via the store's setting/route-keywords/risk record (else the built-in).

func IsSoftRule

func IsSoftRule(s Store, name string) bool

IsSoftRule reports whether a named rule is soft (deny-by-default, overridable).

func ManagedBlock

func ManagedBlock(s Store) string

ManagedBlock renders the full managed block (markers + body) from the store.

func MaxUpdatedAt

func MaxUpdatedAt(st Store) int64

MaxUpdatedAt returns the largest UpdatedAt across every record (0 for an empty store) — the cheap "has anything been committed since?" signal the Stop suggestion uses.

func NormGatePath

func NormGatePath(p string) string

NormGatePath normalizes a path/pattern for gate matching: backslashes→/, strip a leading "./" or "/", drop a trailing "/". So "./secret/x", "secret/x", and "/secret/x" compare the same way and a path can't dodge a rule via a prefix variant.

func OverrideAuthorityOn

func OverrideAuthorityOn(s Store) bool

OverrideAuthorityOn reports whether the human has delegated override authority to the AI (a setting/override-authority gate-rule with an affirmative body). Default false — absent means NOT delegated, so the AI cannot self-authorize a bypass.

func ParseSelectedKeys

func ParseSelectedKeys(reply string, candidates []string) []string

ParseSelectedKeys extracts a JSON array from a selector model's reply and keeps only keys present in the candidate set (the model cannot invent or rename a key). Order follows the reply; nil when no usable array is present.

func ParseTierReply

func ParseTierReply(content string) (tier string, confident bool)

ParseTierReply extracts a valid capability tier + confidence from a triage model's reply. It reads the strict JSON shape {"tier":..,"confident":..} but tolerates surrounding prose and falls back to a bare tier word (confident=false so the decider escalates rather than trusting a sloppy reply). Returns ("", false) when no valid tier is present.

func ProvenanceFor

func ProvenanceFor(by string) string

ProvenanceFor maps a commit actor ("ui"|"agent") to its default provenance. Empty actor or anything else → unknown ("").

func RenderCLIArgs

func RenderCLIArgs(template, prompt, model string) []string

RenderCLIArgs turns a cli template into an argv, substituting {prompt}/{model} as whole elements (never string-spliced, so spaces in the prompt are safe). An empty template yields nil.

func Route

func Route(s Store, task string) (class, cmd string)

Route classifies task and resolves the launch command from the store's KRoute records. Returns the class and the command (cmd is "" if no record for the class).

func RuleTier

func RuleTier(s Store, name string) string

RuleTier resolves the tier of a rule referenced by NAME (e.g. "dispatcher-mode"). An explicit Enforcement on a matching store record wins; otherwise the built-in default; otherwise advisory. s may be nil (defaults only).

func SeedFloor

func SeedFloor(s Store) int

SeedFloor writes the floor contract (conventions + gate rules) into s as project-scoped records. Put replaces by ID, so re-seeding is harmless. Returns the number of records written.

func SessionContext

func SessionContext(st Store, cps CheckpointStore, session, task string, reset bool, sel SelectorFunc) string

SessionContext owns the per-turn lifecycle and returns the text to inject:

  • reset (PreCompact): mark the floor lost, inject nothing.
  • task=="" (SessionStart): the lean floor, fresh checkpoint.
  • otherwise (UserPromptSubmit): the delta — law re-asserted + only new/changed task-relevant records; or, right after a reset, the full floor+slice refill.

sel is the optional v2 semantic selector (nil → deterministic v1). It mutates the session's checkpoint via cps.

func SessionSuggest

func SessionSuggest(st Store, cps CheckpointStore, session string) (msg string, block bool)

SessionSuggest is the Stop suggestion: SUGGEST-ONLY, and only when an @remember was flagged this session and nothing was committed afterward. Returns the nudge text + whether to surface it (block), and disarms the flag so it fires at most once.

func SoftRuleNames

func SoftRuleNames(s Store) []string

SoftRuleNames lists the names of soft rules: the built-in defaults plus any store rule explicitly marked soft. Used for override-command messaging.

func SpliceManagedBlock

func SpliceManagedBlock(existing, block string) string

SpliceManagedBlock replaces the managed block inside existing with block, preserving any user content around it. An empty document gets a minimal header.

func TaskSliceOverflows

func TaskSliceOverflows(st Store, task string) bool

TaskSliceOverflows reports whether the deterministic v1 slice for this task would exceed the per-section cap in any reference section — i.e. the keyword match was AMBIGUOUS (too many hits). Consumers use this to auto-escalate to the semantic v2 selector ONLY when v1 is too broad: deterministic-when-easy, model-when-needed.

func Tier

func Tier(r Record) string

Tier returns a record's enforcement tier: its explicit Enforcement if set, else a value derived from its identity — off-limits gate globs are hard, a known soft setting is soft, everything else advisory. This keeps pre-schema-#4 rows correct.

func WorkerAllowBins

func WorkerAllowBins(s Store) []string

WorkerAllowBins returns the declared worker safe-list (setting/worker-allow) as command names, or DefaultWorkerAllow when the store is nil / the record is absent or blank. Separators may be commas, spaces, or newlines so the human can write it naturally.

func WorkerDirectiveText

func WorkerDirectiveText(s Store) string

WorkerDirectiveText returns the declared worker directive from the store (the setting/worker-directive convention), or DefaultWorkerDirective if s is nil, the record is absent, or its body is blank.

func WorkerFullAutonomy

func WorkerFullAutonomy(s Store) bool

WorkerFullAutonomy reports whether the human has granted workers full autonomy (a setting/worker-autonomy gate-rule with an affirmative body). Default false.

Types

type Checkpoint

type Checkpoint struct {
	// Seen maps recordID -> the UpdatedAt at the time it was last injected, so the delta
	// suppresses records already in the agent's context and re-sends changed ones.
	Seen map[string]int64 `json:"seen"`
	// NeedFloor is set on PreCompact: the next turn must re-send the floor before the slice.
	NeedFloor bool `json:"need_floor"`
	// Flagged records an @remember this session; FlaggedAt is the store's high-water mark
	// at that moment, so Stop can tell whether anything was committed afterward.
	Flagged   bool  `json:"flagged_remember"`
	FlaggedAt int64 `json:"flagged_at"`
	// Focus is the repo/group the session is currently working in — set automatically as
	// the agent edits that repo's files (or explicitly via @focus). It boosts that repo's
	// records in the slice, and "shifts" when work moves to another repo.
	Focus string `json:"focus,omitempty"`
}

Checkpoint is the per-session delta state. JSON tags are stable on disk so a file written by one face round-trips through another.

type CheckpointStore

type CheckpointStore interface {
	Load(session string) Checkpoint
	Save(session string, cp Checkpoint)
}

CheckpointStore persists one Checkpoint per session id. Load returns the zero Checkpoint for an unknown/corrupt session (a fresh session); neither Load nor Save returns an error — losing a checkpoint only costs a little redundant context, never a blocked turn.

type CompletionSpec

type CompletionSpec struct {
	Name      string `json:"-"`
	Transport string `json:"transport"`             // "cli" | "http-openai"
	Template  string `json:"template,omitempty"`    // cli: argv template with {prompt} {model} placeholders
	BaseURL   string `json:"base_url,omitempty"`    // http-openai: OpenAI-compatible base
	APIKeyEnv string `json:"api_key_env,omitempty"` // http-openai: NAME of the env var holding the key (never the key itself)
	Model     string `json:"model,omitempty"`       // optional default model for this provider
}

CompletionSpec is a resolved provider definition for one-shot model calls.

func IntegrationSpec

func IntegrationSpec(s Store, name string) (CompletionSpec, bool)

IntegrationSpec reads a named integration record. Returns ok=false if absent or malformed.

func ResolveCompletion

func ResolveCompletion(s Store) (CompletionSpec, bool)

ResolveCompletion returns the active integration spec and whether one was declared. When none is declared it returns DefaultIntegration/false, so a caller can tell an explicit choice from the built-in fallback.

type Conflict

type Conflict struct {
	ID         string
	Base       Record
	Incoming   Record
	Resolution Resolution // TookIncoming / KeptBase = auto-resolved (LWW); Unresolved = needs you
}

Conflict is one ID present on BOTH sides with a DIFFERENT body. The merge auto-resolves by last-write-wins when the timestamps differ; a true tie (equal/zero UpdatedAt) is reported Unresolved for the caller to reconcile.

func (Conflict) Apply

func (c Conflict) Apply(takeIncoming bool) Record

Apply settles an Unresolved conflict: pick the incoming record (true) or keep base (false), returning the chosen record to Put. The chosen record's UpdatedAt is bumped to now so the decision itself is the newest write (and future merges respect it).

func (Conflict) Resolved

func (c Conflict) Resolved() bool

Resolved reports whether the conflict was settled automatically (LWW). Unresolved conflicts are the only ones a human must reconcile.

type Filter

type Filter struct {
	Scope     *Scope
	Kind      *Kind
	KeyPrefix string
	Text      string
}

Filter selects records. The zero value matches everything; a non-nil/non-empty field narrows. Pointers distinguish "unset" from "the zero Scope/Kind".

KeyPrefix and Text power task-sliced retrieval (the "query, don't dump" pillar): KeyPrefix matches the Key as a path prefix ("minecraft/login" → minecraft/login, minecraft/login/backend, …); Text is a case-insensitive substring of Key OR Body. Both are case-insensitive so a query needn't know exact casing. SQLite mirrors these as WHERE clauses (see SQLite.List) so results match Mem.List exactly.

func InScope

func InScope(s Scope) Filter

InScope is a convenience filter for one scope.

func OfKind

func OfKind(k Kind) Filter

OfKind is a convenience filter for one kind.

type Kind

type Kind int

Kind is the typed record vocabulary. The store holds records; the engines give them meaning.

const (
	KRecipe            Kind = iota // a workflow recipe (global scope only)
	KConvention                    // a style/behavior rule baked into context
	KADR                           // an architecture decision record
	KDoc                           // an explanation / subsystem note
	KHistory                       // an append-only change record (commit output)
	KGateRule                      // a gate/redaction rule
	KDeclaredStructure             // declared module/system grouping for the graph
	KRoute                         // a capability class -> agent launch command (model tier)
)

func (Kind) String

func (k Kind) String() string

type Mem

type Mem struct {
	// contains filtered or unexported fields
}

Mem is an in-memory Store — the first implementation behind the interface. It exists so context/workflow/verify can be built and tested against the contract today; a SQLite-backed Store swaps in later without touching any caller.

List returns records in deterministic ID order so callers and tests reproduce.

func NewMem

func NewMem() *Mem

NewMem returns an empty in-memory store.

func (*Mem) Delete

func (m *Mem) Delete(id string) error

Delete removes a record by ID. Deleting a missing ID is a no-op (not an error).

func (*Mem) Get

func (m *Mem) Get(id string) (Record, bool)

Get returns the record with the given ID, if present.

func (*Mem) List

func (m *Mem) List(f Filter) []Record

List returns all records matching the filter, sorted by ID.

func (*Mem) Put

func (m *Mem) Put(r Record) error

Put inserts or replaces a record by ID.

type MergeResult

type MergeResult struct {
	Merged     []Record   // the consolidated set (ID-sorted), with auto-resolutions applied
	Conflicts  []Conflict // every same-ID-different-body case (check .Resolved())
	Added      int        // ids only in incoming
	Unchanged  int        // ids identical on both sides (deduped)
	AutoWon    int        // conflicts settled by last-write-wins
	NeedReview int        // conflicts left Unresolved (a human must pick)
}

MergeResult is the outcome: the consolidated record set plus a per-ID accounting and the conflicts (auto-resolved AND the ones still needing a pick).

func Merge

func Merge(base, incoming []Record) MergeResult

Merge consolidates `incoming` into `base` by ID:

  • id only in base -> kept
  • id only in incoming -> added
  • same id, same content -> deduped (the newer-stamped copy is kept for metadata)
  • same id, diff content -> CONFLICT: newer UpdatedAt wins (LWW); equal/zero -> Unresolved

Unresolved conflicts keep the BASE record in Merged (no silent overwrite — you stay the author) until the caller picks; resolve with Apply.

type Record

type Record struct {
	ID    string
	Kind  Kind
	Scope Scope
	Key   string
	Body  string
	// UpdatedAt is the last-write clock (0 = unknown), unix-MILLIS-based but guaranteed
	// strictly increasing per store (see stamp). Millis (not seconds) + the monotonic
	// guarantee mean two edits in the same instant never tie — important because the wasm
	// cell's wall clock can be second-resolution. merge/import preserve an incoming value
	// so last-write-wins holds across machines. Origin = which brain/machine wrote it.
	UpdatedAt int64
	Origin    string
	// Enforcement tiers how strictly this rule is applied: "hard" (gate denies, no
	// override — the secrets/off-limits floor), "soft" (denies by default but a
	// recorded, reasoned override may proceed), or "advisory" (context-injected, not
	// gated). Empty means "derive by identity" (see Tier) so rows written before this
	// column existed behave correctly with no data backfill. Persisted since schema #4.
	Enforcement string
	// Provenance records HOW a record's claim was established: "human-confirmed" (a person
	// committed/approved it), "gate-verified" (the verify gate ran build/test and it passed),
	// "agent-asserted" (an AI committed it without an independent check), or "" (unknown).
	// Lets a reader distinguish a proven fact from a narrator's claim. Persisted since #5.
	Provenance string
}

Record is one typed entry. Body is an opaque payload (text/JSON); its concrete schema is deferred. Key is a human handle, unique within (Scope, Kind) — e.g. a recipe name or an ADR id.

func ImportJSON

func ImportJSON(data []byte) ([]Record, error)

ImportJSON parses an ExportJSON blob.

func IntegrationActiveRecord

func IntegrationActiveRecord(name string) Record

IntegrationActiveRecord builds the record naming the active integration.

func IntegrationRecord

func IntegrationRecord(spec CompletionSpec) Record

IntegrationRecord builds the store record for a declared integration (JSON body).

func ListAll

func ListAll(s Store) []Record

ListAll reads every record from a store (helper for export/merge callers).

type Resolution

type Resolution int

Resolution is how a same-ID conflict was (or should be) settled.

const (
	TookIncoming Resolution = iota // incoming record won (newer, or chosen)
	KeptBase                       // base record won (newer, or chosen)
	Unresolved                     // tie that needs a human pick (Newer == Newer == 0, etc.)
)

type RouteDecision

type RouteDecision struct {
	Class  string // cheap-fast | default | deep-reasoning | elevate
	Cmd    string // launch command from the KRoute tier map ("" if none)
	Source string // override | pin | keyword | triage | triage-escalated | default (+floor)
	Reason string // short human explanation
}

RouteDecision is the resolved routing choice plus how it was reached.

func RouteDecide

func RouteDecide(s Store, task string, triage TriageFunc) RouteDecision

RouteDecide is the DECIDER: it resolves a task to a tier by the locked precedence ladder — cheapest possible, escalating only when earned ("lowest model, highest yield"):

  1. per-message @-override (@cheap/@haiku, @sonnet/@default, @opus/@deep) — always wins, bypasses pin AND floor (the user asked for it explicitly, this once).
  2. standing PIN setting — hard-lock to one tier; triage is skipped entirely.
  3. deterministic classifier — a matched keyword routes for free (store-augmentable).
  4. cheap haiku triage — only for the ambiguous middle; escalate-on-uncertainty.
  5. default tier — no signal.

Steps 3–5 are then raised to the standing FLOOR (a minimum tier) if one is set. Pure given a pure triage func; deterministic when triage is nil.

type SQLite

type SQLite struct {
	// contains filtered or unexported fields
}

SQLite is a SQLite-backed Store. It behaves identically to Mem — insert-or-replace Put, (Record, bool) Get, no-op Delete, ID-sorted List — but persists through a sqlConn. On a native build that conn is modernc.org/sqlite (a local file or :memory:); built as a wasm cell it is the Pulp host's storage.sqlite capability. The store logic below is identical for both; only the conn differs (see conn_*.go).

func Open

func Open(path string) (*SQLite, error)

Open opens (or creates) a SQLite-backed Store and runs any pending migrations. On native, path is a file path or ":memory:". As a Pulp cell, path is ignored — the host owns the database file (<storage-root>/<cell>/data.db). The returned *SQLite must be closed with Close.

func (*SQLite) Close

func (s *SQLite) Close() error

Close releases the underlying connection.

func (*SQLite) Delete

func (s *SQLite) Delete(id string) error

Delete removes a record by ID. Deleting a missing ID is a no-op (not an error).

func (*SQLite) Get

func (s *SQLite) Get(id string) (Record, bool)

Get returns the record with the given ID, if present.

func (*SQLite) List

func (s *SQLite) List(f Filter) []Record

List returns all records matching the filter, sorted by ID. The filter is applied as SQL WHERE clauses so results match Mem.List exactly.

func (*SQLite) Put

func (s *SQLite) Put(r Record) error

Put inserts or replaces a record by ID.

type Scope

type Scope int

Scope identifies which physical file a record lives in. THREE scopes across TWO files:

Global, Workspace -> YOUR store   (one file, travels with you between machines)
Project           -> PROJECT store (one per project, stays with the repo)
const (
	// ScopeGlobal: how you work everywhere — recipes, conventions, style. Agnostic.
	ScopeGlobal Scope = iota
	// ScopeWorkspace: this machine's cockpit state — repo list, default gate posture.
	ScopeWorkspace
	// ScopeProject: what's true about this codebase — ADRs, architecture, history,
	// this project's gate rules.
	ScopeProject
)

func (Scope) Owner

func (s Scope) Owner() string

Owner reports which physical LEVEL a scope belongs to: "global" (machine/user), "workspace" (an optional multi-repo folder), or "project" (one repo). The composer routes a write to the store owning that level, falling back UP a level when the owning store is absent — so project-only and project+global setups work with no workspace store.

func (Scope) String

func (s Scope) String() string

type SeedRec

type SeedRec struct{ Key, Body string }

SeedRec is a key/body pair awaiting a Kind + Scope at seed time.

type SelectorFunc

type SelectorFunc func(task string, candidateKeys []string) (relevantKeys []string)

SelectorFunc proposes which reference records are relevant to a task. Given the task and the candidate reference-record keys (the index), it returns the subset of keys to include. A consumer (the engine, backed by a cheap model) injects a real one for v2 SEMANTIC selection; nil falls back to the v1 DETERMINISTIC token selector (the offline floor). This is the "cheap-model-proposed query" the plan layers on top of v1.

type Store

type Store interface {
	Put(Record) error
	Get(id string) (Record, bool)
	List(Filter) []Record
	Delete(id string) error
}

Store is the read/write contract every backend satisfies. Both YOUR store and the PROJECT store are Store values; the caller picks which by Record.Scope's Owner().

type TriageFunc

type TriageFunc func(task string) (class string, confident bool)

TriageFunc is the injectable cheap-model triage seam: given an ambiguous task it returns a proposed class and whether it is confident. The store library stays OS-/network-free — a consumer (the engine) supplies a real haiku-backed func; nil means "deterministic only" (ambiguous tasks fall to the default tier).

type Workspace

type Workspace struct {
	Global  Store // machine/user level (also absorbs workspace-scoped writes when Space is nil)
	Space   Store // OPTIONAL workspace level (nil = not in a workspace)
	Project Store // per-repo level (nil = not pointed at a repo)
}

Workspace ties up to THREE physical stores into one logical Store, realizing the multi-LEVEL model: machine/user GLOBAL, an optional WORKSPACE (a multi-repo folder with its own rules), and the per-repo PROJECT store. Any level except Global may be nil — point it at a bare repo (project + global, no workspace), a workspace with no project open, or just global. Reads COMPOSE whatever is present; writes route by Record.Scope.Owner(), falling back UP a level when the owning store is absent, so a missing level never drops a write and project-only just works.

func NewComposite

func NewComposite(global, space, project Store) *Workspace

NewComposite is the 3-level constructor. space may be nil (project-only / no workspace); project may be nil (a workspace with nothing open). global is always required.

func NewWorkspace

func NewWorkspace(yours, project Store) *Workspace

NewWorkspace is the 2-level (back-compat) constructor: yours holds global + workspace records, project holds project records. Equivalent to NewComposite(yours, nil, project).

func (*Workspace) Delete

func (w *Workspace) Delete(id string) error

Delete removes the ID from every present store (a missing ID is a no-op).

func (*Workspace) Get

func (w *Workspace) Get(id string) (Record, bool)

Get returns the record with the given ID, checking Project → Space → Global; the most specific level's hit wins.

func (*Workspace) List

func (w *Workspace) List(f Filter) []Record

List composes records across the present levels. A scope-pinned filter queries only the store owning that level; otherwise all present levels are merged and sorted by ID so output is deterministic.

func (*Workspace) Put

func (w *Workspace) Put(r Record) error

Put routes the record to the store owning its scope's level (falling back up).

Jump to

Keyboard shortcuts

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