From bf65ea9802bc8875e2a9143c798b6c0c963e7661 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Thu, 12 Nov 2026 14:25:04 -0600 Subject: [PATCH 1/5] docs(plans): starter packs design; system-type toolsets design superseded Design only, per Jason's 2026-11-12 starter-packs ruling. Inventory of existing building blocks, pack format, apply and re-apply, customizing from inside, saved templates, system-type retirement, five shipped packs, build slices and three open questions. --- .../2026-11-10_system-type-toolsets-design.md | 2 + docs/plans/2026-11-12_starter-packs-design.md | 571 ++++++++++++++++++ 2 files changed, 573 insertions(+) create mode 100644 docs/plans/2026-11-12_starter-packs-design.md diff --git a/docs/plans/2026-11-10_system-type-toolsets-design.md b/docs/plans/2026-11-10_system-type-toolsets-design.md index bdcbab5ea..67b1fcd1d 100644 --- a/docs/plans/2026-11-10_system-type-toolsets-design.md +++ b/docs/plans/2026-11-10_system-type-toolsets-design.md @@ -1,5 +1,7 @@ # System-type toolsets at launch +Superseded 2026-11-12 by `docs/plans/2026-11-12_starter-packs-design.md` under Jason's starter-packs ruling; do not build this design. + Status: design only. Nothing here is built and no `roles/` or contract file is changed. Prepared 2026-11-10 against `refactor` `8630b8dfc` for phase P2 of #1873 (unified workspace). PRD requirement: REQ-SYS-2, "An agent working in a diff --git a/docs/plans/2026-11-12_starter-packs-design.md b/docs/plans/2026-11-12_starter-packs-design.md new file mode 100644 index 000000000..5bdc06bb0 --- /dev/null +++ b/docs/plans/2026-11-12_starter-packs-design.md @@ -0,0 +1,571 @@ +# Starter packs + +Status: design only. Nothing here is built. No `roles/`, `contracts/` or +schema file is changed. Prepared 2026-11-12 against `refactor` `2ad4b30eb`. +It replaces `docs/plans/2026-11-10_system-type-toolsets-design.md`, which is +superseded and must not be built. + +## The ruling + +Jason, 2026-11-12, about 20:05Z: + +> I agree. Starter packs instead of over-restrictive configurations. We +> provide a handful of pre-configured options for system types, then the user +> can customize to their needs. Agent roles will be the main limiter or +> permitter of actual capabilities in the system. The default starter packs +> can affect the various Kanban styles, workflow templates, agent roles, and +> other primitives in the system. The user and their agents will be able to +> customize the system operations by allowing the import and implementation of +> the various roles, workflows, and other available items as needed. The user +> will also have the option to create new workflows within their system. The +> agent roles will be customizable within the absolute constraints that are +> offered. We have discussed that no agent can grant permissions beyond those +> they already possess. The org or project configurations will establish the +> "sandbox" for agent operations. The user and chief/assistant can work +> together to customize the org/project as needed to accomplish a goal. The +> user can also save a custom org/project template for use repeatedly to meet +> their needs. + +Earlier he said a system type is there "to provide a basic configuration using +templates for agent setup and system function", and that the user customizes +the system while working inside it. He judged the toolset gating design +counterproductive. + +What this design takes from that: + +- A pack is a starting point that is copied in once. It is not a switch that + stays on and gates what agents can do. +- Roles are the limit. A pack grants nothing; every agent it proposes is hired + through the existing hire decision, bounded by the chief's own role. +- The organization or project is the sandbox. Nothing a pack, a saved + template or an agent does reaches outside it. +- Customizing from inside uses the paths that already exist: the T1 definition + verbs for people, hire and change decisions for the chief, and the chief's + `organization.change` grant for settings that carry no capability. No new + human gate is added. + +## 1. Inventory + +Status means: **exists** (built and usable by a pack as is), **partial** +(built, but a pack cannot set it or it lacks a piece a pack needs), **missing** +(not found in the code). + +| Item | Where | Status | Notes | +|---|---|---|---| +| System-type plug-in kind | `packages/plugins/src/kinds.mjs:84-86` (`switch: "list"`, export `systemType`) | exists | A pure label today. | +| The five system-type modules | `packages/plugins/system-type/{coding,creative-writing,marketing,montage,business-management}/index.mjs`, `systemType()` | partial | Each returns `{id, label, description, toolsets: []}`. Nothing else. | +| Loading system types | `packages/plugins/src/system-types.mjs:5` `installedSystemTypes` | exists | Any refused module, or keys other than `description,id,label,toolsets`, throws `system-type-invalid`. | +| Org enable | `plugins['system-type']` in the org file; checked at `packages/organization/src/organization-config.mjs:360-365`; fenced by `systemTypesFence`, `packages/organization/src/organization.mjs:126-131` (`organization-system-types-human-only`) | exists | Read back by `system-types.current`, `packages/bus/src/runtime.mjs:904`. Console `GET/POST /api/system-types` (`packages/webui/src/system-types-http.mjs:4-9`), right `org.setup` (`packages/organization/src/org-rights.mjs:90`). | +| Project selection | `project.system-types[.set]`, `packages/bus/src/broker.mjs:8476-8495`; table `project_system_types` in `packages/bus/src/migrations/3t-3u.sql` (append-only) | exists | Human only. CLI `mosaic project system-types` and `system-types-set`, `packages/cli/src/project.mjs:17-18`. | +| System type effect on agents | launcher, harness, `packages/organization/src/resolve.mjs` | missing | No launch path reads system types (no hits in those files). Every module's `toolsets` is empty. | +| Shipped role templates | `roles/*.json` + `.md` | exists | V2 roles: assistant, chief-executive-officer, chief-financial-officer, chief-information-officer, chief-information-security-officer, chief-marketing-officer, chief-of-staff, coder, companion, continuity, cto, fact-checker, ideas, merge-gate, pm, proofreader, research, reviewer, security-reviewer. `researcher.json` is V1 and refused by `readTemplate`. | +| Template identity | `packages/organization/src/definitions.mjs:174,180` `templateHash`, `templateHashes` | exists | Name plus sha256. | +| Org definitions: import, invent, edit, remove | `packages/organization/src/definition-verbs.mjs:61` `definitionVerbs`; CLI `mosaic org definitions ...` (`packages/cli/src/org-definitions.mjs:15`) | exists | `authorize` (line 38) refuses any agent (`human-required`) and needs `org.hire`. Writes go through `writer.setDefinition`; a definition in use refuses `definition-in-use`. | +| Template diff | `packages/organization/src/definitions.mjs:252` `diffDefinition` | exists | Compares a copy with its template's current text. | +| Composition check | `packages/organization/src/composition.mjs:443` `checkComposition` | exists | Refuses hard waits (R4). Flags every line above the chief's own role (tools, network, actions, credentials, messaging) and every resolve right. | +| Plug-in role templates | `roleTemplates` in `packages/plugins/src/manifest.mjs:48` `checkManifest`; read by `packages/organization/src/composition.mjs` | partial | A PL1 manifest may declare templates with plugin-prefixed names. I did not find the hire path reading them. | +| Hire as a decision | `packages/organization/src/hire-decisions.mjs:65` `hireDecisions` (`prepare`); `authorizeHire` line 32; broker `#raiseHire`, `packages/bus/src/broker.mjs:6578` | exists | `instance.hire` fails `decision-required` (6236). Only the chief raises (`role-not-chief`, 6583). A human with `org.hire` starts and approves in one step through `hire.start.console`. Flagged and credential lines need the owner; root needs the instance owner. An approved hire writes definition `hire-` and the instance (hire-decisions.mjs lines 307-309). | +| Autonomy setting | `packages/organization/src/autonomy.mjs:2-4` | exists | Classes cross-role, merge, policy, authority. Authority defaults to in the loop. Owner-only (`autonomy.*` null in `VERB_RIGHTS`). | +| The chief's own settings grant | `organization.change` in `roles/chief-of-staff.json`; broker `organization.set`, `packages/bus/src/broker.mjs:6247-6340` | exists | The chief changes limits directly under its within-role grant. The trail records each change. | +| Task board columns | `STORE_STATES`, `packages/bus/src/broker.mjs:334`; `packages/tasks/src/startup.mjs:6` `BUCKETS`; `packages/webui/src/public/tasks-board.js:21` `STATES` | partial | Five fixed states (todo, in-progress, in-review, blocked, done) with fixed labels. Nothing configures them. | +| Kanban styles | `packages/webui/src/public/tasks-board.js:146` `kanban`, `:156` `list` | missing | Two layouts (REQ-WS-8). No per-org or per-project style, label or order. | +| Workflow templates | grep for "workflow" in `packages`, `scripts`, `roles`, `contracts`, `missions` | missing | Only refusal text in `packages/bus/src/refusals.mjs`. No workflow document, store or engine. | +| Mission and task templates | `missions/hello.json`; `tasks/*.json`; schemas in `scripts/mosaic-task.mjs:94,139` | partial | These drive headless worker runs (`run-task.sh`), not org operations. `missions/` holds one file. A pack does not carry them. | +| Skills | repo `skills/` (16 skills); `scripts/skill.sh` install/activate into `/skills-enabled`; role `skills` key resolved in `packages/harness/src/bundle.mjs:126-192` | exists | Machine-scoped. A role names skills; launch refuses a missing one. A pack can name skills but cannot install them. | +| Plug-ins (code) | `packages/plugins/src/loader.mjs:123` `loadInstalled`; `packages/plugins/src/install.mjs:71` `installPlugin` (hash pin, read-only files) | exists | A module runs with the host's authority (loader header). Install is an instance-owner act (R10). | +| Workspace and dashboard sections | `packages/webui/src/workspace-read.mjs:13` `WORKSPACE_SECTIONS` | partial | Fixed: projects, sessions, inbox, tasks, metrics. No per-org configuration found. | +| `mosaic org template render` | `packages/organization/src/org-service.mjs:92` `templateRender`; `packages/organization/src/org-template.json` | exists | Renders human, chief, assistant and one `work` project. Presets from harness and model-provider modules (`discoverPresets`, line 61) overlay `plugins`, `launch` and assistant vars. Console `GET /api/org-template`, `/api/org-presets` (`packages/webui/src/org-admin.mjs:413-419`). | +| Project settings | org file `projects.` keys root, repo, remote, git, agents (`packages/organization/src/organization-config.mjs:306`); repo file `.mosaic/project.json` (`packages/organization/src/project.mjs`, read only) | partial | No board or workflow setting per project. | +| Agent levels | `packages/organization/src/agent-levels.mjs` (`AGENT_LEVELS` line 15, `checkProjectAgents` line 111, `resolveAgentLevels` line 207) | exists | Root and project definitions refuse bound keys: credentials, holder, tracker, identity, publish, hosts (line 21). | +| Copy credential bindings | `copyBindings` fence, `packages/organization/src/organization.mjs:32-36`; writer at line 550 | exists | Owner-approved binding decision only. | +| Pack format, apply, saved templates | none | missing | This design. | + +Missing building blocks, plainly: workflow templates, Kanban styles (labels, +order, hidden columns), any per-org or per-project board setting, a pack +format, a pack apply path, and saved org or project templates. Montage has no +video roles or tools in the repository; the OpenMontage integration was not +found. + +## 2. What a pack is + +A pack is a declarative JSON document. It lists items to copy into an +organization or a project. It never runs, never stays switched on, and grants +nothing. + +### Where packs live + +| Source | Location | Who adds one | +|---|---|---| +| Shipped | Core plug-in modules `packages/plugins/starter-pack//index.mjs`, exporting `interfaceVersion = 1` and `starterPack()`, which returns the document | Reviewed commit | +| Installed | PL1 hash-pinned plug-ins under `/plugins`, kind `starter-pack` | Instance owner, at the host (R10) | +| Saved by a user | `/templates//.json`, mode 0600, written with `wx` (never overwritten) | The organization's owner, through `template.save` (section 5) | + +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. A +saved template is data only. It is read with `parseJsonFileText`, never +imported or run, capped at 256 KiB, and passes the same `checkPack` as a +shipped pack. + +### The document (packVersion 1) + +```json +{ + "packVersion": 1, + "id": "coding", + "version": "1.0.0", + "label": "Coding", + "description": "A small software team with review before merge.", + "scope": "org", + "definitions": { + "coder": { "template": "coder", "sha256": "<64 hex>" }, + "release-writer": { + "from": { "template": "coder", "sha256": "<64 hex>" }, + "role": { "tools": ["read", "edit"], "authority": { "withinRole": ["record.write"] } }, + "instructions": "...", + "personality": { "summary": "..." } + } + }, + "hires": [ + { "name": "coder-1", "definition": "coder", "level": "org", "reason": "writes the code" } + ], + "board": { + "columns": [ + { "state": "todo", "label": "Backlog" }, + { "state": "in-progress", "label": "Building" }, + { "state": "in-review", "label": "Review" }, + { "state": "blocked", "label": "Blocked" }, + { "state": "done", "label": "Merged" } + ], + "layout": "kanban" + }, + "workflows": { + "change": { + "description": "One change from task to merge.", + "steps": [ + { "id": "plan", "title": "Write the task", "role": "pm", "state": "todo" }, + { "id": "build", "title": "Implement", "role": "coder", "state": "in-progress" }, + { "id": "review", "title": "Review", "role": "reviewer", "state": "in-review" }, + { "id": "merge", "title": "Merge", "role": "merge-gate", "state": "done" } + ] + } + }, + "skills": ["ms-sdlc", "ms-unslop"] +} +``` + +`checkPack` (new, in `packages/plugins/src/starter-pack.mjs`) refuses with +`starter-pack-invalid` unless all of these hold: + +- Exact top-level keys as above. `scope` is `org` or `project`. +- `id` matches `NAME` (`packages/plugins/src/kinds.mjs:20`). `version` is a + semver string. +- Each `definitions` entry is one of two shapes: + - A template reference `{template, sha256}`. The name must be a V2 role in + `roles/` and the hash must equal `templateHash` now. A stale pin refuses + `starter-pack-template-changed`, so a pack never imports text nobody + reviewed against it. + - A full entry in the T1 shape (`ENTRY_KEYS`, `definitions.mjs:30`). It + must name `from` and pass `checkDefinition`. Its role must narrow the + template named in `from`, line by line, under the new + `narrowsTemplate(role, template)`: every tool, action, skill, messaging + target and credential line is in the template, and the network is no + wider. This is "no grant above the templates". A shipped or installed pack + may not carry a role without `from`. +- `hires`: at most 8. Each `name` passes `requireId`. Each `definition` names + an entry in this pack's `definitions` or an existing V2 template. `level` + is `org` or `project`, never `root`. +- `board.columns` holds the five `STORE_STATES`, each exactly once, in any + order, each with a label of 1 to 32 characters. `layout` is `kanban` or + `list`. The tracker contract does not change (REQ-TASK-1, REQ-WS-8). +- Each `workflows` entry has 1 to 20 steps. Each step's `state` is in + `STORE_STATES` and its `role` names a definition in the pack or a template. +- `skills` entries match `NAME`. They are a list of what the roles need, not + an install. +- Every string passes the credential store's `assertClean`, as definitions do + today (`checkDefinition`, `definitions.mjs:81`). + +### What a pack never contains + +The schema has no key for any of these, so a pack carrying one refuses: + +- credentials, credential bindings, tokens, tracker bots, identity, publish, + holder or hosts; +- members, rights or the owner; +- the autonomy setting or arbiters; +- plug-in selections (`plugins`), including the task tracker and theme; +- `launch` (models, limits, keep, noHire, account); +- a root-level hire; +- any role line outside the `roles/` rails. `checkComposition` still runs on + every definition at apply time and refuses a hard wait (R4) and `role.hire` + or `role.fire` in a hired composition (R9). + +### Versioning + +`packVersion` is the schema version; an unknown one refuses. `version` is the +pack's own release. An apply records `{id, version, sha256}` where `sha256` +is the hash of the canonical JSON that was applied. A new pack version is a +new document; it never edits an organization that applied an older one. + +## 3. Applying a pack + +### What apply does + +Apply copies items in once. After that the organization or project owns them, +exactly as if a person had made them by hand. The steps: + +1. **Preview.** `pack.preview` reads the pack and the organization and returns + a plan: each item marked `add`, `same` or `kept` (section 3, re-apply), the + flagged lines each proposed hire would carry against the current chief, the + skills not enabled on this machine, and a digest of the plan plus the + organization file revision. Nothing is written. +2. **Apply.** `pack.apply` takes the digest. It refuses `pack-plan-stale` if + the revision or plan changed. In one organization-file write it: + - writes each `add` definition through `writer.setDefinition`, with `from` + kept, so `checkComposition` runs and `rev` starts at 1; + - writes the `board` and `workflows` items marked `add`; + - appends `{id, version, sha256, scope, project, at, by, proposedHires}` to + a new `packs` key. +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}`; 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 + shows the same lines and needs the owner for any flagged one. + + A proposal is filled when an instance by that name exists. A pack applied + to a project proposes project-level hires (`level: "project"`), which land + in `projects.

