chore: add ion package, codesight wiki, work plans, ascli config

New @boocode/ion package (v0.0.1) for inference optimization network.
.codesight/ wiki artifacts for codebase documentation.
.omo/ work plans for openspec cleanup and enhanced file panel.
This commit is contained in:
2026-06-07 22:16:45 +00:00
parent ec48066a80
commit 02063072ab
63 changed files with 14025 additions and 0 deletions

View File

@@ -0,0 +1,22 @@
/**
* Ion workflow engine — format module.
*
* Re-exports the SOP markdown parser, YAML converter, and file discovery
* utilities so consumers can import from a single entry point:
*
* ```ts
* import { parseSopContent, convertSopToWorkflowYaml, discoverSopFiles } from '@boocode/ion/format';
* ```
*/
export {
parseSopContent,
type SopDocument,
type SopParameter,
type SopStep,
} from './sop-parser.js';
export { convertSopToWorkflowYaml } from './sop-to-yaml.js';
export { discoverSopFiles } from './sop-discovery.js';
export type { GlobFn } from './sop-discovery.js';

View File

@@ -0,0 +1,78 @@
/**
* SOP file discovery for the Ion workflow engine.
*
* Locates `.sop.md` files by delegating file-system traversal to a
* caller-provided glob function. This keeps the module pure (no Node
* dependency) and easily testable.
*/
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
/**
* A function that resolves a glob pattern to an array of absolute paths.
*
* The caller typically provides an implementation backed by `node:fs/promises`
* or a test double.
*/
export type GlobFn = (pattern: string) => Promise<string[]>;
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
/** Default search directories (in priority order, relative to cwd). */
const SEARCH_DIRS = ['.archon/workflows', '.'];
/** Glob pattern for SOP markdown files. */
const SOP_GLOB = '**/*.sop.md';
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/**
* Discover all `.sop.md` files in the given working directory.
*
* Searches `.archon/workflows/` first, then the project root, and returns
* absolute paths to every matching file. Duplicate paths are de-duplicated.
*
* @param cwd - The working directory to search from.
* @param globFn - A function that resolves a glob pattern to file paths.
* Typically backed by `glob` from `node:fs/promises` or a
* similar library.
* @returns An array of absolute file paths to discovered `.sop.md` files,
* sorted deterministically.
*/
export async function discoverSopFiles(
cwd: string,
globFn: GlobFn,
): Promise<string[]> {
const seen = new Set<string>();
const results: string[] = [];
for (const dir of SEARCH_DIRS) {
const pattern =
dir === '.' ? `${cwd}/${SOP_GLOB}` : `${cwd}/${dir}/${SOP_GLOB}`;
let paths: string[];
try {
paths = await globFn(pattern);
} catch {
// Glob errors (e.g. directory doesn't exist) are non-fatal.
continue;
}
// Sort for deterministic output and de-duplicate
paths.sort();
for (const p of paths) {
if (!seen.has(p)) {
seen.add(p);
results.push(p);
}
}
}
return results;
}

View File

