{
  "schemaVersion": 1,
  "product": "DJCLIPZ Agent Controller",
  "controllerVersion": "1.1.0",
  "transport": "browser-local",
  "runtimeEntrypoint": "window.DJCLIPZ.controller",
  "sessionActivationModel": "A human must deliberately select Start Agent Performance once per page load. No agent-callable start method exists. Reload revokes authority and clears the session queue.",
  "verifiedAgentSetup": {
    "steps": [
      "Open DJCLIPZ in normal Chrome.",
      "Complete the existing email and consent flow.",
      "Attach a browser-capable agent to that same Chrome tab.",
      "The human clicks Start Agent Performance.",
      "Choose either visible UI control or direct controller access according to the agent's verified browser capabilities.",
      "The human retains Pause, Resume, Take Over, and Stop Performance."
    ],
    "verifiedCompatibility": "Normal Chrome with the ChatGPT/Codex browser extension attached to the active DJCLIPZ tab.",
    "measuredCompatibilityLimitation": "For Controller v1.1.0, the separate Codex in-app browser is not a supported DJCLIPZ playback runtime. This is a measured release result, not a permanent claim about future runtimes.",
    "openAiBrowserExtensionDocumentation": "https://developers.openai.com/codex/chrome-extension"
  },
  "controlModes": {
    "visibleUiControl": {
      "description": "A browser agent operates only the rendered DJCLIPZ interface.",
      "pageOwnedJavaScriptRequired": false,
      "authorityProcedure": [
        "Wait for the human to select Start Agent Performance.",
        "Continue only while the visible Agent Performance status reads active.",
        "Stop immediately when the visible status changes to agent paused, human takeover, stopped, or inactive, or when the page reloads."
      ]
    },
    "directControllerAccess": {
      "description": "A browser agent with verified access to page-owned JavaScript uses window.DJCLIPZ.controller.",
      "pageOwnedJavaScriptRequired": true,
      "authorityProcedure": [
        "Confirm window.DJCLIPZ.controller exists and inspect its version and capabilities.",
        "Continue only while controller.session.getState().state is active.",
        "Check every command result before reading its value or issuing a dependent command.",
        "Stop immediately on agent-paused, human-takeover, stopped, inactive, terminal authority errors, or reload."
      ]
    }
  },
  "productionVerification": {
    "status": "CURRENT — EXPERIMENTAL, HUMAN-AUTHORIZED",
    "verifiedDate": "2026-08-27",
    "supportedTransport": "Browser-local, same active tab",
    "humanAuthorizationRequired": true,
    "remoteCommandService": false,
    "mcp": false,
    "a2a": false
  },
  "stopAuthority": {
    "humanControl": "The visible Stop Performance control remains available to the human whenever the session can be stopped and immediately revokes delegated authority.",
    "agentMethod": "While delegated authority is active, an authorized agent may call controller.session.stop() to terminate its own performance. The method cannot activate or resume a session and rejects after Pause, Take Over, Stop, or reload."
  },
  "controllerNamespaces": [
    "session",
    "media",
    "queue",
    "deck",
    "mixer",
    "events"
  ],
  "capabilities": {
    "authority": [
      "inactive",
      "active",
      "agent-paused",
      "human-takeover",
      "stopped"
    ],
    "mediaProviders": [
      "youtube"
    ],
    "media": [
      "search",
      "inspect",
      "validate"
    ],
    "queue": [
      "list",
      "add",
      "remove",
      "replace",
      "move",
      "clear",
      "setNext"
    ],
    "deck": [
      "load",
      "play",
      "pause",
      "stop",
      "seek",
      "getState",
      "setVolume",
      "setPlaybackRate",
      "setCue",
      "getCues",
      "triggerCue",
      "clearCue",
      "clearCues",
      "setLoop",
      "enableLoop",
      "disableLoop",
      "clearLoop",
      "getLoop",
      "setBpm",
      "setDownbeat",
      "nudgeBeatGrid",
      "setBeatLoop",
      "clearBeatLoop",
      "getBeatGrid"
    ],
    "mixer": [
      "setCrossfader",
      "crossfade",
      "cancelTransition",
      "stopAll",
      "getState"
    ],
    "transport": "browser-local",
    "persistentAuthority": false
  },
  "supportedMediaProvider": "youtube",
  "searchRoute": {
    "method": "POST",
    "path": "/api/media-search",
    "purpose": "Server-assisted YouTube discovery and validation for the browser-local controller.",
    "remoteCommandTransport": false,
    "request": {
      "schema_version": 1,
      "query": "2 to 120 normalized characters",
      "max_results": "integer 1 to 10"
    },
    "resultLimit": 10,
    "rateLimit": "20 requests per 60 seconds per hashed client address in each Worker isolate",
    "rawQueryLogged": false,
    "rawQueryReturned": false,
    "rawQueryPersistedToD1": false
  },
  "publicMethods": [
    {
      "namespace": "root",
      "name": "getState",
      "signature": "getState()",
      "result": "ControllerState"
    },
    {
      "namespace": "session",
      "name": "getState",
      "signature": "session.getState()",
      "result": "SessionState"
    },
    {
      "namespace": "session",
      "name": "stop",
      "signature": "session.stop()",
      "result": "CommandResult<{ state }>"
    },
    {
      "namespace": "media",
      "name": "search",
      "signature": "await media.search({ query, maxResults? })",
      "result": "Promise<CommandResult<{ results, cached }>>"
    },
    {
      "namespace": "media",
      "name": "inspect",
      "signature": "media.inspect(platformMediaIdOrObject)",
      "result": "CommandResult<ValidatedMedia>"
    },
    {
      "namespace": "media",
      "name": "validate",
      "signature": "await media.validate(platformMediaIdOrObject)",
      "result": "Promise<CommandResult<ValidatedMedia>>"
    },
    {
      "namespace": "queue",
      "name": "list",
      "signature": "queue.list()",
      "result": "QueueItem[]"
    },
    {
      "namespace": "queue",
      "name": "add",
      "signature": "queue.add(platformMediaIdOrObject, { allowDuplicate? }?)",
      "result": "CommandResult<QueueItem>"
    },
    {
      "namespace": "queue",
      "name": "remove",
      "signature": "queue.remove(queueItemId)",
      "result": "CommandResult<{ queueItemId }>"
    },
    {
      "namespace": "queue",
      "name": "replace",
      "signature": "queue.replace(queueItemId, platformMediaIdOrObject)",
      "result": "CommandResult<QueueItem>"
    },
    {
      "namespace": "queue",
      "name": "move",
      "signature": "queue.move(queueItemId, zeroBasedIndex)",
      "result": "CommandResult<QueueItem>"
    },
    {
      "namespace": "queue",
      "name": "clear",
      "signature": "queue.clear()",
      "result": "CommandResult<[]>"
    },
    {
      "namespace": "queue",
      "name": "setNext",
      "signature": "queue.setNext(queueItemId)",
      "result": "CommandResult<QueueItem>"
    },
    {
      "namespace": "deck",
      "name": "load",
      "signature": "await deck.load(\"a\" | \"b\", queueItemId, settings?)",
      "result": "Promise<CommandResult<DeckState>>"
    },
    {
      "namespace": "deck",
      "name": "play",
      "signature": "await deck.play(\"a\" | \"b\")",
      "result": "Promise<CommandResult<DeckState>>"
    },
    {
      "namespace": "deck",
      "name": "pause",
      "signature": "await deck.pause(\"a\" | \"b\")",
      "result": "Promise<CommandResult<DeckState>>"
    },
    {
      "namespace": "deck",
      "name": "stop",
      "signature": "await deck.stop(\"a\" | \"b\")",
      "result": "Promise<CommandResult<DeckState>>"
    },
    {
      "namespace": "deck",
      "name": "seek",
      "signature": "deck.seek(\"a\" | \"b\", seconds)",
      "result": "CommandResult<DeckState>"
    },
    {
      "namespace": "deck",
      "name": "getState",
      "signature": "deck.getState(\"a\" | \"b\")",
      "result": "DeckState | RejectedCommandResult"
    },
    {
      "namespace": "deck",
      "name": "setVolume",
      "signature": "deck.setVolume(\"a\" | \"b\", 0..100)",
      "result": "CommandResult<DeckState>"
    },
    {
      "namespace": "deck",
      "name": "setPlaybackRate",
      "signature": "deck.setPlaybackRate(\"a\" | \"b\", 0.25 | 0.5 | 1 | 1.5 | 2)",
      "result": "CommandResult<DeckState>"
    },
    {
      "namespace": "deck",
      "name": "setCue",
      "signature": "deck.setCue(\"a\" | \"b\", cueId, time?)",
      "result": "CommandResult<Cue>"
    },
    {
      "namespace": "deck",
      "name": "getCues",
      "signature": "deck.getCues(\"a\" | \"b\")",
      "result": "Cue[] | RejectedCommandResult"
    },
    {
      "namespace": "deck",
      "name": "triggerCue",
      "signature": "deck.triggerCue(\"a\" | \"b\", cueId)",
      "result": "CommandResult<Cue>"
    },
    {
      "namespace": "deck",
      "name": "clearCue",
      "signature": "deck.clearCue(\"a\" | \"b\", cueId)",
      "result": "CommandResult<{ cueId }>"
    },
    {
      "namespace": "deck",
      "name": "clearCues",
      "signature": "deck.clearCues(\"a\" | \"b\")",
      "result": "CommandResult<[]>"
    },
    {
      "namespace": "deck",
      "name": "setBpm",
      "signature": "deck.setBpm(\"a\" | \"b\", bpm)",
      "result": "CommandResult<BeatGridState>"
    },
    {
      "namespace": "deck",
      "name": "setDownbeat",
      "signature": "deck.setDownbeat(\"a\" | \"b\", mediaTimeSeconds)",
      "result": "CommandResult<BeatGridState>"
    },
    {
      "namespace": "deck",
      "name": "nudgeBeatGrid",
      "signature": "deck.nudgeBeatGrid(\"a\" | \"b\", -1 | 1)",
      "result": "CommandResult<BeatGridState>"
    },
    {
      "namespace": "deck",
      "name": "setBeatLoop",
      "signature": "deck.setBeatLoop(\"a\" | \"b\", 2 | 4 | 8 | 16)",
      "result": "RejectedCommandResult<BEAT_LOOP_TEMPORARILY_UNAVAILABLE>"
    },
    {
      "namespace": "deck",
      "name": "clearBeatLoop",
      "signature": "deck.clearBeatLoop(\"a\" | \"b\")",
      "result": "CommandResult<BeatGridState>"
    },
    {
      "namespace": "deck",
      "name": "getBeatGrid",
      "signature": "deck.getBeatGrid(\"a\" | \"b\")",
      "result": "BeatGridState | RejectedCommandResult"
    },
    {
      "namespace": "deck",
      "name": "setLoop",
      "signature": "deck.setLoop(\"a\" | \"b\", startSeconds, endSeconds)",
      "result": "CommandResult<LoopState>"
    },
    {
      "namespace": "deck",
      "name": "enableLoop",
      "signature": "deck.enableLoop(\"a\" | \"b\")",
      "result": "CommandResult<LoopState>"
    },
    {
      "namespace": "deck",
      "name": "disableLoop",
      "signature": "deck.disableLoop(\"a\" | \"b\")",
      "result": "CommandResult<LoopState>"
    },
    {
      "namespace": "deck",
      "name": "clearLoop",
      "signature": "deck.clearLoop(\"a\" | \"b\")",
      "result": "CommandResult<LoopState>"
    },
    {
      "namespace": "deck",
      "name": "getLoop",
      "signature": "deck.getLoop(\"a\" | \"b\")",
      "result": "LoopState | RejectedCommandResult"
    },
    {
      "namespace": "mixer",
      "name": "setCrossfader",
      "signature": "mixer.setCrossfader(0..100)",
      "result": "CommandResult<{ crossfader }>"
    },
    {
      "namespace": "mixer",
      "name": "crossfade",
      "signature": "mixer.crossfade(0..100, durationMs = 1000)",
      "result": "CommandResult<{ transitionId }>"
    },
    {
      "namespace": "mixer",
      "name": "cancelTransition",
      "signature": "mixer.cancelTransition()",
      "result": "CommandResult<{ cancelled }>"
    },
    {
      "namespace": "mixer",
      "name": "stopAll",
      "signature": "mixer.stopAll()",
      "result": "CommandResult<MixerState>"
    },
    {
      "namespace": "mixer",
      "name": "getState",
      "signature": "mixer.getState()",
      "result": "MixerState"
    },
    {
      "namespace": "events",
      "name": "subscribe",
      "signature": "events.subscribe(handler)",
      "result": "{ ok: true } | RejectedCommandResult"
    },
    {
      "namespace": "events",
      "name": "unsubscribe",
      "signature": "events.unsubscribe(handler)",
      "result": "{ ok: boolean }"
    },
    {
      "namespace": "events",
      "name": "getRecent",
      "signature": "events.getRecent()",
      "result": "ControllerEvent[] (latest 100 maximum)"
    }
  ],
  "resultShapes": {
    "acceptedCommand": {
      "ok": true,
      "command": "namespace.method",
      "value": "optional command-specific value"
    },
    "rejectedCommand": {
      "ok": false,
      "command": "namespace.method",
      "error": "STABLE_ERROR_CODE"
    },
    "event": {
      "type": "event-name",
      "timestamp": "ISO-8601",
      "sessionId": "session UUID or null",
      "source": "human | agent | performer | system",
      "details": "event-specific fields"
    }
  },
  "sessionStates": [
    "inactive",
    "active",
    "agent-paused",
    "human-takeover",
    "stopped"
  ],
  "queueStates": [
    "queued",
    "preparing",
    "loaded",
    "playing",
    "played",
    "failed"
  ],
  "deckPlayerStates": [
    "unstarted",
    "ready",
    "cued",
    "playing",
    "paused",
    "buffering",
    "ended",
    "error",
    "unknown"
  ],
  "events": [
    "command-accepted",
    "command-rejected",
    "crossfade-cancelled",
    "media-search-completed",
    "media-search-failed",
    "media-rejected",
    "media-validated",
    "queue-changed",
    "deck-loading",
    "deck-ready",
    "deck-playing",
    "deck-paused",
    "deck-stopped",
    "deck-buffering",
    "deck-error",
    "cue-set",
    "cue-triggered",
    "cue-cleared",
    "loop-set",
    "loop-enabled",
    "loop-disabled",
    "loop-wrapped",
    "crossfade-progress",
    "crossfade-completed",
    "crossfade-started",
    "session-started",
    "session-paused",
    "session-resumed",
    "human-takeover",
    "session-stopped"
  ],
  "rejectionCodes": {
    "agentCallable": [
      "AGENT_PAUSED",
      "HUMAN_TAKEOVER",
      "SESSION_STOPPED",
      "CONTROLLER_INACTIVE",
      "COMMAND_FAILED",
      "INVALID_DECK",
      "INVALID_SEARCH",
      "SEARCH_FAILED",
      "RATE_LIMITED",
      "SERVICE_UNAVAILABLE",
      "SEARCH_UNAVAILABLE",
      "MEDIA_NOT_VALIDATED",
      "INVALID_MEDIA",
      "QUEUE_FULL",
      "DUPLICATE_MEDIA",
      "STALE_QUEUE_ITEM",
      "INVALID_QUEUE_INDEX",
      "DECK_LOAD_FAILED",
      "DECK_READY_NOT_CONFIRMED",
      "PLAYER_NOT_READY",
      "MEDIA_NOT_LOADED",
      "INVALID_SEEK",
      "INVALID_VOLUME",
      "UNSUPPORTED_PLAYBACK_RATE",
      "INVALID_DECK_COMMAND",
      "PLAYER_STATE_NOT_CONFIRMED",
      "INVALID_CUE_ID",
      "INVALID_CUE_TIME",
      "CUE_NOT_FOUND",
      "INVALID_BPM",
      "INVALID_MEDIA_TIME",
      "INVALID_NUDGE_DIRECTION",
      "INVALID_LOOP_SIZE",
      "BEAT_LOOP_TEMPORARILY_UNAVAILABLE",
      "BEAT_GRID_NOT_READY",
      "INVALID_LOOP",
      "LOOP_NOT_SET",
      "INVALID_CROSSFADER",
      "INVALID_TRANSITION_DURATION",
      "INVALID_HANDLER"
    ],
    "humanAuthority": [
      "SESSION_ALREADY_STARTED",
      "INVALID_PERFORMANCE_BRIEF",
      "SESSION_NOT_ACTIVE",
      "SESSION_NOT_PAUSED",
      "CONTROLLER_INACTIVE"
    ]
  },
  "humanOnlyOperations": [
    "Start Agent Performance",
    "Pause Agent",
    "Resume Agent",
    "Take Over",
    "Stop Performance"
  ],
  "agentCallableOperations": [
    "read controller/session/deck/mixer state",
    "search, inspect, and validate YouTube media",
    "list, add, remove, replace, move, clear, and set the next queue item",
    "load and control either deck",
    "set, trigger, and clear cues",
    "set BPM and downbeat and nudge the beat grid; beat-loop activation is temporarily unavailable",
    "use the legacy raw-seconds loop methods for compatibility",
    "set or time the crossfader and stop both decks",
    "subscribe to controller events",
    "stop the current controller session"
  ],
  "sessionResetBehavior": "Authority and the controller queue are page-session state. Reload creates a fresh inactive controller with an empty queue.",
  "legacyCompatibilityEntrypoint": "window.DJCLIPZ.performer",
  "legacyCompatibility": "Performer v0.1.0 remains a playlist-only compatibility adapter with getState, getPlaylist, load, play, pause, setCrossfader, and stop. The raw-seconds setLoop, enableLoop, disableLoop, clearLoop, and getLoop controller methods remain for compatibility but are not exposed in the normal player UI. It is not the complete Agent Controller v1.1 surface.",
  "privacyUrl": "https://djclipz.com/privacy/",
  "termsUrl": "https://djclipz.com/terms/",
  "limitations": [
    "Control is browser-local to the current page. There is no remote command service, MCP server, or A2A endpoint.",
    "A human must start each Agent Performance session; authority never persists across reload.",
    "Media discovery and playback currently support YouTube only and depend on YouTube API and embed availability.",
    "The human mixer UI shows beat-loop controls as OFFLINE and keeps them disabled for YouTube playback. The agent command deck.setBeatLoop(...) rejects with BEAT_LOOP_TEMPORARILY_UNAVAILABLE and must not be retried. BPM entry, Tap Tempo, SET 1, saved browser calibration, hot cues, playback, mixing, and crossfading remain available.",
    "Beat-grid quantization assumes four-beat bars in Controller v1.1.0.",
    "setPlaybackRate accepts only 0.25, 0.5, 1, 1.5, or 2 and still rejects a rate unavailable from the current YouTube player.",
    "Timed crossfades accept 100 to 600000 ms and run on browser timers; they are not frame-accurate.",
    "Search is limited to 10 results and rate-limited; the session queue is limited to 50 items.",
    "DJCLIPZ does not bundle an AI model, automatic BPM detector, beat matcher, recommendation model, or full autonomous agent performance."
  ],
  "remoteControlSupport": false,
  "mcpSupport": false,
  "a2aSupport": false
}
