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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Changed

- Loosened the `pi_bridge` Mix project (and the bundled `packages/fixtures/demo_project` fixture) `elixir:` requirement from `~> 1.20` to `~> 1.16` so legacy projects on Elixir 1.16–1.19 can adopt the bridge. New-project guidance and startup notices still recommend Elixir 1.20+ / OTP 27+ for the compiler's set-theoretic type-system improvements. The bridge now depends on `json_codec ~> 0.1.5`, which carries the same loosened Elixir requirement.

## 0.6.21 - 2026-06-14

### Changed
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,8 @@ Feature flags are escape hatches for noisy, sensitive, or experimental environme

## Recommended project stack

For new projects, install Elixir 1.20+ with OTP 27+ when possible. Elixir 1.20 introduced compiler type-system improvements, including gradual set-theoretic types, whole-body type inference, occurrence typing, and richer map typing; pi-elixir still supports older Elixir releases for existing legacy projects.

For new web applications, use Phoenix with Igniter and VibeKit, then add pi-elixir in the project:

```sh
Expand Down
4 changes: 2 additions & 2 deletions packages/bridge/mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ defmodule PiBridge.MixProject do
[
app: :pi_bridge,
version: "0.6.21",
elixir: "~> 1.20",
elixir: "~> 1.16",
start_permanent: Mix.env() == :prod,
description: "BEAM runtime bridge for pi development agents",
package: package(),
Expand Down Expand Up @@ -60,7 +60,7 @@ defmodule PiBridge.MixProject do
defp deps do
[
{:jason, "~> 1.4"},
{:json_codec, "~> 0.1.3"},
{:json_codec, "~> 0.1.5"},
{:ex_ast, "~> 0.12"},
{:req, "~> 0.5"},
{:quackdb, "~> 0.5.4"},
Expand Down
2 changes: 1 addition & 1 deletion packages/bridge/mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
"idna": {:hex, :idna, "7.1.0", "1067a13043538129602d2f2ce6899d8713125c7d19734aa557ce2e3ea55bd4f1", [:rebar3], [], "hexpm", "6ae959a025bf36df61a8cab8508d9654891b5426a84c44d82deaffd6ddf8c71f"},
"jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"},
"json_codec": {:hex, :json_codec, "0.1.3", "e4d8bdc8434da7f60cd519455d5b0ce32128fa05dd4049f3c1020cd0eaff4deb", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "3ed072d68de3df45e4d2220db266bf550eaee057e45e544d4ae140bf5726475e"},
"json_codec": {:hex, :json_codec, "0.1.5", "44532ea8604f3587d0e8b0b33002745ba068c9cb1663d73c1eee49a5e7cd7e78", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "6631dd3ce56a567e57ccb7a1129f57bb9206663fbb644c6ee47acdba36933f73"},
"jsv": {:hex, :jsv, "0.19.4", "92cb229f65fe9fb3007128c1dc2e0d901d6d517d959fdbd3f07f1c74658ee5d3", [:mix], [{:abnf_parsec, "~> 2.0", [hex: :abnf_parsec, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:idna, "~> 6.0 or ~> 7.0", [hex: :idna, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:texture, "~> 1.0", [hex: :texture, repo: "hexpm", optional: false]}], "hexpm", "3ff30c9f2413364487b03fb8a8434729debe9fe7cfb3d45d04b6c97390389ec4"},
"libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"},
"llm_db": {:hex, :llm_db, "2026.5.2", "bec38e9489d552464e76158442b49aa7d798040cf138f803551bc6b037508210", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:dotenvy, "~> 1.1", [hex: :dotenvy, repo: "hexpm", optional: false]}, {:igniter, "~> 0.7", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:toml, "~> 0.7", [hex: :toml, repo: "hexpm", optional: false]}, {:zoi, "~> 0.10", [hex: :zoi, repo: "hexpm", optional: false]}], "hexpm", "8e4eee268d6682320ef8d087b270dfbfdf6db022c2392971e506e0d4d59e6dba"},
Expand Down
2 changes: 2 additions & 0 deletions packages/extension/skills/elixir/new-project/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ description: Start or bootstrap a new Elixir project/package using Igniter and V

Use this skill when creating or bootstrapping a new Elixir package/project.

For new projects, recommend Elixir 1.20+ with OTP 27+ when possible. Elixir 1.20 introduced compiler type-system improvements, including gradual set-theoretic types, whole-body type inference, occurrence typing, and richer map typing. `pi_bridge` supports Elixir 1.16+ for existing legacy projects, but do not present 1.16–1.19 as the preferred baseline for new work.

## Default workflow

1. Inspect `/Users/dannote/Development/vibe_kit` for the current installer behavior before starting.
Expand Down
13 changes: 12 additions & 1 deletion packages/extension/src/bridge/startup-info.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { BridgeInfo } from '#src/embedded/stdio-process.ts'
import { detectElixirVersion, shouldRecommendElixir120 } from '#src/mix/runtime.ts'
import { EXTENSION_VERSION } from '#src/version.ts'
import type { ExtensionContext } from '@earendil-works/pi-coding-agent'

Expand All @@ -15,7 +16,17 @@ export function renderStartupInfo(info: BridgeInfo) {
return lines.join('\n')
}

export function showStartupInfo(ctx: ExtensionContext, info: BridgeInfo | undefined) {
export function startupElixirVersionRecommendation(cwd: string): string | null {
const version = detectElixirVersion(cwd)
if (!shouldRecommendElixir120(version)) return null

return `${version?.raw ?? 'Current Elixir'} detected. pi_bridge supports Elixir 1.16+ for legacy projects, but Elixir 1.20+ with OTP 27+ is recommended for new projects because it adds the compiler set-theoretic type system, whole-body type inference, occurrence typing, and richer map typing.`
}

export function showStartupInfo(ctx: ExtensionContext, info: BridgeInfo | undefined, cwd: string) {
const recommendation = startupElixirVersionRecommendation(cwd)
if (recommendation) ctx.ui.notify(recommendation, 'warning')

if (!info) return
ctx.ui.notify(renderStartupInfo(info), 'info')
}
2 changes: 1 addition & 1 deletion packages/extension/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ export default function (pi: ExtensionAPI) {
reportConnectionNotice(key, sessionCwd, kind)
if (conn) await loadSessionSnapshots(ctx, sessionCwd, conn.url)
const info = getBridgeInfo(sessionCwd)
showStartupInfo(ctx, info)
showStartupInfo(ctx, info, sessionCwd)
if (flags.plugins()) registerBridgeCommands(pi, info, registeredCommands, resolveElixirCwd)
if (flags.plugins())
await sendBridgeEvent(sessionCwd, {
Expand Down
39 changes: 39 additions & 0 deletions packages/extension/src/mix/runtime.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import * as childProcess from 'node:child_process'

export interface ElixirVersion {
major: number
minor: number
patch: number | null
raw: string
}

function commandExists(command: string): boolean {
const result = childProcess.spawnSync(command, ['--version'], {
stdio: 'ignore',
Expand All @@ -9,6 +16,38 @@ function commandExists(command: string): boolean {
return result?.status === 0
}

export function detectElixirVersion(cwd = process.cwd()): ElixirVersion | null {
const result = childProcess.spawnSync('elixir', ['--version'], {
cwd,
encoding: 'utf8',
timeout: 3_000
})

if (result.status !== 0) return null

const output = [result.stdout, result.stderr]
.filter((value): value is string => typeof value === 'string')
.join('\n')
const raw = output
.split('\n')
.map((line) => line.trim())
.find((line) => line.startsWith('Elixir '))
const match = raw?.match(/^Elixir\s+(\d+)\.(\d+)(?:\.(\d+))?/)
if (!raw || !match) return null

return {
major: Number(match[1]),
minor: Number(match[2]),
patch: match[3] ? Number(match[3]) : null,
raw
}
}

export function shouldRecommendElixir120(version: ElixirVersion | null): boolean {
if (!version) return false
return version.major < 1 || (version.major === 1 && version.minor < 20)
}

export function elixirRuntimeProblem(): string | null {
if (!commandExists('elixir')) {
return 'Elixir is not installed or not available on PATH. Install Elixir/OTP before using pi-elixir BEAM tools.'
Expand Down
25 changes: 24 additions & 1 deletion packages/extension/test/mix/runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ vi.mock('node:child_process', () => ({

import * as childProcess from 'node:child_process'

import { elixirRuntimeProblem } from '#src/mix/runtime.ts'
import {
detectElixirVersion,
elixirRuntimeProblem,
shouldRecommendElixir120
} from '#src/mix/runtime.ts'

describe('elixirRuntimeProblem', () => {
beforeEach(() => {
Expand Down Expand Up @@ -36,4 +40,23 @@ describe('elixirRuntimeProblem', () => {

expect(elixirRuntimeProblem()).toContain('Mix is not available')
})

it('detects Elixir versions for startup recommendations', () => {
vi.mocked(childProcess.spawnSync).mockReturnValue({
status: 0,
stdout: 'Erlang/OTP 27 [erts-15.0]\nElixir 1.19.3\n',
stderr: ''
} as childProcess.SpawnSyncReturns<Buffer>)

const version = detectElixirVersion('/tmp/project')

expect(version).toMatchObject({ major: 1, minor: 19, patch: 3, raw: 'Elixir 1.19.3' })
expect(shouldRecommendElixir120(version)).toBe(true)
})

it('does not recommend upgrades for Elixir 1.20+', () => {
expect(shouldRecommendElixir120({ major: 1, minor: 20, patch: 0, raw: 'Elixir 1.20.0' })).toBe(
false
)
})
})
2 changes: 1 addition & 1 deletion packages/fixtures/demo_project/mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ defmodule PiDemoProject.MixProject do
[
app: :pi_demo_project,
version: "0.1.0",
elixir: "~> 1.20",
elixir: "~> 1.16",
start_permanent: Mix.env() == :prod,
deps: deps()
]
Expand Down
2 changes: 1 addition & 1 deletion packages/fixtures/demo_project/mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"finch": {:hex, :finch, "0.22.0", "5c48fa6f9706a78eb9036cacb67b8b996b4e66d111c543f4c29bb0f879a6806b", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.8", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b94e83c47780fc6813f746a1f1a34ee65cda42da4c5ea26a68f0acc4498e23dc"},
"hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
"jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"},
"json_codec": {:hex, :json_codec, "0.1.3", "e4d8bdc8434da7f60cd519455d5b0ce32128fa05dd4049f3c1020cd0eaff4deb", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "3ed072d68de3df45e4d2220db266bf550eaee057e45e544d4ae140bf5726475e"},
"json_codec": {:hex, :json_codec, "0.1.5", "44532ea8604f3587d0e8b0b33002745ba068c9cb1663d73c1eee49a5e7cd7e78", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "6631dd3ce56a567e57ccb7a1129f57bb9206663fbb644c6ee47acdba36933f73"},
"libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"},
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
"mint": {:hex, :mint, "1.9.0", "d6f534c2a3e98b2a8cc749b4796eb77e9e3af79a76f96e4c74035a827de0d318", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "007154c7d8c43916aed3c93afd1f11aebbaa9c5ff4b7ba55ebe0d17ee0296042"},
Expand Down