/** * Logic trace extension. * * Maintains EXACTLY ONE cloud-hosted file, `logic.aexol`, inside the Aexol * Studio project the current directory is bound to (`.aexol/aexol.jsonc`). * The file mirrors the project's business logic (types / enums / roles / * workflows / agents) as a valid Aexol specification. * * Automatic refresh works the same way observational memory refreshes * observations (see `../memory/hooks/observer-trigger.ts`): * - `turn_end` hook fires the refresh attempt, * - a token threshold gates how much new material must exist before a run, * - an in-flight singleton prevents concurrent runs, * - a passive flag (`PI_LOGIC_TRACE_PASSIVE`) disables it entirely. * * A `session_before_compact` hook additionally forces one flush attempt with * the accumulated delta before the context is compacted away (mirroring the * flush-before-compact semantics of `../memory/hooks/compaction-hook.ts`). * * Each refresh fetches the CURRENT remote `logic.aexol` and lets the model * return the FULL updated spec (merge, never append-only) through a single * `submit_logic_trace` tool call. Pushes are skipped when the model reports * no change or when the content hash is unchanged (hashId). * * This is a SEPARATE mechanism from memory/observations — it only reuses * generic helpers from `../memory/` and never writes any memory state. */ import { Type } from "../sdk/ai/index.js"; import { type AgentTool } from "../sdk/agent-core/index.js"; import { type ExtensionAPI, type ExtensionUIContext } from "../sdk/coding-agent/index.js"; import { type StudioBinding } from "../studio-binding.js"; import { type AexolBackendBootstrap } from "./aexol-backend-client.js"; import { type BackendRefineDraftRunnerOptions } from "./logic-trace-backend-draft.js"; import { type Entry } from "../memory/branch.js"; /** The single remote file this extension maintains. */ export declare const LOGIC_TRACE_REMOTE_PATH = "logic.aexol"; /** Default token amount of new material required before an automatic sync. */ export declare const DEFAULT_THRESHOLD_TOKENS = 800; /** Default cadence between backend refine task polls (draftBackend path). */ export declare const DEFAULT_DRAFT_BACKEND_POLL_INTERVAL_MS = 2000; /** Default total wait for a backend refine task to reach a terminal status. */ export declare const DEFAULT_DRAFT_BACKEND_TIMEOUT_MS = 120000; export interface LogicTraceModelRef { provider: string; id: string; } export interface LogicTraceSettings { passive?: boolean; thresholdTokens?: number; /** * Opt-in: draft the .aexol spec through the backend from/refine pipeline * (remote_refine_aexol_content) instead of the local LLM draftRunner. * Default false — the local draft runner stays the default path and the * fallback whenever the backend draft produces no submission. */ draftBackend?: boolean; draftBackendPollIntervalMs?: number; draftBackendTimeoutMs?: number; draftBackendMaxRetries?: number; model?: LogicTraceModelRef; } export interface LogicTraceConfig { passive: boolean; thresholdTokens: number; draftBackend: boolean; draftBackendPollIntervalMs: number; draftBackendTimeoutMs: number; draftBackendMaxRetries?: number; model?: LogicTraceModelRef; } /** * Resolve settings from `~/.spectral/settings.json` and `/.spectral/settings.json` * (project wins), then apply the `PI_LOGIC_TRACE_PASSIVE` env override — the * same layered scheme as `../memory/config.ts` under its own namespaced key. */ export declare function loadLogicTraceConfig(cwd: string, env?: NodeJS.ProcessEnv): LogicTraceConfig; /** Exported: also embedded into the backend-refine prompt (draftBackend path). */ export declare const LOGIC_TRACE_GRAMMAR = ".aexol quick grammar (one file, top-level blocks in any order, '#' lines are comments):\n- type Name { fieldName: Type ... } \u2014 one field per line. Scalar types: string, number, boolean, datetime.\n A capitalized identifier references another type or enum; a trailing [] marks a list (e.g. items: Task[]).\n- enum Name { values, one per line, then '}' on its own line }.\n- role Name { permission resource { actions } } \u2014 actions from: create, read, update, delete.\n- workflow Name { a single 'initial stateA' line, then one 'state X -> Y, Z' line per transition }.\n- agent Name: BaseRole { can ..., must ..., should ... } \u2014 ' : BaseRole' is optional.\n- visitor { can ... } \u2014 optional.\n- Reserved grammar words (visitor, type, enum, role, workflow, agent, permission, action, state, transition, \u2026) must NOT appear as identifiers: e.g. do not name a visitor/test step 'state' \u2014 write 'can inspect sync status', never 'can inspect sync state'."; /** * Leak-rule bullet list shared by LOGIC_TRACE_SYSTEM (local draft) and the * backend refine adapter's MERGE_INSTRUCTIONS — defined once so the two * prompts cannot drift apart. */ export declare const LOGIC_TRACE_LEAK_RULES = "- NEVER include secrets: no API keys or tokens (sk-\u2026, xox\u2026, ghp_\u2026, AKIA\u2026, JWTs), no Bearer/Authorization headers, and no KEY=value environment-variable-style assignments.\n- NEVER include absolute filesystem paths (/home/\u2026, /Users/\u2026, /root/\u2026, C:\\Users\\\u2026) or localhost URLs; refer to resources by project-relative names. Redact or omit anything sensitive."; export declare const LOGIC_TRACE_SYSTEM = "You maintain the ONE logic.aexol file of the current Aexol Studio project: a valid, human-readable Aexol specification that mirrors the project's business logic (domain types, enums, roles, workflows, agents). It is always a FULL spec, never a changelog.\n\n.aexol quick grammar (one file, top-level blocks in any order, '#' lines are comments):\n- type Name { fieldName: Type ... } \u2014 one field per line. Scalar types: string, number, boolean, datetime.\n A capitalized identifier references another type or enum; a trailing [] marks a list (e.g. items: Task[]).\n- enum Name { values, one per line, then '}' on its own line }.\n- role Name { permission resource { actions } } \u2014 actions from: create, read, update, delete.\n- workflow Name { a single 'initial stateA' line, then one 'state X -> Y, Z' line per transition }.\n- agent Name: BaseRole { can ..., must ..., should ... } \u2014 ' : BaseRole' is optional.\n- visitor { can ... } \u2014 optional.\n- Reserved grammar words (visitor, type, enum, role, workflow, agent, permission, action, state, transition, \u2026) must NOT appear as identifiers: e.g. do not name a visitor/test step 'state' \u2014 write 'can inspect sync status', never 'can inspect sync state'.\n\nReference template (shape only \u2014 not content you must reproduce):\n\"\"\"\n# Project Logic\n\ntype ChecklistItem {\n id: string\n title: string\n done: boolean\n createdAt: datetime\n assignee: string\n status: ItemState\n}\n\nenum ItemState {\n planned\n started\n done\n archived\n}\n\nworkflow ItemLifecycle {\n initial planned\n state planned -> started, archived\n state started -> done, planned\n state done -> done\n state archived -> archived\n}\n\nrole DomainAdmin {\n permission checklist { create, read, update, delete }\n}\n\nagent ChecklistAgent: DomainAdmin {\n can plan items\n must validate titles\n}\n\nvisitor {\n can view items\n}\n\"\"\"\n\nRules:\n- Return the COMPLETE updated file every time. Merge new facts into the existing structure (\"merge, never append-only\"): extend or reorganize existing types/agents/workflows instead of duplicating them.\n- HARD CONSTRAINT: the submitted file MUST parse with the Aexol parser (types, enums, workflows, roles, agents, visitors). Invalid syntax is rejected with parser errors you must fix and resubmit.\n- Keep the existing correct definitions; remove one only when it is clearly superseded by the new information.\n- Keep the file compact and grammar-valid. Validity beats exhaustiveness; do not invent facts beyond the recent activity.\n- NEVER include secrets: no API keys or tokens (sk-\u2026, xox\u2026, ghp_\u2026, AKIA\u2026, JWTs), no Bearer/Authorization headers, and no KEY=value environment-variable-style assignments.\n- NEVER include absolute filesystem paths (/home/\u2026, /Users/\u2026, /root/\u2026, C:\\Users\\\u2026) or localhost URLs; refer to resources by project-relative names. Redact or omit anything sensitive.\n- If the recent activity does not change business logic meaningfully (docs, prose, file paths, UI cosmetics), submit changed=false.\n- Call submit_logic_trace exactly once with the full content, then reply with a short plain-text confirmation to end the run."; /** * Strip secret/host-specific material from spec content before it is sent to * remote_update_project_file / remote_create_project_file. Best-effort, * format-preserving token pass: anything that looks like an API key * (sk-…, xox*-, ghp…) a Bearer token, a JWT, an AWS access key, a * `KEY=value` environment assignment, an absolute filesystem path, or a * localhost URL is replaced in place with a redaction marker. */ export declare function sanitizeLogicTraceContent(content: string): string; export interface LogicTraceDraftInput { model: unknown; apiKey: string; headers?: Record; /** Current remote logic.aexol content, or undefined when not created yet. */ currentContent: string | undefined; /** Serialized recent conversation delta (already length-capped). */ delta: string; signal?: AbortSignal; } export interface LogicTraceSubmission { content: string; changed: boolean; } export interface LogicTraceDraftOutput { submitted?: LogicTraceSubmission; } export type LogicTraceDraftRunner = (input: LogicTraceDraftInput) => Promise; declare const SubmitLogicTraceSchema: Type.TObject<{ content: Type.TString; changed: Type.TBoolean; }>; /** * Build the draft-loop `submit_logic_trace` tool. Exported (like the internal * `buildReadTool` / `buildWriteTool` builders, but visible to tests) so the * parser-backed gate is unit-testable without an LLM: `collect` fires exactly * once per VALID submission; invalid ones are rejected with parser diagnostics * and never collected. Runtime wiring in `runLogicTraceDraft` is unchanged. */ export declare function buildSubmitTool(collect: (submission: LogicTraceSubmission) => void): AgentTool; /** * Default draft runner: ONE agentLoop call with a single submit tool. * Structurally mirrors `../memory/observer.ts` (drain events, await result, * collect via the tool's execute) but accumulates exactly one submission. */ export declare function runLogicTraceDraft(input: LogicTraceDraftInput): Promise; interface RemoteState { id: string | null; contentHash: string | undefined; } interface LogicTraceServerState { binding: StudioBinding | null; bindingMtimeMs: number | null; /** Last known remote {id, contentHash} per cwd — refreshed on fetch & push. */ remote: RemoteState | null; /** rawTokensSinceLastBound value at the last sync ATTEMPT (skip-gate watermark). */ syncedTokens: number; /** In-flight singleton. */ syncInFlight: boolean; /** Promise of the in-flight sync (null when idle). Test/diagnostic hook. */ syncPromise: Promise | null; } /** * Await/diagnose the in-flight sync for a cwd (null when idle). Exposed for * deterministic tests and diagnostics; production hooks stay fire-and-forget. */ export declare function pendingLogicTraceSync(cwd: string): Promise | null; export interface LogicTraceOutcome { pushed: boolean; ok: boolean; reason?: string; } export interface LogicTraceSyncDeps { cwd: string; config: LogicTraceConfig; state: LogicTraceServerState; /** * Lazy backend bootstrap — invoked ONLY after the binding check and the * token-threshold gate passed, so an unbound or sub-threshold turn never * bootstraps a backend (no token work, no OAuth browser flow). */ getBootstrap: (cwd: string) => Promise; draftRunner: LogicTraceDraftRunner; /** * Factory for the optional backend refine draft runner (draftBackend * setting). Invoked per sync only when config.draftBackend is on; its * empty outcome triggers the local `draftRunner` fallback. */ backendDraftFactory: (options: BackendRefineDraftRunnerOptions) => LogicTraceDraftRunner; signal?: AbortSignal; ui: UiNotifier; model: unknown; modelRegistry: unknown; entries: Entry[]; leafId: string | undefined; /** Force mode (compaction flush): bypass the token-threshold gate. */ force?: boolean; } interface UiNotifier { hasUI: boolean; ui?: ExtensionUIContext; } /** * Run one guarded sync attempt. The in-flight singleton guard is checked AND * set synchronously so two back-to-back hook invocations can never race into * a second concurrent run. The returned promise never rejects; concurrent * callers while a run is in flight get `null` instead. */ export declare function runLogicTraceSync(deps: LogicTraceSyncDeps): Promise | null; export interface LogicTraceExtensionOptions { /** Injectable draft step — tests pass a fake runner here. */ draftRunner?: LogicTraceDraftRunner; /** * Injectable backend refine draft factory — tests point this at a stub * (or leave the default to exercise the real adapter against a mock MCP). */ backendDraftFactory?: (options: BackendRefineDraftRunnerOptions) => LogicTraceDraftRunner; /** Injectable backend bootstrap — tests point this at a mock MCP server. */ clientFactory?: (options: { warn?: (line: string) => void; }) => Promise; } /** * Logic trace extension entry point: automatic refresh hooks * (`turn_end`, `session_before_compact`) plus the two agent tools. */ export declare function logicTraceExtension(ext: ExtensionAPI, options?: LogicTraceExtensionOptions): void; export default logicTraceExtension; //# sourceMappingURL=logic-trace.d.ts.map