# Project export: Squad AI

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: OpenAI Build Week
- Tagline: SquadAI is the Kubernetes-like control plane for Codex agents turning every event into the right Codex task, on the machines where the work already lives!
- Devpost: https://devpost.com/software/squad-ai
- GitHub: https://github.com/mohit17mor/SquadAI
- Video: https://www.youtube.com/embed/H9oD0Xbqa8g?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — Mohit Mor (23 commits)

## Devpost submission (written by the team)

### Inspiration

Agents have become really good at getting things done but they kind of need the manual human input for every small thing. I came across an analogy on social media that says LLM is the brain and skills/tools are hands. Then I thought what is missing to make it more autonomous? I believe answer is - senses, which I think corresponds to event driven waking up of agents. A way for agents to react when something happens, rather than waiting for a person to open a chat and give them work. Another big reason for me starting SquadAI is I think people use codex or any other agent just like another app, write skills around it or basic automations. But its much more powerful than that if we consider agent as a component in our system designs. For that programmatic access is needed and the ability to scale the agents automatically. I have 6 yrs of experience working with distributed systems at scale, thought it would be really cool to have the same concepts applied to AI agents as well :).

### What it does

SquadAI is the infrastructure layer for running Codex agents as part of your systems not just as individual chat windows. It lets your agents wake up and react to changes (events) in the world. Example of changes/events : Ops tickets coming in, your script/monitor detects it and sends over, New work item created in jira, Flight price changes, Or anything around which a script/monitor can be written which can then send events to SquadAI Not only that, SquadAI also handles routing it to the right agent, queueing it, creating or resuming agent instances, managing persistent threads, shared library of skills, coordinating work across machines and giving you one place to see and control it all. Also, added the support to talk to multiple agents through telegram group, the agents could be on any machine and still have the relevant context of the group chat! Basically, SquadAI is a full control plane for your Codex agents. It turns individual agents into a system that can run, coordinate, and scale across any number of machines. What differentiates SquadAI from other solutions in same space? Recently lot of solutions have come out to run agents on cloud. But as a developer/user I would like my agents running on the machine where my setup, files or apps are already there. Squad AI makes it flexible for the users to install the runners (wrapper around codex app server) on any machine they like and connect to the same control plane. As an user you would get a unified view of all the machines. Few important design decisions: Lot of other solutions which support multiple machine, they require 1 agent to ssh into another machine and run commands there. This is not the case with SquadAI, no agent ssh-ing around, the agents live on their machine unaware of the other machine, all the communication is done through control plane. This makes the path for agent to agent communications across machines easier to handle. Agents are treated as instantiable objects (borrowed few concepts from Object Oriented Programming), which allows the control plane to easily instantiate a copy of same agent automatically. You don't need to worry about creating agents manually for same task. How I built it I came across codex app server which is really powerful and the backbone of the project. Made a wrapper around the codex app server APIs so the control plane above it can use it easily. Control plane was built which handles the event listening, queuing of tasks, routing the task to right agent etc. GPT-5.6 sol was used for this as it was bit complex piece. Built a completely new dashboard for visualizing the agents in a 3D topology view, and mainly the aim for it was to get an unified view of all the agents even if they are running on multiple machines. Codex made it easier by giving me images first to select the design and the coding it up. Annotation mode is super useful in building UI. Initially I had made it just keeping event driven waking up in mind (the famous loop engineering), then I realized why not have support for multiple machines and that's when i decoupled the control plane and codex app server wrapper code. Again gpt 5.6 sol helped here! Added support for tailscale to connect multiple machines easily. Once the event driven control and support for multiple machines was in place, then it was relatively easier to build any feature on top of it like for example the telegram group for agents. Whole project was completely built using codex and 5.6 terra/sol combination. That's why i say - built by codex for codex :) Challenges I ran into I didn't want to make a new UI at first as its really tough for people to adapt to new UI and codex desktop app already has a good one. But the issue is the threads started by custom code were not being actively displayed on the app. It does get displayed eventually but i wanted more synchronous way. And codex desktop app doesn't natively support listening to events. Had to take a route of creating the whole UI and wrapper on app server apis. Also, i am not good at networking part, gpt 5.6 suggested the tailscale solution! Accomplishments that I am proud of I have used it to automate the initial level of debugging required for ops tickets and I dont even need to do anything manually for 12 hrs straight. Looking at it work on its own for long makes me confident about the project and happy with its current state. Also, really proud of the fact I was able to make a mini distributed system all by myself with the help of codex. What I learned I realized the power of agent and LLM is not just in chat window but in treating them as intelligent components in any of your system design, to make the agents truly autonomous. While working on the project learned about solutions like tailscale for networking.

### What's next

