mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
Plan J3 / spec §23.3.2 patch 2 / §23.4.
Lays the substrate slice for migrating prompt fragments out of
`apps/daemon/src/prompts/system.ts` and into the bundled atom
SKILL.md bodies registered by §3.I3.
apps/daemon/src/plugins/atom-bodies.ts owns the daemon-side loader:
loadAtomBodies(db, atomIds) → AtomBodyEntry[]
The function looks each atom id up in installed_plugins (bundled
rows win), reads the matching fsPath/SKILL.md, strips
front-matter, and returns the raw body. Atoms with no installed
plugin or unreadable SKILL.md are silently skipped — the caller
drops empty entries from the prompt.
packages/contracts/src/prompts/atom-block.ts ships the pure
renderer:
renderActiveStageBlock({ stageId, bodies, iteration? }) → string
Mirrors spec §23.4's composeSystemPrompt sketch. Empty bodies
return ''; multiple bodies are separated by '---' with no trailing
separator. Lives in contracts so the daemon-side composer and any
future contracts-side composer share one definition (§11.8 PB1
single-import guarantee).
The composeSystemPrompt() rewiring itself is the next PR — this
commit gives that PR zero scaffolding to build: the helpers are
reachable, tested, and the bundled atom plugins from §3.I3 already
have the matching SKILL.md bodies on disk.
Tests: contracts 8 → 12 (+4 cases on atom-block); daemon
1482 → 1486 (+4 cases on plugins-atom-bodies covering the
end-to-end loadAtomBodies → renderActiveStageBlock path).
Co-authored-by: Tom Huang <1043269994@qq.com>
94 lines
4 KiB
TypeScript
94 lines
4 KiB
TypeScript
// Phase 4 / spec §23.3.2 patch 2 — atom SKILL.md body loader.
|
|
//
|
|
// The substrate slice for lifting `composeSystemPrompt`'s prompt
|
|
// constants into the bundled atom plugins. The daemon-side helper
|
|
// reads `<bundled-fsPath>/SKILL.md` and strips frontmatter; the
|
|
// pure renderer in @open-design/contracts then assembles the stage
|
|
// prompt block. This test pins both halves of the contract so a
|
|
// future PR that lifts system.ts has zero scaffolding to build.
|
|
|
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import Database from 'better-sqlite3';
|
|
import { migratePlugins } from '../src/plugins/persistence.js';
|
|
import { registerBundledPlugins } from '../src/plugins/bundled.js';
|
|
import { loadAtomBodies } from '../src/plugins/atom-bodies.js';
|
|
import { renderActiveStageBlock } from '@open-design/contracts';
|
|
|
|
const SAMPLE_MANIFEST = (id: string) =>
|
|
JSON.stringify({
|
|
$schema: 'https://open-design.ai/schemas/plugin.v1.json',
|
|
name: id,
|
|
title: id,
|
|
version: '0.1.0',
|
|
description: `${id} fixture`,
|
|
license: 'MIT',
|
|
od: { kind: 'atom', capabilities: ['prompt:inject'] },
|
|
});
|
|
|
|
const SAMPLE_SKILL = (id: string, body: string) =>
|
|
`---\nname: ${id}\ndescription: ${id} fixture\n---\n\n# ${id}\n\n${body}\n`;
|
|
|
|
let db: Database.Database;
|
|
let tmpRoot: string;
|
|
|
|
beforeEach(async () => {
|
|
tmpRoot = await mkdtemp(path.join(os.tmpdir(), 'od-atom-bodies-'));
|
|
db = new Database(':memory:');
|
|
db.exec(`
|
|
CREATE TABLE projects (id TEXT PRIMARY KEY, name TEXT);
|
|
CREATE TABLE conversations (id TEXT PRIMARY KEY, project_id TEXT, title TEXT);
|
|
`);
|
|
migratePlugins(db);
|
|
|
|
// Build a minimal bundled root with two atom plugins so the loader has
|
|
// something to find.
|
|
const atomA = path.join(tmpRoot, 'atoms', 'discovery-question-form');
|
|
const atomB = path.join(tmpRoot, 'atoms', 'todo-write');
|
|
await mkdir(atomA, { recursive: true });
|
|
await mkdir(atomB, { recursive: true });
|
|
await writeFile(path.join(atomA, 'open-design.json'), SAMPLE_MANIFEST('discovery-question-form'));
|
|
await writeFile(path.join(atomA, 'SKILL.md'), SAMPLE_SKILL('discovery-question-form', 'Ask the user about audience.'));
|
|
await writeFile(path.join(atomB, 'open-design.json'), SAMPLE_MANIFEST('todo-write'));
|
|
await writeFile(path.join(atomB, 'SKILL.md'), SAMPLE_SKILL('todo-write', 'Commit a numbered plan.'));
|
|
|
|
await registerBundledPlugins({ db, bundledRoot: tmpRoot });
|
|
});
|
|
|
|
afterEach(async () => {
|
|
db.close();
|
|
await rm(tmpRoot, { recursive: true, force: true });
|
|
});
|
|
|
|
describe('loadAtomBodies', () => {
|
|
it('reads SKILL.md bodies for bundled atoms (frontmatter stripped)', async () => {
|
|
const out = await loadAtomBodies(db, ['discovery-question-form', 'todo-write']);
|
|
expect(out.map((e) => e.atomId)).toEqual(['discovery-question-form', 'todo-write']);
|
|
expect(out[0]!.body).toContain('# discovery-question-form');
|
|
expect(out[0]!.body).toContain('Ask the user about audience.');
|
|
expect(out[0]!.body.startsWith('---')).toBe(false);
|
|
});
|
|
|
|
it('skips ids without an installed plugin or readable SKILL.md', async () => {
|
|
const out = await loadAtomBodies(db, ['unknown-atom', 'todo-write']);
|
|
expect(out.map((e) => e.atomId)).toEqual(['todo-write']);
|
|
});
|
|
|
|
it('returns an empty array for an empty input', async () => {
|
|
expect(await loadAtomBodies(db, [])).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('renderActiveStageBlock + loadAtomBodies (end-to-end stage block)', () => {
|
|
it('builds a `## Active stage` header followed by every atom body', async () => {
|
|
const bodies = await loadAtomBodies(db, ['discovery-question-form', 'todo-write']);
|
|
const block = renderActiveStageBlock({ stageId: 'plan', bodies });
|
|
expect(block).toContain('## Active stage: plan');
|
|
expect(block).toContain('### discovery-question-form');
|
|
expect(block).toContain('Ask the user about audience.');
|
|
expect(block).toContain('### todo-write');
|
|
expect(block).toContain('Commit a numbered plan.');
|
|
});
|
|
});
|