Commit Graph
152 Commits
Author SHA1 Message Date
ZacharyZhang-NY 66aae376c0 Let the CEO hand out connector toolkits agent by agent
Connections belong to the company - one Composio connected account per
toolkit, mirrored locally in connector_accounts - and access belongs to
individuals: a new agent_connector_grants table records which agent may use
which toolkit. Agent runs now load composio tools from the intersection of
active connections and that agent's grants, and tool execution re-checks the
grant server-side so an ungranted agent cannot call a connector tool it
heard about. Any agent can list_connectors to see what the company has
connected and who holds access; grant_connector/revoke_connector are
CEO-only. The Composio client speaks the v3 contract (user_id,
connected_account_id, toolkit catalog with cursor paging, managed auth
config creation, connect-link initiation). Covered by four grants contract
tests.
2026-07-06 23:13:03 -04:00
ZacharyZhang-NY d528cd6f75 Turn the Messages rail into a real conversation list
Every channel and direct conversation is now its own row the way messengers
work: avatar or channel glyph, name, last-message preview, relative time,
and a true unread count from per-employee read state (the old badge showed
total message count). Rows sort by latest activity and the newest
conversation opens by default. Direct rows title themselves after the
counterpart resolved from memberships; agent-to-agent conversations the
board observes keep their two-name title, and opening one clears its unread
because read state is per-employee bookkeeping, not access control.
Conversation rows get their own .convo-item style instead of fighting the
sidebar's single-line nav-item rules.
2026-07-06 22:56:02 -04:00
ZacharyZhang-NY cab7b01831 Give every 1:1 its own direct conversation instead of piling into #general
Agents could only post to named public channels - the messaging tool even
excluded direct spaces - so assignments, reports, and questions all crowded
into general. Now ChatService.ensureDirectSpace finds or creates the private
two-member conversation for any agent/employee pair; send_message takes
to="Name" for 1:1 traffic (exactly one of to/channel), read_messages reads a
conversation by teammate name, and task assignments land in the manager's DM
with the assignee. In a direct conversation the counterpart agent wakes on
every message without needing an @mention - same social contract as any
messenger - still bounded by the autonomous-turn budget, and wake task
briefings tell agents to reply with to= in kind. Covered by six new
direct-message contract tests; the assignment-thread test now encodes the
DM routing contract.
2026-07-06 22:49:31 -04:00
ZacharyZhang-NY f13ef48a3e Stop the UI freeze: coalesce snapshot storms and bound chat rendering
Busy agents emitted a snapshot-changed broadcast per tool turn; every event
made the renderer reload the full snapshot while the main process re-read
whole audit tables synchronously, and ChatTimeline re-rendered every message
with per-row linear scans - quadratic work per keystroke of agent activity
until clicks stopped responding. Root fixes at each layer: all broadcasts
funnel through one 200ms-coalesced notifier (unit-tested), the renderer
reloads at most every 800ms with a trailing load, audit/access snapshot
reads use SQL limits instead of full-table scans, and the timeline renders
the latest 150 messages via O(n) lookup maps with a real show-earlier
control. cred.txt is now gitignored so the Composio key cannot leak.
2026-07-06 22:40:32 -04:00
ZacharyZhang-NY f61046b7a4 Restore thread panel wiring lost in the flat-timeline refactor
The flat-messages refactor left ChatThreadPanel orphaned: ChatPage passed a
no-op onOpenThread and ChatTimeline hardcoded onOpenThread={null}, so threads
(including /escalate board flows) were unreachable from the UI and the
desktop e2e failed on main. Reconnect open/close/reply/board-intervention
wiring around the flat timeline, show reply counts on thread roots, and
clear the lint debt that severed feature left behind.
2026-07-06 21:39:03 -04:00
ZacharyZhang-NY 8b2f94a715 Show all messages flat in channel and fix V8 OOM crash
Chat: removed all thread abstraction. Every message in a channel now
renders inline chronologically as a flat list — no compact rows, no
thread panel, no thread navigation. Just messages, like Slack.

