# pi-codemode

`pi-codemode` is a source-only extension for pi. It adds the `eval` and `wait` tools.
The tools run persistent JavaScript, Python, and Ruby cells.

This release is verified with pi 0.83.0 and Node.js 24.11 or newer.
See [`TECHNICAL.md`](./TECHNICAL.md) for architecture, limits, and verification.

## Install and load

Install the package in the project that runs pi:

```bash
npm install @juvio15/pi-codemode
```

Load the extension with `-e`:

```bash
pi -e ./node_modules/@juvio15/pi-codemode/src/index.ts
```

Use the same explicit path when you use `-ne`:

```bash
pi -ne -e ./node_modules/@juvio15/pi-codemode/src/index.ts
```

`-ne` disables extension discovery. It does not disable an explicit `-e` path.
pi 0.83.0 has no bundled extension slot for this package.

## Use eval

The extension registers `eval` and `wait`. It also registers the `exec` error stub.
A tool call has this shape:
```json
{
  "language": "js",
  "code": "2 + 2",
  "title": "simple calculation"
}
```

The tool returns the value, text output, display values, status events, and
truncation details. State remains in the language kernel for later cells.

The tool descriptions carry "use when" triggers (batch anything multi-step,
parallel research, computation, detached long work, stateful kernels), and
when `eval`/`wait` are active the extension appends a bounded guidance block
to the system prompt (`promotion.enabled`, default on — see Configure below)
so models reach for the code-mode tools at the right moments.

You can use these fields:

| Field | Use |
| --- | --- |
| `language` | Select `py`, `js`, `rb`, or `jl`. |
| `code` | Send the cell body without changes. |
| `strings` | Named strings exposed as `π.<key>` inside the cell. Use for content that is awkward to quote in code (long literals, file contents, prompts). |
| `title` | Set a short label for the cell. |
| `timeout` | Set the cell timeout in seconds. |
| `on_timeout` | Use `detach` or `error`. |
| `reset` | Reset the selected language before the cell runs. |
| `action` | Use `peek` or `stop` for a detached cell. |
| `cell_id` | Select the detached cell for `peek` or `stop`. |
| `tools` | Allow only these tool names in the cell. Omit to allow all active tools. |

`tool_schema()`, `completion()`, `agent()`, and `output()` stay available in every cell.

Interactive, RPC, and app-server sessions detach on timeout by default. Print
and JSON sessions return an error on timeout by default. A detached cell keeps
its language kernel busy.
Use `peek` to read its state. Use `stop` to cancel it.

```json
{"action":"peek","cell_id":"<cell id>"}
{"action":"stop","cell_id":"<cell id>"}
```

JavaScript stop restarts the worker. JavaScript variables are lost.
Python stop interrupts the process. Python variables remain when the kernel
reports that state survived.

## Use the eval pragma

You can set cell options on the first line of the code. Use `// @eval:` for
JavaScript. Use `# @eval:` for Python and Ruby.

```
# @eval: {"timeout": 60, "on_timeout": "detach", "title": "long build"}
```

Explicit tool parameters take precedence over pragma fields: a `timeout`
parameter overrides a pragma `timeout`, and so on. The pragma line does not
run as code — it is stripped from the cell body before execution.

## Use strings

Pass named strings with `strings`, then read them as `π.<key>` inside the
cell. Use this for content that is awkward to quote in code, such as long
literals, file contents, or prompts. The mapping is read-only, lives for the
current cell only; the state does not leak into later cells.

```json
{"language": "js", "code": "return π.prompt", "strings": {"prompt": "Write a haiku about kernels."}}
```

```json
{"language": "py", "code": "print(π['prompt'])", "strings": {"prompt": "Write a haiku about kernels."}}
```

```json
{"language": "rb", "code": "puts π['prompt']", "strings": {"prompt": "Write a haiku about kernels."}}
```

Without `strings`, `π` is an empty mapping in Python and Ruby. In JavaScript,
reading a missing key throws a descriptive error that lists the provided keys.

## Use wait

A detached cell keeps running after the eval call returns. Use wait to
poll it or stop it.

```json
{"cell_id": "<cell id>", "yield_time_ms": 10000, "max_tokens": 10000}
{"cell_id": "<cell id>", "terminate": true}
```

The call returns only new output since the previous wait.
`yield_time_ms` sets the wait budget before yielding again.
`max_tokens` sets the output budget; one token equals four characters.
Pass `terminate: true` to stop the cell.

## Languages

| Language | Default | Runtime | Requirement |
| --- | --- | --- | --- |
| `js` | enabled | Node.js worker | Node.js 24.11 or newer |
| `py` | enabled | `python3` or `python` | Optional interpreter |
| `rb` | disabled | `ruby` | Optional interpreter |

An unavailable optional interpreter removes that language from the `eval`
schema. It does not fail installation. JavaScript remains available on Node.js
24.11 or newer.

