docs(plans): starter packs fix round 3, system types removed outright

This commit was merged in pull request #1949.
This commit is contained in:
jason.woltje committed 2026-11-12 15:18:16 -06:00
1 parent dd878cefcf
commit c0a60ed3ae
1 file changed
+292 -164
+292 -164
View File
@@ -105,7 +105,8 @@ returns the document.
Shipped and installed packs follow the loader exactly: the same component and
symlink checks, the same refusal codes, a refused installed pack omitted and
logged, a refused core pack failing the listing as system types do today.
logged, a refused core pack failing the listing as `installedSystemTypes`
(`system-types.mjs:5`) does until SP9 removes it.
An installed pack is executable PL1 code. The loader imports its `index.mjs`
(`loadPinnedModule`, `packages/plugins/src/loader.mjs:244-256`), and a loaded
@@ -123,9 +124,13 @@ A saved template is read, never imported or run. `parseJsonFileText`
(`packages/organization/src/util.mjs:66`) is a bare `JSON.parse` with no file
checks, so `readSavedTemplate` does them first:
- the account and the template name each match `NAME`
(`packages/plugins/src/kinds.mjs:20`) before any path is built, so `..`
and `/` never reach the file system;
- the account is the calling session's `s.human`, which the owner-only
verbs already require to equal the organization's `human`. It is checked
against `ID_PATTERN` (`packages/organization/src/vocabulary.mjs:189`),
and the template name against `NAME` (`packages/plugins/src/kinds.mjs:20`),
before any path is built, so `/` and a leading `.` never reach the file
system. When row 193 opens these verbs to members, the account becomes
the member's verified account id under the same check;
- the path resolves only under `<configDir>/templates/<caller account>/`;
`saved:<name>` never names another account's file;
- `lstat` refuses a symlink, a file not owned by the instance's uid, and a
@@ -348,8 +353,11 @@ exactly as if a person had made them by hand. The steps:
re-apply table never meets half-applied items without a `packs` record.
3. **Hires.** Apply never hires. It leaves the pack's hires as proposals in
the `packs` entry. Each becomes a hire through the existing paths:
- the chief raises `hire.raise` (`broker.mjs:6237`) with role
`{definition, rev}` and a route it chooses; the broker computes the
- the chief raises `hire.raise` (`broker.mjs:6237`) with the proposal
and a route it chooses. After a person's apply, which wrote the
definitions, the pin is `{definition, rev}`; after an agent apply,
which writes none, it is a template or inline pin (next subsection
but one). Either way the broker computes the
flagged and credential lines, and the decision routes by the authority
autonomy level;
- or a person with `org.hire` starts it with `hire.start.console`, which
@@ -371,9 +379,11 @@ exactly as if a person had made them by hand. The steps:
| Layer | Name |
|---|---|
| CLI (owner verb) | `mosaic org pack list`, `mosaic org pack preview ID PACK [--project P]`, `mosaic org pack apply ID PACK --digest D [--project P]`; `mosaic org board set ID FILE [--project P]`; `mosaic org workflow set ID NAME FILE [--project P]`, `mosaic org workflow remove ID NAME [--project P]` |
| Console route | `GET /api/packs`, `POST /api/orgs/<id>/packs/preview`, `POST /api/orgs/<id>/packs/apply`, `POST /api/orgs/<id>/board`, `POST /api/orgs/<id>/workflows/<name>` |
| Bus ops | `packs.console` and, for agents, `packs` (reads), `pack.preview.console`, `pack.apply.console`, `board.set.console`, `workflow.set.console`; and for agents, `pack.preview`, `pack.apply`, `board.set` and `workflow.set` for a role whose file lists `organization.change` |
| `VERB_RIGHTS` | `packs` and `packs.console` to `board.read`. `pack.preview.console`, `pack.apply.console`, `board.set.console` and `workflow.set.console` to `null` (owner only) |
| Console route | `GET /api/packs`, `POST /api/orgs/<id>/packs/preview`, `POST /api/orgs/<id>/packs/apply`, `POST /api/orgs/<id>/board`, `POST /api/orgs/<id>/workflows/<name>`, `DELETE /api/orgs/<id>/workflows/<name>` |
| Bus ops, Console | `packs.console` (read), `pack.preview.console`, `pack.apply.console`, `board.set.console`, `workflow.set.console` |
| Bus ops, unsuffixed | `packs`, `pack.preview`, `pack.apply`, `board.set` and `workflow.set`. The CLI reaches the bus as a plain human session, and `broker.mjs:6047` refuses a `.console` verb from one (`human-required`), so the CLI words call these, as `org-definitions.mjs:86-89` calls the unsuffixed `definition.*`. They serve the organization's human only, gated as the definition writes are (`broker.mjs:6305-6308`) (SP4), and an agent whose role file lists `organization.change` (SP5) |
| Remove | `workflow.set` (or `.console`) with `workflow: null` removes that workflow |
| `VERB_RIGHTS` | `packs.console` to `board.read`. `pack.preview.console`, `pack.apply.console`, `board.set.console` and `workflow.set.console` to `null` (owner only). The unsuffixed verbs are not in `VERB_RIGHTS`: unsuffixed keys there are reader session verbs (`BOARD_VERBS`, `broker.mjs:199`) |
Owner only, for now, because that is what the bus does today for definition
writes. `VERB_RIGHTS` maps the T1 definition verbs to `org.hire`, but the
@@ -420,16 +430,25 @@ What it does:
marks such a proposal when its `from` template is in `launch.noHire`. The
broker computes the lines against the chief either way, and the approved
hire writes `hire-<decision>` as it does now.
- the pin recorded in `proposedHires` is checked again when the chief raises
the hire, so a proposal never resolves to text nobody reviewed. A template
pin that no longer matches refuses `decision-stale` and a definition whose
`rev` moved refuses `definition-rev-conflict`, both existing checks
(`hire-decisions.mjs:225-226`). An inline entry whose hash differs from the
recorded one refuses the new `starter-pack-proposal-stale`. For an inline
entry the broker also refuses at raise when the entry's `from` template is
in `launch.noHire`, with `definition-not-hireable`, the code
`hire-decisions.mjs:219` gives for a template, since the `{inline}` path
itself skips that check.
- the chief raises a proposal with `hire.raise {proposal: {pack, project?,
hire}, route, reason}`. The broker reads the matching
`packs[].proposedHires` entry from the loaded organization (SP3 keeps
`packs`). It fills `op: "hire"`, `name`, `level` and `role` from the
recorded pin and refuses any `role`, `name` or `level` the caller also
sends. Then it checks the pin: a `{template, sha256}` whose `templateHash`
moved refuses `decision-stale`; a `{definition, rev}` whose entry moved
refuses `definition-rev-conflict`; an inline proposal reads the full entry
stored in the `packs` entry, refuses `starter-pack-proposal-stale` if its
canonical hash differs from the pin, and refuses `definition-not-hireable`
(the code `hire-decisions.mjs:219` gives for a template) if its `from`
template is in `launch.noHire`. It then passes
`role: {inline: compositionOf(entry)}`. The broker strips `proposal`
before `prepare`, so `requestKeys` (`hire-decisions.mjs:27,190`) is
unchanged. A raise without `proposal` is today's path, unchanged. This
is needed because `hire.raise` passes its args unchanged to `#raiseHire`
(`broker.mjs:6237-6240`), and the existing `decision-stale` and
`definition-rev-conflict` checks (`hire-decisions.mjs:225-226`) compare
against what the caller sends, not against the recorded pin.
### A second pack, and re-applying
@@ -485,7 +504,7 @@ and change decisions stay chief-only.
| Hire, fire, change an agent | `hire.start.console` (`org.hire`) | `hire.raise` (only the chief: `role-not-chief`, `broker.mjs:6583`; hired agents never hold `role.hire`, R9) | Broker-computed flagged and credential lines; authority autonomy level; root needs the instance owner. |
| Apply a pack | `pack.apply.console` (owner; `org.hire` once row 193 lands) | `pack.apply` under `organization.change`, no definitions | As above for every hire it later proposes. |
| Import a workflow or board from a pack | Part of apply | Part of the chief's apply | No capability. |
| Create or edit a workflow | `workflow.set.console` (owner; `org.hire` once row 193 lands) | `workflow.set` under `organization.change` | Steps name existing definitions, templates or `human`, and the five states. A workflow grants nothing; agents read it as guidance below the contract. |
| Create or edit a workflow | `workflow.set.console` (owner; `org.hire` once row 193 lands) | `workflow.set` under `organization.change` | At write time, steps name existing definitions, templates or `human`, and the five states. A workflow grants nothing; agents read it as guidance below the contract. |
| Change board labels or order | `board.set.console` (owner; `org.hire` once row 193 lands) | `board.set` under `organization.change` | The five states stay. |
| Project agents, board and workflow | The same verbs with `--project` | The same, under `organization.change` | Project agent entries refuse instance credential material (`BOUND_KEYS`, `agent-levels.mjs:21`). |
| Enable a skill on the machine | `scripts/skill.sh activate` (instance owner) | Not available | Machine-scoped. |
@@ -614,9 +633,14 @@ There is no create-time shortcut that skips the hire decisions.
## 6. System types
**Decision: the kind is replaced.** The `system-type` kind becomes
`starter-pack`. A pack's `label` is what the UI shows as the system type. The
reasons:
**Decision: removed outright.** Jason, 2026-11-12, about 21:05Z, answering
section 9 question 1:
> The only deployed system is the dev environment on dragon-lin. It can be
> wiped and reconfigured. No reason to hold on to legacy for this change.
The `system-type` kind is deleted and `starter-pack` takes its place. A
pack's `label` is what the UI shows as the system type. The reasons:
- Nothing reads system types today except the enable and select verbs, and
every module's `toolsets` is empty. A list switch that grants nothing has no
@@ -624,39 +648,102 @@ reasons:
- Keeping both would leave a second, empty configuration surface next to the
real one.
### Migration
There is no retired kind, no fence, no conversion step and no new refusal
code.
- **Org files, in two steps.** Removing the kind outright would brick every
org file that names it: `checkPlugins` refuses an unknown kind, even with an
empty list (`unknown-kind`, `kinds.mjs:146-148`, run at
`organization-config.mjs:358`). An organization would stop loading until
its owner converted the file by hand, which is an outage and a forced
human step. So:
1. For one release `system-type` stays in `KINDS` as a retired kind. The
reader accepts the key and ignores it. A write that adds a name refuses
`verb-retired` (a new code). The next writer commit for that organization, by anyone,
drops the key, and its trail event records the old list as evidence.
Nothing is applied for it, since the old modules carried nothing. The
Console offers the matching packs once.
2. A print-only conversion in the style of `migrateLaunch`
(`packages/organization/src/migrate-launch.mjs`, decision 145) covers a
file that is never written in between; it is built in this go (SP10).
Removing the kind from `KINDS` is not in this go. It is a follow-up row
with its own go, after one release, once each instance's owner has run
the conversion or every org file has been written. A build session
cannot see other instances' org files, so it cannot make that check.
- **`project_system_types`.** The table stays, rows and triggers unchanged
(write-once, R2). `project.system-types.set` refuses with a new code,
`verb-retired` (not found today). The read verb keeps working for history
until a later PRD removes it. No DDL change.
- **Code removed.** `systemTypesFence`, `system-types.console`, the system
types Console route and page, the CLI words (`mosaic org system-types`,
`mosaic project system-types`), the channel verbs,
`system-type-not-enabled`, `installedSystemTypes` and its bus reads, each
with its tests (section 1's three system-type rows list every file).
`systemTypesFence` is replaced by a fence that lets any writer drop the
key and refuses any write that adds or changes a name (`verb-retired`). The five modules move to
`packages/plugins/starter-pack/`.
### Org files
`system-type` leaves `KINDS`. An org file that still names it, even with an
empty list, refuses to load as an unknown kind: `checkPlugins` refuses
`unknown-kind` (`packages/plugins/src/kinds.mjs:146-148`, run at
`packages/organization/src/organization-config.mjs:358`), as it does for any
other name not in `KINDS`. Nothing is carried over, since the old modules
carried nothing. The one deployed system, the dragon-lin dev environment, is
wiped and reconfigured (the ruling above).
### `project_system_types` and the bus schema step
A new bus schema step `3v-3w` drops the table:
- `packages/bus/src/migrations/3v-3w.sql` (new) drops the four triggers
`project_system_types_no_update`, `project_system_types_no_delete`,
`project_system_types_no_replace` and `project_system_types_business`,
the index `project_system_types_project`, and the table, all of which
`3t-3u.sql` created. Its rows go with it. This is the one deletion of
append-only rows in this go, and the ruling above is the authority for
it. SQLite removes the table's `sqlite_sequence` row when it drops the
table.
- `packages/bus/src/store.mjs`: `SCHEMA_VERSION` `'3v'` to `'3w'` (line
17); a `{from: '3v', to: '3w'}` entry after the `3u-3v` step
(lines 121-122); `project_system_types` out of `timedTables`
(lines 124-150); `SCHEMA_SHA256` (line 16) recomputed, because
`packages/bus/schema.sql` loses the same DDL and line 153 refuses a
mismatch (`schema-source-mismatch`).
The pins the step moves:
- `packages/bus/tests/schema.test.mjs:533-534`: the table count
(`count('table') - 1`) goes from 33 to 32 and the trigger count from 130
to 126. The view count stays 5. The comment at line 532 gains the
3v to 3w line.
- The restore-guard lists: `COUNTED` in `scripts/stack-release.sh:443` and
`LISTED` in `packages/cli/tests/stack-release.test.mjs:888` lose the
table, so T45 and T46 (`stack-release.test.mjs:940`) still find every
table in exactly one list. The two subtests that use it as their sample
table (`stack-release.test.mjs:906,928`) take another listed table.
- Every test that asserts `'3v'` or edits the `SCHEMA_VERSION = '3v'` line:
20 files today (`grep -rlE "'3v'" packages/*/tests`), the same set the
`3u-3v` step changed in `9b7bf11af`.
- The tests that cut each step's text out of `schema.sql` to build an older
fixture (`request-intake.test.mjs:282,320`,
`request-console.test.mjs:313-316`, `request-continuation.test.mjs:234`,
`prd-activation.test.mjs:202`) and the table lists that name it
(`account-migration.test.mjs:22`, `grant-migration.test.mjs:25`,
`gitea-migration.test.mjs:39`, `session-clear.test.mjs:535`,
`thread-shares.test.mjs:411`). Once `schema.sql` lacks the `3t-3u` text,
cutting it is a no-op and the replayed chain (`3t-3u`, then `3v-3w`) still
ends at the fresh digest. SP9 removes the dead cuts and the list entries.
### Code removed
- `packages/plugins/src/kinds.mjs:84-86` (the kind),
`packages/plugins/src/system-types.mjs`, and
`packages/plugins/system-type/` (the five modules; SP2 reuses their ids,
labels and descriptions as text, nothing else).
- The module check at `organization-config.mjs:360-365`; `systemTypesFence`
and its two calls (`packages/organization/src/organization.mjs:126-131,320,332`)
with its refusal `organization-system-types-human-only`.
- `'system-types.console'` in `CONSOLE_VERBS` (`packages/bus/src/broker.mjs:181`)
and in `VERB_RIGHTS` (`packages/organization/src/org-rights.mjs:90`), its
handler (`broker.mjs:6112-6130`), `project.system-types[.set]`
(`broker.mjs:8476-8495`), and the `'system-type'` half of the tester test
at `broker.mjs:6263` (the `model-provider` half stays).
- `system-types.installed` and `system-types.current` with the
`installedSystemTypes` import (`packages/bus/src/runtime.mjs:30,903-904`).
- `system-type-not-enabled` from `packages/bus/src/refusals.mjs:183`.
- CLI words `mosaic org system-types installed|enabled`
(`packages/cli/src/org.mjs:5,15,49,51`), `mosaic project system-types`
and `system-types-set` (`packages/cli/src/project.mjs:17-18,33-34,50-58`),
and their entries in the `project` dispatch list
(`packages/cli/src/cli.mjs:781`).
- The channel verbs `project.system-types` and `project.system-types.set`
(`packages/plugins/channel/cli/index.mjs:9-10`).
- Console: `packages/webui/src/public/system-types.js` and
`system-types.css`, `packages/webui/src/system-types-http.mjs`, and the
hooks in `public/workspace.js:5,173,193-194`, `public/chat.js:623,691`,
`public/index.html:10`, `public/bus.js:1926` (a comment),
`src/serve.mjs:23,85-86,826,945,997,1068-1073` and
`src/cli.mjs:9,193`.
- Tests: `system-type.test.mjs` in `packages/bus`, `packages/cli`,
`packages/organization` and `packages/plugins`;
`packages/webui/tests/system-types-browser.test.mjs`,
`system-types-http.test.mjs` and `helpers/system-types-fake.mjs`; and the
system-type cases in the other test files that
`grep -rlnE "system-type|systemType|system_type" packages` lists.
- Docs: the system-type text in `packages/cli/README.md`,
`packages/organization/README.md` and `packages/plugins/README.md`;
REQ-SYS-1 and REQ-SYS-2 (next subsection) and their titles in
`packages/tasks/requirements.json:73-74`.
### REQ-SYS-1 and REQ-SYS-2
@@ -777,10 +864,10 @@ proposals. The workflow's "relevant officer" step lists all four in its
## 8. Build slices
The slices run in order, SP1 to SP10, each landing with its own tests and
the full suites green. One go from Jason covers all ten. Nothing in a slice
The slices run in order, SP1 to SP9, each landing with its own tests and
the full suites green. One go from Jason covers all nine (his "full send",
2026-11-12 about 21:05Z, with the section 9 answers). Nothing in a slice
waits on a further design choice, a release, or data on a live instance.
Removing `system-type` from `KINDS` is not in this go (section 6, step 2).
**What the go covers.** No slice edits a file under `roles/` or `contracts/`.
The repository has no separate schema files; the schemas are the validators
@@ -791,22 +878,29 @@ that need sign-off are these:
- new org file keys `packs`, `board` and `workflows`, and project keys
`board` and `workflows` (SP3);
- new verbs in `VERB_RIGHTS` (`packages/organization/src/org-rights.mjs:34`)
and `CONSOLE_VERBS` (`packages/bus/src/broker.mjs:181`): `packs`,
and `CONSOLE_VERBS` (`packages/bus/src/broker.mjs:181`):
`packs.console`, `pack.preview.console`, `pack.apply.console`,
`board.set.console`, `workflow.set.console` (SP4), and
`template.save.console`, `template.list.console` (SP8). Every write verb
is owner only (`null`) until row 193;
- human CLI verbs `packs`, `pack.preview`, `pack.apply`, `board.set`,
`workflow.set` (SP4) and `template.save`, `template.list` (SP8), for the
organization's human only until row 193;
- agent verbs `packs`, `pack.preview`, `pack.apply`, `board.set` and
`workflow.set` under `organization.change` (SP5);
- `hire.raise` gains an optional `proposal` argument (SP5);
- new text in agents' generated prompts (SP7);
- data leaving an organization into a file (SP8);
- a PRD wording change, a retired kind and a changed fence (SP9).
- a PRD wording change, the `system-type` kind and its verbs, routes, CLI
words and Console page removed, and a bus schema step `3v-3w` that drops
`project_system_types` with its rows (SP9).
New refusal codes are added to `packages/bus/src/refusals.mjs` by the slice
that first uses them: `starter-pack-invalid`, `starter-pack-not-found`,
`starter-pack-template-changed` (SP1); `pack-plan-stale` (SP4);
`starter-pack-proposal-stale` (SP5); `template-secret-suspected` (SP8);
`verb-retired` (SP9).
`starter-pack-proposal-stale` (SP5); `template-secret-suspected` (SP8).
SP9 removes `system-type-not-enabled` and
`organization-system-types-human-only`.
**Owner boundary.** "Owns" lists the files a slice may change. Where a
shared file is listed with a scope in brackets, the slice changes only those
@@ -830,7 +924,8 @@ does not widen itself. Tests for a slice live beside the files it owns.
as section 2 states; `installedStarterPacks({dataRoot})`, which lists
shipped and installed packs through `loadInstalled` and sets the origin;
`readSavedTemplate({configDir, account, name})` with every file check in
section 2 (names against `NAME` first, `lstat`, no symlink, owner uid, mode
section 2 (the account, from the caller's `s.human`, against `ID_PATTERN`
and the name against `NAME` first, `lstat`, no symlink, owner uid, mode
0600 or narrower, 256 KiB checked before the read), then
`parseJsonFileText` and `checkPack` with origin `saved`.
- Acceptance: a valid pack loads. Each of these refuses with its code: a
@@ -841,8 +936,10 @@ does not widen itself. Tests for a slice live beside the files it owns.
saved one with either passes. A saved `packs` list with a 33rd entry, an
entry with a key other than `id` and `version`, or a version that is not
semver refuses. `readSavedTemplate` refuses a name of `..`, a name holding
`/`, a symlink, a 0644 file and a 300 KiB file, and a missing file
refuses `starter-pack-not-found`.
`/`, an account holding `/` or starting with `.`, a symlink, a 0644 file
and a 300 KiB file, and a missing file refuses `starter-pack-not-found`;
an account holding a dot, an underscore or a leading digit, which
`ID_PATTERN` allows, reads its own file.
- Red before: `checkPack` refuses a pack whose definition carries
`copyBindings` with `starter-pack-invalid` (today the module does not
exist); `plugins: {"starter-pack": ["coding"]}` refuses `bad-setting`
@@ -878,10 +975,18 @@ does not widen itself. Tests for a slice live beside the files it owns.
`list`;
- `workflows`: `{<name>: {title, description, steps: [{title, state, roles}]}}`,
names matching `NAME`, 1 to 20 steps, titles up to 120 characters,
description up to 1,000, each step's `roles` 1 to 8 names that are an
org definition, a V2 template or `human`;
description up to 1,000, each step's `roles` 1 to 8 names. The reader
checks only shape: each step role matches `NAME` or is `human`.
`applyPack` checks at write time that each name is an org definition, a
V2 template or `human`, and refuses `invalid-request` otherwise. A name
that later stops resolving stays in the file. The prompt renders it as
written, and preview and the workflow editor mark it "names no current
role". So removing a definition or a template never stops an org file
from loading over guidance text;
- `packs`: a list of `{id, version, sha256, origin, scope, project, at, by, added, proposedHires}`,
origin one of `shipped`, `installed`, `saved`.
origin one of `shipped`, `installed`, `saved`. An inline
`proposedHires` entry stores the full role entry beside its
`{inline, sha256}` pin, so the SP5 raise can read it.
- Builds:
- the reader keeps `packs`, `board` and `workflows` on the loaded
organization object, and `board` and `workflows` on each `projects[id]`.
@@ -904,13 +1009,19 @@ does not widen itself. Tests for a slice live beside the files it owns.
writes everything or nothing (a fault injected after the definitions
leaves the file unchanged); an agent actor with a definition in the plan
refuses `human-required`; a plan holding `plugins`, `roles`, `limits` or
`credentials` refuses; a board-only plan changes only `board`.
`credentials` refuses; a board-only plan changes only `board`; a workflow
step naming an unknown role refuses `invalid-request` at `applyPack`,
while an org file whose step names a definition removed since loads, and
removing that definition succeeds.
- Red before: a file with a `board` key refuses today as an unknown top key
(`organization-config.mjs:259`); the test expects it to load.
### SP4: preview, apply, board and workflow writes for people
- Owns: `packages/bus/src/broker.mjs` (the six SP4 verbs only),
- Owns: `packages/bus/src/broker.mjs` (the five SP4 `.console` verbs, and
the unsuffixed `packs`, `pack.preview`, `pack.apply`, `board.set` and
`workflow.set` for the organization's human only, gated as the definition
writes are at `broker.mjs:6305-6308`),
`packages/bus/src/runtime.mjs` (the new `packs`, `pack.preview`,
`pack.apply`, `board.set` and `workflow.set` ops only),
`packages/bus/src/refusals.mjs` (`pack-plan-stale`),
@@ -918,17 +1029,21 @@ does not widen itself. Tests for a slice live beside the files it owns.
(the `pack`, `board` and `workflow` words), `packages/webui/src/packs-http.mjs`
(new: the routes), `packages/webui/src/serve.mjs` (route hookup only),
`packages/webui/src/cli.mjs` (caller wiring only), their tests.
- Schema change: `VERB_RIGHTS` gains `packs: 'board.read'`,
`'packs.console': 'board.read'`, and `null` (owner only) for
- Schema change: `VERB_RIGHTS` gains `'packs.console': 'board.read'`,
and `null` (owner only) for
`pack.preview.console`, `pack.apply.console`, `board.set.console` and
`workflow.set.console`. `CONSOLE_VERBS` gains `packs.console` and those
four. Owner only matches the definition writes today
(`broker.mjs:6221,6303-6308`); when row 193 opens those to `org.hire`,
the four move with them.
four. The unsuffixed verbs are not in `VERB_RIGHTS` (section 3). Owner
only matches the definition writes today (`broker.mjs:6221,6303-6308`);
when row 193 opens those to `org.hire`, the four `.console` writes and
the human unsuffixed verbs move with them.
- Builds: `mosaic org pack list|preview|apply`, `mosaic org board set`,
`mosaic org workflow set|remove`; routes `GET /api/packs`,
`POST /api/orgs/<id>/packs/preview`, `POST /api/orgs/<id>/packs/apply`,
`POST /api/orgs/<id>/board` and `POST /api/orgs/<id>/workflows/<name>`;
`mosaic org workflow set|remove`, calling the unsuffixed verbs; routes
`GET /api/packs`, `POST /api/orgs/<id>/packs/preview`,
`POST /api/orgs/<id>/packs/apply`, `POST /api/orgs/<id>/board`,
`POST /api/orgs/<id>/workflows/<name>` and
`DELETE /api/orgs/<id>/workflows/<name>`, calling the `.console` verbs;
`workflow.set` with `workflow: null` removes the workflow;
the merge table in section 3, including `kept`, `--as` and the per-pack
`removed` tombstone; the held-credential check at preview and at apply;
`saved:<name>` through `readSavedTemplate` with the caller's own account.
@@ -945,7 +1060,11 @@ does not widen itself. Tests for a slice live beside the files it owns.
owner B naming `saved:x` that only owner A saved gets
`starter-pack-not-found`; `packs.console` lists no saved template;
`board.set.console` changes only `board`; a workflow step naming an
unknown role refuses.
unknown role refuses `invalid-request`; the owner's CLI
`mosaic org pack apply` writes the `packs` entry; a member session's
unsuffixed `pack.apply` refuses `human-required`; `mosaic org workflow
remove` and `DELETE /api/orgs/<id>/workflows/<name>` remove only that
workflow.
- Red before: the owner calls `pack.apply.console` for `coding` and the
test expects a `packs` entry in the org file; today the verb is unknown.
@@ -955,20 +1074,28 @@ does not widen itself. Tests for a slice live beside the files it owns.
pin check only), `packages/bus/src/runtime.mjs` (the agent actor on the
SP4 ops only), `packages/bus/src/refusals.mjs`
(`starter-pack-proposal-stale`), their tests.
- Schema change: none to files. Authority: agent verbs `packs`,
- Schema change: none to files. `hire.raise` gains the optional
`proposal` argument, which the broker strips before `prepare`. Authority:
agent verbs `packs`,
`pack.preview`, `pack.apply`, `board.set` and `workflow.set` for a role
whose file lists `organization.change`, the same test `organization.set`
uses (`broker.mjs:6247-6255`). A role without it gets
`decision-required`, as `organization.set` gives today
(`#classification`, `broker.mjs:4743`).
- Builds: the agent apply in section 3: no definitions (`human-required` if
the plan has one), origin `saved` refused with `human-required`, workflow
steps naming an unwritten definition left out and marked in preview; the
pin check when the chief raises a proposal (`decision-stale`,
`definition-rev-conflict`, `starter-pack-proposal-stale` when an inline
entry's canonical hash differs, and `definition-not-hireable` when an
inline entry's `from` is in `launch.noHire`; the broker checks the hash,
then passes `{inline}` on).
- Builds: SP5 extends the same unsuffixed verbs SP4 adds for the human to
a role whose file lists `organization.change`. The `packs` read is
allowed to an `organization.change` holder without spending the grant,
as `organization.revision` is (`broker.mjs:6253`); any other agent gets
`human-required`. The agent apply in section 3: no definitions
(`human-required` if the plan has one), origin `saved` refused with
`human-required`, workflow steps naming an unwritten definition left out
and marked in preview. The `proposal` argument on `hire.raise` and its
pin check, exactly as section 3 states: the broker fills `op`, `name`,
`level` and `role` from the recorded pin, refuses a caller-sent `role`,
`name` or `level`, checks the pin (`decision-stale`,
`definition-rev-conflict`, `starter-pack-proposal-stale`,
`definition-not-hireable`), passes `{inline: compositionOf(entry)}` for an
inline pin, and strips `proposal` before `prepare`.
- Acceptance: a coder without `organization.change` gets
`decision-required` for `pack.apply`, `board.set` and `workflow.set`, the
same as `packages/bus/tests/chief-of-staff.test.mjs:255-261` asserts for
@@ -980,7 +1107,10 @@ does not widen itself. Tests for a slice live beside the files it owns.
refuses `decision-stale`, one whose definition `rev` moved refuses
`definition-rev-conflict`, an inline one whose hash differs refuses
`starter-pack-proposal-stale`, and an inline one whose `from` is in
`launch.noHire` refuses `definition-not-hireable`.
`launch.noHire` refuses `definition-not-hireable`; a raise with
`proposal` that also sends `role` refuses; a raise without `proposal`
behaves as today; a coder's `packs` refuses `human-required` and the
chief's `packs` lists shipped and installed packs.
- Red before: the chief calls `pack.apply` for `coding` and the test
expects the org file to gain its `board`; today the verb is unknown.
@@ -997,7 +1127,8 @@ does not widen itself. Tests for a slice live beside the files it owns.
(`member.doc`, `workspace-read.mjs:140`), so the per-org head gains
`board` and each project row its project `board`, and a new
`GET /api/orgs/<id>/workflows` returns the org and project workflows from
the same document. Then the pages: pack picker, preview and apply through
the same document. It is owner only until row 193, as `packs.console`
is, and then `board.read`. Then the pages: pack picker, preview and apply through
SP4; labels and order from `board` when the view is filtered to one org or
project; `layout` as a default under the viewer's saved `localStorage`
choice; a workflow list and an editor that saves through
@@ -1005,7 +1136,8 @@ does not widen itself. Tests for a slice live beside the files it owns.
- Acceptance: a view filtered to an org with a `board` shows its labels; the
all-orgs view keeps today's labels; a saved viewer layout wins over the
pack default; a label or workflow title holding `<img src=x onerror=...>`
renders as text (`esc()`, `tasks-board.js:108-113`).
renders as text (`esc()`, `tasks-board.js:17`); a step naming a role that
no longer resolves shows "names no current role".
- Red before: a view filtered to an org with a `board` shows the fixed labels
(`tasks-board.js:21`).
@@ -1037,7 +1169,9 @@ does not widen itself. Tests for a slice live beside the files it owns.
### SP8: saving a template
- Owns: `packages/organization/src/template-save.mjs` (new),
`packages/bus/src/broker.mjs` (the two template verbs only),
`packages/bus/src/broker.mjs` (the two template `.console` verbs, and
the unsuffixed `template.save` and `template.list` for the
organization's human only),
`packages/bus/src/runtime.mjs` (the save and list ops only),
`packages/bus/src/refusals.mjs` (`template-secret-suspected`),
`packages/organization/src/org-rights.mjs`, `packages/cli/src/org.mjs`
@@ -1050,8 +1184,9 @@ does not widen itself. Tests for a slice live beside the files it owns.
new schema.
- Builds: capture, strip, placeholder and preview rules in section 5; the
file at `<configDir>/templates/<account>/<name>.json`, directory 0700,
file 0600, written with `wx`, name and account checked against `NAME`
first; `packs` written as at most 32 `{id, version}` entries; save
file 0600, written with `wx`, the account (the caller's `s.human`)
checked against `ID_PATTERN` and the name against `NAME` first; the CLI
words call the unsuffixed verbs, the routes the `.console` ones; `packs` written as at most 32 `{id, version}` entries; save
refuses more than 8 hires and any role with a `from` that
`narrowsTemplate` refuses; `list` shows only the caller's own.
- Acceptance: the file has none of the stripped keys and keeps role
@@ -1061,95 +1196,88 @@ does not widen itself. Tests for a slice live beside the files it owns.
`template-secret-suspected` naming the field path; a second save to the
same name refuses; a name of `..` refuses; a ninth hire refuses; each
saved `packs` entry holds only `id` and `version`; a saved template
applies through SP4 for its saver.
applies through SP4 for its saver; the owner's CLI
`mosaic org template save` writes the file, and a member session's
unsuffixed `template.save` refuses `human-required`.
- Red before: save an org whose instance has `credentials` and
`copyBindings` and assert neither appears in the output; the verb does not
exist today.
### SP9: retire system types, step 1
### SP9: remove system types
- Owns, each scoped to its system-type lines:
- `packages/plugins/src/kinds.mjs`; `packages/plugins/src/system-types.mjs`
and `packages/plugins/system-type/` (deleted);
- `packages/organization/src/organization-config.mjs` and
`packages/organization/src/organization.mjs` (the retired key and
`systemTypesFence`);
- `packages/organization/src/organization-config.mjs` (lines 360-365) and
`packages/organization/src/organization.mjs` (`systemTypesFence` and
its calls);
- `packages/organization/src/org-rights.mjs` (`system-types.console`,
line 90);
- `packages/bus/src/broker.mjs`, `packages/bus/src/runtime.mjs` (the
import at line 30 and the reads at 903-904) and
`packages/bus/src/refusals.mjs` (`system-type-not-enabled` out,
`verb-retired` in);
`packages/bus/src/refusals.mjs` (`system-type-not-enabled` and
`organization-system-types-human-only` out);
- the bus schema step: `packages/bus/src/migrations/3v-3w.sql` (new),
`packages/bus/src/store.mjs` (`SCHEMA_VERSION`, `SCHEMA_SHA256`, the
step entry and `timedTables`), `packages/bus/schema.sql`, and
`scripts/stack-release.sh` (`COUNTED`, line 443);
- `packages/cli/src/org.mjs`, `packages/cli/src/project.mjs` and
`packages/cli/src/cli.mjs` (the dispatch list at line 781);
- `packages/plugins/channel/cli/index.mjs`;
- the Console files in section 1's system-type rows;
- `docs/prd/mosaic-stack.md` (REQ-SYS-1/2 only);
- their tests.
- Schema change: `system-type` stays in `KINDS` with `retired: true`.
`checkPlugins` accepts any list for it without loading modules, the reader
ignores it, and `systemTypesFence` becomes a fence that lets any writer
drop the key and refuses a write that adds or changes a name
(`verb-retired`). The next writer commit drops the key and its trail event
keeps the old list. `project_system_types` is unchanged (no DDL). The PRD
text is section 6's.
- Builds: the removals in section 6; `project.system-types.set`,
`system-types.installed` and `system-types.current` refuse
`verb-retired`; the `project.system-types` read stays.
- Acceptance: an org file with `plugins['system-type']` loads with the key
ignored; the next commit by an agent or a person drops it and its trail
event holds the old list; `project_system_types` rows are unchanged; no
system-type route, CLI word or channel verb is left, and
`grep -rln "installedSystemTypes\|system-type-not-enabled" packages`
finds nothing outside tests.
- Red before: an org file with `plugins['system-type']: ["no-such-type"]`
loads with the key ignored (today it refuses "names unknown module",
`organization-config.mjs:365`); a human write that adds `coding` refuses
`verb-retired` (today it is accepted).
- the Console files section 6 lists;
- `docs/prd/mosaic-stack.md` (REQ-SYS-1/2 only),
`packages/tasks/requirements.json` (lines 73-74), and the three
package READMEs section 6 lists;
- their tests, and every test file section 6 names for the schema pins.
- Schema change: `system-type` leaves `KINDS`. Bus schema step `3v-3w`
drops `project_system_types`, its index and its four triggers, and
`SCHEMA_VERSION` becomes `'3w'`. The pins it moves are section 6's:
`schema.test.mjs:533-534` (tables 33 to 32, triggers 130 to 126),
`COUNTED` and `LISTED` (`stack-release.sh:443`,
`stack-release.test.mjs:888`, with the samples at `:906,928`), the 20
test files that pin `'3v'`, and the migration-chain cuts and table lists.
The PRD text is section 6's.
- Builds: the removals in section 6, and the step.
- Acceptance: an org file with `plugins: {"system-type": []}` refuses
`unknown-kind`; a `3v` store holding `project_system_types` rows opens,
migrates to `3w` and matches the fresh `schema.sql` digest, with no table,
index, trigger or `sqlite_sequence` row named `project_system_types`
left; the schema counts are 32 tables and 126 triggers; T45 and T46
pass; no system-type verb, route, CLI word, channel verb or Console page
is left, and
`grep -rlnE "system-type|systemType|system_type|system-types" packages scripts`
finds only `packages/bus/src/migrations/3t-3u.sql`, `3v-3w.sql`, and the
tests that replay or check them.
- Red before: an org file with `plugins: {"system-type": ["coding"]}`
refuses `unknown-kind` (today it loads, since `coding` is a shipped
module); a store opened at `SCHEMA_VERSION` `'3w'` has no
`project_system_types` table (today the version is `'3v'` and the table
exists).
### SP10: the print-only conversion
## 9. Answered
- Owns: `packages/organization/src/migrate-system-type.mjs` (new, beside
`migrate-launch.mjs`), `packages/cli/src/org.mjs` (the
`migrate-system-type --file ORGANIZATION` word, which prints and writes
nothing, as `mosaic limits migrate` does at `packages/cli/src/limits.mjs:62-72`),
their tests.
- Schema change: none. `system-type` stays in `KINDS` as retired.
- Builds: a conversion that returns the org file without
`plugins['system-type']` and names the old list on stderr.
- Acceptance: the conversion prints the SP9 fixture without the key and
leaves the input file byte-identical; a file without the key prints
unchanged.
- Red before: the conversion does not exist.
Jason answered on 2026-11-12, about 21:05Z, with "full send". The slices
above follow his answers.
Follow-up row, not in this go: remove `system-type` from `KINDS` after one
release, with its own go, once each instance's owner has run the conversion
or every org file has been written since SP9.
1. **Retire system types as a separate kind?** Remove it outright:
## 9. Open questions for Jason
> The only deployed system is the dev environment on dragon-lin. It can
> be wiped and reconfigured. No reason to hold on to legacy for this
> change.
Each answer changes what gets built. The slices above follow the
recommendation; a different answer changes the slice named.
1. **Retire system types as a separate kind?** This design replaces the
`system-type` kind with `starter-pack`. Org files that name the old kind
keep loading for one release and lose the key on their next write;
`project_system_types` stays as history. Recommendation: yes. The modules carry
nothing today, and two surfaces for one idea would confuse users. A no
drops SP9 and SP10 and keeps the system-type label beside packs.
Section 6 and SP9 remove the kind, its modules, verbs, routes, refusals,
reads, CLI words, channel verbs and Console surfaces, and drop
`project_system_types` with bus schema step `3v-3w`. An org file naming
the kind refuses as an unknown kind.
2. **May the chief change board styles and workflows directly, as it changes
limits today?** This design puts `board.set`, `workflow.set` and the
non-definition half of `pack.apply` under the chief's existing
`organization.change` grant, with no decision. Recommendation: yes. They
carry no capability, the trail records each change, and a decision here
would be a new gate. Any role listing `organization.change` gets the same
writes, so the rule is the grant, not the chief's name. A role without it
gets `decision-required`, as `organization.set` gives today. Role and hire
changes stay decisions, and only the chief raises hires. A no drops the
agent writes from SP5 and leaves agents to propose board and workflow
changes in a message to the owner.
limits today?** Yes, as written. `board.set`, `workflow.set` and the
non-definition half of `pack.apply` sit under the existing
`organization.change` grant, with no decision. Any role listing
`organization.change` gets the same writes; a role without it gets
`decision-required`, as `organization.set` gives today. Role and hire
changes stay decisions, and only the chief raises hires.
3. **Board styles as labels and order over the five fixed states, or custom
states?** Custom states would change the task tracker contract, every
tracker module and the Vikunja bucket setup. Recommendation: labels, order
and layout only for now; revisit custom states when a pack needs one. A
no adds a tracker contract change to SP3 and every tracker module.
states?** Yes, as written: labels, order and layout over the five fixed
states. Custom states would change the task tracker contract, every
tracker module and the Vikunja bucket setup, and wait until a pack needs
one.