OOM fix: snapshot now limits fast-growing tables (chat_messages: 500,
task_runs: 200, provider_requests: 100) instead of loading unbounded
rows. This prevents the renderer V8 heap from growing past 3.5GB and
crashing. The notifyDataChanged callback is also wrapped in try/catch
to prevent errors when the renderer frame is disposed during agent runs.
2026-06-30 22:56:58 -04:00
ZacharyZhang-NY 63c760860e Tell agents about available external tools in system prompt
When Composio tools are loaded for an agent run, the system prompt now
mentions their count and purpose so the agent knows it can interact
with external services like GitHub, Slack, and Gmail.
2026-06-30 22:00:56 -04:00
ZacharyZhang-NY 81aae82211 Add resilient error handling for Composio tool execution
Tool execution now returns actionable error messages when a connected
account is missing, directing the admin to set up the connection. Tool
loading is resilient to API failures, falling back to cached tools if
available instead of crashing the agent run.
2026-06-30 21:59:39 -04:00
ZacharyZhang-NY e83f379143 Cache Composio tools and load saved settings in the UI
Tools are now cached per-org with a 5-minute TTL to avoid hitting the
Composio API on every agent run. The ComposioPanel loads the currently
saved API key status and pre-checks enabled toolkits from the snapshot,
so users see their existing configuration when opening Settings > Tools.
2026-06-30 21:58:48 -04:00
ZacharyZhang-NY bb24a444f4 Wake thread participants when a human replies without @mentions
When a human or board member posts in a thread where agents have
previously participated, those agents are now woken even without
explicit @mentions. This matches the old version's behavior where
any message in a conversation could wake the recipient, enabling
natural back-and-forth between humans and agents.
2026-06-30 21:51:32 -04:00
ZacharyZhang-NY 651e7a69ec Fix Composio API endpoints to match verified v3 contract
Updated getTools to use the validated toolkit_slug parameter and
per-toolkit fetching. Updated executeTool to use the verified path
POST /api/v3/tools/execute/{TOOL_NAME} with entityId and arguments
fields. Updated normalizeToolSchema to use slug and input_parameters
from the actual API response format.
2026-06-30 21:50:35 -04:00
ZacharyZhang-NY 13a235a42d Seed Composio API key and default toolkits for all organizations
New orgs get the Composio API key and default toolkits (github, gmail,
slack, notion, linear, google_calendar) during bootstrap. Existing orgs
without Composio settings are seeded at app startup via the recovery
pass, ensuring every org has external tool access from first boot.
2026-06-30 21:47:00 -04:00
ZacharyZhang-NY 453e47274a Inject inbox context into non-chat task runs
Heartbeat and manual task runs now load the agent's recent @mentions and
include them directly in the task prompt. This gives agents conversational
context even before they call read_messages, ensuring they respond to
teammates who addressed them rather than silently ignoring messages.
2026-06-30 21:41:28 -04:00
ZacharyZhang-NY 77644121db Record @mention wake failures instead of silently swallowing them
The catch block in wakeAgentMentions now records wake_state on each
mention (woken/failed) instead of silently discarding the error. Failed
mentions render with a strikethrough and muted color so the user sees
which agents could not be reached, with a tooltip on hover. Migration
044 adds the wake_state column to chat_mentions.
2026-06-30 21:38:19 -04:00
ZacharyZhang-NY f6a2659761 Extract read_messages tool to keep agent-tools under 500 lines 2026-06-30 21:35:11 -04:00
ZacharyZhang-NY 77f191fa25 Integrate Composio for external tool access
Agents can now call external tools (GitHub, Slack, Gmail, etc.) via
Composio's managed API. A thin HTTP client calls the Composio REST API
directly — no heavy SDK dependency. External tools are loaded per-org
from composio_settings and merged with internal tools at run time. The
agent run loop routes composio_* tool calls to the Composio execution
endpoint while internal tools dispatch as before.

