@codex-modules/teams 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/prompt.js ADDED
@@ -0,0 +1,42 @@
1
+ import { memberAgentName, resolveMemberSandbox } from "./team.js";
2
+ export function assembleDeveloperInstructions(team, member) {
3
+ const sandbox = resolveMemberSandbox(team, member);
4
+ const lines = [
5
+ `You are team member "${member.name}" of team "${team.name}". Focus: ${member.focus}. Lens: ${member.lens}. Deliverable: ${member.deliverable}.`,
6
+ `Return your final report with the last line exactly in this format: TEAM-RESULT: <one-line summary>.`,
7
+ `Do not call codex-teams task or note commands yourself; the leader owns durable state updates.`,
8
+ ];
9
+ if (sandbox === "workspace-write") {
10
+ lines.push(`You may also write supporting artifacts under .codex-teams/${team.name}/artifacts/${member.name}/, but the TEAM-RESULT final line remains the canonical result channel.`);
11
+ }
12
+ if (member.instructions && member.instructions.trim())
13
+ lines.push(member.instructions.trim());
14
+ return `${lines.join("\n\n")}\n`;
15
+ }
16
+ export function assembleLeaderPrompt(team, goal) {
17
+ const roster = team.members
18
+ .map(member => `- ${member.name}: agent_type=${memberAgentName(team, member)}; focus=${member.focus}; deliverable=${member.deliverable}`)
19
+ .join("\n");
20
+ return `You are the leader for Codex team "${team.name}".
21
+
22
+ Goal:
23
+ ${goal}
24
+
25
+ Roster:
26
+ ${roster}
27
+
28
+ Use the stable native multi-agent tool path only:
29
+ 1. Load tools with tool_search for multi_agent_v1.spawn_agent, wait, close, resume, and send_input.
30
+ 2. Start durable state before spawning: codex-teams state init ${team.name} --goal ${shellQuote(goal)}
31
+ 3. After each spawn, record the returned agent id: codex-teams member bind ${team.name} <member> --agent-id <id> --nickname <nick>
32
+ 4. Track work with codex-teams task add, task claim, task complete, and task fail. Use codex-teams note add for shared observations.
33
+ 5. Spawn only independent tasks concurrently. Join with wait. If a member fails, close it and return its claimed task to a safe state.
34
+ 6. Treat each member's final TEAM-RESULT line as the canonical result. Artifact files are optional supporting evidence for workspace-write members.
35
+ 7. Integrate and verify the final answer yourself. Finish with: codex-teams state finish ${team.name} --status ok or --status partial.
36
+
37
+ Do not use codex_app.*, spawn_agents_on_csv, multi_agent_v2, or trust/config mutation for this MVP workflow.
38
+ `;
39
+ }
40
+ function shellQuote(value) {
41
+ return `'${value.replace(/'/g, "'\\''")}'`;
42
+ }
@@ -0,0 +1,32 @@
1
+ import type { SandboxMode } from "./types.js";
2
+ export type RunOptions = {
3
+ goal: string;
4
+ codexHome?: string;
5
+ sandbox?: SandboxMode;
6
+ timeoutSec?: number;
7
+ stallSec?: number;
8
+ execute?: boolean;
9
+ allowCodex?: boolean;
10
+ cwd?: string;
11
+ stateDir?: string;
12
+ env?: NodeJS.ProcessEnv | Record<string, string | undefined>;
13
+ };
14
+ export type DryRunPlan = {
15
+ mode: "dry-run";
16
+ argv: string[];
17
+ prompt: string;
18
+ runDir: string;
19
+ };
20
+ export type RunResult = {
21
+ mode: "executed";
22
+ status: "ok" | "error" | "timeout" | "stall";
23
+ exitCode: number | null;
24
+ argv: string[];
25
+ runDir: string;
26
+ eventsPath: string;
27
+ lastMessagePath: string;
28
+ summaryPath: string;
29
+ };
30
+ export declare function buildRunPlan(teamPath: string, opts: RunOptions): DryRunPlan;
31
+ export declare function runTeam(teamPath: string, opts: RunOptions): Promise<DryRunPlan | RunResult>;
32
+ export declare function buildCodexArgv(prompt: string, runDir: string, opts: RunOptions): string[];
package/dist/runner.js ADDED
@@ -0,0 +1,156 @@
1
+ import { spawn } from "node:child_process";
2
+ import { closeSync, createWriteStream, existsSync, mkdirSync, openSync, readFileSync, statSync, writeFileSync } from "node:fs";
3
+ import { basename, join } from "node:path";
4
+ import { findCodexBinary, resolveCodexHome } from "./agents.js";
5
+ import { parseTeamJson } from "./team.js";
6
+ import { assembleLeaderPrompt } from "./prompt.js";
7
+ import { resolveStateRoot, writeJsonAtomic } from "./state.js";
8
+ export function buildRunPlan(teamPath, opts) {
9
+ const team = parseTeamJson(teamPath);
10
+ const prompt = assembleLeaderPrompt(team, opts.goal);
11
+ const runDir = resolveRunDir(team, opts);
12
+ return {
13
+ mode: "dry-run",
14
+ argv: buildCodexArgv(prompt, runDir, opts),
15
+ prompt,
16
+ runDir,
17
+ };
18
+ }
19
+ export async function runTeam(teamPath, opts) {
20
+ assertSafeRunOptions(opts);
21
+ const plan = buildRunPlan(teamPath, opts);
22
+ if (!opts.execute || !opts.allowCodex)
23
+ return plan;
24
+ if ((opts.sandbox ?? "workspace-write") === "read-only") {
25
+ throw new Error("executed team runs require workspace-write because the leader writes .codex-teams state");
26
+ }
27
+ const bin = findCodexBinary(opts.env);
28
+ if (!bin)
29
+ throw new Error("codex binary not found");
30
+ mkdirSync(plan.runDir, { recursive: true });
31
+ const eventsPath = join(plan.runDir, "events.jsonl");
32
+ const stderrPath = join(plan.runDir, "stderr.log");
33
+ const lastMessagePath = join(plan.runDir, "last-message.md");
34
+ const summaryPath = join(plan.runDir, "summary.json");
35
+ const startedAt = Date.now();
36
+ let exitCode = null;
37
+ let status = null;
38
+ await new Promise(resolve => {
39
+ const devNullFd = openSync("/dev/null", "r");
40
+ const events = createWriteStream(eventsPath, { flags: "a" });
41
+ const stderr = createWriteStream(stderrPath, { flags: "a" });
42
+ const child = spawn(bin, plan.argv, {
43
+ cwd: opts.cwd ?? process.cwd(),
44
+ env: {
45
+ ...process.env,
46
+ ...(opts.env ?? {}),
47
+ CODEX_HOME: resolveCodexHome(opts.env, opts.codexHome),
48
+ },
49
+ stdio: [devNullFd, "pipe", "pipe"],
50
+ });
51
+ if (!child.stdout || !child.stderr) {
52
+ status = "error";
53
+ child.kill("SIGTERM");
54
+ closeFd(devNullFd);
55
+ events.end(() => stderr.end(() => resolve()));
56
+ return;
57
+ }
58
+ child.stdout.pipe(events);
59
+ child.stderr.pipe(stderr);
60
+ const timeoutTimer = setTimeout(() => {
61
+ status = "timeout";
62
+ child.kill("SIGTERM");
63
+ }, (opts.timeoutSec ?? 600) * 1000);
64
+ const stallTimer = setInterval(() => {
65
+ if (Date.now() - latestMtimeMs([eventsPath, stderrPath, lastMessagePath], startedAt) > (opts.stallSec ?? 180) * 1000) {
66
+ status = "stall";
67
+ child.kill("SIGTERM");
68
+ }
69
+ }, 1000);
70
+ child.once("error", () => {
71
+ status = "error";
72
+ });
73
+ child.once("close", code => {
74
+ exitCode = code;
75
+ clearTimeout(timeoutTimer);
76
+ clearInterval(stallTimer);
77
+ closeFd(devNullFd);
78
+ events.end(() => stderr.end(() => resolve()));
79
+ });
80
+ });
81
+ if (!status)
82
+ status = exitCode === 0 ? "ok" : "error";
83
+ writeJsonAtomic(summaryPath, summarizeEvents(eventsPath, { status, exitCode, teamPath: basename(teamPath) }));
84
+ return { mode: "executed", status, exitCode, argv: plan.argv, runDir: plan.runDir, eventsPath, lastMessagePath, summaryPath };
85
+ }
86
+ export function buildCodexArgv(prompt, runDir, opts) {
87
+ const sandbox = opts.sandbox ?? "workspace-write";
88
+ if (sandbox !== "read-only" && sandbox !== "workspace-write")
89
+ throw new Error("sandbox must be read-only or workspace-write");
90
+ return [
91
+ "exec",
92
+ "-s",
93
+ sandbox,
94
+ "--skip-git-repo-check",
95
+ "--json",
96
+ "--ephemeral",
97
+ "-o",
98
+ join(runDir, "last-message.md"),
99
+ prompt,
100
+ ];
101
+ }
102
+ function resolveRunDir(team, opts) {
103
+ const safeStamp = new Date().toISOString().replace(/[:.]/g, "-");
104
+ return join(resolveStateRoot({ cwd: opts.cwd, stateDir: opts.stateDir, env: opts.env }), team.name, "runs", safeStamp);
105
+ }
106
+ function assertSafeRunOptions(opts) {
107
+ if (!opts.goal || !opts.goal.trim())
108
+ throw new Error("--goal is required");
109
+ if (opts.sandbox !== undefined && opts.sandbox !== "read-only" && opts.sandbox !== "workspace-write")
110
+ throw new Error("sandbox must be read-only or workspace-write");
111
+ if (opts.timeoutSec !== undefined && (!Number.isFinite(opts.timeoutSec) || opts.timeoutSec <= 0))
112
+ throw new Error("timeout-sec must be > 0");
113
+ if (opts.stallSec !== undefined && (!Number.isFinite(opts.stallSec) || opts.stallSec <= 0))
114
+ throw new Error("stall-sec must be > 0");
115
+ }
116
+ function summarizeEvents(path, base) {
117
+ const collabToolCalls = [];
118
+ if (existsSync(path)) {
119
+ for (const line of readFileSync(path, "utf8").split(/\r?\n/)) {
120
+ if (!line.trim())
121
+ continue;
122
+ try {
123
+ const item = JSON.parse(line);
124
+ if (JSON.stringify(item).includes("collab_tool_call"))
125
+ collabToolCalls.push(item);
126
+ }
127
+ catch {
128
+ // ignore non-JSON event lines
129
+ }
130
+ }
131
+ }
132
+ else {
133
+ writeFileSync(path, "");
134
+ }
135
+ return { ...base, collabToolCallCount: collabToolCalls.length, collabToolCalls };
136
+ }
137
+ function latestMtimeMs(paths, fallback) {
138
+ let latest = fallback;
139
+ for (const path of paths) {
140
+ try {
141
+ latest = Math.max(latest, statSync(path).mtimeMs);
142
+ }
143
+ catch {
144
+ // file may not exist yet
145
+ }
146
+ }
147
+ return latest;
148
+ }
149
+ function closeFd(fd) {
150
+ try {
151
+ closeSync(fd);
152
+ }
153
+ catch {
154
+ // best effort
155
+ }
156
+ }
@@ -0,0 +1,14 @@
1
+ export type SkillInstallOptions = {
2
+ codexHome?: string;
3
+ force?: boolean;
4
+ env?: NodeJS.ProcessEnv | Record<string, string | undefined>;
5
+ };
6
+ export declare function renderTeamsSkill(): string;
7
+ export declare function installSkill(opts?: SkillInstallOptions): {
8
+ file: string;
9
+ backup: string | null;
10
+ };
11
+ export declare function uninstallSkill(opts?: SkillInstallOptions): {
12
+ removed: string[];
13
+ restored: string[];
14
+ };
package/dist/skill.js ADDED
@@ -0,0 +1,84 @@
1
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
2
+ import { createHash } from "node:crypto";
3
+ import { join } from "node:path";
4
+ import { backupSkillFile, resolveCodexHome, readManifest, writeManifest } from "./agents.js";
5
+ import { nowIso, writeFileAtomic } from "./state.js";
6
+ export function renderTeamsSkill() {
7
+ return `---
8
+ name: teams
9
+ description: Use when a task should be handled by a declared Codex team with native stable multi-agent collaboration, durable task leases, and a leader-owned journal.
10
+ ---
11
+
12
+ # Codex Teams
13
+
14
+ Use this skill when the user asks for a team, panel, swarm, or parallel collaboration and a local team definition is available.
15
+
16
+ 1. Find a team definition in ./team.json first, then .codex-teams/*/team.json if present.
17
+ 2. If the team is not installed, tell the user to run codex-teams install <team.json>. Do not edit trust or config.toml automatically.
18
+ 3. Load the native multi-agent tools with tool_search. Use stable multi_agent_v1.spawn_agent, wait, close, resume, and send_input only.
19
+ 4. Start durable state with codex-teams state init <team> --goal "<goal>" before spawning members.
20
+ 5. After each spawn, record the returned id with codex-teams member bind <team> <member> --agent-id <id> --nickname <nick>.
21
+ 6. Use codex-teams task add/claim/complete/fail and codex-teams note add/list for leader-owned state. Members should report through their final TEAM-RESULT line.
22
+ 7. Use wait to collect member results, inspect any optional artifacts, and integrate the final answer yourself.
23
+ 8. Finish with codex-teams state finish <team> --status ok or --status partial.
24
+
25
+ Do not use codex_app.*, spawn_agents_on_csv, multi_agent_v2, or automatic trust mutation for this MVP workflow.
26
+ `;
27
+ }
28
+ export function installSkill(opts = {}) {
29
+ const root = join(resolveCodexHome(opts.env, opts.codexHome), "skills");
30
+ const dir = join(root, "teams");
31
+ const file = join(dir, "SKILL.md");
32
+ mkdirSync(dir, { recursive: true });
33
+ const manifest = readManifest(root);
34
+ const existing = manifest.entries.find(entry => entry.kind === "skill" && entry.file === file);
35
+ let backup = existing?.backup ?? null;
36
+ if (existsSync(file) && !existing) {
37
+ if (!opts.force)
38
+ throw new Error(`refusing to overwrite unmanaged skill file: ${file} (use --force to back it up first)`);
39
+ backup = backupSkillFile(file, join(root, ".codex-teams-backups"));
40
+ }
41
+ const content = renderTeamsSkill();
42
+ writeFileAtomic(file, content);
43
+ const entries = manifest.entries.filter(entry => !(entry.kind === "skill" && entry.file === file));
44
+ entries.push({
45
+ team: "teams",
46
+ scope: "skill",
47
+ file,
48
+ backup,
49
+ hash: sha256(content),
50
+ kind: "skill",
51
+ installed_at: nowIso(opts.env),
52
+ });
53
+ writeManifest(root, { ...manifest, entries });
54
+ return { file, backup };
55
+ }
56
+ export function uninstallSkill(opts = {}) {
57
+ const root = join(resolveCodexHome(opts.env, opts.codexHome), "skills");
58
+ const manifest = readManifest(root);
59
+ const removed = [];
60
+ const restored = [];
61
+ const keep = [];
62
+ for (const entry of manifest.entries) {
63
+ if (entry.kind !== "skill" || entry.team !== "teams") {
64
+ keep.push(entry);
65
+ continue;
66
+ }
67
+ if (existsSync(entry.file)) {
68
+ const currentHash = sha256(readFileSync(entry.file, "utf8"));
69
+ if (currentHash !== entry.hash)
70
+ throw new Error(`installed skill changed since install; refusing uninstall without manual review: ${entry.file}`);
71
+ rmSync(entry.file, { force: true });
72
+ removed.push(entry.file);
73
+ }
74
+ if (entry.backup && existsSync(entry.backup)) {
75
+ copyFileSync(entry.backup, entry.file);
76
+ restored.push(entry.file);
77
+ }
78
+ }
79
+ writeManifest(root, { ...manifest, entries: keep });
80
+ return { removed, restored };
81
+ }
82
+ function sha256(content) {
83
+ return createHash("sha256").update(content).digest("hex");
84
+ }
@@ -0,0 +1,45 @@
1
+ import type { FinishStatus, JournalEntry, JournalKind, StateFile, TaskRecord, TasksFile } from "./types.js";
2
+ export type StateOptions = {
3
+ cwd?: string;
4
+ stateDir?: string;
5
+ env?: NodeJS.ProcessEnv | Record<string, string | undefined>;
6
+ };
7
+ export declare function resolveStateRoot(opts?: StateOptions): string;
8
+ export declare function teamDir(team: string, opts?: StateOptions): string;
9
+ export declare function initState(team: string, goal: string, opts?: StateOptions & {
10
+ noGitignore?: boolean;
11
+ }): StateFile;
12
+ export declare function showState(team: string, opts?: StateOptions): StateFile;
13
+ export declare function finishState(team: string, status: FinishStatus, opts?: StateOptions): StateFile;
14
+ export declare function bindMember(team: string, member: string, agentId: string, opts?: StateOptions & {
15
+ nickname?: string;
16
+ }): StateFile;
17
+ export declare function addTask(team: string, input: {
18
+ title: string;
19
+ detail?: string;
20
+ dependsOn?: string[];
21
+ }, opts?: StateOptions): TaskRecord;
22
+ export declare function claimTask(team: string, taskId: string, input: {
23
+ actor: string;
24
+ leaseSec?: number;
25
+ reclaim?: boolean;
26
+ }, opts?: StateOptions): TaskRecord;
27
+ export declare function completeTask(team: string, taskId: string, input: {
28
+ actor: string;
29
+ result?: string;
30
+ failed?: boolean;
31
+ }, opts?: StateOptions): TaskRecord;
32
+ export declare function listTasks(team: string, opts?: StateOptions & {
33
+ reclaim?: boolean;
34
+ }): TasksFile;
35
+ export declare function addNote(team: string, input: {
36
+ actor: string;
37
+ text: string;
38
+ kind?: JournalKind;
39
+ }, opts?: StateOptions): JournalEntry;
40
+ export declare function listNotes(team: string, opts?: StateOptions): JournalEntry[];
41
+ export declare function loadTasks(team: string, opts?: StateOptions): TasksFile;
42
+ export declare function writeJsonAtomic(path: string, value: unknown): void;
43
+ export declare function readJson<T>(path: string): T;
44
+ export declare function writeFileAtomic(path: string, content: string | Buffer): void;
45
+ export declare function nowIso(env?: NodeJS.ProcessEnv | Record<string, string | undefined>): string;
package/dist/state.js ADDED
@@ -0,0 +1,261 @@
1
+ import { appendFileSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, writeFileSync, } from "node:fs";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { TEAM_NAME_RE, MEMBER_NAME_RE } from "./team.js";
4
+ import { withLock } from "./lock.js";
5
+ import { assertConfinedRoot } from "./paths.js";
6
+ export function resolveStateRoot(opts = {}) {
7
+ const envRoot = opts.env?.CODEX_TEAMS_STATE_DIR;
8
+ if (opts.stateDir)
9
+ return resolve(opts.stateDir);
10
+ if (envRoot)
11
+ return resolve(envRoot);
12
+ const projectRoot = resolve(opts.cwd ?? process.cwd());
13
+ return assertConfinedRoot(join(projectRoot, ".codex-teams"), projectRoot);
14
+ }
15
+ export function teamDir(team, opts = {}) {
16
+ assertTeamName(team);
17
+ return join(resolveStateRoot(opts), team);
18
+ }
19
+ export function initState(team, goal, opts = {}) {
20
+ assertTeamName(team);
21
+ const dir = teamDir(team, opts);
22
+ mkdirSync(join(dir, "locks"), { recursive: true });
23
+ mkdirSync(join(dir, "artifacts"), { recursive: true });
24
+ mkdirSync(join(dir, "runs"), { recursive: true });
25
+ if (!opts.noGitignore && isInsideGitWorktree(opts.cwd ?? process.cwd())) {
26
+ mkdirSync(resolveStateRoot(opts), { recursive: true });
27
+ const gitignore = join(resolveStateRoot(opts), ".gitignore");
28
+ if (!existsSync(gitignore))
29
+ writeFileAtomic(gitignore, "*\n");
30
+ }
31
+ return withTeamLock(team, opts, () => {
32
+ const now = nowIso(opts.env);
33
+ const state = {
34
+ version: 1,
35
+ team,
36
+ goal,
37
+ status: "active",
38
+ members: {},
39
+ created_at: now,
40
+ };
41
+ writeJsonAtomic(join(dir, "state.json"), state);
42
+ if (!existsSync(join(dir, "tasks.json")))
43
+ writeJsonAtomic(join(dir, "tasks.json"), { version: 1, tasks: [] });
44
+ if (!existsSync(join(dir, "journal.jsonl")))
45
+ writeFileAtomic(join(dir, "journal.jsonl"), "");
46
+ return state;
47
+ });
48
+ }
49
+ export function showState(team, opts = {}) {
50
+ return readJson(join(teamDir(team, opts), "state.json"));
51
+ }
52
+ export function finishState(team, status, opts = {}) {
53
+ if (status !== "ok" && status !== "partial")
54
+ throw new Error("status must be ok or partial");
55
+ return withTeamLock(team, opts, () => {
56
+ const path = join(teamDir(team, opts), "state.json");
57
+ const state = readJson(path);
58
+ state.status = "finished";
59
+ state.finish_status = status;
60
+ state.finished_at = nowIso(opts.env);
61
+ writeJsonAtomic(path, state);
62
+ return state;
63
+ });
64
+ }
65
+ export function bindMember(team, member, agentId, opts = {}) {
66
+ assertTeamName(team);
67
+ assertMemberName(member);
68
+ if (!agentId.trim())
69
+ throw new Error("agent-id is required");
70
+ return withTeamLock(team, opts, () => {
71
+ const path = join(teamDir(team, opts), "state.json");
72
+ const state = readJson(path);
73
+ state.members[member] = {
74
+ agent_id: agentId,
75
+ nickname: opts.nickname,
76
+ bound_at: nowIso(opts.env),
77
+ };
78
+ writeJsonAtomic(path, state);
79
+ return state;
80
+ });
81
+ }
82
+ export function addTask(team, input, opts = {}) {
83
+ if (!input.title.trim())
84
+ throw new Error("task title is required");
85
+ return withTaskMutation(team, opts, tasks => {
86
+ const now = nowIso(opts.env);
87
+ const task = {
88
+ id: nextTaskId(tasks),
89
+ title: input.title,
90
+ detail: input.detail,
91
+ depends_on: input.dependsOn ?? [],
92
+ status: "open",
93
+ created_at: now,
94
+ updated_at: now,
95
+ };
96
+ for (const dep of task.depends_on) {
97
+ if (!tasks.tasks.some(existing => existing.id === dep))
98
+ throw new Error(`unknown dependency: ${dep}`);
99
+ }
100
+ tasks.tasks.push(task);
101
+ return task;
102
+ });
103
+ }
104
+ export function claimTask(team, taskId, input, opts = {}) {
105
+ assertMemberName(input.actor);
106
+ const leaseSec = input.leaseSec ?? 900;
107
+ if (!Number.isFinite(leaseSec) || leaseSec <= 0)
108
+ throw new Error("lease-sec must be > 0");
109
+ return withTaskMutation(team, opts, tasks => {
110
+ reclaimExpired(tasks, nowIso(opts.env));
111
+ const task = mustFindTask(tasks, taskId);
112
+ if (task.status !== "open")
113
+ throw new Error(`task ${taskId} is not open`);
114
+ const incomplete = task.depends_on.filter(dep => mustFindTask(tasks, dep).status !== "done");
115
+ if (incomplete.length > 0)
116
+ throw new Error(`task ${taskId} has incomplete dependencies: ${incomplete.join(",")}`);
117
+ const nowMs = Date.parse(nowIso(opts.env));
118
+ task.status = "claimed";
119
+ task.claimed_by = input.actor;
120
+ task.lease_expires_at = new Date(nowMs + leaseSec * 1000).toISOString();
121
+ task.updated_at = new Date(nowMs).toISOString();
122
+ return task;
123
+ });
124
+ }
125
+ export function completeTask(team, taskId, input, opts = {}) {
126
+ assertMemberName(input.actor);
127
+ return withTaskMutation(team, opts, tasks => {
128
+ const task = mustFindTask(tasks, taskId);
129
+ if (task.status !== "claimed" || task.claimed_by !== input.actor) {
130
+ throw new Error(`task ${taskId} is not claimed by ${input.actor}`);
131
+ }
132
+ task.status = input.failed ? "failed" : "done";
133
+ task.result = input.result;
134
+ task.lease_expires_at = undefined;
135
+ task.updated_at = nowIso(opts.env);
136
+ return task;
137
+ });
138
+ }
139
+ export function listTasks(team, opts = {}) {
140
+ if (!opts.reclaim)
141
+ return loadTasks(team, opts);
142
+ return withTaskMutation(team, opts, tasks => {
143
+ reclaimExpired(tasks, nowIso(opts.env));
144
+ return tasks;
145
+ });
146
+ }
147
+ export function addNote(team, input, opts = {}) {
148
+ assertMemberName(input.actor);
149
+ if (!input.text.trim())
150
+ throw new Error("note text is required");
151
+ const kind = input.kind ?? "note";
152
+ if (kind !== "note" && kind !== "handoff" && kind !== "decision")
153
+ throw new Error("kind must be note, handoff, or decision");
154
+ return withTeamLock(team, opts, () => {
155
+ const entry = { ts: nowIso(opts.env), actor: input.actor, kind, text: input.text };
156
+ appendFileSync(join(teamDir(team, opts), "journal.jsonl"), `${JSON.stringify(entry)}\n`, { mode: 0o600 });
157
+ return entry;
158
+ });
159
+ }
160
+ export function listNotes(team, opts = {}) {
161
+ const path = join(teamDir(team, opts), "journal.jsonl");
162
+ if (!existsSync(path))
163
+ return [];
164
+ return readFileSync(path, "utf8")
165
+ .split(/\r?\n/)
166
+ .filter(Boolean)
167
+ .map(line => JSON.parse(line));
168
+ }
169
+ export function loadTasks(team, opts = {}) {
170
+ const path = join(teamDir(team, opts), "tasks.json");
171
+ if (!existsSync(path))
172
+ return { version: 1, tasks: [] };
173
+ return readJson(path);
174
+ }
175
+ export function writeJsonAtomic(path, value) {
176
+ writeFileAtomic(path, `${JSON.stringify(value, null, 2)}\n`);
177
+ }
178
+ export function readJson(path) {
179
+ return JSON.parse(readFileSync(path, "utf8"));
180
+ }
181
+ export function writeFileAtomic(path, content) {
182
+ mkdirSync(dirname(path), { recursive: true });
183
+ const tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
184
+ writeFileSync(tmp, content, { mode: 0o600 });
185
+ const fd = openSync(tmp, "r");
186
+ try {
187
+ fsyncSync(fd);
188
+ }
189
+ finally {
190
+ closeSync(fd);
191
+ }
192
+ renameSync(tmp, path);
193
+ }
194
+ export function nowIso(env = process.env) {
195
+ const override = env.CODEX_TEAMS_NOW;
196
+ if (override) {
197
+ const ms = Date.parse(override);
198
+ if (Number.isNaN(ms))
199
+ throw new Error(`CODEX_TEAMS_NOW is not a valid ISO timestamp: ${override}`);
200
+ return new Date(ms).toISOString();
201
+ }
202
+ return new Date().toISOString();
203
+ }
204
+ function withTaskMutation(team, opts, fn) {
205
+ return withTeamLock(team, opts, () => {
206
+ const tasks = loadTasks(team, opts);
207
+ const result = fn(tasks);
208
+ writeJsonAtomic(join(teamDir(team, opts), "tasks.json"), tasks);
209
+ return result;
210
+ });
211
+ }
212
+ function withTeamLock(team, opts, fn) {
213
+ const dir = teamDir(team, opts);
214
+ mkdirSync(join(dir, "locks"), { recursive: true });
215
+ return withLock(join(dir, "locks", "state.lock"), { ttlMs: 30_000, waitMs: 10_000, purpose: `teams:${team}` }, fn);
216
+ }
217
+ function reclaimExpired(tasks, now) {
218
+ const nowMs = Date.parse(now);
219
+ for (const task of tasks.tasks) {
220
+ if (task.status === "claimed" && task.lease_expires_at && Date.parse(task.lease_expires_at) <= nowMs) {
221
+ task.status = "open";
222
+ task.claimed_by = undefined;
223
+ task.lease_expires_at = undefined;
224
+ task.updated_at = now;
225
+ }
226
+ }
227
+ }
228
+ function mustFindTask(tasks, taskId) {
229
+ const task = tasks.tasks.find(item => item.id === taskId);
230
+ if (!task)
231
+ throw new Error(`unknown task: ${taskId}`);
232
+ return task;
233
+ }
234
+ function nextTaskId(tasks) {
235
+ let n = tasks.tasks.length + 1;
236
+ for (;;) {
237
+ const id = `task-${String(n).padStart(3, "0")}`;
238
+ if (!tasks.tasks.some(task => task.id === id))
239
+ return id;
240
+ n++;
241
+ }
242
+ }
243
+ function assertTeamName(team) {
244
+ if (!TEAM_NAME_RE.test(team))
245
+ throw new Error(`invalid team name: ${team}`);
246
+ }
247
+ function assertMemberName(member) {
248
+ if (!MEMBER_NAME_RE.test(member))
249
+ throw new Error(`invalid member name: ${member}`);
250
+ }
251
+ function isInsideGitWorktree(cwd) {
252
+ let current = resolve(cwd);
253
+ for (;;) {
254
+ if (existsSync(join(current, ".git")))
255
+ return true;
256
+ const next = dirname(current);
257
+ if (next === current)
258
+ return false;
259
+ current = next;
260
+ }
261
+ }
package/dist/team.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ import type { MemberDef, SandboxMode, TeamDef } from "./types.js";
2
+ export declare const TEAM_NAME_RE: RegExp;
3
+ export declare const MEMBER_NAME_RE: RegExp;
4
+ export declare function parseTeamJson(path: string): TeamDef;
5
+ export declare function validateTeamDef(value: unknown, label?: string): TeamDef;
6
+ export declare function collectTeamErrors(value: unknown): string[];
7
+ export declare function scaffoldTeam(preset?: "review-panel" | "swarm" | "pipeline"): TeamDef;
8
+ export declare function writePreset(path: string, preset: "review-panel" | "swarm" | "pipeline"): void;
9
+ export declare function memberAgentName(team: TeamDef | string, member: MemberDef | string): string;
10
+ export declare function resolveMemberModel(team: TeamDef, member: MemberDef): string;
11
+ export declare function resolveMemberSandbox(team: TeamDef, member: MemberDef): SandboxMode;