feat(agents): add narrowly-scoped agent sort shim and install at plugin entry
OpenCode 1.4.x ignores the agent `order` field (sst/opencode#19127), so its `Agent.list()` sorts purely by `agent.name` via Remeda `sortBy` which uses native string `<`/`>` comparison. Without intervention, the four core agents fall into alphabetical order (Atlas -> Hephaestus -> Prometheus -> Sisyphus), which is not the canonical sisyphus -> hephaestus -> prometheus -> atlas order the project ships. Prior attempts to bias the sort key with invisible characters (ZWSP, U+2060 WORD JOINER, U+00AD SOFT HYPHEN, ANSI escape) all caused `Bun.stringWidth()` vs terminal-width drift, producing visible gaps and column truncation in the TUI status bar (#3259, #3238). Solution: a narrowly-scoped shim of `Array.prototype.toSorted` and `Array.prototype.sort` that activates only when the array contains two or more agent objects whose `.name` matches a canonical core display name. The activation predicate guards against mixed-type arrays so unrelated `.sort()` / `.toSorted()` calls (string arrays, number arrays, mixed objects) execute native behavior unchanged. Install is idempotent. Cubic P1 mitigations from PR #3267: - `isAgentArray` rejects any array with non-object or null elements, eliminating the throw-on-mixed-array failure mode. - Strict activation predicate (>= 2 ranked elements) keeps the global prototype patch from affecting unrelated sort calls. Remove this shim once OpenCode honors the agent `order` field (sst/opencode#19127). Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
/// <reference types="bun-types" />
|
||||
|
||||
import { beforeAll, describe, expect, test } from "bun:test"
|
||||
|
||||
import { installAgentSortShim } from "./agent-sort-shim"
|
||||
|
||||
describe("agent-sort-shim", () => {
|
||||
beforeAll(() => {
|
||||
installAgentSortShim()
|
||||
})
|
||||
|
||||
describe("#given an array of all 4 core agent objects in random order", () => {
|
||||
describe("#when toSorted with alphabetical compareFn", () => {
|
||||
test("#then returns canonical sisyphus->hephaestus->prometheus->atlas order", () => {
|
||||
// given
|
||||
const sisyphus = { name: "Sisyphus - Ultraworker" }
|
||||
const hephaestus = { name: "Hephaestus - Deep Agent" }
|
||||
const prometheus = { name: "Prometheus - Plan Builder" }
|
||||
const atlas = { name: "Atlas - Plan Executor" }
|
||||
const input = [atlas, prometheus, hephaestus, sisyphus]
|
||||
|
||||
// when
|
||||
const result = input.toSorted((a, b) => a.name.localeCompare(b.name))
|
||||
|
||||
// then
|
||||
expect(result).toEqual([sisyphus, hephaestus, prometheus, atlas])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("#given 4 core agents mixed with 2 non-core agent objects", () => {
|
||||
describe("#when toSorted with alphabetical compareFn", () => {
|
||||
test("#then core agents come first in canonical order followed by non-core agents alphabetically", () => {
|
||||
// given
|
||||
const sisyphus = { name: "Sisyphus - Ultraworker" }
|
||||
const hephaestus = { name: "Hephaestus - Deep Agent" }
|
||||
const prometheus = { name: "Prometheus - Plan Builder" }
|
||||
const atlas = { name: "Atlas - Plan Executor" }
|
||||
const build = { name: "build" }
|
||||
const plan = { name: "plan" }
|
||||
const input = [atlas, build, prometheus, plan, hephaestus, sisyphus]
|
||||
|
||||
// when
|
||||
const result = input.toSorted((a, b) => a.name.localeCompare(b.name))
|
||||
|
||||
// then
|
||||
expect(result).toEqual([sisyphus, hephaestus, prometheus, atlas, build, plan])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("#given an array with only one core agent and several non-core agent-like objects", () => {
|
||||
describe("#when toSorted with case-sensitive string-comparison compareFn", () => {
|
||||
test("#then activation predicate fails and result is ASCII-sensitive order with capital S before lowercase letters", () => {
|
||||
// given
|
||||
const oracle = { name: "oracle" }
|
||||
const librarian = { name: "librarian" }
|
||||
const sisyphus = { name: "Sisyphus - Ultraworker" }
|
||||
const explore = { name: "explore" }
|
||||
const input = [oracle, librarian, sisyphus, explore]
|
||||
|
||||
// when
|
||||
const result = input.toSorted((a, b) =>
|
||||
a.name < b.name ? -1 : a.name > b.name ? 1 : 0,
|
||||
)
|
||||
|
||||
// then
|
||||
expect(result).toEqual([sisyphus, explore, librarian, oracle])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("#given a mixed-type array containing null, objects, a string, and a number", () => {
|
||||
describe("#when toSorted with a string-coercing compareFn", () => {
|
||||
test("#then activation predicate fails, shim does not throw, and result matches native semantics", () => {
|
||||
// given
|
||||
const sisyphusObj = { name: "Sisyphus - Ultraworker" }
|
||||
const hephaestusObj = { name: "Hephaestus - Deep Agent" }
|
||||
const input: unknown[] = [null, sisyphusObj, "string", 42, hephaestusObj]
|
||||
const compare = (a: unknown, b: unknown): number => {
|
||||
const sa = String(a)
|
||||
const sb = String(b)
|
||||
if (sa < sb) return -1
|
||||
if (sa > sb) return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
// when
|
||||
const result = input.toSorted(compare)
|
||||
|
||||
// then
|
||||
expect(result).toEqual([42, sisyphusObj, hephaestusObj, null, "string"])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("#given a plain string array", () => {
|
||||
describe("#when toSorted with no compareFn", () => {
|
||||
test("#then returns native alphabetical ordering untouched", () => {
|
||||
// given
|
||||
const input = ["zebra", "apple", "mango"]
|
||||
|
||||
// when
|
||||
const result = input.toSorted()
|
||||
|
||||
// then
|
||||
expect(result).toEqual(["apple", "mango", "zebra"])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("#given a number array", () => {
|
||||
describe("#when sort with numeric compareFn (in-place)", () => {
|
||||
test("#then mutates the array and returns the same reference in ascending order", () => {
|
||||
// given
|
||||
const input = [3, 1, 4, 1, 5, 9, 2, 6]
|
||||
|
||||
// when
|
||||
const result = input.sort((a, b) => a - b)
|
||||
|
||||
// then
|
||||
expect(result).toBe(input)
|
||||
expect(input).toEqual([1, 1, 2, 3, 4, 5, 6, 9])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("#given agent objects with all 4 core display names in random order", () => {
|
||||
describe("#when sort with alphabetical compareFn (in-place)", () => {
|
||||
test("#then mutates the original array to canonical order", () => {
|
||||
// given
|
||||
const sisyphus = { name: "Sisyphus - Ultraworker" }
|
||||
const hephaestus = { name: "Hephaestus - Deep Agent" }
|
||||
const prometheus = { name: "Prometheus - Plan Builder" }
|
||||
const atlas = { name: "Atlas - Plan Executor" }
|
||||
const input = [atlas, prometheus, hephaestus, sisyphus]
|
||||
|
||||
// when
|
||||
const result = input.sort((a, b) => a.name.localeCompare(b.name))
|
||||
|
||||
// then
|
||||
expect(result).toBe(input)
|
||||
expect(input).toEqual([sisyphus, hephaestus, prometheus, atlas])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("#given installAgentSortShim has been invoked multiple times", () => {
|
||||
describe("#when toSorted is called on core agents after duplicate installs", () => {
|
||||
test("#then result is canonical order with no double-wrapping side effects", () => {
|
||||
// given
|
||||
installAgentSortShim()
|
||||
installAgentSortShim()
|
||||
const sisyphus = { name: "Sisyphus - Ultraworker" }
|
||||
const hephaestus = { name: "Hephaestus - Deep Agent" }
|
||||
const prometheus = { name: "Prometheus - Plan Builder" }
|
||||
const atlas = { name: "Atlas - Plan Executor" }
|
||||
const input = [atlas, prometheus, hephaestus, sisyphus]
|
||||
|
||||
// when
|
||||
const result = input.toSorted((a, b) => a.name.localeCompare(b.name))
|
||||
|
||||
// then
|
||||
expect(result).toEqual([sisyphus, hephaestus, prometheus, atlas])
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* Agent sort shim.
|
||||
*
|
||||
* OpenCode 1.4.x ignores the agent `order` field (sst/opencode#19127) and
|
||||
* sorts the agent list by `agent.name` via Remeda `sortBy(x => x.name, "asc")`
|
||||
* at packages/opencode/src/agent/agent.ts. Without intervention, the four
|
||||
* core agents collapse into Atlas -> Hephaestus -> Prometheus -> Sisyphus,
|
||||
* which inverts the canonical sisyphus -> hephaestus -> prometheus -> atlas
|
||||
* order this project ships.
|
||||
*
|
||||
* Earlier attempts to bias the sort key with invisible characters (ZWSP,
|
||||
* U+2060 WORD JOINER, U+00AD SOFT HYPHEN, ANSI escape) caused visible-gap
|
||||
* and column-truncation regressions in the TUI status bar (#3259, #3238).
|
||||
*
|
||||
* This shim is the narrowly-scoped alternative from PR #3267 with the Cubic
|
||||
* P1 mitigations applied:
|
||||
* 1. `isAgentArray` rejects any array element that is null, non-object, or
|
||||
* lacks a string `name`, eliminating the throw-on-mixed-array failure
|
||||
* mode that closed the original PR.
|
||||
* 2. The activation predicate requires >= 2 elements whose `.name` is one
|
||||
* of the four canonical core display names, so unrelated `.sort()` and
|
||||
* `.toSorted()` calls (string arrays, number arrays, generic objects)
|
||||
* execute native behavior unchanged.
|
||||
*
|
||||
* Remove this shim once OpenCode honors the agent `order` field
|
||||
* (sst/opencode#19127).
|
||||
*/
|
||||
|
||||
import { CANONICAL_CORE_AGENT_ORDER } from "../plugin-handlers/agent-priority-order"
|
||||
import { AGENT_DISPLAY_NAMES } from "./agent-display-names"
|
||||
|
||||
const AGENT_RANK: ReadonlyMap<string, number> = new Map(
|
||||
CANONICAL_CORE_AGENT_ORDER.map(
|
||||
(configKey, index): [string, number] => [AGENT_DISPLAY_NAMES[configKey], index + 1],
|
||||
),
|
||||
)
|
||||
|
||||
const UNRANKED = Number.MAX_SAFE_INTEGER
|
||||
|
||||
function extractAgentName(value: unknown): string {
|
||||
if (value === null || typeof value !== "object") return ""
|
||||
const candidate = value as { name?: unknown }
|
||||
return typeof candidate.name === "string" ? candidate.name : ""
|
||||
}
|
||||
|
||||
function isAgentArray(arr: ReadonlyArray<unknown>): boolean {
|
||||
if (arr.length < 2) return false
|
||||
|
||||
let rankedCount = 0
|
||||
for (const element of arr) {
|
||||
if (element === null || typeof element !== "object") return false
|
||||
const name = (element as { name?: unknown }).name
|
||||
if (typeof name !== "string") return false
|
||||
if (AGENT_RANK.has(name)) rankedCount++
|
||||
}
|
||||
|
||||
return rankedCount >= 2
|
||||
}
|
||||
|
||||
function agentComparator(
|
||||
a: unknown,
|
||||
b: unknown,
|
||||
fallback: ((a: unknown, b: unknown) => number) | undefined,
|
||||
): number {
|
||||
const aRank = AGENT_RANK.get(extractAgentName(a)) ?? UNRANKED
|
||||
const bRank = AGENT_RANK.get(extractAgentName(b)) ?? UNRANKED
|
||||
|
||||
if (aRank !== bRank) return aRank - bRank
|
||||
if (fallback) return fallback(a, b)
|
||||
return 0
|
||||
}
|
||||
|
||||
let installed = false
|
||||
|
||||
export function installAgentSortShim(): void {
|
||||
if (installed) return
|
||||
|
||||
const originalToSorted = Array.prototype.toSorted
|
||||
const originalSort = Array.prototype.sort
|
||||
|
||||
function patchedToSorted(
|
||||
this: unknown[],
|
||||
compareFn?: (a: unknown, b: unknown) => number,
|
||||
): unknown[] {
|
||||
if (isAgentArray(this)) {
|
||||
return originalToSorted.call(this, (a, b) => agentComparator(a, b, compareFn))
|
||||
}
|
||||
return originalToSorted.call(this, compareFn)
|
||||
}
|
||||
|
||||
function patchedSort(
|
||||
this: unknown[],
|
||||
compareFn?: (a: unknown, b: unknown) => number,
|
||||
): unknown[] {
|
||||
if (isAgentArray(this)) {
|
||||
return originalSort.call(this, (a, b) => agentComparator(a, b, compareFn))
|
||||
}
|
||||
return originalSort.call(this, compareFn)
|
||||
}
|
||||
|
||||
Object.defineProperty(Array.prototype, "toSorted", {
|
||||
value: patchedToSorted,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
enumerable: false,
|
||||
})
|
||||
|
||||
Object.defineProperty(Array.prototype, "sort", {
|
||||
value: patchedSort,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
enumerable: false,
|
||||
})
|
||||
|
||||
installed = true
|
||||
}
|
||||
Reference in New Issue
Block a user