import { type ClaudeProcessFactory } from "./claude-process.js"; import { SessionStore } from "./session-store.js"; import { type EventSink, type AnyEvent } from "./events.js"; import { type AssembledContext } from "./spawn-assembler.js"; import { type CheckAgentBudgetResult } from "./agent-budget.js"; import type { ChiefSource } from "../messenger/base.js"; import { type ChiefStageEvent } from "../util/chief-stage-events.js"; /** * v0.3.0 — Chief session driver (renamed from pm-runner in v1.1). * * Wraps the long-lived Claude Code session that talks to the user. Per * v1.1 PRD §5 (Chief Sub-System), this driver hosts the Chief — the * org-level supervisor that decomposes the user's request and delegates to * the 25 specialists via Claude Code's native Task tool. (Historically the * role shipped as "PM mode" in v0.3 and was rebranded Chief in v1.1.) * * Event names were `pm.*` through v1.2.9 and are renamed `chief.*` in * v1.2.10 to match the rebrand. archive.sqlite never indexed these kinds; * the only on-disk reader (`workflow-reconciler`) accepts both the legacy * `pm.*` and the new `chief.*` forms, so pre-v1.2.10 logs stay readable. * * The flow (per docs/plan/v0.3-pm-mode-orchestration.md §4.2): * * handleUserMessage(call) * 1. Acquire session-id mutex (per PoC #1 §1.5 — concurrent --resume * creates interleaved jsonl that garbles future resumes) * 2. SessionStore.ensure -> sessionId + fresh? flag * 3. Emit chief.message_in * 4. claude.invokeStreaming({ sessionId, resume: !fresh, ... }) * 5. Loop stream-json lines: * - assistant text -> accumulate, forward to messenger * - task_started -> spawn.start event * - task_notification -> spawn.complete (status=completed) * or spawn.fail (status=failed) * - rate_limit_event !=allowed -> chief.rate_limit * - result -> final cost/text capture * 6. Exit/stderr branch: * - "Not logged in" -> AuthExpiredError * - "No conversation found" -> rotate session-id, retry once * - else exit!=0 -> chief.error * 7. Emit chief.message_out * 8. SessionStore.recordTurn (cost accumulate) * 9. Release mutex */ export interface ChiefRunnerDeps { claude: ClaudeProcessFactory; sessions: SessionStore; events: (orgSlug: string, userId: string) => EventSink; maxBudgetUsd?: number; timeoutMs?: number; } export interface ChiefCall { userId: string; orgSlug: string; orgCwd: string; userText: string; /** * v1.2.9 §D — surface this turn came in on. Omitted ⇒ defaults to a * messenger surface in the prompt hint (back-compat with callers that * predate the field). The CLI chat command passes `"cli"`; messenger * adapters pass their `platform` value. */ source?: ChiefSource; /** * v1.3.0 Part C (P0) — live stage callback. Fired synchronously each time * the runner emits a 6+1 stage event (TRIAGE → … → RETROSPECT) *during* the * turn, so the messenger dispatcher can stream progress into the works card * as it happens rather than after the turn returns. Optional and back-compat: * CLI/Slack callers that omit it get the unchanged batch behaviour. The * callback MUST NOT block — it runs inside the runner's stream loop. */ onStage?: (event: ChiefStageEvent) => void; } /** * v1.2 §6.2 — TRIAGE classifier output. Chief is instructed (per * `agents/main/chief/SKILL.md`) to emit `[kind:]` as the first * line of every reply. The runner strips the marker and exposes the * parsed value so messenger adapters can route accordingly. * * `chat` (default) → command channel flat reply. * `workflow` / `cron` / `goal` → works-handle task card + thread. */ export type ChiefKind = "chat" | "workflow" | "cron" | "goal"; export interface ChiefReply { text: string; /** v1.2 §6.2 — parsed from `[kind:...]` marker; defaults to "chat". */ kind: ChiefKind; /** * v1.2 §8 — correlation id for the turn. Used by messenger adapters * to fetch matching entries from `/memory/chief-stage-events.jsonl` * for thread narration (DISPATCH / AWAIT / skills_used). */ turnId: string; costUsd: number; durationMs: number; sessionRotated: boolean; /** * v1.4.0 — true when this turn started a NEW Chief session (brand-new, or a * fresh start after `chief reset` / mid-turn rotation). The messenger shows a * "🆕 세션 시작" marker before the Chief name on this reply. */ newSession: boolean; rateLimited: boolean; /** * v1.4.2 — the Claude Code rate-limit status reported this turn, if any. * `warning` = approaching the limit (announced once per reset window), * `exceeded` = actually limited. The messenger de-dupes the notice off this. */ rateLimit?: { status: "warning" | "exceeded"; resetsAt?: number; }; spawnCount: number; /** * v1.2.9 §D — true when this turn was aborted by the user via `/cancel`. * The messenger dispatcher suppresses the (partial) reply in this case — * the cancel handler already told the user it stopped. */ aborted?: boolean; } /** * Extract the kind marker from a reply, returning the parsed kind plus * the reply text with the marker stripped. When no marker is present, * the text is returned unchanged and kind is null (caller falls back to * `classifyByUserText`). */ export declare function parseKindMarker(reply: string): { kind: ChiefKind | null; text: string; }; export declare class AuthExpiredError extends Error { constructor(); } /** * Per-session-id mutex. Serializes concurrent invocations on the same key so * the underlying jsonl transcript stays coherent. Queue depth cap so a runaway * user can't pile up requests indefinitely. */ export declare class SessionMutex { private readonly maxQueueDepth; private locks; private queued; constructor(maxQueueDepth?: number); acquire(key: string, fn: () => Promise): Promise; } export declare class ChiefRunner { private readonly deps; private readonly mutex; /** * v1.2.9 §D — in-flight turns keyed by `${orgSlug}:${userId}`, so a * `/cancel` from the same user can abort the spawned claude process. */ private readonly inflight; constructor(deps: ChiefRunnerDeps); handleUserMessage(call: ChiefCall): Promise; /** * v1.2.9 §D — abort the in-flight turn for (orgSlug, userId), if any. * Kills the spawned claude process via the stream's abort handle and marks * the turn cancelled so its partial reply gets suppressed downstream. * Returns true when a turn was actually in flight. Mutex-independent: the * cancel must NOT queue behind the turn it cancels. */ cancelTurn(orgSlug: string, userId: string): boolean; resetSession(orgSlug: string, userId: string, reason?: string): Promise<{ previous: string | null; next: string; }>; private runTurn; private invokeWithSessionRecovery; private processLine; } /** * v1.4.0 (S-2a) — normalized token usage for one Chief turn, parsed from the * stream-json `result` line's `usage` block. `contextTokens` (input + cache_read * + cache_creation) approximates the prompt size that went into the model, i.e. * a proxy for context-window occupancy. Observation only — no rotation here. */ export interface TurnUsage { inputTokens: number; outputTokens: number; cacheReadTokens: number; cacheCreationTokens: number; contextTokens: number; } export declare function ifEvent(events: AnyEvent[], kind: K): Array>; export interface SpawnPreflightInput { workspace: string; orgSlug: string; agentRef: { team: string; name: string; }; repoSlug?: string; workflowId?: string; /** User-facing text / task description — drives keyword selection. */ query?: string; } export interface SpawnPreflightResult { budget: CheckAgentBudgetResult; context: AssembledContext; /** True when budget refuses the spawn (action=pause + exceeded). */ refused: boolean; /** Korean-language user-facing message — empty when allowed. */ userMessage: string; } /** * Pre-flight check: load the agent profile once, then return both the budget * verdict and the assembled 8-layer context. Cheap to call (single yaml load * + a directory walk). */ export declare function preflightSpawn(input: SpawnPreflightInput): SpawnPreflightResult;