Skip to content

Commit 63fb88e

Browse files
authored
Merge pull request #8 from Katoshy/core/plugins-based-core
Refactor manifest composition around target-scoped plugins
2 parents d8d6c91 + 7d59e43 commit 63fb88e

69 files changed

Lines changed: 1937 additions & 1414 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "teamcast",
3-
"version": "0.9.2-f",
3+
"version": "0.9.3",
44
"description": "YAML-driven CLI to design, validate, and generate multi-target agent teams for Claude Code and Codex",
55
"type": "module",
66
"bin": {

schema/teamcast.schema.json

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,6 @@
4141
"codex": {
4242
"$ref": "#/definitions/TargetConfig"
4343
},
44-
"policies": {
45-
"$ref": "#/definitions/PoliciesConfig"
46-
},
47-
"settings": {
48-
"$ref": "#/definitions/GenerationSettings"
49-
},
5044
"preset_meta": {
5145
"$ref": "#/definitions/PresetMeta"
5246
}

src/application/team.ts

Lines changed: 74 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import type { CoreAgent, CoreTeam, ReasoningEffort } from '../core/types.js';
22
import type { AgentConfig, TeamCastManifest, TargetConfig } from '../manifest/types.js';
33
import type { TeamRoleName } from '../team-templates/roles.js';
44
import { createPolicies } from '../team-templates/policies.js';
5+
import { buildPresetManifest } from '../team-templates/presets.js';
56
import { createRoleAgent, getRoleRuntimeDefaults, isTeamRoleName } from '../team-templates/roles.js';
6-
import { applyPreset, loadPreset } from '../presets/index.js';
77
import { createManifestForTarget, normalizeManifest } from '../manifest/normalize.js';
88
import type { TargetContext } from '../renderers/target-context.js';
99
import { getTarget } from '../renderers/registry.js';
@@ -90,6 +90,70 @@ function cloneTargetConfig(
9090
})(),
9191
]),
9292
),
93+
policies: targetConfig.policies
94+
? {
95+
...targetConfig.policies,
96+
fragments: targetConfig.policies.fragments ? [...targetConfig.policies.fragments] : undefined,
97+
permissions: targetConfig.policies.permissions
98+
? {
99+
...targetConfig.policies.permissions,
100+
allow: targetConfig.policies.permissions.allow ? [...targetConfig.policies.permissions.allow] : undefined,
101+
ask: targetConfig.policies.permissions.ask ? [...targetConfig.policies.permissions.ask] : undefined,
102+
deny: targetConfig.policies.permissions.deny ? [...targetConfig.policies.permissions.deny] : undefined,
103+
rules: targetConfig.policies.permissions.rules
104+
? {
105+
allow: targetConfig.policies.permissions.rules.allow
106+
? [...targetConfig.policies.permissions.rules.allow]
107+
: undefined,
108+
ask: targetConfig.policies.permissions.rules.ask
109+
? [...targetConfig.policies.permissions.rules.ask]
110+
: undefined,
111+
deny: targetConfig.policies.permissions.rules.deny
112+
? [...targetConfig.policies.permissions.rules.deny]
113+
: undefined,
114+
}
115+
: undefined,
116+
}
117+
: undefined,
118+
sandbox: targetConfig.policies.sandbox
119+
? {
120+
...targetConfig.policies.sandbox,
121+
excluded_commands: targetConfig.policies.sandbox.excluded_commands
122+
? [...targetConfig.policies.sandbox.excluded_commands]
123+
: undefined,
124+
network: targetConfig.policies.sandbox.network
125+
? {
126+
allow_unix_sockets: targetConfig.policies.sandbox.network.allow_unix_sockets
127+
? [...targetConfig.policies.sandbox.network.allow_unix_sockets]
128+
: undefined,
129+
allow_local_binding: targetConfig.policies.sandbox.network.allow_local_binding,
130+
}
131+
: undefined,
132+
}
133+
: undefined,
134+
hooks: targetConfig.policies.hooks
135+
? {
136+
pre_tool_use: targetConfig.policies.hooks.pre_tool_use?.map((entry) => ({ ...entry })),
137+
post_tool_use: targetConfig.policies.hooks.post_tool_use?.map((entry) => ({ ...entry })),
138+
notification: targetConfig.policies.hooks.notification?.map((entry) => ({ ...entry })),
139+
}
140+
: undefined,
141+
network: targetConfig.policies.network
142+
? {
143+
allowed_domains: targetConfig.policies.network.allowed_domains
144+
? [...targetConfig.policies.network.allowed_domains]
145+
: undefined,
146+
}
147+
: undefined,
148+
assertions: targetConfig.policies.assertions ? [...targetConfig.policies.assertions] : undefined,
149+
}
150+
: undefined,
151+
settings: targetConfig.settings
152+
? {
153+
generate_docs: targetConfig.settings.generate_docs,
154+
generate_local_settings: targetConfig.settings.generate_local_settings,
155+
}
156+
: undefined,
93157
};
94158
}
95159

