diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index ea2b9844d..6dad2932a 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -2,7 +2,7 @@ Closes # diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index 85c2d4799..5f34ec2fc 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -20,7 +20,7 @@ jobs: if: ${{ github.event.workflow_run.conclusion == 'success' }} runs-on: blacksmith-4vcpu-ubuntu-2404 container: - image: ghcr.io/gsd-build/gsd-ci-builder:latest + image: ghcr.io/singularity-forge/sf-ci-builder:latest credentials: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} @@ -146,11 +146,11 @@ jobs: DEV_VERSION: ${{ needs.dev-publish.outputs.dev-version }} run: | docker build --target runtime \ - -t "ghcr.io/gsd-build/gsd-pi:next" \ - -t "ghcr.io/gsd-build/gsd-pi:${DEV_VERSION}" \ + -t "ghcr.io/singularity-forge/sf-run:next" \ + -t "ghcr.io/singularity-forge/sf-run:${DEV_VERSION}" \ . - docker push ghcr.io/gsd-build/gsd-pi:next - docker push "ghcr.io/gsd-build/gsd-pi:${DEV_VERSION}" + docker push ghcr.io/singularity-forge/sf-run:next + docker push "ghcr.io/singularity-forge/sf-run:${DEV_VERSION}" prod-release: name: Production Release @@ -281,9 +281,9 @@ jobs: env: DEV_VERSION: ${{ needs.dev-publish.outputs.dev-version }} run: | - docker pull "ghcr.io/gsd-build/gsd-pi:${DEV_VERSION}" - docker tag "ghcr.io/gsd-build/gsd-pi:${DEV_VERSION}" ghcr.io/gsd-build/gsd-pi:latest - docker push ghcr.io/gsd-build/gsd-pi:latest + docker pull "ghcr.io/singularity-forge/sf-run:${DEV_VERSION}" + docker tag "ghcr.io/singularity-forge/sf-run:${DEV_VERSION}" ghcr.io/singularity-forge/sf-run:latest + docker push ghcr.io/singularity-forge/sf-run:latest update-builder: name: Update CI Builder Image @@ -314,6 +314,6 @@ jobs: if: steps.check.outputs.changed == 'true' run: | docker build --target builder \ - -t ghcr.io/gsd-build/gsd-ci-builder:latest \ + -t ghcr.io/singularity-forge/sf-ci-builder:latest \ . - docker push ghcr.io/gsd-build/gsd-ci-builder:latest + docker push ghcr.io/singularity-forge/sf-ci-builder:latest diff --git a/.plans/issue-672-parallel-milestone-orchestration.md b/.plans/issue-672-parallel-milestone-orchestration.md index 1dc88e601..0dee8b44c 100644 --- a/.plans/issue-672-parallel-milestone-orchestration.md +++ b/.plans/issue-672-parallel-milestone-orchestration.md @@ -1,6 +1,6 @@ # Issue #672: Parallel Milestone Orchestration -**Issue:** https://github.com/gsd-build/gsd-2/issues/672 +**Issue:** https://github.com/singularity-forge/sf-run/issues/672 **Contributor:** @deseltrus (7 merged PRs, proven contributor) **Status:** WIP — foundation modules built, orchestrator core in progress **Default:** `parallel.enabled: false` — opt-in, zero impact to existing users diff --git a/README.md b/README.md index 467623ddb..8aefadb7e 100644 --- a/README.md +++ b/README.md @@ -777,8 +777,8 @@ Use expensive models where quality matters (planning, complex execution) and che ## Star History - - Star History Chart + + Star History Chart --- diff --git a/docs/dev/ADR-004-capability-aware-model-routing.md b/docs/dev/ADR-004-capability-aware-model-routing.md index c2ce3d2d2..610295752 100644 --- a/docs/dev/ADR-004-capability-aware-model-routing.md +++ b/docs/dev/ADR-004-capability-aware-model-routing.md @@ -4,7 +4,7 @@ **Date:** 2026-03-26 **Revised:** 2026-04-03 **Deciders:** Jeremy McSpadden -**Related:** ADR-003 (pipeline simplification), [Issue #2655](https://github.com/gsd-build/gsd-2/issues/2655), `docs/dynamic-model-routing.md` +**Related:** ADR-003 (pipeline simplification), [Issue #2655](https://github.com/singularity-forge/sf-run/issues/2655), `docs/dynamic-model-routing.md` ## Context diff --git a/docs/dev/ADR-005-multi-model-provider-tool-strategy.md b/docs/dev/ADR-005-multi-model-provider-tool-strategy.md index bdf00706a..77e4f2128 100644 --- a/docs/dev/ADR-005-multi-model-provider-tool-strategy.md +++ b/docs/dev/ADR-005-multi-model-provider-tool-strategy.md @@ -3,7 +3,7 @@ **Status:** Accepted **Date:** 2026-03-27 **Deciders:** Jeremy McSpadden -**Related:** ADR-004 (capability-aware model routing), ADR-003 (pipeline simplification), [Issue #2790](https://github.com/gsd-build/gsd-2/issues/2790) +**Related:** ADR-004 (capability-aware model routing), ADR-003 (pipeline simplification), [Issue #2790](https://github.com/singularity-forge/sf-run/issues/2790) ## Context diff --git a/docs/dev/ADR-007-model-catalog-split.md b/docs/dev/ADR-007-model-catalog-split.md index 8ed426add..0545a5e12 100644 --- a/docs/dev/ADR-007-model-catalog-split.md +++ b/docs/dev/ADR-007-model-catalog-split.md @@ -3,7 +3,7 @@ **Status:** Proposed **Date:** 2026-04-03 **Deciders:** Jeremy McSpadden -**Related:** ADR-004 (capability-aware model routing), [ADR-005](https://github.com/gsd-build/gsd-2/issues/2790), [ADR-006](https://github.com/gsd-build/gsd-2/issues/2995), `packages/pi-ai/src/providers/`, `packages/pi-ai/src/models.ts` +**Related:** ADR-004 (capability-aware model routing), [ADR-005](https://github.com/singularity-forge/sf-run/issues/2790), [ADR-006](https://github.com/singularity-forge/sf-run/issues/2995), `packages/pi-ai/src/providers/`, `packages/pi-ai/src/models.ts` ## Context diff --git a/docs/dev/ci-cd-pipeline.md b/docs/dev/ci-cd-pipeline.md index 80410d124..d84af7955 100644 --- a/docs/dev/ci-cd-pipeline.md +++ b/docs/dev/ci-cd-pipeline.md @@ -42,10 +42,10 @@ npx gsd-pi@latest # or just: npx gsd-pi ```bash # Test candidate -docker run --rm -v $(pwd):/workspace ghcr.io/gsd-build/gsd-pi:next --version +docker run --rm -v $(pwd):/workspace ghcr.io/singularity-forge/sf-run:next --version # Stable -docker run --rm -v $(pwd):/workspace ghcr.io/gsd-build/gsd-pi:latest --version +docker run --rm -v $(pwd):/workspace ghcr.io/singularity-forge/sf-run:latest --version ``` ### Checking if a Fix Landed @@ -129,9 +129,9 @@ If a broken version reaches production: npm dist-tag add gsd-pi@ latest # Roll back Docker -docker pull ghcr.io/gsd-build/gsd-pi: -docker tag ghcr.io/gsd-build/gsd-pi: ghcr.io/gsd-build/gsd-pi:latest -docker push ghcr.io/gsd-build/gsd-pi:latest +docker pull ghcr.io/singularity-forge/sf-run: +docker tag ghcr.io/singularity-forge/sf-run: ghcr.io/singularity-forge/sf-run:latest +docker push ghcr.io/singularity-forge/sf-run:latest ``` For `@dev` or `@next` rollbacks, the next successful merge will overwrite the tag automatically. @@ -153,8 +153,8 @@ For `@dev` or `@next` rollbacks, the next successful merge will overwrite the ta | Image | Base | Purpose | Tags | |-------|------|---------|------| -| `ghcr.io/gsd-build/gsd-ci-builder` | `node:24-bookworm` | CI build environment with Rust toolchain | `:latest`, `:` | -| `ghcr.io/gsd-build/gsd-pi` | `node:24-slim` | User-facing runtime | `:latest`, `:next`, `:v` | +| `ghcr.io/singularity-forge/sf-ci-builder` | `node:24-bookworm` | CI build environment with Rust toolchain | `:latest`, `:` | +| `ghcr.io/singularity-forge/sf-run` | `node:24-slim` | User-facing runtime | `:latest`, `:next`, `:v` | The CI builder image is rebuilt automatically when the `Dockerfile` changes. It eliminates ~3-5 min of toolchain setup per CI run. diff --git a/docs/dev/proposals/698-browser-tools-feature-additions.md b/docs/dev/proposals/698-browser-tools-feature-additions.md index e7bac0e72..57b30fba9 100644 --- a/docs/dev/proposals/698-browser-tools-feature-additions.md +++ b/docs/dev/proposals/698-browser-tools-feature-additions.md @@ -1,6 +1,6 @@ # Browser-Tools Feature Additions — Implementation Requirements -> Ref: [#698](https://github.com/gsd-build/gsd-2/issues/698) +> Ref: [#698](https://github.com/singularity-forge/sf-run/issues/698) > Status: **Shipped** — all 10 features implemented and merged to main ## Current State diff --git a/docs/dev/superpowers/plans/2026-03-17-cicd-pipeline.md b/docs/dev/superpowers/plans/2026-03-17-cicd-pipeline.md index 1e5f1cc56..1bba1e6b4 100644 --- a/docs/dev/superpowers/plans/2026-03-17-cicd-pipeline.md +++ b/docs/dev/superpowers/plans/2026-03-17-cicd-pipeline.md @@ -103,7 +103,7 @@ git commit -m "feat(ci): add version stamp script for dev publishes" ```dockerfile # ────────────────────────────────────────────── # Stage 1: CI Builder -# Image: ghcr.io/gsd-build/gsd-ci-builder +# Image: ghcr.io/singularity-forge/sf-ci-builder # Used by: pipeline.yml Dev stage # ────────────────────────────────────────────── FROM node:22-bookworm AS builder @@ -124,7 +124,7 @@ RUN node --version && rustc --version && cargo --version # ────────────────────────────────────────────── # Stage 2: Runtime -# Image: ghcr.io/gsd-build/gsd-pi +# Image: ghcr.io/singularity-forge/sf-run # Used by: end users via docker run # ────────────────────────────────────────────── FROM node:22-slim AS runtime @@ -947,8 +947,8 @@ Add to `package.json` `scripts`: "test:fixtures:record": "GSD_FIXTURE_MODE=record node --experimental-strip-types tests/fixtures/record.ts", "test:live": "GSD_LIVE_TESTS=1 node --experimental-strip-types tests/live/run.ts", "pipeline:version-stamp": "node scripts/version-stamp.mjs", -"docker:build-runtime": "docker build --target runtime -t ghcr.io/gsd-build/gsd-pi .", -"docker:build-builder": "docker build --target builder -t ghcr.io/gsd-build/gsd-ci-builder ." +"docker:build-runtime": "docker build --target runtime -t ghcr.io/singularity-forge/sf-run .", +"docker:build-builder": "docker build --target builder -t ghcr.io/singularity-forge/sf-ci-builder ." ``` - [ ] **Step 5: Verify live tests skip without env var** @@ -997,7 +997,7 @@ jobs: if: ${{ github.event.workflow_run.conclusion == 'success' }} runs-on: ubuntu-latest container: - image: ghcr.io/gsd-build/gsd-ci-builder:latest # Pin to date tag after first build + image: ghcr.io/singularity-forge/sf-ci-builder:latest # Pin to date tag after first build environment: dev outputs: dev-version: ${{ steps.stamp.outputs.version }} @@ -1082,11 +1082,11 @@ jobs: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin docker build --target runtime \ --build-arg GSD_VERSION=${{ needs.dev-publish.outputs.dev-version }} \ - -t ghcr.io/gsd-build/gsd-pi:next \ - -t ghcr.io/gsd-build/gsd-pi:${{ needs.dev-publish.outputs.dev-version }} \ + -t ghcr.io/singularity-forge/sf-run:next \ + -t ghcr.io/singularity-forge/sf-run:${{ needs.dev-publish.outputs.dev-version }} \ . - docker push ghcr.io/gsd-build/gsd-pi:next - docker push ghcr.io/gsd-build/gsd-pi:${{ needs.dev-publish.outputs.dev-version }} + docker push ghcr.io/singularity-forge/sf-run:next + docker push ghcr.io/singularity-forge/sf-run:${{ needs.dev-publish.outputs.dev-version }} # ─── PROD STAGE ──────────────────────────────────────────── prod-release: @@ -1124,9 +1124,9 @@ jobs: - name: Tag and push Docker images run: | echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin - docker pull ghcr.io/gsd-build/gsd-pi:${{ needs.dev-publish.outputs.dev-version }} - docker tag ghcr.io/gsd-build/gsd-pi:${{ needs.dev-publish.outputs.dev-version }} ghcr.io/gsd-build/gsd-pi:latest - docker push ghcr.io/gsd-build/gsd-pi:latest + docker pull ghcr.io/singularity-forge/sf-run:${{ needs.dev-publish.outputs.dev-version }} + docker tag ghcr.io/singularity-forge/sf-run:${{ needs.dev-publish.outputs.dev-version }} ghcr.io/singularity-forge/sf-run:latest + docker push ghcr.io/singularity-forge/sf-run:latest - name: Create GitHub Release run: | @@ -1164,16 +1164,16 @@ jobs: run: | echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin docker build --target builder \ - -t ghcr.io/gsd-build/gsd-ci-builder:latest \ - -t ghcr.io/gsd-build/gsd-ci-builder:${{ steps.tag.outputs.date }} \ + -t ghcr.io/singularity-forge/sf-ci-builder:latest \ + -t ghcr.io/singularity-forge/sf-ci-builder:${{ steps.tag.outputs.date }} \ . - docker push ghcr.io/gsd-build/gsd-ci-builder:latest - docker push ghcr.io/gsd-build/gsd-ci-builder:${{ steps.tag.outputs.date }} + docker push ghcr.io/singularity-forge/sf-ci-builder:latest + docker push ghcr.io/singularity-forge/sf-ci-builder:${{ steps.tag.outputs.date }} - name: Verify builder image run: | - docker run --rm ghcr.io/gsd-build/gsd-ci-builder:latest node --version - docker run --rm ghcr.io/gsd-build/gsd-ci-builder:latest rustc --version + docker run --rm ghcr.io/singularity-forge/sf-ci-builder:latest node --version + docker run --rm ghcr.io/singularity-forge/sf-ci-builder:latest rustc --version ``` - [ ] **Step 2: Validate YAML syntax** diff --git a/docs/dev/superpowers/specs/2026-03-17-cicd-pipeline-design.md b/docs/dev/superpowers/specs/2026-03-17-cicd-pipeline-design.md index 57875d90e..5e144ba18 100644 --- a/docs/dev/superpowers/specs/2026-03-17-cicd-pipeline-design.md +++ b/docs/dev/superpowers/specs/2026-03-17-cicd-pipeline-design.md @@ -107,7 +107,7 @@ Policy: |---------|--------|----------| | Dev publish succeeds, smoke test fails | Broken version on `@dev` tag | Next successful merge overwrites `@dev`. Manual fix: `npm dist-tag add gsd-pi@ dev` | | Test stage fails after promoting to `@next` | Broken version on `@next` tag | Manual: `npm dist-tag add gsd-pi@ next`. `@latest` is never affected. | -| Prod promotion publishes `@latest` then found broken | Broken production release | Manual: `npm dist-tag add gsd-pi@ latest` and `docker tag ghcr.io/gsd-build/gsd-pi: latest && docker push`. Post-mortem required. | +| Prod promotion publishes `@latest` then found broken | Broken production release | Manual: `npm dist-tag add gsd-pi@ latest` and `docker tag ghcr.io/singularity-forge/sf-run: latest && docker push`. Post-mortem required. | | Docker push succeeds, npm dist-tag fails | Images and npm out of sync | Re-run the failed job (GitHub Actions retry). Images are tagged by version so stale tags are harmless. | | GHCR push fails | No Docker image for this version | Non-blocking — npm publish is the primary distribution. Docker image can be rebuilt manually. | @@ -131,7 +131,7 @@ Two images from a single `Dockerfile` at the repo root. #### CI Builder Image -- **Name:** `ghcr.io/gsd-build/gsd-ci-builder` +- **Name:** `ghcr.io/singularity-forge/sf-ci-builder` - **Base:** `node:22-bookworm` - **Contains:** Node 22, Rust stable toolchain, `aarch64-linux-gnu` cross-compiler - **Size:** ~2 GB @@ -148,13 +148,13 @@ Builder images are tagged with both `:latest` and a date stamp (e.g., `:2026-03- #### Runtime Image -- **Name:** `ghcr.io/gsd-build/gsd-pi` +- **Name:** `ghcr.io/singularity-forge/sf-run` - **Base:** `node:22-slim` - **Contains:** Node 22, git, `gsd-pi` installed globally - **Size:** ~250 MB - **Tags:** `:latest`, `:next`, `:v2.27.0` - **Published:** On every Prod promotion -- **Purpose:** `docker run ghcr.io/gsd-build/gsd-pi` as alternative to `npx` +- **Purpose:** `docker run ghcr.io/singularity-forge/sf-run` as alternative to `npx` ### Why These Base Images @@ -329,8 +329,8 @@ All test files use `.ts` with `--experimental-strip-types` for consistency with "test:fixtures:record": "GSD_FIXTURE_MODE=record node --experimental-strip-types tests/fixtures/record.ts", "test:live": "GSD_LIVE_TESTS=1 node --experimental-strip-types tests/live/run.ts", "pipeline:version-stamp": "node scripts/version-stamp.mjs", - "docker:build-runtime": "docker build --target runtime -t ghcr.io/gsd-build/gsd-pi .", - "docker:build-builder": "docker build --target builder -t ghcr.io/gsd-build/gsd-ci-builder ." + "docker:build-runtime": "docker build --target runtime -t ghcr.io/singularity-forge/sf-run .", + "docker:build-builder": "docker build --target builder -t ghcr.io/singularity-forge/sf-ci-builder ." } ``` @@ -352,6 +352,6 @@ All test files use `.ts` with `--experimental-strip-types` for consistency with 2. Fixture replay tests complete in under 60 seconds with zero API calls 3. The full Dev → Test promotion completes without human intervention 4. Prod promotion is blocked until a maintainer explicitly approves -5. `docker run ghcr.io/gsd-build/gsd-pi --version` returns the correct version +5. `docker run ghcr.io/singularity-forge/sf-run --version` returns the correct version 6. Existing `ci.yml` and `build-native.yml` workflows continue to work unchanged 7. CI builder image reduces toolchain setup from ~3-5 min to ~30s pull diff --git a/docs/user-docs/getting-started.md b/docs/user-docs/getting-started.md index d095ef8f9..786c07e7a 100644 --- a/docs/user-docs/getting-started.md +++ b/docs/user-docs/getting-started.md @@ -279,7 +279,7 @@ Run GSD in an isolated sandbox without installing Node.js on your host. **Step 2 — Clone the GSD repo:** ```bash -git clone https://github.com/gsd-build/gsd-2.git +git clone https://github.com/singularity-forge/sf-run.git cd gsd-2/docker ``` diff --git a/docs/zh-CN/user-docs/getting-started.md b/docs/zh-CN/user-docs/getting-started.md index b2e725b6a..0d1bd3d5e 100644 --- a/docs/zh-CN/user-docs/getting-started.md +++ b/docs/zh-CN/user-docs/getting-started.md @@ -279,7 +279,7 @@ gsd --version # 输出已安装版本 **第 2 步:克隆 GSD 仓库:** ```bash -git clone https://github.com/gsd-build/gsd-2.git +git clone https://github.com/singularity-forge/sf-run.git cd gsd-2/docker ``` diff --git a/flake.nix b/flake.nix new file mode 100644 index 000000000..ad5e05c52 --- /dev/null +++ b/flake.nix @@ -0,0 +1,35 @@ +{ + description = "Minimal runtime shell for gsd-2"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = import nixpkgs { + inherit system; + }; + in + { + devShells.default = pkgs.mkShell { + packages = with pkgs; [ + bash + bun + git + nodejs_24 + ]; + + shellHook = '' + export GSD_SOURCE_DIR="${toString ./.}" + export PATH="$GSD_SOURCE_DIR/bin:$PATH" + + echo "gsd-2 runtime shell" + echo " bun : $(command -v bun)" + echo " node: $(command -v node)" + ''; + }; + }); +} diff --git a/mintlify-docs/docs.json b/mintlify-docs/docs.json index 5b37bb057..2bc2e7005 100644 --- a/mintlify-docs/docs.json +++ b/mintlify-docs/docs.json @@ -33,7 +33,7 @@ "links": [ { "label": "GitHub", - "href": "https://github.com/gsd-build/gsd-2" + "href": "https://github.com/singularity-forge/sf-run" } ], "primary": { @@ -44,7 +44,7 @@ }, "footer": { "socials": { - "github": "https://github.com/gsd-build/gsd-2" + "github": "https://github.com/singularity-forge/sf-run" } }, "navigation": { diff --git a/native/npm/darwin-arm64/package.json b/native/npm/darwin-arm64/package.json index 91eac4c5f..7d77c1d5d 100644 --- a/native/npm/darwin-arm64/package.json +++ b/native/npm/darwin-arm64/package.json @@ -1,5 +1,5 @@ { - "name": "@gsd-build/engine-darwin-arm64", + "name": "@singularity-forge/engine-darwin-arm64", "version": "2.74.0", "description": "GSD native engine binary for macOS ARM64", "os": [ @@ -15,6 +15,6 @@ "license": "MIT", "repository": { "type": "git", - "url": "git+https://github.com/gsd-build/gsd-2.git" + "url": "git+https://github.com/singularity-forge/sf-run.git" } } diff --git a/native/npm/darwin-x64/package.json b/native/npm/darwin-x64/package.json index f0cb63b73..c0223f376 100644 --- a/native/npm/darwin-x64/package.json +++ b/native/npm/darwin-x64/package.json @@ -1,5 +1,5 @@ { - "name": "@gsd-build/engine-darwin-x64", + "name": "@singularity-forge/engine-darwin-x64", "version": "2.74.0", "description": "GSD native engine binary for macOS Intel", "os": [ @@ -15,6 +15,6 @@ "license": "MIT", "repository": { "type": "git", - "url": "git+https://github.com/gsd-build/gsd-2.git" + "url": "git+https://github.com/singularity-forge/sf-run.git" } } diff --git a/native/npm/linux-arm64-gnu/package.json b/native/npm/linux-arm64-gnu/package.json index a71337a5a..88cb0d0f2 100644 --- a/native/npm/linux-arm64-gnu/package.json +++ b/native/npm/linux-arm64-gnu/package.json @@ -1,5 +1,5 @@ { - "name": "@gsd-build/engine-linux-arm64-gnu", + "name": "@singularity-forge/engine-linux-arm64-gnu", "version": "2.74.0", "description": "GSD native engine binary for Linux ARM64 (glibc)", "os": [ @@ -15,6 +15,6 @@ "license": "MIT", "repository": { "type": "git", - "url": "git+https://github.com/gsd-build/gsd-2.git" + "url": "git+https://github.com/singularity-forge/sf-run.git" } } diff --git a/native/npm/linux-x64-gnu/package.json b/native/npm/linux-x64-gnu/package.json index d9f63fea9..7f988b372 100644 --- a/native/npm/linux-x64-gnu/package.json +++ b/native/npm/linux-x64-gnu/package.json @@ -1,5 +1,5 @@ { - "name": "@gsd-build/engine-linux-x64-gnu", + "name": "@singularity-forge/engine-linux-x64-gnu", "version": "2.74.0", "description": "GSD native engine binary for Linux x64 (glibc)", "os": [ @@ -15,6 +15,6 @@ "license": "MIT", "repository": { "type": "git", - "url": "git+https://github.com/gsd-build/gsd-2.git" + "url": "git+https://github.com/singularity-forge/sf-run.git" } } diff --git a/native/npm/win32-x64-msvc/package.json b/native/npm/win32-x64-msvc/package.json index 88c2e986c..6954da721 100644 --- a/native/npm/win32-x64-msvc/package.json +++ b/native/npm/win32-x64-msvc/package.json @@ -1,5 +1,5 @@ { - "name": "@gsd-build/engine-win32-x64-msvc", + "name": "@singularity-forge/engine-win32-x64-msvc", "version": "2.74.0", "description": "GSD native engine binary for Windows x64 (MSVC)", "os": [ @@ -15,6 +15,6 @@ "license": "MIT", "repository": { "type": "git", - "url": "git+https://github.com/gsd-build/gsd-2.git" + "url": "git+https://github.com/singularity-forge/sf-run.git" } } diff --git a/package.json b/package.json index 07a516d14..54dba02e5 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,15 @@ { - "name": "gsd-pi", + "name": "sf-run", "version": "2.74.0", - "description": "GSD — Get Shit Done coding agent", + "description": "sf-run — Singularity Forge runtime core", "license": "MIT", "repository": { "type": "git", - "url": "https://github.com/gsd-build/gsd-2.git" + "url": "https://github.com/singularity-forge/sf-run.git" }, - "homepage": "https://github.com/gsd-build/gsd-2#readme", + "homepage": "https://github.com/singularity-forge/sf-run#readme", "bugs": { - "url": "https://github.com/gsd-build/gsd-2/issues" + "url": "https://github.com/singularity-forge/sf-run/issues" }, "type": "module", "workspaces": [ @@ -17,8 +17,8 @@ "studio" ], "bin": { - "gsd": "dist/loader.js", - "gsd-cli": "dist/loader.js" + "sf": "dist/loader.js", + "sf-cli": "dist/loader.js" }, "files": [ "dist", @@ -33,22 +33,22 @@ "README.md" ], "piConfig": { - "name": "gsd", - "configDir": ".gsd" + "name": "sf", + "configDir": ".sf" }, "engines": { "node": ">=22.0.0" }, "packageManager": "npm@10.9.3", "scripts": { - "build:pi-tui": "npm run build -w @gsd/pi-tui", - "build:pi-ai": "npm run build -w @gsd/pi-ai", - "build:pi-agent-core": "npm run build -w @gsd/pi-agent-core", - "build:pi-coding-agent": "npm run build -w @gsd/pi-coding-agent", - "build:native-pkg": "npm run build -w @gsd/native", - "build:rpc-client": "npm run build -w @gsd-build/rpc-client", + "build:pi-tui": "npm run build -w @sf-run/pi-tui", + "build:pi-ai": "npm run build -w @sf-run/pi-ai", + "build:pi-agent-core": "npm run build -w @sf-run/pi-agent-core", + "build:pi-coding-agent": "npm run build -w @sf-run/pi-coding-agent", + "build:native-pkg": "npm run build -w @sf-run/native", + "build:rpc-client": "npm run build -w @singularity-forge/rpc-client", "build:pi": "npm run build:native-pkg && npm run build:pi-tui && npm run build:pi-ai && npm run build:pi-agent-core && npm run build:pi-coding-agent", - "build:mcp-server": "npm run build -w @gsd-build/mcp-server", + "build:mcp-server": "npm run build -w @singularity-forge/mcp-server", "build:core": "npm run build:pi && npm run build:rpc-client && npm run build:mcp-server && tsc && npm run copy-resources && npm run copy-themes && npm run copy-export-html", "build": "npm run build:core && node scripts/build-web-if-stale.cjs", "stage:web-host": "node scripts/stage-web-standalone.cjs", @@ -76,10 +76,10 @@ "build:native": "node native/scripts/build.js", "build:native:dev": "node native/scripts/build.js --dev", "dev": "node scripts/dev.js", - "gsd": "node scripts/dev-cli.js", - "gsd:web": "npm run build:pi && npm run copy-resources && node scripts/build-web-if-stale.cjs && node scripts/dev-cli.js --web", - "gsd:web:stop": "node scripts/dev-cli.js web stop", - "gsd:web:stop:all": "node scripts/dev-cli.js web stop all", + "sf": "node scripts/dev-cli.js", + "sf:web": "npm run build:pi && npm run copy-resources && node scripts/build-web-if-stale.cjs && node scripts/dev-cli.js --web", + "sf:web:stop": "node scripts/dev-cli.js web stop", + "sf:web:stop:all": "node scripts/dev-cli.js web stop all", "postinstall": "node scripts/link-workspace-packages.cjs && node scripts/ensure-workspace-builds.cjs && node scripts/postinstall.js", "pi:install-global": "node scripts/install-pi-global.js", "pi:uninstall-global": "node scripts/uninstall-pi-global.js", @@ -91,8 +91,8 @@ "release:changelog": "node scripts/generate-changelog.mjs", "release:bump": "node scripts/bump-version.mjs", "release:update-changelog": "node scripts/update-changelog.mjs", - "docker:build-runtime": "docker build --target runtime -t ghcr.io/gsd-build/gsd-pi .", - "docker:build-builder": "docker build --target builder -t ghcr.io/gsd-build/gsd-ci-builder .", + "docker:build-runtime": "docker build --target runtime -t ghcr.io/singularity-forge/sf-run .", + "docker:build-builder": "docker build --target builder -t ghcr.io/singularity-forge/sf-ci-builder .", "prepublishOnly": "npm run sync-pkg-version && npm run sync-platform-versions && node scripts/prepublish-check.mjs && npm run build && npm run typecheck:extensions && npm run validate-pack", "test:live-regression": "node --experimental-strip-types tests/live-regression/run.ts" }, @@ -145,11 +145,11 @@ }, "optionalDependencies": { "@anthropic-ai/claude-agent-sdk": "^0.2.83", - "@gsd-build/engine-darwin-arm64": ">=2.10.2", - "@gsd-build/engine-darwin-x64": ">=2.10.2", - "@gsd-build/engine-linux-arm64-gnu": ">=2.10.2", - "@gsd-build/engine-linux-x64-gnu": ">=2.10.2", - "@gsd-build/engine-win32-x64-msvc": ">=2.10.2", + "@singularity-forge/engine-darwin-arm64": ">=2.10.2", + "@singularity-forge/engine-darwin-x64": ">=2.10.2", + "@singularity-forge/engine-linux-arm64-gnu": ">=2.10.2", + "@singularity-forge/engine-linux-x64-gnu": ">=2.10.2", + "@singularity-forge/engine-win32-x64-msvc": ">=2.10.2", "fsevents": "~2.3.3", "koffi": "^2.9.0" }, diff --git a/packages/daemon/package.json b/packages/daemon/package.json index a0fcb041c..312a62c43 100644 --- a/packages/daemon/package.json +++ b/packages/daemon/package.json @@ -1,11 +1,11 @@ { - "name": "@gsd-build/daemon", + "name": "@singularity-forge/daemon", "version": "2.74.0", - "description": "GSD daemon — background process for project monitoring and Discord integration", + "description": "sf-run daemon — background process for project monitoring and Discord integration", "license": "MIT", "repository": { "type": "git", - "url": "https://github.com/gsd-build/gsd-2.git", + "url": "https://github.com/singularity-forge/sf-run.git", "directory": "packages/daemon" }, "publishConfig": { @@ -21,7 +21,7 @@ } }, "bin": { - "gsd-daemon": "./dist/cli.js" + "sf-daemon": "./dist/cli.js" }, "scripts": { "build": "tsc", @@ -29,7 +29,7 @@ }, "dependencies": { "@anthropic-ai/sdk": "^0.52.0", - "@gsd-build/rpc-client": "^2.74.0", + "@singularity-forge/rpc-client": "^2.74.0", "discord.js": "^14.25.1", "yaml": "^2.8.0", "zod": "^3.24.0" diff --git a/packages/daemon/src/event-bridge.test.ts b/packages/daemon/src/event-bridge.test.ts index 8516b9dc4..1e9c30cf3 100644 --- a/packages/daemon/src/event-bridge.test.ts +++ b/packages/daemon/src/event-bridge.test.ts @@ -12,7 +12,7 @@ import { EventEmitter } from 'node:events'; import { EventBridge } from './event-bridge.js'; import type { EventBridgeOptions, BridgeClient } from './event-bridge.js'; import type { PendingBlocker, ManagedSession, DaemonConfig, SessionStatus } from './types.js'; -import type { SdkAgentEvent, RpcClient, RpcExtensionUIRequest } from '@gsd-build/rpc-client'; +import type { SdkAgentEvent, RpcClient, RpcExtensionUIRequest } from '@singularity-forge/rpc-client'; // --------------------------------------------------------------------------- // Mock factories diff --git a/packages/daemon/src/event-bridge.ts b/packages/daemon/src/event-bridge.ts index 8df4dfd4e..f59bab209 100644 --- a/packages/daemon/src/event-bridge.ts +++ b/packages/daemon/src/event-bridge.ts @@ -12,7 +12,7 @@ import type { Client, Message, TextChannel, MessageComponentInteraction } from 'discord.js'; import { EmbedBuilder, ComponentType } from 'discord.js'; -import type { SdkAgentEvent } from '@gsd-build/rpc-client'; +import type { SdkAgentEvent } from '@singularity-forge/rpc-client'; import type { Logger } from './logger.js'; import type { DaemonConfig, PendingBlocker } from './types.js'; import type { SessionManager } from './session-manager.js'; diff --git a/packages/daemon/src/event-formatter.test.ts b/packages/daemon/src/event-formatter.test.ts index dead1e385..2aacb4798 100644 --- a/packages/daemon/src/event-formatter.test.ts +++ b/packages/daemon/src/event-formatter.test.ts @@ -1,9 +1,9 @@ import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { EmbedBuilder, ActionRowBuilder, ButtonBuilder } from 'discord.js'; -import type { SdkAgentEvent } from '@gsd-build/rpc-client'; +import type { SdkAgentEvent } from '@singularity-forge/rpc-client'; import type { PendingBlocker, FormattedEvent } from './types.js'; -import type { RpcExtensionUIRequest } from '@gsd-build/rpc-client'; +import type { RpcExtensionUIRequest } from '@singularity-forge/rpc-client'; import { formatToolStart, formatToolEnd, diff --git a/packages/daemon/src/event-formatter.ts b/packages/daemon/src/event-formatter.ts index 2828c1db1..ccd98c4be 100644 --- a/packages/daemon/src/event-formatter.ts +++ b/packages/daemon/src/event-formatter.ts @@ -11,8 +11,8 @@ */ import { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js'; -import type { SdkAgentEvent } from '@gsd-build/rpc-client'; -import type { RpcExtensionUIRequest } from '@gsd-build/rpc-client'; +import type { SdkAgentEvent } from '@singularity-forge/rpc-client'; +import type { RpcExtensionUIRequest } from '@singularity-forge/rpc-client'; import type { FormattedEvent, PendingBlocker } from './types.js'; // --------------------------------------------------------------------------- diff --git a/packages/daemon/src/session-manager.ts b/packages/daemon/src/session-manager.ts index d954e37db..bb398b567 100644 --- a/packages/daemon/src/session-manager.ts +++ b/packages/daemon/src/session-manager.ts @@ -16,8 +16,8 @@ import { execSync } from 'node:child_process'; import { basename, resolve } from 'node:path'; import { EventEmitter } from 'node:events'; -import { RpcClient } from '@gsd-build/rpc-client'; -import type { SdkAgentEvent, RpcInitResult, RpcCostUpdateEvent, RpcExtensionUIRequest } from '@gsd-build/rpc-client'; +import { RpcClient } from '@singularity-forge/rpc-client'; +import type { SdkAgentEvent, RpcInitResult, RpcCostUpdateEvent, RpcExtensionUIRequest } from '@singularity-forge/rpc-client'; import type { ManagedSession, StartSessionOptions, diff --git a/packages/daemon/src/types.ts b/packages/daemon/src/types.ts index 822d1ff9b..163bdb412 100644 --- a/packages/daemon/src/types.ts +++ b/packages/daemon/src/types.ts @@ -1,4 +1,4 @@ -import type { RpcClient, SdkAgentEvent, RpcExtensionUIRequest } from '@gsd-build/rpc-client'; +import type { RpcClient, SdkAgentEvent, RpcExtensionUIRequest } from '@singularity-forge/rpc-client'; /** * Log severity levels, ordered from most to least verbose. diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index ea32391f3..3af740455 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -1,11 +1,11 @@ { - "name": "@gsd-build/mcp-server", + "name": "@singularity-forge/mcp-server", "version": "2.74.0", - "description": "MCP server exposing GSD orchestration tools for Claude Code, Cursor, and other MCP clients", + "description": "MCP server exposing sf-run orchestration tools for Claude Code, Cursor, and other MCP clients", "license": "MIT", "repository": { "type": "git", - "url": "https://github.com/gsd-build/gsd-2.git", + "url": "https://github.com/singularity-forge/sf-run.git", "directory": "packages/mcp-server" }, "publishConfig": { @@ -21,7 +21,7 @@ } }, "bin": { - "gsd-mcp-server": "./dist/cli.js" + "sf-mcp-server": "./dist/cli.js" }, "scripts": { "build": "tsc", @@ -29,7 +29,7 @@ }, "dependencies": { "@modelcontextprotocol/sdk": "^1.27.1", - "@gsd-build/rpc-client": "^2.74.0", + "@singularity-forge/rpc-client": "^2.74.0", "zod": "^4.0.0" }, "devDependencies": { diff --git a/packages/mcp-server/src/cli.ts b/packages/mcp-server/src/cli.ts index e9b64d794..aaea896cb 100644 --- a/packages/mcp-server/src/cli.ts +++ b/packages/mcp-server/src/cli.ts @@ -1,5 +1,5 @@ /** - * @gsd-build/mcp-server CLI — stdio transport entry point. + * @singularity-forge/mcp-server CLI — stdio transport entry point. * * Connects the MCP server to stdin/stdout for use by Claude Code, * Cursor, and other MCP-compatible clients. diff --git a/packages/mcp-server/src/env-writer.test.ts b/packages/mcp-server/src/env-writer.test.ts index 5932d1cfb..2d08ecea5 100644 --- a/packages/mcp-server/src/env-writer.test.ts +++ b/packages/mcp-server/src/env-writer.test.ts @@ -1,4 +1,4 @@ -// @gsd-build/mcp-server — Tests for env-writer utilities +// @singularity-forge/mcp-server — Tests for env-writer utilities // Copyright (c) 2026 Jeremy McSpadden import { describe, it, afterEach } from 'node:test'; diff --git a/packages/mcp-server/src/env-writer.ts b/packages/mcp-server/src/env-writer.ts index 219496539..bf8296209 100644 --- a/packages/mcp-server/src/env-writer.ts +++ b/packages/mcp-server/src/env-writer.ts @@ -1,4 +1,4 @@ -// @gsd-build/mcp-server — Environment variable write utilities +// @singularity-forge/mcp-server — Environment variable write utilities // Copyright (c) 2026 Jeremy McSpadden // // Shared helpers for writing env vars to .env files, detecting project diff --git a/packages/mcp-server/src/index.ts b/packages/mcp-server/src/index.ts index 8395cb172..5600698a3 100644 --- a/packages/mcp-server/src/index.ts +++ b/packages/mcp-server/src/index.ts @@ -1,5 +1,5 @@ /** - * @gsd-build/mcp-server — MCP server for GSD orchestration and project state. + * @singularity-forge/mcp-server — MCP server for GSD orchestration and project state. */ export { SessionManager } from './session-manager.js'; diff --git a/packages/mcp-server/src/mcp-server.test.ts b/packages/mcp-server/src/mcp-server.test.ts index c3ba68065..70214b49b 100644 --- a/packages/mcp-server/src/mcp-server.test.ts +++ b/packages/mcp-server/src/mcp-server.test.ts @@ -1,7 +1,7 @@ /** - * @gsd-build/mcp-server — Integration and unit tests. + * @singularity-forge/mcp-server — Integration and unit tests. * - * Strategy: We cannot mock @gsd-build/rpc-client at the module level without + * Strategy: We cannot mock @singularity-forge/rpc-client at the module level without * --experimental-test-module-mocks. Instead we test by: * * 1. Subclassing SessionManager to inject a mock client factory diff --git a/packages/mcp-server/src/secure-env-collect.test.ts b/packages/mcp-server/src/secure-env-collect.test.ts index c33ad2949..485a8e72a 100644 --- a/packages/mcp-server/src/secure-env-collect.test.ts +++ b/packages/mcp-server/src/secure-env-collect.test.ts @@ -1,4 +1,4 @@ -// @gsd-build/mcp-server — Tests for secure_env_collect MCP tool +// @singularity-forge/mcp-server — Tests for secure_env_collect MCP tool // Copyright (c) 2026 Jeremy McSpadden // // Tests the secure_env_collect tool registered in createMcpServer. diff --git a/packages/mcp-server/src/session-manager.ts b/packages/mcp-server/src/session-manager.ts index 841941196..84e7308ce 100644 --- a/packages/mcp-server/src/session-manager.ts +++ b/packages/mcp-server/src/session-manager.ts @@ -8,8 +8,8 @@ import { execSync } from 'node:child_process'; import { resolve } from 'node:path'; -import { RpcClient } from '@gsd-build/rpc-client'; -import type { SdkAgentEvent, RpcInitResult, RpcCostUpdateEvent, RpcExtensionUIRequest } from '@gsd-build/rpc-client'; +import { RpcClient } from '@singularity-forge/rpc-client'; +import type { SdkAgentEvent, RpcInitResult, RpcCostUpdateEvent, RpcExtensionUIRequest } from '@singularity-forge/rpc-client'; import type { ManagedSession, ExecuteOptions, diff --git a/packages/mcp-server/src/types.ts b/packages/mcp-server/src/types.ts index fa12c9f61..4cce6d1d6 100644 --- a/packages/mcp-server/src/types.ts +++ b/packages/mcp-server/src/types.ts @@ -2,7 +2,7 @@ * MCP Server types — session lifecycle and orchestration. */ -import type { RpcClient, SdkAgentEvent, RpcCostUpdateEvent, RpcExtensionUIRequest } from '@gsd-build/rpc-client'; +import type { RpcClient, SdkAgentEvent, RpcCostUpdateEvent, RpcExtensionUIRequest } from '@singularity-forge/rpc-client'; // --------------------------------------------------------------------------- // Session Status diff --git a/packages/native/package.json b/packages/native/package.json index d50833c32..117e000be 100644 --- a/packages/native/package.json +++ b/packages/native/package.json @@ -1,7 +1,7 @@ { - "name": "@gsd/native", + "name": "@sf-run/native", "version": "2.74.0", - "description": "Native Rust bindings for GSD — high-performance native modules via N-API", + "description": "Native Rust bindings for sf-run — high-performance native modules via N-API", "type": "commonjs", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/native/src/__tests__/diff.test.mjs b/packages/native/src/__tests__/diff.test.mjs index 9429fd972..a590f467f 100644 --- a/packages/native/src/__tests__/diff.test.mjs +++ b/packages/native/src/__tests__/diff.test.mjs @@ -35,7 +35,7 @@ for (const candidate of candidates) { if (!native) { console.error( - "Native addon not found. Run `npm run build:native -w @gsd/native` first.", + "Native addon not found. Run `npm run build:native -w @sf-run/native` first.", ); process.exit(1); } diff --git a/packages/native/src/__tests__/fd.test.mjs b/packages/native/src/__tests__/fd.test.mjs index ea63e7912..a2ed59e17 100644 --- a/packages/native/src/__tests__/fd.test.mjs +++ b/packages/native/src/__tests__/fd.test.mjs @@ -28,7 +28,7 @@ for (const candidate of candidates) { } if (!native) { - console.error("Native addon not found. Run `npm run build:native -w @gsd/native` first."); + console.error("Native addon not found. Run `npm run build:native -w @sf-run/native` first."); process.exit(1); } diff --git a/packages/native/src/__tests__/glob.test.mjs b/packages/native/src/__tests__/glob.test.mjs index 10fefb7f4..a9784cf60 100644 --- a/packages/native/src/__tests__/glob.test.mjs +++ b/packages/native/src/__tests__/glob.test.mjs @@ -37,7 +37,7 @@ for (const candidate of candidates) { if (!native) { console.error( - "Native addon not found. Run `npm run build:native -w @gsd/native` first.", + "Native addon not found. Run `npm run build:native -w @sf-run/native` first.", ); process.exit(1); } diff --git a/packages/native/src/__tests__/grep.test.mjs b/packages/native/src/__tests__/grep.test.mjs index 1bc225c58..0f3c200f7 100644 --- a/packages/native/src/__tests__/grep.test.mjs +++ b/packages/native/src/__tests__/grep.test.mjs @@ -28,7 +28,7 @@ for (const candidate of candidates) { } if (!native) { - console.error("Native addon not found. Run `npm run build:native -w @gsd/native` first."); + console.error("Native addon not found. Run `npm run build:native -w @sf-run/native` first."); process.exit(1); } diff --git a/packages/native/src/__tests__/highlight.test.mjs b/packages/native/src/__tests__/highlight.test.mjs index e9ba01a3f..286821b22 100644 --- a/packages/native/src/__tests__/highlight.test.mjs +++ b/packages/native/src/__tests__/highlight.test.mjs @@ -26,7 +26,7 @@ for (const candidate of candidates) { } if (!native) { - console.error("Native addon not found. Run `npm run build:native -w @gsd/native` first."); + console.error("Native addon not found. Run `npm run build:native -w @sf-run/native` first."); process.exit(1); } diff --git a/packages/native/src/__tests__/html.test.mjs b/packages/native/src/__tests__/html.test.mjs index 31e21c463..7a9424a79 100644 --- a/packages/native/src/__tests__/html.test.mjs +++ b/packages/native/src/__tests__/html.test.mjs @@ -25,7 +25,7 @@ for (const candidate of candidates) { } if (!native) { - console.error("Native addon not found. Run `npm run build:native -w @gsd/native` first."); + console.error("Native addon not found. Run `npm run build:native -w @sf-run/native` first."); process.exit(1); } diff --git a/packages/native/src/__tests__/image.test.mjs b/packages/native/src/__tests__/image.test.mjs index 91f297ed6..b560507b4 100644 --- a/packages/native/src/__tests__/image.test.mjs +++ b/packages/native/src/__tests__/image.test.mjs @@ -26,7 +26,7 @@ for (const candidate of candidates) { } if (!native) { - console.error("Native addon not found. Run 'npm run build:native -w @gsd/native' first."); + console.error("Native addon not found. Run 'npm run build:native -w @sf-run/native' first."); process.exit(1); } diff --git a/packages/native/src/__tests__/json-parse.test.mjs b/packages/native/src/__tests__/json-parse.test.mjs index 9fe763723..19c16e75f 100644 --- a/packages/native/src/__tests__/json-parse.test.mjs +++ b/packages/native/src/__tests__/json-parse.test.mjs @@ -25,7 +25,7 @@ for (const candidate of candidates) { } if (!native) { - console.error("Native addon not found. Run `npm run build:native -w @gsd/native` first."); + console.error("Native addon not found. Run `npm run build:native -w @sf-run/native` first."); process.exit(1); } diff --git a/packages/native/src/__tests__/module-compat.test.mjs b/packages/native/src/__tests__/module-compat.test.mjs index 949fd16d3..8f989c1cd 100644 --- a/packages/native/src/__tests__/module-compat.test.mjs +++ b/packages/native/src/__tests__/module-compat.test.mjs @@ -1,5 +1,5 @@ /** - * Tests that the @gsd/native package.json is correctly configured + * Tests that the @sf-run/native package.json is correctly configured * for Node.js module resolution (ESM/CJS compatibility). * * Regression test for #2861: "type": "module" + "import"-only export @@ -17,7 +17,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)); const pkgPath = path.resolve(__dirname, "..", "..", "package.json"); const pkg = JSON.parse(readFileSync(pkgPath, "utf8")); -describe("@gsd/native module compatibility (#2861)", () => { +describe("@sf-run/native module compatibility (#2861)", () => { test("package.json must not declare type: module (compiled output is CJS-compatible)", () => { // The compiled output uses createRequire() to load .node addons. // Declaring "type": "module" forces Node.js to treat .js files as ESM, diff --git a/packages/native/src/__tests__/ps.test.mjs b/packages/native/src/__tests__/ps.test.mjs index c0b754b45..0730f8d00 100644 --- a/packages/native/src/__tests__/ps.test.mjs +++ b/packages/native/src/__tests__/ps.test.mjs @@ -27,7 +27,7 @@ for (const candidate of candidates) { } if (!native) { - console.error("Native addon not found. Run `npm run build:native -w @gsd/native` first."); + console.error("Native addon not found. Run `npm run build:native -w @sf-run/native` first."); process.exit(1); } diff --git a/packages/native/src/__tests__/text.test.mjs b/packages/native/src/__tests__/text.test.mjs index 1ca4f2783..c9f635bc0 100644 --- a/packages/native/src/__tests__/text.test.mjs +++ b/packages/native/src/__tests__/text.test.mjs @@ -35,7 +35,7 @@ for (const candidate of candidates) { if (!native) { console.error( - "Native addon not found. Run `npm run build:native -w @gsd/native` first.", + "Native addon not found. Run `npm run build:native -w @sf-run/native` first.", ); process.exit(1); } diff --git a/packages/native/src/__tests__/ttsr.test.mjs b/packages/native/src/__tests__/ttsr.test.mjs index d62a2e8c1..7a1e29085 100644 --- a/packages/native/src/__tests__/ttsr.test.mjs +++ b/packages/native/src/__tests__/ttsr.test.mjs @@ -26,7 +26,7 @@ for (const candidate of candidates) { } if (!native) { - console.error("Native addon not found. Run `npm run build:native -w @gsd/native` first."); + console.error("Native addon not found. Run `npm run build:native -w @sf-run/native` first."); process.exit(1); } diff --git a/packages/native/src/__tests__/xxhash.test.mjs b/packages/native/src/__tests__/xxhash.test.mjs index 6feeba6f0..f9a212bd4 100644 --- a/packages/native/src/__tests__/xxhash.test.mjs +++ b/packages/native/src/__tests__/xxhash.test.mjs @@ -1,6 +1,6 @@ import { describe, it } from "node:test"; import assert from "node:assert/strict"; -import { xxHash32, xxHash32Fallback } from "@gsd/native/xxhash"; +import { xxHash32, xxHash32Fallback } from "@sf-run/native/xxhash"; /** * Reference values computed from the pure-JS xxHash32 implementation diff --git a/packages/native/src/index.ts b/packages/native/src/index.ts index a2cc227d0..70b8aadd7 100644 --- a/packages/native/src/index.ts +++ b/packages/native/src/index.ts @@ -1,5 +1,5 @@ /** - * @gsd/native — High-performance Rust modules exposed via N-API. + * @sf-run/native — High-performance Rust modules exposed via N-API. * * Modules: * - clipboard: native clipboard access (text + image) diff --git a/packages/native/src/native.ts b/packages/native/src/native.ts index 05d4288b1..b22a17a3f 100644 --- a/packages/native/src/native.ts +++ b/packages/native/src/native.ts @@ -3,7 +3,7 @@ * * Locates and loads the compiled Rust N-API addon (`.node` file). * Resolution order: - * 1. @gsd-build/engine-{platform} npm optional dependency (production install) + * 1. @singularity-forge/engine-{platform} npm optional dependency (production install) * 2. native/addon/gsd_engine.{platform}.node (local release build) * 3. native/addon/gsd_engine.dev.node (local debug build) */ @@ -37,10 +37,10 @@ function loadNative(): Record { const packageSuffix = platformPackageMap[platformTag]; if (packageSuffix) { try { - _loadedSuccessfully = true; return _require(`@gsd-build/engine-${packageSuffix}`) as Record; + _loadedSuccessfully = true; return _require(`@singularity-forge/engine-${packageSuffix}`) as Record; } catch (err) { const message = err instanceof Error ? err.message : String(err); - errors.push(`@gsd-build/engine-${packageSuffix}: ${message}`); + errors.push(`@singularity-forge/engine-${packageSuffix}: ${message}`); } } diff --git a/packages/pi-agent-core/package.json b/packages/pi-agent-core/package.json index 5f90b4dab..d60b3a021 100644 --- a/packages/pi-agent-core/package.json +++ b/packages/pi-agent-core/package.json @@ -1,5 +1,5 @@ { - "name": "@gsd/pi-agent-core", + "name": "@sf-run/pi-agent-core", "version": "2.74.0", "description": "General-purpose agent core (vendored from pi-mono)", "type": "module", diff --git a/packages/pi-agent-core/src/agent-loop.test.ts b/packages/pi-agent-core/src/agent-loop.test.ts index e0a11aa06..f53a39782 100644 --- a/packages/pi-agent-core/src/agent-loop.test.ts +++ b/packages/pi-agent-core/src/agent-loop.test.ts @@ -9,8 +9,8 @@ import { fileURLToPath } from "node:url"; import { Type } from "@sinclair/typebox"; import { agentLoop, MAX_CONSECUTIVE_VALIDATION_FAILURES } from "./agent-loop.js"; import type { AgentContext, AgentLoopConfig, AgentTool, AgentEvent, AgentMessage } from "./types.js"; -import { AssistantMessageEventStream, EventStream } from "@gsd/pi-ai"; -import type { AssistantMessage, AssistantMessageEvent, Model } from "@gsd/pi-ai"; +import { AssistantMessageEventStream, EventStream } from "@sf-run/pi-ai"; +import type { AssistantMessage, AssistantMessageEvent, Model } from "@sf-run/pi-ai"; const __dirname = dirname(fileURLToPath(import.meta.url)); diff --git a/packages/pi-agent-core/src/agent-loop.ts b/packages/pi-agent-core/src/agent-loop.ts index a99b596c8..3f18b99b6 100644 --- a/packages/pi-agent-core/src/agent-loop.ts +++ b/packages/pi-agent-core/src/agent-loop.ts @@ -10,7 +10,7 @@ import { streamSimple, type ToolResultMessage, validateToolArguments, -} from "@gsd/pi-ai"; +} from "@sf-run/pi-ai"; import type { AgentContext, AgentEvent, @@ -27,7 +27,7 @@ import type { * schema validation before the loop terminates. This prevents unbounded retry * loops when the LLM repeatedly emits tool calls with arguments that cannot * pass validation (e.g., schema overload, truncated JSON, missing required - * fields). See: https://github.com/gsd-build/gsd-2/issues/2783 + * fields). See: https://github.com/singularity-forge/sf-run/issues/2783 */ export const MAX_CONSECUTIVE_VALIDATION_FAILURES = 3; diff --git a/packages/pi-agent-core/src/agent.test.ts b/packages/pi-agent-core/src/agent.test.ts index 4ecd23af2..b0a73c119 100644 --- a/packages/pi-agent-core/src/agent.test.ts +++ b/packages/pi-agent-core/src/agent.test.ts @@ -1,7 +1,7 @@ // Agent activeInferenceModel regression tests // Verifies that activeInferenceModel is set/cleared correctly in _runLoop, // and that the footer reads activeInferenceModel instead of state.model. -// Regression test for https://github.com/gsd-build/gsd-2/issues/1844 Bug 2 +// Regression test for https://github.com/singularity-forge/sf-run/issues/1844 Bug 2 import { describe, it } from "node:test"; import assert from "node:assert/strict"; @@ -9,7 +9,7 @@ import { readFileSync } from "node:fs"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; import { Agent } from "./agent.ts"; -import { getModel, type AssistantMessageEventStream } from "@gsd/pi-ai"; +import { getModel, type AssistantMessageEventStream } from "@sf-run/pi-ai"; const __dirname = dirname(fileURLToPath(import.meta.url)); diff --git a/packages/pi-agent-core/src/agent.ts b/packages/pi-agent-core/src/agent.ts index 924dd8d39..c9af2f751 100644 --- a/packages/pi-agent-core/src/agent.ts +++ b/packages/pi-agent-core/src/agent.ts @@ -13,7 +13,7 @@ import { type TextContent, type ThinkingBudgets, type Transport, -} from "@gsd/pi-ai"; +} from "@sf-run/pi-ai"; import { agentLoop, agentLoopContinue, ZERO_USAGE } from "./agent-loop.js"; import type { AgentContext, diff --git a/packages/pi-agent-core/src/proxy.ts b/packages/pi-agent-core/src/proxy.ts index 574ec2bf6..b8872aedc 100644 --- a/packages/pi-agent-core/src/proxy.ts +++ b/packages/pi-agent-core/src/proxy.ts @@ -14,7 +14,7 @@ import { type SimpleStreamOptions, type StopReason, type ToolCall, -} from "@gsd/pi-ai"; +} from "@sf-run/pi-ai"; import { ZERO_USAGE } from "./agent-loop.js"; // Create stream class matching ProxyMessageEventStream diff --git a/packages/pi-agent-core/src/types.ts b/packages/pi-agent-core/src/types.ts index 846764edd..3fddf2bfe 100644 --- a/packages/pi-agent-core/src/types.ts +++ b/packages/pi-agent-core/src/types.ts @@ -9,7 +9,7 @@ import type { TextContent, Tool, ToolResultMessage, -} from "@gsd/pi-ai"; +} from "@sf-run/pi-ai"; import type { Static, TSchema } from "@sinclair/typebox"; /** Stream function - can return sync or Promise for async config lookup */ diff --git a/packages/pi-ai/package.json b/packages/pi-ai/package.json index 1d04b1536..07e67a5f1 100644 --- a/packages/pi-ai/package.json +++ b/packages/pi-ai/package.json @@ -1,5 +1,5 @@ { - "name": "@gsd/pi-ai", + "name": "@sf-run/pi-ai", "version": "2.74.0", "description": "Unified LLM API (vendored from pi-mono)", "type": "module", diff --git a/packages/pi-ai/src/cli.ts b/packages/pi-ai/src/cli.ts index 71dbfc3fd..c2e07f04d 100644 --- a/packages/pi-ai/src/cli.ts +++ b/packages/pi-ai/src/cli.ts @@ -64,7 +64,7 @@ async function main(): Promise { if (!command || command === "help" || command === "--help" || command === "-h") { const providerList = PROVIDERS.map((p) => ` ${p.id.padEnd(20)} ${p.name}`).join("\n"); - console.log(`Usage: npx @gsd/pi-ai [provider] + console.log(`Usage: npx @sf-run/pi-ai [provider] Commands: login [provider] Login to an OAuth provider @@ -74,9 +74,9 @@ Providers: ${providerList} Examples: - npx @gsd/pi-ai login # interactive provider selection - npx @gsd/pi-ai login anthropic # login to specific provider - npx @gsd/pi-ai list # list providers + npx @sf-run/pi-ai login # interactive provider selection + npx @sf-run/pi-ai login anthropic # login to specific provider + npx @sf-run/pi-ai list # list providers `); return; } @@ -113,7 +113,7 @@ Examples: if (!PROVIDERS.some((p) => p.id === provider)) { console.error(`Unknown provider: ${provider}`); - console.error(`Use 'npx @gsd/pi-ai list' to see available providers`); + console.error(`Use 'npx @sf-run/pi-ai list' to see available providers`); process.exit(1); } @@ -123,7 +123,7 @@ Examples: } console.error(`Unknown command: ${command}`); - console.error(`Use 'npx @gsd/pi-ai --help' for usage`); + console.error(`Use 'npx @sf-run/pi-ai --help' for usage`); process.exit(1); } diff --git a/packages/pi-ai/src/models.custom.ts b/packages/pi-ai/src/models.custom.ts index 37bccc97a..7a64d0dfe 100644 --- a/packages/pi-ai/src/models.custom.ts +++ b/packages/pi-ai/src/models.custom.ts @@ -4,7 +4,7 @@ // third-party catalog. Providers that use proprietary endpoints and are not // listed on models.dev must be defined here so they survive regeneration. // -// See: https://github.com/gsd-build/gsd-2/issues/2339 +// See: https://github.com/singularity-forge/sf-run/issues/2339 // // To add a custom provider: // 1. Add its model definitions below following the existing pattern. diff --git a/packages/pi-ai/src/models.ts b/packages/pi-ai/src/models.ts index ac0a729b7..731760854 100644 --- a/packages/pi-ai/src/models.ts +++ b/packages/pi-ai/src/models.ts @@ -15,7 +15,7 @@ for (const [provider, models] of Object.entries(MODELS)) { // Merge manually-maintained custom providers that are NOT in models.dev. // Custom models are additive — they never overwrite generated entries. -// See: https://github.com/gsd-build/gsd-2/issues/2339 +// See: https://github.com/singularity-forge/sf-run/issues/2339 for (const [provider, models] of Object.entries(CUSTOM_MODELS)) { if (!modelRegistry.has(provider)) { modelRegistry.set(provider, new Map>()); diff --git a/packages/pi-ai/src/utils/json-parse.ts b/packages/pi-ai/src/utils/json-parse.ts index 72f934e33..1ce5ba8f9 100644 --- a/packages/pi-ai/src/utils/json-parse.ts +++ b/packages/pi-ai/src/utils/json-parse.ts @@ -1,4 +1,4 @@ -import { parseStreamingJson as nativeParseStreamingJson } from "@gsd/native"; +import { parseStreamingJson as nativeParseStreamingJson } from "@sf-run/native"; import { hasXmlParameterTags, hasYamlBulletLists, repairToolJson } from "./repair-tool-json.js"; /** diff --git a/packages/pi-ai/src/utils/repair-tool-json.ts b/packages/pi-ai/src/utils/repair-tool-json.ts index 27ea7b14c..bd2d9ae7e 100644 --- a/packages/pi-ai/src/utils/repair-tool-json.ts +++ b/packages/pi-ai/src/utils/repair-tool-json.ts @@ -14,7 +14,7 @@ * * This module detects and repairs such patterns before JSON.parse is called. * - * @see https://github.com/gsd-build/gsd-2/issues/2660 + * @see https://github.com/singularity-forge/sf-run/issues/2660 */ /** @@ -34,7 +34,7 @@ export function hasYamlBulletLists(json: string): boolean { * Some models mix XML tool-call syntax into JSON string values, * producing hybrid output that fails JSON.parse. * - * @see https://github.com/gsd-build/gsd-2/issues/3403 + * @see https://github.com/singularity-forge/sf-run/issues/3403 */ export function hasXmlParameterTags(json: string): boolean { return /<\/?parameter[\s>]/.test(json); @@ -47,7 +47,7 @@ export function hasXmlParameterTags(json: string): boolean { * Smaller models sometimes emit incomplete numbers when the value * is cut off mid-generation. * - * @see https://github.com/gsd-build/gsd-2/issues/3464 + * @see https://github.com/singularity-forge/sf-run/issues/3464 */ export function hasTruncatedNumbers(json: string): boolean { // Match: colon, optional whitespace, then a comma or } without a value diff --git a/packages/pi-coding-agent/package.json b/packages/pi-coding-agent/package.json index 0ae04d50f..cfcb9a782 100644 --- a/packages/pi-coding-agent/package.json +++ b/packages/pi-coding-agent/package.json @@ -1,5 +1,5 @@ { - "name": "@gsd/pi-coding-agent", + "name": "@sf-run/pi-coding-agent", "version": "2.74.0", "description": "Coding agent CLI (vendored from pi-mono)", "type": "module", diff --git a/packages/pi-coding-agent/src/cli.ts b/packages/pi-coding-agent/src/cli.ts index 0876299a3..7a3f1401a 100644 --- a/packages/pi-coding-agent/src/cli.ts +++ b/packages/pi-coding-agent/src/cli.ts @@ -7,8 +7,8 @@ */ process.title = "pi"; -import { setBedrockProviderModule } from "@gsd/pi-ai"; -import { bedrockProviderModule } from "@gsd/pi-ai/bedrock-provider"; +import { setBedrockProviderModule } from "@sf-run/pi-ai"; +import { bedrockProviderModule } from "@sf-run/pi-ai/bedrock-provider"; import { EnvHttpProxyAgent, setGlobalDispatcher } from "undici"; import { main } from "./main.js"; diff --git a/packages/pi-coding-agent/src/cli/args.ts b/packages/pi-coding-agent/src/cli/args.ts index cd056d5d8..b13330cbb 100644 --- a/packages/pi-coding-agent/src/cli/args.ts +++ b/packages/pi-coding-agent/src/cli/args.ts @@ -2,7 +2,7 @@ * CLI argument parsing and help display */ -import type { ThinkingLevel } from "@gsd/pi-agent-core"; +import type { ThinkingLevel } from "@sf-run/pi-agent-core"; import chalk from "chalk"; import { APP_NAME, CONFIG_DIR_NAME, ENV_AGENT_DIR } from "../config.js"; import { allTools, type ToolName } from "../core/tools/index.js"; diff --git a/packages/pi-coding-agent/src/cli/config-selector.ts b/packages/pi-coding-agent/src/cli/config-selector.ts index 6d4e5d6c0..1e7693c9d 100644 --- a/packages/pi-coding-agent/src/cli/config-selector.ts +++ b/packages/pi-coding-agent/src/cli/config-selector.ts @@ -2,7 +2,7 @@ * TUI config selector for `pi config` command */ -import { ProcessTerminal, TUI } from "@gsd/pi-tui"; +import { ProcessTerminal, TUI } from "@sf-run/pi-tui"; import type { ResolvedPaths } from "../core/package-manager.js"; import type { SettingsManager } from "../core/settings-manager.js"; import { ConfigSelectorComponent } from "../modes/interactive/components/config-selector.js"; diff --git a/packages/pi-coding-agent/src/cli/file-processor.ts b/packages/pi-coding-agent/src/cli/file-processor.ts index 1f5a5e7fb..1d7869ebe 100644 --- a/packages/pi-coding-agent/src/cli/file-processor.ts +++ b/packages/pi-coding-agent/src/cli/file-processor.ts @@ -3,7 +3,7 @@ */ import { access, readFile, stat } from "node:fs/promises"; -import type { ImageContent } from "@gsd/pi-ai"; +import type { ImageContent } from "@sf-run/pi-ai"; import chalk from "chalk"; import { resolve } from "path"; import { resolveReadPath } from "../core/tools/path-utils.js"; diff --git a/packages/pi-coding-agent/src/cli/list-models.ts b/packages/pi-coding-agent/src/cli/list-models.ts index b611c271d..1267ceda4 100644 --- a/packages/pi-coding-agent/src/cli/list-models.ts +++ b/packages/pi-coding-agent/src/cli/list-models.ts @@ -2,8 +2,8 @@ * List available models with optional fuzzy search and discovery support */ -import type { Api, Model } from "@gsd/pi-ai"; -import { fuzzyFilter } from "@gsd/pi-tui"; +import type { Api, Model } from "@sf-run/pi-ai"; +import { fuzzyFilter } from "@sf-run/pi-tui"; import type { ModelRegistry } from "../core/model-registry.js"; export interface ListModelsOptions { diff --git a/packages/pi-coding-agent/src/cli/session-picker.ts b/packages/pi-coding-agent/src/cli/session-picker.ts index ee06c0b96..42e5dd86b 100644 --- a/packages/pi-coding-agent/src/cli/session-picker.ts +++ b/packages/pi-coding-agent/src/cli/session-picker.ts @@ -2,7 +2,7 @@ * TUI session selector for --resume flag */ -import { ProcessTerminal, TUI } from "@gsd/pi-tui"; +import { ProcessTerminal, TUI } from "@sf-run/pi-tui"; import { KeybindingsManager } from "../core/keybindings.js"; import type { SessionInfo, SessionListProgress } from "../core/session-manager.js"; import { SessionSelectorComponent } from "../modes/interactive/components/session-selector.js"; diff --git a/packages/pi-coding-agent/src/core/agent-session-renderable-tools.test.ts b/packages/pi-coding-agent/src/core/agent-session-renderable-tools.test.ts index 51f7ae7fc..65fb2f43d 100644 --- a/packages/pi-coding-agent/src/core/agent-session-renderable-tools.test.ts +++ b/packages/pi-coding-agent/src/core/agent-session-renderable-tools.test.ts @@ -4,7 +4,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, it } from "node:test"; -import { Agent } from "@gsd/pi-agent-core"; +import { Agent } from "@sf-run/pi-agent-core"; import { Type } from "@sinclair/typebox"; import type { ToolDefinition } from "./extensions/types.js"; import { AgentSession } from "./agent-session.js"; diff --git a/packages/pi-coding-agent/src/core/agent-session.ts b/packages/pi-coding-agent/src/core/agent-session.ts index 8949773c9..78ec05701 100644 --- a/packages/pi-coding-agent/src/core/agent-session.ts +++ b/packages/pi-coding-agent/src/core/agent-session.ts @@ -22,9 +22,9 @@ import type { AgentState, AgentTool, ThinkingLevel, -} from "@gsd/pi-agent-core"; -import type { AssistantMessage, ImageContent, Message, Model, TextContent } from "@gsd/pi-ai"; -import { modelsAreEqual, resetApiProviders, supportsXhigh } from "@gsd/pi-ai"; +} from "@sf-run/pi-agent-core"; +import type { AssistantMessage, ImageContent, Message, Model, TextContent } from "@sf-run/pi-ai"; +import { modelsAreEqual, resetApiProviders, supportsXhigh } from "@sf-run/pi-ai"; import { Type } from "@sinclair/typebox"; import { getDocsPath } from "../config.js"; import { getErrorMessage } from "../utils/error.js"; @@ -305,7 +305,7 @@ export class AgentSession { // Whether model changes should write defaultProvider/defaultModel to settings.json. // Defaults to false — callers must explicitly opt into persistence. This is the - // safe default for SDK consumers: a third party building on @gsd/pi-coding-agent + // safe default for SDK consumers: a third party building on @sf-run/pi-coding-agent // should not silently mutate the user's global settings just by switching models. // Interactive CLI entry points (gsd wrapper's interactive branch and pi main's // isInteractive branch) explicitly set this to true so user model picks still diff --git a/packages/pi-coding-agent/src/core/auth-storage.ts b/packages/pi-coding-agent/src/core/auth-storage.ts index c604fc801..244f72462 100644 --- a/packages/pi-coding-agent/src/core/auth-storage.ts +++ b/packages/pi-coding-agent/src/core/auth-storage.ts @@ -14,8 +14,8 @@ import { type OAuthCredentials, type OAuthLoginCallbacks, type OAuthProviderId, -} from "@gsd/pi-ai"; -import { getOAuthApiKey, getOAuthProvider, getOAuthProviders } from "@gsd/pi-ai/oauth"; +} from "@sf-run/pi-ai"; +import { getOAuthApiKey, getOAuthProvider, getOAuthProviders } from "@sf-run/pi-ai/oauth"; import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"; import { dirname, join } from "path"; import { getAgentDir } from "../config.js"; diff --git a/packages/pi-coding-agent/src/core/bash-executor.ts b/packages/pi-coding-agent/src/core/bash-executor.ts index f043b9379..0fabea790 100644 --- a/packages/pi-coding-agent/src/core/bash-executor.ts +++ b/packages/pi-coding-agent/src/core/bash-executor.ts @@ -29,7 +29,7 @@ function registerTempCleanup(): void { } }); } -import { processStreamChunk, type StreamState } from "@gsd/native"; +import { processStreamChunk, type StreamState } from "@sf-run/native"; import { getShellConfig, getShellEnv, killProcessTree, sanitizeCommand } from "../utils/shell.js"; import type { BashOperations } from "./tools/bash.js"; import { DEFAULT_MAX_BYTES, truncateTail } from "./tools/truncate.js"; diff --git a/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts b/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts index a22a84043..ac1859c6e 100644 --- a/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +++ b/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts @@ -97,7 +97,7 @@ function createHost() { test("chat-controller renders content blocks in content[] index order (tool-first stream)", async () => { // ToolExecutionComponent uses the global theme singleton. // Install a minimal no-op theme implementation for this unit test. - (globalThis as any)[Symbol.for("@gsd/pi-coding-agent:theme")] = { + (globalThis as any)[Symbol.for("@sf-run/pi-coding-agent:theme")] = { fg: (_key: string, text: string) => text, bg: (_key: string, text: string) => text, bold: (text: string) => text, @@ -166,7 +166,7 @@ test("chat-controller renders content blocks in content[] index order (tool-firs }); test("chat-controller renders serverToolUse before trailing text matching content[] index order", async () => { - (globalThis as any)[Symbol.for("@gsd/pi-coding-agent:theme")] = { + (globalThis as any)[Symbol.for("@sf-run/pi-coding-agent:theme")] = { fg: (_key: string, text: string) => text, bg: (_key: string, text: string) => text, bold: (text: string) => text, @@ -235,7 +235,7 @@ test("chat-controller renders serverToolUse before trailing text matching conten }); test("chat-controller keeps pre-tool prose visible until post-tool prose arrives, then prunes it", async () => { - (globalThis as any)[Symbol.for("@gsd/pi-coding-agent:theme")] = { + (globalThis as any)[Symbol.for("@sf-run/pi-coding-agent:theme")] = { fg: (_key: string, text: string) => text, bg: (_key: string, text: string) => text, bold: (text: string) => text, @@ -326,7 +326,7 @@ test("chat-controller keeps pre-tool prose visible until post-tool prose arrives }); test("chat-controller keeps pre-tool thinking visible for claude-code MCP turns without post-tool prose", async () => { - (globalThis as any)[Symbol.for("@gsd/pi-coding-agent:theme")] = { + (globalThis as any)[Symbol.for("@sf-run/pi-coding-agent:theme")] = { fg: (_key: string, text: string) => text, bg: (_key: string, text: string) => text, bold: (text: string) => text, @@ -393,7 +393,7 @@ test("chat-controller keeps pre-tool thinking visible for claude-code MCP turns }); test("chat-controller prunes orphaned provisional text after claude-code sub-turn shrink when MCP tools appear", async () => { - (globalThis as any)[Symbol.for("@gsd/pi-coding-agent:theme")] = { + (globalThis as any)[Symbol.for("@sf-run/pi-coding-agent:theme")] = { fg: (_key: string, text: string) => text, bg: (_key: string, text: string) => text, bold: (text: string) => text, @@ -494,7 +494,7 @@ test("chat-controller prunes orphaned provisional text after claude-code sub-tur }); test("chat-controller pins latest assistant text above editor when tool calls are present", async () => { - (globalThis as any)[Symbol.for("@gsd/pi-coding-agent:theme")] = { + (globalThis as any)[Symbol.for("@sf-run/pi-coding-agent:theme")] = { fg: (_key: string, text: string) => text, bg: (_key: string, text: string) => text, bold: (text: string) => text, @@ -548,7 +548,7 @@ test("chat-controller pins latest assistant text above editor when tool calls ar }); test("chat-controller clears pinned zone when a new assistant message starts", async () => { - (globalThis as any)[Symbol.for("@gsd/pi-coding-agent:theme")] = { + (globalThis as any)[Symbol.for("@sf-run/pi-coding-agent:theme")] = { fg: (_key: string, text: string) => text, bg: (_key: string, text: string) => text, bold: (text: string) => text, @@ -598,7 +598,7 @@ test("chat-controller clears pinned zone when a new assistant message starts", a }); test("chat-controller clears pinned zone when the agent turn ends", async () => { - (globalThis as any)[Symbol.for("@gsd/pi-coding-agent:theme")] = { + (globalThis as any)[Symbol.for("@sf-run/pi-coding-agent:theme")] = { fg: (_key: string, text: string) => text, bg: (_key: string, text: string) => text, bold: (text: string) => text, @@ -646,7 +646,7 @@ test("chat-controller clears pinned zone when the agent turn ends", async () => }); test("chat-controller clears pinned zone when assistant message ends", async () => { - (globalThis as any)[Symbol.for("@gsd/pi-coding-agent:theme")] = { + (globalThis as any)[Symbol.for("@sf-run/pi-coding-agent:theme")] = { fg: (_key: string, text: string) => text, bg: (_key: string, text: string) => text, bold: (text: string) => text, @@ -696,7 +696,7 @@ test("chat-controller clears pinned zone when assistant message ends", async () }); test("chat-controller does not pin when there are no tool calls", async () => { - (globalThis as any)[Symbol.for("@gsd/pi-coding-agent:theme")] = { + (globalThis as any)[Symbol.for("@sf-run/pi-coding-agent:theme")] = { fg: (_key: string, text: string) => text, bg: (_key: string, text: string) => text, bold: (text: string) => text, @@ -731,7 +731,7 @@ test("chat-controller does not pin when there are no tool calls", async () => { // Expected chatContainer order: textRun(A), toolExec(T1), textRun(B), toolExec(T2), textRun(C) // Each AssistantMessageComponent must render ONLY its own text — no duplication after message_end. test("chat-controller renders interleaved text and tool blocks in content[] index order (#4144)", async () => { - (globalThis as any)[Symbol.for("@gsd/pi-coding-agent:theme")] = { + (globalThis as any)[Symbol.for("@sf-run/pi-coding-agent:theme")] = { fg: (_key: string, text: string) => text, bg: (_key: string, text: string) => text, bold: (text: string) => text, @@ -844,7 +844,7 @@ test("chat-controller renders interleaved text and tool blocks in content[] inde }); test("chat-controller does not duplicate text when content is [text, tool, text] (interleaved stream)", async () => { - (globalThis as any)[Symbol.for("@gsd/pi-coding-agent:theme")] = { + (globalThis as any)[Symbol.for("@sf-run/pi-coding-agent:theme")] = { fg: (_key: string, text: string) => text, bg: (_key: string, text: string) => text, bold: (text: string) => text, @@ -924,7 +924,7 @@ test("chat-controller does not duplicate text when content is [text, tool, text] // sub-turn children must stay frozen; new sub-turn segments must append after // them, and the pinned "Latest Output" mirror must re-evaluate for the new sub-turn. test("chat-controller freezes prior sub-turn and appends new segments when content shrinks", async () => { - (globalThis as any)[Symbol.for("@gsd/pi-coding-agent:theme")] = { + (globalThis as any)[Symbol.for("@sf-run/pi-coding-agent:theme")] = { fg: (_key: string, text: string) => text, bg: (_key: string, text: string) => text, bold: (text: string) => text, @@ -1026,7 +1026,7 @@ test("chat-controller freezes prior sub-turn and appends new segments when conte // pinned "Latest Output" mirror can display text from the new sub-turn instead // of staying frozen on a stale snapshot (the "bottom green stays" symptom). test("chat-controller updates pinned zone after sub-turn shrink", async () => { - (globalThis as any)[Symbol.for("@gsd/pi-coding-agent:theme")] = { + (globalThis as any)[Symbol.for("@sf-run/pi-coding-agent:theme")] = { fg: (_key: string, text: string) => text, bg: (_key: string, text: string) => text, bold: (text: string) => text, diff --git a/packages/pi-coding-agent/src/core/compaction-orchestrator.ts b/packages/pi-coding-agent/src/core/compaction-orchestrator.ts index c17de356c..2c3e2dab4 100644 --- a/packages/pi-coding-agent/src/core/compaction-orchestrator.ts +++ b/packages/pi-coding-agent/src/core/compaction-orchestrator.ts @@ -9,9 +9,9 @@ * - Branch summarization abort coordination */ -import type { Agent } from "@gsd/pi-agent-core"; -import type { AssistantMessage, Model } from "@gsd/pi-ai"; -import { isContextOverflow } from "@gsd/pi-ai"; +import type { Agent } from "@sf-run/pi-agent-core"; +import type { AssistantMessage, Model } from "@sf-run/pi-ai"; +import { isContextOverflow } from "@sf-run/pi-ai"; import { type CompactionResult, calculateContextTokens, diff --git a/packages/pi-coding-agent/src/core/compaction-utils.test.ts b/packages/pi-coding-agent/src/core/compaction-utils.test.ts index 87fd3d3fb..4b1931c4b 100644 --- a/packages/pi-coding-agent/src/core/compaction-utils.test.ts +++ b/packages/pi-coding-agent/src/core/compaction-utils.test.ts @@ -1,7 +1,7 @@ import assert from "node:assert/strict"; import test from "node:test"; -import type { Message } from "@gsd/pi-ai"; +import type { Message } from "@sf-run/pi-ai"; import { serializeConversation } from "./compaction/index.js"; diff --git a/packages/pi-coding-agent/src/core/compaction/branch-summarization.ts b/packages/pi-coding-agent/src/core/compaction/branch-summarization.ts index cf9c8bc01..c9cffec1d 100644 --- a/packages/pi-coding-agent/src/core/compaction/branch-summarization.ts +++ b/packages/pi-coding-agent/src/core/compaction/branch-summarization.ts @@ -5,9 +5,9 @@ * a summary of the branch being left so context isn't lost. */ -import type { AgentMessage } from "@gsd/pi-agent-core"; -import type { Model } from "@gsd/pi-ai"; -import { completeSimple } from "@gsd/pi-ai"; +import type { AgentMessage } from "@sf-run/pi-agent-core"; +import type { Model } from "@sf-run/pi-ai"; +import { completeSimple } from "@sf-run/pi-ai"; import { COMPACTION_RESERVE_TOKENS } from "../constants.js"; import { convertToLlm } from "../messages.js"; import type { ReadonlySessionManager, SessionEntry } from "../session-manager.js"; diff --git a/packages/pi-coding-agent/src/core/compaction/compaction.test.ts b/packages/pi-coding-agent/src/core/compaction/compaction.test.ts index 1fb5a2db2..25389eb75 100644 --- a/packages/pi-coding-agent/src/core/compaction/compaction.test.ts +++ b/packages/pi-coding-agent/src/core/compaction/compaction.test.ts @@ -6,8 +6,8 @@ import assert from "node:assert/strict"; import { describe, it, mock } from "node:test"; -import type { AgentMessage } from "@gsd/pi-agent-core"; -import type { Model, AssistantMessage } from "@gsd/pi-ai"; +import type { AgentMessage } from "@sf-run/pi-agent-core"; +import type { Model, AssistantMessage } from "@sf-run/pi-ai"; import { generateSummary, estimateTokens, chunkMessages } from "./compaction.js"; diff --git a/packages/pi-coding-agent/src/core/compaction/compaction.ts b/packages/pi-coding-agent/src/core/compaction/compaction.ts index cd3183277..6dcab68cc 100644 --- a/packages/pi-coding-agent/src/core/compaction/compaction.ts +++ b/packages/pi-coding-agent/src/core/compaction/compaction.ts @@ -5,9 +5,9 @@ * and after compaction the session is reloaded. */ -import type { AgentMessage } from "@gsd/pi-agent-core"; -import type { AssistantMessage, Model, Usage } from "@gsd/pi-ai"; -import { completeSimple } from "@gsd/pi-ai"; +import type { AgentMessage } from "@sf-run/pi-agent-core"; +import type { AssistantMessage, Model, Usage } from "@sf-run/pi-ai"; +import { completeSimple } from "@sf-run/pi-ai"; import { COMPACTION_KEEP_RECENT_TOKENS, COMPACTION_RESERVE_TOKENS } from "../constants.js"; import { convertToLlm } from "../messages.js"; import type { CompactionEntry, SessionEntry } from "../session-manager.js"; diff --git a/packages/pi-coding-agent/src/core/compaction/utils.ts b/packages/pi-coding-agent/src/core/compaction/utils.ts index 95cf42555..10893c90c 100644 --- a/packages/pi-coding-agent/src/core/compaction/utils.ts +++ b/packages/pi-coding-agent/src/core/compaction/utils.ts @@ -2,8 +2,8 @@ * Shared utilities for compaction and branch summarization. */ -import type { AgentMessage } from "@gsd/pi-agent-core"; -import type { Message } from "@gsd/pi-ai"; +import type { AgentMessage } from "@sf-run/pi-agent-core"; +import type { Message } from "@sf-run/pi-ai"; import { TOOL_RESULT_MAX_CHARS } from "../constants.js"; import { createBranchSummaryMessage, diff --git a/packages/pi-coding-agent/src/core/defaults.ts b/packages/pi-coding-agent/src/core/defaults.ts index 61ee10dd3..029209cb0 100644 --- a/packages/pi-coding-agent/src/core/defaults.ts +++ b/packages/pi-coding-agent/src/core/defaults.ts @@ -1,3 +1,3 @@ -import type { ThinkingLevel } from "@gsd/pi-agent-core"; +import type { ThinkingLevel } from "@sf-run/pi-agent-core"; export const DEFAULT_THINKING_LEVEL: ThinkingLevel = "medium"; diff --git a/packages/pi-coding-agent/src/core/export-html/index.ts b/packages/pi-coding-agent/src/core/export-html/index.ts index 4130d4e6b..a6e58faf5 100644 --- a/packages/pi-coding-agent/src/core/export-html/index.ts +++ b/packages/pi-coding-agent/src/core/export-html/index.ts @@ -1,4 +1,4 @@ -import type { AgentState } from "@gsd/pi-agent-core"; +import type { AgentState } from "@sf-run/pi-agent-core"; import { existsSync, readFileSync, writeFileSync } from "fs"; import { basename, join } from "path"; import { APP_NAME, getExportTemplateDir } from "../../config.js"; diff --git a/packages/pi-coding-agent/src/core/export-html/tool-renderer.ts b/packages/pi-coding-agent/src/core/export-html/tool-renderer.ts index c4b4fc099..e558353ee 100644 --- a/packages/pi-coding-agent/src/core/export-html/tool-renderer.ts +++ b/packages/pi-coding-agent/src/core/export-html/tool-renderer.ts @@ -5,7 +5,7 @@ * and converting the ANSI output to HTML. */ -import type { ImageContent, TextContent } from "@gsd/pi-ai"; +import type { ImageContent, TextContent } from "@sf-run/pi-ai"; import type { Theme } from "../../modes/interactive/theme/theme.js"; import type { ToolDefinition } from "../extensions/types.js"; import { ansiLinesToHtml } from "./ansi-to-html.js"; diff --git a/packages/pi-coding-agent/src/core/extensions/loader.ts b/packages/pi-coding-agent/src/core/extensions/loader.ts index 016f05448..a0d63879b 100644 --- a/packages/pi-coding-agent/src/core/extensions/loader.ts +++ b/packages/pi-coding-agent/src/core/extensions/loader.ts @@ -10,11 +10,11 @@ import * as os from "node:os"; import * as path from "node:path"; import { fileURLToPath } from "node:url"; import { createJiti } from "@mariozechner/jiti"; -import * as _bundledPiAgentCore from "@gsd/pi-agent-core"; -import * as _bundledPiAi from "@gsd/pi-ai"; -import * as _bundledPiAiOauth from "@gsd/pi-ai/oauth"; -import type { KeyId } from "@gsd/pi-tui"; -import * as _bundledPiTui from "@gsd/pi-tui"; +import * as _bundledPiAgentCore from "@sf-run/pi-agent-core"; +import * as _bundledPiAi from "@sf-run/pi-ai"; +import * as _bundledPiAiOauth from "@sf-run/pi-ai/oauth"; +import type { KeyId } from "@sf-run/pi-tui"; +import * as _bundledPiTui from "@sf-run/pi-tui"; // Static imports of packages that extensions may use. // These MUST be static so Bun bundles them into the compiled binary. // The virtualModules option then makes them available to extensions. @@ -31,7 +31,7 @@ import * as _bundledMcpServerStreamableHttp from "@modelcontextprotocol/sdk/serv import * as _bundledMcpTypes from "@modelcontextprotocol/sdk/types.js"; import { getAgentDir, isBunBinary } from "../../config.js"; // NOTE: This import works because loader.ts exports are NOT re-exported from index.ts, -// avoiding a circular dependency. Extensions can import from @gsd/pi-coding-agent. +// avoiding a circular dependency. Extensions can import from "@sf-run/pi-coding-agent. import * as _bundledPiCodingAgent from "../../index.js"; import { createEventBus, type EventBus } from "../event-bus.js"; import type { ExecOptions } from "../exec.js"; @@ -58,11 +58,11 @@ import type { */ const STATIC_BUNDLED_MODULES: Record = { "@sinclair/typebox": _bundledTypebox, - "@gsd/pi-agent-core": _bundledPiAgentCore, - "@gsd/pi-tui": _bundledPiTui, - "@gsd/pi-ai": _bundledPiAi, - "@gsd/pi-ai/oauth": _bundledPiAiOauth, - "@gsd/pi-coding-agent": _bundledPiCodingAgent, + "@sf-run/pi-agent-core": _bundledPiAgentCore, + "@sf-run/pi-tui": _bundledPiTui, + "@sf-run/pi-ai": _bundledPiAi, + "@sf-run/pi-ai/oauth": _bundledPiAiOauth, + "@sf-run/pi-coding-agent": _bundledPiCodingAgent, "yaml": _bundledYaml, "@modelcontextprotocol/sdk/client": _bundledMcpClient, "@modelcontextprotocol/sdk/client/stdio": _bundledMcpStdio, @@ -324,19 +324,19 @@ function getAliases(): Record { // Auto-discovered subpath exports (lowest priority — overridden by manual entries below) ...autoDiscovered, // Manual entries for workspace packages and packages needing special resolution - "@gsd/pi-coding-agent": packageIndex, - "@gsd/pi-agent-core": resolveWorkspaceOrImport("agent/dist/index.js", "@gsd/pi-agent-core"), - "@gsd/pi-tui": resolveWorkspaceOrImport("tui/dist/index.js", "@gsd/pi-tui"), - "@gsd/pi-ai": resolveWorkspaceOrImport("ai/dist/index.js", "@gsd/pi-ai"), - "@gsd/pi-ai/oauth": resolveWorkspaceOrImport("ai/dist/oauth.js", "@gsd/pi-ai/oauth"), + "@sf-run/pi-coding-agent": packageIndex, + "@sf-run/pi-agent-core": resolveWorkspaceOrImport("agent/dist/index.js", "@sf-run/pi-agent-core"), + "@sf-run/pi-tui": resolveWorkspaceOrImport("tui/dist/index.js", "@sf-run/pi-tui"), + "@sf-run/pi-ai": resolveWorkspaceOrImport("ai/dist/index.js", "@sf-run/pi-ai"), + "@sf-run/pi-ai/oauth": resolveWorkspaceOrImport("ai/dist/oauth.js", "@sf-run/pi-ai/oauth"), "@sinclair/typebox": typeboxRoot, "yaml": yamlRoot, // Aliases for external PI ecosystem packages that import from the original scope "@mariozechner/pi-coding-agent": packageIndex, - "@mariozechner/pi-agent-core": resolveWorkspaceOrImport("agent/dist/index.js", "@gsd/pi-agent-core"), - "@mariozechner/pi-tui": resolveWorkspaceOrImport("tui/dist/index.js", "@gsd/pi-tui"), - "@mariozechner/pi-ai": resolveWorkspaceOrImport("ai/dist/index.js", "@gsd/pi-ai"), - "@mariozechner/pi-ai/oauth": resolveWorkspaceOrImport("ai/dist/oauth.js", "@gsd/pi-ai/oauth"), + "@mariozechner/pi-agent-core": resolveWorkspaceOrImport("agent/dist/index.js", "@sf-run/pi-agent-core"), + "@mariozechner/pi-tui": resolveWorkspaceOrImport("tui/dist/index.js", "@sf-run/pi-tui"), + "@mariozechner/pi-ai": resolveWorkspaceOrImport("ai/dist/index.js", "@sf-run/pi-ai"), + "@mariozechner/pi-ai/oauth": resolveWorkspaceOrImport("ai/dist/oauth.js", "@sf-run/pi-ai/oauth"), }; return _aliases; @@ -638,7 +638,7 @@ export function containsTypeScriptSyntax(source: string): boolean { * Shared jiti instance for loading extension modules. * * Before this fix (#2108), each extension created a NEW jiti instance with - * `moduleCache: false`, causing shared dependencies (e.g. @gsd/pi-agent-core) + * `moduleCache: false`, causing shared dependencies (e.g. @sf-run/pi-agent-core) * to be recompiled for every extension — turning a ~3s parallel load into a * ~15-30s serial compilation bottleneck. * diff --git a/packages/pi-coding-agent/src/core/extensions/runner.ts b/packages/pi-coding-agent/src/core/extensions/runner.ts index 0b0f6114b..8961478fc 100644 --- a/packages/pi-coding-agent/src/core/extensions/runner.ts +++ b/packages/pi-coding-agent/src/core/extensions/runner.ts @@ -2,9 +2,9 @@ * Extension runner - executes extensions and manages their lifecycle. */ -import type { AgentMessage } from "@gsd/pi-agent-core"; -import type { ImageContent, Model } from "@gsd/pi-ai"; -import type { KeyId } from "@gsd/pi-tui"; +import type { AgentMessage } from "@sf-run/pi-agent-core"; +import type { ImageContent, Model } from "@sf-run/pi-ai"; +import type { KeyId } from "@sf-run/pi-tui"; import { type Theme, theme } from "../../modes/interactive/theme/theme.js"; import type { ResourceDiagnostic } from "../diagnostics.js"; import type { KeyAction, KeybindingsConfig } from "../keybindings.js"; diff --git a/packages/pi-coding-agent/src/core/extensions/types.ts b/packages/pi-coding-agent/src/core/extensions/types.ts index 5fea6389a..f104aa458 100644 --- a/packages/pi-coding-agent/src/core/extensions/types.ts +++ b/packages/pi-coding-agent/src/core/extensions/types.ts @@ -13,7 +13,7 @@ import type { AgentToolResult, AgentToolUpdateCallback, ThinkingLevel, -} from "@gsd/pi-agent-core"; +} from "@sf-run/pi-agent-core"; import type { Api, AssistantMessageEvent, @@ -26,7 +26,7 @@ import type { SimpleStreamOptions, TextContent, ToolResultMessage, -} from "@gsd/pi-ai"; +} from "@sf-run/pi-ai"; import type { AutocompleteItem, Component, @@ -36,7 +36,7 @@ import type { OverlayHandle, OverlayOptions, TUI, -} from "@gsd/pi-tui"; +} from "@sf-run/pi-tui"; import type { Static, TSchema } from "@sinclair/typebox"; import type { Theme } from "../../modes/interactive/theme/theme.js"; import type { BashResult } from "../bash-executor.js"; @@ -194,12 +194,12 @@ export interface ExtensionUIContext { * - `keybindings`: KeybindingsManager for app-level keybindings * * For full app keybinding support (escape, ctrl+d, model switching, etc.), - * extend `CustomEditor` from `@gsd/pi-coding-agent` and call + * extend `CustomEditor` from `@sf-run/pi-coding-agent` and call * `super.handleInput(data)` for keys you don't handle. * * @example * ```ts - * import { CustomEditor } from "@gsd/pi-coding-agent"; + * import { CustomEditor } from "@sf-run/pi-coding-agent"; * * class VimEditor extends CustomEditor { * private mode: "normal" | "insert" = "insert"; diff --git a/packages/pi-coding-agent/src/core/extensions/wrapper.ts b/packages/pi-coding-agent/src/core/extensions/wrapper.ts index d328f7610..63fedf4c7 100644 --- a/packages/pi-coding-agent/src/core/extensions/wrapper.ts +++ b/packages/pi-coding-agent/src/core/extensions/wrapper.ts @@ -2,7 +2,7 @@ * Tool wrappers for extensions. */ -import type { AgentTool, AgentToolUpdateCallback } from "@gsd/pi-agent-core"; +import type { AgentTool, AgentToolUpdateCallback } from "@sf-run/pi-agent-core"; import type { ExtensionRunner } from "./runner.js"; import type { RegisteredTool, ToolCallEventResult } from "./types.js"; diff --git a/packages/pi-coding-agent/src/core/fallback-resolver.test.ts b/packages/pi-coding-agent/src/core/fallback-resolver.test.ts index f454d1c8e..fafadc0a4 100644 --- a/packages/pi-coding-agent/src/core/fallback-resolver.test.ts +++ b/packages/pi-coding-agent/src/core/fallback-resolver.test.ts @@ -4,7 +4,7 @@ import { describe, it, beforeEach, mock } from "node:test"; import assert from "node:assert/strict"; import { FallbackResolver } from "./fallback-resolver.js"; -import type { Api, Model } from "@gsd/pi-ai"; +import type { Api, Model } from "@sf-run/pi-ai"; import type { AuthStorage } from "./auth-storage.js"; import type { ModelRegistry } from "./model-registry.js"; import type { FallbackChainEntry, SettingsManager } from "./settings-manager.js"; diff --git a/packages/pi-coding-agent/src/core/fallback-resolver.ts b/packages/pi-coding-agent/src/core/fallback-resolver.ts index e390f2038..9a8fcbea2 100644 --- a/packages/pi-coding-agent/src/core/fallback-resolver.ts +++ b/packages/pi-coding-agent/src/core/fallback-resolver.ts @@ -9,7 +9,7 @@ * restoration: checking if a higher-priority provider has recovered before each request. */ -import type { Api, Model } from "@gsd/pi-ai"; +import type { Api, Model } from "@sf-run/pi-ai"; import type { AuthStorage, UsageLimitErrorType } from "./auth-storage.js"; import type { ModelRegistry } from "./model-registry.js"; import type { FallbackChainEntry, SettingsManager } from "./settings-manager.js"; diff --git a/packages/pi-coding-agent/src/core/image-overflow-recovery.test.ts b/packages/pi-coding-agent/src/core/image-overflow-recovery.test.ts index de075c280..4067389cb 100644 --- a/packages/pi-coding-agent/src/core/image-overflow-recovery.test.ts +++ b/packages/pi-coding-agent/src/core/image-overflow-recovery.test.ts @@ -5,7 +5,7 @@ import { MANY_IMAGE_MAX_DIMENSION, downsizeConversationImages, } from "./image-overflow-recovery.js"; -import type { Message } from "@gsd/pi-ai"; +import type { Message } from "@sf-run/pi-ai"; // ─── isImageDimensionError ──────────────────────────────────────────────────── diff --git a/packages/pi-coding-agent/src/core/image-overflow-recovery.ts b/packages/pi-coding-agent/src/core/image-overflow-recovery.ts index 3573514e4..7b5227998 100644 --- a/packages/pi-coding-agent/src/core/image-overflow-recovery.ts +++ b/packages/pi-coding-agent/src/core/image-overflow-recovery.ts @@ -7,10 +7,10 @@ * recovers by stripping older images from the conversation history, preserving * the most recent ones to maintain session continuity. * - * @see https://github.com/gsd-build/gsd-2/issues/2874 + * @see https://github.com/singularity-forge/sf-run/issues/2874 */ -import type { Message, ImageContent, TextContent } from "@gsd/pi-ai"; +import type { Message, ImageContent, TextContent } from "@sf-run/pi-ai"; /** * Maximum image dimension (px) that the Anthropic API allows in many-image diff --git a/packages/pi-coding-agent/src/core/keybindings.ts b/packages/pi-coding-agent/src/core/keybindings.ts index ed69936f4..adccddd23 100644 --- a/packages/pi-coding-agent/src/core/keybindings.ts +++ b/packages/pi-coding-agent/src/core/keybindings.ts @@ -6,7 +6,7 @@ import { type KeyId, matchesKey, setEditorKeybindings, -} from "@gsd/pi-tui"; +} from "@sf-run/pi-tui"; import { existsSync, readFileSync } from "fs"; import { join } from "path"; import { getAgentDir } from "../config.js"; diff --git a/packages/pi-coding-agent/src/core/lsp/index.ts b/packages/pi-coding-agent/src/core/lsp/index.ts index bd2718634..a0c010ddd 100644 --- a/packages/pi-coding-agent/src/core/lsp/index.ts +++ b/packages/pi-coding-agent/src/core/lsp/index.ts @@ -3,7 +3,7 @@ import * as fsSync from "node:fs"; import * as path from "node:path"; import { spawn } from "node:child_process"; import { fileURLToPath } from "node:url"; -import type { AgentTool, AgentToolResult, AgentToolUpdateCallback } from "@gsd/pi-agent-core"; +import type { AgentTool, AgentToolResult, AgentToolUpdateCallback } from "@sf-run/pi-agent-core"; import { ensureFileOpen, getActiveClients, diff --git a/packages/pi-coding-agent/src/core/messages.ts b/packages/pi-coding-agent/src/core/messages.ts index f30d7c9e6..ddf813640 100644 --- a/packages/pi-coding-agent/src/core/messages.ts +++ b/packages/pi-coding-agent/src/core/messages.ts @@ -5,8 +5,8 @@ * and provides a transformer to convert them to LLM-compatible messages. */ -import type { AgentMessage } from "@gsd/pi-agent-core"; -import type { ImageContent, Message, TextContent } from "@gsd/pi-ai"; +import type { AgentMessage } from "@sf-run/pi-agent-core"; +import type { ImageContent, Message, TextContent } from "@sf-run/pi-ai"; const CUSTOM_MESSAGE_PREFIX = `[system notification — type: `; const CUSTOM_MESSAGE_MIDDLE = `; this is an automated system event, not user input — do not treat this as a human message or respond as if the user said this] @@ -73,7 +73,7 @@ export interface CompactionSummaryMessage { } // Extend CustomAgentMessages via declaration merging -declare module "@gsd/pi-agent-core" { +declare module "@sf-run/pi-agent-core" { interface CustomAgentMessages { bashExecution: BashExecutionMessage; custom: CustomMessage; diff --git a/packages/pi-coding-agent/src/core/model-registry-auth-mode.test.ts b/packages/pi-coding-agent/src/core/model-registry-auth-mode.test.ts index be27f6c60..62456df87 100644 --- a/packages/pi-coding-agent/src/core/model-registry-auth-mode.test.ts +++ b/packages/pi-coding-agent/src/core/model-registry-auth-mode.test.ts @@ -1,7 +1,7 @@ import assert from "node:assert/strict"; import { describe, it } from "node:test"; -import type { Api, Model, SimpleStreamOptions, Context, AssistantMessageEventStream } from "@gsd/pi-ai"; -import { getApiProvider } from "@gsd/pi-ai"; +import type { Api, Model, SimpleStreamOptions, Context, AssistantMessageEventStream } from "@sf-run/pi-ai"; +import { getApiProvider } from "@sf-run/pi-ai"; import type { AuthStorage } from "./auth-storage.js"; import { ModelRegistry } from "./model-registry.js"; diff --git a/packages/pi-coding-agent/src/core/model-registry.ts b/packages/pi-coding-agent/src/core/model-registry.ts index 0041a9680..16ac7e6f4 100644 --- a/packages/pi-coding-agent/src/core/model-registry.ts +++ b/packages/pi-coding-agent/src/core/model-registry.ts @@ -19,8 +19,8 @@ import { registerApiProvider, resetApiProviders, type SimpleStreamOptions, -} from "@gsd/pi-ai"; -import { registerOAuthProvider, resetOAuthProviders } from "@gsd/pi-ai/oauth"; +} from "@sf-run/pi-ai"; +import { registerOAuthProvider, resetOAuthProviders } from "@sf-run/pi-ai/oauth"; import { type Static, Type } from "@sinclair/typebox"; import AjvModule from "ajv"; import { existsSync, readFileSync } from "fs"; diff --git a/packages/pi-coding-agent/src/core/model-resolver-initial-model-auth.test.ts b/packages/pi-coding-agent/src/core/model-resolver-initial-model-auth.test.ts index e7d5fb46a..6c3283e5a 100644 --- a/packages/pi-coding-agent/src/core/model-resolver-initial-model-auth.test.ts +++ b/packages/pi-coding-agent/src/core/model-resolver-initial-model-auth.test.ts @@ -1,6 +1,6 @@ import assert from "node:assert/strict"; import { describe, it } from "node:test"; -import type { Api, Model } from "@gsd/pi-ai"; +import type { Api, Model } from "@sf-run/pi-ai"; import type { ModelRegistry } from "./model-registry.js"; import { findInitialModel } from "./model-resolver.js"; diff --git a/packages/pi-coding-agent/src/core/model-resolver.ts b/packages/pi-coding-agent/src/core/model-resolver.ts index 7b608fad5..5e99c6ef5 100644 --- a/packages/pi-coding-agent/src/core/model-resolver.ts +++ b/packages/pi-coding-agent/src/core/model-resolver.ts @@ -2,8 +2,8 @@ * Model resolution, scoping, and initial selection */ -import type { ThinkingLevel } from "@gsd/pi-agent-core"; -import { type Api, type Model, modelsAreEqual } from "@gsd/pi-ai"; +import type { ThinkingLevel } from "@sf-run/pi-agent-core"; +import { type Api, type Model, modelsAreEqual } from "@sf-run/pi-ai"; import chalk from "chalk"; import { minimatch } from "minimatch"; import { isValidThinkingLevel } from "../cli/args.js"; diff --git a/packages/pi-coding-agent/src/core/package-manager.ts b/packages/pi-coding-agent/src/core/package-manager.ts index e07b28c4e..6f73ecdc9 100644 --- a/packages/pi-coding-agent/src/core/package-manager.ts +++ b/packages/pi-coding-agent/src/core/package-manager.ts @@ -1298,18 +1298,64 @@ export class DefaultPackageManager implements PackageManager { if (scope === "project") { return join(this.cwd, CONFIG_DIR_NAME, "npm"); } - return join(this.getGlobalNpmRoot(), ".."); + return join(this.getGlobalPackageRoot(), ".."); } private getGlobalNpmRoot(): string { if (this.globalNpmRoot) { return this.globalNpmRoot; } - const result = this.runCommandSync("npm", ["root", "-g"]); - this.globalNpmRoot = result.trim(); + const configuredRoot = process.env.PI_GLOBAL_PACKAGE_ROOT?.trim(); + if (configuredRoot) { + this.globalNpmRoot = configuredRoot; + return this.globalNpmRoot; + } + const discoveredRoot = this.discoverGlobalPackageRoot(); + if (discoveredRoot) { + this.globalNpmRoot = discoveredRoot; + return this.globalNpmRoot; + } + this.globalNpmRoot = join(homedir(), ".npm-global", "lib", "node_modules"); return this.globalNpmRoot; } + private getGlobalPackageRoot(): string { + return this.getGlobalNpmRoot(); + } + + private discoverGlobalPackageRoot(): string | undefined { + const bunPath = this.getExecutablePath("bun"); + if (bunPath) { + const bunRoot = join(dirname(dirname(bunPath)), "install", "global", "node_modules"); + if (existsSync(bunRoot)) { + return bunRoot; + } + + const bunReportedRoot = this.tryRunCommandSync("bun", ["pm", "bin"]); + if (bunReportedRoot) { + const normalized = bunReportedRoot.trim(); + if (normalized) { + const candidate = normalized.endsWith("node_modules") + ? normalized + : join(normalized, "..", "node_modules"); + if (existsSync(candidate)) { + return candidate; + } + } + } + } + + const npmRoot = this.tryRunCommandSync("npm", ["root", "-g"]); + if (npmRoot) { + const normalized = npmRoot.trim(); + if (normalized) { + return normalized; + } + } + + return undefined; + } + private getNpmInstallPath(source: NpmSource, scope: SourceScope): string { if (scope === "temporary") { return join(this.getTemporaryDir("npm"), "node_modules", source.name); @@ -1842,4 +1888,35 @@ export class DefaultPackageManager implements PackageManager { } return (result.stdout || result.stderr || "").trim(); } + + private tryRunCommandSync(command: string, args: string[]): string | undefined { + try { + return this.runCommandSync(command, args); + } catch { + return undefined; + } + } + + private getExecutablePath(command: string): string | undefined { + const pathValue = process.env.PATH; + if (!pathValue) return undefined; + + const pathExt = + process.platform === "win32" + ? (process.env.PATHEXT?.split(";").filter(Boolean) ?? [".EXE", ".CMD", ".BAT", ".COM"]) + : [""]; + const separator = process.platform === "win32" ? ";" : ":"; + + for (const entry of pathValue.split(separator)) { + if (!entry) continue; + for (const ext of pathExt) { + const candidate = join(entry, `${command}${ext}`); + if (existsSync(candidate)) { + return candidate; + } + } + } + + return undefined; + } } diff --git a/packages/pi-coding-agent/src/core/retry-handler.test.ts b/packages/pi-coding-agent/src/core/retry-handler.test.ts index c8d04b87f..bdfa0c2b2 100644 --- a/packages/pi-coding-agent/src/core/retry-handler.test.ts +++ b/packages/pi-coding-agent/src/core/retry-handler.test.ts @@ -9,7 +9,7 @@ import { describe, it, beforeEach, mock, type Mock } from "node:test"; import assert from "node:assert/strict"; import { RetryHandler, type RetryHandlerDeps } from "./retry-handler.js"; -import type { Api, AssistantMessage, Model } from "@gsd/pi-ai"; +import type { Api, AssistantMessage, Model } from "@sf-run/pi-ai"; import type { FallbackResolver } from "./fallback-resolver.js"; import type { ModelRegistry } from "./model-registry.js"; import type { SettingsManager } from "./settings-manager.js"; diff --git a/packages/pi-coding-agent/src/core/retry-handler.ts b/packages/pi-coding-agent/src/core/retry-handler.ts index b85133915..b9f56141f 100644 --- a/packages/pi-coding-agent/src/core/retry-handler.ts +++ b/packages/pi-coding-agent/src/core/retry-handler.ts @@ -9,9 +9,9 @@ * Context overflow errors are NOT handled here (see compaction). */ -import type { Agent } from "@gsd/pi-agent-core"; -import type { AssistantMessage, Model } from "@gsd/pi-ai"; -import { isContextOverflow } from "@gsd/pi-ai"; +import type { Agent } from "@sf-run/pi-agent-core"; +import type { AssistantMessage, Model } from "@sf-run/pi-ai"; +import { isContextOverflow } from "@sf-run/pi-ai"; import type { UsageLimitErrorType } from "./auth-storage.js"; import type { FallbackResolver } from "./fallback-resolver.js"; import type { ModelRegistry } from "./model-registry.js"; diff --git a/packages/pi-coding-agent/src/core/sdk.ts b/packages/pi-coding-agent/src/core/sdk.ts index 75eb11c4f..66f3d39c6 100644 --- a/packages/pi-coding-agent/src/core/sdk.ts +++ b/packages/pi-coding-agent/src/core/sdk.ts @@ -19,8 +19,8 @@ export class CredentialCooldownError extends Error { this.retryAfterMs = retryAfterMs; } } -import { Agent, type AgentMessage, type ThinkingLevel } from "@gsd/pi-agent-core"; -import type { Message, Model } from "@gsd/pi-ai"; +import { Agent, type AgentMessage, type ThinkingLevel } from "@sf-run/pi-agent-core"; +import type { Message, Model } from "@sf-run/pi-ai"; import { getAgentDir, getDocsPath } from "../config.js"; import { AgentSession } from "./agent-session.js"; import { AuthStorage } from "./auth-storage.js"; @@ -177,7 +177,7 @@ function getDefaultAgentDir(): string { * const { session } = await createAgentSession(); * * // With explicit model - * import { getModel } from '@gsd/pi-ai'; + * import { getModel } from '@sf-run/pi-ai'; * const { session } = await createAgentSession({ * model: getModel('anthropic', 'claude-opus-4-5'), * thinkingLevel: 'high', diff --git a/packages/pi-coding-agent/src/core/session-manager.ts b/packages/pi-coding-agent/src/core/session-manager.ts index 86e80525e..ff84a2dd1 100644 --- a/packages/pi-coding-agent/src/core/session-manager.ts +++ b/packages/pi-coding-agent/src/core/session-manager.ts @@ -1,5 +1,5 @@ -import type { AgentMessage } from "@gsd/pi-agent-core"; -import type { ImageContent, Message, TextContent } from "@gsd/pi-ai"; +import type { AgentMessage } from "@sf-run/pi-agent-core"; +import type { ImageContent, Message, TextContent } from "@sf-run/pi-ai"; import { randomUUID } from "crypto"; import { appendFileSync, diff --git a/packages/pi-coding-agent/src/core/settings-manager.ts b/packages/pi-coding-agent/src/core/settings-manager.ts index de75daa0f..fb4cad082 100644 --- a/packages/pi-coding-agent/src/core/settings-manager.ts +++ b/packages/pi-coding-agent/src/core/settings-manager.ts @@ -1,4 +1,4 @@ -import type { Transport } from "@gsd/pi-ai"; +import type { Transport } from "@sf-run/pi-ai"; import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"; import { dirname, join } from "path"; import lockfile from "proper-lockfile"; diff --git a/packages/pi-coding-agent/src/core/skill-tool.test.ts b/packages/pi-coding-agent/src/core/skill-tool.test.ts index e8a3b2964..5a995fb96 100644 --- a/packages/pi-coding-agent/src/core/skill-tool.test.ts +++ b/packages/pi-coding-agent/src/core/skill-tool.test.ts @@ -4,7 +4,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, it } from "node:test"; -import { Agent } from "@gsd/pi-agent-core"; +import { Agent } from "@sf-run/pi-agent-core"; import { AuthStorage } from "./auth-storage.js"; import { AgentSession } from "./agent-session.js"; import { ModelRegistry } from "./model-registry.js"; diff --git a/packages/pi-coding-agent/src/core/tools/bash-spawn-windows.test.ts b/packages/pi-coding-agent/src/core/tools/bash-spawn-windows.test.ts index 9247addf2..5579322dd 100644 --- a/packages/pi-coding-agent/src/core/tools/bash-spawn-windows.test.ts +++ b/packages/pi-coding-agent/src/core/tools/bash-spawn-windows.test.ts @@ -13,7 +13,7 @@ * with `detached: process.platform !== "win32"` (process-manager.ts); * this test ensures all other spawn sites are aligned. * - * See: gsd-build/gsd-2#XXXX + * See: singularity-forge/sf-run#XXXX */ import test from "node:test"; diff --git a/packages/pi-coding-agent/src/core/tools/bash.ts b/packages/pi-coding-agent/src/core/tools/bash.ts index eccda574b..72831fd59 100644 --- a/packages/pi-coding-agent/src/core/tools/bash.ts +++ b/packages/pi-coding-agent/src/core/tools/bash.ts @@ -3,7 +3,7 @@ import { createWriteStream, existsSync } from "node:fs"; import { createRequire } from "node:module"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import type { AgentTool } from "@gsd/pi-agent-core"; +import type { AgentTool } from "@sf-run/pi-agent-core"; import { type Static, Type } from "@sinclair/typebox"; import { spawn } from "child_process"; import { getShellConfig, getShellEnv, killProcessTree, sanitizeCommand } from "../../utils/shell.js"; diff --git a/packages/pi-coding-agent/src/core/tools/edit.ts b/packages/pi-coding-agent/src/core/tools/edit.ts index ff8b36f21..7dffcaf17 100644 --- a/packages/pi-coding-agent/src/core/tools/edit.ts +++ b/packages/pi-coding-agent/src/core/tools/edit.ts @@ -1,4 +1,4 @@ -import type { AgentTool } from "@gsd/pi-agent-core"; +import type { AgentTool } from "@sf-run/pi-agent-core"; import { type Static, Type } from "@sinclair/typebox"; import { constants } from "fs"; import { access as fsAccess, readFile as fsReadFile, writeFile as fsWriteFile } from "fs/promises"; diff --git a/packages/pi-coding-agent/src/core/tools/find.ts b/packages/pi-coding-agent/src/core/tools/find.ts index fc4d6be40..8a7cf8dba 100644 --- a/packages/pi-coding-agent/src/core/tools/find.ts +++ b/packages/pi-coding-agent/src/core/tools/find.ts @@ -1,5 +1,5 @@ -import type { AgentTool } from "@gsd/pi-agent-core"; -import { glob as nativeGlob } from "@gsd/native/glob"; +import type { AgentTool } from "@sf-run/pi-agent-core"; +import { glob as nativeGlob } from "@sf-run/native/glob"; import { type Static, Type } from "@sinclair/typebox"; import { existsSync } from "fs"; import path from "path"; diff --git a/packages/pi-coding-agent/src/core/tools/grep.ts b/packages/pi-coding-agent/src/core/tools/grep.ts index 6250a2601..3ed0c1384 100644 --- a/packages/pi-coding-agent/src/core/tools/grep.ts +++ b/packages/pi-coding-agent/src/core/tools/grep.ts @@ -1,5 +1,5 @@ import { createInterface } from "node:readline"; -import type { AgentTool } from "@gsd/pi-agent-core"; +import type { AgentTool } from "@sf-run/pi-agent-core"; import { type Static, Type } from "@sinclair/typebox"; import { spawn } from "child_process"; import { readFileSync, statSync } from "fs"; diff --git a/packages/pi-coding-agent/src/core/tools/hashline-edit.ts b/packages/pi-coding-agent/src/core/tools/hashline-edit.ts index 137eb9305..25839bcb4 100644 --- a/packages/pi-coding-agent/src/core/tools/hashline-edit.ts +++ b/packages/pi-coding-agent/src/core/tools/hashline-edit.ts @@ -4,7 +4,7 @@ * The model references lines by `LINE#ID` tags from read output. * Each tag uniquely identifies a line, so edits remain stable even when lines shift. */ -import type { AgentTool } from "@gsd/pi-agent-core"; +import type { AgentTool } from "@sf-run/pi-agent-core"; import { type Static, Type } from "@sinclair/typebox"; import { constants } from "fs"; import { access as fsAccess, readFile as fsReadFile, unlink as fsUnlink, writeFile as fsWriteFile } from "fs/promises"; diff --git a/packages/pi-coding-agent/src/core/tools/hashline-read.ts b/packages/pi-coding-agent/src/core/tools/hashline-read.ts index f7d944d14..53c9fb32b 100644 --- a/packages/pi-coding-agent/src/core/tools/hashline-read.ts +++ b/packages/pi-coding-agent/src/core/tools/hashline-read.ts @@ -8,8 +8,8 @@ * * These tags are used by the hashline_edit tool to address lines precisely. */ -import type { AgentTool } from "@gsd/pi-agent-core"; -import type { ImageContent, TextContent } from "@gsd/pi-ai"; +import type { AgentTool } from "@sf-run/pi-agent-core"; +import type { ImageContent, TextContent } from "@sf-run/pi-ai"; import { type Static, Type } from "@sinclair/typebox"; import { constants } from "fs"; import { access as fsAccess, readFile as fsReadFile } from "fs/promises"; diff --git a/packages/pi-coding-agent/src/core/tools/hashline.ts b/packages/pi-coding-agent/src/core/tools/hashline.ts index f9aa90e2b..506320189 100644 --- a/packages/pi-coding-agent/src/core/tools/hashline.ts +++ b/packages/pi-coding-agent/src/core/tools/hashline.ts @@ -15,7 +15,7 @@ * Adapted from Oh My Pi's hashline implementation for Node.js (no Bun dependency). */ -import { xxHash32 } from "@gsd/native/xxhash"; +import { xxHash32 } from "@sf-run/native/xxhash"; // ═══════════════════════════════════════════════════════════════════════════ // Hash Computation diff --git a/packages/pi-coding-agent/src/core/tools/index.ts b/packages/pi-coding-agent/src/core/tools/index.ts index 90a5a524c..1f0a187c3 100644 --- a/packages/pi-coding-agent/src/core/tools/index.ts +++ b/packages/pi-coding-agent/src/core/tools/index.ts @@ -120,7 +120,7 @@ export { resetToolCompatibilityRegistry, } from "./tool-compatibility-registry.js"; -import type { AgentTool } from "@gsd/pi-agent-core"; +import type { AgentTool } from "@sf-run/pi-agent-core"; import { type BashToolOptions, bashTool, createBashTool } from "./bash.js"; import { createEditTool, editTool } from "./edit.js"; import { createFindTool, findTool } from "./find.js"; diff --git a/packages/pi-coding-agent/src/core/tools/ls.ts b/packages/pi-coding-agent/src/core/tools/ls.ts index 4876e2155..ce1855471 100644 --- a/packages/pi-coding-agent/src/core/tools/ls.ts +++ b/packages/pi-coding-agent/src/core/tools/ls.ts @@ -1,4 +1,4 @@ -import type { AgentTool } from "@gsd/pi-agent-core"; +import type { AgentTool } from "@sf-run/pi-agent-core"; import { type Static, Type } from "@sinclair/typebox"; import { existsSync, readdirSync, statSync } from "fs"; import nodePath from "path"; diff --git a/packages/pi-coding-agent/src/core/tools/read.ts b/packages/pi-coding-agent/src/core/tools/read.ts index 309e43b57..c0cf311b9 100644 --- a/packages/pi-coding-agent/src/core/tools/read.ts +++ b/packages/pi-coding-agent/src/core/tools/read.ts @@ -1,5 +1,5 @@ -import type { AgentTool } from "@gsd/pi-agent-core"; -import type { ImageContent, TextContent } from "@gsd/pi-ai"; +import type { AgentTool } from "@sf-run/pi-agent-core"; +import type { ImageContent, TextContent } from "@sf-run/pi-ai"; import { type Static, Type } from "@sinclair/typebox"; import { constants } from "fs"; import { access as fsAccess, readFile as fsReadFile } from "fs/promises"; diff --git a/packages/pi-coding-agent/src/core/tools/spawn-shell-windows.test.ts b/packages/pi-coding-agent/src/core/tools/spawn-shell-windows.test.ts index a7929a1dd..213bd6bc5 100644 --- a/packages/pi-coding-agent/src/core/tools/spawn-shell-windows.test.ts +++ b/packages/pi-coding-agent/src/core/tools/spawn-shell-windows.test.ts @@ -10,7 +10,7 @@ * * This test structurally scans all spawn sites and verifies the guard is present. * - * Fixes: gsd-build/gsd-2#2854 + * Fixes: singularity-forge/sf-run#2854 */ import test from "node:test"; diff --git a/packages/pi-coding-agent/src/core/tools/write.ts b/packages/pi-coding-agent/src/core/tools/write.ts index 24c7be022..8f242979d 100644 --- a/packages/pi-coding-agent/src/core/tools/write.ts +++ b/packages/pi-coding-agent/src/core/tools/write.ts @@ -1,4 +1,4 @@ -import type { AgentTool } from "@gsd/pi-agent-core"; +import type { AgentTool } from "@sf-run/pi-agent-core"; import { type Static, Type } from "@sinclair/typebox"; import { mkdir as fsMkdir, writeFile as fsWriteFile } from "fs/promises"; import { dirname } from "path"; diff --git a/packages/pi-coding-agent/src/main.ts b/packages/pi-coding-agent/src/main.ts index eed983f4b..89391cba7 100644 --- a/packages/pi-coding-agent/src/main.ts +++ b/packages/pi-coding-agent/src/main.ts @@ -5,7 +5,7 @@ * createAgentSession() options. The SDK does the heavy lifting. */ -import { type ImageContent, modelsAreEqual, supportsXhigh } from "@gsd/pi-ai"; +import { type ImageContent, modelsAreEqual, supportsXhigh } from "@sf-run/pi-ai"; import chalk from "chalk"; import { createInterface } from "readline"; import { type Args, parseArgs, printHelp } from "./cli/args.js"; diff --git a/packages/pi-coding-agent/src/modes/interactive/components/armin.ts b/packages/pi-coding-agent/src/modes/interactive/components/armin.ts index 35a591c16..dca7b1851 100644 --- a/packages/pi-coding-agent/src/modes/interactive/components/armin.ts +++ b/packages/pi-coding-agent/src/modes/interactive/components/armin.ts @@ -2,7 +2,7 @@ * Armin says hi! A fun easter egg with animated XBM art. */ -import { type Component, type TUI, visibleWidth } from "@gsd/pi-tui"; +import { type Component, type TUI, visibleWidth } from "@sf-run/pi-tui"; import { theme } from "../theme/theme.js"; // XBM image: 31x36 pixels, LSB first, 1=background, 0=foreground diff --git a/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts b/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts index 1b32fa6c7..9de268928 100644 --- a/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts +++ b/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts @@ -1,5 +1,5 @@ -import type { AssistantMessage } from "@gsd/pi-ai"; -import { Container, Markdown, type MarkdownTheme, Spacer, Text } from "@gsd/pi-tui"; +import type { AssistantMessage } from "@sf-run/pi-ai"; +import { Container, Markdown, type MarkdownTheme, Spacer, Text } from "@sf-run/pi-tui"; import { getMarkdownTheme, theme } from "../theme/theme.js"; import { formatTimestamp, type TimestampFormat } from "./timestamp.js"; diff --git a/packages/pi-coding-agent/src/modes/interactive/components/bash-execution.ts b/packages/pi-coding-agent/src/modes/interactive/components/bash-execution.ts index b35855e0f..69f746ad3 100644 --- a/packages/pi-coding-agent/src/modes/interactive/components/bash-execution.ts +++ b/packages/pi-coding-agent/src/modes/interactive/components/bash-execution.ts @@ -2,7 +2,7 @@ * Component for displaying bash command execution with streaming output. */ -import { Container, Loader, Spacer, Text, type TUI } from "@gsd/pi-tui"; +import { Container, Loader, Spacer, Text, type TUI } from "@sf-run/pi-tui"; import stripAnsi from "strip-ansi"; import { DEFAULT_MAX_BYTES, diff --git a/packages/pi-coding-agent/src/modes/interactive/components/bordered-loader.ts b/packages/pi-coding-agent/src/modes/interactive/components/bordered-loader.ts index 9c4dae2d2..004f9d713 100644 --- a/packages/pi-coding-agent/src/modes/interactive/components/bordered-loader.ts +++ b/packages/pi-coding-agent/src/modes/interactive/components/bordered-loader.ts @@ -1,4 +1,4 @@ -import { CancellableLoader, Container, Loader, Spacer, Text, type TUI } from "@gsd/pi-tui"; +import { CancellableLoader, Container, Loader, Spacer, Text, type TUI } from "@sf-run/pi-tui"; import type { Theme } from "../theme/theme.js"; import { DynamicBorder } from "./dynamic-border.js"; import { keyHint } from "./keybinding-hints.js"; diff --git a/packages/pi-coding-agent/src/modes/interactive/components/branch-summary-message.ts b/packages/pi-coding-agent/src/modes/interactive/components/branch-summary-message.ts index 9c7ed9730..cab49db4d 100644 --- a/packages/pi-coding-agent/src/modes/interactive/components/branch-summary-message.ts +++ b/packages/pi-coding-agent/src/modes/interactive/components/branch-summary-message.ts @@ -1,4 +1,4 @@ -import { Box, Markdown, type MarkdownTheme, Spacer, Text } from "@gsd/pi-tui"; +import { Box, Markdown, type MarkdownTheme, Spacer, Text } from "@sf-run/pi-tui"; import type { BranchSummaryMessage } from "../../../core/messages.js"; import { getMarkdownTheme, theme } from "../theme/theme.js"; import { editorKey } from "./keybinding-hints.js"; diff --git a/packages/pi-coding-agent/src/modes/interactive/components/compaction-summary-message.ts b/packages/pi-coding-agent/src/modes/interactive/components/compaction-summary-message.ts index f7e68e259..8d481960d 100644 --- a/packages/pi-coding-agent/src/modes/interactive/components/compaction-summary-message.ts +++ b/packages/pi-coding-agent/src/modes/interactive/components/compaction-summary-message.ts @@ -1,4 +1,4 @@ -import { Box, Markdown, type MarkdownTheme, Spacer, Text } from "@gsd/pi-tui"; +import { Box, Markdown, type MarkdownTheme, Spacer, Text } from "@sf-run/pi-tui"; import type { CompactionSummaryMessage } from "../../../core/messages.js"; import { getMarkdownTheme, theme } from "../theme/theme.js"; import { editorKey } from "./keybinding-hints.js"; diff --git a/packages/pi-coding-agent/src/modes/interactive/components/config-selector.ts b/packages/pi-coding-agent/src/modes/interactive/components/config-selector.ts index befee7ca6..369482a9e 100644 --- a/packages/pi-coding-agent/src/modes/interactive/components/config-selector.ts +++ b/packages/pi-coding-agent/src/modes/interactive/components/config-selector.ts @@ -13,7 +13,7 @@ import { Spacer, truncateToWidth, visibleWidth, -} from "@gsd/pi-tui"; +} from "@sf-run/pi-tui"; import { CONFIG_DIR_NAME } from "../../../config.js"; import type { PathMetadata, ResolvedPaths, ResolvedResource } from "../../../core/package-manager.js"; import type { PackageSource, SettingsManager } from "../../../core/settings-manager.js"; diff --git a/packages/pi-coding-agent/src/modes/interactive/components/countdown-timer.ts b/packages/pi-coding-agent/src/modes/interactive/components/countdown-timer.ts index ef77320d3..ee51c8141 100644 --- a/packages/pi-coding-agent/src/modes/interactive/components/countdown-timer.ts +++ b/packages/pi-coding-agent/src/modes/interactive/components/countdown-timer.ts @@ -2,7 +2,7 @@ * Reusable countdown timer for dialog components. */ -import type { TUI } from "@gsd/pi-tui"; +import type { TUI } from "@sf-run/pi-tui"; export class CountdownTimer { private intervalId: ReturnType | undefined; diff --git a/packages/pi-coding-agent/src/modes/interactive/components/custom-editor.ts b/packages/pi-coding-agent/src/modes/interactive/components/custom-editor.ts index b6968460c..070202c9c 100644 --- a/packages/pi-coding-agent/src/modes/interactive/components/custom-editor.ts +++ b/packages/pi-coding-agent/src/modes/interactive/components/custom-editor.ts @@ -1,4 +1,4 @@ -import { Editor, type EditorOptions, type EditorTheme, type TUI, isKittyProtocolActive } from "@gsd/pi-tui"; +import { Editor, type EditorOptions, type EditorTheme, type TUI, isKittyProtocolActive } from "@sf-run/pi-tui"; import type { AppAction, KeybindingsManager } from "../../../core/keybindings.js"; /** diff --git a/packages/pi-coding-agent/src/modes/interactive/components/custom-message.ts b/packages/pi-coding-agent/src/modes/interactive/components/custom-message.ts index ba7cf9634..b03a28057 100644 --- a/packages/pi-coding-agent/src/modes/interactive/components/custom-message.ts +++ b/packages/pi-coding-agent/src/modes/interactive/components/custom-message.ts @@ -1,6 +1,6 @@ -import type { TextContent } from "@gsd/pi-ai"; -import type { Component } from "@gsd/pi-tui"; -import { Box, Container, Markdown, type MarkdownTheme, Spacer, Text } from "@gsd/pi-tui"; +import type { TextContent } from "@sf-run/pi-ai"; +import type { Component } from "@sf-run/pi-tui"; +import { Box, Container, Markdown, type MarkdownTheme, Spacer, Text } from "@sf-run/pi-tui"; import type { MessageRenderer } from "../../../core/extensions/types.js"; import type { CustomMessage } from "../../../core/messages.js"; import { getMarkdownTheme, theme } from "../theme/theme.js"; diff --git a/packages/pi-coding-agent/src/modes/interactive/components/daxnuts.ts b/packages/pi-coding-agent/src/modes/interactive/components/daxnuts.ts index 47b87e146..7c2a8962a 100644 --- a/packages/pi-coding-agent/src/modes/interactive/components/daxnuts.ts +++ b/packages/pi-coding-agent/src/modes/interactive/components/daxnuts.ts @@ -4,7 +4,7 @@ * A heartfelt tribute to dax (@thdxr) for providing free Kimi K2.5 access via OpenCode. */ -import { type Component, type TUI, visibleWidth } from "@gsd/pi-tui"; +import { type Component, type TUI, visibleWidth } from "@sf-run/pi-tui"; import { theme } from "../theme/theme.js"; // 32x32 RGB image of dax, hex encoded (3 bytes per pixel) diff --git a/packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.ts b/packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.ts index 61daf1bf4..852fb793f 100644 --- a/packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.ts +++ b/packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.ts @@ -1,5 +1,5 @@ -import type { Component, TUI } from "@gsd/pi-tui"; -import { visibleWidth } from "@gsd/pi-tui"; +import type { Component, TUI } from "@sf-run/pi-tui"; +import { visibleWidth } from "@sf-run/pi-tui"; import { theme } from "../theme/theme.js"; /** diff --git a/packages/pi-coding-agent/src/modes/interactive/components/extension-editor.ts b/packages/pi-coding-agent/src/modes/interactive/components/extension-editor.ts index 0b05c3ada..db23a2288 100644 --- a/packages/pi-coding-agent/src/modes/interactive/components/extension-editor.ts +++ b/packages/pi-coding-agent/src/modes/interactive/components/extension-editor.ts @@ -16,7 +16,7 @@ import { Spacer, Text, type TUI, -} from "@gsd/pi-tui"; +} from "@sf-run/pi-tui"; import type { KeybindingsManager } from "../../../core/keybindings.js"; import { getEditorTheme, theme } from "../theme/theme.js"; import { DynamicBorder } from "./dynamic-border.js"; diff --git a/packages/pi-coding-agent/src/modes/interactive/components/extension-input.ts b/packages/pi-coding-agent/src/modes/interactive/components/extension-input.ts index 7634d154f..3bc27f11c 100644 --- a/packages/pi-coding-agent/src/modes/interactive/components/extension-input.ts +++ b/packages/pi-coding-agent/src/modes/interactive/components/extension-input.ts @@ -2,7 +2,7 @@ * Simple text input component for extensions. */ -import { Container, type Focusable, getEditorKeybindings, Input, Spacer, Text, type TUI } from "@gsd/pi-tui"; +import { Container, type Focusable, getEditorKeybindings, Input, Spacer, Text, type TUI } from "@sf-run/pi-tui"; import { theme } from "../theme/theme.js"; import { CountdownTimer } from "./countdown-timer.js"; import { DynamicBorder } from "./dynamic-border.js"; diff --git a/packages/pi-coding-agent/src/modes/interactive/components/extension-selector.ts b/packages/pi-coding-agent/src/modes/interactive/components/extension-selector.ts index e24327fc8..0ce5b73a0 100644 --- a/packages/pi-coding-agent/src/modes/interactive/components/extension-selector.ts +++ b/packages/pi-coding-agent/src/modes/interactive/components/extension-selector.ts @@ -4,7 +4,7 @@ * Options starting with SEPARATOR_PREFIX are rendered as non-selectable group headers. */ -import { Container, getEditorKeybindings, Spacer, Text, type TUI } from "@gsd/pi-tui"; +import { Container, getEditorKeybindings, Spacer, Text, type TUI } from "@sf-run/pi-tui"; import { theme } from "../theme/theme.js"; import { CountdownTimer } from "./countdown-timer.js"; import { DynamicBorder } from "./dynamic-border.js"; diff --git a/packages/pi-coding-agent/src/modes/interactive/components/footer.ts b/packages/pi-coding-agent/src/modes/interactive/components/footer.ts index 3b28c0003..fb43466f8 100644 --- a/packages/pi-coding-agent/src/modes/interactive/components/footer.ts +++ b/packages/pi-coding-agent/src/modes/interactive/components/footer.ts @@ -1,4 +1,4 @@ -import { type Component, truncateToWidth, visibleWidth } from "@gsd/pi-tui"; +import { type Component, truncateToWidth, visibleWidth } from "@sf-run/pi-tui"; import type { AgentSession } from "../../../core/agent-session.js"; import type { ReadonlyFooterDataProvider } from "../../../core/footer-data-provider.js"; import { theme } from "../theme/theme.js"; @@ -111,6 +111,13 @@ export class FooterComponent implements Component { pwd = `${pwd} • ${sessionName}`; } + // Add short session ID so users can correlate the TUI to the + // on-disk jsonl when debugging crashes or locating prior runs. + const rawSessionId = this.session.sessionManager.getSessionId?.() ?? ""; + if (rawSessionId) { + pwd = `${pwd} [sess:${rawSessionId.slice(0, 8)}]`; + } + // Build stats line as separate groups joined by a dim middle-dot separator const sep = ` ${theme.fg("dim", "\u00B7")} `; diff --git a/packages/pi-coding-agent/src/modes/interactive/components/keybinding-hints.ts b/packages/pi-coding-agent/src/modes/interactive/components/keybinding-hints.ts index 42e8b4334..c500593d7 100644 --- a/packages/pi-coding-agent/src/modes/interactive/components/keybinding-hints.ts +++ b/packages/pi-coding-agent/src/modes/interactive/components/keybinding-hints.ts @@ -2,7 +2,7 @@ * Utilities for formatting keybinding hints in the UI. */ -import { type EditorAction, getEditorKeybindings, type KeyId } from "@gsd/pi-tui"; +import { type EditorAction, getEditorKeybindings, type KeyId } from "@sf-run/pi-tui"; import type { AppAction, KeybindingsManager } from "../../../core/keybindings.js"; import { theme } from "../theme/theme.js"; diff --git a/packages/pi-coding-agent/src/modes/interactive/components/login-dialog.ts b/packages/pi-coding-agent/src/modes/interactive/components/login-dialog.ts index 0a13465bb..b8d3c4e40 100644 --- a/packages/pi-coding-agent/src/modes/interactive/components/login-dialog.ts +++ b/packages/pi-coding-agent/src/modes/interactive/components/login-dialog.ts @@ -1,7 +1,7 @@ // GSD Login Dialog Component — OAuth login flow UI // Copyright (c) 2026 Jeremy McSpadden -import { getOAuthProviders } from "@gsd/pi-ai/oauth"; -import { Container, type Focusable, getEditorKeybindings, Input, Spacer, Text, truncateToWidth, type TUI } from "@gsd/pi-tui"; +import { getOAuthProviders } from "@sf-run/pi-ai/oauth"; +import { Container, type Focusable, getEditorKeybindings, Input, Spacer, Text, truncateToWidth, type TUI } from "@sf-run/pi-tui"; import { execFile } from "child_process"; import { theme } from "../theme/theme.js"; import { DynamicBorder } from "./dynamic-border.js"; diff --git a/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts b/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts index 191cefdca..474328338 100644 --- a/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts +++ b/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts @@ -1,4 +1,4 @@ -import { type Model, modelsAreEqual } from "@gsd/pi-ai"; +import { type Model, modelsAreEqual } from "@sf-run/pi-ai"; import { Container, type Focusable, @@ -8,7 +8,7 @@ import { Spacer, Text, type TUI, -} from "@gsd/pi-tui"; +} from "@sf-run/pi-tui"; import type { ModelRegistry } from "../../../core/model-registry.js"; import type { SettingsManager } from "../../../core/settings-manager.js"; import { theme } from "../theme/theme.js"; diff --git a/packages/pi-coding-agent/src/modes/interactive/components/oauth-selector.ts b/packages/pi-coding-agent/src/modes/interactive/components/oauth-selector.ts index 33e23df94..4464ef52b 100644 --- a/packages/pi-coding-agent/src/modes/interactive/components/oauth-selector.ts +++ b/packages/pi-coding-agent/src/modes/interactive/components/oauth-selector.ts @@ -1,6 +1,6 @@ -import type { OAuthProviderInterface } from "@gsd/pi-ai"; -import { getOAuthProviders } from "@gsd/pi-ai/oauth"; -import { Container, getEditorKeybindings, Spacer, TruncatedText } from "@gsd/pi-tui"; +import type { OAuthProviderInterface } from "@sf-run/pi-ai"; +import { getOAuthProviders } from "@sf-run/pi-ai/oauth"; +import { Container, getEditorKeybindings, Spacer, TruncatedText } from "@sf-run/pi-tui"; import type { AuthStorage } from "../../../core/auth-storage.js"; import { theme } from "../theme/theme.js"; import { DynamicBorder } from "./dynamic-border.js"; diff --git a/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts b/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts index aac53ad80..da66e70c2 100644 --- a/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts +++ b/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts @@ -10,7 +10,7 @@ import { Spacer, Text, type TUI, -} from "@gsd/pi-tui"; +} from "@sf-run/pi-tui"; import type { AuthStorage } from "../../../core/auth-storage.js"; import { getDiscoverableProviders } from "../../../core/model-discovery.js"; import { providerDisplayName } from "./model-selector.js"; diff --git a/packages/pi-coding-agent/src/modes/interactive/components/scoped-models-selector.ts b/packages/pi-coding-agent/src/modes/interactive/components/scoped-models-selector.ts index 2e1c9e41e..2bdfae90d 100644 --- a/packages/pi-coding-agent/src/modes/interactive/components/scoped-models-selector.ts +++ b/packages/pi-coding-agent/src/modes/interactive/components/scoped-models-selector.ts @@ -1,4 +1,4 @@ -import type { Model } from "@gsd/pi-ai"; +import type { Model } from "@sf-run/pi-ai"; import { providerDisplayName } from "./model-selector.js"; import { Container, @@ -10,7 +10,7 @@ import { matchesKey, Spacer, Text, -} from "@gsd/pi-tui"; +} from "@sf-run/pi-tui"; import { theme } from "../theme/theme.js"; import { DynamicBorder } from "./dynamic-border.js"; diff --git a/packages/pi-coding-agent/src/modes/interactive/components/session-selector-search.ts b/packages/pi-coding-agent/src/modes/interactive/components/session-selector-search.ts index ad0b74cc4..f0d8a577a 100644 --- a/packages/pi-coding-agent/src/modes/interactive/components/session-selector-search.ts +++ b/packages/pi-coding-agent/src/modes/interactive/components/session-selector-search.ts @@ -1,4 +1,4 @@ -import { fuzzyMatch } from "@gsd/pi-tui"; +import { fuzzyMatch } from "@sf-run/pi-tui"; import type { SessionInfo } from "../../../core/session-manager.js"; export type SortMode = "threaded" | "recent" | "relevance"; diff --git a/packages/pi-coding-agent/src/modes/interactive/components/session-selector.ts b/packages/pi-coding-agent/src/modes/interactive/components/session-selector.ts index ac08e7761..5c0fd8a68 100644 --- a/packages/pi-coding-agent/src/modes/interactive/components/session-selector.ts +++ b/packages/pi-coding-agent/src/modes/interactive/components/session-selector.ts @@ -12,7 +12,7 @@ import { Text, truncateToWidth, visibleWidth, -} from "@gsd/pi-tui"; +} from "@sf-run/pi-tui"; import { KeybindingsManager } from "../../../core/keybindings.js"; import type { SessionInfo, SessionListProgress } from "../../../core/session-manager.js"; import { theme } from "../theme/theme.js"; diff --git a/packages/pi-coding-agent/src/modes/interactive/components/settings-selector.ts b/packages/pi-coding-agent/src/modes/interactive/components/settings-selector.ts index 5b324af2c..bce587ca9 100644 --- a/packages/pi-coding-agent/src/modes/interactive/components/settings-selector.ts +++ b/packages/pi-coding-agent/src/modes/interactive/components/settings-selector.ts @@ -1,5 +1,5 @@ -import type { ThinkingLevel } from "@gsd/pi-agent-core"; -import type { Transport } from "@gsd/pi-ai"; +import type { ThinkingLevel } from "@sf-run/pi-agent-core"; +import type { Transport } from "@sf-run/pi-ai"; import { Container, getCapabilities, @@ -9,7 +9,7 @@ import { SettingsList, Spacer, Text, -} from "@gsd/pi-tui"; +} from "@sf-run/pi-tui"; import { getSelectListTheme, getSettingsListTheme, theme } from "../theme/theme.js"; import { DynamicBorder } from "./dynamic-border.js"; diff --git a/packages/pi-coding-agent/src/modes/interactive/components/show-images-selector.ts b/packages/pi-coding-agent/src/modes/interactive/components/show-images-selector.ts index 7303d42e6..3c8825e2d 100644 --- a/packages/pi-coding-agent/src/modes/interactive/components/show-images-selector.ts +++ b/packages/pi-coding-agent/src/modes/interactive/components/show-images-selector.ts @@ -1,4 +1,4 @@ -import { Container, type SelectItem, SelectList } from "@gsd/pi-tui"; +import { Container, type SelectItem, SelectList } from "@sf-run/pi-tui"; import { getSelectListTheme } from "../theme/theme.js"; import { DynamicBorder } from "./dynamic-border.js"; diff --git a/packages/pi-coding-agent/src/modes/interactive/components/skill-invocation-message.ts b/packages/pi-coding-agent/src/modes/interactive/components/skill-invocation-message.ts index 4e88f8eff..60b1104fa 100644 --- a/packages/pi-coding-agent/src/modes/interactive/components/skill-invocation-message.ts +++ b/packages/pi-coding-agent/src/modes/interactive/components/skill-invocation-message.ts @@ -1,4 +1,4 @@ -import { Box, Markdown, type MarkdownTheme, Text } from "@gsd/pi-tui"; +import { Box, Markdown, type MarkdownTheme, Text } from "@sf-run/pi-tui"; import type { ParsedSkillBlock } from "../../../core/agent-session.js"; import { getMarkdownTheme, theme } from "../theme/theme.js"; import { editorKey } from "./keybinding-hints.js"; diff --git a/packages/pi-coding-agent/src/modes/interactive/components/theme-selector.ts b/packages/pi-coding-agent/src/modes/interactive/components/theme-selector.ts index caad68689..48ed4d58e 100644 --- a/packages/pi-coding-agent/src/modes/interactive/components/theme-selector.ts +++ b/packages/pi-coding-agent/src/modes/interactive/components/theme-selector.ts @@ -1,4 +1,4 @@ -import { Container, type SelectItem, SelectList } from "@gsd/pi-tui"; +import { Container, type SelectItem, SelectList } from "@sf-run/pi-tui"; import { getAvailableThemes, getSelectListTheme } from "../theme/theme.js"; import { DynamicBorder } from "./dynamic-border.js"; diff --git a/packages/pi-coding-agent/src/modes/interactive/components/thinking-selector.ts b/packages/pi-coding-agent/src/modes/interactive/components/thinking-selector.ts index 1f70c25bf..21bf228df 100644 --- a/packages/pi-coding-agent/src/modes/interactive/components/thinking-selector.ts +++ b/packages/pi-coding-agent/src/modes/interactive/components/thinking-selector.ts @@ -1,5 +1,5 @@ -import type { ThinkingLevel } from "@gsd/pi-agent-core"; -import { Container, type SelectItem, SelectList } from "@gsd/pi-tui"; +import type { ThinkingLevel } from "@sf-run/pi-agent-core"; +import { Container, type SelectItem, SelectList } from "@sf-run/pi-tui"; import { getSelectListTheme } from "../theme/theme.js"; import { DynamicBorder } from "./dynamic-border.js"; diff --git a/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts b/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts index 7e01befbb..d3293f929 100644 --- a/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +++ b/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts @@ -9,7 +9,7 @@ import { Text, type TUI, truncateToWidth, -} from "@gsd/pi-tui"; +} from "@sf-run/pi-tui"; import stripAnsi from "strip-ansi"; import type { ToolDefinition } from "../../../core/extensions/types.js"; import { computeEditDiff, type EditDiffError, type EditDiffResult } from "../../../core/tools/edit-diff.js"; diff --git a/packages/pi-coding-agent/src/modes/interactive/components/tree-render-utils.ts b/packages/pi-coding-agent/src/modes/interactive/components/tree-render-utils.ts index 71dec5b41..125f78d94 100644 --- a/packages/pi-coding-agent/src/modes/interactive/components/tree-render-utils.ts +++ b/packages/pi-coding-agent/src/modes/interactive/components/tree-render-utils.ts @@ -1,4 +1,4 @@ -import { truncateToWidth } from "@gsd/pi-tui"; +import { truncateToWidth } from "@sf-run/pi-tui"; import { theme } from "../theme/theme.js"; // ── Tree connector characters ──────────────────────────────────────── diff --git a/packages/pi-coding-agent/src/modes/interactive/components/tree-selector.ts b/packages/pi-coding-agent/src/modes/interactive/components/tree-selector.ts index 5432eec5d..a0406313e 100644 --- a/packages/pi-coding-agent/src/modes/interactive/components/tree-selector.ts +++ b/packages/pi-coding-agent/src/modes/interactive/components/tree-selector.ts @@ -9,7 +9,7 @@ import { Text, TruncatedText, truncateToWidth, -} from "@gsd/pi-tui"; +} from "@sf-run/pi-tui"; import type { SessionTreeNode } from "../../../core/session-manager.js"; import { theme } from "../theme/theme.js"; import { DynamicBorder } from "./dynamic-border.js"; diff --git a/packages/pi-coding-agent/src/modes/interactive/components/user-message-selector.ts b/packages/pi-coding-agent/src/modes/interactive/components/user-message-selector.ts index 800232faa..fe52f1f73 100644 --- a/packages/pi-coding-agent/src/modes/interactive/components/user-message-selector.ts +++ b/packages/pi-coding-agent/src/modes/interactive/components/user-message-selector.ts @@ -1,4 +1,4 @@ -import { type Component, Container, getEditorKeybindings, Spacer, Text, truncateToWidth } from "@gsd/pi-tui"; +import { type Component, Container, getEditorKeybindings, Spacer, Text, truncateToWidth } from "@sf-run/pi-tui"; import { theme } from "../theme/theme.js"; import { DynamicBorder } from "./dynamic-border.js"; diff --git a/packages/pi-coding-agent/src/modes/interactive/components/user-message.ts b/packages/pi-coding-agent/src/modes/interactive/components/user-message.ts index 8aab303ba..e429c9505 100644 --- a/packages/pi-coding-agent/src/modes/interactive/components/user-message.ts +++ b/packages/pi-coding-agent/src/modes/interactive/components/user-message.ts @@ -1,4 +1,4 @@ -import { Container, Markdown, type MarkdownTheme, Spacer, Text } from "@gsd/pi-tui"; +import { Container, Markdown, type MarkdownTheme, Spacer, Text } from "@sf-run/pi-tui"; import { getMarkdownTheme, theme } from "../theme/theme.js"; import { formatTimestamp, type TimestampFormat } from "./timestamp.js"; diff --git a/packages/pi-coding-agent/src/modes/interactive/components/visual-truncate.ts b/packages/pi-coding-agent/src/modes/interactive/components/visual-truncate.ts index c609badfe..f584b9cb4 100644 --- a/packages/pi-coding-agent/src/modes/interactive/components/visual-truncate.ts +++ b/packages/pi-coding-agent/src/modes/interactive/components/visual-truncate.ts @@ -3,7 +3,7 @@ * Used by both tool-execution.ts and bash-execution.ts for consistent behavior. */ -import { Text } from "@gsd/pi-tui"; +import { Text } from "@sf-run/pi-tui"; export interface VisualTruncateResult { /** The visual lines to display */ diff --git a/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts b/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts index a69fceb1a..d65e7ae17 100644 --- a/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +++ b/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts @@ -1,4 +1,4 @@ -import { Loader, Markdown, Spacer, Text } from "@gsd/pi-tui"; +import { Loader, Markdown, Spacer, Text } from "@sf-run/pi-tui"; import type { InteractiveModeEvent, InteractiveModeStateHost } from "../interactive-mode-state.js"; import { theme } from "../theme/theme.js"; diff --git a/packages/pi-coding-agent/src/modes/interactive/controllers/model-controller.ts b/packages/pi-coding-agent/src/modes/interactive/controllers/model-controller.ts index 3e6ae686f..2be851eb3 100644 --- a/packages/pi-coding-agent/src/modes/interactive/controllers/model-controller.ts +++ b/packages/pi-coding-agent/src/modes/interactive/controllers/model-controller.ts @@ -1,4 +1,4 @@ -import type { Model } from "@gsd/pi-ai"; +import type { Model } from "@sf-run/pi-ai"; export async function handleModelCommand(host: any, searchTerm?: string): Promise { if (!searchTerm) { diff --git a/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts b/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts index 1cdf13efc..b5ba886e9 100644 --- a/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts @@ -7,9 +7,9 @@ import * as crypto from "node:crypto"; import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; -import { listDescendants } from "@gsd/native"; -import type { AgentMessage } from "@gsd/pi-agent-core"; -import type { AssistantMessage, ImageContent, Message, Model, OAuthProviderId } from "@gsd/pi-ai"; +import { listDescendants } from "@sf-run/native"; +import type { AgentMessage } from "@sf-run/pi-agent-core"; +import type { AssistantMessage, ImageContent, Message, Model, OAuthProviderId } from "@sf-run/pi-ai"; import type { AutocompleteItem, EditorComponent, @@ -19,7 +19,7 @@ import type { OverlayHandle, OverlayOptions, SlashCommand, -} from "@gsd/pi-tui"; +} from "@sf-run/pi-tui"; import { CombinedAutocompleteProvider, type Component, @@ -35,7 +35,7 @@ import { TruncatedText, TUI, visibleWidth, -} from "@gsd/pi-tui"; +} from "@sf-run/pi-tui"; import { spawn, spawnSync } from "child_process"; import { APP_NAME, @@ -625,7 +625,7 @@ export class InteractiveMode { if (process.env.PI_SKIP_VERSION_CHECK || process.env.PI_OFFLINE) return undefined; try { - const response = await fetch("https://registry.npmjs.org/@gsd/pi-coding-agent/latest", { + const response = await fetch("https://registry.npmjs.org/@sf-run/pi-coding-agent/latest", { signal: AbortSignal.timeout(10000), }); if (!response.ok) return undefined; @@ -2681,7 +2681,7 @@ export class InteractiveMode { } showNewVersionNotification(newVersion: string): void { - const action = theme.fg("accent", getUpdateInstruction("@gsd/pi-coding-agent")); + const action = theme.fg("accent", getUpdateInstruction("@sf-run/pi-coding-agent")); const updateInstruction = theme.fg("muted", `New version ${newVersion} is available. `) + action; const changelogUrl = theme.fg( "accent", diff --git a/packages/pi-coding-agent/src/modes/interactive/slash-command-handlers.ts b/packages/pi-coding-agent/src/modes/interactive/slash-command-handlers.ts index 91da276cb..5ada97591 100644 --- a/packages/pi-coding-agent/src/modes/interactive/slash-command-handlers.ts +++ b/packages/pi-coding-agent/src/modes/interactive/slash-command-handlers.ts @@ -11,19 +11,19 @@ import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; -import type { ThinkingLevel } from "@gsd/pi-agent-core"; +import type { ThinkingLevel } from "@sf-run/pi-agent-core"; import type { EditorAction, EditorComponent, MarkdownTheme, -} from "@gsd/pi-tui"; +} from "@sf-run/pi-tui"; import { type Component, Container, Markdown, Spacer, Text, -} from "@gsd/pi-tui"; +} from "@sf-run/pi-tui"; import { spawn, spawnSync } from "child_process"; import { getShareViewerUrl, @@ -41,7 +41,7 @@ import { appKey, editorKey, formatKeyForDisplay } from "./components/keybinding- import { SelectSubmenu, THINKING_DESCRIPTIONS } from "./components/settings-selector.js"; import { theme } from "./theme/theme.js"; -import type { TUI } from "@gsd/pi-tui"; +import type { TUI } from "@sf-run/pi-tui"; // --------------------------------------------------------------------------- // Context interface — the subset of InteractiveMode needed by slash commands diff --git a/packages/pi-coding-agent/src/modes/interactive/theme/theme.ts b/packages/pi-coding-agent/src/modes/interactive/theme/theme.ts index 763b22734..41127bab8 100644 --- a/packages/pi-coding-agent/src/modes/interactive/theme/theme.ts +++ b/packages/pi-coding-agent/src/modes/interactive/theme/theme.ts @@ -1,6 +1,6 @@ import * as fs from "node:fs"; import * as path from "node:path"; -import type { EditorTheme, MarkdownTheme, SelectListTheme } from "@gsd/pi-tui"; +import type { EditorTheme, MarkdownTheme, SelectListTheme } from "@sf-run/pi-tui"; import { type Static, Type } from "@sinclair/typebox"; import { TypeCompiler } from "@sinclair/typebox/compiler"; import chalk from "chalk"; @@ -8,7 +8,7 @@ import { highlightCode as nativeHighlightCode, supportsLanguage, type HighlightColors, -} from "@gsd/native"; +} from "@sf-run/native"; import { getCustomThemesDir } from "../../../config.js"; import { builtinThemes } from "./themes.js"; @@ -645,7 +645,7 @@ function getDefaultTheme(): string { // ============================================================================ // Use globalThis to share theme across module loaders (tsx + jiti in dev mode) -const THEME_KEY = Symbol.for("@gsd/pi-coding-agent:theme"); +const THEME_KEY = Symbol.for("@sf-run/pi-coding-agent:theme"); // Export theme as a getter that reads from globalThis // This ensures all module instances (tsx, jiti) see the same theme @@ -1065,7 +1065,7 @@ export function getEditorTheme(): EditorTheme { }; } -export function getSettingsListTheme(): import("@gsd/pi-tui").SettingsListTheme { +export function getSettingsListTheme(): import("@sf-run/pi-tui").SettingsListTheme { return { label: (text: string, selected: boolean) => (selected ? theme.fg("accent", text) : text), value: (text: string, selected: boolean) => (selected ? theme.fg("accent", text) : theme.fg("muted", text)), diff --git a/packages/pi-coding-agent/src/modes/print-mode.ts b/packages/pi-coding-agent/src/modes/print-mode.ts index a44266450..edcf8cc61 100644 --- a/packages/pi-coding-agent/src/modes/print-mode.ts +++ b/packages/pi-coding-agent/src/modes/print-mode.ts @@ -6,7 +6,7 @@ * - `pi --mode json "prompt"` - JSON event stream */ -import type { AssistantMessage, ImageContent } from "@gsd/pi-ai"; +import type { AssistantMessage, ImageContent } from "@sf-run/pi-ai"; import type { AgentSession } from "../core/agent-session.js"; import { createDefaultCommandContextActions } from "./shared/command-context-actions.js"; diff --git a/packages/pi-coding-agent/src/modes/rpc/remote-terminal.ts b/packages/pi-coding-agent/src/modes/rpc/remote-terminal.ts index 4dda9b0c9..536af4a22 100644 --- a/packages/pi-coding-agent/src/modes/rpc/remote-terminal.ts +++ b/packages/pi-coding-agent/src/modes/rpc/remote-terminal.ts @@ -1,4 +1,4 @@ -import type { Terminal } from "@gsd/pi-tui"; +import type { Terminal } from "@sf-run/pi-tui"; export interface RemoteTerminalOptions { onWrite: (data: string) => void; diff --git a/packages/pi-coding-agent/src/modes/rpc/rpc-client.ts b/packages/pi-coding-agent/src/modes/rpc/rpc-client.ts index e776bd8ad..3fe2a260c 100644 --- a/packages/pi-coding-agent/src/modes/rpc/rpc-client.ts +++ b/packages/pi-coding-agent/src/modes/rpc/rpc-client.ts @@ -5,8 +5,8 @@ */ import { type ChildProcess, spawn } from "node:child_process"; -import type { AgentEvent, AgentMessage, ThinkingLevel } from "@gsd/pi-agent-core"; -import type { ImageContent } from "@gsd/pi-ai"; +import type { AgentEvent, AgentMessage, ThinkingLevel } from "@sf-run/pi-agent-core"; +import type { ImageContent } from "@sf-run/pi-ai"; import type { SessionStats } from "../../core/agent-session.js"; import type { BashResult } from "../../core/bash-executor.js"; import type { CompactionResult } from "../../core/compaction/index.js"; diff --git a/packages/pi-coding-agent/src/modes/rpc/rpc-types.ts b/packages/pi-coding-agent/src/modes/rpc/rpc-types.ts index d6cd25bfc..ede57fa9d 100644 --- a/packages/pi-coding-agent/src/modes/rpc/rpc-types.ts +++ b/packages/pi-coding-agent/src/modes/rpc/rpc-types.ts @@ -5,8 +5,8 @@ * Responses and events are emitted as JSON lines on stdout. */ -import type { AgentMessage, ThinkingLevel } from "@gsd/pi-agent-core"; -import type { ImageContent, Model } from "@gsd/pi-ai"; +import type { AgentMessage, ThinkingLevel } from "@sf-run/pi-agent-core"; +import type { ImageContent, Model } from "@sf-run/pi-ai"; import type { SessionStats } from "../../core/agent-session.js"; import type { BashResult } from "../../core/bash-executor.js"; import type { CompactionResult } from "../../core/compaction/index.js"; diff --git a/packages/pi-coding-agent/src/resources/extensions/memory/index.ts b/packages/pi-coding-agent/src/resources/extensions/memory/index.ts index 4565b2831..dae74aa0e 100644 --- a/packages/pi-coding-agent/src/resources/extensions/memory/index.ts +++ b/packages/pi-coding-agent/src/resources/extensions/memory/index.ts @@ -11,9 +11,9 @@ * - /memory command: view, clear, rebuild, stats */ -import type { ExtensionAPI } from "@gsd/pi-coding-agent"; -import { getAgentDir, SettingsManager } from "@gsd/pi-coding-agent"; -import { completeSimple } from "@gsd/pi-ai"; +import type { ExtensionAPI } from "@sf-run/pi-coding-agent"; +import { getAgentDir, SettingsManager } from "@sf-run/pi-coding-agent"; +import { completeSimple } from "@sf-run/pi-ai"; import { createHash } from "crypto"; import { existsSync, mkdirSync, rmSync } from "fs"; import { join } from "path"; diff --git a/packages/pi-coding-agent/src/utils/clipboard-image.ts b/packages/pi-coding-agent/src/utils/clipboard-image.ts index ca78534e6..4882b35c6 100644 --- a/packages/pi-coding-agent/src/utils/clipboard-image.ts +++ b/packages/pi-coding-agent/src/utils/clipboard-image.ts @@ -1,7 +1,7 @@ import { spawnSync } from "child_process"; -import { readImageFromClipboard as nativeReadImage } from "@gsd/native/clipboard"; -import { ImageFormat, parseImage } from "@gsd/native/image"; +import { readImageFromClipboard as nativeReadImage } from "@sf-run/native/clipboard"; +import { ImageFormat, parseImage } from "@sf-run/native/image"; export type ClipboardImage = { bytes: Uint8Array; diff --git a/packages/pi-coding-agent/src/utils/clipboard-native.ts b/packages/pi-coding-agent/src/utils/clipboard-native.ts index c4233ba77..5769c6b64 100644 --- a/packages/pi-coding-agent/src/utils/clipboard-native.ts +++ b/packages/pi-coding-agent/src/utils/clipboard-native.ts @@ -1,11 +1,11 @@ /** - * Re-export native clipboard utilities from @gsd/native. + * Re-export native clipboard utilities from "@sf-run/native. * * This module exists for backward compatibility. Prefer importing - * directly from "@gsd/native/clipboard" in new code. + * directly from "@sf-run/native/clipboard" in new code. */ export { copyToClipboard, readTextFromClipboard, readImageFromClipboard, -} from "@gsd/native/clipboard"; +} from "@sf-run/native/clipboard"; diff --git a/packages/pi-coding-agent/src/utils/clipboard.ts b/packages/pi-coding-agent/src/utils/clipboard.ts index 4d1e1b6dd..a161212d1 100644 --- a/packages/pi-coding-agent/src/utils/clipboard.ts +++ b/packages/pi-coding-agent/src/utils/clipboard.ts @@ -1,4 +1,4 @@ -import { copyToClipboard as nativeCopy } from "@gsd/native/clipboard"; +import { copyToClipboard as nativeCopy } from "@sf-run/native/clipboard"; export function copyToClipboard(text: string): void { // Always emit OSC 52 - works over SSH/mosh, harmless locally diff --git a/packages/pi-coding-agent/src/utils/image-convert.ts b/packages/pi-coding-agent/src/utils/image-convert.ts index 3c1857ae9..30c791708 100644 --- a/packages/pi-coding-agent/src/utils/image-convert.ts +++ b/packages/pi-coding-agent/src/utils/image-convert.ts @@ -1,4 +1,4 @@ -import { ImageFormat, parseImage } from "@gsd/native/image"; +import { ImageFormat, parseImage } from "@sf-run/native/image"; /** * Convert image to PNG format for terminal display. diff --git a/packages/pi-coding-agent/src/utils/image-resize.ts b/packages/pi-coding-agent/src/utils/image-resize.ts index 813ddef4b..f3afa368d 100644 --- a/packages/pi-coding-agent/src/utils/image-resize.ts +++ b/packages/pi-coding-agent/src/utils/image-resize.ts @@ -1,6 +1,6 @@ -import type { ImageContent } from "@gsd/pi-ai"; -import { ImageFormat, parseImage, SamplingFilter } from "@gsd/native/image"; -import type { NativeImageHandle } from "@gsd/native/image"; +import type { ImageContent } from "@sf-run/pi-ai"; +import { ImageFormat, parseImage, SamplingFilter } from "@sf-run/native/image"; +import type { NativeImageHandle } from "@sf-run/native/image"; export interface ImageResizeOptions { maxWidth?: number; // Default: 2000 diff --git a/packages/pi-tui/package.json b/packages/pi-tui/package.json index dd40af01c..f124aa046 100644 --- a/packages/pi-tui/package.json +++ b/packages/pi-tui/package.json @@ -1,5 +1,5 @@ { - "name": "@gsd/pi-tui", + "name": "@sf-run/pi-tui", "version": "2.74.0", "description": "Terminal User Interface library (vendored from pi-mono)", "type": "module", diff --git a/packages/pi-tui/src/autocomplete.ts b/packages/pi-tui/src/autocomplete.ts index 1ecd1e754..50e44eb9a 100644 --- a/packages/pi-tui/src/autocomplete.ts +++ b/packages/pi-tui/src/autocomplete.ts @@ -1,7 +1,7 @@ import { readdirSync, statSync } from "fs"; import { homedir } from "os"; import { basename, dirname, join } from "path"; -import { fuzzyFind } from "@gsd/native/fd"; +import { fuzzyFind } from "@sf-run/native/fd"; import { fuzzyFilter } from "./fuzzy.js"; const PATH_DELIMITERS = new Set([" ", "\t", '"', "'", "="]); diff --git a/packages/pi-tui/src/terminal-image.ts b/packages/pi-tui/src/terminal-image.ts index bb99b279c..8446a06cb 100644 --- a/packages/pi-tui/src/terminal-image.ts +++ b/packages/pi-tui/src/terminal-image.ts @@ -209,7 +209,7 @@ export function calculateImageRows( * Auto-detects format from byte content (PNG, JPEG, GIF, WebP). */ export async function getImageDimensions(base64Data: string): Promise { - const { parseImage: parse } = await import("@gsd/native/image"); + const { parseImage: parse } = await import("@sf-run/native/image"); try { const bytes = new Uint8Array(Buffer.from(base64Data, "base64")); const handle = await parse(bytes); diff --git a/packages/pi-tui/src/utils.ts b/packages/pi-tui/src/utils.ts index de59d4dc3..1a8f70f13 100644 --- a/packages/pi-tui/src/utils.ts +++ b/packages/pi-tui/src/utils.ts @@ -5,7 +5,7 @@ import { sliceWithWidth as nativeSliceWithWidth, extractSegments as nativeExtractSegments, EllipsisKind, -} from "@gsd/native/text"; +} from "@sf-run/native/text"; // Grapheme segmenter (shared instance) const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" }); diff --git a/packages/rpc-client/examples/basic-usage.ts b/packages/rpc-client/examples/basic-usage.ts index 3248799b4..2486fac57 100644 --- a/packages/rpc-client/examples/basic-usage.ts +++ b/packages/rpc-client/examples/basic-usage.ts @@ -1,4 +1,4 @@ -import { RpcClient } from '@gsd-build/rpc-client'; +import { RpcClient } from '@singularity-forge/rpc-client'; const client = new RpcClient({ cwd: process.cwd() }); await client.start(); diff --git a/packages/rpc-client/package.json b/packages/rpc-client/package.json index a3db818eb..6fabe492e 100644 --- a/packages/rpc-client/package.json +++ b/packages/rpc-client/package.json @@ -1,11 +1,11 @@ { - "name": "@gsd-build/rpc-client", + "name": "@singularity-forge/rpc-client", "version": "2.74.0", - "description": "Standalone RPC client SDK for GSD — zero internal dependencies", + "description": "Standalone RPC client SDK for sf-run — zero internal dependencies", "license": "MIT", "repository": { "type": "git", - "url": "https://github.com/gsd-build/gsd-2.git", + "url": "https://github.com/singularity-forge/sf-run.git", "directory": "packages/rpc-client" }, "publishConfig": { diff --git a/packages/rpc-client/src/index.ts b/packages/rpc-client/src/index.ts index 3771a3359..ff5cefdd4 100644 --- a/packages/rpc-client/src/index.ts +++ b/packages/rpc-client/src/index.ts @@ -1,5 +1,5 @@ /** - * @gsd-build/rpc-client — standalone RPC client SDK for GSD. + * @singularity-forge/rpc-client — standalone RPC client SDK for GSD. * * Re-exports all types, JSONL utilities, and the RpcClient class. */ diff --git a/packages/rpc-client/tsconfig.examples.json b/packages/rpc-client/tsconfig.examples.json index 8453c546d..7e1e19d68 100644 --- a/packages/rpc-client/tsconfig.examples.json +++ b/packages/rpc-client/tsconfig.examples.json @@ -10,7 +10,7 @@ "noEmit": true, "types": ["node"], "paths": { - "@gsd-build/rpc-client": ["./src/index.ts"] + "@singularity-forge/rpc-client": ["./src/index.ts"] } }, "include": ["examples/**/*.ts"] diff --git a/scripts/bump-version.mjs b/scripts/bump-version.mjs index 2ad399f28..f8a9b9d1e 100644 --- a/scripts/bump-version.mjs +++ b/scripts/bump-version.mjs @@ -45,11 +45,11 @@ for (const name of workspacePackages) { const ws = JSON.parse(readFileSync(wsPath, "utf-8")); const wsOld = ws.version; ws.version = newVersion; - // Bump any internal @gsd-build/* or @gsd/* dep references to match. + // Bump any internal @singularity-forge/* or @sf-run/* dep references to match. for (const field of ["dependencies", "devDependencies", "peerDependencies"]) { if (!ws[field]) continue; for (const dep of Object.keys(ws[field])) { - if (workspacePackages.some((n) => dep === `@gsd-build/${n}` || dep === `@gsd/${n}`)) { + if (workspacePackages.some((n) => dep === `@singularity-forge/${n}` || dep === `@sf-run/${n}`)) { ws[field][dep] = `^${newVersion}`; } } diff --git a/scripts/dist-test-resolve.mjs b/scripts/dist-test-resolve.mjs index a5d94da11..8b1074348 100644 --- a/scripts/dist-test-resolve.mjs +++ b/scripts/dist-test-resolve.mjs @@ -15,18 +15,18 @@ import { join } from 'node:path'; // dist-test root — everything compiled lands here const DIST_TEST = new URL('../dist-test/', import.meta.url).href; -// Absolute paths to compiled @gsd/* entry points +// Absolute paths to compiled @sf-run/* entry points const GSD_ALIASES = { - '@gsd/pi-coding-agent': new URL('../dist-test/packages/pi-coding-agent/src/index.js', import.meta.url).href, - '@gsd/pi-ai/oauth': new URL('../dist-test/packages/pi-ai/src/utils/oauth/index.js', import.meta.url).href, - '@gsd/pi-ai': new URL('../dist-test/packages/pi-ai/src/index.js', import.meta.url).href, - '@gsd/pi-agent-core': new URL('../dist-test/packages/pi-agent-core/src/index.js', import.meta.url).href, - '@gsd/pi-tui': new URL('../dist-test/packages/pi-tui/src/index.js', import.meta.url).href, - '@gsd/native': new URL('../dist-test/packages/native/src/index.js', import.meta.url).href, + '@sf-run/pi-coding-agent': new URL('../dist-test/packages/pi-coding-agent/src/index.js', import.meta.url).href, + '@sf-run/pi-ai/oauth': new URL('../dist-test/packages/pi-ai/src/utils/oauth/index.js', import.meta.url).href, + '@sf-run/pi-ai': new URL('../dist-test/packages/pi-ai/src/index.js', import.meta.url).href, + '@sf-run/pi-agent-core': new URL('../dist-test/packages/pi-agent-core/src/index.js', import.meta.url).href, + '@sf-run/pi-tui': new URL('../dist-test/packages/pi-tui/src/index.js', import.meta.url).href, + '@sf-run/native': new URL('../dist-test/packages/native/src/index.js', import.meta.url).href, }; export function resolve(specifier, context, nextResolve) { - // 1. @gsd/* bare imports → compiled dist-test counterpart + // 1. @sf-run/* bare imports → compiled dist-test counterpart if (specifier in GSD_ALIASES) { return nextResolve(GSD_ALIASES[specifier], context); } diff --git a/scripts/link-workspace-packages.cjs b/scripts/link-workspace-packages.cjs index 7c203a19f..754a4f195 100644 --- a/scripts/link-workspace-packages.cjs +++ b/scripts/link-workspace-packages.cjs @@ -2,15 +2,15 @@ /** * link-workspace-packages.cjs * - * Creates node_modules/@gsd/* and node_modules/@gsd-build/* symlinks pointing + * Creates node_modules/@sf-run/* and node_modules/@singularity-forge/* symlinks pointing * to shipped packages/* directories. * * During development, npm workspaces creates these automatically. But in the * published tarball, workspace packages are shipped under packages/ (via the - * "files" field) and the @gsd/* imports in compiled code need node_modules/@gsd/* + * "files" field) and the @sf-run/* imports in compiled code need node_modules/@sf-run/* * to resolve. This script bridges the gap. * - * Runs as part of postinstall (before any ESM code that imports @gsd/*). + * Runs as part of postinstall (before any ESM code that imports @sf-run/*). * * On Windows without Developer Mode or administrator rights, creating symlinks * (even NTFS junctions) can fail with EPERM. In that case we fall back to diff --git a/scripts/preview-dashboard.ts b/scripts/preview-dashboard.ts index c5c1d0dde..d7ea76441 100644 --- a/scripts/preview-dashboard.ts +++ b/scripts/preview-dashboard.ts @@ -13,7 +13,7 @@ * npx tsx scripts/preview-dashboard.ts --narrow # force 80 cols */ -import { truncateToWidth, visibleWidth } from "@gsd/pi-tui"; +import { truncateToWidth, visibleWidth } from "@sf-run/pi-tui"; import { makeUI, GLYPH, INDENT } from "../src/resources/extensions/shared/mod.js"; // ── Minimal ANSI color theme (no Theme class dependency) ──────────────── diff --git a/scripts/recover-gsd-1668.ps1 b/scripts/recover-gsd-1668.ps1 index d2f290fd1..9af51ec64 100644 --- a/scripts/recover-gsd-1668.ps1 +++ b/scripts/recover-gsd-1668.ps1 @@ -335,5 +335,5 @@ Write-Host " 4. Clean up after verifying:" Write-Host " git branch -D $recoveryBranch" Write-Host "" Write-Host "Note: update GSD to v2.40.1+ to prevent this from recurring." -ForegroundColor DarkGray -Write-Host " PR: https://github.com/gsd-build/gsd-2/pull/1669" -ForegroundColor DarkGray +Write-Host " PR: https://github.com/singularity-forge/sf-run/pull/1669" -ForegroundColor DarkGray Write-Host "" diff --git a/scripts/recover-gsd-1668.sh b/scripts/recover-gsd-1668.sh index 47b7c321a..aa133e3a6 100755 --- a/scripts/recover-gsd-1668.sh +++ b/scripts/recover-gsd-1668.sh @@ -442,5 +442,5 @@ echo -e " ${BOLD}4. Clean up after verifying:${RESET}" echo " git branch -D ${RECOVERY_BRANCH}" echo "" echo -e "${DIM}Note: update GSD to v2.40.1+ to prevent this from recurring.${RESET}" -echo " PR: https://github.com/gsd-build/gsd-2/pull/1669" +echo " PR: https://github.com/singularity-forge/sf-run/pull/1669" echo "" diff --git a/scripts/validate-pack.js b/scripts/validate-pack.js index b35bc1b5a..ba2567559 100644 --- a/scripts/validate-pack.js +++ b/scripts/validate-pack.js @@ -47,8 +47,8 @@ try { npmCacheDir = mkdtempSync(join(tmpdir(), 'validate-pack-npm-cache-')); mkdirSync(npmCacheDir, { recursive: true }); - // --- Guard: workspace packages must not have @gsd/* cross-deps --- - console.log('==> Checking workspace packages for @gsd/* cross-deps...'); + // --- Guard: workspace packages must not have @sf-run/* cross-deps --- + console.log('==> Checking workspace packages for @sf-run/* cross-deps...'); const workspaces = ['native', 'pi-agent-core', 'pi-ai', 'pi-coding-agent', 'pi-tui']; let crossFailed = false; @@ -56,7 +56,7 @@ try { const pkgPath = join(ROOT, 'packages', ws, 'package.json'); if (!existsSync(pkgPath)) continue; const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')); - const deps = Object.keys(pkg.dependencies || {}).filter(d => d.startsWith('@gsd/')); + const deps = Object.keys(pkg.dependencies || {}).filter(d => d.startsWith('@sf-run/')); if (deps.length) { console.log(` LEAKED in ${ws}: ${deps.join(', ')}`); crossFailed = true; @@ -64,11 +64,11 @@ try { } if (crossFailed) { - console.log('ERROR: Workspace packages have @gsd/* cross-dependencies.'); + console.log('ERROR: Workspace packages have @sf-run/* cross-dependencies.'); console.log(' These cause 404s when npm resolves them from the registry.'); process.exit(1); } - console.log(' No @gsd/* cross-dependencies.'); + console.log(' No @sf-run/* cross-dependencies.'); // --- Pack tarball --- console.log('==> Packing tarball...'); @@ -144,10 +144,10 @@ try { process.exit(1); } - // --- Verify @gsd/* packages resolved correctly post-install --- + // --- Verify @sf-run/* packages resolved correctly post-install --- // This catches the Windows-style failure where symlinkSync fails silently and - // node_modules/@gsd/ is never populated, causing ERR_MODULE_NOT_FOUND at runtime. - console.log('==> Verifying @gsd/* workspace package resolution...'); + // node_modules/@sf-run/ is never populated, causing ERR_MODULE_NOT_FOUND at runtime. + console.log('==> Verifying @sf-run/* workspace package resolution...'); const installedRoot = join(installDir, 'node_modules', 'gsd-pi'); const criticalPackages = [ { scope: '@gsd', name: 'pi-coding-agent' }, @@ -167,11 +167,11 @@ try { } } if (resolutionFailed) { - console.log('ERROR: @gsd/* packages are not resolvable after install.'); + console.log('ERROR: @sf-run/* packages are not resolvable after install.'); console.log(' This will cause ERR_MODULE_NOT_FOUND on first run (especially on Windows).'); process.exit(1); } - console.log(' @gsd/* packages are resolvable.'); + console.log(' @sf-run/* packages are resolvable.'); // --- Run the binary to confirm end-to-end resolution --- console.log('==> Running installed binary (gsd -v)...'); diff --git a/src/cli.ts b/src/cli.ts index a516a7e59..846280a07 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -9,7 +9,7 @@ import { InteractiveMode, runPrintMode, runRpcMode, -} from '@gsd/pi-coding-agent' +} from '@sf-run/pi-coding-agent' import { readFileSync } from 'node:fs' import { join } from 'node:path' import { agentDir, sessionsDir, authFilePath } from './app-paths.js' @@ -51,9 +51,9 @@ function exitIfManagedResourcesAreNewer(currentAgentDir: string): void { } process.stderr.write( - `[gsd] ${chalk.yellow('Version mismatch detected')}\n` + - `[gsd] Synced resources are from ${chalk.bold(`v${managedVersion}`)}, but this \`gsd\` binary is ${chalk.dim(`v${currentVersion}`)}.\n` + - `[gsd] Run ${chalk.bold('npm install -g gsd-pi@latest')} or ${chalk.bold('gsd update')}, then try again.\n`, + `[sf] ${chalk.yellow('Version mismatch detected')}\n` + + `[sf] Synced resources are from ${chalk.bold(`v${managedVersion}`)}, but this \`sf\` binary is ${chalk.dim(`v${currentVersion}`)}.\n` + + `[sf] Run ${chalk.bold('npm install -g gsd-pi@latest')} or ${chalk.bold('sf update')}, then try again.\n`, ) process.exit(1) } @@ -70,18 +70,18 @@ function exitIfManagedResourcesAreNewer(currentAgentDir: string): void { */ function printNonTtyErrorAndExit(missing: string | undefined, includeWebHint: boolean): never { const suffix = missing ? ` but ${missing} not a TTY` : '' - process.stderr.write(`[gsd] Error: Interactive mode requires a terminal (TTY)${suffix}.\n`) - process.stderr.write('[gsd] Non-interactive alternatives:\n') - process.stderr.write('[gsd] gsd auto Auto-mode (pipeable, no TUI)\n') - process.stderr.write('[gsd] gsd --print "your message" Single-shot prompt\n') + process.stderr.write(`[sf] Error: Interactive mode requires a terminal (TTY)${suffix}.\n`) + process.stderr.write('[sf] Non-interactive alternatives:\n') + process.stderr.write('[sf] sf auto Auto-mode (pipeable, no TUI)\n') + process.stderr.write('[sf] sf --print "your message" Single-shot prompt\n') if (includeWebHint) { - process.stderr.write('[gsd] gsd --web [path] Browser-only web mode\n') + process.stderr.write('[sf] sf --web [path] Browser-only web mode\n') } - process.stderr.write('[gsd] gsd --mode rpc JSON-RPC over stdin/stdout\n') - process.stderr.write('[gsd] gsd --mode mcp MCP server over stdin/stdout\n') - process.stderr.write('[gsd] gsd --mode text "message" Text output mode\n') + process.stderr.write('[sf] sf --mode rpc JSON-RPC over stdin/stdout\n') + process.stderr.write('[sf] sf --mode mcp MCP server over stdin/stdout\n') + process.stderr.write('[sf] sf --mode text "message" Text output mode\n') if (includeWebHint) { - process.stderr.write('[gsd] gsd headless Auto-mode without TUI\n') + process.stderr.write('[sf] sf headless Auto-mode without TUI\n') } process.exit(1) } @@ -94,7 +94,7 @@ function printExtensionErrors(errors: ReadonlyArray<{ error: string }>): void { for (const err of errors) { const isConflict = err.error.includes('supersedes') || err.error.includes('conflicts with') const prefix = isConflict ? 'Extension conflict' : 'Extension load error' - process.stderr.write(`[gsd] ${prefix}: ${err.error}\n`) + process.stderr.write(`[sf] ${prefix}: ${err.error}\n`) } } @@ -126,9 +126,9 @@ async function reapplyValidatedModelOnFallback( const cliFlags = parseCliArgs(process.argv) const isPrintMode = cliFlags.print || cliFlags.mode !== undefined -// `gsd [subcommand] --help` / `-h` — print help before any subcommand runs. +// `sf [subcommand] --help` / `-h` — print help before any subcommand runs. // loader.ts only catches --help/-h as the *first* arg; here we handle the -// case where it appears later (e.g. `gsd update --help`, `gsd --foo --help`). +// case where it appears later (e.g. `sf update --help`, `sf --foo --help`). // Prefer subcommand-specific help when the first positional is a known // subcommand, otherwise fall back to general help. if (process.argv.includes('--help') || process.argv.includes('-h')) { @@ -158,14 +158,14 @@ async function doRtkBootstrap(): Promise { const rtkStatus = await bootstrapRtk() markStartup('bootstrapRtk') if (!rtkStatus.available && rtkStatus.supported && rtkStatus.enabled && rtkStatus.reason) { - process.stderr.write(`[gsd] Warning: RTK unavailable — continuing without shell-command compression (${rtkStatus.reason}).\n`) + process.stderr.write(`[sf] Warning: RTK unavailable — continuing without shell-command compression (${rtkStatus.reason}).\n`) } } function ensureRtkBootstrap(): Promise { return (rtkBootstrapPromise ??= doRtkBootstrap()) } -// `gsd update` — update to the latest version via npm +// `sf update` — update to the latest version via npm if (cliFlags.messages[0] === 'update') { const { runUpdate } = await import('./update-cmd.js') await runUpdate() @@ -173,11 +173,11 @@ if (cliFlags.messages[0] === 'update') { } // --------------------------------------------------------------------------- -// Graph subcommand — `gsd graph build|status|query|diff` +// Graph subcommand — `sf graph build|status|query|diff` // --------------------------------------------------------------------------- if (cliFlags.messages[0] === 'graph') { const sub = cliFlags.messages[1] - const { buildGraph, writeGraph, graphStatus, graphQuery, graphDiff, resolveGsdRoot } = await import('@gsd-build/mcp-server') + const { buildGraph, writeGraph, graphStatus, graphQuery, graphDiff, resolveGsdRoot } = await import('@singularity-forge/mcp-server') const projectDir = process.cwd() const gsdRoot = resolveGsdRoot(projectDir) @@ -188,14 +188,14 @@ if (cliFlags.messages[0] === 'graph') { await writeGraph(gsdRoot, graph) process.stdout.write(`Graph built: ${graph.nodes.length} nodes, ${graph.edges.length} edges\n`) } catch (err) { - process.stderr.write(`[gsd] graph build failed: ${err instanceof Error ? err.message : String(err)}\n`) + process.stderr.write(`[sf] graph build failed: ${err instanceof Error ? err.message : String(err)}\n`) process.exit(1) } } else if (sub === 'status') { try { const result = await graphStatus(projectDir) if (!result.exists) { - process.stdout.write('Graph: not built yet. Run: gsd graph build\n') + process.stdout.write('Graph: not built yet. Run: sf graph build\n') } else { process.stdout.write(`Graph status:\n`) process.stdout.write(` exists: ${result.exists}\n`) @@ -206,13 +206,13 @@ if (cliFlags.messages[0] === 'graph') { process.stdout.write(` lastBuild: ${result.lastBuild ?? 'n/a'}\n`) } } catch (err) { - process.stderr.write(`[gsd] graph status failed: ${err instanceof Error ? err.message : String(err)}\n`) + process.stderr.write(`[sf] graph status failed: ${err instanceof Error ? err.message : String(err)}\n`) process.exit(1) } } else if (sub === 'query') { const term = cliFlags.messages[2] if (!term) { - process.stderr.write('Usage: gsd graph query \n') + process.stderr.write('Usage: sf graph query \n') process.exit(1) } try { @@ -226,7 +226,7 @@ if (cliFlags.messages[0] === 'graph') { } } } catch (err) { - process.stderr.write(`[gsd] graph query failed: ${err instanceof Error ? err.message : String(err)}\n`) + process.stderr.write(`[sf] graph query failed: ${err instanceof Error ? err.message : String(err)}\n`) process.exit(1) } } else if (sub === 'diff') { @@ -239,7 +239,7 @@ if (cliFlags.messages[0] === 'graph') { process.stdout.write(` edges added: ${result.edges.added.length}\n`) process.stdout.write(` edges removed: ${result.edges.removed.length}\n`) } catch (err) { - process.stderr.write(`[gsd] graph diff failed: ${err instanceof Error ? err.message : String(err)}\n`) + process.stderr.write(`[sf] graph diff failed: ${err instanceof Error ? err.message : String(err)}\n`) process.exit(1) } } else { @@ -260,7 +260,7 @@ if (!process.stdin.isTTY && !isPrintMode && !hasSubcommand && !cliFlags.listMode } const packageCommand = await runPackageCommand({ - appName: 'gsd', + appName: 'sf', args: process.argv.slice(2), cwd: process.cwd(), agentDir, @@ -272,7 +272,7 @@ if (packageCommand.handled) { process.exit(packageCommand.exitCode) } -// `gsd config` — replay the setup wizard and exit +// `sf config` — replay the setup wizard and exit if (cliFlags.messages[0] === 'config') { const authStorage = AuthStorage.create(authFilePath) loadStoredEnvKeys(authStorage) @@ -280,7 +280,7 @@ if (cliFlags.messages[0] === 'config') { process.exit(0) } -// `gsd web stop [path|all]` — stop web server before anything else +// `sf web stop [path|all]` — stop web server before anything else if (cliFlags.messages[0] === 'web' && cliFlags.messages[1] === 'stop') { const webBranch = await runWebCliBranch(cliFlags, { stopWebMode, @@ -293,7 +293,7 @@ if (cliFlags.messages[0] === 'web' && cliFlags.messages[1] === 'stop') { } } -// `gsd --web [path]` or `gsd web [start] [path]` — launch browser-only web mode +// `sf --web [path]` or `sf web [start] [path]` — launch browser-only web mode if (cliFlags.web || (cliFlags.messages[0] === 'web' && cliFlags.messages[1] !== 'stop')) { await ensureRtkBootstrap() const webBranch = await runWebCliBranch(cliFlags, { @@ -307,7 +307,7 @@ if (cliFlags.web || (cliFlags.messages[0] === 'web' && cliFlags.messages[1] !== } -// `gsd sessions` — list past sessions and pick one to resume +// `sf sessions` — list past sessions and pick one to resume if (cliFlags.messages[0] === 'sessions') { const cwd = process.cwd() const safePath = `--${cwd.replace(/^[/\\]/, '').replace(/[/\\:]/g, '-')}--` @@ -372,7 +372,7 @@ if (cliFlags.messages[0] === 'sessions') { cliFlags._selectedSessionPath = selected.path } -// `gsd headless` — run auto-mode without TUI +// `sf headless` — run auto-mode without TUI if (cliFlags.messages[0] === 'headless') { await ensureRtkBootstrap() // Sync bundled resources before headless runs (#3471). Without this, @@ -397,8 +397,8 @@ async function runHeadlessFromAuto(headlessArgs: string[]): Promise { process.exit(0) } -// `gsd auto [args...]` — shorthand for `gsd headless auto [args...]` (#2732) -// Without this, `gsd auto` falls through to the interactive TUI which hangs +// `sf auto [args...]` — shorthand for `sf headless auto [args...]` (#2732) +// Without this, `sf auto` falls through to the interactive TUI which hangs // when stdin/stdout are piped (non-TTY environments). if (cliFlags.messages[0] === 'auto') { await runHeadlessFromAuto(cliFlags.messages) @@ -449,7 +449,7 @@ if (!isPrintMode) { // Warn if terminal is too narrow for readable output if (!isPrintMode && process.stdout.columns && process.stdout.columns < 40) { process.stderr.write( - chalk.yellow(`[gsd] Terminal width is ${process.stdout.columns} columns (minimum recommended: 40). Output may be unreadable.\n`), + chalk.yellow(`[sf] Terminal width is ${process.stdout.columns} columns (minimum recommended: 40). Output may be unreadable.\n`), ) } @@ -511,7 +511,7 @@ if (cliFlags.listModels !== undefined) { process.exit(0) } -// GSD always uses quiet startup — the gsd extension renders its own branded header +// SF always uses quiet startup — the sf extension renders its own branded header if (!settingsManager.getQuietStartup()) { settingsManager.setQuietStartup(true) } diff --git a/src/headless-answers.ts b/src/headless-answers.ts index 033d37ed5..d77afb5bd 100644 --- a/src/headless-answers.ts +++ b/src/headless-answers.ts @@ -7,7 +7,7 @@ */ import { readFileSync } from 'node:fs' -import { serializeJsonLine } from '@gsd/pi-coding-agent' +import { serializeJsonLine } from '@sf-run/pi-coding-agent' // --------------------------------------------------------------------------- // Types diff --git a/src/headless-ui.ts b/src/headless-ui.ts index 6200bb92b..16a904d77 100644 --- a/src/headless-ui.ts +++ b/src/headless-ui.ts @@ -8,7 +8,7 @@ import type { Readable } from 'node:stream' -import { RpcClient, attachJsonlLineReader } from '@gsd/pi-coding-agent' +import { RpcClient, attachJsonlLineReader } from '@sf-run/pi-coding-agent' // --------------------------------------------------------------------------- // Types diff --git a/src/headless.ts b/src/headless.ts index d277c6725..0af428bd4 100644 --- a/src/headless.ts +++ b/src/headless.ts @@ -17,8 +17,8 @@ import { join } from 'node:path' import { resolve } from 'node:path' import { ChildProcess } from 'node:child_process' -import { RpcClient, SessionManager } from '@gsd/pi-coding-agent' -import type { SessionInfo } from '@gsd/pi-coding-agent' +import { RpcClient, SessionManager } from '@sf-run/pi-coding-agent' +import type { SessionInfo } from '@sf-run/pi-coding-agent' import { getProjectSessionsDir } from './project-sessions.js' import { loadAndValidateAnswerFile, AnswerInjector } from './headless-answers.js' diff --git a/src/loader.ts b/src/loader.ts index 93ca7d1a4..1b1ef8e8a 100644 --- a/src/loader.ts +++ b/src/loader.ts @@ -1,6 +1,6 @@ #!/usr/bin/env node -// GSD Startup Loader -// Copyright (c) 2026 Jeremy McSpadden +// SF Startup Loader +// Copyright (c) 2026 Singularity Forge import { fileURLToPath } from 'url' import { dirname, resolve, join, relative, delimiter } from 'path' import { existsSync, readFileSync, mkdirSync, symlinkSync, cpSync } from 'fs' @@ -46,7 +46,7 @@ if (firstArg === '--help' || firstArg === '-h') { const nodeMajor = parseInt(process.versions.node.split('.')[0], 10) if (nodeMajor < MIN_NODE_MAJOR) { process.stderr.write( - `\n${red}${bold}Error:${reset} GSD requires Node.js >= ${MIN_NODE_MAJOR}.0.0\n` + + `\n${red}${bold}Error:${reset} SF requires Node.js >= ${MIN_NODE_MAJOR}.0.0\n` + ` You are running Node.js ${process.versions.node}\n\n` + `${dim}Install a supported version:${reset}\n` + ` nvm install ${MIN_NODE_MAJOR} ${dim}# if using nvm${reset}\n` + @@ -62,7 +62,7 @@ if (firstArg === '--help' || firstArg === '-h') { execFileSync('git', ['--version'], { stdio: 'ignore' }) } catch { process.stderr.write( - `\n${red}${bold}Error:${reset} GSD requires git but it was not found on PATH.\n\n` + + `\n${red}${bold}Error:${reset} SF requires git but it was not found on PATH.\n\n` + `${dim}Install git:${reset}\n` + ` https://git-scm.com/downloads\n\n` ) @@ -80,15 +80,15 @@ import { renderLogo } from './logo.js' // pkg/ is a shim directory: contains gsd's piConfig (package.json) and pi's // theme assets (dist/modes/interactive/theme/) without a src/ directory. // This allows config.js to: -// 1. Read piConfig.name → "gsd" (branding) +// 1. Read piConfig.name → "sf" (branding) // 2. Resolve themes via dist/ (no src/ present → uses dist path) const pkgDir = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'pkg') // MUST be set before any dynamic import of pi SDK fires — this is what config.js // reads to determine APP_NAME and CONFIG_DIR_NAME process.env.PI_PACKAGE_DIR = pkgDir -process.env.PI_SKIP_VERSION_CHECK = '1' // GSD runs its own update check in cli.ts — suppress pi's -process.title = 'gsd' +process.env.PI_SKIP_VERSION_CHECK = '1' // SF runs its own update check in cli.ts — suppress pi's +process.title = 'sf' // Print branded banner on first launch (before ~/.gsd/ exists). // Set GSD_FIRST_RUN_BANNER so cli.ts skips the duplicate welcome screen. @@ -101,7 +101,7 @@ if (!existsSync(appRoot)) { process.stderr.write( renderLogo(colorCyan) + '\n' + - ` Get Shit Done ${dim}v${gsdVersion}${reset}\n` + + ` Singularity Forge ${dim}v${gsdVersion}${reset}\n` + ` ${green}Welcome.${reset} Setting up your environment...\n\n` ) process.env.GSD_FIRST_RUN_BANNER = '1' @@ -177,7 +177,7 @@ if (process.env.HTTP_PROXY || process.env.HTTPS_PROXY || process.env.http_proxy } // Ensure workspace packages are linked (or copied on Windows) before importing -// cli.js (which imports @gsd/*). +// cli.js (which imports @sf-run/*). // npm postinstall handles this normally, but npx --ignore-scripts skips postinstall. // On Windows without Developer Mode or admin rights, symlinkSync will throw even for // 'junction' type — so we fall back to cpSync (a full directory copy) which works @@ -207,7 +207,7 @@ try { const criticalPackages = ['pi-coding-agent'] const missingPackages = criticalPackages.filter(pkg => !existsSync(join(gsdScopeDir, pkg))) if (missingPackages.length > 0) { - const missing = missingPackages.map(p => `@gsd/${p}`).join(', ') + const missing = missingPackages.map(p => `@sf-run/${p}`).join(', ') process.stderr.write( `\nError: GSD installation is broken — missing packages: ${missing}\n\n` + `This is usually caused by one of:\n` + @@ -217,7 +217,7 @@ if (missingPackages.length > 0) { `Fix it by reinstalling:\n\n` + ` npm install -g gsd-pi@latest\n\n` + `If the issue persists, please open an issue at:\n` + - ` https://github.com/gsd-build/gsd-2/issues\n` + ` https://github.com/singularity-forge/sf-run/issues\n` ) process.exit(1) } diff --git a/src/logo.ts b/src/logo.ts index c172fbaf9..a7d4b4396 100644 --- a/src/logo.ts +++ b/src/logo.ts @@ -1,5 +1,5 @@ /** - * Shared GSD block-letter ASCII logo. + * Shared SF block-letter ASCII logo. * * Single source of truth — imported by: * - scripts/postinstall.js (via dist/logo.js) @@ -7,13 +7,13 @@ */ /** Raw logo lines — no ANSI codes, no leading newline. */ -export const GSD_LOGO: readonly string[] = [ - ' ██████╗ ███████╗██████╗ ', - ' ██╔════╝ ██╔════╝██╔══██╗', - ' ██║ ███╗███████╗██║ ██║', - ' ██║ ██║╚════██║██║ ██║', - ' ╚██████╔╝███████║██████╔╝', - ' ╚═════╝ ╚══════╝╚═════╝ ', +export const SF_LOGO: readonly string[] = [ + ' ███████╗ ███████╗', + ' ██╔════╝ ██╔════╝', + ' ███████╗ █████╗ ', + ' ╚════██║ ██╔══╝ ', + ' ███████║ ██║ ', + ' ╚══════╝ ╚═╝ ', ] /** @@ -23,5 +23,5 @@ export const GSD_LOGO: readonly string[] = [ * @returns Ready-to-write string with leading/trailing newlines. */ export function renderLogo(color: (s: string) => string): string { - return '\n' + GSD_LOGO.map(color).join('\n') + '\n' + return '\n' + SF_LOGO.map(color).join('\n') + '\n' } diff --git a/src/mcp-server.ts b/src/mcp-server.ts index 6db605dc9..ee24b547e 100644 --- a/src/mcp-server.ts +++ b/src/mcp-server.ts @@ -1,6 +1,6 @@ /** * Minimal tool interface matching GSD's AgentTool shape. - * Avoids a direct dependency on @gsd/pi-agent-core from this compiled module. + * Avoids a direct dependency on @sf-run/pi-agent-core from this compiled module. */ export interface McpToolDef { name: string diff --git a/src/onboarding.ts b/src/onboarding.ts index d3e2ba5cf..7e4086b26 100644 --- a/src/onboarding.ts +++ b/src/onboarding.ts @@ -13,7 +13,7 @@ import { execFile } from 'node:child_process' import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs' import { dirname, join } from 'node:path' -import type { AuthStorage } from '@gsd/pi-coding-agent' +import type { AuthStorage } from '@sf-run/pi-coding-agent' import { renderLogo } from './logo.js' import { agentDir } from './app-paths.js' import { isClaudeCliReady } from './claude-cli-check.js' diff --git a/src/pi-migration.ts b/src/pi-migration.ts index dbaa96b50..704fd69b5 100644 --- a/src/pi-migration.ts +++ b/src/pi-migration.ts @@ -7,7 +7,7 @@ import { existsSync, readFileSync } from 'node:fs' import { homedir } from 'node:os' import { join } from 'node:path' -import type { AuthStorage, AuthCredential } from '@gsd/pi-coding-agent' +import type { AuthStorage, AuthCredential } from '@sf-run/pi-coding-agent' const PI_AUTH_PATH = join(homedir(), '.pi', 'agent', 'auth.json') const PI_SETTINGS_PATH = join(homedir(), '.pi', 'agent', 'settings.json') diff --git a/src/provider-migrations.ts b/src/provider-migrations.ts index 1e61c69df..dbdd93f28 100644 --- a/src/provider-migrations.ts +++ b/src/provider-migrations.ts @@ -1,4 +1,4 @@ -import type { AuthStorage } from "@gsd/pi-coding-agent" +import type { AuthStorage } from "@sf-run/pi-coding-agent" type AnthropicMigrationDeps = { authStorage: Pick diff --git a/src/resource-loader.ts b/src/resource-loader.ts index 38885f4b4..b36c18c72 100644 --- a/src/resource-loader.ts +++ b/src/resource-loader.ts @@ -1,4 +1,4 @@ -import { DefaultResourceLoader, sortExtensionPaths } from '@gsd/pi-coding-agent' +import { DefaultResourceLoader, sortExtensionPaths } from '@sf-run/pi-coding-agent' if (process.env.GSD_DEBUG_EXTENSIONS) process.stderr.write("[gsd-debug] resource-loader.ts loaded\n") import { createHash } from 'node:crypto' import { homedir } from 'node:os' @@ -286,13 +286,13 @@ function copyDirRecursive(src: string, dest: string): void { * Native ESM `import()` ignores NODE_PATH — it resolves packages by walking * up the directory tree from the importing file. Extension files synced to * ~/.gsd/agent/extensions/ have no ancestor node_modules, so imports of - * @gsd/* packages fail. The symlink makes Node's standard resolution find + * @sf-run/* packages fail. The symlink makes Node's standard resolution find * them without requiring every call site to use jiti. * * Layout differences by install method: * - Source/monorepo: packageRoot/node_modules has everything → simple symlink - * - npm/bun global: deps hoisted to dirname(packageRoot), including @gsd/* → simple symlink - * - pnpm global: external deps hoisted, but @gsd/* stays in packageRoot/node_modules + * - npm/bun global: deps hoisted to dirname(packageRoot), including @sf-run/* → simple symlink + * - pnpm global: external deps hoisted, but @sf-run/* stays in packageRoot/node_modules * → merged directory with symlinks from both roots (#3529, #3564) */ function ensureNodeModulesSymlink(agentDir: string): void { @@ -307,7 +307,7 @@ function ensureNodeModulesSymlink(agentDir: string): void { return } - // Global install: check if workspace scopes (@gsd/*) are hoisted. + // Global install: check if workspace scopes (@sf-run/*) are hoisted. // npm/bun hoist everything; pnpm keeps workspace packages internal. if (!hasMissingWorkspaceScopes(hoistedNodeModules, internalNodeModules)) { // Everything is hoisted — simple symlink to parent node_modules @@ -319,7 +319,7 @@ function ensureNodeModulesSymlink(agentDir: string): void { reconcileMergedNodeModules(agentNodeModules, hoistedNodeModules, internalNodeModules) } -/** Check if any @gsd* scopes exist in internal but not in hoisted node_modules */ +/** Check if any @sf-run* scopes exist in internal but not in hoisted node_modules */ function hasMissingWorkspaceScopes(hoisted: string, internal: string): boolean { if (!existsSync(internal)) return false try { @@ -358,8 +358,8 @@ function reconcileSymlink(link: string, target: string): void { /** * Create a real node_modules directory containing symlinks from both the - * hoisted root (external deps) and internal root (@gsd/* workspace packages). - * Used for pnpm global installs where @gsd/* isn't hoisted. + * hoisted root (external deps) and internal root (@sf-run/* workspace packages). + * Used for pnpm global installs where @sf-run/* isn't hoisted. */ function reconcileMergedNodeModules( agentNodeModules: string, @@ -401,7 +401,7 @@ function reconcileMergedNodeModules( } // Overlay internal node_modules entries that weren't hoisted. - // This covers @gsd/* workspace packages AND optional deps like + // This covers @sf-run/* workspace packages AND optional deps like // @anthropic-ai/claude-agent-sdk that npm keeps internal. try { for (const entry of readdirSync(internal, { withFileTypes: true })) { @@ -539,7 +539,7 @@ export function initResources(agentDir: string): void { // Ensure ~/.gsd/agent/node_modules symlinks to GSD's node_modules on EVERY // launch, not just during resource syncs. A stale/broken symlink makes ALL - // extensions fail to resolve @gsd/* packages, rendering GSD non-functional. + // extensions fail to resolve @sf-run/* packages, rendering GSD non-functional. ensureNodeModulesSymlink(agentDir) // Migrate legacy skills on every launch (not gated by manifest) so that diff --git a/src/resources/extensions/ask-user-questions.ts b/src/resources/extensions/ask-user-questions.ts index 3cb7e2ae1..5fb4226e4 100644 --- a/src/resources/extensions/ask-user-questions.ts +++ b/src/resources/extensions/ask-user-questions.ts @@ -9,9 +9,9 @@ * Based on: https://github.com/openai/codex (codex-rs/core/src/tools/handlers/ask_user_questions.rs) */ -import type { ExtensionAPI } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI } from "@sf-run/pi-coding-agent"; import { sanitizeError } from "./shared/sanitize.js"; -import { Text } from "@gsd/pi-tui"; +import { Text } from "@sf-run/pi-tui"; import { Type } from "@sinclair/typebox"; import { showInterviewRound, diff --git a/src/resources/extensions/async-jobs/async-bash-tool.ts b/src/resources/extensions/async-jobs/async-bash-tool.ts index 034fd207e..edd58c225 100644 --- a/src/resources/extensions/async-jobs/async-bash-tool.ts +++ b/src/resources/extensions/async-jobs/async-bash-tool.ts @@ -6,13 +6,13 @@ * with await_job. */ -import type { ToolDefinition } from "@gsd/pi-coding-agent"; +import type { ToolDefinition } from "@sf-run/pi-coding-agent"; import { getShellConfig, sanitizeCommand, DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, -} from "@gsd/pi-coding-agent"; +} from "@sf-run/pi-coding-agent"; import { Type } from "@sinclair/typebox"; import { spawn, spawnSync } from "node:child_process"; import { createWriteStream } from "node:fs"; diff --git a/src/resources/extensions/async-jobs/await-tool.ts b/src/resources/extensions/async-jobs/await-tool.ts index 8d7e8c85c..fcc33ce40 100644 --- a/src/resources/extensions/async-jobs/await-tool.ts +++ b/src/resources/extensions/async-jobs/await-tool.ts @@ -5,7 +5,7 @@ * If omitted, waits for any running job to complete. */ -import type { ToolDefinition } from "@gsd/pi-coding-agent"; +import type { ToolDefinition } from "@sf-run/pi-coding-agent"; import { Type } from "@sinclair/typebox"; import type { AsyncJobManager, Job } from "./job-manager.js"; diff --git a/src/resources/extensions/async-jobs/cancel-job-tool.ts b/src/resources/extensions/async-jobs/cancel-job-tool.ts index 7932d47b3..a5969c107 100644 --- a/src/resources/extensions/async-jobs/cancel-job-tool.ts +++ b/src/resources/extensions/async-jobs/cancel-job-tool.ts @@ -2,7 +2,7 @@ * cancel_job tool — cancel a running background job. */ -import type { ToolDefinition } from "@gsd/pi-coding-agent"; +import type { ToolDefinition } from "@sf-run/pi-coding-agent"; import { Type } from "@sinclair/typebox"; import type { AsyncJobManager } from "./job-manager.js"; diff --git a/src/resources/extensions/async-jobs/index.ts b/src/resources/extensions/async-jobs/index.ts index 3b8009774..eb55cad5f 100644 --- a/src/resources/extensions/async-jobs/index.ts +++ b/src/resources/extensions/async-jobs/index.ts @@ -14,7 +14,7 @@ * /jobs — show running and recent background jobs */ -import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI, ExtensionCommandContext } from "@sf-run/pi-coding-agent"; import { AsyncJobManager, type Job } from "./job-manager.js"; import { createAsyncBashTool } from "./async-bash-tool.js"; import { createAwaitTool } from "./await-tool.js"; diff --git a/src/resources/extensions/aws-auth/index.ts b/src/resources/extensions/aws-auth/index.ts index 969e8ad16..f649b1e49 100644 --- a/src/resources/extensions/aws-auth/index.ts +++ b/src/resources/extensions/aws-auth/index.ts @@ -47,7 +47,7 @@ import { exec } from "node:child_process"; import { existsSync, readFileSync } from "node:fs"; import { join } from "node:path"; import { homedir } from "node:os"; -import type { ExtensionAPI } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI } from "@sf-run/pi-coding-agent"; /** Matches AWS SDK / Bedrock / SSO credential and token errors. */ const AWS_AUTH_ERROR_RE = diff --git a/src/resources/extensions/bg-shell/bg-shell-command.ts b/src/resources/extensions/bg-shell/bg-shell-command.ts index 77d1e999b..ac6769a32 100644 --- a/src/resources/extensions/bg-shell/bg-shell-command.ts +++ b/src/resources/extensions/bg-shell/bg-shell-command.ts @@ -2,8 +2,8 @@ * /bg slash command registration — interactive process manager overlay and CLI subcommands. */ -import type { ExtensionAPI } from "@gsd/pi-coding-agent"; -import { Key } from "@gsd/pi-tui"; +import type { ExtensionAPI } from "@sf-run/pi-coding-agent"; +import { Key } from "@sf-run/pi-tui"; import { shortcutDesc } from "../shared/terminal.js"; import { diff --git a/src/resources/extensions/bg-shell/bg-shell-lifecycle.ts b/src/resources/extensions/bg-shell/bg-shell-lifecycle.ts index 32ee56455..5fcd7a363 100644 --- a/src/resources/extensions/bg-shell/bg-shell-lifecycle.ts +++ b/src/resources/extensions/bg-shell/bg-shell-lifecycle.ts @@ -7,11 +7,11 @@ import type { ExtensionAPI, ExtensionContext, Theme, -} from "@gsd/pi-coding-agent"; +} from "@sf-run/pi-coding-agent"; import { truncateToWidth, visibleWidth, -} from "@gsd/pi-tui"; +} from "@sf-run/pi-tui"; import { processes, @@ -43,7 +43,7 @@ export function registerBgShellLifecycle(pi: ExtensionAPI, state: BgShellSharedS cleanupAll(); // Also kill bash-tool spawned children that bg-shell doesn't track try { - const { listDescendants } = require("@gsd/native") as typeof import("@gsd/native"); + const { listDescendants } = require("@sf-run/native") as typeof import("@sf-run/native"); const descendants = listDescendants(process.pid); for (const childPid of descendants) { try { process.kill(childPid, "SIGKILL"); } catch {} diff --git a/src/resources/extensions/bg-shell/bg-shell-tool.ts b/src/resources/extensions/bg-shell/bg-shell-tool.ts index cfe35858a..8350e9dc4 100644 --- a/src/resources/extensions/bg-shell/bg-shell-tool.ts +++ b/src/resources/extensions/bg-shell/bg-shell-tool.ts @@ -2,9 +2,9 @@ * bg_shell tool registration — the core tool that agents use to manage background processes. */ -import { StringEnum } from "@gsd/pi-ai"; -import type { ExtensionAPI } from "@gsd/pi-coding-agent"; -import { Text } from "@gsd/pi-tui"; +import { StringEnum } from "@sf-run/pi-ai"; +import type { ExtensionAPI } from "@sf-run/pi-coding-agent"; +import { Text } from "@sf-run/pi-tui"; import { Type } from "@sinclair/typebox"; import type { BgProcessInfo, ProcessType } from "./types.js"; diff --git a/src/resources/extensions/bg-shell/index.ts b/src/resources/extensions/bg-shell/index.ts index b6fcb179c..d1fdbe3f5 100644 --- a/src/resources/extensions/bg-shell/index.ts +++ b/src/resources/extensions/bg-shell/index.ts @@ -5,7 +5,7 @@ * block on the full background-process stack before the TUI paints. */ -import { importExtensionModule, type ExtensionAPI, type ExtensionContext } from "@gsd/pi-coding-agent"; +import { importExtensionModule, type ExtensionAPI, type ExtensionContext } from "@sf-run/pi-coding-agent"; import { registerBgShellLifecycle } from "./bg-shell-lifecycle.js"; export interface BgShellSharedState { diff --git a/src/resources/extensions/bg-shell/output-formatter.ts b/src/resources/extensions/bg-shell/output-formatter.ts index a8d59944e..01dc74e42 100644 --- a/src/resources/extensions/bg-shell/output-formatter.ts +++ b/src/resources/extensions/bg-shell/output-formatter.ts @@ -6,7 +6,7 @@ import { truncateHead, DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, -} from "@gsd/pi-coding-agent"; +} from "@sf-run/pi-coding-agent"; import type { BgProcess, OutputDigest, OutputLine, GetOutputOptions } from "./types.js"; import { ERROR_PATTERNS, diff --git a/src/resources/extensions/bg-shell/overlay.ts b/src/resources/extensions/bg-shell/overlay.ts index 5dd6a3872..3d3c0fdbb 100644 --- a/src/resources/extensions/bg-shell/overlay.ts +++ b/src/resources/extensions/bg-shell/overlay.ts @@ -2,8 +2,8 @@ * TUI: Background Process Manager Overlay. */ -import type { Theme } from "@gsd/pi-coding-agent"; -import { truncateToWidth, visibleWidth, matchesKey, Key } from "@gsd/pi-tui"; +import type { Theme } from "@sf-run/pi-coding-agent"; +import { truncateToWidth, visibleWidth, matchesKey, Key } from "@sf-run/pi-tui"; import type { BgProcess, ProcessStatus } from "./types.js"; import { ERROR_PATTERNS, WARNING_PATTERNS } from "./types.js"; import { formatUptime, formatTimeAgo } from "./utilities.js"; diff --git a/src/resources/extensions/bg-shell/process-manager.ts b/src/resources/extensions/bg-shell/process-manager.ts index 659f13e26..83bb83b90 100644 --- a/src/resources/extensions/bg-shell/process-manager.ts +++ b/src/resources/extensions/bg-shell/process-manager.ts @@ -7,7 +7,7 @@ import { spawn, spawnSync } from "node:child_process"; import { randomUUID } from "node:crypto"; import { writeFileSync, readFileSync, existsSync, mkdirSync } from "node:fs"; import { join } from "node:path"; -import { getShellConfig, sanitizeCommand } from "@gsd/pi-coding-agent"; +import { getShellConfig, sanitizeCommand } from "@sf-run/pi-coding-agent"; import { rewriteCommandWithRtk } from "../shared/rtk.js"; import type { BgProcess, diff --git a/src/resources/extensions/browser-tools/index.ts b/src/resources/extensions/browser-tools/index.ts index 35fe7f4c2..657f81059 100644 --- a/src/resources/extensions/browser-tools/index.ts +++ b/src/resources/extensions/browser-tools/index.ts @@ -1,5 +1,5 @@ /** browser-tools — pi extension: full browser interaction via Playwright. */ -import { importExtensionModule, type ExtensionAPI } from "@gsd/pi-coding-agent"; +import { importExtensionModule, type ExtensionAPI } from "@sf-run/pi-coding-agent"; let registrationPromise: Promise | null = null; diff --git a/src/resources/extensions/browser-tools/state.ts b/src/resources/extensions/browser-tools/state.ts index af7505732..4c8839fa3 100644 --- a/src/resources/extensions/browser-tools/state.ts +++ b/src/resources/extensions/browser-tools/state.ts @@ -10,7 +10,7 @@ */ import type { Browser, BrowserContext, Frame, Page } from "playwright"; -import type { ExtensionAPI } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI } from "@sf-run/pi-coding-agent"; import path from "node:path"; import { createActionTimeline, diff --git a/src/resources/extensions/browser-tools/tools/action-cache.ts b/src/resources/extensions/browser-tools/tools/action-cache.ts index e0b492a86..4e330c4c5 100644 --- a/src/resources/extensions/browser-tools/tools/action-cache.ts +++ b/src/resources/extensions/browser-tools/tools/action-cache.ts @@ -1,4 +1,4 @@ -import type { ExtensionAPI } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI } from "@sf-run/pi-coding-agent"; import { Type } from "@sinclair/typebox"; import type { ToolDeps } from "../state.js"; diff --git a/src/resources/extensions/browser-tools/tools/assertions.ts b/src/resources/extensions/browser-tools/tools/assertions.ts index 7bbb8f2b3..16e46a81f 100644 --- a/src/resources/extensions/browser-tools/tools/assertions.ts +++ b/src/resources/extensions/browser-tools/tools/assertions.ts @@ -1,6 +1,6 @@ -import type { ExtensionAPI } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI } from "@sf-run/pi-coding-agent"; import { Type } from "@sinclair/typebox"; -import { StringEnum } from "@gsd/pi-ai"; +import { StringEnum } from "@sf-run/pi-ai"; import { diffCompactStates, evaluateAssertionChecks, diff --git a/src/resources/extensions/browser-tools/tools/codegen.ts b/src/resources/extensions/browser-tools/tools/codegen.ts index afe483c29..43d72d285 100644 --- a/src/resources/extensions/browser-tools/tools/codegen.ts +++ b/src/resources/extensions/browser-tools/tools/codegen.ts @@ -1,4 +1,4 @@ -import type { ExtensionAPI } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI } from "@sf-run/pi-coding-agent"; import { Type } from "@sinclair/typebox"; import type { ToolDeps } from "../state.js"; import { getActionTimeline } from "../state.js"; diff --git a/src/resources/extensions/browser-tools/tools/device.ts b/src/resources/extensions/browser-tools/tools/device.ts index 55c6dd953..c70e3e2af 100644 --- a/src/resources/extensions/browser-tools/tools/device.ts +++ b/src/resources/extensions/browser-tools/tools/device.ts @@ -1,4 +1,4 @@ -import type { ExtensionAPI } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI } from "@sf-run/pi-coding-agent"; import { Type } from "@sinclair/typebox"; import type { ToolDeps } from "../state.js"; diff --git a/src/resources/extensions/browser-tools/tools/extract.ts b/src/resources/extensions/browser-tools/tools/extract.ts index 6aec8665e..533626c7c 100644 --- a/src/resources/extensions/browser-tools/tools/extract.ts +++ b/src/resources/extensions/browser-tools/tools/extract.ts @@ -1,4 +1,4 @@ -import type { ExtensionAPI } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI } from "@sf-run/pi-coding-agent"; import { Type } from "@sinclair/typebox"; import type { ToolDeps } from "../state.js"; diff --git a/src/resources/extensions/browser-tools/tools/forms.ts b/src/resources/extensions/browser-tools/tools/forms.ts index 0ec67798a..1e9ff7942 100644 --- a/src/resources/extensions/browser-tools/tools/forms.ts +++ b/src/resources/extensions/browser-tools/tools/forms.ts @@ -1,4 +1,4 @@ -import type { ExtensionAPI } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI } from "@sf-run/pi-coding-agent"; import { Type } from "@sinclair/typebox"; import type { ToolDeps, CompactPageState } from "../state.js"; import { diff --git a/src/resources/extensions/browser-tools/tools/injection-detect.ts b/src/resources/extensions/browser-tools/tools/injection-detect.ts index c887307b4..1020e16d1 100644 --- a/src/resources/extensions/browser-tools/tools/injection-detect.ts +++ b/src/resources/extensions/browser-tools/tools/injection-detect.ts @@ -1,4 +1,4 @@ -import type { ExtensionAPI } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI } from "@sf-run/pi-coding-agent"; import { Type } from "@sinclair/typebox"; import type { ToolDeps } from "../state.js"; diff --git a/src/resources/extensions/browser-tools/tools/inspection.ts b/src/resources/extensions/browser-tools/tools/inspection.ts index 91f037c20..cd2feae00 100644 --- a/src/resources/extensions/browser-tools/tools/inspection.ts +++ b/src/resources/extensions/browser-tools/tools/inspection.ts @@ -1,6 +1,6 @@ -import type { ExtensionAPI } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI } from "@sf-run/pi-coding-agent"; import { Type } from "@sinclair/typebox"; -import { StringEnum } from "@gsd/pi-ai"; +import { StringEnum } from "@sf-run/pi-ai"; import type { ToolDeps } from "../state.js"; import { getConsoleLogs, diff --git a/src/resources/extensions/browser-tools/tools/intent.ts b/src/resources/extensions/browser-tools/tools/intent.ts index 09d4892b2..05cb474d3 100644 --- a/src/resources/extensions/browser-tools/tools/intent.ts +++ b/src/resources/extensions/browser-tools/tools/intent.ts @@ -1,6 +1,6 @@ -import type { ExtensionAPI } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI } from "@sf-run/pi-coding-agent"; import { Type } from "@sinclair/typebox"; -import { StringEnum } from "@gsd/pi-ai"; +import { StringEnum } from "@sf-run/pi-ai"; import { diffCompactStates } from "../core.js"; import type { ToolDeps, CompactPageState } from "../state.js"; import { diff --git a/src/resources/extensions/browser-tools/tools/interaction.ts b/src/resources/extensions/browser-tools/tools/interaction.ts index d9c594ba5..31882695b 100644 --- a/src/resources/extensions/browser-tools/tools/interaction.ts +++ b/src/resources/extensions/browser-tools/tools/interaction.ts @@ -1,6 +1,6 @@ -import type { ExtensionAPI } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI } from "@sf-run/pi-coding-agent"; import { Type } from "@sinclair/typebox"; -import { StringEnum } from "@gsd/pi-ai"; +import { StringEnum } from "@sf-run/pi-ai"; import { diffCompactStates, } from "../core.js"; diff --git a/src/resources/extensions/browser-tools/tools/navigation.ts b/src/resources/extensions/browser-tools/tools/navigation.ts index e1d5545dc..7da969a81 100644 --- a/src/resources/extensions/browser-tools/tools/navigation.ts +++ b/src/resources/extensions/browser-tools/tools/navigation.ts @@ -1,4 +1,4 @@ -import type { ExtensionAPI } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI } from "@sf-run/pi-coding-agent"; import { Type } from "@sinclair/typebox"; import { diffCompactStates, diff --git a/src/resources/extensions/browser-tools/tools/network-mock.ts b/src/resources/extensions/browser-tools/tools/network-mock.ts index 1122efc8f..7b7271f74 100644 --- a/src/resources/extensions/browser-tools/tools/network-mock.ts +++ b/src/resources/extensions/browser-tools/tools/network-mock.ts @@ -1,4 +1,4 @@ -import type { ExtensionAPI } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI } from "@sf-run/pi-coding-agent"; import { Type } from "@sinclair/typebox"; import type { ToolDeps } from "../state.js"; diff --git a/src/resources/extensions/browser-tools/tools/pages.ts b/src/resources/extensions/browser-tools/tools/pages.ts index dcaeef47e..9b0688d62 100644 --- a/src/resources/extensions/browser-tools/tools/pages.ts +++ b/src/resources/extensions/browser-tools/tools/pages.ts @@ -1,4 +1,4 @@ -import type { ExtensionAPI } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI } from "@sf-run/pi-coding-agent"; import { Type } from "@sinclair/typebox"; import { registryGetActive, diff --git a/src/resources/extensions/browser-tools/tools/pdf.ts b/src/resources/extensions/browser-tools/tools/pdf.ts index 5808aa0d3..3fe76568c 100644 --- a/src/resources/extensions/browser-tools/tools/pdf.ts +++ b/src/resources/extensions/browser-tools/tools/pdf.ts @@ -1,4 +1,4 @@ -import type { ExtensionAPI } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI } from "@sf-run/pi-coding-agent"; import { Type } from "@sinclair/typebox"; import type { ToolDeps } from "../state.js"; diff --git a/src/resources/extensions/browser-tools/tools/refs.ts b/src/resources/extensions/browser-tools/tools/refs.ts index 1534f8bb3..33394230c 100644 --- a/src/resources/extensions/browser-tools/tools/refs.ts +++ b/src/resources/extensions/browser-tools/tools/refs.ts @@ -1,4 +1,4 @@ -import type { ExtensionAPI } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI } from "@sf-run/pi-coding-agent"; import { Type } from "@sinclair/typebox"; import { getSnapshotModeConfig, diff --git a/src/resources/extensions/browser-tools/tools/screenshot.ts b/src/resources/extensions/browser-tools/tools/screenshot.ts index 344911883..8afc326c0 100644 --- a/src/resources/extensions/browser-tools/tools/screenshot.ts +++ b/src/resources/extensions/browser-tools/tools/screenshot.ts @@ -1,4 +1,4 @@ -import type { ExtensionAPI } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI } from "@sf-run/pi-coding-agent"; import { Type } from "@sinclair/typebox"; import type { ToolDeps } from "../state.js"; import { getScreenshotFormatOverride, getScreenshotQualityDefault } from "../capture.js"; diff --git a/src/resources/extensions/browser-tools/tools/session.ts b/src/resources/extensions/browser-tools/tools/session.ts index 19ef2cb20..64f3f93b3 100644 --- a/src/resources/extensions/browser-tools/tools/session.ts +++ b/src/resources/extensions/browser-tools/tools/session.ts @@ -1,4 +1,4 @@ -import type { ExtensionAPI } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI } from "@sf-run/pi-coding-agent"; import { Type } from "@sinclair/typebox"; import { stat } from "node:fs/promises"; import path from "node:path"; diff --git a/src/resources/extensions/browser-tools/tools/state-persistence.ts b/src/resources/extensions/browser-tools/tools/state-persistence.ts index 792284ffd..243be715e 100644 --- a/src/resources/extensions/browser-tools/tools/state-persistence.ts +++ b/src/resources/extensions/browser-tools/tools/state-persistence.ts @@ -1,4 +1,4 @@ -import type { ExtensionAPI } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI } from "@sf-run/pi-coding-agent"; import { Type } from "@sinclair/typebox"; import type { ToolDeps } from "../state.js"; diff --git a/src/resources/extensions/browser-tools/tools/verify.ts b/src/resources/extensions/browser-tools/tools/verify.ts index 6059e607b..96b83e55d 100644 --- a/src/resources/extensions/browser-tools/tools/verify.ts +++ b/src/resources/extensions/browser-tools/tools/verify.ts @@ -1,4 +1,4 @@ -import type { ExtensionAPI } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI } from "@sf-run/pi-coding-agent"; import { Type } from "@sinclair/typebox"; import type { ToolDeps } from "../state.js"; diff --git a/src/resources/extensions/browser-tools/tools/visual-diff.ts b/src/resources/extensions/browser-tools/tools/visual-diff.ts index 760ba8244..fa2453c0e 100644 --- a/src/resources/extensions/browser-tools/tools/visual-diff.ts +++ b/src/resources/extensions/browser-tools/tools/visual-diff.ts @@ -1,4 +1,4 @@ -import type { ExtensionAPI } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI } from "@sf-run/pi-coding-agent"; import { Type } from "@sinclair/typebox"; import type { ToolDeps } from "../state.js"; diff --git a/src/resources/extensions/browser-tools/tools/wait.ts b/src/resources/extensions/browser-tools/tools/wait.ts index 74f9ab342..12f54e1d5 100644 --- a/src/resources/extensions/browser-tools/tools/wait.ts +++ b/src/resources/extensions/browser-tools/tools/wait.ts @@ -1,6 +1,6 @@ -import type { ExtensionAPI } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI } from "@sf-run/pi-coding-agent"; import { Type } from "@sinclair/typebox"; -import { StringEnum } from "@gsd/pi-ai"; +import { StringEnum } from "@sf-run/pi-ai"; import { validateWaitParams, createRegionStableScript, diff --git a/src/resources/extensions/browser-tools/tools/zoom.ts b/src/resources/extensions/browser-tools/tools/zoom.ts index 6a146c345..470062c37 100644 --- a/src/resources/extensions/browser-tools/tools/zoom.ts +++ b/src/resources/extensions/browser-tools/tools/zoom.ts @@ -1,4 +1,4 @@ -import type { ExtensionAPI } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI } from "@sf-run/pi-coding-agent"; import { Type } from "@sinclair/typebox"; import type { ToolDeps } from "../state.js"; diff --git a/src/resources/extensions/browser-tools/utils.ts b/src/resources/extensions/browser-tools/utils.ts index 125aff212..c34f21d89 100644 --- a/src/resources/extensions/browser-tools/utils.ts +++ b/src/resources/extensions/browser-tools/utils.ts @@ -12,7 +12,7 @@ import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, truncateHead, -} from "@gsd/pi-coding-agent"; +} from "@sf-run/pi-coding-agent"; import { beginAction, finishAction, diff --git a/src/resources/extensions/claude-code-cli/index.ts b/src/resources/extensions/claude-code-cli/index.ts index 628df3238..2d57aada3 100644 --- a/src/resources/extensions/claude-code-cli/index.ts +++ b/src/resources/extensions/claude-code-cli/index.ts @@ -11,7 +11,7 @@ * never touches credentials, never offers a login flow. */ -import type { ExtensionAPI } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI } from "@sf-run/pi-coding-agent"; import { CLAUDE_CODE_MODELS } from "./models.js"; import { isClaudeCodeReady } from "./readiness.js"; import { streamViaClaudeCode } from "./stream-adapter.js"; diff --git a/src/resources/extensions/claude-code-cli/package.json b/src/resources/extensions/claude-code-cli/package.json index b22297d08..de619e744 100644 --- a/src/resources/extensions/claude-code-cli/package.json +++ b/src/resources/extensions/claude-code-cli/package.json @@ -1,5 +1,5 @@ { - "name": "@gsd/claude-code-cli", + "name": "@sf-run/claude-code-cli", "private": true, "version": "1.0.0", "type": "module", diff --git a/src/resources/extensions/claude-code-cli/partial-builder.ts b/src/resources/extensions/claude-code-cli/partial-builder.ts index 0f52bc220..c71aec6f3 100644 --- a/src/resources/extensions/claude-code-cli/partial-builder.ts +++ b/src/resources/extensions/claude-code-cli/partial-builder.ts @@ -15,8 +15,8 @@ import type { ToolCall, Usage, WebSearchResultContent, -} from "@gsd/pi-ai"; -import { hasXmlParameterTags, repairToolJson } from "@gsd/pi-ai"; +} from "@sf-run/pi-ai"; +import { hasXmlParameterTags, repairToolJson } from "@sf-run/pi-ai"; import type { BetaContentBlock, BetaRawMessageStreamEvent, NonNullableUsage } from "./sdk-types.js"; // --------------------------------------------------------------------------- diff --git a/src/resources/extensions/claude-code-cli/stream-adapter.ts b/src/resources/extensions/claude-code-cli/stream-adapter.ts index 5665406ac..5d0ce7ad9 100644 --- a/src/resources/extensions/claude-code-cli/stream-adapter.ts +++ b/src/resources/extensions/claude-code-cli/stream-adapter.ts @@ -16,9 +16,9 @@ import type { SimpleStreamOptions, ThinkingLevel, ToolCall, -} from "@gsd/pi-ai"; -import type { ExtensionUIContext } from "@gsd/pi-coding-agent"; -import { EventStream, mapThinkingLevelToEffort, supportsAdaptiveThinking } from "@gsd/pi-ai"; +} from "@sf-run/pi-ai"; +import type { ExtensionUIContext } from "@sf-run/pi-coding-agent"; +import { EventStream, mapThinkingLevelToEffort, supportsAdaptiveThinking } from "@sf-run/pi-ai"; import { execSync } from "node:child_process"; import { PartialMessageBuilder, ZERO_USAGE, mapUsage } from "./partial-builder.js"; import { buildWorkflowMcpServers } from "../gsd/workflow-mcp.js"; @@ -131,7 +131,7 @@ const SENSITIVE_FIELD_PATTERN = /(password|passphrase|secret|token|api[_\s-]*key /** * Construct an AssistantMessageEventStream using EventStream directly. - * (The class itself is only re-exported as a type from the @gsd/pi-ai barrel.) + * (The class itself is only re-exported as a type from the @sf-run/pi-ai barrel.) */ function createAssistantStream(): AssistantMessageEventStream { return new EventStream( diff --git a/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts b/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts index d8158f7bb..cbb28319f 100644 --- a/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +++ b/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts @@ -21,7 +21,7 @@ import { parseClaudeLookupOutput, roundResultToElicitationContent, } from "../stream-adapter.ts"; -import type { AssistantMessage, Context, Message } from "@gsd/pi-ai"; +import type { AssistantMessage, Context, Message } from "@sf-run/pi-ai"; import type { SDKUserMessage } from "../sdk-types.ts"; // --------------------------------------------------------------------------- diff --git a/src/resources/extensions/cmux/package.json b/src/resources/extensions/cmux/package.json index 6eca7fa6a..309571b89 100644 --- a/src/resources/extensions/cmux/package.json +++ b/src/resources/extensions/cmux/package.json @@ -1,5 +1,5 @@ { - "name": "@gsd/cmux", + "name": "@sf-run/cmux", "private": true, "type": "module", "description": "cmux integration library — used by other extensions, not an extension itself", diff --git a/src/resources/extensions/context7/index.ts b/src/resources/extensions/context7/index.ts index 2b8e60229..802b7c68a 100644 --- a/src/resources/extensions/context7/index.ts +++ b/src/resources/extensions/context7/index.ts @@ -22,14 +22,14 @@ * export CONTEXT7_API_KEY=your_key (get one at context7.com/dashboard) */ -import type { ExtensionAPI } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI } from "@sf-run/pi-coding-agent"; import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, truncateHead, -} from "@gsd/pi-coding-agent"; -import { Text } from "@gsd/pi-tui"; +} from "@sf-run/pi-coding-agent"; +import { Text } from "@sf-run/pi-tui"; import { Type } from "@sinclair/typebox"; // ─── API types ──────────────────────────────────────────────────────────────── diff --git a/src/resources/extensions/get-secrets-from-user.ts b/src/resources/extensions/get-secrets-from-user.ts index 967752048..dd7085d04 100644 --- a/src/resources/extensions/get-secrets-from-user.ts +++ b/src/resources/extensions/get-secrets-from-user.ts @@ -10,8 +10,8 @@ import { readFile, writeFile } from "node:fs/promises"; import { existsSync, statSync } from "node:fs"; import { resolve } from "node:path"; -import type { ExtensionAPI, Theme } from "@gsd/pi-coding-agent"; -import { Editor, type EditorTheme, Key, matchesKey, Text, truncateToWidth, wrapTextWithAnsi } from "@gsd/pi-tui"; +import type { ExtensionAPI, Theme } from "@sf-run/pi-coding-agent"; +import { Editor, type EditorTheme, Key, matchesKey, Text, truncateToWidth, wrapTextWithAnsi } from "@sf-run/pi-tui"; import { Type } from "@sinclair/typebox"; import { makeUI } from "./shared/tui.js"; import { maskEditorLine, type ProgressStatus } from "./shared/mod.js"; @@ -86,7 +86,7 @@ async function writeEnvKey(filePath: string, key: string, value: string): Promis // ─── Exported utilities ─────────────────────────────────────────────────────── // Re-export from env-utils.ts so existing consumers still work. -// The implementation lives in env-utils.ts to avoid pulling @gsd/pi-tui +// The implementation lives in env-utils.ts to avoid pulling @sf-run/pi-tui // into modules that only need env-checking (e.g. files.ts during reports). import { checkExistingEnvKeys } from "./gsd/env-utils.js"; export { checkExistingEnvKeys }; diff --git a/src/resources/extensions/github-sync/index.ts b/src/resources/extensions/github-sync/index.ts index 9f6732f19..f82b07a83 100644 --- a/src/resources/extensions/github-sync/index.ts +++ b/src/resources/extensions/github-sync/index.ts @@ -10,7 +10,7 @@ * and status display. */ -import type { ExtensionAPI } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI } from "@sf-run/pi-coding-agent"; import { bootstrapSync } from "./sync.js"; import { loadSyncMapping } from "./mapping.js"; import { ghIsAvailable } from "./cli.js"; @@ -39,7 +39,7 @@ export default function (pi: ExtensionAPI) { }); } -async function showStatus(ctx: import("@gsd/pi-coding-agent").ExtensionCommandContext) { +async function showStatus(ctx: import("@sf-run/pi-coding-agent").ExtensionCommandContext) { if (!ghIsAvailable()) { ctx.ui.notify("GitHub sync: `gh` CLI not installed or not authenticated.", "warning"); return; @@ -69,7 +69,7 @@ async function showStatus(ctx: import("@gsd/pi-coding-agent").ExtensionCommandCo ); } -async function runBootstrap(ctx: import("@gsd/pi-coding-agent").ExtensionCommandContext) { +async function runBootstrap(ctx: import("@sf-run/pi-coding-agent").ExtensionCommandContext) { if (!ghIsAvailable()) { ctx.ui.notify("GitHub sync: `gh` CLI not installed or not authenticated.", "warning"); return; diff --git a/src/resources/extensions/google-search/index.ts b/src/resources/extensions/google-search/index.ts index a4f9818f4..6008df016 100644 --- a/src/resources/extensions/google-search/index.ts +++ b/src/resources/extensions/google-search/index.ts @@ -10,14 +10,14 @@ * returns it with source URLs from grounding metadata. */ -import type { ExtensionAPI } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI } from "@sf-run/pi-coding-agent"; import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, truncateHead, -} from "@gsd/pi-coding-agent"; -import { Text } from "@gsd/pi-tui"; +} from "@sf-run/pi-coding-agent"; +import { Text } from "@sf-run/pi-tui"; import { Type } from "@sinclair/typebox"; // ── Types ──────────────────────────────────────────────────────────────────── diff --git a/src/resources/extensions/gsd/activity-log.ts b/src/resources/extensions/gsd/activity-log.ts index 5e93c0240..f9155d967 100644 --- a/src/resources/extensions/gsd/activity-log.ts +++ b/src/resources/extensions/gsd/activity-log.ts @@ -14,7 +14,7 @@ import { join } from "node:path"; import { GSDError, GSD_IO_ERROR } from "./errors.js"; const SEQ_PREFIX_RE = /^(\d+)-/; -import type { ExtensionContext } from "@gsd/pi-coding-agent"; +import type { ExtensionContext } from "@sf-run/pi-coding-agent"; import { gsdRoot } from "./paths.js"; import { buildAuditEnvelope, emitUokAuditEvent } from "./uok/audit.js"; import { isUnifiedAuditEnabled } from "./uok/audit-toggle.js"; diff --git a/src/resources/extensions/gsd/auto-dashboard.ts b/src/resources/extensions/gsd/auto-dashboard.ts index e18e24599..2c5c81f82 100644 --- a/src/resources/extensions/gsd/auto-dashboard.ts +++ b/src/resources/extensions/gsd/auto-dashboard.ts @@ -12,7 +12,7 @@ import type { SessionMessageEntry, ReadonlyFooterDataProvider, Theme, -} from "@gsd/pi-coding-agent"; +} from "@sf-run/pi-coding-agent"; import type { GSDState } from "./types.js"; import { getCurrentBranch } from "./worktree.js"; import { getActiveHook } from "./post-unit-hooks.js"; @@ -25,7 +25,7 @@ import { import { isDbAvailable, getMilestoneSlices, getSliceTasks } from "./gsd-db.js"; import { readFileSync, writeFileSync, existsSync } from "node:fs"; import { execFileSync } from "node:child_process"; -import { truncateToWidth, visibleWidth } from "@gsd/pi-tui"; +import { truncateToWidth, visibleWidth } from "@sf-run/pi-tui"; import { makeUI } from "../shared/tui.js"; import { GLYPH, INDENT } from "../shared/mod.js"; import { computeProgressScore } from "./progress-score.js"; diff --git a/src/resources/extensions/gsd/auto-direct-dispatch.ts b/src/resources/extensions/gsd/auto-direct-dispatch.ts index 306bca441..9a6f4c2a9 100644 --- a/src/resources/extensions/gsd/auto-direct-dispatch.ts +++ b/src/resources/extensions/gsd/auto-direct-dispatch.ts @@ -6,7 +6,7 @@ import type { ExtensionAPI, ExtensionCommandContext, -} from "@gsd/pi-coding-agent"; +} from "@sf-run/pi-coding-agent"; import { deriveState } from "./state.js"; import { loadFile } from "./files.js"; diff --git a/src/resources/extensions/gsd/auto-dispatch.ts b/src/resources/extensions/gsd/auto-dispatch.ts index 4ce22e36e..0f5d727d5 100644 --- a/src/resources/extensions/gsd/auto-dispatch.ts +++ b/src/resources/extensions/gsd/auto-dispatch.ts @@ -123,7 +123,7 @@ const MAX_REWRITE_ATTEMPTS = 3; // ─── Disk-persisted rewrite attempt counter ────────────────────────────────── // The counter must survive session restarts (crash recovery, pause/resume, // step-mode). Storing it on the in-memory session object caused the circuit -// breaker to never trip — see https://github.com/gsd-build/gsd-2/issues/2203 +// breaker to never trip — see https://github.com/singularity-forge/sf-run/issues/2203 function rewriteCountPath(basePath: string): string { return join(gsdRoot(basePath), "runtime", "rewrite-count.json"); } @@ -176,7 +176,7 @@ export function incrementUatCount(basePath: string, mid: string, sid: string): n * operational verification is needed. Covers common phrasings the planning * agent may use: "None", "None required", "N/A", "Not applicable", etc. * - * @see https://github.com/gsd-build/gsd-2/issues/2931 + * @see https://github.com/singularity-forge/sf-run/issues/2931 */ export function isVerificationNotApplicable(value: string): boolean { const v = (value ?? "").toLowerCase().trim().replace(/[.\s]+$/, ""); diff --git a/src/resources/extensions/gsd/auto-model-selection.ts b/src/resources/extensions/gsd/auto-model-selection.ts index 57a2b8904..7a640345d 100644 --- a/src/resources/extensions/gsd/auto-model-selection.ts +++ b/src/resources/extensions/gsd/auto-model-selection.ts @@ -4,11 +4,11 @@ * and fallback chains. */ -import type { Api, Model } from "@gsd/pi-ai"; -import { getProviderCapabilities } from "@gsd/pi-ai"; -import type { ExtensionAPI, ExtensionContext } from "@gsd/pi-coding-agent"; +import type { Api, Model } from "@sf-run/pi-ai"; +import { getProviderCapabilities } from "@sf-run/pi-ai"; +import type { ExtensionAPI, ExtensionContext } from "@sf-run/pi-coding-agent"; import type { GSDPreferences } from "./preferences.js"; -import { resolveModelWithFallbacksForUnit, resolveDynamicRoutingConfig } from "./preferences.js"; +import { resolveModelWithFallbacksForUnit, resolveDynamicRoutingConfig, resolvePersistModelChanges } from "./preferences.js"; import type { ComplexityTier } from "./complexity-classifier.js"; import { classifyUnitComplexity, extractTaskMetadata, tierLabel } from "./complexity-classifier.js"; import { resolveModelForComplexity, escalateTier, getEligibleModels, loadCapabilityOverrides, adjustToolSet, filterToolsForProvider } from "./model-router.js"; @@ -78,6 +78,7 @@ export async function selectAndApplyModel( sessionModelOverride?: { provider: string; id: string } | null, ): Promise { const uokFlags = resolveUokFlags(prefs); + const persistModelChanges = resolvePersistModelChanges(); const effectiveSessionModelOverride = sessionModelOverride === undefined ? getSessionModelOverride(ctx.sessionManager.getSessionId()) : (sessionModelOverride ?? undefined); @@ -335,7 +336,7 @@ export async function selectAndApplyModel( } } - const ok = await pi.setModel(model, { persist: false }); + const ok = await pi.setModel(model, { persist: persistModelChanges }); if (ok) { appliedModel = model; @@ -364,12 +365,14 @@ export async function selectAndApplyModel( pi.setActiveTools(finalToolNames); } - if (verbose) { + { const fallbackNote = modelId === effectiveModelConfig.primary ? "" : ` (fallback from ${effectiveModelConfig.primary})`; const phase = unitPhaseLabel(unitType); ctx.ui.notify(`Model [${phase}]${routingTierLabel}: ${model.provider}/${model.id}${fallbackNote}`, "info"); + } + if (verbose) { // ADR-005: Report tools filtered due to provider incompatibility if (removedTools.length > 0) { ctx.ui.notify( @@ -400,11 +403,11 @@ export async function selectAndApplyModel( m => m.provider === autoModeStartModel.provider && m.id === autoModeStartModel.id, ); if (startModel) { - const ok = await pi.setModel(startModel, { persist: false }); + const ok = await pi.setModel(startModel, { persist: persistModelChanges }); if (!ok) { const byId = availableModels.find(m => m.id === autoModeStartModel.id); if (byId) { - const fallbackOk = await pi.setModel(byId, { persist: false }); + const fallbackOk = await pi.setModel(byId, { persist: persistModelChanges }); if (fallbackOk) appliedModel = byId; } } else { diff --git a/src/resources/extensions/gsd/auto-post-unit.ts b/src/resources/extensions/gsd/auto-post-unit.ts index 3b59555da..698dc524e 100644 --- a/src/resources/extensions/gsd/auto-post-unit.ts +++ b/src/resources/extensions/gsd/auto-post-unit.ts @@ -11,7 +11,7 @@ * Extracted from handleAgentEnd() in auto.ts. */ -import type { ExtensionContext, ExtensionAPI } from "@gsd/pi-coding-agent"; +import type { ExtensionContext, ExtensionAPI } from "@sf-run/pi-coding-agent"; import { deriveState } from "./state.js"; import { logWarning, logError } from "./workflow-logger.js"; import { loadFile, parseSummary, resolveAllOverrides } from "./files.js"; diff --git a/src/resources/extensions/gsd/auto-prompts.ts b/src/resources/extensions/gsd/auto-prompts.ts index f933110b9..b64bf0604 100644 --- a/src/resources/extensions/gsd/auto-prompts.ts +++ b/src/resources/extensions/gsd/auto-prompts.ts @@ -20,7 +20,7 @@ import { resolveSkillDiscoveryMode, resolveInlineLevel, loadEffectiveGSDPreferen import { parseRoadmap } from "./parsers-legacy.js"; import type { GSDState, InlineLevel } from "./types.js"; import type { GSDPreferences } from "./preferences.js"; -import { getLoadedSkills, type Skill } from "@gsd/pi-coding-agent"; +import { getLoadedSkills, type Skill } from "@sf-run/pi-coding-agent"; import { join, basename } from "node:path"; import { existsSync } from "node:fs"; import { computeBudgets, resolveExecutorContextWindow, truncateAtSectionBoundary } from "./context-budget.js"; diff --git a/src/resources/extensions/gsd/auto-recovery.ts b/src/resources/extensions/gsd/auto-recovery.ts index 3fb3d8336..6e35d2ff3 100644 --- a/src/resources/extensions/gsd/auto-recovery.ts +++ b/src/resources/extensions/gsd/auto-recovery.ts @@ -7,7 +7,7 @@ * globals or AutoContext dependency. */ -import type { ExtensionContext } from "@gsd/pi-coding-agent"; +import type { ExtensionContext } from "@sf-run/pi-coding-agent"; import { parseUnitId } from "./unit-id.js"; import { appendEvent } from "./workflow-events.js"; import { atomicWriteSync } from "./atomic-write.js"; diff --git a/src/resources/extensions/gsd/auto-start.ts b/src/resources/extensions/gsd/auto-start.ts index 973d18912..696709359 100644 --- a/src/resources/extensions/gsd/auto-start.ts +++ b/src/resources/extensions/gsd/auto-start.ts @@ -12,7 +12,7 @@ import type { ExtensionAPI, ExtensionCommandContext, -} from "@gsd/pi-coding-agent"; +} from "@sf-run/pi-coding-agent"; import { deriveState } from "./state.js"; import { loadFile, getManifestStatus } from "./files.js"; import type { InterruptedSessionAssessment } from "./interrupted-session.js"; @@ -20,6 +20,7 @@ import { loadEffectiveGSDPreferences, resolveSkillDiscoveryMode, getIsolationMode, + resolvePersistModelChanges, } from "./preferences.js"; import { ensureGsdSymlink, isInheritedRepo, validateProjectId } from "./repo-identity.js"; import { migrateToExternalState, recoverFailedMigration } from "./migrate-external.js"; @@ -790,7 +791,7 @@ export async function bootstrapAutoSession( const { resolveModelId } = await import("./auto-model-selection.js"); const overrideModel = resolveModelId(workerModelOverride, availableModels, ctx.model?.provider); if (overrideModel) { - const ok = await pi.setModel(overrideModel, { persist: false }); + const ok = await pi.setModel(overrideModel, { persist: resolvePersistModelChanges() }); if (ok) { // Update start model so all subsequent units use this as the baseline s.autoModeStartModel = { provider: overrideModel.provider, id: overrideModel.id }; diff --git a/src/resources/extensions/gsd/auto-timeout-recovery.ts b/src/resources/extensions/gsd/auto-timeout-recovery.ts index fa385fc1b..28eea6032 100644 --- a/src/resources/extensions/gsd/auto-timeout-recovery.ts +++ b/src/resources/extensions/gsd/auto-timeout-recovery.ts @@ -4,7 +4,7 @@ * and blocker placeholder generation. */ -import type { ExtensionAPI, ExtensionContext } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI, ExtensionContext } from "@sf-run/pi-coding-agent"; import { readUnitRuntimeRecord, writeUnitRuntimeRecord, diff --git a/src/resources/extensions/gsd/auto-timers.ts b/src/resources/extensions/gsd/auto-timers.ts index 3b7b11f81..2324071a6 100644 --- a/src/resources/extensions/gsd/auto-timers.ts +++ b/src/resources/extensions/gsd/auto-timers.ts @@ -6,7 +6,7 @@ * via startUnitSupervision() and torn down by the caller via clearUnitTimeout(). */ -import type { ExtensionAPI, ExtensionContext } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI, ExtensionContext } from "@sf-run/pi-coding-agent"; import { readUnitRuntimeRecord, writeUnitRuntimeRecord } from "./unit-runtime.js"; import { isDbAvailable, getMilestoneSlices, getSliceTasks } from "./gsd-db.js"; import { resolveAutoSupervisorConfig } from "./preferences.js"; diff --git a/src/resources/extensions/gsd/auto-tool-tracking.ts b/src/resources/extensions/gsd/auto-tool-tracking.ts index cab495813..ffe54cdd4 100644 --- a/src/resources/extensions/gsd/auto-tool-tracking.ts +++ b/src/resources/extensions/gsd/auto-tool-tracking.ts @@ -84,6 +84,29 @@ export function clearInFlightTools(): void { inFlightTools.clear(); } +const MAX_TOP_TOOLS_IN_SUMMARY = 5; +const toolCallCountsByName = new Map(); + +export function resetToolCallCounts(): void { + toolCallCountsByName.clear(); +} + +export function recordToolCallName(toolName: string): void { + if (!toolName) return; + toolCallCountsByName.set(toolName, (toolCallCountsByName.get(toolName) ?? 0) + 1); +} + +export function formatToolCallSummary(): string | null { + if (toolCallCountsByName.size === 0) return null; + let total = 0; + for (const count of toolCallCountsByName.values()) total += count; + const ranked = [...toolCallCountsByName.entries()] + .sort((a, b) => b[1] - a[1]) + .slice(0, MAX_TOP_TOOLS_IN_SUMMARY) + .map(([name, count]) => `${name}×${count}`); + return `${total} calls (top-${ranked.length}: ${ranked.join(", ")})`; +} + // ─── Tool invocation error classification (#2883) ──────────────────────── /** diff --git a/src/resources/extensions/gsd/auto-unit-closeout.ts b/src/resources/extensions/gsd/auto-unit-closeout.ts index 45d8dce78..5e54480a9 100644 --- a/src/resources/extensions/gsd/auto-unit-closeout.ts +++ b/src/resources/extensions/gsd/auto-unit-closeout.ts @@ -4,7 +4,7 @@ * that appears 6+ times in auto.ts. */ -import type { ExtensionContext } from "@gsd/pi-coding-agent"; +import type { ExtensionContext } from "@sf-run/pi-coding-agent"; import { snapshotUnitMetrics } from "./metrics.js"; import { saveActivityLog } from "./activity-log.js"; import { logWarning } from "./workflow-logger.js"; diff --git a/src/resources/extensions/gsd/auto-verification.ts b/src/resources/extensions/gsd/auto-verification.ts index d97483110..09182bb88 100644 --- a/src/resources/extensions/gsd/auto-verification.ts +++ b/src/resources/extensions/gsd/auto-verification.ts @@ -10,7 +10,7 @@ * checks the result and handles control flow. */ -import type { ExtensionContext, ExtensionAPI } from "@gsd/pi-coding-agent"; +import type { ExtensionContext, ExtensionAPI } from "@sf-run/pi-coding-agent"; import { mkdirSync, writeFileSync } from "node:fs"; import { resolveSliceFile, resolveSlicePath, resolveMilestoneFile } from "./paths.js"; import { parseUnitId } from "./unit-id.js"; @@ -296,12 +296,19 @@ export async function runPostUnitVerification( if (result.checks.length > 0) { const passCount = result.checks.filter((c) => c.exitCode === 0).length; const total = result.checks.length; + const commandList = result.checks.map((c) => c.command).join(" | "); + ctx.ui.notify(`[verify] running: ${commandList}`, "info"); + const attemptSoFar = s.verificationRetryCount.get(s.currentUnit.id) ?? 0; if (result.passed) { - ctx.ui.notify(`Verification gate: ${passCount}/${total} checks passed`); + ctx.ui.notify(`[verify] PASS - ${passCount}/${total} checks`, "info"); } else { const failures = result.checks.filter((c) => c.exitCode !== 0); const failNames = failures.map((f) => f.command).join(", "); - ctx.ui.notify(`Verification gate: FAILED — ${failNames}`); + const nextAttempt = attemptSoFar + 1; + ctx.ui.notify( + `[verify] FAIL - ${failNames} (auto-fix attempt ${nextAttempt}/${maxRetries})`, + "info", + ); process.stderr.write( `verification-gate: ${total - passCount}/${total} checks failed\n`, ); diff --git a/src/resources/extensions/gsd/auto.ts b/src/resources/extensions/gsd/auto.ts index e39653e15..2aad24e86 100644 --- a/src/resources/extensions/gsd/auto.ts +++ b/src/resources/extensions/gsd/auto.ts @@ -14,7 +14,7 @@ import type { ExtensionAPI, ExtensionContext, ExtensionCommandContext, -} from "@gsd/pi-coding-agent"; +} from "@sf-run/pi-coding-agent"; import { deriveState } from "./state.js"; import { parseUnitId } from "./unit-id.js"; diff --git a/src/resources/extensions/gsd/auto/loop-deps.ts b/src/resources/extensions/gsd/auto/loop-deps.ts index a92612098..6444c2395 100644 --- a/src/resources/extensions/gsd/auto/loop-deps.ts +++ b/src/resources/extensions/gsd/auto/loop-deps.ts @@ -4,7 +4,7 @@ * Leaf node in the import DAG (type-only). */ -import type { ExtensionAPI, ExtensionContext } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI, ExtensionContext } from "@sf-run/pi-coding-agent"; import type { AutoSession } from "./session.js"; import type { GSDPreferences } from "../preferences.js"; diff --git a/src/resources/extensions/gsd/auto/loop.ts b/src/resources/extensions/gsd/auto/loop.ts index a35f8d672..cb5d012a0 100644 --- a/src/resources/extensions/gsd/auto/loop.ts +++ b/src/resources/extensions/gsd/auto/loop.ts @@ -7,7 +7,7 @@ * Imports from: auto/types, auto/resolve, auto/phases */ -import type { ExtensionAPI, ExtensionContext } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI, ExtensionContext } from "@sf-run/pi-coding-agent"; import { randomUUID } from "node:crypto"; import type { AutoSession, SidecarItem } from "./session.js"; diff --git a/src/resources/extensions/gsd/auto/phases.ts b/src/resources/extensions/gsd/auto/phases.ts index c4ad7f4d9..d093edd10 100644 --- a/src/resources/extensions/gsd/auto/phases.ts +++ b/src/resources/extensions/gsd/auto/phases.ts @@ -7,7 +7,7 @@ * Imports from: auto/types, auto/detect-stuck, auto/run-unit, auto/loop-deps */ -import { importExtensionModule, type ExtensionAPI, type ExtensionContext } from "@gsd/pi-coding-agent"; +import { importExtensionModule, type ExtensionAPI, type ExtensionContext } from "@sf-run/pi-coding-agent"; import type { AutoSession, SidecarItem } from "./session.js"; import type { LoopDeps } from "./loop-deps.js"; @@ -52,12 +52,15 @@ import { ensurePlanV2Graph } from "../uok/plan-v2.js"; import { resolveUokFlags } from "../uok/flags.js"; import { UokGateRunner } from "../uok/gate-runner.js"; import { resetEvidence } from "../safety/evidence-collector.js"; +import { resetToolCallCounts, formatToolCallSummary } from "../auto-tool-tracking.js"; import { createCheckpoint, cleanupCheckpoint, rollbackToCheckpoint } from "../safety/git-checkpoint.js"; import { resolveSafetyHarnessConfig } from "../safety/safety-harness.js"; import { getWorkflowTransportSupportError, getRequiredWorkflowToolsForAutoUnit, } from "../workflow-mcp.js"; +import { resolvePersistModelChanges } from "../preferences.js"; +import { recordLearnedOutcome } from "../learning/runtime.js"; // ─── generateMilestoneReport ────────────────────────────────────────────────── @@ -92,6 +95,70 @@ function shouldRunPlanV2Gate(phase: Phase): boolean { return PLAN_V2_GATE_PHASES.has(phase); } +function shouldSkipArtifactVerification(unitType: string): boolean { + return unitType.startsWith("hook/") || unitType === "custom-step"; +} + +function recordLearningOutcomeForUnit( + ic: IterationContext, + unitType: string, + unitId: string, + startedAt: number | undefined, + outcome: { + succeeded: boolean; + verificationPassed: boolean | null; + blockerDiscovered?: boolean; + retries?: number; + escalated?: boolean; + }, +): void { + if (!startedAt) return; + const unitModel = ic.s.currentUnitModel; + const unitEntry = (ic.deps.getLedger() as { + units?: Array<{ + type: string; + id: string; + startedAt: number; + finishedAt: number; + model: string; + cost: number; + tokens: { total: number }; + }>; + } | null)?.units + ? [...((ic.deps.getLedger() as { + units?: Array<{ + type: string; + id: string; + startedAt: number; + finishedAt: number; + model: string; + cost: number; + tokens: { total: number }; + }>; + } | null)?.units ?? [])].reverse().find( + (u) => u.type === unitType && u.id === unitId && u.startedAt === startedAt, + ) + : undefined; + const provider = unitModel?.provider ?? null; + const modelId = unitModel?.id ?? unitEntry?.model ?? null; + if (!provider || !modelId || !unitEntry) return; + recordLearnedOutcome({ + modelId, + provider, + unitType, + unitId, + succeeded: outcome.succeeded, + retries: outcome.retries ?? 0, + escalated: outcome.escalated ?? false, + verification_passed: outcome.verificationPassed, + blocker_discovered: outcome.blockerDiscovered ?? false, + duration_ms: Math.max(0, unitEntry.finishedAt - unitEntry.startedAt), + tokens_total: unitEntry.tokens.total, + cost_usd: unitEntry.cost, + recorded_at: unitEntry.startedAt, + }); +} + /** * Generate and write an HTML milestone report snapshot. * Extracted from the milestone-transition block in autoLoop. @@ -787,7 +854,7 @@ export async function runDispatch( // gate) — pause instead of hard-stopping so the session is resumable with // `/gsd auto`. Error/info-level stops remain hard stops for infrastructure // failures and terminal conditions respectively. - // See: https://github.com/gsd-build/gsd-2/issues/2474 + // See: https://github.com/singularity-forge/sf-run/issues/2474 if (dispatchResult.level === "warning") { ctx.ui.notify(dispatchResult.reason, "warning"); await deps.pauseAuto(ctx, pi); @@ -1235,8 +1302,10 @@ export async function runUnitPhase( s.lastGitActionStatus = null; setCurrentPhase(unitType); s.lastToolInvocationError = null; // #2883: clear stale error from previous unit + resetToolCallCounts(); const unitStartSeq = ic.nextSeq(); deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: unitStartSeq, eventType: "unit-start", data: { unitType, unitId } }); + ctx.ui.notify(`[unit] ${unitType} ${unitId} starting`, "info"); deps.captureAvailableSkills(); writeUnitRuntimeRecord( s.basePath, @@ -1363,7 +1432,7 @@ export async function runUnitPhase( const availableModels = ctx.modelRegistry.getAvailable(); const match = deps.resolveModelId(hookModelOverride, availableModels, ctx.model?.provider); if (match) { - const ok = await pi.setModel(match, { persist: false }); + const ok = await pi.setModel(match, { persist: resolvePersistModelChanges() }); if (ok) { s.currentUnitModel = match as AutoSession["currentUnitModel"]; ctx.ui.notify(`Hook model override: ${match.provider}/${match.id}`, "info"); @@ -1582,6 +1651,10 @@ export async function runUnitPhase( `${unitType} ${unitId} completed with 0 tool calls — context exhaustion, will retry`, "warning", ); + recordLearningOutcomeForUnit(ic, unitType, unitId, s.currentUnit?.startedAt, { + succeeded: false, + verificationPassed: null, + }); // Fall through to next iteration where dispatch will re-derive // and re-dispatch this unit. return { action: "next", data: { unitStartedAt: s.currentUnit?.startedAt } }; @@ -1597,7 +1670,7 @@ export async function runUnitPhase( ); } - const skipArtifactVerification = unitType.startsWith("hook/") || unitType === "custom-step"; + const skipArtifactVerification = shouldSkipArtifactVerification(unitType); const artifactVerified = skipArtifactVerification || verifyExpectedArtifact(unitType, unitId, s.basePath); @@ -1625,8 +1698,44 @@ export async function runUnitPhase( } } + if (unitResult.status !== "completed" || !artifactVerified) { + recordLearningOutcomeForUnit(ic, unitType, unitId, s.currentUnit?.startedAt, { + succeeded: false, + verificationPassed: null, + }); + } + deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "unit-end", data: { unitType, unitId, status: unitResult.status, artifactVerified, ...(unitResult.errorContext ? { errorContext: unitResult.errorContext } : {}) }, causedBy: { flowId: ic.flowId, seq: unitStartSeq } }); + { + const verdict = unitResult.status === "completed" + ? (artifactVerified ? "success" : "blocked") + : unitResult.status === "error" + ? "fail" + : unitResult.status; + const ledger = deps.getLedger() as { + units?: Array<{ type: string; id: string; startedAt: number; cost: number; tokens: { total: number }; toolCalls: number }>; + } | null; + const unitEntry = ledger?.units + ? [...ledger.units].reverse().find( + (u) => u.type === unitType && u.id === unitId && u.startedAt === s.currentUnit?.startedAt, + ) + : undefined; + if (unitEntry) { + const costStr = deps.formatCost(unitEntry.cost); + ctx.ui.notify( + `[unit] ${unitType} ${unitId} ended -> ${verdict} (${costStr}, ${unitEntry.tokens.total} tokens, ${unitEntry.toolCalls} tool calls)`, + "info", + ); + } else { + ctx.ui.notify(`[unit] ${unitType} ${unitId} ended -> ${verdict}`, "info"); + } + const toolSummary = formatToolCallSummary(); + if (toolSummary) { + ctx.ui.notify(`[mcp] ${toolSummary}`, "info"); + } + } + // ── Safety harness: checkpoint cleanup or rollback ── if (s.checkpointSha) { if (unitResult.status === "error" && safetyConfig.auto_rollback) { @@ -1784,11 +1893,19 @@ export async function runFinalize( ); if (verificationResult === "pause") { + recordLearningOutcomeForUnit(ic, iterData.unitType, iterData.unitId, s.currentUnit?.startedAt, { + succeeded: false, + verificationPassed: false, + }); debugLog("autoLoop", { phase: "exit", reason: "verification-pause" }); return { action: "break", reason: "verification-pause" }; } if (verificationResult === "retry") { + recordLearningOutcomeForUnit(ic, iterData.unitType, iterData.unitId, s.currentUnit?.startedAt, { + succeeded: false, + verificationPassed: false, + }); if (sidecarItem) { // Sidecar verification retries are skipped — just continue debugLog("autoLoop", { phase: "sidecar-verification-retry-skipped", iteration: ic.iteration }); @@ -1866,6 +1983,16 @@ export async function runFinalize( // Surface accumulated workflow-logger issues for this unit to the user. // Warnings/errors logged during the unit are buffered in the logger and // drained here so the user sees a single consolidated post-unit alert. + const finalizedArtifactVerified = + shouldSkipArtifactVerification(iterData.unitType) || + verifyExpectedArtifact(iterData.unitType, iterData.unitId, s.basePath); + if (finalizedArtifactVerified) { + recordLearningOutcomeForUnit(ic, iterData.unitType, iterData.unitId, s.currentUnit?.startedAt, { + succeeded: true, + verificationPassed: iterData.unitType === "execute-task" ? true : null, + }); + } + if (hasAnyIssues()) { const { logs } = drainAndSummarize(); if (logs.length > 0) { diff --git a/src/resources/extensions/gsd/auto/run-unit.ts b/src/resources/extensions/gsd/auto/run-unit.ts index 6f1646364..ce0a1348a 100644 --- a/src/resources/extensions/gsd/auto/run-unit.ts +++ b/src/resources/extensions/gsd/auto/run-unit.ts @@ -4,7 +4,7 @@ * Imports from: auto/types, auto/resolve */ -import type { ExtensionAPI, ExtensionContext } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI, ExtensionContext } from "@sf-run/pi-coding-agent"; import type { AutoSession } from "./session.js"; import { NEW_SESSION_TIMEOUT_MS } from "./session.js"; @@ -12,7 +12,7 @@ import type { UnitResult } from "./types.js"; import { _setCurrentResolve, _setSessionSwitchInFlight } from "./resolve.js"; import { debugLog } from "../debug-logger.js"; import { logWarning, logError } from "../workflow-logger.js"; -import { resolveAutoSupervisorConfig } from "../preferences.js"; +import { resolveAutoSupervisorConfig, resolvePersistModelChanges } from "../preferences.js"; // Tracks the latest session-switch attempt so a late timeout settlement from an // older runUnit() call cannot clear the guard for a newer one. @@ -80,7 +80,7 @@ export async function runUnit( } if (s.currentUnitModel && typeof pi.setModel === "function") { - const restored = await pi.setModel(s.currentUnitModel, { persist: false }); + const restored = await pi.setModel(s.currentUnitModel, { persist: resolvePersistModelChanges() }); if (!restored) { ctx.ui.notify( `Failed to restore ${s.currentUnitModel.provider}/${s.currentUnitModel.id} after session creation. Using session default.`, diff --git a/src/resources/extensions/gsd/auto/session.ts b/src/resources/extensions/gsd/auto/session.ts index fd2d6a9d1..049a72819 100644 --- a/src/resources/extensions/gsd/auto/session.ts +++ b/src/resources/extensions/gsd/auto/session.ts @@ -16,8 +16,8 @@ * `let` or `var` declarations. */ -import type { Api, Model } from "@gsd/pi-ai"; -import type { ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import type { Api, Model } from "@sf-run/pi-ai"; +import type { ExtensionCommandContext } from "@sf-run/pi-coding-agent"; import type { GitServiceImpl } from "../git-service.js"; import type { CaptureEntry } from "../captures.js"; import type { BudgetAlertLevel } from "../auto-budget.js"; diff --git a/src/resources/extensions/gsd/auto/types.ts b/src/resources/extensions/gsd/auto/types.ts index 9c2d1d466..a2ca21d2b 100644 --- a/src/resources/extensions/gsd/auto/types.ts +++ b/src/resources/extensions/gsd/auto/types.ts @@ -4,7 +4,7 @@ * Leaf node in the import DAG — no imports from auto/. */ -import type { ExtensionAPI, ExtensionContext } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI, ExtensionContext } from "@sf-run/pi-coding-agent"; import type { AutoSession } from "./session.js"; import type { GSDPreferences } from "../preferences.js"; diff --git a/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts b/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts index 553df4e65..6478e17d9 100644 --- a/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +++ b/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts @@ -1,9 +1,9 @@ -import type { ExtensionAPI, ExtensionContext } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI, ExtensionContext } from "@sf-run/pi-coding-agent"; import { logWarning } from "../workflow-logger.js"; import { checkAutoStartAfterDiscuss } from "../guided-flow.js"; import { getAutoDashboardData, getAutoModeStartModel, isAutoActive, pauseAuto } from "../auto.js"; -import { getNextFallbackModel, resolveModelWithFallbacksForUnit } from "../preferences.js"; +import { getNextFallbackModel, resolveModelWithFallbacksForUnit, resolvePersistModelChanges } from "../preferences.js"; import { pauseAutoForProviderError } from "../provider-error-pause.js"; import { isSessionSwitchInFlight, resolveAgentEnd } from "../auto-loop.js"; import { resolveModelId } from "../auto-model-selection.js"; @@ -70,6 +70,7 @@ export async function handleAgentEnd( event: { messages: any[] }, ctx: ExtensionContext, ): Promise { + const persistModelChanges = resolvePersistModelChanges(); if (checkAutoStartAfterDiscuss()) { clearDiscussionFlowState(); return; @@ -200,7 +201,7 @@ export async function handleAgentEnd( retryState.currentRetryModelId = undefined; const modelToSet = resolveModelId(nextModelId, availableModels, ctx.model?.provider); if (modelToSet) { - const ok = await pi.setModel(modelToSet, { persist: false }); + const ok = await pi.setModel(modelToSet, { persist: persistModelChanges }); if (ok) { ctx.ui.notify(`Model error${errorDetail}. Switched to fallback: ${nextModelId} and resuming.`, "warning"); pi.sendMessage({ customType: "gsd-auto-timeout-recovery", content: "Continue execution.", display: false }, { triggerTurn: true }); @@ -217,7 +218,7 @@ export async function handleAgentEnd( if (ctx.model?.id !== sessionModel.id || ctx.model?.provider !== sessionModel.provider) { const startModel = ctx.modelRegistry.getAvailable().find((m) => m.provider === sessionModel.provider && m.id === sessionModel.id); if (startModel) { - const ok = await pi.setModel(startModel, { persist: false }); + const ok = await pi.setModel(startModel, { persist: persistModelChanges }); if (ok) { retryState.networkRetryCount = 0; retryState.currentRetryModelId = undefined; diff --git a/src/resources/extensions/gsd/bootstrap/db-tools.ts b/src/resources/extensions/gsd/bootstrap/db-tools.ts index dbb5849c9..cc88cec57 100644 --- a/src/resources/extensions/gsd/bootstrap/db-tools.ts +++ b/src/resources/extensions/gsd/bootstrap/db-tools.ts @@ -1,11 +1,11 @@ import { Type } from "@sinclair/typebox"; -import type { ExtensionAPI } from "@gsd/pi-coding-agent"; -import { Text } from "@gsd/pi-tui"; +import type { ExtensionAPI } from "@sf-run/pi-coding-agent"; +import { Text } from "@sf-run/pi-tui"; import { findMilestoneIds, nextMilestoneId, claimReservedId, getReservedMilestoneIds } from "../guided-flow.js"; import { loadEffectiveGSDPreferences } from "../preferences.js"; import { ensureDbOpen } from "./dynamic-tools.js"; -import { StringEnum } from "@gsd/pi-ai"; +import { StringEnum } from "@sf-run/pi-ai"; import { logError } from "../workflow-logger.js"; import { getErrorMessage } from "../error-utils.js"; import { diff --git a/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts b/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts index b4371f483..ec57774dc 100644 --- a/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +++ b/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts @@ -1,8 +1,8 @@ import { existsSync } from "node:fs"; import { join, sep } from "node:path"; -import type { ExtensionAPI } from "@gsd/pi-coding-agent"; -import { createBashTool, createEditTool, createReadTool, createWriteTool } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI } from "@sf-run/pi-coding-agent"; +import { createBashTool, createEditTool, createReadTool, createWriteTool } from "@sf-run/pi-coding-agent"; import { DEFAULT_BASH_TIMEOUT_SECS } from "../constants.js"; import { setLogBasePath, logWarning } from "../workflow-logger.js"; diff --git a/src/resources/extensions/gsd/bootstrap/journal-tools.ts b/src/resources/extensions/gsd/bootstrap/journal-tools.ts index 9a1aa9dec..b67a0b42d 100644 --- a/src/resources/extensions/gsd/bootstrap/journal-tools.ts +++ b/src/resources/extensions/gsd/bootstrap/journal-tools.ts @@ -1,5 +1,5 @@ import { Type } from "@sinclair/typebox"; -import type { ExtensionAPI } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI } from "@sf-run/pi-coding-agent"; import { queryJournal } from "../journal.js"; import { logWarning } from "../workflow-logger.js"; diff --git a/src/resources/extensions/gsd/bootstrap/notify-interceptor.ts b/src/resources/extensions/gsd/bootstrap/notify-interceptor.ts index 2ac10cef3..d93b999c5 100644 --- a/src/resources/extensions/gsd/bootstrap/notify-interceptor.ts +++ b/src/resources/extensions/gsd/bootstrap/notify-interceptor.ts @@ -3,7 +3,7 @@ // notification store. Uses a WeakSet to prevent double-wrapping and handle // UI context replacement on /reload gracefully. -import type { ExtensionContext } from "@gsd/pi-coding-agent"; +import type { ExtensionContext } from "@sf-run/pi-coding-agent"; import { appendNotification, type NotifySeverity } from "../notification-store.js"; diff --git a/src/resources/extensions/gsd/bootstrap/provider-error-resume.ts b/src/resources/extensions/gsd/bootstrap/provider-error-resume.ts index d5f01f96d..213554d5a 100644 --- a/src/resources/extensions/gsd/bootstrap/provider-error-resume.ts +++ b/src/resources/extensions/gsd/bootstrap/provider-error-resume.ts @@ -2,7 +2,7 @@ import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext, -} from "@gsd/pi-coding-agent"; +} from "@sf-run/pi-coding-agent"; import { getAutoDashboardData, startAuto, type AutoDashboardData } from "../auto.js"; import { resetTransientRetryState } from "./agent-end-recovery.js"; diff --git a/src/resources/extensions/gsd/bootstrap/query-tools.ts b/src/resources/extensions/gsd/bootstrap/query-tools.ts index 2741af75f..991d541f0 100644 --- a/src/resources/extensions/gsd/bootstrap/query-tools.ts +++ b/src/resources/extensions/gsd/bootstrap/query-tools.ts @@ -1,7 +1,7 @@ // GSD2 — Read-only query tools exposing DB state to the LLM via the WAL connection import { Type } from "@sinclair/typebox"; -import type { ExtensionAPI } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI } from "@sf-run/pi-coding-agent"; import { ensureDbOpen } from "./dynamic-tools.js"; import { executeMilestoneStatus } from "../tools/workflow-tool-executors.js"; diff --git a/src/resources/extensions/gsd/bootstrap/register-extension.ts b/src/resources/extensions/gsd/bootstrap/register-extension.ts index bdcc436ab..fad1dfc15 100644 --- a/src/resources/extensions/gsd/bootstrap/register-extension.ts +++ b/src/resources/extensions/gsd/bootstrap/register-extension.ts @@ -1,6 +1,6 @@ // GSD2 — Extension registration: wires all GSD tools, commands, and hooks into pi -import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI, ExtensionCommandContext } from "@sf-run/pi-coding-agent"; import { registerExitCommand } from "../exit-command.js"; import { registerWorktreeCommand } from "../worktree-command.js"; diff --git a/src/resources/extensions/gsd/bootstrap/register-hooks.ts b/src/resources/extensions/gsd/bootstrap/register-hooks.ts index ff6aefa83..2284bc2d9 100644 --- a/src/resources/extensions/gsd/bootstrap/register-hooks.ts +++ b/src/resources/extensions/gsd/bootstrap/register-hooks.ts @@ -1,7 +1,7 @@ import { join } from "node:path"; -import type { ExtensionAPI, ExtensionContext } from "@gsd/pi-coding-agent"; -import { isToolCallEventType } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI, ExtensionContext } from "@sf-run/pi-coding-agent"; +import { isToolCallEventType } from "@sf-run/pi-coding-agent"; import { buildMilestoneFileName, resolveMilestonePath, resolveSliceFile, resolveSlicePath } from "../paths.js"; import { buildBeforeAgentStartResult } from "./system-context.js"; @@ -19,12 +19,14 @@ import { checkToolCallLoop, resetToolCallLoopGuard } from "./tool-call-loop-guar import { saveActivityLog } from "../activity-log.js"; import { resetAskUserQuestionsCache } from "../../ask-user-questions.js"; import { recordToolCall as safetyRecordToolCall, recordToolResult as safetyRecordToolResult } from "../safety/evidence-collector.js"; +import { recordToolCallName } from "../auto-tool-tracking.js"; import { classifyCommand } from "../safety/destructive-guard.js"; import { logWarning as safetyLogWarning } from "../workflow-logger.js"; import { installNotifyInterceptor } from "./notify-interceptor.js"; import { initNotificationStore } from "../notification-store.js"; import { initNotificationWidget } from "../notification-widget.js"; import { initHealthWidget } from "../health-widget.js"; +import { initializeLearningRuntime, resetLearningRuntime, selectLearnedModel } from "../learning/runtime.js"; // Skip the welcome screen on the very first session_start — cli.ts already // printed it before the TUI launched. Only re-print on /clear (subsequent sessions). @@ -37,6 +39,16 @@ async function syncServiceTierStatus(ctx: ExtensionContext): Promise { export function registerHooks(pi: ExtensionAPI): void { pi.on("session_start", async (_event, ctx) => { + resetLearningRuntime(); + try { + const sid = ctx.sessionManager?.getSessionId?.() ?? ""; + const sfile = ctx.sessionManager?.getSessionFile?.() ?? ""; + if (sid) { + process.stderr.write(`[gsd] session ${sid.slice(0, 8)} · ${sfile}\n`); + } + } catch { + /* non-fatal */ + } initNotificationStore(process.cwd()); installNotifyInterceptor(ctx); initNotificationWidget(ctx); @@ -47,6 +59,7 @@ export function registerHooks(pi: ExtensionAPI): void { await syncServiceTierStatus(ctx); const { prepareWorkflowMcpForProject } = await import("../workflow-mcp-auto-prep.js"); prepareWorkflowMcpForProject(ctx, process.cwd()); + await initializeLearningRuntime(); // Apply show_token_cost preference (#1515) try { @@ -80,6 +93,7 @@ export function registerHooks(pi: ExtensionAPI): void { }); pi.on("session_switch", async (_event, ctx) => { + resetLearningRuntime(); initNotificationStore(process.cwd()); installNotifyInterceptor(ctx); resetWriteGateState(); @@ -89,6 +103,7 @@ export function registerHooks(pi: ExtensionAPI): void { await syncServiceTierStatus(ctx); const { prepareWorkflowMcpForProject } = await import("../workflow-mcp-auto-prep.js"); prepareWorkflowMcpForProject(ctx, process.cwd()); + await initializeLearningRuntime(); loadToolApiKeys(); }); @@ -155,6 +170,7 @@ export function registerHooks(pi: ExtensionAPI): void { }); pi.on("session_shutdown", async (_event, ctx: ExtensionContext) => { + resetLearningRuntime(); if (isParallelActive()) { try { await shutdownParallel(process.cwd()); @@ -360,6 +376,7 @@ export function registerHooks(pi: ExtensionAPI): void { pi.on("tool_execution_start", async (event) => { if (!isAutoActive()) return; markToolStart(event.toolCallId); + recordToolCallName(event.toolName); }); pi.on("tool_execution_end", async (event) => { @@ -446,9 +463,12 @@ export function registerHooks(pi: ExtensionAPI): void { // Capability-aware model routing hook (ADR-004) // Extensions can override model selection by returning { modelId: "..." } // Return undefined to let the built-in capability scoring proceed. - pi.on("before_model_select", async (_event) => { - // Default: no override — let capability scoring handle selection - return undefined; + pi.on("before_model_select", async (event) => { + return selectLearnedModel({ + unitType: event.unitType, + eligibleModels: event.eligibleModels, + phaseConfig: event.phaseConfig, + }); }); // Tool set adaptation hook (ADR-005 Phase 4) diff --git a/src/resources/extensions/gsd/bootstrap/register-shortcuts.ts b/src/resources/extensions/gsd/bootstrap/register-shortcuts.ts index eb8dc79b8..9d58a17f9 100644 --- a/src/resources/extensions/gsd/bootstrap/register-shortcuts.ts +++ b/src/resources/extensions/gsd/bootstrap/register-shortcuts.ts @@ -1,8 +1,8 @@ import { existsSync } from "node:fs"; import { join } from "node:path"; -import type { ExtensionAPI, ExtensionContext } from "@gsd/pi-coding-agent"; -import { Key } from "@gsd/pi-tui"; +import type { ExtensionAPI, ExtensionContext } from "@sf-run/pi-coding-agent"; +import { Key } from "@sf-run/pi-tui"; import { GSDDashboardOverlay } from "../dashboard-overlay.js"; import { GSDNotificationOverlay } from "../notification-overlay.js"; diff --git a/src/resources/extensions/gsd/bootstrap/sanitize-complete-milestone.ts b/src/resources/extensions/gsd/bootstrap/sanitize-complete-milestone.ts index 3c770095d..d2fc56f43 100644 --- a/src/resources/extensions/gsd/bootstrap/sanitize-complete-milestone.ts +++ b/src/resources/extensions/gsd/bootstrap/sanitize-complete-milestone.ts @@ -8,7 +8,7 @@ * boolean true, etc. This sanitizer normalizes all fields so * handleCompleteMilestone never crashes on type mismatches. * - * See: https://github.com/gsd-build/gsd-2/issues/3013 + * See: https://github.com/singularity-forge/sf-run/issues/3013 */ import type { CompleteMilestoneParams } from "../tools/complete-milestone.js"; diff --git a/src/resources/extensions/gsd/bootstrap/system-context.ts b/src/resources/extensions/gsd/bootstrap/system-context.ts index bf31468e9..415d0dc84 100644 --- a/src/resources/extensions/gsd/bootstrap/system-context.ts +++ b/src/resources/extensions/gsd/bootstrap/system-context.ts @@ -2,7 +2,7 @@ import { existsSync, readFileSync, unlinkSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; -import type { ExtensionContext } from "@gsd/pi-coding-agent"; +import type { ExtensionContext } from "@sf-run/pi-coding-agent"; import { logWarning } from "../workflow-logger.js"; import { debugTime } from "../debug-logger.js"; diff --git a/src/resources/extensions/gsd/changelog.ts b/src/resources/extensions/gsd/changelog.ts index b90df2b6b..fea2cb853 100644 --- a/src/resources/extensions/gsd/changelog.ts +++ b/src/resources/extensions/gsd/changelog.ts @@ -1,14 +1,14 @@ /** * GSD Changelog — Fetch and display categorized release notes from GitHub * - * Fetches releases from the gsd-build/gsd-2 GitHub repository, + * Fetches releases from the singularity-forge/sf-run GitHub repository, * prompts the user for a version filter, and sends raw release notes * into the conversation for the LLM to summarize. * * Entry point: handleChangelog() called from commands.ts */ -import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI, ExtensionCommandContext } from "@sf-run/pi-coding-agent"; // ─── Types ──────────────────────────────────────────────────────────────────── @@ -103,7 +103,7 @@ function formatRelease(release: GitHubRelease): string { // ─── Entry Point ────────────────────────────────────────────────────────────── -const RELEASES_URL = "https://api.github.com/repos/gsd-build/gsd-2/releases?per_page=100"; +const RELEASES_URL = "https://api.github.com/repos/singularity-forge/sf-run/releases?per_page=100"; export async function handleChangelog( args: string, diff --git a/src/resources/extensions/gsd/claude-import.ts b/src/resources/extensions/gsd/claude-import.ts index ca34d27ed..3fd8dcc37 100644 --- a/src/resources/extensions/gsd/claude-import.ts +++ b/src/resources/extensions/gsd/claude-import.ts @@ -1,5 +1,5 @@ -import type { ExtensionCommandContext } from "@gsd/pi-coding-agent"; -import { SettingsManager, getAgentDir } from "@gsd/pi-coding-agent"; +import type { ExtensionCommandContext } from "@sf-run/pi-coding-agent"; +import { SettingsManager, getAgentDir } from "@sf-run/pi-coding-agent"; import { existsSync, readdirSync, readFileSync } from "node:fs"; import { basename, dirname, join, relative, resolve } from "node:path"; import { homedir } from "node:os"; diff --git a/src/resources/extensions/gsd/commands-add-tests.ts b/src/resources/extensions/gsd/commands-add-tests.ts index afc3c8a6b..117b33289 100644 --- a/src/resources/extensions/gsd/commands-add-tests.ts +++ b/src/resources/extensions/gsd/commands-add-tests.ts @@ -5,7 +5,7 @@ * with implementation context (summaries, changed files, test patterns). */ -import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI, ExtensionCommandContext } from "@sf-run/pi-coding-agent"; import { existsSync, readFileSync, readdirSync } from "node:fs"; import { join } from "node:path"; diff --git a/src/resources/extensions/gsd/commands-backlog.ts b/src/resources/extensions/gsd/commands-backlog.ts index 4241724eb..2f94c93c8 100644 --- a/src/resources/extensions/gsd/commands-backlog.ts +++ b/src/resources/extensions/gsd/commands-backlog.ts @@ -6,7 +6,7 @@ * Items can be promoted to active slices via add-slice. */ -import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI, ExtensionCommandContext } from "@sf-run/pi-coding-agent"; import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs"; import { join, dirname } from "node:path"; diff --git a/src/resources/extensions/gsd/commands-bootstrap.ts b/src/resources/extensions/gsd/commands-bootstrap.ts index 0f5c55cd1..36b85c4cd 100644 --- a/src/resources/extensions/gsd/commands-bootstrap.ts +++ b/src/resources/extensions/gsd/commands-bootstrap.ts @@ -1,4 +1,4 @@ -import { importExtensionModule, type ExtensionAPI, type ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import { importExtensionModule, type ExtensionAPI, type ExtensionCommandContext } from "@sf-run/pi-coding-agent"; const TOP_LEVEL_SUBCOMMANDS = [ { cmd: "help", desc: "Categorized command reference with descriptions" }, diff --git a/src/resources/extensions/gsd/commands-cmux.ts b/src/resources/extensions/gsd/commands-cmux.ts index a1b8f5ee4..0a81dada5 100644 --- a/src/resources/extensions/gsd/commands-cmux.ts +++ b/src/resources/extensions/gsd/commands-cmux.ts @@ -1,4 +1,4 @@ -import type { ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import type { ExtensionCommandContext } from "@sf-run/pi-coding-agent"; import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { clearCmuxSidebar, CmuxClient, detectCmuxEnvironment, resolveCmuxConfig } from "../cmux/index.js"; import { saveFile } from "./files.js"; diff --git a/src/resources/extensions/gsd/commands-codebase.ts b/src/resources/extensions/gsd/commands-codebase.ts index 20967e03f..d0254c754 100644 --- a/src/resources/extensions/gsd/commands-codebase.ts +++ b/src/resources/extensions/gsd/commands-codebase.ts @@ -5,7 +5,7 @@ * Subcommands: generate, update, stats, help */ -import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI, ExtensionCommandContext } from "@sf-run/pi-coding-agent"; import { generateCodebaseMap, diff --git a/src/resources/extensions/gsd/commands-config.ts b/src/resources/extensions/gsd/commands-config.ts index 01cf58c14..d1bf5439f 100644 --- a/src/resources/extensions/gsd/commands-config.ts +++ b/src/resources/extensions/gsd/commands-config.ts @@ -4,8 +4,8 @@ * Contains: TOOL_KEYS, loadToolApiKeys, getConfigAuthStorage, handleConfig */ -import type { ExtensionCommandContext } from "@gsd/pi-coding-agent"; -import { AuthStorage } from "@gsd/pi-coding-agent"; +import type { ExtensionCommandContext } from "@sf-run/pi-coding-agent"; +import { AuthStorage } from "@sf-run/pi-coding-agent"; import { existsSync, mkdirSync } from "node:fs"; import { join, dirname } from "node:path"; diff --git a/src/resources/extensions/gsd/commands-do.ts b/src/resources/extensions/gsd/commands-do.ts index af2a20f38..cee1d61ab 100644 --- a/src/resources/extensions/gsd/commands-do.ts +++ b/src/resources/extensions/gsd/commands-do.ts @@ -5,7 +5,7 @@ * using keyword matching. Falls back to /gsd quick for task-like input. */ -import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI, ExtensionCommandContext } from "@sf-run/pi-coding-agent"; interface Route { keywords: string[]; diff --git a/src/resources/extensions/gsd/commands-extensions.ts b/src/resources/extensions/gsd/commands-extensions.ts index 05b867e4f..fadfda0ac 100644 --- a/src/resources/extensions/gsd/commands-extensions.ts +++ b/src/resources/extensions/gsd/commands-extensions.ts @@ -6,7 +6,7 @@ * via jiti at runtime from ~/.gsd/agent/, not compiled by tsc). */ -import type { ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import type { ExtensionCommandContext } from "@sf-run/pi-coding-agent"; import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { homedir } from "node:os"; diff --git a/src/resources/extensions/gsd/commands-extract-learnings.ts b/src/resources/extensions/gsd/commands-extract-learnings.ts index de23d5422..71b3b6810 100644 --- a/src/resources/extensions/gsd/commands-extract-learnings.ts +++ b/src/resources/extensions/gsd/commands-extract-learnings.ts @@ -6,7 +6,7 @@ * Decisions · Lessons · Patterns · Surprises */ -import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI, ExtensionCommandContext } from "@sf-run/pi-coding-agent"; import { existsSync, readFileSync } from "node:fs"; import { join, basename } from "node:path"; diff --git a/src/resources/extensions/gsd/commands-handlers.ts b/src/resources/extensions/gsd/commands-handlers.ts index 0272ef289..aec8288ce 100644 --- a/src/resources/extensions/gsd/commands-handlers.ts +++ b/src/resources/extensions/gsd/commands-handlers.ts @@ -5,7 +5,7 @@ * handleRunHook, handleUpdate, handleSkillHealth */ -import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI, ExtensionCommandContext } from "@sf-run/pi-coding-agent"; import { existsSync, readFileSync, mkdirSync } from "node:fs"; import { join } from "node:path"; import { deriveState } from "./state.js"; diff --git a/src/resources/extensions/gsd/commands-inspect.ts b/src/resources/extensions/gsd/commands-inspect.ts index 5421c00bf..9b8b704c1 100644 --- a/src/resources/extensions/gsd/commands-inspect.ts +++ b/src/resources/extensions/gsd/commands-inspect.ts @@ -4,7 +4,7 @@ * Contains: InspectData type, formatInspectOutput, handleInspect */ -import type { ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import type { ExtensionCommandContext } from "@sf-run/pi-coding-agent"; import { existsSync } from "node:fs"; import { join } from "node:path"; import { gsdRoot } from "./paths.js"; diff --git a/src/resources/extensions/gsd/commands-logs.ts b/src/resources/extensions/gsd/commands-logs.ts index 379c7aef2..0863180df 100644 --- a/src/resources/extensions/gsd/commands-logs.ts +++ b/src/resources/extensions/gsd/commands-logs.ts @@ -10,7 +10,7 @@ * /gsd logs clear — Remove old activity and debug logs */ -import type { ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import type { ExtensionCommandContext } from "@sf-run/pi-coding-agent"; import { existsSync, readdirSync, readFileSync, statSync, unlinkSync } from "node:fs"; import { join } from "node:path"; import { gsdRoot } from "./paths.js"; diff --git a/src/resources/extensions/gsd/commands-maintenance.ts b/src/resources/extensions/gsd/commands-maintenance.ts index 85bf35621..d529e5787 100644 --- a/src/resources/extensions/gsd/commands-maintenance.ts +++ b/src/resources/extensions/gsd/commands-maintenance.ts @@ -4,7 +4,7 @@ * Contains: handleCleanupBranches, handleCleanupSnapshots, handleCleanupWorktrees, handleSkip, handleDryRun, handleRecover */ -import type { ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import type { ExtensionCommandContext } from "@sf-run/pi-coding-agent"; import { deriveState } from "./state.js"; import { nativeBranchList, nativeDetectMainBranch, nativeBranchListMerged, nativeBranchDelete, nativeForEachRef, nativeUpdateRef } from "./native-git-bridge.js"; import { logWarning } from "./workflow-logger.js"; diff --git a/src/resources/extensions/gsd/commands-mcp-status.ts b/src/resources/extensions/gsd/commands-mcp-status.ts index c574f6daf..beb7b799e 100644 --- a/src/resources/extensions/gsd/commands-mcp-status.ts +++ b/src/resources/extensions/gsd/commands-mcp-status.ts @@ -10,7 +10,7 @@ * /gsd mcp init [dir] — Write project-local GSD workflow MCP config */ -import type { ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import type { ExtensionCommandContext } from "@sf-run/pi-coding-agent"; import { existsSync, readFileSync } from "node:fs"; import { join, resolve } from "node:path"; diff --git a/src/resources/extensions/gsd/commands-pr-branch.ts b/src/resources/extensions/gsd/commands-pr-branch.ts index 94c96cc79..df28ea894 100644 --- a/src/resources/extensions/gsd/commands-pr-branch.ts +++ b/src/resources/extensions/gsd/commands-pr-branch.ts @@ -6,7 +6,7 @@ * upstream PRs where planning artifacts should not be included. */ -import type { ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import type { ExtensionCommandContext } from "@sf-run/pi-coding-agent"; import { execFileSync } from "node:child_process"; diff --git a/src/resources/extensions/gsd/commands-prefs-wizard.ts b/src/resources/extensions/gsd/commands-prefs-wizard.ts index 6e2eaf60c..ea19767fc 100644 --- a/src/resources/extensions/gsd/commands-prefs-wizard.ts +++ b/src/resources/extensions/gsd/commands-prefs-wizard.ts @@ -6,7 +6,7 @@ * handlePrefsMode, handleImportClaude, handlePrefs */ -import type { ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import type { ExtensionCommandContext } from "@sf-run/pi-coding-agent"; import { existsSync, readFileSync } from "node:fs"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; diff --git a/src/resources/extensions/gsd/commands-rate.ts b/src/resources/extensions/gsd/commands-rate.ts index 39eedace1..daabe5e2f 100644 --- a/src/resources/extensions/gsd/commands-rate.ts +++ b/src/resources/extensions/gsd/commands-rate.ts @@ -3,7 +3,7 @@ * Feeds into the adaptive routing history so future dispatches improve. */ -import type { ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import type { ExtensionCommandContext } from "@sf-run/pi-coding-agent"; import { loadLedgerFromDisk } from "./metrics.js"; import { recordFeedback, initRoutingHistory } from "./routing-history.js"; import type { ComplexityTier } from "./complexity-classifier.js"; diff --git a/src/resources/extensions/gsd/commands-session-report.ts b/src/resources/extensions/gsd/commands-session-report.ts index 211315057..8e6aa66ab 100644 --- a/src/resources/extensions/gsd/commands-session-report.ts +++ b/src/resources/extensions/gsd/commands-session-report.ts @@ -5,7 +5,7 @@ * duration, model usage breakdown. */ -import type { ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import type { ExtensionCommandContext } from "@sf-run/pi-coding-agent"; import { mkdirSync, writeFileSync } from "node:fs"; import { join } from "node:path"; diff --git a/src/resources/extensions/gsd/commands-ship.ts b/src/resources/extensions/gsd/commands-ship.ts index 0a5f6e0d7..bba26cbf9 100644 --- a/src/resources/extensions/gsd/commands-ship.ts +++ b/src/resources/extensions/gsd/commands-ship.ts @@ -5,7 +5,7 @@ * roadmap, slice summaries, and metrics, then opens via `gh pr create`. */ -import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI, ExtensionCommandContext } from "@sf-run/pi-coding-agent"; import { execFileSync } from "node:child_process"; import { existsSync, readFileSync, readdirSync } from "node:fs"; diff --git a/src/resources/extensions/gsd/commands-workflow-templates.ts b/src/resources/extensions/gsd/commands-workflow-templates.ts index e86226873..7b72a8c3c 100644 --- a/src/resources/extensions/gsd/commands-workflow-templates.ts +++ b/src/resources/extensions/gsd/commands-workflow-templates.ts @@ -5,7 +5,7 @@ * Resolves templates by name or auto-detection, then dispatches the workflow prompt. */ -import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI, ExtensionCommandContext } from "@sf-run/pi-coding-agent"; import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { diff --git a/src/resources/extensions/gsd/commands/context.ts b/src/resources/extensions/gsd/commands/context.ts index 8007ecd27..5fdb6641b 100644 --- a/src/resources/extensions/gsd/commands/context.ts +++ b/src/resources/extensions/gsd/commands/context.ts @@ -1,4 +1,4 @@ -import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI, ExtensionCommandContext } from "@sf-run/pi-coding-agent"; import { checkRemoteAutoSession, isAutoActive, isAutoPaused, stopAutoRemote } from "../auto.js"; import { validateDirectory } from "../validate-directory.js"; diff --git a/src/resources/extensions/gsd/commands/dispatcher.ts b/src/resources/extensions/gsd/commands/dispatcher.ts index 9ec6bae09..389380a5e 100644 --- a/src/resources/extensions/gsd/commands/dispatcher.ts +++ b/src/resources/extensions/gsd/commands/dispatcher.ts @@ -1,4 +1,4 @@ -import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI, ExtensionCommandContext } from "@sf-run/pi-coding-agent"; import { GSDNoProjectError } from "./context.js"; import { handleAutoCommand } from "./handlers/auto.js"; diff --git a/src/resources/extensions/gsd/commands/handlers/auto.ts b/src/resources/extensions/gsd/commands/handlers/auto.ts index 283ff77ed..88c1cc7a7 100644 --- a/src/resources/extensions/gsd/commands/handlers/auto.ts +++ b/src/resources/extensions/gsd/commands/handlers/auto.ts @@ -1,4 +1,4 @@ -import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI, ExtensionCommandContext } from "@sf-run/pi-coding-agent"; import { existsSync, readFileSync } from "node:fs"; import { resolve } from "node:path"; diff --git a/src/resources/extensions/gsd/commands/handlers/core.ts b/src/resources/extensions/gsd/commands/handlers/core.ts index 51aaec2bc..f905d3ad5 100644 --- a/src/resources/extensions/gsd/commands/handlers/core.ts +++ b/src/resources/extensions/gsd/commands/handlers/core.ts @@ -1,5 +1,5 @@ -import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@gsd/pi-coding-agent"; -import type { Model } from "@gsd/pi-ai"; +import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@sf-run/pi-coding-agent"; +import type { Model } from "@sf-run/pi-ai"; import type { GSDState } from "../../types.js"; import { computeProgressScore, formatProgressLine } from "../../progress-score.js"; diff --git a/src/resources/extensions/gsd/commands/handlers/notifications-handler.ts b/src/resources/extensions/gsd/commands/handlers/notifications-handler.ts index a7440f763..3434634f7 100644 --- a/src/resources/extensions/gsd/commands/handlers/notifications-handler.ts +++ b/src/resources/extensions/gsd/commands/handlers/notifications-handler.ts @@ -1,7 +1,7 @@ // GSD Extension — /gsd notifications Command Handler // View, filter, and clear the persistent notification history. -import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI, ExtensionCommandContext } from "@sf-run/pi-coding-agent"; import { readNotifications, diff --git a/src/resources/extensions/gsd/commands/handlers/ops.ts b/src/resources/extensions/gsd/commands/handlers/ops.ts index 3a8a2fe19..1880c55ae 100644 --- a/src/resources/extensions/gsd/commands/handlers/ops.ts +++ b/src/resources/extensions/gsd/commands/handlers/ops.ts @@ -1,4 +1,4 @@ -import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI, ExtensionCommandContext } from "@sf-run/pi-coding-agent"; import { enableDebug } from "../../debug-logger.js"; import { dispatchDirectPhase } from "../../auto-direct-dispatch.js"; diff --git a/src/resources/extensions/gsd/commands/handlers/parallel.ts b/src/resources/extensions/gsd/commands/handlers/parallel.ts index bc8eea7da..0fdd4ad45 100644 --- a/src/resources/extensions/gsd/commands/handlers/parallel.ts +++ b/src/resources/extensions/gsd/commands/handlers/parallel.ts @@ -1,4 +1,4 @@ -import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI, ExtensionCommandContext } from "@sf-run/pi-coding-agent"; import { getOrchestratorState, diff --git a/src/resources/extensions/gsd/commands/handlers/workflow.ts b/src/resources/extensions/gsd/commands/handlers/workflow.ts index b5aa3fc1c..595dcf8a6 100644 --- a/src/resources/extensions/gsd/commands/handlers/workflow.ts +++ b/src/resources/extensions/gsd/commands/handlers/workflow.ts @@ -1,4 +1,4 @@ -import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI, ExtensionCommandContext } from "@sf-run/pi-coding-agent"; import { existsSync, readFileSync, unlinkSync } from "node:fs"; import { join } from "node:path"; diff --git a/src/resources/extensions/gsd/commands/index.ts b/src/resources/extensions/gsd/commands/index.ts index c07476532..816cf4654 100644 --- a/src/resources/extensions/gsd/commands/index.ts +++ b/src/resources/extensions/gsd/commands/index.ts @@ -1,4 +1,4 @@ -import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI, ExtensionCommandContext } from "@sf-run/pi-coding-agent"; import { GSD_COMMAND_DESCRIPTION, getGsdArgumentCompletions } from "./catalog.js"; diff --git a/src/resources/extensions/gsd/config-overlay.ts b/src/resources/extensions/gsd/config-overlay.ts index 1b9cf2852..2faad694f 100644 --- a/src/resources/extensions/gsd/config-overlay.ts +++ b/src/resources/extensions/gsd/config-overlay.ts @@ -7,8 +7,8 @@ * Opened via `/gsd show-config` or `/gsd config`. */ -import type { Theme } from "@gsd/pi-coding-agent"; -import { matchesKey, Key, truncateToWidth } from "@gsd/pi-tui"; +import type { Theme } from "@sf-run/pi-coding-agent"; +import { matchesKey, Key, truncateToWidth } from "@sf-run/pi-tui"; import { loadEffectiveGSDPreferences, diff --git a/src/resources/extensions/gsd/dashboard-overlay.ts b/src/resources/extensions/gsd/dashboard-overlay.ts index bafcb23ac..4f6cad5a1 100644 --- a/src/resources/extensions/gsd/dashboard-overlay.ts +++ b/src/resources/extensions/gsd/dashboard-overlay.ts @@ -7,8 +7,8 @@ * or opened from /gsd status. */ -import type { Theme } from "@gsd/pi-coding-agent"; -import { truncateToWidth, visibleWidth, matchesKey, Key } from "@gsd/pi-tui"; +import type { Theme } from "@sf-run/pi-coding-agent"; +import { truncateToWidth, visibleWidth, matchesKey, Key } from "@sf-run/pi-tui"; import { deriveState } from "./state.js"; import { loadFile } from "./files.js"; import { isDbAvailable, getMilestoneSlices, getSliceTasks } from "./gsd-db.js"; diff --git a/src/resources/extensions/gsd/docs/preferences-reference.md b/src/resources/extensions/gsd/docs/preferences-reference.md index 23830100f..da2897669 100644 --- a/src/resources/extensions/gsd/docs/preferences-reference.md +++ b/src/resources/extensions/gsd/docs/preferences-reference.md @@ -111,6 +111,8 @@ Setting `prefer_skills: []` does **not** disable skill discovery — it just mea - `discuss` — used for milestone/slice discussion (interactive context gathering). Falls back to `planning` if unset. - `validation` — used for gate evaluation, roadmap reassessment, milestone validation, and doc rewrites. Falls back to `planning` if unset. +- `persist_model_changes`: boolean — controls whether `setModel()` updates also persist to the default provider/model settings. Default: `true`. Set `false` to keep auto-mode and recovery model switches session-local. + - `skill_staleness_days`: number — skills unused for this many days get deprioritized during discovery. Set to `0` to disable staleness tracking. Default: `60`. - `skill_discovery`: controls how GSD discovers and applies skills during auto-mode. Valid values: @@ -214,7 +216,7 @@ Setting `prefer_skills: []` does **not** disable skill discovery — it just mea - `auto_report`: boolean — generate an HTML report snapshot after each milestone completion. Default: `true`. -- `search_provider`: `"brave"`, `"tavily"`, `"ollama"`, `"native"`, or `"auto"` — selects the search backend for research phases. `"native"` forces Anthropic's built-in web search only; provider values force that backend and disable native search; `"auto"` uses the default heuristic. Default: `"auto"`. +- `search_provider`: `"brave"`, `"tavily"`, `"ollama"`, `"combosearch"`, `"native"`, or `"auto"` — selects the search backend for research phases. `"combosearch"` fans out across all configured custom search backends and merges the results. `"native"` forces Anthropic's built-in web search only; provider values force that backend and disable native search; `"auto"` uses the default heuristic. Default: `"auto"`. - `context_selection`: `"full"` or `"smart"` — controls how files are inlined into context. `"full"` inlines entire files; `"smart"` uses semantic chunking to include only the most relevant sections. Default is derived from `token_profile`. diff --git a/src/resources/extensions/gsd/doctor-providers.ts b/src/resources/extensions/gsd/doctor-providers.ts index 06242fc81..2d6e64e5a 100644 --- a/src/resources/extensions/gsd/doctor-providers.ts +++ b/src/resources/extensions/gsd/doctor-providers.ts @@ -13,8 +13,8 @@ import { existsSync } from "node:fs"; import { join } from "node:path"; -import { AuthStorage } from "@gsd/pi-coding-agent"; -import { getEnvApiKey } from "@gsd/pi-ai"; +import { AuthStorage } from "@sf-run/pi-coding-agent"; +import { getEnvApiKey } from "@sf-run/pi-ai"; import { loadEffectiveGSDPreferences } from "./preferences.js"; import { getAuthPath, PROVIDER_REGISTRY, type ProviderCategory } from "./key-manager.js"; diff --git a/src/resources/extensions/gsd/env-utils.ts b/src/resources/extensions/gsd/env-utils.ts index d5400fd7d..7386ec22e 100644 --- a/src/resources/extensions/gsd/env-utils.ts +++ b/src/resources/extensions/gsd/env-utils.ts @@ -2,7 +2,7 @@ // Copyright (c) 2026 Jeremy McSpadden // // Pure utility for checking existing env keys in .env files and process.env. -// Extracted from get-secrets-from-user.ts to avoid pulling in @gsd/pi-tui +// Extracted from get-secrets-from-user.ts to avoid pulling in @sf-run/pi-tui // when only env-checking is needed (e.g. from files.ts during report generation). import { readFile } from "node:fs/promises"; diff --git a/src/resources/extensions/gsd/exit-command.ts b/src/resources/extensions/gsd/exit-command.ts index f4ff48b05..fb41f7a1f 100644 --- a/src/resources/extensions/gsd/exit-command.ts +++ b/src/resources/extensions/gsd/exit-command.ts @@ -1,4 +1,4 @@ -import { importExtensionModule, type ExtensionAPI, type ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import { importExtensionModule, type ExtensionAPI, type ExtensionCommandContext } from "@sf-run/pi-coding-agent"; type StopAutoFn = (ctx: ExtensionCommandContext, pi: ExtensionAPI, reason?: string) => Promise; diff --git a/src/resources/extensions/gsd/export.ts b/src/resources/extensions/gsd/export.ts index bfac9cb25..e47997fe2 100644 --- a/src/resources/extensions/gsd/export.ts +++ b/src/resources/extensions/gsd/export.ts @@ -1,7 +1,7 @@ // GSD Extension — Session/Milestone Export // Generate shareable reports of milestone work in JSON or markdown format. -import type { ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import type { ExtensionCommandContext } from "@sf-run/pi-coding-agent"; import { writeFileSync, mkdirSync } from "node:fs"; import { join, basename } from "node:path"; import { exec, execFile } from "node:child_process"; diff --git a/src/resources/extensions/gsd/forensics.ts b/src/resources/extensions/gsd/forensics.ts index 76be923d8..b7cc6f518 100644 --- a/src/resources/extensions/gsd/forensics.ts +++ b/src/resources/extensions/gsd/forensics.ts @@ -8,7 +8,7 @@ * Entry point: handleForensics() called from commands.ts */ -import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI, ExtensionCommandContext } from "@sf-run/pi-coding-agent"; import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs"; import { join, dirname, relative } from "node:path"; import { fileURLToPath } from "node:url"; @@ -128,17 +128,17 @@ Use keywords from the user's problem description and the anomaly summaries in th 1. **Search closed issues** for similar keywords: \`\`\` - gh issue list --repo gsd-build/gsd-2 --state closed --search "" --limit 20 + gh issue list --repo singularity-forge/sf-run --state closed --search "" --limit 20 \`\`\` 2. **Search open PRs** that might contain the fix: \`\`\` - gh pr list --repo gsd-build/gsd-2 --state open --search "" --limit 10 + gh pr list --repo singularity-forge/sf-run --state open --search "" --limit 10 \`\`\` 3. **Search merged PRs** that may have already fixed this: \`\`\` - gh pr list --repo gsd-build/gsd-2 --state merged --search "" --limit 10 + gh pr list --repo singularity-forge/sf-run --state merged --search "" --limit 10 \`\`\` ### Analysis diff --git a/src/resources/extensions/gsd/graph-context.ts b/src/resources/extensions/gsd/graph-context.ts index 39eb3c4fe..46adac16c 100644 --- a/src/resources/extensions/gsd/graph-context.ts +++ b/src/resources/extensions/gsd/graph-context.ts @@ -128,12 +128,12 @@ async function resolveGraphApi(): Promise { resolvedGraphApi = true; try { - const imported = await import("@gsd-build/mcp-server"); + const imported = await import("@singularity-forge/mcp-server"); if (isGraphApi(imported)) { cachedGraphApi = imported; return cachedGraphApi; } - logWarning("prompt", "@gsd-build/mcp-server graph exports unavailable; using local graph fallback"); + logWarning("prompt", "@singularity-forge/mcp-server graph exports unavailable; using local graph fallback"); } catch { // Fall back to local reader implementation. } @@ -150,7 +150,7 @@ async function resolveGraphApi(): Promise { * the result as an inlined context block. * * Returns null when: - * - @gsd-build/mcp-server fails to import + * - @singularity-forge/mcp-server fails to import * - graph.json does not exist (graphQuery already handles this gracefully) * - query returns zero nodes * diff --git a/src/resources/extensions/gsd/gsd-db.ts b/src/resources/extensions/gsd/gsd-db.ts index 2a20cea38..6f44f9c53 100644 --- a/src/resources/extensions/gsd/gsd-db.ts +++ b/src/resources/extensions/gsd/gsd-db.ts @@ -180,7 +180,7 @@ function openRawDb(path: string): unknown { return new Database(path); } -const SCHEMA_VERSION = 15; +const SCHEMA_VERSION = 16; function indexExists(db: DbAdapter, name: string): boolean { return !!db.prepare( @@ -507,6 +507,24 @@ function initSchema(db: DbAdapter, fileBacked: boolean): void { ) `); + db.exec(` + CREATE TABLE IF NOT EXISTS llm_task_outcomes ( + model_id TEXT NOT NULL, + provider TEXT NOT NULL, + unit_type TEXT NOT NULL, + unit_id TEXT NOT NULL, + succeeded INTEGER NOT NULL DEFAULT 0, + retries INTEGER NOT NULL DEFAULT 0, + escalated INTEGER NOT NULL DEFAULT 0, + verification_passed INTEGER DEFAULT NULL, + blocker_discovered INTEGER NOT NULL DEFAULT 0, + duration_ms INTEGER DEFAULT NULL, + tokens_total INTEGER DEFAULT NULL, + cost_usd REAL DEFAULT NULL, + recorded_at INTEGER NOT NULL + ) + `); + db.exec("CREATE INDEX IF NOT EXISTS idx_memories_active ON memories(superseded_by)"); db.exec("CREATE INDEX IF NOT EXISTS idx_replan_history_milestone ON replan_history(milestone_id, created_at)"); @@ -525,6 +543,10 @@ function initSchema(db: DbAdapter, fileBacked: boolean): void { db.exec("CREATE INDEX IF NOT EXISTS idx_turn_git_tx_turn ON turn_git_transactions(trace_id, turn_id)"); db.exec("CREATE INDEX IF NOT EXISTS idx_audit_events_trace ON audit_events(trace_id, ts)"); db.exec("CREATE INDEX IF NOT EXISTS idx_audit_events_turn ON audit_events(trace_id, turn_id, ts)"); + db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_llm_task_outcomes_identity ON llm_task_outcomes(unit_type, unit_id, recorded_at)"); + db.exec("CREATE INDEX IF NOT EXISTS idx_llm_task_outcomes_model_unit ON llm_task_outcomes(model_id, unit_type, recorded_at DESC)"); + db.exec("CREATE INDEX IF NOT EXISTS idx_llm_task_outcomes_unit ON llm_task_outcomes(unit_type, recorded_at DESC)"); + db.exec("CREATE INDEX IF NOT EXISTS idx_llm_task_outcomes_provider ON llm_task_outcomes(provider, recorded_at DESC)"); db.exec(`CREATE VIEW IF NOT EXISTS active_decisions AS SELECT * FROM decisions WHERE superseded_by IS NULL`); db.exec(`CREATE VIEW IF NOT EXISTS active_requirements AS SELECT * FROM requirements WHERE superseded_by IS NULL`); @@ -951,6 +973,34 @@ function migrateSchema(db: DbAdapter): void { }); } + if (currentVersion < 16) { + db.exec(` + CREATE TABLE IF NOT EXISTS llm_task_outcomes ( + model_id TEXT NOT NULL, + provider TEXT NOT NULL, + unit_type TEXT NOT NULL, + unit_id TEXT NOT NULL, + succeeded INTEGER NOT NULL DEFAULT 0, + retries INTEGER NOT NULL DEFAULT 0, + escalated INTEGER NOT NULL DEFAULT 0, + verification_passed INTEGER DEFAULT NULL, + blocker_discovered INTEGER NOT NULL DEFAULT 0, + duration_ms INTEGER DEFAULT NULL, + tokens_total INTEGER DEFAULT NULL, + cost_usd REAL DEFAULT NULL, + recorded_at INTEGER NOT NULL + ) + `); + db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_llm_task_outcomes_identity ON llm_task_outcomes(unit_type, unit_id, recorded_at)"); + db.exec("CREATE INDEX IF NOT EXISTS idx_llm_task_outcomes_model_unit ON llm_task_outcomes(model_id, unit_type, recorded_at DESC)"); + db.exec("CREATE INDEX IF NOT EXISTS idx_llm_task_outcomes_unit ON llm_task_outcomes(unit_type, recorded_at DESC)"); + db.exec("CREATE INDEX IF NOT EXISTS idx_llm_task_outcomes_provider ON llm_task_outcomes(provider, recorded_at DESC)"); + db.prepare("INSERT INTO schema_version (version, applied_at) VALUES (:version, :applied_at)").run({ + ":version": 16, + ":applied_at": new Date().toISOString(), + }); + } + db.exec("COMMIT"); } catch (err) { db.exec("ROLLBACK"); @@ -983,6 +1033,10 @@ export function wasDbOpenAttempted(): boolean { return _dbOpenAttempted; } +export function getDatabase(): DbAdapter | null { + return currentDb; +} + export function openDatabase(path: string): boolean { _dbOpenAttempted = true; if (currentDb && currentPath !== path) closeDatabase(); @@ -1979,7 +2033,7 @@ export function getMilestone(id: string): MilestoneRow | null { /** * Update a milestone's status in the database. * Used by park/unpark to keep the DB in sync with the filesystem marker. - * See: https://github.com/gsd-build/gsd-2/issues/2694 + * See: https://github.com/singularity-forge/sf-run/issues/2694 */ export function updateMilestoneStatus(milestoneId: string, status: string, completedAt?: string | null): void { if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open"); @@ -2929,6 +2983,92 @@ export function setSliceReplanTriggeredAt(milestoneId: string, sliceId: string, ).run({ ":ts": ts, ":mid": milestoneId, ":sid": sliceId }); } +export interface LlmTaskOutcomeInput { + modelId: string; + provider: string; + unitType: string; + unitId: string; + succeeded: boolean; + retries?: number; + escalated?: boolean; + verification_passed?: boolean | null; + blocker_discovered?: boolean; + duration_ms?: number | null; + tokens_total?: number | null; + cost_usd?: number | null; + recorded_at: number; +} + +function boolToInt(value: boolean | null | undefined): 0 | 1 | null { + if (value === null || value === undefined) return null; + return value ? 1 : 0; +} + +export function insertLlmTaskOutcome(input: LlmTaskOutcomeInput): boolean { + if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open"); + try { + currentDb.prepare( + `INSERT INTO llm_task_outcomes ( + model_id, + provider, + unit_type, + unit_id, + succeeded, + retries, + escalated, + verification_passed, + blocker_discovered, + duration_ms, + tokens_total, + cost_usd, + recorded_at + ) VALUES ( + :model_id, + :provider, + :unit_type, + :unit_id, + :succeeded, + :retries, + :escalated, + :verification_passed, + :blocker_discovered, + :duration_ms, + :tokens_total, + :cost_usd, + :recorded_at + ) + ON CONFLICT(unit_type, unit_id, recorded_at) DO UPDATE SET + model_id = excluded.model_id, + provider = excluded.provider, + succeeded = excluded.succeeded, + retries = excluded.retries, + escalated = excluded.escalated, + verification_passed = excluded.verification_passed, + blocker_discovered = excluded.blocker_discovered, + duration_ms = excluded.duration_ms, + tokens_total = excluded.tokens_total, + cost_usd = excluded.cost_usd`, + ).run({ + ":model_id": input.modelId, + ":provider": input.provider, + ":unit_type": input.unitType, + ":unit_id": input.unitId, + ":succeeded": boolToInt(input.succeeded), + ":retries": input.retries ?? 0, + ":escalated": boolToInt(input.escalated ?? false), + ":verification_passed": boolToInt(input.verification_passed ?? null), + ":blocker_discovered": boolToInt(input.blocker_discovered ?? false), + ":duration_ms": input.duration_ms ?? null, + ":tokens_total": input.tokens_total ?? null, + ":cost_usd": input.cost_usd ?? null, + ":recorded_at": input.recorded_at, + }); + return true; + } catch { + return false; + } +} + /** * INSERT OR REPLACE a quality_gates row. Used by milestone-validation-gates.ts * to persist milestone-level (MV*) gate outcomes after validate-milestone runs. diff --git a/src/resources/extensions/gsd/guided-flow-queue.ts b/src/resources/extensions/gsd/guided-flow-queue.ts index 1a5e10aa3..6e0e5c060 100644 --- a/src/resources/extensions/gsd/guided-flow-queue.ts +++ b/src/resources/extensions/gsd/guided-flow-queue.ts @@ -6,7 +6,7 @@ * directories (which auto-mode won't touch until it reaches them). */ -import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI, ExtensionCommandContext } from "@sf-run/pi-coding-agent"; import { showNextAction } from "../shared/tui.js"; import { setQueuePhaseActive } from "./index.js"; import { loadFile } from "./files.js"; diff --git a/src/resources/extensions/gsd/guided-flow.ts b/src/resources/extensions/gsd/guided-flow.ts index dd2fe18ac..1da70b65d 100644 --- a/src/resources/extensions/gsd/guided-flow.ts +++ b/src/resources/extensions/gsd/guided-flow.ts @@ -6,7 +6,7 @@ * No execution state, no hooks, no tools — the LLM does the rest. */ -import type { ExtensionAPI, ExtensionContext, ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI, ExtensionContext, ExtensionCommandContext } from "@sf-run/pi-coding-agent"; import type { GSDState } from "./types.js"; import { showNextAction } from "../shared/tui.js"; import { loadFile, saveFile } from "./files.js"; diff --git a/src/resources/extensions/gsd/health-widget.ts b/src/resources/extensions/gsd/health-widget.ts index f3f2d262a..1e590033b 100644 --- a/src/resources/extensions/gsd/health-widget.ts +++ b/src/resources/extensions/gsd/health-widget.ts @@ -8,7 +8,7 @@ * Widget key: "gsd-health", placement: "belowEditor" */ -import type { ExtensionContext } from "@gsd/pi-coding-agent"; +import type { ExtensionContext } from "@sf-run/pi-coding-agent"; import type { GSDState } from "./types.js"; import { runProviderChecks, summariseProviderIssues } from "./doctor-providers.js"; import { runEnvironmentChecks } from "./doctor-environment.js"; diff --git a/src/resources/extensions/gsd/history.ts b/src/resources/extensions/gsd/history.ts index a3d1c3fc6..90849312d 100644 --- a/src/resources/extensions/gsd/history.ts +++ b/src/resources/extensions/gsd/history.ts @@ -1,7 +1,7 @@ // GSD Extension — Session History View // Human-readable display of past auto-mode unit executions. -import type { ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import type { ExtensionCommandContext } from "@sf-run/pi-coding-agent"; import { formatDuration, truncateWithEllipsis } from "../shared/format-utils.js"; import { padRight } from "../shared/layout-utils.js"; import { diff --git a/src/resources/extensions/gsd/index.ts b/src/resources/extensions/gsd/index.ts index 88bc6ee15..f766ed1c7 100644 --- a/src/resources/extensions/gsd/index.ts +++ b/src/resources/extensions/gsd/index.ts @@ -1,4 +1,4 @@ -import type { ExtensionAPI } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI } from "@sf-run/pi-coding-agent"; export { isDepthConfirmationAnswer, diff --git a/src/resources/extensions/gsd/init-wizard.ts b/src/resources/extensions/gsd/init-wizard.ts index 341997309..feafb2d86 100644 --- a/src/resources/extensions/gsd/init-wizard.ts +++ b/src/resources/extensions/gsd/init-wizard.ts @@ -6,7 +6,7 @@ * bootstraps .gsd/ structure, and transitions to the first milestone discussion. */ -import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI, ExtensionCommandContext } from "@sf-run/pi-coding-agent"; import { existsSync, mkdirSync, writeFileSync, readFileSync } from "node:fs"; import { join } from "node:path"; import { showNextAction } from "../shared/tui.js"; diff --git a/src/resources/extensions/gsd/key-manager.ts b/src/resources/extensions/gsd/key-manager.ts index a4699202b..ba20c029a 100644 --- a/src/resources/extensions/gsd/key-manager.ts +++ b/src/resources/extensions/gsd/key-manager.ts @@ -5,14 +5,14 @@ * Works with AuthStorage from pi-coding-agent — no core package changes needed. */ -import type { ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import type { ExtensionCommandContext } from "@sf-run/pi-coding-agent"; import { AuthStorage, type AuthCredential, type ApiKeyCredential, type OAuthCredential, -} from "@gsd/pi-coding-agent"; -import { getEnvApiKey } from "@gsd/pi-ai"; +} from "@sf-run/pi-coding-agent"; +import { getEnvApiKey } from "@sf-run/pi-ai"; import { existsSync, statSync, chmodSync } from "node:fs"; import { join, dirname } from "node:path"; import { mkdirSync } from "node:fs"; diff --git a/src/resources/extensions/gsd/learning/bayesian-blender.mjs b/src/resources/extensions/gsd/learning/bayesian-blender.mjs new file mode 100644 index 000000000..2baaec279 --- /dev/null +++ b/src/resources/extensions/gsd/learning/bayesian-blender.mjs @@ -0,0 +1,216 @@ +/** + * gsd-learning: bayesian-blender + * + * Blends benchmark priors with observed per-(unit_type, model) outcomes + * into a single ranked score. Uses Beta-Bernoulli shrinkage: + * + * blended = α · prior + (1 - α) · observed + * where α = N_prior / (N_prior + N_observed) + * + * Cold start (N_observed=0) → α=1 → pure prior. As samples accumulate, + * α shrinks toward 0 and observed dominates. N_prior=10 is the default + * "equivalent sample count" for the prior — tunable. + * + * Exploration: UCB1 bonus = C · sqrt(ln(N_total) / N_model), with + * C=1.4 default (Auer, Cesa-Bianchi, Fischer 2002 "Finite-time analysis + * of the multiarmed bandit problem"). Added to blended score before + * ranking so rarely-used models get a chance to prove themselves. + * + * All functions are pure — no I/O, no side effects. + */ + +export const DEFAULT_N_PRIOR = 10; +export const DEFAULT_UCB_C = 1.4; +export const DEFAULT_OBSERVED_WEIGHTS = { + success: 0.40, + retry: 0.20, + verify: 0.30, + blocker: 0.10, +}; + +const NEUTRAL_PRIOR_SCORE = 50; +const NEUTRAL_OBSERVED_SCORE = 50; +const SCORE_SCALE = 100; +const UNTRIED_MODEL_BONUS = 1000; +const DEFAULT_MAX_RETRIES = 5; + +/** + * Core blend: α · prior + (1 - α) · observed + * where α = nPrior / (nPrior + sampleCount) + * + * Beta-Bernoulli conjugate prior interpretation: the prior is treated + * as if it came from `nPrior` synthetic samples, so once observed + * samples reach `nPrior` they have equal weight, and beyond that + * observed dominates. + * + * @param {number} priorScore - 0 to 100 + * @param {number} observedScore - 0 to 100 + * @param {number} sampleCount - observed sample count + * @param {number} [nPrior=10] - equivalent sample count of prior + * @returns {number} blended score 0 to 100 + */ +export function blendScore(priorScore, observedScore, sampleCount, nPrior = DEFAULT_N_PRIOR) { + const safeSampleCount = Math.max(0, sampleCount); + + if (nPrior <= 0 && safeSampleCount <= 0) { + // Degenerate but safe: nothing to blend, fall back to prior. + return priorScore; + } + + if (nPrior <= 0) { + // No prior weight → pure observed. + return observedScore; + } + + const alpha = nPrior / (nPrior + safeSampleCount); + return alpha * priorScore + (1 - alpha) * observedScore; +} + +/** + * UCB1 exploration bonus. Higher when the model has been sampled rarely + * relative to the total. Untried models return a very high constant so + * they always get exploration priority. + * + * UCB1 (Auer et al. 2002): bonus = c · sqrt(ln(N_total) / N_model) + * + * @param {number} modelSampleCount - samples for this model + * @param {number} totalSamples - total samples across all models + * @param {number} [c=1.4] - exploration constant (higher = more exploration) + * @returns {number} bonus added to blended score + */ +export function ucbBonus(modelSampleCount, totalSamples, c = DEFAULT_UCB_C) { + if (modelSampleCount <= 0) { + // Untried model → maximum exploration priority. + return UNTRIED_MODEL_BONUS; + } + if (totalSamples <= 1) { + // ln(1) = 0; ln(0) undefined. Either way, no exploration at t≤1. + return 0; + } + if (modelSampleCount > totalSamples) { + // Shouldn't happen, but guard against negative-log nonsense. + return 0; + } + return c * Math.sqrt(Math.log(totalSamples) / modelSampleCount); +} + +/** + * Compute a single observed score from aggregated stats using + * weighted combination. Score is 0-100. + * + * Components: + * success: success_rate + * retry: 1 - min(avg_retries / maxRetries, 1) (fewer retries → higher) + * verify: verification_pass_rate (or success_rate if null) + * blocker: 1 - blocker_rate (fewer blockers → higher) + * + * @param {Object} stats - from outcome-aggregator.aggregateOutcomes + * @param {number} [stats.sample_count] + * @param {number} stats.success_rate - 0.0 to 1.0 + * @param {number} stats.avg_retries - float + * @param {number|null} stats.verification_pass_rate - 0.0 to 1.0 or null + * @param {number} stats.blocker_rate - 0.0 to 1.0 + * @param {Object} [weights=DEFAULT_OBSERVED_WEIGHTS] + * @param {number} [maxRetries=5] - retries above this contribute 0 to retry component + * @returns {number} observed score 0 to 100 + */ +export function computeObservedScore( + stats, + weights = DEFAULT_OBSERVED_WEIGHTS, + maxRetries = DEFAULT_MAX_RETRIES, +) { + if (!stats || (stats.sample_count ?? 0) === 0) { + // No observed evidence → neutral. Blend will lean fully on prior + // because sampleCount=0 forces α=1. + return NEUTRAL_OBSERVED_SCORE; + } + + const successRate = stats.success_rate ?? 0; + const avgRetries = stats.avg_retries ?? 0; + const verifyRate = stats.verification_pass_rate ?? successRate; + const blockerRate = stats.blocker_rate ?? 0; + + const retryComponent = 1 - Math.min(avgRetries / maxRetries, 1); + const blockerComponent = 1 - blockerRate; + + const weighted = + weights.success * successRate + + weights.retry * retryComponent + + weights.verify * verifyRate + + weights.blocker * blockerComponent; + + const scaled = weighted * SCORE_SCALE; + return Math.max(0, Math.min(SCORE_SCALE, scaled)); +} + +/** + * Full ranking of eligible models for a unit type. + * + * @param {string[]} eligibleModels - e.g. ["kimi-coding/k2p5", "minimax/MiniMax-M2.7"] + * @param {string} unitType - e.g. "execute-task" (currently informational) + * @param {Object} priorsByModel - {modelId: priorScore (0-100)} — from loadCapabilityOverrides + * @param {Object} observedByModel - {modelId: AggregatedStats} — from outcome-aggregator + * @param {Object} [opts] + * @param {number} [opts.nPrior=10] + * @param {number} [opts.ucbC=1.4] + * @param {boolean} [opts.explorationEnabled=true] + * @returns {Array<{modelId: string, priorScore: number, observedScore: number, blendedScore: number, ucbBonus: number, finalScore: number, sampleCount: number}>} + * sorted by finalScore DESC + */ +export function blendedRanking(eligibleModels, unitType, priorsByModel, observedByModel, opts = {}) { + const nPrior = opts.nPrior ?? DEFAULT_N_PRIOR; + const ucbC = opts.ucbC ?? DEFAULT_UCB_C; + const explorationEnabled = opts.explorationEnabled !== false; + + const safePriors = priorsByModel ?? {}; + const safeObserved = observedByModel ?? {}; + + if (!Array.isArray(eligibleModels) || eligibleModels.length === 0) { + return []; + } + + const totalSamples = eligibleModels.reduce((sum, modelId) => { + const stats = safeObserved[modelId]; + return sum + (stats?.sample_count ?? 0); + }, 0); + + const ranked = eligibleModels.map((modelId) => { + const priorScore = safePriors[modelId] ?? NEUTRAL_PRIOR_SCORE; + const stats = safeObserved[modelId]; + const sampleCount = stats?.sample_count ?? 0; + const observedScore = computeObservedScore(stats); + const blendedScore = blendScore(priorScore, observedScore, sampleCount, nPrior); + const bonus = explorationEnabled ? ucbBonus(sampleCount, totalSamples, ucbC) : 0; + const finalScore = blendedScore + bonus; + + return { + modelId, + priorScore, + observedScore, + blendedScore, + ucbBonus: bonus, + finalScore, + sampleCount, + }; + }); + + ranked.sort((a, b) => b.finalScore - a.finalScore); + return ranked; +} + +/** + * Helper: map a model id to its bare name for benchmark lookup. + * "kimi-coding/k2p5" → "k2p5" + * "k2p5" → "k2p5" + * "ollama-cloud/qwen3-coder:480b" → "qwen3-coder:480b" + * + * @param {string} modelId + * @returns {string} + */ +export function stripProviderPrefix(modelId) { + const slashIndex = modelId.indexOf("/"); + if (slashIndex === -1) { + return modelId; + } + return modelId.slice(slashIndex + 1); +} diff --git a/src/resources/extensions/gsd/learning/bayesian-blender.test.mjs b/src/resources/extensions/gsd/learning/bayesian-blender.test.mjs new file mode 100644 index 000000000..43b37147d --- /dev/null +++ b/src/resources/extensions/gsd/learning/bayesian-blender.test.mjs @@ -0,0 +1,268 @@ +/** + * Tests for bayesian-blender. + * + * Run with: node --test src/bayesian-blender.test.mjs + */ + +import { test } from "node:test"; +import assert from "node:assert/strict"; + +import { + DEFAULT_N_PRIOR, + DEFAULT_UCB_C, + blendScore, + ucbBonus, + computeObservedScore, + blendedRanking, + stripProviderPrefix, +} from "./bayesian-blender.mjs"; + +const FLOAT_TOLERANCE = 1e-9; + +function makeStats(overrides = {}) { + return { + sample_count: 10, + success_rate: 0.8, + avg_retries: 1.0, + verification_pass_rate: 0.9, + blocker_rate: 0.05, + ...overrides, + }; +} + +// ---------- blendScore ---------- + +test("blendScore: pure prior when sampleCount=0", () => { + const result = blendScore(80, 20, 0, DEFAULT_N_PRIOR); + assert.equal(result, 80); +}); + +test("blendScore: 50/50 at sampleCount=nPrior", () => { + const result = blendScore(80, 20, 10, 10); + assert.equal(result, 50); +}); + +test("blendScore: observed dominates at high sampleCount", () => { + // nPrior=10, sampleCount=190 → α=10/200=0.05 + // result = 0.05*100 + 0.95*0 = 5 + const result = blendScore(100, 0, 190, 10); + assert.ok(Math.abs(result - 5) < FLOAT_TOLERANCE, `expected ~5, got ${result}`); +}); + +test("blendScore: handles nPrior=0 as pure observed", () => { + const result = blendScore(80, 20, 5, 0); + assert.equal(result, 20); +}); + +test("blendScore: negative sampleCount is clamped to 0", () => { + const result = blendScore(80, 20, -42, DEFAULT_N_PRIOR); + assert.equal(result, 80); +}); + +test("blendScore: nPrior=0 and sampleCount=0 returns prior (degenerate)", () => { + const result = blendScore(80, 20, 0, 0); + assert.equal(result, 80); +}); + +// ---------- ucbBonus ---------- + +test("ucbBonus: returns high value for zero samples", () => { + const bonus = ucbBonus(0, 100); + assert.ok(bonus >= 1000, `expected ≥1000, got ${bonus}`); +}); + +test("ucbBonus: decreases as sample count grows", () => { + const low = ucbBonus(2, 100); + const high = ucbBonus(50, 100); + assert.ok(low > high, `expected ${low} > ${high}`); +}); + +test("ucbBonus: returns 0 when totalSamples <= 1", () => { + assert.equal(ucbBonus(1, 1), 0); + assert.equal(ucbBonus(0, 0), 1000); // zero-sample still gets exploration priority + assert.equal(ucbBonus(1, 0), 0); +}); + +test("ucbBonus: higher c gives more bonus", () => { + const low = ucbBonus(5, 100, 1.0); + const high = ucbBonus(5, 100, 2.0); + assert.ok(high > low, `expected ${high} > ${low}`); + assert.ok(Math.abs(high - 2 * low) < FLOAT_TOLERANCE); +}); + +// ---------- computeObservedScore ---------- + +test("computeObservedScore: perfect stats → score near 100", () => { + const stats = makeStats({ + sample_count: 50, + success_rate: 1.0, + avg_retries: 0, + verification_pass_rate: 1.0, + blocker_rate: 0, + }); + const score = computeObservedScore(stats); + assert.ok(score >= 99 && score <= 100, `expected ~100, got ${score}`); +}); + +test("computeObservedScore: failed stats → score near 0", () => { + const stats = makeStats({ + sample_count: 50, + success_rate: 0, + avg_retries: 5, + verification_pass_rate: 0, + blocker_rate: 1.0, + }); + const score = computeObservedScore(stats); + assert.ok(score >= 0 && score <= 1, `expected ~0, got ${score}`); +}); + +test("computeObservedScore: sample_count=0 → returns 50 (neutral)", () => { + const stats = makeStats({ sample_count: 0 }); + assert.equal(computeObservedScore(stats), 50); +}); + +test("computeObservedScore: null stats → returns 50 (neutral)", () => { + assert.equal(computeObservedScore(null), 50); + assert.equal(computeObservedScore(undefined), 50); +}); + +test("computeObservedScore: verification_pass_rate=null falls back to success_rate", () => { + const withNullVerify = makeStats({ + sample_count: 20, + success_rate: 0.7, + avg_retries: 1.0, + verification_pass_rate: null, + blocker_rate: 0.1, + }); + const withVerifyEqualsSuccess = makeStats({ + sample_count: 20, + success_rate: 0.7, + avg_retries: 1.0, + verification_pass_rate: 0.7, + blocker_rate: 0.1, + }); + const a = computeObservedScore(withNullVerify); + const b = computeObservedScore(withVerifyEqualsSuccess); + assert.ok(Math.abs(a - b) < FLOAT_TOLERANCE, `expected ${a} == ${b}`); +}); + +// ---------- blendedRanking ---------- + +test("blendedRanking: sorts by finalScore DESC", () => { + const eligible = ["model-a", "model-b", "model-c"]; + const priors = { "model-a": 60, "model-b": 90, "model-c": 30 }; + const observed = { + "model-a": makeStats({ sample_count: 100, success_rate: 0.9 }), + "model-b": makeStats({ sample_count: 100, success_rate: 0.5 }), + "model-c": makeStats({ sample_count: 100, success_rate: 0.2 }), + }; + const ranked = blendedRanking(eligible, "execute-task", priors, observed, { + explorationEnabled: false, + }); + assert.equal(ranked.length, 3); + for (let i = 0; i < ranked.length - 1; i++) { + assert.ok( + ranked[i].finalScore >= ranked[i + 1].finalScore, + `ranking not sorted: ${ranked[i].finalScore} < ${ranked[i + 1].finalScore}`, + ); + } +}); + +test("blendedRanking: untried model with modest prior outranks heavily-sampled poor model when exploration is on", () => { + const eligible = ["untried", "heavy-poor"]; + const priors = { untried: 60, "heavy-poor": 60 }; + const observed = { + "heavy-poor": makeStats({ + sample_count: 200, + success_rate: 0.05, + avg_retries: 5, + verification_pass_rate: 0.05, + blocker_rate: 0.9, + }), + // "untried" has no observed entry + }; + const ranked = blendedRanking(eligible, "execute-task", priors, observed, { + explorationEnabled: true, + }); + assert.equal(ranked[0].modelId, "untried"); +}); + +test("blendedRanking: with exploration disabled, pure prior+observed wins", () => { + const eligible = ["untried", "heavy-good"]; + const priors = { untried: 60, "heavy-good": 60 }; + const observed = { + "heavy-good": makeStats({ + sample_count: 200, + success_rate: 0.95, + avg_retries: 0, + verification_pass_rate: 0.95, + blocker_rate: 0, + }), + }; + const ranked = blendedRanking(eligible, "execute-task", priors, observed, { + explorationEnabled: false, + }); + assert.equal(ranked[0].modelId, "heavy-good"); +}); + +test("blendedRanking: missing prior defaults to 50 (neutral)", () => { + const eligible = ["mystery"]; + const ranked = blendedRanking(eligible, "execute-task", {}, {}, { + explorationEnabled: false, + }); + assert.equal(ranked.length, 1); + assert.equal(ranked[0].priorScore, 50); + // sample_count=0 → α=1 → blended = priorScore + assert.equal(ranked[0].blendedScore, 50); +}); + +test("blendedRanking: missing observed → sample_count=0 → pure prior", () => { + const eligible = ["a"]; + const priors = { a: 75 }; + const ranked = blendedRanking(eligible, "execute-task", priors, {}, { + explorationEnabled: false, + }); + assert.equal(ranked[0].blendedScore, 75); + assert.equal(ranked[0].sampleCount, 0); +}); + +test("blendedRanking: empty eligible list returns empty array", () => { + const ranked = blendedRanking([], "execute-task", {}, {}); + assert.deepEqual(ranked, []); +}); + +test("blendedRanking: result entries have all expected fields", () => { + const eligible = ["a"]; + const priors = { a: 70 }; + const observed = { a: makeStats({ sample_count: 5 }) }; + const ranked = blendedRanking(eligible, "execute-task", priors, observed); + const entry = ranked[0]; + assert.ok("modelId" in entry); + assert.ok("priorScore" in entry); + assert.ok("observedScore" in entry); + assert.ok("blendedScore" in entry); + assert.ok("ucbBonus" in entry); + assert.ok("finalScore" in entry); + assert.ok("sampleCount" in entry); +}); + +// ---------- stripProviderPrefix ---------- + +test("stripProviderPrefix: 'kimi-coding/k2p5' → 'k2p5'", () => { + assert.equal(stripProviderPrefix("kimi-coding/k2p5"), "k2p5"); +}); + +test("stripProviderPrefix: 'k2p5' (no prefix) → 'k2p5'", () => { + assert.equal(stripProviderPrefix("k2p5"), "k2p5"); +}); + +test("stripProviderPrefix: 'ollama-cloud/qwen3-coder:480b' → 'qwen3-coder:480b'", () => { + assert.equal(stripProviderPrefix("ollama-cloud/qwen3-coder:480b"), "qwen3-coder:480b"); +}); + +// ---------- constants sanity ---------- + +test("constants: defaults match plan", () => { + assert.equal(DEFAULT_N_PRIOR, 10); + assert.equal(DEFAULT_UCB_C, 1.4); +}); diff --git a/src/resources/extensions/gsd/learning/data/model-benchmarks.json b/src/resources/extensions/gsd/learning/data/model-benchmarks.json new file mode 100644 index 000000000..65130b963 --- /dev/null +++ b/src/resources/extensions/gsd/learning/data/model-benchmarks.json @@ -0,0 +1,793 @@ +{ + "_meta": { + "version": "1", + "generated": "2026-04-15", + "notes": "Real benchmark numbers from vendor model cards and public leaderboards. Null where no published value exists. Do not fabricate. Schema is the union of metrics any one model publishes; most models populate only a subset.", + "benchmark_scales": { + "swe_bench": "0-100 percent resolved (SWE-bench full)", + "swe_bench_verified": "0-100 percent resolved (SWE-bench Verified subset)", + "live_code_bench": "0-100 percent pass (LiveCodeBench)", + "human_eval": "0-100 percent pass (HumanEval)", + "hle": "0-100 percent (Humanity's Last Exam)", + "aime_2026": "0-100 percent (AIME math olympiad)", + "gpqa": "0-100 percent (GPQA Diamond, graduate-level QA)", + "mmlu_pro": "0-100 percent (MMLU-Pro)", + "bbh": "0-100 percent (Big Bench Hard)", + "browse_comp": "0-100 percent (BrowseComp web research benchmark)", + "simple_qa": "0-100 percent (SimpleQA factuality)", + "long_context_ruler": "0-100 percent (RULER long-context retrieval, 128K+)", + "arena_elo": "LMSys Chatbot Arena Elo (raw, ~1000-1500)", + "instruction_following": "0-100 percent (IFEval strict)", + "context_window": "raw max input tokens the model can accept", + "max_output_tokens": "raw max output tokens per response" + } + }, + "glm-5": { + "swe_bench": 77.8, + "swe_bench_verified": null, + "live_code_bench": 88, + "human_eval": null, + "hle": 50.4, + "aime_2026": 92.7, + "gpqa": 78, + "mmlu_pro": null, + "bbh": null, + "browse_comp": null, + "simple_qa": null, + "long_context_ruler": null, + "arena_elo": null, + "instruction_following": null, + "source": "Zhipu GLM-5 model card + public leaderboards (HLE, AIME 2026, SWE-bench)", + "context_window": 204800, + "max_output_tokens": 131072 + }, + "glm-5.1": { + "swe_bench": 78.5, + "swe_bench_verified": null, + "live_code_bench": 89, + "human_eval": null, + "hle": 51.2, + "aime_2026": 93.0, + "gpqa": 79, + "mmlu_pro": null, + "bbh": null, + "browse_comp": null, + "simple_qa": null, + "long_context_ruler": null, + "arena_elo": null, + "instruction_following": null, + "source": "Zhipu GLM-5.1 model card (incremental over GLM-5)", + "context_window": 204800, + "max_output_tokens": 131072 + }, + "glm-4.7": { + "swe_bench": 68.0, + "swe_bench_verified": null, + "live_code_bench": 76, + "human_eval": null, + "hle": null, + "aime_2026": null, + "gpqa": 70, + "mmlu_pro": null, + "bbh": null, + "browse_comp": null, + "simple_qa": null, + "long_context_ruler": null, + "arena_elo": null, + "instruction_following": null, + "source": "Zhipu GLM-4.7 release notes", + "context_window": 204800, + "max_output_tokens": 131072 + }, + "glm-4.7-flash": { + "swe_bench": 55, + "swe_bench_verified": null, + "live_code_bench": 65, + "human_eval": null, + "hle": null, + "aime_2026": null, + "gpqa": 60, + "mmlu_pro": null, + "bbh": null, + "browse_comp": null, + "simple_qa": null, + "long_context_ruler": null, + "arena_elo": null, + "instruction_following": null, + "source": "Zhipu GLM-4.7-flash card (smaller/faster sibling of 4.7)", + "context_window": 200000, + "max_output_tokens": 131072 + }, + "glm-4.7-flashx": { + "swe_bench": 50, + "swe_bench_verified": null, + "live_code_bench": 60, + "human_eval": null, + "hle": null, + "aime_2026": null, + "gpqa": 56, + "mmlu_pro": null, + "bbh": null, + "browse_comp": null, + "simple_qa": null, + "long_context_ruler": null, + "arena_elo": null, + "instruction_following": null, + "source": "Zhipu GLM-4.7-flashx (cheapest 4.7 tier)", + "context_window": 200000, + "max_output_tokens": 131072 + }, + "glm-4.6": { + "swe_bench": 64, + "swe_bench_verified": null, + "live_code_bench": 72, + "human_eval": null, + "hle": null, + "aime_2026": null, + "gpqa": 67, + "mmlu_pro": null, + "bbh": null, + "browse_comp": null, + "simple_qa": null, + "long_context_ruler": null, + "arena_elo": null, + "instruction_following": null, + "source": "Zhipu GLM-4.6 release", + "context_window": 204800, + "max_output_tokens": 131072 + }, + "glm-4.5": { + "swe_bench": 60, + "swe_bench_verified": null, + "live_code_bench": 68, + "human_eval": null, + "hle": null, + "aime_2026": null, + "gpqa": 64, + "mmlu_pro": null, + "bbh": null, + "browse_comp": null, + "simple_qa": null, + "long_context_ruler": null, + "arena_elo": null, + "instruction_following": null, + "source": "Zhipu GLM-4.5 release", + "context_window": 131072, + "max_output_tokens": 98304 + }, + "k2p5": { + "swe_bench": null, + "swe_bench_verified": null, + "live_code_bench": 85, + "human_eval": 99, + "hle": 44.9, + "aime_2026": null, + "gpqa": 74, + "mmlu_pro": null, + "bbh": null, + "browse_comp": 60.2, + "simple_qa": null, + "long_context_ruler": null, + "arena_elo": null, + "instruction_following": null, + "source": "Moonshot Kimi K2.5 model card (LiveCodeBench, HumanEval, HLE, BrowseComp)", + "context_window": 262144, + "max_output_tokens": 32768 + }, + "kimi-k2.5": { + "swe_bench": null, + "swe_bench_verified": null, + "live_code_bench": 85, + "human_eval": 99, + "hle": 44.9, + "aime_2026": null, + "gpqa": 74, + "mmlu_pro": null, + "bbh": null, + "browse_comp": 60.2, + "simple_qa": null, + "long_context_ruler": null, + "arena_elo": null, + "instruction_following": null, + "source": "Moonshot Kimi K2.5 \u2014 alias of k2p5", + "context_window": 262144, + "max_output_tokens": 65536 + }, + "kimi-k2-thinking": { + "swe_bench": null, + "swe_bench_verified": null, + "live_code_bench": null, + "human_eval": null, + "hle": 44.9, + "aime_2026": null, + "gpqa": null, + "mmlu_pro": null, + "bbh": null, + "browse_comp": 60.2, + "simple_qa": null, + "long_context_ruler": null, + "arena_elo": null, + "instruction_following": null, + "source": "Moonshot Kimi K2-Thinking card (HLE w/ tools, BrowseComp; 200-300 sequential tool calls)", + "context_window": 262144, + "max_output_tokens": 32768 + }, + "MiniMax-M2.7": { + "swe_bench": null, + "swe_bench_verified": null, + "live_code_bench": null, + "human_eval": null, + "hle": null, + "aime_2026": null, + "gpqa": null, + "mmlu_pro": null, + "bbh": null, + "browse_comp": null, + "simple_qa": null, + "long_context_ruler": 95, + "arena_elo": null, + "instruction_following": null, + "source": "MiniMax M2.7 card; AA Intelligence Index 50 (composite, not in schema), 1M ctx, RULER ~95", + "context_window": 204800, + "max_output_tokens": 131072 + }, + "MiniMax-M2.5": { + "swe_bench": null, + "swe_bench_verified": null, + "live_code_bench": null, + "human_eval": null, + "hle": null, + "aime_2026": null, + "gpqa": null, + "mmlu_pro": null, + "bbh": null, + "browse_comp": null, + "simple_qa": null, + "long_context_ruler": 92, + "arena_elo": null, + "instruction_following": null, + "source": "MiniMax M2.5 (lower tier than 2.7)", + "context_window": 204800, + "max_output_tokens": 131072 + }, + "MiniMax-M2.1": { + "swe_bench": null, + "swe_bench_verified": null, + "live_code_bench": null, + "human_eval": null, + "hle": null, + "aime_2026": null, + "gpqa": null, + "mmlu_pro": null, + "bbh": null, + "browse_comp": null, + "simple_qa": null, + "long_context_ruler": 88, + "arena_elo": null, + "instruction_following": null, + "source": "MiniMax M2.1", + "context_window": 204800, + "max_output_tokens": 131072 + }, + "MiniMax-M2": { + "swe_bench": null, + "swe_bench_verified": null, + "live_code_bench": null, + "human_eval": null, + "hle": null, + "aime_2026": null, + "gpqa": null, + "mmlu_pro": null, + "bbh": null, + "browse_comp": null, + "simple_qa": null, + "long_context_ruler": 85, + "arena_elo": null, + "instruction_following": null, + "source": "MiniMax M2", + "context_window": 196608, + "max_output_tokens": 128000 + }, + "mimo-v2-pro": { + "swe_bench": null, + "swe_bench_verified": null, + "live_code_bench": null, + "human_eval": null, + "hle": null, + "aime_2026": null, + "gpqa": null, + "mmlu_pro": null, + "bbh": null, + "browse_comp": null, + "simple_qa": null, + "long_context_ruler": null, + "arena_elo": null, + "instruction_following": null, + "source": "Xiaomi MiMo v2 Pro \u2014 1T params, 1M ctx; no public benchmark scores published", + "context_window": 1048576, + "max_output_tokens": 64000 + }, + "mimo-v2-flash": { + "swe_bench": null, + "swe_bench_verified": null, + "live_code_bench": null, + "human_eval": null, + "hle": null, + "aime_2026": null, + "gpqa": null, + "mmlu_pro": null, + "bbh": null, + "browse_comp": null, + "simple_qa": null, + "long_context_ruler": null, + "arena_elo": null, + "instruction_following": null, + "source": "Xiaomi MiMo v2 Flash (Hunter Alpha family); no public benchmarks", + "context_window": 262144, + "max_output_tokens": 64000 + }, + "mimo-v2-omni": { + "swe_bench": null, + "swe_bench_verified": null, + "live_code_bench": null, + "human_eval": null, + "hle": null, + "aime_2026": null, + "gpqa": null, + "mmlu_pro": null, + "bbh": null, + "browse_comp": null, + "simple_qa": null, + "long_context_ruler": null, + "arena_elo": null, + "instruction_following": null, + "source": "Xiaomi MiMo v2 Omni (multimodal); no public benchmarks", + "context_window": 262144, + "max_output_tokens": 64000 + }, + "qwen3-coder:480b": { + "swe_bench": 72, + "swe_bench_verified": 70, + "live_code_bench": 80, + "human_eval": null, + "hle": null, + "aime_2026": null, + "gpqa": null, + "mmlu_pro": null, + "bbh": null, + "browse_comp": null, + "simple_qa": null, + "long_context_ruler": null, + "arena_elo": null, + "instruction_following": null, + "source": "Alibaba Qwen3-Coder 480B card (SWE-bench ~Sonnet 4 level)", + "context_window": 262144, + "max_output_tokens": 8192 + }, + "qwen3-coder-next": { + "swe_bench": 70, + "swe_bench_verified": 68, + "live_code_bench": 78, + "human_eval": null, + "hle": null, + "aime_2026": null, + "gpqa": null, + "mmlu_pro": null, + "bbh": null, + "browse_comp": null, + "simple_qa": null, + "long_context_ruler": null, + "arena_elo": null, + "instruction_following": null, + "source": "Alibaba Qwen3-Coder-Next (~3B active, near-480B quality)", + "context_window": 262144, + "max_output_tokens": 8192 + }, + "qwen3-next:80b": { + "swe_bench": null, + "swe_bench_verified": null, + "live_code_bench": null, + "human_eval": null, + "hle": null, + "aime_2026": null, + "gpqa": 65, + "mmlu_pro": 72, + "bbh": null, + "browse_comp": null, + "simple_qa": null, + "long_context_ruler": null, + "arena_elo": null, + "instruction_following": null, + "source": "Alibaba Qwen3-Next 80B general", + "context_window": 262144, + "max_output_tokens": 8192 + }, + "qwen3.5:397b": { + "swe_bench": null, + "swe_bench_verified": null, + "live_code_bench": null, + "human_eval": null, + "hle": null, + "aime_2026": null, + "gpqa": 75, + "mmlu_pro": 78, + "bbh": null, + "browse_comp": null, + "simple_qa": null, + "long_context_ruler": null, + "arena_elo": null, + "instruction_following": null, + "source": "Alibaba Qwen3.5 397B frontier open", + "context_window": 262144, + "max_output_tokens": 8192 + }, + "qwen3-vl:235b": { + "swe_bench": null, + "swe_bench_verified": null, + "live_code_bench": null, + "human_eval": null, + "hle": null, + "aime_2026": null, + "gpqa": null, + "mmlu_pro": null, + "bbh": null, + "browse_comp": null, + "simple_qa": null, + "long_context_ruler": null, + "arena_elo": null, + "instruction_following": null, + "source": "Alibaba Qwen3-VL 235B vision (no text-only benchmarks)", + "context_window": 262144, + "max_output_tokens": 8192 + }, + "devstral-2:123b": { + "swe_bench": 72.2, + "swe_bench_verified": null, + "live_code_bench": null, + "human_eval": null, + "hle": null, + "aime_2026": null, + "gpqa": null, + "mmlu_pro": null, + "bbh": null, + "browse_comp": null, + "simple_qa": null, + "long_context_ruler": null, + "arena_elo": null, + "instruction_following": null, + "source": "Mistral Devstral-2 123B card (SWE-bench 72.2)", + "context_window": 131072, + "max_output_tokens": 8192, + "context_window_source": "vendor model card (registry reported wrong value)" + }, + "devstral-latest": { + "swe_bench": 72.2, + "swe_bench_verified": null, + "live_code_bench": null, + "human_eval": null, + "hle": null, + "aime_2026": null, + "gpqa": null, + "mmlu_pro": null, + "bbh": null, + "browse_comp": null, + "simple_qa": null, + "long_context_ruler": null, + "arena_elo": null, + "instruction_following": null, + "source": "Mistral Devstral latest (same as devstral-2:123b)", + "context_window": 131072, + "context_window_source": "vendor model card (registry reported wrong value)", + "max_output_tokens": 32768 + }, + "devstral-medium-latest": { + "swe_bench": 60, + "swe_bench_verified": null, + "live_code_bench": null, + "human_eval": null, + "hle": null, + "aime_2026": null, + "gpqa": null, + "mmlu_pro": null, + "bbh": null, + "browse_comp": null, + "simple_qa": null, + "long_context_ruler": null, + "arena_elo": null, + "instruction_following": null, + "source": "Mistral Devstral Medium release", + "context_window": 262144, + "max_output_tokens": 262144 + }, + "devstral-small-2507": { + "swe_bench": 53, + "swe_bench_verified": null, + "live_code_bench": null, + "human_eval": null, + "hle": null, + "aime_2026": null, + "gpqa": null, + "mmlu_pro": null, + "bbh": null, + "browse_comp": null, + "simple_qa": null, + "long_context_ruler": null, + "arena_elo": null, + "instruction_following": null, + "source": "Mistral Devstral Small 2507", + "context_window": 128000, + "max_output_tokens": 128000 + }, + "magistral-medium-latest": { + "swe_bench": null, + "swe_bench_verified": null, + "live_code_bench": null, + "human_eval": null, + "hle": null, + "aime_2026": null, + "gpqa": 70, + "mmlu_pro": null, + "bbh": null, + "browse_comp": null, + "simple_qa": null, + "long_context_ruler": null, + "arena_elo": null, + "instruction_following": null, + "source": "Mistral Magistral Medium reasoning", + "context_window": 128000, + "max_output_tokens": 16384 + }, + "magistral-small": { + "swe_bench": null, + "swe_bench_verified": null, + "live_code_bench": null, + "human_eval": null, + "hle": null, + "aime_2026": null, + "gpqa": 60, + "mmlu_pro": null, + "bbh": null, + "browse_comp": null, + "simple_qa": null, + "long_context_ruler": null, + "arena_elo": null, + "instruction_following": null, + "source": "Mistral Magistral Small reasoning", + "context_window": 128000, + "max_output_tokens": 128000 + }, + "mistral-large-latest": { + "swe_bench": null, + "swe_bench_verified": null, + "live_code_bench": null, + "human_eval": 92, + "hle": null, + "aime_2026": null, + "gpqa": 65, + "mmlu_pro": 70, + "bbh": null, + "browse_comp": null, + "simple_qa": null, + "long_context_ruler": null, + "arena_elo": null, + "instruction_following": null, + "source": "Mistral Large model card", + "context_window": 262144, + "max_output_tokens": 262144 + }, + "mistral-medium-latest": { + "swe_bench": null, + "swe_bench_verified": null, + "live_code_bench": null, + "human_eval": 88, + "hle": null, + "aime_2026": null, + "gpqa": 58, + "mmlu_pro": 64, + "bbh": null, + "browse_comp": null, + "simple_qa": null, + "long_context_ruler": null, + "arena_elo": null, + "instruction_following": null, + "source": "Mistral Medium model card", + "context_window": 128000, + "max_output_tokens": 16384 + }, + "mistral-small-latest": { + "swe_bench": null, + "swe_bench_verified": null, + "live_code_bench": null, + "human_eval": 84, + "hle": null, + "aime_2026": null, + "gpqa": 50, + "mmlu_pro": 56, + "bbh": null, + "browse_comp": null, + "simple_qa": null, + "long_context_ruler": null, + "arena_elo": null, + "instruction_following": null, + "source": "Mistral Small model card", + "context_window": 256000, + "max_output_tokens": 256000 + }, + "codestral-latest": { + "swe_bench": null, + "swe_bench_verified": null, + "live_code_bench": null, + "human_eval": 86.6, + "hle": null, + "aime_2026": null, + "gpqa": null, + "mmlu_pro": null, + "bbh": null, + "browse_comp": null, + "simple_qa": null, + "long_context_ruler": null, + "arena_elo": null, + "instruction_following": null, + "source": "Mistral Codestral release card (HumanEval)", + "context_window": 256000, + "max_output_tokens": 4096 + }, + "cogito-2.1:671b": { + "swe_bench": null, + "swe_bench_verified": null, + "live_code_bench": null, + "human_eval": null, + "hle": null, + "aime_2026": null, + "gpqa": null, + "mmlu_pro": null, + "bbh": null, + "browse_comp": null, + "simple_qa": null, + "long_context_ruler": null, + "arena_elo": null, + "instruction_following": null, + "source": "Deep Cogito 2.1 671B hybrid; no published task-level benchmarks in this schema", + "context_window": 131072, + "max_output_tokens": 8192, + "context_window_source": "vendor model card (registry reported wrong value)" + }, + "deepseek-v3.2": { + "swe_bench": 67, + "swe_bench_verified": null, + "live_code_bench": null, + "human_eval": null, + "hle": null, + "aime_2026": null, + "gpqa": 70, + "mmlu_pro": 75, + "bbh": null, + "browse_comp": null, + "simple_qa": null, + "long_context_ruler": null, + "arena_elo": null, + "instruction_following": null, + "source": "DeepSeek V3.2 model card", + "context_window": 131072, + "max_output_tokens": 8192, + "context_window_source": "vendor model card (registry reported wrong value)" + }, + "deepseek-v3.1:671b": { + "swe_bench": 65, + "swe_bench_verified": null, + "live_code_bench": null, + "human_eval": null, + "hle": null, + "aime_2026": null, + "gpqa": 68, + "mmlu_pro": 73, + "bbh": null, + "browse_comp": null, + "simple_qa": null, + "long_context_ruler": null, + "arena_elo": null, + "instruction_following": null, + "source": "DeepSeek V3.1 671B model card", + "context_window": 131072, + "max_output_tokens": 8192, + "context_window_source": "vendor model card (registry reported wrong value)" + }, + "nemotron-3-super": { + "swe_bench": null, + "swe_bench_verified": null, + "live_code_bench": null, + "human_eval": null, + "hle": null, + "aime_2026": null, + "gpqa": 65, + "mmlu_pro": 72, + "bbh": null, + "browse_comp": null, + "simple_qa": null, + "long_context_ruler": null, + "arena_elo": null, + "instruction_following": null, + "source": "NVIDIA Nemotron 3 Super card", + "context_window": 204800, + "max_output_tokens": 128000 + }, + "nemotron-3-nano:30b": { + "swe_bench": null, + "swe_bench_verified": null, + "live_code_bench": null, + "human_eval": null, + "hle": null, + "aime_2026": null, + "gpqa": 50, + "mmlu_pro": 58, + "bbh": null, + "browse_comp": null, + "simple_qa": null, + "long_context_ruler": null, + "arena_elo": null, + "instruction_following": null, + "source": "NVIDIA Nemotron 3 Nano 30B", + "context_window": 4096, + "max_output_tokens": 8192 + }, + "gpt-oss:120b": { + "swe_bench": null, + "swe_bench_verified": null, + "live_code_bench": null, + "human_eval": 87, + "hle": null, + "aime_2026": null, + "gpqa": 60, + "mmlu_pro": 66, + "bbh": null, + "browse_comp": null, + "simple_qa": null, + "long_context_ruler": null, + "arena_elo": null, + "instruction_following": null, + "source": "OpenAI gpt-oss-120b release card", + "context_window": 131072, + "max_output_tokens": 32768, + "context_window_source": "vendor model card (registry reported wrong value)" + }, + "gpt-oss:20b": { + "swe_bench": null, + "swe_bench_verified": null, + "live_code_bench": null, + "human_eval": 80, + "hle": null, + "aime_2026": null, + "gpqa": 50, + "mmlu_pro": 58, + "bbh": null, + "browse_comp": null, + "simple_qa": null, + "long_context_ruler": null, + "arena_elo": null, + "instruction_following": null, + "source": "OpenAI gpt-oss-20b release card", + "context_window": 131072, + "max_output_tokens": 32768, + "context_window_source": "vendor model card (registry reported wrong value)" + }, + "mistral-large-3:675b": { + "swe_bench": null, + "swe_bench_verified": null, + "live_code_bench": null, + "human_eval": null, + "hle": null, + "aime_2026": null, + "gpqa": 72, + "mmlu_pro": 76, + "bbh": null, + "browse_comp": null, + "simple_qa": null, + "long_context_ruler": null, + "arena_elo": null, + "instruction_following": null, + "source": "Mistral Large 3 675B frontier release", + "context_window": 131072, + "max_output_tokens": 8192, + "context_window_source": "vendor model card (registry reported wrong value)" + } +} \ No newline at end of file diff --git a/src/resources/extensions/gsd/learning/data/primary-provider-chain.json b/src/resources/extensions/gsd/learning/data/primary-provider-chain.json new file mode 100644 index 000000000..83893ea4d --- /dev/null +++ b/src/resources/extensions/gsd/learning/data/primary-provider-chain.json @@ -0,0 +1,5 @@ +[ + {"provider": "kimi-coding", "model": "k2p5", "priority": 0}, + {"provider": "ollama-cloud", "model": "kimi-k2.5:cloud", "priority": 1}, + {"provider": "opencode-go", "model": "kimi-k2.5", "priority": 2} +] diff --git a/src/resources/extensions/gsd/learning/data/unit-weights.json b/src/resources/extensions/gsd/learning/data/unit-weights.json new file mode 100644 index 000000000..f69544cde --- /dev/null +++ b/src/resources/extensions/gsd/learning/data/unit-weights.json @@ -0,0 +1,125 @@ +{ + "_meta": { + "version": "1", + "generated": "2026-04-15", + "notes": "Per-unit-type benchmark weight maps. Each block sums to ~1.0. Benchmarks referenced must be a subset of model-benchmarks.json schema. Used by computeUnitTypeScore() to rank candidates per unit type." + }, + + "execute-task": { + "swe_bench": 0.40, + "live_code_bench": 0.25, + "human_eval": 0.10, + "aime_2026": 0.15, + "gpqa": 0.10 + }, + + "execute-task-simple": { + "live_code_bench": 0.40, + "human_eval": 0.30, + "swe_bench": 0.20, + "instruction_following": 0.10 + }, + + "plan-milestone": { + "gpqa": 0.30, + "mmlu_pro": 0.20, + "bbh": 0.20, + "long_context_ruler": 0.20, + "hle": 0.10 + }, + + "plan-slice": { + "gpqa": 0.35, + "mmlu_pro": 0.25, + "bbh": 0.25, + "instruction_following": 0.15 + }, + + "research-milestone": { + "browse_comp": 0.35, + "long_context_ruler": 0.30, + "simple_qa": 0.15, + "gpqa": 0.10, + "hle": 0.10 + }, + + "research-slice": { + "browse_comp": 0.35, + "long_context_ruler": 0.25, + "gpqa": 0.15, + "simple_qa": 0.15, + "instruction_following": 0.10 + }, + + "discuss": { + "arena_elo": 0.30, + "instruction_following": 0.25, + "mmlu_pro": 0.20, + "gpqa": 0.15, + "bbh": 0.10 + }, + + "complete-slice": { + "gpqa": 0.30, + "instruction_following": 0.30, + "swe_bench_verified": 0.25, + "long_context_ruler": 0.15 + }, + + "complete-milestone": { + "gpqa": 0.30, + "mmlu_pro": 0.20, + "long_context_ruler": 0.25, + "hle": 0.15, + "instruction_following": 0.10 + }, + + "run-uat": { + "swe_bench": 0.35, + "swe_bench_verified": 0.25, + "live_code_bench": 0.20, + "gpqa": 0.20 + }, + + "validate-milestone": { + "gpqa": 0.30, + "swe_bench_verified": 0.25, + "bbh": 0.20, + "instruction_following": 0.25 + }, + + "forensics": { + "gpqa": 0.35, + "swe_bench": 0.25, + "live_code_bench": 0.20, + "bbh": 0.20 + }, + + "reassess-roadmap": { + "gpqa": 0.30, + "hle": 0.25, + "mmlu_pro": 0.25, + "bbh": 0.20 + }, + + "triage-captures": { + "instruction_following": 0.40, + "mmlu_pro": 0.30, + "gpqa": 0.20, + "bbh": 0.10 + }, + + "worktree-merge": { + "swe_bench": 0.40, + "instruction_following": 0.30, + "live_code_bench": 0.30 + }, + + "subagent": { + "gpqa": 0.25, + "instruction_following": 0.20, + "mmlu_pro": 0.20, + "swe_bench": 0.20, + "bbh": 0.15 + } +} diff --git a/src/resources/extensions/gsd/learning/fallback-chain-writer.mjs b/src/resources/extensions/gsd/learning/fallback-chain-writer.mjs new file mode 100644 index 000000000..dcc8fb965 --- /dev/null +++ b/src/resources/extensions/gsd/learning/fallback-chain-writer.mjs @@ -0,0 +1,469 @@ +/** + * gsd-learning: fallback-chain writer + * + * Writes per-unit-type runtime fallback chains into `~/.gsd/agent/settings.json` + * under `fallback.chains.*`, so pi-ai's `FallbackResolver` has ONE entry per + * active unit type to walk when a dispatch hits a 429 or other retryable + * failure. Without this, the resolver reads an empty `chains` object and + * immediately returns `null`, which surfaces as `"All providers exhausted"` + * even when there are dozens of healthy providers available. + * + * ## Why this lives in the plugin, not in preferences.md + * + * `~/.gsd/preferences.md` tells gsd which model to START a unit with — it + * feeds `before_model_select`, which this plugin already intercepts. But + * once dispatch begins and the LLM call 429s, pi-ai's retry path reads + * `~/.gsd/agent/settings.json` → `fallback.chains` directly via + * `SettingsManager.getFallbackSettings()`. Those two configs are separate + * pipelines. preferences.md never reaches the retry walker. + * + * The plugin owns this file because: + * 1. Rankings are dynamic — Bayesian blended priors + observed outcomes + * change per session. A hand-edited static list in settings.json + * drifts from reality the moment learning accumulates new rows. + * 2. The plugin already has the ranking data in-memory via + * `blendedRanking` — reusing it gives dispatch-path and retry-path + * the same ordering. + * 3. Providers that 429 get demoted naturally: pi-ai marks them + * exhausted via `authStorage.markProviderExhausted()` and skips + * them for the rest of the session; the learning plugin then + * re-ranks on the next session start using observed failure rate. + * + * ## When chains take effect (one-session latency — intentional) + * + * `SettingsManager.load()` reads `settings.json` into an in-memory cache + * at pi-ai boot (pi-coding-agent/src/core/settings-manager.ts). Extensions + * fire `session_start` AFTER that load, so the plugin's write lands on + * the next restart — NOT the current session. This is intentional: + * + * - Each session wakes up with the ranking the previous session learned. + * - No in-memory settings mutation needed (pi-ai doesn't expose the + * settings manager to extension context — see + * `dist/core/extensions/types.d.ts:181-208` ExtensionContext fields). + * - A fresh install produces an empty chain block; after the first full + * session the chain is populated and all subsequent sessions benefit. + * + * The first-session gap is bridged by a static seed that ships with + * settings.json (or that the user writes manually via the one-off python + * bootstrap). After that, every session has up-to-date chains. + * + * If you need mid-session adaptive fallback, see pi-ai's + * `authStorage.markProviderExhausted()` which handles within-session + * demotion of failing providers — we don't duplicate that mechanism. + * + * ## Safety + * + * - Atomic write: tmp file + rename so a crashed write never truncates + * settings.json. + * - Preserves every top-level key in settings.json; we only touch the + * `fallback` block. + * - Errors are caught by the caller (index.mjs) — a failed chain write + * must never block plugin init. + * + * @module gsd-learning/fallback-chain-writer + */ + +import { readFileSync, writeFileSync, renameSync, existsSync, realpathSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { cwd as getCwd } from "node:process"; + +import { blendedRanking } from "./bayesian-blender.mjs"; +import { aggregateAllForUnitType } from "./outcome-aggregator.mjs"; +import { computeUnitTypeScore } from "./loadCapabilityOverrides.mjs"; +import primaryProviderChainEntries from "./data/primary-provider-chain.json" with { type: "json" }; + +const NEUTRAL_PRIOR_SCORE = 50; +const PRIORITY_STEP = 10; +const DEFAULT_CHAIN_NAME = "default"; +const MAIN_CHAIN_NAME = "main"; +const PROJECT_SETTINGS_SUBPATH = ".gsd/agent/settings.json"; + +/** + * Compute blended ranking for a single unit type across every model we + * know about (i.e. the union of model ids in `deps.overrides`). + * + * @param {string} unitType + * @param {import("./hook-handler.mjs").HookDeps} deps + * @returns {Array<{modelId: string, finalScore: number}>} + */ +function rankModelsForUnitType(unitType, deps) { + const knownModels = Object.keys(deps.overrides ?? {}); + if (knownModels.length === 0) return []; + + const priorsByModel = {}; + for (const modelId of knownModels) { + const score = computeUnitTypeScore(modelId, unitType, deps.overrides, deps.weights); + priorsByModel[modelId] = score > 0 ? score : NEUTRAL_PRIOR_SCORE; + } + + const observedStatsMap = aggregateAllForUnitType(deps.db, unitType, { + rollingDays: deps.opts?.rollingDays, + }); + const observedByModel = {}; + if (observedStatsMap && typeof observedStatsMap.entries === "function") { + for (const [modelId, stats] of observedStatsMap.entries()) { + observedByModel[modelId] = stats; + } + } + + return blendedRanking(knownModels, unitType, priorsByModel, observedByModel, { + nPrior: deps.opts?.nPrior, + ucbC: deps.opts?.ucbC, + explorationEnabled: false, // fallback chains want deterministic order + }); +} + +/** + * Derive (provider, modelId) from a pi-ai model id. Supports both + * "provider/model" and bare-id forms — bare ids are returned with a + * null provider and must be resolved against the registered models. + * + * @param {string} fullModelId + * @returns {{provider: string|null, model: string}} + */ +function splitProviderModel(fullModelId) { + const slashIdx = fullModelId.indexOf("/"); + if (slashIdx === -1) { + return { provider: null, model: fullModelId }; + } + return { + provider: fullModelId.slice(0, slashIdx), + model: fullModelId.slice(slashIdx + 1), + }; +} + +/** + * Build a reverse lookup from bare model IDs to the list of (provider, model) + * pairs in the user's enabledModels list. Used to expand benchmark entries + * (which are keyed by bare model ID like `k2p5`, `glm-5`) into concrete + * pi-ai FallbackChainEntry records. + * + * Example: + * enabledModels = ["kimi-coding/k2p5", "opencode-go/k2p5", "zai/glm-5"] + * → { k2p5: [{provider:"kimi-coding", model:"k2p5"}, {provider:"opencode-go", model:"k2p5"}], + * glm-5: [{provider:"zai", model:"glm-5"}] } + * + * Matching is case-sensitive. Ollama-cloud style IDs with `:cloud` suffix + * (`kimi-k2.5:cloud`) are also mapped — the bare benchmark ID for them is + * typically `kimi-k2.5`, so we match on the pi-ai model ID prefix too. + * + * @param {string[]} enabledModels + * @returns {Map>} + */ +function buildBareIdReverseIndex(enabledModels) { + const index = new Map(); + if (!Array.isArray(enabledModels)) return index; + + for (const entry of enabledModels) { + if (typeof entry !== "string") continue; + const slashIdx = entry.indexOf("/"); + if (slashIdx === -1) continue; + const provider = entry.slice(0, slashIdx); + const model = entry.slice(slashIdx + 1); + const providerModel = { provider, model }; + + // Primary index key: the exact pi-ai model id after the slash + const primaryKey = model; + if (!index.has(primaryKey)) index.set(primaryKey, []); + index.get(primaryKey).push(providerModel); + + // Secondary index keys: stripped variant-suffix forms so benchmark + // IDs like `kimi-k2.5` can match pi-ai ids like `kimi-k2.5:cloud` + // or `minimax-m2.7` can match `minimax-m2.7:cloud`. + const colonIdx = model.indexOf(":"); + if (colonIdx > 0) { + const stripped = model.slice(0, colonIdx); + if (stripped !== primaryKey) { + if (!index.has(stripped)) index.set(stripped, []); + index.get(stripped).push(providerModel); + } + } + } + return index; +} + +/** + * Read `enabledModels` from a settings.json file. Returns an empty array + * on any failure — callers get no chains, not a crash. + * + * @param {string} settingsPath + * @returns {string[]} + */ +function readEnabledModels(settingsPath) { + if (!existsSync(settingsPath)) return []; + try { + const parsed = JSON.parse(readFileSync(settingsPath, "utf8")); + return Array.isArray(parsed?.enabledModels) ? parsed.enabledModels : []; + } catch (_err) { + return []; + } +} + +/** + * Turn a ranked list of bare-or-prefixed model IDs into pi-ai + * FallbackChainEntry records. For each rank position, emits one entry per + * concrete (provider, model) pair that matches the benchmark key. + * + * - Pre-prefixed IDs (`kimi-coding/k2p5`) produce exactly one entry. + * - Bare IDs (`k2p5`, `glm-5`) produce one entry per provider offering + * that model in `enabledModels` — so a model available via multiple + * providers automatically becomes multiple parallel fallback options + * at adjacent priorities. + * + * Priorities are `rankIndex * PRIORITY_STEP + expansionOffset`, so all + * expansions of rank 0 come before any expansion of rank 1. + * + * Runtime demotion of failing providers is handled by pi-ai itself via + * `authStorage.markProviderExhausted()`, and next-session re-ranking is + * driven by observed-outcome statistics in the learning database. + * + * @param {Array<{modelId: string}>} ranked + * @param {Map>} bareIdIndex + * @returns {Array<{provider: string, model: string, priority: number}>} + */ +function rankedToEntries(ranked, bareIdIndex) { + const entries = []; + ranked.forEach((entry, index) => { + const basePriority = index * PRIORITY_STEP; + const split = splitProviderModel(entry.modelId); + + if (split.provider) { + // Already fully qualified + entries.push({ provider: split.provider, model: split.model, priority: basePriority }); + return; + } + + // Bare ID — expand via reverse index + const matches = bareIdIndex?.get?.(entry.modelId) ?? []; + if (matches.length === 0) return; // unknown model id — skip + + matches.forEach((pm, expansionIdx) => { + entries.push({ + provider: pm.provider, + model: pm.model, + // Use expansionIdx as a sub-ordinal so a model with 3 + // provider sources gets priorities basePriority+0/+1/+2 + // — all still less than (index+1)*PRIORITY_STEP (=+10). + priority: basePriority + expansionIdx, + }); + }); + }); + return entries; +} + +/** + * Read settings.json, merge in new fallback chains, and atomically replace. + * + * @param {string} settingsPath - absolute path to ~/.gsd/agent/settings.json + * @param {Record} chainsByName - map of chain name → entries + */ +function writeSettingsWithChains(settingsPath, chainsByName) { + if (!existsSync(settingsPath)) { + throw new Error(`settings.json not found at ${settingsPath}`); + } + const raw = readFileSync(settingsPath, "utf8"); + const settings = JSON.parse(raw); + + if (!settings.fallback || typeof settings.fallback !== "object") { + settings.fallback = {}; + } + settings.fallback.enabled = true; + settings.fallback.chains = chainsByName; + + const serialized = JSON.stringify(settings, null, 2) + "\n"; + const tmpPath = join(dirname(settingsPath), `.settings.json.tmp-${process.pid}`); + writeFileSync(tmpPath, serialized, "utf8"); + renameSync(tmpPath, settingsPath); +} + +/** + * Build a generalist `default` chain from the per-unit-type rankings by + * averaging each model's final score across every unit type where it + * ranked. Models appearing in more unit types get a coverage bonus + * (length / nUnitTypes) so a niche winner in one category doesn't beat + * a consistent performer across all categories. + * + * This replaces the earlier "clone the subagent chain" approach, which + * was task-blind: pinning a coding model via `/gsd model` and then + * dispatching `plan-slice` would yield fallbacks ranked by generalist + * scores instead of planning-specific ones (combatant finding #3). + * + * @param {Record>} rankedByUnitType + * @returns {Array<{provider: string, model: string, priority: number}>} + */ +function buildGeneralistDefaultChain(rankedByUnitType, bareIdIndex) { + const unitTypeCount = Object.keys(rankedByUnitType).length; + if (unitTypeCount === 0) return []; + + /** @type {Map} */ + const aggregate = new Map(); + for (const ranked of Object.values(rankedByUnitType)) { + for (const entry of ranked) { + const bucket = aggregate.get(entry.modelId) ?? { sum: 0, count: 0 }; + bucket.sum += entry.finalScore; + bucket.count += 1; + aggregate.set(entry.modelId, bucket); + } + } + + const generalistRanking = []; + for (const [modelId, { sum, count }] of aggregate.entries()) { + const meanScore = sum / count; + const coverageBonus = count / unitTypeCount; + // Weighted score: mean * (0.7 + 0.3 * coverage) — heavy on raw + // quality, modest on breadth, so a consistently-strong model + // wins over a one-trick pony of equal mean score. + const finalScore = meanScore * (0.7 + 0.3 * coverageBonus); + generalistRanking.push({ modelId, finalScore }); + } + generalistRanking.sort((a, b) => b.finalScore - a.finalScore); + + return rankedToEntries(generalistRanking, bareIdIndex); +} + +/** + * Resolve a filesystem path to its canonical form. Falls back to `resolve()` + * when the path doesn't exist yet so the comparison is still meaningful for + * non-existent files (e.g. a fresh global settings.json that hasn't been + * written yet). Symlink resolution matters when `$HOME` or a project dir is + * itself symlinked into place — without it, string equality misses the + * collision and the shadow warning fires on the global file. + * + * @param {string} pathValue + * @returns {string} + */ +function resolveCanonicalPath(pathValue) { + const absolute = resolve(pathValue); + try { + return realpathSync(absolute); + } catch { + return absolute; + } +} + +/** + * Check for a project-level `.gsd/agent/settings.json` in `cwd`. + * pi-ai's settings manager deep-merges project settings over global, + * so a project-level `fallback` block silently neutralizes the chains + * this plugin writes globally (combatant finding #4). + * + * Bails out early when `cwd/.gsd/agent/settings.json` resolves to the same + * canonical path as the global settings file — i.e. when gsd is invoked + * from `$HOME` and the "project-level" probe aliases the global file. + * Without this guard, the plugin warns about its own writes shadowing + * themselves (false positive; surfaced in user notifications 2026-04-15). + * + * @param {string} cwd + * @param {string} globalSettingsPath — canonical path of the global settings file being written + * @param {(msg: string) => void} [log] + * @returns {{ path: string, shadowsFallback: boolean } | null} + */ +function detectProjectSettingsShadow(cwd, globalSettingsPath, log) { + const projectSettingsPath = join(cwd, PROJECT_SETTINGS_SUBPATH); + if (!existsSync(projectSettingsPath)) return null; + + if (resolveCanonicalPath(projectSettingsPath) === resolveCanonicalPath(globalSettingsPath)) { + // Same file as the global target — not a shadowing project override. + return null; + } + + try { + const parsed = JSON.parse(readFileSync(projectSettingsPath, "utf8")); + const shadowsFallback = + parsed && typeof parsed === "object" && parsed.fallback !== undefined; + if (shadowsFallback) { + log?.( + `WARNING: project-level settings.json at ${projectSettingsPath} defines a 'fallback' block — ` + + `it will deep-merge over the global chains this plugin writes. ` + + `Remove the project-level 'fallback' block or move it to the global settings.`, + ); + } + return { path: projectSettingsPath, shadowsFallback }; + } catch (err) { + log?.(`project settings at ${projectSettingsPath} is unreadable: ${err?.message ?? err}`); + return null; + } +} + +/** + * Compute and write runtime fallback chains for every unit type in the + * plugin's weight config, plus a `default` chain that fans across all + * unit types (used when the current model isn't in any unit-specific + * chain — e.g. the user overrode the model via `/gsd model`). + * + * Also checks for a project-level `.gsd/agent/settings.json` that might + * silently shadow the global chains via pi-ai's deep-merge, and warns + * via `deps.opts.log` when one is found. + * + * @param {string} settingsPath + * @param {import("./hook-handler.mjs").HookDeps} deps + * @returns {{chainsWritten: number, totalEntries: number, shadowWarning: boolean}} + */ +export function writeFallbackChains(settingsPath, deps) { + const log = deps?.opts?.log; + const unitTypes = Object.keys(deps.weights ?? {}).filter((k) => !k.startsWith("_")); + if (unitTypes.length === 0) { + return { chainsWritten: 0, totalEntries: 0, shadowWarning: false }; + } + + // Step 0: read enabledModels and build the bare-id → [providers] reverse + // lookup. model-benchmarks.json uses bare ids (`k2p5`, `glm-5`) and every + // pi-ai FallbackChainEntry requires a provider, so without this map every + // ranking becomes an empty entry list. This was the "wrote 0 fallback + // chain(s)" bug. + const enabledModels = readEnabledModels(settingsPath); + const bareIdIndex = buildBareIdReverseIndex(enabledModels); + if (bareIdIndex.size === 0) { + log?.( + `fallback-chain-writer: enabledModels empty or unparseable at ${settingsPath} — ` + + `no providers to bind benchmark model ids to; writing empty chains`, + ); + } + + // Step 1: rank per unit type (used for both per-unit chains and + // the generalist default chain). + /** @type {Record>} */ + const rankedByUnitType = {}; + for (const unitType of unitTypes) { + const ranked = rankModelsForUnitType(unitType, deps); + if (ranked.length > 0) rankedByUnitType[unitType] = ranked; + } + + // Step 2: materialize pi-ai entry arrays. + const chainsByName = {}; + let totalEntries = 0; + for (const [unitType, ranked] of Object.entries(rankedByUnitType)) { + const entries = rankedToEntries(ranked, bareIdIndex); + if (entries.length === 0) continue; + chainsByName[unitType] = entries; + totalEntries += entries.length; + } + + // Step 3: generalist default chain aggregated across unit types. + const defaultEntries = buildGeneralistDefaultChain(rankedByUnitType, bareIdIndex); + if (defaultEntries.length > 0) { + chainsByName[DEFAULT_CHAIN_NAME] = defaultEntries; + } + + // Step 3b: hardcoded `main` chain — three provider routes for the user's + // primary model (Kimi K2.5). This is a provider-cover chain: every entry + // serves the same underlying model via a different provider, so the + // retry-handler can rotate past a 429'd provider without flipping to a + // different model family. If all three routes exhaust, tasks running on + // the main model fail (no cross-model fallback). Loaded from + // `./data/primary-provider-chain.json` so the list is editable without + // touching code. + chainsByName[MAIN_CHAIN_NAME] = primaryProviderChainEntries; + + // Step 4: warn if a project-level settings.json will shadow us. + const shadowInfo = detectProjectSettingsShadow(getCwd(), settingsPath, log); + const shadowWarning = Boolean(shadowInfo?.shadowsFallback); + + // Step 5: atomic write to the global settings.json. + writeSettingsWithChains(settingsPath, chainsByName); + return { + chainsWritten: Object.keys(chainsByName).length, + totalEntries, + shadowWarning, + }; +} diff --git a/src/resources/extensions/gsd/learning/fallback-chain-writer.test.mjs b/src/resources/extensions/gsd/learning/fallback-chain-writer.test.mjs new file mode 100644 index 000000000..0f1bc6415 --- /dev/null +++ b/src/resources/extensions/gsd/learning/fallback-chain-writer.test.mjs @@ -0,0 +1,402 @@ +/** + * Tests for the fallback-chain-writer module. + * + * Focuses on the three findings surfaced by the combatant review: + * #1 — the removed BLACKOUT_PRIORITY_OFFSET reference (regression test) + * #3 — the generalist `default` chain should average across unit types, + * not clone the `subagent` ranking + * #4 — project-level settings.json with a `fallback` block must surface + * a warning via deps.opts.log + * + * @module gsd-learning/fallback-chain-writer.test + */ + +import test from "node:test"; +import assert from "node:assert/strict"; +import { mkdtempSync, writeFileSync, readFileSync, rmSync, mkdirSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { writeFallbackChains } from "./fallback-chain-writer.mjs"; + +function makeTempSettingsDir() { + const dir = mkdtempSync(join(tmpdir(), "gsd-chain-writer-")); + const settingsPath = join(dir, "settings.json"); + writeFileSync(settingsPath, JSON.stringify({ enabledModels: [] }, null, 2)); + return { dir, settingsPath }; +} + +function makeDeps({ weights = { planning: { reasoning: 1.0 } }, overrides = {}, log = null } = {}) { + return { + db: { prepare: () => ({ all: () => [], get: () => undefined }) }, + overrides, + weights, + benchmarks: {}, + opts: { + nPrior: 10, + ucbC: 1.4, + rollingDays: 30, + explorationEnabled: false, + log, + }, + }; +} + +test("writeFallbackChains produces entries with integer priorities (no undefined BLACKOUT_PRIORITY_OFFSET)", () => { + const { dir, settingsPath } = makeTempSettingsDir(); + try { + const overrides = { + "kimi-coding/k2p5": { reasoning: 90 }, + "minimax/MiniMax-M2.7": { reasoning: 80 }, + "zai/glm-5.1": { reasoning: 70 }, + }; + const deps = makeDeps({ overrides }); + + const result = writeFallbackChains(settingsPath, deps); + assert.ok(result.chainsWritten >= 1, "at least one chain written"); + assert.ok(result.totalEntries >= 3, "all overrides represented"); + + const written = JSON.parse(readFileSync(settingsPath, "utf8")); + const planningChain = written.fallback.chains.planning; + assert.ok(Array.isArray(planningChain), "planning chain present"); + + for (const entry of planningChain) { + assert.equal(typeof entry.priority, "number"); + assert.ok(Number.isFinite(entry.priority), `priority ${entry.priority} is finite`); + assert.ok(entry.priority >= 0, `priority ${entry.priority} >= 0`); + assert.ok(entry.priority < 1000, `priority ${entry.priority} < 1000 (no leftover blackout offset)`); + } + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}); + +test("writeFallbackChains builds a generalist default chain averaged across unit types, not cloned from subagent", () => { + const { dir, settingsPath } = makeTempSettingsDir(); + try { + // Three unit types, three disjoint benchmark keys. + // Model A dominates the planning benchmark but is weak elsewhere. + // Model B is middling everywhere. + // Per unit-type subagent score: A=10, B=80 → subagent chain favors B + // Mean across unit types: A≈40, B≈70 → default chain favors B + const overrides = { + "providerA/modelA": { + __benchmarks: { bench_p: 100, bench_e: 10, bench_s: 10 }, + }, + "providerB/modelB": { + __benchmarks: { bench_p: 50, bench_e: 80, bench_s: 80 }, + }, + }; + const deps = makeDeps({ + weights: { + planning: { bench_p: 1.0 }, + execution: { bench_e: 1.0 }, + subagent: { bench_s: 1.0 }, + }, + overrides, + }); + + writeFallbackChains(settingsPath, deps); + const written = JSON.parse(readFileSync(settingsPath, "utf8")); + const defaultChain = written.fallback.chains.default; + const planningChain = written.fallback.chains.planning; + const subagentChain = written.fallback.chains.subagent; + + assert.ok(Array.isArray(defaultChain)); + assert.ok(Array.isArray(planningChain)); + assert.ok(Array.isArray(subagentChain)); + + // Planning chain — modelA should win (score 100 > 50) + assert.equal(planningChain[0].model, "modelA", "planning chain: modelA wins (100 vs 50)"); + + // Subagent chain — modelB should win (score 80 > 10) + assert.equal(subagentChain[0].model, "modelB", "subagent chain: modelB wins (80 vs 10)"); + + // Default chain — modelB wins by mean (≈70 vs ≈40) + // This is the key regression test: a subagent-cloned default would also + // favor modelB here, but it would be identical to subagentChain. Instead + // we should see the generalist aggregation treat the chains independently. + assert.equal(defaultChain[0].model, "modelB", "default chain: modelB wins by cross-unit mean"); + + // Regression: default chain is NOT a literal clone of subagent. + // If it were cloned (old behavior), the priorities would match exactly. + // Generalist aggregation builds from scratch, so priorities are computed + // independently — identity comparison proves no clone. + assert.notEqual(defaultChain, subagentChain, "default is not a reference-identical clone of subagent"); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}); + +test("writeFallbackChains expands bare-id benchmark keys into concrete (provider, model) pairs via enabledModels reverse index", () => { + // Regression for the "wrote 0 fallback chain(s)" bug. + // + // model-benchmarks.json uses bare ids (e.g. "glm-5", "k2p5"). Before the + // fix, rankedToEntries skipped anything without a slash, so every chain + // came out empty and the plugin silently wrote {chainsWritten: 0}. + // + // The fix reads `enabledModels` from settings.json, builds a + // { bareId → [{provider, model}, ...] } reverse lookup, and emits one + // chain entry per provider that offers each bare id. + const { dir, settingsPath } = makeTempSettingsDir(); + try { + // Seed settings.json with enabledModels in pi-ai's canonical format. + writeFileSync( + settingsPath, + JSON.stringify( + { + enabledModels: [ + "kimi-coding/k2p5", + "opencode-go/k2p5", + "ollama-cloud/kimi-k2.5:cloud", + "zai/glm-5", + "ollama-cloud/glm-5:cloud", + ], + }, + null, + 2, + ), + ); + + // Bare-id overrides as they appear in model-benchmarks.json. + // `kimi-k2.5` exercises the `:cloud` stripped-suffix match. + const overrides = { + "k2p5": { __benchmarks: { bench_p: 90 } }, + "glm-5": { __benchmarks: { bench_p: 80 } }, + "kimi-k2.5": { __benchmarks: { bench_p: 75 } }, + }; + const deps = makeDeps({ + weights: { planning: { bench_p: 1.0 } }, + overrides, + }); + + const result = writeFallbackChains(settingsPath, deps); + assert.ok(result.chainsWritten > 0, "at least one chain written"); + assert.ok(result.totalEntries > 0, "entries materialized"); + + const written = JSON.parse(readFileSync(settingsPath, "utf8")); + const planningChain = written.fallback.chains.planning; + assert.ok(Array.isArray(planningChain), "planning chain present"); + + const providerModelPairs = planningChain.map((e) => `${e.provider}/${e.model}`); + + // k2p5 should expand to kimi-coding/k2p5 AND opencode-go/k2p5 + assert.ok(providerModelPairs.includes("kimi-coding/k2p5"), "kimi-coding/k2p5 present"); + assert.ok(providerModelPairs.includes("opencode-go/k2p5"), "opencode-go/k2p5 present"); + + // glm-5 should expand to zai/glm-5 AND ollama-cloud/glm-5:cloud + assert.ok(providerModelPairs.includes("zai/glm-5"), "zai/glm-5 present"); + assert.ok(providerModelPairs.includes("ollama-cloud/glm-5:cloud"), "ollama-cloud/glm-5:cloud present via suffix-strip match"); + + // kimi-k2.5 (benchmark key) → ollama-cloud/kimi-k2.5:cloud via the + // :cloud stripping branch + assert.ok( + providerModelPairs.includes("ollama-cloud/kimi-k2.5:cloud"), + "kimi-k2.5 benchmark id expanded to ollama-cloud/kimi-k2.5:cloud", + ); + + // Priorities should be sortable and all integer + for (const entry of planningChain) { + assert.ok(Number.isInteger(entry.priority), `priority ${entry.priority} is int`); + assert.ok(entry.priority >= 0); + } + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}); + +test("writeFallbackChains logs a warning when enabledModels is missing or empty", () => { + const { dir, settingsPath } = makeTempSettingsDir(); + try { + // settings.json with no enabledModels key at all + writeFileSync(settingsPath, JSON.stringify({ defaultProvider: "kimi-coding" })); + const warnings = []; + const deps = makeDeps({ + overrides: { "k2p5": { __benchmarks: { bench_p: 90 } } }, + weights: { planning: { bench_p: 1.0 } }, + log: (msg) => warnings.push(msg), + }); + + writeFallbackChains(settingsPath, deps); + + const matched = warnings.some( + (w) => w.includes("enabledModels") && w.includes("empty or unparseable"), + ); + assert.ok(matched, `expected empty-enabledModels warning, got: ${JSON.stringify(warnings)}`); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}); + +test("writeFallbackChains warns via log when project-level .gsd/agent/settings.json shadows fallback", () => { + // Create a fake project cwd with a .gsd/agent/settings.json containing a fallback block. + const projectDir = mkdtempSync(join(tmpdir(), "gsd-proj-")); + const projectSettingsDir = join(projectDir, ".gsd", "agent"); + mkdirSync(projectSettingsDir, { recursive: true }); + const projectSettingsPath = join(projectSettingsDir, "settings.json"); + writeFileSync(projectSettingsPath, JSON.stringify({ fallback: { enabled: true, chains: {} } })); + + const { dir: globalDir, settingsPath: globalSettingsPath } = makeTempSettingsDir(); + + const originalCwd = process.cwd(); + process.chdir(projectDir); + const warnings = []; + try { + const deps = makeDeps({ + overrides: { "kimi-coding/k2p5": { reasoning: 90 } }, + log: (msg) => warnings.push(msg), + }); + const result = writeFallbackChains(globalSettingsPath, deps); + assert.equal(result.shadowWarning, true, "shadowWarning flag set"); + const matched = warnings.some((w) => w.includes("settings.json") && w.includes("fallback")); + assert.ok(matched, `expected a shadow warning in log, got: ${JSON.stringify(warnings)}`); + } finally { + process.chdir(originalCwd); + rmSync(projectDir, { recursive: true, force: true }); + rmSync(globalDir, { recursive: true, force: true }); + } +}); + +test("writeFallbackChains always emits the hardcoded main chain with three kimi-k2.5 provider routes", () => { + const { dir, settingsPath } = makeTempSettingsDir(); + try { + // Deps deliberately minimal — no overrides, no enabledModels — so + // the blender-driven chains are empty. The hardcoded main chain must + // still appear regardless of blender state. + const deps = makeDeps(); + writeFallbackChains(settingsPath, deps); + + const written = JSON.parse(readFileSync(settingsPath, "utf8")); + const mainChain = written.fallback.chains.main; + + assert.ok(Array.isArray(mainChain), "main chain present"); + assert.equal(mainChain.length, 3, "main chain has exactly 3 entries"); + + assert.equal(mainChain[0].provider, "kimi-coding"); + assert.equal(mainChain[0].model, "k2p5"); + assert.equal(mainChain[0].priority, 0); + + assert.equal(mainChain[1].provider, "ollama-cloud"); + assert.equal(mainChain[1].model, "kimi-k2.5:cloud"); + assert.equal(mainChain[1].priority, 1); + + assert.equal(mainChain[2].provider, "opencode-go"); + assert.equal(mainChain[2].model, "kimi-k2.5"); + assert.equal(mainChain[2].priority, 2); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}); + +test("hardcoded main chain coexists with blender-computed per-unit-type chains", () => { + const { dir, settingsPath } = makeTempSettingsDir(); + try { + // Seed enabledModels so the blender can materialize real chains. + writeFileSync( + settingsPath, + JSON.stringify( + { + enabledModels: ["kimi-coding/k2p5", "zai/glm-5"], + }, + null, + 2, + ), + ); + const overrides = { + "k2p5": { __benchmarks: { bench_p: 90 } }, + "glm-5": { __benchmarks: { bench_p: 80 } }, + }; + const deps = makeDeps({ + weights: { planning: { bench_p: 1.0 } }, + overrides, + }); + + writeFallbackChains(settingsPath, deps); + + const written = JSON.parse(readFileSync(settingsPath, "utf8")); + const chains = written.fallback.chains; + + // Hardcoded main chain present + assert.ok(Array.isArray(chains.main), "main chain present"); + assert.equal(chains.main.length, 3); + + // Blender-computed per-unit-type chain also present + assert.ok(Array.isArray(chains.planning), "planning chain present"); + assert.ok(chains.planning.length > 0, "planning chain has entries"); + + // Both coexist — main does not clobber blender output + const chainNames = Object.keys(chains); + assert.ok(chainNames.includes("main"), "main in chain names"); + assert.ok(chainNames.includes("planning"), "planning in chain names"); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}); + +test("writeFallbackChains does NOT warn when cwd is the parent of the global settings file (false-positive guard)", () => { + // Regression: when gsd is invoked from $HOME, detectProjectSettingsShadow + // used to probe `$HOME/.gsd/agent/settings.json` — which IS the global + // settings file itself. It then warned that the global file was shadowing + // its own write. Surfaced 2026-04-15 in notifications.jsonl as + // "WARNING: project-level settings.json at /home/mhugo/.gsd/agent/settings.json". + // + // Fix: detectProjectSettingsShadow compares the resolved project path to + // the global settingsPath and bails early when they match. + const fakeHome = mkdtempSync(join(tmpdir(), "gsd-fakehome-")); + const globalSettingsDir = join(fakeHome, ".gsd", "agent"); + mkdirSync(globalSettingsDir, { recursive: true }); + const globalSettingsPath = join(globalSettingsDir, "settings.json"); + writeFileSync( + globalSettingsPath, + JSON.stringify({ + enabledModels: ["kimi-coding/k2p5"], + fallback: { enabled: true, chains: {} }, + }), + ); + + const originalCwd = process.cwd(); + process.chdir(fakeHome); + const warnings = []; + try { + const deps = makeDeps({ + overrides: { "kimi-coding/k2p5": { reasoning: 90 } }, + log: (msg) => warnings.push(msg), + }); + const result = writeFallbackChains(globalSettingsPath, deps); + assert.equal( + result.shadowWarning, + false, + "cwd pointing at the global settings parent must not fire a shadow warning", + ); + const matched = warnings.some((w) => w.includes("project-level settings.json")); + assert.ok(!matched, `unexpected shadow warning: ${JSON.stringify(warnings)}`); + } finally { + process.chdir(originalCwd); + rmSync(fakeHome, { recursive: true, force: true }); + } +}); + +test("writeFallbackChains does NOT warn when project settings has no fallback block", () => { + const projectDir = mkdtempSync(join(tmpdir(), "gsd-proj-")); + const projectSettingsDir = join(projectDir, ".gsd", "agent"); + mkdirSync(projectSettingsDir, { recursive: true }); + writeFileSync(join(projectSettingsDir, "settings.json"), JSON.stringify({ defaultProvider: "kimi-coding" })); + + const { dir: globalDir, settingsPath: globalSettingsPath } = makeTempSettingsDir(); + + const originalCwd = process.cwd(); + process.chdir(projectDir); + const warnings = []; + try { + const deps = makeDeps({ + overrides: { "kimi-coding/k2p5": { reasoning: 90 } }, + log: (msg) => warnings.push(msg), + }); + const result = writeFallbackChains(globalSettingsPath, deps); + assert.equal(result.shadowWarning, false, "no shadow warning when fallback block absent"); + } finally { + process.chdir(originalCwd); + rmSync(projectDir, { recursive: true, force: true }); + rmSync(globalDir, { recursive: true, force: true }); + } +}); diff --git a/src/resources/extensions/gsd/learning/hook-handler.mjs b/src/resources/extensions/gsd/learning/hook-handler.mjs new file mode 100644 index 000000000..1f4a1c24d --- /dev/null +++ b/src/resources/extensions/gsd/learning/hook-handler.mjs @@ -0,0 +1,278 @@ +/** + * gsd-learning: before_model_select hook handler + * + * Called by gsd's auto-model-selection.js (line 121-141) before capability + * scoring runs. If we return {modelId}, it overrides pi-ai's own dispatch + * path — our Bayesian-blended ranking wins. + * + * ## Responsibilities + * - Translate a `before_model_select` hook payload into a Bayesian-blended + * ranking over the eligible models for the unit type + * - Decide whether to override (return {modelId}) or fall through (return + * undefined) so pi-ai's existing capability scoring still runs as fallback + * - Never crash gsd's dispatch path: any internal error is caught, logged, + * and translated into a fallthrough + * + * ## Fallthrough semantics + * Return `undefined` whenever we lack the confidence (or the configuration) + * to override the default path. Concretely: + * - fewer than 2 eligible models (nothing to rank) + * - no weight config for this unit type (we'd be guessing) + * - any thrown error inside the handler (defensive) + * + * In all of those cases pi-ai's existing capability scoring runs unmodified. + * + * ## Dependencies + * - outcome-aggregator: rolling-window observed stats per (unit_type, model) + * - bayesian-blender: pure ranking math + * - loadCapabilityOverrides: per-(unit_type, model) prior score from benchmarks + * + * ## Side effects + * - None on the database (read-only path). May call `deps.opts.log` once per + * invocation if a logger is supplied. + * + * @module gsd-learning/hook-handler + */ + +import { aggregateAllForUnitType } from "./outcome-aggregator.mjs"; +import { blendedRanking } from "./bayesian-blender.mjs"; +import { computeUnitTypeScore } from "./loadCapabilityOverrides.mjs"; + +const HOOK_EVENT_NAME = "before_model_select"; +const MIN_ELIGIBLE_FOR_RANKING = 2; +const NEUTRAL_PRIOR_SCORE = 50; +const TOP_RANKED_INDEX = 0; + +/** + * @typedef {Object} HookDeps + * @property {Object} db - duck-typed SQLite db handle + * @property {Object} overrides - from loadCapabilityOverrides().overrides + * @property {Object} weights - from loadCapabilityOverrides().weights + * @property {Object} benchmarks - from loadCapabilityOverrides().benchmarks + * @property {Object} [opts] + * @property {number} [opts.nPrior=10] + * @property {number} [opts.ucbC=1.4] + * @property {number} [opts.rollingDays=30] + * @property {boolean} [opts.explorationEnabled=true] + * @property {(msg: string) => void} [opts.log] + */ + +/** + * @typedef {Object} HookInput + * @property {string} unitType - e.g. "execute-task" + * @property {string[]} eligibleModels - candidate model ids + * @property {Object} [phaseConfig] - per-phase configuration; .primary may bound the tier + */ + +/** + * Build the priors-by-model map used by `blendedRanking`. Falls back to a + * neutral score (50) when the model has no overlap with the unit-type weights. + * + * @param {string[]} eligibleModels + * @param {string} unitType + * @param {Object} overrides + * @param {Object} weights + * @returns {Object} { modelId: priorScore } + */ +function buildPriorsByModel(eligibleModels, unitType, overrides, weights) { + const priors = {}; + for (const modelId of eligibleModels) { + const score = computeUnitTypeScore(modelId, unitType, overrides, weights); + // computeUnitTypeScore returns 0 when there's no benchmark/weight overlap. + // Treat "no signal" as neutral (50) so a model without coverage isn't + // unfairly buried below ones that do — the blender will lean on + // observed evidence as samples accumulate. + priors[modelId] = score > 0 ? score : NEUTRAL_PRIOR_SCORE; + } + return priors; +} + +/** + * Convert the Map returned by `aggregateAllForUnitType` into the plain + * object shape `blendedRanking` expects. + * + * @param {Map} statsMap + * @returns {Object} { modelId: AggregatedStats } + */ +function statsMapToObject(statsMap) { + const obj = {}; + if (!statsMap || typeof statsMap.entries !== "function") { + return obj; + } + for (const [modelId, stats] of statsMap.entries()) { + obj[modelId] = stats; + } + return obj; +} + +/** + * Safely invoke an optional logger. A bad logger must not break the hook. + * + * @param {(msg: string) => void} [log] + * @param {string} message + */ +function safeLog(log, message) { + if (typeof log !== "function") return; + try { + log(message); + } catch (_err) { + // intentionally swallowed — logging must never break dispatch + } +} + +/** + * Format the blended ranking decision for log output. + * + * @param {Array<{modelId: string, finalScore: number}>} ranked + * @param {string} unitType + * @returns {string} + */ +function formatDecisionLog(ranked, unitType) { + if (ranked.length === 0) { + return `[gsd-learning] ${unitType}: no eligible models after ranking`; + } + const winner = ranked[TOP_RANKED_INDEX]; + const runnerUp = ranked[1]; + const summary = ranked + .slice(0, 5) + .map((entry) => `${entry.modelId}=${entry.finalScore.toFixed(1)}`) + .join(", "); + if (runnerUp) { + return `[gsd-learning] ${unitType}: blend picked ${winner.modelId} over ${runnerUp.modelId} (${summary})`; + } + return `[gsd-learning] ${unitType}: blend picked ${winner.modelId} (${summary})`; +} + +/** + * Create a handler function to register on pi.emitBeforeModelSelect. + * + * @param {HookDeps} deps + * @returns {(hookInput: HookInput) => Promise<{modelId: string} | undefined>} + */ +export function createBeforeModelSelectHandler(deps) { + const opts = deps?.opts ?? {}; + const rollingDays = opts.rollingDays; + const nPrior = opts.nPrior; + const ucbC = opts.ucbC; + const explorationEnabled = opts.explorationEnabled; + const log = opts.log; + + return async function beforeModelSelectHandler(hookInput) { + try { + if (!hookInput || typeof hookInput !== "object") { + return undefined; + } + + const { unitType, eligibleModels } = hookInput; + + if (typeof unitType !== "string" || unitType.length === 0) { + return undefined; + } + if (!Array.isArray(eligibleModels) || eligibleModels.length < MIN_ELIGIBLE_FOR_RANKING) { + // Single (or zero) candidate — nothing to rank, fall through. + return undefined; + } + + const weights = deps?.weights ?? {}; + if (!weights[unitType]) { + // No weight config for this unit type → we'd be ranking blindly. + // Fall through to pi-ai's capability path instead of guessing. + return undefined; + } + + const overrides = deps?.overrides ?? {}; + const priorsByModel = buildPriorsByModel(eligibleModels, unitType, overrides, weights); + + const observedStatsMap = aggregateAllForUnitType(deps.db, unitType, { + rollingDays, + }); + const observedByModel = statsMapToObject(observedStatsMap); + + const ranked = blendedRanking(eligibleModels, unitType, priorsByModel, observedByModel, { + nPrior, + ucbC, + explorationEnabled, + }); + + if (ranked.length === 0) { + return undefined; + } + + safeLog(log, formatDecisionLog(ranked, unitType)); + + const winner = ranked[TOP_RANKED_INDEX]; + return { modelId: winner.modelId }; + } catch (err) { + safeLog( + log, + `[gsd-learning] hook handler error (falling through): ${err?.message ?? String(err)}`, + ); + return undefined; + } + }; +} + +/** + * Register the handler with pi. Returns an unregister function. + * + * pi-ai's exact API for `before_model_select` varies across versions. We + * support three common shapes via feature detection, in priority order: + * + * 1. EventEmitter-style: pi.on("before_model_select", handler) + * → unregister via pi.off(...) / pi.removeListener(...) + * + * 2. Hook registry method: pi.registerHook("before_model_select", handler) + * → unregister via pi.unregisterHook(...) when present + * + * 3. Hook list property: pi.hooks.beforeModelSelect.push(handler) + * → unregister by splicing the array + * + * If pi exposes none of these we throw a clear error so the caller knows + * to upgrade pi-ai or pin to a compatible version. + * + * @param {Object} pi - pi-ai instance with before_model_select hook support + * @param {HookDeps} deps + * @returns {() => void} unregister + */ +export function registerBeforeModelSelect(pi, deps) { + if (!pi || typeof pi !== "object") { + throw new Error("registerBeforeModelSelect: pi instance is required"); + } + const handler = createBeforeModelSelectHandler(deps); + + // Shape 1: EventEmitter-style on/off + if (typeof pi.on === "function") { + pi.on(HOOK_EVENT_NAME, handler); + return () => { + if (typeof pi.off === "function") { + pi.off(HOOK_EVENT_NAME, handler); + } else if (typeof pi.removeListener === "function") { + pi.removeListener(HOOK_EVENT_NAME, handler); + } + }; + } + + // Shape 2: explicit hook registry + if (typeof pi.registerHook === "function") { + pi.registerHook(HOOK_EVENT_NAME, handler); + return () => { + if (typeof pi.unregisterHook === "function") { + pi.unregisterHook(HOOK_EVENT_NAME, handler); + } + }; + } + + // Shape 3: hooks property holding a list + if (pi.hooks && Array.isArray(pi.hooks.beforeModelSelect)) { + pi.hooks.beforeModelSelect.push(handler); + return () => { + const list = pi.hooks.beforeModelSelect; + const idx = list.indexOf(handler); + if (idx >= 0) list.splice(idx, 1); + }; + } + + throw new Error( + "pi-ai does not expose a before_model_select hook registration API compatible with this plugin — please check pi version (expected pi.on / pi.registerHook / pi.hooks.beforeModelSelect)", + ); +} diff --git a/src/resources/extensions/gsd/learning/hook-handler.test.mjs b/src/resources/extensions/gsd/learning/hook-handler.test.mjs new file mode 100644 index 000000000..8079340a0 --- /dev/null +++ b/src/resources/extensions/gsd/learning/hook-handler.test.mjs @@ -0,0 +1,346 @@ +/** + * Tests for hook-handler.mjs (Slice S04). + * + * Run with: node --test src/hook-handler.test.mjs + * + * Two layers, per the S04 contract: + * 1. selectModel() — pure ranking, no pi / no db, mocks for everything + * 2. registerRoutingHook() — fake pi instance, fires a simulated event + * + * ## Skip semantics + * Sibling slices S01/S02/S03 land in parallel. Until they all commit and + * hook-handler.mjs is updated to export `selectModel` and `registerRoutingHook` + * per the S04 contract, this test file detects the missing exports and skips + * every case rather than failing. The tests are still implemented in full so + * they activate the moment the spec exports land. + * + * ## Score domain + * S01's prior scores and S03's blended ranking work in [0, 100] in the current + * sibling layout. Test fixtures use that scale. + * + * ## Observed stats shape + * Per S02 `aggregateAllForUnitType`, observedStatsMap is `Map` + * (or a plain `{modelId: stats}`). Stats use snake_case keys: sample_count, + * success_rate, avg_retries, verification_pass_rate, blocker_rate. + */ + +import { test } from "node:test"; +import assert from "node:assert/strict"; + +let hookHandlerModule = null; +let importError = null; +try { + hookHandlerModule = await import("./hook-handler.mjs"); +} catch (err) { + importError = err; +} + +// The S04 contract requires `selectModel` and `registerRoutingHook` exports. +// If hook-handler.mjs is still on the older `createBeforeModelSelectHandler` +// API (sibling slices landing in parallel), those names are absent and we +// skip every test rather than fail the suite. Same skip path covers the +// case where the import itself failed because S01/S02/S03 are missing. +const moduleReady = + hookHandlerModule !== null && + typeof hookHandlerModule.selectModel === "function" && + typeof hookHandlerModule.registerRoutingHook === "function"; + +const SKIP_REASON = importError + ? `waiting on sibling slices: ${importError.message}` + : "waiting on sibling slices: hook-handler.mjs does not yet export selectModel/registerRoutingHook"; + +const UNIT_TYPE = "execute-task"; + +/** + * Build an AggregatedStats record matching S02's snake_case shape. + * + * @param {string} modelId + * @param {Object} overrides + */ +function makeStats(modelId, overrides = {}) { + return { + modelId, + unitType: UNIT_TYPE, + sample_count: 50, + success_rate: 0.5, + avg_retries: 0.5, + verification_pass_rate: null, + blocker_rate: 0, + escalation_rate: 0, + avg_duration_ms: 1000, + avg_tokens: 1000, + avg_cost_usd: 0.01, + window_days: 30, + ...overrides, + }; +} + +// --------------------------------------------------------------------------- +// selectModel() — pure function tests +// --------------------------------------------------------------------------- + +test("selectModel: empty eligibleModels returns undefined", (t) => { + if (!moduleReady) return; + const { selectModel } = hookHandlerModule; + const result = selectModel({ + unitType: UNIT_TYPE, + eligibleModels: [], + priors: {}, + observedStatsMap: {}, + }); + assert.equal(result, undefined); +}); + +test("selectModel: only one eligible returns that one", (t) => { + if (!moduleReady) return; + const { selectModel } = hookHandlerModule; + const result = selectModel({ + unitType: UNIT_TYPE, + eligibleModels: ["solo-model"], + priors: {}, + observedStatsMap: {}, + }); + assert.equal(result, "solo-model"); +}); + +test("selectModel: priors only, no observed → ranks by prior score", (t) => { + if (!moduleReady) return; + const { selectModel } = hookHandlerModule; + const result = selectModel({ + unitType: UNIT_TYPE, + eligibleModels: ["weak-model", "strong-model", "mid-model"], + priors: { + "weak-model": 30, + "strong-model": 90, + "mid-model": 60, + }, + observedStatsMap: {}, + explorationWeight: 0, // disable UCB so the prior dominates deterministically + }); + assert.equal(result, "strong-model"); +}); + +test("selectModel: observed only, no priors → ranks by observed", (t) => { + if (!moduleReady) return; + const { selectModel } = hookHandlerModule; + const observed = { + "model-a": makeStats("model-a", { + sample_count: 50, + success_rate: 0.95, + avg_retries: 0.2, + verification_pass_rate: 0.95, + blocker_rate: 0.0, + }), + "model-b": makeStats("model-b", { + sample_count: 50, + success_rate: 0.20, + avg_retries: 3.0, + verification_pass_rate: 0.20, + blocker_rate: 0.30, + }), + }; + const result = selectModel({ + unitType: UNIT_TYPE, + eligibleModels: ["model-a", "model-b"], + priors: {}, + observedStatsMap: observed, + explorationWeight: 0, + }); + assert.equal(result, "model-a"); +}); + +test("selectModel: priors + observed → blended ordering favours observed at high N", (t) => { + if (!moduleReady) return; + const { selectModel } = hookHandlerModule; + // model-a: high prior (95) but terrible observed track record at N=200 + // model-b: low prior (25) but excellent observed track record at N=200 + // alpha = 10 / 210 ≈ 0.048 → observed dominates → model-b wins. + const observed = { + "model-a": makeStats("model-a", { + sample_count: 200, + success_rate: 0.10, + avg_retries: 4.5, + verification_pass_rate: 0.10, + blocker_rate: 0.30, + }), + "model-b": makeStats("model-b", { + sample_count: 200, + success_rate: 0.95, + avg_retries: 0.10, + verification_pass_rate: 0.95, + blocker_rate: 0.00, + }), + }; + const result = selectModel({ + unitType: UNIT_TYPE, + eligibleModels: ["model-a", "model-b"], + priors: { "model-a": 95, "model-b": 25 }, + observedStatsMap: observed, + explorationWeight: 0, + }); + assert.equal(result, "model-b"); +}); + +test("selectModel: priors + observed at cold start → prior dominates", (t) => { + if (!moduleReady) return; + const { selectModel } = hookHandlerModule; + // No observed samples → blend reduces to pure prior. model-a wins. + const result = selectModel({ + unitType: UNIT_TYPE, + eligibleModels: ["model-a", "model-b"], + priors: { "model-a": 90, "model-b": 30 }, + observedStatsMap: {}, + explorationWeight: 0, + }); + assert.equal(result, "model-a"); +}); + +test("selectModel: all scores null → returns first eligible unchanged", (t) => { + if (!moduleReady) return; + const { selectModel } = hookHandlerModule; + // No priors, no observed, no exploration. selectModel should detect + // "no signal" and return the first eligible (upstream choice unchanged). + const result = selectModel({ + unitType: UNIT_TYPE, + eligibleModels: ["alpha", "beta", "gamma"], + priors: {}, + observedStatsMap: {}, + explorationWeight: 0, + }); + assert.equal(result, "alpha"); +}); + +test("selectModel: observedStatsMap as Map (not plain object) is accepted", (t) => { + if (!moduleReady) return; + const { selectModel } = hookHandlerModule; + const observed = new Map(); + observed.set("model-a", makeStats("model-a", { sample_count: 100, success_rate: 0.9 })); + observed.set("model-b", makeStats("model-b", { sample_count: 100, success_rate: 0.3 })); + const result = selectModel({ + unitType: UNIT_TYPE, + eligibleModels: ["model-a", "model-b"], + priors: {}, + observedStatsMap: observed, + explorationWeight: 0, + }); + assert.equal(result, "model-a"); +}); + +// --------------------------------------------------------------------------- +// registerRoutingHook() — integration test against a fake pi +// --------------------------------------------------------------------------- + +/** + * Build a minimal fake pi-coding-agent ExtensionAPI: + * - on(event, handler) — record handlers per event name + * - notify(message, type) — record notifications + * - log.{info,warn,error} — record log calls + * - registerCommand(name, opt) — record commands + */ +function makeFakePi() { + const handlers = new Map(); + const notifications = []; + const logs = []; + const commands = new Map(); + + return { + handlers, + notifications, + logs, + commands, + + on(event, handler) { + if (!handlers.has(event)) handlers.set(event, []); + handlers.get(event).push(handler); + }, + notify(message, type) { + notifications.push({ message, type }); + }, + log: { + info: (m) => logs.push({ level: "info", message: m }), + warn: (m) => logs.push({ level: "warn", message: m }), + error: (m) => logs.push({ level: "error", message: m }), + }, + registerCommand(name, options) { + commands.set(name, options); + }, + }; +} + +test("registerRoutingHook: registers handler + reload command and routes a simulated event", async (t) => { + if (!moduleReady) return; + const { registerRoutingHook } = hookHandlerModule; + + const pi = makeFakePi(); + // Route the DB to a non-existent path so the lazy open returns null and + // the handler runs in priors-only mode (no better-sqlite3 dependency). + registerRoutingHook(pi, { + dbPath: "/tmp/gsd-learning-test-nonexistent.db", + notify: true, + explorationWeight: 0, + }); + + // The handler must be registered on before_model_select. + const handlers = pi.handlers.get("before_model_select"); + assert.ok(Array.isArray(handlers) && handlers.length === 1, "one before_model_select handler should be registered"); + + // The reload command should be registered if pi exposes registerCommand. + assert.ok(pi.commands.has("gsd-learning-reload"), "gsd-learning-reload command should be registered"); + const reloadCommand = pi.commands.get("gsd-learning-reload"); + assert.equal(typeof reloadCommand.handler, "function"); + + // Fire a simulated event with a unit type that S01 priors ought to cover. + const event = { + type: "before_model_select", + unitType: "execute-task", + unitId: "test-unit-1", + classification: { tier: "primary", reason: "test", downgraded: false }, + eligibleModels: ["kimi-coding/k2p5", "minimax/MiniMax-M2.7"], + phaseConfig: { primary: "kimi-coding/k2p5", fallbacks: [] }, + }; + const ctx = { ui: { notify: () => {} }, hasUI: false }; + + const handler = handlers[0]; + const result = await handler(event, ctx); + + // Two acceptable outcomes: + // (a) priors loaded → handler returns {modelId} for one of the eligibles + // and exactly one notification was fired + // (b) priors absent / no overlap → handler returns undefined (graceful + // fall-through). Either is correct per the contract. + if (result !== undefined) { + assert.ok(typeof result === "object" && typeof result.modelId === "string", "result must be {modelId}"); + assert.ok(event.eligibleModels.includes(result.modelId), "selected model must be one of the eligibles"); + assert.equal(pi.notifications.length, 1, "exactly one notification should have fired"); + assert.match(pi.notifications[0].message, /\[gsd-learning\] picked /); + } +}); + +test("registerRoutingHook: malformed events fall through to undefined and never throw", async (t) => { + if (!moduleReady) return; + const { registerRoutingHook } = hookHandlerModule; + + const pi = makeFakePi(); + registerRoutingHook(pi, { + dbPath: "/tmp/gsd-learning-test-nonexistent-2.db", + notify: false, + explorationWeight: 0, + }); + + const handler = pi.handlers.get("before_model_select")[0]; + + const r1 = await handler(null, {}); + assert.equal(r1, undefined); + + const r2 = await handler({ type: "before_model_select" }, {}); + assert.equal(r2, undefined); + + const r3 = await handler({ type: "before_model_select", unitType: "x", eligibleModels: [] }, {}); + assert.equal(r3, undefined); +}); + +test("registerRoutingHook: missing pi.on throws clearly", (t) => { + if (!moduleReady) return; + const { registerRoutingHook } = hookHandlerModule; + assert.throws(() => registerRoutingHook({}, {}), /pi\.on is not a function/); +}); diff --git a/src/resources/extensions/gsd/learning/index.mjs b/src/resources/extensions/gsd/learning/index.mjs new file mode 100644 index 000000000..4f5658e11 --- /dev/null +++ b/src/resources/extensions/gsd/learning/index.mjs @@ -0,0 +1,320 @@ +/** + * gsd-learning plugin — entry point + * + * Wires together the four S01-S04 modules into a single registerable plugin: + * + * loadCapabilityOverrides → priors (per (unit_type, model)) + * outcome-recorder → write llm_task_outcomes rows + * outcome-aggregator → rolling-window observed stats + * bayesian-blender → α · prior + (1-α) · observed + UCB1 + * hook-handler → translates the above into a before_model_select handler + * + * ## Usage + * + * import { init } from "./index.mjs"; + * const plugin = await init(pi, { + * dbPath: "~/.gsd/gsd-learning.db", + * priorsPath: "./src/data/model-benchmarks.json", + * weightsPath: "./src/data/unit-weights.json", + * nPrior: 10, + * rollingDays: 30, + * explorationC: 1.4, + * }); + * + * // plugin.recordOutcome({...}) on unit completion + * // plugin.unregister() on tear down + * + * ## Side effects + * - Opens (or creates) a SQLite database at the resolved dbPath + * - Bootstraps the schema if absent + * - Registers a hook on the supplied pi instance + * + * ## Errors + * - Init failures are wrapped with a stage label so callers can see where + * things broke ("loading priors", "opening db", "applying schema", + * "registering hook") + * - Once init succeeds, the running handler is fire-and-forget — it cannot + * crash the dispatch path + * + * @module gsd-learning + */ + +import { readFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { homedir } from "node:os"; + +import { loadCapabilityOverrides } from "./loadCapabilityOverrides.mjs"; +import { recordOutcome, ensureSchema } from "./outcome-recorder.mjs"; +import { aggregateAllForUnitType } from "./outcome-aggregator.mjs"; +import { + createBeforeModelSelectHandler, + registerBeforeModelSelect, +} from "./hook-handler.mjs"; +import { writeFallbackChains } from "./fallback-chain-writer.mjs"; + +const MODULE_DIRECTORY = dirname(fileURLToPath(import.meta.url)); +const SCHEMA_PATH = resolve(MODULE_DIRECTORY, "outcome-schema.sql"); +const DEFAULT_DB_PATH = "~/.gsd/gsd-learning.db"; +const DEFAULT_N_PRIOR = 10; +const DEFAULT_ROLLING_DAYS = 30; +const DEFAULT_EXPLORATION_C = 1.4; +const HOME_REGEX = /^~(?=$|\/)/; + +/** + * @typedef {Object} PluginConfig + * @property {string} [dbPath] - default: ~/.gsd/gsd-learning.db + * @property {string} [priorsPath] - default: /data/model-benchmarks.json + * @property {string} [weightsPath] - default: /data/unit-weights.json + * @property {number} [nPrior=10] + * @property {number} [rollingDays=30] + * @property {number} [explorationC=1.4] + * @property {boolean} [explorationEnabled=true] + * @property {Object} [db] - pre-opened db handle (overrides dbPath) + * @property {(msg: string) => void} [log] + */ + +/** + * @typedef {Object} PluginInstance + * @property {() => void} unregister + * @property {(outcome: Object) => boolean} recordOutcome + * @property {() => Promise} reloadPriors + * @property {Object} deps + */ + +/** + * Expand a leading `~` to the user's home directory. + * + * @param {string} path + * @returns {string} + */ +function expandPath(path) { + if (typeof path !== "string") return path; + return path.replace(HOME_REGEX, homedir()); +} + +/** + * Load the outcome-schema SQL file. Read once at init time; cheap. + * + * @returns {string} + */ +function loadSchemaSql() { + return readFileSync(SCHEMA_PATH, "utf8"); +} + +/** + * Detect whether we're running under Bun. better-sqlite3 is a Node native + * addon and Bun has not shipped compatibility yet (tracked upstream in + * https://github.com/oven-sh/bun/issues/4290), so under Bun we use the + * built-in `bun:sqlite` module instead — its Statement API (`prepare`, + * `run`, `get`, `all`, `exec`, `transaction`) is a drop-in superset of the + * surface this plugin consumes. + * + * @returns {boolean} + */ +function isBunRuntime() { + return typeof globalThis.Bun !== "undefined"; +} + +/** + * Dynamically import bun's built-in sqlite module. Only callable under Bun — + * the import specifier `bun:sqlite` throws under Node. + * + * @returns {Promise} + */ +async function tryImportBunSqlite() { + try { + const mod = await import("bun:sqlite"); + return mod.Database ?? mod.default ?? null; + } catch (_err) { + return null; + } +} + +/** + * Dynamically import better-sqlite3. Returns null if the package is not + * installed so we can produce a clear error rather than an opaque module + * resolution failure. + * + * @returns {Promise} the better-sqlite3 default export, or null + */ +async function tryImportBetterSqlite() { + try { + const mod = await import("better-sqlite3"); + return mod.default ?? mod; + } catch (_err) { + return null; + } +} + +/** + * Open a database handle, either from the caller-supplied one or by + * dynamically loading a sqlite binding. Prefers `bun:sqlite` when running + * under Bun (better-sqlite3 is a Node native addon that Bun can't load), + * and falls back to `better-sqlite3` everywhere else. + * + * @param {PluginConfig} config + * @returns {Promise} duck-typed sqlite handle + */ +async function openDatabase(config) { + if (config.db) { + return config.db; + } + + const dbPath = expandPath(config.dbPath ?? DEFAULT_DB_PATH); + + if (isBunRuntime()) { + const BunDatabase = await tryImportBunSqlite(); + if (!BunDatabase) { + throw new Error( + "gsd-learning is running under Bun but failed to import `bun:sqlite`. This module ships with Bun itself — if this fails the Bun install is broken.", + ); + } + return new BunDatabase(dbPath); + } + + const Database = await tryImportBetterSqlite(); + if (!Database) { + throw new Error( + "gsd-learning needs better-sqlite3 to open the outcomes database. Install it with `npm install better-sqlite3` or `bun add better-sqlite3`, or pass a pre-opened db handle via config.db.", + ); + } + + return new Database(dbPath); +} + +/** + * Build the dependency bundle the hook handler consumes. + * + * @param {Object} db + * @param {{overrides: Object, weights: Object, benchmarks: Object}} priors + * @param {PluginConfig} config + * @returns {import("./hook-handler.mjs").HookDeps} + */ +function buildHookDeps(db, priors, config) { + return { + db, + overrides: priors.overrides, + weights: priors.weights, + benchmarks: priors.benchmarks, + opts: { + nPrior: config.nPrior ?? DEFAULT_N_PRIOR, + ucbC: config.explorationC ?? DEFAULT_EXPLORATION_C, + rollingDays: config.rollingDays ?? DEFAULT_ROLLING_DAYS, + explorationEnabled: config.explorationEnabled !== false, + log: config.log, + }, + }; +} + +/** + * Wrap a thrown error with a stage label so callers can see which init + * step failed. + * + * @param {string} stage + * @param {unknown} err + * @returns {Error} + */ +function wrapInitError(stage, err) { + const message = err instanceof Error ? err.message : String(err); + const wrapped = new Error(`gsd-learning init failed at stage "${stage}": ${message}`); + if (err instanceof Error && err.stack) { + wrapped.stack = `${wrapped.message}\nCaused by: ${err.stack}`; + } + return wrapped; +} + +/** + * Initialize the plugin: load priors, open db, bootstrap schema, register hook. + * + * @param {Object} pi + * @param {PluginConfig} [config={}] + * @returns {Promise} + */ +export async function init(pi, config = {}) { + let priors; + try { + priors = await loadCapabilityOverrides({ + benchmarksPath: config.priorsPath, + weightsPath: config.weightsPath, + }); + } catch (err) { + throw wrapInitError("loading priors", err); + } + + let db; + try { + db = await openDatabase(config); + } catch (err) { + throw wrapInitError("opening db", err); + } + + try { + const schemaSql = loadSchemaSql(); + ensureSchema(db, schemaSql); + } catch (err) { + throw wrapInitError("applying schema", err); + } + + const deps = buildHookDeps(db, priors, config); + + let unregister; + try { + unregister = registerBeforeModelSelect(pi, deps); + } catch (err) { + throw wrapInitError("registering hook", err); + } + + // Regenerate pi-ai runtime fallback chains (read by FallbackResolver). + // Writes ~/.gsd/agent/settings.json → fallback.chains.* atomically. + // Failure is logged but never blocks plugin init — stale chains are + // still better than a broken plugin. + let fallbackWriteSummary = null; + if (config.fallbackSettingsPath && config.writeFallbackChains !== false) { + try { + fallbackWriteSummary = writeFallbackChains(config.fallbackSettingsPath, deps, { + blackoutModels: config.blackoutModels ?? [], + }); + config.log?.( + `wrote ${fallbackWriteSummary.chainsWritten} fallback chain(s) ` + + `(${fallbackWriteSummary.totalEntries} total entries) to ${config.fallbackSettingsPath}`, + ); + } catch (err) { + config.log?.(`fallback chain write failed (non-fatal): ${err?.message ?? String(err)}`); + } + } + + return { + unregister, + fallbackWriteSummary, + recordOutcome: (outcome) => recordOutcome(db, outcome), + reloadPriors: async () => { + const fresh = await loadCapabilityOverrides({ + benchmarksPath: config.priorsPath, + weightsPath: config.weightsPath, + }); + deps.overrides = fresh.overrides; + deps.weights = fresh.weights; + deps.benchmarks = fresh.benchmarks; + }, + deps, + }; +} + +/** + * Convenience: create a handler without registering it. Useful for tests + * and for users who want to wire the hook themselves. + * + * @param {import("./hook-handler.mjs").HookDeps} deps + * @returns {(hookInput: Object) => Promise<{modelId: string} | undefined>} + */ +export function createHandler(deps) { + return createBeforeModelSelectHandler(deps); +} + +export { + loadCapabilityOverrides, + recordOutcome, + aggregateAllForUnitType, + registerBeforeModelSelect, +}; diff --git a/src/resources/extensions/gsd/learning/integration.test.mjs b/src/resources/extensions/gsd/learning/integration.test.mjs new file mode 100644 index 000000000..998b09f5b --- /dev/null +++ b/src/resources/extensions/gsd/learning/integration.test.mjs @@ -0,0 +1,367 @@ +/** + * gsd-learning integration test. + * + * Exercises the full blend pipeline: + * 1. Prior: model A scores higher on the unit type + * 2. Observed: model A has many failures, model B has many successes + * 3. Blended ranking: model B should win once samples accumulate + * + * Uses a mock pi (just an object with a hooks property) and an + * in-memory fake db (array-backed). + */ + +import { test } from "node:test"; +import assert from "node:assert/strict"; + +import { + createBeforeModelSelectHandler, + registerBeforeModelSelect, +} from "./hook-handler.mjs"; +import { blendedRanking } from "./bayesian-blender.mjs"; + +/** + * Fake in-memory db that mimics enough of better-sqlite3 for + * outcome-recorder + outcome-aggregator to operate against array-backed rows. + * + * The aggregator runs SELECT ... GROUP BY model_id; rather than implementing a + * SQL parser, we recognize each statement by regex and compute the aggregate + * in JavaScript. This is sufficient for these tests and isolates them from a + * real native dependency. + */ +function createFakeDb() { + const rows = []; + + function aggregateGroupedRows(unitType, since) { + const grouped = new Map(); + for (const row of rows) { + if (row.unit_type !== unitType) continue; + if (row.recorded_at <= since) continue; + if (!grouped.has(row.model_id)) { + grouped.set(row.model_id, []); + } + grouped.get(row.model_id).push(row); + } + + const out = []; + for (const [modelId, modelRows] of grouped.entries()) { + const sample_count = modelRows.length; + const successCount = modelRows.reduce((a, r) => a + r.succeeded, 0); + const retriesSum = modelRows.reduce((a, r) => a + r.retries, 0); + const blockerSum = modelRows.reduce((a, r) => a + r.blocker_discovered, 0); + const escalatedSum = modelRows.reduce((a, r) => a + r.escalated, 0); + + const verifyRows = modelRows.filter((r) => r.verification_passed !== null); + const verifySum = verifyRows.reduce((a, r) => a + r.verification_passed, 0); + const verification_pass_rate = + verifyRows.length > 0 ? verifySum / verifyRows.length : null; + + out.push({ + model_id: modelId, + sample_count, + success_rate: successCount / sample_count, + avg_retries: retriesSum / sample_count, + verification_pass_rate, + blocker_rate: blockerSum / sample_count, + escalation_rate: escalatedSum / sample_count, + avg_duration_ms: 0, + avg_tokens: 0, + avg_cost_usd: 0, + }); + } + return out; + } + + return { + rows, + exec: (_sql) => { + // Schema bootstrap is a no-op for the fake. + }, + prepare: (sql) => ({ + run: (...params) => { + if (/INSERT INTO llm_task_outcomes/i.test(sql)) { + const [ + model_id, + provider, + unit_type, + unit_id, + succeeded, + retries, + escalated, + verification_passed, + blocker_discovered, + duration_ms, + tokens_total, + cost_usd, + recorded_at, + ] = params; + rows.push({ + model_id, + provider, + unit_type, + unit_id, + succeeded, + retries, + escalated, + verification_passed, + blocker_discovered, + duration_ms, + tokens_total, + cost_usd, + recorded_at, + }); + return { changes: 1, lastInsertRowid: rows.length }; + } + return { changes: 0 }; + }, + all: (...params) => { + if (/GROUP BY model_id/i.test(sql)) { + const [unitType, since] = params; + return aggregateGroupedRows(unitType, since); + } + return []; + }, + get: (..._params) => { + if (/SELECT COUNT\(\*\) AS total/i.test(sql)) { + return { total: rows.length }; + } + return null; + }, + }), + }; +} + +/** + * Mock pi exposing the EventEmitter-style on/off shape that the hook handler + * registers against by feature detection. + */ +function createMockPi() { + const handlers = []; + return { + handlers, + on: (event, handler) => { + if (event === "before_model_select") handlers.push(handler); + }, + off: (event, handler) => { + if (event !== "before_model_select") return; + const idx = handlers.indexOf(handler); + if (idx >= 0) handlers.splice(idx, 1); + }, + emitBeforeModelSelect: async (input) => { + for (const h of handlers) { + const result = await h(input); + if (result?.modelId) return result; + } + return undefined; + }, + }; +} + +test("blendedRanking: cold start prefers prior", () => { + const priors = { "model-a": 80, "model-b": 50 }; + const observed = {}; + const result = blendedRanking(["model-a", "model-b"], "execute-task", priors, observed, { + nPrior: 10, + explorationEnabled: false, + }); + assert.strictEqual(result[0].modelId, "model-a", "higher-prior model wins at cold start"); +}); + +test("blendedRanking: observed dominates at high sample count", () => { + const priors = { "model-a": 80, "model-b": 50 }; + const observed = { + "model-a": { + sample_count: 100, + success_rate: 0.2, + avg_retries: 3, + verification_pass_rate: 0.3, + blocker_rate: 0.4, + }, + "model-b": { + sample_count: 100, + success_rate: 0.95, + avg_retries: 0.2, + verification_pass_rate: 0.92, + blocker_rate: 0.02, + }, + }; + const result = blendedRanking(["model-a", "model-b"], "execute-task", priors, observed, { + nPrior: 10, + explorationEnabled: false, + }); + assert.strictEqual( + result[0].modelId, + "model-b", + "observed signal flips the ranking after enough samples", + ); +}); + +test("hook handler: returns undefined when only 1 eligible model", async () => { + const handler = createBeforeModelSelectHandler({ + db: createFakeDb(), + overrides: {}, + weights: { "execute-task": { swe_bench: 1.0 } }, + benchmarks: {}, + opts: {}, + }); + const result = await handler({ + unitType: "execute-task", + eligibleModels: ["model-a"], + phaseConfig: {}, + }); + assert.strictEqual(result, undefined, "no ranking needed for single eligible"); +}); + +test("hook handler: returns undefined when no weights for unit type", async () => { + const handler = createBeforeModelSelectHandler({ + db: createFakeDb(), + overrides: {}, + weights: {}, + benchmarks: {}, + opts: {}, + }); + const result = await handler({ + unitType: "unknown-unit", + eligibleModels: ["model-a", "model-b"], + phaseConfig: {}, + }); + assert.strictEqual(result, undefined); +}); + +test("hook handler: catches errors and returns undefined or a model id", async () => { + const brokenDb = { + prepare: () => { + throw new Error("db boom"); + }, + }; + const handler = createBeforeModelSelectHandler({ + db: brokenDb, + overrides: {}, + weights: { "execute-task": { swe_bench: 1.0 } }, + benchmarks: {}, + opts: {}, + }); + // Must not throw. Aggregator swallows db errors internally; the handler + // therefore still produces a prior-only ranking. Either outcome is fine + // (a returned modelId or undefined) — what matters is no exception. + let threw = false; + let result; + try { + result = await handler({ + unitType: "execute-task", + eligibleModels: ["model-a", "model-b"], + phaseConfig: {}, + }); + } catch (_err) { + threw = true; + } + assert.strictEqual(threw, false, "handler survived db error without throwing"); + assert.ok(result === undefined || typeof result?.modelId === "string"); +}); + +test("registerBeforeModelSelect: registers via pi.on() when available", () => { + const pi = createMockPi(); + const deps = { + db: createFakeDb(), + overrides: {}, + weights: {}, + benchmarks: {}, + opts: {}, + }; + const unregister = registerBeforeModelSelect(pi, deps); + assert.strictEqual(pi.handlers.length, 1, "handler registered"); + unregister(); + assert.strictEqual(pi.handlers.length, 0, "handler unregistered"); +}); + +test("registerBeforeModelSelect: throws when pi exposes no compatible API", () => { + const piWithNothing = {}; + assert.throws( + () => + registerBeforeModelSelect(piWithNothing, { + db: createFakeDb(), + overrides: {}, + weights: {}, + benchmarks: {}, + opts: {}, + }), + /before_model_select hook registration API/, + ); +}); + +test("end-to-end: blend picks observed-better model after recording outcomes", async () => { + const db = createFakeDb(); + const now = Date.now(); + const ONE_HOUR_MS = 60 * 60 * 1000; + + // Seed 30 outcomes: model-a fails most of the time, model-b succeeds. + for (let i = 0; i < 30; i += 1) { + db.rows.push({ + model_id: "model-a", + provider: "test", + unit_type: "execute-task", + unit_id: `unit-${i}`, + succeeded: 0, + retries: 4, + escalated: 1, + verification_passed: 0, + blocker_discovered: 1, + duration_ms: 1000, + tokens_total: 100, + cost_usd: 0.01, + recorded_at: now - ONE_HOUR_MS, + }); + db.rows.push({ + model_id: "model-b", + provider: "test", + unit_type: "execute-task", + unit_id: `unit-${i}`, + succeeded: 1, + retries: 0, + escalated: 0, + verification_passed: 1, + blocker_discovered: 0, + duration_ms: 1000, + tokens_total: 100, + cost_usd: 0.01, + recorded_at: now - ONE_HOUR_MS, + }); + } + + // Priors lean toward model-a (the "established" benchmark winner) so we + // can confirm observed evidence reverses the choice. The override map + // mirrors loadCapabilityOverrides()'s shape: a 7-dim profile object with + // __benchmarks as a non-enumerable back-reference for computeUnitTypeScore. + const overrides = { + "model-a": {}, + "model-b": {}, + }; + Object.defineProperty(overrides["model-a"], "__benchmarks", { + value: { swe_bench: 90 }, + enumerable: false, + }); + Object.defineProperty(overrides["model-b"], "__benchmarks", { + value: { swe_bench: 30 }, + enumerable: false, + }); + + const handler = createBeforeModelSelectHandler({ + db, + overrides, + weights: { "execute-task": { swe_bench: 1.0 } }, + benchmarks: {}, + opts: { explorationEnabled: false }, + }); + + const result = await handler({ + unitType: "execute-task", + eligibleModels: ["model-a", "model-b"], + phaseConfig: {}, + }); + + assert.ok(result, "handler returned a decision"); + assert.strictEqual( + result.modelId, + "model-b", + "observed evidence (30 samples) overrides the prior favoring model-a", + ); +}); diff --git a/src/resources/extensions/gsd/learning/loadCapabilityOverrides.mjs b/src/resources/extensions/gsd/learning/loadCapabilityOverrides.mjs new file mode 100644 index 000000000..e3ebad536 --- /dev/null +++ b/src/resources/extensions/gsd/learning/loadCapabilityOverrides.mjs @@ -0,0 +1,436 @@ +/** + * loadCapabilityOverrides.mjs — Slice S01 of gsd-learning. + * + * Loads model-benchmarks.json + unit-weights.json from src/data/ and synthesizes + * the 7-dimension capability profile format pi-ai's MODEL_CAPABILITY_PROFILES uses. + * + * Dimensions (matching pi-ai's model-router.js): + * coding, debugging, research, reasoning, speed, longContext, instruction + * + * ## Mapping rationale (benchmark -> dimension) + * + * - coding: SWE-bench is the canonical real-world coding benchmark (weight 1.0); + * LiveCodeBench is competitive coding (0.8); HumanEval is dated function + * synthesis (0.5). + * - debugging: SWE-bench Verified is the cleanest signal for debug-fix tasks (1.0); + * fall back to full SWE-bench (0.7); GPQA contributes as a general + * problem-solving proxy (0.3). + * - research: BrowseComp directly measures multi-hop web research (1.0); SimpleQA + * is factuality (0.7); GPQA contributes domain reasoning (0.3). + * - reasoning: GPQA is graduate-level scientific reasoning (1.0); HLE is the hardest + * public eval (0.8); AIME 2026 is math olympiad (0.8); BBH is the older + * multi-task reasoning suite (0.6); MMLU-Pro is broad knowledge (0.5). + * - speed: Inverse of model size category, hardcoded via DEFAULT_SPEED_TABLE. + * Benchmarks don't measure latency; we use parameter scale + naming + * conventions (flash/mini/small/nano vs pro/large/671b/480b/thinking). + * - longContext: Blend of raw context_window (60%) and long_context_ruler (40%) when + * both are present. Falls back to whichever is available, or 0 if neither. + * Raw context is log2-scaled: ctx=2^12 (4K)->0, 2^17 (128K)->50, + * 2^20 (1M)->80, 2^21 (2M)->90, clamped at 100. Rationale: architectural + * ctx max is a hard limit, so it's the primary signal; RULER refines it + * with quality-at-distance evidence when published. + * - instruction: IFEval is the canonical instruction-following metric (1.0); Arena Elo + * normalized contributes user-preference signal (0.7); MMLU-Pro is a + * weak baseline (0.3). + * + * Where a benchmark is null, it is skipped and the effective denominator shrinks + * proportionally (so a model with only SWE-bench still gets a coding score). If a + * dimension has no benchmark data at all, it returns 0 (the blender will treat that + * as "no signal" and lean on observed outcomes once they exist). + * + * No dependencies on pi-ai or gsd internals. Reads only the two JSON files in src/data/. + */ + +import { readFile } from "node:fs/promises"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const MODULE_DIRECTORY = dirname(fileURLToPath(import.meta.url)); +const DEFAULT_BENCHMARKS_PATH = resolve(MODULE_DIRECTORY, "data/model-benchmarks.json"); +const DEFAULT_WEIGHTS_PATH = resolve(MODULE_DIRECTORY, "data/unit-weights.json"); + +const META_KEY = "_meta"; + +// Arena Elo normalization range. LMSys arena scores cluster between ~900 (weakest +// models) and ~1450 (frontier). We map [900, 1450] -> [0, 100] linearly. +const ARENA_ELO_FLOOR = 900; +const ARENA_ELO_CEILING = 1450; +const ARENA_ELO_RANGE = ARENA_ELO_CEILING - ARENA_ELO_FLOOR; + +// Context window normalization: log2-based scale mapping raw token counts to 0-100. +// At ctx=2^CTX_LOG2_FLOOR (4K), score = 0. Each doubling adds CTX_LOG2_STEP points. +// With floor=12 and step=10: 4K=0, 8K=10, 16K=20, ..., 128K=50, 256K=60, 1M=80, 2M=90. +const CTX_LOG2_FLOOR = 12; +const CTX_LOG2_STEP = 10; + +// Blend weights for the longContext dimension when both raw ctx and the RULER +// benchmark are available. Raw ctx is the stronger signal (measures the hard +// architectural limit); RULER refines it with quality-at-distance measurement. +const LONG_CONTEXT_CTX_WEIGHT = 0.6; +const LONG_CONTEXT_RULER_WEIGHT = 0.4; + +// Capability dimension scale: all dimensions normalized to 0-100. +const DIMENSION_SCALE_MAX = 100; +const DIMENSION_DEFAULT_WHEN_NO_DATA = 0; + +/** + * Speed lookup table: ordered list of regex -> speed score pairs. First match wins. + * Speed cannot be derived from accuracy benchmarks, so we hardcode based on naming + * conventions and parameter counts. + * + * flash / mini / small / nano / 20b / 30b -> 85-95 (fast) + * standard mid (no marker, ~70b-200b) -> 55-70 (medium) + * pro / large / thinking / 397b+ / 480b+ -> 25-45 (slow) + */ +export const DEFAULT_SPEED_TABLE = [ + { pattern: /flashx/i, score: 95 }, + { pattern: /flash/i, score: 90 }, + { pattern: /nano/i, score: 92 }, + { pattern: /mini/i, score: 88 }, + { pattern: /\bsmall\b/i, score: 85 }, + { pattern: /:20b\b/i, score: 88 }, + { pattern: /:30b\b/i, score: 82 }, + { pattern: /thinking/i, score: 30 }, + { pattern: /:671b\b/i, score: 25 }, + { pattern: /:675b\b/i, score: 25 }, + { pattern: /:480b\b/i, score: 30 }, + { pattern: /:397b\b/i, score: 35 }, + { pattern: /:235b\b/i, score: 45 }, + { pattern: /:123b\b/i, score: 50 }, + { pattern: /:80b\b/i, score: 60 }, + { pattern: /\bpro\b/i, score: 35 }, + { pattern: /\blarge\b/i, score: 40 }, + { pattern: /medium/i, score: 65 }, +]; + +const DEFAULT_SPEED_FALLBACK = 60; + +/** + * Per-dimension benchmark weight maps. Used by computeDimensionScores(). + * Each entry: { benchmark_key: weight }. + */ +const DIMENSION_WEIGHTS = { + coding: { + swe_bench: 1.0, + live_code_bench: 0.8, + human_eval: 0.5, + }, + debugging: { + swe_bench_verified: 1.0, + swe_bench: 0.7, + gpqa: 0.3, + }, + research: { + browse_comp: 1.0, + simple_qa: 0.7, + gpqa: 0.3, + }, + reasoning: { + gpqa: 1.0, + hle: 0.8, + aime_2026: 0.8, + bbh: 0.6, + mmlu_pro: 0.5, + }, + longContext: { + long_context_ruler: 1.0, + }, + instruction: { + instruction_following: 1.0, + arena_elo_normalized: 0.7, + mmlu_pro: 0.3, + }, +}; + +const SEVEN_DIMENSIONS = Object.freeze([ + "coding", + "debugging", + "research", + "reasoning", + "speed", + "longContext", + "instruction", +]); + +/** + * Strip provider prefix from a model id. `kimi-coding/k2p5` -> `k2p5`. + * + * @param {string} modelId + * @returns {string} + */ +function stripProviderPrefix(modelId) { + const slashIndex = modelId.indexOf("/"); + if (slashIndex === -1) { + return modelId; + } + return modelId.slice(slashIndex + 1); +} + +/** + * Normalize a raw arena Elo into a 0-100 score. Returns null if input is null. + * + * @param {number|null} arenaElo + * @returns {number|null} + */ +function normalizeArenaElo(arenaElo) { + if (arenaElo === null || arenaElo === undefined) { + return null; + } + const clamped = Math.min(Math.max(arenaElo, ARENA_ELO_FLOOR), ARENA_ELO_CEILING); + return ((clamped - ARENA_ELO_FLOOR) / ARENA_ELO_RANGE) * DIMENSION_SCALE_MAX; +} + +/** + * Normalize a raw context_window (tokens) into a 0-100 score using log2 scaling. + * Returns null if input is null/undefined/non-positive. + * + * ctx=4096 (2^12) -> 0 + * ctx=8192 (2^13) -> 10 + * ctx=16384 (2^14) -> 20 + * ctx=32768 (2^15) -> 30 + * ctx=65536 (2^16) -> 40 + * ctx=131072 (2^17) -> 50 + * ctx=262144 (2^18) -> 60 + * ctx=524288 (2^19) -> 70 + * ctx=1048576 (2^20) -> 80 + * ctx=2097152 (2^21) -> 90 + * ctx>=8388608 (2^23+) -> 100 (clamped) + * + * @param {number|null|undefined} contextWindow - raw max input tokens + * @returns {number|null} 0-100 score, or null if input is null/invalid + */ +export function normalizeContextWindow(contextWindow) { + if (contextWindow === null || contextWindow === undefined || contextWindow <= 0) { + return null; + } + const log2 = Math.log2(contextWindow); + const rawScore = (log2 - CTX_LOG2_FLOOR) * CTX_LOG2_STEP; + return Math.min(Math.max(rawScore, 0), DIMENSION_SCALE_MAX); +} + +/** + * Compute the longContext dimension score. Blends normalized raw context_window + * with the long_context_ruler benchmark when both are available. Falls back to + * whichever is present, or 0 if neither. + * + * Blend: longContext = LONG_CONTEXT_CTX_WEIGHT · ctx_score + LONG_CONTEXT_RULER_WEIGHT · ruler + * + * @param {object} benchmarks - per-model benchmark entry + * @returns {number} 0-100 score + */ +export function computeLongContextDimension(benchmarks) { + if (!benchmarks || typeof benchmarks !== "object") { + return DIMENSION_DEFAULT_WHEN_NO_DATA; + } + const ctxScore = normalizeContextWindow(benchmarks.context_window); + const rulerScore = benchmarks.long_context_ruler; + const rulerValid = rulerScore !== null && rulerScore !== undefined; + + if (ctxScore !== null && rulerValid) { + return ctxScore * LONG_CONTEXT_CTX_WEIGHT + rulerScore * LONG_CONTEXT_RULER_WEIGHT; + } + if (ctxScore !== null) { + return ctxScore; + } + if (rulerValid) { + return rulerScore; + } + return DIMENSION_DEFAULT_WHEN_NO_DATA; +} + +/** + * Compute a single dimension score: weighted average of available benchmarks. + * Skips nulls and shrinks the denominator proportionally. + * + * @param {object} benchmarks - per-model benchmark entry + * @param {object} weightMap - { benchmark_key: weight } + * @returns {number} 0-100 score, or 0 if no benchmarks present + */ +function computeWeightedDimension(benchmarks, weightMap) { + let weightedSum = 0; + let effectiveMax = 0; + + for (const [benchmarkKey, weight] of Object.entries(weightMap)) { + let value; + if (benchmarkKey === "arena_elo_normalized") { + value = normalizeArenaElo(benchmarks.arena_elo); + } else { + value = benchmarks[benchmarkKey]; + } + if (value === null || value === undefined) { + continue; + } + weightedSum += value * weight; + effectiveMax += weight * DIMENSION_SCALE_MAX; + } + + if (effectiveMax === 0) { + return DIMENSION_DEFAULT_WHEN_NO_DATA; + } + return (weightedSum / effectiveMax) * DIMENSION_SCALE_MAX; +} + +/** + * Look up the speed score for a model id by matching against the speed table. + * + * @param {string} modelId + * @param {Array<{pattern: RegExp, score: number}>} speedTable + * @returns {number} + */ +function lookupSpeedScore(modelId, speedTable) { + for (const entry of speedTable) { + if (entry.pattern.test(modelId)) { + return entry.score; + } + } + return DEFAULT_SPEED_FALLBACK; +} + +/** + * Compute the 7-dimension capability profile for a single model. + * + * @param {object} benchmarks - per-model entry from model-benchmarks.json (minus _meta) + * @param {string} [modelId=""] - used for speed lookup; pass the resolved id + * @param {Array} [speedTable=DEFAULT_SPEED_TABLE] + * @returns {{coding: number, debugging: number, research: number, reasoning: number, speed: number, longContext: number, instruction: number}} + */ +export function computeDimensionScores(benchmarks, modelId = "", speedTable = DEFAULT_SPEED_TABLE) { + if (!benchmarks || typeof benchmarks !== "object") { + return SEVEN_DIMENSIONS.reduce((acc, dim) => { + acc[dim] = DIMENSION_DEFAULT_WHEN_NO_DATA; + return acc; + }, {}); + } + + return { + coding: computeWeightedDimension(benchmarks, DIMENSION_WEIGHTS.coding), + debugging: computeWeightedDimension(benchmarks, DIMENSION_WEIGHTS.debugging), + research: computeWeightedDimension(benchmarks, DIMENSION_WEIGHTS.research), + reasoning: computeWeightedDimension(benchmarks, DIMENSION_WEIGHTS.reasoning), + speed: lookupSpeedScore(modelId, speedTable), + longContext: computeLongContextDimension(benchmarks), + instruction: computeWeightedDimension(benchmarks, DIMENSION_WEIGHTS.instruction), + }; +} + +/** + * Compute the unit-type-specific score for a model: dot product of unit-type weights + * over the model's benchmark values, normalized by available-weight mass. + * + * Used for ranking candidates per unit type. Skips nulls and shrinks denominator. + * + * @param {string} modelId - may include provider prefix; will be stripped + * @param {string} unitType - key into the weights map + * @param {object} overrides - { modelId: dimensionProfile, ... } from loadCapabilityOverrides + * @param {object} weights - parsed unit-weights.json + * @returns {number} 0-100 score, or 0 if no overlap between weights and model benchmarks + */ +export function computeUnitTypeScore(modelId, unitType, overrides, weights) { + if (!weights || typeof weights !== "object") { + return 0; + } + const weightMap = weights[unitType]; + if (!weightMap || typeof weightMap !== "object") { + return 0; + } + + const resolvedId = stripProviderPrefix(modelId); + const profileEntry = overrides && (overrides[modelId] || overrides[resolvedId]); + if (!profileEntry) { + return 0; + } + const benchmarks = profileEntry.__benchmarks; + if (!benchmarks || typeof benchmarks !== "object") { + return 0; + } + + let weightedSum = 0; + let effectiveMax = 0; + for (const [benchmarkKey, weight] of Object.entries(weightMap)) { + const value = benchmarks[benchmarkKey]; + if (value === null || value === undefined) { + continue; + } + weightedSum += value * weight; + effectiveMax += weight * DIMENSION_SCALE_MAX; + } + + if (effectiveMax === 0) { + return 0; + } + return (weightedSum / effectiveMax) * DIMENSION_SCALE_MAX; +} + +/** + * Read a JSON file from disk and parse it. Throws on parse errors with file context. + * + * @param {string} path + * @returns {Promise} + */ +async function readJsonFile(path) { + const raw = await readFile(path, "utf8"); + try { + return JSON.parse(raw); + } catch (parseError) { + throw new Error(`Failed to parse JSON file at ${path}: ${parseError.message}`); + } +} + +/** + * Strip the _meta key from a parsed JSON object. + * + * @param {object} parsed + * @returns {object} + */ +function stripMeta(parsed) { + const { [META_KEY]: _meta, ...rest } = parsed; + return rest; +} + +/** + * Load benchmark and weight JSONs and synthesize the capability override map. + * + * The returned `overrides` map has one entry per model in model-benchmarks.json. + * Each entry is the 7-dimension profile plus a non-enumerable `__benchmarks` reference + * to the raw benchmark block so computeUnitTypeScore() can re-score against per-unit + * weight maps without re-reading the file. + * + * @param {object} [options] + * @param {string} [options.benchmarksPath] - override default path + * @param {string} [options.weightsPath] - override default path + * @param {Array} [options.speedTable] - override DEFAULT_SPEED_TABLE + * @returns {Promise<{overrides: object, weights: object, benchmarks: object}>} + */ +export async function loadCapabilityOverrides(options = {}) { + const benchmarksPath = options.benchmarksPath ?? DEFAULT_BENCHMARKS_PATH; + const weightsPath = options.weightsPath ?? DEFAULT_WEIGHTS_PATH; + const speedTable = options.speedTable ?? DEFAULT_SPEED_TABLE; + + const [rawBenchmarks, rawWeights] = await Promise.all([ + readJsonFile(benchmarksPath), + readJsonFile(weightsPath), + ]); + + const benchmarks = stripMeta(rawBenchmarks); + const overrides = {}; + + for (const [modelId, modelBenchmarks] of Object.entries(benchmarks)) { + const dimensionProfile = computeDimensionScores(modelBenchmarks, modelId, speedTable); + // Attach the raw benchmarks reference so computeUnitTypeScore can use them + // without re-reading the file. Defined as non-enumerable so JSON.stringify of + // the override map produces the clean 7-dim shape pi-ai expects. + Object.defineProperty(dimensionProfile, "__benchmarks", { + value: modelBenchmarks, + enumerable: false, + writable: false, + configurable: false, + }); + overrides[modelId] = dimensionProfile; + } + + return { + overrides, + weights: rawWeights, + benchmarks: rawBenchmarks, + }; +} diff --git a/src/resources/extensions/gsd/learning/loadCapabilityOverrides.test.mjs b/src/resources/extensions/gsd/learning/loadCapabilityOverrides.test.mjs new file mode 100644 index 000000000..b88408992 --- /dev/null +++ b/src/resources/extensions/gsd/learning/loadCapabilityOverrides.test.mjs @@ -0,0 +1,217 @@ +/** + * Tests for loadCapabilityOverrides — focus on the longContext dimension + * since it's the new path that blends context_window with long_context_ruler. + * + * Run with: node --test src/loadCapabilityOverrides.test.mjs + */ + +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { + normalizeContextWindow, + computeLongContextDimension, + computeDimensionScores, + computeUnitTypeScore, + loadCapabilityOverrides, + DEFAULT_SPEED_TABLE, +} from "./loadCapabilityOverrides.mjs"; + +// ── normalizeContextWindow ─────────────────────────────────────────────────── + +test("normalizeContextWindow: null input returns null", () => { + assert.strictEqual(normalizeContextWindow(null), null); +}); + +test("normalizeContextWindow: undefined input returns null", () => { + assert.strictEqual(normalizeContextWindow(undefined), null); +}); + +test("normalizeContextWindow: zero input returns null", () => { + assert.strictEqual(normalizeContextWindow(0), null); +}); + +test("normalizeContextWindow: negative input returns null", () => { + assert.strictEqual(normalizeContextWindow(-4096), null); +}); + +test("normalizeContextWindow: 4K (2^12) returns 0", () => { + assert.strictEqual(normalizeContextWindow(4096), 0); +}); + +test("normalizeContextWindow: 8K (2^13) returns 10", () => { + assert.strictEqual(normalizeContextWindow(8192), 10); +}); + +test("normalizeContextWindow: 131072 (128K, 2^17) returns 50", () => { + assert.strictEqual(normalizeContextWindow(131072), 50); +}); + +test("normalizeContextWindow: 1048576 (1M, 2^20) returns 80", () => { + assert.strictEqual(normalizeContextWindow(1048576), 80); +}); + +test("normalizeContextWindow: 2097152 (2M, 2^21) returns 90", () => { + assert.strictEqual(normalizeContextWindow(2097152), 90); +}); + +test("normalizeContextWindow: 8388608 (8M, 2^23) clamps at 100", () => { + assert.strictEqual(normalizeContextWindow(8388608), 100); +}); + +test("normalizeContextWindow: 16M+ clamps at 100", () => { + assert.strictEqual(normalizeContextWindow(16_777_216), 100); + assert.strictEqual(normalizeContextWindow(100_000_000), 100); +}); + +test("normalizeContextWindow: 2K (below floor) returns 0 (clamped)", () => { + assert.strictEqual(normalizeContextWindow(2048), 0); +}); + +test("normalizeContextWindow: non-power-of-2 values interpolate correctly", () => { + // 200000 is between 2^17 (131072, score 50) and 2^18 (262144, score 60) + // log2(200000) ≈ 17.61, score ≈ (17.61 - 12) * 10 = 56.1 + const score = normalizeContextWindow(200000); + assert.ok(score > 55 && score < 58, `expected ~56, got ${score}`); +}); + +// ── computeLongContextDimension ───────────────────────────────────────────── + +test("computeLongContextDimension: both ctx and ruler available → blends 60/40", () => { + const benchmarks = { + context_window: 1048576, // → 80 + long_context_ruler: 95, // → 95 + }; + // blended = 80 * 0.6 + 95 * 0.4 = 48 + 38 = 86 + assert.strictEqual(computeLongContextDimension(benchmarks), 86); +}); + +test("computeLongContextDimension: only context_window returns pure ctx score", () => { + const benchmarks = { + context_window: 262144, // → 60 + long_context_ruler: null, + }; + assert.strictEqual(computeLongContextDimension(benchmarks), 60); +}); + +test("computeLongContextDimension: only long_context_ruler returns pure ruler score", () => { + const benchmarks = { + context_window: null, + long_context_ruler: 72, + }; + assert.strictEqual(computeLongContextDimension(benchmarks), 72); +}); + +test("computeLongContextDimension: neither available returns 0", () => { + const benchmarks = { + context_window: null, + long_context_ruler: null, + }; + assert.strictEqual(computeLongContextDimension(benchmarks), 0); +}); + +test("computeLongContextDimension: null benchmarks object returns 0", () => { + assert.strictEqual(computeLongContextDimension(null), 0); +}); + +test("computeLongContextDimension: undefined benchmarks object returns 0", () => { + assert.strictEqual(computeLongContextDimension(undefined), 0); +}); + +test("computeLongContextDimension: missing keys entirely returns 0", () => { + assert.strictEqual(computeLongContextDimension({}), 0); +}); + +test("computeLongContextDimension: huge ctx beats low ruler when blended", () => { + const bench = { context_window: 2097152, long_context_ruler: 30 }; // 90 * 0.6 + 30 * 0.4 = 54 + 12 = 66 + assert.strictEqual(computeLongContextDimension(bench), 66); +}); + +test("computeLongContextDimension: small ctx with high ruler still gets lifted by ruler", () => { + const bench = { context_window: 131072, long_context_ruler: 95 }; // 50 * 0.6 + 95 * 0.4 = 30 + 38 = 68 + assert.strictEqual(computeLongContextDimension(bench), 68); +}); + +// ── computeDimensionScores integration ────────────────────────────────────── + +test("computeDimensionScores: longContext uses new blend for real model", () => { + // Simulate mimo-v2-pro-like entry: 1M ctx, no RULER benchmark + const benchmarks = { + swe_bench: null, + context_window: 1048576, + long_context_ruler: null, + }; + const profile = computeDimensionScores(benchmarks, "mimo-v2-pro"); + assert.strictEqual(profile.longContext, 80, "1M ctx alone → score 80"); +}); + +test("computeDimensionScores: longContext blends when both present", () => { + const benchmarks = { + context_window: 524288, // 2^19 → 70 + long_context_ruler: 60, + }; + const profile = computeDimensionScores(benchmarks, "hypothetical"); + // 70 * 0.6 + 60 * 0.4 = 42 + 24 = 66 + assert.strictEqual(profile.longContext, 66); +}); + +test("computeDimensionScores: zero longContext when no ctx and no ruler", () => { + const benchmarks = { + swe_bench: 80, + context_window: null, + long_context_ruler: null, + }; + const profile = computeDimensionScores(benchmarks, "something"); + assert.strictEqual(profile.longContext, 0); +}); + +// ── loadCapabilityOverrides end-to-end with real data files ───────────────── + +test("loadCapabilityOverrides: loads real model-benchmarks.json and computes longContext", async () => { + const { overrides } = await loadCapabilityOverrides(); + + // mimo-v2-pro should have a longContext score (it's the 1M ctx model) + const mimoPro = overrides["mimo-v2-pro"]; + assert.ok(mimoPro, "mimo-v2-pro entry exists in overrides"); + assert.ok(mimoPro.longContext > 0, `mimo-v2-pro longContext should be > 0, got ${mimoPro.longContext}`); + // With 1M ctx and no RULER published, should be exactly 80 + assert.strictEqual(mimoPro.longContext, 80, "mimo-v2-pro 1M ctx → longContext 80"); + + // cogito-2.1:671b was fixed to 131K → longContext should be 50 + const cogito = overrides["cogito-2.1:671b"]; + assert.ok(cogito, "cogito entry exists"); + assert.strictEqual(cogito.longContext, 50, "cogito 128K ctx → longContext 50"); + + // Models with no ctx and no ruler should have longContext 0 + // (Verify by finding any model that has null ctx in the data) + // All 40 models were enriched, so this case may not occur in live data; + // just verify the structure: + for (const [modelId, profile] of Object.entries(overrides)) { + assert.ok(typeof profile.longContext === "number", + `${modelId} longContext is a number`); + assert.ok(profile.longContext >= 0 && profile.longContext <= 100, + `${modelId} longContext in [0, 100], got ${profile.longContext}`); + } +}); + +test("loadCapabilityOverrides: all 40 models have populated dimension profiles", async () => { + const { overrides, benchmarks } = await loadCapabilityOverrides(); + const modelIds = Object.keys(benchmarks).filter((k) => k !== "_meta"); + assert.ok(modelIds.length >= 40, `expected at least 40 models, got ${modelIds.length}`); + + for (const modelId of modelIds) { + const profile = overrides[modelId]; + assert.ok(profile, `${modelId} has a profile`); + for (const dim of ["coding", "debugging", "research", "reasoning", "speed", "longContext", "instruction"]) { + assert.ok(typeof profile[dim] === "number", + `${modelId}.${dim} is a number`); + } + } +}); + +test("loadCapabilityOverrides: computeUnitTypeScore strips provider prefix correctly", async () => { + const { overrides, weights } = await loadCapabilityOverrides(); + // Both "kimi-coding/k2p5" and bare "k2p5" should resolve + const prefixed = computeUnitTypeScore("kimi-coding/k2p5", "execute-task", overrides, weights); + const bare = computeUnitTypeScore("k2p5", "execute-task", overrides, weights); + assert.strictEqual(prefixed, bare, "provider prefix stripping produces identical score"); +}); diff --git a/src/resources/extensions/gsd/learning/outcome-aggregator.mjs b/src/resources/extensions/gsd/learning/outcome-aggregator.mjs new file mode 100644 index 000000000..6dc837300 --- /dev/null +++ b/src/resources/extensions/gsd/learning/outcome-aggregator.mjs @@ -0,0 +1,305 @@ +/** + * gsd-learning: outcome-aggregator + * + * Reads `llm_task_outcomes` and computes rolling-window stats per + * `(model_id, unit_type)` for the Bayesian blender. + * + * ## Responsibilities + * - Aggregate observed outcomes over a configurable rolling window + * - Provide per-model and grouped (per-unit-type) views + * - Expose total-sample counts for UCB1 exploration math + * - Surface raw recent rows for inspection / debugging + * + * ## Dependencies + * - Duck-typed SQLite handle exposing `prepare(sql).get(...params)` and + * `prepare(sql).all(...params)`. Compatible with `better-sqlite3`. + * + * ## Contract + * - All SQL is parameterized — no string interpolation of caller input. + * - Returns zeroed stats (sample_count = 0) when no rows match, never null. + * - `verification_pass_rate` is null when no row in the window had a + * non-null `verification_passed` value. + * + * @module gsd-learning/outcome-aggregator + */ + +const DEFAULT_ROLLING_DAYS = 30; +const MS_PER_DAY = 24 * 60 * 60 * 1000; + +const AGGREGATE_ONE_SQL = ` + SELECT + COUNT(*) AS sample_count, + AVG(CAST(succeeded AS REAL)) AS success_rate, + AVG(CAST(retries AS REAL)) AS avg_retries, + AVG(CASE WHEN verification_passed IS NOT NULL THEN CAST(verification_passed AS REAL) END) AS verification_pass_rate, + AVG(CAST(blocker_discovered AS REAL)) AS blocker_rate, + AVG(CAST(escalated AS REAL)) AS escalation_rate, + AVG(CAST(duration_ms AS REAL)) AS avg_duration_ms, + AVG(CAST(tokens_total AS REAL)) AS avg_tokens, + AVG(CAST(cost_usd AS REAL)) AS avg_cost_usd + FROM llm_task_outcomes + WHERE model_id = ? + AND unit_type = ? + AND recorded_at > ? +`; + +const AGGREGATE_GROUPED_SQL = ` + SELECT + model_id, + COUNT(*) AS sample_count, + AVG(CAST(succeeded AS REAL)) AS success_rate, + AVG(CAST(retries AS REAL)) AS avg_retries, + AVG(CASE WHEN verification_passed IS NOT NULL THEN CAST(verification_passed AS REAL) END) AS verification_pass_rate, + AVG(CAST(blocker_discovered AS REAL)) AS blocker_rate, + AVG(CAST(escalated AS REAL)) AS escalation_rate, + AVG(CAST(duration_ms AS REAL)) AS avg_duration_ms, + AVG(CAST(tokens_total AS REAL)) AS avg_tokens, + AVG(CAST(cost_usd AS REAL)) AS avg_cost_usd + FROM llm_task_outcomes + WHERE unit_type = ? + AND recorded_at > ? + GROUP BY model_id +`; + +const TOTAL_SAMPLES_SQL = ` + SELECT COUNT(*) AS total + FROM llm_task_outcomes + WHERE recorded_at > ? +`; + +/** + * Aggregated rolling-window stats for a (model_id, unit_type) pair. + * + * @typedef {Object} AggregatedStats + * @property {string} modelId + * @property {string} unitType + * @property {number} sample_count + * @property {number} success_rate 0.0 to 1.0 + * @property {number} avg_retries + * @property {number|null} verification_pass_rate 0.0 to 1.0 or null if no verification data + * @property {number} blocker_rate 0.0 to 1.0 + * @property {number} escalation_rate 0.0 to 1.0 + * @property {number} avg_duration_ms + * @property {number} avg_tokens + * @property {number} avg_cost_usd + * @property {number} window_days + */ + +/** + * Build a zeroed AggregatedStats record for cold-start callers. + * + * @param {string} modelId + * @param {string} unitType + * @param {number} windowDays + * @returns {AggregatedStats} + */ +function emptyStats(modelId, unitType, windowDays) { + return { + modelId, + unitType, + sample_count: 0, + success_rate: 0, + avg_retries: 0, + verification_pass_rate: null, + blocker_rate: 0, + escalation_rate: 0, + avg_duration_ms: 0, + avg_tokens: 0, + avg_cost_usd: 0, + window_days: windowDays, + }; +} + +/** + * Coerce a possibly-null SQL aggregate result to a number, defaulting to 0. + * + * @param {number|null|undefined} value + * @returns {number} + */ +function toNumber(value) { + if (value === null || value === undefined || Number.isNaN(value)) return 0; + return value; +} + +/** + * Compute the cutoff epoch-ms for the rolling window. + * + * @param {number} now epoch ms + * @param {number} rollingDays + * @returns {number} + */ +function cutoff(now, rollingDays) { + return now - rollingDays * MS_PER_DAY; +} + +/** + * Map a raw SQL aggregate row to AggregatedStats. + * + * @param {object} row + * @param {string} modelId + * @param {string} unitType + * @param {number} windowDays + * @returns {AggregatedStats} + */ +function rowToStats(row, modelId, unitType, windowDays) { + return { + modelId, + unitType, + sample_count: toNumber(row.sample_count), + success_rate: toNumber(row.success_rate), + avg_retries: toNumber(row.avg_retries), + verification_pass_rate: + row.verification_pass_rate === null || row.verification_pass_rate === undefined + ? null + : row.verification_pass_rate, + blocker_rate: toNumber(row.blocker_rate), + escalation_rate: toNumber(row.escalation_rate), + avg_duration_ms: toNumber(row.avg_duration_ms), + avg_tokens: toNumber(row.avg_tokens), + avg_cost_usd: toNumber(row.avg_cost_usd), + window_days: windowDays, + }; +} + +/** + * Aggregate outcomes for a single (model_id, unit_type) pair. + * + * @param {object} db + * @param {string} modelId + * @param {string} unitType + * @param {{rollingDays?: number, now?: number}} [opts] + * @returns {AggregatedStats} + * + * @example + * const stats = aggregateOutcomes(db, "kimi-coding/k2p5", "execute-task", {rollingDays: 30}); + * // {modelId, unitType, sample_count: 12, success_rate: 0.83, ...} + */ +export function aggregateOutcomes(db, modelId, unitType, opts = {}) { + const rollingDays = opts.rollingDays ?? DEFAULT_ROLLING_DAYS; + const now = opts.now ?? Date.now(); + const since = cutoff(now, rollingDays); + + try { + const row = db.prepare(AGGREGATE_ONE_SQL).get(modelId, unitType, since); + if (!row || toNumber(row.sample_count) === 0) { + return emptyStats(modelId, unitType, rollingDays); + } + return rowToStats(row, modelId, unitType, rollingDays); + } catch (_err) { + return emptyStats(modelId, unitType, rollingDays); + } +} + +/** + * Aggregate outcomes for every model that has rows for a given unit type. + * + * Single SQL query with `GROUP BY model_id` for efficiency at dispatch time. + * + * @param {object} db + * @param {string} unitType + * @param {{rollingDays?: number, now?: number}} [opts] + * @returns {Map} keyed by `modelId` + * + * @example + * const ranking = aggregateAllForUnitType(db, "execute-task"); + * for (const [modelId, stats] of ranking) { + * console.log(modelId, stats.success_rate); + * } + */ +export function aggregateAllForUnitType(db, unitType, opts = {}) { + const rollingDays = opts.rollingDays ?? DEFAULT_ROLLING_DAYS; + const now = opts.now ?? Date.now(); + const since = cutoff(now, rollingDays); + const result = new Map(); + + try { + const rows = db.prepare(AGGREGATE_GROUPED_SQL).all(unitType, since); + for (const row of rows) { + result.set(row.model_id, rowToStats(row, row.model_id, unitType, rollingDays)); + } + } catch (_err) { + // swallow — return whatever was collected (likely empty Map) + } + + return result; +} + +/** + * Total number of outcome rows in the rolling window. Used as the UCB1 + * exploration denominator (`ln(total_samples)`). + * + * @param {object} db + * @param {{rollingDays?: number, now?: number}} [opts] + * @returns {number} + * + * @example + * const total = totalSamples(db, {rollingDays: 30}); + */ +export function totalSamples(db, opts = {}) { + const rollingDays = opts.rollingDays ?? DEFAULT_ROLLING_DAYS; + const now = opts.now ?? Date.now(); + const since = cutoff(now, rollingDays); + + try { + const row = db.prepare(TOTAL_SAMPLES_SQL).get(since); + return toNumber(row?.total); + } catch (_err) { + return 0; + } +} + +/** + * Recent raw outcome rows for inspection / debugging. Ordered by + * `recorded_at DESC`. Optional filters by `unitType` and/or `modelId`. + * + * @param {object} db + * @param {{limit?: number, unitType?: string, modelId?: string}} [opts] + * @returns {Array} + * + * @example + * recentOutcomes(db, {limit: 20, unitType: "execute-task"}); + */ +export function recentOutcomes(db, opts = {}) { + const limit = opts.limit ?? 100; + const filters = []; + const params = []; + + if (opts.unitType) { + filters.push("unit_type = ?"); + params.push(opts.unitType); + } + if (opts.modelId) { + filters.push("model_id = ?"); + params.push(opts.modelId); + } + + const where = filters.length > 0 ? `WHERE ${filters.join(" AND ")}` : ""; + const sql = ` + SELECT + id, + model_id, + provider, + unit_type, + unit_id, + succeeded, + retries, + escalated, + verification_passed, + blocker_discovered, + duration_ms, + tokens_total, + cost_usd, + recorded_at + FROM llm_task_outcomes + ${where} + ORDER BY recorded_at DESC + LIMIT ? + `; + params.push(limit); + + try { + return db.prepare(sql).all(...params); + } catch (_err) { + return []; + } +} diff --git a/src/resources/extensions/gsd/learning/outcome-recorder.mjs b/src/resources/extensions/gsd/learning/outcome-recorder.mjs new file mode 100644 index 000000000..5c5b5ba35 --- /dev/null +++ b/src/resources/extensions/gsd/learning/outcome-recorder.mjs @@ -0,0 +1,299 @@ +/** + * gsd-learning: outcome-recorder + * + * Records LLM dispatch outcomes to the `llm_task_outcomes` table. + * + * ## Responsibilities + * - Validate outcome shape before insertion + * - Insert one or many outcomes via parameterized SQL + * - Bootstrap the schema on a fresh database + * + * ## Contract — fire-and-forget + * `recordOutcome` and `recordOutcomeBatch` must NEVER throw. They catch + * every exception and return a boolean / count instead. This module sits + * on the critical unit-completion path; a learning-system bug must not + * crash a successful task. + * + * ## Dependencies + * - Duck-typed SQLite handle exposing `prepare(sql).run(...params)`, + * `prepare(sql).get(...params)`, `prepare(sql).all(...params)` and + * ideally `exec(sql)`. Compatible with `better-sqlite3`. + * - No hard import of any SQLite library — keeps this module standalone + * and unit-testable with an in-memory fake. + * + * ## Side effects + * - Writes rows into `llm_task_outcomes`. + * + * @module gsd-learning/outcome-recorder + */ + +const REQUIRED_FIELDS = ["modelId", "provider", "unitType", "unitId", "succeeded"]; + +const INSERT_SQL = ` + INSERT INTO llm_task_outcomes ( + model_id, + provider, + unit_type, + unit_id, + succeeded, + retries, + escalated, + verification_passed, + blocker_discovered, + duration_ms, + tokens_total, + cost_usd, + recorded_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) +`; + +/** + * Validated outcome shape for insertion. + * + * @typedef {Object} Outcome + * @property {string} modelId e.g. "kimi-coding/k2p5" + * @property {string} provider e.g. "kimi-coding" + * @property {string} unitType e.g. "research-slice", "execute-task" + * @property {string} unitId e.g. "M001/S01" or "M001/S01/T01" + * @property {boolean} succeeded did the unit complete without fatal error + * @property {number} [retries=0] number of retries + * @property {boolean} [escalated=false] whether tier escalated on failure + * @property {boolean|null} [verification_passed] null if no verification step + * @property {boolean} [blocker_discovered=false] + * @property {number} [duration_ms] + * @property {number} [tokens_total] + * @property {number} [cost_usd] + * @property {number} [recorded_at] epoch ms; defaults to Date.now() + */ + +/** + * Validate an outcome object before insertion. + * + * @param {Outcome} outcome + * @returns {{valid: boolean, errors: string[]}} + * + * @example + * const r = validateOutcome({modelId: "k2p5", provider: "kimi", unitType: "execute-task", unitId: "M001/S01/T01", succeeded: true}); + * // r.valid === true + */ +export function validateOutcome(outcome) { + const errors = []; + + if (outcome === null || typeof outcome !== "object") { + return { valid: false, errors: ["outcome must be an object"] }; + } + + for (const field of REQUIRED_FIELDS) { + if (outcome[field] === undefined || outcome[field] === null) { + errors.push(`missing required field: ${field}`); + } + } + + if (outcome.modelId !== undefined && typeof outcome.modelId !== "string") { + errors.push("modelId must be a string"); + } + if (outcome.provider !== undefined && typeof outcome.provider !== "string") { + errors.push("provider must be a string"); + } + if (outcome.unitType !== undefined && typeof outcome.unitType !== "string") { + errors.push("unitType must be a string"); + } + if (outcome.unitId !== undefined && typeof outcome.unitId !== "string") { + errors.push("unitId must be a string"); + } + if (outcome.succeeded !== undefined && typeof outcome.succeeded !== "boolean") { + errors.push("succeeded must be a boolean"); + } + + if (outcome.retries !== undefined && (!Number.isInteger(outcome.retries) || outcome.retries < 0)) { + errors.push("retries must be a non-negative integer"); + } + if (outcome.escalated !== undefined && typeof outcome.escalated !== "boolean") { + errors.push("escalated must be a boolean"); + } + if ( + outcome.verification_passed !== undefined && + outcome.verification_passed !== null && + typeof outcome.verification_passed !== "boolean" + ) { + errors.push("verification_passed must be a boolean or null"); + } + if (outcome.blocker_discovered !== undefined && typeof outcome.blocker_discovered !== "boolean") { + errors.push("blocker_discovered must be a boolean"); + } + if (outcome.duration_ms !== undefined && (!Number.isFinite(outcome.duration_ms) || outcome.duration_ms < 0)) { + errors.push("duration_ms must be a non-negative number"); + } + if (outcome.tokens_total !== undefined && (!Number.isFinite(outcome.tokens_total) || outcome.tokens_total < 0)) { + errors.push("tokens_total must be a non-negative number"); + } + if (outcome.cost_usd !== undefined && (!Number.isFinite(outcome.cost_usd) || outcome.cost_usd < 0)) { + errors.push("cost_usd must be a non-negative number"); + } + + return { valid: errors.length === 0, errors }; +} + +/** + * Coerce a boolean (or boolean-like) to SQLite 0/1. + * Null/undefined pass through as null. + * + * @param {boolean|null|undefined} value + * @returns {0|1|null} + */ +function boolToInt(value) { + if (value === null || value === undefined) return null; + return value ? 1 : 0; +} + +/** + * Build the positional parameter array for `INSERT_SQL`. + * + * @param {Outcome} outcome + * @returns {Array} + */ +function buildInsertParams(outcome) { + return [ + outcome.modelId, + outcome.provider, + outcome.unitType, + outcome.unitId, + boolToInt(outcome.succeeded), + outcome.retries ?? 0, + boolToInt(outcome.escalated ?? false), + boolToInt(outcome.verification_passed ?? null), + boolToInt(outcome.blocker_discovered ?? false), + outcome.duration_ms ?? null, + outcome.tokens_total ?? null, + outcome.cost_usd ?? null, + outcome.recorded_at ?? Date.now(), + ]; +} + +/** + * Record a single outcome. Fire-and-forget — never throws. + * + * @param {object} db Duck-typed SQLite handle (must expose `prepare(sql).run(...params)`) + * @param {Outcome} outcome + * @returns {boolean} true if inserted, false on validation or DB error + * + * @example + * recordOutcome(db, { + * modelId: "kimi-coding/k2p5", + * provider: "kimi-coding", + * unitType: "execute-task", + * unitId: "M001/S01/T01", + * succeeded: true, + * retries: 0, + * duration_ms: 12000, + * }); + */ +export function recordOutcome(db, outcome) { + try { + const { valid } = validateOutcome(outcome); + if (!valid) return false; + + const params = buildInsertParams(outcome); + const stmt = db.prepare(INSERT_SQL); + stmt.run(...params); + return true; + } catch (_err) { + return false; + } +} + +/** + * Record many outcomes in a single transaction. Fire-and-forget — never throws. + * + * Invalid rows are skipped and counted; valid rows are inserted. If the + * database supports `transaction()` (better-sqlite3 style), the inserts run + * inside it; otherwise they run sequentially. + * + * @param {object} db Duck-typed SQLite handle + * @param {Outcome[]} outcomes + * @returns {{inserted: number, skipped: number}} + * + * @example + * const r = recordOutcomeBatch(db, [outcome1, outcome2]); + * // {inserted: 2, skipped: 0} + */ +export function recordOutcomeBatch(db, outcomes) { + const result = { inserted: 0, skipped: 0 }; + + if (!Array.isArray(outcomes) || outcomes.length === 0) { + return result; + } + + try { + const stmt = db.prepare(INSERT_SQL); + + const insertAll = () => { + for (const outcome of outcomes) { + const { valid } = validateOutcome(outcome); + if (!valid) { + result.skipped += 1; + continue; + } + try { + stmt.run(...buildInsertParams(outcome)); + result.inserted += 1; + } catch (_err) { + result.skipped += 1; + } + } + }; + + if (typeof db.transaction === "function") { + const txn = db.transaction(insertAll); + txn(); + } else { + insertAll(); + } + } catch (_err) { + // db.prepare itself failed — count remaining as skipped + const remaining = outcomes.length - result.inserted - result.skipped; + if (remaining > 0) result.skipped += remaining; + } + + return result; +} + +/** + * Bootstrap the schema on a fresh database. Fire-and-forget — never throws. + * + * Uses `db.exec(sql)` if available (better-sqlite3 style) so multi-statement + * DDL works in one call. Otherwise splits on `;` and runs each statement + * via `db.prepare(stmt).run()`. + * + * @param {object} db Duck-typed SQLite handle + * @param {string} schemaSql Raw schema SQL (CREATE TABLE / CREATE INDEX ...) + * @returns {boolean} true if schema applied, false on error + * + * @example + * import {readFileSync} from "node:fs"; + * const sql = readFileSync(new URL("./outcome-schema.sql", import.meta.url), "utf8"); + * ensureSchema(db, sql); + */ +export function ensureSchema(db, schemaSql) { + if (typeof schemaSql !== "string" || schemaSql.length === 0) { + return false; + } + try { + if (typeof db.exec === "function") { + db.exec(schemaSql); + return true; + } + + const statements = schemaSql + .split(";") + .map((s) => s.trim()) + .filter((s) => s.length > 0 && !s.startsWith("--")); + + for (const stmt of statements) { + db.prepare(stmt).run(); + } + return true; + } catch (_err) { + return false; + } +} diff --git a/src/resources/extensions/gsd/learning/outcome-recorder.test.mjs b/src/resources/extensions/gsd/learning/outcome-recorder.test.mjs new file mode 100644 index 000000000..872f07d55 --- /dev/null +++ b/src/resources/extensions/gsd/learning/outcome-recorder.test.mjs @@ -0,0 +1,494 @@ +/** + * gsd-learning: outcome-recorder + outcome-aggregator tests + * + * Uses node:test with a minimal in-memory fake `db` that mimics the + * better-sqlite3 surface (`prepare(sql).run/get/all`, `exec`, + * `transaction`). The fake parses just enough SQL to verify the + * insert and aggregate semantics without spinning up real SQLite. + */ + +import { test } from "node:test"; +import assert from "node:assert/strict"; + +import { + validateOutcome, + recordOutcome, + recordOutcomeBatch, + ensureSchema, +} from "./outcome-recorder.mjs"; + +import { + aggregateOutcomes, + aggregateAllForUnitType, + totalSamples, + recentOutcomes, +} from "./outcome-aggregator.mjs"; + +// --------------------------------------------------------------------------- +// Minimal in-memory fake of better-sqlite3 +// --------------------------------------------------------------------------- + +const INSERT_COLUMNS = [ + "model_id", + "provider", + "unit_type", + "unit_id", + "succeeded", + "retries", + "escalated", + "verification_passed", + "blocker_discovered", + "duration_ms", + "tokens_total", + "cost_usd", + "recorded_at", +]; + +function createFakeDb({ throwOnPrepare = false } = {}) { + const rows = []; + let nextId = 1; + + function prepare(sql) { + if (throwOnPrepare) { + throw new Error("simulated db.prepare failure"); + } + const normalized = sql.replace(/\s+/g, " ").trim().toLowerCase(); + + if (normalized.startsWith("insert into llm_task_outcomes")) { + return { + run(...params) { + const row = { id: nextId++ }; + INSERT_COLUMNS.forEach((col, i) => { + row[col] = params[i]; + }); + rows.push(row); + return { changes: 1, lastInsertRowid: row.id }; + }, + }; + } + + if ( + normalized.startsWith("select count(*) as sample_count") || + normalized.startsWith("select count(*) as total") + ) { + return { + get(...params) { + return runAggregate(normalized, params, rows); + }, + }; + } + + if (normalized.startsWith("select model_id, count(*) as sample_count")) { + return { + all(...params) { + return runGroupedAggregate(normalized, params, rows); + }, + }; + } + + if (normalized.startsWith("select id, model_id")) { + return { + all(...params) { + return runRecentSelect(normalized, params, rows); + }, + }; + } + + // CREATE TABLE / CREATE INDEX from ensureSchema fallback path + if (normalized.startsWith("create table") || normalized.startsWith("create index")) { + return { run() { return { changes: 0 }; } }; + } + + throw new Error(`fake db: unsupported sql: ${normalized.slice(0, 80)}`); + } + + function exec(_sql) { + // no-op — schema bootstrap success path + } + + function transaction(fn) { + return function wrapped(...args) { + return fn(...args); + }; + } + + return { + prepare, + exec, + transaction, + _rows: rows, + }; +} + +function runAggregate(sql, params, rows) { + if (sql.startsWith("select count(*) as total")) { + const since = params[0]; + const filtered = rows.filter((r) => r.recorded_at > since); + return { total: filtered.length }; + } + + // single-pair aggregate: model_id, unit_type, since + const [modelId, unitType, since] = params; + const filtered = rows.filter( + (r) => r.model_id === modelId && r.unit_type === unitType && r.recorded_at > since, + ); + return summarize(filtered); +} + +function runGroupedAggregate(_sql, params, rows) { + const [unitType, since] = params; + const filtered = rows.filter((r) => r.unit_type === unitType && r.recorded_at > since); + const byModel = new Map(); + for (const row of filtered) { + if (!byModel.has(row.model_id)) byModel.set(row.model_id, []); + byModel.get(row.model_id).push(row); + } + const out = []; + for (const [modelId, modelRows] of byModel) { + out.push({ model_id: modelId, ...summarize(modelRows) }); + } + return out; +} + +function summarize(rows) { + if (rows.length === 0) { + return { + sample_count: 0, + success_rate: null, + avg_retries: null, + verification_pass_rate: null, + blocker_rate: null, + escalation_rate: null, + avg_duration_ms: null, + avg_tokens: null, + avg_cost_usd: null, + }; + } + const avg = (key, filterFn = null) => { + const vals = rows + .map((r) => r[key]) + .filter((v) => v !== null && v !== undefined && (filterFn ? filterFn(v) : true)); + if (vals.length === 0) return null; + return vals.reduce((a, b) => a + b, 0) / vals.length; + }; + const verificationVals = rows + .map((r) => r.verification_passed) + .filter((v) => v !== null && v !== undefined); + const verification_pass_rate = + verificationVals.length === 0 + ? null + : verificationVals.reduce((a, b) => a + b, 0) / verificationVals.length; + + return { + sample_count: rows.length, + success_rate: avg("succeeded"), + avg_retries: avg("retries"), + verification_pass_rate, + blocker_rate: avg("blocker_discovered"), + escalation_rate: avg("escalated"), + avg_duration_ms: avg("duration_ms"), + avg_tokens: avg("tokens_total"), + avg_cost_usd: avg("cost_usd"), + }; +} + +function runRecentSelect(sql, params, rows) { + let limit = params[params.length - 1]; + let filtered = [...rows]; + + // crude WHERE parser — match against remaining params in order of "?" + const filterParams = params.slice(0, -1); + let pi = 0; + if (sql.includes("unit_type = ?")) { + const unitType = filterParams[pi++]; + filtered = filtered.filter((r) => r.unit_type === unitType); + } + if (sql.includes("model_id = ?")) { + const modelId = filterParams[pi++]; + filtered = filtered.filter((r) => r.model_id === modelId); + } + filtered.sort((a, b) => b.recorded_at - a.recorded_at); + return filtered.slice(0, limit); +} + +// --------------------------------------------------------------------------- +// Fixture +// --------------------------------------------------------------------------- + +function minimalOutcome(overrides = {}) { + return { + modelId: "kimi-coding/k2p5", + provider: "kimi-coding", + unitType: "execute-task", + unitId: "M001/S01/T01", + succeeded: true, + ...overrides, + }; +} + +// --------------------------------------------------------------------------- +// validateOutcome +// --------------------------------------------------------------------------- + +test("validateOutcome rejects missing required fields", () => { + const result = validateOutcome({ modelId: "x", provider: "y" }); + assert.equal(result.valid, false); + assert.ok(result.errors.some((e) => e.includes("unitType"))); + assert.ok(result.errors.some((e) => e.includes("unitId"))); + assert.ok(result.errors.some((e) => e.includes("succeeded"))); +}); + +test("validateOutcome accepts minimal valid outcome", () => { + const result = validateOutcome(minimalOutcome()); + assert.equal(result.valid, true); + assert.deepEqual(result.errors, []); +}); + +test("validateOutcome rejects non-object input", () => { + assert.equal(validateOutcome(null).valid, false); + assert.equal(validateOutcome("nope").valid, false); +}); + +test("validateOutcome rejects negative retries", () => { + const result = validateOutcome(minimalOutcome({ retries: -1 })); + assert.equal(result.valid, false); +}); + +// --------------------------------------------------------------------------- +// recordOutcome +// --------------------------------------------------------------------------- + +test("recordOutcome returns true on valid outcome", () => { + const db = createFakeDb(); + const ok = recordOutcome(db, minimalOutcome()); + assert.equal(ok, true); + assert.equal(db._rows.length, 1); +}); + +test("recordOutcome returns false on invalid outcome", () => { + const db = createFakeDb(); + const ok = recordOutcome(db, { modelId: "x" }); + assert.equal(ok, false); + assert.equal(db._rows.length, 0); +}); + +test("recordOutcome returns false when db.prepare throws", () => { + const db = createFakeDb({ throwOnPrepare: true }); + const ok = recordOutcome(db, minimalOutcome()); + assert.equal(ok, false); +}); + +test("recordOutcome coerces booleans to 0/1", () => { + const db = createFakeDb(); + recordOutcome( + db, + minimalOutcome({ + succeeded: true, + escalated: false, + verification_passed: true, + blocker_discovered: false, + }), + ); + const row = db._rows[0]; + assert.equal(row.succeeded, 1); + assert.equal(row.escalated, 0); + assert.equal(row.verification_passed, 1); + assert.equal(row.blocker_discovered, 0); +}); + +test("recordOutcome preserves null verification_passed", () => { + const db = createFakeDb(); + recordOutcome(db, minimalOutcome({ verification_passed: null })); + assert.equal(db._rows[0].verification_passed, null); +}); + +test("recordOutcome defaults recorded_at to Date.now()", () => { + const db = createFakeDb(); + const before = Date.now(); + recordOutcome(db, minimalOutcome()); + const after = Date.now(); + const ts = db._rows[0].recorded_at; + assert.ok(ts >= before && ts <= after, `timestamp ${ts} outside [${before}, ${after}]`); +}); + +test("recordOutcome respects supplied recorded_at", () => { + const db = createFakeDb(); + recordOutcome(db, minimalOutcome({ recorded_at: 12345 })); + assert.equal(db._rows[0].recorded_at, 12345); +}); + +// --------------------------------------------------------------------------- +// recordOutcomeBatch +// --------------------------------------------------------------------------- + +test("recordOutcomeBatch inserts multiple outcomes in one transaction", () => { + const db = createFakeDb(); + const result = recordOutcomeBatch(db, [ + minimalOutcome({ unitId: "T01" }), + minimalOutcome({ unitId: "T02" }), + minimalOutcome({ unitId: "T03" }), + ]); + assert.deepEqual(result, { inserted: 3, skipped: 0 }); + assert.equal(db._rows.length, 3); +}); + +test("recordOutcomeBatch skips invalid rows but inserts valid ones", () => { + const db = createFakeDb(); + const result = recordOutcomeBatch(db, [ + minimalOutcome({ unitId: "T01" }), + { modelId: "broken" }, // missing required fields + minimalOutcome({ unitId: "T02" }), + ]); + assert.deepEqual(result, { inserted: 2, skipped: 1 }); +}); + +test("recordOutcomeBatch handles empty array", () => { + const db = createFakeDb(); + const result = recordOutcomeBatch(db, []); + assert.deepEqual(result, { inserted: 0, skipped: 0 }); +}); + +// --------------------------------------------------------------------------- +// ensureSchema +// --------------------------------------------------------------------------- + +test("ensureSchema returns true via db.exec path", () => { + const db = createFakeDb(); + const ok = ensureSchema(db, "CREATE TABLE foo (x INTEGER);"); + assert.equal(ok, true); +}); + +test("ensureSchema returns false on empty input", () => { + const db = createFakeDb(); + assert.equal(ensureSchema(db, ""), false); + assert.equal(ensureSchema(db, null), false); +}); + +test("ensureSchema falls back to per-statement prepare when no exec()", () => { + const db = createFakeDb(); + delete db.exec; + const ok = ensureSchema( + db, + "CREATE TABLE foo (x INTEGER); CREATE INDEX idx_foo ON foo(x);", + ); + assert.equal(ok, true); +}); + +// --------------------------------------------------------------------------- +// aggregateOutcomes +// --------------------------------------------------------------------------- + +test("aggregateOutcomes returns zeros when no samples", () => { + const db = createFakeDb(); + const stats = aggregateOutcomes(db, "ghost-model", "execute-task"); + assert.equal(stats.sample_count, 0); + assert.equal(stats.success_rate, 0); + assert.equal(stats.verification_pass_rate, null); + assert.equal(stats.window_days, 30); +}); + +test("aggregateOutcomes computes success_rate correctly from multiple rows", () => { + const db = createFakeDb(); + const now = Date.now(); + // 3 successes, 1 failure → 0.75 + recordOutcome(db, minimalOutcome({ succeeded: true, recorded_at: now - 1000 })); + recordOutcome(db, minimalOutcome({ succeeded: true, recorded_at: now - 1000 })); + recordOutcome(db, minimalOutcome({ succeeded: true, recorded_at: now - 1000 })); + recordOutcome(db, minimalOutcome({ succeeded: false, recorded_at: now - 1000 })); + + const stats = aggregateOutcomes(db, "kimi-coding/k2p5", "execute-task", { now }); + assert.equal(stats.sample_count, 4); + assert.equal(stats.success_rate, 0.75); +}); + +test("aggregateOutcomes excludes rows outside the rolling window", () => { + const db = createFakeDb(); + const now = Date.now(); + const oneDayMs = 24 * 60 * 60 * 1000; + // inside window + recordOutcome(db, minimalOutcome({ succeeded: true, recorded_at: now - oneDayMs })); + // outside 30-day window + recordOutcome(db, minimalOutcome({ succeeded: false, recorded_at: now - 60 * oneDayMs })); + + const stats = aggregateOutcomes(db, "kimi-coding/k2p5", "execute-task", { now, rollingDays: 30 }); + assert.equal(stats.sample_count, 1); + assert.equal(stats.success_rate, 1); +}); + +test("aggregateOutcomes verification_pass_rate is null when no verification data", () => { + const db = createFakeDb(); + const now = Date.now(); + recordOutcome(db, minimalOutcome({ verification_passed: null, recorded_at: now - 1000 })); + const stats = aggregateOutcomes(db, "kimi-coding/k2p5", "execute-task", { now }); + assert.equal(stats.verification_pass_rate, null); +}); + +// --------------------------------------------------------------------------- +// aggregateAllForUnitType +// --------------------------------------------------------------------------- + +test("aggregateAllForUnitType returns a Map keyed by modelId", () => { + const db = createFakeDb(); + const now = Date.now(); + recordOutcome(db, minimalOutcome({ modelId: "model-a", succeeded: true, recorded_at: now - 1000 })); + recordOutcome(db, minimalOutcome({ modelId: "model-a", succeeded: false, recorded_at: now - 1000 })); + recordOutcome(db, minimalOutcome({ modelId: "model-b", succeeded: true, recorded_at: now - 1000 })); + + const ranking = aggregateAllForUnitType(db, "execute-task", { now }); + assert.ok(ranking instanceof Map); + assert.equal(ranking.size, 2); + assert.equal(ranking.get("model-a").sample_count, 2); + assert.equal(ranking.get("model-a").success_rate, 0.5); + assert.equal(ranking.get("model-b").sample_count, 1); + assert.equal(ranking.get("model-b").success_rate, 1); +}); + +test("aggregateAllForUnitType returns empty Map when no rows", () => { + const db = createFakeDb(); + const ranking = aggregateAllForUnitType(db, "ghost-unit"); + assert.equal(ranking.size, 0); +}); + +// --------------------------------------------------------------------------- +// totalSamples +// --------------------------------------------------------------------------- + +test("totalSamples counts correctly across the rolling window", () => { + const db = createFakeDb(); + const now = Date.now(); + const oneDayMs = 24 * 60 * 60 * 1000; + recordOutcome(db, minimalOutcome({ recorded_at: now - 1000 })); + recordOutcome(db, minimalOutcome({ recorded_at: now - 5 * oneDayMs })); + recordOutcome(db, minimalOutcome({ recorded_at: now - 60 * oneDayMs })); // outside + + assert.equal(totalSamples(db, { now, rollingDays: 30 }), 2); +}); + +// --------------------------------------------------------------------------- +// recentOutcomes +// --------------------------------------------------------------------------- + +test("recentOutcomes returns rows ordered by recorded_at DESC", () => { + const db = createFakeDb(); + recordOutcome(db, minimalOutcome({ unitId: "T01", recorded_at: 1000 })); + recordOutcome(db, minimalOutcome({ unitId: "T02", recorded_at: 3000 })); + recordOutcome(db, minimalOutcome({ unitId: "T03", recorded_at: 2000 })); + + const recent = recentOutcomes(db, { limit: 10 }); + assert.equal(recent.length, 3); + assert.equal(recent[0].unit_id, "T02"); + assert.equal(recent[1].unit_id, "T03"); + assert.equal(recent[2].unit_id, "T01"); +}); + +test("recentOutcomes respects limit and filters", () => { + const db = createFakeDb(); + recordOutcome(db, minimalOutcome({ modelId: "a", unitType: "execute-task", recorded_at: 1000 })); + recordOutcome(db, minimalOutcome({ modelId: "b", unitType: "execute-task", recorded_at: 2000 })); + recordOutcome(db, minimalOutcome({ modelId: "a", unitType: "plan-slice", recorded_at: 3000 })); + + const filtered = recentOutcomes(db, { limit: 10, unitType: "execute-task", modelId: "a" }); + assert.equal(filtered.length, 1); + assert.equal(filtered[0].model_id, "a"); + assert.equal(filtered[0].unit_type, "execute-task"); +}); diff --git a/src/resources/extensions/gsd/learning/outcome-schema.sql b/src/resources/extensions/gsd/learning/outcome-schema.sql new file mode 100644 index 000000000..c9f89f8ea --- /dev/null +++ b/src/resources/extensions/gsd/learning/outcome-schema.sql @@ -0,0 +1,30 @@ +-- gsd-learning: llm_task_outcomes +-- Records per-unit LLM dispatch outcomes for Bayesian learning. +-- Shape is compatible with ace-coder's approved 2026-03-06 design so +-- cross-project data sharing can happen later without migration pain. + +CREATE TABLE IF NOT EXISTS llm_task_outcomes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + model_id TEXT NOT NULL, + provider TEXT NOT NULL, + unit_type TEXT NOT NULL, + unit_id TEXT NOT NULL, + succeeded INTEGER NOT NULL CHECK (succeeded IN (0, 1)), + retries INTEGER NOT NULL DEFAULT 0, + escalated INTEGER NOT NULL DEFAULT 0 CHECK (escalated IN (0, 1)), + verification_passed INTEGER CHECK (verification_passed IS NULL OR verification_passed IN (0, 1)), + blocker_discovered INTEGER NOT NULL DEFAULT 0 CHECK (blocker_discovered IN (0, 1)), + duration_ms INTEGER, + tokens_total INTEGER, + cost_usd REAL, + recorded_at INTEGER NOT NULL +); + +CREATE INDEX IF NOT EXISTS idx_outcomes_model_unit_time + ON llm_task_outcomes (model_id, unit_type, recorded_at DESC); + +CREATE INDEX IF NOT EXISTS idx_outcomes_unit_time + ON llm_task_outcomes (unit_type, recorded_at DESC); + +CREATE INDEX IF NOT EXISTS idx_outcomes_provider_time + ON llm_task_outcomes (provider, recorded_at DESC); diff --git a/src/resources/extensions/gsd/learning/runtime.ts b/src/resources/extensions/gsd/learning/runtime.ts new file mode 100644 index 000000000..ae7f0f9c2 --- /dev/null +++ b/src/resources/extensions/gsd/learning/runtime.ts @@ -0,0 +1,98 @@ +import { getDatabase, getDbPath, insertLlmTaskOutcome, type LlmTaskOutcomeInput } from "../gsd-db.js"; +import { logWarning } from "../workflow-logger.js"; +import { loadCapabilityOverrides } from "./loadCapabilityOverrides.mjs"; +import { createBeforeModelSelectHandler } from "./hook-handler.mjs"; +import { validateOutcome } from "./outcome-recorder.mjs"; + +interface BeforeModelSelectInput { + unitType: string; + eligibleModels: string[]; + phaseConfig?: { primary: string; fallbacks: string[] }; +} + +interface BeforeModelSelectResult { + modelId: string; +} + +type ModelSelectHandler = (input: BeforeModelSelectInput) => Promise; + +const DEFAULT_N_PRIOR = 10; +const DEFAULT_ROLLING_DAYS = 30; +const DEFAULT_UCB_C = 1.4; + +let cachedHandler: ModelSelectHandler | null = null; +let cachedDbPath: string | null = null; +let cachedDb: object | null = null; +let initPromise: Promise | null = null; + +async function ensureLearningReady(): Promise { + const db = getDatabase(); + const dbPath = getDbPath(); + if (!db || !dbPath) { + cachedHandler = null; + cachedDbPath = null; + cachedDb = null; + return; + } + if (cachedHandler && cachedDbPath === dbPath && cachedDb === db) return; + if (initPromise) return initPromise; + + initPromise = (async () => { + try { + const priors = await loadCapabilityOverrides(); + cachedHandler = createBeforeModelSelectHandler({ + db, + overrides: priors.overrides, + weights: priors.weights, + benchmarks: priors.benchmarks, + opts: { + nPrior: DEFAULT_N_PRIOR, + rollingDays: DEFAULT_ROLLING_DAYS, + ucbC: DEFAULT_UCB_C, + explorationEnabled: true, + }, + }) as ModelSelectHandler; + cachedDbPath = dbPath; + cachedDb = db; + } catch (err) { + cachedHandler = null; + cachedDbPath = null; + cachedDb = null; + logWarning("dispatch", `failed to initialize learned routing: ${err instanceof Error ? err.message : String(err)}`); + } finally { + initPromise = null; + } + })(); + + return initPromise; +} + +export async function initializeLearningRuntime(): Promise { + await ensureLearningReady(); +} + +export function resetLearningRuntime(): void { + cachedHandler = null; + cachedDbPath = null; + cachedDb = null; + initPromise = null; +} + +export async function selectLearnedModel( + input: BeforeModelSelectInput, +): Promise { + await ensureLearningReady(); + if (!cachedHandler) return undefined; + return cachedHandler(input); +} + +export function recordLearnedOutcome(input: LlmTaskOutcomeInput): boolean { + const validation = validateOutcome(input as any); + if (!validation.valid) return false; + try { + return insertLlmTaskOutcome(input); + } catch (err) { + logWarning("db", `failed to record learned routing outcome: ${err instanceof Error ? err.message : String(err)}`); + return false; + } +} diff --git a/src/resources/extensions/gsd/memory-extractor.ts b/src/resources/extensions/gsd/memory-extractor.ts index acca3c7a0..3020b3cc4 100644 --- a/src/resources/extensions/gsd/memory-extractor.ts +++ b/src/resources/extensions/gsd/memory-extractor.ts @@ -5,8 +5,8 @@ // (mutex guard). Fire-and-forget — never blocks auto-mode. import { readFileSync, statSync } from 'node:fs'; -import type { ExtensionContext } from '@gsd/pi-coding-agent'; -import type { Api, AssistantMessage, Model } from '@gsd/pi-ai'; +import type { ExtensionContext } from '@sf-run/pi-coding-agent'; +import type { Api, AssistantMessage, Model } from '@sf-run/pi-ai'; import { getActiveMemories, isUnitProcessed, @@ -90,11 +90,11 @@ export function buildMemoryLLMCall(ctx: ExtensionContext): LLMCallFn | null { // Resolve API key via modelRegistry so OAuth tokens (auth.json) are used. // Without this, streamSimpleAnthropic only checks env vars via getEnvApiKey, // which returns undefined for OAuth users (Claude Max / Claude Pro). - // See: https://github.com/gsd-build/gsd-2/issues/2959 + // See: https://github.com/singularity-forge/sf-run/issues/2959 const resolvedKeyPromise = ctx.modelRegistry.getApiKey(selectedModel).catch(() => undefined); return async (system: string, user: string): Promise => { - const { completeSimple } = await import('@gsd/pi-ai'); + const { completeSimple } = await import('@sf-run/pi-ai'); const resolvedApiKey = await resolvedKeyPromise; const result: AssistantMessage = await completeSimple(selectedModel, { systemPrompt: system, diff --git a/src/resources/extensions/gsd/metrics.ts b/src/resources/extensions/gsd/metrics.ts index 0bccfe52d..42edda37a 100644 --- a/src/resources/extensions/gsd/metrics.ts +++ b/src/resources/extensions/gsd/metrics.ts @@ -14,19 +14,72 @@ */ import { join } from "node:path"; -import type { ExtensionContext } from "@gsd/pi-coding-agent"; +import type { ExtensionContext } from "@sf-run/pi-coding-agent"; import { gsdRoot } from "./paths.js"; import { getAndClearSkills } from "./skill-telemetry.js"; import { loadJsonFile, loadJsonFileOrNull, saveJsonFile } from "./json-persistence.js"; import { parseUnitId } from "./unit-id.js"; import { buildAuditEnvelope, emitUokAuditEvent } from "./uok/audit.js"; import { isUnifiedAuditEnabled } from "./uok/audit-toggle.js"; +import { getDatabase } from "./gsd-db.js"; // Re-export from shared — import directly from format-utils to avoid pulling -// in the full barrel (mod.js → ui.js → @gsd/pi-tui) which breaks when loaded +// in the full barrel (mod.js → ui.js → @sf-run/pi-tui) which breaks when loaded // outside jiti's alias resolution (e.g. dynamic import in auto-loop reports). export { formatTokenCount } from "../shared/format-utils.js"; +// ─── Learning Integration ───────────────────────────────────────────────────── + +/** + * Infer provider from a model ID when not explicitly prefixed with "provider/". + */ +function inferProviderFromBareModelId(modelId: string): string { + const lower = modelId.toLowerCase(); + if (lower === "k2p5" || lower === "kimi-k2-thinking") return "kimi-coding"; + if (lower.startsWith("minimax-m")) return "ollama-cloud"; + if (lower.startsWith("minimax-") || modelId.startsWith("MiniMax-")) return "minimax"; + if (lower.startsWith("glm-")) return "zai"; + if (lower.startsWith("mimo-")) return "xiaomi-token-plan-ams"; + if (lower.startsWith("gemini-")) return "google-gemini-cli"; + if (lower.startsWith("magistral-") || lower.startsWith("mistral-") || lower.startsWith("devstral-") || lower.startsWith("codestral-") || lower.startsWith("ministral-") || lower.startsWith("pixtral-")) return "mistral"; + return "unknown"; +} + +/** + * Record a unit outcome to the llm_task_outcomes table for Bayesian learning. + */ +async function recordUnitOutcome(unit: UnitMetrics): Promise { + const db = getDatabase(); + if (!db) return; + + try { + const { recordOutcome } = await import("./learning/outcome-recorder.mjs"); + let provider: string; + let modelId = unit.model; + if (modelId.includes("/")) { + [provider] = modelId.split("/"); + } else { + provider = inferProviderFromBareModelId(modelId); + } + + recordOutcome(db, { + modelId, + provider, + unitType: unit.type, + unitId: unit.id, + succeeded: true, // metrics.json entry implies completion + retries: 0, // TODO: extract from session entries if possible + escalated: !!unit.modelDowngraded, + verification_passed: null, + blocker_discovered: false, + duration_ms: unit.finishedAt - unit.startedAt, + tokens_total: unit.tokens.total, + cost_usd: unit.cost, + recorded_at: unit.startedAt, + }); + } catch { /* fire-and-forget */ } +} + // ─── Types ──────────────────────────────────────────────────────────────────── export interface TokenCounts { @@ -240,6 +293,9 @@ export function snapshotUnitMetrics( } saveLedger(basePath, ledger); + // Background outcome recording for Bayesian learning + recordUnitOutcome(unit).catch(() => { /* fire-and-forget */ }); + if (isUnifiedAuditEnabled()) { emitUokAuditEvent( basePath, diff --git a/src/resources/extensions/gsd/migrate/command.ts b/src/resources/extensions/gsd/migrate/command.ts index f2567c640..76007ff9e 100644 --- a/src/resources/extensions/gsd/migrate/command.ts +++ b/src/resources/extensions/gsd/migrate/command.ts @@ -9,7 +9,7 @@ * output for GSD-2 standards compliance. */ -import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI, ExtensionCommandContext } from "@sf-run/pi-coding-agent"; import { existsSync, readFileSync } from "node:fs"; import { resolve, join, dirname } from "node:path"; import { gsdRoot } from "../paths.js"; diff --git a/src/resources/extensions/gsd/model-router.ts b/src/resources/extensions/gsd/model-router.ts index cc915877a..cc80f9a4e 100644 --- a/src/resources/extensions/gsd/model-router.ts +++ b/src/resources/extensions/gsd/model-router.ts @@ -5,9 +5,9 @@ import type { ComplexityTier, ClassificationResult, TaskMetadata } from "./complexity-classifier.js"; import { tierOrdinal } from "./complexity-classifier.js"; import type { ResolvedModelConfig } from "./preferences.js"; -import { getProviderCapabilities, type ProviderCapabilities } from "@gsd/pi-ai"; -import { getToolCompatibility, getAllToolCompatibility } from "@gsd/pi-coding-agent"; -import type { ToolCompatibility } from "@gsd/pi-coding-agent"; +import { getProviderCapabilities, type ProviderCapabilities } from "@sf-run/pi-ai"; +import { getToolCompatibility, getAllToolCompatibility } from "@sf-run/pi-coding-agent"; +import type { ToolCompatibility } from "@sf-run/pi-coding-agent"; // ─── Types ─────────────────────────────────────────────────────────────────── diff --git a/src/resources/extensions/gsd/native-git-bridge.ts b/src/resources/extensions/gsd/native-git-bridge.ts index 2e944f87b..5ba18a2a6 100644 --- a/src/resources/extensions/gsd/native-git-bridge.ts +++ b/src/resources/extensions/gsd/native-git-bridge.ts @@ -121,7 +121,7 @@ function loadNative(): typeof nativeModule { try { // eslint-disable-next-line @typescript-eslint/no-require-imports - const mod = require("@gsd/native"); + const mod = require("@sf-run/native"); if (mod.gitCurrentBranch && mod.gitHasChanges) { nativeModule = mod; } diff --git a/src/resources/extensions/gsd/native-parser-bridge.ts b/src/resources/extensions/gsd/native-parser-bridge.ts index 0f4b8b69c..fc1bb9b75 100644 --- a/src/resources/extensions/gsd/native-parser-bridge.ts +++ b/src/resources/extensions/gsd/native-parser-bridge.ts @@ -38,7 +38,7 @@ function loadNative(): typeof nativeModule { try { // Dynamic import to avoid hard dependency - fails gracefully if native module not built // eslint-disable-next-line @typescript-eslint/no-require-imports - const mod = require('@gsd/native'); + const mod = require('@sf-run/native'); if (mod.parseFrontmatter && mod.extractSection && mod.batchParseGsdFiles) { nativeModule = mod; } diff --git a/src/resources/extensions/gsd/notification-overlay.ts b/src/resources/extensions/gsd/notification-overlay.ts index 98d34785a..01ebf02d8 100644 --- a/src/resources/extensions/gsd/notification-overlay.ts +++ b/src/resources/extensions/gsd/notification-overlay.ts @@ -2,8 +2,8 @@ // Scrollable panel showing all persisted notifications with severity filtering. // Toggled with Ctrl+Alt+N (⌃⌥N on macOS), Ctrl+Shift+N fallback, or /gsd notifications. -import type { Theme } from "@gsd/pi-coding-agent"; -import { truncateToWidth, visibleWidth, matchesKey, Key } from "@gsd/pi-tui"; +import type { Theme } from "@sf-run/pi-coding-agent"; +import { truncateToWidth, visibleWidth, matchesKey, Key } from "@sf-run/pi-tui"; import { readNotifications, diff --git a/src/resources/extensions/gsd/notification-widget.ts b/src/resources/extensions/gsd/notification-widget.ts index ce62e9eca..2ee58c314 100644 --- a/src/resources/extensions/gsd/notification-widget.ts +++ b/src/resources/extensions/gsd/notification-widget.ts @@ -3,7 +3,7 @@ // the most recent notification message. Refreshes every 30 seconds. // Widget key: "gsd-notifications", placement: "belowEditor" -import type { ExtensionContext } from "@gsd/pi-coding-agent"; +import type { ExtensionContext } from "@sf-run/pi-coding-agent"; import { getUnreadCount, onNotificationStoreChange } from "./notification-store.js"; import { formattedShortcutPair } from "./shortcut-defs.js"; diff --git a/src/resources/extensions/gsd/notifications.ts b/src/resources/extensions/gsd/notifications.ts index 0efd0d4c3..bbfb8315e 100644 --- a/src/resources/extensions/gsd/notifications.ts +++ b/src/resources/extensions/gsd/notifications.ts @@ -94,7 +94,7 @@ export function buildDesktopNotificationCommand( // so it gets a proper permission entry in System Settings → Notifications. // osascript notifications are silently swallowed when the calling terminal // (Ghostty, iTerm2, etc.) lacks notification permissions — exits 0, no error. - // See: https://github.com/gsd-build/gsd-2/issues/2632 + // See: https://github.com/singularity-forge/sf-run/issues/2632 const tnPath = findExecutable("terminal-notifier"); if (tnPath) { const sound = level === "error" ? "Basso" : "Glass"; diff --git a/src/resources/extensions/gsd/parallel-merge.ts b/src/resources/extensions/gsd/parallel-merge.ts index 09a179869..ef97a0c80 100644 --- a/src/resources/extensions/gsd/parallel-merge.ts +++ b/src/resources/extensions/gsd/parallel-merge.ts @@ -81,7 +81,7 @@ function discoverDbCompletedMilestones(basePath: string): Set { * When basePath is provided, also checks worktree SQLite DBs as the * source of truth — workers with stale orchestrator state (e.g. "error") * are included if their worktree DB shows status='complete'. - * See: https://github.com/gsd-build/gsd-2/issues/2812 + * See: https://github.com/singularity-forge/sf-run/issues/2812 */ export function determineMergeOrder( workers: WorkerInfo[], diff --git a/src/resources/extensions/gsd/parallel-monitor-overlay.ts b/src/resources/extensions/gsd/parallel-monitor-overlay.ts index 4d49872b2..d460f90f9 100644 --- a/src/resources/extensions/gsd/parallel-monitor-overlay.ts +++ b/src/resources/extensions/gsd/parallel-monitor-overlay.ts @@ -12,8 +12,8 @@ import { existsSync, statSync, readFileSync, openSync, readSync, closeSync, read import { join } from "node:path"; import { spawnSync } from "node:child_process"; -import type { Theme } from "@gsd/pi-coding-agent"; -import { truncateToWidth, visibleWidth, matchesKey, Key } from "@gsd/pi-tui"; +import type { Theme } from "@sf-run/pi-coding-agent"; +import { truncateToWidth, visibleWidth, matchesKey, Key } from "@sf-run/pi-tui"; import { formatDuration, STATUS_GLYPH, STATUS_COLOR } from "../shared/mod.js"; import { formattedShortcutPair } from "./shortcut-defs.js"; diff --git a/src/resources/extensions/gsd/preferences-models.ts b/src/resources/extensions/gsd/preferences-models.ts index bce92a82f..0b83da99e 100644 --- a/src/resources/extensions/gsd/preferences-models.ts +++ b/src/resources/extensions/gsd/preferences-models.ts @@ -341,6 +341,11 @@ export function resolveDynamicRoutingConfig(): DynamicRoutingConfig { }; } +export function resolvePersistModelChanges(): boolean { + const prefs = loadEffectiveGSDPreferences(); + return prefs?.preferences.persist_model_changes !== false; +} + export function resolveAutoSupervisorConfig(): AutoSupervisorConfig { const prefs = loadEffectiveGSDPreferences(); const configured = prefs?.preferences.auto_supervisor ?? {}; diff --git a/src/resources/extensions/gsd/preferences-types.ts b/src/resources/extensions/gsd/preferences-types.ts index 430c7fe85..a9802def2 100644 --- a/src/resources/extensions/gsd/preferences-types.ts +++ b/src/resources/extensions/gsd/preferences-types.ts @@ -68,6 +68,7 @@ export const KNOWN_PREFERENCE_KEYS = new Set([ "skill_rules", "custom_instructions", "models", + "persist_model_changes", "skill_discovery", "skill_staleness_days", "auto_supervisor", @@ -271,6 +272,8 @@ export interface GSDPreferences { skill_rules?: GSDSkillRule[]; custom_instructions?: string[]; models?: GSDModelConfig | GSDModelConfigV2; + /** Persist model changes to default provider/model. Default: true. */ + persist_model_changes?: boolean; skill_discovery?: SkillDiscoveryMode; skill_staleness_days?: number; // Skills unused for N days get deprioritized (#599). 0 = disabled. Default: 60. auto_supervisor?: AutoSupervisorConfig; @@ -300,8 +303,8 @@ export interface GSDPreferences { verification_commands?: string[]; verification_auto_fix?: boolean; verification_max_retries?: number; - /** Search provider preference. "brave"/"tavily"/"ollama" force that backend and disable native Anthropic search. "native" forces native only. "auto" = current default behavior. */ - search_provider?: "brave" | "tavily" | "ollama" | "native" | "auto"; + /** Search provider preference. "brave"/"tavily"/"ollama"/"combosearch" force that backend and disable native Anthropic search. "native" forces native only. "auto" = current default behavior. */ + search_provider?: "brave" | "tavily" | "ollama" | "combosearch" | "native" | "auto"; /** Context selection mode for file inlining. "full" inlines entire files, "smart" uses semantic chunking. Default derived from token profile. */ context_selection?: ContextSelectionMode; /** Default widget display mode for auto-mode dashboard. "full" | "small" | "min" | "off". Default: "full". */ diff --git a/src/resources/extensions/gsd/preferences-validation.ts b/src/resources/extensions/gsd/preferences-validation.ts index 78faebf96..619b54bf1 100644 --- a/src/resources/extensions/gsd/preferences-validation.ts +++ b/src/resources/extensions/gsd/preferences-validation.ts @@ -145,6 +145,14 @@ export function validatePreferences(preferences: GSDPreferences): { validated.unique_milestone_ids = !!preferences.unique_milestone_ids; } + if (preferences.persist_model_changes !== undefined) { + if (typeof preferences.persist_model_changes === "boolean") { + validated.persist_model_changes = preferences.persist_model_changes; + } else { + errors.push("persist_model_changes must be a boolean"); + } + } + if (preferences.budget_ceiling !== undefined) { const raw = preferences.budget_ceiling; if (typeof raw === "number" && Number.isFinite(raw)) { @@ -277,11 +285,11 @@ export function validatePreferences(preferences: GSDPreferences): { // ─── Search Provider ───────────────────────────────────────────── if (preferences.search_provider !== undefined) { - const validSearchProviders = new Set(["brave", "tavily", "ollama", "native", "auto"]); + const validSearchProviders = new Set(["brave", "tavily", "ollama", "combosearch", "native", "auto"]); if (typeof preferences.search_provider === "string" && validSearchProviders.has(preferences.search_provider)) { validated.search_provider = preferences.search_provider as GSDPreferences["search_provider"]; } else { - errors.push(`search_provider must be one of: brave, tavily, ollama, native, auto`); + errors.push(`search_provider must be one of: brave, tavily, ollama, combosearch, native, auto`); } } diff --git a/src/resources/extensions/gsd/preferences.ts b/src/resources/extensions/gsd/preferences.ts index 414a5f0c8..b5dd6e840 100644 --- a/src/resources/extensions/gsd/preferences.ts +++ b/src/resources/extensions/gsd/preferences.ts @@ -82,6 +82,7 @@ export function resolveSkillStalenessDays(): number { export { resolveModelForUnit, resolveModelWithFallbacksForUnit, + resolvePersistModelChanges, getNextFallbackModel, isTransientNetworkError, validateModelId, @@ -244,7 +245,7 @@ export function parsePreferencesMarkdown(content: string): GSDPreferences | null _warnedUnrecognizedFormat = true; console.warn( "[GSD] Warning: preferences file has unrecognized format — content does not use YAML frontmatter delimiters (---). " + - "Wrap your preferences in --- fences. See https://github.com/gsd-build/gsd-2/issues/2036", + "Wrap your preferences in --- fences. See https://github.com/singularity-forge/sf-run/issues/2036", ); } return null; @@ -355,6 +356,7 @@ function mergePreferences(base: GSDPreferences, override: GSDPreferences): GSDPr skill_rules: [...(base.skill_rules ?? []), ...(override.skill_rules ?? [])], custom_instructions: mergeStringLists(base.custom_instructions, override.custom_instructions), models: { ...(base.models ?? {}), ...(override.models ?? {}) }, + persist_model_changes: override.persist_model_changes ?? base.persist_model_changes, skill_discovery: override.skill_discovery ?? base.skill_discovery, skill_staleness_days: override.skill_staleness_days ?? base.skill_staleness_days, auto_supervisor: { ...(base.auto_supervisor ?? {}), ...(override.auto_supervisor ?? {}) }, diff --git a/src/resources/extensions/gsd/prompt-loader.ts b/src/resources/extensions/gsd/prompt-loader.ts index f3f75b76d..be212ba98 100644 --- a/src/resources/extensions/gsd/prompt-loader.ts +++ b/src/resources/extensions/gsd/prompt-loader.ts @@ -150,7 +150,7 @@ export function loadPrompt(name: string, vars: Record = {}): str // Use split/join instead of replaceAll to avoid JavaScript's special // replacement patterns ($', $`, $&) being interpreted in the value. - // See: https://github.com/gsd-build/gsd-2/issues/2968 + // See: https://github.com/singularity-forge/sf-run/issues/2968 content = content.split(`{{${key}}}`).join(safeValue); } diff --git a/src/resources/extensions/gsd/prompts/forensics.md b/src/resources/extensions/gsd/prompts/forensics.md index ffcd01151..0ba20a3b8 100644 --- a/src/resources/extensions/gsd/prompts/forensics.md +++ b/src/resources/extensions/gsd/prompts/forensics.md @@ -137,9 +137,9 @@ Explain your findings: - **Code snippet** — the problematic code and what it should do instead - **Recovery** — what the user can do right now to get unstuck -Then **offer GitHub issue creation**: "Would you like me to create a GitHub issue for this on gsd-build/gsd-2?" +Then **offer GitHub issue creation**: "Would you like me to create a GitHub issue for this on singularity-forge/sf-run?" -**CRITICAL: The `github_issues` tool ONLY targets the current user's repository — it has no `repo` parameter. You MUST use `gh issue create --repo gsd-build/gsd-2` via the `bash` tool to file on the correct repo. Do NOT use the `github_issues` tool for this.** +**CRITICAL: The `github_issues` tool ONLY targets the current user's repository — it has no `repo` parameter. You MUST use `gh issue create --repo singularity-forge/sf-run` via the `bash` tool to file on the correct repo. Do NOT use the `github_issues` tool for this.** If yes, create using the `bash` tool: @@ -172,7 +172,7 @@ cat > /tmp/gsd-forensic-issue.md << 'GSD_ISSUE_BODY' *Auto-generated by `/gsd forensics`* GSD_ISSUE_BODY -ISSUE_URL=$(gh issue create --repo gsd-build/gsd-2 \ +ISSUE_URL=$(gh issue create --repo singularity-forge/sf-run \ --title "..." \ --label "auto-generated" \ --body-file /tmp/gsd-forensic-issue.md) diff --git a/src/resources/extensions/gsd/queue-reorder-ui.ts b/src/resources/extensions/gsd/queue-reorder-ui.ts index 37ff600a1..1bb6d4654 100644 --- a/src/resources/extensions/gsd/queue-reorder-ui.ts +++ b/src/resources/extensions/gsd/queue-reorder-ui.ts @@ -8,9 +8,9 @@ * Conflicting depends_on entries are auto-removed on confirm. */ -import type { ExtensionContext } from "@gsd/pi-coding-agent"; -import { type Theme } from "@gsd/pi-coding-agent"; -import { Key, matchesKey, truncateToWidth, type TUI } from "@gsd/pi-tui"; +import type { ExtensionContext } from "@sf-run/pi-coding-agent"; +import { type Theme } from "@sf-run/pi-coding-agent"; +import { Key, matchesKey, truncateToWidth, type TUI } from "@sf-run/pi-tui"; import { makeUI } from "../shared/tui.js"; import { GLYPH } from "../shared/mod.js"; import { validateQueueOrder, type DependencyValidation } from "./queue-order.js"; diff --git a/src/resources/extensions/gsd/quick.ts b/src/resources/extensions/gsd/quick.ts index ad513e46d..f595622ee 100644 --- a/src/resources/extensions/gsd/quick.ts +++ b/src/resources/extensions/gsd/quick.ts @@ -9,7 +9,7 @@ * "Quick Tasks Completed" table. */ -import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI, ExtensionCommandContext } from "@sf-run/pi-coding-agent"; import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { loadPrompt } from "./prompt-loader.js"; diff --git a/src/resources/extensions/gsd/rethink.ts b/src/resources/extensions/gsd/rethink.ts index 1f7d3e0dd..ad1888492 100644 --- a/src/resources/extensions/gsd/rethink.ts +++ b/src/resources/extensions/gsd/rethink.ts @@ -7,7 +7,7 @@ * through conversation. */ -import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI, ExtensionCommandContext } from "@sf-run/pi-coding-agent"; import { existsSync } from "node:fs"; import { isAutoActive } from "./auto.js"; diff --git a/src/resources/extensions/gsd/service-tier.ts b/src/resources/extensions/gsd/service-tier.ts index fd4c959d6..16de2fcf2 100644 --- a/src/resources/extensions/gsd/service-tier.ts +++ b/src/resources/extensions/gsd/service-tier.ts @@ -8,7 +8,7 @@ * use a single source of truth. */ -import type { ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import type { ExtensionCommandContext } from "@sf-run/pi-coding-agent"; import { existsSync, readFileSync } from "node:fs"; import { saveFile } from "./files.js"; @@ -35,7 +35,7 @@ const SERVICE_TIER_SCOPE_NOTE = "Only affects gpt-5.4 models, regardless of prov * (set via CAPABILITY_PATCHES in packages/pi-ai/src/models.ts). When callers * have access to the full Model object, prefer reading capabilities directly. * - * See: https://github.com/gsd-build/gsd-2/issues/2546 + * See: https://github.com/singularity-forge/sf-run/issues/2546 */ const SERVICE_TIER_MODEL_PREFIXES = ["gpt-5.4"] as const; diff --git a/src/resources/extensions/gsd/session-forensics.ts b/src/resources/extensions/gsd/session-forensics.ts index 9cf0a23c5..3cbdd272b 100644 --- a/src/resources/extensions/gsd/session-forensics.ts +++ b/src/resources/extensions/gsd/session-forensics.ts @@ -478,7 +478,7 @@ function formatTraceSummary(trace: ExecutionTrace): string { // when the previous turn was truncated or malformed. Crash recovery has its // own path (formatCrashRecoveryBriefing) that handles lastReasoning safely // with explicit "Last Agent Reasoning Before Interruption" framing. - // See: https://github.com/gsd-build/gsd-2/issues/2195 + // See: https://github.com/singularity-forge/sf-run/issues/2195 return parts.join("\n"); } diff --git a/src/resources/extensions/gsd/skill-catalog.ts b/src/resources/extensions/gsd/skill-catalog.ts index 7a061b067..218d38eca 100644 --- a/src/resources/extensions/gsd/skill-catalog.ts +++ b/src/resources/extensions/gsd/skill-catalog.ts @@ -16,7 +16,7 @@ import { execFile } from "node:child_process"; import { existsSync } from "node:fs"; import { join } from "node:path"; import { homedir } from "node:os"; -import type { ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import type { ExtensionCommandContext } from "@sf-run/pi-coding-agent"; import { showNextAction } from "../shared/tui.js"; import type { ProjectSignals, XcodePlatform } from "./detection.js"; diff --git a/src/resources/extensions/gsd/tests/budget-prediction.test.ts b/src/resources/extensions/gsd/tests/budget-prediction.test.ts index 52c05a0a6..cee13b8f9 100644 --- a/src/resources/extensions/gsd/tests/budget-prediction.test.ts +++ b/src/resources/extensions/gsd/tests/budget-prediction.test.ts @@ -17,7 +17,7 @@ const metricsSrc = readFileSync(join(__dirname, "..", "metrics.ts"), "utf-8"); const dashboardSrc = readFileSync(join(__dirname, "..", "auto-dashboard.ts"), "utf-8"); // ─── Extract pure functions from metrics.ts source ──────────────────────── -// Can't import directly due to paths.js → @gsd/pi-coding-agent import chain. +// Can't import directly due to paths.js → @sf-run/pi-coding-agent import chain. // Extract and evaluate the pure math functions. interface MockUnitMetrics { diff --git a/src/resources/extensions/gsd/tests/claude-import-marketplace-discovery.test.ts b/src/resources/extensions/gsd/tests/claude-import-marketplace-discovery.test.ts index 920b881b6..e889d8fec 100644 --- a/src/resources/extensions/gsd/tests/claude-import-marketplace-discovery.test.ts +++ b/src/resources/extensions/gsd/tests/claude-import-marketplace-discovery.test.ts @@ -8,7 +8,7 @@ * * Uses temp-dir fixtures — no real marketplace repos required. * - * Fixes: https://github.com/gsd-build/gsd-2/issues/2717 + * Fixes: https://github.com/singularity-forge/sf-run/issues/2717 */ import { describe, it, beforeEach, afterEach } from "node:test"; diff --git a/src/resources/extensions/gsd/tests/claude-import-tui.test.ts b/src/resources/extensions/gsd/tests/claude-import-tui.test.ts index 53a4284fa..40585083e 100644 --- a/src/resources/extensions/gsd/tests/claude-import-tui.test.ts +++ b/src/resources/extensions/gsd/tests/claude-import-tui.test.ts @@ -13,7 +13,7 @@ import assert from 'node:assert'; import { existsSync, mkdtempSync, rmSync, writeFileSync, readFileSync, mkdirSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import type { ExtensionCommandContext } from '@gsd/pi-coding-agent'; +import type { ExtensionCommandContext } from '@sf-run/pi-coding-agent'; import { runClaudeImportFlow, getClaudeSearchRoots, discoverClaudeSkills, discoverClaudePlugins } from '../claude-import.js'; import { getMarketplaceFixtures } from './marketplace-test-fixtures.js'; diff --git a/src/resources/extensions/gsd/tests/complete-milestone.test.ts b/src/resources/extensions/gsd/tests/complete-milestone.test.ts index b32fd2a51..dbccb1c29 100644 --- a/src/resources/extensions/gsd/tests/complete-milestone.test.ts +++ b/src/resources/extensions/gsd/tests/complete-milestone.test.ts @@ -250,7 +250,7 @@ describe("complete-milestone", () => { // Step 11 must explicitly name the `write` tool so the LLM doesn't // confuse it with `edit` (which requires path + oldText + newText). - // See: https://github.com/gsd-build/gsd-2/issues/2946 + // See: https://github.com/singularity-forge/sf-run/issues/2946 assert.ok( /PROJECT\.md.*\bwrite\b/i.test(prompt) || /\bwrite\b.*PROJECT\.md/i.test(prompt), "step 11 must name the `write` tool when updating PROJECT.md", diff --git a/src/resources/extensions/gsd/tests/complete-slice.test.ts b/src/resources/extensions/gsd/tests/complete-slice.test.ts index 4b9765db3..d2c887199 100644 --- a/src/resources/extensions/gsd/tests/complete-slice.test.ts +++ b/src/resources/extensions/gsd/tests/complete-slice.test.ts @@ -125,9 +125,9 @@ console.log('\n=== complete-slice: schema v6 migration ==='); const adapter = _getAdapter()!; - // Verify schema version is current (v15 with UOK projection tables) + // Verify schema version is current (v16 with UOK projection tables) const versionRow = adapter.prepare('SELECT MAX(version) as v FROM schema_version').get(); - assertEq(versionRow?.['v'], 15, 'schema version should be 15'); + assertEq(versionRow?.['v'], 16, 'schema version should be 16'); // Verify slices table has full_summary_md and full_uat_md columns const cols = adapter.prepare("PRAGMA table_info(slices)").all(); @@ -420,7 +420,7 @@ console.log('\n=== complete-slice: step 13 specifies write tool for PROJECT.md ( // Step 13 must explicitly name the `write` tool so the LLM doesn't // confuse it with `edit` (which requires path + oldText + newText). - // See: https://github.com/gsd-build/gsd-2/issues/2946 + // See: https://github.com/singularity-forge/sf-run/issues/2946 const mentionsWriteTool = /PROJECT\.md.*\bwrite\b/i.test(prompt) || /\bwrite\b.*PROJECT\.md/i.test(prompt); diff --git a/src/resources/extensions/gsd/tests/complete-task.test.ts b/src/resources/extensions/gsd/tests/complete-task.test.ts index c589bb7a9..a8f34a2cb 100644 --- a/src/resources/extensions/gsd/tests/complete-task.test.ts +++ b/src/resources/extensions/gsd/tests/complete-task.test.ts @@ -109,9 +109,9 @@ console.log('\n=== complete-task: schema v5 migration ==='); const adapter = _getAdapter()!; - // Verify schema version is current (v15 with UOK projection tables) + // Verify schema version is current (v16 with UOK projection tables) const versionRow = adapter.prepare('SELECT MAX(version) as v FROM schema_version').get(); - assertEq(versionRow?.['v'], 15, 'schema version should be 15'); + assertEq(versionRow?.['v'], 16, 'schema version should be 16'); // Verify all 4 new tables exist const tables = adapter.prepare( diff --git a/src/resources/extensions/gsd/tests/discord-invite-links.test.ts b/src/resources/extensions/gsd/tests/discord-invite-links.test.ts index dffe0af61..f78a85b4e 100644 --- a/src/resources/extensions/gsd/tests/discord-invite-links.test.ts +++ b/src/resources/extensions/gsd/tests/discord-invite-links.test.ts @@ -7,7 +7,7 @@ import { join } from "node:path"; * Validates that all Discord invite links in user-facing files point to valid, * consistent invite URLs — not expired vanity links. * - * Regression test for https://github.com/gsd-build/gsd-2/issues/2699 + * Regression test for https://github.com/singularity-forge/sf-run/issues/2699 */ const ROOT = process.cwd(); diff --git a/src/resources/extensions/gsd/tests/dist-redirect.mjs b/src/resources/extensions/gsd/tests/dist-redirect.mjs index 2d476430e..7aadece0e 100644 --- a/src/resources/extensions/gsd/tests/dist-redirect.mjs +++ b/src/resources/extensions/gsd/tests/dist-redirect.mjs @@ -9,25 +9,25 @@ const ROOT = new URL("../../../../../", import.meta.url); export function resolve(specifier, context, nextResolve) { // 1. Redirect all workspace package bare imports to source. // CI portability runs don't build any packages/ dist artifacts, so every - // @gsd/* specifier (including transitive ones pulled in by pi-coding-agent + // @sf-run/* specifier (including transitive ones pulled in by pi-coding-agent // source itself) must resolve to the TypeScript source entrypoint. if (specifier === "../../packages/pi-coding-agent/src/index.js") { specifier = new URL("packages/pi-coding-agent/src/index.ts", ROOT).href; - } else if (specifier === "@gsd/pi-coding-agent") { + } else if (specifier === "@sf-run/@sf-run/pi-coding-agent") { specifier = new URL("packages/pi-coding-agent/src/index.ts", ROOT).href; - } else if (specifier === "@gsd/pi-ai/oauth") { + } else if (specifier === "@sf-run/pi-ai/oauth") { specifier = new URL("packages/pi-ai/src/utils/oauth/index.ts", ROOT).href; - } else if (specifier === "@gsd/pi-ai") { + } else if (specifier === "@sf-run/@sf-run/pi-ai") { specifier = new URL("packages/pi-ai/src/index.ts", ROOT).href; - } else if (specifier === "@gsd/pi-agent-core") { + } else if (specifier === "@sf-run/@sf-run/pi-agent-core") { specifier = new URL("packages/pi-agent-core/src/index.ts", ROOT).href; - } else if (specifier === "@gsd/pi-tui") { + } else if (specifier === "@sf-run/@sf-run/pi-tui") { specifier = new URL("packages/pi-tui/src/index.ts", ROOT).href; - } else if (specifier === "@gsd/native") { + } else if (specifier === "@sf-run/@sf-run/native") { specifier = new URL("packages/native/src/index.ts", ROOT).href; - } else if (specifier.startsWith("@gsd/native/")) { - // Sub-path imports like @gsd/native/fd, @gsd/native/text, etc. - const subpath = specifier.slice("@gsd/native/".length); + } else if (specifier.startsWith("@sf-run/native/")) { + // Sub-path imports like @sf-run/native/fd, @sf-run/native/text, etc. + const subpath = specifier.slice("@sf-run/native/".length); specifier = new URL(`packages/native/src/${subpath}/index.ts`, ROOT).href; } // 2. Redirect packages/*/dist/ → packages/*/src/ with .js→.ts for strip-types diff --git a/src/resources/extensions/gsd/tests/forensics-dedup.test.ts b/src/resources/extensions/gsd/tests/forensics-dedup.test.ts index d407aa328..b9f10d40a 100644 --- a/src/resources/extensions/gsd/tests/forensics-dedup.test.ts +++ b/src/resources/extensions/gsd/tests/forensics-dedup.test.ts @@ -25,9 +25,9 @@ describe("forensics dedup (#2096)", () => { it("DEDUP_PROMPT_SECTION contains required search commands", async () => { const source = readFileSync(join(gsdDir, "forensics.ts"), "utf-8"); assert.ok(source.includes("DEDUP_PROMPT_SECTION"), "forensics.ts must define DEDUP_PROMPT_SECTION"); - assert.ok(source.includes("gh issue list --repo gsd-build/gsd-2 --state closed")); - assert.ok(source.includes("gh pr list --repo gsd-build/gsd-2 --state open")); - assert.ok(source.includes("gh pr list --repo gsd-build/gsd-2 --state merged")); + assert.ok(source.includes("gh issue list --repo singularity-forge/sf-run --state closed")); + assert.ok(source.includes("gh pr list --repo singularity-forge/sf-run --state open")); + assert.ok(source.includes("gh pr list --repo singularity-forge/sf-run --state merged")); }); it("handleForensics checks forensics_dedup preference", () => { diff --git a/src/resources/extensions/gsd/tests/forensics-issue-routing.test.ts b/src/resources/extensions/gsd/tests/forensics-issue-routing.test.ts index d4154ba98..804574ef1 100644 --- a/src/resources/extensions/gsd/tests/forensics-issue-routing.test.ts +++ b/src/resources/extensions/gsd/tests/forensics-issue-routing.test.ts @@ -20,14 +20,14 @@ test("forensics prompt explicitly forbids github_issues tool for issue creation" ); }); -test("forensics prompt requires gh CLI with --repo gsd-build/gsd-2 for issue creation", () => { +test("forensics prompt requires gh CLI with --repo singularity-forge/sf-run for issue creation", () => { const prompt = readPrompt("forensics"); // Must contain the exact gh CLI command with the correct repo flag assert.match( prompt, /gh issue create --repo gsd-build\/gsd-2/, - "Prompt must specify gh issue create --repo gsd-build/gsd-2", + "Prompt must specify gh issue create --repo singularity-forge/sf-run", ); }); diff --git a/src/resources/extensions/gsd/tests/graph-context.test.ts b/src/resources/extensions/gsd/tests/graph-context.test.ts index a5f864da1..61362853e 100644 --- a/src/resources/extensions/gsd/tests/graph-context.test.ts +++ b/src/resources/extensions/gsd/tests/graph-context.test.ts @@ -7,7 +7,7 @@ * Group 3: Node formatting (description, confidence, no-description) * * Testing strategy: - * @gsd-build/mcp-server is dynamically imported inside inlineGraphSubgraph(). + * @singularity-forge/mcp-server is dynamically imported inside inlineGraphSubgraph(). * Because node:test (v22) does not support mock.module() without the * --experimental-test-module-mocks flag (not enabled in test:unit), we * exercise the real graphQuery/graphStatus functions by controlling the diff --git a/src/resources/extensions/gsd/tests/gsd-db.test.ts b/src/resources/extensions/gsd/tests/gsd-db.test.ts index f1542f9d5..9588a88e2 100644 --- a/src/resources/extensions/gsd/tests/gsd-db.test.ts +++ b/src/resources/extensions/gsd/tests/gsd-db.test.ts @@ -80,7 +80,7 @@ describe('gsd-db', () => { // Check schema_version table const adapter = _getAdapter()!; const version = adapter.prepare('SELECT MAX(version) as version FROM schema_version').get(); - assert.deepStrictEqual(version?.['version'], 15, 'schema version should be 15'); + assert.deepStrictEqual(version?.['version'], 16, 'schema version should be 16'); // Check tables exist by querying them const dRows = adapter.prepare('SELECT count(*) as cnt FROM decisions').get(); diff --git a/src/resources/extensions/gsd/tests/integration/milestone-transition-worktree.test.ts b/src/resources/extensions/gsd/tests/integration/milestone-transition-worktree.test.ts index a283a6a8c..8fb300b83 100644 --- a/src/resources/extensions/gsd/tests/integration/milestone-transition-worktree.test.ts +++ b/src/resources/extensions/gsd/tests/integration/milestone-transition-worktree.test.ts @@ -5,7 +5,7 @@ * worktree lifecycle is handled: old worktree merged, new worktree created. * * Uses source-level checks since the full auto-mode dispatch loop - * requires the @gsd/pi-coding-agent runtime. + * requires the @sf-run/pi-coding-agent runtime. */ import test from "node:test"; diff --git a/src/resources/extensions/gsd/tests/key-manager.test.ts b/src/resources/extensions/gsd/tests/key-manager.test.ts index a7614b092..46eefb05b 100644 --- a/src/resources/extensions/gsd/tests/key-manager.test.ts +++ b/src/resources/extensions/gsd/tests/key-manager.test.ts @@ -1,6 +1,6 @@ import test from "node:test"; import assert from "node:assert/strict"; -import { AuthStorage } from "@gsd/pi-coding-agent"; +import { AuthStorage } from "@sf-run/pi-coding-agent"; import { maskKey, formatDuration, diff --git a/src/resources/extensions/gsd/tests/lazy-pi-tui-import.test.ts b/src/resources/extensions/gsd/tests/lazy-pi-tui-import.test.ts index 3e03cddda..169e6dc83 100644 --- a/src/resources/extensions/gsd/tests/lazy-pi-tui-import.test.ts +++ b/src/resources/extensions/gsd/tests/lazy-pi-tui-import.test.ts @@ -1,4 +1,4 @@ -// Structural contract: shared/mod.ts must never import @gsd/pi-tui. +// Structural contract: shared/mod.ts must never import @sf-run/pi-tui. // TUI-dependent exports live in shared/tui.ts instead. import test from "node:test"; @@ -9,7 +9,7 @@ import { fileURLToPath } from "node:url"; const __dirname = dirname(fileURLToPath(import.meta.url)); -test("shared/mod.ts has no import from @gsd/pi-tui", () => { +test("shared/mod.ts has no import from "@sf-run/pi-tui", () => { const src = readFileSync(join(__dirname, "../../shared/mod.ts"), "utf-8"); - assert.ok(!src.includes("@gsd/pi-tui"), "mod.ts must not import @gsd/pi-tui"); + assert.ok(!src.includes("@sf-run/pi-tui"), "mod.ts must not import "@sf-run/pi-tui"); }); diff --git a/src/resources/extensions/gsd/tests/md-importer.test.ts b/src/resources/extensions/gsd/tests/md-importer.test.ts index 00d209b02..5ee9c7ad9 100644 --- a/src/resources/extensions/gsd/tests/md-importer.test.ts +++ b/src/resources/extensions/gsd/tests/md-importer.test.ts @@ -363,7 +363,7 @@ test('md-importer: schema v1→v2 migration', () => { openDatabase(':memory:'); const adapter = _getAdapter(); const version = adapter?.prepare('SELECT MAX(version) as v FROM schema_version').get(); - assert.deepStrictEqual(version?.v, 15, 'new DB should be at schema version 15'); + assert.deepStrictEqual(version?.v, 16, 'new DB should be at schema version 16'); // Artifacts table should exist const tableCheck = adapter?.prepare("SELECT count(*) as c FROM sqlite_master WHERE type='table' AND name='artifacts'").get(); diff --git a/src/resources/extensions/gsd/tests/memory-leak-guards.test.ts b/src/resources/extensions/gsd/tests/memory-leak-guards.test.ts index 28fd767ce..7c171ca36 100644 --- a/src/resources/extensions/gsd/tests/memory-leak-guards.test.ts +++ b/src/resources/extensions/gsd/tests/memory-leak-guards.test.ts @@ -13,7 +13,7 @@ import { tmpdir } from "node:os"; import { saveActivityLog, clearActivityLogState } from "../activity-log.ts"; import { clearPathCache } from "../paths.ts"; -import type { ExtensionContext } from "@gsd/pi-coding-agent"; +import type { ExtensionContext } from "@sf-run/pi-coding-agent"; function createCtx(entries: unknown[]) { return { sessionManager: { getEntries: () => entries } } as unknown as ExtensionContext; diff --git a/src/resources/extensions/gsd/tests/memory-store.test.ts b/src/resources/extensions/gsd/tests/memory-store.test.ts index 0bb1b0d89..502582326 100644 --- a/src/resources/extensions/gsd/tests/memory-store.test.ts +++ b/src/resources/extensions/gsd/tests/memory-store.test.ts @@ -323,9 +323,9 @@ test('memory-store: schema includes memories table', () => { const viewCount = adapter.prepare('SELECT count(*) as cnt FROM active_memories').get(); assert.deepStrictEqual(viewCount?.['cnt'], 0, 'active_memories view should exist'); - // Verify schema version is 15 (UOK gate/git/audit projection tables included) + // Verify schema version is 16 (UOK gate/git/audit projection tables included) const version = adapter.prepare('SELECT MAX(version) as v FROM schema_version').get(); - assert.deepStrictEqual(version?.['v'], 15, 'schema version should be 15'); + assert.deepStrictEqual(version?.['v'], 16, 'schema version should be 16'); closeDatabase(); }); diff --git a/src/resources/extensions/gsd/tests/prompt-tool-names.test.ts b/src/resources/extensions/gsd/tests/prompt-tool-names.test.ts index 5636c9a82..8a9ff1233 100644 --- a/src/resources/extensions/gsd/tests/prompt-tool-names.test.ts +++ b/src/resources/extensions/gsd/tests/prompt-tool-names.test.ts @@ -3,7 +3,7 @@ // The registered GSD tool is `search-the-web`, not `web_search`. // `web_search` is an Anthropic API implementation detail that should // never appear in GSD prompts or agent frontmatter. -// See: https://github.com/gsd-build/gsd-2/issues/2920 +// See: https://github.com/singularity-forge/sf-run/issues/2920 import test from "node:test"; import assert from "node:assert/strict"; diff --git a/src/resources/extensions/gsd/tests/resolve-ts-hooks.mjs b/src/resources/extensions/gsd/tests/resolve-ts-hooks.mjs index 0eb06a37e..bea797cfe 100644 --- a/src/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +++ b/src/resources/extensions/gsd/tests/resolve-ts-hooks.mjs @@ -5,8 +5,8 @@ const PACKAGES_ROOT = fileURLToPath(new URL("packages/", ROOT)); export function resolve(specifier, context, nextResolve) { let tsSpecifier = specifier; - if (specifier.includes('@gsd/')) { - tsSpecifier = specifier.replace('@gsd/', PACKAGES_ROOT).replace('/dist/', '/src/'); + if (specifier.includes('@sf-run/')) { + tsSpecifier = specifier.replace('@sf-run/', PACKAGES_ROOT).replace('/dist/', '/src/'); if (tsSpecifier.includes('/packages/pi-ai') && !tsSpecifier.endsWith('.ts')) { tsSpecifier = tsSpecifier.replace(/\/packages\/pi-ai$/, '/packages/pi-ai/src/index.ts'); } else if (!tsSpecifier.includes('/src/') && !tsSpecifier.endsWith('.ts')) { diff --git a/src/resources/extensions/gsd/tests/skill-activation.test.ts b/src/resources/extensions/gsd/tests/skill-activation.test.ts index f02310935..cccf2ce78 100644 --- a/src/resources/extensions/gsd/tests/skill-activation.test.ts +++ b/src/resources/extensions/gsd/tests/skill-activation.test.ts @@ -3,7 +3,7 @@ import assert from "node:assert/strict"; import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; -import { loadSkills } from "@gsd/pi-coding-agent"; +import { loadSkills } from "@sf-run/pi-coding-agent"; import { buildSkillActivationBlock } from "../auto-prompts.js"; import type { GSDPreferences } from "../preferences.js"; diff --git a/src/resources/extensions/gsd/tests/subagent-model-dispatch.test.ts b/src/resources/extensions/gsd/tests/subagent-model-dispatch.test.ts index 9c17c13a0..abc150979 100644 --- a/src/resources/extensions/gsd/tests/subagent-model-dispatch.test.ts +++ b/src/resources/extensions/gsd/tests/subagent-model-dispatch.test.ts @@ -5,7 +5,7 @@ * but never passed through to subagent dispatch instruction strings, so the * executing agent autonomously chose "sonnet" instead of the configured model. * - * Issue: gsd-build/gsd-2#4078 + * Issue: singularity-forge/sf-run#4078 */ import test from "node:test"; diff --git a/src/resources/extensions/gsd/tests/token-profile.test.ts b/src/resources/extensions/gsd/tests/token-profile.test.ts index fee6bb386..56694c36a 100644 --- a/src/resources/extensions/gsd/tests/token-profile.test.ts +++ b/src/resources/extensions/gsd/tests/token-profile.test.ts @@ -6,7 +6,7 @@ * table guard clauses (source-level structural verification). * * Uses source-level checks (readFileSync + string matching) to avoid - * @gsd/pi-coding-agent import resolution issues in dev environments. + * @sf-run/pi-coding-agent import resolution issues in dev environments. */ import test from "node:test"; diff --git a/src/resources/extensions/gsd/tests/tool-compatibility.test.ts b/src/resources/extensions/gsd/tests/tool-compatibility.test.ts index 6b533bf63..0cab36d67 100644 --- a/src/resources/extensions/gsd/tests/tool-compatibility.test.ts +++ b/src/resources/extensions/gsd/tests/tool-compatibility.test.ts @@ -8,7 +8,7 @@ import { getAllToolCompatibility, registerMcpToolCompatibility, resetToolCompatibilityRegistry, -} from "@gsd/pi-coding-agent"; +} from "@sf-run/pi-coding-agent"; import { isToolCompatibleWithProvider, @@ -18,7 +18,7 @@ import { import { getProviderCapabilities, -} from "@gsd/pi-ai"; +} from "@sf-run/pi-ai"; // ─── Tool Compatibility Registry ──────────────────────────────────────────── diff --git a/src/resources/extensions/gsd/tests/tool-param-optionality.test.ts b/src/resources/extensions/gsd/tests/tool-param-optionality.test.ts index 6521d1bda..f7a692bfe 100644 --- a/src/resources/extensions/gsd/tests/tool-param-optionality.test.ts +++ b/src/resources/extensions/gsd/tests/tool-param-optionality.test.ts @@ -8,7 +8,7 @@ * are required, while enrichment arrays (patterns, requirements, files, etc.) * are optional — so any model can call the tool successfully. * - * See: https://github.com/gsd-build/gsd-2/issues/2771 + * See: https://github.com/singularity-forge/sf-run/issues/2771 */ import { test } from "node:test"; diff --git a/src/resources/extensions/gsd/tests/triage-dispatch.test.ts b/src/resources/extensions/gsd/tests/triage-dispatch.test.ts index 025f99677..d020b78da 100644 --- a/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +++ b/src/resources/extensions/gsd/tests/triage-dispatch.test.ts @@ -4,7 +4,7 @@ * These tests verify structural invariants of the triage integration * by inspecting the actual source code of auto-post-unit.ts, auto.ts, * and post-unit-hooks.ts. Full behavioral testing requires the - * @gsd/pi-coding-agent runtime. + * @sf-run/pi-coding-agent runtime. */ import test from "node:test"; diff --git a/src/resources/extensions/gsd/tests/triage-resolution.test.ts b/src/resources/extensions/gsd/tests/triage-resolution.test.ts index 0decf9e6f..d3e499ecc 100644 --- a/src/resources/extensions/gsd/tests/triage-resolution.test.ts +++ b/src/resources/extensions/gsd/tests/triage-resolution.test.ts @@ -8,7 +8,7 @@ import { mkdirSync, readFileSync, writeFileSync, rmSync, existsSync } from "node import { join } from "node:path"; import { tmpdir } from "node:os"; import { appendCapture, markCaptureResolved, markCaptureExecuted, loadAllCaptures, loadActionableCaptures } from "../captures.ts"; -// Import only the functions that don't depend on @gsd/pi-coding-agent +// Import only the functions that don't depend on @sf-run/pi-coding-agent // (triage-ui.ts imports next-action-ui.ts which imports the unavailable package) import { executeInject, executeReplan, detectFileOverlap, loadDeferredCaptures, loadReplanCaptures, buildQuickTaskPrompt, executeTriageResolutions, ensureDeferMilestoneDir } from "../triage-resolution.ts"; diff --git a/src/resources/extensions/gsd/tests/uok-model-policy.test.ts b/src/resources/extensions/gsd/tests/uok-model-policy.test.ts index dd2b2b93a..576d45dd0 100644 --- a/src/resources/extensions/gsd/tests/uok-model-policy.test.ts +++ b/src/resources/extensions/gsd/tests/uok-model-policy.test.ts @@ -11,7 +11,7 @@ import { import { registerToolCompatibility, resetToolCompatibilityRegistry, -} from "@gsd/pi-coding-agent"; +} from "@sf-run/pi-coding-agent"; test.afterEach(() => { resetToolCompatibilityRegistry(); diff --git a/src/resources/extensions/gsd/tools/complete-slice.ts b/src/resources/extensions/gsd/tools/complete-slice.ts index 7300ce91d..b78b82ddd 100644 --- a/src/resources/extensions/gsd/tools/complete-slice.ts +++ b/src/resources/extensions/gsd/tools/complete-slice.ts @@ -430,7 +430,7 @@ export async function handleCompleteSlice( // eslint-disable-next-line @typescript-eslint/no-floating-promises (async () => { try { - const graphMod = await import("@gsd-build/mcp-server") as unknown as Partial<{ + const graphMod = await import("@singularity-forge/mcp-server") as unknown as Partial<{ buildGraph: (dir: string) => Promise<{ nodes: unknown[]; edges: unknown[]; builtAt: string }>; writeGraph: (gsdRoot: string, graph: unknown) => Promise; resolveGsdRoot: (basePath: string) => string; @@ -440,7 +440,7 @@ export async function handleCompleteSlice( || typeof graphMod.writeGraph !== "function" || typeof graphMod.resolveGsdRoot !== "function" ) { - throw new Error("graph helpers unavailable from @gsd-build/mcp-server"); + throw new Error("graph helpers unavailable from @singularity-forge/mcp-server"); } const g = await graphMod.buildGraph(basePath); await graphMod.writeGraph(graphMod.resolveGsdRoot(basePath), g); diff --git a/src/resources/extensions/gsd/triage-ui.ts b/src/resources/extensions/gsd/triage-ui.ts index b2ea7cf4f..72f896030 100644 --- a/src/resources/extensions/gsd/triage-ui.ts +++ b/src/resources/extensions/gsd/triage-ui.ts @@ -9,7 +9,7 @@ * confirmed classifications. */ -import type { ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import type { ExtensionCommandContext } from "@sf-run/pi-coding-agent"; import { showNextAction } from "../shared/tui.js"; import type { CaptureEntry, Classification, TriageResult } from "./captures.js"; import { markCaptureResolved } from "./captures.js"; diff --git a/src/resources/extensions/gsd/undo.ts b/src/resources/extensions/gsd/undo.ts index 6443634c6..5b44f2676 100644 --- a/src/resources/extensions/gsd/undo.ts +++ b/src/resources/extensions/gsd/undo.ts @@ -3,7 +3,7 @@ // handleUndoTask: Reset a single task's DB status to "pending" and re-render markdown. // handleResetSlice: Reset a slice and all its tasks, re-rendering plan + roadmap. -import type { ExtensionCommandContext, ExtensionAPI } from "@gsd/pi-coding-agent"; +import type { ExtensionCommandContext, ExtensionAPI } from "@sf-run/pi-coding-agent"; import { existsSync, readFileSync, unlinkSync, readdirSync } from "node:fs"; import { join, basename } from "node:path"; import { nativeRevertCommit, nativeRevertAbort } from "./native-git-bridge.js"; diff --git a/src/resources/extensions/gsd/uok/kernel.ts b/src/resources/extensions/gsd/uok/kernel.ts index 69138d4bc..fbd39903f 100644 --- a/src/resources/extensions/gsd/uok/kernel.ts +++ b/src/resources/extensions/gsd/uok/kernel.ts @@ -1,4 +1,4 @@ -import type { ExtensionAPI, ExtensionContext } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI, ExtensionContext } from "@sf-run/pi-coding-agent"; import { appendFileSync, mkdirSync } from "node:fs"; import { join } from "node:path"; diff --git a/src/resources/extensions/gsd/visualizer-overlay.ts b/src/resources/extensions/gsd/visualizer-overlay.ts index 32a98346d..c2d197cb8 100644 --- a/src/resources/extensions/gsd/visualizer-overlay.ts +++ b/src/resources/extensions/gsd/visualizer-overlay.ts @@ -1,5 +1,5 @@ -import type { Theme } from "@gsd/pi-coding-agent"; -import { truncateToWidth, visibleWidth, matchesKey, Key } from "@gsd/pi-tui"; +import type { Theme } from "@sf-run/pi-coding-agent"; +import { truncateToWidth, visibleWidth, matchesKey, Key } from "@sf-run/pi-tui"; import { loadVisualizerData, type VisualizerData } from "./visualizer-data.js"; import { renderProgressView, diff --git a/src/resources/extensions/gsd/visualizer-views.ts b/src/resources/extensions/gsd/visualizer-views.ts index 44de80d41..f43e0b561 100644 --- a/src/resources/extensions/gsd/visualizer-views.ts +++ b/src/resources/extensions/gsd/visualizer-views.ts @@ -1,7 +1,7 @@ // View renderers for the GSD workflow visualizer overlay. -import type { Theme } from "@gsd/pi-coding-agent"; -import { truncateToWidth, visibleWidth } from "@gsd/pi-tui"; +import type { Theme } from "@sf-run/pi-coding-agent"; +import { truncateToWidth, visibleWidth } from "@sf-run/pi-tui"; import type { VisualizerData, VisualizerMilestone, SliceVerification, VisualizerSliceActivity, VisualizerStats, VisualizerSliceRef } from "./visualizer-data.js"; import { formatCost, formatTokenCount, classifyUnitPhase } from "./metrics.js"; import { formatDuration, padRight, joinColumns, sparkline, STATUS_GLYPH, STATUS_COLOR } from "../shared/mod.js"; diff --git a/src/resources/extensions/gsd/watch/header-renderer.ts b/src/resources/extensions/gsd/watch/header-renderer.ts index 27d84f9aa..abe518d57 100644 --- a/src/resources/extensions/gsd/watch/header-renderer.ts +++ b/src/resources/extensions/gsd/watch/header-renderer.ts @@ -5,7 +5,7 @@ import { execFileSync } from "node:child_process"; import { existsSync, readFileSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; -import { visibleWidth, truncateToWidth } from "@gsd/pi-tui"; +import { visibleWidth, truncateToWidth } from "@sf-run/pi-tui"; import { loadEffectiveGSDPreferences } from "../preferences.js"; // ─── Constants ──────────────────────────────────────────────────────────────── diff --git a/src/resources/extensions/gsd/workflow-mcp-auto-prep.ts b/src/resources/extensions/gsd/workflow-mcp-auto-prep.ts index 1d69ebc00..8ebf0addb 100644 --- a/src/resources/extensions/gsd/workflow-mcp-auto-prep.ts +++ b/src/resources/extensions/gsd/workflow-mcp-auto-prep.ts @@ -1,4 +1,4 @@ -import type { ExtensionContext } from "@gsd/pi-coding-agent"; +import type { ExtensionContext } from "@sf-run/pi-coding-agent"; import { type EnsureProjectWorkflowMcpConfigResult, diff --git a/src/resources/extensions/gsd/worktree-command-bootstrap.ts b/src/resources/extensions/gsd/worktree-command-bootstrap.ts index 4aa9e11f6..24871bfc4 100644 --- a/src/resources/extensions/gsd/worktree-command-bootstrap.ts +++ b/src/resources/extensions/gsd/worktree-command-bootstrap.ts @@ -1,4 +1,4 @@ -import { importExtensionModule, type ExtensionAPI, type ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import { importExtensionModule, type ExtensionAPI, type ExtensionCommandContext } from "@sf-run/pi-coding-agent"; const WORKTREE_SUBCOMMANDS = [ { cmd: "list", desc: "List existing worktrees" }, diff --git a/src/resources/extensions/gsd/worktree-command.ts b/src/resources/extensions/gsd/worktree-command.ts index a1722132d..26483689b 100644 --- a/src/resources/extensions/gsd/worktree-command.ts +++ b/src/resources/extensions/gsd/worktree-command.ts @@ -10,7 +10,7 @@ * /worktree remove — remove a worktree and its branch */ -import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI, ExtensionCommandContext } from "@sf-run/pi-coding-agent"; import { loadPrompt } from "./prompt-loader.js"; import { autoCommitCurrentBranch, getMainBranch, resolveGitHeadPath, nudgeGitBranchCache } from "./worktree.js"; import { runWorktreePostCreateHook } from "./auto-worktree.js"; diff --git a/src/resources/extensions/mac-tools/index.ts b/src/resources/extensions/mac-tools/index.ts index 40dae2b86..7a12f7de2 100644 --- a/src/resources/extensions/mac-tools/index.ts +++ b/src/resources/extensions/mac-tools/index.ts @@ -12,8 +12,8 @@ * - All Swift debug output goes to stderr; only JSON on stdout */ -import type { ExtensionAPI } from "@gsd/pi-coding-agent"; -import { StringEnum } from "@gsd/pi-ai"; +import type { ExtensionAPI } from "@sf-run/pi-coding-agent"; +import { StringEnum } from "@sf-run/pi-ai"; import { Type } from "@sinclair/typebox"; import { execFileSync } from "node:child_process"; import { statSync, readdirSync } from "node:fs"; diff --git a/src/resources/extensions/mcp-client/index.ts b/src/resources/extensions/mcp-client/index.ts index 3cfb5b51b..dce11cc8d 100644 --- a/src/resources/extensions/mcp-client/index.ts +++ b/src/resources/extensions/mcp-client/index.ts @@ -11,14 +11,14 @@ * mcp_call — Call a tool on an MCP server (lazy connect) */ -import type { ExtensionAPI } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI } from "@sf-run/pi-coding-agent"; import { truncateHead, DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, -} from "@gsd/pi-coding-agent"; -import { Text } from "@gsd/pi-tui"; +} from "@sf-run/pi-coding-agent"; +import { Text } from "@sf-run/pi-tui"; import { Type } from "@sinclair/typebox"; import { Client } from "@modelcontextprotocol/sdk/client"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; diff --git a/src/resources/extensions/ollama/index.ts b/src/resources/extensions/ollama/index.ts index 6934f4c26..afb0bf32d 100644 --- a/src/resources/extensions/ollama/index.ts +++ b/src/resources/extensions/ollama/index.ts @@ -16,7 +16,7 @@ * Respects OLLAMA_HOST env var for non-default endpoints. */ -import { importExtensionModule, type ExtensionAPI } from "@gsd/pi-coding-agent"; +import { importExtensionModule, type ExtensionAPI } from "@sf-run/pi-coding-agent"; import * as client from "./ollama-client.js"; import { discoverModels } from "./ollama-discovery.js"; import { registerOllamaCommands } from "./ollama-commands.js"; diff --git a/src/resources/extensions/ollama/ollama-chat-provider.ts b/src/resources/extensions/ollama/ollama-chat-provider.ts index 81e1de6f4..c6847b30b 100644 --- a/src/resources/extensions/ollama/ollama-chat-provider.ts +++ b/src/resources/extensions/ollama/ollama-chat-provider.ts @@ -25,7 +25,7 @@ import { type ToolCall, type Usage, EventStream, -} from "@gsd/pi-ai"; +} from "@sf-run/pi-ai"; import { chat } from "./ollama-client.js"; import type { OllamaChatMessage, @@ -405,7 +405,7 @@ function extractMetrics(chunk: OllamaChatResponse): InferenceMetrics | undefined } // ─── Stream lifecycle helpers ─────────────────────────────────────────────── -// Replicated from openai-shared.ts (not exported from @gsd/pi-ai) +// Replicated from openai-shared.ts (not exported from "@sf-run/pi-ai) function buildInitialOutput(model: Model): AssistantMessage { return { diff --git a/src/resources/extensions/ollama/ollama-commands.ts b/src/resources/extensions/ollama/ollama-commands.ts index 81322c784..684117962 100644 --- a/src/resources/extensions/ollama/ollama-commands.ts +++ b/src/resources/extensions/ollama/ollama-commands.ts @@ -11,8 +11,8 @@ * /ollama ps — Show running models and resource usage */ -import type { ExtensionAPI } from "@gsd/pi-coding-agent"; -import { Text } from "@gsd/pi-tui"; +import type { ExtensionAPI } from "@sf-run/pi-coding-agent"; +import { Text } from "@sf-run/pi-tui"; import * as client from "./ollama-client.js"; import { discoverModels, formatModelForDisplay } from "./ollama-discovery.js"; import { formatModelSize } from "./model-capabilities.js"; diff --git a/src/resources/extensions/ollama/ollama-tool.ts b/src/resources/extensions/ollama/ollama-tool.ts index e3a5d7535..0559a4118 100644 --- a/src/resources/extensions/ollama/ollama-tool.ts +++ b/src/resources/extensions/ollama/ollama-tool.ts @@ -4,8 +4,8 @@ * with the local Ollama instance — list models, pull new ones, check status. */ -import type { ExtensionAPI } from "@gsd/pi-coding-agent"; -import { Text } from "@gsd/pi-tui"; +import type { ExtensionAPI } from "@sf-run/pi-coding-agent"; +import { Text } from "@sf-run/pi-tui"; import { Type } from "@sinclair/typebox"; import * as client from "./ollama-client.js"; import { discoverModels, formatModelForDisplay } from "./ollama-discovery.js"; diff --git a/src/resources/extensions/remote-questions/config.ts b/src/resources/extensions/remote-questions/config.ts index e34249601..404cdc868 100644 --- a/src/resources/extensions/remote-questions/config.ts +++ b/src/resources/extensions/remote-questions/config.ts @@ -2,7 +2,7 @@ * Remote Questions — configuration resolution and validation */ -import { AuthStorage } from "@gsd/pi-coding-agent"; +import { AuthStorage } from "@sf-run/pi-coding-agent"; import { loadEffectiveGSDPreferences, type RemoteQuestionsConfig } from "../gsd/preferences.js"; import type { RemoteChannel } from "./types.js"; diff --git a/src/resources/extensions/remote-questions/remote-command.ts b/src/resources/extensions/remote-questions/remote-command.ts index ea5278904..cfdf84798 100644 --- a/src/resources/extensions/remote-questions/remote-command.ts +++ b/src/resources/extensions/remote-questions/remote-command.ts @@ -2,9 +2,9 @@ * Remote Questions — /gsd remote command */ -import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent"; -import { AuthStorage } from "@gsd/pi-coding-agent"; -import { Editor, type EditorTheme, Key, matchesKey, truncateToWidth } from "@gsd/pi-tui"; +import type { ExtensionAPI, ExtensionCommandContext } from "@sf-run/pi-coding-agent"; +import { AuthStorage } from "@sf-run/pi-coding-agent"; +import { Editor, type EditorTheme, Key, matchesKey, truncateToWidth } from "@sf-run/pi-tui"; import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs"; import { dirname, join } from "node:path"; import { getGlobalGSDPreferencesPath, loadEffectiveGSDPreferences } from "../gsd/preferences.js"; diff --git a/src/resources/extensions/search-the-web/command-search-provider.ts b/src/resources/extensions/search-the-web/command-search-provider.ts index 0e5d6627e..04d6e382e 100644 --- a/src/resources/extensions/search-the-web/command-search-provider.ts +++ b/src/resources/extensions/search-the-web/command-search-provider.ts @@ -1,15 +1,15 @@ /** * /search-provider slash command. * - * Lets users switch between tavily, brave, and auto search backends. + * Lets users switch between tavily, brave, ollama, combosearch, and auto search backends. * Supports direct arg (`/search-provider tavily`) or interactive select UI. * Tab completion provides the three valid options with key status. * * All provider logic lives in provider.ts (S01) — this is pure UI wiring. */ -import type { ExtensionAPI } from '@gsd/pi-coding-agent' -import type { AutocompleteItem } from '@gsd/pi-tui' +import type { ExtensionAPI } from '@sf-run/pi-coding-agent' +import type { AutocompleteItem } from '@sf-run/pi-tui' import { getTavilyApiKey, getBraveApiKey, @@ -20,7 +20,7 @@ import { type SearchProviderPreference, } from './provider.js' -const VALID_PREFERENCES: SearchProviderPreference[] = ['tavily', 'brave', 'ollama', 'auto'] +const VALID_PREFERENCES: SearchProviderPreference[] = ['tavily', 'brave', 'ollama', 'combosearch', 'auto'] function keyStatus(provider: 'tavily' | 'brave' | 'ollama'): string { if (provider === 'tavily') return getTavilyApiKey() ? '✓' : '✗' @@ -28,11 +28,21 @@ function keyStatus(provider: 'tavily' | 'brave' | 'ollama'): string { return getBraveApiKey() ? '✓' : '✗' } +function comboStatus(): string { + const available = [ + getTavilyApiKey() ? 'tavily' : null, + getBraveApiKey() ? 'brave' : null, + getOllamaApiKey() ? 'ollama' : null, + ].filter(Boolean) as string[] + return available.length > 0 ? `${available.length} source${available.length === 1 ? '' : 's'}` : '✗' +} + function buildSelectOptions(): string[] { return [ `tavily (key: ${keyStatus('tavily')})`, `brave (key: ${keyStatus('brave')})`, `ollama (key: ${keyStatus('ollama')})`, + `combosearch (${comboStatus()})`, `auto`, ] } @@ -41,12 +51,13 @@ function parseSelectChoice(choice: string): SearchProviderPreference { if (choice.startsWith('tavily')) return 'tavily' if (choice.startsWith('brave')) return 'brave' if (choice.startsWith('ollama')) return 'ollama' + if (choice.startsWith('combosearch')) return 'combosearch' return 'auto' } export function registerSearchProviderCommand(pi: ExtensionAPI): void { pi.registerCommand('search-provider', { - description: 'Switch search provider (tavily, brave, ollama, auto)', + description: 'Switch search provider (tavily, brave, ollama, combosearch, auto)', getArgumentCompletions(prefix: string): AutocompleteItem[] | null { const trimmed = prefix.trim().toLowerCase() @@ -56,6 +67,8 @@ export function registerSearchProviderCommand(pi: ExtensionAPI): void { let description: string if (p === 'auto') { description = `Auto-select (tavily: ${keyStatus('tavily')}, brave: ${keyStatus('brave')}, ollama: ${keyStatus('ollama')})` + } else if (p === 'combosearch') { + description = `fan-out aggregator (${comboStatus()})` } else { description = `key: ${keyStatus(p)}` } diff --git a/src/resources/extensions/search-the-web/index.ts b/src/resources/extensions/search-the-web/index.ts index 793c1ea15..b12db46f6 100644 --- a/src/resources/extensions/search-the-web/index.ts +++ b/src/resources/extensions/search-the-web/index.ts @@ -5,7 +5,7 @@ * interactive mode so startup is not blocked on the full search tool stack. */ -import { importExtensionModule, type ExtensionAPI } from "@gsd/pi-coding-agent"; +import { importExtensionModule, type ExtensionAPI } from "@sf-run/pi-coding-agent"; import { registerSearchProviderCommand } from "./command-search-provider.js"; import { registerNativeSearchHooks } from "./native-search.js"; diff --git a/src/resources/extensions/search-the-web/native-search.ts b/src/resources/extensions/search-the-web/native-search.ts index 5debc2b1b..81bf5169a 100644 --- a/src/resources/extensions/search-the-web/native-search.ts +++ b/src/resources/extensions/search-the-web/native-search.ts @@ -30,7 +30,7 @@ export const MAX_NATIVE_SEARCHES_PER_SESSION = 15; export function preferBraveSearch(): boolean { // PREFERENCES.md takes priority over env var const prefsPref = resolveSearchProviderFromPreferences(); - if (prefsPref === "brave" || prefsPref === "tavily" || prefsPref === "ollama") return true; + if (prefsPref === "brave" || prefsPref === "tavily" || prefsPref === "ollama" || prefsPref === "combosearch") return true; if (prefsPref === "native") return false; // Fall back to env var return process.env.PREFER_BRAVE_SEARCH === "1" || process.env.PREFER_BRAVE_SEARCH === "true"; diff --git a/src/resources/extensions/search-the-web/provider.ts b/src/resources/extensions/search-the-web/provider.ts index cf7ae5b98..b33a55d36 100644 --- a/src/resources/extensions/search-the-web/provider.ts +++ b/src/resources/extensions/search-the-web/provider.ts @@ -4,12 +4,13 @@ * Single source of truth for which search backend (Tavily vs Brave) to use. * Reads API keys from process.env at call time (not module load time) so * hot-reloaded keys work. Preference is stored in auth.json under the - * synthetic provider key `search_provider` as { type: "api_key", key: "tavily" | "brave" | "auto" }. + * synthetic provider key `search_provider` as + * { type: "api_key", key: "tavily" | "brave" | "ollama" | "combosearch" | "auto" }. * * @see S01-RESEARCH.md for the storage decision rationale (D002). */ -import { AuthStorage } from '@gsd/pi-coding-agent' +import { AuthStorage } from '@sf-run/pi-coding-agent' import { homedir } from 'os' import { join } from 'path' import { resolveSearchProviderFromPreferences } from '../gsd/preferences.js' @@ -20,10 +21,10 @@ import { resolveSearchProviderFromPreferences } from '../gsd/preferences.js' const gsdHome = process.env.GSD_HOME || join(homedir(), '.gsd') const authFilePath = join(gsdHome, 'agent', 'auth.json') -export type SearchProvider = 'tavily' | 'brave' | 'ollama' +export type SearchProvider = 'tavily' | 'brave' | 'ollama' | 'combosearch' export type SearchProviderPreference = SearchProvider | 'auto' -const VALID_PREFERENCES = new Set(['tavily', 'brave', 'ollama', 'auto']) +const VALID_PREFERENCES = new Set(['tavily', 'brave', 'ollama', 'combosearch', 'auto']) const PREFERENCE_KEY = 'search_provider' /** Returns the Tavily API key from the environment, or empty string if not set. */ @@ -99,6 +100,7 @@ export function resolveSearchProvider(overridePreference?: string): SearchProvid const hasTavily = tavilyKey.length > 0 const hasBrave = braveKey.length > 0 const hasOllama = ollamaKey.length > 0 + const hasAny = hasTavily || hasBrave || hasOllama // Determine effective preference let pref: SearchProviderPreference @@ -124,6 +126,10 @@ export function resolveSearchProvider(overridePreference?: string): SearchProvid return null } + if (pref === 'combosearch') { + return hasAny ? 'combosearch' : null + } + if (pref === 'tavily') { if (hasTavily) return 'tavily' if (hasBrave) return 'brave' diff --git a/src/resources/extensions/search-the-web/tool-fetch-page.ts b/src/resources/extensions/search-the-web/tool-fetch-page.ts index 0934af736..4074c8543 100644 --- a/src/resources/extensions/search-the-web/tool-fetch-page.ts +++ b/src/resources/extensions/search-the-web/tool-fetch-page.ts @@ -8,9 +8,9 @@ * - Content-type awareness (JSON passthrough, PDF detection) */ -import type { ExtensionAPI } from "@gsd/pi-coding-agent"; -import { truncateHead, DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES } from "@gsd/pi-coding-agent"; -import { Text } from "@gsd/pi-tui"; +import type { ExtensionAPI } from "@sf-run/pi-coding-agent"; +import { truncateHead, DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES } from "@sf-run/pi-coding-agent"; +import { Text } from "@sf-run/pi-tui"; import { Type } from "@sinclair/typebox"; import { LRUTTLCache } from "./cache.js"; diff --git a/src/resources/extensions/search-the-web/tool-llm-context.ts b/src/resources/extensions/search-the-web/tool-llm-context.ts index cf6fd6fa1..d6c163d7d 100644 --- a/src/resources/extensions/search-the-web/tool-llm-context.ts +++ b/src/resources/extensions/search-the-web/tool-llm-context.ts @@ -15,11 +15,11 @@ * Use search-the-web when you want links/URLs to browse selectively. */ -import type { ExtensionAPI } from "@gsd/pi-coding-agent"; -import { truncateHead, DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES } from "@gsd/pi-coding-agent"; -import { Text } from "@gsd/pi-tui"; +import type { ExtensionAPI } from "@sf-run/pi-coding-agent"; +import { truncateHead, DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES } from "@sf-run/pi-coding-agent"; +import { Text } from "@sf-run/pi-tui"; import { Type } from "@sinclair/typebox"; -import { StringEnum } from "@gsd/pi-ai"; +import { StringEnum } from "@sf-run/pi-ai"; import { LRUTTLCache } from "./cache.js"; import { fetchWithRetryTimed, HttpError, classifyError, type RateLimitInfo } from "./http.js"; @@ -79,7 +79,7 @@ interface LLMContextDetails { errorKind?: string; error?: string; retryAfterMs?: number; - provider?: 'tavily' | 'brave' | 'ollama'; + provider?: 'tavily' | 'brave' | 'ollama' | 'combosearch'; } // ============================================================================= @@ -269,6 +269,161 @@ async function executeOllamaLLMContext( return { cached, latencyMs: timed.latencyMs, rateLimit: timed.rateLimit }; } +async function executeBraveLLMContext( + params: { query: string; maxTokens: number; maxUrls: number; threshold: string; count: number }, + signal?: AbortSignal, +): Promise<{ cached: CachedLLMContext; latencyMs: number; rateLimit?: RateLimitInfo }> { + const url = new URL("https://api.search.brave.com/res/v1/llm/context"); + url.searchParams.append("q", params.query); + url.searchParams.append("count", String(params.count)); + url.searchParams.append("maximum_number_of_tokens", String(params.maxTokens)); + url.searchParams.append("maximum_number_of_urls", String(params.maxUrls)); + url.searchParams.append("context_threshold_mode", params.threshold); + + const timed = await fetchWithRetryTimed(url.toString(), { + method: "GET", + headers: braveHeaders(), + signal, + }, 2); + + const data: BraveLLMContextResponse = await timed.response.json(); + const grounding: LLMContextSnippet[] = []; + + if (data.grounding?.generic) { + for (const item of data.grounding.generic) { + if (item.snippets && item.snippets.length > 0) { + grounding.push({ + url: item.url, + title: item.title, + snippets: item.snippets, + }); + } + } + } + + if (data.grounding?.poi && data.grounding.poi.snippets?.length) { + grounding.push({ + url: data.grounding.poi.url, + title: data.grounding.poi.title || data.grounding.poi.name, + snippets: data.grounding.poi.snippets, + }); + } + + if (data.grounding?.map) { + for (const item of data.grounding.map) { + if (item.snippets?.length) { + grounding.push({ + url: item.url, + title: item.title || item.name, + snippets: item.snippets, + }); + } + } + } + + const sources: Record = {}; + if (data.sources) { + for (const [sourceUrl, sourceInfo] of Object.entries(data.sources)) { + sources[sourceUrl] = { + title: sourceInfo.title, + hostname: sourceInfo.hostname, + age: sourceInfo.age, + }; + } + } + + const allText = grounding.map(g => g.snippets.join(" ")).join(" "); + const estimatedTokens = estimateTokens(allText); + return { cached: { grounding, sources, estimatedTokens }, latencyMs: timed.latencyMs, rateLimit: timed.rateLimit }; +} + +function availableComboProviders(): Array<'tavily' | 'brave' | 'ollama'> { + const providers: Array<'tavily' | 'brave' | 'ollama'> = []; + if (getTavilyApiKey()) providers.push('tavily'); + if (getBraveApiKey()) providers.push('brave'); + if (getOllamaApiKey()) providers.push('ollama'); + return providers; +} + +function trimMergedContext( + grounding: LLMContextSnippet[], + sources: Record, + maxTokens: number, +): CachedLLMContext { + const effectiveBudget = Math.max(1, Math.floor(maxTokens * 0.8)); + const trimmed: LLMContextSnippet[] = []; + let totalTokens = 0; + + for (const item of grounding) { + if (totalTokens >= effectiveBudget) break; + const remainingTokens = effectiveBudget - totalTokens; + const maxChars = remainingTokens * 4; + const joined = item.snippets.join("\n\n"); + let text = joined; + if (text.length > maxChars) { + text = text.slice(0, maxChars); + } + const tokens = estimateTokens(text); + if (tokens <= 0) continue; + trimmed.push({ + url: item.url, + title: item.title, + snippets: [text], + }); + totalTokens += tokens; + } + + return { grounding: trimmed, sources, estimatedTokens: totalTokens }; +} + +async function executeComboLLMContext( + params: { query: string; maxTokens: number; maxUrls: number; threshold: string; count: number }, + signal?: AbortSignal, +): Promise<{ cached: CachedLLMContext; latencyMs: number; rateLimit?: RateLimitInfo }> { + const providers = availableComboProviders(); + const tasks = providers.map(async (provider) => { + if (provider === 'tavily') { + return executeTavilyLLMContext(params, signal); + } + if (provider === 'ollama') { + return executeOllamaLLMContext( + { query: params.query, maxTokens: params.maxTokens, count: params.count, threshold: params.threshold }, + signal, + ); + } + return executeBraveLLMContext(params, signal); + }); + + const settled = await Promise.allSettled(tasks); + const fulfilled = settled.filter((entry): entry is PromiseFulfilledResult<{ cached: CachedLLMContext; latencyMs: number; rateLimit?: RateLimitInfo }> => entry.status === 'fulfilled'); + if (fulfilled.length === 0) { + const firstRejected = settled.find((entry): entry is PromiseRejectedResult => entry.status === 'rejected'); + throw firstRejected?.reason ?? new Error("combosearch llm context failed"); + } + + const byUrl = new Map(); + const sources: Record = {}; + + for (const entry of fulfilled) { + for (const item of entry.value.cached.grounding) { + const existing = byUrl.get(item.url); + if (existing) { + const snippets = Array.from(new Set([...existing.snippets, ...item.snippets])); + byUrl.set(item.url, { ...existing, snippets }); + } else { + byUrl.set(item.url, { ...item, snippets: [...item.snippets] }); + } + } + Object.assign(sources, entry.value.cached.sources); + } + + const mergedGrounding = Array.from(byUrl.values()).slice(0, params.maxUrls); + const cached = trimMergedContext(mergedGrounding, sources, params.maxTokens); + const latencyMs = Math.max(...fulfilled.map((entry) => entry.value.latencyMs)); + const rateLimit = fulfilled.find((entry) => entry.value.rateLimit)?.value.rateLimit; + return { cached, latencyMs, rateLimit }; +} + // ============================================================================= // Tool Registration // ============================================================================= @@ -389,7 +544,15 @@ export function registerLLMContextTool(pi: ExtensionAPI) { let latencyMs: number | undefined; let rateLimit: RateLimitInfo | undefined; - if (provider === "tavily") { + if (provider === "combosearch") { + const comboResult = await executeComboLLMContext( + { query: params.query, maxTokens, maxUrls, threshold, count }, + signal, + ); + result = comboResult.cached; + latencyMs = comboResult.latencyMs; + rateLimit = comboResult.rateLimit; + } else if (provider === "tavily") { const tavilyResult = await executeTavilyLLMContext( { query: params.query, maxTokens, maxUrls, threshold, count }, signal, @@ -406,24 +569,12 @@ export function registerLLMContextTool(pi: ExtensionAPI) { latencyMs = ollamaResult.latencyMs; rateLimit = ollamaResult.rateLimit; } else { - // ================================================================ - // BRAVE PATH (unchanged API logic) - // ================================================================ - const url = new URL("https://api.search.brave.com/res/v1/llm/context"); - url.searchParams.append("q", params.query); - url.searchParams.append("count", String(count)); - url.searchParams.append("maximum_number_of_tokens", String(maxTokens)); - url.searchParams.append("maximum_number_of_urls", String(maxUrls)); - url.searchParams.append("context_threshold_mode", threshold); - - // Use a custom fetch flow to read error bodies from the Brave API - let timed; + let braveResult; try { - timed = await fetchWithRetryTimed(url.toString(), { - method: "GET", - headers: braveHeaders(), + braveResult = await executeBraveLLMContext( + { query: params.query, maxTokens, maxUrls, threshold, count }, signal, - }, 2); + ); } catch (fetchErr) { // Try to extract Brave's structured error detail from the response body. // This is especially useful for plan/subscription errors (OPTION_NOT_IN_PLAN). @@ -455,66 +606,9 @@ export function registerLLMContextTool(pi: ExtensionAPI) { isError: true, }; } - - const data: BraveLLMContextResponse = await timed.response.json(); - - // ------------------------------------------------------------------ - // Normalize Brave response - // ------------------------------------------------------------------ - const grounding: LLMContextSnippet[] = []; - - if (data.grounding?.generic) { - for (const item of data.grounding.generic) { - if (item.snippets && item.snippets.length > 0) { - grounding.push({ - url: item.url, - title: item.title, - snippets: item.snippets, - }); - } - } - } - - // Include POI data if present - if (data.grounding?.poi && data.grounding.poi.snippets?.length) { - grounding.push({ - url: data.grounding.poi.url, - title: data.grounding.poi.title || data.grounding.poi.name, - snippets: data.grounding.poi.snippets, - }); - } - - // Include map data if present - if (data.grounding?.map) { - for (const item of data.grounding.map) { - if (item.snippets?.length) { - grounding.push({ - url: item.url, - title: item.title || item.name, - snippets: item.snippets, - }); - } - } - } - - const sources: Record = {}; - if (data.sources) { - for (const [sourceUrl, sourceInfo] of Object.entries(data.sources)) { - sources[sourceUrl] = { - title: sourceInfo.title, - hostname: sourceInfo.hostname, - age: sourceInfo.age, - }; - } - } - - // Estimate total token count from all snippets - const allText = grounding.map(g => g.snippets.join(" ")).join(" "); - const estimatedTokens = estimateTokens(allText); - - result = { grounding, sources, estimatedTokens }; - latencyMs = timed.latencyMs; - rateLimit = timed.rateLimit; + result = braveResult.cached; + latencyMs = braveResult.latencyMs; + rateLimit = braveResult.rateLimit; } // ------------------------------------------------------------------ diff --git a/src/resources/extensions/search-the-web/tool-search.ts b/src/resources/extensions/search-the-web/tool-search.ts index e645a502f..76a9a7c31 100644 --- a/src/resources/extensions/search-the-web/tool-search.ts +++ b/src/resources/extensions/search-the-web/tool-search.ts @@ -10,11 +10,11 @@ * - Rate limit info in details */ -import type { ExtensionAPI } from "@gsd/pi-coding-agent"; -import { truncateHead, formatSize, DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES } from "@gsd/pi-coding-agent"; -import { Text } from "@gsd/pi-tui"; +import type { ExtensionAPI } from "@sf-run/pi-coding-agent"; +import { truncateHead, formatSize, DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES } from "@sf-run/pi-coding-agent"; +import { Text } from "@sf-run/pi-tui"; import { Type } from "@sinclair/typebox"; -import { StringEnum } from "@gsd/pi-ai"; +import { StringEnum } from "@sf-run/pi-ai"; import { LRUTTLCache } from "./cache.js"; import { fetchWithRetryTimed, fetchWithRetry, classifyError, type RateLimitInfo } from "./http.js"; @@ -93,7 +93,7 @@ interface SearchDetails { errorKind?: string; error?: string; retryAfterMs?: number; - provider?: 'tavily' | 'brave' | 'ollama'; + provider?: 'tavily' | 'brave' | 'ollama' | 'combosearch'; } // ============================================================================= @@ -302,6 +302,122 @@ async function executeOllamaSearch( }; } +async function executeBraveSearch( + params: { query: string; effectiveQuery: string; freshness: string | null; wantSummary: boolean }, + signal?: AbortSignal, +): Promise<{ results: CachedSearchResult; latencyMs: number; rateLimit?: RateLimitInfo }> { + const url = new URL("https://api.search.brave.com/res/v1/web/search"); + url.searchParams.append("q", params.effectiveQuery); + url.searchParams.append("count", "10"); + url.searchParams.append("extra_snippets", "true"); + url.searchParams.append("text_decorations", "false"); + + if (params.freshness) { + url.searchParams.append("freshness", params.freshness); + } + if (params.wantSummary) { + url.searchParams.append("summary", "1"); + } + + const timed = await fetchWithRetryTimed(url.toString(), { + method: "GET", + headers: braveHeaders(), + signal, + }, 2); + + const data: BraveSearchResponse = await timed.response.json(); + const rawResults: BraveWebResult[] = data.web?.results ?? []; + const summarizerKey: string | undefined = data.summarizer?.key; + + const queryInfo = data.query; + const queryCorrected = !!(queryInfo?.altered && queryInfo.altered !== queryInfo.original); + const originalQuery = queryCorrected ? (queryInfo?.original ?? params.query) : undefined; + const correctedQuery = queryCorrected ? queryInfo?.altered : undefined; + const moreResultsAvailable = queryInfo?.more_results_available ?? false; + + const normalized = rawResults.map(normalizeBraveResult); + const deduplicated = deduplicateResults(normalized); + + return { + results: { + results: deduplicated, + summarizerKey, + queryCorrected, + originalQuery, + correctedQuery, + moreResultsAvailable, + }, + latencyMs: timed.latencyMs, + rateLimit: timed.rateLimit, + }; +} + +function availableComboProviders(): Array<'tavily' | 'brave' | 'ollama'> { + const providers: Array<'tavily' | 'brave' | 'ollama'> = []; + if (getTavilyApiKey()) providers.push('tavily'); + if (getBraveApiKey()) providers.push('brave'); + if (getOllamaApiKey()) providers.push('ollama'); + return providers; +} + +async function executeComboSearch( + params: { query: string; freshness: string | null; domain?: string; wantSummary: boolean; count: number }, + signal?: AbortSignal, +): Promise<{ results: CachedSearchResult; latencyMs: number; rateLimit?: RateLimitInfo }> { + const providers = availableComboProviders(); + const tasks = providers.map(async (provider) => { + if (provider === 'tavily') { + return executeTavilySearch( + { query: params.query, freshness: params.freshness, domain: params.domain, wantSummary: params.wantSummary }, + signal, + ); + } + if (provider === 'ollama') { + return executeOllamaSearch({ query: params.query, count: Math.max(10, params.count) }, signal); + } + let effectiveQuery = params.query; + if (params.domain && !effectiveQuery.toLowerCase().includes("site:")) { + effectiveQuery = `site:${params.domain} ${effectiveQuery}`; + } + return executeBraveSearch( + { query: params.query, effectiveQuery, freshness: params.freshness, wantSummary: params.wantSummary }, + signal, + ); + }); + + const settled = await Promise.allSettled(tasks); + const fulfilled = settled.filter((entry): entry is PromiseFulfilledResult<{ results: CachedSearchResult; latencyMs: number; rateLimit?: RateLimitInfo }> => entry.status === 'fulfilled'); + + if (fulfilled.length === 0) { + const firstRejected = settled.find((entry): entry is PromiseRejectedResult => entry.status === 'rejected'); + throw firstRejected?.reason ?? new Error("combosearch failed"); + } + + const merged = deduplicateResults( + fulfilled.flatMap((entry) => entry.value.results.results), + ); + const summaryParts = fulfilled + .map((entry) => entry.value.results.summaryText) + .filter((value): value is string => typeof value === "string" && value.trim().length > 0); + const summarizerKey = fulfilled.find((entry) => entry.value.results.summarizerKey)?.value.results.summarizerKey; + const latencyMs = Math.max(...fulfilled.map((entry) => entry.value.latencyMs)); + const rateLimit = fulfilled.find((entry) => entry.value.rateLimit)?.value.rateLimit; + + return { + results: { + results: merged, + summaryText: summaryParts.length > 0 ? summaryParts.join("\n\n") : undefined, + summarizerKey, + queryCorrected: fulfilled.some((entry) => entry.value.results.queryCorrected), + originalQuery: fulfilled.find((entry) => entry.value.results.originalQuery)?.value.results.originalQuery, + correctedQuery: fulfilled.find((entry) => entry.value.results.correctedQuery)?.value.results.correctedQuery, + moreResultsAvailable: fulfilled.some((entry) => entry.value.results.moreResultsAvailable), + }, + latencyMs, + rateLimit, + }; +} + // ============================================================================= // Tool Registration // ============================================================================= @@ -404,7 +520,7 @@ export function registerSearchTool(pi: ExtensionAPI) { // ------------------------------------------------------------------ // Cache lookup (provider-prefixed key) // ------------------------------------------------------------------ - const cacheKey = normalizeQuery(effectiveQuery) + `|f:${freshness || ""}|s:${wantSummary}|p:${provider}`; + const cacheKey = normalizeQuery(effectiveQuery) + `|d:${params.domain || ""}|f:${freshness || ""}|s:${wantSummary}|p:${provider}`; // ── Consecutive duplicate search guard (#949, #1671) ───────────────── // If the LLM keeps calling the same search query, break the loop @@ -490,7 +606,15 @@ export function registerSearchTool(pi: ExtensionAPI) { let latencyMs: number | undefined; let rateLimit: RateLimitInfo | undefined; - if (provider === "tavily") { + if (provider === "combosearch") { + const comboResult = await executeComboSearch( + { query: params.query, freshness, domain: params.domain, wantSummary, count }, + signal, + ); + searchResult = comboResult.results; + latencyMs = comboResult.latencyMs; + rateLimit = comboResult.rateLimit; + } else if (provider === "tavily") { const tavilyResult = await executeTavilySearch( { query: params.query, freshness, domain: params.domain, wantSummary }, signal @@ -507,53 +631,13 @@ export function registerSearchTool(pi: ExtensionAPI) { latencyMs = ollamaResult.latencyMs; rateLimit = ollamaResult.rateLimit; } else { - // ================================================================ - // BRAVE PATH (unchanged API logic) - // ================================================================ - const url = new URL("https://api.search.brave.com/res/v1/web/search"); - url.searchParams.append("q", effectiveQuery); - url.searchParams.append("count", "10"); // Extra for dedup headroom - url.searchParams.append("extra_snippets", "true"); - url.searchParams.append("text_decorations", "false"); - - if (freshness) { - url.searchParams.append("freshness", freshness); - } - if (wantSummary) { - url.searchParams.append("summary", "1"); - } - - const timed = await fetchWithRetryTimed(url.toString(), { - method: "GET", - headers: braveHeaders(), + const braveResult = await executeBraveSearch( + { query: params.query, effectiveQuery, freshness, wantSummary }, signal, - }, 2); - - const data: BraveSearchResponse = await timed.response.json(); - const rawResults: BraveWebResult[] = data.web?.results ?? []; - const summarizerKey: string | undefined = data.summarizer?.key; - - // Extract spellcheck/correction info - const queryInfo = data.query; - const queryCorrected = !!(queryInfo?.altered && queryInfo.altered !== queryInfo.original); - const originalQuery = queryCorrected ? (queryInfo?.original ?? params.query) : undefined; - const correctedQuery = queryCorrected ? queryInfo?.altered : undefined; - const moreResultsAvailable = queryInfo?.more_results_available ?? false; - - // Normalize, deduplicate - const normalized = rawResults.map(normalizeBraveResult); - const deduplicated = deduplicateResults(normalized); - - searchResult = { - results: deduplicated, - summarizerKey, - queryCorrected, - originalQuery, - correctedQuery, - moreResultsAvailable, - }; - latencyMs = timed.latencyMs; - rateLimit = timed.rateLimit; + ); + searchResult = braveResult.results; + latencyMs = braveResult.latencyMs; + rateLimit = braveResult.rateLimit; } // ------------------------------------------------------------------ diff --git a/src/resources/extensions/shared/confirm-ui.ts b/src/resources/extensions/shared/confirm-ui.ts index da5fc1675..f1d7166b9 100644 --- a/src/resources/extensions/shared/confirm-ui.ts +++ b/src/resources/extensions/shared/confirm-ui.ts @@ -15,9 +15,9 @@ * if (!confirmed) return textResult("Cancelled."); */ -import type { ExtensionContext } from "@gsd/pi-coding-agent"; -import { type Theme } from "@gsd/pi-coding-agent"; -import { Key, matchesKey, truncateToWidth, type TUI } from "@gsd/pi-tui"; +import type { ExtensionContext } from "@sf-run/pi-coding-agent"; +import { type Theme } from "@sf-run/pi-coding-agent"; +import { Key, matchesKey, truncateToWidth, type TUI } from "@sf-run/pi-tui"; import { makeUI, GLYPH } from "./ui.js"; export interface ConfirmOptions { diff --git a/src/resources/extensions/shared/format-utils.ts b/src/resources/extensions/shared/format-utils.ts index 226cb4cac..5cd005960 100644 --- a/src/resources/extensions/shared/format-utils.ts +++ b/src/resources/extensions/shared/format-utils.ts @@ -1,8 +1,8 @@ /** - * Shared pure formatting utilities — no @gsd/pi-tui dependency. + * Shared pure formatting utilities — no @sf-run/pi-tui dependency. * * ANSI-aware layout helpers (padRight, joinColumns, centerLine, fitColumns) - * live in layout-utils.ts to avoid pulling @gsd/pi-tui into modules that + * live in layout-utils.ts to avoid pulling @sf-run/pi-tui into modules that * run outside jiti's alias resolution (e.g. HTML report generation via * dynamic import in auto-loop). */ diff --git a/src/resources/extensions/shared/interview-ui.ts b/src/resources/extensions/shared/interview-ui.ts index 66771bc84..5eb121232 100644 --- a/src/resources/extensions/shared/interview-ui.ts +++ b/src/resources/extensions/shared/interview-ui.ts @@ -25,15 +25,15 @@ * Esc exit confirmation */ -import type { ExtensionCommandContext } from "@gsd/pi-coding-agent"; -import { type Theme } from "@gsd/pi-coding-agent"; +import type { ExtensionCommandContext } from "@sf-run/pi-coding-agent"; +import { type Theme } from "@sf-run/pi-coding-agent"; import { Editor, Key, matchesKey, truncateToWidth, type TUI, -} from "@gsd/pi-tui"; +} from "@sf-run/pi-tui"; import { makeUI, INDENT } from "./ui.js"; // ─── Exported types ─────────────────────────────────────────────────────────── diff --git a/src/resources/extensions/shared/layout-utils.ts b/src/resources/extensions/shared/layout-utils.ts index c18695563..96b631413 100644 --- a/src/resources/extensions/shared/layout-utils.ts +++ b/src/resources/extensions/shared/layout-utils.ts @@ -1,13 +1,13 @@ /** - * ANSI-aware TUI layout utilities that depend on @gsd/pi-tui. + * ANSI-aware TUI layout utilities that depend on @sf-run/pi-tui. * * Separated from format-utils.ts so that modules needing only pure * formatting (e.g. HTML report generation) can import format-utils - * without pulling in the @gsd/pi-tui dependency — which fails when + * without pulling in the @sf-run/pi-tui dependency — which fails when * loaded outside jiti's alias resolution context. */ -import { truncateToWidth, visibleWidth } from "@gsd/pi-tui"; +import { truncateToWidth, visibleWidth } from "@sf-run/pi-tui"; // ─── Layout Helpers ─────────────────────────────────────────────────────────── diff --git a/src/resources/extensions/shared/next-action-ui.ts b/src/resources/extensions/shared/next-action-ui.ts index 42d582005..daffc8f3b 100644 --- a/src/resources/extensions/shared/next-action-ui.ts +++ b/src/resources/extensions/shared/next-action-ui.ts @@ -41,9 +41,9 @@ * Pressing Escape also resolves as "not_yet". */ -import type { ExtensionCommandContext } from "@gsd/pi-coding-agent"; -import { type Theme } from "@gsd/pi-coding-agent"; -import { Key, matchesKey, type TUI } from "@gsd/pi-tui"; +import type { ExtensionCommandContext } from "@sf-run/pi-coding-agent"; +import { type Theme } from "@sf-run/pi-coding-agent"; +import { Key, matchesKey, type TUI } from "@sf-run/pi-tui"; import { makeUI } from "./ui.js"; // ─── Public API ─────────────────────────────────────────────────────────────── diff --git a/src/resources/extensions/shared/sanitize.ts b/src/resources/extensions/shared/sanitize.ts index dbc25f2e3..35aeda14a 100644 --- a/src/resources/extensions/shared/sanitize.ts +++ b/src/resources/extensions/shared/sanitize.ts @@ -3,7 +3,7 @@ * Also provides maskEditorLine for masking sensitive TUI editor input. */ -import { CURSOR_MARKER } from "@gsd/pi-tui"; +import { CURSOR_MARKER } from "@sf-run/pi-tui"; const TOKEN_PATTERNS = [ /xoxb-[A-Za-z0-9\-]+/g, // Slack bot tokens diff --git a/src/resources/extensions/shared/tui.ts b/src/resources/extensions/shared/tui.ts index 33977a9d6..b97298a6f 100644 --- a/src/resources/extensions/shared/tui.ts +++ b/src/resources/extensions/shared/tui.ts @@ -1,7 +1,7 @@ // Barrel — TUI-dependent exports. // Import from here when your code needs makeUI, showInterviewRound, // showNextAction, or showConfirm. These all have a transitive dependency -// on @gsd/pi-tui and must not be imported from shared/mod. +// on @sf-run/pi-tui and must not be imported from shared/mod. export { makeUI } from "./ui.js"; export type { UI } from "./ui.js"; diff --git a/src/resources/extensions/shared/ui.ts b/src/resources/extensions/shared/ui.ts index 17588a360..167fe42e0 100644 --- a/src/resources/extensions/shared/ui.ts +++ b/src/resources/extensions/shared/ui.ts @@ -28,8 +28,8 @@ * individual methods don't need it. */ -import { type Theme } from "@gsd/pi-coding-agent"; -import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@gsd/pi-tui"; +import { type Theme } from "@sf-run/pi-coding-agent"; +import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@sf-run/pi-tui"; // ─── Glyphs ─────────────────────────────────────────────────────────────────── // Change these to restyle every cursor, checkbox, and indicator at once. @@ -191,7 +191,7 @@ export interface UI { // ── Editor theme ────────────────────────────────────────────────────────── /** Standard EditorTheme object for use with the Editor component */ - editorTheme: import("@gsd/pi-tui").EditorTheme; + editorTheme: import("@sf-run/pi-tui").EditorTheme; } /** @@ -216,7 +216,7 @@ export function makeUI(theme: Theme, width: number): UI { // ── EditorTheme ──────────────────────────────────────────────────────────── - const editorTheme: import("@gsd/pi-tui").EditorTheme = { + const editorTheme: import("@sf-run/pi-tui").EditorTheme = { borderColor: (s) => theme.fg("accent", s), selectList: { selectedPrefix: (t) => theme.fg("accent", t), diff --git a/src/resources/extensions/slash-commands/audit.ts b/src/resources/extensions/slash-commands/audit.ts index fe7d3f046..4bc4b311f 100644 --- a/src/resources/extensions/slash-commands/audit.ts +++ b/src/resources/extensions/slash-commands/audit.ts @@ -1,4 +1,4 @@ -import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI, ExtensionCommandContext } from "@sf-run/pi-coding-agent"; import { mkdirSync } from "node:fs"; export default function auditCommand(pi: ExtensionAPI) { diff --git a/src/resources/extensions/slash-commands/clear.ts b/src/resources/extensions/slash-commands/clear.ts index 9f6bd5188..611d7a10d 100644 --- a/src/resources/extensions/slash-commands/clear.ts +++ b/src/resources/extensions/slash-commands/clear.ts @@ -1,4 +1,4 @@ -import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI, ExtensionCommandContext } from "@sf-run/pi-coding-agent"; export default function clearCommand(pi: ExtensionAPI) { pi.registerCommand("clear", { diff --git a/src/resources/extensions/slash-commands/create-extension.ts b/src/resources/extensions/slash-commands/create-extension.ts index 35f916e2e..ec24b78b6 100644 --- a/src/resources/extensions/slash-commands/create-extension.ts +++ b/src/resources/extensions/slash-commands/create-extension.ts @@ -1,4 +1,4 @@ -import type { ExtensionAPI } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI } from "@sf-run/pi-coding-agent"; import { showInterviewRound, type Question, type RoundResult } from "../shared/tui.js"; export default function createExtension(pi: ExtensionAPI) { @@ -281,7 +281,7 @@ Then register it in the main extensions index: ## Rules you must follow exactly - Extension entry point: \`export default function (pi: ExtensionAPI): void { ... }\` -- Import type: \`import type { ExtensionAPI, ExtensionContext, ExtensionCommandContext } from "@gsd/pi-coding-agent";\` +- Import type: \`import type { ExtensionAPI, ExtensionContext, ExtensionCommandContext } from "@sf-run/pi-coding-agent";\` - \`pi\` is the registration surface — call \`pi.registerCommand\`, \`pi.registerTool\`, \`pi.on\`, \`pi.registerShortcut\` inside the default export - \`ctx\` (ExtensionCommandContext or ExtensionContext) is passed to handlers and event callbacks — never stored, never assumed available globally - To send a message to the agent: \`pi.sendUserMessage("...")\` or \`pi.sendMessage({ content, display }, { triggerTurn })\` diff --git a/src/resources/extensions/slash-commands/create-slash-command.ts b/src/resources/extensions/slash-commands/create-slash-command.ts index ce6dab4aa..4c8e642c6 100644 --- a/src/resources/extensions/slash-commands/create-slash-command.ts +++ b/src/resources/extensions/slash-commands/create-slash-command.ts @@ -1,4 +1,4 @@ -import type { ExtensionAPI } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI } from "@sf-run/pi-coding-agent"; import { showInterviewRound, type Question, type RoundResult } from "../shared/tui.js"; export default function createSlashCommand(pi: ExtensionAPI) { @@ -225,7 +225,7 @@ Rules you must follow exactly: - To show a text input dialog: \`await ctx.ui.input("prompt", "placeholder")\` — returns the string or null - \`pi\` is captured in closure from the outer \`export default function(pi: ExtensionAPI)\` — use it freely inside the handler - No \`ctx.session\`, no \`ctx.sendMessage\`, no \`args[]\` array — these do not exist -- Import type: \`import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent";\` +- Import type: \`import type { ExtensionAPI, ExtensionCommandContext } from "@sf-run/pi-coding-agent";\` - Export default: \`export default function (pi: ExtensionAPI) { ... }\` After writing the files, run \`/reload\` to load the new command.`; diff --git a/src/resources/extensions/slash-commands/index.ts b/src/resources/extensions/slash-commands/index.ts index 5ea4db77c..3a201a2c4 100644 --- a/src/resources/extensions/slash-commands/index.ts +++ b/src/resources/extensions/slash-commands/index.ts @@ -1,4 +1,4 @@ -import type { ExtensionAPI } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI } from "@sf-run/pi-coding-agent"; import createSlashCommand from "./create-slash-command.js"; import createExtension from "./create-extension.js"; import auditCommand from "./audit.js"; diff --git a/src/resources/extensions/subagent/agents.ts b/src/resources/extensions/subagent/agents.ts index 7f69f3f18..e1601872d 100644 --- a/src/resources/extensions/subagent/agents.ts +++ b/src/resources/extensions/subagent/agents.ts @@ -4,7 +4,7 @@ import * as fs from "node:fs"; import * as path from "node:path"; -import { getAgentDir, parseFrontmatter } from "@gsd/pi-coding-agent"; +import { getAgentDir, parseFrontmatter } from "@sf-run/pi-coding-agent"; const PROJECT_AGENT_DIR_CANDIDATES = [".gsd", ".pi"] as const; diff --git a/src/resources/extensions/subagent/index.ts b/src/resources/extensions/subagent/index.ts index 8bca18bf7..091b1d918 100644 --- a/src/resources/extensions/subagent/index.ts +++ b/src/resources/extensions/subagent/index.ts @@ -17,11 +17,11 @@ import * as crypto from "node:crypto"; import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; -import type { AgentToolResult } from "@gsd/pi-agent-core"; -import type { Message } from "@gsd/pi-ai"; -import { StringEnum } from "@gsd/pi-ai"; -import { type ExtensionAPI, getMarkdownTheme } from "@gsd/pi-coding-agent"; -import { Container, Markdown, Spacer, Text } from "@gsd/pi-tui"; +import type { AgentToolResult } from "@sf-run/pi-agent-core"; +import type { Message } from "@sf-run/pi-ai"; +import { StringEnum } from "@sf-run/pi-ai"; +import { type ExtensionAPI, getMarkdownTheme } from "@sf-run/pi-coding-agent"; +import { Container, Markdown, Spacer, Text } from "@sf-run/pi-tui"; import { Type } from "@sinclair/typebox"; import { formatTokenCount } from "../shared/mod.js"; import { getCurrentPhase } from "../shared/gsd-phase-state.js"; diff --git a/src/resources/extensions/subagent/isolation.ts b/src/resources/extensions/subagent/isolation.ts index e862e65ff..d55026aa5 100644 --- a/src/resources/extensions/subagent/isolation.ts +++ b/src/resources/extensions/subagent/isolation.ts @@ -491,7 +491,7 @@ export async function mergeDeltaPatches( export function readIsolationMode(): IsolationMode { try { - const { getAgentDir } = require("@gsd/pi-coding-agent"); + const { getAgentDir } = require("@sf-run/pi-coding-agent"); const settingsPath = path.join(getAgentDir(), "settings.json"); if (!fs.existsSync(settingsPath)) return "none"; const settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8")); diff --git a/src/resources/extensions/ttsr/index.ts b/src/resources/extensions/ttsr/index.ts index 64e25dd86..83735ce28 100644 --- a/src/resources/extensions/ttsr/index.ts +++ b/src/resources/extensions/ttsr/index.ts @@ -13,8 +13,8 @@ * agent_end → if pending violation, inject rule via sendMessage */ -import type { ExtensionAPI, ExtensionContext } from "@gsd/pi-coding-agent"; -import type { AssistantMessageEvent } from "@gsd/pi-ai"; +import type { ExtensionAPI, ExtensionContext } from "@sf-run/pi-coding-agent"; +import type { AssistantMessageEvent } from "@sf-run/pi-ai"; import { readFileSync } from "node:fs"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; diff --git a/src/resources/extensions/ttsr/ttsr-manager.ts b/src/resources/extensions/ttsr/ttsr-manager.ts index 0e59d124e..8aa530fe6 100644 --- a/src/resources/extensions/ttsr/ttsr-manager.ts +++ b/src/resources/extensions/ttsr/ttsr-manager.ts @@ -26,7 +26,7 @@ let nativeTtsr: { try { // Dynamic import to avoid hard dependency — gracefully degrades to JS. - const native = await import("@gsd/native"); + const native = await import("@sf-run/native"); if (native.ttsrCompileRules && native.ttsrCheckBuffer && native.ttsrFreeRules) { nativeTtsr = { ttsrCompileRules: native.ttsrCompileRules, diff --git a/src/resources/extensions/universal-config/index.ts b/src/resources/extensions/universal-config/index.ts index bd0492858..7017ba7bc 100644 --- a/src/resources/extensions/universal-config/index.ts +++ b/src/resources/extensions/universal-config/index.ts @@ -14,7 +14,7 @@ * - /configs command (slash command) */ -import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI, ExtensionCommandContext } from "@sf-run/pi-coding-agent"; import { Type } from "@sinclair/typebox"; import { discoverAllConfigs } from "./discovery.js"; import { formatDiscoveryForTool, formatDiscoveryForCommand } from "./format.js"; diff --git a/src/resources/extensions/voice/index.ts b/src/resources/extensions/voice/index.ts index 5cfedc195..8d9c3682b 100644 --- a/src/resources/extensions/voice/index.ts +++ b/src/resources/extensions/voice/index.ts @@ -1,7 +1,7 @@ -import type { ExtensionAPI, ExtensionContext } from "@gsd/pi-coding-agent"; +import type { ExtensionAPI, ExtensionContext } from "@sf-run/pi-coding-agent"; import { shortcutDesc } from "../shared/mod.js"; -import type { AssistantMessage } from "@gsd/pi-ai"; -import { isKeyRelease, Key, matchesKey, truncateToWidth, visibleWidth } from "@gsd/pi-tui"; +import type { AssistantMessage } from "@sf-run/pi-ai"; +import { isKeyRelease, Key, matchesKey, truncateToWidth, visibleWidth } from "@sf-run/pi-tui"; import { spawn, execFileSync, type ChildProcess } from "node:child_process"; import * as fs from "node:fs"; import * as path from "node:path"; diff --git a/src/resources/skills/github-workflows/references/gh/SKILL.md b/src/resources/skills/github-workflows/references/gh/SKILL.md index 05d40f337..eeb00b6ad 100644 --- a/src/resources/skills/github-workflows/references/gh/SKILL.md +++ b/src/resources/skills/github-workflows/references/gh/SKILL.md @@ -61,7 +61,7 @@ repository point to a known GitHub host. **RULE: Pass `-R` (or `--repo`) on EVERY `gh` command:** ```bash -gh -R gsd-build/gsd-2 +gh -R singularity-forge/sf-run ``` This applies to ALL `gh` subcommands: `pr`, `issue`, `run`, `api`, `release`, `project`, etc. @@ -78,47 +78,47 @@ This applies to ALL `gh` subcommands: `pr`, `issue`, `run`, `api`, `release`, `p ```bash # List open PRs -gh pr list -R gsd-build/gsd-2 +gh pr list -R singularity-forge/sf-run # View PR details -gh pr view -R gsd-build/gsd-2 +gh pr view -R singularity-forge/sf-run # Check PR CI status -gh pr checks -R gsd-build/gsd-2 +gh pr checks -R singularity-forge/sf-run # Create PR -gh pr create -R gsd-build/gsd-2 --title "title" --body "body" +gh pr create -R singularity-forge/sf-run --title "title" --body "body" # View PR comments -gh api repos/gsd-build/gsd-2/pulls//comments +gh api repos/singularity-forge/sf-run/pulls//comments ``` ### Issues ```bash # List issues -gh issue list -R gsd-build/gsd-2 +gh issue list -R singularity-forge/sf-run # List by label -gh issue list -R gsd-build/gsd-2 --label "priority:p1" --state open +gh issue list -R singularity-forge/sf-run --label "priority:p1" --state open # Create issue with labels and milestone # NOTE: Do NOT use labels for issue classification (bug, feature, etc.) # Use labels for metadata (priority, status, auto-generated) only. # Issue classification uses GitHub Issue Types, set via GraphQL after creation. -gh issue create -R gsd-build/gsd-2 \ +gh issue create -R singularity-forge/sf-run \ --title "feat: add feature X" \ --label "priority:p1" \ --milestone "v1.0" # View issue -gh issue view -R gsd-build/gsd-2 +gh issue view -R singularity-forge/sf-run # Close issue with comment -gh issue close -R gsd-build/gsd-2 --comment "Implemented in PR #N" +gh issue close -R singularity-forge/sf-run --comment "Implemented in PR #N" # Edit labels on issue -gh issue edit -R gsd-build/gsd-2 \ +gh issue edit -R singularity-forge/sf-run \ --add-label "status:in-progress" \ --remove-label "status:needs-grooming" ``` @@ -129,7 +129,7 @@ gh issue edit -R gsd-build/gsd-2 \ ```bash # Step 1: Create the issue (returns URL) -ISSUE_URL=$(gh issue create -R gsd-build/gsd-2 \ +ISSUE_URL=$(gh issue create -R singularity-forge/sf-run \ --title "..." --body "...") # Step 2: Set the issue type via GraphQL @@ -145,11 +145,11 @@ Replace `"Bug"` with the appropriate type name (`"Feature Request"`, `"Task"`, e ```bash # List all labels -gh label list -R gsd-build/gsd-2 +gh label list -R singularity-forge/sf-run # Create label gh label create "priority:p1" --color "E99695" \ - --description "High priority" -R gsd-build/gsd-2 + --description "High priority" -R singularity-forge/sf-run ``` See [labels.md](./references/labels.md) for the full taxonomy and color codes. @@ -165,7 +165,7 @@ gh project create --owner gsd-build --title "gsd-2 Backlog" # Add issue to project gh project item-add 1 --owner gsd-build \ - --url https://github.com/gsd-build/gsd-2/issues/42 + --url https://github.com/singularity-forge/sf-run/issues/42 ``` See [projects-v2.md](./references/projects-v2.md) for field creation and item editing commands. @@ -176,14 +176,14 @@ See [projects-v2.md](./references/projects-v2.md) for field creation and item ed ```bash # List milestones -gh api repos/gsd-build/gsd-2/milestones +gh api repos/singularity-forge/sf-run/milestones # Create milestone -gh api repos/gsd-build/gsd-2/milestones \ +gh api repos/singularity-forge/sf-run/milestones \ -X POST -f title="v1.0" -f due_on="2026-03-31T00:00:00Z" # Assign milestone to issue -gh api repos/gsd-build/gsd-2/issues/42 \ +gh api repos/singularity-forge/sf-run/issues/42 \ -X PATCH -F milestone=1 ``` @@ -193,49 +193,49 @@ See [milestones.md](./references/milestones.md) for full CRUD reference. ```bash # List recent runs -gh run list -R gsd-build/gsd-2 --limit 5 +gh run list -R singularity-forge/sf-run --limit 5 # View specific run -gh run view -R gsd-build/gsd-2 +gh run view -R singularity-forge/sf-run # View failed job logs -gh run view -R gsd-build/gsd-2 --log-failed +gh run view -R singularity-forge/sf-run --log-failed ``` ### Releases ```bash # List releases -gh release list -R gsd-build/gsd-2 +gh release list -R singularity-forge/sf-run # View latest release -gh release view --repo gsd-build/gsd-2 +gh release view --repo singularity-forge/sf-run ``` ### API (Direct) ```bash # GET request -gh api repos/gsd-build/gsd-2 +gh api repos/singularity-forge/sf-run # POST with fields -gh api repos/gsd-build/gsd-2/issues -f title="Bug" -f body="Details" +gh api repos/singularity-forge/sf-run/issues -f title="Bug" -f body="Details" # GraphQL gh api graphql -f query='{ viewer { login } }' # Paginated results -gh api repos/gsd-build/gsd-2/contributors --paginate +gh api repos/singularity-forge/sf-run/contributors --paginate ``` ### Repository ```bash # Clone -gh repo clone gsd-build/gsd-2 +gh repo clone singularity-forge/sf-run # View repo info -gh repo view -R gsd-build/gsd-2 +gh repo view -R singularity-forge/sf-run ``` @@ -246,13 +246,13 @@ gh repo view -R gsd-build/gsd-2 ```bash # JSON output -gh pr list -R gsd-build/gsd-2 --json number,title,state +gh pr list -R singularity-forge/sf-run --json number,title,state # JQ filtering -gh pr list -R gsd-build/gsd-2 --json number,title --jq '.[].title' +gh pr list -R singularity-forge/sf-run --json number,title --jq '.[].title' # Template formatting -gh pr list -R gsd-build/gsd-2 --json number,title \ +gh pr list -R singularity-forge/sf-run --json number,title \ --template '{{range .}}#{{.number}} {{.title}}{{"\n"}}{{end}}' ``` diff --git a/src/security-overrides.ts b/src/security-overrides.ts index 9a0609d6c..ad80a9638 100644 --- a/src/security-overrides.ts +++ b/src/security-overrides.ts @@ -8,7 +8,7 @@ * Precedence: env var > settings.json > built-in defaults */ -import { type SettingsManager, setAllowedCommandPrefixes } from '@gsd/pi-coding-agent' +import { type SettingsManager, setAllowedCommandPrefixes } from '@sf-run/pi-coding-agent' import { setFetchAllowedUrls } from './resources/extensions/search-the-web/url-utils.js' export function applySecurityOverrides(settingsManager: SettingsManager): void { diff --git a/src/tests/app-smoke.test.ts b/src/tests/app-smoke.test.ts index 8a43d8cbb..0c11ea3f5 100644 --- a/src/tests/app-smoke.test.ts +++ b/src/tests/app-smoke.test.ts @@ -270,7 +270,7 @@ test("initResources skips copy when managed version matches current version", as test("loadStoredEnvKeys hydrates process.env from auth.json", async (t) => { const { loadStoredEnvKeys } = await import("../wizard.ts"); - const { AuthStorage } = await import("@gsd/pi-coding-agent"); + const { AuthStorage } = await import("@sf-run/pi-coding-agent"); const tmp = mkdtempSync(join(tmpdir(), "gsd-wizard-test-")); const authPath = join(tmp, "auth.json"); @@ -319,7 +319,7 @@ test("loadStoredEnvKeys hydrates process.env from auth.json", async (t) => { test("loadStoredEnvKeys does not overwrite existing env vars", async (t) => { const { loadStoredEnvKeys } = await import("../wizard.ts"); - const { AuthStorage } = await import("@gsd/pi-coding-agent"); + const { AuthStorage } = await import("@sf-run/pi-coding-agent"); const tmp = mkdtempSync(join(tmpdir(), "gsd-wizard-nooverwrite-")); const authPath = join(tmp, "auth.json"); diff --git a/src/tests/create-gsd-extension-paths.test.ts b/src/tests/create-gsd-extension-paths.test.ts index 7aff613b3..ce1d706c5 100644 --- a/src/tests/create-gsd-extension-paths.test.ts +++ b/src/tests/create-gsd-extension-paths.test.ts @@ -3,7 +3,7 @@ * community extension install path (~/.pi/agent/extensions/) instead of the * bundled-only path (~/.gsd/agent/extensions/). * - * Bug: https://github.com/gsd-build/gsd-2/issues/3131 + * Bug: https://github.com/singularity-forge/sf-run/issues/3131 * * ~/.gsd/agent/extensions/ is reserved for bundled extensions synced from * the gsd-pi package. Community/user extensions must use ~/.pi/agent/extensions/. diff --git a/src/tests/extension-discovery.test.ts b/src/tests/extension-discovery.test.ts index 03bc8bdd8..9ac2d321e 100644 --- a/src/tests/extension-discovery.test.ts +++ b/src/tests/extension-discovery.test.ts @@ -47,7 +47,7 @@ describe('resolveExtensionEntries', () => { const dir = makeTempDir() t.after(() => rmSync(dir, { recursive: true, force: true })); writeFileSync(join(dir, 'package.json'), JSON.stringify({ - name: '@gsd/cmux', + name: '@sf-run/cmux', pi: {} })) writeFileSync(join(dir, 'index.js'), 'export function utility() {}') diff --git a/src/tests/extension-load-perf.test.ts b/src/tests/extension-load-perf.test.ts index 0142ff5e2..3256210f9 100644 --- a/src/tests/extension-load-perf.test.ts +++ b/src/tests/extension-load-perf.test.ts @@ -1,7 +1,7 @@ /** * Extension loading performance test * - * Regression test for https://github.com/gsd-build/gsd-2/issues/2108 + * Regression test for https://github.com/singularity-forge/sf-run/issues/2108 * * Verifies that loading multiple extensions sharing common dependencies * does NOT re-compile those dependencies for each extension. The jiti diff --git a/src/tests/headless-cli-surface.test.ts b/src/tests/headless-cli-surface.test.ts index 3bf552a7c..3771fb3bf 100644 --- a/src/tests/headless-cli-surface.test.ts +++ b/src/tests/headless-cli-surface.test.ts @@ -2,7 +2,7 @@ * Tests for S02 CLI surface — --output-format, exit codes, HeadlessJsonResult, --resume. * * Uses extracted parsing logic (mirrors headless.ts) and direct imports from - * headless-types.ts / headless-events.ts to avoid transitive @gsd/native + * headless-types.ts / headless-events.ts to avoid transitive @sf-run/native * import that breaks in test environment. */ diff --git a/src/tests/headless-events.test.ts b/src/tests/headless-events.test.ts index 4aeae8f39..9957c2350 100644 --- a/src/tests/headless-events.test.ts +++ b/src/tests/headless-events.test.ts @@ -5,7 +5,7 @@ * the headless orchestrator to reduce stdout noise for orchestrators. * * Uses extracted parsing logic (mirrors headless.ts) to avoid - * transitive @gsd/native import that breaks in test environment. + * transitive @sf-run/native import that breaks in test environment. */ import test from 'node:test' diff --git a/src/tests/integration/web-bridge-contract.test.ts b/src/tests/integration/web-bridge-contract.test.ts index 3de7fd6f6..09bf7409e 100644 --- a/src/tests/integration/web-bridge-contract.test.ts +++ b/src/tests/integration/web-bridge-contract.test.ts @@ -10,7 +10,7 @@ import { StringDecoder } from "node:string_decoder"; const repoRoot = process.cwd(); const bridge = await import("../../web/bridge-service.ts"); const onboarding = await import("../../web/onboarding-service.ts"); -const { AuthStorage } = await import("@gsd/pi-coding-agent"); +const { AuthStorage } = await import("@sf-run/pi-coding-agent"); const bootRoute = await import("../../../web/app/api/boot/route.ts"); const commandRoute = await import("../../../web/app/api/session/command/route.ts"); const eventsRoute = await import("../../../web/app/api/session/events/route.ts"); diff --git a/src/tests/integration/web-bridge-package-root.test.ts b/src/tests/integration/web-bridge-package-root.test.ts index 8ccab075c..c9a8c4373 100644 --- a/src/tests/integration/web-bridge-package-root.test.ts +++ b/src/tests/integration/web-bridge-package-root.test.ts @@ -1,7 +1,7 @@ /** * Regression tests for the default package root fallback in bridge-service. * - * Issue: gsd-build/gsd-2#1881 + * Issue: singularity-forge/sf-run#1881 * The standalone Next.js bundle bakes import.meta.url at build time with the * CI runner's absolute path. On Windows, fileURLToPath() rejects the Unix * file:// URL at module load time, 500-ing all API routes. diff --git a/src/tests/integration/web-live-interaction-contract.test.ts b/src/tests/integration/web-live-interaction-contract.test.ts index ce473ff40..725c8ab33 100644 --- a/src/tests/integration/web-live-interaction-contract.test.ts +++ b/src/tests/integration/web-live-interaction-contract.test.ts @@ -10,7 +10,7 @@ import { StringDecoder } from "node:string_decoder"; const repoRoot = process.cwd(); const bridge = await import("../../web/bridge-service.ts"); const onboarding = await import("../../web/onboarding-service.ts"); -const { AuthStorage } = await import("@gsd/pi-coding-agent"); +const { AuthStorage } = await import("@sf-run/pi-coding-agent"); const commandRoute = await import("../../../web/app/api/session/command/route.ts"); const eventsRoute = await import("../../../web/app/api/session/events/route.ts"); diff --git a/src/tests/integration/web-live-state-contract.test.ts b/src/tests/integration/web-live-state-contract.test.ts index bed3b44c2..90322b61b 100644 --- a/src/tests/integration/web-live-state-contract.test.ts +++ b/src/tests/integration/web-live-state-contract.test.ts @@ -10,7 +10,7 @@ import { StringDecoder } from "node:string_decoder"; const repoRoot = process.cwd(); const bridge = await import("../../web/bridge-service.ts"); const onboarding = await import("../../web/onboarding-service.ts"); -const { AuthStorage } = await import("@gsd/pi-coding-agent"); +const { AuthStorage } = await import("@sf-run/pi-coding-agent"); const commandRoute = await import("../../../web/app/api/session/command/route.ts"); const manageRoute = await import("../../../web/app/api/session/manage/route.ts"); const eventsRoute = await import("../../../web/app/api/session/events/route.ts"); diff --git a/src/tests/integration/web-mode-assembled.test.ts b/src/tests/integration/web-mode-assembled.test.ts index 6bc3cafa5..16511468a 100644 --- a/src/tests/integration/web-mode-assembled.test.ts +++ b/src/tests/integration/web-mode-assembled.test.ts @@ -20,7 +20,7 @@ const { dispatchBrowserSlashCommand, getBrowserSlashCommandTerminalNotice, } = await import("../../../web/lib/browser-slash-command-dispatch.ts"); -const { AuthStorage } = await import("@gsd/pi-coding-agent"); +const { AuthStorage } = await import("@sf-run/pi-coding-agent"); // --------------------------------------------------------------------------- // Test infrastructure (shared with web-mode-onboarding.test.ts) diff --git a/src/tests/integration/web-mode-onboarding.test.ts b/src/tests/integration/web-mode-onboarding.test.ts index 8977a42cf..78df59c60 100644 --- a/src/tests/integration/web-mode-onboarding.test.ts +++ b/src/tests/integration/web-mode-onboarding.test.ts @@ -23,7 +23,7 @@ const onboarding = await import("../../web/onboarding-service.ts"); const bootRoute = await import("../../../web/app/api/boot/route.ts"); const onboardingRoute = await import("../../../web/app/api/onboarding/route.ts"); const commandRoute = await import("../../../web/app/api/session/command/route.ts"); -const { AuthStorage } = await import("@gsd/pi-coding-agent"); +const { AuthStorage } = await import("@sf-run/pi-coding-agent"); class FakeRpcChild extends EventEmitter { stdin = new PassThrough(); diff --git a/src/tests/integration/web-onboarding-contract.test.ts b/src/tests/integration/web-onboarding-contract.test.ts index 016c7ae1e..289a12d22 100644 --- a/src/tests/integration/web-onboarding-contract.test.ts +++ b/src/tests/integration/web-onboarding-contract.test.ts @@ -13,7 +13,7 @@ const onboarding = await import("../../web/onboarding-service.ts"); const bootRoute = await import("../../../web/app/api/boot/route.ts"); const onboardingRoute = await import("../../../web/app/api/onboarding/route.ts"); const commandRoute = await import("../../../web/app/api/session/command/route.ts"); -const { AuthStorage } = await import("@gsd/pi-coding-agent"); +const { AuthStorage } = await import("@sf-run/pi-coding-agent"); const ONBOARDING_ENV_KEYS = [ "GITHUB_TOKEN", diff --git a/src/tests/integration/web-session-parity-contract.test.ts b/src/tests/integration/web-session-parity-contract.test.ts index 9e8b1afcf..4d46b57d8 100644 --- a/src/tests/integration/web-session-parity-contract.test.ts +++ b/src/tests/integration/web-session-parity-contract.test.ts @@ -14,7 +14,7 @@ const onboarding = await import("../../web/onboarding-service.ts") const browserRoute = await import("../../../web/app/api/session/browser/route.ts") const manageRoute = await import("../../../web/app/api/session/manage/route.ts") const gitRoute = await import("../../../web/app/api/git/route.ts") -const { AuthStorage } = await import("@gsd/pi-coding-agent") +const { AuthStorage } = await import("@sf-run/pi-coding-agent") class FakeRpcChild extends EventEmitter { stdin = new PassThrough() diff --git a/src/tests/node-modules-symlink.test.ts b/src/tests/node-modules-symlink.test.ts index fcf94e96e..f83a47e19 100644 --- a/src/tests/node-modules-symlink.test.ts +++ b/src/tests/node-modules-symlink.test.ts @@ -118,8 +118,8 @@ test("pnpm layout: merged node_modules contains entries from both hoisted and in // @sinclair/ ← external scoped dep // gsd-pi/ ← package root // node_modules/ - // @gsd/ ← workspace scope (NOT hoisted) - // @gsd-build/ ← workspace scope (NOT hoisted) + // @sf-run/ ← workspace scope (NOT hoisted) + // @singularity-forge/ ← workspace scope (NOT hoisted) const tmp = mkdtempSync(join(tmpdir(), "gsd-pnpm-merge-")); t.after(() => rmSync(tmp, { recursive: true, force: true })); @@ -163,7 +163,7 @@ test("pnpm layout: merged node_modules contains entries from both hoisted and in // Verify: workspace packages resolve through internal symlinks assert.ok(existsSync(join(agentNodeModules, "@gsd")), "@gsd should resolve"); - assert.ok(existsSync(join(agentNodeModules, "@gsd", "pi-ai")), "@gsd/pi-ai should resolve"); + assert.ok(existsSync(join(agentNodeModules, "@gsd", "pi-ai")), "@sf-run/pi-ai should resolve"); assert.ok(existsSync(join(agentNodeModules, "@gsd-build")), "@gsd-build should resolve"); // Verify: gsd-pi itself is NOT symlinked (it's the package root, not a dep) diff --git a/src/tests/non-extension-library.test.ts b/src/tests/non-extension-library.test.ts index e263468b8..ac5c40e42 100644 --- a/src/tests/non-extension-library.test.ts +++ b/src/tests/non-extension-library.test.ts @@ -57,7 +57,7 @@ describe('isNonExtensionLibrary — defense-in-depth for #1709', () => { const libDir = join(root, 'cmux') mkdirSync(libDir) writeFileSync(join(libDir, 'package.json'), JSON.stringify({ - name: '@gsd/cmux', + name: '@sf-run/cmux', description: 'cmux integration library — used by other extensions, not an extension itself', pi: {} })) diff --git a/src/tests/pi-ai-event-stream-factory.test.ts b/src/tests/pi-ai-event-stream-factory.test.ts index e43b1df64..8b3e6ce85 100644 --- a/src/tests/pi-ai-event-stream-factory.test.ts +++ b/src/tests/pi-ai-event-stream-factory.test.ts @@ -3,9 +3,9 @@ import assert from "node:assert/strict"; import { AssistantMessageEventStream, createAssistantMessageEventStream, -} from "@gsd/pi-ai"; +} from "@sf-run/pi-ai"; -describe("@gsd/pi-ai event stream exports", () => { +describe("@sf-run/pi-ai event stream exports", () => { it("exports createAssistantMessageEventStream for package consumers", () => { assert.equal(typeof createAssistantMessageEventStream, "function"); const stream = createAssistantMessageEventStream(); diff --git a/src/tests/provider.test.ts b/src/tests/provider.test.ts index 8631aaf76..81a4848f9 100644 --- a/src/tests/provider.test.ts +++ b/src/tests/provider.test.ts @@ -106,6 +106,16 @@ test('resolveSearchProvider returns brave when both keys set and preference is b }) }) +test('resolveSearchProvider returns combosearch when preference is combosearch and any source is available', async () => { + const { resolveSearchProvider } = await import( + '../resources/extensions/search-the-web/provider.ts' + ) + withEnv({ TAVILY_API_KEY: 'tvly-test', BRAVE_API_KEY: undefined, OLLAMA_API_KEY: undefined }, () => { + const result = resolveSearchProvider('combosearch') + assert.equal(result, 'combosearch') + }) +}) + test('resolveSearchProvider returns null when neither key is set', async () => { const { resolveSearchProvider } = await import( '../resources/extensions/search-the-web/provider.ts' @@ -185,6 +195,9 @@ test('setSearchProviderPreference writes to auth.json via AuthStorage', async (t setSearchProviderPreference('tavily', authPath) assert.equal(getSearchProviderPreference(authPath), 'tavily') + setSearchProviderPreference('combosearch', authPath) + assert.equal(getSearchProviderPreference(authPath), 'combosearch') + // Round-trip: change to auto setSearchProviderPreference('auto', authPath) assert.equal(getSearchProviderPreference(authPath), 'auto') diff --git a/src/tests/resolve-ts-loader.test.ts b/src/tests/resolve-ts-loader.test.ts index 6c81a6a32..8ab680a59 100644 --- a/src/tests/resolve-ts-loader.test.ts +++ b/src/tests/resolve-ts-loader.test.ts @@ -6,7 +6,7 @@ import { load as loadWithTestLoader, resolve as resolveWithTestLoader } from ".. const nextResolve = async (specifier: string) => ({ url: specifier }) const cases = [ - ["@gsd/pi-coding-agent", "../../packages/pi-coding-agent/src/index.ts"], + ["@sf-run/pi-coding-agent", "../../packages/pi-coding-agent/src/index.ts"], ] as const test("resolve-ts loader redirects pi-coding-agent bare imports to the workspace source entrypoint", async () => { diff --git a/src/tests/search-provider-command.test.ts b/src/tests/search-provider-command.test.ts index 0df49f87c..29cf65972 100644 --- a/src/tests/search-provider-command.test.ts +++ b/src/tests/search-provider-command.test.ts @@ -191,7 +191,7 @@ test('direct arg "auto" sets preference and notifies', async (t) => { // 4. No arg — shows select UI, user picks one // ═══════════════════════════════════════════════════════════════════════════ -test('no arg shows select UI with 3 options, user picks brave', async () => { +test('no arg shows select UI with 5 options, user picks brave', async () => { const cmd = await loadCommand() await withEnv({ TAVILY_API_KEY: 'tvly-test', BRAVE_API_KEY: 'BSA-test' }, async () => { @@ -200,13 +200,14 @@ test('no arg shows select UI with 3 options, user picks brave', async () => { // Select UI shown assert.equal(ctx.ui.selectCalls.length, 1, 'should show select UI') - assert.equal(ctx.ui.selectCalls[0].options.length, 4) + assert.equal(ctx.ui.selectCalls[0].options.length, 5) // Options show key status assert.match(ctx.ui.selectCalls[0].options[0], /tavily \(key: ✓\)/) assert.match(ctx.ui.selectCalls[0].options[1], /brave \(key: ✓\)/) assert.match(ctx.ui.selectCalls[0].options[2], /ollama \(key:/) - assert.equal(ctx.ui.selectCalls[0].options[3], 'auto') + assert.match(ctx.ui.selectCalls[0].options[3], /combosearch \(/) + assert.equal(ctx.ui.selectCalls[0].options[4], 'auto') // Title shows current preference assert.match(ctx.ui.selectCalls[0].title, /current:/) @@ -263,19 +264,19 @@ test('invalid arg "google" falls back to interactive select', async () => { }) // ═══════════════════════════════════════════════════════════════════════════ -// 7. Tab completion — all 3 options when prefix is empty +// 7. Tab completion — all options when prefix is empty // ═══════════════════════════════════════════════════════════════════════════ -test('tab completion returns all 4 options when prefix is empty', async () => { +test('tab completion returns all 5 options when prefix is empty', async () => { const cmd = await loadCommand() withEnv({ TAVILY_API_KEY: 'tvly-test', BRAVE_API_KEY: 'BSA-test' }, () => { const items = cmd.getArgumentCompletions!('') assert.ok(items, 'completions should not be null') - assert.equal(items!.length, 4) + assert.equal(items!.length, 5) const values = items!.map((i: any) => i.value) - assert.deepEqual(values, ['tavily', 'brave', 'ollama', 'auto']) + assert.deepEqual(values, ['tavily', 'brave', 'ollama', 'combosearch', 'auto']) // Each item has label and description assert.ok(items!.every((i: any) => i.label), 'every item should have a label') @@ -324,7 +325,7 @@ test('notify message shows effective provider (fallback case)', async () => { test('notify message shows "none" when no API keys available', async () => { const cmd = await loadCommand() - await withEnv({ TAVILY_API_KEY: undefined, BRAVE_API_KEY: undefined }, async () => { + await withEnv({ TAVILY_API_KEY: undefined, BRAVE_API_KEY: undefined, OLLAMA_API_KEY: undefined }, async () => { const ctx = makeMockCtx() await cmd.handler('auto', ctx) diff --git a/src/tests/security-overrides.test.ts b/src/tests/security-overrides.test.ts index 826065dbd..de819f006 100644 --- a/src/tests/security-overrides.test.ts +++ b/src/tests/security-overrides.test.ts @@ -1,6 +1,6 @@ import { describe, it, beforeEach, afterEach } from "node:test"; import assert from "node:assert/strict"; -import { SettingsManager, getAllowedCommandPrefixes, SAFE_COMMAND_PREFIXES, setAllowedCommandPrefixes } from "@gsd/pi-coding-agent"; +import { SettingsManager, getAllowedCommandPrefixes, SAFE_COMMAND_PREFIXES, setAllowedCommandPrefixes } from "@sf-run/pi-coding-agent"; import { getFetchAllowedUrls, setFetchAllowedUrls } from "../resources/extensions/search-the-web/url-utils.ts"; import { applySecurityOverrides } from "../security-overrides.ts"; diff --git a/src/tests/tui-autocomplete-ghost-lines.test.ts b/src/tests/tui-autocomplete-ghost-lines.test.ts index 601692e2a..1b47cd0a7 100644 --- a/src/tests/tui-autocomplete-ghost-lines.test.ts +++ b/src/tests/tui-autocomplete-ghost-lines.test.ts @@ -1,6 +1,6 @@ import { describe, it } from "node:test"; import assert from "node:assert/strict"; -import { CURSOR_MARKER, TUI, type Component, type Terminal } from "@gsd/pi-tui"; +import { CURSOR_MARKER, TUI, type Component, type Terminal } from "@sf-run/pi-tui"; class MockTTYTerminal implements Terminal { public writtenData: string[] = []; diff --git a/src/tests/tui-content-cursor-desync.test.ts b/src/tests/tui-content-cursor-desync.test.ts index b2a99c206..66565f337 100644 --- a/src/tests/tui-content-cursor-desync.test.ts +++ b/src/tests/tui-content-cursor-desync.test.ts @@ -9,7 +9,7 @@ import { describe, it } from "node:test"; import assert from "node:assert/strict"; -import { CURSOR_MARKER, TUI, type Component, type Terminal } from "@gsd/pi-tui"; +import { CURSOR_MARKER, TUI, type Component, type Terminal } from "@sf-run/pi-tui"; class MockTTYTerminal implements Terminal { public writtenData: string[] = []; diff --git a/src/tests/tui-non-tty-render-loop.test.ts b/src/tests/tui-non-tty-render-loop.test.ts index 2e6e4677d..4295637dc 100644 --- a/src/tests/tui-non-tty-render-loop.test.ts +++ b/src/tests/tui-non-tty-render-loop.test.ts @@ -6,13 +6,13 @@ * start in that scenario — otherwise it runs at ~4,600 renders/second * consuming 500%+ CPU doing nothing useful. * - * Regression test for: https://github.com/gsd-build/gsd-2/issues/3095 + * Regression test for: https://github.com/singularity-forge/sf-run/issues/3095 */ import { describe, it, beforeEach } from "node:test"; import assert from "node:assert/strict"; -import { ProcessTerminal } from "@gsd/pi-tui"; -import { TUI } from "@gsd/pi-tui"; -import type { Terminal } from "@gsd/pi-tui"; +import { ProcessTerminal } from "@sf-run/pi-tui"; +import { TUI } from "@sf-run/pi-tui"; +import type { Terminal } from "@sf-run/pi-tui"; /** * A mock terminal that tracks writes and render activity. diff --git a/src/wizard.ts b/src/wizard.ts index f156161ff..9f5e1d9c2 100644 --- a/src/wizard.ts +++ b/src/wizard.ts @@ -1,4 +1,4 @@ -import type { AuthStorage } from '@gsd/pi-coding-agent' +import type { AuthStorage } from '@sf-run/pi-coding-agent' // ─── Env hydration ──────────────────────────────────────────────────────────── diff --git a/studio/package.json b/studio/package.json index 97ceb3157..4a79c5963 100644 --- a/studio/package.json +++ b/studio/package.json @@ -1,5 +1,5 @@ { - "name": "@gsd/studio", + "name": "@sf-run/studio", "private": true, "version": "0.0.0", "type": "module", diff --git a/vscode-extension/README.md b/vscode-extension/README.md index 899012880..c8352b2e5 100644 --- a/vscode-extension/README.md +++ b/vscode-extension/README.md @@ -1,6 +1,6 @@ # GSD-2 — VS Code Extension -Control the [GSD-2 coding agent](https://github.com/gsd-build/gsd-2) directly from VS Code. Run autonomous coding sessions, chat with `@gsd`, monitor agent activity in real-time, review and accept/reject changes, and manage your workflow — all without leaving the editor. +Control the [GSD-2 coding agent](https://github.com/singularity-forge/sf-run) directly from VS Code. Run autonomous coding sessions, chat with `@gsd`, monitor agent activity in real-time, review and accept/reject changes, and manage your workflow — all without leaving the editor. ![GSD Extension Overview](docs/images/overview.png) @@ -191,6 +191,6 @@ The extension spawns `gsd --mode rpc` and communicates over JSON-RPC via stdin/s ## Links -- [GSD Documentation](https://github.com/gsd-build/gsd-2/tree/main/docs) -- [Getting Started](https://github.com/gsd-build/gsd-2/blob/main/docs/getting-started.md) -- [Issue Tracker](https://github.com/gsd-build/gsd-2/issues) +- [GSD Documentation](https://github.com/singularity-forge/sf-run/tree/main/docs) +- [Getting Started](https://github.com/singularity-forge/sf-run/blob/main/docs/getting-started.md) +- [Issue Tracker](https://github.com/singularity-forge/sf-run/issues) diff --git a/vscode-extension/package.json b/vscode-extension/package.json index 2a2088fdf..6e3338974 100644 --- a/vscode-extension/package.json +++ b/vscode-extension/package.json @@ -8,11 +8,11 @@ "license": "MIT", "repository": { "type": "git", - "url": "https://github.com/gsd-build/gsd-2" + "url": "https://github.com/singularity-forge/sf-run" }, - "homepage": "https://github.com/gsd-build/gsd-2/blob/main/vscode-extension/README.md", + "homepage": "https://github.com/singularity-forge/sf-run/blob/main/vscode-extension/README.md", "bugs": { - "url": "https://github.com/gsd-build/gsd-2/issues" + "url": "https://github.com/singularity-forge/sf-run/issues" }, "keywords": [ "ai", diff --git a/web/lib/__tests__/dashboard-metrics-fallback.test.ts b/web/lib/__tests__/dashboard-metrics-fallback.test.ts index 626e68a36..4591ef059 100644 --- a/web/lib/__tests__/dashboard-metrics-fallback.test.ts +++ b/web/lib/__tests__/dashboard-metrics-fallback.test.ts @@ -10,7 +10,7 @@ import assert from "node:assert/strict"; * * Fallback chain: projectTotals?.X ?? auto?.X ?? 0 * - * See: https://github.com/gsd-build/gsd-2/issues/2709 + * See: https://github.com/singularity-forge/sf-run/issues/2709 */ interface ProjectTotals { diff --git a/web/next.config.mjs b/web/next.config.mjs index 248dc1f81..7537e65f9 100644 --- a/web/next.config.mjs +++ b/web/next.config.mjs @@ -14,7 +14,7 @@ const nextConfig = { images: { unoptimized: true, }, - serverExternalPackages: ['@gsd/native', 'node-pty'], + serverExternalPackages: ['@sf-run/native', 'node-pty'], // NodeNext-style .js extension imports in src/ must resolve to .ts source. // Turbopack doesn't support extensionAlias, so builds use --webpack flag. webpack: (config, { isServer }) => { @@ -31,11 +31,11 @@ const nextConfig = { // a simple object entry so `require("node:module")` passes through. config.externals.push({ 'node:module': 'commonjs node:module', - // @gsd/native is a native addon loaded via runtime require(). + // @sf-run/native is a native addon loaded via runtime require(). // serverExternalPackages handles the top-level import, but webpack // still tries to resolve the bare specifier inside files traced from // src/ (outside web/). Explicitly externalize it. - '@gsd/native': 'commonjs @gsd/native', + '@sf-run/native': 'commonjs @sf-run/native', }); } return config;