All messages already render chronologically in the channel pane, so the
thread affordance only duplicated them behind a second view. The reply
chip, the in-place thread pane, and the thread composer are gone;
ChatThreadView is deleted and the thread props are removed from the
conversation, timeline, and message-row chain. Message writes are
untouched.
Root cause found by probing the live API: Composio v3 rejects
POST /connected_accounts for Composio-managed OAuth auth configs with a
400 pointing at POST /connected_accounts/link, and the renderer threw
the failure away inside a .then, so clicking Connect did nothing and no
browser opened.
- createConnectedAccount now calls POST /connected_accounts/link with
auth_config_id and user_id and reads connected_account_id and
redirect_url; the local mirror records INITIALIZING until the poll
adopts Composio's real status.
- Connect failures (and a blocked external-URL open) now show as a
single line on the page instead of vanishing.
- Discover skill directories under ~/.agents/skills and each
workspace''s .agents/skills; a directory qualifies with a full
skill.manifest.json or just a SKILL.md, whose manifest is derived
(frontmatter name or the directory name). Paths resolve per platform
on Windows, macOS, and Linux.
- Sync at startup for every organization and again when the Skills
page opens; skills the org already carries are skipped, and one
broken directory cannot block the rest.
- AGENTCOMPANY_NEW_SKILLS_ROOT points tests at a temporary library
(overriding USERPROFILE crashes Electron on Windows).
- Resolve the API key from cred.txt at the project root in development
and the packaged resources directory in production; the file is the
source of truth, so a rotated key replaces the stored one. Fixes the
401 packaged builds hit when a stale database key won.
- Remove the user-facing Composio key editor; the key ships with the
product and is not configurable.
- Remove the Migration console from Settings.
- Remove the Connectors and Home refresh buttons; connector statuses
sync when the page opens.
Documents duplicated what Files surfaces; the nav item, route, and page go.
Agent-authored documents keep flowing through the document service,
deliverable review, and search.
The credential file only seeded the database when it was empty, so once a
key was stored, rotating it in cred.txt changed nothing and Composio kept
rejecting the dead stored key with 401 - while the file held a freshly
generated, verified-working key. The file is now the source of truth
whenever it exists: a differing key replaces the stored one on resolution,
and packaged installs without the file keep managing the key in Settings.
Four resolution contract tests pin seed, rotation, settings-only, and
absent cases; the Connectors error line truncates instead of wrapping the
raw API response across the page.
The right-hand thread panel repeated exactly what the flat timeline already
showed. Opening a thread now focuses the center pane itself: back control,
board state, the root message with its replies, board intervention
controls, and a thread reply composer - one conversation surface, never
two. The release contract also catches up with the icon shipping inside
the package, a pin the icon commit should have updated.
The legacy application - its src, tests, landing site, promo assets, docs,
release tooling, CI workflows, and root configs - leaves the tree; it stays
reachable in history. Everything under AgentCompany-New moves up unchanged:
same package name, so the production userData/database path is untouched,
and e2e/build scripts already resolved paths relative to the project. The
developer cred.txt stays gitignored at its new location. Full verify:ci
passes from the new root.
Both servers expose OpenAI-compatible /v1/chat/completions and /v1/models,
so we route them through pi-ai's `openai-completions` API the same way
Fireworks does — but with a per-installation base URL the user enters
during onboarding instead of a hardcoded one. The shared schemas grow a
`local` credential payload carrying just the baseUrl; the credential
resolver surfaces it on `ResolvedCredential.baseUrl`, the runtime bridge
forwards it through `prepareRun`, and the worker plumbs it into
`resolveModel` so the streamed Model<openai-completions> targets the
user's server. The local-cost path short-circuits to zero in cost-writer
since users bill their own hardware, and the live model list is fetched
from the saved baseUrl by `fetchLocalConnectorModels` (Ollama and LM
Studio both speak the standard OpenAI catalog shape). Onboarding ships
a new LocalProviderCard and the Settings panel a LocalForm; both default
to the catalog's per-connector localhost URL and accept any custom URL.
The previous toast pasted result.error.message straight into the UI,
so a failed open rendered
"Couldn't open CEO-WEEK2-DAY1-SUMMARY.md: workspace_id=null
task_id=null run_id=61669170-180b-4cb3-99d2-eb925ca9717c — none led
to a workspace with a local_path"
to the operator. That text is a developer log line, not user feedback.
Drop the passthrough — the toast now simply reads
"Couldn't open <filename>"
and the diagnostic details live only in the IPC return payload (which
a devtools or automated caller can still read). The resolver's five
reason codes and their detail strings remain intact in the main
process; they just no longer surface to the UI.
Clicking a Files & Deliverables row fired openWorkProduct and then
discarded the result with \`void\`. When the main process could not
resolve the target — archived work product, missing workspace root,
path outside the managed root, or \`shell.openPath\` reporting no
associated app for the file type — the renderer stayed silent and
the row looked like a dead button. The user had no way to know why
"clicking" did nothing.
Await the result in WorkProductRow and route non-ok responses through
addToast with tone="danger". The caller sees the underlying failure
(OPEN_PATH_FORBIDDEN / OPEN_PATH_FAILED / WORK_PRODUCT_NOT_FOUND /
the shell's own message) and can act on it. No backend change — the
handler already returned structured errors; this just stops dropping
them.
Typecheck and build:verify clean.
The previous commit 86e9c21 only caught the heartbeat icon color —
the main block of edits silently failed to match (comment block
broke the exact-string diff). This commit lands the full cleanup
that commit 86e9c21 claimed:
- Drop the rounded-t [4px bg-primary | 2px bg-border] rank bar.
- Drop the isRunning border-[color:var(--primary)] override and the
success-tinted box-shadow glow.
- Switch hover from shadow-lg + translate-y to hover:bg-muted/30.
- Switch active/running avatar circle from bg-success-muted /
text-success to bg-accent / text-primary.
- Move avatar initials from font-bold to font-semibold.
- Drop the isRoot / stale rank-bar comment block.
Error and pending states keep their destructive borders.
Typecheck and build:verify clean.
Remove the ink-blue top rank bar, running-state border override, and
green glow shadow from org chart cards. The tree layout already
communicates rank by position, and a dashed ink-blue connection line
plus the bottom-right status dot + ping animation already mark
running agents — duplicating the signal through a card-wide color
band and a cyan/green shadow was pure decoration.
- Drop the rounded-t [4px bg-primary | 2px bg-border] top bar entirely.
- Running state stops overriding border to var(--primary); all running
cards fall back to the neutral border-border like everyone else.
- Running state stops emitting a success-tinted glow (shadow-[0_0_12px
color-mix(... var(--success) 15% ...)]) — kami rule 8 bans drop
shadows, and the tint introduced an unauthorized green.
- Hover switches from shadow-lg + translate-y-0.5 to a subtle
hover:bg-muted/30 tint.
- Active/running avatar circle switches from bg-success-muted
text-success to bg-accent text-primary (kami's ink-blue tint),
removing the last green chrome on the page.
- Heartbeat icon switches from text-success to text-primary.
- Avatar initials move from font-bold (700) to font-semibold (600).
Error / pending_approval keep their destructive border (red), since
kami reserves the warm terra red for genuine error/destructive state
per design.md color system.
Typecheck and build:verify clean.
The kanban board, performance metric cards, workspace list, and wait
banner all leaked second and third chromatic hues into the UI: red
bars on critical tasks, amber on in-review columns, green on done /
running / selected workspace rows, colored icon tiles and colored
headline values on metric cards. kami rule 2 allows one accent (ink
blue) at no more than 5% of the surface; these surfaces were spending
30-60%.
KanbanBoard.tsx
- Drop the tone/dotColor fields from KANBAN_COLUMNS; column headers
are plain eyebrows, no colored status dot.
- Delete priorityColors map and border-l-[3px] on cards; cards are
flat bordered rectangles. Priority order still drives the sort,
but stops painting the chrome.
- CRITICAL/HIGH small-caps label routes through text-primary (ink
blue) instead of text-destructive / text-warning.
- "Running" status dot + text switches from text-success / bg-success
to text-primary / bg-primary.
- Hover changes from hover:shadow-sm (banned by kami rule 8) to
hover:bg-muted/40.
MetricsGrid.tsx + Dashboard.tsx
- Drop the tone prop and its toneMap / bgMap / borderMap trio. A
MetricCard renders one way: ivory card, ink-blue-tinted icon tile
(bg-accent), ink-blue glyph, near-black value. No left border
color stripe, no hover shadow, no translate-y.
- Dashboard.tsx stops passing tone on the four performance cards.
Semantics that used to live in tone (pending approvals warn, all
agents running success) are intentionally dropped; those
affordances can come back through typography or a subtle indicator
when kami allows, not through a second color.
WorkspaceList.tsx / WorkspaceInspector.tsx
- Replace every border-l / text / group-hover reference to var(--success)
with var(--primary). Selected + hover states now carry kami's ink
blue signal; the three "X count" spans that used text-success as
a decorative pop become text-primary for the same reason.
WaitStateIndicator.tsx
- The wait banner (wait-for-reply / approval) flips from warning
amber (border-warning, bg-warning-muted, text-warning, bg-warning
button) to primary ink blue across border / bg / text / button.
The banner still reads as an attention signal — the icon shape
(Hourglass) carries the semantic, color no longer has to shout.
feedback.tsx (Alert) and Toast.tsx retain their tone-based colors:
they are accessibility-critical transient state surfaces where a
single ink-blue lane would collapse success / warn / danger into an
undifferentiated stack. They are out of scope for this pass.
Typecheck and build:verify clean. Grep confirms no remaining
border-l-[color:var(--success|warning|destructive|caution)] in
dashboard or execution modules.
The Inbox rendered every row as a heavy card: a 36x36 solid-fill icon
tile in ink-blue / destructive / warm-silver, a rounded border, a 3pt
red left stripe on urgent items, and a hover that fired a drop shadow
plus translate-y. Six loud signals, all fighting for the reader, all
breaking kami rules (no pure-hue tiles, no red accents outside
destructive buttons, no hard drop shadows, no unnecessary chrome).
Per Jony + Jobs consensus the surface collapses to one quiet canvas:
- 36x36 color tile -> removed. Icon is an 18px inline glyph, single
ink-blue stroke for every type. Type meaning lives in the glyph
shape (ShieldCheck / XCircle / Eye / ChatCircle / ...), not in
background color.
- 3pt red severity stripe -> removed. Urgency compresses to one
signal: a 5px ink-blue dot before the timestamp, visible only for
critical/high rows. Title also shifts from font-medium to
font-semibold at the same threshold so the row carries a little
more weight without shouting.
- Card chrome (border, rounded, bg-card) -> removed. Rows sit flat
on parchment, separated by a single border-b at --border/60
hairline. Reads like a printed contents page instead of a trello
board of cards.
- Hover shadow-md + -translate-y-px -> removed (kami rule 8 bans
hard drop shadows). Hover tints the row with bg-muted/40, which
is the kami whisper-shadow equivalent for a list item.
AttentionRow gains a variant prop ("card" | "line", default "card")
so Dashboard's three existing call-sites keep their card rendering
untouched. The "line" branch rejects iconTileBg / rowBgClass /
borderAccentClass overrides — the two variants are orthogonal, not a
half-mixed code path.
inbox-helpers.tsx sheds every token that became dead after the
rewrite: typeConfig.color (constant var(--primary) for every type
after Jony's unification), severityColors, severityBorderClass, and
the unused bg field. Inbox routes icon color through tone="info"
instead of passing the redundant iconColor per row.
Note: the page shows 6 duplicate "Escalation from Maya Rodriguez"
rows and 5 duplicate "Escalation from Taylor Park" rows. That is a
backend dedup bug (same entity + same severity pushed multiple
times), not a visual issue. Flag for a dedicated follow-up; do not
paper over it in the renderer.
Reviewed by Karpathy against /karpathy-guidelines:
- Surgical: pass
- No overcomplication: pass
- Variants orthogonal: pass
- Orphans cleared: pass
- API stability for Dashboard callers: pass
- request_changes item (typeConfig.color is a constant dressed as
data) addressed in this commit by dropping the field and routing
through tone="info".
Typecheck and build:verify clean.
The previous default tone (bg-muted text-muted-foreground) reads as
beige-on-beige in light mode — the "3" count on Work was barely
legible against the parchment sidebar.
Route default counts through bg-primary + text-primary-foreground so
the pill adopts kami's sole accent color. The three tones now fall
into a clean priority scale:
default -> ink blue (you have items)
warn -> warm amber (needs attention)
danger -> warm terra red (critical)
Contrast holds in both modes: ink-on-parchment in light (~9:1),
ink-light-on-dark-parchment in dark (~5:1). Warm amber and terra red
retain their own semantic foregrounds from the prior commit.
Typecheck and build:verify clean.
Badge text color was hard-coded to text-primary-foreground for every
tone, which renders as parchment light. Pairing light text with the
amber warning background (L=0.82) or the warm-silver muted background
(L=0.77) produced light-on-light collisions — the "9" on the active
Messages row and the "4" count on Work were nearly unreadable.
Route each tone through its own semantic foreground token:
danger -> bg-destructive text-destructive-foreground
warn -> bg-warning text-warning-foreground (dark text on amber)
default -> bg-muted text-muted-foreground
Also pull .dark --muted and --secondary off the --card lightness
(0.296 -> 0.360) so bg-muted is actually visible against the sidebar
card surface. This fixes not just the count pill but every hover/muted
fill across the app (toggle rest state, subtle button backgrounds, etc)
that previously vanished into the card.
Typecheck and build:verify clean.
On the Messages page three surfaces collided: the channel list column
and the channel header bar both used bg-card (lighter surface), while
the content area above and below used bg-background (deep). That made
the page read as a sandwich of alternating tones.
Route ChannelSidebar and ChannelHeader through bg-background so the
entire Messages canvas — title bar, channel list, header bar, and
conversation pane — shares the deep kami surface. The left app
SidebarView continues to use bg-card as the single elevated island,
preserving the apple-style "one navigation column floats on the
canvas" hierarchy.
Borders (border-r / border-b on those two panels) already provide the
structural separation, so no shadow is needed — consistent with kami's
rule 8 (ring or whisper shadow only).
Typecheck and build:verify clean.
The previous dark palette set chroma near zero (C=0.003) on hue 95, which
is technically "warm" but visually indistinguishable from neutral black
at low luminance. Screens rendered the surface as pure black, losing the
"暖橄榄底色" kami promises.
Raise chroma to 0.013-0.014 and shift hue from 95 (cool yellow) toward
80 (closer to olive/gold) so the warmth is perceivable. Lift the
background L from 0.156 to 0.172 so it sits one step off true black.
Separate card (0.296) from border (0.340) so elevation reads without
a hard drop shadow.
Specifically:
--kami-deep-dark/--kami-near-black 0.156 C0.003 H95 -> 0.172 C0.013 H80
--kami-dark-surface 0.283 C0.003 H95 -> 0.296 C0.014 H80
--kami-dark-warm 0.336 C0.004 H95 -> 0.350 C0.014 H80
--kami-border-dark 0.283 C0.003 H95 -> 0.340 C0.014 H80
--kami-warm-silver 0.739 C0.010 H95 -> 0.770 C0.015 H85
Dark surface / border / text tokens flow through the same shifts:
.dark --background / --sidebar 0.172 warm
.dark --card / --popover / --secondary / --muted / --sidebar-accent 0.296 warm
.dark --border / --input / --sidebar-border 0.340 warm (visible)
.dark --muted-foreground / --secondary-foreground / --sidebar-foreground
0.770 warm (readable timestamps/role labels)
.dark --accent 0.320 warm ink hint (selection glows ink)
Light mode --foreground / --card-foreground / --popover-foreground /
--warning-foreground inherit the new warmer near-black so body text on
parchment also carries the olive signature.
Typecheck and build:verify clean.
Extend the @layer base h1/h2/h3 rule so every application-level heading
inherits the kami serif stack at weight 500, matching design.md rule 4
(serif carries headline authority) and rule 5 (single serif weight is
the design signature). Prose headings inside .paperclip-markdown already
inherit the same stack via the prose rules added in the token rewrite.
Components that override font-family/font-weight on headings will
continue to win via CSS specificity; the base rule sets the kami default
for every heading that does not opt out.
Typecheck and build:verify clean.
Purge remaining hardcoded color values from the component layer and main
process so every surface flows through the kami token system.
src/main/desktop-shell-window.ts
- BrowserWindow.backgroundColor no longer paints pure white before the
renderer mounts. It now picks kami parchment (#F5F4ED) or deep-dark
(#141413) based on nativeTheme.shouldUseDarkColors so the startup
flash respects the user's OS theme preference.
src/shared/constants.ts
- DEPARTMENT_COLORS collapses 12 unrelated hex brand colors (violet,
red, green, cyan, pink, ...) into six kami tokens: var(--primary),
var(--kami-ink), var(--kami-dark-warm), var(--kami-charcoal),
var(--kami-olive), var(--kami-stone). Departments are distinguished
by warm-neutral ramp + ink-blue, never by second chromatic hue.
src/renderer/components/communication/{MessageItem,SearchResults}.tsx
- Replace the string-concat tint pattern `${color}18` with
color-mix(in oklab, ${color}, transparent 91%). The old pattern
produced invalid CSS (e.g. `var(--primary)18`) whenever `color`
was a CSS custom property, silently falling back to no background.
color-mix works for both hex and var() inputs, eliminating the
latent bug at the source.
src/renderer/components/ui/ActionButton.tsx
- Replace hand-rolled rgba(74,93,122,0.35) and rgba(220,53,69,0.3)
drop shadows with kami ring shadows routed through color-mix over
var(--primary) and var(--destructive).
src/renderer/views/CommandPaletteView.tsx
- Replace rgba(15,23,42,0.08) cold-slate shadow with a kami whisper
shadow driven by color-mix over var(--foreground).
src/renderer/components/skills/SkillAgentToggles.tsx
src/renderer/components/execution/ModelManagementShared.tsx
- Toggle thumbs switch from bg-white to bg-background so they
inherit parchment in light mode and deep-dark in dark mode,
removing the last pure-white surfaces from the renderer.
Third-party brand hex values in src/renderer/components/social/
social-config.ts are retained: they represent external brand identity
(Twitter blue, YouTube red, ...), not kami chrome, and collapsing them
to one hue would destroy recognition.
Typecheck clean. No business-logic signatures changed.
Rewrite styles.css token layer to enforce the kami design system across
light and dark modes:
- Replace grayscale oklch(L 0 0) palette with warm-parchment hue 95
neutrals and ink-blue hue 258/255 brand; zero cold blue-gray remains.
- Introduce --kami-* source-of-truth layer (parchment/ivory/warm-sand,
deep-dark/dark-surface, ink/ink-light, warm neutrals, borders, rings,
ink-blue tag tints) so future components reference semantic names.
- Keep shadcn aliases (--background/--primary/--card/...) so consumer
components require no className changes; kami is a drop-in under the
token layer.
- Reset radius-lg/xl from 0 to 0.5rem/0.75rem (kami 8pt/12pt); default
--radius 0 -> 0.5rem.
- Reset font stacks: serif -> Newsreader + Source Han Serif SC + fallbacks
for headlines/body; sans -> Inter + system + Source Han Sans for UI;
mono -> JetBrains Mono + Maple Mono + ui-monospace for code. Maple Mono
no longer occupies the sans-primary slot.
- Collapse chart-1..5 to a single ink-blue hue (258->252) with L ramp
per Jobs ruling; dark mode inverts the L order for contrast.
- Desaturate success/warning/caution to warm hues (145/75/50);
destructive retains warm terra red #B53333 (oklch(0.516 0.186 25)).
- Replace MDXEditor Catppuccin palette (#1e1e2e/#cdd6f4/#181825/
#585b70/#313244/#45475a/#89b4fa) with kami dark-surface + parchment
+ olive + ink-light equivalents.
- Route markdown link colors through var(--primary) via color-mix
instead of hardcoded #0969da/#58a6ff.
- Enforce serif 500 (not 700) on .paperclip-markdown h1-h4 per kami
rule 5 (single serif weight is the design signature).
CSP font-src is self/data only, so no CDN fonts are introduced; the
stacks rely on system-installed fonts with documented fallbacks.
Self-checks pass: no remaining oklch(L 0 0) grayscale, no Catppuccin or
GitHub link literals, no font-weight 700 on prose headings, no radius: 0
in @theme, Maple Mono only lives in the mono fallback chain.
The operator's mental model: a chat is a room, @X means "X takes the
turn", unmentioned participants keep working. Two separate gaps were
letting 张工程师 answer when the user wrote "@CEO 更新一下进度" inside
their Board↔张工程师 chat:
* `resolveMessageWakeTargets` always woke the DM recipient, regardless
of who the body @-mentioned. CEO woke too, but 张工程师 — the
addressed party — replied first and CEO's busy-queued wake arrived
after the operator had already taken the earlier reply as "the
answer". Explicit mentions now shadow the implicit DM recipient:
with mentions, only the named agents wake; the recipient still sees
the message in their inbox, they just aren't obliged to answer.
No mentions → fall back to the historical "DM wakes the recipient"
contract so every existing flow is untouched.
* `channel-utils` keyed each chat by `sort(fromAgent, toAgent)`. CEO's
reply with `toAgentId=__board__, parentMessageId=<original>` landed
in a fresh Board↔CEO pane — the exact "new session" the operator
complained about. Walk `parentMessageId` back to the root of the
reply chain and use the root's participant pair: every reply in a
thread now collapses into the chat the sender originally opened,
cycle-guarded against malformed parent pointers.
Pinned by a new unit test: DM to CTO with "@CEO 更新一下进度。" wakes
only CEO while keeping the message visible to both CTO's and CEO's
inbox.
Two small but user-visible UX issues in the Communication hub:
1. selectedEntityId was only consumed when the id resolved to a
visible message / comment / task channel. When the referenced
entity was deleted or filtered out, the pointer stuck and the
effect re-ran on every store change, preventing the user from
manually navigating to other channels.
2. The unread-marking effect explicitly skipped while search was
active, even though the search results render in the same message
list pane. Users who opened a channel via search saw unread
badges freeze until they cleared the query and re-entered the
channel.
Consume the entity id on every resolution branch (including the
no-match fallback) and drop the search guard — the viewer intent is
the same in both list modes.
Both the prose-documents and work-products sections rendered as
`{x.length > 0 ? ... : null}` with no else branch, and the whole
DocumentCenter was only mounted when a company was selected. The
combined result: a company with no files gave the operator a page
containing only a title and a "New" button, and an operator who had
not yet picked a company saw nothing at all — the exact "page is blank
even though docs were generated" complaint.
Add an explicit empty state inside DocumentCenter (surfaced only when
both collections are empty) and a separate "select a company" empty
state at the section router level. Strings live in the i18n bundle so
the Chinese locale is not left behind.
Filesystem work products store `external_id` as a workspace-relative
path so artifacts survive a workspace-root move, but the renderer was
handing that relative fragment straight to `shell.openPath`, which
needs an absolute managed path. Clicks flickered into a failed open.
Add a dedicated `openWorkProduct({ companyId, id })` IPC backed by
`resolveWorkProductTarget` on the DB layer. The resolver joins the
work-product row back to its originating task → workspace (or run →
workspace for taskless runs), composes an absolute path, and rejects
any relative fragment that would escape the workspace root. The
renderer now passes the work product's id — path resolution stays on
the main process where the managed-path policy already lives.
The Document Center only read the `documents` table, which is prose
content POSTed via /api/documents. Everything the agents wrote via
`bash` / `write` / `edit` — the bulk of their output on a real
project — landed in `work_products` and never appeared on the page.
Operators saw "0 items" after a full night's run.
Lift work_products into the same UI:
- `listWorkProductsForCompany` returns a newest-first slice with a
500-row cap so the snapshot stays bounded.
- `ProfileSnapshot.workProducts` flows through `useCompanyData` as
`companyWorkProducts`, filtered to the current company.
- `DocumentCenter` renders two labelled sections in order — prose
documents (editable) on top, files & deliverables below. Each
work-product row opens the underlying URL or filesystem path
when clickable, so the agent's bash-generated markdown, PRs,
and artifacts are one click away.
`WorkProductRecord` lifts into `@shared/types/records-extended` so
the renderer, snapshot, and database shells all reference the same
shape. The store-test fixture learns about the new field.
One flat list of `status = in_review` tasks couldn't tell the
operator whether the next decision belonged to the board (approval
pending), to the human user (top-level assignee or explicit
`requires_user_review`), or to a middle manager walking up the
`reports_to` chain. Three very different escalation paths rendered
identically, so "reviewed by X" was the only signal and operators
missed the ones actually waiting on them.
`PendingReviewList` now classifies each in-review task into one of
three kinds (`board` / `user` / `manager`) using the pending
approval set plus the existing assignee/manager lookup. Rendering
groups them under labelled sub-headings in priority order (board
first — the operator is the bottleneck there), each with its own
awaiting-text so the row's status is unambiguous at a glance.
The component accepts approvals via prop; the caller passes the
existing `pendingApprovals` it already holds. Translations for both
locales carry the new group labels.
The thread-panel composer was pinned to `channel="direct"` with no
`directParticipantIds` and no `replyTo`. Clicking reply in a human
review thread sent a message with no recipient and the send button
stayed disabled — a silent dead-end for the entire Co-Founder
review UX.
The fix exposes the thread's participant list to the renderer:
- `listThreadParticipantsSchema` and the matching IPC channel
(`agent-company:list-thread-participants`) return the rows from
`conversation_participants` for a given thread.
- `threadIntegration.onTaskCreated` / `onApprovalCreated` already
enrol the owning agent and the virtual user on thread creation,
so the list is populated without any backfill.
- `ThreadPanel` loads participants alongside events and forwards
the agent ids to `MessageComposer.directParticipantIds`. The
composer's existing `directRecipientId` memo picks the first
non-board agent, so replies now target the correct agent — or
surface the "no recipient" notice when the thread has none.
The Board/user member is deliberately excluded from the list the
composer consumes: the composer wants an agent to route to. All
other wiring (wait-state precision, threadId) stays untouched.
Full-codebase ts-prune + manual audit sweep. Every change below is a pure
deletion (or a 2-line re-wire where a dead function was supposed to be
alive). No scope additions.
Dead wrappers / phantom features
- shouldAutoApproveApproval: zero-value wrapper over shouldAutoApprove.
Four call sites now import shouldAutoApprove from execution-policy
directly.
- evaluateRotation(session, connectorId, thresholdsOverride): the
connectorId arg was never read (underscored), and thresholdsOverride
was only exercised by the test suite — no production caller. Signature
collapses to evaluateRotation(session). DEFAULT_THRESHOLDS /
RotationThresholds exports and their tests go with it.
- installLogRedactor: defensive console wrapper that was never wired
from worker-entry. buildRedactor handles the known threat (env-value
leaks); the pattern-based redactTokens stays for tests and future
composition.
Dead files
- resume-interrupted-tasks.ts: helper function never imported.
- service-graph-orchestration-deps.ts: three factory closures
superseded by the inline closures in create-orchestration-service.
Dead exports
- listConnectorsByAuthKind (shared/connector-catalog)
- getLocalePromptInstruction + whole localization.ts file
- ImportantDocType
- main-side ConnectorAuthKind (duplicated the shared one; not referenced)
- CheckpointEngineService, RunHousekeepingService types
- WorkspaceResolution type
- cmdOrCtrlSymbol
- AgentEventPush, CredentialPayload type aliases
- parseApprovalPayloadJson, parseEnvValue, ProviderCard
- getSOP — buildSOPSection reads the SOPS map directly; the individual
getter had no callers. Deduped to a flat array.
- renderWakeReason in wake-reasons/en.ts: byte-for-byte duplicate of the
one in wake-reasons/index.ts.
Forgotten wiring (not cleanup, but adjacent)
- registerAgentStandupRoute: the GET /api/agents/me/standup endpoint is
advertised in every system prompt and wake-reason doc, but the route
handler was never registered in api-server/index.ts. Agents have been
hitting 404 since Pi Mono. Registered.
Dead DB column
- v44 migration drops runs.context_snapshot_json. Column was added by
v16-v25, never written or read, and v43's deliberate preservation
was an interim measure. Same idempotent table-rebuild pattern as
v41/v43.
Test tree
- Phantom-feature threshold override tests removed (3). The rest of
the rotation suite re-expressed against the real maxRuns=20 floor.
495/495 unit tests pass. typecheck + build clean.
The CLI-adapter era `connector.command` field became vestigial once every
provider dispatch went through pi-ai. The schema still required a non-empty
command, so onboarding saveConnector silently rejected every Pi Mono row
(which carries an empty default). Remove the field from the payload chain
(renderer → IPC schema → DB CRUD → run dispatch → worker job), narrow
RunJob.taskId to match EnqueueRunPayload, and drop the dead helper plus
its test. Schema column stays NOT NULL for DB compatibility; seedConnectors
writes a literal empty string so no migration is needed.
The per-connector "enabled/disabled model" settings lived under a
global key in the settings key-value table
(`disabled_models:${connectorId}`). No company dimension. Deleting a
company left the pool choices behind, and creating a new company
silently inherited them — the visible symptom was "onboarding's team-
model configuration survives company deletion and pollutes the next
company's CEO model list."
Root fix, not a cleanup hook: the storage becomes a real relational
table with `(company_id, connector_id)` primary key and an
`ON DELETE CASCADE` FK from `companies`. Company deletion wipes pool
config by construction; no cleanup step in `deleteCompany()` can
drift.
Schema (migration v42):
- CREATE TABLE connector_model_pool_settings (company_id, connector_id,
disabled_models_json, updated_at), PK (company_id, connector_id),
FK companies(id) ON DELETE CASCADE.
- Drop every `settings.disabled_models:*` key — these were the bug's
residue; there is no right company to attribute them to, so the
migration clears them. Users re-select on next onboarding.
API surgery — every reader/writer now takes companyId:
- db.getDisabledModels(companyId, connectorId)
- db.setDisabledModels(companyId, connectorId, models)
- db.hasModelPoolSettings(companyId, connectorId)
- connector-model-policy: getConnectorModelPolicy(deps, companyId,
connectorId); validateConnectorModelSelection(deps, companyId,
connectorId, model)
- model-dispatch: listEnabledModels(companyId, deps);
pickModelForAgent resolves companyId from agent.companyId
- orchestration heartbeat/task-runner/turn-context/run-dispatch pass
the agent's or run's companyId through to the policy deps
- IPC schema: connectorDisabledModelsSchema gains companyId; new
connectorModelPoolQuerySchema pairs companyId+connectorId for reads
Onboarding becomes commit-on-launch:
- useConnectorModelSelection no longer persists via IPC during
onboarding. Toggles mutate local state; it exposes `poolSelections`
to the wizard.
- bootstrapOnboardingSchema gains connectorPoolSelections. The
onboarding service saves the company first, then persists each
per-connector disabled list scoped to the new company id — CEO
model validation runs against the company's freshly-persisted pool,
not some stale global.
- IPC setConnectorDisabledModels now requires companyId; the only
consumer was onboarding, which no longer calls it at all.
- Main-side listConnectorModels returns the raw provider catalog
(no pool filtering), since that IPC is used from onboarding where
no company exists yet; pool filtering happens at dispatch time.
saveConnector IPC drops the per-company pool validation — connector
config is a global hint, company-scoped enforcement belongs at run
dispatch where the company is known.
Tests: 603/603 green. Typecheck clean. Integration tests rewritten
to set the pool via connectorPoolSelections on bootstrap input, not
a pre-bootstrap global setDisabledModels call.