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
20 changes: 16 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,18 @@ env:

jobs:
test-node:
name: Runtime Node ${{ matrix.node-version }}
name: Runtime Node ${{ matrix.node-version }}${{ matrix.label }}
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
node-version: [22.19.0, 24, 26]
include:
- { node-version: 22.19.0, pi-host: locked, label: "" }
- { node-version: 24, pi-host: locked, label: "" }
- { node-version: 26, pi-host: locked, label: "" }
- { node-version: 22.19.0, pi-host: minimum, label: " (Pi minimum)" }
- { node-version: 22.19.0, pi-host: latest, label: " (Pi latest)" }
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
Expand All @@ -37,6 +42,12 @@ jobs:
env:
BUN_INSTALL_CACHE_DIR: ${{ runner.temp }}/bun-install-cache
run: bun install --frozen-lockfile
- name: Install and verify the selected Pi compatibility host
if: matrix.pi-host != 'locked'
env:
PI_HOST: ${{ matrix.pi-host }}
BUN_INSTALL_CACHE_DIR: ${{ runner.temp }}/bun-install-cache
run: node scripts/select-pi-ci-host.mjs
- run: bun run check
- name: Verify committed Web build
run: |
Expand All @@ -52,6 +63,7 @@ jobs:
agent_dir="$(mktemp -d)"
trap 'rm -rf "$agent_dir"' EXIT
PI_CODING_AGENT_DIR="$agent_dir" ./node_modules/.bin/pi install "$PWD"
PI_CODING_AGENT_DIR="$agent_dir" ./node_modules/.bin/pi list
output="$(PI_CODING_AGENT_DIR="$agent_dir" ./node_modules/.bin/pi --offline --no-session --mode rpc </dev/null)"
grep -q '"statusKey":"plan-mode"' <<<"$output"
- name: Smoke-test packed extensions without optional peers
Expand All @@ -74,7 +86,7 @@ jobs:
--extension "$package/extensions/git-info/index.ts" \
--extension "$package/extensions/ai-providers/index.ts" \
</dev/null
- name: Smoke-test managed Web CLI with a separate locked Pi host
- name: Smoke-test managed Web CLI with a separate selected Pi host
run: |
root="$(mktemp -d)"
server_pid=""
Expand Down Expand Up @@ -148,7 +160,7 @@ jobs:
test -s "$root/source/web/dist/favicon.svg"
npm install --prefix "$root/source" --ignore-scripts --omit=dev --no-audit --no-fund
test ! -e "$root/source/node_modules/@earendil-works/pi-coding-agent"
# Exercise the CLI with the lockfile's Pi host, not package-owned peers.
# Exercise the CLI with this job's verified Pi host, not package-owned peers.
mkdir -p "$root/source/node_modules/@earendil-works"
ln -s "$PWD/node_modules/@earendil-works/pi-coding-agent" \
"$root/source/node_modules/@earendil-works/pi-coding-agent"
Expand Down
106 changes: 106 additions & 0 deletions docs/decisions/0006-supported-pi-versions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
---
decision-status: proposed
created: 2026-10-09
last-reviewed: 2026-10-09
applies-to: OpenPI Pi compatibility checks from adoption of this change forward
owner: OpenPI maintainers
related-issues: "#328"
related-prs: "#719"
supersedes: none
---

# Decision 0006: Supported Pi versions

## Context

