Splits the 155-line thin-core AGENTS.md into: - defaults/CONSTITUTION.md (L0): gates + integrity + escalation + block-vs-done + mode + two-axis precedence + hooks-are-the-gate + framework-PR firewall + structured-reasoning capability + tier-aware self-load. Capability-verb authored. - defaults/AGENTS.md gutted to an ~80-line load-order dispatcher + guide table (kills the false "already in context, do not re-read" line). - constitution/LAYER-MODEL.md: source-only governance spec (layers + precedence). Non-regression wiring (fresh-install functional; upgrade-safety is P4): - launch.ts injects CONSTITUTION.md before AGENTS.md (tolerant of un-reseeded installs) - install.sh + file-adapter.ts seed CONSTITUTION.md (+ test fixture updated) Runtime adapters: capability-verb the sequential-thinking binding; claude/codex/ opencode restate the REQUIRED hard-stop, pi binds to native thinking (gate=false) — restores the force the adversarial review flagged as weakened. Gate hardening (dual-engine review): identity denylist now covers examples/ (closes the Codex open-source gap), self-test-first, *.json in scope, ci.yml typecheck depends on sanitization (fail-fast), L0 line-count ceiling (<=120). Adversarial gate-preservation review: every original rule traced to L0, the dispatcher, or a routed guide — nothing lost. Refs #542, closes #574 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
136 lines
5.7 KiB
TypeScript
136 lines
5.7 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, readFileSync, existsSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { FileConfigAdapter, DEFAULT_SEED_FILES } from './file-adapter.js';
|
|
|
|
/**
|
|
* Regression tests for the `FileConfigAdapter.syncFramework` seed behavior.
|
|
*
|
|
* Background: the bash installer (`framework/install.sh`) and this TS wizard
|
|
* path both seed framework-contract files from `framework/defaults/` into the
|
|
* user's mosaic home on first install. Before this fix:
|
|
*
|
|
* - The bash installer only seeded `AGENTS.md` and `STANDARDS.md`, leaving
|
|
* `TOOLS.md` missing despite it being listed as mandatory in the
|
|
* AGENTS.md load order (position 5).
|
|
* - The TS wizard iterated every file in `defaults/` and copied it to the
|
|
* mosaic home root — including `defaults/SOUL.md` (hardcoded "Jarvis"),
|
|
* `defaults/USER.md` (placeholder), and internal framework files like
|
|
* `README.md` and `AUDIT-*.md`. That clobbered the identity flow on
|
|
* fresh installs and leaked framework-internal clutter into the user's
|
|
* home directory.
|
|
*
|
|
* This suite pins the whitelist and the preservation semantics so both
|
|
* regressions stay fixed.
|
|
*/
|
|
|
|
function makeFixture(): { sourceDir: string; mosaicHome: string; defaultsDir: string } {
|
|
const root = mkdtempSync(join(tmpdir(), 'mosaic-file-adapter-'));
|
|
const sourceDir = join(root, 'source');
|
|
const mosaicHome = join(root, 'mosaic-home');
|
|
const defaultsDir = join(sourceDir, 'defaults');
|
|
|
|
mkdirSync(defaultsDir, { recursive: true });
|
|
mkdirSync(mosaicHome, { recursive: true });
|
|
|
|
// Framework-contract defaults we expect the wizard to seed.
|
|
writeFileSync(join(defaultsDir, 'CONSTITUTION.md'), '# CONSTITUTION default\n');
|
|
writeFileSync(join(defaultsDir, 'AGENTS.md'), '# AGENTS default\n');
|
|
writeFileSync(join(defaultsDir, 'STANDARDS.md'), '# STANDARDS default\n');
|
|
writeFileSync(join(defaultsDir, 'TOOLS.md'), '# TOOLS default\n');
|
|
|
|
// Non-contract files we must NOT seed on first install.
|
|
writeFileSync(join(defaultsDir, 'SOUL.md'), '# SOUL default (should not be seeded)\n');
|
|
writeFileSync(join(defaultsDir, 'USER.md'), '# USER default (should not be seeded)\n');
|
|
writeFileSync(join(defaultsDir, 'README.md'), '# README (framework-internal)\n');
|
|
writeFileSync(
|
|
join(defaultsDir, 'AUDIT-2026-02-17-framework-consistency.md'),
|
|
'# Audit snapshot\n',
|
|
);
|
|
|
|
return { sourceDir, mosaicHome, defaultsDir };
|
|
}
|
|
|
|
describe('FileConfigAdapter.syncFramework — defaults seeding', () => {
|
|
let fixture: ReturnType<typeof makeFixture>;
|
|
|
|
beforeEach(() => {
|
|
fixture = makeFixture();
|
|
});
|
|
|
|
afterEach(() => {
|
|
rmSync(join(fixture.sourceDir, '..'), { recursive: true, force: true });
|
|
});
|
|
|
|
it('seeds the four framework-contract files on a fresh mosaic home', async () => {
|
|
const adapter = new FileConfigAdapter(fixture.mosaicHome, fixture.sourceDir);
|
|
|
|
await adapter.syncFramework('fresh');
|
|
|
|
for (const name of DEFAULT_SEED_FILES) {
|
|
expect(existsSync(join(fixture.mosaicHome, name))).toBe(true);
|
|
}
|
|
expect(readFileSync(join(fixture.mosaicHome, 'TOOLS.md'), 'utf-8')).toContain(
|
|
'# TOOLS default',
|
|
);
|
|
});
|
|
|
|
it('does NOT seed SOUL.md or USER.md from defaults/ (wizard stages own those)', async () => {
|
|
const adapter = new FileConfigAdapter(fixture.mosaicHome, fixture.sourceDir);
|
|
|
|
await adapter.syncFramework('fresh');
|
|
|
|
// SOUL.md and USER.md live in defaults/ for historical reasons, but they
|
|
// are template-rendered per-user by the wizard stages. Seeding them here
|
|
// would clobber the identity flow and leak placeholder content.
|
|
expect(existsSync(join(fixture.mosaicHome, 'SOUL.md'))).toBe(false);
|
|
expect(existsSync(join(fixture.mosaicHome, 'USER.md'))).toBe(false);
|
|
});
|
|
|
|
it('does NOT seed README.md or AUDIT-*.md from defaults/', async () => {
|
|
const adapter = new FileConfigAdapter(fixture.mosaicHome, fixture.sourceDir);
|
|
|
|
await adapter.syncFramework('fresh');
|
|
|
|
expect(existsSync(join(fixture.mosaicHome, 'README.md'))).toBe(false);
|
|
expect(existsSync(join(fixture.mosaicHome, 'AUDIT-2026-02-17-framework-consistency.md'))).toBe(
|
|
false,
|
|
);
|
|
});
|
|
|
|
it('preserves existing contract files — never overwrites user customization', async () => {
|
|
// Also plant a root-level AGENTS.md in sourceDir so that `syncDirectory`
|
|
// itself (not just the seed loop) has something to try to overwrite.
|
|
// Without this, the test would silently pass even if preserve semantics
|
|
// were broken in syncDirectory.
|
|
writeFileSync(join(fixture.sourceDir, 'AGENTS.md'), '# shipped AGENTS from source root\n');
|
|
|
|
writeFileSync(join(fixture.mosaicHome, 'TOOLS.md'), '# user-customized TOOLS\n');
|
|
writeFileSync(join(fixture.mosaicHome, 'AGENTS.md'), '# user-customized AGENTS\n');
|
|
|
|
const adapter = new FileConfigAdapter(fixture.mosaicHome, fixture.sourceDir);
|
|
await adapter.syncFramework('keep');
|
|
|
|
expect(readFileSync(join(fixture.mosaicHome, 'TOOLS.md'), 'utf-8')).toBe(
|
|
'# user-customized TOOLS\n',
|
|
);
|
|
expect(readFileSync(join(fixture.mosaicHome, 'AGENTS.md'), 'utf-8')).toBe(
|
|
'# user-customized AGENTS\n',
|
|
);
|
|
// And the missing contract file still gets seeded.
|
|
expect(readFileSync(join(fixture.mosaicHome, 'STANDARDS.md'), 'utf-8')).toContain(
|
|
'# STANDARDS default',
|
|
);
|
|
});
|
|
|
|
it('is a no-op for seeding when defaults/ dir does not exist', async () => {
|
|
rmSync(fixture.defaultsDir, { recursive: true });
|
|
|
|
const adapter = new FileConfigAdapter(fixture.mosaicHome, fixture.sourceDir);
|
|
await expect(adapter.syncFramework('fresh')).resolves.toBeUndefined();
|
|
|
|
expect(existsSync(join(fixture.mosaicHome, 'TOOLS.md'))).toBe(false);
|
|
});
|
|
});
|