From c0a60ed3ae936a7b6c1acb265bda478161f7da28 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Thu, 12 Nov 2026 15:18:16 -0600 Subject: [PATCH] docs(plans): starter packs fix round 3, system types removed outright --- docs/plans/2026-11-12_starter-packs-design.md | 456 +++++++++++------- 1 file changed, 292 insertions(+), 164 deletions(-) diff --git a/docs/plans/2026-11-12_starter-packs-design.md b/docs/plans/2026-11-12_starter-packs-design.md index a197d479f..f174eb173 100644 --- a/docs/plans/2026-11-12_starter-packs-design.md +++ b/docs/plans/2026-11-12_starter-packs-design.md @@ -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 `/templates//`; `saved:` 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//packs/preview`, `POST /api/orgs//packs/apply`, `POST /api/orgs//board`, `POST /api/orgs//workflows/` | -| 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//packs/preview`, `POST /api/orgs//packs/apply`, `POST /api/orgs//board`, `POST /api/orgs//workflows/`, `DELETE /api/orgs//workflows/` | +| 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-` 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`: `{: {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//packs/preview`, `POST /api/orgs//packs/apply`, - `POST /api/orgs//board` and `POST /api/orgs//workflows/`; + `mosaic org workflow set|remove`, calling the unsuffixed verbs; routes + `GET /api/packs`, `POST /api/orgs//packs/preview`, + `POST /api/orgs//packs/apply`, `POST /api/orgs//board`, + `POST /api/orgs//workflows/` and + `DELETE /api/orgs//workflows/`, 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:` 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//workflows/` 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//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 `` - 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 `/templates//.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.