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]>
This commit is contained in:
1 parent
9edac91073
commit
240d255187
7 files changed
+335
-43
No files matched your search
@@ -3209,3 +3209,58 @@ which stay with him. Each item names who decided it and what happened.
|
||||
Mos's first paragraph in sage-0187 also listed 9b as its own row.
|
||||
Ruling 8 is the later and more specific instruction, so Sage left 9b
|
||||
unfiled. Decision 123 applies to build steps 5 and 11.
|
||||
|
||||
143. **PRD 1.3: usage sources and limit checks as plug-ins, and an
|
||||
app-wide plug-in layer (2026-10-14, Jason, relayed by Mos (keys
|
||||
mos-38bc4c81-sage-0192 to -0194, replying to sage-0236 and
|
||||
sage-0237)).** Jason answered 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, with no CLIProxyAPI queue
|
||||
reader, and usage sources and quota checks sit behind a plug-in
|
||||
interface. Jason answered 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: every part the core calls
|
||||
out to gets a registered plug-in interface (harnesses, model
|
||||
providers, usage sources, the task tracker, the git forge, 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,
|
||||
and hard-coded parts convert as rows touch them. Jason answered "Q155:
|
||||
yes" (Vikunja #286).
|
||||
|
||||
Mos ruled on the draft's three points:
|
||||
- a. An unknown turn is never zero. Checks count known records only,
|
||||
each unknown turn is logged, and Settings shows the unknown count
|
||||
next to the total.
|
||||
- b. The switches live in the organization file.
|
||||
- c. Because of Q155, the loader is the app-wide plug-in layer from
|
||||
the start (`packages/plugins`, kind registry in
|
||||
`packages/plugins/src/kinds.mjs`), with usage sources and limit
|
||||
checks as its first two kinds. The name rule, the no-symlink rule
|
||||
and "a failed source is unknown, never zero" stay. Plug-ins from
|
||||
outside the repository stay out until Jason rules. Mos is taking
|
||||
that security call to Jason (#286).
|
||||
|
||||
The version is 1.3, not 1.2.1: `packages/tasks/src/requirements.mjs`
|
||||
accepts only `major.minor`, and a new minor needs no code change.
|
||||
The PRD gains REQ-LIMIT-4, REQ-PLUG-1 and REQ-PLUG-2 and amends
|
||||
REQ-LIMIT-2 and REQ-LIMIT-3. Mos reviewed the full text before it
|
||||
landed. The build plan gains step 6b, the plug-in layer, and rows 124
|
||||
to 127 ship their counting and checks as modules.
|
||||
|
||||
Sage proposed an order for converting the hard-coded parts
|
||||
(sage-0237), and Mos put it to Jason as Q156. Jason answered "Q156:
|
||||
yes", so the order stands as written:
|
||||
- 1. Usage first: step 6b, then rows 124 to 126.
|
||||
- 2. Harnesses during row 65, which runs before rows 64 and 66.
|
||||
- 3. Providers named as data in row 122. The provider plug-in waits
|
||||
for row 127.
|
||||
- 4. Channels: `cli` in row 128, `webui` in row 129, Discord when a
|
||||
row connects it to the bus.
|
||||
- 5. The tracker gets its own row after the 1.2 rows, design packet
|
||||
first.
|
||||
- 6. The forge goes last, during the next row that changes the Gitea
|
||||
scripts.
|
||||
- 7. Row 87 decides whether the service manager becomes a kind.
|
||||
The build plan records the order, and the queue runs row 65 before
|
||||
64 and 66.
|
||||
@@ -9,6 +9,12 @@ time in the order below, each after the one before it is done, so no two
|
||||
candidates edit the same files at once. Sage owns all three. The order
|
||||
is after rows 55, 42 and 49.
|
||||
|
||||
Q156 (2026-10-14, lead decision 143) changed the order: row 65 runs
|
||||
first, then 64, then 66. Row 65 is where the harnesses become plug-in
|
||||
modules (PRD 1.3 REQ-PLUG-2), so 64 and 66 add their files to those
|
||||
modules. Sage adds the harness-kind scope to row 65's brief before the
|
||||
row starts.
|
||||
|
||||
Two decisions shape the set. Q84 is its own row, not part of the skills
|
||||
row: the ms-unslop skill is guidance the model reads, and the hook is a
|
||||
check that runs on its output. They differ in files, failure mode and
|
||||
|
||||
@@ -4,6 +4,9 @@ Filed by Sage after Mos's go for build step 1 (sage-0187, lead decision
|
||||
142). The requirements are PRD version 1.2, `docs/prd/mosaic-stack.md`,
|
||||
landed in 7eb5ad0cb. Each section below is one queue row's brief. The
|
||||
steps keep the PRD 1.2 draft's numbering (`agents/sage/work/prd-v1-2/`).
|
||||
PRD version 1.3 (lead decision 143) adds the plug-in layer, build step
|
||||
6b, and rows 124 to 127 follow it: usage sources and limit checks are
|
||||
plug-in modules (REQ-LIMIT-4, REQ-PLUG-1).
|
||||
|
||||
## Rules for every row in this file
|
||||
|
||||
@@ -14,8 +17,11 @@ steps keep the PRD 1.2 draft's numbering (`agents/sage/work/prd-v1-2/`).
|
||||
(decision 109). A landing that changes `packages/webui/src/public/`
|
||||
waits for Mos's go too (decision 123).
|
||||
- No row edits Jason's organization file or role workspaces. If a row
|
||||
needs his file changed, the packet gives the exact change and Sage
|
||||
needs Jason's file changed, the packet gives the exact change and Sage
|
||||
sends it to Mos.
|
||||
- A call-out to a part of a plug-in kind goes through the loader
|
||||
(REQ-PLUG-1, REQ-PLUG-2). No core module imports a plug-in module by
|
||||
name.
|
||||
- A bus schema change ships a migration, tested on a copy of a
|
||||
schema-current database, and lands with a re-pin that has downtime.
|
||||
- One mutant per check, listed in the packet. Each fails a test.
|
||||
@@ -24,6 +30,31 @@ steps keep the PRD 1.2 draft's numbering (`agents/sage/work/prd-v1-2/`).
|
||||
- Limits have no ceiling in code (Q150). Don't add one back as a
|
||||
constant, a validator bound or a default that can't be changed.
|
||||
|
||||
## Plug-in conversion order (Q156)
|
||||
|
||||
Not a queue row. Jason approved this order in Q156 (lead decision 143).
|
||||
Each hard-coded part converts to its plug-in kind on the row named here
|
||||
(PRD REQ-PLUG-2).
|
||||
|
||||
1. Usage sources and limit checks: step 6b, then rows 124, 125 and 126.
|
||||
2. Harnesses: row 65, the model routing table, makes `pi` and
|
||||
`claude-code` the first harness modules. Row 65 runs before rows 64
|
||||
and 66, so skills and unslop land as harness module exports. The
|
||||
conversation and Discord Pi engines convert when a row changes them.
|
||||
3. Model providers: row 122 names the provider as data in its model
|
||||
table. The provider kind (environment allowlist, credential
|
||||
reference, base URL, a CLIProxyAPI module) waits for row 127 and
|
||||
#237.
|
||||
4. Channels: row 128 makes `cli` the first channel module and row 129
|
||||
adds `webui`. Discord becomes a module when a row connects it to the
|
||||
bus. The human proof stays in core.
|
||||
5. Task tracker: its own row after the 1.2 rows, design packet first,
|
||||
because `vikunja:` refs and branch names need a migration.
|
||||
6. Git forge: last, during the next row that changes `gitea-api.sh`,
|
||||
`packages/queue/src/review.mjs` or `land-work.sh`.
|
||||
7. Service manager: not on Q155's list. Row 87 (the I1 design) decides
|
||||
whether it becomes a kind.
|
||||
|
||||
## Role files for the chief of staff and the assistant
|
||||
|
||||
Build step 2, small. Gitea #1613.
|
||||
@@ -219,21 +250,62 @@ logged with its reason; another role still needs one; a chief-of-staff
|
||||
change is validated, written and logged like a human's; an invalid
|
||||
change is refused. Mutants for each.
|
||||
|
||||
## The plug-in layer: kind registry and loader
|
||||
|
||||
Build step 6b, medium. Gitea #1626.
|
||||
|
||||
- Owner: dewey, after step 5.
|
||||
- Reviewers: filbert (organization file and loader) and darkwing (the
|
||||
loader runs code with the host's authority).
|
||||
- Requirements: REQ-PLUG-1, REQ-LIMIT-4 (the first two kinds).
|
||||
|
||||
**What ships.**
|
||||
|
||||
1. `packages/plugins`, with `src/kinds.mjs`, the kind registry. Each
|
||||
entry names a kind, the exports a module must have, the interface
|
||||
version, and whether the organization file switches on one module of
|
||||
that kind or a list.
|
||||
2. `src/loader.mjs`. It reads the names switched on from the
|
||||
organization file's `plugins` object, one key per kind, and loads
|
||||
only `packages/plugins/<kind>/<name>/index.mjs` from the tree the host
|
||||
runs from. It refuses a name that doesn't match `^[a-z][a-z0-9-]*$`,
|
||||
a missing directory, a symlink anywhere in the path including
|
||||
`index.mjs`, a module that fails to load, and a module missing an
|
||||
export or with another interface version. A refusal keeps the old
|
||||
setting and is logged.
|
||||
3. The kinds `usage-source` and `limit-check` at interface version 1,
|
||||
with the exports PRD REQ-LIMIT-4 describes. No module of either kind
|
||||
ships in this row.
|
||||
4. The `plugins` key in the organization-file schema, written through
|
||||
step 5's write path and logged like a limit change.
|
||||
5. A README for the package: how to add a module, and that adding a
|
||||
kind is a core change.
|
||||
|
||||
Only modules in the repository load until Jason rules (Vikunja #286).
|
||||
The loader takes no path from config.
|
||||
|
||||
**Acceptance.** Tests for each refusal with the old setting kept, and a
|
||||
fixture module of each kind switched on in a test organization file with
|
||||
no core file changed. One mutant per check. Darkwing and Filbert
|
||||
approve.
|
||||
|
||||
## Token accounting per run
|
||||
|
||||
Build step 7, medium. Gitea #1618.
|
||||
|
||||
- Owner: rocko, after step 5.
|
||||
- Owner: rocko, after steps 5 and 6b.
|
||||
- Reviewer: dewey (decision 130: the host reads files inside the run's
|
||||
HOME, which the session can write).
|
||||
- Requirements: REQ-LIMIT-2 and REQ-LIMIT-3 (their base), REQ-WEB-1 (the
|
||||
readout).
|
||||
- Requirements: REQ-LIMIT-2 and REQ-LIMIT-3 (their base), REQ-LIMIT-4,
|
||||
REQ-PLUG-1, REQ-WEB-1 (the readout).
|
||||
|
||||
**What ships.**
|
||||
|
||||
1. After each turn, the host reads usage from the Claude Code transcript
|
||||
1. The first usage source, `packages/plugins/usage-source/transcript/`.
|
||||
After each turn the bus calls every usage source switched on, through
|
||||
step 6b's loader. This one reads usage from the Claude Code transcript
|
||||
in the run's HOME (`home/.claude/projects/<dir>/<session id>.jsonl`)
|
||||
or from the Pi session file. It records each kind separately (input,
|
||||
or from the Pi session file. It returns each kind separately (input,
|
||||
output, cache write, cache read) per run, role instance, provider,
|
||||
model and account (lead decision 142, ruling 6). The account is null
|
||||
until step 9a names one.
|
||||
@@ -248,8 +320,11 @@ Build step 7, medium. Gitea #1618.
|
||||
reading, not proof.
|
||||
|
||||
**Acceptance.** Fixture transcripts for both harnesses, including a cut
|
||||
line, a symlink and an oversized file. The migration runs on a copy of a
|
||||
schema-current database. Mutants for each guard. Dewey approves.
|
||||
line, a symlink and an oversized file. A test that the bus reaches the
|
||||
transcript source only through the loader. A test that an unknown turn
|
||||
is logged and that `mosaic usage` and Settings show the unknown count
|
||||
next to each total. The migration runs on a copy of a schema-current
|
||||
database. Mutants for each guard. Dewey approves.
|
||||
|
||||
## Token limits per instance, model, provider and key
|
||||
|
||||
@@ -257,9 +332,12 @@ Build step 8, medium. Gitea #1619.
|
||||
|
||||
- Owner: researcher, after steps 6 and 7.
|
||||
- Reviewer: filbert (launcher and broker checks, no sandbox change).
|
||||
- Requirements: REQ-LIMIT-3.
|
||||
- Requirements: REQ-LIMIT-3, REQ-LIMIT-4.
|
||||
|
||||
**What ships.**
|
||||
**What ships.** The first limit check,
|
||||
`packages/plugins/limit-check/token-limit/`. The launcher asks every
|
||||
check switched on before a launch, and the bus asks after each turn
|
||||
whether to stop, both through step 6b's loader. The check does this:
|
||||
|
||||
1. Token limits per role instance, model, provider and API key, set
|
||||
through step 5's write path and the chief of staff's verb (step 6).
|
||||
@@ -272,7 +350,13 @@ Build step 8, medium. Gitea #1619.
|
||||
logged with the limit and the count.
|
||||
|
||||
**Acceptance.** Tests for each subject kind, the reset, the refusal and
|
||||
the end-of-turn stop (stub harness). Mutants for each.
|
||||
the end-of-turn stop (stub harness). A launch with no check switched on,
|
||||
a fixture check that refuses, a test that the launcher reaches
|
||||
`token-limit` only through the loader, and a test that a check counts
|
||||
known records only. A test that with no usage source switched on every
|
||||
turn is recorded as unknown. A test that a limit no switched-on check
|
||||
enforces shows in Settings and `mosaic limits` as not enforced. Mutants
|
||||
for each.
|
||||
|
||||
## Daily quota for the credential the stack uses today
|
||||
|
||||
@@ -280,7 +364,7 @@ Build step 9a, small to medium. Gitea #1620.
|
||||
|
||||
- Owner: dewey, after step 7.
|
||||
- Reviewer: filbert (organization file and launcher, no sandbox change).
|
||||
- Requirements: REQ-LIMIT-2.
|
||||
- Requirements: REQ-LIMIT-2, REQ-LIMIT-4.
|
||||
|
||||
**What ships.**
|
||||
|
||||
@@ -290,21 +374,25 @@ Build step 9a, small to medium. Gitea #1620.
|
||||
(invariant 3). Launches log the account name, and step 7's records
|
||||
carry it.
|
||||
2. A daily quota on that account, in tokens (a draft position, PRD
|
||||
REQ-LIMIT-2), with a reset time and time zone.
|
||||
REQ-LIMIT-2), with a reset time and time zone. It ships as a second
|
||||
limit check, `packages/plugins/limit-check/daily-quota/`, with no
|
||||
change to the launcher or bus.
|
||||
3. At the quota, new launches on the account are refused and running
|
||||
sessions stop at the end of their current turn. Both are logged.
|
||||
4. Settings and `mosaic limits` show the quota with the line that it
|
||||
counts only the stack's own use (lead decision 142, ruling 9).
|
||||
|
||||
**Acceptance.** Tests for the count, the day boundary in a non-UTC time
|
||||
zone, the refusal and the stop. Mutants for each.
|
||||
zone, the refusal and the stop. Outside
|
||||
`packages/plugins/limit-check/daily-quota/`, the diff touches only item
|
||||
1's account naming and the organization-file schema. Mutants for each.
|
||||
|
||||
## Quota for more than one account or per proxy key (parked on #237)
|
||||
|
||||
Build step 9c, large, not sized. Gitea #1621. Parked until the #237
|
||||
design settles how the stack hands a credential to each session.
|
||||
|
||||
- Requirements: REQ-LIMIT-2.
|
||||
- Requirements: REQ-LIMIT-2, REQ-LIMIT-4.
|
||||
|
||||
Mos's facts (sage-0188, CLIProxyAPI v8.0.29 public source):
|
||||
|
||||
@@ -319,6 +407,12 @@ enforcement needs management API access, which agents don't use and
|
||||
which is Jason's call. The proxy route is an open design point. Nobody
|
||||
reads the proxy's config or key files for this row.
|
||||
|
||||
Under Q153 the stack counts tokens itself, and no proxy reader is built
|
||||
now. A proxy reader would be its own usage source in
|
||||
`packages/plugins/usage-source/`, switched on in config. Per-key quotas
|
||||
would reuse the `daily-quota` check with the key as subject. Both still
|
||||
wait on #237 and on Jason's call on management API access.
|
||||
|
||||
## mosaic talks to the assistant in a named thread
|
||||
|
||||
Build step 10, small to medium. Gitea #1622.
|
||||
|
||||
+157
-23
@@ -1,12 +1,34 @@
|
||||
# PRD: Mosaic Stack
|
||||
|
||||
- Status: approved, version 1.2. Jason approved draft 0.4 unchanged as
|
||||
1.0 on 2026-10-05 (lead decision 61). He approved version 1.1, the
|
||||
- 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. He approved version 1.2, the chief of staff, the
|
||||
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). It is never edited in place. Changes are a new
|
||||
version.
|
||||
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
|
||||
@@ -58,10 +80,10 @@ outside users later, which means:
|
||||
## Problem statement
|
||||
|
||||
In priority order (round 1):
|
||||
1. **Admin falls on Jason.** He launches agents, relays between them,
|
||||
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 on
|
||||
him.**
|
||||
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.
|
||||
|
||||
@@ -72,7 +94,7 @@ In priority order (round 1):
|
||||
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 his approval.
|
||||
- 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.
|
||||
@@ -298,22 +320,90 @@ model, provider, or API key".
|
||||
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, 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 count covers sessions the stack ran. Use of the same account
|
||||
outside the stack isn't counted, and Settings says so. Measuring the
|
||||
quota in tokens is a draft position: Q150 names no unit, and OAuth
|
||||
plans meter by usage windows, not daily tokens.
|
||||
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, and the user and the chief of staff can reset
|
||||
it. A count is the sum of every kind the harness 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.
|
||||
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
|
||||
|
||||
@@ -463,6 +553,42 @@ itself from them, five pieces in a row. For each piece:
|
||||
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
|
||||
|
||||
@@ -481,6 +607,14 @@ itself from them, five pieces in a row. For each piece:
|
||||
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
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ test('requirements returns the committed list to a holding role session, and no
|
||||
const pm = agent('pm');
|
||||
const list = call(b, pm, 'requirements');
|
||||
assert.deepEqual(list, loadRequirements());
|
||||
assert.equal(list.requirements.length, 38);
|
||||
assert.equal(list.requirements.length, 41);
|
||||
assert.deepEqual(call(b, agent('coder'), 'requirements'), list);
|
||||
assert.throws(() => call(b, human, 'requirements'), /agent-required/);
|
||||
assert.throws(() => call(b, reader, 'requirements'), /read-only/);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"prd": "docs/prd/mosaic-stack.md",
|
||||
"version": "1.2",
|
||||
"version": "1.3",
|
||||
"requirements": [
|
||||
{ "id": "REQ-ROLE-1", "title": "Each role is a reviewed roles/<name>.json: contract, authority map, credentials" },
|
||||
{ "id": "REQ-ROLE-2", "title": "One holder per role instance, one per thread for the assistant; the chief of staff may end a run, others need a decision" },
|
||||
@@ -29,6 +29,9 @@
|
||||
{ "id": "REQ-LIMIT-1", "title": "Agent limits have no code ceiling and are set from WebUI Settings and mosaic: total, per provider, per model" },
|
||||
{ "id": "REQ-LIMIT-2", "title": "A daily quota per OAuth account or CLIProxyAPI client key, counting only the stack's own use" },
|
||||
{ "id": "REQ-LIMIT-3", "title": "A token limit per role instance, model, provider or API key, reset by hand, stopping at the end of a turn" },
|
||||
{ "id": "REQ-LIMIT-4", "title": "Usage sources and limit checks are the first plug-in kinds; an unknown turn is logged and shown, never counted as zero" },
|
||||
{ "id": "REQ-PLUG-1", "title": "Every part the core calls out to is a plug-in kind; modules load from packages/plugins, switched on in the org file" },
|
||||
{ "id": "REQ-PLUG-2", "title": "New call-outs go through a plug-in kind from the start; a hard-coded part converts when a row changes it" },
|
||||
{ "id": "REQ-HARN-1", "title": "The meta-harness generates each session's launch bundle from role and variables" },
|
||||
{ "id": "REQ-HARN-2", "title": "Credential scope, verbs, the tool limit and a container enforce the rules" },
|
||||
{ "id": "REQ-EVT-1", "title": "Every slice 1 step emits an event; measures come from events" },
|
||||
|
||||
@@ -18,10 +18,10 @@ test("the list holds the PRD's requirement ids, in its order, at its version", (
|
||||
const ids = [...prd.matchAll(/^- \*\*(REQ-[A-Z]+-[0-9]+)\.\*\*/gm)].map((m) => m[1]);
|
||||
// Any other mention of a bold id would be a bullet this pattern missed.
|
||||
assert.equal(ids.length, [...prd.matchAll(/\*\*REQ-/g)].length);
|
||||
assert.equal(ids.length, 38);
|
||||
assert.equal(ids.length, 41);
|
||||
assert.deepEqual(list.requirements.map((r) => r.id), ids);
|
||||
assert.match(prd, new RegExp(`^- Status: approved, version ${list.version.replace('.', '\\.')}\\.`, 'm'));
|
||||
assert.deepEqual([list.prd, list.version], ['docs/prd/mosaic-stack.md', '1.2']);
|
||||
assert.deepEqual([list.prd, list.version], ['docs/prd/mosaic-stack.md', '1.3']);
|
||||
});
|
||||
|
||||
test('the committed list loads frozen', () => {
|
||||
@@ -74,7 +74,7 @@ test('the schema refuses any other shape', () => {
|
||||
const twice = raw();
|
||||
twice.requirements.push(structuredClone(twice.requirements[0]));
|
||||
writeFileSync(join(dir, 'twice.json'), JSON.stringify(twice));
|
||||
assert.throws(() => loadRequirements(join(dir, 'twice.json')), listError('requirements[38].id: REQ-ROLE-1 twice'));
|
||||
assert.throws(() => loadRequirements(join(dir, 'twice.json')), listError('requirements[41].id: REQ-ROLE-1 twice'));
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user