Library#

@kontourai/flow exports the same primitives the CLI uses, fully typed for TypeScript. Public usage is limited to the package root and the flow CLI — dist/ subpaths are implementation output, not API.

import {
  startRun,
  attachEvidence,
  continuePausedGate,
  evaluateRun,
  flowRunHead,
  loadRun,
  validateRunStateConsistency,
  validateDefinitionWithDiagnostics,
  validateRunTransition
} from "@kontourai/flow";

Run lifecycle#

import { startRun, attachEvidence, evaluateRun, loadRun, acceptException, listRuns } from "@kontourai/flow";

const { runId, state } = await startRun(".flow/definitions/agent-dev-flow.json", {
  cwd: process.cwd(),
  runId: "dev-1847",
  params: { subject: "feature-search-filters" }
});

await attachEvidence("dev-1847", {
  gate: "plan-gate",
  file: "./acceptance-bundle.json",
  kind: "trust.bundle",
  expectedRunHead: flowRunHead(state)
});

const result = await evaluateRun("dev-1847");
console.log(result.state.current_step, result.state.next_action);

const run = await loadRun("dev-1847"); // { dir, definition, state, manifest, config }

These functions read and write the same .kontourai/flow/runs/<run-id>/ files as the CLI, so library and CLI usage interleave freely — an agent harness can attach evidence programmatically while a human inspects with flow status. They do not fall back to .flow/runs/; migrate generated state from older versions before loading it.

Run recovery fence#

Recovery coordinators can close one fixed canonical run path without adding provider behavior to Flow:

import {
  FLOW_RUN_RECOVERY_FINALIZE_BEFORE_OPEN,
  FLOW_RUN_RECOVERY_FENCE_PROTOCOL,
  finalizeRunRecoveryFence,
  inspectRunRecoveryFence,
  withRunRecoveryLock,
  writeRunRecoveryFence
} from "@kontourai/flow";

console.log(FLOW_RUN_RECOVERY_FINALIZE_BEFORE_OPEN);

const activeFence = await writeRunRecoveryFence("dev-1847", {
  protocol: FLOW_RUN_RECOVERY_FENCE_PROTOCOL,
  run_id: "dev-1847",
  recovery_id: "recovery-01",
  status: "active",
  updated_at: new Date().toISOString()
}, process.cwd());
console.log(activeFence.fence.generation); // Flow-generated UUID

await withRunRecoveryLock(
  "dev-1847",
  "recovery-01",
  process.cwd(),
  async () => {
    // Verify and publish the coordinator-owned recovery transaction here.
  }
);

// After recovery work releases its ticket, reopen through a new native ticket:
await finalizeRunRecoveryFence("dev-1847", {
  recovery_id: "recovery-01",
  expected_generation: activeFence.fence.generation,
  updated_at: new Date().toISOString()
}, process.cwd(), {
  beforeOpen: async () => {
    // Revalidate coordinator-owned protected inputs under Flow's native ticket.
  }
});

console.log(await inspectRunRecoveryFence("dev-1847", process.cwd()));

The caller never supplies generation; Flow adds a canonical UUID v4 on every durable temp-file fsync → rename → parent-directory fsync publication. Inspection returns the exact-byte fingerprint and fixed run-directory device/inode with the persisted fence. Newly finalized open records include the exact active previous_generation they succeeded. writeRunRecoveryFence() is active-only. A mutation invoked while recovery is already active fails closed. A mutation that had already queued on Flow's native ticket before the fence became active releases and requeues its ticket until that exact recovery publishes open, then proceeds without losing the caller's operation. A waiter still behind an older writer at the ordinary contention deadline first binds any active recovery; ordinary contention is not otherwise extended. finalizeRunRecoveryFence() is the sole supported activeopen transition: it acquires Flow's native mutation ticket, verifies the exact expected active generation again, runs the optional coordinator-owned beforeOpen assertion under that same ticket, durably publishes open with that predecessor link, and only then releases the ticket. If the assertion throws, the active fence remains unchanged. Active publication uses the same ticket, and a same-ticket replacement during the assertion makes the stale finalizer reject. Callers validating external inputs must keep their own mutation guard held across the complete finalizer call when those inputs have writers outside Flow's native ticket.

