Skip to content

Commit 328660f

Browse files
Merge pull request #48 from first-fluke/chore/update-oh-my-agent
chore(deps): update oh-my-agent skills
2 parents 2c6fc33 + 7bd1602 commit 328660f

54 files changed

Lines changed: 3706 additions & 1251 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.agents/eval/oma-scholar/_rollouts/69cf97a7b7bfbc2f.json

Lines changed: 15 additions & 15 deletions
Large diffs are not rendered by default.

.agents/hooks/core/agentmemory-client.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,88 @@ export async function isAgentMemoryReachable(): Promise<boolean> {
134134
}
135135
}
136136

137+
export interface RecalledFact {
138+
text: string;
139+
source?: string;
140+
score?: number;
141+
}
142+
143+
interface SearchResult {
144+
score?: number;
145+
observation?: {
146+
narrative?: unknown;
147+
facts?: unknown;
148+
title?: unknown;
149+
type?: unknown;
150+
};
151+
}
152+
153+
function parseSearchResults(body: string, k: number): RecalledFact[] {
154+
let parsed: { results?: unknown };
155+
try {
156+
parsed = JSON.parse(body) as { results?: unknown };
157+
} catch {
158+
return [];
159+
}
160+
if (!Array.isArray(parsed.results)) return [];
161+
162+
const minScore = (() => {
163+
const raw = Number(process.env.OMA_RECALL_MIN_SCORE);
164+
return Number.isFinite(raw) ? raw : 1;
165+
})();
166+
167+
const facts: RecalledFact[] = [];
168+
for (const entry of parsed.results as SearchResult[]) {
169+
const score = typeof entry.score === "number" ? entry.score : 0;
170+
// Raw `/observe` envelopes score near-zero (~0.006); enriched facts score
171+
// in the single digits. Drop the noise floor so the snapshot stays useful.
172+
if (score < minScore) continue;
173+
const obs = entry.observation ?? {};
174+
const narrative =
175+
typeof obs.narrative === "string" && obs.narrative.trim()
176+
? obs.narrative.trim()
177+
: "";
178+
const factsText = Array.isArray(obs.facts)
179+
? obs.facts.filter((f): f is string => typeof f === "string").join("; ")
180+
: "";
181+
const title = typeof obs.title === "string" ? obs.title.trim() : "";
182+
const text = narrative || factsText || title;
183+
if (!text) continue;
184+
const source = typeof obs.type === "string" ? obs.type : undefined;
185+
facts.push({ text, source, score });
186+
if (facts.length >= k) break;
187+
}
188+
return facts;
189+
}
190+
191+
/**
192+
* Recall enriched facts from AgentMemory for boundary rehydration. Best-effort:
193+
* returns [] when the daemon is unreachable, on timeout (recall budget 2s per
194+
* design D34), or on any parse/transport error — never throws, never blocks L1.
195+
*/
196+
export async function recallFacts(
197+
query: string,
198+
k = 5,
199+
): Promise<RecalledFact[]> {
200+
if (!query.trim()) return [];
201+
if (!(await isAgentMemoryReachable())) return [];
202+
const url = endpointUrl();
203+
if (!url) return [];
204+
205+
try {
206+
const response = await requestAgentMemory(url, "/agentmemory/search", {
207+
method: "POST",
208+
headers: { "content-type": "application/json" },
209+
body: JSON.stringify({ query, limit: k }),
210+
timeoutMs: 2000,
211+
});
212+
if (response.statusCode < 200 || response.statusCode >= 300) return [];
213+
return parseSearchResults(response.body, k);
214+
} catch {
215+
return [];
216+
}
217+
}
218+
137219
export async function observeWithTimeout(payload: {
138220
sessionId: string;
139221
content: string;

.agents/hooks/core/keyword-detector.ts

Lines changed: 80 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,17 @@ import { VENDORS } from "./constants.ts";
3131
import { resolveGitRoot } from "./fs-utils.ts";
3232
import { clearGrokContext } from "./grok-context.ts";
3333
import { makePromptOutput } from "./hook-output.ts";
34-
import type { ModeState, Vendor } from "./types.ts";
34+
// triggers.json is imported statically: the bundler inlines it into the oma
35+
// binary (bundled `oma hook` path needs no file on disk), while a standalone
36+
// bun run resolves the sibling file next to this module (pi / direct run).
37+
import embeddedTriggers from "./triggers.json" with { type: "json" };
38+
import type {
39+
HandlerCtx,
40+
HandlerResult,
41+
HookInput,
42+
ModeState,
43+
Vendor,
44+
} from "./types.ts";
3545

3646
// ── Unicode normalization ─────────────────────────────────────
3747

@@ -377,9 +387,9 @@ interface TriggerConfig {
377387
extensionRouting?: Record<string, string[]>;
378388
}
379389

390+
/** Load the triggers config from the embedded (bundler-inlined / sibling-resolved) JSON. */
380391
function loadConfig(): TriggerConfig {
381-
const configPath = join(import.meta.dirname, "triggers.json");
382-
return JSON.parse(readFileSync(configPath, "utf-8"));
392+
return structuredClone(embeddedTriggers) as TriggerConfig;
383393
}
384394

385395
function detectLanguage(projectDir: string): string {
@@ -742,37 +752,32 @@ export function deactivateAllPersistentModes(
742752
}
743753
}
744754

745-
// ── Main ──────────────────────────────────────────────────────
755+
// ── Pure handler (canonical ABI) ─────────────────────────────
746756

747-
async function main() {
748-
const raw = readFileSync(0, "utf-8");
749-
let input: Record<string, unknown>;
750-
try {
751-
input = JSON.parse(raw);
752-
} catch {
753-
process.exit(0);
754-
}
755-
756-
// Guard 1: Only process genuine user prompts — skip agent-generated content
757-
if (!isGenuineUserPrompt(input)) process.exit(0);
758-
759-
const vendor = detectVendor(input);
760-
const projectDir = getProjectDir(vendor, input);
761-
const sessionId = getSessionId(input);
762-
let prompt = (input.prompt as string) ?? "";
757+
/**
758+
* Pure decision function — the single logic source for keyword detection.
759+
*
760+
* Called in-process by `oma hook` dispatch (Task 3+) and by the standalone
761+
* `main()` entry below (pi subprocess path). Both paths share exactly this
762+
* code; no business logic is duplicated.
763+
*
764+
* Returns a `context` HandlerResult when a workflow keyword matches, or
765+
* `null` when no match / early-exit condition (no stdout side-effect here).
766+
*
767+
* NOTE: `ctx.cwd` is expected to be the resolved git-root project directory,
768+
* as computed by `getProjectDir()` in the standalone path.
769+
*/
770+
export async function run(
771+
input: HookInput,
772+
ctx: HandlerCtx,
773+
): Promise<HandlerResult | null> {
774+
if (input.kind !== "prompt") return null;
763775

764-
// agy's PreInvocation stdin carries no `prompt` — recover the user request
765-
// from the transcript. PreInvocation fires before every model call, so only
766-
// act on the first invocation of a turn (invocationNum) to avoid re-running
767-
// keyword detection mid-turn.
768-
if (vendor === "antigravity" && !prompt) {
769-
const invocationNum = input.invocationNum;
770-
if (typeof invocationNum === "number" && invocationNum > 1) process.exit(0);
771-
prompt = readAgyPrompt(input.transcriptPath);
772-
}
776+
const { prompt } = input;
777+
const { vendor, cwd: projectDir, sid: sessionId = "unknown" } = ctx;
773778

774-
if (!prompt.trim()) process.exit(0);
775-
if (startsWithSlashCommand(prompt)) process.exit(0);
779+
if (!prompt.trim()) return null;
780+
if (startsWithSlashCommand(prompt)) return null;
776781

777782
const config = loadConfig();
778783
const lang = detectLanguage(projectDir);
@@ -782,14 +787,12 @@ async function main() {
782787
deactivateAllPersistentModes(projectDir, sessionId);
783788
// Grok's resume context lives in a session-start file, not L1 stdout — clear it.
784789
if (vendor === "grok") clearGrokContext(projectDir);
785-
process.exit(0);
790+
return null;
786791
}
792+
787793
const infoPatterns = buildInformationalPatterns(config, lang);
788794
// Guard 2: Strip code blocks, inline code, and pasted system-echo blocks
789-
// before scanning for keywords. System echo stripping prevents oma's own
790-
// hook outputs (when pasted back into the prompt) from re-triggering.
791-
// NFKC normalization collapses fullwidth Latin from CJK IMEs onto ASCII
792-
// so keyword regexes cannot be silently bypassed by parallel-style input.
795+
// before scanning for keywords. NFKC normalization collapses fullwidth Latin.
793796
const cleaned = normalizeForMatching(
794797
stripSystemEchoes(stripCodeBlocks(prompt)),
795798
);
@@ -803,18 +806,11 @@ async function main() {
803806

804807
for (const [workflow, def] of Object.entries(config.workflows)) {
805808
if (excluded.has(workflow)) continue;
806-
807-
// Global CLI-invocation guard: prompts that start with a CLI invocation
808-
// of `oma` or a `VENDORS` entry are tool invocations, not natural-language
809-
// workflow requests. Skip silently to avoid false-positive matches.
810809
if (shouldSkipAllWorkflows(cleaned)) continue;
811810

812-
// Per-workflow override: if a predicate is registered for this specific
813-
// workflow, evaluate it and skip just this workflow when it returns true.
814811
const workflowPredicate = KEYWORD_SKIP_PREDICATES[workflow];
815812
if (workflowPredicate?.(cleaned)) continue;
816813

817-
// Analytical questions should never trigger persistent workflows
818814
if (analytical && def.persistent) continue;
819815

820816
const patterns = [
@@ -826,18 +822,14 @@ async function main() {
826822
const match = pattern.exec(cleaned);
827823
if (!match) continue;
828824
if (isInformationalContext(cleaned, match.index, infoPatterns)) continue;
829-
// Keywords deep in long prompts are likely pasted content, not user intent
830825
if (isPastedContent(match.index, def.persistent, cleaned.length))
831826
continue;
832-
833-
// Guard 3: Suppress if same workflow triggered too many times in 60s
834827
if (isReinforcementSuppressed(kwState, workflow)) continue;
835828

836829
if (def.persistent) {
837830
activateMode(projectDir, workflow, sessionId);
838831
}
839832
await activateL1WorkflowSession(projectDir, workflow, vendor, sessionId);
840-
// Record this trigger for reinforcement tracking
841833
const updatedState = recordKwTrigger(kwState, workflow);
842834
saveKwState(projectDir, updatedState);
843835

@@ -860,13 +852,50 @@ async function main() {
860852
}
861853
}
862854

863-
const context = contextLines.join("\n");
864-
865-
process.stdout.write(makePromptOutput(vendor, context));
866-
process.exit(0);
855+
return { type: "context", additionalContext: contextLines.join("\n") };
867856
}
868857
}
869858

859+
return null;
860+
}
861+
862+
// ── Standalone entry (pi subprocess / direct bun invocation) ──
863+
864+
async function main() {
865+
const raw = readFileSync(0, "utf-8");
866+
let input: Record<string, unknown>;
867+
try {
868+
input = JSON.parse(raw);
869+
} catch {
870+
process.exit(0);
871+
}
872+
873+
// Guard 1: Only process genuine user prompts — skip agent-generated content
874+
if (!isGenuineUserPrompt(input)) process.exit(0);
875+
876+
const vendor = detectVendor(input);
877+
const projectDir = getProjectDir(vendor, input);
878+
const sessionId = getSessionId(input);
879+
let prompt = (input.prompt as string) ?? "";
880+
881+
// agy's PreInvocation stdin carries no `prompt` — recover the user request
882+
// from the transcript. PreInvocation fires before every model call, so only
883+
// act on the first invocation of a turn (invocationNum) to avoid re-running
884+
// keyword detection mid-turn.
885+
if (vendor === "antigravity" && !prompt) {
886+
const invocationNum = input.invocationNum;
887+
if (typeof invocationNum === "number" && invocationNum > 1) process.exit(0);
888+
prompt = readAgyPrompt(input.transcriptPath);
889+
}
890+
891+
// Build canonical inputs and delegate to run() — single logic source.
892+
const hookInput: HookInput = { kind: "prompt", prompt, cwd: projectDir };
893+
const ctx: HandlerCtx = { vendor, cwd: projectDir, sid: sessionId };
894+
895+
const result = await run(hookInput, ctx);
896+
if (result && result.type === "context") {
897+
process.stdout.write(makePromptOutput(vendor, result.additionalContext));
898+
}
870899
process.exit(0);
871900
}
872901

0 commit comments

Comments
 (0)