diff --git a/AGENTS.md b/AGENTS.md index c61f5521..065c09ac 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,18 +1,18 @@ # PROJECT KNOWLEDGE BASE -**Generated:** 2026-03-06 -**Commit:** 3985e1a +**Generated:** 2026-03-29 +**Commit:** 045cac8 **Branch:** main ## OVERVIEW -GitHub Action harness for [OpenCode](https://opencode.ai/) + [oMo](https://github.com/code-yeongyu/oh-my-openagent) agents with **persistent session state** across CI runs. TypeScript, ESM-only, Node 24. +GitHub Action harness for [OpenCode](https://opencode.ai/) + [oMo](https://github.com/code-yeongyu/oh-my-openagent) agents with **persistent session state** across CI runs. Includes bundled `@fro.bot/systematic` plugin injection during setup. TypeScript, ESM-only, Node 24. ## STRUCTURE ``` ./ -├── src/ # TypeScript source (132 source files, 13.6k lines) +├── src/ # TypeScript source (145 source files, 15.0k lines) │ ├── main.ts # Thin entry point → harness/run.ts │ ├── post.ts # Thin entry point → harness/post.ts │ ├── index.ts # Public API re-exports @@ -30,7 +30,10 @@ GitHub Action harness for [OpenCode](https://opencode.ai/) + [oMo](https://githu │ ├── services/ # Layer 1: External adapters (GitHub, cache, session, setup) │ │ ├── github/ # Octokit client, context parsing, NormalizedEvent │ │ ├── session/ # Persistence layer (search, prune, storage, writeback) -│ │ ├── setup/ # Bun, oMo, OpenCode installation +│ │ ├── setup/ # Bun, oMo, OpenCode + Systematic config/install +│ │ │ ├── ci-config.ts # CI config assembly (extracted from setup.ts) +│ │ │ ├── systematic-config.ts # Systematic plugin config writer +│ │ │ └── adapters.ts # Extracted adapter factories │ │ └── cache/ # Cache restore/save with corruption detection │ ├── features/ # Layer 2: Business logic (agent, triggers, reviews, etc.) │ │ ├── agent/ # SDK execution, prompts, reactions, streaming @@ -55,27 +58,29 @@ GitHub Action harness for [OpenCode](https://opencode.ai/) + [oMo](https://githu ## WHERE TO LOOK -| Task | Location | Notes | -| ---------------- | ---------------------------------- | -------------------------------------------------- | -| Add action logic | `src/harness/run.ts` | Main orchestration via phases | -| Post-action hook | `src/harness/post.ts` | Durable cache save (RFC-017) | -| Setup library | `src/services/setup/` | Bun/oMo/OpenCode installation (auto-setup) | -| Cache operations | `src/services/cache/` | `restore.ts`, `save.ts` | -| GitHub API | `src/services/github/client.ts` | `createClient()`, `createAppClient()` | -| Event parsing | `src/services/github/context.ts` | `parseGitHubContext()`, `normalizeEvent()` | -| Event types | `src/services/github/types.ts` | `NormalizedEvent` discriminated union (8 variants) | -| Agent execution | `src/features/agent/execution.ts` | `executeOpenCode()` logic | -| Prompt building | `src/features/agent/prompt.ts` | `buildAgentPrompt()`, response protocol sections | -| Session storage | `src/services/session/` | `storage.ts`, `storage-mappers.ts` | -| Session search | `src/services/session/search.ts` | `listSessions()`, `searchSessions()` | -| Event routing | `src/features/triggers/router.ts` | `routeEvent()` orchestration | -| Context hydrate | `src/features/context/` | GraphQL/REST issue/PR data (RFC-015) | -| Comment posting | `src/features/comments/writer.ts` | `postComment()`, GraphQL mutations | -| PR reviews | `src/features/reviews/reviewer.ts` | `submitReview()`, line comments | -| Input parsing | `src/harness/config/inputs.ts` | `parseActionInputs()` returns Result | -| Logging | `src/shared/logger.ts` | `createLogger()` with redaction | -| Core types | `src/shared/types.ts` | `ActionInputs`, `CacheResult`, `RunContext` | -| Build config | `tsdown.config.ts` | ESM shim, bundled deps, license extraction | +| Task | Location | Notes | +| --- | --- | --- | +| Add action logic | `src/harness/run.ts` | Main orchestration via phases | +| Post-action hook | `src/harness/post.ts` | Durable cache save (RFC-017) | +| Setup library | `src/services/setup/` | Bun/oMo/OpenCode installation (auto-setup) | +| CI config | `src/services/setup/ci-config.ts` | `buildCIConfig()` — assembles OPENCODE_CONFIG_CONTENT | +| Systematic config | `src/services/setup/systematic-config.ts` | `writeSystematicConfig()` — plugin config writer | +| Cache operations | `src/services/cache/` | `restore.ts`, `save.ts` | +| GitHub API | `src/services/github/client.ts` | `createClient()`, `createAppClient()` | +| Event parsing | `src/services/github/context.ts` | `parseGitHubContext()`, `normalizeEvent()` | +| Event types | `src/services/github/types.ts` | `NormalizedEvent` discriminated union (8 variants) | +| Agent execution | `src/features/agent/execution.ts` | `executeOpenCode()` logic | +| Prompt building | `src/features/agent/prompt.ts` | `buildAgentPrompt()`, response protocol sections | +| Session storage | `src/services/session/` | `storage.ts`, `storage-mappers.ts` | +| Session search | `src/services/session/search.ts` | `listSessions()`, `searchSessions()` | +| Event routing | `src/features/triggers/router.ts` | `routeEvent()` orchestration | +| Context hydrate | `src/features/context/` | GraphQL/REST issue/PR data (RFC-015) | +| Comment posting | `src/features/comments/writer.ts` | `postComment()`, GraphQL mutations | +| PR reviews | `src/features/reviews/reviewer.ts` | `submitReview()`, line comments | +| Input parsing | `src/harness/config/inputs.ts` | `parseActionInputs()` returns Result | +| Logging | `src/shared/logger.ts` | `createLogger()` with redaction | +| Core types | `src/shared/types.ts` | `ActionInputs`, `CacheResult`, `RunContext` | +| Build config | `tsdown.config.ts` | ESM shim, bundled deps, license extraction | ## CODE MAP @@ -84,6 +89,8 @@ GitHub Action harness for [OpenCode](https://opencode.ai/) + [oMo](https://githu | `run` | Function | `src/harness/run.ts` | Main entry, phase orchestration | | `runPost` | Function | `src/harness/post.ts` | Post-action cache save | | `runSetup` | Function | `src/services/setup/setup.ts` | Setup orchestration | +| `buildCIConfig` | Function | `src/services/setup/ci-config.ts` | CI config assembly with plugin injection | +| `writeSystematicConfig` | Function | `src/services/setup/systematic-config.ts` | Systematic plugin config writer | | `restoreCache` | Function | `src/services/cache/restore.ts` | Restore OpenCode state | | `saveCache` | Function | `src/services/cache/save.ts` | Persist state to cache | | `executeOpenCode` | Function | `src/features/agent/execution.ts` | SDK execution orchestration | @@ -100,6 +107,7 @@ GitHub Action harness for [OpenCode](https://opencode.ai/) + [oMo](https://githu | `submitReview` | Function | `src/features/reviews/reviewer.ts` | Submit PR review | | `parseActionInputs` | Function | `src/harness/config/inputs.ts` | Parse/validate inputs | | `createLogger` | Function | `src/shared/logger.ts` | Logger with redaction | +| `DEFAULT_SYSTEMATIC_VERSION` | Constant | `src/shared/constants.ts` | Pinned Systematic version | | `ActionInputs` | Interface | `src/shared/types.ts` | Input schema | | `NormalizedEvent` | Union | `src/services/github/types.ts` | 8-variant discriminated event union | | `TriggerDirective` | Interface | `src/features/agent/prompt.ts` | Directive + appendMode for triggers | @@ -126,18 +134,18 @@ post.ts → harness/post.ts ## COMPLEXITY HOTSPOTS -| File | Lines | Reason | -| --------------------------------- | ----- | ----------------------------------------- | -| `features/triggers/__fixtures__/` | 627 | Factory-style payload generation | -| `features/agent/prompt.ts` | 420 | Prompt templates, trigger directives | -| `services/session/types.ts` | 292 | Session/message/part type hierarchy | -| `features/context/types.ts` | 279 | GraphQL context types, budget constraints | -| `features/comments/reader.ts` | 257 | Thread reading, pagination | -| `services/github/api.ts` | 255 | Reactions, labels, branch discovery | -| `services/setup/setup.ts` | 247 | Setup orchestration | -| `services/github/context.ts` | 226 | normalizeEvent() 8-variant union builder | -| `harness/config/inputs.ts` | 224 | Input parsing and validation | -| `services/session/search.ts` | 220 | Session listing and cross-session search | +| File | Lines | Reason | +| --------------------------------- | ----- | ------------------------------------------------------ | +| `features/triggers/__fixtures__/` | 627 | Factory-style payload generation | +| `features/agent/prompt.ts` | 512 | Prompt templates, trigger directives | +| `services/session/types.ts` | 292 | Session/message/part type hierarchy | +| `services/github/api.ts` | 289 | Reactions, labels, branch discovery | +| `features/context/types.ts` | 279 | GraphQL context types, budget constraints | +| `features/comments/reader.ts` | 257 | Thread reading, pagination | +| `services/github/context.ts` | 254 | normalizeEvent() 8-variant union builder | +| `harness/config/inputs.ts` | 247 | Input parsing and validation | +| `services/session/search.ts` | 220 | Session listing and cross-session search | +| `services/setup/setup.ts` | 209 | Refactored setup orchestration (split config/adapters) | ## CONVENTIONS @@ -181,6 +189,7 @@ pnpm build # Type check + bundle to dist/ (must stay in sync) - **Node 24 required**: Matches `action.yaml` runtime - **19 RFCs total**: Foundation, cache, GitHub client, sessions, triggers, security, observability, comments, PR review, delegated work, setup, execution, SDK mode, file attachments, GraphQL context, additional triggers, post-action hook, plugin, S3 backend - **SDK-based execution**: Uses `@opencode-ai/sdk` for server lifecycle + event streaming +- **Bundled Systematic plugin**: Setup injects `@fro.bot/systematic@` into CI OpenCode config by default - **Persistent memory**: Sessions survive across CI runs via GitHub Actions cache - **NormalizedEvent**: All webhook payloads pass through `normalizeEvent()` before routing; router never touches raw payloads - **Dual action entry points**: `main.ts` (execution) and `post.ts` (durable cache save) diff --git a/docs/solutions/best-practices/versioned-tool-config-plugin-pattern-2026-03-29.md b/docs/solutions/best-practices/versioned-tool-config-plugin-pattern-2026-03-29.md new file mode 100644 index 00000000..3a19a743 --- /dev/null +++ b/docs/solutions/best-practices/versioned-tool-config-plugin-pattern-2026-03-29.md @@ -0,0 +1,182 @@ +--- +title: "Adding a Config-Declared Plugin to the Versioned Tool Pattern" +date: 2026-03-29 +problem_type: best_practice +component: tooling +root_cause: missing_tooling +resolution_type: tooling_addition +severity: medium +tags: + - versioning + - plugin + - opencode + - systematic + - setup + - renovate +category: best_practice +--- + +# Adding a Config-Declared Plugin to the Versioned Tool Pattern + +## Problem + +The fro-bot/agent project manages external CLI tools (OpenCode, Bun, oMo) through a single-source-of-truth version constant pattern documented in `.agents/skills/versioned-tool/SKILL.md`. The `@fro.bot/systematic` OpenCode plugin needed to be added as a fourth bundled tool, but it installs differently from all existing tools — it's a config-declared plugin, not a downloaded binary or bunx-installed package. + +## Symptoms + +- No `@fro.bot/systematic` reference anywhere in the codebase +- Agent running in CI had no access to Systematic skills/workflows (brainstorming, planning, code review) +- The versioned-tool skill pattern covered binary downloads and bunx installs but not config-declared plugins + +## What Didn't Work + +The initial instinct was to model Systematic after oMo's installer pattern (`bunx oh-my-openagent@{version} install --no-tui --skip-auth`). This was wrong because: + +1. Systematic has no `install` CLI subcommand — it's an OpenCode plugin, not a standalone CLI tool +2. OpenCode resolves plugins declared in its config at startup; no separate install step is needed +3. Creating a `systematic.ts` installer file would have been dead code + +The versioned-tool skill itself warns: _"Do not copy installer structure blindly between tools. Copy the version-source pattern; verify the install mechanics per tool."_ + +## Solution + +### Adapted Pattern: Version Constant → Config Plugin Injection + +Instead of a separate installer, the version flows through the existing data pipeline and lands in `OPENCODE_CONFIG_CONTENT`'s `plugins` array. + +**Data flow:** + +``` +constants.ts → inputs.ts fallback → ActionInputs → bootstrap.ts + → EnsureOpenCodeOptions → SetupInputs → setup.ts ciConfig.plugins + → OPENCODE_CONFIG_CONTENT env var → OpenCode resolves at startup +``` + +### 1. Version Constant (single source of truth) + +```typescript +// src/shared/constants.ts +export const DEFAULT_SYSTEMATIC_VERSION = "2.1.0" +``` + +### 2. Type Thread (4 interfaces updated) + +```typescript +// src/shared/types.ts — ActionInputs +readonly systematicVersion: string + +// src/services/setup/types.ts — SetupInputs +readonly systematicVersion: string + +// src/features/agent/server.ts — EnsureOpenCodeOptions +systematicVersion: string +``` + +### 3. Input Parsing with Constant Fallback + +```typescript +// src/harness/config/inputs.ts +const systematicVersionRaw = core.getInput("systematic-version").trim() +const systematicVersion = systematicVersionRaw.length > 0 ? systematicVersionRaw : DEFAULT_SYSTEMATIC_VERSION +``` + +### 4. Plugin Registration in OpenCode Config (the key adaptation) + +```typescript +// src/services/setup/setup.ts — after user opencode-config merge +const systematicPlugin = `@fro.bot/systematic@${inputs.systematicVersion}` +const rawPlugins: unknown[] = Array.isArray(ciConfig.plugins) ? (ciConfig.plugins as unknown[]) : [] +const hasSystematic = rawPlugins.some((p: unknown) => typeof p === "string" && p.startsWith("@fro.bot/systematic")) +if (!hasSystematic) { + ciConfig.plugins = [...rawPlugins, systematicPlugin] +} +``` + +Design decisions: + +- Plugin appended AFTER user `opencode-config` merge, so it's always present +- Dedup via `@fro.bot/systematic` prefix match so user version pins win +- Explicit `unknown[]` annotation avoids `@typescript-eslint/no-unsafe-assignment` lint error + +### 5. Renovate Tracking (npm datasource) + +```json5 +// .github/renovate.json5 — customManagers entry +{ + customType: "regex", + managerFilePatterns: ["/src\\/shared\\/constants\\.ts/"], + matchStrings: ["DEFAULT_SYSTEMATIC_VERSION = '(?\\d+\\.\\d+\\.\\d+)'"], + depNameTemplate: "@fro.bot/systematic", + datasourceTemplate: "npm", +} +``` + +### 6. Action Input (no hardcoded default) + +```yaml +# action.yaml — NO default: value, falls through to constant +systematic-version: + description: Systematic plugin version to register with OpenCode. + required: false +``` + +## Why This Works + +OpenCode reads `OPENCODE_CONFIG_CONTENT` as its highest-precedence config. By adding `plugins: ["@fro.bot/systematic@2.1.0"]` to this env var during setup, OpenCode resolves and installs the plugin at startup. The version is pinned via the same constant pattern used by all other tools, with Renovate automating npm version bumps through the `customManagers` regex. + +## Prevention + +### 1. Follow the versioned-tool skill + +`.agents/skills/versioned-tool/SKILL.md` documents the exact pattern. Every new tool should follow its verification checklist: + +- Constant updated in `constants.ts` +- No duplicate version literals (grep for old version string) +- `action.yaml` has no hardcoded default +- Renovate regex matches the constant pattern +- Tests pass, lint clean, dist/ rebuilt + +### 2. Verify install mechanics BEFORE copying patterns + +The four tools now use four different installation mechanisms: + +| Tool | Mechanism | Install Location | +| ---------- | ----------------------------------------------- | ---------------------------- | +| OpenCode | Binary download + tool-cache | PATH binary | +| Bun | Binary download + tool-cache | PATH binary | +| oMo | `bunx` CLI with `install` subcommand | npm global + config | +| Systematic | Config declaration in `OPENCODE_CONFIG_CONTENT` | OpenCode resolves at startup | + +When adding a fifth tool, check the tool's README first. The install mechanism determines whether you need an installer file or just config wiring. + +### 3. Dedup array injections + +When programmatically injecting into arrays that users can also populate (like `plugins`), always check for existing entries before appending. Prefix matching (`p.startsWith('@fro.bot/systematic')`) handles version differences gracefully. + +### 4. Type safety with `Record` + +When spreading arrays extracted from `unknown`-typed objects, `Array.isArray()` narrows to `any[]` in TypeScript. Use explicit `unknown[]` annotation (`ciConfig.plugins as unknown[]`) to satisfy `@typescript-eslint/no-unsafe-assignment`. + +### 5. Update ALL ciConfig test assertions + +When changing what goes into `OPENCODE_CONFIG_CONTENT`, grep `setup.test.ts` for every `OPENCODE_CONFIG_CONTENT` assertion. There were 3 that needed updating for the plugins array addition. + +## Related Documentation + +- [Tool Binary Caching on Ephemeral Runners](../build-errors/tool-binary-caching-ephemeral-runners.md) — Covers the tools cache layer, oMo version pinning, and the input→type→constant flow pattern that Systematic also follows +- [Versioned Tool Skill](/.agents/skills/versioned-tool/SKILL.md) — The canonical reference for this pattern +- [PR #409](https://github.com/fro-bot/agent/pull/409) — Implementation PR + +## Files Changed + +| File | Change | +| --------------------------------- | ----------------------------------------------- | +| `src/shared/constants.ts` | Added `DEFAULT_SYSTEMATIC_VERSION` | +| `src/shared/types.ts` | Added `systematicVersion` to `ActionInputs` | +| `src/harness/config/inputs.ts` | Parse `systematic-version` with fallback | +| `src/services/setup/types.ts` | Added `systematicVersion` to `SetupInputs` | +| `src/features/agent/server.ts` | Added to `EnsureOpenCodeOptions` + pass-through | +| `src/harness/phases/bootstrap.ts` | Pass `inputs.systematicVersion` downstream | +| `src/services/setup/setup.ts` | Plugin injection into `ciConfig.plugins` | +| `action.yaml` | Added `systematic-version` input | +| `.github/renovate.json5` | npm customManager + packageRule | diff --git a/src/features/agent/AGENTS.md b/src/features/agent/AGENTS.md index 1c6edd61..1cf14363 100644 --- a/src/features/agent/AGENTS.md +++ b/src/features/agent/AGENTS.md @@ -9,7 +9,7 @@ OpenCode SDK execution with GitHub context injection, multi-section prompt const | **Execution** | `execution.ts` | SDK session creation, prompt retry loop, result collection (178 L) | | **Prompting** | `prompt-sender.ts` | Prompt body construction, model resolution (73 L) | | **Retry** | `retry.ts` | Retry constants, `runPromptAttempt` with event stream (94 L) | -| **Server** | `server.ts` | SDK server bootstrap, health check (106 L) | +| **Server** | `server.ts` | SDK server bootstrap, health check, setup option wiring (110 L) | | **Polling** | `session-poll.ts` | Poll for session completion, event processor shutdown (110 L) | | **Streaming** | `streaming.ts` | Event stream processing, artifact detection (137 L) | | **Context** | `context.ts` | Gathers event data from NormalizedEvent, diff, hydrated context | @@ -83,6 +83,7 @@ gh CLI Reference (always) - **Reaction-based UX**: Non-fatal state machine (Eyes → Hooray/Confused); failures never crash execution - **Context Budgeting**: Two-tier enforcement (50 files fetched, 20 files injected into prompt) - **NormalizedEvent Intake**: `collectAgentContext` reads from `NormalizedEvent` (not raw payloads) +- **Setup Option Pass-through**: `EnsureOpenCodeOptions` includes `systematicVersion` and `systematicConfig` for setup handoff ## ANTI-PATTERNS diff --git a/src/services/setup/AGENTS.md b/src/services/setup/AGENTS.md index dbf7211a..fc43fe0a 100644 --- a/src/services/setup/AGENTS.md +++ b/src/services/setup/AGENTS.md @@ -6,30 +6,35 @@ Environment bootstrap logic: Bun runtime, OpenCode CLI, and oMo plugin installat ## WHERE TO LOOK -| Component | File | Responsibility | -| -------------- | ---------------- | ---------------------------------------------- | -| **Setup** | `setup.ts` | Orchestration entry point (runSetup) (247 L) | -| **OpenCode** | `opencode.ts` | CLI resolution & installation (168 L) | -| **Bun** | `bun.ts` | Bun runtime setup (required for oMo) (170 L) | -| **oMo** | `omo.ts` | oh-my-openagent install (graceful fail) (120 L) | -| **oMo Config** | `omo-config.ts` | Plugin configuration (97 L) | -| **GH Auth** | `gh-auth.ts` | gh CLI auth & Git user identity (101 L) | -| **Auth JSON** | `auth-json.ts` | Temporary auth.json generation (70 L) | -| **Project ID** | `project-id.ts` | Deterministic project ID generation (108 L) | -| **Cache** | `tools-cache.ts` | Low-level tool cache operations (137 L) | -| **Types** | `types.ts` | Setup-specific types & interfaces (150 L) | +| Component | File | Responsibility | +| -------------- | ---------------------- | ------------------------------------------------------- | +| **Setup** | `setup.ts` | Orchestration entry point (runSetup) (209 L) | +| **CI Config** | `ci-config.ts` | CI config assembly + Systematic plugin injection (43 L) | +| **Systematic** | `systematic-config.ts` | Systematic config writer (deep-merge) (36 L) | +| **Adapters** | `adapters.ts` | Exec/tool-cache adapter factories (20 L) | +| **OpenCode** | `opencode.ts` | CLI resolution & installation (169 L) | +| **Bun** | `bun.ts` | Bun runtime setup (required for oMo) (170 L) | +| **oMo** | `omo.ts` | oh-my-openagent install (graceful fail) (120 L) | +| **oMo Config** | `omo-config.ts` | Plugin configuration (97 L) | +| **GH Auth** | `gh-auth.ts` | gh CLI auth & Git user identity (101 L) | +| **Auth JSON** | `auth-json.ts` | Temporary auth.json generation (70 L) | +| **Project ID** | `project-id.ts` | Deterministic project ID generation (121 L) | +| **Cache** | `tools-cache.ts` | Low-level tool cache operations (142 L) | +| **Types** | `types.ts` | Setup-specific types & interfaces (152 L) | ## CODE MAP -| Symbol | Type | Location | Role | -| ------------------ | -------- | ------------------ | ----------------------------- | -| `runSetup` | Function | `setup.ts:124` | Main orchestration | -| `installOpenCode` | Function | `opencode.ts:87` | CLI install + cache | -| `installBun` | Function | `bun.ts:77` | Runtime setup | -| `installOmo` | Function | `omo.ts:56` | Plugin setup (graceful fail) | -| `configureGhAuth` | Function | `gh-auth.ts:7` | CLI authentication | -| `populateAuthJson` | Function | `auth-json.ts:41` | Secure credentials write | -| `ensureProjectId` | Function | `project-id.ts:35` | Deterministic ID for OpenCode | +| Symbol | Type | Location | Role | +| ----------------------- | -------- | ------------------------- | ----------------------------- | +| `runSetup` | Function | `setup.ts:22` | Main orchestration | +| `buildCIConfig` | Function | `ci-config.ts:8` | Build OPENCODE_CONFIG_CONTENT | +| `writeSystematicConfig` | Function | `systematic-config.ts:12` | Write merged systematic.json | +| `installOpenCode` | Function | `opencode.ts:87` | CLI install + cache | +| `installBun` | Function | `bun.ts:77` | Runtime setup | +| `installOmo` | Function | `omo.ts:56` | Plugin setup (graceful fail) | +| `configureGhAuth` | Function | `gh-auth.ts:7` | CLI authentication | +| `populateAuthJson` | Function | `auth-json.ts:41` | Secure credentials write | +| `ensureProjectId` | Function | `project-id.ts:35` | Deterministic ID for OpenCode | ## PATTERNS @@ -38,6 +43,7 @@ Environment bootstrap logic: Bun runtime, OpenCode CLI, and oMo plugin installat - **Graceful Fail**: Optional components (oMo, Bun) warn on error, don't crash. - **Dynamic Version**: Resolves 'latest' via GitHub Releases API. - **Verification**: Validates binaries (`--version`) BEFORE caching. +- **Systematic Bundling**: `buildCIConfig()` ensures `@fro.bot/systematic@` exists in OpenCode CI plugins. - `parseOmoProviders` moved to `src/harness/config/omo-providers.ts`. ## SECURITY