Commit graph

451 commits

Author SHA1 Message Date
Ramnique Singh
d7c19a9a38 docs(x): fix pointers the reorg docs pass got wrong or missed
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>
2026-07-10 17:57:03 +05:30
Ramnique Singh
1f0cdbd366 refactor(x): single live homes for getErrorDetails and isPathInside
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>
2026-07-10 17:54:43 +05:30
Ramnique Singh
7fdfa32c7e fix(x): copilot prompt's Composio section uses the shared connection check
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>
2026-07-10 17:51:23 +05:30
Ramnique Singh
3b6c30fac9 fix(x): blank tool paths error instead of resolving to the process cwd
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>
2026-07-10 16:47:15 +05:30
Ramnique Singh
05545ba283 fix(x): gate session skill carry-forward on the skillCarryForward trait
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>
2026-07-10 16:44:56 +05:30
Ramnique Singh
b6c2905af7 fix(x): notify-user derives its use-case fallback from the turn record
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>
2026-07-10 16:40:44 +05:30
Ramnique Singh
6a8e67878d ci(x): typecheck test files — the gap vitest and the build tsconfigs both miss
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>
2026-07-10 16:30:26 +05:30
Ramnique Singh
02912ba9ef fix(x): clear remaining tsc drift in runtime test fixtures
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>
2026-07-10 16:30:26 +05:30
Ramnique Singh
05eb94cd3a fix(x): FakeTurnEventBus satisfies ITurnEventBus
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>
2026-07-10 16:30:26 +05:30
Ramnique Singh
de84b2e9ce fix(x): real-permission-checker test types against permission-metadata, not the legacy engine
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>
2026-07-10 16:30:26 +05:30
Ramnique Singh
24fb0d38e5 fix(x): skill catalog advertises the post-reorg path; legacy path stays an alias
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>
2026-07-10 16:30:26 +05:30
Ramnique Singh
a3d2ddf34c docs(x): design docs and prompt catalogs follow the runtime/ move
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>
2026-07-10 16:30:26 +05:30
Ramnique Singh
5f4788cf56 refactor(x): dissolve the tools support grab-bag into its owners
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>
2026-07-10 16:30:26 +05:30
Ramnique Singh
e808d6d4f0 refactor(x): the runtime moves under one roof — core/src/runtime/
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>
2026-07-10 16:30:26 +05:30
gagan
25a465880d
Merge pull request #723 from rowboatlabs/feature/apps-copilot-bridge
Apps: generic copilot bridge — answer from app data, surface the app
2026-07-10 15:39:47 +05:30
Gagan
269734a3d5 feat(x): chat history redesign, session-store search, header cleanup
- 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
2026-07-10 15:16:28 +05:30
Gagan
5df26e6fca feat(apps): run an app's own agents when its data/config.json changes
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.
2026-07-10 15:15:24 +05:30
Gagan
7d38dc84a2 feat(apps): copilot answers from installed apps and surfaces them
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.
2026-07-10 14:23:03 +05:30
Ramnique Singh
36ce35a058 Add reasoning effort for spawned agents 2026-07-10 11:59:11 +05:30
Ramnique Singh
fdce639839 refactor(x): decouple the new runtime from the legacy engine file
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>
2026-07-10 11:39:39 +05:30
Ramnique Singh
7ab7143372 fix(x): round-trip signed reasoning via message-level providerOptions
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>
2026-07-10 11:34:16 +05:30
Ramnique Singh
3b447d07b9 refactor(x): models.dev — single startup writer, cache-only consumers
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>
2026-07-10 10:38:33 +05:30
Ramnique Singh
885d4bc5cf fix(x): join gateway model ids to models.dev across id dialects
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>
2026-07-10 10:20:56 +05:30
Ramnique Singh
1ffe29db9d feat(x): thread the models.dev reasoning flag to the renderer
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>
2026-07-10 10:11:25 +05:30
Ramnique Singh
125d39ee0b feat(x): per-turn reasoning effort — core plumbing
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>
2026-07-10 10:11:25 +05:30
Ramnique Singh
091f3e3acd fix(x): preserve block-level provider metadata in the turns bridge
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>
2026-07-10 10:11:25 +05:30
Ramnique Singh
38e63b9609 refactor(x): split the builtin-tool monolith into domain modules
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>
2026-07-10 09:47:53 +05:30
Ramnique Singh
ca077faa95 refactor(x): shared lazyResolve for the no-static-DI-edge pattern
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>
2026-07-10 08:50:39 +05:30
Ramnique Singh
8dd5a2b49f refactor(x): skills declare their own availability; excludeIds dies
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>
2026-07-10 08:48:24 +05:30
Ramnique Singh
efeaae1e19 refactor(x): workspace-context loading gets one trait-gated chokepoint
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>
2026-07-10 08:45:58 +05:30
Ramnique Singh
d0018daad3 refactor(x): one ModeFlags schema behind all three composition shapes
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>
2026-07-10 08:43:24 +05:30
Ramnique Singh
8cf1335577 refactor(x): make capability activation a discriminated union; fence the loadSkill path
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>
2026-07-10 08:38:51 +05:30
Ramnique Singh
a9a342653f fix(x): review fixes — prototype-safe registry lookup, shared parsing, slimmer records
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>
2026-07-09 23:10:58 +05:30
Ramnique Singh
f2a874a214 refactor(x): workspace context becomes an always-activated capability
The agent-notes and work-directory prompt blocks move out of the
composer into WORKSPACE_CONTEXT_CAPABILITY (activation: 'always'),
whose fragment is pure over the resolved inputs — the resolver still
loads notes/work-dir only for agents with the workspaceContext trait
and passes null otherwise, so trait gating is unchanged. The composer
now iterates one capability list (workspace + modes) in fixed order;
golden snapshots pass unchanged, byte-identical output. The composer's
own body is down to: base instructions + hidden-user-context + the
capability loop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 22:40:27 +05:30
Ramnique Singh
e194e32a23 refactor(x): modes become app-activated capabilities
The six mode blocks (voice input, video, coach, voice output
summary/full, search, code mode) move out of the composer's if-chain
into capability records (capabilities/modes.ts), each owning its
fragment text as a pure function of the composition context — the
code-mode fragment keeps its chip/cwd parameterization. The composer
now iterates MODE_CAPABILITIES in declared order, which is the fixed
total order that keeps composed prompts byte-stable; the 13 golden
snapshot tests pass unchanged, proving byte-identical output. Fragment
text was extracted from the if-chain programmatically (not retyped) so
the bytes could not drift. Adding a mode is now one record in one
file instead of a flag threaded through the resolver plus a concat
site in the composer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 22:38:06 +05:30
Ramnique Singh
484a9f0495 refactor(x): capability record — skills become the model-activated subset
capabilities/types.ts introduces the assembly unit: id/title/summary +
lazy guidance + owned tools (what skills already were), plus an
activation axis ('model' | 'app' | 'always') and an eager
promptFragment(ctx) for app/always entries — pure over a persisted
composition context so composed prompts stay byte-identical.
SkillDefinition is now CapabilityDefinition & {content}: bundled skills
are unchanged model-activated entries, the loadSkill catalog filters to
that subset, and disk skills remain structurally limited to it (eager
fragments and app activation are bundled-only powers — a disk file must
never gain a standing system-prompt injection channel).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 22:35:41 +05:30
Ramnique Singh
bd34531305 refactor(x): extract the system-prompt composer from the legacy runtime file
composeSystemInstructions (+ the hidden-user-context block) moves
verbatim to agents/compose-instructions.ts, owned by the assembly
layer; the legacy engine and old import paths keep working via
re-export. New golden-bytes snapshot tests pin the composed output for
a 12-case composition matrix plus the block ordering — the safety net
for folding the mode blocks into capability records: prefix caching
and snapshot inheritance require byte-identical prompts, so any
restructuring must keep these snapshots green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 22:32:47 +05:30
Ramnique Singh
b3330c09c4 refactor(x): agent registry — one table instead of the loadAgent ladder
Built-in agent identity moves to agents/registry.ts: a table of
definitions with builders, an alias (rowboatx → copilot) expressed as
shared entries, and declared traits. One prompt-file loader replaces
the five copy-pasted frontmatter-parsing blocks, and the stringly
"copilot" || "rowboatx" comparisons in the resolver and the legacy
runtime become hasWorkspaceContext(agentId) trait lookups. loadAgent
keeps its signature and its legacy import path via re-export; the
user-agents repo fallback resolves the container lazily so the
registry adds no static edge into the DI graph.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 22:31:02 +05:30
Ramnique Singh
876bc35e9e refactor(x): one delivery path for turn events — chat, channels, deltas
Completes the turn-spine unification. Chat's SessionChatStore now
consumes turns:events like every other surface, joining live turns by
file offset (drop covered, append contiguous, refetch on gap) instead
of blind-appending session-bus events. Text/reasoning deltas cross IPC
only for turns a window subscribed to (turns:subscribe/unsubscribe, a
per-webContents registry that also survives window teardown), so
headless pipeline chatter never reaches windows that aren't watching.
The channels bridge settles turns off the turn event bus instead of
filtering the session broadcast. With no turn-event consumers left,
SessionsImpl stops forwarding entirely (it drains its execution streams
and keeps outcome/index handling) and sessions:events shrinks to
index-changed entries — one channel for turn events, one for session
metadata.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 15:47:21 +05:30
Ramnique Singh
f916cb1d70 refactor(x): finish turn-spine adoption for headless surfaces
Background-task and live-note transcript views move from fetch-once to
the shared useAgentRunTranscript hook (useTurn over the turns:events
spine, with a one-shot legacy runs:fetch fallback for pre-migration run
ids), so an in-flight run's transcript now streams live. The headless
runner drains its execution stream — delivery rides the turn event bus,
and an unconsumed HotStream buffered every durable event until settle.
Deletes the caller-less runHeadlessTurn duplicate (HeadlessAgentRunner
is the implementation; session-design.md now says so) and the orphaned
web-search skill file that was never registered.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 14:59:22 +05:30
Ramnique Singh
12a7118445 feat(x): turn event spine — one bus for every turn's events
TurnRuntime now publishes every turn's events (durable ones tagged with
their 1-based file offset, deltas without) to an injected TurnEventHub,
regardless of who started the turn. Main forwards durable events to all
windows on one turns:events channel; the renderer's new useTurn(turnId)
hook joins live turns gap/duplicate-free via the offset protocol. The
sub-agent card drops its 1s polling for push updates, and the nine
per-channel window fan-out loops collapse into one broadcastToWindows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 14:19:53 +05:30
Ramnique Singh
2ab3e4e751 feat(x): regroup copilot prompt guidance into skills
Capability guidance now co-locates with the tools that skills own:

