Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 25 additions & 68 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ env:
IMAGE_NAME: ${{ github.repository }}

jobs:
build:
name: Build (Node.js)
ci:
name: Build & Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
Expand All @@ -33,86 +33,45 @@ jobs:
with:
version: 9

- name: Install dependencies
run: pnpm install

- name: Build
run: pnpm run build

- name: Upload build artifacts
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: dist
path: packages/*/dist
retention-days: 1

lint:
name: Lint & Format (Biome)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Get pnpm store directory
id: pnpm-cache
shell: bash
run: |
STORE_PATH=$(pnpm store path --silent 2>/dev/null || true)
STORE_PATH="${STORE_PATH:-${XDG_DATA_HOME:-$HOME/.local/share}/pnpm/store}"
echo "store=$STORE_PATH" >> $GITHUB_OUTPUT

- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
node-version: '22'

- uses: pnpm/action-setup@9fd676a19091d4595eefd76e4bd31c97133911f1 # v4.2.0
- uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
version: 9
path: ${{ steps.pnpm-cache.outputs.store }}
key: pnpm-store-${{ hashFiles('pnpm-lock.yaml', 'package.json') }}
restore-keys: pnpm-store-

- name: Install dependencies
run: pnpm install
run: pnpm install --no-frozen-lockfile

- name: Biome check (lint + format)
- name: Lint and Format Check
run: pnpm run check

typecheck:
name: TypeCheck (TypeScript)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1

- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
node-version: '22'

- uses: pnpm/action-setup@9fd676a19091d4595eefd76e4bd31c97133911f1 # v4.2.0
with:
version: 9

- name: Install dependencies
run: pnpm install
- name: Build
run: pnpm run build

- name: TypeScript type check
- name: Type check
run: pnpm run typecheck

test:
name: Test (Node.js)
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1

- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
node-version: '22'

- uses: pnpm/action-setup@9fd676a19091d4595eefd76e4bd31c97133911f1 # v4.2.0
with:
version: 9

- name: Install dependencies
run: pnpm install

- name: Build packages
run: pnpm run build

- name: Run tests with coverage
run: pnpm run test:coverage

- name: Report Coveralls
uses: coverallsapp/github-action@648a8eb78e6d50909eff900e4ec85cab4524a45b # v2.3.6

- name: Upload build artifacts
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: dist
path: packages/*/dist
retention-days: 1

docker:
name: Docker Build
runs-on: ubuntu-latest
Expand All @@ -134,8 +93,6 @@ jobs:
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: .
# Only build amd64 for PRs to avoid QEMU memory issues
# Full multi-arch build happens in CD workflow
platforms: linux/amd64
push: false
tags: ${{ steps.meta.outputs.tags }}
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,8 @@ coverage/
.DS_Store

node-compile-cache/

# Repositories
otterfall/
game-generator/
python/
6 changes: 4 additions & 2 deletions packages/agentic-control/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,9 @@
],
"scripts": {
"dev:cli": "tsx src/cli.ts",
"build": "tsc",
"dev": "tsc --watch",
"build": "tsup",
"build:types": "tsc --emitDeclarationOnly",
"dev": "tsup --watch",
"lint": "biome lint src/ tests/",
"lint:fix": "biome lint --write src/ tests/",
"format": "biome format --write src/ tests/",
Expand Down Expand Up @@ -115,6 +116,7 @@
"devDependencies": {
"@ai-sdk/anthropic": "^3.0.1",
"@types/node": "^22.15.0",
"tsup": "^8.5.0",
"tsx": "4.21.0",
"typescript": "^5.7.0",
"vitest": "^4.0.14",
Expand Down
172 changes: 172 additions & 0 deletions packages/agentic-control/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ import type { Agent, Result } from './core/types.js';
import { Fleet } from './fleet/index.js';
import { HandoffManager } from './handoff/index.js';
import { VERSION } from './index.js';
import {
DEFAULT_ROLES,
executeSageRole,
findRoleByTrigger,
getDefaultRoleIds,
getEffectiveRole,
listRoles,
} from './roles/index.js';
import { AIAnalyzer } from './triage/index.js';

const program = new Command();
Expand Down Expand Up @@ -932,6 +940,170 @@ triageCmd
}
});

