The commit ritual persisted first and reduced second, so an illegal
append (e.g. a misbehaving tool reporting progress after its terminal
result) became durable before the reducer rejected it — permanently
corrupting the turn file and, through context references, blocking
every later turn in the session. Reduce the batch against the in-memory
history first: illegal batches now reject in memory for their caller
only, the file stays legal, and a failed commit no longer poisons
this.events for subsequent appends in the same invocation.
Adds a test that fires a stashed reportProgress after the tool's result
is durable (while a sibling keeps the invocation alive) and asserts the
late append rejects, nothing illegal reaches the file/stream/bus, and
the turn completes and stays re-advanceable. Updates the design doc
(§4.5, §24) to the reduce → persist → stream order.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Phase B docs pass introduced two errors in VIDEO_MODE.md's prompt
catalog (composer still at the pre-move agents/ path; frame-context
pointed at legacy/engine.ts when convertFromMessages lives in
runtime/assembly/message-encoding.ts) and missed ANALYTICS.md entirely
(copilot_chat emit point still cited agents/runtime.ts:1313, file_parse
still cited the deleted builtin-tools.ts monolith).
turn-runtime-design.md referenced the repo-root AGENTS.md, which is not
committed — the carve-out is now stated inline. Also drops a leftover
debug comment in skills/index.ts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
getErrorDetails was the last live import out of runtime/legacy/ that
isn't part of the code-mode / mini-apps carve-out — four knowledge
pipelines pulled it from legacy/utils.ts, blocking 'legacy deletes as
a unit'. Their agents run via the headless runner now, so the
RunFailedError unwrap branch was dead for them; the live version in
application/lib/errors.ts is the plain Error->message idiom, and the
legacy copy (with the RunFailedError coupling) is deleted as
consumerless.
isPathInside backs path-grant permission decisions, and byte-identical
copies lived in filesystem/files.ts, assembly/permission-metadata.ts,
and legacy/repo.ts — a place for a future divergence to become a
permission bypass in exactly one caller. files.ts now exports the one
live copy; permission-metadata imports it (it already imports files.ts,
so no new edge). legacy/repo.ts deliberately keeps its frozen private
copy: the quarantine must not import live modules.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
getComposioToolsPrompt still called the raw composio client's
isConfigured; everything else (skill catalog availability, the other
prompt connection blocks) goes through assembly/connections.ts, whose
check wraps in try/catch -> false. On an errored config read the two
could diverge: the prompt advertises Composio tools while the catalog
hides the composio skill the model would need to use them.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
tools/paths.ts' lenient expandHome passed blank input through, and
both call sites feed the result to path.resolve — so a model calling
code_agent_run with cwd: '' spawned the coding agent in the Electron
process cwd, and resolveCodeProject('') could register that cwd (a
real, existing directory) as a code project.
The 'lenient variant' turns out to be filesystem/files.ts'
expandHomePath minus exactly this guard, so this deletes tools/
paths.ts and exports the filesystem one instead of keeping a third
variant. A blank path now surfaces as a clear tool error ('Path is
required') the model can react to. Also drops the stray
resolveCodeProject comment the extraction left in paths.ts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
withActiveSkills injected carried-forward skills into every non-inline
agent request, while the resolver only honors them for agents with the
skillCarryForward trait (real-agent-resolver). The mismatch persisted
an ever-growing activeSkills list into the requested composition of
every turn for agents that would never use it.
The gate now lives in withActiveSkills, mirroring the resolver. That
required a structural fix: sessions cannot import assembly/registry.js
(its agent builders transitively reach di/container, which imports
sessions — the module cycle broke SessionsImpl registration under
test). Traits move to assembly/traits.ts, a zero-import leaf, with
registry re-exporting them so existing assembly-level importers are
unchanged. Regression test pins the non-trait agent case.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The ALS-miss fallback still imported fetchRun from the legacy runs
store, but ctx.runId has been a turn id since the runs->turns
migration — the lookup always threw and fell through, so a
background-task notification delivered outside its starting ALS
context (crash recovery, resume paths) lost its background_task
gating and fired as a plain notification.
The durable equivalent lives on the turn itself: turn_created's
agent.resolved.agentId, and only the background-task runner starts
'background-task-agent' turns. Also removes one of the last live
imports into runtime/legacy/.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Test files were typechecked by nothing: tsconfig.build.json excludes
them (correctly — they must not land in dist), and vitest transforms
with esbuild, which strips types without checking them. That gap is
where the review's two tsc breaks and the fixture drift accumulated.
Adds 'typecheck' scripts per package (dev tsconfigs, which include
src/**/*.test.ts; renderer reuses its existing tsc -b), a root
orchestrator mirroring the test script, and a CI step in the vitest
job. All three packages are currently clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Same class as the two review findings — type errors only tsc sees
(tests run untyped under vitest; the build tsconfig excludes them):
- headless.test.ts read .agent.overrides without narrowing the
RequestedAgent union; the assertions now compare the whole agent
object, which is also stricter
- real-agent-resolver.test.ts's byte-identity fixture predates the
videoMode/coachMode flags (explicit false = the schema defaults,
so the composed bytes are unchanged)
- real-tool-registry.test.ts's permission asks predate isRead being
required (options: [] was from the older ask shape)
npx tsc --noEmit -p tsconfig.json is now clean for the whole package.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The interface grew subscribe/subscribeAll when the IPC bridge moved
to resolving ITurnEventBus, but the runtime-test fake still only had
publish — a TS2739 invisible to vitest and the build tsconfig. The
fake now declares 'implements ITurnEventBus' so the next interface
change fails loudly here instead of silently drifting.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Phase A extraction moved getToolPermissionMetadata to
assembly/permission-metadata.ts but this test's type-only import was
mechanically retargeted to legacy/engine.js during the tree move,
where the symbol is imported rather than exported — a TS2459 that
vitest and the build tsconfig never see, and one of the last
live->legacy edges.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The bundled-skill CATALOG_PREFIX still pointed at the deleted
src/application/assistant/skills tree, so the system prompt and the
loadSkill example advertised paths that no longer exist — while the
real post-reorg path (src/runtime/assembly/skills) was NOT a
resolvable alias, so a model referencing the actual source tree got
'Skill not found'.
The new path is now canonical in the catalog and tool description;
the old prefix is kept as a loadSkill alias because agent snapshots
baked before the reorg still carry it. Regression test pins both.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Path pass over turn-runtime-design.md, session-design.md,
VIDEO_MODE.md, and LIVE_NOTE.md: every source pointer updated to the
runtime/ layout, the turn doc's suggested-module-layout section (§28)
refreshed to the real tree (it predated the event hub, bridges, and
composer), and the session doc's §14 layout fixed including its old
§11/§14 contradiction about where the headless runner lives.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
domains/support.ts bundled five unrelated concerns. Each moves to its
owner: the catalog schema → tools/types.ts (every domain's typing);
lenient ~-expansion → tools/paths.ts (documented as distinct from
filesystem's throwing variant); the bg-task input schemas and
resolveCodeProject → domains/background-tasks.ts; the code-run
coalescer → domains/code.ts (catalog re-export retargeted); parser
plumbing → domains/parsing.ts, now module-private.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Everything "model runtime" now lives under core/src/runtime/, whose
five children are the architecture: turns/ (the engine + bridges),
sessions/, assembly/ (what an agent is: registry, composer, workspace
context, message encoding, permission metadata, headless runners,
spawn-agent, copilot/ definition, capabilities/, skills/ — no longer
buried at application/assistant), tools/ (catalog + domains + exec
plumbing + descriptors, which leave turns/bridges and fix the upward
lib→bridges edge), and legacy/ (the quarantined runs engine, with
agents/runtime.ts finally honestly named engine.ts).
Pure moves via git mv (history follows) with mechanical import
rewrites; no durable schema, wire format, or behavior changes — the
golden prompt snapshots and the catalog key-order pin pass unchanged.
di/ stays top-level (whole-app composition root); application/ shrinks
to browser-control/browser-skills/notification and true cross-cutting
lib utils.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Chat history: shared tab design (heading, bg, centered column, card
table), hover kebab + context menu with rename/delete/open-in-new-tab
- Search: chat search now targets the session store (titles from the
sessions index, content grepped from turn logs) — results open again
and new chats are searchable; segmented scope control, scope-aware
placeholder and empty states, Tab flips scope, keyboard-hint footer
- Sidebar pins: stale ids of deleted chats no longer eat pin slots
- Chat header: session token-usage as a ghost bar-chart button with
tooltip that opens stats directly; options menu (download log) moved
into the shared ChatHeader so full-screen chats get it too; caffeinate
indicator renders nothing while off (was a 32px hole)
- Turn usage dots: borderless, aligned with message text edge
- Workspace: breadcrumb baseline alignment
Apps store user settings in data/config.json (e.g. pr-dashboard's
tracked repo) and rely on their bundled agents to turn config into
data. Without this, changing a setting meant staring at an empty app
until the next cron tick. The apps-server watcher now debounces
config.json writes and fires a one-shot manual run of the app's owned
tasks (app--<slug>--*). Generic for any app; dynamic import keeps the
server decoupled from the bg-task runner at load time.
Ollama returns errors as {"error":"<string>"}, but ollama-ai-provider-v2's
error parser expects OpenAI-style {"error":{"message":...}}. The schema
mismatch makes the AI SDK fall back to the HTTP statusText, so users only
ever saw "Bad Request"/"Not Found" while Ollama's actual reason (model not
found, no tool support, ...) was swallowed -- which is why reports like
#696 were impossible to debug.
makeOllamaThinkFetch now rewraps failed responses' plain-string error
bodies into the nested shape the provider parses, preserving status,
statusText and headers (content-length dropped so the runtime recomputes
it). Successful responses, already-nested errors and non-JSON bodies pass
through untouched. The wrapper's two fetch call sites were merged into one
so both the /api/chat path and the passthrough get the same error handling
without a duplicate-request hazard.
Refs #696
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generic app-copilot bridge (no app-specific wiring anywhere):
- app-read-data builtin: read (or list) an installed app's data/ files —
the agent-maintained JSON the app renders. Same path confinement as
app-set-data; 50k char cap with truncation flag; parsed JSON returned
when possible. Attached via the app-navigation skill.
- read-view view=apps: installed apps with name/description/dataFiles/
agentSlugs, navigating the user's screen to the Apps view (same
show-while-telling contract as email/bg-tasks). Renderer routes the
new view value.
- copilot instructions embed the installed-apps list (name +
description per app) with routing guidance: prefer an app's fresh
data over external calls, answer from it, open-app to surface it.
The apps:list handler fingerprints the app set and invalidates the
instructions cache when it changes (installs, deletes, copilot-
created folders all flow through that poll).
- app-navigation skill: apps section + a worked example framing the
pattern generically (match apps by their own descriptions, never a
hardcoded topic map).
Verified against real data: app-read-data returns pr-dashboard's live
10-PR data.json, escape paths rejected, key-order golden test updated
and passing.
Findings from a fresh post-refactor review. The turn-runtime bridges
imported convertFromMessages and getToolPermissionMetadata from
agents/runtime.ts — the legacy engine file — making it undeletable;
both move to neutral modules (agents/message-encoding.ts,
agents/permission-metadata.ts) that both engines now share. The
abort registry, used by the live tool registry, moves out of the
legacy runs/ cluster into turns/. Dead surface goes: the zero-consumer
CopilotInstructions, skillCatalog, RunLogger, and MappedToolCall
exports are deleted, mapAgentTool/StreamStepMessageBuilder become
module-private, and the mid-file import scar from the composer
extraction is hoisted.
Three review fixes ride along: ITurnEventBus gains
subscribe/subscribeAll so consumers stop resolving the concrete hub;
skill carry-forward becomes its own registry trait instead of
overloading workspaceContext; and connection checks (slack/composio/
code-mode/google) collapse into one shared connections.ts consumed by
both skill availability and the copilot prompt blocks — plus stale
doc pointers (VIDEO_MODE.md to moved prompt text, CLAUDE.md's dangling
AGENTS.md reference).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Thorough-effort Anthropic runs through the gateway failed on the tool
loop: Bedrock rejected the echoed thinking block ("Invalid `signature`
in `thinking` block"). OpenRouter streams reasoning_details as
per-delta FRAGMENTS on reasoning events — the part-level capture kept
only the last fragment (unsigned) — while the fully accumulated,
signed array rides the finish event's providerMetadata, and OpenRouter
gives message-level reasoning_details precedence on read-back.
The bridge now attaches finish-step providerMetadata to the assistant
message as message-level providerOptions (restoring parity with the
legacy StreamStepMessageBuilder, which always did this);
convertFromMessages already echoes it. The authoritative signed array
wins over the per-part fragments, which providers then ignore.
Also persist provider failure detail on turn_failed: errorMessage()
appends the API error's status code and responseBody (bounded to 2KB)
— "Failed after 3 attempts" alone was undebuggable from the turn log.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Previously catalog readers (listOnboardingModels, getChatModelIds)
fetched models.dev inline on a 24h TTL, while the reasoning-capability
paths read the cache only — so a signed-in install could run forever
with no cache and the effort chip never appeared, and catalog calls
could stall on a slow third-party fetch.
Now main calls startModelsDevRefresh() once at boot: refresh if the
cache is missing or older than 24h, then every 24h while running, with
a 10s fetch timeout, errors logged and swallowed (existing cache stays
in use). Every consumer reads the on-disk cache only. Catalog-shaped
readers (models:list, gateway annotation) await the warm-up's first
attempt so a fresh install's first list sees the fetched data; the
turn-start capability gate never waits. A missing cache is an empty
catalog, not an error — chat is unaffected either way.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The reasoning-capability join failed for most gateway models, hiding
the effort chip for everything except the Gemini default. Two id
dialects broke it: OpenRouter-style ids spell versions with dots
("anthropic/claude-opus-4.8") where models.dev uses dashes
("claude-opus-4-8"), and the rowboat gateway serves OpenAI models with
no vendor prefix at all ("gpt-5.4").
The lookup is now a pure index (buildReasoningIndex/lookupReasoningFlag,
unit-tested) that normalizes ids case-insensitively with dots folded to
dashes, keyed both vendor-qualified and bare; bare ids that clash across
vendors with different flags are dropped rather than guessed. Verified
against the live gateway list: all 11 models now resolve.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a Brain chip next to the model picker (Auto / Fast / Balanced /
Thorough) in both the full-screen and side-pane composer. Selection is
per-tab, next-turn intent like the model picker (same ref pattern), but
unlike model it is never frozen on a run — it applies turn by turn,
riding sendConfig.reasoningEffort into turn_created.config.
The chip renders only when the effective model (run-frozen, picked, or
app default) is known-reasoning per the models:list capability flag,
and a stale selection resets when switching to a non-reasoning model.
Turns that ran at a non-auto effort show the level beside the existing
token-usage affordance, read retroactively from the persisted turn
config; reasoning tokens were already displayed there.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Widens the models.dev schema to keep the per-model `reasoning` boolean
and carries it through normalizeModels → ProviderSummary → models:list,
so the composer can gate the reasoning-effort control on actual model
capability. Gateway model lists (bare "vendor/model" ids from the
server) are annotated from the models.dev cache in one batched,
cache-only read; unknown models keep the flag absent, which the UI
treats as "hide the control".
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a canonical reasoning-effort ladder (low|medium|high, absent =
auto/provider default) that travels: send config → turn_created.config →
every model call's persisted parameters (§8.3) → provider-specific
options at invoke time. Behavior-neutral: nothing sends an effort yet,
and auto produces byte-identical requests to today.
- shared: ReasoningEffort enum (single source, reused by models.json
provider config), TurnCreated.config.reasoningEffort (additive
optional on a non-strict object — old builds strip it, no
schemaVersion bump), sessions:sendMessage IPC schema.
- turns: CreateTurnInput/SendMessageConfig/HeadlessAgentOptions carry
the value; runModelStep stamps it on each call's parameters so every
step durably records what it ran with.
- bridge: maps canonical effort to provider options, transport-only
like prompt caching — OpenAI reasoningEffort, Anthropic thinking
budgets (raising maxOutputTokens to the budget floor, never lowering
an explicit value), Gemini thinkingLevel/thinkingBudget by
generation, OpenRouter/rowboat reasoning.effort. Capability-gated
via a cache-only models.dev lookup (never blocks a turn on the
network); unknown support fails closed on strict flavors, while
OpenRouter-shaped flavors map permissively since OpenRouter drops
the field for non-reasoning models. Explicit persisted
providerOptions win over the mapping. Ollama keeps its existing
provider-level think rewrite; openai-compatible endpoints get
nothing (no safe universal parameter).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The bridge dropped providerMetadata from content-block stream events,
so reasoning parts persisted as bare {type, text}. Anthropic thinking
signatures (which arrive on a reasoning-delta with an empty text delta),
Gemini thoughtSignatures (on text-end / reasoning-end / tool-call), and
OpenAI encrypted reasoning were all lost — breaking multi-step tool
turns whenever extended thinking is enabled, since providers require
signed blocks to be echoed back verbatim within the tool loop.
Now every -start event opens a new part (distinct blocks keep distinct
signatures) and metadata from each event of a block is merged opaquely
onto that part's providerOptions, which convertFromMessages already
echoes verbatim. The bridge never interprets the contents; each
provider reads back only its own keys. Empty blocks with no metadata
are dropped, matching the previous lazy-creation behavior. finish-step
metadata stays top-level as before.
Schemas already allowed providerOptions on all part types, so persisted
files remain readable in both directions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 2,091-line BuiltinTools object literal becomes 15 domain modules
(files, parsing, mcp, shell, code, browser, app, web, memory, composio,
models, live-note, background-tasks, notifications, agent-analysis)
plus a shared support module; the root builtin-tools.ts keeps the same
public path and exports and shrinks to loadSkill + spawn-agent (catalog
infrastructure) and the ordered merge.
Catalog key order is provider-payload bytes (tool declarations sit in
the cached prompt prefix), and the historical order interleaves domains
— so the spread order preserves it verbatim, with code/app/web each
contributing two fragments at their original positions. Entries were
moved by script, not retyped. Two proofs pin the order: a
HISTORICAL_KEY_ORDER test on the merged catalog, and a pre/post runtime
key dump compared during the split (byte-identical). Adding a tool now
means editing its domain module; adding a domain means one fragment in
the ordered merge list.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Four modules hand-rolled the same lazy container-import-and-resolve
dance (agent registry, spawn-agent, background-task runner, notifier),
each restating the rationale. di/lazy-resolve.ts holds the pattern and
the reasoning once; all four sites migrate.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Catalog membership for connection-gated skills (composio-integration,
code-with-agents, slack) was decided by hand-rolled excludeIds forks
inside buildCopilotInstructions — a third place that had to know which
skill depends on which connection. Now each entry declares an
availability() check (repos resolved via the new lazyResolve helper, so
the skills module keeps no static DI edge; failures read as
unavailable, the historical default), buildAvailableSkillCatalog
evaluates them concurrently for the system prompt, and the excludeIds
mechanism is deleted. Availability gates catalog VISIBILITY only —
loadSkill still resolves explicitly-requested ids, exactly matching the
old behavior. Tests cover the pure filter and pin that gated ids remain
resolvable.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The workspaceContext trait was consulted separately at every assembly
site before loading agent notes / work dir — a convention each caller
had to re-remember, where a forgotten check either leaks the user's
agent-memory into non-copilot prompts or silently omits it for the
copilot, and neither fails loudly. The loaders move out of the legacy
runtime file into agents/workspace-context.ts, and
loadWorkspaceContext() applies the trait gate INSIDE: both engines now
call it (the resolver keeps its loader test-seams by injecting them
through), so a future assembly site structurally cannot skip the gate.
Tests pin the gate: non-workspace agents get nulls even when the
loaders would yield values.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The app-toggled mode flags were hand-mirrored in three places (the
resolver's CompositionOverrides zod schema, CapabilityContext, and
ComposeSystemInstructionsInput) and copied field-by-field with '?? false'
defaults — so a mode missed at one site compiled clean and silently
never composed. Now capabilities/types.ts declares one zod ModeFlags
schema with defaults: the wire shape (all-optional, what turn files
carry), the resolver's concrete parse output, and the composition
context all derive from it; CompositionOverrides just extends it with
the resolver-only keys, the composer takes {instructions, notes,
workDir} & ModeFlags with a rest-spread (no copy list), and the legacy
call site passes explicit false for the modes it never composes.
Adding a mode = one schema key + one record; anything less is a
compile/parse error. New resolver test pins historical-composition
compatibility (sparse, null-heavy, unknown-key, and garbage
compositions all compose exactly as before). Golden snapshots
unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The design rule "app/always capabilities are never model-loadable" was
held only by two arrays happening to be separate. Now it is structural:
CapabilityDefinition is a union — ModelCapability (requires catalog
title/summary and lazy content, cannot carry a fragment) vs
EagerCapability (requires promptFragment, never enters the catalog) —
and DiskSkill is typed as ModelCapability, making the disk trust
boundary a compile-time fact instead of a comment. The kitchen is
fenced along with the menu: catalog building and skill-tool attachment
go through pure, fenced helpers (buildCatalogFromEntries /
toolNamesFromEntries), and the boundary test now feeds a real eager
capability through a mixed list and asserts both the catalog hides it
and the tool path refuses its tools. Golden snapshots unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The side-pane copilot renders messages through its own markup in
chat-sidebar.tsx, which never got the hover copy button added to the
full-screen chat renderer. Maximizing the pane stays on the same code
path, so it was missing there too.
- Hover kebab menu on sidebar chat rows with Pin / Rename / Delete
- Rename: inline edit persisted via the existing sessions:setTitle;
custom titles survive since auto-titling only fills empty titles
- Pin: up to 3 chats float to the top of the list (localStorage)
- Delete: confirm dialog, sessions:delete, closes any open tab
- Dialog shell: sectioned sidebar nav (Configure / App), larger header
without divider, more padding
- Connections & Mobile: real brand logos (Google, Fireflies, Slack,
WhatsApp, Telegram), no icon tiles, no separator lines
- Models: provider logos on the your-own-providers cards
- Code Mode: Anthropic/OpenAI marks for Claude Code and Codex
- Help: GitHub and Discord logos, tile-free rows
- Dialog overlay: frosted backdrop blur
- Chat empty state: smaller type scale in the side-pane copilot,
compact prop on connections card
- Background tasks: stop button sized to match the Updating pill,
wraps under it in narrow panes, aligned with the state toggle
- Sidebar: Code above Meetings
- Brain: page + graph background matches other tabs, darker light-mode
borders for Base rows and Files containers, bolder section labels
- Top-align icons with titles on two-line nav rows; consistent ink
color for Apps/Background agents/Workspaces (sublabels stay muted)
- Quick actions: labeled New chat button, secondary actions as
compact ghost icons
Findings from an independent review of the branch:
- loadAgent's plain-object lookup traversed Object.prototype, so a
user-defined agent named "constructor"/"toString" threw a TypeError
instead of falling through to the agents repo as it did before the
registry (Object.hasOwn now guards both lookups; regression test
pins the fallthrough without depending on the environment).
- agentFromRaw now uses the same parseFrontmatter helper FSAgentsRepo
uses, so builtin and user agents can never drift on one file format.
- The fused workspace-context capability splits into agent-notes and
work-directory records — the composer's own separator joining makes
the output byte-identical, so the fusion bought nothing.
- Mode records drop their write-only title/summary (optional on the
base record, still required on skills where the catalog renders
them), the composer body is reindented and iterates a hoisted
PROMPT_CAPABILITIES list, and the two single-consumer re-export
shims in the legacy runtime file are deleted (consumers retargeted).
Golden snapshots unchanged throughout: composed prompts remain
byte-identical. Deferred to the follow-up PR with availability wiring:
discriminated-union typing on activation + fencing resolveSkill, and
unifying the three mirrored composition shapes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>