@firatcand/roster 0.1.0

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.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +272 -0
  3. package/agents/critic.md +74 -0
  4. package/agents/enricher.md +56 -0
  5. package/agents/lesson-drafter.md +64 -0
  6. package/agents/pattern-detector.md +62 -0
  7. package/agents/promotion-arbiter.md +71 -0
  8. package/agents/prospector.md +51 -0
  9. package/agents/writer.md +58 -0
  10. package/bin/roster.js +2093 -0
  11. package/lib/.gitkeep +0 -0
  12. package/package.json +68 -0
  13. package/skills/chief-of-staff/SKILL.md +218 -0
  14. package/skills/dreamer/SKILL.md +112 -0
  15. package/skills/roster-orchestrator/SKILL.md +122 -0
  16. package/skills/sdr/SKILL.md +147 -0
  17. package/templates/CLAUDE.project.template.md +45 -0
  18. package/templates/CONTEXT.template.md +51 -0
  19. package/templates/env.example +25 -0
  20. package/templates/gitignore-defaults.txt +28 -0
  21. package/templates/scaffold/.config/functions.yaml +22 -0
  22. package/templates/scaffold/chief-of-staff/README.md +86 -0
  23. package/templates/scaffold/chief-of-staff/agent.md +122 -0
  24. package/templates/scaffold/chief-of-staff/logs/.gitkeep +0 -0
  25. package/templates/scaffold/chief-of-staff/plans/add-agent-to-project.yaml +45 -0
  26. package/templates/scaffold/chief-of-staff/plans/archive-project.yaml +51 -0
  27. package/templates/scaffold/chief-of-staff/plans/audit-agent.yaml +32 -0
  28. package/templates/scaffold/chief-of-staff/plans/audit-project.yaml +34 -0
  29. package/templates/scaffold/chief-of-staff/plans/audit-repo.yaml +26 -0
  30. package/templates/scaffold/chief-of-staff/plans/create-agent.yaml +123 -0
  31. package/templates/scaffold/chief-of-staff/plans/create-function.yaml +48 -0
  32. package/templates/scaffold/chief-of-staff/plans/create-project.yaml +65 -0
  33. package/templates/scaffold/chief-of-staff/plans/remove-agent-from-project.yaml +50 -0
  34. package/templates/scaffold/chief-of-staff/plans/rename-project.yaml +62 -0
  35. package/templates/scaffold/chief-of-staff/plans/unarchive-project.yaml +41 -0
  36. package/templates/scaffold/chief-of-staff/playbook/.gitkeep +0 -0
  37. package/templates/scaffold/conventions.md +608 -0
  38. package/templates/scaffold/design/.gitkeep +0 -0
  39. package/templates/scaffold/design/EXPERT.md +68 -0
  40. package/templates/scaffold/dreamer/README.md +32 -0
  41. package/templates/scaffold/dreamer/agent.md +101 -0
  42. package/templates/scaffold/dreamer/logs/.gitkeep +0 -0
  43. package/templates/scaffold/dreamer/pending/.gitkeep +0 -0
  44. package/templates/scaffold/dreamer/plans/nightly-reflection.yaml +113 -0
  45. package/templates/scaffold/dreamer/playbook/.gitkeep +0 -0
  46. package/templates/scaffold/dreamer/state.md +13 -0
  47. package/templates/scaffold/dreamer/subagents/lesson-drafter.md +56 -0
  48. package/templates/scaffold/dreamer/subagents/pattern-detector.md +55 -0
  49. package/templates/scaffold/dreamer/subagents/promotion-arbiter.md +64 -0
  50. package/templates/scaffold/gtm/EXPERT.md +83 -0
  51. package/templates/scaffold/gtm/sdr/.claude/settings.json +3 -0
  52. package/templates/scaffold/gtm/sdr/.mcp.json +21 -0
  53. package/templates/scaffold/gtm/sdr/README.md +46 -0
  54. package/templates/scaffold/gtm/sdr/agent.md +136 -0
  55. package/templates/scaffold/gtm/sdr/plans/cold-outreach.yaml +92 -0
  56. package/templates/scaffold/gtm/sdr/playbook/.gitkeep +0 -0
  57. package/templates/scaffold/gtm/sdr/projects/_demo/asset-references.md +7 -0
  58. package/templates/scaffold/gtm/sdr/projects/_demo/config/default.yaml +69 -0
  59. package/templates/scaffold/gtm/sdr/projects/_demo/log/feedback/.gitkeep +0 -0
  60. package/templates/scaffold/gtm/sdr/projects/_demo/log/runs/.gitkeep +0 -0
  61. package/templates/scaffold/gtm/sdr/projects/_demo/playbook/.gitkeep +0 -0
  62. package/templates/scaffold/gtm/sdr/subagents/critic.md +67 -0
  63. package/templates/scaffold/gtm/sdr/subagents/enricher.md +49 -0
  64. package/templates/scaffold/gtm/sdr/subagents/prospector.md +44 -0
  65. package/templates/scaffold/gtm/sdr/subagents/writer.md +51 -0
  66. package/templates/scaffold/logs/cron/.gitkeep +0 -0
  67. package/templates/scaffold/ops/.gitkeep +0 -0
  68. package/templates/scaffold/ops/EXPERT.md +84 -0
  69. package/templates/scaffold/product/.gitkeep +0 -0
  70. package/templates/scaffold/product/EXPERT.md +87 -0
  71. package/templates/scaffold/projects/_demo/CLAUDE.md +35 -0
  72. package/templates/scaffold/projects/_demo/README.md +16 -0
  73. package/templates/scaffold/projects/_demo/assets/.gitkeep +0 -0
  74. package/templates/scaffold/projects/_demo/config/default.yaml +28 -0
  75. package/templates/scaffold/projects/_demo/guidelines/asset-links.md +15 -0
  76. package/templates/scaffold/projects/_demo/guidelines/brand-book.md +25 -0
  77. package/templates/scaffold/projects/_demo/guidelines/icps/_persona-template.md +44 -0
  78. package/templates/scaffold/projects/_demo/guidelines/messaging.md +20 -0
  79. package/templates/scaffold/projects/_demo/guidelines/voice.md +29 -0
  80. package/templates/scaffold/projects/_demo/state.md +11 -0
  81. package/templates/scaffold/scripts/lib/README.md +13 -0
  82. package/templates/scaffold/scripts/lib/functions.sh +89 -0
  83. package/templates/scaffold/scripts/new-project.sh +125 -0
