brief: managed session config rows, skills (Q82), model routing (Q83), unslop (Q84) (sage)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-11 14:52:45 -05:00
co-authored by Claude Opus 5.5
parent 02da0929c4
commit 26cc4e1960
@@ -0,0 +1,293 @@
# 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.