- Register the previously orphaned slack skill in the catalog (excluded
  when Slack isn't connected, like composio-integration) and shrink the
  prompt's Slack block to a routing line + followed-channels hint — the
  agent-slack command patterns already live in the skill body.
- Compress the per-capability routing directives (meeting-prep,
  create-presentations, doc-collab, app-navigation, background-task,
  apps, browser-control, notify-user) to when-to-load signals; the
  how-to detail loads with each skill.
- Slim the knowledge-graph access examples to the essential patterns.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 11:03:29 +05:30
Ramnique Singh
2e45035ece feat(x): skill-scoped contextual tool loading for the copilot
Skills now own their tools. The copilot attaches only a small hardcoded
base set (~16 tools instead of all ~42); loading a skill via loadSkill
attaches its declared tools as native tool definitions from the very
next model step, and they stay attached for the rest of the session.

- tools_extended: new durable turn event (the one sanctioned exception
  to per-turn tool immutability — explicit, replayable, never silent).
  Reducer tracks extensions per model-call index; effectiveTools()
  composes base + extensions for both the live loop and inspect.
- Runtime: sync tool results may carry metadata.toolAdditions; dedupe
  and the durable append run atomically on the serialized commit chain
  (safe under concurrent sync tools). Crash recovery rebuilds the
  extended toolset from the log.
- Skills declare tools (SkillDefinition.tools; SKILL.md tools:/
  allowed-tools frontmatter); catalog entries list them; loadSkill
  returns them via a reserved $toolAdditions key the registry lifts
  into result metadata. builtin-tools skill = escape hatch, derived as
  "every non-base builtin" at module init.
- Sessions derive composition.activeSkills from the previous turn's
  request + its tools_extended events; the resolver attaches active
  skills' tools on top of the base set (stable order, so snapshot
  inheritance keeps working).
