Files
stack/docs/plans/2026-10-11_managed-session-config.md

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.
  • --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-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.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/plugins/harness/pi/index.mjs, packages/plugins/harness/claude-code/index.mjs (their bundle and env), packages/plugins/tests/harness.test.mjs, and the harness lines of packages/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.mjs and its tests (the refusal only)
  • adapters/pi/adapter.sh, adapters/claude/adapter.sh, adapters/README.md
  • agents/sage/work/queue-<row>/

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 <dataRoot>/skills-enabled/<name>/SKILL.md. A name that isn't enabled refuses the launch with skill-not-enabled, naming the skill and scripts/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 skills lists each name, its bundle path and the sha256 of its SKILL.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-skills and Claude Code keeps --disable-slash-commands and --restricted, so neither harness discovers a skill of its own. Sage chose this on 2026-10-19 from a probe (evidence in agents/sage/work/queue-64/probe.md, the choice in adapters/README.md):

    • Pi's --skill puts only each skill's name and description in the prompt, and the model must read the SKILL.md itself (docs/skills.md:67-69 in the pinned pi package). The gate refuses every file-tool path outside the workspace (packages/harness/src/gate.mjs:3-4), so through MOSAIC_SKILLS a 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-dir skill under --restricted, but only with the Skill tool on, and then it also offers its own bundled skills. disableBundledSkills and a skillOverrides entry for its built-in plugin-authoring cut 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 the Skill tool 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_SKILLS stay unchanged.
  • 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/plugins/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.

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.adapter and execution.model when an instance sets no harness or model (packages/harness/src/bundle.mjs:68-70). The Console views do the same (packages/business/src/launch-settings.mjs:84-88 and :162, packages/business/src/instance-settings.mjs:132). The system config has no execution.adapter, and its execution.model is 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.
  • thinking is 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, where harness takes only pi or claude-code;
    • packages/harness/src/bundle.mjs:31, :37, :47-53, :104, :179 and :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:251 and :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, :249 and :316, the credential check, the row 126 account and the versions;
    • packages/bus/src/broker.mjs:421, the invalid-harness check;
    • 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.mjs and their tests, packages/business/README.md
  • packages/harness/src/bundle.mjs, runner.mjs, tests, README
  • packages/seat/src/session.mjs, packages/seat/src/sandbox.mjs (the mounts only) and their tests
  • packages/cli/src/launcher.mjs and its tests
  • packages/bus/src/broker.mjs (the harness check only), packages/bus/src/usage.mjs (the session directory only) and their tests
  • adapters/pi/adapter.sh, adapters/claude/adapter.sh, adapters/README.md
  • the harness entry in packages/plugins/src/kinds.mjs, packages/plugins/harness/pi/ and packages/plugins/harness/claude-code/, their tests, and the harness section of packages/plugins/README.md
  • test business fixtures that set harness, model or thinking
  • packages/business/src/launch-settings.mjs (the routes read and write)
  • packages/bus/src/broker.mjs and runtime.mjs: the routes.set verb only
  • packages/webui/src/public/bus.js (the launch settings section only), packages/webui/src/cli.mjs and serve.mjs (the routes post only), reads.mjs if the settings read needs a field, and the webui tests for them. packages/webui/src/bus.mjs stays unchanged, because packages/bus/tests/channel-proof.test.mjs pins 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 harness entry in packages/plugins/src/kinds.mjs at interface version 1. The organization file switches on a list, plugins.harness, through mosaic plugins set.
    • Two modules, packages/plugins/harness/pi/ and packages/plugins/harness/claude-code/. Each index.mjs exports a function for every place core branches on a harness today: the adapter script it runs (a path under adapters/, 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 against launch.account, whether it serves harness-builtin web, and how it passes thinking. 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 pi or claude-code after this row. The broker's invalid-harness check accepts a harness that is switched on.
    • The harness var 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 with harness-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 while plugins.harness is 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.
  • 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.adapter and execution.model. Those system keys stay for scripts/run-task.sh workers, which this row doesn't touch, and for the print-only migrate-launch.mjs. A launch for an instance missing any of the three refuses with no-route and names the missing var.
  • Fallback. Three agent vars, fallback.harness, fallback.model (a launch.models key) and fallback.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 and mayLaunch run on the route the launch actually takes. The manifest records route as primary or fallback, with the preflight reason that chose the fallback.
  • 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. Probed 2026-10-19: Pi 0.85.1 takes --thinking with off, minimal, low, medium, high, xhigh or max. Claude Code 2.1.302 takes --effort with 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 with thinking-unsupported and 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.models keys 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 outside ORGANIZATION_KEYS, as row 165's instances.set is, so the chief of staff's tool never reaches it. Through organization.set that 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 as routes.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 reads execution.adapter or execution.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 with thinking-unsupported at a write and at launch; the chief of staff's write of the assistant's vars refuses harness-off, thinking-unsupported and 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.set and the chief of staff's tool refuse a routes key, and routes.set refuses 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 no plugins.harness still 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 under packages/*/src outside packages/plugins names pi or claude-code as a harness, with the exceptions under Out of scope listed in the test. One mutant per refusal (no-route, harness-off at launch, harness-off at a write, a partial fallback, a fallback taken when the primary passes), and one that puts a harness name back in core, plus one for thinking-unsupported and 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 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. 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.mjs and pi-extension.mjs enforce 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/conversation and packages/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 and lists.json change only if a test needs it)
  • packages/harness/src/bundle.mjs, runner.mjs, tests, README
  • packages/plugins/harness/pi/index.mjs, packages/plugins/harness/claude-code/index.mjs (their bundle and env), packages/plugins/tests/harness.test.mjs, and the harness lines of packages/plugins/README.md. The harness kind keeps interface 1 and its export list; a new export stops the row.
  • adapters/README.md
  • agents/sage/work/queue-<row>/

What ships

  • Pi. The runner adds tools/unslop-hook/extension.ts to MOSAIC_EXTENSIONS and points MOSAIC_UNSLOP_LOG at <run>/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/plugins/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.