Merge pull request #4400 from code-yeongyu/refactor/prompts-core-oracle-cleanup
refactor(prompts-core): address Oracle caveats — stale AGENTS.md docs + byte-exact characterization tests
This commit is contained in:
@@ -73,7 +73,7 @@ agents/
|
||||
├── metis.ts # Pre-planning
|
||||
├── momus.ts # Plan review
|
||||
├── atlas/agent.ts # Todo orchestrator
|
||||
├── prometheus/ # Strategic planner — system-prompt.ts, identity-constraints.ts, interview-mode.ts, plan-template.ts, gemini.ts, gpt.ts
|
||||
├── prometheus/ # Strategic planner thin loaders: system-prompt.ts, gemini.ts, gpt.ts; prompt content in packages/prompts-core/prompts/prometheus/
|
||||
├── types.ts # BuiltinAgentName, AgentMode, AgentConfig
|
||||
├── builtin-agents.ts # agentSources registry (10 → 11 with sisyphus-junior)
|
||||
├── builtin-agents/ # maybeCreateXXXConfig conditional factories + general-agents.ts + available-skills.ts
|
||||
|
||||
@@ -1,30 +1,47 @@
|
||||
---
|
||||
name: prometheus-agent
|
||||
description: Developer reference for the Prometheus strategic planner agent — interview flow, plan output format, and key constraints.
|
||||
description: Developer reference for the Prometheus strategic planner agent prompt loaders, prompts-core markdown variants, and model routing.
|
||||
---
|
||||
|
||||
# src/agents/prometheus/ -- Strategic Planner
|
||||
|
||||
**Generated:** 2026-05-15
|
||||
**Generated:** 2026-05-24
|
||||
|
||||
## OVERVIEW
|
||||
|
||||
11 files. Prometheus agent -- interview-mode strategic planner. Reads codebase, questions user, builds detailed work plan before any code is written. Markdown-only output (enforced by `prometheus-md-only` hook).
|
||||
5 TypeScript files plus 3 markdown prompt variants in [`packages/prompts-core/prompts/prometheus/`](file:///Users/yeongyu/local-workspaces/omo/packages/prompts-core/prompts/prometheus/). Prometheus remains the interview-mode strategic planner, but this directory is now a thin adapter layer. Prompt content lives in `packages/prompts-core`; `src/agents/prometheus/` only loads the right markdown variant and applies runtime tool gating.
|
||||
|
||||
This shape follows the package layering refactor in [`ROADMAP.md`](file:///Users/yeongyu/local-workspaces/omo/ROADMAP.md): prompts are harness-neutral core assets, while the OpenCode adapter keeps only model routing and runtime integration.
|
||||
|
||||
## FILES
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `system-prompt.ts` | Composes full system prompt from sections |
|
||||
| `identity-constraints.ts` | FORBIDDEN actions, .md-only enforcement, path restrictions |
|
||||
| `interview-mode.ts` | Interview flow: gather requirements, clarify scope |
|
||||
| `plan-generation.ts` | Plan output structure and validation |
|
||||
| `plan-template.ts` | YAML plan template with task graph, dependencies, waves |
|
||||
| `behavioral-summary.ts` | Behavioral guidelines section |
|
||||
| `high-accuracy-mode.ts` | Enhanced accuracy mode for complex plans |
|
||||
| `gemini.ts` | Gemini-optimized prompt variant |
|
||||
| `gpt.ts` | GPT-optimized prompt variant |
|
||||
| `index.ts` | Barrel exports |
|
||||
| `system-prompt.ts` | Thin loader using `loadPromptSync()` and `prometheusPromptVariants` from `@oh-my-opencode/prompts-core`; exports prompt source routing and disabled-tool filtering |
|
||||
| `gpt.ts` | Thin loader for `PROMETHEUS_GPT_SYSTEM_PROMPT` from `packages/prompts-core/prompts/prometheus/gpt.md` |
|
||||
| `gemini.ts` | Thin loader for `PROMETHEUS_GEMINI_SYSTEM_PROMPT` from `packages/prompts-core/prompts/prometheus/gemini.md` |
|
||||
| `system-prompt.test.ts` | Runtime behavior tests for Question tool filtering |
|
||||
| `prometheus-byte-exactness.test.ts` | Byte-exact sha256 characterization tests for all variants and Question disabled state |
|
||||
| `packages/prompts-core/prompts/prometheus/default.md` | Default/Claude markdown prompt variant |
|
||||
| `packages/prompts-core/prompts/prometheus/gpt.md` | GPT-optimized markdown prompt variant |
|
||||
| `packages/prompts-core/prompts/prometheus/gemini.md` | Gemini-optimized markdown prompt variant |
|
||||
|
||||
## MODEL VARIANT ROUTING
|
||||
|
||||
[`system-prompt.ts`](file:///Users/yeongyu/local-workspaces/omo/src/agents/prometheus/system-prompt.ts) exposes `getPrometheusPromptSource(model)`:
|
||||
|
||||
- GPT family models, as detected by `isGptModel(model)`, route to `"gpt"`.
|
||||
- Gemini family models, as detected by `isGeminiModel(model)`, route to `"gemini"`.
|
||||
- Missing models and all other families route to `"default"`.
|
||||
|
||||
`getPrometheusPrompt(model, disabledTools)` then loads the selected markdown through `loadPromptSync({ source: prometheusPromptVariants[variant], name: "prometheus", variant })` and returns the loaded body.
|
||||
|
||||
## DISABLED TOOL HANDLING
|
||||
|
||||
Prometheus normally includes `Question({ ... })` examples because interview mode uses the Question tool to clarify scope. When the runtime passes `disabledTools` containing `"question"`, `getPrometheusPrompt()` strips fenced TypeScript `Question({ ... })` examples with `QUESTION_TOOL_BLOCK_RE` before returning the prompt.
|
||||
|
||||
This filtering is runtime adapter behavior. Do not duplicate stripped markdown variants in `packages/prompts-core`; keep one source of truth per model family and let `system-prompt.ts` remove Question examples only when the tool is disabled.
|
||||
|
||||
## KEY CONSTRAINTS
|
||||
|
||||
@@ -33,10 +50,12 @@ description: Developer reference for the Prometheus strategic planner agent —
|
||||
- Must explore codebase before planning (NEVER plan blind)
|
||||
- Plans saved to `.omo/plans/`
|
||||
- Acceptance criteria requiring "user manually tests" are FORBIDDEN
|
||||
- Prompt edits belong in [`packages/prompts-core/prompts/prometheus/`](file:///Users/yeongyu/local-workspaces/omo/packages/prompts-core/prompts/prometheus/), not in TypeScript section files
|
||||
|
||||
## PLAN OUTPUT FORMAT
|
||||
|
||||
Plans use YAML with parallel task graph:
|
||||
The markdown variants instruct Prometheus to produce YAML plans with a parallel task graph:
|
||||
|
||||
- Waves (parallel execution groups)
|
||||
- Tasks with dependencies, category, skills
|
||||
- Each task has atomic scope + verification criteria
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
/// <reference types="bun-types" />
|
||||
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { createHash } from "node:crypto"
|
||||
import { getPrometheusPrompt } from "./system-prompt"
|
||||
|
||||
type PrometheusPromptBaseline = {
|
||||
readonly name: string
|
||||
readonly model: string | undefined
|
||||
readonly disabledTools: readonly string[]
|
||||
readonly sha256: string
|
||||
readonly shouldContainQuestionTool: boolean
|
||||
}
|
||||
|
||||
const PROMETHEUS_PROMPT_BASELINES: readonly PrometheusPromptBaseline[] = [
|
||||
{
|
||||
name: "default-enabled",
|
||||
model: undefined,
|
||||
disabledTools: [],
|
||||
sha256: "7cd6dcc764c4b6c7cca61cf3878a1a2b2fb91836b38cbd0ed348d3e778cea4d9",
|
||||
shouldContainQuestionTool: true,
|
||||
},
|
||||
{
|
||||
name: "default-question-disabled",
|
||||
model: undefined,
|
||||
disabledTools: ["question"],
|
||||
sha256: "db181638b60c222e5238daa8c090b8b908235d4a1cef7ff760839d4200195f59",
|
||||
shouldContainQuestionTool: false,
|
||||
},
|
||||
{
|
||||
name: "gpt-enabled",
|
||||
model: "gpt-5.5",
|
||||
disabledTools: [],
|
||||
sha256: "95e42fb8112a6aac3d702fa40ec4a8f89923acd239e1041646ed5a0fd2a9feb9",
|
||||
shouldContainQuestionTool: true,
|
||||
},
|
||||
{
|
||||
name: "gpt-question-disabled",
|
||||
model: "gpt-5.5",
|
||||
disabledTools: ["question"],
|
||||
sha256: "8792637920d271caec5675e63ed6685b1e5e6e292824fd6cf3f9a699e83a42fe",
|
||||
shouldContainQuestionTool: false,
|
||||
},
|
||||
{
|
||||
name: "gemini-enabled",
|
||||
model: "gemini-3.1-pro",
|
||||
disabledTools: [],
|
||||
sha256: "df846993f69aef852bfe14569231453c673d369d69229f6e67f5af0915c05a1d",
|
||||
shouldContainQuestionTool: true,
|
||||
},
|
||||
{
|
||||
name: "gemini-question-disabled",
|
||||
model: "gemini-3.1-pro",
|
||||
disabledTools: ["question"],
|
||||
sha256: "f9f9b7498c681a7d98a388a2e2aaf875227a145b4f824277ef54ed3a8d80106b",
|
||||
shouldContainQuestionTool: false,
|
||||
},
|
||||
]
|
||||
|
||||
describe("Prometheus prompt byte exactness", () => {
|
||||
test("#given captured Prometheus prompt baselines #then every variant keeps the same bytes", () => {
|
||||
for (const baseline of PROMETHEUS_PROMPT_BASELINES) {
|
||||
const prompt = getPrometheusPrompt(baseline.model, baseline.disabledTools)
|
||||
|
||||
expect(prompt.length, baseline.name).toBeGreaterThan(0)
|
||||
expect(hashPrompt(prompt), baseline.name).toBe(baseline.sha256)
|
||||
}
|
||||
})
|
||||
|
||||
test("#given Question tool availability changes #then Question examples follow disabledTools", () => {
|
||||
for (const baseline of PROMETHEUS_PROMPT_BASELINES) {
|
||||
const prompt = getPrometheusPrompt(baseline.model, baseline.disabledTools)
|
||||
|
||||
expect(prompt.includes("Question({"), baseline.name).toBe(baseline.shouldContainQuestionTool)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
function hashPrompt(prompt: string): string {
|
||||
return createHash("sha256").update(prompt).digest("hex")
|
||||
}
|
||||
@@ -1,55 +1,91 @@
|
||||
# src/hooks/keyword-detector/ — Mode Keyword Injection
|
||||
# src/hooks/keyword-detector/ -- Mode Keyword Injection
|
||||
|
||||
**Generated:** 2026-05-15
|
||||
**Generated:** 2026-05-24
|
||||
|
||||
## OVERVIEW
|
||||
|
||||
Transform Tier hook on `messages.transform`. Scans first user message for mode keywords (ultrawork, search, analyze, team) and injects mode-specific system prompts.
|
||||
Transform Tier hook on `messages.transform`. Scans the first user message for mode keywords and injects mode-specific system prompts. The detector and routing logic stay in `src/hooks/keyword-detector/`; prompt bodies now live in [`packages/prompts-core/prompts/`](file:///Users/yeongyu/local-workspaces/omo/packages/prompts-core/prompts/) so they can be shared by future harness adapters.
|
||||
|
||||
This matches the package layering direction in [`ROADMAP.md`](file:///Users/yeongyu/local-workspaces/omo/ROADMAP.md): `packages/prompts-core` owns static prompt content, while this OpenCode hook owns keyword detection, model routing, and message injection.
|
||||
|
||||
## KEYWORDS
|
||||
|
||||
| Keyword | Pattern | Effect |
|
||||
|---------|---------|--------|
|
||||
| `ultrawork` / `ulw` | `/\b(ultrawork|ulw)\b/i` | Full orchestration mode — parallel agents, deep exploration, relentless execution |
|
||||
| `ultrawork` / `ulw` | `/\b(ultrawork|ulw)\b/i` | Full orchestration mode: parallel agents, deep exploration, relentless execution |
|
||||
| Search mode | `SEARCH_PATTERN` (from `search/`) | Web/doc search focus prompt injection |
|
||||
| Analyze mode | `ANALYZE_PATTERN` (from `analyze/`) | Deep analysis mode prompt injection |
|
||||
| Team mode | `TEAM_PATTERN` (from `team/`) | Forces orchestration via `team_*` tools when user invokes `team mode` / `team-mode` / `team_mode` / `teammode`; instructs user to enable `team_mode.enabled` if tools are absent and reminds lead to run the closure sequence once every task is terminal |
|
||||
| Hyperplan mode | `HYPERPLAN_PATTERN` (from `hyperplan/`) | Loads the `hyperplan` skill and injects adversarial planning mode guidance |
|
||||
| Hyperplan-ultrawork combo | `HYPERPLAN_ULTRAWORK_PATTERN` (from `constants.ts`) | Prepends the combo banner, requires the `hyperplan` skill, then appends the routed ultrawork message |
|
||||
|
||||
## STRUCTURE
|
||||
|
||||
```
|
||||
keyword-detector/
|
||||
├── index.ts # Barrel export
|
||||
├── hook.ts # createKeywordDetectorHook() — chat.message handler
|
||||
├── hook.ts # createKeywordDetectorHook() chat.message handler
|
||||
├── detector.ts # detectKeywordsWithType() + extractPromptText()
|
||||
├── constants.ts # KEYWORD_DETECTORS array, re-exports from submodules
|
||||
├── types.ts # KeywordDetector, DetectedKeyword types
|
||||
├── ultrawork/
|
||||
│ ├── index.ts
|
||||
│ ├── message.ts # getUltraworkMessage() — dynamic prompt by agent/model
|
||||
│ └── isPlannerAgent.ts
|
||||
│ ├── index.ts # getUltraworkMessage() router
|
||||
│ ├── source-detector.ts # agent/model routing helpers
|
||||
│ ├── default.ts # thin loader for prompts-core/prompts/ultrawork/default.md
|
||||
│ ├── gpt.ts # thin loader for prompts-core/prompts/ultrawork/gpt.md
|
||||
│ ├── gemini.ts # thin loader for prompts-core/prompts/ultrawork/gemini.md
|
||||
│ └── planner.ts # thin loader for prompts-core/prompts/ultrawork/planner.md
|
||||
├── search/
|
||||
│ ├── index.ts
|
||||
│ ├── pattern.ts # SEARCH_PATTERN regex
|
||||
│ └── message.ts # SEARCH_MESSAGE
|
||||
│ └── default.ts # SEARCH_PATTERN + SEARCH_MESSAGE from prompts-core mode prompt
|
||||
├── analyze/
|
||||
│ ├── index.ts
|
||||
│ └── default.ts # ANALYZE_PATTERN + ANALYZE_MESSAGE
|
||||
└── team/
|
||||
│ └── default.ts # ANALYZE_PATTERN + ANALYZE_MESSAGE from prompts-core mode prompt
|
||||
├── team/
|
||||
│ ├── index.ts
|
||||
│ └── default.ts # TEAM_PATTERN + TEAM_MESSAGE from prompts-core mode prompt
|
||||
└── hyperplan/
|
||||
├── index.ts
|
||||
└── default.ts # TEAM_PATTERN + TEAM_MESSAGE
|
||||
└── default.ts # HYPERPLAN_PATTERN + HYPERPLAN_MESSAGE from prompts-core mode prompt
|
||||
```
|
||||
|
||||
## PROMPT CONTENT LOCATIONS
|
||||
|
||||
| Prompt family | Markdown source |
|
||||
|---------------|-----------------|
|
||||
| Ultrawork default | [`packages/prompts-core/prompts/ultrawork/default.md`](file:///Users/yeongyu/local-workspaces/omo/packages/prompts-core/prompts/ultrawork/default.md) |
|
||||
| Ultrawork GPT | [`packages/prompts-core/prompts/ultrawork/gpt.md`](file:///Users/yeongyu/local-workspaces/omo/packages/prompts-core/prompts/ultrawork/gpt.md) |
|
||||
| Ultrawork Gemini | [`packages/prompts-core/prompts/ultrawork/gemini.md`](file:///Users/yeongyu/local-workspaces/omo/packages/prompts-core/prompts/ultrawork/gemini.md) |
|
||||
| Ultrawork planner | [`packages/prompts-core/prompts/ultrawork/planner.md`](file:///Users/yeongyu/local-workspaces/omo/packages/prompts-core/prompts/ultrawork/planner.md) |
|
||||
| Search mode | [`packages/prompts-core/prompts/mode/search.md`](file:///Users/yeongyu/local-workspaces/omo/packages/prompts-core/prompts/mode/search.md) |
|
||||
| Analyze mode | [`packages/prompts-core/prompts/mode/analyze.md`](file:///Users/yeongyu/local-workspaces/omo/packages/prompts-core/prompts/mode/analyze.md) |
|
||||
| Team mode | [`packages/prompts-core/prompts/mode/team.md`](file:///Users/yeongyu/local-workspaces/omo/packages/prompts-core/prompts/mode/team.md) |
|
||||
| Hyperplan mode | [`packages/prompts-core/prompts/mode/hyperplan.md`](file:///Users/yeongyu/local-workspaces/omo/packages/prompts-core/prompts/mode/hyperplan.md) |
|
||||
|
||||
The `src/hooks/keyword-detector/{search,analyze,team,hyperplan}/default.ts` files keep the regex triggers in the hook layer and import the markdown-backed constants from `@oh-my-opencode/prompts-core`. The ultrawork files import markdown with Bun's `.md` text loader so the exact prompt bytes are bundled into `dist/index.js`.
|
||||
|
||||
## ULTRAWORK VARIANT ROUTING
|
||||
|
||||
[`ultrawork/source-detector.ts`](file:///Users/yeongyu/local-workspaces/omo/src/hooks/keyword-detector/ultrawork/source-detector.ts) decides the ultrawork source in priority order:
|
||||
|
||||
1. Planner agents (`prometheus`, `planner`, or normalized `plan`) route to `planner.md`.
|
||||
2. GPT family models, as detected by `isGptModel(modelID)`, route to `gpt.md`.
|
||||
3. Gemini family models, as detected by `isGeminiModel(modelID)`, route to `gemini.md`.
|
||||
4. Everything else routes to `default.md`.
|
||||
|
||||
[`ultrawork/index.ts`](file:///Users/yeongyu/local-workspaces/omo/src/hooks/keyword-detector/ultrawork/index.ts) exposes `getUltraworkMessage(agentName, modelID)`, switches on that source, and returns the loaded markdown body.
|
||||
|
||||
## DETECTION LOGIC
|
||||
|
||||
```
|
||||
chat.message (user input)
|
||||
→ extractPromptText(parts)
|
||||
→ isSystemDirective? → skip
|
||||
→ removeSystemReminders(text) # strip <SYSTEM_REMINDER> blocks
|
||||
→ detectKeywordsWithType(cleanText, agentName, modelID, disabledKeywords)
|
||||
→ isPlannerAgent(agentName)? → filter out ultrawork
|
||||
→ for each detected keyword: inject mode message into output
|
||||
-> extractPromptText(parts)
|
||||
-> isSystemDirective? skip
|
||||
-> removeSystemReminders(text) # strip <SYSTEM_REMINDER> blocks
|
||||
-> detectKeywordsWithType(cleanText, agentName, modelID, disabledKeywords)
|
||||
-> isNonOmoAgent(agentName)? filter keyword injection
|
||||
-> isPlannerAgent(agentName)? filter standalone ultrawork
|
||||
-> for each detected keyword: inject mode message into output
|
||||
```
|
||||
|
||||
## CONFIG
|
||||
@@ -57,17 +93,20 @@ chat.message (user input)
|
||||
```jsonc
|
||||
{
|
||||
"keyword_detector": {
|
||||
// Skip injection for any keyword in this list. Allowed: "ultrawork", "search", "analyze", "team".
|
||||
// Skip injection for any keyword in this list.
|
||||
// Allowed: "ultrawork", "search", "analyze", "team", "hyperplan", "hyperplan-ultrawork".
|
||||
"disabled_keywords": ["search", "analyze"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Default: empty/missing → all four detectors active. Schema lives at [src/config/schema/keyword-detector.ts](../../config/schema/keyword-detector.ts).
|
||||
Default: empty/missing means every detector is active. Schema lives at [`src/config/schema/keyword-detector.ts`](file:///Users/yeongyu/local-workspaces/omo/src/config/schema/keyword-detector.ts).
|
||||
|
||||
## GUARDS
|
||||
|
||||
- **System directive skip**: Messages tagged as system directives are not scanned (prevents infinite loops)
|
||||
- **Planner agent filter**: Prometheus/plan agents do not receive `ultrawork` injection
|
||||
- **Non-OMO agent filter**: OpenCode built-in Builder/Plan agents do not receive keyword injection
|
||||
- **Session agent tracking**: Uses `getSessionAgent()` to get actual agent (not just input hint)
|
||||
- **Model-aware messages**: `getUltraworkMessage(agentName, modelID)` adapts message to active model
|
||||
- **Prompt byte baselines**: `mode-prompt-baseline.test.ts` pins mode prompt hashes; `ultrawork/ultrawork-byte-exactness.test.ts` pins ultrawork prompt hashes
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/// <reference types="bun-types" />
|
||||
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { createHash } from "node:crypto"
|
||||
import { getUltraworkMessage, getUltraworkSource } from "./index"
|
||||
import type { UltraworkSource } from "./source-detector"
|
||||
|
||||
type UltraworkPromptBaseline = {
|
||||
readonly name: string
|
||||
readonly agentName: string
|
||||
readonly modelID: string
|
||||
readonly expectedSource: UltraworkSource
|
||||
readonly sha256: string
|
||||
}
|
||||
|
||||
const ULTRAWORK_PROMPT_BASELINES: readonly UltraworkPromptBaseline[] = [
|
||||
{
|
||||
name: "default",
|
||||
agentName: "sisyphus",
|
||||
modelID: "claude-sonnet-4-6",
|
||||
expectedSource: "default",
|
||||
sha256: "78aa43e2e2b7db307827d9ddda30a4c6a24aa35a9255efe1ee39a4476d71acca",
|
||||
},
|
||||
{
|
||||
name: "gpt",
|
||||
agentName: "sisyphus",
|
||||
modelID: "gpt-5.5",
|
||||
expectedSource: "gpt",
|
||||
sha256: "8f31f0053256914e94605944b28e123c584a0ad093e0d44d5ad66da009a632ae",
|
||||
},
|
||||
{
|
||||
name: "gemini",
|
||||
agentName: "sisyphus",
|
||||
modelID: "gemini-3.1-pro",
|
||||
expectedSource: "gemini",
|
||||
sha256: "5c5766549e868e7a1c87252e742e491b7015138948c6e26fb704346bb55d5d7c",
|
||||
},
|
||||
{
|
||||
name: "planner",
|
||||
agentName: "prometheus",
|
||||
modelID: "gpt-5.5",
|
||||
expectedSource: "planner",
|
||||
sha256: "8897b3a11b61c12a02bfba13a76c80742bc4e5356cfc30e2f0c38464aa587bf3",
|
||||
},
|
||||
]
|
||||
|
||||
describe("Ultrawork prompt byte exactness", () => {
|
||||
test("#given captured ultrawork prompt baselines #then every routed source keeps the same bytes", () => {
|
||||
for (const baseline of ULTRAWORK_PROMPT_BASELINES) {
|
||||
const source = getUltraworkSource(baseline.agentName, baseline.modelID)
|
||||
const prompt = getUltraworkMessage(baseline.agentName, baseline.modelID)
|
||||
|
||||
expect(source, baseline.name).toBe(baseline.expectedSource)
|
||||
expect(prompt.length, baseline.name).toBeGreaterThan(0)
|
||||
expect(hashPrompt(prompt), baseline.name).toBe(baseline.sha256)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
function hashPrompt(prompt: string): string {
|
||||
return createHash("sha256").update(prompt).digest("hex")
|
||||
}
|
||||
Reference in New Issue
Block a user