feat(advisor): added advisor agent for passive code review with severity-tagged advice

- Created AdvisorRuntime and AdviseTool to drive a read-only advisor agent that delivers severity-tagged advice (nit, concern, blocker) with interruption policy and transcript delta rendering.
- Added /advisor slash command with on/off/status/dump subcommands to control advisor lifecycle and inspect advisor metrics (model, messages, tokens, cost).
- Added advisor.enabled and advisor.subagents settings to enable passive advisor review on main agent and spawned task/eval subagents.
- Implemented advisor message rendering with severity-color badges (blocker=error, concern=warning, nit=muted) in chat log and status line indicator (++ badge).
- Extended yield-queue and session-history-format to support advisor batching and optional thinking block inclusion.
This commit is contained in:
can1357
2026-06-15 16:32:13 +02:00
parent 0b05911d11
commit 37ecd3e73a
24 changed files with 1438 additions and 22 deletions
@@ -84,9 +84,9 @@ export class CommandController {
}
}
handleDumpCommand() {
handleDumpCommand(isRaw = false) {
try {
const formatted = this.ctx.session.formatSessionAsText();
const formatted = this.ctx.session.formatSessionAsText({ compact: !isRaw });
if (!formatted) {
this.ctx.showError("No messages to dump yet.");
return;
@@ -98,6 +98,26 @@ export class CommandController {
}
}
handleAdvisorDumpCommand(isRaw = false) {
try {
const advisorHistory = this.ctx.session.formatAdvisorHistoryAsText({ compact: !isRaw });
if (advisorHistory === null) {
this.ctx.showError("Advisor is not active for this session.");
return;
}
if (!advisorHistory) {
this.ctx.showError("Advisor has no history yet.");
return;
}
copyToClipboard(advisorHistory);
this.ctx.showStatus("Advisor history copied to clipboard");
} catch (error: unknown) {
this.ctx.showError(
`Failed to copy advisor history: ${error instanceof Error ? error.message : "Unknown error"}`,
);
}
}
async handleDebugTranscriptCommand(): Promise<void> {
try {
const width = Math.max(1, this.ctx.ui.terminal.columns);
@@ -305,6 +325,53 @@ export class CommandController {
this.ctx.present([new Spacer(1), new Text(info, 1, 0)]);
}
async handleAdvisorStatusCommand(): Promise<void> {
const stats = this.ctx.session.getAdvisorStats();
if (!stats.active) {
this.ctx.present([
new Spacer(1),
new Text(
stats.configured
? "Advisor setting is enabled, but no model is assigned to the 'advisor' role."
: "Advisor is disabled.",
1,
0,
),
]);
return;
}
const model = stats.model!;
let info = `${theme.bold("Advisor Status")}\n\n`;
info += `${theme.bold("Provider")}\n`;
info += `${theme.fg("dim", "Model:")} ${model.provider}/${model.id}\n`;
info += `\n${theme.bold("Messages")}\n`;
info += `${theme.fg("dim", "User:")} ${stats.messages.user.toLocaleString()}\n`;
info += `${theme.fg("dim", "Assistant:")} ${stats.messages.assistant.toLocaleString()}\n`;
info += `${theme.fg("dim", "Total:")} ${stats.messages.total.toLocaleString()}\n`;
info += `\n${theme.bold("Context")}\n`;
if (stats.contextWindow > 0) {
const percent = Math.round((stats.contextTokens / stats.contextWindow) * 100);
info += `${theme.fg("dim", "Tokens:")} ${stats.contextTokens.toLocaleString()} / ${stats.contextWindow.toLocaleString()} (${percent}%)\n`;
} else {
info += `${theme.fg("dim", "Tokens:")} ${stats.contextTokens.toLocaleString()}\n`;
}
info += `\n${theme.bold("Spend")}\n`;
info += `${theme.fg("dim", "Input:")} ${stats.tokens.input.toLocaleString()}\n`;
info += `${theme.fg("dim", "Output:")} ${stats.tokens.output.toLocaleString()}\n`;
if (stats.tokens.cacheRead > 0) {
info += `${theme.fg("dim", "Cache Read:")} ${stats.tokens.cacheRead.toLocaleString()}\n`;
}
if (stats.tokens.cacheWrite > 0) {
info += `${theme.fg("dim", "Cache Write:")} ${stats.tokens.cacheWrite.toLocaleString()}\n`;
}
info += `${theme.fg("dim", "Total:")} ${stats.tokens.total.toLocaleString()}\n`;
if (stats.cost > 0) {
info += `\n${theme.bold("Cost")}\n`;
info += `${theme.fg("dim", "Total:")} $${stats.cost.toFixed(4)}\n`;
}
this.ctx.present([new Spacer(1), new Text(info, 1, 0)]);
}
async handleJobsCommand(): Promise<void> {
const snapshot = this.ctx.session.getAsyncJobSnapshot({ recentLimit: 5 });
if (!snapshot) {