diff --git a/.github/workflows/sync-cep.yaml b/.github/workflows/sync-cep.yaml
deleted file mode 100644
index b6dea57..0000000
--- a/.github/workflows/sync-cep.yaml
+++ /dev/null
@@ -1,168 +0,0 @@
----
-name: Sync CEP
-
-on:
- schedule:
- - cron: '0 0 * * *'
- workflow_dispatch:
- inputs:
- scope:
- description: What to sync
- type: choice
- options:
- - all
- - skills
- - agents
- - commands
- default: all
- dry_run:
- description: Preview changes without creating PR
- type: boolean
- default: false
-
-permissions:
- contents: read
-
-concurrency:
- group: sync-cep
- cancel-in-progress: false
-
-jobs:
- precheck:
- name: CEP Pre-check
- runs-on: ubuntu-latest
- outputs:
- exit_code: ${{ steps.precheck.outputs.exit_code }}
- summary: ${{ steps.precheck.outputs.summary }}
- steps:
- - name: Checkout repository
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- with:
- fetch-depth: 0
- token: ${{ secrets.FRO_BOT_PAT }}
-
- - name: Setup Bun
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
-
- - name: Install dependencies
- run: bun install --frozen-lockfile
-
- - name: Run pre-check
- id: precheck
- run: |
- set +e
- bun scripts/check-cep-upstream.ts > precheck.json
- exit_code=$?
- summary='{}'
- if [ -s precheck.json ]; then
- summary=$(jq -c '.' < precheck.json 2>/dev/null || echo '{}')
- fi
- echo "exit_code=$exit_code" >> "$GITHUB_OUTPUT"
- echo "summary=$summary" >> "$GITHUB_OUTPUT"
- jq -c '{hashChanges, newUpstream, deletions, skipped, converterVersionChanged, errors}' < precheck.json 2>/dev/null || cat precheck.json
-
- env:
- GITHUB_TOKEN: ${{ secrets.FRO_BOT_PAT }}
-
- - name: Report precheck errors
- if: steps.precheck.outputs.exit_code == '2'
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
- with:
- github-token: ${{ secrets.FRO_BOT_PAT }}
- script: |
- const summary = JSON.parse(process.env.PRECHECK_SUMMARY || '{}');
- const errors = summary.errors || [];
- const errorList = errors.map(e => `- ${e}`).join('\n');
- const runUrl = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`;
- const body = [
- '## Pre-check Errors',
- '',
- `Pre-check failed with exit code 2. Found ${errors.length} error(s).`,
- '',
- '',
- `Errors (${errors.length})
`,
- '',
- errorList || '_None_',
- '',
- ' ',
- '',
- `**Hash changes:** ${(summary.hashChanges || []).length}`,
- `**New upstream:** ${(summary.newUpstream || []).length}`,
- `**Deletions:** ${(summary.deletions || []).length}`,
- '',
- `[Workflow run](${runUrl})`
- ].join('\n');
-
- const { data: issues } = await github.rest.issues.listForRepo({
- owner: context.repo.owner,
- repo: context.repo.repo,
- labels: 'sync-cep',
- state: 'open'
- });
-
- if (issues[0]) {
- await github.rest.issues.createComment({
- owner: context.repo.owner,
- repo: context.repo.repo,
- issue_number: issues[0].number,
- body
- });
- } else {
- await github.rest.issues.create({
- owner: context.repo.owner,
- repo: context.repo.repo,
- title: 'sync-cep: precheck errors',
- labels: ['sync-cep'],
- body
- });
- }
- env:
- PRECHECK_SUMMARY: ${{ steps.precheck.outputs.summary }}
-
- sync:
- name: CEP Sync
- runs-on: ubuntu-latest
- needs: precheck
- if: needs.precheck.outputs.exit_code != '' && needs.precheck.outputs.exit_code != '0'
- permissions:
- contents: read
- env:
- SYNC_PROMPT: |
- /sync-cep ${{ inputs.scope || 'all' }} ${{ inputs.dry_run && '--dry-run' || '' }}
-
- ${{ needs.precheck.outputs.exit_code }}
-
-
- ${{ needs.precheck.outputs.summary }}
-
-
- Note: headless CI run — user will not see live output.
- steps:
- - name: Checkout repository
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- with:
- token: ${{ secrets.FRO_BOT_PAT }}
-
- - name: Setup Bun
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
-
- - name: Install dependencies
- run: bun install --frozen-lockfile
-
- - name: Run build checks
- run: |
- bun run build
- bun run typecheck
- bun run lint
- bun test
-
- - name: Run Sync Bot
- uses: fro-bot/agent@9083d9d3fe350d32c4917bd0312fe78f241ad0aa # v0.32.1
- env:
- OPENCODE_PROMPT_ARTIFACT: 'true'
- with:
- auth-json: ${{ secrets.OPENCODE_AUTH_JSON }}
- github-token: ${{ secrets.FRO_BOT_PAT }}
- model: ${{ vars.SYNC_CEP_MODEL }}
- omo-providers: ${{ secrets.OMO_PROVIDERS }}
- prompt: ${{ env.SYNC_PROMPT }}
diff --git a/.opencode/commands/sync-cep.md b/.opencode/commands/sync-cep.md
deleted file mode 100644
index 5fc69f0..0000000
--- a/.opencode/commands/sync-cep.md
+++ /dev/null
@@ -1,347 +0,0 @@
----
-name: sync-cep
-description: Sync upstream CEP definitions into Systematic using convert-cc-defs. Detects changes, converts files, reports override conflicts, and prepares issue/PR summaries.
-argument-hint: "[all|skills|agents|commands] [--dry-run]"
----
-
-# Sync CEP Definitions
-
-Dry-run takes priority. Determine dry-run **only** from the `` arguments line (the `/sync-cep ...` invocation or arguments passed to this command). Ignore any other mentions of `--dry-run` elsewhere in the prompt.
-
-When `--dry-run` is present, follow the Pre-check Gate to obtain precheck data, then follow the Dry-Run Output Format exactly. **Do not proceed to conversion or PR creation.**
-Any additional text beyond the required dry-run format is a failure.
-
-## Arguments
-
-
-$ARGUMENTS
-
-
-Defaults:
-- target: `all`
-- dry-run: `false`
-
-## Identity
-
-You are running a CEP-to-Systematic re-sync. Your output must be structured and machine-parseable so CI can build issue and PR summaries without guessing.
-
-## Core Behavior
-
-- Always read `sync-manifest.json` before any conversion (except dry-run).
-- Never overwrite manual overrides.
-- Never auto-import new upstream definitions or auto-delete removed ones; report only.
-- **Focus on CHANGED content only** — If upstream hasn't changed a section, preserve it exactly. Do not make gratuitous modifications.
-- Produce a single, deterministic summary.
-
-### Change Detection Critical Rules
-
-| Rule | Reason |
-|------|--------|
-| Only modify changed portions | Unchanged content should be preserved verbatim |
-| `~/.config/opencode/` is correct | Never change this to `~/.opencode/` |
-| `question` is the correct tool name | Never change this to `AskUserQuestion` |
-| Preserve formatting | Keep trailing newlines, don't strip EOL |
-| Report discrepancies | Flag unexpected patterns for human review |
-
-## Skill: convert-cc-defs
-
-Before performing any conversion, use the `skill` tool to load `convert-cc-defs`. Do NOT use the `systematic_skill` tool — `convert-cc-defs` is a project-specific skill, not a bundled Systematic skill.
-
-After loading the skill, follow its workflow: Phase 2 (Mechanical Conversion) for each definition, then Phase 3 (Intelligent Rewrite) for context-aware adjustments, then Phase 4 (Write and Register) to update files and manifest.
-
-The precheck summary contains `hashChanges`, `newUpstream`, `newUpstreamFiles`, and `deletions` arrays. Each entry is a definition path like `skills/brainstorming` or `commands/workflows/review`. Process ALL definition types in the precheck's `hashChanges` array — agents, skills, AND commands. Do not skip a type.
-
-### New Upstream File Lists
-
-The `newUpstreamFiles` field is a map from definition key to its file list (e.g., `{"skills/my-skill": ["SKILL.md", "references/guide.md"]}`). When importing new definitions listed in `newUpstream`, use the file list from `newUpstreamFiles` to fetch ALL files — not just the primary definition file. For skills, this means fetching SKILL.md AND every sub-file (references/, scripts/, assets/, etc.). **Importing only SKILL.md while ignoring sub-files renders most multi-file skills non-functional.**
-
-## Feature: Pre-check Gate
-
-This command supports two modes for obtaining pre-check data. **The pre-check is a prerequisite — it runs before the dry-run decision.** Even in dry-run mode, you must have precheck data (either injected by the workflow or obtained by running the script) before producing the summary.
-
-### Mode 1: Workflow-injected (CI)
-
-When `` and `` XML tags are present in the prompt, use them directly. The sync workflow already ran the pre-check script — do not rerun it.
-
-### Mode 2: Interactive (local session)
-
-When the XML tags are absent, run the pre-check script yourself:
-
-```bash
-bun scripts/check-cep-upstream.ts
-```
-
-The script outputs JSON to stdout and uses its exit code to signal results:
-- The Bash tool captures both stdout (the JSON precheck summary) and the exit code.
-- Use the JSON output as the precheck summary and the exit code as the precheck exit code.
-- Then proceed with the same exit-code logic described below.
-
-**Note:** If the JSON output is large, you can redirect to a file and read it back:
-```bash
-bun scripts/check-cep-upstream.ts | tee /tmp/precheck.json; exit ${PIPESTATUS[0]}
-```
-
-**Environment:** The script requires `GITHUB_TOKEN` for authenticated GitHub API access. If not set, try `export GITHUB_TOKEN=$(gh auth token)` before running.
-
-### Pre-check Exit Codes
-
-| Exit Code | Meaning | Action |
-|-----------|---------|--------|
-| `0` | No changes detected | Stop and report "no changes." (Sync job should not run in this case.) |
-| `1` | Changes detected, no errors | Proceed with conversion run normally. |
-| `2` | Changes detected but with errors | Errors indicate missing upstream files (the manifest references files that no longer exist upstream). Proceed with conversion for definitions that are **not** affected by errors. Report errored definitions separately — do not attempt to convert them. Include the errors list from the pre-check summary in the output. |
-
-### Pre-check Error Handling
-
-When `` is `2`:
-- The `errors` array in the pre-check summary lists missing upstream content paths.
-- Extract the affected definition keys from the error paths (e.g., `Missing upstream content: plugins/compound-engineering/skills/foo/SKILL.md` → `skills/foo`).
-- Skip those definitions during conversion.
-- Include an **Errors** section in the output summary listing each error and the affected definitions.
-- The remaining `hashChanges`, `newUpstream`, and `deletions` are still valid and should be processed normally.
-
-### Dry-Run Exit Condition (HARD STOP)
-
-If `--dry-run` is present in the user request:
-- Output the dry-run summary only.
-- If `` is `2`, the summary MUST include the errors and which definitions would be skipped.
-- Do **not** call conversion tools or skills (no `convert-cc-defs`, no file editing). Running the pre-check script to obtain data is allowed and required in interactive mode.
-- Do **not** proceed to live sync.
-- Do **not** say you will continue or proceed with live sync.
-- End the response immediately after the summary.
-- Final line MUST be exactly: `DRY_RUN_STOP`
-- Never ask follow-up questions in dry-run mode.
-- Do not include any text after `DRY_RUN_STOP`.
-- Do not mention `convert-cc-defs` or how to proceed with a live sync.
-
-### Dry-Run Output Format
-
-When in dry-run, output exactly and only the following structure. The word `Summary` must be a heading. Nothing else is allowed:
-
-```
-## Summary
-
-
-DRY_RUN_STOP
-```
-
-Rules:
-- No tables, code blocks, or extra headings.
-- No follow-up questions.
-- The last non-empty line must be exactly `DRY_RUN_STOP`.
-
-The **only** acceptable dry-run output is the literal template above with `` replaced by plain sentences. You must end immediately after `DRY_RUN_STOP`.
-
-## Feature: Conversion Run
-
-- If `--dry-run` is set: do not invoke `convert-cc-defs`, do not edit files, do not run conversions, and do not proceed to live sync. Only report what would happen using the pre-check summary (which was already obtained as a prerequisite) and then stop.
-- Otherwise: invoke the `convert-cc-defs` skill for the selected target scope and apply the re-sync workflow steps in that skill (mechanical conversion + intelligent rewrite + merge).
-
-## Feature: Mandatory Post-Conversion Fixup
-
-**This step is NON-NEGOTIABLE.** After every conversion run (mechanical converter + LLM rewrite), run the batch sed fixup defined in `convert-cc-defs` Phase 2c. LLMs consistently fail at simple string replacement — the sed pass is the only reliable way to catch all remaining CC/CEP references.
-
-### Why This Exists
-
-In every sync run to date, the LLM rewrite pass has left behind:
-- `compound-engineering:` prefixes that should be `systematic:`
-- `.claude/` paths that should be `.opencode/`
-- `Claude Code` product references that should be `OpenCode`
-- `AskUserQuestion` tool names that should be `question`
-- `CLAUDE.md` references that should be `AGENTS.md`
-
-These are simple string replacements. The LLM does not need to do them — and repeatedly fails to. The batch sed catches them deterministically.
-
-### Execution Steps
-
-1. **Run the ordered sed** from `convert-cc-defs` Phase 2c on all converted `.md` files
-2. **Exclude** `sync-manifest.json` (upstream paths are correct) and `claude-permissions-optimizer/SKILL.md` (CC refs are intentional — only convert prefix and tool names)
-3. **Fix edge cases manually**: badge URLs, `EveryInc/systematic`, `claude.com/opencode`, `Task()` → `task()` (see Phase 2c edge cases table)
-4. **Run verification grep** (both CHECK 1 and CHECK 2 from Phase 2c) — zero hits required on non-exception files
-5. **Fail the run** if CHECK 2 (over-conversions) returns any hits — this means a double-conversion bug
-
-### File Loop Pattern
-
-```bash
-{
- git diff --name-only HEAD -- '*.md'
- git ls-files --others --exclude-standard -- '*.md'
-} | sort -u | grep -v '^skills/claude-permissions-optimizer/SKILL\.md$' | while IFS= read -r f; do
- if [ -f "$f" ]; then
- sed -i '' \
- -e 's/compound-engineering\.local\.md/systematic.local.md/g' \
- -e 's/compound-engineering-plugin/systematic/g' \
- -e 's/compound-engineering pipeline artifacts/systematic pipeline artifacts/g' \
- -e 's|\.context/compound-engineering/|.context/systematic/|g' \
- -e 's|plugins/compound-engineering/|plugins/systematic/|g' \
- -e 's/compound-engineering:/systematic:/g' \
- -e 's/Compound_Engineering/Systematic/g' \
- -e 's/Compound Engineering/Systematic/g' \
- -e 's/compound-engineering/systematic/g' \
- -e 's|~/\.claude/skills/|~/.config/opencode/skills/|g' \
- -e 's|~/\.claude/settings\.json|~/.config/opencode/settings.json|g' \
- -e 's|~/\.claude/|~/.config/opencode/|g' \
- -e 's|`\.claude/skills/|`.opencode/skills/|g' \
- -e 's|`\.claude/settings\.json|`.opencode/settings.json|g' \
- -e 's| \.claude/skills/| .opencode/skills/|g' \
- -e 's| \.claude/settings\.json| .opencode/settings.json|g' \
- -e 's|(\.claude/|(\.opencode/|g' \
- -e 's|`\.claude/|`.opencode/|g' \
- -e 's| \.claude/| .opencode/|g' \
- -e 's|"\.claude/|".opencode/|g' \
- -e 's|CLAUDE\.md|AGENTS.md|g' \
- -e 's/Claude Code/OpenCode/g' \
- -e 's/claude-code/opencode/g' \
- -e 's/AskUserQuestion/question/g' \
- -e 's/TaskCreate/todowrite/g' \
- "$f"
- fi
-done
-```
-
-Then apply targeted fixes to `claude-permissions-optimizer/SKILL.md`:
-
-```bash
-sed -i '' \
- -e 's/compound-engineering-plugin/systematic/g' \
- -e 's/compound-engineering:/systematic:/g' \
- -e 's/compound-engineering/systematic/g' \
- -e 's/Compound Engineering/Systematic/g' \
- -e 's/AskUserQuestion/question/g' \
- skills/claude-permissions-optimizer/SKILL.md
-```
-
-## Tooling and Command Safety
-
-- Never use `gh` or other external CLI tools in dry-run mode (exception: the pre-check script must run in interactive mode to obtain summary data).
-- Do not call conversion tools or edit files during dry-run.
-- Prefer local reads of `sync-manifest.json` and bundled files when summarizing outside dry-run.
-
-## Feature: Commit, Branch, and PR (MANDATORY after changes)
-
-After a successful conversion run (not dry-run) that modified any files, you **MUST** create or update a PR. A sync run that changes files but does not produce a PR is a **failed run**.
-
-### Step 1: Check for changes
-
-```bash
-git status --porcelain agents/ skills/ commands/ sync-manifest.json
-```
-
-If the output is empty, no files were changed — skip to Step 4: Post to tracking issue.
-
-### Step 2: Create branch and commit
-
-```bash
-git checkout -B chore/sync-cep
-git add agents/ skills/ commands/ sync-manifest.json
-git commit -m "chore: sync CEP upstream definitions"
-```
-
-### Step 3: Push and create or update PR
-
-First, write the output summary to a temp file for use as the PR body. The summary MUST follow the Output Formatting template (hash changes table, conflicts, errors, etc.):
-
-```bash
-cat > /tmp/sync-cep-pr-body.md <<'ENDOFBODY'
-## CEP Sync Summary
-
-(paste the full output summary here)
-ENDOFBODY
-```
-
-Push the branch:
-```bash
-git push -u origin chore/sync-cep --force-with-lease
-```
-
-Check if a PR already exists:
-```bash
-gh pr list --head chore/sync-cep --state open --json number --jq '.[0].number // empty'
-```
-
-- **If a PR number is returned:** update its body:
- ```bash
- gh pr edit --body-file /tmp/sync-cep-pr-body.md
- ```
-- **If empty (no PR):** create one:
- ```bash
- gh pr create --base main --head chore/sync-cep \
- --title "chore: sync CEP upstream definitions" \
- --body-file /tmp/sync-cep-pr-body.md \
- --label "sync-cep"
- ```
-
-**Important:** Environment variables do not persist across separate Bash tool calls. Always write the PR body to a file first, then reference it with `--body-file`.
-
-### Step 4: Post to tracking issue
-
-Find the open tracking issue labeled `sync-cep`:
-```bash
-gh issue list --label sync-cep --state open --json number --jq '.[0].number // empty'
-```
-
-- **If an issue exists:** post a comment with the summary and a link to the PR.
-- **If no issue exists:** create one with title `CEP Sync Run - YYYY-MM-DD`, label `sync-cep`, and the summary as the body.
-
-### Reuse rules
-
-- Always reuse branch `chore/sync-cep` — do not create timestamped or numbered branches.
-- If a PR already exists for that branch, update it instead of creating a new one.
-- Always link the PR in the tracking issue comment.
-
-## Feature: Conflict Detection
-
-Use the override merge matrix:
-- Upstream unchanged + override exists → keep override
-- Upstream changed + override on SAME field → conflict, report only
-- Upstream changed + override on DIFFERENT field → apply upstream, preserve overrides
-- Override is `"*"` → skip re-sync entirely
-
-## Feature: Output Formatting
-
-Use this exact template for all output. Copy it and fill in the placeholders:
-
-```
-## Summary
-- **Scope**: [all|skills|agents|commands]
-- **Definitions processed**: N
-- **Hash changes applied**: N
-- **Conflicts detected**: N
-- **Errors (from precheck)**: N
-
-### Hash Changes
-| Definition | Old Hash | New Hash | Status |
-|------------|----------|----------|--------|
-| path/to/def | abc123 | def456 | ✅ Applied |
-
-### Conflicts
-| Definition | Field | Override Value | Upstream Value | Action |
-|------------|-------|---------------|----------------|--------|
-(None detected / list conflicts)
-
-### New Upstream (report-only)
-| Definition | Files |
-|------------|-------|
-| path/to/new-def | SKILL.md, references/guide.md |
-
-### Upstream Deletions (report-only)
-- path/to/deleted-def
-
-### Errors
-- [error message from precheck] → Affected: [definition key]
-
-### Rewrite Failures
-- (None / list failures)
-
-### Phantom References
-- (None / list commands referencing missing agents/skills)
-```
-
-## Boundaries
-
-- Do not use `gh` commands or call external CLI tools during dry-run mode (exception: the pre-check script may run in interactive mode).
-- Do not auto-merge conflicts.
-- Do not modify files outside `agents/`, `skills/`, `commands/`, and `sync-manifest.json`.
-- Use `gh` for PR creation, PR updates, issue comments, and (in interactive mode) authentication token setup.
-- Branch name is always `chore/sync-cep`. Label is always `sync-cep`.
-- **A sync run that changes files but produces no PR is a FAILED run.**
diff --git a/.opencode/skills/convert-cc-defs/SKILL.md b/.opencode/skills/convert-cc-defs/SKILL.md
deleted file mode 100644
index af2d364..0000000
--- a/.opencode/skills/convert-cc-defs/SKILL.md
+++ /dev/null
@@ -1,814 +0,0 @@
----
-name: convert-cc-defs
-description: Use when importing, converting, or syncing agent, skill, or command definitions from CEP or other Claude Code-format sources into the Systematic plugin for OpenCode. Triggers on "import from CEP", "sync upstream", "convert CC definition", "add agent/skill/command from upstream", or when updating existing bundled definitions from upstream sources.
----
-
-# Convert CC Definitions
-
-Import and convert agent, skill, and command definitions written in Claude Code (CC) format — primarily from the Compound Engineering Plugin (CEP) upstream — into Systematic's bundled assets. Applies mechanical conversion via the converter pipeline, then intelligent LLM-powered rewrites to ensure definitions work correctly in OpenCode.
-
-**This is NOT a copy-paste operation.** Systematic is a cohesive OpenCode plugin, not a mirror. Every imported definition must be evaluated, adapted, and branded for the Systematic ecosystem.
-
-## When to Use
-
-- Importing new definitions from CEP or Superpowers upstream repos
-- Re-syncing existing bundled definitions after upstream changes
-- Converting a one-off CC definition file for inclusion in Systematic
-- Populating `sync-manifest.json` entries for existing bundled content
-
-## When NOT to Use
-
-- Writing new Systematic-native skills/agents/commands (use `create-agent-skill` skill instead)
-- Editing existing bundled content that has no upstream source
-- Converting definitions for a different project (use the CLI: `systematic convert`)
-
-## Core Workflow
-
-```dot
-digraph convert_flow {
- rankdir=TB;
- node [shape=box];
-
- "Identify target definitions" [shape=doublecircle];
- "Fetch upstream content" [shape=box];
- "Read manifest for existing entries" [shape=box];
- "Check manual_overrides" [shape=diamond];
- "Run mechanical converter" [shape=box];
- "Intelligent rewrite pass" [shape=box];
- "Branding & consistency review" [shape=box];
- "Write to bundled directory" [shape=box];
- "Update sync-manifest.json" [shape=box];
- "Verify (build + tests)" [shape=box];
- "Done" [shape=doublecircle];
-
- "Identify target definitions" -> "Fetch upstream content";
- "Fetch upstream content" -> "Read manifest for existing entries";
- "Read manifest for existing entries" -> "Check manual_overrides";
- "Check manual_overrides" -> "Run mechanical converter" [label="no overrides"];
- "Check manual_overrides" -> "Merge preserving overrides" [label="has overrides"];
- "Merge preserving overrides" -> "Intelligent rewrite pass";
- "Run mechanical converter" -> "Intelligent rewrite pass";
- "Intelligent rewrite pass" -> "Branding & consistency review";
- "Branding & consistency review" -> "Write to bundled directory";
- "Write to bundled directory" -> "Update sync-manifest.json";
- "Update sync-manifest.json" -> "Verify (build + tests)";
- "Verify (build + tests)" -> "Done";
-}
-```
-
-## Phase 1: Identify and Fetch
-
-### 1a. Identify Upstream Sources
-
-Determine what to import. Supported sources:
-
-| Source | Repo | Content |
-|--------|------|---------|
-| **CEP** | `EveryInc/compound-engineering-plugin` | Agents, skills, commands |
-| **Superpowers** | `obra/superpowers` | Skills (personal workflow skills) |
-| **Local file** | N/A | Single CC-format .md file |
-
-For GitHub repos, use `gh` CLI to fetch content. Note: CEP content lives under `plugins/compound-engineering/` — always use the full path:
-
-```bash
-# Fetch a specific file from CEP (note the full path under plugins/)
-gh api repos/EveryInc/compound-engineering-plugin/contents/plugins/compound-engineering/agents/review/security-sentinel.md \
- --jq '.content' | base64 -d > /tmp/upstream-security-sentinel.md
-
-# Get the latest commit SHA for the agents directory
-gh api "repos/EveryInc/compound-engineering-plugin/commits?path=plugins/compound-engineering/agents&per_page=1" \
- --jq '.[0].sha'
-
-# Get content hash for change detection
-shasum -a 256 /tmp/upstream-security-sentinel.md | cut -d' ' -f1
-```
-
-**Batch fetch pattern** — for importing multiple files, loop over a list:
-
-```bash
-mkdir -p /tmp/cep-upstream
-AGENTS=("review/architecture-strategist" "research/best-practices-researcher" "workflow/bug-reproduction-validator")
-for agent_path in "${AGENTS[@]}"; do
- name=$(basename "$agent_path")
- gh api "repos/EveryInc/compound-engineering-plugin/contents/plugins/compound-engineering/agents/${agent_path}.md" \
- --jq '.content' | base64 -d > "/tmp/cep-upstream/${name}.md"
- shasum -a 256 "/tmp/cep-upstream/${name}.md"
-done
-```
-
-**Recursive file listing** — the contents API fails on subdirectories. Use the git tree API for a complete one-shot listing:
-
-```bash
-# List ALL files under skills/ recursively (one API call)
-gh api "repos/EveryInc/compound-engineering-plugin/git/trees/?recursive=1" \
- --jq '.tree[] | select(.path | startswith("plugins/compound-engineering/skills/")) | select(.type == "blob") | .path' \
- | sed 's|plugins/compound-engineering/skills/||'
-```
-
-**Skill folders** — Skills are directories, not just SKILL.md files. A skill folder may contain references/, templates/, workflows/, scripts/, assets/, and schema files. The CLI converter (`bun src/cli.ts convert skill`) ONLY processes SKILL.md files. All other files in the skill folder must be:
-1. Fetched from upstream individually via the contents API
-2. Copied to the local skill directory, preserving the folder structure
-3. Manually rewritten with CC→OC text replacements (`.claude/` → `.opencode/`, `CLAUDE.md` → `AGENTS.md`, `Claude Code` → `OpenCode`, `compound-engineering:` → `systematic:`, etc.)
-4. `${CLAUDE_PLUGIN_ROOT}/skills//...` paths simplified to relative paths (skills are bundled in the plugin — no env var prefix needed)
-
-### Discovering Sub-Files for New Skills
-
-When importing a **new** skill (not yet in the manifest), you must discover ALL files in the skill directory before fetching. There are two ways to get the file list:
-
-**Option A: Use precheck `newUpstreamFiles` (automated sync)** — When running via the sync-cep workflow, the precheck summary includes a `newUpstreamFiles` map that lists all files for each new definition:
-
-```json
-{
- "newUpstreamFiles": {
- "skills/every-style-editor": ["SKILL.md", "references/EVERY_WRITE_STYLE.md"],
- "skills/gemini-imagegen": ["SKILL.md", "requirements.txt", "scripts/generate.py", "scripts/setup.sh"]
- }
-}
-```
-
-Use this file list directly — it was collected from the upstream git tree and is authoritative.
-
-**Option B: Query the git tree API (manual import)** — When importing outside the sync-cep workflow, discover files yourself:
-
-```bash
-# Get the full tree and filter for the skill directory
-gh api "repos/EveryInc/compound-engineering-plugin/git/trees/main?recursive=1" \
- --jq '.tree[] | select(.path | startswith("plugins/compound-engineering/skills//")) | select(.type == "blob") | .path' \
- | sed 's|plugins/compound-engineering/skills//||'
-```
-
-**After discovering the file list**, fetch each file individually using the contents API:
-
-```bash
-for file in SKILL.md references/guide.md scripts/setup.sh; do
- mkdir -p "/tmp/cep-upstream//$(dirname "$file")"
- gh api "repos/EveryInc/compound-engineering-plugin/contents/plugins/compound-engineering/skills//${file}" \
- --jq '.content' | base64 -d > "/tmp/cep-upstream//${file}"
-done
-```
-
-**CRITICAL**: Failing to discover sub-files results in incomplete skill imports — only SKILL.md gets copied while references/, scripts/, assets/ etc. are silently dropped. This renders many skills non-functional.
-
-### 1b. Check Existing Manifest
-
-Read `sync-manifest.json` to determine if this is a new import or an update:
-
-```bash
-# Check if definition already exists in manifest
-cat sync-manifest.json | jq '.definitions["agents/review/security-sentinel"]'
-```
-
-If updating an existing definition:
-- Compare `upstream_content_hash` with current upstream to detect changes
-- Check `manual_overrides` — these fields/sections MUST be preserved
-- Review `rewrites` log — these intelligent rewrites should be re-applied
-
-### 1c. Evaluate Fit
-
-**STOP and evaluate before converting.** Not all upstream definitions belong in Systematic. Present your evaluation to the user before proceeding.
-
-**Evaluation criteria (ALL must pass):**
-
-1. **Gap analysis:** Does this fill a gap in Systematic's current capabilities? Run `bun src/cli.ts list ` to check.
-2. **Philosophy fit:** Is it consistent with Systematic's opinionated workflow approach? (Structured phases, explicit deliverables, skill-driven discipline)
-3. **Overlap check:** Does it duplicate an existing bundled definition? If partial overlap, propose enhancing the existing definition instead.
-4. **Dependency check:** Does the definition reference agents, skills, or commands that don't exist in Systematic? List any missing dependencies — note as WARN (they can be imported later using this skill, and references should be kept).
-5. **Phantom check:** If importing agents or skills referenced by commands, verify they actually exist upstream. Commands may reference definitions that were never created upstream ("phantoms"). Fetch the upstream directory listing to confirm:
- ```bash
- # Check agents
- gh api repos/EveryInc/compound-engineering-plugin/contents/plugins/compound-engineering/agents/review \
- --jq '.[].name'
- # Check skills
- gh api "repos/EveryInc/compound-engineering-plugin/git/trees/?recursive=1" \
- --jq '.tree[] | select(.path | startswith("plugins/compound-engineering/skills/")) | select(.type == "tree") | .path' \
- | sed 's|plugins/compound-engineering/skills/||' | grep -v '/'
- ```
-6. **User value:** Would a Systematic plugin user actually invoke this? Niche CC-specific tools (e.g., `feature-video`, `test-browser`) may not translate.
-
-**Present your evaluation as a table:**
-
-```markdown
-| Criterion | Pass/Fail | Notes |
-|-----------|-----------|-------|
-| Gap analysis | PASS | No existing code review simplification agent |
-| Philosophy fit | PASS | Structured review methodology |
-| Overlap check | PASS | No overlap with existing review agents |
-| Dependencies | WARN | References `resolve_todo_parallel` command (not in Systematic) |
-| User value | PASS | General-purpose, not framework-specific |
-```
-
-If any criterion fails, document why and **do not proceed** unless the user explicitly overrides.
-
-## Phase 2: Mechanical Conversion
-
-**Consult `docs/CONVERSION-GUIDE.md` for the authoritative field mapping reference.** That document has the complete tables for frontmatter fields, tool names, paths, and edge cases.
-
-### 2a. Change Detection (CRITICAL)
-
-**The sync workflow MUST focus on converting CHANGED content only.** If upstream hasn't changed a section, do NOT modify it.
-
-1. **Compare content hashes** — Only process definitions whose `upstream_content_hash` differs from the current upstream.
-2. **Diff before converting** — Run `git diff` or text comparison to identify WHAT changed upstream.
-3. **Selective application** — Apply conversions ONLY to the changed portions. Preserve unchanged sections exactly as they are.
-
-**CRITICAL: Preserve Stable Content**
-
-| Content Type | Action |
-|--------------|--------|
-| `~/.config/opencode/` paths | **DO NOT CHANGE** — This is the correct OpenCode global config path |
-| `~/.opencode/` paths | **DO NOT CHANGE** — This is WRONG. If you see this, it was a mistake. The correct path is `~/.config/opencode/` |
-| `question` tool references | **DO NOT CHANGE** — This is the correct OpenCode tool name |
-| `AskUserQuestion` in existing bundled files | Change to `question` — this was a CC tool that must be converted |
-| Trailing newlines | **PRESERVE** — Do not strip EOL characters |
-
-**Path Conversion Rules (MEMORIZE):**
-
-| CC Path | OC Path | Notes |
-|---------|---------|-------|
-| `~/.claude/` | `~/.config/opencode/` | Global user config |
-| `.claude/` | `.opencode/` | Project-relative |
-| `~/.config/opencode/` | `~/.config/opencode/` | **ALREADY CORRECT — NEVER CHANGE** |
-| `~/.opencode/` | `~/.config/opencode/` | **WRONG PATH — FIX IF FOUND** |
-
-**Tool Name Conversion Rules (MEMORIZE):**
-
-| CC Tool | OC Tool | Direction |
-|---------|---------|-----------|
-| `AskUserQuestion` | `question` | CC → OC only |
-| `TodoWrite` | `todowrite` | CC → OC only |
-| `Task` | `task` | CC → OC only |
-| `question` | `question` | **ALREADY CORRECT — NEVER CHANGE** |
-| `todowrite` | `todowrite` | **ALREADY CORRECT — NEVER CHANGE** |
-
-> **Common mistake:** Converting `question` → `AskUserQuestion`. This is BACKWARDS. OpenCode uses lowercase tool names.
-
-### 2b. Apply Mechanical Converter
-
-Apply the existing converter pipeline. This handles:
-- Tool name mappings (`Task` -> `task`, `TodoWrite` -> `todowrite`, etc.)
-- Path replacements (`.claude/` -> `.opencode/`, `CLAUDE.md` -> `AGENTS.md`)
-- Prefix conversions (`compound-engineering:` -> `systematic:`)
-- Frontmatter field mapping (tools array->map, permissionMode->permission, maxSteps->steps, etc.)
-- Model normalization (provider prefix)
-
-> **Note:** The `color` field in CC agent frontmatter (e.g., `color: violet`) is passed through by the converter. OpenCode does not currently use this field, but it is harmless to keep.
-
-**For a single file via CLI:**
-
-```bash
-bun src/cli.ts convert agent /tmp/upstream-security-sentinel.md
-```
-
-> **Tip:** Use `bun src/cli.ts` for local development instead of `bunx systematic` to avoid slow resolution.
-
-**For programmatic use:**
-
-```typescript
-import { convertContent } from './lib/converter.js'
-const converted = convertContent(upstreamContent, 'agent')
-```
-
-**What mechanical conversion does NOT handle** (requires intelligent rewrite):
-- Description enhancement with trigger conditions
-- Branding beyond regex patterns (contextual references to "Claude Code", "Claude", "CEP")
-- Content restructuring for Systematic's style
-- Removing CC-specific features with no OC equivalent (`${CLAUDE_SESSION_ID}`)
-- Adding Systematic-specific sections (integration points, skill cross-references)
-- **Skill path patterns in body text** — the converter handles `.claude/` -> `.opencode/` in general, but does NOT handle `~/.claude/skills/` -> `~/.config/opencode/skills/` or bare `.claude/skills/` -> `.opencode/skills/` references embedded in skill-discovery instructions. Audit these manually.
-- **Tool/path references inside code blocks** — the converter skips fenced code blocks to avoid false positives, so `Task()`, `TodoWrite`, `CLAUDE.md`, `.claude/skills/` inside ``` blocks must be fixed manually
-- **Attribution badges and footers** inside heredoc code blocks (commit messages, PR bodies)
-- **CC-specific features with no OC equivalent** (Swarm Mode / `Teammate` API, "remote" execution via Claude Code web)
-- **CC-specific exclusion rules** — some agents reference `compound-engineering pipeline artifacts` or other CC-specific content (e.g., `docs/plans/*.md` exclusions) that should be removed as they're not applicable to Systematic
-- **Frontmatter quoting normalization** — the converter may change double quotes to single quotes in `argument-hint` and other frontmatter string values. This is cosmetic but verify quoting consistency after conversion.
-
-### 2c. Mandatory Post-Conversion Fixup (Batch sed)
-
-**LLMs consistently fail at simple string replacement.** The mechanical converter catches some patterns, but many remain — particularly inside code blocks, multi-platform tool lists, and contextual references. After every conversion (both mechanical converter AND LLM rewrite), run these deterministic sed replacements as a mandatory safety net.
-
-**Replacement order matters.** Apply more-specific patterns before general ones to avoid double-conversion. Run all phases in sequence on each file:
-
-```bash
-# Target: all converted .md files EXCEPT exclusions (see below)
-# PHASE A: compound-engineering → systematic (most specific first)
-sed -i '' \
- -e 's/compound-engineering\.local\.md/systematic.local.md/g' \
- -e 's/compound-engineering-plugin/systematic/g' \
- -e 's/compound-engineering pipeline artifacts/systematic pipeline artifacts/g' \
- -e 's|\.context/compound-engineering/|.context/systematic/|g' \
- -e 's|plugins/compound-engineering/|plugins/systematic/|g' \
- -e 's/compound-engineering:/systematic:/g' \
- -e 's/Compound_Engineering/Systematic/g' \
- -e 's/Compound Engineering/Systematic/g' \
- -e 's/compound-engineering/systematic/g' \
- "$FILE"
-
-# PHASE B: Path conversions (most specific first)
-sed -i '' \
- -e 's|~/\.claude/skills/|~/.config/opencode/skills/|g' \
- -e 's|~/\.claude/settings\.json|~/.config/opencode/settings.json|g' \
- -e 's|~/\.claude/|~/.config/opencode/|g' \
- -e 's|`\.claude/skills/|`.opencode/skills/|g' \
- -e 's|`\.claude/settings\.json|`.opencode/settings.json|g' \
- -e 's| \.claude/skills/| .opencode/skills/|g' \
- -e 's| \.claude/settings\.json| .opencode/settings.json|g' \
- -e 's|(\.claude/|(\.opencode/|g' \
- -e 's|`\.claude/|`.opencode/|g' \
- -e 's| \.claude/| .opencode/|g' \
- -e 's|"\.claude/|".opencode/|g' \
- -e 's|CLAUDE\.md|AGENTS.md|g' \
- "$FILE"
-
-# PHASE C: Branding and tool names
-sed -i '' \
- -e 's/Claude Code/OpenCode/g' \
- -e 's/claude-code/opencode/g' \
- -e 's/AskUserQuestion/question/g' \
- -e 's/TaskCreate/todowrite/g' \
- "$FILE"
-```
-
-#### File Exclusions
-
-| File/Pattern | Reason | What to convert instead |
-|---|---|---|
-| `sync-manifest.json` | Upstream paths (`plugins/compound-engineering/...`) are correct source references | Never run sed on manifest upstream paths |
-| `claude-permissions-optimizer/SKILL.md` | Skill targets Claude Code settings files — CC refs and `.claude/` paths are intentional | Only convert: prefix (`compound-engineering:` → `systematic:`), tool names (`AskUserQuestion` → `question`), product name (`Compound Engineering` → `Systematic`) |
-
-#### Edge Cases Requiring Manual Review After sed
-
-These patterns recur across every sync and need manual attention:
-
-| Pattern | Problem | Fix |
-|---|---|---|
-| `EveryInc/systematic` in URLs | Over-conversion of `EveryInc/compound-engineering-plugin` | Use generic example (`acme/my-app`) or remove entirely |
-| `claude.com/opencode` | `Claude Code` → `OpenCode` mangles URLs like `claude.com/claude-code` | Replace with `opencode.ai` |
-| CEP version attribution badges | `[![Compound Engineering v[VERSION]]...]` becomes `[![Systematic v[VERSION]]...]` with dead URL | Remove the entire badge line |
-| `Task()` in code blocks | Uppercase `Task()` in code examples not caught by branding sed | Manually fix to `task()` |
-| `https://github.com/EveryInc/systematic` | Non-existent URL from catch-all sed | Context-dependent: remove, genericize, or update to actual Systematic repo URL |
-| Multi-platform tool lists | `(question in OpenCode, ...)` after conversion — verify the list is coherent | Should read naturally with OpenCode as the primary platform |
-
-#### Post-Fixup Verification (MANDATORY)
-
-After running the batch sed fixup, verify with grep. Both checks MUST pass:
-
-```bash
-# CHECK 1: Remaining CC/CEP refs (should be zero for non-exception files)
-grep -rnE 'Claude Code|claude-code|\.claude/|CLAUDE\.md|~/\.claude|AskUserQuestion|TaskCreate|compound-engineering|Compound Engineering|Compound_Engineering|EveryInc/systematic|claude\.com/opencode'
-
-# CHECK 2: Over-conversions (should ALWAYS be zero across ALL files)
-grep -rnE '\.opencode/\.opencode/|config/opencode/\.config/|systematic\.systematic|systematic:systematic:|opencode\.ai/opencode'
-```
-
-If CHECK 1 returns hits on non-exception files, fix them. If CHECK 2 returns any hits, you have a double-conversion bug — investigate immediately.
-
-## Phase 3: Intelligent Rewrite
-
-This is the critical step that distinguishes a good import from a broken one. Every converted definition MUST go through intelligent rewrite.
-
-### 3a. Description Rewrite
-
-CC descriptions are typically short and capability-focused. OC descriptions must include **trigger conditions** for auto-invocation.
-
-**CC style (bad for OC):**
-```yaml
-description: Reviews code for unnecessary complexity and suggests simplifications
-```
-
-**OC/Systematic style (good):**
-```yaml
-description: "Use this agent when you need a final review pass focused on simplicity, YAGNI principles, and removing unnecessary complexity. ..."
-```
-
-**Rewrite rules for descriptions:**
-- Start with "Use this agent/skill/command when..." or "This skill should be used when..."
-- Include specific trigger symptoms and situations
-- For agents: include `` blocks showing context + user message + assistant response + commentary
-- Max 1024 characters total for skill descriptions
-- Write in third person (injected into system prompt)
-
-### 3b. Branding Audit
-
-The mechanical converter catches regex-matchable patterns. You must catch contextual ones:
-
-| Pattern to find | Replacement |
-|-----------------|-------------|
-| "Claude Code" (the product) | "OpenCode" |
-| "Claude" (the AI model, in instructions) | Keep — model name is fine |
-| "CEP" or "Compound Engineering Plugin" | "Systematic" |
-| "claude-code" (in paths not caught by converter) | "opencode" |
-| References to CC-specific behavior | Adapt or remove |
-| `Task agent-name("prompt")` patterns | `task` tool or `@agent-name` |
-| `TodoWrite` in prose (not just tool calls) | `todowrite` or "update your task list" |
-| "the built-in grep tool" (lowercase tool name) | "the built-in Grep tool" (OC capitalizes tool names) |
-| `compound-engineering pipeline artifacts` | Remove or replace with "systematic pipeline artifacts" |
-| Version attribution footers (e.g., `*Based on Claude Code v2.1.19*`) | **Remove entirely** — CC version numbers are not applicable to Systematic. Blind `Claude Code` → `OpenCode` rewrite turns these into nonsensical `Based on OpenCode v2.1.19`. |
-| Source attribution URLs (e.g., `claude.com`, `docs.anthropic.com`) | **Keep as-is** — these are upstream source references, not branding |
-
-### 3c. Content Adaptation
-
-Review and adapt the body content for Systematic's style:
-
-**For agents:**
-- Ensure the system prompt is self-contained (agent definitions are the full prompt)
-- Add structured output format if missing (numbered phases, deliverables)
-- Reference Systematic tools and skill cross-references where appropriate
-- Match temperature/mode conventions (see `docs/CONVERSION-GUIDE.md`)
-
-**For skills:**
-- Add "When to Use" / "When NOT to Use" sections if missing
-- Add integration points with other Systematic workflows
-- Add cross-references to related bundled skills/commands
-- Ensure progressive disclosure (SKILL.md + references/ if content is heavy)
-
-**For commands:**
-- Verify slash command sequences use `systematic:` prefix
-- Ensure `$ARGUMENTS` and positional args are correct (OC is 1-indexed)
-- Verify referenced agents and skills exist in Systematic's bundled set
-- **Keep references to not-yet-imported agents/skills** — they will be brought over later using this same skill and will keep their names. Only apply mechanical CC→OC tool/path conversion, not removal.
-
-### 3d. Code Block Audit
-
-The mechanical converter intentionally skips content inside fenced code blocks to avoid false positives. You MUST manually audit all code blocks for:
-
-| Pattern in code blocks | Action |
-|------------------------|--------|
-| `Task(agent-name)` or `Task({ ... })` | → `task(agent-name)` / `task({ ... })` |
-| `TodoWrite` | → `todowrite` |
-| `CLAUDE.md` | → `AGENTS.md` |
-| `.claude/skills/` or `.claude/` paths | → `.opencode/skills/` or `.opencode/` |
-| `Teammate({ operation: ... })` | Add aspirational note or adapt to `task` with background execution |
-| Attribution badges/footers (`Compound Engineered`, `Claude Code` links) | → Systematic branding |
-| `AskUserQuestion` | → `question tool` |
-
-> **High-risk pattern:** Long skills with many code examples (e.g., orchestrating-swarms has 47 `Task({` calls across 1700+ lines). After mechanical conversion, run a targeted search for capitalized tool names inside code blocks: `grep -n "Task(\|TodoWrite\|AskUserQuestion" `. Fix all occurrences — users copying broken examples will get runtime errors.
-
-### 3e. CC-Specific Features
-
-Some CC features have no direct OC equivalent. For each, make a case-by-case decision:
-
-| CC Feature | OC Equivalent | Recommendation |
-|------------|---------------|----------------|
-| `Teammate` API (spawnTeam, requestShutdown, cleanup) | None — `task` with `run_in_background` is partial | Keep as aspirational reference with explanatory note |
-| "Remote" execution (Claude Code web background) | None | Remove — no OC equivalent exists |
-| `${CLAUDE_SESSION_ID}` | None | Remove (keep in upstream API spec docs as-is) |
-| `${CLAUDE_PLUGIN_ROOT}` | None — bundled skills use relative paths | Simplify to relative paths (e.g., `scripts/worktree-manager.sh` not `${CLAUDE_PLUGIN_ROOT}/skills/git-worktree/scripts/worktree-manager.sh`) |
-| `AskUserQuestion` with complex schemas | `question` tool (simpler) | Adapt to OC's question tool format |
-
-Present CC-specific feature decisions to the user before proceeding.
-
-### 3f. Quality Checklist
-
-Before writing the file, verify:
-
-- [ ] Frontmatter parses without errors
-- [ ] Description includes trigger conditions (not just capability summary)
-- [ ] No stale references to Claude Code, CEP, `.claude/` paths
-- [ ] Tool references use OC names (`task`, `todowrite`, `google_search`, `systematic_skill`)
-- [ ] **Code blocks audited** — tool names, paths, and branding inside ``` blocks are fixed
-- [ ] Attribution badges/footers in heredoc code blocks updated to Systematic branding
-- [ ] CC-specific features with no OC equivalent handled (removed, adapted, or noted as aspirational)
-- [ ] Cross-referenced agents/skills/commands exist in Systematic (or are marked for future import)
-- [ ] **Path sanity check** — No `~/.opencode/` paths (should be `~/.config/opencode/`)
-- [ ] **Tool name sanity check** — No `AskUserQuestion` (should be `question`)
-- [ ] **Formatting preserved** — Trailing newlines maintained, no gratuitous whitespace changes
-- [ ] Content matches Systematic's style (structured phases, explicit deliverables)
-- [ ] Agent `mode` field is set (`subagent`, `primary`, or `all`)
-- [ ] Agent `temperature` is appropriate for the agent's purpose
-- [ ] **Batch sed fixup ran** — Phase 2c sed commands executed on all converted files
-- [ ] **Grep verification passed** — Phase 2c CHECK 1 (remaining refs) and CHECK 2 (over-conversions) both clean
-- [ ] **Edge cases reviewed** — Checked for mangled URLs, dead badge links, uppercase `Task()` in code blocks (see Phase 2c edge cases table)
-
-### 3g. Discrepancy Reporting
-
-If you encounter content that looks incorrect but wasn't changed by upstream:
-
-1. **DO NOT silently fix it** — You might break something
-2. **Report it as a discrepancy** — Include in the sync summary
-3. **Flag for human review** — "Found existing path `~/.opencode/` in line X — should this be `~/.config/opencode/`?"
-
-Example discrepancy report:
-```markdown
-### Discrepancies Found (not from upstream changes)
-
-| File | Line | Issue | Recommended Action |
-|------|------|-------|-------------------|
-| skills/foo/SKILL.md | 42 | Uses `~/.opencode/` path | Verify if intentional, fix to `~/.config/opencode/` if not |
-| commands/bar.md | 15 | Uses `AskUserQuestion` | Convert to `question` tool |
-```
-
-## Phase 4: Write and Register
-
-### 4a. Place the File
-
-| Type | Location | Naming |
-|------|----------|--------|
-| Agent | `agents//.md` | kebab-case, category from purpose |
-| Skill | `skills//SKILL.md` | kebab-case directory |
-| Command | `commands/.md` or `commands/workflows/.md` | kebab-case, `workflows/` for workflow commands |
-
-Categories for agents: `design/`, `research/`, `review/`, `workflow/`
-
-### 4b. Update sync-manifest.json
-
-**Every imported definition MUST have a manifest entry.** This is not optional.
-
-**Use the `date` CLI to generate the `synced_at` timestamp in ISO 8601 UTC format:**
-
-```bash
-date -u +'%Y-%m-%dT%H:%M:%SZ'
-```
-
-Run this once at the start of a batch import and use the same timestamp for all entries in that batch.
-
-```json
-{
- "definitions": {
- "agents/review/security-sentinel": {
- "source": "cep",
- "upstream_path": "plugins/compound-engineering/agents/review/security-sentinel.md",
- "upstream_commit": "abc123def456...",
- "synced_at": "",
- "notes": "Imported from CEP. Enhanced description with trigger examples. Updated tool references.",
- "upstream_content_hash": "sha256-of-upstream-content",
- "rewrites": [
- {
- "field": "description",
- "reason": "CC description lacked OC trigger conditions and examples",
- "original": "Security audits, vulnerability assessment, OWASP compliance"
- },
- {
- "field": "body:branding",
- "reason": "Contextual reference to Claude Code in analysis instructions"
- }
- ],
- "manual_overrides": []
- }
- }
-}
-```
-
-**Manifest key format:** Repo-relative path without file extension.
-- Agents: `agents//`
-- Skills: `skills/`
-- Commands: `commands/` or `commands/workflows/`
-
-**Multi-file skills MUST include a `files` array** listing all files in the skill directory (relative to the skill's upstream path). This is how the precheck script knows which files to hash for change detection. Without it, sub-file changes go undetected:
-
-```json
-{
- "skills/my-skill": {
- "files": ["SKILL.md", "references/guide.md", "scripts/setup.sh"],
- ...
- }
-}
-```
-
-**Ensure `sources` has an entry for the upstream repo:**
-
-```json
-{
- "sources": {
- "cep": {
- "repo": "EveryInc/compound-engineering-plugin",
- "branch": "main",
- "url": "https://github.com/EveryInc/compound-engineering-plugin"
- },
- "superpowers": {
- "repo": "obra/superpowers",
- "branch": "main",
- "url": "https://github.com/obra/superpowers"
- }
- }
-}
-```
-
-### 4c. Record Rewrites
-
-**Every intelligent rewrite MUST be logged in the `rewrites` array.** This is how future syncs know what to re-apply.
-
-Each rewrite entry needs:
-- `field`: What was changed (`description`, `body:branding`, `body:tool-references`, `body:structure`, `frontmatter:`)
-- `reason`: Why (one sentence)
-- `original`: The original value before rewrite (optional but recommended for descriptions)
-
-### 4d. Respect Manual Overrides
-
-If `manual_overrides` contains entries, those fields/sections were customized after import. On re-import:
-1. Read the current bundled file
-2. Extract the overridden fields/sections
-3. Apply conversion to non-overridden content only
-4. Merge overrides back in
-5. Update manifest `synced_at` and `upstream_commit` but keep `manual_overrides` intact
-
-**Override entries MUST be structured objects (string arrays are invalid):**
-
-```json
-{
- "manual_overrides": [
- {
- "field": "description",
- "reason": "Customized triggers for our auth-heavy codebase",
- "original": "Security audits, vulnerability assessment, OWASP compliance",
- "overridden_at": "2026-02-10T06:30:00Z"
- }
- ]
-}
-```
-
-Each entry has:
-- `field`: Same naming convention as `rewrites[].field` (e.g., `description`, `body:section-name`, `frontmatter:`, `*` for full local ownership)
-- `reason`: Why the override exists — one sentence
-- `original`: Pre-override value (for conflict detection and rollback; truncate to 500 chars for large sections)
-- `overridden_at`: ISO 8601 UTC timestamp (`date -u +'%Y-%m-%dT%H:%M:%SZ'`)
-
-**Override vs rewrite precedence:** If a field appears in both `rewrites` and `manual_overrides`, the manual override takes precedence. The rewrite is kept for historical record but will NOT be re-applied to that field on re-sync.
-
-### 4e. Record Manual Edit
-
-When you make a targeted edit to an already-imported definition file (NOT during initial import — this is for post-import customization):
-
-1. **Before editing**: Read and store the current value of the field(s) you will change
-2. **Make your edit** to the bundled definition file
-3. **Update `sync-manifest.json`**:
- a. Read the current manifest entry for this definition
- b. For each field you changed:
- - If field is already in `manual_overrides`: update `reason` if it changed, keep the `original` from the FIRST override (don't overwrite history), keep `overridden_at` unchanged
- - If field is NOT in `manual_overrides`: add new entry with `field`, `reason`, `original` (value from step 1), and `overridden_at` (current UTC timestamp)
- c. Write the updated manifest
-4. **Validate**: `cat sync-manifest.json | python3 -m json.tool > /dev/null` (confirm valid JSON)
-
-**Idempotency rules:**
-- Running this workflow twice for the same edit MUST NOT duplicate entries (check `field` name before adding)
-- MUST NOT overwrite the `original` value (first override's original is canonical)
-- MUST NOT change `overridden_at` if the field was already overridden
-
-| Thought | Reality |
-|---------|---------|
-| "I'll update the manifest later" | Update NOW, in the same operation as the edit |
-| "The reason is obvious" | Future agents can't read your mind. Write it. |
-| "I don't need to store the original" | Without it, you can't detect conflicts on re-sync |
-| "This field is too small to track" | If you changed it, track it. No exceptions. |
-
-## Phase 5: Verify
-
-### 5a. Build and Test
-
-```bash
-bun run build && bun run typecheck && bun run lint && bun test
-```
-
-### 5b. Validate Converted Content
-
-```bash
-# Check the definition loads via the plugin
-bun src/cli.ts list agents
-bun src/cli.ts list skills
-bun src/cli.ts list commands
-
-# For agents: verify frontmatter extracts correctly
-bun test tests/unit/agents.test.ts
-
-# For skills: verify skill loading
-bun test tests/unit/skills.test.ts
-
-# Integration: verify conversion round-trip
-bun test tests/integration/converter-validation.test.ts
-```
-
-### 5c. Manual Spot-Check
-
-For each converted definition, verify:
-1. `systematic_skill` tool lists it (for skills)
-2. Agent config merges correctly (for agents)
-3. Command is available via slash prefix (for commands)
-
-## Re-Sync Workflow (Updating Existing Definitions)
-
-When pulling upstream changes for an already-imported definition:
-
-1. **Read manifest entry** — Get `upstream_content_hash`, `rewrites`, `manual_overrides`
-2. **Fetch upstream** — Get current content and compute hash
-3. **Compare hashes** — If unchanged, skip (idempotent)
-4. **Diff upstream changes** — Use `git diff` or text diff to understand what changed
-5. **Re-apply mechanical conversion** on the new upstream content
-6. **Re-apply rewrites** from the manifest log — same fields, same reasons, adapted to new content (skip fields that have manual overrides — overrides take precedence)
-7. **Handle manual overrides** using the merge matrix below
-8. **Update manifest** — New commit SHA, hash, timestamp (`date -u +'%Y-%m-%dT%H:%M:%SZ'`). Update rewrites if they changed.
-9. **Verify** — Build, test, spot-check
-
-### Issue/PR Dedupe and Reporting
-
-When running automated syncs, always:
-- Reuse branch `chore/sync-cep` for all sync PRs.
-- If a PR exists for that branch, update it instead of creating a new one.
-- Use or create a tracking issue labeled `sync-cep` and append run summaries as comments.
-
-Include the following sections in both issue and PR bodies:
-- Summary
-- Hash changes table (definition, old hash, new hash)
-- Conflicts (manual overrides)
-- New upstream definitions (report-only)
-- Upstream deletions (report-only, include keep/remove prompt)
-- Rewrite failures
-- Phantom references (commands referencing missing agents/skills)
-
-### Override Merge Matrix
-
-| Scenario | Detection | Agent Behavior |
-|----------|-----------|----------------|
-| Upstream unchanged + override exists | New upstream hash matches stored hash | **Preserve override.** No action needed. |
-| Upstream changed + override on SAME field | Changed upstream field overlaps `manual_overrides[].field` | **Flag conflict.** Present both versions to user. Do NOT auto-merge. |
-| Upstream changed + override on DIFFERENT field | Changed fields don't intersect with override fields | **Apply upstream changes normally**, preserve override fields. |
-| Override is `"*"` (full local ownership) | Any upstream change | **Skip re-sync entirely.** Log that upstream was skipped. |
-
-### Conflict Presentation
-
-When upstream changes conflict with a manual override, present to the user:
-
-```markdown
-CONFLICT: `agents/review/security-sentinel` field `description`
-
-**Your override** (overridden_at: 2026-02-15T10:30:00Z):
-> "Use when auditing authentication flows for OWASP Top 10..."
-
-**Upstream change** (commit abc123):
-> "Security audits with enhanced SAST integration..."
-
-**Override reason**: "Customized triggers for our auth-heavy codebase"
-
-Options:
-1. Keep override (skip upstream change for this field)
-2. Accept upstream (remove override entry from manifest)
-3. Merge manually (edit the field, then record as new override via Phase 4e)
-```
-
-## Batch Import
-
-For importing multiple definitions at once:
-
-1. **Get shared metadata** — fetch the upstream commit SHA and generate a UTC timestamp (`date -u +'%Y-%m-%dT%H:%M:%SZ'`) once at the start. Use the same values for all entries in the batch.
-2. **Fetch all upstream files** — use the batch fetch pattern from Phase 1a to download all targets in one pass.
-3. List target definitions (files to import)
-4. For each definition, run Phases 1-4 sequentially (converter + intelligent rewrite + write + manifest)
-5. Run Phase 5 once at the end (build/test covers all)
-6. Commit all changes together with a descriptive message:
-
-```bash
-git add agents/ skills/ commands/ sync-manifest.json
-git commit -m "feat: import N definitions from CEP upstream (commit abc123)"
-```
-
-## Red Flags — STOP If You Think This
-
-| Thought | Reality |
-|---------|---------|
-| "The converter handles everything" | Converter is mechanical only — you MUST do intelligent rewrite |
-| "The description is fine as-is" | CC descriptions lack trigger conditions. Always rewrite. |
-| "I'll update the manifest later" | Manifest update is part of the workflow, not a follow-up |
-| "This is just a quick import" | Every import requires evaluation, conversion, rewrite, manifest, verification |
-| "I don't need to check for branding" | Regex misses contextual references. Always audit manually. |
-| "I'll skip the evaluation since the user asked for it" | User request != automatic fit. Evaluate and present findings. |
-| "Manual overrides don't apply — this is a new import" | Check anyway. Previous imports may have been customized since. |
-
-## Anti-Patterns
-
-| Anti-Pattern | Correct Approach |
-|--------------|-----------------|
-| Copy file and only run converter | Always do intelligent rewrite pass |
-| Import everything from upstream | Curate — evaluate fit before importing |
-| Skip manifest update | Every import MUST have a manifest entry |
-| Overwrite human edits on re-sync | Check `manual_overrides` first, use merge matrix |
-| Leave CC descriptions as-is | Rewrite with OC trigger conditions |
-| Forget to log rewrites | Every intelligent change goes in `rewrites[]` |
-| Import definition that duplicates existing | Enhance existing instead |
-| Skip branding audit | Always check for contextual Claude/CEP references |
-
-## Handling Upstream Deletions
-
-If an upstream file was deleted but the manifest still has an entry:
-
-1. The `findStaleEntries()` function in `src/lib/manifest.ts` detects these
-2. Do NOT auto-remove the bundled definition — it may have been intentionally kept
-3. Flag to the user: "Upstream deleted ``. Keep local copy or remove?"
-4. If removing: delete the bundled file AND the manifest entry
-5. If keeping: add `"manual_overrides": [{"field": "*", "reason": "Local ownership", "overridden_at": ""}]` to indicate full local ownership
-
-## Reference: Existing Bundled Content
-
-**Always check live inventory** — this list may be stale:
-
-```bash
-bun src/cli.ts list agents
-bun src/cli.ts list skills
-bun src/cli.ts list commands
-```
-
-## Reference: Key Files
-
-| File | Purpose |
-|------|---------|
-| `src/lib/converter.ts` | Mechanical conversion pipeline |
-| `src/lib/manifest.ts` | Manifest types and read/write/validate |
-| `sync-manifest.json` | Provenance data (repo root) |
-| `sync-manifest.schema.json` | JSON Schema for manifest |
-| `docs/CONVERSION-GUIDE.md` | Full field mapping reference |
diff --git a/AGENTS.md b/AGENTS.md
index 7c0f390..39dd96d 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -4,7 +4,7 @@
## Overview
-OpenCode plugin providing structured engineering workflows. Ported from the [Compound Engineering Plugin (CEP)](https://github.com/EveryInc/compound-engineering-plugin) for Claude Code, with improvements and OpenCode SDK integration. Converts CC-format agents and skills to OpenCode format. Tracks upstream provenance via `sync-manifest.json`.
+OpenCode plugin providing structured engineering workflows for AI-powered development. Originally adapted from the [Compound Engineering Plugin (CEP)](https://github.com/EveryInc/compound-engineering-plugin) for Claude Code, Systematic now evolves independently with its own direction for advanced AI workflows. The CLI retains CC-format conversion capabilities for ad-hoc imports. Historical provenance is tracked in `sync-manifest.json`.
**Two distinct parts:**
1. **TypeScript source** (`src/`) — Plugin logic, tools, config handling
@@ -48,19 +48,19 @@ systematic/
│ └── lib/ # 13 core modules (see src/lib/AGENTS.md)
├── skills/ # 48 bundled skills (SKILL.md format)
├── agents/ # 29 bundled agents (5 categories: design/docs/research/review/workflow)
-├── commands/ # Empty (.gitkeep) — all commands converted to skills in CEP sync
+├── commands/ # Empty (.gitkeep) — commands converted to skills; dir kept for backward compat
├── docs/ # Starlight docs workspace (see docs/AGENTS.md)
│ ├── scripts/ # Content generation from bundled assets
│ └── src/content/ # Manual guides + generated reference
├── registry/ # OCX registry config + profiles (omo, standalone)
-├── scripts/ # Build scripts (build-registry.ts, check-cep-upstream.ts)
+├── scripts/ # Build scripts (build-registry.ts)
├── assets/ # Static assets (banner SVG)
├── tests/
│ ├── unit/ # 13 test files
│ └── integration/ # 2 test files
├── .opencode/ # Project-specific OC config + skills + commands
-│ ├── skills/ # Project-only skills (convert-cc-defs)
-│ └── commands/ # Project-only commands (generate-readme, sync-cep)
+│ ├── skills/ # Project-only skills
+│ └── commands/ # Project-only commands (generate-readme)
├── sync-manifest.json # Upstream provenance tracking
└── dist/ # Build output
```
@@ -74,7 +74,7 @@ systematic/
| Skill tool implementation | `src/lib/skill-tool.ts` |
| Skill loading + formatting | `src/lib/skill-loader.ts` |
| Bootstrap injection | `src/lib/bootstrap.ts` |
-| CEP→OpenCode conversion | `src/lib/converter.ts` |
+| CC→OpenCode conversion (CLI) | `src/lib/converter.ts` |
| YAML frontmatter parsing | `src/lib/frontmatter.ts` |
| Agent config validation + type guards | `src/lib/validation.ts` |
| Asset discovery | `src/lib/skills.ts`, `agents.ts`, `commands.ts` |
@@ -84,9 +84,7 @@ systematic/
| CLI commands | `src/cli.ts` |
| Add new skill | `skills//SKILL.md` |
| Add new agent | `agents//.md` |
-| Import from CEP upstream | `.opencode/skills/convert-cc-defs/SKILL.md` |
| OCX registry building | `scripts/build-registry.ts` |
-| Upstream sync checking | `scripts/check-cep-upstream.ts` |
| Docs content generation | `docs/scripts/transform-content.ts` |
| Docs site config | `docs/astro.config.mjs` |
@@ -98,7 +96,7 @@ systematic/
| `createConfigHandler` | fn | src/lib/config-handler.ts:215 | 3 | Config hook — merges bundled assets |
| `createSkillTool` | fn | src/lib/skill-tool.ts:87 | 3 | systematic_skill tool factory |
| `getBootstrapContent` | fn | src/lib/bootstrap.ts:32 | 3 | System prompt injection |
-| `convertContent` | fn | src/lib/converter.ts:371 | 4 | CEP→OpenCode body conversion |
+| `convertContent` | fn | src/lib/converter.ts:371 | 4 | CC→OpenCode body conversion |
| `convertFileWithCache` | fn | src/lib/converter.ts:411 | 6 | Cached file conversion (mtime invalidation) |
| `findSkillsInDir` | fn | src/lib/skills.ts:90 | 6 | Skill discovery (highest centrality) |
| `findAgentsInDir` | fn | src/lib/agents.ts:49 | 4 | Agent discovery (category from subdir) |
@@ -153,7 +151,7 @@ All disabled lists merge (union), bootstrap config shallow-merges.
## Upstream Sync
-CEP definitions are imported via the `convert-cc-defs` skill (`.opencode/skills/`). `sync-manifest.json` tracks provenance: upstream commit, content hash, rewrites applied, and manual overrides. Re-sync compares hashes for idempotency.
+CEP definitions were historically imported via the `convert-cc-defs` skill. `sync-manifest.json` tracks provenance: upstream commit, content hash, rewrites applied, and manual overrides. **Automated sync is now disabled** — Systematic evolves independently. The CLI `convert` command remains available for ad-hoc CC→OpenCode conversions.
The latest upstream sync (commit 74fb717) converted all commands to skills — `commands/` now contains only `.gitkeep`. Command code paths (`findCommandsInDir`, `loadCommandAsConfig`) remain for backward compatibility and project-specific commands.
@@ -167,5 +165,5 @@ The latest upstream sync (commit 74fb717) converted all commands to skills — `
- Use `bun src/cli.ts` for local dev instead of `bunx systematic` to avoid slow resolution
- `commands/` dir retained (with `.gitkeep`) for backward compatibility — code paths still support commands
- `registry/` provides OCX component-level installation with omo and standalone profiles
-- `scripts/check-cep-upstream.ts` detects new/changed upstream definitions for sync
-- `.opencode/commands/` has project-only commands: `generate-readme` (README generation), `sync-cep` (upstream sync)
+- `.opencode/commands/` has project-only commands: `generate-readme` (README generation)
+- `sync-manifest.json` is historical provenance data — no longer actively synced
diff --git a/scripts/check-cep-upstream.ts b/scripts/check-cep-upstream.ts
deleted file mode 100644
index 392a222..0000000
--- a/scripts/check-cep-upstream.ts
+++ /dev/null
@@ -1,448 +0,0 @@
-#!/usr/bin/env bun
-import { createHash } from 'node:crypto'
-import { CONVERTER_VERSION } from '../src/lib/converter.js'
-import { readManifest, type SyncManifest } from '../src/lib/manifest.js'
-
-export interface CheckSummary {
- hashChanges: string[]
- newUpstream: string[]
- newUpstreamFiles: Record
- deletions: string[]
- skipped: string[]
- converterVersionChanged: boolean
- errors: string[]
-}
-
-export interface CheckInputs {
- manifest: SyncManifest
- upstreamDefinitionKeys: string[]
- upstreamContents: Record
- treePaths: string[]
- converterVersion: number
-}
-
-export interface FetchResult {
- definitionKeys: string[]
- contents: Record
- treePaths: string[]
- hadError: boolean
-}
-
-const MANIFEST_PATH = 'sync-manifest.json'
-
-const hashContent = (content: string): string =>
- createHash('sha256').update(content).digest('hex')
-
-const joinUpstreamPath = (base: string, file: string): string =>
- `${base.replace(/\/$/, '')}/${file}`
-
-const CEP_PREFIX = 'plugins/compound-engineering/'
-
-export const toDefinitionKey = (path: string): string | null => {
- const prefix = CEP_PREFIX
- if (!path.startsWith(prefix)) return null
-
- const rest = path.slice(prefix.length)
- if (rest.startsWith('agents/') && rest.endsWith('.md')) {
- return rest.replace(/\.md$/, '')
- }
-
- if (rest.startsWith('commands/') && rest.endsWith('.md')) {
- return rest.replace(/\.md$/, '')
- }
-
- if (rest.startsWith('skills/')) {
- const parts = rest.split('/')
- if (parts.length === 2 && parts[1].endsWith('.md')) {
- return `${parts[0]}/${parts[1].replace(/\.md$/, '')}`
- }
- if (parts.length >= 3 && parts[2] === 'SKILL.md') {
- return `${parts[0]}/${parts[1]}`
- }
- }
-
- return null
-}
-
-const collectSkillFiles = (treePaths: string[], key: string): string[] => {
- const dirPrefix = `${CEP_PREFIX}${key}/`
- const files: string[] = []
- for (const path of treePaths) {
- if (path.startsWith(dirPrefix)) {
- files.push(path.slice(dirPrefix.length))
- }
- }
- return files.sort()
-}
-
-/**
- * Given the full tree paths and a set of new definition keys, collect all files
- * belonging to each new definition. For skills this means all files under the
- * skill directory; for agents/commands it's the single .md file.
- */
-export const collectNewUpstreamFiles = (
- treePaths: string[],
- newKeys: string[],
-): Record => {
- const result: Record = {}
- const treeSet = new Set(treePaths)
- for (const key of newKeys) {
- if (key.startsWith('skills/')) {
- const files = collectSkillFiles(treePaths, key)
- if (files.length > 0) {
- result[key] = files
- }
- } else {
- const filePath = `${CEP_PREFIX}${key}.md`
- if (treeSet.has(filePath)) {
- result[key] = [`${key.split('/').pop()}.md`]
- }
- }
- }
- return result
-}
-
-const hasWildcardOverride = (manifest: SyncManifest, key: string): boolean => {
- const overrides = manifest.definitions[key]?.manual_overrides ?? []
- return overrides.some((override) => override.field === '*')
-}
-
-const computeSkillHash = (
- basePath: string,
- files: string[],
- upstreamContents: Record,
- errors: string[],
-): string | null => {
- const ordered = [...files].sort()
- let hasMissing = false
- const parts: string[] = []
- for (const file of ordered) {
- const path = joinUpstreamPath(basePath, file)
- const content = upstreamContents[path]
- if (content == null) {
- errors.push(
- `Missing upstream content for sub-file (may be a transient fetch failure or the file was removed upstream): ${path}`,
- )
- hasMissing = true
- continue
- }
- parts.push(content)
- }
- if (hasMissing) return null
- return hashContent(parts.join('\0'))
-}
-
-const recordMissingContent = (
- upstreamContents: Record,
- path: string,
- errors: string[],
-): boolean => {
- if (path in upstreamContents) return false
- errors.push(
- `Missing upstream content for sub-file (may be a transient fetch failure or the file was removed upstream): ${path}`,
- )
- return true
-}
-
-const computeEntryHash = (
- entry: SyncManifest['definitions'][string],
- upstreamContents: Record,
- errors: string[],
-): string | null => {
- const upstreamPath = entry.upstream_path
- if (!entry.files?.length) {
- if (recordMissingContent(upstreamContents, upstreamPath, errors)) {
- return null
- }
- return hashContent(upstreamContents[upstreamPath] ?? '')
- }
-
- return computeSkillHash(upstreamPath, entry.files, upstreamContents, errors)
-}
-
-export const getRequiredUpstreamContentPaths = ({
- manifest,
- upstreamDefinitionKeys,
-}: {
- manifest: SyncManifest
- upstreamDefinitionKeys: string[]
-}): string[] => {
- const paths = new Set()
- for (const key of upstreamDefinitionKeys) {
- const entry = manifest.definitions[key]
- if (!entry) continue
- if (entry.files && entry.files.length > 0) {
- for (const file of entry.files) {
- paths.add(joinUpstreamPath(entry.upstream_path, file))
- }
- } else {
- paths.add(entry.upstream_path)
- }
- }
- return Array.from(paths).sort()
-}
-
-const isObject = (value: unknown): value is Record =>
- typeof value === 'object' && value !== null && !Array.isArray(value)
-
-const isString = (value: unknown): value is string => typeof value === 'string'
-
-const parseTreePaths = (raw: string): string[] => {
- let parsed: unknown
- try {
- parsed = JSON.parse(raw)
- } catch {
- return []
- }
-
- if (!isObject(parsed)) return []
- const tree = parsed.tree
- if (!Array.isArray(tree)) return []
-
- const results: string[] = []
- for (const item of tree) {
- if (!isObject(item)) continue
- if (item.type !== 'blob') continue
- if (!isString(item.path)) continue
- results.push(item.path)
- }
- return results
-}
-
-const MAX_RETRIES = 3
-const BASE_DELAY_MS = 1000
-const MAX_DELAY_MS = 10000
-
-const isRetryStatus = (status: number): boolean =>
- status === 403 || status === 429
-
-const readRetryAfterSeconds = (response: Response): number | null => {
- const value = response.headers.get('retry-after')
- if (!value) return null
- const parsed = Number.parseInt(value, 10)
- return Number.isNaN(parsed) ? null : parsed
-}
-
-const computeDelayMs = (attempt: number, response?: Response): number => {
- const retryAfter = response ? readRetryAfterSeconds(response) : null
- if (retryAfter != null) {
- return Math.min(retryAfter * 1000, MAX_DELAY_MS)
- }
- return Math.min(BASE_DELAY_MS * 2 ** (attempt - 1), MAX_DELAY_MS)
-}
-
-const sleep = (ms: number): Promise =>
- new Promise((resolve) => setTimeout(resolve, ms))
-
-const fetchWithRetry = async (
- url: string,
- fetchFn: (url: string) => Promise,
-): Promise<{ response: Response | null; hadError: boolean }> => {
- for (let attempt = 1; attempt <= MAX_RETRIES; attempt += 1) {
- const response = await fetchFn(url)
- if (response.ok) return { response, hadError: false }
- if (!isRetryStatus(response.status)) {
- return { response, hadError: true }
- }
- if (attempt === MAX_RETRIES) {
- return { response, hadError: true }
- }
- await sleep(computeDelayMs(attempt, response))
- }
-
- return { response: null, hadError: true }
-}
-
-export const fetchUpstreamData = async (
- repo: string,
- branch: string,
- paths: string[],
- fetchFn: (url: string) => Promise,
-): Promise => {
- let hadError = false
- const definitionKeys = new Set()
- const contents: Record = {}
-
- const treeUrl = `https://api.github.com/repos/${repo}/git/trees/${branch}?recursive=1`
- const treeResult = await fetchWithRetry(treeUrl, fetchFn)
- if (!treeResult.response || !treeResult.response.ok) {
- return { definitionKeys: [], contents: {}, treePaths: [], hadError: true }
- }
- const treeRaw = await treeResult.response.text()
- const treePaths = parseTreePaths(treeRaw)
- for (const path of treePaths) {
- const key = toDefinitionKey(path)
- if (key != null) definitionKeys.add(key)
- }
-
- for (const path of paths) {
- const contentUrl = `https://api.github.com/repos/${repo}/contents/${path}?ref=${branch}`
- const result = await fetchWithRetry(contentUrl, fetchFn)
- if (!result.response || !result.response.ok) {
- if (result.response?.status !== 404) {
- hadError = true
- }
- continue
- }
- const payload: unknown = await result.response.json()
- if (!isObject(payload) || !isString(payload.content)) {
- hadError = true
- continue
- }
- const decoded = Buffer.from(payload.content, 'base64').toString('utf8')
- contents[path] = decoded
- }
-
- return {
- definitionKeys: Array.from(definitionKeys).sort(),
- contents,
- treePaths,
- hadError,
- }
-}
-
-export const computeCheckSummary = ({
- manifest,
- upstreamDefinitionKeys,
- upstreamContents,
- treePaths,
- converterVersion,
-}: CheckInputs): CheckSummary => {
- const hashChanges: string[] = []
- const newUpstream: string[] = []
- const deletions: string[] = []
- const skipped: string[] = []
- const errors: string[] = []
-
- const manifestKeys = Object.keys(manifest.definitions)
- const upstreamSet = new Set(upstreamDefinitionKeys)
-
- for (const key of upstreamDefinitionKeys) {
- if (!manifest.definitions[key]) {
- newUpstream.push(key)
- }
- }
-
- for (const key of manifestKeys) {
- if (!upstreamSet.has(key)) {
- deletions.push(key)
- continue
- }
-
- if (hasWildcardOverride(manifest, key)) {
- skipped.push(key)
- continue
- }
-
- const entry = manifest.definitions[key]
- const currentHash = entry.upstream_content_hash ?? ''
- const nextHash = computeEntryHash(entry, upstreamContents, errors)
- if (!nextHash) {
- continue
- }
-
- if (nextHash !== currentHash) {
- hashChanges.push(key)
- }
- }
-
- const newUpstreamFiles = collectNewUpstreamFiles(treePaths, newUpstream)
-
- return {
- hashChanges,
- newUpstream,
- newUpstreamFiles,
- deletions,
- skipped,
- errors,
- converterVersionChanged:
- manifest.converter_version !== undefined &&
- manifest.converter_version !== converterVersion,
- }
-}
-
-export const hasChanges = (summary: CheckSummary): boolean => {
- return (
- summary.hashChanges.length > 0 ||
- summary.newUpstream.length > 0 ||
- summary.deletions.length > 0 ||
- summary.converterVersionChanged
- )
-}
-
-export const getExitCode = (
- summary: CheckSummary,
- hadError: boolean,
-): number => {
- if (hadError || summary.errors.length > 0) return 2
- return hasChanges(summary) ? 1 : 0
-}
-
-/**
- * Creates an authenticated fetch wrapper for GitHub API requests.
- *
- * Returns a fetch function that automatically includes GitHub authentication headers
- * when provided a token. Falls back to unauthenticated fetch for empty/missing tokens.
- * This increases API rate limits from 60 to 5000 requests/hour during CI runs.
- *
- * @param token - GitHub authentication token (PAT or fine-grained token)
- * @returns A fetch function with authentication headers, or raw fetch if no token
- */
-export const createAuthenticatedFetch = (
- token: string | undefined,
-): ((url: string) => Promise) => {
- if (!token) return fetch
- return (url: string) =>
- fetch(url, {
- headers: {
- Authorization: `Bearer ${token}`,
- Accept: 'application/vnd.github.v3+json',
- },
- })
-}
-
-const main = (): void => {
- const manifest = readManifest(MANIFEST_PATH)
- if (!manifest) {
- process.exit(2)
- }
-
- const source = manifest.sources.cep
- if (!source) {
- process.exit(2)
- }
-
- const run = async (): Promise => {
- const requiredPaths = getRequiredUpstreamContentPaths({
- manifest,
- upstreamDefinitionKeys: Object.keys(manifest.definitions),
- })
- const fetchFn = createAuthenticatedFetch(process.env.GITHUB_TOKEN)
- const fetchResult = await fetchUpstreamData(
- source.repo,
- source.branch,
- requiredPaths,
- fetchFn,
- )
-
- const summary = computeCheckSummary({
- manifest,
- upstreamDefinitionKeys: fetchResult.definitionKeys,
- upstreamContents: fetchResult.contents,
- treePaths: fetchResult.treePaths,
- converterVersion: CONVERTER_VERSION,
- })
-
- console.log(JSON.stringify(summary, null, 2))
- process.exit(getExitCode(summary, fetchResult.hadError))
- }
-
- run().catch((error: unknown) => {
- console.error('check-cep-upstream failed:', error)
- process.exit(2)
- })
-}
-
-if (import.meta.main) {
- main()
-}
diff --git a/skills/orchestrating-swarms/SKILL.md b/skills/orchestrating-swarms/SKILL.md
index 39764a9..8528987 100644
--- a/skills/orchestrating-swarms/SKILL.md
+++ b/skills/orchestrating-swarms/SKILL.md
@@ -314,7 +314,7 @@ task({
## Plugin Agent Types
-From the `compound-engineering` plugin (examples):
+From the Systematic plugin (examples):
### Review Agents
```javascript
diff --git a/tests/integration/opencode.test.ts b/tests/integration/opencode.test.ts
index d43a2e1..f05b3df 100644
--- a/tests/integration/opencode.test.ts
+++ b/tests/integration/opencode.test.ts
@@ -3,9 +3,7 @@ import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import type { Config } from '@opencode-ai/sdk'
-import { extractCommandFrontmatter } from '../../src/lib/commands.ts'
import { createConfigHandler } from '../../src/lib/config-handler.ts'
-import { parseFrontmatter } from '../../src/lib/frontmatter.ts'
const OPENCODE_AVAILABLE = (() => {
const result = Bun.spawnSync(['which', 'opencode'])
@@ -19,14 +17,6 @@ const OPENCODE_TEST_MODEL = 'opencode/big-pickle'
const REPO_ROOT = path.resolve(import.meta.dirname, '../..')
-interface PrecheckSummary {
- hashChanges: string[]
- newUpstream: string[]
- deletions: string[]
- skipped: string[]
- converterVersionChanged: boolean
-}
-
interface OpencodeResult {
stdout: string
stderr: string
@@ -45,59 +35,6 @@ function buildOpencodeConfig(): string {
})
}
-function buildSyncCepTestConfig(): string {
- const commandPath = path.join(REPO_ROOT, '.opencode/commands/sync-cep.md')
- const content = fs.readFileSync(commandPath, 'utf8')
- const { body } = parseFrontmatter(content)
- const frontmatter = extractCommandFrontmatter(content)
-
- return JSON.stringify({
- command: {
- 'sync-cep': {
- template: body.trim(),
- description: frontmatter.description,
- agent: frontmatter.agent,
- model: frontmatter.model,
- subtask: frontmatter.subtask,
- },
- },
- agent: {
- build: {
- permission: {
- edit: 'deny',
- bash: 'deny',
- },
- },
- },
- })
-}
-
-function buildSyncPrompt(
- summary: PrecheckSummary,
- scope: string,
- dryRun: boolean,
- exitCode: number = 1,
-): string {
- const dryRunFlag = dryRun ? '--dry-run' : ''
- const dryRunNotice = dryRun
- ? 'DRY-RUN MODE: Do not call any tools or external commands.'
- : ''
- return `/sync-cep ${scope} ${dryRunFlag}
-${dryRunNotice}
-
-${exitCode}
-
-
-${JSON.stringify(summary)}
-
-
-Note: headless CI run — user will not see live output.`
-}
-
-function shouldRunSync(exitCode: number): boolean {
- return exitCode !== 0 && exitCode !== -1
-}
-
async function runOpencode(
prompt: string,
options: RunOpencodeOptions,
@@ -214,85 +151,6 @@ describe.skipIf(!OPENCODE_AVAILABLE)('opencode integration', () => {
)
})
-describe('sync-cep workflow simulation', () => {
- const fixtures = [
- {
- name: 'hash-change',
- summary: {
- hashChanges: ['skills/brainstorming'],
- newUpstream: [],
- deletions: [],
- skipped: [],
- converterVersionChanged: false,
- },
- },
- {
- name: 'report-only',
- summary: {
- hashChanges: [],
- newUpstream: ['skills/new-skill'],
- deletions: ['agents/review/security-sentinel'],
- skipped: [],
- converterVersionChanged: false,
- },
- },
- {
- name: 'converter-version',
- summary: {
- hashChanges: [],
- newUpstream: [],
- deletions: [],
- skipped: [],
- converterVersionChanged: true,
- },
- },
- ]
-
- test.each(fixtures)('builds sync prompt for $name', ({ summary }) => {
- const prompt = buildSyncPrompt(summary, 'all', true)
- expect(prompt).toContain(JSON.stringify(summary))
- expect(prompt).toContain('/sync-cep all --dry-run')
- expect(prompt).toContain('1')
- expect(prompt).toContain('headless CI')
- })
-
- test('builds sync prompt with exit code 2 and errors', () => {
- const summary: PrecheckSummary = {
- hashChanges: ['skills/brainstorming'],
- newUpstream: [],
- deletions: [],
- skipped: [],
- converterVersionChanged: false,
- }
- const prompt = buildSyncPrompt(summary, 'all', true, 2)
- expect(prompt).toContain('2')
- expect(prompt).toContain(JSON.stringify(summary))
- expect(prompt).toContain('/sync-cep all --dry-run')
- })
-
- test('sync gate honors precheck exit codes', () => {
- expect(shouldRunSync(0)).toBe(false)
- expect(shouldRunSync(1)).toBe(true)
- expect(shouldRunSync(2)).toBe(true)
- expect(shouldRunSync(-1)).toBe(false)
- })
-
- test.skipIf(!OPENCODE_AVAILABLE)(
- 'runs sync-cep command with dry-run prompt',
- async () => {
- const prompt = buildSyncPrompt(fixtures[0].summary, 'all', true)
- const result = await runOpencode(prompt, {
- cwd: REPO_ROOT,
- configContent: buildSyncCepTestConfig(),
- })
-
- expect(result.exitCode).not.toBe(-1)
- expect(result.stdout).not.toMatch(/\n\s*[→$⚙]/)
- },
- TIMEOUT_MS * MAX_RETRIES,
- )
-})
-
describe('config handler integration', () => {
let testEnv: {
tempDir: string
@@ -416,43 +274,3 @@ describe('opencode availability check', () => {
expect(true).toBe(true)
})
})
-
-describe('convert-cc-defs skill discoverability', () => {
- test('SKILL.md exists and is readable', () => {
- const skillPath = path.join(
- REPO_ROOT,
- '.opencode/skills/convert-cc-defs/SKILL.md',
- )
- expect(fs.existsSync(skillPath)).toBe(true)
-
- const content = fs.readFileSync(skillPath, 'utf8')
- expect(content.length).toBeGreaterThan(0)
- })
-
- test('SKILL.md has valid frontmatter with name: convert-cc-defs', () => {
- const skillPath = path.join(
- REPO_ROOT,
- '.opencode/skills/convert-cc-defs/SKILL.md',
- )
- const content = fs.readFileSync(skillPath, 'utf8')
-
- const result = parseFrontmatter(content)
- expect(result.hadFrontmatter).toBe(true)
- expect(result.parseError).toBe(false)
- expect((result.data as Record).name).toBe(
- 'convert-cc-defs',
- )
- })
-
- test('SKILL.md contains Phase 2, Phase 3, and Phase 4 section headings', () => {
- const skillPath = path.join(
- REPO_ROOT,
- '.opencode/skills/convert-cc-defs/SKILL.md',
- )
- const content = fs.readFileSync(skillPath, 'utf8')
-
- expect(content).toContain('## Phase 2: Mechanical Conversion')
- expect(content).toContain('## Phase 3: Intelligent Rewrite')
- expect(content).toContain('## Phase 4: Write and Register')
- })
-})
diff --git a/tests/unit/check-cep-upstream.test.ts b/tests/unit/check-cep-upstream.test.ts
deleted file mode 100644
index 9651d87..0000000
--- a/tests/unit/check-cep-upstream.test.ts
+++ /dev/null
@@ -1,753 +0,0 @@
-import { describe, expect, it } from 'bun:test'
-import { createHash } from 'node:crypto'
-import {
- collectNewUpstreamFiles,
- computeCheckSummary,
- createAuthenticatedFetch,
- fetchUpstreamData,
- getExitCode,
- getRequiredUpstreamContentPaths,
- hasChanges,
- toDefinitionKey,
-} from '../../scripts/check-cep-upstream.ts'
-import type { SyncManifest } from '../../src/lib/manifest.ts'
-
-const hash = (content: string): string =>
- createHash('sha256').update(content).digest('hex')
-
-const baseManifest = (): SyncManifest => ({
- converter_version: 2,
- sources: {
- cep: {
- repo: 'EveryInc/compound-engineering-plugin',
- branch: 'main',
- url: 'https://github.com/EveryInc/compound-engineering-plugin',
- },
- },
- definitions: {
- 'agents/review/security-sentinel': {
- source: 'cep',
- upstream_path:
- 'plugins/compound-engineering/agents/review/security-sentinel.md',
- upstream_commit: 'abc123',
- synced_at: '2026-02-15T00:00:00Z',
- notes: 'test',
- upstream_content_hash: hash('agent'),
- },
- },
-})
-
-describe('check-cep-upstream helpers', () => {
- it('maps upstream paths to manifest definition keys', () => {
- expect(
- toDefinitionKey(
- 'plugins/compound-engineering/agents/review/security-sentinel.md',
- ),
- ).toBe('agents/review/security-sentinel')
- expect(
- toDefinitionKey(
- 'plugins/compound-engineering/commands/workflows/plan.md',
- ),
- ).toBe('commands/workflows/plan')
- expect(
- toDefinitionKey(
- 'plugins/compound-engineering/skills/agent-native-architecture/SKILL.md',
- ),
- ).toBe('skills/agent-native-architecture')
- expect(
- toDefinitionKey(
- 'plugins/compound-engineering/skills/agent-native-architecture/references/one.md',
- ),
- ).toBeNull()
- })
-
- it('collects upstream content paths for tracked definitions', () => {
- const manifest = baseManifest()
- manifest.definitions['skills/agent-native-architecture'] = {
- source: 'cep',
- upstream_path:
- 'plugins/compound-engineering/skills/agent-native-architecture',
- upstream_commit: 'abc123',
- synced_at: '2026-02-15T00:00:00Z',
- notes: 'test',
- files: ['SKILL.md', 'references/one.md'],
- upstream_content_hash: hash('ab'),
- }
-
- const required = getRequiredUpstreamContentPaths({
- manifest,
- upstreamDefinitionKeys: [
- 'agents/review/security-sentinel',
- 'skills/agent-native-architecture',
- ],
- })
-
- expect(required).toEqual([
- 'plugins/compound-engineering/agents/review/security-sentinel.md',
- 'plugins/compound-engineering/skills/agent-native-architecture/SKILL.md',
- 'plugins/compound-engineering/skills/agent-native-architecture/references/one.md',
- ])
- })
- it('returns no changes when hashes and converter version match', () => {
- const manifest = baseManifest()
- const upstreamContents = {
- 'plugins/compound-engineering/agents/review/security-sentinel.md':
- 'agent',
- }
-
- const summary = computeCheckSummary({
- manifest,
- upstreamDefinitionKeys: ['agents/review/security-sentinel'],
- upstreamContents,
- treePaths: [],
- converterVersion: 2,
- })
-
- expect(hasChanges(summary)).toBe(false)
- expect(summary.errors).toEqual([])
- expect(getExitCode(summary, false)).toBe(0)
- })
-
- it('reports hash changes when upstream content differs', () => {
- const manifest = baseManifest()
- const upstreamContents = {
- 'plugins/compound-engineering/agents/review/security-sentinel.md':
- 'changed',
- }
-
- const summary = computeCheckSummary({
- manifest,
- upstreamDefinitionKeys: ['agents/review/security-sentinel'],
- upstreamContents,
- treePaths: [],
- converterVersion: 2,
- })
-
- expect(summary.hashChanges).toEqual(['agents/review/security-sentinel'])
- expect(summary.errors).toEqual([])
- expect(hasChanges(summary)).toBe(true)
- expect(getExitCode(summary, false)).toBe(1)
- })
-
- it('reports converter version change', () => {
- const manifest = baseManifest()
- manifest.converter_version = 1
- const upstreamContents = {
- 'plugins/compound-engineering/agents/review/security-sentinel.md':
- 'agent',
- }
-
- const summary = computeCheckSummary({
- manifest,
- upstreamDefinitionKeys: ['agents/review/security-sentinel'],
- upstreamContents,
- treePaths: [],
- converterVersion: 2,
- })
-
- expect(summary.converterVersionChanged).toBe(true)
- expect(summary.errors).toEqual([])
- expect(hasChanges(summary)).toBe(true)
- })
-
- it('reports new upstream definitions and deletions', () => {
- const manifest = baseManifest()
- const upstreamContents = {
- 'plugins/compound-engineering/agents/review/security-sentinel.md':
- 'agent',
- }
-
- const summary = computeCheckSummary({
- manifest,
- upstreamDefinitionKeys: ['skills/new-skill'],
- upstreamContents,
- treePaths: [
- 'plugins/compound-engineering/skills/new-skill/SKILL.md',
- 'plugins/compound-engineering/skills/new-skill/references/guide.md',
- ],
- converterVersion: 2,
- })
-
- expect(summary.newUpstream).toEqual(['skills/new-skill'])
- expect(summary.newUpstreamFiles).toEqual({
- 'skills/new-skill': ['SKILL.md', 'references/guide.md'],
- })
- expect(summary.deletions).toEqual(['agents/review/security-sentinel'])
- expect(summary.errors).toEqual([])
- expect(hasChanges(summary)).toBe(true)
- })
-
- it('handles multi-file skills hashing', () => {
- const manifest: SyncManifest = {
- converter_version: 2,
- sources: {
- cep: {
- repo: 'EveryInc/compound-engineering-plugin',
- branch: 'main',
- url: 'https://github.com/EveryInc/compound-engineering-plugin',
- },
- },
- definitions: {
- 'skills/agent-native-architecture': {
- source: 'cep',
- upstream_path:
- 'plugins/compound-engineering/skills/agent-native-architecture',
- upstream_commit: 'abc123',
- synced_at: '2026-02-15T00:00:00Z',
- notes: 'test',
- files: ['SKILL.md', 'references/one.md'],
- upstream_content_hash: hash(`a\0b`),
- },
- },
- }
-
- const upstreamContents = {
- 'plugins/compound-engineering/skills/agent-native-architecture/SKILL.md':
- 'a',
- 'plugins/compound-engineering/skills/agent-native-architecture/references/one.md':
- 'c',
- }
-
- const summary = computeCheckSummary({
- manifest,
- upstreamDefinitionKeys: ['skills/agent-native-architecture'],
- upstreamContents,
- treePaths: [],
- converterVersion: 2,
- })
-
- expect(summary.hashChanges).toEqual(['skills/agent-native-architecture'])
- expect(summary.errors).toEqual([])
- })
-
- it('reports no change for multi-file skill with matching content', () => {
- const manifest: SyncManifest = {
- converter_version: 2,
- sources: {
- cep: {
- repo: 'EveryInc/compound-engineering-plugin',
- branch: 'main',
- url: 'https://github.com/EveryInc/compound-engineering-plugin',
- },
- },
- definitions: {
- 'skills/agent-native-architecture': {
- source: 'cep',
- upstream_path:
- 'plugins/compound-engineering/skills/agent-native-architecture',
- upstream_commit: 'abc123',
- synced_at: '2026-02-15T00:00:00Z',
- notes: 'test',
- files: ['SKILL.md', 'references/one.md'],
- upstream_content_hash: hash(`a\0b`),
- },
- },
- }
-
- const upstreamContents = {
- 'plugins/compound-engineering/skills/agent-native-architecture/SKILL.md':
- 'a',
- 'plugins/compound-engineering/skills/agent-native-architecture/references/one.md':
- 'b',
- }
-
- const summary = computeCheckSummary({
- manifest,
- upstreamDefinitionKeys: ['skills/agent-native-architecture'],
- upstreamContents,
- treePaths: [],
- converterVersion: 2,
- })
-
- expect(summary.hashChanges).toEqual([])
- expect(summary.errors).toEqual([])
- expect(hasChanges(summary)).toBe(false)
- })
-
- it('skips definitions with wildcard manual_overrides', () => {
- const manifest = baseManifest()
- manifest.definitions['agents/review/security-sentinel'].manual_overrides = [
- {
- field: '*',
- reason: 'Local ownership',
- overridden_at: '2026-02-15T00:00:00Z',
- },
- ]
-
- const upstreamContents = {
- 'plugins/compound-engineering/agents/review/security-sentinel.md':
- 'changed',
- }
-
- const summary = computeCheckSummary({
- manifest,
- upstreamDefinitionKeys: ['agents/review/security-sentinel'],
- upstreamContents,
- treePaths: [],
- converterVersion: 2,
- })
-
- expect(summary.skipped).toEqual(['agents/review/security-sentinel'])
- expect(summary.errors).toEqual([])
- expect(summary.hashChanges).toEqual([])
- })
-
- it('returns error exit code when error flag is set', () => {
- const summary = {
- hashChanges: [],
- newUpstream: [],
- newUpstreamFiles: {},
- deletions: [],
- converterVersionChanged: false,
- skipped: [],
- errors: [],
- }
-
- expect(getExitCode(summary, true)).toBe(2)
- })
-
- it('flags missing multi-file contents as errors', () => {
- const manifest: SyncManifest = {
- converter_version: 2,
- sources: {
- cep: {
- repo: 'EveryInc/compound-engineering-plugin',
- branch: 'main',
- url: 'https://github.com/EveryInc/compound-engineering-plugin',
- },
- },
- definitions: {
- 'skills/agent-native-architecture': {
- source: 'cep',
- upstream_path:
- 'plugins/compound-engineering/skills/agent-native-architecture',
- upstream_commit: 'abc123',
- synced_at: '2026-02-15T00:00:00Z',
- notes: 'test',
- files: ['SKILL.md', 'references/one.md'],
- upstream_content_hash: hash('a' + 'b'),
- },
- },
- }
-
- const upstreamContents = {
- 'plugins/compound-engineering/skills/agent-native-architecture/SKILL.md':
- 'a',
- }
-
- const summary = computeCheckSummary({
- manifest,
- upstreamDefinitionKeys: ['skills/agent-native-architecture'],
- upstreamContents,
- treePaths: [],
- converterVersion: 2,
- })
-
- expect(summary.hashChanges).toEqual([])
- expect(summary.errors).toEqual([
- 'Missing upstream content for sub-file (may be a transient fetch failure or the file was removed upstream): plugins/compound-engineering/skills/agent-native-architecture/references/one.md',
- ])
- expect(getExitCode(summary, false)).toBe(2)
- })
-
- it('fetches upstream data using tree and content endpoints', async () => {
- const responses = new Map()
- const repo = 'EveryInc/compound-engineering-plugin'
- const branch = 'main'
-
- responses.set(
- `https://api.github.com/repos/${repo}/git/trees/${branch}?recursive=1`,
- new Response(
- JSON.stringify({
- tree: [
- {
- path: 'plugins/compound-engineering/agents/review/security-sentinel.md',
- type: 'blob',
- },
- ],
- }),
- { status: 200 },
- ),
- )
-
- responses.set(
- `https://api.github.com/repos/${repo}/contents/plugins/compound-engineering/agents/review/security-sentinel.md?ref=${branch}`,
- new Response(
- JSON.stringify({ content: Buffer.from('agent').toString('base64') }),
- { status: 200 },
- ),
- )
-
- const fetchFn = async (url: string): Promise => {
- const response = responses.get(url)
- if (!response) return new Response('missing', { status: 404 })
- return response
- }
-
- const result = await fetchUpstreamData(
- repo,
- branch,
- ['plugins/compound-engineering/agents/review/security-sentinel.md'],
- fetchFn,
- )
-
- expect(result.hadError).toBe(false)
- expect(result.definitionKeys).toEqual(['agents/review/security-sentinel'])
- expect(result.treePaths).toEqual([
- 'plugins/compound-engineering/agents/review/security-sentinel.md',
- ])
- expect(result.contents).toEqual({
- 'plugins/compound-engineering/agents/review/security-sentinel.md':
- 'agent',
- })
- })
-
- it('retries content fetch on 429 and succeeds', async () => {
- const repo = 'EveryInc/compound-engineering-plugin'
- const branch = 'main'
- const contentPath =
- 'plugins/compound-engineering/agents/review/security-sentinel.md'
- const responses = new Map()
-
- responses.set(
- `https://api.github.com/repos/${repo}/git/trees/${branch}?recursive=1`,
- new Response(
- JSON.stringify({
- tree: [
- {
- path: contentPath,
- type: 'blob',
- },
- ],
- }),
- { status: 200 },
- ),
- )
-
- let contentCalls = 0
- const fetchFn = async (url: string): Promise => {
- if (
- url ===
- `https://api.github.com/repos/${repo}/contents/${contentPath}?ref=${branch}`
- ) {
- contentCalls += 1
- if (contentCalls < 2) {
- return new Response('rate limited', { status: 429 })
- }
- return new Response(
- JSON.stringify({ content: Buffer.from('agent').toString('base64') }),
- { status: 200 },
- )
- }
- return responses.get(url) ?? new Response('missing', { status: 404 })
- }
-
- const result = await fetchUpstreamData(repo, branch, [contentPath], fetchFn)
-
- expect(contentCalls).toBe(2)
- expect(result.hadError).toBe(false)
- expect(result.contents[contentPath]).toBe('agent')
- })
-
- it('retries tree fetch on 403 and succeeds', async () => {
- const repo = 'EveryInc/compound-engineering-plugin'
- const branch = 'main'
- const contentPath =
- 'plugins/compound-engineering/agents/review/security-sentinel.md'
-
- let treeCalls = 0
- const fetchFn = async (url: string): Promise => {
- if (
- url ===
- `https://api.github.com/repos/${repo}/git/trees/${branch}?recursive=1`
- ) {
- treeCalls += 1
- if (treeCalls < 2) {
- return new Response('forbidden', { status: 403 })
- }
- return new Response(
- JSON.stringify({
- tree: [
- {
- path: contentPath,
- type: 'blob',
- },
- ],
- }),
- { status: 200 },
- )
- }
- if (
- url ===
- `https://api.github.com/repos/${repo}/contents/${contentPath}?ref=${branch}`
- ) {
- return new Response(
- JSON.stringify({ content: Buffer.from('agent').toString('base64') }),
- { status: 200 },
- )
- }
- return new Response('missing', { status: 404 })
- }
-
- const result = await fetchUpstreamData(repo, branch, [contentPath], fetchFn)
-
- expect(treeCalls).toBe(2)
- expect(result.hadError).toBe(false)
- expect(result.definitionKeys).toEqual(['agents/review/security-sentinel'])
- })
-
- it('returns hadError when retries are exhausted', async () => {
- const repo = 'EveryInc/compound-engineering-plugin'
- const branch = 'main'
- const contentPath =
- 'plugins/compound-engineering/agents/review/security-sentinel.md'
-
- const fetchFn = async (url: string): Promise => {
- if (
- url ===
- `https://api.github.com/repos/${repo}/git/trees/${branch}?recursive=1`
- ) {
- return new Response('rate limited', { status: 429 })
- }
- return new Response('missing', { status: 404 })
- }
-
- const result = await fetchUpstreamData(repo, branch, [contentPath], fetchFn)
-
- expect(result.hadError).toBe(true)
- })
-
- it('does not set hadError for 404 content responses', async () => {
- const repo = 'EveryInc/compound-engineering-plugin'
- const branch = 'main'
- const contentPath =
- 'plugins/compound-engineering/agents/review/security-sentinel.md'
-
- const fetchFn = async (url: string): Promise => {
- if (
- url ===
- `https://api.github.com/repos/${repo}/git/trees/${branch}?recursive=1`
- ) {
- return new Response(
- JSON.stringify({
- tree: [
- {
- path: contentPath,
- type: 'blob',
- },
- ],
- }),
- { status: 200 },
- )
- }
- return new Response('not found', { status: 404 })
- }
-
- const result = await fetchUpstreamData(repo, branch, [contentPath], fetchFn)
-
- expect(result.hadError).toBe(false)
- expect(result.contents).toEqual({})
- expect(result.definitionKeys).toEqual(['agents/review/security-sentinel'])
- })
-
- it('sets hadError for 500 content responses', async () => {
- const repo = 'EveryInc/compound-engineering-plugin'
- const branch = 'main'
- const contentPath =
- 'plugins/compound-engineering/agents/review/security-sentinel.md'
-
- const fetchFn = async (url: string): Promise => {
- if (
- url ===
- `https://api.github.com/repos/${repo}/git/trees/${branch}?recursive=1`
- ) {
- return new Response(
- JSON.stringify({
- tree: [
- {
- path: contentPath,
- type: 'blob',
- },
- ],
- }),
- { status: 200 },
- )
- }
- return new Response('server error', { status: 500 })
- }
-
- const result = await fetchUpstreamData(repo, branch, [contentPath], fetchFn)
-
- expect(result.hadError).toBe(true)
- expect(result.contents).toEqual({})
- })
-
- it('returns treePaths from fetchUpstreamData', async () => {
- const repo = 'EveryInc/compound-engineering-plugin'
- const branch = 'main'
-
- const fetchFn = async (url: string): Promise => {
- if (url.includes('/git/trees/')) {
- return new Response(
- JSON.stringify({
- tree: [
- {
- path: 'plugins/compound-engineering/skills/my-skill/SKILL.md',
- type: 'blob',
- },
- {
- path: 'plugins/compound-engineering/skills/my-skill/references/guide.md',
- type: 'blob',
- },
- {
- path: 'plugins/compound-engineering/skills/my-skill/scripts',
- type: 'tree',
- },
- ],
- }),
- { status: 200 },
- )
- }
- return new Response('not found', { status: 404 })
- }
-
- const result = await fetchUpstreamData(repo, branch, [], fetchFn)
-
- expect(result.treePaths).toEqual([
- 'plugins/compound-engineering/skills/my-skill/SKILL.md',
- 'plugins/compound-engineering/skills/my-skill/references/guide.md',
- ])
- })
-
- it('collects files for new skill definitions from tree paths', () => {
- const treePaths = [
- 'plugins/compound-engineering/skills/new-skill/SKILL.md',
- 'plugins/compound-engineering/skills/new-skill/references/guide.md',
- 'plugins/compound-engineering/skills/new-skill/scripts/setup.sh',
- 'plugins/compound-engineering/skills/existing-skill/SKILL.md',
- 'plugins/compound-engineering/agents/review/some-agent.md',
- ]
-
- const result = collectNewUpstreamFiles(treePaths, ['skills/new-skill'])
-
- expect(result).toEqual({
- 'skills/new-skill': [
- 'SKILL.md',
- 'references/guide.md',
- 'scripts/setup.sh',
- ],
- })
- })
-
- it('collects single file for new agent definitions', () => {
- const treePaths = [
- 'plugins/compound-engineering/agents/review/new-agent.md',
- 'plugins/compound-engineering/agents/review/other-agent.md',
- ]
-
- const result = collectNewUpstreamFiles(treePaths, [
- 'agents/review/new-agent',
- ])
-
- expect(result).toEqual({
- 'agents/review/new-agent': ['new-agent.md'],
- })
- })
-
- it('returns empty files for keys not found in tree', () => {
- const treePaths = ['plugins/compound-engineering/agents/review/existing.md']
-
- const result = collectNewUpstreamFiles(treePaths, ['skills/ghost-skill'])
-
- expect(result).toEqual({})
- })
-
- it('collects files for multiple new definitions at once', () => {
- const treePaths = [
- 'plugins/compound-engineering/skills/skill-a/SKILL.md',
- 'plugins/compound-engineering/skills/skill-a/references/ref.md',
- 'plugins/compound-engineering/skills/skill-b/SKILL.md',
- 'plugins/compound-engineering/commands/workflows/new-cmd.md',
- ]
-
- const result = collectNewUpstreamFiles(treePaths, [
- 'skills/skill-a',
- 'skills/skill-b',
- 'commands/workflows/new-cmd',
- ])
-
- expect(result).toEqual({
- 'skills/skill-a': ['SKILL.md', 'references/ref.md'],
- 'skills/skill-b': ['SKILL.md'],
- 'commands/workflows/new-cmd': ['new-cmd.md'],
- })
- })
-
- it('returns raw fetch when no token is provided', () => {
- const fetchFn = createAuthenticatedFetch(undefined)
- expect(fetchFn).toBe(fetch)
- })
-
- it('returns raw fetch when token is empty string', () => {
- const fetchFn = createAuthenticatedFetch('')
- expect(fetchFn).toBe(fetch)
- })
-
- it('returns authenticated fetch wrapper when token is provided', async () => {
- const fetchFn = createAuthenticatedFetch('ghp_test123')
- expect(fetchFn).not.toBe(fetch)
-
- let capturedHeaders: Headers | undefined
- const originalFetch = globalThis.fetch
- const mockFetch = async (_input: RequestInfo | URL, init?: RequestInit) => {
- capturedHeaders = new Headers(init?.headers)
- return new Response('ok', { status: 200 })
- }
- globalThis.fetch = Object.assign(mockFetch, {
- preconnect: originalFetch.preconnect,
- }) as typeof fetch
-
- try {
- await fetchFn('https://api.github.com/test')
- expect(capturedHeaders?.get('Authorization')).toBe('Bearer ghp_test123')
- expect(capturedHeaders?.get('Accept')).toBe(
- 'application/vnd.github.v3+json',
- )
- } finally {
- globalThis.fetch = originalFetch
- }
- })
-
- it('includes newUpstreamFiles in computeCheckSummary for new skills', () => {
- const manifest = baseManifest()
- const treePaths = [
- 'plugins/compound-engineering/skills/new-multi-skill/SKILL.md',
- 'plugins/compound-engineering/skills/new-multi-skill/references/api.md',
- 'plugins/compound-engineering/skills/new-multi-skill/scripts/init.sh',
- 'plugins/compound-engineering/agents/review/security-sentinel.md',
- ]
-
- const summary = computeCheckSummary({
- manifest,
- upstreamDefinitionKeys: [
- 'agents/review/security-sentinel',
- 'skills/new-multi-skill',
- ],
- upstreamContents: {
- 'plugins/compound-engineering/agents/review/security-sentinel.md':
- 'agent',
- },
- treePaths,
- converterVersion: 2,
- })
-
- expect(summary.newUpstream).toEqual(['skills/new-multi-skill'])
- expect(summary.newUpstreamFiles).toEqual({
- 'skills/new-multi-skill': [
- 'SKILL.md',
- 'references/api.md',
- 'scripts/init.sh',
- ],
- })
- })
-})