oh-my-opencode 4.18.2 → 4.19.1
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/.agents/command/omomomo.md +1 -1
- package/.agents/skills/omomomo/SKILL.md +1 -1
- package/.opencode/command/omomomo.md +1 -1
- package/README.ja.md +6 -6
- package/README.ko.md +6 -6
- package/README.md +8 -8
- package/README.ru.md +6 -6
- package/README.zh-cn.md +6 -6
- package/dist/agents/atlas/agent.d.ts +7 -6
- package/dist/agents/gpt-prompt-identity.d.ts +3 -0
- package/dist/agents/sisyphus/gpt-5-5.d.ts +1 -1
- package/dist/agents/sisyphus/gpt-task-system-guide.d.ts +1 -0
- package/dist/agents/sisyphus/index.d.ts +5 -0
- package/dist/agents/sisyphus/kimi-k3.d.ts +19 -0
- package/dist/agents/sisyphus-agent-factory.d.ts +1 -1
- package/dist/agents/sisyphus-junior/agent.d.ts +8 -4
- package/dist/agents/sisyphus-junior/gpt-5-5.d.ts +2 -2
- package/dist/agents/sisyphus-junior/index.d.ts +2 -0
- package/dist/agents/sisyphus-junior/kimi-k3.d.ts +12 -0
- package/dist/agents/types.d.ts +2 -2
- package/dist/cli/index.js +781 -215
- package/dist/cli-node/index.js +781 -215
- package/dist/config/index.d.ts +1 -1
- package/dist/config/schema/agent-overrides.d.ts +48 -48
- package/dist/config/schema/browser-automation.d.ts +1 -0
- package/dist/config/schema/categories.d.ts +6 -6
- package/dist/config/schema/codegraph.d.ts +2 -0
- package/dist/config/schema/commands.d.ts +1 -3
- package/dist/config/schema/default-mode.d.ts +1 -1
- package/dist/config/schema/fallback-models.d.ts +5 -5
- package/dist/config/schema/goal.d.ts +7 -0
- package/dist/config/schema/hooks.d.ts +1 -1
- package/dist/config/schema/oh-my-opencode-config.d.ts +56 -58
- package/dist/config/schema/ralph-loop.d.ts +1 -1
- package/dist/config/schema.d.ts +1 -1
- package/dist/create-hooks.d.ts +1 -1
- package/dist/features/builtin-commands/templates/goal.d.ts +1 -0
- package/dist/features/builtin-commands/templates/stop-continuation.d.ts +1 -1
- package/dist/features/builtin-commands/types.d.ts +1 -1
- package/dist/hooks/auto-slash-command/executor.d.ts +1 -0
- package/dist/hooks/codegraph-bootstrap/hook.d.ts +3 -2
- package/dist/hooks/goal/command-arguments.d.ts +13 -0
- package/dist/hooks/goal/controller.d.ts +26 -0
- package/dist/hooks/goal/index.d.ts +23 -0
- package/dist/hooks/goal/prompt.d.ts +3 -0
- package/dist/hooks/goal/store.d.ts +8 -0
- package/dist/hooks/goal/tools.d.ts +10 -0
- package/dist/hooks/goal/types.d.ts +93 -0
- package/dist/hooks/goal/validation.d.ts +4 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/keyword-detector/hook.d.ts +1 -2
- package/dist/hooks/ralph-loop/types.d.ts +1 -1
- package/dist/hooks/sisyphus-junior-notepad/constants.d.ts +1 -1
- package/dist/hooks/start-work/notepad-scaffold.d.ts +10 -0
- package/dist/index.js +5398 -5109
- package/dist/oh-my-opencode.schema.json +34 -19
- package/dist/plugin/chat-message/loop-commands.d.ts +1 -1
- package/dist/plugin/chat-message/types.d.ts +23 -10
- package/dist/plugin/hooks/create-core-hooks.d.ts +1 -1
- package/dist/plugin/hooks/create-session-hooks.d.ts +3 -2
- package/dist/plugin/hooks/create-transform-hooks.d.ts +0 -2
- package/dist/plugin/stop-continuation.d.ts +2 -2
- package/dist/shared/omo-process-sweep.d.ts +18 -0
- package/dist/shared/system-directive.d.ts +0 -1
- package/dist/skills/programming/scripts/typescript/check-no-excuse-rules.test.ts +87 -0
- package/dist/skills/programming/scripts/typescript/check-no-excuse-rules.ts +35 -5
- package/dist/skills/ulw-plan/SKILL.md +31 -8
- package/dist/skills/ulw-plan/references/full-workflow.md +117 -6
- package/dist/skills/ulw-plan/scripts/scaffold-plan.mjs +46 -10
- package/dist/skills/ulw-research/SKILL.md +3 -3
- package/dist/testing/create-plugin-module.d.ts +1 -0
- package/dist/tools/delegate-task/types.d.ts +0 -1
- package/dist/tui.js +138 -80
- package/package.json +14 -14
- package/packages/git-bash-mcp/dist/cli.js +54 -8
- package/packages/lsp-core/src/lsp/client-diagnostics-freshness.integration.test.ts +60 -0
- package/packages/lsp-core/src/lsp/client.ts +14 -1
- package/packages/lsp-core/src/lsp/fixtures/diagnostics-freshness-contract-probe.ts +2 -2
- package/packages/lsp-core/src/mcp.ts +9 -0
- package/packages/lsp-daemon/dist/cli.js +319 -51
- package/packages/lsp-daemon/dist/client.js +136 -48
- package/packages/lsp-daemon/dist/daemon-client.d.ts +0 -10
- package/packages/lsp-daemon/dist/daemon-client.js +4 -34
- package/packages/lsp-daemon/dist/daemon-failure-result.d.ts +3 -0
- package/packages/lsp-daemon/dist/daemon-failure-result.js +38 -0
- package/packages/lsp-daemon/dist/daemon-request-error.d.ts +14 -0
- package/packages/lsp-daemon/dist/daemon-request-error.js +26 -0
- package/packages/lsp-daemon/dist/daemon-server.js +5 -1
- package/packages/lsp-daemon/dist/index.js +137 -48
- package/packages/lsp-daemon/dist/proxy.d.ts +2 -0
- package/packages/lsp-daemon/dist/proxy.js +1 -0
- package/packages/lsp-daemon/dist/version-reap.d.ts +26 -0
- package/packages/lsp-daemon/dist/version-reap.js +182 -0
- package/packages/lsp-tools-mcp/dist/cli.js +61 -10
- package/packages/lsp-tools-mcp/dist/lsp/manager.js +6 -1
- package/packages/lsp-tools-mcp/dist/mcp.js +61 -10
- package/packages/lsp-tools-mcp/dist/tools.js +6 -1
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +416 -25
- 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 +22 -2
- package/packages/omo-codex/plugin/components/codegraph/AGENTS.md +7 -6
- package/packages/omo-codex/plugin/components/codegraph/dist/cli.js +638 -267
- package/packages/omo-codex/plugin/components/codegraph/dist/serve.js +178 -89
- package/packages/omo-codex/plugin/components/codegraph/package.json +2 -2
- package/packages/omo-codex/plugin/components/codegraph/src/hook-types.ts +3 -1
- package/packages/omo-codex/plugin/components/codegraph/src/hook.ts +3 -1
- package/packages/omo-codex/plugin/components/codegraph/src/mcp-bridge.ts +98 -32
- package/packages/omo-codex/plugin/components/codegraph/src/mcp-unavailable.ts +5 -0
- package/packages/omo-codex/plugin/components/codegraph/src/serve.ts +15 -6
- package/packages/omo-codex/plugin/components/codegraph/src/session-start-worker.ts +6 -2
- package/packages/omo-codex/plugin/components/codegraph/src/sweep-cli.ts +1 -0
- package/packages/omo-codex/plugin/components/codegraph/test/hook-exclusion.test.ts +1 -1
- package/packages/omo-codex/plugin/components/codegraph/test/hook-store-upgrade.test.ts +148 -0
- package/packages/omo-codex/plugin/components/codegraph/test/mcp-bridge-fixtures.ts +31 -2
- package/packages/omo-codex/plugin/components/codegraph/test/package-runtime.test.ts +1 -1
- package/packages/omo-codex/plugin/components/codegraph/test/serve-mcp-bridge-lifecycle.test.ts +65 -1
- package/packages/omo-codex/plugin/components/codegraph/test/serve-mcp-bridge.test.ts +3 -7
- package/packages/omo-codex/plugin/components/codegraph/test/serve-mcp-facade.test.ts +1 -1
- package/packages/omo-codex/plugin/components/codegraph/test/serve-provision.test.ts +57 -3
- package/packages/omo-codex/plugin/components/codegraph/test/serve-unavailable.test.ts +41 -0
- package/packages/omo-codex/plugin/components/codegraph/test/serve.test.ts +109 -8
- package/packages/omo-codex/plugin/components/codegraph/test/session-start-node-support.test.ts +2 -2
- package/packages/omo-codex/plugin/components/codegraph/test/session-start-worker-flow.test.ts +43 -0
- 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/AGENTS.md +1 -0
- package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +3 -3
- package/packages/omo-codex/plugin/components/lsp/dist/cli.js +80 -40
- 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/dist/cli.js +11 -0
- 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/rules/src/post-compact-budget.ts +11 -0
- package/packages/omo-codex/plugin/components/rules/test/post-compact-budget.test.ts +60 -0
- package/packages/omo-codex/plugin/components/start-work-continuation/AGENTS.md +8 -0
- package/packages/omo-codex/plugin/components/start-work-continuation/dist/cli.js +147 -60
- package/packages/omo-codex/plugin/components/start-work-continuation/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/start-work-continuation/package.json +1 -1
- package/packages/omo-codex/plugin/components/start-work-continuation/src/boulder-reader.ts +3 -71
- package/packages/omo-codex/plugin/components/start-work-continuation/src/plan-checklist.ts +171 -0
- package/packages/omo-codex/plugin/components/start-work-continuation/test/boulder-reader.test.ts +198 -23
- package/packages/omo-codex/plugin/components/start-work-continuation/test/cli.test.ts +4 -3
- package/packages/omo-codex/plugin/components/start-work-continuation/test/codex-hook.test.ts +6 -5
- package/packages/omo-codex/plugin/components/start-work-continuation/test/fixtures/plan-scaffold.md +15 -0
- 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.md +10 -10
- package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-clone-fidelity-reviewer.toml +2 -2
- package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-code-reviewer.toml +2 -2
- package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-gate-reviewer.toml +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-worker-high.toml +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-worker-medium.toml +2 -2
- package/packages/omo-codex/plugin/components/ultrawork/agents/momus.toml +3 -3
- package/packages/omo-codex/plugin/components/ultrawork/agents/plan.toml +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/dist/cli.js +1 -1
- 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/skills/ulw-plan/SKILL.md +31 -8
- package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/references/full-workflow.md +117 -6
- package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/scripts/scaffold-plan.mjs +46 -10
- package/packages/omo-codex/plugin/components/ultrawork/src/codex-hook.ts +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/test/codex-hook-trigger-policy.test.ts +42 -0
- package/packages/omo-codex/plugin/components/ulw-loop/README.md +6 -4
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-continuation.d.ts +21 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-continuation.js +64 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.js +15 -2
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-arg-parser.js +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-commands.js +2 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-output.d.ts +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-output.js +3 -3
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-steering.d.ts +3 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-steering.js +85 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-subcommands.d.ts +0 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-subcommands.js +13 -33
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +998 -593
- package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-instruction.js +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/constants.d.ts +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/constants.js +2 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/domain-types.d.ts +6 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/goal-status.d.ts +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/goal-status.js +3 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-crud.d.ts +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-crud.js +4 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-io.d.ts +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-io.js +10 -2
- package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-verdicts.js +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/steering-batch.d.ts +15 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/steering-batch.js +84 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/steering-mutations.d.ts +5 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/steering-mutations.js +81 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/steering.js +7 -59
- package/packages/omo-codex/plugin/components/ulw-loop/dist/stop-resume-hook.js +12 -4
- package/packages/omo-codex/plugin/components/ulw-loop/dist/validation-batch.d.ts +9 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/validation-batch.js +122 -0
- package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +4 -4
- 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 +8 -6
- package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-continuation.ts +84 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +13 -2
- package/packages/omo-codex/plugin/components/ulw-loop/src/cli-arg-parser.ts +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/src/cli-commands.ts +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/src/cli-output.ts +3 -3
- package/packages/omo-codex/plugin/components/ulw-loop/src/cli-steering.ts +49 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/cli-subcommands.ts +13 -45
- package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-instruction.ts +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/constants.ts +2 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/domain-types.ts +7 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/goal-status.ts +4 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/plan-crud.ts +4 -1
- package/packages/omo-codex/plugin/components/ulw-loop/src/plan-io.ts +20 -2
- package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-verdicts.ts +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/src/steering-batch.ts +125 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/steering-mutations.ts +59 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/steering.ts +6 -58
- package/packages/omo-codex/plugin/components/ulw-loop/src/stop-resume-hook.ts +12 -4
- package/packages/omo-codex/plugin/components/ulw-loop/src/validation-batch.ts +122 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint-continuation.test.ts +76 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/cli-checkpoint-continuation.test.ts +111 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/cli-steering-batch.test.ts +67 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/cli-validation-batch.test.ts +57 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/paths.test.ts +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/steering-batch.test.ts +152 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/stop-resume-hook.test.ts +29 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/validation-batch-checkpoint.test.ts +147 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/validation-batch.test.ts +73 -0
- 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-codegraph-init-guidance.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/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 +2 -2
- package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +2 -2
- 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 +2 -2
- package/packages/omo-codex/plugin/hooks/stop-checking-start-work-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-checking-start-work-continuation.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 +41 -41
- package/packages/omo-codex/plugin/package.json +1 -1
- package/packages/omo-codex/plugin/scripts/migrate-codex-config/multi-agent-v2-guard.mjs +32 -28
- package/packages/omo-codex/plugin/scripts/migrate-codex-config/subagent-limit-guard.mjs +17 -45
- package/packages/omo-codex/plugin/scripts/migrate-codex-config/toml-section-editor.mjs +455 -0
- package/packages/omo-codex/plugin/scripts/sync-skills.mjs +3 -18
- package/packages/omo-codex/plugin/shared/test/config-loader.test.ts +43 -0
- package/packages/omo-codex/plugin/skills/programming/scripts/typescript/check-no-excuse-rules.ts +35 -5
- package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +8 -6
- package/packages/omo-codex/plugin/skills/ulw-plan/SKILL.md +31 -8
- package/packages/omo-codex/plugin/skills/ulw-plan/references/full-workflow.md +117 -6
- package/packages/omo-codex/plugin/skills/ulw-plan/scripts/scaffold-plan.mjs +46 -10
- package/packages/omo-codex/plugin/test/aggregate-agents.test.mjs +11 -11
- package/packages/omo-codex/plugin/test/aggregate-hooks.test.mjs +26 -0
- package/packages/omo-codex/plugin/test/bootstrap-setup.test.mjs +41 -0
- package/packages/omo-codex/plugin/test/lcx-bug-skills.test.mjs +2 -2
- package/packages/omo-codex/plugin/test/migrate-codex-config.test.mjs +6 -6
- package/packages/omo-codex/plugin/test/multi-agent-v2-regression.test.mjs +3 -3
- package/packages/omo-codex/plugin/test/scaffold-plan.test.mjs +62 -0
- package/packages/omo-codex/plugin/test/subagent-limit-migration.test.mjs +271 -14
- package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +3 -18
- package/packages/omo-codex/plugin/test/sync-skills.test.mjs +0 -5
- package/packages/omo-codex/plugin/test/ulw-plan-review-state-contract.test.mjs +293 -0
- package/packages/omo-codex/scripts/install-config.test.mjs +10 -10
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +401 -22
- package/packages/omo-codex/scripts/install-generated-bundle.test.mjs +39 -2
- package/packages/omo-codex/scripts/install-marketplace-cache.test.mjs +4 -1
- package/packages/shared-skills/skills/programming/scripts/typescript/check-no-excuse-rules.test.ts +87 -0
- package/packages/shared-skills/skills/programming/scripts/typescript/check-no-excuse-rules.ts +35 -5
- package/packages/shared-skills/skills/ulw-plan/SKILL.md +31 -8
- package/packages/shared-skills/skills/ulw-plan/references/full-workflow.md +117 -6
- package/packages/shared-skills/skills/ulw-plan/scripts/scaffold-plan.mjs +46 -10
- package/packages/shared-skills/skills/ulw-research/SKILL.md +3 -3
- package/dist/features/builtin-commands/templates/ralph-loop.d.ts +0 -3
- package/dist/skills/ultraresearch/SKILL.md +0 -10
- package/packages/shared-skills/skills/lcx-contribute-bug-fix/SKILL.md +0 -273
- package/packages/shared-skills/skills/lcx-contribute-bug-fix/agents/openai.yaml +0 -12
- package/packages/shared-skills/skills/lcx-contribute-bug-fix/scripts/create-pr-body.mjs +0 -107
- package/packages/shared-skills/skills/lcx-doctor/SKILL.md +0 -128
- package/packages/shared-skills/skills/lcx-doctor/agents/openai.yaml +0 -11
- package/packages/shared-skills/skills/lcx-report-bug/SKILL.md +0 -268
- package/packages/shared-skills/skills/lcx-report-bug/agents/openai.yaml +0 -11
- package/packages/shared-skills/skills/ultraresearch/SKILL.md +0 -10
- /package/{dist → packages/omo-codex/plugin/components/lcx}/skills/lcx-contribute-bug-fix/SKILL.md +0 -0
- /package/{dist → packages/omo-codex/plugin/components/lcx}/skills/lcx-contribute-bug-fix/agents/openai.yaml +0 -0
- /package/{dist → packages/omo-codex/plugin/components/lcx}/skills/lcx-contribute-bug-fix/scripts/create-pr-body.mjs +0 -0
- /package/{dist → packages/omo-codex/plugin/components/lcx}/skills/lcx-doctor/SKILL.md +0 -0
- /package/{dist → packages/omo-codex/plugin/components/lcx}/skills/lcx-doctor/agents/openai.yaml +0 -0
- /package/{dist → packages/omo-codex/plugin/components/lcx}/skills/lcx-report-bug/SKILL.md +0 -0
- /package/{dist → packages/omo-codex/plugin/components/lcx}/skills/lcx-report-bug/agents/openai.yaml +0 -0
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// components/ulw-loop/src/
|
|
3
|
+
// components/ulw-loop/src/checkpoint.ts
|
|
4
|
+
import { existsSync as existsSync3, statSync } from "node:fs";
|
|
5
|
+
import { readFile as readFile5 } from "node:fs/promises";
|
|
6
|
+
import { resolve as resolve3 } from "node:path";
|
|
7
|
+
|
|
8
|
+
// components/ulw-loop/src/checkpoint-reconciliation.ts
|
|
9
|
+
import { existsSync } from "node:fs";
|
|
4
10
|
import { readFile } from "node:fs/promises";
|
|
11
|
+
|
|
12
|
+
// components/ulw-loop/src/paths.ts
|
|
13
|
+
import { isAbsolute, join, relative, sep } from "node:path";
|
|
5
14
|
// components/ulw-loop/src/constants.ts
|
|
6
15
|
var ULW_LOOP_DIR = ".omo/ulw-loop";
|
|
7
16
|
var ULW_LOOP_BRIEF = "brief.md";
|
|
@@ -38,179 +47,7 @@ class UlwLoopError extends Error {
|
|
|
38
47
|
function iso() {
|
|
39
48
|
return new Date().toISOString();
|
|
40
49
|
}
|
|
41
|
-
// components/ulw-loop/src/cli-arg-parser.ts
|
|
42
|
-
var VALUE_FLAGS = new Set("--brief --brief-file --session-id --codex-goal-mode --goal --goal-id --criterion-id --status --evidence --notes --codex-goal-json --quality-gate-json --kind --rationale --title --objective --target-goal-id --source --after-json --directive-json --directive-file --idempotency-key".split(" "));
|
|
43
|
-
var SUBCOMMANDS = new Set("create-goals status complete-goals criteria record-evidence checkpoint steer add-goal record-review-blockers".split(" "));
|
|
44
|
-
function hasFlag(argv, flag) {
|
|
45
|
-
return argv.includes(flag);
|
|
46
|
-
}
|
|
47
|
-
function readValue(argv, flag) {
|
|
48
|
-
const index = argv.indexOf(flag);
|
|
49
|
-
if (index >= 0) {
|
|
50
|
-
const next = argv[index + 1];
|
|
51
|
-
return next === undefined || next.startsWith("--") ? undefined : next;
|
|
52
|
-
}
|
|
53
|
-
const prefix = `${flag}=`;
|
|
54
|
-
return argv.find((arg) => arg.startsWith(prefix))?.slice(prefix.length);
|
|
55
|
-
}
|
|
56
|
-
function parseGoalArg(argv) {
|
|
57
|
-
return readValue(argv, "--goal-id") ?? readValue(argv, "--goal");
|
|
58
|
-
}
|
|
59
|
-
async function readStdin() {
|
|
60
|
-
const chunks = [];
|
|
61
|
-
for await (const chunk of process.stdin)
|
|
62
|
-
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
63
|
-
return Buffer.concat(chunks).toString("utf8");
|
|
64
|
-
}
|
|
65
|
-
function positionalText(argv) {
|
|
66
|
-
const words = [];
|
|
67
|
-
for (let index = SUBCOMMANDS.has(argv[0] ?? "") ? 1 : 0;index < argv.length; index += 1) {
|
|
68
|
-
const arg = argv[index];
|
|
69
|
-
if (arg === undefined)
|
|
70
|
-
continue;
|
|
71
|
-
if (VALUE_FLAGS.has(arg)) {
|
|
72
|
-
index += 1;
|
|
73
|
-
continue;
|
|
74
|
-
}
|
|
75
|
-
if (arg.startsWith("--"))
|
|
76
|
-
continue;
|
|
77
|
-
words.push(arg);
|
|
78
|
-
}
|
|
79
|
-
return words.join(" ").trim();
|
|
80
|
-
}
|
|
81
|
-
function looksLikeJson(value) {
|
|
82
|
-
const trimmed = value.trim();
|
|
83
|
-
return trimmed.startsWith("{") || trimmed.startsWith("[");
|
|
84
|
-
}
|
|
85
|
-
async function readJsonInput(value) {
|
|
86
|
-
if (value === undefined)
|
|
87
|
-
return;
|
|
88
|
-
try {
|
|
89
|
-
return JSON.parse(looksLikeJson(value) ? value : await readFile(value, "utf8"));
|
|
90
|
-
} catch (error) {
|
|
91
|
-
const message = error instanceof Error ? error.message : "unknown error";
|
|
92
|
-
throw new UlwLoopError(`Invalid JSON input: ${message}`, "ULW_LOOP_JSON_INPUT_INVALID", { cause: error });
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
async function parseCodexGoalJson(value) {
|
|
96
|
-
if (value === undefined)
|
|
97
|
-
return;
|
|
98
|
-
const raw = looksLikeJson(value) ? value : await readFile(value, "utf8");
|
|
99
|
-
try {
|
|
100
|
-
JSON.parse(raw);
|
|
101
|
-
return raw;
|
|
102
|
-
} catch (error) {
|
|
103
|
-
const message = error instanceof Error ? error.message : "unknown error";
|
|
104
|
-
throw new UlwLoopError(`Invalid --codex-goal-json: ${message}`, "ULW_LOOP_CODEX_GOAL_JSON_INVALID", { cause: error });
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
function required(argv, flag, code) {
|
|
108
|
-
const value = readValue(argv, flag)?.trim();
|
|
109
|
-
if (value)
|
|
110
|
-
return value;
|
|
111
|
-
throw new UlwLoopError(`Missing ${flag}.`, code, { details: { flag } });
|
|
112
|
-
}
|
|
113
|
-
function evidenceStatus(value) {
|
|
114
|
-
switch (value) {
|
|
115
|
-
case "pass":
|
|
116
|
-
return "pass";
|
|
117
|
-
case "fail":
|
|
118
|
-
return "fail";
|
|
119
|
-
case "blocked":
|
|
120
|
-
return "blocked";
|
|
121
|
-
default:
|
|
122
|
-
throw new UlwLoopError("Invalid --status; expected pass, fail, or blocked.", "ULW_LOOP_EVIDENCE_STATUS_INVALID", { details: { status: value } });
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
function parseRecordEvidenceArgs(argv) {
|
|
126
|
-
const result = { goalId: required(argv, "--goal-id", "ULW_LOOP_GOAL_ID_REQUIRED"), criterionId: required(argv, "--criterion-id", "ULW_LOOP_CRITERION_ID_REQUIRED"), status: evidenceStatus(required(argv, "--status", "ULW_LOOP_EVIDENCE_STATUS_REQUIRED")), evidence: required(argv, "--evidence", "ULW_LOOP_EVIDENCE_REQUIRED") };
|
|
127
|
-
const notes = readValue(argv, "--notes")?.trim();
|
|
128
|
-
return notes ? { ...result, notes } : result;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// components/ulw-loop/src/cli-output.ts
|
|
132
|
-
var ULW_LOOP_HELP = `Usage:
|
|
133
|
-
omo ulw-loop create-goals --brief "..." [--brief-file <path>] [--from-stdin] [--codex-goal-mode aggregate|per_story] [--force] [--json]
|
|
134
|
-
omo ulw-loop status [--json]
|
|
135
|
-
omo ulw-loop complete-goals [--retry-failed] [--json]
|
|
136
|
-
omo ulw-loop criteria --goal-id <id> [--json]
|
|
137
|
-
omo ulw-loop record-evidence --goal-id <id> --criterion-id <id> --status pass|fail|blocked --evidence "..." [--notes "..."] [--json]
|
|
138
|
-
omo ulw-loop checkpoint --goal-id <id> --status complete|failed|blocked --evidence "..." --codex-goal-json <...> [--quality-gate-json <...>] [--json]
|
|
139
|
-
omo ulw-loop steer --kind <kind> ... --evidence "..." --rationale "..." [--json]
|
|
140
|
-
omo ulw-loop add-goal --title "..." --objective "..." [--json]
|
|
141
|
-
omo ulw-loop record-review-blockers --goal-id <id> --title "..." --objective "..." --evidence "..." --codex-goal-json <...> [--json]
|
|
142
|
-
|
|
143
|
-
All subcommands accept [--session-id <id>] to isolate state under .omo/ulw-loop/<id>/; without it, Codex session env is used when present.`;
|
|
144
|
-
function printJson(value) {
|
|
145
|
-
process.stdout.write(`${JSON.stringify(value, null, 2)}
|
|
146
|
-
`);
|
|
147
|
-
}
|
|
148
|
-
function printJsonError(error) {
|
|
149
|
-
if (error instanceof UlwLoopError) {
|
|
150
|
-
printJson({
|
|
151
|
-
ok: false,
|
|
152
|
-
error: {
|
|
153
|
-
code: error.code,
|
|
154
|
-
message: error.message,
|
|
155
|
-
...error.details === undefined ? {} : { details: error.details }
|
|
156
|
-
}
|
|
157
|
-
});
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
if (error instanceof Error) {
|
|
161
|
-
printJson({ ok: false, error: { code: "ULW_LOOP_UNEXPECTED", message: error.message } });
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
printJson({ ok: false, error: { code: "ULW_LOOP_UNKNOWN", message: "unknown error" } });
|
|
165
|
-
}
|
|
166
|
-
function criteriaCounts(goal) {
|
|
167
|
-
let pass = 0;
|
|
168
|
-
for (const criterion of goal.successCriteria)
|
|
169
|
-
if (criterion.status === "pass")
|
|
170
|
-
pass += 1;
|
|
171
|
-
return { pass, total: goal.successCriteria.length };
|
|
172
|
-
}
|
|
173
|
-
function printStatus(plan) {
|
|
174
|
-
let totalCriteria = 0;
|
|
175
|
-
let passCriteria = 0;
|
|
176
|
-
const lines = ["ulw-loop status", "", "goals:"];
|
|
177
|
-
for (const goal of plan.goals) {
|
|
178
|
-
const counts = criteriaCounts(goal);
|
|
179
|
-
totalCriteria += counts.total;
|
|
180
|
-
passCriteria += counts.pass;
|
|
181
|
-
const marker = goal.id === plan.activeGoalId ? "*" : "-";
|
|
182
|
-
lines.push(`${marker} ${goal.id} [${goal.status}] ${goal.title} (criteria: ${counts.pass}/${counts.total})`);
|
|
183
|
-
}
|
|
184
|
-
lines.push("", "summary:", `total goals: ${plan.goals.length}`, `criteria: ${passCriteria}/${totalCriteria} pass`);
|
|
185
|
-
process.stdout.write(`${lines.join(`
|
|
186
|
-
`)}
|
|
187
|
-
`);
|
|
188
|
-
}
|
|
189
|
-
function blockedDecisionHandoff(plan) {
|
|
190
|
-
const blocked = plan.goals.find((goal) => goal.status === "needs_user_decision" && goal.nonRetriable);
|
|
191
|
-
if (blocked === undefined)
|
|
192
|
-
return "";
|
|
193
|
-
return [
|
|
194
|
-
"ulw-loop: blocked on repeated external authorization; no retryable failed goals remain.",
|
|
195
|
-
`Goal: ${blocked.id} - ${blocked.title}`,
|
|
196
|
-
`Required external decision: ${blocked.requiredExternalDecision ?? "provide the missing authorization or choose a different unblock path"}.`,
|
|
197
|
-
"Do not run complete-goals --retry-failed again until external state changes or the user authorizes an unblock path."
|
|
198
|
-
].join(`
|
|
199
|
-
`);
|
|
200
|
-
}
|
|
201
|
-
function normalizeCodexGoalMode(value) {
|
|
202
|
-
if (value === undefined)
|
|
203
|
-
return "aggregate";
|
|
204
|
-
if (value === "aggregate" || value === "per_story")
|
|
205
|
-
return value;
|
|
206
|
-
throw new UlwLoopError("Invalid --codex-goal-mode; expected aggregate or per_story.", "ULW_LOOP_CODEX_GOAL_MODE_INVALID", { details: { value } });
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// components/ulw-loop/src/cli-subcommands.ts
|
|
210
|
-
import { readFile as readFile6 } from "node:fs/promises";
|
|
211
|
-
|
|
212
50
|
// components/ulw-loop/src/paths.ts
|
|
213
|
-
import { isAbsolute, join, relative, sep } from "node:path";
|
|
214
51
|
var SESSION_ENV_KEYS = ["OMO_ULW_LOOP_SESSION_ID", "CODEX_SESSION_ID", "CODEX_THREAD_ID"];
|
|
215
52
|
function normalizeUlwLoopSessionId(sessionId) {
|
|
216
53
|
const trimmed = sessionId?.trim();
|
|
@@ -276,15 +113,6 @@ function isWithinAttemptDir(absolutePath, attemptRoot, pathApi = PLATFORM_PATH_A
|
|
|
276
113
|
return !pathApi.isAbsolute(relativePath);
|
|
277
114
|
}
|
|
278
115
|
|
|
279
|
-
// components/ulw-loop/src/checkpoint.ts
|
|
280
|
-
import { existsSync as existsSync3, statSync } from "node:fs";
|
|
281
|
-
import { readFile as readFile5 } from "node:fs/promises";
|
|
282
|
-
import { resolve as resolve3 } from "node:path";
|
|
283
|
-
|
|
284
|
-
// components/ulw-loop/src/checkpoint-reconciliation.ts
|
|
285
|
-
import { existsSync } from "node:fs";
|
|
286
|
-
import { readFile as readFile2 } from "node:fs/promises";
|
|
287
|
-
|
|
288
116
|
// components/ulw-loop/src/goal-status.ts
|
|
289
117
|
var ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE = aggregateCodexObjectiveForScope();
|
|
290
118
|
function aggregateCodexObjectiveForScope(scope) {
|
|
@@ -296,6 +124,9 @@ function codexGoalMode(plan) {
|
|
|
296
124
|
function isResolvedStatus(status) {
|
|
297
125
|
return status === "complete";
|
|
298
126
|
}
|
|
127
|
+
function isMemberResolved(goal, plan) {
|
|
128
|
+
return isResolvedStatus(goal.status) || isSupersededResolved(goal, plan);
|
|
129
|
+
}
|
|
299
130
|
function isSupersededResolved(goal, plan) {
|
|
300
131
|
if (goal.steeringStatus !== "superseded")
|
|
301
132
|
return false;
|
|
@@ -389,7 +220,7 @@ async function snapshotObjectiveMapsToUlwLoopPlan(repoRoot, snapshotObjective, s
|
|
|
389
220
|
if (actual.length < 24 || !existsSync(ulwLoopBriefPath(repoRoot, scope)))
|
|
390
221
|
return false;
|
|
391
222
|
try {
|
|
392
|
-
const brief = normalizeObjective(await
|
|
223
|
+
const brief = normalizeObjective(await readFile(ulwLoopBriefPath(repoRoot, scope), "utf8")).toLowerCase();
|
|
393
224
|
return brief.length >= 24 && (brief.includes(actual) || actual.includes(brief));
|
|
394
225
|
} catch (error) {
|
|
395
226
|
if (error instanceof Error)
|
|
@@ -438,7 +269,7 @@ function buildTaskScopedAggregateReconciliationHint(goal, final) {
|
|
|
438
269
|
|
|
439
270
|
// components/ulw-loop/src/codex-goal-snapshot.ts
|
|
440
271
|
import { existsSync as existsSync2 } from "node:fs";
|
|
441
|
-
import { readFile as
|
|
272
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
442
273
|
import { resolve } from "node:path";
|
|
443
274
|
|
|
444
275
|
class CodexGoalSnapshotError extends Error {
|
|
@@ -492,7 +323,7 @@ async function readCodexGoalSnapshotInput(raw, cwd = process.cwd()) {
|
|
|
492
323
|
throw new CodexGoalSnapshotError(`Codex goal snapshot is neither valid JSON nor a readable path: ${trimmed}`);
|
|
493
324
|
}
|
|
494
325
|
try {
|
|
495
|
-
return parseCodexGoalSnapshot(JSON.parse(await
|
|
326
|
+
return parseCodexGoalSnapshot(JSON.parse(await readFile2(path, "utf-8")));
|
|
496
327
|
} catch (error) {
|
|
497
328
|
throw new CodexGoalSnapshotError(`Codex goal snapshot path does not contain valid JSON: ${trimmed}${error instanceof Error ? ` (${error.message})` : ""}`);
|
|
498
329
|
}
|
|
@@ -535,7 +366,7 @@ function formatCodexGoalReconciliation(reconciliation) {
|
|
|
535
366
|
|
|
536
367
|
// components/ulw-loop/src/plan-io.ts
|
|
537
368
|
import { createReadStream } from "node:fs";
|
|
538
|
-
import { appendFile, mkdir, readFile as
|
|
369
|
+
import { appendFile, mkdir, readFile as readFile3, rename, writeFile } from "node:fs/promises";
|
|
539
370
|
import { createInterface } from "node:readline";
|
|
540
371
|
var LEGACY_OBJECTIVE_PREFIX = `Complete all ulw-loop stories in ${ULW_LOOP_DIR}/${ULW_LOOP_GOALS}: `;
|
|
541
372
|
var LEGACY_OBJECTIVE = `Complete all ulw-loop stories listed in ${ULW_LOOP_DIR}/${ULW_LOOP_GOALS}. Use ${ULW_LOOP_DIR}/${ULW_LOOP_LEDGER} as the durable audit trail.`;
|
|
@@ -547,7 +378,7 @@ function isLegacyEnumeratedAggregateObjective(objective) {
|
|
|
547
378
|
return objective === LEGACY_OBJECTIVE || Boolean(objective?.startsWith(LEGACY_OBJECTIVE_PREFIX));
|
|
548
379
|
}
|
|
549
380
|
function isSteeringKind(value) {
|
|
550
|
-
return value === "steering_accepted" || value === "steering_rejected" || value === "criteria_revised";
|
|
381
|
+
return value === "steering_accepted" || value === "steering_rejected" || value === "criteria_revised" || value === "batch_updated";
|
|
551
382
|
}
|
|
552
383
|
async function withUlwLoopMutationLock(repoRoot, scopeOrFn, maybeFn) {
|
|
553
384
|
const scope = typeof scopeOrFn === "function" ? undefined : scopeOrFn;
|
|
@@ -573,7 +404,7 @@ async function readUlwLoopPlan(repoRoot, scope) {
|
|
|
573
404
|
const path = ulwLoopGoalsPath(repoRoot, scope);
|
|
574
405
|
let raw;
|
|
575
406
|
try {
|
|
576
|
-
raw = await
|
|
407
|
+
raw = await readFile3(path, "utf8");
|
|
577
408
|
} catch (error) {
|
|
578
409
|
if (!hasCode(error, "ENOENT"))
|
|
579
410
|
throw error;
|
|
@@ -609,8 +440,14 @@ async function writePlan(repoRoot, plan, scope) {
|
|
|
609
440
|
await rename(tmpPath, path);
|
|
610
441
|
}
|
|
611
442
|
async function appendLedger(repoRoot, entry, scope) {
|
|
443
|
+
await appendLedgerEntries(repoRoot, [entry], scope);
|
|
444
|
+
}
|
|
445
|
+
async function appendLedgerEntries(repoRoot, entries, scope) {
|
|
446
|
+
if (entries.length === 0)
|
|
447
|
+
return;
|
|
612
448
|
await mkdir(ulwLoopDir(repoRoot, scope), { recursive: true });
|
|
613
|
-
await appendFile(ulwLoopLedgerPath(repoRoot, scope), `${JSON.stringify(entry)
|
|
449
|
+
await appendFile(ulwLoopLedgerPath(repoRoot, scope), `${entries.map((entry) => JSON.stringify(entry)).join(`
|
|
450
|
+
`)}
|
|
614
451
|
`, "utf8");
|
|
615
452
|
}
|
|
616
453
|
async function* ledgerLines(repoRoot, scope) {
|
|
@@ -1039,36 +876,245 @@ function parseAdversarialCases(value, byId) {
|
|
|
1039
876
|
});
|
|
1040
877
|
}
|
|
1041
878
|
|
|
1042
|
-
// components/ulw-loop/src/
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
879
|
+
// components/ulw-loop/src/cli-arg-parser.ts
|
|
880
|
+
import { readFile as readFile4 } from "node:fs/promises";
|
|
881
|
+
var VALUE_FLAGS = new Set("--brief --brief-file --session-id --codex-goal-mode --validation-batch-json --goal --goal-id --criterion-id --status --evidence --notes --codex-goal-json --quality-gate-json --kind --rationale --title --objective --target-goal-id --source --after-json --directive-json --directive-file --idempotency-key --proposals-json".split(" "));
|
|
882
|
+
var SUBCOMMANDS = new Set("create-goals status complete-goals criteria record-evidence checkpoint steer add-goal record-review-blockers".split(" "));
|
|
883
|
+
function hasFlag(argv, flag) {
|
|
884
|
+
return argv.includes(flag);
|
|
1046
885
|
}
|
|
1047
|
-
function
|
|
1048
|
-
|
|
886
|
+
function readValue(argv, flag) {
|
|
887
|
+
const index = argv.indexOf(flag);
|
|
888
|
+
if (index >= 0) {
|
|
889
|
+
const next = argv[index + 1];
|
|
890
|
+
return next === undefined || next.startsWith("--") ? undefined : next;
|
|
891
|
+
}
|
|
892
|
+
const prefix = `${flag}=`;
|
|
893
|
+
return argv.find((arg) => arg.startsWith(prefix))?.slice(prefix.length);
|
|
1049
894
|
}
|
|
1050
|
-
function
|
|
1051
|
-
|
|
1052
|
-
return trimmed || ulwLoopFail2("Evidence must be a non-empty string.", "ulw_loop_evidence_required");
|
|
895
|
+
function parseGoalArg(argv) {
|
|
896
|
+
return readValue(argv, "--goal-id") ?? readValue(argv, "--goal");
|
|
1053
897
|
}
|
|
1054
|
-
function
|
|
1055
|
-
const
|
|
1056
|
-
|
|
898
|
+
async function readStdin() {
|
|
899
|
+
const chunks = [];
|
|
900
|
+
for await (const chunk of process.stdin)
|
|
901
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
902
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
1057
903
|
}
|
|
1058
|
-
|
|
1059
|
-
|
|
904
|
+
function positionalText(argv) {
|
|
905
|
+
const words = [];
|
|
906
|
+
for (let index = SUBCOMMANDS.has(argv[0] ?? "") ? 1 : 0;index < argv.length; index += 1) {
|
|
907
|
+
const arg = argv[index];
|
|
908
|
+
if (arg === undefined)
|
|
909
|
+
continue;
|
|
910
|
+
if (VALUE_FLAGS.has(arg)) {
|
|
911
|
+
index += 1;
|
|
912
|
+
continue;
|
|
913
|
+
}
|
|
914
|
+
if (arg.startsWith("--"))
|
|
915
|
+
continue;
|
|
916
|
+
words.push(arg);
|
|
917
|
+
}
|
|
918
|
+
return words.join(" ").trim();
|
|
919
|
+
}
|
|
920
|
+
function looksLikeJson(value) {
|
|
921
|
+
const trimmed = value.trim();
|
|
922
|
+
return trimmed.startsWith("{") || trimmed.startsWith("[");
|
|
923
|
+
}
|
|
924
|
+
async function readJsonInput(value) {
|
|
925
|
+
if (value === undefined)
|
|
1060
926
|
return;
|
|
1061
|
-
const trimmed = raw.trim();
|
|
1062
927
|
try {
|
|
1063
|
-
return JSON.parse(
|
|
928
|
+
return JSON.parse(looksLikeJson(value) ? value : await readFile4(value, "utf8"));
|
|
1064
929
|
} catch (error) {
|
|
1065
|
-
|
|
1066
|
-
|
|
930
|
+
const message = error instanceof Error ? error.message : "unknown error";
|
|
931
|
+
throw new UlwLoopError(`Invalid JSON input: ${message}`, "ULW_LOOP_JSON_INPUT_INVALID", { cause: error });
|
|
1067
932
|
}
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
933
|
+
}
|
|
934
|
+
async function parseCodexGoalJson(value) {
|
|
935
|
+
if (value === undefined)
|
|
936
|
+
return;
|
|
937
|
+
const raw = looksLikeJson(value) ? value : await readFile4(value, "utf8");
|
|
938
|
+
try {
|
|
939
|
+
JSON.parse(raw);
|
|
940
|
+
return raw;
|
|
941
|
+
} catch (error) {
|
|
942
|
+
const message = error instanceof Error ? error.message : "unknown error";
|
|
943
|
+
throw new UlwLoopError(`Invalid --codex-goal-json: ${message}`, "ULW_LOOP_CODEX_GOAL_JSON_INVALID", { cause: error });
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
function required(argv, flag, code) {
|
|
947
|
+
const value = readValue(argv, flag)?.trim();
|
|
948
|
+
if (value)
|
|
949
|
+
return value;
|
|
950
|
+
throw new UlwLoopError(`Missing ${flag}.`, code, { details: { flag } });
|
|
951
|
+
}
|
|
952
|
+
function evidenceStatus(value) {
|
|
953
|
+
switch (value) {
|
|
954
|
+
case "pass":
|
|
955
|
+
return "pass";
|
|
956
|
+
case "fail":
|
|
957
|
+
return "fail";
|
|
958
|
+
case "blocked":
|
|
959
|
+
return "blocked";
|
|
960
|
+
default:
|
|
961
|
+
throw new UlwLoopError("Invalid --status; expected pass, fail, or blocked.", "ULW_LOOP_EVIDENCE_STATUS_INVALID", { details: { status: value } });
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
function parseRecordEvidenceArgs(argv) {
|
|
965
|
+
const result = { goalId: required(argv, "--goal-id", "ULW_LOOP_GOAL_ID_REQUIRED"), criterionId: required(argv, "--criterion-id", "ULW_LOOP_CRITERION_ID_REQUIRED"), status: evidenceStatus(required(argv, "--status", "ULW_LOOP_EVIDENCE_STATUS_REQUIRED")), evidence: required(argv, "--evidence", "ULW_LOOP_EVIDENCE_REQUIRED") };
|
|
966
|
+
const notes = readValue(argv, "--notes")?.trim();
|
|
967
|
+
return notes ? { ...result, notes } : result;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// components/ulw-loop/src/validation-batch.ts
|
|
971
|
+
function isObject(value) {
|
|
972
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
973
|
+
}
|
|
974
|
+
function read(value, key) {
|
|
975
|
+
return Object.entries(value).find(([name]) => name === key)?.[1];
|
|
976
|
+
}
|
|
977
|
+
function text(value, key) {
|
|
978
|
+
const candidate = read(value, key);
|
|
979
|
+
return typeof candidate === "string" && candidate.trim().length > 0 ? candidate.trim() : undefined;
|
|
980
|
+
}
|
|
981
|
+
function strings(value, key) {
|
|
982
|
+
const candidate = read(value, key);
|
|
983
|
+
return Array.isArray(candidate) && candidate.every((item) => typeof item === "string" && item.trim().length > 0) ? candidate.map((item) => item.trim()) : undefined;
|
|
984
|
+
}
|
|
985
|
+
async function parseValidationBatches(input, goals) {
|
|
986
|
+
const raw = await readJsonInput(input);
|
|
987
|
+
if (raw === undefined)
|
|
988
|
+
return;
|
|
989
|
+
if (!Array.isArray(raw))
|
|
990
|
+
fail("--validation-batch-json must be a JSON array.");
|
|
991
|
+
const batches = raw.map(batchFromObject);
|
|
992
|
+
validateBatches(batches, goals);
|
|
993
|
+
return batches;
|
|
994
|
+
}
|
|
995
|
+
function batchFromObject(value) {
|
|
996
|
+
if (!isObject(value))
|
|
997
|
+
fail("validation batch entries must be objects.");
|
|
998
|
+
const batchId = text(value, "batchId");
|
|
999
|
+
const memberIds = strings(value, "memberIds");
|
|
1000
|
+
const finalGoalId = text(value, "finalGoalId");
|
|
1001
|
+
if (batchId === undefined)
|
|
1002
|
+
fail("validation batch requires batchId.");
|
|
1003
|
+
if (memberIds === undefined || memberIds.length < 2)
|
|
1004
|
+
fail("validation batch requires at least two memberIds.");
|
|
1005
|
+
if (finalGoalId === undefined)
|
|
1006
|
+
fail("validation batch requires finalGoalId.");
|
|
1007
|
+
return { batchId, memberIds, finalGoalId };
|
|
1008
|
+
}
|
|
1009
|
+
function validateBatches(batches, goals) {
|
|
1010
|
+
const goalIds = new Set(goals.map((goal) => goal.id));
|
|
1011
|
+
const batchIds = new Set;
|
|
1012
|
+
const members = new Set;
|
|
1013
|
+
for (const batch of batches) {
|
|
1014
|
+
if (batchIds.has(batch.batchId))
|
|
1015
|
+
fail(`duplicate validation batch id: ${batch.batchId}.`);
|
|
1016
|
+
batchIds.add(batch.batchId);
|
|
1017
|
+
if (new Set(batch.memberIds).size !== batch.memberIds.length)
|
|
1018
|
+
fail(`validation batch ${batch.batchId} has duplicate memberIds.`);
|
|
1019
|
+
if (!batch.memberIds.includes(batch.finalGoalId))
|
|
1020
|
+
fail(`validation batch ${batch.batchId} finalGoalId must be a member.`, "ULW_LOOP_VALIDATION_BATCH_FINAL_NOT_MEMBER");
|
|
1021
|
+
for (const memberId of batch.memberIds) {
|
|
1022
|
+
if (!goalIds.has(memberId))
|
|
1023
|
+
fail(`validation batch ${batch.batchId} references unknown goal: ${memberId}.`, "ULW_LOOP_VALIDATION_BATCH_MEMBER_UNKNOWN");
|
|
1024
|
+
if (members.has(memberId))
|
|
1025
|
+
fail(`goal appears in multiple validation batches: ${memberId}.`, "ULW_LOOP_VALIDATION_BATCH_OVERLAP");
|
|
1026
|
+
members.add(memberId);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
function updateBatchesAfterSupersede(plan, targetId, replacementIds) {
|
|
1031
|
+
if (replacementIds.length === 0 || plan.validationBatches === undefined)
|
|
1032
|
+
return;
|
|
1033
|
+
plan.validationBatches = plan.validationBatches.map((batch) => {
|
|
1034
|
+
if (!batch.memberIds.includes(targetId))
|
|
1035
|
+
return batch;
|
|
1036
|
+
const memberIds = batch.memberIds.flatMap((id) => id === targetId ? [...replacementIds] : [id]);
|
|
1037
|
+
const replacementFinalGoalId = replacementIds[replacementIds.length - 1] ?? batch.finalGoalId;
|
|
1038
|
+
const finalGoalId = batch.finalGoalId === targetId ? replacementFinalGoalId : batch.finalGoalId;
|
|
1039
|
+
return { batchId: batch.batchId, memberIds, finalGoalId };
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
function batchUpdateLedgerEntry(before, after, at) {
|
|
1043
|
+
if (JSON.stringify(before.validationBatches ?? []) === JSON.stringify(after.validationBatches ?? []))
|
|
1044
|
+
return null;
|
|
1045
|
+
return { at, kind: "batch_updated", before: before.validationBatches ?? [], after: after.validationBatches ?? [], message: "Validation batch membership updated after steering." };
|
|
1046
|
+
}
|
|
1047
|
+
function batchOf(plan, goalId) {
|
|
1048
|
+
return plan.validationBatches?.find((batch) => batch.memberIds.includes(goalId));
|
|
1049
|
+
}
|
|
1050
|
+
function batchClosedBy(plan, goalId) {
|
|
1051
|
+
const batch = batchOf(plan, goalId);
|
|
1052
|
+
return batch?.finalGoalId === goalId ? batch : undefined;
|
|
1053
|
+
}
|
|
1054
|
+
function requireBatchFinalReady(plan, goal) {
|
|
1055
|
+
const batch = batchClosedBy(plan, goal.id);
|
|
1056
|
+
if (batch === undefined)
|
|
1057
|
+
return;
|
|
1058
|
+
const open = batch.memberIds.filter((id) => id !== goal.id && !memberResolved(plan, id));
|
|
1059
|
+
if (open.length > 0)
|
|
1060
|
+
throw new UlwLoopError("Validation batch has unresolved members.", "ULW_LOOP_VALIDATION_BATCH_OPEN", { details: { batchId: batch.batchId, open } });
|
|
1061
|
+
}
|
|
1062
|
+
function requireAllValidationBatchesClosed(plan, closingGoalId) {
|
|
1063
|
+
const open = (plan.validationBatches ?? []).filter((batch) => batch.memberIds.some((id) => id !== closingGoalId && !memberResolved(plan, id)));
|
|
1064
|
+
if (open.length > 0)
|
|
1065
|
+
throw new UlwLoopError("Validation batches remain open.", "ULW_LOOP_VALIDATION_BATCH_OPEN", { details: { batchIds: open.map((batch) => batch.batchId) } });
|
|
1066
|
+
}
|
|
1067
|
+
function requireBatchGate(plan, goal, gate) {
|
|
1068
|
+
const batch = batchClosedBy(plan, goal.id);
|
|
1069
|
+
if (batch === undefined)
|
|
1070
|
+
return;
|
|
1071
|
+
const members = batch.memberIds.map((id) => plan.goals.find((item) => item.id === id)).filter((item) => item !== undefined);
|
|
1072
|
+
const pending = members.flatMap((member) => member.successCriteria.filter((criterion) => criterion.status !== "pass").map((criterion) => `${member.id}:${criterion.id}`));
|
|
1073
|
+
if (pending.length > 0)
|
|
1074
|
+
throw new UlwLoopError("Validation batch criteria remain pending.", "ULW_LOOP_VALIDATION_BATCH_CRITERIA_PENDING", { details: { batchId: batch.batchId, pending } });
|
|
1075
|
+
const totalCriteria = members.reduce((sum, member) => sum + member.successCriteria.length, 0);
|
|
1076
|
+
const passCount = members.reduce((sum, member) => sum + member.successCriteria.filter((criterion) => criterion.status === "pass").length, 0);
|
|
1077
|
+
if (gate.criteriaCoverage.totalCriteria !== totalCriteria || gate.criteriaCoverage.passCount !== passCount)
|
|
1078
|
+
throw new UlwLoopError("Validation batch gate coverage does not match member criteria.", "ULW_LOOP_VALIDATION_BATCH_GATE_MISMATCH", { details: { batchId: batch.batchId, expected: { totalCriteria, passCount }, actual: gate.criteriaCoverage } });
|
|
1079
|
+
}
|
|
1080
|
+
function memberResolved(plan, goalId) {
|
|
1081
|
+
const goal = plan.goals.find((candidate) => candidate.id === goalId);
|
|
1082
|
+
return goal !== undefined && isMemberResolved(goal, plan);
|
|
1083
|
+
}
|
|
1084
|
+
function fail(message, code = "ULW_LOOP_VALIDATION_BATCH_INVALID") {
|
|
1085
|
+
throw new UlwLoopError(message, code);
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// components/ulw-loop/src/checkpoint.ts
|
|
1089
|
+
var QUALITY_GATE_FS = { existsSync: existsSync3, statSync };
|
|
1090
|
+
function ulwLoopFail2(message, code) {
|
|
1091
|
+
throw new UlwLoopError(message, code);
|
|
1092
|
+
}
|
|
1093
|
+
function normalizeObjective3(value) {
|
|
1094
|
+
return value.replace(/\s+/g, " ").trim();
|
|
1095
|
+
}
|
|
1096
|
+
function nonEmptyEvidence2(value) {
|
|
1097
|
+
const trimmed = value.trim();
|
|
1098
|
+
return trimmed || ulwLoopFail2("Evidence must be a non-empty string.", "ulw_loop_evidence_required");
|
|
1099
|
+
}
|
|
1100
|
+
function findGoal2(plan, goalId) {
|
|
1101
|
+
const goal = plan.goals.find((candidate) => candidate.id === goalId);
|
|
1102
|
+
return goal ?? ulwLoopFail2(`Unknown ulw-loop id: ${goalId}.`, "ulw_loop_goal_not_found");
|
|
1103
|
+
}
|
|
1104
|
+
async function readJsonInput2(raw, repoRoot) {
|
|
1105
|
+
if (raw === undefined || raw.trim() === "")
|
|
1106
|
+
return;
|
|
1107
|
+
const trimmed = raw.trim();
|
|
1108
|
+
try {
|
|
1109
|
+
return JSON.parse(trimmed);
|
|
1110
|
+
} catch (error) {
|
|
1111
|
+
if (!(error instanceof SyntaxError))
|
|
1112
|
+
throw error;
|
|
1113
|
+
}
|
|
1114
|
+
const path = resolve3(repoRoot, trimmed);
|
|
1115
|
+
if (!existsSync3(path))
|
|
1116
|
+
return ulwLoopFail2("Quality gate JSON is neither valid JSON nor a readable path.", "ulw_loop_json_input_invalid");
|
|
1117
|
+
try {
|
|
1072
1118
|
return JSON.parse(await readFile5(path, "utf8"));
|
|
1073
1119
|
} catch (error) {
|
|
1074
1120
|
return ulwLoopFail2(`Quality gate path does not contain valid JSON${error instanceof Error ? `: ${error.message}` : "."}`, "ulw_loop_json_input_invalid");
|
|
@@ -1141,9 +1187,11 @@ async function checkpointUlwLoop(repoRoot, args, scope) {
|
|
|
1141
1187
|
if (args.status === "complete") {
|
|
1142
1188
|
const aggregate = codexGoalMode(plan) === "aggregate";
|
|
1143
1189
|
const final = isFinalRunCompletionCandidate(plan, goal);
|
|
1190
|
+
const closesBatch = batchClosedBy(plan, goal.id) !== undefined;
|
|
1144
1191
|
if (final) {
|
|
1145
1192
|
requireAllCriteriaPass(goal);
|
|
1146
1193
|
requireAllPlanCriteriaPass(plan);
|
|
1194
|
+
requireAllValidationBatchesClosed(plan, goal.id);
|
|
1147
1195
|
} else if (aggregate)
|
|
1148
1196
|
requireEssentialCriteriaPass(goal);
|
|
1149
1197
|
else
|
|
@@ -1166,14 +1214,20 @@ async function checkpointUlwLoop(repoRoot, args, scope) {
|
|
|
1166
1214
|
if (!taskScoped)
|
|
1167
1215
|
throw new UlwLoopError(`${formatCodexGoalReconciliation(reconciliation)}${aggregate && snapshot?.status === "complete" && objective !== undefined ? buildTaskScopedAggregateReconciliationHint(goal, final) : ""}`, "ulw_loop_codex_snapshot_mismatch");
|
|
1168
1216
|
}
|
|
1217
|
+
if (closesBatch)
|
|
1218
|
+
requireBatchFinalReady(plan, goal);
|
|
1219
|
+
if (closesBatch && args.qualityGateJson === undefined)
|
|
1220
|
+
throw new UlwLoopError("Validation batch final checkpoint requires --quality-gate-json.", "ULW_LOOP_VALIDATION_BATCH_GATE_REQUIRED");
|
|
1169
1221
|
if (final)
|
|
1170
1222
|
aggregateCompletion = makeAggregateCompletion(now, evidence, codexGoal);
|
|
1171
|
-
if (final || aggregateCompletion !== undefined)
|
|
1223
|
+
if (final || aggregateCompletion !== undefined || closesBatch) {
|
|
1172
1224
|
qualityGate = validateQualityGate(await readJsonInput2(args.qualityGateJson, repoRoot), {
|
|
1173
1225
|
repoRoot,
|
|
1174
1226
|
fs: QUALITY_GATE_FS,
|
|
1175
1227
|
...plan.evidenceLayoutVersion === 2 ? { currentAttemptDir: ulwLoopAttemptEvidenceDir(goal.id, goal.attempt, scope) } : {}
|
|
1176
1228
|
});
|
|
1229
|
+
requireBatchGate(plan, goal, qualityGate);
|
|
1230
|
+
}
|
|
1177
1231
|
goal.status = "complete";
|
|
1178
1232
|
goal.completedAt = now;
|
|
1179
1233
|
goal.evidence = evidence;
|
|
@@ -1191,215 +1245,89 @@ async function checkpointUlwLoop(repoRoot, args, scope) {
|
|
|
1191
1245
|
await writePlan(repoRoot, plan, scope);
|
|
1192
1246
|
const ledgerEntry = buildLedger(now, args, goal, qualityGate, codexGoal, aggregateCompletion);
|
|
1193
1247
|
await appendLedger(repoRoot, ledgerEntry, scope);
|
|
1248
|
+
const closedBatch = args.status === "complete" ? batchClosedBy(plan, goal.id) : undefined;
|
|
1249
|
+
if (closedBatch !== undefined)
|
|
1250
|
+
await appendLedger(repoRoot, { at: now, kind: "batch_closed", goalId: goal.id, message: closedBatch.batchId }, scope);
|
|
1194
1251
|
return aggregateCompletion === undefined ? { plan, goal, ledgerEntry } : { plan, goal, ledgerEntry, aggregateCompletion };
|
|
1195
1252
|
});
|
|
1196
1253
|
}
|
|
1197
1254
|
|
|
1198
|
-
// components/ulw-loop/src/cli-
|
|
1199
|
-
var
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
"
|
|
1209
|
-
" mark_blocked_superseded: --goal-id, optional --replacements, --evidence, --rationale",
|
|
1210
|
-
'Example: omo ulw-loop steer --kind annotate_ledger --evidence "observed behavior" --rationale "why this changes the plan" --json'
|
|
1211
|
-
].join(`
|
|
1212
|
-
`);
|
|
1213
|
-
function isKind(value) {
|
|
1214
|
-
return value !== undefined && ULW_LOOP_STEERING_MUTATION_KINDS.some((kind) => kind === value);
|
|
1215
|
-
}
|
|
1216
|
-
function isSource(value) {
|
|
1217
|
-
return value !== undefined && SOURCES.some((source) => source === value);
|
|
1218
|
-
}
|
|
1219
|
-
function isModel(value) {
|
|
1220
|
-
return ULW_LOOP_SUCCESS_CRITERION_USER_MODELS.some((model) => model === value);
|
|
1221
|
-
}
|
|
1222
|
-
function fail(message, code, details) {
|
|
1223
|
-
throw new UlwLoopError(message, code, { details });
|
|
1224
|
-
}
|
|
1225
|
-
function kindMessage(prefix) {
|
|
1226
|
-
return `${prefix}
|
|
1255
|
+
// components/ulw-loop/src/cli-output.ts
|
|
1256
|
+
var ULW_LOOP_HELP = `Usage:
|
|
1257
|
+
omo ulw-loop create-goals --brief "..." [--brief-file <path>] [--from-stdin] [--codex-goal-mode aggregate|per_story] [--validation-batch-json <json-or-path>] [--force] [--json]
|
|
1258
|
+
omo ulw-loop status [--json]
|
|
1259
|
+
omo ulw-loop complete-goals [--retry-failed] [--json]
|
|
1260
|
+
omo ulw-loop criteria --goal-id <id> [--json]
|
|
1261
|
+
omo ulw-loop record-evidence --goal-id <id> --criterion-id <id> --status pass|fail|blocked --evidence "..." [--notes "..."] [--json]
|
|
1262
|
+
omo ulw-loop checkpoint --goal-id <id> --status complete|failed|blocked --evidence "..." --codex-goal-json <...> [--quality-gate-json <...>] [--no-advance] [--json]
|
|
1263
|
+
omo ulw-loop steer --kind <kind> ... --evidence "..." --rationale "..." [--proposals-json <json-or-path>] [--json]
|
|
1264
|
+
omo ulw-loop add-goal --title "..." --objective "..." [--json]
|
|
1265
|
+
omo ulw-loop record-review-blockers --goal-id <id> --title "..." --objective "..." --evidence "..." --codex-goal-json <...> [--json]
|
|
1227
1266
|
|
|
1228
|
-
|
|
1267
|
+
All subcommands accept [--session-id <id>] to isolate state under .omo/ulw-loop/<id>/; without it, Codex session env is used when present.`;
|
|
1268
|
+
function printJson(value) {
|
|
1269
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}
|
|
1270
|
+
`);
|
|
1229
1271
|
}
|
|
1230
|
-
function
|
|
1231
|
-
if (
|
|
1272
|
+
function printJsonError(error) {
|
|
1273
|
+
if (error instanceof UlwLoopError) {
|
|
1274
|
+
printJson({
|
|
1275
|
+
ok: false,
|
|
1276
|
+
error: {
|
|
1277
|
+
code: error.code,
|
|
1278
|
+
message: error.message,
|
|
1279
|
+
...error.details === undefined ? {} : { details: error.details }
|
|
1280
|
+
}
|
|
1281
|
+
});
|
|
1232
1282
|
return;
|
|
1233
|
-
const trimmed = value.trim();
|
|
1234
|
-
if (trimmed.length > 0)
|
|
1235
|
-
return trimmed;
|
|
1236
|
-
return fail(`Empty ${field}.`, "ULW_LOOP_STEERING_FIELD_EMPTY", { field });
|
|
1237
|
-
}
|
|
1238
|
-
function required2(argv, flag) {
|
|
1239
|
-
const value = text(readValue(argv, flag), flag);
|
|
1240
|
-
return value ?? fail(`Missing ${flag}.`, "ULW_LOOP_STEERING_FIELD_REQUIRED", { flag });
|
|
1241
|
-
}
|
|
1242
|
-
function requiredGoal(argv) {
|
|
1243
|
-
const value = text(parseGoalArg(argv), "--goal-id");
|
|
1244
|
-
return value ?? fail("Missing --goal-id.", "ULW_LOOP_GOAL_ID_REQUIRED", { flag: "--goal-id" });
|
|
1245
|
-
}
|
|
1246
|
-
function readObject(value, key) {
|
|
1247
|
-
return Object.entries(value).find(([name]) => name === key)?.[1];
|
|
1248
|
-
}
|
|
1249
|
-
function isPlain(value) {
|
|
1250
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1251
|
-
}
|
|
1252
|
-
function objectText(value, key) {
|
|
1253
|
-
const candidate = readObject(value, key);
|
|
1254
|
-
return typeof candidate === "string" ? candidate : undefined;
|
|
1255
|
-
}
|
|
1256
|
-
function parseSteeringKind(argv) {
|
|
1257
|
-
const value = readValue(argv, "--kind");
|
|
1258
|
-
if (isKind(value))
|
|
1259
|
-
return value;
|
|
1260
|
-
return value === undefined ? fail(kindMessage("Missing --kind."), "ULW_LOOP_STEERING_KIND_REQUIRED", { flag: "--kind", expected: ULW_LOOP_STEERING_MUTATION_KINDS, usage: STEERING_KIND_HELP }) : fail(kindMessage(`Invalid --kind: ${value}.`), "ULW_LOOP_STEERING_KIND_INVALID", { value, expected: ULW_LOOP_STEERING_MUTATION_KINDS, usage: STEERING_KIND_HELP });
|
|
1261
|
-
}
|
|
1262
|
-
function parseSteeringSource(argv) {
|
|
1263
|
-
const value = readValue(argv, "--source");
|
|
1264
|
-
if (value === undefined)
|
|
1265
|
-
return "cli";
|
|
1266
|
-
return isSource(value) ? value : fail(`Invalid --source: ${value}.`, "ULW_LOOP_STEERING_SOURCE_INVALID", { value, expected: SOURCES });
|
|
1267
|
-
}
|
|
1268
|
-
function child(value) {
|
|
1269
|
-
if (!isPlain(value))
|
|
1270
|
-
return null;
|
|
1271
|
-
const title = text(objectText(value, "title"), "title");
|
|
1272
|
-
const objective = text(objectText(value, "objective"), "objective");
|
|
1273
|
-
if (title === undefined || objective === undefined)
|
|
1274
|
-
return null;
|
|
1275
|
-
return { title, objective };
|
|
1276
|
-
}
|
|
1277
|
-
async function children(argv, flag, needed) {
|
|
1278
|
-
const input = needed ? required2(argv, flag) : text(readValue(argv, flag), flag);
|
|
1279
|
-
if (input === undefined)
|
|
1280
|
-
return [];
|
|
1281
|
-
const raw = await readJsonInput(input);
|
|
1282
|
-
if (!Array.isArray(raw))
|
|
1283
|
-
return fail(`${flag} must be a JSON array.`, "ULW_LOOP_STEERING_JSON_ARRAY_REQUIRED", { flag });
|
|
1284
|
-
const parsed = [];
|
|
1285
|
-
for (const item of raw) {
|
|
1286
|
-
const next = child(item);
|
|
1287
|
-
if (next === null)
|
|
1288
|
-
return fail(`${flag} entries require title/objective.`, "ULW_LOOP_STEERING_CHILD_INVALID", { flag });
|
|
1289
|
-
parsed.push(next);
|
|
1290
1283
|
}
|
|
1291
|
-
|
|
1292
|
-
}
|
|
1293
|
-
async function stringArray2(argv, flag) {
|
|
1294
|
-
const raw = await readJsonInput(required2(argv, flag));
|
|
1295
|
-
if (!Array.isArray(raw))
|
|
1296
|
-
return fail(`${flag} must be a JSON array.`, "ULW_LOOP_STEERING_JSON_ARRAY_REQUIRED", { flag });
|
|
1297
|
-
const values = [];
|
|
1298
|
-
for (const item of raw) {
|
|
1299
|
-
if (typeof item !== "string")
|
|
1300
|
-
return fail(`${flag} entries must be strings.`, "ULW_LOOP_STEERING_STRING_ARRAY_REQUIRED", { flag });
|
|
1301
|
-
values.push(text(item, flag) ?? "");
|
|
1302
|
-
}
|
|
1303
|
-
return values;
|
|
1304
|
-
}
|
|
1305
|
-
function model(value) {
|
|
1306
|
-
const trimmed = text(value, "--user-model");
|
|
1307
|
-
if (trimmed === undefined)
|
|
1284
|
+
if (error instanceof Error) {
|
|
1285
|
+
printJson({ ok: false, error: { code: "ULW_LOOP_UNEXPECTED", message: error.message } });
|
|
1308
1286
|
return;
|
|
1309
|
-
return isModel(trimmed) ? trimmed : fail(`Invalid --user-model: ${trimmed}.`, "ULW_LOOP_STEERING_USER_MODEL_INVALID", { value: trimmed, expected: ULW_LOOP_SUCCESS_CRITERION_USER_MODELS });
|
|
1310
|
-
}
|
|
1311
|
-
function neverKind(kind) {
|
|
1312
|
-
return fail(`Unsupported steering kind: ${String(kind)}.`, "ULW_LOOP_STEERING_KIND_UNSUPPORTED", { kind });
|
|
1313
|
-
}
|
|
1314
|
-
async function parseSteeringProposal(argv) {
|
|
1315
|
-
const kind = parseSteeringKind(argv);
|
|
1316
|
-
const source = parseSteeringSource(argv);
|
|
1317
|
-
const idempotencyKey = text(readValue(argv, "--idempotency-key"), "--idempotency-key");
|
|
1318
|
-
const base = { kind, source, evidence: required2(argv, "--evidence"), rationale: required2(argv, "--rationale"), ...idempotencyKey === undefined ? {} : { idempotencyKey } };
|
|
1319
|
-
switch (kind) {
|
|
1320
|
-
case "add_subgoal":
|
|
1321
|
-
return normalizeSteeringProposal({ ...base, title: required2(argv, "--title"), objective: required2(argv, "--objective") });
|
|
1322
|
-
case "split_subgoal": {
|
|
1323
|
-
const goalId = requiredGoal(argv);
|
|
1324
|
-
return normalizeSteeringProposal({ ...base, goalId, targetGoalId: goalId, childGoals: await children(argv, "--children", true) });
|
|
1325
|
-
}
|
|
1326
|
-
case "reorder_pending":
|
|
1327
|
-
return normalizeSteeringProposal({ ...base, pendingOrder: await stringArray2(argv, "--order") });
|
|
1328
|
-
case "revise_pending_wording": {
|
|
1329
|
-
const goalId = requiredGoal(argv);
|
|
1330
|
-
const revisedTitle = readValue(argv, "--title");
|
|
1331
|
-
const revisedObjective = readValue(argv, "--objective");
|
|
1332
|
-
if (revisedTitle === undefined && revisedObjective === undefined)
|
|
1333
|
-
return fail("revise_pending_wording requires --title or --objective.", "ULW_LOOP_STEERING_UPDATE_REQUIRED", { kind });
|
|
1334
|
-
return normalizeSteeringProposal({ ...base, goalId, targetGoalId: goalId, ...revisedTitle === undefined ? {} : { revisedTitle }, ...revisedObjective === undefined ? {} : { revisedObjective } });
|
|
1335
|
-
}
|
|
1336
|
-
case "revise_criterion": {
|
|
1337
|
-
const goalId = requiredGoal(argv);
|
|
1338
|
-
const criterionId = required2(argv, "--criterion-id");
|
|
1339
|
-
const scenario = readValue(argv, "--scenario");
|
|
1340
|
-
const expectedEvidence = readValue(argv, "--expected-evidence");
|
|
1341
|
-
const userModel = model(readValue(argv, "--user-model"));
|
|
1342
|
-
if (scenario === undefined && expectedEvidence === undefined && userModel === undefined)
|
|
1343
|
-
return fail("revise_criterion requires scenario, expected-evidence, or user-model.", "ULW_LOOP_STEERING_UPDATE_REQUIRED", { kind });
|
|
1344
|
-
return normalizeSteeringProposal({ ...base, goalId, targetGoalId: goalId, criterionId, ...scenario === undefined ? {} : { scenario }, ...expectedEvidence === undefined ? {} : { expectedEvidence }, ...userModel === undefined ? {} : { userModel } });
|
|
1345
|
-
}
|
|
1346
|
-
case "annotate_ledger":
|
|
1347
|
-
return normalizeSteeringProposal(base);
|
|
1348
|
-
case "mark_blocked_superseded": {
|
|
1349
|
-
const goalId = requiredGoal(argv);
|
|
1350
|
-
const childGoals = await children(argv, "--replacements", false);
|
|
1351
|
-
return normalizeSteeringProposal({ ...base, goalId, targetGoalId: goalId, ...childGoals.length === 0 ? {} : { childGoals } });
|
|
1352
|
-
}
|
|
1353
|
-
default:
|
|
1354
|
-
return neverKind(kind);
|
|
1355
1287
|
}
|
|
1288
|
+
printJson({ ok: false, error: { code: "ULW_LOOP_UNKNOWN", message: "unknown error" } });
|
|
1356
1289
|
}
|
|
1357
|
-
function
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
if (values === undefined)
|
|
1364
|
-
return;
|
|
1365
|
-
return values.map((value) => text(value, field) ?? "");
|
|
1366
|
-
}
|
|
1367
|
-
function normalizeSteeringProposal(proposal) {
|
|
1368
|
-
const evidence = text(proposal.evidence, "evidence") ?? "";
|
|
1369
|
-
const rationale = text(proposal.rationale, "rationale") ?? "";
|
|
1370
|
-
const goalId = text(proposal.goalId, "goalId");
|
|
1371
|
-
const targetGoalId = text(proposal.targetGoalId, "targetGoalId");
|
|
1372
|
-
const targetGoalIds = normalizedStrings(proposal.targetGoalIds, "targetGoalIds");
|
|
1373
|
-
const criterionId = text(proposal.criterionId, "criterionId");
|
|
1374
|
-
const title = text(proposal.title, "title");
|
|
1375
|
-
const objective = text(proposal.objective, "objective");
|
|
1376
|
-
const revisedTitle = text(proposal.revisedTitle, "revisedTitle");
|
|
1377
|
-
const revisedObjective = text(proposal.revisedObjective, "revisedObjective");
|
|
1378
|
-
const blockedReason = text(proposal.blockedReason, "blockedReason");
|
|
1379
|
-
const directiveText = text(proposal.directiveText, "directiveText");
|
|
1380
|
-
const promptSignature = text(proposal.promptSignature, "promptSignature");
|
|
1381
|
-
const idempotencyKey = text(proposal.idempotencyKey, "idempotencyKey");
|
|
1382
|
-
const scenario = text(proposal.scenario, "scenario");
|
|
1383
|
-
const expectedEvidence = text(proposal.expectedEvidence, "expectedEvidence");
|
|
1384
|
-
const childGoals = normalizedChildren(proposal.childGoals);
|
|
1385
|
-
const pendingOrder = normalizedStrings(proposal.pendingOrder, "pendingOrder");
|
|
1386
|
-
return { kind: proposal.kind, source: proposal.source, evidence, rationale, ...goalId === undefined ? {} : { goalId }, ...targetGoalId === undefined ? {} : { targetGoalId }, ...targetGoalIds === undefined ? {} : { targetGoalIds }, ...criterionId === undefined ? {} : { criterionId }, ...title === undefined ? {} : { title }, ...objective === undefined ? {} : { objective }, ...childGoals === undefined ? {} : { childGoals }, ...revisedTitle === undefined ? {} : { revisedTitle }, ...revisedObjective === undefined ? {} : { revisedObjective }, ...pendingOrder === undefined ? {} : { pendingOrder }, ...blockedReason === undefined ? {} : { blockedReason }, ...proposal.after === undefined ? {} : { after: proposal.after }, ...directiveText === undefined ? {} : { directiveText }, ...promptSignature === undefined ? {} : { promptSignature }, ...idempotencyKey === undefined ? {} : { idempotencyKey }, ...proposal.now === undefined ? {} : { now: proposal.now }, ...scenario === undefined ? {} : { scenario }, ...expectedEvidence === undefined ? {} : { expectedEvidence }, ...proposal.userModel === undefined ? {} : { userModel: proposal.userModel } };
|
|
1290
|
+
function criteriaCounts(goal) {
|
|
1291
|
+
let pass = 0;
|
|
1292
|
+
for (const criterion of goal.successCriteria)
|
|
1293
|
+
if (criterion.status === "pass")
|
|
1294
|
+
pass += 1;
|
|
1295
|
+
return { pass, total: goal.successCriteria.length };
|
|
1387
1296
|
}
|
|
1388
|
-
function
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1297
|
+
function printStatus(plan) {
|
|
1298
|
+
let totalCriteria = 0;
|
|
1299
|
+
let passCriteria = 0;
|
|
1300
|
+
const lines = ["ulw-loop status", "", "goals:"];
|
|
1301
|
+
for (const goal of plan.goals) {
|
|
1302
|
+
const counts = criteriaCounts(goal);
|
|
1303
|
+
totalCriteria += counts.total;
|
|
1304
|
+
passCriteria += counts.pass;
|
|
1305
|
+
const marker = goal.id === plan.activeGoalId ? "*" : "-";
|
|
1306
|
+
lines.push(`${marker} ${goal.id} [${goal.status}] ${goal.title} (criteria: ${counts.pass}/${counts.total})`);
|
|
1392
1307
|
}
|
|
1393
|
-
|
|
1394
|
-
process.stdout.write(
|
|
1395
|
-
`)
|
|
1396
|
-
if (result.rejectedReasons.length > 0)
|
|
1397
|
-
process.stdout.write(`rejected: ${result.rejectedReasons.join("; ")}
|
|
1308
|
+
lines.push("", "summary:", `total goals: ${plan.goals.length}`, `criteria: ${passCriteria}/${totalCriteria} pass`);
|
|
1309
|
+
process.stdout.write(`${lines.join(`
|
|
1310
|
+
`)}
|
|
1398
1311
|
`);
|
|
1399
|
-
|
|
1400
|
-
|
|
1312
|
+
}
|
|
1313
|
+
function blockedDecisionHandoff(plan) {
|
|
1314
|
+
const blocked = plan.goals.find((goal) => goal.status === "needs_user_decision" && goal.nonRetriable);
|
|
1315
|
+
if (blocked === undefined)
|
|
1316
|
+
return "";
|
|
1317
|
+
return [
|
|
1318
|
+
"ulw-loop: blocked on repeated external authorization; no retryable failed goals remain.",
|
|
1319
|
+
`Goal: ${blocked.id} - ${blocked.title}`,
|
|
1320
|
+
`Required external decision: ${blocked.requiredExternalDecision ?? "provide the missing authorization or choose a different unblock path"}.`,
|
|
1321
|
+
"Do not run complete-goals --retry-failed again until external state changes or the user authorizes an unblock path."
|
|
1322
|
+
].join(`
|
|
1401
1323
|
`);
|
|
1402
|
-
|
|
1324
|
+
}
|
|
1325
|
+
function normalizeCodexGoalMode(value) {
|
|
1326
|
+
if (value === undefined)
|
|
1327
|
+
return "aggregate";
|
|
1328
|
+
if (value === "aggregate" || value === "per_story")
|
|
1329
|
+
return value;
|
|
1330
|
+
throw new UlwLoopError("Invalid --codex-goal-mode; expected aggregate or per_story.", "ULW_LOOP_CODEX_GOAL_MODE_INVALID", { details: { value } });
|
|
1403
1331
|
}
|
|
1404
1332
|
|
|
1405
1333
|
// components/ulw-loop/src/codex-goal-instruction.ts
|
|
@@ -1645,6 +1573,9 @@ async function createUlwLoopPlan(repoRoot, args, scope) {
|
|
|
1645
1573
|
codexGoalMode: args.codexGoalMode ?? "aggregate",
|
|
1646
1574
|
goals
|
|
1647
1575
|
};
|
|
1576
|
+
const validationBatches = await parseValidationBatches(args.validationBatchesJson, goals);
|
|
1577
|
+
if (validationBatches !== undefined)
|
|
1578
|
+
plan.validationBatches = validationBatches;
|
|
1648
1579
|
if (plan.codexGoalMode === "aggregate")
|
|
1649
1580
|
plan.codexObjective = aggregateCodexObjectiveForScope(scope);
|
|
1650
1581
|
await mkdir2(ulwLoopDir(repoRoot, scope), { recursive: true });
|
|
@@ -1729,7 +1660,380 @@ function summarizeUlwLoopPlan(plan) {
|
|
|
1729
1660
|
fail: countCriteria("fail"),
|
|
1730
1661
|
blocked: countCriteria("blocked")
|
|
1731
1662
|
}
|
|
1732
|
-
};
|
|
1663
|
+
};
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
// components/ulw-loop/src/checkpoint-continuation.ts
|
|
1667
|
+
async function checkpointAndContinue(repoRoot, args, scope) {
|
|
1668
|
+
const result = await checkpointUlwLoop(repoRoot, args, scope);
|
|
1669
|
+
if (args.status !== "complete" || result.aggregateCompletion !== undefined || !args.advance)
|
|
1670
|
+
return result;
|
|
1671
|
+
const next = await startNextUlwLoop(repoRoot, {}, scope);
|
|
1672
|
+
if ("done" in next)
|
|
1673
|
+
return { ...result, plan: next.plan, next: doneNext(next.plan) };
|
|
1674
|
+
const instruction = buildCodexGoalInstruction({ plan: next.plan, goal: next.goal });
|
|
1675
|
+
return { ...result, plan: next.plan, next: { resumed: next.resumed, goal: next.goal, instruction } };
|
|
1676
|
+
}
|
|
1677
|
+
async function checkpoint(repoRoot, argv, json, scope) {
|
|
1678
|
+
const goalId = required2(argv, "--goal-id");
|
|
1679
|
+
const statusValue = checkpointStatus(required2(argv, "--status"));
|
|
1680
|
+
const evidence = required2(argv, "--evidence");
|
|
1681
|
+
const codexGoalJson = await parseCodexGoalJson(statusValue === "complete" ? required2(argv, "--codex-goal-json") : readValue(argv, "--codex-goal-json"));
|
|
1682
|
+
if (statusValue === "complete" && codexGoalJson === undefined) {
|
|
1683
|
+
throw new UlwLoopError("Missing --codex-goal-json.", "ULW_LOOP_CODEX_GOAL_JSON_REQUIRED");
|
|
1684
|
+
}
|
|
1685
|
+
const qualityGateJson = readValue(argv, "--quality-gate-json");
|
|
1686
|
+
const args = {
|
|
1687
|
+
goalId,
|
|
1688
|
+
status: statusValue,
|
|
1689
|
+
evidence,
|
|
1690
|
+
advance: !hasFlag(argv, "--no-advance"),
|
|
1691
|
+
...codexGoalJson === undefined ? {} : { codexGoalJson },
|
|
1692
|
+
...qualityGateJson === undefined ? {} : { qualityGateJson }
|
|
1693
|
+
};
|
|
1694
|
+
const result = await checkpointAndContinue(repoRoot, args, scope);
|
|
1695
|
+
if (json)
|
|
1696
|
+
printJson({ ok: true, ...result, summary: summarizeUlwLoopPlan(result.plan) });
|
|
1697
|
+
else
|
|
1698
|
+
printCheckpointText(result);
|
|
1699
|
+
return 0;
|
|
1700
|
+
}
|
|
1701
|
+
function printCheckpointText(result) {
|
|
1702
|
+
process.stdout.write(`ulw-loop checkpoint: ${result.goal.id} -> ${result.goal.status}
|
|
1703
|
+
`);
|
|
1704
|
+
if (result.next === undefined)
|
|
1705
|
+
return;
|
|
1706
|
+
if ("instruction" in result.next)
|
|
1707
|
+
process.stdout.write(`${result.next.instruction.text}
|
|
1708
|
+
`);
|
|
1709
|
+
else
|
|
1710
|
+
process.stdout.write(`${result.next.handoff || "ulw-loop: all goals complete"}
|
|
1711
|
+
`);
|
|
1712
|
+
}
|
|
1713
|
+
function doneNext(plan) {
|
|
1714
|
+
const handoff = blockedDecisionHandoff(plan);
|
|
1715
|
+
return { done: true, blocked: handoff.length > 0, handoff };
|
|
1716
|
+
}
|
|
1717
|
+
function required2(argv, flag) {
|
|
1718
|
+
const value = readValue(argv, flag)?.trim();
|
|
1719
|
+
if (value)
|
|
1720
|
+
return value;
|
|
1721
|
+
throw new UlwLoopError(`Missing ${flag}.`, "ULW_LOOP_ARGUMENT_MISSING", { details: { flag } });
|
|
1722
|
+
}
|
|
1723
|
+
function checkpointStatus(value) {
|
|
1724
|
+
if (value === "complete" || value === "failed" || value === "blocked")
|
|
1725
|
+
return value;
|
|
1726
|
+
throw new UlwLoopError("Missing or invalid --status; expected complete, failed, or blocked.", "ULW_LOOP_STATUS_INVALID", { details: { status: value } });
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
// components/ulw-loop/src/cli-subcommands.ts
|
|
1730
|
+
import { readFile as readFile6 } from "node:fs/promises";
|
|
1731
|
+
|
|
1732
|
+
// components/ulw-loop/src/cli-steering.ts
|
|
1733
|
+
var SOURCES = ["user_prompt_submit", "finding", "cli"];
|
|
1734
|
+
var STEERING_KIND_HELP = [
|
|
1735
|
+
`Allowed --kind values: ${ULW_LOOP_STEERING_MUTATION_KINDS.join(", ")}`,
|
|
1736
|
+
"Kind-specific required flags:",
|
|
1737
|
+
" add_subgoal: --title, --objective, --evidence, --rationale",
|
|
1738
|
+
" split_subgoal: --goal-id, --children, --evidence, --rationale",
|
|
1739
|
+
" reorder_pending: --order, --evidence, --rationale",
|
|
1740
|
+
" revise_pending_wording: --goal-id, --title or --objective, --evidence, --rationale",
|
|
1741
|
+
" revise_criterion: --goal-id, --criterion-id, one of --scenario/--expected-evidence/--user-model, --evidence, --rationale",
|
|
1742
|
+
" annotate_ledger: --evidence, --rationale",
|
|
1743
|
+
" mark_blocked_superseded: --goal-id, optional --replacements, --evidence, --rationale",
|
|
1744
|
+
'Example: omo ulw-loop steer --kind annotate_ledger --evidence "observed behavior" --rationale "why this changes the plan" --json'
|
|
1745
|
+
].join(`
|
|
1746
|
+
`);
|
|
1747
|
+
function isKind(value) {
|
|
1748
|
+
return value !== undefined && ULW_LOOP_STEERING_MUTATION_KINDS.some((kind) => kind === value);
|
|
1749
|
+
}
|
|
1750
|
+
function isSource(value) {
|
|
1751
|
+
return value !== undefined && SOURCES.some((source) => source === value);
|
|
1752
|
+
}
|
|
1753
|
+
function isModel(value) {
|
|
1754
|
+
return ULW_LOOP_SUCCESS_CRITERION_USER_MODELS.some((model) => model === value);
|
|
1755
|
+
}
|
|
1756
|
+
function fail2(message, code, details) {
|
|
1757
|
+
throw new UlwLoopError(message, code, { details });
|
|
1758
|
+
}
|
|
1759
|
+
function kindMessage(prefix) {
|
|
1760
|
+
return `${prefix}
|
|
1761
|
+
|
|
1762
|
+
${STEERING_KIND_HELP}`;
|
|
1763
|
+
}
|
|
1764
|
+
function text2(value, field) {
|
|
1765
|
+
if (value === undefined)
|
|
1766
|
+
return;
|
|
1767
|
+
const trimmed = value.trim();
|
|
1768
|
+
if (trimmed.length > 0)
|
|
1769
|
+
return trimmed;
|
|
1770
|
+
return fail2(`Empty ${field}.`, "ULW_LOOP_STEERING_FIELD_EMPTY", { field });
|
|
1771
|
+
}
|
|
1772
|
+
function required3(argv, flag) {
|
|
1773
|
+
const value = text2(readValue(argv, flag), flag);
|
|
1774
|
+
return value ?? fail2(`Missing ${flag}.`, "ULW_LOOP_STEERING_FIELD_REQUIRED", { flag });
|
|
1775
|
+
}
|
|
1776
|
+
function requiredGoal(argv) {
|
|
1777
|
+
const value = text2(parseGoalArg(argv), "--goal-id");
|
|
1778
|
+
return value ?? fail2("Missing --goal-id.", "ULW_LOOP_GOAL_ID_REQUIRED", { flag: "--goal-id" });
|
|
1779
|
+
}
|
|
1780
|
+
function readObject(value, key) {
|
|
1781
|
+
return Object.entries(value).find(([name]) => name === key)?.[1];
|
|
1782
|
+
}
|
|
1783
|
+
function isPlain(value) {
|
|
1784
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1785
|
+
}
|
|
1786
|
+
function objectText(value, key) {
|
|
1787
|
+
const candidate = readObject(value, key);
|
|
1788
|
+
return typeof candidate === "string" ? candidate : undefined;
|
|
1789
|
+
}
|
|
1790
|
+
function objectStrings(value, key) {
|
|
1791
|
+
const candidate = readObject(value, key);
|
|
1792
|
+
return Array.isArray(candidate) && candidate.every((item) => typeof item === "string") ? candidate : undefined;
|
|
1793
|
+
}
|
|
1794
|
+
function objectChildren(value, key) {
|
|
1795
|
+
const candidate = readObject(value, key);
|
|
1796
|
+
if (!Array.isArray(candidate))
|
|
1797
|
+
return;
|
|
1798
|
+
const parsed = [];
|
|
1799
|
+
for (const item of candidate) {
|
|
1800
|
+
const next = child(item);
|
|
1801
|
+
if (next === null)
|
|
1802
|
+
return fail2(`${key} entries require title/objective.`, "ULW_LOOP_STEERING_CHILD_INVALID", { key });
|
|
1803
|
+
parsed.push(next);
|
|
1804
|
+
}
|
|
1805
|
+
return parsed;
|
|
1806
|
+
}
|
|
1807
|
+
function isProposalKind(value) {
|
|
1808
|
+
return typeof value === "string" && ULW_LOOP_STEERING_MUTATION_KINDS.some((kind) => kind === value);
|
|
1809
|
+
}
|
|
1810
|
+
function isProposalSource(value) {
|
|
1811
|
+
return typeof value === "string" && SOURCES.some((source) => source === value);
|
|
1812
|
+
}
|
|
1813
|
+
function parseSteeringKind(argv) {
|
|
1814
|
+
const value = readValue(argv, "--kind");
|
|
1815
|
+
if (isKind(value))
|
|
1816
|
+
return value;
|
|
1817
|
+
return value === undefined ? fail2(kindMessage("Missing --kind."), "ULW_LOOP_STEERING_KIND_REQUIRED", { flag: "--kind", expected: ULW_LOOP_STEERING_MUTATION_KINDS, usage: STEERING_KIND_HELP }) : fail2(kindMessage(`Invalid --kind: ${value}.`), "ULW_LOOP_STEERING_KIND_INVALID", { value, expected: ULW_LOOP_STEERING_MUTATION_KINDS, usage: STEERING_KIND_HELP });
|
|
1818
|
+
}
|
|
1819
|
+
function parseSteeringSource(argv) {
|
|
1820
|
+
const value = readValue(argv, "--source");
|
|
1821
|
+
if (value === undefined)
|
|
1822
|
+
return "cli";
|
|
1823
|
+
return isSource(value) ? value : fail2(`Invalid --source: ${value}.`, "ULW_LOOP_STEERING_SOURCE_INVALID", { value, expected: SOURCES });
|
|
1824
|
+
}
|
|
1825
|
+
function child(value) {
|
|
1826
|
+
if (!isPlain(value))
|
|
1827
|
+
return null;
|
|
1828
|
+
const title = text2(objectText(value, "title"), "title");
|
|
1829
|
+
const objective = text2(objectText(value, "objective"), "objective");
|
|
1830
|
+
if (title === undefined || objective === undefined)
|
|
1831
|
+
return null;
|
|
1832
|
+
return { title, objective };
|
|
1833
|
+
}
|
|
1834
|
+
async function children(argv, flag, needed) {
|
|
1835
|
+
const input = needed ? required3(argv, flag) : text2(readValue(argv, flag), flag);
|
|
1836
|
+
if (input === undefined)
|
|
1837
|
+
return [];
|
|
1838
|
+
const raw = await readJsonInput(input);
|
|
1839
|
+
if (!Array.isArray(raw))
|
|
1840
|
+
return fail2(`${flag} must be a JSON array.`, "ULW_LOOP_STEERING_JSON_ARRAY_REQUIRED", { flag });
|
|
1841
|
+
const parsed = [];
|
|
1842
|
+
for (const item of raw) {
|
|
1843
|
+
const next = child(item);
|
|
1844
|
+
if (next === null)
|
|
1845
|
+
return fail2(`${flag} entries require title/objective.`, "ULW_LOOP_STEERING_CHILD_INVALID", { flag });
|
|
1846
|
+
parsed.push(next);
|
|
1847
|
+
}
|
|
1848
|
+
return parsed;
|
|
1849
|
+
}
|
|
1850
|
+
async function stringArray2(argv, flag) {
|
|
1851
|
+
const raw = await readJsonInput(required3(argv, flag));
|
|
1852
|
+
if (!Array.isArray(raw))
|
|
1853
|
+
return fail2(`${flag} must be a JSON array.`, "ULW_LOOP_STEERING_JSON_ARRAY_REQUIRED", { flag });
|
|
1854
|
+
const values = [];
|
|
1855
|
+
for (const item of raw) {
|
|
1856
|
+
if (typeof item !== "string")
|
|
1857
|
+
return fail2(`${flag} entries must be strings.`, "ULW_LOOP_STEERING_STRING_ARRAY_REQUIRED", { flag });
|
|
1858
|
+
values.push(text2(item, flag) ?? "");
|
|
1859
|
+
}
|
|
1860
|
+
return values;
|
|
1861
|
+
}
|
|
1862
|
+
function model(value) {
|
|
1863
|
+
const trimmed = text2(value, "--user-model");
|
|
1864
|
+
if (trimmed === undefined)
|
|
1865
|
+
return;
|
|
1866
|
+
return isModel(trimmed) ? trimmed : fail2(`Invalid --user-model: ${trimmed}.`, "ULW_LOOP_STEERING_USER_MODEL_INVALID", { value: trimmed, expected: ULW_LOOP_SUCCESS_CRITERION_USER_MODELS });
|
|
1867
|
+
}
|
|
1868
|
+
function neverKind(kind) {
|
|
1869
|
+
return fail2(`Unsupported steering kind: ${String(kind)}.`, "ULW_LOOP_STEERING_KIND_UNSUPPORTED", { kind });
|
|
1870
|
+
}
|
|
1871
|
+
async function parseSteeringProposal(argv) {
|
|
1872
|
+
const kind = parseSteeringKind(argv);
|
|
1873
|
+
const source = parseSteeringSource(argv);
|
|
1874
|
+
const idempotencyKey = text2(readValue(argv, "--idempotency-key"), "--idempotency-key");
|
|
1875
|
+
const base = { kind, source, evidence: required3(argv, "--evidence"), rationale: required3(argv, "--rationale"), ...idempotencyKey === undefined ? {} : { idempotencyKey } };
|
|
1876
|
+
switch (kind) {
|
|
1877
|
+
case "add_subgoal":
|
|
1878
|
+
return normalizeSteeringProposal({ ...base, title: required3(argv, "--title"), objective: required3(argv, "--objective") });
|
|
1879
|
+
case "split_subgoal": {
|
|
1880
|
+
const goalId = requiredGoal(argv);
|
|
1881
|
+
return normalizeSteeringProposal({ ...base, goalId, targetGoalId: goalId, childGoals: await children(argv, "--children", true) });
|
|
1882
|
+
}
|
|
1883
|
+
case "reorder_pending":
|
|
1884
|
+
return normalizeSteeringProposal({ ...base, pendingOrder: await stringArray2(argv, "--order") });
|
|
1885
|
+
case "revise_pending_wording": {
|
|
1886
|
+
const goalId = requiredGoal(argv);
|
|
1887
|
+
const revisedTitle = readValue(argv, "--title");
|
|
1888
|
+
const revisedObjective = readValue(argv, "--objective");
|
|
1889
|
+
if (revisedTitle === undefined && revisedObjective === undefined)
|
|
1890
|
+
return fail2("revise_pending_wording requires --title or --objective.", "ULW_LOOP_STEERING_UPDATE_REQUIRED", { kind });
|
|
1891
|
+
return normalizeSteeringProposal({ ...base, goalId, targetGoalId: goalId, ...revisedTitle === undefined ? {} : { revisedTitle }, ...revisedObjective === undefined ? {} : { revisedObjective } });
|
|
1892
|
+
}
|
|
1893
|
+
case "revise_criterion": {
|
|
1894
|
+
const goalId = requiredGoal(argv);
|
|
1895
|
+
const criterionId = required3(argv, "--criterion-id");
|
|
1896
|
+
const scenario = readValue(argv, "--scenario");
|
|
1897
|
+
const expectedEvidence = readValue(argv, "--expected-evidence");
|
|
1898
|
+
const userModel = model(readValue(argv, "--user-model"));
|
|
1899
|
+
if (scenario === undefined && expectedEvidence === undefined && userModel === undefined)
|
|
1900
|
+
return fail2("revise_criterion requires scenario, expected-evidence, or user-model.", "ULW_LOOP_STEERING_UPDATE_REQUIRED", { kind });
|
|
1901
|
+
return normalizeSteeringProposal({ ...base, goalId, targetGoalId: goalId, criterionId, ...scenario === undefined ? {} : { scenario }, ...expectedEvidence === undefined ? {} : { expectedEvidence }, ...userModel === undefined ? {} : { userModel } });
|
|
1902
|
+
}
|
|
1903
|
+
case "annotate_ledger":
|
|
1904
|
+
return normalizeSteeringProposal(base);
|
|
1905
|
+
case "mark_blocked_superseded": {
|
|
1906
|
+
const goalId = requiredGoal(argv);
|
|
1907
|
+
const childGoals = await children(argv, "--replacements", false);
|
|
1908
|
+
return normalizeSteeringProposal({ ...base, goalId, targetGoalId: goalId, ...childGoals.length === 0 ? {} : { childGoals } });
|
|
1909
|
+
}
|
|
1910
|
+
default:
|
|
1911
|
+
return neverKind(kind);
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
function normalizedChildren(values) {
|
|
1915
|
+
if (values === undefined)
|
|
1916
|
+
return;
|
|
1917
|
+
return values.map((item) => ({ title: text2(item.title, "child.title") ?? "", objective: text2(item.objective, "child.objective") ?? "" }));
|
|
1918
|
+
}
|
|
1919
|
+
function normalizedStrings(values, field) {
|
|
1920
|
+
if (values === undefined)
|
|
1921
|
+
return;
|
|
1922
|
+
return values.map((value) => text2(value, field) ?? "");
|
|
1923
|
+
}
|
|
1924
|
+
function normalizeSteeringProposal(proposal) {
|
|
1925
|
+
const evidence = text2(proposal.evidence, "evidence") ?? "";
|
|
1926
|
+
const rationale = text2(proposal.rationale, "rationale") ?? "";
|
|
1927
|
+
const goalId = text2(proposal.goalId, "goalId");
|
|
1928
|
+
const targetGoalId = text2(proposal.targetGoalId, "targetGoalId");
|
|
1929
|
+
const targetGoalIds = normalizedStrings(proposal.targetGoalIds, "targetGoalIds");
|
|
1930
|
+
const criterionId = text2(proposal.criterionId, "criterionId");
|
|
1931
|
+
const title = text2(proposal.title, "title");
|
|
1932
|
+
const objective = text2(proposal.objective, "objective");
|
|
1933
|
+
const revisedTitle = text2(proposal.revisedTitle, "revisedTitle");
|
|
1934
|
+
const revisedObjective = text2(proposal.revisedObjective, "revisedObjective");
|
|
1935
|
+
const blockedReason = text2(proposal.blockedReason, "blockedReason");
|
|
1936
|
+
const directiveText = text2(proposal.directiveText, "directiveText");
|
|
1937
|
+
const promptSignature = text2(proposal.promptSignature, "promptSignature");
|
|
1938
|
+
const idempotencyKey = text2(proposal.idempotencyKey, "idempotencyKey");
|
|
1939
|
+
const scenario = text2(proposal.scenario, "scenario");
|
|
1940
|
+
const expectedEvidence = text2(proposal.expectedEvidence, "expectedEvidence");
|
|
1941
|
+
const childGoals = normalizedChildren(proposal.childGoals);
|
|
1942
|
+
const pendingOrder = normalizedStrings(proposal.pendingOrder, "pendingOrder");
|
|
1943
|
+
return { kind: proposal.kind, source: proposal.source, evidence, rationale, ...goalId === undefined ? {} : { goalId }, ...targetGoalId === undefined ? {} : { targetGoalId }, ...targetGoalIds === undefined ? {} : { targetGoalIds }, ...criterionId === undefined ? {} : { criterionId }, ...title === undefined ? {} : { title }, ...objective === undefined ? {} : { objective }, ...childGoals === undefined ? {} : { childGoals }, ...revisedTitle === undefined ? {} : { revisedTitle }, ...revisedObjective === undefined ? {} : { revisedObjective }, ...pendingOrder === undefined ? {} : { pendingOrder }, ...blockedReason === undefined ? {} : { blockedReason }, ...proposal.after === undefined ? {} : { after: proposal.after }, ...directiveText === undefined ? {} : { directiveText }, ...promptSignature === undefined ? {} : { promptSignature }, ...idempotencyKey === undefined ? {} : { idempotencyKey }, ...proposal.now === undefined ? {} : { now: proposal.now }, ...scenario === undefined ? {} : { scenario }, ...expectedEvidence === undefined ? {} : { expectedEvidence }, ...proposal.userModel === undefined ? {} : { userModel: proposal.userModel } };
|
|
1944
|
+
}
|
|
1945
|
+
async function parseSteeringProposals(argv) {
|
|
1946
|
+
const input = text2(readValue(argv, "--proposals-json"), "--proposals-json");
|
|
1947
|
+
if (input === undefined)
|
|
1948
|
+
return [await parseSteeringProposal(argv)];
|
|
1949
|
+
if (readValue(argv, "--kind") !== undefined)
|
|
1950
|
+
return fail2("--kind and --proposals-json are mutually exclusive.", "ULW_LOOP_STEERING_BATCH_CONFLICT", { flags: ["--kind", "--proposals-json"] });
|
|
1951
|
+
const raw = await readJsonInput(input);
|
|
1952
|
+
if (!Array.isArray(raw) || raw.length === 0)
|
|
1953
|
+
return fail2("--proposals-json must be a non-empty JSON array.", "ULW_LOOP_STEERING_BATCH_ARRAY_REQUIRED", { flag: "--proposals-json" });
|
|
1954
|
+
const proposals = [];
|
|
1955
|
+
for (const item of raw)
|
|
1956
|
+
proposals.push(normalizeSteeringProposal(proposalFromObject(item)));
|
|
1957
|
+
return proposals;
|
|
1958
|
+
}
|
|
1959
|
+
function proposalFromObject(value) {
|
|
1960
|
+
if (!isPlain(value))
|
|
1961
|
+
return fail2("--proposals-json entries must be objects.", "ULW_LOOP_STEERING_BATCH_ITEM_INVALID", { flag: "--proposals-json" });
|
|
1962
|
+
const kind = readObject(value, "kind");
|
|
1963
|
+
const source = readObject(value, "source") ?? "cli";
|
|
1964
|
+
if (!isProposalKind(kind))
|
|
1965
|
+
return fail2(`Invalid batch steering kind: ${String(kind)}.`, "ULW_LOOP_STEERING_KIND_INVALID", { value: kind });
|
|
1966
|
+
if (!isProposalSource(source))
|
|
1967
|
+
return fail2(`Invalid batch steering source: ${String(source)}.`, "ULW_LOOP_STEERING_SOURCE_INVALID", { value: source });
|
|
1968
|
+
let proposal = { kind, source, evidence: objectText(value, "evidence") ?? "", rationale: objectText(value, "rationale") ?? "" };
|
|
1969
|
+
const goalId = objectText(value, "goalId");
|
|
1970
|
+
const targetGoalId = objectText(value, "targetGoalId");
|
|
1971
|
+
const criterionId = objectText(value, "criterionId");
|
|
1972
|
+
const title = objectText(value, "title");
|
|
1973
|
+
const objective = objectText(value, "objective");
|
|
1974
|
+
const revisedTitle = objectText(value, "revisedTitle");
|
|
1975
|
+
const revisedObjective = objectText(value, "revisedObjective");
|
|
1976
|
+
const scenario = objectText(value, "scenario");
|
|
1977
|
+
const expectedEvidence = objectText(value, "expectedEvidence");
|
|
1978
|
+
const idempotencyKey = objectText(value, "idempotencyKey");
|
|
1979
|
+
const targetGoalIds = objectStrings(value, "targetGoalIds");
|
|
1980
|
+
const pendingOrder = objectStrings(value, "pendingOrder");
|
|
1981
|
+
const childGoals = objectChildren(value, "childGoals");
|
|
1982
|
+
if (goalId !== undefined)
|
|
1983
|
+
proposal = { ...proposal, goalId };
|
|
1984
|
+
if (targetGoalId !== undefined)
|
|
1985
|
+
proposal = { ...proposal, targetGoalId };
|
|
1986
|
+
if (criterionId !== undefined)
|
|
1987
|
+
proposal = { ...proposal, criterionId };
|
|
1988
|
+
if (title !== undefined)
|
|
1989
|
+
proposal = { ...proposal, title };
|
|
1990
|
+
if (objective !== undefined)
|
|
1991
|
+
proposal = { ...proposal, objective };
|
|
1992
|
+
if (revisedTitle !== undefined)
|
|
1993
|
+
proposal = { ...proposal, revisedTitle };
|
|
1994
|
+
if (revisedObjective !== undefined)
|
|
1995
|
+
proposal = { ...proposal, revisedObjective };
|
|
1996
|
+
if (scenario !== undefined)
|
|
1997
|
+
proposal = { ...proposal, scenario };
|
|
1998
|
+
if (expectedEvidence !== undefined)
|
|
1999
|
+
proposal = { ...proposal, expectedEvidence };
|
|
2000
|
+
if (idempotencyKey !== undefined)
|
|
2001
|
+
proposal = { ...proposal, idempotencyKey };
|
|
2002
|
+
if (targetGoalIds !== undefined)
|
|
2003
|
+
proposal = { ...proposal, targetGoalIds };
|
|
2004
|
+
if (pendingOrder !== undefined)
|
|
2005
|
+
proposal = { ...proposal, pendingOrder };
|
|
2006
|
+
if (childGoals !== undefined)
|
|
2007
|
+
proposal = { ...proposal, childGoals };
|
|
2008
|
+
return proposal;
|
|
2009
|
+
}
|
|
2010
|
+
function printSteerResult(result, json) {
|
|
2011
|
+
if (json) {
|
|
2012
|
+
printJson({ ok: result.accepted, accepted: result.accepted, rejectedReasons: result.rejectedReasons, deduped: result.deduped, audit: result.audit, plan: result.plan });
|
|
2013
|
+
return;
|
|
2014
|
+
}
|
|
2015
|
+
const outcome = result.deduped ? "deduped" : result.accepted ? "accepted" : "rejected";
|
|
2016
|
+
process.stdout.write(`ulw-loop steer: ${outcome} ${result.audit.kind}
|
|
2017
|
+
`);
|
|
2018
|
+
if (result.rejectedReasons.length > 0)
|
|
2019
|
+
process.stdout.write(`rejected: ${result.rejectedReasons.join("; ")}
|
|
2020
|
+
`);
|
|
2021
|
+
if (result.audit.idempotencyKey !== undefined)
|
|
2022
|
+
process.stdout.write(`idempotency-key: ${result.audit.idempotencyKey}
|
|
2023
|
+
`);
|
|
2024
|
+
printStatus(result.plan);
|
|
2025
|
+
}
|
|
2026
|
+
function printSteerBatchResult(result, json) {
|
|
2027
|
+
if (json) {
|
|
2028
|
+
printJson({ ok: result.accepted, accepted: result.accepted, rejectedReasons: result.rejectedReasons, results: result.results, plan: result.plan });
|
|
2029
|
+
return;
|
|
2030
|
+
}
|
|
2031
|
+
process.stdout.write(`ulw-loop steer batch: ${result.accepted ? "accepted" : "rejected"} ${result.results.length} proposal(s)
|
|
2032
|
+
`);
|
|
2033
|
+
if (result.rejectedReasons.length > 0)
|
|
2034
|
+
process.stdout.write(`rejected: ${result.rejectedReasons.join("; ")}
|
|
2035
|
+
`);
|
|
2036
|
+
printStatus(result.plan);
|
|
1733
2037
|
}
|
|
1734
2038
|
|
|
1735
2039
|
// components/ulw-loop/src/review-blockers.ts
|
|
@@ -1798,24 +2102,111 @@ async function recordFinalReviewBlockers(repoRoot, args, scope) {
|
|
|
1798
2102
|
});
|
|
1799
2103
|
}
|
|
1800
2104
|
|
|
2105
|
+
// components/ulw-loop/src/steering-mutations.ts
|
|
2106
|
+
var read2 = (value, key) => Object.entries(value).find(([name]) => name === key)?.[1];
|
|
2107
|
+
var isText = (value) => typeof value === "string" && value.trim().length > 0;
|
|
2108
|
+
var text3 = (value, key) => {
|
|
2109
|
+
const candidate = read2(value, key);
|
|
2110
|
+
return isText(candidate) ? candidate.trim() : undefined;
|
|
2111
|
+
};
|
|
2112
|
+
var isModel2 = (value) => typeof value === "string" && ULW_LOOP_SUCCESS_CRITERION_USER_MODELS.some((model2) => model2 === value);
|
|
2113
|
+
var after = (proposal) => {
|
|
2114
|
+
const candidate = read2(proposal, "after");
|
|
2115
|
+
return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate) ? candidate : undefined;
|
|
2116
|
+
};
|
|
2117
|
+
var revised = (proposal, direct, nested) => text3(proposal, direct) ?? text3(after(proposal) ?? proposal, nested);
|
|
2118
|
+
var targets = (proposal) => proposal.targetGoalIds ?? [proposal.targetGoalId ?? text3(proposal, "goalId") ?? ""].filter(Boolean);
|
|
2119
|
+
var child2 = (value) => {
|
|
2120
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
2121
|
+
return null;
|
|
2122
|
+
const title = text3(value, "title");
|
|
2123
|
+
const objective = text3(value, "objective");
|
|
2124
|
+
return title === undefined || objective === undefined ? null : { title, objective };
|
|
2125
|
+
};
|
|
2126
|
+
var children2 = (proposal) => {
|
|
2127
|
+
const direct = proposal.childGoals;
|
|
2128
|
+
if (direct !== undefined && direct.length > 0)
|
|
2129
|
+
return direct;
|
|
2130
|
+
const nested = after(proposal);
|
|
2131
|
+
const fromAfter = nested === undefined ? undefined : read2(nested, "children");
|
|
2132
|
+
return Array.isArray(fromAfter) ? fromAfter.map(child2).filter((item) => item !== null) : [];
|
|
2133
|
+
};
|
|
2134
|
+
var goal = (plan, id) => id === undefined ? undefined : plan.goals.find((item) => item.id === id);
|
|
2135
|
+
function nextId(plan, offset) {
|
|
2136
|
+
const max = plan.goals.reduce((current, item) => {
|
|
2137
|
+
const digits = /^G(\d+)(?:-|$)/u.exec(item.id)?.[1];
|
|
2138
|
+
return digits === undefined ? current : Math.max(current, Number(digits));
|
|
2139
|
+
}, 0);
|
|
2140
|
+
return `G${String(max + offset).padStart(3, "0")}`;
|
|
2141
|
+
}
|
|
2142
|
+
function makeGoal2(plan, childGoal, evidence, now, offset) {
|
|
2143
|
+
const id = nextId(plan, offset);
|
|
2144
|
+
const digits = /^G(\d+)/u.exec(id)?.[1];
|
|
2145
|
+
const goalIndex = digits === undefined ? plan.goals.length + offset - 1 : Number(digits) - 1;
|
|
2146
|
+
return { id, title: childGoal.title, objective: childGoal.objective, status: "pending", successCriteria: seedDefaultSuccessCriteria(goalIndex, childGoal.objective), attempt: 0, createdAt: now, updatedAt: now, evidence };
|
|
2147
|
+
}
|
|
2148
|
+
function reviseWording(plan, proposal, now) {
|
|
2149
|
+
const target = goal(plan, targets(proposal)[0]);
|
|
2150
|
+
if (target === undefined)
|
|
2151
|
+
return;
|
|
2152
|
+
target.title = revised(proposal, "revisedTitle", "title") ?? target.title;
|
|
2153
|
+
target.objective = revised(proposal, "revisedObjective", "objective") ?? target.objective;
|
|
2154
|
+
target.steeringEvidence = proposal.evidence;
|
|
2155
|
+
target.steeringRationale = proposal.rationale;
|
|
2156
|
+
target.updatedAt = now;
|
|
2157
|
+
}
|
|
2158
|
+
function splitOrBlock(plan, proposal, now) {
|
|
2159
|
+
const target = goal(plan, targets(proposal)[0]);
|
|
2160
|
+
if (target === undefined)
|
|
2161
|
+
return;
|
|
2162
|
+
const replacements = children2(proposal).map((item, index) => makeGoal2(plan, item, proposal.evidence, now, index + 1));
|
|
2163
|
+
target.steeringEvidence = proposal.evidence;
|
|
2164
|
+
target.steeringRationale = proposal.rationale;
|
|
2165
|
+
target.updatedAt = now;
|
|
2166
|
+
if (replacements.length === 0) {
|
|
2167
|
+
target.status = "blocked";
|
|
2168
|
+
target.steeringStatus = "blocked";
|
|
2169
|
+
target.blockedReason = proposal.blockedReason ?? proposal.rationale;
|
|
2170
|
+
} else {
|
|
2171
|
+
target.steeringStatus = "superseded";
|
|
2172
|
+
target.supersededBy = replacements.map((item) => item.id);
|
|
2173
|
+
for (const item of replacements)
|
|
2174
|
+
item.supersedes = [target.id];
|
|
2175
|
+
plan.goals.splice(plan.goals.indexOf(target) + 1, 0, ...replacements);
|
|
2176
|
+
updateBatchesAfterSupersede(plan, target.id, replacements.map((item) => item.id));
|
|
2177
|
+
}
|
|
2178
|
+
if (plan.activeGoalId === target.id)
|
|
2179
|
+
delete plan.activeGoalId;
|
|
2180
|
+
}
|
|
2181
|
+
function reviseCriterion(plan, proposal, now) {
|
|
2182
|
+
const target = goal(plan, targets(proposal)[0]);
|
|
2183
|
+
const index = target?.successCriteria.findIndex((item) => item.id === proposal.criterionId) ?? -1;
|
|
2184
|
+
const current = target?.successCriteria[index];
|
|
2185
|
+
if (target === undefined || current === undefined)
|
|
2186
|
+
return;
|
|
2187
|
+
const model2 = read2(proposal, "userModel");
|
|
2188
|
+
target.successCriteria[index] = { ...current, scenario: text3(proposal, "scenario") ?? current.scenario, expectedEvidence: text3(proposal, "expectedEvidence") ?? current.expectedEvidence, userModel: isModel2(model2) ? model2 : current.userModel };
|
|
2189
|
+
target.updatedAt = now;
|
|
2190
|
+
}
|
|
2191
|
+
|
|
1801
2192
|
// components/ulw-loop/src/steering-snapshot.ts
|
|
1802
2193
|
function buildSteeringPlanSnapshot(plan, changedGoalIds) {
|
|
1803
2194
|
const snapshot = {
|
|
1804
2195
|
updatedAt: plan.updatedAt,
|
|
1805
2196
|
goalCount: plan.goals.length,
|
|
1806
|
-
goalIds: plan.goals.map((
|
|
1807
|
-
goals: plan.goals.filter((
|
|
2197
|
+
goalIds: plan.goals.map((goal2) => goal2.id),
|
|
2198
|
+
goals: plan.goals.filter((goal2) => changedGoalIds.has(goal2.id))
|
|
1808
2199
|
};
|
|
1809
2200
|
return plan.activeGoalId === undefined ? snapshot : { ...snapshot, activeGoalId: plan.activeGoalId };
|
|
1810
2201
|
}
|
|
1811
|
-
function changedGoalIdsBetween(before,
|
|
1812
|
-
const beforeById = new Map(before.goals.map((
|
|
2202
|
+
function changedGoalIdsBetween(before, after2) {
|
|
2203
|
+
const beforeById = new Map(before.goals.map((goal2) => [goal2.id, goal2]));
|
|
1813
2204
|
const changed = new Set;
|
|
1814
|
-
for (const
|
|
1815
|
-
const prior = beforeById.get(
|
|
1816
|
-
if (prior === undefined || JSON.stringify(prior) !== JSON.stringify(
|
|
1817
|
-
changed.add(
|
|
1818
|
-
beforeById.delete(
|
|
2205
|
+
for (const goal2 of after2.goals) {
|
|
2206
|
+
const prior = beforeById.get(goal2.id);
|
|
2207
|
+
if (prior === undefined || JSON.stringify(prior) !== JSON.stringify(goal2))
|
|
2208
|
+
changed.add(goal2.id);
|
|
2209
|
+
beforeById.delete(goal2.id);
|
|
1819
2210
|
}
|
|
1820
2211
|
for (const id of beforeById.keys())
|
|
1821
2212
|
changed.add(id);
|
|
@@ -1825,55 +2216,54 @@ function changedGoalIdsBetween(before, after) {
|
|
|
1825
2216
|
// components/ulw-loop/src/steering.ts
|
|
1826
2217
|
var SOURCES2 = ["user_prompt_submit", "finding", "cli"];
|
|
1827
2218
|
var PROTECTED = new Set(["aggregateCompletion", "codexObjective", "codexObjectiveAliases", "originalConstraints", "qualityGate", "status", "completedAt", "completionStatus"]);
|
|
1828
|
-
var
|
|
1829
|
-
var isPlain2 = (value) =>
|
|
1830
|
-
var
|
|
1831
|
-
var
|
|
1832
|
-
var
|
|
1833
|
-
const candidate =
|
|
1834
|
-
return
|
|
2219
|
+
var isObject2 = (value) => typeof value === "object" && value !== null;
|
|
2220
|
+
var isPlain2 = (value) => isObject2(value) && !Array.isArray(value);
|
|
2221
|
+
var read3 = (value, key) => Object.entries(value).find(([name]) => name === key)?.[1];
|
|
2222
|
+
var isText2 = (value) => typeof value === "string" && value.trim().length > 0;
|
|
2223
|
+
var text4 = (value, key) => {
|
|
2224
|
+
const candidate = read3(value, key);
|
|
2225
|
+
return isText2(candidate) ? candidate.trim() : undefined;
|
|
1835
2226
|
};
|
|
1836
2227
|
var isKind2 = (value) => typeof value === "string" && ULW_LOOP_STEERING_MUTATION_KINDS.some((kind) => kind === value);
|
|
1837
2228
|
var isSource2 = (value) => typeof value === "string" && SOURCES2.some((source) => source === value);
|
|
1838
|
-
var
|
|
2229
|
+
var isModel3 = (value) => typeof value === "string" && ULW_LOOP_SUCCESS_CRITERION_USER_MODELS.some((model2) => model2 === value);
|
|
1839
2230
|
var texts = (value, key) => {
|
|
1840
|
-
const candidate =
|
|
2231
|
+
const candidate = read3(value, key);
|
|
1841
2232
|
return Array.isArray(candidate) && candidate.every((item) => typeof item === "string") ? candidate : [];
|
|
1842
2233
|
};
|
|
1843
|
-
function
|
|
2234
|
+
function targets2(proposal) {
|
|
1844
2235
|
const many = texts(proposal, "targetGoalIds");
|
|
1845
|
-
const one =
|
|
2236
|
+
const one = text4(proposal, "targetGoalId") ?? text4(proposal, "goalId");
|
|
1846
2237
|
return many.length > 0 ? many : one === undefined ? [] : [one];
|
|
1847
2238
|
}
|
|
1848
|
-
var
|
|
1849
|
-
const candidate =
|
|
2239
|
+
var after2 = (proposal) => {
|
|
2240
|
+
const candidate = read3(proposal, "after");
|
|
1850
2241
|
return isPlain2(candidate) ? candidate : undefined;
|
|
1851
2242
|
};
|
|
1852
|
-
var
|
|
1853
|
-
function
|
|
2243
|
+
var revised2 = (proposal, direct, nested) => text4(proposal, direct) ?? text4(after2(proposal) ?? proposal, nested);
|
|
2244
|
+
function child3(value) {
|
|
1854
2245
|
if (!isPlain2(value))
|
|
1855
2246
|
return null;
|
|
1856
|
-
const title =
|
|
1857
|
-
const objective =
|
|
2247
|
+
const title = text4(value, "title");
|
|
2248
|
+
const objective = text4(value, "objective");
|
|
1858
2249
|
if (title === undefined || objective === undefined)
|
|
1859
2250
|
return null;
|
|
1860
2251
|
return { title, objective };
|
|
1861
2252
|
}
|
|
1862
2253
|
function childValues(proposal) {
|
|
1863
|
-
const direct =
|
|
2254
|
+
const direct = read3(proposal, "childGoals");
|
|
1864
2255
|
if (Array.isArray(direct) && direct.length > 0)
|
|
1865
2256
|
return direct;
|
|
1866
|
-
const nested =
|
|
1867
|
-
const fromAfter = nested === undefined ? undefined :
|
|
2257
|
+
const nested = after2(proposal);
|
|
2258
|
+
const fromAfter = nested === undefined ? undefined : read3(nested, "children");
|
|
1868
2259
|
return Array.isArray(fromAfter) ? fromAfter : [];
|
|
1869
2260
|
}
|
|
1870
|
-
var children2 = (proposal) => childValues(proposal).map(child2).filter((item) => item !== null);
|
|
1871
2261
|
var pendingOrder = (proposal) => {
|
|
1872
2262
|
const direct = texts(proposal, "pendingOrder");
|
|
1873
|
-
return direct.length > 0 ? direct : texts(
|
|
2263
|
+
return direct.length > 0 ? direct : texts(after2(proposal) ?? proposal, "pendingGoalIds");
|
|
1874
2264
|
};
|
|
1875
2265
|
function hasProtected(value) {
|
|
1876
|
-
if (!
|
|
2266
|
+
if (!isObject2(value))
|
|
1877
2267
|
return false;
|
|
1878
2268
|
for (const [key, childValue] of Object.entries(value))
|
|
1879
2269
|
if (PROTECTED.has(key) || key.toLowerCase().includes("complete") || hasProtected(childValue))
|
|
@@ -1883,7 +2273,7 @@ function hasProtected(value) {
|
|
|
1883
2273
|
function allText(value) {
|
|
1884
2274
|
if (typeof value === "string")
|
|
1885
2275
|
return value;
|
|
1886
|
-
return
|
|
2276
|
+
return isObject2(value) ? Object.values(value).map(allText).filter(Boolean).join(`
|
|
1887
2277
|
`) : "";
|
|
1888
2278
|
}
|
|
1889
2279
|
function weakens(value) {
|
|
@@ -1892,17 +2282,17 @@ function weakens(value) {
|
|
|
1892
2282
|
}
|
|
1893
2283
|
function auditFor(proposal, reasons) {
|
|
1894
2284
|
const object = isPlain2(proposal) ? proposal : undefined;
|
|
1895
|
-
const kindRaw = object === undefined ? undefined :
|
|
1896
|
-
const sourceRaw = object === undefined ? undefined :
|
|
1897
|
-
const evidence = object === undefined ? "" :
|
|
1898
|
-
const rationale = object === undefined ? "" :
|
|
1899
|
-
const audit = { kind: isKind2(kindRaw) ? kindRaw : "annotate_ledger", source: isSource2(sourceRaw) ? sourceRaw : "cli", targetGoalIds: object === undefined ? [] :
|
|
2285
|
+
const kindRaw = object === undefined ? undefined : read3(object, "kind");
|
|
2286
|
+
const sourceRaw = object === undefined ? undefined : read3(object, "source");
|
|
2287
|
+
const evidence = object === undefined ? "" : text4(object, "evidence") ?? "";
|
|
2288
|
+
const rationale = object === undefined ? "" : text4(object, "rationale") ?? "";
|
|
2289
|
+
const audit = { kind: isKind2(kindRaw) ? kindRaw : "annotate_ledger", source: isSource2(sourceRaw) ? sourceRaw : "cli", targetGoalIds: object === undefined ? [] : targets2(object), evidence, rationale, invariant: { accepted: reasons.length === 0, structuralInvariantAccepted: reasons.length === 0, evidenceBackedNecessity: evidence.length > 0 && rationale.length > 0, noEasierCompletion: !weakens(proposal), rejectedReasons: reasons, reasons } };
|
|
1900
2290
|
if (object === undefined)
|
|
1901
2291
|
return audit;
|
|
1902
|
-
const criterionId =
|
|
1903
|
-
const directiveText =
|
|
1904
|
-
const promptSignature =
|
|
1905
|
-
const idempotencyKey =
|
|
2292
|
+
const criterionId = text4(object, "criterionId");
|
|
2293
|
+
const directiveText = text4(object, "directiveText");
|
|
2294
|
+
const promptSignature = text4(object, "promptSignature");
|
|
2295
|
+
const idempotencyKey = text4(object, "idempotencyKey");
|
|
1906
2296
|
if (criterionId !== undefined)
|
|
1907
2297
|
audit.criterionId = criterionId;
|
|
1908
2298
|
if (directiveText !== undefined)
|
|
@@ -1918,14 +2308,14 @@ function validateUlwLoopSteeringProposal(plan, proposal) {
|
|
|
1918
2308
|
if (!isPlain2(proposal))
|
|
1919
2309
|
reasons.push("proposal must be an object");
|
|
1920
2310
|
const object = isPlain2(proposal) ? proposal : {};
|
|
1921
|
-
const kind =
|
|
2311
|
+
const kind = read3(object, "kind");
|
|
1922
2312
|
if (!isKind2(kind))
|
|
1923
2313
|
reasons.push(`invalid kind: ${String(kind)}`);
|
|
1924
|
-
if (!isSource2(
|
|
1925
|
-
reasons.push(`invalid source: ${String(
|
|
1926
|
-
if (
|
|
2314
|
+
if (!isSource2(read3(object, "source")))
|
|
2315
|
+
reasons.push(`invalid source: ${String(read3(object, "source"))}`);
|
|
2316
|
+
if (text4(object, "evidence") === undefined)
|
|
1927
2317
|
reasons.push("missing evidence");
|
|
1928
|
-
if (
|
|
2318
|
+
if (text4(object, "rationale") === undefined)
|
|
1929
2319
|
reasons.push("missing rationale");
|
|
1930
2320
|
if (hasProtected(proposal))
|
|
1931
2321
|
reasons.push("protected payload");
|
|
@@ -1937,12 +2327,12 @@ function validateUlwLoopSteeringProposal(plan, proposal) {
|
|
|
1937
2327
|
validateKind(plan, object, kind, reasons);
|
|
1938
2328
|
return auditFor(proposal, reasons);
|
|
1939
2329
|
}
|
|
1940
|
-
function
|
|
2330
|
+
function goal2(plan, id) {
|
|
1941
2331
|
return id === undefined ? undefined : plan.goals.find((item) => item.id === id);
|
|
1942
2332
|
}
|
|
1943
2333
|
function validateKind(plan, proposal, kind, reasons) {
|
|
1944
|
-
const target =
|
|
1945
|
-
if (kind === "add_subgoal" && (
|
|
2334
|
+
const target = goal2(plan, targets2(proposal)[0]);
|
|
2335
|
+
if (kind === "add_subgoal" && (text4(proposal, "title") === undefined || text4(proposal, "objective") === undefined))
|
|
1946
2336
|
reasons.push("add_subgoal requires title/objective");
|
|
1947
2337
|
if ((kind === "split_subgoal" || kind === "revise_pending_wording" || kind === "mark_blocked_superseded") && target === undefined)
|
|
1948
2338
|
reasons.push(`${kind} requires target`);
|
|
@@ -1951,11 +2341,11 @@ function validateKind(plan, proposal, kind, reasons) {
|
|
|
1951
2341
|
const rawChildren = childValues(proposal);
|
|
1952
2342
|
if (kind === "split_subgoal" && rawChildren.length === 0)
|
|
1953
2343
|
reasons.push("split_subgoal requires children");
|
|
1954
|
-
if ((kind === "split_subgoal" || kind === "mark_blocked_superseded") && rawChildren.some((item) =>
|
|
2344
|
+
if ((kind === "split_subgoal" || kind === "mark_blocked_superseded") && rawChildren.some((item) => child3(item) === null))
|
|
1955
2345
|
reasons.push(`${kind} children require title/objective`);
|
|
1956
2346
|
if (kind === "reorder_pending")
|
|
1957
2347
|
validateOrder(plan, proposal, reasons);
|
|
1958
|
-
if (kind === "revise_pending_wording" &&
|
|
2348
|
+
if (kind === "revise_pending_wording" && revised2(proposal, "revisedTitle", "title") === undefined && revised2(proposal, "revisedObjective", "objective") === undefined)
|
|
1959
2349
|
reasons.push("revise_pending_wording requires update");
|
|
1960
2350
|
if (kind === "revise_criterion")
|
|
1961
2351
|
validateCriterion(plan, proposal, reasons);
|
|
@@ -1971,31 +2361,18 @@ function validateOrder(plan, proposal, reasons) {
|
|
|
1971
2361
|
reasons.push("unknown pending id");
|
|
1972
2362
|
}
|
|
1973
2363
|
function validateCriterion(plan, proposal, reasons) {
|
|
1974
|
-
const target =
|
|
1975
|
-
const criterionId =
|
|
2364
|
+
const target = goal2(plan, targets2(proposal)[0]);
|
|
2365
|
+
const criterionId = text4(proposal, "criterionId");
|
|
1976
2366
|
if (target === undefined)
|
|
1977
2367
|
reasons.push("revise_criterion requires goalId");
|
|
1978
2368
|
else if (criterionId === undefined || target.successCriteria.every((item) => item.id !== criterionId))
|
|
1979
2369
|
reasons.push("revise_criterion requires criterionId");
|
|
1980
|
-
const model2 =
|
|
1981
|
-
if (
|
|
2370
|
+
const model2 = read3(proposal, "userModel");
|
|
2371
|
+
if (read3(proposal, "scenario") === undefined && read3(proposal, "expectedEvidence") === undefined && model2 === undefined)
|
|
1982
2372
|
reasons.push("revise_criterion requires update");
|
|
1983
|
-
if (model2 !== undefined && !
|
|
2373
|
+
if (model2 !== undefined && !isModel3(model2))
|
|
1984
2374
|
reasons.push("invalid userModel");
|
|
1985
2375
|
}
|
|
1986
|
-
function nextId(plan, offset) {
|
|
1987
|
-
const max = plan.goals.reduce((current, item) => {
|
|
1988
|
-
const digits = /^G(\d+)(?:-|$)/u.exec(item.id)?.[1];
|
|
1989
|
-
return digits === undefined ? current : Math.max(current, Number(digits));
|
|
1990
|
-
}, 0);
|
|
1991
|
-
return `G${String(max + offset).padStart(3, "0")}`;
|
|
1992
|
-
}
|
|
1993
|
-
function makeGoal2(plan, childGoal, evidence, now, offset) {
|
|
1994
|
-
const id = nextId(plan, offset);
|
|
1995
|
-
const digits = /^G(\d+)/u.exec(id)?.[1];
|
|
1996
|
-
const goalIndex = digits === undefined ? plan.goals.length + offset - 1 : Number(digits) - 1;
|
|
1997
|
-
return { id, title: childGoal.title, objective: childGoal.objective, status: "pending", successCriteria: seedDefaultSuccessCriteria(goalIndex, childGoal.objective), attempt: 0, createdAt: now, updatedAt: now, evidence };
|
|
1998
|
-
}
|
|
1999
2376
|
function applySteeringMutation(plan, proposal, audit) {
|
|
2000
2377
|
const next = structuredClone(plan);
|
|
2001
2378
|
if (!audit.invariant.accepted)
|
|
@@ -2005,7 +2382,7 @@ function applySteeringMutation(plan, proposal, audit) {
|
|
|
2005
2382
|
next.goals.push(makeGoal2(next, { title: proposal.title ?? "", objective: proposal.objective ?? "" }, proposal.evidence, now, 1));
|
|
2006
2383
|
if (proposal.kind === "reorder_pending") {
|
|
2007
2384
|
const order = pendingOrder(proposal);
|
|
2008
|
-
next.goals = [...order.map((id) =>
|
|
2385
|
+
next.goals = [...order.map((id) => goal2(next, id)).filter((item) => item !== undefined), ...next.goals.filter((item) => !order.includes(item.id))];
|
|
2009
2386
|
}
|
|
2010
2387
|
if (proposal.kind === "revise_pending_wording")
|
|
2011
2388
|
reviseWording(next, proposal, now);
|
|
@@ -2017,53 +2394,11 @@ function applySteeringMutation(plan, proposal, audit) {
|
|
|
2017
2394
|
next.updatedAt = now;
|
|
2018
2395
|
return next;
|
|
2019
2396
|
}
|
|
2020
|
-
function reviseWording(plan, proposal, now) {
|
|
2021
|
-
const target = goal(plan, targets(proposal)[0]);
|
|
2022
|
-
if (target === undefined)
|
|
2023
|
-
return;
|
|
2024
|
-
target.title = revised(proposal, "revisedTitle", "title") ?? target.title;
|
|
2025
|
-
target.objective = revised(proposal, "revisedObjective", "objective") ?? target.objective;
|
|
2026
|
-
target.steeringEvidence = proposal.evidence;
|
|
2027
|
-
target.steeringRationale = proposal.rationale;
|
|
2028
|
-
target.updatedAt = now;
|
|
2029
|
-
}
|
|
2030
|
-
function splitOrBlock(plan, proposal, now) {
|
|
2031
|
-
const target = goal(plan, targets(proposal)[0]);
|
|
2032
|
-
if (target === undefined)
|
|
2033
|
-
return;
|
|
2034
|
-
const replacements = children2(proposal).map((item, index) => makeGoal2(plan, item, proposal.evidence, now, index + 1));
|
|
2035
|
-
target.steeringEvidence = proposal.evidence;
|
|
2036
|
-
target.steeringRationale = proposal.rationale;
|
|
2037
|
-
target.updatedAt = now;
|
|
2038
|
-
if (replacements.length === 0) {
|
|
2039
|
-
target.status = "blocked";
|
|
2040
|
-
target.steeringStatus = "blocked";
|
|
2041
|
-
target.blockedReason = proposal.blockedReason ?? proposal.rationale;
|
|
2042
|
-
} else {
|
|
2043
|
-
target.steeringStatus = "superseded";
|
|
2044
|
-
target.supersededBy = replacements.map((item) => item.id);
|
|
2045
|
-
for (const item of replacements)
|
|
2046
|
-
item.supersedes = [target.id];
|
|
2047
|
-
plan.goals.splice(plan.goals.indexOf(target) + 1, 0, ...replacements);
|
|
2048
|
-
}
|
|
2049
|
-
if (plan.activeGoalId === target.id)
|
|
2050
|
-
delete plan.activeGoalId;
|
|
2051
|
-
}
|
|
2052
|
-
function reviseCriterion(plan, proposal, now) {
|
|
2053
|
-
const target = goal(plan, targets(proposal)[0]);
|
|
2054
|
-
const index = target?.successCriteria.findIndex((item) => item.id === proposal.criterionId) ?? -1;
|
|
2055
|
-
const current = target?.successCriteria[index];
|
|
2056
|
-
if (target === undefined || current === undefined)
|
|
2057
|
-
return;
|
|
2058
|
-
const model2 = read(proposal, "userModel");
|
|
2059
|
-
target.successCriteria[index] = { ...current, scenario: text2(proposal, "scenario") ?? current.scenario, expectedEvidence: text2(proposal, "expectedEvidence") ?? current.expectedEvidence, userModel: isModel2(model2) ? model2 : current.userModel };
|
|
2060
|
-
target.updatedAt = now;
|
|
2061
|
-
}
|
|
2062
2397
|
function isProposal(value) {
|
|
2063
|
-
return isPlain2(value) && isKind2(
|
|
2398
|
+
return isPlain2(value) && isKind2(read3(value, "kind")) && isSource2(read3(value, "source")) && isText2(read3(value, "evidence")) && isText2(read3(value, "rationale"));
|
|
2064
2399
|
}
|
|
2065
|
-
function parseUlwLoopSteeringDirective(
|
|
2066
|
-
const match = /(?:^|\s)(?:OMO_ULW_LOOP_STEER|omo\.ulw-loop\.steer|omo ulw-loop steer):\s*([\s\S]+)$/u.exec(
|
|
2400
|
+
function parseUlwLoopSteeringDirective(text5) {
|
|
2401
|
+
const match = /(?:^|\s)(?:OMO_ULW_LOOP_STEER|omo\.ulw-loop\.steer|omo ulw-loop steer):\s*([\s\S]+)$/u.exec(text5);
|
|
2067
2402
|
if (match?.[1] === undefined)
|
|
2068
2403
|
return null;
|
|
2069
2404
|
try {
|
|
@@ -2093,9 +2428,13 @@ async function steerUlwLoop(repoRoot, proposal, scope) {
|
|
|
2093
2428
|
finalAudit.before = buildSteeringPlanSnapshot(plan, changed);
|
|
2094
2429
|
finalAudit.after = buildSteeringPlanSnapshot(next, changed);
|
|
2095
2430
|
}
|
|
2431
|
+
const at = proposal.now?.toISOString() ?? iso();
|
|
2432
|
+
const batchEntry = accepted ? batchUpdateLedgerEntry(plan, next, at) : null;
|
|
2096
2433
|
if (accepted)
|
|
2097
2434
|
await writePlan(repoRoot, next, scope);
|
|
2098
|
-
await appendLedger(repoRoot, ledgerEntry(proposal, finalAudit,
|
|
2435
|
+
await appendLedger(repoRoot, ledgerEntry(proposal, finalAudit, at), scope);
|
|
2436
|
+
if (batchEntry !== null)
|
|
2437
|
+
await appendLedger(repoRoot, batchEntry, scope);
|
|
2099
2438
|
return { plan: next, accepted, audit: finalAudit, rejectedReasons: audit.invariant.rejectedReasons, deduped: false };
|
|
2100
2439
|
});
|
|
2101
2440
|
}
|
|
@@ -2111,6 +2450,87 @@ function ledgerEntry(proposal, audit, at) {
|
|
|
2111
2450
|
return entry;
|
|
2112
2451
|
}
|
|
2113
2452
|
|
|
2453
|
+
// components/ulw-loop/src/steering-batch.ts
|
|
2454
|
+
async function steerUlwLoopBatch(repoRoot, proposals, scope) {
|
|
2455
|
+
return withUlwLoopMutationLock(repoRoot, scope, async () => {
|
|
2456
|
+
const plan = await readUlwLoopPlan(repoRoot, scope);
|
|
2457
|
+
const prepared = await prepareBatch(repoRoot, plan, proposals, scope);
|
|
2458
|
+
const failed = prepared.results.find((item) => !item.accepted);
|
|
2459
|
+
if (failed !== undefined) {
|
|
2460
|
+
const entry = rejectedLedgerEntry(prepared.results);
|
|
2461
|
+
await appendLedger(repoRoot, entry, scope);
|
|
2462
|
+
return rejected(plan, prepared.results, failed.rejectedReasons);
|
|
2463
|
+
}
|
|
2464
|
+
let next = plan;
|
|
2465
|
+
for (const item of prepared.items)
|
|
2466
|
+
if (item.kind === "fresh")
|
|
2467
|
+
next = item.prepared.next;
|
|
2468
|
+
const fresh = prepared.items.filter((item) => item.kind === "fresh");
|
|
2469
|
+
if (fresh.length > 0) {
|
|
2470
|
+
await writePlan(repoRoot, next, scope);
|
|
2471
|
+
const entries = fresh.map((item) => ledgerEntry2(item.prepared.proposal, item.prepared.audit, item.prepared.proposal.now?.toISOString() ?? iso()));
|
|
2472
|
+
const batchEntry = batchUpdateLedgerEntry(plan, next, iso());
|
|
2473
|
+
await appendLedgerEntries(repoRoot, batchEntry === null ? entries : [...entries, batchEntry], scope);
|
|
2474
|
+
}
|
|
2475
|
+
return { plan: next, accepted: true, results: prepared.results, rejectedReasons: [] };
|
|
2476
|
+
});
|
|
2477
|
+
}
|
|
2478
|
+
async function prepareBatch(repoRoot, plan, proposals, scope) {
|
|
2479
|
+
const items = [];
|
|
2480
|
+
const results = [];
|
|
2481
|
+
let current = plan;
|
|
2482
|
+
for (const proposal of proposals) {
|
|
2483
|
+
const key = proposal.idempotencyKey ?? proposal.promptSignature;
|
|
2484
|
+
const prior = key === undefined ? undefined : await findAcceptedSteeringLedgerEntry(repoRoot, key, scope);
|
|
2485
|
+
if (prior?.steering !== undefined) {
|
|
2486
|
+
const result2 = { accepted: true, deduped: true, audit: { ...prior.steering, deduped: true }, rejectedReasons: [] };
|
|
2487
|
+
items.push({ kind: "deduped", result: result2 });
|
|
2488
|
+
results.push(result2);
|
|
2489
|
+
continue;
|
|
2490
|
+
}
|
|
2491
|
+
const audit = validateUlwLoopSteeringProposal(current, proposal);
|
|
2492
|
+
if (!audit.invariant.accepted) {
|
|
2493
|
+
const result2 = { accepted: false, deduped: false, audit, rejectedReasons: audit.invariant.rejectedReasons };
|
|
2494
|
+
items.push({ kind: "deduped", result: result2 });
|
|
2495
|
+
results.push(result2);
|
|
2496
|
+
continue;
|
|
2497
|
+
}
|
|
2498
|
+
const next = applySteeringMutation(current, proposal, audit);
|
|
2499
|
+
const changed = changedGoalIdsBetween(current, next);
|
|
2500
|
+
const finalAudit = { ...audit, before: buildSteeringPlanSnapshot(current, changed), after: buildSteeringPlanSnapshot(next, changed) };
|
|
2501
|
+
const result = { accepted: true, deduped: false, audit: finalAudit, rejectedReasons: [] };
|
|
2502
|
+
items.push({ kind: "fresh", prepared: { proposal, audit: finalAudit, before: current, next } });
|
|
2503
|
+
results.push(result);
|
|
2504
|
+
current = next;
|
|
2505
|
+
}
|
|
2506
|
+
return { items, results };
|
|
2507
|
+
}
|
|
2508
|
+
function rejected(plan, results, rejectedReasons) {
|
|
2509
|
+
return { plan, accepted: false, results, rejectedReasons };
|
|
2510
|
+
}
|
|
2511
|
+
function rejectedLedgerEntry(results) {
|
|
2512
|
+
const rejectedItems = results.map((result, index) => ({ result, index })).filter((item) => !item.result.accepted);
|
|
2513
|
+
return { at: iso(), kind: "steering_rejected", message: rejectedItems.map((item) => `index ${item.index}: ${item.result.rejectedReasons.join(", ")}`).join("; ") };
|
|
2514
|
+
}
|
|
2515
|
+
function ledgerEntry2(proposal, audit, at) {
|
|
2516
|
+
const entry = {
|
|
2517
|
+
at,
|
|
2518
|
+
kind: proposal.kind === "revise_criterion" ? "criteria_revised" : "steering_accepted",
|
|
2519
|
+
evidence: proposal.evidence,
|
|
2520
|
+
message: proposal.rationale,
|
|
2521
|
+
steering: audit,
|
|
2522
|
+
mutationKind: proposal.kind
|
|
2523
|
+
};
|
|
2524
|
+
const goalId = audit.targetGoalIds[0];
|
|
2525
|
+
if (goalId !== undefined)
|
|
2526
|
+
entry.goalId = goalId;
|
|
2527
|
+
if (proposal.criterionId !== undefined)
|
|
2528
|
+
entry.criterionId = proposal.criterionId;
|
|
2529
|
+
if (proposal.idempotencyKey !== undefined)
|
|
2530
|
+
entry.idempotencyKey = proposal.idempotencyKey;
|
|
2531
|
+
return entry;
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2114
2534
|
// components/ulw-loop/src/cli-subcommands.ts
|
|
2115
2535
|
async function createGoals(repoRoot, argv, json, scope) {
|
|
2116
2536
|
const briefFile = readValue(argv, "--brief-file");
|
|
@@ -2118,10 +2538,12 @@ async function createGoals(repoRoot, argv, json, scope) {
|
|
|
2118
2538
|
if (!brief.trim()) {
|
|
2119
2539
|
throw new UlwLoopError("Missing brief text. Pass --brief, --brief-file, --from-stdin, or positional text.", "ULW_LOOP_BRIEF_REQUIRED");
|
|
2120
2540
|
}
|
|
2541
|
+
const validationBatchesJson = readValue(argv, "--validation-batch-json");
|
|
2121
2542
|
const plan = await createUlwLoopPlan(repoRoot, {
|
|
2122
2543
|
brief,
|
|
2123
2544
|
codexGoalMode: normalizeCodexGoalMode(readValue(argv, "--codex-goal-mode")),
|
|
2124
|
-
force: hasFlag(argv, "--force")
|
|
2545
|
+
force: hasFlag(argv, "--force"),
|
|
2546
|
+
...validationBatchesJson === undefined ? {} : { validationBatchesJson }
|
|
2125
2547
|
}, scope);
|
|
2126
2548
|
if (json)
|
|
2127
2549
|
printJson({ ok: true, plan, summary: summarizeUlwLoopPlan(plan) });
|
|
@@ -2137,7 +2559,7 @@ ledger: ${plan.ledgerPath}
|
|
|
2137
2559
|
async function status(repoRoot, json, scope) {
|
|
2138
2560
|
const plan = await readUlwLoopPlan(repoRoot, scope);
|
|
2139
2561
|
if (json) {
|
|
2140
|
-
const active = plan.goals.find((
|
|
2562
|
+
const active = plan.goals.find((goal3) => goal3.id === plan.activeGoalId);
|
|
2141
2563
|
const currentAttemptDir = plan.evidenceLayoutVersion === 2 && active ? ulwLoopAttemptEvidenceDir(active.id, active.attempt, scope) : undefined;
|
|
2142
2564
|
printJson({
|
|
2143
2565
|
ok: true,
|
|
@@ -2175,38 +2597,20 @@ async function completeGoals(repoRoot, argv, json, scope) {
|
|
|
2175
2597
|
`);
|
|
2176
2598
|
return 0;
|
|
2177
2599
|
}
|
|
2178
|
-
async function checkpoint(repoRoot, argv, json, scope) {
|
|
2179
|
-
const goalId = required3(argv, "--goal-id");
|
|
2180
|
-
const statusValue = checkpointStatus(required3(argv, "--status"));
|
|
2181
|
-
const evidence = required3(argv, "--evidence");
|
|
2182
|
-
const codexGoalJson = await parseCodexGoalJson(statusValue === "complete" ? required3(argv, "--codex-goal-json") : readValue(argv, "--codex-goal-json"));
|
|
2183
|
-
if (statusValue === "complete" && codexGoalJson === undefined) {
|
|
2184
|
-
throw new UlwLoopError("Missing --codex-goal-json.", "ULW_LOOP_CODEX_GOAL_JSON_REQUIRED");
|
|
2185
|
-
}
|
|
2186
|
-
const qualityGateJson = readValue(argv, "--quality-gate-json");
|
|
2187
|
-
const args = {
|
|
2188
|
-
goalId,
|
|
2189
|
-
status: statusValue,
|
|
2190
|
-
evidence,
|
|
2191
|
-
...codexGoalJson === undefined ? {} : { codexGoalJson },
|
|
2192
|
-
...qualityGateJson === undefined ? {} : { qualityGateJson }
|
|
2193
|
-
};
|
|
2194
|
-
const result = await checkpointUlwLoop(repoRoot, args, scope);
|
|
2195
|
-
if (json)
|
|
2196
|
-
printJson({ ok: true, ...result, summary: summarizeUlwLoopPlan(result.plan) });
|
|
2197
|
-
else
|
|
2198
|
-
process.stdout.write(`ulw-loop checkpoint: ${result.goal.id} -> ${result.goal.status}
|
|
2199
|
-
`);
|
|
2200
|
-
return 0;
|
|
2201
|
-
}
|
|
2202
2600
|
async function steer(repoRoot, argv, json, scope) {
|
|
2203
|
-
const
|
|
2204
|
-
const
|
|
2205
|
-
|
|
2601
|
+
const proposals = await parseSteeringProposals(argv);
|
|
2602
|
+
const single = proposals[0];
|
|
2603
|
+
if (single !== undefined && proposals.length === 1 && readValue(argv, "--proposals-json") === undefined) {
|
|
2604
|
+
const result2 = await steerUlwLoop(repoRoot, single, scope);
|
|
2605
|
+
printSteerResult(result2, json);
|
|
2606
|
+
return result2.accepted ? 0 : 1;
|
|
2607
|
+
}
|
|
2608
|
+
const result = await steerUlwLoopBatch(repoRoot, proposals, scope);
|
|
2609
|
+
printSteerBatchResult(result, json);
|
|
2206
2610
|
return result.accepted ? 0 : 1;
|
|
2207
2611
|
}
|
|
2208
2612
|
async function addGoal(repoRoot, argv, json, scope) {
|
|
2209
|
-
const result = await addUlwLoopGoal(repoRoot, { title:
|
|
2613
|
+
const result = await addUlwLoopGoal(repoRoot, { title: required4(argv, "--title"), objective: required4(argv, "--objective") }, scope);
|
|
2210
2614
|
if (json)
|
|
2211
2615
|
printJson({ ok: true, plan: result.plan, goal: result.goal, summary: summarizeUlwLoopPlan(result.plan) });
|
|
2212
2616
|
else {
|
|
@@ -2217,13 +2621,13 @@ async function addGoal(repoRoot, argv, json, scope) {
|
|
|
2217
2621
|
return 0;
|
|
2218
2622
|
}
|
|
2219
2623
|
async function criteria(repoRoot, argv, json, scope) {
|
|
2220
|
-
const goalId =
|
|
2221
|
-
const
|
|
2624
|
+
const goalId = required4(argv, "--goal-id");
|
|
2625
|
+
const goal3 = findGoal3(await readUlwLoopPlan(repoRoot, scope), goalId);
|
|
2222
2626
|
if (json)
|
|
2223
|
-
printJson({ ok: true, goalId:
|
|
2627
|
+
printJson({ ok: true, goalId: goal3.id, criteria: goal3.successCriteria });
|
|
2224
2628
|
else {
|
|
2225
|
-
process.stdout.write(`criteria for ${
|
|
2226
|
-
${
|
|
2629
|
+
process.stdout.write(`criteria for ${goal3.id}:
|
|
2630
|
+
${goal3.successCriteria.map(formatCriterionForCli).join(`
|
|
2227
2631
|
`)}
|
|
2228
2632
|
`);
|
|
2229
2633
|
}
|
|
@@ -2240,15 +2644,15 @@ async function captureEvidence(repoRoot, argv, json, scope) {
|
|
|
2240
2644
|
return 0;
|
|
2241
2645
|
}
|
|
2242
2646
|
async function reviewBlockers(repoRoot, argv, json, scope) {
|
|
2243
|
-
const codexGoalJson = await parseCodexGoalJson(
|
|
2647
|
+
const codexGoalJson = await parseCodexGoalJson(required4(argv, "--codex-goal-json"));
|
|
2244
2648
|
if (codexGoalJson === undefined) {
|
|
2245
2649
|
throw new UlwLoopError("Missing --codex-goal-json.", "ULW_LOOP_CODEX_GOAL_JSON_REQUIRED");
|
|
2246
2650
|
}
|
|
2247
2651
|
const result = await recordFinalReviewBlockers(repoRoot, {
|
|
2248
|
-
goalId:
|
|
2249
|
-
title:
|
|
2250
|
-
objective:
|
|
2251
|
-
evidence:
|
|
2652
|
+
goalId: required4(argv, "--goal-id"),
|
|
2653
|
+
title: required4(argv, "--title"),
|
|
2654
|
+
objective: required4(argv, "--objective"),
|
|
2655
|
+
evidence: required4(argv, "--evidence"),
|
|
2252
2656
|
codexGoalJson
|
|
2253
2657
|
}, scope);
|
|
2254
2658
|
if (json) {
|
|
@@ -2270,21 +2674,16 @@ function formatCriterionForCli(criterion) {
|
|
|
2270
2674
|
const marker = isEssentialCriterion(criterion) ? "essential" : "non-essential";
|
|
2271
2675
|
return `- ${criterion.id} [${criterion.status}] [${marker}] (${criterion.userModel}) ${criterion.scenario} evidence: ${criterion.capturedEvidence ?? "pending"}`;
|
|
2272
2676
|
}
|
|
2273
|
-
function
|
|
2677
|
+
function required4(argv, flag) {
|
|
2274
2678
|
const value = readValue(argv, flag)?.trim();
|
|
2275
2679
|
if (value)
|
|
2276
2680
|
return value;
|
|
2277
2681
|
throw new UlwLoopError(`Missing ${flag}.`, "ULW_LOOP_ARGUMENT_MISSING", { details: { flag } });
|
|
2278
2682
|
}
|
|
2279
|
-
function checkpointStatus(value) {
|
|
2280
|
-
if (value === "complete" || value === "failed" || value === "blocked")
|
|
2281
|
-
return value;
|
|
2282
|
-
throw new UlwLoopError("Missing or invalid --status; expected complete, failed, or blocked.", "ULW_LOOP_STATUS_INVALID", { details: { status: value } });
|
|
2283
|
-
}
|
|
2284
2683
|
function findGoal3(plan, goalId) {
|
|
2285
|
-
const
|
|
2286
|
-
if (
|
|
2287
|
-
return
|
|
2684
|
+
const goal3 = plan.goals.find((candidate) => candidate.id === goalId);
|
|
2685
|
+
if (goal3 !== undefined)
|
|
2686
|
+
return goal3;
|
|
2288
2687
|
throw new UlwLoopError(`Unknown ulw-loop id: ${goalId}.`, "ULW_LOOP_GOAL_NOT_FOUND", { details: { goalId } });
|
|
2289
2688
|
}
|
|
2290
2689
|
|
|
@@ -2727,26 +3126,26 @@ function consumeFanOutBudget(stateDir) {
|
|
|
2727
3126
|
function missingGateArtifact(payload, plan) {
|
|
2728
3127
|
if (!isGateReviewerSpawn(payload.tool_input))
|
|
2729
3128
|
return null;
|
|
2730
|
-
const
|
|
2731
|
-
if (
|
|
3129
|
+
const goal3 = plan.goals.find((candidate) => isFinalRunCompletionCandidate(plan, candidate));
|
|
3130
|
+
if (goal3 === undefined || goal3.status === "complete")
|
|
2732
3131
|
return null;
|
|
2733
|
-
if (!
|
|
3132
|
+
if (!goal3.successCriteria.every((criterion) => criterion.status === "pass"))
|
|
2734
3133
|
return null;
|
|
2735
3134
|
const scope = { sessionId: payload.session_id };
|
|
2736
3135
|
if (plan.evidenceLayoutVersion === 2) {
|
|
2737
|
-
const attemptDir = ulwLoopAttemptEvidenceDir(
|
|
2738
|
-
for (const name of [`${
|
|
3136
|
+
const attemptDir = ulwLoopAttemptEvidenceDir(goal3.id, goal3.attempt, scope);
|
|
3137
|
+
for (const name of [`${goal3.id}-code-review.md`, `${goal3.id}-manual-qa.md`]) {
|
|
2739
3138
|
const relative2 = `${attemptDir}/${name}`;
|
|
2740
3139
|
if (!isNonEmptyFile(join2(payload.cwd, relative2)))
|
|
2741
3140
|
return relative2;
|
|
2742
3141
|
}
|
|
2743
3142
|
return null;
|
|
2744
3143
|
}
|
|
2745
|
-
const flatReport = `.omo/evidence/${
|
|
3144
|
+
const flatReport = `.omo/evidence/${goal3.id}-code-review.md`;
|
|
2746
3145
|
if (!isNonEmptyFile(join2(payload.cwd, flatReport)))
|
|
2747
3146
|
return flatReport;
|
|
2748
|
-
if (!hasOtherEvidenceFile(join2(payload.cwd, ".omo", "evidence"), `${
|
|
2749
|
-
return `.omo/evidence/<any manual-QA artifact besides ${
|
|
3147
|
+
if (!hasOtherEvidenceFile(join2(payload.cwd, ".omo", "evidence"), `${goal3.id}-code-review.md`))
|
|
3148
|
+
return `.omo/evidence/<any manual-QA artifact besides ${goal3.id}-code-review.md>`;
|
|
2750
3149
|
return null;
|
|
2751
3150
|
}
|
|
2752
3151
|
function isGateReviewerSpawn(toolInput) {
|
|
@@ -2817,7 +3216,7 @@ function readPlan(goalsPath) {
|
|
|
2817
3216
|
|
|
2818
3217
|
// components/ulw-loop/src/stop-resume-hook.ts
|
|
2819
3218
|
import { existsSync as existsSync7, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2820
|
-
import { isAbsolute as isAbsolute2, join as join3 } from "node:path";
|
|
3219
|
+
import { isAbsolute as isAbsolute2, join as join3, resolve as resolve4, sep as sep2 } from "node:path";
|
|
2821
3220
|
var RESUME_CAP = 2;
|
|
2822
3221
|
var CONTEXT_PRESSURE_MARKERS2 = [
|
|
2823
3222
|
"context compacted",
|
|
@@ -2840,14 +3239,14 @@ function runStopResumeHook(input) {
|
|
|
2840
3239
|
const plan = readPlan2(join3(stateDir, "goals.json"));
|
|
2841
3240
|
if (plan === null || plan.aggregateCompletion?.status === "complete")
|
|
2842
3241
|
return "";
|
|
2843
|
-
const
|
|
2844
|
-
if (
|
|
3242
|
+
const goal3 = resumableGoal(plan);
|
|
3243
|
+
if (goal3 === undefined)
|
|
2845
3244
|
return "";
|
|
2846
|
-
if (!consumeResumeBudget(stateDir,
|
|
3245
|
+
if (!consumeResumeBudget(stateDir, goal3.id))
|
|
2847
3246
|
return "";
|
|
2848
3247
|
const output = {
|
|
2849
3248
|
decision: "block",
|
|
2850
|
-
reason: renderResumeDirective(plan,
|
|
3249
|
+
reason: renderResumeDirective(plan, goal3, payload.session_id)
|
|
2851
3250
|
};
|
|
2852
3251
|
return JSON.stringify(output);
|
|
2853
3252
|
}
|
|
@@ -2865,36 +3264,42 @@ async function runStopResumeHookCli(stdin, stdout) {
|
|
|
2865
3264
|
}
|
|
2866
3265
|
}
|
|
2867
3266
|
function resumableGoal(plan) {
|
|
2868
|
-
const active = plan.goals.find((
|
|
3267
|
+
const active = plan.goals.find((goal3) => goal3.id === plan.activeGoalId);
|
|
2869
3268
|
if (active !== undefined && isResumableStatus(active.status))
|
|
2870
3269
|
return active;
|
|
2871
|
-
return plan.goals.find((
|
|
3270
|
+
return plan.goals.find((goal3) => isResumableStatus(goal3.status));
|
|
2872
3271
|
}
|
|
2873
3272
|
function isResumableStatus(status2) {
|
|
2874
3273
|
return status2 === "pending" || status2 === "in_progress";
|
|
2875
3274
|
}
|
|
2876
3275
|
function consumeResumeBudget(stateDir, goalId) {
|
|
2877
3276
|
const ledgerLineCount = countLedgerLines(join3(stateDir, "ledger.jsonl"));
|
|
2878
|
-
const counterPath =
|
|
3277
|
+
const counterPath = resolve4(stateDir, `auto-resume-${goalId}.json`);
|
|
3278
|
+
const stuckPath = resolve4(stateDir, `auto-resume-${goalId}.stuck`);
|
|
3279
|
+
if (!isInsideDir(stateDir, counterPath) || !isInsideDir(stateDir, stuckPath))
|
|
3280
|
+
return false;
|
|
2879
3281
|
const previous = readCounter(counterPath);
|
|
2880
3282
|
const count = previous !== null && previous.ledgerLineCount === ledgerLineCount ? previous.count : 0;
|
|
2881
3283
|
if (count >= RESUME_CAP) {
|
|
2882
|
-
writeFileSync2(
|
|
3284
|
+
writeFileSync2(stuckPath, `no ledger progress after ${count} resumes
|
|
2883
3285
|
`);
|
|
2884
3286
|
return false;
|
|
2885
3287
|
}
|
|
2886
3288
|
writeFileSync2(counterPath, JSON.stringify({ count: count + 1, ledgerLineCount }));
|
|
2887
3289
|
return true;
|
|
2888
3290
|
}
|
|
2889
|
-
function
|
|
3291
|
+
function isInsideDir(dir, candidate) {
|
|
3292
|
+
return candidate.startsWith(resolve4(dir) + sep2);
|
|
3293
|
+
}
|
|
3294
|
+
function renderResumeDirective(plan, goal3, sessionId) {
|
|
2890
3295
|
const normalized = normalizeUlwLoopSessionId(sessionId);
|
|
2891
3296
|
const option = normalized !== null && plan.goalsPath.includes(`/${normalized}/`) ? ` --session-id ${normalized}` : "";
|
|
2892
3297
|
return [
|
|
2893
|
-
`The ulw-loop run in this session still has unfinished goals (next: ${
|
|
3298
|
+
`The ulw-loop run in this session still has unfinished goals (next: ${goal3.id} — ${goal3.title}).`,
|
|
2894
3299
|
"The turn ended before the loop completed. Resume it now:",
|
|
2895
3300
|
`1. Run \`omo ulw-loop status${option} --json\` to reload the plan, the active goal, and currentAttemptDir.`,
|
|
2896
3301
|
"2. Continue the active goal's remaining success criteria, recording evidence with record-evidence.",
|
|
2897
|
-
`3. Checkpoint through \`omo ulw-loop checkpoint${option}\` when the goal's criteria are proven.`,
|
|
3302
|
+
`3. Checkpoint through \`omo ulw-loop checkpoint${option}\` when the goal's criteria are proven; a complete checkpoint prints the next goal instruction.`,
|
|
2898
3303
|
"If the loop is genuinely blocked on the user, checkpoint the goal as blocked with the reason instead."
|
|
2899
3304
|
].join(`
|
|
2900
3305
|
`);
|