Absence and a stable open record allow supported access. active, malformed, or unknown records fail closed. Exact bytes, generation, and directory identity must remain stable across a supported read. Flow's CLI, high-level run APIs, Console file load/optional repair/projection and artifact paths participate. FileConsoleSink recomputes the current projection under its mutation ticket. Direct filesystem access, generic JSON helpers, pure in-memory validation/projection, and rendering to an arbitrary directory do not. docs/decisions/run-recovery-fence.md defines the boundary.

expectedRunHead is an optional optimistic-concurrency guard for evidence attachment. Flow validates its shape before acquiring the run mutation ticket, then reloads canonical state and compares the head while holding the same per-run mutation lock that commits evidence. A stale or malformed head rejects before evidence bytes or manifests are written. This lets a consumer bind a definition-sensitive capability to the exact state it authorized without reimplementing Flow's lock or amendment semantics. The CLI exposes the same contract as flow attach-evidence --expected-run-head <sha256>.

Atomic paused-gate continuation#

continuePausedGate() holds Flow's existing per-run mutation ticket while it checks a required expectedRunHead, accepts evidence for the persisted current gate, and evaluates it. It commits only a passing outcome. resumeOnPass is an explicit caller choice; a requested resume also carries the normal provider-neutral lifecycle reason and authority record. Flow records that ordinary resume before evaluating the gate, then preserves the active or completed status derived by the gate evaluation. resumeOnPass: false is a dry evaluation mode: even a passing outcome writes nothing.

const paused = await loadRun("dev-1847");
const result = await continuePausedGate("dev-1847", {
  cwd: process.cwd(),
  gate: "verify-gate",
  expectedRunHead: flowRunHead(paused.state),
  evidence: { file: "./review.json", kind: "trust.bundle" },
  resumeOnPass: true,
  resume: {
    reason: "The authenticated operator requested continuation after review.",
    authority: {
      kind: "operator_request", actor: "operator:alex",
      request_ref: "review-request:418", requested_at: "2026-07-22T12:00:00.000Z"
    }
  }
});

On a non-pass — or when resumeOnPass is false — result.committed is false and Flow returns the computed outcome without writing evidence, manifests, state, or reports. Durable rejected or held review records remain with their evidence producer. Existing attachEvidence, evaluateRun, and resumeRun behavior is unchanged.

Pure canonical state validation#

validateRunStateConsistency(startDefinition, state, { runId }) is the pure, non-mutating validation path used by loadRun. It validates the shipped run schema and lifecycle, replays the complete definition-amendment ledger, checks the effective definition and optional run id, and proves semantic retry and route-back history. It returns normalized { startDefinition, definition, state } and performs no file reads, report repair, locking, or writes.

Consumers that securely read canonical bytes themselves can use this public root export instead of importing private dist/ modules or copying only part of Flow's validation semantics.

Multi-cursor resource claims#

Definitions that a host may execute with more than one active cursor opt in explicitly. Every step then declares the bounded mutable-resource set it may change; an empty array is the explicit read-only declaration.

{
  "execution": { "mode": "multi-cursor", "claim_contract_version": "1" },
  "steps": [
    { "id": "render", "next": null, "mutable_resources": ["dist"] },
    { "id": "review", "next": null, "mutable_resources": [] }
  ]
}

projectReadyStepFrontier() derives the Flow-owned ready frontier and binds it to the run id, normalized definition id/version/digest, and exact run head. buildActiveStepClaim() builds a versioned claim for one ready step from a caller-supplied claim id, liveness id, and structured actor. Before recording a claim, a host can call validateActiveStepClaim() with other active claims: the result exposes deterministic same-resource conflicts, stale run heads, stale definition identities, and no-longer-ready steps. Flow first binds the supplied run state's definition id/version and optional legacy-compatible digest to the supplied definition. It revalidates every supplied active claim against that same identity, run head, and the step's authored resource set before using the claim for conflict comparison.

