pleach
Build

DevTools

`window.__HARNESS_DEVTOOLS__` — the browser-console surface for inspecting sessions, walking checkpoints, forcing syncs, and listing tools during development.

DevTools is one surface in the frontend integration thematic island — siblings of react, server, api-routes, and query.

In development, the runtime exposes a debugging interface on window.__HARNESS_DEVTOOLS__. The surface is small on purpose — just enough to inspect what the runtime sees, walk back through checkpoints, and force-sync the outbox without leaving the browser console.

Gate the wiring behind NODE_ENV !== "production" so the surface doesn't ship in production bundles.

import {
  useHarnessDevTools,
  updateDevToolsSession,
} from "@pleach/core/react";
import type { HarnessDevToolsAPI } from "@pleach/core/react";
Sourcesrc/dev/

Wiring

Call the hook once near the provider:

function App() {
  if (process.env.NODE_ENV !== "production") {
    // eslint-disable-next-line react-hooks/rules-of-hooks
    useHarnessDevTools();
  }
  return <HarnessProvider runtime={runtime}>...</HarnessProvider>;
}

Once mounted, window.__HARNESS_DEVTOOLS__ is the active runtime's debug surface. Refreshing the page rebuilds it.

For TypeScript shims:

// types/global.d.ts
import type { HarnessDevToolsAPI } from "@pleach/core/react";

declare global {
  interface Window {
    __HARNESS_DEVTOOLS__?: HarnessDevToolsAPI;
  }
}

The surface

PropertyReturnsUse
sessionSessionStateCurrent full state — messages, tools, channels
checkpoints()Checkpoint[]List checkpoints for the current session
rollback(cpId)Promise<void>Time-travel to a checkpoint
tools()ToolDefinition[]Active tool registry
syncStatus()SyncStatsVersion vectors, pending changes, last sync
forceSync()Promise<void>Drain the outbox now
events()HarnessEvent[]Recent event log entries
interrupts()PendingInterrupt[]Outstanding HITL approvals
ledger()AuditableCall[]In-memory audit rows (when using MemoryProviderDecisionLedger)

session

A property, not a method. Always reflects the latest state.

__HARNESS_DEVTOOLS__.session.messages.length
__HARNESS_DEVTOOLS__.session.pendingToolCalls
__HARNESS_DEVTOOLS__.session.versionVector

For React state debugging — when the UI looks stale, compare session against the rendered transcript. If they disagree, the hook's subscription got dropped.

checkpoints()

Returns the checkpoint list for the current session, in creation order (ULID-sorted).

const cps = __HARNESS_DEVTOOLS__.checkpoints();
cps.map((c) => `${c.id} — ${c.stageId} — ${new Date(c.createdAt)}`);

Each checkpoint carries id, sessionId, stageId, createdAt, and the channel snapshot map.

rollback(checkpointId)

Time-travel. Restores the session to the checkpoint and re-renders.

await __HARNESS_DEVTOOLS__.rollback("cp_018f...");

The next executeMessage continues from the restored point. Subsequent checkpoints are preserved by default — pass { prune: true } as a second argument to drop them:

await __HARNESS_DEVTOOLS__.rollback("cp_018f...", { prune: true });

The pruning option is what you want when branching for an eval re-run.

tools()

The currently-registered tool definitions.

__HARNESS_DEVTOOLS__.tools().map((t) => t.name);
// → ["search_corpus", "calculator", "fetch_url"]

Inspect schemas:

const tool = __HARNESS_DEVTOOLS__.tools().find((t) => t.name === "search_corpus");
console.log(tool.inputSchema, tool.description);

Useful when the LLM is calling a tool name you don't recognize — verify it's actually in the registry.

syncStatus()

Coarse-grained sync state.

__HARNESS_DEVTOOLS__.syncStatus();
// → {
//     local:        { clientId, vector },
//     remote:       { vector },
//     pending:      3,
//     lastSyncedAt: 1717350000000,
//     errors:       []
//   }

For the rich shape, the React useSyncStatus hook returns the full SyncStats + SyncError[]. DevTools is the quick-look surface.

forceSync()

Drains the outbox immediately rather than waiting for the next flushIntervalMs tick. Returns when the cycle completes.

await __HARNESS_DEVTOOLS__.forceSync();
__HARNESS_DEVTOOLS__.syncStatus().pending; // → 0 if cycle succeeded

Useful when you want to verify a write made it through before closing the tab.

events()

Recent event log entries. Returns the last N (default 100); pass a filter for typed slices:

__HARNESS_DEVTOOLS__.events();

__HARNESS_DEVTOOLS__.events({ types: ["tool.failed"], limit: 20 });

__HARNESS_DEVTOOLS__.events({ since: "01jc8..." });

The shape is the same HarnessEvent shape the event log documents.

interrupts()

Outstanding HITL approvals on the current session. Useful when the UI's approval modal isn't surfacing what the runtime is waiting on.

__HARNESS_DEVTOOLS__.interrupts();
// → [{ id, action_request, config, description, ... }]

Resolve from the console:

const [pending] = __HARNESS_DEVTOOLS__.interrupts();
await runtime.resolveInterrupt(pending.id, { type: "accept", args: null });

ledger()

The in-memory audit ledger contents. Only populated when the runtime is configured with MemoryProviderDecisionLedger. Returns empty when the production Supabase adapter is wired (use the query API for that).

__HARNESS_DEVTOOLS__.ledger().filter((r) => r.callClass === "synthesize");
// → typically exactly one row per turn

The one-synthesize-per-turn invariant is the easiest property to spot-check from DevTools — if you see two synthesize rows for a single turnId, something has drifted.

updateDevToolsSession(state)

The manual push API. Normally the hook subscribes to runtime events and updates the DevTools surface automatically; this is the escape hatch for tests or imperative state writes.

import { updateDevToolsSession } from "@pleach/core/react";

updateDevToolsSession(synthesizedState);

Use sparingly. The hook subscription is the supported path.

Production safety

useHarnessDevTools does not check NODE_ENV internally — the caller is responsible. The hook body unconditionally writes to window.__HARNESS_DEVTOOLS__; ship it in production and your production bundle gains a debug surface and a tree-shake escape for the underlying modules.

Three options to gate it:

// Option 1 — conditional hook (lint rule will complain; disable it):
if (process.env.NODE_ENV !== "production") {
  // eslint-disable-next-line react-hooks/rules-of-hooks
  useHarnessDevTools();
}

// Option 2 — separate dev-only component, code-split by env:
const DevTools = process.env.NODE_ENV !== "production"
  ? require("./DevTools").default
  : null;

// Option 3 — always-on but pre-stripped at build time via dead-code elimination:
if (false /* @__PURE__ */) useHarnessDevTools();

Option 1 is the simplest. Option 2 is the cleanest for bundle size. Option 3 is for build pipelines that don't tree-shake conditionals well.

Where to go next

On this page