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,17 +1,124 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
//
|
|
4
|
-
import { existsSync as
|
|
3
|
+
// components/ulw-loop/src/checkpoint.ts
|
|
4
|
+
import { existsSync as existsSync4, statSync } from "node:fs";
|
|
5
5
|
import { readFile as readFile5 } from "node:fs/promises";
|
|
6
|
-
import { resolve as
|
|
6
|
+
import { resolve as resolve4 } from "node:path";
|
|
7
|
+
|
|
8
|
+
// components/ulw-loop/src/checkpoint-reconciliation.ts
|
|
9
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
10
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
7
11
|
|
|
8
|
-
//
|
|
12
|
+
// components/ulw-loop/src/codex-goal-snapshot.ts
|
|
9
13
|
import { existsSync } from "node:fs";
|
|
10
14
|
import { readFile } from "node:fs/promises";
|
|
15
|
+
import { resolve } from "node:path";
|
|
11
16
|
|
|
12
|
-
|
|
17
|
+
class CodexGoalSnapshotError extends Error {
|
|
18
|
+
}
|
|
19
|
+
function safeObject(value) {
|
|
20
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
21
|
+
}
|
|
22
|
+
function safeString(value) {
|
|
23
|
+
return typeof value === "string" ? value.trim() : "";
|
|
24
|
+
}
|
|
25
|
+
function normalizeStatus(value) {
|
|
26
|
+
const status = safeString(value).toLowerCase();
|
|
27
|
+
if (status === "complete" || status === "completed" || status === "done")
|
|
28
|
+
return "complete";
|
|
29
|
+
if (status === "cancelled" || status === "canceled")
|
|
30
|
+
return "cancelled";
|
|
31
|
+
if (status === "failed" || status === "failure")
|
|
32
|
+
return "failed";
|
|
33
|
+
if (status === "active" || status === "in_progress" || status === "pending" || status === "running")
|
|
34
|
+
return "active";
|
|
35
|
+
return "unknown";
|
|
36
|
+
}
|
|
37
|
+
function normalizeObjective(value) {
|
|
38
|
+
return value.replace(/\s+/g, " ").trim();
|
|
39
|
+
}
|
|
40
|
+
function parseCodexGoalSnapshot(value) {
|
|
41
|
+
const root = safeObject(value);
|
|
42
|
+
const goalValue = Object.hasOwn(root, "goal") ? root["goal"] : value;
|
|
43
|
+
if (goalValue === null || goalValue === undefined || goalValue === false) {
|
|
44
|
+
return { available: false, raw: value };
|
|
45
|
+
}
|
|
46
|
+
const goal = safeObject(goalValue);
|
|
47
|
+
const objective = safeString(goal["objective"] ?? goal["goal"] ?? goal["description"] ?? goal["title"] ?? root["objective"] ?? root["title"]);
|
|
48
|
+
const status = normalizeStatus(goal["status"] ?? root["status"]);
|
|
49
|
+
return {
|
|
50
|
+
available: Boolean(objective || status !== "unknown"),
|
|
51
|
+
...objective ? { objective } : {},
|
|
52
|
+
status,
|
|
53
|
+
raw: value
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
async function readCodexGoalSnapshotInput(raw, cwd = process.cwd()) {
|
|
57
|
+
if (!raw?.trim())
|
|
58
|
+
return null;
|
|
59
|
+
const trimmed = raw.trim();
|
|
60
|
+
try {
|
|
61
|
+
return parseCodexGoalSnapshot(JSON.parse(trimmed));
|
|
62
|
+
} catch {
|
|
63
|
+
const path = resolve(cwd, trimmed);
|
|
64
|
+
if (!existsSync(path)) {
|
|
65
|
+
throw new CodexGoalSnapshotError(`Codex goal snapshot is neither valid JSON nor a readable path: ${trimmed}`);
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
return parseCodexGoalSnapshot(JSON.parse(await readFile(path, "utf-8")));
|
|
69
|
+
} catch (error) {
|
|
70
|
+
throw new CodexGoalSnapshotError(`Codex goal snapshot path does not contain valid JSON: ${trimmed}${error instanceof Error ? ` (${error.message})` : ""}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function reconcileCodexGoalSnapshot(snapshot, options) {
|
|
75
|
+
const effectiveSnapshot = snapshot ?? { available: false, raw: null };
|
|
76
|
+
const errors = [];
|
|
77
|
+
const warnings = [];
|
|
78
|
+
if (!effectiveSnapshot.available) {
|
|
79
|
+
const message = "Codex goal snapshot is absent or reports no active goal; call get_goal and pass its JSON with --codex-goal-json.";
|
|
80
|
+
if (options.requireSnapshot)
|
|
81
|
+
errors.push(message);
|
|
82
|
+
else
|
|
83
|
+
warnings.push(message);
|
|
84
|
+
return { ok: errors.length === 0, snapshot: effectiveSnapshot, warnings, errors };
|
|
85
|
+
}
|
|
86
|
+
const expected = normalizeObjective(options.expectedObjective);
|
|
87
|
+
const accepted = new Set([expected, ...(options.acceptedObjectives ?? []).map((objective) => normalizeObjective(objective))].filter(Boolean));
|
|
88
|
+
const actual = normalizeObjective(effectiveSnapshot.objective ?? "");
|
|
89
|
+
if (!actual) {
|
|
90
|
+
errors.push("Codex goal snapshot is missing objective text.");
|
|
91
|
+
} else if (!accepted.has(actual)) {
|
|
92
|
+
errors.push(`Codex goal objective mismatch: expected "${expected}", got "${actual}".`);
|
|
93
|
+
}
|
|
94
|
+
const allowed = options.allowedStatuses ?? (options.requireComplete ? ["complete"] : ["active", "complete"]);
|
|
95
|
+
const actualStatus = effectiveSnapshot.status ?? "unknown";
|
|
96
|
+
if (!allowed.includes(actualStatus)) {
|
|
97
|
+
errors.push(`Codex goal status mismatch: expected ${allowed.join(" or ")}, got ${actualStatus}.`);
|
|
98
|
+
}
|
|
99
|
+
if (options.requireComplete && actualStatus !== "complete") {
|
|
100
|
+
errors.push('Codex goal is not complete; call update_goal({status: "complete"}) only after the objective is actually complete, then pass the fresh get_goal JSON.');
|
|
101
|
+
}
|
|
102
|
+
return { ok: errors.length === 0, snapshot: effectiveSnapshot, warnings, errors };
|
|
103
|
+
}
|
|
104
|
+
function formatCodexGoalReconciliation(reconciliation) {
|
|
105
|
+
const parts = [...reconciliation.errors, ...reconciliation.warnings];
|
|
106
|
+
return parts.join(" ");
|
|
107
|
+
}
|
|
108
|
+
function codexGoalMismatchRecovery(expectedObjective, snapshot) {
|
|
109
|
+
const receivedObjective = snapshot?.objective ?? "";
|
|
110
|
+
const message = [
|
|
111
|
+
"Recovery: the Codex goal objective must equal the plan's codexObjective exactly — copy the expected value below into update_goal and re-run get_goal.",
|
|
112
|
+
`expected codexObjective: ${expectedObjective}`,
|
|
113
|
+
`received objective: ${receivedObjective || "(none)"}`
|
|
114
|
+
].join(`
|
|
115
|
+
`);
|
|
116
|
+
return { message, details: { expectedObjective, receivedObjective } };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// components/ulw-loop/src/paths.ts
|
|
13
120
|
import { isAbsolute, join, relative, sep } from "node:path";
|
|
14
|
-
//
|
|
121
|
+
// components/ulw-loop/src/constants.ts
|
|
15
122
|
var ULW_LOOP_DIR = ".omo/ulw-loop";
|
|
16
123
|
var ULW_LOOP_BRIEF = "brief.md";
|
|
17
124
|
var ULW_LOOP_GOALS = "goals.json";
|
|
@@ -31,7 +138,7 @@ var ULW_LOOP_SUCCESS_CRITERION_USER_MODELS = [
|
|
|
31
138
|
"regression",
|
|
32
139
|
"adversarial"
|
|
33
140
|
];
|
|
34
|
-
//
|
|
141
|
+
// components/ulw-loop/src/runtime.ts
|
|
35
142
|
class UlwLoopError extends Error {
|
|
36
143
|
constructor(message, code, opts) {
|
|
37
144
|
super(message, opts?.cause === undefined ? undefined : { cause: opts.cause });
|
|
@@ -45,7 +152,7 @@ class UlwLoopError extends Error {
|
|
|
45
152
|
function iso() {
|
|
46
153
|
return new Date().toISOString();
|
|
47
154
|
}
|
|
48
|
-
//
|
|
155
|
+
// components/ulw-loop/src/paths.ts
|
|
49
156
|
var SESSION_ENV_KEYS = ["OMO_ULW_LOOP_SESSION_ID", "CODEX_SESSION_ID", "CODEX_THREAD_ID", "PI_SESSION_ID"];
|
|
50
157
|
function normalizeUlwLoopSessionId(sessionId) {
|
|
51
158
|
const trimmed = sessionId?.trim();
|
|
@@ -111,7 +218,7 @@ function isWithinAttemptDir(absolutePath, attemptRoot, pathApi = PLATFORM_PATH_A
|
|
|
111
218
|
return !pathApi.isAbsolute(relativePath);
|
|
112
219
|
}
|
|
113
220
|
|
|
114
|
-
//
|
|
221
|
+
// components/ulw-loop/src/goal-status.ts
|
|
115
222
|
var ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE = aggregateCodexObjectiveForScope();
|
|
116
223
|
function aggregateCodexObjectiveForScope(scope) {
|
|
117
224
|
return `Complete the durable ulw-loop plan in ${ulwLoopGoalsRelativePath(scope)}, including later accepted/appended stories, under the original brief constraints; use ${ulwLoopLedgerRelativePath(scope)} as the audit trail.`;
|
|
@@ -194,8 +301,15 @@ function hasEssentialCriteriaPass(goal) {
|
|
|
194
301
|
return criteria.length > 0 && criteria.every((criterion) => criterion.status === "pass");
|
|
195
302
|
}
|
|
196
303
|
|
|
197
|
-
//
|
|
198
|
-
function
|
|
304
|
+
// components/ulw-loop/src/checkpoint-reconciliation.ts
|
|
305
|
+
function codexSnapshotMismatchError(input) {
|
|
306
|
+
const { reconciliation, snapshot, taskScopedHint } = input;
|
|
307
|
+
const hint = taskScopedHint?.aggregate === true && snapshot?.status === "complete" && snapshot.objective !== undefined ? buildTaskScopedAggregateReconciliationHint(taskScopedHint.goal, taskScopedHint.final) : "";
|
|
308
|
+
const recovery = codexGoalMismatchRecovery(input.expectedObjective, snapshot);
|
|
309
|
+
return new UlwLoopError(`${formatCodexGoalReconciliation(reconciliation)}${hint}
|
|
310
|
+
${recovery.message}`, "ulw_loop_codex_snapshot_mismatch", { details: recovery.details });
|
|
311
|
+
}
|
|
312
|
+
function normalizeObjective2(value) {
|
|
199
313
|
return value.replace(/\s+/g, " ").trim();
|
|
200
314
|
}
|
|
201
315
|
function textMentionsUlwLoopPlanArtifact(value) {
|
|
@@ -212,13 +326,13 @@ function textHasCompletionValidationEvidence(value) {
|
|
|
212
326
|
return done && verified;
|
|
213
327
|
}
|
|
214
328
|
async function snapshotObjectiveMapsToUlwLoopPlan(repoRoot, snapshotObjective, scope) {
|
|
215
|
-
const actual =
|
|
329
|
+
const actual = normalizeObjective2(snapshotObjective).toLowerCase();
|
|
216
330
|
if (textMentionsUlwLoopPlanArtifact(actual))
|
|
217
331
|
return true;
|
|
218
|
-
if (actual.length < 24 || !
|
|
332
|
+
if (actual.length < 24 || !existsSync2(ulwLoopBriefPath(repoRoot, scope)))
|
|
219
333
|
return false;
|
|
220
334
|
try {
|
|
221
|
-
const brief =
|
|
335
|
+
const brief = normalizeObjective2(await readFile2(ulwLoopBriefPath(repoRoot, scope), "utf8")).toLowerCase();
|
|
222
336
|
return brief.length >= 24 && (brief.includes(actual) || actual.includes(brief));
|
|
223
337
|
} catch (error) {
|
|
224
338
|
if (error instanceof Error)
|
|
@@ -265,107 +379,32 @@ function buildTaskScopedAggregateReconciliationHint(goal, final) {
|
|
|
265
379
|
return ` Completed task-scoped aggregate reconciliation requires the checkpoint goal to be the active in-progress OMO goal, evidence that names that active OMO goal id, names .omo/ulw-loop/goals.json or ledger.jsonl, includes completed implementation plus validation/review evidence, and a get_goal objective that maps to the ulw-loop brief/artifact. ${buildCompletedLegacyGoalRemediation(goal)}`;
|
|
266
380
|
}
|
|
267
381
|
|
|
268
|
-
//
|
|
269
|
-
import {
|
|
270
|
-
import { readFile as
|
|
271
|
-
import {
|
|
382
|
+
// components/ulw-loop/src/plan-io.ts
|
|
383
|
+
import { createReadStream, readdirSync } from "node:fs";
|
|
384
|
+
import { appendFile, mkdir, readFile as readFile3, rename, writeFile } from "node:fs/promises";
|
|
385
|
+
import { createInterface } from "node:readline";
|
|
272
386
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
function
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
return "unknown";
|
|
292
|
-
}
|
|
293
|
-
function normalizeObjective2(value) {
|
|
294
|
-
return value.replace(/\s+/g, " ").trim();
|
|
295
|
-
}
|
|
296
|
-
function parseCodexGoalSnapshot(value) {
|
|
297
|
-
const root = safeObject(value);
|
|
298
|
-
const goalValue = Object.hasOwn(root, "goal") ? root["goal"] : value;
|
|
299
|
-
if (goalValue === null || goalValue === undefined || goalValue === false) {
|
|
300
|
-
return { available: false, raw: value };
|
|
301
|
-
}
|
|
302
|
-
const goal = safeObject(goalValue);
|
|
303
|
-
const objective = safeString(goal["objective"] ?? goal["goal"] ?? goal["description"] ?? goal["title"] ?? root["objective"] ?? root["title"]);
|
|
304
|
-
const status = normalizeStatus(goal["status"] ?? root["status"]);
|
|
305
|
-
return {
|
|
306
|
-
available: Boolean(objective || status !== "unknown"),
|
|
307
|
-
...objective ? { objective } : {},
|
|
308
|
-
status,
|
|
309
|
-
raw: value
|
|
310
|
-
};
|
|
311
|
-
}
|
|
312
|
-
async function readCodexGoalSnapshotInput(raw, cwd = process.cwd()) {
|
|
313
|
-
if (!raw?.trim())
|
|
314
|
-
return null;
|
|
315
|
-
const trimmed = raw.trim();
|
|
316
|
-
try {
|
|
317
|
-
return parseCodexGoalSnapshot(JSON.parse(trimmed));
|
|
318
|
-
} catch {
|
|
319
|
-
const path = resolve(cwd, trimmed);
|
|
320
|
-
if (!existsSync2(path)) {
|
|
321
|
-
throw new CodexGoalSnapshotError(`Codex goal snapshot is neither valid JSON nor a readable path: ${trimmed}`);
|
|
322
|
-
}
|
|
323
|
-
try {
|
|
324
|
-
return parseCodexGoalSnapshot(JSON.parse(await readFile2(path, "utf-8")));
|
|
325
|
-
} catch (error) {
|
|
326
|
-
throw new CodexGoalSnapshotError(`Codex goal snapshot path does not contain valid JSON: ${trimmed}${error instanceof Error ? ` (${error.message})` : ""}`);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
function reconcileCodexGoalSnapshot(snapshot, options) {
|
|
331
|
-
const effectiveSnapshot = snapshot ?? { available: false, raw: null };
|
|
332
|
-
const errors = [];
|
|
333
|
-
const warnings = [];
|
|
334
|
-
if (!effectiveSnapshot.available) {
|
|
335
|
-
const message = "Codex goal snapshot is absent or reports no active goal; call get_goal and pass its JSON with --codex-goal-json.";
|
|
336
|
-
if (options.requireSnapshot)
|
|
337
|
-
errors.push(message);
|
|
338
|
-
else
|
|
339
|
-
warnings.push(message);
|
|
340
|
-
return { ok: errors.length === 0, snapshot: effectiveSnapshot, warnings, errors };
|
|
341
|
-
}
|
|
342
|
-
const expected = normalizeObjective2(options.expectedObjective);
|
|
343
|
-
const accepted = new Set([expected, ...(options.acceptedObjectives ?? []).map((objective) => normalizeObjective2(objective))].filter(Boolean));
|
|
344
|
-
const actual = normalizeObjective2(effectiveSnapshot.objective ?? "");
|
|
345
|
-
if (!actual) {
|
|
346
|
-
errors.push("Codex goal snapshot is missing objective text.");
|
|
347
|
-
} else if (!accepted.has(actual)) {
|
|
348
|
-
errors.push(`Codex goal objective mismatch: expected "${expected}", got "${actual}".`);
|
|
349
|
-
}
|
|
350
|
-
const allowed = options.allowedStatuses ?? (options.requireComplete ? ["complete"] : ["active", "complete"]);
|
|
351
|
-
const actualStatus = effectiveSnapshot.status ?? "unknown";
|
|
352
|
-
if (!allowed.includes(actualStatus)) {
|
|
353
|
-
errors.push(`Codex goal status mismatch: expected ${allowed.join(" or ")}, got ${actualStatus}.`);
|
|
354
|
-
}
|
|
355
|
-
if (options.requireComplete && actualStatus !== "complete") {
|
|
356
|
-
errors.push('Codex goal is not complete; call update_goal({status: "complete"}) only after the objective is actually complete, then pass the fresh get_goal JSON.');
|
|
357
|
-
}
|
|
358
|
-
return { ok: errors.length === 0, snapshot: effectiveSnapshot, warnings, errors };
|
|
359
|
-
}
|
|
360
|
-
function formatCodexGoalReconciliation(reconciliation) {
|
|
361
|
-
const parts = [...reconciliation.errors, ...reconciliation.warnings];
|
|
362
|
-
return parts.join(" ");
|
|
387
|
+
// components/ulw-loop/src/plan-missing-recovery.ts
|
|
388
|
+
var ULW_LOOP_CREATE_GOALS_COMMAND = 'omo-agent-toolkit ulw-loop create-goals --brief "<brief>" --json';
|
|
389
|
+
function planMissingRecovery(existingSessionIds) {
|
|
390
|
+
const lines = [`Recovery: bootstrap the plan with \`${ULW_LOOP_CREATE_GOALS_COMMAND}\`.`];
|
|
391
|
+
if (existingSessionIds.length === 0)
|
|
392
|
+
return { message: lines.join(`
|
|
393
|
+
`) };
|
|
394
|
+
lines.push(`Existing ulw-loop session ids under .omo/ulw-loop/: ${existingSessionIds.join(", ")}. Re-run with \`--session-id <id>\` to target one of them.`);
|
|
395
|
+
return { message: lines.join(`
|
|
396
|
+
`), details: { existingSessionIds } };
|
|
397
|
+
}
|
|
398
|
+
function sessionIdRequiredMessage(flag) {
|
|
399
|
+
return [
|
|
400
|
+
`${flag} requires a non-empty value.`,
|
|
401
|
+
"Recovery: subprocess, eval, and hook contexts do not inherit the ulw-loop session env (OMO_ULW_LOOP_SESSION_ID / CODEX_SESSION_ID / CODEX_THREAD_ID / PI_SESSION_ID),",
|
|
402
|
+
`so pass the scope explicitly: \`${flag} <id>\` (for example \`${flag} 01a05b8a-6763-7780-834f-319423b071ce\`).`
|
|
403
|
+
].join(`
|
|
404
|
+
`);
|
|
363
405
|
}
|
|
364
406
|
|
|
365
|
-
//
|
|
366
|
-
import { createReadStream } from "node:fs";
|
|
367
|
-
import { appendFile, mkdir, readFile as readFile3, rename, writeFile } from "node:fs/promises";
|
|
368
|
-
import { createInterface } from "node:readline";
|
|
407
|
+
// components/ulw-loop/src/plan-io.ts
|
|
369
408
|
var LEGACY_OBJECTIVE_PREFIX = `Complete all ulw-loop stories in ${ULW_LOOP_DIR}/${ULW_LOOP_GOALS}: `;
|
|
370
409
|
var LEGACY_OBJECTIVE = `Complete all ulw-loop stories listed in ${ULW_LOOP_DIR}/${ULW_LOOP_GOALS}. Use ${ULW_LOOP_DIR}/${ULW_LOOP_LEDGER} as the durable audit trail.`;
|
|
371
410
|
var locks = new Map;
|
|
@@ -406,7 +445,9 @@ async function readUlwLoopPlan(repoRoot, scope) {
|
|
|
406
445
|
} catch (error) {
|
|
407
446
|
if (!hasCode(error, "ENOENT"))
|
|
408
447
|
throw error;
|
|
409
|
-
|
|
448
|
+
const recovery = planMissingRecovery(readSessionDirs(repoRoot));
|
|
449
|
+
throw new UlwLoopError(`No ulw-loop plan found at ${repoRelative(path, repoRoot)}.
|
|
450
|
+
${recovery.message}`, "ULW_LOOP_PLAN_MISSING", { cause: error, ...recovery.details === undefined ? {} : { details: recovery.details } });
|
|
410
451
|
}
|
|
411
452
|
const parsed = JSON.parse(raw);
|
|
412
453
|
if (parsed.version !== 1 || !Array.isArray(parsed.goals)) {
|
|
@@ -429,6 +470,13 @@ async function readUlwLoopPlan(repoRoot, scope) {
|
|
|
429
470
|
}
|
|
430
471
|
return parsed;
|
|
431
472
|
}
|
|
473
|
+
function readSessionDirs(repoRoot) {
|
|
474
|
+
try {
|
|
475
|
+
return readdirSync(ulwLoopDir(repoRoot), { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
|
|
476
|
+
} catch {
|
|
477
|
+
return [];
|
|
478
|
+
}
|
|
479
|
+
}
|
|
432
480
|
async function writePlan(repoRoot, plan, scope) {
|
|
433
481
|
await mkdir(ulwLoopDir(repoRoot, scope), { recursive: true });
|
|
434
482
|
const path = ulwLoopGoalsPath(repoRoot, scope);
|
|
@@ -480,7 +528,7 @@ async function findAcceptedSteeringLedgerEntry(repoRoot, key, scope) {
|
|
|
480
528
|
return;
|
|
481
529
|
}
|
|
482
530
|
|
|
483
|
-
//
|
|
531
|
+
// components/ulw-loop/src/evidence.ts
|
|
484
532
|
function ulwLoopFail(message, code, details) {
|
|
485
533
|
throw new UlwLoopError(message, code, { details });
|
|
486
534
|
}
|
|
@@ -586,11 +634,11 @@ function requireEssentialCriteriaPass(goal) {
|
|
|
586
634
|
});
|
|
587
635
|
}
|
|
588
636
|
|
|
589
|
-
//
|
|
637
|
+
// components/ulw-loop/src/quality-gate-aggregate.ts
|
|
590
638
|
import { resolve as resolve2 } from "node:path";
|
|
591
639
|
|
|
592
|
-
//
|
|
593
|
-
var PLACEHOLDER_PATTERN = /^(
|
|
640
|
+
// components/ulw-loop/src/quality-gate-fields.ts
|
|
641
|
+
var PLACEHOLDER_PATTERN = /^(?:<replace:[^>]+>|placeholder|todo|tbd|n\/a|stub)$/i;
|
|
594
642
|
function invalid(message, field) {
|
|
595
643
|
throw new UlwLoopError(message, "ULW_LOOP_QUALITY_GATE_INVALID", { details: { field } });
|
|
596
644
|
}
|
|
@@ -627,81 +675,61 @@ function literal(value, expected, field) {
|
|
|
627
675
|
invalid(`${field} must be ${String(expected)}.`, field);
|
|
628
676
|
}
|
|
629
677
|
|
|
630
|
-
//
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
return literal(value, "passed", field);
|
|
678
|
+
// components/ulw-loop/src/quality-gate-aggregate.ts
|
|
679
|
+
var PLACEHOLDER = /^(?:<replace:[^>]+>|placeholder|todo|tbd|n\/a|stub)$/i;
|
|
680
|
+
function add(defects, field, message) {
|
|
681
|
+
defects.push({ field, message });
|
|
635
682
|
}
|
|
636
|
-
function
|
|
637
|
-
if (value
|
|
638
|
-
|
|
639
|
-
|
|
683
|
+
function text(value, field, defects) {
|
|
684
|
+
if (typeof value !== "string" || value.trim() === "")
|
|
685
|
+
add(defects, field, `Final quality gate requires non-empty ${field}.`);
|
|
686
|
+
else if (PLACEHOLDER.test(value.trim()))
|
|
687
|
+
add(defects, field, `Final quality gate rejects placeholder ${field}.`);
|
|
640
688
|
}
|
|
641
|
-
function
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
return {
|
|
645
|
-
if (value === "not_applicable") {
|
|
646
|
-
return { verdict: "not_applicable", reason: textField(row["reason"], `${field}.reason`) };
|
|
689
|
+
function section2(value, field, defects) {
|
|
690
|
+
if (!isRecord(value)) {
|
|
691
|
+
add(defects, field, `Final quality gate is missing ${field} evidence.`);
|
|
692
|
+
return {};
|
|
647
693
|
}
|
|
648
|
-
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
// packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-blockers.ts
|
|
652
|
-
var BLOCKER_FIELD_KEYS = "blocker blockerSignature blockerEvidence blockerOccurrences blockedAt".split(" ");
|
|
653
|
-
var URL_PATTERN = /https?:\/\/\S+/g;
|
|
654
|
-
var PUNCTUATION_PATTERN = /[`"'()[\]{}:,;]/g;
|
|
655
|
-
var WHITESPACE_PATTERN = /\s+/g;
|
|
656
|
-
var AUTH_PATTERN = /\b(auth\w*|credential\w*|token|permission\w*|scope\w*|access|unauthorized|forbidden|401|403)\b/;
|
|
657
|
-
var MISSING_PATTERN = /\b(unset|missing|required|requires|without|omit\w*|not set|not available|no read packages|read packages)\b/;
|
|
658
|
-
var GHCR_PATTERN = /\b(ghcr|github container registry|read packages|imagepullsecret|package api|anonymous|container image)\b/;
|
|
659
|
-
var GHCR_401_PATTERN = /\b(401|unauthorized|anonymous pull|authentication required)\b/;
|
|
660
|
-
var GHCR_403_PATTERN = /\b(403|forbidden|read packages|package api)\b/;
|
|
661
|
-
function isRecord2(value) {
|
|
662
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
663
|
-
}
|
|
664
|
-
function normalizeBlockerEvidence(evidence) {
|
|
665
|
-
const withoutUrls = evidence.toLowerCase().replace(URL_PATTERN, " ");
|
|
666
|
-
const withoutPunctuation = withoutUrls.replace(PUNCTUATION_PATTERN, " ");
|
|
667
|
-
return withoutPunctuation.replace(WHITESPACE_PATTERN, " ").trim();
|
|
668
|
-
}
|
|
669
|
-
function classifyExternalAuthorizationBlocker(evidence) {
|
|
670
|
-
const normalized = normalizeBlockerEvidence(evidence);
|
|
671
|
-
if (!normalized || !AUTH_PATTERN.test(normalized) || !MISSING_PATTERN.test(normalized))
|
|
672
|
-
return null;
|
|
673
|
-
if (!GHCR_PATTERN.test(normalized))
|
|
674
|
-
return "EXTERNAL_AUTHORIZATION_REQUIRED";
|
|
675
|
-
const status401 = GHCR_401_PATTERN.test(normalized) ? "HTTP_401_ANONYMOUS" : null;
|
|
676
|
-
const status403 = GHCR_403_PATTERN.test(normalized) ? "HTTP_403_NO_READ_PACKAGES" : null;
|
|
677
|
-
const status = [status401, status403].filter((part) => part !== null).join("+");
|
|
678
|
-
return `GHCR_PULL_ACCESS:${status || "AUTHORIZATION_REQUIRED"}:GHCR_VISIBILITY_OR_CREDENTIAL_REQUIRED`;
|
|
694
|
+
return value;
|
|
679
695
|
}
|
|
680
|
-
function
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
696
|
+
function aggregateQualityGateDefects(input, opts) {
|
|
697
|
+
if (!isRecord(input))
|
|
698
|
+
return;
|
|
699
|
+
const defects = [];
|
|
700
|
+
const gate = input;
|
|
701
|
+
const manual = section2(gate["manualQa"], "manualQa", defects);
|
|
702
|
+
const review = section2(gate["gateReview"], "gateReview", defects);
|
|
703
|
+
text(manual["evidence"], "manualQa.evidence", defects);
|
|
704
|
+
text(review["evidence"], "gateReview.evidence", defects);
|
|
705
|
+
if (review["recommendation"] !== "APPROVE")
|
|
706
|
+
add(defects, "gateReview.recommendation", "gateReview.recommendation must be APPROVE.");
|
|
707
|
+
const artifacts = Array.isArray(manual["artifactRefs"]) ? manual["artifactRefs"] : [];
|
|
708
|
+
for (const [index, item] of artifacts.entries()) {
|
|
709
|
+
if (!isRecord(item))
|
|
710
|
+
continue;
|
|
711
|
+
const path = item["path"];
|
|
712
|
+
if (typeof path !== "string" || path.trim() === "")
|
|
713
|
+
continue;
|
|
714
|
+
if (opts?.repoRoot !== undefined && opts.fs !== undefined && !opts.fs.existsSync(resolve2(opts.repoRoot, path)))
|
|
715
|
+
add(defects, `manualQa.artifactRefs[${index}].path`, `manualQa.artifactRefs[${index}].path must point to an existing artifact.`);
|
|
716
|
+
}
|
|
717
|
+
if (defects.length === 0)
|
|
718
|
+
return;
|
|
719
|
+
const truncated = defects.length > 25;
|
|
720
|
+
const fields = defects.slice(0, 25).map(({ field, message: message2 }) => ({ field, message: message2 }));
|
|
721
|
+
const message = [
|
|
722
|
+
`Final quality gate has ${fields.length}${truncated ? "+" : ""} validation defects:`,
|
|
723
|
+
...fields.map((item) => `- ${item.field}: ${item.message}`)
|
|
724
|
+
].join(`
|
|
725
|
+
`);
|
|
726
|
+
throw new UlwLoopError(message, "ULW_LOOP_QUALITY_GATE_INVALID", {
|
|
727
|
+
details: { field: fields[0]?.field, fields, ...truncated ? { truncated: true } : {} }
|
|
728
|
+
});
|
|
691
729
|
}
|
|
692
730
|
|
|
693
|
-
//
|
|
694
|
-
|
|
695
|
-
codeReview: "lazycodex-code-reviewer",
|
|
696
|
-
manualQa: "lazycodex-qa-executor",
|
|
697
|
-
gateReview: "lazycodex-gate-reviewer"
|
|
698
|
-
};
|
|
699
|
-
function reviewerRoleField(value, expected, field) {
|
|
700
|
-
const actual = textField(value, field);
|
|
701
|
-
if (actual !== expected)
|
|
702
|
-
invalid(`${field} must be ${expected}.`, field);
|
|
703
|
-
return expected;
|
|
704
|
-
}
|
|
731
|
+
// components/ulw-loop/src/quality-gate-artifacts.ts
|
|
732
|
+
import { resolve as resolve3 } from "node:path";
|
|
705
733
|
function surfaceField(value, field) {
|
|
706
734
|
if (value === "cli" || value === "http" || value === "tmux" || value === "browser" || value === "gui" || value === "data")
|
|
707
735
|
return value;
|
|
@@ -729,15 +757,15 @@ function artifactCompatible(surface, kind) {
|
|
|
729
757
|
}
|
|
730
758
|
}
|
|
731
759
|
function checkFile(path, field, opts) {
|
|
732
|
-
if (opts === undefined)
|
|
760
|
+
if (opts?.repoRoot === undefined || opts.fs === undefined)
|
|
733
761
|
return;
|
|
734
|
-
const absolute =
|
|
762
|
+
const absolute = resolve3(opts.repoRoot, path);
|
|
735
763
|
if (!opts.fs.existsSync(absolute))
|
|
736
764
|
invalid(`${field} must point to an existing artifact.`, field);
|
|
737
765
|
if (opts.fs.statSync(absolute).size <= 0)
|
|
738
766
|
invalid(`${field} must point to a non-empty artifact.`, field);
|
|
739
|
-
if (opts.currentAttemptDir !== undefined) {
|
|
740
|
-
const attemptRoot =
|
|
767
|
+
if (opts.currentAttemptDir !== undefined && opts.repoRoot !== undefined) {
|
|
768
|
+
const attemptRoot = resolve3(opts.repoRoot, opts.currentAttemptDir);
|
|
741
769
|
if (!isWithinAttemptDir(absolute, attemptRoot))
|
|
742
770
|
invalid(`${field} (${path}) must point to an artifact from the current attempt (${opts.currentAttemptDir}).`, field);
|
|
743
771
|
}
|
|
@@ -774,13 +802,164 @@ function referencedArtifacts(value, field, byId) {
|
|
|
774
802
|
return artifact;
|
|
775
803
|
});
|
|
776
804
|
}
|
|
805
|
+
|
|
806
|
+
// components/ulw-loop/src/quality-gate-verdicts.ts
|
|
807
|
+
function passedVerdict(value, field) {
|
|
808
|
+
if (value === "not_applicable")
|
|
809
|
+
invalid(`${field} must not be not_applicable.`, field);
|
|
810
|
+
return literal(value, "passed", field);
|
|
811
|
+
}
|
|
812
|
+
function codeQualityStatusField(value, field) {
|
|
813
|
+
if (value === "CLEAR" || value === "WATCH")
|
|
814
|
+
return value;
|
|
815
|
+
invalid(`${field} must be CLEAR or WATCH.`, field);
|
|
816
|
+
}
|
|
817
|
+
function adversarialVerdict(row, field) {
|
|
818
|
+
const value = row["verdict"];
|
|
819
|
+
if (value === "passed")
|
|
820
|
+
return { verdict: "passed" };
|
|
821
|
+
if (value === "not_applicable") {
|
|
822
|
+
return { verdict: "not_applicable", reason: textField(row["reason"], `${field}.reason`) };
|
|
823
|
+
}
|
|
824
|
+
invalid(`${field} must be passed or not_applicable with a reason.`, field);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// components/ulw-loop/src/surface.ts
|
|
828
|
+
import { existsSync as existsSync3, readFileSync } from "node:fs";
|
|
829
|
+
import { dirname, join as join2 } from "node:path";
|
|
830
|
+
import { fileURLToPath } from "node:url";
|
|
831
|
+
var REVIEWER_ROLES_BY_SURFACE = {
|
|
832
|
+
lazycodex: {
|
|
833
|
+
codeReview: "lazycodex-code-reviewer",
|
|
834
|
+
manualQa: "lazycodex-qa-executor",
|
|
835
|
+
gateReview: "lazycodex-gate-reviewer"
|
|
836
|
+
},
|
|
837
|
+
"omo-senpi": {
|
|
838
|
+
codeReview: "omo-senpi-code-reviewer",
|
|
839
|
+
manualQa: "omo-senpi-qa-executor",
|
|
840
|
+
gateReview: "omo-senpi-gate-reviewer"
|
|
841
|
+
}
|
|
842
|
+
};
|
|
843
|
+
var GATE_REVIEWER_AGENT_NAMES = new Set(Object.values(REVIEWER_ROLES_BY_SURFACE).map((roles) => roles.gateReview));
|
|
844
|
+
var REQUIRED_GATE_SECTIONS_BY_SURFACE = {
|
|
845
|
+
lazycodex: ["codeReview", "manualQa", "gateReview", "iteration", "criteriaCoverage"],
|
|
846
|
+
"omo-senpi": ["manualQa", "gateReview", "iteration", "criteriaCoverage"]
|
|
847
|
+
};
|
|
848
|
+
var GATE_SECTION_BY_ACCEPTOR = {
|
|
849
|
+
lazycodex: {
|
|
850
|
+
codeReview: [REVIEWER_ROLES_BY_SURFACE.lazycodex.codeReview],
|
|
851
|
+
manualQa: [REVIEWER_ROLES_BY_SURFACE.lazycodex.manualQa],
|
|
852
|
+
gateReview: [REVIEWER_ROLES_BY_SURFACE.lazycodex.gateReview]
|
|
853
|
+
},
|
|
854
|
+
"omo-senpi": {
|
|
855
|
+
manualQa: ["main-session"],
|
|
856
|
+
gateReview: ["category:deep", "category:unspecified-high", "category:unspecified-low"]
|
|
857
|
+
}
|
|
858
|
+
};
|
|
859
|
+
function reviewerRolesFor(surface) {
|
|
860
|
+
return REVIEWER_ROLES_BY_SURFACE[surface];
|
|
861
|
+
}
|
|
862
|
+
var SURFACE_MARKER_FILENAME = "surface.json";
|
|
863
|
+
var SURFACE_ENV_KEY = "OMO_AGENT_TOOLKIT_SURFACE";
|
|
864
|
+
function parseSurface(value) {
|
|
865
|
+
return value === "lazycodex" || value === "omo-senpi" ? value : null;
|
|
866
|
+
}
|
|
867
|
+
function resolveToolkitSurface(options) {
|
|
868
|
+
const env = options?.env ?? process.env;
|
|
869
|
+
const fromEnv = parseSurface(env[SURFACE_ENV_KEY]);
|
|
870
|
+
if (fromEnv !== null)
|
|
871
|
+
return fromEnv;
|
|
872
|
+
const entryDir = options?.entryDir ?? dirname(fileURLToPath(import.meta.url));
|
|
873
|
+
const markerPath = join2(entryDir, SURFACE_MARKER_FILENAME);
|
|
874
|
+
return readMarkerSurface(markerPath) ?? "lazycodex";
|
|
875
|
+
}
|
|
876
|
+
function readMarkerSurface(markerPath) {
|
|
877
|
+
try {
|
|
878
|
+
if (!existsSync3(markerPath))
|
|
879
|
+
return null;
|
|
880
|
+
const parsed = JSON.parse(readFileSync(markerPath, "utf8"));
|
|
881
|
+
return parseSurface(parsed["surface"]);
|
|
882
|
+
} catch (error) {
|
|
883
|
+
if (error instanceof Error)
|
|
884
|
+
return null;
|
|
885
|
+
throw error;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// components/ulw-loop/src/quality-gate-blockers.ts
|
|
890
|
+
var BLOCKER_FIELD_KEYS = "blocker blockerSignature blockerEvidence blockerOccurrences blockedAt".split(" ");
|
|
891
|
+
var URL_PATTERN = /https?:\/\/\S+/g;
|
|
892
|
+
var PUNCTUATION_PATTERN = /[`"'()[\]{}:,;]/g;
|
|
893
|
+
var WHITESPACE_PATTERN = /\s+/g;
|
|
894
|
+
var AUTH_PATTERN = /\b(auth\w*|credential\w*|token|permission\w*|scope\w*|access|unauthorized|forbidden|401|403)\b/;
|
|
895
|
+
var MISSING_PATTERN = /\b(unset|missing|required|requires|without|omit\w*|not set|not available|no read packages|read packages)\b/;
|
|
896
|
+
var GHCR_PATTERN = /\b(ghcr|github container registry|read packages|imagepullsecret|package api|anonymous|container image)\b/;
|
|
897
|
+
var GHCR_401_PATTERN = /\b(401|unauthorized|anonymous pull|authentication required)\b/;
|
|
898
|
+
var GHCR_403_PATTERN = /\b(403|forbidden|read packages|package api)\b/;
|
|
899
|
+
function isRecord2(value) {
|
|
900
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
901
|
+
}
|
|
902
|
+
function normalizeBlockerEvidence(evidence) {
|
|
903
|
+
const withoutUrls = evidence.toLowerCase().replace(URL_PATTERN, " ");
|
|
904
|
+
const withoutPunctuation = withoutUrls.replace(PUNCTUATION_PATTERN, " ");
|
|
905
|
+
return withoutPunctuation.replace(WHITESPACE_PATTERN, " ").trim();
|
|
906
|
+
}
|
|
907
|
+
function classifyExternalAuthorizationBlocker(evidence) {
|
|
908
|
+
const normalized = normalizeBlockerEvidence(evidence);
|
|
909
|
+
if (!normalized || !AUTH_PATTERN.test(normalized) || !MISSING_PATTERN.test(normalized))
|
|
910
|
+
return null;
|
|
911
|
+
if (!GHCR_PATTERN.test(normalized))
|
|
912
|
+
return "EXTERNAL_AUTHORIZATION_REQUIRED";
|
|
913
|
+
const status401 = GHCR_401_PATTERN.test(normalized) ? "HTTP_401_ANONYMOUS" : null;
|
|
914
|
+
const status403 = GHCR_403_PATTERN.test(normalized) ? "HTTP_403_NO_READ_PACKAGES" : null;
|
|
915
|
+
const status = [status401, status403].filter((part) => part !== null).join("+");
|
|
916
|
+
return `GHCR_PULL_ACCESS:${status || "AUTHORIZATION_REQUIRED"}:GHCR_VISIBILITY_OR_CREDENTIAL_REQUIRED`;
|
|
917
|
+
}
|
|
918
|
+
function nestedBlockerSignature(goal) {
|
|
919
|
+
const blocker = Reflect.get(goal, "blocker");
|
|
920
|
+
const signature = isRecord2(blocker) ? blocker["signature"] : null;
|
|
921
|
+
return typeof signature === "string" ? signature : null;
|
|
922
|
+
}
|
|
923
|
+
function sameBlockerOccurrences(plan, signature) {
|
|
924
|
+
return plan.goals.filter((goal) => goal.blockerSignature === signature || nestedBlockerSignature(goal) === signature).length;
|
|
925
|
+
}
|
|
926
|
+
function clearGoalBlockerFields(goal) {
|
|
927
|
+
for (const key of BLOCKER_FIELD_KEYS)
|
|
928
|
+
Reflect.deleteProperty(goal, key);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// components/ulw-loop/src/quality-gate.ts
|
|
932
|
+
function reviewerRoleField(value, expected, field) {
|
|
933
|
+
const actual = textField(value, field);
|
|
934
|
+
if (actual !== expected)
|
|
935
|
+
invalid(`${field} must be ${expected}.`, field);
|
|
936
|
+
return expected;
|
|
937
|
+
}
|
|
938
|
+
function reviewerAcceptorField(value, surface, sectionName) {
|
|
939
|
+
const field = `${sectionName}.by`;
|
|
940
|
+
const actual = textField(value, field);
|
|
941
|
+
const accepted = GATE_SECTION_BY_ACCEPTOR[surface][sectionName];
|
|
942
|
+
if (accepted === undefined || !accepted.includes(actual))
|
|
943
|
+
invalid(`${field} must be one of ${accepted?.join(", ") ?? "the configured reviewers"}.`, field);
|
|
944
|
+
return actual;
|
|
945
|
+
}
|
|
777
946
|
function validateQualityGate(input, opts) {
|
|
947
|
+
const surface = opts?.reviewerSurface ?? "lazycodex";
|
|
948
|
+
aggregateQualityGateDefects(input, opts);
|
|
778
949
|
const gate = section(input, "qualityGate");
|
|
779
|
-
const
|
|
950
|
+
const requiredSections = REQUIRED_GATE_SECTIONS_BY_SURFACE[surface];
|
|
951
|
+
for (const sectionName of requiredSections)
|
|
952
|
+
section(gate[sectionName], sectionName);
|
|
953
|
+
if (surface === "omo-senpi" && gate["codeReview"] !== undefined)
|
|
954
|
+
invalid("omo-senpi gate has no codeReview lane.", "codeReview");
|
|
780
955
|
const manualQa = section(gate["manualQa"], "manualQa");
|
|
781
956
|
const gateReview = section(gate["gateReview"], "gateReview");
|
|
782
957
|
const iteration = section(gate["iteration"], "iteration");
|
|
783
958
|
const coverage = section(gate["criteriaCoverage"], "criteriaCoverage");
|
|
959
|
+
const manualQaBy = reviewerAcceptorField(manualQa["by"], surface, "manualQa");
|
|
960
|
+
const gateReviewBy = reviewerAcceptorField(gateReview["by"], surface, "gateReview");
|
|
961
|
+
if (surface === "lazycodex")
|
|
962
|
+
reviewerRoleField(section(gate["codeReview"], "codeReview")["by"], "lazycodex-code-reviewer", "codeReview.by");
|
|
784
963
|
const totalCriteria = numberField(coverage["totalCriteria"], "criteriaCoverage.totalCriteria");
|
|
785
964
|
const passCount = numberField(coverage["passCount"], "criteriaCoverage.passCount");
|
|
786
965
|
if (passCount < totalCriteria)
|
|
@@ -789,21 +968,11 @@ function validateQualityGate(input, opts) {
|
|
|
789
968
|
const byId = artifactMap(artifactRefs);
|
|
790
969
|
const surfaceEvidence = parseSurfaceEvidence(manualQa["surfaceEvidence"], byId);
|
|
791
970
|
const adversarialCases = parseAdversarialCases(manualQa["adversarialCases"], byId);
|
|
792
|
-
const codeReportPath = textField(codeReview["reportPath"], "codeReview.reportPath");
|
|
793
971
|
const gateReportPath = textField(gateReview["reportPath"], "gateReview.reportPath");
|
|
794
|
-
checkFile(codeReportPath, "codeReview.reportPath", opts);
|
|
795
972
|
checkFile(gateReportPath, "gateReview.reportPath", opts);
|
|
796
|
-
|
|
797
|
-
codeReview: {
|
|
798
|
-
by: reviewerRoleField(codeReview["by"], REVIEWER_ROLES.codeReview, "codeReview.by"),
|
|
799
|
-
recommendation: literal(codeReview["recommendation"], "APPROVE", "codeReview.recommendation"),
|
|
800
|
-
codeQualityStatus: codeQualityStatusField(codeReview["codeQualityStatus"], "codeReview.codeQualityStatus"),
|
|
801
|
-
reportPath: codeReportPath,
|
|
802
|
-
evidence: textField(codeReview["evidence"], "codeReview.evidence"),
|
|
803
|
-
blockers: emptyBlockers(codeReview["blockers"], "codeReview.blockers")
|
|
804
|
-
},
|
|
973
|
+
const common = {
|
|
805
974
|
manualQa: {
|
|
806
|
-
by:
|
|
975
|
+
by: manualQaBy,
|
|
807
976
|
status: literal(manualQa["status"], "passed", "manualQa.status"),
|
|
808
977
|
evidence: textField(manualQa["evidence"], "manualQa.evidence"),
|
|
809
978
|
surfaceEvidence,
|
|
@@ -811,7 +980,7 @@ function validateQualityGate(input, opts) {
|
|
|
811
980
|
artifactRefs
|
|
812
981
|
},
|
|
813
982
|
gateReview: {
|
|
814
|
-
by:
|
|
983
|
+
by: gateReviewBy,
|
|
815
984
|
recommendation: literal(gateReview["recommendation"], "APPROVE", "gateReview.recommendation"),
|
|
816
985
|
reportPath: gateReportPath,
|
|
817
986
|
evidence: textField(gateReview["evidence"], "gateReview.evidence"),
|
|
@@ -832,6 +1001,23 @@ function validateQualityGate(input, opts) {
|
|
|
832
1001
|
adversarialClassesCovered: stringArray(coverage["adversarialClassesCovered"], "criteriaCoverage.adversarialClassesCovered")
|
|
833
1002
|
}
|
|
834
1003
|
};
|
|
1004
|
+
if (surface === "omo-senpi")
|
|
1005
|
+
return { surface, ...common };
|
|
1006
|
+
const codeReview = section(gate["codeReview"], "codeReview");
|
|
1007
|
+
const codeReportPath = textField(codeReview["reportPath"], "codeReview.reportPath");
|
|
1008
|
+
checkFile(codeReportPath, "codeReview.reportPath", opts);
|
|
1009
|
+
return {
|
|
1010
|
+
surface,
|
|
1011
|
+
...common,
|
|
1012
|
+
codeReview: {
|
|
1013
|
+
by: "lazycodex-code-reviewer",
|
|
1014
|
+
recommendation: literal(codeReview["recommendation"], "APPROVE", "codeReview.recommendation"),
|
|
1015
|
+
codeQualityStatus: codeQualityStatusField(codeReview["codeQualityStatus"], "codeReview.codeQualityStatus"),
|
|
1016
|
+
reportPath: codeReportPath,
|
|
1017
|
+
evidence: textField(codeReview["evidence"], "codeReview.evidence"),
|
|
1018
|
+
blockers: emptyBlockers(codeReview["blockers"], "codeReview.blockers")
|
|
1019
|
+
}
|
|
1020
|
+
};
|
|
835
1021
|
}
|
|
836
1022
|
function parseSurfaceEvidence(value, byId) {
|
|
837
1023
|
if (!Array.isArray(value) || value.length === 0)
|
|
@@ -874,7 +1060,7 @@ function parseAdversarialCases(value, byId) {
|
|
|
874
1060
|
});
|
|
875
1061
|
}
|
|
876
1062
|
|
|
877
|
-
//
|
|
1063
|
+
// components/ulw-loop/src/cli-arg-parser.ts
|
|
878
1064
|
import { readFile as readFile4 } from "node:fs/promises";
|
|
879
1065
|
var VALUE_FLAGS = new Set("--brief --brief-file --session-id --codex-goal-mode --validation-batch-json --goal --goal-id --criterion-id --status --evidence --notes --codex-goal-json --quality-gate-json --kind --rationale --title --objective --target-goal-id --source --after-json --directive-json --directive-file --idempotency-key --proposals-json".split(" "));
|
|
880
1066
|
var SUBCOMMANDS = new Set("create-goals status complete-goals criteria record-evidence checkpoint steer add-goal record-review-blockers".split(" "));
|
|
@@ -965,14 +1151,14 @@ function parseRecordEvidenceArgs(argv) {
|
|
|
965
1151
|
return notes ? { ...result, notes } : result;
|
|
966
1152
|
}
|
|
967
1153
|
|
|
968
|
-
//
|
|
1154
|
+
// components/ulw-loop/src/validation-batch.ts
|
|
969
1155
|
function isObject(value) {
|
|
970
1156
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
971
1157
|
}
|
|
972
1158
|
function read(value, key) {
|
|
973
1159
|
return Object.entries(value).find(([name]) => name === key)?.[1];
|
|
974
1160
|
}
|
|
975
|
-
function
|
|
1161
|
+
function text2(value, key) {
|
|
976
1162
|
const candidate = read(value, key);
|
|
977
1163
|
return typeof candidate === "string" && candidate.trim().length > 0 ? candidate.trim() : undefined;
|
|
978
1164
|
}
|
|
@@ -993,9 +1179,9 @@ async function parseValidationBatches(input, goals) {
|
|
|
993
1179
|
function batchFromObject(value) {
|
|
994
1180
|
if (!isObject(value))
|
|
995
1181
|
fail("validation batch entries must be objects.");
|
|
996
|
-
const batchId =
|
|
1182
|
+
const batchId = text2(value, "batchId");
|
|
997
1183
|
const memberIds = strings(value, "memberIds");
|
|
998
|
-
const finalGoalId =
|
|
1184
|
+
const finalGoalId = text2(value, "finalGoalId");
|
|
999
1185
|
if (batchId === undefined)
|
|
1000
1186
|
fail("validation batch requires batchId.");
|
|
1001
1187
|
if (memberIds === undefined || memberIds.length < 2)
|
|
@@ -1083,8 +1269,8 @@ function fail(message, code = "ULW_LOOP_VALIDATION_BATCH_INVALID") {
|
|
|
1083
1269
|
throw new UlwLoopError(message, code);
|
|
1084
1270
|
}
|
|
1085
1271
|
|
|
1086
|
-
//
|
|
1087
|
-
var QUALITY_GATE_FS = { existsSync:
|
|
1272
|
+
// components/ulw-loop/src/checkpoint.ts
|
|
1273
|
+
var QUALITY_GATE_FS = { existsSync: existsSync4, statSync };
|
|
1088
1274
|
function ulwLoopFail2(message, code) {
|
|
1089
1275
|
throw new UlwLoopError(message, code);
|
|
1090
1276
|
}
|
|
@@ -1109,8 +1295,8 @@ async function readJsonInput2(raw, repoRoot) {
|
|
|
1109
1295
|
if (!(error instanceof SyntaxError))
|
|
1110
1296
|
throw error;
|
|
1111
1297
|
}
|
|
1112
|
-
const path =
|
|
1113
|
-
if (!
|
|
1298
|
+
const path = resolve4(repoRoot, trimmed);
|
|
1299
|
+
if (!existsSync4(path))
|
|
1114
1300
|
return ulwLoopFail2("Quality gate JSON is neither valid JSON nor a readable path.", "ulw_loop_json_input_invalid");
|
|
1115
1301
|
try {
|
|
1116
1302
|
return JSON.parse(await readFile5(path, "utf8"));
|
|
@@ -1153,13 +1339,13 @@ function ledgerKind2(status, goal, aggregateCompletion) {
|
|
|
1153
1339
|
return status === "blocked" ? "goal_blocked" : "goal_failed";
|
|
1154
1340
|
}
|
|
1155
1341
|
function buildLedger(now, args, goal, qualityGate, codexGoal, aggregateCompletion) {
|
|
1156
|
-
const watch = qualityGate?.codeReview.codeQualityStatus === "WATCH";
|
|
1342
|
+
const watch = qualityGate?.surface === "lazycodex" && qualityGate.codeReview.codeQualityStatus === "WATCH";
|
|
1157
1343
|
const entry = {
|
|
1158
1344
|
at: now,
|
|
1159
1345
|
kind: ledgerKind2(args.status, goal, aggregateCompletion),
|
|
1160
1346
|
goalId: goal.id,
|
|
1161
1347
|
status: goal.status,
|
|
1162
|
-
evidence: watch ? `${args.evidence} | codeQuality=WATCH: ${qualityGate.codeReview.evidence}` : args.evidence
|
|
1348
|
+
evidence: watch && qualityGate.surface === "lazycodex" ? `${args.evidence} | codeQuality=WATCH: ${qualityGate.codeReview.evidence}` : args.evidence
|
|
1163
1349
|
};
|
|
1164
1350
|
if (codexGoal !== undefined)
|
|
1165
1351
|
entry.codexGoal = codexGoal;
|
|
@@ -1210,7 +1396,7 @@ async function checkpointUlwLoop(repoRoot, args, scope) {
|
|
|
1210
1396
|
const activeFinalTaskScoped = mismatchedTaskObjective && snapshot.status === "active" && await canReconcileActiveFinalTaskScopedAggregateSnapshot(repoRoot, plan, goal, objective, evidence, scope);
|
|
1211
1397
|
const taskScoped = completedTaskScoped || activeFinalTaskScoped;
|
|
1212
1398
|
if (!taskScoped)
|
|
1213
|
-
throw
|
|
1399
|
+
throw codexSnapshotMismatchError({ reconciliation, snapshot, expectedObjective: expectedCodexObjective(plan, goal), taskScopedHint: { goal, aggregate, final } });
|
|
1214
1400
|
}
|
|
1215
1401
|
if (closesBatch)
|
|
1216
1402
|
requireBatchFinalReady(plan, goal);
|
|
@@ -1222,6 +1408,7 @@ async function checkpointUlwLoop(repoRoot, args, scope) {
|
|
|
1222
1408
|
qualityGate = validateQualityGate(await readJsonInput2(args.qualityGateJson, repoRoot), {
|
|
1223
1409
|
repoRoot,
|
|
1224
1410
|
fs: QUALITY_GATE_FS,
|
|
1411
|
+
reviewerSurface: resolveToolkitSurface(),
|
|
1225
1412
|
...plan.evidenceLayoutVersion === 2 ? { currentAttemptDir: ulwLoopAttemptEvidenceDir(goal.id, goal.attempt, scope) } : {}
|
|
1226
1413
|
});
|
|
1227
1414
|
requireBatchGate(plan, goal, qualityGate);
|
|
@@ -1250,7 +1437,116 @@ async function checkpointUlwLoop(repoRoot, args, scope) {
|
|
|
1250
1437
|
});
|
|
1251
1438
|
}
|
|
1252
1439
|
|
|
1253
|
-
//
|
|
1440
|
+
// components/ulw-loop/src/checkpoint-template.ts
|
|
1441
|
+
function artifactPath(base, name) {
|
|
1442
|
+
return `${base}/${name}`;
|
|
1443
|
+
}
|
|
1444
|
+
function gateTemplate(surface, base) {
|
|
1445
|
+
const artifacts = [
|
|
1446
|
+
{
|
|
1447
|
+
id: "artifact-cli",
|
|
1448
|
+
kind: "cli-transcript",
|
|
1449
|
+
description: "<replace:artifact description>",
|
|
1450
|
+
path: artifactPath(base, "cli-transcript.txt")
|
|
1451
|
+
},
|
|
1452
|
+
{
|
|
1453
|
+
id: "artifact-data",
|
|
1454
|
+
kind: "data-diff",
|
|
1455
|
+
description: "<replace:artifact description>",
|
|
1456
|
+
path: artifactPath(base, "data-diff.txt")
|
|
1457
|
+
}
|
|
1458
|
+
];
|
|
1459
|
+
const manualQa = {
|
|
1460
|
+
by: surface === "omo-senpi" ? "main-session" : "lazycodex-qa-executor",
|
|
1461
|
+
status: "passed",
|
|
1462
|
+
evidence: "<replace:manual QA evidence>",
|
|
1463
|
+
surfaceEvidence: [
|
|
1464
|
+
{
|
|
1465
|
+
id: "surface-cli",
|
|
1466
|
+
criterionRef: "<replace:criterion id>",
|
|
1467
|
+
surface: "cli",
|
|
1468
|
+
invocation: "<replace:command>",
|
|
1469
|
+
verdict: "passed",
|
|
1470
|
+
artifactRefs: ["artifact-cli"]
|
|
1471
|
+
},
|
|
1472
|
+
{
|
|
1473
|
+
id: "surface-data",
|
|
1474
|
+
criterionRef: "<replace:criterion id>",
|
|
1475
|
+
surface: "data",
|
|
1476
|
+
invocation: "<replace:command>",
|
|
1477
|
+
verdict: "passed",
|
|
1478
|
+
artifactRefs: ["artifact-data"]
|
|
1479
|
+
}
|
|
1480
|
+
],
|
|
1481
|
+
adversarialCases: [
|
|
1482
|
+
{
|
|
1483
|
+
id: "<replace:adversarial case id>",
|
|
1484
|
+
criterionRef: "<replace:criterion id>",
|
|
1485
|
+
scenario: "<replace:scenario>",
|
|
1486
|
+
expectedBehavior: "<replace:expected behavior>",
|
|
1487
|
+
verdict: "not_applicable",
|
|
1488
|
+
reason: "<replace:reason>",
|
|
1489
|
+
artifactRefs: ["artifact-cli"]
|
|
1490
|
+
}
|
|
1491
|
+
],
|
|
1492
|
+
artifactRefs: artifacts
|
|
1493
|
+
};
|
|
1494
|
+
const common = {
|
|
1495
|
+
manualQa,
|
|
1496
|
+
gateReview: {
|
|
1497
|
+
by: surface === "omo-senpi" ? "category:deep" : "lazycodex-gate-reviewer",
|
|
1498
|
+
recommendation: "APPROVE",
|
|
1499
|
+
reportPath: artifactPath(base, "gate-review.md"),
|
|
1500
|
+
evidence: "<replace:gate review evidence>",
|
|
1501
|
+
blockers: []
|
|
1502
|
+
},
|
|
1503
|
+
iteration: {
|
|
1504
|
+
fullRerun: true,
|
|
1505
|
+
status: "passed",
|
|
1506
|
+
rerunCommands: ["<replace:verification command>"],
|
|
1507
|
+
evidence: "<replace:iteration evidence>"
|
|
1508
|
+
},
|
|
1509
|
+
criteriaCoverage: {
|
|
1510
|
+
totalCriteria: 0,
|
|
1511
|
+
passCount: 0,
|
|
1512
|
+
originalIntent: "<replace:original intent>",
|
|
1513
|
+
desiredOutcome: "<replace:desired outcome>",
|
|
1514
|
+
userOutcomeReview: "<replace:user outcome review>",
|
|
1515
|
+
adversarialClassesCovered: ["<replace:adversarial class>"]
|
|
1516
|
+
}
|
|
1517
|
+
};
|
|
1518
|
+
if (surface === "omo-senpi")
|
|
1519
|
+
return common;
|
|
1520
|
+
return {
|
|
1521
|
+
codeReview: {
|
|
1522
|
+
by: "lazycodex-code-reviewer",
|
|
1523
|
+
recommendation: "APPROVE",
|
|
1524
|
+
codeQualityStatus: "CLEAR",
|
|
1525
|
+
reportPath: artifactPath(base, "code-review.md"),
|
|
1526
|
+
evidence: "<replace:code review evidence>",
|
|
1527
|
+
blockers: []
|
|
1528
|
+
},
|
|
1529
|
+
...common
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
async function checkpointTemplate(repoRoot, scope, goalId) {
|
|
1533
|
+
const plan = await readUlwLoopPlan(repoRoot, scope);
|
|
1534
|
+
const targetId = goalId ?? plan.activeGoalId;
|
|
1535
|
+
const active = plan.goals.find((goal) => goal.id === targetId);
|
|
1536
|
+
if (goalId !== undefined && active === undefined)
|
|
1537
|
+
throw new UlwLoopError(`Unknown ulw-loop id: ${goalId}.`, "ULW_LOOP_GOAL_NOT_FOUND", { details: { goalId } });
|
|
1538
|
+
const hasAttempt = plan.evidenceLayoutVersion === 2 && active !== undefined;
|
|
1539
|
+
const attemptDir = hasAttempt ? ulwLoopAttemptEvidenceDir(active.id, active.attempt, scope) : ".omo/evidence";
|
|
1540
|
+
return {
|
|
1541
|
+
qualityGateTemplate: gateTemplate(resolveToolkitSurface(), attemptDir),
|
|
1542
|
+
codexGoalTemplate: {
|
|
1543
|
+
goal: { objective: plan.codexObjective ?? "<replace:codex objective>", status: "complete" }
|
|
1544
|
+
},
|
|
1545
|
+
...hasAttempt ? { attemptDir } : { guidance: "This plan is evidence-layout v1; artifacts go under .omo/evidence/." }
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
// components/ulw-loop/src/cli-output.ts
|
|
1254
1550
|
var ULW_LOOP_HELP = `Usage:
|
|
1255
1551
|
omo-agent-toolkit hook user-prompt-submit [--with-ultrawork] (Codex UserPromptSubmit hook)
|
|
1256
1552
|
omo-agent-toolkit help | --help | -h (this message)
|
|
@@ -1260,6 +1556,7 @@ var ULW_LOOP_HELP = `Usage:
|
|
|
1260
1556
|
omo-agent-toolkit ulw-loop complete-goals [--retry-failed] [--json]
|
|
1261
1557
|
omo-agent-toolkit ulw-loop criteria --goal-id <id> [--json]
|
|
1262
1558
|
omo-agent-toolkit ulw-loop record-evidence --goal-id <id> --criterion-id <id> --status pass|fail|blocked --evidence "..." [--notes "..."] [--json]
|
|
1559
|
+
omo-agent-toolkit ulw-loop checkpoint --print-template [--goal-id <id>] [--json]
|
|
1263
1560
|
omo-agent-toolkit ulw-loop checkpoint --goal-id <id> --status complete|failed|blocked --evidence "..." --codex-goal-json <...> [--quality-gate-json <...>] [--no-advance] [--json]
|
|
1264
1561
|
omo-agent-toolkit ulw-loop steer --kind <kind> ... --evidence "..." --rationale "..." [--proposals-json <json-or-path>] [--json]
|
|
1265
1562
|
omo-agent-toolkit ulw-loop add-goal --title "..." --objective "..." [--json]
|
|
@@ -1340,17 +1637,18 @@ function normalizeCodexGoalMode(value) {
|
|
|
1340
1637
|
throw new UlwLoopError("Invalid --codex-goal-mode; expected aggregate or per_story.", "ULW_LOOP_CODEX_GOAL_MODE_INVALID", { details: { value } });
|
|
1341
1638
|
}
|
|
1342
1639
|
|
|
1343
|
-
//
|
|
1640
|
+
// components/ulw-loop/src/codex-goal-instruction.ts
|
|
1344
1641
|
function buildCodexGoalInstruction(args) {
|
|
1345
1642
|
const mode = codexGoalMode(args.plan);
|
|
1346
1643
|
const createGoal = buildCreateGoalPayload(args.plan, args.goal);
|
|
1347
1644
|
const isFinal = args.isFinal ?? isFinalRunCompletionCandidate(args.plan, args.goal);
|
|
1348
|
-
|
|
1645
|
+
const surface = args.surface ?? resolveToolkitSurface();
|
|
1646
|
+
return { text: buildText(mode, args.plan, args.goal, createGoal, isFinal, surface), json: createGoal };
|
|
1349
1647
|
}
|
|
1350
1648
|
function buildCreateGoalPayload(plan, goal) {
|
|
1351
1649
|
return { objective: expectedCodexObjective(plan, goal) };
|
|
1352
1650
|
}
|
|
1353
|
-
function buildText(mode, plan, goal, createGoal, isFinal) {
|
|
1651
|
+
function buildText(mode, plan, goal, createGoal, isFinal, surface) {
|
|
1354
1652
|
return joinLines([
|
|
1355
1653
|
mode === "aggregate" ? "UlwLoop aggregate-goal handoff" : "UlwLoop active-goal handoff",
|
|
1356
1654
|
`Mode: ${mode}`,
|
|
@@ -1367,7 +1665,7 @@ function buildText(mode, plan, goal, createGoal, isFinal) {
|
|
|
1367
1665
|
"- Goals are unlimited. Do not add numeric limits.",
|
|
1368
1666
|
...modeConstraintLines(mode, isFinal),
|
|
1369
1667
|
...evidenceLayoutLines(plan),
|
|
1370
|
-
finalSection(plan, goal, isFinal, mode === "aggregate"),
|
|
1668
|
+
finalSection(plan, goal, isFinal, mode === "aggregate", surface),
|
|
1371
1669
|
...checkpointLines(plan, mode),
|
|
1372
1670
|
"",
|
|
1373
1671
|
"create_goal payload:",
|
|
@@ -1419,18 +1717,21 @@ function evidenceLayoutLines(plan) {
|
|
|
1419
1717
|
"- Evidence layout v2: write every artifact for the active goal (QA matrix, review reports, receipts) under the current attempt directory — read currentAttemptDir from `omo-agent-toolkit ulw-loop status --json` (.omo/evidence/ulw/<session>/<goalId>/a<attempt>). The final checkpoint rejects quality-gate artifacts outside that directory."
|
|
1420
1718
|
];
|
|
1421
1719
|
}
|
|
1422
|
-
function finalSection(plan, goal, isFinal, aggregate) {
|
|
1720
|
+
function finalSection(plan, goal, isFinal, aggregate, surface) {
|
|
1721
|
+
const roles = reviewerRolesFor(surface);
|
|
1423
1722
|
if (!isFinal)
|
|
1424
1723
|
return "- This is not the final ulw-loop story; do not run the final reviewer/manual-QA/gate-review quality gate yet.";
|
|
1425
1724
|
const option = sessionOption(plan);
|
|
1725
|
+
if (surface === "omo-senpi")
|
|
1726
|
+
return senpiFinalSection(plan, goal, aggregate);
|
|
1426
1727
|
const blockerCommand = `omo-agent-toolkit ulw-loop record-review-blockers${option} --goal-id ${goal.id} --title "Resolve final code-review blockers" --objective "<blocker-resolution objective>" --evidence "<review findings>" --codex-goal-json "<active get_goal JSON or path>"`;
|
|
1427
1728
|
const checkpointCommand = `omo-agent-toolkit ulw-loop checkpoint${option} --goal-id ${goal.id} --status complete --evidence "<targeted verification/manualQa/gateReview evidence>" --codex-goal-json "<fresh complete get_goal JSON or path>" --quality-gate-json "<quality gate JSON or path>"`;
|
|
1428
1729
|
return joinLines([
|
|
1429
1730
|
"Final story — run mandatory quality gate before update_goal:",
|
|
1430
1731
|
"- Run targeted verification for changed behavior.",
|
|
1431
1732
|
"- Confirm every manualQa artifact path exists and has non-zero size.",
|
|
1432
|
-
|
|
1433
|
-
|
|
1733
|
+
`- First spawn ${roles.codeReview} and ${roles.manualQa} in parallel (fork_context: false on the v1 surface; fork_turns: "none" on v2). Include the original brief, goal objectives, desired user-visible outcome, diff, and evidence; wait for BOTH to return and confirm their report artifacts exist on disk (code-review report + manualQa matrix).`,
|
|
1734
|
+
`- Only then spawn ${roles.gateReview} (same fork settings), passing those artifact paths.`,
|
|
1434
1735
|
"- Require clean codeReview, manualQa, gateReview, iteration, and criteriaCoverage. criteriaCoverage must summarize originalIntent, desiredOutcome, and userOutcomeReview; counts alone are not approval.",
|
|
1435
1736
|
"- On a reviewer REJECT, fix only the cited blockers, rerun the affected verification/Manual-QA, and re-review the delta at most TWICE; if blockers remain, record them and surface to the user.",
|
|
1436
1737
|
"- If codeQualityStatus is WATCH, include the WATCH notes verbatim in your final user-facing message.",
|
|
@@ -1440,6 +1741,20 @@ function finalSection(plan, goal, isFinal, aggregate) {
|
|
|
1440
1741
|
` ${checkpointCommand}`
|
|
1441
1742
|
]);
|
|
1442
1743
|
}
|
|
1744
|
+
function senpiFinalSection(plan, goal, aggregate) {
|
|
1745
|
+
const option = sessionOption(plan);
|
|
1746
|
+
const checkpointCommand = `omo-agent-toolkit ulw-loop checkpoint${option} --goal-id ${goal.id} --status complete --evidence "<manualQa/gateReview evidence>" --codex-goal-json "<fresh complete get_goal JSON or path>" --quality-gate-json "$(omo-agent-toolkit ulw-loop checkpoint${option} --print-template)"`;
|
|
1747
|
+
return joinLines([
|
|
1748
|
+
"Final story — run the single-reviewer quality gate before update_goal:",
|
|
1749
|
+
'- Run manual QA yourself and write the non-empty artifact under currentAttemptDir; set manualQa.by to "main-session".',
|
|
1750
|
+
'- Spawn exactly one gate reviewer with task(category: "deep").',
|
|
1751
|
+
"- If that task fails with any model_unavailable failure, retry with category:unspecified-high, then category:unspecified-low.",
|
|
1752
|
+
"- Set gateReview.by to the exact category:<name> literal used for the successful task.",
|
|
1753
|
+
"- Build the gate JSON with omo-agent-toolkit ulw-loop checkpoint --print-template, then fill manualQa, gateReview, iteration, and criteriaCoverage.",
|
|
1754
|
+
'- Require passed manualQa, approved gateReview, passed iteration, and complete criteriaCoverage before update_goal({status: "complete"}).',
|
|
1755
|
+
aggregate ? `- If the gate is clean, call update_goal({status: "complete"}), call get_goal again, then checkpoint the aggregate story: ${checkpointCommand}` : `- If the gate is clean, call update_goal({status: "complete"}), call get_goal again, then checkpoint: ${checkpointCommand}`
|
|
1756
|
+
]);
|
|
1757
|
+
}
|
|
1443
1758
|
function sessionOption(plan) {
|
|
1444
1759
|
const prefix = ".omo/ulw-loop/";
|
|
1445
1760
|
const suffix = "/goals.json";
|
|
@@ -1453,11 +1768,11 @@ function joinLines(lines) {
|
|
|
1453
1768
|
`);
|
|
1454
1769
|
}
|
|
1455
1770
|
|
|
1456
|
-
//
|
|
1457
|
-
import { existsSync as
|
|
1771
|
+
// components/ulw-loop/src/plan-crud.ts
|
|
1772
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
1458
1773
|
import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
|
|
1459
1774
|
|
|
1460
|
-
//
|
|
1775
|
+
// components/ulw-loop/src/plan-goal-factory.ts
|
|
1461
1776
|
function cleanLine(line) {
|
|
1462
1777
|
return line.replace(/^\s*(?:[-*+]\s+|\d+[.)]\s+)/, "").trim();
|
|
1463
1778
|
}
|
|
@@ -1546,7 +1861,7 @@ function appendGoalToPlan(plan, title, objective, now) {
|
|
|
1546
1861
|
return goal;
|
|
1547
1862
|
}
|
|
1548
1863
|
|
|
1549
|
-
//
|
|
1864
|
+
// components/ulw-loop/src/plan-crud.ts
|
|
1550
1865
|
function isScheduleEligible(goal) {
|
|
1551
1866
|
return goal.steeringStatus !== "superseded" && goal.steeringStatus !== "blocked";
|
|
1552
1867
|
}
|
|
@@ -1564,7 +1879,7 @@ function clearGoalBlockerFields2(goal) {
|
|
|
1564
1879
|
}
|
|
1565
1880
|
async function createUlwLoopPlan(repoRoot, args, scope) {
|
|
1566
1881
|
return withUlwLoopMutationLock(repoRoot, scope, async () => {
|
|
1567
|
-
if (!args.force &&
|
|
1882
|
+
if (!args.force && existsSync5(ulwLoopGoalsPath(repoRoot, scope))) {
|
|
1568
1883
|
const existing = await readUlwLoopPlan(repoRoot, scope);
|
|
1569
1884
|
if (isUlwLoopDone(existing))
|
|
1570
1885
|
throw completedPlanExistsError(scope);
|
|
@@ -1673,7 +1988,7 @@ function summarizeUlwLoopPlan(plan) {
|
|
|
1673
1988
|
};
|
|
1674
1989
|
}
|
|
1675
1990
|
|
|
1676
|
-
//
|
|
1991
|
+
// components/ulw-loop/src/checkpoint-continuation.ts
|
|
1677
1992
|
async function checkpointAndContinue(repoRoot, args, scope) {
|
|
1678
1993
|
const result = await checkpointUlwLoop(repoRoot, args, scope);
|
|
1679
1994
|
if (args.status !== "complete" || result.aggregateCompletion !== undefined || !args.advance)
|
|
@@ -1685,6 +2000,14 @@ async function checkpointAndContinue(repoRoot, args, scope) {
|
|
|
1685
2000
|
return { ...result, plan: next.plan, next: { resumed: next.resumed, goal: next.goal, instruction } };
|
|
1686
2001
|
}
|
|
1687
2002
|
async function checkpoint(repoRoot, argv, json, scope) {
|
|
2003
|
+
if (hasFlag(argv, "--print-template")) {
|
|
2004
|
+
const template = await checkpointTemplate(repoRoot, scope, readValue(argv, "--goal-id"));
|
|
2005
|
+
if (json)
|
|
2006
|
+
printJson({ ok: true, ...template });
|
|
2007
|
+
else
|
|
2008
|
+
printJson(template);
|
|
2009
|
+
return 0;
|
|
2010
|
+
}
|
|
1688
2011
|
const goalId = required2(argv, "--goal-id");
|
|
1689
2012
|
const statusValue = checkpointStatus(required2(argv, "--status"));
|
|
1690
2013
|
const evidence = required2(argv, "--evidence");
|
|
@@ -1736,10 +2059,10 @@ function checkpointStatus(value) {
|
|
|
1736
2059
|
throw new UlwLoopError("Missing or invalid --status; expected complete, failed, or blocked.", "ULW_LOOP_STATUS_INVALID", { details: { status: value } });
|
|
1737
2060
|
}
|
|
1738
2061
|
|
|
1739
|
-
//
|
|
2062
|
+
// components/ulw-loop/src/cli-subcommands.ts
|
|
1740
2063
|
import { readFile as readFile6 } from "node:fs/promises";
|
|
1741
2064
|
|
|
1742
|
-
//
|
|
2065
|
+
// components/ulw-loop/src/cli-steering.ts
|
|
1743
2066
|
var SOURCES = ["user_prompt_submit", "finding", "cli"];
|
|
1744
2067
|
var STEERING_KIND_HELP = [
|
|
1745
2068
|
`Allowed --kind values: ${ULW_LOOP_STEERING_MUTATION_KINDS.join(", ")}`,
|
|
@@ -1771,7 +2094,7 @@ function kindMessage(prefix) {
|
|
|
1771
2094
|
|
|
1772
2095
|
${STEERING_KIND_HELP}`;
|
|
1773
2096
|
}
|
|
1774
|
-
function
|
|
2097
|
+
function text3(value, field) {
|
|
1775
2098
|
if (value === undefined)
|
|
1776
2099
|
return;
|
|
1777
2100
|
const trimmed = value.trim();
|
|
@@ -1780,11 +2103,11 @@ function text2(value, field) {
|
|
|
1780
2103
|
return fail2(`Empty ${field}.`, "ULW_LOOP_STEERING_FIELD_EMPTY", { field });
|
|
1781
2104
|
}
|
|
1782
2105
|
function required3(argv, flag) {
|
|
1783
|
-
const value =
|
|
2106
|
+
const value = text3(readValue(argv, flag), flag);
|
|
1784
2107
|
return value ?? fail2(`Missing ${flag}.`, "ULW_LOOP_STEERING_FIELD_REQUIRED", { flag });
|
|
1785
2108
|
}
|
|
1786
2109
|
function requiredGoal(argv) {
|
|
1787
|
-
const value =
|
|
2110
|
+
const value = text3(parseGoalArg(argv), "--goal-id");
|
|
1788
2111
|
return value ?? fail2("Missing --goal-id.", "ULW_LOOP_GOAL_ID_REQUIRED", { flag: "--goal-id" });
|
|
1789
2112
|
}
|
|
1790
2113
|
function readObject(value, key) {
|
|
@@ -1835,14 +2158,14 @@ function parseSteeringSource(argv) {
|
|
|
1835
2158
|
function child(value) {
|
|
1836
2159
|
if (!isPlain(value))
|
|
1837
2160
|
return null;
|
|
1838
|
-
const title =
|
|
1839
|
-
const objective =
|
|
2161
|
+
const title = text3(objectText(value, "title"), "title");
|
|
2162
|
+
const objective = text3(objectText(value, "objective"), "objective");
|
|
1840
2163
|
if (title === undefined || objective === undefined)
|
|
1841
2164
|
return null;
|
|
1842
2165
|
return { title, objective };
|
|
1843
2166
|
}
|
|
1844
2167
|
async function children(argv, flag, needed) {
|
|
1845
|
-
const input = needed ? required3(argv, flag) :
|
|
2168
|
+
const input = needed ? required3(argv, flag) : text3(readValue(argv, flag), flag);
|
|
1846
2169
|
if (input === undefined)
|
|
1847
2170
|
return [];
|
|
1848
2171
|
const raw = await readJsonInput(input);
|
|
@@ -1865,12 +2188,12 @@ async function stringArray2(argv, flag) {
|
|
|
1865
2188
|
for (const item of raw) {
|
|
1866
2189
|
if (typeof item !== "string")
|
|
1867
2190
|
return fail2(`${flag} entries must be strings.`, "ULW_LOOP_STEERING_STRING_ARRAY_REQUIRED", { flag });
|
|
1868
|
-
values.push(
|
|
2191
|
+
values.push(text3(item, flag) ?? "");
|
|
1869
2192
|
}
|
|
1870
2193
|
return values;
|
|
1871
2194
|
}
|
|
1872
2195
|
function model(value) {
|
|
1873
|
-
const trimmed =
|
|
2196
|
+
const trimmed = text3(value, "--user-model");
|
|
1874
2197
|
if (trimmed === undefined)
|
|
1875
2198
|
return;
|
|
1876
2199
|
return isModel(trimmed) ? trimmed : fail2(`Invalid --user-model: ${trimmed}.`, "ULW_LOOP_STEERING_USER_MODEL_INVALID", { value: trimmed, expected: ULW_LOOP_SUCCESS_CRITERION_USER_MODELS });
|
|
@@ -1881,7 +2204,7 @@ function neverKind(kind) {
|
|
|
1881
2204
|
async function parseSteeringProposal(argv) {
|
|
1882
2205
|
const kind = parseSteeringKind(argv);
|
|
1883
2206
|
const source = parseSteeringSource(argv);
|
|
1884
|
-
const idempotencyKey =
|
|
2207
|
+
const idempotencyKey = text3(readValue(argv, "--idempotency-key"), "--idempotency-key");
|
|
1885
2208
|
const base = { kind, source, evidence: required3(argv, "--evidence"), rationale: required3(argv, "--rationale"), ...idempotencyKey === undefined ? {} : { idempotencyKey } };
|
|
1886
2209
|
switch (kind) {
|
|
1887
2210
|
case "add_subgoal":
|
|
@@ -1924,36 +2247,36 @@ async function parseSteeringProposal(argv) {
|
|
|
1924
2247
|
function normalizedChildren(values) {
|
|
1925
2248
|
if (values === undefined)
|
|
1926
2249
|
return;
|
|
1927
|
-
return values.map((item) => ({ title:
|
|
2250
|
+
return values.map((item) => ({ title: text3(item.title, "child.title") ?? "", objective: text3(item.objective, "child.objective") ?? "" }));
|
|
1928
2251
|
}
|
|
1929
2252
|
function normalizedStrings(values, field) {
|
|
1930
2253
|
if (values === undefined)
|
|
1931
2254
|
return;
|
|
1932
|
-
return values.map((value) =>
|
|
2255
|
+
return values.map((value) => text3(value, field) ?? "");
|
|
1933
2256
|
}
|
|
1934
2257
|
function normalizeSteeringProposal(proposal) {
|
|
1935
|
-
const evidence =
|
|
1936
|
-
const rationale =
|
|
1937
|
-
const goalId =
|
|
1938
|
-
const targetGoalId =
|
|
2258
|
+
const evidence = text3(proposal.evidence, "evidence") ?? "";
|
|
2259
|
+
const rationale = text3(proposal.rationale, "rationale") ?? "";
|
|
2260
|
+
const goalId = text3(proposal.goalId, "goalId");
|
|
2261
|
+
const targetGoalId = text3(proposal.targetGoalId, "targetGoalId");
|
|
1939
2262
|
const targetGoalIds = normalizedStrings(proposal.targetGoalIds, "targetGoalIds");
|
|
1940
|
-
const criterionId =
|
|
1941
|
-
const title =
|
|
1942
|
-
const objective =
|
|
1943
|
-
const revisedTitle =
|
|
1944
|
-
const revisedObjective =
|
|
1945
|
-
const blockedReason =
|
|
1946
|
-
const directiveText =
|
|
1947
|
-
const promptSignature =
|
|
1948
|
-
const idempotencyKey =
|
|
1949
|
-
const scenario =
|
|
1950
|
-
const expectedEvidence =
|
|
2263
|
+
const criterionId = text3(proposal.criterionId, "criterionId");
|
|
2264
|
+
const title = text3(proposal.title, "title");
|
|
2265
|
+
const objective = text3(proposal.objective, "objective");
|
|
2266
|
+
const revisedTitle = text3(proposal.revisedTitle, "revisedTitle");
|
|
2267
|
+
const revisedObjective = text3(proposal.revisedObjective, "revisedObjective");
|
|
2268
|
+
const blockedReason = text3(proposal.blockedReason, "blockedReason");
|
|
2269
|
+
const directiveText = text3(proposal.directiveText, "directiveText");
|
|
2270
|
+
const promptSignature = text3(proposal.promptSignature, "promptSignature");
|
|
2271
|
+
const idempotencyKey = text3(proposal.idempotencyKey, "idempotencyKey");
|
|
2272
|
+
const scenario = text3(proposal.scenario, "scenario");
|
|
2273
|
+
const expectedEvidence = text3(proposal.expectedEvidence, "expectedEvidence");
|
|
1951
2274
|
const childGoals = normalizedChildren(proposal.childGoals);
|
|
1952
2275
|
const pendingOrder = normalizedStrings(proposal.pendingOrder, "pendingOrder");
|
|
1953
2276
|
return { kind: proposal.kind, source: proposal.source, evidence, rationale, ...goalId === undefined ? {} : { goalId }, ...targetGoalId === undefined ? {} : { targetGoalId }, ...targetGoalIds === undefined ? {} : { targetGoalIds }, ...criterionId === undefined ? {} : { criterionId }, ...title === undefined ? {} : { title }, ...objective === undefined ? {} : { objective }, ...childGoals === undefined ? {} : { childGoals }, ...revisedTitle === undefined ? {} : { revisedTitle }, ...revisedObjective === undefined ? {} : { revisedObjective }, ...pendingOrder === undefined ? {} : { pendingOrder }, ...blockedReason === undefined ? {} : { blockedReason }, ...proposal.after === undefined ? {} : { after: proposal.after }, ...directiveText === undefined ? {} : { directiveText }, ...promptSignature === undefined ? {} : { promptSignature }, ...idempotencyKey === undefined ? {} : { idempotencyKey }, ...proposal.now === undefined ? {} : { now: proposal.now }, ...scenario === undefined ? {} : { scenario }, ...expectedEvidence === undefined ? {} : { expectedEvidence }, ...proposal.userModel === undefined ? {} : { userModel: proposal.userModel } };
|
|
1954
2277
|
}
|
|
1955
2278
|
async function parseSteeringProposals(argv) {
|
|
1956
|
-
const input =
|
|
2279
|
+
const input = text3(readValue(argv, "--proposals-json"), "--proposals-json");
|
|
1957
2280
|
if (input === undefined)
|
|
1958
2281
|
return [await parseSteeringProposal(argv)];
|
|
1959
2282
|
if (readValue(argv, "--kind") !== undefined)
|
|
@@ -2046,7 +2369,7 @@ function printSteerBatchResult(result, json) {
|
|
|
2046
2369
|
printStatus(result.plan);
|
|
2047
2370
|
}
|
|
2048
2371
|
|
|
2049
|
-
//
|
|
2372
|
+
// components/ulw-loop/src/review-blockers.ts
|
|
2050
2373
|
var BLOCKER_FIELDS = "blockedReason blockerSignature blockerOccurrenceCount requiredExternalDecision nonRetriable failedAt failureReason completedAt blocker blockerEvidence blockerOccurrences blockedAt".split(" ");
|
|
2051
2374
|
function ulwLoopError(message, code) {
|
|
2052
2375
|
throw new UlwLoopError(message, code);
|
|
@@ -2085,9 +2408,10 @@ async function recordFinalReviewBlockers(repoRoot, args, scope) {
|
|
|
2085
2408
|
ulwLoopError(`${goal.id} is not final.`, "ulw_loop_not_final_story");
|
|
2086
2409
|
const snapshot = await readCodexGoalSnapshotInput(args.codexGoalJson, repoRoot);
|
|
2087
2410
|
const aggregate = codexGoalMode(plan) === "aggregate";
|
|
2088
|
-
const
|
|
2411
|
+
const expected = expectedCodexObjective(plan, goal);
|
|
2412
|
+
const reconciliation = reconcileCodexGoalSnapshot(snapshot, { expectedObjective: expected, ...aggregate ? { acceptedObjectives: compatibleCodexObjectives(plan) } : {}, allowedStatuses: ["active"], requireSnapshot: true, requireComplete: false });
|
|
2089
2413
|
if (!reconciliation.ok)
|
|
2090
|
-
|
|
2414
|
+
throw codexSnapshotMismatchError({ reconciliation, snapshot, expectedObjective: expected });
|
|
2091
2415
|
const now = iso();
|
|
2092
2416
|
for (const field of BLOCKER_FIELDS)
|
|
2093
2417
|
Reflect.deleteProperty(goal, field);
|
|
@@ -2112,10 +2436,39 @@ async function recordFinalReviewBlockers(repoRoot, args, scope) {
|
|
|
2112
2436
|
});
|
|
2113
2437
|
}
|
|
2114
2438
|
|
|
2115
|
-
//
|
|
2439
|
+
// components/ulw-loop/src/status-next-actions.ts
|
|
2440
|
+
function statusNextActions(plan) {
|
|
2441
|
+
const actions = [];
|
|
2442
|
+
const active = plan.goals.find((goal) => goal.id === plan.activeGoalId);
|
|
2443
|
+
if (plan.goals.length === 0)
|
|
2444
|
+
actions.push(`No goals yet: bootstrap with \`${ULW_LOOP_CREATE_GOALS_COMMAND}\`.`);
|
|
2445
|
+
else if (active !== undefined)
|
|
2446
|
+
actions.push(...activeGoalActions(plan, active));
|
|
2447
|
+
for (const goal of plan.goals.filter((candidate) => candidate.status === "review_blocked"))
|
|
2448
|
+
actions.push(`${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>'\`.`);
|
|
2449
|
+
if (plan.evidenceLayoutVersion !== 2 || active === undefined)
|
|
2450
|
+
actions.push("plan is evidence-layout v1; artifacts go under .omo/evidence/");
|
|
2451
|
+
return actions;
|
|
2452
|
+
}
|
|
2453
|
+
function activeGoalActions(plan, active) {
|
|
2454
|
+
const unresolved = active.successCriteria.filter((criterion) => criterion.status !== "pass");
|
|
2455
|
+
if (unresolved.length > 0)
|
|
2456
|
+
return [
|
|
2457
|
+
`${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>"\`.`
|
|
2458
|
+
];
|
|
2459
|
+
if (hasAllCriteriaPass(active) && isFinalRunCompletionCandidate(plan, active))
|
|
2460
|
+
return [
|
|
2461
|
+
`${active.id} passes every criterion and is the final story: update_goal complete, then checkpoint --print-template to build the final quality gate.`
|
|
2462
|
+
];
|
|
2463
|
+
return [
|
|
2464
|
+
`${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>'\`.`
|
|
2465
|
+
];
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
// components/ulw-loop/src/steering-mutations.ts
|
|
2116
2469
|
var read2 = (value, key) => Object.entries(value).find(([name]) => name === key)?.[1];
|
|
2117
2470
|
var isText = (value) => typeof value === "string" && value.trim().length > 0;
|
|
2118
|
-
var
|
|
2471
|
+
var text4 = (value, key) => {
|
|
2119
2472
|
const candidate = read2(value, key);
|
|
2120
2473
|
return isText(candidate) ? candidate.trim() : undefined;
|
|
2121
2474
|
};
|
|
@@ -2124,13 +2477,13 @@ var after = (proposal) => {
|
|
|
2124
2477
|
const candidate = read2(proposal, "after");
|
|
2125
2478
|
return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate) ? candidate : undefined;
|
|
2126
2479
|
};
|
|
2127
|
-
var revised = (proposal, direct, nested) =>
|
|
2128
|
-
var targets = (proposal) => proposal.targetGoalIds ?? [proposal.targetGoalId ??
|
|
2480
|
+
var revised = (proposal, direct, nested) => text4(proposal, direct) ?? text4(after(proposal) ?? proposal, nested);
|
|
2481
|
+
var targets = (proposal) => proposal.targetGoalIds ?? [proposal.targetGoalId ?? text4(proposal, "goalId") ?? ""].filter(Boolean);
|
|
2129
2482
|
var child2 = (value) => {
|
|
2130
2483
|
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
2131
2484
|
return null;
|
|
2132
|
-
const title =
|
|
2133
|
-
const objective =
|
|
2485
|
+
const title = text4(value, "title");
|
|
2486
|
+
const objective = text4(value, "objective");
|
|
2134
2487
|
return title === undefined || objective === undefined ? null : { title, objective };
|
|
2135
2488
|
};
|
|
2136
2489
|
var children2 = (proposal) => {
|
|
@@ -2195,11 +2548,11 @@ function reviseCriterion(plan, proposal, now) {
|
|
|
2195
2548
|
if (target === undefined || current === undefined)
|
|
2196
2549
|
return;
|
|
2197
2550
|
const model2 = read2(proposal, "userModel");
|
|
2198
|
-
target.successCriteria[index] = { ...current, scenario:
|
|
2551
|
+
target.successCriteria[index] = { ...current, scenario: text4(proposal, "scenario") ?? current.scenario, expectedEvidence: text4(proposal, "expectedEvidence") ?? current.expectedEvidence, userModel: isModel2(model2) ? model2 : current.userModel };
|
|
2199
2552
|
target.updatedAt = now;
|
|
2200
2553
|
}
|
|
2201
2554
|
|
|
2202
|
-
//
|
|
2555
|
+
// components/ulw-loop/src/steering-snapshot.ts
|
|
2203
2556
|
function buildSteeringPlanSnapshot(plan, changedGoalIds) {
|
|
2204
2557
|
const snapshot = {
|
|
2205
2558
|
updatedAt: plan.updatedAt,
|
|
@@ -2223,14 +2576,14 @@ function changedGoalIdsBetween(before, after2) {
|
|
|
2223
2576
|
return changed;
|
|
2224
2577
|
}
|
|
2225
2578
|
|
|
2226
|
-
//
|
|
2579
|
+
// components/ulw-loop/src/steering.ts
|
|
2227
2580
|
var SOURCES2 = ["user_prompt_submit", "finding", "cli"];
|
|
2228
2581
|
var PROTECTED = new Set(["aggregateCompletion", "codexObjective", "codexObjectiveAliases", "originalConstraints", "qualityGate", "status", "completedAt", "completionStatus"]);
|
|
2229
2582
|
var isObject2 = (value) => typeof value === "object" && value !== null;
|
|
2230
2583
|
var isPlain2 = (value) => isObject2(value) && !Array.isArray(value);
|
|
2231
2584
|
var read3 = (value, key) => Object.entries(value).find(([name]) => name === key)?.[1];
|
|
2232
2585
|
var isText2 = (value) => typeof value === "string" && value.trim().length > 0;
|
|
2233
|
-
var
|
|
2586
|
+
var text5 = (value, key) => {
|
|
2234
2587
|
const candidate = read3(value, key);
|
|
2235
2588
|
return isText2(candidate) ? candidate.trim() : undefined;
|
|
2236
2589
|
};
|
|
@@ -2243,19 +2596,19 @@ var texts = (value, key) => {
|
|
|
2243
2596
|
};
|
|
2244
2597
|
function targets2(proposal) {
|
|
2245
2598
|
const many = texts(proposal, "targetGoalIds");
|
|
2246
|
-
const one =
|
|
2599
|
+
const one = text5(proposal, "targetGoalId") ?? text5(proposal, "goalId");
|
|
2247
2600
|
return many.length > 0 ? many : one === undefined ? [] : [one];
|
|
2248
2601
|
}
|
|
2249
2602
|
var after2 = (proposal) => {
|
|
2250
2603
|
const candidate = read3(proposal, "after");
|
|
2251
2604
|
return isPlain2(candidate) ? candidate : undefined;
|
|
2252
2605
|
};
|
|
2253
|
-
var revised2 = (proposal, direct, nested) =>
|
|
2606
|
+
var revised2 = (proposal, direct, nested) => text5(proposal, direct) ?? text5(after2(proposal) ?? proposal, nested);
|
|
2254
2607
|
function child3(value) {
|
|
2255
2608
|
if (!isPlain2(value))
|
|
2256
2609
|
return null;
|
|
2257
|
-
const title =
|
|
2258
|
-
const objective =
|
|
2610
|
+
const title = text5(value, "title");
|
|
2611
|
+
const objective = text5(value, "objective");
|
|
2259
2612
|
if (title === undefined || objective === undefined)
|
|
2260
2613
|
return null;
|
|
2261
2614
|
return { title, objective };
|
|
@@ -2294,15 +2647,15 @@ function auditFor(proposal, reasons) {
|
|
|
2294
2647
|
const object = isPlain2(proposal) ? proposal : undefined;
|
|
2295
2648
|
const kindRaw = object === undefined ? undefined : read3(object, "kind");
|
|
2296
2649
|
const sourceRaw = object === undefined ? undefined : read3(object, "source");
|
|
2297
|
-
const evidence = object === undefined ? "" :
|
|
2298
|
-
const rationale = object === undefined ? "" :
|
|
2650
|
+
const evidence = object === undefined ? "" : text5(object, "evidence") ?? "";
|
|
2651
|
+
const rationale = object === undefined ? "" : text5(object, "rationale") ?? "";
|
|
2299
2652
|
const audit = { kind: isKind2(kindRaw) ? kindRaw : "annotate_ledger", source: isSource2(sourceRaw) ? sourceRaw : "cli", targetGoalIds: object === undefined ? [] : targets2(object), evidence, rationale, invariant: { accepted: reasons.length === 0, structuralInvariantAccepted: reasons.length === 0, evidenceBackedNecessity: evidence.length > 0 && rationale.length > 0, noEasierCompletion: !weakens(proposal), rejectedReasons: reasons, reasons } };
|
|
2300
2653
|
if (object === undefined)
|
|
2301
2654
|
return audit;
|
|
2302
|
-
const criterionId =
|
|
2303
|
-
const directiveText =
|
|
2304
|
-
const promptSignature =
|
|
2305
|
-
const idempotencyKey =
|
|
2655
|
+
const criterionId = text5(object, "criterionId");
|
|
2656
|
+
const directiveText = text5(object, "directiveText");
|
|
2657
|
+
const promptSignature = text5(object, "promptSignature");
|
|
2658
|
+
const idempotencyKey = text5(object, "idempotencyKey");
|
|
2306
2659
|
if (criterionId !== undefined)
|
|
2307
2660
|
audit.criterionId = criterionId;
|
|
2308
2661
|
if (directiveText !== undefined)
|
|
@@ -2323,9 +2676,9 @@ function validateUlwLoopSteeringProposal(plan, proposal) {
|
|
|
2323
2676
|
reasons.push(`invalid kind: ${String(kind)}`);
|
|
2324
2677
|
if (!isSource2(read3(object, "source")))
|
|
2325
2678
|
reasons.push(`invalid source: ${String(read3(object, "source"))}`);
|
|
2326
|
-
if (
|
|
2679
|
+
if (text5(object, "evidence") === undefined)
|
|
2327
2680
|
reasons.push("missing evidence");
|
|
2328
|
-
if (
|
|
2681
|
+
if (text5(object, "rationale") === undefined)
|
|
2329
2682
|
reasons.push("missing rationale");
|
|
2330
2683
|
if (hasProtected(proposal))
|
|
2331
2684
|
reasons.push("protected payload");
|
|
@@ -2342,7 +2695,7 @@ function goal2(plan, id) {
|
|
|
2342
2695
|
}
|
|
2343
2696
|
function validateKind(plan, proposal, kind, reasons) {
|
|
2344
2697
|
const target = goal2(plan, targets2(proposal)[0]);
|
|
2345
|
-
if (kind === "add_subgoal" && (
|
|
2698
|
+
if (kind === "add_subgoal" && (text5(proposal, "title") === undefined || text5(proposal, "objective") === undefined))
|
|
2346
2699
|
reasons.push("add_subgoal requires title/objective");
|
|
2347
2700
|
if ((kind === "split_subgoal" || kind === "revise_pending_wording" || kind === "mark_blocked_superseded") && target === undefined)
|
|
2348
2701
|
reasons.push(`${kind} requires target`);
|
|
@@ -2372,7 +2725,7 @@ function validateOrder(plan, proposal, reasons) {
|
|
|
2372
2725
|
}
|
|
2373
2726
|
function validateCriterion(plan, proposal, reasons) {
|
|
2374
2727
|
const target = goal2(plan, targets2(proposal)[0]);
|
|
2375
|
-
const criterionId =
|
|
2728
|
+
const criterionId = text5(proposal, "criterionId");
|
|
2376
2729
|
if (target === undefined)
|
|
2377
2730
|
reasons.push("revise_criterion requires goalId");
|
|
2378
2731
|
else if (criterionId === undefined || target.successCriteria.every((item) => item.id !== criterionId))
|
|
@@ -2407,8 +2760,8 @@ function applySteeringMutation(plan, proposal, audit) {
|
|
|
2407
2760
|
function isProposal(value) {
|
|
2408
2761
|
return isPlain2(value) && isKind2(read3(value, "kind")) && isSource2(read3(value, "source")) && isText2(read3(value, "evidence")) && isText2(read3(value, "rationale"));
|
|
2409
2762
|
}
|
|
2410
|
-
function parseUlwLoopSteeringDirective(
|
|
2411
|
-
const match = /(?:^|\s)(?:OMO_ULW_LOOP_STEER|omo\.ulw-loop\.steer|omo ulw-loop steer|omo-agent-toolkit ulw-loop steer):\s*([\s\S]+)$/u.exec(
|
|
2763
|
+
function parseUlwLoopSteeringDirective(text6) {
|
|
2764
|
+
const match = /(?:^|\s)(?:OMO_ULW_LOOP_STEER|omo\.ulw-loop\.steer|omo ulw-loop steer|omo-agent-toolkit ulw-loop steer):\s*([\s\S]+)$/u.exec(text6);
|
|
2412
2765
|
if (match?.[1] === undefined)
|
|
2413
2766
|
return null;
|
|
2414
2767
|
try {
|
|
@@ -2460,7 +2813,7 @@ function ledgerEntry(proposal, audit, at) {
|
|
|
2460
2813
|
return entry;
|
|
2461
2814
|
}
|
|
2462
2815
|
|
|
2463
|
-
//
|
|
2816
|
+
// components/ulw-loop/src/steering-batch.ts
|
|
2464
2817
|
async function steerUlwLoopBatch(repoRoot, proposals, scope) {
|
|
2465
2818
|
return withUlwLoopMutationLock(repoRoot, scope, async () => {
|
|
2466
2819
|
const plan = await readUlwLoopPlan(repoRoot, scope);
|
|
@@ -2541,7 +2894,7 @@ function ledgerEntry2(proposal, audit, at) {
|
|
|
2541
2894
|
return entry;
|
|
2542
2895
|
}
|
|
2543
2896
|
|
|
2544
|
-
//
|
|
2897
|
+
// components/ulw-loop/src/cli-subcommands.ts
|
|
2545
2898
|
async function createGoals(repoRoot, argv, json, scope) {
|
|
2546
2899
|
const briefFile = readValue(argv, "--brief-file");
|
|
2547
2900
|
const brief = readValue(argv, "--brief") ?? (briefFile === undefined ? undefined : await readFile6(briefFile, "utf8")) ?? (hasFlag(argv, "--from-stdin") ? await readStdin() : undefined) ?? positionalText(argv);
|
|
@@ -2575,7 +2928,8 @@ async function status(repoRoot, json, scope) {
|
|
|
2575
2928
|
ok: true,
|
|
2576
2929
|
plan,
|
|
2577
2930
|
summary: summarizeUlwLoopPlan(plan),
|
|
2578
|
-
...currentAttemptDir === undefined ? {} : { currentAttemptDir }
|
|
2931
|
+
...currentAttemptDir === undefined ? {} : { currentAttemptDir },
|
|
2932
|
+
nextActions: statusNextActions(plan)
|
|
2579
2933
|
});
|
|
2580
2934
|
} else
|
|
2581
2935
|
printStatus(plan);
|
|
@@ -2697,7 +3051,7 @@ function findGoal3(plan, goalId) {
|
|
|
2697
3051
|
throw new UlwLoopError(`Unknown ulw-loop id: ${goalId}.`, "ULW_LOOP_GOAL_NOT_FOUND", { details: { goalId } });
|
|
2698
3052
|
}
|
|
2699
3053
|
|
|
2700
|
-
//
|
|
3054
|
+
// components/ulw-loop/src/cli-commands.ts
|
|
2701
3055
|
var ULW_LOOP_SUBCOMMANDS = [
|
|
2702
3056
|
"help",
|
|
2703
3057
|
"create-goals",
|
|
@@ -2791,7 +3145,7 @@ function commandScope(argv) {
|
|
|
2791
3145
|
if (sessionIdFlagPresent(argv)) {
|
|
2792
3146
|
const sessionId2 = readValue(argv, SESSION_ID_FLAG)?.trim();
|
|
2793
3147
|
if (!sessionId2) {
|
|
2794
|
-
throw new UlwLoopError(
|
|
3148
|
+
throw new UlwLoopError(sessionIdRequiredMessage(SESSION_ID_FLAG), "ULW_LOOP_SESSION_ID_REQUIRED", {
|
|
2795
3149
|
details: { flag: SESSION_ID_FLAG }
|
|
2796
3150
|
});
|
|
2797
3151
|
}
|
|
@@ -2801,12 +3155,12 @@ function commandScope(argv) {
|
|
|
2801
3155
|
return sessionId === null ? undefined : { sessionId };
|
|
2802
3156
|
}
|
|
2803
3157
|
|
|
2804
|
-
//
|
|
2805
|
-
import { readFileSync as
|
|
3158
|
+
// components/ulw-loop/src/ultrawork-directive.ts
|
|
3159
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
2806
3160
|
|
|
2807
|
-
//
|
|
2808
|
-
import { existsSync as
|
|
2809
|
-
import { fileURLToPath } from "node:url";
|
|
3161
|
+
// components/ulw-loop/src/ultrawork-skill-pointer.ts
|
|
3162
|
+
import { existsSync as existsSync6, readFileSync as readFileSync2 } from "node:fs";
|
|
3163
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
2810
3164
|
var ULTRAWORK_SKILL_POINTER_TEMPLATE = `<ultrawork-mode>
|
|
2811
3165
|
ULTRAWORK MODE IS ACTIVE FOR THIS TASK.
|
|
2812
3166
|
|
|
@@ -2837,22 +3191,22 @@ Do not start the requested work until all three steps are complete.
|
|
|
2837
3191
|
`;
|
|
2838
3192
|
var ULTRAWORK_SKILL_PATH_PLACEHOLDER = "{{ULTRAWORK_SKILL_PATH}}";
|
|
2839
3193
|
var ULTRAWORK_SKILL_FILE_URL = new URL("../../../skills/ultrawork/SKILL.md", import.meta.url);
|
|
2840
|
-
var ULTRAWORK_DIRECTIVE =
|
|
3194
|
+
var ULTRAWORK_DIRECTIVE = readFileSync2(new URL("../directive.md", import.meta.url), "utf8");
|
|
2841
3195
|
function resolveUltraworkSkillFilePath() {
|
|
2842
|
-
return
|
|
3196
|
+
return fileURLToPath2(ULTRAWORK_SKILL_FILE_URL);
|
|
2843
3197
|
}
|
|
2844
3198
|
function buildUltraworkSkillPointer(skillFilePath) {
|
|
2845
3199
|
return ULTRAWORK_SKILL_POINTER_TEMPLATE.replace(ULTRAWORK_SKILL_PATH_PLACEHOLDER, skillFilePath);
|
|
2846
3200
|
}
|
|
2847
3201
|
function buildUltraworkAdditionalContext(options = {}) {
|
|
2848
3202
|
const skillFilePath = options.skillFilePath === undefined ? resolveUltraworkSkillFilePath() : options.skillFilePath;
|
|
2849
|
-
if (skillFilePath !== null &&
|
|
3203
|
+
if (skillFilePath !== null && existsSync6(skillFilePath)) {
|
|
2850
3204
|
return buildUltraworkSkillPointer(skillFilePath);
|
|
2851
3205
|
}
|
|
2852
3206
|
return ULTRAWORK_DIRECTIVE;
|
|
2853
3207
|
}
|
|
2854
3208
|
|
|
2855
|
-
//
|
|
3209
|
+
// components/ulw-loop/src/ultrawork-directive.ts
|
|
2856
3210
|
var ULTRAWORK_CURRENT_PROMPT_PATTERN = /(?:ultrawork|ulw)/i;
|
|
2857
3211
|
var ULTRAWORK_DIRECTIVE_MARKER = "<ultrawork-mode>";
|
|
2858
3212
|
var TRANSCRIPT_SEARCH_BYTES = 512000;
|
|
@@ -2900,7 +3254,7 @@ function hasUltraworkDirectiveAlreadyInTranscript(transcriptPath) {
|
|
|
2900
3254
|
return false;
|
|
2901
3255
|
}
|
|
2902
3256
|
function readTranscriptTail(transcriptPath) {
|
|
2903
|
-
const rawTranscript =
|
|
3257
|
+
const rawTranscript = readFileSync3(transcriptPath);
|
|
2904
3258
|
return rawTranscript.subarray(Math.max(0, rawTranscript.byteLength - TRANSCRIPT_SEARCH_BYTES)).toString("utf8");
|
|
2905
3259
|
}
|
|
2906
3260
|
function isUltraworkPrompt(prompt) {
|
|
@@ -2955,7 +3309,7 @@ function isRecord3(value) {
|
|
|
2955
3309
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2956
3310
|
}
|
|
2957
3311
|
|
|
2958
|
-
//
|
|
3312
|
+
// components/ulw-loop/src/codex-hook.ts
|
|
2959
3313
|
var CREATE_GOAL_TOOL_NAME = "create_goal";
|
|
2960
3314
|
var CREATE_GOAL_PAYLOAD_WARNING = "Use create_goal with objective only. Omit token_budget so the goal stays unlimited, and put lifecycle status changes on update_goal.";
|
|
2961
3315
|
function parseUserPromptSubmitPayload(raw) {
|
|
@@ -3081,36 +3435,60 @@ function optionalString(value) {
|
|
|
3081
3435
|
return value === undefined || typeof value === "string";
|
|
3082
3436
|
}
|
|
3083
3437
|
function readAll(stdin) {
|
|
3084
|
-
return new Promise((
|
|
3438
|
+
return new Promise((resolve5, reject) => {
|
|
3085
3439
|
let data = "";
|
|
3086
3440
|
stdin.setEncoding("utf8");
|
|
3087
3441
|
stdin.on("data", (chunk) => {
|
|
3088
3442
|
data += chunk instanceof Buffer ? chunk.toString() : String(chunk);
|
|
3089
3443
|
});
|
|
3090
3444
|
stdin.once("error", reject);
|
|
3091
|
-
stdin.once("end", () =>
|
|
3445
|
+
stdin.once("end", () => resolve5(data));
|
|
3092
3446
|
});
|
|
3093
3447
|
}
|
|
3094
3448
|
|
|
3095
|
-
//
|
|
3096
|
-
import {
|
|
3097
|
-
import {
|
|
3449
|
+
// components/ulw-loop/src/spawn-guard.ts
|
|
3450
|
+
import { randomBytes } from "node:crypto";
|
|
3451
|
+
import {
|
|
3452
|
+
existsSync as existsSync7,
|
|
3453
|
+
mkdirSync,
|
|
3454
|
+
openSync,
|
|
3455
|
+
readdirSync as readdirSync2,
|
|
3456
|
+
readFileSync as readFileSync4,
|
|
3457
|
+
renameSync,
|
|
3458
|
+
statSync as statSync2,
|
|
3459
|
+
unlinkSync,
|
|
3460
|
+
writeFileSync
|
|
3461
|
+
} from "node:fs";
|
|
3462
|
+
import { dirname as dirname2, join as join3 } from "node:path";
|
|
3098
3463
|
var SPAWN_TOOL_TOKENS = new Set(["spawn_agent", "collaborationspawn_agent", "collaboration.spawn_agent"]);
|
|
3099
3464
|
var DEFAULT_FANOUT_LIMIT = 60;
|
|
3100
|
-
var
|
|
3465
|
+
var DEFAULT_REVIEW_SPAWN_LIMIT = 3;
|
|
3466
|
+
var GATE_MESSAGE_PATTERN = /lazycodex-gate-reviewer|omo-senpi-gate-reviewer|final gate review/i;
|
|
3467
|
+
var REVIEW_AGENT_TYPES = [
|
|
3468
|
+
...Object.values(REVIEWER_ROLES_BY_SURFACE).map((roles) => roles.gateReview),
|
|
3469
|
+
...Object.values(REVIEWER_ROLES_BY_SURFACE).map((roles) => roles.codeReview),
|
|
3470
|
+
...Object.values(REVIEWER_ROLES_BY_SURFACE).map((roles) => roles.manualQa)
|
|
3471
|
+
];
|
|
3472
|
+
var REVIEW_AGENT_TYPE_SET = new Set(REVIEW_AGENT_TYPES);
|
|
3101
3473
|
function applySpawnGuards(payload) {
|
|
3102
3474
|
if (payload.hook_event_name !== "PreToolUse" || !SPAWN_TOOL_TOKENS.has(payload.tool_name))
|
|
3103
3475
|
return "";
|
|
3104
3476
|
const stateDir = ulwLoopDir(payload.cwd, { sessionId: payload.session_id });
|
|
3105
|
-
const plan = readPlan(
|
|
3477
|
+
const plan = readPlan(join3(stateDir, "goals.json"));
|
|
3106
3478
|
if (plan === null)
|
|
3107
3479
|
return "";
|
|
3108
|
-
const
|
|
3109
|
-
if (
|
|
3110
|
-
return deny(
|
|
3480
|
+
const fanOutPeek = peekFanOutBudget(stateDir);
|
|
3481
|
+
if (fanOutPeek !== null)
|
|
3482
|
+
return deny(fanOutPeek);
|
|
3111
3483
|
const missingArtifact = missingGateArtifact(payload, plan);
|
|
3112
3484
|
if (missingArtifact !== null)
|
|
3113
3485
|
return deny(`spawn code-review + QA first; gate audits their artifacts: missing ${missingArtifact}`);
|
|
3486
|
+
const reviewDenial = consumeReviewSpawnBudget(payload, plan, stateDir);
|
|
3487
|
+
if (reviewDenial !== null)
|
|
3488
|
+
return deny(reviewDenial);
|
|
3489
|
+
const fanOutDenial = consumeFanOutBudget(stateDir);
|
|
3490
|
+
if (fanOutDenial !== null)
|
|
3491
|
+
return deny(fanOutDenial);
|
|
3114
3492
|
return "";
|
|
3115
3493
|
}
|
|
3116
3494
|
async function runSpawnGuardCli(stdin, stdout) {
|
|
@@ -3129,15 +3507,44 @@ async function runSpawnGuardCli(stdin, stdout) {
|
|
|
3129
3507
|
return;
|
|
3130
3508
|
}
|
|
3131
3509
|
}
|
|
3510
|
+
function peekFanOutBudget(stateDir) {
|
|
3511
|
+
const counterPath = join3(stateDir, "spawn-count.json");
|
|
3512
|
+
const count = readCount(counterPath) + 1;
|
|
3513
|
+
const limit = fanOutLimit();
|
|
3514
|
+
if (count <= limit)
|
|
3515
|
+
return null;
|
|
3516
|
+
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.`;
|
|
3517
|
+
}
|
|
3132
3518
|
function consumeFanOutBudget(stateDir) {
|
|
3133
|
-
const counterPath =
|
|
3519
|
+
const counterPath = join3(stateDir, "spawn-count.json");
|
|
3134
3520
|
const count = readCount(counterPath) + 1;
|
|
3135
|
-
|
|
3521
|
+
atomicWriteJson(counterPath, { count });
|
|
3136
3522
|
const limit = fanOutLimit();
|
|
3137
3523
|
if (count <= limit)
|
|
3138
3524
|
return null;
|
|
3139
3525
|
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.`;
|
|
3140
3526
|
}
|
|
3527
|
+
function consumeReviewSpawnBudget(payload, plan, stateDir) {
|
|
3528
|
+
const agentType = reviewAgentType(payload.tool_input);
|
|
3529
|
+
if (agentType === null)
|
|
3530
|
+
return null;
|
|
3531
|
+
const goal3 = plan.goals.find((candidate) => candidate.id === plan.activeGoalId) ?? plan.goals.find((candidate) => isFinalRunCompletionCandidate(plan, candidate));
|
|
3532
|
+
if (goal3 === undefined)
|
|
3533
|
+
return null;
|
|
3534
|
+
const counterPath = join3(stateDir, "review-spawn-counts.json");
|
|
3535
|
+
const lockPath = `${counterPath}.lock`;
|
|
3536
|
+
const limit = reviewSpawnLimit();
|
|
3537
|
+
return withExclusiveLock(lockPath, () => {
|
|
3538
|
+
const counts = readCounts(counterPath);
|
|
3539
|
+
const key = `${agentType}:${goal3.id}:a${goal3.attempt}`;
|
|
3540
|
+
const count = (counts[key] ?? 0) + 1;
|
|
3541
|
+
if (count > limit)
|
|
3542
|
+
return `ulw-loop reviewer no-progress cap reached (${agentType} ${count}/${limit}) for ${goal3.id} attempt ${goal3.attempt}. Consolidate existing review findings, or checkpoint and start a new attempt after concrete progress.`;
|
|
3543
|
+
counts[key] = count;
|
|
3544
|
+
atomicWriteJson(counterPath, counts);
|
|
3545
|
+
return null;
|
|
3546
|
+
});
|
|
3547
|
+
}
|
|
3141
3548
|
function missingGateArtifact(payload, plan) {
|
|
3142
3549
|
if (!isGateReviewerSpawn(payload.tool_input))
|
|
3143
3550
|
return null;
|
|
@@ -3147,31 +3554,109 @@ function missingGateArtifact(payload, plan) {
|
|
|
3147
3554
|
if (!goal3.successCriteria.every((criterion) => criterion.status === "pass"))
|
|
3148
3555
|
return null;
|
|
3149
3556
|
const scope = { sessionId: payload.session_id };
|
|
3557
|
+
const surface = resolveToolkitSurface();
|
|
3558
|
+
const requiredArtifacts = surface === "omo-senpi" ? [`${goal3.id}-manual-qa.md`] : [`${goal3.id}-code-review.md`, `${goal3.id}-manual-qa.md`];
|
|
3150
3559
|
if (plan.evidenceLayoutVersion === 2) {
|
|
3151
3560
|
const attemptDir = ulwLoopAttemptEvidenceDir(goal3.id, goal3.attempt, scope);
|
|
3152
|
-
for (const name of
|
|
3561
|
+
for (const name of requiredArtifacts) {
|
|
3153
3562
|
const relative2 = `${attemptDir}/${name}`;
|
|
3154
|
-
if (!isNonEmptyFile(
|
|
3563
|
+
if (!isNonEmptyFile(join3(payload.cwd, relative2)))
|
|
3155
3564
|
return relative2;
|
|
3156
3565
|
}
|
|
3157
3566
|
return null;
|
|
3158
3567
|
}
|
|
3159
3568
|
const flatReport = `.omo/evidence/${goal3.id}-code-review.md`;
|
|
3160
|
-
if (!isNonEmptyFile(
|
|
3569
|
+
if (surface !== "omo-senpi" && !isNonEmptyFile(join3(payload.cwd, flatReport)))
|
|
3161
3570
|
return flatReport;
|
|
3162
|
-
if (
|
|
3163
|
-
|
|
3571
|
+
if (surface === "omo-senpi") {
|
|
3572
|
+
const manualQa = `.omo/evidence/${goal3.id}-manual-qa.md`;
|
|
3573
|
+
return isNonEmptyFile(join3(payload.cwd, manualQa)) ? null : manualQa;
|
|
3574
|
+
}
|
|
3575
|
+
if (!hasOtherEvidenceFile(join3(payload.cwd, ".omo", "evidence"), `${goal3.id}-code-review.md`))
|
|
3576
|
+
return `.omo/evidence/${goal3.id}-manual-qa.md`;
|
|
3164
3577
|
return null;
|
|
3165
3578
|
}
|
|
3166
3579
|
function isGateReviewerSpawn(toolInput) {
|
|
3580
|
+
const agentType = reviewAgentType(toolInput);
|
|
3581
|
+
return agentType !== null && GATE_REVIEWER_AGENT_NAMES.has(agentType);
|
|
3582
|
+
}
|
|
3583
|
+
function reviewAgentType(toolInput) {
|
|
3167
3584
|
if (typeof toolInput !== "object" || toolInput === null)
|
|
3168
|
-
return
|
|
3585
|
+
return null;
|
|
3169
3586
|
const record = toolInput;
|
|
3170
3587
|
const agentType = record["agent_type"];
|
|
3171
|
-
if (typeof agentType === "string")
|
|
3172
|
-
|
|
3588
|
+
if (typeof agentType === "string") {
|
|
3589
|
+
if (!REVIEW_AGENT_TYPE_SET.has(agentType))
|
|
3590
|
+
return null;
|
|
3591
|
+
return activeSurfaceReviewerAlias(agentType);
|
|
3592
|
+
}
|
|
3173
3593
|
const message = record["message"];
|
|
3174
|
-
|
|
3594
|
+
if (typeof message !== "string")
|
|
3595
|
+
return null;
|
|
3596
|
+
const normalizedMessage = message.toLowerCase();
|
|
3597
|
+
const allRoleNames = [...REVIEW_AGENT_TYPES];
|
|
3598
|
+
const explicitAssignment = allRoleNames.map((name) => ({
|
|
3599
|
+
name,
|
|
3600
|
+
index: normalizedMessage.search(new RegExp(`\\bact as (?:an? )?${name}\\b`))
|
|
3601
|
+
})).filter(({ index }) => index >= 0).sort((left, right) => left.index - right.index)[0];
|
|
3602
|
+
if (explicitAssignment !== undefined)
|
|
3603
|
+
return activeSurfaceReviewerAlias(explicitAssignment.name);
|
|
3604
|
+
const nonReviewerActAs = /\bact as (?:an? )?\S+/.test(normalizedMessage);
|
|
3605
|
+
if (nonReviewerActAs)
|
|
3606
|
+
return null;
|
|
3607
|
+
const namedReviewer = REVIEW_AGENT_TYPES.find((name) => normalizedMessage.includes(name));
|
|
3608
|
+
if (namedReviewer !== undefined)
|
|
3609
|
+
return activeSurfaceReviewerAlias(namedReviewer);
|
|
3610
|
+
return GATE_MESSAGE_PATTERN.test(message) ? reviewerRolesFor(resolveToolkitSurface()).gateReview : null;
|
|
3611
|
+
}
|
|
3612
|
+
function activeSurfaceReviewerAlias(reviewer) {
|
|
3613
|
+
const activeRoles = reviewerRolesFor(resolveToolkitSurface());
|
|
3614
|
+
for (const roles of Object.values(REVIEWER_ROLES_BY_SURFACE)) {
|
|
3615
|
+
if (reviewer === roles.codeReview)
|
|
3616
|
+
return activeRoles.codeReview;
|
|
3617
|
+
if (reviewer === roles.manualQa)
|
|
3618
|
+
return activeRoles.manualQa;
|
|
3619
|
+
if (reviewer === roles.gateReview)
|
|
3620
|
+
return activeRoles.gateReview;
|
|
3621
|
+
}
|
|
3622
|
+
return reviewer;
|
|
3623
|
+
}
|
|
3624
|
+
function withExclusiveLock(lockPath, fn) {
|
|
3625
|
+
mkdirSync(dirname2(lockPath), { recursive: true });
|
|
3626
|
+
const maxAttempts = 10;
|
|
3627
|
+
const baseDelayMs = 10;
|
|
3628
|
+
for (let attempt = 0;attempt < maxAttempts; attempt++) {
|
|
3629
|
+
let fd = null;
|
|
3630
|
+
try {
|
|
3631
|
+
fd = openSync(lockPath, "wx");
|
|
3632
|
+
writeFileSync(fd, process.pid.toString());
|
|
3633
|
+
try {
|
|
3634
|
+
return fn();
|
|
3635
|
+
} finally {
|
|
3636
|
+
try {
|
|
3637
|
+
unlinkSync(lockPath);
|
|
3638
|
+
} catch {}
|
|
3639
|
+
}
|
|
3640
|
+
} catch (error) {
|
|
3641
|
+
if (fd !== null) {
|
|
3642
|
+
try {
|
|
3643
|
+
unlinkSync(lockPath);
|
|
3644
|
+
} catch {}
|
|
3645
|
+
}
|
|
3646
|
+
if (error.code !== "EEXIST") {
|
|
3647
|
+
return fn();
|
|
3648
|
+
}
|
|
3649
|
+
const delayMs = baseDelayMs * 2 ** attempt + Math.random() * baseDelayMs;
|
|
3650
|
+
const deadline = Date.now() + delayMs;
|
|
3651
|
+
while (Date.now() < deadline) {}
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3654
|
+
return fn();
|
|
3655
|
+
}
|
|
3656
|
+
function atomicWriteJson(targetPath, data) {
|
|
3657
|
+
const tmp = join3(dirname2(targetPath), `.tmp-${randomBytes(6).toString("hex")}`);
|
|
3658
|
+
writeFileSync(tmp, JSON.stringify(data));
|
|
3659
|
+
renameSync(tmp, targetPath);
|
|
3175
3660
|
}
|
|
3176
3661
|
function deny(reason) {
|
|
3177
3662
|
return `${JSON.stringify({
|
|
@@ -3191,9 +3676,16 @@ function fanOutLimit() {
|
|
|
3191
3676
|
const parsed = Number.parseInt(raw, 10);
|
|
3192
3677
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_FANOUT_LIMIT;
|
|
3193
3678
|
}
|
|
3679
|
+
function reviewSpawnLimit() {
|
|
3680
|
+
const raw = process.env["OMO_ULW_LOOP_REVIEW_SPAWN_LIMIT"];
|
|
3681
|
+
if (raw === undefined)
|
|
3682
|
+
return DEFAULT_REVIEW_SPAWN_LIMIT;
|
|
3683
|
+
const parsed = Number.parseInt(raw, 10);
|
|
3684
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_REVIEW_SPAWN_LIMIT;
|
|
3685
|
+
}
|
|
3194
3686
|
function isNonEmptyFile(path) {
|
|
3195
3687
|
try {
|
|
3196
|
-
return
|
|
3688
|
+
return existsSync7(path) && statSync2(path).size > 0;
|
|
3197
3689
|
} catch (error) {
|
|
3198
3690
|
if (error instanceof Error)
|
|
3199
3691
|
return false;
|
|
@@ -3202,7 +3694,7 @@ function isNonEmptyFile(path) {
|
|
|
3202
3694
|
}
|
|
3203
3695
|
function hasOtherEvidenceFile(evidenceDir, excludedName) {
|
|
3204
3696
|
try {
|
|
3205
|
-
return
|
|
3697
|
+
return readdirSync2(evidenceDir).some((name) => name !== excludedName && isNonEmptyFile(join3(evidenceDir, name)));
|
|
3206
3698
|
} catch (error) {
|
|
3207
3699
|
if (error instanceof Error)
|
|
3208
3700
|
return false;
|
|
@@ -3211,7 +3703,7 @@ function hasOtherEvidenceFile(evidenceDir, excludedName) {
|
|
|
3211
3703
|
}
|
|
3212
3704
|
function readCount(counterPath) {
|
|
3213
3705
|
try {
|
|
3214
|
-
const parsed = JSON.parse(
|
|
3706
|
+
const parsed = JSON.parse(readFileSync4(counterPath, "utf8"));
|
|
3215
3707
|
return typeof parsed["count"] === "number" && parsed["count"] >= 0 ? parsed["count"] : 0;
|
|
3216
3708
|
} catch (error) {
|
|
3217
3709
|
if (error instanceof Error)
|
|
@@ -3219,9 +3711,26 @@ function readCount(counterPath) {
|
|
|
3219
3711
|
throw error;
|
|
3220
3712
|
}
|
|
3221
3713
|
}
|
|
3714
|
+
function readCounts(counterPath) {
|
|
3715
|
+
try {
|
|
3716
|
+
const parsed = JSON.parse(readFileSync4(counterPath, "utf8"));
|
|
3717
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
3718
|
+
return {};
|
|
3719
|
+
const counts = {};
|
|
3720
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
3721
|
+
if (typeof value === "number" && value >= 0)
|
|
3722
|
+
counts[key] = value;
|
|
3723
|
+
}
|
|
3724
|
+
return counts;
|
|
3725
|
+
} catch (error) {
|
|
3726
|
+
if (error instanceof Error)
|
|
3727
|
+
return {};
|
|
3728
|
+
throw error;
|
|
3729
|
+
}
|
|
3730
|
+
}
|
|
3222
3731
|
function readPlan(goalsPath) {
|
|
3223
3732
|
try {
|
|
3224
|
-
return JSON.parse(
|
|
3733
|
+
return JSON.parse(readFileSync4(goalsPath, "utf8"));
|
|
3225
3734
|
} catch (error) {
|
|
3226
3735
|
if (error instanceof Error)
|
|
3227
3736
|
return null;
|
|
@@ -3229,9 +3738,9 @@ function readPlan(goalsPath) {
|
|
|
3229
3738
|
}
|
|
3230
3739
|
}
|
|
3231
3740
|
|
|
3232
|
-
//
|
|
3233
|
-
import { existsSync as
|
|
3234
|
-
import { isAbsolute as isAbsolute2, join as
|
|
3741
|
+
// components/ulw-loop/src/stop-resume-hook.ts
|
|
3742
|
+
import { existsSync as existsSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "node:fs";
|
|
3743
|
+
import { isAbsolute as isAbsolute2, join as join4, resolve as resolve5, sep as sep2 } from "node:path";
|
|
3235
3744
|
var RESUME_CAP = 2;
|
|
3236
3745
|
var CONTEXT_PRESSURE_MARKERS2 = [
|
|
3237
3746
|
"context compacted",
|
|
@@ -3251,7 +3760,7 @@ function runStopResumeHook(input) {
|
|
|
3251
3760
|
if (boulderContinuationWillFire(payload.cwd, payload.session_id))
|
|
3252
3761
|
return "";
|
|
3253
3762
|
const stateDir = ulwLoopDir(payload.cwd, { sessionId: payload.session_id });
|
|
3254
|
-
const plan = readPlan2(
|
|
3763
|
+
const plan = readPlan2(join4(stateDir, "goals.json"));
|
|
3255
3764
|
if (plan === null || plan.aggregateCompletion?.status === "complete")
|
|
3256
3765
|
return "";
|
|
3257
3766
|
const goal3 = resumableGoal(plan);
|
|
@@ -3288,9 +3797,9 @@ function isResumableStatus(status2) {
|
|
|
3288
3797
|
return status2 === "pending" || status2 === "in_progress";
|
|
3289
3798
|
}
|
|
3290
3799
|
function consumeResumeBudget(stateDir, goalId) {
|
|
3291
|
-
const ledgerLineCount = countLedgerLines(
|
|
3292
|
-
const counterPath =
|
|
3293
|
-
const stuckPath =
|
|
3800
|
+
const ledgerLineCount = countLedgerLines(join4(stateDir, "ledger.jsonl"));
|
|
3801
|
+
const counterPath = resolve5(stateDir, `auto-resume-${goalId}.json`);
|
|
3802
|
+
const stuckPath = resolve5(stateDir, `auto-resume-${goalId}.stuck`);
|
|
3294
3803
|
if (!isInsideDir(stateDir, counterPath) || !isInsideDir(stateDir, stuckPath))
|
|
3295
3804
|
return false;
|
|
3296
3805
|
const previous = readCounter(counterPath);
|
|
@@ -3304,7 +3813,7 @@ function consumeResumeBudget(stateDir, goalId) {
|
|
|
3304
3813
|
return true;
|
|
3305
3814
|
}
|
|
3306
3815
|
function isInsideDir(dir, candidate) {
|
|
3307
|
-
return candidate.startsWith(
|
|
3816
|
+
return candidate.startsWith(resolve5(dir) + sep2);
|
|
3308
3817
|
}
|
|
3309
3818
|
function renderResumeDirective(plan, goal3, sessionId) {
|
|
3310
3819
|
const normalized = normalizeUlwLoopSessionId(sessionId);
|
|
@@ -3321,7 +3830,7 @@ function renderResumeDirective(plan, goal3, sessionId) {
|
|
|
3321
3830
|
}
|
|
3322
3831
|
function readPlan2(goalsPath) {
|
|
3323
3832
|
try {
|
|
3324
|
-
return JSON.parse(
|
|
3833
|
+
return JSON.parse(readFileSync5(goalsPath, "utf8"));
|
|
3325
3834
|
} catch (error) {
|
|
3326
3835
|
if (error instanceof Error)
|
|
3327
3836
|
return null;
|
|
@@ -3330,7 +3839,7 @@ function readPlan2(goalsPath) {
|
|
|
3330
3839
|
}
|
|
3331
3840
|
function countLedgerLines(ledgerPath) {
|
|
3332
3841
|
try {
|
|
3333
|
-
return
|
|
3842
|
+
return readFileSync5(ledgerPath, "utf8").split(`
|
|
3334
3843
|
`).filter(Boolean).length;
|
|
3335
3844
|
} catch (error) {
|
|
3336
3845
|
if (error instanceof Error)
|
|
@@ -3340,9 +3849,9 @@ function countLedgerLines(ledgerPath) {
|
|
|
3340
3849
|
}
|
|
3341
3850
|
function readCounter(counterPath) {
|
|
3342
3851
|
try {
|
|
3343
|
-
if (!
|
|
3852
|
+
if (!existsSync8(counterPath))
|
|
3344
3853
|
return null;
|
|
3345
|
-
const parsed = JSON.parse(
|
|
3854
|
+
const parsed = JSON.parse(readFileSync5(counterPath, "utf8"));
|
|
3346
3855
|
if (typeof parsed["count"] !== "number" || typeof parsed["ledgerLineCount"] !== "number")
|
|
3347
3856
|
return null;
|
|
3348
3857
|
return { count: parsed["count"], ledgerLineCount: parsed["ledgerLineCount"] };
|
|
@@ -3354,7 +3863,7 @@ function readCounter(counterPath) {
|
|
|
3354
3863
|
}
|
|
3355
3864
|
function boulderContinuationWillFire(cwd, sessionId) {
|
|
3356
3865
|
try {
|
|
3357
|
-
const raw = JSON.parse(
|
|
3866
|
+
const raw = JSON.parse(readFileSync5(join4(cwd, ".omo", "boulder.json"), "utf8"));
|
|
3358
3867
|
const works = raw["works"];
|
|
3359
3868
|
const entries = typeof works === "object" && works !== null ? Object.values(works) : [raw];
|
|
3360
3869
|
return entries.some((work) => {
|
|
@@ -3373,7 +3882,7 @@ function boulderContinuationWillFire(cwd, sessionId) {
|
|
|
3373
3882
|
}
|
|
3374
3883
|
function transcriptShowsContextPressure(transcriptPath) {
|
|
3375
3884
|
try {
|
|
3376
|
-
const transcript =
|
|
3885
|
+
const transcript = readFileSync5(transcriptPath, "utf8").toLowerCase();
|
|
3377
3886
|
return CONTEXT_PRESSURE_MARKERS2.some((marker) => transcript.includes(marker));
|
|
3378
3887
|
} catch (error) {
|
|
3379
3888
|
if (error instanceof Error)
|
|
@@ -3385,12 +3894,12 @@ function boulderPlanHasChecklist(cwd, entry) {
|
|
|
3385
3894
|
const activePlan = entry["active_plan"];
|
|
3386
3895
|
if (typeof activePlan !== "string" || activePlan.trim().length === 0)
|
|
3387
3896
|
return false;
|
|
3388
|
-
const planPath = isAbsolute2(activePlan) ? activePlan :
|
|
3897
|
+
const planPath = isAbsolute2(activePlan) ? activePlan : join4(cwd, activePlan);
|
|
3389
3898
|
const worktree = entry["worktree_path"];
|
|
3390
|
-
const candidates = typeof worktree === "string" && worktree.trim().length > 0 && !isAbsolute2(activePlan) ? [
|
|
3899
|
+
const candidates = typeof worktree === "string" && worktree.trim().length > 0 && !isAbsolute2(activePlan) ? [join4(isAbsolute2(worktree) ? worktree : join4(cwd, worktree), activePlan), planPath] : [planPath];
|
|
3391
3900
|
for (const candidate of candidates) {
|
|
3392
3901
|
try {
|
|
3393
|
-
return
|
|
3902
|
+
return readFileSync5(candidate, "utf8").split(/\r?\n/).some((line) => line.startsWith("- [ ] ") || line.startsWith("- [x] ") || line.startsWith("- [X] "));
|
|
3394
3903
|
} catch (error) {
|
|
3395
3904
|
if (!(error instanceof Error))
|
|
3396
3905
|
throw error;
|
|
@@ -3414,7 +3923,7 @@ function parseStopPayload(value) {
|
|
|
3414
3923
|
};
|
|
3415
3924
|
}
|
|
3416
3925
|
|
|
3417
|
-
//
|
|
3926
|
+
// components/ulw-loop/src/cli.ts
|
|
3418
3927
|
async function main() {
|
|
3419
3928
|
const argv = process.argv.slice(2);
|
|
3420
3929
|
const command = argv[0];
|