diff --git a/AGENTS.md b/AGENTS.md index 7bbe40a58..e6bd1b2ad 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -32,13 +32,15 @@ oh-my-opencode/ │ ├── openclaw/ # Bidirectional external integration (Discord/Telegram/HTTP/shell + reply listener daemon) │ ├── generated/ # model-capabilities.generated.json (refreshed via build:model-capabilities) │ └── testing/ # Test utilities + `create-plugin-module.ts` (extracted plugin entry factory, 182 LOC) -├── packages/ # 11 platform binaries + 2 MCP packages + 7 Core packages + web +├── packages/ # 11 platform binaries + 2 MCP packages + 9 Core packages + web │ ├── utils/ # Shared utilities — deep-merge, snake-case, frontmatter, file-utils, etc. │ ├── model-core/ # Model resolution pipeline with ProviderCache DI +│ ├── prompts-core/ # Markdown prompt loading + bundled mode prompts (search/analyze/team/hyperplan) │ ├── rules-engine/ # Rule discovery + matching (renamed from rules-core) │ ├── agents-md-core/ # AGENTS.md walk-up discovery + injection │ ├── ast-grep-core/ # ast-grep types, pattern-hints, runner core with injectable spawn │ ├── comment-checker-core/ # apply-patch parser, binary runner with injectable spawn +│ ├── hashline-core/ # Hashline edit primitives and diff helpers shared by adapter shims │ ├── boulder-state/ # Work tracking state machine, split storage │ └── web/ # Marketing site (Next.js 15 + Cloudflare Workers). Independent package with own bun.lock ├── bin/ # Platform-detection JS shim (oh-my-opencode + oh-my-openagent) diff --git a/packages/AGENTS.md b/packages/AGENTS.md index 61216a27e..68eac8296 100644 --- a/packages/AGENTS.md +++ b/packages/AGENTS.md @@ -4,7 +4,7 @@ ## OVERVIEW -15 sibling packages across 4 roles. None of these are published as part of the main `oh-my-opencode` / `oh-my-openagent` npm dist (root `package.json` `files` only ships `dist/`, `bin/`, `postinstall.mjs`). They are sibling packages with their own publication / deployment targets. +23 sibling packages across 4 roles. None of these are published as part of the main `oh-my-opencode` / `oh-my-openagent` npm dist (root `package.json` `files` only ships `dist/`, `bin/`, `postinstall.mjs`). They are sibling packages with their own publication / deployment targets. ## ROLE MAP @@ -12,7 +12,7 @@ |------|-------|----------| | **Platform binaries** | 11 | One per (OS × arch × variant). Uniform layout: `bin/` + `package.json` only. Selected at install time by `bin/` shim + `postinstall.mjs`. | | **MCP packages** | 2 | `lsp-tools-mcp` (git submodule), `ast-grep-mcp` | -| **Core packages** | 7 | `utils`, `model-core`, `rules-engine` (was `rules-core`), `agents-md-core`, `ast-grep-core`, `comment-checker-core`, `boulder-state` | +| **Core packages** | 9 | `utils`, `model-core`, `prompts-core`, `rules-engine` (was `rules-core`), `agents-md-core`, `ast-grep-core`, `comment-checker-core`, `hashline-core`, `boulder-state` | | **Web** | 1 | `web` | ## PLATFORM BINARIES (11) @@ -36,10 +36,12 @@ Each contains only a `bin/` and a `package.json`. Built by [`script/buil |---------|--------|---------| | `utils/` | `src/`, `tsconfig.json` | Shared utilities: deep-merge, snake-case, frontmatter, file-utils, etc. | | `model-core/` | `src/`, `tsconfig.json` | Model resolution pipeline with ProviderCache dependency injection. | +| `prompts-core/` | `src/`, `prompts/`, `test/`, `tsconfig.json` | Harness-neutral markdown prompt loading, model-variant routing, and bundled mode prompts for search/analyze/team/hyperplan. | | `rules-engine/` | `src/`, `tsconfig.json` | Rule discovery + matching engine (renamed from `rules-core`). | | `agents-md-core/` | `src/`, `tsconfig.json` | AGENTS.md walk-up discovery and injection logic. | | `ast-grep-core/` | `src/`, `tsconfig.json` | ast-grep types, pattern-hints, and runner core with injectable spawn. | | `comment-checker-core/` | `src/`, `tsconfig.json` | apply-patch parser and binary runner with injectable spawn. | +| `hashline-core/` | `src/`, `tsconfig.json` | Hashline edit primitives and diff helpers shared by adapter shims. | | `boulder-state/` | `src/`, `tsconfig.json` | Work tracking state machine with split storage. | ## WEB diff --git a/packages/prompts-core/prompts/mode/analyze.md b/packages/prompts-core/prompts/mode/analyze.md new file mode 100644 index 000000000..b60f9952c --- /dev/null +++ b/packages/prompts-core/prompts/mode/analyze.md @@ -0,0 +1,16 @@ +[analyze-mode] +ANALYSIS MODE. Gather context before diving deep: + +CONTEXT GATHERING (parallel): +- 1-2 explore agents (codebase patterns, implementations) +- 1-2 librarian agents (if external library involved) +- Direct tools: Grep, AST-grep, LSP for targeted searches + +IF COMPLEX - DO NOT STRUGGLE ALONE. Consult specialists: +- **Oracle**: Conventional problems (architecture, debugging, complex logic) +- **Artistry**: Non-conventional problems (different approach needed) + +SYNTHESIZE findings before proceeding. +--- +MANDATORY delegate_task params: ALWAYS include load_skills and run_in_background when calling delegate_task. Evaluate available skills before dispatch - pass task-appropriate skills when relevant, pass [] ONLY when no skill matches the task domain. +Example: delegate_task(subagent_type="explore", prompt="...", run_in_background=true, load_skills=[]) diff --git a/packages/prompts-core/prompts/mode/hyperplan.md b/packages/prompts-core/prompts/mode/hyperplan.md new file mode 100644 index 000000000..bb70c4ee0 --- /dev/null +++ b/packages/prompts-core/prompts/mode/hyperplan.md @@ -0,0 +1,25 @@ + +**MANDATORY**: Say "HYPERPLAN MODE ENABLED!" as your first response, exactly once. + +The user invoked **hyperplan mode** — adversarial multi-agent planning via team-mode. + +LOAD THE HYPERPLAN SKILL IMMEDIATELY: + +``` +skill(name="hyperplan") +``` + +After loading, follow the skill's full workflow EXACTLY: +1. Acknowledge and capture the planning request +2. Spawn the adversarial team via `team_create` with category members `unspecified-low`, `unspecified-high`, `ultrabrain`, and `artistry`; include `deep` only if the category is enabled +3. Round 1 — Independent analysis (each member produces findings) +4. Round 2 — Cross-attack (each member ruthlessly attacks the other 4's findings) +5. Round 3 — Defend, refine, or concede +6. Distill defensible insights into a structured bundle (Lead does NOT write the plan) +7. MANDATORY: hand the bundle to the `plan` agent via `task(subagent_type="plan", ...)` — the plan agent owns sequencing, parallelization, and verification gates +8. Present the plan agent's output verbatim with provenance line, then clean up the team + +Do NOT improvise. Do NOT skip rounds. Do NOT write the plan yourself in step 6 — the handoff to the plan agent in step 7 is non-negotiable. Be the lead orchestrator and let the adversarial members do the cross-critique. + +If team-mode is unavailable (`team_*` tools missing), instruct the user to set `team_mode.enabled: true` in `~/.config/opencode/oh-my-opencode.jsonc` and restart opencode. + diff --git a/packages/prompts-core/prompts/mode/search.md b/packages/prompts-core/prompts/mode/search.md new file mode 100644 index 000000000..0ba054e1a --- /dev/null +++ b/packages/prompts-core/prompts/mode/search.md @@ -0,0 +1,6 @@ +[search-mode] +MAXIMIZE SEARCH EFFORT. Launch multiple background agents IN PARALLEL: +- explore agents (codebase patterns, file structures, ast-grep) +- librarian agents (remote repos, official docs, GitHub examples) +Plus direct tools: Grep, ripgrep (rg), ast-grep (sg) +NEVER stop at first result - be exhaustive. diff --git a/packages/prompts-core/prompts/mode/team.md b/packages/prompts-core/prompts/mode/team.md new file mode 100644 index 000000000..ed90fd575 --- /dev/null +++ b/packages/prompts-core/prompts/mode/team.md @@ -0,0 +1,2 @@ +[team-mode] +Team-mode reference detected. Orchestrate via team_* tools (team_create -> team_task_create + team_send_message); NEVER substitute with delegate_task — it is not equivalent. After every team_task_update that completes or fails a task, re-check team_task_list: if every task is terminal, run the closure sequence (team_shutdown_request + team_approve_shutdown per active member, then team_delete) in the same turn. Closing the team is the lead's responsibility, not the user's. If the team_* tools are absent, team_mode is disabled — tell the user to set team_mode.enabled=true and restart opencode. diff --git a/packages/prompts-core/src/index.ts b/packages/prompts-core/src/index.ts index 2b0ee9c90..fe92ade94 100644 --- a/packages/prompts-core/src/index.ts +++ b/packages/prompts-core/src/index.ts @@ -15,3 +15,9 @@ export { atlasPromptVariants } from "./atlas-prompts" export { resolveVariant } from "./variant-resolver" export type { ResolveVariantInput } from "./variant-resolver" export { loadPrompt, loadPromptSync, PromptFileNotFoundError, PromptPathTraversalError } from "./loader" +export { + ANALYZE_MODE_PROMPT, + HYPERPLAN_MODE_PROMPT, + SEARCH_MODE_PROMPT, + TEAM_MODE_PROMPT, +} from "./mode-prompts" diff --git a/packages/prompts-core/src/markdown.d.ts b/packages/prompts-core/src/markdown.d.ts new file mode 100644 index 000000000..2a2a99ee8 --- /dev/null +++ b/packages/prompts-core/src/markdown.d.ts @@ -0,0 +1,4 @@ +declare module "*.md" { + const markdown: string + export default markdown +} diff --git a/packages/prompts-core/src/mode-prompts.ts b/packages/prompts-core/src/mode-prompts.ts new file mode 100644 index 000000000..3603e3c12 --- /dev/null +++ b/packages/prompts-core/src/mode-prompts.ts @@ -0,0 +1,13 @@ +import hyperplanModePrompt from "../prompts/mode/hyperplan.md" with { type: "text" } +import analyzeModePrompt from "../prompts/mode/analyze.md" with { type: "text" } +import searchModePrompt from "../prompts/mode/search.md" with { type: "text" } +import teamModePrompt from "../prompts/mode/team.md" with { type: "text" } + +export const ANALYZE_MODE_PROMPT = stripFinalLineFeed(analyzeModePrompt) +export const HYPERPLAN_MODE_PROMPT = stripFinalLineFeed(hyperplanModePrompt) +export const SEARCH_MODE_PROMPT = stripFinalLineFeed(searchModePrompt) +export const TEAM_MODE_PROMPT = stripFinalLineFeed(teamModePrompt) + +function stripFinalLineFeed(prompt: string): string { + return prompt.endsWith("\n") ? prompt.slice(0, -1) : prompt +} diff --git a/src/hooks/keyword-detector/analyze/default.ts b/src/hooks/keyword-detector/analyze/default.ts index 01a0c08d1..d1a533529 100644 --- a/src/hooks/keyword-detector/analyze/default.ts +++ b/src/hooks/keyword-detector/analyze/default.ts @@ -1,3 +1,5 @@ +import { ANALYZE_MODE_PROMPT } from "@oh-my-opencode/prompts-core" + /** * Analyze mode keyword detector. * @@ -12,19 +14,4 @@ export const ANALYZE_PATTERN = /\b(analyze|analyse|investigate|examine|research|study|deep[\s-]?dive|inspect|audit|evaluate|assess|review|diagnose|scrutinize|dissect|debug|comprehend|interpret|breakdown|understand)\b|why\s+is|how\s+does|how\s+to|분석|조사|파악|연구|검토|진단|이해|설명|원인|이유|뜯어봐|따져봐|평가|해석|디버깅|디버그|어떻게|왜|살펴|分析|調査|解析|検討|研究|診断|理解|説明|検証|精査|究明|デバッグ|なぜ|どう|仕組み|调查|检查|剖析|深入|诊断|解释|调试|为什么|原理|搞清楚|弄明白|phân tích|điều tra|nghiên cứu|kiểm tra|xem xét|chẩn đoán|giải thích|tìm hiểu|gỡ lỗi|tại sao/i -export const ANALYZE_MESSAGE = `[analyze-mode] -ANALYSIS MODE. Gather context before diving deep: - -CONTEXT GATHERING (parallel): -- 1-2 explore agents (codebase patterns, implementations) -- 1-2 librarian agents (if external library involved) -- Direct tools: Grep, AST-grep, LSP for targeted searches - -IF COMPLEX - DO NOT STRUGGLE ALONE. Consult specialists: -- **Oracle**: Conventional problems (architecture, debugging, complex logic) -- **Artistry**: Non-conventional problems (different approach needed) - -SYNTHESIZE findings before proceeding. ---- -MANDATORY delegate_task params: ALWAYS include load_skills and run_in_background when calling delegate_task. Evaluate available skills before dispatch - pass task-appropriate skills when relevant, pass [] ONLY when no skill matches the task domain. -Example: delegate_task(subagent_type="explore", prompt="...", run_in_background=true, load_skills=[])` +export const ANALYZE_MESSAGE = ANALYZE_MODE_PROMPT diff --git a/src/hooks/keyword-detector/hyperplan/default.ts b/src/hooks/keyword-detector/hyperplan/default.ts index cf27e087a..e049c8c5d 100644 --- a/src/hooks/keyword-detector/hyperplan/default.ts +++ b/src/hooks/keyword-detector/hyperplan/default.ts @@ -1,3 +1,5 @@ +import { HYPERPLAN_MODE_PROMPT } from "@oh-my-opencode/prompts-core" + /** * Hyperplan keyword detector. * @@ -17,28 +19,4 @@ export const HYPERPLAN_PATTERN = /\bhyperplan\b|(? -**MANDATORY**: Say "HYPERPLAN MODE ENABLED!" as your first response, exactly once. - -The user invoked **hyperplan mode** — adversarial multi-agent planning via team-mode. - -LOAD THE HYPERPLAN SKILL IMMEDIATELY: - -\`\`\` -skill(name="hyperplan") -\`\`\` - -After loading, follow the skill's full workflow EXACTLY: -1. Acknowledge and capture the planning request -2. Spawn the adversarial team via \`team_create\` with category members \`unspecified-low\`, \`unspecified-high\`, \`ultrabrain\`, and \`artistry\`; include \`deep\` only if the category is enabled -3. Round 1 — Independent analysis (each member produces findings) -4. Round 2 — Cross-attack (each member ruthlessly attacks the other 4's findings) -5. Round 3 — Defend, refine, or concede -6. Distill defensible insights into a structured bundle (Lead does NOT write the plan) -7. MANDATORY: hand the bundle to the \`plan\` agent via \`task(subagent_type="plan", ...)\` — the plan agent owns sequencing, parallelization, and verification gates -8. Present the plan agent's output verbatim with provenance line, then clean up the team - -Do NOT improvise. Do NOT skip rounds. Do NOT write the plan yourself in step 6 — the handoff to the plan agent in step 7 is non-negotiable. Be the lead orchestrator and let the adversarial members do the cross-critique. - -If team-mode is unavailable (\`team_*\` tools missing), instruct the user to set \`team_mode.enabled: true\` in \`~/.config/opencode/oh-my-opencode.jsonc\` and restart opencode. -` +export const HYPERPLAN_MESSAGE = HYPERPLAN_MODE_PROMPT diff --git a/src/hooks/keyword-detector/mode-prompt-baseline.test.ts b/src/hooks/keyword-detector/mode-prompt-baseline.test.ts new file mode 100644 index 000000000..f5fd70b95 --- /dev/null +++ b/src/hooks/keyword-detector/mode-prompt-baseline.test.ts @@ -0,0 +1,96 @@ +import { describe, expect, test } from "bun:test" +import { createHash } from "node:crypto" +import { dirname, join } from "node:path" +import { fileURLToPath } from "node:url" +import { ANALYZE_MESSAGE, HYPERPLAN_MESSAGE, SEARCH_MESSAGE, TEAM_MESSAGE } from "./constants" + +type PromptBaseline = { + readonly name: string + readonly message: string + readonly sha256: string + readonly byteLength: number +} + +type ShimBaseline = { + readonly name: string + readonly filePath: string +} + +const MODE_PROMPT_BASELINES: readonly PromptBaseline[] = [ + { + name: "search", + message: SEARCH_MESSAGE, + sha256: "aa38d1011edcf083394441321564330661868411ec13b575e5994812fae27f62", + byteLength: 311, + }, + { + name: "analyze", + message: ANALYZE_MESSAGE, + sha256: "63f9de6f7afb67ab68bc4abcc7e78c8450a6ce556378a7a5d2b9061a3c519d7f", + byteLength: 865, + }, + { + name: "team", + message: TEAM_MESSAGE, + sha256: "21fd4110835ce380e307cf29e132753b04a58758b86cfaaf5dda26e0e3193d69", + byteLength: 614, + }, + { + name: "hyperplan", + message: HYPERPLAN_MESSAGE, + sha256: "cea6f378370c736909be99bd9a66a06db1e4819848336dd7951298e949270ced", + byteLength: 1500, + }, +] + +const KEYWORD_DETECTOR_DIR = dirname(fileURLToPath(import.meta.url)) + +const MODE_SHIMS: readonly ShimBaseline[] = [ + { name: "search", filePath: join(KEYWORD_DETECTOR_DIR, "search", "default.ts") }, + { name: "analyze", filePath: join(KEYWORD_DETECTOR_DIR, "analyze", "default.ts") }, + { name: "team", filePath: join(KEYWORD_DETECTOR_DIR, "team", "default.ts") }, + { name: "hyperplan", filePath: join(KEYWORD_DETECTOR_DIR, "hyperplan", "default.ts") }, +] + +describe("keyword-detector mode prompt baselines", () => { + test("#given captured prompt baselines #then each mode message keeps the same bytes", () => { + for (const baseline of MODE_PROMPT_BASELINES) { + expect(hashPrompt(baseline.message), baseline.name).toBe(baseline.sha256) + expect(Buffer.byteLength(baseline.message, "utf8"), baseline.name).toBe(baseline.byteLength) + } + }) + + test("#given migrated mode shims #then each shim stays within the LOC ceiling", async () => { + for (const shim of MODE_SHIMS) { + const source = await Bun.file(shim.filePath).text() + + expect(countPureLoc(source), shim.name).toBeLessThanOrEqual(20) + } + }) +}) + +function hashPrompt(prompt: string): string { + return createHash("sha256").update(prompt, "utf8").digest("hex") +} + +function countPureLoc(source: string): number { + let pureLoc = 0 + let insideBlockComment = false + + for (const rawLine of source.split("\n")) { + const line = rawLine.trim() + if (line.length === 0) continue + if (insideBlockComment) { + insideBlockComment = !line.includes("*/") + continue + } + if (line.startsWith("/*")) { + insideBlockComment = !line.includes("*/") + continue + } + if (line.startsWith("//")) continue + pureLoc += 1 + } + + return pureLoc +} diff --git a/src/hooks/keyword-detector/search/default.ts b/src/hooks/keyword-detector/search/default.ts index 579574e18..2b999b280 100644 --- a/src/hooks/keyword-detector/search/default.ts +++ b/src/hooks/keyword-detector/search/default.ts @@ -1,3 +1,5 @@ +import { SEARCH_MODE_PROMPT } from "@oh-my-opencode/prompts-core" + /** * Search mode keyword detector. * @@ -12,9 +14,4 @@ export const SEARCH_PATTERN = /\b(search|find|locate|lookup|look\s*up|explore|discover|scan|grep|query|browse|detect|trace|seek|track|pinpoint|hunt)\b|where\s+is|show\s+me|list\s+all|검색|찾아|탐색|조회|스캔|서치|뒤져|찾기|어디|추적|탐지|찾아봐|찾아내|보여줘|목록|検索|探して|見つけて|サーチ|探索|スキャン|どこ|発見|捜索|見つけ出す|一覧|搜索|查找|寻找|查询|检索|定位|扫描|发现|在哪里|找出来|列出|tìm kiếm|tra cứu|định vị|quét|phát hiện|truy tìm|tìm ra|ở đâu|liệt kê/i -export const SEARCH_MESSAGE = `[search-mode] -MAXIMIZE SEARCH EFFORT. Launch multiple background agents IN PARALLEL: -- explore agents (codebase patterns, file structures, ast-grep) -- librarian agents (remote repos, official docs, GitHub examples) -Plus direct tools: Grep, ripgrep (rg), ast-grep (sg) -NEVER stop at first result - be exhaustive.` +export const SEARCH_MESSAGE = SEARCH_MODE_PROMPT diff --git a/src/hooks/keyword-detector/team/default.ts b/src/hooks/keyword-detector/team/default.ts index 43e0ce700..31a9399db 100644 --- a/src/hooks/keyword-detector/team/default.ts +++ b/src/hooks/keyword-detector/team/default.ts @@ -1,3 +1,5 @@ +import { TEAM_MODE_PROMPT } from "@oh-my-opencode/prompts-core" + /** * Team mode keyword detector. * @@ -7,5 +9,4 @@ export const TEAM_PATTERN = /\bteam[\s_-]?mode\b/i -export const TEAM_MESSAGE = `[team-mode] -Team-mode reference detected. Orchestrate via team_* tools (team_create -> team_task_create + team_send_message); NEVER substitute with delegate_task — it is not equivalent. After every team_task_update that completes or fails a task, re-check team_task_list: if every task is terminal, run the closure sequence (team_shutdown_request + team_approve_shutdown per active member, then team_delete) in the same turn. Closing the team is the lead's responsibility, not the user's. If the team_* tools are absent, team_mode is disabled — tell the user to set team_mode.enabled=true and restart opencode.` +export const TEAM_MESSAGE = TEAM_MODE_PROMPT diff --git a/src/markdown.d.ts b/src/markdown.d.ts new file mode 100644 index 000000000..2a2a99ee8 --- /dev/null +++ b/src/markdown.d.ts @@ -0,0 +1,4 @@ +declare module "*.md" { + const markdown: string + export default markdown +}