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>
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
import { mkdtemp, readFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "bun:test";
|
||||
|
||||
import { makeUltragoalScope } from "../src/session-scope.js";
|
||||
import { createUltragoalPlan, startNextUltragoal } from "../src/plan-crud.js";
|
||||
import { checkpointUltragoal } from "../src/checkpoint.js";
|
||||
import { recordEvidence } from "../src/evidence.js";
|
||||
import { steerUltragoal } from "../src/steering.js";
|
||||
import { buildGoalInstruction } from "../src/goal-instruction.js";
|
||||
import { readUltragoalPlan } from "../src/plan-io.js";
|
||||
import { ultragoalLedgerPath } from "../src/paths.js";
|
||||
|
||||
const BRIEF = "- Implement the alpha feature\n- Implement the beta feature\n";
|
||||
|
||||
async function tmpRepo(): Promise<string> {
|
||||
return mkdtemp(join(tmpdir(), "ug-life-"));
|
||||
}
|
||||
|
||||
describe("scope-threaded lifecycle still works end-to-end", () => {
|
||||
it("create -> complete-goals handoff -> record-evidence -> checkpoint (per_story)", async () => {
|
||||
const repoRoot = await tmpRepo();
|
||||
const scope = makeUltragoalScope(repoRoot, "life-1");
|
||||
const plan = await createUltragoalPlan(scope, { brief: BRIEF, goalMode: "per_story" });
|
||||
expect(plan.goals.length).toBe(2);
|
||||
|
||||
const started = await startNextUltragoal(scope, {});
|
||||
expect("goal" in started).toBe(true);
|
||||
if (!("goal" in started)) throw new Error("expected a goal");
|
||||
const goal = started.goal;
|
||||
|
||||
// handoff text is file/steering based, no create_goal/get_goal language.
|
||||
const instruction = buildGoalInstruction({ plan: started.plan, goal });
|
||||
expect(instruction.text).not.toContain("create_goal");
|
||||
expect(instruction.text).not.toContain("get_goal");
|
||||
expect(instruction.text).toContain(started.plan.goalsPath);
|
||||
|
||||
// pass all seeded criteria
|
||||
for (const c of goal.successCriteria) {
|
||||
await recordEvidence(scope, { goalId: goal.id, criterionId: c.id, status: "pass", evidence: "verified ok" });
|
||||
}
|
||||
// checkpoint complete WITHOUT a goal snapshot (snapshot is optional now)
|
||||
const result = await checkpointUltragoal(scope, {
|
||||
goalId: goal.id,
|
||||
status: "complete",
|
||||
evidence: "alpha done; tests pass; review clean",
|
||||
});
|
||||
expect(result.goal.status).toBe("complete");
|
||||
|
||||
const reread = await readUltragoalPlan(scope);
|
||||
expect(reread.goals.find((g) => g.id === goal.id)?.status).toBe("complete");
|
||||
});
|
||||
|
||||
it("steering adds a subgoal and is written under the session scope", async () => {
|
||||
const repoRoot = await tmpRepo();
|
||||
const scope = makeUltragoalScope(repoRoot, "life-2");
|
||||
await createUltragoalPlan(scope, { brief: BRIEF });
|
||||
const result = await steerUltragoal(scope, {
|
||||
kind: "add_subgoal",
|
||||
source: "cli",
|
||||
title: "Gamma feature",
|
||||
objective: "Implement the gamma feature with care",
|
||||
evidence: "user asked for gamma",
|
||||
rationale: "newly discovered requirement",
|
||||
});
|
||||
expect(result.accepted).toBe(true);
|
||||
const reread = await readUltragoalPlan(scope);
|
||||
expect(reread.goals.some((g) => g.title === "Gamma feature")).toBe(true);
|
||||
// ledger lives in the session scope
|
||||
const ledger = await readFile(ultragoalLedgerPath(scope), "utf8");
|
||||
expect(ledger).toContain("steering_accepted");
|
||||
});
|
||||
|
||||
it("steering is a no-op when the plan is missing for the scope", async () => {
|
||||
const repoRoot = await tmpRepo();
|
||||
const scope = makeUltragoalScope(repoRoot, "life-noplan");
|
||||
await expect(
|
||||
steerUltragoal(scope, {
|
||||
kind: "annotate_ledger",
|
||||
source: "cli",
|
||||
evidence: "x",
|
||||
rationale: "y",
|
||||
}),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user