From 9b16d6792f6af06ad28065e97de1ddb2742b3fa1 Mon Sep 17 00:00:00 2001 From: Christopher Nelson Date: Thu, 20 Aug 2026 04:32:03 -0400 Subject: [PATCH] chore: rename project to Hex Zero --- .env.example | 7 ++ .gitignore | 1 + AGENTS.md | 2 +- README.md | 29 +++++- apps/game-api/package.json | 8 +- apps/game-api/src/app.test.ts | 49 +++++++++- apps/game-api/src/app.ts | 34 +++++-- apps/game-api/src/experiment-export.ts | 2 +- apps/game-api/src/geocoder.ts | 4 +- apps/game-api/src/server.ts | 7 +- apps/game-api/src/simulation-service.test.ts | 6 +- apps/game-api/src/simulation-service.ts | 6 +- apps/world-lab/next.config.ts | 2 +- apps/world-lab/package.json | 6 +- apps/world-lab/src/app/layout.tsx | 4 +- .../src/components/model-options.test.ts | 2 +- .../world-lab/src/components/model-options.ts | 2 +- .../src/components/personality-presets.ts | 2 +- .../world-lab/src/components/ui-color.test.ts | 2 +- apps/world-lab/src/components/ui-color.ts | 2 +- .../src/components/world-lab.test.tsx | 67 +++++++++++-- apps/world-lab/src/components/world-lab.tsx | 90 +++++++++++++++--- apps/world-lab/src/components/world-map.tsx | 2 +- docs/ARCHITECTURE.md | 2 +- docs/EXPERIMENT_ARCHIVE.md | 9 +- docs/GAMEPLAY_FOUNDATION.md | 2 +- docs/SECURITY.md | 2 +- docs/TESTING.md | 4 +- .../0009-capability-driven-model-catalog.md | 2 +- package.json | 12 +-- packages/agent-runtime/package.json | 4 +- packages/agent-runtime/src/index.test.ts | 4 +- packages/agent-runtime/src/index.ts | 2 +- .../agent-runtime/src/model-catalog.test.ts | 2 +- packages/agent-runtime/src/model-catalog.ts | 2 +- .../agent-runtime/src/real-provider-smoke.ts | 2 +- packages/experiment-archive/package.json | 8 +- .../experiment-archive/src/archive.test.ts | 93 +++++++++++++++++-- packages/experiment-archive/src/cli.ts | 3 +- packages/experiment-archive/src/database.ts | 53 +++++++++-- packages/experiment-archive/src/importer.ts | 2 +- packages/shared/package.json | 2 +- packages/shared/src/index.test.ts | 7 ++ packages/world-engine/package.json | 4 +- packages/world-engine/src/index.test.ts | 2 +- packages/world-engine/src/index.ts | 2 +- packages/world-engine/src/scenario.test.ts | 2 +- playwright.config.ts | 5 +- pnpm-lock.yaml | 20 ++-- tests/e2e/world-lab.spec.ts | 2 +- 50 files changed, 461 insertions(+), 129 deletions(-) diff --git a/.env.example b/.env.example index 45b461c..2c52104 100644 --- a/.env.example +++ b/.env.example @@ -3,8 +3,15 @@ OPENROUTER_API_KEY=replace-with-your-openrouter-key # The scripted provider is exclusively for deterministic automation. Normal # interactive development must leave this unset so every turn uses OpenRouter. +# HEXZERO_PROVIDER=scripted +# Deprecated compatibility alias (HEXZERO_PROVIDER takes precedence): # AGENTBORNE_PROVIDER=scripted +# Optional local experiment archive path. +# HEXZERO_EXPERIMENT_DB=.hexzero/experiments.sqlite +# Deprecated compatibility alias (HEXZERO_EXPERIMENT_DB takes precedence): +# AGENTBORNE_EXPERIMENT_DB=.agentborne/experiments.sqlite + # Development-map center (Toledo, Ohio) and H3 resolution. NEXT_PUBLIC_DEV_MAP_LATITUDE=41.6528 NEXT_PUBLIC_DEV_MAP_LONGITUDE=-83.5379 diff --git a/.gitignore b/.gitignore index 6c6bb28..6af21e6 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ test-results/ *.log logs/ .agentborne/ +.hexzero/ diff --git a/AGENTS.md b/AGENTS.md index 22923dd..32b2f6c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ These instructions apply throughout the repository. ## Product boundaries -- Treat “Agentborne” as a working title. Avoid heavy branding, logo, lore, or name-specific design investment. +- Use “Hex Zero” consistently while avoiding unnecessary logo, lore, or name-specific design investment. - Keep the World Lab as a production developer/admin surface, not a disposable demo. - The only initial hex states are `open` and `infected`. - The only agent world actions are adjacent move, infect current cell, capture an abandoned infected current cell from a non-allied controller, and wait. One optional public or range-limited direct communication and one optional formal diplomacy intent may accompany that action. diff --git a/README.md b/README.md index ee26ff0..50a0c52 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# Agentborne +# Hex Zero -Agentborne is the working title for an agent-first geographic experiment. A configurable roster of model-backed agents moves, infects, captures contested territory, communicates, and conducts formal alliance diplomacy while the World Lab exposes every safe decision record. +Hex Zero is an agent-first geographic experiment. A configurable roster of model-backed agents moves, infects, captures contested territory, communicates, and conducts formal alliance diplomacy while the World Lab exposes every safe decision record. Scenarios may optionally designate one experimental Patient Zero coordinator. The role receives bounded global strategic information and can send private @@ -31,6 +31,10 @@ pnpm install --frozen-lockfile pnpm dev ``` +For deterministic local automation, `pnpm dev:test-provider` sets +`HEXZERO_PROVIDER=scripted`. `HEXZERO_EXPERIMENT_DB` overrides the local +experiment archive path. + Open the World Lab at . The Game API binds to ; Next.js narrowly proxies `/api/game/*` to it. `OPENROUTER_API_KEY` is the only required OpenRouter environment value. Select a compatible global model in World Lab, then optionally override individual agents. Each assignment may use the provider's default reasoning behavior, disable optional reasoning, or select only an effort advertised by that model's catalog metadata. Each Single turn or completed playback interval makes one third-party OpenRouter request and may incur cost. Start is deliberately disabled when the server has no key. This development API has no authentication or cost controls and is not suitable for an unauthenticated public deployment. @@ -66,3 +70,24 @@ The agent roster defaults to browser-local **Follow turn** behavior: the inspect See [Testing](docs/TESTING.md), [Architecture](docs/ARCHITECTURE.md), [Security](docs/SECURITY.md), the accepted future [Gameplay Foundation](docs/GAMEPLAY_FOUNDATION.md), and the [Roadmap](ROADMAP.md). Completed schema-v9 exports can be imported into an ignored local SQLite archive and queried without repeatedly loading full JSON artifacts. See [Local experiment archive](docs/EXPERIMENT_ARCHIVE.md). + +## Rename compatibility + +Hex Zero was formerly named Agentborne. Workspace packages now use the +`@hexzero/*` namespace, and the repository URL will be +`https://github.com/BlackSwampAI/hexzero` after the external repository rename. +Existing environments may continue using `AGENTBORNE_PROVIDER` and +`AGENTBORNE_EXPERIMENT_DB`; the corresponding `HEXZERO_` variable takes +precedence, and selecting a legacy alias emits a value-free deprecation notice. + +New archives default to `.hexzero/experiments.sqlite`. When that file does not +exist, an existing `.agentborne/experiments.sqlite` is opened in place with a +migration notice; Hex Zero never moves or overwrites it automatically. To +migrate manually, stop every Hex Zero process, create `.hexzero`, copy the +legacy database (including any `-wal` and `-shm` sidecars if present), verify +the copy opens, and only then remove the legacy files if desired. + +New downloads use `hexzero-experiment-`. Existing +`agentborne-experiment-*.json` files and their schema-v9 contents remain +importable unchanged. Browser-owned settings stored under legacy `agentborne` +keys are schema-validated and copied once to the new `hexzero` keys. diff --git a/apps/game-api/package.json b/apps/game-api/package.json index 16d331f..b2b7298 100644 --- a/apps/game-api/package.json +++ b/apps/game-api/package.json @@ -1,5 +1,5 @@ { - "name": "@agentborne/game-api", + "name": "@hexzero/game-api", "version": "0.0.0", "private": true, "type": "module", @@ -12,9 +12,9 @@ "typecheck": "tsc -p tsconfig.json" }, "dependencies": { - "@agentborne/agent-runtime": "workspace:*", - "@agentborne/shared": "workspace:*", - "@agentborne/world-engine": "workspace:*", + "@hexzero/agent-runtime": "workspace:*", + "@hexzero/shared": "workspace:*", + "@hexzero/world-engine": "workspace:*", "@hono/node-server": "2.1.0", "h3-js": "4.5.0", "hono": "4.13.2" diff --git a/apps/game-api/src/app.test.ts b/apps/game-api/src/app.test.ts index d54fb6e..60af0a1 100644 --- a/apps/game-api/src/app.test.ts +++ b/apps/game-api/src/app.test.ts @@ -1,11 +1,12 @@ -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import { AgentProviderError, + BrowserTestAgentProvider, OpenRouterAgentProvider, ScriptedAgentProvider, type AgentProvider, type ProviderDecision, -} from '@agentborne/agent-runtime'; +} from '@hexzero/agent-runtime'; import { cancelledTurnResponseSchema, h3CellSchema, @@ -20,8 +21,46 @@ import { updateAgentPersonalityResponseSchema, updateExperimentModelsResponseSchema, verifyModelResponseSchema, -} from '@agentborne/shared'; -import { createApp } from './app'; +} from '@hexzero/shared'; +import { + createApp, + providerFromEnvironment, + resolveProviderModeFromEnvironment, +} from './app'; + +describe('provider environment compatibility', () => { + it('uses the canonical provider variable without a warning', () => { + const warn = vi.fn(); + expect( + providerFromEnvironment({ HEXZERO_PROVIDER: 'scripted' }, warn), + ).toBeInstanceOf(BrowserTestAgentProvider); + expect(warn).not.toHaveBeenCalled(); + }); + + it('prefers the canonical provider variable over the legacy alias', () => { + const warn = vi.fn(); + expect( + resolveProviderModeFromEnvironment( + { + HEXZERO_PROVIDER: 'openrouter', + AGENTBORNE_PROVIDER: 'scripted', + }, + warn, + ), + ).toBe('openrouter'); + expect(warn).not.toHaveBeenCalled(); + }); + + it('supports the legacy provider alias with a value-free warning', () => { + const warn = vi.fn(); + expect( + providerFromEnvironment({ AGENTBORNE_PROVIDER: 'scripted' }, warn), + ).toBeInstanceOf(BrowserTestAgentProvider); + expect(warn).toHaveBeenCalledWith( + 'AGENTBORNE_PROVIDER is deprecated; use HEXZERO_PROVIDER. Continuing with the legacy setting.', + ); + }); +}); describe('game API simulation boundary', () => { it('coalesces repeated delivery of the same turn mutation ID', async () => { @@ -48,7 +87,7 @@ describe('game API simulation boundary', () => { const request = () => app.request('/api/simulation/turn', { method: 'POST', - headers: { 'X-Agentborne-Mutation-Id': 'mutation_same_001' }, + headers: { 'X-Hexzero-Mutation-Id': 'mutation_same_001' }, }); const [first, duplicate] = await Promise.all([request(), request()]); expect(first.status).toBe(200); diff --git a/apps/game-api/src/app.ts b/apps/game-api/src/app.ts index e4fba1f..22437c2 100644 --- a/apps/game-api/src/app.ts +++ b/apps/game-api/src/app.ts @@ -6,7 +6,7 @@ import { OpenRouterModelCatalog, OpenRouterAgentProvider, type AgentProvider, -} from '@agentborne/agent-runtime'; +} from '@hexzero/agent-runtime'; import { apiErrorSchema, AGENT_DECISION_CONTRACT_VERSION, @@ -43,11 +43,11 @@ import { locationSearchResponseSchema, defaultWorldSetupResponseSchema, type ModelVerification, -} from '@agentborne/shared'; +} from '@hexzero/shared'; import { createDevelopmentWorld, generateDeterministicRoster, -} from '@agentborne/world-engine'; +} from '@hexzero/world-engine'; import { SimulationConflictError, SimulationService, @@ -66,12 +66,30 @@ export interface AppOptions { geocoder?: Geocoder; } -export function providerFromEnvironment(): AgentProvider { - if (process.env.AGENTBORNE_PROVIDER === 'scripted') { +export function resolveProviderModeFromEnvironment( + environment: NodeJS.ProcessEnv = process.env, + warn: (message: string) => void = console.warn, +): string | undefined { + if (environment.HEXZERO_PROVIDER !== undefined) + return environment.HEXZERO_PROVIDER; + if (environment.AGENTBORNE_PROVIDER !== undefined) { + warn( + 'AGENTBORNE_PROVIDER is deprecated; use HEXZERO_PROVIDER. Continuing with the legacy setting.', + ); + return environment.AGENTBORNE_PROVIDER; + } + return undefined; +} + +export function providerFromEnvironment( + environment: NodeJS.ProcessEnv = process.env, + warn: (message: string) => void = console.warn, +): AgentProvider { + if (resolveProviderModeFromEnvironment(environment, warn) === 'scripted') { return new BrowserTestAgentProvider(); } return new OpenRouterAgentProvider({ - apiKey: process.env.OPENROUTER_API_KEY, + apiKey: environment.OPENROUTER_API_KEY, }); } @@ -95,7 +113,7 @@ export function createApp(options: AppOptions = {}) { execute: () => Promise, ): Promise => { const supplied = - context.req.header('x-agentborne-mutation-id') ?? + context.req.header('x-hexzero-mutation-id') ?? context.req.query('mutationId'); const mutationId = supplied && /^[A-Za-z0-9_-]{8,80}$/.test(supplied) ? supplied : undefined; @@ -119,7 +137,7 @@ export function createApp(options: AppOptions = {}) { ? origin : null, allowMethods: ['GET', 'POST'], - allowHeaders: ['Content-Type', 'X-Agentborne-Mutation-Id'], + allowHeaders: ['Content-Type', 'X-Hexzero-Mutation-Id'], }), ); diff --git a/apps/game-api/src/experiment-export.ts b/apps/game-api/src/experiment-export.ts index ab396a8..0fd8cdb 100644 --- a/apps/game-api/src/experiment-export.ts +++ b/apps/game-api/src/experiment-export.ts @@ -29,7 +29,7 @@ import { type WorldSnapshot, type BehaviorConfiguration, type AppliedScenario, -} from '@agentborne/shared'; +} from '@hexzero/shared'; export interface ExperimentSource { id: ExperimentId; diff --git a/apps/game-api/src/geocoder.ts b/apps/game-api/src/geocoder.ts index 9541e69..ddf1d0f 100644 --- a/apps/game-api/src/geocoder.ts +++ b/apps/game-api/src/geocoder.ts @@ -1,7 +1,7 @@ import { locationSearchResponseSchema, type LocationSearchResponse, -} from '@agentborne/shared'; +} from '@hexzero/shared'; export interface Geocoder { search(query: string): Promise; @@ -43,7 +43,7 @@ export class NominatimGeocoder implements Geocoder { const response = await fetch(url, { headers: { 'User-Agent': - 'Agentborne-WorldLab (+https://github.com/BlackSwampAI/agentborne)', + 'Hex Zero-WorldLab (+https://github.com/BlackSwampAI/hexzero)', Accept: 'application/json', }, signal: controller.signal, diff --git a/apps/game-api/src/server.ts b/apps/game-api/src/server.ts index 8e576a2..e136833 100644 --- a/apps/game-api/src/server.ts +++ b/apps/game-api/src/server.ts @@ -1,9 +1,12 @@ import { readFileSync } from 'node:fs'; import { serve } from '@hono/node-server'; -import { applyProviderEnvironmentFile } from '@agentborne/agent-runtime'; +import { applyProviderEnvironmentFile } from '@hexzero/agent-runtime'; import { createApp } from './app'; -if (process.env.AGENTBORNE_PROVIDER !== 'scripted') { +if ( + (process.env.HEXZERO_PROVIDER ?? process.env.AGENTBORNE_PROVIDER) !== + 'scripted' +) { try { process.loadEnvFile('../../.env'); applyProviderEnvironmentFile( diff --git a/apps/game-api/src/simulation-service.test.ts b/apps/game-api/src/simulation-service.test.ts index 6db489d..18ee05d 100644 --- a/apps/game-api/src/simulation-service.test.ts +++ b/apps/game-api/src/simulation-service.test.ts @@ -5,7 +5,7 @@ import { ScriptedAgentProvider, type AgentProvider, type ProviderDecision, -} from '@agentborne/agent-runtime'; +} from '@hexzero/agent-runtime'; import { PERSONALITY_MAX_LENGTH, agentIdSchema, @@ -16,12 +16,12 @@ import { type AgentTurnRecord, type CompatibleModel, type WorldEvent, -} from '@agentborne/shared'; +} from '@hexzero/shared'; import { DEVELOPMENT_AGENT_BLUEPRINTS, defaultWorldSetupRequest, physicalDistanceKm, -} from '@agentborne/world-engine'; +} from '@hexzero/world-engine'; import { SimulationConflictError, SimulationService, diff --git a/apps/game-api/src/simulation-service.ts b/apps/game-api/src/simulation-service.ts index 588b95d..7877a74 100644 --- a/apps/game-api/src/simulation-service.ts +++ b/apps/game-api/src/simulation-service.ts @@ -3,7 +3,7 @@ import { AgentProviderError, type AgentProvider, type ProviderDecision, -} from '@agentborne/agent-runtime'; +} from '@hexzero/agent-runtime'; import { agentIdSchema, appliedScenarioSchema, @@ -57,7 +57,7 @@ import { type AppliedScenario, type WorldSetupPreviewResponse, type WorldSetupRequest, -} from '@agentborne/shared'; +} from '@hexzero/shared'; import { applyCommunication, applyDiplomacy, @@ -75,7 +75,7 @@ import { expireAllianceProposals, toWorldState, type WorldState, -} from '@agentborne/world-engine'; +} from '@hexzero/world-engine'; import { createExperimentExport, createExperimentPreview, diff --git a/apps/world-lab/next.config.ts b/apps/world-lab/next.config.ts index d0db6bc..dcefe48 100644 --- a/apps/world-lab/next.config.ts +++ b/apps/world-lab/next.config.ts @@ -2,7 +2,7 @@ import type { NextConfig } from 'next'; const nextConfig: NextConfig = { allowedDevOrigins: ['127.0.0.1'], - transpilePackages: ['@agentborne/shared', '@agentborne/world-engine'], + transpilePackages: ['@hexzero/shared', '@hexzero/world-engine'], experimental: { useTypeScriptCli: false }, async rewrites() { return [ diff --git a/apps/world-lab/package.json b/apps/world-lab/package.json index b20ec4a..e27e0e6 100644 --- a/apps/world-lab/package.json +++ b/apps/world-lab/package.json @@ -1,5 +1,5 @@ { - "name": "@agentborne/world-lab", + "name": "@hexzero/world-lab", "version": "0.0.0", "private": true, "type": "module", @@ -12,8 +12,8 @@ "typecheck": "next typegen && tsc --noEmit" }, "dependencies": { - "@agentborne/shared": "workspace:*", - "@agentborne/world-engine": "workspace:*", + "@hexzero/shared": "workspace:*", + "@hexzero/world-engine": "workspace:*", "h3-js": "4.5.0", "maplibre-gl": "6.3.0", "next": "16.3.1", diff --git a/apps/world-lab/src/app/layout.tsx b/apps/world-lab/src/app/layout.tsx index fc433f9..4f6ac92 100644 --- a/apps/world-lab/src/app/layout.tsx +++ b/apps/world-lab/src/app/layout.tsx @@ -3,8 +3,8 @@ import 'maplibre-gl/dist/maplibre-gl.css'; import './styles.css'; export const metadata: Metadata = { - title: 'World Lab', - description: 'Developer interface for observing the Agentborne world.', + title: 'Hex Zero — World Lab', + description: 'Developer interface for observing the Hex Zero world.', }; export default function RootLayout({ diff --git a/apps/world-lab/src/components/model-options.test.ts b/apps/world-lab/src/components/model-options.test.ts index 2ffb050..ad2db40 100644 --- a/apps/world-lab/src/components/model-options.test.ts +++ b/apps/world-lab/src/components/model-options.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import type { CompatibleModel } from '@agentborne/shared'; +import type { CompatibleModel } from '@hexzero/shared'; import { buildModelOptions, formatModelOption } from './model-options'; const model = (id: string, name: string, price = '0'): CompatibleModel => ({ diff --git a/apps/world-lab/src/components/model-options.ts b/apps/world-lab/src/components/model-options.ts index 69ae90d..f1c150b 100644 --- a/apps/world-lab/src/components/model-options.ts +++ b/apps/world-lab/src/components/model-options.ts @@ -1,4 +1,4 @@ -import type { CompatibleModel } from '@agentborne/shared'; +import type { CompatibleModel } from '@hexzero/shared'; export interface ModelOption { value: string; diff --git a/apps/world-lab/src/components/personality-presets.ts b/apps/world-lab/src/components/personality-presets.ts index 103eafb..08ff48a 100644 --- a/apps/world-lab/src/components/personality-presets.ts +++ b/apps/world-lab/src/components/personality-presets.ts @@ -1,4 +1,4 @@ -import { personalitySchema } from '@agentborne/shared'; +import { personalitySchema } from '@hexzero/shared'; export const PERSONALITY_PRESET_IDS = [ 'aggressive-infector', diff --git a/apps/world-lab/src/components/ui-color.test.ts b/apps/world-lab/src/components/ui-color.test.ts index 3535fbe..f561ac3 100644 --- a/apps/world-lab/src/components/ui-color.test.ts +++ b/apps/world-lab/src/components/ui-color.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import type { AgentId, SimulationSnapshot } from '@agentborne/shared'; +import type { AgentId, SimulationSnapshot } from '@hexzero/shared'; import { neutralAgentColor, resolveAgentColor } from './ui-color'; const agentId = '11111111-1111-4111-8111-111111111111' as AgentId; diff --git a/apps/world-lab/src/components/ui-color.ts b/apps/world-lab/src/components/ui-color.ts index c3851d2..5c6b128 100644 --- a/apps/world-lab/src/components/ui-color.ts +++ b/apps/world-lab/src/components/ui-color.ts @@ -2,7 +2,7 @@ import { NEUTRAL_AGENT_COLOR, type AgentId, type SimulationSnapshot, -} from '@agentborne/shared'; +} from '@hexzero/shared'; export const neutralAgentColor = NEUTRAL_AGENT_COLOR; diff --git a/apps/world-lab/src/components/world-lab.test.tsx b/apps/world-lab/src/components/world-lab.test.tsx index 2b247c1..c20d36f 100644 --- a/apps/world-lab/src/components/world-lab.test.tsx +++ b/apps/world-lab/src/components/world-lab.test.tsx @@ -17,14 +17,14 @@ import { simulationSnapshotSchema, NEUTRAL_AGENT_COLOR, type SimulationSnapshot, -} from '@agentborne/shared'; +} from '@hexzero/shared'; import { createDefaultAppliedScenario, createDevelopmentWorld, defaultWorldSetupRequest, generateDeterministicRoster, previewWorldSetup, -} from '@agentborne/world-engine'; +} from '@hexzero/world-engine'; import { WorldLab } from './world-lab'; import { PERSONALITY_PRESETS } from './personality-presets'; @@ -957,6 +957,55 @@ async function openAgentsWorkspace(user: ReturnType) { } describe('WorldLab', () => { + it('migrates valid legacy browser preferences once and rejects malformed legacy data', async () => { + window.localStorage.setItem('agentborne.world-lab.follow-turn', 'false'); + window.localStorage.setItem( + 'agentborne.world-lab.activity-dock', + 'collapsed', + ); + window.localStorage.setItem( + 'agentborne.world-lab.unattended-recovery', + JSON.stringify({ enabled: true, retryLimit: 3 }), + ); + window.sessionStorage.setItem('agentborne.world-lab.run-target', '500'); + const first = render(); + await screen.findByRole('button', { name: 'Start' }); + await waitFor(() => { + expect(window.localStorage.getItem('hexzero.world-lab.follow-turn')).toBe( + 'false', + ); + expect( + window.localStorage.getItem('hexzero.world-lab.activity-dock'), + ).toBe('collapsed'); + expect( + window.localStorage.getItem('hexzero.world-lab.unattended-recovery'), + ).toBe(JSON.stringify({ enabled: true, retryLimit: 3 })); + expect( + window.sessionStorage.getItem('hexzero.world-lab.run-target'), + ).toBe('500'); + }); + first.unmount(); + + window.localStorage.setItem('agentborne.world-lab.follow-turn', 'true'); + const second = render(); + await screen.findByRole('button', { name: 'Start' }); + expect(window.localStorage.getItem('hexzero.world-lab.follow-turn')).toBe( + 'false', + ); + + window.localStorage.removeItem('hexzero.world-lab.unattended-recovery'); + window.localStorage.setItem( + 'agentborne.world-lab.unattended-recovery', + '{"enabled":"yes","retryLimit":99}', + ); + second.unmount(); + render(); + await screen.findByRole('button', { name: 'Start' }); + expect( + window.localStorage.getItem('hexzero.world-lab.unattended-recovery'), + ).not.toBe('{"enabled":"yes","retryLimit":99}'); + }); + it('renders all controls, status, H3 readiness, and eight visible markers', async () => { const user = userEvent.setup(); render(); @@ -1561,9 +1610,9 @@ describe('WorldLab', () => { expect( within(roster).getByRole('checkbox', { name: 'Follow turn' }), ).not.toBeChecked(); - expect( - window.localStorage.getItem('agentborne.world-lab.follow-turn'), - ).toBe('false'); + expect(window.localStorage.getItem('hexzero.world-lab.follow-turn')).toBe( + 'false', + ); await openOverflow(user); await user.click(screen.getByRole('button', { name: 'Export' })); expect( @@ -2860,9 +2909,12 @@ describe('WorldLab', () => { const createObjectURL = vi.fn(() => 'blob:experiment'); const revokeObjectURL = vi.fn(); vi.stubGlobal('URL', { ...URL, createObjectURL, revokeObjectURL }); + let downloadedFilename: string | undefined; const click = vi .spyOn(HTMLAnchorElement.prototype, 'click') - .mockImplementation(() => undefined); + .mockImplementation(function (this: HTMLAnchorElement) { + downloadedFilename = this.download; + }); render(); await openOverflow(user); await user.click(screen.getByRole('button', { name: 'Export' })); @@ -2884,6 +2936,9 @@ describe('WorldLab', () => { expect(createObjectURL).toHaveBeenCalledTimes(1); expect(revokeObjectURL).toHaveBeenCalledWith('blob:experiment'); expect(click).toHaveBeenCalledTimes(1); + expect(downloadedFilename).toMatch( + /^hexzero-experiment-.+-one-agent-entire-retained\.json$/, + ); await user.selectOptions( screen.getByLabelText('JSON serialization'), 'pretty', diff --git a/apps/world-lab/src/components/world-lab.tsx b/apps/world-lab/src/components/world-lab.tsx index 6602cfc..908ebe9 100644 --- a/apps/world-lab/src/components/world-lab.tsx +++ b/apps/world-lab/src/components/world-lab.tsx @@ -55,7 +55,7 @@ import { type SimulationSnapshot, type WorldSetupRequest, type WorldSetupPreviewResponse, -} from '@agentborne/shared'; +} from '@hexzero/shared'; import { matchingPersonalityPreset, PERSONALITY_PRESETS, @@ -66,12 +66,47 @@ import { resolveAgentColor } from './ui-color'; const apiBase = process.env.NEXT_PUBLIC_GAME_API_BASE_URL ?? '/api/game/simulation'; -const followTurnStorageKey = 'agentborne.world-lab.follow-turn'; -const runTargetStorageKey = 'agentborne.world-lab.run-target'; -const unattendedRecoveryStorageKey = 'agentborne.world-lab.unattended-recovery'; -const activityDockStorageKey = 'agentborne.world-lab.activity-dock'; +const followTurnStorageKey = 'hexzero.world-lab.follow-turn'; +const runTargetStorageKey = 'hexzero.world-lab.run-target'; +const unattendedRecoveryStorageKey = 'hexzero.world-lab.unattended-recovery'; +const activityDockStorageKey = 'hexzero.world-lab.activity-dock'; +const legacyFollowTurnStorageKey = 'agentborne.world-lab.follow-turn'; +const legacyRunTargetStorageKey = 'agentborne.world-lab.run-target'; +const legacyUnattendedRecoveryStorageKey = + 'agentborne.world-lab.unattended-recovery'; +const legacyActivityDockStorageKey = 'agentborne.world-lab.activity-dock'; export const runTargets = [25, 50, 100, 200, 500, 1000] as const; +function readStoredPreference( + storage: Storage, + key: string, + legacyKey: string, + valid: (value: string) => boolean, +): string | null { + const current = storage.getItem(key); + if (current !== null) return valid(current) ? current : null; + const legacy = storage.getItem(legacyKey); + if (legacy === null || !valid(legacy)) return null; + storage.setItem(key, legacy); + return legacy; +} + +function validUnattendedPreference(value: string): boolean { + try { + const parsed: unknown = JSON.parse(value); + if (!parsed || typeof parsed !== 'object') return false; + const preference = parsed as Record; + return ( + typeof preference.enabled === 'boolean' && + (preference.retryLimit === 1 || + preference.retryLimit === 2 || + preference.retryLimit === 3) + ); + } catch { + return false; + } +} + function unattendedFailureEligible( failure: NonNullable['failure'], ): boolean { @@ -157,6 +192,7 @@ export function WorldLab() { const [verifyingModelId, setVerifyingModelId] = useState(null); const [cancelling, setCancelling] = useState(false); const [chatCollapsed, setChatCollapsed] = useState(false); + const [activityDockLoaded, setActivityDockLoaded] = useState(false); const [followTurn, setFollowTurn] = useState(true); const [followPreferenceLoaded, setFollowPreferenceLoaded] = useState(false); const inFlightRef = useRef(false); @@ -182,23 +218,34 @@ export function WorldLab() { }, [running]); useEffect(() => { - const collapsed = window.localStorage.getItem(activityDockStorageKey); - const hydrationTask = window.setTimeout( - () => setChatCollapsed(collapsed === 'collapsed'), - 0, + const collapsed = readStoredPreference( + window.localStorage, + activityDockStorageKey, + legacyActivityDockStorageKey, + (value) => value === 'collapsed' || value === 'expanded', ); + const hydrationTask = window.setTimeout(() => { + setChatCollapsed(collapsed === 'collapsed'); + setActivityDockLoaded(true); + }, 0); return () => window.clearTimeout(hydrationTask); }, []); useEffect(() => { + if (!activityDockLoaded) return; window.localStorage.setItem( activityDockStorageKey, chatCollapsed ? 'collapsed' : 'expanded', ); - }, [chatCollapsed]); + }, [activityDockLoaded, chatCollapsed]); useEffect(() => { - const saved = window.localStorage.getItem(unattendedRecoveryStorageKey); + const saved = readStoredPreference( + window.localStorage, + unattendedRecoveryStorageKey, + legacyUnattendedRecoveryStorageKey, + validUnattendedPreference, + ); const hydrationTask = window.setTimeout(() => { if (saved) { try { @@ -249,7 +296,15 @@ export function WorldLab() { }, [recoveryNotice]); useEffect(() => { - const stored = Number(window.sessionStorage.getItem(runTargetStorageKey)); + const stored = Number( + readStoredPreference( + window.sessionStorage, + runTargetStorageKey, + legacyRunTargetStorageKey, + (value) => + runTargets.includes(Number(value) as (typeof runTargets)[number]), + ), + ); const hydrationTask = window.setTimeout(() => { if (runTargets.includes(stored as (typeof runTargets)[number])) { setRunTarget(stored as (typeof runTargets)[number]); @@ -266,7 +321,12 @@ export function WorldLab() { useEffect(() => { const storedFollowTurn = - window.localStorage.getItem(followTurnStorageKey) !== 'false'; + readStoredPreference( + window.localStorage, + followTurnStorageKey, + legacyFollowTurnStorageKey, + (value) => value === 'true' || value === 'false', + ) !== 'false'; const hydrationTask = window.setTimeout(() => { setFollowTurn(storedFollowTurn); setFollowPreferenceLoaded(true); @@ -2188,7 +2248,7 @@ function WorldSetupPanel({ method: 'POST', headers: { 'content-type': 'application/json', - 'x-agentborne-mutation-id': `setup-${Date.now()}`, + 'x-hexzero-mutation-id': `setup-${Date.now()}`, }, body: key, }); @@ -4711,7 +4771,7 @@ function ExperimentExportPanel({ ? `latest-${document.filters.turns.count}` : 'entire-retained'; link.href = url; - link.download = `agentborne-experiment-${document.experiment.id}-${scope}-${range}.json`; + link.download = `hexzero-experiment-${document.experiment.id}-${scope}-${range}.json`; link.click(); URL.revokeObjectURL(url); setNotice('Export JSON download started.'); diff --git a/apps/world-lab/src/components/world-map.tsx b/apps/world-lab/src/components/world-map.tsx index 68138ba..9261f26 100644 --- a/apps/world-lab/src/components/world-map.tsx +++ b/apps/world-lab/src/components/world-map.tsx @@ -21,7 +21,7 @@ import type { HexState, Alliance, SimulationSnapshot, -} from '@agentborne/shared'; +} from '@hexzero/shared'; import { resolveAgentColor } from './ui-color'; import { DARK_TILE_ATTRIBUTION, DARK_TILE_URLS } from './map-config'; diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index bb20376..3fb0fe2 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -119,7 +119,7 @@ The catalog has an eight-second timeout and five-minute in-memory TTL. A success Every agent resolves an explicit global assignment or per-agent override before execution, including its reasoning profile. The acting agent's resolved slug and profile are passed to its request. Assignments may change while playback is paused and no provider/reset mutation is active. Each change is exported with timestamp, scope, prior/new slug, prior/new reasoning profile, and effective next-turn boundary. No unavailable model/profile or missing model is substituted. -The centralized 75-second provider abort timeout covers the complete response lifecycle, including body reading, response decoding, bounded JSON extraction/repair, normalization, and schema validation, and is cleared after every outcome. The same AbortController supports an explicit non-turn-consuming operator cancellation. Safe records expose only bounded status/code/message/request ID/model/finish-reason/latency/usage fields. Scripted providers are explicit deterministic seams selected only by tests or `AGENTBORNE_PROVIDER=scripted`; there is no automatic fallback. Manual probes use the exact text/flat-JSON contract and selected reasoning profile, never mutate or advance the world, may incur a small charge, and are cached only for the current server session by model ID, reasoning profile, and contract version. +The centralized 75-second provider abort timeout covers the complete response lifecycle, including body reading, response decoding, bounded JSON extraction/repair, normalization, and schema validation, and is cleared after every outcome. The same AbortController supports an explicit non-turn-consuming operator cancellation. Safe records expose only bounded status/code/message/request ID/model/finish-reason/latency/usage fields. Scripted providers are explicit deterministic seams selected only by tests or `HEXZERO_PROVIDER=scripted`; there is no automatic fallback. Manual probes use the exact text/flat-JSON contract and selected reasoning profile, never mutate or advance the world, may incur a small charge, and are cached only for the current server session by model ID, reasoning profile, and contract version. The deadline is shared by both permitted automatic attempts rather than renewed per call. Turn and Retry browser mutations carry bounded client operation IDs, diff --git a/docs/EXPERIMENT_ARCHIVE.md b/docs/EXPERIMENT_ARCHIVE.md index 4569c19..9d8022e 100644 --- a/docs/EXPERIMENT_ARCHIVE.md +++ b/docs/EXPERIMENT_ARCHIVE.md @@ -4,9 +4,14 @@ The experiment archive is a durable, local research surface for completed or par ## Storage and configuration -`@agentborne/experiment-archive` uses SQLite built into the pinned Node 24 runtime. Versioned migrations create strict tables with foreign keys and indexes. File-backed databases enable WAL and a five-second busy timeout; imports use prepared statements inside one transaction. Tests use in-memory or temporary databases. +`@hexzero/experiment-archive` uses SQLite built into the pinned Node 24 runtime. Versioned migrations create strict tables with foreign keys and indexes. File-backed databases enable WAL and a five-second busy timeout; imports use prepared statements inside one transaction. Tests use in-memory or temporary databases. -The default database is `.agentborne/experiments.sqlite`, an ignored development path. Override it with `--db ` or `AGENTBORNE_EXPERIMENT_DB`; the flag takes precedence. +The new default database is `.hexzero/experiments.sqlite`, an ignored development path. Resolution order is explicit `--db`, `HEXZERO_EXPERIMENT_DB`, legacy `AGENTBORNE_EXPERIMENT_DB`, an existing `.hexzero/experiments.sqlite`, an existing `.agentborne/experiments.sqlite`, then a new `.hexzero/experiments.sqlite`. Legacy selections emit a concise notice and open normally; no database is moved, overwritten, or recreated for branding. + +For an optional manual migration, stop all Hex Zero processes, create the +`.hexzero` directory, copy `experiments.sqlite` and any matching `-wal` and +`-shm` sidecars from `.agentborne`, verify the copied archive opens, and only +then remove the legacy files if desired. The schema normalizes experiments, source exports, roster/assignments, topology, turns, model attempts, communications and recipients, diplomacy attempts, alliance events, world events, configuration changes, and research notes. Stable source IDs are retained; deterministic experiment/turn/attempt IDs make imports idempotent. Source aggregate metrics remain audit evidence, while summaries derive canonical metrics from stored records. diff --git a/docs/GAMEPLAY_FOUNDATION.md b/docs/GAMEPLAY_FOUNDATION.md index 6e3c193..d5609bd 100644 --- a/docs/GAMEPLAY_FOUNDATION.md +++ b/docs/GAMEPLAY_FOUNDATION.md @@ -35,7 +35,7 @@ current sequential turn model until the later simultaneous-tick milestone. ## Product goal -Agentborne should remain mechanically simple. Replayability should emerge from +Hex Zero should remain mechanically simple. Replayability should emerge from real geography, hidden agent locations, visible infection trails, persistent 24/7 agent activity, model personalities and strategies, alliances and communication, and human intervention. diff --git a/docs/SECURITY.md b/docs/SECURITY.md index 6143ea3..2ee5fa8 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -65,7 +65,7 @@ Export requests, agent IDs, levels, ranges, outcome/world-action filters, commun Actual cost is accepted only from OpenRouter's safe `usage.cost`. Missing cost is unknown, never zero; scripted-test providers explicitly report zero. The active Game API still has no authentication, budget enforcement, restartable persistence, provider-management endpoint, upload, or sharing link. The loopback-only boundary remains mandatory. -The offline experiment archive adds local persistence only for complete schema-validated safe exports and explicitly curated Markdown notes. Imports scan for prohibited credential/private-reasoning fields and recognizable credential values before a transaction begins; failures roll back. The default `.agentborne/` database is ignored. The CLI exposes bounded typed queries, not arbitrary SQL, and adds no MCP, embedding, vector-store, or network-listener surface. +The offline experiment archive adds local persistence only for complete schema-validated safe exports and explicitly curated Markdown notes. Imports scan for prohibited credential/private-reasoning fields and recognizable credential values before a transaction begins; failures roll back. Both the canonical `.hexzero/` and compatible legacy `.agentborne/` database locations are ignored. The CLI exposes bounded typed queries, not arbitrary SQL, and adds no MCP, embedding, vector-store, or network-listener surface. ## Reporting diff --git a/docs/TESTING.md b/docs/TESTING.md index b31270c..9dc880b 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -34,7 +34,7 @@ pnpm exec playwright install chromium # only if Chromium is not already install pnpm test:e2e ``` -`pnpm validate` aggregates formatting, lint, type checking, unit/integration tests, and builds. Playwright remains separate. Its web server starts both applications with the explicit deterministic provider; `AGENTBORNE_PROVIDER=scripted` is never an implicit OpenRouter fallback. +`pnpm validate` aggregates formatting, lint, type checking, unit/integration tests, and builds. Playwright remains separate. Its web server starts both applications with the explicit deterministic provider; `HEXZERO_PROVIDER=scripted` is never an implicit OpenRouter fallback. Environment coverage verifies that the canonical provider and archive variables override their deprecated `AGENTBORNE_` aliases. ## Coverage @@ -104,4 +104,4 @@ The separately opted-in `pnpm smoke:openrouter -- ` comma World Lab also offers an explicit “Test selected model” probe. It uses the production text/flat-JSON contract and selected reasoning profile, does not advance or mutate the world, may incur a small charge, and is cached by model plus profile plus contract version. It is never invoked by deterministic validation or CI. -Experiment-archive tests generate current schema-v9 exports through the deterministic scripted provider and cover migration/reopen, transactional rollback, stable idempotent identifiers, normalized round trips, bounded filters, summary/comparison calculations, canonical metric agreement with preserved source inconsistencies, Patient Zero classifications, retention completeness, FTS notes/supersession, explicit persistence failures, and credential/private-reasoning rejection. They use only in-memory or temporary SQLite databases and make no live provider or geocoding requests. +Experiment-archive tests generate current schema-v9 exports through the deterministic scripted provider and cover migration/reopen, canonical and legacy path resolution, legacy export filenames, transactional rollback, stable idempotent identifiers, normalized round trips, bounded filters, summary/comparison calculations, canonical metric agreement with preserved source inconsistencies, Patient Zero classifications, retention completeness, FTS notes/supersession, explicit persistence failures, and credential/private-reasoning rejection. They use only in-memory or temporary SQLite databases and make no live provider or geocoding requests. diff --git a/docs/adr/0009-capability-driven-model-catalog.md b/docs/adr/0009-capability-driven-model-catalog.md index ca7cee7..bbe0a81 100644 --- a/docs/adr/0009-capability-driven-model-catalog.md +++ b/docs/adr/0009-capability-driven-model-catalog.md @@ -6,7 +6,7 @@ Accepted ## Decision -Agentborne discovers models through OpenRouter's server-side models API and accepts a model only when local validation confirms text input, text output, chat completions, advertised `max_tokens`, and at least 16,384 context tokens. Requests are non-streaming plain-text chat completions. The prompt requires exactly one deliberately flat JSON object whose sentinel-bearing fields normalize into the existing local decision unions. +Hex Zero discovers models through OpenRouter's server-side models API and accepts a model only when local validation confirms text input, text output, chat completions, advertised `max_tokens`, and at least 16,384 context tokens. Requests are non-streaming plain-text chat completions. The prompt requires exactly one deliberately flat JSON object whose sentinel-bearing fields normalize into the existing local decision unions. The context floor, 4,096-token completion ceiling, 75-second provider timeout, and decision-contract version are centralized in `packages/shared`. The complete observation is bounded: eight-agent summaries, at most 12 public and six relevant direct messages, bounded event windows, a 600-character personality, and 280-character messages. A 16,384-token context therefore leaves substantial headroom for that fixed prompt and observation while reserving the full completion ceiling for the JSON response. diff --git a/package.json b/package.json index 591786a..9bb3b54 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "agentborne", + "name": "hexzero", "version": "0.0.0", "private": true, "packageManager": "pnpm@11.21.0", @@ -9,16 +9,16 @@ }, "scripts": { "build": "pnpm -r --if-present build", - "dev": "pnpm --parallel --filter @agentborne/game-api --filter @agentborne/world-lab dev", - "dev:test-provider": "AGENTBORNE_PROVIDER=scripted pnpm dev", - "dev:api": "pnpm --filter @agentborne/game-api dev", - "experiment:db": "pnpm --filter @agentborne/experiment-archive cli", + "dev": "pnpm --parallel --filter @hexzero/game-api --filter @hexzero/world-lab dev", + "dev:test-provider": "HEXZERO_PROVIDER=scripted pnpm dev", + "dev:api": "pnpm --filter @hexzero/game-api dev", + "experiment:db": "pnpm --filter @hexzero/experiment-archive cli", "format": "prettier --write .", "format:check": "prettier --check .", "lint": "pnpm -r --if-present lint", "test": "pnpm -r --if-present test", "test:e2e": "playwright test", - "smoke:openrouter": "pnpm --filter @agentborne/agent-runtime smoke:openrouter", + "smoke:openrouter": "pnpm --filter @hexzero/agent-runtime smoke:openrouter", "typecheck": "pnpm -r --if-present typecheck", "validate": "pnpm format:check && pnpm lint && pnpm typecheck && pnpm test && pnpm build" }, diff --git a/packages/agent-runtime/package.json b/packages/agent-runtime/package.json index 894a57c..2ef4a96 100644 --- a/packages/agent-runtime/package.json +++ b/packages/agent-runtime/package.json @@ -1,5 +1,5 @@ { - "name": "@agentborne/agent-runtime", + "name": "@hexzero/agent-runtime", "version": "0.0.0", "private": true, "type": "module", @@ -14,7 +14,7 @@ "typecheck": "tsc -p tsconfig.json" }, "dependencies": { - "@agentborne/shared": "workspace:*", + "@hexzero/shared": "workspace:*", "h3-js": "4.5.0", "zod": "4.4.3" }, diff --git a/packages/agent-runtime/src/index.test.ts b/packages/agent-runtime/src/index.test.ts index a5ae471..503fd86 100644 --- a/packages/agent-runtime/src/index.test.ts +++ b/packages/agent-runtime/src/index.test.ts @@ -4,7 +4,7 @@ import { OPENROUTER_MAX_OUTPUT_TOKENS, OPENROUTER_PROVIDER_TIMEOUT_MS, agentObservationSchema, -} from '@agentborne/shared'; +} from '@hexzero/shared'; import { AgentProviderError, OpenRouterAgentProvider, @@ -1095,7 +1095,7 @@ describe('OpenRouter provider environment', () => { applyProviderEnvironmentFile( [ 'OPENROUTER_API_KEY=file-key', - 'AGENTBORNE_MODEL=ignored/legacy-value', + 'HEXZERO_MODEL=ignored/legacy-value', 'NEXT_PUBLIC_GAME_API_BASE_URL=https://browser.example', ].join('\n'), environment, diff --git a/packages/agent-runtime/src/index.ts b/packages/agent-runtime/src/index.ts index 58c5d27..5b401cc 100644 --- a/packages/agent-runtime/src/index.ts +++ b/packages/agent-runtime/src/index.ts @@ -14,7 +14,7 @@ import { type ProviderDecisionEnvelope, type ProviderMetadata, type ReasoningProfile, -} from '@agentborne/shared'; +} from '@hexzero/shared'; export { applyProviderEnvironmentFile } from './provider-environment'; export * from './model-catalog'; diff --git a/packages/agent-runtime/src/model-catalog.test.ts b/packages/agent-runtime/src/model-catalog.test.ts index b95311a..002af8d 100644 --- a/packages/agent-runtime/src/model-catalog.test.ts +++ b/packages/agent-runtime/src/model-catalog.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it, vi } from 'vitest'; import { OPENROUTER_MODEL_CONTEXT_MINIMUM, OPENROUTER_REQUIRED_PARAMETERS, -} from '@agentborne/shared'; +} from '@hexzero/shared'; import { OpenRouterModelCatalog, sanitizeCompatibleModel, diff --git a/packages/agent-runtime/src/model-catalog.ts b/packages/agent-runtime/src/model-catalog.ts index 95376b8..44a4e11 100644 --- a/packages/agent-runtime/src/model-catalog.ts +++ b/packages/agent-runtime/src/model-catalog.ts @@ -6,7 +6,7 @@ import { OPENROUTER_REQUIRED_PARAMETERS, type CompatibleModel, type ModelCatalogResponse, -} from '@agentborne/shared'; +} from '@hexzero/shared'; export const OPENROUTER_MODELS_ENDPOINT = 'https://openrouter.ai/api/v1/models'; export const DEFAULT_MODEL_CATALOG_TTL_MS = 5 * 60 * 1_000; diff --git a/packages/agent-runtime/src/real-provider-smoke.ts b/packages/agent-runtime/src/real-provider-smoke.ts index fbf27a3..330ec80 100644 --- a/packages/agent-runtime/src/real-provider-smoke.ts +++ b/packages/agent-runtime/src/real-provider-smoke.ts @@ -4,7 +4,7 @@ import { agentIdSchema, agentObservationSchema, h3CellSchema, -} from '@agentborne/shared'; +} from '@hexzero/shared'; import { AgentProviderError, OpenRouterAgentProvider, diff --git a/packages/experiment-archive/package.json b/packages/experiment-archive/package.json index 816ebfc..78a94a9 100644 --- a/packages/experiment-archive/package.json +++ b/packages/experiment-archive/package.json @@ -1,5 +1,5 @@ { - "name": "@agentborne/experiment-archive", + "name": "@hexzero/experiment-archive", "version": "0.0.0", "private": true, "type": "module", @@ -14,11 +14,11 @@ "typecheck": "tsc -p tsconfig.json" }, "dependencies": { - "@agentborne/shared": "workspace:*" + "@hexzero/shared": "workspace:*" }, "devDependencies": { - "@agentborne/agent-runtime": "workspace:*", - "@agentborne/world-engine": "workspace:*", + "@hexzero/agent-runtime": "workspace:*", + "@hexzero/world-engine": "workspace:*", "tsx": "4.23.12", "vitest": "4.1.10" } diff --git a/packages/experiment-archive/src/archive.test.ts b/packages/experiment-archive/src/archive.test.ts index 451f04b..48efea8 100644 --- a/packages/experiment-archive/src/archive.test.ts +++ b/packages/experiment-archive/src/archive.test.ts @@ -1,31 +1,33 @@ -import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs'; +import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import type { - AgentProvider, - ProviderDecision, -} from '@agentborne/agent-runtime'; +import type { AgentProvider, ProviderDecision } from '@hexzero/agent-runtime'; import { experimentExportDocumentSchema, type AgentObservation, type ExperimentExportDocument, -} from '@agentborne/shared'; -import { defaultWorldSetupRequest } from '@agentborne/world-engine'; -import { describe, expect, it } from 'vitest'; +} from '@hexzero/shared'; +import { defaultWorldSetupRequest } from '@hexzero/world-engine'; +import { afterEach, describe, expect, it, vi } from 'vitest'; import { SimulationService } from '../../../apps/game-api/src/simulation-service.js'; import { ArchiveDatabase, + DATABASE_PATH_ENV, ExperimentImportError, ExperimentQueryService, + LEGACY_DATABASE_PATH_ENV, ResearchNoteService, importExperimentExport, + resolveArchivePath, } from './index.js'; const EXPERIMENT_ID = '10000000-0000-4000-8000-000000000001'; const NOW = '2026-08-20T12:00:00.000Z'; +afterEach(() => vi.unstubAllEnvs()); + function nextUuid() { let counter = 10; return () => `20000000-0000-4000-8000-${String(counter++).padStart(12, '0')}`; @@ -105,10 +107,73 @@ async function currentExport(): Promise { } function temporaryPath(name: string): string { - return join(mkdtempSync(join(tmpdir(), 'agentborne-archive-')), name); + return join(mkdtempSync(join(tmpdir(), 'hexzero-archive-')), name); } describe('experiment archive', () => { + it('resolves canonical and legacy archive locations without moving either database', () => { + const root = temporaryPath('workspace'); + mkdirSync(join(root, '.agentborne'), { recursive: true }); + const legacy = join(root, '.agentborne', 'experiments.sqlite'); + writeFileSync(legacy, 'legacy-database'); + vi.stubEnv('INIT_CWD', root); + + expect(resolveArchivePath()).toEqual({ + path: '.agentborne/experiments.sqlite', + source: 'legacy-default', + }); + expect(readFileSync(legacy, 'utf8')).toBe('legacy-database'); + expect( + resolveArchivePath({ + environment: { [LEGACY_DATABASE_PATH_ENV]: 'legacy-env.sqlite' }, + }), + ).toEqual({ + path: 'legacy-env.sqlite', + source: 'legacy-environment', + }); + expect( + resolveArchivePath({ + environment: { + [DATABASE_PATH_ENV]: 'canonical.sqlite', + [LEGACY_DATABASE_PATH_ENV]: 'legacy-env.sqlite', + }, + }), + ).toEqual({ path: 'canonical.sqlite', source: 'environment' }); + + mkdirSync(join(root, '.hexzero'), { recursive: true }); + writeFileSync(join(root, '.hexzero', 'experiments.sqlite'), 'canonical'); + expect(resolveArchivePath()).toEqual({ + path: '.hexzero/experiments.sqlite', + source: 'default', + }); + expect(readFileSync(legacy, 'utf8')).toBe('legacy-database'); + }); + + it('defaults a new archive to .hexzero without creating or moving a legacy database', () => { + const root = temporaryPath('fresh-workspace'); + mkdirSync(root, { recursive: true }); + vi.stubEnv('INIT_CWD', root); + const archive = new ArchiveDatabase({ environment: {} }); + expect(archive.path).toBe(join(root, '.hexzero', 'experiments.sqlite')); + expect(() => + readFileSync(join(root, '.agentborne', 'experiments.sqlite')), + ).toThrow(); + archive.close(); + }); + + it('warns without exposing values when the legacy archive variable is selected', () => { + const warn = vi.fn(); + const archive = new ArchiveDatabase({ + environment: { [LEGACY_DATABASE_PATH_ENV]: ':memory:' }, + warn, + }); + expect(warn).toHaveBeenCalledWith( + 'AGENTBORNE_EXPERIMENT_DB is deprecated; use HEXZERO_EXPERIMENT_DB. Continuing with the legacy setting.', + ); + expect(warn.mock.calls.flat().join(' ')).not.toContain(':memory:'); + archive.close(); + }); + it('migrates a fresh database and reopens it without replaying migrations', () => { const path = temporaryPath('archive.sqlite'); const first = new ArchiveDatabase({ path, clock: () => new Date(NOW) }); @@ -162,6 +227,16 @@ describe('experiment archive', () => { archive.close(); }); + it('imports an existing schema-v9 export with the legacy download filename', async () => { + const path = temporaryPath( + 'agentborne-experiment-existing-full-entire.json', + ); + writeFileSync(path, JSON.stringify(await currentExport())); + const archive = new ArchiveDatabase({ path: ':memory:' }); + expect(importExperimentExport(archive, path).rejected).toBe(0); + archive.close(); + }); + it('rolls back malformed and unsupported exports without leaving partial state', async () => { const archive = new ArchiveDatabase({ path: ':memory:' }); const malformed = temporaryPath('malformed.json'); diff --git a/packages/experiment-archive/src/cli.ts b/packages/experiment-archive/src/cli.ts index b45ee70..6912fb8 100644 --- a/packages/experiment-archive/src/cli.ts +++ b/packages/experiment-archive/src/cli.ts @@ -195,7 +195,8 @@ const HELP = `Usage: pnpm experiment:db notes search [--type type] [--status status] [--tag tag] [--experiment id] [--limit n] pnpm experiment:db notes list [filters] -Database path defaults to .agentborne/experiments.sqlite and can also be set with AGENTBORNE_EXPERIMENT_DB. +Database path defaults to .hexzero/experiments.sqlite and can also be set with HEXZERO_EXPERIMENT_DB. +Existing .agentborne/experiments.sqlite and AGENTBORNE_EXPERIMENT_DB remain supported with a migration notice. Detail commands default to 50 rows and enforce a maximum of 500. Arbitrary SQL is not supported.`; async function main(): Promise { diff --git a/packages/experiment-archive/src/database.ts b/packages/experiment-archive/src/database.ts index eacb391..f7d6d5d 100644 --- a/packages/experiment-archive/src/database.ts +++ b/packages/experiment-archive/src/database.ts @@ -1,11 +1,13 @@ -import { mkdirSync } from 'node:fs'; +import { existsSync, mkdirSync } from 'node:fs'; import { dirname, resolve } from 'node:path'; import { DatabaseSync } from 'node:sqlite'; import { migrations } from './migrations.js'; -export const DEFAULT_DATABASE_PATH = '.agentborne/experiments.sqlite'; -export const DATABASE_PATH_ENV = 'AGENTBORNE_EXPERIMENT_DB'; +export const DEFAULT_DATABASE_PATH = '.hexzero/experiments.sqlite'; +export const LEGACY_DATABASE_PATH = '.agentborne/experiments.sqlite'; +export const DATABASE_PATH_ENV = 'HEXZERO_EXPERIMENT_DB'; +export const LEGACY_DATABASE_PATH_ENV = 'AGENTBORNE_EXPERIMENT_DB'; export function archiveInvocationRoot(): string { return process.env.INIT_CWD?.trim() || process.cwd(); @@ -14,6 +16,34 @@ export function archiveInvocationRoot(): string { export interface OpenArchiveOptions { path?: string; clock?: () => Date; + environment?: NodeJS.ProcessEnv; + warn?: (message: string) => void; +} + +export interface ResolvedArchivePath { + path: string; + source: + 'cli' | 'environment' | 'legacy-environment' | 'default' | 'legacy-default'; +} + +export function resolveArchivePath( + options: Pick = {}, +): ResolvedArchivePath { + if (options.path !== undefined) return { path: options.path, source: 'cli' }; + const environment = options.environment ?? process.env; + if (environment[DATABASE_PATH_ENV] !== undefined) + return { path: environment[DATABASE_PATH_ENV]!, source: 'environment' }; + if (environment[LEGACY_DATABASE_PATH_ENV] !== undefined) + return { + path: environment[LEGACY_DATABASE_PATH_ENV]!, + source: 'legacy-environment', + }; + const root = archiveInvocationRoot(); + if (existsSync(resolve(root, DEFAULT_DATABASE_PATH))) + return { path: DEFAULT_DATABASE_PATH, source: 'default' }; + if (existsSync(resolve(root, LEGACY_DATABASE_PATH))) + return { path: LEGACY_DATABASE_PATH, source: 'legacy-default' }; + return { path: DEFAULT_DATABASE_PATH, source: 'default' }; } export class ArchivePersistenceError extends Error { @@ -32,12 +62,19 @@ export class ArchiveDatabase { readonly clock: () => Date; constructor(options: OpenArchiveOptions = {}) { - const configured = - options.path ?? process.env[DATABASE_PATH_ENV] ?? DEFAULT_DATABASE_PATH; + const configured = resolveArchivePath(options); + if (configured.source === 'legacy-environment') + (options.warn ?? console.warn)( + `${LEGACY_DATABASE_PATH_ENV} is deprecated; use ${DATABASE_PATH_ENV}. Continuing with the legacy setting.`, + ); + if (configured.source === 'legacy-default') + (options.warn ?? console.warn)( + `Using the existing legacy archive at ${LEGACY_DATABASE_PATH}. Optionally move it to ${DEFAULT_DATABASE_PATH} while Hex Zero is stopped.`, + ); this.path = - configured === ':memory:' - ? configured - : resolve(archiveInvocationRoot(), configured); + configured.path === ':memory:' + ? configured.path + : resolve(archiveInvocationRoot(), configured.path); this.clock = options.clock ?? (() => new Date()); try { if (this.path !== ':memory:') diff --git a/packages/experiment-archive/src/importer.ts b/packages/experiment-archive/src/importer.ts index a786c6c..5d12b5f 100644 --- a/packages/experiment-archive/src/importer.ts +++ b/packages/experiment-archive/src/importer.ts @@ -7,7 +7,7 @@ import { experimentExportDocumentSchema, exportedCommunicationSchema, type ExperimentExportDocument, -} from '@agentborne/shared'; +} from '@hexzero/shared'; import { ArchiveDatabase, diff --git a/packages/shared/package.json b/packages/shared/package.json index ee4d6dc..5da0fa6 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,5 +1,5 @@ { - "name": "@agentborne/shared", + "name": "@hexzero/shared", "version": "0.0.0", "private": true, "type": "module", diff --git a/packages/shared/src/index.test.ts b/packages/shared/src/index.test.ts index 0ef2406..7e53d8c 100644 --- a/packages/shared/src/index.test.ts +++ b/packages/shared/src/index.test.ts @@ -1,6 +1,8 @@ import { describe, expect, it } from 'vitest'; import { MODEL_SUMMARY_MAX_LENGTH, + AGENT_DECISION_CONTRACT_VERSION, + OBJECTIVE_PROMPT_VERSION, MESSAGE_MAX_LENGTH, PERSONALITY_MAX_LENGTH, apiErrorSchema, @@ -242,6 +244,11 @@ const snapshot = { }; describe('agent observation and decision schemas', () => { + it('preserves established engine contract identifiers through branding changes', () => { + expect(AGENT_DECISION_CONTRACT_VERSION).toBe('text-flat-json-v3'); + expect(OBJECTIVE_PROMPT_VERSION).toBe('durable-influence-v2'); + }); + it('centralizes eight-agent, 127-cell alliance and diplomacy limits', () => { expect(DEVELOPMENT_WORLD_CONFIG).toMatchObject({ radius: 6, diff --git a/packages/world-engine/package.json b/packages/world-engine/package.json index 7509042..42641a5 100644 --- a/packages/world-engine/package.json +++ b/packages/world-engine/package.json @@ -1,5 +1,5 @@ { - "name": "@agentborne/world-engine", + "name": "@hexzero/world-engine", "version": "0.0.0", "private": true, "type": "module", @@ -13,7 +13,7 @@ "typecheck": "tsc -p tsconfig.json" }, "dependencies": { - "@agentborne/shared": "workspace:*", + "@hexzero/shared": "workspace:*", "h3-js": "4.5.0" }, "devDependencies": { diff --git a/packages/world-engine/src/index.test.ts b/packages/world-engine/src/index.test.ts index 9f2cfa9..197e7b0 100644 --- a/packages/world-engine/src/index.test.ts +++ b/packages/world-engine/src/index.test.ts @@ -1,6 +1,6 @@ import { gridDisk, gridDistance, latLngToCell } from 'h3-js'; import { describe, expect, it } from 'vitest'; -import { agentIdSchema, h3CellSchema, type Agent } from '@agentborne/shared'; +import { agentIdSchema, h3CellSchema, type Agent } from '@hexzero/shared'; import { applyCommunication, applyDiplomacy, diff --git a/packages/world-engine/src/index.ts b/packages/world-engine/src/index.ts index ab48ff8..2547d39 100644 --- a/packages/world-engine/src/index.ts +++ b/packages/world-engine/src/index.ts @@ -43,7 +43,7 @@ import { type ScenarioRosterEntry, type WorldSetupPreviewResponse, type WorldSetupRequest, -} from '@agentborne/shared'; +} from '@hexzero/shared'; export interface WorldState { readonly hexes: ReadonlyMap; diff --git a/packages/world-engine/src/scenario.test.ts b/packages/world-engine/src/scenario.test.ts index 1764585..5a5138f 100644 --- a/packages/world-engine/src/scenario.test.ts +++ b/packages/world-engine/src/scenario.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { assignBehavior, WORLD_RADIUS_PRESETS } from '@agentborne/shared'; +import { assignBehavior, WORLD_RADIUS_PRESETS } from '@hexzero/shared'; import { DEVELOPMENT_AGENT_BLUEPRINTS, allocateDeterministicSpawns, diff --git a/playwright.config.ts b/playwright.config.ts index f26c8fa..cb778ab 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -14,15 +14,14 @@ export default defineConfig({ }, webServer: [ { - command: - 'AGENTBORNE_PROVIDER=scripted pnpm --filter @agentborne/game-api dev', + command: 'HEXZERO_PROVIDER=scripted pnpm --filter @hexzero/game-api dev', url: 'http://127.0.0.1:8787/health', reuseExistingServer: false, timeout: 120_000, }, { command: - 'NEXT_PUBLIC_GAME_API_BASE_URL=http://127.0.0.1:8787/api/simulation pnpm --filter @agentborne/world-lab dev', + 'NEXT_PUBLIC_GAME_API_BASE_URL=http://127.0.0.1:8787/api/simulation pnpm --filter @hexzero/world-lab dev', url: baseURL, reuseExistingServer: false, timeout: 120_000, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0255ea2..b0a2a02 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,13 +28,13 @@ importers: apps/game-api: dependencies: - '@agentborne/agent-runtime': + '@hexzero/agent-runtime': specifier: workspace:* version: link:../../packages/agent-runtime - '@agentborne/shared': + '@hexzero/shared': specifier: workspace:* version: link:../../packages/shared - '@agentborne/world-engine': + '@hexzero/world-engine': specifier: workspace:* version: link:../../packages/world-engine '@hono/node-server': @@ -62,10 +62,10 @@ importers: apps/world-lab: dependencies: - '@agentborne/shared': + '@hexzero/shared': specifier: workspace:* version: link:../../packages/shared - '@agentborne/world-engine': + '@hexzero/world-engine': specifier: workspace:* version: link:../../packages/world-engine h3-js: @@ -111,7 +111,7 @@ importers: packages/agent-runtime: dependencies: - '@agentborne/shared': + '@hexzero/shared': specifier: workspace:* version: link:../shared h3-js: @@ -130,14 +130,14 @@ importers: packages/experiment-archive: dependencies: - '@agentborne/shared': + '@hexzero/shared': specifier: workspace:* version: link:../shared devDependencies: - '@agentborne/agent-runtime': + '@hexzero/agent-runtime': specifier: workspace:* version: link:../agent-runtime - '@agentborne/world-engine': + '@hexzero/world-engine': specifier: workspace:* version: link:../world-engine tsx: @@ -159,7 +159,7 @@ importers: packages/world-engine: dependencies: - '@agentborne/shared': + '@hexzero/shared': specifier: workspace:* version: link:../shared h3-js: diff --git a/tests/e2e/world-lab.spec.ts b/tests/e2e/world-lab.spec.ts index 5b8455b..3bb52be 100644 --- a/tests/e2e/world-lab.spec.ts +++ b/tests/e2e/world-lab.spec.ts @@ -1,6 +1,6 @@ import { expect, test } from '@playwright/test'; import { readFile } from 'node:fs/promises'; -import { experimentExportDocumentSchema } from '@agentborne/shared'; +import { experimentExportDocumentSchema } from '@hexzero/shared'; const ROOK_ID = '2507bb46-7ae4-45ca-8dda-644c4f85ca14'; const EMBER_ID = '128f3f38-6b7d-4db7-9e95-751b4ce2681e';