oh-my-opencode 5.0.0-beta.30 → 5.0.0-beta.31
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 +75 -26
- package/dist/cli-node/index.js +75 -26
- package/dist/config/schema/background-task.d.ts +1 -0
- package/dist/config/schema/oh-my-opencode-config.d.ts +1 -0
- package/dist/features/background-agent/constants.d.ts +0 -7
- package/dist/features/background-agent/manager.d.ts +2 -1
- package/dist/features/background-agent/parent-wake-dispatched-tracker.d.ts +0 -1
- package/dist/features/background-agent/subagent-spawn-limits.d.ts +8 -0
- package/dist/features/task-toast-manager/manager.d.ts +0 -24
- package/dist/hooks/anthropic-context-window-limit-recovery/message-storage-directory.d.ts +0 -3
- package/dist/hooks/anthropic-context-window-limit-recovery/state.d.ts +1 -0
- package/dist/hooks/anthropic-context-window-limit-recovery/storage/empty-messages.d.ts +0 -1
- package/dist/hooks/claude-code-hooks/session-hook-state.d.ts +1 -0
- package/dist/hooks/claude-code-hooks/stop.d.ts +2 -0
- package/dist/hooks/claude-code-hooks/todo.d.ts +0 -1
- package/dist/hooks/claude-code-hooks/transcript.d.ts +2 -0
- package/dist/hooks/fsync-skip-warning/index.d.ts +4 -0
- package/dist/hooks/hashline-edit-diff-enhancer/hook.d.ts +1 -0
- package/dist/hooks/hashline-edit-diff-enhancer/pending-captures.d.ts +13 -0
- package/dist/hooks/keyword-detector/hook.d.ts +7 -0
- package/dist/hooks/keyword-detector/ultrawork/source-detector.d.ts +0 -1
- package/dist/hooks/model-fallback/hook.d.ts +1 -1
- package/dist/index.js +439 -218
- package/dist/oh-my-opencode.schema.json +5 -0
- package/dist/plugin-dispose.d.ts +3 -0
- package/dist/shared/agent-tool-restrictions.d.ts +0 -1
- package/dist/tools/delegate-task/sync-session-cleanup.d.ts +3 -0
- package/dist/tools/delegate-task/sync-session-poller.d.ts +1 -0
- package/dist/tools/delegate-task/types.d.ts +1 -0
- package/dist/tui.js +56 -3
- package/package.json +15 -15
- package/packages/git-bash-mcp/dist/cli.js +1 -0
- package/packages/lsp-core/src/lsp/client-wrapper.test.ts +97 -0
- package/packages/lsp-core/src/lsp/client-wrapper.ts +21 -6
- package/packages/lsp-core/src/lsp/constants.ts +5 -0
- package/packages/lsp-core/src/lsp/errors.ts +15 -0
- package/packages/lsp-core/src/lsp/manager-lifecycle.test.ts +262 -0
- package/packages/lsp-core/src/lsp/manager.ts +106 -13
- package/packages/lsp-core/src/lsp/workspace-markers.ts +18 -1
- package/packages/lsp-core/src/mcp.ts +3 -0
- package/packages/lsp-daemon/dist/cli.js +114 -18
- package/packages/lsp-daemon/dist/client.js +114 -18
- package/packages/lsp-daemon/dist/index.js +114 -18
- package/packages/lsp-daemon/dist/proxy.js +3 -0
- package/packages/lsp-tools-mcp/dist/cli.js +114 -18
- package/packages/lsp-tools-mcp/dist/lsp/manager.js +93 -12
- package/packages/lsp-tools-mcp/dist/mcp.js +114 -18
- package/packages/lsp-tools-mcp/dist/tools.js +113 -18
- 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 +31 -2
- package/packages/omo-codex/plugin/components/codegraph/dist/serve.js +31 -2
- package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
- package/packages/omo-codex/plugin/components/codegraph/src/mcp-unavailable.ts +12 -0
- package/packages/omo-codex/plugin/components/codegraph/test/serve-unavailable-idle.test.ts +47 -0
- package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
- package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
- package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +3 -3
- package/packages/omo-codex/plugin/components/lsp/dist/cli.js +113 -18
- 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/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -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 +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.js +2 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +100 -54
- package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-instruction.d.ts +2 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-instruction.js +9 -6
- package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate.d.ts +4 -2
- package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate.js +7 -10
- package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-guard.js +3 -2
- package/packages/omo-codex/plugin/components/ulw-loop/dist/surface.d.ts +15 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/surface.js +50 -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/src/checkpoint.ts +2 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-instruction.ts +16 -5
- package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate.ts +11 -14
- package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-guard.ts +3 -2
- package/packages/omo-codex/plugin/components/ulw-loop/src/surface.ts +73 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint-final.test.ts +44 -1
- package/packages/omo-codex/plugin/components/ulw-loop/test/codex-goal-instruction.test.ts +25 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/quality-gate-builder.ts +7 -3
- package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-roles.test.ts +45 -2
- package/packages/omo-codex/plugin/components/ulw-loop/test/spawn-guard.test.ts +20 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/surface.test.ts +74 -0
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +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/scripts/install-dist/install-local.mjs +2 -2
- package/dist/hooks/ralph-loop/completion-promise-detector-test-input.d.ts +0 -11
- package/dist/hooks/ralph-loop/loop-session-recovery.d.ts +0 -7
- package/dist/tools/call-omo-agent/message-storage-directory.d.ts +0 -1
- package/dist/tools/delegate-task/model-string-parser.d.ts +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/cleanup-errors.d.ts +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/constants.d.ts +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/language-mappings.d.ts +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/process-signal-cleanup.d.ts +0 -1
- package/packages/lsp-tools-mcp/dist/missing-dependency-result.d.ts +0 -1
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
export const REVIEWER_ROLES_BY_SURFACE = {
|
|
5
|
+
lazycodex: {
|
|
6
|
+
codeReview: "lazycodex-code-reviewer",
|
|
7
|
+
manualQa: "lazycodex-qa-executor",
|
|
8
|
+
gateReview: "lazycodex-gate-reviewer",
|
|
9
|
+
},
|
|
10
|
+
"omo-senpi": {
|
|
11
|
+
codeReview: "omo-senpi-code-reviewer",
|
|
12
|
+
manualQa: "omo-senpi-qa-executor",
|
|
13
|
+
gateReview: "omo-senpi-gate-reviewer",
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
export const GATE_REVIEWER_AGENT_NAMES = new Set(Object.values(REVIEWER_ROLES_BY_SURFACE).map((roles) => roles.gateReview));
|
|
17
|
+
export function reviewerRolesFor(surface) {
|
|
18
|
+
return REVIEWER_ROLES_BY_SURFACE[surface];
|
|
19
|
+
}
|
|
20
|
+
export const SURFACE_MARKER_FILENAME = "surface.json";
|
|
21
|
+
const SURFACE_ENV_KEY = "OMO_AGENT_TOOLKIT_SURFACE";
|
|
22
|
+
function parseSurface(value) {
|
|
23
|
+
return value === "lazycodex" || value === "omo-senpi" ? value : null;
|
|
24
|
+
}
|
|
25
|
+
// Resolution order: explicit env override (tests, doctor probes) -> staged surface.json marker
|
|
26
|
+
// sitting next to the running bundle -> lazycodex default. The marker is distribution-baked, so a
|
|
27
|
+
// malformed marker falls back to the default instead of crashing checkpoint validation; the env
|
|
28
|
+
// override remains available to force the intended surface on a damaged install.
|
|
29
|
+
export function resolveToolkitSurface(options) {
|
|
30
|
+
const env = options?.env ?? process.env;
|
|
31
|
+
const fromEnv = parseSurface(env[SURFACE_ENV_KEY]);
|
|
32
|
+
if (fromEnv !== null)
|
|
33
|
+
return fromEnv;
|
|
34
|
+
const entryDir = options?.entryDir ?? dirname(fileURLToPath(import.meta.url));
|
|
35
|
+
const markerPath = join(entryDir, SURFACE_MARKER_FILENAME);
|
|
36
|
+
return readMarkerSurface(markerPath) ?? "lazycodex";
|
|
37
|
+
}
|
|
38
|
+
function readMarkerSurface(markerPath) {
|
|
39
|
+
try {
|
|
40
|
+
if (!existsSync(markerPath))
|
|
41
|
+
return null;
|
|
42
|
+
const parsed = JSON.parse(readFileSync(markerPath, "utf8"));
|
|
43
|
+
return parseSurface(parsed["surface"]);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (error instanceof Error)
|
|
47
|
+
return null;
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"type": "command",
|
|
8
8
|
"command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook user-prompt-submit --with-ultrawork",
|
|
9
9
|
"timeout": 10,
|
|
10
|
-
"statusMessage": "(OmO 5.0.0-beta.
|
|
10
|
+
"statusMessage": "(OmO 5.0.0-beta.31) Checking Ulw-Loop Steering"
|
|
11
11
|
}
|
|
12
12
|
]
|
|
13
13
|
}
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"type": "command",
|
|
21
21
|
"command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook pre-tool-use",
|
|
22
22
|
"timeout": 5,
|
|
23
|
-
"statusMessage": "(OmO 5.0.0-beta.
|
|
23
|
+
"statusMessage": "(OmO 5.0.0-beta.31) Enforcing Unlimited Ulw-Loop Budget"
|
|
24
24
|
}
|
|
25
25
|
]
|
|
26
26
|
},
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"type": "command",
|
|
32
32
|
"command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook pre-tool-use-spawn",
|
|
33
33
|
"timeout": 5,
|
|
34
|
-
"statusMessage": "(OmO 5.0.0-beta.
|
|
34
|
+
"statusMessage": "(OmO 5.0.0-beta.31) Guarding Ulw-Loop Spawns"
|
|
35
35
|
}
|
|
36
36
|
]
|
|
37
37
|
}
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"type": "command",
|
|
44
44
|
"command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook stop",
|
|
45
45
|
"timeout": 10,
|
|
46
|
-
"statusMessage": "(OmO 5.0.0-beta.
|
|
46
|
+
"statusMessage": "(OmO 5.0.0-beta.31) Checking Ulw-Loop Resume"
|
|
47
47
|
}
|
|
48
48
|
]
|
|
49
49
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@code-yeongyu/codex-ulw-loop",
|
|
3
|
-
"version": "5.0.0-beta.
|
|
3
|
+
"version": "5.0.0-beta.31",
|
|
4
4
|
"description": "Codex plugin: durable repo-native multi-goal orchestration with embedded success criteria and observable evidence audit.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "npm@11.12.1",
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
sameBlockerOccurrences,
|
|
29
29
|
validateQualityGate,
|
|
30
30
|
} from "./quality-gate.js";
|
|
31
|
+
import { resolveToolkitSurface } from "./surface.js";
|
|
31
32
|
import type {
|
|
32
33
|
UlwLoopAggregateCompletion,
|
|
33
34
|
UlwLoopItem,
|
|
@@ -230,6 +231,7 @@ export async function checkpointUlwLoop(
|
|
|
230
231
|
qualityGate = validateQualityGate(await readJsonInput(args.qualityGateJson, repoRoot), {
|
|
231
232
|
repoRoot,
|
|
232
233
|
fs: QUALITY_GATE_FS,
|
|
234
|
+
reviewerSurface: resolveToolkitSurface(),
|
|
233
235
|
...(plan.evidenceLayoutVersion === 2
|
|
234
236
|
? { currentAttemptDir: ulwLoopAttemptEvidenceDir(goal.id, goal.attempt, scope) }
|
|
235
237
|
: {}),
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
isEssentialCriterion,
|
|
6
6
|
isFinalRunCompletionCandidate,
|
|
7
7
|
} from "./goal-status.js";
|
|
8
|
+
import { resolveToolkitSurface, reviewerRolesFor, type UlwLoopToolkitSurface } from "./surface.js";
|
|
8
9
|
import type { UlwLoopCodexGoalMode, UlwLoopItem, UlwLoopPlan, UlwLoopSuccessCriterion } from "./types.js";
|
|
9
10
|
|
|
10
11
|
export interface CodexCreateGoalPayload {
|
|
@@ -20,11 +21,13 @@ export function buildCodexGoalInstruction(args: {
|
|
|
20
21
|
readonly plan: UlwLoopPlan;
|
|
21
22
|
readonly goal: UlwLoopItem;
|
|
22
23
|
readonly isFinal?: boolean;
|
|
24
|
+
readonly surface?: UlwLoopToolkitSurface;
|
|
23
25
|
}): UlwLoopGoalInstruction {
|
|
24
26
|
const mode = codexGoalMode(args.plan);
|
|
25
27
|
const createGoal = buildCreateGoalPayload(args.plan, args.goal);
|
|
26
28
|
const isFinal = args.isFinal ?? isFinalRunCompletionCandidate(args.plan, args.goal);
|
|
27
|
-
|
|
29
|
+
const surface = args.surface ?? resolveToolkitSurface();
|
|
30
|
+
return { text: buildText(mode, args.plan, args.goal, createGoal, isFinal, surface), json: createGoal };
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
function buildCreateGoalPayload(plan: UlwLoopPlan, goal: UlwLoopItem): CodexCreateGoalPayload {
|
|
@@ -37,6 +40,7 @@ function buildText(
|
|
|
37
40
|
goal: UlwLoopItem,
|
|
38
41
|
createGoal: CodexCreateGoalPayload,
|
|
39
42
|
isFinal: boolean,
|
|
43
|
+
surface: UlwLoopToolkitSurface,
|
|
40
44
|
): string {
|
|
41
45
|
return joinLines([
|
|
42
46
|
mode === "aggregate" ? "UlwLoop aggregate-goal handoff" : "UlwLoop active-goal handoff",
|
|
@@ -54,7 +58,7 @@ function buildText(
|
|
|
54
58
|
"- Goals are unlimited. Do not add numeric limits.",
|
|
55
59
|
...modeConstraintLines(mode, isFinal),
|
|
56
60
|
...evidenceLayoutLines(plan),
|
|
57
|
-
finalSection(plan, goal, isFinal, mode === "aggregate"),
|
|
61
|
+
finalSection(plan, goal, isFinal, mode === "aggregate", surface),
|
|
58
62
|
...checkpointLines(plan, mode),
|
|
59
63
|
"",
|
|
60
64
|
"create_goal payload:",
|
|
@@ -112,7 +116,14 @@ function evidenceLayoutLines(plan: UlwLoopPlan): string[] {
|
|
|
112
116
|
];
|
|
113
117
|
}
|
|
114
118
|
|
|
115
|
-
function finalSection(
|
|
119
|
+
function finalSection(
|
|
120
|
+
plan: UlwLoopPlan,
|
|
121
|
+
goal: UlwLoopItem,
|
|
122
|
+
isFinal: boolean,
|
|
123
|
+
aggregate: boolean,
|
|
124
|
+
surface: UlwLoopToolkitSurface,
|
|
125
|
+
): string {
|
|
126
|
+
const roles = reviewerRolesFor(surface);
|
|
116
127
|
if (!isFinal)
|
|
117
128
|
return "- This is not the final ulw-loop story; do not run the final reviewer/manual-QA/gate-review quality gate yet.";
|
|
118
129
|
const option = sessionOption(plan);
|
|
@@ -122,8 +133,8 @@ function finalSection(plan: UlwLoopPlan, goal: UlwLoopItem, isFinal: boolean, ag
|
|
|
122
133
|
"Final story — run mandatory quality gate before update_goal:",
|
|
123
134
|
"- Run targeted verification for changed behavior.",
|
|
124
135
|
"- Confirm every manualQa artifact path exists and has non-zero size.",
|
|
125
|
-
|
|
126
|
-
|
|
136
|
+
`- First spawn ${roles.codeReview} and ${roles.manualQa} in parallel (fork_context: false on the v1 surface; fork_turns: "none" on v2). Include the original brief, goal objectives, desired user-visible outcome, diff, and evidence; wait for BOTH to return and confirm their report artifacts exist on disk (code-review report + manualQa matrix).`,
|
|
137
|
+
`- Only then spawn ${roles.gateReview} (same fork settings), passing those artifact paths.`,
|
|
127
138
|
"- Require clean codeReview, manualQa, gateReview, iteration, and criteriaCoverage. criteriaCoverage must summarize originalIntent, desiredOutcome, and userOutcomeReview; counts alone are not approval.",
|
|
128
139
|
"- On a reviewer REJECT, fix only the cited blockers, rerun the affected verification/Manual-QA, and re-review the delta at most TWICE; if blockers remain, record them and surface to the user.",
|
|
129
140
|
"- If codeQualityStatus is WATCH, include the WATCH notes verbatim in your final user-facing message.",
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
textField,
|
|
11
11
|
} from "./quality-gate-fields.js";
|
|
12
12
|
import { adversarialVerdict, codeQualityStatusField, passedVerdict } from "./quality-gate-verdicts.js";
|
|
13
|
+
import { reviewerRolesFor, type UlwLoopToolkitSurface } from "./surface.js";
|
|
13
14
|
import type {
|
|
14
15
|
UlwLoopManualQaArtifactKind,
|
|
15
16
|
UlwLoopManualQaArtifactRef,
|
|
@@ -17,12 +18,6 @@ import type {
|
|
|
17
18
|
UlwLoopQualityGate,
|
|
18
19
|
} from "./types.js";
|
|
19
20
|
|
|
20
|
-
const REVIEWER_ROLES = {
|
|
21
|
-
codeReview: "lazycodex-code-reviewer",
|
|
22
|
-
manualQa: "lazycodex-qa-executor",
|
|
23
|
-
gateReview: "lazycodex-gate-reviewer",
|
|
24
|
-
} as const;
|
|
25
|
-
|
|
26
21
|
export {
|
|
27
22
|
classifyExternalAuthorizationBlocker,
|
|
28
23
|
clearGoalBlockerFields,
|
|
@@ -36,12 +31,13 @@ export interface QualityGateFs {
|
|
|
36
31
|
}
|
|
37
32
|
|
|
38
33
|
export interface ValidateQualityGateOptions {
|
|
39
|
-
readonly repoRoot
|
|
40
|
-
readonly fs
|
|
34
|
+
readonly repoRoot?: string;
|
|
35
|
+
readonly fs?: QualityGateFs;
|
|
41
36
|
readonly currentAttemptDir?: string;
|
|
37
|
+
readonly reviewerSurface?: UlwLoopToolkitSurface;
|
|
42
38
|
}
|
|
43
39
|
|
|
44
|
-
function reviewerRoleField
|
|
40
|
+
function reviewerRoleField(value: unknown, expected: string, field: string): string {
|
|
45
41
|
const actual = textField(value, field);
|
|
46
42
|
if (actual !== expected) invalid(`${field} must be ${expected}.`, field);
|
|
47
43
|
return expected;
|
|
@@ -91,11 +87,11 @@ function artifactCompatible(surface: UlwLoopManualQaSurface, kind: UlwLoopManual
|
|
|
91
87
|
}
|
|
92
88
|
|
|
93
89
|
function checkFile(path: string, field: string, opts?: ValidateQualityGateOptions): void {
|
|
94
|
-
if (opts === undefined) return;
|
|
90
|
+
if (opts?.repoRoot === undefined || opts.fs === undefined) return;
|
|
95
91
|
const absolute = resolve(opts.repoRoot, path);
|
|
96
92
|
if (!opts.fs.existsSync(absolute)) invalid(`${field} must point to an existing artifact.`, field);
|
|
97
93
|
if (opts.fs.statSync(absolute).size <= 0) invalid(`${field} must point to a non-empty artifact.`, field);
|
|
98
|
-
if (opts.currentAttemptDir !== undefined) {
|
|
94
|
+
if (opts.currentAttemptDir !== undefined && opts.repoRoot !== undefined) {
|
|
99
95
|
const attemptRoot = resolve(opts.repoRoot, opts.currentAttemptDir);
|
|
100
96
|
if (!isWithinAttemptDir(absolute, attemptRoot))
|
|
101
97
|
invalid(
|
|
@@ -143,6 +139,7 @@ function referencedArtifacts(
|
|
|
143
139
|
}
|
|
144
140
|
|
|
145
141
|
export function validateQualityGate(input: unknown, opts?: ValidateQualityGateOptions): UlwLoopQualityGate {
|
|
142
|
+
const reviewerRoles = reviewerRolesFor(opts?.reviewerSurface ?? "lazycodex");
|
|
146
143
|
const gate = section(input, "qualityGate");
|
|
147
144
|
const codeReview = section(gate["codeReview"], "codeReview");
|
|
148
145
|
const manualQa = section(gate["manualQa"], "manualQa");
|
|
@@ -163,7 +160,7 @@ export function validateQualityGate(input: unknown, opts?: ValidateQualityGateOp
|
|
|
163
160
|
checkFile(gateReportPath, "gateReview.reportPath", opts);
|
|
164
161
|
return {
|
|
165
162
|
codeReview: {
|
|
166
|
-
by: reviewerRoleField(codeReview["by"],
|
|
163
|
+
by: reviewerRoleField(codeReview["by"], reviewerRoles.codeReview, "codeReview.by"),
|
|
167
164
|
recommendation: literal(codeReview["recommendation"], "APPROVE", "codeReview.recommendation"),
|
|
168
165
|
codeQualityStatus: codeQualityStatusField(codeReview["codeQualityStatus"], "codeReview.codeQualityStatus"),
|
|
169
166
|
reportPath: codeReportPath,
|
|
@@ -171,7 +168,7 @@ export function validateQualityGate(input: unknown, opts?: ValidateQualityGateOp
|
|
|
171
168
|
blockers: emptyBlockers(codeReview["blockers"], "codeReview.blockers"),
|
|
172
169
|
},
|
|
173
170
|
manualQa: {
|
|
174
|
-
by: reviewerRoleField(manualQa["by"],
|
|
171
|
+
by: reviewerRoleField(manualQa["by"], reviewerRoles.manualQa, "manualQa.by"),
|
|
175
172
|
status: literal(manualQa["status"], "passed", "manualQa.status"),
|
|
176
173
|
evidence: textField(manualQa["evidence"], "manualQa.evidence"),
|
|
177
174
|
surfaceEvidence,
|
|
@@ -179,7 +176,7 @@ export function validateQualityGate(input: unknown, opts?: ValidateQualityGateOp
|
|
|
179
176
|
artifactRefs,
|
|
180
177
|
},
|
|
181
178
|
gateReview: {
|
|
182
|
-
by: reviewerRoleField(gateReview["by"],
|
|
179
|
+
by: reviewerRoleField(gateReview["by"], reviewerRoles.gateReview, "gateReview.by"),
|
|
183
180
|
recommendation: literal(gateReview["recommendation"], "APPROVE", "gateReview.recommendation"),
|
|
184
181
|
reportPath: gateReportPath,
|
|
185
182
|
evidence: textField(gateReview["evidence"], "gateReview.evidence"),
|
|
@@ -5,6 +5,7 @@ import type { PreToolUsePayload } from "./codex-hook.js";
|
|
|
5
5
|
import { parsePreToolUsePayload } from "./codex-hook.js";
|
|
6
6
|
import { isFinalRunCompletionCandidate } from "./goal-status.js";
|
|
7
7
|
import { ulwLoopAttemptEvidenceDir, ulwLoopDir } from "./paths.js";
|
|
8
|
+
import { GATE_REVIEWER_AGENT_NAMES } from "./surface.js";
|
|
8
9
|
import type { UlwLoopPlan } from "./types.js";
|
|
9
10
|
|
|
10
11
|
// spawn_agent = v1; collaborationspawn_agent = the delimiter-free flattened v2
|
|
@@ -12,7 +13,7 @@ import type { UlwLoopPlan } from "./types.js";
|
|
|
12
13
|
// dotted token observed live in the task-1 probe (hook-tool-tokens.txt).
|
|
13
14
|
const SPAWN_TOOL_TOKENS = new Set(["spawn_agent", "collaborationspawn_agent", "collaboration.spawn_agent"]);
|
|
14
15
|
const DEFAULT_FANOUT_LIMIT = 60;
|
|
15
|
-
const GATE_MESSAGE_PATTERN = /lazycodex-gate-reviewer|final gate review/i;
|
|
16
|
+
const GATE_MESSAGE_PATTERN = /lazycodex-gate-reviewer|omo-senpi-gate-reviewer|final gate review/i;
|
|
16
17
|
|
|
17
18
|
export function applySpawnGuards(payload: PreToolUsePayload): string {
|
|
18
19
|
if (payload.hook_event_name !== "PreToolUse" || !SPAWN_TOOL_TOKENS.has(payload.tool_name)) return "";
|
|
@@ -77,7 +78,7 @@ function isGateReviewerSpawn(toolInput: unknown): boolean {
|
|
|
77
78
|
if (typeof toolInput !== "object" || toolInput === null) return false;
|
|
78
79
|
const record = toolInput as Record<string, unknown>;
|
|
79
80
|
const agentType = record["agent_type"];
|
|
80
|
-
if (typeof agentType === "string") return agentType
|
|
81
|
+
if (typeof agentType === "string") return GATE_REVIEWER_AGENT_NAMES.has(agentType);
|
|
81
82
|
const message = record["message"];
|
|
82
83
|
return typeof message === "string" && GATE_MESSAGE_PATTERN.test(message);
|
|
83
84
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
// The agent toolkit ships on two surfaces: the LazyCodex (Codex CLI) plugin runs the component
|
|
6
|
+
// bundle directly, while omo-senpi stages the same bundle through stage-agent-toolkit.mjs. The
|
|
7
|
+
// reviewer identities the final quality gate enforces differ per surface, so the staging script
|
|
8
|
+
// bakes a `surface.json` marker next to the staged bundle and this module resolves it. Without a
|
|
9
|
+
// marker (the Codex layout) the surface stays "lazycodex".
|
|
10
|
+
export type UlwLoopToolkitSurface = "lazycodex" | "omo-senpi";
|
|
11
|
+
|
|
12
|
+
export interface UlwLoopReviewerRoles {
|
|
13
|
+
readonly codeReview: string;
|
|
14
|
+
readonly manualQa: string;
|
|
15
|
+
readonly gateReview: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const REVIEWER_ROLES_BY_SURFACE: Readonly<Record<UlwLoopToolkitSurface, UlwLoopReviewerRoles>> = {
|
|
19
|
+
lazycodex: {
|
|
20
|
+
codeReview: "lazycodex-code-reviewer",
|
|
21
|
+
manualQa: "lazycodex-qa-executor",
|
|
22
|
+
gateReview: "lazycodex-gate-reviewer",
|
|
23
|
+
},
|
|
24
|
+
"omo-senpi": {
|
|
25
|
+
codeReview: "omo-senpi-code-reviewer",
|
|
26
|
+
manualQa: "omo-senpi-qa-executor",
|
|
27
|
+
gateReview: "omo-senpi-gate-reviewer",
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const GATE_REVIEWER_AGENT_NAMES: ReadonlySet<string> = new Set(
|
|
32
|
+
Object.values(REVIEWER_ROLES_BY_SURFACE).map((roles) => roles.gateReview),
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
export function reviewerRolesFor(surface: UlwLoopToolkitSurface): UlwLoopReviewerRoles {
|
|
36
|
+
return REVIEWER_ROLES_BY_SURFACE[surface];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const SURFACE_MARKER_FILENAME = "surface.json";
|
|
40
|
+
const SURFACE_ENV_KEY = "OMO_AGENT_TOOLKIT_SURFACE";
|
|
41
|
+
|
|
42
|
+
function parseSurface(value: unknown): UlwLoopToolkitSurface | null {
|
|
43
|
+
return value === "lazycodex" || value === "omo-senpi" ? value : null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ResolveToolkitSurfaceOptions {
|
|
47
|
+
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
48
|
+
readonly entryDir?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Resolution order: explicit env override (tests, doctor probes) -> staged surface.json marker
|
|
52
|
+
// sitting next to the running bundle -> lazycodex default. The marker is distribution-baked, so a
|
|
53
|
+
// malformed marker falls back to the default instead of crashing checkpoint validation; the env
|
|
54
|
+
// override remains available to force the intended surface on a damaged install.
|
|
55
|
+
export function resolveToolkitSurface(options?: ResolveToolkitSurfaceOptions): UlwLoopToolkitSurface {
|
|
56
|
+
const env = options?.env ?? process.env;
|
|
57
|
+
const fromEnv = parseSurface(env[SURFACE_ENV_KEY]);
|
|
58
|
+
if (fromEnv !== null) return fromEnv;
|
|
59
|
+
const entryDir = options?.entryDir ?? dirname(fileURLToPath(import.meta.url));
|
|
60
|
+
const markerPath = join(entryDir, SURFACE_MARKER_FILENAME);
|
|
61
|
+
return readMarkerSurface(markerPath) ?? "lazycodex";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function readMarkerSurface(markerPath: string): UlwLoopToolkitSurface | null {
|
|
65
|
+
try {
|
|
66
|
+
if (!existsSync(markerPath)) return null;
|
|
67
|
+
const parsed = JSON.parse(readFileSync(markerPath, "utf8")) as Record<string, unknown>;
|
|
68
|
+
return parseSurface(parsed["surface"]);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (error instanceof Error) return null;
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { writeFile } from "node:fs/promises";
|
|
2
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
3
3
|
|
|
4
4
|
import { checkpointUlwLoop } from "../src/checkpoint.js";
|
|
5
5
|
import { ulwLoopBriefPath } from "../src/paths.js";
|
|
@@ -7,6 +7,49 @@ import { startNextUlwLoop } from "../src/plan-crud.js";
|
|
|
7
7
|
import { criterion, expectCode, goal, passGoal, plan, repoWith, snapshot } from "./fixtures/checkpoint-builders.js";
|
|
8
8
|
import { MISSING_ARTIFACT_PATH, qualityGateJson } from "./fixtures/quality-gate-builder.js";
|
|
9
9
|
|
|
10
|
+
describe("checkpointUlwLoop final story surface resolution", () => {
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
delete process.env["OMO_AGENT_TOOLKIT_SURFACE"];
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("#given the omo-senpi surface #when the gate names omo-senpi reviewers #then the final story completes", async () => {
|
|
16
|
+
process.env["OMO_AGENT_TOOLKIT_SURFACE"] = "omo-senpi";
|
|
17
|
+
const repo = await repoWith(
|
|
18
|
+
plan([passGoal("G001", { status: "complete" }), passGoal("G002")], { activeGoalId: "G002" }),
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const result = await checkpointUlwLoop(repo, {
|
|
22
|
+
goalId: "G002",
|
|
23
|
+
status: "complete",
|
|
24
|
+
evidence: "final work complete and validation passed",
|
|
25
|
+
codexGoalJson: snapshot("complete"),
|
|
26
|
+
qualityGateJson: await qualityGateJson(repo, undefined, "G001-finished", "omo-senpi"),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
expect(result.aggregateCompletion?.status).toBe("complete");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("#given the omo-senpi surface #when the gate names lazycodex reviewers #then the final story is rejected", async () => {
|
|
33
|
+
process.env["OMO_AGENT_TOOLKIT_SURFACE"] = "omo-senpi";
|
|
34
|
+
const repo = await repoWith(
|
|
35
|
+
plan([passGoal("G001", { status: "complete" }), passGoal("G002")], { activeGoalId: "G002" }),
|
|
36
|
+
);
|
|
37
|
+
const gateJson = await qualityGateJson(repo);
|
|
38
|
+
|
|
39
|
+
await expectCode(
|
|
40
|
+
() =>
|
|
41
|
+
checkpointUlwLoop(repo, {
|
|
42
|
+
goalId: "G002",
|
|
43
|
+
status: "complete",
|
|
44
|
+
evidence: "final work complete and validation passed",
|
|
45
|
+
codexGoalJson: snapshot("complete"),
|
|
46
|
+
qualityGateJson: gateJson,
|
|
47
|
+
}),
|
|
48
|
+
"ULW_LOOP_QUALITY_GATE_INVALID",
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
10
53
|
describe("checkpointUlwLoop final story", () => {
|
|
11
54
|
it("requires quality-gate-json for the final goal complete", async () => {
|
|
12
55
|
const repo = await repoWith(
|
|
@@ -6,6 +6,7 @@ import type { UlwLoopItem, UlwLoopPlan, UlwLoopSuccessCriterion } from "../src/t
|
|
|
6
6
|
|
|
7
7
|
const NOW = "2026-05-23T00:00:00.000Z";
|
|
8
8
|
const FINAL_REVIEW_ROLES = ["lazycodex-code-reviewer", "lazycodex-qa-executor", "lazycodex-gate-reviewer"] as const;
|
|
9
|
+
const SENPI_REVIEW_ROLES = ["omo-senpi-code-reviewer", "omo-senpi-qa-executor", "omo-senpi-gate-reviewer"] as const;
|
|
9
10
|
const QUALITY_GATE_SECTIONS = ["codeReview", "manualQa", "gateReview", "iteration", "criteriaCoverage"] as const;
|
|
10
11
|
|
|
11
12
|
function makeCriterion(overrides: Partial<UlwLoopSuccessCriterion> = {}): UlwLoopSuccessCriterion {
|
|
@@ -94,6 +95,30 @@ describe("buildCodexGoalInstruction aggregate mode", () => {
|
|
|
94
95
|
expect(text).toContain("checkpoint");
|
|
95
96
|
});
|
|
96
97
|
|
|
98
|
+
it("#given a final story on the omo-senpi surface #when rendering instructions #then names the omo-senpi reviewers only", () => {
|
|
99
|
+
const { text } = buildCodexGoalInstruction({
|
|
100
|
+
plan: makePlan({ codexGoalMode: "aggregate" }),
|
|
101
|
+
goal: makeGoal(),
|
|
102
|
+
isFinal: true,
|
|
103
|
+
surface: "omo-senpi",
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
expectTextToContainAll(text, SENPI_REVIEW_ROLES);
|
|
107
|
+
for (const role of FINAL_REVIEW_ROLES) expect(text).not.toContain(role);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("#given a final story with the explicit lazycodex surface #when rendering instructions #then keeps the lazycodex reviewers", () => {
|
|
111
|
+
const { text } = buildCodexGoalInstruction({
|
|
112
|
+
plan: makePlan({ codexGoalMode: "aggregate" }),
|
|
113
|
+
goal: makeGoal(),
|
|
114
|
+
isFinal: true,
|
|
115
|
+
surface: "lazycodex",
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
expectTextToContainAll(text, FINAL_REVIEW_ROLES);
|
|
119
|
+
for (const role of SENPI_REVIEW_ROLES) expect(text).not.toContain(role);
|
|
120
|
+
});
|
|
121
|
+
|
|
97
122
|
it("#given a scoped plan #when rendering final commands #then includes the session id option", () => {
|
|
98
123
|
const { text } = buildCodexGoalInstruction({
|
|
99
124
|
plan: makePlan({
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
|
|
4
|
+
import { reviewerRolesFor, type UlwLoopToolkitSurface } from "../../src/surface.js";
|
|
5
|
+
|
|
4
6
|
// v2 evidence layout: artifacts must live inside the goal's current attempt dir
|
|
5
7
|
export function qaDirFor(goalId: string, attempt = 1, sessionId = "session"): string {
|
|
6
8
|
return `.omo/evidence/ulw/${sessionId}/${goalId}/a${attempt}`;
|
|
@@ -25,13 +27,15 @@ export async function qualityGateJson(
|
|
|
25
27
|
repoRoot: string,
|
|
26
28
|
cliArtifactPath?: string,
|
|
27
29
|
goalId = "G001-finished",
|
|
30
|
+
surface: UlwLoopToolkitSurface = "lazycodex",
|
|
28
31
|
): Promise<string> {
|
|
32
|
+
const roles = reviewerRolesFor(surface);
|
|
29
33
|
const qaDir = qaDirFor(goalId);
|
|
30
34
|
await writeQualityGateArtifacts(repoRoot, qaDir);
|
|
31
35
|
const cliPath = cliArtifactPath ?? `${qaDir}/cli-pass.txt`;
|
|
32
36
|
return JSON.stringify({
|
|
33
37
|
codeReview: {
|
|
34
|
-
by:
|
|
38
|
+
by: roles.codeReview,
|
|
35
39
|
recommendation: "APPROVE",
|
|
36
40
|
codeQualityStatus: "CLEAR",
|
|
37
41
|
reportPath: `${qaDir}/code-review.md`,
|
|
@@ -39,7 +43,7 @@ export async function qualityGateJson(
|
|
|
39
43
|
blockers: [],
|
|
40
44
|
},
|
|
41
45
|
manualQa: {
|
|
42
|
-
by:
|
|
46
|
+
by: roles.manualQa,
|
|
43
47
|
status: "passed",
|
|
44
48
|
evidence: "Ran CLI checkpoint validation with artifact-backed evidence.",
|
|
45
49
|
surfaceEvidence: [
|
|
@@ -78,7 +82,7 @@ export async function qualityGateJson(
|
|
|
78
82
|
],
|
|
79
83
|
},
|
|
80
84
|
gateReview: {
|
|
81
|
-
by:
|
|
85
|
+
by: roles.gateReview,
|
|
82
86
|
recommendation: "APPROVE",
|
|
83
87
|
reportPath: `${qaDir}/gate-review.md`,
|
|
84
88
|
evidence: "Verified all criteria and artifact evidence.",
|
|
@@ -75,9 +75,9 @@ function gateWith(overrides: Record<string, unknown>): Record<string, unknown> {
|
|
|
75
75
|
return { ...BASE_GATE, ...overrides };
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
function qualityGateError(input: unknown): UlwLoopError {
|
|
78
|
+
function qualityGateError(input: unknown, surface?: "lazycodex" | "omo-senpi"): UlwLoopError {
|
|
79
79
|
try {
|
|
80
|
-
validateQualityGate(input);
|
|
80
|
+
validateQualityGate(input, surface === undefined ? undefined : { reviewerSurface: surface });
|
|
81
81
|
} catch (error) {
|
|
82
82
|
if (error instanceof UlwLoopError) return error;
|
|
83
83
|
throw error;
|
|
@@ -85,6 +85,20 @@ function qualityGateError(input: unknown): UlwLoopError {
|
|
|
85
85
|
throw new Error("Expected UlwLoopError");
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
function senpiGate(): Record<string, unknown> {
|
|
89
|
+
return gateWith({
|
|
90
|
+
codeReview: { ...BASE_GATE.codeReview, by: "omo-senpi-code-reviewer" },
|
|
91
|
+
manualQa: { ...BASE_GATE.manualQa, by: "omo-senpi-qa-executor" },
|
|
92
|
+
gateReview: { ...BASE_GATE.gateReview, by: "omo-senpi-gate-reviewer" },
|
|
93
|
+
criteriaCoverage: {
|
|
94
|
+
...BASE_GATE.criteriaCoverage,
|
|
95
|
+
originalIntent: "User wanted the senpi surface to accept honest reviewer attribution.",
|
|
96
|
+
desiredOutcome: "A gate naming the omo-senpi reviewers validates on the omo-senpi surface.",
|
|
97
|
+
userOutcomeReview: "The senpi-surface gate passed with truthful reviewer identities.",
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
88
102
|
describe("validateQualityGate reviewer roles", () => {
|
|
89
103
|
it("#given arbitrary reviewer role names #when validated #then each role field is rejected", () => {
|
|
90
104
|
const cases = [
|
|
@@ -106,6 +120,35 @@ describe("validateQualityGate reviewer roles", () => {
|
|
|
106
120
|
}
|
|
107
121
|
});
|
|
108
122
|
|
|
123
|
+
it("#given omo-senpi reviewer identities #when validated on the omo-senpi surface #then the gate is accepted", () => {
|
|
124
|
+
expect(() => validateQualityGate(senpiGate(), { reviewerSurface: "omo-senpi" })).not.toThrow();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("#given lazycodex identities #when validated on the omo-senpi surface #then each role names the omo-senpi identity", () => {
|
|
128
|
+
const error = qualityGateError(gateWith({}), "omo-senpi");
|
|
129
|
+
expect(error.code).toBe("ULW_LOOP_QUALITY_GATE_INVALID");
|
|
130
|
+
expect(error.message).toContain("omo-senpi-code-reviewer");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("#given omo-senpi identities #when validated on the default lazycodex surface #then the gate is rejected", () => {
|
|
134
|
+
const error = qualityGateError(senpiGate());
|
|
135
|
+
expect(error.code).toBe("ULW_LOOP_QUALITY_GATE_INVALID");
|
|
136
|
+
expect(error.message).toContain("lazycodex-code-reviewer");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("#given swapped omo-senpi reviewer roles #when validated on the omo-senpi surface #then section-specific roles are enforced", () => {
|
|
140
|
+
const swapped = gateWith({
|
|
141
|
+
codeReview: { ...BASE_GATE.codeReview, by: "omo-senpi-qa-executor" },
|
|
142
|
+
manualQa: { ...BASE_GATE.manualQa, by: "omo-senpi-qa-executor" },
|
|
143
|
+
gateReview: { ...BASE_GATE.gateReview, by: "omo-senpi-gate-reviewer" },
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const error = qualityGateError(swapped, "omo-senpi");
|
|
147
|
+
|
|
148
|
+
expect(error.code).toBe("ULW_LOOP_QUALITY_GATE_INVALID");
|
|
149
|
+
expect(error.message).toContain("codeReview.by");
|
|
150
|
+
});
|
|
151
|
+
|
|
109
152
|
it("#given swapped LazyCodex reviewer roles #when validated #then section-specific roles are enforced", () => {
|
|
110
153
|
const cases = [
|
|
111
154
|
{
|
|
@@ -146,6 +146,26 @@ describe("applySpawnGuards gate-artifact guard", () => {
|
|
|
146
146
|
expect(parsed.permissionDecisionReason).toContain("g1-code-review.md");
|
|
147
147
|
});
|
|
148
148
|
|
|
149
|
+
it("#given an omo-senpi gate spawn by agent_type without artifacts #when guarded #then denies naming the missing path", () => {
|
|
150
|
+
writeGoals();
|
|
151
|
+
|
|
152
|
+
const output = applySpawnGuards(
|
|
153
|
+
payload("collaborationspawn_agent", { agent_type: "omo-senpi-gate-reviewer", message: "audit the artifacts" }),
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const parsed = deny(output);
|
|
157
|
+
expect(parsed.permissionDecision).toBe("deny");
|
|
158
|
+
expect(parsed.permissionDecisionReason).toContain("g1-code-review.md");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("#given an omo-senpi gate reviewer named in the message #when guarded #then still denies", () => {
|
|
162
|
+
writeGoals();
|
|
163
|
+
|
|
164
|
+
const output = applySpawnGuards(payload("spawn_agent", { message: "spawn omo-senpi-gate-reviewer now" }));
|
|
165
|
+
|
|
166
|
+
expect(deny(output).permissionDecision).toBe("deny");
|
|
167
|
+
});
|
|
168
|
+
|
|
149
169
|
it("#given a gate spawn identified by message only #when guarded #then still denies", () => {
|
|
150
170
|
writeGoals();
|
|
151
171
|
|