* feat(cli): migrate packages/cli to Bun-first with full test suite
- Replace fs-extra with Bun.file/Bun.write/node:fs/promises across all 9 source files
- Fix --help fast-path so all 8 subcommands are visible
- Replace tsup/tsx/vitest with bun build/bun run/bun:test
- Replace __dirname with import.meta.dir; use JSON import assertion for version
- Fix hardcoded OAC_VERSION in add.ts; add hashesMatch import to status.ts
- Replace computeFileHash with Bun.file().bytes() in sha256.ts
- Rename checkNodeVersion → checkBunVersion; parallelise doctor checks
- Fix TypeScript void-inference errors in update.ts, list.ts, status.ts
by replacing .catch() with let/try-catch where result is used downstream
- Add ManifestError named error class; remove unsafe type casts
- Add 43 bun:test unit tests (sha256, manifest, installer, version) — 0 failures
* fix(cli): resolve critical bugs and standards violations found in review
Critical bug fixes:
- ide-detect.ts: replace Bun.file(dir).exists() with stat().isDirectory()
for all directory checks — Bun.file().exists() always returns false for
directories, breaking Cursor/Windsurf/OpenCode/Claude detection entirely
- bundled.ts: add registry.json exclusion anchor to findPackageRoot() to
prevent monorepo root from matching before the CLI package root
Standards cleanup (§4.1, §5.1, §6.1, §15.3, §21.1):
- status.ts: parallelise findModifiedFiles + detectIdes with Promise.all
- apply.ts: fix duplicate warn/limit messages; remove else after return in
reportWarnings; remove redundant mkdir before Bun.write
- version.ts: remove unnecessary (pkgJson as {version?:string}) cast
- manifest.ts: remove redundant mkdir before Bun.write; remove as unknown cast
- installer.ts: remove redundant mkdir calls before Bun.write throughout
- add.ts: add comment explaining why node:fs/promises rm is used
Tests (43 → 142, +99 new tests across 4 new files):
- ide-detect.test.ts: 26 tests covering all 4 IDEs, both claude indicators,
detectIdes parallel, isIdePresent — directly validates the directory fix
- bundled.test.ts: 27 tests for classifyBundledFile, findPackageRoot,
listBundledFiles, getBundledFilePath, bundledFileExists
- config.test.ts: 25 tests for readConfig/writeConfig round-trips,
createDefaultConfig, mergeConfig, isYoloMode, isAutoBackup
- installer-update.test.ts: 14 tests covering all 5 updateFiles decision
branches (install/update/skip/yolo/dry-run) plus isProjectRoot
- sha256.test.ts: +4 tests for empty file, large file, binary content
* fix(cli): fix P0 bugs — global flags, .git detection, package root resolution
- Remove duplicate --dry-run/--yolo/--verbose from parent program; Commander.js
global option stealing caused all safety flags to be silently dropped in every
subcommand action callback
- Fix isProjectRoot() to use stat() for .git detection; Bun.file().exists()
returns false for directories, breaking oac init in standard git repos
- Add OAC_PACKAGE_ROOT env var override to getPackageRoot() for dev/monorepo mode;
registry.json heuristic excluded the repo root causing oac init/add/update to
throw when run from source
- Fix build script: remove --banner flag that caused double shebang in dist/index.js
- Rewrite bin/oac.js to invoke bun instead of node (dist is bun-only target)
- Refactor installer.ts let accumulators to const using Promise.all + reduce
- Add MVP planning docs (00-MVP-PLAN.md, master synthesis, project breakdown)
* fix(install.sh): handle both singular and plural component type formats
The get_registry_key() function was always adding 's' to the type,
causing 'contexts' to become 'contextss' which doesn't exist in registry.json.
Now handles:
- Singular forms: context → contexts, agent → agents, skill → skills
- Plural forms: contexts → contexts (unchanged), agents → agents
- Config stays singular
- Fallback for any type ending in 's'
Fixes #257
45 KiB
OAC Package Refactor — Project Breakdown
Document: 13-PROJECT-BREAKDOWN.md
GitHub Issue: #206
Status: Approved for Implementation
Date: 2026-02-19
Based on: 12-MASTER-SYNTHESIS.md + Architecture Review (CodeReviewer)
Architecture Review Key Finding: The synthesis plan is 75% solid. The critical gap is a missing provider/adapter pattern for non-IDE subsystems (context, task management, registry). This must be built in Project 1 before anything else. It enables users to swap out any subsystem — including the context system and task management — with their own implementations.
Overview: 6 Discrete Projects
| # | Project | What It Builds | Depends On |
|---|---|---|---|
| P1 | @nextsystems/oac-core |
Shared interfaces, schemas, provider contracts | Nothing |
| P2 | @nextsystems/oac-cli |
Full CLI (commander.js, all commands, config, lockfile) | P1 |
| P3 | Context System | 6-layer resolver, bundle/manifest, auto-update | P1, P2 |
| P4 | Agent & Skill Management | agent.json+prompt.md, presets, skills packaging | P1, P2 |
| P5 | Plugin System | OpenCode TS plugin, Claude Code rewrite, Cursor/Windsurf | P1, P2, P4 |
| P6 | Registry & Community | shadcn registry, oac.lock, community publishing | P1, P2 |
Projects P3–P6 can be worked in parallel once P1 and P2 are complete.
Project 1: @nextsystems/oac-core — Shared Interfaces & Provider Contracts
Purpose: Zero-dependency package containing all TypeScript interfaces, Zod schemas, and provider contracts. Every other package depends on this. Users implement these interfaces to swap subsystems.
Why first: Without this, schemas diverge between packages (already happening: AgentFrontmatterSchema in compatibility-layer vs planned AgentSchema in CLI). Fixes the dual-source-of-truth problem identified in the review.
What to Build
1.1 Provider Interfaces (The Extensibility Layer)
These are the interfaces users implement to replace OAC's default subsystems:
// src/providers/context.ts
export interface IContextProvider {
readonly id: string;
readonly displayName: string;
resolve(name: string): Promise<ContextFile | null>;
list(query?: ContextQuery): Promise<ContextFile[]>;
install(name: string, source: string | Buffer): Promise<void>;
update(name: string, newContent: string, expectedSha256: string): Promise<'updated' | 'skipped' | 'conflict'>;
isModified(name: string, installedSha256: string): Promise<boolean>;
validate(name: string): Promise<ValidationResult>;
}
// src/providers/task-management.ts
export interface ITaskManagementProvider {
readonly id: string;
readonly displayName: string;
createSession(tasks: Omit<Task, 'id'>[]): Promise<TaskSession>;
getCurrentSession(): Promise<TaskSession | null>;
getNextTask(sessionId: string): Promise<Task | null>;
completeTask(sessionId: string, taskId: string): Promise<void>;
listSessions(): Promise<TaskSession[]>;
cleanSessions(olderThanDays: number): Promise<number>;
}
// src/providers/registry.ts
export interface IRegistryProvider {
readonly id: string;
readonly displayName: string;
readonly baseUrl: string;
fetch(name: string, type: ComponentType): Promise<RegistryItem>;
search(query: string, options?: RegistrySearchOptions): Promise<RegistryItem[]>;
download(item: RegistryItem): Promise<Array<{ file: RegistryFile; content: string }>>;
ping(): Promise<boolean>;
getLatestVersion(name: string, type: ComponentType): Promise<string>;
}
// src/providers/ide-adapter.ts
export interface IIDEAdapter {
readonly id: string;
readonly displayName: string;
fromOAC(agent: OACAgent, context: OACContext[]): Promise<IDEAdapterResult>;
toOAC(source: string): Promise<OACAgent>;
getOutputPath(): string;
getCapabilities(): IDECapabilities;
validate(output: IDEAdapterResult): ValidationResult;
}
// src/providers/agent-profile.ts
export interface IAgentProfileProvider {
readonly id: string;
readonly displayName: string;
list(): Promise<AgentProfile[]>;
get(name: string): Promise<AgentProfile | null>;
has(name: string): Promise<boolean>;
}
1.2 Canonical Zod Schemas
One schema per concept, used by all packages:
// src/schemas/agent.ts — The canonical agent schema
export const AgentConfigSchema = z.object({
name: z.string().regex(/^[a-z][a-z0-9-]*$/),
displayName: z.string(),
version: z.string(),
description: z.string(),
model: z.string().optional(),
temperature: z.number().min(0).max(1).optional(),
maxSteps: z.number().optional(),
mode: z.enum(['primary', 'subagent', 'all']).optional(),
permission: z.array(PermissionRuleSchema).optional(),
tools: z.array(z.string()).optional(),
skills: z.array(z.string()).optional(),
oac: z.object({
bundledSha256: z.string().optional(),
installedAt: z.string().optional(),
source: z.enum(['registry', 'bundled', 'local']),
presetApplied: z.string().optional(),
tags: z.array(z.string()).default([]),
category: z.string().optional(),
}).optional(),
});
// src/schemas/config.ts — Global + project config
// src/schemas/lockfile.ts — oac.lock format
// src/schemas/registry.ts — registry.json + registry item format
// src/schemas/skill.ts — SKILL.md frontmatter
// src/schemas/context.ts — Context file frontmatter
// src/schemas/manifest.ts — manifest.json (npm bundle inventory)
1.3 Provider Registry (Wiring Layer)
// src/provider-registry.ts
export class ProviderRegistry {
private contextProvider: IContextProvider;
private taskProvider: ITaskManagementProvider;
private registryProviders: Map<string, IRegistryProvider>;
private ideAdapters: Map<string, IIDEAdapter>;
private agentProfileProviders: Map<string, IAgentProfileProvider>;
constructor(config: OACConfig) { /* initialize defaults */ }
async loadFromConfig(config: OACConfig): Promise<void> {
// Dynamically import custom providers from config.providers.*
// Supports: npm package name, local path, or URL (for registry)
}
getContextProvider(): IContextProvider { ... }
getTaskProvider(): ITaskManagementProvider { ... }
getRegistry(name?: string): IRegistryProvider { ... }
getAllRegistries(): IRegistryProvider[] { ... }
getIDEAdapter(id: string): IIDEAdapter | undefined { ... }
}
1.4 Shared Types
// src/types/index.ts
export type ComponentType = 'agent' | 'skill' | 'context' | 'plugin';
export type UpdateMode = 'manual' | 'auto-safe' | 'auto-all' | 'locked';
export type ConflictStrategy = 'ask' | 'skip' | 'overwrite' | 'backup' | 'yolo';
export type InstallLocation = 'local' | 'global';
export interface ValidationResult {
valid: boolean;
errors: string[];
warnings: string[];
}
export interface ContextFile {
name: string;
content: string;
path: string;
layer: string;
userOwned: boolean;
sha256: string;
}
export interface OACAgent {
config: AgentConfig;
promptMd: string;
systemMd?: string;
}
// ... all shared types
Package Structure
packages/core/
├── src/
│ ├── providers/
│ │ ├── context.ts # IContextProvider interface
│ │ ├── task-management.ts # ITaskManagementProvider interface
│ │ ├── registry.ts # IRegistryProvider interface
│ │ ├── ide-adapter.ts # IIDEAdapter interface
│ │ └── agent-profile.ts # IAgentProfileProvider interface
│ ├── schemas/
│ │ ├── agent.ts # AgentConfigSchema (Zod)
│ │ ├── config.ts # OACConfigSchema (Zod)
│ │ ├── lockfile.ts # LockfileSchema (Zod)
│ │ ├── registry.ts # RegistrySchema + RegistryItemSchema (Zod)
│ │ ├── skill.ts # SkillFrontmatterSchema (Zod)
│ │ ├── context.ts # ContextFrontmatterSchema (Zod)
│ │ └── manifest.ts # ManifestSchema (Zod)
│ ├── provider-registry.ts # ProviderRegistry class
│ └── types/
│ └── index.ts # All shared TypeScript types
├── package.json # Zero runtime deps (only zod as peer)
└── tsconfig.json
Key Constraints
- Zero runtime dependencies (except
zodas a peer dep) - All interfaces must be stable — breaking changes require major version bump
- Export everything from
index.tsfor clean imports:import { IContextProvider } from '@nextsystems/oac-core' - Must be usable by third-party plugin authors to implement custom providers
Context Files Needed
.opencode/context/core/standards/code-quality.md.opencode/context/core/standards/test-coverage.md
Validation Criteria
- All 5 provider interfaces defined and exported
- All Zod schemas match existing codebase field names (no divergence from compatibility-layer)
ProviderRegistryloads custom providers fromconfig.providers.*via dynamic import- Zero runtime dependencies (only zod peer dep)
- 100% TypeScript strict mode
- All types exported from single
index.ts
Project 2: @nextsystems/oac-cli — Full CLI Package
Purpose: The main oac command. Commander.js-based CLI with all commands, config system, lockfile, approval/YOLO system, and wiring to all providers.
Depends on: Project 1 (@nextsystems/oac-core)
What to Build
2.1 CLI Entry Point & Command Structure
// src/index.ts — lazy-loaded commands (keeps oac --version < 50ms)
program
.command('init')
.action(async (...args) => {
const { initCommand } = await import('./commands/init.js');
return initCommand(...args);
});
// All commands use dynamic import — never eager-load
Complete command surface:
| Command | Description |
|---|---|
oac init [profile] |
First-run wizard, project setup |
oac install <ide> [profile] |
Install for specific IDE |
oac add <component> |
Add individual component (agent/skill/context) |
oac update [component] |
Update installed components |
oac remove <component> |
Remove a component |
oac list [type] |
List installed/available components |
oac browse [type] |
Interactive TUI browser |
oac search <query> |
Search registry |
oac configure [subcommand] |
Manage configuration |
oac context <subcommand> |
Context system management |
oac skill <subcommand> |
Skill management |
oac plugin <subcommand> |
Plugin management |
oac doctor |
Diagnose installation health |
oac rollback [component] |
Undo last operation |
oac publish <path> |
Publish to community registry |
oac compat <subcommand> |
IDE compatibility tools |
oac show <component> |
Show component details |
oac presets <subcommand> |
Manage personal presets |
oac registry <subcommand> |
Manage registry sources |
Global flags (all commands):
--yolo Skip all confirmations, auto-resolve conflicts
--dry-run Preview changes without executing
--local Force local install (.opencode/ in CWD)
--global Force global install (~/.config/oac/)
--verbose Detailed output
--quiet Suppress output except errors
2.2 Config System
// src/config/manager.ts
export class ConfigManager {
// Merges: defaults → global (~/.config/oac/config.json) → project (.oac/config.json)
async load(): Promise<OACConfig>;
async set(keyPath: string, value: unknown, scope: 'global' | 'local'): Promise<void>;
async get(keyPath: string): Promise<unknown>;
async validate(config: unknown): Promise<OACConfig>; // Uses OACConfigSchema from core
}
Config file locations:
- Global:
~/.config/oac/config.json - Project:
.oac/config.json(committed to git) - Env overrides:
OAC_YOLO=true,OAC_REGISTRY_URL=...,OAC_BRANCH=...
Config schema (key sections):
{
"version": "1.0.0",
"preferences": {
"defaultIDE": "opencode",
"installLocation": "local",
"yoloMode": false,
"conflictStrategy": "ask",
"autoBackup": true,
"updateMode": "manual"
},
"providers": {
"context": null,
"taskManagement": null,
"registry": null,
"ideAdapters": []
},
"registries": [
{ "name": "official", "url": "https://registry.nextsystems.dev/oac", "priority": 1 }
],
"ides": {
"opencode": { "enabled": true, "path": ".opencode", "profile": "developer" },
"cursor": { "enabled": false, "path": ".cursor", "profile": "developer" },
"claude": { "enabled": false, "path": ".claude", "profile": "developer" }
}
}
2.3 Approval / YOLO System
// src/approval/manager.ts
export class ApprovalManager {
constructor(private opts: { yolo: boolean; strategy: ConflictStrategy }) {}
// Returns: 'proceed' | 'skip' | 'backup-and-proceed' | 'abort'
async resolveConflict(file: ConflictFile): Promise<ConflictResolution>;
// Batch approval for multiple files
async resolveAll(files: ConflictFile[]): Promise<Map<string, ConflictResolution>>;
// Show diff between existing and new file
async showDiff(existing: string, incoming: string): Promise<void>;
}
Conflict resolution flow:
- File doesn't exist → install silently
- File exists, SHA256 matches installed version → update silently (not user-modified)
- File exists, SHA256 differs from installed → user-modified → prompt (or YOLO: backup+overwrite)
--yolo→ backup all conflicts, overwrite all, report at end
2.4 Lockfile System
// src/lockfile/manager.ts
export class LockfileManager {
// oac.lock format (committed to git)
async read(): Promise<OACLock>;
async write(lock: OACLock): Promise<void>;
async addComponent(component: InstalledComponent): Promise<void>;
async removeComponent(name: string, type: ComponentType): Promise<void>;
async getComponent(name: string, type: ComponentType): Promise<InstalledComponent | null>;
async isModified(name: string, type: ComponentType): Promise<boolean>;
async pruneHistory(maxEntries?: number): Promise<void>; // Prevents unbounded growth
}
oac.lock format:
{
"version": "1",
"oacVersion": "1.0.0",
"generated": "2026-02-19T00:00:00Z",
"installed": {
"opencode": {
"profile": "developer",
"location": "local",
"path": ".opencode",
"components": {
"agent:openagent": {
"version": "1.0.0",
"sha256": "abc123",
"installedAt": "2026-02-19T10:00:00Z",
"source": "https://registry.nextsystems.dev/oac",
"userModified": false
}
}
}
},
"historyPolicy": { "maxEntries": 50 },
"history": [
{ "timestamp": "...", "action": "install", "component": "agent:openagent", "version": "1.0.0" }
]
}
2.5 Backup System
// src/backup/manager.ts
export class BackupManager {
// Backups stored in .oac/backups/ (git-ignored)
async backup(filePath: string): Promise<string>; // Returns backup path
async restore(backupPath: string): Promise<void>;
async listBackups(component?: string): Promise<Backup[]>;
async pruneBackups(maxPerComponent?: number): Promise<void>;
}
2.6 oac doctor Command
Checks (in order):
- Node.js version ≥ 18
- Config files valid JSON + schema
- Registry reachable (network check)
- All installed component files exist on disk
- SHA256 integrity of installed files vs lockfile
- Dependency graph complete (no missing deps)
- IDE-specific paths exist and readable
oac.lockin sync with installed filestask-cli.jscompiled (not just.ts)context-manager/router.shis not a stub- Git integration (uncommitted changes warning)
- Custom providers loadable (if configured)
2.7 Developer Tooling Scripts
scripts/
├── generate-manifest.ts # Auto-generate manifest.json from .opencode/context/ files
├── generate-navigation.ts # Auto-generate navigation.md files per category
├── validate-registry.ts # Validate registry.json integrity (already exists, keep)
├── validate-content.ts # Validate all context/agent/skill files
└── add-content.ts # Scaffold new content with correct metadata
Adding a new context file (the simplified workflow):
# Before: manual SHA256, manual manifest.json, manual registry.json
# After:
npm run add-content -- --type context --name typescript-patterns --category development
# → Creates .opencode/context/development/typescript-patterns.md with frontmatter
# → Auto-updates manifest.json with SHA256
# → Auto-updates registry.json
# → Auto-updates navigation.md for that category
Package Structure
packages/cli/
├── src/
│ ├── commands/
│ │ ├── init.ts
│ │ ├── install.ts
│ │ ├── add.ts
│ │ ├── update.ts
│ │ ├── remove.ts
│ │ ├── list.ts
│ │ ├── browse.ts
│ │ ├── search.ts
│ │ ├── configure.ts
│ │ ├── context.ts
│ │ ├── skill.ts
│ │ ├── plugin.ts
│ │ ├── doctor.ts
│ │ ├── rollback.ts
│ │ ├── publish.ts
│ │ ├── compat.ts
│ │ ├── show.ts
│ │ ├── presets.ts
│ │ └── registry.ts
│ ├── config/
│ │ ├── manager.ts
│ │ └── defaults.ts
│ ├── approval/
│ │ ├── manager.ts
│ │ └── strategies.ts
│ ├── lockfile/
│ │ └── manager.ts
│ ├── backup/
│ │ └── manager.ts
│ ├── ui/
│ │ ├── prompts.ts # @inquirer/prompts wrappers
│ │ ├── progress.ts # ora + cli-progress wrappers
│ │ └── logger.ts # chalk + log levels
│ └── index.ts # Commander setup, lazy command loading
├── scripts/
│ ├── generate-manifest.ts
│ ├── generate-navigation.ts
│ ├── validate-content.ts
│ └── add-content.ts
├── package.json
└── tsconfig.json
Key Dependencies
{
"dependencies": {
"@nextsystems/oac-core": "workspace:*",
"commander": "^12.0.0",
"@inquirer/prompts": "^5.0.0",
"chalk": "^5.3.0",
"ora": "^8.0.1",
"cli-progress": "^3.12.0",
"update-notifier": "^7.3.1",
"conf": "^13.0.0",
"fs-extra": "^11.2.0",
"glob": "^10.3.0",
"gray-matter": "^4.0.3",
"semver": "^7.6.0",
"diff": "^5.2.0"
}
}
Validation Criteria
oac --versioncompletes in < 50ms (lazy loading)oac init developer --yolocompletes in < 2 minutes- All commands have
--dry-runsupport --yoloflag skips all interactive prompts- Config merges correctly: defaults → global → project → env vars
oac.lockwritten after every install/update/removeoac.lockhistory capped at 50 entriesoac doctorcatches all 12 defined failure modes- Backups created before any overwrite
- Custom providers load from
config.providers.* - 80%+ test coverage on core modules
Project 3: Context System
Purpose: 6-layer context resolution, bundle/manifest management, auto-update, and the oac context * commands. Implements IContextProvider from Project 1.
Depends on: P1 (core interfaces), P2 (CLI commands, config)
What to Build
3.1 Default Context Provider (6-Layer Resolver)
// src/providers/default-context-provider.ts
export class DefaultContextProvider implements IContextProvider {
readonly id = 'oac-default';
readonly displayName = 'OAC 6-Layer Context Resolver';
private layers: string[];
private cache: Map<string, ContextFile>; // (filename, mtime) → ContextFile
constructor(config: OACConfig) {
this.layers = this.buildLayers(config);
}
private buildLayers(config: OACConfig): string[] {
const projectRoot = process.cwd();
const home = os.homedir();
const pkgPath = dirname(require.resolve('@nextsystems/oac/package.json'));
return [
join(projectRoot, '.oac/context'), // L1: project override (highest)
join(projectRoot, '.opencode/context'), // L2: project context
join(projectRoot, '.claude/context'), // L3: IDE-specific (claude)
join(projectRoot, '.cursor/context'), // L3: IDE-specific (cursor)
join(projectRoot, 'docs/context'), // L4: project docs
join(home, '.config/oac/context'), // L5: user global
join(pkgPath, 'context'), // L6: OAC bundled (lowest)
];
}
async resolve(name: string): Promise<ContextFile | null> {
// Check cache first (invalidate on mtime change)
for (const [index, basePath] of this.layers.entries()) {
const fullPath = join(basePath, name);
if (await pathExists(fullPath)) {
return this.buildContextFile(fullPath, LAYER_NAMES[index]);
}
}
return null;
}
}
3.2 Manifest System
// src/manifest/manager.ts
export class ManifestManager {
// manifest.json = npm package inventory (never modified by users)
async readBundled(): Promise<BundledManifest>; // Reads from npm package
// .opencode/.oac-manifest.json = project install state
async readInstalled(projectRoot: string): Promise<InstalledManifest>;
async writeInstalled(projectRoot: string, manifest: InstalledManifest): Promise<void>;
// Compare to find what needs updating
async computeDrift(projectRoot: string): Promise<DriftReport>;
}
Manifest contract (clarified from review):
manifest.json(in npm package) = what ships in the package — never modified by users, auto-generated byscripts/generate-manifest.ts.opencode/.oac-manifest.json(in project) = what's installed — written byoac context install, read byoac context updateoac.lock= full install state including non-context components
3.3 Auto-Generated Manifest
// scripts/generate-manifest.ts (runs at npm publish time)
// Scans .opencode/context/ → computes SHA256 for each file → writes manifest.json
// NEVER hand-maintain SHA256s
3.4 Auto-Generated Navigation Files
// scripts/generate-navigation.ts (runs at npm publish time)
// Reads manifest.json → generates navigation.md per category directory
// NEVER hand-maintain navigation.md files
3.5 Context Resolution Map (for AI Sessions)
// Written by OpenCode plugin at session.created
// Tells ContextScout which layer each file came from
// .oac/context-resolution-map.json (git-ignored)
{
"generatedAt": "2026-02-19T10:00:00Z",
"resolved": {
"core/standards/code-quality.md": {
"layer": 2,
"layerName": "project-context",
"path": ".opencode/context/core/standards/code-quality.md",
"userModified": false
}
}
}
3.6 CLI Commands
oac context install # Install from npm bundle (interactive)
oac context install --profile standard # Select profile
oac context install --global # Install to ~/.config/oac/context/
oac context install --ide claude # Install to .claude/context/
oac context install --dry-run # Preview
oac context update # Update from npm bundle (interactive)
oac context update --check # Show what would change
oac context update --yolo # Auto-apply all updates
oac context validate # Full validation report
oac context validate --ci # Exit 1 on failure (for CI)
oac context validate --fix # Auto-fix recoverable issues
oac context list # List all context files
oac context list --tree # As directory tree
oac context resolve <ref> # Show which layer wins
oac context sources # Show all context source directories
oac context override <ref> # Copy to .oac/context/ for customization
oac context add <source> # Add external context (GitHub/local)
oac context diff <ref> # Diff installed vs bundled version
3.7 Context Profiles
// In manifest.json
{
"profiles": {
"essential": {
"files": ["core/standards/code-quality", "core/standards/documentation", "core/standards/test-coverage"]
},
"standard": {
"extends": "essential",
"files": ["core/workflows/task-delegation-basics", "core/workflows/code-review", "core/standards/security-patterns"]
},
"developer": {
"extends": "standard",
"files": ["development/principles/clean-code", "development/principles/api-design"]
}
}
}
Key Design Decisions
- Bundle into npm, not fetch at runtime — deterministic, offline-capable
- Auto-generate manifest.json and navigation.md — never hand-maintain SHA256s
- Resolution map written at session start — ContextScout knows which layer each file came from
- Layer 1 (
.oac/context/) is the escape hatch — always wins, user-owned - Layer 6 (npm bundle) is always present — no install required for fallback
Validation Criteria
oac context resolve core/standards/code-quality.mdshows correct layer- Layer 1 override wins over all other layers
scripts/generate-manifest.tsproduces correct SHA256sscripts/generate-navigation.tsproduces valid navigation.md filesoac context validate --ciexits 1 on broken references- Resolution cache invalidates on file mtime change
- Custom context provider loads from
config.providers.context - Context resolution map written at session start
Project 4: Agent & Skill Management
Purpose: agent.json + prompt.md architecture, preset/customization system, skill packaging, multi-IDE format conversion, and oac add/customize/presets/skill commands.
Depends on: P1 (core interfaces), P2 (CLI, config, lockfile)
What to Build
4.1 Agent Architecture
Source of truth: agent.json (config) + prompt.md (prose) per agent directory.
.opencode/agents/
├── core/
│ ├── openagent/
│ │ ├── agent.json # Config, permissions, metadata
│ │ └── prompt.md # Prose content (what the AI reads)
│ └── opencoder/
│ ├── agent.json
│ └── prompt.md
└── subagents/
├── contextscout/
│ ├── agent.json
│ └── prompt.md
└── ...
agent.json schema (from @nextsystems/oac-core):
{
"name": "openagent",
"displayName": "OpenAgent",
"version": "1.0.0",
"description": "Universal orchestrator for complex tasks",
"mode": "primary",
"temperature": 0.1,
"permission": [
{ "bash": { "*": "deny", "git status*": "allow" } },
{ "edit": { "**/*.env*": "deny" } }
],
"skills": ["task-management", "context-manager"],
"oac": {
"source": "bundled",
"tags": ["universal", "orchestration"],
"category": "core"
}
}
IDE format generation (from agent.json + prompt.md):
- OpenCode: generates YAML frontmatter + prompt body →
.opencode/agent/core/openagent.md - Claude Code: generates Claude-compatible frontmatter →
.claude/agents/openagent.md - Cursor: merges all agents into
.cursorrules(router pattern) - Windsurf: generates
.windsurf/agents/openagent.json
4.2 Preset / Customization System
~/.config/oac/presets/ # User's personal presets (global)
├── agents/
│ ├── my-openagent.md # Preset file with CUSTOMIZATION markers
│ └── strict-reviewer.md
└── .presets.json # Index of presets
.oac/presets/ # Team presets (committed to git)
├── team-lead.json
└── solo-dev.json
Preset file format (merge-safe):
---
preset:
name: my-openagent
base: agent:openagent
baseVersion: 1.0.0
updateStrategy: manual
---
<!-- CUSTOMIZATION: Approval Gates -->
Auto-approve read operations (glob, read, grep)
<!-- END CUSTOMIZATION -->
[Rest of base agent prompt unchanged]
Preset commands:
oac customize agent:openagent # Create preset (wizard)
oac use preset:my-openagent # Activate for project
oac use preset:my-openagent --global # Activate globally
oac presets list # List all presets
oac presets list --active # Show active presets
oac import preset ./team-preset.md # Import team preset
oac export preset:my-openagent # Export for sharing
4.3 Skill Packaging
Skill structure (standardized):
.opencode/skills/{skill-name}/
├── SKILL.md # REQUIRED: frontmatter + instructions
├── router.sh # OPTIONAL: CLI entry point
├── scripts/
│ └── *.js # COMPILED (not .ts) — eliminates ts-node dependency
└── config/
└── *.json
Critical: All TypeScript scripts compiled to JS as part of package build. task-cli.ts → task-cli.js. No ts-node at runtime.
Skill commands:
oac skill install task-management # Install from OAC registry
oac skill install task-management@1.0.0 # Specific version
oac skill install --all # Install all bundled skills
oac skill list # List installed skills
oac skill update # Update all skills
oac skill remove task-management # Remove skill
oac skill validate # Validate all skills
oac skill doctor # Health check (router.sh, scripts exist, etc.)
4.4 context-manager Skill Implementation
Critical gap: context-manager/router.sh is currently a stub. Must implement:
.opencode/skills/context-manager/
├── SKILL.md
├── router.sh # Routes to scripts below
└── scripts/
├── discover.js # Glob-based context file discovery
├── fetch.js # Calls ExternalScout / Context7 API
├── harvest.js # Parses source doc, creates permanent context
├── extract.js # Targeted extraction from context files
├── compress.js # Summary/truncation of large files
├── organize.js # File reorganization by concern
├── cleanup.js # Removes stale .tmp/ files
└── process.js # Orchestrates multi-step guided workflows
4.5 Task CLI Compilation
# Build step: compile task-cli.ts → task-cli.js
# Included in npm package files
# oac doctor checks for task-cli.js presence
oac task commands (wrapping compiled task-cli.js):
oac task status [feature] # Show task status
oac task next [feature] # Show next eligible tasks
oac task complete <feature> <seq> "msg" # Mark complete
oac task validate [feature] # Validate JSON files
oac task plan [feature] --visualize # Show execution plan
oac session commands:
oac session list # List active sessions
oac session resume {session-id} # Resume a session
oac session cleanup {session-id} # Remove session files
oac session archive {session-id} # Archive to .tmp/archive/
Validation Criteria
agent.json+prompt.mdgenerates valid OpenCode frontmatteragent.json+prompt.mdgenerates valid Claude Code formatagent.json+prompt.mdgenerates valid.cursorrulesrouter- Preset
<!-- CUSTOMIZATION: -->markers survive agent updates task-cli.jscompiled and included in npm packagecontext-manager/router.shroutes to real implementations (not stub)- All skill scripts are
.js(no.tsat runtime) - Custom task management provider loads from
config.providers.taskManagement - Team presets in
.oac/presets/override global presets
Project 5: Plugin System
Purpose: OpenCode TypeScript plugin (primary), Claude Code plugin rewrite, Cursor/Windsurf adapters, and oac plugin * commands.
Depends on: P1 (core interfaces), P2 (CLI), P4 (agent/skill management)
What to Build
5.1 OpenCode TypeScript Plugin (Primary — New)
// .opencode/plugin/oac.ts (installed by oac plugin install opencode)
import type { Plugin } from "@opencode-ai/plugin";
export const OACPlugin: Plugin = async ({ project, client, $, directory }) => {
const config = await loadOACConfig(directory);
const manifest = await loadInstalledManifest(directory);
const skillMap = await buildSkillMap(directory);
return {
// Register OAC agents
config: async (currentConfig) => ({
...currentConfig,
agents: [...(currentConfig.agents || []), ...await loadOACAgents(directory)]
}),
// Skills as tools + tool.execute.before hooks
tool: createSkillTools(skillMap),
// Session start: inject workflow + check updates
"session.created": async ({ event }) => {
// 1. Write context resolution map
await writeContextResolutionMap(directory);
// 2. Inject using-oac workflow (non-blocking)
const workflow = skillMap.get('using-oac')?.content;
if (workflow) {
await client.session.prompt({
path: { id: event.id },
body: { noReply: true, parts: [{ type: "text", text: workflow }] }
});
}
// 3. Check for updates (throttled: once per 24h, non-blocking)
checkForUpdates(directory, client, event.id, config).catch(() => {});
},
// Skill invocation via tool hooks
"tool.execute.before": async (input, output) => {
if (input.tool.startsWith("oac_skill_")) {
const skill = skillMap.get(input.tool);
if (skill) {
await client.session.prompt({
path: { id: input.sessionID },
body: { noReply: true, parts: [{ type: "text", text: skill.content }] }
});
}
}
},
// Background cleanup on session idle
"session.idle": async ({ event }) => {
if (config.cleanup?.autoPrompt) {
await cleanupOldTempFiles(directory, config.cleanup);
}
},
};
};
Auto-update via session.created:
- Throttled: check once per 24 hours max
- Non-blocking: never delays session start
- Notification via
client.tui.showToast() - If
autoUpdate: "safe": silently update non-modified files - If
autoUpdate: false(default): show toast withoac updatehint
5.2 Claude Code Plugin Rewrite
Current: session-start.sh (bash, fragile JSON escaping)
Target: session-start.js (compiled TypeScript, proper JSON.stringify)
// plugins/claude-code/hooks/session-start.ts → compiled to session-start.js
import { readFileSync } from 'fs';
import { join } from 'path';
const skillPath = join(__dirname, '../skills/using-oac/SKILL.md');
const skillContent = readFileSync(skillPath, 'utf-8');
const output = {
additionalContext: skillContent,
hookSpecificOutput: {
type: 'session-start',
message: '🤖 OAC Active — 6-stage workflow enabled'
}
};
process.stdout.write(JSON.stringify(output));
5.3 Cursor Adapter (Router Pattern)
// Generates .cursorrules from all installed agents
// Router pattern: single file, all agents merged with section headers
// 100KB limit enforced with warnings
export class CursorPluginAdapter {
async generate(agents: OACAgent[], contexts: ContextFile[]): Promise<string> {
// Sort: core agents first, then specialists
// Embed essential context inline
// Warn if > 80KB
// Error if > 100KB
}
}
5.4 Plugin Commands
oac plugin install opencode # Install OpenCode TypeScript plugin
oac plugin install claude # Install Claude Code plugin
oac plugin install cursor # Generate .cursorrules
oac plugin install windsurf # Install Windsurf config
oac plugin install --all # Install for all configured IDEs
oac plugin update opencode # Update plugin
oac plugin update --all # Update all plugins
oac plugin update --check # Check only
oac plugin remove opencode # Remove plugin
oac plugin list # List installed plugins
oac plugin status # Health check
oac plugin configure opencode # Configure plugin settings
oac plugin create <name> # Scaffold new plugin
oac plugin test <name> # Test plugin
oac plugin publish <path> # Publish to community
5.5 Third-Party Plugin Support
// oac.json — plugin manifest for community plugins
{
"name": "oac-plugin-security-agents",
"version": "1.0.0",
"type": "plugin",
"provides": ["agents", "skills", "context"],
"ides": ["opencode", "claude"],
"registry": "./registry.json"
}
# Install community plugin
oac plugin add oac-plugin-security-agents
oac plugin add https://github.com/user/my-oac-plugin
Validation Criteria
- OpenCode plugin installs to
.opencode/plugin/oac.ts session.createdfires and injects workflow within 5 seconds- Update check throttled to once per 24 hours
session.creatednever crashes (silent failure on errors)- Claude Code
session-start.jsusesJSON.stringify(not manual escaping) - Cursor
.cursorruleswarns at 80KB, errors at 100KB - Custom IDE adapters load from
config.providers.ideAdapters oac plugin statusshows health for all installed plugins
Project 6: Registry & Community
Purpose: shadcn-inspired registry, oac.lock lockfile, community publishing, security scanning, and oac publish/browse/search/registry commands.
Depends on: P1 (core interfaces), P2 (CLI, lockfile)
What to Build
6.1 Registry Client
// src/registry/client.ts
export class RegistryClient {
constructor(private providers: IRegistryProvider[]) {}
// Multi-registry resolution: highest priority wins
async fetch(name: string, type: ComponentType): Promise<RegistryItem> {
for (const provider of this.providers) {
try {
return await provider.fetch(name, type);
} catch { continue; }
}
throw new Error(`Component not found: ${type}:${name}`);
}
async search(query: string, options?: RegistrySearchOptions): Promise<RegistryItem[]> {
// Search all registries, deduplicate by name+type, sort by priority
const results = await Promise.allSettled(
this.providers.map(p => p.search(query, options))
);
return deduplicateAndSort(results);
}
}
6.2 Registry Format (shadcn-inspired)
Registry index (registry.json):
{
"$schema": "https://registry.nextsystems.dev/oac/schema/registry.json",
"version": "3.0.0",
"items": [
{
"name": "openagent",
"type": "oac:agent",
"title": "OpenAgent",
"description": "Universal orchestrator",
"version": "1.0.0",
"ides": ["opencode", "claude", "cursor", "windsurf"],
"registryDependencies": ["contextscout", "task-manager"],
"files": [
{
"path": "agents/core/openagent/agent.json",
"type": "oac:agent-config",
"target": ".opencode/agents/core/openagent/agent.json"
},
{
"path": "agents/core/openagent/prompt.md",
"type": "oac:agent-prompt",
"target": ".opencode/agents/core/openagent/prompt.md"
}
]
}
]
}
6.3 Multi-Registry Support
// config.json
{
"registries": [
{ "name": "private", "url": "https://registry.company.com/oac", "priority": 1, "authToken": "${OAC_PRIVATE_TOKEN}" },
{ "name": "official", "url": "https://registry.nextsystems.dev/oac", "priority": 2 }
]
}
Resolution: highest priority registry wins. oac add agent:openagent --registry official to override.
6.4 Community Publishing
oac publish ./my-agent/ # Publish to community registry
# Flow:
# 1. Validate oac.json schema
# 2. Run security scan (secrets detection, malware check)
# 3. Compute SHA256 for all files
# 4. Submit PR to community registry repo
# 5. CI runs security scan
# 6. Maintainer reviews and merges
oac publish ./my-agent/ --private # Publish to private registry
6.5 Security Scanning
// src/security/scanner.ts
export class SecurityScanner {
async scan(files: string[]): Promise<ScanResult> {
return {
secrets: await this.detectSecrets(files), // gitleaks patterns
malware: await this.scanMalware(files), // basic pattern matching
permissions: await this.analyzePermissions(files), // permission audit
externalCalls: await this.findExternalCalls(files), // network calls
};
}
}
6.6 Browse TUI
// src/commands/browse.ts — interactive TUI using @inquirer/prompts
// Categories → Components → Preview → Install
// Arrow keys to navigate, Space to select, Enter to preview, 'i' to install
6.7 Registry Commands
oac browse [type] # Interactive TUI browser
oac search <query> # Search all registries
oac show agent:openagent # Show component details
oac verify agent:openagent # Verify SHA256 + signature
oac registry list # List configured registries
oac registry add <url> [--name <name>] # Add registry
oac registry remove <name> # Remove registry
oac registry ping # Check all registries reachable
oac registry sync # Sync local cache
oac publish <path> # Publish to community
oac publish <path> --registry private # Publish to private registry
Validation Criteria
- Multi-registry resolution: private registry wins over official
oac searchworks across all configured registries- SHA256 verification on every download
- Security scan runs before
oac publish oac browseTUI navigable with arrow keys- Private registry supports auth token via env var
- Custom registry provider loads from
config.providers.registry oac.lockupdated after every registry operation
Cross-Project: How Users Swap Subsystems
This is the key extensibility story. Users configure custom providers in .oac/config.json:
{
"providers": {
"context": "@my-company/oac-notion-context",
"taskManagement": "@my-company/oac-linear-provider",
"registry": "https://registry.internal.company.com/oac",
"ideAdapters": ["@my-company/oac-jetbrains-adapter"]
}
}
Custom context provider (replaces ContextScout + 6-layer resolution):
// @my-company/oac-notion-context
import type { IContextProvider } from '@nextsystems/oac-core';
export default class NotionContextProvider implements IContextProvider {
readonly id = 'notion';
readonly displayName = 'Notion Context Provider';
// Fetches context from Notion database instead of filesystem
}
Custom task management provider (replaces TaskManager/BatchExecutor):
// @my-company/oac-linear-provider
import type { ITaskManagementProvider } from '@nextsystems/oac-core';
export default class LinearTaskProvider implements ITaskManagementProvider {
readonly id = 'linear';
readonly displayName = 'Linear Issue Tracker';
// Creates/reads tasks from Linear API instead of .tmp/tasks/ JSON files
}
Custom IDE adapter (adds JetBrains support):
// @my-company/oac-jetbrains-adapter
import type { IIDEAdapter } from '@nextsystems/oac-core';
export default class JetBrainsAdapter implements IIDEAdapter {
readonly id = 'jetbrains';
readonly displayName = 'JetBrains AI Assistant';
// Converts OAC agents to JetBrains AI Assistant format
}
Implementation Order & Dependencies
Week 1-2: P1 (core interfaces) — MUST BE FIRST
Week 3-4: P2 (CLI foundation) — MUST BE SECOND
Week 5-6: P3 + P4 in parallel (context system + agent/skill management)
Week 7: P5 (plugin system) — needs P4 complete
Week 8-9: P6 (registry + community) — can start after P2
P1 ──► P2 ──► P3 (parallel)
P4 (parallel) ──► P5
P6 (parallel)
What Each Project Needs From the Existing Codebase
| Project | Reuse | Rewrite | New |
|---|---|---|---|
| P1 (core) | Types from compatibility-layer | Unify schemas | Provider interfaces |
| P2 (CLI) | bin/oac.js entry point |
Full CLI (91 lines → full commander) | Config, lockfile, approval, backup |
| P3 (context) | .opencode/context/ files |
Context resolution (currently ContextScout only) | Manifest auto-gen, navigation auto-gen |
| P4 (agents/skills) | All .opencode/agent/ files, skills |
task-cli.ts → .js, context-manager stub | agent.json+prompt.md split, preset system |
| P5 (plugins) | Claude Code plugin structure | session-start.sh → .js | OpenCode TS plugin (new) |
| P6 (registry) | registry.json structure, validate-registry.ts |
Registry format (v2→v3) | Multi-registry, community publishing, TUI |
Success Criteria (All Projects)
npx @nextsystems/oac init developercompletes in < 2 minutesoac --versionresponds in < 50ms- Custom context provider replaces ContextScout with zero CLI changes
- Custom task management provider replaces TaskManager with zero CLI changes
- Private registry works with auth token
oac doctorcatches all known failure modes- All content files survive
npm updatewithout overwriting user customizations - OpenCode auto-updates on session start (non-blocking)
- Zero bash script dependency for any core functionality
- 80%+ test coverage on all packages
oac.lockenables reproducible installs across machines