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.
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.
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.
Tests verify: canonical handle generation for multi-word names, mention
resolution via canonical handles, read_messages from channel, and
read_messages inbox (agent @mentions across channels).
Each agent sits at a workstation with a monitor, status-colored glow for
active agents, and a rich tooltip showing name, role, and current task.
Departments render as labeled rooms with subtle borders. The monitor
lights up when the agent has a running task run, giving a real sense of
who is working and who is idle.
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.
Settings > Tools tab lets the user enter their Composio API key and
select which toolkits (GitHub, Slack, Gmail, etc.) to enable for agents.
The configuration is saved per-org and loaded at agent run time.
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.
A collapsible right sidebar renders agent presence as a visual office
floor plan grouped by department. Each agent shows at their desk with a
status ring (active/thinking/busy/idle) and a tooltip revealing their
current task on hover. Toggle via the Office button in the left nav.
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.
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.
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.
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.
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.
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.
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.
The policy fell back to default-src for scripts and allowed
'unsafe-inline', so any future injection sink would run with the
renderer's full contextBridge authority. The page has no inline scripts
(one external module), so split the directives: script-src 'self' with
no inline, style-src keeps 'unsafe-inline' for React inline styles, and
add object-src/base-uri/frame-src 'none'. localhost stays only on
script/connect for the dev server. Verified the production build still
loads and renders end to end.
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.
Bodies were printed verbatim, so an agent's **bold**, `code`, or
[link](https://...) showed as literal punctuation — the opposite of
people talking. Render the inline markdown agents and humans actually
use in chat into React nodes, composed around the existing @mention
highlighting.
Links are restricted to http(s) and nothing is ever set as raw HTML
(no dangerouslySetInnerHTML), so a message body can never inject markup
or a javascript: URL. A plain message still comes back as one string.
The thread panel rendered an "Audit" feed — "Chat message sent", "Task
run completed", "Approval requested" rows with monospace timestamps —
a server log sitting inside a conversation. Attachments showed an eight-
character SHA-256 fragment next to the file type. Neither belongs in a
surface a non-technical operator reads as people talking.
Remove the audit replay from the chat thread (and its now-dead
replayEventsForThread/auditPayload plumbing) and drop the checksum from
the attachment card. The audit trail itself is unchanged — it is still
recorded and still shown on the task activity tab. The e2e thread check
now asserts the reply box, not the log.
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.
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.
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.
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.
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.
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>
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>
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>
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>
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>
Sweep the remaining inline type across every page onto the system scale:
oversized metric numbers (22/24px) down to the 18px title step, body text
13px → 14px, captions/meta 11px and 12.5px → 12px, and small control icons
13/11px → 16/14px. No structural or layout changes — the foundation
already gridded spacing, radius, and color; this finishes the type so the
whole app reads from one ramp. Full interactive e2e and the overflow
guards stay green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add CascadeMenu/CascadeRow: a menu whose rows reveal a side submenu on
hover with an open delay, a forgiving close delay, and a cursor→submenu
safe triangle — while the cursor sweeps toward an open submenu, hovers
over sibling rows are ignored, so a natural diagonal move never snaps the
target shut. The triangle geometry (pointInTriangle) is unit-tested.
Apply it to the org switcher: each organization reveals its actions
(switch / delete) on hover instead of a hidden hover-only "x", while the
destructive delete keeps its deliberate, non-hover-dismissible confirm.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The first-run flow still carried its own sizes: a 34px hero, 13/12.5px
intros and helper text, 13px control icons. Restrain the hero to the 24px
display step and move intros/errors to 14px, helper text to 13px, and the
provider/workspace icons to the 16–18px standard. Structure was already
clean (two-pane, single cards) — this is purely scale alignment.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Audit of the "agents barely communicate / nothing runs" reports: the
bootstrap run, hiring, delegation, channel messages, file handoffs, and
heartbeat continuation are already covered by autonomous-run.test. The one
untested link was the round trip — an @mention actually waking a teammate
who then replies. This test drives a scripted CEO that hires an engineer
and @mentions them in #engineering, asserts the mention created a queued
chat_mention run for that engineer, executes it, and confirms the engineer
replies inside the same thread (two senders, one thread). The orchestration
is sound; real-world silence is a function of the model's tool-use, not the
control plane.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "Spaces" section badged each department with its agent count, so an
org with one CEO and zero files showed "Executive 1". Clicking it filtered
to that department's files and revealed nothing — the tree advertised a
folder that opened empty. collectDepartments now counts files per
department and omits departments with none, so every badge equals what the
section contains. Unit-tested for file-based counting and empty omission.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The run rows used a .nav-row class that was never defined, so the button
had no width or flex constraint: long run titles pushed the row past the
340px pane and forced a horizontal scrollbar with the title clipped.
Define .nav-row as a constrained selectable row (width:100%, min-width:0)
so the inner title ellipsizes in place. Align the pane and detail
typography to the 14/12 scale while here.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The home page was 13 cards deep with empty roll-ups (Today, In progress,
Team duplicated other pages) and a stark full-black goal card that fought
the rest of the surface. Now:
- Three intent cards lead — Quarter goal (light, integrated), Monthly
budget, and Waiting-on-you with a real "all caught up" empty state.
- A four-up metric strip (workload, run activity, success rate, token
usage) replaces the redundant budget chart. Each tile lifts on hover
and its header shows a live readout of the hovered datum.
- Activity (agent runs, recent messages) closes the page.
No data wiring or approval logic changed; HomePage drops 500 → 344 lines.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The flat 13-item list read as clutter and reused icons (Team/Meetings,
Work/Sprints, Inbox/Approvals all shared a glyph). Now:
- Primary group leads with the core loop: Home, Work, Team, Messages,
Runs, Files, Approvals.
- A divider demotes the occasional surfaces: Inbox, Documents,
Knowledge, Meetings, Sprints, Social — still one click away, nothing
orphaned (search can't route to several of them).
- Distinct calendar/target/inbox/folder icons end the collisions.
- Every nav glyph is the 18px icon bound in a 24px box; the collapsed
rail centers icons instead of hiding them.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Establish one token system across the shell and primitives:
- Typography: vendor Geist (variable woff2, self-hosted, CSP-clean);
body 14/16px, title 18px, captions 12px; drop oversized headings.
- Color: primary #181818, secondary #6A6A6A over a pure-neutral ramp.
- Spacing: strict 4px grid via --sp-* tokens.
- Radius: 8 → 40 scale (--r-sm..--r-2xl, --r-full).
- Icons: 18px glyph, 1.5 stroke, 24px bound by default.
Sweeps tokens through styles.css, components.css, and overlays.css so
buttons, inputs, cards, pills, menus, the sidebar, topbar, and page
titles all read from the same scale. No page logic touched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The rollback pack canonicalizes the source path via realpathSync.native
(correct: it resolves symlinks before comparing). On macOS tmpdir() is
/var → /private/var, so the test's raw legacyPath never matched. Compare
against the canonical path instead.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Every interactive control now acknowledges input: buttons and icon
buttons press down (scale on :active) and ease their hover states, nav
items, menu rows, segmented controls, cards, table rows, and inputs
transition over shared motion tokens (120/200/280ms, ease-out curves).
Dialogs and popovers enter with a short rise + fade over a fading
backdrop; switching routes and onboarding steps plays a quiet view
entrance keyed on the route/step. State pills cross-fade their tones
as runs progress. All motion collapses under prefers-reduced-motion.
PRODUCT.md captures the register and design principles guiding this.
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.