Phase 005: vendor ArkScope, land the Arco style baseline, and fix bridge interpreter resolution

- Vendor ArkScope decorative layers (ScrambleTitle / IsometricWave) with provenance notes; decorative-only per AGENTS.md red lines
- Migrate theme tokens to Arco, register the Arco theme, and add hook adapter composables (useMessage / useDialog / useNotification) with matching signatures
- Sample pages: LoginView and AppSidebar on the new baseline; docs/arco-mapping.md and docs/ui-baseline.md land
- agent-bridge: prefer the PM-selected venv from facts.json over legacy in-tree venvs (legacy lacks the mcp extra, so every MCP server showed 0 tools / not connected)
- agent-bridge python (vendored, deliberate deviation): MCP discovery import fallback and pyyaml -> hermes_yaml so plugins and MCP work under the PM env; install the mcp extra via `hermes pm install --extra mcp`
- Chat-chain fragment: docs/chat-chain-changes/2026-10-17-phase005-arkscope-baseline.md

Verified: harness:check, test (2020 passed), test:e2e (30 passed), build all green; manual: plugins 100/0 warnings, MCP 6/6 servers connected with 104 tools.
This commit is contained in:
2026-10-17 09:45:30 +08:00
parent 4701bf0ad0
commit 4baccd5d88
106 changed files with 1493 additions and 221 deletions
@@ -1,5 +1,6 @@
import { execFileSync, spawn, type ChildProcess } from 'child_process'
import { existsSync, readFileSync } from 'fs'
import { createHash } from 'crypto'
import { existsSync, readFileSync, realpathSync } from 'fs'
import { createConnection, createServer } from 'net'
import { dirname, isAbsolute, join, resolve } from 'path'
import { logger } from '../../logger'
@@ -103,18 +104,22 @@ export function buildAgentBridgeProcessEnv(endpoint: string, hermesHome: string
function pathCandidates(agentRoot?: string): string[] {
if (!agentRoot) return []
// Legacy in-tree virtualenvs. PM-managed installs live under
// `<hermesHome>/installs/<key>/environments/.../venv` (see
// pmSelectedVenvPython below); these trees predate PM and may be a bare
// interpreter, so they are only a fallback.
return process.platform === 'win32'
? [
join(agentRoot, 'venv', 'Scripts', 'python.exe'),
join(agentRoot, 'venv', 'Scripts', 'python3.exe'),
join(agentRoot, '.venv', 'Scripts', 'python.exe'),
join(agentRoot, '.venv', 'Scripts', 'python3.exe'),
join(agentRoot, 'venv', 'Scripts', 'python.exe'),
join(agentRoot, 'venv', 'Scripts', 'python3.exe'),
]
: [
join(agentRoot, 'venv', 'bin', 'python3'),
join(agentRoot, 'venv', 'bin', 'python'),
join(agentRoot, '.venv', 'bin', 'python3'),
join(agentRoot, '.venv', 'bin', 'python'),
join(agentRoot, 'venv', 'bin', 'python3'),
join(agentRoot, 'venv', 'bin', 'python'),
]
}
@@ -218,6 +223,41 @@ function firstExistingExecutable(candidates: string[]): string | undefined {
return undefined
}
// PM (hermes's package manager) records the selected venv in
// `<hermesHome>/installs/<installKey>/facts.json`, where installKey is
// sha256(resolved agentRoot)[:16] (see pm/environments.py). That venv carries
// hermes's extras (mcp, ruamel, ...); the in-tree `.venv`/`venv` trees predate
// PM and often lack them.
function pmSelectedVenvPython(agentRoot: string | undefined, hermesHome: string): string | undefined {
if (!agentRoot) return undefined
let resolvedRoot = agentRoot
try {
resolvedRoot = realpathSync(agentRoot)
} catch {}
const installKey = createHash('sha256').update(resolve(resolvedRoot)).digest('hex').slice(0, 16)
const factsCandidates = [
join(hermesHome, 'installs', installKey, 'facts.json'),
join(process.env.HERMES_HOME?.trim() || join(process.env.HOME || '', '.hermes'), 'installs', installKey, 'facts.json'),
]
for (const factsPath of factsCandidates) {
try {
if (!existsSync(factsPath)) continue
const facts = JSON.parse(readFileSync(factsPath, 'utf8')) as {
packages?: { venv?: { environment?: string } }
}
const environment = facts?.packages?.venv?.environment
if (!environment) continue
const python = firstExistingExecutable(
process.platform === 'win32'
? [join(environment, 'Scripts', 'python.exe'), join(environment, 'Scripts', 'python3.exe')]
: [join(environment, 'bin', 'python3'), join(environment, 'bin', 'python')],
)
if (python) return python
} catch {}
}
return undefined
}
function resolveAgentRoot(explicit?: string, hermesHome = detectHermesHome()): string | undefined {
const candidates = [
explicit,
@@ -242,6 +282,11 @@ export function resolveAgentBridgeCommand(options: AgentBridgeManagerOptions = {
return { command: explicitPython, argsPrefix: [], agentRoot, hermesHome }
}
const pmPython = pmSelectedVenvPython(agentRoot, hermesHome)
if (pmPython) {
return { command: pmPython, argsPrefix: [], agentRoot, hermesHome }
}
const venvPython = firstExistingExecutable(pathCandidates(agentRoot))
if (venvPython) {
return { command: venvPython, argsPrefix: [], agentRoot, hermesHome }