import type { Database } from "bun:sqlite"; import type { EventSource, JsonObject, Workspace, WorkspaceIntegrations } from "../types/workspace.js"; /** * Project -> conversations channel linkage. * * Every project has exactly one conversations channel (fleet comms protocol). * The channel name is stored on the project record as * `integrations.conversations_channel`. * * ## The channel name is the project name * * When the integration is unset the channel name is the project slug, * normalized — nothing more. This CLI does not own the fleet channel naming * convention and must not carry a copy of it: rewriting the slug here can only * drift from, contradict, or double-apply the standard. It previously did * exactly that — a `project`-kind slug fell through to a hardcoded `internal-` * prefix, so `iproj-agent-ceo` derived `internal-iproj-agent-ceo`. * * This is emphatically NOT a claim that every registry slug already conforms. * Many do not (a large share of `project`-kind slugs lack `iproj-`, and most * `open-source` slugs are not the flat repo name the convention asks for). The * claim is narrower and is about authority, not conformance: the slug is the * only name this CLI is entitled to use. Where a slug is non-conforming the * repair belongs in the project rename/lifecycle path, not in a prefix table * here — renaming a project is a decision with a channel migration attached, * and silently papering over it at derivation time is what produced the * double-prefixed names in the first place. * * Projects whose channel does not match their slug say so explicitly by * setting `integrations.conversations_channel`; that link always wins over * derivation. * * ## Ensure never writes the link * * {@link ensureProjectChannel} makes the channel exist; it does not pin its * name onto the project record. Writing a *derived* name back would convert * this package's current opinion into an explicit link, and because an explicit * link outranks derivation forever, that write is one-way: it would survive a * revert of the very change that produced it, silently repointing a project * away from the channel holding its history. The link is established once, at * project creation, or deliberately by an operator. * * ## Channel class * * The class is a property of the project, not of the channel string, so it is * read from the project record — never guessed from the channel name's * prefix. See {@link resolveProjectChannelClass}. */ /** * Channel class vocabulary, per the fleet channel naming + classes convention * (`knowledge get hasna-channel-naming-convention`). `fleet` and `personal` * are deliberately absent: they are not project channels and this CLI never * creates one. */ export declare const PROJECT_CHANNEL_CLASSES: readonly ["package", "product", "work-project", "initiative", "loop-lane"]; export type ProjectChannelClass = (typeof PROJECT_CHANNEL_CLASSES)[number]; export declare const PROJECT_CHANNEL_INTEGRATION_KEY = "conversations_channel"; /** * Optional per-project override for the channel class. Set it on the project * record when a project's class does not follow from its kind; it takes * precedence over {@link WORKSPACE_KIND_CHANNEL_CLASSES}. */ export declare const PROJECT_CHANNEL_CLASS_INTEGRATION_KEY = "conversations_channel_class"; export interface ProjectChannelDerivation { channel: string; /** `null` when the project's kind does not imply a class — see {@link WORKSPACE_KIND_CHANNEL_CLASSES}. */ channel_class: ProjectChannelClass | null; source: "integration" | "derived"; } export interface ProjectChannelResolution extends ProjectChannelDerivation { project: Pick; linked: boolean; integration_key: typeof PROJECT_CHANNEL_INTEGRATION_KEY; /** * Non-fatal problems, e.g. an unusable `conversations_channel_class`. The * read path carries these too: it is the path agents actually use, so a * typo'd override must not be silent just because nothing is being created. */ warnings: string[]; } export interface ConversationsRunResult { ok: boolean; stdout: string; stderr: string; } export type ConversationsChannelRunner = (args: string[]) => ConversationsRunResult; export type ProjectAgentOnlineNotificationStatus = "sent" | "planned" | "skipped" | "error"; export interface ProjectAgentOnlineNotificationResult { status: ProjectAgentOnlineNotificationStatus; enabled: boolean; sent: boolean; channel: string | null; from: string; agent_tool: string; session_name: string; message: string; reason?: string; } export interface NotifyProjectAgentOnlineOptions { agentTool: string; sessionName: string; /** Whether this start created the session that launched the managed agent command. */ agentStarted: boolean; /** Whether this start has a managed coding-agent command at all. */ hasAgentCommand: boolean; enabled?: boolean; dryRun?: boolean; from?: string; runner?: ConversationsChannelRunner; } export interface EnsureProjectChannelOptions { db?: Database; agentId?: string; source?: EventSource; command?: string; /** Conversations identity recorded as channel creator. */ from?: string; dryRun?: boolean; runner?: ConversationsChannelRunner; } /** * What actually landed during an ensure run. Ensure touches three independent * systems (conversations channel, project integration link, audit event), so a * single boolean cannot describe the outcome: callers need to know which side * effects were committed before deciding whether (and how) to retry. */ export interface ProjectChannelSideEffects { /** The conversations channel was created by this run. */ channel_created: boolean; /** The conversations channel exists now (created by this run or already there). */ channel_present: boolean; /** * The project record carries an explicit `integrations.conversations_channel`. * Ensure never sets this — it reports what the record already held. */ integration_linked: boolean; /** The `channel_ensured` audit event was recorded on the project. */ event_recorded: boolean; } export interface ProjectChannelEnsureResult extends ProjectChannelDerivation { status: "created" | "exists" | "planned" | "error"; created: boolean; /** The project record carries an explicit `integrations.conversations_channel`. */ linked: boolean; message?: string; /** * Non-fatal problems (e.g. the audit event could not be recorded). Present * even on success; they never change `status`. */ warnings: string[]; side_effects: ProjectChannelSideEffects; project: Workspace; } export declare function normalizeProjectChannelName(value: string): string; /** * The project's channel class: an explicit `conversations_channel_class` * integration if the project carries one, otherwise whatever its kind implies, * otherwise `null` (unknown — assert nothing). * * Note this never inspects the channel *name*. Classifying by name prefix is * what made a correctly named `iproj-*` channel report `package`, since the * prefix table it consulted had no `iproj-` row. */ export declare function resolveProjectChannelClass(project: Pick & { integrations?: WorkspaceIntegrations; }): ProjectChannelClass | null; /** * As {@link resolveProjectChannelClass}, but also reports an unusable explicit * override. This override is the answer to "the class is configurable without a * code change", so a typo in it silently reverting to the kind default is * exactly the failure that wastes an afternoon — callers surface `warning`. */ export declare function resolveProjectChannelClassDetailed(project: Pick & { integrations?: WorkspaceIntegrations; }): { channel_class: ProjectChannelClass | null; warning?: string; }; export declare function deriveProjectChannel(project: Pick & { integrations?: WorkspaceIntegrations; }): ProjectChannelDerivation; /** * The channel to show an agent, and whether it is pinned or derived. * * Bundle and display surfaces must never render a blank channel just because * the project has no explicit link: since ensure stopped writing the link, the * overwhelming majority of projects resolve their channel by derivation, and a * surface that tells an agent where to post would otherwise say `null`. The * `source` sibling keeps that honest — a derived name is a current opinion, not * a commitment, and callers can say so. * * Never throws: an underivable slug yields a `null` channel rather than taking * down `projects show` or a handoff bundle. */ export declare function projectChannelSummary(project: Pick & { integrations?: WorkspaceIntegrations; }): { channel: string | null; source: ProjectChannelDerivation["source"] | null; }; export declare function resolveProjectChannelForProject(project: Workspace): ProjectChannelResolution; export declare function resolveProjectChannel(target: string | undefined, options?: { cwd?: string; db?: Database; }): ProjectChannelResolution; /** * Channel ensure runs by default outside of tests; opt out with * PROJECTS_CHANNEL_ENSURE=0 (or force on in tests with PROJECTS_CHANNEL_ENSURE=1). */ export declare function shouldEnsureProjectChannel(env?: Record): boolean; /** * Online announcements are enabled by default. Set * PROJECTS_AGENT_ONLINE_NOTIFICATIONS=0 to opt out. */ export declare function shouldNotifyProjectAgentOnline(env?: Record): boolean; export declare const CONVERSATIONS_CLI_TIMEOUT_MS = 15000; export declare function conversationsCliRunner(binary?: string): ConversationsChannelRunner; /** * Post a project-scoped chat announcement when a start actually brings a * coding agent online. This is best-effort: chat failures are returned to the * caller and never turn a successful project start into a failure. */ export declare function notifyProjectAgentOnline(project: Workspace, options: NotifyProjectAgentOnlineOptions): ProjectAgentOnlineNotificationResult; /** * Ensure the project's conversations channel exists. It does NOT link the * channel on the project record — see "Ensure never writes the link" above. * Failures (unreachable conversations CLI, underivable slug) never throw; they * are reported through `status: "error"` so project create/start keep working. */ export declare function ensureProjectChannel(project: Workspace, options?: EnsureProjectChannelOptions): ProjectChannelEnsureResult; /** * Minimal structural view of the projects Store used by the store-routed * ensure. `ProjectStore` (local + HTTP) is assignable to this. * * Ensure no longer writes the channel link, so this carries no `updateProject`: * the only thing routed through the Store is the audit event, which must land * wherever the project actually lives (the hosted backend) rather than in a * local sqlite file that does not contain the project. */ export interface ProjectChannelStore { readonly transport: "local" | "http"; recordEvent(idOrSlug: string, input: { event_type: string; source: EventSource; agentId?: string; command?: string; after?: JsonObject | null; }): Promise; } export interface StoreEnsureChannelOptions { agentId?: string; source?: EventSource; command?: string; /** Conversations identity recorded as channel creator. */ from?: string; dryRun?: boolean; runner?: ConversationsChannelRunner; } /** * Store-routed variant of {@link ensureProjectChannel}. The channel derivation * is pure and the conversations channel creation is a machine-local side effect * (the local `conversations` client itself routes to the shared hosted service); the * audit event goes through the Store so it lands wherever the project actually * lives. Nothing here writes the project record. Never throws for * conversations/derivation failures; reports them via `status: "error"`. */ export declare function ensureProjectChannelViaStore(store: ProjectChannelStore, project: Workspace, options?: StoreEnsureChannelOptions): Promise; //# sourceMappingURL=project-channel.d.ts.map