Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions AGON.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ npm run build # tsc -b --force
npm run test # vitest run
npm run typecheck # tsc -b

## Git Workflow — NEVER commit/push to main

- NEVER commit or push directly to `main`/`master`. Always: feature branch → push → open PR. This applies to **Cesar/builder auto-commits too** — an autonomous build leaves work for a human merge gate; it does not land on main.
- Stage explicit paths; never `git add -A` in the shared working tree (it sweeps other sessions' WIP).
- Run the gate (`npm run kern:compile && npm run test`) green before committing; "done" from a builder is unverified until the gate passes.

## Conventions

- ESM only, .js extensions in imports
Expand Down
33 changes: 17 additions & 16 deletions packages/cli/src/generated/cesar/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ RULE 2b — INTAKE FLOW: Every normal turn includes ROUTING CONTEXT with INTAKE
forge-slice / forge-full: define the target clearly first; forge only the hard slice or full broad task that benefits from competing implementations.
brainstorm / tribunal / campfire / review: call the matching orchestration tool directly when that is the recommended flow.
answer: answer directly.
RULE 2c — MODE IS A CHOICE, NOT A REFLEX (decisions, not retrieval): Before firing ANY orchestration mode, ask what the task actually needs, then pick the mode that fits — never the one your reflex reaches for. Orchestration (Brainstorm/Tribunal/Campfire/Council) exists to resolve TRADEOFFS and DECISIONS: competing approaches, contested correctness, hard-to-reverse calls. It is NOT a way to FIND an answer you can produce yourself by reading code. If the ask is a lookup, an analysis, or already in hand → answer directly; convening a panel to discover what one investigation would reveal burns the user's tokens and time. "Find me X / are there issues in Y / how does Z work" = investigate and answer, NOT brainstorm. Escalate by stakes and reversibility: QuickNero = one adversary · Tribunal = two-side debate · Council = whole-panel + chair · Forge = competitive build. When the user explicitly names a mode ("brainstorm it", "tribunal this"), honor it — this rule governs the turns where YOU own the routing choice.

CRITICAL: You have orchestration modes as DIRECT TOOL CALLS. NEVER use Bash to run CLI commands for orchestration. Call the tools directly:

Expand Down Expand Up @@ -177,7 +178,7 @@ RULE 10 — TURN CLOSURE: End every turn with one clear closing line so the user
/**
* Build the full Cesar system prompt with project context, engine list, and mode flags.
*/
// @kern-source: session:162
// @kern-source: session:163
export function buildCesarSystemPrompt(ctx: HandlerContext): string {
const config = ctx.config;
const cesarCwd = resolveWorkingDir();
Expand Down Expand Up @@ -355,19 +356,19 @@ export function buildCesarSystemPrompt(ctx: HandlerContext): string {
return systemParts.join('\n\n');
}

// @kern-source: session:341
// @kern-source: session:342
export const CESAR_SNAPSHOT_MSG_CHAR_CAP: number = 4000;

// @kern-source: session:343
// @kern-source: session:344
export const CONFIDENCE_BLOCK_LIMIT: number = 2;

// @kern-source: session:345
// @kern-source: session:346
export const SEARCH_NUDGE_THRESHOLD: number = 40;

/**
* Bound one message's text to CESAR_SNAPSHOT_MSG_CHAR_CAP with a truncation marker. Applied on BOTH snapshot paths (direct session history AND the chat-transcript fallback) so oversized content never floods Cesar's continuity context regardless of which path produced it.
*/
// @kern-source: session:347
// @kern-source: session:348
export function capSnapshotMessageContent(content: string): string {
if (content.length <= CESAR_SNAPSHOT_MSG_CHAR_CAP) return content;
return `${content.slice(0, CESAR_SNAPSHOT_MSG_CHAR_CAP)}\n… [${content.length - CESAR_SNAPSHOT_MSG_CHAR_CAP} chars truncated for Cesar context]`;
Expand All @@ -376,7 +377,7 @@ export function capSnapshotMessageContent(content: string): string {
/**
* Build a normalized continuity snapshot. Prefer the session's internal history; fall back to the visible chat transcript. Per-message string content is capped on EITHER path so review/brainstorm spam (or a huge tool result) doesn't flood Cesar's context; tool_calls/tool_call_id and non-string content are preserved untouched.
*/
// @kern-source: session:354
// @kern-source: session:355
export function buildCesarConversationSnapshot(session: PersistentSession|null, chatSession: any): Array<{role:string,content:any,tool_calls?:any[],tool_call_id?:string}> {
const directHistory = session?.getMessageHistory?.() ?? [];
if (directHistory.length > 0) {
Expand Down Expand Up @@ -409,7 +410,7 @@ export function buildCesarConversationSnapshot(session: PersistentSession|null,
/**
* Persist the active Cesar conversation before the session is discarded.
*/
// @kern-source: session:379
// @kern-source: session:380
export function saveCesarConversationSnapshot(session: PersistentSession|null, chatSession: any): void {
if (!session) return;
const snapshot = buildCesarConversationSnapshot(session, chatSession);
Expand All @@ -424,7 +425,7 @@ export function saveCesarConversationSnapshot(session: PersistentSession|null, c
/**
* Build the onToolCall callback for API engines with native function calling.
*/
// @kern-source: session:392
// @kern-source: session:393
export function buildOnToolCall(ctx: HandlerContext, toolRegistry: ToolRegistry, config: any): ((name:string, args:Record<string,unknown>, callId:string) => Promise<string>) | undefined {
const cwd = resolveWorkingDir();
const fsc = getProjectFileStateCache(cwd);
Expand Down Expand Up @@ -728,7 +729,7 @@ export function buildOnToolCall(ctx: HandlerContext, toolRegistry: ToolRegistry,
/**
* Build the onApproval callback for engine tool approvals. Returns true to approve, false to deny silently, or a string to deny with a reason the engine can see.
*/
// @kern-source: session:694
// @kern-source: session:695
export function buildOnApproval(ctx: HandlerContext, engineId: string): (tool:string, command:string) => Promise<boolean|string> {
const engine = ctx.registry.get(engineId);
return async (tool: string, command: string): Promise<boolean | string> => {
Expand Down Expand Up @@ -911,7 +912,7 @@ export function buildOnApproval(ctx: HandlerContext, engineId: string): (tool:st
};
}

// @kern-source: session:878
// @kern-source: session:879
export function normalizeCesarMcpServers(raw: unknown): Array<Record<string,unknown>> {
const isRecord = (value: unknown): value is Record<string, unknown> =>
!!value && typeof value === 'object' && !Array.isArray(value);
Expand Down Expand Up @@ -945,7 +946,7 @@ export function normalizeCesarMcpServers(raw: unknown): Array<Record<string,unkn
return normalizeNamedRecord(raw);
}

// @kern-source: session:912
// @kern-source: session:913
export function loadCesarMcpServers(config: any, cwd: string): Array<Record<string,unknown>>|undefined {
if (!(config as any).cesarMcpEnabled) return undefined;

Expand All @@ -969,7 +970,7 @@ export function loadCesarMcpServers(config: any, cwd: string): Array<Record<stri
return servers;
}

// @kern-source: session:936
// @kern-source: session:937
export function canUseCesarMcp(engine: any, binaryPath: string): boolean {
if (!binaryPath) {
return false;
Expand All @@ -981,7 +982,7 @@ export function canUseCesarMcp(engine: any, binaryPath: string): boolean {
/**
* Compute a fingerprint of MCP-related config to detect changes. Includes both manual config and auto-discovery sources.
*/
// @kern-source: session:943
// @kern-source: session:944
export function mcpConfigFingerprint(config: any): string {
const enabled = !!(config as any).cesarMcpEnabled;
const configPath = String((config as any).cesarMcpConfigPath ?? '');
Expand All @@ -1001,7 +1002,7 @@ export function mcpConfigFingerprint(config: any): string {
/**
* Resolve the agon-orchestration MCP server entry. The CLI ships as a tsup BUNDLE that ALSO emits the MCP server to <cli-dist>/mcp/index.js (see tsup.config.ts), so the published install is self-contained — no @kernlang/agon-mcp npm dependency. Resolution order: (0) the bundled sibling <cli-dist>/mcp/index.js (the published, self-contained path), (1) node module resolution of @kernlang/agon-mcp (monorepo-via-symlink / legacy installs), (2) walk up to the repo root containing packages/mcp/dist/index.js (monorepo without a symlink), (3) the original relative guess as a last resort. `fromUrl` is for tests; defaults to this module's URL.
*/
// @kern-source: session:961
// @kern-source: session:962
export function resolveAgonMcpServerPath(fromUrl?: string): string {
const raw = fromUrl ?? import.meta.url;
// Accept either a file: URL (normal) or a bare path (defensive): fileURLToPath
Expand Down Expand Up @@ -1035,7 +1036,7 @@ export function resolveAgonMcpServerPath(fromUrl?: string): string {
/**
* Single source of truth for which backend a Cesar engine will actually use. Honours config.cesarBackend preference ('auto' | 'cli' | 'api'). Pure — no side effects beyond registry lookups. Returns backend='none' when the engine has neither a usable binary nor an API key; callers decide how to handle that.
*/
// @kern-source: session:993
// @kern-source: session:994
export function resolveCesarBackend(ctx: HandlerContext, engineId?: string): { backend: 'cli'|'api'|'none', binaryPath: string, hasBinary: boolean, hasApi: boolean, engine: any } {
const config = ctx.config;
const cesarEngineId = engineId ?? (config as any).cesarEngine ?? config.forgeFixedStarter ?? 'claude';
Expand All @@ -1060,7 +1061,7 @@ export function resolveCesarBackend(ctx: HandlerContext, engineId?: string): { b
return { backend: 'none', binaryPath: '', hasBinary, hasApi, engine };
}

// @kern-source: session:1019
// @kern-source: session:1020
export async function ensureCesarSession(ctx: HandlerContext): Promise<PersistentSession> {
const config = ctx.config;
const cesarEngineId = (config as any).cesarEngine ?? config.forgeFixedStarter ?? 'claude';
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/kern/cesar/session.kern
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ RULE 2b — INTAKE FLOW: Every normal turn includes ROUTING CONTEXT with INTAKE
forge-slice / forge-full: define the target clearly first; forge only the hard slice or full broad task that benefits from competing implementations.
brainstorm / tribunal / campfire / review: call the matching orchestration tool directly when that is the recommended flow.
answer: answer directly.
RULE 2c — MODE IS A CHOICE, NOT A REFLEX (decisions, not retrieval): Before firing ANY orchestration mode, ask what the task actually needs, then pick the mode that fits — never the one your reflex reaches for. Orchestration (Brainstorm/Tribunal/Campfire/Council) exists to resolve TRADEOFFS and DECISIONS: competing approaches, contested correctness, hard-to-reverse calls. It is NOT a way to FIND an answer you can produce yourself by reading code. If the ask is a lookup, an analysis, or already in hand → answer directly; convening a panel to discover what one investigation would reveal burns the user's tokens and time. "Find me X / are there issues in Y / how does Z work" = investigate and answer, NOT brainstorm. Escalate by stakes and reversibility: QuickNero = one adversary · Tribunal = two-side debate · Council = whole-panel + chair · Forge = competitive build. When the user explicitly names a mode ("brainstorm it", "tribunal this"), honor it — this rule governs the turns where YOU own the routing choice.

CRITICAL: You have orchestration modes as DIRECT TOOL CALLS. NEVER use Bash to run CLI commands for orchestration. Call the tools directly:

Expand Down