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
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, rm } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
|
|
6
|
+
import { ulwLoopCommand } from "../src/cli-commands.js";
|
|
7
|
+
import { ulwLoopDir } from "../src/paths.js";
|
|
8
|
+
import { writePlan } from "../src/plan-io.js";
|
|
9
|
+
import type { UlwLoopItem, UlwLoopPlan, UlwLoopSuccessCriterion } from "../src/types.js";
|
|
10
|
+
|
|
11
|
+
const NOW = "2026-05-23T00:00:00.000Z";
|
|
12
|
+
const SESSION_ENV_KEYS = ["OMO_ULW_LOOP_SESSION_ID", "CODEX_SESSION_ID", "CODEX_THREAD_ID", "PI_SESSION_ID"] as const;
|
|
13
|
+
|
|
14
|
+
let testDir: string;
|
|
15
|
+
let out: string[];
|
|
16
|
+
let err: string[];
|
|
17
|
+
let savedEnv: Record<string, string | undefined>;
|
|
18
|
+
|
|
19
|
+
beforeEach(async () => {
|
|
20
|
+
testDir = await mkdtemp(join(tmpdir(), "ug-status-next-"));
|
|
21
|
+
out = [];
|
|
22
|
+
err = [];
|
|
23
|
+
savedEnv = {};
|
|
24
|
+
for (const key of SESSION_ENV_KEYS) {
|
|
25
|
+
savedEnv[key] = process.env[key];
|
|
26
|
+
delete process.env[key];
|
|
27
|
+
}
|
|
28
|
+
vi.spyOn(process, "cwd").mockReturnValue(testDir);
|
|
29
|
+
vi.spyOn(process.stdout, "write").mockImplementation((chunk: string | Uint8Array): boolean => {
|
|
30
|
+
out.push(chunk.toString());
|
|
31
|
+
return true;
|
|
32
|
+
});
|
|
33
|
+
vi.spyOn(process.stderr, "write").mockImplementation((chunk: string | Uint8Array): boolean => {
|
|
34
|
+
err.push(chunk.toString());
|
|
35
|
+
return true;
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
afterEach(async () => {
|
|
40
|
+
vi.restoreAllMocks();
|
|
41
|
+
for (const key of SESSION_ENV_KEYS) {
|
|
42
|
+
const saved = savedEnv[key];
|
|
43
|
+
if (saved === undefined) delete process.env[key];
|
|
44
|
+
else process.env[key] = saved;
|
|
45
|
+
}
|
|
46
|
+
await rm(testDir, { recursive: true, force: true });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
function stdoutJson(): Record<string, unknown> {
|
|
50
|
+
return JSON.parse(out.join(""));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function nextActions(): readonly string[] {
|
|
54
|
+
const value = stdoutJson()["nextActions"];
|
|
55
|
+
if (!Array.isArray(value)) throw new Error(`expected nextActions array, got ${JSON.stringify(value)}`);
|
|
56
|
+
return value.map((entry) => String(entry));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function criterion(id: string, status: UlwLoopSuccessCriterion["status"]): UlwLoopSuccessCriterion {
|
|
60
|
+
return {
|
|
61
|
+
id,
|
|
62
|
+
scenario: `${id} scenario`,
|
|
63
|
+
userModel: "happy",
|
|
64
|
+
expectedEvidence: `${id} proof`,
|
|
65
|
+
capturedEvidence: status === "pass" ? `${id} passed` : null,
|
|
66
|
+
status,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function goal(overrides: Partial<UlwLoopItem> = {}): UlwLoopItem {
|
|
71
|
+
return {
|
|
72
|
+
id: "G001",
|
|
73
|
+
title: "Build auth",
|
|
74
|
+
objective: "Implement JWT auth endpoint",
|
|
75
|
+
status: "in_progress",
|
|
76
|
+
successCriteria: [criterion("C001", "pass")],
|
|
77
|
+
attempt: 1,
|
|
78
|
+
createdAt: NOW,
|
|
79
|
+
updatedAt: NOW,
|
|
80
|
+
...overrides,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function plan(goals: UlwLoopItem[], overrides: Partial<UlwLoopPlan> = {}): UlwLoopPlan {
|
|
85
|
+
const result: UlwLoopPlan = {
|
|
86
|
+
version: 1,
|
|
87
|
+
evidenceLayoutVersion: 2,
|
|
88
|
+
createdAt: NOW,
|
|
89
|
+
updatedAt: NOW,
|
|
90
|
+
briefPath: ".omo/ulw-loop/brief.md",
|
|
91
|
+
goalsPath: ".omo/ulw-loop/goals.json",
|
|
92
|
+
ledgerPath: ".omo/ulw-loop/ledger.jsonl",
|
|
93
|
+
goals,
|
|
94
|
+
};
|
|
95
|
+
Object.assign(result, overrides);
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function legacyLayoutPlan(goals: UlwLoopItem[], overrides: Partial<UlwLoopPlan> = {}): UlwLoopPlan {
|
|
100
|
+
const result = plan(goals, overrides);
|
|
101
|
+
delete result.evidenceLayoutVersion;
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function seed(seedPlan: UlwLoopPlan): Promise<void> {
|
|
106
|
+
await mkdir(ulwLoopDir(testDir), { recursive: true });
|
|
107
|
+
await writePlan(testDir, seedPlan);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
describe("#given a plan with no goals", () => {
|
|
111
|
+
it("#when status --json #then nextActions points at create-goals", async () => {
|
|
112
|
+
await seed(plan([]));
|
|
113
|
+
|
|
114
|
+
expect(await ulwLoopCommand(["status", "--json"])).toBe(0);
|
|
115
|
+
expect(nextActions().join("\n")).toContain('omo-agent-toolkit ulw-loop create-goals --brief "<brief>" --json');
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("#given an active goal with unresolved criteria", () => {
|
|
120
|
+
it("#when status --json #then nextActions names record-evidence with the remaining criterion ids", async () => {
|
|
121
|
+
await seed(
|
|
122
|
+
plan(
|
|
123
|
+
[
|
|
124
|
+
goal({
|
|
125
|
+
successCriteria: [criterion("C001", "pass"), criterion("C002", "pending"), criterion("C003", "fail")],
|
|
126
|
+
}),
|
|
127
|
+
],
|
|
128
|
+
{
|
|
129
|
+
activeGoalId: "G001",
|
|
130
|
+
},
|
|
131
|
+
),
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
expect(await ulwLoopCommand(["status", "--json"])).toBe(0);
|
|
135
|
+
const actions = nextActions().join("\n");
|
|
136
|
+
expect(actions).toContain("record-evidence");
|
|
137
|
+
expect(actions).toContain("C002");
|
|
138
|
+
expect(actions).toContain("C003");
|
|
139
|
+
expect(actions).not.toContain("C001");
|
|
140
|
+
expect(stdoutJson()).toHaveProperty("currentAttemptDir");
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe("#given the final story with every criterion passing", () => {
|
|
145
|
+
it("#when status --json #then nextActions tells the agent to complete the goal then print the template", async () => {
|
|
146
|
+
await seed(plan([goal({ successCriteria: [criterion("C001", "pass")] })], { activeGoalId: "G001" }));
|
|
147
|
+
|
|
148
|
+
expect(await ulwLoopCommand(["status", "--json"])).toBe(0);
|
|
149
|
+
const actions = nextActions().join("\n");
|
|
150
|
+
expect(actions).toContain("update_goal complete, then checkpoint --print-template");
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe("#given a review_blocked goal", () => {
|
|
155
|
+
it("#when status --json #then nextActions points at record-review-blockers", async () => {
|
|
156
|
+
await seed(plan([goal({ status: "review_blocked", successCriteria: [criterion("C001", "pass")] })]));
|
|
157
|
+
|
|
158
|
+
expect(await ulwLoopCommand(["status", "--json"])).toBe(0);
|
|
159
|
+
expect(nextActions().join("\n")).toContain("record-review-blockers");
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe("#given an evidence-layout v1 plan", () => {
|
|
164
|
+
it("#when status --json #then currentAttemptDir stays absent and nextActions explains the v1 layout", async () => {
|
|
165
|
+
await seed(legacyLayoutPlan([goal()], { activeGoalId: "G001" }));
|
|
166
|
+
|
|
167
|
+
expect(await ulwLoopCommand(["status", "--json"])).toBe(0);
|
|
168
|
+
expect(stdoutJson()).not.toHaveProperty("currentAttemptDir");
|
|
169
|
+
expect(nextActions().join("\n")).toContain("plan is evidence-layout v1; artifacts go under .omo/evidence/");
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe("#given a valueless --session-id in a subprocess context", () => {
|
|
174
|
+
it("#when any subcommand runs #then the error explains the missing session env and the flag usage", async () => {
|
|
175
|
+
expect(await ulwLoopCommand(["status", "--session-id", "--json"])).toBe(1);
|
|
176
|
+
|
|
177
|
+
const payload = stdoutJson();
|
|
178
|
+
expect(payload).toMatchObject({ ok: false, error: { code: "ULW_LOOP_SESSION_ID_REQUIRED" } });
|
|
179
|
+
const message = String((payload["error"] as Record<string, unknown>)["message"]);
|
|
180
|
+
expect(message).toContain("--session-id <id>");
|
|
181
|
+
expect(message).toContain("subprocess");
|
|
182
|
+
});
|
|
183
|
+
});
|
|
@@ -95,7 +95,7 @@ describe("buildCodexGoalInstruction aggregate mode", () => {
|
|
|
95
95
|
expect(text).toContain("checkpoint");
|
|
96
96
|
});
|
|
97
97
|
|
|
98
|
-
it("#given a final story on the omo-senpi surface #when rendering instructions #then
|
|
98
|
+
it("#given a final story on the omo-senpi surface #when rendering instructions #then teaches the single-gate category chain", () => {
|
|
99
99
|
const { text } = buildCodexGoalInstruction({
|
|
100
100
|
plan: makePlan({ codexGoalMode: "aggregate" }),
|
|
101
101
|
goal: makeGoal(),
|
|
@@ -103,8 +103,18 @@ describe("buildCodexGoalInstruction aggregate mode", () => {
|
|
|
103
103
|
surface: "omo-senpi",
|
|
104
104
|
});
|
|
105
105
|
|
|
106
|
-
expectTextToContainAll(text,
|
|
107
|
-
|
|
106
|
+
expectTextToContainAll(text, [
|
|
107
|
+
"main-session",
|
|
108
|
+
'category: "deep"',
|
|
109
|
+
"unspecified-high",
|
|
110
|
+
"unspecified-low",
|
|
111
|
+
"gateReview.by",
|
|
112
|
+
"category:<name>",
|
|
113
|
+
"--print-template",
|
|
114
|
+
]);
|
|
115
|
+
for (const role of [...FINAL_REVIEW_ROLES, ...SENPI_REVIEW_ROLES]) expect(text).not.toContain(role);
|
|
116
|
+
expect(text).toMatch(/model_unavailable/);
|
|
117
|
+
expect(text).not.toContain("attempted_chain");
|
|
108
118
|
});
|
|
109
119
|
|
|
110
120
|
it("#given a final story with the explicit lazycodex surface #when rendering instructions #then keeps the lazycodex reviewers", () => {
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { mkdir, mkdtemp } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
|
|
6
|
+
import { checkpointUlwLoop } from "../src/checkpoint.js";
|
|
7
|
+
import { ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE } from "../src/goal-status.js";
|
|
8
|
+
import { ulwLoopDir } from "../src/paths.js";
|
|
9
|
+
import { readUlwLoopPlan } from "../src/plan-io.js";
|
|
10
|
+
import { recordFinalReviewBlockers } from "../src/review-blockers.js";
|
|
11
|
+
import { UlwLoopError } from "../src/types.js";
|
|
12
|
+
import { goal, passGoal, plan, repoWith, snapshot } from "./fixtures/checkpoint-builders.js";
|
|
13
|
+
|
|
14
|
+
async function captureError(action: () => Promise<unknown>): Promise<UlwLoopError> {
|
|
15
|
+
try {
|
|
16
|
+
await action();
|
|
17
|
+
} catch (error) {
|
|
18
|
+
expect(error).toBeInstanceOf(UlwLoopError);
|
|
19
|
+
if (error instanceof UlwLoopError) return error;
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
throw new Error("Expected UlwLoopError");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe("#given a codex goal snapshot whose objective differs from the plan", () => {
|
|
26
|
+
it("#when checkpoint reconciles it #then the mismatch carries the verbatim expected and received objectives", async () => {
|
|
27
|
+
const repo = await repoWith(plan([passGoal("G001"), goal({ id: "G002", status: "pending" })]));
|
|
28
|
+
|
|
29
|
+
const error = await captureError(() =>
|
|
30
|
+
checkpointUlwLoop(repo, {
|
|
31
|
+
goalId: "G001",
|
|
32
|
+
status: "complete",
|
|
33
|
+
evidence: "work complete and validation passed",
|
|
34
|
+
codexGoalJson: snapshot("active", "wrong objective"),
|
|
35
|
+
}),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
expect(error.code).toBe("ulw_loop_codex_snapshot_mismatch");
|
|
39
|
+
expect(error.details).toMatchObject({
|
|
40
|
+
expectedObjective: ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE,
|
|
41
|
+
receivedObjective: "wrong objective",
|
|
42
|
+
});
|
|
43
|
+
expect(error.message).toContain(
|
|
44
|
+
"objective must equal the plan's codexObjective exactly — copy the expected value below",
|
|
45
|
+
);
|
|
46
|
+
expect(error.message).toContain(ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("#when record-review-blockers reconciles it #then the same guided payload is emitted", async () => {
|
|
50
|
+
const repo = await repoWith(
|
|
51
|
+
plan([passGoal("G001", { status: "complete" }), goal({ id: "G002", status: "in_progress" })]),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const error = await captureError(() =>
|
|
55
|
+
recordFinalReviewBlockers(repo, {
|
|
56
|
+
goalId: "G002",
|
|
57
|
+
title: "Resolve final review blockers",
|
|
58
|
+
objective: "Address the BLOCK findings",
|
|
59
|
+
evidence: "review verdict: REQUEST_CHANGES",
|
|
60
|
+
codexGoalJson: JSON.stringify({ goal: { objective: "stale objective", status: "active" } }),
|
|
61
|
+
}),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
expect(error.code).toBe("ulw_loop_codex_snapshot_mismatch");
|
|
65
|
+
expect(error.details).toMatchObject({
|
|
66
|
+
expectedObjective: ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE,
|
|
67
|
+
receivedObjective: "stale objective",
|
|
68
|
+
});
|
|
69
|
+
expect(error.message).toContain(
|
|
70
|
+
"objective must equal the plan's codexObjective exactly — copy the expected value below",
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe("#given no ulw-loop plan on disk", () => {
|
|
76
|
+
it("#when the plan is read #then the error names the exact create-goals bootstrap command", async () => {
|
|
77
|
+
const repo = await mkdtemp(join(tmpdir(), "ug-guided-plan-"));
|
|
78
|
+
|
|
79
|
+
const error = await captureError(() => readUlwLoopPlan(repo));
|
|
80
|
+
|
|
81
|
+
expect(error.code).toBe("ULW_LOOP_PLAN_MISSING");
|
|
82
|
+
expect(error.message).toContain('omo-agent-toolkit ulw-loop create-goals --brief "<brief>" --json');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("#when sibling session dirs exist #then the error lists the existing session ids", async () => {
|
|
86
|
+
const repo = await mkdtemp(join(tmpdir(), "ug-guided-plan-"));
|
|
87
|
+
await mkdir(join(ulwLoopDir(repo), "session-alpha"), { recursive: true });
|
|
88
|
+
await mkdir(join(ulwLoopDir(repo), "session-beta"), { recursive: true });
|
|
89
|
+
|
|
90
|
+
const error = await captureError(() => readUlwLoopPlan(repo, { sessionId: "session-gamma" }));
|
|
91
|
+
|
|
92
|
+
expect(error.code).toBe("ULW_LOOP_PLAN_MISSING");
|
|
93
|
+
expect(error.message).toContain("session-alpha");
|
|
94
|
+
expect(error.message).toContain("session-beta");
|
|
95
|
+
expect(error.details).toMatchObject({ existingSessionIds: ["session-alpha", "session-beta"] });
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("#when no session dirs exist #then the error lists no session ids", async () => {
|
|
99
|
+
const repo = await mkdtemp(join(tmpdir(), "ug-guided-plan-"));
|
|
100
|
+
|
|
101
|
+
const error = await captureError(() => readUlwLoopPlan(repo, { sessionId: "session-gamma" }));
|
|
102
|
+
|
|
103
|
+
expect(error.details?.["existingSessionIds"]).toBeUndefined();
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { existsSync, statSync } from "node:fs";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { validateQualityGate } from "../src/quality-gate.js";
|
|
4
|
+
import { UlwLoopError } from "../src/types.js";
|
|
5
|
+
|
|
6
|
+
const opts = { repoRoot: process.cwd(), fs: { existsSync, statSync }, reviewerSurface: "omo-senpi" as const };
|
|
7
|
+
|
|
8
|
+
describe("aggregated quality gate validation", () => {
|
|
9
|
+
it("#given four independent defects #when validating #then reports all defects in one error", () => {
|
|
10
|
+
const gate = {
|
|
11
|
+
manualQa: {
|
|
12
|
+
by: "main-session",
|
|
13
|
+
status: "passed",
|
|
14
|
+
evidence: "placeholder",
|
|
15
|
+
surfaceEvidence: [],
|
|
16
|
+
adversarialCases: [],
|
|
17
|
+
artifactRefs: [
|
|
18
|
+
{ id: "missing", kind: "cli-transcript", description: "artifact", path: "does-not-exist.txt" },
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
gateReview: {
|
|
22
|
+
by: "category:deep",
|
|
23
|
+
recommendation: "REJECT",
|
|
24
|
+
reportPath: "test/fixtures/artifacts/gate-review.md",
|
|
25
|
+
blockers: [],
|
|
26
|
+
},
|
|
27
|
+
iteration: { fullRerun: true, status: "passed", rerunCommands: ["test"], evidence: "rerun" },
|
|
28
|
+
criteriaCoverage: {
|
|
29
|
+
totalCriteria: 1,
|
|
30
|
+
passCount: 1,
|
|
31
|
+
originalIntent: "intent",
|
|
32
|
+
desiredOutcome: "outcome",
|
|
33
|
+
userOutcomeReview: "review",
|
|
34
|
+
adversarialClassesCovered: ["none"],
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
try {
|
|
38
|
+
validateQualityGate(gate, opts);
|
|
39
|
+
throw new Error("expected validation failure");
|
|
40
|
+
} catch (error) {
|
|
41
|
+
expect(error).toBeInstanceOf(UlwLoopError);
|
|
42
|
+
if (!(error instanceof UlwLoopError)) throw error;
|
|
43
|
+
expect(error.code).toBe("ULW_LOOP_QUALITY_GATE_INVALID");
|
|
44
|
+
expect(error.details?.["field"]).toBe("manualQa.evidence");
|
|
45
|
+
expect(error.details?.["fields"]).toEqual(
|
|
46
|
+
expect.arrayContaining([
|
|
47
|
+
{ field: "manualQa.evidence", message: expect.stringContaining("placeholder") },
|
|
48
|
+
{ field: "gateReview.evidence", message: expect.stringContaining("non-empty") },
|
|
49
|
+
{ field: "gateReview.recommendation", message: expect.stringContaining("APPROVE") },
|
|
50
|
+
{ field: "manualQa.artifactRefs[0].path", message: expect.stringContaining("existing") },
|
|
51
|
+
]),
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("#given more than twenty-five defects #when validating #then caps the list and flags truncation", () => {
|
|
57
|
+
const refs = Array.from({ length: 30 }, (_, index) => ({
|
|
58
|
+
id: `ref-${index}`,
|
|
59
|
+
kind: "cli-transcript",
|
|
60
|
+
description: "artifact",
|
|
61
|
+
path: `does-not-exist-${index}.txt`,
|
|
62
|
+
}));
|
|
63
|
+
const gate = {
|
|
64
|
+
manualQa: {
|
|
65
|
+
by: "main-session",
|
|
66
|
+
status: "passed",
|
|
67
|
+
evidence: "real evidence",
|
|
68
|
+
surfaceEvidence: [],
|
|
69
|
+
adversarialCases: [],
|
|
70
|
+
artifactRefs: refs,
|
|
71
|
+
},
|
|
72
|
+
gateReview: {
|
|
73
|
+
by: "category:deep",
|
|
74
|
+
recommendation: "APPROVE",
|
|
75
|
+
reportPath: "test/fixtures/artifacts/gate-review.md",
|
|
76
|
+
evidence: "gate evidence",
|
|
77
|
+
blockers: [],
|
|
78
|
+
},
|
|
79
|
+
iteration: { fullRerun: true, status: "passed", rerunCommands: ["test"], evidence: "rerun" },
|
|
80
|
+
criteriaCoverage: {
|
|
81
|
+
totalCriteria: 1,
|
|
82
|
+
passCount: 1,
|
|
83
|
+
originalIntent: "intent",
|
|
84
|
+
desiredOutcome: "outcome",
|
|
85
|
+
userOutcomeReview: "review",
|
|
86
|
+
adversarialClassesCovered: ["none"],
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
try {
|
|
90
|
+
validateQualityGate(gate, opts);
|
|
91
|
+
throw new Error("expected validation failure");
|
|
92
|
+
} catch (error) {
|
|
93
|
+
expect(error).toBeInstanceOf(UlwLoopError);
|
|
94
|
+
if (!(error instanceof UlwLoopError)) throw error;
|
|
95
|
+
const fields = error.details?.["fields"];
|
|
96
|
+
expect(Array.isArray(fields) && fields.length).toBe(25);
|
|
97
|
+
expect(error.details?.["truncated"]).toBe(true);
|
|
98
|
+
expect(error.message).toContain("25+");
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -85,18 +85,20 @@ function qualityGateError(input: unknown, surface?: "lazycodex" | "omo-senpi"):
|
|
|
85
85
|
throw new Error("Expected UlwLoopError");
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
function senpiGate(): Record<string, unknown> {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
88
|
+
function senpiGate(overrides: Record<string, unknown> = {}): Record<string, unknown> {
|
|
89
|
+
const { codeReview: _codeReview, ...withoutCodeReview } = BASE_GATE;
|
|
90
|
+
return {
|
|
91
|
+
...withoutCodeReview,
|
|
92
|
+
manualQa: { ...BASE_GATE.manualQa, by: "main-session" },
|
|
93
|
+
gateReview: { ...BASE_GATE.gateReview, by: "category:deep" },
|
|
93
94
|
criteriaCoverage: {
|
|
94
95
|
...BASE_GATE.criteriaCoverage,
|
|
95
96
|
originalIntent: "User wanted the senpi surface to accept honest reviewer attribution.",
|
|
96
97
|
desiredOutcome: "A gate naming the omo-senpi reviewers validates on the omo-senpi surface.",
|
|
97
98
|
userOutcomeReview: "The senpi-surface gate passed with truthful reviewer identities.",
|
|
98
99
|
},
|
|
99
|
-
|
|
100
|
+
...overrides,
|
|
101
|
+
};
|
|
100
102
|
}
|
|
101
103
|
|
|
102
104
|
describe("validateQualityGate reviewer roles", () => {
|
|
@@ -124,29 +126,28 @@ describe("validateQualityGate reviewer roles", () => {
|
|
|
124
126
|
expect(() => validateQualityGate(senpiGate(), { reviewerSurface: "omo-senpi" })).not.toThrow();
|
|
125
127
|
});
|
|
126
128
|
|
|
127
|
-
it("#given
|
|
129
|
+
it("#given a codeReview lane #when validated on the omo-senpi surface #then it is rejected", () => {
|
|
128
130
|
const error = qualityGateError(gateWith({}), "omo-senpi");
|
|
129
131
|
expect(error.code).toBe("ULW_LOOP_QUALITY_GATE_INVALID");
|
|
130
|
-
expect(error.message).toContain("
|
|
132
|
+
expect(error.message).toContain("no codeReview lane");
|
|
131
133
|
});
|
|
132
134
|
|
|
133
|
-
it("#given
|
|
135
|
+
it("#given a four-section senpi gate #when validated on the default lazycodex surface #then codeReview is required", () => {
|
|
134
136
|
const error = qualityGateError(senpiGate());
|
|
135
137
|
expect(error.code).toBe("ULW_LOOP_QUALITY_GATE_INVALID");
|
|
136
|
-
expect(error.message).toContain("
|
|
138
|
+
expect(error.message).toContain("codeReview");
|
|
137
139
|
});
|
|
138
140
|
|
|
139
141
|
it("#given swapped omo-senpi reviewer roles #when validated on the omo-senpi surface #then section-specific roles are enforced", () => {
|
|
140
|
-
const swapped =
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
gateReview: { ...BASE_GATE.gateReview, by: "omo-senpi-gate-reviewer" },
|
|
142
|
+
const swapped = senpiGate({
|
|
143
|
+
manualQa: { ...BASE_GATE.manualQa, by: "category:deep" },
|
|
144
|
+
gateReview: { ...BASE_GATE.gateReview, by: "main-session" },
|
|
144
145
|
});
|
|
145
146
|
|
|
146
147
|
const error = qualityGateError(swapped, "omo-senpi");
|
|
147
148
|
|
|
148
149
|
expect(error.code).toBe("ULW_LOOP_QUALITY_GATE_INVALID");
|
|
149
|
-
expect(error.message).toContain("
|
|
150
|
+
expect(error.message).toContain("manualQa.by");
|
|
150
151
|
});
|
|
151
152
|
|
|
152
153
|
it("#given swapped LazyCodex reviewer roles #when validated #then section-specific roles are enforced", () => {
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
|
|
5
|
+
import { validateQualityGate } from "../src/quality-gate.js";
|
|
6
|
+
import { UlwLoopError } from "../src/types.js";
|
|
7
|
+
|
|
8
|
+
const COMMON = {
|
|
9
|
+
manualQa: {
|
|
10
|
+
status: "passed",
|
|
11
|
+
evidence: "Ran senpi manual QA with artifact-backed evidence.",
|
|
12
|
+
surfaceEvidence: [
|
|
13
|
+
{
|
|
14
|
+
id: "surface-cli-pass",
|
|
15
|
+
criterionRef: "C1",
|
|
16
|
+
surface: "cli",
|
|
17
|
+
invocation: "omo-agent-toolkit ulw-loop checkpoint --status complete",
|
|
18
|
+
verdict: "passed",
|
|
19
|
+
artifactRefs: ["artifact-cli-pass"],
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
adversarialCases: [
|
|
23
|
+
{
|
|
24
|
+
id: "adv-role",
|
|
25
|
+
criterionRef: "C2",
|
|
26
|
+
scenario: "a reviewer role is invalid",
|
|
27
|
+
expectedBehavior: "the gate rejects the invalid role",
|
|
28
|
+
verdict: "passed",
|
|
29
|
+
artifactRefs: ["artifact-cli-pass"],
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
artifactRefs: [
|
|
33
|
+
{
|
|
34
|
+
id: "artifact-cli-pass",
|
|
35
|
+
kind: "cli-transcript",
|
|
36
|
+
description: "CLI transcript for valid checkpoint.",
|
|
37
|
+
path: "cli-pass.txt",
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
iteration: {
|
|
42
|
+
fullRerun: true,
|
|
43
|
+
status: "passed",
|
|
44
|
+
rerunCommands: ["bunx vitest run"],
|
|
45
|
+
evidence: "Focused tests passed.",
|
|
46
|
+
},
|
|
47
|
+
criteriaCoverage: {
|
|
48
|
+
totalCriteria: 2,
|
|
49
|
+
passCount: 2,
|
|
50
|
+
originalIntent: "User wanted a valid senpi quality gate.",
|
|
51
|
+
desiredOutcome: "The senpi gate accepts its four required lanes.",
|
|
52
|
+
userOutcomeReview: "The requested senpi outcome is covered.",
|
|
53
|
+
adversarialClassesCovered: ["role_mismatch"],
|
|
54
|
+
},
|
|
55
|
+
} as const;
|
|
56
|
+
|
|
57
|
+
function senpiGate(overrides: Record<string, unknown> = {}): Record<string, unknown> {
|
|
58
|
+
return {
|
|
59
|
+
...COMMON,
|
|
60
|
+
manualQa: { ...COMMON.manualQa, by: "main-session" },
|
|
61
|
+
gateReview: {
|
|
62
|
+
by: "category:deep",
|
|
63
|
+
recommendation: "APPROVE",
|
|
64
|
+
reportPath: "gate-review.md",
|
|
65
|
+
evidence: "Verified the senpi gate.",
|
|
66
|
+
blockers: [],
|
|
67
|
+
},
|
|
68
|
+
...overrides,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function section(input: Record<string, unknown>, key: string): Record<string, unknown> {
|
|
73
|
+
const value = input[key];
|
|
74
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
75
|
+
throw new Error(`missing object section: ${key}`);
|
|
76
|
+
return value as Record<string, unknown>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function errorFor(input: unknown): UlwLoopError {
|
|
80
|
+
try {
|
|
81
|
+
validateQualityGate(input, { reviewerSurface: "omo-senpi" });
|
|
82
|
+
} catch (error) {
|
|
83
|
+
if (error instanceof UlwLoopError) return error;
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
throw new Error("Expected quality gate validation to fail");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
describe("validateQualityGate omo-senpi surface", () => {
|
|
90
|
+
it("#given a four-section senpi gate #when validated #then it accepts main-session manual QA and category:deep review", () => {
|
|
91
|
+
expect(() => validateQualityGate(senpiGate(), { reviewerSurface: "omo-senpi" })).not.toThrow();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("#given a senpi gate with codeReview #when validated #then it rejects the forbidden codeReview lane", () => {
|
|
95
|
+
const error = errorFor(senpiGate({ codeReview: { by: "anything" } }));
|
|
96
|
+
expect(error.details?.["field"]).toContain("codeReview");
|
|
97
|
+
expect(error.message).toContain("omo-senpi gate has no codeReview lane");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it.each(["category:deep", "category:unspecified-high", "category:unspecified-low"])(
|
|
101
|
+
"#given gateReview.by %s #when validated on senpi #then it accepts the category role",
|
|
102
|
+
(by) => {
|
|
103
|
+
expect(() =>
|
|
104
|
+
validateQualityGate(senpiGate({ gateReview: { ...section(senpiGate(), "gateReview"), by } }), {
|
|
105
|
+
reviewerSurface: "omo-senpi",
|
|
106
|
+
}),
|
|
107
|
+
).not.toThrow();
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
it.each(["omo-senpi-gate-reviewer", "lazycodex-gate-reviewer", "arbitrary"])(
|
|
112
|
+
"#given gateReview.by %s #when validated on senpi #then it rejects the non-category role",
|
|
113
|
+
(by) => {
|
|
114
|
+
const error = errorFor(senpiGate({ gateReview: { ...section(senpiGate(), "gateReview"), by } }));
|
|
115
|
+
expect(error.message).toContain("gateReview.by");
|
|
116
|
+
},
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
it.each(["omo-senpi-qa-executor", "lazycodex-qa-executor", "arbitrary"])(
|
|
120
|
+
"#given manualQa.by %s #when validated on senpi #then it rejects the non-main-session role",
|
|
121
|
+
(by) => {
|
|
122
|
+
const error = errorFor(senpiGate({ manualQa: { ...COMMON.manualQa, by } }));
|
|
123
|
+
expect(error.message).toContain("manualQa.by");
|
|
124
|
+
},
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
it("#given the existing lazycodex fixture #when validated on the default surface #then it remains valid", async () => {
|
|
128
|
+
const raw = await readFile(new URL("./fixtures/sample-quality-gate.json", import.meta.url), "utf8");
|
|
129
|
+
expect(() => validateQualityGate(JSON.parse(raw))).not.toThrow();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -111,7 +111,10 @@ describe("validateQualityGate", () => {
|
|
|
111
111
|
"gateReview",
|
|
112
112
|
"iteration",
|
|
113
113
|
"manualQa",
|
|
114
|
+
"surface",
|
|
114
115
|
]);
|
|
116
|
+
expect("codeReview" in gate).toBe(true);
|
|
117
|
+
if (!("codeReview" in gate)) throw new Error("expected lazycodex codeReview section");
|
|
115
118
|
expect(gate.codeReview.codeQualityStatus).toBe("CLEAR");
|
|
116
119
|
expect(gate).toMatchObject({
|
|
117
120
|
criteriaCoverage: { totalCriteria: 9, passCount: 9, userOutcomeReview: expect.stringContaining("user") },
|
|
@@ -127,6 +130,8 @@ describe("validateQualityGate", () => {
|
|
|
127
130
|
const gate = validateQualityGate(parsed, FS_OPTS);
|
|
128
131
|
|
|
129
132
|
// then
|
|
133
|
+
expect("codeReview" in gate).toBe(true);
|
|
134
|
+
if (!("codeReview" in gate)) throw new Error("expected lazycodex codeReview section");
|
|
130
135
|
expect(gate.codeReview.recommendation).toBe("APPROVE");
|
|
131
136
|
expect(gate.manualQa.artifactRefs).toHaveLength(5);
|
|
132
137
|
});
|