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 +32 -2
- package/dist/cli.mjs +162 -17
- package/dist/index.mjs +1 -1
- package/dist/{src-Bh7d7ERC.mjs → src-CZK2Ixbt.mjs} +1 -1
- package/package.json +1 -1
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-
|
|
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/
|
|
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 =
|
|
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
|
|
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
|
-
})
|
|
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
|
|
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-
|
|
1
|
+
export { t as VERSION } from "./src-CZK2Ixbt.mjs";
|
package/package.json
CHANGED