fix: enforce GSDError usage and activate unused error codes (#997)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
89ee5e439a
commit
f2838d326f
12 changed files with 46 additions and 33 deletions
|
|
@ -11,6 +11,7 @@
|
|||
import { writeFileSync, writeSync, mkdirSync, readdirSync, unlinkSync, statSync, openSync, closeSync, constants } from "node:fs";
|
||||
import { createHash } from "node:crypto";
|
||||
import { join } from "node:path";
|
||||
import { GSDError, GSD_IO_ERROR } from "./errors.js";
|
||||
|
||||
const SEQ_PREFIX_RE = /^(\d+)-/;
|
||||
import type { ExtensionContext } from "@gsd/pi-coding-agent";
|
||||
|
|
@ -95,7 +96,7 @@ function nextActivityFilePath(
|
|||
}
|
||||
}
|
||||
// Fallback: should never reach here in practice
|
||||
throw new Error(`Failed to find available activity log sequence in ${activityDir}`);
|
||||
throw new GSDError(GSD_IO_ERROR, `Failed to find available activity log sequence in ${activityDir}`);
|
||||
}
|
||||
|
||||
export function saveActivityLog(
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import { existsSync, cpSync, readFileSync, writeFileSync, readdirSync, mkdirSync, realpathSync, utimesSync, unlinkSync } from "node:fs";
|
||||
import { isAbsolute, join, resolve } from "node:path";
|
||||
import { GSDError, GSD_IO_ERROR, GSD_GIT_ERROR } from "./errors.js";
|
||||
import { copyWorktreeDb, reconcileWorktreeDb, isDbAvailable } from "./gsd-db.js";
|
||||
import { execSync, execFileSync } from "node:child_process";
|
||||
import {
|
||||
|
|
@ -301,7 +302,8 @@ export function createAutoWorktree(basePath: string, milestoneId: string): strin
|
|||
} catch (err) {
|
||||
// If chdir fails, the worktree was created but we couldn't enter it.
|
||||
// Don't store originalBase -- caller can retry or clean up.
|
||||
throw new Error(
|
||||
throw new GSDError(
|
||||
GSD_IO_ERROR,
|
||||
`Auto-worktree created at ${info.path} but chdir failed: ${err instanceof Error ? err.message : String(err)}`,
|
||||
);
|
||||
}
|
||||
|
|
@ -367,7 +369,8 @@ export function teardownAutoWorktree(
|
|||
process.chdir(originalBasePath);
|
||||
originalBase = null;
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
throw new GSDError(
|
||||
GSD_IO_ERROR,
|
||||
`Failed to chdir back to ${originalBasePath} during teardown: ${err instanceof Error ? err.message : String(err)}`,
|
||||
);
|
||||
}
|
||||
|
|
@ -425,22 +428,22 @@ export function getAutoWorktreePath(basePath: string, milestoneId: string): stri
|
|||
export function enterAutoWorktree(basePath: string, milestoneId: string): string {
|
||||
const p = worktreePath(basePath, milestoneId);
|
||||
if (!existsSync(p)) {
|
||||
throw new Error(`Auto-worktree for ${milestoneId} does not exist at ${p}`);
|
||||
throw new GSDError(GSD_IO_ERROR, `Auto-worktree for ${milestoneId} does not exist at ${p}`);
|
||||
}
|
||||
|
||||
// Validate this is a real git worktree, not a stray directory (#695)
|
||||
const gitPath = join(p, ".git");
|
||||
if (!existsSync(gitPath)) {
|
||||
throw new Error(`Auto-worktree path ${p} exists but is not a git worktree (no .git)`);
|
||||
throw new GSDError(GSD_GIT_ERROR, `Auto-worktree path ${p} exists but is not a git worktree (no .git)`);
|
||||
}
|
||||
try {
|
||||
const content = readFileSync(gitPath, "utf8").trim();
|
||||
if (!content.startsWith("gitdir: ")) {
|
||||
throw new Error(`Auto-worktree path ${p} has a .git but it is not a worktree gitdir pointer`);
|
||||
throw new GSDError(GSD_GIT_ERROR, `Auto-worktree path ${p} has a .git but it is not a worktree gitdir pointer`);
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof Error && err.message.includes("worktree")) throw err;
|
||||
throw new Error(`Auto-worktree path ${p} exists but .git is unreadable`);
|
||||
throw new GSDError(GSD_IO_ERROR, `Auto-worktree path ${p} exists but .git is unreadable`);
|
||||
}
|
||||
|
||||
const previousCwd = process.cwd();
|
||||
|
|
@ -449,7 +452,8 @@ export function enterAutoWorktree(basePath: string, milestoneId: string): string
|
|||
process.chdir(p);
|
||||
originalBase = basePath;
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
throw new GSDError(
|
||||
GSD_IO_ERROR,
|
||||
`Failed to enter auto-worktree at ${p}: ${err instanceof Error ? err.message : String(err)}`,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ import {
|
|||
getProjectTotals, formatCost, formatTokenCount,
|
||||
} from "./metrics.js";
|
||||
import { computeBudgets, resolveExecutorContextWindow } from "./context-budget.js";
|
||||
import { GSDError, GSD_ARTIFACT_MISSING } from "./errors.js";
|
||||
import { join } from "node:path";
|
||||
import { sep as pathSep } from "node:path";
|
||||
import { readdirSync, readFileSync, existsSync, mkdirSync, writeFileSync, renameSync, unlinkSync, statSync } from "node:fs";
|
||||
|
|
@ -2301,7 +2302,7 @@ async function dispatchNextUnit(
|
|||
if (s.currentMilestoneId && isInAutoWorktree(s.basePath) && s.originalBasePath) {
|
||||
try {
|
||||
const roadmapPath = resolveMilestoneFile(s.originalBasePath, s.currentMilestoneId, "ROADMAP");
|
||||
if (!roadmapPath) throw new Error(`Cannot resolve ROADMAP file for milestone ${ s.currentMilestoneId }`);
|
||||
if (!roadmapPath) throw new GSDError(GSD_ARTIFACT_MISSING, `Cannot resolve ROADMAP file for milestone ${ s.currentMilestoneId }`);
|
||||
const roadmapContent = readFileSync(roadmapPath, "utf-8");
|
||||
const mergeResult = mergeMilestoneToMain(s.originalBasePath, s.currentMilestoneId, roadmapContent);
|
||||
s.basePath = s.originalBasePath;
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { join, resolve } from 'node:path';
|
|||
import type { Decision, Requirement } from './types.js';
|
||||
import { resolveGsdRootFile } from './paths.js';
|
||||
import { saveFile } from './files.js';
|
||||
import { GSDError, GSD_STALE_STATE, GSD_IO_ERROR } from './errors.js';
|
||||
|
||||
// ─── Markdown Generators ──────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -249,7 +250,7 @@ export async function updateRequirementInDb(
|
|||
|
||||
const existing = db.getRequirementById(id);
|
||||
if (!existing) {
|
||||
throw new Error(`Requirement ${id} not found`);
|
||||
throw new GSDError(GSD_STALE_STATE, `Requirement ${id} not found`);
|
||||
}
|
||||
|
||||
// Merge updates into existing
|
||||
|
|
@ -331,7 +332,7 @@ export async function saveArtifactToDb(
|
|||
const gsdDir = resolve(basePath, '.gsd');
|
||||
const fullPath = resolve(basePath, '.gsd', opts.path);
|
||||
if (!fullPath.startsWith(gsdDir)) {
|
||||
throw new Error(`saveArtifactToDb: path escapes .gsd/ directory: ${opts.path}`);
|
||||
throw new GSDError(GSD_IO_ERROR, `saveArtifactToDb: path escapes .gsd/ directory: ${opts.path}`);
|
||||
}
|
||||
await saveFile(fullPath, opts.content);
|
||||
} catch (err) {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import { execFileSync, execFile } from "node:child_process";
|
||||
import { resolve } from "node:path";
|
||||
import { GSDError, GSD_PARSE_ERROR } from "./errors.js";
|
||||
|
||||
// ─── Types ──────────────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -75,7 +76,7 @@ export async function getRecentlyChangedFiles(
|
|||
|
||||
try {
|
||||
const days = Math.max(1, Math.floor(Number(sinceDays)));
|
||||
if (!Number.isFinite(days)) throw new Error("invalid sinceDays");
|
||||
if (!Number.isFinite(days)) throw new GSDError(GSD_PARSE_ERROR, "invalid sinceDays");
|
||||
|
||||
// Run all three queries concurrently — they read independent git state
|
||||
const [logRaw, stagedRaw, statusRaw] = await Promise.all([
|
||||
|
|
|
|||
|
|
@ -10,8 +10,6 @@
|
|||
|
||||
export const GSD_STALE_STATE = "GSD_STALE_STATE";
|
||||
export const GSD_LOCK_HELD = "GSD_LOCK_HELD";
|
||||
export const GSD_DISPATCH_FAILED = "GSD_DISPATCH_FAILED";
|
||||
export const GSD_TIMEOUT = "GSD_TIMEOUT";
|
||||
export const GSD_ARTIFACT_MISSING = "GSD_ARTIFACT_MISSING";
|
||||
export const GSD_GIT_ERROR = "GSD_GIT_ERROR";
|
||||
export const GSD_MERGE_CONFLICT = "GSD_MERGE_CONFLICT";
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import {
|
|||
nativeUpdateRef,
|
||||
nativeAddPaths,
|
||||
} from "./native-git-bridge.js";
|
||||
import { GSDError, GSD_MERGE_CONFLICT } from "./errors.js";
|
||||
import { GSDError, GSD_MERGE_CONFLICT, GSD_GIT_ERROR } from "./errors.js";
|
||||
|
||||
// ─── Types ─────────────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -292,7 +292,7 @@ export function runGit(basePath: string, args: string[], options: { allowFailure
|
|||
} catch (error) {
|
||||
if (options.allowFailure) return "";
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
throw new Error(`git ${args.join(" ")} failed in ${basePath}: ${filterGitSvnNoise(message)}`);
|
||||
throw new GSDError(GSD_GIT_ERROR, `git ${args.join(" ")} failed in ${basePath}: ${filterGitSvnNoise(message)}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { createRequire } from 'node:module';
|
|||
import { copyFileSync, existsSync, mkdirSync } from 'node:fs';
|
||||
import { dirname } from 'node:path';
|
||||
import type { Decision, Requirement } from './types.js';
|
||||
import { GSDError, GSD_STALE_STATE } from './errors.js';
|
||||
|
||||
// Create a require function for loading native modules in ESM context
|
||||
const _require = createRequire(import.meta.url);
|
||||
|
|
@ -416,7 +417,7 @@ export function closeDatabase(): void {
|
|||
* Runs a function inside a transaction. Rolls back on error.
|
||||
*/
|
||||
export function transaction<T>(fn: () => T): T {
|
||||
if (!currentDb) throw new Error('gsd-db: No database open');
|
||||
if (!currentDb) throw new GSDError(GSD_STALE_STATE, 'gsd-db: No database open');
|
||||
currentDb.exec('BEGIN');
|
||||
try {
|
||||
const result = fn();
|
||||
|
|
@ -434,7 +435,7 @@ export function transaction<T>(fn: () => T): T {
|
|||
* Insert a decision. The `seq` field is auto-generated.
|
||||
*/
|
||||
export function insertDecision(d: Omit<Decision, 'seq'>): void {
|
||||
if (!currentDb) throw new Error('gsd-db: No database open');
|
||||
if (!currentDb) throw new GSDError(GSD_STALE_STATE, 'gsd-db: No database open');
|
||||
currentDb.prepare(
|
||||
`INSERT INTO decisions (id, when_context, scope, decision, choice, rationale, revisable, superseded_by)
|
||||
VALUES (:id, :when_context, :scope, :decision, :choice, :rationale, :revisable, :superseded_by)`,
|
||||
|
|
@ -495,7 +496,7 @@ export function getActiveDecisions(): Decision[] {
|
|||
* Insert a requirement.
|
||||
*/
|
||||
export function insertRequirement(r: Requirement): void {
|
||||
if (!currentDb) throw new Error('gsd-db: No database open');
|
||||
if (!currentDb) throw new GSDError(GSD_STALE_STATE, 'gsd-db: No database open');
|
||||
currentDb.prepare(
|
||||
`INSERT INTO requirements (id, class, status, description, why, source, primary_owner, supporting_slices, validation, notes, full_content, superseded_by)
|
||||
VALUES (:id, :class, :status, :description, :why, :source, :primary_owner, :supporting_slices, :validation, :notes, :full_content, :superseded_by)`,
|
||||
|
|
@ -747,7 +748,7 @@ export function _resetProvider(): void {
|
|||
* Insert or replace a decision. Uses the `id` UNIQUE constraint for idempotency.
|
||||
*/
|
||||
export function upsertDecision(d: Omit<Decision, 'seq'>): void {
|
||||
if (!currentDb) throw new Error('gsd-db: No database open');
|
||||
if (!currentDb) throw new GSDError(GSD_STALE_STATE, 'gsd-db: No database open');
|
||||
currentDb.prepare(
|
||||
`INSERT OR REPLACE INTO decisions (id, when_context, scope, decision, choice, rationale, revisable, superseded_by)
|
||||
VALUES (:id, :when_context, :scope, :decision, :choice, :rationale, :revisable, :superseded_by)`,
|
||||
|
|
@ -767,7 +768,7 @@ export function upsertDecision(d: Omit<Decision, 'seq'>): void {
|
|||
* Insert or replace a requirement. Uses the `id` PK for idempotency.
|
||||
*/
|
||||
export function upsertRequirement(r: Requirement): void {
|
||||
if (!currentDb) throw new Error('gsd-db: No database open');
|
||||
if (!currentDb) throw new GSDError(GSD_STALE_STATE, 'gsd-db: No database open');
|
||||
currentDb.prepare(
|
||||
`INSERT OR REPLACE INTO requirements (id, class, status, description, why, source, primary_owner, supporting_slices, validation, notes, full_content, superseded_by)
|
||||
VALUES (:id, :class, :status, :description, :why, :source, :primary_owner, :supporting_slices, :validation, :notes, :full_content, :superseded_by)`,
|
||||
|
|
@ -813,7 +814,7 @@ export function insertArtifact(a: {
|
|||
task_id: string | null;
|
||||
full_content: string;
|
||||
}): void {
|
||||
if (!currentDb) throw new Error('gsd-db: No database open');
|
||||
if (!currentDb) throw new GSDError(GSD_STALE_STATE, 'gsd-db: No database open');
|
||||
currentDb.prepare(
|
||||
`INSERT OR REPLACE INTO artifacts (path, artifact_type, milestone_id, slice_id, task_id, full_content, imported_at)
|
||||
VALUES (:path, :artifact_type, :milestone_id, :slice_id, :task_id, :full_content, :imported_at)`,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
import { execSync, execFileSync } from "node:child_process";
|
||||
import { existsSync, readFileSync, unlinkSync, rmSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { GSDError, GSD_GIT_ERROR } from "./errors.js";
|
||||
|
||||
/** Env overlay that suppresses interactive git credential prompts and git-svn noise. */
|
||||
const GIT_NO_PROMPT_ENV = {
|
||||
|
|
@ -148,7 +149,7 @@ function gitExec(basePath: string, args: string[], allowFailure = false): string
|
|||
}).trim();
|
||||
} catch {
|
||||
if (allowFailure) return "";
|
||||
throw new Error(`git ${args.join(" ")} failed in ${basePath}`);
|
||||
throw new GSDError(GSD_GIT_ERROR, `git ${args.join(" ")} failed in ${basePath}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -162,7 +163,7 @@ function gitFileExec(basePath: string, args: string[], allowFailure = false): st
|
|||
}).trim();
|
||||
} catch {
|
||||
if (allowFailure) return "";
|
||||
throw new Error(`git ${args.join(" ")} failed in ${basePath}`);
|
||||
throw new GSDError(GSD_GIT_ERROR, `git ${args.join(" ")} failed in ${basePath}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import {
|
|||
type MarketplaceDiscoveryResult,
|
||||
type DiscoveredPlugin,
|
||||
} from './marketplace-discovery.js';
|
||||
import { GSDError, GSD_STALE_STATE } from './errors.js';
|
||||
import {
|
||||
NamespacedRegistry,
|
||||
componentsFromDiscovery,
|
||||
|
|
@ -252,7 +253,7 @@ export class PluginImporter {
|
|||
componentFilter: (component: NamespacedComponent) => boolean
|
||||
): NamespacedComponent[] {
|
||||
if (!this.registry) {
|
||||
throw new Error('Must call discover() before selectComponents()');
|
||||
throw new GSDError(GSD_STALE_STATE, 'Must call discover() before selectComponents()');
|
||||
}
|
||||
|
||||
return this.registry.getAll().filter(componentFilter);
|
||||
|
|
@ -270,7 +271,7 @@ export class PluginImporter {
|
|||
*/
|
||||
validateImport(selected: NamespacedComponent[]): ValidationResult {
|
||||
if (!this.registry) {
|
||||
throw new Error('Must call discover() before validateImport()');
|
||||
throw new GSDError(GSD_STALE_STATE, 'Must call discover() before validateImport()');
|
||||
}
|
||||
|
||||
// Create a temporary resolver for the selected components
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
*/
|
||||
|
||||
import { readFileSync, readdirSync } from "node:fs";
|
||||
import { GSDError, GSD_PARSE_ERROR } from "./errors.js";
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
|
|
@ -87,10 +88,11 @@ export function loadPrompt(name: string, vars: Record<string, string> = {}): str
|
|||
.map(m => m.slice(2, -2))
|
||||
.filter(key => !(key in vars));
|
||||
if (missing.length > 0) {
|
||||
throw new Error(
|
||||
throw new GSDError(
|
||||
GSD_PARSE_ERROR,
|
||||
`loadPrompt("${name}"): template declares {{${missing.join("}}, {{")}}}} but no value was provided. ` +
|
||||
`This usually means the extension code in memory is older than the template on disk. ` +
|
||||
`Restart pi to reload the extension.`
|
||||
`Restart pi to reload the extension.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
import { existsSync, mkdirSync, readFileSync, realpathSync } from "node:fs";
|
||||
import { join, resolve, sep } from "node:path";
|
||||
import { GSDError, GSD_PARSE_ERROR, GSD_STALE_STATE, GSD_LOCK_HELD, GSD_GIT_ERROR, GSD_MERGE_CONFLICT } from "./errors.js";
|
||||
import {
|
||||
nativeBranchDelete,
|
||||
nativeBranchExists,
|
||||
|
|
@ -121,14 +122,14 @@ export function worktreeBranchName(name: string): string {
|
|||
export function createWorktree(basePath: string, name: string, opts: { branch?: string; startPoint?: string; reuseExistingBranch?: boolean } = {}): WorktreeInfo {
|
||||
// Validate name: alphanumeric, hyphens, underscores only
|
||||
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
||||
throw new Error(`Invalid worktree name "${name}". Use only letters, numbers, hyphens, and underscores.`);
|
||||
throw new GSDError(GSD_PARSE_ERROR, `Invalid worktree name "${name}". Use only letters, numbers, hyphens, and underscores.`);
|
||||
}
|
||||
|
||||
const wtPath = worktreePath(basePath, name);
|
||||
const branch = opts.branch ?? worktreeBranchName(name);
|
||||
|
||||
if (existsSync(wtPath)) {
|
||||
throw new Error(`Worktree "${name}" already exists at ${wtPath}`);
|
||||
throw new GSDError(GSD_STALE_STATE, `Worktree "${name}" already exists at ${wtPath}`);
|
||||
}
|
||||
|
||||
// Ensure the .gsd/worktrees/ directory exists
|
||||
|
|
@ -151,7 +152,8 @@ export function createWorktree(basePath: string, name: string, opts: { branch?:
|
|||
const branchInUse = worktreeEntries.some(entry => entry.branch === branch);
|
||||
|
||||
if (branchInUse) {
|
||||
throw new Error(
|
||||
throw new GSDError(
|
||||
GSD_LOCK_HELD,
|
||||
`Branch "${branch}" is already in use by another worktree. ` +
|
||||
`Remove the existing worktree first with /worktree remove ${name}.`,
|
||||
);
|
||||
|
|
@ -432,12 +434,12 @@ export function mergeWorktreeToMain(basePath: string, name: string, commitMessag
|
|||
const current = nativeGetCurrentBranch(basePath);
|
||||
|
||||
if (current !== mainBranch) {
|
||||
throw new Error(`Must be on ${mainBranch} to merge. Currently on ${current}.`);
|
||||
throw new GSDError(GSD_GIT_ERROR, `Must be on ${mainBranch} to merge. Currently on ${current}.`);
|
||||
}
|
||||
|
||||
const result = nativeMergeSquash(basePath, branch);
|
||||
if (!result.success) {
|
||||
throw new Error(`Merge conflicts detected in: ${result.conflicts.join(", ")}`);
|
||||
throw new GSDError(GSD_MERGE_CONFLICT, `Merge conflicts detected in: ${result.conflicts.join(", ")}`);
|
||||
}
|
||||
|
||||
nativeCommit(basePath, commitMessage);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue