diff --git a/.agents/skills/architecture-analysis/SKILL.md b/.agents/skills/architecture-analysis/SKILL.md index 5da2dfc..e8ad702 100644 --- a/.agents/skills/architecture-analysis/SKILL.md +++ b/.agents/skills/architecture-analysis/SKILL.md @@ -1,6 +1,6 @@ --- name: Architecture Analysis -description: Deep codebase analysis: module boundaries, dependency graph, design patterns. +description: "Deep codebase analysis: module boundaries, dependency graph, design patterns." --- # Architecture Analysis diff --git a/.agents/skills/clean-code/SKILL.md b/.agents/skills/clean-code/SKILL.md index 2c241aa..e7867eb 100644 --- a/.agents/skills/clean-code/SKILL.md +++ b/.agents/skills/clean-code/SKILL.md @@ -1,6 +1,6 @@ --- name: Clean Code -description: Follow clean code principles: small functions, meaningful names, no duplication. +description: "Follow clean code principles: small functions, meaningful names, no duplication." --- # Clean Code diff --git a/.agents/skills/triage/SKILL.md b/.agents/skills/triage/SKILL.md index 74ddbec..e1e0ec3 100644 --- a/.agents/skills/triage/SKILL.md +++ b/.agents/skills/triage/SKILL.md @@ -1,6 +1,6 @@ --- name: Triage -description: Assess incoming requests: classify priority, estimate scope, identify blockers. +description: "Assess incoming requests: classify priority, estimate scope, identify blockers." --- # Triage diff --git a/src/application/validate-team.ts b/src/application/validate-team.ts index 12f8c74..eec45b9 100644 --- a/src/application/validate-team.ts +++ b/src/application/validate-team.ts @@ -16,6 +16,7 @@ import { resolveEnvironmentPolicies, } from '../core/environment-resolver.js'; import { checkManifestRegistry } from '../validator/checks/manifest-registry.js'; +import { builtinResourceLoader } from '../registry/resource-loader.js'; export interface TeamValidationSummary { schemaErrors: Array<{ path: string; message: string }>; @@ -36,6 +37,7 @@ export function evaluateTeam( }; } + if (options?.cwd) builtinResourceLoader.loadUserResources(options.cwd); const rawManifest = applyDefaults(schemaResult.data); const resolvedManifest = options?.cwd ? resolveEnvironmentPolicies(rawManifest, options.cwd) : rawManifest; diff --git a/src/cli/manage.ts b/src/cli/manage.ts index 6883abc..997abbe 100644 --- a/src/cli/manage.ts +++ b/src/cli/manage.ts @@ -11,6 +11,7 @@ import { printError, printHeader, printCommandSuccess, + printNextSteps, } from '../utils/chalk-helpers.js'; import type { CoreAgent, @@ -364,6 +365,10 @@ export async function runAddAgentCommand(name: string, options: AddAgentOptions) applyManifestChanges(cwd, manifest, targetName, nextTeam); printCommandSuccess(`Agent "${name}" added and configuration regenerated`); + printNextSteps([ + `Open ${chalk.bold('teamcast.yaml')} and fill in agent instructions based on ${chalk.yellow('// TODO')} comments`, + `Run ${chalk.bold('teamcast generate')} to apply your changes`, + ]); } export async function runCreateSkillCommand(name: string, options: TargetedOption): Promise { @@ -570,7 +575,11 @@ async function promptAgentConfig(name: string, targetContext: TargetContext): Pr instructions: [ { kind: 'behavior', - content: `You are ${name}. Focus on the responsibilities described in your role and use your allowed tools appropriately.`, + 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."`, + }, + { + kind: 'workflow', + 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.`, }, ], }; diff --git a/src/core/environment-resolver.ts b/src/core/environment-resolver.ts index b2a9a07..7b09135 100644 --- a/src/core/environment-resolver.ts +++ b/src/core/environment-resolver.ts @@ -3,7 +3,7 @@ import type { CoreTeam } from './types.js'; import type { PoliciesConfig, TeamCastManifest, TargetConfig } from '../manifest/types.js'; import { TARGET_NAMES, getManifestTargetConfig, setManifestTargetConfig } from '../manifest/targets.js'; import { getEnvironment, detectEnvironments } from '../registry/environments.js'; -import { isEnvironmentId } from '../registry/types.js'; +import { isEnvironmentId } from '../registry/environments.js'; import type { CapabilityToolMap, EnvironmentId, EnvironmentInstruction } from '../registry/types.js'; import { agentHasCapability } from './capability-resolver.js'; import type { TargetContext } from '../renderers/target-context.js'; @@ -98,31 +98,30 @@ function mergePoliciesSimple(base: PoliciesConfig, extra: PoliciesConfig): Polic } : undefined, hooks: base.hooks || extra.hooks - ? { - pre_tool_use: [...(base.hooks?.pre_tool_use ?? []), ...(extra.hooks?.pre_tool_use ?? [])].length > 0 - ? [...(base.hooks?.pre_tool_use ?? []), ...(extra.hooks?.pre_tool_use ?? [])] - : undefined, - post_tool_use: [...(base.hooks?.post_tool_use ?? []), ...(extra.hooks?.post_tool_use ?? [])].length > 0 - ? [...(base.hooks?.post_tool_use ?? []), ...(extra.hooks?.post_tool_use ?? [])] - : undefined, - notification: [...(base.hooks?.notification ?? []), ...(extra.hooks?.notification ?? [])].length > 0 - ? [...(base.hooks?.notification ?? []), ...(extra.hooks?.notification ?? [])] - : undefined, - } + ? (() => { + const pre = [...(base.hooks?.pre_tool_use ?? []), ...(extra.hooks?.pre_tool_use ?? [])]; + const post = [...(base.hooks?.post_tool_use ?? []), ...(extra.hooks?.post_tool_use ?? [])]; + const notif = [...(base.hooks?.notification ?? []), ...(extra.hooks?.notification ?? [])]; + return { + pre_tool_use: pre.length > 0 ? pre : undefined, + post_tool_use: post.length > 0 ? post : undefined, + notification: notif.length > 0 ? notif : undefined, + }; + })() : undefined, network: base.network || extra.network - ? { - allowed_domains: [...new Set([ + ? (() => { + const domains = [...new Set([ ...(base.network?.allowed_domains ?? []), ...(extra.network?.allowed_domains ?? []), - ])].length > 0 - ? [...new Set([...(base.network?.allowed_domains ?? []), ...(extra.network?.allowed_domains ?? [])])] - : undefined, - } - : undefined, - assertions: [...(base.assertions ?? []), ...(extra.assertions ?? [])].length > 0 - ? [...(base.assertions ?? []), ...(extra.assertions ?? [])] + ])]; + return { allowed_domains: domains.length > 0 ? domains : undefined }; + })() : undefined, + assertions: (() => { + const merged = [...(base.assertions ?? []), ...(extra.assertions ?? [])]; + return merged.length > 0 ? merged : undefined; + })(), }; } diff --git a/src/generator/index.ts b/src/generator/index.ts index d16ee1c..52416de 100644 --- a/src/generator/index.ts +++ b/src/generator/index.ts @@ -10,11 +10,13 @@ import { resolveEnvironmentIds, resolveEnvironmentPolicies, } from '../core/environment-resolver.js'; +import { builtinResourceLoader } from '../registry/resource-loader.js'; export function generate( manifest: TeamCastManifest, options: BuildGeneratedOutputsOptions, ) { + builtinResourceLoader.loadUserResources(options.cwd); const rawManifest = resolveEnvironmentPolicies(applyDefaults(manifest), options.cwd); const envIds = resolveEnvironmentIds(rawManifest, options.cwd); diff --git a/src/registry/environment-schema.ts b/src/registry/environment-schema.ts new file mode 100644 index 0000000..15ed65f --- /dev/null +++ b/src/registry/environment-schema.ts @@ -0,0 +1,101 @@ +// Environment YAML schema — parse and convert YAML environment definitions +// to runtime EnvironmentDef objects. + +import { existsSync } from 'fs'; +import { join } from 'path'; +import type { CapabilityId, EnvironmentDef, EnvironmentInstruction } from './types.js'; +import { isCapability } from './types.js'; + +// --- YAML shape (what users write in .yaml files) --- + +export interface EnvironmentYamlInstruction { + content: string; + requires_capabilities: string[]; +} + +export interface EnvironmentYaml { + id: string; + description: string; + detect_files?: string[]; + policy_rules: { + sandbox?: { enabled?: boolean }; + allow?: string[]; + }; + instruction_fragments: Record; +} + +// --- Validation --- + +export function parseEnvironmentYaml(raw: unknown): EnvironmentYaml { + if (!raw || typeof raw !== 'object') { + throw new Error('Environment definition must be an object'); + } + + const obj = raw as Record; + + if (typeof obj.id !== 'string' || !obj.id) { + throw new Error('Environment definition requires a non-empty "id" field'); + } + if (typeof obj.description !== 'string') { + throw new Error(`Environment "${obj.id}": "description" must be a string`); + } + + if (obj.detect_files !== undefined) { + if (!Array.isArray(obj.detect_files) || !obj.detect_files.every((f: unknown) => typeof f === 'string')) { + throw new Error(`Environment "${obj.id}": "detect_files" must be a string array`); + } + } + + if (!obj.policy_rules || typeof obj.policy_rules !== 'object') { + throw new Error(`Environment "${obj.id}": "policy_rules" must be an object`); + } + + const policies = obj.policy_rules as Record; + if (policies.allow !== undefined) { + if (!Array.isArray(policies.allow) || !policies.allow.every((a: unknown) => typeof a === 'string')) { + throw new Error(`Environment "${obj.id}": "policy_rules.allow" must be a string array`); + } + } + + if (!obj.instruction_fragments || typeof obj.instruction_fragments !== 'object') { + throw new Error(`Environment "${obj.id}": "instruction_fragments" must be an object`); + } + + return obj as unknown as EnvironmentYaml; +} + +// --- Conversion to runtime EnvironmentDef --- + +function toEnvironmentInstruction( + value: string | EnvironmentYamlInstruction, +): string | EnvironmentInstruction { + if (typeof value === 'string') return value; + return { + content: value.content, + requires_capabilities: value.requires_capabilities.filter(isCapability) as CapabilityId[], + }; +} + +export function environmentYamlToDef(yaml: EnvironmentYaml): EnvironmentDef { + const fragments: Record = {}; + for (const [key, value] of Object.entries(yaml.instruction_fragments)) { + fragments[key] = toEnvironmentInstruction(value); + } + + const def: EnvironmentDef = { + id: yaml.id, + description: yaml.description, + policyRules: { + sandbox: yaml.policy_rules.sandbox, + allow: yaml.policy_rules.allow, + }, + instructionFragments: fragments, + }; + + if (yaml.detect_files?.length) { + const files = yaml.detect_files; + def.detect = (cwd: string) => files.some((file) => existsSync(join(cwd, file))); + } + + return def; +} diff --git a/src/registry/environments.ts b/src/registry/environments.ts index 9f4a5b1..cbfcd46 100644 --- a/src/registry/environments.ts +++ b/src/registry/environments.ts @@ -1,111 +1,22 @@ -import { existsSync } from 'fs'; -import { join } from 'path'; -import type { EnvironmentDef, EnvironmentId } from './types.js'; +// Environment registry — delegates to ResourceLoader (YAML is the sole source). -const ENVIRONMENTS: Record = { - node: { - id: 'node', - description: 'Node.js environment, auto-detected via package.json', - detect: (cwd: string) => existsSync(join(cwd, 'package.json')), - policyRules: { - sandbox: { enabled: true }, - allow: [ - 'Bash(npm run *)', - 'Bash(npm test *)', - 'Bash(npx *)', - 'Bash(npm install)', - 'Bash(node *)', - ], - }, - instructionFragments: { - node_code_patterns: { - content: [ - 'This is a Node.js project.', - 'Use ESM module syntax (import/export). All relative imports must use .js extensions.', - 'Prefer named exports over default exports.', - 'Use TypeScript strict mode when tsconfig.json is present.', - ].join('\n'), - requires_capabilities: ['read_files'], - }, - node_development: { - content: [ - 'Install dependencies with `npm install`.', - 'Use `npm run