492 lines
18 KiB
TypeScript
492 lines
18 KiB
TypeScript
import { ModelRouter, type RoutingResponse, type TaskComplexity } from "./model-router.js";
|
|
import { IndependentVerifier, type VerificationResult } from "./independent-verifier.js";
|
|
import { FiveStageStateFile, type FiveStageState } from "./state-file-5stage.js";
|
|
import { GoalPattern, type GoalDefinition } from "./goal-pattern.js";
|
|
import { WorktreeManager } from "./worktree-isolation.js";
|
|
import { DynamicWorkflows, type AdversarialResult, type FanOutResult, type FusionPanelResult } from "./dynamic-workflows.js";
|
|
import { ProxyClient } from "../pai/proxy-client.js";
|
|
import { SkillSync } from "../pai/skill-sync.js";
|
|
import type { LoopIteration } from "../core/types.js";
|
|
import { SafetyBoundary } from "../upgrades/safety-boundary.js";
|
|
import {
|
|
VisionSelfCheck,
|
|
type VisualArtifact,
|
|
type VisionCheckResult,
|
|
} from "../upgrades/vision-self-check.js";
|
|
import { FeedbackLoop, type PhaseExecutor } from "../tier2-primitives/loops/feedback-loop.js";
|
|
import { type FusionPanelSlug } from "../core/fusion-types.js";
|
|
|
|
export interface StackConfig {
|
|
modelRouter: boolean;
|
|
verifier: boolean;
|
|
stateFile: boolean;
|
|
goalPattern: boolean;
|
|
worktree: boolean;
|
|
visionCheck: boolean;
|
|
safetyBoundary: boolean;
|
|
dynamicWorkflows: boolean;
|
|
skillCompounding: boolean;
|
|
lifecycleHooks: boolean;
|
|
fusionPanel: boolean;
|
|
godModeRace: boolean;
|
|
ultraPlinian: boolean;
|
|
parseltongue: boolean;
|
|
autoTune: boolean;
|
|
stmModules: boolean;
|
|
abliteration: boolean;
|
|
transparency: boolean;
|
|
promptObservatory: boolean;
|
|
promptLiberation: boolean;
|
|
}
|
|
|
|
export interface StackStatus {
|
|
layers: Record<string, string>;
|
|
modelRoute: RoutingResponse | null;
|
|
verification: VerificationResult | null;
|
|
stateFile: FiveStageState | null;
|
|
activeGoal: string | null;
|
|
worktrees: number;
|
|
sessionId: string;
|
|
}
|
|
|
|
const DEFAULT_CONFIG: StackConfig = {
|
|
modelRouter: true,
|
|
verifier: true,
|
|
stateFile: true,
|
|
goalPattern: true,
|
|
worktree: false,
|
|
visionCheck: true, // enabled by default; only runs for UI/visual tasks
|
|
safetyBoundary: true,
|
|
dynamicWorkflows: false,
|
|
skillCompounding: true,
|
|
lifecycleHooks: true,
|
|
fusionPanel: false,
|
|
godModeRace: false, // enabled when GodModeClassic workflow is requested
|
|
ultraPlinian: false, // enabled when UltraPlinian evaluation is requested
|
|
parseltongue: false, // enabled when perturbation testing is requested
|
|
autoTune: false, // enabled when adaptive params are configured
|
|
stmModules: false, // enabled when STM pipeline is configured
|
|
abliteration: false, // enabled when refusal analysis is needed
|
|
transparency: true, // enabled by default — records all decisions
|
|
promptObservatory: false, // enabled when observatory is queried
|
|
promptLiberation: false, // enabled when liberation testing is active
|
|
};
|
|
|
|
/** Keywords that suggest a task produces UI or visual output. */
|
|
const VISUAL_HINTS = new Set([
|
|
"image", "picture", "photo", "screenshot", "ui", "ux", "interface", "render",
|
|
"design", "visual", "graphic", "layout", "frontend", "web page", "webpage",
|
|
"html", "css", "svg", "canvas", "figma", "mockup", "wireframe", "icon", "logo",
|
|
"banner", "thumbnail", "diagram", "chart", "plot", "illustration",
|
|
]);
|
|
|
|
export class CompoundStack {
|
|
readonly modelRouter: ModelRouter;
|
|
readonly verifier: IndependentVerifier;
|
|
readonly stateFile: FiveStageStateFile;
|
|
readonly goalPattern: GoalPattern;
|
|
readonly worktree: WorktreeManager;
|
|
readonly safetyBoundary: SafetyBoundary;
|
|
readonly dynamicWorkflows: DynamicWorkflows;
|
|
readonly skillSync: SkillSync;
|
|
readonly proxyClient: ProxyClient;
|
|
readonly visionSelfCheck: VisionSelfCheck;
|
|
public config: StackConfig;
|
|
|
|
private activeGoalId: string | null = null;
|
|
private currentProject: string = "";
|
|
private currentTask: string = "";
|
|
private sessionId: string;
|
|
|
|
constructor(config?: Partial<StackConfig>) {
|
|
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
this.modelRouter = new ModelRouter();
|
|
this.verifier = new IndependentVerifier();
|
|
this.stateFile = new FiveStageStateFile();
|
|
this.goalPattern = new GoalPattern();
|
|
this.worktree = new WorktreeManager();
|
|
this.safetyBoundary = new SafetyBoundary();
|
|
this.dynamicWorkflows = new DynamicWorkflows(this.verifier, this.modelRouter);
|
|
this.skillSync = new SkillSync();
|
|
this.proxyClient = new ProxyClient();
|
|
this.visionSelfCheck = new VisionSelfCheck({ proxyClient: this.proxyClient });
|
|
this.sessionId = `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
}
|
|
|
|
async run(task: string, project: string): Promise<StackStatus> {
|
|
this.currentProject = project;
|
|
this.currentTask = task;
|
|
const status: StackStatus = {
|
|
layers: {},
|
|
modelRoute: null,
|
|
verification: null,
|
|
stateFile: null,
|
|
activeGoal: null,
|
|
worktrees: 0,
|
|
sessionId: this.sessionId,
|
|
};
|
|
|
|
// ── Layer 1: Lifecycle — read state file at session start ──
|
|
if (this.config.lifecycleHooks && this.config.stateFile) {
|
|
const { contextPrompt, isResume } = this.stateFile.beginSession(project, this.sessionId);
|
|
status.layers["lifecycle"] = isResume ? `resumed (${this.sessionId.slice(0, 16)}...)` : "fresh";
|
|
status.stateFile = this.stateFile.load(project);
|
|
}
|
|
|
|
// ── Layer 2: Model Router ──
|
|
if (this.config.modelRouter) {
|
|
const complexity = this.modelRouter.taskComplexity(task);
|
|
const route = this.modelRouter.route({ task, domain: "code", complexity, requiresVision: false });
|
|
status.modelRoute = route;
|
|
status.layers["model-router"] = `${route.primary.tier}(${route.primary.displayName})`;
|
|
}
|
|
|
|
// ── Layer 3: Safety Boundary ──
|
|
if (this.config.safetyBoundary) {
|
|
const available = this.safetyBoundary.getAvailable();
|
|
if (available.length === 0) {
|
|
status.layers["safety-boundary"] = "BLOCKED: No available models";
|
|
await this.writeExit("BLOCKED — No available models", []);
|
|
return status;
|
|
}
|
|
const route = this.safetyBoundary.route("task");
|
|
if (route.modelId === "none") {
|
|
status.layers["safety-boundary"] = `BLOCKED: ${route.reason}`;
|
|
await this.writeExit(`BLOCKED — ${route.reason}`, []);
|
|
return status;
|
|
}
|
|
status.layers["safety-boundary"] = `PASS (${route.modelName})`;
|
|
}
|
|
|
|
// ── Layer 4: Goal Pattern ──
|
|
if (this.config.goalPattern) {
|
|
const goalDef: GoalDefinition = {
|
|
text: task,
|
|
criteria: [
|
|
{ label: "Correctness", description: "Output is technically correct", weight: 0.4, required: true },
|
|
{ label: "Completeness", description: "All requirements addressed", weight: 0.3, required: true },
|
|
{ label: "Clarity", description: "Output is clear", weight: 0.2, required: false },
|
|
{ label: "Efficiency", description: "Solution is efficient", weight: 0.1, required: false },
|
|
],
|
|
maxIterations: 10,
|
|
minScore: 0.7,
|
|
};
|
|
this.activeGoalId = this.goalPattern.setGoal(goalDef);
|
|
status.activeGoal = this.activeGoalId;
|
|
status.layers["goal-pattern"] = `goal set: ${goalDef.text.slice(0, 40)}...`;
|
|
}
|
|
|
|
// ── Layer 5: Verifier ──
|
|
if (this.config.verifier) {
|
|
status.layers["verifier"] = "ready";
|
|
}
|
|
|
|
// ── Layer 6: State File ──
|
|
if (this.config.stateFile && !status.stateFile) {
|
|
const existing = this.stateFile.load(project);
|
|
status.stateFile = existing;
|
|
status.layers["state-file"] = existing ? `loaded (${existing.verifiedFacts.length} facts, ${existing.generalRules.length} rules)` : "created";
|
|
}
|
|
|
|
// ── Layer 7: Dynamic Workflows (fan-out + adversarial + fusion) ──
|
|
if (this.config.dynamicWorkflows) {
|
|
status.layers["dynamic-workflows"] = "ready (fan-out + adversarial + loop-until-done + fusion)";
|
|
}
|
|
|
|
// ── Layer 7b: Fusion Panel ──
|
|
if (this.config.fusionPanel) {
|
|
status.layers["fusion-panel"] = "ready (draft → critique → fuse)";
|
|
}
|
|
|
|
// ── Layer 8: Worktrees ──
|
|
if (this.config.godModeRace) {
|
|
status.layers["godmode-race"] = "ready (parallel model racing)";
|
|
}
|
|
|
|
if (this.config.ultraPlinian) {
|
|
status.layers["ultra-plinian"] = "ready (5-tier output evaluation)";
|
|
}
|
|
|
|
if (this.config.parseltongue) {
|
|
status.layers["parseltongue"] = "ready (safety-gate perturbation testing)";
|
|
}
|
|
|
|
if (this.config.autoTune) {
|
|
status.layers["auto-tune"] = "ready (adaptive sampling parameters)";
|
|
}
|
|
|
|
if (this.config.stmModules) {
|
|
status.layers["stm-modules"] = "ready (semantic output transforms)";
|
|
}
|
|
|
|
if (this.config.abliteration) {
|
|
status.layers["abliteration-awareness"] = "ready (refusal pattern analysis)";
|
|
}
|
|
|
|
if (this.config.transparency) {
|
|
status.layers["transparency"] = "ready (prompt and routing records)";
|
|
}
|
|
|
|
if (this.config.promptObservatory) {
|
|
status.layers["prompt-observatory"] = "ready (system prompt catalog)";
|
|
}
|
|
|
|
if (this.config.promptLiberation) {
|
|
status.layers["prompt-liberation"] = "ready (prompt constraint analysis)";
|
|
}
|
|
|
|
if (this.config.worktree) {
|
|
const trees = this.worktree.list();
|
|
status.worktrees = trees.length;
|
|
status.layers["worktree"] = `${trees.length} active`;
|
|
}
|
|
|
|
// ── Layer 9: Vision Check ──
|
|
if (this.config.visionCheck) {
|
|
const visual = this.isVisualTask(task);
|
|
status.layers["vision-check"] = visual
|
|
? "ready (visual task detected — vision check active)"
|
|
: "ready (non-visual task — vision check skipped)";
|
|
}
|
|
|
|
return status;
|
|
}
|
|
|
|
/**
|
|
* Determine whether a task likely produces UI/visual output.
|
|
* Used to gate vision self-checks so they only run when relevant.
|
|
*/
|
|
isVisualTask(task: string): boolean {
|
|
const lower = task.toLowerCase();
|
|
for (const hint of VISUAL_HINTS) {
|
|
if (lower.includes(hint)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Run a vision self-check after a loop iteration.
|
|
* If the task is non-visual or vision checks are disabled, returns null.
|
|
* Results are recorded in the state file: Lessons Learned for PASS/PARTIAL,
|
|
* Open Failures for FAIL.
|
|
*/
|
|
async visionCheckIteration(
|
|
iteration: LoopIteration,
|
|
task?: string,
|
|
artifact?: VisualArtifact
|
|
): Promise<VisionCheckResult | null> {
|
|
const goal = task ?? this.currentTask;
|
|
if (!this.config.visionCheck || !this.isVisualTask(goal)) {
|
|
return null;
|
|
}
|
|
|
|
const resolvedArtifact = artifact ?? this.extractVisualArtifact(iteration);
|
|
if (!resolvedArtifact) {
|
|
return null;
|
|
}
|
|
|
|
const result = await this.visionSelfCheck.check(resolvedArtifact, goal);
|
|
this.recordVisionResult(result, goal, iteration.number);
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Run a full feedback loop with vision self-checks hooked after each iteration.
|
|
* Only runs vision checks when the task hints at UI/visual output.
|
|
*/
|
|
async runLoopWithVision(
|
|
task: string,
|
|
executor: PhaseExecutor,
|
|
options?: {
|
|
maxIterations?: number;
|
|
convergenceThreshold?: number;
|
|
loopDelayMs?: number;
|
|
}
|
|
): Promise<{ loopResult: import("../core/types.js").LoopResult; visionResults: VisionCheckResult[] }> {
|
|
this.currentTask = task;
|
|
const loop = new FeedbackLoop(undefined, this.sessionId, options);
|
|
const visionResults: VisionCheckResult[] = [];
|
|
|
|
loop.onIteration(async (iteration) => {
|
|
const result = await this.visionCheckIteration(iteration, task);
|
|
if (result) {
|
|
visionResults.push(result);
|
|
}
|
|
});
|
|
|
|
const loopResult = await loop.run(task, executor);
|
|
return { loopResult, visionResults };
|
|
}
|
|
|
|
private extractVisualArtifact(iteration: LoopIteration): VisualArtifact | null {
|
|
const haystack = `${iteration.executed ?? ""}\n${iteration.observation ?? ""}`;
|
|
|
|
// Look for base64 data URIs
|
|
const dataUriMatch = haystack.match(/data:image\/[a-zA-Z0-9.+]+;base64,[A-Za-z0-9+/=]+/);
|
|
if (dataUriMatch) {
|
|
const source = dataUriMatch[0];
|
|
const mime = source.match(/data:([^;]+)/)?.[1] ?? "image/png";
|
|
return { source, mimeType: mime, type: "image", provenance: "extracted from iteration output" };
|
|
}
|
|
|
|
// Look for file paths or URLs ending in image extensions
|
|
const imageRefMatch = haystack.match(/(?:file:\/\/|\b)\S+\.(?:png|jpe?g|gif|webp|svg|bmp)(?:\?\S*)?/i);
|
|
if (imageRefMatch) {
|
|
const source = imageRefMatch[0];
|
|
return { source, mimeType: this.guessMimeType(source), type: "image", provenance: "extracted from iteration output" };
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private recordVisionResult(result: VisionCheckResult, goal: string, iterationNumber: number): void {
|
|
if (!this.config.stateFile || !this.currentProject) return;
|
|
|
|
const summary = `Iteration ${iterationNumber} vision check for "${goal.slice(0, 80)}...": ${result.verdict} (${(result.confidence * 100).toFixed(0)}% confidence)`;
|
|
|
|
if (result.verdict === "FAIL") {
|
|
const detail = result.gaps.length > 0 ? ` — ${result.gaps.join("; ")}` : "";
|
|
this.stateFile.addFailure(this.currentProject, `${summary}${detail}`);
|
|
} else {
|
|
const detail = result.gaps.length > 0
|
|
? ` — gaps: ${result.gaps.join("; ")}`
|
|
: ` — matches: ${result.matches.join("; ") || "visual output aligned with goal"}`;
|
|
this.stateFile.addLesson(this.currentProject, `${summary}${detail}`);
|
|
}
|
|
}
|
|
|
|
private guessMimeType(path: string): string {
|
|
const ext = path.split(".").pop()?.toLowerCase() ?? "";
|
|
const mimeMap: Record<string, string> = {
|
|
png: "image/png",
|
|
jpg: "image/jpeg",
|
|
jpeg: "image/jpeg",
|
|
gif: "image/gif",
|
|
webp: "image/webp",
|
|
bmp: "image/bmp",
|
|
svg: "image/svg+xml",
|
|
};
|
|
return mimeMap[ext] ?? "image/png";
|
|
}
|
|
|
|
/**
|
|
* Execute a fan-out-and-synthesize dynamic workflow.
|
|
* Splits a task into N sub-tasks, runs each independently, synthesizes results.
|
|
*/
|
|
async fanOut(
|
|
task: string,
|
|
subTasks: string[],
|
|
executor: (subTask: string, index: number) => LoopIteration | Promise<LoopIteration>,
|
|
synthesizer: (results: LoopIteration[], originalTask: string) => LoopIteration | Promise<LoopIteration>,
|
|
maxConcurrency?: number,
|
|
): Promise<FanOutResult> {
|
|
this.ensureConfig("dynamicWorkflows");
|
|
return this.dynamicWorkflows.fanOutAndSynthesize(task, subTasks, executor, synthesizer, maxConcurrency);
|
|
}
|
|
|
|
/**
|
|
* Execute adversarial verification: maker artifact → independent verifier.
|
|
*/
|
|
async adversarialVerify(
|
|
makerIteration: LoopIteration,
|
|
task: string,
|
|
): Promise<AdversarialResult> {
|
|
this.ensureConfig("dynamicWorkflows");
|
|
return this.dynamicWorkflows.adversarialVerify(makerIteration, task);
|
|
}
|
|
|
|
/**
|
|
* Execute a fusion panel: dispatch task to multiple panelists in parallel,
|
|
* then synthesize results with the judge model.
|
|
*
|
|
* Implements the draft → critique → fuse pattern.
|
|
*/
|
|
async fusionPanel(
|
|
task: string,
|
|
executor: (prompt: string, panelistIndex: number) => LoopIteration | Promise<LoopIteration>,
|
|
judge: (panelists: LoopIteration[], originalTask: string) => LoopIteration | Promise<LoopIteration>,
|
|
panelSlug: FusionPanelSlug = "opus4.8-4.8",
|
|
maxConcurrency?: number,
|
|
): Promise<FusionPanelResult> {
|
|
this.ensureConfig("fusionPanel");
|
|
return this.dynamicWorkflows.fusionPanel(task, executor, judge, panelSlug, maxConcurrency);
|
|
}
|
|
|
|
/**
|
|
* Compound a verified failure into the most relevant PAI skill.
|
|
* Implements the "write the lesson into the Skill" pattern.
|
|
*/
|
|
compoundLesson(lesson: string, context: string): { skillName: string; path: string } | null {
|
|
if (!this.config.skillCompounding) return null;
|
|
|
|
const relevant = this.skillSync.findRelevantSkill(context);
|
|
if (!relevant) return null;
|
|
|
|
const path = this.skillSync.compoundLesson(relevant.name, lesson, "compound-stack");
|
|
if (!path) return null;
|
|
|
|
return { skillName: relevant.name, path };
|
|
}
|
|
|
|
/**
|
|
* End the current session: write summary + next actions to state file.
|
|
* Implements the "write before walking away" rule.
|
|
*/
|
|
async writeExit(summary: string, nextActions: string[], options?: { lessons?: string[]; facts?: string[]; failures?: string[] }): Promise<void> {
|
|
if (!this.config.lifecycleHooks || !this.config.stateFile) return;
|
|
|
|
this.stateFile.endSession(this.currentProject, summary, nextActions, this.sessionId, {
|
|
addLesson: options?.lessons?.[0],
|
|
addFact: options?.facts?.[0],
|
|
failures: options?.failures,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get a status report string for display.
|
|
*/
|
|
getStatus(): string {
|
|
const parts: string[] = ["Fable 5 Compound Stack", "━━━━━━━━━━━━━━━━━"];
|
|
const layerOrder: Array<keyof StackConfig> = [
|
|
"lifecycleHooks",
|
|
"modelRouter",
|
|
"safetyBoundary",
|
|
"goalPattern",
|
|
"verifier",
|
|
"dynamicWorkflows",
|
|
"fusionPanel",
|
|
"godModeRace",
|
|
"ultraPlinian",
|
|
"parseltongue",
|
|
"autoTune",
|
|
"stmModules",
|
|
"abliteration",
|
|
"transparency",
|
|
"promptObservatory",
|
|
"promptLiberation",
|
|
"stateFile",
|
|
"worktree",
|
|
"visionCheck",
|
|
"skillCompounding",
|
|
];
|
|
|
|
for (const layer of layerOrder) {
|
|
const enabled = this.config[layer];
|
|
parts.push(` ${enabled ? "✓" : "○"} ${layer}: ${enabled ? "enabled" : "disabled"}`);
|
|
}
|
|
|
|
const goals = this.goalPattern.getStatus();
|
|
parts.push(` ${goals}`);
|
|
parts.push(` ${this.activeGoalId ? `Active goal: ${this.activeGoalId}` : "No active goal"}`);
|
|
parts.push(` Session: ${this.sessionId.slice(0, 16)}...`);
|
|
|
|
return parts.join("\n");
|
|
}
|
|
|
|
private ensureConfig(key: keyof StackConfig): void {
|
|
if (!this.config[key]) {
|
|
throw new Error(`Layer "${key}" is disabled in config. Enable it to use this feature.`);
|
|
}
|
|
}
|
|
}
|