@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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ArcSolver
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.ko.md ADDED
@@ -0,0 +1,158 @@
1
+ <p align="right"><a href="README.md">English</a> | 한국어</p>
2
+
3
+ # @codex-modules/teams
4
+
5
+ `codex-teams`는 `team.json`을 Codex agent TOML, 리더 지시문, 프로젝트 로컬 팀 상태로 바꾸는 CLI입니다.
6
+
7
+ 멀티 에이전트 실행 엔진은 Codex native stable `multi_agent_v1.spawn_agent`를 리더 세션 안에서 사용합니다. 이 패키지는 엔진을 다시 만들지 않고, 팀 정의, 안전한 install/uninstall, task lease, journal, dry-run 기본 headless runner를 관리합니다.
8
+
9
+ ## 설치
10
+
11
+ ```bash
12
+ npm install -g @codex-modules/teams
13
+ ```
14
+
15
+ 또는 source에서:
16
+
17
+ ```bash
18
+ npm install
19
+ npm run build
20
+ ```
21
+
22
+ 이 패키지의 CLI를 바로 실행하려면:
23
+
24
+ ```bash
25
+ node dist/cli.js doctor
26
+ ```
27
+
28
+ global 설치나 npm linking 후에는:
29
+
30
+ ```bash
31
+ codex-teams doctor
32
+ ```
33
+
34
+ ## 사용법
35
+
36
+ starter team을 만듭니다:
37
+
38
+ ```bash
39
+ codex-teams init --preset review-panel --out team.json
40
+ codex-teams validate team.json
41
+ ```
42
+
43
+ 팀을 Codex home에 설치합니다:
44
+
45
+ ```bash
46
+ codex-teams install team.json
47
+ ```
48
+
49
+ 기본값은 `$CODEX_HOME/agents/<team>-<member>.toml`을 쓰고 `$CODEX_HOME/agents/.codex-teams-manifest.json`에 소유권을 기록합니다. sandbox나 다른 Codex home을 쓰려면 `--codex-home <dir>`를 넘기세요.
50
+
51
+ Project scope 설치는 명시적으로 선택합니다:
52
+
53
+ ```bash
54
+ codex-teams install team.json --scope project
55
+ ```
56
+
57
+ Project scope는 `<cwd>/.codex/agents/`에 쓰고 trust 경고를 출력합니다. `config.toml`을 편집하거나 프로젝트를 trusted로 등록하지 않습니다.
58
+
59
+ 리더 프롬프트를 생성합니다:
60
+
61
+ ```bash
62
+ codex-teams leader-prompt team.json --goal "Review this change for security and correctness"
63
+ ```
64
+
65
+ 프로젝트 내구 상태를 시작합니다:
66
+
67
+ ```bash
68
+ codex-teams state init review-panel --goal "Review this change"
69
+ codex-teams task add review-panel --title "Security review"
70
+ codex-teams task claim review-panel task-001 --actor security
71
+ codex-teams note add review-panel --actor leader --text "Security and correctness can run in parallel"
72
+ ```
73
+
74
+ `run`은 기본적으로 dry-run입니다:
75
+
76
+ ```bash
77
+ codex-teams run team.json --goal "Review this change"
78
+ ```
79
+
80
+ 실제 `codex exec` 실행은 두 opt-in이 모두 필요합니다:
81
+
82
+ ```bash
83
+ codex-teams run team.json --goal "Review this change" --execute --allow-codex
84
+ ```
85
+
86
+ 실행 runner는 기본적으로 `codex exec -s workspace-write --skip-git-repo-check --json --ephemeral`을 사용하고 run artifact를 `.codex-teams/<team>/runs/` 아래에 남깁니다. runner는 `read-only` 또는 `workspace-write` sandbox만 허용하고, danger-access flag를 전달하지 않으며, 조립된 prompt를 `codex exec`의 단일 positional 인자로만 넘깁니다.
87
+
88
+ 선택적 Codex skill을 설치합니다:
89
+
90
+ ```bash
91
+ codex-teams skill install
92
+ ```
93
+
94
+ ## 동작 방식
95
+
96
+ `team.json`은 team name, defaults, 2명에서 8명 사이의 members를 정의합니다. Member name은 `<team>-<member>` 형식의 Codex agent type이 됩니다.
97
+
98
+ Install은 각 member를 다음 필드가 있는 TOML로 렌더합니다:
99
+
100
+ - `name`
101
+ - `description`
102
+ - `model`
103
+ - `sandbox_mode`
104
+ - `nickname_candidates`
105
+ - `developer_instructions`
106
+
107
+ 모든 TOML string은 basic string으로 렌더되어 따옴표, 백슬래시, 개행, TOML 제어문자가 escape됩니다. `nickname_candidates`는 string array로 렌더됩니다.
108
+
109
+ 기존 unmanaged file은 `--force` 없이는 절대 덮어쓰지 않습니다. `--force`를 사용한 overwrite는 먼저 backup을 만듭니다. Uninstall은 선택한 target root의 manifest에 기록된 파일만 건드립니다.
110
+
111
+ 프로젝트 상태는 다음 위치에 있습니다:
112
+
113
+ ```text
114
+ .codex-teams/<team>/
115
+ state.json
116
+ tasks.json
117
+ journal.jsonl
118
+ artifacts/<member>/
119
+ runs/
120
+ locks/
121
+ ```
122
+
123
+ `state.json`과 `tasks.json`은 mkdir lock 안에서 atomic write로 저장됩니다. `journal.jsonl`은 같은 lock 안에서 append-only로 추가됩니다. Task claim은 lease를 사용하며, 만료된 claim은 `task claim`과 `task list --reclaim`에서 회수됩니다. 결정론적 테스트에는 `CODEX_TEAMS_NOW`로 시간을 override할 수 있습니다.
124
+
125
+ 상태 CLI는 리더 또는 사람이 호출하는 표면입니다. 멤버는 마지막 `TEAM-RESULT: <one-line summary>` 줄로 보고합니다. Workspace-write 멤버는 선택적으로 artifact를 남길 수 있지만, canonical 결과 채널은 final message입니다.
126
+
127
+ `doctor`는 Codex binary, version, native feature state, model catalog availability, write access, installed teams를 user/project scope로 나눠 보고합니다. 건강한 native workflow에는 `multi_agent`가 stable and enabled여야 합니다. `enable_fanout`과 `multi_agent_v2`는 under-development surface로만 보고합니다.
128
+
129
+ 이 package는 runtime dependency가 없습니다. package root는 team parsing, install/uninstall, doctor, prompt/run planning, durable state/task/note operation을 위한 지원 대상 high-level helper만 export합니다.
130
+
131
+ ## Attribution
132
+
133
+ No third-party code is included; the state protocol is an original clean-room design.
134
+
135
+ ## 제거와 롤백
136
+
137
+ 설치된 team을 제거합니다:
138
+
139
+ ```bash
140
+ codex-teams uninstall review-panel
141
+ ```
142
+
143
+ sandbox 또는 project scope를 썼다면 install 때와 같은 target root를 지정합니다:
144
+
145
+ ```bash
146
+ codex-teams uninstall review-panel --codex-home /tmp/codex-home
147
+ codex-teams uninstall review-panel --scope project
148
+ ```
149
+
150
+ Uninstall은 manifest-owned file만 삭제합니다. `--force` install이 unmanaged file을 backup했다면 uninstall 때 그 backup을 복원합니다. 설치된 파일이 install 이후 바뀌었다면, 수동 확인을 위해 uninstall이 삭제를 거부합니다.
151
+
152
+ 선택적 skill 제거:
153
+
154
+ ```bash
155
+ codex-teams skill uninstall
156
+ ```
157
+
158
+ `.codex-teams/` 아래 상태는 project-local runtime data입니다. 프로젝트가 git worktree로 보이면 기본적으로 ignore됩니다. task board, journal, runs, artifacts가 더 필요 없으면 `.codex-teams/<team>/`을 삭제하세요.
package/README.md ADDED
@@ -0,0 +1,158 @@
1
+ <p align="right">English | <a href="README.ko.md">한국어</a></p>
2
+
3
+ # @codex-modules/teams
4
+
5
+ `codex-teams` turns a `team.json` file into Codex agent TOML files, leader instructions, and durable project-local team state.
6
+
7
+ It uses Codex native stable `multi_agent_v1.spawn_agent` through a leader session. This package does not reimplement the multi-agent engine. It manages team definitions, safe install/uninstall, task leases, a journal, and a dry-run-first headless runner.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install -g @codex-modules/teams
13
+ ```
14
+
15
+ Or from source:
16
+
17
+ ```bash
18
+ npm install
19
+ npm run build
20
+ ```
21
+
22
+ Use the CLI from this package:
23
+
24
+ ```bash
25
+ node dist/cli.js doctor
26
+ ```
27
+
28
+ When installed globally or through npm linking, the command is:
29
+
30
+ ```bash
31
+ codex-teams doctor
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ Create a starter team:
37
+
38
+ ```bash
39
+ codex-teams init --preset review-panel --out team.json
40
+ codex-teams validate team.json
41
+ ```
42
+
43
+ Install the team into a Codex home:
44
+
45
+ ```bash
46
+ codex-teams install team.json
47
+ ```
48
+
49
+ By default this writes `$CODEX_HOME/agents/<team>-<member>.toml` and records ownership in `$CODEX_HOME/agents/.codex-teams-manifest.json`. Use `--codex-home <dir>` for a sandbox or alternate Codex home.
50
+
51
+ Project-scope install is explicit:
52
+
53
+ ```bash
54
+ codex-teams install team.json --scope project
55
+ ```
56
+
57
+ Project scope writes `<cwd>/.codex/agents/` and prints a trust warning. It does not edit `config.toml` or mark the project trusted.
58
+
59
+ Generate the leader prompt:
60
+
61
+ ```bash
62
+ codex-teams leader-prompt team.json --goal "Review this change for security and correctness"
63
+ ```
64
+
65
+ Start durable state in the project:
66
+
67
+ ```bash
68
+ codex-teams state init review-panel --goal "Review this change"
69
+ codex-teams task add review-panel --title "Security review"
70
+ codex-teams task claim review-panel task-001 --actor security
71
+ codex-teams note add review-panel --actor leader --text "Security and correctness can run in parallel"
72
+ ```
73
+
74
+ Run is dry-run by default:
75
+
76
+ ```bash
77
+ codex-teams run team.json --goal "Review this change"
78
+ ```
79
+
80
+ Actual `codex exec` launch requires both opt-ins:
81
+
82
+ ```bash
83
+ codex-teams run team.json --goal "Review this change" --execute --allow-codex
84
+ ```
85
+
86
+ The executed runner uses `codex exec -s workspace-write --skip-git-repo-check --json --ephemeral` by default and writes run artifacts under `.codex-teams/<team>/runs/`. The runner only accepts `read-only` or `workspace-write` sandbox modes, never forwards danger-access flags, and passes the assembled prompt as one positional `codex exec` argument.
87
+
88
+ Install the optional Codex skill:
89
+
90
+ ```bash
91
+ codex-teams skill install
92
+ ```
93
+
94
+ ## How It Works
95
+
96
+ `team.json` defines a team name, defaults, and 2 to 8 members. Member names become Codex agent types named `<team>-<member>`.
97
+
98
+ Install renders each member to TOML with:
99
+
100
+ - `name`
101
+ - `description`
102
+ - `model`
103
+ - `sandbox_mode`
104
+ - `nickname_candidates`
105
+ - `developer_instructions`
106
+
107
+ All TOML strings are basic strings with quotes, backslashes, newlines, and TOML control characters escaped. `nickname_candidates` is rendered as a string array.
108
+
109
+ Existing unmanaged files are never overwritten unless `--force` is passed. Forced overwrites are backed up first. Uninstall only touches files recorded in the manifest for the selected target root.
110
+
111
+ Project state lives in:
112
+
113
+ ```text
114
+ .codex-teams/<team>/
115
+ state.json
116
+ tasks.json
117
+ journal.jsonl
118
+ artifacts/<member>/
119
+ runs/
120
+ locks/
121
+ ```
122
+
123
+ `state.json` and `tasks.json` are written atomically under a mkdir lock. `journal.jsonl` is append-only under the same lock. Task claims use leases; expired claims are reclaimed by `task claim` and `task list --reclaim`. `CODEX_TEAMS_NOW` can override time for deterministic tests.
124
+
125
+ Leader state commands are for the leader or a human operator. Members report through their final `TEAM-RESULT: <one-line summary>` line. Workspace-write members may also leave optional artifacts, but the final message is the canonical result channel.
126
+
127
+ `doctor` reports the Codex binary, version, native feature state, model catalog availability, write access, and installed teams split into user and project scopes. `multi_agent` must be stable and enabled for a healthy native workflow. `enable_fanout` and `multi_agent_v2` are reported as under-development surfaces only.
128
+
129
+ The package has zero runtime dependencies. Its package root exports only the supported high-level helpers for team parsing, install/uninstall, doctor, prompt/run planning, and durable state/task/note operations.
130
+
131
+ ## Attribution
132
+
133
+ No third-party code is included; the state protocol is an original clean-room design.
134
+
135
+ ## Uninstall Rollback
136
+
137
+ Remove an installed team:
138
+
139
+ ```bash
140
+ codex-teams uninstall review-panel
141
+ ```
142
+
143
+ For a sandbox or project scope, use the same target root used during install:
144
+
145
+ ```bash
146
+ codex-teams uninstall review-panel --codex-home /tmp/codex-home
147
+ codex-teams uninstall review-panel --scope project
148
+ ```
149
+
150
+ Uninstall deletes only manifest-owned files. If install backed up an unmanaged file with `--force`, uninstall restores that backup. If an installed file changed after install, uninstall refuses to remove it so you can inspect the file manually.
151
+
152
+ Remove the optional skill:
153
+
154
+ ```bash
155
+ codex-teams skill uninstall
156
+ ```
157
+
158
+ State under `.codex-teams/` is project-local runtime data. It is ignored by default when the project appears to be a git worktree. Delete `.codex-teams/<team>/` when you no longer need the task board, journal, runs, or artifacts.
@@ -0,0 +1,38 @@
1
+ import type { InstallScope, TeamDef, TeamsManifest } from "./types.js";
2
+ export type InstallOptions = {
3
+ codexHome?: string;
4
+ scope?: InstallScope;
5
+ cwd?: string;
6
+ force?: boolean;
7
+ skipModelCheck?: boolean;
8
+ env?: NodeJS.ProcessEnv | Record<string, string | undefined>;
9
+ };
10
+ export type InstallResult = {
11
+ team: string;
12
+ scope: InstallScope;
13
+ targetRoot: string;
14
+ files: string[];
15
+ removed: string[];
16
+ warnings: string[];
17
+ };
18
+ export type UninstallResult = {
19
+ team: string;
20
+ scope: InstallScope;
21
+ targetRoot: string;
22
+ removed: string[];
23
+ restored: string[];
24
+ warnings: string[];
25
+ };
26
+ export declare function installTeam(teamPath: string, opts?: InstallOptions): InstallResult;
27
+ export declare function installTeamDef(team: TeamDef, opts?: InstallOptions): InstallResult;
28
+ export declare function uninstallTeam(team: string, opts?: InstallOptions): UninstallResult;
29
+ export declare function renderAgentToml(team: TeamDef, memberName: string): string;
30
+ export declare function resolveCodexHome(env?: NodeJS.ProcessEnv | Record<string, string | undefined>, explicit?: string): string;
31
+ export declare function resolveAgentsRoot(scope: InstallScope, opts?: InstallOptions): string;
32
+ export declare function manifestPath(targetRoot: string): string;
33
+ export declare function readManifest(targetRoot: string, warnings?: string[]): TeamsManifest;
34
+ export declare function writeManifest(targetRoot: string, manifest: TeamsManifest): void;
35
+ export declare function findCodexBinary(env?: NodeJS.ProcessEnv | Record<string, string | undefined>): string | null;
36
+ export declare function listInstalledTeams(targetRoot: string): string[];
37
+ export declare function parseModelCatalog(stdout: string): string[];
38
+ export declare function backupSkillFile(file: string, backupDir: string): string;
package/dist/agents.js ADDED
@@ -0,0 +1,285 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { accessSync, chmodSync, constants as fsConstants, copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, } from "node:fs";
3
+ import { createHash } from "node:crypto";
4
+ import { homedir } from "node:os";
5
+ import { basename, delimiter, dirname, join, resolve } from "node:path";
6
+ import { assembleDeveloperInstructions } from "./prompt.js";
7
+ import { memberAgentName, parseTeamJson, resolveMemberModel, resolveMemberSandbox } from "./team.js";
8
+ import { renderToml } from "./toml.js";
9
+ import { nowIso, readJson, writeFileAtomic, writeJsonAtomic } from "./state.js";
10
+ import { assertConfinedRoot, isConfinedPath } from "./paths.js";
11
+ export function installTeam(teamPath, opts = {}) {
12
+ const team = parseTeamJson(teamPath);
13
+ return installTeamDef(team, opts);
14
+ }
15
+ export function installTeamDef(team, opts = {}) {
16
+ const scope = opts.scope ?? "user";
17
+ const targetRoot = resolveAgentsRoot(scope, opts);
18
+ mkdirSync(targetRoot, { recursive: true });
19
+ if (scope === "project")
20
+ assertProjectRoot(targetRoot, opts);
21
+ const warnings = [];
22
+ const removed = [];
23
+ if (scope === "project") {
24
+ warnings.push("project scope requires a trusted Codex project; config.toml is not modified automatically");
25
+ }
26
+ if (!opts.skipModelCheck)
27
+ warnings.push(...checkModels(team, opts));
28
+ const manifest = readManifest(targetRoot, warnings);
29
+ const installedAt = nowIso(opts.env);
30
+ const newMembers = new Set(team.members.map(member => member.name));
31
+ const oldTeamEntries = manifest.entries.filter(entry => entry.team === team.name && entry.kind === "agent");
32
+ const nextEntries = manifest.entries.filter(entry => !(entry.team === team.name && entry.kind === "agent"));
33
+ const files = [];
34
+ for (const entry of oldTeamEntries) {
35
+ if (entry.member && newMembers.has(entry.member))
36
+ continue;
37
+ const unsafeReason = validateManagedEntry(entry, targetRoot, scope);
38
+ if (unsafeReason) {
39
+ warnings.push(`skipping stale cleanup for ${entry.file}: ${unsafeReason}`);
40
+ nextEntries.push(entry);
41
+ continue;
42
+ }
43
+ if (!existsSync(entry.file))
44
+ continue;
45
+ const currentHash = sha256(readFileSync(entry.file, "utf8"));
46
+ if (currentHash !== entry.hash) {
47
+ warnings.push(`stale managed agent changed since install; leaving in place: ${entry.file}`);
48
+ nextEntries.push(entry);
49
+ continue;
50
+ }
51
+ rmSync(entry.file, { force: true });
52
+ removed.push(entry.file);
53
+ warnings.push(`removed stale managed agent file: ${entry.file}`);
54
+ }
55
+ for (const member of team.members) {
56
+ const file = join(targetRoot, `${memberAgentName(team, member)}.toml`);
57
+ const content = renderAgentToml(team, member.name);
58
+ const existing = manifest.entries.find(entry => entry.file === file && entry.kind === "agent" && entry.scope === scope);
59
+ let backup = existing?.backup ?? null;
60
+ if (existsSync(file) && !existing) {
61
+ if (!opts.force)
62
+ throw new Error(`refusing to overwrite unmanaged agent file: ${file} (use --force to back it up first)`);
63
+ backup = backupFile(file, join(targetRoot, ".codex-teams-backups"));
64
+ }
65
+ writeFileAtomic(file, content);
66
+ files.push(file);
67
+ nextEntries.push({
68
+ team: team.name,
69
+ scope,
70
+ file,
71
+ backup,
72
+ hash: sha256(content),
73
+ kind: "agent",
74
+ member: member.name,
75
+ installed_at: installedAt,
76
+ });
77
+ }
78
+ writeManifest(targetRoot, { ...manifest, entries: nextEntries });
79
+ return { team: team.name, scope, targetRoot, files, removed, warnings };
80
+ }
81
+ export function uninstallTeam(team, opts = {}) {
82
+ const scope = opts.scope ?? "user";
83
+ const targetRoot = resolveAgentsRoot(scope, opts);
84
+ const warnings = [];
85
+ const manifest = readManifest(targetRoot, warnings);
86
+ const removed = [];
87
+ const restored = [];
88
+ const keep = [];
89
+ for (const entry of manifest.entries) {
90
+ if (entry.team !== team || entry.kind !== "agent") {
91
+ keep.push(entry);
92
+ continue;
93
+ }
94
+ const unsafeReason = validateManagedEntry(entry, targetRoot, scope);
95
+ if (unsafeReason) {
96
+ warnings.push(`skipping manifest entry for ${entry.file}: ${unsafeReason}`);
97
+ keep.push(entry);
98
+ continue;
99
+ }
100
+ if (existsSync(entry.file)) {
101
+ const currentHash = sha256(readFileSync(entry.file, "utf8"));
102
+ if (currentHash !== entry.hash)
103
+ throw new Error(`installed file changed since install; refusing uninstall without manual review: ${entry.file}`);
104
+ rmSync(entry.file, { force: true });
105
+ removed.push(entry.file);
106
+ }
107
+ if (entry.backup && existsSync(entry.backup)) {
108
+ copyFileSync(entry.backup, entry.file);
109
+ restored.push(entry.file);
110
+ }
111
+ }
112
+ writeManifest(targetRoot, { ...manifest, entries: keep });
113
+ return { team, scope, targetRoot, removed, restored, warnings };
114
+ }
115
+ export function renderAgentToml(team, memberName) {
116
+ const member = team.members.find(item => item.name === memberName);
117
+ if (!member)
118
+ throw new Error(`unknown member: ${memberName}`);
119
+ const agentName = memberAgentName(team, member);
120
+ return renderToml({
121
+ name: agentName,
122
+ description: member.focus,
123
+ model: resolveMemberModel(team, member),
124
+ sandbox_mode: resolveMemberSandbox(team, member),
125
+ nickname_candidates: [member.name],
126
+ developer_instructions: assembleDeveloperInstructions(team, member),
127
+ });
128
+ }
129
+ export function resolveCodexHome(env = process.env, explicit) {
130
+ return resolve(explicit ?? env.CODEX_HOME ?? join(homedir(), ".codex"));
131
+ }
132
+ export function resolveAgentsRoot(scope, opts = {}) {
133
+ if (scope === "project") {
134
+ const projectRoot = realProjectRoot(opts.cwd ?? process.cwd());
135
+ return assertConfinedRoot(join(projectRoot, ".codex", "agents"), projectRoot);
136
+ }
137
+ return join(resolveCodexHome(opts.env, opts.codexHome), "agents");
138
+ }
139
+ export function manifestPath(targetRoot) {
140
+ return join(targetRoot, ".codex-teams-manifest.json");
141
+ }
142
+ export function readManifest(targetRoot, warnings = []) {
143
+ const path = manifestPath(targetRoot);
144
+ if (!existsSync(path))
145
+ return { version: 1, owner: "@codex-modules/teams", entries: [] };
146
+ const manifest = readJson(path);
147
+ if (manifest.version !== 1 || manifest.owner !== "@codex-modules/teams" || !Array.isArray(manifest.entries)) {
148
+ throw new Error(`unsupported teams manifest format: ${path}`);
149
+ }
150
+ return { ...manifest, entries: manifest.entries.flatMap((entry, index) => validateManifestEntry(entry, `${path} entries[${index}]`, warnings)) };
151
+ }
152
+ export function writeManifest(targetRoot, manifest) {
153
+ mkdirSync(targetRoot, { recursive: true });
154
+ writeJsonAtomic(manifestPath(targetRoot), manifest);
155
+ }
156
+ export function findCodexBinary(env = process.env) {
157
+ const pathValue = env.PATH ?? env.Path ?? "";
158
+ if (!pathValue)
159
+ return null;
160
+ const names = process.platform === "win32" ? ["codex.exe", "codex.cmd", "codex.bat", "codex"] : ["codex"];
161
+ for (const dir of pathValue.split(delimiter)) {
162
+ if (!dir)
163
+ continue;
164
+ for (const name of names) {
165
+ const candidate = join(dir, name);
166
+ try {
167
+ accessSync(candidate, fsConstants.X_OK);
168
+ return candidate;
169
+ }
170
+ catch {
171
+ // keep searching
172
+ }
173
+ }
174
+ }
175
+ return null;
176
+ }
177
+ export function listInstalledTeams(targetRoot) {
178
+ return [...new Set(readManifest(targetRoot).entries.filter(entry => entry.kind === "agent").map(entry => entry.team))].sort();
179
+ }
180
+ function checkModels(team, opts) {
181
+ const warnings = [];
182
+ const bin = findCodexBinary(opts.env);
183
+ if (!bin)
184
+ return ["codex binary not found; skipping model catalog preflight"];
185
+ const result = spawnSync(bin, ["debug", "models"], {
186
+ env: { ...process.env, ...(opts.env ?? {}), CODEX_HOME: resolveCodexHome(opts.env, opts.codexHome) },
187
+ encoding: "utf8",
188
+ stdio: ["ignore", "pipe", "pipe"],
189
+ timeout: 10_000,
190
+ });
191
+ if (result.status !== 0 || result.error) {
192
+ const reason = result.error ? result.error.message : result.stderr.trim();
193
+ return [`codex debug models unavailable; continuing without strict model preflight${reason ? ` (${reason})` : ""}`];
194
+ }
195
+ let catalog;
196
+ try {
197
+ catalog = parseModelCatalog(result.stdout);
198
+ }
199
+ catch (error) {
200
+ const reason = error instanceof Error ? error.message : String(error);
201
+ return [`codex debug models returned non-JSON catalog; continuing without strict model preflight (${reason})`];
202
+ }
203
+ if (catalog.length === 0)
204
+ return ["codex debug models returned no parseable catalog entries; continuing"];
205
+ const available = new Set(catalog);
206
+ for (const member of team.members) {
207
+ const model = resolveMemberModel(team, member);
208
+ if (!available.has(model))
209
+ throw new Error(`model ${model} for member ${member.name} is not in codex debug models catalog`);
210
+ }
211
+ return warnings;
212
+ }
213
+ export function parseModelCatalog(stdout) {
214
+ const parsed = JSON.parse(stdout);
215
+ if (!isRecord(parsed) || !Array.isArray(parsed.models))
216
+ return [];
217
+ const values = new Set();
218
+ for (const item of parsed.models) {
219
+ if (!isRecord(item))
220
+ continue;
221
+ const value = typeof item.slug === "string" ? item.slug : typeof item.id === "string" ? item.id : null;
222
+ if (value)
223
+ values.add(value);
224
+ }
225
+ return [...values].sort();
226
+ }
227
+ function backupFile(file, backupDir) {
228
+ mkdirSync(backupDir, { recursive: true });
229
+ const backup = join(backupDir, `${basename(file)}.${new Date().toISOString().replace(/[:.]/g, "-")}.${process.pid}.bak`);
230
+ copyFileSync(file, backup);
231
+ try {
232
+ chmodSync(backup, statSync(file).mode & 0o777);
233
+ }
234
+ catch {
235
+ // best effort
236
+ }
237
+ return backup;
238
+ }
239
+ export function backupSkillFile(file, backupDir) {
240
+ return backupFile(file, backupDir);
241
+ }
242
+ function sha256(content) {
243
+ return createHash("sha256").update(content).digest("hex");
244
+ }
245
+ function assertProjectRoot(targetRoot, opts) {
246
+ assertConfinedRoot(targetRoot, realProjectRoot(opts.cwd ?? process.cwd()));
247
+ }
248
+ function realProjectRoot(cwd) {
249
+ return statSync(resolve(cwd)).isDirectory() ? resolve(cwd) : dirname(resolve(cwd));
250
+ }
251
+ function validateManagedEntry(entry, targetRoot, scope) {
252
+ if (entry.scope !== scope)
253
+ return `scope ${entry.scope} does not match ${scope}`;
254
+ if (!entry.member)
255
+ return "agent entry is missing member";
256
+ if (!isConfinedPath(entry.file, targetRoot))
257
+ return "file path is outside target root";
258
+ if (entry.backup && !isConfinedPath(entry.backup, targetRoot))
259
+ return "backup path is outside target root";
260
+ return null;
261
+ }
262
+ function validateManifestEntry(value, label, warnings) {
263
+ if (!isRecord(value)) {
264
+ warnings.push(`skipping invalid manifest entry at ${label}: expected object`);
265
+ return [];
266
+ }
267
+ const backup = value.backup;
268
+ const kind = value.kind;
269
+ const scope = value.scope;
270
+ if (typeof value.team !== "string" ||
271
+ (scope !== "user" && scope !== "project" && scope !== "skill") ||
272
+ typeof value.file !== "string" ||
273
+ !(backup === undefined || backup === null || typeof backup === "string") ||
274
+ typeof value.hash !== "string" ||
275
+ (kind !== "agent" && kind !== "skill") ||
276
+ typeof value.installed_at !== "string" ||
277
+ (kind === "agent" && typeof value.member !== "string")) {
278
+ warnings.push(`skipping invalid manifest entry at ${label}: unsupported entry schema`);
279
+ return [];
280
+ }
281
+ return [value];
282
+ }
283
+ function isRecord(value) {
284
+ return typeof value === "object" && value !== null;
285
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { main } from "./main.js";
3
+ void main(process.argv.slice(2));
@@ -0,0 +1,10 @@
1
+ import type { DoctorReport } from "./types.js";
2
+ export type DoctorOptions = {
3
+ codexHome?: string;
4
+ stateDir?: string;
5
+ cwd?: string;
6
+ env?: NodeJS.ProcessEnv | Record<string, string | undefined>;
7
+ };
8
+ export declare function doctor(opts?: DoctorOptions): DoctorReport;
9
+ export declare function doctorIsHealthy(report: DoctorReport): boolean;
10
+ export declare function formatDoctor(report: DoctorReport): string;