## Configure the extension

The extension reads settings in this order, merging per key (project keys
override global keys, which override defaults):

1. `.pi/codemode.json` in the working directory.
2. `~/.pi/agent/codemode.json`.
3. Built-in defaults.
4. Environment overrides (`PI_CODEMODE_PY`, `PI_CODEMODE_JS`, `PI_CODEMODE_RB`)
   apply on top for the language settings.

Use this example as a starting point:

```json
{
  "languages": {"py": true, "js": true, "rb": false},
  "cellTimeoutSeconds": 120,
  "parallelPoolWidth": 4,
  "taskTools": {"task": "task", "output": "task_output"},
  "outputSink": {"headBytes": 20480, "maxColumns": 768},
  "statusEvents": true,
  "promotion": {"enabled": true}
}
```

| Setting | Default | Description |
| --- | --- | --- |
| `languages` | `py`, `js` enabled | Select languages before interpreter detection. |
| `cellTimeoutSeconds` | `120` | Set the default cell timeout. |
| `bridgeTimeoutSeconds` | `120` | Set the bridge read timeout for py/rb tool calls. Escape hatch for genuinely long calls such as `agent()` spawns; replaces the old 60 s cap. |
| `parallelPoolWidth` | `4` | Limit concurrent `parallel()` work. |
| `taskTools.task` | `"task"` | Set the active tool for `agent()`. |
| `taskTools.output` | `"task_output"` | Set the active tool for `output()`. |
| `outputSink.headBytes` | `20480` | Keep this many bytes from the output head. Use `0` to disable head output. |
| `outputSink.maxColumns` | `768` | Limit rendered output columns. Use `0` to disable the limit. |
| `statusEvents` | `true` | Forward and render kernel status events. Each cell keeps 100 rows. |
| `hardenedCells` | `false` | Hide `console`, `Atomics`, `SharedArrayBuffer`, and `WebAssembly` from JavaScript cells. |
| `jitless` | `false` | Run the JavaScript kernel as a `--jitless` child process. |
| `promotion.enabled` | `true` | Append the bounded eval/wait guidance block to the system prompt when the code-mode tools are active. |

The environment variables `PI_CODEMODE_PY`, `PI_CODEMODE_JS`, and
`PI_CODEMODE_RB` override the matching language setting.
Use `1` or `true` to enable a language. Use `0` or `false` to disable it.
Other values leave the file setting unchanged.

Invalid JSON or settings use the defaults and emit a warning.

## /codemode settings TUI

`/codemode` opens a menu that shows the **effective merged configuration**
(every setting with its source: `default`, `global`, `project`, or `edited`)
and the language availability (enabled flag + interpreter detection result
for `py`/`js`/`rb`). From the menu you can edit every setting in the table
above, then save the draft — validated against the settings schema — to
`.pi/codemode.json` (project) or `~/.pi/agent/codemode.json` (global);
the extension refuses invalid values with a warning. Headless (print/json)
runs print
the effective settings as JSON with a hint and never crash.

## Cell helpers

Each enabled language provides the same helper set:

| Helper | Function |
| --- | --- |
| `display(value)` | Display text, JSON, Markdown, or supported image data. |
| `print(value, ...)` | Write text output. |
| `read(path, offset?, limit?)` | Read text with one-based line ranges. |
| `write(path, content)` | Create parent directories and write text. |
| `env(key?, value?)` | Read or set environment values. |
| `tool.<name>(args)` | Call an available session tool. |
| `tool_schema(name?)` | Read one tool schema or list tool names. |
| `completion(prompt, model?, system?, schema?)` | Request one host completion. |
| `agent(prompt, ...)` | Call the configured task tool. |
| `output(ids, format?, offset?, limit?)` | Read `raw` or `tail` task transcript output. |
| `store(key, value)` | Stage a session value; commits when the cell completes. |
| `load(key)` | Read a stored session value, or `null` when missing. |
| `approve(toolName, args?, risk?)` | Check the approval gate; returns the verdict dict. |
| `parallel(thunks)` | Run bounded work in input order. |
| `pipeline(items, ...stages)` | Run stages with a barrier between stages. |
| `log(message)` / `phase(title)` | Report progress and start a status phase. |

`local://` paths resolve inside the session artifact directory. `agent()` needs
an active task tool. `output()` needs an active task-output tool.

The eval prompt shows typed declarations for active tools. The declarations
are a rendering of each tool's JSON Schema. Use `tool_schema()` for the full
schema.

