/** * Sentient Loop Tick — Single-iteration tick runner for the Tier-1 daemon. * * A tick is one complete pass of: * 1. Check killSwitch (abort if true) * 2. Pick an unblocked task via @cleocode/core/sdk * 3. Check killSwitch again (abort if true) * 4. Spawn worker via `cleo orchestrate spawn --adapter ` * 5. Check killSwitch again before recording result * 6. Record success (receipt + stats) or failure (retry/backoff) * * Each step re-reads the state file so that a killSwitch flipped mid-tick is * honoured on the very next instruction (Round 2 audit §1: "mid-experiment * kill limbo"). * * Rate limit: driven by the cron schedule (`*\/5 * * * *` → ≤12 ticks/hour ≤ * 12 spawns/hour). No in-tick sleep is required — cron provides the cadence. * * Scoped OUT: Tier 2 (propose) and Tier 3 (sandbox auto-merge) per ADR-054. * * @task T946 * @see ADR-054 — Sentient Loop Tier-1 */ import type { Task } from '@cleocode/contracts'; import { type ReVerifyOptions, type WorkerReport } from '../orchestrate/worker-verify.js'; import { type ContextStalenessScanOutcome } from './detectors/context-staleness.js'; import { type SentientState } from './state.js'; /** * Number of new brain observations since the last consolidation that causes * the tick loop to trigger a dream cycle (volume tier). * Configurable via the injected `dreamVolumeThreshold` option. */ export declare const DREAM_VOLUME_THRESHOLD_DEFAULT = 50; /** * Number of consecutive no-task ticks before the idle dream trigger fires. * Represents "N idle ticks" — when no task has been picked for this many * consecutive ticks, the system is considered sufficiently idle. */ export declare const DREAM_IDLE_TICKS_DEFAULT = 5; /** Default adapter used when spawning workers. */ export declare const DEFAULT_ADAPTER: "claude-code"; /** * Backoff delays between successive retries for the same task (milliseconds). * Index n = delay before attempt n+1. After exhaustion the task is `stuck`. * 30 s → 5 min → 30 min, then the task is marked stuck. */ export declare const RETRY_BACKOFF_MS: readonly number[]; /** * Maximum spawn attempts per task before it is classified as `stuck`. * Matches RETRY_BACKOFF_MS.length but surfaced as a named constant for * readability in tests and status output. */ export declare const MAX_TASK_ATTEMPTS: number; /** * Threshold for self-pause: if this many tasks become `stuck` within a * rolling 1-hour window, the daemon flips killSwitch=true and exits. */ export declare const SELF_PAUSE_STUCK_THRESHOLD = 5; /** Rolling window (ms) used for stuck-rate calculation. */ export declare const SELF_PAUSE_WINDOW_MS: number; /** Reason stored on the state file when self-pause fires. */ export declare const SELF_PAUSE_REASON = "self-pause: 5 stuck tasks in 1 hour"; /** Max wall-clock time for a single spawn before forceful kill (30 min). */ export declare const SPAWN_TIMEOUT_MS: number; /** Discriminant for the tick outcome. */ export type TickOutcomeKind = 'killed' | 'no-task' | 'backoff' | 'success' | 'failure' | 'stuck' | 'self-paused' | 'error'; /** Structured outcome of a single tick. */ export interface TickOutcome { /** Discriminant describing how the tick ended. */ kind: TickOutcomeKind; /** Task id that was the subject of this tick (if any). */ taskId: string | null; /** Human-readable detail (one line). */ detail: string; } /** Options for {@link runTick}. */ export interface TickOptions { /** Absolute path to the project root (contains `.cleo/`). */ projectRoot: string; /** Absolute path to sentient-state.json. */ statePath: string; /** * Adapter to pass to `cleo orchestrate spawn --adapter`. Defaults to * {@link DEFAULT_ADAPTER}. Overridden in tests via options.spawn. */ adapter?: string; /** * Dry-run mode: skip the actual `cleo orchestrate spawn` subprocess and * treat the pick as a no-op (still records picked stat). Used by * `cleo sentient tick --dry-run`. */ dryRun?: boolean; /** * Override for the spawn function — lets tests inject a deterministic fake * without forking real subprocesses. Must resolve to an exit code. * * When omitted, the default implementation spawns * `cleo orchestrate spawn --adapter ` and resolves with * the child's exit code. */ spawn?: (taskId: string, adapter: string) => Promise; /** * Override for the "pick next unblocked task" source. Lets tests return * a deterministic task without constructing a SQLite fixture. */ pickTask?: (projectRoot: string) => Promise; /** * Scope the picker to tasks that are members of a specific Saga (or its * member Epics' children). When set, `defaultPickTask` fetches the Saga's * member Epic IDs and restricts candidates to tasks whose `parentId` is one * of those Epics. Dependency-wave ordering within the filtered set is applied. * * Used by the headless / walk-away variant (`cleo daemon install --saga ` * and `cleo daemon start --foreground --saga `). * * @task T11497 E5-HEADLESS */ scopeSagaId?: string; /** * Scope the picker to tasks whose `parentId` is exactly this Epic ID. * Dependency-wave ordering within the filtered set is applied. * Takes precedence over `scopeSagaId` when both are set. * * Used by the headless / walk-away variant (`cleo daemon install --epic ` * and `cleo daemon start --foreground --epic `). * * @task T11497 E5-HEADLESS */ scopeEpicId?: string; /** * New observation count since last consolidation that triggers the volume * dream cycle. Defaults to {@link DREAM_VOLUME_THRESHOLD_DEFAULT}. * Pass 0 to disable volume trigger. Injected by tests. */ dreamVolumeThreshold?: number; /** * Number of consecutive no-task ticks before the idle dream trigger fires. * Defaults to {@link DREAM_IDLE_TICKS_DEFAULT}. * Pass 0 to disable idle trigger. Injected by tests. */ dreamIdleTicks?: number; /** * Override for the dream trigger function — lets tests assert dream calls * without touching the real brain.db stack. * Signature mirrors `checkAndDream` from `@cleocode/core`. */ checkAndDream?: (projectRoot: string, opts?: { volumeThreshold?: number; inline?: boolean; }) => Promise<{ triggered: boolean; tier: string | null; skippedReason?: string; }>; /** * When set to `false`, skips the deriver batch trigger in `safeRunTick`. * Useful in tests that don't need the deriver path loaded. * Default: true (deriver batch fires when queue has pending items). * * @task T1145 */ runDeriverBatch?: boolean; /** * Override for the orchestrator-side worker re-verification gate (T1589). * * When omitted, the default {@link reVerifyWorkerReport} runs `tool:test` * (project-resolved per ADR-061) and compares the worker's claimed * touched-files against `git status --porcelain`. Tests inject a stub to * force accept/reject without spawning real subprocesses. * * @task T1589 */ reVerify?: (report: WorkerReport, options: ReVerifyOptions) => Promise<{ accepted: boolean; }>; /** * Disable the worker re-verification gate entirely. Defaults to `false` * (gate enabled). Only set to `true` for `--dry-run` ticks or controlled * test paths that have already verified the worker by other means. * * @task T1589 */ skipReVerify?: boolean; /** * Override for the stage-drift scan function. Injected by tests to avoid * opening a real tasks.db. When omitted the default * {@link safeRunStageDriftScan} from `./stage-drift-tick.js` is used. * * Pass `null` to disable the drift scan entirely (e.g. unit tests that * don't exercise the drift path). * * @task T1635 */ stageDriftScan?: ((projectRoot: string, statePath: string) => Promise) | null; /** * Interval (ms) between stage-drift scan passes. * Defaults to {@link DRIFT_SCAN_INTERVAL_MS} (30 min). * Pass 0 to scan on every tick (useful for integration tests). * * @task T1635 */ stageDriftIntervalMs?: number; /** * Override for the hygiene scan function. Injected by tests to avoid opening * a real tasks.db or brain.db. When omitted, the default * {@link safeRunHygieneScan} from `./hygiene-scan.js` is used. * * Pass `null` to disable the hygiene scan entirely (e.g. unit tests that * don't exercise the hygiene path). * * @task T1636 */ hygieneScan?: ((projectRoot: string, statePath: string) => Promise) | null; /** * Interval (ms) between hygiene scan passes. * Defaults to {@link HYGIENE_SCAN_INTERVAL_MS} (4 hours). * Pass 0 to scan on every tick (useful for integration tests). * * @task T1636 */ hygieneScanIntervalMs?: number; /** * Override for the dream cycle function. Injected by tests to avoid opening * a real brain.db or making real LLM calls. When omitted, the default * {@link safeRunDreamCycle} from `./dream-cycle.js` is used. * * Pass `null` to disable the dream cycle entirely (e.g. unit tests that * don't exercise the dream cycle path). * * @task T1680 */ dreamCycle?: ((options: import('./dream-cycle.js').DreamCycleOptions) => Promise) | null; /** * Interval (ms) between dream cycle runs. * Defaults to {@link DREAM_CYCLE_INTERVAL_MS} (4 hours). * Pass 0 to run on every tick (useful for integration tests). * * @task T1680 */ dreamCycleIntervalMs?: number; /** * Override for the context-staleness scan function. Injected by tests to * avoid reading `.cleo/project-context.json` from disk. When omitted the * default {@link safeRunContextStalenessScan} from * `./detectors/context-staleness.js` is used. * * Pass `null` to disable the staleness scan entirely (e.g. unit tests that * don't exercise this path). * * @task T9896 */ contextStalenessScan?: ((projectRoot: string, statePath: string) => Promise) | null; /** * Interval (ms) between context-staleness scan passes. * Defaults to {@link CONTEXT_STALENESS_SCAN_INTERVAL_MS} (6 hours). * Pass 0 to scan on every tick (useful for integration tests). * * @task T9896 */ contextStalenessIntervalMs?: number; } /** Result of a spawn invocation. */ export interface SpawnResult { /** Process exit code (0 = success). */ exitCode: number; /** Captured stdout, truncated by the caller if needed. */ stdout: string; /** Captured stderr, truncated by the caller if needed. */ stderr: string; } /** * How many ticks between each worktree prune pass. * * Prune runs every N ticks to clean up stale worktree directories from * experiments that have already completed or been abandoned. Default: 10 ticks. */ export declare const WORKTREE_PRUNE_INTERVAL_TICKS = 10; /** * Run a single tick of the sentient loop. * * Every checkpoint re-reads state so that a killSwitch flipped mid-tick is * honoured on the next instruction. * * @param options - Tick options (see {@link TickOptions}) * @returns Structured outcome describing how the tick ended. */ export declare function runTick(options: TickOptions): Promise; /** * Convenience wrapper used by the daemon cron handler and the `cleo sentient * tick` CLI command. Reads state, runs a tick, swallows any unexpected * exception so the cron scheduler never sees a rejection. * * After the tick completes, evaluates volume + idle dream triggers via * {@link maybeTriggerDream}. Dream errors are swallowed independently so * they never affect the tick outcome. * * @param options - Tick options * @returns The tick outcome (or an `error` outcome if the tick itself threw). */ export declare function safeRunTick(options: TickOptions): Promise; /** * Type-narrowing helper used by tests and status rendering to identify tick * outcomes that consumed a retry attempt. */ export declare function isFailureOutcome(outcome: TickOutcome): outcome is TickOutcome & { kind: 'failure' | 'stuck' | 'self-paused'; }; /** * Returns a shallow view of the current state's kill status. * Exposed for diagnostic/test consumers. */ export declare function getKillStatus(statePath: string): Promise>; /** * Reset dream-cycle in-process state. * * Intended for test teardown only — clears `consecutiveIdleTicks` so that * successive test cases start from a clean slate. * * @internal */ export declare function _resetDreamTickState(): void; /** * Reset worktree prune tick counter. * * Intended for test teardown only — allows tests to reset the prune cadence * counter so successive test cases start from a clean slate. * * @internal */ export declare function _resetWorktreePruneTickCount(): void; /** * Return the current worktree prune tick counter value. * * Read-only accessor for test assertions. * * @internal */ export declare function _getWorktreePruneTickCount(): number; /** * Return the current consecutive-idle-tick counter value. * * Read-only accessor for test assertions. The counter is reset to 0 whenever * a task is picked, and incremented on each no-task tick. * * @internal */ export declare function _getConsecutiveIdleTicks(): number; /** * Reset the stage-drift scan timestamp. * * Intended for test teardown only — allows tests to reset the scan cadence * so successive test cases start from a clean slate (next tick fires immediately). * * @internal * @task T1635 */ export declare function _resetStageDriftScanAt(): void; /** * Return the unix-epoch-ms timestamp of the last stage-drift scan. * * Read-only accessor for test assertions. * * @internal * @task T1635 */ export declare function _getLastStageDriftScanAt(): number; /** * Reset the hygiene scan timestamp. * * Intended for test teardown only — allows tests to reset the scan cadence * so successive test cases start from a clean slate (next tick fires immediately). * * @internal * @task T1636 */ export declare function _resetHygieneScanAt(): void; /** * Return the unix-epoch-ms timestamp of the last hygiene scan. * * Read-only accessor for test assertions. * * @internal * @task T1636 */ export declare function _getLastHygieneScanAt(): number; /** * Reset the dream cycle scan timestamp. * * Intended for test teardown only — allows tests to reset the scan cadence * so successive test cases start from a clean slate (next tick fires immediately). * * @internal * @task T1680 */ export declare function _resetDreamCycleScanAt(): void; /** * Return the unix-epoch-ms timestamp of the last dream cycle scan. * * Read-only accessor for test assertions. * * @internal * @task T1680 */ export declare function _getLastDreamCycleScanAt(): number; /** T1030: Tier 3 cadence — 15 minutes between merge attempts. */ export declare const TIER3_CADENCE_MS: number; /** Task reference passed between tick steps. */ type Tier3Task = import('@cleocode/contracts').Task; /** Options for {@link runTier3Tick}. All side-effecting deps are injectable. */ export interface Tier3TickOptions { /** Absolute path to the project root (target branch worktree). */ projectRoot: string; /** Absolute path to sentient-state.json. */ statePath: string; /** Whether Tier 3 is enabled. When false, tick no-ops with skipped:disabled. */ enabled: boolean; /** * ISO-8601 timestamp of last tick. Used for cadence gating; callers * typically pass `state.tier3LastTickAt`. */ lastTickAt: string | null; /** Optional override for target branch (defaults to 'main'). */ targetBranch?: string; /** Picker returning the next Tier-3-eligible task or null. */ pickTask: (projectRoot: string) => Promise; /** Anchor the rollback baseline before spawning the sandbox. */ captureBaseline: (projectRoot: string, commitSha: string) => Promise<{ receiptId: string; }>; /** Resolve the current HEAD SHA of the target branch. */ getHeadSha: (projectRoot: string) => Promise; /** Spawn the experimental sandbox and return its metadata. */ spawnSandbox: (task: Tier3Task, projectRoot: string) => Promise; /** Block until the sandbox's patch completes. */ waitForPatch: (spawn: SandboxSpawnRecord) => Promise; /** Run verification gates inside the experiment worktree. */ verifyInWorktree: (spawn: SandboxSpawnRecord, patch: PatchRecord) => Promise; /** Sign the experiment (receipt for the merge intent). */ signExperiment: (spawn: SandboxSpawnRecord, verify: VerifyRecord) => Promise; /** Record task as auto-merged in tasks.db after successful merge. */ markTaskAutoMerged: (projectRoot: string, taskId: string, mergedSha: string) => Promise; } /** Spawn record produced by the sandbox orchestrator. */ export interface SandboxSpawnRecord { /** Stable UUID for the experiment run. */ experimentId: string; /** Absolute path to the experiment worktree (FF source). */ worktree: string; /** Receipt id produced by {@link captureBaseline} or the spawner. */ receiptId: string; } /** Patch record emitted when the sandbox completes its change. */ export interface PatchRecord { /** Files modified by the patch (relative to worktree root). */ patchFiles: string[]; /** One-line human-readable patch summary. */ patchSummary: string; /** Content hash of the applied patch (hex). */ patchSha: string; /** Receipt id linking the patch to its sign/verify chain. */ receiptId: string; } /** Verify record summarising IVTR gate outcomes. */ export interface VerifyRecord { /** Whether ALL gates passed. */ passed: boolean; /** Per-gate outcomes. */ gates: readonly { readonly gate: string; readonly passed: boolean; readonly evidenceAtoms?: readonly string[]; }[]; /** Post-run metrics captured by the verifier. */ afterMetrics?: Record; /** Receipt id linking verify output to the signed experiment. */ receiptId: string; } /** Sign record produced when the experiment is committed to merge intent. */ export interface SignRecord { /** Hex-encoded 64-byte Ed25519 signature over the canonical receipt. */ signature: string; /** Receipt id of the sign step. */ receiptId: string; } /** Outcome returned by {@link runTier3Tick}. */ export interface Tier3TickOutcome { /** Kind of outcome. */ kind: 'skipped' | 'killed' | 'no-eligible-tasks' | 'aborted' | 'merged'; /** Human-readable detail (included in every outcome). */ detail: string; /** Task id for outcomes that picked a task. */ taskId?: string; /** HEAD SHA after merge (only for `merged` outcomes). */ mergedSha?: string; } /** * T1030: Run one iteration of the Tier-3 auto-merge ritual. * * Invariants: * - NEVER auto-rebases; ONLY FF merge via {@link gitFfMerge}. * - Kill-switch checkpoints at pre-pick, post-pick, post-spawn, pre-verify. * - verify-failed → aborts BEFORE sign / merge. * - ff-failed → aborts AFTER sign (sign records intent; merge fails). * - Happy path bumps `state.tier3Stats.mergesCompleted` + `tier3LastTickAt`. * * @task T1030 */ export declare function runTier3Tick(options: Tier3TickOptions): Promise; export {}; //# sourceMappingURL=tick.d.ts.map