import {
  buildActiveStepClaim,
  projectReadyStepFrontier,
  validateActiveStepClaim
} from "@kontourai/flow";

const frontier = projectReadyStepFrontier(definition, state);
const claim = buildActiveStepClaim(definition, state, {
  claim_id: "claim-render-1",
  liveness_id: "lease-worker-7",
  step_id: "render",
  actor: { key: "worker-7", kind: "host" }
});
const admission = validateActiveStepClaim(definition, state, claim, activeClaims);

These preflight functions are pure: they do not persist a claim, acquire a host lock, dispatch work, infer actor authority, or treat observed writes as scheduling authority. They remain useful to hosts that want to inspect a candidate before requesting Flow admission.

For a durable run, use Flow's atomic lease APIs. They persist a versioned step-local claim base (not hash(state.json)), so the claim ledger and renewal timestamps cannot self-invalidate a lease. The base includes only the claimed step's prerequisite settlements and route/retry history; a disjoint sibling may settle without invalidating the other claim.

import {
  claimReadyStep,
  evaluateClaimedStep,
  recoverExpiredStepClaims,
  releaseStepClaim,
  renewStepClaim
} from "@kontourai/flow";

const claim = await claimReadyStep("dev-1847", {
  cwd: process.cwd(),
  claim_id: "claim-render-1",
  liveness_id: "lease-worker-7",
  step_id: "render",
  actor: { key: "worker-7", kind: "host" },
  lease_seconds: 300
});

await renewStepClaim("dev-1847", {
  cwd: process.cwd(), claim_id: claim.claim.claim_id,
  liveness_id: claim.claim.liveness_id, actor: { key: "worker-7", kind: "host" }
});

// Evidence is attached through the normal Flow API. This evaluates only the
// active claimed step and atomically settles pass, block, or route-back.
await evaluateClaimedStep("dev-1847", {
  cwd: process.cwd(), claim_id: claim.claim.claim_id,
  liveness_id: claim.claim.liveness_id, actor: { key: "worker-7", kind: "host" }
});
await recoverExpiredStepClaims("dev-1847", { cwd: process.cwd() });
await releaseStepClaim("dev-1847", {
  cwd: process.cwd(), claim_id: claim.claim.claim_id,
  liveness_id: claim.claim.liveness_id, actor: { key: "worker-7", kind: "host" },
  reason: "host did not start execution"
});

These APIs serialize their state changes with Flow's per-run ticket but still do not dispatch work. Hosts own placement, authentication, and execution. Existing definitions that omit execution remain legacy single-cursor definitions and are readable and serial; they are rejected by multi-cursor claim APIs until migrated explicitly.

Pure trust attachment reducer#

reduceTrustAttachment() is the separately versioned 1.1.0 reducer for an OS-owned lifecycle coordinator. It accepts canonical in-memory run state, manifest, bundle, attachment metadata (including ID, source digest, and timestamp), an explicit now, and version-pinned Hachure/Surface dependency adapters. It returns the next manifest/state, derived report, evaluation result, and a complete descriptive write set. It never reads files, uses ambient time, or performs network/process operations.

The default evaluate mode preserves the atomic attach-and-evaluate behavior. Use evaluation_mode: "attach-only" for provenance refreshes, critique supersession, and other authority-only synchronization. That mode validates and attaches the bundle, derives reports, and deliberately leaves run state and route-back accounting unchanged; its write set therefore omits state.json.

