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>
132 lines
4.4 KiB
TypeScript
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)}`,
|
|
);
|
|
}
|
|
}
|
|
}
|