Compare commits
3 Commits
feat/p5-ov
...
release/mo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41845959ce | ||
| 23343bb7f0 | |||
| c8b2dab0ca |
@@ -45,3 +45,9 @@ Active workstream is **W1 — Federation v1**. Workers should:
|
||||
- Status: PR open, awaiting maintainer merge ratification (fleet-governing change).
|
||||
- Cut always-injected contract AGENTS+TOOLS+RUNTIME 8,827→4,122 tok (−53%); all 12 hard gates intact.
|
||||
- Validation: deterministic gate-checklist PASS; headless A/B thin 7/9 vs monolith 5/9. Detail: scratchpads/contract-thin-core.md.
|
||||
|
||||
## P5 — Overlay composer + cross-harness (#604) — feat/p5-overlay-composer
|
||||
|
||||
- Status: in progress. R7 (compose-contract) + R8 (cross-harness) + R9 (composer test).
|
||||
- `composeContract({harness, mosaicHome})` pure fn + `.local` overlay deltas-by-value; `mosaic compose-contract <harness>` command; AGENTS bare-launch nudge; composer spec (per-tier anchor + Tier-3 byte-equality). Detail: scratchpads/p5-overlay-composer.md.
|
||||
- Defer to P6: CONTRIBUTING.md + compliance matrix; line-count CI ceiling; aiguide; alpha tag.
|
||||
|
||||
@@ -20,8 +20,9 @@ functional, we use the fleet itself to continue the work.
|
||||
## Requirements
|
||||
|
||||
### A. Configure-without-AI CLI
|
||||
|
||||
| ID | Requirement |
|
||||
|---|---|
|
||||
| --- | ------------------------------------------------------------------------------------------------------------- |
|
||||
| R1 | `mosaic fleet` command set is functional end-to-end (init/install/start/stop/status/ps/verify + agent verbs). |
|
||||
| R2 | `mosaic fleet init` is an interactive, **AI-free** CLI wizard. |
|
||||
| R3 | Init asks the **configuration type**: `general`, `coding`, `research`, `hybrid`, … (extensible). |
|
||||
@@ -32,24 +33,27 @@ functional, we use the fleet itself to continue the work.
|
||||
| R17 | Fleet controls are **simple and intuitive**. |
|
||||
|
||||
### B. Comms & orchestrator chat-ops
|
||||
|
||||
| ID | Requirement |
|
||||
|---|---|
|
||||
| --- | --------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| R6 | Init can wire the orchestrator to a chat connector — **Telegram / Discord / Matrix / Slack** — for command + comms. |
|
||||
| R7 | Designed with the end-goal of **Matrix comms on a locally-controlled server**. |
|
||||
| R16 | Fleet supports **tmux AND Matrix** comms, **user-configurable** at init or any time. Not all users want Matrix. |
|
||||
| R19 | **"Mos" orchestrator on Discord** (`chan 1517622518662434996` / `srv 1112631390438166618`) on `w-jarvis` — the first live target. |
|
||||
|
||||
### C. Runtime, health, lifecycle
|
||||
|
||||
| ID | Requirement |
|
||||
|---|---|
|
||||
| --- | ---------------------------------------------------------------------------------- |
|
||||
| R9 | Fleet is **mutable by the orchestrator agent** — add/remove agents per need. |
|
||||
| R13 | Fleet **gracefully handles Pi + Claude harness updates** — keep harnesses current. |
|
||||
| R14 | The **Pi harness is customized** for proper tool usage, etc. |
|
||||
| R15 | **Agent heartbeat** properly configured for **Claude AND GPT/Pi** agents. |
|
||||
|
||||
### D. Surfaces, testing, docs
|
||||
|
||||
| ID | Requirement |
|
||||
|---|---|
|
||||
| --- | ----------------------------------------------------------------------------------- |
|
||||
| R18 | Fleet built so the **webUI can view / monitor / terminate / butt-in** on a session. |
|
||||
| R11 | Installed and **tested on both `w-jarvis` and `dragon-lin`**. |
|
||||
| R12 | **Documentation**: how to install, configure, and use the fleet. |
|
||||
@@ -66,7 +70,7 @@ functional, we use the fleet itself to continue the work.
|
||||
## Phases (incremental, each shippable)
|
||||
|
||||
| Phase | Deliverable | Notes |
|
||||
|---|---|---|
|
||||
| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------- |
|
||||
| **F1 Presets + init wizard** | preset rosters (general/coding/research/hybrid) + always-orchestrator + AI-free `fleet init` selecting a preset; re-init idempotent | R1–R5, R8, R10, R17 |
|
||||
| **F2 Connector + Mos-on-Discord** | orchestrator chat-connector config (Discord first) + **Mos live on Discord `1517…`/`1112…`** on w-jarvis | R6, R19, partial R16 |
|
||||
| **F3 Heartbeat + harness** | HB confirmed for claude + pi/gpt; **custom Pi harness** (tool usage, native HB, model self-report); graceful harness updates | R13, R14, R15 |
|
||||
|
||||
43
docs/scratchpads/p5-overlay-composer.md
Normal file
43
docs/scratchpads/p5-overlay-composer.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# P5 — Overlay composer + cross-harness (compose-contract)
|
||||
|
||||
- **Issue:** #604 · **Branch:** `feat/p5-overlay-composer` · **Lineage:** #542 → constitution alpha
|
||||
- **Requirements:** R7 (compose-contract) + R8 (cross-harness) + R9 (composer test)
|
||||
- **Design of record:** `docs/design/framework-constitution/{DESIGN.md §3.2, PRD.md §4}` (on `feat/framework-constitution-alpha`)
|
||||
|
||||
## Locked design (sequential-thinking)
|
||||
|
||||
Current `launch.ts` assembly (`buildComposedPrompt`) injects by value: mission + PRD + hard-gate +
|
||||
CONSTITUTION + AGENTS + USER + TOOLS + runtime. It does **not** inject SOUL or STANDARDS (those are
|
||||
read-on-demand per the gutted AGENTS dispatcher), and has no `.local` overlay support.
|
||||
|
||||
**Decision (ASSUMPTION — recorded for the PR):** overlays are injected as **deltas by value** under
|
||||
labeled sections; base files keep their existing residency.
|
||||
|
||||
- `USER.local.md` → appended directly under the `# User Profile` block (USER is injected).
|
||||
- `SOUL.local.md` + `STANDARDS.local.md` → a trailing `# Operator Overlays` section (their bases are
|
||||
load-on-demand, so only the small delta is injected — not the full base prose).
|
||||
- **Why:** honors DESIGN §3.2 ("model gets one pre-merged blob, no read-merge ritual") while preserving
|
||||
the P3 byte-budget tiering (don't re-inject large SOUL/STANDARDS prose). Precedence order kept: base
|
||||
layers first, operator overlays at recency.
|
||||
- Base-only is automatic when a `.local` file is absent (`readOptional`).
|
||||
|
||||
## Plan
|
||||
|
||||
| # | Task | File |
|
||||
| --- | ------------------------------------------------------------------------------------------------------ | --------------------------------------- |
|
||||
| 1 | Extract `composeContract({harness, mosaicHome})` pure fn; `buildComposedPrompt` delegates | `src/commands/launch.ts` |
|
||||
| 2 | Overlay logic (USER.local under profile; SOUL/STANDARDS.local in `# Operator Overlays`) | `src/commands/launch.ts` |
|
||||
| 3 | `mosaic compose-contract <harness>` command → prints blob to stdout | `src/commands/launch.ts` |
|
||||
| 4 | Bare-launch overlay nudge in self-load fallback | `framework/defaults/AGENTS.md` |
|
||||
| 5 | `compose-contract.spec.ts`: per-tier anchor, Tier-3 byte-equality, overlay present/absent, per-harness | `src/commands/compose-contract.spec.ts` |
|
||||
|
||||
## Deferred to P6
|
||||
|
||||
CONTRIBUTING.md + harness×gate compliance matrix; resident line-count CI ceiling; `aiguide` reconcile;
|
||||
alpha tag `mosaic-vX.Y.Z-alpha`.
|
||||
|
||||
## Status
|
||||
|
||||
- [x] Phase scaffold (branch, issue #604, scratchpad, TASKS)
|
||||
- [ ] Implementation (tasks 1–5)
|
||||
- [ ] prettier + vitest green; PR via wrapper → Lead (rides 0.0.39; 0.0.38 mid-cut)
|
||||
@@ -9,7 +9,10 @@ overwritten on upgrade. (Layer model: `constitution/LAYER-MODEL.md`.)
|
||||
1. Your context already includes `CONSTITUTION.md` + `USER.md` + the TOOLS index + the runtime
|
||||
contract (injected by `mosaic` launch) — do not re-read those. **If you were launched bare**
|
||||
(a harness started without `mosaic`, so the law is NOT in your context), read
|
||||
`~/.config/mosaic/CONSTITUTION.md` now, before your first action.
|
||||
`~/.config/mosaic/CONSTITUTION.md` now, before your first action. A bare launch also gets
|
||||
**base contracts only** — operator overlays (`*.local.md`) are composed by the launcher, so if
|
||||
`SOUL.local.md`/`USER.local.md`/`STANDARDS.local.md` exist, relaunch via `mosaic <harness>` (or run
|
||||
`mosaic doctor`) to pick them up.
|
||||
2. Read `SOUL.md` (agent persona — small, once).
|
||||
3. Read project-local `AGENTS.md` / `CLAUDE.md` if present (these may only make behavior stricter).
|
||||
4. Read guides ONLY as triggered by the table below — pull role-relevant depth on demand, not up front.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mosaicstack/mosaic",
|
||||
"version": "0.0.37",
|
||||
"version": "0.0.39",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git",
|
||||
|
||||
118
packages/mosaic/src/commands/compose-contract.spec.ts
Normal file
118
packages/mosaic/src/commands/compose-contract.spec.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, readFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { composeContract } from './launch.js';
|
||||
|
||||
/**
|
||||
* Composer unit test (R7/R8/R9): asserts the launcher-composed runtime contract
|
||||
*
|
||||
* - includes the per-tier anchors (CONSTITUTION / AGENTS / USER / runtime),
|
||||
* - keeps the CONSTITUTION block byte-equal to the on-disk file (Tier-3
|
||||
* byte-equality — the bare-launch fallback read must match what is injected),
|
||||
* - merges `*.local.md` operator overlays as deltas-by-value, and omits them
|
||||
* entirely when absent (base-only),
|
||||
* - selects the correct per-harness RUNTIME.md.
|
||||
*
|
||||
* `composeContract` takes `mosaicHome` as a param, so each test runs against an
|
||||
* isolated fixture home. We also chdir to an empty temp cwd so the cwd-relative
|
||||
* mission/PRD blocks contribute nothing (deterministic output).
|
||||
*/
|
||||
|
||||
const CONSTITUTION = '# CONSTITUTION\n\nGATE-1: the non-negotiable law.\n';
|
||||
const AGENTS = '# Mosaic Agent Dispatcher\n\nLoad order + guide router.\n';
|
||||
const USER = '# operator\n\nName: Test Operator\n';
|
||||
const TOOLS = '# tools index\n';
|
||||
|
||||
function makeHome(): { home: string; root: string } {
|
||||
const root = mkdtempSync(join(tmpdir(), 'mosaic-compose-'));
|
||||
const home = join(root, 'mosaic-home');
|
||||
for (const h of ['claude', 'codex', 'opencode', 'pi']) {
|
||||
mkdirSync(join(home, 'runtime', h), { recursive: true });
|
||||
writeFileSync(join(home, 'runtime', h, 'RUNTIME.md'), `# ${h} runtime contract\n`);
|
||||
}
|
||||
writeFileSync(join(home, 'CONSTITUTION.md'), CONSTITUTION);
|
||||
writeFileSync(join(home, 'AGENTS.md'), AGENTS);
|
||||
writeFileSync(join(home, 'USER.md'), USER);
|
||||
writeFileSync(join(home, 'TOOLS.md'), TOOLS);
|
||||
return { home, root };
|
||||
}
|
||||
|
||||
describe('composeContract — overlay composer', () => {
|
||||
let fixture: ReturnType<typeof makeHome>;
|
||||
let prevCwd: string;
|
||||
let cwdDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = makeHome();
|
||||
prevCwd = process.cwd();
|
||||
cwdDir = mkdtempSync(join(tmpdir(), 'mosaic-cwd-'));
|
||||
process.chdir(cwdDir); // neutralize cwd-relative mission/PRD blocks
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.chdir(prevCwd);
|
||||
rmSync(fixture.root, { recursive: true, force: true });
|
||||
rmSync(cwdDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('includes the per-tier anchors and the selected harness runtime', () => {
|
||||
const out = composeContract('claude', fixture.home);
|
||||
expect(out).toContain('GATE-1: the non-negotiable law.'); // L0
|
||||
expect(out).toContain('Mosaic Agent Dispatcher'); // AGENTS
|
||||
expect(out).toContain('# User Profile'); // USER header
|
||||
expect(out).toContain('Name: Test Operator'); // USER body
|
||||
expect(out).toContain('# Runtime-Specific Contract');
|
||||
expect(out).toContain('# claude runtime contract');
|
||||
});
|
||||
|
||||
it('keeps the CONSTITUTION block byte-equal to the on-disk file (Tier-3)', () => {
|
||||
const out = composeContract('pi', fixture.home);
|
||||
const onDisk = readFileSync(join(fixture.home, 'CONSTITUTION.md'), 'utf-8');
|
||||
// The injected L0 must be a byte-equal substring of the composed blob, so a
|
||||
// bare-launch fallback read of CONSTITUTION.md matches what was injected.
|
||||
expect(out.includes(onDisk)).toBe(true);
|
||||
});
|
||||
|
||||
it('is base-only when no *.local overlays exist', () => {
|
||||
const out = composeContract('claude', fixture.home);
|
||||
expect(out).not.toContain('# Operator Overlays');
|
||||
expect(out).not.toContain('Operator Overlay (USER.local.md)');
|
||||
expect(out).not.toContain('Persona Overlay');
|
||||
expect(out).not.toContain('Standards Overlay');
|
||||
});
|
||||
|
||||
it('merges USER.local.md directly under the operator profile', () => {
|
||||
writeFileSync(join(fixture.home, 'USER.local.md'), 'Prefer terse status updates.\n');
|
||||
const out = composeContract('claude', fixture.home);
|
||||
expect(out).toContain('## Operator Overlay (USER.local.md)');
|
||||
expect(out).toContain('Prefer terse status updates.');
|
||||
// Overlay appears AFTER its base profile.
|
||||
expect(out.indexOf('# User Profile')).toBeLessThan(
|
||||
out.indexOf('## Operator Overlay (USER.local.md)'),
|
||||
);
|
||||
});
|
||||
|
||||
it('merges SOUL.local.md + STANDARDS.local.md as deltas in the Operator Overlays block', () => {
|
||||
writeFileSync(join(fixture.home, 'SOUL.local.md'), 'Tone: dry and direct.\n');
|
||||
writeFileSync(join(fixture.home, 'STANDARDS.local.md'), 'Require 90% coverage on auth code.\n');
|
||||
const out = composeContract('claude', fixture.home);
|
||||
expect(out).toContain('# Operator Overlays');
|
||||
expect(out).toContain('## Persona Overlay (SOUL.local.md)');
|
||||
expect(out).toContain('Tone: dry and direct.');
|
||||
expect(out).toContain('## Standards Overlay (STANDARDS.local.md)');
|
||||
expect(out).toContain('Require 90% coverage on auth code.');
|
||||
});
|
||||
|
||||
it('ignores whitespace-only *.local overlays (no empty overlay section)', () => {
|
||||
writeFileSync(join(fixture.home, 'SOUL.local.md'), ' \n\n');
|
||||
const out = composeContract('claude', fixture.home);
|
||||
expect(out).not.toContain('# Operator Overlays');
|
||||
});
|
||||
|
||||
it('selects a different RUNTIME.md per harness', () => {
|
||||
expect(composeContract('codex', fixture.home)).toContain('# codex runtime contract');
|
||||
expect(composeContract('pi', fixture.home)).toContain('# pi runtime contract');
|
||||
expect(composeContract('codex', fixture.home)).not.toContain('# pi runtime contract');
|
||||
});
|
||||
});
|
||||
@@ -291,12 +291,23 @@ function buildPrdBlock(): string {
|
||||
|
||||
// ─── Runtime prompt builder ──────────────────────────────────────────────────
|
||||
|
||||
function buildRuntimePrompt(runtime: RuntimeName): string {
|
||||
/**
|
||||
* Compose the full runtime contract for a harness: the resident-by-value core
|
||||
* (CONSTITUTION + AGENTS + USER + TOOLS + runtime) plus operator overlays
|
||||
* (`*.local.md` deltas), merged in precedence order so the model gets one
|
||||
* pre-merged blob (DESIGN §3.2 / R7). Overlays are injected as deltas by value;
|
||||
* base files keep their existing residency (USER injected; SOUL/STANDARDS are
|
||||
* load-on-demand, so only their small `.local` deltas are injected here).
|
||||
*
|
||||
* `mosaicHome` is parameterized for testability; production callers use the
|
||||
* module-level default.
|
||||
*/
|
||||
export function composeContract(runtime: RuntimeName, mosaicHome: string = MOSAIC_HOME): string {
|
||||
const runtimeContractPaths: Record<RuntimeName, string> = {
|
||||
claude: join(MOSAIC_HOME, 'runtime', 'claude', 'RUNTIME.md'),
|
||||
codex: join(MOSAIC_HOME, 'runtime', 'codex', 'RUNTIME.md'),
|
||||
opencode: join(MOSAIC_HOME, 'runtime', 'opencode', 'RUNTIME.md'),
|
||||
pi: join(MOSAIC_HOME, 'runtime', 'pi', 'RUNTIME.md'),
|
||||
claude: join(mosaicHome, 'runtime', 'claude', 'RUNTIME.md'),
|
||||
codex: join(mosaicHome, 'runtime', 'codex', 'RUNTIME.md'),
|
||||
opencode: join(mosaicHome, 'runtime', 'opencode', 'RUNTIME.md'),
|
||||
pi: join(mosaicHome, 'runtime', 'pi', 'RUNTIME.md'),
|
||||
};
|
||||
|
||||
const runtimeFile = runtimeContractPaths[runtime];
|
||||
@@ -331,27 +342,55 @@ For required push/merge/issue-close/release actions, execute without routine con
|
||||
`);
|
||||
|
||||
// CONSTITUTION.md (L0 — the non-negotiable law; lead with it). Tolerant of
|
||||
// pre-constitution installs that have not been re-seeded yet.
|
||||
const constitution = readOptional(join(MOSAIC_HOME, 'CONSTITUTION.md'));
|
||||
// pre-constitution installs that have not been re-seeded yet. Injected by
|
||||
// value verbatim so the bare-launch fallback read is byte-equal (R8).
|
||||
const constitution = readOptional(join(mosaicHome, 'CONSTITUTION.md'));
|
||||
if (constitution) parts.push(constitution);
|
||||
|
||||
// AGENTS.md
|
||||
parts.push(readFileSync(join(MOSAIC_HOME, 'AGENTS.md'), 'utf-8'));
|
||||
parts.push(readFileSync(join(mosaicHome, 'AGENTS.md'), 'utf-8'));
|
||||
|
||||
// USER.md
|
||||
const user = readOptional(join(MOSAIC_HOME, 'USER.md'));
|
||||
// USER.md (+ USER.local.md operator overlay, appended directly under the
|
||||
// profile its base owns).
|
||||
const user = readOptional(join(mosaicHome, 'USER.md'));
|
||||
if (user) parts.push('\n\n# User Profile\n\n' + user);
|
||||
const userLocal = readOptional(join(mosaicHome, 'USER.local.md'));
|
||||
if (userLocal.trim()) {
|
||||
parts.push('\n\n## Operator Overlay (USER.local.md)\n\n' + userLocal);
|
||||
}
|
||||
|
||||
// TOOLS.md
|
||||
const tools = readOptional(join(MOSAIC_HOME, 'TOOLS.md'));
|
||||
const tools = readOptional(join(mosaicHome, 'TOOLS.md'));
|
||||
if (tools) parts.push('\n\n# Machine Tools\n\n' + tools);
|
||||
|
||||
// Operator overlays whose base layers are load-on-demand (SOUL, STANDARDS):
|
||||
// inject only the small `.local` delta by value so the customization reaches
|
||||
// the model without re-injecting the full base prose (preserves the byte
|
||||
// budget). Absent `.local` files → base-only, automatically (R7 §3.2).
|
||||
const overlayBlocks: string[] = [];
|
||||
const soulLocal = readOptional(join(mosaicHome, 'SOUL.local.md'));
|
||||
if (soulLocal.trim()) {
|
||||
overlayBlocks.push('## Persona Overlay (SOUL.local.md)\n\n' + soulLocal.trim());
|
||||
}
|
||||
const standardsLocal = readOptional(join(mosaicHome, 'STANDARDS.local.md'));
|
||||
if (standardsLocal.trim()) {
|
||||
overlayBlocks.push('## Standards Overlay (STANDARDS.local.md)\n\n' + standardsLocal.trim());
|
||||
}
|
||||
if (overlayBlocks.length > 0) {
|
||||
parts.push('\n\n# Operator Overlays\n\n' + overlayBlocks.join('\n\n'));
|
||||
}
|
||||
|
||||
// Runtime-specific contract
|
||||
parts.push('\n\n# Runtime-Specific Contract\n\n' + readFileSync(runtimeFile, 'utf-8'));
|
||||
|
||||
return parts.join('\n');
|
||||
}
|
||||
|
||||
/** @deprecated internal alias — use composeContract. Retained for call-site clarity. */
|
||||
function buildRuntimePrompt(runtime: RuntimeName): string {
|
||||
return composeContract(runtime);
|
||||
}
|
||||
|
||||
// ─── Session lock ────────────────────────────────────────────────────────────
|
||||
|
||||
function writeSessionLock(runtime: string): void {
|
||||
@@ -976,6 +1015,22 @@ export function registerLaunchCommands(program: Command): void {
|
||||
launchRuntime(runtime, extraArgs, yolo);
|
||||
});
|
||||
|
||||
// compose-contract — emit the composed runtime contract (base + operator
|
||||
// overlays) for a harness to stdout, without launching. For inspection,
|
||||
// `mosaic doctor`, diffing, and the composer test (R7).
|
||||
program
|
||||
.command('compose-contract <harness>')
|
||||
.description('Print the composed runtime contract (base + *.local overlays) for a harness')
|
||||
.action((harness: string) => {
|
||||
const valid: RuntimeName[] = ['claude', 'codex', 'opencode', 'pi'];
|
||||
if (!valid.includes(harness as RuntimeName)) {
|
||||
console.error(`Unknown harness '${harness}'. Expected one of: ${valid.join(', ')}.`);
|
||||
process.exitCode = 64;
|
||||
return;
|
||||
}
|
||||
process.stdout.write(composeContract(harness as RuntimeName));
|
||||
});
|
||||
|
||||
// Coord (mission orchestrator)
|
||||
program
|
||||
.command('coord')
|
||||
|
||||
Reference in New Issue
Block a user