Includes: migration 043 for composio_settings table, IPC handler for
saving Composio API key and toolkit configuration, and the composio
client/bridge modules.
2026-06-30 21:31:04 -04:00
ZacharyZhang-NY 65c5bec9a3 Add read_messages tool and conversational task prompts
Agents now have a read_messages tool to check their inbox and channel
history during runs, enabling them to catch up on conversation before
acting. Task prompts are rewritten to encourage checking messages first
and discussing work with teammates rather than posting status updates.
2026-06-30 21:21:21 -04:00
ZacharyZhang-NY 5bb9aea4f9 Fix @mention resolution for multi-word agent names
MentionTextarea inserted raw agent names (e.g. @Sarah Chen) but the
backend regex captured only the first word (@Sarah), causing mention
resolution to fail silently. Moved canonicalMentionHandle to shared/
so the renderer can produce space-free handles that the backend resolves
correctly.
2026-06-30 21:17:43 -04:00
ZacharyZhang-NY 8923a2bf0a Clarify agent mention reply readiness 2026-06-26 19:41:56 -04:00
ZacharyZhang-NY 91991f7c4a Restrict external browser URLs 2026-06-26 19:37:11 -04:00
ZacharyZhang-NY d85a47c40c Harden custom provider endpoint validation 2026-06-26 19:31:32 -04:00
ZacharyZhang-NY 745c22ec77 Fix agent assignment conversations 2026-06-26 19:27:28 -04:00
ZacharyZhang-NY bd63d42aa7 Extract slash-command routing out of ChatService
ChatService was 542 lines, mixing message delivery with the dispatch of
nine slash commands. Move the command cluster — /board, /approve, /deny,
/agent, /skill, /file, /runbook, /handoff, /escalate — into a
ChatSlashRouter that owns the approval and board-intervention services it
needs. ChatService keeps message delivery and delegates two calls.
Behaviour identical: chat-service 376 lines, router 196, full suite and
e2e green; the slash-routing governance test exercises every branch.
2026-06-26 18:25:14 -04:00
ZacharyZhang-NY 12c8402644 Extract organization seeding into its own module
OrganizationService was 552 lines, over the 500-line ceiling. The
80-line createOrganization routine — seed the org, the Board Chair,
identity/governance/operations/SDLC/policy defaults, starter channels,
the provider catalog — is a self-contained concern. Move it to
organization-seed.ts; the service now delegates and drops the two
service fields (operations, sdlc) only that routine used. Pure
relocation, behaviour identical: 469 lines, full suite and e2e green.
2026-06-26 18:19:05 -04:00
ZacharyZhang-NY 7db777b5d3 Harden main window navigation and permissions (sec #7)
The main window guarded will-navigate but not will-redirect, and left
the default session's permission handler at allow — looser than the
provider sign-in windows. Gate redirects with the same off-origin check
and deny every permission request/check by default. The first-party
renderer requests no web permissions, so denial changes nothing it does
while removing the grant a compromised page would otherwise inherit.
Verified end to end.
2026-06-26 18:11:05 -04:00
ZacharyZhang-NY 23a007d157 Require the OAuth state on the Codex callback (sec #4)
The Codex flow guarded with `result.state && result.state !== state`,
so a callback that simply omitted state skipped CSRF validation while a
sign-in window was open. Match the Anthropic and Google flows: compare
strictly, so a missing or mismatched state is rejected. PKCE already
blocks code injection; this closes the login-CSRF gap.
2026-06-26 18:06:58 -04:00
ZacharyZhang-NY 867070f575 Contain workspace file ingestion to the bound folder (sec #3)
writeDeliverable guards against path traversal, but recordWorkspaceFile
resolved and readFileSync'd its path with no containment check — safe
only because today's lone caller hands it the already-sanitized result.
Any future caller passing a raw agent path (../../.ssh/id_rsa) could read
an arbitrary file into the org's file store.

Extract the resolve+reject-".."/absolute guard into one workspace-path
helper and route both the writer and the reader through it, so the
defense lives on the function rather than the caller.
2026-06-26 18:04:13 -04:00
ZacharyZhang-NY e80c8487f3 Return who is in the room from send_message, not a receipt id
send_message answered the agent with "Posted to #x (message_id=...,
thread_id=...)" — a transaction receipt that gives no sense anyone is
there. An agent posting into that void has no reason to converse.

Now it answers with presence: who was addressed and can reply, who else
is in the channel, and the thread_id to stay in the conversation. The
message_id noise is gone. The tool's own description now frames it as
talking to teammates rather than posting a coordination status.
2026-06-26 17:49:11 -04:00
ZacharyZhang-NY a9049612c5 Frame agent chat runs as a conversation, not a ticket
A woken agent was handed a "Chat reply" ticket whose description was
four source ids and "you were @mentioned... reply", then a closing that
told it to "report progress and completion" and "update the task status"
— so it answered like a status bot.

Now the wake reason names who spoke, in which channel, and quotes what
they said, and asks the agent to reply to that person in their own voice.
When a run carries a thread, buildTaskPrompt ends as a conversation
("reply to the people here... not a status report") and skips the work
closing entirely. The ordinary work closing also drops "reporting your
progress and completion" for "tell the team what you did and what's next,
the way you'd bring a colleague up to speed." The thread_id reply token
is preserved. This is the second half of making the company feel human.
2026-06-26 17:43:41 -04:00
ZacharyZhang-NY 1efd432eea Give each agent a role-shaped persona and a human voice
The system prompt was a single flat role line, so every agent sounded
like the same status bot. Replace it with an identity (name, title,
department, company, "a colleague, not a generic assistant") and a voice
that differs by standing — executive, manager, or individual contributor
— plus an explicit "talk like a person on a team, not a status bot"
directive. The classification is derived from existing fields (top of
org = CEO, has reports = manager), so there is no schema change.

This is the first half of why the old app felt human and the new one
did not: identical model calls now carry a distinct person behind them.
2026-06-26 17:38:06 -04:00
ZacharyZhang-NY fd52dc0861 Gate autonomous task runs on the execution approval
The manual runTask path enforces assertExecutionApproval, but the
autonomous heartbeat path (enqueueHeartbeatRuns -> enqueueRun -> the
queue worker) did not, so a high-risk or filesystem task whose approval
was still pending — the normal state in ai_cofounder mode, or after an
operator declines — was executed unsupervised.

Apply the same gate in enqueueRun. Pending/declined approvals leave the
task unqueued (the heartbeat's existing try/catch skips it) and it is
picked up on the next tick once approved. ai_ceo mode auto-approves at
task creation, so autonomous companies are unaffected.
2026-06-26 17:34:09 -04:00
ZacharyZhang-NY c8de72c810 Bound autonomous agent-to-agent chat wake loops per thread
An @mention by an agent wakes the mentioned teammate by creating a task
run, and the woken agent's reply can mention back, an unbounded A->B->A
loop whose only backstop was the per-provider monthly request cap (which,
once tripped, fails every agent on that provider). Before enriching agent
conversation this had to be made safe.

Cap the agent-authored messages a thread may accumulate since its last
human/board message; past the cap, agent-triggered wakes pause until a
person re-engages. A human message always wakes mentions and resets the
budget. Counted from chat_messages, so no schema change.
2026-06-26 17:28:13 -04:00
ZacharyZhang-NYandClaude Opus 4.8 cbb87171e6 Accumulate Codex stream output items so tool calls aren't lost
Root cause of the GPT-account "empty turn: no text and no tool calls"
failures, found by replaying the real stored response: the CEO actually
called hire_agent five times, but the ChatGPT/Codex backend delivers each
output item in a response.output_item.done event and then sends a
response.completed whose response.output is []. accumulateResponsesStream
read only the completion event, so every tool call was discarded and the
turn parsed as empty — the run failed and the company never staffed.

Collect items from output_item.done and fall back to them when the
completion carries no output of its own; keep using the completion output
when it is populated (non-Codex Responses backends). Verified against the
real captured stream (5 hires recovered) and unit-tested with the exact
event shape.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 00:23:52 -04:00
ZacharyZhang-NYandClaude Opus 4.8 97242ae684 Revert tool-choice forcing — the Codex regression was strict:null
The GPT-account (Codex) path supports tool calls and worked before; my
forced tool_choice broke it and was the wrong fix. The real regression was
strict:null in the rebuild's tool definitions, which made the endpoint drop
the tool list so the model had nothing to call — a "Done" run that hired no
one. Forcing then turned that silent failure into a red one.

Restore the original "auto" loop (no forced tool_choice, no finish tool) and
keep only the one-line strict:null omission. Codex now receives valid tools
on the working "auto" choice, as in the version that worked.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 23:17:42 -04:00
ZacharyZhang-NYandClaude Opus 4.8 74422da4f8 Stop sending strict:null in Codex/Responses tool defs
The gpt-account (Codex) turn built its function tools with strict:null. The
Responses API expects strict to be a boolean or omitted; null is malformed,
so the endpoint dropped the whole tool list — leaving the model with nothing
to call. That is exactly the "model returned no tool calls" failure on the
GPT-account path. Omit strict when null; keep the boolean otherwise. Locked
with a test, alongside the Bedrock/Gemini forced-tool-choice encoding.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 23:11:36 -04:00
ZacharyZhang-NYandClaude Opus 4.8 f7543e506d Compel agents to keep acting until they explicitly finish
Forcing only the opening turn let a weak model take one token action and
stop, so a kickoff hired one agent (or none) and went quiet. Now every turn
is forced (tool_choice required) and the agent ends only by calling the new
`finish` tool — so it must keep hiring, delegating, and messaging until the
work is actually done. Finishing with zero actions, or returning no tool
call at all, fails the run loudly instead of faking success.

Prompts tell each agent it must call a tool every turn and finish only when
done. Tests drive weak-model stubs that act then finish, proving a full team
forms rather than a single gesture.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 22:17:49 -04:00
ZacharyZhang-NYandClaude Opus 4.8 ef9ee463c9 Force agents to act so weak models can't complete a no-op
The whole premise is cheap models + skills + a team — so the control plane,
not the model, must guarantee action. Every adapter hardcoded
tool_choice:"auto", letting a weak model answer the kickoff with prose and
"succeed" having hired no one, delegated nothing, and said nothing.

Now the agent loop compels a tool call on the opening turn (tool_choice
plumbed through every backend: OpenAI/Responses/Chat, Anthropic, Codex,
Copilot, Azure, Custom, Gemini ANY mode, Bedrock any). If the forced turn
still yields no tool call, the run fails loudly instead of recording a
deceptive success — surfacing the problem (let it crash) rather than a
silent dead company. After it has acted, the model chooses freely.

Tested: a stub that rambles on "auto" but acts when forced now posts a
kickoff message; a prose-only first turn fails the run.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 21:43:49 -04:00
ZacharyZhang-NY de9e6a9fa3 Push live updates to the UI while agent runs execute
Engine-side writes - hires, delegated tasks, chat messages, run state -
only reached the renderer after a run finished: the queue worker sent
one snapshot-changed at the end, and nothing fired at claim time, so
the window sat frozen for the minutes a CEO run takes unless the user
reloaded by hand. The execution service now takes a notifyDataChanged
callback, the agent loop fires it after every tool-bearing iteration,
and the worker announces the claim itself, so the UI follows the
company working in real time.
2026-06-09 21:51:29 -04:00
ZacharyZhang-NY 921c5b4456 Harden recovery and department paths after adversarial review
Interrupted runs now return to the queue instead of failing - an app
shutdown is not a model error and must not spend the task's bounded
retry budget. Startup work moves behind the single-instance lock so a
losing second instance can never run recovery against the live
instance's database. Background provider requests that died before
receiving an external response id now fail at boot (the background
worker can only resume accepted ones). Agent updates resolve the
department through the same canonical lookup as hires, replacing the
free-text write and its invented "General" default.
2026-06-09 21:36:18 -04:00
ZacharyZhang-NY b4e17cbd35 Fail runs whose first turn carries no text and no tool calls
A reasoning-heavy model can burn the whole max_tokens budget on
thinking blocks; the parsed turn then has neither text nor tool calls
and the loop treated it as a successful completion, marking a run done
when nothing happened. Such a first turn now fails the run so the
failure is visible and the heartbeat retries; later empty-text turns
after real tool work still complete normally.
2026-06-09 21:22:44 -04:00
ZacharyZhang-NY d6d6656428 Make the hire_agent department contract explicit
The CEO's first run hired five reports into departments "general" and
"engineering"; the hiring-policy lookup was exact-match against the
seeded names (Executive, Engineering, ...), so every hire failed and
the kickoff produced nothing. The tool schema now enumerates the
canonical departments, the policy lookup matches case-insensitively
and stores the canonical name on the agent, and an unknown department
error lists the valid choices so the model can self-correct.
2026-06-09 21:21:01 -04:00
ZacharyZhang-NY 2dd4f3976a Enforce deadlines across the whole provider request lifecycle
The abort timer was cleared as soon as fetch resolved with headers, so
a server that stalled the response body hung the request forever - and
one hung run froze the autonomous queue behind the worker re-entrancy
guard. JSON requests now carry a header deadline plus a longer body
deadline on one abort controller, and event streams fail when no chunk
arrives within an idle window, cancelling the reader. Timeouts surface
as ProviderHttpError so the run records a clear failure and the
heartbeat retries.
2026-06-09 21:18:51 -04:00
ZacharyZhang-NY f15ed8cb6f Recover interrupted runs and requests at boot
A task_run still marked running at startup has no executor - the
single-instance app that owned it is gone - yet nothing failed it, the
queue worker only claims queued rows, and the heartbeat excludes tasks
with a running run, so one interrupted shutdown wedged the company
forever. Boot now fails orphaned running runs and their in-flight
foreground provider requests (auditably), which hands the task back to
the heartbeat for an automatic retry. Background provider requests are
untouched; the background worker resumes those by external id.
2026-06-09 21:16:42 -04:00
ZacharyZhang-NY 4e47da4d6b Route goal kickoffs through task policy and revive waiting goals on hire
Adversarial review of the kickoff feature surfaced two gaps. First, the
kickoff task was inserted with raw SQL, so a workspace-bound kickoff
carried requires_filesystem without the execution approval every other
task receives - manual runs failed and review-mode orgs had no approval
trail. Kickoffs (in-app and onboarding) now go through createTask and
its policy gates. Second, a goal created before any agent existed
stayed inert forever; creating an agent now operationalizes every
active goal that has no task yet. Goal owners must be active agents:
an archived owner falls back to the top-level agent and the owner
dropdown only offers active agents.
2026-06-09 20:38:35 -04:00
ZacharyZhang-NY cfbe5e1c8e Split oversized modules along domain seams
Every source file is now under 500 lines. Task prompt construction
moves from task-execution-service into task-prompts; goal assignee
resolution moves from organization-service into goal-kickoff; the
shared command types become a domain-structured shared/commands
directory with unchanged import paths; the provider adapter contract
types move to provider-adapter-contract, re-exported through
provider-adapters so callers keep a single public surface.
2026-06-09 20:19:34 -04:00
ZacharyZhang-NY 3704cec19b Kick off goal execution when a goal is created
A goal created from the Work page only wrote a goals row; nothing ever
read it, so the company never started working on it. Creating a goal now
produces an "Operationalize goal" kickoff task for the chosen owner (or
the top-level agent when unowned) and re-enables that agent's heartbeat,
so the autonomous queue worker starts the run - the same activation
contract the previous runtime had via goal_activated wakes and CEO
bootstrap. Onboarding shares the same kickoff module instead of its own
copy.
2026-06-09 20:03:09 -04:00
ZacharyZhang-NY ed9b3a3dd1 Add meetings, sprints, and approval decision notes
Migration 042 creates meetings and sprints tables, adds decision_notes
to approvals. Full-stack: MeetingService, SprintService, IPC channels,
preload bridge, domain types. UI: MeetingsPage with scheduling and
type filtering, SprintsPage with progress tracking, ApprovalDetailDialog
with decision notes textarea.
2026-06-07 18:01:58 -04:00
ZacharyZhang-NY 75e5d17a86 Implement full task inspector with subtasks, activity, and inline edit
Migration 041 adds parent_task_id to tasks for subtask hierarchy.
TaskDetailDialog now has three tabs: Overview (description, assignee,
project, goal, workspace, runs with error messages), Subtasks (child
tasks with status), and Activity (audit events and approval history).
Inline edit overlay covers title, description, assignee, risk, priority,
and parent task. Task delete is available from the dialog footer.
2026-06-07 17:55:59 -04:00
ZacharyZhang-NY 7a36a12d08 Add create_document tool to agent runtime
Agents can now create versioned company documents (PRD, spec, memo,
report, runbook, proposal, policy, guide) via the create_document tool.
Documents are persisted in the documents table and visible in the
Documents page.
2026-06-07 17:51:12 -04:00
ZacharyZhang-NY b5b8f3ba3c Add document, knowledge, and automation subsystems
Migration 040 creates documents, knowledge_entries, and automation_rules
tables. Full-stack services (DocumentService, KnowledgeService,
AutomationService), IPC handlers, preload bridge, desktop-api types,
and domain records wired into the snapshot. UI pages follow next.
2026-06-07 17:46:55 -04:00