.agents` and refuse bound keys + (`agent-levels.mjs:21`). + +### The surface + +| 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]` | +| Console route | `GET /api/packs`, `POST /api/orgs//packs/preview`, `POST /api/orgs//packs/apply` | +| Bus ops | `packs` (read), `pack.preview.console`, `pack.apply.console`; and for the chief, `pack.preview` and `pack.apply` under `organization.change` | +| `VERB_RIGHTS` | `packs` to `board.read`; `pack.preview.console` and `pack.apply.console` to `org.hire` | + +`org.hire` fits because the only parts of a pack that could ever matter for +capability are role definitions, and T1 already gives definition writes to +`org.hire`. Board and workflows carry no capability. + +### The chief applying a pack + +The roles-and-rails design says the chief proposes a definition only inside a +decision (section 5). The chief's `pack.apply` respects that: + +- it writes `board` and `workflows` directly under the chief's + `organization.change` grant, the same way `organization.set` writes limits + today (`broker.mjs:6247-6340`); +- it writes no definitions. Each proposed hire the chief raises carries the + pack's composition as role `{inline}` or `{template, sha256}`, so the broker + computes the lines against the chief and the approved hire writes + `hire-` as it does now. + +### A second pack, and re-applying + +Items merge by name and nothing is overwritten: + +| Item in the pack | Organization already has | Result | +|---|---|---| +| Definition | nothing by that name | `add` | +| Definition | same composition, instructions and personality | `same`, no write | +| Definition | different (edited, or a newer pack version) | `kept`. The preview shows `diffDefinition`. The person may import the pack's version under another name with `--as`. | +| Board | no `board` key | `add` | +| Board | a different board | `kept` | +| Workflow | nothing by that name | `add` | +| Workflow | a different workflow by that name | `kept`, with `--as` | +| Proposed hire | an instance or open hire decision by that name | dropped from the new proposals | + +There is no forced overwrite flag. To take a newer pack version of a definition +a hired agent uses, the person or chief raises a change decision, which is the +existing path. + +### Removing a pack + +Removing a pack takes nothing away. Items were copied, so the organization +keeps them. The `packs` entry stays as history. Uninstalling an installed pack +plug-in uses `removePlugin` (`install.mjs:99`); a pack is never "enabled", so +`plugin-enabled` never refuses it, and no organization changes. + +## 4. Customizing from inside + +The ruling asks that the user and their agents can import roles, workflows and +other items, create new workflows, and customize roles inside fixed limits. +The table shows who does what. Every row reuses a path that exists or adds a +verb at the same level as its neighbours. None adds a person to a path that +had none. + +| Change | A person does it directly | The chief does it | The limit at that step | +|---|---|---|---| +| Import a role template | `definition.import` (T1, `org.hire`) | Inside a hire or change decision, role `{template, sha256}` | `checkComposition`. Nothing runs until a hire. | +| Customize or invent a role | `definition.edit` or `invent` (T1) | Inside a hire or change decision, role `{inline}` | `checkComposition`; lines above the chief are flagged and only the owner approves them (R8). Editing a definition in use refuses, so a running agent never widens without a change decision. | +| Hire, fire, change an agent | `hire.start.console` (`org.hire`) | `hire.raise` (only the chief, R9) | Broker-computed flagged and credential lines; authority autonomy level; root needs the instance owner. | +| Apply a pack | `pack.apply.console` (`org.hire`) | `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` (`org.hire`) | `workflow.set` under `organization.change` | Steps name existing definitions or templates and the five states. A workflow grants nothing. | +| Change board labels or order | `board.set.console` (`org.hire`) | `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 agents refuse bound keys (`agent-levels.mjs:21`). | +| Enable a skill on the machine | `scripts/skill.sh activate` (instance owner) | Not available | Machine-scoped. | +| Install a pack plug-in | `installPlugin` (instance owner, R10) | Not available | Host authority. | +| Autonomy level | Owner only | Never (R12) | Unchanged. | + +**"No agent grants beyond its own permissions", step by step.** + +1. A pack: its definitions narrow the shipped templates (`narrowsTemplate`). +2. Apply: definitions pass `checkComposition`; apply writes no instance. +3. The chief's proposal: the broker, not the chief, computes the flagged lines + against the chief's own role (`checkComposition` step 7, + `composition.mjs:491`). The chief cannot omit them. +4. Resolve: an agent resolver can resolve only a plain hire with no flagged + or credential lines, not at root, not about itself (`authorizeHire`, + `hire-decisions.mjs:39-42`). Anything flagged goes to the owner. +5. A hired agent: `role.hire` and `role.fire` refuse in its composition (R9), + so it cannot hire in turn. +6. Saved templates go through steps 2 to 5 again in the new organization. + +**The sandbox.** Every agent a pack proposes is an org or project agent. It +runs in one organization, sandboxed as the instance uid with only its role's +tools (R6, R7). A project agent sees only that project's workspace. A pack +cannot name another organization, a host, a credential or a root-level hire. + +**The assistant.** The assistant does not hire, direct agents or assign tasks +(REQ-ASSIST-1). It helps the user pick and preview a pack and passes the +request to the chief. It gets no new verb. + +## 5. Saving a template + +### The verb + +| Layer | Name | +|---|---| +| CLI (owner verb) | `mosaic org template save ID NAME [--project P] --digest D`, with a preview `mosaic org template save ID NAME --preview` | +| Console route | `POST /api/orgs//templates/preview`, `POST /api/orgs//templates` | +| Bus op | `template.save.console`, `VERB_RIGHTS` null (owner only) | +| Lists | `mosaic org template list`, `GET /api/templates`: only the caller's own | + +Owner only, because a saved template copies the organization's role text, +which is the owner's work product, out of the organization. The preview shows +the exact JSON that will be written; save takes its digest. + +### What is captured + +- `definitions`: role, instructions, personality and `from`. Definitions named + `hire-` are renamed after the instance that uses them, so no + decision id leaves the organization. +- `hires`: one proposal per org instance (or project agent, with `--project`): + instance name, definition and level. Never the chief or the assistant, + which every organization already has. +- `board` and `workflows`. +- `skills`: the union of the captured roles' `skills`. +- `packs`: the ids and versions applied before, as provenance text. + +The result is a pack document with `source: "saved"`. A role in a saved +template may lack `from` (an invented role); that is the one difference from a +shipped pack, and it still passes `checkComposition` on apply. + +### What is stripped + +Everything not listed above. In particular: + +- the owner, members, accounts and rights; +- identities: `human`, instance `identity`, `publish`, `holder`, `hosts`, + tracker bot names and ids; +- credentials: role `credentials` lines, instance credentials, `copyBindings` + and their `copyId`, `orgIncarnation` and `generation`; +- `launch` (models, limits, keep, noHire, account, grant scope), `autonomy`, + `arbiters`, `plugins`, `appearance`, `usage`, `replyLimits`, `retired`; +- instance and organization `vars`; +- project roots, repos, remotes and git state; +- definition `rev` counters and `hireDecision` references; +- everything on the bus: run history, sessions, threads, tasks, decisions, + the trail. + +Stripping role `credentials` lines means a template never carries a request +for a credential. A role that needs one gets it after hire through the +owner-approved binding, as today. + +Before writing, save runs `assertClean` on every string, as definitions do. +A hit refuses `template-secret-suspected` and names the field path, never the +text. + +### Applying one to a new organization + +1. `mosaic org template render ID --output FILE`, then `mosaic org create ID + --from FILE`: the base organization with human, chief and assistant, as + today. +2. `mosaic org pack apply ID saved: --digest D`: the same preview and + apply as a shipped pack. A saved pack applies only to an organization the + caller owns. +3. The chief raises the proposed hires. + +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: + +- 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 + job once packs exist. +- Keeping both would leave a second, empty configuration surface next to the + real one. + +### Migration + +- **Org files.** A print-only conversion in the style of `migrateLaunch` + (`packages/organization/src/migrate-launch.mjs`, decision 145) prints the + file without `plugins['system-type']`. The owner applies it through the + organization writer, so the write gets the usual receipt and the trail + event keeps the old list as evidence. Nothing is applied for it, since the + old modules carried nothing. The Console offers the matching packs once. + After the kind is removed, the existing unknown-module check + (`organization-config.mjs:360-365`) refuses a file that still names + `system-type`, so an unmigrated file fails closed. The refusal text points + at the conversion. +- **`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 CLI words, and `installedSystemTypes`, each with its + tests. The five modules move to `packages/plugins/starter-pack/`. + +### REQ-SYS-1 and REQ-SYS-2 + +Current (PRD 1.20, `docs/prd/mosaic-stack.md:1247-1252`): + +- REQ-SYS-1: "System types are a plug-in kind. Its modules include coding, + creative writing, marketing, Mosaic Montage (the OpenMontage fork) and + business management. An organization enables a list of them." +- REQ-SYS-2: "A project may use any system type its organization enables, and + none is required. An agent working in a project gets the toolsets of the + system types that project uses." + +Proposed: + +- **REQ-SYS-1.** Starter packs are a plug-in kind. Mosaic Stack ships five: + coding, creative writing, marketing, Mosaic Montage and business + management. A pack is a starting configuration of role definitions, + proposed hires, a board style and workflow templates. An organization or + project copies a pack in once and then owns and changes what it copied. A + user can save an organization or project as their own template and apply it + the same way. +- **REQ-SYS-2.** Applying a pack grants nothing by itself. Every agent it + proposes is hired through the hire decision (REQ-AGENT-3), bounded by the + chief's own role, with any line above it approved by the owner. A pack never + sets credentials, rights, the autonomy setting or plug-in choices. Removing + a pack removes nothing it copied. + +## 7. The five shipped packs + +Every organization already has a chief and an assistant from +`org-template.json`, so no pack lists them. Columns map onto the five fixed +states in the order todo, in-progress, in-review, blocked, done. Every role +named is a shipped V2 template. + +### Coding + +| Item | Contents | +|---|---| +| Definitions | `coder`, `reviewer`, `security-reviewer`, `pm`, `merge-gate`, `cto` (template references) | +| Proposed hires | `pm-1` (pm), `coder-1` (coder), `reviewer-1` (reviewer) | +| Board | Backlog, Building, Review, Blocked, Merged; kanban | +| Workflows | `change`: pm writes the task, coder implements, reviewer reviews, merge-gate merges | +| Skills | ms-sdlc, ms-unslop, ms-file-read, ms-file-write, ms-goal, ms-communications | + +`cto` holds `decision.resolve.technical`, a resolve right, so it is flagged on +hire and the owner approves it. It is a definition, not a proposed hire. + +### Creative writing + +| Item | Contents | +|---|---| +| Definitions | `ideas`, `research`, `proofreader`, `fact-checker`, `continuity`, `companion` | +| Proposed hires | `ideas-1` (ideas), `proofreader-1` (proofreader), `continuity-1` (continuity) | +| Board | Ideas, Drafting, Editing, On hold, Finished; kanban | +| Workflows | `chapter`: ideas outlines, the user or companion drafts, continuity checks, proofreader edits | +| Skills | ms-unslop | + +### Marketing + +| Item | Contents | +|---|---| +| Definitions | `chief-marketing-officer`, `research`, `ideas`, `proofreader`, `fact-checker` | +| Proposed hires | `cmo` (chief-marketing-officer), `research-1` (research), `proofreader-1` (proofreader) | +| Board | Ideas, In progress, Approval, Blocked, Live; kanban | +| Workflows | `campaign`: research finds the angle, ideas drafts, fact-checker checks claims, proofreader edits, the user approves publishing | +| Skills | ms-unslop | + +Publishing outside the organization is `message.external` or a hard wait; no +role here holds it, so the user publishes. + +### Mosaic Montage + +| Item | Contents | +|---|---| +| Definitions | `ideas`, `research`, `proofreader`, `reviewer` | +| Proposed hires | `ideas-1` (ideas), `research-1` (research) | +| Board | Concept, Script, Review, Blocked, Final; kanban | +| Workflows | `episode`: research gathers sources, ideas writes the script, proofreader edits, reviewer checks | +| Skills | ms-unslop | + +This pack is a placeholder for scripting work. No video or montage role or +tool exists in the repository, and the OpenMontage integration was not found. +The module name is still open (PRD 1.20 open question 1). + +### Business management + +| Item | Contents | +|---|---| +| Definitions | `chief-executive-officer`, `chief-financial-officer`, `chief-information-officer`, `chief-information-security-officer`, `pm`, `research`, `continuity` | +| Proposed hires | `pm-1` (pm), `research-1` (research) | +| Board | To do, Doing, Waiting on review, Blocked, Done; list | +| Workflows | `decision-brief`: research gathers facts, pm writes the task, the relevant officer drafts, the user decides | +| Skills | ms-unslop, ms-goal, ms-communications | + +The officer roles hold only `message.send` and `record.write` today +(`roles/chief-*.json`). They are definitions to hire when wanted, not +proposals. + +## 8. Build slices + +Each slice lands with its own tests. "Sign-off" marks a slice that touches +`roles/`, `contracts/`, a schema or a security boundary; Jason approves it +before build. + +| Slice | Scope | Acceptance | Test that fails first | Sign-off | +|---|---|---|---|---| +| SP1 | `checkPack` and `narrowsTemplate` in `packages/plugins/src/starter-pack.mjs`; kind `starter-pack` in `kinds.mjs`; loader listing | A valid pack loads; each forbidden key, stale template hash, role wider than its template, root hire, ninth hire, bad state and secret-shaped string refuses with its code | `packages/plugins/tests/starter-pack.test.mjs`: a pack with `credentials` in a definition loads (red until `checkPack` exists) | Yes: new plug-in kind, a pack schema | +| SP2 | The five shipped pack modules | Each loads through SP1; every template pin equals `templateHash` now; no proposed hire carries a flagged line against `roles/chief-of-staff.json` | A test that loads `starter-pack/coding` and checks its pins against `templateHashes` | No (data only, built on SP1) | +| SP3 | Org file keys `packs`, `board`, `workflows`, `projects.

.board`, `projects.

