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:
+33
-11
@@ -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
|
||||||
|
|||||||
@@ -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/);
|
||||||
|
|||||||
@@ -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)" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 });
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user