Skip to content

Latest commit

 

History

History
594 lines (445 loc) · 17.9 KB

File metadata and controls

594 lines (445 loc) · 17.9 KB

Module Reference

All modules live in src/. This document covers every module's public API, inputs, and outputs.


Core Orchestration

orchestrator.js

Master task loop. Consumes all other modules.

const { run } = require('./src/orchestrator');

await run({
  goal:                'Add JWT authentication to the FastAPI backend',
  type:                'python',
  buildCommand:        'pytest',
  maxBudgetUSD:        5.00,
  maxTokens:           500000,
  resume:              false,
  noGit:               false,
  targetDir:           '/path/to/project',
  liteOnly:            false,
  debateOnly:          false,
  apply:               true,
  dryRun:              false,
  autoCommit:          false,
  allowProtected:      false,
  allowISR:            false,
  confidenceThreshold: 70,
  verbose:             false,  // true = dump raw model JSON in debate viewer
});

Risk & Mode

riskClassifier.js

Scores a potential change on a 0-100 scale.

const { classify } = require('./src/riskClassifier');

const result = classify({
  filePaths:        ['src/payments/processor.ts', 'src/db/transactions.ts'],
  diffText:         '--- a/src/payments/processor.ts\n+++ b/src/payments/processor.ts\n...',
  taskDescription:  'Refactor payment rollback handler',
  allowProtected:   false,
});

// result:
{
  risk_score:        75,
  risk_level:        'high',       // 'low' | 'medium' | 'high' | 'critical'
  triggers:          ['Critical path keywords (transaction/rollback)', 'Protected path: src/payments'],
  recommended_mode:  'debate',     // 'lite' | 'debate'
}

modeSelector.js

Converts classifier output + CLI flags into a mode decision.

const { selectMode } = require('./src/modeSelector');

const { mode, reason } = selectMode(classifierResult, {
  liteOnly:   false,
  debateOnly: false,
});

// mode: 'lite' | 'debate'
// reason: human-readable explanation

Review Pipelines

liteReviewer.js

Single OpenAI pass producing a unified diff.

const liteReviewer = require('./src/liteReviewer');

const result = await liteReviewer.runLite(
  goal,          // string — task description
  codeContext,   // string — source file contents (truncated)
  arch,          // string — latest architecture decision summary
  constraints,   // string[] — project constraints from memory
  recentErrors,  // string[] — last N build errors
  budget,        // BudgetManager instance
  riskInfo       // riskClassifier.classify() result
);

// result:
{
  summary:       'Add NULL guard before packet dispatch',
  root_cause:    'Missing bounds check on payload length',
  plan:          '1. Add guard at line 42\n2. Update unit test',
  patch:         '--- a/src/auth/middleware.ts\n+++ b/src/auth/middleware.ts\n@@...',
  risk_level:    'medium',
  confidence:    82,
  justification: 'Change is isolated to receive path, no struct modification',
}

Returns a degraded result (low confidence, empty patch) on JSON parse failure instead of throwing.


debateOrchestrator.js

4-phase adversarial debate pipeline.

const debateOrchestrator = require('./src/debateOrchestrator');

const result = await debateOrchestrator.runDebate(
  task,          // TaskManager task object { id, description }
  codeContext,   // string
  arch,          // string
  constraints,   // string[]
  recentErrors,  // string[]
  budget,        // BudgetManager instance
  targetDir,     // string — project root dir (for saving debate record)
  {
    confidenceThreshold: 70,
    allowProtected:      false,
    allowISR:            false,
    riskClassification:  riskInfo,
    verbose:             false,  // true = debateViewer dumps raw JSON per phase
  }
);

// result:
{
  patch:          '--- a/src/payments/processor.ts\n+++ b/src/payments/processor.ts\n@@...',
  blocked:        false,
  blockReason:    null,
  approved:       true,   // false if user said 'n' at the debateViewer prompt
  consensusResult: { allow_apply, allow_commit, reason, final_confidence, ... },
  debateRecord:   { phase1_proposal, phase2_critique, phase3_rebuttal, phase4_audit, ts, taskId },
}

Phase budget tracking: each phase calls budget.startPhase('debate-p1') through debate-p4.

debateViewer.js is automatically invoked inside runDebate() — no separate call needed.


debateViewer.js

Full-featured Debate Viewer CLI UI. Renders structured, color-coded terminal panels for each debate phase. Called internally by debateOrchestrator.runDebate() but also usable standalone.

const debateViewer = require('./src/debateViewer');

