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]>
624 lines
31 KiB
Markdown
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).
|