diff --git a/assets/oh-my-opencode.schema.json b/assets/oh-my-opencode.schema.json index f11fbffcf..063e0ab25 100644 --- a/assets/oh-my-opencode.schema.json +++ b/assets/oh-my-opencode.schema.json @@ -54,6 +54,7 @@ "git-master", "review-work", "ai-slop-remover", + "init-deep", "team-mode" ] } @@ -69,7 +70,6 @@ "items": { "type": "string", "enum": [ - "init-deep", "ralph-loop", "ulw-loop", "cancel-ralph", diff --git a/packages/omo-codex/plugin/skills/init-deep/SKILL.md b/packages/omo-codex/plugin/skills/init-deep/SKILL.md new file mode 100644 index 000000000..08129ca25 --- /dev/null +++ b/packages/omo-codex/plugin/skills/init-deep/SKILL.md @@ -0,0 +1,325 @@ +--- +name: init-deep +description: "(builtin) Initialize hierarchical AGENTS.md knowledge base" +--- +## Codex Harness Tool Compatibility + +This skill may include examples copied from the OpenCode harness. In Codex, do not call OpenCode-only tools such as `call_omo_agent(...)`, `task(...)`, `background_output(...)`, or `team_*(...)` literally. Translate those examples to Codex native tools: + +| OpenCode example | Codex tool to use | +| --- | --- | +| `call_omo_agent(subagent_type="explore", ...)` | `spawn_agent(agent_type="explorer", task_name="...", message="...")` | +| `call_omo_agent(subagent_type="librarian", ...)` | `spawn_agent(agent_type="librarian", task_name="...", message="...")` | +| `task(subagent_type="plan", ...)` | `spawn_agent(agent_type="plan", task_name="...", message="...")` | +| `task(subagent_type="oracle", ...)` for final verification | `spawn_agent(agent_type="codex-ultrawork-reviewer", task_name="...", message="...")` | +| `task(category="...", ...)` for implementation or QA | `spawn_agent(agent_type="worker", task_name="...", message="...")` | +| `background_output(task_id="...")` | `wait_agent(...)` to wait for subagent completion and mailbox updates | +| `team_*(...)` | Use Codex native subagents plus `send_message`, `followup_task`, `wait_agent`, and `close_agent` | + +When translating `load_skills=[...]`, include the requested skill names in the spawned agent's `message`. If a code block below conflicts with this section, this section wins. + +# /init-deep + +Generate hierarchical AGENTS.md files. Root + complexity-scored subdirectories. + +## Usage + +``` +/init-deep # Update mode: modify existing + create new where warranted +/init-deep --create-new # Read existing → remove all → regenerate from scratch +/init-deep --max-depth=2 # Limit directory depth (default: 3) +``` + +--- + +## Workflow (High-Level) + +1. **Discovery + Analysis** (concurrent) + - Fire background explore agents immediately + - Main session: bash structure + LSP codemap + read existing AGENTS.md +2. **Score & Decide** - Determine AGENTS.md locations from merged findings +3. **Generate** - Root first, then subdirs in parallel +4. **Review** - Deduplicate, trim, validate + + +**TodoWrite ALL phases. Mark in_progress → completed in real-time.** +``` +TodoWrite([ + { id: "discovery", content: "Fire explore agents + LSP codemap + read existing", status: "pending", priority: "high" }, + { id: "scoring", content: "Score directories, determine locations", status: "pending", priority: "high" }, + { id: "generate", content: "Generate AGENTS.md files (root + subdirs)", status: "pending", priority: "high" }, + { id: "review", content: "Deduplicate, validate, trim", status: "pending", priority: "medium" } +]) +``` + + +--- + +## Phase 1: Discovery + Analysis (Concurrent) + +**Mark "discovery" as in_progress.** + +### Fire Background Explore Agents IMMEDIATELY + +Don't wait-these run async while main session works. + +``` +// Fire all at once, collect results later +task(subagent_type="explore", load_skills=[], description="Explore project structure", run_in_background=true, prompt="Project structure: PREDICT standard patterns for detected language → REPORT deviations only") +task(subagent_type="explore", load_skills=[], description="Find entry points", run_in_background=true, prompt="Entry points: FIND main files → REPORT non-standard organization") +task(subagent_type="explore", load_skills=[], description="Find conventions", run_in_background=true, prompt="Conventions: FIND config files (.eslintrc, pyproject.toml, .editorconfig) → REPORT project-specific rules") +task(subagent_type="explore", load_skills=[], description="Find anti-patterns", run_in_background=true, prompt="Anti-patterns: FIND 'DO NOT', 'NEVER', 'ALWAYS', 'DEPRECATED' comments → LIST forbidden patterns") +task(subagent_type="explore", load_skills=[], description="Explore build/CI", run_in_background=true, prompt="Build/CI: FIND .github/workflows, Makefile → REPORT non-standard patterns") +task(subagent_type="explore", load_skills=[], description="Find test patterns", run_in_background=true, prompt="Test patterns: FIND test configs, test structure → REPORT unique conventions") +``` + + +**DYNAMIC AGENT SPAWNING**: After bash analysis, spawn ADDITIONAL explore agents based on project scale: + +| Factor | Threshold | Additional Agents | +|--------|-----------|-------------------| +| **Total files** | >100 | +1 per 100 files | +| **Total lines** | >10k | +1 per 10k lines | +| **Directory depth** | ≥4 | +2 for deep exploration | +| **Large files (>500 lines)** | >10 files | +1 for complexity hotspots | +| **Monorepo** | detected | +1 per package/workspace | +| **Multiple languages** | >1 | +1 per language | + +```bash +# Measure project scale first +total_files=$(find . -type f -not -path '*/node_modules/*' -not -path '*/.git/*' | wc -l) +total_lines=$(find . -type f \\( -name "*.ts" -o -name "*.py" -o -name "*.go" \\) -not -path '*/node_modules/*' -exec wc -l {} + 2>/dev/null | tail -1 | awk '{print $1}') +large_files=$(find . -type f \\( -name "*.ts" -o -name "*.py" \\) -not -path '*/node_modules/*' -exec wc -l {} + 2>/dev/null | awk '$1 > 500 {count++} END {print count+0}') +max_depth=$(find . -type d -not -path '*/node_modules/*' -not -path '*/.git/*' | awk -F/ '{print NF}' | sort -rn | head -1) +``` + +Example spawning: +``` +// 500 files, 50k lines, depth 6, 15 large files → spawn 5+5+2+1 = 13 additional agents +task(subagent_type="explore", load_skills=[], description="Analyze large files", run_in_background=true, prompt="Large file analysis: FIND files >500 lines, REPORT complexity hotspots") +task(subagent_type="explore", load_skills=[], description="Explore deep modules", run_in_background=true, prompt="Deep modules at depth 4+: FIND hidden patterns, internal conventions") +task(subagent_type="explore", load_skills=[], description="Find shared utilities", run_in_background=true, prompt="Cross-cutting concerns: FIND shared utilities across directories") +// ... more based on calculation +``` + + +### Main Session: Concurrent Analysis + +**While background agents run**, main session does: + +#### 1. Bash Structural Analysis +```bash +# Directory depth + file counts +find . -type d -not -path '*/\\.*' -not -path '*/node_modules/*' -not -path '*/venv/*' -not -path '*/dist/*' -not -path '*/build/*' | awk -F/ '{print NF-1}' | sort -n | uniq -c + +# Files per directory (top 30) +find . -type f -not -path '*/\\.*' -not -path '*/node_modules/*' | sed 's|/[^/]*$||' | sort | uniq -c | sort -rn | head -30 + +# Code concentration by extension +find . -type f \\( -name "*.py" -o -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.go" -o -name "*.rs" \\) -not -path '*/node_modules/*' | sed 's|/[^/]*$||' | sort | uniq -c | sort -rn | head -20 + +# Existing AGENTS.md / CLAUDE.md +find . -type f \\( -name "AGENTS.md" -o -name "CLAUDE.md" \\) -not -path '*/node_modules/*' 2>/dev/null +``` + +#### 2. Read Existing AGENTS.md +``` +For each existing file found: + Read(filePath=file) + Extract: key insights, conventions, anti-patterns + Store in EXISTING_AGENTS map +``` + +If `--create-new`: Read all existing first (preserve context) → then delete all → regenerate. + +#### 3. LSP Codemap (if available) +``` +LspServers() # Check availability + +# Entry points (parallel) +LspDocumentSymbols(filePath="src/index.ts") +LspDocumentSymbols(filePath="main.py") + +# Key symbols (parallel) +LspWorkspaceSymbols(filePath=".", query="class") +LspWorkspaceSymbols(filePath=".", query="interface") +LspWorkspaceSymbols(filePath=".", query="function") + +# Centrality for top exports +LspFindReferences(filePath="...", line=X, character=Y) +``` + +**LSP Fallback**: If unavailable, rely on explore agents + AST-grep. + +### Collect Background Results + +``` +// After main session analysis done, collect all task results +for each background task ID (`bg_...`): background_output(task_id="bg_...") +``` + +**Merge: bash + LSP + existing + explore findings. Mark "discovery" as completed.** + +--- + +## Phase 2: Scoring & Location Decision + +**Mark "scoring" as in_progress.** + +### Scoring Matrix + +| Factor | Weight | High Threshold | Source | +|--------|--------|----------------|--------| +| File count | 3x | >20 | bash | +| Subdir count | 2x | >5 | bash | +| Code ratio | 2x | >70% | bash | +| Unique patterns | 1x | Has own config | explore | +| Module boundary | 2x | Has index.ts/__init__.py | bash | +| Symbol density | 2x | >30 symbols | LSP | +| Export count | 2x | >10 exports | LSP | +| Reference centrality | 3x | >20 refs | LSP | + +### Decision Rules + +| Score | Action | +|-------|--------| +| **Root (.)** | ALWAYS create | +| **>15** | Create AGENTS.md | +| **8-15** | Create if distinct domain | +| **<8** | Skip (parent covers) | + +### Output +``` +AGENTS_LOCATIONS = [ + { path: ".", type: "root" }, + { path: "src/hooks", score: 18, reason: "high complexity" }, + { path: "src/api", score: 12, reason: "distinct domain" } +] +``` + +**Mark "scoring" as completed.** + +--- + +## Phase 3: Generate AGENTS.md + +**Mark "generate" as in_progress.** + + +**File Writing Rule**: If AGENTS.md already exists at the target path → use `Edit` tool. If it does NOT exist → use `Write` tool. +NEVER use Write to overwrite an existing file. ALWAYS check existence first via `Read` or discovery results. + + +### Root AGENTS.md (Full Treatment) + +```markdown +# PROJECT KNOWLEDGE BASE + +**Generated:** {TIMESTAMP} +**Commit:** {SHORT_SHA} +**Branch:** {BRANCH} + +## OVERVIEW +{1-2 sentences: what + core stack} + +## STRUCTURE +``` +{root}/ +├── {dir}/ # {non-obvious purpose only} +└── {entry} +``` + +## WHERE TO LOOK +| Task | Location | Notes | +|------|----------|-------| + +## CODE MAP +{From LSP - skip if unavailable or project <10 files} + +| Symbol | Type | Location | Refs | Role | +|--------|------|----------|------|------| + +## CONVENTIONS +{ONLY deviations from standard} + +## ANTI-PATTERNS (THIS PROJECT) +{Explicitly forbidden here} + +## UNIQUE STYLES +{Project-specific} + +## COMMANDS +```bash +{dev/test/build} +``` + +## NOTES +{Gotchas} +``` + +**Quality gates**: 50-150 lines, no generic advice, no obvious info. + +### Subdirectory AGENTS.md (Parallel) + +Launch writing tasks for each location: + +``` +for loc in AGENTS_LOCATIONS (except root): + task(category="writing", load_skills=[], run_in_background=false, description="Generate AGENTS.md", prompt=` + Generate AGENTS.md for: ${loc.path} + - Reason: ${loc.reason} + - 30-80 lines max + - NEVER repeat parent content + - Sections: OVERVIEW (1 line), STRUCTURE (if >5 subdirs), WHERE TO LOOK, CONVENTIONS (if different), ANTI-PATTERNS + `) +``` + +**Wait for all. Mark "generate" as completed.** + +--- + +## Phase 4: Review & Deduplicate + +**Mark "review" as in_progress.** + +For each generated file: +- Remove generic advice +- Remove parent duplicates +- Trim to size limits +- Verify telegraphic style + +**Mark "review" as completed.** + +--- + +## Final Report + +``` +=== init-deep Complete === + +Mode: {update | create-new} + +Files: + [OK] ./AGENTS.md (root, {N} lines) + [OK] ./src/hooks/AGENTS.md ({N} lines) + +Dirs Analyzed: {N} +AGENTS.md Created: {N} +AGENTS.md Updated: {N} + +Hierarchy: + ./AGENTS.md + └── src/hooks/AGENTS.md +``` + +--- + +## Anti-Patterns + +- **Static agent count**: MUST vary agents based on project size/depth +- **Sequential execution**: MUST parallel (explore + LSP concurrent) +- **Ignoring existing**: ALWAYS read existing first, even with --create-new +- **Over-documenting**: Not every dir needs AGENTS.md +- **Redundancy**: Child never repeats parent +- **Generic content**: Remove anything that applies to ALL projects +- **Verbose style**: Telegraphic or die diff --git a/packages/omo-codex/plugin/test/sync-skills.test.mjs b/packages/omo-codex/plugin/test/sync-skills.test.mjs index 637b08a6e..002149e69 100644 --- a/packages/omo-codex/plugin/test/sync-skills.test.mjs +++ b/packages/omo-codex/plugin/test/sync-skills.test.mjs @@ -11,6 +11,7 @@ const expectedSkills = [ "comment-checker", "debugging", "frontend-ui-ux", + "init-deep", "lsp", "metis", "momus", diff --git a/src/features/builtin-commands/templates/init-deep.ts b/packages/shared-skills/skills/init-deep/SKILL.md similarity index 92% rename from src/features/builtin-commands/templates/init-deep.ts rename to packages/shared-skills/skills/init-deep/SKILL.md index cebca0b20..a3dedbcfd 100644 --- a/src/features/builtin-commands/templates/init-deep.ts +++ b/packages/shared-skills/skills/init-deep/SKILL.md @@ -1,14 +1,18 @@ -export const INIT_DEEP_TEMPLATE = `# /init-deep +--- +name: init-deep +description: "(builtin) Initialize hierarchical AGENTS.md knowledge base" +--- +# /init-deep Generate hierarchical AGENTS.md files. Root + complexity-scored subdirectories. ## Usage -\`\`\` +``` /init-deep # Update mode: modify existing + create new where warranted /init-deep --create-new # Read existing → remove all → regenerate from scratch /init-deep --max-depth=2 # Limit directory depth (default: 3) -\`\`\` +``` --- @@ -23,14 +27,14 @@ Generate hierarchical AGENTS.md files. Root + complexity-scored subdirectories. **TodoWrite ALL phases. Mark in_progress → completed in real-time.** -\`\`\` +``` TodoWrite([ { id: "discovery", content: "Fire explore agents + LSP codemap + read existing", status: "pending", priority: "high" }, { id: "scoring", content: "Score directories, determine locations", status: "pending", priority: "high" }, { id: "generate", content: "Generate AGENTS.md files (root + subdirs)", status: "pending", priority: "high" }, { id: "review", content: "Deduplicate, validate, trim", status: "pending", priority: "medium" } ]) -\`\`\` +``` --- @@ -43,7 +47,7 @@ TodoWrite([ Don't wait-these run async while main session works. -\`\`\` +``` // Fire all at once, collect results later task(subagent_type="explore", load_skills=[], description="Explore project structure", run_in_background=true, prompt="Project structure: PREDICT standard patterns for detected language → REPORT deviations only") task(subagent_type="explore", load_skills=[], description="Find entry points", run_in_background=true, prompt="Entry points: FIND main files → REPORT non-standard organization") @@ -51,7 +55,7 @@ task(subagent_type="explore", load_skills=[], description="Find conventions", ru task(subagent_type="explore", load_skills=[], description="Find anti-patterns", run_in_background=true, prompt="Anti-patterns: FIND 'DO NOT', 'NEVER', 'ALWAYS', 'DEPRECATED' comments → LIST forbidden patterns") task(subagent_type="explore", load_skills=[], description="Explore build/CI", run_in_background=true, prompt="Build/CI: FIND .github/workflows, Makefile → REPORT non-standard patterns") task(subagent_type="explore", load_skills=[], description="Find test patterns", run_in_background=true, prompt="Test patterns: FIND test configs, test structure → REPORT unique conventions") -\`\`\` +``` **DYNAMIC AGENT SPAWNING**: After bash analysis, spawn ADDITIONAL explore agents based on project scale: @@ -65,22 +69,22 @@ task(subagent_type="explore", load_skills=[], description="Find test patterns", | **Monorepo** | detected | +1 per package/workspace | | **Multiple languages** | >1 | +1 per language | -\`\`\`bash +```bash # Measure project scale first total_files=$(find . -type f -not -path '*/node_modules/*' -not -path '*/.git/*' | wc -l) total_lines=$(find . -type f \\( -name "*.ts" -o -name "*.py" -o -name "*.go" \\) -not -path '*/node_modules/*' -exec wc -l {} + 2>/dev/null | tail -1 | awk '{print $1}') large_files=$(find . -type f \\( -name "*.ts" -o -name "*.py" \\) -not -path '*/node_modules/*' -exec wc -l {} + 2>/dev/null | awk '$1 > 500 {count++} END {print count+0}') max_depth=$(find . -type d -not -path '*/node_modules/*' -not -path '*/.git/*' | awk -F/ '{print NF}' | sort -rn | head -1) -\`\`\` +``` Example spawning: -\`\`\` +``` // 500 files, 50k lines, depth 6, 15 large files → spawn 5+5+2+1 = 13 additional agents task(subagent_type="explore", load_skills=[], description="Analyze large files", run_in_background=true, prompt="Large file analysis: FIND files >500 lines, REPORT complexity hotspots") task(subagent_type="explore", load_skills=[], description="Explore deep modules", run_in_background=true, prompt="Deep modules at depth 4+: FIND hidden patterns, internal conventions") task(subagent_type="explore", load_skills=[], description="Find shared utilities", run_in_background=true, prompt="Cross-cutting concerns: FIND shared utilities across directories") // ... more based on calculation -\`\`\` +``` ### Main Session: Concurrent Analysis @@ -88,7 +92,7 @@ task(subagent_type="explore", load_skills=[], description="Find shared utilities **While background agents run**, main session does: #### 1. Bash Structural Analysis -\`\`\`bash +```bash # Directory depth + file counts find . -type d -not -path '*/\\.*' -not -path '*/node_modules/*' -not -path '*/venv/*' -not -path '*/dist/*' -not -path '*/build/*' | awk -F/ '{print NF-1}' | sort -n | uniq -c @@ -100,20 +104,20 @@ find . -type f \\( -name "*.py" -o -name "*.ts" -o -name "*.tsx" -o -name "*.js" # Existing AGENTS.md / CLAUDE.md find . -type f \\( -name "AGENTS.md" -o -name "CLAUDE.md" \\) -not -path '*/node_modules/*' 2>/dev/null -\`\`\` +``` #### 2. Read Existing AGENTS.md -\`\`\` +``` For each existing file found: Read(filePath=file) Extract: key insights, conventions, anti-patterns Store in EXISTING_AGENTS map -\`\`\` +``` -If \`--create-new\`: Read all existing first (preserve context) → then delete all → regenerate. +If `--create-new`: Read all existing first (preserve context) → then delete all → regenerate. #### 3. LSP Codemap (if available) -\`\`\` +``` LspServers() # Check availability # Entry points (parallel) @@ -127,16 +131,16 @@ LspWorkspaceSymbols(filePath=".", query="function") # Centrality for top exports LspFindReferences(filePath="...", line=X, character=Y) -\`\`\` +``` **LSP Fallback**: If unavailable, rely on explore agents + AST-grep. ### Collect Background Results -\`\`\` +``` // After main session analysis done, collect all task results -for each background task ID (\`bg_...\`): background_output(task_id="bg_...") -\`\`\` +for each background task ID (`bg_...`): background_output(task_id="bg_...") +``` **Merge: bash + LSP + existing + explore findings. Mark "discovery" as completed.** @@ -169,13 +173,13 @@ for each background task ID (\`bg_...\`): background_output(task_id="bg_...") | **<8** | Skip (parent covers) | ### Output -\`\`\` +``` AGENTS_LOCATIONS = [ { path: ".", type: "root" }, { path: "src/hooks", score: 18, reason: "high complexity" }, { path: "src/api", score: 12, reason: "distinct domain" } ] -\`\`\` +``` **Mark "scoring" as completed.** @@ -186,13 +190,13 @@ AGENTS_LOCATIONS = [ **Mark "generate" as in_progress.** -**File Writing Rule**: If AGENTS.md already exists at the target path → use \`Edit\` tool. If it does NOT exist → use \`Write\` tool. -NEVER use Write to overwrite an existing file. ALWAYS check existence first via \`Read\` or discovery results. +**File Writing Rule**: If AGENTS.md already exists at the target path → use `Edit` tool. If it does NOT exist → use `Write` tool. +NEVER use Write to overwrite an existing file. ALWAYS check existence first via `Read` or discovery results. ### Root AGENTS.md (Full Treatment) -\`\`\`markdown +```markdown # PROJECT KNOWLEDGE BASE **Generated:** {TIMESTAMP} @@ -203,11 +207,11 @@ NEVER use Write to overwrite an existing file. ALWAYS check existence first via {1-2 sentences: what + core stack} ## STRUCTURE -\\\`\\\`\\\` +``` {root}/ ├── {dir}/ # {non-obvious purpose only} └── {entry} -\\\`\\\`\\\` +``` ## WHERE TO LOOK | Task | Location | Notes | @@ -229,13 +233,13 @@ NEVER use Write to overwrite an existing file. ALWAYS check existence first via {Project-specific} ## COMMANDS -\\\`\\\`\\\`bash +```bash {dev/test/build} -\\\`\\\`\\\` +``` ## NOTES {Gotchas} -\`\`\` +``` **Quality gates**: 50-150 lines, no generic advice, no obvious info. @@ -243,16 +247,16 @@ NEVER use Write to overwrite an existing file. ALWAYS check existence first via Launch writing tasks for each location: -\`\`\` +``` for loc in AGENTS_LOCATIONS (except root): - task(category="writing", load_skills=[], run_in_background=false, description="Generate AGENTS.md", prompt=\\\` - Generate AGENTS.md for: \${loc.path} - - Reason: \${loc.reason} + task(category="writing", load_skills=[], run_in_background=false, description="Generate AGENTS.md", prompt=` + Generate AGENTS.md for: ${loc.path} + - Reason: ${loc.reason} - 30-80 lines max - NEVER repeat parent content - Sections: OVERVIEW (1 line), STRUCTURE (if >5 subdirs), WHERE TO LOOK, CONVENTIONS (if different), ANTI-PATTERNS - \\\`) -\`\`\` + `) +``` **Wait for all. Mark "generate" as completed.** @@ -274,7 +278,7 @@ For each generated file: ## Final Report -\`\`\` +``` === init-deep Complete === Mode: {update | create-new} @@ -290,7 +294,7 @@ AGENTS.md Updated: {N} Hierarchy: ./AGENTS.md └── src/hooks/AGENTS.md -\`\`\` +``` --- @@ -302,4 +306,4 @@ Hierarchy: - **Over-documenting**: Not every dir needs AGENTS.md - **Redundancy**: Child never repeats parent - **Generic content**: Remove anything that applies to ALL projects -- **Verbose style**: Telegraphic or die` +- **Verbose style**: Telegraphic or die diff --git a/src/config/schema/agent-names.test.ts b/src/config/schema/agent-names.test.ts index d6b80a29b..d6ed1fea8 100644 --- a/src/config/schema/agent-names.test.ts +++ b/src/config/schema/agent-names.test.ts @@ -1,11 +1,13 @@ +/// + import { describe, expect, test } from "bun:test" import { OhMyOpenCodeConfigSchema } from "./oh-my-opencode-config" describe("OhMyOpenCodeConfigSchema disabled_skills", () => { - test("accepts review-work and ai-slop-remover", () => { + test("accepts review-work, ai-slop-remover, and init-deep", () => { // given const config = { - disabled_skills: ["review-work", "ai-slop-remover"], + disabled_skills: ["review-work", "ai-slop-remover", "init-deep"], } // when @@ -17,6 +19,7 @@ describe("OhMyOpenCodeConfigSchema disabled_skills", () => { expect(result.data.disabled_skills).toEqual([ "review-work", "ai-slop-remover", + "init-deep", ]) } }) diff --git a/src/config/schema/agent-names.ts b/src/config/schema/agent-names.ts index 7fefdadce..0c49ccf64 100644 --- a/src/config/schema/agent-names.ts +++ b/src/config/schema/agent-names.ts @@ -22,6 +22,7 @@ export const BuiltinSkillNameSchema = z.enum([ "git-master", "review-work", "ai-slop-remover", + "init-deep", "team-mode", ]) diff --git a/src/config/schema/commands.ts b/src/config/schema/commands.ts index ea2a11287..1844284c0 100644 --- a/src/config/schema/commands.ts +++ b/src/config/schema/commands.ts @@ -1,7 +1,6 @@ import { z } from "zod" export const BuiltinCommandNameSchema = z.enum([ - "init-deep", "ralph-loop", "ulw-loop", "cancel-ralph", diff --git a/src/features/builtin-commands/commands.ts b/src/features/builtin-commands/commands.ts index aa15f8f58..d6d773f15 100644 --- a/src/features/builtin-commands/commands.ts +++ b/src/features/builtin-commands/commands.ts @@ -1,7 +1,6 @@ import type { CommandDefinition } from "../claude-code-command-loader" import { isAgentRegistered } from "../claude-code-session-state" import type { BuiltinCommandName, BuiltinCommands } from "./types" -import { INIT_DEEP_TEMPLATE } from "./templates/init-deep" import { RALPH_LOOP_TEMPLATE, ULW_LOOP_TEMPLATE, CANCEL_RALPH_TEMPLATE } from "./templates/ralph-loop" import { STOP_CONTINUATION_TEMPLATE } from "./templates/stop-continuation" import { REFACTOR_TEMPLATE, REFACTOR_TEAM_MODE_ADDENDUM } from "./templates/refactor" @@ -39,17 +38,6 @@ function createBuiltinCommandDefinitions( ) return { - "init-deep": { - description: "(builtin) Initialize hierarchical AGENTS.md knowledge base", - template: ` -${INIT_DEEP_TEMPLATE} - - - -$ARGUMENTS -`, - argumentHint: "[--create-new] [--max-depth=N]", - }, "ralph-loop": { description: "(builtin) Start self-referential development loop until completion", template: ` diff --git a/src/features/builtin-commands/init-deep-migration.test.ts b/src/features/builtin-commands/init-deep-migration.test.ts new file mode 100644 index 000000000..dd8c36c03 --- /dev/null +++ b/src/features/builtin-commands/init-deep-migration.test.ts @@ -0,0 +1,16 @@ +/// + +import { describe, expect, test } from "bun:test" +import { loadBuiltinCommands } from "./commands" + +describe("init-deep skill migration", () => { + test("#given builtin commands #when loaded #then init-deep no longer ships as a command", () => { + // given + + // when + const commands = loadBuiltinCommands() + + // then + expect(commands["init-deep"]).toBeUndefined() + }) +}) diff --git a/src/features/builtin-commands/types.ts b/src/features/builtin-commands/types.ts index 4d9100a99..3181d4939 100644 --- a/src/features/builtin-commands/types.ts +++ b/src/features/builtin-commands/types.ts @@ -1,6 +1,6 @@ import type { CommandDefinition } from "../claude-code-command-loader" -export type BuiltinCommandName = "init-deep" | "ralph-loop" | "cancel-ralph" | "ulw-loop" | "refactor" | "start-work" | "stop-continuation" | "handoff" | "remove-ai-slops" | "hyperplan" +export type BuiltinCommandName = "ralph-loop" | "cancel-ralph" | "ulw-loop" | "refactor" | "start-work" | "stop-continuation" | "handoff" | "remove-ai-slops" | "hyperplan" export interface BuiltinCommandConfig { disabled_commands?: BuiltinCommandName[] diff --git a/src/features/builtin-skills/shared-skill-extraction.test.ts b/src/features/builtin-skills/shared-skill-extraction.test.ts index 4731b9e92..955d963d6 100644 --- a/src/features/builtin-skills/shared-skill-extraction.test.ts +++ b/src/features/builtin-skills/shared-skill-extraction.test.ts @@ -1,7 +1,13 @@ +/// + import { describe, expect, test } from "bun:test" import type { BuiltinSkill } from "./types" -const TARGET_SKILLS = ["ai-slop-remover", "review-work", "frontend-ui-ux"] as const +declare const Bun: { + file(path: string): { text(): Promise } +} + +const TARGET_SKILLS = ["ai-slop-remover", "review-work", "frontend-ui-ux", "init-deep"] as const type TargetSkill = (typeof TARGET_SKILLS)[number] @@ -28,10 +34,13 @@ async function readSkillSource(name: TargetSkill): Promise { case "review-work": skill = (await import("./skills/review-work")).reviewWorkSkill break - case "frontend-ui-ux": - skill = (await import("./skills/frontend-ui-ux")).frontendUiUxSkill - break - } + case "frontend-ui-ux": + skill = (await import("./skills/frontend-ui-ux")).frontendUiUxSkill + break + case "init-deep": + skill = (await import("./skills/init-deep")).initDeepSkill + break + } return { name, description: skill.description, template: skill.template } } diff --git a/src/features/builtin-skills/skill-file-loader.test.ts b/src/features/builtin-skills/skill-file-loader.test.ts index eb77810db..94585979c 100644 --- a/src/features/builtin-skills/skill-file-loader.test.ts +++ b/src/features/builtin-skills/skill-file-loader.test.ts @@ -1,9 +1,15 @@ +/// + import { describe, expect, test } from "bun:test" import { parseFrontmatter } from "../../shared/frontmatter" import { createBuiltinSkills } from "./skills" import { createSharedSkillTemplateLoader, loadSharedSkillTemplate } from "./skill-file-loader" -const SHARED_BUILTIN_SKILLS = ["ai-slop-remover", "review-work", "frontend-ui-ux"] as const +declare const Bun: { + file(path: string): { text(): Promise } +} + +const SHARED_BUILTIN_SKILLS = ["ai-slop-remover", "review-work", "frontend-ui-ux", "init-deep"] as const describe("shared builtin skill file loader", () => { test("#given extracted shared skill files #when builtin skills are created #then templates load from SKILL.md bodies", async () => { diff --git a/src/features/builtin-skills/skills.test.ts b/src/features/builtin-skills/skills.test.ts index 5b17c6319..5d507f1c0 100644 --- a/src/features/builtin-skills/skills.test.ts +++ b/src/features/builtin-skills/skills.test.ts @@ -1,3 +1,5 @@ +/// + import { describe, test, expect } from "bun:test" import { createBuiltinSkills } from "./skills" @@ -83,7 +85,7 @@ describe("createBuiltinSkills", () => { expect(agentBrowserSkill!.template).toContain("agent-browser snapshot") }) - test("always includes frontend-ui-ux, git-master, review-work, and ai-slop-remover skills", () => { + test("always includes frontend-ui-ux, git-master, review-work, ai-slop-remover, and init-deep skills", () => { // given - both provider options // when @@ -97,10 +99,11 @@ describe("createBuiltinSkills", () => { expect(skills.find((s) => s.name === "git-master")).toBeDefined() expect(skills.find((s) => s.name === "review-work")).toBeDefined() expect(skills.find((s) => s.name === "ai-slop-remover")).toBeDefined() + expect(skills.find((s) => s.name === "init-deep")).toBeDefined() } }) - test("returns exactly 5 skills regardless of provider", () => { + test("returns exactly 6 skills regardless of provider", () => { // given // when @@ -109,9 +112,9 @@ describe("createBuiltinSkills", () => { const devBrowserSkills = createBuiltinSkills({ browserProvider: "dev-browser" }) // then - expect(defaultSkills).toHaveLength(5) - expect(agentBrowserSkills).toHaveLength(5) - expect(devBrowserSkills).toHaveLength(5) + expect(defaultSkills).toHaveLength(6) + expect(agentBrowserSkills).toHaveLength(6) + expect(devBrowserSkills).toHaveLength(6) }) test("should exclude playwright when it is in disabledSkills", () => { @@ -128,7 +131,8 @@ describe("createBuiltinSkills", () => { expect(skills.map((s) => s.name)).not.toContain("dev-browser") expect(skills.map((s) => s.name)).toContain("review-work") expect(skills.map((s) => s.name)).toContain("ai-slop-remover") - expect(skills.length).toBe(4) + expect(skills.map((s) => s.name)).toContain("init-deep") + expect(skills.length).toBe(5) }) test("should exclude multiple skills when they are in disabledSkills", () => { @@ -145,12 +149,13 @@ describe("createBuiltinSkills", () => { expect(skills.map((s) => s.name)).not.toContain("dev-browser") expect(skills.map((s) => s.name)).toContain("review-work") expect(skills.map((s) => s.name)).toContain("ai-slop-remover") - expect(skills.length).toBe(3) + expect(skills.map((s) => s.name)).toContain("init-deep") + expect(skills.length).toBe(4) }) test("should return an empty array when all skills are disabled", () => { // #given - const options = { disabledSkills: new Set(["playwright", "frontend-ui-ux", "git-master", "review-work", "ai-slop-remover"]) } + const options = { disabledSkills: new Set(["playwright", "frontend-ui-ux", "git-master", "review-work", "ai-slop-remover", "init-deep"]) } // #when const skills = createBuiltinSkills(options) @@ -167,7 +172,22 @@ describe("createBuiltinSkills", () => { const skills = createBuiltinSkills(options) // #then - expect(skills.length).toBe(5) + expect(skills.length).toBe(6) + }) + + test("init-deep skill has correct structure", () => { + // #given - default options + + // #when + const skills = createBuiltinSkills() + const initDeep = skills.find((s) => s.name === "init-deep") + + // #then + expect(initDeep).toBeDefined() + expect(initDeep!.description).toContain("hierarchical AGENTS.md") + expect(initDeep!.argumentHint).toBe("[--create-new] [--max-depth=N]") + expect(initDeep!.template).toContain("Generate hierarchical AGENTS.md files") + expect(initDeep!.template).toContain("Discovery + Analysis") }) test("review-work skill has correct structure", () => { diff --git a/src/features/builtin-skills/skills.ts b/src/features/builtin-skills/skills.ts index 8c544e186..76017dae3 100644 --- a/src/features/builtin-skills/skills.ts +++ b/src/features/builtin-skills/skills.ts @@ -8,6 +8,7 @@ import { frontendUiUxSkill, gitMasterSkill, devBrowserSkill, + initDeepSkill, reviewWorkSkill, aiSlopRemoverSkill, teamModeSkill, @@ -33,7 +34,7 @@ export function createBuiltinSkills(options: CreateBuiltinSkillsOptions = {}): B browserSkill = playwrightSkill } - const skills = [browserSkill, frontendUiUxSkill, gitMasterSkill, reviewWorkSkill, aiSlopRemoverSkill] + const skills = [browserSkill, frontendUiUxSkill, gitMasterSkill, reviewWorkSkill, aiSlopRemoverSkill, initDeepSkill] if (teamModeEnabled && !disabledSkills?.has("team-mode")) { skills.push(teamModeSkill) diff --git a/src/features/builtin-skills/skills/index.ts b/src/features/builtin-skills/skills/index.ts index 2990cf178..ec6690518 100644 --- a/src/features/builtin-skills/skills/index.ts +++ b/src/features/builtin-skills/skills/index.ts @@ -5,4 +5,5 @@ export { gitMasterSkill } from "./git-master" export { devBrowserSkill } from "./dev-browser" export { reviewWorkSkill } from "./review-work" export { aiSlopRemoverSkill } from "./ai-slop-remover" +export { initDeepSkill } from "./init-deep" export * from "./team-mode" diff --git a/src/features/builtin-skills/skills/init-deep.ts b/src/features/builtin-skills/skills/init-deep.ts new file mode 100644 index 000000000..5c3f560f0 --- /dev/null +++ b/src/features/builtin-skills/skills/init-deep.ts @@ -0,0 +1,9 @@ +import { loadSharedSkillTemplate } from "../skill-file-loader" +import type { BuiltinSkill } from "../types" + +export const initDeepSkill: BuiltinSkill = { + name: "init-deep", + description: "(builtin) Initialize hierarchical AGENTS.md knowledge base", + template: loadSharedSkillTemplate("init-deep"), + argumentHint: "[--create-new] [--max-depth=N]", +} diff --git a/src/hooks/auto-slash-command/init-deep-skill.test.ts b/src/hooks/auto-slash-command/init-deep-skill.test.ts new file mode 100644 index 000000000..2730255c8 --- /dev/null +++ b/src/hooks/auto-slash-command/init-deep-skill.test.ts @@ -0,0 +1,43 @@ +/// + +import { describe, expect, test } from "bun:test" +import { createBuiltinSkills } from "../../features/builtin-skills" +import type { LoadedSkill } from "../../features/opencode-skill-loader" +import { executeSlashCommand } from "./executor" + +function asLoadedSkill(name: string): LoadedSkill { + const skill = createBuiltinSkills().find((candidate) => candidate.name === name) + if (!skill) { + throw new Error(`missing builtin skill: ${name}`) + } + + return { + name: skill.name, + definition: { + name: skill.name, + description: skill.description, + template: skill.template, + argumentHint: skill.argumentHint, + }, + scope: "builtin", + } +} + +describe("init-deep slash surface", () => { + test("#given init-deep builtin skill #when slash command executes #then it renders skill instructions with arguments", async () => { + // given + const skills = [asLoadedSkill("init-deep")] + + // when + const result = await executeSlashCommand( + { command: "init-deep", args: "--max-depth=2", raw: "/init-deep --max-depth=2" }, + { skills, pluginsEnabled: false }, + ) + + // then + expect(result.success).toBe(true) + expect(result.replacementText).toContain("**Scope**: skill") + expect(result.replacementText).toContain("--max-depth=2") + expect(result.replacementText).toContain("Generate hierarchical AGENTS.md files") + }) +})