// Must be called before any show* functions:
debateViewer.setVerbose(true);  // print raw JSON after each phase

// 1. Phase overview panel (risk box)
debateViewer.showSessionOpen(riskInfo, taskId);
// riskInfo: { risk_score, risk_level, triggers }

// 2. GPT Proposal panel
debateViewer.showPhase1(proposal);
// proposal: critiqueParser.parseProposal() output

// 3. Claude Challenge panel (color-coded by severity)
debateViewer.showPhase2(critique);
// critique: critiqueParser.parseCritique() output

// 4. GPT Defense panel + optional patch diff
debateViewer.showPhase3(rebuttal, proposal);
// rebuttal: rebuttalEngine.runRebuttal() output
// proposal: phase-1 output (for v1 patch comparison)

// 5. Final Audit panel
debateViewer.showPhase4(audit);
// audit: critiqueParser.parseFinalAudit() output

// 6. Consensus panel
debateViewer.showConsensus(consensusOut);
// consensusOut: consensus.fromDebate() output

// 7. Risk heatmap (ASCII bars per-file)
debateViewer.showRiskHeatmap(proposal, rebuttal, critique);

// 8. Patch comparison panel (v1 vs v2)
debateViewer.showPatchComparison(v1PatchString, v2PatchString);

// 9. Human approval prompt — returns boolean
const approved = debateViewer.promptApply();
// Prints: "Apply patch? (y/n)"
// Returns: true = apply, false = abort

// 10. Persist session to <targetDir>/debates/
const { sessionFile, v1File, v2File } = debateViewer.persistSession(debateRecord, targetDir);
// Writes:
//   debates/<ts>_session.json
//   debates/<ts>_patch_v1.diff
//   debates/<ts>_patch_v2.diff  (only if v2 differs from v1)

Color coding:

  • CRITICAL findings → chalk.bold.red
  • HIGH findings → chalk.red
  • MEDIUM / design flaws → chalk.yellow
  • Safe / passing → chalk.green
  • Risk score ≥ 75 → red, ≥ 50 → yellow, < 50 → green
  • Confidence ≥ 80% → green, ≥ 60% → yellow, < 60% → red

Dependencies: chalk (v4), cli-table3, diff, readline-sync


critiqueParser.js

Parses and normalises AI-generated JSON for each debate phase. All parsers return a _parseOk boolean and fall back to safe defaults rather than throwing — the fallback object always contains the same fields as the success object (arrays default to [], strings to '', numbers to fallback values) so downstream code never hits undefined.length.

const parser = require('./src/critiqueParser');

parser.parseProposal(rawJSON)
// Phase 1 → { claim, plan_summary, reasoning, risks_identified, risks, patch, confidence, _parseOk }

parser.parseCritique(rawJSON)
// Phase 2 → { claim, reasoning, risks, critical_findings, design_flaws, unsafe_changes,
//             missing_tests, recommended_changes, confidence, _parseOk }
// On complete JSON failure: all arrays = [], confidence = 0, _parseOk = false

parser.parseRebuttal(rawJSON)
// Phase 3 → { rebuttal_summary, addressed_items, updated_patch, residual_risks, confidence, _parseOk }

parser.parseFinalAudit(rawJSON)
// Phase 4 → { final_risk_level, remaining_issues, approval_recommendation, confidence, _parseOk }

rebuttalEngine.js

Handles Phase 3 — Proposer defends against the Critic's challenge. Uses the same proposer provider as Phase 1.

const rebuttalEngine = require('./src/rebuttalEngine');

const rebuttal = await rebuttalEngine.runRebuttal(
  proposal,    // Phase 1 output
  critique,    // Phase 2 output
  taskDescription,
  codeContext,
  budget
);

// rebuttal:
{
  rebuttal_summary: 'Fixed ordering issue; justified NULL check',
  addressed_items:  [{ item, action: 'changed'|'justified', notes }],
  updated_patch:    '--- a/src/payments/processor.ts ...',
  residual_risks:   ['Memory pressure at high message rate'],
  confidence:       88,
}

consensus.js

Normalises LITE or DEBATE output into a single CONSENSUS_OUTPUT object.

const consensus = require('./src/consensus');

// From LITE:
const out = consensus.fromLite(liteResult, riskInfo, guardrailResult, { confidenceThreshold: 70 });

// From DEBATE:
const out = consensus.fromDebate(finalAudit, riskInfo, guardrailResult, { confidenceThreshold: 70 });

// Display in terminal:
consensus.display(out);

Output schema:

