From b64961a02a1765c9abed1f58c921ece888efeb03 Mon Sep 17 00:00:00 2001 From: akougkas Date: Sat, 25 Apr 2026 07:08:03 -0500 Subject: [PATCH 1/5] fix(build): chmod node-pty darwin spawn-helper on postinstall node-pty 1.1.0's published tarball ships prebuilds/darwin-{arm64,x64}/spawn-helper without the executable bit (0o644). The first time the test pty harness called pty.spawn() on macOS, posix_spawnp from the native binding refused the helper and returned "posix_spawnp failed". Linux dodged this because no Linux prebuild ships; node-gyp recompiled the helper on install and the compiler emits an executable. Windows uses winpty/conpty. The postinstall hook chmods the helpers to 0o755. Idempotent and defensive: missing prebuilds are skipped, chmod failures are swallowed so postinstall can never fail an install. End users do not call pty.spawn anywhere in clio's runtime; this only affects test harness usage on macOS, but shipping it as a real postinstall keeps `npm install` and `npm ci` honest on every platform. --- package.json | 1 + scripts/fix-node-pty-permissions.mjs | 39 ++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 scripts/fix-node-pty-permissions.mjs diff --git a/package.json b/package.json index 4a8325c..0b04edd 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "test:e2e": "npm run build && node --import tsx --test 'tests/e2e/**/*.test.ts'", "ci": "npm run typecheck && npm run lint && npm run test && npm run build && npm run test:e2e", "hooks:install": "bash scripts/install-hooks.sh", + "postinstall": "node scripts/fix-node-pty-permissions.mjs", "prepublishOnly": "npm run typecheck && npm run lint && npm run build && node scripts/check-dist.mjs" }, "dependencies": { diff --git a/scripts/fix-node-pty-permissions.mjs b/scripts/fix-node-pty-permissions.mjs new file mode 100644 index 0000000..96be1c0 --- /dev/null +++ b/scripts/fix-node-pty-permissions.mjs @@ -0,0 +1,39 @@ +#!/usr/bin/env node +// node-pty 1.1.0's published tarball ships prebuilds/-/spawn-helper +// without the executable bit on macOS targets. Without exec, posix_spawnp from +// the native binding fails with "posix_spawnp failed" the first time the pty +// harness tries to fork a subprocess. Linux dodges this because no Linux +// prebuild exists; node-gyp recompiles the helper, and the compiler emits an +// executable. Windows uses winpty/conpty and never touches spawn-helper. +// +// Idempotent and defensive: we never let postinstall fail. +import { chmodSync, existsSync } from "node:fs"; +import { createRequire } from "node:module"; +import { dirname, join } from "node:path"; + +const require = createRequire(import.meta.url); + +let ptyDir; +try { + ptyDir = dirname(require.resolve("node-pty/package.json")); +} catch { + process.exit(0); +} + +const candidates = [ + ["darwin", "arm64"], + ["darwin", "x64"], + ["linux", "x64"], + ["linux", "arm64"], +]; + +for (const [platform, arch] of candidates) { + const helper = join(ptyDir, "prebuilds", `${platform}-${arch}`, "spawn-helper"); + if (existsSync(helper)) { + try { + chmodSync(helper, 0o755); + } catch { + // ignore; postinstall must not fail. + } + } +} From a7b635f80ba6b81aa953f89e6f0247152dd22049 Mon Sep 17 00:00:00 2001 From: akougkas Date: Sat, 25 Apr 2026 07:11:25 -0500 Subject: [PATCH 2/5] ci: collapse to a single workflow, deactivate dependabot Early-stage heavy dev does not need a 3-job split, a separate cross-platform workflow with auto-issue creation, or a dependabot that opens a PR every time someone pushes a patch upstream. Drop: - .github/workflows/ci-cross-platform.yml (macOS post-merge + nightly) - .github/dependabot.yml (the whole config; revisit when the surface is stable enough to want passive update PRs) Reshape ci.yml to one job that runs typecheck + lint + test + build + e2e on Ubuntu / Node 24. Same coverage as before, single check, no theater. Branch-protection contexts collapse to just "ci" to match. --- .github/branch-protection-main.json | 2 +- .github/dependabot.yml | 107 ------------------------ .github/workflows/ci-cross-platform.yml | 49 ----------- .github/workflows/ci.yml | 42 +--------- 4 files changed, 3 insertions(+), 197 deletions(-) delete mode 100644 .github/dependabot.yml delete mode 100644 .github/workflows/ci-cross-platform.yml diff --git a/.github/branch-protection-main.json b/.github/branch-protection-main.json index 2a8320c..fc0d977 100644 --- a/.github/branch-protection-main.json +++ b/.github/branch-protection-main.json @@ -1,7 +1,7 @@ { "required_status_checks": { "strict": true, - "contexts": ["ci (ubuntu-latest)", "ci (macos-14)"] + "contexts": ["ci"] }, "enforce_admins": true, "required_pull_request_reviews": { diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 715a283..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,107 +0,0 @@ -version: 2 -updates: - - package-ecosystem: npm - directory: / - schedule: - interval: weekly - day: monday - time: "09:00" - timezone: America/Chicago - open-pull-requests-limit: 3 - labels: - - dependencies - commit-message: - prefix: build - include: scope - ignore: - # Pi-mono is the vendored engine. The release line is the meaningful - # boundary (0.69.x -> 0.70.x), so alert on minor and major. Patches on - # the same release line are noise we absorb in periodic manual sweeps. - - dependency-name: "@mariozechner/pi-agent-core" - update-types: ["version-update:semver-patch"] - - dependency-name: "@mariozechner/pi-ai" - update-types: ["version-update:semver-patch"] - - dependency-name: "@mariozechner/pi-tui" - update-types: ["version-update:semver-patch"] - # @types/node tracks the CI Node version. Bumped manually when the CI - # runner moves; dependabot stays out of it entirely. - - dependency-name: "@types/node" - # Every other dep: alert on major bumps only. Patches and minors are - # absorbed in periodic proactive sweeps so dependabot stays quiet. - - dependency-name: "@anthropic-ai/claude-agent-sdk" - update-types: ["version-update:semver-patch", "version-update:semver-minor"] - - dependency-name: "@biomejs/biome" - update-types: ["version-update:semver-patch", "version-update:semver-minor"] - - dependency-name: "@lmstudio/sdk" - update-types: ["version-update:semver-patch", "version-update:semver-minor"] - - dependency-name: "@types/diff" - update-types: ["version-update:semver-patch", "version-update:semver-minor"] - - dependency-name: "chalk" - update-types: ["version-update:semver-patch", "version-update:semver-minor"] - - dependency-name: "diff" - update-types: ["version-update:semver-patch", "version-update:semver-minor"] - - dependency-name: "esbuild" - update-types: ["version-update:semver-patch", "version-update:semver-minor"] - - dependency-name: "node-pty" - update-types: ["version-update:semver-patch", "version-update:semver-minor"] - - dependency-name: "ollama" - update-types: ["version-update:semver-patch", "version-update:semver-minor"] - - dependency-name: "tsup" - update-types: ["version-update:semver-patch", "version-update:semver-minor"] - - dependency-name: "tsx" - update-types: ["version-update:semver-patch", "version-update:semver-minor"] - - dependency-name: "typebox" - update-types: ["version-update:semver-patch", "version-update:semver-minor"] - - dependency-name: "typescript" - update-types: ["version-update:semver-patch", "version-update:semver-minor"] - - dependency-name: "undici" - update-types: ["version-update:semver-patch", "version-update:semver-minor"] - - dependency-name: "uuid" - update-types: ["version-update:semver-patch", "version-update:semver-minor"] - - dependency-name: "yaml" - update-types: ["version-update:semver-patch", "version-update:semver-minor"] - groups: - # Pi-mono minors+majors land in one PR so the boundary review is a single - # batch when the engine moves. - pi-mono: - patterns: - - "@mariozechner/pi-*" - update-types: - - minor - - major - # Other production majors share a PR. - production-majors: - dependency-type: production - update-types: - - major - # Dev-tool majors share a PR. - dev-majors: - dependency-type: development - update-types: - - major - - - package-ecosystem: github-actions - directory: / - schedule: - interval: weekly - day: monday - time: "09:30" - timezone: America/Chicago - open-pull-requests-limit: 1 - labels: - - dependencies - commit-message: - prefix: ci - include: scope - ignore: - # Actions: alert on major only. Patches and minors are absorbed manually. - - dependency-name: "*" - update-types: - - "version-update:semver-patch" - - "version-update:semver-minor" - groups: - github-actions-majors: - patterns: - - "*" - update-types: - - major diff --git a/.github/workflows/ci-cross-platform.yml b/.github/workflows/ci-cross-platform.yml deleted file mode 100644 index b349276..0000000 --- a/.github/workflows/ci-cross-platform.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: ci-cross-platform - -on: - push: - branches: [main] - schedule: - - cron: '0 3 * * *' - workflow_dispatch: - -permissions: - contents: read - issues: write - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: false - -jobs: - ci-macos: - name: ci (macos-14) - runs-on: macos-14 - timeout-minutes: 15 - steps: - - uses: actions/checkout@v6 - with: - persist-credentials: false - - uses: actions/setup-node@v6 - with: - node-version: 24 - cache: npm - - run: npm ci --prefer-offline --no-audit --no-fund - - run: npm run ci - - - name: file or update flake-tracking issue on failure - if: failure() - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - SHA: ${{ github.sha }} - run: | - set -e - existing=$(gh issue list --state open --search 'in:title "ci-cross-platform failure tracking"' --json number --jq '.[0].number') - ts=$(date -u +%Y-%m-%dT%H:%M:%SZ) - body="Failed on \`$SHA\` ($ts): $RUN_URL" - if [ -n "$existing" ]; then - gh issue comment "$existing" --body "$body" - else - gh issue create --title "ci-cross-platform failure tracking" --body "$body" - fi diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fcab399..65fc7a8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,7 +5,6 @@ on: branches: [main] pull_request: branches: [main] - types: [opened, synchronize, reopened, ready_for_review] workflow_dispatch: permissions: @@ -16,45 +15,8 @@ concurrency: cancel-in-progress: true jobs: - lint: - if: github.event.pull_request.draft != true - name: lint + ci: runs-on: ubuntu-latest - timeout-minutes: 5 - steps: - - uses: actions/checkout@v6 - with: - persist-credentials: false - - uses: actions/setup-node@v6 - with: - node-version: 24 - cache: npm - - run: npm ci --prefer-offline --no-audit --no-fund - - run: npm run typecheck - - run: npm run lint - - test: - if: github.event.pull_request.draft != true - name: test - runs-on: ubuntu-latest - needs: lint - timeout-minutes: 5 - steps: - - uses: actions/checkout@v6 - with: - persist-credentials: false - - uses: actions/setup-node@v6 - with: - node-version: 24 - cache: npm - - run: npm ci --prefer-offline --no-audit --no-fund - - run: npm run test - - e2e: - if: github.event.pull_request.draft != true - name: e2e - runs-on: ubuntu-latest - needs: test timeout-minutes: 10 steps: - uses: actions/checkout@v6 @@ -65,4 +27,4 @@ jobs: node-version: 24 cache: npm - run: npm ci --prefer-offline --no-audit --no-fund - - run: npm run test:e2e + - run: npm run ci From 2d35fb7bf709d902c87da59b55a1cf66695a488a Mon Sep 17 00:00:00 2001 From: akougkas Date: Sat, 25 Apr 2026 07:14:39 -0500 Subject: [PATCH 3/5] build(deps): pin every dependency to an exact version Caret ranges meant npm pulled fresh patches on every install and dependabot proposed PRs for the same. With dependabot deactivated and the project under heavy iteration, we want reproducible installs: two contributors running `npm ci` two months apart get identical trees. All three pi-mono packages aligned at 0.70.2 (the user-merged bump in #18 left @mariozechner/pi-agent-core ahead of pi-ai and pi-tui). Full local CI clean against the matched 0.70.2 set. --- package-lock.json | 77 ++++++++++++++--------------------------------- package.json | 34 ++++++++++----------- 2 files changed, 39 insertions(+), 72 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6cc31f2..57ea12f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,33 +7,34 @@ "": { "name": "@iowarp/clio-coder", "version": "0.1.2", + "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@anthropic-ai/claude-agent-sdk": "^0.2.120", + "@anthropic-ai/claude-agent-sdk": "0.2.120", "@lmstudio/sdk": "1.5.0", "@mariozechner/pi-agent-core": "0.70.2", - "@mariozechner/pi-ai": "0.69.0", - "@mariozechner/pi-tui": "0.69.0", - "chalk": "^5.3.0", - "diff": "^9.0.0", - "esbuild": "^0.28.0", + "@mariozechner/pi-ai": "0.70.2", + "@mariozechner/pi-tui": "0.70.2", + "chalk": "5.6.2", + "diff": "9.0.0", + "esbuild": "0.28.0", "ollama": "0.6.3", - "typebox": "^1.1.33", - "undici": "^8.1.0", - "uuid": "^14.0.0", - "yaml": "^2.6.0" + "typebox": "1.1.33", + "undici": "8.1.0", + "uuid": "14.0.0", + "yaml": "2.8.3" }, "bin": { "clio": "dist/cli/index.js" }, "devDependencies": { - "@biomejs/biome": "^2.4.13", - "@types/diff": "^8.0.0", - "@types/node": "^24.12.2", - "node-pty": "^1.1.0", - "tsup": "^8.3.0", - "tsx": "^4.19.0", - "typescript": "^6.0.3" + "@biomejs/biome": "2.4.13", + "@types/diff": "8.0.0", + "@types/node": "24.12.2", + "node-pty": "1.1.0", + "tsup": "8.5.1", + "tsx": "4.21.0", + "typescript": "6.0.3" }, "engines": { "node": ">=22.0.0" @@ -1682,7 +1683,7 @@ "node": ">=20.0.0" } }, - "node_modules/@mariozechner/pi-agent-core/node_modules/@mariozechner/pi-ai": { + "node_modules/@mariozechner/pi-ai": { "version": "0.70.2", "resolved": "https://registry.npmjs.org/@mariozechner/pi-ai/-/pi-ai-0.70.2.tgz", "integrity": "sha512-+30LRPjXsXF+oI96DvGWMbdPGeqoLJvadh6UPev7wx2DzhC9FEqXkQcoMZ0usbCm7E9pl8ua8a9s/pQ5ikaUbg==", @@ -1707,40 +1708,6 @@ "node": ">=20.0.0" } }, - "node_modules/@mariozechner/pi-agent-core/node_modules/undici": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", - "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", - "license": "MIT", - "engines": { - "node": ">=20.18.1" - } - }, - "node_modules/@mariozechner/pi-ai": { - "version": "0.69.0", - "resolved": "https://registry.npmjs.org/@mariozechner/pi-ai/-/pi-ai-0.69.0.tgz", - "integrity": "sha512-bl838sr57zx/apkiTeNPFQgbBkGXN4HHhm2oghzSRohJIBrR+H001bTeco7Osuke+EZTxO3V5Aev2qlZdUa2xw==", - "license": "MIT", - "dependencies": { - "@anthropic-ai/sdk": "^0.90.0", - "@aws-sdk/client-bedrock-runtime": "^3.1030.0", - "@google/genai": "^1.40.0", - "@mistralai/mistralai": "^2.2.0", - "chalk": "^5.6.2", - "openai": "6.26.0", - "partial-json": "^0.1.7", - "proxy-agent": "^6.5.0", - "typebox": "^1.1.24", - "undici": "^7.19.1", - "zod-to-json-schema": "^3.24.6" - }, - "bin": { - "pi-ai": "dist/cli.js" - }, - "engines": { - "node": ">=20.0.0" - } - }, "node_modules/@mariozechner/pi-ai/node_modules/undici": { "version": "7.25.0", "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", @@ -1751,9 +1718,9 @@ } }, "node_modules/@mariozechner/pi-tui": { - "version": "0.69.0", - "resolved": "https://registry.npmjs.org/@mariozechner/pi-tui/-/pi-tui-0.69.0.tgz", - "integrity": "sha512-/82SOMzCA1srivwfcSO8r9XLdAgJNcgpWzNZY33IqqvGev8Q4iWY/hA1wAkLB0l7PKJJ4Dh57xS4IGHbyVM7lA==", + "version": "0.70.2", + "resolved": "https://registry.npmjs.org/@mariozechner/pi-tui/-/pi-tui-0.70.2.tgz", + "integrity": "sha512-PtKC0NepnrYcqMx6MXkWTrBzC9tI62KeC6w940oT46lCbfvgmfqXciR15+9BZpxxc1H4jd3CMrKsmOPVeUqZ0A==", "license": "MIT", "dependencies": { "@types/mime-types": "^2.1.4", diff --git a/package.json b/package.json index 0b04edd..5882b36 100644 --- a/package.json +++ b/package.json @@ -73,27 +73,27 @@ "prepublishOnly": "npm run typecheck && npm run lint && npm run build && node scripts/check-dist.mjs" }, "dependencies": { - "@anthropic-ai/claude-agent-sdk": "^0.2.120", + "@anthropic-ai/claude-agent-sdk": "0.2.120", "@lmstudio/sdk": "1.5.0", "@mariozechner/pi-agent-core": "0.70.2", - "@mariozechner/pi-ai": "0.69.0", - "@mariozechner/pi-tui": "0.69.0", - "chalk": "^5.3.0", - "diff": "^9.0.0", - "esbuild": "^0.28.0", + "@mariozechner/pi-ai": "0.70.2", + "@mariozechner/pi-tui": "0.70.2", + "chalk": "5.6.2", + "diff": "9.0.0", + "esbuild": "0.28.0", "ollama": "0.6.3", - "typebox": "^1.1.33", - "undici": "^8.1.0", - "uuid": "^14.0.0", - "yaml": "^2.6.0" + "typebox": "1.1.33", + "undici": "8.1.0", + "uuid": "14.0.0", + "yaml": "2.8.3" }, "devDependencies": { - "@biomejs/biome": "^2.4.13", - "@types/diff": "^8.0.0", - "@types/node": "^24.12.2", - "node-pty": "^1.1.0", - "tsup": "^8.3.0", - "tsx": "^4.19.0", - "typescript": "^6.0.3" + "@biomejs/biome": "2.4.13", + "@types/diff": "8.0.0", + "@types/node": "24.12.2", + "node-pty": "1.1.0", + "tsup": "8.5.1", + "tsx": "4.21.0", + "typescript": "6.0.3" } } From 467c5a1f1704c502823e92d6ee2de4abb834385c Mon Sep 17 00:00:00 2001 From: akougkas Date: Sat, 25 Apr 2026 07:19:01 -0500 Subject: [PATCH 4/5] test(bash): widen abort timing to absorb cold-runner shell startup bashTool spawns `/bin/bash -lc`. On a cold CI runner /etc/profile takes long enough to read that the SIGTERM from a 250ms abort can land before the inline `trap "" TERM` is installed, killing the shell at the default disposition. The test then sees ~250ms total elapsed and fails the >=5000ms assertion even though the production escalation path is fine. Move the abort to 1500ms (well past the slowest profile read we see) and assert the >=4500ms grace window relative to the abort instead of the test start. Same coverage, no flake. --- tests/unit/bash-tool-env.test.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/tests/unit/bash-tool-env.test.ts b/tests/unit/bash-tool-env.test.ts index bb644a6..b954d37 100644 --- a/tests/unit/bash-tool-env.test.ts +++ b/tests/unit/bash-tool-env.test.ts @@ -83,22 +83,30 @@ describe("bash tool environment", () => { it("escalates aborted commands that ignore sigterm", async () => { const controller = new AbortController(); + // The shell runs with `-l`, which on cold CI runners can spend a few + // hundred ms reading /etc/profile before reaching the user command. If + // SIGTERM arrives before the trap is installed, bash dies at the + // default disposition and the escalation path never runs. Wait long + // enough for the trap to take effect on the slowest runners we see. const startedAt = Date.now(); const started = bashTool.run( { - command: 'trap "" TERM; end=$((SECONDS + 9)); while [ "$SECONDS" -lt "$end" ]; do sleep 1; done', - timeout_ms: 12_000, + command: 'trap "" TERM; end=$((SECONDS + 12)); while [ "$SECONDS" -lt "$end" ]; do sleep 1; done', + timeout_ms: 16_000, }, { signal: controller.signal }, ); - setTimeout(() => controller.abort(), 250); + setTimeout(() => controller.abort(), 1500); const result = await started; const elapsedMs = Date.now() - startedAt; strictEqual(result.kind, "error"); if (result.kind === "error") strictEqual(result.message, "bash: command aborted"); - ok(elapsedMs >= 5000, `expected abort escalation after the grace period, got ${elapsedMs}ms`); - ok(elapsedMs < 8000, `expected abort escalation within 8s, got ${elapsedMs}ms`); + ok( + elapsedMs >= 1500 + 4_500, + `expected abort escalation after the grace period, got ${elapsedMs}ms`, + ); + ok(elapsedMs < 1500 + 9_000, `expected abort escalation within window, got ${elapsedMs}ms`); }); it("reports output cap exits explicitly", async () => { From a9a21cb62e4bf987d503a960ed89a953ae32a166 Mon Sep 17 00:00:00 2001 From: akougkas Date: Sat, 25 Apr 2026 07:21:03 -0500 Subject: [PATCH 5/5] ci: bump biome schema URL to 2.4.13, reflow test for biome formatter --- biome.json | 2 +- tests/unit/bash-tool-env.test.ts | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/biome.json b/biome.json index f8c19cc..bfe0086 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.4.12/schema.json", + "$schema": "https://biomejs.dev/schemas/2.4.13/schema.json", "assist": { "actions": { "source": { "organizeImports": "on" } } }, "formatter": { "enabled": true, diff --git a/tests/unit/bash-tool-env.test.ts b/tests/unit/bash-tool-env.test.ts index b954d37..b6cbf1e 100644 --- a/tests/unit/bash-tool-env.test.ts +++ b/tests/unit/bash-tool-env.test.ts @@ -102,10 +102,7 @@ describe("bash tool environment", () => { strictEqual(result.kind, "error"); if (result.kind === "error") strictEqual(result.message, "bash: command aborted"); - ok( - elapsedMs >= 1500 + 4_500, - `expected abort escalation after the grace period, got ${elapsedMs}ms`, - ); + ok(elapsedMs >= 1500 + 4_500, `expected abort escalation after the grace period, got ${elapsedMs}ms`); ok(elapsedMs < 1500 + 9_000, `expected abort escalation within window, got ${elapsedMs}ms`); });