mirror of
https://github.com/xmanrui/OpenClaw-bot-review.git
synced 2026-07-27 07:22:14 +00:00
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:
@@ -6,3 +6,8 @@ next-env.d.ts
|
||||
.DS_Store
|
||||
/.openclaw
|
||||
/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
|
||||
|
||||
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { runSelfImprovementCommand } from "../self-improvement-command";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
return runSelfImprovementCommand("dry-run" as any, request);
|
||||
}
|
||||
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { runSelfImprovementCommand } from "../self-improvement-command";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
return runSelfImprovementCommand("ingest-usage" as any, request);
|
||||
}
|
||||
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { runSelfImprovementCommand } from "../self-improvement-command";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
return runSelfImprovementCommand("proactive-real" as any, request);
|
||||
}
|
||||
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { runSelfImprovementCommand } from "../self-improvement-command";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
return runSelfImprovementCommand("weekly-review" as any, request);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 };
|
||||
@@ -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&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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 };
|
||||
@@ -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 © {new Date().getFullYear()}</p>
|
||||
<Link href="/" className="text-[var(--accent)] hover:underline mt-2 inline-block">
|
||||
← {t("common.backHome")}
|
||||
</Link>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -208,6 +208,13 @@ const NAV_ITEMS: { group: string; items: { href: string; icon: NavIconName; labe
|
||||
{ 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",
|
||||
items: [
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
@@ -28,6 +28,33 @@ const translations: Record<Locale, Record<string, string>> = {
|
||||
"nav.bugsOff": "Bugs Off",
|
||||
"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.title": "警報中心",
|
||||
"alerts.subtitle": "設定系統警報與通知",
|
||||
@@ -316,6 +343,37 @@ const translations: Record<Locale, Record<string, string>> = {
|
||||
"nav.bugsOff": "Bugs Off",
|
||||
"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.title": "告警中心",
|
||||
"alerts.subtitle": "配置系统告警和通知",
|
||||
@@ -604,6 +662,41 @@ const translations: Record<Locale, Record<string, string>> = {
|
||||
"nav.bugsOff": "Bugs Off",
|
||||
"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.title": "Alert Center",
|
||||
"alerts.subtitle": "Configure system alerts and notifications",
|
||||
|
||||
@@ -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
@@ -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
Reference in New Issue
Block a user