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

14 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.

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 <dataRoot>/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-<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.

  • 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-<row>/

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-<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/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.