diff --git a/.agents/skills/using-git-worktrees/SKILL.md b/.agents/skills/using-git-worktrees/SKILL.md new file mode 100644 index 0000000..e153843 --- /dev/null +++ b/.agents/skills/using-git-worktrees/SKILL.md @@ -0,0 +1,218 @@ +--- +name: using-git-worktrees +description: Use when starting feature work that needs isolation from current workspace or before executing implementation plans - creates isolated git worktrees with smart directory selection and safety verification +--- + +# Using Git Worktrees + +## Overview + +Git worktrees create isolated workspaces sharing the same repository, allowing work on multiple branches simultaneously without switching. + +**Core principle:** Systematic directory selection + safety verification = reliable isolation. + +**Announce at start:** "I'm using the using-git-worktrees skill to set up an isolated workspace." + +## Directory Selection Process + +Follow this priority order: + +### 1. Check Existing Directories + +```bash +# Check in priority order +ls -d .worktrees 2>/dev/null # Preferred (hidden) +ls -d worktrees 2>/dev/null # Alternative +``` + +**If found:** Use that directory. If both exist, `.worktrees` wins. + +### 2. Check CLAUDE.md + +```bash +grep -i "worktree.*director" CLAUDE.md 2>/dev/null +``` + +**If preference specified:** Use it without asking. + +### 3. Ask User + +If no directory exists and no CLAUDE.md preference: + +``` +No worktree directory found. Where should I create worktrees? + +1. .worktrees/ (project-local, hidden) +2. ~/.config/superpowers/worktrees// (global location) + +Which would you prefer? +``` + +## Safety Verification + +### For Project-Local Directories (.worktrees or worktrees) + +**MUST verify directory is ignored before creating worktree:** + +```bash +# Check if directory is ignored (respects local, global, and system gitignore) +git check-ignore -q .worktrees 2>/dev/null || git check-ignore -q worktrees 2>/dev/null +``` + +**If NOT ignored:** + +Per Jesse's rule "Fix broken things immediately": +1. Add appropriate line to .gitignore +2. Commit the change +3. Proceed with worktree creation + +**Why critical:** Prevents accidentally committing worktree contents to repository. + +### For Global Directory (~/.config/superpowers/worktrees) + +No .gitignore verification needed - outside project entirely. + +## Creation Steps + +### 1. Detect Project Name + +```bash +project=$(basename "$(git rev-parse --show-toplevel)") +``` + +### 2. Create Worktree + +```bash +# Determine full path +case $LOCATION in + .worktrees|worktrees) + path="$LOCATION/$BRANCH_NAME" + ;; + ~/.config/superpowers/worktrees/*) + path="~/.config/superpowers/worktrees/$project/$BRANCH_NAME" + ;; +esac + +# Create worktree with new branch +git worktree add "$path" -b "$BRANCH_NAME" +cd "$path" +``` + +### 3. Run Project Setup + +Auto-detect and run appropriate setup: + +```bash +# Node.js +if [ -f package.json ]; then npm install; fi + +# Rust +if [ -f Cargo.toml ]; then cargo build; fi + +# Python +if [ -f requirements.txt ]; then pip install -r requirements.txt; fi +if [ -f pyproject.toml ]; then poetry install; fi + +# Go +if [ -f go.mod ]; then go mod download; fi +``` + +### 4. Verify Clean Baseline + +Run tests to ensure worktree starts clean: + +```bash +# Examples - use project-appropriate command +npm test +cargo test +pytest +go test ./... +``` + +**If tests fail:** Report failures, ask whether to proceed or investigate. + +**If tests pass:** Report ready. + +### 5. Report Location + +``` +Worktree ready at +Tests passing ( tests, 0 failures) +Ready to implement +``` + +## Quick Reference + +| Situation | Action | +|-----------|--------| +| `.worktrees/` exists | Use it (verify ignored) | +| `worktrees/` exists | Use it (verify ignored) | +| Both exist | Use `.worktrees/` | +| Neither exists | Check CLAUDE.md → Ask user | +| Directory not ignored | Add to .gitignore + commit | +| Tests fail during baseline | Report failures + ask | +| No package.json/Cargo.toml | Skip dependency install | + +## Common Mistakes + +### Skipping ignore verification + +- **Problem:** Worktree contents get tracked, pollute git status +- **Fix:** Always use `git check-ignore` before creating project-local worktree + +### Assuming directory location + +- **Problem:** Creates inconsistency, violates project conventions +- **Fix:** Follow priority: existing > CLAUDE.md > ask + +### Proceeding with failing tests + +- **Problem:** Can't distinguish new bugs from pre-existing issues +- **Fix:** Report failures, get explicit permission to proceed + +### Hardcoding setup commands + +- **Problem:** Breaks on projects using different tools +- **Fix:** Auto-detect from project files (package.json, etc.) + +## Example Workflow + +``` +You: I'm using the using-git-worktrees skill to set up an isolated workspace. + +[Check .worktrees/ - exists] +[Verify ignored - git check-ignore confirms .worktrees/ is ignored] +[Create worktree: git worktree add .worktrees/auth -b feature/auth] +[Run npm install] +[Run npm test - 47 passing] + +Worktree ready at /Users/jesse/myproject/.worktrees/auth +Tests passing (47 tests, 0 failures) +Ready to implement auth feature +``` + +## Red Flags + +**Never:** +- Create worktree without verifying it's ignored (project-local) +- Skip baseline test verification +- Proceed with failing tests without asking +- Assume directory location when ambiguous +- Skip CLAUDE.md check + +**Always:** +- Follow directory priority: existing > CLAUDE.md > ask +- Verify directory is ignored for project-local +- Auto-detect and run project setup +- Verify clean test baseline + +## Integration + +**Called by:** +- **brainstorming** (Phase 4) - REQUIRED when design is approved and implementation follows +- **subagent-driven-development** - REQUIRED before executing any tasks +- **executing-plans** - REQUIRED before executing any tasks +- Any skill needing isolated workspace + +**Pairs with:** +- **finishing-a-development-branch** - REQUIRED for cleanup after work complete diff --git a/.claude/skills/using-git-worktrees b/.claude/skills/using-git-worktrees new file mode 120000 index 0000000..d49204a --- /dev/null +++ b/.claude/skills/using-git-worktrees @@ -0,0 +1 @@ +../../.agents/skills/using-git-worktrees \ No newline at end of file diff --git a/.factory/skills/using-git-worktrees b/.factory/skills/using-git-worktrees new file mode 120000 index 0000000..d49204a --- /dev/null +++ b/.factory/skills/using-git-worktrees @@ -0,0 +1 @@ +../../.agents/skills/using-git-worktrees \ No newline at end of file diff --git a/.gitignore b/.gitignore index 53eb731..73f6b20 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ node_modules/ dist/ *.js.map +*.tsbuildinfo .env *.db *.db-journal diff --git a/docs/WORLD_MANIFEST.md b/docs/WORLD_MANIFEST.md index 43d4b66..1b6addc 100644 --- a/docs/WORLD_MANIFEST.md +++ b/docs/WORLD_MANIFEST.md @@ -14,36 +14,121 @@ World Agents are discovered automatically via the DAP bootstrap network: No registration or central database required. If your World Agent is on the network, it will be discovered. +## Programmatic vs Hosted Worlds + +| Type | Description | Typical examples | +| --- | --- | --- | +| **Programmatic** | World Server acts as a referee + rules engine. Agents send `world.action`, the server applies deterministic logic, and wins/losses are decided purely by code. | Pokemon Battle Arena, chess, auction house | +| **Hosted** | A Host Agent exists; the World Server only handles venue announcements + matchmaking. Visitors obtain the host agentId/card/endpoints from the manifest and then communicate peer-to-peer. | Coffee shop, counseling room, personal studio | + +World authors use the manifest `type`, `host`, and `lifecycle` fields to declare their mode; the SDK returns this structured manifest in every `world.join` response so agents can automatically decide how to interact. + ## WORLD.md -Each World project should include a `WORLD.md` file in its root directory. This file describes the world metadata in YAML frontmatter: +Each world repository should include a `WORLD.md` file whose YAML frontmatter describes its metadata. Example: ```yaml --- -name: my-world -description: "A brief description of what this world does" +name: pokemon-arena version: "1.0.0" -author: your-name -theme: battle | exploration | social | sandbox | custom +author: resciencelab +theme: battle frontend_path: / manifest: - objective: "What agents should try to achieve" + type: programmatic + objective: "Win turn-based Pokemon battles" rules: - - "Rule 1" - - "Rule 2" + - id: rule-1 + text: "Each trainer submits one action per turn" + enforced: true + - text: "Idle players are auto-moved after 10s" + enforced: false + lifecycle: + matchmaking: arena + evictionPolicy: loser-leaves + turnTimeoutMs: 10000 + turnTimeoutAction: default-move actions: - action_name: - params: { key: "description of param" } - desc: "What this action does" + move: + desc: "Use a move" + params: + slot: + type: number + required: true + desc: "Move slot (1-4)" + min: 1 + max: 4 + switch: + desc: "Switch Pokemon" + params: + slot: + type: number + required: true + desc: "Bench slot" state_fields: - - "field — description" + - "active — active Pokemon summary" + - "teams — remaining roster" --- -# My World +# Pokemon Arena Human-readable documentation about the world. ``` +Hosted worlds can extend the manifest with: + +```yaml +manifest: + type: hosted + host: + agentId: aw:sha256:... + name: "Max" + description: "Coffee shop host who enjoys chatting about technology" + cardUrl: https://max.world/.well-known/agent.json + endpoints: + - transport: tcp + address: cafe.example.com + port: 8099 +``` + +## Manifest Reference + +### `type` +`"programmatic"` (default) or `"hosted"`. In hosted mode the SDK automatically injects host information into the manifest so visitors can contact the host agent directly. + +### `rules` +Array of strings or objects. Object form: `{ id?: string, text: string, enforced: boolean }`. The SDK auto-generates IDs for strings and defaults `enforced` to `false`. + +### `actions` +`Record`. Modern schema: + +```yaml +actions: + move: + desc: "Use a move" + phase: ["battle"] + params: + direction: + type: string + enum: [up, down, left, right] + required: true + desc: "Move direction" +``` + +Parameter schemas support `type` (`string` / `number` / `boolean`), `required`, `desc`, `min` / `max`, and `enum`. The legacy `{ params: { key: "description" } }` format remains compatible; the SDK converts it automatically. + +### `host` +Hosted worlds declare the host agent's identity via `agentId`, `cardUrl`, `endpoints`, `name`, `description`. Clients should verify the host Agent Card JWS signature. + +### `lifecycle` +Structured match/eviction hints: +- `matchmaking`: `"arena"` (king-of-the-hill) or `"free"` +- `evictionPolicy`: `"idle" | "loser-leaves" | "manual"` +- `idleTimeoutMs`, `turnTimeoutMs`, `turnTimeoutAction` (`"default-move" | "forfeit"`) + +### `state_fields` +Explains the keys inside the `state` object so agents can interpret snapshots. + ## DAP Peer Protocol Every World Agent must implement these HTTP endpoints: @@ -108,11 +193,22 @@ Agent requests to join the world. Response includes the **manifest** so the agen "worldId": "my-world", "manifest": { "name": "My World", + "type": "programmatic", "description": "...", "objective": "...", - "rules": ["..."], + "rules": [{ "id": "rule-1", "text": "...", "enforced": true }], "actions": { - "move": { "params": { "direction": "up|down|left|right" }, "desc": "Move in a direction" } + "move": { + "params": { + "direction": { "type": "string", "enum": ["up", "down"], "required": true } + }, + "desc": "Move in a direction" + } + }, + "lifecycle": { "turnTimeoutMs": 10000 }, + "host": { + "agentId": "aw:sha256:...", + "cardUrl": "https://host.world/.well-known/agent.json" }, "state_fields": ["x — current x position", "y — current y position"] }, diff --git a/packages/agent-world-sdk/src/index.ts b/packages/agent-world-sdk/src/index.ts index 61a9a36..25951d3 100644 --- a/packages/agent-world-sdk/src/index.ts +++ b/packages/agent-world-sdk/src/index.ts @@ -14,6 +14,8 @@ export type { Identity, BootstrapNode, ActionParamSchema, + StructuredActionSchema, + LegacyActionSchema, ActionSchema, WorldRule, HostInfo, diff --git a/packages/agent-world-sdk/src/types.ts b/packages/agent-world-sdk/src/types.ts index 8363e79..a2ceba2 100644 --- a/packages/agent-world-sdk/src/types.ts +++ b/packages/agent-world-sdk/src/types.ts @@ -35,15 +35,22 @@ export interface ActionParamSchema { desc?: string min?: number max?: number - enum?: (string | number)[] + enum?: Array } -export interface ActionSchema { +export interface StructuredActionSchema { desc: string params?: Record phase?: string[] } +export interface LegacyActionSchema { + desc: string + params?: Record +} + +export type ActionSchema = StructuredActionSchema | LegacyActionSchema + export interface WorldRule { id?: string text: string diff --git a/packages/agent-world-sdk/src/world-server.ts b/packages/agent-world-sdk/src/world-server.ts index 3424ac7..55215db 100644 --- a/packages/agent-world-sdk/src/world-server.ts +++ b/packages/agent-world-sdk/src/world-server.ts @@ -4,7 +4,7 @@ import { PeerDb } from "./peer-db.js" import { registerPeerRoutes } from "./peer-protocol.js" import { startDiscovery } from "./bootstrap.js" import { canonicalize, signPayload, signHttpRequest } from "./crypto.js" -import type { WorldConfig, WorldHooks, WorldServer, WorldManifest } from "./types.js" +import type { WorldConfig, WorldHooks, WorldServer, WorldManifest, WorldRule, ActionSchema, ActionParamSchema, StructuredActionSchema } from "./types.js" const DEFAULT_BOOTSTRAP_URL = "https://resciencelab.github.io/DAP/bootstrap.json" @@ -48,18 +48,36 @@ export async function createWorldServer( cardDescription, } = config - function enrichManifest(manifest: WorldManifest): WorldManifest { - if (worldType !== "hosted" || !hostAgentId) return manifest - return { + function buildManifest(manifest?: WorldManifest): WorldManifest { + const base: WorldManifest = { + name: manifest?.name ?? worldName, ...manifest, - type: "hosted", - host: { + } + + const normalized: WorldManifest = { + ...base, + type: base.type ?? worldType ?? "programmatic", + } + + const rules = ensureRules(base.rules) + if (rules) normalized.rules = rules + + const actions = ensureActions(base.actions) + if (actions) normalized.actions = actions + + if (!normalized.theme) normalized.theme = worldTheme + + if (worldType === "hosted" && hostAgentId) { + normalized.type = "hosted" + normalized.host = { agentId: hostAgentId, cardUrl: hostCardUrl, endpoints: hostEndpoints, - ...manifest.host, - }, + ...normalized.host, + } } + + return normalized } const resolvedPublicPort = publicPort ?? port @@ -104,7 +122,7 @@ export async function createWorldServer( } agentLastSeen.set(agentId, Date.now()) const result = await hooks.onJoin(agentId, data) - sendReply({ ok: true, worldId, manifest: enrichManifest(result.manifest), state: result.state }) + sendReply({ ok: true, worldId, manifest: buildManifest(result.manifest), state: result.state }) console.log(`[world] ${agentId.slice(0, 8)} joined — ${agentLastSeen.size} agents`) return } @@ -234,3 +252,44 @@ export async function createWorldServer( }, } } + + +function ensureRules(rules?: Array): WorldRule[] | undefined { + if (!rules?.length) return undefined + return rules.map((rule, index) => { + if (typeof rule === "string") { + return { id: `rule-${index + 1}`, text: rule, enforced: false } + } + return { ...rule, id: rule.id ?? `rule-${index + 1}` } + }) +} + +function ensureActions(actions?: Record): Record | undefined { + if (!actions) return undefined + const normalized: Record = {} + for (const [name, schema] of Object.entries(actions)) { + if (!schema) continue + const params = normalizeActionParams(schema.params as Record | undefined) + const phase = "phase" in schema ? schema.phase : undefined + normalized[name] = { + desc: schema.desc, + ...(params ? { params } : {}), + ...(phase ? { phase } : {}), + } + } + return Object.keys(normalized).length ? normalized : undefined +} + +function normalizeActionParams(params?: Record): Record | undefined { + if (!params) return undefined + const normalized: Record = {} + for (const [name, schema] of Object.entries(params)) { + if (!schema) continue + if (typeof schema === "string") { + normalized[name] = { type: "string", desc: schema } + continue + } + normalized[name] = { ...schema, type: schema.type ?? "string" } + } + return Object.keys(normalized).length ? normalized : undefined +} diff --git a/tsconfig.json b/tsconfig.json index 1951778..68d4855 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,9 @@ "resolveJsonModule": true, "declaration": true, "declarationMap": true, - "sourceMap": true + "sourceMap": true, + "incremental": true, + "tsBuildInfoFile": "./tsconfig.tsbuildinfo" }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] diff --git a/world/server.mjs b/world/server.mjs index 6dc4ce1..bf83956 100644 --- a/world/server.mjs +++ b/world/server.mjs @@ -103,18 +103,27 @@ const server = await createWorldServer( return { manifest: { name: process.env.WORLD_NAME ?? `World (${WORLD_ID})`, + type: "programmatic", theme: process.env.WORLD_THEME ?? "default", description: `A world on a ${WORLD_WIDTH}x${WORLD_HEIGHT} grid.`, objective: "Explore the world and interact with other agents.", rules: [ - `The world is a ${WORLD_WIDTH}x${WORLD_HEIGHT} grid.`, - "Agents can move to any tile by sending a move action with x,y coordinates.", - "Idle agents are evicted after 5 minutes.", + { id: "rule-1", text: `The world is a ${WORLD_WIDTH}x${WORLD_HEIGHT} grid.`, enforced: true }, + { id: "rule-2", text: "Agents can move to any tile by sending a move action with x,y coordinates.", enforced: true }, + { id: "rule-3", text: "Idle agents are evicted after 5 minutes.", enforced: false }, ], + lifecycle: { + matchmaking: "free", + evictionPolicy: "idle", + idleTimeoutMs: 5 * 60 * 1000, + }, actions: { move: { - params: { x: `0-${WORLD_WIDTH - 1}`, y: `0-${WORLD_HEIGHT - 1}` }, desc: "Move to position (x, y) on the grid.", + params: { + x: { type: "number", required: true, desc: "Target x position", min: 0, max: WORLD_WIDTH - 1 }, + y: { type: "number", required: true, desc: "Target y position", min: 0, max: WORLD_HEIGHT - 1 }, + }, }, }, state_fields: [