The reducer does not embed Hachure schema or Surface trust semantics. A caller supplies the hachure@0.15.0 schema validator and @kontourai/surface@2.12.0 validator/report builder as explicit dependencies; FLOW_TRUST_ATTACHMENT_REDUCER_DEPENDENCIES is Flow's adapter for those locked package versions. Pin the published package integrity plus trustAttachmentReducerIdentity() when a privileged coordinator needs a stable reducer contract. The identity hash binds the reducer API version and dependency versions, while package integrity binds the artifact bytes.

Authorized definition amendment#

amendRunDefinition changes the effective definition for an active run without replacing that run. Read the exact state and effective identity first, have the consumer authenticate its authority, then submit a complete successor and request. definition.json and evidence remain immutable start artifacts.

import { amendRunDefinition, definitionDigest, effectiveDefinitionIdentity, flowRunHead, loadRun } from "@kontourai/flow";

const run = await loadRun("dev-1847");
const successor = { ...run.definition, version: "corrected-opaque-version" };
await amendRunDefinition("dev-1847", {
  definition: successor,
  request: {
    reason: "Authorized route correction.",
    expected_run_head: flowRunHead(run.state),
    expected_definition: effectiveDefinitionIdentity(run.startDefinition, run.state),
    successor_digest: definitionDigest(successor),
    authority: { kind: "operator_request", actor: "operator:42", request_ref: "request:42", requested_at: "2026-07-20T05:00:00.000Z" }
  }
});

The request is exact-head and non-idempotent: stale heads or a reused request_ref reject without mutation. Flow validates the neutral authority record but does not authenticate the actor. Each audit event retains the exact pre-amendment state without nesting the ledger; Flow reconstructs the ledger prefix and replays compatibility against the history that existed when that successor was admitted, not against history created by the successor later. The repository decision record docs/decisions/definition-amendment.md carries the durable rationale.

Use listRunsWithDiagnostics(cwd) when corrupt or incomplete canonical entries must be surfaced alongside valid run summaries. listRuns(cwd) preserves the original summaries-only return shape.

Pause, resume, and cancellation#

import { pauseRun, resumeRun, cancelRun, FlowLifecycleError } from "@kontourai/flow";

const authority = {
  kind: "user_request" as const,
  actor: "user:brian",
  request_ref: "conversation:01J2/request:42",
  requested_at: "2026-07-10T12:00:00.000Z"
};

await pauseRun("dev-1847", {
  cwd: process.cwd(),
  reason: "The user asked to pause.",
  authority
});

await resumeRun("dev-1847", {
  cwd: process.cwd(),
  reason: "The user asked to continue.",
  authority: { ...authority, request_ref: "conversation:01J2/request:43" }
});

const canceled = await cancelRun("dev-1847", {
  cwd: process.cwd(),
  reason: "The user asked to stop.",
  authority: { ...authority, request_ref: "conversation:01J2/request:44" }
});
console.log(canceled.idempotent); // false on the first application

Lifecycle operations preserve current_step, Step transitions, gate outcomes, evidence, and exceptions. Pause records whether the run was active, blocked, or needs_decision; resume accepts only paused and restores that exact status. Cancellation accepts declared nonterminal states including paused and produces the terminal canceled status. Evaluation and Step advancement reject both paused and canceled before freshness derivation or persistence, and readiness returns an empty frontier.

All operations require the same structured external authority record as the CLI. Flow accepts only user_request and operator_request; a calling product must authenticate the actor and preserve the immutable request reference. Flow does not infer an invoking agent's authority. Invalid requests throw FlowLifecycleError, whose code and diagnostics use stable flow.lifecycle.* identifiers. Exact cancellation replay returns the existing event with idempotent: true and does not write. A conflicting replay throws flow.lifecycle.replay.conflict, also without writing.

Lifecycle audit text is bounded printable Unicode: actor is limited to 256 characters, request_ref to 2048, and reason to 4096. C0, DEL, and C1 control characters (including CR/LF and terminal ESC/OSC sequences) are rejected. Markdown and shell punctuation remain inert data and are escaped when rendered.

