tunestra 0.4.0 → 0.5.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.
package/README.md CHANGED
@@ -70,9 +70,26 @@ the manifest only holds metadata and overrides.
70
70
  rules/<key>.md # rules, one topic per file
71
71
  skills/<key>/SKILL.md # skills (Agent Skills format)
72
72
  commands/<key>.md # slash commands
73
+ agents/<key>.md # subagents
73
74
  mcp/<key>.yaml # MCP servers
74
75
  ```
75
76
 
77
+ A subagent is a markdown file with frontmatter; the text after it is the instructions:
78
+
79
+ ```md
80
+ ---
81
+ description: Reviews a diff for bugs and missing tests # required: when to use it
82
+ model: # optional: the model for each agent
83
+ claude: sonnet # otherwise the agent uses the model of the main session
84
+ codex: gpt-5.5
85
+ effort: medium # optional: low | medium | high | xhigh
86
+ readOnly: true # optional: the subagent must not change files
87
+ ---
88
+ Review the diff. Report each finding with a file and a line.
89
+ ```
90
+
91
+ The file name is the name of the subagent: lowercase letters, digits and single hyphens.
92
+
76
93
  ### Manifest: `tunestra.preset.yaml`
77
94
 
78
95
  ```yaml
@@ -143,6 +160,7 @@ chain once, before both of them.
143
160
  | Level instructions (`AGENTS.md`) | `instructions@<preset name>` | never collides: every level keeps its own | `exclude.rules` |
144
161
  | Skill | `name` from `SKILL.md`, otherwise the folder name | replaces it | `exclude.skills` |
145
162
  | Command | file name without `.md` | replaces it | `exclude.commands` |
163
+ | Subagent | file name without `.md` | replaces it | `exclude.subagents` |
146
164
  | MCP server | file name without `.yaml` | replaces it | `exclude.mcp` |
147
165
  | Environment variable | name in `env` | replaces it | `exclude.env` |
148
166
  | Permission rule | the rule, like `Bash(npm test *)` | lists are joined: every level adds its rules | `exclude.permissions`, only for `allow` and `ask` |
@@ -166,7 +184,9 @@ are merged by key (the specific value wins); `command`, `args` and `url` are rep
166
184
  - an `env` value refers to another variable, like `${TOKEN}`: agents do not expand it in
167
185
  their settings, so write the value itself and keep secrets out of presets;
168
186
  - `env` sets a variable that Claude Code ignores in project settings (see below);
169
- - `exclude.permissions` tries to remove a `deny` rule.
187
+ - `exclude.permissions` tries to remove a `deny` rule;
188
+ - a subagent has no `description` or no instructions, an unknown field, or a file name that
189
+ cannot be a subagent name.
170
190
 
171
191
  All problems are reported at once, in the order of the files, so you can fix them in one pass.
172
192
 
@@ -176,7 +196,7 @@ For every agent:
176
196
 
177
197
  | Path | Content | Committed |
178
198
  |---|---|---|
179
- | `AGENTS.md` | Index: hierarchy, rules, skills, commands, MCP servers | yes |
199
+ | `AGENTS.md` | Index: hierarchy, rules, skills, commands, MCP servers, subagents | yes |
180
200
  | `.agents/rules/<NN>-<level>-<key>.md` | Rule texts, ordered from specific to general | no |
181
201
  | `.agents/skills/<key>/` | Skills | no |
182
202
  | `.tunestra/state/` | What tunestra generated last time | no |
@@ -190,6 +210,7 @@ For every agent:
190
210
  | `.claude/rules/tunestra/<NN>-<level>-<key>.md` | Copies of the rules; Claude Code loads them in every session | no |
191
211
  | `.claude/skills/<key>` | Symlinks to `.agents/skills/<key>` | no |
192
212
  | `.claude/commands/<key>.md` | Commands | no |
213
+ | `.claude/agents/<key>.md` | Subagents | no |
193
214
  | `.mcp.json` | MCP servers | no |
194
215
  | `.claude/settings.json` | `env` and `permissions`, when presets set them | yes |
195
216
 
@@ -207,6 +228,9 @@ the ones that choose its own directories (`HOME`, `XDG_*`, `CLAUDE_CONFIG_DIR`).
207
228
  on them, because they would not work; set them in managed settings, `~/.claude/settings.json`
