oh-my-opencode 5.0.0-beta.31 → 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 +136 -44
- package/dist/cli-node/index.js +136 -44
- 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/monitor/manager-internals.d.ts +1 -1
- package/dist/features/monitor/output-injector-types.d.ts +1 -1
- 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/tool-result-storage.d.ts +11 -0
- package/dist/index.js +286 -99
- package/dist/skills/ultimate-browsing/references/insane-search/README.md +9 -2
- package/dist/tools/session-manager/directory-filter.d.ts +2 -0
- package/dist/tui.js +109 -23
- package/package.json +16 -15
- 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.ts +6 -3
- package/packages/lsp-core/src/lsp/controlled-clock-test-support.ts +61 -0
- package/packages/lsp-core/src/lsp/fixtures/workspace-edit-server.mjs +10 -3
- 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/tools/parameters.test.ts +51 -0
- package/packages/lsp-daemon/dist/cli.js +41 -16
- package/packages/lsp-daemon/dist/client.js +41 -16
- package/packages/lsp-daemon/dist/index.js +41 -16
- package/packages/lsp-tools-mcp/dist/cli.js +41 -16
- package/packages/lsp-tools-mcp/dist/lsp/manager.js +41 -16
- package/packages/lsp-tools-mcp/dist/mcp.js +41 -16
- package/packages/lsp-tools-mcp/dist/tools.js +41 -16
- 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 +3 -17
- package/packages/omo-codex/plugin/components/codegraph/dist/serve.js +3 -17
- package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
- package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
- package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +3 -3
- package/packages/omo-codex/plugin/components/lsp/dist/cli.js +39 -16
- 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 +4 -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 +7 -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 +775 -312
- package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-instruction.js +18 -0
- 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.js +41 -99
- 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 +184 -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 +3 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/surface.js +15 -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 +8 -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 +18 -0
- 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 +50 -116
- 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 +186 -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 +22 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint-final.test.ts +66 -3
- 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 +13 -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 +16 -15
- 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 +300 -3
- 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/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/packages/omo-codex/plugin/components/ultrawork/directive.md +0 -483
- package/packages/omo-codex/plugin/components/ultrawork/skills/ultrawork/SKILL.md +0 -490
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
steer,
|
|
13
13
|
} from "./cli-subcommands.js";
|
|
14
14
|
import { resolveUlwLoopSessionIdFromEnv, type UlwLoopScope } from "./paths.js";
|
|
15
|
+
import { sessionIdRequiredMessage } from "./plan-missing-recovery.js";
|
|
15
16
|
import { UlwLoopError } from "./types.js";
|
|
16
17
|
|
|
17
18
|
export const ULW_LOOP_SUBCOMMANDS = [
|
|
@@ -108,7 +109,7 @@ function commandScope(argv: readonly string[]): UlwLoopScope | undefined {
|
|
|
108
109
|
if (sessionIdFlagPresent(argv)) {
|
|
109
110
|
const sessionId = readValue(argv, SESSION_ID_FLAG)?.trim();
|
|
110
111
|
if (!sessionId) {
|
|
111
|
-
throw new UlwLoopError(
|
|
112
|
+
throw new UlwLoopError(sessionIdRequiredMessage(SESSION_ID_FLAG), "ULW_LOOP_SESSION_ID_REQUIRED", {
|
|
112
113
|
details: { flag: SESSION_ID_FLAG },
|
|
113
114
|
});
|
|
114
115
|
}
|
|
@@ -10,6 +10,7 @@ export const ULW_LOOP_HELP = `Usage:
|
|
|
10
10
|
omo-agent-toolkit ulw-loop complete-goals [--retry-failed] [--json]
|
|
11
11
|
omo-agent-toolkit ulw-loop criteria --goal-id <id> [--json]
|
|
12
12
|
omo-agent-toolkit ulw-loop record-evidence --goal-id <id> --criterion-id <id> --status pass|fail|blocked --evidence "..." [--notes "..."] [--json]
|
|
13
|
+
omo-agent-toolkit ulw-loop checkpoint --print-template [--goal-id <id>] [--json]
|
|
13
14
|
omo-agent-toolkit ulw-loop checkpoint --goal-id <id> --status complete|failed|blocked --evidence "..." --codex-goal-json <...> [--quality-gate-json <...>] [--no-advance] [--json]
|
|
14
15
|
omo-agent-toolkit ulw-loop steer --kind <kind> ... --evidence "..." --rationale "..." [--proposals-json <json-or-path>] [--json]
|
|
15
16
|
omo-agent-toolkit ulw-loop add-goal --title "..." --objective "..." [--json]
|
|
@@ -17,6 +17,7 @@ import { type UlwLoopScope, ulwLoopAttemptEvidenceDir } from "./paths.js";
|
|
|
17
17
|
import { addUlwLoopGoal, createUlwLoopPlan, startNextUlwLoop, summarizeUlwLoopPlan } from "./plan-crud.js";
|
|
18
18
|
import { readUlwLoopPlan } from "./plan-io.js";
|
|
19
19
|
import { recordFinalReviewBlockers } from "./review-blockers.js";
|
|
20
|
+
import { statusNextActions } from "./status-next-actions.js";
|
|
20
21
|
import { steerUlwLoop } from "./steering.js";
|
|
21
22
|
import { steerUlwLoopBatch } from "./steering-batch.js";
|
|
22
23
|
import type { UlwLoopItem } from "./types.js";
|
|
@@ -73,6 +74,7 @@ export async function status(repoRoot: string, json: boolean, scope?: UlwLoopSco
|
|
|
73
74
|
plan,
|
|
74
75
|
summary: summarizeUlwLoopPlan(plan),
|
|
75
76
|
...(currentAttemptDir === undefined ? {} : { currentAttemptDir }),
|
|
77
|
+
nextActions: statusNextActions(plan),
|
|
76
78
|
});
|
|
77
79
|
} else printStatus(plan);
|
|
78
80
|
return 0;
|
|
@@ -127,6 +127,7 @@ function finalSection(
|
|
|
127
127
|
if (!isFinal)
|
|
128
128
|
return "- This is not the final ulw-loop story; do not run the final reviewer/manual-QA/gate-review quality gate yet.";
|
|
129
129
|
const option = sessionOption(plan);
|
|
130
|
+
if (surface === "omo-senpi") return senpiFinalSection(plan, goal, aggregate);
|
|
130
131
|
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>"`;
|
|
131
132
|
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>"`;
|
|
132
133
|
return joinLines([
|
|
@@ -147,6 +148,23 @@ function finalSection(
|
|
|
147
148
|
]);
|
|
148
149
|
}
|
|
149
150
|
|
|
151
|
+
function senpiFinalSection(plan: UlwLoopPlan, goal: UlwLoopItem, aggregate: boolean): string {
|
|
152
|
+
const option = sessionOption(plan);
|
|
153
|
+
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)"`;
|
|
154
|
+
return joinLines([
|
|
155
|
+
"Final story — run the single-reviewer quality gate before update_goal:",
|
|
156
|
+
"- Run manual QA yourself and write the non-empty artifact under currentAttemptDir; set manualQa.by to \"main-session\".",
|
|
157
|
+
'- Spawn exactly one gate reviewer with task(category: "deep").',
|
|
158
|
+
"- If that task fails with any model_unavailable failure, retry with category:unspecified-high, then category:unspecified-low.",
|
|
159
|
+
'- Set gateReview.by to the exact category:<name> literal used for the successful task.',
|
|
160
|
+
'- Build the gate JSON with omo-agent-toolkit ulw-loop checkpoint --print-template, then fill manualQa, gateReview, iteration, and criteriaCoverage.',
|
|
161
|
+
'- Require passed manualQa, approved gateReview, passed iteration, and complete criteriaCoverage before update_goal({status: "complete"}).',
|
|
162
|
+
aggregate
|
|
163
|
+
? `- If the gate is clean, call update_goal({status: "complete"}), call get_goal again, then checkpoint the aggregate story: ${checkpointCommand}`
|
|
164
|
+
: `- If the gate is clean, call update_goal({status: "complete"}), call get_goal again, then checkpoint: ${checkpointCommand}`,
|
|
165
|
+
]);
|
|
166
|
+
}
|
|
167
|
+
|
|
150
168
|
function sessionOption(plan: UlwLoopPlan): string {
|
|
151
169
|
const prefix = ".omo/ulw-loop/";
|
|
152
170
|
const suffix = "/goals.json";
|
|
@@ -139,3 +139,26 @@ export function formatCodexGoalReconciliation(reconciliation: CodexGoalReconcili
|
|
|
139
139
|
const parts = [...reconciliation.errors, ...reconciliation.warnings];
|
|
140
140
|
return parts.join(" ");
|
|
141
141
|
}
|
|
142
|
+
|
|
143
|
+
export interface CodexGoalMismatchRecovery {
|
|
144
|
+
readonly message: string;
|
|
145
|
+
readonly details: { readonly expectedObjective: string; readonly receivedObjective: string };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The reconciliation errors normalize whitespace for comparison, which makes the
|
|
150
|
+
* quoted objective unusable as a copy source. Recovery therefore carries the
|
|
151
|
+
* plan's `codexObjective` verbatim so the agent can paste it into `update_goal`.
|
|
152
|
+
*/
|
|
153
|
+
export function codexGoalMismatchRecovery(
|
|
154
|
+
expectedObjective: string,
|
|
155
|
+
snapshot: CodexGoalSnapshot | null | undefined,
|
|
156
|
+
): CodexGoalMismatchRecovery {
|
|
157
|
+
const receivedObjective = snapshot?.objective ?? "";
|
|
158
|
+
const message = [
|
|
159
|
+
"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.",
|
|
160
|
+
`expected codexObjective: ${expectedObjective}`,
|
|
161
|
+
`received objective: ${receivedObjective || "(none)"}`,
|
|
162
|
+
].join("\n");
|
|
163
|
+
return { message, details: { expectedObjective, receivedObjective } };
|
|
164
|
+
}
|
|
@@ -107,15 +107,7 @@ export interface UlwLoopManualQaAdversarialCase {
|
|
|
107
107
|
readonly artifactRefs: readonly string[];
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
readonly codeReview: {
|
|
112
|
-
readonly by: string;
|
|
113
|
-
readonly recommendation: "APPROVE";
|
|
114
|
-
readonly codeQualityStatus: "CLEAR" | "WATCH";
|
|
115
|
-
readonly reportPath: string;
|
|
116
|
-
readonly evidence: string;
|
|
117
|
-
readonly blockers: readonly [];
|
|
118
|
-
};
|
|
110
|
+
interface UlwLoopQualityGateCommon {
|
|
119
111
|
readonly manualQa: {
|
|
120
112
|
readonly by: string;
|
|
121
113
|
readonly status: "passed";
|
|
@@ -147,6 +139,24 @@ export interface UlwLoopQualityGate {
|
|
|
147
139
|
};
|
|
148
140
|
}
|
|
149
141
|
|
|
142
|
+
export interface UlwLoopQualityGateLazycodex extends UlwLoopQualityGateCommon {
|
|
143
|
+
readonly surface: "lazycodex";
|
|
144
|
+
readonly codeReview: {
|
|
145
|
+
readonly by: string;
|
|
146
|
+
readonly recommendation: "APPROVE";
|
|
147
|
+
readonly codeQualityStatus: "CLEAR" | "WATCH";
|
|
148
|
+
readonly reportPath: string;
|
|
149
|
+
readonly evidence: string;
|
|
150
|
+
readonly blockers: readonly [];
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface UlwLoopQualityGateSenpi extends UlwLoopQualityGateCommon {
|
|
155
|
+
readonly surface: "omo-senpi";
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export type UlwLoopQualityGate = UlwLoopQualityGateLazycodex | UlwLoopQualityGateSenpi;
|
|
159
|
+
|
|
150
160
|
export interface UlwLoopLedgerEntry {
|
|
151
161
|
at: string;
|
|
152
162
|
kind: UlwLoopLedgerEventKind;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createReadStream } from "node:fs";
|
|
1
|
+
import { createReadStream, readdirSync } from "node:fs";
|
|
2
2
|
import { appendFile, mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
3
3
|
import { createInterface } from "node:readline";
|
|
4
4
|
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
ulwLoopLedgerPath,
|
|
12
12
|
ulwLoopRelativeDir,
|
|
13
13
|
} from "./paths.js";
|
|
14
|
+
import { planMissingRecovery } from "./plan-missing-recovery.js";
|
|
14
15
|
import type { UlwLoopLedgerEntry, UlwLoopPlan } from "./types.js";
|
|
15
16
|
import { iso, ULW_LOOP_DIR, ULW_LOOP_GOALS, ULW_LOOP_LEDGER, UlwLoopError } from "./types.js";
|
|
16
17
|
|
|
@@ -73,10 +74,11 @@ export async function readUlwLoopPlan(repoRoot: string, scope?: UlwLoopScope): P
|
|
|
73
74
|
raw = await readFile(path, "utf8");
|
|
74
75
|
} catch (error) {
|
|
75
76
|
if (!hasCode(error, "ENOENT")) throw error;
|
|
77
|
+
const recovery = planMissingRecovery(readSessionDirs(repoRoot));
|
|
76
78
|
throw new UlwLoopError(
|
|
77
|
-
`No ulw-loop plan found at ${repoRelative(path, repoRoot)}.
|
|
79
|
+
`No ulw-loop plan found at ${repoRelative(path, repoRoot)}.\n${recovery.message}`,
|
|
78
80
|
"ULW_LOOP_PLAN_MISSING",
|
|
79
|
-
{ cause: error },
|
|
81
|
+
{ cause: error, ...(recovery.details === undefined ? {} : { details: recovery.details }) },
|
|
80
82
|
);
|
|
81
83
|
}
|
|
82
84
|
const parsed: UlwLoopPlan = JSON.parse(raw);
|
|
@@ -108,6 +110,19 @@ export async function readUlwLoopPlan(repoRoot: string, scope?: UlwLoopScope): P
|
|
|
108
110
|
return parsed;
|
|
109
111
|
}
|
|
110
112
|
|
|
113
|
+
// Session dirs are the only recovery hint that matters when a plan is missing: the
|
|
114
|
+
// caller is almost always scoped to a session whose sibling actually holds the plan.
|
|
115
|
+
function readSessionDirs(repoRoot: string): readonly string[] {
|
|
116
|
+
try {
|
|
117
|
+
return readdirSync(ulwLoopDir(repoRoot), { withFileTypes: true })
|
|
118
|
+
.filter((entry) => entry.isDirectory())
|
|
119
|
+
.map((entry) => entry.name)
|
|
120
|
+
.sort();
|
|
121
|
+
} catch {
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
111
126
|
export async function writePlan(repoRoot: string, plan: UlwLoopPlan, scope?: UlwLoopScope): Promise<void> {
|
|
112
127
|
await mkdir(ulwLoopDir(repoRoot, scope), { recursive: true });
|
|
113
128
|
const path = ulwLoopGoalsPath(repoRoot, scope);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const ULW_LOOP_CREATE_GOALS_COMMAND = 'omo-agent-toolkit ulw-loop create-goals --brief "<brief>" --json';
|
|
2
|
+
|
|
3
|
+
export interface PlanMissingRecovery {
|
|
4
|
+
readonly message: string;
|
|
5
|
+
readonly details?: { readonly existingSessionIds: readonly string[] };
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A missing plan is either "never bootstrapped" or "bootstrapped under a different
|
|
10
|
+
* session id"; the recovery text has to answer both without the caller guessing.
|
|
11
|
+
*/
|
|
12
|
+
export function planMissingRecovery(existingSessionIds: readonly string[]): PlanMissingRecovery {
|
|
13
|
+
const lines = [`Recovery: bootstrap the plan with \`${ULW_LOOP_CREATE_GOALS_COMMAND}\`.`];
|
|
14
|
+
if (existingSessionIds.length === 0) return { message: lines.join("\n") };
|
|
15
|
+
lines.push(
|
|
16
|
+
`Existing ulw-loop session ids under .omo/ulw-loop/: ${existingSessionIds.join(", ")}. Re-run with \`--session-id <id>\` to target one of them.`,
|
|
17
|
+
);
|
|
18
|
+
return { message: lines.join("\n"), details: { existingSessionIds } };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function sessionIdRequiredMessage(flag: string): string {
|
|
22
|
+
return [
|
|
23
|
+
`${flag} requires a non-empty value.`,
|
|
24
|
+
"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),",
|
|
25
|
+
`so pass the scope explicitly: \`${flag} <id>\` (for example \`${flag} 01a05b8a-6763-7780-834f-319423b071ce\`).`,
|
|
26
|
+
].join("\n");
|
|
27
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import type { ValidateQualityGateOptions } from "./quality-gate.js";
|
|
3
|
+
import { isRecord } from "./quality-gate-fields.js";
|
|
4
|
+
import { UlwLoopError } from "./types.js";
|
|
5
|
+
|
|
6
|
+
type Defect = { readonly field: string; readonly message: string };
|
|
7
|
+
const PLACEHOLDER = /^(?:<replace:[^>]+>|placeholder|todo|tbd|n\/a|stub)$/i;
|
|
8
|
+
|
|
9
|
+
function add(defects: Defect[], field: string, message: string): void {
|
|
10
|
+
defects.push({ field, message });
|
|
11
|
+
}
|
|
12
|
+
function text(value: unknown, field: string, defects: Defect[]): void {
|
|
13
|
+
if (typeof value !== "string" || value.trim() === "")
|
|
14
|
+
add(defects, field, `Final quality gate requires non-empty ${field}.`);
|
|
15
|
+
else if (PLACEHOLDER.test(value.trim())) add(defects, field, `Final quality gate rejects placeholder ${field}.`);
|
|
16
|
+
}
|
|
17
|
+
function section(value: unknown, field: string, defects: Defect[]): Record<string, unknown> {
|
|
18
|
+
if (!isRecord(value)) {
|
|
19
|
+
add(defects, field, `Final quality gate is missing ${field} evidence.`);
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function aggregateQualityGateDefects(input: unknown, opts: ValidateQualityGateOptions | undefined): void {
|
|
26
|
+
if (!isRecord(input)) return;
|
|
27
|
+
const defects: Defect[] = [];
|
|
28
|
+
const gate = input;
|
|
29
|
+
const manual = section(gate["manualQa"], "manualQa", defects);
|
|
30
|
+
const review = section(gate["gateReview"], "gateReview", defects);
|
|
31
|
+
text(manual["evidence"], "manualQa.evidence", defects);
|
|
32
|
+
text(review["evidence"], "gateReview.evidence", defects);
|
|
33
|
+
if (review["recommendation"] !== "APPROVE")
|
|
34
|
+
add(defects, "gateReview.recommendation", "gateReview.recommendation must be APPROVE.");
|
|
35
|
+
const artifacts = Array.isArray(manual["artifactRefs"]) ? manual["artifactRefs"] : [];
|
|
36
|
+
for (const [index, item] of artifacts.entries()) {
|
|
37
|
+
if (!isRecord(item)) continue;
|
|
38
|
+
const path = item["path"];
|
|
39
|
+
if (typeof path !== "string" || path.trim() === "") continue;
|
|
40
|
+
if (opts?.repoRoot !== undefined && opts.fs !== undefined && !opts.fs.existsSync(resolve(opts.repoRoot, path)))
|
|
41
|
+
add(
|
|
42
|
+
defects,
|
|
43
|
+
`manualQa.artifactRefs[${index}].path`,
|
|
44
|
+
`manualQa.artifactRefs[${index}].path must point to an existing artifact.`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
if (defects.length === 0) return;
|
|
48
|
+
const truncated = defects.length > 25;
|
|
49
|
+
const fields = defects.slice(0, 25).map(({ field, message }) => ({ field, message }));
|
|
50
|
+
const message = [
|
|
51
|
+
`Final quality gate has ${fields.length}${truncated ? "+" : ""} validation defects:`,
|
|
52
|
+
...fields.map((item) => `- ${item.field}: ${item.message}`),
|
|
53
|
+
].join("\n");
|
|
54
|
+
throw new UlwLoopError(message, "ULW_LOOP_QUALITY_GATE_INVALID", {
|
|
55
|
+
details: { field: fields[0]?.field, fields, ...(truncated ? { truncated: true } : {}) },
|
|
56
|
+
});
|
|
57
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { isWithinAttemptDir } from "./paths.js";
|
|
3
|
+
import type { ValidateQualityGateOptions } from "./quality-gate.js";
|
|
4
|
+
import { invalid, section, stringArray, textField } from "./quality-gate-fields.js";
|
|
5
|
+
import type { UlwLoopManualQaArtifactKind, UlwLoopManualQaArtifactRef, UlwLoopManualQaSurface } from "./types.js";
|
|
6
|
+
|
|
7
|
+
export function surfaceField(value: unknown, field: string): UlwLoopManualQaSurface {
|
|
8
|
+
if (
|
|
9
|
+
value === "cli" ||
|
|
10
|
+
value === "http" ||
|
|
11
|
+
value === "tmux" ||
|
|
12
|
+
value === "browser" ||
|
|
13
|
+
value === "gui" ||
|
|
14
|
+
value === "data"
|
|
15
|
+
)
|
|
16
|
+
return value;
|
|
17
|
+
invalid(`${field} must be a supported manual QA surface.`, field);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function kindField(value: unknown, field: string): UlwLoopManualQaArtifactKind {
|
|
21
|
+
if (
|
|
22
|
+
value === "cli-transcript" ||
|
|
23
|
+
value === "log" ||
|
|
24
|
+
value === "screenshot" ||
|
|
25
|
+
value === "image" ||
|
|
26
|
+
value === "http-dump" ||
|
|
27
|
+
value === "data-diff"
|
|
28
|
+
)
|
|
29
|
+
return value;
|
|
30
|
+
invalid(`${field} must be a supported artifact kind.`, field);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function artifactCompatible(surface: UlwLoopManualQaSurface, kind: UlwLoopManualQaArtifactKind): boolean {
|
|
34
|
+
switch (surface) {
|
|
35
|
+
case "cli":
|
|
36
|
+
case "tmux":
|
|
37
|
+
return kind === "cli-transcript" || kind === "log";
|
|
38
|
+
case "http":
|
|
39
|
+
return kind === "http-dump";
|
|
40
|
+
case "browser":
|
|
41
|
+
case "gui":
|
|
42
|
+
return kind === "screenshot" || kind === "image";
|
|
43
|
+
case "data":
|
|
44
|
+
return kind === "data-diff";
|
|
45
|
+
default:
|
|
46
|
+
invalid("manualQa.surfaceEvidence has an unsupported surface.", "manualQa.surfaceEvidence.surface");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function checkFile(path: string, field: string, opts?: ValidateQualityGateOptions): void {
|
|
51
|
+
if (opts?.repoRoot === undefined || opts.fs === undefined) return;
|
|
52
|
+
const absolute = resolve(opts.repoRoot, path);
|
|
53
|
+
if (!opts.fs.existsSync(absolute)) invalid(`${field} must point to an existing artifact.`, field);
|
|
54
|
+
if (opts.fs.statSync(absolute).size <= 0) invalid(`${field} must point to a non-empty artifact.`, field);
|
|
55
|
+
if (opts.currentAttemptDir !== undefined && opts.repoRoot !== undefined) {
|
|
56
|
+
const attemptRoot = resolve(opts.repoRoot, opts.currentAttemptDir);
|
|
57
|
+
if (!isWithinAttemptDir(absolute, attemptRoot))
|
|
58
|
+
invalid(
|
|
59
|
+
`${field} (${path}) must point to an artifact from the current attempt (${opts.currentAttemptDir}).`,
|
|
60
|
+
field,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function artifactMap(refs: readonly UlwLoopManualQaArtifactRef[]): Map<string, UlwLoopManualQaArtifactRef> {
|
|
66
|
+
const byId = new Map<string, UlwLoopManualQaArtifactRef>();
|
|
67
|
+
for (const ref of refs) {
|
|
68
|
+
if (byId.has(ref.id)) invalid(`manualQa.artifactRefs contains duplicate ${ref.id}.`, "manualQa.artifactRefs");
|
|
69
|
+
byId.set(ref.id, ref);
|
|
70
|
+
}
|
|
71
|
+
return byId;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function parseArtifactRefs(
|
|
75
|
+
value: unknown,
|
|
76
|
+
opts?: ValidateQualityGateOptions,
|
|
77
|
+
): readonly UlwLoopManualQaArtifactRef[] {
|
|
78
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
79
|
+
invalid("manualQa.artifactRefs must not be empty.", "manualQa.artifactRefs");
|
|
80
|
+
return value.map((item, index) => {
|
|
81
|
+
const ref = section(item, `manualQa.artifactRefs[${index}]`);
|
|
82
|
+
const path = textField(ref["path"], `manualQa.artifactRefs[${index}].path`);
|
|
83
|
+
checkFile(path, `manualQa.artifactRefs[${index}].path`, opts);
|
|
84
|
+
return {
|
|
85
|
+
id: textField(ref["id"], `manualQa.artifactRefs[${index}].id`),
|
|
86
|
+
kind: kindField(ref["kind"], `manualQa.artifactRefs[${index}].kind`),
|
|
87
|
+
description: textField(ref["description"], `manualQa.artifactRefs[${index}].description`),
|
|
88
|
+
path,
|
|
89
|
+
};
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function referencedArtifacts(
|
|
94
|
+
value: unknown,
|
|
95
|
+
field: string,
|
|
96
|
+
byId: ReadonlyMap<string, UlwLoopManualQaArtifactRef>,
|
|
97
|
+
): readonly UlwLoopManualQaArtifactRef[] {
|
|
98
|
+
return stringArray(value, field).map((id) => {
|
|
99
|
+
const artifact = byId.get(id);
|
|
100
|
+
if (artifact === undefined) invalid(`${field} references unknown artifact ${id}.`, field);
|
|
101
|
+
return artifact;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { UlwLoopError } from "./types.js";
|
|
2
2
|
|
|
3
|
-
const PLACEHOLDER_PATTERN = /^(
|
|
3
|
+
const PLACEHOLDER_PATTERN = /^(?:<replace:[^>]+>|placeholder|todo|tbd|n\/a|stub)$/i;
|
|
4
4
|
|
|
5
5
|
export function invalid(message: string, field: string): never {
|
|
6
6
|
throw new UlwLoopError(message, "ULW_LOOP_QUALITY_GATE_INVALID", { details: { field } });
|
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { aggregateQualityGateDefects } from "./quality-gate-aggregate.js";
|
|
2
|
+
import {
|
|
3
|
+
artifactCompatible,
|
|
4
|
+
artifactMap,
|
|
5
|
+
checkFile,
|
|
6
|
+
parseArtifactRefs,
|
|
7
|
+
referencedArtifacts,
|
|
8
|
+
surfaceField,
|
|
9
|
+
} from "./quality-gate-artifacts.js";
|
|
3
10
|
import {
|
|
4
11
|
emptyBlockers,
|
|
5
12
|
invalid,
|
|
@@ -10,13 +17,8 @@ import {
|
|
|
10
17
|
textField,
|
|
11
18
|
} from "./quality-gate-fields.js";
|
|
12
19
|
import { adversarialVerdict, codeQualityStatusField, passedVerdict } from "./quality-gate-verdicts.js";
|
|
13
|
-
import {
|
|
14
|
-
import type {
|
|
15
|
-
UlwLoopManualQaArtifactKind,
|
|
16
|
-
UlwLoopManualQaArtifactRef,
|
|
17
|
-
UlwLoopManualQaSurface,
|
|
18
|
-
UlwLoopQualityGate,
|
|
19
|
-
} from "./types.js";
|
|
20
|
+
import { GATE_SECTION_BY_ACCEPTOR, REQUIRED_GATE_SECTIONS_BY_SURFACE, type UlwLoopToolkitSurface } from "./surface.js";
|
|
21
|
+
import type { UlwLoopManualQaArtifactRef, UlwLoopQualityGate } from "./types.js";
|
|
20
22
|
|
|
21
23
|
export {
|
|
22
24
|
classifyExternalAuthorizationBlocker,
|
|
@@ -43,109 +45,35 @@ function reviewerRoleField(value: unknown, expected: string, field: string): str
|
|
|
43
45
|
return expected;
|
|
44
46
|
}
|
|
45
47
|
|
|
46
|
-
function
|
|
47
|
-
if (
|
|
48
|
-
value === "cli" ||
|
|
49
|
-
value === "http" ||
|
|
50
|
-
value === "tmux" ||
|
|
51
|
-
value === "browser" ||
|
|
52
|
-
value === "gui" ||
|
|
53
|
-
value === "data"
|
|
54
|
-
)
|
|
55
|
-
return value;
|
|
56
|
-
invalid(`${field} must be a supported manual QA surface.`, field);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function kindField(value: unknown, field: string): UlwLoopManualQaArtifactKind {
|
|
60
|
-
if (
|
|
61
|
-
value === "cli-transcript" ||
|
|
62
|
-
value === "log" ||
|
|
63
|
-
value === "screenshot" ||
|
|
64
|
-
value === "image" ||
|
|
65
|
-
value === "http-dump" ||
|
|
66
|
-
value === "data-diff"
|
|
67
|
-
)
|
|
68
|
-
return value;
|
|
69
|
-
invalid(`${field} must be a supported artifact kind.`, field);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function artifactCompatible(surface: UlwLoopManualQaSurface, kind: UlwLoopManualQaArtifactKind): boolean {
|
|
73
|
-
switch (surface) {
|
|
74
|
-
case "cli":
|
|
75
|
-
case "tmux":
|
|
76
|
-
return kind === "cli-transcript" || kind === "log";
|
|
77
|
-
case "http":
|
|
78
|
-
return kind === "http-dump";
|
|
79
|
-
case "browser":
|
|
80
|
-
case "gui":
|
|
81
|
-
return kind === "screenshot" || kind === "image";
|
|
82
|
-
case "data":
|
|
83
|
-
return kind === "data-diff";
|
|
84
|
-
default:
|
|
85
|
-
invalid("manualQa.surfaceEvidence has an unsupported surface.", "manualQa.surfaceEvidence.surface");
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function checkFile(path: string, field: string, opts?: ValidateQualityGateOptions): void {
|
|
90
|
-
if (opts?.repoRoot === undefined || opts.fs === undefined) return;
|
|
91
|
-
const absolute = resolve(opts.repoRoot, path);
|
|
92
|
-
if (!opts.fs.existsSync(absolute)) invalid(`${field} must point to an existing artifact.`, field);
|
|
93
|
-
if (opts.fs.statSync(absolute).size <= 0) invalid(`${field} must point to a non-empty artifact.`, field);
|
|
94
|
-
if (opts.currentAttemptDir !== undefined && opts.repoRoot !== undefined) {
|
|
95
|
-
const attemptRoot = resolve(opts.repoRoot, opts.currentAttemptDir);
|
|
96
|
-
if (!isWithinAttemptDir(absolute, attemptRoot))
|
|
97
|
-
invalid(
|
|
98
|
-
`${field} (${path}) must point to an artifact from the current attempt (${opts.currentAttemptDir}).`,
|
|
99
|
-
field,
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function artifactMap(refs: readonly UlwLoopManualQaArtifactRef[]): Map<string, UlwLoopManualQaArtifactRef> {
|
|
105
|
-
const byId = new Map<string, UlwLoopManualQaArtifactRef>();
|
|
106
|
-
for (const ref of refs) {
|
|
107
|
-
if (byId.has(ref.id)) invalid(`manualQa.artifactRefs contains duplicate ${ref.id}.`, "manualQa.artifactRefs");
|
|
108
|
-
byId.set(ref.id, ref);
|
|
109
|
-
}
|
|
110
|
-
return byId;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function parseArtifactRefs(value: unknown, opts?: ValidateQualityGateOptions): readonly UlwLoopManualQaArtifactRef[] {
|
|
114
|
-
if (!Array.isArray(value) || value.length === 0)
|
|
115
|
-
invalid("manualQa.artifactRefs must not be empty.", "manualQa.artifactRefs");
|
|
116
|
-
return value.map((item, index) => {
|
|
117
|
-
const ref = section(item, `manualQa.artifactRefs[${index}]`);
|
|
118
|
-
const path = textField(ref["path"], `manualQa.artifactRefs[${index}].path`);
|
|
119
|
-
checkFile(path, `manualQa.artifactRefs[${index}].path`, opts);
|
|
120
|
-
return {
|
|
121
|
-
id: textField(ref["id"], `manualQa.artifactRefs[${index}].id`),
|
|
122
|
-
kind: kindField(ref["kind"], `manualQa.artifactRefs[${index}].kind`),
|
|
123
|
-
description: textField(ref["description"], `manualQa.artifactRefs[${index}].description`),
|
|
124
|
-
path,
|
|
125
|
-
};
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function referencedArtifacts(
|
|
48
|
+
function reviewerAcceptorField(
|
|
130
49
|
value: unknown,
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
):
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
50
|
+
surface: UlwLoopToolkitSurface,
|
|
51
|
+
sectionName: "manualQa" | "gateReview",
|
|
52
|
+
): string {
|
|
53
|
+
const field = `${sectionName}.by`;
|
|
54
|
+
const actual = textField(value, field);
|
|
55
|
+
const accepted = GATE_SECTION_BY_ACCEPTOR[surface][sectionName];
|
|
56
|
+
if (accepted === undefined || !accepted.includes(actual))
|
|
57
|
+
invalid(`${field} must be one of ${accepted?.join(", ") ?? "the configured reviewers"}.`, field);
|
|
58
|
+
return actual;
|
|
139
59
|
}
|
|
140
60
|
|
|
141
61
|
export function validateQualityGate(input: unknown, opts?: ValidateQualityGateOptions): UlwLoopQualityGate {
|
|
142
|
-
const
|
|
62
|
+
const surface = opts?.reviewerSurface ?? "lazycodex";
|
|
63
|
+
aggregateQualityGateDefects(input, opts);
|
|
143
64
|
const gate = section(input, "qualityGate");
|
|
144
|
-
const
|
|
65
|
+
const requiredSections = REQUIRED_GATE_SECTIONS_BY_SURFACE[surface];
|
|
66
|
+
for (const sectionName of requiredSections) section(gate[sectionName], sectionName);
|
|
67
|
+
if (surface === "omo-senpi" && gate["codeReview"] !== undefined)
|
|
68
|
+
invalid("omo-senpi gate has no codeReview lane.", "codeReview");
|
|
145
69
|
const manualQa = section(gate["manualQa"], "manualQa");
|
|
146
70
|
const gateReview = section(gate["gateReview"], "gateReview");
|
|
147
71
|
const iteration = section(gate["iteration"], "iteration");
|
|
148
72
|
const coverage = section(gate["criteriaCoverage"], "criteriaCoverage");
|
|
73
|
+
const manualQaBy = reviewerAcceptorField(manualQa["by"], surface, "manualQa");
|
|
74
|
+
const gateReviewBy = reviewerAcceptorField(gateReview["by"], surface, "gateReview");
|
|
75
|
+
if (surface === "lazycodex")
|
|
76
|
+
reviewerRoleField(section(gate["codeReview"], "codeReview")["by"], "lazycodex-code-reviewer", "codeReview.by");
|
|
149
77
|
const totalCriteria = numberField(coverage["totalCriteria"], "criteriaCoverage.totalCriteria");
|
|
150
78
|
const passCount = numberField(coverage["passCount"], "criteriaCoverage.passCount");
|
|
151
79
|
if (passCount < totalCriteria)
|
|
@@ -154,21 +82,11 @@ export function validateQualityGate(input: unknown, opts?: ValidateQualityGateOp
|
|
|
154
82
|
const byId = artifactMap(artifactRefs);
|
|
155
83
|
const surfaceEvidence = parseSurfaceEvidence(manualQa["surfaceEvidence"], byId);
|
|
156
84
|
const adversarialCases = parseAdversarialCases(manualQa["adversarialCases"], byId);
|
|
157
|
-
const codeReportPath = textField(codeReview["reportPath"], "codeReview.reportPath");
|
|
158
85
|
const gateReportPath = textField(gateReview["reportPath"], "gateReview.reportPath");
|
|
159
|
-
checkFile(codeReportPath, "codeReview.reportPath", opts);
|
|
160
86
|
checkFile(gateReportPath, "gateReview.reportPath", opts);
|
|
161
|
-
|
|
162
|
-
codeReview: {
|
|
163
|
-
by: reviewerRoleField(codeReview["by"], reviewerRoles.codeReview, "codeReview.by"),
|
|
164
|
-
recommendation: literal(codeReview["recommendation"], "APPROVE", "codeReview.recommendation"),
|
|
165
|
-
codeQualityStatus: codeQualityStatusField(codeReview["codeQualityStatus"], "codeReview.codeQualityStatus"),
|
|
166
|
-
reportPath: codeReportPath,
|
|
167
|
-
evidence: textField(codeReview["evidence"], "codeReview.evidence"),
|
|
168
|
-
blockers: emptyBlockers(codeReview["blockers"], "codeReview.blockers"),
|
|
169
|
-
},
|
|
87
|
+
const common = {
|
|
170
88
|
manualQa: {
|
|
171
|
-
by:
|
|
89
|
+
by: manualQaBy,
|
|
172
90
|
status: literal(manualQa["status"], "passed", "manualQa.status"),
|
|
173
91
|
evidence: textField(manualQa["evidence"], "manualQa.evidence"),
|
|
174
92
|
surfaceEvidence,
|
|
@@ -176,7 +94,7 @@ export function validateQualityGate(input: unknown, opts?: ValidateQualityGateOp
|
|
|
176
94
|
artifactRefs,
|
|
177
95
|
},
|
|
178
96
|
gateReview: {
|
|
179
|
-
by:
|
|
97
|
+
by: gateReviewBy,
|
|
180
98
|
recommendation: literal(gateReview["recommendation"], "APPROVE", "gateReview.recommendation"),
|
|
181
99
|
reportPath: gateReportPath,
|
|
182
100
|
evidence: textField(gateReview["evidence"], "gateReview.evidence"),
|
|
@@ -200,6 +118,22 @@ export function validateQualityGate(input: unknown, opts?: ValidateQualityGateOp
|
|
|
200
118
|
),
|
|
201
119
|
},
|
|
202
120
|
};
|
|
121
|
+
if (surface === "omo-senpi") return { surface, ...common };
|
|
122
|
+
const codeReview = section(gate["codeReview"], "codeReview");
|
|
123
|
+
const codeReportPath = textField(codeReview["reportPath"], "codeReview.reportPath");
|
|
124
|
+
checkFile(codeReportPath, "codeReview.reportPath", opts);
|
|
125
|
+
return {
|
|
126
|
+
surface,
|
|
127
|
+
...common,
|
|
128
|
+
codeReview: {
|
|
129
|
+
by: "lazycodex-code-reviewer",
|
|
130
|
+
recommendation: literal(codeReview["recommendation"], "APPROVE", "codeReview.recommendation"),
|
|
131
|
+
codeQualityStatus: codeQualityStatusField(codeReview["codeQualityStatus"], "codeReview.codeQualityStatus"),
|
|
132
|
+
reportPath: codeReportPath,
|
|
133
|
+
evidence: textField(codeReview["evidence"], "codeReview.evidence"),
|
|
134
|
+
blockers: emptyBlockers(codeReview["blockers"], "codeReview.blockers"),
|
|
135
|
+
},
|
|
136
|
+
};
|
|
203
137
|
}
|
|
204
138
|
|
|
205
139
|
function parseSurfaceEvidence(
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// biome-ignore-all format: compact port must stay within the requested pure LOC budget.
|
|
2
2
|
|
|
3
|
+
import { codexSnapshotMismatchError } from "./checkpoint-reconciliation.js";
|
|
3
4
|
import { readCodexGoalSnapshotInput, reconcileCodexGoalSnapshot } from "./codex-goal-snapshot.js";
|
|
4
5
|
import { codexGoalMode, compatibleCodexObjectives, expectedCodexObjective, isFinalRunCompletionCandidate } from "./goal-status.js";
|
|
5
6
|
import type { UlwLoopScope } from "./paths.js";
|
|
@@ -55,8 +56,9 @@ export async function recordFinalReviewBlockers(
|
|
|
55
56
|
|
|
56
57
|
const snapshot = await readCodexGoalSnapshotInput(args.codexGoalJson, repoRoot);
|
|
57
58
|
const aggregate = codexGoalMode(plan) === "aggregate";
|
|
58
|
-
const
|
|
59
|
-
|
|
59
|
+
const expected = expectedCodexObjective(plan, goal);
|
|
60
|
+
const reconciliation = reconcileCodexGoalSnapshot(snapshot, { expectedObjective: expected, ...(aggregate ? { acceptedObjectives: compatibleCodexObjectives(plan) } : {}), allowedStatuses: ["active"], requireSnapshot: true, requireComplete: false });
|
|
61
|
+
if (!reconciliation.ok) throw codexSnapshotMismatchError({ reconciliation, snapshot, expectedObjective: expected });
|
|
60
62
|
|
|
61
63
|
const now = iso();
|
|
62
64
|
for (const field of BLOCKER_FIELDS) Reflect.deleteProperty(goal, field);
|