Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,18 @@ jobs:
python -m pytest
python -m ruff check .

- name: Set up Node for Pi integration tests
uses: actions/setup-node@v4
with:
node-version: "22.19.0"
cache: npm
cache-dependency-path: integrations/pi/package-lock.json

- name: Test Pi integrations
run: |
npm ci --prefix integrations/pi
npm test --prefix integrations/pi

- name: Build distributions
run: python -m build

Expand Down
39 changes: 39 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Tests

on:
pull_request:
push:
branches: [main]

permissions:
contents: read

jobs:
python:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.11', '3.12', '3.13']
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- run: python -m pip install '.[dev]'
- run: python -m pytest
- run: python -m ruff check .
pi:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22.19.0'
cache: npm
cache-dependency-path: integrations/pi/package-lock.json
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: python -m pip install .
- run: npm ci --prefix integrations/pi
- run: npm test --prefix integrations/pi
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ printf '%s\n' '{"session_id":"thr_123","cwd":"/workspace/project","hook_event_na
| foreman hook --client codex
```

The explicit `--client` selects a protocol adapter; `codex` is currently the only adapter and
The explicit `--client` selects `codex`, `pi`, or `pi-durable`; `codex` remains
the compatibility default. Foreman does not infer a client from arbitrary JSON, and unknown
clients fail closed. The adapter normalizes events before they reach the shared attached-worker
runtime and translates semantic outcomes back into client-specific hook JSON.
Expand All @@ -394,6 +394,11 @@ See [coding-assistant hooks and attached workers](docs/hooks.md) for the process
[Codex plugin README](https://github.com/thruwire/marketplace/tree/main/plugins/foreman) for
installation, trust, and diagnostics.

The [Pi and Pi Durable bridges](integrations/pi/README.md) forward native extension events and
task hooks to the same runtime. They include prompt routing, tool supervision, completion
continuations, and a Durable replay contract. Build and load them from this checkout; the bridge
package has not been published to npm.

## Persistence and inspection

Each repository gets local, ignored state:
Expand Down
24 changes: 22 additions & 2 deletions docs/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ target repository.
## Client adapters

The `--client` option selects the protocol adapter. Foreman does not guess from arbitrary input
JSON. It currently ships a `codex` adapter, which remains the default for compatibility:
JSON. It ships `codex`, `pi`, and `pi-durable` adapters. Codex remains the default for compatibility:

```bash
foreman hook --client codex
Expand All @@ -41,6 +41,19 @@ its assistant's event names and fields into `HookEvent`, then renders Foreman's
session code only sees those normalized types. Adding another assistant therefore requires a new
adapter and registry entry, not another supervision runtime.

Pi uses the TypeScript [bridge package](../integrations/pi/README.md), which wraps native events
in a session/cwd envelope and applies semantic response objects through Pi's native APIs. Its
events are `session_start`, `input`, `tool_call`, `tool_result`, `agent_before_settle`, and
`session_shutdown`. Input can consume a refused prompt before starting the agent; the final
actionable settle boundary supplies completion feedback and requests continuation.

Pi Durable uses native `GenerationTask` and `ToolTask` hooks: `beforeRequest`, `beforeTool`,
`afterTool`, and `onYield`. The bridge combines a stable storage identity with the conversation
ID, uses actual submitted entries as work, and deduplicates routing using their durable IDs.
Before-tool steering becomes a tool block because that hook cannot inject context. Generation
halt uses the public task abort API because ordinary hook exceptions are logged and swallowed.
See the bridge guide for setup, version support, and parallel-tool limitations.

## Event flow

- `SessionStart` creates or refreshes the local session record.
Expand Down Expand Up @@ -84,6 +97,13 @@ Sessions expire after seven days of inactivity by default. Set
`FOREMAN_HOOK_SESSION_TTL_SECONDS` to change that lifetime, or pass `--data-dir` to isolate the
state directory during development and testing.

Clients may provide `work_id` to route a repeated original submission idempotently. The saved
work prompt, responsibility context, and one-continuation allowance survive retries and process
restarts. A new work ID resets the allowance. Optional `event_id` caches a completion response,
so replay between an external decision and a client's own commit does not spend that allowance
again. Reusing an ID with different input fails. The replay cache is bounded by event history
limits and expires with the session; it does not promise exactly-once model or tool execution.

