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
4 changes: 2 additions & 2 deletions packages/cli/src/generated/blocks/rendering.entry.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env node
// @generated by kern v3.5.7 — DO NOT EDIT. Source: src/kern/blocks/rendering.kern
// @generated by kern v3.5.8 — DO NOT EDIT. Source: src/kern/blocks/rendering.kern

// @kern-source: rendering:428
// @kern-source: rendering:438

import React from 'react';
import { render } from 'ink';
Expand Down
38 changes: 24 additions & 14 deletions packages/cli/src/generated/blocks/rendering.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,28 +71,37 @@ export function CodeBlockView({ segment, borderColor }: { segment:ContentSegment
const bc = borderColor || '#585858';
const maxLineLen = capped.reduce((m: number, l: string) => Math.max(m, l.length), 0);
const headerLen = (segment.language || 'code').length + (segment.index !== undefined ? ` [${segment.index}]`.length : 0);
const innerWidth = Math.max(maxLineLen, headerLen);
const boxWidth = innerWidth + 4;
const rule = '\u2500'.repeat(boxWidth);
// Single coherent inner-content width for every row. Clamp to the terminal
// budget so a long line truncates instead of forcing the box wider than the
// screen. Every row is built as: `\u2502 \u258c <content padded to body> \u2502`
// left frame `\u2502 \u258c ` = 5 cols, right frame ` \u2502` = 3 cols \u2192 rowWidth = body + 8.
// border row `\u2502 ` + rule + ` \u2502` = rule.length + 4, so rule = body + 4.
// Previously the box was sized to body+2 while rows rendered at body+4..body+11,
// so every row overflowed and Ink wrapped it \u2014 dropping the trailing `\u2502` onto a
// blank row (the stray pipes + huge gaps between lines).
const body = Math.min(Math.max(maxLineLen, headerLen), codeWidth);
const rowWidth = body + 8;
const rule = '\u2500'.repeat(body + 4);
const overflowLabel = `\u2026 ${overflow} more lines`;
return (
<Box flexDirection="column" width={boxWidth + 2} flexShrink={0}>
<Box flexDirection="column" width={rowWidth} flexShrink={0}>
<Text color={bc}>{'\u2502 '}{rule}{' \u2502'}</Text>
<Text>
<Text color={bc}>{'\u2502 '}</Text>
<Text color={CODE_RAIL_COLOR}>{CODE_RAIL}</Text>
<Text> </Text>
<Text dimColor>{segment.language || 'code'}</Text>
{segment.index !== undefined && <Text color="#585858">{` [${segment.index}]`}</Text>}
<Text>{' '.repeat(Math.max(0, boxWidth - headerLen - 1))}</Text>
<Text>{' '.repeat(Math.max(0, body - headerLen))}</Text>
<Text color={bc}>{' \u2502'}</Text>
</Text>
{capped.map((line: string, i: number) => (
<Text key={`code-${i}`}>
<Text color={bc}>{'\u2502 '}</Text>
<Text color={CODE_RAIL_COLOR}>{CODE_RAIL}</Text>
<Text> </Text>
{isDiff ? <DiffLine line={line} maxWidth={codeWidth} /> : <SyntaxLine line={line} maxWidth={codeWidth} />}
<Text>{' '.repeat(Math.max(0, codeWidth - line.length - 4))}</Text>
{isDiff ? <DiffLine line={line} maxWidth={body} /> : <SyntaxLine line={line} maxWidth={body} />}
<Text>{' '.repeat(Math.max(0, body - line.length))}</Text>
<Text color={bc}>{' \u2502'}</Text>
</Text>
))}
Expand All @@ -101,7 +110,8 @@ export function CodeBlockView({ segment, borderColor }: { segment:ContentSegment
<Text color={bc}>{'\u2502 '}</Text>
<Text color={CODE_RAIL_COLOR}>{CODE_RAIL}</Text>
<Text> </Text>
<Text dimColor>{'\u2026 '}{overflow}{' more lines'}</Text>
<Text dimColor>{overflowLabel}</Text>
<Text>{' '.repeat(Math.max(0, body - overflowLabel.length))}</Text>
<Text color={bc}>{' \u2502'}</Text>
</Text>
)}
Expand All @@ -110,7 +120,7 @@ export function CodeBlockView({ segment, borderColor }: { segment:ContentSegment
);
}

// @kern-source: rendering:254
// @kern-source: rendering:264
export function RichSpanView({ span }: { span:InlineSpan }) {
if (span.style.code) {
return <Text color="#a78bfa" backgroundColor="#1e1033">{span.text}</Text>;
Expand All @@ -127,7 +137,7 @@ export function RichSpanView({ span }: { span:InlineSpan }) {
return el;
}

// @kern-source: rendering:275
// @kern-source: rendering:285
export function RichLineView({ line, borderColor }: { line:RichLine; borderColor?:string }) {
const border = borderColor ? <Text color={borderColor}>{'\u2502 '}</Text> : null;
const indent = line.indent > 0 ? ' '.repeat(line.indent) : '';
Expand All @@ -149,7 +159,7 @@ export function RichLineView({ line, borderColor }: { line:RichLine; borderColor
return <Text>{border}{indent}{listIndent}{marker}{line.spans.map((s: InlineSpan, i: number) => <RichSpanView key={i} span={s} />)}</Text>;
}

// @kern-source: rendering:302
// @kern-source: rendering:312
export function MarkdownTableView({ headers, rows, alignments, borderColor }: { headers:string[]; rows:string[][]; alignments:('left' | 'center' | 'right')[]; borderColor:string }) {
const colWidths = headers.map((h: string, i: number) => {
let max = h.length;
Expand Down Expand Up @@ -185,7 +195,7 @@ export function MarkdownTableView({ headers, rows, alignments, borderColor }: {
);
}

// @kern-source: rendering:345
// @kern-source: rendering:355
export function RenderedSegments({ segments, borderColor, wrapWidth }: { segments:ContentSegment[]; borderColor:string; wrapWidth:number }) {
return (
<>
Expand Down Expand Up @@ -246,7 +256,7 @@ export function RenderedSegments({ segments, borderColor, wrapWidth }: { segment
);
}

// @kern-source: rendering:412
// @kern-source: rendering:422
export function GradientLine({ text, colors }: { text:string; colors:readonly string[] }) {
const step = Math.max(1, Math.ceil(text.length / colors.length));
return (
Expand All @@ -259,7 +269,7 @@ export function GradientLine({ text, colors }: { text:string; colors:readonly st
);
}

// @kern-source: rendering:428
// @kern-source: rendering:438
export function AnsiLine({ text, maxWidth, fallbackDim }: { text:string; maxWidth:number; fallbackDim?:boolean }) {
if (!hasAnsiCodes(text)) {
const display = text.length > maxWidth ? text.slice(0, maxWidth - 4) + '\u2026' : text;
Expand Down
25 changes: 20 additions & 5 deletions packages/cli/src/generated/cesar/brain-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,24 @@ export function detectMutationIntentStall(text: string): boolean {
}

/**
* Return unique tool names from failed eager tool results. Used to restrict one-shot repair retries to the tool that just failed.
* Detect a response that CLAIMS an async review/forge/tribunal/brainstorm/agent or background job was dispatched or is now running — e.g. 'review delegated to codex, claude, agy', 'three reviewers are reading the diff in parallel', 'I kicked off the review', "I'll get back when they report". The caller pairs this with 'no delegation was actually emitted this turn' (ctx.cesar.pendingDelegation is null) to catch the confabulation where a weak engine narrates a dispatch it never made. Requires BOTH a delegable target AND a dispatch/running claim, so a plain answer that merely mentions the word 'review' does not trip it.
*/
// @kern-source: brain-helpers:113
export function detectFabricatedDelegation(text: string): boolean {
const body = String(text ?? '').trim();
if (!body) return false;
// A delegable target: review / forge / tribunal / brainstorm / campfire / agents / engines / a background job.
const TARGET_RE = /\b(?:review(?:er)?s?|forg(?:e|ing)|tribunal|brainstorm|campfire|agents?|engines?|jobs?)\b/i;
if (!TARGET_RE.test(body)) return false;
// A claim that the target was dispatched or is now running / will report back.
const DISPATCH_RE = /\b(?:kick(?:ed|ing)?\s*(?:it|them|that|the\s+\w+)?\s*off|fired?\s*(?:it|them|off)|dispatch(?:ed|ing)|delegat(?:ed|ing)|(?:is|are|now)\s+running|running\s+(?:in|now)|in\s+parallel|reading\s+the\s+(?:diff|changes|code)|working\s+(?:on\s+it|in\s+parallel)|in\s+progress|under\s*way|i'?ll\s+(?:get\s+back|report|let\s+you\s+know|surface|update)|report(?:s|ing)?\s+back|when\s+they\s+(?:report|land|return|finish|come\s+back)|still\s+(?:running|going|working|in\s+progress)|spun?\s+up|started\s+(?:the|a)\s+(?:review|forge|job|tribunal|brainstorm))\b/i;
return DISPATCH_RE.test(body);
}

/**
* Return unique tool names from failed eager tool results. Used to restrict one-shot repair retries to the tool that just failed.
*/
// @kern-source: brain-helpers:126
export function eagerFailedToolNames(results: ToolCallResult[]): string[] {
const names: string[] = [];
for (const result of results ?? []) {
Expand All @@ -141,7 +156,7 @@ export function eagerFailedToolNames(results: ToolCallResult[]): string[] {
/**
* Gate eager tool repair retries. A corrected tool call may run once only if the same tool failed in the immediately previous eager batch.
*/
// @kern-source: brain-helpers:125
// @kern-source: brain-helpers:138
export function shouldRunEagerRepairTool(toolName: string, meta: any, failedToolNames: string[], usedToolNames: string[]): boolean {
const name = String(toolName ?? '').trim();
if (!name) return false;
Expand All @@ -156,7 +171,7 @@ export function shouldRunEagerRepairTool(toolName: string, meta: any, failedTool
/**
* Return true for XML tools that hand control back to the Agon dispatcher. These tools do not produce inline results; continuing the XML tool loop after them can make Cesar claim a delegation happened while the actual forge/brainstorm/etc. job has not started yet.
*/
// @kern-source: brain-helpers:138
// @kern-source: brain-helpers:151
export function shouldStopAfterXmlToolCall(toolName: string): boolean {
const HANDOFF_TOOLS = new Set(['Forge', 'Brainstorm', 'Tribunal', 'Campfire', 'Pipeline', 'Review', 'Agent', 'Goal', 'ProposePlan', 'ExitPlanMode']);
return HANDOFF_TOOLS.has(String(toolName ?? ''));
Expand All @@ -165,7 +180,7 @@ export function shouldStopAfterXmlToolCall(toolName: string): boolean {
/**
* Expand a bare 'fix it' follow-up into an explicit prompt grounded in the most recent stored review result. This avoids making Cesar guess which reviewer findings the user means, especially because /review runs outside Cesar's live session history.
*/
// @kern-source: brain-helpers:144
// @kern-source: brain-helpers:157
export function buildReviewFollowupPrompt(input: string, ctx: HandlerContext): { matched: boolean; prompt: string } {
const trimmed = input.trim();
const match = trimmed.match(/^fix it(?:\s+with\s+([a-z0-9._-]+))?[\s?!.,;:]*$/i);
Expand All @@ -186,7 +201,7 @@ export function buildReviewFollowupPrompt(input: string, ctx: HandlerContext): {
return { matched: true, prompt: prompt };
}

// @kern-source: brain-helpers:163
// @kern-source: brain-helpers:176
export function extractDelegation(toolName: string, args: Record<string,unknown>): PendingDelegation {
const argsRecord = args as Record<string, unknown>;
const taskKindRaw = argsRecord.taskKind;
Expand Down
47 changes: 45 additions & 2 deletions packages/cli/src/generated/cesar/brain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import { applyCesarSelfTurnApproval } from './self-turn-approval.js';

import { createCesarTurnId, recordCesarApprovalDecision, recordCesarToolTimeline, recordCesarConfidence } from './tool-observability.js';

import { yieldToInk, splitBeforeToolMarkup, XML_TOOL_MARKUP_HOLD_CHARS, findTrailingUserQuestion, detectAwaitingUserInput, detectNarratedToolStall, detectMutationIntentStall, eagerFailedToolNames, shouldRunEagerRepairTool, shouldStopAfterXmlToolCall, buildReviewFollowupPrompt, extractDelegation } from './brain-helpers.js';
import { yieldToInk, splitBeforeToolMarkup, XML_TOOL_MARKUP_HOLD_CHARS, findTrailingUserQuestion, detectAwaitingUserInput, detectNarratedToolStall, detectMutationIntentStall, detectFabricatedDelegation, eagerFailedToolNames, shouldRunEagerRepairTool, shouldStopAfterXmlToolCall, buildReviewFollowupPrompt, extractDelegation } from './brain-helpers.js';

// @kern-source: brain:19
export async function commitTurnAndDelegate(pendingDel: PendingDelegation, input: string, response: string, cesarEngineId: string, streaming: boolean, dispatch: Dispatch, ctx: HandlerContext, telemetry?: Record<string,unknown>): Promise<CesarTurnOutcome> {
Expand Down Expand Up @@ -319,8 +319,14 @@ export async function handleCesarBrain(input: string, dispatch: Dispatch, ctx: H
const outputDir = join(RUNS_DIR, `cesar-fallback-${Date.now()}`);
mkdirSync(outputDir, { recursive: true });
const primedPrompt = buildHistoryPrimedPrompt(ctx.chatSession, input);
// Cesar is an agentic leading role, so dispatch in 'agent' mode when the
// engine supports it. 'exec' triggers agy's OUTPUT-RULES gag (adapter-helpers:
// engine.id === 'agy' && mode !== 'agent') which forbids file edits / tool use
// and forces a single-pass text answer — exactly why agy could not run tools
// as Cesar. Fall back to 'exec' for engines without an agent mode (no regression).
const fallbackMode = ((engine as any)?.agent ? 'agent' : 'exec') as any;
const freshResult = await ctx.adapter.dispatch({
engine, prompt: primedPrompt, cwd: resolveWorkingDir(), mode: 'exec' as any,
engine, prompt: primedPrompt, cwd: resolveWorkingDir(), mode: fallbackMode,
timeout: config.timeout ?? 120, outputDir, signal: abort.signal, systemPrompt: buildCesarSystemPrompt(ctx),
});
dispatch({ type: 'spinner-stop' });
Expand Down Expand Up @@ -1625,6 +1631,43 @@ export async function handleCesarBrain(input: string, dispatch: Dispatch, ctx: H
}
}

// ── Fabricated-delegation guard: ground a confabulated dispatch ──
// Catches the failure where a weak engine narrates that it dispatched or is
// running an async review/forge/agent job ("three reviewers are reading the
// diff in parallel", "I kicked off the review", "I'll report when they land")
// WITHOUT having emitted any delegation this turn — pendingDelegation is null,
// so nothing is actually queued or running. Re-prompt once to ground it: call
// the real tool now, or tell the user plainly that nothing is running. If the
// re-prompt dispatches for real, pendingDelegation gets set and the existing
// downstream delegation path takes over.
if (
!ctx.cesar!.pendingDelegation
&& session.alive
&& !abort.signal.aborted
&& detectFabricatedDelegation(response.trim())
) {
dispatch({ type: 'warning', message: 'Cesar claimed a job was running but never dispatched one — grounding...' });
dispatch({ type: 'spinner-start', message: 'Cesar grounding…', color });
try {
let groundResponse = '';
const groundGen = session.send({
message: '[SYSTEM] GROUNDING CHECK: You did NOT dispatch any review/forge/tribunal/brainstorm/agent/job this turn, and none is pending or running. Do NOT claim background work is "running", "in parallel", "kicked off", or that anyone "will report back" — that is false and misleads the user. If the user wants that work done, call the actual tool now (Review/Forge/Tribunal/Brainstorm/Agent). Otherwise tell the user plainly that nothing is currently running and ask whether to start it.',
signal: abort.signal,
});
for await (const chunk of groundGen) {
if (chunk.type === 'text') groundResponse += chunk.content;
if (chunk.type === 'done' || chunk.type === 'error') break;
}
dispatch({ type: 'spinner-stop' });
if (groundResponse.trim()) {
dispatch({ type: 'engine-block', engineId: cesarEngineId, color, content: groundResponse.trim() });
response = groundResponse.trim();
}
} catch {
dispatch({ type: 'spinner-stop' });
}
}

// ── Protocol enforcement: DISABLED ──
// Cesar decides all delegations. The system never forces brainstorm/tribunal on the user.
// If Cesar wants to delegate, he calls the tool. If he doesn't, that's his call.
Expand Down
Loading