- Legacy code-mode path keeps working on the base set (which includes
  code_agent_run/launch-code-task) and strips $toolAdditions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 11:00:13 +05:30
Ramnique Singh
f546d1ad59 feat(x): teach copilot when to spawn sub-agents; fence shared surfaces
The spawn-agent tool shipped with mechanics but no judgment layer —
the only guidance was the tool description. This adds the ambient
decision heuristics and closes a child-profile hole.

- Copilot system prompt gains a "Sub-Agents (parallel & heavy work)"
  section (same strong/anti signal structure as Background Tasks):
  the organizing principle is context hygiene — a sub-agent's reads
  and fetches never enter the main conversation, only its distilled
  answer. Research-shaped requests (catch-me-up, dig-into, meeting
  prep) now route through sub-agents by default, with copilot as
  synthesizer. Anti-signals: single quick lookups, journeys-as-answer,
  anything needing mid-task user input, driving shared surfaces.
- Background-task agent gets a matching (shorter) note — children
  have their own model-call budgets, so a bg-task covers more ground
  per run by fanning out.
- The inline default child profile now also excludes app-navigation
  and browser-control: both drive shared visible surfaces (the UI the
  user is watching; the single embedded browser pane), which headless
  or parallel children would corrupt, not just clutter. Still
  available via explicit tools selection.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 06:20:38 +05:30
