docs: PRD 1.2, chief of staff, assistant, threads and agent limits (Q147-Q151)

PRD version 1.2, approved by Jason through Mos on 2026-10-14 (lead
decisions 141 and 142). Adds REQ-COS-1, REQ-ASSIST-1/2, REQ-LIMIT-1/2/3,
REQ-USER-2 and REQ-THREAD-2, rewrites the role, launch, message, CLI and
WebUI requirements, and quotes Q147 to Q151 in the source line.
REQ-LIMIT-2 marks the token unit for the daily quota as a draft position.

requirements.json moves to 1.2 with 38 ids; the tasks and bus id-count
tests follow. Full gate green at bea5dfa47 plus these four files.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-14 13:39:58 -05:00
co-authored by Claude Opus 5.5
parent bea5dfa47f
commit 7eb5ad0cb8
4 changed files with 235 additions and 38 deletions
+214 -25
View File
@@ -1,9 +1,28 @@
# PRD: Mosaic Stack # PRD: Mosaic Stack
- Status: approved, version 1.1. Jason approved draft 0.4 unchanged as - 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 1.0 on 2026-10-05 (lead decision 61). He approved version 1.1, the
Organization requirements, in decision 03130d08-ddce-480d-a601-e59f64744fc3 Organization requirements, in decision 03130d08-ddce-480d-a601-e59f64744fc3
on 2026-10-14. It is never edited in place. Changes are a new version. on 2026-10-14. He 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.
- 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. - Owner: Jason. Sage writes it from the PRDY interview.
- Template: PRDY "software" (`v1/packages/prdy/src/templates.ts`), filled - Template: PRDY "software" (`v1/packages/prdy/src/templates.ts`), filled
by hand until PRDY is ported. by hand until PRDY is ported.
@@ -89,21 +108,49 @@ parent requirement is refused.
- the credentials it needs, by service and scope. - the credentials it needs, by service and scope.
Any action the file doesn't list is gated. Any action the file doesn't list is gated.
- **REQ-ROLE-2.** Each role instance has one holder at a time. A claim - **REQ-ROLE-2.** Each role instance has one holder at a time. The
takes a lock and logs it, and releasing the role or ending the run frees assistant is the exception: it has one holder per thread at a time. A
it. Revoking a stuck holder is gated, and the revoke cites a resolved claim takes a lock and logs it, and releasing the role or ending the run
decision. 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, - **REQ-ROLE-3.** An organization file declares the role instances,
the arbiters and the credential references. The user writes it, the stack the arbiters and the credential references. The user and the chief of
never writes it, and a missing or invalid file fails closed. staff write it. The chief of staff's writes go through a broker verb
- **REQ-ROLE-4.** The first organization is Mosaic Stack, with four role 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: instances:
- chief of staff;
- assistant;
- PM, held by Sage; - PM, held by Sage;
- CTO, held by Darkwing; - CTO, held by Darkwing;
- coder; - coder;
- reviewer. - reviewer.
The schema can also declare CEO, CFO and the other C-suite roles. 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 ### Variables
@@ -159,9 +206,12 @@ parent requirement is refused.
### Messages ### Messages
- **REQ-MSG-1.** Messages are addressed to a role, not a thread. They are - **REQ-MSG-1.** Messages are addressed to a role. They are append-only
append-only and go to whoever currently holds the role. T3 and Discord and go to whoever currently holds the role. A message to or from the
carry messages; the address is still the role. 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 ### Tasks
@@ -185,26 +235,85 @@ parent requirement is refused.
### Interface ### Interface
- **REQ-CLI-1.** `mosaic` is the front door. From it Jason can: - **REQ-CLI-1.** `mosaic` is the front door. From it Jason can:
- talk to the PM; - talk to the assistant in a project thread they choose or create;
- see the decision inbox and resolve decisions; - see the decision inbox and resolve decisions;
- see tasks and agents; - see tasks and agents;
- follow the trail of any piece of work. - follow the trail of any piece of work.
- **REQ-CLI-2.** The PM session that `mosaic` talks to is one the stack - **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 launches and owns, running Pi or Claude Code without a window. The
product doesn't depend on T3. (Round 2, answer 5A.) product doesn't depend on T3. (Round 2, answer 5A.)
- **REQ-WEB-1.** The WebUI shows the same data as the CLI: conversations, - **REQ-WEB-1.** The WebUI shows the same data as the CLI: conversations,
the inbox, tasks, agents and trails. 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 ### Launching
- **REQ-LAUNCH-1.** The PM launches role sessions within limits written in - **REQ-LAUNCH-1.** The chief of staff launches role sessions within the
the organization file: agent limits written in the organization file (REQ-LIMIT-1 to
REQ-LIMIT-3):
- role instances only; - role instances only;
- at most 4 Opus and 4 Sonnet sessions at once; - a launch that would exceed any agent limit is refused, and the
- every launch logged; 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. - revocable with one word.
(Round 2, answer 6A.) 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, 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.
- **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.
### Meta-harness ### Meta-harness
@@ -246,20 +355,30 @@ Added in version 1.1 (decision 03130d08).
- **REQ-USER-1.** Users and role-based access control exist at install - **REQ-USER-1.** Users and role-based access control exist at install
and organization level. An agent belongs to one organization and and organization level. An agent belongs to one organization and
reaches only its allowed projects and threads. 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-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 - **REQ-TPL-1.** Templates exist for agent roles and for organization
types. types.
- **REQ-FED-1.** Installs can federate with each other. This is post-v1. - **REQ-FED-1.** Installs can federate with each other. This is post-v1.
## Acceptance criteria and success measures ## Acceptance criteria and success measures
v1 is done when Jason gives one-sentence requests in the CLI and Mosaic v1 is done when Jason gives one-sentence requests to the assistant, in a
Stack builds pieces of itself from them, five pieces in a row. For each thread, from the CLI or the WebUI, and Mosaic Stack builds pieces of
piece: itself from them, five pieces in a row. For each piece:
- only gated decisions reach Jason; - only gated decisions reach Jason;
- every step leaves a trail in the CLI and the WebUI. - every step leaves a trail in the CLI and the WebUI.
(Round 1, answer A.) (Round 1, answer A; the assistant and threads from version 1.2.)
## Technical considerations ## Technical considerations
@@ -280,6 +399,70 @@ piece:
DELETE and REPLACE. Run records stay write-once files. DELETE and REPLACE. Run records stay write-once files.
- None of the three harnesses' hooks fails closed by default except Pi's - 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`. `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`).
## Risks and open questions ## Risks and open questions
@@ -292,6 +475,12 @@ piece:
overwritten in an owned field (lead decision 51). overwritten in an owned field (lead decision 51).
- The Vikunja owner password and Gitea token creation are gated. Jason - The Vikunja owner password and Gitea token creation are gated. Jason
holds them. 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.
## Milestones ## Milestones
+1 -1
View File
@@ -45,7 +45,7 @@ test('requirements returns the committed list to a holding role session, and no
const pm = agent('pm'); const pm = agent('pm');
const list = call(b, pm, 'requirements'); const list = call(b, pm, 'requirements');
assert.deepEqual(list, loadRequirements()); assert.deepEqual(list, loadRequirements());
assert.equal(list.requirements.length, 30); assert.equal(list.requirements.length, 38);
assert.deepEqual(call(b, agent('coder'), 'requirements'), list); assert.deepEqual(call(b, agent('coder'), 'requirements'), list);
assert.throws(() => call(b, human, 'requirements'), /agent-required/); assert.throws(() => call(b, human, 'requirements'), /agent-required/);
assert.throws(() => call(b, reader, 'requirements'), /read-only/); assert.throws(() => call(b, reader, 'requirements'), /read-only/);
+17 -9
View File
@@ -1,11 +1,14 @@
{ {
"prd": "docs/prd/mosaic-stack.md", "prd": "docs/prd/mosaic-stack.md",
"version": "1.1", "version": "1.2",
"requirements": [ "requirements": [
{ "id": "REQ-ROLE-1", "title": "Each role is a reviewed roles/<name>.json: contract, authority map, credentials" }, { "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; a claim locks and logs; revoking a holder is gated" }, { "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" },
{ "id": "REQ-ROLE-3", "title": "A user-written organization file declares instances, arbiters and credential references" }, { "id": "REQ-ROLE-3", "title": "The user and the chief of staff write the organization file; the chief of staff's writes go through a logged broker verb" },
{ "id": "REQ-ROLE-4", "title": "The first organization is Mosaic Stack: PM, CTO, coder and reviewer" }, { "id": "REQ-ROLE-4", "title": "The first organization is Mosaic Stack: chief of staff, assistant, PM, CTO, coder and reviewer" },
{ "id": "REQ-COS-1", "title": "The chief of staff runs the organization: hires, terminates, launches; never talks to the user" },
{ "id": "REQ-ASSIST-1", "title": "The assistant is the user's main agent and passes organization work to the chief of staff" },
{ "id": "REQ-ASSIST-2", "title": "The broker limits the assistant to the user and the chief of staff in both directions; refusals logged" },
{ "id": "REQ-VAR-1", "title": "Variables in four layers, declared in code; limits intersect; unknown keys refused" }, { "id": "REQ-VAR-1", "title": "Variables in four layers, declared in code; limits intersect; unknown keys refused" },
{ "id": "REQ-VAR-2", "title": "One system config, never auto-written; secrets only as references the broker reads" }, { "id": "REQ-VAR-2", "title": "One system config, never auto-written; secrets only as references the broker reads" },
{ "id": "REQ-CRED-1", "title": "Each role instance has its own Gitea and Vikunja token, held by the broker" }, { "id": "REQ-CRED-1", "title": "Each role instance has its own Gitea and Vikunja token, held by the broker" },
@@ -14,22 +17,27 @@
{ "id": "REQ-DEC-2", "title": "Decisions route by class: logged, to the domain arbiter, or to the human" }, { "id": "REQ-DEC-2", "title": "Decisions route by class: logged, to the domain arbiter, or to the human" },
{ "id": "REQ-DEC-3", "title": "Authorized-human resolutions come only from a mosaic CLI session outside any agent run" }, { "id": "REQ-DEC-3", "title": "Authorized-human resolutions come only from a mosaic CLI session outside any agent run" },
{ "id": "REQ-DEC-4", "title": "A blocking gated decision reaches an authorized human at once; the rest go in a daily digest" }, { "id": "REQ-DEC-4", "title": "A blocking gated decision reaches an authorized human at once; the rest go in a daily digest" },
{ "id": "REQ-MSG-1", "title": "Messages are addressed to a role, append-only, delivered to the current holder" }, { "id": "REQ-MSG-1", "title": "Messages are addressed to a role, append-only, delivered to the current holder; assistant messages name a thread" },
{ "id": "REQ-TASK-1", "title": "Tasks live in Vikunja, each citing a requirement id; the verbs enforce field writers" }, { "id": "REQ-TASK-1", "title": "Tasks live in Vikunja, each citing a requirement id; the verbs enforce field writers" },
{ "id": "REQ-TASK-2", "title": "Polling is the source of truth: a 30-second board poll and an hourly reconcile" }, { "id": "REQ-TASK-2", "title": "Polling is the source of truth: a 30-second board poll and an hourly reconcile" },
{ "id": "REQ-TASK-3", "title": "The installer points at an existing Vikunja or deploys the unmodified upstream image" }, { "id": "REQ-TASK-3", "title": "The installer points at an existing Vikunja or deploys the unmodified upstream image" },
{ "id": "REQ-TASK-4", "title": "The queue keeps the agents' lock, review rounds and audit log; no two-way sync" }, { "id": "REQ-TASK-4", "title": "The queue keeps the agents' lock, review rounds and audit log; no two-way sync" },
{ "id": "REQ-CLI-1", "title": "mosaic is the front door: talk to the PM, the inbox, tasks, agents and trails" }, { "id": "REQ-CLI-1", "title": "mosaic is the front door: talk to the assistant in a thread, the inbox, tasks, agents and trails" },
{ "id": "REQ-CLI-2", "title": "The PM session mosaic talks to is launched and owned by the stack, not T3" }, { "id": "REQ-CLI-2", "title": "The assistant session mosaic talks to is launched and owned by the stack, not T3" },
{ "id": "REQ-WEB-1", "title": "The WebUI shows the same data as the CLI" }, { "id": "REQ-WEB-1", "title": "The WebUI shows the CLI's data, a chat area of projects and threads, and Settings for agent limits" },
{ "id": "REQ-LAUNCH-1", "title": "The PM launches role sessions within organization-file limits, logged and revocable" }, { "id": "REQ-LAUNCH-1", "title": "The chief of staff launches role sessions within the agent limits, logged and revocable; threads start on demand" },
{ "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-HARN-1", "title": "The meta-harness generates each session's launch bundle from role and variables" }, { "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-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" }, { "id": "REQ-EVT-1", "title": "Every slice 1 step emits an event; measures come from events" },
{ "id": "REQ-ORG-1", "title": "Business becomes organization; an install holds 1-N, each with its own bus as tenant boundary" }, { "id": "REQ-ORG-1", "title": "Business becomes organization; an install holds 1-N, each with its own bus as tenant boundary" },
{ "id": "REQ-ORG-2", "title": "Data model: install > organization > projects > threads" }, { "id": "REQ-ORG-2", "title": "Data model: install > organization > projects > threads" },
{ "id": "REQ-USER-1", "title": "Users and RBAC at install and organization level; an agent reaches only its allowed projects and threads" }, { "id": "REQ-USER-1", "title": "Users and RBAC at install and organization level; an agent reaches only its allowed projects and threads" },
{ "id": "REQ-USER-2", "title": "Only a thread's owning user, their assistant and the chief of staff's replies reach the thread" },
{ "id": "REQ-THREAD-1", "title": "Threads live under projects" }, { "id": "REQ-THREAD-1", "title": "Threads live under projects" },
{ "id": "REQ-THREAD-2", "title": "The user creates projects and named threads; each thread is its own assistant session with kept history" },
{ "id": "REQ-TPL-1", "title": "Templates for agent roles and for organization types" }, { "id": "REQ-TPL-1", "title": "Templates for agent roles and for organization types" },
{ "id": "REQ-FED-1", "title": "Federation between installs (post-v1)" } { "id": "REQ-FED-1", "title": "Federation between installs (post-v1)" }
] ]
+3 -3
View File
@@ -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]); 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. // 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, [...prd.matchAll(/\*\*REQ-/g)].length);
assert.equal(ids.length, 30); assert.equal(ids.length, 38);
assert.deepEqual(list.requirements.map((r) => r.id), ids); assert.deepEqual(list.requirements.map((r) => r.id), ids);
assert.match(prd, new RegExp(`^- Status: approved, version ${list.version.replace('.', '\\.')}\\.`, 'm')); assert.match(prd, new RegExp(`^- Status: approved, version ${list.version.replace('.', '\\.')}\\.`, 'm'));
assert.deepEqual([list.prd, list.version], ['docs/prd/mosaic-stack.md', '1.1']); assert.deepEqual([list.prd, list.version], ['docs/prd/mosaic-stack.md', '1.2']);
}); });
test('the committed list loads frozen', () => { test('the committed list loads frozen', () => {
@@ -74,7 +74,7 @@ test('the schema refuses any other shape', () => {
const twice = raw(); const twice = raw();
twice.requirements.push(structuredClone(twice.requirements[0])); twice.requirements.push(structuredClone(twice.requirements[0]));
writeFileSync(join(dir, 'twice.json'), JSON.stringify(twice)); writeFileSync(join(dir, 'twice.json'), JSON.stringify(twice));
assert.throws(() => loadRequirements(join(dir, 'twice.json')), listError('requirements[30].id: REQ-ROLE-1 twice')); assert.throws(() => loadRequirements(join(dir, 'twice.json')), listError('requirements[38].id: REQ-ROLE-1 twice'));
} finally { } finally {
rmSync(dir, { recursive: true, force: true }); rmSync(dir, { recursive: true, force: true });
} }