### Start Cairn Server Manually Source: https://github.com/oritera/cairn/blob/main/README.md Starts the Cairn server manually using the 'uv' command. This is part of the manual setup process. ```bash # Start the server uv run --project cairn cairn serve ``` -------------------------------- ### Create Dispatcher Configuration Source: https://github.com/oritera/cairn/blob/main/README.md Copies the example dispatcher configuration file to be customized with LLM endpoints and API keys. ```bash cp dispatch.example.yaml dispatch.yaml ``` -------------------------------- ### Start Cairn with Docker Compose Source: https://github.com/oritera/cairn/blob/main/README.md Starts the Cairn server and dispatcher using Docker Compose. The server runs on port 8000, and the dispatcher connects to Docker via the host socket. ```bash docker compose up --build ``` -------------------------------- ### Start Project Replay Source: https://github.com/oritera/cairn/blob/main/cairn/src/cairn/server/static/index.html Initiates a project replay by fetching project data and building replay frames. Initializes replay state and applies the first frame. ```typescript async startProjectReplay() { if (!this.project || this.replay.active || !this.selectedProjectId) return; try { const sourceProject = await this.api('GET', `/projects/${this.selectedProjectId}`); const baseEvents = this.timelineEvents().map(event => ({ ...event, meta: [...(event.meta || [])], sourceFactIds: [...(event.sourceFactIds || [])] })); const frames = this.buildReplayFrames(sourceProject, baseEvents); if (frames.length === 0) { this.showToast('No timeline to replay', 'error'); return; } const stepMs = String(this.replay.stepMs || '1100'); this.stopReplayTimer(); this.replay = { active: true, playing: true, stepMs, frameIndex: -1, frames, visibleEvents: [], sourceProject, timer: null, }; this.polling = false; this.sideTab = 'log'; this.selectedNode = null; this.selectedFacts = []; this.selectedTimelineEntryId = null; this.applyReplayFrame(0, { reinitialize: true }); this.scheduleReplayTick(); } catch (e) { this.showToast(e.message, 'error'); } } ``` -------------------------------- ### Dispatcher Project Directory Structure Source: https://github.com/oritera/cairn/blob/main/docs/specs/dispatcher-design.md An example directory structure for organizing dispatcher-related files, including models, prompting logic, and worker adapters. ```text dispatcher/ models.py prompting.py output_parser.py contracts.py prompts/ default/ bootstrap.md bootstrap_conclude.md reason.md explore.md explore_conclude.md mock/ bootstrap.md bootstrap_conclude.md reason.md explore.md explore_conclude.md workers/ base.py registry.py adapters/ claudecode.py codex.py mock.py ``` -------------------------------- ### Start Polling for Updates Source: https://github.com/oritera/cairn/blob/main/cairn/src/cairn/server/static/index.html Starts an interval timer to periodically poll for project updates. Loads projects or a specific project based on the current view and selection. ```javascript startPolling() { this.pollTimer = setInterval(async () => { if (!this.polling) return; if (this.selectedProjectId && this.view === 'graph') { await this.loadProject(this.selectedProjectId); this.updateGraph(); } else { await this.loadProjects(); } }, 5000); } ``` -------------------------------- ### Start Panel Resize Source: https://github.com/oritera/cairn/blob/main/cairn/src/cairn/server/static/index.html Initiates the panel resizing process by capturing the pointer event and setting a flag indicating that resizing is in progress. Calls onPanelResize immediately to update the width. ```javascript startPanelResize(e) { e.currentTarget?.setPointerCapture?.(e.pointerId); this.isResizingPanel = true; this.onPanelResize(e); } ``` -------------------------------- ### Setup Auto Fit for Graph Resizing Source: https://github.com/oritera/cairn/blob/main/cairn/src/cairn/server/static/index.html Sets up a ResizeObserver to automatically adjust the graph layout when the container size changes. It includes debouncing to avoid excessive refits and ensures the graph fits within the viewport. ```javascript setupAutoFit() { this.teardownAutoFit(); const container = document.getElementById('cy'); if (!container || !this.cy) return; let fitTimer = null; this._resizeObserver = new ResizeObserver(() => { clearTimeout(fitTimer); fitTimer = setTimeout(() => { if (this.cy) { const clampedWidth = this.clampPanelWidth(this.sidePanelWidth); if (clampedWidth !== this.sidePanelWidth) { this.sidePanelWidth = clampedWidth; this.saveSidePanelWidth(); } this.cy.resize(); this.cy.fit(undefined, 50); } }, 200); }); this._resizeObserver.observe(container); } ``` -------------------------------- ### Run Cairn Dispatcher Manually Source: https://github.com/oritera/cairn/blob/main/README.md Runs the Cairn dispatcher manually using the 'uv' command, specifying the configuration file. This is part of the manual setup process. ```bash # Run the dispatcher uv run --project cairn cairn dispatch --config dispatch.yaml ``` -------------------------------- ### Get Selected Timeline Summary Source: https://github.com/oritera/cairn/blob/main/cairn/src/cairn/server/static/index.html Calculates and returns summary statistics for the selected timeline entry, including sequence and time percentages. ```javascript selectedTimelineSummary() { const targetEntryId = this.activeTimelineEntryId(false); if (!targetEntryId) return null; const events = this.timelineEvents(); const index = events.findIndex(entry => entry.id === targetEntryId); if (index < 0) return null; const entry = events[index]; const total = events.length; const sequencePercent = total <= 1 ? 100 : Math.round((index / (total - 1)) * 100); const replayMode = this.replay.active; const totalDuration = replayMode ? this.replayTimelineElapsedDurationMs(events, Math.max(0, total - 1)) : Math.max(0, Date.parse(events[total - 1].timestamp) - Date.parse(events[0].timestamp)); const elapsedDuration = replayMode ? this.replayTimelineElapsedDurationMs(events, index) : Math.max(0, Date.parse(entry.timestamp) - Date.parse(events[0].timestamp)); const timePercent = totalDuration === 0 ? 100 : Math.round((elapsedDuration / totalDuration) * 100); return { sequencePercent, sequenceLabel: `${index + 1} / ${total} · ${sequencePercent}%`, timePercent, timeLabel: `${this.formatDurationMs(elapsedDuration)} / ${this.formatDurationMs(totalDuration)} · ${timePercent}%`, }; } ``` -------------------------------- ### Get Replay Progress Label Source: https://github.com/oritera/cairn/blob/main/cairn/src/cairn/server/static/index.html Generates a label indicating the current progress of a replay, showing the current frame and total frames. ```javascript replayProgressLabel() { if (!this.replay.active || this.replay.frames.length === 0) return 'Replay'; return `Replay ${Math.min(this.replay.frameIndex + 1, this.replay.frames.length)} / ${this.replay.frames.length}`; } ``` -------------------------------- ### Pull Base Docker Image Source: https://github.com/oritera/cairn/blob/main/README.md Pulls the base Docker image used for building Cairn. This is part of the Docker Compose setup. ```bash docker pull ghcr.io/astral-sh/uv:python3.13-trixie ``` -------------------------------- ### Get Producing Intent Source: https://github.com/oritera/cairn/blob/main/cairn/src/cairn/server/static/index.html Retrieves the intent that produces a given fact. Returns null if the project is not available or no such intent exists. ```javascript getProducingIntent(fid) { return this.project ? this.project.intents.find(i => i.to === fid) || null : null; } ``` -------------------------------- ### Get Fact Record Source: https://github.com/oritera/cairn/blob/main/cairn/src/cairn/server/static/index.html Retrieves a specific fact record by its ID. Returns null if the project is unavailable or the fact is not found. ```javascript getFactRecord(fid) { return this.project ? this.project.facts.find(f => f.id === fid) || null : null; } ``` -------------------------------- ### Get YAML Section Tint Colors Source: https://github.com/oritera/cairn/blob/main/cairn/src/cairn/server/static/index.html Returns predefined color tints for different sections of a YAML configuration (project, hints, facts, intents). Used for styling UI elements associated with these sections. ```javascript yamlSectionTint(sectionName) { const tints = { project: { header: '#eff6ff', body: '#fafcff', itemA: '#f3f8ff', itemB: '#edf5ff' }, hints: { header: '#fffbeb', body: '#fffef8', itemA: '#fffaf0', itemB: '#fff6e8' }, facts: { header: '#eef2ff', body: '#fafaff', itemA: '#f5f7ff', itemB: '#eef3ff' }, intents: { header: '#ecfdf5', body: '#f8fdfb', itemA: '#f1fbf5', itemB: '#eaf8ef' }, }; return tints[sectionName] || { header: '#f8fafc', body: '#ffffff', it ``` -------------------------------- ### Selected Open Intent Record Source: https://github.com/oritera/cairn/blob/main/cairn/src/cairn/server/static/index.html Gets the record for the selected intent if it's an open intent (not concluded and project is active). Returns null otherwise. ```javascript selectedOpenIntentRecord() { const intent = this.selectedIntentRecord(); if (!intent || intent.to || !this.projectIsActive()) return null; return intent; } ``` -------------------------------- ### Create a New Project Source: https://github.com/oritera/cairn/blob/main/cairn/src/cairn/server/static/index.html Creates a new project with specified details, including title, origin, goal, and optional hints. It then reloads the project list and opens the newly created project. ```javascript async createProject() { try { const actor = this.actorName(); const hintContents = this.newProject.hints.filter(h => h.content?.trim()); const body = { title: this.newProject.title, origin: this.newProject.origin, goal: this.newProject.goal, bootstrap_enabled: this.newProject.bootstrap }; const hints = hintContents.map(h => ({ content: h.content.trim(), creator: actor })); if (hints.length > 0) body.hints = hints; const data = await this.api('POST', '/projects', body); this.showNewProject = false; this.newProject = { title:'', origin:'', goal:'', bootstrap: true, hints: [{ content:'' }] }; await this.loadProjects(); await this.openProject(data.project.id); this.showToast('Project created'); } catch(e) { this.showToast(e.message, 'error'); } } ``` -------------------------------- ### Get Latest Timeline Entry ID Source: https://github.com/oritera/cairn/blob/main/cairn/src/cairn/server/static/index.html Retrieves the ID of the most recent entry in the timeline. ```javascript latestTimelineEntryId() { const events = this.timelineEvents(); return events.length > 0 ? events[events.length - 1].id : null; } ``` -------------------------------- ### Get Timeline Entry Element Source: https://github.com/oritera/cairn/blob/main/cairn/src/cairn/server/static/index.html Retrieves the DOM element for a specific timeline entry using its ID. ```javascript timelineEntryElement(entryId) { return document.getElementById(this.timelineEntryDomId(entryId)); } ``` -------------------------------- ### Build Initial Replay Project State Source: https://github.com/oritera/cairn/blob/main/cairn/src/cairn/server/static/index.html Constructs the initial state for a project replay, including essential facts like 'origin' and 'goal'. Clones data to prevent mutation. ```javascript buildInitialReplayProject(sourceProject) { const origin = sourceProject.facts.find(fact => fact.id === 'origin'); const goal = sourceProject.facts.find(fact => fact.id === 'goal'); return { project: { ...this.cloneData(sourceProject.project), status: 'active', reason: null, }, facts: [origin, goal].filter(Boolean).map(fact => this.cloneData(fact)), intents: [], hints: [], }; } ``` -------------------------------- ### Open Reopen Project Modal Source: https://github.com/oritera/cairn/blob/main/cairn/src/cairn/server/static/index.html Initializes the form and shows the modal for reopening a project. ```javascript openReopenProject(projectId, projectTitle = '') { if (!projectId) return; this.reopenForm = { projectId, projectTitle, description: '' }; this.showReopenModal = true; } ``` -------------------------------- ### Open Project Source: https://github.com/oritera/cairn/blob/main/cairn/src/cairn/server/static/index.html Loads a project by its ID. If a replay is active, it's stopped and reset before loading the project. Sets the view to 'graph'. ```typescript async openProject(id) { if (this.replay.active) { this.stopReplayTimer(); this.replay.active = false; this.replay.playing = false; this.replay.frames = []; this.replay.visibleEvents = []; this.replay.frameIndex = -1; this.replay.sourceProject = null; this._timelineEventsCacheProject = null; this._timelineEventsCache = []; this.polling = true; } this.rememberProjectListScroll(); this.selectedProjectId = id; this.selectedNode = null; this.selectedFacts = []; this.selectedTimelineEntryId = null; await this.loadProject(id); this.view = 'graph'; if ``` -------------------------------- ### Get Active Timeline Entry ID Source: https://github.com/oritera/cairn/blob/main/cairn/src/cairn/server/static/index.html Returns the currently active timeline entry ID, considering graph selection if necessary. ```javascript activeTimelineEntryId(allowMultiFact = false) { return this.selectedTimelineEntryId || this.timelineTargetEntryIdForGraphSelection(allowMultiFact); } ``` -------------------------------- ### Initiate Project Deletion Source: https://github.com/oritera/cairn/blob/main/cairn/src/cairn/server/static/index.html Sets the selected project for deletion and displays the confirmation modal. ```javascript deleteProject() { if (!this.selectedProjectId) return; this.requestDeleteProject(this.selectedProjectId, this.project?.project?.title || ''); } ``` -------------------------------- ### Open Hint Creation Form Source: https://github.com/oritera/cairn/blob/main/cairn/src/cairn/server/static/index.html Opens the modal form for adding a hint to the project, ensuring the project allows hint writing. ```javascript openHintModal() { if (!this.projectCanWriteHints()) return; this.hintForm = { content:'' }; this.showHintModal = true; } ``` -------------------------------- ### Pull Worker Container Image Source: https://github.com/oritera/cairn/blob/main/README.md Pulls the necessary worker container image for Cairn. This is a prerequisite for both Docker Compose and manual setup methods. ```bash docker pull --platform=linux/amd64 ghcr.io/oritera/cairn-worker-container:latest ``` -------------------------------- ### Load Project Settings Source: https://github.com/oritera/cairn/blob/main/cairn/src/cairn/server/static/index.html Fetches project settings from the API and updates the local settings form. Handles potential errors during the API call. ```javascript async loadSettings() { try { const s = await this.api('GET', '/settings'); this.settingsForm.intent_timeout = s.intent_timeout; this.settingsForm.reason_timeout = s.reason_timeout; } catch(e) { console.error(e); } } ``` -------------------------------- ### Add a Hint to Project Source: https://github.com/oritera/cairn/blob/main/cairn/src/cairn/server/static/index.html Adds a new hint to the selected project with the provided content and creator. It then reloads projects, the current project, and switches to the hints tab. ```javascript async addHint() { if (!this.projectCanWriteHints()) return; try { const actor = this.actorName(); await this.api('POST', `/projects/${this.selectedProjectId}/hints`, { content: this.hintForm.content, creator: actor }); this.showHintModal = false; this.hintForm = { content:'' }; await this.loadProjects(); await this.loadProject(this.selectedProjectId); this.sideTab = 'hints'; this.showToast('Hint added'); } catch(e) { this.showToast(e.message, 'error'); } } ``` -------------------------------- ### Selected Fact Producing Intent Source: https://github.com/oritera/cairn/blob/main/cairn/src/cairn/server/static/index.html Gets the intent that produced the currently selected fact. Returns null if no fact is selected or the fact has no producing intent. ```javascript selectedFactProducingIntent() { const factId = this.selectedFactId(); return factId ? this.getProducingIntent(factId) : null; } ``` -------------------------------- ### Back to Project List Source: https://github.com/oritera/cairn/blob/main/cairn/src/cairn/server/static/index.html Resets the application state to display the project list. Handles cleanup of replay state, graph instance, and navigation. ```javascript backToList(fromRoute) { if (this.replay.active) { this.stopReplayTimer(); this.replay.active = false; this.replay.playing = false; this.replay.frames = []; this.replay.visibleEvents = []; this.replay.frameIndex = -1; this.replay.sourceProject = null; this._timelineEventsCacheProject = null; this._timelineEventsCache = []; this.polling = true; } this.view = 'list'; this.shouldRestoreProjectListScroll = true; this.project = null; this.selectedProjectId = ''; this.selectedNode = null; this.selectedFacts = []; this.selectedTimelineEntryId = null; this.teardownAutoFit(); if (this.cy) { this.cy.destroy(); this.cy = null; } if (!fromRoute) location.hash = '/'; this.loadProjects().then(() => this.restoreProjectListScroll()); } ``` -------------------------------- ### Dispatcher Execution Flow Source: https://github.com/oritera/cairn/blob/main/docs/specs/dispatcher-design.md Illustrates the main steps involved in the Dispatcher's execution, from reading project data to worker output parsing and project completion. ```text 1. Dispatcher 从 Server 读取项目图 2. Dispatcher 依据调度规则选择任务类型和 Worker 3. Dispatcher 渲染 prompt 与命令占位符 4. 如果是 `explore`,Dispatcher 先通过 `POST /projects/{project_id}/intents/{intent_id}/heartbeat` 认领目标 intent;如果是 `reason`,则先通过 `POST /projects/{project_id}/reason/claim` 认领项目级 reason lease 5. Dispatcher 在项目容器内启动 Worker 进程 6. Worker 输出结构化 JSON 7. Dispatcher 解析结果,并调用 `POST /projects/{project_id}/complete`、`POST /projects/{project_id}/intents`、`POST /projects/{project_id}/intents/{intent_id}/conclude`、`POST /projects/{project_id}/intents/{intent_id}/release` 或 `POST /projects/{project_id}/reason/release` ``` -------------------------------- ### Complete Project After Bootstrap Source: https://github.com/oritera/cairn/blob/main/docs/specs/server-protocol.md Used when a newly discovered fact is sufficient to meet the project's goal after bootstrapping. This snippet shows how to mark the project as complete. ```json { "from": ["新写入的 fact id"], "description": "为什么该事实已足以满足 goal", "worker": "dispatcher-worker-A" } ``` -------------------------------- ### Open Rename Project Modal Source: https://github.com/oritera/cairn/blob/main/cairn/src/cairn/server/static/index.html Initializes the form and shows the modal for renaming a project. Focuses and selects the input field after the modal is shown. ```javascript openRenameProject(projectId, projectTitle = '') { if (!projectId) return; this.renameForm = { projectId, originalTitle: projectTitle, title: projectTitle }; this.showRenameModal = true; this.$nextTick(() => { this.$refs.renameTitleInput?.focus(); this.$refs.renameTitleInput?.select?.(); }); } ``` -------------------------------- ### Center Graph on Elements Source: https://github.com/oritera/cairn/blob/main/cairn/src/cairn/server/static/index.html Centers the graph viewport on a given set of elements using an animated transition. Stops any ongoing centering animation before starting a new one. ```javascript centerGraphOnElements(eles) { if (!this.cy || !eles || eles.length === 0) return; if (this._centerAnimation) this._centerAnimation.stop(); this._centerAnimation = this.cy.animation({ center: { eles }, duration: 220, easing: 'ease-in-out-cubic' }); this._centerAnimation.play(); } ``` -------------------------------- ### Create Bootstrap Intent Source: https://github.com/oritera/cairn/blob/main/docs/specs/server-protocol.md Used when a project is in its initial stage and lacks a bootstrap intent. This snippet shows how to create a new intent for bootstrapping. ```json { "from": ["origin"], "description": "bootstrap", "creator": "dispatcher.bootstrap", "worker": null } ``` -------------------------------- ### Build Replay Frames from Events Source: https://github.com/oritera/cairn/blob/main/cairn/src/cairn/server/static/index.html Generates a sequence of replay frames from a project's base events. It transforms and adds specific event types like 'reason_started' and 'intent_running' to simulate replay progression. Assumes `cloneData` and `applyReplayEvent` are defined. ```javascript buildReplayFrames(sourceProject, baseEvents) { const sourceIntents = new Map(sourceProject.intents.map(intent => [intent.id, intent])); const replayEvents = []; for (const event of baseEvents) { if (event.type !== 'intent_declared' || !event.intentId) { replayEvents.push(this.cloneData(event)); continue; } const sourceIntent = sourceIntents.get(event.intentId); replayEvents.push({ id: `reason-started-${event.intentId}`, type: 'reason_started', timestamp: sourceIntent?.created_at || event.timestamp, actor: sourceIntent?.creator || event.actor || 'reasoner', title: sourceIntent?.description || event.title, meta: [ sourceIntent?.id || event.intentId, `from ${(sourceIntent?.from || event.sourceFactIds || []).join(', ')}`, ], targetType: 'reason', targetId: sourceIntent?.id || event.intentId, order: `${event.order}.reason`, intentId: sourceIntent?.id || event.intentId, producedFactId: null, sourceFactIds: [...(sourceIntent?.from || event.sourceFactIds || [])], }); replayEvents.push(this.cloneData(event)); if (!sourceIntent?.worker) continue; replayEvents.push({ id: `intent-running-${sourceIntent.id}`, type: 'intent_running', timestamp: sourceIntent.last_heartbeat_at || sourceIntent.created_at, actor: sourceIntent.worker, title: sourceIntent.description, meta: [sourceIntent.id, `worker ${sourceIntent.worker}`], targetType: 'intent', targetId: sourceIntent.id, order: `${event.order}.run`, intentId: sourceIntent.id, producedFactId: null, sourceFactIds: [...sourceIntent.from], }); } const replayProject = this.buildInitialReplayProject(sourceProject); const frames = []; for (const event of replayEvents) { this.applyReplayEvent(replayProject, sourceProject, event); frames.push({ event: this.cloneData(event), project: this.cloneData(replayProject), }); } if (frames.length > 0) { frames[frames.length - 1].project.project.status = sourceProject.project.status; frames[frames.length - 1].project.project.reason = this.cloneData(sourceProject.project.reason); } return frames; } ``` -------------------------------- ### Format Project Label Source: https://github.com/oritera/cairn/blob/main/cairn/src/cairn/server/static/index.html Combines a project ID with its title for display, returning only the ID if the title is not provided. ```javascript projectLabel(projectId, projectTitle) { return projectTitle ? `${projectId} - ${projectTitle}` : projectId; } ``` -------------------------------- ### Run Dispatcher with Startup Health Checks Only Source: https://github.com/oritera/cairn/blob/main/README.md Runs the Cairn dispatcher with only startup health checks enabled, useful for verifying the dispatcher's configuration without full operation. ```bash # Run startup health checks only uv run --project cairn cairn dispatch --config dispatch.yaml --startup-healthcheck-only ``` -------------------------------- ### Bootstrap Task API Mapping Source: https://github.com/oritera/cairn/blob/main/docs/specs/dispatcher-design.md Illustrates how standard intent APIs are mapped for bootstrap tasks, including creation, heartbeat, conclusion, and completion calls. ```text | 接口 | 用途 | 何时使用 | | --- | --- | --- | | `POST /projects/{project_id}/intents` | 创建保留 `bootstrap` intent | 初始态项目首次进入时,如尚不存在该 intent | | `POST /projects/{project_id}/intents/{intent_id}/heartbeat` | claim 并维持 `bootstrap` intent | 派发前先 claim;执行中按 `interval` 周期发送 | | `POST /projects/{project_id}/intents/{intent_id}/conclude` | 将 `bootstrap` 产出的关键结果写成 Fact | `bootstrap` 或 `bootstrap_conclude` 返回合法 `fact` 后调用 | | `POST /projects/{project_id}/complete` | 基于刚写入的 bootstrap fact 直接完成项目 | 仅当 `bootstrap` 主阶段成功返回 `fact + complete` 时调用 | | `POST /projects/{project_id}/intents/{intent_id}/release` | 放弃本次 bootstrap 尝试 | 两阶段都失败,或命令直接失败时调用 | ``` -------------------------------- ### Apply Replay Event to Project State Source: https://github.com/oritera/cairn/blob/main/cairn/src/cairn/server/static/index.html Applies a single replay event to the current project state during replay simulation. Handles various event types like 'project_created', 'hint_added', 'reason_started', 'intent_declared', 'intent_running', and 'intent_concluded'. Assumes `cloneData` is available. ```javascript applyReplayEvent(replayProject, sourceProject, event) { if (!replayProject || !sourceProject || !event) return; const sourceIntent = event.intentId ? sourceProject.intents.find(intent => intent.id === event.intentId) || null : null; if (event.type === 'project_created') { replayProject.project.title = sourceProject.project.title; replayProject.project.status = 'active'; return; } if (event.type === 'hint_added') { const hint = sourceProject.hints.find(item => item.id === event.targetId); if (hint && !replayProject.hints.some(item => item.id === hint.id)) { replayProject.hints.push(this.cloneData(hint)); } return; } if (event.type === 'reason_started') { replayProject.project.reason = { worker: event.actor || 'reasoner', trigger: 'new_facts', started_at: event.timestamp, last_heartbeat_at: event.timestamp, }; return; } if (event.type === 'intent_declared') { if (!sourceIntent) return; replayProject.project.reason = null; if (!replayProject.intents.some(intent => intent.id === sourceIntent.id)) { replayProject.intents.push({ id: sourceIntent.id, from: [...sourceIntent.from], to: null, description: sourceIntent.description, creator: sourceIntent.creator, worker: null, last_heartbeat_at: null, created_at: sourceIntent.created_at, concluded_at: null, }); } return; } if (event.type === 'intent_running') { if (!sourceIntent) return; const replayIntent = replayProject.intents.find(intent => intent.id === sourceIntent.id); if (!replayIntent) return; replayIntent.worker = sourceIntent.worker || sourceIntent.creator; replayIntent.last_heartbeat_at = sourceIntent.last_heartbeat_at || sourceIntent.created_at; return; } if (event.type !== 'intent_concluded' && event.type !== 'project_completed') return; if (!sourceIntent) return; const replayIntent = replayProject.intents.find(intent => intent.id === sourceIntent.id); if (replayIntent) { replayIntent.to = sourceIntent.to; replayIntent.worker = sourceIntent.worker || ``` -------------------------------- ### Select Intent Source: https://github.com/oritera/cairn/blob/main/cairn/src/cairn/server/static/index.html Selects an intent, clearing any existing fact selections and setting the selected node to the specified intent. It then applies lineage highlighting for that intent. ```javascript selectIntent(intent) { const intentId = typeof intent === 'string' ? intent : intent?.id; if (!intentId) return; this.selectedFacts = []; this.selectedTimelineEntryId = null; this.selectedNode = { type:'intent', id: intentId }; this.applyLineageHighlightForIntent(intentId); } ``` -------------------------------- ### Cairn Application State Management Source: https://github.com/oritera/cairn/blob/main/cairn/src/cairn/server/static/index.html Initializes the main application state object for Cairn. This includes UI states, project data, and configuration options. It also sets up event listeners for panel resizing and route changes. ```javascript function cairnApp() { return { view: 'list', projects: [], project: null, selectedProjectId: '', polling: true, pollTimer: null, cy: null, sideTab: 'detail', selectedNode: null, selectedFacts: [], selectedTimelineEntryId: null, layoutMode: 'dagre_tb', sidePanelWidth: 320, isResizingPanel: false, showNewProject: false, showIntentModal: false, showConcludeModal: false, showCompleteModal: false, showHintModal: false, showReopenModal: false, showRenameModal: false, showLocalPrefs: false, showSettings: false, showYamlModal: false, exportTab: 'yaml', exportProjectId: '', showDeleteModal: false, newProject: { title: '', origin: '', goal: '', bootstrap: true, hints: [{ content: '' }] }, intentForm: { description: '' }, concludeForm: { description: '', intentId: '' }, completeForm: { description: '' }, hintForm: { content: '' }, reopenForm: { projectId: '', projectTitle: '', description: '' }, renameForm: { projectId: '', originalTitle: '', title: '' }, localPrefs: { actor_name: 'Human', layout_mode: 'dagre_tb' }, settingsForm: { intent_timeout: 5, reason_timeout: 5 }, deleteConfirm: { id: '', title: '' }, isDeletingProject: false, yamlPreviewTitle: '', yamlPreviewText: '', yamlPreviewHtml: '', toast: { show: false, message: '', type: 'info' }, replay: { active: false, playing: false, stepMs: '1600', frameIndex: -1, frames: [], visibleEvents: [], sourceProject: null, timer: null, }, _timelineEventsCacheProject: null, _timelineEventsCache: [], _centerAnimation: null, projectListScrollTop: 0, shouldRestoreProjectListScroll: false, isStoppingAllProjects: false, async init() { this._panelResizeMove = (e) => this.onPanelResize(e); this._panelResizeStop = () => this.stopPanelResize(); window.addEventListener('pointermove', this._panelResizeMove); window.addEventListener('pointerup', this._panelResizeStop); this.loadLocalPrefs(); await this.loadProjects(); await this.loadSettings(); this.startPolling(); window.addEventListener('hashchange', () => this.handleRoute()); this.handleRoute(); }, handleRoute() { const hash = location.hash || '#/'; const m = hash.match(/^#\/projects\/(.+)$/); if (m) { const id = m[1]; if (this.selectedProjectId !== id || this.view !== 'graph') { this.openProject(id); } } else { if (this.view !== 'list') this.backToList(true); } }, async api(method, path, body) { const opts = { method, headers: { 'Content-Type': 'application/json' } }; if (body) opts.body = JSON.stringify(body); const r = await fetch(path, opts); if (r.status === 204) return null; const data = await r.json(); if (!r.ok) { let msg = `HTTP ${r.status}`; if (typeof data.detail === 'string') msg = data.detail; else if (Array.isArray(data.detail)) msg = data.detail.map(e => e.msg).join('; '); throw new Error(msg); } return data; }, async fetchText(path) { const r = await fetch(path); const text = await r.text(); if (!r.ok) { let detail = text; try { const data = JSON.parse(text); detail = data.detail || text; } catch {} throw new Error(detail || `HTTP ${r.status}`); } return text; }, showToast(msg, type = 'info') { this.toast = { show: true, message: msg, type }; setTimeout(() => this.toast.show = false, 3000); }, cloneData(value) { try { return JSON.parse(JSON.stringify(value)); } catch (error) { if (typeof structuredClone === 'function') return structuredClone(value); throw error; } }, isValidLayoutMode(mode) { return ['dagre_tb', 'dagre_lr', 'klay_tb', 'klay_lr', 'elk_tb', 'elk_lr'].includes(mode); }, layoutEngine(mode = this.layoutMode) { if (mode.startsWith('elk')) return 'elk'; return mode.startsWith('klay') ? 'klay' : 'dagre'; }, layoutDirection(mode = this.layoutMode) { return mode.endsWith('lr') ? 'LR' : 'TB'; }, loadLocalPrefs() { try { const raw = localStorage.getItem('cairn.localPrefs'); if (!raw) { this.localPrefs.actor_name = 'Human'; } else { const parsed = JSON.parse(raw); if (typeof parsed.actor_name === 'string') this.localPrefs.actor_name = parsed.actor_name; if (this.isValidLayoutMode(parsed.layout_mode)) { this.localPrefs.layout_mode = parsed.layout_mode; } else if (parsed.layout_dir === 'TB' || parsed.layout_dir === 'LR') { this.localPrefs.layout_mode = parsed.layout_dir === 'LR' ? 'dagre_lr' : 'dagre_tb'; } } const rawPanelWidth = localStorage.getItem('cairn.sidePanelWidth'); if (rawPanelWidth !== null) { const savedPanelWidth = Number(rawPanelWidth); if (Number.isFinite(savedPanelWidth)) this.sidePanelWidth = savedPanelWidth; } } catch (e) { console.error(e); } if (!this.localPrefs.actor_name.trim()) this.localPrefs.actor_name = 'Human'; if (!this.isValidLayoutMode(this.localPrefs.layout_mode)) this.localPrefs.layout_mode = 'dagre_tb'; this.layoutMode = this.localPrefs.layout_mode; }, saveLocalPrefs() { try { localStorage.setItem('cairn.localPrefs', JSON.stringify(this.localPrefs)); } catch (e) { console.error(e); } }, saveSidePanelWidth() { try { localStorage.se ``` -------------------------------- ### View Project YAML Export Source: https://github.com/oritera/cairn/blob/main/cairn/src/cairn/server/static/index.html Opens a modal to display the YAML export of a specific project. It sets the export tab to 'yaml' and loads the content. ```javascript async viewProjectYaml(projectId, projectTitle = '') { if (!projectId) return; this.exportProjectId = projectId; this.yamlPreviewTitle = this.projectLabel(projectId, projectTitle); this.exportTab = 'yaml'; await this.loadExportTab('yaml'); this.showYamlModal = true; } ``` -------------------------------- ### Load Specific Project Source: https://github.com/oritera/cairn/blob/main/cairn/src/cairn/server/static/index.html Retrieves details for a specific project by its ID using the API. Displays an error toast if the project cannot be loaded. ```javascript async loadProject(id) { try { this.project = await this.api('GET', `/projects/${id}`); } catch(e) { this.showToast(e.message, 'error'); } } ``` -------------------------------- ### Reopen Project Source: https://github.com/oritera/cairn/blob/main/cairn/src/cairn/server/static/index.html Reopens a project using the API. Handles form data and shows a success or error toast. ```typescript async reopenProject() { const projectId = this.reopenForm.projectId || this.selectedProjectId; if (!projectId) return; try { const actor = this.actorName(); await this.api('POST', `/projects/${projectId}/reopen`, { description: this.reopenForm.description, creator: actor }); this.showReopenModal = false; this.reopenForm = { projectId: '', projectTitle: '', description: '' }; await this.loadProjects(); if (this.selectedProjectId === projectId && this.view === 'graph') { await this.loadProject(projectId); this.updateGraph(); } this.showToast('Project reopened'); } catch (e) { this.showToast(e.message, 'error'); } } ``` -------------------------------- ### Dagre Layout Options Source: https://github.com/oritera/cairn/blob/main/cairn/src/cairn/server/static/index.html Configuration options for the Dagre layout algorithm, used for arranging graph elements. Includes settings for direction, spacing, and animation. ```javascript return { name: 'dagre', rankDir: direction, nodeSep: 60, rankSep: 80, padding: 50, fit: true, animate, animationDuration: 400, animationEasing: 'ease-in-in-out-cubic', }; ``` -------------------------------- ### Complete Project After Reason Source: https://github.com/oritera/cairn/blob/main/docs/specs/server-protocol.md Used when the 'reason' task returns a conclusion that satisfies the project's goal. This snippet shows how to mark the project as complete. ```json { "from": ["f008"], "description": "flag{abc} 满足 goal 要求", "worker": "dispatcher-worker-A" } ``` -------------------------------- ### HTML for Cairn Server Static Index Source: https://github.com/oritera/cairn/blob/main/cairn/src/cairn/server/static/index.html The main HTML file for the Cairn server's static assets. It includes basic structure and placeholders for dynamic content. ```html Cairn
``` -------------------------------- ### Request Project Deletion Source: https://github.com/oritera/cairn/blob/main/cairn/src/cairn/server/static/index.html Initiates the project deletion process by setting the confirmation dialog's project details and showing the modal. ```javascript requestDeleteProject(projectId, projectTitle = '') { if (!projectId) return; this.deleteConfirm = { id: projectId, title: projectTitle }; this.showDeleteModal = true; } ``` -------------------------------- ### Klay Layout Options Source: https://github.com/oritera/cairn/blob/main/cairn/src/cairn/server/static/index.html Configures the Klay layout engine for graph visualization. Supports animated transitions and specifies parameters like fit, padding, and direction. ```javascript layoutOpts(animate = true) { const direction = this.layoutDirection(); // ... ELK layout options if (this.layoutEngine() === 'klay') { const isHorizontal = direction === 'LR'; return { name: 'klay', fit: true, padding: 50, // ... other klay options ``` -------------------------------- ### Open Project Completion Form Source: https://github.com/oritera/cairn/blob/main/cairn/src/cairn/server/static/index.html Opens the modal form for completing a project, ensuring the user can act on selected facts. ```javascript openCompleteProject() { if (!this.canActOnSelectedFacts()) return; this.completeForm = { description:'' }; this.showCompleteModal = true; } ``` -------------------------------- ### ELK Layout Options Source: https://github.com/oritera/cairn/blob/main/cairn/src/cairn/server/static/index.html Configures the ELK layout engine for graph visualization. Supports animated transitions and specifies parameters like algorithm, direction, aspect ratio, and node/edge spacing. ```javascript layoutOpts(animate = true) { const direction = this.layoutDirection(); if (this.layoutEngine() === 'elk') { const elkDirection = direction === 'TB' ? 'DOWN' : 'RIGHT'; return { name: 'elk', fit: true, padding: 50, animate: animate, animationDuration: 350, animationEasing: 'ease-in-out-cubic', elk: { algorithm: 'layered', 'elk.direction': elkDirection, 'elk.aspectRatio': '1.5', 'elk.layered.nodePlacement.strategy': 'BRANDES_KOEPF', 'elk.spacing.nodeNode': '50', 'elk.layered.spacing.nodeNodeBetweenLayers': '80', 'elk.spacing.edgeNode': '25', 'elk.layered.crossingMinimization.strategy': 'LAYER_SWEEP', 'elk.layered.nodePlacement.bk.fixedAlignment': 'BALANCED', }, }; } // ... other layout engines ``` -------------------------------- ### Update Server Settings Source: https://github.com/oritera/cairn/blob/main/cairn/src/cairn/server/static/index.html Saves server settings by making a PUT request to the '/settings' endpoint. Handles success by updating UI state and showing a toast message, and catches errors to display an error toast. ```javascript wait this.api('PUT', '/settings', this.settingsForm); this.showSettings = false; this.showToast('Server settings saved'); } catch(e) { this.showToast(e.message, 'error'); } }, }; } ``` -------------------------------- ### Run Fast Regression Tests Source: https://github.com/oritera/cairn/blob/main/README.md Executes the fast regression test suite for Cairn without requiring Docker or live model endpoints. This is useful for quick checks during development. ```bash uv run --project cairn --group dev pytest ``` -------------------------------- ### Initialize Cytoscape Graph Source: https://github.com/oritera/cairn/blob/main/cairn/src/cairn/server/static/index.html Initializes the Cytoscape.js graph instance within a specified container. Sets up event listeners for node and edge taps, and manages graph interactions. ```javascript initGraph() { const container = document.getElementById('cy'); if (!container) return; const { nodes, edges } = this.buildElements(); const rawCy = cytoscape({ container, elements: [...nodes, ...edges], style: this.graphStyles(), layout: this.layoutOpts(), minZoom: 0.15, maxZoom: 3.5, }); this.cy = rawCy; const self = this; rawCy.on('tap', 'node', e => self.onNodeTap(e)); rawCy.on('tap', 'edge', e => self.onEdgeTap(e)); rawCy.on('tap', e => { if (e.target === rawCy) self.clearSelection(); }); this.pulseActive(); this.setupAutoFit(); } ``` -------------------------------- ### Open Hint Timeline Entry Source: https://github.com/oritera/cairn/blob/main/cairn/src/cairn/server/static/index.html Opens a specific hint in the timeline log and scrolls to it. Requires the hint ID. ```javascript openHintTimelineEntry(hintId) { if (!hintId) return; const entry = this.timelineEvents().find(event => event.type === 'hint_added' && event.targetId === hintId); if (!entry) return; this.sideTab = 'log'; this.openTimelineEntry(entry); this.scrollTimelineToEntry(entry.id); } ``` -------------------------------- ### Cairn System Architecture Diagram Source: https://github.com/oritera/cairn/blob/main/README.md Illustrates the interaction between the Cairn Server, Dispatcher, and Worker Containers. The server maintains graph consistency, while the dispatcher manages tasks and worker lifecycle. ```text ┌──────────────────────────────────┐ │ Cairn Server │ │ Facts + Intents + Hints │ └─────────────────┬────────────────┘ │ Read / Write API │ ┌─────────────────┴────────────────┐ │ Dispatcher │ │ Schedules tasks, manages │ │ containers, writes protocol │ └──────────┬───────────────┬───────┘ │ │ ┌───────────────┴──┐ ┌──────┴──────────────┐ │ Worker Container│ │ Worker Container │ │ (Project A) │ │ (Project B) │ │ ┌────┐ ┌────┐ │ │ ┌────┐ ┌────┐ │ │ │ W. │ │ W. │ │ │ │ W. │ │ W. │ │ │ └────┘ └────┘ │ │ └────┘ └────┘ │ └──────────────────┘ └─────────────────────┘ ``` -------------------------------- ### Restart Project Replay Source: https://github.com/oritera/cairn/blob/main/cairn/src/cairn/server/static/index.html Restarts the project replay from the beginning. Resets replay state and applies the first frame. ```typescript restartProjectReplay() { if (!this.replay.active || this.replay.frames.length === 0) return; this.stopReplayTimer(); this.replay.playing = true; this.selectedNode = null; this.selectedFacts = []; this.selectedTimelineEntryId = null; this.applyReplayFrame(0, { reinitialize: true }); this.scheduleReplayTick(); } ``` -------------------------------- ### Complete a Project Source: https://github.com/oritera/cairn/blob/main/cairn/src/cairn/server/static/index.html Completes the selected project with a description and marks the current actor as the worker. It then reloads projects, the current project, and updates the graph. ```javascript async completeProject() { try { const actor = this.actorName(); await this.api('POST', `/projects/${this.selectedProjectId}/complete`, { from: this.selectedFacts, description: this.completeForm.description, worker: actor }); this.showCompleteModal = false; await this.loadProjects(); await this.loadProject(this.selectedProjectId); this.updateGraph(); this.showToast('Project completed'); } catch(e) { this.showToast(e.message, 'error'); } } ``` -------------------------------- ### Highlight Full YAML Lines Source: https://github.com/oritera/cairn/blob/main/cairn/src/cairn/server/static/index.html Highlights entire lines of YAML, applying specific formatting to comments, list items with keys, key-value pairs, and list values. It leverages `highlightYamlScalar` for value highlighting. ```javascript highlightYamlLine(line) { if (/^\s\*$/.test(line)) return ''; if (/^\s\*#/.test(line)) return `${this.escapeHtml(line)}`; const listKeyMatch = line.match(/^(\s\*-\s+)([^:#\n][^:]*):(.*)$/); if (listKeyMatch) { const [, prefix, key, rest] = listKeyMatch; return `${this.escapeHtml(prefix)}${this.escapeHtml(key)}:${this.highlightYamlScalar(rest)}`; } const keyMatch = line.match(/^(\s\*)([^:#\n][^:]*):(.*)$/); if (keyMatch) { const [, indent, key, rest] = keyMatch; return `${this.escapeHtml(indent)}${this.escapeHtml(key)}:${this.highlightYamlScalar(rest)}`; } const listValueMatch = line.match(/^(\s\*-\s+)(.*)$/); if (listValueMatch) { const [, prefix, rest] = listValueMatch; return `${this.escapeHtml(prefix)}${this.highlightYamlScalar(rest)}`; } return this.highlightYamlScalar(line); } ```