import { type PresenceRoleTier } from './presence-role.js'; export type SkillPresenceMode = 'full-auto' | 'assisted' | 'strict' | '24h'; export declare const VALID_SKILL_PRESENCE_MODES: ReadonlyArray; export declare const LEGACY_SKILL_PRESENCE_MODE_ALIASES: Readonly>; export declare function isSkillPresenceMode(value: string): value is SkillPresenceMode; /** * Normalize a raw on-disk / CLI mode token to a current * `SkillPresenceMode`. Current values pass through; the legacy * `'swarm'` alias maps to `'full-auto'`; anything else returns * `undefined` so the caller can decide (drop the field, not the * presence). Never throws. */ export declare function normalizeSkillPresenceMode(value: string | undefined | null): SkillPresenceMode | undefined; export type SkillPresence = { skill: string; mode?: SkillPresenceMode; gate?: string; /** The skill's role tier (`orchestrator` | `bee`; `null` for an unknown skill). */ role?: PresenceRoleTier | null; sessionId?: string; /** * Identifier of the *outer* session — the Claude Code / Cursor / * VSCode-plugin / other harness session that is currently driving * the LLM. Sourced from the `PEAKS_OUTER_SESSION_ID` environment * variable when set, with `CLAUDE_CODE_SESSION_ID` as a fallback for * Claude Code. Stamped onto the presence file so the status line * can tell whether the recorded skill belongs to the live outer * session (show it) or a previous one (render idle), and so * `setSkillPresence` can detect a session swap and AskUserQuestion * the user about rolling a new peaks session. */ outerSessionId?: string; /** * Set by `setSkillPresence` when the outer session id changed * between the last presence write and this one AND the bound * peaks session has a different (or no) recorded outer session id. * * informational only — it tells the statusline and any log / * observability consumer that an outer-session swap was observed * on the previous heartbeat. The actual binding rotation is * field and turn it into an AskUserQuestion; that ask is no * longer needed because the rotation already happened by the time * the skill is invoked. */ outerSessionMismatch?: { previous?: string; current: string; boundSessionId: string; boundOuterSessionId?: string; }; setAt: string; lastHeartbeat?: string; }; /** * Resolve the active peaks session id. * * Resolution order (slice caller-first-session-resolution): * 1. The per-caller binding (via `getSessionId`) — the SAME source * `peaks session info --active` resolves through. * 2. The project-global `.peaks/_runtime/session.json` (legacy: * `.peaks/.session.json`) — unchanged back-compat fallback. * 3. `null` when neither is present. * * The caller-first step is what keeps two IDE windows on one project * apart. The project-global file is last-writer-wins, so the window * that initialised most recently owned it for every reader; commands * that resolved through this helper (job / dispatch / worktree / * share / ...) then wrote into THAT window's session tree, silently * cross-contaminating the two sessions. * * Returns `null` when no session is bound. Never throws. * * Public export: callers like `peaks sub-agent dispatch` use this * to auto-resolve `--session-id` when the LLM driver forgets to * pass the flag, so dispatch records land in the right * `.peaks/_sub_agents//` tree instead of an `unknown-sid` */ export declare function getCurrentSessionId(projectRootOverride?: string): string | null; /** * Write the LEGACY per-caller marker * `.peaks/_runtime//active-skill-.json` (D6). This path * was superseded by the sid-scoped lease; it stays for the audit enforcer, * which still reads those files. New code calls `setPresenceLease` instead. */ export declare function setSkillPresenceForCaller(projectRootOverride: string, callerId: string, peakSessionId: string, skill: string, mode?: string, gate?: string): SkillPresence; /** * The optional inputs of `setSkillPresence`. They travel as ONE object so * the function stays inside the repository's 4-parameter cap: the caller id * override is the escape hatch a non-Claude-Code host uses, and the project * root override is what the CLI passes so the read and the write agree on * which project they are talking about. */ export interface SetSkillPresenceOptions { readonly projectRootOverride?: string | undefined; readonly callerIdOverride?: string | undefined; } /** * Set the presence for the current skill. * * ASYNC, AND AWAITED. The canonical write — the graph the lease names, the * lease itself, the caller index — is awaited here, so a caller that awaits * this function KNOWS whether the presence landed. It used to run inside a * non-awaited `void (async () => { ... catch {} })()`: a graph that could * not be built, or a lease that could not be written, was swallowed and the * caller was handed a success-shaped envelope anyway. That failure now * ESCAPES, and the CLI turns it into a fail-closed envelope. * * The compat shim (slice 4.0.8) still holds: * 1. the canonical session id is resolved (caller binding first, then the * project-global `.peaks/_runtime/session.json`); * 2. the caller id the caller already resolved wins (`--caller-id` > * `PEAKS_CALLER_ID` > active IDE adapter — the chain lives in * `resolve-caller-id.ts`); when none is supplied the write resolves one * itself and fails closed when that is impossible; * 3. the canonical write is delegated to `setPresenceLease`, AWAITED. */ export declare function setSkillPresence(skill: string, mode?: string, gate?: string, options?: SetSkillPresenceOptions): Promise; export declare function getSkillPresence(projectRootOverride?: string): SkillPresence | null; /** * * Compare the *outer* session id stamped onto the current presence * marker against the *current* outer session id (from * `PEAKS_OUTER_SESSION_ID` / `CLAUDE_CODE_SESSION_ID`). * * Stale reasons (in priority order): * * - `'outer-session-mismatch'` — the presence was stamped by a * different outer (Claude / harness) session than the one that * is now driving peaks. Most common cause: the LLM closed the * previous Claude session, the next session boots and finds a * presence leftover from the old one. peaks-code Step 1 must * AskUserQuestion to confirm the user wants the old mode. * * - `'no-presence'` — there is no presence file on disk. * Not strictly "stale", but callers (peaks-code Step 1, `peaks * code should-pause --step step-1-mode-select`) want to treat * this case as "no opinion yet — must ask". Surfaced as * `stale: true` with reason `'no-presence'` so the ask path is * uniform. * * - `null` — the presence exists AND its outer * session id matches the current outer session id. Not stale. * Caller may safely reuse the recorded mode. * * Pure read-only: never deletes the presence file (the rotation * auto-clear path is a separate concern; see * `clearStalePresenceOnRotation`). Test seam: `--current-outer` lets * tests inject a deterministic current outer id without touching env * vars. */ export type StaleReason = 'outer-session-mismatch' | 'no-presence'; export type StalenessCheck = { stale: boolean; reason: StaleReason | null; /** What was on disk; null when no presence was found. */ presence: SkillPresence | null; /** Current outer session id (env-driven). Undefined when no signal. */ currentOuterSessionId: string | undefined; /** Outer session id recorded on the presence marker. Undefined when no signal. */ recordedOuterSessionId: string | undefined; }; export declare function checkStalePresence(opts?: { projectRootOverride?: string | undefined; /** * Override the *current* outer session id. Test seam only. * - `undefined` (omitted): read from `PEAKS_OUTER_SESSION_ID` / * `CLAUDE_CODE_SESSION_ID` env vars. * - explicit `string`: use the value verbatim (empty string means * "no signal", same as a missing env var). */ currentOuter?: string | undefined; }): StalenessCheck; /** * * Called from `peaks workspace init` immediately after a successful * `outer-session-mismatch` rotation. When the previous session's * presence is still on disk under the OLD outer session id, * clearing it prevents peaks-code Step 1 from picking up a stale * `mode` field that the user never explicitly chose. * * intent-source guard at the top of the function. The user constraint * (preserved in `.peaks/memory/2026-08-11-peaks-code-skill-persistence-pause.md`) * requires that "the current bee (peaks-code) can only be replaced or * cleared when the user explicitly asks, or when the user invokes * another non-bee Peaks skill; superpowers auto-hits, plain bug * reports, compact, and outer-session rotations must NOT change the * bee." Per the constraint, the `currentOuterSessionId` argument * doubles as the caller-binding intent source: when the harness env * var is set (`PEAKS_OUTER_SESSION_ID` or `CLAUDE_CODE_SESSION_ID`), * the call was user-explicit and the existing 3-branch logic is * authoritative; when the env var is absent (currentOuterSessionId * is `undefined`), the rotation is system-triggered and MUST NOT * mutate user-explicit presence state. * * The guard is the minimal surface that satisfies the user constraint * while preserving the existing reconnect + live-different-outer + * stale-clear behavior for the user-explicit path. No new arguments, * no signature change — the 2 existing call sites * (`init-command.ts:329-334`, `primer-command.ts:128-133`) keep * working unchanged. * * Only clears when: * - the caller supplies a user-explicit intent source * (`currentOuterSessionId !== undefined`), AND * - the recorded outer session id does NOT match the current outer * session id (i.e. the presence IS stale), AND * - the recorded outer session id matches the just-rotated-out * session id (i.e. we're clearing exactly the old binding, not a * user-explicitly-set mode from another live outer session). * * Returns `{ cleared, reason }` so callers can surface the outcome * in the JSON envelope and audit log. Pure: no env-var reads, all * inputs are passed explicitly so the call site (init command) can * decide what the "rotated-out" id was. */ export declare function clearStalePresenceOnRotation(opts: { projectRootOverride?: string; currentOuterSessionId: string | undefined; rotatedOutSessionId: string | null; }): { cleared: boolean; reason: string | null; recordedOuter?: string; }; export declare function touchSkillHeartbeat(projectRootOverride?: string): SkillPresence | null; export declare function clearSkillPresence(projectRootOverride?: string): boolean;