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,13 @@
|
|
|
1
|
+
export function barrier() {
|
|
2
|
+
let resolve: () => void = () => {
|
|
3
|
+
throw new Error("barrier not initialized");
|
|
4
|
+
};
|
|
5
|
+
const promise = new Promise<void>((done, reject) => {
|
|
6
|
+
const timeout = setTimeout(() => reject(new Error("barrier timed out")), 5_000);
|
|
7
|
+
resolve = () => {
|
|
8
|
+
clearTimeout(timeout);
|
|
9
|
+
done();
|
|
10
|
+
};
|
|
11
|
+
});
|
|
12
|
+
return { promise, resolve };
|
|
13
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { fork } from "node:child_process";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
const running = new Set<ReturnType<typeof childWriter>>();
|
|
5
|
+
export async function cleanupWriters(): Promise<void> {
|
|
6
|
+
await Promise.all(
|
|
7
|
+
[...running].map(async (writer) => {
|
|
8
|
+
writer.child.kill("SIGKILL");
|
|
9
|
+
await writer.exited;
|
|
10
|
+
}),
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
export function childWriter(rootPath: string, mode: string, now = Date.now()) {
|
|
14
|
+
const child = fork(join(import.meta.dirname, "commit-writer.ts"), [rootPath, mode, String(now)], {
|
|
15
|
+
execPath: "bun",
|
|
16
|
+
execArgv: [],
|
|
17
|
+
stdio: ["ignore", "inherit", "inherit", "ipc"],
|
|
18
|
+
});
|
|
19
|
+
const pending = new Map<string, { resolve: () => void; reject: (error: Error) => void }>();
|
|
20
|
+
const seen = new Set<string>();
|
|
21
|
+
child.on("message", (message) => {
|
|
22
|
+
if (typeof message !== "string") return;
|
|
23
|
+
seen.add(message);
|
|
24
|
+
pending.get(message)?.resolve();
|
|
25
|
+
});
|
|
26
|
+
const exited = new Promise<number | null>((resolve, reject) => {
|
|
27
|
+
const timeout = setTimeout(() => {
|
|
28
|
+
child.kill("SIGKILL");
|
|
29
|
+
reject(new Error("child exit timed out"));
|
|
30
|
+
}, 15_000);
|
|
31
|
+
child.once("error", (error) => {
|
|
32
|
+
clearTimeout(timeout);
|
|
33
|
+
reject(error);
|
|
34
|
+
});
|
|
35
|
+
child.once("exit", (code) => {
|
|
36
|
+
clearTimeout(timeout);
|
|
37
|
+
running.delete(writer);
|
|
38
|
+
resolve(code);
|
|
39
|
+
for (const [name, waiter] of pending)
|
|
40
|
+
if (!seen.has(name)) waiter.reject(new Error(`child exited before ${name}: ${code}`));
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
const writer = {
|
|
44
|
+
child,
|
|
45
|
+
exited,
|
|
46
|
+
wait(name: string) {
|
|
47
|
+
if (seen.has(name)) return Promise.resolve();
|
|
48
|
+
return new Promise<void>((resolve, reject) => {
|
|
49
|
+
const timeout = setTimeout(() => reject(new Error(`IPC timeout: ${name}`)), 10_000);
|
|
50
|
+
pending.set(name, {
|
|
51
|
+
resolve: () => {
|
|
52
|
+
clearTimeout(timeout);
|
|
53
|
+
resolve();
|
|
54
|
+
},
|
|
55
|
+
reject: (error) => {
|
|
56
|
+
clearTimeout(timeout);
|
|
57
|
+
reject(error);
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
running.add(writer);
|
|
64
|
+
return writer;
|
|
65
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { fork, spawnSync } from "node:child_process";
|
|
3
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join, resolve } from "node:path";
|
|
6
|
+
import { withCommitHooks } from "../../dist/plan-commit.js";
|
|
7
|
+
import { commit } from "../../dist/plan-commit.js";
|
|
8
|
+
import { readLedger, readUlwLoopPlan, withUlwLoopMutationLock } from "../../dist/plan-io.js";
|
|
9
|
+
const cli = resolve("dist/cli.js");
|
|
10
|
+
const root = mkdtempSync(join(tmpdir(), "ulw-cli-qa-"));
|
|
11
|
+
const legacyRoot = mkdtempSync(join(tmpdir(), "ulw-legacy-qa-"));
|
|
12
|
+
const scope = { sessionId: "commit-log" };
|
|
13
|
+
const dir = join(root, ".omo", "ulw-loop", scope.sessionId);
|
|
14
|
+
const json = (path) => JSON.parse(readFileSync(path, "utf8"));
|
|
15
|
+
function run(cwd, ...args) {
|
|
16
|
+
const argv = [...args, "--session-id", scope.sessionId, "--json"];
|
|
17
|
+
const result = spawnSync(process.execPath, [cli, ...argv], { cwd, encoding: "utf8", timeout: 15_000, env: { ...process.env, PI_SESSION_ID: "", CODEX_SESSION_ID: "", CODEX_THREAD_ID: "", OMO_ULW_LOOP_SESSION_ID: "" } });
|
|
18
|
+
console.log(`command=${process.execPath} ${cli} ${argv.map((arg) => JSON.stringify(arg)).join(" ")} cwd=${cwd}\n${result.stdout}${result.stderr}\nexit=${result.status}`);
|
|
19
|
+
assert.equal(result.status, 0);
|
|
20
|
+
return JSON.parse(result.stdout);
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
run(root, "create-goals", "--brief", "Build durable state");
|
|
24
|
+
const original = json(join(dir, "goals.json"));
|
|
25
|
+
assert.equal(run(root, "status").plan.revision, 1);
|
|
26
|
+
rmSync(join(dir, "goals.json"));
|
|
27
|
+
assert.equal(run(root, "status").plan.revision, 1);
|
|
28
|
+
assert.equal(existsSync(join(dir, "goals.json")), false);
|
|
29
|
+
const goal = original.goals[0];
|
|
30
|
+
run(root, "record-evidence", "--goal-id", goal.id, "--criterion-id", goal.successCriteria[0].id, "--status", "pass", "--evidence", "Observed real CLI publication");
|
|
31
|
+
assert.equal(json(join(dir, "revisions", "00000002.json")).ledger[0].kind, "evidence_captured");
|
|
32
|
+
assert.equal(json(join(dir, "goals.json")).revision, 2);
|
|
33
|
+
console.log("QA=PASS log-only status is read-only; CLI evidence repairs and commits plan plus audit");
|
|
34
|
+
const clock = { now: () => 0, schedule: () => { throw new Error("Unexpected scheduler use"); } };
|
|
35
|
+
await assert.rejects(withCommitHooks({ beforeCommit: async () => {
|
|
36
|
+
const child = fork(resolve("test/fixtures/commit-writer.ts"), [root, "write", "201"], { execPath: "bun", execArgv: [], stdio: ["ignore", "inherit", "inherit", "ipc"] });
|
|
37
|
+
const exit = await new Promise((done, reject) => {
|
|
38
|
+
const timeout = setTimeout(() => { child.kill("SIGKILL"); reject(new Error("writer B timed out")); }, 10_000);
|
|
39
|
+
child.once("error", (error) => { clearTimeout(timeout); reject(error); });
|
|
40
|
+
child.once("exit", (code) => { clearTimeout(timeout); done(code); });
|
|
41
|
+
});
|
|
42
|
+
assert.equal(exit, 0);
|
|
43
|
+
} }, () => withUlwLoopMutationLock(root, scope, async () => {
|
|
44
|
+
const plan = await readUlwLoopPlan(root, scope);
|
|
45
|
+
plan.brief = "zombie";
|
|
46
|
+
await commit(root, scope, { plan, entries: [{ at: "A", kind: "goal_added", message: "A" }] });
|
|
47
|
+
}, { clock, leaseMs: 200, heartbeatMs: 0 })), { code: "ULW_LOOP_LOCK_LOST" });
|
|
48
|
+
assert.equal(readLedger(root, scope).at(-1).message, "B");
|
|
49
|
+
assert.equal(readLedger(root, scope).some((entry) => entry.message === "A"), false);
|
|
50
|
+
assert.deepEqual(readdirSync(join(dir, "revisions")), ["00000001.json", "00000002.json", "00000003.json"]);
|
|
51
|
+
console.log(`QA=PASS real fork zombie fencing ULW_LOOP_LOCK_LOST record=${JSON.stringify(json(join(dir, "revisions", "00000003.json")))}`);
|
|
52
|
+
run(root, "create-goals", "--brief", "Replacement brief", "--force");
|
|
53
|
+
const recreated = json(join(dir, "goals.json"));
|
|
54
|
+
assert.equal(recreated.revision, 4);
|
|
55
|
+
assert.equal(recreated.ledgerResetRevision, 4);
|
|
56
|
+
assert.equal(readFileSync(join(dir, "brief.md"), "utf8"), recreated.brief);
|
|
57
|
+
assert.equal(readLedger(root, scope).length, 1);
|
|
58
|
+
console.log("QA=PASS force recreation retains revision history and resets the logical audit");
|
|
59
|
+
const legacyDir = join(legacyRoot, ".omo", "ulw-loop", scope.sessionId);
|
|
60
|
+
mkdirSync(legacyDir, { recursive: true });
|
|
61
|
+
delete original.revision; delete original.brief; delete original.ledgerResetRevision;
|
|
62
|
+
writeFileSync(join(legacyDir, "goals.json"), JSON.stringify(original));
|
|
63
|
+
writeFileSync(join(legacyDir, "brief.md"), "Legacy brief\n");
|
|
64
|
+
writeFileSync(join(legacyDir, "ledger.jsonl"), '{"at":"legacy","kind":"goal_started"}\n');
|
|
65
|
+
assert.equal(run(legacyRoot, "status").plan.revision ?? 0, 0);
|
|
66
|
+
assert.equal(existsSync(join(legacyDir, "revisions")), false);
|
|
67
|
+
run(legacyRoot, "add-goal", "--title", "Extra", "--objective", "Verify legacy hydration");
|
|
68
|
+
assert.equal(json(join(legacyDir, "revisions", "00000001.json")).plan.brief, "Legacy brief\n");
|
|
69
|
+
assert.equal(readLedger(legacyRoot, scope)[0].id, "legacy-1");
|
|
70
|
+
console.log("QA=PASS legacy CLI status and first commit preserve brief and legacy audit");
|
|
71
|
+
} finally {
|
|
72
|
+
rmSync(root, { recursive: true, force: true });
|
|
73
|
+
rmSync(legacyRoot, { recursive: true, force: true });
|
|
74
|
+
assert.equal(existsSync(root) || existsSync(legacyRoot), false);
|
|
75
|
+
console.log(`cleanup=REMOVED ${root} ${legacyRoot}; real home directories not accessed by this driver`);
|
|
76
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { rm } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { type UlwLoopScope, ulwLoopDir } from "../../src/paths.js";
|
|
5
|
+
import { createUlwLoopPlan } from "../../src/plan-crud.js";
|
|
6
|
+
|
|
7
|
+
export async function seedCrash(root: string, scope: UlwLoopScope, first: boolean) {
|
|
8
|
+
const dir = ulwLoopDir(root, scope);
|
|
9
|
+
const plan = await createUlwLoopPlan(root, { brief: "original" }, scope);
|
|
10
|
+
const next = { ...plan, revision: (plan.revision ?? 0) + 1, brief: "committed\n", codexObjective: "committed" };
|
|
11
|
+
mkdirSync(join(dir, "revisions"), { recursive: true });
|
|
12
|
+
writeFileSync(
|
|
13
|
+
join(dir, "revisions", `${String(next.revision).padStart(8, "0")}.json`),
|
|
14
|
+
JSON.stringify({
|
|
15
|
+
version: 1,
|
|
16
|
+
revision: next.revision,
|
|
17
|
+
plan: next,
|
|
18
|
+
ledger: [{ at: "now", kind: "goal_added", revision: next.revision, id: `${next.revision}-0` }],
|
|
19
|
+
}),
|
|
20
|
+
);
|
|
21
|
+
if (first) await rm(join(dir, "goals.json"));
|
|
22
|
+
return next;
|
|
23
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Run only in an isolated remote test checkout. Each mutant is restored before the next.
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
function replace(source, before, after) {
|
|
7
|
+
if (source.split(before).length !== 2) throw new Error(`Mutation anchor is not unique: ${before}`);
|
|
8
|
+
return source.replace(before, after);
|
|
9
|
+
}
|
|
10
|
+
const publication = '(hooks.getStore()?.link ?? linkSync)(temp, join(dir, "revisions", `${String(next).padStart(8, "0")}.json`));';
|
|
11
|
+
const logTest = "test/plan-commit-log.test.ts";
|
|
12
|
+
const recoveryTest = "test/plan-commit-recovery.test.ts";
|
|
13
|
+
const mutations = [
|
|
14
|
+
{ name: "heartbeat-no-refresh", file: "src/state-lock.ts", test: "test/state-lock.test.ts", pattern: "\\(3\\)", change: (s) => replace(s, "holder.record.leaseUntil = clock.now()", "holder.record.leaseUntil ??= clock.now()") },
|
|
15
|
+
{ name: "heartbeat-by-path", file: "src/state-lock.ts", test: "test/state-lock.test.ts", pattern: "\\(2\\)", change: (s) => 'import { writeFileSync } from "node:fs";\n' + replace(s, "writeRecord(holder);\n\t\t\tschedule();", "writeFileSync(lockPath, JSON.stringify(holder.record));\n\t\t\tschedule();") },
|
|
16
|
+
{ name: "rename-over-goals", file: "src/plan-commit.ts", test: logTest, pattern: "B wins write", change: (s) => replace(s, publication, `if (next === 1) { ${publication} } else { writeFileSync(temp, JSON.stringify(record.plan)); renameSync(temp, join(dir, "goals.json")); }`) },
|
|
17
|
+
{ name: "delete-published-record", file: "src/plan-commit.ts", test: logTest, pattern: "B wins write", change: (s) => replace(s, "await materialize(dir);", 'await materialize(dir);\n if (next > 1) rmSync(join(dir, "revisions", `${String(next).padStart(8, "0")}.json`));') },
|
|
18
|
+
{ name: "next-from-directory", file: "src/plan-commit.ts", test: logTest, pattern: "\\(6\\)", change: (s) => replace(s, "const next = (plan.revision ?? 0) + 1;", "const next = Math.max(plan.revision ?? 0, ...readRecords(dir).map((record) => record.revision)) + 1;") },
|
|
19
|
+
{ name: "no-holder-retry", file: "src/plan-io.ts", test: logTest, pattern: "\\(6\\)", change: (s) => replace(s, "if (attempt !== 0) throw error;", "throw error;") },
|
|
20
|
+
{ name: "no-plan-reconciliation", file: "src/plan-log.ts", test: logTest, pattern: "\\(7a\\)", change: (s) => replace(s, "const latest = readNewestRecord(dir);", "const latest = cached === undefined ? readNewestRecord(dir) : undefined;") },
|
|
21
|
+
{ name: "no-ledger-reconciliation", file: "src/ledger.ts", test: logTest, pattern: "\\(7a\\)", change: (s) => replace(s, "for (const record of readRecords(dir))", "for (const record of [])") },
|
|
22
|
+
{ name: "cache-only-existence", file: "src/plan-log.ts", test: recoveryTest, pattern: "\\(7b\\)", change: (s) => replace(s, 'return existsSync(join(dir, "goals.json")) || logNames(dir).length > 0;', 'return existsSync(join(dir, "goals.json"));') },
|
|
23
|
+
{ name: "in-place-ledger-append", file: "src/plan-commit.ts", test: logTest, pattern: "\\(7c\\)", change: (s) => {
|
|
24
|
+
const start = s.indexOf("export async function materialize(dir:");
|
|
25
|
+
if (start < 0) throw new Error("materializer not found");
|
|
26
|
+
const body = replace(s.slice(start), 'const temp = join(dir, "tmp", `${process.pid}-${randomUUID()}-${name}`);', 'const temp = name === "ledger.jsonl" && readRecords(dir).length > 1 ? join(dir, name) : join(dir, "tmp", `${process.pid}-${randomUUID()}-${name}`);');
|
|
27
|
+
return 'import { appendFile } from "node:fs/promises";\n' + s.slice(0, start) + replace(body, 'await writeViewFile(temp, content);', 'if (name === "ledger.jsonl" && readRecords(dir).length > 1) await (hooks.getStore()?.writeView ?? appendFile)(temp, content); else await writeViewFile(temp, content);');
|
|
28
|
+
} },
|
|
29
|
+
{ name: "brief-before-publication", file: "src/plan-crud.ts", test: recoveryTest, pattern: "\\(9\\)", change: (s) => replace(replace(s, 'import { beforePlanMutation, commit }', 'import { beforePlanMutation, commit, writeViewFile }'), "await beforePlanMutation();", 'await beforePlanMutation();\n await writeViewFile(`${repoRoot}/${plan.briefPath}`, plan.brief ?? "");') },
|
|
30
|
+
{ name: "steering-audit-outside-record", file: "src/steering.ts", test: logTest, pattern: "\\(11\\)", change: (s) => replace(s, "await commit(repoRoot, scope, { plan: next, entries });", 'await commit(repoRoot, scope, { plan: next, entries: [] });\n await (await import("node:fs/promises")).appendFile(`${repoRoot}/${next.ledgerPath}`, entries.map((entry) => JSON.stringify(entry)).join("\\n") + "\\n");') },
|
|
31
|
+
{ name: "copy-fallback", file: "src/plan-commit.ts", test: logTest, pattern: "\\(12\\)", change: (s) => 'import { copyFileSync } from "node:fs";\n' + replace(s, publication, `try { ${publication} } catch (error) { if (hasCode(error, "EPERM")) copyFileSync(temp, join(dir, "revisions", \`\${String(next).padStart(8, "0")}.json\`)); else throw error; }`) },
|
|
32
|
+
];
|
|
33
|
+
const digest = (content) => createHash("sha256").update(content).digest("hex");
|
|
34
|
+
let failures = 0;
|
|
35
|
+
for (const mutation of mutations) {
|
|
36
|
+
const original = readFileSync(mutation.file, "utf8");
|
|
37
|
+
console.log(`\n## MUTATION ${mutation.name}\nfile=${mutation.file}\nbefore_sha256=${digest(original)}`);
|
|
38
|
+
try {
|
|
39
|
+
writeFileSync(mutation.file, mutation.change(original));
|
|
40
|
+
const args = ["run", "test", "--", mutation.test, "-t", mutation.pattern];
|
|
41
|
+
console.log(`command=bun ${args.map((arg) => JSON.stringify(arg)).join(" ")}`);
|
|
42
|
+
const result = spawnSync("bun", args, { encoding: "utf8", timeout: 60_000 });
|
|
43
|
+
const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
|
|
44
|
+
console.log(output);
|
|
45
|
+
console.log(`mutation_exit=${result.status}`);
|
|
46
|
+
if (result.status !== 1 || !/[1-9]\d* failed/.test(output.replace(/\x1b\[[0-9;]*m/g, "")) || !output.includes("AssertionError")) {
|
|
47
|
+
failures += 1;
|
|
48
|
+
console.log(`PROOF=FAILED ${mutation.name}`);
|
|
49
|
+
} else console.log(`PROOF=RED ${mutation.name}`);
|
|
50
|
+
} finally {
|
|
51
|
+
writeFileSync(mutation.file, original);
|
|
52
|
+
const restored = readFileSync(mutation.file, "utf8");
|
|
53
|
+
if (restored !== original) throw new Error(`Failed to restore ${mutation.file}`);
|
|
54
|
+
console.log(`restored_sha256=${digest(restored)} cleanup=RESTORED`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
console.log(`mutation_proofs=${mutations.length} failed_proofs=${failures}`);
|
|
58
|
+
process.exitCode = failures === 0 ? 0 : 1;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { writeFile } from "node:fs/promises";
|
|
2
|
+
import { commit, withCommitHooks } from "../../src/plan-commit.js";
|
|
3
|
+
import { createUlwLoopPlan } from "../../src/plan-crud.js";
|
|
4
|
+
import { readUlwLoopPlan, withMutationLockOptions, withUlwLoopMutationLock } from "../../src/plan-io.js";
|
|
5
|
+
import { steerUlwLoop } from "../../src/steering.js";
|
|
6
|
+
import { leaseClock } from "./lease-clock.js";
|
|
7
|
+
|
|
8
|
+
const root = process.argv[2];
|
|
9
|
+
const mode = process.argv[3];
|
|
10
|
+
if (root === undefined) throw new Error("root required");
|
|
11
|
+
const scope = { sessionId: "commit-log" };
|
|
12
|
+
function barrier(name: string): Promise<void> {
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
const timeout = setTimeout(() => reject(new Error(`barrier timed out: ${name}`)), 10_000);
|
|
15
|
+
process.once("message", () => {
|
|
16
|
+
clearTimeout(timeout);
|
|
17
|
+
resolve();
|
|
18
|
+
});
|
|
19
|
+
process.send?.(name);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
let calls = 0;
|
|
23
|
+
await withMutationLockOptions({ clock: leaseClock(Number(process.argv[4])), heartbeatMs: 0, timeoutMs: 0 }, () =>
|
|
24
|
+
withCommitHooks(
|
|
25
|
+
{
|
|
26
|
+
beforeCommit: async () => {
|
|
27
|
+
calls += 1;
|
|
28
|
+
if (mode === "mirror" && calls === 1) await barrier("before");
|
|
29
|
+
},
|
|
30
|
+
afterCommit: async () => {
|
|
31
|
+
if (mode === "accepted" || mode === "rejected") await barrier("linked");
|
|
32
|
+
},
|
|
33
|
+
writeView: async (path, content) => {
|
|
34
|
+
if (mode === "partial" && path.endsWith("ledger.jsonl")) {
|
|
35
|
+
await writeFile(path, content.slice(0, Math.max(1, Math.floor(content.length / 2))));
|
|
36
|
+
await barrier("partial");
|
|
37
|
+
}
|
|
38
|
+
await writeFile(path, content);
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
async () => {
|
|
42
|
+
if (mode === "force") await createUlwLoopPlan(root, { brief: "B\n", force: true }, scope);
|
|
43
|
+
else if (mode === "accepted" || mode === "rejected")
|
|
44
|
+
await steerUlwLoop(
|
|
45
|
+
root,
|
|
46
|
+
{
|
|
47
|
+
kind: "annotate_ledger",
|
|
48
|
+
source: "cli",
|
|
49
|
+
evidence: mode === "rejected" ? "" : "observed",
|
|
50
|
+
rationale: "audit",
|
|
51
|
+
idempotencyKey: "crash-key",
|
|
52
|
+
},
|
|
53
|
+
scope,
|
|
54
|
+
);
|
|
55
|
+
else
|
|
56
|
+
await withUlwLoopMutationLock(root, scope, async () => {
|
|
57
|
+
const plan = await readUlwLoopPlan(root, scope);
|
|
58
|
+
plan.codexObjective = `${plan.codexObjective}|B`;
|
|
59
|
+
await commit(root, scope, { plan, entries: [{ at: "B", kind: "goal_added", message: "B" }] });
|
|
60
|
+
});
|
|
61
|
+
},
|
|
62
|
+
),
|
|
63
|
+
);
|
|
64
|
+
process.send?.("done");
|
|
65
|
+
process.disconnect?.();
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function leaseClock(initial = 0) {
|
|
2
|
+
let now = initial;
|
|
3
|
+
const tasks = new Set<{ at: number; fn: () => void }>();
|
|
4
|
+
return {
|
|
5
|
+
now: () => now,
|
|
6
|
+
schedule(fn: () => void, ms: number) {
|
|
7
|
+
const task = { at: now + ms, fn };
|
|
8
|
+
tasks.add(task);
|
|
9
|
+
return { unref() {}, cancel: () => tasks.delete(task) };
|
|
10
|
+
},
|
|
11
|
+
advance(ms: number) {
|
|
12
|
+
const end = now + ms;
|
|
13
|
+
for (;;) {
|
|
14
|
+
const next = [...tasks].filter((task) => task.at <= end).sort((a, b) => a.at - b.at)[0];
|
|
15
|
+
if (next === undefined) break;
|
|
16
|
+
now = next.at;
|
|
17
|
+
tasks.delete(next);
|
|
18
|
+
next.fn();
|
|
19
|
+
}
|
|
20
|
+
now = end;
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, 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 * as io from "../src/plan-io.js";
|
|
9
|
+
import { childWriter, cleanupWriters } from "./fixtures/commit-child.js";
|
|
10
|
+
import { seedCrash } from "./fixtures/commit-crash.js";
|
|
11
|
+
import { leaseClock } from "./fixtures/lease-clock.js";
|
|
12
|
+
|
|
13
|
+
let root: string;
|
|
14
|
+
let dir: string;
|
|
15
|
+
const scope = { sessionId: "commit-log" };
|
|
16
|
+
beforeEach(async () => {
|
|
17
|
+
root = await mkdtemp(join(tmpdir(), "ulw-commit-"));
|
|
18
|
+
dir = ulwLoopDir(root, scope);
|
|
19
|
+
});
|
|
20
|
+
afterEach(async () => {
|
|
21
|
+
await cleanupWriters();
|
|
22
|
+
await rm(root, { recursive: true, force: true });
|
|
23
|
+
});
|
|
24
|
+
function snapshot() {
|
|
25
|
+
return Object.fromEntries(
|
|
26
|
+
["goals.json", "ledger.jsonl", "brief.md"].map((name) => [
|
|
27
|
+
name,
|
|
28
|
+
existsSync(join(dir, name)) ? readFileSync(join(dir, name), "utf8") : null,
|
|
29
|
+
]),
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("#given an immutable plan and audit log", () => {
|
|
34
|
+
it("#when (7a) a writer dies after publishing #then reads reconcile without writes and locked reads repair views", async () => {
|
|
35
|
+
const next = await seedCrash(root, scope, false);
|
|
36
|
+
const before = snapshot();
|
|
37
|
+
expect(await io.readUlwLoopPlan(root, scope)).toEqual(next);
|
|
38
|
+
expect(io.readLedger(root, scope).at(-1)?.id).toBe(`${next.revision}-0`);
|
|
39
|
+
expect(snapshot()).toEqual(before);
|
|
40
|
+
await io.withUlwLoopMutationLock(root, scope, () => io.readUlwLoopPlan(root, scope));
|
|
41
|
+
expect(JSON.parse(readFileSync(join(dir, "goals.json"), "utf8"))).toEqual(next);
|
|
42
|
+
expect(readFileSync(join(dir, "brief.md"), "utf8")).toBe(next.brief);
|
|
43
|
+
});
|
|
44
|
+
it("#when (7b) first creation has no goals cache #then existence and reads use the log", async () => {
|
|
45
|
+
const next = await seedCrash(root, scope, true);
|
|
46
|
+
expect(io.planExists(root, scope)).toBe(true);
|
|
47
|
+
expect(await io.readUlwLoopPlan(root, scope)).toEqual(next);
|
|
48
|
+
});
|
|
49
|
+
it("#when (8) a legacy plan first commits #then revision zero and legacy ledger and brief survive", async () => {
|
|
50
|
+
const plan = await createUlwLoopPlan(root, { brief: "legacy" }, scope);
|
|
51
|
+
delete plan.revision;
|
|
52
|
+
delete plan.brief;
|
|
53
|
+
delete plan.ledgerResetRevision;
|
|
54
|
+
await rm(join(dir, "revisions"), { recursive: true, force: true });
|
|
55
|
+
writeFileSync(join(dir, "goals.json"), JSON.stringify(plan));
|
|
56
|
+
writeFileSync(join(dir, "ledger.jsonl"), '{"at":"legacy","kind":"goal_started"}\n{"torn":');
|
|
57
|
+
expect(io.readLedger(root, scope)[0]).toMatchObject({ revision: 0, id: "legacy-1" });
|
|
58
|
+
const { commit } = await import("../src/plan-commit.js");
|
|
59
|
+
await io.withUlwLoopMutationLock(root, scope, async () => {
|
|
60
|
+
const legacy = await io.readUlwLoopPlan(root, scope);
|
|
61
|
+
await commit(root, scope, { plan: legacy, entries: [] });
|
|
62
|
+
});
|
|
63
|
+
expect(readdirSync(join(dir, "revisions"))).toEqual(["00000001.json"]);
|
|
64
|
+
expect(io.readLedger(root, scope)[0]?.id).toBe("legacy-1");
|
|
65
|
+
expect(readFileSync(join(dir, "brief.md"), "utf8")).toBe("legacy\n");
|
|
66
|
+
});
|
|
67
|
+
it("#when (9) force recreation succeeds #then the reset is a new revision with filtered views", async () => {
|
|
68
|
+
const first = await createUlwLoopPlan(root, { brief: "old" }, scope);
|
|
69
|
+
const next = await createUlwLoopPlan(root, { brief: "new", force: true }, scope);
|
|
70
|
+
expect(next.revision).toBe((first.revision ?? 0) + 1);
|
|
71
|
+
expect(next.ledgerResetRevision).toBe(next.revision);
|
|
72
|
+
expect(io.readLedger(root, scope)).toHaveLength(1);
|
|
73
|
+
expect(readFileSync(join(dir, "brief.md"), "utf8")).toBe(next.brief);
|
|
74
|
+
expect(readdirSync(join(dir, "revisions"))).toHaveLength(2);
|
|
75
|
+
});
|
|
76
|
+
it("#when (12) hard links are unsupported #then publication fails closed without a record or view changes", async () => {
|
|
77
|
+
await createUlwLoopPlan(root, { brief: "old" }, scope);
|
|
78
|
+
const before = snapshot();
|
|
79
|
+
const { withCommitHooks, commit } = await import("../src/plan-commit.js");
|
|
80
|
+
await expect(
|
|
81
|
+
withCommitHooks(
|
|
82
|
+
{
|
|
83
|
+
link: () => {
|
|
84
|
+
throw Object.assign(new Error("unsupported"), { code: "EPERM" });
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
() =>
|
|
88
|
+
io.withUlwLoopMutationLock(root, scope, async () => {
|
|
89
|
+
const plan = await io.readUlwLoopPlan(root, scope);
|
|
90
|
+
plan.brief = "forbidden";
|
|
91
|
+
await commit(root, scope, { plan, entries: [] });
|
|
92
|
+
}),
|
|
93
|
+
),
|
|
94
|
+
).rejects.toMatchObject({ code: "ULW_LOOP_PUBLISH_UNSUPPORTED_FS" });
|
|
95
|
+
expect(snapshot()).toEqual(before);
|
|
96
|
+
expect(readdirSync(join(dir, "revisions"))).toHaveLength(1);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("#given paused writers in separate processes", () => {
|
|
101
|
+
it.each(["write", "force"])(
|
|
102
|
+
"#when (5)/(9) B wins %s after A's precheck #then A loses without publishing or changing views",
|
|
103
|
+
async (mode) => {
|
|
104
|
+
await createUlwLoopPlan(root, { brief: "initial" }, scope);
|
|
105
|
+
const { withCommitHooks, commit } = await import("../src/plan-commit.js");
|
|
106
|
+
let successor: ReturnType<typeof snapshot> | undefined;
|
|
107
|
+
let writes = 0;
|
|
108
|
+
const clock = leaseClock();
|
|
109
|
+
await expect(
|
|
110
|
+
withCommitHooks(
|
|
111
|
+
{
|
|
112
|
+
beforeCommit: async () => {
|
|
113
|
+
clock.advance(201);
|
|
114
|
+
const writer = childWriter(root, mode, clock.now());
|
|
115
|
+
expect(await writer.exited).toBe(0);
|
|
116
|
+
successor = snapshot();
|
|
117
|
+
},
|
|
118
|
+
writeView: async () => {
|
|
119
|
+
writes += 1;
|
|
120
|
+
throw new Error("loser wrote a view");
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
() =>
|
|
124
|
+
io.withUlwLoopMutationLock(
|
|
125
|
+
root,
|
|
126
|
+
scope,
|
|
127
|
+
async () => {
|
|
128
|
+
const plan = await io.readUlwLoopPlan(root, scope);
|
|
129
|
+
plan.brief = "A\n";
|
|
130
|
+
await commit(root, scope, { plan, entries: [{ at: "A", kind: "goal_added", message: "A" }] });
|
|
131
|
+
},
|
|
132
|
+
{ clock, leaseMs: 200, heartbeatMs: 0 },
|
|
133
|
+
),
|
|
134
|
+
),
|
|
135
|
+
).rejects.toMatchObject({ code: "ULW_LOOP_LOCK_LOST" });
|
|
136
|
+
expect(writes).toBe(0);
|
|
137
|
+
expect(snapshot()).toEqual(successor);
|
|
138
|
+
const published = JSON.parse(readFileSync(join(dir, "revisions", "00000002.json"), "utf8"));
|
|
139
|
+
expect(published.plan).toEqual(await io.readUlwLoopPlan(root, scope));
|
|
140
|
+
expect(published.ledger).toHaveLength(1);
|
|
141
|
+
expect(io.readLedger(root, scope).at(-1)).toEqual(published.ledger[0]);
|
|
142
|
+
expect(published.ledger[0].kind).toBe(mode === "force" ? "plan_created" : "goal_added");
|
|
143
|
+
expect(io.readLedger(root, scope).some((entry) => entry.message === "A")).toBe(false);
|
|
144
|
+
expect(readdirSync(join(dir, "revisions"))).toEqual(["00000001.json", "00000002.json"]);
|
|
145
|
+
},
|
|
146
|
+
);
|
|
147
|
+
it("#when (6) A wins before the valid holder B #then B retries once from A's revision", async () => {
|
|
148
|
+
await createUlwLoopPlan(root, { brief: "initial" }, scope);
|
|
149
|
+
const { withCommitHooks, commit } = await import("../src/plan-commit.js");
|
|
150
|
+
let writer: ReturnType<typeof childWriter> | undefined;
|
|
151
|
+
const clock = leaseClock();
|
|
152
|
+
await withCommitHooks(
|
|
153
|
+
{
|
|
154
|
+
beforeCommit: async () => {
|
|
155
|
+
clock.advance(201);
|
|
156
|
+
writer = childWriter(root, "mirror", clock.now());
|
|
157
|
+
await writer.wait("before");
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
() =>
|
|
161
|
+
io.withUlwLoopMutationLock(
|
|
162
|
+
root,
|
|
163
|
+
scope,
|
|
164
|
+
async () => {
|
|
165
|
+
const plan = await io.readUlwLoopPlan(root, scope);
|
|
166
|
+
plan.codexObjective = "A";
|
|
167
|
+
await commit(root, scope, { plan, entries: [{ at: "A", kind: "goal_added", message: "A" }] });
|
|
168
|
+
},
|
|
169
|
+
{ clock, leaseMs: 200, heartbeatMs: 0 },
|
|
170
|
+
),
|
|
171
|
+
);
|
|
172
|
+
if (writer === undefined) throw new Error("B not started");
|
|
173
|
+
writer.child.send("resume");
|
|
174
|
+
expect(await writer.exited).toBe(0);
|
|
175
|
+
expect((await io.readUlwLoopPlan(root, scope)).codexObjective).toBe("A|B");
|
|
176
|
+
expect(
|
|
177
|
+
io
|
|
178
|
+
.readLedger(root, scope)
|
|
179
|
+
.slice(-2)
|
|
180
|
+
.map((entry) => entry.message),
|
|
181
|
+
).toEqual(["A", "B"]);
|
|
182
|
+
expect(readdirSync(join(dir, "revisions"))).toHaveLength(3);
|
|
183
|
+
});
|
|
184
|
+
it("#when (7c) a child dies after an acknowledged partial temp write #then every public view remains complete", async () => {
|
|
185
|
+
await createUlwLoopPlan(root, { brief: "initial" }, scope);
|
|
186
|
+
const before = snapshot();
|
|
187
|
+
const writer = childWriter(root, "partial");
|
|
188
|
+
await writer.wait("partial");
|
|
189
|
+
writer.child.kill("SIGKILL");
|
|
190
|
+
await writer.exited;
|
|
191
|
+
const after = snapshot();
|
|
192
|
+
const published = JSON.parse(readFileSync(join(dir, "revisions", "00000002.json"), "utf8"));
|
|
193
|
+
expect([before["goals.json"], `${JSON.stringify(published.plan, null, 2)}\n`]).toContain(after["goals.json"]);
|
|
194
|
+
expect([before["brief.md"], published.plan.brief]).toContain(after["brief.md"]);
|
|
195
|
+
expect(after["ledger.jsonl"]).toBe(before["ledger.jsonl"]);
|
|
196
|
+
for (const line of readFileSync(join(dir, "ledger.jsonl"), "utf8").trim().split("\n"))
|
|
197
|
+
expect(() => JSON.parse(line)).not.toThrow();
|
|
198
|
+
expect(io.readLedger(root, scope).at(-1)?.message).toBe("B");
|
|
199
|
+
});
|
|
200
|
+
it.each(["accepted", "rejected"])(
|
|
201
|
+
"#when (11) %s steering dies after link #then its audit survives and accepted retries dedupe",
|
|
202
|
+
async (mode) => {
|
|
203
|
+
const original = await createUlwLoopPlan(root, { brief: "initial" }, scope);
|
|
204
|
+
const writer = childWriter(root, mode);
|
|
205
|
+
await writer.wait("linked");
|
|
206
|
+
writer.child.kill("SIGKILL");
|
|
207
|
+
await writer.exited;
|
|
208
|
+
expect(io.readLedger(root, scope).at(-1)?.kind).toBe(`steering_${mode}`);
|
|
209
|
+
const prior = await io.findAcceptedSteeringLedgerEntry(root, "crash-key", scope);
|
|
210
|
+
if (mode === "rejected") {
|
|
211
|
+
expect(prior).toBeUndefined();
|
|
212
|
+
expect(await io.readUlwLoopPlan(root, scope)).toEqual({ ...original, revision: 2 });
|
|
213
|
+
} else {
|
|
214
|
+
const { steerUlwLoop } = await import("../src/steering.js");
|
|
215
|
+
const result = await steerUlwLoop(
|
|
216
|
+
root,
|
|
217
|
+
{
|
|
218
|
+
kind: "annotate_ledger",
|
|
219
|
+
source: "cli",
|
|
220
|
+
evidence: "observed",
|
|
221
|
+
rationale: "audit",
|
|
222
|
+
idempotencyKey: "crash-key",
|
|
223
|
+
},
|
|
224
|
+
scope,
|
|
225
|
+
);
|
|
226
|
+
expect(result.deduped).toBe(true);
|
|
227
|
+
expect(io.readLedger(root, scope)).toHaveLength(2);
|
|
228
|
+
expect(readdirSync(join(dir, "revisions"))).toHaveLength(2);
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
);
|
|
232
|
+
it("#when (10) retry fails before commit #then no goal_retried audit escapes", async () => {
|
|
233
|
+
const { startNextUlwLoop } = await import("../src/plan-crud.js");
|
|
234
|
+
const plan = await createUlwLoopPlan(root, { brief: "initial" }, scope);
|
|
235
|
+
const { commit, withCommitHooks } = await import("../src/plan-commit.js");
|
|
236
|
+
for (const goal of plan.goals) goal.status = "failed";
|
|
237
|
+
await commit(root, scope, { plan, entries: [] });
|
|
238
|
+
const before = snapshot();
|
|
239
|
+
await expect(
|
|
240
|
+
withCommitHooks(
|
|
241
|
+
{
|
|
242
|
+
beforeCommit: () => {
|
|
243
|
+
throw new Error("stop before commit");
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
() => startNextUlwLoop(root, { retryFailed: true }, scope),
|
|
247
|
+
),
|
|
248
|
+
).rejects.toThrow("stop before commit");
|
|
249
|
+
expect(snapshot()).toEqual(before);
|
|
250
|
+
expect(io.readLedger(root, scope).some((entry) => entry.kind === "goal_retried")).toBe(false);
|
|
251
|
+
});
|
|
252
|
+
});
|