prd: version 1.1, the Organization requirements (decision 03130d08, task 19/265)

Squash of the PM's work/vikunja-19-265 branch, net diff 0c3826910..8ae5a1491
(patch sha256 47b34e6054a74eedd8a5b6577e40537a07b2fb8fe5846f309e100146238ef743).
The PRD moves to version 1.1. "Business" becomes "organization" in
REQ-ROLE-3, REQ-ROLE-4 and REQ-LAUNCH-1. REQ-DEC-3 and REQ-DEC-4 name an
authorized human instead of Jason. Non-goal D adds that v1 has no hosted
SaaS, and "Where it runs" says Mosaic Stack stays self-hosted open source.
A new Organizations section adds REQ-ORG-1, REQ-ORG-2, REQ-USER-1,
REQ-THREAD-1, REQ-TPL-1 and REQ-FED-1. packages/tasks/requirements.json
matches at version 1.1 with 30 ids, and the tasks and bus requirements
tests expect 30.

The branch's 010d30cbe (parseRequirementId, task 19/229) is reverted on
the branch and is not part of this squash.

Jason approved the PRD in decision 03130d08 and the merge in decision
f71c73c9 (resolved yes via the CLI, 2026-10-14T16:35:32Z). The stack
reviewer's verdict d3f97f9d approves 8ae5a1491. Sage's gate on 1c9a4910d
plus the patch passed: every package suite and every scripts/test-*.sh.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-14 11:49:36 -05:00
co-authored by Claude Opus 5.5
parent 4bc908991c
commit f7c8537b32
4 changed files with 50 additions and 22 deletions
+33 -11
View File
@@ -1,8 +1,9 @@
# PRD: Mosaic Stack # PRD: Mosaic Stack
- Status: approved, version 1.0. Jason approved draft 0.4 unchanged as - Status: approved, version 1.1. Jason approved draft 0.4 unchanged as
1.0 on 2026-10-05 (lead decision 61). It is never edited in place. 1.0 on 2026-10-05 (lead decision 61). He approved version 1.1, the
Changes are a new version. Organization requirements, in decision 03130d08-ddce-480d-a601-e59f64744fc3
on 2026-10-14. It is never edited in place. Changes are a new version.
- 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.
@@ -54,11 +55,14 @@ off the table (round 1, "all five"):
- A. Spend money without Jason. - A. Spend money without Jason.
- B. Speak externally as Jason without his approval. - B. Speak externally as Jason without his approval.
- C. Give agents Jason's personal credentials. Each role gets its own. - C. Give agents Jason's personal credentials. Each role gets its own.
- D. Become a multi-tenant SaaS in v1. - 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. - E. Replace Claude Code, Codex or Pi. It wraps them.
### Where it runs ### 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 Self-hosted, on Jason's machines and homelab (round 1, "A first"). Cloud
workers aren't ruled out later. workers aren't ruled out later.
@@ -89,10 +93,10 @@ parent requirement is refused.
takes a lock and logs it, and releasing the role or ending the run frees takes a lock and logs it, and releasing the role or ending the run frees
it. Revoking a stuck holder is gated, and the revoke cites a resolved it. Revoking a stuck holder is gated, and the revoke cites a resolved
decision. decision.
- **REQ-ROLE-3.** A business file declares the role instances, the - **REQ-ROLE-3.** An organization file declares the role instances,
arbiters and the credential references. The user writes it, the stack the arbiters and the credential references. The user writes it, the stack
never writes it, and a missing or invalid file fails closed. never writes it, and a missing or invalid file fails closed.
- **REQ-ROLE-4.** The first business is Mosaic Stack, with four role - **REQ-ROLE-4.** The first organization is Mosaic Stack, with four role
instances: instances:
- PM, held by Sage; - PM, held by Sage;
- CTO, held by Darkwing; - CTO, held by Darkwing;
@@ -145,11 +149,11 @@ parent requirement is refused.
- routine and within-role decisions are logged; - routine and within-role decisions are logged;
- cross-role decisions go to the business's arbiter for that domain; - cross-role decisions go to the business's arbiter for that domain;
- gated decisions go to the human. - gated decisions go to the human.
- **REQ-DEC-3.** A human resolution comes only from a `mosaic` CLI session - **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 started outside any agent run. Agents reach the decision store only
through a broker. The broker stamps role and run from the launch record. through a broker. The broker stamps role and run from the launch record.
- **REQ-DEC-4.** A gated decision that blocks work reaches Jason right - **REQ-DEC-4.** A gated decision that blocks work reaches an authorized human
away: in the CLI inbox, plus a Discord DM sent through the existing 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 connector (#1509). Everything else goes into one daily digest at 08:00
Central. (Round 2, answers 2A and 3A; round 3, 4A and 5A.) Central. (Round 2, answers 2A and 3A; round 3, 4A and 5A.)
@@ -194,7 +198,7 @@ parent requirement is refused.
### Launching ### Launching
- **REQ-LAUNCH-1.** The PM launches role sessions within limits written in - **REQ-LAUNCH-1.** The PM launches role sessions within limits written in
the business file: the organization file:
- role instances only; - role instances only;
- at most 4 Opus and 4 Sonnet sessions at once; - at most 4 Opus and 4 Sonnet sessions at once;
- every launch logged; - every launch logged;
@@ -229,6 +233,24 @@ parent requirement is refused.
store. Measures are computed from events, never from tagging chat store. Measures are computed from events, never from tagging chat
messages by hand. 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-THREAD-1.** Threads live under projects.
- **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 ## 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 in the CLI and Mosaic
+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, 24); assert.equal(list.requirements.length, 30);
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/);
+13 -7
View File
@@ -1,19 +1,19 @@
{ {
"prd": "docs/prd/mosaic-stack.md", "prd": "docs/prd/mosaic-stack.md",
"version": "1.0", "version": "1.1",
"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; a claim locks and logs; revoking a holder is gated" },
{ "id": "REQ-ROLE-3", "title": "A user-written business file declares instances, arbiters and credential references" }, { "id": "REQ-ROLE-3", "title": "A user-written organization file declares instances, arbiters and credential references" },
{ "id": "REQ-ROLE-4", "title": "The first business is Mosaic Stack: PM, CTO, coder and reviewer" }, { "id": "REQ-ROLE-4", "title": "The first organization is Mosaic Stack: PM, CTO, coder and reviewer" },
{ "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" },
{ "id": "REQ-CRED-2", "title": "A session that finds only founder credentials stops" }, { "id": "REQ-CRED-2", "title": "A session that finds only founder credentials stops" },
{ "id": "REQ-DEC-1", "title": "Every decision is an append-only record: raiser, class, options, resolver, times" }, { "id": "REQ-DEC-1", "title": "Every decision is an append-only record: raiser, class, options, resolver, times" },
{ "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": "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 Jason 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" },
{ "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" },
@@ -22,9 +22,15 @@
{ "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 PM, 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 PM 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 same data as the CLI" },
{ "id": "REQ-LAUNCH-1", "title": "The PM launches role sessions within business-file limits, logged and revocable" }, { "id": "REQ-LAUNCH-1", "title": "The PM launches role sessions within organization-file limits, logged and revocable" },
{ "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-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-THREAD-1", "title": "Threads live under projects" },
{ "id": "REQ-TPL-1", "title": "Templates for agent roles and for organization types" },
{ "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, 24); assert.equal(ids.length, 30);
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.0']); assert.deepEqual([list.prd, list.version], ['docs/prd/mosaic-stack.md', '1.1']);
}); });
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[24].id: REQ-ROLE-1 twice')); assert.throws(() => loadRequirements(join(dir, 'twice.json')), listError('requirements[30].id: REQ-ROLE-1 twice'));
} finally { } finally {
rmSync(dir, { recursive: true, force: true }); rmSync(dir, { recursive: true, force: true });
} }