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
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { isAbsolute, join, resolve, sep } from "node:path";
|
|
3
3
|
|
|
4
|
+
import { readLedgerAt } from "./ledger.js";
|
|
4
5
|
import { normalizeUlwLoopSessionId, ulwLoopDir, ulwLoopStateLockPath } from "./paths.js";
|
|
6
|
+
import { readUlwLoopPlanSync } from "./plan-io.js";
|
|
5
7
|
import { isStateLockTimeout, withStateLockSync } from "./state-lock.js";
|
|
6
8
|
import type { UlwLoopItem, UlwLoopPlan } from "./types.js";
|
|
7
9
|
|
|
@@ -37,7 +39,7 @@ export function runStopResumeHook(input: unknown): string {
|
|
|
37
39
|
if (boulderContinuationWillFire(payload.cwd, payload.session_id)) return "";
|
|
38
40
|
const scope = { sessionId: payload.session_id } as const;
|
|
39
41
|
const stateDir = ulwLoopDir(payload.cwd, scope);
|
|
40
|
-
const plan = readPlan(
|
|
42
|
+
const plan = readPlan(payload.cwd, payload.session_id);
|
|
41
43
|
if (plan === null || plan.aggregateCompletion?.status === "complete") return "";
|
|
42
44
|
const goal = resumableGoal(plan);
|
|
43
45
|
if (goal === undefined) return "";
|
|
@@ -81,11 +83,10 @@ function consumeResumeBudgetLocked(lockPath: string, stateDir: string, goalId: s
|
|
|
81
83
|
}
|
|
82
84
|
}
|
|
83
85
|
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
// a separate file — a ledger append would change the count and self-reset.
|
|
86
|
+
// Hook budget counters are exempt from plan/audit commits. Count reconciled
|
|
87
|
+
// entries, not cache lines: a writer dying after link still made progress.
|
|
87
88
|
function consumeResumeBudget(stateDir: string, goalId: string): boolean {
|
|
88
|
-
const ledgerLineCount =
|
|
89
|
+
const ledgerLineCount = readLedgerAt(stateDir).length;
|
|
89
90
|
const counterPath = resolve(stateDir, `auto-resume-${goalId}.json`);
|
|
90
91
|
const stuckPath = resolve(stateDir, `auto-resume-${goalId}.stuck`);
|
|
91
92
|
// goals.json is untrusted input: a crafted goal id (e.g. `../../x`) must
|
|
@@ -119,24 +120,15 @@ function renderResumeDirective(plan: UlwLoopPlan, goal: UlwLoopItem, sessionId:
|
|
|
119
120
|
].join("\n");
|
|
120
121
|
}
|
|
121
122
|
|
|
122
|
-
function readPlan(
|
|
123
|
+
function readPlan(repoRoot: string, sessionId: string): UlwLoopPlan | null {
|
|
123
124
|
try {
|
|
124
|
-
return
|
|
125
|
+
return readUlwLoopPlanSync(repoRoot, { sessionId });
|
|
125
126
|
} catch (error) {
|
|
126
127
|
if (error instanceof Error) return null;
|
|
127
128
|
throw error;
|
|
128
129
|
}
|
|
129
130
|
}
|
|
130
131
|
|
|
131
|
-
function countLedgerLines(ledgerPath: string): number {
|
|
132
|
-
try {
|
|
133
|
-
return readFileSync(ledgerPath, "utf8").split("\n").filter(Boolean).length;
|
|
134
|
-
} catch (error) {
|
|
135
|
-
if (error instanceof Error) return 0;
|
|
136
|
-
throw error;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
132
|
function readCounter(counterPath: string): { count: number; ledgerLineCount: number } | null {
|
|
141
133
|
try {
|
|
142
134
|
if (!existsSync(counterPath)) return null;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { UlwLoopSuccessCriterion, UlwLoopSuccessCriterionUserModel } from "./types.js";
|
|
2
|
+
import { ULW_LOOP_SUCCESS_CRITERION_USER_MODELS, UlwLoopError } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export interface SuccessCriterionInput {
|
|
5
|
+
readonly scenario: string;
|
|
6
|
+
readonly expectedEvidence: string;
|
|
7
|
+
readonly userModel?: UlwLoopSuccessCriterionUserModel;
|
|
8
|
+
readonly essential?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function invalid(message: string, details: Record<string, unknown>): never {
|
|
12
|
+
throw new UlwLoopError(`Invalid successCriteria: ${message}`, "ULW_LOOP_ARGUMENT_INVALID", { details });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isUserModel(value: unknown): value is UlwLoopSuccessCriterionUserModel {
|
|
16
|
+
return typeof value === "string" && ULW_LOOP_SUCCESS_CRITERION_USER_MODELS.some((model) => model === value);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function requireText(value: unknown, field: string, index: number): string {
|
|
20
|
+
const trimmed = typeof value === "string" ? value.trim() : "";
|
|
21
|
+
return trimmed || invalid(`entry ${index + 1} needs a non-empty ${field}.`, { index, field });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function criterionId(index: number): string {
|
|
25
|
+
return `C${String(index + 1).padStart(3, "0")}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function criteriaFromInput(input: readonly unknown[]): UlwLoopSuccessCriterion[] {
|
|
29
|
+
if (input.length === 0) invalid("provide at least one criterion or omit the field for placeholders.", { count: 0 });
|
|
30
|
+
return input.map((entry, index) => {
|
|
31
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry))
|
|
32
|
+
return invalid(`entry ${index + 1} must be an object.`, { index });
|
|
33
|
+
const record: Record<string, unknown> = { ...entry };
|
|
34
|
+
const userModel = record["userModel"] ?? "happy";
|
|
35
|
+
if (!isUserModel(userModel))
|
|
36
|
+
return invalid(
|
|
37
|
+
`entry ${index + 1} userModel must be one of ${ULW_LOOP_SUCCESS_CRITERION_USER_MODELS.join(", ")}.`,
|
|
38
|
+
{
|
|
39
|
+
index,
|
|
40
|
+
userModel: String(userModel),
|
|
41
|
+
},
|
|
42
|
+
);
|
|
43
|
+
const essential = record["essential"];
|
|
44
|
+
if (essential !== undefined && typeof essential !== "boolean")
|
|
45
|
+
return invalid(`entry ${index + 1} essential must be a boolean.`, { index });
|
|
46
|
+
return {
|
|
47
|
+
id: criterionId(index),
|
|
48
|
+
scenario: requireText(record["scenario"], "scenario", index),
|
|
49
|
+
userModel,
|
|
50
|
+
expectedEvidence: requireText(record["expectedEvidence"], "expectedEvidence", index),
|
|
51
|
+
essential: essential ?? true,
|
|
52
|
+
capturedEvidence: null,
|
|
53
|
+
status: "pending",
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
}
|
|
@@ -152,6 +152,41 @@ describe("reconcileCodexGoalSnapshot", () => {
|
|
|
152
152
|
expect(reconciliation.ok).toBe(true);
|
|
153
153
|
expect(reconciliation.errors).toHaveLength(0);
|
|
154
154
|
expect(reconciliation.warnings.join(" ")).toContain("driver_objective_differs");
|
|
155
|
+
expect(reconciliation.nextActions).toEqual([]);
|
|
156
|
+
expect(reconciliation.unacknowledgedObjective).toBe("X");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("#given the differing objective was acknowledged #when reconciled again #then it is silent", () => {
|
|
160
|
+
const reconciliation = reconcileCodexGoalSnapshot(
|
|
161
|
+
{ available: true, objective: " X \n", status: "active", raw: null },
|
|
162
|
+
{ expectedObjective: "Y", acknowledgedObjectives: ["X"] },
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
expect(reconciliation.warnings).toEqual([]);
|
|
166
|
+
expect(reconciliation.nextActions).toEqual([]);
|
|
167
|
+
expect(reconciliation.unacknowledgedObjective).toBeUndefined();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("#given no driver snapshot #when reconciled #then create_goal is a next action, not a warning", () => {
|
|
171
|
+
const reconciliation = reconcileCodexGoalSnapshot(null, { expectedObjective: "Y" });
|
|
172
|
+
|
|
173
|
+
expect(reconciliation.nextActions.join(" ")).toContain("create_goal");
|
|
174
|
+
expect(reconciliation.warnings).toEqual([]);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("#given a completed or limited driver #when reconciled #then the advice is a next action", () => {
|
|
178
|
+
const completed = reconcileCodexGoalSnapshot(
|
|
179
|
+
{ available: true, objective: "Y", status: "complete", raw: null },
|
|
180
|
+
{ expectedObjective: "Y" },
|
|
181
|
+
);
|
|
182
|
+
const limited = reconcileCodexGoalSnapshot(
|
|
183
|
+
{ available: true, objective: "Y", status: "usage_limited", raw: null },
|
|
184
|
+
{ expectedObjective: "Y" },
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
expect(completed.nextActions.join(" ")).toContain("create_goal");
|
|
188
|
+
expect(limited.nextActions.join(" ")).toContain("/goal resume");
|
|
189
|
+
expect([...completed.warnings, ...limited.warnings]).toEqual([]);
|
|
155
190
|
});
|
|
156
191
|
|
|
157
192
|
it("accepts a parsable snapshot without objective as advisory", () => {
|
|
@@ -176,7 +211,8 @@ describe("reconcileCodexGoalSnapshot", () => {
|
|
|
176
211
|
// then
|
|
177
212
|
expect(reconciliation.ok).toBe(true);
|
|
178
213
|
expect(reconciliation.errors).toHaveLength(0);
|
|
179
|
-
expect(reconciliation.
|
|
214
|
+
expect(reconciliation.nextActions.join(" ")).toContain("/goal resume");
|
|
215
|
+
expect(reconciliation.warnings).toEqual([]);
|
|
180
216
|
});
|
|
181
217
|
});
|
|
182
218
|
|
|
@@ -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
|
+
}
|