208
229
  or your shell. Values that turn telemetry off, like `OTEL_LOGS_EXPORTER: none`, are allowed.
209
230
 
231
+ A read-only subagent of Claude Code loses the tools that change files: `Write`, `Edit`,
232
+ `NotebookEdit` and `Bash`. It keeps the tools that read and search, and MCP tools.
233
+
210
234
  ### Codex
211
235
 
212
236
  Codex reads `AGENTS.md` and `.agents/skills/` by itself. It does not follow the links in
@@ -217,6 +241,7 @@ Codex reads `AGENTS.md` and `.agents/skills/` by itself. It does not follow the
217
241
  | `.codex/config.toml` | `developer_instructions` with every rule, from specific to general, `[shell_environment_policy] set` with `env`, and `[mcp_servers]` | no |
218
242
  | `.codex/rules/tunestra.rules` | `deny` and `ask` rules for shell commands as `prefix_rule`: `forbidden` and `prompt` | no |
219
243
  | `.agents/skills/<command>/` | Each command as a skill that runs only when you call it: `$release-notes` | no |
244
+ | `.codex/agents/<key>.toml` | Subagents | no |
220
245
 
221
246
  Codex reads `.codex/config.toml` only in a trusted project: trust the project when Codex asks.
222
247
  Codex gets `env` as the environment of the commands it runs; it does not change how Codex
@@ -232,6 +257,11 @@ Codex has no `type` for MCP servers and does not support SSE: an SSE server is s
232
257
  note, and `headers` become `http_headers`. tunestra owns the whole `.codex/config.toml`; keep
233
258
  your personal Codex settings in `~/.codex/config.toml`.
234
259
 
260
+ A Codex subagent gets the sandbox, the permissions and the MCP servers of the main session; its
261
+ file can change only the model and the instructions. So Codex cannot make a subagent
262
+ read-only: tunestra turns off its shell (`features.shell_tool = false`) and asks it not to
263
+ edit files in the instructions, and setup says so in a note.
264
+
235
265
  `<NN>` orders rules from specific to general: `10` project, `20` team, `30` domain, `40` base.
236
266
  Level instructions get the key `instructions`, and the preset name when it differs from the
237
267
  level: `20-team-instructions.md`, but `20-team-instructions-frontend.md` for a `team` preset
package/dist/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as VERSION } from "./src-Bh7d7ERC.mjs";
2
+ import { t as VERSION } from "./src-CZK2Ixbt.mjs";
3
3
  import { Command } from "@commander-js/extra-typings";
4
4
  import pc from "picocolors";
5
5
  import { copyFile, lstat, mkdir, readFile, readdir, readlink, rename, rm, rmdir, stat, symlink, unlink, writeFile } from "node:fs/promises";
