oh-my-opencode 5.0.0-beta.60 → 5.0.0-beta.63
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/THIRD-PARTY-NOTICES.md +8 -6
- package/dist/cli/doctor/checks/browser-provider.d.ts +2 -0
- package/dist/cli/index.js +465 -348
- package/dist/cli-node/index.js +465 -348
- package/dist/config/schema/agent-names.d.ts +0 -1
- package/dist/config/schema/browser-automation.d.ts +0 -2
- package/dist/config/schema/oh-my-opencode-config.d.ts +0 -1
- package/dist/hooks/comment-checker/__fixtures__/download-probe.fixture.d.ts +1 -0
- package/dist/index.js +61 -535
- package/dist/oh-my-opencode.schema.json +0 -1
- package/dist/skills/ast-grep/SKILL.md +12 -0
- package/dist/skills/debugging/references/tools/playwright-cli.md +79 -161
- package/dist/skills/frontend/SKILL.md +1 -1
- package/dist/skills/frontend/references/design/aside.md +1 -1
- package/dist/skills/frontend/references/design/clone-from-url.md +1 -1
- package/dist/skills/frontend/scripts/perfection/lighthouse-audit.py +2 -2
- package/dist/skills/review-work/SKILL.md +1 -1
- package/dist/skills/ultimate-browsing/ATTRIBUTION.md +1 -82
- package/dist/skills/ultimate-browsing/SKILL.md +17 -29
- package/dist/skills/ultimate-browsing/engine/AGENTS.md +2 -1
- package/dist/skills/ultimate-browsing/engine/templates/package.json +2 -2
- package/dist/skills/ultimate-browsing/engine/templates/playwright_mobile_chrome.js +5 -2
- package/dist/skills/ultimate-browsing/engine/templates/playwright_real_chrome.js +8 -6
- package/dist/skills/ultimate-browsing/engine/tests/test_playwright_stealth.py +86 -0
- package/dist/skills/ultimate-browsing/engine/tests/test_playwright_templates.py +4 -7
- package/dist/skills/ultimate-browsing/references/chrome-stealth.md +86 -100
- package/dist/skills/ultimate-browsing/references/insane-search/README.md +9 -3
- package/dist/skills/ultimate-browsing/references/insane-search/playwright.md +16 -13
- package/dist/skills/ultimate-browsing/scripts/extract_cookies.py +3 -3
- package/dist/skills/ulw-execute/SKILL.md +1 -1
- package/dist/skills/ulw-plan/scripts/scaffold-plan.mjs +1 -1
- package/dist/skills/ulw-research/SKILL.md +3 -3
- package/dist/skills/visual-qa/AGENTS.md +1 -1
- package/dist/skills/visual-qa/SKILL.md +1 -1
- package/dist/skills/visual-qa/references/browser-setup.md +75 -0
- package/dist/tui.js +12 -5
- package/package.json +16 -23
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +6887 -249
- 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/bootstrap/src/setup.ts +7 -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 +25 -15
- 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 +15 -0
- package/packages/omo-codex/plugin/components/ulw-loop/directive.md +25 -15
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.js +5 -4
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +640 -327
- package/packages/omo-codex/plugin/components/ulw-loop/dist/domain-types.d.ts +5 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/evidence.js +16 -12
- package/packages/omo-codex/plugin/components/ulw-loop/dist/ledger.d.ts +4 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/ledger.js +34 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-commit.d.ts +19 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-commit.js +107 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-crud.d.ts +2 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-crud.js +48 -22
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-io.d.ts +9 -7
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-io.js +105 -114
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-log.d.ts +15 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-log.js +86 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-missing-recovery.d.ts +5 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-missing-recovery.js +15 -3
- package/packages/omo-codex/plugin/components/ulw-loop/dist/review-blockers.js +3 -4
- package/packages/omo-codex/plugin/components/ulw-loop/dist/sdk/factory.js +29 -6
- 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.d.ts +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-guard.js +29 -66
- package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-role-guard.d.ts +2 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-role-guard.js +22 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/state-lock.d.ts +11 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/state-lock.js +120 -88
- package/packages/omo-codex/plugin/components/ulw-loop/dist/status-next-actions.d.ts +3 -2
- package/packages/omo-codex/plugin/components/ulw-loop/dist/status-next-actions.js +18 -10
- package/packages/omo-codex/plugin/components/ulw-loop/dist/steering-batch.js +4 -4
- package/packages/omo-codex/plugin/components/ulw-loop/dist/steering.js +5 -5
- package/packages/omo-codex/plugin/components/ulw-loop/dist/stop-resume-hook.js +8 -17
- package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +7 -7
- package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +2 -2
- package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +5 -4
- package/packages/omo-codex/plugin/components/ulw-loop/src/domain-types.ts +5 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/evidence.ts +16 -16
- package/packages/omo-codex/plugin/components/ulw-loop/src/ledger.ts +33 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/plan-commit.ts +124 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/plan-crud.ts +58 -51
- package/packages/omo-codex/plugin/components/ulw-loop/src/plan-io.ts +126 -140
- package/packages/omo-codex/plugin/components/ulw-loop/src/plan-log.ts +84 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/plan-missing-recovery.ts +26 -3
- package/packages/omo-codex/plugin/components/ulw-loop/src/review-blockers.ts +3 -3
- package/packages/omo-codex/plugin/components/ulw-loop/src/sdk/factory.ts +55 -6
- 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 +29 -62
- package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-role-guard.ts +22 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/state-lock.ts +129 -111
- package/packages/omo-codex/plugin/components/ulw-loop/src/status-next-actions.ts +19 -10
- package/packages/omo-codex/plugin/components/ulw-loop/src/steering-batch.ts +4 -4
- package/packages/omo-codex/plugin/components/ulw-loop/src/steering.ts +5 -4
- package/packages/omo-codex/plugin/components/ulw-loop/src/stop-resume-hook.ts +8 -16
- package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/barrier.ts +13 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-child.ts +65 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-cli-qa.mjs +76 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-crash.ts +23 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-mutations.mjs +58 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-writer.ts +65 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/lease-clock.ts +23 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/plan-commit-log.test.ts +252 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/plan-commit-recovery.test.ts +190 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/plan-io.test.ts +50 -35
- package/packages/omo-codex/plugin/components/ulw-loop/test/plan-log-newest.test.ts +72 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/sdk-completed-plan.test.ts +43 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/sdk-driver-states.test.ts +145 -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/spawn-guard.test.ts +7 -1
- package/packages/omo-codex/plugin/components/ulw-loop/test/spawn-role-matrix.test.ts +113 -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 +18 -10
- 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/init-deep/SKILL.md +3 -3
- package/packages/omo-codex/plugin/skills/refactor/SKILL.md +3 -3
- package/packages/omo-codex/plugin/skills/remove-ai-slops/SKILL.md +3 -3
- package/packages/omo-codex/plugin/skills/review-work/SKILL.md +4 -4
- 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 +25 -15
- package/packages/omo-codex/plugin/skills/ulw-execute/SKILL.md +4 -4
- package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +3 -3
- package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +2 -2
- package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +6 -6
- package/packages/omo-codex/plugin/skills/visual-qa/AGENTS.md +1 -1
- package/packages/omo-codex/plugin/skills/visual-qa/SKILL.md +4 -4
- package/packages/omo-codex/plugin/skills/visual-qa/references/browser-setup.md +75 -0
- package/packages/omo-codex/plugin/test/bootstrap-default-role.test.mjs +66 -0
- package/packages/omo-codex/plugin/test/component-hook-contract-cases.mjs +13 -1
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +7060 -417
- package/packages/prompts-core/prompts/ultrawork/codex.md +25 -15
- 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,36 +1,45 @@
|
|
|
1
1
|
import { hasAllCriteriaPass, isFinalRunCompletionCandidate } from "./goal-status.js";
|
|
2
|
-
import {
|
|
2
|
+
import { createGoalsAction } from "./plan-missing-recovery.js";
|
|
3
|
+
import type { UlwLoopToolkitSurface } from "./surface.js";
|
|
3
4
|
import type { UlwLoopItem, UlwLoopPlan } from "./types.js";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
|
-
*
|
|
7
|
+
* Status is the one call every agent already makes between steps, so it is
|
|
7
8
|
* also the cheapest place to answer "what now?" without a second round trip.
|
|
8
9
|
*/
|
|
9
|
-
export function statusNextActions(plan: UlwLoopPlan): readonly string[] {
|
|
10
|
+
export function statusNextActions(plan: UlwLoopPlan, surface: UlwLoopToolkitSurface = "lazycodex"): readonly string[] {
|
|
10
11
|
const actions: string[] = [];
|
|
11
12
|
const active = plan.goals.find((goal) => goal.id === plan.activeGoalId);
|
|
12
|
-
if (plan.goals.length === 0) actions.push(`No goals yet: bootstrap with \`${
|
|
13
|
-
else if (active !== undefined) actions.push(...activeGoalActions(plan, active));
|
|
13
|
+
if (plan.goals.length === 0) actions.push(`No goals yet: bootstrap with \`${createGoalsAction(surface)}\`.`);
|
|
14
|
+
else if (active !== undefined) actions.push(...activeGoalActions(plan, active, surface));
|
|
14
15
|
for (const goal of plan.goals.filter((candidate) => candidate.status === "review_blocked"))
|
|
15
16
|
actions.push(
|
|
16
|
-
|
|
17
|
+
surface === "omo-senpi"
|
|
18
|
+
? `${goal.id} is review_blocked: capture the reviewer verdict with \`agentToolkit.recordReviewBlockers({ goalId: "${goal.id}", title: "<title>", objective: "<objective>", evidence: "<verdict>" })\`.`
|
|
19
|
+
: `${goal.id} is review_blocked: capture the reviewer verdict with \`omo-agent-toolkit ulw-loop record-review-blockers --goal-id ${goal.id} --title "<title>" --objective "<objective>" --evidence "<verdict>" --codex-goal-json '<get_goal json>'\`.`,
|
|
17
20
|
);
|
|
18
21
|
if (plan.evidenceLayoutVersion !== 2 || active === undefined)
|
|
19
22
|
actions.push("plan is evidence-layout v1; artifacts go under .omo/evidence/");
|
|
20
23
|
return actions;
|
|
21
24
|
}
|
|
22
25
|
|
|
23
|
-
function activeGoalActions(plan: UlwLoopPlan, active: UlwLoopItem): readonly string[] {
|
|
26
|
+
function activeGoalActions(plan: UlwLoopPlan, active: UlwLoopItem, surface: UlwLoopToolkitSurface): readonly string[] {
|
|
24
27
|
const unresolved = active.successCriteria.filter((criterion) => criterion.status !== "pass");
|
|
25
28
|
if (unresolved.length > 0)
|
|
26
29
|
return [
|
|
27
|
-
|
|
30
|
+
surface === "omo-senpi"
|
|
31
|
+
? `${active.id} has ${unresolved.length} unresolved criterion(s) (${unresolved.map((criterion) => criterion.id).join(", ")}): record proof with \`agentToolkit.recordEvidence({ goalId: "${active.id}", criterionId: "<id>", status: "pass", evidence: "<observable proof>" })\`.`
|
|
32
|
+
: `${active.id} has ${unresolved.length} unresolved criterion(s) (${unresolved.map((criterion) => criterion.id).join(", ")}): record proof with \`omo-agent-toolkit ulw-loop record-evidence --goal-id ${active.id} --criterion-id <id> --status pass --evidence "<observable proof>"\`.`,
|
|
28
33
|
];
|
|
29
34
|
if (hasAllCriteriaPass(active) && isFinalRunCompletionCandidate(plan, active))
|
|
30
35
|
return [
|
|
31
|
-
|
|
36
|
+
surface === "omo-senpi"
|
|
37
|
+
? `${active.id} passes every criterion and is the final story: complete the driver goal, then call \`agentToolkit.checkpoint({ goalId: "${active.id}", printTemplate: true })\` to build the final quality gate. Use status().result.currentAttemptDir for all quality-gate artifacts.`
|
|
38
|
+
: `${active.id} passes every criterion and is the final story: update_goal complete, then checkpoint --print-template to build the final quality gate. Use status --json's currentAttemptDir for all quality-gate artifacts.`,
|
|
32
39
|
];
|
|
33
40
|
return [
|
|
34
|
-
|
|
41
|
+
surface === "omo-senpi"
|
|
42
|
+
? `${active.id} passes every criterion: close it with \`agentToolkit.checkpoint({ goalId: "${active.id}", status: "complete", evidence: "<proof>" })\`. Put quality-gate artifacts under status().result.currentAttemptDir.`
|
|
43
|
+
: `${active.id} passes every criterion: close it with \`omo-agent-toolkit ulw-loop checkpoint --goal-id ${active.id} --status complete --evidence "<proof>" --codex-goal-json '<get_goal json>'\`. Put quality-gate artifacts under the currentAttemptDir shown by status --json.`,
|
|
35
44
|
];
|
|
36
45
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// biome-ignore-all format: compact batch steering module stays below the pure LOC budget.
|
|
2
2
|
import type { UlwLoopScope } from "./paths.js";
|
|
3
|
-
import {
|
|
3
|
+
import { commit } from "./plan-commit.js";
|
|
4
|
+
import { findAcceptedSteeringLedgerEntry, readUlwLoopPlan, withUlwLoopMutationLock } from "./plan-io.js";
|
|
4
5
|
import { applySteeringMutation, validateUlwLoopSteeringProposal } from "./steering.js";
|
|
5
6
|
import { buildSteeringPlanSnapshot, changedGoalIdsBetween } from "./steering-snapshot.js";
|
|
6
7
|
import type { UlwLoopLedgerEntry, UlwLoopPlan, UlwLoopSteeringAudit, UlwLoopSteeringProposal } from "./types.js";
|
|
@@ -43,17 +44,16 @@ export async function steerUlwLoopBatch(
|
|
|
43
44
|
const failed = prepared.results.find((item) => !item.accepted);
|
|
44
45
|
if (failed !== undefined) {
|
|
45
46
|
const entry = rejectedLedgerEntry(prepared.results);
|
|
46
|
-
await
|
|
47
|
+
await commit(repoRoot, scope, { plan, entries: [entry] });
|
|
47
48
|
return rejected(plan, prepared.results, failed.rejectedReasons);
|
|
48
49
|
}
|
|
49
50
|
let next = plan;
|
|
50
51
|
for (const item of prepared.items) if (item.kind === "fresh") next = item.prepared.next;
|
|
51
52
|
const fresh = prepared.items.filter((item): item is Extract<PreparedItem, { readonly kind: "fresh" }> => item.kind === "fresh");
|
|
52
53
|
if (fresh.length > 0) {
|
|
53
|
-
await writePlan(repoRoot, next, scope);
|
|
54
54
|
const entries = fresh.map((item) => ledgerEntry(item.prepared.proposal, item.prepared.audit, item.prepared.proposal.now?.toISOString() ?? iso()));
|
|
55
55
|
const batchEntry = batchUpdateLedgerEntry(plan, next, iso());
|
|
56
|
-
await
|
|
56
|
+
await commit(repoRoot, scope, { plan: next, entries: batchEntry === null ? entries : [...entries, batchEntry] });
|
|
57
57
|
}
|
|
58
58
|
return { plan: next, accepted: true, results: prepared.results, rejectedReasons: [] };
|
|
59
59
|
});
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// biome-ignore-all format: compact steering module must stay below the 240 pure-LOC budget
|
|
2
2
|
import { isUlwLoopDone } from "./goal-status.js";
|
|
3
3
|
import type { UlwLoopScope } from "./paths.js";
|
|
4
|
-
import {
|
|
4
|
+
import { commit } from "./plan-commit.js";
|
|
5
|
+
import { findAcceptedSteeringLedgerEntry, readUlwLoopPlan, withUlwLoopMutationLock } from "./plan-io.js";
|
|
5
6
|
import { makeGoal, reviseCriterion, reviseWording, splitOrBlock } from "./steering-mutations.js";
|
|
6
7
|
import { buildSteeringPlanSnapshot, changedGoalIdsBetween } from "./steering-snapshot.js";
|
|
7
8
|
import type {
|
|
@@ -209,9 +210,9 @@ export async function steerUlwLoop(repoRoot: string, proposal: UlwLoopSteeringPr
|
|
|
209
210
|
}
|
|
210
211
|
const at = proposal.now?.toISOString() ?? iso();
|
|
211
212
|
const batchEntry = accepted ? batchUpdateLedgerEntry(plan, next, at) : null;
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
213
|
+
const entries = [ledgerEntry(proposal, finalAudit, at)];
|
|
214
|
+
if (batchEntry !== null) entries.push(batchEntry);
|
|
215
|
+
await commit(repoRoot, scope, { plan: next, entries });
|
|
215
216
|
return { plan: next, accepted, audit: finalAudit, rejectedReasons: audit.invariant.rejectedReasons, deduped: false };
|
|
216
217
|
});
|
|
217
218
|
}
|
|
@@ -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,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
|
+
}
|