@@ -108,16 +172,21 @@ export function buildManifestFromPreset(
108172
projectName: string,
109173
selection: InitTargetSelection = 'claude',
110174
): TeamCastManifest {
111-
const preset = loadPreset(presetName);
112-
const manifest = applyPreset(preset, projectName);
175+
const presetManifest = buildPresetManifest(presetName);
176+
const manifest: TeamCastManifest = {
177+
...presetManifest,
178+
project: {
179+
...presetManifest.project,
180+
name: projectName,
181+
},
182+
};
113183
const sourceTarget = manifest.claude ?? manifest.codex;
114184
const targetNames = resolveInitTargets(selection);
115185

116186
return {
117187
version: '2',
118188
project: { ...manifest.project },
119-
policies: manifest.policies ? { ...manifest.policies } : undefined,
120-
settings: manifest.settings ? { ...manifest.settings } : undefined,
189+
plugins: manifest.plugins ? [...manifest.plugins] : undefined,
121190
preset_meta: manifest.preset_meta ? { ...manifest.preset_meta } : undefined,
122191
claude: targetNames.includes('claude')
123192
? cloneTargetConfig(manifest.claude ?? sourceTarget, {

src/application/validate-team.ts

Lines changed: 21 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,27 @@
1-
import chalk from 'chalk';
21
import type { TeamCastManifest } from '../types/manifest.js';
32
import { applyDefaults } from '../manifest/defaults.js';
3+
import { getManifestTargetConfig, isManifestTargetName } from '../manifest/targets.js';
44
import { validateSchema } from '../manifest/schema-validator.js';
55
import { normalizeManifest } from '../manifest/normalize.js';
66
import { runValidation } from '../validator/index.js';
7-
import { hasErrors } from '../validator/reporter.js';
7+
import {
8+
hasErrors,
9+
printManifestValidationSummary,
10+
} from '../validator/reporter.js';
811
import type { ValidationResult } from '../validator/types.js';
912
import { getTarget, getRegisteredTargetNames } from '../renderers/registry.js';
13+
import { injectEnvironmentPolicies } from '../plugins/inject.js';
1014

1115
export interface TeamValidationSummary {
1216
schemaErrors: Array<{ path: string; message: string }>;
1317
validationResults: ValidationResult[];
1418
policyAssertionCount?: number;
1519
}
1620

17-
export function evaluateTeam(manifest: TeamCastManifest): TeamValidationSummary {
21+
export function evaluateTeam(
22+
manifest: TeamCastManifest,
23+
options?: { cwd?: string },
24+
): TeamValidationSummary {
1825
const schemaResult = validateSchema(manifest);
1926

2027
if (!schemaResult.valid) {
@@ -25,15 +32,21 @@ export function evaluateTeam(manifest: TeamCastManifest): TeamValidationSummary
2532
}
2633

2734
const rawManifest = applyDefaults(schemaResult.data);
28-
const rawManifestRecord = rawManifest as unknown as Record<string, unknown>;
29-
const activeTargets = getRegisteredTargetNames().filter((targetName) => rawManifestRecord[targetName]);
35+
const resolvedManifest = options?.cwd ? injectEnvironmentPolicies(rawManifest, options.cwd) : rawManifest;
36+
const activeTargets = getRegisteredTargetNames().filter(
37+
(targetName) =>
38+
isManifestTargetName(targetName) &&
39+
Boolean(getManifestTargetConfig(resolvedManifest, targetName)),
40+
);
3041
const validationResults: ValidationResult[] = [];
42+
let policyAssertionCount = 0;
3143

3244
for (const targetName of activeTargets) {
3345
const targetContext = getTarget(targetName);
34-
const team = normalizeManifest(rawManifest, targetContext);
46+
const team = normalizeManifest(resolvedManifest, targetContext);
3547
const targetResults = runValidation(team, targetContext);
3648
const prefix = activeTargets.length > 1 ? `[${targetName}] ` : '';
49+
policyAssertionCount += team.policies?.assertions?.length ?? 0;
3750
validationResults.push(
3851
...targetResults.map((result) => ({
3952
...result,
@@ -45,7 +58,7 @@ export function evaluateTeam(manifest: TeamCastManifest): TeamValidationSummary
4558
return {
4659
schemaErrors: [],
4760
validationResults,
48-
policyAssertionCount: rawManifest.policies?.assertions?.length ?? 0,
61+
policyAssertionCount,
4962
};
5063
}
5164

@@ -54,90 +67,5 @@ export function teamHasBlockingIssues(summary: TeamValidationSummary): boolean {
5467
}
5568

5669
export function printManifestValidation(summary: TeamValidationSummary): void {
57-
if (summary.schemaErrors.length > 0) {
58-
console.log('');
59-
console.log(` ${chalk.red('[!!]')} ${chalk.bold('Schema')} ${chalk.dim('—')} ${chalk.red('manifest structure invalid')}`);
60-
for (const error of summary.schemaErrors) {
61-
console.log(` ${chalk.red('[error]')} ${error.path}: ${error.message}`);
62-
}
63-
console.log('');
64-
return;
65-
}
66-
67-
console.log('');
68-
console.log(chalk.bold('Validation results:'));
69-
console.log(` ${chalk.green('[ok]')} ${chalk.bold('Schema')} ${chalk.dim('—')} ${chalk.dim('manifest structure valid')}`);
70-
71-
printCategoryRows(summary.validationResults, summary.policyAssertionCount);
72-
}
73-
74-
function printCategoryRows(results: ValidationResult[], policyAssertionCount?: number): void {
75-
const categoryOrder = [
76-
{ label: 'Handoff graph', okDescription: 'delegation paths verified' },
77-
{ label: 'Tool conflicts', okDescription: 'no allow/deny overlaps' },
78-
{ label: 'Role separation', okDescription: 'roles match capabilities' },
79-
{ label: 'Security', okDescription: 'sandbox and permissions checked' },
80-
{ label: 'Instruction blocks', okDescription: 'all blocks valid' },
81-
{ label: 'policy', okDescription: 'all assertions passed' },
82-
];
83-
84-
const byCategory = new Map<string, ValidationResult[]>();
85-
for (const result of results) {
86-
const existing = byCategory.get(result.category) ?? [];
87-
byCategory.set(result.category, [...existing, result]);
88-
}
89-
90-
for (const meta of categoryOrder) {
91-
const categoryResults = byCategory.get(meta.label) ?? [];
92-
const errors = categoryResults.filter((result) => result.severity === 'error');
93-
const warnings = categoryResults.filter((result) => result.severity === 'warning');
94-
95-
if (errors.length > 0) {
96-
const desc = chalk.red(`${errors.length} error${errors.length !== 1 ? 's' : ''}`);
97-
const extra =
98-
warnings.length > 0
99-
? `, ${chalk.yellow(`${warnings.length} warning${warnings.length !== 1 ? 's' : ''}`)}`
100-
: '';
101-
console.log(` ${chalk.red('[!!]')} ${chalk.bold(meta.label)} ${chalk.dim('—')} ${desc}${extra}`);
102-
for (const result of [...errors, ...warnings]) {
103-
const prefix = result.severity === 'error' ? chalk.red(' [error]') : chalk.yellow(' [warn]');
104-
console.log(` ${prefix} ${result.message}`);
105-
}
106-
} else if (warnings.length > 0) {
107-
const desc = chalk.yellow(`${warnings.length} warning${warnings.length !== 1 ? 's' : ''}`);
108-
console.log(` ${chalk.yellow('[--]')} ${chalk.bold(meta.label)} ${chalk.dim('—')} ${desc}`);
109-
for (const result of warnings) {
110-
console.log(` ${chalk.yellow(' [warn]')} ${result.message}`);
111-
}
112-
} else {
113-
let okDesc: string;
114-
if (meta.label === 'policy' && policyAssertionCount !== undefined) {
115-
okDesc =
116-
policyAssertionCount === 0
117-
? chalk.dim('no assertions defined')
118-
: chalk.dim(`${policyAssertionCount} rule${policyAssertionCount !== 1 ? 's' : ''} passed`);
119-
} else {
120-
okDesc = chalk.dim(meta.okDescription);
121-
}
122-
console.log(` ${chalk.green('[ok]')} ${chalk.bold(meta.label)} ${chalk.dim('—')} ${okDesc}`);
123-
}
124-
}
125-
126-
console.log('');
127-
128-
const totalErrors = results.filter((result) => result.severity === 'error').length;
129-
const totalWarnings = results.filter((result) => result.severity === 'warning').length;
130-
131-
if (totalErrors === 0 && totalWarnings === 0) {
132-
console.log(chalk.green('All checks passed.'));
133-
} else {
134-
const parts: string[] = [];
135-
if (totalErrors > 0) parts.push(chalk.red(`${totalErrors} error${totalErrors !== 1 ? 's' : ''}`));
136-
if (totalWarnings > 0) {
137-
parts.push(chalk.yellow(`${totalWarnings} warning${totalWarnings !== 1 ? 's' : ''}`));
138-
}
139-
console.log(`${parts.join(', ')} found.`);
140-
}
141-
142-
console.log('');
70+
printManifestValidationSummary(summary);
14371
}

src/cli/diff.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import type { Command } from 'commander';
21
import chalk from 'chalk';
32
import { readManifest, ManifestError } from '../manifest/reader.js';
43
import { diffManifest } from '../diff/index.js';
54
import { printHeader, printCommandSuccess, printDim } from '../utils/chalk-helpers.js';
5+
import { abortCli } from './errors.js';
66

77
export function runDiffCommand(): void {
88
const cwd = process.cwd();
@@ -13,7 +13,7 @@ export function runDiffCommand(): void {
1313
} catch (err) {
1414
if (err instanceof ManifestError) {
1515
console.error(chalk.red(`\nError: ${err.message}`));
16-
process.exit(1);
16+
abortCli(1);
1717
}
1818
throw err;
1919
}
@@ -59,9 +59,4 @@ export function runDiffCommand(): void {
5959
console.log('');
6060
}
6161

62-
export function registerDiffCommand(program: Command): void {
63-
program
64-
.command('diff')
65-
.description('Show differences between teamcast.yaml and generated files on disk')
66-
.action(runDiffCommand);
67-
}
62+
export { registerDiffCommand } from './registrars/diff.js';

src/cli/errors.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
export class CLIAbortError extends Error {
2+
public readonly exitCode: number;
3+
4+
constructor(exitCode = 1) {
5+
super(`CLI aborted with exit code ${exitCode}`);
6+
this.name = 'CLIAbortError';
7+
this.exitCode = exitCode;
8+
}
9+
}
10+
11+
export function abortCli(exitCode = 1): never {
12+
throw new CLIAbortError(exitCode);
13+
}
14+
15+
export function isCLIAbortError(error: unknown): error is CLIAbortError {
16+
return error instanceof CLIAbortError;
17+
}

src/cli/explain.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import type { Command } from 'commander';
21
import chalk from 'chalk';
32
import { readManifest, ManifestError } from '../manifest/reader.js';
43
import { applyDefaults } from '../manifest/defaults.js';
54
import { normalizeManifest } from '../manifest/normalize.js';
65
import { buildExplanation } from '../explainer/index.js';
76
import { getRegisteredTargetNames, getTarget } from '../renderers/registry.js';
7+
import { abortCli } from './errors.js';
88

99
export function runExplainCommand(): void {
1010
const cwd = process.cwd();
@@ -18,7 +18,7 @@ export function runExplainCommand(): void {
1818
if (err.details?.length) {
1919
for (const d of err.details) console.error(chalk.dim(` ${d}`));
2020
}
21-
process.exit(1);
21+
abortCli(1);
2222
}
2323
throw err;
2424
}
@@ -45,9 +45,4 @@ export function runExplainCommand(): void {
4545
}
4646
}
4747

48-
export function registerExplainCommand(program: Command): void {
49-
program
50-
.command('explain')
51-
.description('Show a human-readable view of the agent team architecture')
52-
.action(runExplainCommand);
53-
}
48+
export { registerExplainCommand } from './registrars/explain.js';

0 commit comments

Comments
 (0)