Dev.to AI πŸ€– Ai πŸ‘ 0 πŸ“– 9 min read

Anthropic Opened Claude Code Projects. Let's Build a Tiny Project Coordinator in TypeScript.

Most multi-agent demos start the same way. Spin up three agents. Tell them the same goal. Hope they do not step on each other. Sometimes that works. Often you get three pull requests that edit the same file, and a f

Most multi-agent demos start the same way.

Spin up three agents.

Tell them the same goal.

Hope they do not step on each other.

Sometimes that works.

Often you get three pull requests that edit the same file, and a fourth conversation explaining why.

On October 9, 2026, ClaudeDevs (Anthropic's developer account) wrote:

We just let in every Pro and Max user from the Claude Code Projects waitlist!

That is the product Anthropic describes in Projects redesigned: from folder to conversation. A project is not a folder of chats anymore. It is a coordinator conversation that scopes a goal, delegates work to parallel threads, reviews what comes back, and assembles a finished result.

Anthropic's own framing is the part I keep:

Projects have threads that do the work and a coordinator that directs them.

And:

Under the hood, each thread is a Claude Code cloud session working on its own branch and copy of the repo. The coordinator keeps work organized, but if any threads work on the same code, the overlap is resolved as a merge conflict just like any other PR.

That distinction matters.

Parallel work is easy.

Coordinated work is the product.

If you can start five agents but cannot say who owns which files, you do not have a project. You have a traffic jam with logs.

So let's build a tiny version.

By the end, you'll run one command:

npx tsx coordinator.ts

And watch one goal split into threads under a daily cap, another goal block on a file clash, and both print an assembled status board.

This is my small model of the idea. It is not how Claude Code Projects works inside. Every goal, file path, and thread cap below is made up.

Table of Contents

  1. The Problem
  2. Step 1: Shared Memory
  3. Step 2: Scope the Goal
  4. Step 3: Open Threads Under a Cap
  5. Step 4: Claim Files Before You Edit
  6. Step 5: Assemble What Finished
  7. Step 6: Run It
  8. Where It Breaks Down
  9. The Bigger Idea

1. The Problem

A project coordinator has to answer four questions.

What work belongs in its own thread?

How many threads can we start today?

Who already claimed which files?

What can we ship from what finished?

Most agent loops answer only the first one, by dumping the whole goal into one prompt.

Then the merge conflict answers the third one for you.

Scope first. Cap second. Claim third. Assemble last. The order is the design.

2. Step 1: Shared Memory

Anthropic says every thread draws from shared project memory. Release dates, decisions, and who to ping before touching billing.

In the tiny version, memory is a list of facts.

type Fact = { key: string; value: string; by: string };

type ThreadPlan = { id: string; title: string; files: string[] };

type Thread = ThreadPlan & {
  status: "queued" | "running" | "done" | "blocked";
  result?: string;
  reason?: string;
};

type Project = {
  name: string;
  goal: string;
  memory: Fact[];
  threads: Thread[];
  dailyCap: number;
  startedToday: number;
  claimed: Map<string, string>; // file -> thread id
};

function remember(p: Project, key: string, value: string, by: string) {
  const i = p.memory.findIndex((f) => f.key === key);
  const fact = { key, value, by };
  if (i >= 0) p.memory[i] = fact;
  else p.memory.push(fact);
}

function recall(p: Project, key: string): string | undefined {
  return p.memory.find((f) => f.key === key)?.value;
}

remember overwrites a key. recall reads it.

The human writes release=Friday. Threads read it when they finish, so the assembled board stays consistent without re-prompting.

That is not RAG. It is a whiteboard with keys.

3. Step 2: Scope the Goal

Scoping is where a real system would call a model.

Here the planner is mocked. Two demo goals return fixed thread plans.

function scope(goal: string): ThreadPlan[] {
  if (goal.includes("v1 checkout")) {
    return [
      { id: "api", title: "migrate API callers off v1", files: ["api/routes/checkout.ts", "api/clients/v1.ts"] },
      { id: "web", title: "point web checkout at v2", files: ["web/checkout.tsx", "web/api.ts"] },
      { id: "mobile", title: "point mobile checkout at v2", files: ["mobile/Checkout.tsx", "mobile/api.ts"] },
      { id: "docs", title: "mark v1 deprecated in docs", files: ["docs/checkout.md"] },
    ];
  }
  if (goal.includes("p75")) {
    return [
      { id: "profile", title: "profile checkout endpoints", files: ["api/routes/checkout.ts"] },
      { id: "cache", title: "add response cache for catalog", files: ["api/cache.ts", "api/routes/catalog.ts"] },
      { id: "query", title: "trim checkout SQL", files: ["api/db/checkout.sql", "api/routes/checkout.ts"] },
    ];
  }
  return [{ id: "solo", title: "handle the whole goal", files: ["README.md"] }];
}

Each plan has an id, a title, and the files it will touch.

Anthropic's article uses the same shape: retire a deprecated v1 endpoint with one thread per repo, or profile endpoints and open PRs in parallel.

The important part is that the coordinator owns the split. The threads do not invent their own scope.

4. Step 3: Open Threads Under a Cap

Claude Code Projects can reach usage limits faster because each thread is a full session. Anthropic documents project-specific usage controls. Press reports around the waitlist opening also mention a daily new-thread allowance. Treat that number as press, not as a quote from the article above.

In this demo the cap is an example input: three threads per day for the first project.

function openThreads(p: Project, plans: ThreadPlan[]): string[] {
  const notes: string[] = [];
  for (const plan of plans) {
    if (p.startedToday >= p.dailyCap) {
      p.threads.push({ ...plan, status: "queued", reason: `daily thread cap ${p.dailyCap} hit` });
      notes.push(`[QUEUE] ${plan.id}  daily cap ${p.dailyCap} hit`);
      continue;
    }
    p.threads.push({ ...plan, status: "running" });
    p.startedToday += 1;
    notes.push(`[START] ${plan.id}  ${plan.title}`);
  }
  return notes;
}

Threads that fit the cap start as running.

The rest stay queued with a reason.

Queued is not failed. It is waiting for tomorrow, or for a human to raise the cap.

5. Step 4: Claim Files Before You Edit

Anthropic resolves overlapping edits as merge conflicts on branches.

The tiny version is stricter and earlier. A thread must claim its files before it can finish. First claim wins.

function claimFiles(p: Project, t: Thread): string | null {
  const taken = t.files.filter((f) => p.claimed.has(f) && p.claimed.get(f) !== t.id);
  if (taken.length > 0) {
    const owners = [...new Set(taken.map((f) => p.claimed.get(f)!))];
    return `files claimed by ${owners.join(", ")}: ${taken.join(", ")}`;
  }
  for (const f of t.files) p.claimed.set(f, t.id);
  return null;
}

function runThread(p: Project, t: Thread): string {
  const clash = claimFiles(p, t);
  if (clash) {
    t.status = "blocked";
    t.reason = clash;
    return `[BLOCK] ${t.id}  ${t.reason}`;
  }

  const deadline = recall(p, "release") ?? "unset";
  t.status = "done";
  t.result = `${t.title} (release ${deadline})`;
  remember(p, `done:${t.id}`, t.result, t.id);
  if (t.id === "profile") remember(p, "hotspot", "POST /checkout confirm", t.id);
  if (t.id === "api") remember(p, "v1-callers", "3 remaining in api/", t.id);
  return `[DONE ] ${t.id}  ${t.result}`;
}

If another thread already claimed api/routes/checkout.ts, the new thread is blocked with a printed reason.

That is the whole feature.

You want the conflict in the coordinator log, not in a surprise PR fight on Friday.

6. Step 5: Assemble What Finished

A coordinator that never summarizes is just a process spawner.

function assemble(p: Project): string {
  const done = p.threads.filter((t) => t.status === "done");
  const blocked = p.threads.filter((t) => t.status === "blocked");
  const queued = p.threads.filter((t) => t.status === "queued");
  const lines = [
    `project: ${p.name}`,
    `goal: ${p.goal}`,
    `memory: ${p.memory.map((f) => `${f.key}=${f.value}`).join("; ") || "(empty)"}`,
    `done: ${done.map((t) => t.id).join(", ") || "(none)"}`,
    `blocked: ${blocked.map((t) => `${t.id} (${t.reason})`).join("; ") || "(none)"}`,
    `queued: ${queued.map((t) => t.id).join(", ") || "(none)"}`,
  ];
  return lines.join("\n");
}

The board lists memory, done threads, blocked threads, and queued threads.

Humans read boards. Agents should leave one.

7. Step 6: Run It

Save the file as coordinator.ts, install, and run:

npm install
npx tsx coordinator.ts

Example output (example inputs only):

tiny-project-coordinator demo

=== demo 1: retire v1 checkout (cap 3) ===
[START] api  migrate API callers off v1
[START] web  point web checkout at v2
[START] mobile  point mobile checkout at v2
[QUEUE] docs  daily cap 3 hit
[DONE ] api  migrate API callers off v1 (release Friday)
[DONE ] web  point web checkout at v2 (release Friday)
[DONE ] mobile  point mobile checkout at v2 (release Friday)

assembled:
project: checkout-v2
goal: retire v1 checkout endpoint across api, web, and mobile
memory: release=Friday; done:api=migrate API callers off v1 (release Friday); v1-callers=3 remaining in api/; done:web=point web checkout at v2 (release Friday); done:mobile=point mobile checkout at v2 (release Friday)
done: api, web, mobile
blocked: (none)
queued: docs

=== demo 2: cut p75 (overlap on checkout.ts) ===
[START] profile  profile checkout endpoints
[START] cache  add response cache for catalog
[START] query  trim checkout SQL
[DONE ] profile  profile checkout endpoints (release next Tue)
[DONE ] cache  add response cache for catalog (release next Tue)
[BLOCK] query  files claimed by profile: api/routes/checkout.ts

assembled:
project: checkout-perf
goal: reduce checkout p75 latency
memory: release=next Tue; done:profile=profile checkout endpoints (release next Tue); hotspot=POST /checkout confirm; done:cache=add response cache for catalog (release next Tue)
done: profile, cache
blocked: query (files claimed by profile: api/routes/checkout.ts)
queued: (none)

Demo 1 retires v1 checkout across api, web, and mobile.

Three threads start. Docs waits behind the daily cap. Memory carries release=Friday into every done result.

Demo 2 tries to cut p75 latency.

profile and cache finish. query blocks because profile already claimed api/routes/checkout.ts.

That is the traffic jam, caught on purpose.

8. Where It Breaks Down

Scoping is mocked. A real coordinator needs a model (or a human) to split work. Bad splits are worse than no splits.

File claims are coarse. Real repos need finer locks, or intentional shared ownership with merge rules. Anthropic leans on git conflicts. This demo refuses the second writer.

Memory is a flat map. No provenance beyond by, no expiry, no access control. Shared memory without rules becomes shared pollution.

Caps are crude. A daily thread count is not the same as a token budget, a dollar budget, or a risk budget. You probably want all three.

No review step. Anthropic's coordinator reviews outputs before assembly. This board only counts statuses. Review is the next gate.

Queued work never auto-resumes. Raising the cap and draining the queue is left as an exercise, same idea as a spend pause in a model router.

9. The Bigger Idea

goal
  β””β†’ scope: thread plans + files
       β””β†’ open: under the daily cap
            β””β†’ claim: files before edit
                 β”œβ†’ done, with shared memory
                 β”œβ†’ blocked, with a reason
                 β””β†’ queued, until the cap resets
                      β””β†’ assemble: one board for a human

The goal provides the intent.

The scope provides the split.

The cap provides the ceiling.

The claim provides the ownership.

The memory provides continuity.

The board provides the handoff.

This part is prediction, not history.

I think "how many agents can we run" is becoming the wrong question for multi-agent work. The interesting question is who coordinates them.

Anthropic putting a coordinator conversation above parallel cloud threads is the right direction. Google’s Gemini agent, Meta’s Muse Wonder focus loop, and OpenAI’s tool-heavy stacks are all circling the same shape from different sides.

Different products. Same direction.

Parallel is a feature. Coordination is the product.

Try Roster

I'm building Roster around this idea: AI employees with real responsibilities, tools, memory, schedules and computer access. They do not just fan out. They keep a lane, share what they learned, and hand back a board a human can read.

If the same follow-ups, handoffs, and waiting loops keep eating your week, give them to an AI employee.

Try Roster β†’

πŸ“° Read the original article on Dev.to AI

Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.