Files
oh-my-opencode/packages/omo-claude/plugin/components/ultragoal/src/goal-snapshot.ts
T
YeonGyu-Kim 71e4e2a6d0 feat(omo-claude): port ultragoal to per-session file-based goals
Goal content under ./.omo/ultragoal/sessions/claude-<id>/ keyed by Claude Code
session_id (claude: prefix); UltragoalScope struct threaded; plan version 2 +
index.json registry with read-only v1 forward-migration (never deletes v1);
create_goal/get_goal/update_goal dependency dropped (file/steering-based); the
PreToolUse create_goal budget guard kept compiled+unit-tested but its hooks.json
registration removed (inert in CC). 26 tests pass. ultragoal removed from
sync-components HANDLED set — it is a hand-fork, not patch-synced.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 13:40:11 +09:00

135 lines
4.6 KiB
TypeScript

import { existsSync } from "node:fs";
import { readFile } from "node:fs/promises";
import { resolve } from "node:path";
export type GoalSnapshotStatus = "active" | "complete" | "cancelled" | "failed" | "unknown";
export interface GoalSnapshot {
available: boolean;
objective?: string;
status?: GoalSnapshotStatus;
raw: unknown;
}
export interface GoalReconciliation {
ok: boolean;
snapshot: GoalSnapshot;
warnings: string[];
errors: string[];
}
export interface ReconcileGoalOptions {
expectedObjective: string;
acceptedObjectives?: readonly string[];
allowedStatuses?: readonly GoalSnapshotStatus[];
requireSnapshot?: boolean;
requireComplete?: boolean;
}
export class GoalSnapshotError extends Error {}
function safeObject(value: unknown): Record<string, unknown> {
return value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : {};
}
function safeString(value: unknown): string {
return typeof value === "string" ? value.trim() : "";
}
function normalizeStatus(value: unknown): GoalSnapshotStatus {
const status = safeString(value).toLowerCase();
if (status === "complete" || status === "completed" || status === "done") return "complete";
if (status === "cancelled" || status === "canceled") return "cancelled";
if (status === "failed" || status === "failure") return "failed";
if (status === "active" || status === "in_progress" || status === "pending" || status === "running") return "active";
return "unknown";
}
function normalizeObjective(value: string): string {
return value.replace(/\s+/g, " ").trim();
}
export function parseGoalSnapshot(value: unknown): GoalSnapshot {
const root = safeObject(value);
const goalValue = Object.hasOwn(root, "goal") ? root["goal"] : value;
if (goalValue === null || goalValue === undefined || goalValue === false) {
return { available: false, raw: value };
}
const goal = safeObject(goalValue);
const objective = safeString(goal["objective"] ?? goal["goal"] ?? goal["description"] ?? root["objective"]);
const status = normalizeStatus(goal["status"] ?? root["status"]);
return {
available: Boolean(objective || status !== "unknown"),
...(objective ? { objective } : {}),
status,
raw: value,
};
}
export async function readGoalSnapshotInput(raw: string | undefined, cwd = process.cwd()): Promise<GoalSnapshot | null> {
if (!raw?.trim()) return null;
const trimmed = raw.trim();
try {
return parseGoalSnapshot(JSON.parse(trimmed));
} catch {
const path = resolve(cwd, trimmed);
if (!existsSync(path)) {
throw new GoalSnapshotError(`Goal snapshot is neither valid JSON nor a readable path: ${trimmed}`);
}
try {
return parseGoalSnapshot(JSON.parse(await readFile(path, "utf-8")));
} catch (error) {
throw new GoalSnapshotError(
`Goal snapshot path does not contain valid JSON: ${trimmed}${error instanceof Error ? ` (${error.message})` : ""}`,
);
}
}
}
export function reconcileGoalSnapshot(
snapshot: GoalSnapshot | null | undefined,
options: ReconcileGoalOptions,
): GoalReconciliation {
const effectiveSnapshot = snapshot ?? { available: false, raw: null };
const errors: string[] = [];
const warnings: string[] = [];
if (!effectiveSnapshot.available) {
const message = "Goal snapshot is absent or reports no active goal.";
if (options.requireSnapshot) errors.push(message);
else warnings.push(message);
return { ok: errors.length === 0, snapshot: effectiveSnapshot, warnings, errors };
}
const expected = normalizeObjective(options.expectedObjective);
const accepted = new Set(
[expected, ...(options.acceptedObjectives ?? []).map((objective) => normalizeObjective(objective))].filter(
Boolean,
),
);
const actual = normalizeObjective(effectiveSnapshot.objective ?? "");
if (!actual) {
errors.push("Goal snapshot is missing objective text.");
} else if (!accepted.has(actual)) {
errors.push(`Goal objective mismatch: expected "${expected}", got "${actual}".`);
}
const allowed = options.allowedStatuses ?? (options.requireComplete ? ["complete"] : ["active", "complete"]);
const actualStatus = effectiveSnapshot.status ?? "unknown";
if (!allowed.includes(actualStatus)) {
errors.push(`Goal status mismatch: expected ${allowed.join(" or ")}, got ${actualStatus}.`);
}
if (options.requireComplete && actualStatus !== "complete") {
errors.push("Goal is not complete; finish the work and pass a complete snapshot.");
}
return { ok: errors.length === 0, snapshot: effectiveSnapshot, warnings, errors };
}
export function formatGoalReconciliation(reconciliation: GoalReconciliation): string {
const parts = [...reconciliation.errors, ...reconciliation.warnings];
return parts.join(" ");
}