oh-my-opencode 5.0.0-beta.62 → 5.0.0-beta.64
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 +70 -41
- package/dist/cli-node/index.js +70 -41
- 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 +89 -588
- package/dist/oh-my-opencode.schema.json +4 -5
- 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 +18 -11
- 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 +4 -2
- 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-codex-validation.d.ts +7 -5
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-codex-validation.js +7 -2
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.js +7 -4
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +931 -442
- package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-snapshot.d.ts +6 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-snapshot.js +20 -12
- package/packages/omo-codex/plugin/components/ulw-loop/dist/domain-types.d.ts +8 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/driver-objective-ack.d.ts +4 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/driver-objective-ack.js +18 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/evidence-artifacts.d.ts +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/evidence-artifacts.js +37 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/evidence.d.ts +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/evidence.js +24 -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/paths.d.ts +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/paths.js +6 -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 +6 -2
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-crud.js +54 -25
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-goal-factory.d.ts +10 -3
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-goal-factory.js +38 -31
- 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 +11 -8
- package/packages/omo-codex/plugin/components/ulw-loop/dist/sdk/factory.js +28 -12
- package/packages/omo-codex/plugin/components/ulw-loop/dist/sdk/manifest.d.ts +335 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/sdk/manifest.js +109 -10
- package/packages/omo-codex/plugin/components/ulw-loop/dist/sdk/types.d.ts +3 -0
- 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-mutations.d.ts +3 -2
- package/packages/omo-codex/plugin/components/ulw-loop/dist/steering-mutations.js +4 -4
- package/packages/omo-codex/plugin/components/ulw-loop/dist/steering-types.d.ts +9 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/steering.d.ts +3 -2
- package/packages/omo-codex/plugin/components/ulw-loop/dist/steering.js +11 -11
- package/packages/omo-codex/plugin/components/ulw-loop/dist/stop-resume-hook.js +8 -17
- package/packages/omo-codex/plugin/components/ulw-loop/dist/success-criteria-input.d.ts +9 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/success-criteria-input.js +41 -0
- 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-codex-validation.ts +15 -3
- package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +7 -4
- package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-snapshot.ts +26 -14
- package/packages/omo-codex/plugin/components/ulw-loop/src/domain-types.ts +8 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/driver-objective-ack.ts +20 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/evidence-artifacts.ts +45 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/evidence.ts +23 -16
- package/packages/omo-codex/plugin/components/ulw-loop/src/ledger.ts +33 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/paths.ts +7 -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 +67 -54
- package/packages/omo-codex/plugin/components/ulw-loop/src/plan-goal-factory.ts +63 -31
- 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 +11 -7
- package/packages/omo-codex/plugin/components/ulw-loop/src/sdk/factory.ts +47 -12
- package/packages/omo-codex/plugin/components/ulw-loop/src/sdk/manifest.ts +124 -10
- package/packages/omo-codex/plugin/components/ulw-loop/src/sdk/types.ts +3 -0
- 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-mutations.ts +5 -4
- package/packages/omo-codex/plugin/components/ulw-loop/src/steering-types.ts +13 -1
- package/packages/omo-codex/plugin/components/ulw-loop/src/steering.ts +12 -10
- package/packages/omo-codex/plugin/components/ulw-loop/src/stop-resume-hook.ts +8 -16
- package/packages/omo-codex/plugin/components/ulw-loop/src/success-criteria-input.ts +56 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/codex-goal-snapshot.test.ts +37 -1
- 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-driver-states.test.ts +33 -4
- package/packages/omo-codex/plugin/components/ulw-loop/test/sdk-dx.test.ts +219 -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 +6 -4
- 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
|
@@ -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
|
+
});
|
|
@@ -21,12 +21,17 @@ interface Seeded {
|
|
|
21
21
|
|
|
22
22
|
// One fixture shape for every driver state: a real plan whose first goal is ready to close, so the
|
|
23
23
|
// only thing under test is what the driver snapshot does to the checkpoint.
|
|
24
|
-
async function seed(sessionId: string): Promise<Seeded> {
|
|
24
|
+
async function seed(sessionId: string, brief = "- alpha goal\n- beta goal"): Promise<Seeded> {
|
|
25
25
|
const cwd = await mkdtemp(join(tmpdir(), "ulw-driver-"));
|
|
26
26
|
workDirs.push(cwd);
|
|
27
27
|
const toolkit = createAgentToolkit({ cwd, sessionId, surface: "omo-senpi" });
|
|
28
|
-
const created = await toolkit.createGoals({ brief
|
|
28
|
+
const created = await toolkit.createGoals({ brief });
|
|
29
29
|
if (!created.ok) throw new Error("fixture could not create goals");
|
|
30
|
+
return passNextGoal(cwd, sessionId);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function passNextGoal(cwd: string, sessionId: string): Promise<Seeded> {
|
|
34
|
+
const toolkit = createAgentToolkit({ cwd, sessionId, surface: "omo-senpi" });
|
|
30
35
|
const started = await toolkit.completeGoals({});
|
|
31
36
|
if (!started.ok || started.operation !== "complete-goals" || "done" in started.result)
|
|
32
37
|
throw new Error("fixture could not start a goal");
|
|
@@ -46,6 +51,10 @@ async function seed(sessionId: string): Promise<Seeded> {
|
|
|
46
51
|
return { cwd, sessionId, goalId, objective: plan.codexObjective ?? "" };
|
|
47
52
|
}
|
|
48
53
|
|
|
54
|
+
function warningsOf(result: object): readonly string[] {
|
|
55
|
+
return "warnings" in result && Array.isArray(result.warnings) ? result.warnings : [];
|
|
56
|
+
}
|
|
57
|
+
|
|
49
58
|
async function closeWith(seeded: Seeded, snapshot?: Record<string, unknown>) {
|
|
50
59
|
const toolkit = createAgentToolkit({ cwd: seeded.cwd, sessionId: seeded.sessionId, surface: "omo-senpi" });
|
|
51
60
|
return toolkit.checkpoint({
|
|
@@ -108,15 +117,35 @@ describe("SDK driver-goal states", () => {
|
|
|
108
117
|
});
|
|
109
118
|
|
|
110
119
|
describe("#given a driver whose objective differs from the plan", () => {
|
|
111
|
-
it("#when the goal is checkpointed #then the difference is
|
|
120
|
+
it("#when the goal is checkpointed #then the difference is a warning, never a rejection and never a next action", async () => {
|
|
112
121
|
const seeded = await seed("driver-differs");
|
|
113
122
|
|
|
114
123
|
const closed = await closeWith(seeded, { goal: { objective: "a different objective", status: "active" } });
|
|
115
124
|
|
|
116
125
|
expect(closed.ok).toBe(true);
|
|
117
126
|
if (closed.ok) {
|
|
118
|
-
expect(
|
|
127
|
+
expect(warningsOf(closed.result).join(" ")).toContain("driver_objective_differs");
|
|
128
|
+
expect(closed.nextActions.join(" ")).not.toContain("driver_objective_differs");
|
|
129
|
+
expect(closed.nextActions.join(" ")).not.toContain("create_goal");
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("#when a later goal is checkpointed under the same driver #then the difference is acknowledged in the plan and not repeated", async () => {
|
|
134
|
+
const first = await seed("driver-differs-once", "- alpha goal\n- beta goal\n- gamma goal");
|
|
135
|
+
const driver = { goal: { objective: " a different\n objective ", status: "active" } };
|
|
136
|
+
|
|
137
|
+
const firstClose = await closeWith(first, driver);
|
|
138
|
+
const second = await passNextGoal(first.cwd, first.sessionId);
|
|
139
|
+
const secondClose = await closeWith(second, driver);
|
|
140
|
+
|
|
141
|
+
expect(firstClose.ok && secondClose.ok).toBe(true);
|
|
142
|
+
if (firstClose.ok && secondClose.ok) {
|
|
143
|
+
expect(warningsOf(firstClose.result).join(" ")).toContain("driver_objective_differs");
|
|
144
|
+
expect(warningsOf(secondClose.result)).toEqual([]);
|
|
145
|
+
expect(secondClose.nextActions).toEqual([]);
|
|
119
146
|
}
|
|
147
|
+
const plan = await readUlwLoopPlan(first.cwd, { sessionId: first.sessionId });
|
|
148
|
+
expect(plan.acknowledgedDriverObjectives).toEqual(["a different objective"]);
|
|
120
149
|
});
|
|
121
150
|
});
|
|
122
151
|
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } 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
|
+
|
|
6
|
+
import { readUlwLoopPlan } from "../src/plan-io.js";
|
|
7
|
+
import { createAgentToolkit, ULW_LOOP_MANIFEST, type ULW_LOOP_OPERATIONS } from "../src/sdk.js";
|
|
8
|
+
|
|
9
|
+
const workDirs: string[] = [];
|
|
10
|
+
afterEach(async () => {
|
|
11
|
+
for (const dir of workDirs.splice(0)) await rm(dir, { recursive: true, force: true });
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
async function fixture(sessionId: string, surface: "omo-senpi" | "lazycodex" = "omo-senpi") {
|
|
15
|
+
const cwd = await mkdtemp(join(tmpdir(), "ulw-dx-"));
|
|
16
|
+
workDirs.push(cwd);
|
|
17
|
+
const toolkit = createAgentToolkit({ cwd, sessionId, surface });
|
|
18
|
+
const created = await toolkit.createGoals({ brief: "- alpha goal\n- beta goal" });
|
|
19
|
+
if (!created.ok) throw new Error("fixture could not create goals");
|
|
20
|
+
return { cwd, toolkit };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("help() is self-describing", () => {
|
|
24
|
+
it("#given the manifest #when read #then every operation carries its camelCase method, a description, and an args map", () => {
|
|
25
|
+
const methods: Record<(typeof ULW_LOOP_OPERATIONS)[number], string> = {
|
|
26
|
+
help: "help",
|
|
27
|
+
"create-goals": "createGoals",
|
|
28
|
+
status: "status",
|
|
29
|
+
"complete-goals": "completeGoals",
|
|
30
|
+
checkpoint: "checkpoint",
|
|
31
|
+
steer: "steer",
|
|
32
|
+
"add-goal": "addGoal",
|
|
33
|
+
criteria: "criteria",
|
|
34
|
+
"record-evidence": "recordEvidence",
|
|
35
|
+
"record-review-blockers": "recordReviewBlockers",
|
|
36
|
+
};
|
|
37
|
+
for (const operation of ULW_LOOP_MANIFEST.operations) {
|
|
38
|
+
expect(operation.method).toBe(methods[operation.name]);
|
|
39
|
+
expect(operation.description.length).toBeGreaterThan(20);
|
|
40
|
+
expect(typeof operation.args).toBe("object");
|
|
41
|
+
}
|
|
42
|
+
const record = ULW_LOOP_MANIFEST.operations.find((operation) => operation.name === "record-evidence");
|
|
43
|
+
expect(record?.args).toMatchObject({ goalId: "string", criterionId: "string", evidence: "string" });
|
|
44
|
+
expect(Object.keys(record?.args ?? {})).toEqual(expect.arrayContaining(["status", "notes", "artifacts"]));
|
|
45
|
+
const add = ULW_LOOP_MANIFEST.operations.find((operation) => operation.name === "add-goal");
|
|
46
|
+
expect(Object.keys(add?.args ?? {})).toEqual(expect.arrayContaining(["title", "objective", "successCriteria"]));
|
|
47
|
+
const steer = ULW_LOOP_MANIFEST.operations.find((operation) => operation.name === "steer");
|
|
48
|
+
expect(Object.keys(steer?.args ?? {})).toEqual(
|
|
49
|
+
expect.arrayContaining([
|
|
50
|
+
"kind",
|
|
51
|
+
"source",
|
|
52
|
+
"evidence",
|
|
53
|
+
"rationale",
|
|
54
|
+
"goalId",
|
|
55
|
+
"criterionId",
|
|
56
|
+
"scenario",
|
|
57
|
+
"expectedEvidence",
|
|
58
|
+
]),
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("#given a toolkit #when help() runs #then the result is the manifest", async () => {
|
|
63
|
+
const { toolkit } = await fixture("dx-help");
|
|
64
|
+
const help = await toolkit.help();
|
|
65
|
+
expect(help.ok).toBe(true);
|
|
66
|
+
if (help.ok) expect(help.result.operations.map((operation) => operation.method)).toContain("recordEvidence");
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("addGoal accepts success criteria", () => {
|
|
71
|
+
it("#given explicit successCriteria #when the goal is added #then exactly those criteria persist with sequential ids", async () => {
|
|
72
|
+
const { cwd, toolkit } = await fixture("dx-add-criteria");
|
|
73
|
+
const added = await toolkit.addGoal({
|
|
74
|
+
title: "lane",
|
|
75
|
+
objective: "ship the lane",
|
|
76
|
+
successCriteria: [
|
|
77
|
+
{ scenario: "curl -i /health returns 200", expectedEvidence: "status line captured in health.log" },
|
|
78
|
+
{
|
|
79
|
+
scenario: "malformed body returns 400",
|
|
80
|
+
expectedEvidence: "curl transcript",
|
|
81
|
+
userModel: "edge",
|
|
82
|
+
essential: false,
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
});
|
|
86
|
+
expect(added.ok).toBe(true);
|
|
87
|
+
if (!added.ok) return;
|
|
88
|
+
expect(added.result.goal.successCriteria).toEqual([
|
|
89
|
+
expect.objectContaining({
|
|
90
|
+
id: "C001",
|
|
91
|
+
scenario: "curl -i /health returns 200",
|
|
92
|
+
expectedEvidence: "status line captured in health.log",
|
|
93
|
+
userModel: "happy",
|
|
94
|
+
essential: true,
|
|
95
|
+
status: "pending",
|
|
96
|
+
}),
|
|
97
|
+
expect.objectContaining({
|
|
98
|
+
id: "C002",
|
|
99
|
+
scenario: "malformed body returns 400",
|
|
100
|
+
userModel: "edge",
|
|
101
|
+
essential: false,
|
|
102
|
+
status: "pending",
|
|
103
|
+
}),
|
|
104
|
+
]);
|
|
105
|
+
const plan = await readUlwLoopPlan(cwd, { sessionId: "dx-add-criteria" });
|
|
106
|
+
expect(plan.goals.at(-1)?.successCriteria).toHaveLength(2);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("#given invalid successCriteria #when the goal is added #then the call fails closed with ULW_LOOP_ARGUMENT_INVALID and nothing persists", async () => {
|
|
110
|
+
const { cwd, toolkit } = await fixture("dx-add-invalid");
|
|
111
|
+
const attempts = [
|
|
112
|
+
{ successCriteria: [] },
|
|
113
|
+
{ successCriteria: [{ scenario: " ", expectedEvidence: "x" }] },
|
|
114
|
+
{ successCriteria: [{ scenario: "s", expectedEvidence: "" }] },
|
|
115
|
+
{ successCriteria: [{ scenario: "s", expectedEvidence: "e", userModel: "vibes" }] },
|
|
116
|
+
];
|
|
117
|
+
for (const attempt of attempts) {
|
|
118
|
+
const added = await toolkit.addGoal({
|
|
119
|
+
title: "lane",
|
|
120
|
+
objective: "ship the lane",
|
|
121
|
+
...(attempt as { successCriteria: never }),
|
|
122
|
+
});
|
|
123
|
+
expect(added.ok).toBe(false);
|
|
124
|
+
if (!added.ok) expect(added.error.code).toBe("ULW_LOOP_ARGUMENT_INVALID");
|
|
125
|
+
}
|
|
126
|
+
const plan = await readUlwLoopPlan(cwd, { sessionId: "dx-add-invalid" });
|
|
127
|
+
expect(plan.goals).toHaveLength(2);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("#given no successCriteria #when the goal is added #then the placeholders name the exact surface call", async () => {
|
|
131
|
+
const senpi = await fixture("dx-add-placeholder", "omo-senpi");
|
|
132
|
+
const added = await senpi.toolkit.addGoal({ title: "lane", objective: "ship the lane" });
|
|
133
|
+
expect(added.ok).toBe(true);
|
|
134
|
+
if (!added.ok) return;
|
|
135
|
+
const placeholder = added.result.goal.successCriteria[0]?.expectedEvidence ?? "";
|
|
136
|
+
expect(placeholder).toContain('agentToolkit.steer({ kind: "revise_criterion"');
|
|
137
|
+
expect(placeholder).toContain(`goalId: "${added.result.goal.id}"`);
|
|
138
|
+
expect(placeholder).toContain('criterionId: "C001"');
|
|
139
|
+
expect(placeholder).toContain("successCriteria");
|
|
140
|
+
expect(placeholder).not.toContain("Replace via revise_criterion with");
|
|
141
|
+
|
|
142
|
+
const codex = await fixture("dx-add-placeholder-cli", "lazycodex");
|
|
143
|
+
const cliAdded = await codex.toolkit.addGoal({ title: "lane", objective: "ship the lane" });
|
|
144
|
+
expect(cliAdded.ok).toBe(true);
|
|
145
|
+
if (cliAdded.ok) {
|
|
146
|
+
expect(cliAdded.result.goal.successCriteria[0]?.expectedEvidence).toContain(
|
|
147
|
+
`omo-agent-toolkit ulw-loop steer --kind revise_criterion --goal-id ${cliAdded.result.goal.id} --criterion-id C001`,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe("recordEvidence binds artifacts", () => {
|
|
154
|
+
it("#given existing artifact paths #when evidence is recorded #then repo-relative paths persist on the criterion and the ledger", async () => {
|
|
155
|
+
const { cwd, toolkit } = await fixture("dx-artifacts");
|
|
156
|
+
await mkdir(join(cwd, "evidence"), { recursive: true });
|
|
157
|
+
await writeFile(join(cwd, "evidence", "gate.log"), "exit 0\n");
|
|
158
|
+
const outside = join(await mkdtemp(join(tmpdir(), "ulw-dx-outside-")), "shot.png");
|
|
159
|
+
workDirs.push(join(outside, ".."));
|
|
160
|
+
await writeFile(outside, "png");
|
|
161
|
+
const started = await toolkit.completeGoals({});
|
|
162
|
+
if (!started.ok || started.operation !== "complete-goals" || "done" in started.result) throw new Error("fixture");
|
|
163
|
+
const goalId = started.result.goal.id;
|
|
164
|
+
const recorded = await toolkit.recordEvidence({
|
|
165
|
+
goalId,
|
|
166
|
+
criterionId: "C001",
|
|
167
|
+
status: "pass",
|
|
168
|
+
evidence: "gate green",
|
|
169
|
+
artifacts: ["evidence/gate.log", join(cwd, "evidence", "gate.log"), outside],
|
|
170
|
+
});
|
|
171
|
+
expect(recorded.ok).toBe(true);
|
|
172
|
+
if (!recorded.ok) return;
|
|
173
|
+
expect(recorded.result.criterion.artifacts).toEqual(["evidence/gate.log", outside]);
|
|
174
|
+
expect(recorded.result.ledgerEntry.artifacts).toEqual(["evidence/gate.log", outside]);
|
|
175
|
+
const ledger = (await readFile(join(cwd, ".omo", "ulw-loop", "dx-artifacts", "ledger.jsonl"), "utf8"))
|
|
176
|
+
.trim()
|
|
177
|
+
.split("\n");
|
|
178
|
+
expect(ledger.at(-1)).toContain('"artifacts":["evidence/gate.log"');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("#given a missing artifact #when evidence is recorded #then it fails with ULW_LOOP_EVIDENCE_ARTIFACT_MISSING naming the path and records nothing", async () => {
|
|
182
|
+
const { cwd, toolkit } = await fixture("dx-artifact-missing");
|
|
183
|
+
const started = await toolkit.completeGoals({});
|
|
184
|
+
if (!started.ok || started.operation !== "complete-goals" || "done" in started.result) throw new Error("fixture");
|
|
185
|
+
const goalId = started.result.goal.id;
|
|
186
|
+
const recorded = await toolkit.recordEvidence({
|
|
187
|
+
goalId,
|
|
188
|
+
criterionId: "C001",
|
|
189
|
+
status: "pass",
|
|
190
|
+
evidence: "gate green",
|
|
191
|
+
artifacts: ["evidence/does-not-exist.log"],
|
|
192
|
+
});
|
|
193
|
+
expect(recorded.ok).toBe(false);
|
|
194
|
+
if (!recorded.ok) {
|
|
195
|
+
expect(recorded.error.code).toBe("ULW_LOOP_EVIDENCE_ARTIFACT_MISSING");
|
|
196
|
+
expect(recorded.error.message).toContain("evidence/does-not-exist.log");
|
|
197
|
+
}
|
|
198
|
+
const plan = await readUlwLoopPlan(cwd, { sessionId: "dx-artifact-missing" });
|
|
199
|
+
expect(plan.goals[0]?.successCriteria[0]?.status).toBe("pending");
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe("status() exposes a stable evidence root", () => {
|
|
204
|
+
it("#given a session-scoped plan #when status runs #then evidenceRoot is the plan-level directory beside the moving attempt dir", async () => {
|
|
205
|
+
const { toolkit } = await fixture("dx-evidence-root");
|
|
206
|
+
const before = await toolkit.status();
|
|
207
|
+
expect(before.ok).toBe(true);
|
|
208
|
+
if (before.ok) expect(before.result.evidenceRoot).toBe(".omo/evidence/ulw/dx-evidence-root");
|
|
209
|
+
await toolkit.completeGoals({});
|
|
210
|
+
const after = await toolkit.status();
|
|
211
|
+
expect(after.ok).toBe(true);
|
|
212
|
+
if (after.ok) {
|
|
213
|
+
expect(after.result.evidenceRoot).toBe(".omo/evidence/ulw/dx-evidence-root");
|
|
214
|
+
expect(after.result.currentAttemptDir).toMatch(
|
|
215
|
+
/^\.omo\/evidence\/ulw\/dx-evidence-root\/G001-alpha-goal\/a\d+$/,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
});
|