refactor: strip internal pi branding (Phase 2A)

- 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>
This commit is contained in:
Mikael Hugo 2026-05-10 11:50:55 +02:00
parent 02a4339a51
commit cab8b5decc
9 changed files with 54 additions and 53 deletions

View file

@ -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/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.ts` | Non-interactive (headless) mode driver — exit codes 0/1/10/11/12 |
| `src/headless-events.ts` | Transcript event parsing and notification routing | | `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/` | All SF extension source (TypeScript) |
| `src/resources/extensions/sf/auto/` | Autonomous workflow orchestrator (UOK lifecycle, dispatch, planning) | | `src/resources/extensions/sf/auto/` | Autonomous workflow orchestrator (UOK lifecycle, dispatch, planning) |
| `src/resources/extensions/sf/bootstrap/` | Context injection, system prompt assembly | | `src/resources/extensions/sf/bootstrap/` | Context injection, system prompt assembly |

View file

@ -5,7 +5,7 @@
* *
* Test with: npx tsx src/cli-new.ts [args...] * Test with: npx tsx src/cli-new.ts [args...]
*/ */
process.title = "pi"; process.title = "sf";
import { setBedrockProviderModule } from "@singularity-forge/ai"; import { setBedrockProviderModule } from "@singularity-forge/ai";
import { bedrockProviderModule } from "@singularity-forge/ai/bedrock-provider"; import { bedrockProviderModule } from "@singularity-forge/ai/bedrock-provider";

View file

@ -806,10 +806,11 @@ async function loadExtensionModule(extensionPath: string) {
* Check whether a module path belongs to a non-extension library that should * Check whether a module path belongs to a non-extension library that should
* be silently skipped rather than reported as an error. * be silently skipped rather than reported as an error.
* *
* A directory is a non-extension library when its package.json has a "pi" * A directory is a non-extension library when its package.json has an "sf"
* manifest that declares no extensions (e.g. `"pi": {}`). This is the * (or "pi" for backwards compat) manifest that declares no extensions
* opt-out convention used by shared libraries like cmux that live inside * (e.g. `"sf": {}`). This is the opt-out convention used by shared libraries
* the extensions/ directory but are not extensions themselves. * 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 * 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 * 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)) { if (fs.existsSync(packageJsonPath)) {
try { try {
const content = fs.readFileSync(packageJsonPath, "utf-8"); const content = fs.readFileSync(packageJsonPath, "utf-8");
const pkg = JSON.parse(content); const pkg = JSON.parse(content) as { sf?: SFManifest; pi?: SFManifest };
if (pkg.pi && typeof pkg.pi === "object") { // Check sf field first, fall back to pi for backwards compat
// Has a pi manifest — check if it declares any extensions const manifest = pkg.sf ?? pkg.pi;
const extensions = pkg.pi.extensions; 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) { if (!Array.isArray(extensions) || extensions.length === 0) {
return true; return true;
} }
@ -996,21 +999,19 @@ export async function loadExtensions(
}; };
} }
interface PiManifest { interface SFManifest {
extensions?: string[]; extensions?: string[];
themes?: string[]; themes?: string[];
skills?: string[]; skills?: string[];
prompts?: string[]; prompts?: string[];
} }
function readPiManifest(packageJsonPath: string): PiManifest | null { function readSFManifest(packageJsonPath: string): SFManifest | null {
try { try {
const content = fs.readFileSync(packageJsonPath, "utf-8"); const content = fs.readFileSync(packageJsonPath, "utf-8");
const pkg = JSON.parse(content); const pkg = JSON.parse(content) as { sf?: SFManifest; pi?: SFManifest };
if (pkg.pi && typeof pkg.pi === "object") { // Read sf field first, fall back to pi for backwards compat
return pkg.pi as PiManifest; return pkg.sf ?? pkg.pi ?? null;
}
return null;
} catch { } catch {
return null; return null;
} }
@ -1027,20 +1028,20 @@ function isExtensionFile(name: string): boolean {
* Resolve extension entry points from a directory. * Resolve extension entry points from a directory.
* *
* Checks for: * 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 * 2. index.ts or index.js -> returns the index file
* *
* Returns resolved paths or null if no entry points found. * Returns resolved paths or null if no entry points found.
*/ */
function resolveExtensionEntries(dir: string): string[] | null { 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"); const packageJsonPath = path.join(dir, "package.json");
if (fs.existsSync(packageJsonPath)) { if (fs.existsSync(packageJsonPath)) {
const manifest = readPiManifest(packageJsonPath); const manifest = readSFManifest(packageJsonPath);
if (manifest) { 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 // 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) { if (!manifest.extensions?.length) {
return null; return null;
} }
@ -1074,7 +1075,7 @@ function resolveExtensionEntries(dir: string): string[] | null {
* Discovery rules: * Discovery rules:
* 1. Direct files: `extensions/*.ts` or `*.js` load * 1. Direct files: `extensions/*.ts` or `*.js` load
* 2. Subdirectory with index: `extensions/* /index.ts` or `index.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. * 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). // Only loaded when the project path has been explicitly trusted (TOFU model).
const localExtDir = path.join(cwd, ".pi", "extensions"); const localExtDir = path.join(cwd, ".pi", "extensions");
const localDiscovered = discoverExtensionsInDir(localExtDir); const localDiscovered = discoverExtensionsInDir(localExtDir);
@ -1154,7 +1155,7 @@ export async function discoverAndLoadExtensions(
); );
if (untrusted.length > 0) { if (untrusted.length > 0) {
process.stderr.write( 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)); const trusted = localDiscovered.filter((p) => !untrusted.includes(p));

View file

@ -179,13 +179,13 @@ describe("collectRuntimeDependencies", () => {
describe("verifyRuntimeDependencies", () => { describe("verifyRuntimeDependencies", () => {
it("does not throw for empty deps array", () => { it("does not throw for empty deps array", () => {
assert.doesNotThrow(() => assert.doesNotThrow(() =>
verifyRuntimeDependencies([], "test-source", "pi"), verifyRuntimeDependencies([], "test-source", "sf"),
); );
}); });
it("does not throw when all deps are present", () => { it("does not throw when all deps are present", () => {
assert.doesNotThrow(() => assert.doesNotThrow(() =>
verifyRuntimeDependencies(["node"], "test-source", "pi"), verifyRuntimeDependencies(["node"], "test-source", "sf"),
); );
}); });
@ -195,7 +195,7 @@ describe("verifyRuntimeDependencies", () => {
verifyRuntimeDependencies( verifyRuntimeDependencies(
["__nonexistent_dep_for_test__"], ["__nonexistent_dep_for_test__"],
"test-source", "test-source",
"pi", "sf",
), ),
(err: Error) => { (err: Error) => {
assert.ok(err.message.includes("Missing runtime dependencies")); assert.ok(err.message.includes("Missing runtime dependencies"));
@ -211,7 +211,7 @@ describe("verifyRuntimeDependencies", () => {
verifyRuntimeDependencies( verifyRuntimeDependencies(
["__missing_1__", "__missing_2__"], ["__missing_1__", "__missing_2__"],
"test-source", "test-source",
"pi", "sf",
), ),
(err: Error) => { (err: Error) => {
assert.ok(err.message.includes("__missing_1__")); assert.ok(err.message.includes("__missing_1__"));

View file

@ -51,7 +51,7 @@ describe("runPackageCommand lifecycle hooks", () => {
"package.json": JSON.stringify({ "package.json": JSON.stringify({
name: "ext-registered", name: "ext-registered",
type: "module", type: "module",
pi: { extensions: ["./index.js"] }, sf: { extensions: ["./index.js"] },
}), }),
"index.js": [ "index.js": [
'import { writeFileSync } from "node:fs";', 'import { writeFileSync } from "node:fs";',
@ -101,7 +101,7 @@ describe("runPackageCommand lifecycle hooks", () => {
"package.json": JSON.stringify({ "package.json": JSON.stringify({
name: "ext-legacy", name: "ext-legacy",
type: "module", type: "module",
pi: { extensions: ["./index.js"] }, sf: { extensions: ["./index.js"] },
}), }),
"index.js": [ "index.js": [
'import { writeFileSync } from "node:fs";', 'import { writeFileSync } from "node:fs";',
@ -175,7 +175,7 @@ describe("runPackageCommand lifecycle hooks", () => {
"package.json": JSON.stringify({ "package.json": JSON.stringify({
name: "ext-empty", name: "ext-empty",
type: "module", type: "module",
pi: { extensions: ["./index.js"] }, sf: { extensions: ["./index.js"] },
}), }),
"index.js": "export default function () {}", "index.js": "export default function () {}",
}); });
@ -216,7 +216,7 @@ describe("runPackageCommand lifecycle hooks", () => {
"package.json": JSON.stringify({ "package.json": JSON.stringify({
name: "ext-runtime-deps", name: "ext-runtime-deps",
type: "module", type: "module",
pi: { extensions: ["./index.js"] }, sf: { extensions: ["./index.js"] },
}), }),
"index.js": "export default function () {}", "index.js": "export default function () {}",
"extension-manifest.json": JSON.stringify({ "extension-manifest.json": JSON.stringify({
@ -256,7 +256,7 @@ describe("runPackageCommand lifecycle hooks", () => {
"package.json": JSON.stringify({ "package.json": JSON.stringify({
name: "ext-after-remove", name: "ext-after-remove",
type: "module", type: "module",
pi: { extensions: ["./index.js"] }, sf: { extensions: ["./index.js"] },
}), }),
"index.js": [ "index.js": [
'import { writeFileSync, existsSync } from "node:fs";', 'import { writeFileSync, existsSync } from "node:fs";',

View file

@ -106,7 +106,7 @@ type LocalSource = {
type ParsedSource = NpmSource | GitSource | LocalSource; type ParsedSource = NpmSource | GitSource | LocalSource;
interface PiManifest { interface SFManifest {
extensions?: string[]; extensions?: string[];
skills?: string[]; skills?: string[];
prompts?: string[]; prompts?: string[];
@ -443,11 +443,11 @@ function collectAutoThemeEntries(dir: string): string[] {
return entries; return entries;
} }
function readPiManifestFile(packageJsonPath: string): PiManifest | null { function readSFManifestFile(packageJsonPath: string): SFManifest | null {
try { try {
const content = readFileSync(packageJsonPath, "utf-8"); const content = readFileSync(packageJsonPath, "utf-8");
const pkg = JSON.parse(content) as { pi?: PiManifest }; const pkg = JSON.parse(content) as { sf?: SFManifest; pi?: SFManifest };
return pkg.pi ?? null; return pkg.sf ?? pkg.pi ?? null;
} catch { } catch {
return null; return null;
} }
@ -456,11 +456,11 @@ function readPiManifestFile(packageJsonPath: string): PiManifest | null {
function resolveExtensionEntries(dir: string): string[] | null { function resolveExtensionEntries(dir: string): string[] | null {
const packageJsonPath = join(dir, "package.json"); const packageJsonPath = join(dir, "package.json");
if (existsSync(packageJsonPath)) { if (existsSync(packageJsonPath)) {
const manifest = readPiManifestFile(packageJsonPath); const manifest = readSFManifestFile(packageJsonPath);
if (manifest) { 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 // 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) { if (!manifest.extensions?.length) {
return null; return null;
} }
@ -1728,10 +1728,10 @@ export class DefaultPackageManager implements PackageManager {
return true; return true;
} }
const manifest = this.readPiManifest(packageRoot); const manifest = this.readSFManifest(packageRoot);
if (manifest) { if (manifest) {
for (const resourceType of RESOURCE_TYPES) { for (const resourceType of RESOURCE_TYPES) {
const entries = manifest[resourceType as keyof PiManifest]; const entries = manifest[resourceType as keyof SFManifest];
this.addManifestEntries( this.addManifestEntries(
entries, entries,
packageRoot, packageRoot,
@ -1769,8 +1769,8 @@ export class DefaultPackageManager implements PackageManager {
target: Map<string, { metadata: PathMetadata; enabled: boolean }>, target: Map<string, { metadata: PathMetadata; enabled: boolean }>,
metadata: PathMetadata, metadata: PathMetadata,
): void { ): void {
const manifest = this.readPiManifest(packageRoot); const manifest = this.readSFManifest(packageRoot);
const entries = manifest?.[resourceType as keyof PiManifest]; const entries = manifest?.[resourceType as keyof SFManifest];
if (entries) { if (entries) {
this.addManifestEntries( this.addManifestEntries(
entries, entries,
@ -1826,8 +1826,8 @@ export class DefaultPackageManager implements PackageManager {
packageRoot: string, packageRoot: string,
resourceType: ResourceType, resourceType: ResourceType,
): { allFiles: string[]; enabledByManifest: Set<string> } { ): { allFiles: string[]; enabledByManifest: Set<string> } {
const manifest = this.readPiManifest(packageRoot); const manifest = this.readSFManifest(packageRoot);
const entries = manifest?.[resourceType as keyof PiManifest]; const entries = manifest?.[resourceType as keyof SFManifest];
if (entries && entries.length > 0) { if (entries && entries.length > 0) {
const allFiles = this.collectFilesFromManifestEntries( const allFiles = this.collectFilesFromManifestEntries(
entries, entries,
@ -1850,7 +1850,7 @@ export class DefaultPackageManager implements PackageManager {
return { allFiles, enabledByManifest: new Set(allFiles) }; return { allFiles, enabledByManifest: new Set(allFiles) };
} }
private readPiManifest(packageRoot: string): PiManifest | null { private readSFManifest(packageRoot: string): SFManifest | null {
const packageJsonPath = join(packageRoot, "package.json"); const packageJsonPath = join(packageRoot, "package.json");
if (!existsSync(packageJsonPath)) { if (!existsSync(packageJsonPath)) {
return null; return null;
@ -1858,8 +1858,8 @@ export class DefaultPackageManager implements PackageManager {
try { try {
const content = readFileSync(packageJsonPath, "utf-8"); const content = readFileSync(packageJsonPath, "utf-8");
const pkg = JSON.parse(content) as { pi?: PiManifest }; const pkg = JSON.parse(content) as { sf?: SFManifest; pi?: SFManifest };
return pkg.pi ?? null; return pkg.sf ?? pkg.pi ?? null;
} catch { } catch {
return null; return null;
} }

View file

@ -68,7 +68,7 @@ function getGroupLabel(metadata: PathMetadata): string {
} }
// Top-level resources // Top-level resources
if (metadata.source === "auto") { 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"; return metadata.scope === "user" ? "User settings" : "Project settings";
} }

View file

@ -3206,7 +3206,7 @@ export class InteractiveMode {
const currentText = const currentText =
this.editor.getExpandedText?.() ?? this.editor.getText(); 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 { try {
// Write current content to temp file // Write current content to temp file

View file

@ -76,7 +76,7 @@ export function isFocusable(
* Components emit this at the cursor position when focused. * Components emit this at the cursor position when focused.
* TUI finds and strips this marker, then positions the hardware cursor there. * 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 }; export { visibleWidth };