oh-my-opencode 5.0.0-beta.62 → 5.0.0-beta.63
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/THIRD-PARTY-NOTICES.md +8 -6
- package/dist/cli/doctor/checks/browser-provider.d.ts +2 -0
- package/dist/cli/index.js +64 -35
- package/dist/cli-node/index.js +64 -35
- package/dist/config/schema/agent-names.d.ts +0 -1
- package/dist/config/schema/browser-automation.d.ts +0 -2
- package/dist/config/schema/oh-my-opencode-config.d.ts +0 -1
- package/dist/hooks/comment-checker/__fixtures__/download-probe.fixture.d.ts +1 -0
- package/dist/index.js +61 -535
- package/dist/oh-my-opencode.schema.json +0 -1
- package/dist/skills/ast-grep/SKILL.md +12 -0
- package/dist/skills/debugging/references/tools/playwright-cli.md +79 -161
- package/dist/skills/frontend/SKILL.md +1 -1
- package/dist/skills/frontend/references/design/aside.md +1 -1
- package/dist/skills/frontend/references/design/clone-from-url.md +1 -1
- package/dist/skills/frontend/scripts/perfection/lighthouse-audit.py +2 -2
- package/dist/skills/review-work/SKILL.md +1 -1
- package/dist/skills/ultimate-browsing/ATTRIBUTION.md +1 -82
- package/dist/skills/ultimate-browsing/SKILL.md +17 -29
- package/dist/skills/ultimate-browsing/engine/AGENTS.md +2 -1
- package/dist/skills/ultimate-browsing/engine/templates/package.json +2 -2
- package/dist/skills/ultimate-browsing/engine/templates/playwright_mobile_chrome.js +5 -2
- package/dist/skills/ultimate-browsing/engine/templates/playwright_real_chrome.js +8 -6
- package/dist/skills/ultimate-browsing/engine/tests/test_playwright_stealth.py +86 -0
- package/dist/skills/ultimate-browsing/engine/tests/test_playwright_templates.py +4 -7
- package/dist/skills/ultimate-browsing/references/chrome-stealth.md +86 -100
- package/dist/skills/ultimate-browsing/references/insane-search/README.md +9 -3
- package/dist/skills/ultimate-browsing/references/insane-search/playwright.md +16 -13
- package/dist/skills/ultimate-browsing/scripts/extract_cookies.py +3 -3
- package/dist/skills/ulw-execute/SKILL.md +1 -1
- package/dist/skills/ulw-plan/scripts/scaffold-plan.mjs +1 -1
- package/dist/skills/ulw-research/SKILL.md +3 -3
- package/dist/skills/visual-qa/AGENTS.md +1 -1
- package/dist/skills/visual-qa/SKILL.md +1 -1
- package/dist/skills/visual-qa/references/browser-setup.md +75 -0
- package/dist/tui.js +12 -5
- package/package.json +16 -23
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +2 -0
- package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
- package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
- package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
- package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/rules/package.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/agents/plan.toml +2 -2
- package/packages/omo-codex/plugin/components/ultrawork/dist/cli.js +5 -4
- package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/src/directive-content.ts +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/directive.md +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/CHANGELOG.md +13 -0
- package/packages/omo-codex/plugin/components/ulw-loop/directive.md +5 -4
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.js +5 -4
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +582 -322
- package/packages/omo-codex/plugin/components/ulw-loop/dist/domain-types.d.ts +5 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/evidence.js +16 -12
- package/packages/omo-codex/plugin/components/ulw-loop/dist/ledger.d.ts +4 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/ledger.js +34 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-commit.d.ts +19 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-commit.js +107 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-crud.d.ts +2 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-crud.js +48 -22
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-io.d.ts +9 -7
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-io.js +105 -114
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-log.d.ts +15 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-log.js +86 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-missing-recovery.d.ts +5 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-missing-recovery.js +15 -3
- package/packages/omo-codex/plugin/components/ulw-loop/dist/review-blockers.js +3 -4
- package/packages/omo-codex/plugin/components/ulw-loop/dist/sdk/factory.js +12 -5
- package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-budget-io.d.ts +5 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-budget-io.js +67 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-guard.js +8 -62
- package/packages/omo-codex/plugin/components/ulw-loop/dist/state-lock.d.ts +11 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/state-lock.js +120 -88
- package/packages/omo-codex/plugin/components/ulw-loop/dist/status-next-actions.d.ts +3 -2
- package/packages/omo-codex/plugin/components/ulw-loop/dist/status-next-actions.js +18 -10
- package/packages/omo-codex/plugin/components/ulw-loop/dist/steering-batch.js +4 -4
- package/packages/omo-codex/plugin/components/ulw-loop/dist/steering.js +5 -5
- package/packages/omo-codex/plugin/components/ulw-loop/dist/stop-resume-hook.js +8 -17
- package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +5 -5
- package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +2 -2
- package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +5 -4
- package/packages/omo-codex/plugin/components/ulw-loop/src/domain-types.ts +5 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/evidence.ts +16 -16
- package/packages/omo-codex/plugin/components/ulw-loop/src/ledger.ts +33 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/plan-commit.ts +124 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/plan-crud.ts +58 -51
- package/packages/omo-codex/plugin/components/ulw-loop/src/plan-io.ts +126 -140
- package/packages/omo-codex/plugin/components/ulw-loop/src/plan-log.ts +84 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/plan-missing-recovery.ts +26 -3
- package/packages/omo-codex/plugin/components/ulw-loop/src/review-blockers.ts +3 -3
- package/packages/omo-codex/plugin/components/ulw-loop/src/sdk/factory.ts +30 -5
- package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-budget-io.ts +59 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-guard.ts +8 -59
- package/packages/omo-codex/plugin/components/ulw-loop/src/state-lock.ts +129 -111
- package/packages/omo-codex/plugin/components/ulw-loop/src/status-next-actions.ts +19 -10
- package/packages/omo-codex/plugin/components/ulw-loop/src/steering-batch.ts +4 -4
- package/packages/omo-codex/plugin/components/ulw-loop/src/steering.ts +5 -4
- package/packages/omo-codex/plugin/components/ulw-loop/src/stop-resume-hook.ts +8 -16
- package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/barrier.ts +13 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-child.ts +65 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-cli-qa.mjs +76 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-crash.ts +23 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-mutations.mjs +58 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-writer.ts +65 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/lease-clock.ts +23 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/plan-commit-log.test.ts +252 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/plan-commit-recovery.test.ts +190 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/plan-io.test.ts +50 -35
- package/packages/omo-codex/plugin/components/ulw-loop/test/plan-log-newest.test.ts +72 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/sdk-completed-plan.test.ts +43 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/sdk-session-context.test.ts +21 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/state-lock.test.ts +92 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/status-next-actions-surface.test.ts +133 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/steering-batch.test.ts +12 -12
- package/packages/omo-codex/plugin/components/ulw-loop/test/steering.test.ts +4 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/package-lock.json +12 -12
- package/packages/omo-codex/plugin/package.json +1 -1
- package/packages/omo-codex/plugin/scripts/sync-skills.d.mts +4 -0
- package/packages/omo-codex/plugin/scripts/sync-skills.mjs +15 -7
- package/packages/omo-codex/plugin/skills/ast-grep/SKILL.md +12 -0
- package/packages/omo-codex/plugin/skills/debugging/references/tools/playwright-cli.md +79 -161
- package/packages/omo-codex/plugin/skills/frontend/SKILL.md +1 -1
- package/packages/omo-codex/plugin/skills/frontend/references/design/aside.md +1 -1
- package/packages/omo-codex/plugin/skills/frontend/references/design/clone-from-url.md +1 -1
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/lighthouse-audit.py +2 -2
- package/packages/omo-codex/plugin/skills/review-work/SKILL.md +1 -1
- package/packages/omo-codex/plugin/skills/ultimate-browsing/ATTRIBUTION.md +1 -82
- package/packages/omo-codex/plugin/skills/ultimate-browsing/SKILL.md +17 -29
- package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/AGENTS.md +2 -1
- package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/templates/package.json +2 -2
- package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/templates/playwright_mobile_chrome.js +5 -2
- package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/templates/playwright_real_chrome.js +8 -6
- package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/tests/test_playwright_stealth.py +86 -0
- package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/tests/test_playwright_templates.py +4 -7
- package/packages/omo-codex/plugin/skills/ultimate-browsing/references/chrome-stealth.md +86 -100
- package/packages/omo-codex/plugin/skills/ultimate-browsing/references/insane-search/README.md +9 -3
- package/packages/omo-codex/plugin/skills/ultimate-browsing/references/insane-search/playwright.md +16 -13
- package/packages/omo-codex/plugin/skills/ultimate-browsing/scripts/extract_cookies.py +3 -3
- package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +5 -4
- package/packages/omo-codex/plugin/skills/ulw-execute/SKILL.md +1 -1
- package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +2 -2
- package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +3 -3
- package/packages/omo-codex/plugin/skills/visual-qa/AGENTS.md +1 -1
- package/packages/omo-codex/plugin/skills/visual-qa/SKILL.md +1 -1
- package/packages/omo-codex/plugin/skills/visual-qa/references/browser-setup.md +75 -0
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +4 -2
- package/packages/prompts-core/prompts/ultrawork/codex.md +5 -4
- package/packages/shared-skills/skills/ast-grep/SKILL.md +12 -0
- package/packages/shared-skills/skills/debugging/references/tools/playwright-cli.md +79 -161
- package/packages/shared-skills/skills/frontend/SKILL.md +1 -1
- package/packages/shared-skills/skills/frontend/references/design/aside.md +1 -1
- package/packages/shared-skills/skills/frontend/references/design/clone-from-url.md +1 -1
- package/packages/shared-skills/skills/frontend/scripts/perfection/lighthouse-audit.py +2 -2
- package/packages/shared-skills/skills/review-work/SKILL.md +1 -1
- package/packages/shared-skills/skills/ultimate-browsing/ATTRIBUTION.md +1 -82
- package/packages/shared-skills/skills/ultimate-browsing/SKILL.md +17 -29
- package/packages/shared-skills/skills/ultimate-browsing/engine/AGENTS.md +2 -1
- package/packages/shared-skills/skills/ultimate-browsing/engine/templates/package.json +2 -2
- package/packages/shared-skills/skills/ultimate-browsing/engine/templates/playwright_mobile_chrome.js +5 -2
- package/packages/shared-skills/skills/ultimate-browsing/engine/templates/playwright_real_chrome.js +8 -6
- package/packages/shared-skills/skills/ultimate-browsing/engine/tests/test_playwright_stealth.py +86 -0
- package/packages/shared-skills/skills/ultimate-browsing/engine/tests/test_playwright_templates.py +4 -7
- package/packages/shared-skills/skills/ultimate-browsing/references/chrome-stealth.md +86 -100
- package/packages/shared-skills/skills/ultimate-browsing/references/insane-search/README.md +9 -3
- package/packages/shared-skills/skills/ultimate-browsing/references/insane-search/playwright.md +16 -13
- package/packages/shared-skills/skills/ultimate-browsing/scripts/extract_cookies.py +3 -3
- package/packages/shared-skills/skills/ulw-execute/SKILL.md +1 -1
- package/packages/shared-skills/skills/ulw-plan/scripts/scaffold-plan.mjs +1 -1
- package/packages/shared-skills/skills/ulw-research/SKILL.md +3 -3
- package/packages/shared-skills/skills/visual-qa/AGENTS.md +1 -1
- package/packages/shared-skills/skills/visual-qa/SKILL.md +1 -1
- package/packages/shared-skills/skills/visual-qa/references/browser-setup.md +75 -0
- package/dist/skills/visual-qa/references/agent-browser-setup.md +0 -45
- package/packages/omo-codex/plugin/skills/visual-qa/references/agent-browser-setup.md +0 -45
- package/packages/shared-skills/skills/visual-qa/references/agent-browser-setup.md +0 -45
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import type { PreToolUsePayload } from "../src/codex-hook.js";
|
|
7
|
+
import { ulwLoopDir } from "../src/paths.js";
|
|
8
|
+
import { commit, materialize, withCommitHooks } from "../src/plan-commit.js";
|
|
9
|
+
import { createUlwLoopPlan } from "../src/plan-crud.js";
|
|
10
|
+
import {
|
|
11
|
+
planExists,
|
|
12
|
+
readLedger,
|
|
13
|
+
readUlwLoopPlan,
|
|
14
|
+
withMutationLockOptions,
|
|
15
|
+
withUlwLoopMutationLock,
|
|
16
|
+
} from "../src/plan-io.js";
|
|
17
|
+
import { applySpawnBudgetGuards } from "../src/spawn-guard.js";
|
|
18
|
+
import { runStopResumeHook } from "../src/stop-resume-hook.js";
|
|
19
|
+
import { childWriter, cleanupWriters } from "./fixtures/commit-child.js";
|
|
20
|
+
import { leaseClock } from "./fixtures/lease-clock.js";
|
|
21
|
+
|
|
22
|
+
let root: string;
|
|
23
|
+
let dir: string;
|
|
24
|
+
const scope = { sessionId: "commit-log" };
|
|
25
|
+
beforeEach(async () => {
|
|
26
|
+
root = await mkdtemp(join(tmpdir(), "ulw-recovery-"));
|
|
27
|
+
dir = ulwLoopDir(root, scope);
|
|
28
|
+
});
|
|
29
|
+
afterEach(async () => {
|
|
30
|
+
await cleanupWriters();
|
|
31
|
+
vi.unstubAllEnvs();
|
|
32
|
+
await rm(root, { recursive: true, force: true });
|
|
33
|
+
});
|
|
34
|
+
function signal() {
|
|
35
|
+
let resolve: () => void = () => {
|
|
36
|
+
throw new Error("signal not initialized");
|
|
37
|
+
};
|
|
38
|
+
const promise = new Promise<void>((done, reject) => {
|
|
39
|
+
const timeout = setTimeout(() => reject(new Error("signal timed out")), 5_000);
|
|
40
|
+
resolve = () => {
|
|
41
|
+
clearTimeout(timeout);
|
|
42
|
+
done();
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
return { promise, resolve };
|
|
46
|
+
}
|
|
47
|
+
function views() {
|
|
48
|
+
return ["goals.json", "ledger.jsonl", "brief.md"].map((name) => readFileSync(join(dir, name), "utf8"));
|
|
49
|
+
}
|
|
50
|
+
describe("#given crash recovery through public surfaces", () => {
|
|
51
|
+
it("#when (7b) only revision one exists #then both hooks and existence gates use it", async () => {
|
|
52
|
+
const plan = await createUlwLoopPlan(root, { brief: "unfinished" }, scope);
|
|
53
|
+
for (const name of ["goals.json", "ledger.jsonl", "brief.md"]) await rm(join(dir, name));
|
|
54
|
+
expect(readdirSync(join(dir, "revisions"))).toEqual(["00000001.json"]);
|
|
55
|
+
expect(planExists(root, scope)).toBe(true);
|
|
56
|
+
expect((await readUlwLoopPlan(root, scope)).revision).toBe(1);
|
|
57
|
+
const payload = {
|
|
58
|
+
session_id: scope.sessionId,
|
|
59
|
+
turn_id: "turn",
|
|
60
|
+
cwd: root,
|
|
61
|
+
model: "test",
|
|
62
|
+
permission_mode: "default",
|
|
63
|
+
};
|
|
64
|
+
const stop = {
|
|
65
|
+
...payload,
|
|
66
|
+
hook_event_name: "Stop",
|
|
67
|
+
transcript_path: join(root, "transcript"),
|
|
68
|
+
stop_hook_active: false,
|
|
69
|
+
};
|
|
70
|
+
expect(JSON.parse(runStopResumeHook(stop)).decision).toBe("block");
|
|
71
|
+
expect(JSON.parse(readFileSync(join(dir, `auto-resume-${plan.goals[0]?.id}.json`), "utf8")).ledgerLineCount).toBe(
|
|
72
|
+
1,
|
|
73
|
+
);
|
|
74
|
+
vi.stubEnv("OMO_SPAWN_FANOUT_LIMIT", "1");
|
|
75
|
+
vi.stubEnv("PLUGIN_DATA", join(root, "plugin-data"));
|
|
76
|
+
const spawn: PreToolUsePayload = {
|
|
77
|
+
...payload,
|
|
78
|
+
hook_event_name: "PreToolUse",
|
|
79
|
+
transcript_path: null,
|
|
80
|
+
tool_name: "spawn_agent",
|
|
81
|
+
tool_use_id: "tool",
|
|
82
|
+
tool_input: {},
|
|
83
|
+
};
|
|
84
|
+
expect(applySpawnBudgetGuards(spawn)).toBe("");
|
|
85
|
+
expect(JSON.parse(applySpawnBudgetGuards(spawn)).hookSpecificOutput.permissionDecision).toBe("deny");
|
|
86
|
+
});
|
|
87
|
+
it("#when (7c) two materializers overlap #then every public inode is complete even if a view regresses", async () => {
|
|
88
|
+
const plan = await createUlwLoopPlan(root, { brief: "old" }, scope);
|
|
89
|
+
const old = views();
|
|
90
|
+
await withCommitHooks(
|
|
91
|
+
{
|
|
92
|
+
afterCommit: () => {
|
|
93
|
+
throw new Error("crash");
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
() =>
|
|
97
|
+
commit(root, scope, {
|
|
98
|
+
plan: { ...plan, brief: "middle\n" },
|
|
99
|
+
entries: [{ at: "middle", kind: "goal_added" }],
|
|
100
|
+
}),
|
|
101
|
+
).catch((error: unknown) => {
|
|
102
|
+
expect(error).toBeInstanceOf(Error);
|
|
103
|
+
});
|
|
104
|
+
const middleViews = [
|
|
105
|
+
`${JSON.stringify(await readUlwLoopPlan(root, scope), null, 2)}\n`,
|
|
106
|
+
`${readLedger(root, scope)
|
|
107
|
+
.map((entry) => JSON.stringify(entry))
|
|
108
|
+
.join("\n")}\n`,
|
|
109
|
+
"middle\n",
|
|
110
|
+
];
|
|
111
|
+
const partial = signal();
|
|
112
|
+
const resume = signal();
|
|
113
|
+
const first = withCommitHooks(
|
|
114
|
+
{
|
|
115
|
+
writeView: async (path, content) => {
|
|
116
|
+
if (path.endsWith("ledger.jsonl")) {
|
|
117
|
+
await writeFile(path, content.slice(0, 5));
|
|
118
|
+
partial.resolve();
|
|
119
|
+
await resume.promise;
|
|
120
|
+
}
|
|
121
|
+
await writeFile(path, content);
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
() => materialize(dir),
|
|
125
|
+
);
|
|
126
|
+
await partial.promise;
|
|
127
|
+
expect(readFileSync(join(dir, "ledger.jsonl"), "utf8")).toBe(old[1]);
|
|
128
|
+
const latest = await readUlwLoopPlan(root, scope);
|
|
129
|
+
await commit(root, scope, { plan: { ...latest, brief: "new\n" }, entries: [{ at: "new", kind: "goal_added" }] });
|
|
130
|
+
const newViews = views();
|
|
131
|
+
resume.resolve();
|
|
132
|
+
await first;
|
|
133
|
+
const final = views();
|
|
134
|
+
expect(() => JSON.parse(final[0] ?? "")).not.toThrow();
|
|
135
|
+
for (const line of (final[1] ?? "").trim().split("\n")) expect(() => JSON.parse(line)).not.toThrow();
|
|
136
|
+
for (const [index, content] of final.entries()) expect([middleViews[index], newViews[index]]).toContain(content);
|
|
137
|
+
expect(
|
|
138
|
+
readLedger(root, scope)
|
|
139
|
+
.map((entry) => entry.at)
|
|
140
|
+
.slice(-2),
|
|
141
|
+
).toEqual(["middle", "new"]);
|
|
142
|
+
});
|
|
143
|
+
it("#when (9) a force-recreator loses before its first possible view write #then its successor stays untouched", async () => {
|
|
144
|
+
await createUlwLoopPlan(root, { brief: "old" }, scope);
|
|
145
|
+
let successor: string[] = [];
|
|
146
|
+
let writes = 0;
|
|
147
|
+
const clock = leaseClock();
|
|
148
|
+
await expect(
|
|
149
|
+
withCommitHooks(
|
|
150
|
+
{
|
|
151
|
+
beforeMutation: async () => {
|
|
152
|
+
clock.advance(201);
|
|
153
|
+
const writer = childWriter(root, "force", clock.now());
|
|
154
|
+
expect(await writer.exited).toBe(0);
|
|
155
|
+
successor = views();
|
|
156
|
+
},
|
|
157
|
+
writeView: async (path, content) => {
|
|
158
|
+
writes += 1;
|
|
159
|
+
await writeFile(path, content);
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
() =>
|
|
163
|
+
withMutationLockOptions({ clock, leaseMs: 200, heartbeatMs: 0 }, () =>
|
|
164
|
+
createUlwLoopPlan(root, { brief: "A", force: true }, scope),
|
|
165
|
+
),
|
|
166
|
+
),
|
|
167
|
+
).rejects.toMatchObject({ code: "ULW_LOOP_LOCK_LOST" });
|
|
168
|
+
expect(writes).toBe(0);
|
|
169
|
+
expect(views()).toEqual(successor);
|
|
170
|
+
expect(readLedger(root, scope)).toHaveLength(1);
|
|
171
|
+
});
|
|
172
|
+
it("#when a locked ledger-only read follows a crash #then all views are repaired without another revision", async () => {
|
|
173
|
+
const plan = await createUlwLoopPlan(root, { brief: "old" }, scope);
|
|
174
|
+
await expect(
|
|
175
|
+
withCommitHooks(
|
|
176
|
+
{
|
|
177
|
+
afterCommit: () => {
|
|
178
|
+
throw new Error("crash");
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
() => commit(root, scope, { plan: { ...plan, brief: "new\n" }, entries: [] }),
|
|
182
|
+
),
|
|
183
|
+
).rejects.toThrow("crash");
|
|
184
|
+
await withUlwLoopMutationLock(root, scope, async () => {
|
|
185
|
+
expect(readLedger(root, scope)).toHaveLength(1);
|
|
186
|
+
});
|
|
187
|
+
expect(readFileSync(join(dir, "brief.md"), "utf8")).toBe("new\n");
|
|
188
|
+
expect(readdirSync(join(dir, "revisions"))).toHaveLength(2);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
@@ -2,7 +2,6 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { appendFile, copyFile, mkdir, mkdtemp, readdir, readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import { setImmediate as tick } from "node:timers/promises";
|
|
6
5
|
import { beforeEach, describe, expect, it } from "vitest";
|
|
7
6
|
import { ulwLoopDir, ulwLoopGoalsPath, ulwLoopLedgerPath } from "../src/paths.js";
|
|
8
7
|
import {
|
|
@@ -15,6 +14,7 @@ import {
|
|
|
15
14
|
} from "../src/plan-io.js";
|
|
16
15
|
import type { UlwLoopItem, UlwLoopLedgerEntry, UlwLoopPlan, UlwLoopSteeringAudit } from "../src/types.js";
|
|
17
16
|
import { UlwLoopError } from "../src/types.js";
|
|
17
|
+
import { barrier } from "./fixtures/barrier.js";
|
|
18
18
|
|
|
19
19
|
const NOW = "2026-05-23T00:00:00.000Z";
|
|
20
20
|
const STABLE_OBJECTIVE =
|
|
@@ -97,6 +97,12 @@ async function makeRepo(): Promise<string> {
|
|
|
97
97
|
return mkdtemp(join(tmpdir(), "ug-io-"));
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
async function makeLedgerRepo(): Promise<string> {
|
|
101
|
+
const root = await makeRepo();
|
|
102
|
+
await writePlan(root, makePlan());
|
|
103
|
+
return root;
|
|
104
|
+
}
|
|
105
|
+
|
|
100
106
|
async function writeRawPlan(repoRoot: string, plan: UlwLoopPlan): Promise<void> {
|
|
101
107
|
await mkdir(ulwLoopDir(repoRoot), { recursive: true });
|
|
102
108
|
await writeFile(ulwLoopGoalsPath(repoRoot), `${JSON.stringify(plan, null, 2)}\n`, "utf8");
|
|
@@ -136,13 +142,17 @@ describe("readUlwLoopPlan", () => {
|
|
|
136
142
|
expect(plan.goals[0]?.successCriteria).toHaveLength(3);
|
|
137
143
|
});
|
|
138
144
|
|
|
139
|
-
it("migrates legacy aggregate objective
|
|
145
|
+
it("migrates legacy aggregate objective in memory and folds its audit into the enclosing commit", async () => {
|
|
140
146
|
// given
|
|
141
147
|
const legacyObjective = "Complete all ulw-loop stories in .omo/ulw-loop/goals.json: G001 Build auth service";
|
|
142
148
|
await writeRawPlan(repoRoot, makePlan({ codexObjective: legacyObjective }));
|
|
143
149
|
|
|
144
150
|
// when: read inside the mutation lock, the way every mutating command does
|
|
145
|
-
const plan = await withUlwLoopMutationLock(repoRoot, () =>
|
|
151
|
+
const plan = await withUlwLoopMutationLock(repoRoot, async () => {
|
|
152
|
+
const migrated = await readUlwLoopPlan(repoRoot);
|
|
153
|
+
await writePlan(repoRoot, migrated);
|
|
154
|
+
return migrated;
|
|
155
|
+
});
|
|
146
156
|
|
|
147
157
|
// then
|
|
148
158
|
expect(plan.codexObjective).toBe(STABLE_OBJECTIVE);
|
|
@@ -157,22 +167,17 @@ describe("readUlwLoopPlan", () => {
|
|
|
157
167
|
});
|
|
158
168
|
});
|
|
159
169
|
|
|
160
|
-
it("readUlwLoopPlan
|
|
170
|
+
it("readUlwLoopPlan migrates a legacy aggregate plan in memory outside the mutation lock without writing", async () => {
|
|
161
171
|
// given: the same legacy plan, but the read is not wrapped in withUlwLoopMutationLock
|
|
162
172
|
const legacyObjective = "Complete all ulw-loop stories in .omo/ulw-loop/goals.json: G001 Build auth service";
|
|
163
173
|
await writeRawPlan(repoRoot, makePlan({ codexObjective: legacyObjective }));
|
|
164
174
|
|
|
165
175
|
// when
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// then: it refuses with a recovery hint and writes nothing
|
|
174
|
-
expect(caught).toBeInstanceOf(UlwLoopError);
|
|
175
|
-
expect((caught as UlwLoopError).code).toBe("ULW_LOOP_MIGRATION_REQUIRED");
|
|
176
|
+
const migrated = await readUlwLoopPlan(repoRoot);
|
|
177
|
+
|
|
178
|
+
// then: only the returned shape changes
|
|
179
|
+
expect(migrated.codexObjective).toBe(STABLE_OBJECTIVE);
|
|
180
|
+
expect(migrated.codexObjectiveAliases).toContain(legacyObjective);
|
|
176
181
|
const persisted = JSON.parse(await readFile(ulwLoopGoalsPath(repoRoot), "utf8"));
|
|
177
182
|
expect(persisted.codexObjective).toBe(legacyObjective);
|
|
178
183
|
expect(existsSync(ulwLoopLedgerPath(repoRoot))).toBe(false);
|
|
@@ -211,7 +216,8 @@ describe("writePlan", () => {
|
|
|
211
216
|
await writePlan(repoRoot, makePlan({ codexObjective: "first" }));
|
|
212
217
|
|
|
213
218
|
// when
|
|
214
|
-
await
|
|
219
|
+
const current = await readUlwLoopPlan(repoRoot);
|
|
220
|
+
await writePlan(repoRoot, { ...current, codexObjective: "second" });
|
|
215
221
|
|
|
216
222
|
// then
|
|
217
223
|
expect(JSON.parse(await readFile(ulwLoopGoalsPath(repoRoot), "utf8"))).toMatchObject({
|
|
@@ -223,7 +229,7 @@ describe("writePlan", () => {
|
|
|
223
229
|
describe("appendLedger", () => {
|
|
224
230
|
it("appends a single JSONL line to ledger.jsonl", async () => {
|
|
225
231
|
// given
|
|
226
|
-
const repoRoot = await
|
|
232
|
+
const repoRoot = await makeLedgerRepo();
|
|
227
233
|
const ledgerEntry = entry("goal_started");
|
|
228
234
|
|
|
229
235
|
// when
|
|
@@ -236,6 +242,7 @@ describe("appendLedger", () => {
|
|
|
236
242
|
it("creates ledger.jsonl if missing", async () => {
|
|
237
243
|
// given
|
|
238
244
|
const repoRoot = await makeRepo();
|
|
245
|
+
await writeRawPlan(repoRoot, makePlan());
|
|
239
246
|
|
|
240
247
|
// when
|
|
241
248
|
await appendLedger(repoRoot, entry("goal_completed"));
|
|
@@ -246,7 +253,7 @@ describe("appendLedger", () => {
|
|
|
246
253
|
|
|
247
254
|
it("preserves prior entries", async () => {
|
|
248
255
|
// given
|
|
249
|
-
const repoRoot = await
|
|
256
|
+
const repoRoot = await makeLedgerRepo();
|
|
250
257
|
const first = entry("goal_started");
|
|
251
258
|
const second = entry("goal_completed");
|
|
252
259
|
|
|
@@ -262,7 +269,7 @@ describe("appendLedger", () => {
|
|
|
262
269
|
describe("readSteeringLedgerEntries", () => {
|
|
263
270
|
it("returns only steering-related event kinds", async () => {
|
|
264
271
|
// given
|
|
265
|
-
const repoRoot = await
|
|
272
|
+
const repoRoot = await makeLedgerRepo();
|
|
266
273
|
await appendLedger(repoRoot, entry("steering_accepted"));
|
|
267
274
|
await appendLedger(repoRoot, entry("goal_started"));
|
|
268
275
|
await appendLedger(repoRoot, entry("steering_rejected"));
|
|
@@ -297,7 +304,7 @@ describe("findAcceptedSteeringLedgerEntry", () => {
|
|
|
297
304
|
],
|
|
298
305
|
] as const)("finds an accepted entry by %s", async (_name, target) => {
|
|
299
306
|
// given
|
|
300
|
-
const repoRoot = await
|
|
307
|
+
const repoRoot = await makeLedgerRepo();
|
|
301
308
|
await appendLedger(repoRoot, steeringEntry("steering_accepted", steeringAudit({ idempotencyKey: "other-key" })));
|
|
302
309
|
await appendLedger(repoRoot, target);
|
|
303
310
|
|
|
@@ -310,7 +317,7 @@ describe("findAcceptedSteeringLedgerEntry", () => {
|
|
|
310
317
|
|
|
311
318
|
it("skips a rejected entry carrying the same key", async () => {
|
|
312
319
|
// given
|
|
313
|
-
const repoRoot = await
|
|
320
|
+
const repoRoot = await makeLedgerRepo();
|
|
314
321
|
await appendLedger(
|
|
315
322
|
repoRoot,
|
|
316
323
|
steeringEntry("steering_rejected", rejectedSteeringAudit({ idempotencyKey: "needle-key" })),
|
|
@@ -322,7 +329,7 @@ describe("findAcceptedSteeringLedgerEntry", () => {
|
|
|
322
329
|
|
|
323
330
|
it("returns the accepted entry even when a rejected one with the same key precedes it", async () => {
|
|
324
331
|
// given
|
|
325
|
-
const repoRoot = await
|
|
332
|
+
const repoRoot = await makeLedgerRepo();
|
|
326
333
|
const accepted = steeringEntry("steering_accepted", steeringAudit({ idempotencyKey: "needle-key" }));
|
|
327
334
|
await appendLedger(
|
|
328
335
|
repoRoot,
|
|
@@ -344,7 +351,7 @@ describe("findAcceptedSteeringLedgerEntry", () => {
|
|
|
344
351
|
|
|
345
352
|
it("ignores the key when it only appears in non-steering entries", async () => {
|
|
346
353
|
// given
|
|
347
|
-
const repoRoot = await
|
|
354
|
+
const repoRoot = await makeLedgerRepo();
|
|
348
355
|
await appendLedger(repoRoot, { at: NOW, kind: "goal_completed", goalId: "G001", idempotencyKey: "needle-key" });
|
|
349
356
|
await appendLedger(repoRoot, { at: NOW, kind: "evidence_captured", goalId: "G001", evidence: "needle-key" });
|
|
350
357
|
|
|
@@ -354,7 +361,7 @@ describe("findAcceptedSteeringLedgerEntry", () => {
|
|
|
354
361
|
|
|
355
362
|
it("ignores an accepted entry whose key only appears in evidence text", async () => {
|
|
356
363
|
// given
|
|
357
|
-
const repoRoot = await
|
|
364
|
+
const repoRoot = await makeLedgerRepo();
|
|
358
365
|
await appendLedger(repoRoot, steeringEntry("steering_accepted", steeringAudit({ evidence: "needle-key" })));
|
|
359
366
|
|
|
360
367
|
// when/then
|
|
@@ -440,13 +447,13 @@ describe("withUlwLoopMutationLock", () => {
|
|
|
440
447
|
const body = async (): Promise<void> => {
|
|
441
448
|
active += 1;
|
|
442
449
|
maxActive = Math.max(maxActive, active);
|
|
443
|
-
await
|
|
450
|
+
await Promise.resolve();
|
|
444
451
|
active -= 1;
|
|
445
452
|
};
|
|
446
453
|
|
|
447
454
|
// when
|
|
448
455
|
await Promise.all([1, 2, 3].map(() => withUlwLoopMutationLock(repoRoot, body)));
|
|
449
|
-
await
|
|
456
|
+
await Promise.resolve();
|
|
450
457
|
await Promise.all([1, 2].map(() => withUlwLoopMutationLock(repoRoot, body)));
|
|
451
458
|
|
|
452
459
|
// then
|
|
@@ -457,7 +464,8 @@ describe("withUlwLoopMutationLock", () => {
|
|
|
457
464
|
// given
|
|
458
465
|
const repoRoot = await makeRepo();
|
|
459
466
|
const events: string[] = [];
|
|
460
|
-
|
|
467
|
+
const releaseSecond = barrier();
|
|
468
|
+
const enteredSecond = barrier();
|
|
461
469
|
|
|
462
470
|
// when
|
|
463
471
|
const first = withUlwLoopMutationLock(repoRoot, async () => {
|
|
@@ -465,15 +473,16 @@ describe("withUlwLoopMutationLock", () => {
|
|
|
465
473
|
});
|
|
466
474
|
const second = withUlwLoopMutationLock(repoRoot, async () => {
|
|
467
475
|
events.push("second:start");
|
|
468
|
-
|
|
476
|
+
enteredSecond.resolve();
|
|
477
|
+
await releaseSecond.promise;
|
|
469
478
|
events.push("second:end");
|
|
470
479
|
});
|
|
471
480
|
await first;
|
|
472
|
-
await
|
|
481
|
+
await enteredSecond.promise;
|
|
473
482
|
const third = withUlwLoopMutationLock(repoRoot, async () => {
|
|
474
483
|
events.push("third");
|
|
475
484
|
});
|
|
476
|
-
releaseSecond
|
|
485
|
+
releaseSecond.resolve();
|
|
477
486
|
await Promise.all([second, third]);
|
|
478
487
|
|
|
479
488
|
// then
|
|
@@ -485,18 +494,21 @@ describe("withUlwLoopMutationLock", () => {
|
|
|
485
494
|
const rootA = await makeRepo();
|
|
486
495
|
const rootB = await makeRepo();
|
|
487
496
|
const events: string[] = [];
|
|
488
|
-
|
|
497
|
+
const releaseA = barrier();
|
|
498
|
+
const enteredA = barrier();
|
|
489
499
|
|
|
490
500
|
// when
|
|
491
501
|
const held = withUlwLoopMutationLock(rootA, async () => {
|
|
492
502
|
events.push("a:start");
|
|
493
|
-
|
|
503
|
+
enteredA.resolve();
|
|
504
|
+
await releaseA.promise;
|
|
494
505
|
events.push("a:end");
|
|
495
506
|
});
|
|
507
|
+
await enteredA.promise;
|
|
496
508
|
await withUlwLoopMutationLock(rootB, async () => {
|
|
497
509
|
events.push("b");
|
|
498
510
|
});
|
|
499
|
-
releaseA
|
|
511
|
+
releaseA.resolve();
|
|
500
512
|
await held;
|
|
501
513
|
|
|
502
514
|
// then
|
|
@@ -508,21 +520,24 @@ describe("withUlwLoopMutationLock", () => {
|
|
|
508
520
|
const repoRoot = await makeRepo();
|
|
509
521
|
const scoped = { sessionId: "session-a" };
|
|
510
522
|
const events: string[] = [];
|
|
511
|
-
|
|
523
|
+
const releaseScoped = barrier();
|
|
524
|
+
const enteredScoped = barrier();
|
|
512
525
|
|
|
513
526
|
// when
|
|
514
527
|
const heldScoped = withUlwLoopMutationLock(repoRoot, scoped, async () => {
|
|
515
528
|
events.push("scoped-1:start");
|
|
516
|
-
|
|
529
|
+
enteredScoped.resolve();
|
|
530
|
+
await releaseScoped.promise;
|
|
517
531
|
events.push("scoped-1:end");
|
|
518
532
|
});
|
|
533
|
+
await enteredScoped.promise;
|
|
519
534
|
const queuedScoped = withUlwLoopMutationLock(repoRoot, scoped, async () => {
|
|
520
535
|
events.push("scoped-2");
|
|
521
536
|
});
|
|
522
537
|
await withUlwLoopMutationLock(repoRoot, async () => {
|
|
523
538
|
events.push("root-scope");
|
|
524
539
|
});
|
|
525
|
-
releaseScoped
|
|
540
|
+
releaseScoped.resolve();
|
|
526
541
|
await Promise.all([heldScoped, queuedScoped]);
|
|
527
542
|
|
|
528
543
|
// then
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
6
|
+
import { ulwLoopDir } from "../src/paths.js";
|
|
7
|
+
import { createUlwLoopPlan } from "../src/plan-crud.js";
|
|
8
|
+
import { readNewestRecord, readRecords, reconcilePlan } from "../src/plan-log.js";
|
|
9
|
+
import type { UlwLoopPlan } from "../src/types.js";
|
|
10
|
+
|
|
11
|
+
let root: string;
|
|
12
|
+
let dir: string;
|
|
13
|
+
let base: UlwLoopPlan;
|
|
14
|
+
const scope = { sessionId: "newest-record" };
|
|
15
|
+
beforeEach(async () => {
|
|
16
|
+
root = await mkdtemp(join(tmpdir(), "ulw-newest-"));
|
|
17
|
+
dir = ulwLoopDir(root, scope);
|
|
18
|
+
base = await createUlwLoopPlan(root, { brief: "initial" }, scope);
|
|
19
|
+
});
|
|
20
|
+
afterEach(async () => {
|
|
21
|
+
await rm(root, { recursive: true, force: true });
|
|
22
|
+
});
|
|
23
|
+
// The names are deliberately mixed-width: lexicographically "000000010.json" sorts BEFORE "00000009.json",
|
|
24
|
+
// so only numeric ordering of the parsed revision picks revision 10 as the newest record.
|
|
25
|
+
function seedRecord(name: string, revision: number, brief: string): UlwLoopPlan {
|
|
26
|
+
const plan: UlwLoopPlan = { ...base, revision, brief };
|
|
27
|
+
mkdirSync(join(dir, "revisions"), { recursive: true });
|
|
28
|
+
writeFileSync(
|
|
29
|
+
join(dir, "revisions", name),
|
|
30
|
+
`${JSON.stringify({ version: 1, revision, plan, ledger: [{ at: "now", kind: "goal_added", revision, id: `${revision}-0` }] })}\n`,
|
|
31
|
+
);
|
|
32
|
+
return plan;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe("#given a commit log whose newest record is the only one a plan read needs", () => {
|
|
36
|
+
it("#when the newest record is valid #then it wins over older records and a stale goals cache", async () => {
|
|
37
|
+
seedRecord("00000009.json", 9, "nine");
|
|
38
|
+
const newest = seedRecord("000000010.json", 10, "ten");
|
|
39
|
+
|
|
40
|
+
expect(base.revision).toBe(1);
|
|
41
|
+
expect(readNewestRecord(dir)?.revision).toBe(10);
|
|
42
|
+
expect(reconcilePlan(dir)).toEqual(newest);
|
|
43
|
+
});
|
|
44
|
+
it("#when the newest record is corrupt #then the newest valid older record wins", () => {
|
|
45
|
+
const older = seedRecord("00000009.json", 9, "nine");
|
|
46
|
+
writeFileSync(join(dir, "revisions", "000000010.json"), "{not json");
|
|
47
|
+
|
|
48
|
+
expect(readNewestRecord(dir)?.revision).toBe(9);
|
|
49
|
+
expect(reconcilePlan(dir)).toEqual(older);
|
|
50
|
+
});
|
|
51
|
+
it("#when an OLDER record is unreadable for a non-syntax reason #then the read stops at the newest record", () => {
|
|
52
|
+
const newest = seedRecord("000000010.json", 10, "ten");
|
|
53
|
+
rmSync(join(dir, "revisions", "00000001.json"));
|
|
54
|
+
mkdirSync(join(dir, "revisions", "00000001.json"));
|
|
55
|
+
|
|
56
|
+
expect(reconcilePlan(dir)).toEqual(newest);
|
|
57
|
+
expect(readNewestRecord(dir)).toEqual({
|
|
58
|
+
version: 1,
|
|
59
|
+
revision: 10,
|
|
60
|
+
plan: newest,
|
|
61
|
+
ledger: [{ at: "now", kind: "goal_added", revision: 10, id: "10-0" }],
|
|
62
|
+
});
|
|
63
|
+
expect(() => readRecords(dir)).toThrow(/EISDIR/);
|
|
64
|
+
});
|
|
65
|
+
it("#when the NEWEST record is unreadable for a non-syntax reason #then the error propagates", () => {
|
|
66
|
+
seedRecord("00000009.json", 9, "nine");
|
|
67
|
+
mkdirSync(join(dir, "revisions", "000000010.json"));
|
|
68
|
+
|
|
69
|
+
expect(() => readNewestRecord(dir)).toThrow(/EISDIR/);
|
|
70
|
+
expect(() => reconcilePlan(dir)).toThrow(/EISDIR/);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { commit } from "../src/plan-commit.js";
|
|
6
|
+
import { readUlwLoopPlan, withUlwLoopMutationLock } from "../src/plan-io.js";
|
|
7
|
+
import { createAgentToolkit } from "../src/sdk.js";
|
|
8
|
+
import type { UlwLoopToolkitSurface } from "../src/surface.js";
|
|
9
|
+
|
|
10
|
+
const directories: string[] = [];
|
|
11
|
+
afterEach(async () => {
|
|
12
|
+
for (const directory of directories.splice(0)) await rm(directory, { recursive: true, force: true });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const surfaces: UlwLoopToolkitSurface[] = ["lazycodex", "omo-senpi"];
|
|
16
|
+
describe.each(surfaces)("#given a completed plan on %s", (surface) => {
|
|
17
|
+
it("#when createGoals is called #then it rejects without overwriting, unless explicitly forced", async () => {
|
|
18
|
+
const cwd = await mkdtemp(join(tmpdir(), "sdk-completed-plan-"));
|
|
19
|
+
directories.push(cwd);
|
|
20
|
+
const scope = { sessionId: "completed-plan" };
|
|
21
|
+
const toolkit = createAgentToolkit({ cwd, ...scope, surface });
|
|
22
|
+
expect((await toolkit.createGoals({ brief: "- original" })).ok).toBe(true);
|
|
23
|
+
await withUlwLoopMutationLock(cwd, scope, async () => {
|
|
24
|
+
const plan = await readUlwLoopPlan(cwd, scope);
|
|
25
|
+
for (const goal of plan.goals) goal.status = "complete";
|
|
26
|
+
await commit(cwd, scope, { plan, entries: [] });
|
|
27
|
+
});
|
|
28
|
+
const before = await readUlwLoopPlan(cwd, scope);
|
|
29
|
+
const response = await toolkit.createGoals({ brief: "- replacement" });
|
|
30
|
+
expect(response).toMatchObject({ ok: false, error: { code: "ULW_LOOP_PLAN_EXISTS_COMPLETE" } });
|
|
31
|
+
if (response.ok) throw new Error("expected createGoals to fail");
|
|
32
|
+
if (surface === "omo-senpi") {
|
|
33
|
+
expect(response.error.message).toMatch(/agentToolkit\.createGoals\(/);
|
|
34
|
+
expect(response.error.message).not.toMatch(/omo-agent-toolkit ulw-loop/);
|
|
35
|
+
} else {
|
|
36
|
+
expect(response.error.message).toMatch(/omo-agent-toolkit ulw-loop create-goals/);
|
|
37
|
+
expect(response.error.message).not.toMatch(/agentToolkit\./);
|
|
38
|
+
}
|
|
39
|
+
expect(await readUlwLoopPlan(cwd, scope)).toEqual(before);
|
|
40
|
+
expect((await toolkit.createGoals({ brief: "- replacement", force: true })).ok).toBe(true);
|
|
41
|
+
expect((await readUlwLoopPlan(cwd, scope)).goals[0]?.id).toBe("G001-replacement");
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -72,6 +72,27 @@ async function seedSession(
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
describe("agent toolkit session context", () => {
|
|
75
|
+
describe("#given a session id that normalizes to null", () => {
|
|
76
|
+
it("#when an omo-senpi SDK context is created #then it rejects without creating session state", async () => {
|
|
77
|
+
const cwd = await makeWorkdir();
|
|
78
|
+
|
|
79
|
+
expect(() => createAgentToolkit({ cwd, sessionId: "../../x", surface: "omo-senpi" })).toThrowError(
|
|
80
|
+
expect.objectContaining({ code: "ULW_LOOP_SESSION_ID_INVALID" }),
|
|
81
|
+
);
|
|
82
|
+
expect(existsSync(join(cwd, ".omo", "ulw-loop"))).toBe(false);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("#given a blank session id", () => {
|
|
87
|
+
it("#when an SDK context is created #then it reports the required-id error", async () => {
|
|
88
|
+
const cwd = await makeWorkdir();
|
|
89
|
+
|
|
90
|
+
expect(() => createAgentToolkit({ cwd, sessionId: " ", surface: "omo-senpi" })).toThrowError(
|
|
91
|
+
expect.objectContaining({ code: "ULW_LOOP_SESSION_ID_REQUIRED" }),
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
75
96
|
describe("#given a plan written under session A", () => {
|
|
76
97
|
it("#when session B reads through its own context #then the read fails closed instead of borrowing A", async () => {
|
|
77
98
|
const cwd = await makeWorkdir();
|