333 lines
23 KiB
Markdown
333 lines
23 KiB
Markdown
# 04 — API Contracts
|
|
|
|
Two contract surfaces:
|
|
1. **Frontend ⇄ Rust** — Tauri **commands** (request/response) and **events** (Rust→UI push).
|
|
2. **Rust internal** — service **traits** that decouple callers from concrete engines (NFR-MNT-1/2).
|
|
|
|
All payloads are `serde`-serializable; timestamps are unix epoch ms unless noted. Errors are typed:
|
|
each command returns `Result<T, WaError>` where `WaError` carries a `kind` (machine-readable) and a
|
|
`message` (human-readable).
|
|
|
|
## 1. Tauri commands (frontend → Rust)
|
|
|
|
```ts
|
|
// ---- Recording lifecycle ----
|
|
// `record` (default false) controls audio RETENTION (ADR-0009). When false, working audio is
|
|
// deleted on finalize and only the transcript/notes persist. It can be toggled mid-meeting.
|
|
// templateId (Phase 8, T8.1, FR-NOTE-5) picks a NoteTemplate — see list_note_templates below.
|
|
// language (T8.7, FR-TRX-4, M4.2): omitted/"auto" requests auto-detection; an ISO-639-1 code
|
|
// (e.g. "es") forces that language. Falls back to Settings.whisper_language when omitted.
|
|
// Only takes effect with a multilingual model loaded (ModelInfo.multilingual) — an English-only
|
|
// model forces "en" regardless (see resolve_language in src-tauri/src/transcription/mod.rs).
|
|
start_recording(input: { meetingTitle?: string; calendarEventId?: string; record?: boolean; templateId?: string; language?: string }): MeetingId
|
|
stop_recording(input: { meetingId: MeetingId }): MeetingSummaryRef
|
|
pause_recording(input: { meetingId: MeetingId }): void
|
|
resume_recording(input: { meetingId: MeetingId }): void
|
|
set_recording_retention(input: { meetingId: MeetingId; record: boolean }): void // toggle mid-meeting (FR-REC-1)
|
|
acknowledge_recording_consent(): void // one-time (FR-REC-2)
|
|
|
|
// ---- Live notes (Granola-style redesign, `03-data-model.md`'s manual_notes.json) ----
|
|
// Both are live-session only (err "no matching active recording" once finalized — post-finalize,
|
|
// notes.md is the single editable document and `update_notes` is the command for it).
|
|
update_live_notes(input: { meetingId: MeetingId; markdown: string }): void // freeform notes typed while recording
|
|
// anchorMs: the clicked transcript segment's start_ms, not its id (survives re-transcription).
|
|
// text: "" clears that moment's note. Merged into notes.md right after the transcript paragraph
|
|
// covering anchorMs when the meeting finalizes (notes::MarkdownNotes::merge).
|
|
set_segment_note(input: { meetingId: MeetingId; anchorMs: number; text: string }): void
|
|
|
|
// ---- Hardware ----
|
|
hardware_status(): { backends: BackendInfo[]; active: BackendId; modelSize: string; estRtf: number }
|
|
set_preferred_backend(input: { backend: BackendId | "auto" }): void
|
|
|
|
// ---- Transcription / models ----
|
|
// language (T8.7, M4.2): omitted reuses the meeting's current language rather than resetting it.
|
|
reprocess_transcript(input: { meetingId: MeetingId; model: string; language?: string }): void // batch mode (FR-TRX-3)
|
|
// ModelInfo gained `multilingual: boolean` (T8.7, FR-TRX-4, M4.2) — false for `.en` (English-only)
|
|
// ggml variants, true for the multilingual ones; gates the Settings language picker.
|
|
list_models(): ModelInfo[]
|
|
list_diarization_models(): ModelInfo[] // fixed seg+emb pair (T4.7, FR-MODEL-1)
|
|
download_model(input: { kind: "whisper" | "diar-seg" | "diar-emb"; id: string }): void // emits progress events
|
|
remove_model(input: { id: string }): void // disambiguated by id, not kind — ids never collide across catalogs
|
|
// Static catalog of whisper.cpp-recognized ISO-639-1 codes for the Settings language dropdown
|
|
// (T8.7, FR-TRX-4, M4.2); "Auto-detect" is a frontend-only addition, not in this list.
|
|
list_whisper_languages(): { code: string; label: string }[]
|
|
|
|
// ---- Speakers ----
|
|
rename_speaker(input: { meetingId: MeetingId; label: string; name: string }): void
|
|
merge_speakers(input: { meetingId: MeetingId; from: string[]; into: string }): void
|
|
map_speaker_to_participant(input: { meetingId: MeetingId; label: string; participantId: string }): void
|
|
|
|
// ---- Meetings / storage ----
|
|
// MeetingListItem gained a `tags: string[]` field (Phase 8, FR-SEARCH-2).
|
|
// list_meetings dropped limit/offset (never implemented — no pagination need
|
|
// yet at local-desktop meeting counts) and gained from/to date filters, per
|
|
// FR-SEARCH-2's "filter by date, tag, or participant".
|
|
list_meetings(input: { query?: string; tag?: string; participantId?: string; from?: number; to?: number }): MeetingListItem[]
|
|
// Meeting also carries `action_items: ActionItem[]` — table-backed (the confirmed/edited source
|
|
// of truth), falling back to summary.json drafts until any are saved (FR-LLM-3). TranscriptSegment
|
|
// keeps start_ms/end_ms; the UI renders these as per-line timestamps.
|
|
get_meeting(input: { meetingId: MeetingId }): Meeting // includes transcript + speakers + summary (null until generated)
|
|
delete_meeting(input: { meetingId: MeetingId }): void
|
|
// format "bundle" writes a portable folder: audio.wav + transcript.json + notes.md + summary.json
|
|
// (all decrypted) + meeting.json (the MeetingBundle manifest), re-importable on another machine.
|
|
// format "obsidian" writes one self-contained vault note (dest is a .md file path, no audio):
|
|
// YAML frontmatter (title/date/duration/participants/tags/source) + notes + summary + decisions
|
|
// + action items + timestamped transcript. For dropping a meeting into an Obsidian vault.
|
|
export_meeting(input: { meetingId: MeetingId; dest: string; format: "md" | "pdf" | "docx" | "bundle" | "obsidian" }): string
|
|
// Edit commands that change an uploaded artifact — update_notes, set_tags, generate_summary,
|
|
// reprocess_transcript, confirm_action_items — auto-resync when sync is enabled (FR-SYNC-5):
|
|
// they enqueue for finalize-trigger targets and pump in the background. SHA-256 dedup means an
|
|
// edit that didn't alter a file uploads nothing.
|
|
update_notes(input: { meetingId: MeetingId; markdown: string }): void
|
|
// SearchHit = MeetingListItem fields (id, title, started_at, duration_secs, status, tags) + snippet: string
|
|
search(input: { query: string }): SearchHit[] // FTS (FR-SEARCH-1)
|
|
set_tags(input: { meetingId: MeetingId; tags: string[] }): void
|
|
list_tags(): string[] // all known tag names, sorted
|
|
// NoteTemplate = { id: string, name: string, sections: string[] } (T8.1, FR-NOTE-5). Meeting
|
|
// gained `template_id: string | null` — the template picked at start_recording.
|
|
list_note_templates(): NoteTemplate[]
|
|
// Bulk export (T8.5, FR-STORE-4): every meeting matching tag/from/to, one file (or bundle
|
|
// folder) per meeting under destDir. Returns the count actually exported.
|
|
bulk_export_meetings(input: { destDir: string; format: "md" | "pdf" | "docx" | "bundle"; tag?: string; from?: number; to?: number }): number
|
|
// Import bundle(s) (FR-STORE-4): `dir` is a single bundle folder (has meeting.json) or a parent
|
|
// folder of them (from a bulk export). Each is reconstructed under a fresh meeting id (original
|
|
// title/date/duration/speakers/tags/action items preserved). Returns the count imported.
|
|
import_meeting_bundle(input: { dir: string }): number
|
|
|
|
// ---- LLM / AI provider (ADR-0007/0011) ----
|
|
// provider ∈ ollama | custom | anthropic | openai | off. Hosted-provider API keys are passed to
|
|
// set_llm_provider but stored only in the OS credential store; never returned by llm_status.
|
|
llm_status(): { provider: string; reachable: boolean; isLocal: boolean; models: string[] }
|
|
set_llm_provider(input: { provider: string; endpoint?: string; model?: string; apiKey?: string }): void // FR-AI-1/2
|
|
generate_summary(input: { meetingId: MeetingId; templateId?: string }): void // streams via events (FR-LLM-2/4)
|
|
// ActionItem gained `reminder_set: boolean` (T8.6, FR-CAL-5). confirm_action_items now also
|
|
// schedules/cancels each item's local reminder (Windows scheduled toast notification — the OS
|
|
// itself delivers it at due_at, no app-side polling timer; see src-tauri/src/reminders.rs).
|
|
confirm_action_items(input: { meetingId: MeetingId; items: ActionItem[] }): void
|
|
llm_setup_suggestions(): { ollamaInstalled: boolean; installUrl: string; suggestedModel: { id: string; label: string; approxSizeGb: number } } // T5.7, FR-LLM-5
|
|
pull_ollama_model(input: { model: string }): void // guided download via Ollama's own /api/pull; emits model://progress (T5.7)
|
|
|
|
// ---- Calendar / .pst ----
|
|
// rangeDays: only import events starting within the last N days; omitted imports the full mailbox
|
|
// history. Bug fix: a long-lived .pst has no natural upper bound on history (every recurring
|
|
// series expands to its cap, T4.1's RECURRENCE_MAX_OCCURRENCES, plus every one-off entry the file
|
|
// ever held, e.g. a decade of Outlook's auto-generated yearly holidays) -- unbounded import could
|
|
// produce tens of thousands of rows. Settings.pst_import_range_days persists the last choice and
|
|
// applies it to pst_auto_sync's startup re-import too.
|
|
import_pst(input: { path: string; password?: string; rangeDays?: number }): number // eventsImported; emits pst://progress (FR-CAL-1)
|
|
list_calendar_events(input: { from?: number; to?: number }): CalendarEvent[]
|
|
get_calendar_event(input: { eventId: string }): { event: CalendarEvent; participants: Participant[] } // pre-meeting panel + naming dropdown (FR-CAL-3, FR-SPK-4)
|
|
// olderThanDays omitted deletes every unlinked event ("Delete all"); Some(n) only those starting
|
|
// more than n days ago. An event attached to a recorded meeting (meetings.calendar_event_id) is
|
|
// always kept regardless of the choice -- protected reports how many were skipped for that reason.
|
|
cleanup_calendar_events(input: { olderThanDays?: number }): { deleted: number; protected: number }
|
|
attach_meeting_to_event(input: { meetingId: MeetingId; eventId: string }): void
|
|
// Optional MS Graph calendar source (M4.4, T8.9, FR-CAL-6): opt-in, explicit consent (OAuth PKCE
|
|
// + Microsoft's own consent screen), metadata-only (subject/organizer/start/end/attendees, never
|
|
// the event body). Not a SyncTarget — begin_graph_calendar_link stores its token separately from
|
|
// sync_targets and never appears in list_sync_targets.
|
|
begin_graph_calendar_link(): { authUrl: string } // opens in browser; emits calendar://linked when done
|
|
import_graph_calendar(input: { from?: number; to?: number }): number // eventsImported; emits calendar://progress
|
|
disconnect_graph_calendar(): void // best-effort credential cleanup + settings reset
|
|
|
|
// ---- Sync / upload (ADR-0010) ---- secrets are passed to add/update but stored only in the OS
|
|
// credential store; they are NEVER returned by list_sync_targets.
|
|
list_sync_targets(): SyncTargetInfo[]
|
|
add_sync_target(input: SyncTargetConfig & { secret: string }): SyncTargetInfo // FR-SYNC-2/9
|
|
update_sync_target(input: { id: string } & Partial<SyncTargetConfig> & { secret?: string }): SyncTargetInfo
|
|
remove_sync_target(input: { id: string }): void
|
|
test_sync_target(input: { id: string } | SyncTargetConfig & { secret: string }): { ok: boolean; message: string } // FR-SYNC-4
|
|
set_sync_enabled(input: { enabled: boolean }): void // master switch (FR-SYNC-1)
|
|
sync_meeting(input: { meetingId: MeetingId; targetId?: string }): void // manual "Upload now" (FR-SYNC-3)
|
|
sync_status(input?: { meetingId?: MeetingId }): SyncJobInfo[] // queue state (FR-SYNC-5)
|
|
retry_sync_job(input: { jobId: string }): void
|
|
// OAuth (secondary targets, FR-SYNC-9): begins loopback-redirect PKCE flow, returns when linked.
|
|
begin_oauth_link(input: { kind: "onedrive" | "dropbox" | "box" }): { ok: boolean; account?: string }
|
|
|
|
// ---- Feature briefs + MCP server (ADR-0011) ----
|
|
create_feature_brief(input: { meetingId: MeetingId; targetRepo?: string }): FeatureBrief // FR-MCP-4 (LLM-distilled)
|
|
list_feature_briefs(input: { meetingId?: MeetingId }): FeatureBriefInfo[]
|
|
get_feature_brief(input: { id: string }): FeatureBrief
|
|
set_brief_exposed(input: { id: string; exposed: boolean }): void // scope control (FR-MCP-3)
|
|
mcp_status(): { enabled: boolean; transport: "http" | "stdio"; endpoint: string; tokenSet: boolean; exposeScope: string }
|
|
set_mcp_enabled(input: { enabled: boolean; transport?: "http" | "stdio"; port?: number }): { endpoint: string; token: string } // FR-MCP-1/6
|
|
set_mcp_scope(input: { expose: "none" | "selected" | "all"; exposeRecordings?: boolean }): void // FR-MCP-3
|
|
mcp_access_log(input?: { limit?: number }): McpAccessEntry[] // audit (FR-MCP-5)
|
|
// (Layer 3, later) push handoff — spawn a local agent CLI / open a tracker issue from a brief.
|
|
run_agent(input: { briefId: string; tool: "claude" | "codex" | "opencode" | "copilot"; repoPath: string }): { ok: boolean } // FR-AGENT-1
|
|
create_issue_from_brief(input: { briefId: string; tracker: "github"; assignCopilot?: boolean }): { url: string } // FR-AGENT-2
|
|
|
|
// ---- Settings ----
|
|
// Settings gained `whisper_language: string | null` (T8.7, FR-TRX-4, M4.2) — the default
|
|
// transcription language applied at the next start_recording; null = auto-detect. Mirrors
|
|
// this doc's settings.json `transcription.language` (03-data-model.md).
|
|
get_settings(): Settings
|
|
update_settings(input: Partial<Settings>): Settings
|
|
// Reports the full egress allowlist so the UI can prove exactly what may leave the device (FR-SEC-2).
|
|
privacy_self_check(): {
|
|
llmEndpoint: string; llmIsLocal: boolean;
|
|
syncEnabled: boolean;
|
|
syncTargets: { name: string; host: string; thirdParty: boolean; tls: boolean }[];
|
|
allowlistedHosts: string[];
|
|
}
|
|
```
|
|
|
|
### Conventions
|
|
- Commands return promptly; anything long-running (recording, transcription, summary, model
|
|
download, PST import) reports progress/results through **events** below.
|
|
- `MeetingId` is a uuid string. `BackendId` ∈ `"npu" | "nvidia" | "amd" | "intel" | "cpu"`.
|
|
|
|
## 2. Tauri events (Rust → frontend)
|
|
|
|
```ts
|
|
"recording://state" { meetingId, state: "recording"|"paused"|"stopped"|"cancelled", elapsedMs }
|
|
"recording://level" { meetingId, rms: number, peak: number } // waveform (FR-CAP-5)
|
|
"recording://device" { meetingId, recovered: boolean, message: string } // capture device change (FR-CAP-6)
|
|
"transcript://segment" { meetingId, segment: TranscriptSegment } // live segments (FR-TRX-2); may re-emit a committed segment with a refined `speaker` — replace by `segment.id`
|
|
"transcript://finalized" { meetingId, segmentCount }
|
|
"diarization://updated" { meetingId, speakers: SpeakerInfo[] } // post-pass AND live 15s provisional passes (FR-SPK); carries "You" once the mic voiceprint matches
|
|
"llm://token" { meetingId, text } // streamed summary (FR-LLM-4)
|
|
"llm://done" { meetingId, summary: SummaryFile } // full summary.json contents, not just a pointer
|
|
"model://progress" { id, receivedBytes, totalBytes }
|
|
"pst://progress" { processed, total }
|
|
"calendar://linked" { ok: boolean, error?: string } // MS Graph OAuth handshake settled (M4.4)
|
|
"calendar://progress" { processed, total } // MS Graph import (M4.4)
|
|
"hardware://changed" { active: BackendId, reason: string } // fallback occurred (FR-HW-4)
|
|
"recording://retention" { meetingId, record: boolean } // retention toggled (FR-REC-1/3)
|
|
"sync://job" { jobId, meetingId, targetId, artifact, status, bytesSent, bytesTotal } // FR-SYNC-5
|
|
"sync://done" { meetingId, targetId, uploaded: number, failed: number }
|
|
"mcp://access" { at, tool, meetingId?, client? } // agent read something (FR-MCP-5)
|
|
"agent://progress" { briefId, tool, line } // push run output (FR-AGENT-1)
|
|
"error" { kind, message, context? }
|
|
```
|
|
|
|
## 3. Internal Rust service traits
|
|
|
|
These define the seams that let engines be swapped without touching callers. Signatures are
|
|
indicative (async where I/O-bound).
|
|
|
|
```rust
|
|
// audio/mod.rs
|
|
pub trait AudioCapture: Send + Sync {
|
|
/// Begin WASAPI loopback capture, writing PCM to `wav_path`; frames also pushed to `sink`.
|
|
fn start(&self, wav_path: &Path, sink: FrameSink) -> Result<CaptureHandle, AudioError>;
|
|
/// Capture the user's microphone (FR-CAP-7); frames pushed to `sink`, no WAV.
|
|
fn start_microphone(&self, device_id: Option<&str>, sink: FrameSink) -> Result<CaptureHandle, AudioError>;
|
|
fn pause(&self, h: &CaptureHandle) -> Result<(), AudioError>;
|
|
fn resume(&self, h: &CaptureHandle) -> Result<(), AudioError>;
|
|
fn stop(&self, h: CaptureHandle) -> Result<CaptureSummary, AudioError>;
|
|
}
|
|
// When the mic is enabled, `spawn_mixer` sums the loopback + mic 16kHz-mono
|
|
// frames into the single transcription stream (`list_input_devices` enumerates
|
|
// mic devices, mirroring `list_audio_devices` for render devices).
|
|
|
|
// hardware/mod.rs
|
|
pub trait HardwareDetector: Send + Sync {
|
|
fn detect(&self) -> Vec<BackendInfo>; // ranked NPU→NVIDIA→AMD→Intel→CPU
|
|
fn best(&self, preferred: Option<BackendId>) -> BackendInfo;
|
|
}
|
|
|
|
// transcription/mod.rs
|
|
pub trait Transcriber: Send + Sync {
|
|
fn load(model: &Path, backend: BackendId) -> Result<Self, TrxError> where Self: Sized;
|
|
/// Stream interim + final segments for an audio window.
|
|
fn transcribe_stream(&self, audio: AudioWindow, out: SegmentSink) -> Result<(), TrxError>;
|
|
/// One-shot batch transcription (higher accuracy).
|
|
fn transcribe_file(&self, wav: &Path) -> Result<Vec<TranscriptSegment>, TrxError>;
|
|
}
|
|
|
|
// diarization/mod.rs
|
|
pub trait Diarizer: Send + Sync {
|
|
fn diarize(&self, wav: &Path) -> Result<Vec<SpeakerSpan>, DiarError>;
|
|
fn assign(&self, segments: &mut [TranscriptSegment], spans: &[SpeakerSpan]);
|
|
}
|
|
|
|
// storage/mod.rs (async, sqlx)
|
|
#[async_trait] pub trait Store: Send + Sync {
|
|
async fn create_meeting(&self, m: NewMeeting) -> Result<MeetingId, StoreError>;
|
|
async fn finalize_meeting(&self, id: &MeetingId, s: FinalizeMeeting) -> Result<(), StoreError>;
|
|
async fn list_meetings(&self, f: MeetingFilter) -> Result<Vec<MeetingListItem>, StoreError>;
|
|
async fn get_meeting(&self, id: &MeetingId) -> Result<Meeting, StoreError>;
|
|
async fn delete_meeting(&self, id: &MeetingId) -> Result<(), StoreError>;
|
|
async fn search(&self, q: &str) -> Result<Vec<SearchHit>, StoreError>;
|
|
async fn recover_scan(&self) -> Result<Vec<MeetingId>, StoreError>; // FR-REL-1
|
|
async fn enforce_retention(&self, policy: Retention) -> Result<u32, StoreError>;
|
|
}
|
|
|
|
// llm/mod.rs
|
|
#[async_trait] pub trait LlmProvider: Send + Sync {
|
|
async fn status(&self) -> LlmStatus; // reachable? local? models
|
|
async fn summarize(&self, prompt: Prompt, out: TokenSink) -> Result<Summary, LlmError>;
|
|
fn is_local(&self) -> bool; // FR-LLM-6 guard
|
|
}
|
|
|
|
// calendar/mod.rs
|
|
// `attendees()` (a second, separate trait method in the original design) was dropped — every
|
|
// source (PstSource, GraphSource) lists attendees inline per-appointment, so import() returns
|
|
// them together (see ADR-0008's update). CalImport gained `from`/`to` (M4.4) for a source that
|
|
// fetches by date range (Graph's calendarView); PstSource ignores them.
|
|
pub trait CalendarSource: Send + Sync {
|
|
fn import(&self, input: CalImport) -> Result<Vec<ImportedEvent>, CalError>; // pst|graph|ics
|
|
}
|
|
|
|
// notes/mod.rs
|
|
pub trait NotesRenderer: Send + Sync {
|
|
fn to_markdown(&self, t: &Transcript, speakers: &[SpeakerInfo], s: Option<&Summary>) -> String;
|
|
fn export(&self, md: &str, dest: &Path, fmt: ExportFormat) -> Result<PathBuf, NotesError>;
|
|
}
|
|
// MarkdownNotes::merge(segments, speakers, manual: &ManualNotes, summary, template) -> String is an
|
|
// inherent method (not part of the trait — only one renderer needs it): what `stop_recording` calls
|
|
// instead of `to_markdown` to fold manual_notes.json into the generated notes.md (see 03-data-model.md).
|
|
|
|
// sync/mod.rs
|
|
// One impl per provider; `WebDavTarget` covers Nextcloud/ownCloud/Cloudreve/Seafile/Synology.
|
|
// Secondary OAuth impls: OneDriveTarget, DropboxTarget, BoxTarget (ADR-0010).
|
|
#[async_trait] pub trait SyncTarget: Send + Sync {
|
|
fn kind(&self) -> SyncKind;
|
|
fn is_third_party(&self) -> bool; // false for self-hosted WebDAV
|
|
async fn test(&self) -> Result<(), SyncError>; // reachability + auth (FR-SYNC-4)
|
|
async fn ensure_dir(&self, remote_dir: &str) -> Result<(), SyncError>;
|
|
async fn exists(&self, remote_path: &str, sha256: &str) -> Result<bool, SyncError>; // skip-if-unchanged
|
|
/// Upload a file; resumable/chunked for large artifacts. Reports progress via `prog`.
|
|
async fn put(&self, local: &Path, remote_path: &str, prog: ProgressSink) -> Result<(), SyncError>;
|
|
}
|
|
|
|
// Owns the durable queue, retry/backoff, credential resolution, and TLS enforcement.
|
|
#[async_trait] pub trait SyncManager: Send + Sync {
|
|
async fn enqueue_meeting(&self, meeting_id: &MeetingId, target_id: Option<&str>) -> Result<(), SyncError>;
|
|
async fn pump(&self) -> Result<(), SyncError>; // drive pending jobs (called on finalize + startup + timer)
|
|
async fn status(&self, meeting_id: Option<&MeetingId>) -> Result<Vec<SyncJobInfo>, SyncError>;
|
|
async fn retry(&self, job_id: &str) -> Result<(), SyncError>;
|
|
}
|
|
|
|
// llm/mod.rs — `LlmProvider` (above) gains hosted impls behind the same trait (ADR-0011):
|
|
// OllamaProvider (local) · OpenAiCompatProvider (/v1/chat/completions) · AnthropicProvider (/v1/messages)
|
|
// `is_local()` stays the egress guard; hosted impls return false and require an API key from the keychain.
|
|
|
|
// mcp/mod.rs — WA as an MCP server (loopback, off by default). Tools-first (FR-MCP-2).
|
|
#[async_trait] pub trait McpServer: Send + Sync {
|
|
async fn start(&self, cfg: McpConfig) -> Result<McpHandle, McpError>; // returns endpoint + token
|
|
async fn stop(&self, h: McpHandle) -> Result<(), McpError>;
|
|
fn tools(&self) -> Vec<McpToolDescriptor>; // list_recent_meetings, get_transcript, get_action_items, get_feature_brief
|
|
}
|
|
// Builds the agent-ready spec from a transcript via the configured LlmProvider.
|
|
#[async_trait] pub trait FeatureBriefBuilder: Send + Sync {
|
|
async fn build(&self, meeting_id: &MeetingId, target_repo: Option<&str>) -> Result<FeatureBrief, BriefError>;
|
|
}
|
|
|
|
// agent/mod.rs — Layer 3 (later). Push handoff; one impl per CLI / tracker.
|
|
#[async_trait] pub trait AgentRunner: Send + Sync {
|
|
async fn run(&self, brief: &FeatureBrief, repo: &Path, out: LineSink) -> Result<RunOutcome, AgentError>; // claude -p / codex exec / …
|
|
}
|
|
#[async_trait] pub trait IssueTracker: Send + Sync {
|
|
async fn create_issue(&self, brief: &FeatureBrief, assign_copilot: bool) -> Result<String /*url*/, AgentError>;
|
|
}
|
|
```
|
|
|
|
## Versioning
|
|
|
|
- Command/event names and payload shapes are versioned implicitly by `schema` fields in persisted
|
|
JSON (`03-data-model.md`) and explicitly in a `CONTRACTS_VERSION` constant. Breaking a command
|
|
shape requires bumping it and updating the frontend client in the same change (see `CLAUDE.md`
|
|
"source of truth" rule).
|