oh-my-opencode 5.0.0-beta.30 → 5.0.0-beta.32
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/dist/cli/index.js +178 -37
- package/dist/cli-node/index.js +178 -37
- package/dist/config/schema/background-task.d.ts +1 -0
- package/dist/config/schema/oh-my-opencode-config.d.ts +1 -0
- package/dist/features/background-agent/constants.d.ts +0 -7
- package/dist/features/background-agent/manager.d.ts +2 -1
- package/dist/features/background-agent/parent-wake-dispatched-tracker.d.ts +0 -1
- package/dist/features/background-agent/parent-wake-notifier-types.d.ts +1 -1
- package/dist/features/background-agent/parent-wake-prompt-dispatch.d.ts +1 -1
- package/dist/features/background-agent/parent-wake-session-inspector.d.ts +1 -1
- package/dist/features/background-agent/subagent-spawn-limits.d.ts +8 -0
- package/dist/features/monitor/manager-internals.d.ts +1 -1
- package/dist/features/monitor/output-injector-types.d.ts +1 -1
- package/dist/features/task-toast-manager/manager.d.ts +0 -24
- package/dist/features/team-mode/tools/messaging-live-delivery-recipient.d.ts +1 -0
- package/dist/features/team-mode/tools/messaging-runtime.d.ts +1 -0
- package/dist/hooks/anthropic-context-window-limit-recovery/message-storage-directory.d.ts +0 -3
- package/dist/hooks/anthropic-context-window-limit-recovery/state.d.ts +1 -0
- package/dist/hooks/anthropic-context-window-limit-recovery/storage/empty-messages.d.ts +0 -1
- package/dist/hooks/anthropic-context-window-limit-recovery/tool-result-storage.d.ts +11 -0
- package/dist/hooks/claude-code-hooks/session-hook-state.d.ts +1 -0
- package/dist/hooks/claude-code-hooks/stop.d.ts +2 -0
- package/dist/hooks/claude-code-hooks/todo.d.ts +0 -1
- package/dist/hooks/claude-code-hooks/transcript.d.ts +2 -0
- package/dist/hooks/fsync-skip-warning/index.d.ts +4 -0
- package/dist/hooks/hashline-edit-diff-enhancer/hook.d.ts +1 -0
- package/dist/hooks/hashline-edit-diff-enhancer/pending-captures.d.ts +13 -0
- package/dist/hooks/keyword-detector/hook.d.ts +7 -0
- package/dist/hooks/keyword-detector/ultrawork/source-detector.d.ts +0 -1
- package/dist/hooks/model-fallback/hook.d.ts +1 -1
- package/dist/index.js +681 -273
- package/dist/oh-my-opencode.schema.json +5 -0
- package/dist/plugin-dispose.d.ts +3 -0
- package/dist/shared/agent-tool-restrictions.d.ts +0 -1
- package/dist/skills/ultimate-browsing/references/insane-search/README.md +9 -2
- package/dist/tools/delegate-task/sync-session-cleanup.d.ts +3 -0
- package/dist/tools/delegate-task/sync-session-poller.d.ts +1 -0
- package/dist/tools/delegate-task/types.d.ts +1 -0
- package/dist/tools/session-manager/directory-filter.d.ts +2 -0
- package/dist/tui.js +148 -9
- package/package.json +16 -15
- package/packages/git-bash-mcp/dist/cli.js +1 -0
- package/packages/lsp-core/src/lsp/client-diagnostics-freshness.integration.test.ts +47 -19
- package/packages/lsp-core/src/lsp/client-diagnostics-pull-timeout.test.ts +64 -0
- package/packages/lsp-core/src/lsp/client-wrapper.test.ts +97 -0
- package/packages/lsp-core/src/lsp/client-wrapper.ts +21 -6
- package/packages/lsp-core/src/lsp/client.ts +6 -3
- package/packages/lsp-core/src/lsp/constants.ts +5 -0
- package/packages/lsp-core/src/lsp/controlled-clock-test-support.ts +61 -0
- package/packages/lsp-core/src/lsp/errors.ts +15 -0
- package/packages/lsp-core/src/lsp/fixtures/workspace-edit-server.mjs +10 -3
- package/packages/lsp-core/src/lsp/manager-lifecycle.test.ts +262 -0
- package/packages/lsp-core/src/lsp/manager.ts +106 -13
- package/packages/lsp-core/src/lsp/timer-provider.ts +13 -0
- package/packages/lsp-core/src/lsp/transport-request-timeout.test.ts +73 -0
- package/packages/lsp-core/src/lsp/transport.ts +7 -2
- package/packages/lsp-core/src/lsp/workspace-document-state.test.ts +84 -1
- package/packages/lsp-core/src/lsp/workspace-document-state.ts +30 -11
- package/packages/lsp-core/src/lsp/workspace-markers.ts +18 -1
- package/packages/lsp-core/src/mcp.ts +3 -0
- package/packages/lsp-core/src/tools/parameters.test.ts +51 -0
- package/packages/lsp-daemon/dist/cli.js +155 -34
- package/packages/lsp-daemon/dist/client.js +155 -34
- package/packages/lsp-daemon/dist/index.js +155 -34
- package/packages/lsp-daemon/dist/proxy.js +3 -0
- package/packages/lsp-tools-mcp/dist/cli.js +155 -34
- package/packages/lsp-tools-mcp/dist/lsp/manager.js +134 -28
- package/packages/lsp-tools-mcp/dist/mcp.js +155 -34
- package/packages/lsp-tools-mcp/dist/tools.js +154 -34
- package/packages/lsp-tools-mcp/package.json +1 -1
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
- 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/codegraph/dist/cli.js +17 -2
- package/packages/omo-codex/plugin/components/codegraph/dist/serve.js +17 -2
- package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
- package/packages/omo-codex/plugin/components/codegraph/src/mcp-unavailable.ts +12 -0
- package/packages/omo-codex/plugin/components/codegraph/test/serve-unavailable-idle.test.ts +47 -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/dist/.omo-runtime-manifest.json +3 -3
- package/packages/omo-codex/plugin/components/lsp/dist/cli.js +152 -34
- package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
- package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/rules/package.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/AGENTS.md +5 -4
- package/packages/omo-codex/plugin/components/ultrawork/dist/cli.js +489 -4
- package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -2
- package/packages/omo-codex/plugin/components/ultrawork/scripts/sync-directive.mjs +30 -14
- package/packages/omo-codex/plugin/components/ultrawork/src/directive-content.ts +4 -0
- package/packages/omo-codex/plugin/components/ultrawork/src/directive.ts +5 -2
- package/packages/omo-codex/plugin/components/ultrawork/test/directive-source.test.ts +14 -8
- package/packages/omo-codex/plugin/components/ultrawork/test/package-smoke.test.ts +12 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/AGENTS.md +5 -2
- package/packages/omo-codex/plugin/components/ulw-loop/README.md +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-continuation.js +9 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-reconciliation.d.ts +14 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-reconciliation.js +10 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-template.d.ts +8 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-template.js +113 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.js +9 -5
- 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 +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-subcommands.js +2 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +856 -347
- package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-instruction.d.ts +2 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-instruction.js +27 -6
- package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-snapshot.d.ts +13 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-snapshot.js +14 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/domain-types.d.ts +17 -9
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-io.js +17 -2
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-missing-recovery.d.ts +13 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-missing-recovery.js +19 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-aggregate.d.ts +2 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-aggregate.js +53 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-artifacts.d.ts +9 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-artifacts.js +85 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-fields.js +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate.d.ts +4 -2
- package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate.js +41 -102
- package/packages/omo-codex/plugin/components/ulw-loop/dist/review-blockers.js +4 -2
- package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-guard.js +185 -14
- package/packages/omo-codex/plugin/components/ulw-loop/dist/status-next-actions.d.ts +6 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/status-next-actions.js +33 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/surface.d.ts +18 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/surface.js +65 -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 +17 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-continuation.ts +7 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-reconciliation.ts +25 -1
- package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-template.ts +127 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +10 -12
- package/packages/omo-codex/plugin/components/ulw-loop/src/cli-commands.ts +2 -1
- package/packages/omo-codex/plugin/components/ulw-loop/src/cli-output.ts +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/cli-subcommands.ts +2 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-instruction.ts +34 -5
- package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-snapshot.ts +23 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/domain-types.ts +19 -9
- package/packages/omo-codex/plugin/components/ulw-loop/src/plan-io.ts +18 -3
- package/packages/omo-codex/plugin/components/ulw-loop/src/plan-missing-recovery.ts +27 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-aggregate.ts +57 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-artifacts.ts +103 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-fields.ts +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate.ts +54 -123
- package/packages/omo-codex/plugin/components/ulw-loop/src/review-blockers.ts +4 -2
- package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-guard.ts +187 -12
- package/packages/omo-codex/plugin/components/ulw-loop/src/status-next-actions.ts +36 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/surface.ts +95 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint-final.test.ts +107 -1
- package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint-template.test.ts +92 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/cli-status-next-actions.test.ts +183 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/codex-goal-instruction.test.ts +35 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/quality-gate-builder.ts +7 -3
- package/packages/omo-codex/plugin/components/ulw-loop/test/guided-recovery.test.ts +105 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-aggregate.test.ts +101 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-roles.test.ts +46 -2
- package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-senpi-surface.test.ts +131 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate.test.ts +5 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/spawn-guard.test.ts +318 -1
- package/packages/omo-codex/plugin/components/ulw-loop/test/surface.test.ts +74 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/ultrawork-directive.test.ts +9 -3
- 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 +1 -1
- 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 +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-checking-ulw-execute-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 +13 -13
- package/packages/omo-codex/plugin/package.json +1 -1
- package/packages/omo-codex/plugin/scripts/sync-skills.mjs +31 -2
- package/packages/omo-codex/plugin/skills/ultimate-browsing/references/insane-search/README.md +9 -2
- package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +17 -0
- package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +11 -1
- package/packages/omo-codex/plugin/test/sync-skills.test.mjs +13 -0
- package/packages/omo-codex/plugin/test/ultrawork-skill-pointer.test.mjs +5 -2
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +2 -2
- package/packages/shared-skills/skills/ultimate-browsing/references/insane-search/README.md +9 -2
- package/dist/features/builtin-skills/index.d.ts +0 -1
- package/dist/features/builtin-skills/skill-file-loader.d.ts +0 -1
- package/dist/features/builtin-skills/skills/agent-browser-skill.d.ts +0 -1
- package/dist/features/builtin-skills/skills/agent-browser-template.d.ts +0 -1
- package/dist/features/builtin-skills/skills/debugging.d.ts +0 -1
- package/dist/features/builtin-skills/skills/dev-browser.d.ts +0 -1
- package/dist/features/builtin-skills/skills/frontend.d.ts +0 -1
- package/dist/features/builtin-skills/skills/git-master-sections/commit-atomic-planning.d.ts +0 -1
- package/dist/features/builtin-skills/skills/git-master-sections/commit-context-analysis.d.ts +0 -1
- package/dist/features/builtin-skills/skills/git-master-sections/commit-execution-verification.d.ts +0 -1
- package/dist/features/builtin-skills/skills/git-master-sections/commit-workflow.d.ts +0 -1
- package/dist/features/builtin-skills/skills/git-master-sections/history-search-workflow.d.ts +0 -1
- package/dist/features/builtin-skills/skills/git-master-sections/overview.d.ts +0 -1
- package/dist/features/builtin-skills/skills/git-master-sections/quick-reference.d.ts +0 -1
- package/dist/features/builtin-skills/skills/git-master-sections/rebase-workflow.d.ts +0 -1
- package/dist/features/builtin-skills/skills/git-master-skill-metadata.d.ts +0 -1
- package/dist/features/builtin-skills/skills/git-master.d.ts +0 -1
- package/dist/features/builtin-skills/skills/index.d.ts +0 -1
- package/dist/features/builtin-skills/skills/init-deep.d.ts +0 -1
- package/dist/features/builtin-skills/skills/playwright-cli.d.ts +0 -1
- package/dist/features/builtin-skills/skills/playwright-mcp-skill.d.ts +0 -1
- package/dist/features/builtin-skills/skills/playwright.d.ts +0 -1
- package/dist/features/builtin-skills/skills/remove-ai-slops.d.ts +0 -1
- package/dist/features/builtin-skills/skills/review-work.d.ts +0 -1
- package/dist/features/builtin-skills/skills/security-research.d.ts +0 -1
- package/dist/features/builtin-skills/skills/security-review.d.ts +0 -1
- package/dist/features/builtin-skills/skills/team-mode.d.ts +0 -1
- package/dist/features/builtin-skills/skills/visual-qa.d.ts +0 -1
- package/dist/features/builtin-skills/skills.d.ts +0 -1
- package/dist/features/builtin-skills/types.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/agent-loader.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/command-loader.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/discovery-core.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/discovery-paths.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/discovery.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/hook-loader.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/index.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/install-path-resolver.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/installed-plugin-database.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/loaded-plugin.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/loader.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/mcp-server-loader.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/plugin-key.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/plugin-manifest.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/plugin-path-resolver.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/plugin-settings.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/scope-filter.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/skill-loader.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/types.d.ts +0 -1
- package/dist/hooks/ralph-loop/completion-promise-detector-test-input.d.ts +0 -11
- package/dist/hooks/ralph-loop/loop-session-recovery.d.ts +0 -7
- package/dist/shared/prompt-async-gate/message-inspection-error.d.ts +0 -1
- package/dist/shared/prompt-async-gate/pending-tool-turn.d.ts +0 -1
- package/dist/shared/prompt-async-gate/prompt-message-state.d.ts +0 -1
- package/dist/shared/prompt-async-gate/queue.d.ts +0 -1
- package/dist/shared/prompt-async-gate/recent-dispatches.d.ts +0 -1
- package/dist/shared/prompt-async-gate/reservations.d.ts +0 -1
- package/dist/shared/prompt-async-gate/semantic-dedupe.d.ts +0 -1
- package/dist/shared/prompt-async-gate/session-idle-dispatch.d.ts +0 -1
- package/dist/shared/prompt-async-gate/timing.d.ts +0 -1
- package/dist/shared/prompt-async-gate/types.d.ts +0 -1
- package/dist/tools/call-omo-agent/message-storage-directory.d.ts +0 -1
- package/dist/tools/delegate-task/model-string-parser.d.ts +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/cleanup-errors.d.ts +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/constants.d.ts +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/language-mappings.d.ts +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/process-signal-cleanup.d.ts +0 -1
- package/packages/lsp-tools-mcp/dist/missing-dependency-result.d.ts +0 -1
- package/packages/omo-codex/plugin/components/ultrawork/directive.md +0 -483
- package/packages/omo-codex/plugin/components/ultrawork/skills/ultrawork/SKILL.md +0 -490
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { aggregateQualityGateDefects } from "./quality-gate-aggregate.js";
|
|
2
|
+
import {
|
|
3
|
+
artifactCompatible,
|
|
4
|
+
artifactMap,
|
|
5
|
+
checkFile,
|
|
6
|
+
parseArtifactRefs,
|
|
7
|
+
referencedArtifacts,
|
|
8
|
+
surfaceField,
|
|
9
|
+
} from "./quality-gate-artifacts.js";
|
|
3
10
|
import {
|
|
4
11
|
emptyBlockers,
|
|
5
12
|
invalid,
|
|
@@ -10,18 +17,8 @@ import {
|
|
|
10
17
|
textField,
|
|
11
18
|
} from "./quality-gate-fields.js";
|
|
12
19
|
import { adversarialVerdict, codeQualityStatusField, passedVerdict } from "./quality-gate-verdicts.js";
|
|
13
|
-
import type
|
|
14
|
-
|
|
15
|
-
UlwLoopManualQaArtifactRef,
|
|
16
|
-
UlwLoopManualQaSurface,
|
|
17
|
-
UlwLoopQualityGate,
|
|
18
|
-
} from "./types.js";
|
|
19
|
-
|
|
20
|
-
const REVIEWER_ROLES = {
|
|
21
|
-
codeReview: "lazycodex-code-reviewer",
|
|
22
|
-
manualQa: "lazycodex-qa-executor",
|
|
23
|
-
gateReview: "lazycodex-gate-reviewer",
|
|
24
|
-
} as const;
|
|
20
|
+
import { GATE_SECTION_BY_ACCEPTOR, REQUIRED_GATE_SECTIONS_BY_SURFACE, type UlwLoopToolkitSurface } from "./surface.js";
|
|
21
|
+
import type { UlwLoopManualQaArtifactRef, UlwLoopQualityGate } from "./types.js";
|
|
25
22
|
|
|
26
23
|
export {
|
|
27
24
|
classifyExternalAuthorizationBlocker,
|
|
@@ -36,119 +33,47 @@ export interface QualityGateFs {
|
|
|
36
33
|
}
|
|
37
34
|
|
|
38
35
|
export interface ValidateQualityGateOptions {
|
|
39
|
-
readonly repoRoot
|
|
40
|
-
readonly fs
|
|
36
|
+
readonly repoRoot?: string;
|
|
37
|
+
readonly fs?: QualityGateFs;
|
|
41
38
|
readonly currentAttemptDir?: string;
|
|
39
|
+
readonly reviewerSurface?: UlwLoopToolkitSurface;
|
|
42
40
|
}
|
|
43
41
|
|
|
44
|
-
function reviewerRoleField
|
|
42
|
+
function reviewerRoleField(value: unknown, expected: string, field: string): string {
|
|
45
43
|
const actual = textField(value, field);
|
|
46
44
|
if (actual !== expected) invalid(`${field} must be ${expected}.`, field);
|
|
47
45
|
return expected;
|
|
48
46
|
}
|
|
49
47
|
|
|
50
|
-
function
|
|
51
|
-
if (
|
|
52
|
-
value === "cli" ||
|
|
53
|
-
value === "http" ||
|
|
54
|
-
value === "tmux" ||
|
|
55
|
-
value === "browser" ||
|
|
56
|
-
value === "gui" ||
|
|
57
|
-
value === "data"
|
|
58
|
-
)
|
|
59
|
-
return value;
|
|
60
|
-
invalid(`${field} must be a supported manual QA surface.`, field);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function kindField(value: unknown, field: string): UlwLoopManualQaArtifactKind {
|
|
64
|
-
if (
|
|
65
|
-
value === "cli-transcript" ||
|
|
66
|
-
value === "log" ||
|
|
67
|
-
value === "screenshot" ||
|
|
68
|
-
value === "image" ||
|
|
69
|
-
value === "http-dump" ||
|
|
70
|
-
value === "data-diff"
|
|
71
|
-
)
|
|
72
|
-
return value;
|
|
73
|
-
invalid(`${field} must be a supported artifact kind.`, field);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function artifactCompatible(surface: UlwLoopManualQaSurface, kind: UlwLoopManualQaArtifactKind): boolean {
|
|
77
|
-
switch (surface) {
|
|
78
|
-
case "cli":
|
|
79
|
-
case "tmux":
|
|
80
|
-
return kind === "cli-transcript" || kind === "log";
|
|
81
|
-
case "http":
|
|
82
|
-
return kind === "http-dump";
|
|
83
|
-
case "browser":
|
|
84
|
-
case "gui":
|
|
85
|
-
return kind === "screenshot" || kind === "image";
|
|
86
|
-
case "data":
|
|
87
|
-
return kind === "data-diff";
|
|
88
|
-
default:
|
|
89
|
-
invalid("manualQa.surfaceEvidence has an unsupported surface.", "manualQa.surfaceEvidence.surface");
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function checkFile(path: string, field: string, opts?: ValidateQualityGateOptions): void {
|
|
94
|
-
if (opts === undefined) return;
|
|
95
|
-
const absolute = resolve(opts.repoRoot, path);
|
|
96
|
-
if (!opts.fs.existsSync(absolute)) invalid(`${field} must point to an existing artifact.`, field);
|
|
97
|
-
if (opts.fs.statSync(absolute).size <= 0) invalid(`${field} must point to a non-empty artifact.`, field);
|
|
98
|
-
if (opts.currentAttemptDir !== undefined) {
|
|
99
|
-
const attemptRoot = resolve(opts.repoRoot, opts.currentAttemptDir);
|
|
100
|
-
if (!isWithinAttemptDir(absolute, attemptRoot))
|
|
101
|
-
invalid(
|
|
102
|
-
`${field} (${path}) must point to an artifact from the current attempt (${opts.currentAttemptDir}).`,
|
|
103
|
-
field,
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function artifactMap(refs: readonly UlwLoopManualQaArtifactRef[]): Map<string, UlwLoopManualQaArtifactRef> {
|
|
109
|
-
const byId = new Map<string, UlwLoopManualQaArtifactRef>();
|
|
110
|
-
for (const ref of refs) {
|
|
111
|
-
if (byId.has(ref.id)) invalid(`manualQa.artifactRefs contains duplicate ${ref.id}.`, "manualQa.artifactRefs");
|
|
112
|
-
byId.set(ref.id, ref);
|
|
113
|
-
}
|
|
114
|
-
return byId;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function parseArtifactRefs(value: unknown, opts?: ValidateQualityGateOptions): readonly UlwLoopManualQaArtifactRef[] {
|
|
118
|
-
if (!Array.isArray(value) || value.length === 0)
|
|
119
|
-
invalid("manualQa.artifactRefs must not be empty.", "manualQa.artifactRefs");
|
|
120
|
-
return value.map((item, index) => {
|
|
121
|
-
const ref = section(item, `manualQa.artifactRefs[${index}]`);
|
|
122
|
-
const path = textField(ref["path"], `manualQa.artifactRefs[${index}].path`);
|
|
123
|
-
checkFile(path, `manualQa.artifactRefs[${index}].path`, opts);
|
|
124
|
-
return {
|
|
125
|
-
id: textField(ref["id"], `manualQa.artifactRefs[${index}].id`),
|
|
126
|
-
kind: kindField(ref["kind"], `manualQa.artifactRefs[${index}].kind`),
|
|
127
|
-
description: textField(ref["description"], `manualQa.artifactRefs[${index}].description`),
|
|
128
|
-
path,
|
|
129
|
-
};
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function referencedArtifacts(
|
|
48
|
+
function reviewerAcceptorField(
|
|
134
49
|
value: unknown,
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
):
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
50
|
+
surface: UlwLoopToolkitSurface,
|
|
51
|
+
sectionName: "manualQa" | "gateReview",
|
|
52
|
+
): string {
|
|
53
|
+
const field = `${sectionName}.by`;
|
|
54
|
+
const actual = textField(value, field);
|
|
55
|
+
const accepted = GATE_SECTION_BY_ACCEPTOR[surface][sectionName];
|
|
56
|
+
if (accepted === undefined || !accepted.includes(actual))
|
|
57
|
+
invalid(`${field} must be one of ${accepted?.join(", ") ?? "the configured reviewers"}.`, field);
|
|
58
|
+
return actual;
|
|
143
59
|
}
|
|
144
60
|
|
|
145
61
|
export function validateQualityGate(input: unknown, opts?: ValidateQualityGateOptions): UlwLoopQualityGate {
|
|
62
|
+
const surface = opts?.reviewerSurface ?? "lazycodex";
|
|
63
|
+
aggregateQualityGateDefects(input, opts);
|
|
146
64
|
const gate = section(input, "qualityGate");
|
|
147
|
-
const
|
|
65
|
+
const requiredSections = REQUIRED_GATE_SECTIONS_BY_SURFACE[surface];
|
|
66
|
+
for (const sectionName of requiredSections) section(gate[sectionName], sectionName);
|
|
67
|
+
if (surface === "omo-senpi" && gate["codeReview"] !== undefined)
|
|
68
|
+
invalid("omo-senpi gate has no codeReview lane.", "codeReview");
|
|
148
69
|
const manualQa = section(gate["manualQa"], "manualQa");
|
|
149
70
|
const gateReview = section(gate["gateReview"], "gateReview");
|
|
150
71
|
const iteration = section(gate["iteration"], "iteration");
|
|
151
72
|
const coverage = section(gate["criteriaCoverage"], "criteriaCoverage");
|
|
73
|
+
const manualQaBy = reviewerAcceptorField(manualQa["by"], surface, "manualQa");
|
|
74
|
+
const gateReviewBy = reviewerAcceptorField(gateReview["by"], surface, "gateReview");
|
|
75
|
+
if (surface === "lazycodex")
|
|
76
|
+
reviewerRoleField(section(gate["codeReview"], "codeReview")["by"], "lazycodex-code-reviewer", "codeReview.by");
|
|
152
77
|
const totalCriteria = numberField(coverage["totalCriteria"], "criteriaCoverage.totalCriteria");
|
|
153
78
|
const passCount = numberField(coverage["passCount"], "criteriaCoverage.passCount");
|
|
154
79
|
if (passCount < totalCriteria)
|
|
@@ -157,21 +82,11 @@ export function validateQualityGate(input: unknown, opts?: ValidateQualityGateOp
|
|
|
157
82
|
const byId = artifactMap(artifactRefs);
|
|
158
83
|
const surfaceEvidence = parseSurfaceEvidence(manualQa["surfaceEvidence"], byId);
|
|
159
84
|
const adversarialCases = parseAdversarialCases(manualQa["adversarialCases"], byId);
|
|
160
|
-
const codeReportPath = textField(codeReview["reportPath"], "codeReview.reportPath");
|
|
161
85
|
const gateReportPath = textField(gateReview["reportPath"], "gateReview.reportPath");
|
|
162
|
-
checkFile(codeReportPath, "codeReview.reportPath", opts);
|
|
163
86
|
checkFile(gateReportPath, "gateReview.reportPath", opts);
|
|
164
|
-
|
|
165
|
-
codeReview: {
|
|
166
|
-
by: reviewerRoleField(codeReview["by"], REVIEWER_ROLES.codeReview, "codeReview.by"),
|
|
167
|
-
recommendation: literal(codeReview["recommendation"], "APPROVE", "codeReview.recommendation"),
|
|
168
|
-
codeQualityStatus: codeQualityStatusField(codeReview["codeQualityStatus"], "codeReview.codeQualityStatus"),
|
|
169
|
-
reportPath: codeReportPath,
|
|
170
|
-
evidence: textField(codeReview["evidence"], "codeReview.evidence"),
|
|
171
|
-
blockers: emptyBlockers(codeReview["blockers"], "codeReview.blockers"),
|
|
172
|
-
},
|
|
87
|
+
const common = {
|
|
173
88
|
manualQa: {
|
|
174
|
-
by:
|
|
89
|
+
by: manualQaBy,
|
|
175
90
|
status: literal(manualQa["status"], "passed", "manualQa.status"),
|
|
176
91
|
evidence: textField(manualQa["evidence"], "manualQa.evidence"),
|
|
177
92
|
surfaceEvidence,
|
|
@@ -179,7 +94,7 @@ export function validateQualityGate(input: unknown, opts?: ValidateQualityGateOp
|
|
|
179
94
|
artifactRefs,
|
|
180
95
|
},
|
|
181
96
|
gateReview: {
|
|
182
|
-
by:
|
|
97
|
+
by: gateReviewBy,
|
|
183
98
|
recommendation: literal(gateReview["recommendation"], "APPROVE", "gateReview.recommendation"),
|
|
184
99
|
reportPath: gateReportPath,
|
|
185
100
|
evidence: textField(gateReview["evidence"], "gateReview.evidence"),
|
|
@@ -203,6 +118,22 @@ export function validateQualityGate(input: unknown, opts?: ValidateQualityGateOp
|
|
|
203
118
|
),
|
|
204
119
|
},
|
|
205
120
|
};
|
|
121
|
+
if (surface === "omo-senpi") return { surface, ...common };
|
|
122
|
+
const codeReview = section(gate["codeReview"], "codeReview");
|
|
123
|
+
const codeReportPath = textField(codeReview["reportPath"], "codeReview.reportPath");
|
|
124
|
+
checkFile(codeReportPath, "codeReview.reportPath", opts);
|
|
125
|
+
return {
|
|
126
|
+
surface,
|
|
127
|
+
...common,
|
|
128
|
+
codeReview: {
|
|
129
|
+
by: "lazycodex-code-reviewer",
|
|
130
|
+
recommendation: literal(codeReview["recommendation"], "APPROVE", "codeReview.recommendation"),
|
|
131
|
+
codeQualityStatus: codeQualityStatusField(codeReview["codeQualityStatus"], "codeReview.codeQualityStatus"),
|
|
132
|
+
reportPath: codeReportPath,
|
|
133
|
+
evidence: textField(codeReview["evidence"], "codeReview.evidence"),
|
|
134
|
+
blockers: emptyBlockers(codeReview["blockers"], "codeReview.blockers"),
|
|
135
|
+
},
|
|
136
|
+
};
|
|
206
137
|
}
|
|
207
138
|
|
|
208
139
|
function parseSurfaceEvidence(
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// biome-ignore-all format: compact port must stay within the requested pure LOC budget.
|
|
2
2
|
|
|
3
|
+
import { codexSnapshotMismatchError } from "./checkpoint-reconciliation.js";
|
|
3
4
|
import { readCodexGoalSnapshotInput, reconcileCodexGoalSnapshot } from "./codex-goal-snapshot.js";
|
|
4
5
|
import { codexGoalMode, compatibleCodexObjectives, expectedCodexObjective, isFinalRunCompletionCandidate } from "./goal-status.js";
|
|
5
6
|
import type { UlwLoopScope } from "./paths.js";
|
|
@@ -55,8 +56,9 @@ export async function recordFinalReviewBlockers(
|
|
|
55
56
|
|
|
56
57
|
const snapshot = await readCodexGoalSnapshotInput(args.codexGoalJson, repoRoot);
|
|
57
58
|
const aggregate = codexGoalMode(plan) === "aggregate";
|
|
58
|
-
const
|
|
59
|
-
|
|
59
|
+
const expected = expectedCodexObjective(plan, goal);
|
|
60
|
+
const reconciliation = reconcileCodexGoalSnapshot(snapshot, { expectedObjective: expected, ...(aggregate ? { acceptedObjectives: compatibleCodexObjectives(plan) } : {}), allowedStatuses: ["active"], requireSnapshot: true, requireComplete: false });
|
|
61
|
+
if (!reconciliation.ok) throw codexSnapshotMismatchError({ reconciliation, snapshot, expectedObjective: expected });
|
|
60
62
|
|
|
61
63
|
const now = iso();
|
|
62
64
|
for (const field of BLOCKER_FIELDS) Reflect.deleteProperty(goal, field);
|
|
@@ -1,10 +1,27 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
openSync,
|
|
6
|
+
readdirSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
renameSync,
|
|
9
|
+
statSync,
|
|
10
|
+
unlinkSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from "node:fs";
|
|
13
|
+
import { dirname, join } from "node:path";
|
|
3
14
|
|
|
4
15
|
import type { PreToolUsePayload } from "./codex-hook.js";
|
|
5
16
|
import { parsePreToolUsePayload } from "./codex-hook.js";
|
|
6
17
|
import { isFinalRunCompletionCandidate } from "./goal-status.js";
|
|
7
18
|
import { ulwLoopAttemptEvidenceDir, ulwLoopDir } from "./paths.js";
|
|
19
|
+
import {
|
|
20
|
+
GATE_REVIEWER_AGENT_NAMES,
|
|
21
|
+
REVIEWER_ROLES_BY_SURFACE,
|
|
22
|
+
resolveToolkitSurface,
|
|
23
|
+
reviewerRolesFor,
|
|
24
|
+
} from "./surface.js";
|
|
8
25
|
import type { UlwLoopPlan } from "./types.js";
|
|
9
26
|
|
|
10
27
|
// spawn_agent = v1; collaborationspawn_agent = the delimiter-free flattened v2
|
|
@@ -12,18 +29,29 @@ import type { UlwLoopPlan } from "./types.js";
|
|
|
12
29
|
// dotted token observed live in the task-1 probe (hook-tool-tokens.txt).
|
|
13
30
|
const SPAWN_TOOL_TOKENS = new Set(["spawn_agent", "collaborationspawn_agent", "collaboration.spawn_agent"]);
|
|
14
31
|
const DEFAULT_FANOUT_LIMIT = 60;
|
|
15
|
-
const
|
|
32
|
+
const DEFAULT_REVIEW_SPAWN_LIMIT = 3;
|
|
33
|
+
const GATE_MESSAGE_PATTERN = /lazycodex-gate-reviewer|omo-senpi-gate-reviewer|final gate review/i;
|
|
34
|
+
const REVIEW_AGENT_TYPES = [
|
|
35
|
+
...Object.values(REVIEWER_ROLES_BY_SURFACE).map((roles) => roles.gateReview),
|
|
36
|
+
...Object.values(REVIEWER_ROLES_BY_SURFACE).map((roles) => roles.codeReview),
|
|
37
|
+
...Object.values(REVIEWER_ROLES_BY_SURFACE).map((roles) => roles.manualQa),
|
|
38
|
+
] as const;
|
|
39
|
+
const REVIEW_AGENT_TYPE_SET = new Set<string>(REVIEW_AGENT_TYPES);
|
|
16
40
|
|
|
17
41
|
export function applySpawnGuards(payload: PreToolUsePayload): string {
|
|
18
42
|
if (payload.hook_event_name !== "PreToolUse" || !SPAWN_TOOL_TOKENS.has(payload.tool_name)) return "";
|
|
19
43
|
const stateDir = ulwLoopDir(payload.cwd, { sessionId: payload.session_id });
|
|
20
44
|
const plan = readPlan(join(stateDir, "goals.json"));
|
|
21
45
|
if (plan === null) return "";
|
|
22
|
-
const
|
|
23
|
-
if (
|
|
46
|
+
const fanOutPeek = peekFanOutBudget(stateDir);
|
|
47
|
+
if (fanOutPeek !== null) return deny(fanOutPeek);
|
|
24
48
|
const missingArtifact = missingGateArtifact(payload, plan);
|
|
25
49
|
if (missingArtifact !== null)
|
|
26
50
|
return deny(`spawn code-review + QA first; gate audits their artifacts: missing ${missingArtifact}`);
|
|
51
|
+
const reviewDenial = consumeReviewSpawnBudget(payload, plan, stateDir);
|
|
52
|
+
if (reviewDenial !== null) return deny(reviewDenial);
|
|
53
|
+
const fanOutDenial = consumeFanOutBudget(stateDir);
|
|
54
|
+
if (fanOutDenial !== null) return deny(fanOutDenial);
|
|
27
55
|
return "";
|
|
28
56
|
}
|
|
29
57
|
|
|
@@ -40,46 +68,171 @@ export async function runSpawnGuardCli(stdin: NodeJS.ReadableStream, stdout: Nod
|
|
|
40
68
|
}
|
|
41
69
|
}
|
|
42
70
|
|
|
71
|
+
// Read-only fan-out eligibility check. Returns a denial reason when the next
|
|
72
|
+
// spawn would exceed the limit, without incrementing the counter. Call this
|
|
73
|
+
// before charging any per-reviewer quota so a saturated global cap cannot
|
|
74
|
+
// silently consume reviewer allowances for spawns that will never run.
|
|
75
|
+
function peekFanOutBudget(stateDir: string): string | null {
|
|
76
|
+
const counterPath = join(stateDir, "spawn-count.json");
|
|
77
|
+
const count = readCount(counterPath) + 1;
|
|
78
|
+
const limit = fanOutLimit();
|
|
79
|
+
if (count <= limit) return null;
|
|
80
|
+
return `ulw-loop spawn fan-out cap reached (${count}/${limit}). Consolidate work into the agents already running, or raise OMO_SPAWN_FANOUT_LIMIT if this volume is intentional.`;
|
|
81
|
+
}
|
|
82
|
+
|
|
43
83
|
// Per-session spawn counter; depth/lineage tracking is descoped — this is a
|
|
44
84
|
// total-volume backstop against fan-out explosions, not a recursion tracker.
|
|
45
85
|
function consumeFanOutBudget(stateDir: string): string | null {
|
|
46
86
|
const counterPath = join(stateDir, "spawn-count.json");
|
|
47
87
|
const count = readCount(counterPath) + 1;
|
|
48
|
-
|
|
88
|
+
atomicWriteJson(counterPath, { count });
|
|
49
89
|
const limit = fanOutLimit();
|
|
50
90
|
if (count <= limit) return null;
|
|
51
91
|
return `ulw-loop spawn fan-out cap reached (${count}/${limit}). Consolidate work into the agents already running, or raise OMO_SPAWN_FANOUT_LIMIT if this volume is intentional.`;
|
|
52
92
|
}
|
|
53
93
|
|
|
94
|
+
function consumeReviewSpawnBudget(payload: PreToolUsePayload, plan: UlwLoopPlan, stateDir: string): string | null {
|
|
95
|
+
const agentType = reviewAgentType(payload.tool_input);
|
|
96
|
+
if (agentType === null) return null;
|
|
97
|
+
const goal =
|
|
98
|
+
plan.goals.find((candidate) => candidate.id === plan.activeGoalId) ??
|
|
99
|
+
plan.goals.find((candidate) => isFinalRunCompletionCandidate(plan, candidate));
|
|
100
|
+
if (goal === undefined) return null;
|
|
101
|
+
const counterPath = join(stateDir, "review-spawn-counts.json");
|
|
102
|
+
const lockPath = `${counterPath}.lock`;
|
|
103
|
+
const limit = reviewSpawnLimit();
|
|
104
|
+
return withExclusiveLock(lockPath, () => {
|
|
105
|
+
const counts = readCounts(counterPath);
|
|
106
|
+
const key = `${agentType}:${goal.id}:a${goal.attempt}`;
|
|
107
|
+
const count = (counts[key] ?? 0) + 1;
|
|
108
|
+
if (count > limit)
|
|
109
|
+
return `ulw-loop reviewer no-progress cap reached (${agentType} ${count}/${limit}) for ${goal.id} attempt ${goal.attempt}. Consolidate existing review findings, or checkpoint and start a new attempt after concrete progress.`;
|
|
110
|
+
counts[key] = count;
|
|
111
|
+
atomicWriteJson(counterPath, counts);
|
|
112
|
+
return null;
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
54
116
|
function missingGateArtifact(payload: PreToolUsePayload, plan: UlwLoopPlan): string | null {
|
|
55
117
|
if (!isGateReviewerSpawn(payload.tool_input)) return null;
|
|
56
118
|
const goal = plan.goals.find((candidate) => isFinalRunCompletionCandidate(plan, candidate));
|
|
57
119
|
if (goal === undefined || goal.status === "complete") return null;
|
|
58
120
|
if (!goal.successCriteria.every((criterion) => criterion.status === "pass")) return null;
|
|
59
121
|
const scope = { sessionId: payload.session_id } as const;
|
|
122
|
+
const surface = resolveToolkitSurface();
|
|
123
|
+
const requiredArtifacts =
|
|
124
|
+
surface === "omo-senpi" ? [`${goal.id}-manual-qa.md`] : [`${goal.id}-code-review.md`, `${goal.id}-manual-qa.md`];
|
|
60
125
|
if (plan.evidenceLayoutVersion === 2) {
|
|
61
126
|
const attemptDir = ulwLoopAttemptEvidenceDir(goal.id, goal.attempt, scope);
|
|
62
|
-
for (const name of
|
|
127
|
+
for (const name of requiredArtifacts) {
|
|
63
128
|
const relative = `${attemptDir}/${name}`;
|
|
64
129
|
if (!isNonEmptyFile(join(payload.cwd, relative))) return relative;
|
|
65
130
|
}
|
|
66
131
|
return null;
|
|
67
132
|
}
|
|
68
133
|
const flatReport = `.omo/evidence/${goal.id}-code-review.md`;
|
|
69
|
-
if (!isNonEmptyFile(join(payload.cwd, flatReport))) return flatReport;
|
|
134
|
+
if (surface !== "omo-senpi" && !isNonEmptyFile(join(payload.cwd, flatReport))) return flatReport;
|
|
135
|
+
if (surface === "omo-senpi") {
|
|
136
|
+
const manualQa = `.omo/evidence/${goal.id}-manual-qa.md`;
|
|
137
|
+
return isNonEmptyFile(join(payload.cwd, manualQa)) ? null : manualQa;
|
|
138
|
+
}
|
|
70
139
|
// v1 manual-QA approximation: any other non-empty evidence file counts.
|
|
71
140
|
if (!hasOtherEvidenceFile(join(payload.cwd, ".omo", "evidence"), `${goal.id}-code-review.md`))
|
|
72
|
-
return `.omo/evidence
|
|
141
|
+
return `.omo/evidence/${goal.id}-manual-qa.md`;
|
|
73
142
|
return null;
|
|
74
143
|
}
|
|
75
144
|
|
|
76
145
|
function isGateReviewerSpawn(toolInput: unknown): boolean {
|
|
77
|
-
|
|
146
|
+
const agentType = reviewAgentType(toolInput);
|
|
147
|
+
return agentType !== null && GATE_REVIEWER_AGENT_NAMES.has(agentType);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function reviewAgentType(toolInput: unknown): string | null {
|
|
151
|
+
if (typeof toolInput !== "object" || toolInput === null) return null;
|
|
78
152
|
const record = toolInput as Record<string, unknown>;
|
|
79
153
|
const agentType = record["agent_type"];
|
|
80
|
-
if (typeof agentType === "string")
|
|
154
|
+
if (typeof agentType === "string") {
|
|
155
|
+
// V1: agent_type is present — only reviewer types proceed; any other type is not a review spawn.
|
|
156
|
+
if (!REVIEW_AGENT_TYPE_SET.has(agentType)) return null;
|
|
157
|
+
return activeSurfaceReviewerAlias(agentType);
|
|
158
|
+
}
|
|
81
159
|
const message = record["message"];
|
|
82
|
-
|
|
160
|
+
if (typeof message !== "string") return null;
|
|
161
|
+
const normalizedMessage = message.toLowerCase();
|
|
162
|
+
// Explicit "act as <role>" assignment takes priority. If the assigned role is not a reviewer,
|
|
163
|
+
// treat the spawn as non-review so a message that merely mentions a reviewer name does not
|
|
164
|
+
// accidentally charge that reviewer's quota.
|
|
165
|
+
const allRoleNames = [...REVIEW_AGENT_TYPES];
|
|
166
|
+
const explicitAssignment = allRoleNames
|
|
167
|
+
.map((name) => ({
|
|
168
|
+
name,
|
|
169
|
+
index: normalizedMessage.search(new RegExp(`\\bact as (?:an? )?${name}\\b`)),
|
|
170
|
+
}))
|
|
171
|
+
.filter(({ index }) => index >= 0)
|
|
172
|
+
.sort((left, right) => left.index - right.index)[0];
|
|
173
|
+
if (explicitAssignment !== undefined) return activeSurfaceReviewerAlias(explicitAssignment.name);
|
|
174
|
+
// Check for an explicit "act as <non-reviewer-role>" assignment. If found, the spawn is not a
|
|
175
|
+
// review spawn even if the message body mentions a reviewer name.
|
|
176
|
+
const nonReviewerActAs = /\bact as (?:an? )?\S+/.test(normalizedMessage);
|
|
177
|
+
if (nonReviewerActAs) return null;
|
|
178
|
+
const namedReviewer = REVIEW_AGENT_TYPES.find((name) => normalizedMessage.includes(name));
|
|
179
|
+
if (namedReviewer !== undefined) return activeSurfaceReviewerAlias(namedReviewer);
|
|
180
|
+
return GATE_MESSAGE_PATTERN.test(message) ? reviewerRolesFor(resolveToolkitSurface()).gateReview : null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function activeSurfaceReviewerAlias(reviewer: string): string {
|
|
184
|
+
const activeRoles = reviewerRolesFor(resolveToolkitSurface());
|
|
185
|
+
for (const roles of Object.values(REVIEWER_ROLES_BY_SURFACE)) {
|
|
186
|
+
if (reviewer === roles.codeReview) return activeRoles.codeReview;
|
|
187
|
+
if (reviewer === roles.manualQa) return activeRoles.manualQa;
|
|
188
|
+
if (reviewer === roles.gateReview) return activeRoles.gateReview;
|
|
189
|
+
}
|
|
190
|
+
return reviewer;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function withExclusiveLock<T>(lockPath: string, fn: () => T): T {
|
|
194
|
+
mkdirSync(dirname(lockPath), { recursive: true });
|
|
195
|
+
const maxAttempts = 10;
|
|
196
|
+
const baseDelayMs = 10;
|
|
197
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
198
|
+
let fd: number | null = null;
|
|
199
|
+
try {
|
|
200
|
+
fd = openSync(lockPath, "wx");
|
|
201
|
+
writeFileSync(fd, process.pid.toString());
|
|
202
|
+
try {
|
|
203
|
+
return fn();
|
|
204
|
+
} finally {
|
|
205
|
+
try {
|
|
206
|
+
unlinkSync(lockPath);
|
|
207
|
+
} catch {
|
|
208
|
+
/* empty */
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
} catch (error) {
|
|
212
|
+
if (fd !== null) {
|
|
213
|
+
try {
|
|
214
|
+
unlinkSync(lockPath);
|
|
215
|
+
} catch {
|
|
216
|
+
/* empty */
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") {
|
|
220
|
+
return fn();
|
|
221
|
+
}
|
|
222
|
+
const delayMs = baseDelayMs * 2 ** attempt + Math.random() * baseDelayMs;
|
|
223
|
+
const deadline = Date.now() + delayMs;
|
|
224
|
+
while (Date.now() < deadline) {
|
|
225
|
+
/* spin */
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return fn();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function atomicWriteJson(targetPath: string, data: unknown): void {
|
|
233
|
+
const tmp = join(dirname(targetPath), `.tmp-${randomBytes(6).toString("hex")}`);
|
|
234
|
+
writeFileSync(tmp, JSON.stringify(data));
|
|
235
|
+
renameSync(tmp, targetPath);
|
|
83
236
|
}
|
|
84
237
|
|
|
85
238
|
function deny(reason: string): string {
|
|
@@ -100,6 +253,13 @@ function fanOutLimit(): number {
|
|
|
100
253
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_FANOUT_LIMIT;
|
|
101
254
|
}
|
|
102
255
|
|
|
256
|
+
function reviewSpawnLimit(): number {
|
|
257
|
+
const raw = process.env["OMO_ULW_LOOP_REVIEW_SPAWN_LIMIT"];
|
|
258
|
+
if (raw === undefined) return DEFAULT_REVIEW_SPAWN_LIMIT;
|
|
259
|
+
const parsed = Number.parseInt(raw, 10);
|
|
260
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_REVIEW_SPAWN_LIMIT;
|
|
261
|
+
}
|
|
262
|
+
|
|
103
263
|
function isNonEmptyFile(path: string): boolean {
|
|
104
264
|
try {
|
|
105
265
|
return existsSync(path) && statSync(path).size > 0;
|
|
@@ -128,6 +288,21 @@ function readCount(counterPath: string): number {
|
|
|
128
288
|
}
|
|
129
289
|
}
|
|
130
290
|
|
|
291
|
+
function readCounts(counterPath: string): Record<string, number> {
|
|
292
|
+
try {
|
|
293
|
+
const parsed: unknown = JSON.parse(readFileSync(counterPath, "utf8"));
|
|
294
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
|
|
295
|
+
const counts: Record<string, number> = {};
|
|
296
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
297
|
+
if (typeof value === "number" && value >= 0) counts[key] = value;
|
|
298
|
+
}
|
|
299
|
+
return counts;
|
|
300
|
+
} catch (error) {
|
|
301
|
+
if (error instanceof Error) return {};
|
|
302
|
+
throw error;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
131
306
|
function readPlan(goalsPath: string): UlwLoopPlan | null {
|
|
132
307
|
try {
|
|
133
308
|
return JSON.parse(readFileSync(goalsPath, "utf8")) as UlwLoopPlan;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { hasAllCriteriaPass, isFinalRunCompletionCandidate } from "./goal-status.js";
|
|
2
|
+
import { ULW_LOOP_CREATE_GOALS_COMMAND } from "./plan-missing-recovery.js";
|
|
3
|
+
import type { UlwLoopItem, UlwLoopPlan } from "./types.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `status --json` is the one call every agent already makes between steps, so it is
|
|
7
|
+
* also the cheapest place to answer "what now?" without a second round trip.
|
|
8
|
+
*/
|
|
9
|
+
export function statusNextActions(plan: UlwLoopPlan): readonly string[] {
|
|
10
|
+
const actions: string[] = [];
|
|
11
|
+
const active = plan.goals.find((goal) => goal.id === plan.activeGoalId);
|
|
12
|
+
if (plan.goals.length === 0) actions.push(`No goals yet: bootstrap with \`${ULW_LOOP_CREATE_GOALS_COMMAND}\`.`);
|
|
13
|
+
else if (active !== undefined) actions.push(...activeGoalActions(plan, active));
|
|
14
|
+
for (const goal of plan.goals.filter((candidate) => candidate.status === "review_blocked"))
|
|
15
|
+
actions.push(
|
|
16
|
+
`${goal.id} is review_blocked: capture the reviewer verdict with \`omo-agent-toolkit ulw-loop record-review-blockers --goal-id ${goal.id} --title "<title>" --objective "<objective>" --evidence "<verdict>" --codex-goal-json '<get_goal json>'\`.`,
|
|
17
|
+
);
|
|
18
|
+
if (plan.evidenceLayoutVersion !== 2 || active === undefined)
|
|
19
|
+
actions.push("plan is evidence-layout v1; artifacts go under .omo/evidence/");
|
|
20
|
+
return actions;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function activeGoalActions(plan: UlwLoopPlan, active: UlwLoopItem): readonly string[] {
|
|
24
|
+
const unresolved = active.successCriteria.filter((criterion) => criterion.status !== "pass");
|
|
25
|
+
if (unresolved.length > 0)
|
|
26
|
+
return [
|
|
27
|
+
`${active.id} has ${unresolved.length} unresolved criterion(s) (${unresolved.map((criterion) => criterion.id).join(", ")}): record proof with \`omo-agent-toolkit ulw-loop record-evidence --goal-id ${active.id} --criterion-id <id> --status pass --evidence "<observable proof>"\`.`,
|
|
28
|
+
];
|
|
29
|
+
if (hasAllCriteriaPass(active) && isFinalRunCompletionCandidate(plan, active))
|
|
30
|
+
return [
|
|
31
|
+
`${active.id} passes every criterion and is the final story: update_goal complete, then checkpoint --print-template to build the final quality gate.`,
|
|
32
|
+
];
|
|
33
|
+
return [
|
|
34
|
+
`${active.id} passes every criterion: close it with \`omo-agent-toolkit ulw-loop checkpoint --goal-id ${active.id} --status complete --evidence "<proof>" --codex-goal-json '<get_goal json>'\`.`,
|
|
35
|
+
];
|
|
36
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
// The agent toolkit ships on two surfaces: the LazyCodex (Codex CLI) plugin runs the component
|
|
6
|
+
// bundle directly, while omo-senpi stages the same bundle through stage-agent-toolkit.mjs. The
|
|
7
|
+
// reviewer identities the final quality gate enforces differ per surface, so the staging script
|
|
8
|
+
// bakes a `surface.json` marker next to the staged bundle and this module resolves it. Without a
|
|
9
|
+
// marker (the Codex layout) the surface stays "lazycodex".
|
|
10
|
+
export type UlwLoopToolkitSurface = "lazycodex" | "omo-senpi";
|
|
11
|
+
|
|
12
|
+
export interface UlwLoopReviewerRoles {
|
|
13
|
+
readonly codeReview: string;
|
|
14
|
+
readonly manualQa: string;
|
|
15
|
+
readonly gateReview: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const REVIEWER_ROLES_BY_SURFACE: Readonly<Record<UlwLoopToolkitSurface, UlwLoopReviewerRoles>> = {
|
|
19
|
+
lazycodex: {
|
|
20
|
+
codeReview: "lazycodex-code-reviewer",
|
|
21
|
+
manualQa: "lazycodex-qa-executor",
|
|
22
|
+
gateReview: "lazycodex-gate-reviewer",
|
|
23
|
+
},
|
|
24
|
+
"omo-senpi": {
|
|
25
|
+
codeReview: "omo-senpi-code-reviewer",
|
|
26
|
+
manualQa: "omo-senpi-qa-executor",
|
|
27
|
+
gateReview: "omo-senpi-gate-reviewer",
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const GATE_REVIEWER_AGENT_NAMES: ReadonlySet<string> = new Set(
|
|
32
|
+
Object.values(REVIEWER_ROLES_BY_SURFACE).map((roles) => roles.gateReview),
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
export type UlwLoopGateSection = "codeReview" | "manualQa" | "gateReview" | "iteration" | "criteriaCoverage";
|
|
36
|
+
|
|
37
|
+
export const REQUIRED_GATE_SECTIONS_BY_SURFACE: Readonly<Record<UlwLoopToolkitSurface, readonly UlwLoopGateSection[]>> =
|
|
38
|
+
{
|
|
39
|
+
lazycodex: ["codeReview", "manualQa", "gateReview", "iteration", "criteriaCoverage"],
|
|
40
|
+
"omo-senpi": ["manualQa", "gateReview", "iteration", "criteriaCoverage"],
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const GATE_SECTION_BY_ACCEPTOR: Readonly<
|
|
44
|
+
Record<UlwLoopToolkitSurface, Readonly<Partial<Record<UlwLoopGateSection, readonly string[]>>>>
|
|
45
|
+
> = {
|
|
46
|
+
lazycodex: {
|
|
47
|
+
codeReview: [REVIEWER_ROLES_BY_SURFACE.lazycodex.codeReview],
|
|
48
|
+
manualQa: [REVIEWER_ROLES_BY_SURFACE.lazycodex.manualQa],
|
|
49
|
+
gateReview: [REVIEWER_ROLES_BY_SURFACE.lazycodex.gateReview],
|
|
50
|
+
},
|
|
51
|
+
"omo-senpi": {
|
|
52
|
+
manualQa: ["main-session"],
|
|
53
|
+
gateReview: ["category:deep", "category:unspecified-high", "category:unspecified-low"],
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export function reviewerRolesFor(surface: UlwLoopToolkitSurface): UlwLoopReviewerRoles {
|
|
58
|
+
return REVIEWER_ROLES_BY_SURFACE[surface];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const SURFACE_MARKER_FILENAME = "surface.json";
|
|
62
|
+
const SURFACE_ENV_KEY = "OMO_AGENT_TOOLKIT_SURFACE";
|
|
63
|
+
|
|
64
|
+
function parseSurface(value: unknown): UlwLoopToolkitSurface | null {
|
|
65
|
+
return value === "lazycodex" || value === "omo-senpi" ? value : null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface ResolveToolkitSurfaceOptions {
|
|
69
|
+
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
70
|
+
readonly entryDir?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Resolution order: explicit env override (tests, doctor probes) -> staged surface.json marker
|
|
74
|
+
// sitting next to the running bundle -> lazycodex default. The marker is distribution-baked, so a
|
|
75
|
+
// malformed marker falls back to the default instead of crashing checkpoint validation; the env
|
|
76
|
+
// override remains available to force the intended surface on a damaged install.
|
|
77
|
+
export function resolveToolkitSurface(options?: ResolveToolkitSurfaceOptions): UlwLoopToolkitSurface {
|
|
78
|
+
const env = options?.env ?? process.env;
|
|
79
|
+
const fromEnv = parseSurface(env[SURFACE_ENV_KEY]);
|
|
80
|
+
if (fromEnv !== null) return fromEnv;
|
|
81
|
+
const entryDir = options?.entryDir ?? dirname(fileURLToPath(import.meta.url));
|
|
82
|
+
const markerPath = join(entryDir, SURFACE_MARKER_FILENAME);
|
|
83
|
+
return readMarkerSurface(markerPath) ?? "lazycodex";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function readMarkerSurface(markerPath: string): UlwLoopToolkitSurface | null {
|
|
87
|
+
try {
|
|
88
|
+
if (!existsSync(markerPath)) return null;
|
|
89
|
+
const parsed = JSON.parse(readFileSync(markerPath, "utf8")) as Record<string, unknown>;
|
|
90
|
+
return parseSurface(parsed["surface"]);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (error instanceof Error) return null;
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|