Files
stack/docs/prd/mosaic-stack.md
T
jason.woltjeandClaude Opus 5.5 240d255187 docs: PRD 1.3, usage plug-ins and the app-wide plug-in layer (Q153-Q155)
PRD version 1.3 records Jason's rulings Q153 to Q155, relayed by Mos
(lead decision 143), and Mos reviewed the text before it landed. Adds
REQ-LIMIT-4 (usage sources and limit checks as the first plug-in kinds;
an unknown turn is never zero, no source switched on means every turn is
unknown, an unenforced limit shows as not enforced) and a Plug-ins
section with REQ-PLUG-1 (kind registry and loader in packages/plugins,
in-repo modules only until Jason rules, Vikunja #286) and REQ-PLUG-2
(new call-outs go through a kind; hard-coded parts convert as rows touch
them). REQ-LIMIT-2 and REQ-LIMIT-3 count through usage sources.
Technical considerations list where each kind is hard-coded today.

requirements.json moves to 1.3 with 41 ids; the tasks and bus id-count
tests follow. The PRD 1.2 build plan gains step 6b, the plug-in layer
(#1626), and rows 124 to 127 ship their counting and checks as modules.

Jason approved the conversion order for hard-coded parts in Q156. Lead
decision 143 and the build plan record it, and the managed-session
config brief notes that row 65 now runs before 64 and 66.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-14 17:39:37 -05:00

624 lines
31 KiB
Markdown

# PRD: Mosaic Stack
- Status: approved, version 1.3. Jason approved draft 0.4 unchanged as
1.0 on 2026-10-05 (lead decision 61). They approved version 1.1, the
Organization requirements, in decision 03130d08-ddce-480d-a601-e59f64744fc3
on 2026-10-14. They approved version 1.2, the chief of staff, the
assistant, threads and agent limits, through Mos on 2026-10-14 (lead
decisions 141 and 142). Version 1.3 records their rulings on plug-ins
and token counting, Q153 to Q155, relayed by Mos on 2026-10-14 (lead
decision 143). Mos reviewed the 1.3 text before it landed. It is never
edited in place. Changes are a new version.
- Version 1.3 source, Jason's words relayed by Mos:
- Q153: "2. agreed. Make it modular though. We should be able to
extend without code change on the core app." Mos's reading, which
Jason confirmed: the stack counts tokens itself for now, with no
CLIProxyAPI queue reader. Usage sources and quota enforcement sit
behind a plug-in interface. The first source reads the stack's own
harness transcripts. Later sources are separate modules switched on
in config, with no change to core bus or broker code.
- Q154: "Q154: yes. In fact, the whole app should use plug-in
interface. The core should allow for extension". Mos read the second
sentence back as Q155.
- Q155: "Q155: yes", to this read-back: every part the core calls out
to gets a registered plug-in interface: harnesses, model providers,
usage sources, the task tracker, the git forge, and channels such as
Discord. Adding one means adding a module and switching it on in
config, with no core code change. This applies to new work now.
Existing hard-coded parts convert as rows touch them, not in one big
rewrite.
- Plug-ins from outside the repository are a security call Q155 didn't
make. Mos is taking it to Jason (Vikunja #286).
- Version 1.2 source, Jason's words relayed by Mos:
- Q147 (full text in lead decision 141): "COS should focus on COS
duties. This will include hire/terminate authority for agents, org
structure, etc. ... This necessitates the creation of a new role for
user interaction. ... The user should be allowed to have 1 - N
sessions with this agent to allow for topics of focus in a given
thread. ... The interaction agent in each session is the user's main
agent."
- Q148, Mos's read-back of Q147: "Q148: Yes".
- Q149, on the 1.2 draft: "approved as long as the agent limits are
tunable via "Settings" in the WebUI and via the CLI tooling."
- Q150: "No hard cap on agent limits. All tunable." The five kinds of
limit are quoted under Launching.
- Q151, Mos's read-back of Q150, with the default that launches over a
limit are refused and logged and a run that hits its token limit is
stopped: "Q151: yes".
- Owner: Jason. Sage writes it from the PRDY interview.
- Template: PRDY "software" (`v1/packages/prdy/src/templates.ts`), filled
by hand until PRDY is ported.
- Interview record: Sage's thread 1ef1e4f8. Round 1 is lead decision 45.
Round 2 is lead decision 48. Round 3 is lead decision 53. Design
inputs are lead decisions 43, 44, 46, 47 and 49 to 52.
## Introduction
### Objective
The working north star, ratified as lead decision 44: "Jason declares
businesses, projects and roles. Agents in those roles carry the work end
to end under declared policy, and only gated decisions reach him."
### Context
See `docs/plans/2026-10-04_foundation-direction.md`. Agents already do
real work here. But a person has to launch them, relay between them, hand
them credentials and correct them. Jason has no CLI and no complete WebUI,
and outstanding decisions don't collect anywhere.
## Users
For now the only user is Jason. The stack must still be installable by
outside users later, which means:
- an installer;
- no paths or names specific to Jason;
- configuration through the variable layers.
(Round 1, answer D.)
## Problem statement
In priority order (round 1):
1. **Admin falls on Jason.** Jason launches agents, relays between them,
finds credentials and corrects them.
2. **Jason can't see what the agents are doing or what's waiting for
Jason.**
3. **Agents drift from what the business needs.** Nothing ties a piece of
work to a stated goal.
## Scope and non-goals
### Hard limits
The stack doesn't do any of these. Each one is either a gated decision or
off the table (round 1, "all five"):
- A. Spend money without Jason.
- B. Speak externally as Jason without Jason's approval.
- C. Give agents Jason's personal credentials. Each role gets its own.
- D. Become a multi-tenant SaaS in v1. There is no hosted SaaS in v1.
- E. Replace Claude Code, Codex or Pi. It wraps them.
### Where it runs
Mosaic Stack stays self-hosted open source. The licence is open, and Jason
decides it later.
Self-hosted, on Jason's machines and homelab (round 1, "A first"). Cloud
workers aren't ruled out later.
### Out of scope for v1
Round 2, "all":
- businesses other than Mosaic Stack (SetSpark joins after v1);
- minting credentials (v1 hands out tokens Jason creates);
- Codex (v1 covers Pi and Claude Code, and Codex comes next);
- Pocket ID and SSO;
- access from beyond localhost.
## Requirements
Every task the stack creates cites one of these ids. A task with no
parent requirement is refused.
### Roles
- **REQ-ROLE-1.** Each role is a reviewed file, `roles/<name>.json` at
version 2. The file holds:
- a contract;
- an authority map over a closed vocabulary of actions;
- the credentials it needs, by service and scope.
Any action the file doesn't list is gated.
- **REQ-ROLE-2.** Each role instance has one holder at a time. The
assistant is the exception: it has one holder per thread at a time. A
claim takes a lock and logs it, and releasing the role or ending the run
frees it. The chief of staff may end another role's run, and that act is
logged with its reason. Anyone else who revokes a stuck holder needs a
resolved decision, and the revoke cites it.
- **REQ-ROLE-3.** An organization file declares the role instances,
the arbiters and the credential references. The user and the chief of
staff write it. The chief of staff's writes go through a broker verb
that logs each change. No other role writes it. A missing or invalid
file fails closed. (Q133, confirmed as Q140: "We need the human and the
Chief of Staff to be able to edit the Business file(s)".)
- **REQ-ROLE-4.** The first organization is Mosaic Stack, with six role
instances:
- chief of staff;
- assistant;
- PM, held by Sage;
- CTO, held by Darkwing;
- coder;
- reviewer.
The schema can also declare CEO, CFO and the other C-suite roles. The
assistant's name is set in the organization file. Jason's may be named
Mos.
Added in version 1.2:
- **REQ-COS-1.** The chief of staff runs the organization. It hires and
terminates role instances, owns the organization's structure and is
the only role that launches sessions (REQ-LAUNCH-1). It takes work
from the assistant and assigns it through the roles that do it. It
never talks to the user. Gated decisions still reach the human
through the inbox (REQ-DEC-2, REQ-DEC-4).
- **REQ-ASSIST-1.** The assistant is the user's main agent. Each user has
one assistant role per organization. It talks with the user, and it
passes work for the organization to the chief of staff. It doesn't
direct other agents, create or assign tasks, or launch sessions.
- **REQ-ASSIST-2.** The broker limits the assistant's messages, not its
contract alone:
- the assistant may send only to the user, in the thread, and to the
chief of staff;
- only the user and the chief of staff may send to the assistant;
- every refused send is logged with its reason.
### Variables
- **REQ-VAR-1.** Variables come in four layers: system, business, project
and agent. Every key is declared in code with a type, the layers allowed
to set it, and a merge rule.
- The most specific layer wins for plain values.
- Limits intersect, so a lower layer can only narrow them.
- An unknown key is refused, and so is a key set at a layer it isn't
allowed in.
- **REQ-VAR-2.** `~/.config/mosaic-dev/config.json` stays the only system
config, and nothing writes it automatically. A secret appears only as a
reference, either a file path or an environment variable name. Only
the broker reads a secret's contents. It holds them in memory and
never logs or writes them. Everything else checks a referenced file
with `stat`.
### Credentials
- **REQ-CRED-1.** Every role instance has its own token for each service
(Gitea, Vikunja). The broker holds them, and they never enter an agent's
environment or files. In v1 Jason creates them by hand from a runbook
Sage writes (round 3, 1A):
- in Gitea, a new bot user per role, named `<business>-<role>-bot`,
for example `mosaic-stack-pm-bot` (round 3, 2A; lead decision 58);
- in Vikunja, a `bot-<business>-<role>` user per role, plus one
read-only `bot-<business>-sync` that does all polling (lead
decisions 52 and 58).
- **REQ-CRED-2.** A session that finds only founder credentials stops.
### Decisions
- **REQ-DEC-1.** Every decision is a record in an append-only SQLite
table. The record holds:
- the role and run that raised it;
- its class and action;
- the options and a recommendation;
- who resolved it;
- its timestamps.
A correction is a new row.
- **REQ-DEC-2.** Routing follows the decision's class:
- routine and within-role decisions are logged;
- cross-role decisions go to the business's arbiter for that domain;
- gated decisions go to the human.
- **REQ-DEC-3.** A human resolution, by an authorized human, comes only from a `mosaic` CLI session
started outside any agent run. Agents reach the decision store only
through a broker. The broker stamps role and run from the launch record.
- **REQ-DEC-4.** A gated decision that blocks work reaches an authorized human
right away: in the CLI inbox, plus a Discord DM sent through the existing
connector (#1509). Everything else goes into one daily digest at 08:00
Central. (Round 2, answers 2A and 3A; round 3, 4A and 5A.)
### Messages
- **REQ-MSG-1.** Messages are addressed to a role. They are append-only
and go to whoever currently holds the role. A message to or from the
assistant also names its thread and goes to that thread's holder. When
the assistant sends work to the chief of staff, the reply comes back to
the same thread. T3 and Discord carry messages; the address is still
the role.
### Tasks
- **REQ-TASK-1.** Tasks live in Vikunja, reached through `/api/v2` with a
bot token for each role. Each task cites a requirement id. Each task
field has one writing role, listed in the slice 1 brief's field table,
and the assigned role writes the task's state. Vikunja's token scopes
cover whole route groups, so the broker's verbs enforce this, not the
tokens. A person's edits come in as events.
- **REQ-TASK-2.** Polling is the source of truth. Every 30 seconds the
sync bot reads each project's open-task board and the tasks updated
since the last check, so column moves and deletions of open tasks show
up within one poll. An hourly full reconcile catches the rest. Slice 1
uses no webhooks. (Lead decisions 51 and 52.)
- **REQ-TASK-3.** The installer offers two choices: point at an existing
Vikunja, or deploy the bundled one. The bundled one is the unmodified
upstream image. No Vikunja code enters the repository.
- **REQ-TASK-4.** The queue keeps the agents' lock, review rounds and
audit log. No field syncs in both directions.
### Interface
- **REQ-CLI-1.** `mosaic` is the front door. From it Jason can:
- talk to the assistant in a project thread they choose or create;
- see the decision inbox and resolve decisions;
- see tasks and agents;
- follow the trail of any piece of work.
- **REQ-CLI-2.** The assistant session that `mosaic` talks to is one the stack
launches and owns, running Pi or Claude Code without a window. The
product doesn't depend on T3. (Round 2, answer 5A.)
- **REQ-WEB-1.** The WebUI shows the same data as the CLI: conversations,
the inbox, tasks, agents and trails. It has a chat area that lists the
user's projects and the named threads in each, with one conversation
view per thread where the user talks to the assistant. The user creates
and names projects and threads there without a terminal. Its Settings
view shows every agent limit, the usage counted against each, and the
model each role instance runs, and lets the user change them
(REQ-LIMIT-1 to REQ-LIMIT-3).
### Launching
- **REQ-LAUNCH-1.** The chief of staff launches role sessions within the
agent limits written in the organization file (REQ-LIMIT-1 to
REQ-LIMIT-3):
- role instances only;
- a launch that would exceed any agent limit is refused, and the
refusal is logged with the limit it hit;
- every launch is logged with its harness, provider, model, and the
account or key it runs on;
- revocable with one word.
Assistant thread sessions are the exception to who launches. The stack
starts one when the user opens or writes to a thread, and ends it after
the thread has been idle for the time the organization file sets. A
running thread session counts against the limits like any other. An
idle thread holds no session and counts against none. The stack starts
the chief of staff itself. (Round 2, answer 6A; Q147; Q150.)
Added in version 1.2. Jason, Q150, verbatim: "No hard cap on agent
limits. All tunable. Agent limit: A parallel execution limit arbitrarily
set by the user or COS. This limit can apply to: overall executing agent
limit across all models; provider parallel agent limit; provider parallel
agent limit by model; daily quota limit based on OAuth account limit or
CLIProxyAPI config in Mosaic Stack; absolute token limit per agent,
model, provider, or API key".
- **REQ-LIMIT-1.** Agent limits are settings with no ceiling in code. The
user and the chief of staff view and change them from the WebUI's
Settings view and from a `mosaic` command, without editing a file. The
chief of staff changes them through its organization-file broker verb
(REQ-ROLE-3). The same places view and change the model each role
instance runs. The running-session limits are:
- the total number of sessions running at once, across all models;
- the number running at once per provider;
- the number running at once per provider and model. The default is 4
Opus and 4 Sonnet, as a default that can be changed, not a cap.
Any kind may be left unset, which means no limit of that kind. A value
is a whole number of 0 or more, and 0 stops new launches of that kind.
A change is checked by the same rules as the organization file, then
written to it and logged with who made it and the old and new values.
An invalid value is refused, and the old value stays. A change applies
to the next launch without a restart, and lowering a limit doesn't end
sessions already running.
- **REQ-LIMIT-2.** A daily quota per OAuth account, or per client key of
the CLIProxyAPI that Mosaic Stack configures. The stack counts the
tokens its sessions use against the account or key each ran on, from
the usage sources switched on (REQ-LIMIT-4), and the day resets at a
time and time zone set with the quota. When an account or key reaches
its quota, new launches on it are refused, and running sessions on it
stop at the end of their current turn. Both are logged. The daily quota
is a limit check (REQ-LIMIT-4). The count covers sessions the stack
ran. Use of the same account outside the stack isn't counted, and
Settings says so. The stack doesn't read the proxy's usage queue
(Q153). A per-key count from the proxy would be a later usage source.
Measuring the quota in tokens is a draft position: Q150 names no unit,
and OAuth plans meter by usage windows, not daily tokens.
- **REQ-LIMIT-3.** A token limit per role instance, model, provider or API
key. Each limit counts the tokens its subject has used since the limit
was set or last reset, from the usage sources switched on
(REQ-LIMIT-4), and the user and the chief of staff can reset it. A
count is the sum of every kind a source reports (input, output, cache
write and cache read), and Settings shows each kind. A launch whose
subject is at its limit is refused, and a running session that reaches
a limit is stopped at the end of its current turn. Both are logged with
the limit and the count. The token limit is a limit check
(REQ-LIMIT-4).
- **REQ-LIMIT-4.** Token counting and limit enforcement are the first two
plug-in kinds (REQ-PLUG-1, Q153):
- `usage-source`: after each turn it reads what the session used and
returns records per run, role instance, provider, model and account,
with each token kind separate, or "unknown" for the turn.
- `limit-check`: from those records and the organization file's
limits, it answers whether a launch may start and whether a running
session must stop at the end of its turn.
The organization file can switch on several of each. A source that
fails during a turn, or can't read it, records the turn as unknown,
never as zero. With no source switched on, every turn is unknown.
Checks count known records only. Each unknown turn is logged, and
Settings shows the number of unknown turns next to each total, so a
limit never looks further off than it is. A limit that no switched-on
check enforces shows in Settings as not enforced. The first
source, `transcript`, reads the stack's own harness transcripts. The
first checks are `token-limit` (REQ-LIMIT-3) and `daily-quota`
(REQ-LIMIT-2). A CLIProxyAPI reader or another provider's report would
be a later source.
### Plug-ins
Added in version 1.3 (Q155).
- **REQ-PLUG-1.** Every part the core calls out to is a plug-in kind:
harnesses, model providers, usage sources, limit checks, the task
tracker, the git forge and channels. The kinds are a registry in one
file, `packages/plugins/src/kinds.mjs`. Each entry names the kind, the
exports a module of that kind must have, the kind's interface version,
and whether the organization file switches on one module of the kind
or a list. A module is a directory, `packages/plugins/<kind>/<name>/`,
whose `index.mjs` exports what its kind needs.
The organization file's `plugins` object switches modules on by name,
one key per kind, through the write path (REQ-ROLE-3). Each change is
logged with who made it and the old and new names. Core code reaches a
module only through the loader, never by importing it. Adding a module,
or switching one on or off, needs no core code change. Adding a kind is
a core change, because it adds a place the core calls out, and it lands
by reviewed commit with its first module.
The loader takes names from the organization file, never paths. It
refuses a name, and the old setting stays, when:
- the name doesn't match `^[a-z][a-z0-9-]*$`;
- no directory of that name exists under its kind;
- any part of the path, `index.mjs` included, is a symlink;
- the module fails to load;
- the module lacks an export its kind needs, or declares another
interface version.
A module runs inside the host process with the host's authority, so
only modules in the repository's `packages/plugins` load, until Jason
rules on others (Vikunja #286). A module goes live like any other code,
through a re-pin. Switching a module on gives it no authority of its
own: the human proof (REQ-DEC-3), the broker's credentials and the role
checks stay in core, and a channel module carries messages without
proving who sent them. A module's credential is a reference the broker
resolves, never the secret (invariant 3).
- **REQ-PLUG-2.** New work that calls out to a part of a plug-in kind goes
through that kind from the start. A hard-coded part converts when a row
changes it, not in one rewrite (Q155). Until it converts, Technical
considerations lists where it is hard-coded.
### Meta-harness
- **REQ-HARN-1.** The meta-harness generates each session's launch bundle
from the role and the resolved variables:
- the prompt;
- the policy;
- skills;
- typed tools for the vocabulary actions;
- a manifest.
Pi comes first, then Claude Code. Codex comes after v1.
- **REQ-HARN-2.** Four layers actually enforce the rules:
- credential scope;
- verbs that refuse without a resolved decision;
- the harness's tool limit;
- a container.
Hooks only refuse early and log, and no document may call them
enforcement. A probe run against the live harnesses must pass before any
build relies on a block.
### Events
- **REQ-EVT-1.** Every step in slice 1 emits an event to the append-only
store. Measures are computed from events, never from tagging chat
messages by hand.
### Organizations
Added in version 1.1 (decision 03130d08).
- **REQ-ORG-1.** "Business" is renamed "organization". One install holds
one or more organizations, and each organization has its own bus as the
tenant boundary.
- **REQ-ORG-2.** The data model is install, then organization, then
projects, then threads, so v1 doesn't lock in one human per
organization.
- **REQ-USER-1.** Users and role-based access control exist at install
and organization level. An agent belongs to one organization and
reaches only its allowed projects and threads.
- **REQ-USER-2.** Only the user who owns a thread reads it or writes to
it, plus that user's assistant and the chief of staff's replies on work
the thread sent. This fills in REQ-USER-1's "threads" for the chat
area. Added in version 1.2.
- **REQ-THREAD-1.** Threads live under projects.
- **REQ-THREAD-2.** The user creates projects and names threads inside
them, from the CLI or the WebUI. Each thread is a separate assistant
session. A thread's messages are kept. When the user comes back to a
thread, after an idle stop or a restart, the assistant picks up where
it left off with that history. One thread's conversation never appears
in another's. Added in version 1.2.
- **REQ-TPL-1.** Templates exist for agent roles and for organization
types.
- **REQ-FED-1.** Installs can federate with each other. This is post-v1.
## Acceptance criteria and success measures
v1 is done when Jason gives one-sentence requests to the assistant, in a
thread, from the CLI or the WebUI, and Mosaic Stack builds pieces of
itself from them, five pieces in a row. For each piece:
- only gated decisions reach Jason;
- every step leaves a trail in the CLI and the WebUI.
(Round 1, answer A; the assistant and threads from version 1.2.)
## Technical considerations
- Vikunja v2.7.0 is AGPL-3.0. Its API tokens can't create users or mint
more tokens. A Mosaic-owned account mints one bot user and one scoped,
expiring token per role. The owner password for that account is the one
high-value Vikunja secret. Instance admin features need a paid licence,
and the plan doesn't use them. Source:
`agents/researcher/work/2026-10-04_vikunja-pocketid.md`.
- Gitea tokens never expire, and creating one needs a password or the host
shell. Rotation will be a scheduled step with a record.
- Pocket ID v2.17.0 is BSD-2. It offers passkey-only login for people and
a client-credentials flow, but it can't issue tokens that Vikunja or
Gitea accept. Agents use per-service role tokens. Pocket ID is wired in
after v1.
- Decisions, messages, role claims and events share one SQLite file,
`<dataRoot>/bus/bus.sqlite`. A guard on every table refuses UPDATE,
DELETE and REPLACE. Run records stay write-once files.
- None of the three harnesses' hooks fails closed by default except Pi's
`tool_call`. Source: `agents/filbert/work/meta-harness-survey-2026-10-04.md`.
- Version 1.2:
- The organization file's `projects` key names git repositories to
clone (`packages/seat/src/checkout.mjs:282-296`). A REQ-ORG-2 project
is the user's container for threads. The build has to keep the two
apart, or define one in terms of the other, before the chat area
stores projects.
- Threads change the bus schema: messages, claims and deliveries gain a
thread. The bus refuses a database whose schema doesn't match
(`store.mjs:113-115`), so this needs a migration and a re-pin with
downtime.
- Claude Code resumes a conversation only inside one launch today, because
its session directory and HOME are per run (`launcher.mjs:204-206`,
`sandbox.mjs:157`). REQ-THREAD-2 needs a per-thread session store that
outlives the run, or a rebuild of context from the thread's messages.
- Limits today: `launch.max` is keyed by model family, and a launch's
family is the one key its model name contains
(`packages/seat/src/session.mjs:52-57`). Each count must be 0 to
`LAUNCH_CEILING = { opus: 4, sonnet: 4 }`
(`packages/business/src/vocabulary.mjs:86`, `business.mjs:129-137`).
The launcher reads the file once at start (`launcher.mjs:123`), so a
change needs a bus restart. REQ-LIMIT-1 removes the ceiling, replaces
family matching with a model table that names each model's provider,
and re-reads limits at each launch. Jason's file needs a one-time
migration from `{ opus: 4, sonnet: 4 }`.
- The stack doesn't know a session's provider today. The only provider
setting is the system-level `execution.provider` (`vars.mjs:84-87`),
read by `sessionModel` (`packages/harness/src/bundle.mjs:49-55`).
Claude Code sessions reach whatever `ANTHROPIC_BASE_URL` points at, or
Anthropic when it's unset.
- Tokens are on disk today, and nothing reads them:
- Claude Code: the adapter uses `--output-format text`
(`adapters/claude/adapter.sh`), so the runner sees only the answer.
Claude Code's own transcript, in the run's HOME at
`home/.claude/projects/<dir>/<session id>.jsonl`, has `model` and
`usage` (input, output, cache write, cache read tokens) on each
answer line. Checked on run re32fc7bd8143, model claude-sonnet-5-5.
- Pi: the session file in the run's session directory has `provider`,
`model` and `usage` (input, output, cacheRead, cacheWrite,
totalTokens, cost) on each answer line. Checked on a Researcher
session, provider zai.
- The launch log has harness, model and family per launch, and no
tokens (`launcher.mjs:269`, `:294`).
- Today every Claude Code session runs on one credential, which the bus
unit's environment file sets (`SESSION_CREDENTIALS`,
`packages/seat/src/session.mjs:76`, decision 119). The stack has no
name for that account. A quota per account needs the organization file
to name each account and its credential reference (never the secret,
invariant 3), and the launch to pick one and log its name. More than
one account at a time needs the bus to hand a credential to each
session, which is the open question in the #237 design row.
- CLIProxyAPI, from Researcher's stand-in test (commit 587e64a27,
`agents/researcher/work/i1-proxy-test/REPORT.md`): usage counters are
kept per upstream credential, not per client key. Per-request events
name the client key, but they last 60 seconds, a read removes them, and
they carry the full plaintext key. All of it is behind the management
API, which no agent calls. The stand-in didn't model token counts.
So the stack can't read per-key usage from the proxy today. It counts
its own sessions' tokens instead, and a per-key quota needs the stack
to issue the keys (#237).
- Token limits stop a session at the end of a turn, because the runner
learns usage only when the turn ends. A long turn can go past its
limit. Stopping inside a turn needs the runner to read streamed usage
from each harness, which changes the adapter contract (stdout is the
answer only, `adapters/README.md`).
- Version 1.3:
- Nothing loads a module by name today. The seams that exist are the
adapter script contract (`adapters/README.md`), the tasks handle the
broker takes (`startBroker({ tasks })`, `packages/bus/src/runtime.mjs`),
the engine the Discord connector takes (`createConnector({ engine })`)
and the validated provider, account and harness records in
`packages/mosaic/src/registry.mjs`, which nothing outside
`packages/mosaic` imports.
- The loader reads the tree the stack runs from. Since row 85 that is a
pinned copy, so the copy's `packages/plugins` is the set of modules a
running stack can switch on.
- Where each kind is hard-coded today (REQ-PLUG-2), from Sage's
read-only survey on 2026-10-14:
- Harnesses: the list `["pi", "claude-code"]` and its branches in
`packages/harness/src/bundle.mjs`, the same pair in the broker and
`packages/business/src/vars.mjs`, an adapter map in
`packages/seat/src/session.mjs`, per-harness mounts in the sandbox,
and `claude --version` in the launcher. `packages/conversation`
and the Discord CLI start Pi directly.
- Model providers: the family match and `LAUNCH_CEILING` (REQ-LIMIT-1
removes both), a session environment that passes only `ANTHROPIC_*`
and the Claude OAuth token (`session.mjs`), and a free-text
`execution.provider` that nothing checks.
- Task tracker: the tasks verbs build Vikunja paths with no
interface between them and the client. The bus always loads
`packages/tasks/src/adapter.mjs`. Task refs (`vikunja:`), branch
names and the grantable services name Vikunja. `tracker.kind`
exists in `vars.mjs` and nothing reads it.
- Git forge: the Gitea host in `scripts/gitea-api.sh` and the git
credential helper, the repository path and seat-to-account mapping
in `packages/queue/src/review.mjs`, the ledger's repository paths,
and the `refactor` branch in `scripts/land-work.sh`.
`gitea.baseUrl` exists in `vars.mjs` and the scripts don't read it.
- Channels: the human channel is `via: 'cli'` in the broker, the
WebUI reaches the bus by running the human CLI, and Discord isn't
connected to the bus.
## Risks and open questions
- In slice 1, agents run as Jason's OS user (round 3, 3A). They can
write anything that user can write. Until seats run in a container,
which comes next, the broker is a rule they follow, not a wall. What
does hold in slice 1 is that agents never hold service tokens.
- Vikunja doesn't enforce `If-Match`. The broker compares before it
writes, and a person editing in the same moment can still be
overwritten in an owned field (lead decision 51).
- The Vikunja owner password and Gitea token creation are gated. Jason
holds them.
- With no ceiling in code, the chief of staff can raise its own launch
capacity and spend (REQ-LIMIT-1, Q150). The controls are the change
log, with who made each change and the old and new values, and the
user's power to change it back.
- A quota or token limit counts only what the stack ran. It can't see the
provider's own remaining quota or use of the same account elsewhere.
- The `transcript` usage source reads files inside the run's HOME, which
the session can edit. Its count is the stack's best reading, not proof.
A source that reads the provider's own report would be harder to
forge.
- A plug-in module runs with the host's authority, and the organization
file only picks among modules already in the tree. The reviewed commit
that adds a module is the only check on what it does, which is why
modules from outside the repository wait for Jason (REQ-PLUG-1).
## Milestones
Slice 1, in the order on the foundation direction page. The slice 1 brief
maps each step to requirement ids. CHAT-03's Gate E demonstration happens
during the slice 1 WebUI step, not separately (round 3, 7B).