> ## Documentation Index
> Fetch the complete documentation index at: https://obversa.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Core

> @obversa/core: run one child process to a deadline and get a result you can read whatever the child did.

`@obversa/core` runs one child process with a deadline and returns a result
you can read whatever happened: it finished, it hung, it filled a pipe, or
it ignored a signal. Workflows run child processes all the time, a `git`
call, a model CLI, a test command, and a hang carries no error; this
package makes every one of them end with a result. The engine command
runner, the runtime's Git and command sites and the Git memory adapter run
their children through it.

## Install

<CodeGroup>
  ```bash npm theme={null}
  npm install @obversa/core
  ```

  ```bash pnpm theme={null}
  pnpm add @obversa/core
  ```
</CodeGroup>

Included in `@obversa/obversa`.

## Run one

Run a child with a deadline and an output cap, and read what it wrote:

```ts examples/run-child.ts theme={null}
import { runChild } from '@obversa/core';

const result = await runChild({
  executable: process.execPath,
  args: ['-e', 'process.stdout.write("ready")'],
  cwd: process.cwd(),
  env: {},
  stdin: '',
  timeoutMs: 30_000,
  killGraceMs: 5_000,
  maxOutputBytes: 1_024 * 1_024,
});

console.log(new TextDecoder().decode(result.stdout));
```

```text Output theme={null}
ready
```

`runChild(options)` needs `executable`, `timeoutMs` and `maxOutputBytes`.
It returns `exitCode` (a number, or `null` when a signal stopped the
child), `stdout` and `stderr` as bytes, and the `timedOut` and `aborted`
flags. All of it comes from facts the helper recorded, never from whichever
callback fired first. `stdout` and `stderr` hold what arrived before the
child exited and during a short drain after it; a timed-out child's result
still carries what it wrote before the deadline, up to the cap.

When the deadline passes, the child is stopped and the result says so, even
when the stopped child reports no exit code. Standard input is closed after
the optional input, and both output streams are drained until they close or
for a short grace after the child exits, under one combined byte cap.
Children the helper started are stopped when your process exits or is
interrupted: on exit they get a terminate signal, and on an interrupt the
helper stops its children and re-raises the signal. Your own handler for a
signal takes precedence.

## Find an executable

`resolveCommandExecutable` from `@obversa/core/command` reads your `PATH`
when the file runs, so a copied example doesn't carry a machine-specific
path:

```ts examples/teams/threshold-panel.ts (excerpt) {5} theme={null}
const realEngines: ThresholdPanelEngines = {
  claude,
  codex,
  opencode: (model) => opencode(model, {
    executable: resolveCommandExecutable('opencode'),
  }),
};
```

## Run an owned command

`runOwnedCommand` from `@obversa/core/command` runs a command-line tool to a
deadline with output and memory bounds and returns a typed result, and
`stopOwnedProcessTree` stops what it started. Both accept an `ownerId`, a
`sha256:` digest the command passes to its children as `OBVERSA_RUN_OWNER`.
Nested commands keep an inherited marker, and it wins over a supplied
`ownerId`; only the outer command that supplied the id sweeps that owner's
marked processes during cleanup.

`commandCleanupCapability()` says how the platform cleans up: on Linux
`inherited-owner`, processes carrying the marker are found through `/proc`;
elsewhere `observed-processes`, cleanup follows the observed tree, and a
helper that starts a new session before it's seen can escape.
`inspectOwnerMarkedProcesses(ownerId)` lists marked processes on Linux
without stopping them and returns an empty list elsewhere, which proves
nothing. Owner markers coordinate cleanup; they aren't a security boundary.
`ownedCommandIdentity`, `attemptEnvironment`, `retryAfterHeaderToMs`,
`scrubCapture`, `redactEnvValues` and `redactSecrets` are the helpers the
engine plugins share.

## Read Claude's stream

`@obversa/core/claude-stream-json` exports `mapMessage`, `newAccumulator`
and the `Accumulator` type for reading Claude stream messages.
`@obversa/core/claude-tools` exports `claudeToolOptions`, the tool
configuration the Claude adapters share: it keeps only requested `Read`,
`Grep` and `Glob` tools in `read` mode, no tools in `none` mode, removes
permission rules for withheld tools, and refuses malformed rules and custom
tools it can't bound with an `EngineError` of kind `invalid-config`.

## Options

### `timeoutMs` and `killGraceMs`

`timeoutMs` is the deadline; at it the child is signalled. `killGraceMs`
is how long the child has to stop before it is killed, and a child that
still doesn't stop makes the call throw `TEARDOWN_INCOMPLETE`.

### `detached`

By default only the child is signalled, so a process the child started and
left behind isn't stopped, and the child sits in your process group so a
terminal interrupt reaches it. `detached: true` gives the child its own
group and stops the whole group at the deadline.

### The rest

| Field | Type | Default | Description |
| - | - | - | - |
| `executable` | `string` | required | The program to run. |
| `args` | `readonly string[]` | none | Its arguments. |
| `cwd` | `string` | none | The working directory. |
| `env` | `Record<string, string \| undefined>` | none | Extra environment, merged over the parent's. |
| `inheritParentEnv` | `boolean` | `true` | `false` runs the child with `env` alone. |
| `stdin` | `string \| Uint8Array` | none | Written to the child, then standard input is closed. |
| `maxOutputBytes` | `number` | required | Combined cap on both output streams. |
| `signal` | `AbortSignal` | none | Abort the child from outside; the result says `aborted`. |

## Errors

* **`RunChildError`** with a `code`: `INVALID_OPTIONS`, `SPAWN_FAILED` (the
  executable can't start), `OUTPUT_LIMIT` (the child wrote past the cap),
  `TEARDOWN_INCOMPLETE` (the child didn't stop within the grace). It
  carries the `stdout` and `stderr` captured so far.
* **`OwnedCommandError`** on the `command` subpath: `INVALID_EXECUTABLE`,
  `INVALID_COMMAND`, `SPAWN_FAILED`, `OUTPUT_LIMIT`, `MEMORY_LIMIT`,
  `PROCESS_INSPECTION`, `TEARDOWN_INCOMPLETE`, with `remainingProcesses`.

## API

* **`runChild(options)`**: one child to a deadline. [Run one](#run-one).
* **`@obversa/core/command`**: `runOwnedCommand`, `stopOwnedProcessTree`,
  `ownedCommandIdentity`, `resolveCommandExecutable`,
  `commandCleanupCapability`, `inspectOwnerMarkedProcesses`,
  `inspectOwnedProcessTree`, `inspectAttemptMarkedProcesses`,
  `inspectPipeHoldingProcesses`, `capturePipeOwnerProbe`,
  `measureOwnedProcessMemory`, `readAttemptMarkerProcessIds`,
  `parseWindowsProcessRows`, `DEFAULT_OWNED_COMMAND_LIMITS`,
  `OwnedCommandError`. [Run an owned command](#run-an-owned-command).
* **`@obversa/core/claude-stream-json`**: `mapMessage`, `newAccumulator`.
  **`@obversa/core/claude-tools`**: `claudeToolOptions`.
  [Read Claude's stream](#read-claudes-stream).
* **`@obversa/core/testing`**: `MockEngine`, `MockResponder`, `mockVerdict`
  for scripted engine results. The adapter conformance kits are in
  `@obversa/api/testing`.

## Next steps

* [Safe node attempts](/docs/recording/node-attempts): how an engine turn is
  bounded and cleaned up on top of this.
* [API](/docs/packages/api): the engine contract the command runner serves.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.