[#328](https://github.com/openpi-dev/openpi/issues/328) tracks the difference
between a declared support range and the Pi host exercised by frozen-lock CI.
[#410](https://github.com/openpi-dev/openpi/pull/410) raised the minimum to
0.85.1; [#620](https://github.com/openpi-dev/openpi/pull/620) repaired the 0.86
transcript boundary; [#636](https://github.com/openpi-dev/openpi/pull/636) raised
the minimum to 0.99.1 and replaced local replay with Pi's native helpers.
The earlier [#330](https://github.com/openpi-dev/openpi/pull/330) and
[#333](https://github.com/openpi-dev/openpi/pull/333) were closed without merging.

At source `a18f457687b45c877bc8a15d689a7580c4f0659a`, the three Pi development
ranges are `^0.99.1`, the lock resolves 0.99.1, and the published peers are
`>=0.99.1`. The minimum and lock currently coincide. Existing package and Git
smokes select that locked host; they do not continuously exercise newer hosts
allowed by the peers. Historical compatibility evidence is retained in the
[0.99.1 investigation](../research/PI_0_99_COMPATIBILITY_2026-09-30.md).

## Decision

Proposed for maintainer adoption with [#719](https://github.com/openpi-dev/openpi/pull/719):

- Keep Pi packages host-owned peers. Preserve the current open-ended peer range;
do not bundle a second Pi runtime or add an OpenPI runtime version-rejection gate.
- Declare one common Pi minimum in the three peer ranges and in README/SETUP.
Development ranges may advance independently above that floor. The package
contract checks the declarations and their matching lock importer. The resolved host packages must agree and cannot be below the minimum.
- Keep frozen-lock checks on the existing Node versions. Add independent minimum
and latest-stable Pi jobs on the minimum supported Node version. Read the minimum
from the published peer declaration; resolve `pi-coding-agent@latest` once per
job and record the exact selected version.
- Install the selected Pi family transiently, check the resolved family and actual
installed package versions, and restore the publishable manifest and lock before
normal checks. Run the existing check, Node suite, package-discovery, packed
extension, packed Web, and Git-source smokes with that selected host.
- Make failures in either endpoint fail the existing aggregate CI gates. On a Pi
release or compatibility report between OpenPI changes, rerun CI at the affected
source revision. A failure requires investigation and a reviewed compatibility
repair or support-range change; it must not be hidden by moving the lock alone.

## Evidence boundary

The open-ended peer range admits newer hosts; it does not provide test evidence
for an unpublished version or every intervening release. CI records the minimum,
locked, and latest stable hosts actually exercised at that revision and time.
The latest job changes when npm's stable tag changes, so its result is not
reproducible without retaining that exact host version.

Repository checks cover native contract fixtures and offline package startup.
They do not prove live Cursor/Antigravity requests, real credentials or proxies,
interactive terminal pixels, or every newer host's behavior. Version selection
does not upgrade a user's installed Pi or alter their configuration.

Local proposal validation on 2026-10-09 used the source above plus this change,
macOS arm64, Node 22.23.1, Bun 1.3.14, and isolated Pi configuration. Exact
0.99.1 and 1.1.0 hosts each passed `bun run check`, the complete Node/UI suites,
and all four offline discovery/packed/source smoke steps. These local results
do not replace the Linux/Windows/E2E matrix or maintainer adoption.

## Alternatives considered

- **Frozen lock only:** preserves reproducibility but repeats the blind spot from
#328 and loses minimum-version evidence when the lock advances.
- **Raise the minimum to every new host:** removes useful compatibility without
proving that the old minimum stopped working; retain it until a real dependency
or native contract requires a reviewed increase.
- **Immediately cap the peer range:** changes the published support contract and
can exclude already-working hosts. A bounded range remains available when
concrete incompatibility evidence warrants it; this change preserves the range.
- **A custom runtime rejection layer:** would duplicate host ownership and would
not replace compatibility tests.

## Consequences

Two additional Linux jobs verify host contracts without multiplying the entire
OS/Node matrix. Latest-host failures may be caused by an upstream release even
when OpenPI source is unchanged; the exact selected version makes this visible.
The development lock remains reproducible and package installation remains
Pi-native. This is a compatibility policy, not a guarantee about future releases.

## Amendments

Append adopted range changes and their validation evidence here, or link a
superseding Decision. Historical investigations retain their original boundaries.

2026-10-15 integration note: this record remains **proposed**. The historical
validation above belongs to its named source and hosts. Main `f98d6a47` uses
development `^1.2.0` and peer minimum `>=0.99.1`; endpoint CI does not adopt
this policy or equate development versions with the supported floor.

2026-11-03 integration note: rebased onto main `0addb739`. Renumbered this
proposal to 0006 because main adopted an unrelated Decision 0005. Its status
remains **proposed**; the existing frozen tool surface decision is unchanged.
2 changes: 2 additions & 0 deletions docs/decisions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,5 @@ Start from [`TEMPLATE.md`](TEMPLATE.md).
- [`0004-capability-name-discovery.md`](0004-capability-name-discovery.md) — named delegate/workflow discovery with model-owned execution judgment and unchanged authority ([#655](https://github.com/openpi-dev/openpi/issues/655)). **Superseded** by Decision 0005 ([#879](https://github.com/openpi-dev/openpi/issues/879)).

- [`0005-frozen-tool-surface.md`](0005-frozen-tool-surface.md) — one fixed resident tool set per Session; model-visible tools change only at explicit user boundaries ([#879](https://github.com/openpi-dev/openpi/issues/879)).

- [`0006-supported-pi-versions.md`](0006-supported-pi-versions.md) — proposed minimum/locked/latest host verification and the boundary of the open-ended peer range ([#328](https://github.com/openpi-dev/openpi/issues/328)).
55 changes: 35 additions & 20 deletions extensions/setup/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { StringEnum } from "@earendil-works/pi-ai";
import type {
ExtensionAPI,
ExtensionCommandContext,
Expand Down Expand Up @@ -548,27 +547,30 @@ export default function openPiSetup(pi: ExtensionAPI) {
}),
),
response_verbosity: Type.Optional(
StringEnum(RESPONSE_VERBOSITIES, {
Type.Enum(RESPONSE_VERBOSITIES, {
type: "string",
description:
"Optional output verbosity for OpenAI Responses/Codex Responses: default preserves Pi's request; low, medium, or high overrides only text.verbosity. Unsupported APIs are unchanged. Omit to preserve the saved preference.",
}),
),
response_reasoning_summary: Type.Optional(
StringEnum(RESPONSE_REASONING_SUMMARIES, {
Type.Enum(RESPONSE_REASONING_SUMMARIES, {
type: "string",
description:
"Optional reasoning summary for an existing native reasoning request on OpenAI Responses/Codex Responses. default preserves Pi's request; auto, concise, and detailed override summary; none removes summary without changing effort. Unsupported requests are unchanged. Omit to preserve the saved preference.",
}),
),
web_hidden_thinking_levels: Type.Optional(
Type.Array(StringEnum(REASONING_LEVELS), {
Type.Array(Type.Enum(REASONING_LEVELS, { type: "string" }), {
uniqueItems: true,
maxItems: REASONING_LEVELS.length,
description:
"Web presentation-only hidden reasoning levels. Empty (default) hides none. Replaces the saved list; a hidden current value stays visible. Does not change Pi's supported levels, current effort, or defaults. Omit to preserve.",
}),
),
web_access_action: Type.Optional(
StringEnum(WEB_ACCESS_ACTIONS, {
Type.Enum(WEB_ACCESS_ACTIONS, {
type: "string",
description:
"Reviewed optional pi-web-access package operation. install-exa selects keyless Exa with a minimal new profile; install-existing preserves existing plugin preferences; disable disables its native Pi resources. Native confirmation is required. Use this field alone. Never reload the Session automatically.",
}),
Expand All @@ -586,7 +588,8 @@ export default function openPiSetup(pi: ExtensionAPI) {
Type.String({ description: "Configured Pi model id." }),
),
suggestion_reasoning: Type.Optional(
StringEnum(REASONING_LEVELS, {
Type.Enum(REASONING_LEVELS, {
type: "string",
description: "Reasoning level for the suggestion model.",
}),
),
Expand Down Expand Up @@ -628,19 +631,20 @@ export default function openPiSetup(pi: ExtensionAPI) {
}),
),
ui_web_theme: Type.Optional(
StringEnum(WEB_THEMES, {
Type.Enum(WEB_THEMES, {
type: "string",
description:
"Canonical OpenPI Web theme: system follows the browser/OS color scheme; light, dark, mist, rose, and pine force that appearance. Stored in package setup rather than browser storage. Omit to preserve the current value.",
}),
),
ui_web_appearance: Type.Optional(
Type.Object(
{
palette: StringEnum(WEB_APPEARANCE_PALETTES),
palette: Type.Enum(WEB_APPEARANCE_PALETTES, { type: "string" }),
accent: Type.String({ pattern: "^(?:|#[0-9A-Fa-f]{6})$" }),
background: Type.String({ pattern: "^(?:|#[0-9A-Fa-f]{6})$" }),
foreground: Type.String({ pattern: "^(?:|#[0-9A-Fa-f]{6})$" }),
font: StringEnum(WEB_APPEARANCE_FONTS),
font: Type.Enum(WEB_APPEARANCE_FONTS, { type: "string" }),
},
{
additionalProperties: false,
Expand Down Expand Up @@ -730,25 +734,29 @@ export default function openPiSetup(pi: ExtensionAPI) {
}),
),
ui_web_follow_up_behavior: Type.Optional(
StringEnum(["followUp", "steer"] as const, {
Type.Enum(["followUp", "steer"] as const, {
type: "string",
description:
"Default delivery for messages sent while OpenPI Web is running: followUp queues the next turn (default), steer guides the active turn after its current tool step. This only sets the composer default; it never sends a message. Omit to preserve the current value.",
}),
),
ui_web_pinned_sort: Type.Optional(
StringEnum(["manual", "updated"] as const, {
Type.Enum(["manual", "updated"] as const, {
type: "string",
description:
"OpenPI Web pinned conversation order: manual (default) or recently updated. Omit to preserve the current value.",
}),
),
ui_web_sidebar_grouping: Type.Optional(
StringEnum(["project", "list"] as const, {
Type.Enum(["project", "list"] as const, {
type: "string",
description:
"OpenPI Web sidebar grouping: project (default) or one recent conversation list. Omit to preserve the current value.",
}),
),
ui_web_chats_sort: Type.Optional(
StringEnum(["manual", "updated"] as const, {
Type.Enum(["manual", "updated"] as const, {
type: "string",
description:
"OpenPI Web conversation order within projects and the recent list: recently updated (default) or manual. Omit to preserve the current value.",
}),
Expand All @@ -766,19 +774,23 @@ export default function openPiSetup(pi: ExtensionAPI) {
}),
),
ui_footer_preset: Type.Optional(
StringEnum(FOOTER_PRESETS, {
Type.Enum(FOOTER_PRESETS, {
type: "string",
description: `Convenient footer preset applied first: powerline (one-line ANSI256 blocks), powerline-mono (one-line gray powerline), compact (one-line plain text). ${FOOTER_PRESET_GUIDANCE} Style/lines overrides still win after the preset. Omit to preserve the current layout unless other footer fields are set.`,
}),
),
ui_footer_style: Type.Optional(
StringEnum(FOOTER_STYLES, {
Type.Enum(FOOTER_STYLES, {
type: "string",
description:
"Footer visual style: plain (Pi theme separators), powerline (ANSI256 colored blocks with  seams), powerline-mono (high-contrast gray powerline). A Nerd Font renders Codicon metric glyphs and powerline seams as designed; text stays readable without it. Omit to preserve the current style (or the preset's style when a preset is applied).",
}),
),
ui_footer_lines: Type.Optional(
Type.Array(
Type.Array(StringEnum(FOOTER_LAYOUT_ITEMS), { minItems: 1 }),
Type.Array(Type.Enum(FOOTER_LAYOUT_ITEMS, { type: "string" }), {
minItems: 1,
}),
{
minItems: 1,
description:
Expand All @@ -787,27 +799,30 @@ export default function openPiSetup(pi: ExtensionAPI) {
),
),
ui_footer_items: Type.Optional(
Type.Array(StringEnum(FOOTER_ITEMS), {
Type.Array(Type.Enum(FOOTER_ITEMS, { type: "string" }), {
minItems: 1,
uniqueItems: true,
description:
"Legacy flat footer metric selection. Mapped onto the default one-line skeleton (metrics not listed are hidden). Prefer ui_footer_lines for custom multi-line layouts. Cannot be combined with ui_footer_lines. Operational activity remains visible whenever the custom footer is enabled. Omit to preserve the current selection.",
}),
),
subagent_result_display: Type.Optional(
StringEnum(DETAIL_DISPLAYS, {
Type.Enum(DETAIL_DISPLAYS, {
type: "string",
description:
"How completed Subagent results render by default in Pi and Web: full shows complete output; compact shows bounded status rows, with disclosure controls revealing the report. Omit to preserve the current value.",
}),
),
bash_tool_display: Type.Optional(
StringEnum(DETAIL_DISPLAYS, {
Type.Enum(DETAIL_DISPLAYS, {
type: "string",
description:
"How Bash commands and output render by default in Pi and Web: compact shows a semantic activity row with execution state; disclosure controls reveal command, output and evidence. Full expands the evidence and enclosing Web execution group by default. Omit to preserve the current value.",
}),
),
file_mutation_display: Type.Optional(
StringEnum(DETAIL_DISPLAYS, {
Type.Enum(DETAIL_DISPLAYS, {
type: "string",
description:
"How Write/Edit content and diffs render by default in Pi and Web: compact shows path, status and change counts; disclosure controls reveal native preview, output, error and diff. Full expands the evidence and enclosing Web execution group by default. Omit to preserve the current value.",
}),
Expand Down
15 changes: 15 additions & 0 deletions extensions/shared/reasoning-levels.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { ModelThinkingLevel } from "@earendil-works/pi-ai";

// Browser-safe projection of the native levels, including hosts that only
// export their type and do not provide providers/model-schema at runtime.
export const REASONING_LEVELS = [
"off",
"minimal",
"low",
"medium",
"high",
"xhigh",
"max",
] as const satisfies readonly ModelThinkingLevel[];

export type ReasoningLevel = (typeof REASONING_LEVELS)[number];
13 changes: 2 additions & 11 deletions extensions/shared/setup-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,9 @@ import {
type SubagentRoleModel,
type SubagentRoleModels,
} from "./subagent-roles.ts";
import { REASONING_LEVELS, type ReasoningLevel } from "./reasoning-levels.ts";

export const REASONING_LEVELS = [
"off",
"minimal",
"low",
"medium",
"high",
"xhigh",
"max",
] as const;

export type ReasoningLevel = (typeof REASONING_LEVELS)[number];
export { REASONING_LEVELS, type ReasoningLevel };

export const RESPONSE_VERBOSITIES = [
"default",
Expand Down
Loading
Loading