@@ -0,0 +1,203 @@
/**
* SOP Markdown parser for the Ion workflow engine.
*
* Parses `.sop.md` files (Agent SOP format) into structured `SopDocument`
* objects that can be converted to YAML workflow definitions.
*/
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
/** A single parameter declared in the SOP's Parameters section. */
export interface SopParameter {
/** Parameter name (camelCase by convention). */
name: string;
/** Whether the parameter is required or optional. */
type: 'required' | 'optional';
/** Default value (only present when type is 'optional'). */
default?: string;
/** Human-readable description of the parameter. */
description: string;
}
/** A single step declared in the SOP's Steps section. */
export interface SopStep {
/** Step number (1-based). */
number: number;
/** Short human-readable step name. */
name: string;
/** Full body text of the step (may be multi-line). */
body: string;
/** Constraints text extracted from the step, if any. */
constraints?: string;
}
/** The fully-parsed SOP document. */
export interface SopDocument {
/** Title extracted from the first `# heading`. */
title: string;
/** Overview section content. */
overview: string;
/** Parsed parameters (empty array if section absent). */
parameters: SopParameter[];
/** Parsed steps (empty array if section absent). */
steps: SopStep[];
/** Optional examples section content. */
examples?: string;
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/**
* Extract a section body from markdown text.
*
* A section starts with `## <Title>` and ends at the next `## ` or `# `
* heading (or end of string).
*/
function extractSection(markdown: string, heading: string): string | null {
const pattern = new RegExp(
`^##\\s+${escapeRegex(heading)}\\s*\\n([\\s\\S]*?)(?=\\n##|\\n#|$)`,
'm',
);
const match = markdown.match(pattern);
return match?.[1]?.trim() ?? null;
}
/** Escape special regex characters in a literal string. */
function escapeRegex(str: string): string {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
// ---------------------------------------------------------------------------
// Section parsers
// ---------------------------------------------------------------------------
/** Parse the Parameters section into structured `SopParameter` objects. */
function parseParameters(raw: string): SopParameter[] {
const parameters: SopParameter[] = [];
// Match lines like: - **paramName** (required): Description here
// - **paramName** (optional, default: value): Description here
const paramRegex =
/^-\s+\*\*(\w+)\*\*\s+\((required|optional)(?:,\s*default:\s*([^)]+))?\):\s+(.+)$/gm;
let match: RegExpExecArray | null;
while ((match = paramRegex.exec(raw)) !== null) {
const name = match[1]!;
const type = match[2]! as 'required' | 'optional';
const defaultVal = match[3]; // may be undefined (optional group)
const description = match[4]!;
const param: SopParameter = {
name,
type,
description,
};
if (defaultVal !== undefined) {
param.default = defaultVal.trim();
}
parameters.push(param);
}
return parameters;
}
/** Parse the Steps section into structured `SopStep` objects. */
function parseSteps(raw: string): SopStep[] {
const steps: SopStep[] = [];
// Find all ### sub-headings like "### 1. Step Name"
const stepHeadingRegex = /^###\s+(\d+)\.\s+(.+)$/gm;
// Collect heading positions: [startIndex, endIndex, number, name]
const headings: { number: number; name: string; start: number; end: number }[] = [];
let match: RegExpExecArray | null;
while ((match = stepHeadingRegex.exec(raw)) !== null) {
headings.push({
number: parseInt(match[1]!, 10),
name: match[2]!.trim(),
start: match.index,
end: -1, // filled in below
});
}
// Set end positions: each heading ends where the next one starts, or at EOF
for (let i = 0; i < headings.length; i++) {
const heading = headings[i]!;
heading.end =
i + 1 < headings.length ? headings[i + 1]!.start : raw.length;
}
for (const heading of headings) {
// The body starts after the heading line itself
const headingLineEnd = raw.indexOf('\n', heading.start);
const bodyStart = headingLineEnd === -1 ? raw.length : headingLineEnd + 1;
const sectionText = raw.slice(bodyStart, heading.end).trim();
// Extract constraints if present
const constraintsMatch = sectionText.match(
/\*\*Constraints:\*\*\s*\n([\s\S]*?)(?=\n###|\n##|$)/,
);
const constraints = constraintsMatch?.[1]?.trim();
// Body is everything before the Constraints heading (or the whole text)
let body: string;
if (constraintsMatch?.index !== undefined) {
body = sectionText.slice(0, constraintsMatch.index).trim();
} else {
body = sectionText;
}
steps.push({
number: heading.number,
name: heading.name,
body,
...(constraints ? { constraints } : {}),
});
}
return steps;
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/**
* Parse a `.sop.md` markdown string into a structured `SopDocument`.
*
* @param markdown - The raw markdown content of a `.sop.md` file.
* @returns A parsed `SopDocument` with title, overview, parameters, steps,
* and optional examples.
*/
export function parseSopContent(markdown: string): SopDocument {
// --- Title (first h1) ---
const titleMatch = markdown.match(/^#\s+(.+)$/m);
const title = titleMatch?.[1]?.trim() ?? 'Untitled SOP';
// --- Overview ---
const overviewRaw = extractSection(markdown, 'Overview');
const overview = overviewRaw ?? '';
// --- Parameters ---
const parametersRaw = extractSection(markdown, 'Parameters');
const parameters = parametersRaw ? parseParameters(parametersRaw) : [];
// --- Steps ---
const stepsRaw = extractSection(markdown, 'Steps');
const steps = stepsRaw ? parseSteps(stepsRaw) : [];
// --- Examples (optional) ---
const examplesRaw = extractSection(markdown, 'Examples');
return {
title,
overview,
parameters,
steps,
...(examplesRaw !== null ? { examples: examplesRaw } : {}),
};
}

View File

@@ -0,0 +1,102 @@
/**
* SOP-to-YAML converter for the Ion workflow engine.
*
* Converts a parsed `SopDocument` into a YAML workflow definition string
* that can be fed back into the Ion YAML loader.
*/
import type { SopDocument } from './sop-parser.js';
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/** Convert a title string to kebab-case for use as a YAML identifier. */
function toKebabCase(title: string): string {
return title
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-') // non-alphanumeric → hyphen
.replace(/^-+|-+$/g, ''); // strip leading/trailing hyphens
}
/**
* Indent every line of a multi-line string by the given number of spaces.
* Empty lines are preserved without extra indentation.
*/
function indentBlock(text: string, spaces: number): string {
const prefix = ' '.repeat(spaces);
return text
.split('\n')
.map((line) => (line.length > 0 ? prefix + line : line))
.join('\n');
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/**
* Convert a parsed `SopDocument` into a YAML workflow definition string.
*
* The output is valid YAML that can be loaded by the Ion YAML loader.
* Steps become sequential prompt nodes with `depends_on` linking.
* Constraints are appended to the prompt body as plain text.
* Only `prompt:` nodes are emitted — SOP is conversation-only.
*
* @param sop - The parsed SOP document.
* @returns A YAML string representing the workflow.
*/
export function convertSopToWorkflowYaml(sop: SopDocument): string {
const name = toKebabCase(sop.title);
const lines: string[] = [];
// --- Header comment with parameter info ---
if (sop.parameters.length > 0) {
lines.push('# Parameters:');
for (const param of sop.parameters) {
const tag = param.type === 'required' ? 'required' : 'optional';
const defaultPart = param.default ? `, default: ${param.default}` : '';
lines.push(`# ${param.name} (${tag}${defaultPart}): ${param.description}`);
}
lines.push('');
}
// --- Top-level fields ---
lines.push(`name: ${name}`);
lines.push(`description: |`);
lines.push(indentBlock(sop.overview || 'No description provided.', 2));
lines.push('');
// --- Nodes ---
lines.push('nodes:');
for (let i = 0; i < sop.steps.length; i++) {
const step = sop.steps[i]!;
const stepId = `step_${step.number}`;
const isFirst = i === 0;
// Build the prompt body: step body + constraints (if any)
let promptBody = step.body;
if (step.constraints) {
promptBody += `\n\nConstraints:\n${step.constraints}`;
}
lines.push(` - id: ${stepId}`);
lines.push(` prompt: |`);
lines.push(indentBlock(promptBody, 6));
if (isFirst) {
lines.push(` depends_on: []`);
} else {
const prevStep = sop.steps[i - 1]!;
lines.push(` depends_on: [step_${prevStep.number}]`);
}
// Blank line between nodes (but not after the last one)
if (i < sop.steps.length - 1) {
lines.push('');
}
}
return lines.join('\n') + '\n';
}