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

View file

@ -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";

View file

@ -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));

View file

@ -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__"));

View file

@ -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";',

View file

@ -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<string, { metadata: PathMetadata; enabled: boolean }>,
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<string> } {
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;
}

View file

@ -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";
}

View file

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

View file

@ -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 };