.workflows` in `organization-config.mjs`; writer support | Unknown keys inside each refuse; board needs all five states once; workflow steps name known definitions or templates | `organization-config` test: a file with `board` refuses today as an unknown top key; flip to accept the valid shape and refuse the invalid ones | Yes: org file schema | +| SP4 | `pack.preview` and `pack.apply` (owner verbs, bus ops, `VERB_RIGHTS`) | Preview writes nothing; apply writes add items only and the `packs` entry; stale digest refuses; second apply of the same pack is all `same`; an edited definition is `kept`; apply raises no decision | Broker test: `pack.apply.console` by a caller without `org.hire` refuses `right-required` (red until the verb exists), then the re-apply case | Yes: new verbs in `VERB_RIGHTS` | +| SP5 | The chief's `pack.apply`, `board.set` and `workflow.set` under `organization.change`; proposed hires surfaced in `packs` | The chief's apply writes no definition; a hire the chief raises from a proposal carries broker-computed flagged lines; a hired non-chief agent's apply refuses `not-authorized` | Broker test: a coder session calling `pack.apply` refuses (red until the authority check is wired) | Yes: chief authority surface | +| SP6 | Console: pack picker, preview, apply; board labels on `tasks-board.js`; workflow view and editor | Board reads labels and order from the org or project; no label set means today's labels | `tasks-board` test: an org with a `board` shows its labels (red today, labels are fixed at line 21) | No | +| SP7 | `template.save` and saved-template apply | Save writes a file with none of the stripped keys; a seeded secret refuses; list shows only the caller's own; apply of a saved template goes through SP4 | Test: save an org whose instance has `credentials` and `copyBindings`; assert neither key appears anywhere in the output (red until the stripper exists) | Yes: data leaving an organization | +| SP8 | System-type retirement: print-only conversion, retired verbs, PRD REQ-SYS-1/2 text, removal of `systemTypesFence` and the Console and CLI system-type surface | An org file with `plugins['system-type']` refuses until converted; the applied write's trail event holds the old list; `project.system-types.set` refuses `verb-retired`; `project_system_types` rows unchanged | Migration test with a fixture org holding `["coding"]` | Yes: PRD and data migration | + +Order: SP1, SP2, SP3, SP4, SP5, SP6, SP7, SP8. SP8 can run after SP4 if the +Console needs the space earlier. + +## 9. Open questions for Jason + +1. **Retire system types as a separate kind?** This design replaces the + `system-type` kind with `starter-pack`, migrates org files and keeps + `project_system_types` as history. Recommendation: yes. The modules carry + nothing today, and two surfaces for one idea would confuse users. +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. Role and hire changes stay decisions. +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. -- 2.54.0 From 2912482cd0c0486e3706bd34a29b46124ecb5cc9 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Thu, 12 Nov 2026 14:39:50 -0600 Subject: [PATCH 2/5] docs(plans): starter packs review round Design review (8 findings) and security review (6 findings) fixed: expected flagged lines per proposed hire, applyPack writer op, loader-set origin, organization.change holders for board and workflow writes, filtered-view board labels, workflow prompt guidance (inert, no runner), system-type retired for one release before removal, installed packs classified as executable PL1 code, saved-template placeholders and free-text preview, pinned proposals, and removal tombstones. --- docs/plans/2026-11-12_starter-packs-design.md | 412 +++++++++++++----- 1 file changed, 305 insertions(+), 107 deletions(-) diff --git a/docs/plans/2026-11-12_starter-packs-design.md b/docs/plans/2026-11-12_starter-packs-design.md index 5bdc06bb0..c791968c1 100644 --- a/docs/plans/2026-11-12_starter-packs-design.md +++ b/docs/plans/2026-11-12_starter-packs-design.md @@ -55,8 +55,9 @@ Status means: **exists** (built and usable by a pack as is), **partial** | System-type plug-in kind | `packages/plugins/src/kinds.mjs:84-86` (`switch: "list"`, export `systemType`) | exists | A pure label today. | | The five system-type modules | `packages/plugins/system-type/{coding,creative-writing,marketing,montage,business-management}/index.mjs`, `systemType()` | partial | Each returns `{id, label, description, toolsets: []}`. Nothing else. | | Loading system types | `packages/plugins/src/system-types.mjs:5` `installedSystemTypes` | exists | Any refused module, or keys other than `description,id,label,toolsets`, throws `system-type-invalid`. | -| Org enable | `plugins['system-type']` in the org file; checked at `packages/organization/src/organization-config.mjs:360-365`; fenced by `systemTypesFence`, `packages/organization/src/organization.mjs:126-131` (`organization-system-types-human-only`) | exists | Read back by `system-types.current`, `packages/bus/src/runtime.mjs:904`. Console `GET/POST /api/system-types` (`packages/webui/src/system-types-http.mjs:4-9`), right `org.setup` (`packages/organization/src/org-rights.mjs:90`). | +| Org enable | `plugins['system-type']` in the org file; the kind is checked by `checkPlugins` (`packages/plugins/src/kinds.mjs:146-148`, `unknown-kind`, called at `packages/organization/src/organization-config.mjs:358`) and each module at `organization-config.mjs:360-365`; fenced by `systemTypesFence`, `packages/organization/src/organization.mjs:126-131` (`organization-system-types-human-only`) | exists | Read back by `system-types.current`, `packages/bus/src/runtime.mjs:904`. Console `GET/POST /api/system-types` (`packages/webui/src/system-types-http.mjs:4-9`), right `org.setup` (`packages/organization/src/org-rights.mjs:90`). | | Project selection | `project.system-types[.set]`, `packages/bus/src/broker.mjs:8476-8495`; table `project_system_types` in `packages/bus/src/migrations/3t-3u.sql` (append-only) | exists | Human only. CLI `mosaic project system-types` and `system-types-set`, `packages/cli/src/project.mjs:17-18`. | +| Other system-type surfaces | CLI `mosaic org system-types installed\|enabled` (`packages/cli/src/org.mjs:5,15,49,51`); channel verbs (`packages/plugins/channel/cli/index.mjs:9-10`); Console page `packages/webui/src/public/system-types.js` with hooks in `workspace.js:173,193-194`, `chat.js:623`, `serve.mjs:23,85-86,826`, `webui/src/cli.mjs:9,193`; refusal `system-type-not-enabled` (`packages/bus/src/refusals.mjs:183`) | exists | All label plumbing; none affects an agent. | | System type effect on agents | launcher, harness, `packages/organization/src/resolve.mjs` | missing | No launch path reads system types (no hits in those files). Every module's `toolsets` is empty. | | Shipped role templates | `roles/*.json` + `.md` | exists | V2 roles: assistant, chief-executive-officer, chief-financial-officer, chief-information-officer, chief-information-security-officer, chief-marketing-officer, chief-of-staff, coder, companion, continuity, cto, fact-checker, ideas, merge-gate, pm, proofreader, research, reviewer, security-reviewer. `researcher.json` is V1 and refused by `readTemplate`. | | Template identity | `packages/organization/src/definitions.mjs:174,180` `templateHash`, `templateHashes` | exists | Name plus sha256. | @@ -64,19 +65,19 @@ Status means: **exists** (built and usable by a pack as is), **partial** | Template diff | `packages/organization/src/definitions.mjs:252` `diffDefinition` | exists | Compares a copy with its template's current text. | | Composition check | `packages/organization/src/composition.mjs:443` `checkComposition` | exists | Refuses hard waits (R4). Flags every line above the chief's own role (tools, network, actions, credentials, messaging) and every resolve right. | | Plug-in role templates | `roleTemplates` in `packages/plugins/src/manifest.mjs:48` `checkManifest`; read by `packages/organization/src/composition.mjs` | partial | A PL1 manifest may declare templates with plugin-prefixed names. I did not find the hire path reading them. | -| Hire as a decision | `packages/organization/src/hire-decisions.mjs:65` `hireDecisions` (`prepare`); `authorizeHire` line 32; broker `#raiseHire`, `packages/bus/src/broker.mjs:6578` | exists | `instance.hire` fails `decision-required` (6236). Only the chief raises (`role-not-chief`, 6583). A human with `org.hire` starts and approves in one step through `hire.start.console`. Flagged and credential lines need the owner; root needs the instance owner. An approved hire writes definition `hire-` and the instance (hire-decisions.mjs lines 307-309). | +| Hire as a decision | `packages/organization/src/hire-decisions.mjs:65` `hireDecisions` (`prepare`); `authorizeHire` line 32; broker `#raiseHire`, `packages/bus/src/broker.mjs:6578` | exists | `instance.hire` fails `decision-required` (6236). Only the chief raises (`role-not-chief`, 6583). A human with `org.hire` starts and approves in one step through `hire.start.console`. Flagged and credential lines need the owner; root needs the instance owner. The definition name `hire-` is set at `hire-decisions.mjs:260`; `apply` (line 337 on) writes it and the instance. The input needs a `route` (`hire-decisions.mjs:198-199`). | | Autonomy setting | `packages/organization/src/autonomy.mjs:2-4` | exists | Classes cross-role, merge, policy, authority. Authority defaults to in the loop. Owner-only (`autonomy.*` null in `VERB_RIGHTS`). | | The chief's own settings grant | `organization.change` in `roles/chief-of-staff.json`; broker `organization.set`, `packages/bus/src/broker.mjs:6247-6340` | exists | The chief changes limits directly under its within-role grant. The trail records each change. | | Task board columns | `STORE_STATES`, `packages/bus/src/broker.mjs:334`; `packages/tasks/src/startup.mjs:6` `BUCKETS`; `packages/webui/src/public/tasks-board.js:21` `STATES` | partial | Five fixed states (todo, in-progress, in-review, blocked, done) with fixed labels. Nothing configures them. | | Kanban styles | `packages/webui/src/public/tasks-board.js:146` `kanban`, `:156` `list` | missing | Two layouts (REQ-WS-8). No per-org or per-project style, label or order. | -| Workflow templates | grep for "workflow" in `packages`, `scripts`, `roles`, `contracts`, `missions` | missing | Only refusal text in `packages/bus/src/refusals.mjs`. No workflow document, store or engine. | +| Workflow templates | grep for "workflow" in `packages`, `scripts`, `roles`, `contracts`, `missions` | missing | No workflow document, store or engine. The one built-in flow is the fixed request, PRD and approval path (`packages/bus/src/refusals.mjs:194,251`, "Role workflow", "Role PRD workflow"); it is not pack-settable and this design leaves it alone. | | Mission and task templates | `missions/hello.json`; `tasks/*.json`; schemas in `scripts/mosaic-task.mjs:94,139` | partial | These drive headless worker runs (`run-task.sh`), not org operations. `missions/` holds one file. A pack does not carry them. | | Skills | repo `skills/` (16 skills); `scripts/skill.sh` install/activate into `/skills-enabled`; role `skills` key resolved in `packages/harness/src/bundle.mjs:126-192` | exists | Machine-scoped. A role names skills; launch refuses a missing one. A pack can name skills but cannot install them. | | Plug-ins (code) | `packages/plugins/src/loader.mjs:123` `loadInstalled`; `packages/plugins/src/install.mjs:71` `installPlugin` (hash pin, read-only files) | exists | A module runs with the host's authority (loader header). Install is an instance-owner act (R10). | | Workspace and dashboard sections | `packages/webui/src/workspace-read.mjs:13` `WORKSPACE_SECTIONS` | partial | Fixed: projects, sessions, inbox, tasks, metrics. No per-org configuration found. | | `mosaic org template render` | `packages/organization/src/org-service.mjs:92` `templateRender`; `packages/organization/src/org-template.json` | exists | Renders human, chief, assistant and one `work` project. Presets from harness and model-provider modules (`discoverPresets`, line 61) overlay `plugins`, `launch` and assistant vars. Console `GET /api/org-template`, `/api/org-presets` (`packages/webui/src/org-admin.mjs:413-419`). | | Project settings | org file `projects.` keys root, repo, remote, git, agents (`packages/organization/src/organization-config.mjs:306`); repo file `.mosaic/project.json` (`packages/organization/src/project.mjs`, read only) | partial | No board or workflow setting per project. | -| Agent levels | `packages/organization/src/agent-levels.mjs` (`AGENT_LEVELS` line 15, `checkProjectAgents` line 111, `resolveAgentLevels` line 207) | exists | Root and project definitions refuse bound keys: credentials, holder, tracker, identity, publish, hosts (line 21). | +| Agent levels | `packages/organization/src/agent-levels.mjs` (`AGENT_LEVELS` line 15, `checkProjectAgents` line 111, `resolveAgentLevels` line 207) | exists | Root and project agent entries refuse the instance keys that bind an agent to one organization: credentials, holder, tracker, identity, publish, hosts (`BOUND_KEYS`, line 21; `noBoundKeys`). This is instance material, not the role's credential request lines. | | Copy credential bindings | `copyBindings` fence, `packages/organization/src/organization.mjs:32-36`; writer at line 550 | exists | Owner-approved binding decision only. | | Pack format, apply, saved templates | none | missing | This design. | @@ -89,8 +90,10 @@ found. ## 2. What a pack is A pack is a declarative JSON document. It lists items to copy into an -organization or a project. It never runs, never stays switched on, and grants -nothing. +organization or a project. Once applied it is not a running module, it never +stays switched on, and it grants nothing. How the document is produced +differs by source (next subsection): a shipped or installed pack is code that +returns the document. ### Where packs live @@ -102,10 +105,29 @@ nothing. 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. A -saved template is data only. It is read with `parseJsonFileText`, never -imported or run, capped at 256 KiB, and passes the same `checkPack` as a -shipped pack. +logged, a refused core pack failing the listing as system types do today. + +An installed pack is executable PL1 code. The loader imports its `index.mjs` +(`loadPinnedModule`, `packages/plugins/src/loader.mjs:244-256`), and a loaded +module runs with the host's authority (`loader.mjs:1-6`). Importing it can do +anything the host can, before `checkPack` ever sees the document it returns. +The hash pin proves the bytes are the ones the owner installed, not that they +are safe. So installing a pack is the same trust act as installing any PL1 +plug-in: the instance owner does it at the host (`installPlugin`, R10), under +the same host-authority warning. `checkPack` guards what a pack copies into +an organization; it does not contain the code that produced it. Someone who +wants to share a pack without that trust shares a saved template file, which +is data only. A +saved template is data only. It is read with `parseJsonFileText` +(`packages/organization/src/util.mjs:66`), never imported or run, capped at +256 KiB, and passes `checkPack` with origin `saved`. + +The origin is set by the code that read the document, from where it was +read: `shipped` for a core module, `installed` for a PL1 module, `saved` for a +file under `/templates/`. It is a closed set of those three values +and never a field the document supplies, so a shipped or installed pack +cannot claim the saved rules. Origin `saved` relaxes exactly the two rules +named below and runs every other structural, role, scope and content check. ### The document (packVersion 1) @@ -143,10 +165,10 @@ shipped pack. "change": { "description": "One change from task to merge.", "steps": [ - { "id": "plan", "title": "Write the task", "role": "pm", "state": "todo" }, - { "id": "build", "title": "Implement", "role": "coder", "state": "in-progress" }, - { "id": "review", "title": "Review", "role": "reviewer", "state": "in-review" }, - { "id": "merge", "title": "Merge", "role": "merge-gate", "state": "done" } + { "id": "plan", "title": "Write the task", "roles": ["pm"], "state": "todo" }, + { "id": "build", "title": "Implement", "roles": ["coder"], "state": "in-progress" }, + { "id": "review", "title": "Review", "roles": ["reviewer"], "state": "in-review" }, + { "id": "merge", "title": "Approve the merge", "roles": ["human"], "state": "done" } ] } }, @@ -154,10 +176,15 @@ shipped pack. } ``` -`checkPack` (new, in `packages/plugins/src/starter-pack.mjs`) refuses with -`starter-pack-invalid` unless all of these hold: +Pack definitions carry no `rev`. Apply sets `rev: 1` before +`checkDefinition`, which requires it (`definitions.mjs:30,85`). -- Exact top-level keys as above. `scope` is `org` or `project`. +`checkPack(doc, {origin})` (new, in `packages/plugins/src/starter-pack.mjs`) +refuses with `starter-pack-invalid` unless all of these hold: + +- Exact top-level keys as above. Origin `saved` also allows one more key, + `packs` (provenance text, section 5). Nothing else differs by origin except + the `from` rule below. `scope` is `org` or `project`. - `id` matches `NAME` (`packages/plugins/src/kinds.mjs:20`). `version` is a semver string. - Each `definitions` entry is one of two shapes: @@ -169,36 +196,91 @@ shipped pack. must name `from` and pass `checkDefinition`. Its role must narrow the template named in `from`, line by line, under the new `narrowsTemplate(role, template)`: every tool, action, skill, messaging - target and credential line is in the template, and the network is no - wider. This is "no grant above the templates". A shipped or installed pack - may not carry a role without `from`. + target and credential request line is in the template, and the network + is no wider. This is "no grant above the templates". Origins `shipped` + and `installed` may not carry a role without `from`. Origin `saved` may + (an invented role); it still passes `checkComposition` on apply and is + hired only through a decision. +- Role `credentials` lines are credential requests: a service and scope + names, the same key `ROLE_KEYS` allows today (`definitions.mjs:32`) and + `entryFromTemplate` copies from a template (`definitions.mjs:198-213`). A + pack may carry them as a subset of the `from` template. Credential + material is a different thing and never allowed (next subsection). - `hires`: at most 8. Each `name` passes `requireId`. Each `definition` names - an entry in this pack's `definitions` or an existing V2 template. `level` - is `org` or `project`, never `root`. + an entry in this pack's `definitions`, so every hire is covered by a hash + pin and `starter-pack-template-changed`. There is no `level` field: a hire + is org-level in an `org`-scope pack and project-level in a `project`-scope + pack, never root. A hire carries no `route`; the raiser supplies it, by + default from the organization's `launch` settings. - `board.columns` holds the five `STORE_STATES`, each exactly once, in any order, each with a label of 1 to 32 characters. `layout` is `kanban` or `list`. The tracker contract does not change (REQ-TASK-1, REQ-WS-8). - Each `workflows` entry has 1 to 20 steps. Each step's `state` is in - `STORE_STATES` and its `role` names a definition in the pack or a template. + `STORE_STATES`. Each step's `roles` is a list of 1 to 8 names, each a + definition in the pack, a V2 template, or `human` (the user does that + step). A list means "any of these", for steps like "the relevant officer + drafts". - `skills` entries match `NAME`. They are a list of what the roles need, not an install. -- Every string passes the credential store's `assertClean`, as definitions do - today (`checkDefinition`, `definitions.mjs:81`). + +The held-credential scan cannot run at load: a file load has no credential store +(`NO_STORE`, `definitions.mjs:51`). The bus runs the credential store's +`assertClean` on every string of the pack at preview, at apply and at template +save, as `checkDefinition` does for definitions today (`definitions.mjs:81`). ### What a pack never contains The schema has no key for any of these, so a pack carrying one refuses: -- credentials, credential bindings, tokens, tracker bots, identity, publish, - holder or hosts; +- credential material: instance `credentials`, `copyBindings` or a binding + `copyId`, tokens, tracker bots, identity, publish, holder or hosts; - members, rights or the owner; - the autonomy setting or arbiters; - plug-in selections (`plugins`), including the task tracker and theme; - `launch` (models, limits, keep, noHire, account); - a root-level hire; - any role line outside the `roles/` rails. `checkComposition` still runs on - every definition at apply time and refuses a hard wait (R4) and `role.hire` - or `role.fire` in a hired composition (R9). + every definition at apply time and refuses a hard wait (R4) and + `role.hire`, `role.fire` or `role.change` in a hired composition + (`NEVER_HIRED`, `composition.mjs:51`, R9). + +### What a board style and a workflow do + +**Board.** Labels, order and a default layout over the five fixed states. The +tasks view aggregates every organization (REQ-WS-8, `docs/prd/mosaic-stack.md` +around line 1102), builds one set of columns, and groups each column's cards +by organization (`tasks-board.js:146-150`). So: + +- an organization's or project's labels and order apply only when the view is + filtered to that one organization or project; across several, today's labels + and order stay; +- `layout` is a default used only when the viewer has no saved choice. The + viewer's choice is kept per device in `localStorage` (`tasks-board.js:18-20`) + and wins. + +**Workflow.** A workflow is guidance text that agents plan by. It is not an +engine and moves no task by itself. In v1 the generated prompt +(`prompt`, `packages/harness/src/bundle.mjs:271`) gains a read-only workflows +section for the organization or project the session runs in. It goes to the +chief, to any role holding `task.create` or `task.assign`, and to any role a +step names, which sees its own steps. The section says that workflows are the +organization's guidance, that they grant nothing, and that the contract and +the role rails win on any conflict. The chief and the pm then write and +assign tasks by them through the task verbs they already hold. A step with +`human` in its `roles` is one the agents leave to the user. + +Workflows are inert data. No runner exists, and this design adds none. A +runner that acts on workflow steps is a separate design with its own +sign-off. Any such runner must resolve a live hired instance for each step +and authorize every action against that session's own effective role and +project sandbox, as every bus verb does today. A step's `roles` names are +labels for planning, never authority. + +Workflow text reaches other agents' prompts, so its writers matter. Only a +person with `org.hire` or a holder of `organization.change` writes it, and +the chief can already message any agent, so this opens no new channel. The +text still gets the held-credential scan and a size cap (20 steps, titles up to 120 +characters, description up to 1,000). ### Versioning @@ -216,28 +298,53 @@ exactly as if a person had made them by hand. The steps: 1. **Preview.** `pack.preview` reads the pack and the organization and returns a plan: each item marked `add`, `same` or `kept` (section 3, re-apply), the - flagged lines each proposed hire would carry against the current chief, the + flagged and credential lines each proposed hire would carry against the + current chief, any proposal whose definition is in `launch.noHire`, the skills not enabled on this machine, and a digest of the plan plus the organization file revision. Nothing is written. 2. **Apply.** `pack.apply` takes the digest. It refuses `pack-plan-stale` if - the revision or plan changed. In one organization-file write it: - - writes each `add` definition through `writer.setDefinition`, with `from` - kept, so `checkComposition` runs and `rev` starts at 1; - - writes the `board` and `workflows` items marked `add`; - - appends `{id, version, sha256, scope, project, at, by, proposedHires}` to - a new `packs` key. + the revision or plan changed. It commits once, through a new writer + operation `applyPack(id, plan, {revision, actor, assertClean})`. + `writer.setDefinition` cannot do this: it commits one definition per call + and refuses any other change in the same write + (`organization-definitions-only`, `organization.mjs:401-430`). `applyPack` + reuses its rules, not the function: + - each `add` definition gets `rev: 1`, passes `checkDefinition` with + `assertClean` and `checkComposition`, and keeps `from`; + - an agent actor that writes any definition refuses `human-required`, as + `setDefinition`'s guard does; + - the write refuses if any key changes outside `definitions`, `board`, + `workflows`, `projects.

.board`, `projects.

.workflows` and `packs`; + - it writes the `board` and `workflows` items marked `add` and appends + `{id, version, sha256, origin, scope, project, at, by, added, + proposedHires}` to a new `packs` key. `added` names each item this + apply wrote. Each `proposedHires` entry records the pin the chief will + raise it with: `{template, sha256}` for a template reference, + `{definition, rev}` when this apply wrote the definition, or + `{inline, sha256}` (the canonical hash of the full entry) when it did + not. + + One commit means a crash leaves either the whole pack or none of it, so the + 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}`; the broker computes the flagged and credential - lines, and the decision routes by the authority autonomy level; + `{definition, rev}` and a route it chooses; 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 shows the same lines and needs the owner for any flagged one. + Most shipped hires carry flagged or credential lines, so under the + existing `authorizeHire` rule (`hire-decisions.mjs:40,46`) the owner + approves them on every autonomy setting. That is today's rule for any + hire of these roles, not a gate this design adds. Section 7 lists each + proposal's lines. + A proposal is filled when an instance by that name exists. A pack applied - to a project proposes project-level hires (`level: "project"`), which land - in `projects.

.agents` and refuse bound keys - (`agent-levels.mjs:21`). + to a project proposes project-level hires, which land in + `projects.

.agents`; those entries refuse instance credential material + (`BOUND_KEYS`, `agent-levels.mjs:21`). ### The surface @@ -245,25 +352,46 @@ exactly as if a person had made them by hand. The steps: |---|---| | 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]` | | Console route | `GET /api/packs`, `POST /api/orgs//packs/preview`, `POST /api/orgs//packs/apply` | -| Bus ops | `packs` (read), `pack.preview.console`, `pack.apply.console`; and for the chief, `pack.preview` and `pack.apply` under `organization.change` | -| `VERB_RIGHTS` | `packs` to `board.read`; `pack.preview.console` and `pack.apply.console` to `org.hire` | +| Bus ops | `packs` (read), `pack.preview.console`, `pack.apply.console`; and for agents, `pack.preview` and `pack.apply` for a role whose file lists `organization.change` | +| `VERB_RIGHTS` | `packs` to `board.read`; `pack.preview.console` and `pack.apply.console` to `org.hire`, plus an owner check when the pack's origin is `saved` | `org.hire` fits because the only parts of a pack that could ever matter for capability are role definitions, and T1 already gives definition writes to -`org.hire`. Board and workflows carry no capability. +`org.hire`. Board and workflows carry no capability. A saved pack applies +only to an organization its saver owns (section 5). ### The chief applying a pack The roles-and-rails design says the chief proposes a definition only inside a -decision (section 5). The chief's `pack.apply` respects that: +decision (section 5). The agent `pack.apply` respects that. -- it writes `board` and `workflows` directly under the chief's - `organization.change` grant, the same way `organization.set` writes limits - today (`broker.mjs:6247-6340`); -- it writes no definitions. Each proposed hire the chief raises carries the - pack's composition as role `{inline}` or `{template, sha256}`, so the broker - computes the lines against the chief and the approved hire writes - `hire-` as it does now. +Who may call it: any role whose file lists `organization.change`, the same +test `organization.set` uses (`broker.mjs:6248-6255,6321`). The shipped chief +holds it. The chief could hire another agent holding it as a plain hire, +since step 7 does not flag a line the chief already holds; that agent could +then change boards and workflows too, which carry no capability. "The chief" +in section 4 means any holder of `organization.change`. + +What it does: + +- it writes `board` and `workflows` directly under `organization.change`, + the same way `organization.set` writes limits today + (`broker.mjs:6247-6340`), through `applyPack`; +- it writes no definitions. Each proposed hire is raised by the chief (only + the chief raises, `role-not-chief`). For a template-reference definition + the role is `{template, sha256}`, which the `launch.noHire` check reads + (`hire-decisions.mjs:219`) and which keeps `from`. Only a full-entry + definition goes as `{inline}`; that path starts from an empty role + (`hire-decisions.mjs:227-228`), drops `from` and skips `noHire`, so preview + 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`. ### A second pack, and re-applying @@ -271,15 +399,23 @@ Items merge by name and nothing is overwritten: | Item in the pack | Organization already has | Result | |---|---|---| -| Definition | nothing by that name | `add` | +| Definition | nothing by that name, never added by this pack | `add` | +| Definition | nothing by that name, but an earlier apply of this pack id lists it in `added` | `removed`, no write | | Definition | same composition, instructions and personality | `same`, no write | | Definition | different (edited, or a newer pack version) | `kept`. The preview shows `diffDefinition`. The person may import the pack's version under another name with `--as`. | | Board | no `board` key | `add` | | Board | a different board | `kept` | -| Workflow | nothing by that name | `add` | +| Workflow | nothing by that name, never added by this pack | `add` | +| Workflow | nothing by that name, but an earlier apply of this pack id lists it in `added` | `removed`, no write | | Workflow | a different workflow by that name | `kept`, with `--as` | | Proposed hire | an instance or open hire decision by that name | dropped from the new proposals | +`removed` keeps a deliberate removal from coming back on its own: the +earlier `packs` entries act as the tombstone. The person may bring the item +back on purpose with `--as` and a new name, which the preview shows as an +`add`. The same rule covers a board the organization removed after this pack +added it. + There is no forced overwrite flag. To take a newer pack version of a definition a hired agent uses, the person or chief raises a change decision, which is the existing path. @@ -299,16 +435,20 @@ The table shows who does what. Every row reuses a path that exists or adds a verb at the same level as its neighbours. None adds a person to a path that had none. +"The chief" in this table means any role whose file lists +`organization.change` (section 3) for board, workflow and pack writes. Hire +and change decisions stay chief-only. + | Change | A person does it directly | The chief does it | The limit at that step | |---|---|---|---| | Import a role template | `definition.import` (T1, `org.hire`) | Inside a hire or change decision, role `{template, sha256}` | `checkComposition`. Nothing runs until a hire. | | Customize or invent a role | `definition.edit` or `invent` (T1) | Inside a hire or change decision, role `{inline}` | `checkComposition`; lines above the chief are flagged and only the owner approves them (R8). Editing a definition in use refuses, so a running agent never widens without a change decision. | -| Hire, fire, change an agent | `hire.start.console` (`org.hire`) | `hire.raise` (only the chief, R9) | Broker-computed flagged and credential lines; authority autonomy level; root needs the instance owner. | +| 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` (`org.hire`) | `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` (`org.hire`) | `workflow.set` under `organization.change` | Steps name existing definitions or templates and the five states. A workflow grants nothing. | +| Create or edit a workflow | `workflow.set.console` (`org.hire`) | `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. | | Change board labels or order | `board.set.console` (`org.hire`) | `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 agents refuse bound keys (`agent-levels.mjs:21`). | +| 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. | | Install a pack plug-in | `installPlugin` (instance owner, R10) | Not available | Host authority. | | Autonomy level | Owner only | Never (R12) | Unchanged. | @@ -322,9 +462,11 @@ had none. `composition.mjs:491`). The chief cannot omit them. 4. Resolve: an agent resolver can resolve only a plain hire with no flagged or credential lines, not at root, not about itself (`authorizeHire`, - `hire-decisions.mjs:39-42`). Anything flagged goes to the owner. -5. A hired agent: `role.hire` and `role.fire` refuse in its composition (R9), - so it cannot hire in turn. + `hire-decisions.mjs:39-42`). Anything flagged goes to the owner. Of the + shipped proposals only `cmo` is plain (section 7), so in practice the + owner approves the rest, as for any hire of those roles today. +5. A hired agent: `role.hire`, `role.fire` and `role.change` refuse in its + composition (`NEVER_HIRED`, R9), so it cannot hire in turn. 6. Saved templates go through steps 2 to 5 again in the new organization. **The sandbox.** Every agent a pack proposes is an org or project agent. It @@ -354,18 +496,21 @@ the exact JSON that will be written; save takes its digest. ### What is captured - `definitions`: role, instructions, personality and `from`. Definitions named - `hire-` are renamed after the instance that uses them, so no - decision id leaves the organization. + `hire-` are renamed `-` (for example `coder-1`), so no + decision id or instance name leaves the organization. - `hires`: one proposal per org instance (or project agent, with `--project`): - instance name, definition and level. Never the chief or the assistant, - which every organization already has. + a generated name `-` and the definition. Instance names are not + copied; they often name a person or a client. The level follows the template's scope. Never + the chief or the assistant, which every organization already has. - `board` and `workflows`. - `skills`: the union of the captured roles' `skills`. - `packs`: the ids and versions applied before, as provenance text. -The result is a pack document with `source: "saved"`. A role in a saved -template may lack `from` (an invented role); that is the one difference from a -shipped pack, and it still passes `checkComposition` on apply. +The result is a pack document read later with origin `saved` (section 2). A +role in a saved template may lack `from` (an invented role) and the document +may carry `packs` provenance; those are the only differences from a shipped +pack. Every role still passes `checkComposition` on apply and is hired only +through a decision. ### What is stripped @@ -374,8 +519,8 @@ Everything not listed above. In particular: - the owner, members, accounts and rights; - identities: `human`, instance `identity`, `publish`, `holder`, `hosts`, tracker bot names and ids; -- credentials: role `credentials` lines, instance credentials, `copyBindings` - and their `copyId`, `orgIncarnation` and `generation`; +- credential material: instance credentials, `copyBindings` and their + `copyId`, `orgIncarnation` and `generation`; - `launch` (models, limits, keep, noHire, account, grant scope), `autonomy`, `arbiters`, `plugins`, `appearance`, `usage`, `replyLimits`, `retired`; - instance and organization `vars`; @@ -384,13 +529,32 @@ Everything not listed above. In particular: - everything on the bus: run history, sessions, threads, tasks, decisions, the trail. -Stripping role `credentials` lines means a template never carries a request -for a credential. A role that needs one gets it after hire through the -owner-approved binding, as today. +Role `credentials` lines are kept. They are requests (a service and scope +names), part of what the role is, the same lines a shipped template carries. +They carry no token or binding. On apply the broker lists them as credential +lines on the hire, so the owner approves each one (`authorizeHire`), and the +credential itself arrives only through the owner-approved binding, as today. -Before writing, save runs `assertClean` on every string, as definitions do. -A hit refuses `template-secret-suspected` and names the field path, never the -text. +Free text is the owner's work product and is kept: instructions, +personality, workflow titles and descriptions, and board labels. It can still +carry private details, so save handles it in three ways: + +1. Save replaces each literal occurrence of the organization id, its project + ids, its instance names, the owner's account name and the project roots + with the placeholders `{org}`, `{project}`, `{agent}`, `{owner}` and + `{root}`. The preview lists every replacement by field path. +2. The preview shows every kept free-text field in full, grouped by field + path, so the owner reads what leaves before taking the digest. A field the + owner wants gone is edited in the organization first and saved again. +3. Save runs `assertClean` on every string, as definitions do. That check is + credential-value detection only: it refuses text that contains a value the + credential store holds now (`assertNotHeld`, `packages/bus/src/credentials.mjs:721-723`). + It does not find names, paths, clients or a secret the store never held. + A hit refuses `template-secret-suspected` and names the field path, never + the text. + +Nothing else scrubs free text. The owner is the reviewer of what a template +says, which is why save is owner only and shows the exact JSON first. ### Applying one to a new organization @@ -418,23 +582,32 @@ reasons: ### Migration -- **Org files.** A print-only conversion in the style of `migrateLaunch` - (`packages/organization/src/migrate-launch.mjs`, decision 145) prints the - file without `plugins['system-type']`. The owner applies it through the - organization writer, so the write gets the usual receipt and the trail - event keeps the old list as evidence. Nothing is applied for it, since the - old modules carried nothing. The Console offers the matching packs once. - After the kind is removed, the existing unknown-module check - (`organization-config.mjs:360-365`) refuses a file that still names - `system-type`, so an unmigrated file fails closed. The refusal text points - at the conversion. +- **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 later slice removes the kind once no org file on the instance names + it. A print-only conversion in the style of `migrateLaunch` + (`packages/organization/src/migrate-launch.mjs`, decision 145) covers a + file that was never written in between. - **`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 CLI words, and `installedSystemTypes`, each with its - tests. The five modules move to `packages/plugins/starter-pack/`. + types Console route and page, the CLI words (`mosaic org system-types`, + `mosaic project system-types`), the channel verbs, + `system-type-not-enabled`, and `installedSystemTypes`, each with its tests + (section 1 lists every file). The five modules move to + `packages/plugins/starter-pack/`. ### REQ-SYS-1 and REQ-SYS-2 @@ -469,6 +642,24 @@ Every organization already has a chief and an assistant from states in the order todo, in-progress, in-review, blocked, done. Every role named is a shipped V2 template. +**What each proposed hire carries against the chief.** The chief holds tools +`read, grep, find, ls`, network `api-only`, and gitea `read:repository, +read:user` (`roles/chief-of-staff.json`). `checkComposition` flags every line +above that (step 7) and every resolve right (step 5). Under `authorizeHire` +any flagged or credential line sends the hire to the owner. That is the rule +today for any hire of these roles; packs add no gate. The lines below are +tools, actions, resolve rights and credentials read from `roles/*.json`. +Messaging lines also count; SP2 checks in the exact broker-computed list. + +| Proposal | Flagged lines | Credential lines | Who approves | +|---|---|---|---| +| `pm` | tools `write, edit`; actions `task.create, task.assign, task.reassign, task.schedule, task.update.assigned, task.close`; crossRole `task.priority.change, task.scope.change`; resolve `decision.resolve.delivery` | gitea `write:issue`; vikunja task scopes | owner | +| `coder` | tools `write, edit, bash`; actions `task.update.assigned, git.push.working, review.request`; crossRole `task.scope.change` | gitea `write:issue, write:repository`; vikunja task scopes | owner | +| `reviewer` | tool `bash`; actions `review.verdict, task.update.assigned` | gitea `write:issue, write:repository`; vikunja task scopes | owner | +| `research` | tool `web`; action `library.write` | gitea `write:repository` | owner | +| `ideas`, `proofreader`, `continuity` | action `library.write` | gitea `write:repository` | owner | +| `chief-marketing-officer` | none found | none | plain: by the authority autonomy level | + ### Coding | Item | Contents | @@ -476,11 +667,12 @@ named is a shipped V2 template. | Definitions | `coder`, `reviewer`, `security-reviewer`, `pm`, `merge-gate`, `cto` (template references) | | Proposed hires | `pm-1` (pm), `coder-1` (coder), `reviewer-1` (reviewer) | | Board | Backlog, Building, Review, Blocked, Merged; kanban | -| Workflows | `change`: pm writes the task, coder implements, reviewer reviews, merge-gate merges | +| Workflows | `change`: pm writes the task, coder implements, reviewer reviews, the user (or a hired merge-gate) approves the merge (`roles: ["human", "merge-gate"]`) | | Skills | ms-sdlc, ms-unslop, ms-file-read, ms-file-write, ms-goal, ms-communications | -`cto` holds `decision.resolve.technical`, a resolve right, so it is flagged on -hire and the owner approves it. It is a definition, not a proposed hire. +`cto`, `security-reviewer` and `merge-gate` are definitions to hire when the +team grows, not first proposals. The chief is the default technical arbiter +(`org-template.json` `arbiters`), so a new team does not need a `cto` first. ### Creative writing @@ -489,7 +681,7 @@ hire and the owner approves it. It is a definition, not a proposed hire. | Definitions | `ideas`, `research`, `proofreader`, `fact-checker`, `continuity`, `companion` | | Proposed hires | `ideas-1` (ideas), `proofreader-1` (proofreader), `continuity-1` (continuity) | | Board | Ideas, Drafting, Editing, On hold, Finished; kanban | -| Workflows | `chapter`: ideas outlines, the user or companion drafts, continuity checks, proofreader edits | +| Workflows | `chapter`: ideas outlines, the user or companion drafts (`roles: ["human", "companion"]`), continuity checks, proofreader edits | | Skills | ms-unslop | ### Marketing @@ -499,7 +691,7 @@ hire and the owner approves it. It is a definition, not a proposed hire. | Definitions | `chief-marketing-officer`, `research`, `ideas`, `proofreader`, `fact-checker` | | Proposed hires | `cmo` (chief-marketing-officer), `research-1` (research), `proofreader-1` (proofreader) | | Board | Ideas, In progress, Approval, Blocked, Live; kanban | -| Workflows | `campaign`: research finds the angle, ideas drafts, fact-checker checks claims, proofreader edits, the user approves publishing | +| Workflows | `campaign`: research finds the angle, ideas drafts, fact-checker checks claims, proofreader edits, the user approves publishing (`roles: ["human"]`) | | Skills | ms-unslop | Publishing outside the organization is `message.external` or a hard wait; no @@ -526,12 +718,13 @@ The module name is still open (PRD 1.20 open question 1). | Definitions | `chief-executive-officer`, `chief-financial-officer`, `chief-information-officer`, `chief-information-security-officer`, `pm`, `research`, `continuity` | | Proposed hires | `pm-1` (pm), `research-1` (research) | | Board | To do, Doing, Waiting on review, Blocked, Done; list | -| Workflows | `decision-brief`: research gathers facts, pm writes the task, the relevant officer drafts, the user decides | +| Workflows | `decision-brief`: research gathers facts, pm writes the task, the relevant officer drafts (`roles` lists the four officers), the user decides (`roles: ["human"]`) | | Skills | ms-unslop, ms-goal, ms-communications | The officer roles hold only `message.send` and `record.write` today (`roles/chief-*.json`). They are definitions to hire when wanted, not -proposals. +proposals. The workflow's "relevant officer" step lists all four in its +`roles`. ## 8. Build slices @@ -541,30 +734,35 @@ before build. | Slice | Scope | Acceptance | Test that fails first | Sign-off | |---|---|---|---|---| -| SP1 | `checkPack` and `narrowsTemplate` in `packages/plugins/src/starter-pack.mjs`; kind `starter-pack` in `kinds.mjs`; loader listing | A valid pack loads; each forbidden key, stale template hash, role wider than its template, root hire, ninth hire, bad state and secret-shaped string refuses with its code | `packages/plugins/tests/starter-pack.test.mjs`: a pack with `credentials` in a definition loads (red until `checkPack` exists) | Yes: new plug-in kind, a pack schema | -| SP2 | The five shipped pack modules | Each loads through SP1; every template pin equals `templateHash` now; no proposed hire carries a flagged line against `roles/chief-of-staff.json` | A test that loads `starter-pack/coding` and checks its pins against `templateHashes` | No (data only, built on SP1) | -| SP3 | Org file keys `packs`, `board`, `workflows`, `projects.

.board`, `projects.

.workflows` in `organization-config.mjs`; writer support | Unknown keys inside each refuse; board needs all five states once; workflow steps name known definitions or templates | `organization-config` test: a file with `board` refuses today as an unknown top key; flip to accept the valid shape and refuse the invalid ones | Yes: org file schema | -| SP4 | `pack.preview` and `pack.apply` (owner verbs, bus ops, `VERB_RIGHTS`) | Preview writes nothing; apply writes add items only and the `packs` entry; stale digest refuses; second apply of the same pack is all `same`; an edited definition is `kept`; apply raises no decision | Broker test: `pack.apply.console` by a caller without `org.hire` refuses `right-required` (red until the verb exists), then the re-apply case | Yes: new verbs in `VERB_RIGHTS` | -| SP5 | The chief's `pack.apply`, `board.set` and `workflow.set` under `organization.change`; proposed hires surfaced in `packs` | The chief's apply writes no definition; a hire the chief raises from a proposal carries broker-computed flagged lines; a hired non-chief agent's apply refuses `not-authorized` | Broker test: a coder session calling `pack.apply` refuses (red until the authority check is wired) | Yes: chief authority surface | -| SP6 | Console: pack picker, preview, apply; board labels on `tasks-board.js`; workflow view and editor | Board reads labels and order from the org or project; no label set means today's labels | `tasks-board` test: an org with a `board` shows its labels (red today, labels are fixed at line 21) | No | -| SP7 | `template.save` and saved-template apply | Save writes a file with none of the stripped keys; a seeded secret refuses; list shows only the caller's own; apply of a saved template goes through SP4 | Test: save an org whose instance has `credentials` and `copyBindings`; assert neither key appears anywhere in the output (red until the stripper exists) | Yes: data leaving an organization | -| SP8 | System-type retirement: print-only conversion, retired verbs, PRD REQ-SYS-1/2 text, removal of `systemTypesFence` and the Console and CLI system-type surface | An org file with `plugins['system-type']` refuses until converted; the applied write's trail event holds the old list; `project.system-types.set` refuses `verb-retired`; `project_system_types` rows unchanged | Migration test with a fixture org holding `["coding"]` | Yes: PRD and data migration | +| SP1 | `checkPack` and `narrowsTemplate` in `packages/plugins/src/starter-pack.mjs`; kind `starter-pack` in `kinds.mjs`; loader listing with origin set by the loader | A valid pack loads; each forbidden key, stale template hash, role wider than its template, credential line wider than its template, root hire, ninth hire, unpinned hire definition and bad state refuses with its code; a shipped or installed pack with a `packs` key or a role without `from` refuses | `packages/plugins/tests/starter-pack.test.mjs`: a pack whose definition carries `copyBindings` loads (red until `checkPack` exists) | Yes: new plug-in kind, a pack schema | +| SP2 | The five shipped pack modules, each with a checked-in list of the flagged and credential lines its proposed hires carry | Each loads through SP1; every template pin equals `templateHash` now; for each proposed hire the lines `checkComposition` computes against `roles/chief-of-staff.json` equal the checked-in list (section 7); `chief-marketing-officer` is the only plain proposal | A test that loads `starter-pack/coding` and compares its pins against `templateHashes` and its computed lines against the list | No (data only, built on SP1) | +| SP3 | Org file keys `packs`, `board`, `workflows`, `projects.

.board`, `projects.

.workflows` in `organization-config.mjs`; writer op `applyPack` in `organization.mjs` | Unknown keys inside each refuse; board needs all five states once; workflow step `roles` name known definitions, templates or `human`; the size caps hold; `applyPack` commits in one write, refuses keys outside its list, refuses `definition-in-use` like `setDefinition`, and refuses `human-required` when an agent actor writes a definition | `organization-config` test: a file with `board` refuses today as an unknown top key; flip to accept the valid shape and refuse the invalid ones | Yes: org file schema and a writer op | +| SP4 | `pack.preview` and `pack.apply` (owner verbs, bus ops, `VERB_RIGHTS`, the saved-origin owner check) on top of `applyPack` | Preview writes nothing and lists flagged lines, credential lines and `noHire` marks; a pack string holding a held credential value refuses at preview and at apply; apply writes add items only and the `packs` entry with `added` and pinned `proposedHires`; stale digest refuses; second apply of the same pack is all `same`; an edited definition is `kept`; a removed item an earlier apply added is `removed` and not re-added; apply raises no decision | Broker test: `pack.apply.console` by a caller without `org.hire` refuses `right-required` (red until the verb exists), then the re-apply case | Yes: new verbs in `VERB_RIGHTS` | +| SP5 | Agent `pack.apply`, `board.set` and `workflow.set` for a role whose file lists `organization.change`; proposed hires surfaced in `packs` | An agent's apply writes no definition and refuses `human-required` if the plan has one; an agent without `organization.change` refuses `not-authorized`; a hire the chief raises from a proposal carries broker-computed flagged lines and refuses when its recorded pin no longer matches; a non-chief raise still refuses `role-not-chief` | Broker test: a coder session calling `pack.apply` refuses (red until the authority check is wired) | Yes: agent authority surface | +| SP6 | Console: pack picker, preview, apply; board labels on `tasks-board.js`; workflow view and editor | Labels and order apply only when the view is filtered to one org or project; the all-orgs view keeps today's labels; the pack layout is a default and the viewer's saved localStorage choice wins | `tasks-board` test: a view filtered to an org with a `board` shows its labels (red today, labels are fixed at line 21) | No | +| SP7 | Workflow guidance in the harness: a read-only workflows section in `prompt()` (`packages/harness/src/bundle.mjs:271`) for the chief, roles holding `task.create` or `task.assign`, and roles a step names | The section lists the org's or project's workflows and says they grant nothing; a role no step names and without those actions gets no section; contract text is unchanged | Bundle test: a chief bundle for an org with a `change` workflow contains the section (red until it is added) | Yes: changes what agents are told | +| SP8 | `template.save` and saved-template apply | Save writes a 0600 file with none of the stripped keys and keeps role credential request lines; instance names, org and project ids, the owner's account and project roots become placeholders; the preview lists every kept free-text field; a seeded held credential value refuses; list shows only the caller's own; apply of a saved template goes through SP4 with the owner check | Test: save an org whose instance has `credentials` and `copyBindings`; assert neither appears anywhere in the output (red until the stripper exists) | Yes: data leaving an organization | +| SP9 | System-type retirement, step 1: `system-type` kept as a retired kind; PRD REQ-SYS-1/2 text; removal of `systemTypesFence` and the Console, CLI and channel system-type surface | An org file with `plugins['system-type']` loads and the key is ignored; a write adding a name refuses `verb-retired`; the next writer commit drops the key and its trail event holds the old list; `project.system-types.set` refuses `verb-retired`; `project_system_types` rows unchanged | Migration test with a fixture org holding `["coding"]`: it loads after `system-types` code is gone (red if the kind is dropped) | Yes: PRD and data migration | +| SP10 | Step 2: remove the retired kind and add the print-only conversion | Runs only once no org file on the instance names `system-type`; the conversion prints a file without the key | Test: the conversion of the SP9 fixture prints no `system-type` key | No (follows SP9) | -Order: SP1, SP2, SP3, SP4, SP5, SP6, SP7, SP8. SP8 can run after SP4 if the -Console needs the space earlier. +Order: SP1 to SP10 in number order. SP9 can run after SP4 if the Console +needs the space earlier. SP10 waits at least one release after SP9. ## 9. Open questions for Jason 1. **Retire system types as a separate kind?** This design replaces the - `system-type` kind with `starter-pack`, migrates org files and keeps - `project_system_types` as history. Recommendation: yes. The modules carry + `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. 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. Role and hire changes stay decisions. + 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. 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 -- 2.54.0 From 24047292320cd0d8425d77343be2f3d448f8c35b Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Thu, 12 Nov 2026 14:42:39 -0600 Subject: [PATCH 3/5] docs(plans): starter packs slices ready for one go Each slice names its owned files, the exact KINDS, org file key, VERB_RIGHTS and CONSOLE_VERBS change it makes, acceptance and a test that fails before it. No slice edits roles/ or contracts/. Open questions say which slice each answer changes. --- docs/plans/2026-11-12_starter-packs-design.md | 256 ++++++++++++++++-- 1 file changed, 236 insertions(+), 20 deletions(-) diff --git a/docs/plans/2026-11-12_starter-packs-design.md b/docs/plans/2026-11-12_starter-packs-design.md index c791968c1..7fb4edfb9 100644 --- a/docs/plans/2026-11-12_starter-packs-design.md +++ b/docs/plans/2026-11-12_starter-packs-design.md @@ -728,33 +728,246 @@ proposals. The workflow's "relevant officer" step lists all four in its ## 8. Build slices -Each slice lands with its own tests. "Sign-off" marks a slice that touches -`roles/`, `contracts/`, a schema or a security boundary; Jason approves it -before build. +The slices run in order, SP1 to SP10, each landing with its own tests and +the full suites green. They are written so that one go from Jason covers all +of them; nothing in a slice waits on a further design choice. -| Slice | Scope | Acceptance | Test that fails first | Sign-off | -|---|---|---|---|---| -| SP1 | `checkPack` and `narrowsTemplate` in `packages/plugins/src/starter-pack.mjs`; kind `starter-pack` in `kinds.mjs`; loader listing with origin set by the loader | A valid pack loads; each forbidden key, stale template hash, role wider than its template, credential line wider than its template, root hire, ninth hire, unpinned hire definition and bad state refuses with its code; a shipped or installed pack with a `packs` key or a role without `from` refuses | `packages/plugins/tests/starter-pack.test.mjs`: a pack whose definition carries `copyBindings` loads (red until `checkPack` exists) | Yes: new plug-in kind, a pack schema | -| SP2 | The five shipped pack modules, each with a checked-in list of the flagged and credential lines its proposed hires carry | Each loads through SP1; every template pin equals `templateHash` now; for each proposed hire the lines `checkComposition` computes against `roles/chief-of-staff.json` equal the checked-in list (section 7); `chief-marketing-officer` is the only plain proposal | A test that loads `starter-pack/coding` and compares its pins against `templateHashes` and its computed lines against the list | No (data only, built on SP1) | -| SP3 | Org file keys `packs`, `board`, `workflows`, `projects.

.board`, `projects.

.workflows` in `organization-config.mjs`; writer op `applyPack` in `organization.mjs` | Unknown keys inside each refuse; board needs all five states once; workflow step `roles` name known definitions, templates or `human`; the size caps hold; `applyPack` commits in one write, refuses keys outside its list, refuses `definition-in-use` like `setDefinition`, and refuses `human-required` when an agent actor writes a definition | `organization-config` test: a file with `board` refuses today as an unknown top key; flip to accept the valid shape and refuse the invalid ones | Yes: org file schema and a writer op | -| SP4 | `pack.preview` and `pack.apply` (owner verbs, bus ops, `VERB_RIGHTS`, the saved-origin owner check) on top of `applyPack` | Preview writes nothing and lists flagged lines, credential lines and `noHire` marks; a pack string holding a held credential value refuses at preview and at apply; apply writes add items only and the `packs` entry with `added` and pinned `proposedHires`; stale digest refuses; second apply of the same pack is all `same`; an edited definition is `kept`; a removed item an earlier apply added is `removed` and not re-added; apply raises no decision | Broker test: `pack.apply.console` by a caller without `org.hire` refuses `right-required` (red until the verb exists), then the re-apply case | Yes: new verbs in `VERB_RIGHTS` | -| SP5 | Agent `pack.apply`, `board.set` and `workflow.set` for a role whose file lists `organization.change`; proposed hires surfaced in `packs` | An agent's apply writes no definition and refuses `human-required` if the plan has one; an agent without `organization.change` refuses `not-authorized`; a hire the chief raises from a proposal carries broker-computed flagged lines and refuses when its recorded pin no longer matches; a non-chief raise still refuses `role-not-chief` | Broker test: a coder session calling `pack.apply` refuses (red until the authority check is wired) | Yes: agent authority surface | -| SP6 | Console: pack picker, preview, apply; board labels on `tasks-board.js`; workflow view and editor | Labels and order apply only when the view is filtered to one org or project; the all-orgs view keeps today's labels; the pack layout is a default and the viewer's saved localStorage choice wins | `tasks-board` test: a view filtered to an org with a `board` shows its labels (red today, labels are fixed at line 21) | No | -| SP7 | Workflow guidance in the harness: a read-only workflows section in `prompt()` (`packages/harness/src/bundle.mjs:271`) for the chief, roles holding `task.create` or `task.assign`, and roles a step names | The section lists the org's or project's workflows and says they grant nothing; a role no step names and without those actions gets no section; contract text is unchanged | Bundle test: a chief bundle for an org with a `change` workflow contains the section (red until it is added) | Yes: changes what agents are told | -| SP8 | `template.save` and saved-template apply | Save writes a 0600 file with none of the stripped keys and keeps role credential request lines; instance names, org and project ids, the owner's account and project roots become placeholders; the preview lists every kept free-text field; a seeded held credential value refuses; list shows only the caller's own; apply of a saved template goes through SP4 with the owner check | Test: save an org whose instance has `credentials` and `copyBindings`; assert neither appears anywhere in the output (red until the stripper exists) | Yes: data leaving an organization | -| SP9 | System-type retirement, step 1: `system-type` kept as a retired kind; PRD REQ-SYS-1/2 text; removal of `systemTypesFence` and the Console, CLI and channel system-type surface | An org file with `plugins['system-type']` loads and the key is ignored; a write adding a name refuses `verb-retired`; the next writer commit drops the key and its trail event holds the old list; `project.system-types.set` refuses `verb-retired`; `project_system_types` rows unchanged | Migration test with a fixture org holding `["coding"]`: it loads after `system-types` code is gone (red if the kind is dropped) | Yes: PRD and data migration | -| SP10 | Step 2: remove the retired kind and add the print-only conversion | Runs only once no org file on the instance names `system-type`; the conversion prints a file without the key | Test: the conversion of the SP9 fixture prints no `system-type` key | No (follows SP9) | +**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 +named below, and every change to one is listed in its slice. The changes +that need sign-off are these: -Order: SP1 to SP10 in number order. SP9 can run after SP4 if the Console -needs the space earlier. SP10 waits at least one release after SP9. +- a new plug-in kind and a new `switch` value (SP1); +- new org file keys (SP3); +- new verbs in `VERB_RIGHTS` (`packages/organization/src/org-rights.mjs:34`) + and in `CONSOLE_VERBS` (`packages/bus/src/broker.mjs:181`) (SP4, SP8); +- an agent authority surface for `organization.change` holders (SP5); +- new text in agents' generated prompts (SP7); +- data leaving an organization (SP8); +- a PRD wording change and a retired kind (SP9). + +New refusal codes are added to `packages/bus/src/refusals.mjs` by the slice +that first uses them: `starter-pack-invalid`, +`starter-pack-template-changed`, `pack-plan-stale`, +`starter-pack-proposal-stale`, `template-secret-suspected` and +`verb-retired`. + +**Owner boundary.** "Owns" lists the files a slice may change. A slice that +needs a change outside its list stops and reports; it does not widen +itself. Tests for a slice live beside the files it owns. + +### SP1: the pack kind and its checker + +- Owns: `packages/plugins/src/starter-pack.mjs` (new), + `packages/plugins/src/kinds.mjs`, `packages/plugins/src/loader.mjs` (only + if the listing needs a hook), `packages/bus/src/refusals.mjs`, + `packages/plugins/tests/starter-pack.test.mjs` (new). +- Schema change: `KINDS` gains `"starter-pack"` with `interfaceVersion: 1`, + `exports: ["starterPack"]` and a new `switch: "none"`. `namesFor` refuses + any setting for a `none` kind with `bad-setting`, so an org file can never + switch a pack on through `plugins`. +- Builds: `checkPack(doc, {origin})` and `narrowsTemplate(role, template)` + as section 2 states; `installedStarterPacks({dataRoot})`, which lists + shipped and installed packs through `loadInstalled` and sets the origin; + `readSavedTemplate(path)` through `parseJsonFileText` with the 256 KiB cap. +- Acceptance: a valid pack loads. Each of these refuses with its code: a + forbidden key, a stale template hash, a role wider than its template, a + credential line wider than its template, a root hire, a ninth hire, a hire + naming no pack definition, a bad state, a duplicate column. A shipped or + installed pack with a `packs` key or a role without `from` refuses; a + saved one with either passes. `plugins: {"starter-pack": ["coding"]}` + refuses `bad-setting`. +- Red before: a pack whose definition carries `copyBindings` passes (there + is no `checkPack`, so nothing refuses it). + +### SP2: the five shipped packs + +- Owns: `packages/plugins/starter-pack/{coding,creative-writing,marketing,montage,business-management}/` + (new: `index.mjs` and `expected-lines.json` each), + `packages/plugins/tests/starter-pack-shipped.test.mjs` (new). +- Schema change: none. +- Builds: the five documents in section 7, each pinned to today's + `templateHash` values. +- Acceptance: each loads through SP1 with origin `shipped`. For each + proposed hire, the flagged and credential lines `checkComposition` + computes against `roles/chief-of-staff.json` equal `expected-lines.json`; + `chief-marketing-officer` is the only proposal with none. A template edit + that moves a hash turns this test red until the pack is re-pinned, which + is the intended review point. +- Red before: loading `starter-pack/coding` finds no module. + +### SP3: org file keys and the writer op + +- Owns: `packages/organization/src/organization-config.mjs`, + `packages/organization/src/organization.mjs`, their tests. +- Schema change: `TOP_KEYS` (`organization-config.mjs:32`) gains `packs`, + `board` and `workflows`. The project entry keys (`organization-config.mjs:306`) + gain `board` and `workflows`. Shapes, each refusing unknown keys: + - `board`: `{columns: [{state, label}] x5, layout}`, all five + `STORE_STATES` once, labels 1 to 32 characters, layout `kanban` or + `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`; + - `packs`: a list of `{id, version, sha256, origin, scope, project, at, by, added, proposedHires}`, + origin one of `shipped`, `installed`, `saved`. +- Builds: `applyPack(id, plan, {revision, actor, assertClean})` on the writer + with the rules in section 3: one commit, `rev: 1` on each added + definition, `checkDefinition` with `assertClean`, `definition-in-use` as + `setDefinition` refuses it, `human-required` when an agent actor writes a + definition, and a refusal for any key outside its list. +- Acceptance: valid shapes load and each invalid one refuses; `applyPack` + 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`. +- 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 and apply for people + +- Owns: `packages/bus/src/broker.mjs` (the new verbs only), + `packages/organization/src/org-rights.mjs`, `packages/cli/src/org.mjs` + (the `pack` words), `packages/webui/src/serve.mjs` (the two routes), their + tests. +- Schema change: `VERB_RIGHTS` gains `packs: 'board.read'`, + `'pack.preview.console': 'org.hire'` and `'pack.apply.console': 'org.hire'`. + `CONSOLE_VERBS` gains the two console verbs. A saved-origin pack also + needs the caller to be the owner. +- Builds: `mosaic org pack list|preview|apply`, `POST /api/orgs//packs/preview` + and `POST /api/orgs//packs/apply`, and the merge table in section 3, + including `kept`, `--as` and the `removed` tombstone. +- Acceptance: preview writes nothing and lists flagged lines, credential + lines and `noHire` marks; a pack string holding a held credential value + refuses at preview and at apply; apply writes only `add` items and the + `packs` entry with `added` and pinned `proposedHires`; a stale digest + refuses `pack-plan-stale`; a second apply of the same pack is all `same`; + an edited definition is `kept`; an item an earlier apply added and the + owner then removed is `removed`; apply raises no decision. +- Red before: `pack.apply.console` by a member without `org.hire` refuses + `right-required`, and the verb is unknown today. + +### SP5: apply and board and workflow writes for agents + +- Owns: `packages/bus/src/broker.mjs` (the agent verbs and the hire pin + check only), their tests. +- Schema change: none to files. Authority: agent verbs `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`). +- Builds: the agent apply in section 3 (no definitions, `human-required` if + the plan has one); the pin check when the chief raises a proposal + (`decision-stale`, `definition-rev-conflict`, and + `starter-pack-proposal-stale` when an inline entry's canonical hash + differs; the broker checks that hash, then passes `{inline}` on). +- Acceptance: an agent without `organization.change` refuses + `not-authorized`; an agent apply with a definition in the plan refuses + `human-required`; a hire the chief raises from a proposal carries the + broker-computed lines; a non-chief raise still refuses `role-not-chief`; + a proposal whose pinned template moved refuses `decision-stale`. +- Red before: a coder session calling `pack.apply` gets an unknown-verb + refusal today; the test expects `not-authorized`. + +### SP6: Console pack picker, board labels and workflow editor + +- Owns: `packages/webui/src/public/tasks-board.js`, a new + `packages/webui/src/public/packs.js`, the workflow view, their tests. +- Schema change: none. +- Builds: 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 + editor through `workflow.set` or the owner path. +- 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. +- Red before: a view filtered to an org with a `board` shows the fixed labels + (`tasks-board.js:21`). + +### SP7: workflow guidance in the prompt + +- Owns: `packages/harness/src/bundle.mjs` (`prompt`, line 271), its tests. +- Schema change: none to files. New prompt text, which is why it needs + sign-off. +- Builds: the read-only workflows section in section 2, for the chief, roles + holding `task.create` or `task.assign`, and roles a step names (their own + steps only). The section says workflows grant nothing and the contract and + rails win. +- Acceptance: a chief bundle for an org with a workflow contains the + section; a role no step names and without those actions gets none; the + contract text is byte-identical to before. +- Red before: a chief bundle for an org with a `change` workflow has no + workflows section. + +### SP8: saving a template + +- Owns: `packages/organization/src/template-save.mjs` (new), + `packages/bus/src/broker.mjs` (`template.save.console` only), + `packages/organization/src/org-rights.mjs`, `packages/cli/src/org.mjs` + (the `template save|list` words), `packages/webui/src/serve.mjs` (the + routes), their tests. +- Schema change: `VERB_RIGHTS` gains `'template.save.console': null` (owner + only). The saved file is a pack document read with origin `saved`; it adds + no new schema. +- Builds: capture, strip, placeholder and preview rules in section 5; the + file at `/templates//.json`, mode 0600, written + with `wx`; `list` shows only the caller's own. +- Acceptance: the file has none of the stripped keys and keeps role + credential request lines; instance names, org and project ids, the owner's + account and project roots become placeholders; the preview lists every + kept free-text field; a seeded held credential value refuses + `template-secret-suspected` naming the field path; a second save to the + same name refuses; applying a saved template goes through SP4 with the + owner check. +- 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 + +- Owns: `packages/plugins/src/kinds.mjs`, `packages/plugins/src/system-types.mjs` + (deleted), `packages/organization/src/organization-config.mjs` and + `organization.mjs` (the retired-key handling only), the system-type code in + `packages/bus/src/broker.mjs`, `packages/cli/src/org.mjs`, + `packages/plugins/channel/cli/index.mjs` and the Console files listed in + section 1, `packages/plugins/system-type/` (deleted), `docs/prd/mosaic-stack.md` + (REQ-SYS-1/2 only), their tests. +- Schema change: `system-type` stays in `KINDS` with `retired: true`. + `checkPlugins` accepts a valid list for it, the reader ignores it, and a + write that adds a name refuses `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` refuses + `verb-retired`; the read verb stays. +- Acceptance: an org file with `plugins['system-type']` loads with the key + ignored; the next commit 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. +- Red before: a fixture org holding `["coding"]` must still load once the + system-type modules are gone; dropping the kind outright would fail it + with `unknown-kind`. + +### SP10: remove the retired kind + +- Owns: `packages/plugins/src/kinds.mjs`, a print-only conversion beside + `packages/organization/src/migrate-launch.mjs`, their tests. +- Schema change: `system-type` leaves `KINDS`. +- Runs: one release after SP9, and only once no org file on the instance + names `system-type`. The slice checks that first and stops if one does. +- Acceptance: the conversion prints the SP9 fixture without the key; with + no file naming the kind, removal leaves every org file loading. +- Red before: the conversion does not exist. ## 9. Open questions for Jason +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. + 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. 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 @@ -762,8 +975,11 @@ needs the space earlier. SP10 waits at least one release after SP9. 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. Role and hire - changes stay decisions, and only the chief raises hires. + 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. 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. + 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. -- 2.54.0 From dd878cefcfbffd329b2882fa9c8bf3702bc52674 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Thu, 12 Nov 2026 14:57:58 -0600 Subject: [PATCH 4/5] docs(plans): starter packs security and re-check fixes Saved templates: owner-scoped path, file checks before read, provenance reduced to id and version, agent verbs refuse origin saved. Pack, board and workflow writes stay owner only until row 193. Board and workflow verbs, writer path, read paths and owner lists now sit in named slices. SP5 matches organization.set (decision-required). SP9 red test is red today. Kind removal moves to a follow-up row; SP10 is the conversion. --- docs/plans/2026-11-12_starter-packs-design.md | 468 ++++++++++++------ 1 file changed, 319 insertions(+), 149 deletions(-) diff --git a/docs/plans/2026-11-12_starter-packs-design.md b/docs/plans/2026-11-12_starter-packs-design.md index 7fb4edfb9..a197d479f 100644 --- a/docs/plans/2026-11-12_starter-packs-design.md +++ b/docs/plans/2026-11-12_starter-packs-design.md @@ -57,7 +57,7 @@ Status means: **exists** (built and usable by a pack as is), **partial** | Loading system types | `packages/plugins/src/system-types.mjs:5` `installedSystemTypes` | exists | Any refused module, or keys other than `description,id,label,toolsets`, throws `system-type-invalid`. | | Org enable | `plugins['system-type']` in the org file; the kind is checked by `checkPlugins` (`packages/plugins/src/kinds.mjs:146-148`, `unknown-kind`, called at `packages/organization/src/organization-config.mjs:358`) and each module at `organization-config.mjs:360-365`; fenced by `systemTypesFence`, `packages/organization/src/organization.mjs:126-131` (`organization-system-types-human-only`) | exists | Read back by `system-types.current`, `packages/bus/src/runtime.mjs:904`. Console `GET/POST /api/system-types` (`packages/webui/src/system-types-http.mjs:4-9`), right `org.setup` (`packages/organization/src/org-rights.mjs:90`). | | Project selection | `project.system-types[.set]`, `packages/bus/src/broker.mjs:8476-8495`; table `project_system_types` in `packages/bus/src/migrations/3t-3u.sql` (append-only) | exists | Human only. CLI `mosaic project system-types` and `system-types-set`, `packages/cli/src/project.mjs:17-18`. | -| Other system-type surfaces | CLI `mosaic org system-types installed\|enabled` (`packages/cli/src/org.mjs:5,15,49,51`); channel verbs (`packages/plugins/channel/cli/index.mjs:9-10`); Console page `packages/webui/src/public/system-types.js` with hooks in `workspace.js:173,193-194`, `chat.js:623`, `serve.mjs:23,85-86,826`, `webui/src/cli.mjs:9,193`; refusal `system-type-not-enabled` (`packages/bus/src/refusals.mjs:183`) | exists | All label plumbing; none affects an agent. | +| Other system-type surfaces | CLI `mosaic org system-types installed\|enabled` (`packages/cli/src/org.mjs:5,15,49,51`); channel verbs (`packages/plugins/channel/cli/index.mjs:9-10`); Console page `packages/webui/src/public/system-types.js` with hooks in `workspace.js:173,193-194`, `chat.js:623`, `serve.mjs:23,85-86,826`, `webui/src/cli.mjs:9,193`; refusal `system-type-not-enabled` (`packages/bus/src/refusals.mjs:183`); bus reads `system-types.installed` and `system-types.current` with the `installedSystemTypes` import (`packages/bus/src/runtime.mjs:30,903-904`); right `'system-types.console': 'org.setup'` (`packages/organization/src/org-rights.mjs:90`); the `project` dispatch list (`packages/cli/src/cli.mjs:781`) | exists | All label plumbing; none affects an agent. | | System type effect on agents | launcher, harness, `packages/organization/src/resolve.mjs` | missing | No launch path reads system types (no hits in those files). Every module's `toolsets` is empty. | | Shipped role templates | `roles/*.json` + `.md` | exists | V2 roles: assistant, chief-executive-officer, chief-financial-officer, chief-information-officer, chief-information-security-officer, chief-marketing-officer, chief-of-staff, coder, companion, continuity, cto, fact-checker, ideas, merge-gate, pm, proofreader, research, reviewer, security-reviewer. `researcher.json` is V1 and refused by `readTemplate`. | | Template identity | `packages/organization/src/definitions.mjs:174,180` `templateHash`, `templateHashes` | exists | Name plus sha256. | @@ -117,10 +117,27 @@ plug-in: the instance owner does it at the host (`installPlugin`, R10), under the same host-authority warning. `checkPack` guards what a pack copies into an organization; it does not contain the code that produced it. Someone who wants to share a pack without that trust shares a saved template file, which -is data only. A -saved template is data only. It is read with `parseJsonFileText` -(`packages/organization/src/util.mjs:66`), never imported or run, capped at -256 KiB, and passes `checkPack` with origin `saved`. +is data only. + +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 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 + mode wider than 0600; the account directory is created 0700; +- the size is checked before the read and refuses above 256 KiB; +- the parsed document passes `checkPack` with origin `saved`. + +Saved templates are visible only to their saver. The `packs` reads list +shipped and installed packs only; saved ones appear only through +`template list`, which is owner only and lists the caller's own. The agent +verbs refuse origin `saved` with `human-required`, so an agent in one +organization can never preview the owner's role text from another. The origin is set by the code that read the document, from where it was read: `shipped` for a core module, `installed` for a PL1 module, `saved` for a @@ -183,7 +200,8 @@ Pack definitions carry no `rev`. Apply sets `rev: 1` before refuses with `starter-pack-invalid` unless all of these hold: - Exact top-level keys as above. Origin `saved` also allows one more key, - `packs` (provenance text, section 5). Nothing else differs by origin except + `packs`, a list of at most 32 `{id, version}` entries (`id` matching + `NAME`, `version` semver, no other key). Nothing else differs by origin except the `from` rule below. `scope` is `org` or `project`. - `id` matches `NAME` (`packages/plugins/src/kinds.mjs:20`). `version` is a semver string. @@ -276,10 +294,12 @@ and authorize every action against that session's own effective role and project sandbox, as every bus verb does today. A step's `roles` names are labels for planning, never authority. -Workflow text reaches other agents' prompts, so its writers matter. Only a -person with `org.hire` or a holder of `organization.change` writes it, and -the chief can already message any agent, so this opens no new channel. The -text still gets the held-credential scan and a size cap (20 steps, titles up to 120 +Workflow text reaches other agents' prompts, so its writers matter. Only +the owner (a person with `org.hire` once row 193 lands) or a holder of +`organization.change` writes it. The chief can already message any agent, +so a workflow is a standing copy of text the chief could send anyway, but +it persists in every matching prompt. That is why it renders as quoted +data under a fixed heading, below the contract (SP7). The text still gets the held-credential scan and a size cap (20 steps, titles up to 120 characters, description up to 1,000). ### Versioning @@ -350,15 +370,22 @@ 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]` | -| Console route | `GET /api/packs`, `POST /api/orgs//packs/preview`, `POST /api/orgs//packs/apply` | -| Bus ops | `packs` (read), `pack.preview.console`, `pack.apply.console`; and for agents, `pack.preview` and `pack.apply` for a role whose file lists `organization.change` | -| `VERB_RIGHTS` | `packs` to `board.read`; `pack.preview.console` and `pack.apply.console` to `org.hire`, plus an owner check when the pack's origin is `saved` | +| 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) | -`org.hire` fits because the only parts of a pack that could ever matter for -capability are role definitions, and T1 already gives definition writes to -`org.hire`. Board and workflows carry no capability. A saved pack applies -only to an organization its saver owns (section 5). +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 +bus still lets only the organization's one human through until row 193 +(`broker.mjs:6303-6308`, refusing `human-required`), and a non-owner +member's Console session reaches only the four hire verbs +(`broker.mjs:6221`). The pack verbs follow the same rule, so this design +widens no human boundary. When row 193 opens definition writes to +`org.hire` holders, these four verbs move to `org.hire` in the same change. +Board and workflow writes go through the same writer op as apply +(`applyPack`, with only the board and workflow keys in the plan), so they +share its key fence. ### The chief applying a pack @@ -367,7 +394,10 @@ decision (section 5). The agent `pack.apply` respects that. Who may call it: any role whose file lists `organization.change`, the same test `organization.set` uses (`broker.mjs:6248-6255,6321`). The shipped chief -holds it. The chief could hire another agent holding it as a plain hire, +holds it. A role without it gets what `organization.set` gives it today: +`decision-required` (`#classification`, `broker.mjs:4743`; asserted in +`packages/bus/tests/chief-of-staff.test.mjs:255-261`), so the existing +decision path stays and no new gate appears. The chief could hire another agent holding it as a plain hire, since step 7 does not flag a line the chief already holds; that agent could then change boards and workflows too, which carry no capability. "The chief" in section 4 means any holder of `organization.change`. @@ -377,6 +407,10 @@ What it does: - it writes `board` and `workflows` directly under `organization.change`, the same way `organization.set` writes limits today (`broker.mjs:6247-6340`), through `applyPack`; +- it refuses a pack of origin `saved` with `human-required` (section 2); +- a workflow step that names a definition this apply cannot write is left + out, and preview marks that workflow, so an agent apply never leaves a + step naming a role that does not exist; - it writes no definitions. Each proposed hire is raised by the chief (only the chief raises, `role-not-chief`). For a template-reference definition the role is `{template, sha256}`, which the `launch.noHire` check reads @@ -391,7 +425,11 @@ What it does: 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`. + 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. ### A second pack, and re-applying @@ -411,8 +449,9 @@ Items merge by name and nothing is overwritten: | Proposed hire | an instance or open hire decision by that name | dropped from the new proposals | `removed` keeps a deliberate removal from coming back on its own: the -earlier `packs` entries act as the tombstone. The person may bring the item -back on purpose with `--as` and a new name, which the preview shows as an +earlier `packs` entries act as the tombstone. The tombstone is per pack id: +pack B naming an item pack A added and the owner removed is a plain `add`. +The person may bring the item back on purpose with `--as` and a new name, which the preview shows as an `add`. The same rule covers a board the organization removed after this pack added it. @@ -441,13 +480,13 @@ and change decisions stay chief-only. | Change | A person does it directly | The chief does it | The limit at that step | |---|---|---|---| -| Import a role template | `definition.import` (T1, `org.hire`) | Inside a hire or change decision, role `{template, sha256}` | `checkComposition`. Nothing runs until a hire. | +| Import a role template | `definition.import` (T1; `org.hire` in `VERB_RIGHTS`, owner at the bus until row 193) | Inside a hire or change decision, role `{template, sha256}` | `checkComposition`. Nothing runs until a hire. | | Customize or invent a role | `definition.edit` or `invent` (T1) | Inside a hire or change decision, role `{inline}` | `checkComposition`; lines above the chief are flagged and only the owner approves them (R8). Editing a definition in use refuses, so a running agent never widens without a change decision. | | 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` (`org.hire`) | `pack.apply` under `organization.change`, no definitions | As above for every hire it later proposes. | +| 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` (`org.hire`) | `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. | -| Change board labels or order | `board.set.console` (`org.hire`) | `board.set` under `organization.change` | The five states stay. | +| 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. | +| 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. | | Install a pack plug-in | `installPlugin` (instance owner, R10) | Not available | Host authority. | @@ -504,7 +543,11 @@ the exact JSON that will be written; save takes its digest. the chief or the assistant, which every organization already has. - `board` and `workflows`. - `skills`: the union of the captured roles' `skills`. -- `packs`: the ids and versions applied before, as provenance text. +- `packs`: `{id, version}` for each pack applied before, and nothing else. + The org's own entries carry `by`, `project`, `at`, `added` and + `proposedHires`; none of those is copied. Applying the template records + one new entry in the target organization's `packs` with origin `saved`; + the template's own `packs` list is not copied into it. The result is a pack document read later with origin `saved` (section 2). A role in a saved template may lack `from` (an invented role) and the document @@ -562,8 +605,9 @@ says, which is why save is owner only and shows the exact JSON first. --from FILE`: the base organization with human, chief and assistant, as today. 2. `mosaic org pack apply ID saved: --digest D`: the same preview and - apply as a shipped pack. A saved pack applies only to an organization the - caller owns. + apply as a shipped pack. `saved:` resolves only under the caller's + own account directory (section 2), and the caller must own the target + organization. 3. The chief raises the proposed hires. There is no create-time shortcut that skips the hire decisions. @@ -594,10 +638,13 @@ reasons: 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 later slice removes the kind once no org file on the instance names - it. A print-only conversion in the style of `migrateLaunch` + 2. A print-only conversion in the style of `migrateLaunch` (`packages/organization/src/migrate-launch.mjs`, decision 145) covers a - file that was never written in between. + 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 @@ -605,8 +652,10 @@ reasons: - **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`, and `installedSystemTypes`, each with its tests - (section 1 lists every file). The five modules move to + `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/`. ### REQ-SYS-1 and REQ-SYS-2 @@ -729,8 +778,9 @@ 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. They are written so that one go from Jason covers all -of them; nothing in a slice waits on a further design choice. +the full suites green. One go from Jason covers all ten. 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 @@ -738,47 +788,66 @@ named below, and every change to one is listed in its slice. The changes that need sign-off are these: - a new plug-in kind and a new `switch` value (SP1); -- new org file keys (SP3); +- 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 in `CONSOLE_VERBS` (`packages/bus/src/broker.mjs:181`) (SP4, SP8); -- an agent authority surface for `organization.change` holders (SP5); + and `CONSOLE_VERBS` (`packages/bus/src/broker.mjs:181`): `packs`, + `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; +- agent verbs `packs`, `pack.preview`, `pack.apply`, `board.set` and + `workflow.set` under `organization.change` (SP5); - new text in agents' generated prompts (SP7); -- data leaving an organization (SP8); -- a PRD wording change and a retired kind (SP9). +- data leaving an organization into a file (SP8); +- a PRD wording change, a retired kind and a changed fence (SP9). New refusal codes are added to `packages/bus/src/refusals.mjs` by the slice -that first uses them: `starter-pack-invalid`, -`starter-pack-template-changed`, `pack-plan-stale`, -`starter-pack-proposal-stale`, `template-secret-suspected` and -`verb-retired`. +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). -**Owner boundary.** "Owns" lists the files a slice may change. A slice that -needs a change outside its list stops and reports; it does not widen -itself. Tests for a slice live beside the files it owns. +**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 +lines. A slice that needs a change outside its list stops and reports; it +does not widen itself. Tests for a slice live beside the files it owns. ### SP1: the pack kind and its checker - Owns: `packages/plugins/src/starter-pack.mjs` (new), - `packages/plugins/src/kinds.mjs`, `packages/plugins/src/loader.mjs` (only - if the listing needs a hook), `packages/bus/src/refusals.mjs`, - `packages/plugins/tests/starter-pack.test.mjs` (new). + `packages/plugins/src/kinds.mjs`, `packages/bus/src/refusals.mjs` (the + three SP1 codes), `packages/plugins/tests/starter-pack.test.mjs` (new). - Schema change: `KINDS` gains `"starter-pack"` with `interfaceVersion: 1`, `exports: ["starterPack"]` and a new `switch: "none"`. `namesFor` refuses any setting for a `none` kind with `bad-setting`, so an org file can never - switch a pack on through `plugins`. + switch a pack on through `plugins`. This touches nothing else: `switch` is + read only in `namesFor` (`kinds.mjs:123,128`), and the loader starts each + kind at `names = []` and calls `namesFor` only when the key is present + (`loader.mjs:190-195`), so a `none` kind loads no names and `loader.mjs` + needs no change. - Builds: `checkPack(doc, {origin})` and `narrowsTemplate(role, template)` as section 2 states; `installedStarterPacks({dataRoot})`, which lists shipped and installed packs through `loadInstalled` and sets the origin; - `readSavedTemplate(path)` through `parseJsonFileText` with the 256 KiB cap. + `readSavedTemplate({configDir, account, name})` with every file check in + section 2 (names 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 forbidden key, a stale template hash, a role wider than its template, a credential line wider than its template, a root hire, a ninth hire, a hire naming no pack definition, a bad state, a duplicate column. A shipped or installed pack with a `packs` key or a role without `from` refuses; a - saved one with either passes. `plugins: {"starter-pack": ["coding"]}` - refuses `bad-setting`. -- Red before: a pack whose definition carries `copyBindings` passes (there - is no `checkPack`, so nothing refuses it). + 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`. +- 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` + (today `checkPlugins` refuses `unknown-kind`, `kinds.mjs:146-148`, so the + assertion fails). ### SP2: the five shipped packs @@ -799,7 +868,8 @@ itself. Tests for a slice live beside the files it owns. ### SP3: org file keys and the writer op - Owns: `packages/organization/src/organization-config.mjs`, - `packages/organization/src/organization.mjs`, their tests. + `packages/organization/src/organization.mjs` (`applyPack` only), their + tests. - Schema change: `TOP_KEYS` (`organization-config.mjs:32`) gains `packs`, `board` and `workflows`. The project entry keys (`organization-config.mjs:306`) gain `board` and `workflows`. Shapes, each refusing unknown keys: @@ -812,151 +882,250 @@ itself. Tests for a slice live beside the files it owns. org definition, a V2 template or `human`; - `packs`: a list of `{id, version, sha256, origin, scope, project, at, by, added, proposedHires}`, origin one of `shipped`, `installed`, `saved`. -- Builds: `applyPack(id, plan, {revision, actor, assertClean})` on the writer - with the rules in section 3: one commit, `rev: 1` on each added - definition, `checkDefinition` with `assertClean`, `definition-in-use` as - `setDefinition` refuses it, `human-required` when an agent actor writes a - definition, and a refusal for any key outside its list. -- Acceptance: valid shapes load and each invalid one refuses; `applyPack` +- Builds: + - the reader keeps `packs`, `board` and `workflows` on the loaded + organization object, and `board` and `workflows` on each `projects[id]`. + Today the copy after `organization-config.mjs:306` keeps only `root`, + `repo`, `remote` and `git`, so widening the key list alone would load + the keys and then lose them; + - `applyPack(id, plan, {revision, actor, assertClean})` on the writer with + the rules in section 3: one commit, `rev: 1` on each added definition, + `checkDefinition` with `assertClean`, `definition-in-use` as + `setDefinition` refuses it, `human-required` when an agent actor writes + a definition. Plan keys are limited to `definitions`, `board`, + `workflows`, `projects.

.board`, `projects.

.workflows` and + `packs`; any other refuses. + - A board write or a workflow write is an `applyPack` call whose plan + holds only that key and no `packs` entry. The generic `write()` cannot + carry it, because it refuses any change under `projects` + (`projectsProtected`, `organization.mjs:383-391`). +- Acceptance: valid shapes load and each invalid one refuses; the loaded + object and each `projects[id]` keep `board` and `workflows`; `applyPack` 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`. + refuses `human-required`; a plan holding `plugins`, `roles`, `limits` or + `credentials` refuses; a board-only plan changes only `board`. - 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 and apply for people +### SP4: preview, apply, board and workflow writes for people -- Owns: `packages/bus/src/broker.mjs` (the new verbs only), +- Owns: `packages/bus/src/broker.mjs` (the six SP4 verbs only), + `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`), `packages/organization/src/org-rights.mjs`, `packages/cli/src/org.mjs` - (the `pack` words), `packages/webui/src/serve.mjs` (the two routes), their - tests. + (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'`, - `'pack.preview.console': 'org.hire'` and `'pack.apply.console': 'org.hire'`. - `CONSOLE_VERBS` gains the two console verbs. A saved-origin pack also - needs the caller to be the owner. -- Builds: `mosaic org pack list|preview|apply`, `POST /api/orgs//packs/preview` - and `POST /api/orgs//packs/apply`, and the merge table in section 3, - including `kept`, `--as` and the `removed` tombstone. + `'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. +- 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/`; + 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. + Board and workflow writes call `applyPack` with a one-key plan (SP3). - Acceptance: preview writes nothing and lists flagged lines, credential lines and `noHire` marks; a pack string holding a held credential value refuses at preview and at apply; apply writes only `add` items and the `packs` entry with `added` and pinned `proposedHires`; a stale digest refuses `pack-plan-stale`; a second apply of the same pack is all `same`; an edited definition is `kept`; an item an earlier apply added and the - owner then removed is `removed`; apply raises no decision. -- Red before: `pack.apply.console` by a member without `org.hire` refuses - `right-required`, and the verb is unknown today. + owner then removed is `removed`, while a different pack naming it adds + it; apply raises no decision; a non-owner member's `pack.apply.console`, + `board.set.console` and `workflow.set.console` refuse `human-required`; + 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. +- 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. -### SP5: apply and board and workflow writes for agents +### SP5: the same writes for agents, and the hire pin check -- Owns: `packages/bus/src/broker.mjs` (the agent verbs and the hire pin - check only), their tests. -- Schema change: none to files. Authority: agent verbs `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`). -- Builds: the agent apply in section 3 (no definitions, `human-required` if - the plan has one); the pin check when the chief raises a proposal - (`decision-stale`, `definition-rev-conflict`, and - `starter-pack-proposal-stale` when an inline entry's canonical hash - differs; the broker checks that hash, then passes `{inline}` on). -- Acceptance: an agent without `organization.change` refuses - `not-authorized`; an agent apply with a definition in the plan refuses - `human-required`; a hire the chief raises from a proposal carries the - broker-computed lines; a non-chief raise still refuses `role-not-chief`; - a proposal whose pinned template moved refuses `decision-stale`. -- Red before: a coder session calling `pack.apply` gets an unknown-verb - refusal today; the test expects `not-authorized`. +- Owns: `packages/bus/src/broker.mjs` (the five agent verbs and the hire + 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`, + `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). +- 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 + `organization.set`; the chief's `board.set` and `workflow.set` write; an + agent apply with a definition in the plan refuses `human-required`; an + agent naming `saved:x` refuses `human-required`; a hire the chief raises + from a proposal carries the broker-computed lines; a non-chief raise + still refuses `role-not-chief`; a proposal whose pinned template moved + 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`. +- 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. ### SP6: Console pack picker, board labels and workflow editor - Owns: `packages/webui/src/public/tasks-board.js`, a new - `packages/webui/src/public/packs.js`, the workflow view, their tests. -- Schema change: none. -- Builds: 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 - editor through `workflow.set` or the owner path. + `packages/webui/src/public/packs.js`, a new + `packages/webui/src/public/workflows.js`, `packages/webui/src/public/workspace.js` + (the page hooks only), `packages/webui/src/workspace-read.mjs` (the + `board` field only), `packages/webui/src/packs-http.mjs` (the workflow + read route only), their tests. +- Schema change: none. The workspace read gains a `board` field per org. +- Builds: the server side first. `readOrg` already holds the org document + (`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 + 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 + `workflow.set.console`. - 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. + pack default; a label or workflow title holding `` + renders as text (`esc()`, `tasks-board.js:108-113`). - Red before: a view filtered to an org with a `board` shows the fixed labels (`tasks-board.js:21`). ### SP7: workflow guidance in the prompt -- Owns: `packages/harness/src/bundle.mjs` (`prompt`, line 271), its tests. +- Owns: `packages/organization/src/resolve.mjs` (`resolveInstance`, the + `workflows` field only), `packages/harness/src/bundle.mjs` (`prompt`, + line 271), their tests. - Schema change: none to files. New prompt text, which is why it needs sign-off. -- Builds: the read-only workflows section in section 2, for the chief, roles - holding `task.create` or `task.assign`, and roles a step names (their own - steps only). The section says workflows grant nothing and the contract and - rails win. +- Builds: `resolveInstance` puts `workflows` on `resolved`: the org's, plus + the project's when it is given a project, the project's winning by name. + The launcher calls it with no project today (`cli/src/launcher.mjs:401`), + so this go delivers org workflows to prompts, and project workflows reach + a prompt only once a launch names its project; this slice does not change + the launcher. `prompt` renders the read-only workflows section in section + 2 for the chief, roles holding `task.create` or `task.assign`, and roles a + step names (their own steps only). Titles and descriptions appear as + quoted data under a fixed heading, never as headings of their own. The + section says workflows grant nothing and the contract and rails win. - Acceptance: a chief bundle for an org with a workflow contains the - section; a role no step names and without those actions gets none; the - contract text is byte-identical to before. + section; a role no step names and without those actions gets none; a + named role sees only its own steps; a description holding + `## Contract` stays inside the quoted block; the contract text is + byte-identical to before. - Red before: a chief bundle for an org with a `change` workflow has no workflows section. ### SP8: saving a template - Owns: `packages/organization/src/template-save.mjs` (new), - `packages/bus/src/broker.mjs` (`template.save.console` only), + `packages/bus/src/broker.mjs` (the two template verbs 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` - (the `template save|list` words), `packages/webui/src/serve.mjs` (the - routes), their tests. -- Schema change: `VERB_RIGHTS` gains `'template.save.console': null` (owner - only). The saved file is a pack document read with origin `saved`; it adds - no new schema. + (the `template save|list` words), `packages/webui/src/template-http.mjs` + (new), `packages/webui/src/serve.mjs` (route hookup only), + `packages/webui/src/cli.mjs` (caller wiring only), their tests. +- Schema change: `VERB_RIGHTS` gains `'template.save.console': null` and + `'template.list.console': null` (owner only); `CONSOLE_VERBS` gains both. + The saved file is a pack document read with origin `saved`; it adds no + new schema. - Builds: capture, strip, placeholder and preview rules in section 5; the - file at `/templates//.json`, mode 0600, written - with `wx`; `list` shows only the caller's own. + 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 + 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 credential request lines; instance names, org and project ids, the owner's account and project roots become placeholders; the preview lists every kept free-text field; a seeded held credential value refuses `template-secret-suspected` naming the field path; a second save to the - same name refuses; applying a saved template goes through SP4 with the - owner check. + 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. - 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 -- Owns: `packages/plugins/src/kinds.mjs`, `packages/plugins/src/system-types.mjs` - (deleted), `packages/organization/src/organization-config.mjs` and - `organization.mjs` (the retired-key handling only), the system-type code in - `packages/bus/src/broker.mjs`, `packages/cli/src/org.mjs`, - `packages/plugins/channel/cli/index.mjs` and the Console files listed in - section 1, `packages/plugins/system-type/` (deleted), `docs/prd/mosaic-stack.md` - (REQ-SYS-1/2 only), their tests. +- 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/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/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 a valid list for it, the reader ignores it, and a - write that adds a name refuses `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` refuses - `verb-retired`; the read verb stays. + `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 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. -- Red before: a fixture org holding `["coding"]` must still load once the - system-type modules are gone; dropping the kind outright would fail it - with `unknown-kind`. + 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). -### SP10: remove the retired kind +### SP10: the print-only conversion -- Owns: `packages/plugins/src/kinds.mjs`, a print-only conversion beside - `packages/organization/src/migrate-launch.mjs`, their tests. -- Schema change: `system-type` leaves `KINDS`. -- Runs: one release after SP9, and only once no org file on the instance - names `system-type`. The slice checks that first and stops if one does. -- Acceptance: the conversion prints the SP9 fixture without the key; with - no file naming the kind, removal leaves every org file loading. +- 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. +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. + ## 9. Open questions for Jason Each answer changes what gets built. The slices above follow the @@ -974,7 +1143,8 @@ recommendation; a different answer changes the slice named. `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. Role and hire + 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. -- 2.54.0 From c0a60ed3ae936a7b6c1acb265bda478161f7da28 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Thu, 12 Nov 2026 15:18:16 -0600 Subject: [PATCH 5/5] 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. -- 2.54.0