The state.lifecycle ledger is distinct from Step transitions. A run created by an older compatible release may omit lifecycle; loadRun normalizes that absence to [] in memory without creating a second compatibility API or rewriting the file. A present malformed ledger fails schema validation instead of being repaired. Canonical validation also rejects broken event sequences, incorrect prior-status restoration, status/latest-event disagreement, and terminal cancellation reversal. Generic persistence is internal; public consumers mutate canonical runs only through guarded domain operations. Ordinary Flow evaluation may move among resumable statuses between lifecycle events. Resume still pairs with and restores the most recent unmatched pause; paused intervals accept only their matching resume or cancellation, and no event may follow cancellation.

Lifecycle persistence is fail-closed before its first write: Flow validates the request and eligibility and computes canonical state plus both report projections first. The local file store then writes state.json, report.json, and report.md sequentially. It is a single-writer filesystem contract, not a multi-file transaction; an exceptional I/O failure between writes can leave a stale derived report. Reload canonical state.json and regenerate reports before retrying. Flow serializes public same-run mutations with the shared owner-recorded mutation lock; consumers still must not write run files directly. Lifecycle output targets use no-follow descriptor writes and regular-file verification. This closes target-symlink swaps without expanding the trusted single-writer boundary into a hostile multi-writer filesystem guarantee.

Authorized retry epochs#

authorizeRetry() recovers only the current exhausted on_exceeded: "block" route-back on the same run. It appends a retry_authorized run transition, moves the cursor only to that exhausted transition's selected_route, and starts the next persisted retry epoch. It is separate from lifecycle resume and does not pass a gate, accept an exception, remove failed history, or choose a caller-selected recovery route.

import { authorizeRetry, flowRunHead, flowTransitionRef, loadRun } from "@kontourai/flow";

const run = await loadRun("dev-1847");
const block = run.state.transitions.at(-1);
const result = await authorizeRetry("dev-1847", {
  request: {
    reason: "Approved one additional bounded retry epoch.",
    target_step: block.selected_route,
    blocked_transition_ref: flowTransitionRef(block),
    expected_run_head: flowRunHead(run.state),
    authority: {
      kind: "operator_request",
      actor: "operator:alex",
      request_ref: "change-request:418",
      requested_at: "2026-07-19T15:30:00.000Z"
    }
  }
});
console.log(result.transition.retry_epoch); // 2

The matching request may be replayed exactly and returns the stored transition without writing. A stale run head, forged block ref, wrong target, non-blocked or terminal run, malformed authority, or changed request content under the same request_ref fails before mutation. Flow records provider-neutral authority but callers authenticate it. The stored prior_run_head is the event-time optimistic-concurrency and audit binding copied from the request's expected_run_head. Because local run state is unsigned, that value is not an independently reconstructible post-persistence tamper-evidence guarantee; authenticity requires a signature or an externally trusted append-only store. Local unsigned state is a trusted persistence boundary: Flow rejects malformed or partial reserved transition records and binds requests to event-time state, but does not claim resistance to an attacker who rewrites an entire valid ledger and recomputes every unsigned hash. Signed or externally anchored history belongs to the trust layer tracked in #93. Routes without retry_epoch remain compatible epoch 1 records; later matching failures count only within the new epoch.

All same-run state writers share an owner-recorded mutation lock. Each contender owns a unique deterministically ordered ticket, release quarantines and removes only that ticket, and stale recovery removes only a demonstrably dead ticket. Reclaimers therefore never rename a shared canonical owner or detach a live successor. New ticket roots permanently contain both a reserved foreign-host compatibility owner.json sentinel and the ticket-lock-v1 marker; neither is rewritten or removed by ticket cleanup. An unmarked legacy root — including a dead, released, malformed, live, or ownerless legacy owner — fails with flow.run_mutation.lock.migration_required and Flow makes no change. A marked root with a missing, malformed, or linked sentinel/marker likewise fails closed. Perform any legacy-root cleanup only during an operator-confirmed quiescent window after checking that no process can still use the run; never blindly delete .mutation.lock. Live tickets serialize retry, lifecycle, evidence, evaluation, and exception mutations. Retry authorization reloads and rechecks the bound head inside that lock, derives its timestamp internally, stages reports, and atomically replaces state.json as the final commit point. Every report carries the represented state_head; a projection observed across a crash boundary is current only when that value matches the canonical state hash. transitions and gate_outcome_history are audit ledgers; gate_outcomes is the compatible current projection. Authorization removes the exhausted prior-epoch decision from that current projection while retaining it in both audit ledgers. Reports show the new epoch's evolving consumed_attempts, next_attempt, and remaining_attempts, and distinguish current from historical epoch budgets.

