feat(ask): add Muse and fix Grok role prompt transport

This commit is contained in:
Abdullah Siddique committed 2026-10-13 06:26:43 -07:00
1 parent 7eb8f08fc1
commit 859b146c74
9 files changed
+237 -52

No files matched your search

+6 -3
View File
@@ -589,14 +589,16 @@ omc ask codex "review this patch from a security perspective"
omc ask gemini --prompt "suggest UX improvements"
omc ask antigravity --prompt "suggest UX improvements"
omc ask cursor --prompt "apply this implementation plan"
omc ask muse --agent-prompt critic --prompt "review this plan"
omc ask claude --agent-prompt executor --prompt "create an implementation plan"
```
- Provider matrix: `claude | codex | gemini | antigravity | grok | cursor`
- Provider matrix: `claude | codex | gemini | antigravity | grok | cursor | muse`
- Artifacts: `.omc/artifacts/ask/{provider}-{slug}-{timestamp}.md`
- Canonical env vars: `OMC_ASK_ADVISOR_SCRIPT`, `OMC_ASK_ORIGINAL_TASK`
- Phase-1 aliases (deprecated warning): `OMX_ASK_ADVISOR_SCRIPT`, `OMX_ASK_ORIGINAL_TASK`
- Skill entrypoint: `/oh-my-claudecode:ask <claude|codex|gemini|antigravity|grok|cursor> <prompt>` routes to this command
- Skill entrypoint: `/oh-my-claudecode:ask <claude|codex|gemini|antigravity|grok|cursor|muse> <prompt>` routes to this command
- Muse and Grok require CLI versions supporting `--prompt-file`; role prompts are passed through a private temporary file. Muse disables approval prompts while preserving its configured sandbox behavior.
### `omc team` (CLI runtime surface)
@@ -707,6 +709,7 @@ Use OMC's terminal and library surfaces in non-interactive environments:
- Run CLI commands that have deterministic exit codes, for example `omc setup`, `omc ask ...`, `omc session search ... --json`, or repo-owned verification scripts such as `npm run sync-metadata:verify`.
- Provide authentication through runner environment variables (`ANTHROPIC_API_KEY`) or pre-authenticated provider CLIs for `codex`, `gemini`, `antigravity`, `grok`, or `cursor` when using `omc ask` / `omc team`.
- For `omc ask muse`, install and authenticate the Muse CLI before running automation.
- Keep state explicit for ephemeral runners by setting `OMC_STATE_DIR` when state must survive worktree deletion or checkout replacement.
- Avoid interactive slash skills (`/oh-my-claudecode:autopilot`, `/oh-my-claudecode:ralph`, `/oh-my-claudecode:execute`, `/oh-my-claudecode:deep-interview`, `/oh-my-claudecode:team`) in CI jobs; they require an active Claude Code session and user-visible conversation loop.
- OMC does not currently provide a VS Code extension or VS Code-specific automation contract. The documented IDE path is to use Claude Code's own integrations, then install OMC through the Claude Code plugin surface.
@@ -1014,7 +1017,7 @@ Most installed skills are exposed as `/oh-my-claudecode:<registered-name>`. The
| -------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| `/oh-my-claudecode:ai-slop-cleaner <target>` | Run the anti-slop cleanup workflow (`--review` for reviewer-only pass) |
| `/oh-my-claudecode:agent-doc-discipline` | Apply the writing-time discipline for agent-facing documents |
| `/oh-my-claudecode:ask <claude\|codex\|gemini\|antigravity\|grok\|cursor> <prompt>` | Route a prompt through the selected advisor CLI and capture an ask artifact |
| `/oh-my-claudecode:ask <claude\|codex\|gemini\|antigravity\|grok\|cursor\|muse> <prompt>` | Route a prompt through the selected advisor CLI and capture an ask artifact |
| `/oh-my-claudecode:architecture-survey [area]` | Survey the repo for architecture-deepening candidates with evidence; reports only, never edits |
| `/oh-my-claudecode:ask-navigator <idea\|map>` | Chart a foggy effort into a map of decision tickets (or work the open map), then hand off to launch |
| `/oh-my-claudecode:autopilot <task>` | Full autonomous execution |
+1 -1
View File
@@ -207,7 +207,7 @@ function isExplicitRalplanSlashInvocation(prompt) {
}
function isExplicitAskSlashInvocation(prompt) {
return /^\s*\/(?:oh-my-claudecode:)?ask\s+(?:claude|codex|gemini|antigravity|agy|grok|cursor)\b/i.test(prompt);
return /^\s*\/(?:oh-my-claudecode:)?ask\s+(?:claude|codex|gemini|antigravity|agy|grok|cursor|muse)\b/i.test(prompt);
}
function isRetiredSlashInvocation(prompt) {
+57 -24
View File
@@ -1,6 +1,8 @@
#!/usr/bin/env node
import { spawnSync } from 'child_process';
import { mkdtempSync, rmSync, writeFileSync } from 'fs';
import { mkdir, writeFile } from 'fs/promises';
import { tmpdir } from 'os';
import { join } from 'path';
import process from 'process';
import { resolveOmcStateRoot } from './lib/state-root.mjs';
@@ -12,7 +14,15 @@ const PROVIDER_BINARIES = {
antigravity: 'agy',
grok: 'grok',
cursor: 'cursor-agent',
muse: 'muse',
};
// muse and grok parse a prompt value that begins with `-` (e.g. the YAML
// frontmatter `---` that every `--agent-prompt` role file starts with) as an
// option and abort before calling the model, and grok reserves stdin for ACP
// JSON-RPC. Both accept `--prompt-file`, so hand them the prompt in a private
// temp file instead of argv or stdin.
const PROMPT_FILE_PROVIDERS = new Set(['muse', 'grok']);
const SHOULD_USE_WINDOWS_SHELL = process.platform === 'win32';
// Antigravity (`agy`) headless print mode has a known upstream non-TTY bug
@@ -46,12 +56,14 @@ const ANTIGRAVITY_TIMEOUT_MS = (() => {
* - gemini: `gemini -p <prompt> --yolo`
* - antigravity: `agy --dangerously-skip-permissions -p <prompt>` (Antigravity CLI;
* `-p` takes the prompt as its value and cannot read it from stdin, so the
* prompt is always passed as an arg with approval flags first, like grok)
* - grok: `grok -p <prompt> --always-approve` (headless mode takes the prompt
* as an arg; grok's stdin is reserved for ACP JSON-RPC, never the prompt)
* prompt is always passed as an arg with approval flags first)
* - grok: `grok --prompt-file <file> --always-approve` (single-turn headless
* mode; grok's stdin is reserved for ACP JSON-RPC, never the prompt)
* - cursor: `cursor-agent --print --force --trust --sandbox disabled <prompt>`
* - muse: `muse exec --prompt-file <file> --approval-mode never --user-input-auto-resolve`
* (headless; preserves Muse's configured sandbox behavior)
*/
function buildProviderArgs(provider, prompt, { pipePromptViaStdin = false } = {}) {
function buildProviderArgs(provider, prompt, { pipePromptViaStdin = false, promptFile } = {}) {
if (provider === 'codex') {
return ['exec', '--dangerously-bypass-approvals-and-sandbox', pipePromptViaStdin ? '-' : prompt];
}
@@ -62,14 +74,17 @@ function buildProviderArgs(provider, prompt, { pipePromptViaStdin = false } = {}
// Antigravity (`agy`): `-p`/`--print` takes the prompt as its VALUE (next
// token), not as a boolean, and it does NOT read the prompt from stdin
// (`agy -p` with no value errors "flag needs an argument"). So always pass
// the prompt as the `-p` value with approval flags first, like grok.
// the prompt as the `-p` value with approval flags first.
// Verified on agy 1.0.10.
return ['--dangerously-skip-permissions', '-p', prompt];
}
if (provider === 'grok') {
// Grok's headless mode always takes the prompt as a `-p` arg; its stdin is
// for ACP JSON-RPC, not the prompt, so it never uses the stdin pipe path.
return ['-p', prompt, '--always-approve'];
// Grok's stdin is for ACP JSON-RPC, not the prompt, and `-p <prompt>` rejects
// a dash-leading prompt, so the prompt always arrives via --prompt-file.
return ['--prompt-file', promptFile, '--always-approve'];
}
if (provider === 'muse') {
return ['exec', '--prompt-file', promptFile, '--approval-mode', 'never', '--user-input-auto-resolve'];
}
if (provider === 'cursor') {
// Cursor Agent's print mode takes the prompt as a positional arg. Keep stdin
@@ -106,8 +121,8 @@ function shouldPipePromptViaStdin(provider, prompt) {
return prompt.includes('\n') || prompt.length > 500 || /^\s*-/.test(prompt);
}
// grok (ACP stdin), cursor-agent (interactive stdin), and any other provider
// never pipe the prompt.
// grok and muse (prompt file), cursor-agent (interactive stdin), and any other
// provider never pipe the prompt.
return false;
}
@@ -115,8 +130,8 @@ const ASK_ORIGINAL_TASK_ENV = 'OMC_ASK_ORIGINAL_TASK';
const ASK_ORIGINAL_TASK_ENV_ALIAS = 'OMX_ASK_ORIGINAL_TASK';
function usage() {
console.error('Usage: omc ask <claude|codex|gemini|antigravity|grok|cursor> "<prompt>"');
console.error('Legacy direct usage: node scripts/run-provider-advisor.js <claude|codex|gemini|antigravity|grok|cursor> <prompt...>');
console.error('Usage: omc ask <claude|codex|gemini|antigravity|grok|cursor|muse> "<prompt>"');
console.error('Legacy direct usage: node scripts/run-provider-advisor.js <claude|codex|gemini|antigravity|grok|cursor|muse> <prompt...>');
console.error(' or: node scripts/run-provider-advisor.js claude --print "<prompt>"');
console.error(' or: node scripts/run-provider-advisor.js gemini --prompt "<prompt>"');
}
@@ -311,22 +326,40 @@ async function main() {
ensureBinary(provider, binary);
const pipePromptViaStdin = shouldPipePromptViaStdin(provider, prompt);
const providerArgs = buildProviderArgs(provider, prompt, { pipePromptViaStdin });
// Antigravity on Windows: spawn without a shell to avoid cmd.exe prompt parsing.
// The `-p <prompt>` format is safe as a direct argv value when spawned without
// shell preprocessing (agy is a real executable, not a shell function or alias).
const useShell = provider === 'antigravity' ? false : SHOULD_USE_WINDOWS_SHELL;
const run = spawnSync(binary, providerArgs, {
encoding: 'utf8',
maxBuffer: 10 * 1024 * 1024,
env: buildProviderEnv(provider),
shell: useShell,
// Bound antigravity so an upstream non-TTY hang (#76) fails cleanly instead of
// blocking forever; agy's own --print-timeout does not work. SIGKILL (not a
// catchable SIGTERM) guarantees spawnSync returns even if agy traps signals.
...(provider === 'antigravity' ? { timeout: ANTIGRAVITY_TIMEOUT_MS, killSignal: ANTIGRAVITY_TIMEOUT_KILL_SIGNAL } : {}),
...(pipePromptViaStdin ? { input: prompt } : { stdio: ['ignore', 'pipe', 'pipe'] }),
});
const promptDir = PROMPT_FILE_PROVIDERS.has(provider) ? mkdtempSync(join(tmpdir(), 'omc-ask-')) : null;
let run;
try {
let promptFile;
if (promptDir) {
promptFile = join(promptDir, 'prompt.md');
writeFileSync(promptFile, prompt, { encoding: 'utf8', mode: 0o600 });
// A shell spawn joins argv with spaces unquoted, so quote the path in case
// the temp dir contains spaces (e.g. a Windows profile like "John Smith").
if (useShell) {
promptFile = `"${promptFile}"`;
}
}
const providerArgs = buildProviderArgs(provider, prompt, { pipePromptViaStdin, promptFile });
run = spawnSync(binary, providerArgs, {
encoding: 'utf8',
maxBuffer: 10 * 1024 * 1024,
env: buildProviderEnv(provider),
shell: useShell,
// Bound antigravity so an upstream non-TTY hang (#76) fails cleanly instead of
// blocking forever; agy's own --print-timeout does not work. SIGKILL (not a
// catchable SIGTERM) guarantees spawnSync returns even if agy traps signals.
...(provider === 'antigravity' ? { timeout: ANTIGRAVITY_TIMEOUT_MS, killSignal: ANTIGRAVITY_TIMEOUT_KILL_SIGNAL } : {}),
...(pipePromptViaStdin ? { input: prompt } : { stdio: ['ignore', 'pipe', 'pipe'] }),
});
} finally {
if (promptDir) {
rmSync(promptDir, { recursive: true, force: true });
}
}
const stdout = run.stdout || '';
const stderr = run.stderr || '';
+7 -4
View File
@@ -1,16 +1,16 @@
---
name: ask
description: Process-first advisor routing for Claude, Codex, Gemini, Antigravity, Grok, or Cursor via `omc ask`, with artifact capture and no raw CLI assembly
description: Process-first advisor routing for Claude, Codex, Gemini, Antigravity, Grok, Cursor, or Muse via `omc ask`, with artifact capture and no raw CLI assembly
---
# Ask
Use OMC's canonical advisor skill to route a prompt through the local Claude, Codex, Gemini, Antigravity, Grok, or Cursor CLI and persist the result as an ask artifact.
Use OMC's canonical advisor skill to route a prompt through the local Claude, Codex, Gemini, Antigravity, Grok, Cursor, or Muse CLI and persist the result as an ask artifact.
## Usage
```bash
/oh-my-claudecode:ask <claude|codex|gemini|antigravity|grok|cursor> <question or task>
/oh-my-claudecode:ask <claude|codex|gemini|antigravity|grok|cursor|muse> <question or task>
```
Examples:
@@ -21,6 +21,7 @@ Examples:
/oh-my-claudecode:ask antigravity "suggest UX improvements for this flow"
/oh-my-claudecode:ask claude "draft an implementation plan for issue #123"
/oh-my-claudecode:ask cursor "apply this implementation plan"
/oh-my-claudecode:ask muse --agent-prompt critic "review this plan"
```
## Routing
@@ -31,7 +32,7 @@ Examples:
omc ask {{ARGUMENTS}}
```
**Do NOT manually construct raw provider CLI commands.** Never run `codex`, `claude`, `gemini`, `agy`, `grok`, or `cursor-agent` directly to fulfill this skill. The `omc ask` wrapper handles correct flag selection, artifact persistence, and provider-version compatibility automatically. Manually assembling provider CLI flags will produce incorrect or outdated invocations.
**Do NOT manually construct raw provider CLI commands.** Never run `codex`, `claude`, `gemini`, `agy`, `grok`, `cursor-agent`, or `muse` directly to fulfill this skill. The `omc ask` wrapper handles correct flag selection, artifact persistence, and provider-version compatibility automatically. Manually assembling provider CLI flags will produce incorrect or outdated invocations.
## Requirements
@@ -45,6 +46,7 @@ gemini --version
agy --version
grok --version
cursor-agent --version
muse --version
```
- **Antigravity CLI install** (Google's successor to the Gemini CLI): install the `agy`
@@ -52,6 +54,7 @@ cursor-agent --version
any installer before running it). Verify: `agy --version`
> **Platform note:** `omc ask antigravity` is supported on macOS/Linux. On Windows it is guarded with a clear error, because `agy --print` takes the prompt as an argv value (it cannot read stdin) and has known upstream Windows `-p` limitations; use `omc ask gemini` on Windows.
- **Gemini CLI** remains supported for enterprise/API-key use cases.
- **Muse and Grok** require versions supporting `--prompt-file`. The wrapper uses private temporary files for prompts, including role files with YAML frontmatter. Muse runs with approvals disabled and preserves its configured sandbox behavior.
## Artifacts
@@ -496,6 +496,22 @@ describe('keyword-detector.mjs mode-message dispatch', () => {
}
});
it.each(['/ask muse', '/oh-my-claudecode:ask muse'])('does not activate modes from a delegated %s payload', (command) => {
const tempDir = mkdtempSync(join(tmpdir(), 'keyword-detector-ask-muse-'));
try {
const sessionId = 'ask-muse-session';
const output = runKeywordDetector(`${command} ralplan ralph ultrawork autopilot review`, tempDir, sessionId);
expect(output.continue).toBe(true);
expect(output.suppressOutput).toBe(true);
expect(output.hookSpecificOutput).toBeUndefined();
for (const mode of ['ralplan', 'ralph', 'ultrawork', 'autopilot']) {
expect(existsSync(join(tempDir, '.omc', 'state', 'sessions', sessionId, `${mode}-state.json`))).toBe(false);
}
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
});
it('does not activate ralplan from a delegated /ask codex payload', () => {
const tempDir = mkdtempSync(join(tmpdir(), 'keyword-detector-ask-codex-'));
+123 -12
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs';
import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'fs';
import { mkdtempSync } from 'fs';
import { join, dirname } from 'path';
import { tmpdir } from 'os';
@@ -282,7 +282,115 @@ exit 9
return binDir;
}
describe('private provider prompt files', () => {
it.each([
['muse', 0], ['muse', 7], ['grok', 0], ['grok', 7],
] as const)('delivers UTF-8 frontmatter to %s and preserves exit %i', (provider, exitCode) => {
// Given a real executable that consumes the provider prompt-file contract.
const wd = mkdtempSync(join(tmpdir(), 'omc-ask-file-child-'));
try {
const bin = join(wd, provider);
const capture = join(wd, 'received.json');
const prompt = '---\nname: critic\n---\n한국어 café & | > "quoted" $HOME `echo unsafe`';
writeFileSync(bin, `#!${process.execPath}
const fs = require('node:fs');
const path = require('node:path');
const args = process.argv.slice(2);
if (args[0] === '--version') process.exit(0);
const file = args[args.indexOf('--prompt-file') + 1];
const contents = fs.readFileSync(file, 'utf8');
fs.writeFileSync(process.env.ASK_CAPTURE, JSON.stringify({
args, file, contents, mode: fs.statSync(file).mode & 0o777,
dirMode: fs.statSync(path.dirname(file)).mode & 0o777,
stdin: fs.readFileSync(0, 'utf8'),
}));
process.stdout.write('CHILD_OUTPUT');
process.stderr.write('CHILD_DIAGNOSTIC');
process.exit(Number(process.env.ASK_EXIT));
`, { mode: 0o755 });
// When the real advisor launches the executable.
const result = runAdvisorScript([provider, '--prompt', prompt], wd, {
PATH: `${wd}${process.platform === 'win32' ? ';' : ':'}${process.env.PATH}`,
ASK_CAPTURE: capture, ASK_EXIT: String(exitCode), TMPDIR: wd,
OMC_STATE_DIR: join(wd, 'state'),
});
// Then the child sees exact bytes, private permissions, and closed stdin;
// the advisor keeps its status/output artifact after removing the file.
expect(result.status, result.stderr).toBe(exitCode);
const received = JSON.parse(readFileSync(capture, 'utf8'));
expect(received.contents).toBe(prompt);
expect(received.stdin).toBe('');
expect(received.mode).toBe(0o600);
expect(received.dirMode).toBe(0o700);
expect(received.args).toEqual(provider === 'muse'
? ['exec', '--prompt-file', received.file, '--approval-mode', 'never', '--user-input-auto-resolve']
: ['--prompt-file', received.file, '--always-approve']);
expect(existsSync(dirname(received.file))).toBe(false);
const artifact = readFileSync(result.stdout.trim(), 'utf8');
expect(artifact).toContain(`- Exit code: ${exitCode}`);
expect(artifact).toContain(prompt);
expect(artifact).toContain('CHILD_OUTPUT\n\nCHILD_DIAGNOSTIC');
} finally {
rmSync(wd, { recursive: true, force: true });
}
});
it.each(['muse', 'grok'])('cleans up and records failure when the %s executable disappears after probing', (provider) => {
const wd = mkdtempSync(join(tmpdir(), 'omc-ask-file-enoent-'));
try {
const bin = join(wd, provider);
writeFileSync(bin, `#!${process.execPath}\nrequire('node:fs').unlinkSync(__filename);\n`, { mode: 0o755 });
const result = runAdvisorScript([provider, '---\nrole: critic'], wd, {
PATH: wd, TMPDIR: wd, OMC_STATE_DIR: join(wd, 'state'),
});
expect(result.status).toBe(1);
expect(result.stderr).toContain('ENOENT');
expect(readFileSync(result.stdout.trim(), 'utf8')).toContain('- Exit code: 1');
expect(readdirSync(wd).filter((name) => name.startsWith('omc-ask-'))).toEqual([]);
} finally {
rmSync(wd, { recursive: true, force: true });
}
});
it.each(['write', 'spawn'])('cleans up when prompt %s throws', (failure) => {
const wd = mkdtempSync(join(tmpdir(), 'omc-ask-file-throw-'));
try {
const prelude = join(wd, 'throw.mjs');
writeFileSync(prelude, `
import fs from 'node:fs';
import cp from 'node:child_process';
import { syncBuiltinESMExports } from 'node:module';
const originalWrite = fs.writeFileSync;
fs.writeFileSync = (file, ...args) => {
if (${JSON.stringify(failure)} === 'write' && String(file).endsWith('prompt.md')) throw new Error('WRITE_FAILED');
return originalWrite(file, ...args);
};
cp.spawnSync = (_command, args) => {
if (args[0] === '--version') return { status: 0 };
throw new Error('SPAWN_FAILED');
};
syncBuiltinESMExports();
`);
const result = runAdvisorScriptWithPrelude(prelude, ['grok', '---\nrole: critic'], wd, {
TMPDIR: wd, OMC_STATE_DIR: join(wd, 'state'),
});
expect(result.status).toBe(1);
expect(result.stderr).toContain(`${failure.toUpperCase()}_FAILED`);
expect(readdirSync(wd).filter((name) => name.startsWith('omc-ask-'))).toEqual([]);
} finally {
rmSync(wd, { recursive: true, force: true });
}
});
});
describe('parseAskArgs', () => {
it('accepts Muse with a role prompt', () => {
expect(parseAskArgs(['muse', '--agent-prompt', 'critic', '--prompt', 'review'])).toEqual({
provider: 'muse', agentPromptRole: 'critic', prompt: 'review',
});
});
it('supports positional and print/prompt flag forms', () => {
expect(parseAskArgs(['claude', 'review', 'this'])).toEqual({ provider: 'claude', prompt: 'review this' });
expect(parseAskArgs(['gemini', '-p', 'brainstorm'])).toEqual({ provider: 'gemini', prompt: 'brainstorm' });
@@ -574,6 +682,7 @@ describe('run-provider-advisor script contract', () => {
['antigravity', ['antigravity', '--prompt', 'nested antigravity prompt']],
['grok', ['grok', '--prompt', 'nested grok prompt']],
['cursor', ['cursor', '--prompt', 'nested cursor prompt']],
['muse', ['muse', '--prompt', 'nested muse prompt']],
] as const)('strips Claude session env vars for %s advisor spawns', (provider, args) => {
const wd = mkdtempSync(join(tmpdir(), `omc-ask-${provider}-advisor-env-`));
try {
@@ -622,18 +731,16 @@ describe('run-provider-advisor script contract', () => {
}
});
it('launches grok as `grok -p <prompt> --always-approve` and never pipes stdin', () => {
const wd = mkdtempSync(join(tmpdir(), 'omc-ask-grok-args-'));
it.each(['grok', 'muse'])('quotes the %s prompt-file path for Windows shell launches', (provider) => {
const wd = mkdtempSync(join(tmpdir(), 'omc ask file args '));
try {
const capturePath = join(wd, 'spawn-sync-calls.json');
const preludePath = writeSpawnSyncCapturePrelude(wd);
// A multiline prompt is piped over stdin for codex/gemini; grok reserves stdin
// for ACP JSON-RPC, so it must take the prompt as a `-p` arg instead.
const result = runAdvisorScriptWithPrelude(
preludePath,
['grok', '--prompt', 'review this\nand that'],
[provider, '--prompt', '---\nrole: critic\n---\nreview this & that'],
wd,
{ SPAWN_CAPTURE_PATH: capturePath },
{ SPAWN_CAPTURE_PATH: capturePath, TMPDIR: wd, TEMP: wd, TMP: wd, OMC_STATE_DIR: join(wd, 'state') },
);
expect(result.error).toBeUndefined();
@@ -645,13 +752,17 @@ describe('run-provider-advisor script contract', () => {
options: { input: string | null };
}>;
// version probe + launch, both via the `grok` binary
expect(calls).toHaveLength(2);
const launch = calls.find((c) => !c.args.includes('--version'));
expect(launch).toBeDefined();
expect(launch!.command).toBe('grok');
expect(launch!.args).toEqual(['-p', 'review this\nand that', '--always-approve']);
expect(launch!.options.input ?? null).toBeNull();
if (!launch) throw new Error('Missing provider launch');
expect(launch.command).toBe(provider);
const promptFile = launch.args[launch.args.indexOf('--prompt-file') + 1];
expect(promptFile).toMatch(/^".*omc ask file args .*prompt\.md"$/);
expect(launch.args).toEqual(provider === 'muse'
? ['exec', '--prompt-file', promptFile, '--approval-mode', 'never', '--user-input-auto-resolve']
: ['--prompt-file', promptFile, '--always-approve']);
expect(launch.options.input ?? null).toBeNull();
expect(existsSync(promptFile.slice(1, -1))).toBe(false);
} finally {
rmSync(wd, { recursive: true, force: true });
}
+7 -7
View File
@@ -7,15 +7,15 @@ import { fileURLToPath } from 'url';
import { isExternalLLMDisabled } from '../lib/security-config.js';
export const ASK_USAGE = [
'Usage: omc ask <claude|codex|gemini|antigravity|grok|cursor> <question or task>',
' or: omc ask <claude|codex|gemini|antigravity|grok|cursor> -p "<prompt>"',
' or: omc ask <claude|codex|gemini|antigravity|grok|cursor> --print "<prompt>"',
' or: omc ask <claude|codex|gemini|antigravity|grok|cursor> --prompt "<prompt>"',
' or: omc ask <claude|codex|gemini|antigravity|grok|cursor> --agent-prompt <role> "<prompt>"',
' or: omc ask <claude|codex|gemini|antigravity|grok|cursor> --agent-prompt=<role> --prompt "<prompt>"',
'Usage: omc ask <claude|codex|gemini|antigravity|grok|cursor|muse> <question or task>',
' or: omc ask <claude|codex|gemini|antigravity|grok|cursor|muse> -p "<prompt>"',
' or: omc ask <claude|codex|gemini|antigravity|grok|cursor|muse> --print "<prompt>"',
' or: omc ask <claude|codex|gemini|antigravity|grok|cursor|muse> --prompt "<prompt>"',
' or: omc ask <claude|codex|gemini|antigravity|grok|cursor|muse> --agent-prompt <role> "<prompt>"',
' or: omc ask <claude|codex|gemini|antigravity|grok|cursor|muse> --agent-prompt=<role> --prompt "<prompt>"',
].join('\n');
const ASK_PROVIDERS = ['claude', 'codex', 'gemini', 'antigravity', 'grok', 'cursor'] as const;
const ASK_PROVIDERS = ['claude', 'codex', 'gemini', 'antigravity', 'grok', 'cursor', 'muse'] as const;
export type AskProvider = (typeof ASK_PROVIDERS)[number];
const ASK_PROVIDER_SET = new Set<string>(ASK_PROVIDERS);
@@ -1037,6 +1037,25 @@ $ ultrawork search the codebase`,
}
});
it.each(['/ask muse', '/oh-my-claudecode:ask muse'])('does not arm modes for keywords inside %s payloads', async (command) => {
const tempDir = mkdtempSync(join(tmpdir(), 'bridge-routing-ask-muse-'));
try {
execFileSync('git', ['init'], { cwd: tempDir, stdio: 'pipe' });
const sessionId = 'ask-muse-session';
const result = await processHook('keyword-detector', {
sessionId, directory: tempDir,
prompt: `${command} ralplan ralph ultrawork autopilot review`,
});
expect(result.continue).toBe(true);
expect(result.message).toBeUndefined();
for (const mode of ['ralplan', 'ralph', 'ultrawork', 'autopilot']) {
expect(existsSync(join(tempDir, '.omc', 'state', 'sessions', sessionId, `${mode}-state.json`))).toBe(false);
}
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
});
it('does not arm ralplan state for keywords inside delegated /ask grok prompts', async () => {
const tempDir = mkdtempSync(join(tmpdir(), 'bridge-routing-ask-grok-'));
try {
+1 -1
View File
@@ -1279,7 +1279,7 @@ function getPromptText(input: HookInput): string {
}
function isExplicitAskSlashInvocation(promptText: string): boolean {
return /^\s*\/(?:oh-my-claudecode:)?ask\s+(?:claude|codex|gemini|antigravity|agy|grok|cursor)\b/i.test(promptText);
return /^\s*\/(?:oh-my-claudecode:)?ask\s+(?:claude|codex|gemini|antigravity|agy|grok|cursor|muse)\b/i.test(promptText);
}
function activateRalplanStartupState(directory: string, sessionId?: string): void {