## Responsibility configuration

### Central attached-worker configuration
Expand Down Expand Up @@ -156,7 +176,7 @@ Coding-assistant plugin packaging remains separate from the runtime. The
protocol and is published through the ThruWire marketplace. The placeholder under
`integrations/claude` reserves the future Claude Code adapter location but is not installable.

Command evidence is likewise assistant-neutral: a Codex or future assistant plugin only forwards
Command evidence is likewise assistant-neutral: a Codex, Pi, or Pi Durable bridge only forwards
lifecycle events to `foreman hook`. Foreman owns provider invocation and check-specific evidence,
so a CLI verifier does not need a separate SDK integration for every coding assistant. See
[Evidence providers](evidence.md).
3 changes: 3 additions & 0 deletions integrations/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,8 @@ are available to marketplace discovery before installation.

- The [Foreman Codex plugin](https://github.com/thruwire/marketplace/tree/main/plugins/foreman)
invokes the core `foreman hook --client codex` protocol.
- [`pi/`](pi/) supplies buildable Pi extension and Pi Durable task-hook bridges, invoking
`foreman hook --client pi` and `foreman hook --client pi-durable` respectively. This is a local
integration package with installation documentation and offline tests, not an npm release.
- [`claude/`](claude/) reserves the future Claude Code integration location. It is not currently an
installable plugin.
3 changes: 3 additions & 0 deletions integrations/pi/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
dist/
*.tgz
155 changes: 155 additions & 0 deletions integrations/pi/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
# Foreman for Pi and Pi Durable

These bridges forward Pi lifecycle events and Pi Durable task hooks to the same
`foreman hook` supervisor used by Codex. Foreman owns routing, evidence, decisions,
request budgets, and attached-session storage. The bridge never makes a model call.

Tested against `@earendil-works/pi-coding-agent` and
`@earendil-works/pi-durable` **1.0.4**, with Node **22.19+**. Pi Durable's API is
experimental; the peer range is limited to 1.0.x. This integration is a local
package in the repository, not a published npm package. The former
`@mariozechner` packages are not part of this tested contract.

## Build and configure

Until this PR is released, install the Foreman core from this checkout:

```bash
python3 -m pip install -e /absolute/path/to/foreman
cd /absolute/path/to/foreman/integrations/pi
npm ci
npm run build
```

Put `TYPESAFE_API_KEY` in the environment or `~/.foreman/.env`. Existing Foreman
responsibilities, repository scope, extensions, and request budgets apply to both
clients; see [the hook guide](../../docs/hooks.md). The default bridge command is
`foreman hook --client pi` or `foreman hook --client pi-durable`, found on `PATH`.

## Pi coding agent

Load the built extension:

```bash
pi -e /absolute/path/to/foreman/integrations/pi/dist/pi.js
```

For persistent loading, add that absolute path to `extensions` in Pi's
`~/.pi/agent/settings.json`. Keep the adjacent `transport.js` in place.
For a custom executable, create a Pi extension that exports the factory result:

```js
import { createForemanPi } from '/absolute/path/to/foreman/integrations/pi/dist/pi.js';
export default createForemanPi({
command: ['/absolute/path/to/foreman'],
// dataDir: '/absolute/path/to/isolated-foreman-data',
// timeoutMs: 120000,
});
```

The extension handles session start/shutdown, prompt input, tool calls/results,
and `agent_before_settle`. Routing happens at `input`, which can consume a refused
prompt before the agent starts; `before_agent_start` supplies the returned
responsibility context. Pi's ordinary extension exceptions are notification-only,
so a routing failure consumes the input, and a tool failure returns a denial.

A completion block appends model-visible Foreman feedback and requests one
continuation. Foreman persists that allowance; another incomplete finish halts.
Post-tool blocking or halt preserves the result, adds feedback, and aborts the
agent. Aborted/error runs are not automatically continued. Manual user commands
such as `!bash` are outside agent tool supervision. Extensions that transform
input after this handler may change the eventual prompt; register Foreman after
input transformers when using them.

## Pi Durable

Install the extension in the host's registry before opening the harness:

```js
import { createRegistry, Harness } from '@earendil-works/pi-durable';
import { createForemanDurable } from '/absolute/path/to/foreman/integrations/pi/dist/durable.js';

let harness;
const registry = createRegistry();
// Install your tools and other extensions here.
registry.install(createForemanDurable({
sessionId: '/absolute/path/to/my-pi-storage',
cwd: '/absolute/path/to/my-repository',
harness: () => harness,
}));
harness = await Harness.open(storage, { models, registry }, context);
```

`storage`, `models`, and `context` are the host's normal durable setup. The lazy
`harness` accessor resolves after opening, before any work starts. `sessionId`
must uniquely identify the storage and stay the same when reopening it. A
conversation ID is only unique within one storage, so the bridge combines both.
A conversation's configured `cwd` takes precedence over the fallback above.
The extension is selected by default; when explicitly selecting extensions,
include `foreman` in the conversation's selection.

The native hooks are `GenerationTask.beforeRequest`, `GenerationTask.onYield`,
`GenerationTask.afterTools`, `ToolTask.beforeTool`, and `ToolTask.afterTool`. `beforeRequest` reads the run's
actual submitted entries through the harness, rather than routing the transcript
or a synthetic continuation prompt. Stable submission IDs keep routing and the
one-continuation allowance intact through retries and generation handovers. New
user submissions route new work. Text prompts are required; image-only submissions
are refused because Foreman has no text job to assess.

Pi Durable's `beforeTool` supports blocking and argument replacement, but no
context injection. Foreman steering at this boundary therefore blocks the
proposed tool with feedback for the model to act on. After a tool, steering is
appended to its result; blocking or halt also marks an error and requests
termination through the native result control. A saved conversation control
record also halts the generation at `afterTools`, including mixed parallel
batches where one result's terminate flag would otherwise be ignored. As with Pi, a tool already run
cannot be undone, and other calls in an existing parallel batch may already be
running. For strict sequential boundaries set `toolExecution: 'sequential'` in
harness settings.

Pi Durable reports and swallows ordinary generation-hook exceptions. The bridge
uses the public `abortTask` API and waits for invocation cancellation, rather
than joining its own invocation, before propagating a refused request or halt.
Completion decisions use both a task memo and Foreman's replay record, covering
the gap between the external decision and Pi's memo commit. This does not make
external model calls or tool executions exactly once. Foreman records expire
under its normal session TTL; reopening after expiry starts fresh supervision.
There is no shutdown task hook, so durable Foreman sessions expire naturally.

## Process contract and tests

Bridge input is one JSON envelope on stdin:

```json
{"session_id":"native identity","cwd":"/workspace","work_id":"stable work identity","event":{"type":"input","text":"Fix the tests"}}
```

The Pi Durable adapter instead accepts `beforeRequest` with `prompt`, tool hooks
with a native `call` (`id`, `name`, `arguments`), `afterTool` with `result`, and
`onYield` with `last_assistant_message`. `work_id` deduplicates routing;
`event_id` identifies replayable completion calls. Reusing either identity with
changed input fails. Successful output is `{}` or a semantic action object:
`{"action":"inject_context|block|halt","reason":"..."}`. The bridge applies it
through the client's native API; this JSON is not a native Pi hook response.

Commands are executed without a shell. Full arguments and results go over stdin.
Missing executables, nonzero status, invalid/oversized JSON, cancellation, and
timeouts prevent supervised operations. Diagnostics are drained rather than
retained in the bridge; use Foreman's saved state for assessment failure details.

```bash
npm test
# From the Foreman repository root:
PYTHONPATH=src .venv/bin/python -m pytest
.venv/bin/python -m ruff check .
```

Tests use published Pi packages, their offline faux provider, isolated temporary
storage, and a fixture hook process. They do not need provider credentials or
alter the user's installed integrations.

Upstream contracts: [Pi extensions](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/extensions.md),
[Pi Durable hooks](https://github.com/earendil-works/pi/blob/main/packages/durable/README.md#hooks).
The source inspected for implementation is
[`ae92585`](https://github.com/earendil-works/pi/tree/ae92585d3b3e5f1e4b123d14a34314d826d8d9f5);
type checks and runtime tests use the published 1.0.4 packages in the lockfile.
Loading
Loading