Skip to content

Repository files navigation

Codex Team Coordinator

English | 简体中文

Codex Team Coordinator is a local, repository-agnostic coordination layer for concurrent Codex tasks. It combines a locally installable Codex plugin, a zero-dependency Node.js CLI, a local stdio MCP server, and a shared SQLite event store.

The system prevents two tasks from silently overwriting the same path or repeating the same remote side effect. Conflicts create explicit, explainable negotiation proposals instead of automatic preemption.

Version: 0.1.0 Release record date: 2026-08-20 Runtime: Node.js >=22.5 with built-in node:sqlite

What It Provides

  • Task registration, heartbeat, TTL, and crash detection.
  • Exact file, directory, remote-resource, and capacity-aware leases.
  • One machine-level database, with Git common-dir identity separating path namespaces across repositories and linked worktrees.
  • Parent/child path conflict detection and dependency DAG cycle/block checks.
  • Explainable priority arbitration with no silent lease theft.
  • Negotiation, outbox, quorum, timeout, and manual-override workflows.
  • Lease-authorized, crash-recoverable remote side-effect reservations and 429/backoff signals.
  • Compare-and-swap through optional expectedVersion on mutable aggregates.
  • Lease-scoped Git staging, commit evidence, push state, validation, and handoff.
  • Structured JSON CLI output, MCP tools, append-only events, and recovery tests.

Architecture

flowchart LR
  T1["Codex task A"] --> P1["Plugin skill and MCP client"]
  T2["Codex task B"] --> P2["Plugin skill and MCP client"]
  T3["Codex task C"] --> P3["Plugin skill and MCP client"]
  P1 --> C["Coordinator core"]
  P2 --> C
  P3 --> C
  CLI["ctc CLI"] --> C
  C --> DB["Machine-level SQLite WAL"]
  C --> O["Structured outbox"]
  O --> H["Host thread messaging tool or human delivery"]
  C --> G["Lease-scoped Git commands"]
  C --> R["Remote side-effect idempotency records"]
Loading

There is no central daemon. Each CLI or MCP process opens the same machine-level SQLite database, uses WAL plus BEGIN IMMEDIATE, commits one atomic mutation, and exits or waits for the next MCP call. The default database is:

~/.codex/team-coordinator/state.sqlite

Path leases are keyed by the canonical Git common-dir, so linked worktrees conflict while unrelated repositories do not. Remote resources, rate-limit signals, side-effect reservations, and maxParallel are machine-global. The state directory is mode 0700 and SQLite files are mode 0600 on POSIX systems. Set CTC_DB or pass --db to override the location.

Current Codex Boundary

As reviewed for this release on 2026-08-20, official Codex documentation supports plugins containing skills and bundled MCP servers, and the Codex App Server exposes thread and turn methods. It does not document a plugin capability that grants a bundled local MCP server authority to inject messages into arbitrary existing Codex tasks.

Therefore this project does not claim direct cross-task delivery. It creates a structured outbox item containing the destination thread ID and payload. A cooperating task claims the item with a stable delivery key, uses a real host thread-messaging tool for the final hop, reads back the same host message ID, and only then acknowledges delivery. Otherwise the item remains pending or in-flight for explicit recovery.

The bundled stdio MCP server intentionally implements protocol 2025-06-18: initialize, then notifications/initialized, then tool calls. It does not claim compatibility with the final 2026-07-28 protocol, which removed lifecycle initialization. This release targets the lifecycle currently exercised by the local Codex plugin integration and fails closed on calls before initialization.

References:

Install

Local Checkout

git clone https://github.com/aermin/codex-team-coordinator.git
cd codex-team-coordinator
npm test
npm install -g --prefix "$HOME/.local" .
codex plugin marketplace add "$(pwd)"
codex plugin add codex-team-coordinator@codex-team-coordinator-local --json
codex plugin list --json

The repository is public. Authentication is only required for write access; read-only clones can be anonymous.

After plugin installation, start a new Codex task so its skill and MCP catalog are loaded from the installed plugin snapshot.

Development Refresh

npm run sync:plugin
python3 /path/to/plugin-creator/scripts/validate_plugin.py plugins/codex-team-coordinator
python3 /path/to/plugin-creator/scripts/update_plugin_cachebuster.py plugins/codex-team-coordinator
codex plugin add codex-team-coordinator@codex-team-coordinator-local --json

Run release checks before adding the local cachebuster. The cachebuster is installation-only build metadata and should not be committed as the semantic release version.

First Task

Use stable request IDs for retries. Reusing a request ID returns the original result instead of applying the mutation twice.

