diff --git a/.agents/agents/variants/opencode.json b/.agents/agents/variants/opencode.json new file mode 100644 index 0000000..b304624 --- /dev/null +++ b/.agents/agents/variants/opencode.json @@ -0,0 +1,22 @@ +{ + "$schema": "./agent-variant.schema.json", + "vendor": "opencode", + "destDir": ".opencode/agents", + "modelDefault": "opencode-go/deepseek-v4-flash", + "toolsDefault": [], + "protocolPath": ".agents/skills/_shared/runtime/execution-protocols/opencode.md", + "agents": { + "backend-engineer": {}, + "frontend-engineer": {}, + "db-engineer": {}, + "debug-investigator": {}, + "architecture-reviewer": {}, + "tf-infra-engineer": {}, + "mobile-engineer": {}, + "pm-planner": {}, + "qa-reviewer": {}, + "docs-curator": {}, + "refactor-engineer": {}, + "research-explorer": {} + } +} diff --git a/.agents/config/defaults.yaml b/.agents/config/defaults.yaml index 1701aab..ca87bb6 100644 --- a/.agents/config/defaults.yaml +++ b/.agents/config/defaults.yaml @@ -55,21 +55,6 @@ runtime_profiles: tf-infra: { model: "openai/gpt-5.5", effort: "high" } explore: { model: "openai/gpt-5.4-mini", effort: "low" } - gemini: - description: "Gemini — Google AI Pro" - agent_defaults: - orchestrator: { model: "google/gemini-3-flash" } - architecture: { model: "google/gemini-3.1-pro-preview", thinking: true } - qa: { model: "google/gemini-3-flash", thinking: true } - pm: { model: "google/gemini-3-flash" } - backend: { model: "google/gemini-3-flash", thinking: true } - frontend: { model: "google/gemini-3-flash", thinking: true } - mobile: { model: "google/gemini-3-flash", thinking: true } - db: { model: "google/gemini-3-flash", thinking: true } - debug: { model: "google/gemini-3-flash", thinking: true } - tf-infra: { model: "google/gemini-3-flash", thinking: true } - explore: { model: "google/gemini-3.1-flash-lite" } - mixed: description: "Mixed — role-optimal vendors per agent (Claude for orchestration/QA/PM, Codex for impl, Gemini for explore)" agent_defaults: diff --git a/.agents/hooks/core/agentmemory-client.ts b/.agents/hooks/core/agentmemory-client.ts index 5816663..2611828 100644 --- a/.agents/hooks/core/agentmemory-client.ts +++ b/.agents/hooks/core/agentmemory-client.ts @@ -142,15 +142,63 @@ export interface RecalledFact { interface SearchResult { score?: number; + timestamp?: unknown; + created_at?: unknown; observation?: { narrative?: unknown; facts?: unknown; title?: unknown; type?: unknown; + timestamp?: unknown; + created_at?: unknown; }; } -function parseSearchResults(body: string, k: number): RecalledFact[] { +/** + * Recall TTL: facts older than this many days are dropped from the snapshot so + * stale, long-resolved decisions stop rehydrating every boundary. Default 30 + * days; set `OMA_RECALL_MAX_AGE_DAYS=0` (or a non-positive value) to disable. + * Returns the max age in ms, or null when disabled. + */ +function recallMaxAgeMs(): number | null { + const raw = process.env.OMA_RECALL_MAX_AGE_DAYS; + const days = raw === undefined ? 30 : Number(raw); + if (!Number.isFinite(days) || days <= 0) return null; + return days * 24 * 60 * 60 * 1000; +} + +/** + * Best-effort timestamp extraction from a search result. AgentMemory's response + * envelope is not contractually fixed across versions, so several candidate + * field names / locations are probed. Numeric epoch seconds are normalised to + * ms. Returns null when no parseable timestamp is present — callers then keep + * the fact (TTL filtering is fail-open, never dropping facts of unknown age). + */ +function extractTimestampMs(entry: SearchResult): number | null { + const obs = entry.observation ?? {}; + const candidates: unknown[] = [ + obs.timestamp, + obs.created_at, + entry.timestamp, + entry.created_at, + ]; + for (const candidate of candidates) { + if (typeof candidate === "number" && Number.isFinite(candidate)) { + return candidate < 1e12 ? candidate * 1000 : candidate; + } + if (typeof candidate === "string" && candidate.trim()) { + const parsed = Date.parse(candidate); + if (Number.isFinite(parsed)) return parsed; + } + } + return null; +} + +export function parseSearchResults( + body: string, + k: number, + nowMs: number = Date.now(), +): RecalledFact[] { let parsed: { results?: unknown }; try { parsed = JSON.parse(body) as { results?: unknown }; @@ -164,12 +212,20 @@ function parseSearchResults(body: string, k: number): RecalledFact[] { return Number.isFinite(raw) ? raw : 1; })(); + const maxAgeMs = recallMaxAgeMs(); + const cutoffMs = maxAgeMs === null ? null : nowMs - maxAgeMs; + const facts: RecalledFact[] = []; for (const entry of parsed.results as SearchResult[]) { const score = typeof entry.score === "number" ? entry.score : 0; // Raw `/observe` envelopes score near-zero (~0.006); enriched facts score // in the single digits. Drop the noise floor so the snapshot stays useful. if (score < minScore) continue; + // TTL: drop facts older than the cutoff (fail-open on unknown age). + if (cutoffMs !== null) { + const tsMs = extractTimestampMs(entry); + if (tsMs !== null && tsMs < cutoffMs) continue; + } const obs = entry.observation ?? {}; const narrative = typeof obs.narrative === "string" && obs.narrative.trim() diff --git a/.agents/hooks/core/triggers.json b/.agents/hooks/core/triggers.json index 29747e0..a0c6335 100644 --- a/.agents/hooks/core/triggers.json +++ b/.agents/hooks/core/triggers.json @@ -3033,6 +3033,54 @@ "会议转录" ] } + }, + "oma-refactor": { + "keywords": { + "*": ["oma-refactor"], + "en": [ + "refactor this", + "refactoring plan", + "extract class", + "extract method", + "reduce complexity", + "measure complexity", + "code smell", + "technical debt", + "characterization test", + "hotspot analysis", + "split this class", + "clean up this code" + ], + "ko": [ + "리팩토링", + "리팩터링", + "복잡도 줄여", + "복잡도 측정", + "코드 스멜", + "기술 부채", + "클래스 분리", + "메서드 추출", + "코드 정리해줘" + ], + "ja": [ + "リファクタリング", + "複雑度を下げ", + "複雑度を測定", + "コードスメル", + "技術的負債", + "クラスを分割", + "メソッド抽出" + ], + "zh": [ + "重构", + "降低复杂度", + "测量复杂度", + "代码异味", + "技术债", + "拆分类", + "提取方法" + ] + } } }, "informationalPatterns": { diff --git a/.agents/hooks/variants/claude.json b/.agents/hooks/variants/claude.json index 901d5a9..b9a8a41 100644 --- a/.agents/hooks/variants/claude.json +++ b/.agents/hooks/variants/claude.json @@ -9,19 +9,19 @@ "UserPromptSubmit": [ { "hook": "keyword-detector.ts", - "timeout": 5 + "timeout": 2 }, { "hook": "state-boundary.ts", - "timeout": 5 + "timeout": 4 }, { "hook": "skill-injector.ts", - "timeout": 3 + "timeout": 2 }, { "hook": "serena-primer.ts", - "timeout": 3 + "timeout": 2 } ], "PreToolUse": { diff --git a/.agents/hooks/variants/opencode/oma.ts b/.agents/hooks/variants/opencode/oma.ts new file mode 100644 index 0000000..2e0e5c6 --- /dev/null +++ b/.agents/hooks/variants/opencode/oma.ts @@ -0,0 +1,211 @@ +/** + * oh-my-agent — opencode (Sst opencode) plugin bridge. + * + * SSOT source. At install time `installOpencodePlugin` copies this file to + * `.opencode/plugins/oma/oma.ts` alongside the core hook scripts. opencode + * auto-discovers plugins under each `.opencode/plugins` subdirectory. + * + * Why a bridge instead of a per-vendor variants JSON entry: opencode does NOT + * register settings-file hooks like the other vendors. It loads in-process + * TypeScript plugins and dispatches plugin event handlers. So rather than the + * generic `installHooksFromVariant` path (events → settings file → `bun + *