Flow owns canonical lifecycle validation and persistence only. Authentication, provider updates, assignment release, artifact archival, and branch/worktree cleanup belong to the calling consumer.

Validation#

import { validateDefinition, validateDefinitionWithDiagnostics, validateRunTransition } from "@kontourai/flow";

// pass/throw API
validateDefinition(definition);

// diagnostics API
const result = validateDefinitionWithDiagnostics(definition);
if (!result.valid) console.error(result.diagnostics);

// provider-neutral transition legality
const transition = validateRunTransition({
  definition,
  current_state: state,
  proposed_transition: { from_step: "verify", to_step: "publish", gate_id: "verify-gate" },
  manifest
});
if (!transition.valid) console.error(transition.diagnostics);

Console projection#

projectFlowRunFromFiles is the Flow boundary for console consumers: read-only, local-file-first, and deterministic. It reads authoritative definition.json, state.json, and evidence/manifest.json, then re-derives the report view instead of trusting disposable report.json. It preserves explicit external refs (Surface, Veritas, artifacts, pull requests, CI, release reports) when they already exist in authoritative run files — it never synthesizes refs from git, network calls, or Markdown parsing.

import { projectFlowRunFromFiles, startFlowConsoleServer } from "@kontourai/flow";

const projection = await projectFlowRunFromFiles("dev-1847", { cwd: process.cwd() });
console.log(projection.current_step);
console.log(projection.gates);

// or serve the packaged local console programmatically
const server = await startFlowConsoleServer({ runId: "dev-1847", cwd: process.cwd(), port: 0 });
console.log(server.url);
await server.close();

The packaged local console uses this same projection. Hosted behavior and companion console startup are outside the v0.1 package boundary.

Release readiness and reports#

import {
  evaluateReleaseReadiness,
  changeManagementFixtureAdapter,
  deploymentWindowFixtureAdapter,
  freezeStateFixtureAdapter,
  projectVersionReleaseReport,
  renderVersionReleaseReportMarkdown
} from "@kontourai/flow";

See Release Readiness for a worked example against the bundled scenario fixtures.

Config merge#

import {
  previewFlowConfigMerge,
  applyFlowConfigMerge,
  renderConfigMergeMarkdown
} from "@kontourai/flow";

const report = previewFlowConfigMerge(localConfig, proposedConfig);
console.log(renderConfigMergeMarkdown(report));

See Project Config for merge semantics and conflict handling.

Types#

The package root exports the public contract types — among them FlowDefinition, FlowRunState, FlowRunStatus, FlowLifecycleAction, FlowLifecycleAuthority, FlowLifecycleEvent, FlowLifecycleRequest, FlowLifecycleDiagnostic, FlowGate, FlowExpectation, FlowEvidenceEntry, FlowEvidenceManifest, GateOutcome, FlowDiagnostic, TransitionValidationResult, ReleaseReadinessPolicy, ReleaseReadinessResult, VersionReleaseReport, ConfigMergeReport, and the FlowConsole*Projection family. It also exports flowRoot(), flowConfigPath(), flowRuntimeRoot(), and canonical runDir() path helpers. The corresponding JSON Schemas live in schemas/, and npm test fails if the runtime drifts from them. See Runtime Roots for the semver-major runDir() contract and compatibility guidance.