oh-my-opencode 4.19.0 → 4.19.2
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 +6 -6
- package/README.ru.md +6 -6
- package/README.zh-cn.md +6 -6
- package/dist/agents/atlas/agent.d.ts +5 -3
- package/dist/agents/explore.d.ts +4 -2
- package/dist/agents/gpt-prompt-identity.d.ts +3 -0
- package/dist/agents/hephaestus/agent.d.ts +4 -2
- package/dist/agents/librarian.d.ts +4 -2
- package/dist/agents/metis.d.ts +4 -2
- package/dist/agents/momus.d.ts +3 -2
- package/dist/agents/multimodal-looker.d.ts +4 -2
- package/dist/agents/oracle.d.ts +4 -2
- package/dist/agents/sisyphus/claude-opus-5.d.ts +33 -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 +2 -0
- package/dist/agents/sisyphus-agent-factory.d.ts +5 -2
- package/dist/agents/sisyphus-junior/agent.d.ts +4 -1
- package/dist/agents/sisyphus-junior/gpt-5-5.d.ts +2 -2
- package/dist/agents/types.d.ts +2 -2
- package/dist/cli/index.js +23577 -19269
- package/dist/cli-node/index.js +23577 -19269
- package/dist/config/schema/agent-names.d.ts +26 -26
- package/dist/config/schema/agent-overrides.d.ts +400 -400
- package/dist/config/schema/browser-automation.d.ts +2 -2
- package/dist/config/schema/categories.d.ts +36 -36
- package/dist/config/schema/codegraph.d.ts +2 -0
- package/dist/config/schema/commands.d.ts +2 -2
- package/dist/config/schema/dynamic-context-pruning.d.ts +1 -1
- package/dist/config/schema/experimental.d.ts +1 -1
- package/dist/config/schema/fallback-models.d.ts +25 -25
- package/dist/config/schema/hooks.d.ts +43 -43
- package/dist/config/schema/internal/permission.d.ts +9 -9
- package/dist/config/schema/keyword-detector.d.ts +6 -6
- package/dist/config/schema/oh-my-opencode-config.d.ts +404 -402
- package/dist/config/schema/tmux.d.ts +6 -6
- package/dist/create-hooks.d.ts +35 -35
- package/dist/features/background-agent/spawner/tmux-callback-invoker.d.ts +11 -0
- package/dist/features/builtin-commands/templates/start-work.d.ts +1 -1
- package/dist/features/claude-tasks/types.d.ts +4 -4
- package/dist/features/team-mode/team-runtime/shutdown-helpers.d.ts +1 -1
- package/dist/features/team-mode/team-runtime/shutdown-test-fixtures.d.ts +4 -4
- package/dist/features/team-mode/tools/lifecycle-test-fixture.d.ts +42 -42
- package/dist/features/tmux-subagent/environment-eligibility.d.ts +2 -0
- package/dist/features/tmux-subagent/index.d.ts +1 -0
- package/dist/features/tmux-subagent/tracked-session-state.d.ts +1 -0
- package/dist/features/tui-sidebar/snapshot-schema.d.ts +6 -6
- package/dist/help/schema/acp.d.ts +3 -3
- package/dist/help/schema/doctor.d.ts +4 -4
- package/dist/help/schema/status.d.ts +3 -3
- package/dist/hooks/codegraph-bootstrap/hook.d.ts +3 -2
- package/dist/hooks/goal/types.d.ts +5 -5
- package/dist/hooks/non-interactive-env/constants.d.ts +4 -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/hooks/start-work/parse-user-request.d.ts +2 -0
- package/dist/hooks/start-work/worktree-block.d.ts +5 -0
- package/dist/index.js +27370 -21301
- package/dist/mcp/types.d.ts +3 -3
- package/dist/oh-my-opencode.schema.json +11 -0
- package/dist/plugin/chat-message/loop-commands.d.ts +1 -0
- package/dist/plugin/command-execute-before.d.ts +3 -3
- package/dist/plugin/hooks/create-core-hooks.d.ts +25 -25
- package/dist/shared/omo-process-sweep.d.ts +18 -0
- package/dist/shared/tmux/tmux-utils/environment.d.ts +1 -1
- package/dist/shared/tmux/tmux-utils.d.ts +1 -1
- package/dist/shared/typescript-native-source-parser.d.ts +7 -0
- package/dist/skills/coding-agent-sessions/SKILL.md +5 -2
- package/dist/skills/coding-agent-sessions/references/all-platforms.md +3 -1
- package/dist/skills/coding-agent-sessions/references/senpi.md +15 -5
- package/dist/skills/coding-agent-sessions/scripts/agent_sessions/pi_family.py +46 -0
- package/dist/skills/coding-agent-sessions/scripts/agent_sessions/scanners.py +19 -10
- package/dist/skills/debugging/SKILL.md +2 -1
- package/dist/skills/debugging/references/methodology/03-flaky-triage.md +47 -0
- package/dist/skills/programming/SKILL.md +11 -1
- package/dist/skills/programming/references/logging.md +6 -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 +75 -11
- package/dist/skills/start-work/SKILL.md +12 -2
- package/dist/skills/ulw-plan/SKILL.md +21 -2
- package/dist/skills/ulw-plan/references/full-workflow.md +12 -1
- package/dist/skills/ulw-plan/references/intent-clear.md +1 -1
- package/dist/testing/create-plugin-module.d.ts +1 -0
- package/dist/tools/task/types.d.ts +10 -10
- package/dist/tui.js +23442 -19182
- 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 +62 -2
- 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 +72 -12
- 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/dist/cli.js +779 -385
- package/packages/omo-codex/plugin/components/codegraph/dist/serve.js +302 -173
- 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 +23 -42
- 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 +17 -9
- package/packages/omo-codex/plugin/components/codegraph/src/session-start-worker.ts +18 -25
- 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 +5 -0
- package/packages/omo-codex/plugin/components/codegraph/test/hook-store-upgrade.test.ts +176 -0
- package/packages/omo-codex/plugin/components/codegraph/test/hook.test.ts +6 -1
- 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/provisioned-node-guard.test.ts +6 -0
- 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 +2 -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 +42 -0
- package/packages/omo-codex/plugin/components/codegraph/test/serve.test.ts +122 -10
- 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 +101 -64
- package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
- package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
- package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +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/directive.md +1 -1
- 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/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/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/plan.toml +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/directive.md +12 -7
- 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/ultrawork/SKILL.md +12 -7
- package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/SKILL.md +21 -2
- package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/references/full-workflow.md +12 -1
- package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/references/intent-clear.md +1 -1
- 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/directive.md +12 -7
- 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/SKILL.md +2 -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/shared/test/config-loader.test.ts +44 -0
- package/packages/omo-codex/plugin/skills/coding-agent-sessions/SKILL.md +5 -2
- package/packages/omo-codex/plugin/skills/coding-agent-sessions/references/all-platforms.md +3 -1
- package/packages/omo-codex/plugin/skills/coding-agent-sessions/references/senpi.md +15 -5
- package/packages/omo-codex/plugin/skills/coding-agent-sessions/scripts/agent_sessions/pi_family.py +46 -0
- package/packages/omo-codex/plugin/skills/coding-agent-sessions/scripts/agent_sessions/scanners.py +19 -10
- package/packages/omo-codex/plugin/skills/debugging/SKILL.md +2 -1
- package/packages/omo-codex/plugin/skills/debugging/references/methodology/03-flaky-triage.md +47 -0
- package/packages/omo-codex/plugin/skills/programming/SKILL.md +11 -1
- package/packages/omo-codex/plugin/skills/programming/references/logging.md +6 -1
- package/packages/omo-codex/plugin/skills/programming/scripts/typescript/check-no-excuse-rules.ts +75 -11
- package/packages/omo-codex/plugin/skills/start-work/SKILL.md +12 -2
- package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +12 -7
- package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +2 -1
- package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +8 -6
- package/packages/omo-codex/plugin/skills/ulw-plan/SKILL.md +21 -2
- package/packages/omo-codex/plugin/skills/ulw-plan/references/full-workflow.md +12 -1
- package/packages/omo-codex/plugin/skills/ulw-plan/references/intent-clear.md +1 -1
- package/packages/omo-codex/plugin/test/aggregate-agents.test.mjs +9 -9
- 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/sync-skills-orchestration.test.mjs +22 -0
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +49 -5
- package/packages/omo-codex/scripts/install-marketplace-cache.test.mjs +4 -1
- package/packages/shared-skills/skills/coding-agent-sessions/SKILL.md +5 -2
- package/packages/shared-skills/skills/coding-agent-sessions/references/all-platforms.md +3 -1
- package/packages/shared-skills/skills/coding-agent-sessions/references/senpi.md +15 -5
- package/packages/shared-skills/skills/coding-agent-sessions/scripts/agent_sessions/pi_family.py +46 -0
- package/packages/shared-skills/skills/coding-agent-sessions/scripts/agent_sessions/scanners.py +19 -10
- package/packages/shared-skills/skills/debugging/SKILL.md +2 -1
- package/packages/shared-skills/skills/debugging/references/methodology/03-flaky-triage.md +47 -0
- package/packages/shared-skills/skills/programming/SKILL.md +11 -1
- package/packages/shared-skills/skills/programming/references/logging.md +6 -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 +75 -11
- package/packages/shared-skills/skills/start-work/SKILL.md +12 -2
- package/packages/shared-skills/skills/ulw-plan/SKILL.md +21 -2
- package/packages/shared-skills/skills/ulw-plan/references/full-workflow.md +12 -1
- package/packages/shared-skills/skills/ulw-plan/references/intent-clear.md +1 -1
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// biome-ignore-all format: compact batch steering module stays below the pure LOC budget.
|
|
2
|
+
import type { UlwLoopScope } from "./paths.js";
|
|
3
|
+
import { appendLedger, appendLedgerEntries, findAcceptedSteeringLedgerEntry, readUlwLoopPlan, withUlwLoopMutationLock, writePlan } from "./plan-io.js";
|
|
4
|
+
import { applySteeringMutation, validateUlwLoopSteeringProposal } from "./steering.js";
|
|
5
|
+
import { buildSteeringPlanSnapshot, changedGoalIdsBetween } from "./steering-snapshot.js";
|
|
6
|
+
import type { UlwLoopLedgerEntry, UlwLoopPlan, UlwLoopSteeringAudit, UlwLoopSteeringProposal } from "./types.js";
|
|
7
|
+
import { iso } from "./types.js";
|
|
8
|
+
import { batchUpdateLedgerEntry } from "./validation-batch.js";
|
|
9
|
+
|
|
10
|
+
export interface SteerUlwLoopBatchItemResult {
|
|
11
|
+
readonly accepted: boolean;
|
|
12
|
+
readonly deduped: boolean;
|
|
13
|
+
readonly audit: UlwLoopSteeringAudit;
|
|
14
|
+
readonly rejectedReasons: readonly string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface SteerUlwLoopBatchResult {
|
|
18
|
+
readonly plan: UlwLoopPlan;
|
|
19
|
+
readonly accepted: boolean;
|
|
20
|
+
readonly results: readonly SteerUlwLoopBatchItemResult[];
|
|
21
|
+
readonly rejectedReasons: readonly string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type PreparedFresh = {
|
|
25
|
+
readonly proposal: UlwLoopSteeringProposal;
|
|
26
|
+
readonly audit: UlwLoopSteeringAudit;
|
|
27
|
+
readonly before: UlwLoopPlan;
|
|
28
|
+
readonly next: UlwLoopPlan;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type PreparedItem =
|
|
32
|
+
| { readonly kind: "deduped"; readonly result: SteerUlwLoopBatchItemResult }
|
|
33
|
+
| { readonly kind: "fresh"; readonly prepared: PreparedFresh };
|
|
34
|
+
|
|
35
|
+
export async function steerUlwLoopBatch(
|
|
36
|
+
repoRoot: string,
|
|
37
|
+
proposals: readonly UlwLoopSteeringProposal[],
|
|
38
|
+
scope?: UlwLoopScope,
|
|
39
|
+
): Promise<SteerUlwLoopBatchResult> {
|
|
40
|
+
return withUlwLoopMutationLock(repoRoot, scope, async () => {
|
|
41
|
+
const plan = await readUlwLoopPlan(repoRoot, scope);
|
|
42
|
+
const prepared = await prepareBatch(repoRoot, plan, proposals, scope);
|
|
43
|
+
const failed = prepared.results.find((item) => !item.accepted);
|
|
44
|
+
if (failed !== undefined) {
|
|
45
|
+
const entry = rejectedLedgerEntry(prepared.results);
|
|
46
|
+
await appendLedger(repoRoot, entry, scope);
|
|
47
|
+
return rejected(plan, prepared.results, failed.rejectedReasons);
|
|
48
|
+
}
|
|
49
|
+
let next = plan;
|
|
50
|
+
for (const item of prepared.items) if (item.kind === "fresh") next = item.prepared.next;
|
|
51
|
+
const fresh = prepared.items.filter((item): item is Extract<PreparedItem, { readonly kind: "fresh" }> => item.kind === "fresh");
|
|
52
|
+
if (fresh.length > 0) {
|
|
53
|
+
await writePlan(repoRoot, next, scope);
|
|
54
|
+
const entries = fresh.map((item) => ledgerEntry(item.prepared.proposal, item.prepared.audit, item.prepared.proposal.now?.toISOString() ?? iso()));
|
|
55
|
+
const batchEntry = batchUpdateLedgerEntry(plan, next, iso());
|
|
56
|
+
await appendLedgerEntries(repoRoot, batchEntry === null ? entries : [...entries, batchEntry], scope);
|
|
57
|
+
}
|
|
58
|
+
return { plan: next, accepted: true, results: prepared.results, rejectedReasons: [] };
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function prepareBatch(
|
|
63
|
+
repoRoot: string,
|
|
64
|
+
plan: UlwLoopPlan,
|
|
65
|
+
proposals: readonly UlwLoopSteeringProposal[],
|
|
66
|
+
scope: UlwLoopScope | undefined,
|
|
67
|
+
): Promise<{ readonly items: readonly PreparedItem[]; readonly results: readonly SteerUlwLoopBatchItemResult[] }> {
|
|
68
|
+
const items: PreparedItem[] = [];
|
|
69
|
+
const results: SteerUlwLoopBatchItemResult[] = [];
|
|
70
|
+
let current = plan;
|
|
71
|
+
for (const proposal of proposals) {
|
|
72
|
+
const key = proposal.idempotencyKey ?? proposal.promptSignature;
|
|
73
|
+
const prior = key === undefined ? undefined : await findAcceptedSteeringLedgerEntry(repoRoot, key, scope);
|
|
74
|
+
if (prior?.steering !== undefined) {
|
|
75
|
+
const result = { accepted: true, deduped: true, audit: { ...prior.steering, deduped: true }, rejectedReasons: [] };
|
|
76
|
+
items.push({ kind: "deduped", result });
|
|
77
|
+
results.push(result);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const audit = validateUlwLoopSteeringProposal(current, proposal);
|
|
81
|
+
if (!audit.invariant.accepted) {
|
|
82
|
+
const result = { accepted: false, deduped: false, audit, rejectedReasons: audit.invariant.rejectedReasons };
|
|
83
|
+
items.push({ kind: "deduped", result });
|
|
84
|
+
results.push(result);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const next = applySteeringMutation(current, proposal, audit);
|
|
88
|
+
const changed = changedGoalIdsBetween(current, next);
|
|
89
|
+
const finalAudit = { ...audit, before: buildSteeringPlanSnapshot(current, changed), after: buildSteeringPlanSnapshot(next, changed) };
|
|
90
|
+
const result = { accepted: true, deduped: false, audit: finalAudit, rejectedReasons: [] };
|
|
91
|
+
items.push({ kind: "fresh", prepared: { proposal, audit: finalAudit, before: current, next } });
|
|
92
|
+
results.push(result);
|
|
93
|
+
current = next;
|
|
94
|
+
}
|
|
95
|
+
return { items, results };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function rejected(
|
|
99
|
+
plan: UlwLoopPlan,
|
|
100
|
+
results: readonly SteerUlwLoopBatchItemResult[],
|
|
101
|
+
rejectedReasons: readonly string[],
|
|
102
|
+
): SteerUlwLoopBatchResult {
|
|
103
|
+
return { plan, accepted: false, results, rejectedReasons };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function rejectedLedgerEntry(results: readonly SteerUlwLoopBatchItemResult[]): UlwLoopLedgerEntry {
|
|
107
|
+
const rejectedItems = results.map((result, index) => ({ result, index })).filter((item) => !item.result.accepted);
|
|
108
|
+
return { at: iso(), kind: "steering_rejected", message: rejectedItems.map((item) => `index ${item.index}: ${item.result.rejectedReasons.join(", ")}`).join("; ") };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function ledgerEntry(proposal: UlwLoopSteeringProposal, audit: UlwLoopSteeringAudit, at: string): UlwLoopLedgerEntry {
|
|
112
|
+
const entry: UlwLoopLedgerEntry = {
|
|
113
|
+
at,
|
|
114
|
+
kind: proposal.kind === "revise_criterion" ? "criteria_revised" : "steering_accepted",
|
|
115
|
+
evidence: proposal.evidence,
|
|
116
|
+
message: proposal.rationale,
|
|
117
|
+
steering: audit,
|
|
118
|
+
mutationKind: proposal.kind,
|
|
119
|
+
};
|
|
120
|
+
const goalId = audit.targetGoalIds[0];
|
|
121
|
+
if (goalId !== undefined) entry.goalId = goalId;
|
|
122
|
+
if (proposal.criterionId !== undefined) entry.criterionId = proposal.criterionId;
|
|
123
|
+
if (proposal.idempotencyKey !== undefined) entry.idempotencyKey = proposal.idempotencyKey;
|
|
124
|
+
return entry;
|
|
125
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// biome-ignore-all format: compact extracted steering mutations stay below the pure LOC budget.
|
|
2
|
+
import { seedDefaultSuccessCriteria } from "./plan-crud.js";
|
|
3
|
+
import type { UlwLoopItem, UlwLoopPlan, UlwLoopSteeringChildGoal, UlwLoopSteeringProposal, UlwLoopSuccessCriterionUserModel } from "./types.js";
|
|
4
|
+
import { ULW_LOOP_SUCCESS_CRITERION_USER_MODELS } from "./types.js";
|
|
5
|
+
import { updateBatchesAfterSupersede } from "./validation-batch.js";
|
|
6
|
+
|
|
7
|
+
const read = (value: object, key: string): unknown => Object.entries(value).find(([name]) => name === key)?.[1];
|
|
8
|
+
const isText = (value: unknown): value is string => typeof value === "string" && value.trim().length > 0;
|
|
9
|
+
const text = (value: object, key: string): string | undefined => { const candidate = read(value, key); return isText(candidate) ? candidate.trim() : undefined; };
|
|
10
|
+
const isModel = (value: unknown): value is UlwLoopSuccessCriterionUserModel => typeof value === "string" && ULW_LOOP_SUCCESS_CRITERION_USER_MODELS.some((model) => model === value);
|
|
11
|
+
const after = (proposal: object): object | undefined => { const candidate = read(proposal, "after"); return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate) ? candidate : undefined; };
|
|
12
|
+
const revised = (proposal: object, direct: string, nested: string): string | undefined => text(proposal, direct) ?? text(after(proposal) ?? proposal, nested);
|
|
13
|
+
const targets = (proposal: UlwLoopSteeringProposal): string[] => proposal.targetGoalIds ?? [proposal.targetGoalId ?? text(proposal, "goalId") ?? ""].filter(Boolean);
|
|
14
|
+
const child = (value: unknown): UlwLoopSteeringChildGoal | null => {
|
|
15
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
16
|
+
const title = text(value, "title"); const objective = text(value, "objective");
|
|
17
|
+
return title === undefined || objective === undefined ? null : { title, objective };
|
|
18
|
+
};
|
|
19
|
+
const children = (proposal: UlwLoopSteeringProposal): UlwLoopSteeringChildGoal[] => {
|
|
20
|
+
const direct = proposal.childGoals;
|
|
21
|
+
if (direct !== undefined && direct.length > 0) return direct;
|
|
22
|
+
const nested = after(proposal);
|
|
23
|
+
const fromAfter = nested === undefined ? undefined : read(nested, "children");
|
|
24
|
+
return Array.isArray(fromAfter) ? fromAfter.map(child).filter((item): item is UlwLoopSteeringChildGoal => item !== null) : [];
|
|
25
|
+
};
|
|
26
|
+
const goal = (plan: UlwLoopPlan, id: string | undefined): UlwLoopItem | undefined => id === undefined ? undefined : plan.goals.find((item) => item.id === id);
|
|
27
|
+
|
|
28
|
+
function nextId(plan: UlwLoopPlan, offset: number): string {
|
|
29
|
+
const max = plan.goals.reduce((current, item) => { const digits = /^G(\d+)(?:-|$)/u.exec(item.id)?.[1]; return digits === undefined ? current : Math.max(current, Number(digits)); }, 0);
|
|
30
|
+
return `G${String(max + offset).padStart(3, "0")}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function makeGoal(plan: UlwLoopPlan, childGoal: UlwLoopSteeringChildGoal, evidence: string, now: string, offset: number): UlwLoopItem {
|
|
34
|
+
const id = nextId(plan, offset); const digits = /^G(\d+)/u.exec(id)?.[1]; const goalIndex = digits === undefined ? plan.goals.length + offset - 1 : Number(digits) - 1;
|
|
35
|
+
return { id, title: childGoal.title, objective: childGoal.objective, status: "pending", successCriteria: seedDefaultSuccessCriteria(goalIndex, childGoal.objective), attempt: 0, createdAt: now, updatedAt: now, evidence };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function reviseWording(plan: UlwLoopPlan, proposal: UlwLoopSteeringProposal, now: string): void {
|
|
39
|
+
const target = goal(plan, targets(proposal)[0]); if (target === undefined) return;
|
|
40
|
+
target.title = revised(proposal, "revisedTitle", "title") ?? target.title; target.objective = revised(proposal, "revisedObjective", "objective") ?? target.objective;
|
|
41
|
+
target.steeringEvidence = proposal.evidence; target.steeringRationale = proposal.rationale; target.updatedAt = now;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function splitOrBlock(plan: UlwLoopPlan, proposal: UlwLoopSteeringProposal, now: string): void {
|
|
45
|
+
const target = goal(plan, targets(proposal)[0]); if (target === undefined) return;
|
|
46
|
+
const replacements = children(proposal).map((item, index) => makeGoal(plan, item, proposal.evidence, now, index + 1));
|
|
47
|
+
target.steeringEvidence = proposal.evidence; target.steeringRationale = proposal.rationale; target.updatedAt = now;
|
|
48
|
+
if (replacements.length === 0) { target.status = "blocked"; target.steeringStatus = "blocked"; target.blockedReason = proposal.blockedReason ?? proposal.rationale; }
|
|
49
|
+
else { target.steeringStatus = "superseded"; target.supersededBy = replacements.map((item) => item.id); for (const item of replacements) item.supersedes = [target.id]; plan.goals.splice(plan.goals.indexOf(target) + 1, 0, ...replacements); updateBatchesAfterSupersede(plan, target.id, replacements.map((item) => item.id)); }
|
|
50
|
+
if (plan.activeGoalId === target.id) delete plan.activeGoalId;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function reviseCriterion(plan: UlwLoopPlan, proposal: UlwLoopSteeringProposal, now: string): void {
|
|
54
|
+
const target = goal(plan, targets(proposal)[0]); const index = target?.successCriteria.findIndex((item) => item.id === proposal.criterionId) ?? -1; const current = target?.successCriteria[index];
|
|
55
|
+
if (target === undefined || current === undefined) return;
|
|
56
|
+
const model = read(proposal, "userModel");
|
|
57
|
+
target.successCriteria[index] = { ...current, scenario: text(proposal, "scenario") ?? current.scenario, expectedEvidence: text(proposal, "expectedEvidence") ?? current.expectedEvidence, userModel: isModel(model) ? model : current.userModel };
|
|
58
|
+
target.updatedAt = now;
|
|
59
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// biome-ignore-all format: compact steering module must stay below the 240 pure-LOC budget
|
|
2
2
|
import { isUlwLoopDone } from "./goal-status.js";
|
|
3
3
|
import type { UlwLoopScope } from "./paths.js";
|
|
4
|
-
import { seedDefaultSuccessCriteria } from "./plan-crud.js";
|
|
5
4
|
import { appendLedger, findAcceptedSteeringLedgerEntry, readUlwLoopPlan, withUlwLoopMutationLock, writePlan } from "./plan-io.js";
|
|
5
|
+
import { makeGoal, reviseCriterion, reviseWording, splitOrBlock } from "./steering-mutations.js";
|
|
6
6
|
import { buildSteeringPlanSnapshot, changedGoalIdsBetween } from "./steering-snapshot.js";
|
|
7
7
|
import type {
|
|
8
8
|
SteerUlwLoopResult,
|
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
UlwLoopSuccessCriterionUserModel,
|
|
18
18
|
} from "./types.js";
|
|
19
19
|
import { iso, ULW_LOOP_STEERING_MUTATION_KINDS, ULW_LOOP_SUCCESS_CRITERION_USER_MODELS } from "./types.js";
|
|
20
|
+
import { batchUpdateLedgerEntry } from "./validation-batch.js";
|
|
20
21
|
|
|
21
22
|
const SOURCES = ["user_prompt_submit", "finding", "cli"] as const satisfies readonly UlwLoopSteeringSource[];
|
|
22
23
|
const PROTECTED = new Set(["aggregateCompletion", "codexObjective", "codexObjectiveAliases", "originalConstraints", "qualityGate", "status", "completedAt", "completionStatus"]);
|
|
@@ -63,7 +64,6 @@ function childValues(proposal: object): unknown[] {
|
|
|
63
64
|
return Array.isArray(fromAfter) ? fromAfter : [];
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
const children = (proposal: object): UlwLoopSteeringChildGoal[] => childValues(proposal).map(child).filter((item): item is UlwLoopSteeringChildGoal => item !== null);
|
|
67
67
|
const pendingOrder = (proposal: object): string[] => {
|
|
68
68
|
const direct = texts(proposal, "pendingOrder");
|
|
69
69
|
return direct.length > 0 ? direct : texts(after(proposal) ?? proposal, "pendingGoalIds");
|
|
@@ -155,21 +155,6 @@ function validateCriterion(plan: UlwLoopPlan, proposal: object, reasons: string[
|
|
|
155
155
|
if (model !== undefined && !isModel(model)) reasons.push("invalid userModel");
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
function nextId(plan: UlwLoopPlan, offset: number): string {
|
|
159
|
-
const max = plan.goals.reduce((current, item) => {
|
|
160
|
-
const digits = /^G(\d+)(?:-|$)/u.exec(item.id)?.[1];
|
|
161
|
-
return digits === undefined ? current : Math.max(current, Number(digits));
|
|
162
|
-
}, 0);
|
|
163
|
-
return `G${String(max + offset).padStart(3, "0")}`;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function makeGoal(plan: UlwLoopPlan, childGoal: UlwLoopSteeringChildGoal, evidence: string, now: string, offset: number): UlwLoopItem {
|
|
167
|
-
const id = nextId(plan, offset);
|
|
168
|
-
const digits = /^G(\d+)/u.exec(id)?.[1];
|
|
169
|
-
const goalIndex = digits === undefined ? plan.goals.length + offset - 1 : Number(digits) - 1;
|
|
170
|
-
return { id, title: childGoal.title, objective: childGoal.objective, status: "pending", successCriteria: seedDefaultSuccessCriteria(goalIndex, childGoal.objective), attempt: 0, createdAt: now, updatedAt: now, evidence };
|
|
171
|
-
}
|
|
172
|
-
|
|
173
158
|
export function applySteeringMutation(plan: UlwLoopPlan, proposal: UlwLoopSteeringProposal, audit: UlwLoopSteeringAudit): UlwLoopPlan {
|
|
174
159
|
const next = structuredClone(plan);
|
|
175
160
|
if (!audit.invariant.accepted) return next;
|
|
@@ -186,46 +171,6 @@ export function applySteeringMutation(plan: UlwLoopPlan, proposal: UlwLoopSteeri
|
|
|
186
171
|
return next;
|
|
187
172
|
}
|
|
188
173
|
|
|
189
|
-
function reviseWording(plan: UlwLoopPlan, proposal: UlwLoopSteeringProposal, now: string): void {
|
|
190
|
-
const target = goal(plan, targets(proposal)[0]);
|
|
191
|
-
if (target === undefined) return;
|
|
192
|
-
target.title = revised(proposal, "revisedTitle", "title") ?? target.title;
|
|
193
|
-
target.objective = revised(proposal, "revisedObjective", "objective") ?? target.objective;
|
|
194
|
-
target.steeringEvidence = proposal.evidence;
|
|
195
|
-
target.steeringRationale = proposal.rationale;
|
|
196
|
-
target.updatedAt = now;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function splitOrBlock(plan: UlwLoopPlan, proposal: UlwLoopSteeringProposal, now: string): void {
|
|
200
|
-
const target = goal(plan, targets(proposal)[0]);
|
|
201
|
-
if (target === undefined) return;
|
|
202
|
-
const replacements = children(proposal).map((item, index) => makeGoal(plan, item, proposal.evidence, now, index + 1));
|
|
203
|
-
target.steeringEvidence = proposal.evidence;
|
|
204
|
-
target.steeringRationale = proposal.rationale;
|
|
205
|
-
target.updatedAt = now;
|
|
206
|
-
if (replacements.length === 0) {
|
|
207
|
-
target.status = "blocked";
|
|
208
|
-
target.steeringStatus = "blocked";
|
|
209
|
-
target.blockedReason = proposal.blockedReason ?? proposal.rationale;
|
|
210
|
-
} else {
|
|
211
|
-
target.steeringStatus = "superseded";
|
|
212
|
-
target.supersededBy = replacements.map((item) => item.id);
|
|
213
|
-
for (const item of replacements) item.supersedes = [target.id];
|
|
214
|
-
plan.goals.splice(plan.goals.indexOf(target) + 1, 0, ...replacements);
|
|
215
|
-
}
|
|
216
|
-
if (plan.activeGoalId === target.id) delete plan.activeGoalId;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function reviseCriterion(plan: UlwLoopPlan, proposal: UlwLoopSteeringProposal, now: string): void {
|
|
220
|
-
const target = goal(plan, targets(proposal)[0]);
|
|
221
|
-
const index = target?.successCriteria.findIndex((item) => item.id === proposal.criterionId) ?? -1;
|
|
222
|
-
const current = target?.successCriteria[index];
|
|
223
|
-
if (target === undefined || current === undefined) return;
|
|
224
|
-
const model = read(proposal, "userModel");
|
|
225
|
-
target.successCriteria[index] = { ...current, scenario: text(proposal, "scenario") ?? current.scenario, expectedEvidence: text(proposal, "expectedEvidence") ?? current.expectedEvidence, userModel: isModel(model) ? model : current.userModel };
|
|
226
|
-
target.updatedAt = now;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
174
|
function isProposal(value: unknown): value is UlwLoopSteeringProposal {
|
|
230
175
|
return isPlain(value) && isKind(read(value, "kind")) && isSource(read(value, "source")) && isText(read(value, "evidence")) && isText(read(value, "rationale"));
|
|
231
176
|
}
|
|
@@ -262,8 +207,11 @@ export async function steerUlwLoop(repoRoot: string, proposal: UlwLoopSteeringPr
|
|
|
262
207
|
finalAudit.before = buildSteeringPlanSnapshot(plan, changed);
|
|
263
208
|
finalAudit.after = buildSteeringPlanSnapshot(next, changed);
|
|
264
209
|
}
|
|
210
|
+
const at = proposal.now?.toISOString() ?? iso();
|
|
211
|
+
const batchEntry = accepted ? batchUpdateLedgerEntry(plan, next, at) : null;
|
|
265
212
|
if (accepted) await writePlan(repoRoot, next, scope);
|
|
266
|
-
await appendLedger(repoRoot, ledgerEntry(proposal, finalAudit,
|
|
213
|
+
await appendLedger(repoRoot, ledgerEntry(proposal, finalAudit, at), scope);
|
|
214
|
+
if (batchEntry !== null) await appendLedger(repoRoot, batchEntry, scope);
|
|
267
215
|
return { plan: next, accepted, audit: finalAudit, rejectedReasons: audit.invariant.rejectedReasons, deduped: false };
|
|
268
216
|
});
|
|
269
217
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { isAbsolute, join } from "node:path";
|
|
2
|
+
import { isAbsolute, join, resolve, sep } from "node:path";
|
|
3
3
|
|
|
4
4
|
import { normalizeUlwLoopSessionId, ulwLoopDir } from "./paths.js";
|
|
5
5
|
import type { UlwLoopItem, UlwLoopPlan } from "./types.js";
|
|
@@ -73,17 +73,25 @@ function isResumableStatus(status: UlwLoopItem["status"]): boolean {
|
|
|
73
73
|
// a separate file — a ledger append would change the count and self-reset.
|
|
74
74
|
function consumeResumeBudget(stateDir: string, goalId: string): boolean {
|
|
75
75
|
const ledgerLineCount = countLedgerLines(join(stateDir, "ledger.jsonl"));
|
|
76
|
-
const counterPath =
|
|
76
|
+
const counterPath = resolve(stateDir, `auto-resume-${goalId}.json`);
|
|
77
|
+
const stuckPath = resolve(stateDir, `auto-resume-${goalId}.stuck`);
|
|
78
|
+
// goals.json is untrusted input: a crafted goal id (e.g. `../../x`) must
|
|
79
|
+
// never drive a write outside the session state dir. Deny the resume.
|
|
80
|
+
if (!isInsideDir(stateDir, counterPath) || !isInsideDir(stateDir, stuckPath)) return false;
|
|
77
81
|
const previous = readCounter(counterPath);
|
|
78
82
|
const count = previous !== null && previous.ledgerLineCount === ledgerLineCount ? previous.count : 0;
|
|
79
83
|
if (count >= RESUME_CAP) {
|
|
80
|
-
writeFileSync(
|
|
84
|
+
writeFileSync(stuckPath, `no ledger progress after ${count} resumes\n`);
|
|
81
85
|
return false;
|
|
82
86
|
}
|
|
83
87
|
writeFileSync(counterPath, JSON.stringify({ count: count + 1, ledgerLineCount }));
|
|
84
88
|
return true;
|
|
85
89
|
}
|
|
86
90
|
|
|
91
|
+
function isInsideDir(dir: string, candidate: string): boolean {
|
|
92
|
+
return candidate.startsWith(resolve(dir) + sep);
|
|
93
|
+
}
|
|
94
|
+
|
|
87
95
|
function renderResumeDirective(plan: UlwLoopPlan, goal: UlwLoopItem, sessionId: string): string {
|
|
88
96
|
const normalized = normalizeUlwLoopSessionId(sessionId);
|
|
89
97
|
const option =
|
|
@@ -93,7 +101,7 @@ function renderResumeDirective(plan: UlwLoopPlan, goal: UlwLoopItem, sessionId:
|
|
|
93
101
|
"The turn ended before the loop completed. Resume it now:",
|
|
94
102
|
`1. Run \`omo ulw-loop status${option} --json\` to reload the plan, the active goal, and currentAttemptDir.`,
|
|
95
103
|
"2. Continue the active goal's remaining success criteria, recording evidence with record-evidence.",
|
|
96
|
-
`3. Checkpoint through \`omo ulw-loop checkpoint${option}\` when the goal's criteria are proven.`,
|
|
104
|
+
`3. Checkpoint through \`omo ulw-loop checkpoint${option}\` when the goal's criteria are proven; a complete checkpoint prints the next goal instruction.`,
|
|
97
105
|
"If the loop is genuinely blocked on the user, checkpoint the goal as blocked with the reason instead.",
|
|
98
106
|
].join("\n");
|
|
99
107
|
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// biome-ignore-all format: compact validation-batch helpers stay below the pure LOC budget.
|
|
2
|
+
import { readJsonInput } from "./cli-arg-parser.js";
|
|
3
|
+
import { isMemberResolved } from "./goal-status.js";
|
|
4
|
+
import type { UlwLoopItem, UlwLoopLedgerEntry, UlwLoopPlan, UlwLoopQualityGate, UlwLoopValidationBatch } from "./types.js";
|
|
5
|
+
import { UlwLoopError } from "./types.js";
|
|
6
|
+
|
|
7
|
+
function isObject(value: unknown): value is object {
|
|
8
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function read(value: object, key: string): unknown {
|
|
12
|
+
return Object.entries(value).find(([name]) => name === key)?.[1];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function text(value: object, key: string): string | undefined {
|
|
16
|
+
const candidate = read(value, key);
|
|
17
|
+
return typeof candidate === "string" && candidate.trim().length > 0 ? candidate.trim() : undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function strings(value: object, key: string): readonly string[] | undefined {
|
|
21
|
+
const candidate = read(value, key);
|
|
22
|
+
return Array.isArray(candidate) && candidate.every((item) => typeof item === "string" && item.trim().length > 0)
|
|
23
|
+
? candidate.map((item) => item.trim())
|
|
24
|
+
: undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function parseValidationBatches(
|
|
28
|
+
input: string | undefined,
|
|
29
|
+
goals: readonly UlwLoopItem[],
|
|
30
|
+
): Promise<readonly UlwLoopValidationBatch[] | undefined> {
|
|
31
|
+
const raw = await readJsonInput(input);
|
|
32
|
+
if (raw === undefined) return undefined;
|
|
33
|
+
if (!Array.isArray(raw)) fail("--validation-batch-json must be a JSON array.");
|
|
34
|
+
const batches = raw.map(batchFromObject);
|
|
35
|
+
validateBatches(batches, goals);
|
|
36
|
+
return batches;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function batchFromObject(value: unknown): UlwLoopValidationBatch {
|
|
40
|
+
if (!isObject(value)) fail("validation batch entries must be objects.");
|
|
41
|
+
const batchId = text(value, "batchId");
|
|
42
|
+
const memberIds = strings(value, "memberIds");
|
|
43
|
+
const finalGoalId = text(value, "finalGoalId");
|
|
44
|
+
if (batchId === undefined) fail("validation batch requires batchId.");
|
|
45
|
+
if (memberIds === undefined || memberIds.length < 2) fail("validation batch requires at least two memberIds.");
|
|
46
|
+
if (finalGoalId === undefined) fail("validation batch requires finalGoalId.");
|
|
47
|
+
return { batchId, memberIds, finalGoalId };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function validateBatches(batches: readonly UlwLoopValidationBatch[], goals: readonly UlwLoopItem[]): void {
|
|
51
|
+
const goalIds = new Set(goals.map((goal) => goal.id));
|
|
52
|
+
const batchIds = new Set<string>();
|
|
53
|
+
const members = new Set<string>();
|
|
54
|
+
for (const batch of batches) {
|
|
55
|
+
if (batchIds.has(batch.batchId)) fail(`duplicate validation batch id: ${batch.batchId}.`);
|
|
56
|
+
batchIds.add(batch.batchId);
|
|
57
|
+
if (new Set(batch.memberIds).size !== batch.memberIds.length) fail(`validation batch ${batch.batchId} has duplicate memberIds.`);
|
|
58
|
+
if (!batch.memberIds.includes(batch.finalGoalId)) fail(`validation batch ${batch.batchId} finalGoalId must be a member.`, "ULW_LOOP_VALIDATION_BATCH_FINAL_NOT_MEMBER");
|
|
59
|
+
for (const memberId of batch.memberIds) {
|
|
60
|
+
if (!goalIds.has(memberId)) fail(`validation batch ${batch.batchId} references unknown goal: ${memberId}.`, "ULW_LOOP_VALIDATION_BATCH_MEMBER_UNKNOWN");
|
|
61
|
+
if (members.has(memberId)) fail(`goal appears in multiple validation batches: ${memberId}.`, "ULW_LOOP_VALIDATION_BATCH_OVERLAP");
|
|
62
|
+
members.add(memberId);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function updateBatchesAfterSupersede(plan: UlwLoopPlan, targetId: string, replacementIds: readonly string[]): void {
|
|
68
|
+
if (replacementIds.length === 0 || plan.validationBatches === undefined) return;
|
|
69
|
+
plan.validationBatches = plan.validationBatches.map((batch) => {
|
|
70
|
+
if (!batch.memberIds.includes(targetId)) return batch;
|
|
71
|
+
const memberIds = batch.memberIds.flatMap((id) => (id === targetId ? [...replacementIds] : [id]));
|
|
72
|
+
const replacementFinalGoalId = replacementIds[replacementIds.length - 1] ?? batch.finalGoalId;
|
|
73
|
+
const finalGoalId = batch.finalGoalId === targetId ? replacementFinalGoalId : batch.finalGoalId;
|
|
74
|
+
return { batchId: batch.batchId, memberIds, finalGoalId };
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function batchUpdateLedgerEntry(before: UlwLoopPlan, after: UlwLoopPlan, at: string): UlwLoopLedgerEntry | null {
|
|
79
|
+
if (JSON.stringify(before.validationBatches ?? []) === JSON.stringify(after.validationBatches ?? [])) return null;
|
|
80
|
+
return { at, kind: "batch_updated", before: before.validationBatches ?? [], after: after.validationBatches ?? [], message: "Validation batch membership updated after steering." };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function batchOf(plan: UlwLoopPlan, goalId: string): UlwLoopValidationBatch | undefined {
|
|
84
|
+
return plan.validationBatches?.find((batch) => batch.memberIds.includes(goalId));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function batchClosedBy(plan: UlwLoopPlan, goalId: string): UlwLoopValidationBatch | undefined {
|
|
88
|
+
const batch = batchOf(plan, goalId);
|
|
89
|
+
return batch?.finalGoalId === goalId ? batch : undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function requireBatchFinalReady(plan: UlwLoopPlan, goal: UlwLoopItem): void {
|
|
93
|
+
const batch = batchClosedBy(plan, goal.id);
|
|
94
|
+
if (batch === undefined) return;
|
|
95
|
+
const open = batch.memberIds.filter((id) => id !== goal.id && !memberResolved(plan, id));
|
|
96
|
+
if (open.length > 0) throw new UlwLoopError("Validation batch has unresolved members.", "ULW_LOOP_VALIDATION_BATCH_OPEN", { details: { batchId: batch.batchId, open } });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function requireAllValidationBatchesClosed(plan: UlwLoopPlan, closingGoalId?: string): void {
|
|
100
|
+
const open = (plan.validationBatches ?? []).filter((batch) => batch.memberIds.some((id) => id !== closingGoalId && !memberResolved(plan, id)));
|
|
101
|
+
if (open.length > 0) throw new UlwLoopError("Validation batches remain open.", "ULW_LOOP_VALIDATION_BATCH_OPEN", { details: { batchIds: open.map((batch) => batch.batchId) } });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function requireBatchGate(plan: UlwLoopPlan, goal: UlwLoopItem, gate: UlwLoopQualityGate): void {
|
|
105
|
+
const batch = batchClosedBy(plan, goal.id);
|
|
106
|
+
if (batch === undefined) return;
|
|
107
|
+
const members = batch.memberIds.map((id) => plan.goals.find((item) => item.id === id)).filter((item): item is UlwLoopItem => item !== undefined);
|
|
108
|
+
const pending = members.flatMap((member) => member.successCriteria.filter((criterion) => criterion.status !== "pass").map((criterion) => `${member.id}:${criterion.id}`));
|
|
109
|
+
if (pending.length > 0) throw new UlwLoopError("Validation batch criteria remain pending.", "ULW_LOOP_VALIDATION_BATCH_CRITERIA_PENDING", { details: { batchId: batch.batchId, pending } });
|
|
110
|
+
const totalCriteria = members.reduce((sum, member) => sum + member.successCriteria.length, 0);
|
|
111
|
+
const passCount = members.reduce((sum, member) => sum + member.successCriteria.filter((criterion) => criterion.status === "pass").length, 0);
|
|
112
|
+
if (gate.criteriaCoverage.totalCriteria !== totalCriteria || gate.criteriaCoverage.passCount !== passCount) 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 } });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function memberResolved(plan: UlwLoopPlan, goalId: string): boolean {
|
|
116
|
+
const goal = plan.goals.find((candidate) => candidate.id === goalId);
|
|
117
|
+
return goal !== undefined && isMemberResolved(goal, plan);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function fail(message: string, code = "ULW_LOOP_VALIDATION_BATCH_INVALID"): never {
|
|
121
|
+
throw new UlwLoopError(message, code);
|
|
122
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// biome-ignore-all format: compact continuation tests stay under the pure LOC budget.
|
|
2
|
+
import { rm } from "node:fs/promises";
|
|
3
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
4
|
+
|
|
5
|
+
import { checkpointAndContinue } from "../src/checkpoint-continuation.js";
|
|
6
|
+
import { readUlwLoopPlan } from "../src/plan-io.js";
|
|
7
|
+
import { goal, plan, repoWith, snapshot } from "./fixtures/checkpoint-builders.js";
|
|
8
|
+
|
|
9
|
+
let repo: string | undefined;
|
|
10
|
+
|
|
11
|
+
afterEach(async () => {
|
|
12
|
+
if (repo !== undefined) await rm(repo, { recursive: true, force: true });
|
|
13
|
+
repo = undefined;
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe("checkpointAndContinue", () => {
|
|
17
|
+
it("#given a completed non-final goal #when advance is enabled #then starts the next pending goal", async () => {
|
|
18
|
+
repo = await repoWith(
|
|
19
|
+
plan([
|
|
20
|
+
goal({ id: "G001", status: "in_progress" }),
|
|
21
|
+
goal({ id: "G002", status: "pending", attempt: 0 }),
|
|
22
|
+
]),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const result = await checkpointAndContinue(repo, {
|
|
26
|
+
goalId: "G001",
|
|
27
|
+
status: "complete",
|
|
28
|
+
evidence: "implementation done and validation passed",
|
|
29
|
+
codexGoalJson: snapshot("active"),
|
|
30
|
+
advance: true,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
expect(result.next).toMatchObject({ resumed: false, goal: { id: "G002", status: "in_progress" } });
|
|
34
|
+
expect(result.next && "instruction" in result.next ? result.next.instruction.text : "").toContain("Goal: G002");
|
|
35
|
+
expect((await readUlwLoopPlan(repo)).activeGoalId).toBe("G002");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("#given advance disabled #when a non-final goal completes #then preserves the old two-call state", async () => {
|
|
39
|
+
repo = await repoWith(
|
|
40
|
+
plan([
|
|
41
|
+
goal({ id: "G001", status: "in_progress" }),
|
|
42
|
+
goal({ id: "G002", status: "pending", attempt: 0 }),
|
|
43
|
+
]),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const result = await checkpointAndContinue(repo, {
|
|
47
|
+
goalId: "G001",
|
|
48
|
+
status: "complete",
|
|
49
|
+
evidence: "implementation done and validation passed",
|
|
50
|
+
codexGoalJson: snapshot("active"),
|
|
51
|
+
advance: false,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
expect(result.next).toBeUndefined();
|
|
55
|
+
expect((await readUlwLoopPlan(repo)).activeGoalId).toBeUndefined();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("#given a failed checkpoint #when advance is enabled #then it does not start the next goal", async () => {
|
|
59
|
+
repo = await repoWith(
|
|
60
|
+
plan([
|
|
61
|
+
goal({ id: "G001", status: "in_progress" }),
|
|
62
|
+
goal({ id: "G002", status: "pending", attempt: 0 }),
|
|
63
|
+
]),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const result = await checkpointAndContinue(repo, {
|
|
67
|
+
goalId: "G001",
|
|
68
|
+
status: "failed",
|
|
69
|
+
evidence: "implementation failed and validation captured",
|
|
70
|
+
advance: true,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
expect(result.next).toBeUndefined();
|
|
74
|
+
expect((await readUlwLoopPlan(repo)).activeGoalId).toBeUndefined();
|
|
75
|
+
});
|
|
76
|
+
});
|
package/packages/omo-codex/plugin/components/ulw-loop/test/cli-checkpoint-continuation.test.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
|
|
6
|
+
import { ulwLoopCommand } from "../src/cli-commands.js";
|
|
7
|
+
import { ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE } from "../src/goal-status.js";
|
|
8
|
+
|
|
9
|
+
let testDir: string;
|
|
10
|
+
let out: string[];
|
|
11
|
+
let originalOmoSessionId: string | undefined;
|
|
12
|
+
|
|
13
|
+
beforeEach(async () => {
|
|
14
|
+
testDir = await mkdtemp(join(tmpdir(), "ug-cli-checkpoint-next-"));
|
|
15
|
+
out = [];
|
|
16
|
+
originalOmoSessionId = process.env["OMO_ULW_LOOP_SESSION_ID"];
|
|
17
|
+
delete process.env["OMO_ULW_LOOP_SESSION_ID"];
|
|
18
|
+
vi.spyOn(process, "cwd").mockReturnValue(testDir);
|
|
19
|
+
vi.spyOn(process.stdout, "write").mockImplementation((chunk: string | Uint8Array): boolean => {
|
|
20
|
+
out.push(chunk.toString());
|
|
21
|
+
return true;
|
|
22
|
+
});
|
|
23
|
+
vi.spyOn(process.stderr, "write").mockImplementation((): boolean => true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(async () => {
|
|
27
|
+
vi.restoreAllMocks();
|
|
28
|
+
if (originalOmoSessionId === undefined) delete process.env["OMO_ULW_LOOP_SESSION_ID"];
|
|
29
|
+
else process.env["OMO_ULW_LOOP_SESSION_ID"] = originalOmoSessionId;
|
|
30
|
+
await rm(testDir, { recursive: true, force: true });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
function resetOutput(): void {
|
|
34
|
+
out = [];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function stdoutJson(): Record<string, unknown> {
|
|
38
|
+
return JSON.parse(out.join(""));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function codexSnapshot(): string {
|
|
42
|
+
return JSON.stringify({ goal: { objective: ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE, status: "active" } });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function passCriterion(criterionId: string): Promise<void> {
|
|
46
|
+
expect(
|
|
47
|
+
await ulwLoopCommand([
|
|
48
|
+
"record-evidence",
|
|
49
|
+
"--goal-id",
|
|
50
|
+
"G001-goal-a",
|
|
51
|
+
"--criterion-id",
|
|
52
|
+
criterionId,
|
|
53
|
+
"--status",
|
|
54
|
+
"pass",
|
|
55
|
+
"--evidence",
|
|
56
|
+
`${criterionId} observable proof`,
|
|
57
|
+
]),
|
|
58
|
+
).toBe(0);
|
|
59
|
+
resetOutput();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
describe("ulwLoopCommand checkpoint continuation", () => {
|
|
63
|
+
it("#given another pending goal #when checkpoint completes #then JSON includes the next goal", async () => {
|
|
64
|
+
expect(await ulwLoopCommand(["create-goals", "--brief", "- Goal A\n- Goal B", "--json"])).toBe(0);
|
|
65
|
+
resetOutput();
|
|
66
|
+
await passCriterion("C001");
|
|
67
|
+
await passCriterion("C002");
|
|
68
|
+
|
|
69
|
+
expect(
|
|
70
|
+
await ulwLoopCommand([
|
|
71
|
+
"checkpoint",
|
|
72
|
+
"--goal-id",
|
|
73
|
+
"G001-goal-a",
|
|
74
|
+
"--status",
|
|
75
|
+
"complete",
|
|
76
|
+
"--evidence",
|
|
77
|
+
"implementation done and validation passed",
|
|
78
|
+
"--codex-goal-json",
|
|
79
|
+
codexSnapshot(),
|
|
80
|
+
"--json",
|
|
81
|
+
]),
|
|
82
|
+
).toBe(0);
|
|
83
|
+
expect(stdoutJson()).toMatchObject({
|
|
84
|
+
next: { resumed: false, goal: { id: "G002-goal-b", status: "in_progress" } },
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("#given no-advance #when checkpoint completes #then JSON omits next", async () => {
|
|
89
|
+
expect(await ulwLoopCommand(["create-goals", "--brief", "- Goal A\n- Goal B", "--json"])).toBe(0);
|
|
90
|
+
resetOutput();
|
|
91
|
+
await passCriterion("C001");
|
|
92
|
+
await passCriterion("C002");
|
|
93
|
+
|
|
94
|
+
expect(
|
|
95
|
+
await ulwLoopCommand([
|
|
96
|
+
"checkpoint",
|
|
97
|
+
"--goal-id",
|
|
98
|
+
"G001-goal-a",
|
|
99
|
+
"--status",
|
|
100
|
+
"complete",
|
|
101
|
+
"--evidence",
|
|
102
|
+
"implementation done and validation passed",
|
|
103
|
+
"--codex-goal-json",
|
|
104
|
+
codexSnapshot(),
|
|
105
|
+
"--no-advance",
|
|
106
|
+
"--json",
|
|
107
|
+
]),
|
|
108
|
+
).toBe(0);
|
|
109
|
+
expect(stdoutJson()).not.toHaveProperty("next");
|
|
110
|
+
});
|
|
111
|
+
});
|