mirror of
https://github.com/Yeachan-Heo/oh-my-claudecode.git
synced 2026-10-19 01:39:52 -05:00
feat(ask): add Muse and fix Grok role prompt transport
This commit is contained in:
1 parent
7eb8f08fc1
commit
859b146c74
9 files changed
+237
-52
No files matched your search
+6
-3
@@ -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 |
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user