feat: productionize self-improvement and R&D council workflows

- add canonical self-improvement/R&D Council data helpers, APIs, and tests

- persist council decisions, evidence telemetry, and work-order handoff

- harden operator UI states and require operator token by default

- ignore local R&D runtime artifacts
This commit is contained in:
Faisal C
2026-04-30 21:43:45 -05:00
parent 81deb042b5
commit f59116866f
26 changed files with 5046 additions and 0 deletions
+5
View File
@@ -6,3 +6,8 @@ next-env.d.ts
.DS_Store .DS_Store
/.openclaw /.openclaw
/public/assets/pixel-office/*.mp3 /public/assets/pixel-office/*.mp3
# Local R&D Council runtime state
rd-council-work-orders.json
self-improvement-command-log.jsonl
rd-council-decision-log.jsonl
+51
View File
@@ -0,0 +1,51 @@
import { NextRequest, NextResponse } from "next/server";
import {
applyCouncilDecision,
localOperatorDeniedPayload,
validateLocalOperatorRequest,
} from "@/lib/self-improvement-data.mjs";
export async function POST(request: NextRequest) {
const validation = validateLocalOperatorRequest(request);
if (!validation.ok) {
return NextResponse.json(localOperatorDeniedPayload(validation.reason), { status: validation.status ?? 403 });
}
try {
const body = await request.json();
if (typeof body?.itemId !== "string" || body.itemId.trim() === "") {
return NextResponse.json(
{
success: false,
message: "itemId is required",
},
{ status: 400 }
);
}
const result = applyCouncilDecision({
itemId: body.itemId,
decision: "approved",
note: body.note,
actor: body.actor ?? "rd-council-api",
});
return NextResponse.json({
success: true,
message: "R&D Council item approved",
item: result.item,
audit: result.auditEntry,
workOrder: result.workOrder,
});
} catch (error) {
const message = (error as Error).message || "Approval failed";
const status = /not found/i.test(message) ? 404 : /required|unsupported/i.test(message) ? 400 : 500;
return NextResponse.json(
{
success: false,
message,
},
{ status }
);
}
}
+5
View File
@@ -0,0 +1,5 @@
import { runSelfImprovementCommand } from "../self-improvement-command";
export async function POST(request: Request) {
return runSelfImprovementCommand("dry-run" as any, request);
}
+130
View File
@@ -0,0 +1,130 @@
import {
IDEA_LEDGER_PATH,
localOperatorDeniedPayload,
readJsonFile,
validateLocalOperatorRequest,
writeJsonFile,
} from "@/lib/self-improvement-data.mjs";
const LEDGER_TYPES = new Set(["proposed", "adopted", "rejected", "dormant"]);
function localOnly(request: Request) {
const validation = validateLocalOperatorRequest(request);
if (validation.ok) return null;
return Response.json(localOperatorDeniedPayload(validation.reason), { status: validation.status ?? 403 });
}
function defaultLedger() {
return {
proposed: [],
adopted: [],
rejected: [],
dormant: [],
};
}
export async function GET(request: Request) {
const denied = localOnly(request);
if (denied) return denied;
try {
const data = readJsonFile(IDEA_LEDGER_PATH, defaultLedger());
return Response.json({
success: true,
...data,
});
} catch (error) {
return Response.json(
{
success: false,
message: (error as Error).message,
},
{ status: 500 }
);
}
}
export async function POST(request: Request) {
const denied = localOnly(request);
if (denied) return denied;
try {
const body = await request.json();
const { type, idea } = body;
if (!LEDGER_TYPES.has(type)) {
return Response.json(
{
success: false,
message: `Invalid type: ${type}`,
},
{ status: 400 }
);
}
if (!idea || typeof idea !== "object") {
return Response.json(
{
success: false,
message: "idea object is required",
},
{ status: 400 }
);
}
const data = readJsonFile(IDEA_LEDGER_PATH, defaultLedger());
const ideaWithTimestamp = { ...idea, added_at: new Date().toISOString() };
data[type].push(ideaWithTimestamp);
writeJsonFile(IDEA_LEDGER_PATH, data);
return Response.json({
success: true,
message: `Idea added to ${type} list`,
idea: ideaWithTimestamp,
});
} catch (error) {
return Response.json(
{
success: false,
message: (error as Error).message,
},
{ status: 500 }
);
}
}
export async function DELETE(request: Request) {
const denied = localOnly(request);
if (denied) return denied;
try {
const body = await request.json();
const { type, id } = body;
if (!LEDGER_TYPES.has(type)) {
return Response.json(
{
success: false,
message: `Invalid type: ${type}`,
},
{ status: 400 }
);
}
const data = readJsonFile(IDEA_LEDGER_PATH, defaultLedger());
data[type] = data[type].filter((item: any) => item.id !== id);
writeJsonFile(IDEA_LEDGER_PATH, data);
return Response.json({
success: true,
message: `Idea removed from ${type} list`,
});
} catch (error) {
return Response.json(
{
success: false,
message: (error as Error).message,
},
{ status: 500 }
);
}
}
+5
View File
@@ -0,0 +1,5 @@
import { runSelfImprovementCommand } from "../self-improvement-command";
export async function POST(request: Request) {
return runSelfImprovementCommand("ingest-usage" as any, request);
}
+41
View File
@@ -0,0 +1,41 @@
import {
appendCouncilItems,
buildCouncilItemsFromUsage,
findSessionFile,
localOperatorDeniedPayload,
readJsonFile,
upsertLedgerProposed,
validateLocalOperatorRequest,
} from "@/lib/self-improvement-data.mjs";
export async function POST(request: Request) {
const validation = validateLocalOperatorRequest(request);
if (!validation.ok) {
return Response.json(localOperatorDeniedPayload(validation.reason), { status: validation.status ?? 403 });
}
try {
const sessionPath = findSessionFile();
const sessionsData = sessionPath ? readJsonFile(sessionPath, { messages: [] }) : { messages: [] };
const items = buildCouncilItemsFromUsage(sessionsData);
const allItems = appendCouncilItems(items);
upsertLedgerProposed(items);
return Response.json({
success: true,
items,
total: items.length,
stored_total: allItems.length,
source: sessionPath,
message: "Proactive analysis complete",
});
} catch (error) {
return Response.json(
{
success: false,
message: (error as Error).message,
},
{ status: 500 }
);
}
}
+5
View File
@@ -0,0 +1,5 @@
import { runSelfImprovementCommand } from "../self-improvement-command";
export async function POST(request: Request) {
return runSelfImprovementCommand("proactive-real" as any, request);
}
+34
View File
@@ -0,0 +1,34 @@
import { NextRequest, NextResponse } from "next/server";
import {
localOperatorDeniedPayload,
readActiveProjectsInventory,
validateLocalOperatorRequest,
} from "@/lib/self-improvement-data.mjs";
export async function GET(request: NextRequest) {
const validation = validateLocalOperatorRequest(request);
if (!validation.ok) {
return NextResponse.json(localOperatorDeniedPayload(validation.reason), { status: validation.status ?? 403 });
}
try {
const inventory = readActiveProjectsInventory();
return NextResponse.json({
success: true,
projects: inventory.projects,
total: inventory.total,
status: inventory.status,
source: inventory.source,
reason: inventory.reason,
});
} catch (error) {
return NextResponse.json(
{
success: false,
message: (error as Error).message,
},
{ status: 500 }
);
}
}
@@ -0,0 +1,52 @@
import { NextRequest, NextResponse } from "next/server";
import {
applyCouncilDecision,
localOperatorDeniedPayload,
validateLocalOperatorRequest,
} from "@/lib/self-improvement-data.mjs";
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ itemId: string }> }
) {
const validation = validateLocalOperatorRequest(request);
if (!validation.ok) {
return NextResponse.json(localOperatorDeniedPayload(validation.reason), { status: validation.status ?? 403 });
}
try {
const { itemId } = await params;
const body = await request.json().catch(() => ({}));
const now = new Date().toISOString();
const snoozeUntil =
typeof body?.snoozeUntil === "string" && body.snoozeUntil.trim() !== ""
? body.snoozeUntil
: new Date(Date.parse(now) + 7 * 24 * 60 * 60 * 1000).toISOString();
const result = applyCouncilDecision({
itemId,
decision: "snoozed",
note: body?.note,
actor: body?.actor ?? "rd-council-api",
snoozeUntil,
now,
});
return NextResponse.json({
success: true,
message: "R&D Council item snoozed",
item: result.item,
audit: result.auditEntry,
});
} catch (error) {
const message = (error as Error).message || "Snooze failed";
const status = /not found/i.test(message) ? 404 : /required|unsupported/i.test(message) ? 400 : 500;
return NextResponse.json(
{
success: false,
message,
},
{ status }
);
}
}
+30
View File
@@ -0,0 +1,30 @@
import { NextRequest, NextResponse } from "next/server";
import {
localOperatorDeniedPayload,
readCouncilItems,
validateLocalOperatorRequest,
} from "@/lib/self-improvement-data.mjs";
export async function GET(request: NextRequest) {
const validation = validateLocalOperatorRequest(request);
if (!validation.ok) {
return NextResponse.json(localOperatorDeniedPayload(validation.reason), { status: validation.status ?? 403 });
}
try {
const items = readCouncilItems();
return NextResponse.json({
success: true,
items,
total: items.length,
});
} catch (error) {
return NextResponse.json(
{
success: false,
message: (error as Error).message,
},
{ status: 500 }
);
}
}
+50
View File
@@ -0,0 +1,50 @@
import { NextRequest, NextResponse } from "next/server";
import {
applyCouncilDecision,
localOperatorDeniedPayload,
validateLocalOperatorRequest,
} from "@/lib/self-improvement-data.mjs";
export async function POST(request: NextRequest) {
const validation = validateLocalOperatorRequest(request);
if (!validation.ok) {
return NextResponse.json(localOperatorDeniedPayload(validation.reason), { status: validation.status ?? 403 });
}
try {
const body = await request.json();
if (typeof body?.itemId !== "string" || body.itemId.trim() === "") {
return NextResponse.json(
{
success: false,
message: "itemId is required",
},
{ status: 400 }
);
}
const result = applyCouncilDecision({
itemId: body.itemId,
decision: "rejected",
note: body.note,
actor: body.actor ?? "rd-council-api",
});
return NextResponse.json({
success: true,
message: "R&D Council item rejected",
item: result.item,
audit: result.auditEntry,
});
} catch (error) {
const message = (error as Error).message || "Rejection failed";
const status = /not found/i.test(message) ? 404 : /required|unsupported/i.test(message) ? 400 : 500;
return NextResponse.json(
{
success: false,
message,
},
{ status }
);
}
}
+162
View File
@@ -0,0 +1,162 @@
import {
appendCommandAudit,
appendCouncilItems,
buildCouncilItemsFromUsage,
commandError,
commandOk,
findSessionFile,
getCommandConfig,
IDEA_LEDGER_PATH,
readCouncilItems,
readJsonFile,
localOperatorDeniedPayload,
upsertLedgerProposed,
validateCommandRequest,
validateLocalOperatorRequest,
} from "@/lib/self-improvement-data.mjs";
export type SelfImprovementCommandId = "proactive-real" | "ingest-usage" | "dry-run" | "weekly-review";
type CommandBody = {
confirm?: boolean;
timestamp?: string;
};
async function readBody(request: Request): Promise<CommandBody> {
try {
return (await request.json()) as CommandBody;
} catch {
return {};
}
}
function sessionContext() {
const sessionPath = findSessionFile();
const sessionData = sessionPath ? readJsonFile(sessionPath, { messages: [] }) : { messages: [] };
const sessionMessages = Array.isArray(sessionData?.messages) ? sessionData.messages.length : 0;
return { sessionPath, sessionData, sessionMessages };
}
function itemSummary(items: any[]) {
return items.map((item) => `${item.title} (${item.council_confidence ?? "unknown"})`);
}
export async function runSelfImprovementCommand(commandId: SelfImprovementCommandId, request: Request) {
const localValidation = validateLocalOperatorRequest(request);
if (!localValidation.ok) {
return Response.json(localOperatorDeniedPayload(localValidation.reason), { status: localValidation.status ?? 403 });
}
const body = await readBody(request);
const validation = validateCommandRequest(commandId, body);
if (!validation.ok) {
const payload = commandError({
commandId,
code: validation.confirmationRequired ? "CONFIRMATION_REQUIRED" : "UNKNOWN_COMMAND",
message: validation.message,
details: validation,
status: validation.status,
});
return Response.json(payload, { status: validation.status });
}
const command = getCommandConfig(commandId)!;
const startedAt = new Date().toISOString();
try {
const { sessionPath, sessionData, sessionMessages } = sessionContext();
let responsePayload: any;
if (commandId === "dry-run") {
const previewItems = buildCouncilItemsFromUsage(sessionData);
responsePayload = commandOk({
command,
message: `Dry run generated ${previewItems.length} improvement item${previewItems.length === 1 ? "" : "s"}; no files were changed.`,
output: [
`DRY RUN ONLY — no writes performed`,
`Source: ${sessionPath ?? "no session file found"}`,
`Session messages inspected: ${sessionMessages}`,
`Items previewed: ${previewItems.length}`,
...itemSummary(previewItems),
],
details: {
mode: "dry-run",
source: sessionPath,
session_messages: sessionMessages,
items: previewItems,
},
});
} else if (commandId === "weekly-review") {
const councilItems = readCouncilItems();
const ledger = readJsonFile(IDEA_LEDGER_PATH, { proposed: [], adopted: [], rejected: [], dormant: [] });
responsePayload = commandOk({
command,
message: `Weekly review complete: ${councilItems.length} council items, ${(ledger.proposed ?? []).length} proposed ideas.`,
output: [
`Council items: ${councilItems.length}`,
`Ledger proposed: ${(ledger.proposed ?? []).length}`,
`Ledger adopted: ${(ledger.adopted ?? []).length}`,
`Ledger rejected: ${(ledger.rejected ?? []).length}`,
`Ledger dormant: ${(ledger.dormant ?? []).length}`,
...itemSummary(councilItems.slice(0, 8)),
],
details: {
council_total: councilItems.length,
ledger_counts: {
proposed: (ledger.proposed ?? []).length,
adopted: (ledger.adopted ?? []).length,
rejected: (ledger.rejected ?? []).length,
dormant: (ledger.dormant ?? []).length,
},
},
});
} else {
const items = buildCouncilItemsFromUsage(sessionData);
const allItems = appendCouncilItems(items);
upsertLedgerProposed(items);
responsePayload = commandOk({
command,
message: `${command.label} complete: ${items.length} item${items.length === 1 ? "" : "s"} processed, ${allItems.length} stored total.`,
output: [
`Source: ${sessionPath ?? "no session file found"}`,
`Session messages inspected: ${sessionMessages}`,
`Items processed: ${items.length}`,
`Stored council total: ${allItems.length}`,
`Ledger updated: yes`,
...itemSummary(items),
],
details: {
source: sessionPath,
session_messages: sessionMessages,
items,
stored_total: allItems.length,
writes: ["rd-council-items.json", "idea_ledger.json"],
},
});
}
appendCommandAudit({
command_id: commandId,
command_label: command.label,
success: true,
started_at: startedAt,
finished_at: new Date().toISOString(),
message: responsePayload.message,
details: responsePayload.details,
});
return Response.json(responsePayload);
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown command failure";
const payload = commandError({ commandId, message });
appendCommandAudit({
command_id: commandId,
command_label: command.label,
success: false,
started_at: startedAt,
finished_at: new Date().toISOString(),
message,
});
return Response.json(payload, { status: 500 });
}
}
+5
View File
@@ -0,0 +1,5 @@
import { runSelfImprovementCommand } from "../self-improvement-command";
export async function POST(request: Request) {
return runSelfImprovementCommand("weekly-review" as any, request);
}
+44
View File
@@ -0,0 +1,44 @@
# Council Context - v1.1
## Council Mode
- **Reactive Mode**: Analyzes your current work and recommends next steps
- **Proactive Mode**: Ignores current work, asks "what should I be doing?"
## Proactive Lenses (4 per session)
Each model must surface at least one proactive idea from these lenses:
1. **Efficiency**: What could be automated? What takes too long? What gets repeated?
2. **Infrastructure**: What foundational system would unlock 3+ things?
3. **Pattern**: What keeps getting built pieces without finishing the full version?
4. **External**: What's the market rewarding right now?
## Idea Ledger Structure
```json
{
"proposed": [],
"adopted": [],
"rejected": [],
"dormant": []
}
```
## Debate Framework for Proactive Ideas
**Proposer opens with**: One idea from each of the 4 lenses
**Debate rounds**:
- **Model 1 (Devil's Advocate)**: Why this is a distraction right now
- **Model 2 (Resource Check)**: How long to build, what skills needed
- **Model 3 (Impact Estimator)**: Measurable outcome if built
- **Model 4 (Timing Analyst)**: Is now the right time?
**Proposer responds** to all 4 concerns before final vote
## Confidence Levels
- **Low**: Idea needs more research, not fully formed
- **Medium**: Clear concept, needs validation
- **High**: Strong evidence, ready for action
## Learning Rules
- Reject 3x same idea type → stop pitching that lens
- 2 approvals in same category → auto-promote to next lens
- Dormant ideas (snoozed) → re-evaluate with fresh context
+308
View File
@@ -0,0 +1,308 @@
export {}; // Empty export to treat this as a module
type Stage = "trending" | "opportunity" | "council";
type OriginType = "reactive" | "proactive";
type ProactiveLens = "efficiency" | "infrastructure" | "pattern" | "external" | null;
type CouncilConfidence = "high" | "medium" | "low";
type EscalationType = "auto" | "manual";
type SessionType = "scheduled" | "emergency";
type TimeOfDay = "morning" | "evening";
type Decision = null | "approved" | "rejected" | "snoozed";
interface CouncilItem {
id: string;
title: string;
summary: string;
created_at: string;
stage: Stage;
council_session_id: string;
promotion_reason: string;
proposing_model: string;
origin_type: OriginType;
origin_tags: string[];
proactive_lens: ProactiveLens;
idea_ledger_id: string;
council_confidence: CouncilConfidence;
debate_summary: string[];
key_disagreements: { model: string; concern: string; resolution: string }[];
council_recommendation: string;
passing_reasoning: string;
escalation_type: EscalationType;
session_type: SessionType;
scheduled_for: string;
session_time_of_day: TimeOfDay;
decision: Decision;
decision_note?: string;
decision_at?: string;
snooze_until?: string;
is_hidden: boolean;
hidden_until?: string;
}
const mockedItems: CouncilItem[] = [
{
id: "rdc-001",
title: "SaaS Pricing Tier Optimization",
summary: "Data shows 23% of free-tier users upgrade when premium features are unlocked via API integrations",
created_at: "2026-03-24T14:30:00Z",
stage: "council",
council_session_id: "session-morning-2026-03-24",
promotion_reason: "Multiple mentions across 3+ sessions with high signal strength",
proposing_model: "llama-3.1-70b",
origin_type: "reactive",
origin_tags: ["📊 Reactive Analysis"],
proactive_lens: null,
idea_ledger_id: "ledger-reactive-001",
council_confidence: "high",
debate_summary: [
"Model A: Pricing tiers should align with feature adoption patterns",
"Model B: Free tier users need more engagement triggers before upgrade",
"Model C: API integration unlock point is too high —降低 barrier"
],
key_disagreements: [
{ model: "llama-3.1-70b", concern: "Free tier users need more engagement triggers", resolution: "Added engagement score threshold" },
{ model: "deepseek-v3", concern: "API unlocks too high", resolution: "Lowered unlock threshold from 5 to 3 features" }
],
council_recommendation: "Implement API-integration unlock tracking withengagement score thresholds. Free tier users who interact with3+ premium features via API should see upgrade prompts.",
passing_reasoning: "Met all criteria: clear data signal, actionable recommendation, supported by 4 of 5 models",
escalation_type: "auto",
session_type: "scheduled",
scheduled_for: "2026-03-24T09:00:00Z",
session_time_of_day: "morning",
decision: null,
is_hidden: false
},
{
id: "rdc-002",
title: "Mobile App Feature Prioritization",
summary: "User session data indicates 40% drop-off at onboarding step 3",
created_at: "2026-03-25T08:15:00Z",
stage: "council",
council_session_id: "session-morning-2026-03-25",
promotion_reason: "Sustained signal across 2 consecutive sessions",
proposing_model: "claude-sonnet-4-6",
origin_type: "reactive",
origin_tags: ["🔍 Pattern Detected"],
proactive_lens: null,
idea_ledger_id: "ledger-reactive-002",
council_confidence: "medium",
debate_summary: [
"Model A: Simplify onboarding flow — reduce to 2 steps",
"Model B: Add intermediate progress indicator instead",
"Model C: A/B test both approaches simultaneously"
],
key_disagreements: [
{ model: "claude-sonnet-4-6", concern: "Progress indicator may confuse users", resolution: "Added on-screen guidance text" },
{ model: "gemini-ultra", concern: "Too many changes at once", resolution: "Pilot with 10% of new users first" }
],
council_recommendation: "Deploy simplified onboarding flow (2 steps) to 50% of new users. Add progress indicator variant toA/B test with existing 50%.",
passing_reasoning: "Actionable, measurable, supported by 3 models with clear testing plan",
escalation_type: "auto",
session_type: "scheduled",
scheduled_for: "2026-03-25T09:00:00Z",
session_time_of_day: "morning",
decision: null,
is_hidden: false
},
{
id: "rdc-003",
title: "Template Generator for New Projects",
summary: "You create same project structure repeatedly — build automated template generator",
created_at: "2026-03-25T09:00:00Z",
stage: "trending",
council_session_id: "session-morning-2026-03-25-proactive",
promotion_reason: "First proactive idea from today's council session",
proposing_model: "deepseek-v3",
origin_type: "proactive",
origin_tags: ["⚙️ Efficiency Gap"],
proactive_lens: "efficiency",
idea_ledger_id: "ledger-proactive-001",
council_confidence: "high",
debate_summary: [
"Devil's Advocate: Current project structures vary too much for templates",
"Resource Check: 2-3 days to build CLI tool, minimal maintenance",
"Impact Estimator: 50% time savings on new project setup",
"Timing Analyst: Market slow, perfect time for infrastructure work"
],
key_disagreements: [
{ model: "claude-sonnet-4-6", concern: "Projects vary too much for templates", resolution: "Add flexible template variables and presets" },
{ model: "llama-3.1-70b", concern: "Requires CLI learning curve", resolution: "Build UI wrapper with preset library" }
],
council_recommendation: "Build CLI template generator with flexible variables. Add preset library (Next.js, Remix, FastAPI, etc).",
passing_reasoning: "Clear ROI, minimal risk, matches current project patterns",
escalation_type: "auto",
session_type: "scheduled",
scheduled_for: "2026-03-25T17:00:00Z",
session_time_of_day: "evening",
decision: null,
is_hidden: false
},
{
id: "rdc-004",
title: "Client-Facing Status Page",
summary: "No central status page — clients ask for updates manually via email",
created_at: "2026-03-25T09:00:00Z",
stage: "opportunity",
council_session_id: "session-morning-2026-03-25-proactive",
promotion_reason: "Proactive infrastructure idea promoted by council",
proposing_model: "gemini-ultra",
origin_type: "proactive",
origin_tags: ["🏗️ Infrastructure Idea"],
proactive_lens: "infrastructure",
idea_ledger_id: "ledger-proactive-002",
council_confidence: "medium",
debate_summary: [
"Devil's Advocate: Status pages often become outdated and damage trust",
"Resource Check: 1 week with Stripe Status clone, 40hr/mo maintenance",
"Impact Estimator: 30% support ticket reduction, faster deal velocity",
"Timing Analyst: Q2 roadmap light, good window for feature dev"
],
key_disagreements: [
{ model: "claude-sonnet-4-6", concern: "Status pages become outdated", resolution: "Auto-sync with GitHub issues, manual override only" },
{ model: "deepseek-v3", concern: "Maintenance burden", resolution: "Start with read-only status on docs site, upgrade later" }
],
council_recommendation: "Deploy read-only status page on docs.flobase.ai, sync with GitHub issues. Start with Q2 projects only.",
passing_reasoning: "Low-risk MVP, clear business impact, addresses recurring client pain point",
escalation_type: "auto",
session_type: "scheduled",
scheduled_for: "2026-03-25T17:00:00Z",
session_time_of_day: "evening",
decision: null,
is_hidden: false
},
// Ingested from usage analysis
{
id: "rdc-ingest-001",
title: "OpenClaw Model Routing Optimization",
summary: "Frequent requests for model selection suggest need for smarter auto-routing based on task complexity",
created_at: "2026-03-25T23:36:37.974Z",
stage: "trending",
council_session_id: "auto-ingestion",
promotion_reason: "Detected from usage pattern analysis",
proposing_model: "ollama-cloud/qwen3.5:397b-cloud",
origin_type: "proactive",
origin_tags: ["⚙️ Efficiency Gap", "🔍 Pattern Detected"],
proactive_lens: "pattern",
idea_ledger_id: "ledger-ingest-001",
council_confidence: "medium",
debate_summary: [
"Devil's Advocate: Current model routing works, optimization is marginal",
"Resource Check: 1 day to implement smart routing logic",
"Impact Estimator: 15% reduction in token waste",
"Timing Analyst: Q2 roadmap allows for optimization work"
],
key_disagreements: [
{ model: "claude-sonnet-4-6", concern: "Current routing works", resolution: "Add gradual rollout with A/B testing" },
{ model: "llama-3.1-70b", concern: "Token savings small", resolution: "Focus on cost reduction for scale" }
],
council_recommendation: "Implement intelligent model routing based on task complexity detected from session logs. Start with A/B test.",
passing_reasoning: "Clear optimization path, measurable cost impact, low implementation risk",
escalation_type: "auto",
session_type: "scheduled",
scheduled_for: "2026-03-25T17:00:00Z",
session_time_of_day: "evening",
decision: null,
is_hidden: false
},
{
id: "rdc-ingest-002",
title: "Telegram Webhook Integration",
summary: "Multiple Telegram interactions indicate opportunity for better webhook handling and async message processing",
created_at: "2026-03-25T23:36:37.974Z",
stage: "trending",
council_session_id: "auto-ingestion",
promotion_reason: "Detected from usage pattern analysis",
proposing_model: "ollama-cloud/qwen3.5:397b-cloud",
origin_type: "proactive",
origin_tags: ["⚙️ Efficiency Gap", "🏗️ Infrastructure Idea"],
proactive_lens: "infrastructure",
idea_ledger_id: "ledger-ingest-002",
council_confidence: "medium",
debate_summary: [
"Devil's Advocate: Telegram integration works, webhook changes aren't urgent",
"Resource Check: 3 days to implement async webhook processor",
"Impact Estimator: 20% faster Telegram response times",
"Timing Analyst: Good time before peak season"
],
key_disagreements: [
{ model: "deepseek-v3", concern: "Not urgent", resolution: "Add to Q2 backlog" },
{ model: "gemini-ultra", concern: "Complex async architecture", resolution: "Start with simple queue buffering" }
],
council_recommendation: "Build async webhook processing layer with Redis queue buffer. Add dead-letter queue for failed messages.",
passing_reasoning: "High ROI on response times, handles current traffic volume, scalable architecture",
escalation_type: "auto",
session_type: "scheduled",
scheduled_for: "2026-03-25T17:00:00Z",
session_time_of_day: "evening",
decision: null,
is_hidden: false
},
{
id: "rdc-ingest-003",
title: "Docker Deployment Pipeline",
summary: "Repeated Docker build requests suggest need for automated CI/CD pipeline integration",
created_at: "2026-03-25T23:36:37.974Z",
stage: "opportunity",
council_session_id: "auto-ingestion",
promotion_reason: "Detected from usage pattern analysis - Docker builds recurring",
proposing_model: "ollama-cloud/qwen3.5:397b-cloud",
origin_type: "proactive",
origin_tags: ["⚙️ Efficiency Gap", "🏗️ Infrastructure Idea"],
proactive_lens: "efficiency",
idea_ledger_id: "ledger-ingest-003",
council_confidence: "high",
debate_summary: [
"Devil's Advocate: Manual Docker builds work, CI/CD may be overkill",
"Resource Check: 1 week to set up GitHub Actions workflow",
"Impact Estimator: 4 hours/week saved on deployment",
"Timing Analyst: Perfect for pre-production work"
],
key_disagreements: [
{ model: "claude-sonnet-4-6", concern: "Overkill for current scale", resolution: "Start with basic push-to-registry workflow" },
{ model: "llama-3.1-70b", concern: "Security concerns", resolution: "Add approval gates for production pushes" }
],
council_recommendation: "Build GitHub Actions CI/CD pipeline for Docker builds with approval gates for production. Add artifact registry.",
passing_reasoning: "Clear time savings, handles current workflow, security-aware architecture",
escalation_type: "auto",
session_type: "scheduled",
scheduled_for: "2026-03-25T17:00:00Z",
session_time_of_day: "evening",
decision: null,
is_hidden: false
},
{
id: "rdc-ingest-004",
title: "Self-Improvement Automation",
summary: "Dashboard updates require frequent redeploy - automate with GitHub Actions workflow",
created_at: "2026-03-25T23:36:37.974Z",
stage: "opportunity",
council_session_id: "auto-ingestion",
promotion_reason: "Detected from usage pattern analysis - frequent redeploy requests",
proposing_model: "ollama-cloud/qwen3.5:397b-cloud",
origin_type: "proactive",
origin_tags: ["⚙️ Efficiency Gap", "🏗️ Infrastructure Idea"],
proactive_lens: "infrastructure",
idea_ledger_id: "ledger-ingest-004",
council_confidence: "high",
debate_summary: [
"Devil's Advocate: Manual redeploy is quick, automation complexity may not be worth it",
"Resource Check: 2 days to configure GitHub Actions with Docker cache",
"Impact Estimator: 30 minutes/week saved on deployment",
"Timing Analyst: Excellent timing with current dashboard development"
],
key_disagreements: [
{ model: "gemini-ultra", concern: "Manual redeploy is simple", resolution: "Add one-click redeploy button first, then automate" },
{ model: "deepseek-v3", concern: "CI/CD setup complexity", resolution: "Use pre-built Docker images for faster pushes" }
],
council_recommendation: "Add GitHub Actions workflow that builds and deploys to Vercel on push to main branch. Cache Docker layers for speed.",
passing_reasoning: "Minimal setup complexity, significant time savings, proven CI/CD pattern",
escalation_type: "auto",
session_type: "scheduled",
scheduled_for: "2026-03-25T17:00:00Z",
session_time_of_day: "evening",
decision: null,
is_hidden: false
}
];
export { mockedItems };
+790
View File
@@ -0,0 +1,790 @@
"use client";
import { useState, useEffect } from "react";
import { useI18n } from "@/lib/i18n";
type CouncilDecision = "approved" | "rejected" | "snoozed";
type CouncilConfidence = "high" | "medium" | "low";
interface EvidenceTelemetry {
evidence_snippets?: string[];
source_ids?: string[];
recurrence_count?: number;
most_recent_at?: string | null;
oldest_at?: string | null;
staleness_days?: number | null;
stale_signal_decay?: number | null;
confidence_score?: number | null;
confidence_label?: CouncilConfidence;
}
interface DecisionHistoryEntry {
decision: CouncilDecision;
note?: string | null;
actor?: string | null;
at: string;
snooze_until?: string | null;
}
interface CouncilItem {
id: string;
title: string;
summary: string;
created_at: string;
stage: "trending" | "opportunity" | "council";
proposing_model: string;
origin_type: "reactive" | "proactive";
origin_tags: string[];
proactive_lens: "efficiency" | "infrastructure" | "pattern" | "external" | null;
council_confidence: CouncilConfidence;
debate_summary: string[];
key_disagreements: { model: string; concern: string; resolution: string }[];
council_recommendation: string;
passing_reasoning: string;
session_type: "scheduled" | "emergency";
session_time_of_day: "morning" | "evening";
decision: null | CouncilDecision;
decision_note?: string | null;
decision_at?: string | null;
decision_history?: DecisionHistoryEntry[];
snooze_until?: string | null;
is_hidden: boolean;
hidden_until?: string | null;
work_order_id?: string | null;
work_order_handoff_id?: string | null;
evidence_telemetry?: EvidenceTelemetry;
source_ids?: string[];
recurrence_count?: number;
confidence_score?: number;
stale_signal_decay?: number;
}
const RDCOUNCIL_TABS = [
{ id: "trending", label: "nav.trending" },
{ id: "opportunity", label: "nav.opportunity" },
{ id: "council", label: "nav.rdCouncil" },
] as const;
function formatTimestamp(ts?: string | null): string {
if (!ts) return "Unknown";
const d = new Date(ts);
return Number.isNaN(d.getTime()) ? ts : d.toLocaleString();
}
function isFutureTimestamp(ts?: string | null, now = new Date()): boolean {
if (!ts) return false;
const date = new Date(ts);
return !Number.isNaN(date.getTime()) && date.getTime() > now.getTime();
}
function getDecisionLabel(decision: CouncilDecision | null): string {
if (decision === "approved") return "Approved";
if (decision === "rejected") return "Rejected";
if (decision === "snoozed") return "Snoozed";
return "Undecided";
}
function getActionLabel(decision: CouncilDecision): string {
if (decision === "approved") return "Approve";
if (decision === "rejected") return "Reject";
return "Snooze";
}
function getActionErrorMessage(
decision: CouncilDecision,
input: {
response?: { status?: number; statusText?: string };
payload?: Record<string, unknown> | null;
error?: unknown;
}
): string {
const status = typeof input.response?.status === "number" ? input.response.status : null;
const payload = input.payload ?? {};
const payloadMessage =
typeof payload.message === "string" && payload.message.trim() !== ""
? payload.message
: typeof payload.error === "string" && payload.error.trim() !== ""
? payload.error
: null;
const errorMessage =
input.error instanceof Error && input.error.message.trim() !== "" ? input.error.message : null;
const statusText =
typeof input.response?.statusText === "string" && input.response.statusText.trim() !== ""
? input.response.statusText
: null;
const message = payloadMessage ?? errorMessage ?? statusText ?? "Request failed";
return `${getActionLabel(decision)} failed${status ? ` (${status})` : ""}: ${message}`;
}
function getConfidenceTone(confidence: CouncilConfidence | undefined): string {
if (confidence === "high") return "bg-green-100 text-green-800";
if (confidence === "medium") return "bg-amber-100 text-amber-800";
return "bg-slate-200 text-slate-700";
}
function summarizeOperatorState(item: CouncilItem, now = new Date()) {
const latestDecision =
item.decision_history?.[item.decision_history.length - 1] ??
(item.decision
? {
decision: item.decision,
note: item.decision_note ?? null,
actor: null,
at: item.decision_at ?? item.created_at,
snooze_until: item.snooze_until ?? null,
}
: null);
const workOrderId = item.work_order_id ?? item.work_order_handoff_id ?? null;
const hiddenUntil = item.hidden_until ?? item.snooze_until ?? null;
const badges: Array<{ label: string; className: string }> = [];
let summary = "Awaiting operator decision";
if (item.decision === "approved") {
summary = workOrderId ? "Approved and handed off" : "Approved";
badges.push({ label: "Approved", className: "bg-emerald-100 text-emerald-800" });
if (workOrderId) {
badges.push({ label: "Work order ready", className: "bg-emerald-100 text-emerald-800" });
}
} else if (item.decision === "rejected") {
summary = "Rejected";
badges.push({ label: "Rejected", className: "bg-rose-100 text-rose-800" });
} else if (item.decision === "snoozed" || item.is_hidden) {
summary = item.is_hidden ? "Snoozed and hidden" : "Snoozed";
if (item.decision === "snoozed") {
badges.push({ label: "Snoozed", className: "bg-amber-100 text-amber-900" });
}
if (item.is_hidden) {
badges.push({ label: "Hidden", className: "bg-slate-200 text-slate-700" });
}
} else {
badges.push({ label: "Undecided", className: "bg-slate-200 text-slate-700" });
}
return {
summary,
badges,
workOrderId,
hiddenUntil,
historyCount: item.decision_history?.length ?? 0,
latestDecision,
evidenceConfidence:
item.evidence_telemetry?.confidence_label ?? item.council_confidence,
evidenceConfidenceScore:
item.evidence_telemetry?.confidence_score ?? item.confidence_score ?? null,
recurrenceCount:
item.evidence_telemetry?.recurrence_count ?? item.recurrence_count ?? 0,
sourceCount:
item.evidence_telemetry?.source_ids?.length ?? item.source_ids?.length ?? 0,
stalenessDays: item.evidence_telemetry?.staleness_days ?? null,
isCurrentlyHidden: item.is_hidden || isFutureTimestamp(hiddenUntil, now),
};
}
function getActionAvailability(item: CouncilItem, now = new Date()) {
const hiddenUntil = item.hidden_until ?? item.snooze_until ?? null;
const hiddenUntilFuture = isFutureTimestamp(hiddenUntil, now);
const workOrderId = item.work_order_id ?? item.work_order_handoff_id ?? null;
return {
approved:
item.decision === "approved"
? {
disabled: true,
label: "Approved",
reason: workOrderId
? `Already approved and handed off to work order ${workOrderId}.`
: "Already approved.",
}
: {
disabled: false,
label: "Approve",
reason: null,
},
rejected:
item.decision === "rejected"
? {
disabled: true,
label: "Rejected",
reason: "Already rejected.",
}
: {
disabled: false,
label: "Reject",
reason: null,
},
snoozed:
item.is_hidden || hiddenUntilFuture || item.decision === "snoozed"
? {
disabled: true,
label: hiddenUntil ? `Snoozed until ${formatTimestamp(hiddenUntil)}` : "Hidden",
reason: hiddenUntil
? `Item is hidden until ${formatTimestamp(hiddenUntil)}.`
: "Item is already hidden.",
}
: {
disabled: false,
label: "Snooze 7d",
reason: null,
},
};
}
function Card({
item,
t,
onDecision,
isActionPending,
actionError,
}: {
item: CouncilItem;
t: any;
onDecision: (itemId: string, decision: CouncilDecision) => void;
isActionPending: boolean;
actionError?: string | null;
}) {
const [expanded, setExpanded] = useState(false);
const [showModal, setShowModal] = useState(false);
const decisionHistory = item.decision_history ?? [];
const operatorState = summarizeOperatorState(item);
const actionAvailability = getActionAvailability(item);
const disabledReasons = Object.values(actionAvailability)
.map((action) => action.reason)
.filter((reason): reason is string => Boolean(reason));
return (
<div className="bg-[var(--card)] border border-[var(--border)] rounded-xl p-5 space-y-4">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2">
<div>
<span className="text-2xl font-bold text-[var(--text)] min-w-[160px]">{item.title}</span>
</div>
<div className="flex items-center gap-2 flex-wrap">
<span className="text-xs px-2 py-0.5 rounded-full bg-[var(--accent)]/10 text-[var(--accent)]">
{t("rdc.estimated")} {item.proposing_model}
</span>
<span className="text-xs px-2 py-0.5 rounded-full bg-[var(--border)] text-[var(--text-muted)]">
{item.session_type === "scheduled" ? t("rdc.scheduled") : t("rdc.emergency")}
</span>
{item.origin_type === "proactive" && item.proactive_lens && (
<span className="text-xs px-2 py-0.5 rounded-full bg-[var(--accent)]/30 text-[var(--accent)]">
{t(item.proactive_lens === "efficiency" ? "rdc.originTags.efficiency" :
item.proactive_lens === "infrastructure" ? "rdc.originTags.infrastructure" :
item.proactive_lens === "pattern" ? "rdc.originTags.pattern" : "rdc.originTags.external")}
</span>
)}
<span className={`text-xs px-2 py-0.5 rounded-full ${getConfidenceTone(operatorState.evidenceConfidence)}`}>
{(operatorState.evidenceConfidence ?? "medium").toUpperCase()} confidence
{typeof operatorState.evidenceConfidenceScore === "number" ? ` ${operatorState.evidenceConfidenceScore.toFixed(3)}` : ""}
</span>
{operatorState.badges.map((badge) => (
<span key={badge.label} className={`text-xs px-2 py-0.5 rounded-full ${badge.className}`}>
{badge.label}
</span>
))}
{item.origin_tags.length > 0 && item.origin_tags.map((tag, i) => (
<span key={i} className="text-xs px-2 py-0.5 rounded-full bg-[var(--accent)]/20 text-[var(--accent)]">
{tag}
</span>
))}
</div>
<div className="text-xs text-[var(--text-muted)] whitespace-nowrap">
{t("rdc.detected")} {formatTimestamp(item.created_at)}
</div>
</div>
{/* Summary */}
<div className="text-sm text-[var(--text-muted)] leading-relaxed">
{item.summary}
</div>
<div className="grid gap-3 md:grid-cols-2">
<div className="border border-[var(--border)] rounded-lg p-3 space-y-2">
<div className="text-xs font-semibold text-[var(--text-muted)]">Operator State</div>
<div className="text-sm font-medium text-[var(--text)]">{operatorState.summary}</div>
<div className="text-xs text-[var(--text-muted)]">
Decision: {getDecisionLabel(item.decision)}
{item.decision_at ? ` on ${formatTimestamp(item.decision_at)}` : ""}
</div>
{operatorState.workOrderId && (
<div className="text-xs text-[var(--text-muted)]">
Work order: <span className="text-[var(--text)]">{operatorState.workOrderId}</span>
</div>
)}
{operatorState.hiddenUntil && (
<div className="text-xs text-[var(--text-muted)]">
Hidden until: <span className="text-[var(--text)]">{formatTimestamp(operatorState.hiddenUntil)}</span>
</div>
)}
{operatorState.historyCount > 0 && operatorState.latestDecision && (
<div className="text-xs text-[var(--text-muted)]">
History: {operatorState.historyCount} entr{operatorState.historyCount === 1 ? "y" : "ies"}
{operatorState.latestDecision.actor ? `, latest by ${operatorState.latestDecision.actor}` : ""}
{operatorState.latestDecision.at ? ` at ${formatTimestamp(operatorState.latestDecision.at)}` : ""}
</div>
)}
{operatorState.latestDecision?.note && (
<div className="text-xs text-[var(--text-muted)]">
Latest note: {operatorState.latestDecision.note}
</div>
)}
</div>
<div className="border border-[var(--border)] rounded-lg p-3 space-y-2">
<div className="text-xs font-semibold text-[var(--text-muted)]">Evidence Telemetry</div>
<div className="grid grid-cols-2 gap-2 text-xs text-[var(--text-muted)]">
<div>
Confidence: <span className="text-[var(--text)]">{operatorState.evidenceConfidence ?? "unknown"}</span>
</div>
<div>
Recurrence: <span className="text-[var(--text)]">{operatorState.recurrenceCount}</span>
</div>
<div>
Source IDs: <span className="text-[var(--text)]">{operatorState.sourceCount}</span>
</div>
<div>
Staleness: <span className="text-[var(--text)]">{operatorState.stalenessDays ?? "n/a"}d</span>
</div>
</div>
{item.evidence_telemetry?.most_recent_at && (
<div className="text-xs text-[var(--text-muted)]">
Most recent signal: <span className="text-[var(--text)]">{formatTimestamp(item.evidence_telemetry.most_recent_at)}</span>
</div>
)}
{item.evidence_telemetry?.evidence_snippets && item.evidence_telemetry.evidence_snippets.length > 0 && (
<div className="space-y-1">
{item.evidence_telemetry.evidence_snippets.slice(0, 3).map((snippet, index) => (
<div key={`${item.id}-evidence-${index}`} className="text-xs text-[var(--text-muted)]">
Evidence {index + 1}: <span className="text-[var(--text)]">{snippet}</span>
</div>
))}
</div>
)}
</div>
</div>
{/* Expandable Debate Summary */}
<div className="border-t border-[var(--border)] pt-3 mt-2">
<button
onClick={() => setExpanded(!expanded)}
className="text-xs font-medium text-[var(--text)] hover:text-[var(--accent)] transition-colors flex items-center gap-1"
>
{expanded ? "▼" : "▲"} {t("rdc.debateSummary")}
</button>
{expanded && (
<div className="mt-2 space-y-2">
{item.debate_summary.map((bullet, i) => (
<div key={i} className="flex items-start gap-2 text-xs text-[var(--text)]">
<span className="text-[var(--accent)]"></span>
<span>{bullet}</span>
</div>
))}
{item.key_disagreements.length > 0 && (
<div className="mt-2 pt-2 border-t border-[var(--border)]">
<div className="text-xs font-semibold text-[var(--text-muted)] mb-1">
{t("rdc.disagreements")}
</div>
{item.key_disagreements.map((disagreement, i) => (
<div key={i} className="text-xs text-[var(--text)]">
<span className="text-[var(--accent)] font-medium">{disagreement.model}:</span>
<span className="mx-1 opacity-60">- {disagreement.concern}</span>
<span className="mx-1 opacity-60"></span>
<span className="opacity-60">{disagreement.resolution}</span>
</div>
))}
</div>
)}
</div>
)}
</div>
{/* Recommendation */}
<div className="bg-[var(--bg)]/60 border border-[var(--accent)]/20 rounded-lg p-3">
<div className="text-xs font-semibold text-[var(--accent)] mb-1">
{t("rdc.recommendation")}
</div>
<div className="text-sm text-[var(--text)] leading-relaxed">
{item.council_recommendation}
</div>
<div className="mt-2 text-xs text-[var(--text-muted)]">
{t("rdc.passedReasoning")}: {item.passing_reasoning}
</div>
</div>
{decisionHistory.length > 0 && (
<div className="border border-[var(--border)] rounded-lg p-3 space-y-2">
<div className="text-xs font-semibold text-[var(--text-muted)]">Decision History</div>
{decisionHistory.slice().reverse().map((entry, index) => (
<div key={`${entry.at}-${index}`} className="text-xs text-[var(--text)]">
<span className="font-medium">
{entry.decision === "approved" ? "Approved" : entry.decision === "rejected" ? "Rejected" : "Snoozed"}
</span>
<span className="mx-2 text-[var(--text-muted)]">{formatTimestamp(entry.at)}</span>
{entry.actor && <span className="text-[var(--text-muted)]">by {entry.actor}</span>}
{entry.note && <div className="mt-1 text-[var(--text-muted)]">{entry.note}</div>}
{entry.snooze_until && (
<div className="mt-1 text-[var(--text-muted)]">Until {formatTimestamp(entry.snooze_until)}</div>
)}
</div>
))}
</div>
)}
{/* Action Buttons */}
<div className="pt-2 border-t border-[var(--border)] space-y-2">
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
<button
onClick={() => onDecision(item.id, "approved")}
disabled={isActionPending || actionAvailability.approved.disabled}
title={actionAvailability.approved.reason ?? undefined}
className="px-4 py-2 rounded-lg bg-emerald-600 text-white text-sm font-medium hover:bg-emerald-700 disabled:opacity-60 disabled:cursor-not-allowed transition-colors"
>
{isActionPending ? "Saving..." : actionAvailability.approved.label}
</button>
<button
onClick={() => onDecision(item.id, "rejected")}
disabled={isActionPending || actionAvailability.rejected.disabled}
title={actionAvailability.rejected.reason ?? undefined}
className="px-4 py-2 rounded-lg bg-rose-600 text-white text-sm font-medium hover:bg-rose-700 disabled:opacity-60 disabled:cursor-not-allowed transition-colors"
>
{isActionPending ? "Saving..." : actionAvailability.rejected.label}
</button>
<button
onClick={() => onDecision(item.id, "snoozed")}
disabled={isActionPending || actionAvailability.snoozed.disabled}
title={actionAvailability.snoozed.reason ?? undefined}
className="px-4 py-2 rounded-lg bg-amber-500 text-white text-sm font-medium hover:bg-amber-600 disabled:opacity-60 disabled:cursor-not-allowed transition-colors"
>
{isActionPending ? "Saving..." : actionAvailability.snoozed.label}
</button>
</div>
{disabledReasons.length > 0 && (
<div className="space-y-1">
{disabledReasons.map((reason) => (
<div key={reason} className="text-xs text-[var(--text-muted)]">
{reason}
</div>
))}
</div>
)}
{actionError && (
<div className="rounded-lg border border-rose-200 bg-rose-50 px-3 py-2 text-xs text-rose-800">
{actionError}
</div>
)}
<div className="grid grid-cols-2 gap-2">
<button
onClick={() => setShowModal(true)}
className="px-4 py-2 rounded-lg bg-[var(--accent)] text-white text-sm font-medium hover:bg-[var(--accent)]/90 transition-colors"
>
{t("rdc.escalate")}
</button>
<button
disabled
className="px-4 py-2 rounded-lg bg-[var(--card)] border border-[var(--border)] text-[var(--text-muted)] text-sm cursor-not-allowed"
>
{t("rdc.schedule")}
</button>
</div>
</div>
{/* Escalation Modal */}
{showModal && (
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="bg-[var(--card)] border border-[var(--border)] rounded-xl max-w-md w-full p-5 space-y-4 shadow-2xl">
<div className="flex items-center justify-between">
<h3 className="text-lg font-bold text-[var(--text)]">{t("rdc.escalateToCouncil")}</h3>
<button
onClick={() => setShowModal(false)}
className="text-[var(--text-muted)] hover:text-[var(--text)]"
>
</button>
</div>
<div className="space-y-3">
<div>
<label className="block text-xs font-medium text-[var(--text-muted)] mb-1">
{t("rdc.whyEscalate")}
</label>
<textarea
className="w-full px-3 py-2 rounded-lg bg-[var(--bg)] border border-[var(--border)] text-sm text-[var(--text)]"
rows={2}
placeholder="Optional: Why are you escalating this?"
/>
</div>
<div>
<label className="block text-xs font-medium text-[var(--text-muted)] mb-2">
{t("rdc.priority")}
</label>
<div className="flex gap-2">
<button
className="flex-1 px-3 py-2 rounded-lg bg-[var(--accent)] text-white text-sm font-medium"
onClick={() => {
setShowModal(false);
alert("Normal priority item queued for next council session");
}}
>
{t("rdc.normal")}
</button>
<button
className="flex-1 px-3 py-2 rounded-lg bg-red-600 text-white text-sm font-medium hover:bg-red-700"
onClick={() => {
setShowModal(false);
alert("Urgent council session convened - memo incoming");
}}
>
{t("rdc.urgent")}
</button>
</div>
</div>
</div>
<div className="pt-2 border-t border-[var(--border)] flex justify-end gap-2">
<button
onClick={() => setShowModal(false)}
className="px-4 py-2 rounded-lg bg-[var(--bg)] border border-[var(--border)] text-sm text-[var(--text-muted)] hover:text-[var(--text)]"
>
{t("common.cancel")}
</button>
</div>
</div>
</div>
)}
</div>
);
}
function getOperatorHeaders(): HeadersInit {
if (typeof window === "undefined") return {};
const token = window.localStorage.getItem("openclaw_operator_token")?.trim();
return token ? { "x-openclaw-operator-token": token } : {};
}
export default function RDCouncilPage() {
const { t } = useI18n();
const [activeTab, setActiveTab] = useState("council");
const [items, setItems] = useState<CouncilItem[]>([]);
const [loading, setLoading] = useState(true);
const [loadError, setLoadError] = useState<string | null>(null);
const [actionKey, setActionKey] = useState<string | null>(null);
const [actionErrors, setActionErrors] = useState<Record<string, string>>({});
const [reloadNonce, setReloadNonce] = useState(0);
useEffect(() => {
let cancelled = false;
const fetchItems = async () => {
if (!cancelled) {
setLoading(true);
setLoadError(null);
}
try {
const response = await fetch("/api/rd-council-items", {
cache: "no-store",
headers: getOperatorHeaders(),
});
const data = await response.json().catch(() => null);
if (!response.ok || !data?.success) {
throw new Error(
typeof data?.message === "string" && data.message.trim() !== ""
? data.message
: response.statusText || "Failed to load R&D Council items."
);
}
if (!cancelled) {
setItems(Array.isArray(data.items) ? data.items : []);
}
} catch (error) {
console.error("Failed to load R&D Council items:", error);
if (!cancelled) {
setItems([]);
setLoadError(
error instanceof Error && error.message.trim() !== ""
? error.message
: "Failed to load R&D Council items."
);
}
} finally {
if (!cancelled) setLoading(false);
}
};
fetchItems();
return () => {
cancelled = true;
};
}, [reloadNonce]);
const submitDecision = async (itemId: string, decision: CouncilDecision) => {
const key = `${itemId}:${decision}`;
setActionKey(key);
setActionErrors((current) => {
if (!current[itemId]) return current;
const next = { ...current };
delete next[itemId];
return next;
});
try {
const request =
decision === "approved"
? {
url: "/api/approve-v25",
body: {
itemId,
note: "Approved from the R&D Council dashboard.",
actor: "rd-council-ui",
},
}
: decision === "rejected"
? {
url: "/api/reject-v25",
body: {
itemId,
note: "Rejected from the R&D Council dashboard.",
actor: "rd-council-ui",
},
}
: {
url: `/api/rd-council-items/${encodeURIComponent(itemId)}/snooze`,
body: {
note: "Snoozed from the R&D Council dashboard.",
actor: "rd-council-ui",
snoozeUntil: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
},
};
const response = await fetch(request.url, {
method: "POST",
headers: {
"Content-Type": "application/json",
...getOperatorHeaders(),
},
body: JSON.stringify(request.body),
});
const data = await response.json().catch(() => null);
if (!response.ok || !data?.success || !data?.item) {
throw new Error(
getActionErrorMessage(decision, {
response,
payload: data,
})
);
}
setItems((current) => current.map((item) => (item.id === data.item.id ? data.item : item)));
} catch (error) {
console.error("Failed to persist R&D Council decision:", error);
setActionErrors((current) => ({
...current,
[itemId]:
error instanceof Error && error.message.trim() !== ""
? error.message
: getActionErrorMessage(decision, { error }),
}));
} finally {
setActionKey(null);
}
};
return (
<div className="min-h-screen bg-[var(--bg)] p-4 sm:p-8">
<div className="max-w-6xl mx-auto space-y-6">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<h1 className="text-2xl font-bold text-[var(--text)]">R&D Council</h1>
<p className="text-sm text-[var(--text-muted)] mt-1">
{t("rdc.description")}
</p>
</div>
<div className="flex items-center gap-2">
<div className="px-3 py-1.5 rounded-lg bg-[var(--accent)]/10 text-[var(--accent)] font-medium text-sm">
{items.length} {t("rdc.active")}
</div>
</div>
</div>
{/* Tabs */}
<div className="flex flex-wrap gap-2 border-b border-[var(--border)] pb-1">
{RDCOUNCIL_TABS.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
activeTab === tab.id
? "bg-[var(--accent)]/15 text-[var(--accent)]"
: "text-[var(--text-muted)] hover:text-[var(--text)]"
}`}
>
{t(tab.label)}
{tab.id === "council" && <span className="ml-2 text-xs bg-[var(--accent)] text-white px-1.5 py-0.5 rounded-full">{items.length}</span>}
</button>
))}
</div>
{/* Content */}
<div className="space-y-4">
{loading ? (
<div className="rounded-xl border border-[var(--border)] bg-[var(--card)] px-6 py-12 text-center text-[var(--text-muted)]">
{t("common.loading")}...
</div>
) : loadError ? (
<div className="rounded-xl border border-rose-200 bg-rose-50 px-6 py-8 text-center space-y-3">
<div className="text-sm font-medium text-rose-900">Failed to load live R&amp;D Council state</div>
<div className="text-sm text-rose-800">{loadError}</div>
<div>
<button
onClick={() => setReloadNonce((current) => current + 1)}
className="px-4 py-2 rounded-lg bg-rose-700 text-white text-sm font-medium hover:bg-rose-800 transition-colors"
>
Retry
</button>
</div>
</div>
) : activeTab === "council" ? (
<div className="grid gap-4">
{items.map((item) => (
<Card
key={item.id}
item={item}
t={t}
onDecision={submitDecision}
isActionPending={actionKey !== null && actionKey.startsWith(`${item.id}:`)}
actionError={actionErrors[item.id] ?? null}
/>
))}
{items.length === 0 && (
<div className="rounded-xl border border-[var(--border)] bg-[var(--card)] px-6 py-12 text-center space-y-2">
<div className="text-sm font-medium text-[var(--text)]">No council items are currently queued.</div>
<div className="text-sm text-[var(--text-muted)]">
The backend returned an empty list, so there is nothing for an operator to review right now.
</div>
</div>
)}
</div>
) : (
<div className="text-center py-12 text-[var(--text-muted)]">
{t("rdc.comingSoon")}
</div>
)}
</div>
{/* Global Memo Panel */}
<div className="mt-6 border-t border-[var(--border)] pt-6">
<div className="bg-[var(--accent)]/5 border border-[var(--accent)]/20 rounded-xl p-4">
<div className="flex items-center justify-between mb-2">
<h2 className="text-sm font-semibold text-[var(--text)]">
{t("rdc.latestMemo")}
</h2>
<span className="text-xs text-[var(--text-muted)]">
{formatTimestamp("2026-03-25T09:00:00Z")}
</span>
</div>
<p className="text-sm text-[var(--text)]">
{t("rdc.memoPreview")}
</p>
</div>
</div>
</div>
</div>
);
}
+50
View File
@@ -0,0 +1,50 @@
// Types for R&D Council Dashboard
export type Stage = 'trending' | 'opportunity' | 'council';
export type SessionType = 'scheduled' | 'emergency';
export type TimeOfDay = 'morning' | 'evening';
export type Decision = 'approved' | 'rejected' | 'sent_back' | 'snoozed' | null;
export type EscalationType = 'auto' | 'manual_normal' | 'manual_urgent';
export type OriginType = 'reactive' | 'proactive';
export type ProactiveLens = 'efficiency' | 'infrastructure' | 'pattern' | 'external' | null;
export type CouncilConfidence = 'low' | 'medium' | 'high';
export interface CouncilItem {
id: string;
title: string;
summary: string;
created_at: string;
stage: Stage;
council_session_id: string;
promotion_reason: string;
proposing_model: string;
origin_type: OriginType;
origin_tags: string[];
proactive_lens: ProactiveLens;
idea_ledger_id: string;
council_confidence: CouncilConfidence;
debate_summary: string[];
key_disagreements: { model: string; concern: string; resolution: string }[];
council_recommendation: string;
passing_reasoning: string;
escalation_type: EscalationType;
session_type: SessionType;
scheduled_for: string;
session_time_of_day: TimeOfDay;
decision: Decision;
decision_note?: string;
decision_at?: string;
snooze_until?: string;
is_hidden: boolean;
hidden_until?: string;
}
export interface Memo {
id: string;
session_id: string;
session_time: string;
proposer: string;
summary: string;
recommendations: string[];
created_at: string;
}
+65
View File
@@ -0,0 +1,65 @@
import type { ReactNode } from 'react';
export interface CommandConfig {
id: string;
label: string;
description: string;
endpoint: string;
method: 'POST';
variant: 'primary' | 'success' | 'danger' | 'neutral';
icon: ReactNode;
requiresConfirmation: boolean;
confirmationText?: string;
sideEffect: 'none' | 'writes' | 'external';
}
const commands: CommandConfig[] = [
{
id: 'dry-run',
label: 'Dry Run',
description: 'Preview improvement items without writing files.',
endpoint: '/api/dry-run',
method: 'POST',
variant: 'neutral',
icon: null,
requiresConfirmation: false,
sideEffect: 'none',
},
{
id: 'proactive-real',
label: 'Run Proactive Analysis',
description: 'Analyze usage and write deduped R&D council + ledger records.',
endpoint: '/api/proactive-real',
method: 'POST',
variant: 'primary',
icon: null,
requiresConfirmation: true,
confirmationText: 'This writes deduped proactive ideas to rd-council-items.json and idea_ledger.json.',
sideEffect: 'writes',
},
{
id: 'ingest-usage',
label: 'Ingest Usage Data',
description: 'Run canonical usage ingestion from available session data.',
endpoint: '/api/ingest-usage',
method: 'POST',
variant: 'primary',
icon: null,
requiresConfirmation: true,
confirmationText: 'This updates the local R&D council and idea ledger from usage signals.',
sideEffect: 'writes',
},
{
id: 'weekly-review',
label: 'Weekly Review',
description: 'Summarize current council and ledger state without side effects.',
endpoint: '/api/weekly-review',
method: 'POST',
variant: 'success',
icon: null,
requiresConfirmation: false,
sideEffect: 'none',
},
];
export { commands };
+302
View File
@@ -0,0 +1,302 @@
"use client";
import { useState, useCallback, useEffect } from "react";
import { useI18n } from "@/lib/i18n";
import { commands, type CommandConfig } from "./commands";
import Link from "next/link";
// Icons
const Icons = {
Refresh: () => (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"/><path d="M16 21h5v-5"/>
</svg>
),
Check: () => (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="20 6 9 17 4 12"/>
</svg>
),
X: () => (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
</svg>
),
Search: () => (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>
</svg>
),
Info: () => (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/>
</svg>
),
Terminal: () => (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/>
</svg>
),
};
const Button: React.FC<{
config: CommandConfig;
isLoading: boolean;
onClick: () => void;
disabled: boolean;
}> = ({ config, isLoading, onClick, disabled }) => {
return (
<button
type="button"
onClick={onClick}
disabled={disabled || isLoading}
className={`flex items-center justify-center gap-2 px-4 py-2 rounded-md font-medium transition-all focus:outline-none w-full sm:w-auto text-sm ${
isLoading ? 'opacity-50 cursor-wait' : ''
} ${
config.variant === 'primary' ? 'bg-blue-600 hover:bg-blue-700 text-white' :
config.variant === 'success' ? 'bg-green-600 hover:bg-green-700 text-white' :
config.variant === 'danger' ? 'bg-red-600 hover:bg-red-700 text-white' :
'bg-gray-600 hover:bg-gray-700 text-white'
}`}
>
{isLoading ? (
<span className="animate-spin rounded-full h-4 w-4 border-b-2 border-white" />
) : (
config.icon
)}
{config.label}
</button>
);
};
// Status badges for improvement items
const StatusBadge: React.FC<{ status: "Pending" | "Approved" | "Rejected" }> = ({ status }) => {
const styles = {
Pending: "bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300",
Approved: "bg-emerald-100 text-emerald-800 dark:bg-emerald-900/30 dark:text-emerald-300",
Rejected: "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300",
};
return (
<span className={`px-2.5 py-0.5 rounded text-xs font-medium ${styles[status] || styles.Pending}`}>
{status}
</span>
);
};
interface Project {
id: string;
title: string;
description: string;
status: "Pending" | "Approved" | "Rejected";
date: string;
tasks: any[];
}
function getOperatorHeaders(): HeadersInit {
if (typeof window === "undefined") return {};
const token = window.localStorage.getItem("openclaw_operator_token")?.trim();
return token ? { "x-openclaw-operator-token": token } : {};
}
export default function SelfImprovementPage() {
const { t } = useI18n();
const [isLoading, setIsLoading] = useState<Record<string, boolean>>({});
const [lastResult, setLastResult] = useState<{ success: boolean; message: string; version?: string; details?: unknown } | null>(null);
const [logs, setLogs] = useState<string[]>([]);
const [projectError, setProjectError] = useState<string | null>(null);
const [projects, setProjects] = useState<Project[]>([]);
const [loadingProjects, setLoadingProjects] = useState(true);
// Load projects on mount
useEffect(() => {
const fetchProjects = async () => {
try {
const response = await fetch("/api/projects", { headers: getOperatorHeaders() });
const data = await response.json().catch(() => ({ success: false, message: 'Invalid JSON response from command API' }));
if (!response.ok || !data.success) {
throw new Error(data.message || data.error || `Project API failed (${response.status})`);
}
setProjects(Array.isArray(data.projects) ? data.projects : []);
setProjectError(null);
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Failed to load projects';
setProjectError(errorMsg);
setLogs((prev) => [`[${new Date().toLocaleTimeString()}] ❌ Load projects: ${errorMsg}`, ...prev].slice(0, 50));
} finally {
setLoadingProjects(false);
}
};
fetchProjects();
}, []);
const executeCommand = useCallback(async (config: CommandConfig) => {
if (isLoading[config.id]) return;
if (config.requiresConfirmation && !window.confirm(config.confirmationText || `Are you sure you want to ${config.label}?`)) return;
const started = new Date();
setIsLoading((prev) => ({ ...prev, [config.id]: true }));
setLogs((prev) => [`[${started.toLocaleTimeString()}] ▶ ${config.label}: started`, ...prev].slice(0, 50));
try {
const response = await fetch(config.endpoint, {
method: config.method,
headers: {
'Content-Type': 'application/json',
...getOperatorHeaders(),
},
body: JSON.stringify({ timestamp: started.toISOString(), confirm: config.requiresConfirmation }),
});
const data = await response.json().catch(() => ({ success: false, message: 'Invalid JSON response from command API' }));
if (!response.ok || data.success === false) {
throw new Error(data.message || data.error || `Command failed (${response.status})`);
}
setLastResult({
success: true,
message: data.message || 'Command completed successfully',
version: data.version,
details: data.details,
});
const durationMs = Date.now() - started.getTime();
const outputLines = Array.isArray(data.log_lines) ? data.log_lines : String(data.output || data.message || '').split('\n').filter(Boolean);
setLogs((prev) => [
`[${new Date().toLocaleTimeString()}] ✅ ${config.label}: ${data.message} (${durationMs}ms)`,
...outputLines.map((line: string) => ` ${line}`),
...prev,
].slice(0, 80));
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
setLastResult({ success: false, message: errorMsg });
setLogs((prev) => [`[${new Date().toLocaleTimeString()}] ❌ ${config.label}: ${errorMsg}`, ...prev].slice(0, 80));
} finally {
setIsLoading((prev) => ({ ...prev, [config.id]: false }));
}
}, [isLoading]);
return (
<div className="min-h-screen bg-[var(--bg)] p-4 sm:p-8">
<div className="max-w-6xl mx-auto space-y-8">
{/* Header */}
<header className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 border-b border-[var(--border)] pb-6">
<div>
<h1 className="text-2xl font-bold text-[var(--text)]">Self-Improvement Protocol</h1>
<p className="text-sm text-[var(--text-muted)] mt-1">
{t("selfImprovement.subtitle")}
</p>
</div>
<div className="flex items-center gap-3">
<StatusBadge status={lastResult?.success ? "Approved" : lastResult ? "Rejected" : "Pending"} />
<span className="text-xs text-[var(--text-muted)] font-mono">
{lastResult?.version ? `v${lastResult.version}` : 'v2.4.0'}
</span>
</div>
</header>
{/* Projects List */}
<section>
<div className="flex items-center justify-between mb-4">
<h2 className="text-sm font-semibold text-[var(--text-muted)] uppercase tracking-wider">
{t("selfImprovement.projects")}
</h2>
<span className="text-xs text-[var(--text-muted)]">
{projects.length} total
</span>
</div>
<div className="space-y-3">
{loadingProjects ? (
<div className="text-center py-12 text-[var(--text-muted)]">
Loading projects...
</div>
) : projects.length === 0 ? (
<div className="text-center py-12 text-[var(--text-muted)]">
{projectError ? `Failed to load projects: ${projectError}` : 'No improvement projects found'}
</div>
) : (
projects.map((project) => (
<div key={project.id} className="bg-[var(--card)] border border-[var(--border)] rounded-xl p-5">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2">
<div>
<h3 className="text-lg font-semibold text-[var(--text)]">{project.title}</h3>
<p className="text-sm text-[var(--text-muted)] mt-1 max-w-2xl">{project.description}</p>
</div>
<StatusBadge status={project.status} />
</div>
<div className="mt-4 flex items-center gap-4 text-xs text-[var(--text-muted)]">
<span>
{project.tasks.length} tasks
{project.tasks.some((t: any) => t.status === "completed")
? `${project.tasks.filter((t: any) => t.status === "completed").length} completed`
: ""
}
</span>
<span></span>
<span>Updated: {new Date(project.date).toLocaleDateString()}</span>
</div>
</div>
))
)}
</div>
</section>
{/* Action Grid */}
<section>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{commands.map((cmd) => (
<div key={cmd.id} className="space-y-2">
<Button
config={cmd}
isLoading={Boolean(isLoading[cmd.id])}
onClick={() => executeCommand(cmd)}
disabled={false}
/>
<p className="text-xs text-[var(--text-muted)] leading-snug">{cmd.description}</p>
{cmd.sideEffect !== 'none' ? (
<p className="text-[10px] uppercase tracking-wide text-amber-500">Requires confirmation</p>
) : null}
</div>
))}
</div>
</section>
{/* Terminal Output */}
<section>
<div className="flex items-center justify-between mb-2">
<h2 className="text-sm font-semibold text-[var(--text-muted)] uppercase tracking-wider">
{t("selfImprovement.terminal")}
</h2>
<div className="flex items-center gap-2 text-xs text-[var(--text-muted)]">
<Icons.Terminal />
<span>{logs.length} retained lines</span>
</div>
</div>
<div className="bg-[var(--bg)] border border-[var(--border)] rounded-md p-4 h-64 overflow-y-auto">
{logs.length === 0 ? (
<div className="text-[var(--text-muted)] italic">No commands executed yet</div>
) : (
<div className="space-y-2">
{logs.map((log, i) => (
<div key={i} className="text-xs font-mono break-words">
{log}
</div>
))}
</div>
)}
</div>
</section>
{/* Footer */}
<footer className="pt-6 border-t border-[var(--border)] text-center text-xs text-[var(--text-muted)]">
<p>Self-Improvement Dashboard &copy; {new Date().getFullYear()}</p>
<Link href="/" className="text-[var(--accent)] hover:underline mt-2 inline-block">
{t("common.backHome")}
</Link>
</footer>
</div>
</div>
);
}
+7
View File
@@ -208,6 +208,13 @@ const NAV_ITEMS: { group: string; items: { href: string; icon: NavIconName; labe
{ href: "/models", icon: "models", labelKey: "nav.models" }, { href: "/models", icon: "models", labelKey: "nav.models" },
], ],
}, },
{
group: "nav.rnd",
items: [
{ href: "/rd-council", icon: "alerts", labelKey: "nav.rdCouncil" },
{ href: "/self-improvement", icon: "alerts", labelKey: "nav.selfImprovement" },
],
},
{ {
group: "nav.monitor", group: "nav.monitor",
items: [ items: [
+163
View File
@@ -0,0 +1,163 @@
{
"proposed": [
{
"id": "ingestion-openclaw-model-routing-optimization",
"title": "OpenClaw Model Routing Optimization",
"summary": "Frequent model/routing discussion suggests smarter model selection based on task complexity, latency, and cost.",
"created_at": "2026-04-30T21:10:44.620Z",
"stage": "opportunity",
"council_session_id": "auto-ingestion",
"promotion_reason": "Detected from usage pattern analysis (17521 signals)",
"proposing_model": "ollama-cloud/qwen3.5:397b-cloud",
"origin_type": "proactive",
"origin_tags": [
"⚙️ Efficiency Gap",
"🔍 Pattern Detected"
],
"proactive_lens": "efficiency",
"idea_ledger_id": "ledger-openclaw-model-routing-optimization",
"council_confidence": "high",
"debate_summary": [
"Evidence: 17521 matching usage signals found in available session/log text",
"Impact: Frequent model/routing discussion suggests smarter model selection based on task complexity, latency, and cost.",
"Resource check: start with a narrow, reversible implementation before broader automation"
],
"key_disagreements": [
{
"model": "devil-advocate",
"concern": "Signal could be inflated by repeated discussion rather than repeated operational pain",
"resolution": "Require dedupe and keep items in trending/opportunity until supported by concrete task telemetry"
}
],
"council_recommendation": "Add read-only model routing telemetry first, then A/B test routing rules by task class and cost/latency outcome.",
"passing_reasoning": "high confidence based on 17521 observed signals and low-risk staged rollout path",
"escalation_type": "auto",
"session_type": "scheduled",
"scheduled_for": "2026-04-30T21:10:44.620Z",
"session_time_of_day": "evening",
"decision": null,
"is_hidden": false,
"added_at": "2026-04-30T21:10:44.758Z"
},
{
"id": "ingestion-telegram-webhook-integration",
"title": "Telegram Webhook Integration",
"summary": "Telegram-heavy operations would benefit from async processing, idempotency, and status feedback.",
"created_at": "2026-04-30T21:10:44.620Z",
"stage": "opportunity",
"council_session_id": "auto-ingestion",
"promotion_reason": "Detected from usage pattern analysis (3495 signals)",
"proposing_model": "ollama-cloud/qwen3.5:397b-cloud",
"origin_type": "proactive",
"origin_tags": [
"🏗️ Infrastructure Idea",
"🔍 Pattern Detected"
],
"proactive_lens": "infrastructure",
"idea_ledger_id": "ledger-telegram-webhook-integration",
"council_confidence": "high",
"debate_summary": [
"Evidence: 3495 matching usage signals found in available session/log text",
"Impact: Telegram-heavy operations would benefit from async processing, idempotency, and status feedback.",
"Resource check: start with a narrow, reversible implementation before broader automation"
],
"key_disagreements": [
{
"model": "devil-advocate",
"concern": "Signal could be inflated by repeated discussion rather than repeated operational pain",
"resolution": "Require dedupe and keep items in trending/opportunity until supported by concrete task telemetry"
}
],
"council_recommendation": "Back Telegram-triggered long jobs with the detached runner/outbox pattern, idempotency keys, and confirmation gates for side effects.",
"passing_reasoning": "high confidence based on 3495 observed signals and low-risk staged rollout path",
"escalation_type": "auto",
"session_type": "scheduled",
"scheduled_for": "2026-04-30T21:10:44.620Z",
"session_time_of_day": "evening",
"decision": null,
"is_hidden": false,
"added_at": "2026-04-30T21:10:44.758Z"
},
{
"id": "ingestion-docker-deployment-pipeline",
"title": "Docker Deployment Pipeline",
"summary": "Repeated build/deploy signals suggest a repo-specific CI/CD pipeline with caching and approval gates.",
"created_at": "2026-04-30T21:10:44.620Z",
"stage": "opportunity",
"council_session_id": "auto-ingestion",
"promotion_reason": "Detected from usage pattern analysis (4979 signals)",
"proposing_model": "ollama-cloud/qwen3.5:397b-cloud",
"origin_type": "proactive",
"origin_tags": [
"⚙️ Efficiency Gap",
"🔍 Pattern Detected"
],
"proactive_lens": "efficiency",
"idea_ledger_id": "ledger-docker-deployment-pipeline",
"council_confidence": "high",
"debate_summary": [
"Evidence: 4979 matching usage signals found in available session/log text",
"Impact: Repeated build/deploy signals suggest a repo-specific CI/CD pipeline with caching and approval gates.",
"Resource check: start with a narrow, reversible implementation before broader automation"
],
"key_disagreements": [
{
"model": "devil-advocate",
"concern": "Signal could be inflated by repeated discussion rather than repeated operational pain",
"resolution": "Require dedupe and keep items in trending/opportunity until supported by concrete task telemetry"
}
],
"council_recommendation": "Start with one dashboard repository workflow: build, cache dependencies, run tests, and require approval for production pushes.",
"passing_reasoning": "high confidence based on 4979 observed signals and low-risk staged rollout path",
"escalation_type": "auto",
"session_type": "scheduled",
"scheduled_for": "2026-04-30T21:10:44.620Z",
"session_time_of_day": "evening",
"decision": null,
"is_hidden": false,
"added_at": "2026-04-30T21:10:44.758Z"
},
{
"id": "ingestion-self-improvement-automation",
"title": "Self-Improvement Automation",
"summary": "Self-improvement dashboard changes should flow from real usage evidence into reviewable improvement items.",
"created_at": "2026-04-30T21:10:44.620Z",
"stage": "opportunity",
"council_session_id": "auto-ingestion",
"promotion_reason": "Detected from usage pattern analysis (2383 signals)",
"proposing_model": "ollama-cloud/qwen3.5:397b-cloud",
"origin_type": "proactive",
"origin_tags": [
"🔍 Pattern Detected",
"🔍 Pattern Detected"
],
"proactive_lens": "pattern",
"idea_ledger_id": "ledger-self-improvement-automation",
"council_confidence": "high",
"debate_summary": [
"Evidence: 2383 matching usage signals found in available session/log text",
"Impact: Self-improvement dashboard changes should flow from real usage evidence into reviewable improvement items.",
"Resource check: start with a narrow, reversible implementation before broader automation"
],
"key_disagreements": [
{
"model": "devil-advocate",
"concern": "Signal could be inflated by repeated discussion rather than repeated operational pain",
"resolution": "Require dedupe and keep items in trending/opportunity until supported by concrete task telemetry"
}
],
"council_recommendation": "Use this canonical ingestion API as the single source of truth, with dedupe, complete schema, and evidence snippets.",
"passing_reasoning": "high confidence based on 2383 observed signals and low-risk staged rollout path",
"escalation_type": "auto",
"session_type": "scheduled",
"scheduled_for": "2026-04-30T21:10:44.620Z",
"session_time_of_day": "evening",
"decision": null,
"is_hidden": false,
"added_at": "2026-04-30T21:10:44.758Z"
}
],
"adopted": [],
"rejected": [],
"dormant": []
}
+93
View File
@@ -28,6 +28,33 @@ const translations: Record<Locale, Record<string, string>> = {
"nav.bugsOff": "Bugs Off", "nav.bugsOff": "Bugs Off",
"nav.bugsCount": "數量", "nav.bugsCount": "數量",
// R&D Council
"nav.rnd": "研發",
"nav.trending": "趨勢",
"nav.opportunity": "機會",
"nav.rdCouncil": "R&D 委員會",
"rdc.description": " AI 委員會 - 自動化決策與投資建議",
"rdc.active": "個項目",
"rdc.detected": "檢測於",
"rdc.estimated": "預測模型",
"rdc.scheduled": "排程",
"rdc.emergency": "緊急",
"rdc.debateSummary": " debates 摘要",
"rdc.disagreements": "爭議點",
"rdc.recommendation": "最終建議",
"rdc.passedReasoning": "通過原因",
"rdc.escalate": "快速上傳",
"rdc.schedule": "排程",
"rdc.escalateToCouncil": "快速上傳到委員會",
"rdc.whyEscalate": "為什麼要快速上傳?",
"rdc.priority": "優先級",
"rdc.normal": "正常",
"rdc.urgent": "緊急",
"rdc.comingSoon": "即將推出",
"rdc.latestMemo": "最新委員會备忘录",
"rdc.memoPreview": "新一期委員會會議已完成,請查看最新投資建議。",
"rdc.noItems": "暫無待處理項目",
// alerts page // alerts page
"alerts.title": "警報中心", "alerts.title": "警報中心",
"alerts.subtitle": "設定系統警報與通知", "alerts.subtitle": "設定系統警報與通知",
@@ -316,6 +343,37 @@ const translations: Record<Locale, Record<string, string>> = {
"nav.bugsOff": "Bugs Off", "nav.bugsOff": "Bugs Off",
"nav.bugsCount": "数量", "nav.bugsCount": "数量",
// R&D Council
"nav.rnd": "R&D",
"nav.trending": "Trending",
"nav.opportunity": "Opportunity",
"nav.rdCouncil": "R&D Council",
"nav.selfImprovement": "Self-Improvement",
"rdc.description": "AI Council - Automated Decision & Investment Recommendations",
"rdc.active": "个项目",
"rdc.detected": "检测于",
"rdc.estimated": "预测模型",
"rdc.scheduled": "排程",
"rdc.emergency": "紧急",
"rdc.debateSummary": "辩论摘要",
"rdc.disagreements": "争议点",
"rdc.recommendation": "最终建议",
"rdc.passedReasoning": "通过原因",
"rdc.escalate": "快速上传",
"rdc.schedule": "排程",
"rdc.escalateToCouncil": "快速上传到委员会",
"rdc.whyEscalate": "为什么快速上传?",
"rdc.priority": "优先级",
"rdc.normal": "正常",
"rdc.urgent": "紧急",
"rdc.comingSoon": "即将推出",
"rdc.latestMemo": "最新委员会备忘录",
"rdc.memoPreview": "新一期委员会会议已完成,请查看最新投资建议。",
"rdc.noItems": "暂无待处理项目",
"selfImprovement.subtitle": "协议审核、批准、拒绝或发送回AI建议",
"selfImprovement.terminal": "执行日志",
"selfImprovement.projects": "改进项目",
// alerts page // alerts page
"alerts.title": "告警中心", "alerts.title": "告警中心",
"alerts.subtitle": "配置系统告警和通知", "alerts.subtitle": "配置系统告警和通知",
@@ -604,6 +662,41 @@ const translations: Record<Locale, Record<string, string>> = {
"nav.bugsOff": "Bugs Off", "nav.bugsOff": "Bugs Off",
"nav.bugsCount": "Count", "nav.bugsCount": "Count",
// R&D Council
"nav.rnd": "R&D",
"nav.trending": "Trending",
"nav.opportunity": "Opportunity",
"nav.rdCouncil": "R&D Council",
"nav.selfImprovement": "Self-Improvement",
"rdc.description": "AI Council - Automated Decision & Investment Recommendations",
"rdc.active": "items",
"rdc.detected": "Detected",
"rdc.estimated": "Estimated Model",
"rdc.scheduled": "Scheduled",
"rdc.emergency": "Emergency",
"rdc.debateSummary": "Debate Summary",
"rdc.disagreements": "Disagreements",
"rdc.recommendation": "Recommendation",
"rdc.passedReasoning": "Passed Reasoning",
"rdc.escalate": "Escalate",
"rdc.schedule": "Schedule",
"rdc.escalateToCouncil": "Escalate to Council",
"rdc.whyEscalate": "Why escalate this?",
"rdc.priority": "Priority",
"rdc.normal": "Normal",
"rdc.urgent": "Urgent",
"rdc.comingSoon": "Coming Soon",
"rdc.latestMemo": "Latest Council Memo",
"rdc.memoPreview": "Council session completed. Check latest investment recommendations.",
"rdc.noItems": "No pending items",
"rdc.originTags.reactive": "📊 Reactive Analysis",
"rdc.originTags.efficiency": "⚙️ Efficiency Gap",
"rdc.originTags.infrastructure": "🏗️ Infrastructure Idea",
"rdc.originTags.pattern": "🔍 Pattern Detected",
"rdc.originTags.external": "🌐 External Opportunity",
"selfImprovement.subtitle": "Protocol to review, approve, reject, or send back AI recommendations",
"selfImprovement.terminal": "Execution Log",
// alerts page // alerts page
"alerts.title": "Alert Center", "alerts.title": "Alert Center",
"alerts.subtitle": "Configure system alerts and notifications", "alerts.subtitle": "Configure system alerts and notifications",
+55
View File
@@ -0,0 +1,55 @@
export const REPO_ROOT: string;
export const COUNCIL_ITEMS_PATH: string;
export const IDEA_LEDGER_PATH: string;
export const COUNCIL_WORK_ORDERS_PATH: string;
export const ACTIVE_PROJECTS_ENV_VAR: string;
export const ACTIVE_PROJECTS_PATH: string;
export const COMMAND_AUDIT_LOG_PATH: string;
export const RD_COUNCIL_AUDIT_LOG_PATH: string;
export const RD_COUNCIL_DECISION_LEDGER_BUCKETS: Record<string, string>;
export const SELF_IMPROVEMENT_COMMANDS: Record<string, any>;
export function getCommandConfig(commandId: string): any | null;
export function validateCommandRequest(commandId: string, body?: Record<string, any>): any;
export function appendCommandAudit(entry: Record<string, any>, filePath?: string): Record<string, any>;
export function appendCouncilDecisionAudit(entry: Record<string, any>, filePath?: string): Record<string, any>;
export function commandOk(args: { command?: any; message: string; output?: string[] | string; details?: Record<string, any>; version?: string }): any;
export function commandError(args: { commandId?: string; message: string; code?: string; details?: Record<string, any>; status?: number }): any;
export function validateLocalOperatorRequest(request: any, opts?: { allowRemote?: boolean; allowUnauthenticatedLocal?: boolean }): any;
export function localOperatorDeniedPayload(reason?: string): any;
export function buildEvidenceTelemetry(input?: any): any;
export function buildCouncilDeliberation(input?: any): any;
export function summarizeCouncilItemOperatorState(raw: any, opts?: any): any;
export function getCouncilItemActionAvailability(raw: any, opts?: any): any;
export function formatCouncilDecisionApiError(decision: string, input?: any): string;
export function buildCouncilItemsFromUsage(sessionsData?: any, opts?: any): any[];
export function normalizeCouncilItem(raw: any, opts?: any): any;
export function mergeCouncilItems(existing?: any[], incoming?: any[], opts?: any): any[];
export function transformActiveProjects(activeProjects?: Record<string, any>): any[];
export function getActiveProjectsSourceCandidates(opts?: any): any[];
export function discoverActiveProjectsSource(opts?: any): any;
export function readActiveProjectsInventory(opts?: any): any;
export function findSessionFile(paths?: string[]): string | null;
export function readJsonFile(filePath: string, fallback: any): any;
export function writeJsonFile(filePath: string, value: any): void;
export function readCouncilItems(filePath?: string): any[];
export function buildCouncilWorkOrder(item: any, opts?: any): any;
export function upsertCouncilWorkOrder(item: any, filePath?: string, opts?: any): any;
export function appendCouncilItems(incoming: any[], filePath?: string): any[];
export function upsertLedgerProposed(items: any[], ledgerPath?: string): any;
export function applyCouncilDecision(args?: {
itemId: string;
decision: string;
note?: string | null;
actor?: string | null;
snoozeUntil?: string | null;
now?: string;
councilPath?: string;
ledgerPath?: string;
auditPath?: string;
workOrdersPath?: string;
}): {
item: any;
ledger: any;
auditEntry: any;
workOrder: any;
};
File diff suppressed because it is too large Load Diff
+154
View File
@@ -0,0 +1,154 @@
[
{
"id": "ingestion-openclaw-model-routing-optimization",
"title": "OpenClaw Model Routing Optimization",
"summary": "Frequent model/routing discussion suggests smarter model selection based on task complexity, latency, and cost.",
"created_at": "2026-04-30T21:18:35.794Z",
"stage": "opportunity",
"council_session_id": "auto-ingestion",
"promotion_reason": "Detected from usage pattern analysis (1 signal)",
"proposing_model": "ollama-cloud/qwen3.5:397b-cloud",
"origin_type": "proactive",
"origin_tags": [
"⚙️ Efficiency Gap",
"📊 Early Signal"
],
"proactive_lens": "efficiency",
"idea_ledger_id": "ledger-openclaw-model-routing-optimization",
"council_confidence": "high",
"debate_summary": [
"Evidence: 1 matching usage signal found in available session/log text",
"Impact: Frequent model/routing discussion suggests smarter model selection based on task complexity, latency, and cost.",
"Resource check: start with a narrow, reversible implementation before broader automation"
],
"key_disagreements": [
{
"model": "devil-advocate",
"concern": "Signal could be inflated by repeated discussion rather than repeated operational pain",
"resolution": "Require dedupe and keep items in trending/opportunity until supported by concrete task telemetry"
}
],
"council_recommendation": "Add read-only model routing telemetry first, then A/B test routing rules by task class and cost/latency outcome.",
"passing_reasoning": "high confidence based on 1 observed signal and low-risk staged rollout path",
"escalation_type": "auto",
"session_type": "scheduled",
"scheduled_for": "2026-04-30T21:19:24.061Z",
"session_time_of_day": "evening",
"decision": null,
"is_hidden": false
},
{
"id": "ingestion-telegram-webhook-integration",
"title": "Telegram Webhook Integration",
"summary": "Telegram-heavy operations would benefit from async processing, idempotency, and status feedback.",
"created_at": "2026-04-30T21:18:35.794Z",
"stage": "trending",
"council_session_id": "auto-ingestion",
"promotion_reason": "Detected from usage pattern analysis (1 signal)",
"proposing_model": "ollama-cloud/qwen3.5:397b-cloud",
"origin_type": "proactive",
"origin_tags": [
"🏗️ Infrastructure Idea",
"📊 Early Signal"
],
"proactive_lens": "infrastructure",
"idea_ledger_id": "ledger-telegram-webhook-integration",
"council_confidence": "medium",
"debate_summary": [
"Evidence: 1 matching usage signal found in available session/log text",
"Impact: Telegram-heavy operations would benefit from async processing, idempotency, and status feedback.",
"Resource check: start with a narrow, reversible implementation before broader automation"
],
"key_disagreements": [
{
"model": "devil-advocate",
"concern": "Signal could be inflated by repeated discussion rather than repeated operational pain",
"resolution": "Require dedupe and keep items in trending/opportunity until supported by concrete task telemetry"
}
],
"council_recommendation": "Back Telegram-triggered long jobs with the detached runner/outbox pattern, idempotency keys, and confirmation gates for side effects.",
"passing_reasoning": "medium confidence based on 1 observed signal and low-risk staged rollout path",
"escalation_type": "auto",
"session_type": "scheduled",
"scheduled_for": "2026-04-30T21:19:24.061Z",
"session_time_of_day": "evening",
"decision": null,
"is_hidden": false
},
{
"id": "ingestion-docker-deployment-pipeline",
"title": "Docker Deployment Pipeline",
"summary": "Repeated build/deploy signals suggest a repo-specific CI/CD pipeline with caching and approval gates.",
"created_at": "2026-04-30T21:18:35.794Z",
"stage": "opportunity",
"council_session_id": "auto-ingestion",
"promotion_reason": "Detected from usage pattern analysis (1 signal)",
"proposing_model": "ollama-cloud/qwen3.5:397b-cloud",
"origin_type": "proactive",
"origin_tags": [
"⚙️ Efficiency Gap",
"📊 Early Signal"
],
"proactive_lens": "efficiency",
"idea_ledger_id": "ledger-docker-deployment-pipeline",
"council_confidence": "high",
"debate_summary": [
"Evidence: 1 matching usage signal found in available session/log text",
"Impact: Repeated build/deploy signals suggest a repo-specific CI/CD pipeline with caching and approval gates.",
"Resource check: start with a narrow, reversible implementation before broader automation"
],
"key_disagreements": [
{
"model": "devil-advocate",
"concern": "Signal could be inflated by repeated discussion rather than repeated operational pain",
"resolution": "Require dedupe and keep items in trending/opportunity until supported by concrete task telemetry"
}
],
"council_recommendation": "Start with one dashboard repository workflow: build, cache dependencies, run tests, and require approval for production pushes.",
"passing_reasoning": "high confidence based on 1 observed signal and low-risk staged rollout path",
"escalation_type": "auto",
"session_type": "scheduled",
"scheduled_for": "2026-04-30T21:19:24.061Z",
"session_time_of_day": "evening",
"decision": null,
"is_hidden": false
},
{
"id": "ingestion-self-improvement-automation",
"title": "Self-Improvement Automation",
"summary": "Self-improvement dashboard changes should flow from real usage evidence into reviewable improvement items.",
"created_at": "2026-04-30T21:18:35.794Z",
"stage": "trending",
"council_session_id": "auto-ingestion",
"promotion_reason": "Detected from usage pattern analysis (1 signal)",
"proposing_model": "ollama-cloud/qwen3.5:397b-cloud",
"origin_type": "proactive",
"origin_tags": [
"🔍 Pattern Detected",
"📊 Early Signal"
],
"proactive_lens": "pattern",
"idea_ledger_id": "ledger-self-improvement-automation",
"council_confidence": "medium",
"debate_summary": [
"Evidence: 1 matching usage signal found in available session/log text",
"Impact: Self-improvement dashboard changes should flow from real usage evidence into reviewable improvement items.",
"Resource check: start with a narrow, reversible implementation before broader automation"
],
"key_disagreements": [
{
"model": "devil-advocate",
"concern": "Signal could be inflated by repeated discussion rather than repeated operational pain",
"resolution": "Require dedupe and keep items in trending/opportunity until supported by concrete task telemetry"
}
],
"council_recommendation": "Use this canonical ingestion API as the single source of truth, with dedupe, complete schema, and evidence snippets.",
"passing_reasoning": "medium confidence based on 1 observed signal and low-risk staged rollout path",
"escalation_type": "auto",
"session_type": "scheduled",
"scheduled_for": "2026-04-30T21:19:24.061Z",
"session_time_of_day": "evening",
"decision": null,
"is_hidden": false
}
]
File diff suppressed because it is too large Load Diff