@@ -350,6 +350,7 @@ const excludeSchema = z.strictObject({
350
350
  skills: keys.optional(),
351
351
  commands: keys.optional(),
352
352
  mcp: keys.optional(),
353
+ subagents: keys.optional(),
353
354
  env: keys.optional(),
354
355
  /** Inherited `allow` and `ask` rules; a `deny` rule cannot be removed. */
355
356
  permissions: keys.optional()
@@ -463,6 +464,15 @@ async function mapSettled(items, map) {
463
464
  throw buildResultsError(results);
464
465
  }
465
466
  //#endregion
467
+ //#region src/shared/constants/subagents.ts
468
+ /** Reasoning effort of a subagent: the levels that both Claude Code and Codex accept. */
469
+ const SubagentEffort = {
470
+ Low: "low",
471
+ Medium: "medium",
472
+ High: "high",
473
+ ExtraHigh: "xhigh"
474
+ };
475
+ //#endregion
466
476
  //#region src/shared/schemas/frontmatter.ts
467
477
  const ruleFrontmatterSchema = z.looseObject({ description: z.string().optional() });
468
478
  const skillFrontmatterSchema = z.looseObject({
@@ -470,6 +480,15 @@ const skillFrontmatterSchema = z.looseObject({
470
480
  description: z.string().min(1)
471
481
  });
472
482
  const commandFrontmatterSchema = z.looseObject({ description: z.string().optional() });
483
+ /** The model names differ between agents, so a subagent sets one for each agent. */
484
+ const subagentModelSchema = z.partialRecord(z.enum(Agent), z.string().min(1));
485
+ /** tunestra writes the files of the agents itself, so it knows every field here. */
486
+ const subagentFrontmatterSchema = z.strictObject({
487
+ description: z.string().min(1),
488
+ model: subagentModelSchema.default({}),
489
+ effort: z.enum(SubagentEffort).optional(),
490
+ readOnly: z.boolean().default(false)
491
+ });
473
492
  //#endregion
474
493
  //#region src/presets/frontmatter.ts
475
494
  /**
@@ -602,6 +621,28 @@ function findDuplicateSkills(skills, dir) {
602
621
  return problems;
603
622
  }
604
623
  //#endregion
624
+ //#region src/presets/artifacts/subagents.ts
625
+ /** Subagents live in `agents/`, like in Claude Code and Codex. */
626
+ async function loadSubagents(dir) {
627
+ return mapSettled(await listFiles(dir, "agents/*.md"), (source) => loadSubagent(dir, source));
628
+ }
629
+ async function loadSubagent(dir, source) {
630
+ const file = join(dir, source);
631
+ const { data, body } = parseFrontmatter(await readFile(file, "utf8"), file);
632
+ const frontmatter = validate(data, subagentFrontmatterSchema, `frontmatter in ${file}`);
633
+ const key = validate(basename(source, ".md"), skillName, `subagent file ${source}`);
634
+ if (body.trim().length > 0 === false) throw new TunestraError(`${file} has no instructions: write them after the frontmatter`);
635
+ return {
636
+ key,
637
+ source,
638
+ description: frontmatter.description,
639
+ model: frontmatter.model,
640
+ effort: frontmatter.effort,
641
+ readOnly: frontmatter.readOnly,
642
+ body
643
+ };
644
+ }
645
+ //#endregion
605
646
  //#region src/presets/artifacts/load-artifacts.ts
606
647
  /** Loads every kind of artifact at the same time and reports the problems of all of them. */
607
648
  async function loadArtifacts(dir, presetName) {
@@ -609,19 +650,22 @@ async function loadArtifacts(dir, presetName) {
609
650
  const skillsLoading = loadSkills(dir);
610
651
  const commandsLoading = loadCommands(dir);
611
652
  const mcpLoading = loadMcpServers(dir);
653
+ const subagentsLoading = loadSubagents(dir);
612
654
  const results = await Promise.allSettled([
613
655
  rulesLoading,
614
656
  skillsLoading,
615
657
  commandsLoading,
616
- mcpLoading
658
+ mcpLoading,
659
+ subagentsLoading
617
660
  ]);
618
661
  if (isEveryFulfilled(results) === false) throw buildResultsError(results);
619
- const [rulesResult, skillsResult, commandsResult, mcpResult] = results;
662
+ const [rulesResult, skillsResult, commandsResult, mcpResult, subagentsResult] = results;
620
663
  return {
621
664
  rules: rulesResult.value,
622
665
  skills: skillsResult.value,
623
666
  commands: commandsResult.value,
624
- mcp: mcpResult.value
667
+ mcp: mcpResult.value,
668
+ subagents: subagentsResult.value
625
669
  };
626
670
  }
627
671
  //#endregion
@@ -1123,11 +1167,33 @@ function buildPermissionLists(permissions) {
1123
1167
  return lists;
1124
1168
  }
1125
1169
  //#endregion
1170
+ //#region src/agents/claude/subagent-md.ts
1171
+ /** Tools that change files; a read-only subagent keeps the tools that read and search. */
1172
+ const WRITING_TOOLS = [
1173
+ "Write",
1174
+ "Edit",
1175
+ "NotebookEdit",
1176
+ "Bash"
1177
+ ];
1178
+ /** `.claude/agents/<name>.md`: the frontmatter of Claude Code and the instructions. */
1179
+ function renderSubagentMd(subagent) {
1180
+ const { key, description, model, effort, readOnly, body } = subagent;
1181
+ const frontmatter = {
1182
+ name: key,
1183
+ description
1184
+ };
1185
+ if (typeof model.claude === "string") frontmatter.model = model.claude;
1186
+ if (typeof effort === "string") frontmatter.effort = effort;
1187
+ if (readOnly) frontmatter.disallowedTools = WRITING_TOOLS;
1188
+ return `---\n${stringify(frontmatter)}---\n${body}`;
1189
+ }
1190
+ //#endregion
1126
1191
  //#region src/agents/claude/claude-adapter.ts
1127
1192
  const CLAUDE_FILE = "CLAUDE.md";
1128
1193
  const RULES_DIR = ".claude/rules/tunestra";
1129
1194
  const SKILLS_DIR = ".claude/skills";
1130
1195
  const COMMANDS_DIR = ".claude/commands";
1196
+ const SUBAGENTS_DIR$1 = ".claude/agents";
1131
1197
  const MCP_FILE = ".mcp.json";
1132
1198
  const MCP_SOURCE = "the mcp/ folders of tunestra.yaml and its presets";
1133
1199
  const SETTINGS_FILE = ".claude/settings.json";
@@ -1156,11 +1222,15 @@ function planClaude(context) {
1156
1222
  const source = describeArtifactSource(projectDir, command.origin, command.artifact.source);
1157
1223
  return createFileOutput(path, command.artifact.content, source);
1158
1224
  });
1225
+ const subagentFiles = resolution.subagents.map((subagent) => {
1226
+ return createFileOutput(`${SUBAGENTS_DIR$1}/${subagent.artifact.key}.md`, renderSubagentMd(subagent.artifact), describeArtifactSource(projectDir, subagent.origin, subagent.artifact.source));
1227
+ });
1159
1228
  const outputs = [
1160
1229
  claudeFile,
1161
1230
  ...ruleCopies,
1162
1231
  ...skillLinks,
1163
- ...commandFiles
1232
+ ...commandFiles,
1233
+ ...subagentFiles
1164
1234
  ];
1165
1235
  if (resolution.mcp.length > 0) {
1166
1236
  const mcpJson = renderMcpJson(resolution.mcp);
@@ -1179,6 +1249,7 @@ function planClaude(context) {
1179
1249
  `${RULES_DIR}/`,
1180
1250
  `${SKILLS_DIR}/`,
1181
1251
  `${COMMANDS_DIR}/`,
1252
+ `${SUBAGENTS_DIR$1}/`,
1182
1253
  MCP_FILE
1183
1254
  ],
1184
1255
  protectedPaths,
@@ -1247,7 +1318,7 @@ function planCommandSkill(projectDir, command) {
1247
1318
  return [createFileOutput(skillPath, skill, source), createFileOutput(policyPath, OPENAI_YAML, source)];
1248
1319
  }
1249
1320
  //#endregion
1250
- //#region src/agents/codex/config-toml.ts
1321
+ //#region src/agents/codex/toml-text.ts
1251
1322
  /** Starts and ends a multi-line literal string, where TOML escapes nothing. */
1252
1323
  const LITERAL_DELIMITER = "'''";
1253
1324
  /**
@@ -1259,6 +1330,18 @@ const LITERAL_DELIMITER = "'''";
1259
1330
  */
1260
1331
  const CONTROL_CHARACTER_PATTERN = /[\u0000-\u0008\u000B-\u001F\u007F]/;
1261
1332
  /**
1333
+ * A key with a long text, like `developer_instructions`. The text is a literal string, so
1334
+ * that people can read it; a text that a literal string cannot hold is escaped.
1335
+ */
1336
+ function renderTomlText(key, text) {
1337
+ const hasDelimiter = text.includes(LITERAL_DELIMITER);
1338
+ const hasControlCharacter = CONTROL_CHARACTER_PATTERN.test(text);
1339
+ if (hasDelimiter || hasControlCharacter) return stringify$1({ [key]: text });
1340
+ return `${key} = ${LITERAL_DELIMITER}\n${text}${LITERAL_DELIMITER}`;
1341
+ }
1342
+ //#endregion
1343
+ //#region src/agents/codex/config-toml.ts
1344
+ /**
1262
1345
  * `.codex/config.toml`: the rules as `developer_instructions`, which Codex adds to every
1263
1346
  * session, the environment of the commands that Codex runs, and the MCP servers. The rules
1264
1347
  * are a literal string, so that people can read them.
@@ -1266,7 +1349,7 @@ const CONTROL_CHARACTER_PATTERN = /[\u0000-\u0008\u000B-\u001F\u007F]/;
1266
1349
  function renderConfigToml(config) {
1267
1350
  const parts = [GENERATED_HASH_NOTICE];
1268
1351
  if (typeof config.instructions === "string") {
1269
- const line = renderInstructions$1(config.instructions);
1352
+ const line = renderTomlText("developer_instructions", config.instructions);
1270
1353
  parts.push(line);
1271
1354
  }
1272
1355
  const tables = {};
@@ -1286,12 +1369,6 @@ function renderConfigToml(config) {
1286
1369
  }
1287
1370
  return `${parts.join("\n\n")}\n`;
1288
1371
  }
1289
- function renderInstructions$1(text) {
1290
- const hasDelimiter = text.includes(LITERAL_DELIMITER);
1291
- const hasControlCharacter = CONTROL_CHARACTER_PATTERN.test(text);
1292
- if (hasDelimiter || hasControlCharacter) return stringify$1({ developer_instructions: text });
1293
- return `developer_instructions = ${LITERAL_DELIMITER}\n${text}${LITERAL_DELIMITER}`;
1294
- }
1295
1372
  /** Codex picks the transport by `command` or `url` and has no `type`. */
1296
1373
  function convertMcpConfig(config) {
1297
1374
  const { type: _type, headers, ...server } = config;
@@ -1366,18 +1443,48 @@ function renderExecRule(rule) {
1366
1443
  ].join("\n");
1367
1444
  }
1368
1445
  //#endregion
1446
+ //#region src/agents/codex/subagent-toml.ts
1447
+ /**
1448
+ * Codex has no read-only mode for a subagent: it can turn off the shell, but the subagent
1449
+ * still edits files with patches. So the instructions ask it not to.
1450
+ */
1451
+ const READ_ONLY_INSTRUCTION = "Do not create, edit or delete files.";
1452
+ /**
1453
+ * `.codex/agents/<name>.toml`. Codex gives a subagent the sandbox, the permissions and the
1454
+ * MCP servers of the main session, so the file has only the model and the instructions.
1455
+ */
1456
+ function renderSubagentToml(subagent) {
1457
+ const { key, description, model, effort, readOnly, body } = subagent;
1458
+ const fields = {
1459
+ name: key,
1460
+ description
1461
+ };
1462
+ if (typeof model.codex === "string") fields.model = model.codex;
1463
+ if (typeof effort === "string") fields.model_reasoning_effort = effort;
1464
+ const instructions = readOnly ? `${READ_ONLY_INSTRUCTION}\n\n${body}` : body;
1465
+ const parts = [
1466
+ GENERATED_HASH_NOTICE,
1467
+ stringify$1(fields).trimEnd(),
1468
+ renderTomlText("developer_instructions", instructions)
1469
+ ];
1470
+ if (readOnly) parts.push(stringify$1({ features: { shell_tool: false } }).trimEnd());
1471
+ return `${parts.join("\n\n")}\n`;
1472
+ }
1473
+ //#endregion
1369
1474
  //#region src/agents/codex/codex-adapter.ts
1370
1475
  const CONFIG_FILE = ".codex/config.toml";
1371
1476
  const CONFIG_SOURCE = "the rules/ and mcp/ folders and env of tunestra.yaml and its presets";
1372
1477
  const EXEC_RULES_FILE = ".codex/rules/tunestra.rules";
1373
1478
  const EXEC_RULES_SOURCE = "permissions in tunestra.yaml and its presets";
1479
+ const SUBAGENTS_DIR = ".codex/agents";
1374
1480
  const INSTRUCTIONS_INTRO = "Rules of this project, ordered from specific to general. When two rules conflict, follow the one that comes first.";
1375
1481
  const TRUST_NOTE = "Codex reads .codex/ only in a trusted project: trust the project when Codex asks.";
1376
1482
  /**
1377
1483
  * Codex reads AGENTS.md and the skills in `.agents/skills` by itself. Rules go to
1378
1484
  * `developer_instructions` in `.codex/config.toml`, because Codex does not follow the links
1379
1485
  * in AGENTS.md; MCP servers and env go to the same file. Commands become skills, and
1380
- * permission rules for shell commands become `.codex/rules/tunestra.rules`.
1486
+ * permission rules for shell commands become `.codex/rules/tunestra.rules`. Subagents go
1487
+ * to `.codex/agents`.
1381
1488
  */
1382
1489
  const codexAdapter = {
1383
1490
  agent: Agent.Codex,
@@ -1394,13 +1501,24 @@ function planCodex(context) {
1394
1501
  return false;
1395
1502
  }).flatMap((command) => planCommandSkill(projectDir, command));
1396
1503
  outputs.push(...commandSkills);
1397
- const configFile = planConfigFile(ruleFiles, resolution.mcp.filter((server) => {
1504
+ const servers = resolution.mcp.filter((server) => {
1398
1505
  if (server.artifact.config.type === "sse") {
1399
1506
  notes.push(`Codex skips the MCP server "${server.artifact.key}": Codex does not support SSE.`);
1400
1507
  return false;
1401
1508
  }
1402
1509
  return true;
1403
- }), resolution.env);
1510
+ });
1511
+ const subagentFiles = resolution.subagents.map((subagent) => {
1512
+ return createFileOutput(`${SUBAGENTS_DIR}/${subagent.artifact.key}.toml`, renderSubagentToml(subagent.artifact), describeArtifactSource(projectDir, subagent.origin, subagent.artifact.source));
1513
+ });
1514
+ const readOnlySubagents = resolution.subagents.filter((subagent) => subagent.artifact.readOnly).map((subagent) => subagent.artifact.key);
1515
+ const hasReadOnlySubagents = readOnlySubagents.length > 0;
1516
+ outputs.push(...subagentFiles);
1517
+ if (hasReadOnlySubagents) {
1518
+ const names = readOnlySubagents.join(", ");
1519
+ notes.push(`Codex cannot make subagents read-only: ${names}. It turns off their shell and asks them not to edit files.`);
1520
+ }
1521
+ const configFile = planConfigFile(ruleFiles, servers, resolution.env);
1404
1522
  const execRules = planExecRules(resolution.permissions);
1405
1523
  const ignoredPaths = [];
1406
1524
  notes.push(...execRules.notes);
@@ -1412,6 +1530,7 @@ function planCodex(context) {
1412
1530
  outputs.push(execRules.file);
1413
1531
  ignoredPaths.push(EXEC_RULES_FILE);
1414
1532
  }
1533
+ if (subagentFiles.length > 0) ignoredPaths.push(`${SUBAGENTS_DIR}/`);
1415
1534
  if (ignoredPaths.length > 0) notes.push(TRUST_NOTE);
1416
1535
  return {
1417
1536
  outputs,
@@ -1576,6 +1695,7 @@ function renderLevelLine(rule) {
1576
1695
  const HIERARCHY_INTRO = "Instructions are ordered from specific to general. When two instructions conflict, follow the one from the higher level in this list.";
1577
1696
  const RULES_INTRO = "Always follow these rules. Claude Code and Codex load them automatically; other agents should read them before starting work.";
1578
1697
  const SKILLS_INTRO = "Use a skill when the task matches its description.";
1698
+ const SUBAGENTS_INTRO = "Hand a task to a subagent when the task matches its description.";
1579
1699
  const ENV_INTRO = "Agents set these variables in every session, also for the commands they run. The values are in the presets.";
1580
1700
  const CHANGING_SECTION = [
1581
1701
  "## Changing this setup",
@@ -1678,6 +1798,21 @@ function renderMcpServers(servers) {
1678
1798
  ];
1679
1799
  })));
1680
1800
  }
1801
+ function renderSubagents(subagents) {
1802
+ const table = renderTable([
1803
+ "Subagent",
1804
+ "Level",
1805
+ "When to use"
1806
+ ], subagents.map((subagent) => {
1807
+ const origin = describeOrigin(subagent);
1808
+ return [
1809
+ subagent.artifact.key,
1810
+ origin,
1811
+ subagent.artifact.description
1812
+ ];
1813
+ }));
1814
+ return renderSection("Subagents", SUBAGENTS_INTRO, table);
1815
+ }
1681
1816
  function renderEnv(env) {
1682
1817
  const table = renderTable(["Variable", "Level"], env.map((variable) => {
1683
1818
  const origin = describeOrigin(variable);
@@ -1710,7 +1845,7 @@ function renderSection(title, intro, table) {
1710
1845
  //#region src/generate/agents-index.ts
1711
1846
  /**
1712
1847
  * AGENTS.md: an index of the setup, not the rules themselves. It lists the levels, every
1713
- * rule, skill, command and MCP server, where each comes from and where its file is.
1848
+ * rule, skill, command, MCP server and subagent, where each comes from and where its file is.
1714
1849
  */
1715
1850
  function renderAgentsIndex(resolution) {
1716
1851
  const title = `# AI agent context: ${resolution.chain.project.name}`;
@@ -1724,6 +1859,7 @@ function renderAgentsIndex(resolution) {
1724
1859
  const hasSkills = resolution.skills.length > 0;
1725
1860
  const hasCommands = resolution.commands.length > 0;
1726
1861
  const hasMcpServers = resolution.mcp.length > 0;
1862
+ const hasSubagents = resolution.subagents.length > 0;
1727
1863
  const hasEnv = resolution.env.length > 0;
1728
1864
  if (hasRules) {
1729
1865
  const rules = renderRules(resolution.rules);
@@ -1741,6 +1877,10 @@ function renderAgentsIndex(resolution) {
1741
1877
  const servers = renderMcpServers(resolution.mcp);
1742
1878
  sections.push(servers);
1743
1879
  }
1880
+ if (hasSubagents) {
1881
+ const subagents = renderSubagents(resolution.subagents);
1882
+ sections.push(subagents);
1883
+ }
1744
1884
  if (hasEnv) {
1745
1885
  const env = renderEnv(resolution.env);
1746
1886
  sections.push(env);
@@ -1830,6 +1970,7 @@ const ArtifactKind = {
1830
1970
  Skills: "skills",
1831
1971
  Commands: "commands",
1832
1972
  Mcp: "mcp",
1973
+ Subagents: "subagents",
1833
1974
  Env: "env"
1834
1975
  };
1835
1976
  //#endregion
@@ -2024,6 +2165,7 @@ function applyInheritance(chain) {
2024
2165
  const skills = inheritArtifacts(chain.levels, ArtifactKind.Skills, (level) => level.skills);
2025
2166
  const commands = inheritArtifacts(chain.levels, ArtifactKind.Commands, (level) => level.commands);
2026
2167
  const mcp = inheritArtifacts(chain.levels, ArtifactKind.Mcp, (level) => level.mcp, patchMcpServers);
2168
+ const subagents = inheritArtifacts(chain.levels, ArtifactKind.Subagents, (level) => level.subagents);
2027
2169
  const env = inheritArtifacts(chain.levels, ArtifactKind.Env, (level) => level.env);
2028
2170
  const permissions = inheritPermissions(chain.levels);
2029
2171
  const problems = [
@@ -2031,6 +2173,7 @@ function applyInheritance(chain) {
2031
2173
  ...skills.problems,
2032
2174
  ...commands.problems,
2033
2175
  ...mcp.problems,
2176
+ ...subagents.problems,
2034
2177
  ...env.problems,
2035
2178
  ...permissions.problems
2036
2179
  ];
@@ -2041,6 +2184,7 @@ function applyInheritance(chain) {
2041
2184
  skills: skills.artifacts,
2042
2185
  commands: commands.artifacts,
2043
2186
  mcp: mcp.artifacts,
2187
+ subagents: subagents.artifacts,
2044
2188
  env: env.artifacts,
2045
2189
  permissions: permissions.artifacts
2046
2190
  };
@@ -2400,6 +2544,7 @@ function describeResolution(resolution) {
2400
2544
  describeSection("Skills", resolution.skills, projectDir),
2401
2545
  describeSection("Commands", resolution.commands, projectDir),
2402
2546
  describeSection("MCP servers", resolution.mcp, projectDir),
2547
+ describeSection("Subagents", resolution.subagents, projectDir),
2403
2548
  describeSection("Environment", resolution.env, projectDir),
2404
2549
  describeSection("Permissions", permissions, projectDir)
2405
2550
  ].filter((section) => section.length > 0).join("\n\n");
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- export { t as VERSION } from "./src-Bh7d7ERC.mjs";
1
+ export { t as VERSION } from "./src-CZK2Ixbt.mjs";
@@ -1,5 +1,5 @@
1
1
  //#endregion
2
2
  //#region src/index.ts
3
- const VERSION = "0.4.0";
3
+ const VERSION = "0.5.0";
4
4
  //#endregion
5
5
  export { VERSION as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tunestra",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Tune your AI agents to play as one orchestra: one source of rules, skills, commands and MCP servers for Claude Code, Codex and more.",
5
5
  "keywords": [
6
6
  "ai",