refactor(agents): replace broken ZWSP sort prefixes with leading ASCII spaces

The ZWSP-based core agent sort prefixes silently failed to produce the
canonical sisyphus -> hephaestus -> prometheus -> atlas order.

Empirical testing of OpenCode's Agent.list() sort behavior shows that
Unicode collation treats zero-width characters as ignorable at the primary
level, so ZWSP-prefixed names sorted alphabetically with non-core agents
interleaved (e.g. Sisyphus, athena, Atlas, explore, Hephaestus, ...).

This commit replaces the ZWSP prefixes with leading ASCII spaces in
descending lengths (sisyphus=4, hephaestus=3, prometheus=2, atlas=1).
ASCII spaces sort reliably before alphabetic characters in localeCompare
under all locales and render correctly in every terminal.

Changes:
- AGENT_LIST_SORT_PREFIXES: ZWSP -> leading spaces (4-3-2-1 descending)
- stripAgentListSortPrefix: now strips both legacy ZWSP and new leading
  whitespace, preserving backward compatibility with existing sessions
- normalizeStoredAgentName / normalizeRegisteredAgentName: extract a
  shared stripSortPrefix helper that handles both prefix formats
- agent-config-handler: resolve user-provided default_agent display
  names through getAgentConfigKey before applying the runtime prefix,
  so configs like default_agent="Hephaestus - Deep Agent" are normalized
- agent-runtime-name-sort.test.ts: new regression test simulating
  OpenCode's exact sortBy logic (default_agent desc + name asc localeCompare)
  to verify canonical core agent order under randomised input permutations
- AGENTS.md: document the empirical finding that ZWSP was broken, why
  ASCII spaces work, and the descending prefix-length contract

Existing strip functions retain ZWSP support so legacy session state and
configs continue to resolve correctly without migration.
This commit is contained in:
YeonGyu-Kim
2026-04-27 13:48:09 +09:00
parent f74d03ca90
commit 0e9bb5969d
6 changed files with 196 additions and 20 deletions
+5 -5
View File
@@ -27,10 +27,10 @@ export const AGENT_DISPLAY_NAMES: Record<string, string> = {
}
const AGENT_LIST_SORT_PREFIXES: Record<string, string> = {
sisyphus: "\u200B",
hephaestus: "\u200B\u200B",
prometheus: "\u200B\u200B\u200B",
atlas: "\u200B\u200B\u200B\u200B",
sisyphus: " ",
hephaestus: " ",
prometheus: " ",
atlas: " ",
}
const INVISIBLE_AGENT_CHARACTERS_REGEX = /[\u200B\u200C\u200D\uFEFF]/g
@@ -40,7 +40,7 @@ export function stripInvisibleAgentCharacters(agentName: string): string {
}
export function stripAgentListSortPrefix(agentName: string): string {
return stripInvisibleAgentCharacters(agentName)
return stripInvisibleAgentCharacters(agentName).replace(/^\s+/, "")
}
export function getAgentRuntimeName(configKey: string): string {