Add Grok orchestration group routing (#8058)

* docs: design Grok orchestration group

* docs: plan Grok orchestration group implementation

* fix: add Grok orchestration group

* test(orchestration): accept Windows skill newlines

* Fix @grok orchestration group matching and remove stale planning docs

- Reuse the shared buildAgentNameRe matcher in groups.ts instead of a
  divergent local regex, so orchestration groups honor the same
  Windows launcher-suffix rule (grok.exe/.cmd/.bat/.ps1) as the rest
  of Orca's agent-title detection.
- Add test coverage for real Grok OSC title shapes (spinner-collapsed,
  session titles) and Windows launcher-suffix titles.
- Delete the now-completed design and implementation-plan docs for
  the Grok orchestration group work.

---------

Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
This commit is contained in:
BingZ 2026-07-12 12:43:20 +08:00 committed by GitHub
parent 92ea918b63
commit be258e23ec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 109 additions and 63 deletions

View File

@ -11,7 +11,9 @@ function readSkill() {
function getSection(markdown, heading) {
const escapedHeading = heading.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
const match = markdown.match(new RegExp(`## ${escapedHeading}\\n([\\s\\S]*?)(?=\\n## |$)`))
const match = markdown.match(
new RegExp(`## ${escapedHeading}\\r?\\n([\\s\\S]*?)(?=\\r?\\n## |$)`)
)
expect(match).not.toBeNull()
@ -189,6 +191,13 @@ describe('orchestration skill guidance', () => {
expect(skill).not.toContain('every 2 minutes')
})
it('documents @grok in the Messaging group address list', () => {
const skill = readSkill()
const messaging = getSection(skill, 'Messaging')
expect(messaging).toContain('`@grok`')
})
it('keeps agent-first launch, handle recovery, and inbox injection distinct', () => {
const skill = readSkill()
const messaging = getSection(skill, 'Messaging')

View File

@ -95,7 +95,7 @@ Rules:
- Heartbeats and visible terminal activity mean the worker is alive, not done. Do not stop, close, kill, or restart a worker just because it has not produced a completion message yet.
- Use `ask` when a worker needs a blocking answer from the coordinator; it waits for the reply and returns the answer directly.
- `check --wait` returns one message at a time. If N workers may finish together, loop N times and dispatch newly ready tasks after each completion.
- Group addresses include `@all`, `@idle`, `@claude`, `@codex`, `@opencode`, `@gemini`, `@droid`, and `@worktree:<id>`.
- Group addresses include `@all`, `@idle`, `@claude`, `@codex`, `@opencode`, `@gemini`, `@droid`, `@grok`, and `@worktree:<id>`.
- Message types include `status`, `dispatch`, `worker_done`, `merge_ready`, `escalation`, `handoff`, `decision_gate`, and `heartbeat`.
- Use group addresses only for messages that are genuinely useful to many terminals, such as `status` broadcasts or intentional fan-out questions. Do not send dispatch lifecycle messages to groups.
- `worker_done` must target the concrete coordinator handle from the live preamble. It is completion authority for one dispatch; group fanout would create false lifecycle mail in unrelated terminals.

View File

@ -120,10 +120,7 @@ export class KimiSessionIndexCache {
}
}
function identitiesMatch(
left: KimiSessionIndexIdentity,
right: KimiSessionIndexIdentity
): boolean {
function identitiesMatch(left: KimiSessionIndexIdentity, right: KimiSessionIndexIdentity): boolean {
return (
left.changeTimeMs === right.changeTimeMs &&
left.mtimeMs === right.mtimeMs &&

View File

@ -114,7 +114,7 @@ describe('submodule path cache', () => {
expect(gitExecFileAsyncMock).toHaveBeenCalledTimes(2)
})
it('does not reuse another branch\'s submodule paths after local or WSL checkout', async () => {
it("does not reuse another branch's submodule paths after local or WSL checkout", async () => {
let modulePath = 'main-lib'
gitExecFileAsyncMock.mockImplementation((args: string[]) => {
if (args[0] === 'checkout') {
@ -219,9 +219,7 @@ describe('submodule path cache', () => {
...runtime,
checkoutExistingBranch: true
})
await expect(listSubmodulePaths('/repo-feature', runtime)).resolves.toEqual([
'recreated-lib'
])
await expect(listSubmodulePaths('/repo-feature', runtime)).resolves.toEqual(['recreated-lib'])
const configReads = gitExecFileAsyncMock.mock.calls.filter(
([args]) => args[0] === 'config' && args.includes('.gitmodules')

View File

@ -1,8 +1,5 @@
import { describe, expect, it } from 'vitest'
import {
DEFAULT_WARNING_DEDUPE_MAX_KEYS,
shouldEmitBoundedWarning
} from './bounded-warning-dedupe'
import { DEFAULT_WARNING_DEDUPE_MAX_KEYS, shouldEmitBoundedWarning } from './bounded-warning-dedupe'
describe('shouldEmitBoundedWarning', () => {
it('keeps retained warning keys quiet without cascade eviction after saturation', () => {
@ -29,8 +26,6 @@ describe('shouldEmitBoundedWarning', () => {
)
expect(keys.filter((key) => shouldEmitBoundedWarning(warningKeys, key))).toEqual(keys)
expect(keys.filter((key) => shouldEmitBoundedWarning(warningKeys, key))).toEqual([
keys.at(-1)
])
expect(keys.filter((key) => shouldEmitBoundedWarning(warningKeys, key))).toEqual([keys.at(-1)])
})
})

View File

@ -155,10 +155,7 @@ vi.mock('./ssh', () => ({
}))
import { registerRepoHandlers } from './repos'
import {
clearSubmodulePathsCacheForTests,
listSubmodulePaths
} from '../git/status'
import { clearSubmodulePathsCacheForTests, listSubmodulePaths } from '../git/status'
beforeEach(() => {
clearGitCapabilityStateForTests()

View File

@ -30,6 +30,7 @@ describe('isGroupAddress', () => {
expect(isGroupAddress('@idle')).toBe(true)
expect(isGroupAddress('@claude')).toBe(true)
expect(isGroupAddress('@droid')).toBe(true)
expect(isGroupAddress('@grok')).toBe(true)
expect(isGroupAddress('@worktree:wt_1')).toBe(true)
})
@ -169,6 +170,54 @@ describe('resolveGroupAddress', () => {
const result = resolveGroupAddress('@Claude', 'term_a', terminals, noStatus)
expect(result).toEqual(['term_b'])
})
it('matches @grok as a standalone title token and excludes sender', () => {
const terminals = [
makeSummary('term_a', { title: 'Grok' }),
makeSummary('term_b', { title: 'GROK CLI' }),
makeSummary('term_c', { title: '⠋ Grok' }),
makeSummary('term_d', { title: 'ngrok' }),
makeSummary('term_e', { title: '/tmp/grok' }),
makeSummary('term_f', { title: 'my-grok-worker' }),
makeSummary('term_g', { title: 'Codex CLI' })
]
const result = resolveGroupAddress('@GrOk', 'term_a', terminals, noStatus)
expect(result).toEqual(['term_b', 'term_c'])
})
// Why: the resolver sees the raw OSC title, and Grok CLI's real working/session
// titles carry a trailing " - grok" identity or a spinner-collapsed "⠋ grok"
// (see terminal-title-agent-type.ts). Prove those production shapes resolve.
it('matches real Grok OSC working and session titles', () => {
const terminals = [
makeSummary('coordinator', { title: 'Coordinator' }),
makeSummary('term_rotating', { title: '⠋ - fix the flaky suite - grok' }),
makeSummary('term_collapsed', { title: '⠋ grok' }),
makeSummary('term_session', { title: 'Fix the auth bug - grok' })
]
const result = resolveGroupAddress('@grok', 'coordinator', terminals, noStatus)
expect(result).toEqual(['term_rotating', 'term_collapsed', 'term_session'])
})
// Why: Windows agent titles can surface the launcher process name (`grok.exe`);
// the shared matcher accepts .exe/.cmd/.bat/.ps1 suffixes but still rejects
// arbitrary dotted fragments like `grok.py`.
it('matches Windows launcher-suffix titles but not arbitrary dotted tokens', () => {
const terminals = [
makeSummary('coordinator', { title: 'Coordinator' }),
makeSummary('term_exe', { title: 'grok.exe' }),
makeSummary('term_cmd', { title: 'grok.cmd running' }),
makeSummary('term_dotted', { title: 'grok.py' })
]
const result = resolveGroupAddress('@grok', 'coordinator', terminals, noStatus)
expect(result).toEqual(['term_exe', 'term_cmd'])
})
})
describe('unknown groups', () => {

View File

@ -1,3 +1,4 @@
import { buildAgentNameRe } from '../../../shared/agent-name-token-match'
import type { RuntimeTerminalSummary } from '../../../shared/runtime-types'
// Why: group addresses enable broadcast messaging to logical groups of agents.
@ -11,7 +12,8 @@ const AGENT_NAME_GROUPS = [
'opencode',
'mimo',
'gemini',
'droid'
'droid',
'grok'
] as const
export type GroupAddress =
@ -24,13 +26,11 @@ export function isGroupAddress(to: string): boolean {
return to.startsWith('@')
}
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}
function titleMatchesAgentNameGroup(title: string, agentName: string): boolean {
const tokenRe = new RegExp(`(?<![\\w./\\\\-])${escapeRegExp(agentName)}(?![\\w./\\\\-])`, 'i')
return tokenRe.test(title)
// Why: reuse the shared whole-token matcher so orchestration groups honor the
// same Windows launcher-suffix rule (e.g. `grok.exe`) as the rest of Orca's
// agent-title detection, instead of maintaining a divergent regex here.
return buildAgentNameRe(agentName).test(title)
}
export function resolveGroupAddress(

View File

@ -53,9 +53,9 @@ describe('project-picker-browse-cache', () => {
0
)
for (let wave = 0; wave < 4; wave += 1) {
expect(
getProjectPickerBrowseCacheEntry('runtime:retained', inserted)
).toMatchObject({ projects: [expect.objectContaining({ owner: 'retained' })] })
expect(getProjectPickerBrowseCacheEntry('runtime:retained', inserted)).toMatchObject({
projects: [expect.objectContaining({ owner: 'retained' })]
})
for (let index = 1; index < PROJECT_PICKER_BROWSE_CACHE_MAX_ENTRIES; index += 1) {
const scope = `scope-${inserted}`
rememberProjectPickerBrowseCacheEntry(

View File

@ -26,8 +26,7 @@ const { appState, cacheMocks } = vi.hoisted(() => ({
}))
vi.mock('../../store', () => ({
useAppStore: (selector: (state: typeof appState.current) => unknown) =>
selector(appState.current)
useAppStore: (selector: (state: typeof appState.current) => unknown) => selector(appState.current)
}))
vi.mock('./pet-blob-cache', () => ({

View File

@ -137,15 +137,12 @@ export function useLinearAgentSkillSetupReminderToast({
}
}, [localDismissStorageKey, missingSetup])
useEffect(
() => {
if (surface !== 'modal') {
return
}
return () => {
dismissLinearAgentSkillSetupReminderToast(localDismissStorageKey)
}
},
[localDismissStorageKey, surface]
)
useEffect(() => {
if (surface !== 'modal') {
return
}
return () => {
dismissLinearAgentSkillSetupReminderToast(localDismissStorageKey)
}
}, [localDismissStorageKey, surface])
}

View File

@ -24,9 +24,9 @@ describe('linear agent skill setup reminders', () => {
MAX_LINEAR_AGENT_SKILL_SETUP_REMINDER_RUNTIME_KEYS
)
expect(hasLinearAgentSkillSetupReminderStateForTests('runtime-0')).toBe(false)
expect(hasLinearAgentSkillSetupReminderStateForTests(`runtime-${churnedRuntimeCount - 1}`)).toBe(
true
)
expect(
hasLinearAgentSkillSetupReminderStateForTests(`runtime-${churnedRuntimeCount - 1}`)
).toBe(true)
})
it('retains recently reused keys while trimming', () => {

View File

@ -228,9 +228,9 @@ describe('openMobileEmulatorTab', () => {
)
expect(ensureSimulatorTab).toHaveBeenCalledTimes(2)
const attachCalls = vi.mocked(callRuntimeRpc).mock.calls.filter(([, method]) =>
method === 'emulator.attach'
)
const attachCalls = vi
.mocked(callRuntimeRpc)
.mock.calls.filter(([, method]) => method === 'emulator.attach')
expect(attachCalls).toHaveLength(1)
expect(isManualSimulatorLaunchPending('wt-1')).toBe(false)
})
@ -245,10 +245,12 @@ describe('openMobileEmulatorTab', () => {
)
vi.mocked(ensureSimulatorTab)
.mockImplementationOnce(() => {
mockStoreState.unifiedTabsByWorktree['wt-1'] = [{
id: 'sim-1',
contentType: 'simulator'
}]
mockStoreState.unifiedTabsByWorktree['wt-1'] = [
{
id: 'sim-1',
contentType: 'simulator'
}
]
return 'sim-1'
})
.mockImplementationOnce(() => {

View File

@ -54,11 +54,7 @@ export function beginHugeRepoWarningProbe(
export function hasDismissedHugeRepoWarning(probe: HugeRepoWarningProbe): boolean {
const state = hugeRepoWarningStateByWorktreeId.get(probe.worktreeId)
if (
!state ||
state.lifecycleToken !== probe.lifecycleToken ||
!state.dismissed
) {
if (!state || state.lifecycleToken !== probe.lifecycleToken || !state.dismissed) {
return false
}
refreshHugeRepoWarningState(probe.worktreeId, state)

View File

@ -956,7 +956,10 @@ describe('shared agent-hook-listener', () => {
'grok',
{
paneKey: PANE_KEY,
payload: { hookEventName: 'user_prompt_submit', prompt: '<user_query>fix the bug</user_query>' }
payload: {
hookEventName: 'user_prompt_submit',
prompt: '<user_query>fix the bug</user_query>'
}
},
'production'
)

View File

@ -33,7 +33,7 @@ export const AGENT_NAMES = [
// `openclaude.exe`; still reject arbitrary dotted path fragments.
const WINDOWS_EXECUTABLE_SUFFIX_RE = String.raw`(?:\.(?:exe|cmd|bat|ps1))`
function buildAgentNameRe(name: string): RegExp {
export function buildAgentNameRe(name: string): RegExp {
return new RegExp(
`(?<![\\w./\\\\-])${name}(?:${WINDOWS_EXECUTABLE_SUFFIX_RE})?(?![\\w./\\\\-])`,
'i'

View File

@ -38,7 +38,9 @@ describe('isKnownHarnessInjectedUserTurnText', () => {
isKnownHarnessInjectedUserTurnText('A message arrived from teammate-b:\n<agent-message>hi')
).toBe(true)
expect(
isKnownHarnessInjectedUserTurnText('Another Claude session sent a message:\n<agent-message>hi')
isKnownHarnessInjectedUserTurnText(
'Another Claude session sent a message:\n<agent-message>hi'
)
).toBe(true)
expect(isKnownHarnessInjectedUserTurnText('No response requested.')).toBe(true)
expect(isKnownHarnessInjectedUserTurnText('[Request interrupted by user]')).toBe(true)
@ -78,7 +80,9 @@ describe('isKnownHarnessInjectedUserTurnText', () => {
expect(
isKnownHarnessInjectedUserTurnText('<script>alert(1)</script> — why is this flagged?')
).toBe(false)
expect(isKnownHarnessInjectedUserTurnText('<https://example.com/a-b> what is this?')).toBe(false)
expect(isKnownHarnessInjectedUserTurnText('<https://example.com/a-b> what is this?')).toBe(
false
)
expect(isKnownHarnessInjectedUserTurnText('<foo-bar@example.com> sent me this')).toBe(false)
})