singularity-forge/src/errors.ts
Mikael Hugo a611cd5792 feat: introduce repo-vcs skill and add JSDoc annotations across core modules
- Add repository-vcs-context.ts to detect and inject VCS context (Git/Jujutsu)
  into the agent system prompt; wire in repo-vcs bundled skill trigger
- Add src/resources/skills/repo-vcs/ skill for commit, push, and safe-push workflows
- Add JSDoc Purpose/Consumer annotations to app-paths, bundled-extension-paths,
  errors, extension-discovery, extension-registry, headless-types, headless, and traces
- Add justfile and just to flake.nix devShell
- Fill out new-user-onboarding.md spec (Draft) and core-beliefs.md (Status: Accepted)
- Add notification-event-model.md design doc and notification-source-hygiene.md spec

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 21:36:32 +02:00

161 lines
5.5 KiB
TypeScript

/**
* errors.ts — Structured error types for consistent, actionable CLI diagnostics.
*
* Purpose: every error path in the CLI and headless orchestrator should be
* able to emit context that helps users (and future debuggers) understand
* *what* failed, *where*, and *what to try next* — without depending on
* heavy error-handling libraries.
*
* Consumer: cli.ts, headless.ts, and any extension that surfaces user-facing
* failures. The types are plain data so they serialize cleanly to stderr,
* JSON batch output, and trace spans.
*/
// ---------------------------------------------------------------------------
// Core structured error type
// ---------------------------------------------------------------------------
/**
* A user-facing or machine-readable error record with rich context.
*
* Purpose: provide a single, serializable shape that every error path can
* enrich incrementally (operation, file, guidance, retry hint) so that
* consumers — stderr printers, JSON exporters, and trace emitters — receive
* enough context to produce actionable output without guessing.
*
* Consumer: cli.ts catch blocks, headless.ts event handlers, trace span
* error events, and any extension that surfaces user-facing failures.
*/
export interface StructuredError {
/** Human-readable description of what went wrong. */
message: string;
/** The high-level operation that was in progress when the error occurred,
* e.g. "graph build", "session resume", "model validation". */
operation?: string;
/** The file path most relevant to the failure (the file being read,
* written, or expected). */
file?: string;
/** The line number inside `file` if known (e.g. from a parser error). */
line?: number;
/** Actionable guidance for the user — what to check or try next. */
guidance?: string;
/** Whether retrying the same operation (with the same inputs) might
* succeed, e.g. transient network failures. */
retry?: boolean;
/** The underlying cause, if this error wraps another. Kept as `unknown`
* so callers aren't forced to coerce to Error. */
cause?: unknown;
}
// ---------------------------------------------------------------------------
// Convenience constructors
// ---------------------------------------------------------------------------
/**
* Create a {@link StructuredError} from a message and optional context.
*
* Purpose: reduce boilerplate at catch sites where we want to enrich a raw
* exception with operation/file context before logging or returning it.
*
* Consumer: cli.ts catch blocks, headless.ts event handlers.
*/
export function error(
message: string,
ctx?: Omit<StructuredError, "message">,
): StructuredError {
return { message, ...ctx };
}
// ---------------------------------------------------------------------------
// Formatters
// ---------------------------------------------------------------------------
/**
* Format a {@link StructuredError} as plain text suitable for stderr.
*
* Output shape (fields omitted when undefined):
* [sf] Error: <message>
* Operation: <operation>
* File: <file>:<line>
* Guidance: <guidance>
* Retryable: yes|no
*
* Purpose: give users a consistent, scannable error layout in the terminal
* so they can spot the file, guidance, and retry hint without parsing JSON.
*
* Consumer: cli.ts before writing to process.stderr.
*/
export function formatStructuredError(
err: StructuredError,
prefix = "[sf]",
): string {
const parts: string[] = [`${prefix} Error: ${err.message}`];
if (err.operation) {
parts.push(` Operation: ${err.operation}`);
}
if (err.file) {
const line = err.line !== undefined ? `:${err.line}` : "";
parts.push(` File: ${err.file}${line}`);
}
if (err.guidance) {
parts.push(` Guidance: ${err.guidance}`);
}
if (err.retry !== undefined) {
parts.push(` Retryable: ${err.retry ? "yes" : "no"}`);
}
return parts.join("\n") + "\n";
}
/**
* Format a {@link StructuredError} as a JSON object.
*
* Purpose: headless --output-format json mode can embed structured errors
* in the result payload instead of interleaving free-form text on stderr.
*
* Consumer: headless.ts when emitting batch JSON results.
*/
export function errorToJson(err: StructuredError): Record<string, unknown> {
const out: Record<string, unknown> = { message: err.message };
if (err.operation !== undefined) out.operation = err.operation;
if (err.file !== undefined) out.file = err.file;
if (err.line !== undefined) out.line = err.line;
if (err.guidance !== undefined) out.guidance = err.guidance;
if (err.retry !== undefined) out.retry = err.retry;
if (err.cause !== undefined) {
out.cause =
err.cause instanceof Error
? { message: err.cause.message, name: err.cause.name }
: String(err.cause);
}
return out;
}
// ---------------------------------------------------------------------------
// Predicates
// ---------------------------------------------------------------------------
/**
* Narrow an `unknown` value to a {@link StructuredError}.
*
* Purpose: safe type guards at catch boundaries where the thrown value may
* be a plain Error, a StructuredError, or something else entirely.
*
* Consumer: headless.ts before deciding whether to call formatStructuredError
* or fall back to String(err).
*/
export function isStructuredError(val: unknown): val is StructuredError {
return (
typeof val === "object" &&
val !== null &&
"message" in val &&
typeof (val as Record<string, unknown>).message === "string"
);
}