/** * Sentient Loop State — Persistent state for the Tier-1 autonomous daemon. * * Stored in `.cleo/sentient-state.json` (plain JSON, not SQLite) to avoid * SQLite WAL conflicts between the long-running daemon process and the * main CLEO CLI process. Human-readable for debugging. * * The file is gitignored (see .gitignore §.cleo/ section) and survives * restarts. Only `killSwitch`, `pid`, and `stats` fields are load-bearing * across process boundaries. * * @see ADR-054 — Sentient Loop Tier-1 (autonomous task execution) * @task T946 */ import type { Tier2Stats } from '@cleocode/contracts'; /** Schema version for sentient-state.json. Bump on breaking field changes. */ export declare const SENTIENT_STATE_SCHEMA_VERSION: "1.0"; /** * Per-task failure/backoff tracking for stuck detection. * Keyed by task id in {@link SentientState.stuckTasks}. */ export interface StuckTaskRecord { /** Number of consecutive failed spawn attempts for this task. */ attempts: number; /** ISO-8601 timestamp of the most recent failure. */ lastFailureAt: string; /** Unix epoch ms when the next retry becomes eligible. */ nextRetryAt: number; /** Last captured failure reason (truncated to 500 chars). */ lastReason: string; } /** * Rolling counters persisted across daemon restarts. */ export interface SentientStats { /** Total tasks picked by the loop since creation. */ tasksPicked: number; /** Total tasks that completed successfully. */ tasksCompleted: number; /** Total tasks whose spawn exited non-zero. */ tasksFailed: number; /** Total ticks executed (including no-op ticks). */ ticksExecuted: number; /** Total ticks aborted early because kill switch was active. */ ticksKilled: number; } /** * Persistent sentient daemon state. * * Design principles: * - `killSwitch` is the single load-bearing kill signal — the daemon re-checks * it between every step of a tick, not just at tick start (Round 2 audit). * - `stuckTasks` keys are task ids; values encode backoff + failure counts. * - `stuckTimestamps` is a rolling 1-hour window used for the self-pause rule * (5 stucks in 1 hour → killSwitch=true). * - `stats` fields are monotonic counters that only ever increase. */ export interface SentientState { /** JSON schema version for forward-compatibility checks. */ schemaVersion: typeof SENTIENT_STATE_SCHEMA_VERSION; /** PID of the currently running daemon process. null = daemon not running. */ pid: number | null; /** ISO-8601 timestamp when the daemon was last started. */ startedAt: string | null; /** ISO-8601 timestamp of the last completed tick (any outcome). */ lastTickAt: string | null; /** * ISO-8601 timestamp of the last cron-callback dispatch — written BEFORE * `safeRunTick` is invoked. Distinguishes "cron did not fire" from "cron * fired but tick hung" when diagnosing stale `lastTickAt`: * * - both stale → cron timer dead (node-cron lock leak, GC'd handle). * - heartbeat fresh + tick stale → tick hung mid-execution. * * Surfaced by `cleo sentient status` so `lastTickAt - lastCronFiredAt` * exposes per-tick duration trend. * * @task T-DAEMON-LASTTICKAT (T9320 follow-up) */ lastCronFiredAt: string | null; /** * Kill-switch flag. When true, the daemon re-checks at every step of a tick * and exits cleanly without picking or spawning a task. */ killSwitch: boolean; /** Reason supplied when killSwitch was last set (diagnostic only). */ killSwitchReason: string | null; /** * T1074: true when the daemon was paused by `pauseAllTiers(...)` as part of * an owner-triggered revert. Separate from `killSwitch` because resume * requires owner attestation (not just `cleo sentient resume`). */ pausedByRevert: boolean; /** * T1074: receipt id of the revert event that triggered the pause. Must match * the `afterRevertReceiptId` field in the owner attestation used to resume. */ revertReceiptId: string | null; /** Rolling counters; see {@link SentientStats}. */ stats: SentientStats; /** Per-task backoff + failure metadata for retry/stuck detection. */ stuckTasks: Record; /** * Unix-epoch-ms timestamps of `stuck` events within the last hour. * When length ≥ 5 the daemon self-pauses (killSwitch=true). */ stuckTimestamps: number[]; /** * Currently-active task id (set while a spawn is in-flight, cleared afterward). * Enables `status` to show the in-progress task during a long-running tick. */ activeTaskId: string | null; /** * Tier-2 proposal queue enabled flag. * * Default: `false` — Tier 2 is OFF by default to prevent surprise proposal * floods on first daemon start. Owner enables via `cleo sentient propose enable` * (patches this flag). See ADR-054 §Tier-2. * * @task T1008 */ tier2Enabled: boolean; /** * Rolling counters for Tier-2 proposal activity. * * @task T1008 */ tier2Stats: Tier2Stats; /** * T1030: Tier-3 autonomous auto-merge tick enabled flag. * * Default: `false` — Tier 3 is OFF by default. Owner opts in via * configuration or `cleo sentient tier3 enable`. */ tier3Enabled: boolean; /** * T1030: ISO-8601 timestamp of the last Tier-3 tick completion (any outcome). * Used for cadence gating — next tick only eligible after * {@link TIER3_CADENCE_MS} milliseconds. */ tier3LastTickAt: string | null; /** * T1030: Rolling counters for Tier-3 ritual outcomes. */ tier3Stats: Tier3Stats; /** * T1637: ISO-8601 timestamp of the last completed cross-project hygiene loop. * Null when the loop has never run. Surfaced by `cleo daemon status`. */ hygieneLastRunAt: string | null; /** * T1637: One-line human-readable summary of the last hygiene run. * Surfaced by `cleo daemon status` for quick diagnostics. */ hygieneSummary: string | null; /** * T1637: Rolling counts from the last hygiene loop. */ hygieneStats: HygieneStats; } /** * T1637: Rolling counts from the last cross-project hygiene loop. */ export interface HygieneStats { /** Total projects checked in the last NEXUS integrity scan. */ projectsChecked: number; /** Projects reported healthy in the last scan. */ projectsHealthy: number; /** Temp-project GC candidates flagged in the last scan. */ tempGcCandidates: number; /** Duplicate-epic groups detected in the last scan. */ duplicateEpicGroups: number; /** Stale agent worktrees pruned in the last scan. */ worktreesPruned: number; } /** * T1030: Rolling counters for Tier-3 merge ritual outcomes. */ export interface Tier3Stats { /** Total ticks killed mid-ritual by the kill-switch. */ ticksKilled: number; /** Total ticks aborted (verify failed OR FF merge failed). */ abortsTotal: number; /** Total successful FF merges. */ mergesCompleted: number; } /** Default (empty) sentient state for fresh initialisation. */ export declare const DEFAULT_SENTIENT_STATE: SentientState; /** * T1074: Error code thrown when `resumeSentientDaemon()` is called but the * state has `pausedByRevert=true`. Owner must provide a valid attestation * via `resumeAfterRevert()` first. */ export declare const E_OWNER_ATTESTATION_REQUIRED: "E_OWNER_ATTESTATION_REQUIRED"; /** * T1074: Owner attestation payload required to resume the sentient daemon * after an owner-triggered revert. The attestation is a signed commitment * that the owner has verified the revert receipt and authorises resumption. * * Canonical serialisation for signing: `JSON.stringify(sortedKeys)` of the * `{afterRevertReceiptId, issuedAt, ownerPubkey}` triple (alphabetically * sorted keys). The `sig` is the hex-encoded Ed25519 signature over those * serialised bytes. */ export interface OwnerRevertAttestation { /** Revert event receipt id being attested. Must match `state.revertReceiptId`. */ afterRevertReceiptId: string; /** ISO-8601 timestamp when the attestation was issued. */ issuedAt: string; /** Hex-encoded 32-byte Ed25519 public key of the signing owner. */ ownerPubkey: string; /** Hex-encoded 64-byte Ed25519 signature over the canonical payload. */ sig: string; } /** * Read the sentient state from disk. * * Returns the default state if the file does not exist or is malformed. * Never throws — absence is not an error. * * @param statePath - Absolute path to sentient-state.json */ export declare function readSentientState(statePath: string): Promise; /** * Write the sentient state to disk atomically via tmp-then-rename. * * Atomic write prevents partial reads if the daemon crashes mid-write. * * @param statePath - Absolute path to sentient-state.json * @param state - State to persist */ export declare function writeSentientState(statePath: string, state: SentientState): Promise; /** * Patch a subset of fields in the sentient state file. * * Reads current state, merges patch, writes back. Nested `stats` merges * with existing stats (never clobbered wholesale). * * @param statePath - Absolute path to sentient-state.json * @param patch - Partial state to merge over the existing state * @returns The merged state that was written to disk. */ export declare function patchSentientState(statePath: string, patch: Partial): Promise; /** * Increment stats counters atomically. * * @param statePath - Absolute path to sentient-state.json * @param delta - Partial stats to add to current counters */ export declare function incrementStats(statePath: string, delta: Partial): Promise; /** * Set the global kill-switch to pause ALL sentient tiers (1/2/3) after an * owner-triggered revert. Mirrors `cleo revert --from ` semantics: * no new ticks start until owner runs `cleo sentient resume`. * * @param statePath - Absolute path to sentient-state.json * @param receiptId - Revert event receiptId for audit trail * @task T1036-T1040 */ export declare function pauseAllTiers(statePath: string, receiptId: string): Promise; /** * T1074: Resume the sentient daemon after an owner-triggered revert. * * Requires a valid owner attestation signed by a pubkey in `allowedPubkeys`. * The attestation's `afterRevertReceiptId` MUST match `state.revertReceiptId`. * On success clears `killSwitch`, `pausedByRevert`, and `revertReceiptId`. * * Rejects with `E_OWNER_ATTESTATION_REQUIRED` (via thrown Error) when: * - state is not in `pausedByRevert=true` mode * - `attestation.afterRevertReceiptId` is empty or mismatched * - `attestation.ownerPubkey` is not in `allowedPubkeys` * - signature verification fails * * @param statePath - Absolute path to sentient-state.json * @param attestation - Owner attestation payload (must pass `verifySignature`) * @param allowedPubkeys - Set of hex pubkeys authorised to resume * @task T1074 */ export declare function resumeAfterRevert(statePath: string, attestation: OwnerRevertAttestation, allowedPubkeys: Set): Promise; //# sourceMappingURL=state.d.ts.map