package/bin/roster.js ADDED
@@ -0,0 +1,2093 @@
1
+ #!/usr/bin/env node
2
+ import chalk from "chalk";
3
+ import { homedir } from "node:os";
4
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
+ import { copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
6
+ import { fileURLToPath } from "node:url";
7
+ import { rm } from "node:fs/promises";
8
+ import fsExtra from "fs-extra";
9
+ import { execFileSync } from "node:child_process";
10
+ import YAML from "yaml";
11
+ import { z } from "zod";
12
+ //#region src/lib/paths.ts
13
+ function findRosterRoot() {
14
+ const here = dirname(fileURLToPath(import.meta.url));
15
+ const candidates = [
16
+ resolve(here, "..", ".."),
17
+ resolve(here, ".."),
18
+ here
19
+ ];
20
+ for (const dir of candidates) try {
21
+ if (JSON.parse(readFileSync(resolve(dir, "package.json"), "utf8")).name === "@firatcand/roster") return dir;
22
+ } catch {
23
+ continue;
24
+ }
25
+ throw new Error("roster: could not locate package.json");
26
+ }
27
+ const ROSTER_ROOT = findRosterRoot();
28
+ function getPackageVersion() {
29
+ const pkg = JSON.parse(readFileSync(resolve(ROSTER_ROOT, "package.json"), "utf8"));
30
+ if (typeof pkg.version !== "string" || pkg.version.length === 0) throw new Error("roster: package.json has no version field");
31
+ return pkg.version;
32
+ }
33
+ //#endregion
34
+ //#region src/lib/tools.ts
35
+ function claudeHome() {
36
+ return process.env["ROSTER_CLAUDE_HOME"] ?? join(homedir(), ".claude");
37
+ }
38
+ function codexHome() {
39
+ return process.env["ROSTER_CODEX_HOME"] ?? join(homedir(), ".codex");
40
+ }
41
+ function geminiHome() {
42
+ return process.env["ROSTER_GEMINI_HOME"] ?? join(homedir(), ".gemini");
43
+ }
44
+ function defaultDefinitions() {
45
+ const claude = claudeHome();
46
+ const codex = codexHome();
47
+ const gemini = geminiHome();
48
+ return [
49
+ {
50
+ key: "claude",
51
+ name: "Claude Code",
52
+ configRoot: claude,
53
+ skillsTarget: join(claude, "skills"),
54
+ agentsTarget: join(claude, "agents"),
55
+ skillsLayout: "dir",
56
+ skillsFileExt: null,
57
+ agentsLayout: "md-copy",
58
+ installLink: "https://claude.ai/code"
59
+ },
60
+ {
61
+ key: "codex",
62
+ name: "Codex CLI",
63
+ configRoot: codex,
64
+ skillsTarget: join(codex, "skills"),
65
+ agentsTarget: join(codex, "agents"),
66
+ skillsLayout: "dir",
67
+ skillsFileExt: null,
68
+ agentsLayout: "codex-toml",
69
+ installLink: "https://github.com/openai/codex"
70
+ },
71
+ {
72
+ key: "gemini",
73
+ name: "Gemini CLI",
74
+ configRoot: gemini,
75
+ skillsTarget: join(gemini, "extensions"),
76
+ agentsTarget: join(gemini, "agents"),
77
+ skillsLayout: "dir",
78
+ skillsFileExt: null,
79
+ agentsLayout: "md-copy",
80
+ installLink: "https://github.com/google-gemini/gemini-cli"
81
+ }
82
+ ];
83
+ }
84
+ function detectTools() {
85
+ return defaultDefinitions().filter((def) => existsSync(def.configRoot));
86
+ }
87
+ function allTools() {
88
+ return defaultDefinitions();
89
+ }
90
+ var RosterError = class extends Error {
91
+ header;
92
+ body;
93
+ remedy;
94
+ exitCode;
95
+ constructor(opts) {
96
+ super(`${opts.header}\n${opts.body}\n${opts.remedy}`);
97
+ this.name = "RosterError";
98
+ this.header = opts.header;
99
+ this.body = opts.body;
100
+ this.remedy = opts.remedy;
101
+ this.exitCode = opts.exitCode;
102
+ }
103
+ };
104
+ function isRosterError(err) {
105
+ return err instanceof RosterError;
106
+ }
107
+ function permissionError(targetPath, cause) {
108
+ const syscall = cause.syscall ? ` (${cause.syscall})` : "";
109
+ return new RosterError({
110
+ header: `${chalk.red.bold("roster:")} permission denied`,
111
+ body: ` ${cause.code ?? "EACCES"}${syscall} writing ${targetPath}`,
112
+ remedy: ` Re-run with sudo, or run: sudo chown -R "$USER" ${targetPath}`,
113
+ exitCode: 1
114
+ });
115
+ }
116
+ function noToolsError(tools) {
117
+ const links = tools.map((t) => ` ${t.name.padEnd(12)} ${t.installLink}`).join("\n");
118
+ return new RosterError({
119
+ header: `${chalk.red.bold("roster:")} no AI tools detected on this machine`,
120
+ body: "Install at least one of:\n" + links,
121
+ remedy: `Re-run ${chalk.bold("roster install")} after installing one.`,
122
+ exitCode: 3
123
+ });
124
+ }
125
+ function missingScaffoldError(scaffoldPath) {
126
+ return new RosterError({
127
+ header: `${chalk.red.bold("roster:")} scaffold templates missing`,
128
+ body: ` Expected at ${scaffoldPath}`,
129
+ remedy: " This roster install is broken — reinstall with: npm install -g @firatcand/roster",
130
+ exitCode: 1
131
+ });
132
+ }
133
+ function userCancelledInit() {
134
+ return new RosterError({
135
+ header: `${chalk.dim("roster:")} cancelled`,
136
+ body: " Nothing written.",
137
+ remedy: " Re-run with --force to overwrite an existing CLAUDE.md.",
138
+ exitCode: 2
139
+ });
140
+ }
141
+ function userCancelledInstall() {
142
+ return new RosterError({
143
+ header: `${chalk.dim("roster:")} cancelled`,
144
+ body: " Nothing written.",
145
+ remedy: ` Re-run ${chalk.bold("roster install")} when ready.`,
146
+ exitCode: 2
147
+ });
148
+ }
149
+ function unexpectedError(err) {
150
+ const message = err instanceof Error ? err.message : String(err);
151
+ const wrapped = new RosterError({
152
+ header: `${chalk.red.bold("roster:")} unexpected error`,
153
+ body: ` ${message}`,
154
+ remedy: " Re-run with --debug for a full stack trace.",
155
+ exitCode: 1
156
+ });
157
+ if (err instanceof Error && err.stack) {
158
+ const wrapperHeader = wrapped.stack?.split("\n", 1)[0] ?? `RosterError: ${message}`;
159
+ const originalFrames = err.stack.replace(/^[^\n]*\n?/, "");
160
+ wrapped.stack = originalFrames ? `${wrapperHeader}\n${originalFrames}` : wrapperHeader;
161
+ }
162
+ return wrapped;
163
+ }
164
+ function renderError(err, opts) {
165
+ const out = opts.stream ?? process.stderr;
166
+ out.write(err.header + "\n");
167
+ if (err.body) out.write(err.body + "\n");
168
+ out.write(err.remedy + "\n");
169
+ if (opts.debug && err.stack) out.write(err.stack + "\n");
170
+ }
171
+ //#endregion
172
+ //#region src/lib/frontmatter.ts
173
+ function renderSkillFrontmatterContent(content, toolKey) {
174
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n/);
175
+ if (!fmMatch) return content;
176
+ const lines = (fmMatch[1] ?? "").split("\n").filter((l) => !/^installed_for:\s/.test(l));
177
+ lines.push(`installed_for: ${toolKey}`);
178
+ return `---\n${lines.join("\n")}\n---\n` + content.slice(fmMatch[0].length);
179
+ }
180
+ //#endregion
181
+ //#region src/lib/agent-render.ts
182
+ var RosterAgentRenderError = class extends Error {
183
+ field;
184
+ constructor(message, field) {
185
+ super(message);
186
+ this.name = "RosterAgentRenderError";
187
+ this.field = field;
188
+ }
189
+ };
190
+ const HEADER_COMMENT = "# Generated by roster install — DO NOT EDIT.\n# Tracks openai/codex#19399 (Codex Windows subagent TOML ignored).\n# Remove the Windows runtime-injection workaround when this issue closes.\n";
191
+ const REQUIRED_FIELDS = ["name", "description"];
192
+ function parseAgentSource(content) {
193
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n?/);
194
+ if (!fmMatch) throw new RosterAgentRenderError("Agent source has no YAML frontmatter block", "frontmatter");
195
+ const fmBody = fmMatch[1] ?? "";
196
+ const body = content.slice(fmMatch[0].length);
197
+ const fields = parseFlatFrontmatter(fmBody);
198
+ for (const required of REQUIRED_FIELDS) if (!fields[required] || fields[required].length === 0) throw new RosterAgentRenderError(`Required frontmatter field missing or empty: ${required}`, required);
199
+ const name = fields["name"];
200
+ if (name.endsWith(".persona")) throw new RosterAgentRenderError(`Agent name "${name}" must not end with ".persona" — collides with sidecar sibling`, "name");
201
+ const legacyReasoning = fields["reasoning_effort"];
202
+ const modelReasoningEffort = fields["model_reasoning_effort"] ?? legacyReasoning;
203
+ return {
204
+ name,
205
+ description: fields["description"],
206
+ body: body.replace(/^[\n\r]+/, "").replace(/\s+$/, "") + "\n",
207
+ model: fields["model"],
208
+ modelReasoningEffort
209
+ };
210
+ }
211
+ function parseFlatFrontmatter(fm) {
212
+ const out = {};
213
+ const lines = fm.split("\n");
214
+ for (const raw of lines) {
215
+ const line = raw.replace(/\r$/, "");
216
+ if (line.length === 0) continue;
217
+ if (line.startsWith("#")) continue;
218
+ const match = line.match(/^([A-Za-z_][A-Za-z0-9_-]*):\s*(.*)$/);
219
+ if (!match) throw new RosterAgentRenderError(`Unparseable frontmatter line: ${line}`, "frontmatter");
220
+ const key = match[1];
221
+ out[key] = unquoteYamlScalar((match[2] ?? "").trim(), key);
222
+ }
223
+ return out;
224
+ }
225
+ function unquoteYamlScalar(raw, key) {
226
+ if (raw.length === 0) return "";
227
+ const first = raw[0];
228
+ if (first === "\"" || first === "'") {
229
+ if (raw.length < 2 || raw[raw.length - 1] !== first) throw new RosterAgentRenderError(`Unterminated quoted scalar for ${key}: ${raw}`, key);
230
+ const inner = raw.slice(1, -1);
231
+ if (first === "\"") return inner.replace(/\\"/g, "\"").replace(/\\\\/g, "\\");
232
+ return inner;
233
+ }
234
+ return raw;
235
+ }
236
+ function renderCodexAgentToml(source) {
237
+ return renderFromParsed(parseAgentSource(source));
238
+ }
239
+ function renderFromParsed(s) {
240
+ const lines = [];
241
+ lines.push(HEADER_COMMENT.trimEnd());
242
+ lines.push("");
243
+ lines.push(`name = ${quoteBasic(s.name)}`);
244
+ lines.push(`description = ${quoteBasic(s.description)}`);
245
+ if (s.model !== void 0) lines.push(`model = ${quoteBasic(s.model)}`);
246
+ if (s.modelReasoningEffort !== void 0) lines.push(`model_reasoning_effort = ${quoteBasic(s.modelReasoningEffort)}`);
247
+ lines.push("");
248
+ lines.push("developer_instructions = \"\"\"");
249
+ lines.push(escapeTripleBasic(s.body).replace(/\n$/, ""));
250
+ lines.push("\"\"\"");
251
+ lines.push("");
252
+ return {
253
+ toml: lines.join("\n"),
254
+ personaBody: s.body
255
+ };
256
+ }
257
+ function quoteBasic(value) {
258
+ return "\"" + value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"") + "\"";
259
+ }
260
+ function escapeTripleBasic(body) {
261
+ return body.replace(/"""/g, "\\\"\\\"\\\"");
262
+ }
263
+ //#endregion
264
+ //#region src/lib/install.ts
265
+ const { copy, ensureDir } = fsExtra;
266
+ var RosterPathTraversalError = class extends Error {
267
+ target;
268
+ root;
269
+ label;
270
+ constructor(target, root, label) {
271
+ super(`Refusing to write ${label} outside configRoot: ${target} is not under ${root}`);
272
+ this.name = "RosterPathTraversalError";
273
+ this.target = target;
274
+ this.root = root;
275
+ this.label = label;
276
+ }
277
+ };
278
+ function assertWithinRoot(target, root, label) {
279
+ const absRoot = resolve(root);
280
+ const absTarget = resolve(target);
281
+ const rel = relative(absRoot, absTarget);
282
+ if (rel === "" || rel === ".") return;
283
+ if (rel === ".." || rel.startsWith(".." + sep) || isAbsolute(rel)) throw new RosterPathTraversalError(absTarget, absRoot, label);
284
+ }
285
+ function isEacces(err) {
286
+ return typeof err === "object" && err !== null && err.code === "EACCES";
287
+ }
288
+ async function defaultConfirm$1(message) {
289
+ const { confirm } = await import("@inquirer/prompts");
290
+ return confirm({
291
+ message,
292
+ default: false
293
+ });
294
+ }
295
+ const consoleLogger$1 = {
296
+ log: (m) => console.log(m),
297
+ warn: (m) => console.warn(m)
298
+ };
299
+ function renderSkillFrontmatter(skillMdPath, toolKey) {
300
+ if (!existsSync(skillMdPath)) return;
301
+ const content = readFileSync(skillMdPath, "utf8");
302
+ const newContent = renderSkillFrontmatterContent(content, toolKey);
303
+ if (newContent !== content) writeFileSync(skillMdPath, newContent);
304
+ }
305
+ async function prepareTargetForWrite(targetPath, kind, logger, confirm) {
306
+ if (existsSync(targetPath) && lstatSync(targetPath).isSymbolicLink()) {
307
+ if (!await confirm(`${targetPath} is a symbolic link. Replace it with the bundled ${kind}?`)) {
308
+ logger.warn(chalk.dim(` ~ preserved symlink: ${targetPath}`));
309
+ return false;
310
+ }
311
+ await rm(targetPath, { force: true });
312
+ }
313
+ return true;
314
+ }
315
+ async function copyOne(srcPath, targetPath, kind, logger, confirm) {
316
+ try {
317
+ if (!await prepareTargetForWrite(targetPath, kind, logger, confirm)) return false;
318
+ await copy(srcPath, targetPath, {
319
+ overwrite: true,
320
+ errorOnExist: false
321
+ });
322
+ return true;
323
+ } catch (err) {
324
+ if (isEacces(err)) throw permissionError(targetPath, err);
325
+ throw err;
326
+ }
327
+ }
328
+ async function writeRenderedOne(targetPath, contents, kind, logger, confirm) {
329
+ try {
330
+ if (!await prepareTargetForWrite(targetPath, kind, logger, confirm)) return false;
331
+ writeFileSync(targetPath, contents);
332
+ return true;
333
+ } catch (err) {
334
+ if (isEacces(err)) throw permissionError(targetPath, err);
335
+ throw err;
336
+ }
337
+ }
338
+ async function installToTool(tool, opts) {
339
+ const logger = opts.logger ?? consoleLogger$1;
340
+ const confirm = opts.confirm ?? defaultConfirm$1;
341
+ const silent = opts.silent ?? false;
342
+ const info = (msg) => {
343
+ if (!silent) logger.log(msg);
344
+ };
345
+ assertWithinRoot(tool.skillsTarget, tool.configRoot, "skillsTarget");
346
+ if (tool.agentsTarget) assertWithinRoot(tool.agentsTarget, tool.configRoot, "agentsTarget");
347
+ try {
348
+ await ensureDir(tool.skillsTarget);
349
+ if (tool.agentsTarget) await ensureDir(tool.agentsTarget);
350
+ } catch (err) {
351
+ if (isEacces(err)) throw permissionError(tool.skillsTarget, err);
352
+ throw err;
353
+ }
354
+ let skillsCount = 0;
355
+ if (existsSync(opts.skills)) {
356
+ const entries = readdirSync(opts.skills, { withFileTypes: true });
357
+ for (const dirent of entries) {
358
+ if (!dirent.isDirectory()) continue;
359
+ const srcDir = join(opts.skills, dirent.name);
360
+ const srcSkillMd = join(srcDir, "SKILL.md");
361
+ if (!existsSync(srcSkillMd)) {
362
+ logger.warn(chalk.yellow(` ! skill ${dirent.name}: SKILL.md missing — skipped`));
363
+ continue;
364
+ }
365
+ let srcPath;
366
+ let targetPath;
367
+ let renderedSkillMd;
368
+ if (tool.skillsLayout === "file") {
369
+ const ext = tool.skillsFileExt ?? ".md";
370
+ srcPath = srcSkillMd;
371
+ targetPath = join(tool.skillsTarget, `${dirent.name}${ext}`);
372
+ renderedSkillMd = targetPath;
373
+ } else {
374
+ srcPath = srcDir;
375
+ targetPath = join(tool.skillsTarget, dirent.name);
376
+ renderedSkillMd = join(targetPath, "SKILL.md");
377
+ }
378
+ assertWithinRoot(targetPath, tool.configRoot, "skill targetPath");
379
+ info(chalk.dim(` + skill ${dirent.name} -> ${targetPath}`));
380
+ if (await copyOne(srcPath, targetPath, "skill", logger, confirm)) {
381
+ renderSkillFrontmatter(renderedSkillMd, tool.key);
382
+ skillsCount++;
383
+ }
384
+ }
385
+ }
386
+ let agentsCount = 0;
387
+ if (tool.agentsTarget && existsSync(opts.agents)) {
388
+ const entries = readdirSync(opts.agents, { withFileTypes: true });
389
+ for (const dirent of entries) {
390
+ if (!dirent.isFile() || !dirent.name.endsWith(".md")) continue;
391
+ const srcPath = join(opts.agents, dirent.name);
392
+ if (tool.agentsLayout === "codex-toml") {
393
+ const baseName = dirent.name.replace(/\.md$/, "");
394
+ const tomlTarget = join(tool.agentsTarget, `${baseName}.toml`);
395
+ const personaTarget = join(tool.agentsTarget, `${baseName}.persona.md`);
396
+ assertWithinRoot(tomlTarget, tool.configRoot, "agent targetPath");
397
+ assertWithinRoot(personaTarget, tool.configRoot, "agent persona targetPath");
398
+ let rendered;
399
+ try {
400
+ rendered = renderCodexAgentToml(readFileSync(srcPath, "utf8"));
401
+ } catch (err) {
402
+ if (err instanceof RosterAgentRenderError) {
403
+ logger.warn(chalk.yellow(` ! agent ${dirent.name}: ${err.message} — skipped`));
404
+ continue;
405
+ }
406
+ throw err;
407
+ }
408
+ info(chalk.dim(` + agent ${dirent.name} -> ${tomlTarget}`));
409
+ const wroteToml = await writeRenderedOne(tomlTarget, rendered.toml, "agent", logger, confirm);
410
+ const wrotePersona = await writeRenderedOne(personaTarget, rendered.personaBody, "agent persona", logger, confirm);
411
+ if (wroteToml && wrotePersona) agentsCount++;
412
+ continue;
413
+ }
414
+ const targetPath = join(tool.agentsTarget, dirent.name);
415
+ assertWithinRoot(targetPath, tool.configRoot, "agent targetPath");
416
+ info(chalk.dim(` + agent ${dirent.name} -> ${targetPath}`));
417
+ if (await copyOne(srcPath, targetPath, "agent", logger, confirm)) agentsCount++;
418
+ }
419
+ }
420
+ return {
421
+ skillsCount,
422
+ skillsTarget: tool.skillsTarget,
423
+ agentsCount,
424
+ agentsTarget: tool.agentsTarget
425
+ };
426
+ }
427
+ //#endregion
428
+ //#region src/lib/install-args.ts
429
+ const KNOWN_TOOL_KEYS = [
430
+ "claude",
431
+ "codex",
432
+ "gemini"
433
+ ];
434
+ const TOOL_LIST = KNOWN_TOOL_KEYS.join(" | ");
435
+ function isToolKey(value) {
436
+ return KNOWN_TOOL_KEYS.includes(value);
437
+ }
438
+ function parseInstallArgs(args) {
439
+ let silent = false;
440
+ let verbose = false;
441
+ let all = false;
442
+ let toolValue = null;
443
+ let toolFlagSeen = false;
444
+ for (let i = 0; i < args.length; i++) {
445
+ const arg = args[i];
446
+ if (arg === "--silent") silent = true;
447
+ else if (arg === "--verbose") verbose = true;
448
+ else if (arg === "--all") all = true;
449
+ else if (arg === "--tool") {
450
+ toolFlagSeen = true;
451
+ const next = args[i + 1];
452
+ if (next === void 0 || next.startsWith("-")) return {
453
+ kind: "err",
454
+ message: `--tool requires a tool name (${TOOL_LIST})`
455
+ };
456
+ toolValue = next;
457
+ i++;
458
+ } else if (arg.startsWith("--tool=")) {
459
+ toolFlagSeen = true;
460
+ const value = arg.slice(7);
461
+ if (value === "") return {
462
+ kind: "err",
463
+ message: `--tool requires a tool name (${TOOL_LIST})`
464
+ };
465
+ toolValue = value;
466
+ }
467
+ }
468
+ if (all && toolFlagSeen) return {
469
+ kind: "err",
470
+ message: "flags --all and --tool are mutually exclusive"
471
+ };
472
+ if (toolFlagSeen && toolValue !== null) {
473
+ if (!isToolKey(toolValue)) return {
474
+ kind: "err",
475
+ message: `unknown tool '${toolValue}'; expected one of: ${TOOL_LIST}`
476
+ };
477
+ return {
478
+ kind: "ok",
479
+ silent,
480
+ verbose,
481
+ target: {
482
+ mode: "tool",
483
+ key: toolValue
484
+ }
485
+ };
486
+ }
487
+ if (all) return {
488
+ kind: "ok",
489
+ silent,
490
+ verbose,
491
+ target: { mode: "all" }
492
+ };
493
+ return {
494
+ kind: "ok",
495
+ silent,
496
+ verbose,
497
+ target: { mode: "interactive" }
498
+ };
499
+ }
500
+ //#endregion
501
+ //#region src/lib/doctor-args.ts
502
+ function parseDoctorArgs(args) {
503
+ let json = false;
504
+ let silent = false;
505
+ for (const arg of args) if (arg === "--json") json = true;
506
+ else if (arg === "--silent") silent = true;
507
+ return {
508
+ kind: "ok",
509
+ json,
510
+ silent
511
+ };
512
+ }
513
+ //#endregion
514
+ //#region src/lib/schedule-args.ts
515
+ const SCHEDULE_SUBCOMMANDS = new Set(["validate"]);
516
+ function parseScheduleArgs(args) {
517
+ const [first, ...rest] = args;
518
+ if (first === void 0) return {
519
+ kind: "err",
520
+ message: "missing subcommand for 'schedule' (available: validate)"
521
+ };
522
+ if (!SCHEDULE_SUBCOMMANDS.has(first)) return {
523
+ kind: "err",
524
+ message: `unknown 'schedule' subcommand '${first}' (available: validate)`
525
+ };
526
+ let json = false;
527
+ let silent = false;
528
+ let cwd;
529
+ for (let i = 0; i < rest.length; i++) {
530
+ const arg = rest[i];
531
+ if (arg === "--json") json = true;
532
+ else if (arg === "--silent") silent = true;
533
+ else if (arg === "--cwd") {
534
+ const next = rest[i + 1];
535
+ if (next === void 0) return {
536
+ kind: "err",
537
+ message: "--cwd requires a path argument"
538
+ };
539
+ cwd = next;
540
+ i++;
541
+ } else if (arg.startsWith("--cwd=")) cwd = arg.slice(6);
542
+ else if (arg.startsWith("-")) return {
543
+ kind: "err",
544
+ message: `unknown flag for 'schedule ${first}': ${arg}`
545
+ };
546
+ else return {
547
+ kind: "err",
548
+ message: `unexpected positional argument for 'schedule ${first}': ${arg}`
549
+ };
550
+ }
551
+ return {
552
+ kind: "ok",
553
+ subcommand: first,
554
+ json,
555
+ silent,
556
+ cwd
557
+ };
558
+ }
559
+ //#endregion
560
+ //#region src/lib/fs-utils.ts
561
+ function entryAtPath$1(path) {
562
+ try {
563
+ const st = lstatSync(path);
564
+ return {
565
+ exists: true,
566
+ isSymlink: st.isSymbolicLink(),
567
+ isDirectory: st.isDirectory(),
568
+ unreadable: false
569
+ };
570
+ } catch (err) {
571
+ const code = err.code;
572
+ if (code === "ENOENT" || code === "ENOTDIR") return {
573
+ exists: false,
574
+ isSymlink: false,
575
+ isDirectory: false,
576
+ unreadable: false
577
+ };
578
+ return {
579
+ exists: true,
580
+ isSymlink: false,
581
+ isDirectory: false,
582
+ unreadable: true,
583
+ error: err.message
584
+ };
585
+ }
586
+ }
587
+ const FALLBACK_CODES = new Set([
588
+ "EPERM",
589
+ "ENOSYS",
590
+ "EXDEV"
591
+ ]);
592
+ function probeSymlinkSupport(cwd) {
593
+ const tempPath = join(cwd, `.roster-probe-${Date.now()}`);
594
+ try {
595
+ symlinkSync("CONTEXT.md", tempPath);
596
+ unlinkSync(tempPath);
597
+ return true;
598
+ } catch (err) {
599
+ const code = err.code;
600
+ if (code !== void 0 && FALLBACK_CODES.has(code)) return false;
601
+ throw err;
602
+ }
603
+ }
604
+ function safeRead(path) {
605
+ try {
606
+ return readFileSync(path, "utf8");
607
+ } catch {
608
+ return null;
609
+ }
610
+ }
611
+ function safeReadlink(path) {
612
+ try {
613
+ return readlinkSync(path);
614
+ } catch {
615
+ return null;
616
+ }
617
+ }
618
+ //#endregion
619
+ //#region src/lib/project-context.ts
620
+ function substitute$1(template, vars) {
621
+ return template.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? `{{${key}}}`);
622
+ }
623
+ const MARKER_RE = /^<!--\s*roster:(managed|user):(start|end)\s+(\S+)\s*-->$/;
624
+ function parseRegions(content) {
625
+ const managed = /* @__PURE__ */ new Map();
626
+ const user = /* @__PURE__ */ new Map();
627
+ const errors = [];
628
+ let open = null;
629
+ const lines = content.split("\n");
630
+ for (let i = 0; i < lines.length; i++) {
631
+ const line = lines[i];
632
+ const m = MARKER_RE.exec(line.trim());
633
+ if (!m) {
634
+ if (open) open.lines.push(line);
635
+ continue;
636
+ }
637
+ const [, kind, action, name] = m;
638
+ if (action === "start") if (open) {
639
+ errors.push(`Line ${i + 1}: start of region '${name}' found while region '${open.name}' is still open`);
640
+ open.lines.push(line);
641
+ } else open = {
642
+ kind,
643
+ name,
644
+ startLine: i + 1,
645
+ lines: []
646
+ };
647
+ else if (!open) errors.push(`Line ${i + 1}: end marker for '${name}' found with no open region`);
648
+ else if (open.name !== name) {
649
+ errors.push(`Line ${i + 1}: end marker for '${name}' does not match open region '${open.name}'`);
650
+ open = null;
651
+ } else {
652
+ const innerContent = open.lines.join("\n");
653
+ if (open.kind === "managed") managed.set(open.name, innerContent);
654
+ else user.set(open.name, innerContent);
655
+ open = null;
656
+ }
657
+ }
658
+ if (open) errors.push(`End of file: region '${open.name}' (started at line ${open.startLine}) was never closed`);
659
+ return {
660
+ managed,
661
+ user,
662
+ ok: errors.length === 0,
663
+ errors
664
+ };
665
+ }
666
+ function mergeRegions(existing, fresh, opts) {
667
+ const existingParsed = parseRegions(existing);
668
+ if (!existingParsed.ok) {
669
+ if (!opts.force) throw new Error(`roster: CONTEXT.md has malformed region markers:\n` + existingParsed.errors.map((e) => ` - ${e}`).join("\n") + `\n Re-run with --force to overwrite, or repair the markers manually.`);
670
+ return {
671
+ merged: fresh,
672
+ warnings: ["Malformed markers in existing CONTEXT.md — overwrote with fresh template."]
673
+ };
674
+ }
675
+ const freshParsed = parseRegions(fresh);
676
+ const warnings = [];
677
+ const outputLines = [];
678
+ let skipLines = false;
679
+ const freshLines = fresh.split("\n");
680
+ for (const line of freshLines) {
681
+ const m = MARKER_RE.exec(line.trim());
682
+ if (!m) {
683
+ if (!skipLines) outputLines.push(line);
684
+ continue;
685
+ }
686
+ const [, kind, action, name] = m;
687
+ if (action === "start") {
688
+ outputLines.push(line);
689
+ if (kind === "managed") {
690
+ const freshContent = freshParsed.managed.get(name) ?? "";
691
+ outputLines.push(freshContent);
692
+ skipLines = true;
693
+ } else {
694
+ const existingContent = existingParsed.user.get(name);
695
+ if (existingContent !== void 0) outputLines.push(existingContent);
696
+ else {
697
+ const freshContent = freshParsed.user.get(name) ?? "";
698
+ outputLines.push(freshContent);
699
+ }
700
+ skipLines = true;
701
+ }
702
+ } else {
703
+ skipLines = false;
704
+ outputLines.push(line);
705
+ }
706
+ }
707
+ for (const [name, content] of existingParsed.user) if (!freshParsed.user.has(name)) {
708
+ outputLines.push("");
709
+ outputLines.push(`<!-- roster:user:start ${name} -->`);
710
+ outputLines.push(content);
711
+ outputLines.push(`<!-- roster:user:end ${name} -->`);
712
+ }
713
+ return {
714
+ merged: outputLines.join("\n"),
715
+ warnings
716
+ };
717
+ }
718
+ function renderTemplate(projectName) {
719
+ return substitute$1(readFileSync(join(ROSTER_ROOT, "templates", "CONTEXT.template.md"), "utf8"), { PROJECT_NAME: projectName });
720
+ }
721
+ function ensureSymlink(cwd, linkName, target, force, result) {
722
+ const linkPath = join(cwd, linkName);
723
+ const entry = entryAtPath$1(linkPath);
724
+ if (entry.unreadable) throw new Error(`roster: could not stat ${linkPath}: ${entry.error ?? "unknown error"}`);
725
+ if (entry.isDirectory) throw new Error(`roster: cannot create symlink at ${linkPath} — a directory exists at that path. Remove it and re-run.`);
726
+ if (!entry.exists) {
727
+ symlinkSync(target, linkPath);
728
+ result.filesLinked.push(linkName);
729
+ return;
730
+ }
731
+ if (entry.isSymlink) {
732
+ const actual = safeReadlink(linkPath);
733
+ if (actual === null) throw new Error(`roster: could not read symlink at ${linkPath}`);
734
+ if (actual === target) {
735
+ result.filesSkipped.push(linkName);
736
+ return;
737
+ }
738
+ if (!force) throw new Error(`roster: ${linkPath} is a symlink pointing to '${actual}', expected '${target}'. Re-run with --force to re-link.`);
739
+ unlinkSync(linkPath);
740
+ symlinkSync(target, linkPath);
741
+ result.filesLinked.push(linkName);
742
+ return;
743
+ }
744
+ if (!force) throw new Error(`roster: ${linkPath} is a regular file. Re-run with --force to replace it with a symlink.`);
745
+ unlinkSync(linkPath);
746
+ symlinkSync(target, linkPath);
747
+ result.filesLinked.push(linkName);
748
+ }
749
+ function writeContextAndLinks(cwd, projectName, opts) {
750
+ const result = {
751
+ filesWritten: [],
752
+ filesUpdated: [],
753
+ filesSkipped: [],
754
+ filesLinked: [],
755
+ warnings: []
756
+ };
757
+ const fresh = renderTemplate(projectName);
758
+ const contextPath = join(cwd, "CONTEXT.md");
759
+ let effectiveContent;
760
+ if (existsSync(contextPath)) {
761
+ const existing = readFileSync(contextPath, "utf8");
762
+ const { merged, warnings } = mergeRegions(existing, fresh, { force: opts.force });
763
+ result.warnings.push(...warnings);
764
+ effectiveContent = merged;
765
+ if (merged !== existing) {
766
+ writeFileSync(contextPath, merged, "utf8");
767
+ result.filesUpdated.push("CONTEXT.md");
768
+ } else result.filesSkipped.push("CONTEXT.md");
769
+ } else {
770
+ writeFileSync(contextPath, fresh, "utf8");
771
+ result.filesWritten.push("CONTEXT.md");
772
+ effectiveContent = fresh;
773
+ }
774
+ if ((opts.platform ?? process.platform) === "win32") {
775
+ for (const name of ["CLAUDE.md", "AGENTS.md"]) {
776
+ writeFileSync(join(cwd, name), effectiveContent, "utf8");
777
+ result.filesWritten.push(name);
778
+ }
779
+ return result;
780
+ }
781
+ if (!probeSymlinkSupport(cwd)) {
782
+ for (const name of ["CLAUDE.md", "AGENTS.md"]) {
783
+ writeFileSync(join(cwd, name), effectiveContent, "utf8");
784
+ result.filesWritten.push(name);
785
+ }
786
+ result.warnings.push("Symlinks not supported on this filesystem — wrote CLAUDE.md and AGENTS.md as regular files.");
787
+ return result;
788
+ }
789
+ for (const name of ["CLAUDE.md", "AGENTS.md"]) ensureSymlink(cwd, name, "CONTEXT.md", opts.force, result);
790
+ return result;
791
+ }
792
+ function auditWorkspace(cwd, opts) {
793
+ const contextPath = join(cwd, "CONTEXT.md");
794
+ const contextMdExists = existsSync(contextPath);
795
+ const items = [];
796
+ const warnings = [];
797
+ const platform = opts?.platform ?? process.platform;
798
+ if (!contextMdExists) return {
799
+ cwd,
800
+ contextMdExists: false,
801
+ items: [],
802
+ warnings,
803
+ ok: true
804
+ };
805
+ for (const linkName of ["CLAUDE.md", "AGENTS.md"]) {
806
+ const linkPath = join(cwd, linkName);
807
+ const entry = entryAtPath$1(linkPath);
808
+ if (!entry.exists && !entry.unreadable) {
809
+ items.push({
810
+ name: linkName,
811
+ status: "missing"
812
+ });
813
+ continue;
814
+ }
815
+ if (entry.unreadable) {
816
+ items.push({
817
+ name: linkName,
818
+ status: "unreadable",
819
+ reason: entry.error
820
+ });
821
+ continue;
822
+ }
823
+ if (entry.isDirectory) {
824
+ items.push({
825
+ name: linkName,
826
+ status: "is-directory",
827
+ reason: "directory found at expected link path"
828
+ });
829
+ continue;
830
+ }
831
+ if (platform === "win32") {
832
+ const contextContent = safeRead(contextPath);
833
+ const fileContent = safeRead(linkPath);
834
+ if (contextContent === null || fileContent === null) {
835
+ items.push({
836
+ name: linkName,
837
+ status: "unreadable",
838
+ reason: "could not read file for comparison"
839
+ });
840
+ continue;
841
+ }
842
+ if (contextContent === fileContent) items.push({
843
+ name: linkName,
844
+ status: "ok"
845
+ });
846
+ else items.push({
847
+ name: linkName,
848
+ status: "content-diverged",
849
+ reason: "file content differs from CONTEXT.md"
850
+ });
851
+ continue;
852
+ }
853
+ if (!entry.isSymlink) {
854
+ items.push({
855
+ name: linkName,
856
+ status: "not-a-symlink",
857
+ reason: "regular file, re-run roster init --force to repair"
858
+ });
859
+ continue;
860
+ }
861
+ const actual = safeReadlink(linkPath);
862
+ if (actual === null) {
863
+ items.push({
864
+ name: linkName,
865
+ status: "unreadable",
866
+ reason: "could not read symlink target"
867
+ });
868
+ continue;
869
+ }
870
+ if (actual === "CONTEXT.md") items.push({
871
+ name: linkName,
872
+ status: "ok"
873
+ });
874
+ else items.push({
875
+ name: linkName,
876
+ status: "wrong-target",
877
+ reason: `points to '${actual}', expected 'CONTEXT.md'`
878
+ });
879
+ }
880
+ return {
881
+ cwd,
882
+ contextMdExists,
883
+ items,
884
+ warnings,
885
+ ok: items.every((i) => i.status === "ok")
886
+ };
887
+ }
888
+ const FORGE_MARKERS = [
889
+ "BRIEF.md",
890
+ "spec/PRD.md",
891
+ "plans/phases.yaml"
892
+ ];
893
+ const TEMPLATE_SUFFIX_RE = /\.template(\.[^.]+)?$/;
894
+ function readTemplate(name) {
895
+ return readFileSync(join(ROSTER_ROOT, "templates", name), "utf8");
896
+ }
897
+ function substitute(template, vars) {
898
+ return template.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? `{{${key}}}`);
899
+ }
900
+ function detectForgeMarkers(cwd) {
901
+ return FORGE_MARKERS.filter((m) => existsSync(join(cwd, m)));
902
+ }
903
+ function entryAtPath(path) {
904
+ try {
905
+ return {
906
+ exists: true,
907
+ isSymlink: lstatSync(path).isSymbolicLink()
908
+ };
909
+ } catch {
910
+ return {
911
+ exists: false,
912
+ isSymlink: false
913
+ };
914
+ }
915
+ }
916
+ function walkScaffold(srcRoot, dstRoot, vars) {
917
+ const written = [];
918
+ function walk(srcDir, dstDir) {
919
+ const entries = readdirSync(srcDir, { withFileTypes: true });
920
+ for (const entry of entries) {
921
+ const srcPath = join(srcDir, entry.name);
922
+ if (entry.isDirectory()) {
923
+ const subDst = join(dstDir, entry.name);
924
+ mkdirSync(subDst, { recursive: true });
925
+ walk(srcPath, subDst);
926
+ continue;
927
+ }
928
+ if (!entry.isFile()) throw new Error(`roster: unexpected entry type in scaffold: ${srcPath}. Scaffold must contain only regular files and directories.`);
929
+ if (TEMPLATE_SUFFIX_RE.test(entry.name)) {
930
+ const dstPath = join(dstDir, entry.name.replace(TEMPLATE_SUFFIX_RE, "$1"));
931
+ if (entryAtPath(dstPath).isSymlink) throw new Error(`roster: refusing to overwrite symlink at ${dstPath}. Remove the symlink and re-run roster init.`);
932
+ writeFileSync(dstPath, substitute(readFileSync(srcPath, "utf8"), vars), "utf8");
933
+ written.push(relative(dstRoot, dstPath));
934
+ continue;
935
+ }
936
+ const dstPath = join(dstDir, entry.name);
937
+ if (entryAtPath(dstPath).exists) continue;
938
+ copyFileSync(srcPath, dstPath);
939
+ written.push(relative(dstRoot, dstPath));
940
+ }
941
+ }
942
+ mkdirSync(dstRoot, { recursive: true });
943
+ walk(srcRoot, dstRoot);
944
+ return written;
945
+ }
946
+ async function defaultConfirm(message) {
947
+ const { confirm } = await import("@inquirer/prompts");
948
+ return confirm({
949
+ message,
950
+ default: false
951
+ });
952
+ }
953
+ const consoleLogger = {
954
+ log: (m) => console.log(m),
955
+ warn: (m) => console.warn(m)
956
+ };
957
+ function ensureEnvExample(cwd) {
958
+ const target = join(cwd, ".env.example");
959
+ if (existsSync(target)) return;
960
+ writeFileSync(target, readTemplate("env.example"), "utf8");
961
+ }
962
+ function appendGitignoreBlock(cwd) {
963
+ const path = join(cwd, ".gitignore");
964
+ const block = readTemplate("gitignore-defaults.txt");
965
+ if (!existsSync(path)) {
966
+ writeFileSync(path, block, "utf8");
967
+ return "written";
968
+ }
969
+ const current = readFileSync(path, "utf8");
970
+ if (current.includes("# Roster defaults")) return "skipped";
971
+ writeFileSync(path, current + (current.endsWith("\n") ? "\n" : "\n\n") + block, "utf8");
972
+ return "written";
973
+ }
974
+ async function maybeGitInit(cwd, noGit, confirm) {
975
+ if (noGit) return false;
976
+ if (existsSync(join(cwd, ".git"))) return false;
977
+ if (!await confirm("Initialize a git repo here?")) return false;
978
+ try {
979
+ execFileSync("git", ["init", "-q"], {
980
+ cwd,
981
+ stdio: "ignore"
982
+ });
983
+ return true;
984
+ } catch {
985
+ return false;
986
+ }
987
+ }
988
+ async function executeInit(opts) {
989
+ const projectName = opts.name ?? basename(opts.cwd);
990
+ const logger = opts.logger ?? consoleLogger;
991
+ const silent = opts.silent ?? false;
992
+ const force = opts.force ?? false;
993
+ const migrate = opts.migrate ?? false;
994
+ const confirm = opts.confirm ?? defaultConfirm;
995
+ const info = (msg) => {
996
+ if (!silent) logger.log(msg);
997
+ };
998
+ const forgeMarkers = detectForgeMarkers(opts.cwd);
999
+ const contextMdPath = join(opts.cwd, "CONTEXT.md");
1000
+ const claudeMdPath = join(opts.cwd, "CLAUDE.md");
1001
+ const contextMdExists = existsSync(contextMdPath);
1002
+ const claudeMdExists = existsSync(claudeMdPath);
1003
+ const claudeMdIsSymlink = claudeMdExists ? (() => {
1004
+ try {
1005
+ return lstatSync(claudeMdPath).isSymbolicLink();
1006
+ } catch {
1007
+ return false;
1008
+ }
1009
+ })() : false;
1010
+ const isMigration = claudeMdExists && !contextMdExists && !claudeMdIsSymlink;
1011
+ const cancelledResult = {
1012
+ status: "cancelled",
1013
+ workspaceRoot: opts.cwd,
1014
+ projectName,
1015
+ filesWritten: [],
1016
+ filesUpdated: [],
1017
+ filesSkipped: [],
1018
+ filesLinked: [],
1019
+ warnings: [],
1020
+ gitInitialized: false
1021
+ };
1022
+ if (isMigration && !force && !migrate) {
1023
+ info("roster: detected a pre-CONTEXT.md workspace (CLAUDE.md is a regular file).\n - Re-run with --migrate to upgrade and preserve your CLAUDE.md content.\n - Re-run with --force to replace CLAUDE.md without preserving content.\n - Or manually delete CLAUDE.md and re-run roster init.");
1024
+ return cancelledResult;
1025
+ }
1026
+ if (!isMigration && (contextMdExists || claudeMdExists) && !force) {
1027
+ const message = forgeMarkers.length > 0 ? `This looks like a forge-initialized project (found: ${forgeMarkers.join(", ")}). Running roster init will update CONTEXT.md and add the agent-team scaffold. Continue?` : `CONTEXT.md or CLAUDE.md already exists in this directory. Overwrite?`;
1028
+ let ok;
1029
+ try {
1030
+ ok = await confirm(message);
1031
+ } catch {
1032
+ ok = false;
1033
+ }
1034
+ if (!ok) return cancelledResult;
1035
+ } else if (forgeMarkers.length > 0 && !isMigration && !silent) logger.warn(chalk.yellow(`Detected forge artifacts (${forgeMarkers.join(", ")}); roster scaffold will be added alongside.`));
1036
+ const filesWritten = [];
1037
+ const filesUpdated = [];
1038
+ const filesSkipped = [];
1039
+ const filesLinked = [];
1040
+ const warnings = [];
1041
+ const scaffoldSrc = join(ROSTER_ROOT, "templates", "scaffold");
1042
+ if (!existsSync(scaffoldSrc)) throw missingScaffoldError(scaffoldSrc);
1043
+ const scaffoldFiles = walkScaffold(scaffoldSrc, opts.cwd, { PROJECT_NAME: projectName });
1044
+ filesWritten.push(...scaffoldFiles);
1045
+ if (isMigration && migrate) {
1046
+ const existingClaudeMd = readFileSync(claudeMdPath, "utf8");
1047
+ const oldTemplateSrc = join(ROSTER_ROOT, "templates", "CLAUDE.project.template.md");
1048
+ let oldTemplateRendered = "";
1049
+ try {
1050
+ oldTemplateRendered = substitute(readFileSync(oldTemplateSrc, "utf8"), { PROJECT_NAME: projectName });
1051
+ } catch {}
1052
+ const userLines = [];
1053
+ const existingLines = existingClaudeMd.split("\n");
1054
+ if (!oldTemplateRendered || existingClaudeMd.trim() !== oldTemplateRendered.trim()) {
1055
+ const templateSet = new Set(oldTemplateRendered.split("\n").map((l) => l.trim()));
1056
+ for (const line of existingLines) if (!templateSet.has(line.trim())) userLines.push(line);
1057
+ }
1058
+ if (userLines.length > 0) {
1059
+ const rendered = substitute(readFileSync(join(ROSTER_ROOT, "templates", "CONTEXT.template.md"), "utf8"), { PROJECT_NAME: projectName });
1060
+ const USER_START = `<!-- roster:user:start workspace -->`;
1061
+ const USER_END = `<!-- roster:user:end workspace -->`;
1062
+ const userPlaceholder = `## Workspace: ${projectName}\n\n[Replace this section with project-specific context: domain, goals, constraints.]`;
1063
+ const userInjected = userLines.join("\n").trim();
1064
+ writeFileSync(contextMdPath, rendered.replace(`${USER_START}\n${userPlaceholder}\n${USER_END}`, `${USER_START}\n${userInjected}\n${USER_END}`), "utf8");
1065
+ }
1066
+ unlinkSync(claudeMdPath);
1067
+ const agentsMdPath = join(opts.cwd, "AGENTS.md");
1068
+ if (existsSync(agentsMdPath) && !lstatSync(agentsMdPath).isSymbolicLink()) unlinkSync(agentsMdPath);
1069
+ }
1070
+ const writeResult = writeContextAndLinks(opts.cwd, projectName, {
1071
+ force,
1072
+ platform: opts.platform
1073
+ });
1074
+ filesWritten.push(...writeResult.filesWritten);
1075
+ filesUpdated.push(...writeResult.filesUpdated);
1076
+ filesSkipped.push(...writeResult.filesSkipped);
1077
+ filesLinked.push(...writeResult.filesLinked);
1078
+ warnings.push(...writeResult.warnings);
1079
+ if (!existsSync(join(opts.cwd, ".env.example"))) {
1080
+ ensureEnvExample(opts.cwd);
1081
+ filesWritten.push(".env.example");
1082
+ }
1083
+ if (appendGitignoreBlock(opts.cwd) === "written") filesWritten.push(".gitignore");
1084
+ else filesSkipped.push(".gitignore");
1085
+ const gitInitialized = await maybeGitInit(opts.cwd, opts.noGit ?? false, confirm);
1086
+ const totalChanged = filesWritten.length + filesUpdated.length + filesLinked.length;
1087
+ if (!silent) {
1088
+ info("");
1089
+ info(`${chalk.green("✓")} Initialized ${chalk.bold(projectName)} in ${opts.cwd}`);
1090
+ if (totalChanged > 8) info(chalk.dim(`Files: ${totalChanged} written/linked`));
1091
+ else {
1092
+ const changed = [
1093
+ ...filesWritten,
1094
+ ...filesUpdated,
1095
+ ...filesLinked
1096
+ ];
1097
+ info(chalk.dim("Files: ") + changed.join(", "));
1098
+ }
1099
+ for (const w of warnings) info(chalk.yellow(w));
1100
+ if (gitInitialized) info(chalk.dim("Git: initialized .git/"));
1101
+ info("");
1102
+ info(`${chalk.dim("Next: ")}${chalk.bold("open in Claude Code")}${chalk.dim(" and run ")}${chalk.bold("/chief-of-staff audit-repo")}`);
1103
+ }
1104
+ return {
1105
+ status: "ok",
1106
+ workspaceRoot: opts.cwd,
1107
+ projectName,
1108
+ filesWritten,
1109
+ filesUpdated,
1110
+ filesSkipped,
1111
+ filesLinked,
1112
+ warnings,
1113
+ gitInitialized
1114
+ };
1115
+ }
1116
+ //#endregion
1117
+ //#region src/lib/audit.ts
1118
+ function listDirNames(root, kind) {
1119
+ if (!existsSync(root)) return [];
1120
+ return readdirSync(root, { withFileTypes: true }).filter((d) => kind === "dir" ? d.isDirectory() : d.isFile()).map((d) => d.name).sort();
1121
+ }
1122
+ function walkSourceFiles(root) {
1123
+ const out = [];
1124
+ function recurse(dir, rel) {
1125
+ for (const dirent of readdirSync(dir, { withFileTypes: true })) {
1126
+ const full = join(dir, dirent.name);
1127
+ const nextRel = rel ? join(rel, dirent.name) : dirent.name;
1128
+ if (dirent.isDirectory()) recurse(full, nextRel);
1129
+ else if (dirent.isFile()) out.push(nextRel);
1130
+ }
1131
+ }
1132
+ recurse(root, "");
1133
+ return out;
1134
+ }
1135
+ function auditSkillDir(name, srcDir, targetDir, toolKey) {
1136
+ if (!existsSync(targetDir)) return {
1137
+ kind: "skill",
1138
+ name,
1139
+ status: "missing",
1140
+ targetPath: targetDir
1141
+ };
1142
+ try {
1143
+ const files = walkSourceFiles(srcDir);
1144
+ for (const rel of files) {
1145
+ const tgt = join(targetDir, rel);
1146
+ if (!existsSync(tgt)) return {
1147
+ kind: "skill",
1148
+ name,
1149
+ status: "stale",
1150
+ targetPath: targetDir,
1151
+ reason: `missing file: ${rel}`
1152
+ };
1153
+ if (rel === "SKILL.md") {
1154
+ if (renderSkillFrontmatterContent(readFileSync(join(srcDir, rel), "utf8"), toolKey) !== readFileSync(tgt, "utf8")) return {
1155
+ kind: "skill",
1156
+ name,
1157
+ status: "stale",
1158
+ targetPath: targetDir,
1159
+ reason: `bytes differ: ${rel}`
1160
+ };
1161
+ continue;
1162
+ }
1163
+ const srcBytes = readFileSync(join(srcDir, rel));
1164
+ const tgtBytes = readFileSync(tgt);
1165
+ if (Buffer.compare(srcBytes, tgtBytes) !== 0) return {
1166
+ kind: "skill",
1167
+ name,
1168
+ status: "stale",
1169
+ targetPath: targetDir,
1170
+ reason: `bytes differ: ${rel}`
1171
+ };
1172
+ }
1173
+ return {
1174
+ kind: "skill",
1175
+ name,
1176
+ status: "ok",
1177
+ targetPath: targetDir
1178
+ };
1179
+ } catch (err) {
1180
+ return {
1181
+ kind: "skill",
1182
+ name,
1183
+ status: "stale",
1184
+ targetPath: targetDir,
1185
+ reason: err.code ?? "EUNKNOWN"
1186
+ };
1187
+ }
1188
+ }
1189
+ function auditSkillFlatFile(name, srcSkillMd, targetFile, toolKey) {
1190
+ if (!existsSync(targetFile)) return {
1191
+ kind: "skill",
1192
+ name,
1193
+ status: "missing",
1194
+ targetPath: targetFile
1195
+ };
1196
+ try {
1197
+ if (renderSkillFrontmatterContent(readFileSync(srcSkillMd, "utf8"), toolKey) !== readFileSync(targetFile, "utf8")) return {
1198
+ kind: "skill",
1199
+ name,
1200
+ status: "stale",
1201
+ targetPath: targetFile,
1202
+ reason: "bytes differ"
1203
+ };
1204
+ return {
1205
+ kind: "skill",
1206
+ name,
1207
+ status: "ok",
1208
+ targetPath: targetFile
1209
+ };
1210
+ } catch (err) {
1211
+ return {
1212
+ kind: "skill",
1213
+ name,
1214
+ status: "stale",
1215
+ targetPath: targetFile,
1216
+ reason: err.code ?? "EUNKNOWN"
1217
+ };
1218
+ }
1219
+ }
1220
+ function auditAgentMdCopy(name, srcFile, targetFile) {
1221
+ if (!existsSync(targetFile)) return {
1222
+ kind: "agent",
1223
+ name,
1224
+ status: "missing",
1225
+ targetPath: targetFile
1226
+ };
1227
+ try {
1228
+ const srcBytes = readFileSync(srcFile);
1229
+ const tgtBytes = readFileSync(targetFile);
1230
+ if (Buffer.compare(srcBytes, tgtBytes) !== 0) return {
1231
+ kind: "agent",
1232
+ name,
1233
+ status: "stale",
1234
+ targetPath: targetFile,
1235
+ reason: "bytes differ"
1236
+ };
1237
+ return {
1238
+ kind: "agent",
1239
+ name,
1240
+ status: "ok",
1241
+ targetPath: targetFile
1242
+ };
1243
+ } catch (err) {
1244
+ return {
1245
+ kind: "agent",
1246
+ name,
1247
+ status: "stale",
1248
+ targetPath: targetFile,
1249
+ reason: err.code ?? "EUNKNOWN"
1250
+ };
1251
+ }
1252
+ }
1253
+ function auditAgentCodexToml(name, srcFile, tomlTarget, personaTarget) {
1254
+ if (!existsSync(tomlTarget)) return {
1255
+ kind: "agent",
1256
+ name,
1257
+ status: "missing",
1258
+ targetPath: tomlTarget
1259
+ };
1260
+ if (!existsSync(personaTarget)) return {
1261
+ kind: "agent",
1262
+ name,
1263
+ status: "missing",
1264
+ targetPath: personaTarget,
1265
+ reason: "persona sidecar missing"
1266
+ };
1267
+ try {
1268
+ const rendered = renderCodexAgentToml(readFileSync(srcFile, "utf8"));
1269
+ if (readFileSync(tomlTarget, "utf8") !== rendered.toml) return {
1270
+ kind: "agent",
1271
+ name,
1272
+ status: "stale",
1273
+ targetPath: tomlTarget,
1274
+ reason: "toml bytes differ"
1275
+ };
1276
+ if (readFileSync(personaTarget, "utf8") !== rendered.personaBody) return {
1277
+ kind: "agent",
1278
+ name,
1279
+ status: "stale",
1280
+ targetPath: personaTarget,
1281
+ reason: "persona bytes differ"
1282
+ };
1283
+ return {
1284
+ kind: "agent",
1285
+ name,
1286
+ status: "ok",
1287
+ targetPath: tomlTarget
1288
+ };
1289
+ } catch (err) {
1290
+ if (err instanceof RosterAgentRenderError) return {
1291
+ kind: "agent",
1292
+ name,
1293
+ status: "stale",
1294
+ targetPath: tomlTarget,
1295
+ reason: `render: ${err.field}`
1296
+ };
1297
+ return {
1298
+ kind: "agent",
1299
+ name,
1300
+ status: "stale",
1301
+ targetPath: tomlTarget,
1302
+ reason: err.code ?? "EUNKNOWN"
1303
+ };
1304
+ }
1305
+ }
1306
+ function auditTool(tool, sources) {
1307
+ const items = [];
1308
+ for (const skillName of listDirNames(sources.skills, "dir")) {
1309
+ const srcDir = join(sources.skills, skillName);
1310
+ if (tool.skillsLayout === "file") {
1311
+ const srcSkillMd = join(srcDir, "SKILL.md");
1312
+ if (!existsSync(srcSkillMd)) continue;
1313
+ const ext = tool.skillsFileExt ?? ".md";
1314
+ const targetFile = join(tool.skillsTarget, `${skillName}${ext}`);
1315
+ items.push(auditSkillFlatFile(skillName, srcSkillMd, targetFile, tool.key));
1316
+ } else {
1317
+ if (!existsSync(join(srcDir, "SKILL.md"))) continue;
1318
+ const targetDir = join(tool.skillsTarget, skillName);
1319
+ items.push(auditSkillDir(skillName, srcDir, targetDir, tool.key));
1320
+ }
1321
+ }
1322
+ if (tool.agentsTarget) for (const agentName of listDirNames(sources.agents, "file")) {
1323
+ if (!agentName.endsWith(".md")) continue;
1324
+ const srcFile = join(sources.agents, agentName);
1325
+ if (tool.agentsLayout === "codex-toml") {
1326
+ const baseName = agentName.replace(/\.md$/, "");
1327
+ const tomlTarget = join(tool.agentsTarget, `${baseName}.toml`);
1328
+ const personaTarget = join(tool.agentsTarget, `${baseName}.persona.md`);
1329
+ items.push(auditAgentCodexToml(agentName, srcFile, tomlTarget, personaTarget));
1330
+ } else {
1331
+ const targetFile = join(tool.agentsTarget, agentName);
1332
+ items.push(auditAgentMdCopy(agentName, srcFile, targetFile));
1333
+ }
1334
+ }
1335
+ const ok = items.every((i) => i.status === "ok");
1336
+ return {
1337
+ tool: tool.key,
1338
+ toolName: tool.name,
1339
+ configRoot: tool.configRoot,
1340
+ items,
1341
+ ok
1342
+ };
1343
+ }
1344
+ const KEBAB_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
1345
+ const CRON_ALIASES = new Set([
1346
+ "@hourly",
1347
+ "@daily",
1348
+ "@weekly",
1349
+ "@monthly",
1350
+ "@yearly",
1351
+ "@annually"
1352
+ ]);
1353
+ const FIELD_RANGES = [
1354
+ {
1355
+ min: 0,
1356
+ max: 59
1357
+ },
1358
+ {
1359
+ min: 0,
1360
+ max: 23
1361
+ },
1362
+ {
1363
+ min: 1,
1364
+ max: 31
1365
+ },
1366
+ {
1367
+ min: 1,
1368
+ max: 12
1369
+ },
1370
+ {
1371
+ min: 0,
1372
+ max: 7
1373
+ }
1374
+ ];
1375
+ const FIELD_NAMES = [
1376
+ "minute",
1377
+ "hour",
1378
+ "day-of-month",
1379
+ "month",
1380
+ "day-of-week"
1381
+ ];
1382
+ function validateCronAtom(atom, range) {
1383
+ if (atom === "*") return true;
1384
+ if (atom.includes("/")) {
1385
+ const parts = atom.split("/");
1386
+ if (parts.length !== 2) return false;
1387
+ const [base, stepStr] = parts;
1388
+ if (!base || !stepStr) return false;
1389
+ const step = Number(stepStr);
1390
+ if (!Number.isInteger(step) || step <= 0) return false;
1391
+ if (base === "*") return true;
1392
+ return validateCronAtom(base, range);
1393
+ }
1394
+ if (atom.includes("-")) {
1395
+ const parts = atom.split("-");
1396
+ if (parts.length !== 2) return false;
1397
+ const [aStr, bStr] = parts;
1398
+ if (!aStr || !bStr) return false;
1399
+ const a = Number(aStr);
1400
+ const b = Number(bStr);
1401
+ if (!Number.isInteger(a) || !Number.isInteger(b)) return false;
1402
+ if (a < range.min || b > range.max || a > b) return false;
1403
+ return true;
1404
+ }
1405
+ if (atom.length === 0) return false;
1406
+ const n = Number(atom);
1407
+ if (!Number.isInteger(n)) return false;
1408
+ return n >= range.min && n <= range.max;
1409
+ }
1410
+ function validateCronField(field, range) {
1411
+ if (field.length === 0) return false;
1412
+ return field.split(",").every((atom) => validateCronAtom(atom, range));
1413
+ }
1414
+ function validateCronExpression(expr) {
1415
+ const trimmed = expr.trim();
1416
+ if (trimmed.length === 0) return {
1417
+ ok: false,
1418
+ reason: "empty cron expression"
1419
+ };
1420
+ if (trimmed.startsWith("@")) {
1421
+ if (CRON_ALIASES.has(trimmed)) return { ok: true };
1422
+ return {
1423
+ ok: false,
1424
+ reason: `unsupported alias '${trimmed}' (allowed: @hourly @daily @weekly @monthly @yearly @annually)`
1425
+ };
1426
+ }
1427
+ const fields = trimmed.split(/\s+/);
1428
+ if (fields.length !== 5) return {
1429
+ ok: false,
1430
+ reason: `expected 5 space-separated fields, got ${fields.length}`
1431
+ };
1432
+ for (let i = 0; i < 5; i++) {
1433
+ const field = fields[i];
1434
+ const range = FIELD_RANGES[i];
1435
+ if (!validateCronField(field, range)) return {
1436
+ ok: false,
1437
+ reason: `${FIELD_NAMES[i]} field '${field}' is invalid (range ${range.min}-${range.max})`
1438
+ };
1439
+ }
1440
+ return { ok: true };
1441
+ }
1442
+ function isValidIanaTimezone(tz) {
1443
+ try {
1444
+ new Intl.DateTimeFormat("en-US", { timeZone: tz });
1445
+ return true;
1446
+ } catch {
1447
+ return false;
1448
+ }
1449
+ }
1450
+ const kebabString = (label) => z.string().min(1, { message: `${label}: required` }).refine((s) => KEBAB_RE.test(s), { message: `${label}: must be kebab-case (lowercase letters, digits, hyphens)` });
1451
+ const cronString = z.string().min(1, { message: "cron: required" }).superRefine((expr, ctx) => {
1452
+ const result = validateCronExpression(expr);
1453
+ if (!result.ok) ctx.addIssue({
1454
+ code: z.ZodIssueCode.custom,
1455
+ message: `cron: '${expr}' is not a valid cron expression (${result.reason})`
1456
+ });
1457
+ });
1458
+ const timezoneString = z.string().min(1).refine(isValidIanaTimezone, { message: "timezone: not a valid IANA timezone" });
1459
+ const retryPolicySchema = z.object({
1460
+ max_attempts: z.number().int({ message: "retry_policy.max_attempts: must be an integer" }).min(1, { message: "retry_policy.max_attempts: must be ≥ 1" }).max(5, { message: "retry_policy.max_attempts: must be ≤ 5" }),
1461
+ backoff_seconds: z.number().int({ message: "retry_policy.backoff_seconds: must be an integer" }).min(0, { message: "retry_policy.backoff_seconds: must be ≥ 0" }).max(3600, { message: "retry_policy.backoff_seconds: must be ≤ 3600" })
1462
+ }).strict();
1463
+ const TOOL_VALUES = ["claude", "codex"];
1464
+ const INSTALL_MODE_VALUES = ["ui-handoff", "via-cron"];
1465
+ const scheduleEntrySchema = z.object({
1466
+ name: kebabString("name"),
1467
+ agent: kebabString("agent"),
1468
+ plan: kebabString("plan"),
1469
+ cron: cronString,
1470
+ tool: z.enum(TOOL_VALUES, { error: (issue) => {
1471
+ const base = `tool: must be one of ${TOOL_VALUES.map((v) => `'${v}'`).join(" | ")}`;
1472
+ return issue.code === "invalid_value" ? `${base} (got '${String(issue.input)}')` : base;
1473
+ } }),
1474
+ install_mode: z.enum(INSTALL_MODE_VALUES, { error: (issue) => {
1475
+ const base = `install_mode: must be one of ${INSTALL_MODE_VALUES.map((v) => `'${v}'`).join(" | ")}`;
1476
+ return issue.code === "invalid_value" ? `${base} (got '${String(issue.input)}')` : base;
1477
+ } }),
1478
+ timezone: timezoneString.optional(),
1479
+ max_duration_minutes: z.number().int({ message: "max_duration_minutes: must be an integer" }).min(1, { message: "max_duration_minutes: must be ≥ 1" }).max(1440, { message: "max_duration_minutes: must be ≤ 1440" }).optional(),
1480
+ hitl_routing: z.string().min(1).refine((p) => p.startsWith("roster/"), { message: "hitl_routing: must be a path under roster/" }).optional(),
1481
+ retry_policy: retryPolicySchema.optional()
1482
+ }).strict();
1483
+ const scheduleFileSchema = z.object({
1484
+ version: z.number().int({ message: "version: must be an integer" }).refine((n) => n === 1, { message: `version: unsupported schema version (expected 1)` }),
1485
+ schedules: z.array(scheduleEntrySchema)
1486
+ }).strict();
1487
+ function flattenZodErrors(error) {
1488
+ return error.issues.map((e) => ({
1489
+ path: e.path.length === 0 ? "<root>" : e.path.map((p) => String(p)).join("."),
1490
+ message: e.message
1491
+ }));
1492
+ }
1493
+ function findDuplicateNames(entries) {
1494
+ const seen = /* @__PURE__ */ new Map();
1495
+ const errors = [];
1496
+ entries.forEach((entry, i) => {
1497
+ const first = seen.get(entry.name);
1498
+ if (first !== void 0) errors.push({
1499
+ path: `schedules.${i}.name`,
1500
+ message: `name: duplicate of entry ${first} ('${entry.name}')`
1501
+ });
1502
+ else seen.set(entry.name, i);
1503
+ });
1504
+ return errors;
1505
+ }
1506
+ //#endregion
1507
+ //#region src/lib/schedule-validate.ts
1508
+ function rosterDir(cwd) {
1509
+ return join(cwd, "roster");
1510
+ }
1511
+ function findScheduleFiles(cwd) {
1512
+ const root = rosterDir(cwd);
1513
+ let topEntries;
1514
+ try {
1515
+ topEntries = readdirSync(root);
1516
+ } catch {
1517
+ return [];
1518
+ }
1519
+ const found = [];
1520
+ for (const entry of topEntries) {
1521
+ const fnDir = join(root, entry);
1522
+ let s;
1523
+ try {
1524
+ s = statSync(fnDir);
1525
+ } catch {
1526
+ continue;
1527
+ }
1528
+ if (!s.isDirectory()) continue;
1529
+ const candidate = join(fnDir, "schedules.yaml");
1530
+ try {
1531
+ statSync(candidate);
1532
+ found.push(candidate);
1533
+ } catch {}
1534
+ }
1535
+ return found.sort();
1536
+ }
1537
+ function readFile(path) {
1538
+ try {
1539
+ return {
1540
+ ok: true,
1541
+ content: readFileSync(path, "utf8")
1542
+ };
1543
+ } catch (err) {
1544
+ const e = err;
1545
+ return {
1546
+ ok: false,
1547
+ error: e.code ?? e.message ?? "unreadable"
1548
+ };
1549
+ }
1550
+ }
1551
+ function parseYaml(content) {
1552
+ try {
1553
+ return {
1554
+ ok: true,
1555
+ value: YAML.parse(content)
1556
+ };
1557
+ } catch (err) {
1558
+ return {
1559
+ ok: false,
1560
+ error: (err instanceof Error ? err.message : String(err)).replace(/\n+/g, " ").trim()
1561
+ };
1562
+ }
1563
+ }
1564
+ function validateOneFile(cwd, absPath) {
1565
+ const relativePath = relative(cwd, absPath);
1566
+ let stat;
1567
+ try {
1568
+ stat = statSync(absPath);
1569
+ } catch (err) {
1570
+ return {
1571
+ path: absPath,
1572
+ relativePath,
1573
+ status: "fail",
1574
+ entryCount: 0,
1575
+ errors: [{
1576
+ path: "<file>",
1577
+ message: `cannot stat: ${err.code ?? "unknown"}`
1578
+ }]
1579
+ };
1580
+ }
1581
+ if (!stat.isFile()) return {
1582
+ path: absPath,
1583
+ relativePath,
1584
+ status: "fail",
1585
+ entryCount: 0,
1586
+ errors: [{
1587
+ path: "<file>",
1588
+ message: stat.isDirectory() ? "expected file, found directory" : "expected regular file (got non-file entry)"
1589
+ }]
1590
+ };
1591
+ const read = readFile(absPath);
1592
+ if (!read.ok) return {
1593
+ path: absPath,
1594
+ relativePath,
1595
+ status: "fail",
1596
+ entryCount: 0,
1597
+ errors: [{
1598
+ path: "<file>",
1599
+ message: `cannot read file: ${read.error}`
1600
+ }]
1601
+ };
1602
+ const parsed = parseYaml(read.content);
1603
+ if (!parsed.ok) return {
1604
+ path: absPath,
1605
+ relativePath,
1606
+ status: "fail",
1607
+ entryCount: 0,
1608
+ errors: [{
1609
+ path: "<file>",
1610
+ message: `YAML parse error: ${parsed.error}`
1611
+ }]
1612
+ };
1613
+ if (parsed.value === null || parsed.value === void 0) return {
1614
+ path: absPath,
1615
+ relativePath,
1616
+ status: "fail",
1617
+ entryCount: 0,
1618
+ errors: [{
1619
+ path: "<file>",
1620
+ message: "file is empty or contains only null"
1621
+ }]
1622
+ };
1623
+ const schemaResult = scheduleFileSchema.safeParse(parsed.value);
1624
+ if (!schemaResult.success) return {
1625
+ path: absPath,
1626
+ relativePath,
1627
+ status: "fail",
1628
+ entryCount: 0,
1629
+ errors: flattenZodErrors(schemaResult.error)
1630
+ };
1631
+ const errors = findDuplicateNames(schemaResult.data.schedules);
1632
+ return {
1633
+ path: absPath,
1634
+ relativePath,
1635
+ status: errors.length === 0 ? "pass" : "fail",
1636
+ entryCount: schemaResult.data.schedules.length,
1637
+ errors
1638
+ };
1639
+ }
1640
+ function validateSchedulesInCwd(cwd) {
1641
+ const reports = findScheduleFiles(cwd).map((f) => validateOneFile(cwd, f));
1642
+ return {
1643
+ ok: reports.every((r) => r.status === "pass"),
1644
+ cwd,
1645
+ files: reports
1646
+ };
1647
+ }
1648
+ //#endregion
1649
+ //#region src/lib/platform.ts
1650
+ function getPlatform() {
1651
+ const override = process.env["ROSTER_PLATFORM"];
1652
+ if (override) return override;
1653
+ return process.platform;
1654
+ }
1655
+ function isWindows() {
1656
+ return getPlatform() === "win32";
1657
+ }
1658
+ //#endregion
1659
+ //#region src/commands/doctor.ts
1660
+ function tildify$2(path) {
1661
+ const home = homedir();
1662
+ return path.startsWith(home) ? "~" + path.slice(home.length) : path;
1663
+ }
1664
+ function icon(status) {
1665
+ if (status === "ok") return chalk.green("✓");
1666
+ if (status === "missing") return chalk.red("✗");
1667
+ return chalk.yellow("!");
1668
+ }
1669
+ function label(status) {
1670
+ if (status === "ok") return chalk.dim("OK");
1671
+ if (status === "missing") return chalk.red("MISSING");
1672
+ return chalk.yellow("STALE");
1673
+ }
1674
+ function computeSummary(results) {
1675
+ const s = {
1676
+ ok: 0,
1677
+ missing: 0,
1678
+ stale: 0
1679
+ };
1680
+ for (const r of results) for (const item of r.items) s[item.status]++;
1681
+ return s;
1682
+ }
1683
+ function workspaceIcon(status) {
1684
+ if (status === "ok") return chalk.green("✓");
1685
+ return chalk.red("✗");
1686
+ }
1687
+ function workspaceLabel(status) {
1688
+ switch (status) {
1689
+ case "ok": return chalk.dim("OK");
1690
+ case "missing": return chalk.red("MISSING");
1691
+ case "wrong-target": return chalk.red("WRONG TARGET");
1692
+ case "not-a-symlink": return chalk.yellow("NOT A SYMLINK");
1693
+ case "content-diverged": return chalk.yellow("CONTENT DIVERGED");
1694
+ case "is-directory": return chalk.red("IS DIRECTORY");
1695
+ case "unreadable": return chalk.red("UNREADABLE");
1696
+ }
1697
+ }
1698
+ function renderSchedulingSection(report) {
1699
+ if (report.files.length === 0) return [];
1700
+ const lines = [""];
1701
+ lines.push(`Scheduling ${tildify$2(report.cwd)}`);
1702
+ for (const file of report.files) if (file.status === "pass") {
1703
+ const entryWord = file.entryCount === 1 ? "entry" : "entries";
1704
+ lines.push(` ${chalk.green("✓")} ${file.relativePath} ${chalk.dim("OK")} ${chalk.dim(`(${file.entryCount} ${entryWord})`)}`);
1705
+ } else {
1706
+ lines.push(` ${chalk.red("✗")} ${file.relativePath} ${chalk.red("FAIL")}`);
1707
+ for (const e of file.errors) lines.push(` ${chalk.red("-")} ${chalk.dim(e.path + ":")} ${e.message}`);
1708
+ }
1709
+ return lines;
1710
+ }
1711
+ function renderWorkspaceSection(audit) {
1712
+ if (!audit.contextMdExists && audit.items.length === 0) return [];
1713
+ const lines = [""];
1714
+ lines.push(`Workspace ${tildify$2(audit.cwd)}`);
1715
+ if (audit.contextMdExists) lines.push(` ${chalk.green("✓")} CONTEXT.md ${chalk.dim("present")}`);
1716
+ else lines.push(` ${chalk.red("✗")} CONTEXT.md ${chalk.red("MISSING")}`);
1717
+ for (const item of audit.items) {
1718
+ const nameCol = item.name.padEnd(12);
1719
+ const detail = item.reason ? chalk.dim(` (${item.reason})`) : "";
1720
+ lines.push(` ${workspaceIcon(item.status)} ${nameCol} ${workspaceLabel(item.status)}${detail}`.trimEnd());
1721
+ }
1722
+ for (const w of audit.warnings) lines.push(` ${chalk.yellow("!")} ${w}`);
1723
+ return lines;
1724
+ }
1725
+ function computeWorkarounds(results) {
1726
+ const workarounds = [];
1727
+ if (isWindows() && results.some((r) => r.tool === "codex")) workarounds.push({
1728
+ id: "codex-windows-19399",
1729
+ toolKey: "codex",
1730
+ status: "active",
1731
+ summary: "Codex Windows TOML config ignored — runtime injection ACTIVE",
1732
+ reference: "https://github.com/openai/codex/issues/19399"
1733
+ });
1734
+ return workarounds;
1735
+ }
1736
+ function renderText$1(results, summary, workarounds) {
1737
+ const lines = [""];
1738
+ lines.push(chalk.bold("roster doctor"));
1739
+ for (const r of results) {
1740
+ lines.push("");
1741
+ lines.push(`${chalk.bold(r.toolName)} ${chalk.dim(tildify$2(r.configRoot))}`);
1742
+ for (const item of r.items) {
1743
+ const kindCol = chalk.dim(item.kind.padEnd(5));
1744
+ const nameCol = item.name.padEnd(22);
1745
+ const detail = item.status !== "ok" && item.reason ? chalk.dim(` (${item.reason})`) : "";
1746
+ lines.push(` ${icon(item.status)} ${kindCol} ${nameCol} ${label(item.status)}${detail}`.trimEnd());
1747
+ }
1748
+ for (const w of workarounds.filter((x) => x.toolKey === r.tool)) {
1749
+ lines.push(` ${chalk.yellow("!")} ${chalk.dim("w/a ")} ${w.id.padEnd(22)} ${chalk.yellow(w.summary)}`);
1750
+ lines.push(` ${chalk.dim(w.reference)}`);
1751
+ }
1752
+ }
1753
+ lines.push("");
1754
+ if (summary.missing === 0 && summary.stale === 0) lines.push(chalk.green("All installed skills and agents are up to date."));
1755
+ else {
1756
+ const bits = [];
1757
+ if (summary.missing > 0) bits.push(`${summary.missing} missing`);
1758
+ if (summary.stale > 0) bits.push(`${summary.stale} stale`);
1759
+ const toolWord = results.length === 1 ? "tool" : "tools";
1760
+ lines.push(chalk.yellow(`Summary: ${bits.join(", ")} across ${results.length} ${toolWord}.`));
1761
+ lines.push(`${chalk.dim("Run ")}${chalk.bold("roster install")}${chalk.dim(" to repair.")}`);
1762
+ }
1763
+ return lines;
1764
+ }
1765
+ function executeDoctor(opts) {
1766
+ const detected = detectTools();
1767
+ const sources = {
1768
+ skills: join(ROSTER_ROOT, "skills"),
1769
+ agents: join(ROSTER_ROOT, "agents")
1770
+ };
1771
+ const workspace = auditWorkspace(opts.cwd);
1772
+ const scheduling = validateSchedulesInCwd(opts.cwd);
1773
+ if (detected.length === 0) {
1774
+ if (opts.json) {
1775
+ const payload = {
1776
+ ok: scheduling.ok,
1777
+ rosterVersion: getPackageVersion(),
1778
+ tools: [],
1779
+ summary: {
1780
+ ok: 0,
1781
+ missing: 0,
1782
+ stale: 0
1783
+ },
1784
+ workspace,
1785
+ scheduling,
1786
+ workarounds: [],
1787
+ note: "no tools detected"
1788
+ };
1789
+ console.log(JSON.stringify(payload, null, 2));
1790
+ }
1791
+ return 3;
1792
+ }
1793
+ const results = detected.map((t) => auditTool(t, sources));
1794
+ const summary = computeSummary(results);
1795
+ const workarounds = computeWorkarounds(results);
1796
+ const allOk = results.every((r) => r.ok) && workspace.ok && scheduling.ok;
1797
+ if (opts.json) {
1798
+ const payload = {
1799
+ ok: allOk,
1800
+ rosterVersion: getPackageVersion(),
1801
+ tools: results,
1802
+ summary,
1803
+ workspace,
1804
+ scheduling,
1805
+ workarounds
1806
+ };
1807
+ console.log(JSON.stringify(payload, null, 2));
1808
+ } else if (!opts.silent) {
1809
+ for (const line of renderText$1(results, summary, workarounds)) console.log(line);
1810
+ for (const line of renderWorkspaceSection(workspace)) console.log(line);
1811
+ for (const line of renderSchedulingSection(scheduling)) console.log(line);
1812
+ }
1813
+ return allOk ? 0 : 1;
1814
+ }
1815
+ //#endregion
1816
+ //#region src/commands/schedule.ts
1817
+ function tildify$1(path) {
1818
+ const home = homedir();
1819
+ return path.startsWith(home) ? "~" + path.slice(home.length) : path;
1820
+ }
1821
+ function countTotalErrors(report) {
1822
+ return report.files.reduce((acc, f) => acc + f.errors.length, 0);
1823
+ }
1824
+ function renderText(report) {
1825
+ const lines = [""];
1826
+ lines.push(chalk.bold("roster schedule validate"));
1827
+ lines.push(chalk.dim(`cwd: ${tildify$1(report.cwd)}`));
1828
+ if (report.files.length === 0) {
1829
+ lines.push("");
1830
+ lines.push(chalk.dim("No roster/<function>/schedules.yaml files found."));
1831
+ return lines;
1832
+ }
1833
+ for (const file of report.files) {
1834
+ lines.push("");
1835
+ if (file.status === "pass") {
1836
+ const entryWord = file.entryCount === 1 ? "entry" : "entries";
1837
+ lines.push(`${chalk.green("✓")} ${file.relativePath} ${chalk.dim("PASS")} ${chalk.dim(`(${file.entryCount} ${entryWord})`)}`);
1838
+ } else {
1839
+ lines.push(`${chalk.red("✗")} ${file.relativePath} ${chalk.red("FAIL")}`);
1840
+ for (const e of file.errors) lines.push(` ${chalk.red("-")} ${chalk.dim(e.path + ":")} ${e.message}`);
1841
+ }
1842
+ }
1843
+ lines.push("");
1844
+ if (report.ok) {
1845
+ const totalEntries = report.files.reduce((acc, f) => acc + f.entryCount, 0);
1846
+ const fileWord = report.files.length === 1 ? "file" : "files";
1847
+ const entryWord = totalEntries === 1 ? "entry" : "entries";
1848
+ lines.push(chalk.green(`All schedules valid (${totalEntries} ${entryWord} across ${report.files.length} ${fileWord}).`));
1849
+ } else {
1850
+ const errs = countTotalErrors(report);
1851
+ const fileWord = report.files.length === 1 ? "file" : "files";
1852
+ lines.push(chalk.yellow(`Summary: ${errs} error${errs === 1 ? "" : "s"} across ${report.files.length} ${fileWord}.`));
1853
+ lines.push(`${chalk.dim("Run ")}${chalk.bold("roster schedule validate --json")}${chalk.dim(" for machine-readable output.")}`);
1854
+ }
1855
+ return lines;
1856
+ }
1857
+ function executeScheduleValidate(opts) {
1858
+ const report = validateSchedulesInCwd(opts.cwd);
1859
+ if (opts.json) console.log(JSON.stringify(report, null, 2));
1860
+ else if (!opts.silent) for (const line of renderText(report)) console.log(line);
1861
+ return report.ok ? 0 : 1;
1862
+ }
1863
+ //#endregion
1864
+ //#region src/bin/roster.ts
1865
+ const SUBCOMMANDS = new Set([
1866
+ "install",
1867
+ "init",
1868
+ "doctor",
1869
+ "schedule"
1870
+ ]);
1871
+ function tildify(path) {
1872
+ const home = homedir();
1873
+ return path.startsWith(home) ? "~" + path.slice(home.length) : path;
1874
+ }
1875
+ function printBanner(version) {
1876
+ console.log();
1877
+ console.log(`${chalk.bold.cyan("roster")}${chalk.dim(` v${version}`)}`);
1878
+ console.log(chalk.dim("Multi-agent workspace scaffolder for Claude Code, Codex CLI, and Gemini."));
1879
+ console.log();
1880
+ }
1881
+ function printHelp(version) {
1882
+ printBanner(version);
1883
+ const lines = [
1884
+ chalk.bold("Usage:"),
1885
+ ` roster ${chalk.dim("Interactive install (alias of `roster install`)")}`,
1886
+ ` roster install ${chalk.dim("Copy skills + agents into detected AI tool config dirs")}`,
1887
+ ` roster init [name] ${chalk.dim("Scaffold a multi-agent workspace in the current dir")}`,
1888
+ ` roster doctor ${chalk.dim("Audit installed skills + agents per AI tool")}`,
1889
+ ` roster schedule validate ${chalk.dim("Validate roster/<function>/schedules.yaml files")}`,
1890
+ "",
1891
+ chalk.bold("Flags:"),
1892
+ ` -h, --help ${chalk.dim("Show this help")}`,
1893
+ ` -v, --version ${chalk.dim("Print version and exit")}`,
1894
+ ` --silent ${chalk.dim("Suppress non-error output (install)")}`,
1895
+ ` --verbose ${chalk.dim("Log each file path written (install)")}`,
1896
+ ` --all ${chalk.dim("Install to every detected tool (install)")}`,
1897
+ ` --tool <name> ${chalk.dim("Install to a single tool: claude | codex | gemini")}`,
1898
+ ` --migrate ${chalk.dim("Upgrade pre-CONTEXT.md workspace, preserving CLAUDE.md content (init)")}`,
1899
+ ` --json ${chalk.dim("Emit machine-readable JSON (doctor, schedule validate)")}`,
1900
+ ` --cwd <dir> ${chalk.dim("Run schedule validate against a different cwd")}`,
1901
+ ` --debug ${chalk.dim("Print full stack trace on error (global)")}`,
1902
+ "",
1903
+ chalk.bold("Exit codes:"),
1904
+ ` 0 ${chalk.dim("success")}`,
1905
+ ` 1 ${chalk.dim("generic error")}`,
1906
+ ` 2 ${chalk.dim("user cancelled")}`,
1907
+ ` 3 ${chalk.dim("no AI tool detected")}`,
1908
+ "",
1909
+ chalk.dim("Docs: https://github.com/firatcand/roster")
1910
+ ];
1911
+ console.log(lines.join("\n"));
1912
+ console.log();
1913
+ }
1914
+ function unknownCommandError(command) {
1915
+ return new RosterError({
1916
+ header: `${chalk.red.bold("roster:")} unknown command ${chalk.yellow(`'${command}'`)}`,
1917
+ body: "",
1918
+ remedy: ` Run ${chalk.bold("roster --help")} to see available commands.`,
1919
+ exitCode: 1
1920
+ });
1921
+ }
1922
+ function toolHints(tools) {
1923
+ return tools.map((t) => ({
1924
+ name: t.name,
1925
+ installLink: t.installLink
1926
+ }));
1927
+ }
1928
+ function summarizeInstall(tool, result) {
1929
+ const skillsLine = `${result.skillsCount} skills → ${tildify(result.skillsTarget)}`;
1930
+ const agentsLine = result.agentsTarget ? `${result.agentsCount} agents → ${tildify(result.agentsTarget)}` : `${result.agentsCount} agents → (n/a)`;
1931
+ return `${chalk.green("✓")} ${chalk.bold(tool.name)} — ${skillsLine}, ${agentsLine}`;
1932
+ }
1933
+ async function promptForTools(detected) {
1934
+ if (detected.length === 1) return detected;
1935
+ const { checkbox, confirm } = await import("@inquirer/prompts");
1936
+ let selectedKeys;
1937
+ try {
1938
+ selectedKeys = await checkbox({
1939
+ message: "Install roster into which AI tools?",
1940
+ choices: detected.map((t) => ({
1941
+ name: t.name,
1942
+ value: t.key,
1943
+ checked: true
1944
+ }))
1945
+ });
1946
+ } catch {
1947
+ return null;
1948
+ }
1949
+ if (selectedKeys.length === 0) {
1950
+ let exitAnyway;
1951
+ try {
1952
+ exitAnyway = await confirm({
1953
+ message: "No tools selected. Exit without installing?",
1954
+ default: true
1955
+ });
1956
+ } catch {
1957
+ return null;
1958
+ }
1959
+ if (exitAnyway) return null;
1960
+ return promptForTools(detected);
1961
+ }
1962
+ return detected.filter((t) => selectedKeys.includes(t.key));
1963
+ }
1964
+ async function runInstall(args) {
1965
+ const parsed = parseInstallArgs(args);
1966
+ if (parsed.kind === "err") throw new RosterError({
1967
+ header: `${chalk.red.bold("roster:")} ${parsed.message}`,
1968
+ body: "",
1969
+ remedy: ` Run ${chalk.bold("roster --help")} for usage.`,
1970
+ exitCode: 1
1971
+ });
1972
+ const { silent, verbose, target } = parsed;
1973
+ const version = getPackageVersion();
1974
+ if (!silent) printBanner(version);
1975
+ const detected = detectTools();
1976
+ if (detected.length === 0) throw noToolsError(toolHints(allTools()));
1977
+ let targetTools;
1978
+ if (target.mode === "all") targetTools = detected;
1979
+ else if (target.mode === "tool") {
1980
+ const match = detected.find((t) => t.key === target.key);
1981
+ if (!match) throw new RosterError({
1982
+ header: `${chalk.red.bold("roster:")} ${target.key} not detected on this machine`,
1983
+ body: ` --tool ${target.key} was requested, but no ${target.key} install was found.`,
1984
+ remedy: ` Install it first, or omit ${chalk.bold("--tool")} to install to all detected tools.`,
1985
+ exitCode: 3
1986
+ });
1987
+ targetTools = [match];
1988
+ } else targetTools = await promptForTools(detected);
1989
+ if (targetTools === null) throw userCancelledInstall();
1990
+ const skillsSrc = join(ROSTER_ROOT, "skills");
1991
+ const agentsSrc = join(ROSTER_ROOT, "agents");
1992
+ const confirmFn = target.mode !== "interactive" ? async () => false : void 0;
1993
+ for (const tool of targetTools) {
1994
+ const result = await installToTool(tool, {
1995
+ skills: skillsSrc,
1996
+ agents: agentsSrc,
1997
+ silent: !verbose,
1998
+ ...confirmFn ? { confirm: confirmFn } : {}
1999
+ });
2000
+ if (!silent) console.log(summarizeInstall(tool, result));
2001
+ }
2002
+ if (!silent) {
2003
+ console.log();
2004
+ console.log(`${chalk.dim("Next: ")}${chalk.bold("roster init")}${chalk.dim(" to scaffold a workspace.")}`);
2005
+ }
2006
+ return 0;
2007
+ }
2008
+ async function runInit(args) {
2009
+ const silent = args.includes("--silent");
2010
+ const force = args.includes("--force");
2011
+ const migrate = args.includes("--migrate");
2012
+ const noGit = args.includes("--no-git") || args.includes("--skip-git");
2013
+ const name = args.find((a) => !a.startsWith("-"));
2014
+ if (!silent) printBanner(getPackageVersion());
2015
+ if ((await executeInit({
2016
+ cwd: process.cwd(),
2017
+ name,
2018
+ silent,
2019
+ force,
2020
+ migrate,
2021
+ noGit
2022
+ })).status === "cancelled") throw userCancelledInit();
2023
+ return 0;
2024
+ }
2025
+ function runSchedule(args) {
2026
+ const parsed = parseScheduleArgs(args);
2027
+ if (parsed.kind === "err") throw new RosterError({
2028
+ header: `${chalk.red.bold("roster:")} ${parsed.message}`,
2029
+ body: "",
2030
+ remedy: ` Run ${chalk.bold("roster --help")} for usage.`,
2031
+ exitCode: 1
2032
+ });
2033
+ if (parsed.subcommand === "validate") return executeScheduleValidate({
2034
+ cwd: parsed.cwd ?? process.cwd(),
2035
+ json: parsed.json,
2036
+ silent: parsed.silent
2037
+ });
2038
+ throw new RosterError({
2039
+ header: `${chalk.red.bold("roster:")} schedule subcommand not implemented`,
2040
+ body: "",
2041
+ remedy: ` Run ${chalk.bold("roster --help")} for usage.`,
2042
+ exitCode: 1
2043
+ });
2044
+ }
2045
+ function runDoctor(args) {
2046
+ const parsed = parseDoctorArgs(args);
2047
+ if (parsed.kind === "err") throw new RosterError({
2048
+ header: `${chalk.red.bold("roster:")} ${parsed.message}`,
2049
+ body: "",
2050
+ remedy: ` Run ${chalk.bold("roster --help")} for usage.`,
2051
+ exitCode: 1
2052
+ });
2053
+ const code = executeDoctor({
2054
+ json: parsed.json,
2055
+ silent: parsed.silent,
2056
+ cwd: process.cwd()
2057
+ });
2058
+ if (code === 3 && !parsed.json) throw noToolsError(toolHints(allTools()));
2059
+ return code;
2060
+ }
2061
+ function isSubcommand(value) {
2062
+ return SUBCOMMANDS.has(value);
2063
+ }
2064
+ const rawArgs = process.argv.slice(2);
2065
+ const debugMode = rawArgs.includes("--debug");
2066
+ async function main() {
2067
+ const version = getPackageVersion();
2068
+ const args = debugMode ? rawArgs.filter((a) => a !== "--debug") : rawArgs;
2069
+ if (args.includes("--help") || args.includes("-h")) {
2070
+ printHelp(version);
2071
+ return 0;
2072
+ }
2073
+ if (args.includes("--version") || args.includes("-v")) {
2074
+ console.log(version);
2075
+ return 0;
2076
+ }
2077
+ const [first, ...rest] = args;
2078
+ if (first === void 0) return runInstall(rest);
2079
+ if (isSubcommand(first)) {
2080
+ if (first === "install") return runInstall(rest);
2081
+ if (first === "init") return await runInit(rest);
2082
+ if (first === "doctor") return runDoctor(rest);
2083
+ if (first === "schedule") return runSchedule(rest);
2084
+ }
2085
+ throw unknownCommandError(first);
2086
+ }
2087
+ main().then((code) => process.exit(code)).catch((err) => {
2088
+ const rosterErr = isRosterError(err) ? err : unexpectedError(err);
2089
+ renderError(rosterErr, { debug: debugMode });
2090
+ process.exit(rosterErr.exitCode);
2091
+ });
2092
+ //#endregion
2093
+ export {};