FENCE="$(ctc task register \
  --task task-a \
  --cwd "$PWD" \
  --thread <codex-thread-id> \
  --priority shared_infrastructure \
  --progress 0.25 \
  --remaining 30 \
  --yield src/shared.ts \
  --risk "schema change" \
  --request-id task-a-register \
  --json | node -pe 'JSON.parse(require("node:fs").readFileSync(0,"utf8")).fencingToken')"

export CTC_FENCING_TOKEN="$FENCE"

ctc lease acquire \
  --task task-a \
  --fence "$FENCE" \
  --resource path:src/shared.ts \
  --request-id task-a-shared-ts \
  --json

ctc lease acquire \
  --task task-a \
  --fence "$FENCE" \
  --resource remote:deploy/us-east \
  --request-id task-a-deploy-us-east \
  --json

ctc task heartbeat \
  --task task-a \
  --fence "$FENCE" \
  --progress 0.6 \
  --remaining 12 \
  --expected-version 1 \
  --request-id task-a-heartbeat-2 \
  --json

Path resources are relative to the registered worktree root. Existing symlinks are resolved to canonical paths, repository escapes are rejected, and case-insensitive repositories normalize lease keys. A lease on path:src conflicts with path:src/a.ts, and vice versa.

The fencing token is a local capability. It is returned only by task registration and must be retained by that task. Status, lease, negotiation, event, outbox, and side-effect query responses redact it.

Conflict Negotiation

When a resource conflicts, acquisition returns negotiation_required. No lease is revoked.

ctc negotiation show --id <negotiation-id> --json

ctc negotiation respond \
  --id <negotiation-id> \
  --task task-a \
  --fence "$TASK_A_FENCE" \
  --action acknowledge \
  --payload '{"remainingMinutes":5,"canYield":["src/shared.ts"]}' \
  --request-id negotiation-a-ack \
  --json

ctc negotiation decide \
  --id <negotiation-id> \
  --winner task-b \
  --task task-b \
  --fence "$TASK_B_FENCE" \
  --reason "release critical path" \
  --request-id negotiation-decision \
  --json

Default quorum is two distinct participant acknowledgements. A timed-out negotiation or vanished task requires --override-reason; it never silently resolves itself.

Arbitration Algorithm

The default base priority is:

Class Score
incident_security 4000
release_critical 3000
shared_infrastructure 2000
independent_optimization 1000

The explanation includes these additive factors:

score = class
      + explicit override
      + dependency critical-path depth * 100
      + completion ratio * 200
      + 150 when a minimal committable closure is ready
      + side-effect protection ratio * 250

The requester is only recommended when its score exceeds the holder by preemptionMargin. The recommendation is informational. Both tasks still acknowledge or counter, and the holder must commit/push and yield an exact scope before the requester acquires it.

Negotiation State Machine

stateDiagram-v2
  [*] --> detected
  detected --> proposal
  proposal --> acknowledged
  proposal --> countered
  countered --> acknowledged
  acknowledged --> countered
  acknowledged --> decided
  decided --> commit_push
  commit_push --> yielded
  yielded --> handed_off
  handed_off --> resolved
  proposal --> manual_override_required: timeout or vanished task
  acknowledged --> manual_override_required: timeout or vanished task
  countered --> manual_override_required: timeout or vanished task
  manual_override_required --> decided: documented override
  manual_override_required --> cancelled: documented override
  manual_override_required --> resolved: documented override after scope audit
  resolved --> [*]
Loading

When the requester wins, the loser first records a verified handoff. commit_push and yielded must reference that handoff ID; the coordinator verifies the commit object, lease scope, validation evidence, and pushed remote ref before allowing release. yielded releases only matching loser leases. handed_off records transfer completion; resolved closes the negotiation. If the current holder wins, it may resolve directly after the acknowledged decision.

Policy Configuration

Initialize a database with JSON or a JSON file:

ctc init --policy-file ./coordinator.config.example.json --request-id policy-v1 --json
{
  "taskTtlMs": 120000,
  "leaseTtlMs": 120000,
  "negotiationTimeoutMs": 300000,
  "ackRequired": 2,
  "maxParallel": 8,
  "preemptionMargin": 250,
  "requirePushForHandoff": true,
  "resourceCapacities": {
    "remote:build-pool": 3,
    "remote:*": 1
  },
  "priorityClasses": {
    "incident_security": 4000,
    "release_critical": 3000,
    "shared_infrastructure": 2000,
    "independent_optimization": 1000
  }
}

