@rivus/agent-kit/harness
Effect: planInstall, applyInstall, verify, uninstall, inventory, doctor, HarnessLive and its ports — anywhere, with effect and a platform
Functions
applyInstall()
function applyInstall(plan: InstallPlan, options?: ApplyInstallOptions): Effect<ApplyReport, ApplyInstallError, HarnessServices>;Applies a plan from planInstall once. Under the scope's LedgerLock it probes what an earlier holder left pending,
refuses the plan when the ledger moved on since it was built or when any target no longer holds what the plan saw
(nothing is written then), and otherwise runs the steps with the guarantees of applyLocked. The lock is released
only after the step in flight finished and its outcome was recorded.
Writes into files that agents and other tools share are not compare-and-swap: a writer that does not take the LedgerLock can still change a file between the re-check and the write, and that change is lost without a trace.
Parameters
plan
options?
ApplyInstallOptions
Returns
Effect<ApplyReport, ApplyInstallError, HarnessServices>
discardPlan()
function discardPlan(plan: InstallPlan): Effect<void, PlanStale>;Drops a plan without applying it; applying it afterwards fails with PlanStale.
Parameters
plan
Returns
Effect<void, PlanStale>
doctor()
function doctor(options?: DoctorOptions): Effect<readonly Check[]>;Reports, without changing anything an agent reads: whether the ledger is readable and has unfinished operations,
whether the LedgerLock is free (and what its holder recorded), owned Artifacts that drifted from the ledger or are
broken symlinks, and the command hooks each agent loads (its settings files and every plugin's or extension's hooks
file, plus the other agents' files it runs) that never run (unknown events), use the wrong timeout unit, point at a
missing program whose first word shell-quote reads as a certain absolute path, or fire twice for one event: the
same command twice, or, given markers, two hooks of one application, such as an old hook in a settings file next
to the plugin that replaced it, or two bridges of one owner in a plugin directory. Taking the lock to test it
creates the lock files in the harness state directory and nothing else.
Parameters
options?
Returns
Effect<readonly Check[]>
inventory()
function inventory(options?: ScopeOptions): Effect<Inventory, LedgerReadError, LedgerRepository>;What the scope's ledger records, read without the lock; it may change right after.
Parameters
options?
ScopeOptions
Returns
Effect<Inventory, LedgerReadError, LedgerRepository>
locatorKey()
function locatorKey(locator: ArtifactLocator): string;Parameters
locator
Returns
string
planInstall()
function planInstall(bundle: Bundle, options: PlanInstallOptions): Effect<InstallPlan>;Plans the installation of a bundle into agents, without writing: renders the bundle with each agent's preferred
strategy (hooks placed so that no event fires twice in an agent that runs another agent's hooks), reads what is on
disk and in the ledger, and checks the plan invariants. A plan with a conflict and no explicit choice fails with
PlanConflict, listing the choices that would resolve each. When an earlier change left operations pending, it
takes the LedgerLock long enough to probe and settle them first.
Parameters
bundle
options
Returns
Effect<InstallPlan>
uninstall()
function uninstall(owner: string, options?: UninstallOptions): Effect<UninstallReport>;Removes what the ledger records for an owner, under one holding of the LedgerLock: plans the removal (an install
plan in which the owner wants nothing) against the ledger read under the lock and applies it at once. Only the
owner's entries go: a file harness created is deleted, a pre-image restored, an entry other owners or agents still
use released, and an Artifact the user changed since is kept and reported. Empty shared configuration files are
deleted only with recorded file-creation provenance; pre-existing files and implicit parent directories stay.
With legacyMarkers, what older versions
of the owner left without a ledger is removed as well. Nothing is written at a path a dotfiles manager owns or
through a symlink; such steps are notes of the plan.
Parameters
owner
string
options?
Returns
Effect<UninstallReport>
verify()
function verify(owner: string, options?: VerifyOptions): Effect<VerifyReport>;Compares the owner's Artifacts on disk with the ledger and, given the bundle, with what it wants, the way chezmoi
does (see verifyOwner). Reading needs no lock; only when the ledger is behind the disk does verify take the
LedgerLock, compare again under it and record the disk's hash (acknowledge).
Parameters
owner
string
options?
Returns
Effect<VerifyReport>
Classes
AgentCli
Extends
AgentCliBase
Constructors
Constructor
new AgentCli(_: never): AgentCli;Parameters
_
never
Returns
Inherited from
AgentCliBase.constructor
ArtifactFiles
The port for the agents' files; PlatformArtifactFilesLive provides it over PlatformService.
Extends
ArtifactFilesBase
Constructors
Constructor
new ArtifactFiles(_: never): ArtifactFiles;Parameters
_
never
Returns
Inherited from
ArtifactFilesBase.constructor
ExternalOwner
The ExternalOwner port; ChezmoiExternalOwnerLive asks chezmoi managed.
Extends
ExternalOwnerBase
Constructors
Constructor
new ExternalOwner(_: never): ExternalOwner;Parameters
_
never
Returns
Inherited from
ExternalOwnerBase.constructor
LedgerRepository
The ledger repository port; FileLedgerRepositoryLive keeps ledgers under $XDG_STATE_HOME.
Extends
LedgerRepositoryBase
Constructors
Constructor
new LedgerRepository(_: never): LedgerRepository;Parameters
_
never
Returns
Inherited from
LedgerRepositoryBase.constructor
Interfaces
AgentCommand
One call of an agent's own command line, such as registering a plugin.
Properties
agent
readonly agent: string;args
readonly args: readonly string[];command
readonly command: string;The executable's name, looked up on the platform's PATH.
ArtifactLocator
Where one Artifact lives.
Path convention, the same for InstallTarget.roots: absolute and normalized, with every directory above the
Artifact resolved by realpath and the Artifact's own last segment not followed (realpath(dirname) + "/" + basename). Two spellings of one file are then one locator, and a symlink at the Artifact itself stays visible: the
application reports it as ObservedArtifact.symlinkTarget, and a step that would write or delete through it is a
symlinked-target conflict, because an atomic write replaces the link with a regular file.
Properties
kind
readonly kind: ArtifactKind;member?
readonly member?: string;For a json-entry or toml-entry that is an element of the array at pointer: the element's identity, such as a
hook's command, which the configuration editor matches to find the element's index right before it writes. Other
elements inserted or removed around it then never redirect a step to the user's element.
memberIn?
readonly memberIn?: "element" | "hook-group";Where member sits. element (the default): an element of the array at pointer. hook-group: one hook inside
the groups of an event's hook list, as Claude Code's settings file nests them (/hooks/<event>/<group>/hooks/<i>):
the editor matches the hook whose command is member in any group, adds a new hook as a group of its own, and
drops a group that a removal leaves empty. A HookSpec sets no matcher, so the command alone identifies a bundle's
hook in an event; once a bundle can set matchers, the group's matcher belongs in member too. Not part of the key.
path
readonly path: string;See the path convention above. For a cli-registration, the file in which the agent's command line records it.
pointer?
readonly pointer?: string;Required for the kinds inside a file and absent for the others: a JSON pointer (/hooks/Stop) for json-entry and
toml-entry (over the TOML document's data), the block id for managed-block, the registered name for
cli-registration. A JSON pointer never names an array index, because indexes move when the user edits the array;
a segment of digits or - reads as one, so object keys the kit writes never look like that (see Bundle.restore).
Bundle
What one Owner wants installed into the agents' harnesses.
Properties
artifacts
readonly artifacts: readonly ArtifactSpecSnapshot[];digest
readonly digest: string;Identifies the artifacts' content, such as a hash the owner computes over them.
legacyMarkers?
readonly legacyMarkers?: readonly string[];Substrings by which the owner recognizes what its older versions installed without a ledger, such as their hook command lines. Such artifacts are replaced or removed instead of being installed a second time.
owner
readonly owner: string;version
readonly version: string;CliRegistration
How a cli-registration Artifact is made and removed. Its locator's path is the file in which the agent's command
line records it and its pointer the JSON pointer of that record, so that reading the file tells whether it exists;
its content is true when active and false when its configuration explicitly disables it.
Properties
installedCopy?
readonly installedCopy?: string;A file in which the agent keeps its own copy of what it registered, such as a plugin's hooks in Codex's plugin
cache. When set, an active registration's content is that file's text: a registration whose copy is outdated is
unregistered and registered again, and verify compares the copy the agent runs. A disabled registration stays
false, so a matching cached copy cannot hide the user's disabling it. This path and its parents are checked
for symlinks even before registration exists, and before each command-line mutation.
recorded?
readonly recorded?: object;The entries and copied directories affected by the command line. Declare all of them for its write guards;
symlinks at, above, or inside those directories block the command. Without the command on PATH, uninstall
removes these entries and directories itself. Include the registration's own entry at the command line's
declared config path: after resolving its specific agent home, that path must equal the planned locator's path.
Paths under an agent home resolve that home, never descendants.
copies
readonly copies: readonly string[];entries
readonly entries: readonly ArtifactLocator[];register
readonly register: CommandLine;unregister
readonly unregister: CommandLine;DoctorOptions
Properties
agents?
readonly agents?: readonly string[];The agents whose hook configuration is read; every agent with an install adapter by default.
markers?
readonly markers?: readonly string[];Substrings that identify one application's hooks, such as its current command and its legacy markers: an event that runs more than one of them in an agent is reported as firing twice.
owner?
readonly owner?: string;Only this owner's ledger entries are compared with the disk; every owner's by default.
projectRoot?
readonly projectRoot?: string;The project root, required for project: absolute, as the caller resolved it.
providers?
readonly providers?: readonly Provider[];scope?
readonly scope?: InstallScope;user by default.
HookRegistration
One hook as an agent's configuration registers it.
Properties
command
readonly command: string;event
readonly event: string;timeout?
readonly timeout?: number;In the dialect's timeout unit; absent when the spec sets none or the agent delivers events to a plugin.
HookSpec
Hooks that observe lifecycle events; they never decide a permission gate. Events are named per agent in that
agent's HookDialect, and hookRegistrations turns them into registrations in the agent's units.
Properties
command
readonly command: string;The command each hook runs; {agent} in it stands for the id of the agent a registration is for, so that the
hook process can name the declared agent to readHookEvent. Agents that review hooks by content hash ask again
whenever it changes, so it points at a stable shim and carries no version.
events
readonly events: Readonly<Partial<Record<CodingAgentId, readonly string[]>>>;Native event names by agent, keys of that agent's HookDialect.events; an agent missing here gets no hooks.
timeoutSeconds?
readonly timeoutSeconds?: number;Unset leaves each agent's default.
type
readonly type: "hooks";InstallAdapter
How Artifacts reach one agent: which strategies it supports, in order of preference, and how each renders a bundle. Pure: paths come from the context, and the application resolves them, reads what is there and plans.
Properties
agent
readonly agent: string;hookStrategies?
readonly hookStrategies?: readonly Strategy[];Strategies for hooks, most preferred first; absent when the agent takes no hooks.
requires?
readonly requires?: Readonly<Partial<Record<Strategy, string>>>;The executable a strategy needs on PATH; a strategy whose executable is missing is not chosen.
skillStrategies?
readonly skillStrategies?: readonly Strategy[];Strategies for skills, most preferred first; absent when the agent reads no skills.
specificationVersion
readonly specificationVersion: "harness-v1";Methods
bridgeSources()?
bridgeSources?(context: InstallContext): readonly BridgeSource[];The directories of bridge files this agent loads without a configuration entry, so that doctor can find an
event firing twice when two bridges of one owner coexist in one of them.
Parameters
context
Returns
readonly BridgeSource[]
cliRegistration()?
cliRegistration?(locator: ArtifactLocator, context: InstallContext): CliRegistration | undefined;The command lines behind a cli-registration locator this adapter renders, undefined for any other.
Parameters
locator
context
Returns
CliRegistration | undefined
hookFile()
hookFile(strategy: Strategy, bundle: BundleRef): string;The configuration file a hook strategy registers hooks in, written as in ForeignHooks.files (~/ and a path
under the home), so that other agents that run that file are found.
Parameters
strategy
Strategy
bundle
Returns
string
hookSources()?
hookSources?(context: InstallContext): readonly HookSource[];Every file this agent loads command hooks from, any owner's, so that doctor can find an event firing twice.
Parameters
context
Returns
readonly HookSource[]
legacySources()?
legacySources?(context: InstallContext, owner: string): readonly ArtifactSource[];Where older versions of this owner may have installed things for this agent without a ledger.
Parameters
context
owner
string
Returns
readonly ArtifactSource[]
renderHooks()
renderHooks(
strategy: Strategy,
registrations: readonly PlacedRegistration[],
bundle: BundleRef,
context: InstallContext
): readonly RenderedArtifact[];The hook Artifacts for the registrations placeHooks kept for this agent; none for an empty list.
Parameters
strategy
Strategy
registrations
readonly PlacedRegistration[]
bundle
context
Returns
readonly RenderedArtifact[]
renderSkill()
renderSkill(
strategy: Strategy,
skill: SkillSpec,
context: InstallContext
): readonly RenderedArtifact[];Parameters
strategy
Strategy
skill
SkillSpec
context
Returns
readonly RenderedArtifact[]
roots()
roots(context: InstallContext): readonly string[];The directories this adapter writes in, which every planned path must be inside.
Parameters
context
Returns
readonly string[]
InstallContext
What an install adapter renders against: the user's home and environment, as the application's platform has them.
Properties
env
readonly env: Readonly<Record<string, string | undefined>>;home
readonly home: string;InstallPlan
An install plan, obtained from planInstall (or uninstall, which applies one at once) and applied once with
applyInstall or dropped with discardPlan. It is a read-only view: changes shows each file's text before and
after, commands the agent command lines to run, expectedTrustPrompts what the agents will ask the user. The plan
holds against the ledger revision it was built on; applyInstall refuses it once the ledger has moved on or a
target changed.
Properties
agents
readonly agents: readonly string[];basedOn
readonly basedOn: PlanBasisSnapshot;bundle
readonly bundle: BundleRef;changes
readonly changes: readonly PlannedFileChange[];commands
readonly commands: readonly PlannedCommand[];droppedHooks
readonly droppedHooks: readonly DroppedHook[];expectedTrustPrompts
readonly expectedTrustPrompts: readonly TrustPrompt[];notes
readonly notes: readonly object[];Steps that leave a file to a dotfiles manager or a symlink instead of deleting or restoring it.
planId
readonly planId: string;status
readonly status: PlanStatus;ready until it is applied or discarded.
steps
readonly steps: readonly PlanStepSnapshot[];In execution order.
LedgerScope
Which ledger: one per scope, user or a project.
Properties
key
readonly key: string;A file-name-safe name: user, or project- and a hash of the project root.
projectRoot?
readonly projectRoot?: string;scope
readonly scope: "user" | "project";PlanConflict
The plan has conflicts without an explicit choice, so it is rejected as a whole before anything is written. Each conflicting step lists the choices that would resolve it; an empty list means no choice can.
Properties
_tag
readonly _tag: "PlanConflict";conflicts
readonly conflicts: readonly object[];planId
readonly planId: string;PlanInstallOptions
Properties
agents
readonly agents: readonly string[];The agents to install into. The plan covers the owner's Artifacts for these agents only.
choices?
readonly choices?: Readonly<Record<string, ConflictChoice>>;Explicit decisions for conflicting locators, keyed by locatorKey; see PlanConflict for what each allows.
compat?
readonly compat?: readonly HookCompat[];What the application read about runners of other agents' hooks, such as Grok's [compat.claude] hooks = false.
lockWaitMs?
readonly lockWaitMs?: number;How long to wait for the LedgerLock when an earlier change left operations to recover; 10 s by default.
projectRoot?
readonly projectRoot?: string;The project root, required for project: absolute, as the caller resolved it.
providers?
readonly providers?: readonly Provider[];scope?
readonly scope?: InstallScope;user by default.
strategies?
readonly strategies?: Readonly<Partial<Record<string, {
hooks?: Strategy;
skills?: Strategy;
}>>>;A strategy per agent that replaces the adapter's preference, such as shared-config for hooks.
TrustPrompt
A confirmation the agent will ask the user for after installation. A plan announces it and never approves it.
Properties
agent
readonly agent: string;kind
readonly kind: TrustPromptKind;locator
readonly locator: ArtifactLocator;UninstallOptions
Properties
agents?
readonly agents?: readonly string[];The agents to uninstall from: by default every agent the owner's ledger entries name, and with legacyMarkers
every agent with an install adapter as well.
legacyMarkers?
readonly legacyMarkers?: readonly string[];Substrings by which the owner recognizes what its older versions installed without a ledger; such Artifacts are removed too.
lockWaitMs?
readonly lockWaitMs?: number;projectRoot?
readonly projectRoot?: string;The project root, required for project: absolute, as the caller resolved it.
providers?
readonly providers?: readonly Provider[];scope?
readonly scope?: InstallScope;user by default.
VerifyOptions
Properties
agents?
readonly agents?: readonly string[];The agents to render the bundle for and verify. Owner entries naming any selected agent or occupying a desired locator are compared, reported or acknowledged. Omit to include all owner entries and render for their agents.
bundle?
readonly bundle?: Bundle;What the owner wants installed now. With it, verify also tells an outdated Artifact from one the user changed, finds desired Artifacts that are missing, and records silently in the ledger what is on disk already as desired.
compat?
readonly compat?: readonly HookCompat[];lockWaitMs?
readonly lockWaitMs?: number;projectRoot?
readonly projectRoot?: string;The project root, required for project: absolute, as the caller resolved it.
providers?
readonly providers?: readonly Provider[];scope?
readonly scope?: InstallScope;user by default.
strategies?
readonly strategies?: Readonly<Partial<Record<string, {
hooks?: Strategy;
skills?: Strategy;
}>>>;The strategies the bundle was installed with, when they were not the adapters' preference.
Type Aliases
ArtifactContent
type ArtifactContent = JsonValue;What an Artifact holds: the text of a file or managed block, a symlink's target, a directory's files as
{ "<relative path>": "<text>" }, or the value of a configuration entry or command-line registration.
BundleRef
type BundleRef = Pick<Bundle, "owner" | "version" | "digest">;ConflictChoice
type ConflictChoice = "force" | "adopt" | "backup";An explicit decision for one conflicting locator. adopt: what is there counts as the owner's own and is replaced,
so uninstall deletes it. backup: it is replaced and kept as the pre-image, so uninstall restores it. force:
the owner's content replaces another owner's or the user's.
HarnessServices
type HarnessServices =
| ArtifactFiles
| LedgerRepository
| LedgerLock
| AgentCli
| ExternalOwner
| PlatformService;JsonValue
type JsonValue =
| null
| boolean
| number
| string
| readonly JsonValue[]
| {
[key: string]: JsonValue;
};Variables
CONFLICT_CHOICES
const CONFLICT_CHOICES: Readonly<Record<ConflictSnapshot, readonly ConflictChoice[]>>;The choices that can resolve each conflict. Nothing ever writes at a dotfiles-managed path or through a symlink:
plan another strategy, change the dotfiles source, or plan the symlink's resolved path. adopt resolves a symlinked
target only where nothing has to be written, taking the link's current content as the owner's; conflictChoices
gives the choices for one conflicting step.
FileLedgerRepositoryLive
const FileLedgerRepositoryLive: Layer.Layer<LedgerRepository, never, PlatformService>;Ledgers as JSON files under $XDG_STATE_HOME/agent-kit/harness/<scope>/ledger.json (~/.local/state when the
variable is unset), written atomically, with pre-images beside them in pre-images/. See LedgerRepositoryShape
for the calling convention; a file of an unknown schemaVersion is never written over.
HarnessLive
const HarnessLive: Layer.Layer<
| ArtifactFiles
| LedgerRepository
| LedgerLock
| AgentCli
| ExternalOwner, never, PlatformService>;The default ports of the harness use cases: the agents' files through the platform, the file ledger repository,
the SQLite LedgerLock, agent command lines through Platform.process.run, and chezmoi as the ExternalOwner. The use
cases read PlatformService as well, so provide the platform with Layer.provideMerge, for example
HarnessLive.pipe(Layer.provideMerge(NodePlatformLive)). Replace one port by merging another Layer over it.
PlatformArtifactFilesLive
const PlatformArtifactFilesLive: Layer.Layer<ArtifactFiles, never, PlatformService>;The agents' files through PlatformService: JSON and JSONC entries edited with jsonc-parser, TOML entries with
@decimalturn/toml-patch, both keeping comments and formatting; whole files written atomically.
SqliteLedgerLockLive
const SqliteLedgerLockLive: Layer.Layer<LedgerLock, never, PlatformService>;The LedgerLock as an exclusive SQLite database per scope, <state>/agent-kit/harness/<scope>.lock.db, held with
locking_mode=EXCLUSIVE and BEGIN EXCLUSIVE until the Scope closes. The kernel releases it when the holder exits
or crashes, so the next process takes it at once and probes the pending operations. The holder writes its identity
to <scope>.lock.holder, which only diagnostics read. Without platform.sqlite, acquire fails with
LedgerLockUnavailable. Local directories only: fcntl locks are unreliable on NFS.