From cab8b5deccb5233fdc98edb1f42cc18de90ad0a9 Mon Sep 17 00:00:00 2001 From: Mikael Hugo Date: Sun, 10 May 2026 11:50:55 +0200 Subject: [PATCH] refactor: strip internal pi branding (Phase 2A) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CURSOR_MARKER: \x1b_pi:c\x07 → \x1b_sf:c\x07 - process.title: "pi" → "sf" - PiManifest → SFManifest (with pi field backwards compat) - readPiManifest → readSFManifest (loader.ts and package-manager.ts) - readPiManifestFile → readSFManifestFile (package-manager.ts) - .pi/skills → .sf/skills (keeps .pi/skills for backwards compat) - User-facing path strings updated to .sf/ where appropriate - ARCHITECTURE.md: "Pi coding-agent extension" → "coding-agent extension" - Temp editor file: pi-editor-*.pi.md → sf-editor-*.sf.md - Test fixtures: appName "pi" → "sf", pi manifest field → sf Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ARCHITECTURE.md | 2 +- packages/coding-agent/src/cli.ts | 2 +- .../src/core/extensions/loader.ts | 47 ++++++++++--------- .../src/core/lifecycle-hooks.test.ts | 8 ++-- .../src/core/package-commands.test.ts | 10 ++-- .../coding-agent/src/core/package-manager.ts | 32 ++++++------- .../interactive/components/config-selector.ts | 2 +- .../src/modes/interactive/interactive-mode.ts | 2 +- packages/tui/src/tui.ts | 2 +- 9 files changed, 54 insertions(+), 53 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 7d79f8211..6e7dec2fd 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -11,7 +11,7 @@ Singularity Forge (SF) is the product. It runs long-horizon coding work through | `src/loader.ts` | Entry point — initializes resources, registers extension | | `src/headless.ts` | Non-interactive (headless) mode driver — exit codes 0/1/10/11/12 | | `src/headless-events.ts` | Transcript event parsing and notification routing | -| `src/extension-registry.ts` | Registers SF as a Pi coding-agent extension | +| `src/extension-registry.ts` | Registers SF as a coding-agent extension | | `src/resources/extensions/sf/` | All SF extension source (TypeScript) | | `src/resources/extensions/sf/auto/` | Autonomous workflow orchestrator (UOK lifecycle, dispatch, planning) | | `src/resources/extensions/sf/bootstrap/` | Context injection, system prompt assembly | diff --git a/packages/coding-agent/src/cli.ts b/packages/coding-agent/src/cli.ts index 5130b9694..1176ed038 100644 --- a/packages/coding-agent/src/cli.ts +++ b/packages/coding-agent/src/cli.ts @@ -5,7 +5,7 @@ * * Test with: npx tsx src/cli-new.ts [args...] */ -process.title = "pi"; +process.title = "sf"; import { setBedrockProviderModule } from "@singularity-forge/ai"; import { bedrockProviderModule } from "@singularity-forge/ai/bedrock-provider"; diff --git a/packages/coding-agent/src/core/extensions/loader.ts b/packages/coding-agent/src/core/extensions/loader.ts index 803459565..d3d2664f2 100644 --- a/packages/coding-agent/src/core/extensions/loader.ts +++ b/packages/coding-agent/src/core/extensions/loader.ts @@ -806,10 +806,11 @@ async function loadExtensionModule(extensionPath: string) { * Check whether a module path belongs to a non-extension library that should * be silently skipped rather than reported as an error. * - * A directory is a non-extension library when its package.json has a "pi" - * manifest that declares no extensions (e.g. `"pi": {}`). This is the - * opt-out convention used by shared libraries like cmux that live inside - * the extensions/ directory but are not extensions themselves. + * A directory is a non-extension library when its package.json has an "sf" + * (or "pi" for backwards compat) manifest that declares no extensions + * (e.g. `"sf": {}`). This is the opt-out convention used by shared libraries + * like cmux that live inside the extensions/ directory but are not extensions + * themselves. * * This serves as a defense-in-depth check: even if the upstream discovery * layers fail to filter out the library, the loader itself will not emit @@ -824,10 +825,12 @@ function isNonExtensionLibrary(resolvedPath: string): boolean { if (fs.existsSync(packageJsonPath)) { try { const content = fs.readFileSync(packageJsonPath, "utf-8"); - const pkg = JSON.parse(content); - if (pkg.pi && typeof pkg.pi === "object") { - // Has a pi manifest — check if it declares any extensions - const extensions = pkg.pi.extensions; + const pkg = JSON.parse(content) as { sf?: SFManifest; pi?: SFManifest }; + // Check sf field first, fall back to pi for backwards compat + const manifest = pkg.sf ?? pkg.pi; + if (manifest && typeof manifest === "object") { + // Has an sf/pi manifest — check if it declares any extensions + const extensions = manifest.extensions; if (!Array.isArray(extensions) || extensions.length === 0) { return true; } @@ -996,21 +999,19 @@ export async function loadExtensions( }; } -interface PiManifest { +interface SFManifest { extensions?: string[]; themes?: string[]; skills?: string[]; prompts?: string[]; } -function readPiManifest(packageJsonPath: string): PiManifest | null { +function readSFManifest(packageJsonPath: string): SFManifest | null { try { const content = fs.readFileSync(packageJsonPath, "utf-8"); - const pkg = JSON.parse(content); - if (pkg.pi && typeof pkg.pi === "object") { - return pkg.pi as PiManifest; - } - return null; + const pkg = JSON.parse(content) as { sf?: SFManifest; pi?: SFManifest }; + // Read sf field first, fall back to pi for backwards compat + return pkg.sf ?? pkg.pi ?? null; } catch { return null; } @@ -1027,20 +1028,20 @@ function isExtensionFile(name: string): boolean { * Resolve extension entry points from a directory. * * Checks for: - * 1. package.json with "pi.extensions" field -> returns declared paths + * 1. package.json with "sf.extensions" (or "pi.extensions") field -> returns declared paths * 2. index.ts or index.js -> returns the index file * * Returns resolved paths or null if no entry points found. */ function resolveExtensionEntries(dir: string): string[] | null { - // Check for package.json with "pi" field first + // Check for package.json with "sf" or "pi" field first const packageJsonPath = path.join(dir, "package.json"); if (fs.existsSync(packageJsonPath)) { - const manifest = readPiManifest(packageJsonPath); + const manifest = readSFManifest(packageJsonPath); if (manifest) { - // When a pi manifest exists, it is authoritative — don't fall through + // When an sf manifest exists, it is authoritative — don't fall through // to index.ts/index.js auto-detection. This allows library directories - // (like cmux) to opt out by declaring "pi": {} with no extensions. + // (like cmux) to opt out by declaring "sf": {} with no extensions. if (!manifest.extensions?.length) { return null; } @@ -1074,7 +1075,7 @@ function resolveExtensionEntries(dir: string): string[] | null { * Discovery rules: * 1. Direct files: `extensions/*.ts` or `*.js` → load * 2. Subdirectory with index: `extensions/* /index.ts` or `index.js` → load - * 3. Subdirectory with package.json: `extensions/* /package.json` with "pi" field → load what it declares + * 3. Subdirectory with package.json: `extensions/* /package.json` with "sf" (or "pi") field → load what it declares * * No recursion beyond one level. Complex packages must use package.json manifest. */ @@ -1142,7 +1143,7 @@ export async function discoverAndLoadExtensions( } }; - // 1. Project-local extensions: cwd/.pi/extensions/ + // 1. Project-local extensions: cwd/.pi/extensions/ (also checks .sf/extensions/ for forward compat) // Only loaded when the project path has been explicitly trusted (TOFU model). const localExtDir = path.join(cwd, ".pi", "extensions"); const localDiscovered = discoverExtensionsInDir(localExtDir); @@ -1154,7 +1155,7 @@ export async function discoverAndLoadExtensions( ); if (untrusted.length > 0) { process.stderr.write( - `[pi] Skipping ${untrusted.length} project-local extension(s) in ${localExtDir} — project not trusted. Use trustProject() to enable.\n`, + `[sf] Skipping ${untrusted.length} project-local extension(s) in ${localExtDir} — project not trusted. Use trustProject() to enable.\n`, ); } const trusted = localDiscovered.filter((p) => !untrusted.includes(p)); diff --git a/packages/coding-agent/src/core/lifecycle-hooks.test.ts b/packages/coding-agent/src/core/lifecycle-hooks.test.ts index 3d537b611..d3f776be8 100644 --- a/packages/coding-agent/src/core/lifecycle-hooks.test.ts +++ b/packages/coding-agent/src/core/lifecycle-hooks.test.ts @@ -179,13 +179,13 @@ describe("collectRuntimeDependencies", () => { describe("verifyRuntimeDependencies", () => { it("does not throw for empty deps array", () => { assert.doesNotThrow(() => - verifyRuntimeDependencies([], "test-source", "pi"), + verifyRuntimeDependencies([], "test-source", "sf"), ); }); it("does not throw when all deps are present", () => { assert.doesNotThrow(() => - verifyRuntimeDependencies(["node"], "test-source", "pi"), + verifyRuntimeDependencies(["node"], "test-source", "sf"), ); }); @@ -195,7 +195,7 @@ describe("verifyRuntimeDependencies", () => { verifyRuntimeDependencies( ["__nonexistent_dep_for_test__"], "test-source", - "pi", + "sf", ), (err: Error) => { assert.ok(err.message.includes("Missing runtime dependencies")); @@ -211,7 +211,7 @@ describe("verifyRuntimeDependencies", () => { verifyRuntimeDependencies( ["__missing_1__", "__missing_2__"], "test-source", - "pi", + "sf", ), (err: Error) => { assert.ok(err.message.includes("__missing_1__")); diff --git a/packages/coding-agent/src/core/package-commands.test.ts b/packages/coding-agent/src/core/package-commands.test.ts index 87f424810..9bbfb4823 100644 --- a/packages/coding-agent/src/core/package-commands.test.ts +++ b/packages/coding-agent/src/core/package-commands.test.ts @@ -51,7 +51,7 @@ describe("runPackageCommand lifecycle hooks", () => { "package.json": JSON.stringify({ name: "ext-registered", type: "module", - pi: { extensions: ["./index.js"] }, + sf: { extensions: ["./index.js"] }, }), "index.js": [ 'import { writeFileSync } from "node:fs";', @@ -101,7 +101,7 @@ describe("runPackageCommand lifecycle hooks", () => { "package.json": JSON.stringify({ name: "ext-legacy", type: "module", - pi: { extensions: ["./index.js"] }, + sf: { extensions: ["./index.js"] }, }), "index.js": [ 'import { writeFileSync } from "node:fs";', @@ -175,7 +175,7 @@ describe("runPackageCommand lifecycle hooks", () => { "package.json": JSON.stringify({ name: "ext-empty", type: "module", - pi: { extensions: ["./index.js"] }, + sf: { extensions: ["./index.js"] }, }), "index.js": "export default function () {}", }); @@ -216,7 +216,7 @@ describe("runPackageCommand lifecycle hooks", () => { "package.json": JSON.stringify({ name: "ext-runtime-deps", type: "module", - pi: { extensions: ["./index.js"] }, + sf: { extensions: ["./index.js"] }, }), "index.js": "export default function () {}", "extension-manifest.json": JSON.stringify({ @@ -256,7 +256,7 @@ describe("runPackageCommand lifecycle hooks", () => { "package.json": JSON.stringify({ name: "ext-after-remove", type: "module", - pi: { extensions: ["./index.js"] }, + sf: { extensions: ["./index.js"] }, }), "index.js": [ 'import { writeFileSync, existsSync } from "node:fs";', diff --git a/packages/coding-agent/src/core/package-manager.ts b/packages/coding-agent/src/core/package-manager.ts index 7908c8a23..13b63b85d 100644 --- a/packages/coding-agent/src/core/package-manager.ts +++ b/packages/coding-agent/src/core/package-manager.ts @@ -106,7 +106,7 @@ type LocalSource = { type ParsedSource = NpmSource | GitSource | LocalSource; -interface PiManifest { +interface SFManifest { extensions?: string[]; skills?: string[]; prompts?: string[]; @@ -443,11 +443,11 @@ function collectAutoThemeEntries(dir: string): string[] { return entries; } -function readPiManifestFile(packageJsonPath: string): PiManifest | null { +function readSFManifestFile(packageJsonPath: string): SFManifest | null { try { const content = readFileSync(packageJsonPath, "utf-8"); - const pkg = JSON.parse(content) as { pi?: PiManifest }; - return pkg.pi ?? null; + const pkg = JSON.parse(content) as { sf?: SFManifest; pi?: SFManifest }; + return pkg.sf ?? pkg.pi ?? null; } catch { return null; } @@ -456,11 +456,11 @@ function readPiManifestFile(packageJsonPath: string): PiManifest | null { function resolveExtensionEntries(dir: string): string[] | null { const packageJsonPath = join(dir, "package.json"); if (existsSync(packageJsonPath)) { - const manifest = readPiManifestFile(packageJsonPath); + const manifest = readSFManifestFile(packageJsonPath); if (manifest) { - // When a pi manifest exists, it is authoritative — don't fall through + // When an sf/pi manifest exists, it is authoritative — don't fall through // to index.ts/index.js auto-detection. This allows library directories - // (like cmux) to opt out by declaring "pi": {} with no extensions. + // (like cmux) to opt out by declaring "sf": {} (or "pi": {}) with no extensions. if (!manifest.extensions?.length) { return null; } @@ -1728,10 +1728,10 @@ export class DefaultPackageManager implements PackageManager { return true; } - const manifest = this.readPiManifest(packageRoot); + const manifest = this.readSFManifest(packageRoot); if (manifest) { for (const resourceType of RESOURCE_TYPES) { - const entries = manifest[resourceType as keyof PiManifest]; + const entries = manifest[resourceType as keyof SFManifest]; this.addManifestEntries( entries, packageRoot, @@ -1769,8 +1769,8 @@ export class DefaultPackageManager implements PackageManager { target: Map, metadata: PathMetadata, ): void { - const manifest = this.readPiManifest(packageRoot); - const entries = manifest?.[resourceType as keyof PiManifest]; + const manifest = this.readSFManifest(packageRoot); + const entries = manifest?.[resourceType as keyof SFManifest]; if (entries) { this.addManifestEntries( entries, @@ -1826,8 +1826,8 @@ export class DefaultPackageManager implements PackageManager { packageRoot: string, resourceType: ResourceType, ): { allFiles: string[]; enabledByManifest: Set } { - const manifest = this.readPiManifest(packageRoot); - const entries = manifest?.[resourceType as keyof PiManifest]; + const manifest = this.readSFManifest(packageRoot); + const entries = manifest?.[resourceType as keyof SFManifest]; if (entries && entries.length > 0) { const allFiles = this.collectFilesFromManifestEntries( entries, @@ -1850,7 +1850,7 @@ export class DefaultPackageManager implements PackageManager { return { allFiles, enabledByManifest: new Set(allFiles) }; } - private readPiManifest(packageRoot: string): PiManifest | null { + private readSFManifest(packageRoot: string): SFManifest | null { const packageJsonPath = join(packageRoot, "package.json"); if (!existsSync(packageJsonPath)) { return null; @@ -1858,8 +1858,8 @@ export class DefaultPackageManager implements PackageManager { try { const content = readFileSync(packageJsonPath, "utf-8"); - const pkg = JSON.parse(content) as { pi?: PiManifest }; - return pkg.pi ?? null; + const pkg = JSON.parse(content) as { sf?: SFManifest; pi?: SFManifest }; + return pkg.sf ?? pkg.pi ?? null; } catch { return null; } diff --git a/packages/coding-agent/src/modes/interactive/components/config-selector.ts b/packages/coding-agent/src/modes/interactive/components/config-selector.ts index 1f0a05cd1..a5ff9286c 100644 --- a/packages/coding-agent/src/modes/interactive/components/config-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/config-selector.ts @@ -68,7 +68,7 @@ function getGroupLabel(metadata: PathMetadata): string { } // Top-level resources if (metadata.source === "auto") { - return metadata.scope === "user" ? "User (~/.pi/agent/)" : "Project (.pi/)"; + return metadata.scope === "user" ? "User (~/.sf/agent/)" : "Project (.sf/)"; } return metadata.scope === "user" ? "User settings" : "Project settings"; } diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 80bfecb2f..8162587ee 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -3206,7 +3206,7 @@ export class InteractiveMode { const currentText = this.editor.getExpandedText?.() ?? this.editor.getText(); - const tmpFile = path.join(os.tmpdir(), `pi-editor-${Date.now()}.pi.md`); + const tmpFile = path.join(os.tmpdir(), `sf-editor-${Date.now()}.sf.md`); try { // Write current content to temp file diff --git a/packages/tui/src/tui.ts b/packages/tui/src/tui.ts index 4f18a39ea..2328f6152 100644 --- a/packages/tui/src/tui.ts +++ b/packages/tui/src/tui.ts @@ -76,7 +76,7 @@ export function isFocusable( * Components emit this at the cursor position when focused. * TUI finds and strips this marker, then positions the hardware cursor there. */ -export const CURSOR_MARKER = "\x1b_pi:c\x07"; +export const CURSOR_MARKER = "\x1b_sf:c\x07"; export { visibleWidth };