`store()` and `load()` share one session value store across all kernels.
Values must be JSON-serializable; `store` stages a value under a string key
and the host commits it only when the cell completes — a cancelled or failed
cell discards its staged writes. `load(key)` returns the staged value when
present, then the committed value, then `null` for a missing key (the `null`
convention diverges from Codex's `undefined`). The store lives for the
session and is not persisted across sessions.

A failed `tool.<name>()` call includes the expected schema when pi provides it.
The cell can use that schema to correct the call. `eval` cannot call itself.

## Output and artifacts

Output streams while a cell runs. The extension limits each preview to 50 KiB
by default. It keeps head and tail text, clamps columns, and writes large output
to a file.

With a session file at `/path/session.jsonl`, artifacts use
`/path/session-artifacts/`. A session without a file uses a temporary directory.
The result reports a plain absolute path:

```text
[Full output: /absolute/path/eval-<id>.log]
```

The extension does not use an `artifact://` path scheme.

## Rendered output

The pi terminal renderer shows:

- Syntax-highlighted cell code.
- Cell output and structured values.
- Status rows and task progress.
- Truncation notices and image fallbacks.
- Bounded nested tool-call widgets.

The same custom renderer works in pi HTML export. A nested widget stores at most
30 calls, 4096 serialized argument characters, and a 160-code-point result
preview. The widget does not add session messages or extension events.

## Security and limits

Kernels run with the permissions of the pi process. The bridge listens on the
loopback interface. Each session uses a random bearer token. Request bodies are
bounded. A disconnected client aborts its bridge work.

JavaScript cells can be hardened with two optional settings. `hardenedCells`
removes `console`, `Atomics`, `SharedArrayBuffer`, and `WebAssembly` from the
cell scope for each hardened cell and restores them afterwards. `jitless` runs
the JavaScript kernel as a `--jitless` child process, which disables
just-in-time compilation and executable-memory allocation. Both settings
default to false and are opt-in.

## Approval gate

Risky tool calls from cells are gated before they execute. Each host tool call
is classified into a risk class — `read`, `write`, or `execute` — and checked
against a policy. The default policy allows `read` and asks for `write` and
`execute`. An `ask` opens the pi approval prompt (allow once, allow this
session, or deny) when an interactive UI is available; without a UI the call
fails with an approval error. A denied call fails with the policy or user
message.

Cells can check the gate explicitly with the `approve(toolName, args?, risk?)`
helper, which returns the verdict dict (`allow`, `ask`, or `deny`). The helper
is available in every kernel and uses the reserved `__approval__` bridge name.
Each verdict is recorded in the cell call trace.

Session grants persist for the pi session: after "Allow this session", the
risk class passes without asking again.

Session generations retire old kernels and callbacks. Each cell settles once.
The extension does not import an orchestration workspace package.

## Differences from senpi-codemode

- pi 0.83.0 has no `pi.executeTool`. `tool.<name>()` calls tools registered by
  this package. Other tools return `unknown_tool` or `inactive_tool` errors.
  `getActiveTools()` is the host's complete active-tool listing; appearing in
  that list does not make a tool callable from a cell.
- Opt-in extensions can expose their tools to cells over `pi.events`
  (cooperative peer protocol, Plan 003): import `pi-codemode/peer-server` and
  register your tool definitions with `createToolPeerServer`. Uncooperative
  tools keep the fail-closed refusal.
- pi 0.83.0 has no removed-tool hint API. `exec` is an error stub that
  explains its removal.
- pi stacks call and result lanes in one container. The call lane shows a
  compact header after execution starts. The result lane owns the status frame.
- Settings use `.pi/codemode.json` and `PI_CODEMODE_*` names.
- `pi -ne` requires an explicit `-e` path for this extension.
- The package has no `budget` helper and no `artifact://` protocol.

## Verify a local checkout

Run the one-command gate:

```bash
npm run qa
```

This gate runs the typecheck, the unit tests, and every QA driver. It prints
`QA: all N checks passed` when everything passes. It skips drivers whose
runtimes are missing. Ruby is optional.

Run the individual checks when you need them:

```bash
npx tsc --noEmit
npm test
node scripts/qa-e2e-eval.ts
node scripts/qa-e2e-eval.ts --abort-scenario
node scripts/qa-detached-peek.ts
node scripts/qa-reserved-bridge.ts
node scripts/qa-timeout-state.ts
node scripts/qa-render-dump.ts --fixture success
node scripts/qa-html-export.ts
node scripts/qa-js-cell.ts --code "1+1"
node scripts/qa-py-cell.ts --code "1+1"
node scripts/qa-rb-cell.ts --code "1+1"   # requires Ruby
pi -ne -e ./src/index.ts -p "eval js: 2+2"
```

The verified suite reports 653 passed tests and 9 skipped tests. The HTML
export check reports
`HTML_EXPORT: true` and `EVAL_RENDERED: true`.

## Documentation style

This README and [`TECHNICAL.md`](./TECHNICAL.md) use ASD-STE100 principles:
short sentences, active voice, controlled terms, and direct instructions.
The documents do not claim ASD certification.