Report rate limiting before retrying a shared remote service:

ctc signal rate-limit \
  --task task-a \
  --fence "$FENCE" \
  --resource remote:provider/api \
  --backoff-ms 60000 \
  --request-id provider-429-1 \
  --json

Idempotent Remote Side Effects

First acquire the exact remote lease. A side-effect reservation without a matching active lease is rejected.

REMOTE_LEASE_ID="$(ctc lease acquire \
  --task task-a \
  --fence "$FENCE" \
  --resource remote:release-lane \
  --request-id task-a-release-lane \
  --json | node -pe 'JSON.parse(require("node:fs").readFileSync(0,"utf8")).lease.id')"

ctc side-effect reserve \
  --task task-a \
  --fence "$FENCE" \
  --resource remote:release-lane \
  --lease "$REMOTE_LEASE_ID" \
  --key release:v1:us-east \
  --json

# Perform the external action once, then read it back authoritatively.

ctc side-effect commit \
  --task task-a \
  --fence "$FENCE" \
  --key release:v1:us-east \
  --result '{"downstreamId":"example-123","readback":"confirmed"}' \
  --request-id release-v1-commit \
  --json

A repeated reservation reports already_reserved or already_committed and does not authorize a second side effect. If the owner or lease expires while a reservation is unresolved, it becomes recovery_required; retry is forbidden until an operator commits authoritative readback, abandons it, or explicitly reauthorizes one retry under a new active lease.

Git Discipline And Handoff

ctc git stage computes changed files and writes only leased paths into a task-specific review index under the coordinator state directory. It does not modify the worktree's shared Git index and preserves unrelated shared staged changes. ctc git commit accepts only that recorded reviewed tree, builds a commit with commit-tree, revalidates task and lease TTL/version immediately before the ref write, advances the branch with compare-and-swap, and then aligns only the committed paths in the shared index. It does not run worktree hooks or silently consume unrelated staged files.

Commit proof persistence is two-phase. A proof is stored as prepared before update-ref, then becomes available only after the ref and shared index are synchronized. If the process dies in that window, new staging is blocked until the exact proof is recovered. Use adopt only when the reviewed commit is current HEAD; use abandon only when the branch ref never moved or has been explicitly moved back.

Each path lease records the Git HEAD that existed when it was acquired, including the zero object ID for an unborn branch. A direct ctc lease release is allowed only when the scope is clean and no reachable commit since that baseline has touched the scope. A later revert does not erase that history. Dirty scopes, manually committed scope changes, and unresolved prepared proofs require a verified handoff or explicit recovery; they cannot be hidden by releasing the lease and completing the task. Root commits are supported through the same isolated-index and proof flow.

ctc git inspect --task task-a --fence "$FENCE" --cwd "$PWD" --json
ctc git stage --task task-a --fence "$FENCE" --cwd "$PWD" --json
COMMIT_SHA="$(ctc git commit --task task-a --fence "$FENCE" --cwd "$PWD" \
  --message "fix: complete shared change" --json \
  | node -pe 'JSON.parse(require("node:fs").readFileSync(0,"utf8")).commitSha')"

git push origin HEAD

HANDOFF_ID="$(ctc handoff record \
  --task task-a \
  --fence "$FENCE" \
  --negotiation <negotiation-id> \
  --commit "$COMMIT_SHA" \
  --pushed \
  --remote origin \
  --validation '{"tests":"npm test","result":"passed"}' \
  --release <lease-id-1> \
  --request-id task-a-handoff \
  --json | node -pe 'JSON.parse(require("node:fs").readFileSync(0,"utf8")).handoffId')"

ctc negotiation advance --id <negotiation-id> --task task-a --fence "$FENCE" \
  --to commit_push --payload "{\"handoffId\":\"$HANDOFF_ID\"}" \
  --request-id task-a-commit-push --json
ctc negotiation advance --id <negotiation-id> --task task-a --fence "$FENCE" \
  --to yielded --payload "{\"handoffId\":\"$HANDOFF_ID\"}" \
  --request-id task-a-yielded --json
ctc negotiation advance --id <negotiation-id> --task task-a --fence "$FENCE" \
  --to handed_off --payload "{\"handoffId\":\"$HANDOFF_ID\"}" \
  --request-id task-a-handed-off --json

The default policy refuses an unpushed handoff. A documented override is available for intentionally local workflows.

Handoff accepts only the current HEAD, requires a matching CTC commit proof, rejects dirty released path scopes, verifies all released lease IDs were active in the proof, and reads back a declared remote ref when pushed=true.

Crash Recovery

