Compare commits
1 Commits
fix/person
...
feat/a3b-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1bb9fbd83a |
@@ -353,25 +353,6 @@ re-evaluate if isolation or write-volume demands it.
|
|||||||
- **Docs as projections:** `docs/TASKS.md` and `MISSION-MANIFEST.md` become generated exports of the DB, not hand-maintained.
|
- **Docs as projections:** `docs/TASKS.md` and `MISSION-MANIFEST.md` become generated exports of the DB, not hand-maintained.
|
||||||
- **Sub-decision pending:** dedicated schema in existing PG instance (recommended) vs. dedicated PG instance. Revisit if isolation or write-volume demands it.
|
- **Sub-decision pending:** dedicated schema in existing PG instance (recommended) vs. dedicated PG instance. Revisit if isolation or write-volume demands it.
|
||||||
|
|
||||||
## Decisions of record (2026-06-24, with Jason)
|
|
||||||
|
|
||||||
- **Per-agent model switch (operator-configurable, NOT a global lock):** model selection is
|
|
||||||
**per-agent**, never a host-global pin. Claude sessions MUST NOT be locked to a single model in
|
|
||||||
`~/.claude/settings.json`; each agent chooses its model independently. The plumbing already exists —
|
|
||||||
roster `model_hint` → `MOSAIC_AGENT_MODEL` → `start-agent-session.sh` appends `--model <hint>` to that
|
|
||||||
agent's harness (claude or pi); settable today via `mosaic fleet add|edit <agent> --model <hint>`.
|
|
||||||
**North-star target:** surface this as a **per-agent model switch in the webUI** (with CLI/TUI parity
|
|
||||||
per MVP-X1) — read the roster, expose a per-agent model dropdown, write `model_hint` back, and restart
|
|
||||||
that one agent to apply. Unset = inherit the harness default. This **composes with** the budget
|
|
||||||
downgrade ladder (opus → sonnet → haiku, then Claude → Codex): the operator sets the per-agent model
|
|
||||||
_intent/ceiling_; budget pacing may downgrade within policy. Tracked as a Fleet `TASKS.md` entry under
|
|
||||||
the Phase-5 webUI surface.
|
|
||||||
- **Orchestrator runtime (confirmed live):** the **orchestrator and enhancer run Claude Opus 4.8 in the
|
|
||||||
Claude Code harness**; only workers (coder/reviewer) run pi/gpt-5.5. Consistent with the 2026-06-20
|
|
||||||
"Claude reserved for Claude Code only" decision (the orchestrator runs _in_ Claude Code, not an
|
|
||||||
alternate Claude harness). Pi/gpt-5.5 as the orchestrator is permitted **only if proven** at least as
|
|
||||||
satisfactory; absent that proof, the orchestrator stays on Claude Opus 4.8.
|
|
||||||
|
|
||||||
## Future enhancements (north-star, post-MVP — not on the MVP track)
|
## Future enhancements (north-star, post-MVP — not on the MVP track)
|
||||||
|
|
||||||
- **Mosaic Claude Discord Plugin** — a first-party Mosaic Discord connector that properly
|
- **Mosaic Claude Discord Plugin** — a first-party Mosaic Discord connector that properly
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mosaicstack/db",
|
"name": "@mosaicstack/db",
|
||||||
"version": "0.0.4",
|
"version": "0.0.3",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git",
|
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git",
|
||||||
|
|||||||
@@ -114,21 +114,10 @@ MOSAIC_RUNTIME_BIN_PREFIX=$(_build_runtime_bin_prefix)
|
|||||||
# safe single bash token regardless of the name's characters.
|
# safe single bash token regardless of the name's characters.
|
||||||
AGENT_NAME_Q=$(printf '%q' "$AGENT_NAME")
|
AGENT_NAME_Q=$(printf '%q' "$AGENT_NAME")
|
||||||
|
|
||||||
# MOSAIC_AGENT_CLASS must ALSO be exported INTO the pane, for the same reason as
|
|
||||||
# MOSAIC_AGENT_NAME above: the pane inherits the tmux SERVER environment (not this
|
|
||||||
# script's env, and not the systemd unit's EnvironmentFile), so the per-agent class
|
|
||||||
# written to agents/<name>.env would otherwise be invisible in-pane. The launcher
|
|
||||||
# composes the persona contract from process.env.MOSAIC_AGENT_CLASS at launch
|
|
||||||
# (compose-contract -> readPersonaContractBlock); without this export it sees an
|
|
||||||
# undefined class and silently injects NO persona contract. %q-quote it so it is a
|
|
||||||
# safe single bash token; an empty/unset class %q-quotes to '' and is a harmless
|
|
||||||
# no-op downstream (readPersonaContractBlock returns '' for an empty class).
|
|
||||||
AGENT_CLASS_Q=$(printf '%q' "${MOSAIC_AGENT_CLASS:-}")
|
|
||||||
|
|
||||||
if [ -n "$MOSAIC_RUNTIME_BIN_PREFIX" ]; then
|
if [ -n "$MOSAIC_RUNTIME_BIN_PREFIX" ]; then
|
||||||
PANE_SHELL_SNIPPET="export MOSAIC_AGENT_NAME=${AGENT_NAME_Q}; export MOSAIC_AGENT_CLASS=${AGENT_CLASS_Q}; export PATH=\"${MOSAIC_RUNTIME_BIN_PREFIX}:\${PATH}\"; exec ${MOSAIC_AGENT_COMMAND}"
|
PANE_SHELL_SNIPPET="export MOSAIC_AGENT_NAME=${AGENT_NAME_Q}; export PATH=\"${MOSAIC_RUNTIME_BIN_PREFIX}:\${PATH}\"; exec ${MOSAIC_AGENT_COMMAND}"
|
||||||
else
|
else
|
||||||
PANE_SHELL_SNIPPET="export MOSAIC_AGENT_NAME=${AGENT_NAME_Q}; export MOSAIC_AGENT_CLASS=${AGENT_CLASS_Q}; exec ${MOSAIC_AGENT_COMMAND}"
|
PANE_SHELL_SNIPPET="export MOSAIC_AGENT_NAME=${AGENT_NAME_Q}; exec ${MOSAIC_AGENT_COMMAND}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mkdir -p "$MOSAIC_AGENT_WORKDIR"
|
mkdir -p "$MOSAIC_AGENT_WORKDIR"
|
||||||
|
|||||||
@@ -104,7 +104,6 @@ PATH="$FAKE_BIN:$PATH" \
|
|||||||
MOSAIC_TMUX_SOCKET="$SOCKET3" \
|
MOSAIC_TMUX_SOCKET="$SOCKET3" \
|
||||||
MOSAIC_AGENT_WORKDIR="$WORKDIR3" \
|
MOSAIC_AGENT_WORKDIR="$WORKDIR3" \
|
||||||
MOSAIC_AGENT_RUNTIME="pi" \
|
MOSAIC_AGENT_RUNTIME="pi" \
|
||||||
MOSAIC_AGENT_CLASS="code" \
|
|
||||||
MOSAIC_RUNTIME_BIN="$FAKE_RUNTIME_BIN" \
|
MOSAIC_RUNTIME_BIN="$FAKE_RUNTIME_BIN" \
|
||||||
MOSAIC_AGENT_COMMAND="mosaic yolo pi --model openai-codex/gpt-5.5:high" \
|
MOSAIC_AGENT_COMMAND="mosaic yolo pi --model openai-codex/gpt-5.5:high" \
|
||||||
MOSAIC_HEARTBEAT_RUN_DIR="$HB_RUN_DIR3" \
|
MOSAIC_HEARTBEAT_RUN_DIR="$HB_RUN_DIR3" \
|
||||||
@@ -128,18 +127,6 @@ echo "$all_args" | grep -qF "exec " || fail "pane command does not use exec"
|
|||||||
echo "$all_args" | grep -qF "mosaic yolo pi --model openai-codex/gpt-5.5:high" || \
|
echo "$all_args" | grep -qF "mosaic yolo pi --model openai-codex/gpt-5.5:high" || \
|
||||||
fail "pane command does not forward MOSAIC_AGENT_COMMAND with flags intact"
|
fail "pane command does not forward MOSAIC_AGENT_COMMAND with flags intact"
|
||||||
|
|
||||||
# d) MOSAIC_AGENT_NAME and the per-agent MOSAIC_AGENT_CLASS must BOTH be exported
|
|
||||||
# INTO the pane. The pane inherits the tmux SERVER environment (not this
|
|
||||||
# script's env, nor the systemd unit's EnvironmentFile), so any per-agent var
|
|
||||||
# the launcher needs in-pane must be re-exported in the snippet. CLASS is
|
|
||||||
# load-bearing: the launcher composes the persona contract from
|
|
||||||
# process.env.MOSAIC_AGENT_CLASS, so a missing export silently drops the
|
|
||||||
# persona (regression guard for the A3a pane-propagation gap).
|
|
||||||
echo "$all_args" | grep -qF "export MOSAIC_AGENT_NAME=" || \
|
|
||||||
fail "pane command does not export MOSAIC_AGENT_NAME into the pane"
|
|
||||||
echo "$all_args" | grep -qF "export MOSAIC_AGENT_CLASS=code" || \
|
|
||||||
fail "pane command does not export MOSAIC_AGENT_CLASS into the pane (persona would silently drop)"
|
|
||||||
|
|
||||||
# ── Test 4: when no extra runtime-bin dirs exist, exec still appears ───────────
|
# ── Test 4: when no extra runtime-bin dirs exist, exec still appears ───────────
|
||||||
TMUX_ARGS_FILE2=$(mktemp)
|
TMUX_ARGS_FILE2=$(mktemp)
|
||||||
FAKE_BIN2=$(mktemp -d)
|
FAKE_BIN2=$(mktemp -d)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mosaicstack/mosaic",
|
"name": "@mosaicstack/mosaic",
|
||||||
"version": "0.0.47",
|
"version": "0.0.45",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git",
|
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git",
|
||||||
|
|||||||
@@ -246,8 +246,6 @@ describe('fleet roster parsing', () => {
|
|||||||
expect(generateAgentEnv(roster, getRosterAgent(roster, 'coder0'))).toBe(
|
expect(generateAgentEnv(roster, getRosterAgent(roster, 'coder0'))).toBe(
|
||||||
[
|
[
|
||||||
'MOSAIC_AGENT_NAME=coder0',
|
'MOSAIC_AGENT_NAME=coder0',
|
||||||
// Reflects the roster's non-default `class: implementer` (A3a).
|
|
||||||
'MOSAIC_AGENT_CLASS=implementer',
|
|
||||||
'MOSAIC_AGENT_RUNTIME=codex',
|
'MOSAIC_AGENT_RUNTIME=codex',
|
||||||
'MOSAIC_AGENT_MODEL=',
|
'MOSAIC_AGENT_MODEL=',
|
||||||
'MOSAIC_AGENT_WORKDIR=/srv/mosaic',
|
'MOSAIC_AGENT_WORKDIR=/srv/mosaic',
|
||||||
@@ -257,40 +255,6 @@ describe('fleet roster parsing', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('emits MOSAIC_AGENT_CLASS=worker for an agent that declares no class', async () => {
|
|
||||||
cleanup = await tempDir();
|
|
||||||
const rosterPath = join(cleanup, 'roster.json');
|
|
||||||
await writeFile(
|
|
||||||
rosterPath,
|
|
||||||
JSON.stringify({
|
|
||||||
version: 1,
|
|
||||||
transport: 'tmux',
|
|
||||||
agents: [{ name: 'coder0', runtime: 'codex' }],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const roster = await loadFleetRoster(rosterPath);
|
|
||||||
expect(generateAgentEnv(roster, getRosterAgent(roster, 'coder0'))).toContain(
|
|
||||||
'MOSAIC_AGENT_CLASS=worker\n',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('shell-escapes MOSAIC_AGENT_CLASS so a launcher reads it verbatim', async () => {
|
|
||||||
cleanup = await tempDir();
|
|
||||||
const rosterPath = join(cleanup, 'roster.json');
|
|
||||||
await writeFile(
|
|
||||||
rosterPath,
|
|
||||||
JSON.stringify({
|
|
||||||
version: 1,
|
|
||||||
transport: 'tmux',
|
|
||||||
agents: [{ name: 'coder0', runtime: 'codex', class: 'orchestrator' }],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const roster = await loadFleetRoster(rosterPath);
|
|
||||||
expect(generateAgentEnv(roster, getRosterAgent(roster, 'coder0'))).toContain(
|
|
||||||
'MOSAIC_AGENT_CLASS=orchestrator\n',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('preserves site-owned agent EnvironmentFile overrides while refreshing roster keys', () => {
|
it('preserves site-owned agent EnvironmentFile overrides while refreshing roster keys', () => {
|
||||||
const generated = [
|
const generated = [
|
||||||
'MOSAIC_AGENT_NAME=coder0',
|
'MOSAIC_AGENT_NAME=coder0',
|
||||||
@@ -322,28 +286,6 @@ describe('fleet roster parsing', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('updates (does not duplicate) MOSAIC_AGENT_CLASS on re-launch', () => {
|
|
||||||
const generated = [
|
|
||||||
'MOSAIC_AGENT_NAME=coder0',
|
|
||||||
'MOSAIC_AGENT_CLASS=orchestrator',
|
|
||||||
'MOSAIC_AGENT_RUNTIME=codex',
|
|
||||||
'',
|
|
||||||
].join('\n');
|
|
||||||
const existing = [
|
|
||||||
'MOSAIC_AGENT_NAME=coder0',
|
|
||||||
'MOSAIC_AGENT_CLASS=worker',
|
|
||||||
'MOSAIC_AGENT_RUNTIME=codex',
|
|
||||||
'',
|
|
||||||
].join('\n');
|
|
||||||
|
|
||||||
const merged = mergeAgentEnv(generated, existing);
|
|
||||||
// mergeAgentEnv keys by VAR name, so the regenerated CLASS wins and there is
|
|
||||||
// exactly one MOSAIC_AGENT_CLASS line (no stale worker value left behind).
|
|
||||||
expect(merged).toContain('MOSAIC_AGENT_CLASS=orchestrator');
|
|
||||||
expect(merged).not.toContain('MOSAIC_AGENT_CLASS=worker');
|
|
||||||
expect(merged.match(/^MOSAIC_AGENT_CLASS=/gm)).toHaveLength(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects unknown roster fields instead of silently defaulting', async () => {
|
it('rejects unknown roster fields instead of silently defaulting', async () => {
|
||||||
cleanup = await tempDir();
|
cleanup = await tempDir();
|
||||||
const rosterPath = join(cleanup, 'roster.yaml');
|
const rosterPath = join(cleanup, 'roster.yaml');
|
||||||
|
|||||||
@@ -490,9 +490,6 @@ export function generateAgentEnv(roster: FleetRoster, agent: FleetAgent): string
|
|||||||
const workingDirectory = agent.workingDirectory ?? roster.defaults.workingDirectory;
|
const workingDirectory = agent.workingDirectory ?? roster.defaults.workingDirectory;
|
||||||
return [
|
return [
|
||||||
`MOSAIC_AGENT_NAME=${shellEnvValue(agent.name)}`,
|
`MOSAIC_AGENT_NAME=${shellEnvValue(agent.name)}`,
|
||||||
// Per-agent class → start-agent-session.sh / launcher reads this to inject the
|
|
||||||
// matching persona contract for the agent's class (default `worker`).
|
|
||||||
`MOSAIC_AGENT_CLASS=${shellEnvValue(agent.className)}`,
|
|
||||||
`MOSAIC_AGENT_RUNTIME=${shellEnvValue(agent.runtime)}`,
|
`MOSAIC_AGENT_RUNTIME=${shellEnvValue(agent.runtime)}`,
|
||||||
// Per-agent model hint → start-agent-session.sh appends `--model <hint>` to
|
// Per-agent model hint → start-agent-session.sh appends `--model <hint>` to
|
||||||
// the `mosaic yolo` launch so workers run on the roster's model (e.g. pi on
|
// the `mosaic yolo` launch so workers run on the roster's model (e.g. pi on
|
||||||
|
|||||||
Reference in New Issue
Block a user