teamcast 1.0.2 → 1.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.
package/README.md CHANGED
@@ -1,11 +1,17 @@
1
1
  # TeamCast
2
2
 
3
+ [![CI](https://github.com/Katoshy/teamcast/actions/workflows/ci.yml/badge.svg)](https://github.com/Katoshy/teamcast/actions/workflows/ci.yml)
4
+ [![npm version](https://img.shields.io/npm/v/teamcast)](https://www.npmjs.com/package/teamcast)
5
+ [![License](https://img.shields.io/github/license/Katoshy/teamcast)](https://github.com/Katoshy/teamcast/blob/main/LICENSE)
6
+
3
7
  CLI to design, generate, and validate multi-target agent teams for Claude Code and Codex from a single manifest.
4
8
 
5
9
  Define your agent team in one `teamcast.yaml` file. TeamCast validates the manifest, generates `.claude/` and/or `.codex/` config files, and keeps generated output in sync with the source config.
6
10
 
7
11
  ## Install
8
12
 
13
+ Requires Node.js 24 or newer.
14
+
9
15
  ```bash
10
16
  npm install -g teamcast
11
17
  ```
@@ -16,6 +22,14 @@ Or run without installing:
16
22
  npx teamcast <command>
17
23
  ```
18
24
 
25
+ ## Community
26
+
27
+ - Contribution guide: [CONTRIBUTING.md](./CONTRIBUTING.md)
28
+ - Code of conduct: [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md)
29
+ - Security policy: [SECURITY.md](./SECURITY.md)
30
+ - Bug reports and feature requests: use the GitHub issue templates
31
+ - Pull requests: use the repository PR template and run `npm test` plus `npm run build` before opening one
32
+
19
33
  ## Quick Start
20
34
 
21
35
  ```bash
@@ -43,6 +57,7 @@ After `generate`, your project will have files for the selected target(s):
43
57
  - Codex target:
44
58
  - `.codex/config.toml` - workspace-level Codex config
45
59
  - `.codex/agents/<name>.toml` - one TOML config per agent
60
+ - `.agents/skills/<skill>/SKILL.md` - one stub file per unique skill
46
61
  - `AGENTS.md` - team-level instructions for Codex agents
47
62
 
48
63
  Use `teamcast init --target claude|codex|both` to choose which outputs are generated up front.
@@ -54,7 +69,7 @@ TeamCast now uses a canonical manifest shape with target-specific blocks:
54
69
  - `claude.agents.<name>` - native Claude Code runtime fields and doc outputs
55
70
  - `codex.agents.<name>` - native Codex runtime fields and TOML outputs
56
71
  - `<target>.agents.<name>.forge` - TeamCast-only metadata such as delegation graph
57
- - `project.environments` - active project environments such as `node`, `python` — auto-detected or explicit
72
+ - `project.environments` - active project environments (`node`, `python`, `go`, `rust`, `java`, `ruby`, `docker`, `terraform`) — auto-detected or explicit
58
73
 
59
74
  TeamCast includes a built-in registry of capabilities, traits, instruction fragments, policy fragments, models, and skills. These are not serialized into `teamcast.yaml`.
60
75
 
@@ -273,7 +288,7 @@ Use `--yes` to skip confirmation.
273
288
 
274
289
  ### `create skill <name>`
275
290
 
276
- `teamcast create skill <name>` registers a new skill name on one agent, writes `teamcast.yaml`, and generates `.claude/skills/<name>/SKILL.md`.
291
+ `teamcast create skill <name>` registers a new skill name on one agent, writes `teamcast.yaml`, and generates a target-specific stub: `.claude/skills/<name>/SKILL.md` for Claude or `.agents/skills/<name>/SKILL.md` for Codex.
277
292
 
278
293
  Rules and prompts:
279
294
 
@@ -310,7 +325,7 @@ teamcast generate --dry-run
310
325
  Behavior:
311
326
 
312
327
  - `generate` overwrites generated agent/docs/settings files
313
- - skill stub files under `.claude/skills/` are created only if missing
328
+ - skill stub files under `.claude/skills/` and `.agents/skills/` are created only if missing
314
329
  - `--dry-run` shows what would be generated without writing anything
315
330
 
316
331
  ### `diff`
@@ -429,6 +444,7 @@ It targets:
429
444
 
430
445
  - `.claude/agents`
431
446
  - `.claude/skills`
447
+ - generated Codex skill files under `.agents/skills/`
432
448
  - `.claude/settings.json`
433
449
  - `.claude/settings.local.json`
434
450
  - `CLAUDE.md`
@@ -467,9 +483,9 @@ Everything is defined in `teamcast.yaml` at the root of your project.
467
483
 
468
484
  For Claude targets, TeamCast renders `.claude/agents/<name>.md` and `CLAUDE.md` from `claude.agents.<name>`.
469
485
 
470
- For Codex targets, TeamCast renders `.codex/agents/<name>.toml` plus `.codex/config.toml` from `codex.agents.<name>`.
486
+ For Codex targets, TeamCast renders `.codex/agents/<name>.toml` plus `.codex/config.toml` from `codex.agents.<name>`, and writes Codex skill docs to `.agents/skills/<skill>/`.
471
487
 
472
- `.codex/config.toml` is the workspace-level agent index. Concrete agent runtime config lives in `.codex/agents/<name>.toml`.
488
+ `.codex/config.toml` is the workspace-level agent index. Concrete agent runtime config lives in `.codex/agents/<name>.toml`. Codex skill discovery uses `.agents/skills/`, not `.codex/`.
473
489
 
474
490
  Native Claude Code fields are rendered into frontmatter:
475
491
 
@@ -510,6 +526,50 @@ This means a **reviewer** (read + execute, no write) gets code patterns but NOT
510
526
 
511
527
  Custom agents work the same way — a `react-dev` with `write_files` + `execute` automatically gets the right fragments without any role-name matching.
512
528
 
529
+ #### Built-in environments
530
+
531
+ | Environment | Auto-detected by | Policy allows |
532
+ |---|---|---|
533
+ | `node` | `package.json` | `npm`, `npx`, `node` |
534
+ | `python` | `pyproject.toml`, `requirements.txt`, `setup.py` | `pytest`, `python`, `uv`, `poetry` |
535
+ | `go` | `go.mod` | `go build/test/run/vet/mod` |
536
+ | `rust` | `Cargo.toml` | `cargo`, `rustfmt`, `clippy` |
537
+ | `java` | `pom.xml`, `build.gradle` | `mvn`, `gradle`, `./gradlew` |
538
+ | `ruby` | `Gemfile` | `bundle`, `rake`, `rspec` |
539
+ | `docker` | `Dockerfile`, `docker-compose.yml` | `docker`, `docker compose` |
540
+ | `terraform` | `main.tf`, `terraform.tf` | `terraform init/plan/validate/fmt` |
541
+
542
+ #### Custom environments
543
+
544
+ Drop a YAML file into `.agentforge/environments/` to add a new environment or override a builtin:
545
+
546
+ ```yaml
547
+ # .agentforge/environments/bun.yaml
548
+ id: bun
549
+ description: "Bun runtime environment"
550
+ detect_files:
551
+ - bun.lockb
552
+ policy_rules:
553
+ sandbox:
554
+ enabled: true
555
+ allow:
556
+ - "Bash(bun *)"
557
+ instruction_fragments:
558
+ bun_patterns:
559
+ content: |
560
+ This project uses Bun.
561
+ Use `bun install`, `bun run`, and `bun test`.
562
+ requires_capabilities:
563
+ - read_files
564
+ ```
565
+
566
+ Reference it in `teamcast.yaml` by id:
567
+
568
+ ```yaml
569
+ project:
570
+ environments: [node, bun]
571
+ ```
572
+
513
573
  ### Instruction Layers
514
574
 
515
575
  Agent prompts are composed from three layers:
@@ -518,7 +578,7 @@ Agent prompts are composed from three layers:
518
578
  |-------|--------|-------|
519
579
  | **instruction_blocks** | `teamcast.yaml` or preset | Project-specific behavior, workflow rules |
520
580
  | **instruction_fragments** | Built-in registry | Reusable role patterns (e.g. `feature-developer-workflow`) |
521
- | **environment instructions** | Built-in environments | Toolchain best practices, injected by capability |
581
+ | **environment instructions** | Built-in + custom environments | Toolchain best practices, injected by capability |
522
582
 
523
583
  Presets provide sensible defaults for `instruction_blocks` and `instruction_fragments`. For deeper customization, edit `teamcast.yaml` and run `teamcast generate`.
524
584
 
@@ -642,6 +702,7 @@ claude:
642
702
  | `claude.tools` | string[] | Native Claude Code tool allow-list |
643
703
  | `claude.disallowed_tools` | string[] | Native Claude Code tool deny-list |
644
704
  | `claude.skills` | string[] | Skill names. Each unique skill generates `.claude/skills/<skill>/SKILL.md` |
705
+ | `codex.skills` | string[] | Skill names. Each unique skill generates `.agents/skills/<skill>/SKILL.md` |
645
706
  | `claude.max_turns` | number | Maximum agentic turns |
646
707
  | `claude.mcp_servers` | object[] | MCP server definitions |
647
708
  | `claude.permission_mode` | `default \| acceptEdits \| bypassPermissions \| plan \| dontAsk` | Claude Code permission mode |
@@ -7,8 +7,11 @@ export function buildGeneratedOutputs(team, targetName, options) {
7
7
  const renderer = target.renderer;
8
8
  const files = renderer.render(spec);
9
9
  if (!options.dryRun) {
10
- const editable = files.filter((file) => isUserEditableGeneratedFile(file.path));
11
- const generated = files.filter((file) => !isUserEditableGeneratedFile(file.path));
10
+ const editable = [];
11
+ const generated = [];
12
+ for (const file of files) {
13
+ (isUserEditableGeneratedFile(file.path) ? editable : generated).push(file);
14
+ }
12
15
  writeFiles(generated, options.cwd);
13
16
  writeFiles(editable, options.cwd, { skipExisting: true });
14
17
  }
@@ -7,6 +7,7 @@ import { hasErrors, printManifestValidationSummary, } from '../validator/reporte
7
7
  import { getTarget, getRegisteredTargetNames } from '../renderers/registry.js';
8
8
  import { applyEnvironmentInstructions, resolveEnvironmentIds, resolveEnvironmentPolicies, } from '../core/environment-resolver.js';
9
9
  import { checkManifestRegistry } from '../validator/checks/manifest-registry.js';
10
+ import { builtinResourceLoader } from '../registry/resource-loader.js';
10
11
  export function evaluateTeam(manifest, options) {
11
12
  const schemaResult = validateSchema(manifest);
12
13
  if (!schemaResult.valid) {
@@ -15,6 +16,8 @@ export function evaluateTeam(manifest, options) {
15
16
  validationResults: [],
16
17
  };
17
18
  }
19
+ if (options?.cwd)
20
+ builtinResourceLoader.loadUserResources(options.cwd);
18
21
  const rawManifest = applyDefaults(schemaResult.data);
19
22
  const resolvedManifest = options?.cwd ? resolveEnvironmentPolicies(rawManifest, options.cwd) : rawManifest;
20
23
  const manifestRegistryResults = checkManifestRegistry(resolvedManifest);
@@ -6,7 +6,7 @@ import { writeManifest } from '../manifest/writer.js';
6
6
  import { expandCapabilities } from '../core/capability-resolver.js';
7
7
  import { generate } from '../generator/index.js';
8
8
  import { defaultRegistry } from '../registry/index.js';
9
- import { printSuccess, printError, printHeader, printCommandSuccess, } from '../utils/chalk-helpers.js';
9
+ import { printSuccess, printError, printHeader, printCommandSuccess, printNextSteps, } from '../utils/chalk-helpers.js';
10
10
  import { getTarget, getRegisteredTargetNames } from '../renderers/registry.js';
11
11
  import { evaluateTeam, teamHasBlockingIssues, printManifestValidation, } from '../application/validate-team.js';
12
12
  import { promptConfirm, promptInput, promptList, promptCheckbox, } from '../utils/prompts.js';
@@ -248,6 +248,10 @@ export async function runAddAgentCommand(name, options) {
248
248
  const nextTeam = addAgentToTeam(team, name, agent);
249
249
  applyManifestChanges(cwd, manifest, targetName, nextTeam);
250
250
  printCommandSuccess(`Agent "${name}" added and configuration regenerated`);
251
+ printNextSteps([
252
+ `Open ${chalk.bold('teamcast.yaml')} and fill in agent instructions based on ${chalk.yellow('// TODO')} comments`,
253
+ `Run ${chalk.bold('teamcast generate')} to apply your changes`,
254
+ ]);
251
255
  }
252
256
  export async function runCreateSkillCommand(name, options) {
253
257
  const cwd = process.cwd();
@@ -407,7 +411,11 @@ async function promptAgentConfig(name, targetContext) {
407
411
  instructions: [
408
412
  {
409
413
  kind: 'behavior',
410
- content: `You are ${name}. Focus on the responsibilities described in your role and use your allowed tools appropriately.`,
414
+ content: `You are ${name}.\n// TODO: Describe the agent's core personality, rules, and constraints here.\n// Example: "You are a strict security auditor. Never trust user input."`,
415
+ },
416
+ {
417
+ kind: 'workflow',
418
+ content: `// TODO: Define the step-by-step process the agent should follow.\n// 1. Read the provided context.\n// 2. Perform analysis.\n// 3. Output the result.`,
411
419
  },
412
420
  ],
413
421
  };
@@ -12,7 +12,7 @@ export function registerManageCommands(program) {
12
12
  const createCmd = program.command('create').description('Create a new resource');
13
13
  createCmd
14
14
  .command('skill <name>')
15
- .description('Create a new skill (generates stub file in .claude/skills/)')
15
+ .description('Create a new skill (Claude: .claude/skills/, Codex: .agents/skills/)')
16
16
  .option('--target <name>', 'Target block to modify')
17
17
  .action(async (name, options) => {
18
18
  const { runCreateSkillCommand } = await import('../manage.js');
package/dist/cli/reset.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import chalk from 'chalk';
2
2
  import { existsSync, rmSync, readdirSync, statSync } from 'fs';
3
- import { join } from 'path';
3
+ import { dirname, join } from 'path';
4
4
  import { readManifest, ManifestError } from '../manifest/reader.js';
5
5
  import { generate } from '../generator/index.js';
6
6
  import { printSuccess, printHeader, printCommandSuccess, printDim, } from '../utils/chalk-helpers.js';
@@ -47,6 +47,22 @@ function deleteEmptyDirIfPossible(cwd, rel) {
47
47
  printSuccess(`Deleted ${rel}/`);
48
48
  }
49
49
  }
50
+ function pruneEmptyAncestorDirs(cwd, rel) {
51
+ let current = dirname(rel);
52
+ while (current !== '.' && current !== '') {
53
+ const abs = join(cwd, current);
54
+ if (!existsSync(abs)) {
55
+ current = dirname(current);
56
+ continue;
57
+ }
58
+ if (readdirSync(abs).length > 0) {
59
+ break;
60
+ }
61
+ rmSync(abs, { recursive: true });
62
+ printSuccess(`Deleted ${current}/`);
63
+ current = dirname(current);
64
+ }
65
+ }
50
66
  function deleteFiles(cwd, paths) {
51
67
  for (const rel of paths) {
52
68
  const abs = join(cwd, rel);
@@ -60,6 +76,7 @@ function deleteFiles(cwd, paths) {
60
76
  rmSync(abs);
61
77
  }
62
78
  printSuccess(`Deleted ${rel}`);
79
+ pruneEmptyAncestorDirs(cwd, rel);
63
80
  }
64
81
  deleteEmptyDirIfPossible(cwd, '.claude/agents');
65
82
  deleteEmptyDirIfPossible(cwd, '.claude/skills');
@@ -1,6 +1,6 @@
1
1
  import { TARGET_NAMES, getManifestTargetConfig, setManifestTargetConfig } from '../manifest/targets.js';
2
2
  import { getEnvironment, detectEnvironments } from '../registry/environments.js';
3
- import { isEnvironmentId } from '../registry/types.js';
3
+ import { isEnvironmentId } from '../registry/environments.js';
4
4
  import { agentHasCapability } from './capability-resolver.js';
5
5
  /**
6
6
  * Resolves environment IDs from the manifest, combining:
@@ -80,31 +80,30 @@ function mergePoliciesSimple(base, extra) {
80
80
  }
81
81
  : undefined,
82
82
  hooks: base.hooks || extra.hooks
83
- ? {
84
- pre_tool_use: [...(base.hooks?.pre_tool_use ?? []), ...(extra.hooks?.pre_tool_use ?? [])].length > 0
85
- ? [...(base.hooks?.pre_tool_use ?? []), ...(extra.hooks?.pre_tool_use ?? [])]
86
- : undefined,
87
- post_tool_use: [...(base.hooks?.post_tool_use ?? []), ...(extra.hooks?.post_tool_use ?? [])].length > 0
88
- ? [...(base.hooks?.post_tool_use ?? []), ...(extra.hooks?.post_tool_use ?? [])]
89
- : undefined,
90
- notification: [...(base.hooks?.notification ?? []), ...(extra.hooks?.notification ?? [])].length > 0
91
- ? [...(base.hooks?.notification ?? []), ...(extra.hooks?.notification ?? [])]
92
- : undefined,
93
- }
83
+ ? (() => {
84
+ const pre = [...(base.hooks?.pre_tool_use ?? []), ...(extra.hooks?.pre_tool_use ?? [])];
85
+ const post = [...(base.hooks?.post_tool_use ?? []), ...(extra.hooks?.post_tool_use ?? [])];
86
+ const notif = [...(base.hooks?.notification ?? []), ...(extra.hooks?.notification ?? [])];
87
+ return {
88
+ pre_tool_use: pre.length > 0 ? pre : undefined,
89
+ post_tool_use: post.length > 0 ? post : undefined,
90
+ notification: notif.length > 0 ? notif : undefined,
91
+ };
92
+ })()
94
93
  : undefined,
95
94
  network: base.network || extra.network
96
- ? {
97
- allowed_domains: [...new Set([
95
+ ? (() => {
96
+ const domains = [...new Set([
98
97
  ...(base.network?.allowed_domains ?? []),
99
98
  ...(extra.network?.allowed_domains ?? []),
100
- ])].length > 0
101
- ? [...new Set([...(base.network?.allowed_domains ?? []), ...(extra.network?.allowed_domains ?? [])])]
102
- : undefined,
103
- }
104
- : undefined,
105
- assertions: [...(base.assertions ?? []), ...(extra.assertions ?? [])].length > 0
106
- ? [...(base.assertions ?? []), ...(extra.assertions ?? [])]
99
+ ])];
100
+ return { allowed_domains: domains.length > 0 ? domains : undefined };
101
+ })()
107
102
  : undefined,
103
+ assertions: (() => {
104
+ const merged = [...(base.assertions ?? []), ...(extra.assertions ?? [])];
105
+ return merged.length > 0 ? merged : undefined;
106
+ })(),
108
107
  };
109
108
  }
110
109
  function resolveTargetPolicies(envPolicies, targetConfig) {
@@ -1,3 +1,3 @@
1
1
  export function isUserEditableGeneratedFile(path) {
2
- return path.startsWith('.claude/skills/');
2
+ return path.startsWith('.claude/skills/') || path.startsWith('.agents/skills/');
3
3
  }
@@ -3,7 +3,9 @@ import { buildGeneratedOutputs } from '../application/build-generated-files.js';
3
3
  import { getRegisteredTargetNames, getTarget } from '../renderers/registry.js';
4
4
  import { normalizeManifest } from '../manifest/normalize.js';
5
5
  import { applyEnvironmentInstructions, resolveEnvironmentIds, resolveEnvironmentPolicies, } from '../core/environment-resolver.js';
6
+ import { builtinResourceLoader } from '../registry/resource-loader.js';
6
7
  export function generate(manifest, options) {
8
+ builtinResourceLoader.loadUserResources(options.cwd);
7
9
  const rawManifest = resolveEnvironmentPolicies(applyDefaults(manifest), options.cwd);
8
10
  const envIds = resolveEnvironmentIds(rawManifest, options.cwd);
9
11
  const rawManifestRecord = rawManifest;
@@ -0,0 +1,65 @@
1
+ // Environment YAML schema — parse and convert YAML environment definitions
2
+ // to runtime EnvironmentDef objects.
3
+ import { existsSync } from 'fs';
4
+ import { join } from 'path';
5
+ import { isCapability } from './types.js';
6
+ // --- Validation ---
7
+ export function parseEnvironmentYaml(raw) {
8
+ if (!raw || typeof raw !== 'object') {
9
+ throw new Error('Environment definition must be an object');
10
+ }
11
+ const obj = raw;
12
+ if (typeof obj.id !== 'string' || !obj.id) {
13
+ throw new Error('Environment definition requires a non-empty "id" field');
14
+ }
15
+ if (typeof obj.description !== 'string') {
16
+ throw new Error(`Environment "${obj.id}": "description" must be a string`);
17
+ }
18
+ if (obj.detect_files !== undefined) {
19
+ if (!Array.isArray(obj.detect_files) || !obj.detect_files.every((f) => typeof f === 'string')) {
20
+ throw new Error(`Environment "${obj.id}": "detect_files" must be a string array`);
21
+ }
22
+ }
23
+ if (!obj.policy_rules || typeof obj.policy_rules !== 'object') {
24
+ throw new Error(`Environment "${obj.id}": "policy_rules" must be an object`);
25
+ }
26
+ const policies = obj.policy_rules;
27
+ if (policies.allow !== undefined) {
28
+ if (!Array.isArray(policies.allow) || !policies.allow.every((a) => typeof a === 'string')) {
29
+ throw new Error(`Environment "${obj.id}": "policy_rules.allow" must be a string array`);
30
+ }
31
+ }
32
+ if (!obj.instruction_fragments || typeof obj.instruction_fragments !== 'object') {
33
+ throw new Error(`Environment "${obj.id}": "instruction_fragments" must be an object`);
34
+ }
35
+ return obj;
36
+ }
37
+ // --- Conversion to runtime EnvironmentDef ---
38
+ function toEnvironmentInstruction(value) {
39
+ if (typeof value === 'string')
40
+ return value;
41
+ return {
42
+ content: value.content,
43
+ requires_capabilities: value.requires_capabilities.filter(isCapability),
44
+ };
45
+ }
46
+ export function environmentYamlToDef(yaml) {
47
+ const fragments = {};
48
+ for (const [key, value] of Object.entries(yaml.instruction_fragments)) {
49
+ fragments[key] = toEnvironmentInstruction(value);
50
+ }
51
+ const def = {
52
+ id: yaml.id,
53
+ description: yaml.description,
54
+ policyRules: {
55
+ sandbox: yaml.policy_rules.sandbox,
56
+ allow: yaml.policy_rules.allow,
57
+ },
58
+ instructionFragments: fragments,
59
+ };
60
+ if (yaml.detect_files?.length) {
61
+ const files = yaml.detect_files;
62
+ def.detect = (cwd) => files.some((file) => existsSync(join(cwd, file)));
63
+ }
64
+ return def;
65
+ }
@@ -1,105 +1,17 @@
1
- import { existsSync } from 'fs';
2
- import { join } from 'path';
3
- const ENVIRONMENTS = {
4
- node: {
5
- id: 'node',
6
- description: 'Node.js environment, auto-detected via package.json',
7
- detect: (cwd) => existsSync(join(cwd, 'package.json')),
8
- policyRules: {
9
- sandbox: { enabled: true },
10
- allow: [
11
- 'Bash(npm run *)',
12
- 'Bash(npm test *)',
13
- 'Bash(npx *)',
14
- 'Bash(npm install)',
15
- 'Bash(node *)',
16
- ],
17
- },
18
- instructionFragments: {
19
- node_code_patterns: {
20
- content: [
21
- 'This is a Node.js project.',
22
- 'Use ESM module syntax (import/export). All relative imports must use .js extensions.',
23
- 'Prefer named exports over default exports.',
24
- 'Use TypeScript strict mode when tsconfig.json is present.',
25
- ].join('\n'),
26
- requires_capabilities: ['read_files'],
27
- },
28
- node_development: {
29
- content: [
30
- 'Install dependencies with `npm install`.',
31
- 'Use `npm run <script>` to execute package.json scripts.',
32
- 'Prefer async/await over raw Promises or callbacks.',
33
- 'Handle errors at system boundaries. Use typed error classes where the project defines them.',
34
- ].join('\n'),
35
- requires_capabilities: ['write_files'],
36
- },
37
- node_testing: {
38
- content: [
39
- 'Run tests with `npm test`.',
40
- 'Run a specific test file with `npx vitest run <path>` (vitest) or `npx jest <path>` (jest).',
41
- 'Always run tests after making changes to verify nothing broke.',
42
- 'Follow existing test patterns: check the tests/ directory for conventions before writing new tests.',
43
- ].join('\n'),
44
- requires_capabilities: ['execute', 'write_files'],
45
- },
46
- },
47
- },
48
- python: {
49
- id: 'python',
50
- description: 'Python environment, auto-detected via pyproject.toml or requirements.txt',
51
- detect: (cwd) => existsSync(join(cwd, 'pyproject.toml')) ||
52
- existsSync(join(cwd, 'requirements.txt')) ||
53
- existsSync(join(cwd, 'setup.py')),
54
- policyRules: {
55
- sandbox: { enabled: true },
56
- allow: [
57
- 'Bash(pytest *)',
58
- 'Bash(python -m pytest *)',
59
- 'Bash(uv run *)',
60
- 'Bash(poetry run *)',
61
- 'Bash(python *)',
62
- ],
63
- },
64
- instructionFragments: {
65
- python_code_patterns: {
66
- content: [
67
- 'This is a Python project.',
68
- 'Follow PEP 8 style conventions.',
69
- 'Use type hints for function signatures and class attributes.',
70
- 'Prefer pathlib.Path over os.path for file operations.',
71
- ].join('\n'),
72
- requires_capabilities: ['read_files'],
73
- },
74
- python_development: {
75
- content: [
76
- 'If using poetry: `poetry install` and `poetry run <cmd>`. If using uv: `uv sync` and `uv run <cmd>`.',
77
- 'Otherwise use pip and virtualenv.',
78
- 'Use structured logging (logging module) instead of print statements.',
79
- 'Handle exceptions with specific types, not bare except clauses.',
80
- ].join('\n'),
81
- requires_capabilities: ['write_files'],
82
- },
83
- python_testing: {
84
- content: [
85
- 'Run tests with `pytest`. If using poetry or uv, prefix with `poetry run` or `uv run`.',
86
- 'Run a specific test: `pytest <path>::<test_name>`.',
87
- 'Always run tests after changes. Follow existing test patterns in the tests/ directory.',
88
- 'Use fixtures for shared setup. Prefer parametrize for similar test cases.',
89
- ].join('\n'),
90
- requires_capabilities: ['execute', 'write_files'],
91
- },
92
- },
93
- },
94
- };
1
+ // Environment registry delegates to ResourceLoader (YAML is the sole source).
2
+ import { builtinResourceLoader } from './resource-loader.js';
3
+ export function isEnvironmentId(value) {
4
+ return builtinResourceLoader.hasEnvironment(value);
5
+ }
95
6
  export function getEnvironment(id) {
96
- return ENVIRONMENTS[id];
7
+ const env = builtinResourceLoader.getEnvironment(id);
8
+ if (!env)
9
+ throw new Error(`Unknown environment "${id}"`);
10
+ return env;
97
11
  }
98
12
  export function listEnvironments() {
99
- return Object.values(ENVIRONMENTS);
13
+ return builtinResourceLoader.listEnvironments();
100
14
  }
101
15
  export function detectEnvironments(cwd) {
102
- return Object.values(ENVIRONMENTS)
103
- .filter((env) => env.detect(cwd))
104
- .map((env) => env.id);
16
+ return builtinResourceLoader.detectEnvironments(cwd);
105
17
  }
@@ -0,0 +1,78 @@
1
+ // ResourceLoader — scans directories for YAML resource files and registers them.
2
+ import { readFileSync, readdirSync } from 'fs';
3
+ import { dirname, join } from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import { parse } from 'yaml';
6
+ import { parseEnvironmentYaml, environmentYamlToDef } from './environment-schema.js';
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = dirname(__filename);
9
+ const BUILTIN_ENVIRONMENTS_DIR = join(__dirname, '../../templates/environments');
10
+ /** Check whether an environment matches the given cwd. */
11
+ function matchesEnv(env, cwd) {
12
+ return env.detect ? env.detect(cwd) : false;
13
+ }
14
+ export class ResourceLoader {
15
+ environments = new Map();
16
+ loadedDirs = new Set();
17
+ /** Load all *.yaml files from a directory as environment definitions.
18
+ * Each directory is only loaded once — adding files after the first call has no effect. */
19
+ loadEnvironmentsFromDir(dir, allowOverride = false) {
20
+ if (this.loadedDirs.has(dir))
21
+ return;
22
+ this.loadedDirs.add(dir);
23
+ let files;
24
+ try {
25
+ files = readdirSync(dir).filter((f) => f.endsWith('.yaml') || f.endsWith('.yml'));
26
+ }
27
+ catch {
28
+ return; // Directory does not exist or is inaccessible
29
+ }
30
+ for (const file of files) {
31
+ const filePath = join(dir, file);
32
+ try {
33
+ const raw = parse(readFileSync(filePath, 'utf-8'));
34
+ const yaml = parseEnvironmentYaml(raw);
35
+ const def = environmentYamlToDef(yaml);
36
+ if (this.environments.has(def.id) && !allowOverride)
37
+ continue;
38
+ this.environments.set(def.id, def);
39
+ }
40
+ catch (err) {
41
+ if (allowOverride) {
42
+ // User-defined file — surface the error so the user can fix it
43
+ process.stderr.write(`[agentforge] Warning: skipping "${filePath}": ${err instanceof Error ? err.message : String(err)}\n`);
44
+ }
45
+ // Builtin files should never fail — skip silently
46
+ }
47
+ }
48
+ }
49
+ /** Load user-defined resources from a project's .agentforge/ directory. */
50
+ loadUserResources(projectDir) {
51
+ const envDir = join(projectDir, '.agentforge', 'environments');
52
+ this.loadEnvironmentsFromDir(envDir, true);
53
+ }
54
+ hasEnvironment(id) {
55
+ return this.environments.has(id);
56
+ }
57
+ getEnvironment(id) {
58
+ return this.environments.get(id);
59
+ }
60
+ listEnvironments() {
61
+ return [...this.environments.values()];
62
+ }
63
+ listEnvironmentIds() {
64
+ return [...this.environments.keys()];
65
+ }
66
+ detectEnvironments(cwd) {
67
+ return this.listEnvironments()
68
+ .filter((env) => matchesEnv(env, cwd))
69
+ .map((env) => env.id);
70
+ }
71
+ }
72
+ // Singleton — loads builtin environments from templates/environments/
73
+ function createBuiltinLoader() {
74
+ const loader = new ResourceLoader();
75
+ loader.loadEnvironmentsFromDir(BUILTIN_ENVIRONMENTS_DIR);
76
+ return loader;
77
+ }
78
+ export const builtinResourceLoader = createBuiltinLoader();
@@ -14,7 +14,74 @@ export const CAPABILITY_IDS = [
14
14
  export function isCapability(value) {
15
15
  return CAPABILITY_IDS.includes(value);
16
16
  }
17
- export const ENVIRONMENT_IDS = ['node', 'python'];
18
- export function isEnvironmentId(value) {
19
- return ENVIRONMENT_IDS.includes(value);
20
- }
17
+ // --- Capability Trait (named bundle of capabilities) ---
18
+ export const BUILTIN_CAPABILITY_TRAIT_IDS = [
19
+ 'base-read',
20
+ 'file-authoring',
21
+ 'command-execution',
22
+ 'web-research',
23
+ 'delegation',
24
+ 'interaction',
25
+ 'notebook-editing',
26
+ 'no-file-edits',
27
+ 'no-commands',
28
+ 'no-web',
29
+ 'full-access',
30
+ ];
31
+ // --- Policy Fragment ---
32
+ export const BUILTIN_POLICY_FRAGMENT_IDS = [
33
+ 'allow-git-read',
34
+ 'allow-git-write',
35
+ 'ask-git-push',
36
+ 'deny-destructive-shell',
37
+ 'deny-network-downloads',
38
+ 'deny-dynamic-exec',
39
+ 'deny-env-files',
40
+ 'sandbox-default',
41
+ ];
42
+ export const BUILTIN_INSTRUCTION_FRAGMENT_IDS = [
43
+ 'coordination-core',
44
+ 'delegate-first',
45
+ 'planning-core',
46
+ 'planning-read-only',
47
+ 'research-core',
48
+ 'research-citation',
49
+ 'research-no-file-edits',
50
+ 'development-core',
51
+ 'development-workflow',
52
+ 'tester-core',
53
+ 'tester-read-only',
54
+ 'review-core',
55
+ 'review-feedback',
56
+ 'security-audit-core',
57
+ 'security-audit-severity',
58
+ 'research-handoff',
59
+ 'secure-planning',
60
+ 'secure-development',
61
+ 'secure-development-tests',
62
+ 'security-review-gate',
63
+ 'post-audit-review',
64
+ 'solo-dev-core',
65
+ 'solo-dev-workflow',
66
+ 'solo-dev-style',
67
+ 'feature-orchestrator-workflow',
68
+ 'feature-orchestrator-output',
69
+ 'feature-planner-workflow',
70
+ 'feature-planner-read-only',
71
+ 'feature-developer-core',
72
+ 'feature-developer-workflow',
73
+ 'feature-developer-summary',
74
+ 'feature-reviewer-checklist',
75
+ 'feature-reviewer-style',
76
+ 'research-orchestrator-core',
77
+ 'research-orchestrator-workflow',
78
+ 'research-orchestrator-output',
79
+ 'research-planner-core',
80
+ 'research-planner-constraints',
81
+ 'research-developer-core',
82
+ 'research-developer-tests',
83
+ 'secure-orchestrator-core',
84
+ 'secure-orchestrator-workflow',
85
+ 'secure-orchestrator-gate',
86
+ 'post-audit-review-core',
87
+ ];
@@ -1,8 +1,11 @@
1
- // Codex skill renderer — generates {id}/SKILL.md + agents/openai.yaml per skill.
1
+ // Codex skill renderer — generates .agents/skills/{id}/SKILL.md + agents/openai.yaml per skill.
2
2
  import { defaultRegistry } from '../../registry/index.js';
3
+ function getSkillBasePath(skillId) {
4
+ return `.agents/skills/${skillId}`;
5
+ }
3
6
  // --- Frontmatter (Codex: name + description only) ---
4
7
  function buildFrontmatter(name, description) {
5
- return ['---', `name: ${name}`, `description: ${description}`, '---'].join('\n');
8
+ return ['---', `name: ${JSON.stringify(name)}`, `description: ${JSON.stringify(description)}`, '---'].join('\n');
6
9
  }
7
10
  // --- Stub for unknown skills ---
8
11
  function generateSkillStub(skillName) {
@@ -59,15 +62,16 @@ function renderOpenaiYaml(skill) {
59
62
  if (skill.metadata.version)
60
63
  lines.push(`version: "${skill.metadata.version}"`);
61
64
  }
62
- if (hasTools) {
65
+ if (hasTools || hasMcp) {
63
66
  lines.push('dependencies:');
67
+ }
68
+ if (hasTools) {
64
69
  lines.push(' tools:');
65
70
  for (const tool of skill.allowed_tools) {
66
71
  lines.push(` - ${tool}`);
67
72
  }
68
73
  }
69
74
  if (hasMcp) {
70
- lines.push('dependencies:');
71
75
  lines.push(' mcp_servers:');
72
76
  for (const server of skill.required_mcp_servers) {
73
77
  lines.push(` - ${server}`);
@@ -78,7 +82,7 @@ function renderOpenaiYaml(skill) {
78
82
  // --- Companion files ---
79
83
  function renderCompanionFiles(skillId, skill) {
80
84
  const files = [];
81
- const base = skillId;
85
+ const base = getSkillBasePath(skillId);
82
86
  if (skill.reference_files) {
83
87
  for (const [name, content] of Object.entries(skill.reference_files)) {
84
88
  files.push({ path: `${base}/references/${name}`, content });
@@ -108,14 +112,14 @@ export function renderCodexSkillMd(team) {
108
112
  const definition = defaultRegistry.getSkill(skillName);
109
113
  if (definition) {
110
114
  files.push({
111
- path: `${skillName}/SKILL.md`,
115
+ path: `${getSkillBasePath(skillName)}/SKILL.md`,
112
116
  content: renderSkillContent(definition),
113
117
  });
114
118
  files.push(...renderCompanionFiles(skillName, definition));
115
119
  }
116
120
  else {
117
121
  files.push({
118
- path: `${skillName}/SKILL.md`,
122
+ path: `${getSkillBasePath(skillName)}/SKILL.md`,
119
123
  content: generateSkillStub(skillName),
120
124
  });
121
125
  }
@@ -1,7 +1,7 @@
1
1
  import { isCapabilityTraitName } from '../../registry/traits.js';
2
2
  import { isPolicyFragmentId } from '../../registry/policy-fragments.js';
3
3
  import { isInstructionFragmentId } from '../../registry/instruction-fragments.js';
4
- import { isEnvironmentId } from '../../registry/types.js';
4
+ import { isEnvironmentId } from '../../registry/environments.js';
5
5
  import { getManifestTargetEntries } from '../../manifest/targets.js';
6
6
  /**
7
7
  * Pre-normalization manifest-level registry checks.
@@ -66,5 +66,24 @@ export function checkTeamGraphEnhanced(team) {
66
66
  message: `Multiple root agents detected: ${roots.join(', ')} — consider a single orchestrator entry point`,
67
67
  });
68
68
  }
69
+ // HANDOFF_CAPABILITY_MISMATCH — delegating to an agent with no tools
70
+ for (const [agentId, agent] of agentEntries) {
71
+ for (const target of agent.metadata?.handoffs ?? []) {
72
+ const targetAgent = team.agents[target];
73
+ if (!targetAgent)
74
+ continue; // already caught by checkHandoffGraph
75
+ const targetTools = targetAgent.runtime.tools ?? [];
76
+ if (targetTools.length === 0) {
77
+ results.push({
78
+ severity: 'warning',
79
+ category: 'Team graph',
80
+ code: 'HANDOFF_CAPABILITY_MISMATCH',
81
+ phase: 'team-graph',
82
+ message: `Agent "${agentId}" hands off to "${target}" but "${target}" has no capabilities — delegation may be ineffective`,
83
+ agent: agentId,
84
+ });
85
+ }
86
+ }
87
+ }
69
88
  return results;
70
89
  }
@@ -15,8 +15,8 @@ import { checkSkillRequirements } from './checks/skill-requirements.js';
15
15
  import { checkMcpServers } from './checks/mcp.js';
16
16
  import { checkTeamGraphEnhanced } from './checks/team-graph-enhanced.js';
17
17
  const CHECKERS = (skillMap, targetName) => [
18
- checkRegistryReferences,
19
- checkEnvironments,
18
+ checkRegistryReferences, // Phase 1
19
+ checkEnvironments, // Phase 9
20
20
  checkTraitCapabilities, // Phase 2
21
21
  (team) => checkCapabilityTools(team, skillMap), // Phase 3
22
22
  (team) => checkHandoffGraph(team, skillMap),
@@ -76,7 +76,8 @@ export async function runWizard(options) {
76
76
  printCommandSuccess(`Agent team initialized for project "${rawManifest.project.name}"`);
77
77
  printManifestValidation(validation);
78
78
  printNextSteps([
79
+ `Open ${chalk.bold('teamcast.yaml')} and fill in agent instructions based on ${chalk.yellow('// TODO')} comments`,
79
80
  `${chalk.bold('teamcast explain')} - view the team structure`,
80
- `Edit ${chalk.bold('teamcast.yaml')} and run ${chalk.bold('teamcast generate')} to apply changes`,
81
+ `Run ${chalk.bold('teamcast generate')} to apply your changes`,
81
82
  ]);
82
83
  }
@@ -1,6 +1,6 @@
1
1
  import chalk from 'chalk';
2
2
  import { detectEnvironments, listEnvironments } from '../../registry/environments.js';
3
- import { isEnvironmentId } from '../../registry/types.js';
3
+ import { isEnvironmentId } from '../../registry/environments.js';
4
4
  import { promptCheckbox } from '../../utils/prompts.js';
5
5
  function mergeEnvironmentIds(...lists) {
6
6
  return [...new Set(lists.flatMap((list) => list ?? []))].filter(isEnvironmentId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teamcast",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "YAML-driven CLI to design, validate, and generate multi-target agent teams for Claude Code and Codex",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,36 @@
1
+ id: docker
2
+ description: "Docker environment, auto-detected via Dockerfile"
3
+ detect_files:
4
+ - Dockerfile
5
+ - docker-compose.yml
6
+ - docker-compose.yaml
7
+ - compose.yml
8
+ - compose.yaml
9
+ policy_rules:
10
+ sandbox:
11
+ enabled: true
12
+ allow:
13
+ - "Bash(docker build *)"
14
+ - "Bash(docker run *)"
15
+ - "Bash(docker compose *)"
16
+ - "Bash(docker-compose *)"
17
+ - "Bash(docker ps *)"
18
+ - "Bash(docker logs *)"
19
+ - "Bash(docker images *)"
20
+ instruction_fragments:
21
+ docker_patterns:
22
+ content: |
23
+ This project uses Docker.
24
+ Use multi-stage builds to minimize image size.
25
+ Prefer alpine or slim base images where practical.
26
+ Use .dockerignore to exclude unnecessary files from build context.
27
+ requires_capabilities:
28
+ - read_files
29
+ docker_development:
30
+ content: |
31
+ Build images with `docker build -t <tag> .`.
32
+ Use `docker compose up` for multi-container setups.
33
+ Pin base image versions for reproducible builds.
34
+ Order Dockerfile instructions to maximize layer caching (dependencies before source code).
35
+ requires_capabilities:
36
+ - write_files
@@ -0,0 +1,40 @@
1
+ id: go
2
+ description: "Go environment, auto-detected via go.mod"
3
+ detect_files:
4
+ - go.mod
5
+ policy_rules:
6
+ sandbox:
7
+ enabled: true
8
+ allow:
9
+ - "Bash(go build *)"
10
+ - "Bash(go test *)"
11
+ - "Bash(go run *)"
12
+ - "Bash(go vet *)"
13
+ - "Bash(go mod *)"
14
+ - "Bash(go generate *)"
15
+ instruction_fragments:
16
+ go_code_patterns:
17
+ content: |
18
+ This is a Go project.
19
+ Follow standard Go conventions and idiomatic patterns.
20
+ Use gofmt/goimports for formatting.
21
+ Prefer short variable names in small scopes, descriptive names in larger scopes.
22
+ requires_capabilities:
23
+ - read_files
24
+ go_development:
25
+ content: |
26
+ Use `go build ./...` to compile all packages.
27
+ Use `go mod tidy` to clean up dependencies.
28
+ Handle errors explicitly — do not ignore returned errors.
29
+ Prefer returning errors over panicking.
30
+ requires_capabilities:
31
+ - write_files
32
+ go_testing:
33
+ content: |
34
+ Run tests with `go test ./...`.
35
+ Run a specific test: `go test -run TestName ./path/to/package`.
36
+ Use table-driven tests for multiple cases.
37
+ Always run tests after changes to verify nothing broke.
38
+ requires_capabilities:
39
+ - execute
40
+ - write_files
@@ -0,0 +1,41 @@
1
+ id: java
2
+ description: "Java environment, auto-detected via pom.xml or build.gradle"
3
+ detect_files:
4
+ - pom.xml
5
+ - build.gradle
6
+ - build.gradle.kts
7
+ policy_rules:
8
+ sandbox:
9
+ enabled: true
10
+ allow:
11
+ - "Bash(mvn *)"
12
+ - "Bash(gradle *)"
13
+ - "Bash(./gradlew *)"
14
+ - "Bash(java *)"
15
+ - "Bash(javac *)"
16
+ instruction_fragments:
17
+ java_code_patterns:
18
+ content: |
19
+ This is a Java project.
20
+ Follow standard Java naming conventions (camelCase for methods, PascalCase for classes).
21
+ Use appropriate access modifiers.
22
+ Prefer composition over inheritance where practical.
23
+ requires_capabilities:
24
+ - read_files
25
+ java_development:
26
+ content: |
27
+ If using Maven: `mvn compile` to build, `mvn package` to create artifacts.
28
+ If using Gradle: `./gradlew build` or `gradle build`.
29
+ Handle exceptions with specific types, not bare catch blocks.
30
+ Use try-with-resources for AutoCloseable resources.
31
+ requires_capabilities:
32
+ - write_files
33
+ java_testing:
34
+ content: |
35
+ Run tests with `mvn test` (Maven) or `./gradlew test` (Gradle).
36
+ Run a specific test: `mvn -Dtest=TestClassName test` or `./gradlew test --tests TestClassName`.
37
+ Use JUnit 5 annotations. Follow existing test patterns in the project.
38
+ Always run tests after changes to verify nothing broke.
39
+ requires_capabilities:
40
+ - execute
41
+ - write_files
@@ -0,0 +1,39 @@
1
+ id: node
2
+ description: "Node.js environment, auto-detected via package.json"
3
+ detect_files:
4
+ - package.json
5
+ policy_rules:
6
+ sandbox:
7
+ enabled: true
8
+ allow:
9
+ - "Bash(npm run *)"
10
+ - "Bash(npm test *)"
11
+ - "Bash(npx *)"
12
+ - "Bash(npm install)"
13
+ - "Bash(node *)"
14
+ instruction_fragments:
15
+ node_code_patterns:
16
+ content: |
17
+ This is a Node.js project.
18
+ Use ESM module syntax (import/export). All relative imports must use .js extensions.
19
+ Prefer named exports over default exports.
20
+ Use TypeScript strict mode when tsconfig.json is present.
21
+ requires_capabilities:
22
+ - read_files
23
+ node_development:
24
+ content: |
25
+ Install dependencies with `npm install`.
26
+ Use `npm run <script>` to execute package.json scripts.
27
+ Prefer async/await over raw Promises or callbacks.
28
+ Handle errors at system boundaries. Use typed error classes where the project defines them.
29
+ requires_capabilities:
30
+ - write_files
31
+ node_testing:
32
+ content: |
33
+ Run tests with `npm test`.
34
+ Run a specific test file with `npx vitest run <path>` (vitest) or `npx jest <path>` (jest).
35
+ Always run tests after making changes to verify nothing broke.
36
+ Follow existing test patterns: check the tests/ directory for conventions before writing new tests.
37
+ requires_capabilities:
38
+ - execute
39
+ - write_files
@@ -0,0 +1,41 @@
1
+ id: python
2
+ description: "Python environment, auto-detected via pyproject.toml or requirements.txt"
3
+ detect_files:
4
+ - pyproject.toml
5
+ - requirements.txt
6
+ - setup.py
7
+ policy_rules:
8
+ sandbox:
9
+ enabled: true
10
+ allow:
11
+ - "Bash(pytest *)"
12
+ - "Bash(python -m pytest *)"
13
+ - "Bash(uv run *)"
14
+ - "Bash(poetry run *)"
15
+ - "Bash(python *)"
16
+ instruction_fragments:
17
+ python_code_patterns:
18
+ content: |
19
+ This is a Python project.
20
+ Follow PEP 8 style conventions.
21
+ Use type hints for function signatures and class attributes.
22
+ Prefer pathlib.Path over os.path for file operations.
23
+ requires_capabilities:
24
+ - read_files
25
+ python_development:
26
+ content: |
27
+ If using poetry: `poetry install` and `poetry run <cmd>`. If using uv: `uv sync` and `uv run <cmd>`.
28
+ Otherwise use pip and virtualenv.
29
+ Use structured logging (logging module) instead of print statements.
30
+ Handle exceptions with specific types, not bare except clauses.
31
+ requires_capabilities:
32
+ - write_files
33
+ python_testing:
34
+ content: |
35
+ Run tests with `pytest`. If using poetry or uv, prefix with `poetry run` or `uv run`.
36
+ Run a specific test: `pytest <path>::<test_name>`.
37
+ Always run tests after changes. Follow existing test patterns in the tests/ directory.
38
+ Use fixtures for shared setup. Prefer parametrize for similar test cases.
39
+ requires_capabilities:
40
+ - execute
41
+ - write_files
@@ -0,0 +1,39 @@
1
+ id: ruby
2
+ description: "Ruby environment, auto-detected via Gemfile"
3
+ detect_files:
4
+ - Gemfile
5
+ policy_rules:
6
+ sandbox:
7
+ enabled: true
8
+ allow:
9
+ - "Bash(bundle *)"
10
+ - "Bash(rake *)"
11
+ - "Bash(rspec *)"
12
+ - "Bash(ruby *)"
13
+ - "Bash(rails *)"
14
+ instruction_fragments:
15
+ ruby_code_patterns:
16
+ content: |
17
+ This is a Ruby project.
18
+ Follow Ruby style conventions (snake_case for methods/variables, PascalCase for classes).
19
+ Use frozen string literal comments where the project follows that convention.
20
+ Prefer blocks and enumerators over manual loops.
21
+ requires_capabilities:
22
+ - read_files
23
+ ruby_development:
24
+ content: |
25
+ Install dependencies with `bundle install`.
26
+ Use `bundle exec` to run commands in the context of the bundle.
27
+ Prefer keyword arguments for methods with multiple optional parameters.
28
+ Handle errors with specific exception classes.
29
+ requires_capabilities:
30
+ - write_files
31
+ ruby_testing:
32
+ content: |
33
+ Run tests with `bundle exec rspec` (RSpec) or `bundle exec rake test` (Minitest).
34
+ Run a specific test: `bundle exec rspec path/to/spec.rb:LINE`.
35
+ Follow existing test patterns. Use shared examples for reusable test behaviors.
36
+ Always run tests after changes to verify nothing broke.
37
+ requires_capabilities:
38
+ - execute
39
+ - write_files
@@ -0,0 +1,41 @@
1
+ id: rust
2
+ description: "Rust environment, auto-detected via Cargo.toml"
3
+ detect_files:
4
+ - Cargo.toml
5
+ policy_rules:
6
+ sandbox:
7
+ enabled: true
8
+ allow:
9
+ - "Bash(cargo build *)"
10
+ - "Bash(cargo test *)"
11
+ - "Bash(cargo run *)"
12
+ - "Bash(cargo clippy *)"
13
+ - "Bash(cargo fmt *)"
14
+ - "Bash(cargo check *)"
15
+ - "Bash(rustfmt *)"
16
+ instruction_fragments:
17
+ rust_code_patterns:
18
+ content: |
19
+ This is a Rust project.
20
+ Follow Rust idioms: prefer ownership over borrowing when practical.
21
+ Use clippy lints to catch common mistakes.
22
+ Prefer Result/Option over panicking.
23
+ requires_capabilities:
24
+ - read_files
25
+ rust_development:
26
+ content: |
27
+ Use `cargo build` to compile the project.
28
+ Use `cargo check` for fast feedback without full compilation.
29
+ Run `cargo clippy` before committing to catch lint issues.
30
+ Use `cargo fmt` to format code consistently.
31
+ requires_capabilities:
32
+ - write_files
33
+ rust_testing:
34
+ content: |
35
+ Run tests with `cargo test`.
36
+ Run a specific test: `cargo test test_name`.
37
+ Use `#[cfg(test)]` modules for unit tests within source files.
38
+ Always run tests after changes to verify nothing broke.
39
+ requires_capabilities:
40
+ - execute
41
+ - write_files
@@ -0,0 +1,31 @@
1
+ id: terraform
2
+ description: "Terraform environment, auto-detected via main.tf"
3
+ detect_files:
4
+ - main.tf
5
+ - terraform.tf
6
+ policy_rules:
7
+ sandbox:
8
+ enabled: true
9
+ allow:
10
+ - "Bash(terraform init *)"
11
+ - "Bash(terraform plan *)"
12
+ - "Bash(terraform validate *)"
13
+ - "Bash(terraform fmt *)"
14
+ - "Bash(terraform state *)"
15
+ instruction_fragments:
16
+ terraform_patterns:
17
+ content: |
18
+ This project uses Terraform for infrastructure as code.
19
+ Follow HCL conventions: use snake_case for resource names and variables.
20
+ Organize configuration into logical files (main.tf, variables.tf, outputs.tf).
21
+ Use modules for reusable infrastructure components.
22
+ requires_capabilities:
23
+ - read_files
24
+ terraform_development:
25
+ content: |
26
+ Run `terraform init` to initialize providers and modules.
27
+ Run `terraform plan` to preview changes before applying.
28
+ Run `terraform validate` and `terraform fmt` before committing.
29
+ Never apply changes without reviewing the plan first.
30
+ requires_capabilities:
31
+ - write_files