singularity-forge/src/resources/extensions/sf/bootstrap/register-extension.ts
Mikael Hugo a8cf2cd941 feat(workflow): add product-audit (slim port)
Milestone-end workflow that compares declared product intent (VISION.md,
RUNBOOKS.md, etc.) against actual code/test/deploy/docs evidence and
emits structured gaps with severity. Soft gates — adds follow-up slices
but doesn't hard-block merge.

Slim port (4 new files + 1 registration) — extracts only the audit
feature itself, not bunker's parallel rewrite of dispatch/prompts/
benchmark-selector that came with it in commit 2aa785475.

Created:
- prompts/product-audit.md         — prompt verbatim, gsd_*→sf_* and .gsd→.sf
- tools/product-audit-tool.ts      — slim file-write implementation,
                                     atomicWriteAsync to .sf/active/{mid}/
                                     PRODUCT-AUDIT.{json,md}; no DB deps
- bootstrap/product-audit-tool.ts  — pi-coding-agent tool registration,
                                     TypeBox schema for sf_product_audit
- workflow-templates/product-audit.md — workflow template

Modified:
- bootstrap/register-extension.ts  — 2 lines: import + add to nonCriticalRegistrations
- workflow-templates/registry.json — registry entry
- package.json — version 2.75.0 → 2.75.1

Verdict logic (no-gaps | gaps-found | contract-underspecified) is the
load-bearing innovation: contract-underspecified forces the auditor to
flag unverifiable docs as a real gap rather than rubber-stamping
no-gaps when the product contract is silent.

Out of scope: phase enum changes, dispatch hookup. Wire-up to the phase
machine is a follow-up; the prompt + tool + template stand alone.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 13:55:23 +02:00

132 lines
4.4 KiB
TypeScript

// SF2 — Extension registration: wires all SF tools, commands, and hooks into pi
import type {
ExtensionAPI,
ExtensionCommandContext,
} from "@singularity-forge/pi-coding-agent";
import { loadEcosystemExtensions } from "../ecosystem/loader.js";
import type { SFEcosystemBeforeAgentStartHandler } from "../ecosystem/sf-extension-api.js";
import { registerExitCommand } from "../exit-command.js";
import { logWarning } from "../workflow-logger.js";
import { registerWorktreeCommand } from "../worktree-command.js";
import { writeCrashLog } from "./crash-log.js";
import { registerDbTools } from "./db-tools.js";
import { registerDynamicTools } from "./dynamic-tools.js";
import { registerExecTools } from "./exec-tools.js";
import { registerJournalTools } from "./journal-tools.js";
import { registerMemoryTools } from "./memory-tools.js";
import { registerProductAuditTool } from "./product-audit-tool.js";
import { registerQueryTools } from "./query-tools.js";
import { registerHooks } from "./register-hooks.js";
import { registerShortcuts } from "./register-shortcuts.js";
export { writeCrashLog } from "./crash-log.js";
export function handleRecoverableExtensionProcessError(err: Error): boolean {
if ((err as NodeJS.ErrnoException).code === "EPIPE") {
process.exit(0);
}
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
const syscall = (err as NodeJS.ErrnoException).syscall;
if (syscall?.startsWith("spawn")) {
process.stderr.write(
`[forge] spawn ENOENT: ${(err as any).path ?? "unknown"} — command not found\n`,
);
return true;
}
if (syscall === "uv_cwd") {
process.stderr.write(`[forge] ENOENT (${syscall}): ${err.message}\n`);
return true;
}
}
return false;
}
function installEpipeGuard(): void {
if (
!process
.listeners("uncaughtException")
.some((listener) => listener.name === "_sfEpipeGuard")
) {
const _sfEpipeGuard = (err: Error): void => {
if (handleRecoverableExtensionProcessError(err)) return;
// Write crash log and exit cleanly for unrecoverable errors.
// Logging and continuing was the original double-fault fix (#3163), but
// continuing in an indeterminate state is worse than a clean exit (#3348).
writeCrashLog(err, "uncaughtException");
process.exit(1);
};
process.on("uncaughtException", _sfEpipeGuard);
}
if (
!process
.listeners("unhandledRejection")
.some((listener) => listener.name === "_sfRejectionGuard")
) {
const _sfRejectionGuard = (
reason: unknown,
_promise: Promise<unknown>,
): void => {
const err = reason instanceof Error ? reason : new Error(String(reason));
if (handleRecoverableExtensionProcessError(err)) return;
writeCrashLog(err, "unhandledRejection");
process.exit(1);
};
process.on("unhandledRejection", _sfRejectionGuard);
}
}
export function registerSfExtension(pi: ExtensionAPI): void {
// Note: registerSFCommand is called by index.ts before this function,
// so we intentionally skip it here to avoid double-registration.
registerWorktreeCommand(pi);
registerExitCommand(pi);
installEpipeGuard();
pi.registerCommand("kill", {
description: "Exit SF immediately (no cleanup)",
handler: async (_args: string, _ctx: ExtensionCommandContext) => {
process.exit(0);
},
});
const ecosystemHandlers: SFEcosystemBeforeAgentStartHandler[] = [];
// Wrap non-critical registrations individually so one failure
// doesn't prevent the others from loading.
const nonCriticalRegistrations: Array<[string, () => void]> = [
["dynamic-tools", () => registerDynamicTools(pi)],
["db-tools", () => registerDbTools(pi)],
["exec-tools", () => registerExecTools(pi)],
["memory-tools", () => registerMemoryTools(pi)],
["product-audit-tool", () => registerProductAuditTool(pi)],
["journal-tools", () => registerJournalTools(pi)],
["query-tools", () => registerQueryTools(pi)],
["shortcuts", () => registerShortcuts(pi)],
["hooks", () => registerHooks(pi, ecosystemHandlers)],
[
"ecosystem",
() => {
void loadEcosystemExtensions(pi, ecosystemHandlers).catch((err) => {
logWarning(
"bootstrap",
`Failed to load ecosystem extensions: ${err instanceof Error ? err.message : String(err)}`,
);
});
},
],
];
for (const [name, register] of nonCriticalRegistrations) {
try {
register();
} catch (err) {
logWarning(
"bootstrap",
`Failed to register ${name}: ${err instanceof Error ? err.message : String(err)}`,
);
}
}
}