mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-10-02 04:43:58 +00:00
Implement OpenSpec artifacts and SDK contracts
This commit is contained in:
parent
1aa67367b8
commit
17942eef57
16 changed files with 486 additions and 21 deletions
176
packages/cli/src/openspec.ts
Normal file
176
packages/cli/src/openspec.ts
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
||||
import { basename, join } from "node:path";
|
||||
|
||||
export interface OpenSpecChangeInput {
|
||||
id: string;
|
||||
title: string;
|
||||
summary: string;
|
||||
capability: string;
|
||||
repo?: string;
|
||||
agents?: string[];
|
||||
outDir?: string;
|
||||
tasks?: string[];
|
||||
}
|
||||
|
||||
export interface OpenSpecImportSummary {
|
||||
root: string;
|
||||
specs: Array<{ id: string; path: string; title: string }>;
|
||||
changes: Array<{ id: string; path: string; files: string[] }>;
|
||||
}
|
||||
|
||||
export function writeOpenSpecChange(input: OpenSpecChangeInput) {
|
||||
const root = input.outDir ?? join("openspec", "changes", input.id);
|
||||
const specRoot = join(root, "specs", input.capability);
|
||||
mkdirSync(specRoot, { recursive: true });
|
||||
|
||||
const tasks = input.tasks?.length ? input.tasks : [
|
||||
"Review existing LogicSRC schemas and CLI conventions.",
|
||||
"Open the AgentSwarm master agent session.",
|
||||
"Assign slave agents to reproduction, patching, review, and evidence.",
|
||||
"Export audit artifacts back into LogicSRC run/event documents."
|
||||
];
|
||||
|
||||
const proposal = `# ${input.title}
|
||||
|
||||
## Summary
|
||||
|
||||
${input.summary}
|
||||
|
||||
## Repository
|
||||
|
||||
${input.repo ?? "not specified"}
|
||||
|
||||
## Compatibility
|
||||
|
||||
This change uses OpenSpec.dev-style repo-local planning artifacts while preserving LogicSRC task, agent, run, event, plugin, SDK, MCP, CLI, TUI, PWA, and API contract names.
|
||||
`;
|
||||
|
||||
const design = `# ${input.title} Design
|
||||
|
||||
## Architecture
|
||||
|
||||
- A master agent owns the session goal, policy, and final decision packet.
|
||||
- Slave agents execute scoped work such as reproduction, patching, review, documentation, and release evidence.
|
||||
- LogicSRC records agent runs, model/tool use, artifacts, audit events, and schema versions.
|
||||
|
||||
## OpenSpec Compatibility
|
||||
|
||||
OpenSpec-compatible files stay under \`openspec/changes/${input.id}\` and can be reviewed through normal git workflows.
|
||||
`;
|
||||
|
||||
const taskList = tasks.map((task, index) => `- [ ] ${index + 1}. ${task}`).join("\n");
|
||||
const spec = `# ${input.capability} Specification
|
||||
|
||||
## Purpose
|
||||
|
||||
${input.summary}
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Master agent session
|
||||
|
||||
The system SHALL create a master agent session that can coordinate scoped slave agents.
|
||||
|
||||
#### Scenario: YOLO AgentSwarm session
|
||||
|
||||
- GIVEN a repository target${input.repo ? ` of \`${input.repo}\`` : ""}
|
||||
- WHEN \`logicsrc --openspec agentswarm --yolo\` runs
|
||||
- THEN create OpenSpec-compatible proposal, design, task, and spec-delta artifacts
|
||||
- AND keep LogicSRC audit and run contracts available for export
|
||||
|
||||
### Requirement: Slave agent coordination
|
||||
|
||||
The system SHALL record slave agent roles for reproducible review.
|
||||
|
||||
#### Scenario: Declared slave agents
|
||||
|
||||
- GIVEN slave agents ${input.agents?.length ? input.agents.map((agent) => `\`${agent}\``).join(", ") : "are declared"}
|
||||
- WHEN the session opens
|
||||
- THEN include those roles in the LogicSRC session output
|
||||
- AND include them in the repo-local OpenSpec artifacts
|
||||
`;
|
||||
|
||||
const files = [
|
||||
{ path: join(root, "proposal.md"), content: proposal },
|
||||
{ path: join(root, "design.md"), content: design },
|
||||
{ path: join(root, "tasks.md"), content: `# ${input.title} Tasks\n\n${taskList}\n` },
|
||||
{ path: join(specRoot, "spec.md"), content: spec }
|
||||
];
|
||||
|
||||
for (const file of files) {
|
||||
writeFileSync(file.path, file.content);
|
||||
}
|
||||
|
||||
return {
|
||||
id: input.id,
|
||||
root,
|
||||
files: files.map((file) => file.path)
|
||||
};
|
||||
}
|
||||
|
||||
export function importOpenSpec(root = "openspec"): OpenSpecImportSummary {
|
||||
const specsRoot = join(root, "specs");
|
||||
const changesRoot = join(root, "changes");
|
||||
const specs = existsSync(specsRoot)
|
||||
? listDirectories(specsRoot).map((directory) => {
|
||||
const specPath = join(specsRoot, directory, "spec.md");
|
||||
return {
|
||||
id: directory,
|
||||
path: specPath,
|
||||
title: readMarkdownTitle(specPath) ?? directory
|
||||
};
|
||||
})
|
||||
: [];
|
||||
|
||||
const changes = existsSync(changesRoot)
|
||||
? listDirectories(changesRoot).map((directory) => {
|
||||
const changePath = join(changesRoot, directory);
|
||||
return {
|
||||
id: directory,
|
||||
path: changePath,
|
||||
files: listFiles(changePath).map((file) => file.slice(changePath.length + 1))
|
||||
};
|
||||
})
|
||||
: [];
|
||||
|
||||
return { root, specs, changes };
|
||||
}
|
||||
|
||||
export function exportOpenSpecSummary(summary: OpenSpecImportSummary, outFile: string) {
|
||||
const markdown = `# OpenSpec Import Summary
|
||||
|
||||
Root: \`${summary.root}\`
|
||||
|
||||
## Specs
|
||||
|
||||
${summary.specs.length ? summary.specs.map((spec) => `- \`${spec.id}\` — ${spec.title} (${spec.path})`).join("\n") : "- none"}
|
||||
|
||||
## Changes
|
||||
|
||||
${summary.changes.length ? summary.changes.map((change) => `- \`${change.id}\` — ${change.files.length} file(s) (${change.path})`).join("\n") : "- none"}
|
||||
`;
|
||||
|
||||
writeFileSync(outFile, markdown);
|
||||
return outFile;
|
||||
}
|
||||
|
||||
function listDirectories(root: string) {
|
||||
return readdirSync(root).filter((entry) => statSync(join(root, entry)).isDirectory()).sort();
|
||||
}
|
||||
|
||||
function listFiles(root: string): string[] {
|
||||
const entries = readdirSync(root);
|
||||
return entries.flatMap((entry) => {
|
||||
const path = join(root, entry);
|
||||
return statSync(path).isDirectory() ? listFiles(path) : [path];
|
||||
}).sort();
|
||||
}
|
||||
|
||||
function readMarkdownTitle(path: string) {
|
||||
if (!existsSync(path)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const line = readFileSync(path, "utf8").split(/\r?\n/).find((entry) => entry.startsWith("# "));
|
||||
return line ? line.replace(/^#\s+/, "").trim() : basename(path);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue