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
-
-
+
+
---
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