Expired tasks become vanished. Their active leases become orphaned and continue blocking conflicting work.

ctc recover reconcile --request-id recovery-scan-1 --json

ctc recover release-orphaned \
  --lease <lease-id> \
  --override-reason "owner process confirmed terminated and worktree inspected" \
  --request-id recovery-release-1 \
  --json

ctc recover side-effect \
  --key <side-effect-key> \
  --recovery-action commit \
  --result '{"downstreamId":"example-123","readback":"confirmed"}' \
  --override-reason "authoritative downstream readback confirms completion" \
  --request-id recovery-side-effect-1 \
  --json

ctc recover outbox \
  --id <outbox-id> \
  --recovery-action reset \
  --override-reason "sender crashed before receipt was recorded" \
  --request-id recovery-outbox-1 \
  --json

ctc recover git-proof \
  --commit <prepared-commit-sha> \
  --recovery-action adopt \
  --override-reason "HEAD, parent, tree, files, leases, and shared index inspected" \
  --request-id recovery-git-proof-1 \
  --json

This is intentionally fail-closed. TTL expiration is a detection signal, not permission to overwrite unknown work.

A prepared proof can be adopted after the original task and leases expire because recovery verifies the persisted tree, parent, changed files, lease evidence, current HEAD, and shared index under a documented override. Any prepared proof for the worktree blocks every task incarnation from restaging, and its orphaned lease cannot be released until that proof is adopted or safely abandoned.

Manual override commands are local operator assertions recorded in the event log. They do not authenticate a human identity or provide multi-user authorization; a process with the same OS-user access can invoke them. Use filesystem and host access controls as the identity boundary.

Outbox Delivery

ctc outbox list --task <recipient-task> --status pending --json

DELIVERY_KEY="codex:<destination-thread>:<outbox-id>"

ctc outbox claim \
  --id <outbox-id> \
  --task <dispatcher-task> \
  --fence "$DISPATCHER_FENCE" \
  --delivery-key "$DELIVERY_KEY" \
  --request-id outbox-claim-1 \
  --json

# Send the claimed payload once with a real host thread-messaging tool. Read
# back the same host message ID and retain the claimed item's payload_digest.

ctc outbox ack \
  --id <outbox-id> \
  --task <dispatcher-task> \
  --fence "$DISPATCHER_FENCE" \
  --delivery-key "$DELIVERY_KEY" \
  --receipt '{"provider":"codex-host-thread-tool","threadId":"...","delivery":"read-back-confirmed","observedAt":"2026-08-20T12:00:00Z","messageId":"host-message-1","readbackMessageId":"host-message-1","payloadDigest":"<payload_digest>"}' \
  --request-id outbox-delivery-1 \
  --json

Any active dispatcher task may claim an item using its own fencing token; it does not need the recipient's capability. The coordinator binds the claim to that dispatcher incarnation and validates destination, matching sent/readback IDs, timestamp, delivery key, and payload digest. The receipt is host-attested data supplied by the dispatcher; the coordinator cannot independently query or authenticate the host messaging provider.

An outbox item without a concrete recipient thread ID cannot be claimed or acknowledged. Register the recipient's actual thread, then use exact outbox reset recovery to rebind the item before attempting delivery.

observedAt must be at or after the claim time. A (provider, threadId, messageId) identity can deliver only one outbox item, preventing an old readback receipt from being reused for a second payload. If the recipient task is re-registered with a new thread ID, the item is stale until exact reset recovery rebinds both the new fencing token and destination thread.

Do not acknowledge an item based only on an attempted send. in_flight is single-flight and blocks a second delivery identity. If the sender crashes, inspect host state and use exact recover outbox reset|cancel; never silently resend.

Migration

Add the coordination contract to an existing AGENTS.md without replacing user-authored content:

ctc migrate agents --path ./AGENTS.md --json
ctc migrate agents --path ./AGENTS.md --write --json

Print a mapping from common legacy session-context operations:

ctc migrate legacy-map --json

A compatibility wrapper is available at scripts/session-context-adapter.mjs for status, claim, release, and guard. It is generic and does not inspect or modify an existing project unless explicitly invoked with that project's path.

Demo And Tests

npm test
npm run demo

The demo runs three tasks that contend for one file, use different remote resources, and form a dependency chain. It asserts:

  • only one active file lease exists at a time;
  • negotiation reaches resolved;
  • the dependent task is blocked until its prerequisite completes;
  • one stable side-effect key produces one committed side effect;
  • all leases are released and no deadlock remains.