There is a lot of work that needs to be done but next immediate plan for Squad AI is to support graphs, IYKYK :). Few other interesting areas are to automatically scale agents across machines and agent to agent communication across machines. In future, I also want to onboard more agents so that project is not specific to codex only. Pre-existing work disclosure I had started working on the project before the build week. Before the build week I had the wrapper around codex app server API and control plane code with UI. During the build week I added the support for tailscale to be able to easily connect multiple machines, added shared skill library making it easier to transfer skills from one machine to another and added the whole telegram group chat integration supporting multiple agent tagging, conversation context handling and reply to msg feature. Few other changes were around making improvements in UI and fixing windows specific bugs.

## README (from the GitHub repository)

# SquadAI

> SquadAI is the Kubernetes-like control plane for Codex agents turning every event into the right Codex task, on the machine where the work already lives!

SquadAI gives you one place to manage and send work to Codex agents, even when
those agents run on different machines. Your projects, tools, skills, and
credentials stay on the machine where they are already set up. From the SquadAI
dashboard, you can talk to an agent directly or send it work when something
happens in another tool, such as a webhook, monitor, or Telegram message.

![SquadAI live agent topology](docs/assets/squadai-topology.png)

## Get SquadAI Running

The fastest setup runs the control plane and agents on one machine. You need
Git, Node.js 22.13 or newer, and a ChatGPT account that can sign in to
Codex.

<details open>
<summary><strong>Windows</strong></summary>

Open PowerShell and run:

```powershell
powershell -ExecutionPolicy ByPass -c "irm https://chatgpt.com/codex/install.ps1 | iex"
codex login
git clone https://github.com/mohit17mor/SquadAI.git
Set-Location SquadAI\codex-control
npm ci
npm run build
Set-Location ..\codex-agent-manager
npm ci
npm run build
npm start -- --mode embedded --host 127.0.0.1 --port 4317
```

</details>

<details>
<summary><strong>macOS</strong></summary>

Open Terminal and run:

```bash
brew install --cask codex
codex login
git clone https://github.com/mohit17mor/SquadAI.git
cd SquadAI/codex-control
npm ci
npm run build
cd ../codex-agent-manager
npm ci
npm run build
npm start -- --mode embedded --host 127.0.0.1 --port 4317
```

</details>

<details>
<summary><strong>Linux</strong></summary>

Open Terminal and run:

```bash
curl -fsSL https://chatgpt.com/codex/install.sh | sh
codex login
git clone https://github.com/mohit17mor/SquadAI.git
cd SquadAI/codex-control
npm ci
npm run build
cd ../codex-agent-manager
npm ci
npm run build
npm start -- --mode embedded --host 127.0.0.1 --port 4317
```

</details>

