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
@@ -18,12 +18,16 @@ const registeredAgentAliases = new Map<string, string>()
const ZERO_WIDTH_CHARACTERS_REGEX = /[\u200B\u200C\u200D\uFEFF]/g
function stripSortPrefix(name: string): string {
return name.replace(ZERO_WIDTH_CHARACTERS_REGEX, "").replace(/^\s+/, "")
}
function normalizeRegisteredAgentName(name: string): string {
return name.replace(ZERO_WIDTH_CHARACTERS_REGEX, "").toLowerCase()
return stripSortPrefix(name).toLowerCase()
}
function normalizeStoredAgentName(name: string): string {
return name.replace(ZERO_WIDTH_CHARACTERS_REGEX, "")
return stripSortPrefix(name)
}
export function registerAgentName(name: string): void {