Ramnique Singh
3822bda1f7 fix(x): spawn-agent works from task alone — instructions now optional
First real-world use showed the model calling spawn-agent with
{name, tools, task} and no instructions — a complete spec by any
reasonable reading — and burning a correction round-trip on the
"exactly one of agent_id or instructions" rejection (both children
then succeeded on retry).

Requiring instructions bought nothing: the task is the spec. Omitting
both agent_id and instructions now spawns a general-purpose worker
with a default headless prompt; only supplying BOTH remains an error.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 06:08:10 +05:30
Ramnique Singh
b62ed6a2f4 feat(x): spawn-agent — run sub-agents as headless child turns
Adds the agent-as-tool capability deferred in turn-runtime-design §29.2:
a `spawn-agent` builtin that runs a sub-agent in its own standalone
headless turn and returns its final answer to the parent. Multiple
spawn calls in one assistant batch run concurrently (previous commit).

- RequestedAgent is now a union: by-id (unchanged shape) | inline
  {name, instructions, model?, tools?}. Inline definitions persist
  verbatim in turn_created and resolve to the same immutable snapshot.
- Agent resolution splits by variant: DispatchingAgentResolver narrows
  the union once; InlineAgentResolver materializes inline specs
  (builtin catalog validation, headless default profile when tools are
  omitted); RealAgentResolver keeps the by-id path byte-identical. The
  builtin→ToolDescriptor conversion is extracted to a shared helper.
- The spawn handler (RealToolRegistry branch → runSpawnedAgent) runs
  the child via HeadlessAgentRunner on the parent's model by default,
  clamps the model-call budget at 20, cascades the parent's abort
  signal, and records {kind:"subagent", childTurnId} as durable tool
  progress — the only parent→child link; no parentTurnId is added to
  the schema. Task-level failures return as conversational isError
  results, never terminal.
- Depth is capped at 1: both resolvers strip spawn-agent from children
  (inline always; by-id via the new subagent composition flag) and the
  handler refuses child-shaped parents outright.