Open [http://127.0.0.1:4317](http://127.0.0.1:4317), create an agent, choose
its working directory, and send it a task. SquadAI stores your agents and their
conversations locally, so they remain available after a restart.

## Quick How To Use SquadAI

1. Click **Create Agent** (or **Add agent** in the topology view).
2. Choose the machine where the project lives, select its working directory,
   add instructions, and create the agent.
3. Select the agent and send it a message in its conversation, for example:
   `Review this repository and tell me the three riskiest areas.`
4. To test event-driven work, replace `my-coder` with your agent ID (the short,
   lowercase name created for the agent; for example, `Repository Coder` becomes
   `repository-coder`) and send this from a terminal on the control-plane machine:

   ```bash
   curl http://127.0.0.1:4317/api/sensor-events \
     -H 'content-type: application/json' \
     -d '{
       "source": "quick-start",
       "type": "task.requested",
       "body": "Inspect the current project and report the most important next step.",
       "targetAgentId": "my-coder",
       "executionPolicy": "reuse"
     }'
   ```

   The task will appear in the work queue and run on that agent's machine.

> Want your Telegram group connected too? Start with
> `npm start -- --mode embedded --telegram-token YOUR_CONTROL_BOT_TOKEN`, then
> follow [Telegram Group Control](#telegram-group-control).

## Built with Codex and GPT-5.6

SquadAI is built on `codex app-server`, which is the backbone that lets the
control plane create, resume, observe, and manage Codex sessions across
machines.

The entire project was written with Codex using GPT-5.6 Sol and GPT-5.6 Terra;
no other model was used. Sol was used for the major architecture discussions
and the core distributed control-plane pieces. Terra helped implement less
complex features and sharpen ideas before they became requirements.

Codex also helped shape the UI: it generated visual directions, one was chosen
as the reference, and the interface was refined with annotation-based pointed
feedback. Across the project, the GPT-5.6 models were especially useful for
understanding high-level requirements, turning them into concrete work, and
proactively identifying edge cases that had not been specified yet.

## Why SquadAI?

Coding agents are already effective when a person opens one conversation and
gives it one task. The roughness appears when you need several agents, recurring
work, parallel tasks, or long-running workflows:

- conversations become difficult to find and supervise;
- work must be copied manually from one tool or agent to another;
- nothing is listening for new work while you are away;
- multiple tasks aimed at one repository need isolated workspaces;
- approvals, failures, model changes, and unfinished work need one visible home;
- agents may need to run on different laptops, workstations, or VMs.

SquadAI provides the missing operational layer. Codex remains responsible for
reasoning, coding, tools, MCP servers, skills, plugins, and sandboxing. SquadAI
is responsible for organizing agents and work around it.

## What You Get

- **One command center:** See agents, live status, conversations, commentary,
  tool activity, approvals, events, and queued work in one browser UI.
- **Persistent agents:** Resume Codex threads instead of starting every task
  from an empty conversation.
- **Event-driven work:** Send work from issue trackers, webhooks, monitors,
  schedulers, or any system that can call an HTTP endpoint.
- **Reusable or isolated execution:** Reuse one long-running agent for a stream
  of events, or automatically create a separate agent instance for every task.
- **Human control:** Choose Ask for approval, Approve for me, or Full access,
  and answer approval requests from the conversation.
- **Repository isolation:** Git repositories use managed worktrees so parallel
  agent tasks do not edit the same checkout.
- **Remote runners:** Keep the control plane on one machine while agents run
  where the repositories, credentials, skills, plugins, and MCP servers exist.
- **Easy machine enrollment:** Add a Windows, macOS, or Linux runner from the
  UI with one expiring command over a private Tailscale connection.
- **Live runner inventory:** See every control-plane and remote machine,
  connection status, assigned agents, active work, and last heartbeat.
- **Telegram team chat:** Give selected agents their own Telegram bots, tag
  them in one group, and receive their work, approvals, and final summaries in
  the same conversation.
- **Shared skill library:** Import a user skill from one runner and install it
  on another without creating a temporary agent or spending model tokens.
- **Upgrade awareness:** Detect incompatible pinned model settings and request
  a migration decision instead of repeatedly failing without explanation.

## How It Works

```text
People / webhooks / monitors / Telegram
            |
            v
  +-----------------------+
  | SquadAI control plane |
  | UI, API, SQLite, work |
  | queue, approvals      |
  +-----------+-----------+
              |
       commands and events
              |
       +------+------+
       |             |
       v             v
  Local runner   Remote runner(s)
       |             |
       v             v
  codex app-server on each runner machine
       |
       v
  Repositories, Git worktrees, tools, skills, plugins, and MCP servers
```

The control plane stores coordination state and presents the UI. A runner
executes Codex sessions on its own machine and connects outward to the control
plane. This means source code and machine-local tools do not have to be copied
to the control-plane host.

## Core Concepts

### Agent

An agent is a reusable configuration: name, instructions, working directory,
model settings, permissions, selected skills, and runner. Its Codex thread is
created lazily and persisted for later conversations.

### Agent instance

For task-oriented work, SquadAI can creat

[README truncated for size]

## Detected evidence (automated analysis)

Indexed codebase: 48 recognized source files, 872 KB.
- TypeScript (language) — detected in the code

## Codebase structure (from repository index)

### Files (59 of 59)

```
.gitattributes
.github/workflows/cross-platform.yml
.gitignore
codex-agent-manager/.gitignore
codex-agent-manager/package.json
codex-agent-manager/README.md
codex-agent-manager/scripts/tsc.mjs
codex-agent-manager/src/cli.ts
codex-agent-manager/src/codexControlFactory.ts
codex-agent-manager/src/compatibility.ts
codex-agent-manager/src/gitWorkspace.ts
codex-agent-manager/src/index.ts
codex-agent-manager/src/manager.ts
codex-agent-manager/src/runnerConfig.ts
codex-agent-manager/src/runnerDaemon.ts
codex-agent-manager/src/runnerEnrollment.ts
codex-agent-manager/src/runnerHub.ts
codex-agent-manager/src/server.ts
codex-agent-manager/src/shutdown.ts
codex-agent-manager/src/skillLibrary.ts
codex-agent-manager/src/skillPackages.ts
codex-agent-manager/src/stateStore.ts
codex-agent-manager/src/tailscale.ts
codex-agent-manager/src/telegram.ts
codex-agent-manager/src/telegramBindings.ts
codex-agent-manager/src/telegramCoordinator.ts
codex-agent-manager/src/telegramMessenger.ts
codex-agent-manager/src/telegramRequests.ts
codex-agent-manager/src/types.ts
codex-agent-manager/src/ui/topology.ts
codex-agent-manager/test/codexControlFactory.test.ts
codex-agent-manager/test/compatibility.test.ts
codex-agent-manager/test/gitWorkspace.test.ts
codex-agent-manager/test/manager.test.ts
codex-agent-manager/test/runner.test.ts
codex-agent-manager/test/runnerEnrollment.test.ts
codex-agent-manager/test/server.test.ts
codex-agent-manager/test/shutdown.test.ts
codex-agent-manager/test/skillLibrary.test.ts
codex-agent-manager/test/stateStore.test.ts
codex-agent-manager/test/tailscale.test.ts
codex-agent-manager/test/telegram.test.ts
codex-agent-manager/tsconfig.json
codex-control/.gitignore
codex-control/examples/basic.mjs
codex-control/package.json
codex-control/README.md
codex-control/src/approval.ts
codex-control/src/client.ts
codex-control/src/dynamicTools.ts
codex-control/src/index.ts
codex-control/src/jsonRpcPeer.ts
codex-control/src/session.ts
codex-control/src/stdioTransport.ts
codex-control/src/types.ts
codex-control/test/control.test.ts
codex-control/tsconfig.json
LICENSE
README.md
```

### Dependencies

- codex-agent-manager/package.json: @types/node@^25.7.0, @types/three@^0.185.0, codex-control@file:../codex-control, three@^0.185.1, typescript@^6.0.3
- codex-control/package.json: @types/node@^25.7.0, typescript@^6.0.3

### Recent commits (newest first)

- Refresh README and add MIT license
- Release Telegram bot bindings on agent deletion
- Fix cross-platform tests and disable push CI
- Configure agent instance scaling
- Expand topology workspace
- Use modern Windows folder picker
- Add Windows folder picker and creation feedback
- Filter portable skills and clarify machine labels
- Ship fixed Windows runner launcher
- Fix Windows runner startup and shutdown
- Add live runner inventory
- Document Telegram runners and skill sharing
- Add shared runner skill library
- Add owner-only Telegram approvals
- Automate private Tailscale runner setup
- Add one-command runner enrollment
- Add Telegram reply routing
- Complete Telegram agent workflow
- Add Telegram mention intake
- Add Telegram setup to topology inspector

## Key source files (fetched from GitHub, selected and truncated for size)

### codex-control/package.json

```
{
  "name": "codex-control",
  "version": "1.0.0",
  "description": "Reusable control layer for Codex App Server.",
  "type": "module",
  "main": "dist/src/index.js",
  "types": "dist/src/index.d.ts",
  "files": [
    "dist",
    "README.md"
  ],
  "engines": {
    "node": ">=22.13.0"
  },
  "scripts": {
    "build": "tsc -p tsconfig.json",
    "test": "npm run build && node --test dist/test/*.test.js",
    "check": "npm run test"
  },
  "keywords": [],
  "author": "",
  "license": "MIT",
  "devDependencies": {
    "@types/node": "^25.7.0",
    "typescript": "^6.0.3"
  }
}

```

### codex-agent-manager/package.json

```
{
  "name": "squadai",
  "version": "0.1.0",
  "description": "Control plane and distributed runner for teams of Codex agents.",
  "type": "module",
  "main": "dist/src/index.js",
  "types": "dist/src/index.d.ts",
  "bin": {
    "squadai": "dist/src/cli.js"
  },
  "files": [
    "dist/src",
    "README.md"
  ],
  "engines": {
    "node": ">=22.13.0"
  },
  "scripts": {
    "build": "node scripts/tsc.mjs",
    "test": "npm run build && node --test dist/test/*.test.js",
    "coverage": "npm run build && node --test --experimental-test-coverage '--test-coverage-include=dist/src/**/*.js' --test-coverage-lines=80 --test-coverage-branches=80 --test-coverage-functions=80 dist/test/*.test.js",
    "check": "npm run coverage",
    "start": "npm run build && node dist/src/cli.js"
  },
  "dependencies": {
    "codex-control": "file:../codex-control",
    "three": "^0.185.1"
  },
  "devDependencies": {
    "@types/node": "^25.7.0",
    "@types/three": "^0.185.0",
    "typescript": "^6.0.3"
  },
  "license": "MIT"
}

```

### codex-control/src/index.ts

```typescript
export { CodexControlClient, type CodexControlClientOptions } from "./client.js";
export { DynamicToolManager } from "./dynamicTools.js";
export { CodexSession } from "./session.js";
export {
  CODEX_DESKTOP_BINARY,
  createCodexLaunchSpec,
  resolveCodexBinary,
  StdioCodexAppServerTransport,
  type CodexBinaryResolutionOptions,
  type CodexLaunchResolutionOptions,
  type CodexLaunchSpec,
  type StdioTransportOptions,
} from "./stdioTransport.js";
export type {
  AppServerTransport,
  ApprovalHandler,
  ApprovalPolicy,
  ApprovalRequest,
  ApprovalResponse,
  AskOptions,
  AuditRecord,
  AuditSink,
  CodexModelCatalogProvider,
  CodexModelOption,
  CodexSkillMetadata,
  CodexSkillScope,
  CodexRuntimeInfo,
  Confirmation,
  DynamicTool,
  DynamicToolCallOutputContentItem,
  DynamicToolCallResponse,
  DynamicToolHandler,
  DynamicToolSpec,
  ExternalWritePolicy,
  JsonRpcMessage,
  ModelListOptions,
  ModelListResult,
  ModelServiceTier,
  MutationPolicy,
  ReasoningEffort,
  ReasoningEffortOption,
  SandboxMode,
  SessionStartOptions,
  SessionResumeOptions,
  SkillListOptions,
  SkillListResult,
  TurnResult,
} from "./types.js";
export { CodexAppServerError, CodexControlError, CodexTurnTimeoutError } from "./types.js";

```

### codex-agent-manager/src/index.ts

```typescript
export { CodexAgentManager } from "./manager.js";
export { CommandCenterServer, createCommandCenterServer } from "./server.js";
export type { CommandCenterServerOptions } from "./server.js";
export { createDefaultClientFactory } from "./codexControlFactory.js";
export { RunnerDaemon } from "./runnerDaemon.js";
export {
  decodeRunnerEnrollmentBundle,
  encodeRunnerEnrollmentBundle,
  SqliteRunnerEnrollmentStore,
} from "./runnerEnrollment.js";
export {
  enrollRunner,
  loadRunnerConfig,
  runnerConfigPath,
  saveRunnerConfig,
} from "./runnerConfig.js";
export type { SavedRunnerConfig } from "./runnerConfig.js";
export { RemoteSession, RunnerAwareWorkspaceManager, RunnerHub } from "./runnerHub.js";
export { TailscaleService, TailscaleSetupError } from "./tailscale.js";
export {
  codexUserSkillsDirectory,
  exportUserSkill,
  installUserSkill,
  validateSkillPackage,
} from "./skillPackages.js";
export type { SkillPackage, SkillPackageFile } from "./skillPackages.js";
export { SkillLibraryService } from "./skillLibrary.js";
export type {
  DiscoveredRunnerSkill,
  SkillLibraryEntry,
  SkillLibrarySnapshot,
} from "./skillLibrary.js";
export type { TailscalePrivateAccess } from "./tailscale.js";
export {
  SqliteTelegramMessageStore,
  TelegramAgentCallbackListener,
  TelegramListener,
} from "./telegram.js";
export type {
  TelegramAgentCallbackListenerOptions,
  TelegramCallbackQuery,
  TelegramGroupMessage,
  TelegramListenerOptions,
} from "./telegram.js";
export { SqliteTelegramAgentBindingStore, TelegramAgentBindingService } from "./telegramBindings.js";
export type {
  TelegramAgentBinding,
  TelegramAgentBindingServiceOptions,
} from "./telegramBindings.js";
export { SqliteTelegramRequestStore, TelegramMentionIntake } from "./telegramRequests.js";
export type {
  TelegramAgentRequest,
  TelegramApproval,
  TelegramAgentResponse,
  TelegramAgentRequestStatus,
  TelegramMentionIntakeOptions,
} from "./telegramRequests.js";
export { TelegramBotMessenger } from "./telegramMessenger.js";
export type { TelegramBotMessengerOptions } from "./telegramMessenger.js";
export { TelegramCoordinator } from "./telegramCoordinator.js";
export type { TelegramCoordinatorOptions } from "./telegramCoordinator.js";
export { GitWorkspaceManager } from "./gitWorkspace.js";
export type { GitWorkspaceManagerOptions } from "./gitWorkspace.js";
export { JsonFileAgentStateStore, MemoryAgentStateStore, SqliteAgentStateStore } from "./stateStore.js";
export type {
  AgentDefinition,
  AgentDefinitionUpdate,
  AgentExecutionPolicy,
  AgentInstanceLifecycle,
  AgentInstanceResolution,
  AgentEvent,
  AgentEventCursor,
  AgentEventPage,
  AgentEventQuery,
  AgentEventType,
  AgentModelCatalog,
  AgentModelOption,
  AgentNotification,
  AgentNotificationKind,
  AgentNotificationSeverity,
  AgentNotificationStatus,
  AgentSnapshot,
  AgentSkillCatalog,
  AgentSkillMetadata,
  AgentSkillReference,
  AgentSkillScope,
  AgentStateStore,
  AgentStatus,
  AgentWorkspaceManager,
  AgentWorkspaceStatus,
  AskOptions,
  ApprovalHandler,
  ApprovalRequest,
  ApprovalResponse,
  ApprovalScope,
  ApprovalsReviewer,
  CodexAgentManagerOptions,
  CompatibilityApproval,
  CompatibilityApprovalResolution,
  CompatibilityApprovalStatus,
  CompatibilityIssue,
  CompatibilityIssueKind,
  CodexControlClientContext,
  CodexControlClientFactory,
  CodexControlClientLike,
  CodexRuntimeInfo,
  CodexSessionLike,
  JarvisNotificationDelivery,
  ModelServiceTier,
  PersistedAgentManagerState,
  PersistedAgentState,
  ReasoningEffort,
  ReasoningEffortOption,
  RunnerCommand,
  RunnerCommandCompletion,
  RunnerCommandEvent,
  RunnerCommandType,
  RunnerDirectoryListing,
  RunnerRegistration,
  RunnerSnapshot,
  RunnerStatus,
  RoutingMode,
  RoutingDecision,
  SandboxPolicy,
  SensorEvent,
  SensorEventInput,
  SensorEventStatus,
  SendResult,
  WorkItem,
  WorkItemStatus,
} from "./types.js";
export { CodexAgentManagerError } from "./types.js";

```

### codex-agent-manager/src/cli.ts

```typescript
#!/usr/bin/env node
import { dirname, join, resolve } from "node:path";

import { createDefaultClientFactory } from "./codexControlFactory.js";
import { GitWorkspaceManager } from "./gitWorkspace.js";
import { CodexAgentManager } from "./manager.js";
import { SkillLibraryService } from "./skillLibrary.js";
import { enrollRunner, loadRunnerConfig, runnerConfigPath, type SavedRunnerConfig } from "./runnerConfig.js";
import { RunnerDaemon } from "./runnerDaemon.js";
import { SqliteRunnerEnrollmentStore } from "./runnerEnrollment.js";
import { RunnerAwareWorkspaceManager, RunnerHub } from "./runnerHub.js";
import { createCommandCenterServer } from "./server.js";
import { createControlPlaneShutdown } from "./shutdown.js";
import { SqliteAgentStateStore } from "./stateStore.js";
import {
  SqliteTelegramMessageStore,
  TelegramAgentCallbackListener,
  TelegramListener,
} from "./telegram.js";
import { SqliteTelegramAgentBindingStore, TelegramAgentBindingService } from "./telegramBindings.js";
import { TelegramCoordinator } from "./telegramCoordinator.js";
import { SqliteTelegramRequestStore, TelegramMentionIntake } from "./telegramRequests.js";
import { TailscaleService } from "./tailscale.js";
import type { RoutingMode } from "./types.js";

const args = new Map<string, string>();
const positionals: string[] = [];
for (let index = 2; index < process.argv.length; index += 1) {
  const arg = process.argv[index];
  if (!arg?.startsWith("--")) {
    positionals.push(arg ?? "");
    continue;
  }
  const next = process.argv[index + 1];
  args.set(arg.slice(2), next && !next.startsWith("--") ? next : "true");
  if (next && !next.startsWith("--")) index += 1;
}

const port = Number(args.get("port") ?? process.env.CODEX_AGENT_MANAGER_PORT ?? 4317);
const host = args.get("host") ?? process.env.CODEX_AGENT_MANAGER_HOST ?? "127.0.0.1";
const mode = args.get("mode") ?? process.env.CODEX_AGENT_MANAGER_MODE ?? "embedded";
const runnerToken = args.get("runner-token") ?? process.env.CODEX_AGENT_MANAGER_RUNNER_TOKEN ?? "";
const statePath = resolve(
  args.get("state") ?? process.env.CODEX_AGENT_MANAGER_STATE ?? "./codex-agents.state.json",
);
const databasePath = resolve(
  args.get("database") ?? process.env.CODEX_AGENT_MANAGER_DATABASE ?? "./command-center.db",
);
const codexBinary = args.get("codex-binary");
const telegramToken = args.get("telegram-token") ?? process.env.SQUADAI_TELEGRAM_TOKEN;
const routingMode = parseRoutingMode(
  args.get("routing-mode") ?? process.env.CODEX_AGENT_MANAGER_ROUTING_MODE ?? "explicit",
);
if (codexBinary) {
  process.env.CODEX_BINARY = codexBinary;
}

if (positionals[0] === "runner") {
  const command = positionals[1];
  if (command === "connect") {
    const bundle = positionals[2];
    if (!bundle) throw new Error('Usage: squadai runner connect <bundle> [--runner-name "My machine"]');
    const runnerName = args.get("runner-name");
    const sshHost = args.get("ssh-host");
    const config = await enrollRunner(bundle, {
      ...(runnerName ? { name: runnerName } : {}),
      ...(sshHost ? { sshHost } : {}),
    });
    console.log(`Runner ${config.id} enrolled. Configuration saved to ${runnerConfigPath()}`);
    await runRunner(config);
    process.exit(0);
  }
  if (command === "start") {
    await runRunner(await loadRunnerConfig());
    process.exit(0);
  }
  if (command === "status") {
    const config = await loadRunnerConfig();
    const response = await fetch(`${config.controlUrl}/api/runners`);
    const body = await response.json() as {
      runners?: Array<{ id: string; status: string; lastSeenAt: string }>;
      error?: string;
    };
    if (!response.ok) throw new Error(body.error || `Control plane returned HTTP ${response.status}.`);
    const runner = body.runners?.find((item) => item.id === config.id);
    console.log(runner
      ? `Runner ${config.id}: ${runner.status} (last seen ${runner.lastSeenAt})`
      : `Runner ${config.id}: not currently registered with ${config.controlUrl}`);
    process.exit(0);
  }
  throw new Error("Usage: squadai runner <connect|start|status>");
}

if (mode === "runner") {
  const controlUrl = args.get("control-url") ?? process.env.CODEX_AGENT_MANAGER_CONTROL_URL;
  const runnerId = args.get("runner-id") ?? process.env.CODEX_AGENT_MANAGER_RUNNER_ID;
  if (!controlUrl) throw new Error("Runner mode requires --control-url or CODEX_AGENT_MANAGER_CONTROL_URL.");
  if (!runnerId) throw new Error("Runner mode requires --runner-id or CODEX_AGENT_MANAGER_RUNNER_ID.");
  const runnerName = args.get("runner-name") ?? process.env.CODEX_AGENT_MANAGER_RUNNER_NAME;
  const sshHost = args.get("ssh-host") ?? process.env.CODEX_AGENT_MANAGER_SSH_HOST;
  const daemon = new RunnerDaemon({
    controlUrl,
    token: runnerToken,
    id: runnerId,
    ...(runnerName ? { name: runnerName } : {}),
    ...(sshHost ? { sshHost } : {}),
  });
  console.log(`SquadAI runner ${runnerId} connecting to ${controlUrl}`);
  for (const signal of shutdownSignals()) {
    process.once(signal, () => void daemon.close().finally(() => process.exit(0)));
  }
  await daemon.start();
  process.exit(0);
}

if (mode !== "embedded" && mode !== "control") {
  throw new Error(`Invalid mode: ${mode}. Use embedded, control, or runner.`);
}

const runnerEnrollments = new SqliteRunnerEnrollmentStore(databasePath);
const tailscale = new TailscaleService();
const runnerHub = new RunnerHub(
  runnerToken,
  () => new Date(),
  (runnerId, token) => runnerEnrollments.authenticate(runnerId, token),
);
const localClientFactory = createDefaultClientFactory();

const manager = new CodexAgentManager({
  agents: [],
  stateStore: new SqliteAgentStateStore(databasePath, { legacyJsonPath: statePath }),
  routingMode,
  clientFactory: runnerHub.createClientFactory(localClientFactory),
  workspaceManager: new RunnerAwareWorkspaceManager(runnerHub, new GitWorkspaceManager()),
});
const telegramStore = telegramToken ? new SqliteTelegramMessageStore(databasePath) : null;
const telegramBindi
[truncated — 4513 more characters]
```

### codex-agent-manager/src/shutdown.ts

```typescript
export function createControlPlaneShutdown(options: {
  stopInputs: () => Promise<void>;
  stopServer: () => Promise<void>;
  closeServices: () => Promise<void>;
  onComplete?: () => void;
}): () => Promise<void> {
  let shutdownPromise: Promise<void> | null = null;
  return () => {
    shutdownPromise ??= run();
    return shutdownPromise;
  };

  async function run(): Promise<void> {
    await options.stopInputs();
    await options.stopServer();
    await options.closeServices();
    options.onComplete?.();
  }
}

```

### .github/workflows/cross-platform.yml

```yaml
name: Cross-platform

on:
  workflow_dispatch:

jobs:
  test:
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 24
          cache: npm
          cache-dependency-path: |
            codex-control/package-lock.json
            codex-agent-manager/package-lock.json
      - name: Install codex-control
        run: npm ci
        working-directory: codex-control
      - name: Test codex-control
        run: npm test
        working-directory: codex-control
      - name: Install agent manager
        run: npm ci
        working-directory: codex-agent-manager
      - name: Test runner and agent manager
        run: npm test
        working-directory: codex-agent-manager

```

### codex-agent-manager/test/shutdown.test.ts

```typescript
import assert from "node:assert/strict";
import test from "node:test";

import { createControlPlaneShutdown } from "../src/shutdown.js";

test("control plane shutdown stops HTTP before closing databases and runs only once", async () => {
  const calls: string[] = [];
  let serverAcceptingRequests = true;
  let databaseOpen = true;
  const shutdown = createControlPlaneShutdown({
    async stopInputs() {
      calls.push("inputs");
    },
    async stopServer() {
      assert.equal(databaseOpen, true);
      serverAcceptingRequests = false;
      calls.push("server");
    },
    async closeServices() {
      assert.equal(serverAcceptingRequests, false);
      databaseOpen = false;
      calls.push("services");
    },
    onComplete() {
      calls.push("complete");
    },
  });

  const first = shutdown();
  const second = shutdown();
  assert.equal(first, second);
  await Promise.all([first, second]);
  assert.deepEqual(calls, ["inputs", "server", "services", "complete"]);
});

```

### codex-control/src/dynamicTools.ts

```typescript
import type {
  DynamicTool,
  DynamicToolCallResponse,
  DynamicToolSpec,
  JsonRpcMessage,
} from "./types.js";

type JsonObject = Record<string, unknown>;

export class DynamicToolManager {
  private readonly handlers = new Map<string, DynamicTool["handler"]>();

  register(threadId: string, tools: DynamicTool[] = []): void {
    for (const tool of tools) {
      if (tool.handler) {
        this.handlers.set(this.key(threadId, tool.name), tool.handler);
      }
    }
  }

  specs(tools: DynamicTool[] = []): DynamicToolSpec[] {
    return tools.map((tool) => ({
      name: tool.name,
      description: tool.description,
      inputSchema: tool.inputSchema,
      deferLoading: tool.deferLoading ?? false,
    }));
  }

  canHandle(message: JsonRpcMessage): boolean {
    if (message.method !== "item/tool/call") {
      return false;
    }
    const params = (message.params ?? {}) as JsonObject;
    return this.handlers.has(this.key(String(params.threadId ?? ""), String(params.tool ?? "")));
  }

  async handle(message: JsonRpcMessage): Promise<DynamicToolCallResponse> {
    const params = (message.params ?? {}) as JsonObject;
    const threadId = String(params.threadId ?? "");
    const tool = String(params.tool ?? "");
    const handler = this.handlers.get(this.key(threadId, tool));
    if (!handler) {
      return {
        contentItems: [
          { type: "inputText", text: `No dynamic tool handler registered for ${tool}.` },
        ],
        success: false,
      };
    }
    const args = isRecord(params.arguments) ? params.arguments : {};
    try {
      return await handler(args, params);
    } catch (error) {
      return {
        contentItems: [
          {
            type: "inputText",
            text: error instanceof Error ? error.message : String(error),
          },
        ],
        success: false,
      };
    }
  }

  private key(threadId: string, tool: string): string {
    return `${threadId}:${tool}`;
  }
}

function isRecord(value: unknown): value is JsonObject {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

```

### codex-agent-manager/src/codexControlFactory.ts

```typescript
import { isAbsolute, resolve } from "node:path";
import { pathToFileURL } from "node:url";

import type {
  CodexControlClientContext,
  CodexControlClientFactory,
  CodexControlClientLike,
} from "./types.js";

type CodexControlModule = {
  CodexControlClient: new (options?: Record<string, unknown>) => CodexControlClientLike;
};

export function createDefaultClientFactory(
  codexControlModule = process.env.CODEX_CONTROL_PATH ?? "codex-control",
): CodexControlClientFactory {
  return (context?: CodexControlClientContext) => {
    let clientPromise: Promise<CodexControlClientLike> | null = null;
    return {
      async startSession(options: Record<string, unknown>) {
        return (await client()).startSession(options);
      },
      async resumeSession(threadId: string, options?: Record<string, unknown>) {
        return (await client()).resumeSession(threadId, options);
      },
      async listModels(options?: { includeHidden?: boolean }) {
        const loaded = await client();
        if (!loaded.listModels) {
          return { models: [] };
        }
        return loaded.listModels(options);
      },
      async listSkills(options: { cwd: string; forceReload?: boolean }) {
        const loaded = await client();
        if (!loaded.listSkills) {
          throw new Error("This Codex App Server does not support skills/list.");
        }
        return loaded.listSkills(options);
      },
      async getRuntimeInfo() {
        const loaded = await client();
        if (!loaded.getRuntimeInfo) {
          return { userAgent: "", platformFamily: "", platformOs: "", codexHome: "" };
        }
        return loaded.getRuntimeInfo();
      },
      async close() {
        if (clientPromise) {
          await (await clientPromise).close();
        }
      },
    };

    async function client(): Promise<CodexControlClientLike> {
      clientPromise ??= importCodexControl(codexControlModule).then((module) => {
        const { CodexControlClient } = module as CodexControlModule;
        return new CodexControlClient({
          approvalHandler: context?.approvalHandler,
        });
      });
      return clientPromise;
    }
  };
}

function importCodexControl(specifier: string): Promise<unknown> {
  if (isAbsolute(specifier) || specifier.startsWith(".")) {
    const path = isAbsolute(specifier) ? specifier : resolve(specifier);
    return import(pathToFileURL(path).href);
  }
  return import(specifier);
}

```

[33 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]