// ============================================
// Roles Commands
// ============================================

const rolesCmd = program.command('roles').description('Configurable AI agent personas');

rolesCmd
.command('list')
.description('List available roles')
.option('--json', 'Output as JSON')
.action((opts) => {
const cfg = getConfig();
const roles = listRoles(cfg.roles);

if (opts.json) {
output(roles, true);
} else {
console.log('=== Available Roles ===\n');
for (const role of roles) {
const triggers = role.triggers
.filter((t) => t.type === 'comment')
.map((t) => (t as { pattern: string }).pattern)
.join(', ');

console.log(`${role.icon} ${role.name} (${role.id})`);
console.log(` ${role.description}`);
console.log(` Triggers: ${triggers || 'manual only'}`);
console.log(
` Capabilities: ${role.capabilities.slice(0, 3).join(', ')}${role.capabilities.length > 3 ? '...' : ''}`
);
console.log();
}
}
});

rolesCmd
.command('info')
.description('Show detailed information about a role')
.argument('<role-id>', 'Role ID (sage, harvester, curator, reviewer, fixer, delegator)')
.option('--json', 'Output as JSON')
.action((roleId, opts) => {
const cfg = getConfig();
const role = getEffectiveRole(roleId, cfg.roles);

if (!role) {
console.error(`❌ Role not found: ${roleId}`);
console.error(`Available roles: ${getDefaultRoleIds().join(', ')}`);
process.exit(1);
}

if (opts.json) {
output(role, true);
} else {
console.log(`\n${role.icon} ${role.name}\n`);
console.log(`ID: ${role.id}`);
console.log(`Description: ${role.description}`);
console.log(`\nCapabilities:`);
for (const cap of role.capabilities) {
console.log(` • ${cap}`);
}
console.log(`\nTriggers:`);
for (const trigger of role.triggers) {
if (trigger.type === 'comment') {
console.log(` 💬 Comment: ${(trigger as { pattern: string }).pattern}`);
} else if (trigger.type === 'schedule') {
console.log(` ⏰ Schedule: ${(trigger as { cron: string }).cron}`);
} else if (trigger.type === 'event') {
console.log(` 🎯 Events: ${(trigger as { events: string[] }).events.join(', ')}`);
Comment on lines +1005 to +1010
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The type assertions like (trigger as { pattern: string }).pattern are redundant. The RoleTrigger type is a discriminated union based on the type property. When you check trigger.type === 'comment', TypeScript's control flow analysis automatically narrows the type of trigger to { type: 'comment'; pattern: string }, so you can safely access trigger.pattern without a cast. Removing these casts makes the code cleaner and relies on the strength of the type system.

Suggested change
if (trigger.type === 'comment') {
console.log(` 💬 Comment: ${(trigger as { pattern: string }).pattern}`);
} else if (trigger.type === 'schedule') {
console.log(` ⏰ Schedule: ${(trigger as { cron: string }).cron}`);
} else if (trigger.type === 'event') {
console.log(` 🎯 Events: ${(trigger as { events: string[] }).events.join(', ')}`);
if (trigger.type === 'comment') {
console.log(` 💬 Comment: ${trigger.pattern}`);
} else if (trigger.type === 'schedule') {
console.log(` ⏰ Schedule: ${trigger.cron}`);
} else if (trigger.type === 'event') {
console.log(` 🎯 Events: ${trigger.events.join(', ')}`);

} else {
console.log(` 🖐️ Manual`);
}
}
console.log(`\nPermissions:`);
console.log(` Can spawn agents: ${role.canSpawnAgents ? '✅' : '❌'}`);
console.log(` Can modify repo: ${role.canModifyRepo ? '✅' : '❌'}`);
console.log(` Can merge PRs: ${role.canMerge ? '✅' : '❌'}`);
console.log(`\nDefault Model: ${role.defaultModel || 'provider default'}`);
console.log(`Temperature: ${role.temperature ?? 0.3}`);
console.log(`\nSystem Prompt (first 500 chars):`);
console.log(` ${role.systemPrompt.slice(0, 500).replace(/\n/g, '\n ')}...`);
}
});

rolesCmd
.command('sage')
.description('Run the Sage advisor')
.argument('<query>', 'Question or request')
.option('--repo <owner/repo>', 'Repository context')
.option('--issue <number>', 'Issue number for context')
.option('--json', 'Output as JSON')
.action(async (query, opts) => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The sage command is tightly coupled to the triage configuration and hardcoded values, which bypasses the new flexible roles configuration system.

  • It uses getTriageConfig() and getTriageApiKey() instead of sourcing configuration from the sage role definition.
  • The error message on line 1038 is hardcoded to ANTHROPIC_API_KEY, which will be incorrect if the user configures a different provider (e.g., OpenAI).
  • The fallback model on line 1043 is hardcoded, whereas the SAGE_ROLE definition already specifies a defaultModel.

To align with the new roles architecture, this command should use getEffectiveRole('sage') to get its configuration and then dynamically determine the provider, model, and API key based on that role's effective configuration.

try {
const { getOrLoadProvider } = await import('./core/providers.js');
const { getConfig, getTriageApiKey } = await import('./core/config.js');

const cfg = getConfig();
const role = getEffectiveRole('sage', cfg.roles);

if (!role) {
console.error('❌ Sage role is disabled or not found');
process.exit(1);
}

const apiKey = getTriageApiKey();

if (!apiKey) {
console.error(
'❌ No API key found. Set ANTHROPIC_API_KEY or configure in agentic.config.json'
);
process.exit(1);
}

const providerFn = await getOrLoadProvider(cfg.triage?.provider || 'anthropic', apiKey);
const model = providerFn(
role.defaultModel || cfg.triage?.model || 'claude-sonnet-4-20250514'
);

console.log('🔮 Sage is thinking...\n');

const result = await executeSageRole(query, model as Parameters<typeof executeSageRole>[1], {
role,
});

if (opts.json) {
output(result, true);
} else {
if (result.success && result.response) {
console.log('## 🔮 Sage Response\n');
console.log(result.response);
} else {
console.error(`❌ ${result.error || 'Unknown error'}`);
process.exit(1);
}
}
} catch (err) {
console.error('❌ Sage failed:', err instanceof Error ? err.message : err);
process.exit(1);
}
});

rolesCmd
.command('match')
.description('Find which role matches a trigger pattern')
.argument('<pattern>', 'Trigger pattern (e.g., @sage, /cursor)')
.action((pattern) => {
const cfg = getConfig();
const role = findRoleByTrigger(pattern, cfg.roles);

if (role) {
console.log(`✅ Matched: ${role.icon} ${role.name} (${role.id})`);
console.log(` ${role.description}`);
} else {
console.log(`❌ No role matches pattern: ${pattern}`);
console.log(`\nAvailable trigger patterns:`);
for (const r of Object.values(DEFAULT_ROLES)) {
for (const trigger of r.triggers) {
if (trigger.type === 'comment') {
console.log(` ${(trigger as { pattern: string }).pattern} → ${r.icon} ${r.name}`);
}
}
}
}
});

// ============================================
// Handoff Commands
// ============================================
Expand Down
4 changes: 4 additions & 0 deletions packages/agentic-control/src/core/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
*/

import { cosmiconfigSync } from 'cosmiconfig';
import type { RolesConfig } from '../roles/types.js';
import { setTokenConfig } from './tokens.js';
import type { TokenConfig } from './types.js';
import { validateConfig } from './validation.js';
Expand Down Expand Up @@ -117,6 +118,9 @@ export interface AgenticConfig {

/** MCP server configuration */
mcp?: MCPConfig;

/** Roles configuration */
roles?: RolesConfig;
}

// ============================================
Expand Down
4 changes: 2 additions & 2 deletions packages/agentic-control/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,11 @@ export { HandoffManager, type TakeoverOptions } from './handoff/index.js';
export * from './orchestrators/index.js';
// Pipeline automation - CI resolution, PR lifecycle
export * from './pipelines/index.js';

// Roles - Configurable AI agent personas
export * from './roles/index.js';
// Sandbox execution
export type { ContainerConfig, ContainerResult, SandboxOptions } from './sandbox/index.js';
export { ContainerManager, SandboxExecutor } from './sandbox/index.js';

// AI Triage
export { AIAnalyzer, type AIAnalyzerOptions } from './triage/index.js';

Expand Down
Loading