{
  allow_apply:         boolean,
  allow_commit:        boolean,
  reason:              string,
  final_confidence:    number,
  mode_used:           'lite' | 'debate',
  triggers:            string[],
  risk_level:          string,
  low_confidence_mode: boolean,
  blocked_reasons:     string[],
}

Patch Lifecycle

patchApplier.js

const patchApplier = require('./src/patchApplier');

// Apply a unified diff to disk via git apply:
const result = patchApplier.applyDiff(diffText, targetDir, patchesDir, taskId);
// result: { applied: boolean, error: string|null, diffFile: string }

// Save diff without applying (audit trail):
const filePath = patchApplier.saveDiff(diffText, patchesDir, taskId);

// Log affected files to console (preview):
patchApplier.previewDiff(diffText);

// Legacy full-file write (kept for compatibility):
patchApplier.writeFiles(files, targetDir);
patchApplier.previewFiles(files);

applyDiff() saves the diff first, then applies it through a three-step fallback chain:

  1. git apply --check dry-run → git apply (hunk headers are sanitised before this step to fix AI-generated line-count errors)
  2. git apply --3way (merge-style apply)
  3. Direct full-file reconstruction from the diff content

Only if all three steps fail does it return { applied: false }.


guardrails.js

const guardrails = require('./src/guardrails');

guardrails.checkPatch(patchText, { allowISR, allowEmptyPatch })
// → { safe: boolean, violations: string[] }
// Note: consecutive-deletion and per-file deletion-ratio checks are automatically
// skipped for web/script/markup file extensions (.css, .html, .js, .ts, .py, etc.)
// The check uses the file path from each hunk's "--- a/..." line.

guardrails.checkPaths(filePaths, allowProtected)
// → { safe: boolean, violations: string[] }

guardrails.checkFiles(newFiles, originalFiles)
// → { safe: boolean, violations: string[] }

// Constants:
guardrails.MAX_PATCH_LINES            // 400
guardrails.MAX_DELETION_RATIO         // 0.30
guardrails.MAX_CONSECUTIVE_DELETIONS  // 50  (not enforced on safe-extension files)
guardrails.PROTECTED_PATH_PREFIXES    // string[]
guardrails.SAFE_EXTENSIONS            // Set<string> — exempt file extensions

Build & Git

buildRunner.js

const buildRunner = require('./src/buildRunner');

const result = buildRunner.runBuild(command, targetDir);
// result: { success: boolean, stdout: string, stderr: string, exitCode: number }

const errorText = buildRunner.extractErrors(stdout, stderr);
// Returns condensed error summary string

gitManager.js

const git = require('./src/gitManager');

git.ensureRepo(targetDir)
git.createBranch(branchName, targetDir)
git.stageFiles(filePathsArray, targetDir)    // empty array = git add -A
git.commit(message, targetDir)
git.awaitApproval(question, autoApprove)     // returns boolean

AI Providers

All debate phases use the provider abstraction from src/providers/. The proposer and critic are selected at runtime via PROPOSER_PROVIDER / CRITIC_PROVIDER env vars. Lite mode continues to call the legacy openai.js / claude.js helpers directly.

src/providers/providerFactory.js

const { createProvider } = require('./src/providers/providerFactory');

// String form:
const provider = createProvider('openai');    // → OpenAIProvider
const provider = createProvider('claude');    // → ClaudeProvider
const provider = createProvider('opencode');  // → OpenCodeProvider

// Object descriptor form (model override):
const provider = createProvider({ provider: 'opencode', model: 'kimi-k2.5-free' });

src/providers/LLMProvider.js

Abstract base class. All providers extend it and implement generate(prompt, options).

// interface:
async generate(prompt, options = {})  { content: string, tokensUsed: number }

src/providers/OpenCodeProvider.js

LLMProvider for the OpenCode Zen API (OpenAI-compatible, /zen/v1/chat/completions).

const { OpenCodeProvider, ALLOWLIST } = require('./src/providers/OpenCodeProvider');

const provider = new OpenCodeProvider({ baseUrl, apiKey, model });
await provider.generate(prompt, {
  model,          // per-call override (must be in ALLOWLIST or OPENCODE_ALLOW_ANY_MODEL=1)
  systemPrompt,   // or options.system
  temperature,    // default 0.2
  max_tokens,     // default 4096
  budgetManager,
  debateId,       // for llm_invalid_json event
  phase,
});