The test suite covers unit behavior, concurrent processes, fencing and CAS conflicts, crash recovery, negotiation revision/quorum safety, task-incarnation binding, symlink aliases, linked worktrees, cross-repository remote conflicts, capacity upgrades, side-effect recovery, outbox single-flight recovery, idempotency request hashes, root and reviewed-index commits, handoff verification, database permissions, and real stdio CLI/MCP integration. Release validation separately checks plugin manifests/runtime synchronization, plugin and skill schemas, package contents, global CLI installation, installed plugin discovery, and an installed-snapshot MCP initialize, tools/list, and tools/call smoke.

Failure Semantics

Condition Result
File, parent directory, or remote capacity conflict negotiation_required; current lease remains valid
Missing or incomplete dependency dependency_blocked
Configured parallel task limit reached parallelism_blocked
Active 429/backoff signal rate_limited with retry time
Stale expectedVersion VERSION_CONFLICT; no mutation
Task TTL expires task becomes vanished; leases become orphaned
Any conflict overlaps an orphaned or stale-holder lease recovery_required; exact manual release override is required
Negotiation deadline expires manual_override_required
Side effect lacks a matching remote lease SIDE_EFFECT_LEASE_REQUIRED; no reservation
Reserved side effect owner/lease expires recovery_required; no automatic retry
Outbox item is already claimed OUTBOX_ALREADY_CLAIMED; no second delivery identity
Outbox item has no destination thread OUTBOX_DESTINATION_REQUIRED; register and reset before delivery
Outbox claim replay belongs to an expired incarnation IDEMPOTENCY_STALE; exact recovery required
Receipt predates claim or reuses a host message identity rejected; item remains in_flight
Dispatcher tries to complete with an in-flight claim ACTIVE_COORDINATION_REMAINS
Unrelated changes exist in the shared Git index preserved; task review uses a separate index
Path scope is dirty at direct release LEASE_SCOPE_DIRTY; lease remains held
Path scope changed in Git since lease acquisition VERIFIED_HANDOFF_REQUIRED; direct release is rejected
Reviewed tree contains a path outside the lease COMMIT_SCOPE_VIOLATION; branch ref is unchanged
Git authorization changes before final ref write proof remains prepared or commit is not attached; exact recovery is required
Prepared proof already matches current HEAD abandon is rejected; operator must adopt or explicitly move the ref back
Database schema version is newer than this binary SCHEMA_VERSION_UNSUPPORTED; no downgrade is attempted
Handoff lacks CTC proof, current HEAD, clean scope, or push evidence rejected; leases remain held
Duplicate request ID with identical operation and payload original response is returned
Duplicate request ID with different payload IDEMPOTENCY_CONFLICT; no mutation
Duplicate side-effect key reports prior reservation/commit; no second authorization

Threat Model

Protected:

  • accidental concurrent edits across worktrees, parent/child paths, symlink aliases, and case aliases;
  • stale or duplicate local mutations;
  • implicit lease takeover after a crash;
  • duplicate remote side effects by cooperating tasks, including uncertain crash outcomes;
  • unrelated Git staging, hook/index races, stale authorization, fabricated or old commits, and ambiguous handoff evidence;
  • duplicate outbox delivery identities and false delivery acknowledgements.

Not protected:

  • a malicious process running as the same OS user that edits or deletes the SQLite database;
  • verified human identity or multi-user authorization for manual overrides;
  • tasks that bypass the coordinator entirely;
  • cross-machine consensus or distributed clock guarantees;
  • correctness of an external service without downstream idempotency and readback;
  • message delivery when the host exposes no thread messaging API.

The database contains task metadata, canonical local repository identities, paths relative to repositories, thread IDs, event payloads, and delivery receipts. It must not contain credentials, secrets, proprietary source, or sensitive message bodies. Keep the database under normal local filesystem protections and do not commit it.

Security And Privacy

This repository contains no organization-specific source, internal paths, task/run identifiers, or credentials. See SECURITY.md for reporting and operational guidance.

npm run check requires every current release file to be present in the Git index with no unstaged changes. It rejects symlinks, scans current and staged files, scans Git ref names, and scans every reachable blob plus commit/tag metadata with the same secret rules. A credential committed and later deleted, or hidden in a commit or annotated-tag message, still blocks release. Repository-specific protected terms are supplied outside the repository as a JSON array in CTC_RELEASE_DENYLIST_JSON; the release scanner never embeds private organization or project names in source.

License

MIT. The GitHub repository is public and retains an open-source-style structure and license.

About

Fail-closed coordination for concurrent Codex tasks

Resources

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages