26 KiB
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.
Q156 (2026-10-14, lead decision 143) changed the order: row 65 runs first, then 64, then 66. Row 65 is where the harnesses become plug-in modules (PRD 1.3 REQ-PLUG-2), so 64 and 66 add their files to those modules. Row 65's brief carries the harness-kind scope (added 2026-10-14), and the row now waits on row 131, which ships the plug-in loader.
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 instance vars that row 65 makes the one source (amended 2026-10-19).
Refreshed 2026-10-19 for rows 64 and 66: their line numbers are at
origin/refactor 7319bd867, after row 65 (c216f47db) moved each harness's
adapter variables and bundle files into packages/plugins/harness/pi
and packages/plugins/harness/claude-code. Both rows now own those
module files. What ships is unchanged.
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.
--restrictedstays 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-186
reads a seat's skills, checks each is in <dataRoot>/skills-enabled
and refuses if not. Managed sessions get none.
packages/harness/src/bundle.mjs:212 writes skills: []. The runner's
adapterEnv (packages/harness/src/runner.mjs:263-278) passes each
harness module's env answer through, and neither module
(packages/plugins/harness/pi/index.mjs:53-61,
packages/plugins/harness/claude-code/index.mjs) sets MOSAIC_SKILLS.
So adapters/pi/adapter.sh:52 runs Pi with --no-skills, and
adapters/claude/adapter.sh:91 passes --disable-slash-commands. Role
files have no place for a skill list (packages/business/src/role.mjs:20,
V2_KEYS).
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.mjsand their testsroles/coder.json,roles/cto.json,roles/pm.json,roles/reviewer.json(theskillskey only)packages/harness/src/bundle.mjs,packages/harness/src/runner.mjs,packages/harness/tests/,packages/harness/README.mdpackages/plugins/harness/pi/index.mjs,packages/plugins/harness/claude-code/index.mjs(theirbundleandenv),packages/plugins/tests/harness.test.mjs, and the harness lines ofpackages/plugins/README.md. The harness kind keeps interface 1 and its export list (packages/plugins/src/kinds.mjs:52-56). If the skills need a new export, the row stops and the packet says why.packages/cli/src/launcher.mjsand its tests (the refusal only)adapters/pi/adapter.sh,adapters/claude/adapter.sh,adapters/README.mdagents/sage/work/queue-<row>/
What ships
-
Role files take an optional
skillslist: 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
<dataRoot>/skills-enabled/<name>/SKILL.md. A name that isn't enabled refuses the launch withskill-not-enabled, naming the skill andscripts/skill.sh activate <name>. -
The bundle copies each resolved skill directory into the bundle, so a deactivate during the session doesn't change it. The manifest's
skillslists each name, its bundle path and the sha256 of itsSKILL.md. -
Both harnesses get the skills as text in the generated
prompt.md, rendered from the bundle copies: each skill's files under its name, and no other skill. Pi keeps--no-skillsand Claude Code keeps--disable-slash-commandsand--restricted, so neither harness discovers a skill of its own. Sage chose this on 2026-10-19 from a probe (evidence inagents/sage/work/queue-64/probe.md, the choice inadapters/README.md):- Pi's
--skillputs only each skill's name and description in the prompt, and the model mustreadthe SKILL.md itself (docs/skills.md:67-69in the pinned pi package). The gate refuses every file-tool path outside the workspace (packages/harness/src/gate.mjs:3-4), so throughMOSAIC_SKILLSa session would see its skills and be unable to open them. Opening the gate to the bundle changes a security boundary, which this row doesn't do. - Claude Code 2.1.303 loads a bundle-local
--plugin-dirskill under--restricted, but only with theSkilltool on, and then it also offers its own bundled skills.disableBundledSkillsand askillOverridesentry for its built-inplugin-authoringcut the offer to the contracted skill, yet the debug log still counts two bundled skills loaded. That list changes with the Claude Code version, and theSkilltool would pass through the gate hook. - The cost: a role's skill text is in every turn's prompt (17 to 36 KB
for the four lists below), where the other routes load a skill when
it's used.
The two harness modules and
MOSAIC_SKILLSstay unchanged.
- Pi's
-
First skill lists.
ms-unslopis on every list because Jason requires it on all output.ms-toolsis 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-skillsand gives Claude Code none. -
Gate suites:
node --test 'packages/business/tests/*.test.mjs',node --test 'packages/harness/tests/*.test.mjs',node --test 'packages/plugins/tests/*.test.mjs',node --test 'packages/cli/tests/*.test.mjs', and everyscripts/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.
Sage is both the author and the gate owner of this row, so the candidate says so in its first line for the reviewers (mos-0855). The landing steps change the live data root and start live sessions, so they wait for Mos's go.
Model routing table: sessions resolve harness, model and thinking from one table
Amended 2026-10-19 by Sage before the build started (lead decision 151).
Rows 122 and 165 landed after this section was written and built most of
the table it asked for. launch.models maps a key to a provider and a
model. Each role instance's agent vars name a harness, a model (a
launch.models key) and a thinking level. The mosaic CLI and the
Console edit them, and the launch limits count exact provider and model,
so family() and the family cap are already gone. The live file sets
all three vars on every instance. A second routing table would hold
the same data twice, so the vars stay the one source and this row adds
what is still missing: no execution fallback, a fallback route, thinking
applied on both harnesses, and the harness plug-in kind. The first
version of this section is blob 8872802c in git.
Mos's reading of Q83 option A (mos-0840, 2026-10-19) keeps this
amendment on one condition, which keeps A apart from the rejected option
C: the Console shows every instance's harness, model, thinking and
fallback together in one table, and the human edits them there. No view
does that at 54786df03. The Agent limits form edits only the model key,
and the Role instances table shows vars as raw JSON. So this row adds
that table. If Jason overrules Mos's reading, this section is redone
before review.
Problem
At 54786df03:
- Session resolution falls back to the system's
execution.adapterandexecution.modelwhen an instance sets noharnessormodel(packages/harness/src/bundle.mjs:68-70). The Console views do the same (packages/business/src/launch-settings.mjs:84-88and:162,packages/business/src/instance-settings.mjs:132). The system config has noexecution.adapter, and itsexecution.modelis the pi worker model, so an instance that drops a var refuses with a message about a system key instead of the var it is missing. thinkingis optional. The manifest records it and neither harness applies it (packages/harness/README.md).- There is no fallback. A launch whose harness binary or credential is missing refuses.
- The harness names are hard-coded, which PRD 1.3 REQ-PLUG-2 says this
row converts (Q156):
packages/business/src/vars.mjs:128, whereharnesstakes onlypiorclaude-code;packages/harness/src/bundle.mjs:31,:37,:47-53,:104,:179and:204-212: the web-builtin check, the list, the relies-on lines and gate timeout, the builtins prompt line, the pi extension or Claude settings files, and the manifest fields;packages/harness/src/runner.mjs:251and:275, the turn-end marker and the adapter environment;packages/seat/src/session.mjs:42-43, the adapter scripts;packages/seat/src/sandbox.mjs:105-106, the mounts;packages/cli/src/launcher.mjs:242,:249and:316, the credential check, the row 126 account and the versions;packages/bus/src/broker.mjs:421, theinvalid-harnesscheck;packages/bus/src/usage.mjs:77-78, where each harness keeps its session files.
Owner and reviewer
- Owner: Sage.
- Reviewers: Darkwing and Dewey.
Files owned
packages/business/src/vars.mjs,business.mjs,launch-settings.mjs,instance-settings.mjs,index.mjsand their tests,packages/business/README.mdpackages/harness/src/bundle.mjs,runner.mjs, tests, READMEpackages/seat/src/session.mjs,packages/seat/src/sandbox.mjs(the mounts only) and their testspackages/cli/src/launcher.mjsand its testspackages/bus/src/broker.mjs(the harness check only),packages/bus/src/usage.mjs(the session directory only) and their testsadapters/pi/adapter.sh,adapters/claude/adapter.sh,adapters/README.md- the
harnessentry inpackages/plugins/src/kinds.mjs,packages/plugins/harness/pi/andpackages/plugins/harness/claude-code/, their tests, and the harness section ofpackages/plugins/README.md - test business fixtures that set
harness,modelorthinking packages/business/src/launch-settings.mjs(the routes read and write)packages/bus/src/broker.mjsandruntime.mjs: theroutes.setverb onlypackages/webui/src/public/bus.js(the launch settings section only),packages/webui/src/cli.mjsandserve.mjs(the routes post only),reads.mjsif the settings read needs a field, and the webui tests for them.packages/webui/src/bus.mjsstays unchanged, becausepackages/bus/tests/channel-proof.test.mjspins its sha256.agents/sage/work/queue-65/
A change outside this list is named in the packet with its reason.
What ships
- Harnesses as plug-in modules (PRD 1.3 REQ-PLUG-1, REQ-PLUG-2; Q156).
Row 131's loader and kind registry are in place.
- A
harnessentry inpackages/plugins/src/kinds.mjsat interface version 1. The organization file switches on a list,plugins.harness, throughmosaic plugins set. - Two modules,
packages/plugins/harness/pi/andpackages/plugins/harness/claude-code/. Eachindex.mjsexports a function for every place core branches on a harness today: the adapter script it runs (a path underadapters/, which stay where they are), a preflight (binary present, version, credential status), its sandbox mounts, its bundle files, relies-on lines and gate timeout, the adapter environment, the turn-end check, where its session files live for the usage reader, whether its launches count againstlaunch.account, whether it servesharness-builtinweb, and how it passesthinking. The packet and the plug-ins README list the final export names. Rows 64 and 66 add their skills and unslop exports to these modules. - Core reaches a harness only through the loader. None of the places
listed under Problem names
piorclaude-codeafter this row. The broker'sinvalid-harnesscheck accepts a harness that is switched on. - The
harnessvar takes a module name (the loader's name pattern), not a fixed pair. A launch whose harness, primary or fallback, isn't switched on refuses withharness-off. So does a CLI or Console write that sets one. Loading the business file doesn't refuse on it, so the Console and the bus keep working whileplugins.harnessis unset, and only launches refuse. The loader's own refusals (bad name, missing directory, symlink, load failure, missing export, other version) refuse the launch and name the module.
- A
- One source. A managed session takes its harness, model and thinking
from its instance's three vars. Session resolution and the Console
views stop reading
execution.adapterandexecution.model. Those system keys stay forscripts/run-task.shworkers, which this row doesn't touch, and for the print-onlymigrate-launch.mjs. A launch for an instance missing any of the three refuses withno-routeand names the missing var. - Fallback. Three agent vars,
fallback.harness,fallback.model(alaunch.modelskey) andfallback.thinking, set all together or not at all. A partial set refuses the business file at load. The fallback is used only at launch, when the primary harness fails its preflight (binary missing, or credential status not ok). Never mid-session. The limit checks andmayLaunchrun on the route the launch actually takes. The manifest recordsrouteasprimaryorfallback, with the preflight reason that chose the fallback. thinkingreaches 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 andadapters/README.md. The README line saying thinking isn't applied goes. Probed 2026-10-19: Pi 0.85.1 takes--thinkingwith off, minimal, low, medium, high, xhigh or max. Claude Code 2.1.302 takes--effortwith low, medium, high, xhigh or max, and has no off or minimal. Each module exports the levels it applies. A launch or a write that gives a harness a level it can't apply refuses withthinking-unsupportedand names the harness. Nothing is remapped. Every live instance uses high or medium on claude-code.- One table in the Console (mos-0840). The launch settings section gets a
routing table with one row per instance and these columns: harness,
model, thinking, and the fallback's harness, model and thinking. Each
cell is a select drawn from the switched-on harnesses, the
launch.modelskeys and the levels that harness applies. A fallback is set as a whole or cleared as a whole. Save goes through a new broker verb,routes.set, which takes the revision and a map from instance to its route and writes them in one organization write, with the writer's identity log and receipt. It is human-only and outsideORGANIZATION_KEYS, as row 165'sinstances.setis, so the chief of staff's tool never reaches it. Throughorganization.setthat tool changes only an instance's model key, with one exception: the assistant, whose whole vars it may set under Q169 ("The COS and the user should have the ability to (re)configure the assistant role and seat name", Jason). That write runs the same route checks asroutes.set(round 2 ruling, sage-1006). The table replaces the "Role models" fieldset, which edited only the model key. The write checks the same rules as every other write of these vars:harness-off,thinking-unsupported, unknown model, partial fallback. The Role instances section stays as it is. - No new table. The live file already sets the three vars on every instance, and this row changes none of them. Every fallback starts unset. The first table in the old version of this section is void.
- Tests: a session resolves harness, model and thinking from its vars;
a missing var refuses with
no-route, and a session never readsexecution.adapterorexecution.model; a partial fallback refuses the file at load; a stated fallback is used when the primary's preflight fails, an unstated one never is, and a primary that passes never uses it; the manifest records the route and the reason; the limit check sees the fallback's provider and model; thinking reaches the Pi and Claude Code fake adapters as the recorded flag; a level the harness can't apply refuses withthinking-unsupportedat a write and at launch; the chief of staff's write of the assistant's vars refusesharness-off,thinking-unsupportedand a partial fallback, and a valid one saves with the chief as the actor. - Console tests: the routing table lists every instance with its six
values; a browser test edits a harness, a model, a thinking level and
a fallback, saves, and reads each back from the file; a partial
fallback, a switched-off harness and an unsupported level each refuse
with the draft kept; a stale revision refuses as the other settings
saves do;
organization.setand the chief of staff's tool refuse arouteskey, androutes.setrefuses a role session. - Harness tests: a launch on a harness that isn't switched on refuses
with
harness-off, primary or fallback, and so does a write that sets one; a business file with harness vars and noplugins.harnessstill loads and its Console view reads; a fixture harness module switched on in a test organization file launches a fake session with no core file changed; a grep test that no file underpackages/*/srcoutsidepackages/pluginsnamespiorclaude-codeas a harness, with the exceptions under Out of scope listed in the test. One mutant per refusal (no-route,harness-offat launch,harness-offat a write, a partial fallback, a fallback taken when the primary passes), and one that puts a harness name back in core, plus one forthinking-unsupportedand one that drops the fallback columns from the table, and one where the chief's write of the assistant skips the route checks. - Gate suites:
node --test 'packages/plugins/tests/*.test.mjs',node --test 'packages/bus/tests/*.test.mjs',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',node --test 'packages/webui/tests/*.test.mjs'(the Console reads the launch view), and everyscripts/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. Changing any instance's harness, model or thinking in the
live file.
These keep a harness name, and the grep test lists them:
- the tool-policy gate:
packages/harness/src/gate.mjs,claude-gate.mjsandpi-extension.mjsenforce the session's tool policy, and a module mustn't be able to change that boundary (PRD REQ-PLUG-1); packages/bus/src/human.mjs:85, the human-proof check that refuses any agent harness binary, which is a security boundary;packages/conversationandpackages/discord, which start pi directly and convert when a row changes those engines (Q156 item 2);packages/control-board/src/scan.mjs, which matches running process names.
No model provider kind; row 122 names providers as data, and the kind waits on row 127.
Gate
Darkwing and Dewey approve on the issue, naming the candidate manifest.
The suites above pass on a gate rerun in a clean worktree. Jason edits
the business file himself (decision 144). This row needs one entry from
him: at landing Sage sends him, through Mos, the exact
mosaic plugins set patch that adds "harness": ["claude-code"] to
whatever plugins holds then. Only claude-code goes on, because no live
instance uses pi. Until he runs it on a stack pinned past this row,
every managed launch refuses harness-off, as chat refuses before
plugins.channel (row 164), and the re-pin plan says so. Once he has
run it, Sage starts one managed coder session and attaches the manifest
showing the module loaded, the route used, the provider and model
counted, and the thinking flag passed. The modules go live only through
a re-pin on Mos's go (decision 109).
The table changes packages/webui/src/public/, so its diff goes to Mos
under decision 123 before the in-review move. The landing report names
the view that meets mos-0840.
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 pi module passes Pi only the
harness's own extension (MOSAIC_EXTENSIONS,
packages/plugins/harness/pi/index.mjs:56), and the Claude Code bundle
has one hook, the PreToolUse gate
(packages/plugins/harness/claude-code/index.mjs:94-96). 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 andlists.jsonchange only if a test needs it)packages/harness/src/bundle.mjs,runner.mjs, tests, READMEpackages/plugins/harness/pi/index.mjs,packages/plugins/harness/claude-code/index.mjs(theirbundleandenv),packages/plugins/tests/harness.test.mjs, and the harness lines ofpackages/plugins/README.md. The harness kind keeps interface 1 and its export list; a new export stops the row.adapters/README.mdagents/sage/work/queue-<row>/
What ships
- Pi. The runner adds
tools/unslop-hook/extension.tstoMOSAIC_EXTENSIONSand pointsMOSAIC_UNSLOP_LOGat<run>/session/unslop.jsonl. - Claude Code. The bundle's
settings.jsongains a hook on reply end that checks the last assistant text withunslop-check.jsand 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 talkalready 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.jsondrives both. The manifest records the extension, the hook script andlists.jsonwith digests. - Fail closed, as the Pi extension does: a broken
lists.jsonstops the checks, logsbroken_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.jsonlin the run directory records per-turncheckedandflaggedevents 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.jsonfails 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/plugins/tests/*.test.mjs',node --test 'packages/cli/tests/*.test.mjs', and everyscripts/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.