# Managed session config: skills, model routing, unslop (2026-10-11) Status: written by Sage, lead. It follows `docs/plans/BRIEF-TEMPLATE.md`. Jason ruled Q82, Q83 and Q84 option A, relayed by Mos. All three rows change what a managed session (the row 41 bundle, what `mosaic talk` starts) is given at launch, and all three touch `packages/harness/src/bundle.mjs` and the two adapters. They run one at a time in the order below, each after the one before it is done, so no two candidates edit the same files at once. Sage owns all three. The order is after rows 55, 42 and 49. Two decisions shape the set. Q84 is its own row, not part of the skills row: the ms-unslop skill is guidance the model reads, and the hook is a check that runs on its output. They differ in files, failure mode and tests. Row 29 (console model switching) stays parked. It is a WebUI feature. When Jason reopens it, it reads the routing table this set adds. Constraints that hold for all three rows: - Fail closed. A launch that can't give a session what its role declares refuses, with a refusal code and a message naming the missing piece. Nothing is silently dropped or defaulted. - `--restricted` stays on the Claude Code adapter, and no harness reads a default config location (`~/.pi`, `~/.claude`) for skills, settings, extensions or hooks. Every file a session loads is in its bundle or named in its manifest with a digest. - Run records stay write-once. Anything a session writes for later reading goes under its run directory. ## Managed sessions get their role's skills on both harnesses ### Problem ROADMAP M17 says role contracts declare skill names and the runner passes them explicitly. Only the older path does it: `scripts/agent.sh:173-184` reads a seat's `skills`, checks each is in `/skills-enabled` and refuses if not. Managed sessions get none. `packages/harness/src/bundle.mjs:147` writes `skills: []`, `packages/harness/src/runner.mjs` sets no `MOSAIC_SKILLS`, so `adapters/pi/adapter.sh:52` runs Pi with `--no-skills`, and `adapters/claude/adapter.sh` passes `--disable-slash-commands`. Role files have no place for a skill list (`packages/business/src/role.mjs:19`). Today `skills-enabled` holds only `ms-communications`; `ms-tools` is installed but not enabled. ### Owner and reviewer - Owner: Sage. - Reviewers: Darkwing and Filbert. ### Files owned - `packages/business/src/role.mjs`, `packages/business/src/resolve.mjs` and their tests - `roles/coder.json`, `roles/cto.json`, `roles/pm.json`, `roles/reviewer.json` (the `skills` key only) - `packages/harness/src/bundle.mjs`, `packages/harness/src/runner.mjs`, `packages/harness/tests/`, `packages/harness/README.md` - `packages/cli/src/launcher.mjs` and its tests (the refusal only) - `adapters/pi/adapter.sh`, `adapters/claude/adapter.sh`, `adapters/README.md` - `agents/sage/work/queue-/` ### What ships - Role files take an optional `skills` list: distinct names matching `[A-Za-z0-9._-]+`. A role without the key has no skills. The role version stays 2; the key is optional. - Resolution carries the role's skill list. The bundle resolves each name against `/skills-enabled//SKILL.md`. A name that isn't enabled refuses the launch with `skill-not-enabled`, naming the skill and `scripts/skill.sh activate `. - The bundle copies each resolved skill directory into the bundle, so a deactivate during the session doesn't change it. The manifest's `skills` lists each name, its bundle path and the sha256 of its `SKILL.md`. - Pi gets the bundle copies through `MOSAIC_SKILLS`, which the adapter already turns into `--skill` flags. Claude Code gets the same skills and no others. Probe the way in first (a bundle-local plugin directory, or the skill text in the generated system prompt are the two candidates) with `--restricted` kept, and record the choice and its evidence in the packet and in `adapters/README.md`. - First skill lists. `ms-unslop` is on every list because Jason requires it on all output. `ms-tools` is on none: it documents the conductor's repository scripts, which a managed session can't run. | Role | Skills | |---|---| | coder | ms-unslop, ms-sdlc, ms-file-read, ms-file-write | | reviewer | ms-unslop, ms-sdlc, ms-file-read | | cto | ms-unslop, ms-sdlc, ms-archify, ms-grill-me | | pm | ms-unslop, ms-goal, ms-communications | - Tests, each on both harnesses with the existing fake binaries: a session gets exactly its contracted skills (the argv or prompt the fake records lists those names and no others); a contracted skill that isn't enabled refuses the launch before any spawn; an enabled skill that isn't on the role's list is absent; a role with no list runs Pi with `--no-skills` and gives Claude Code none. - Gate suites: `node --test 'packages/business/tests/*.test.mjs'`, `node --test 'packages/harness/tests/*.test.mjs'`, `node --test 'packages/cli/tests/*.test.mjs'`, and every `scripts/test-*.sh`. ### Out of scope Per-instance or per-business skill lists (role files only for now). The `mosaic skill` CLI verbs (`scripts/skill.sh` stays the lifecycle tool). Enabling skills in the live data root: that is a landing step, below. ### Gate Darkwing and Filbert approve on the issue, naming the candidate manifest. The suites above pass on a gate rerun in a clean worktree. At landing, Sage installs and activates the skills the four lists name, then starts one managed coder session on each harness and attaches the manifest showing the four skills and their digests. ## Model routing table: sessions resolve harness, model and thinking from one table ### Problem A session's harness, model and thinking come from free-string agent-layer vars (`packages/business/src/vars.mjs:106-108`) with the system's `execution.adapter` and `execution.model` as fallback (`packages/harness/src/bundle.mjs:51-55`). The launch cap guesses a model's family by substring (`packages/seat/src/session.mjs:51`, used at `packages/cli/src/launcher.mjs:148`), so a model name that contains neither `opus` nor `sonnet`, or both, refuses for the wrong reason, and a renamed model can move between families unnoticed. `thinking` is recorded in the manifest and applied to neither harness (`packages/harness/README.md:188-190`). The live business file `mosaic-stack.json` sets no harness, model or thinking for any role, and the system config has no `execution.adapter`, so a managed launch there refuses today with `unsupported-harness`. The row 41 recorded run used a test business with `claude-code` and `claude-sonnet-5-5`. ### Owner and reviewer - Owner: Sage. - Reviewers: Darkwing and Dewey. ### Files owned - `packages/business/src/business.mjs`, `vars.mjs`, `vocabulary.mjs`, `index.mjs` and their tests, `packages/business/README.md` - `packages/harness/src/bundle.mjs`, `runner.mjs`, tests, README - `packages/seat/src/session.mjs` and its tests (the `family` match goes) - `packages/cli/src/launcher.mjs` and its tests - `adapters/pi/adapter.sh`, `adapters/claude/adapter.sh`, `adapters/README.md` - test business fixtures that set `harness`, `model` or `thinking` - `agents/sage/work/queue-/` ### What ships - A top-level `routing` table in the business file, keyed by role instance name (the keys of `roles`). Each entry has `harness` (`pi` or `claude-code`), `model`, `family` (a key of `LAUNCH_CEILING`), `thinking` (the existing six levels), and `fallback`: null, or an object with the same four keys. Unknown keys refuse. A `launch.instances` or `launch.by` name with no entry refuses the business file at load. - One source. The agent-layer `harness`, `model` and `thinking` vars are removed, and session resolution no longer reads `execution.adapter` or `execution.model`. Those system keys stay for `scripts/run-task.sh` workers, which this row doesn't touch. A business file that still sets the removed vars refuses with a message pointing at `routing`. - Resolution. A launch for an instance with no `routing` entry refuses with `no-route`. The fallback is used only when the entry states one, and only at launch, when the primary harness fails its preflight (binary missing, or credential status not ok). Never mid-session. The manifest records which entry was used (`primary` or `fallback`) and why. - The launch cap counts sessions by the `family` of the entry each was launched under. `family()` in `session.mjs` goes. `launch.max` keeps its meaning. - `thinking` reaches both harnesses. Pi gets its thinking flag. Claude Code gets its effort or thinking setting. Probe the exact flag or setting on the pinned versions first and record it in the packet and `adapters/README.md`. The README line saying thinking isn't applied goes. - First table. The live business file sets nothing today, so this table starts from the row 41 recorded run and the R26 caps. Every fallback is null: a pi fallback on `zai/glm-5.3-flash` would need a `glm` family ceiling, which is a separate ruling. | Role | Harness | Model | Family | Thinking | Fallback | |---|---|---|---|---|---| | coder | claude-code | claude-sonnet-5-5 | sonnet | medium | none | | reviewer | claude-code | claude-opus-5-5 | opus | high | none | | cto | claude-code | claude-opus-5-5 | opus | high | none | | pm | claude-code | claude-opus-5-5 | opus | high | none | - Tests: a session resolves harness, model and thinking from its table entry; an unknown role or a missing entry refuses the launch; a stated fallback is used when the primary's preflight fails and an unstated one never is; the cap counts by entry family, so a model name holding `opus` launched under a `sonnet` entry counts as sonnet; thinking reaches the Pi and Claude Code fake adapters as the recorded flag. - Gate suites: `node --test 'packages/business/tests/*.test.mjs'`, `node --test 'packages/harness/tests/*.test.mjs'`, `node --test 'packages/seat/tests/*.test.mjs'`, `node --test 'packages/cli/tests/*.test.mjs'`, and every `scripts/test-*.sh`. ### Out of scope The provider and profile registry in `packages/mosaic` stays parked; it comes with CLIProxyAPI. Model switching in a running session and the console UI for it (row 29, parked). `scripts/run-task.sh` worker model selection. ### Gate Darkwing and Dewey approve on the issue, naming the candidate manifest. The suites above pass on a gate rerun in a clean worktree. At landing, Sage writes the first table into `mosaic-stack.json` and starts one managed coder session, attaching the manifest that shows the entry used, the family counted and the thinking flag passed. ## Managed sessions run the unslop check on both harnesses ### Problem `tools/unslop-hook` checks assistant replies against `lists.json`, but only as a Pi extension loaded by hand (`tools/unslop-hook/README.md`). Managed sessions don't load it: the runner passes Pi only the bundle's own extension (`packages/harness/src/runner.mjs:223`), and the Claude Code bundle has one hook, the PreToolUse gate (`packages/harness/src/bundle.mjs:125-127`). A managed session can drift into the tells the ms-unslop skill bans and nothing notices. ### Owner and reviewer - Owner: Sage. - Reviewers: Dewey and Filbert. ### Files owned - `tools/unslop-hook/` (a Claude Code hook script beside the extension; the checker and `lists.json` change only if a test needs it) - `packages/harness/src/bundle.mjs`, `runner.mjs`, tests, README - `adapters/README.md` - `agents/sage/work/queue-/` ### What ships - Pi. The runner adds `tools/unslop-hook/extension.ts` to `MOSAIC_EXTENSIONS` and points `MOSAIC_UNSLOP_LOG` at `/session/unslop.jsonl`. - Claude Code. The bundle's `settings.json` gains a hook on reply end that checks the last assistant text with `unslop-check.js` and logs the result, and a hook on the next prompt that adds the correction notice when the last reply was flagged. Same notify-then-correct behaviour as the Pi extension. Neither hook blocks a reply or a turn. - The operator is told about a flagged reply on the channel `mosaic talk` already shows session events on. Find that channel and name it in the packet; a notice only in a log file doesn't meet this. - One `lists.json` drives both. The manifest records the extension, the hook script and `lists.json` with digests. - Fail closed, as the Pi extension does: a broken `lists.json` stops the checks, logs `broken_lists`, tells the operator once, and the session keeps running. That includes the headless case the README lists as a limit (print mode notifies nobody); a managed session must reach the operator. - Counts for a later blocking decision (Q84 option B). `unslop.jsonl` in the run directory records per-turn `checked` and `flagged` events with the pattern ids hit. A test reads the session's checked and flagged counts from it after the session has ended. - Tests, on both harnesses: a reply with a known tell produces a correction notice on the next turn; a clean reply produces none; a broken `lists.json` fails closed (no clean verdict, operator told once, session alive); the counts are readable after the session ends. - Gate suites: `node tools/unslop-hook/test-unslop-check.js`, `node --test 'packages/harness/tests/*.test.mjs'`, `node --test 'packages/cli/tests/*.test.mjs'`, and every `scripts/test-*.sh`. ### Out of scope Blocking a reply (Q84 option B), which waits on the measured false-positive rate. Changes to the word lists. The ms-unslop skill itself, which the skills row delivers. ### Gate Dewey and Filbert approve on the issue, naming the candidate manifest. The suites above pass on a gate rerun in a clean worktree. At landing, Sage runs one managed session on each harness with a prompt that draws a known tell, and attaches the operator notice, the next-turn correction and the `unslop.jsonl` counts.