Built-in allowlist (ALLOWLIST Set): glm-5-free, minimax-m2.5-free, kimi-k2.5-free, big-pickle.
Set OPENCODE_ALLOW_ANY_MODEL=1 to bypass.
Emits logger.logEvent({ event: 'llm_invalid_json', ... }) when the response content is not parseable JSON, so orchestrators can log partial-parse warnings.

src/core/DebateSession.js

Isolated container for a single debate run. Resolves provider instances from env vars.

const DebateSession = require('./src/core/DebateSession');
const session = new DebateSession({ budget });

session.proposerProvider  // LLMProvider instance for PROPOSER_PROVIDER
session.criticProvider    // LLMProvider instance for CRITIC_PROVIDER
session.proposerModel     // string — PROPOSER_MODEL env value
session.criticModel       // string — CRITIC_MODEL env value
session.id                // UUID for this debate run
session.meta              // { proposerProvider, proposerModel, criticProvider, criticModel }

openai.js

Legacy helpers (still used by liteReviewer.js and other non-debate code).

const openai = require('./src/openai');

// Returns { parsed: object, content: string } — throws on budget exceeded
const { parsed, content } = await openai.completeJSON(systemPrompt, userPrompt, budget);

// Plain string completion:
const text = await openai.complete(systemPrompt, userPrompt, budget);

Also exports: class OpenAIProvider extends LLMProvider — imported by providerFactory.js.

claude.js

const claude = require('./src/claude');

const text = await claude.complete(systemPrompt, userPrompt, budget);
const { parsed, content } = await claude.completeJSON(systemPrompt, userPrompt, budget);

Also exports: class ClaudeProvider extends LLMProvider — imported by providerFactory.js.

Both legacy clients implement:

  • Automatic retry with exponential backoff on rate-limit errors
  • Pre-call budget check (throws Error('Budget exceeded') if limit reached)
  • Token + cost recording via budget.record()

Infrastructure

budgetManager.js

const BudgetManager = require('./src/budgetManager');
const budget = new BudgetManager(maxUSD, maxTokens);

budget.record(model, promptTokens, completionTokens)  // cost computed from built-in PRICING table
budget.checkBudget(estimatedTokens)    // throws if over limit
budget.hasHeadroom(estimatedUSD, estimatedTokens)  // boolean
budget.estimateDebateCost()            // rough USD estimate for 4-phase debate
budget.startPhase(phaseName)
budget.endPhase()
budget.getSummary()
// returns:
// {
//   totalTokens, promptTokens, completionTokens,
//   totalCostUSD,
//   openaiCostUSD,   // cumulative spend on OpenAI models only
//   claudeCostUSD,   // cumulative spend on Claude models only
//   remaining, percentUsed
// }
budget.phaseSummary()                  // [{ phase, promptTokens, completionTokens, costUSD, calls }]
budget.toJSON()
budget.restoreFrom(savedState)

logger.js

const logger = require('./src/logger');

logger.banner()
logger.section(title)
logger.step(n, total, description)
logger.success(message)
logger.warn(message)
logger.error(message)
logger.info(message)
logger.hr()
logger.budget(summary)
// New in v2.0:
logger.riskScore(riskInfo)
logger.modeDecision(mode, reason, riskInfo)
logger.phaseHeader(mode, phase, total, description)
logger.consensusResult(consensus)

memory.js

const Memory = require('./src/memory');
const mem = new Memory(filePath);

mem.load()
mem.addArchitectureDecision(summary, details)
mem.addConstraint(constraint)
mem.addError(taskId, errorText, relatedFile)
mem.getArchitectureDecisions()   // array, newest last
mem.getConstraints()             // string[]
mem.getRecentErrors(n)           // last N error objects

stateTracker.js

const StateTracker = require('./src/stateTracker');
const state = new StateTracker(filePath);

state.load()
state.init(goal, type, buildCommand)
state.nextIteration()            // returns new iteration number
state.setCurrentTask(taskId)
state.completeTask(taskId)
state.failTask(taskId)
state.setBuildSuccess(bool)
state.setLastPatch(filePath)
state.updateBudget(budgetJSON)
state.getState()                 // full state object

taskManager.js

const TaskManager = require('./src/taskManager');
const tasks = new TaskManager(memory);

tasks.initTasks(taskListArray)
tasks.all()                      // all tasks
tasks.getNextTask()              // next runnable task (deps satisfied)
tasks.markInProgress(id)
tasks.markDone(id)
tasks.markFailed(id, reason)
tasks.allDone()                  // boolean
tasks.summary()                  // { total, done, failed, inProgress, pending }