/** * StandingAnswersService — question-anchored pages kept current from memory. * * Memory is a flat pile (in this repo: ~330 decisions, ~190 gotchas, ~120 * patterns) that agents must `recall`, and recall rarely hits at startup. * A standing-answer page is the settled answer to one recurring question * ("What decisions are in force?"), written by an agent from the entries * behind it and read at boot as a plain file (#816). The idea is the * "mental models" of vectorize-io/hindsight, copied as an idea only. * * Page file: * * ``` * --- * question: "What decisions are in force?" * last_refreshed: 2026-09-26T15:00:00.000Z * watermark: 2026-09-26T14:58:12.000Z * --- * * ## Prompt assembly * New prompt content must be a prompt module ... * * Sources: dec:0f3c..., dec:9a1b... * ``` * * - `watermark` is the newest timestamp among the page's in-scope memory * entries when it was last written. A page is **stale** when an in-scope * entry is newer than its watermark. * - Every section cites the entries it was built from. Writes go through * {@link StandingAnswersService.writeSection}, which checks each citation * against the page's sources and masks anything that looks like a secret. * - A section's **basis is invalidated** when a cited entry was deleted or * is no longer in force (superseded, expired, resolved). The watermark * does not move for these, so they are checked on every read * ({@link findInvalidatedSections}) and also make the page need a refresh. * - Edits are per section (keep / modify / remove / add), so a refresh never * rewrites what it did not touch. Before a content change the page as it * was is snapshotted and the write is logged (`changes.log`), so every * change is auditable (#914 P1). * - No LLM runs here. The only writer is an agent working a refresh * WorkItem (see standing-refresh.service.ts), raised only when the * watermark moves. * * @module services/memory/standing-answers.service */ /** Whose knowledge a page summarises. */ export type StandingScope = 'project' | 'agent'; /** * Which memory collection a page is built from. `owner` is not a collection: * the owner rules page has no sources and is written by the owner only (#914 P2). */ export type StandingSource = 'decisions' | 'gotchas' | 'user_preferences' | 'agent_memory' | 'owner'; /** Sources a per-project custom question may use. */ export declare const CUSTOM_QUESTION_SOURCES: readonly StandingSource[]; /** A standing-answer page definition. */ export interface StandingPageDef { /** Stable page id; also the file name for project pages (`.md`). */ id: string; scope: StandingScope; /** The recurring question the page answers. */ question: string; source: StandingSource; /** * Human-owned page (#914 P2): only the owner writes it, no citations are * required, no refresh WorkItem is ever raised for it, and it is never * stale. Automated curation must not change it. */ ownerOnly?: boolean; /** * Defined by the project's `standing-questions.json` rather than built in * (#914 P2). Custom pages are refreshed and written like built-in ones. */ custom?: boolean; /** * Keep only source entries whose title or text contains one of these * keywords (case-insensitive). Empty or absent: every entry of the source. */ match?: readonly string[]; } /** One entry of a project's `standing-questions.json` (#914 P2). */ export interface CustomStandingQuestion { /** Page id: `^[a-z][a-z0-9-]{1,40}$`, not a built-in id. */ id: string; /** The question, one line. */ question: string; /** One of {@link CUSTOM_QUESTION_SOURCES}. */ source: string; /** Optional keyword filter on the source entries. */ match?: string[]; } /** One memory entry a page may be built from, normalised across sources. */ export interface StandingSourceEntry { /** Citation token, `:` (kinds: dec, got, pref, mem). */ cite: string; title: string; text: string; /** Newest relevant timestamp of the entry (ISO); drives the watermark. */ at: string; /** * False for superseded/deprecated decisions, resolved gotchas, and * superseded or expired memories ({@link isHiddenFromDefaultRecall}). */ inForce: boolean; /** Citation of the entry that replaced this one, when recorded (#884). */ supersededBy?: string; } /** Why a cited entry no longer supports a section. */ export type InvalidCiteReason = 'deleted' | 'not_in_force'; /** One citation a section can no longer rely on. */ export interface InvalidCite { cite: string; reason: InvalidCiteReason; } /** A section at least one of whose cited entries is gone or no longer in force. */ export interface InvalidatedSection { heading: string; invalidCites: InvalidCite[]; } /** One `## heading` section of a page. */ export interface StandingSection { heading: string; body: string; cites: string[]; } /** A parsed page file. */ export interface StandingPage { question?: string; lastRefreshed?: string; watermark?: string; sections: StandingSection[]; } /** Where to look: a project, an agent session, or both. */ export interface StandingLocation { projectPath?: string; sessionName?: string; } /** A page definition resolved against the disk and its sources. */ export interface StandingPageStatus { def: StandingPageDef; filePath: string; /** Parsed page, or undefined when the file does not exist. */ page?: StandingPage; /** Newest in-scope entry timestamp now, or null when there are no entries. */ currentWatermark: string | null; /** In-scope entries examined. */ entriesInScope: number; /** In-scope entries newer than the page's watermark (all of them when it has none). */ newerEntries: number; /** True when the page is missing, has no watermark, or has newer entries. */ stale: boolean; /** * Sections whose basis was invalidated (a cited entry deleted, superseded, * expired or resolved). Non-empty means the page needs a refresh even when * it is not stale, since retractions do not move the watermark (#914). */ invalidatedSections: InvalidatedSection[]; } /** Who is writing a section: the owner (dashboard, API token, relay) or an agent. */ export type StandingActor = 'owner' | 'agent'; /** Input to {@link StandingAnswersService.writeSection}. */ export interface WriteSectionInput extends StandingLocation { pageId: string; heading: string; /** Section body; an empty body removes the section. */ body: string; /** Citations (`dec:` …); at least one unless removing. Ignored on an owner-only page. */ cites: string[]; /** Caller kind; defaults to `agent`. Only `owner` may write an owner-only page. */ actor?: StandingActor; } /** What a section write did to the page. */ export type SectionWriteOp = 'added' | 'modified' | 'removed' | 'unchanged'; /** Result of a section write. */ export interface WriteSectionResult { /** * What changed: `added` (new heading), `modified` (body or citations * differ), `removed` (empty body on an existing section) or `unchanged` * (the same body and citations again — only `last_refreshed` and the * watermark move; use it to confirm a section after a refresh). */ op: SectionWriteOp; /** Snapshot of the page as it was before this write, null when nothing changed or the page is new. */ snapshot: string | null; filePath: string; watermark: string | null; removed: boolean; /** True when something that looked like a secret was masked out. */ masked: boolean; } /** Error codes for rejected writes. `owner_only`: an agent tried to write the owner rules page. */ export type StandingErrorCode = 'unknown_page' | 'invalid_input' | 'unknown_cite' | 'missing_cite' | 'owner_only'; /** A write rejected for a reason the caller can fix. */ export declare class StandingAnswersError extends Error { readonly code: StandingErrorCode; constructor(code: StandingErrorCode, message: string); } /** Project-scope pages, under `/.crewly/wiki/llm-curated/standing/`. */ export declare const PROJECT_STANDING_PAGES: readonly StandingPageDef[]; /** The agent page, at `/agents//standing.md`. */ export declare const AGENT_STANDING_PAGE: StandingPageDef; /** * The owner's explicit rules (#914 P2), at `/.crewly/wiki/owner/rules.md`. * Human-owned: written by the owner only (API with an owner credential, or * by hand), never refreshed, never stale, shown first in the prompt. Separate * from the LLM-curated pages so no automated pass can rewrite a rule. */ export declare const OWNER_RULES_PAGE: StandingPageDef; /** * Look up a built-in page definition by id. Custom per-project questions are * resolved through {@link StandingAnswersService.resolvePageDef}. * * @param pageId - Page id * @returns The definition, or undefined */ export declare function findStandingPageDef(pageId: string): StandingPageDef | undefined; /** * Validate the entries of a project's `standing-questions.json` and turn the * good ones into page definitions (#914 P2). Accepts `{ questions: [...] }` * or a bare array. Bad entries are dropped, not fatal: a typo in one * question must not hide the others. Built-in ids, duplicates, overlong * questions and unknown sources are dropped; at most * `CUSTOM_QUESTIONS_MAX` are kept. * * @param parsed - Parsed JSON file contents * @returns Definitions in file order, plus one reason per dropped entry * * @example * ```typescript * parseCustomQuestions({ questions: [{ id: 'release-rules', question: 'What must happen before a release?', source: 'decisions', match: ['release', 'publish'] }] }); * ``` */ export declare function parseCustomQuestions(parsed: unknown): { defs: StandingPageDef[]; dropped: string[]; }; /** * Whether an entry passes a definition's keyword filter. * * @param def - Page definition * @param entry - Source entry * @returns True when there is no filter or a keyword occurs in the title or text */ export declare function matchesQuestion(def: StandingPageDef, entry: Pick): boolean; /** * Parse a standing page file. * * Tolerant of hand edits: unknown frontmatter keys are ignored, text before * the first `## ` heading is ignored, and a section's trailing * `Sources: a, b` line becomes its citations. * * @param raw - File contents * @returns The parsed page */ export declare function parseStandingPage(raw: string): StandingPage; /** * Serialise a page back to its file form. * * @param page - Page to write * @returns File contents */ export declare function serializeStandingPage(page: StandingPage): string; /** * Replace, append or remove one section (matched by heading, case-insensitive). * * @param page - Page to edit (not mutated) * @param section - New section; an empty body removes the heading * @returns The edited page and whether a section was removed */ export declare function upsertSection(page: StandingPage, section: StandingSection): { page: StandingPage; removed: boolean; }; /** * The newest timestamp among entries, as ISO, or null for none. * * @param entries - Source entries * @returns ISO watermark or null */ export declare function computeWatermark(entries: readonly StandingSourceEntry[]): string | null; /** * Count entries strictly newer than a watermark (all, when there is none). * * @param entries - Source entries * @param watermark - Page watermark (ISO) or undefined * @returns Number of newer entries */ export declare function countNewer(entries: readonly StandingSourceEntry[], watermark: string | undefined): number; /** * Sections whose cited entries are no longer all valid (#914). * * A citation is invalid when no in-scope entry carries it (the entry was * deleted, or is no longer in scope) or when its entry is not in force * (superseded, deprecated, expired, resolved). Deterministic: no LLM, just * a set difference per section. * * @param page - Parsed page (undefined when missing) * @param entries - The page's current source entries * @returns One item per affected section, in page order, each naming its invalid citations * * @example * ```typescript * findInvalidatedSections(page, entries); * // [{ heading: 'Prompts', invalidCites: [{ cite: 'dec:d2', reason: 'not_in_force' }] }] * ``` */ export declare function findInvalidatedSections(page: StandingPage | undefined, entries: readonly StandingSourceEntry[]): InvalidatedSection[]; /** * A stable key for a page's invalidated citations (sorted, comma-joined), * '' when none. The refresh service stores it next to the watermark so a * retraction re-raises a refresh once, not on every tick. * * @param sections - Invalidated sections of one page * @returns The key */ export declare function invalidationKey(sections: readonly InvalidatedSection[]): string; /** * Format invalid citations for display: `dec:x (deleted), got:y (no longer in force)`. * * @param cites - Invalid citations * @returns One-line list */ export declare function formatInvalidCites(cites: readonly InvalidCite[]): string; /** * Reads, checks and writes standing-answer pages. * * Stateless apart from its paths; every call reads the files fresh, which * keeps the boot-time read honest (no cache can hide a newer memory). */ export declare class StandingAnswersService { private readonly crewlyHome; /** * @param options - `crewlyHome` overrides {@link getCrewlyHomePath} (tests) */ private readonly historyKeep; /** * @param options - `crewlyHome` overrides {@link getCrewlyHomePath} (tests); * `historyKeep` overrides {@link STANDING_ANSWERS_CONSTANTS.HISTORY_KEEP} */ constructor(options?: { crewlyHome?: string; historyKeep?: number; }); /** * Directory holding a page's snapshots and `changes.log`. * * @param def - Page definition * @param loc - Location * @returns Absolute path, or null when the location lacks what the scope needs */ historyDir(def: StandingPageDef, loc: StandingLocation): string | null; /** * Snapshots of a page, oldest first (absolute paths). * * @param def - Page definition * @param loc - Location * @returns Snapshot files; empty when there is no history */ listSnapshots(def: StandingPageDef, loc: StandingLocation): Promise; /** * File path of a page for a location. * * @param def - Page definition * @param loc - Project path (project pages) or session name (agent page) * @returns Absolute path, or null when the location lacks what the scope needs */ pagePath(def: StandingPageDef, loc: StandingLocation): string | null; /** * Load the memory entries a page is built from. Missing or unreadable * files yield an empty list (a fresh project has no memory yet). * * @param def - Page definition * @param loc - Location * @returns Normalised entries, newest first */ loadSourceEntries(def: StandingPageDef, loc: StandingLocation): Promise; /** * Page definitions a project holds: the built-in project pages, the owner * rules page, then the project's custom questions from * `/standing-questions.json` (#914 P2). A missing or unreadable * file means no custom questions. * * @param projectPath - Project root * @returns Definitions, built-ins first */ listProjectPageDefs(projectPath: string): Promise; /** * The project's custom questions and the reasons any entry was dropped. * * @param projectPath - Project root * @returns Parsed definitions; empty when the file is missing or not JSON */ loadCustomQuestions(projectPath: string): Promise<{ defs: StandingPageDef[]; dropped: string[]; }>; /** * Resolve a page id for a location: built-in pages first, then the * project's custom questions. * * @param pageId - Page id * @param loc - Location (custom pages need `projectPath`) * @returns The definition, or undefined */ resolvePageDef(pageId: string, loc: StandingLocation): Promise; /** * Resolve a page against the disk and its sources. * * @param def - Page definition * @param loc - Location * @returns Status, or null when the location cannot hold this page */ getPageStatus(def: StandingPageDef, loc: StandingLocation): Promise; /** * Status of every page a location can hold: the project pages (built-in, * owner rules, custom questions) when a project path is given, the agent * page when a session is given. * * @param loc - Location * @returns Statuses, project pages first */ listPageStatuses(loc: StandingLocation): Promise; /** * Write one section of a page (section-level edit): keep the rest, replace * or add this heading, or remove it (empty body). Before a content change * the previous page is snapshotted under {@link historyDir} and every write * appends a `changes.log` line, so each change is auditable (#914 P1). * * Validates the heading and body, requires every citation to name an * entry in the page's sources, masks secrets, then stamps * `last_refreshed` and moves `watermark` to the sources' current * watermark. Creates the page (with its question) when missing. * * @param input - Page, location, section and citations * @returns Where it was written and the new watermark * The owner rules page (#914 P2) is written by the owner only: an agent * write is refused with `owner_only`, citations are not required, and the * page never carries a watermark. * * @throws StandingAnswersError for an unknown page, bad input, an agent * writing an owner-only page, or a citation that is missing or does not resolve */ writeSection(input: WriteSectionInput): Promise; /** * The brief for a refresh WorkItem: the question, the current sections, * and the in-scope entries to use (newer than the watermark first), with * secrets masked and each entry's citation token. * * @param status - The page's status * @param loc - Location (for the skill command) * @param agentSkillsPath - Absolute path of the agent skills directory * @returns Markdown brief */ buildRefreshBrief(status: StandingPageStatus, loc: StandingLocation, agentSkillsPath: string): Promise; } /** * What writing `section` would do to `page`. * * @param page - Page as it stands * @param section - Incoming section (empty body = remove) * @returns The op */ export declare function sectionWriteOp(page: StandingPage, section: StandingSection): SectionWriteOp; /** * Pairs of in-force entries from the same UTC day that share a topic: the * later one may replace the earlier one without a supersede link (#884). * Returned as brief lines so the refresher checks their order. * * @param entries - Entries shown in the brief * @returns Brief lines (empty when there is nothing to flag) */ export declare function possibleSupersedeLines(entries: StandingSourceEntry[]): string[]; //# sourceMappingURL=standing-answers.service.d.ts.map