- Renderer: spawn-agent calls render as a SubAgentBlock — a collapsed
  status card that expands to the child's live transcript
  (CompactConversation over sessions:getTurn, polled at 1s while
  running; standalone child turns don't reach the session bus).
- The BuiltinTools entry gives copilot (and other catalog-attached
  agents) the tool automatically; its execute is the degraded legacy
  path only, since the turn runtime intercepts builtin:spawn-agent.

Schema note: RequestedAgent widened under schemaVersion 1 (pre-release)
— requires wiping ~/.rowboat/storage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 23:31:41 +05:30
Ramnique Singh
d2b68a4684 feat(x): execute sync tools concurrently within a turn batch
Sync tools in one assistant batch now run via Promise.all instead of
sequentially — a tool pending on I/O no longer blocks its siblings.
Three coordinated changes keep the event-sourced runtime sound:

- executeAllowedTools is two-phase: invocation events are appended
  serially in source order (durable before any side effect, deterministic
  log prefix), then all sync executions run concurrently, each appending
  progress/results as they land. Per-call error and cancel semantics are
  unchanged (moved to executeSyncTool).
- append() commits through an internal queue: persist → reduce → stream
  runs to completion per batch, so file order, in-memory order, and
  stream order stay identical even while executions overlap. A failed
  commit rejects only its caller; the chain survives for siblings.
- Abort-registry state is scoped per tool call (turnId:toolCallId) via a
  wrapper, fixing two latent races: createForRun destroying a running
  sibling's tracked processes, and cleanup tearing down the turn-wide
  force-kill scope when the first tool finished.

Wire ordering is untouched: model requests already reference tool
results by the assistant message's source order, pinned by a new test.
No concurrency cap and no per-tool serialization by design; tools that
share state must tolerate racing (file edits already reject stale
writes via their search/replace precondition).

Spec §4.5/§10.5 updated. New runtime tests cover overlap (deadlock
unless concurrent), progress interleaving, sibling failure isolation,
mid-batch cancellation, and crash recovery with multiple open
invocations.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 22:51:51 +05:30
Arjun
d3aeeb87d7 feat(x): expand curated Composio toolkits and fix broken Microsoft slugs
Expand the curated toolkit whitelist from 26 to 67 entries, adding
well-known services that support Composio-managed OAuth2 (the only
auth scheme the app's connect flow supports): Zoom, Discord, ClickUp,
monday.com, Confluence, GitLab, Bitbucket, Supabase, Sentry, PagerDuty,
Stripe, Square, QuickBooks, Mailchimp, Google Ads/Analytics/Search
Console, Figma, Canva, YouTube, Instagram, Facebook, Box, SharePoint,
and more. Adds three new categories: design, marketing, finance.

Also fix two entries that were never connectable: 'microsoft_outlook'
and 'onedrive' do not exist in Composio's catalog (the API returns 404,
so initiateConnection always failed). The real slugs are 'outlook' and
'one_drive'.

Excluded per curation policy: Composio's own utility toolkits
(composio_search, codeinterpreter, browser_tool), no-auth toolkits
(Hacker News, OpenWeatherMap), API-key-only toolkits the app cannot
connect (Firecrawl, Tavily, Exa, PostHog, etc.), and niche/low-quality
entries.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 00:33:32 +05:30
arkml
3261d64dd4
Merge pull request #691 from rowboatlabs/feat/caffeinate-toggle
feat(x): Caffeinate toggle to keep the system awake
2026-07-08 00:10:03 +05:30
Arjun
94727a238b feat(x): add Caffeinate toggle to keep the system awake
Adds a Caffeinate switch (Settings → Mobile channels) that uses
Electron's powerSaveBlocker ('prevent-app-suspension', equivalent to
caffeinate -i) to stop the machine from idle-sleeping so the
WhatsApp/Telegram bridge stays connected. While active, an amber
coffee icon in the titlebar shows the state and can be clicked to
turn it off; a power:caffeinateChanged push keeps the titlebar
indicator and the settings switch in sync.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 00:07:14 +05:30