oh-my-opencode 4.16.3 → 4.17.1
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/.agents/command/omomomo.md +1 -1
- package/.agents/command/publish.md +31 -58
- package/.agents/skills/hyperplan/SKILL.md +3 -3
- package/.agents/skills/omomomo/SKILL.md +1 -1
- package/.agents/skills/pre-publish-review/SKILL.md +1 -1
- package/.agents/skills/publish/SKILL.md +32 -59
- package/.opencode/command/omomomo.md +1 -1
- package/.opencode/command/publish.md +31 -58
- package/.opencode/skills/hyperplan/SKILL.md +3 -3
- package/.opencode/skills/pre-publish-review/SKILL.md +4 -1
- package/README.ja.md +4 -4
- package/README.ko.md +4 -4
- package/README.md +2 -2
- package/README.ru.md +4 -4
- package/README.zh-cn.md +4 -4
- package/dist/cli/index.js +195 -76
- package/dist/cli-node/index.js +195 -76
- package/dist/features/team-mode/test-support/async-test-helpers.d.ts +2 -2
- package/dist/index.js +310 -299
- package/dist/plugin/chat-message/types.d.ts +4 -0
- package/dist/plugin/command-execute-before.d.ts +1 -0
- package/dist/plugin/stop-continuation.d.ts +17 -0
- package/dist/skills/remove-ai-slops/SKILL.md +2 -2
- package/dist/skills/review-work/SKILL.md +10 -2
- package/dist/skills/start-work/SKILL.md +6 -3
- package/dist/skills/ultimate-browsing/ATTRIBUTION.md +2 -2
- package/dist/skills/ultimate-browsing/engine/templates/package.json +1 -1
- package/dist/skills/ultimate-browsing/references/chrome-stealth.md +11 -11
- package/dist/skills/ulw-plan/SKILL.md +2 -1
- package/dist/skills/ulw-plan/references/full-workflow.md +1 -1
- package/dist/skills/ulw-plan/references/intent-unclear.md +4 -4
- package/dist/skills/ulw-plan/scripts/scaffold-plan.mjs +2 -2
- package/dist/skills/visual-qa/SKILL.md +9 -5
- package/dist/tui.js +79 -32
- package/package.json +15 -14
- package/packages/git-bash-mcp/package.json +27 -0
- package/packages/lsp-daemon/dist/cli.js +7 -13
- package/packages/lsp-daemon/dist/daemon-client.js +3 -5
- package/packages/lsp-daemon/dist/index.js +12 -18
- package/packages/lsp-daemon/dist/request-routing.js +6 -8
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +3 -1
- package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +1080 -1017
- 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/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/AGENTS.md +2 -2
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/dist/cli.js +6 -2
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/dist/codex-hook.js +6 -2
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/src/codex-hook.ts +6 -2
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/test/cli.test.ts +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/test/codex-hook.test.ts +67 -2
- package/packages/omo-codex/plugin/components/lsp/dist/cli.js +14 -14
- 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/lsp/test/package-smoke.test.ts +0 -13
- package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-5.5.md +2 -2
- package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-5.6.md +3 -3
- 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/start-work-continuation/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/start-work-continuation/package.json +1 -1
- package/packages/omo-codex/plugin/components/start-work-continuation/test/codex-hook.test.ts +1 -79
- package/packages/omo-codex/plugin/components/teammode/AGENTS.md +2 -2
- 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/teammode/skills/teammode/SKILL.md +33 -16
- package/packages/omo-codex/plugin/components/teammode/skills/teammode/scripts/team.mjs +2 -1
- package/packages/omo-codex/plugin/components/teammode/test/v2-spawn-schema.test.ts +69 -0
- 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/explorer.toml +2 -2
- package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-code-reviewer.toml +2 -2
- package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-gate-reviewer.toml +6 -6
- package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-qa-executor.toml +5 -5
- package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-worker-high.toml +26 -0
- package/packages/omo-codex/plugin/components/ultrawork/agents/{lazycodex-executor.toml → lazycodex-worker-low.toml} +6 -4
- package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-worker-medium.toml +26 -0
- package/packages/omo-codex/plugin/components/ultrawork/agents/librarian.toml +2 -2
- package/packages/omo-codex/plugin/components/ultrawork/agents/plan.toml +7 -7
- package/packages/omo-codex/plugin/components/ultrawork/directive.md +76 -37
- 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/ultrawork/skills/ultrawork/SKILL.md +76 -37
- package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/SKILL.md +2 -1
- package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/references/full-workflow.md +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/references/intent-unclear.md +4 -4
- package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/scripts/scaffold-plan.mjs +2 -2
- package/packages/omo-codex/plugin/components/ultrawork/test/codex-hook.test.ts +25 -0
- package/packages/omo-codex/plugin/components/ultrawork/test/package-smoke.test.ts +0 -68
- package/packages/omo-codex/plugin/components/ulw-loop/AGENTS.md +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/CHANGELOG.md +2 -0
- package/packages/omo-codex/plugin/components/ulw-loop/README.md +3 -1
- package/packages/omo-codex/plugin/components/ulw-loop/directive.md +76 -37
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.js +6 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-subcommands.d.ts +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-subcommands.js +13 -2
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +405 -25
- package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-instruction.js +12 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/domain-types.d.ts +4 -2
- package/packages/omo-codex/plugin/components/ulw-loop/dist/paths.d.ts +7 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/paths.js +16 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-crud.js +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-verdicts.d.ts +6 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-verdicts.js +20 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate.d.ts +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate.js +12 -9
- package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-guard.d.ts +3 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-guard.js +148 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/stop-resume-hook.d.ts +2 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/stop-resume-hook.js +209 -0
- package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +25 -2
- package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +14 -15
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +30 -28
- package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +6 -1
- package/packages/omo-codex/plugin/components/ulw-loop/src/cli-subcommands.ts +14 -3
- package/packages/omo-codex/plugin/components/ulw-loop/src/cli.ts +10 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-instruction.ts +12 -1
- package/packages/omo-codex/plugin/components/ulw-loop/src/domain-types.ts +4 -2
- package/packages/omo-codex/plugin/components/ulw-loop/src/paths.ts +27 -1
- package/packages/omo-codex/plugin/components/ulw-loop/src/plan-crud.ts +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-verdicts.ts +23 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate.ts +16 -9
- package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-guard.ts +138 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/stop-resume-hook.ts +208 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/cli-create-goals.test.ts +12 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/cli-entrypoint.test.ts +4 -1
- package/packages/omo-codex/plugin/components/ulw-loop/test/codex-goal-instruction.test.ts +8 -35
- package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/quality-gate-builder.ts +24 -13
- package/packages/omo-codex/plugin/components/ulw-loop/test/package-smoke.test.ts +5 -2
- package/packages/omo-codex/plugin/components/ulw-loop/test/paths.test.ts +43 -8
- package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate.test.ts +55 -2
- package/packages/omo-codex/plugin/components/ulw-loop/test/spawn-guard.test.ts +228 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/stop-resume-hook.test.ts +193 -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 +18 -0
- 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-start-work-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +17 -0
- package/packages/omo-codex/plugin/hooks/subagent-stop-checking-start-work-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +2 -2
- 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/model-catalog.json +16 -7
- 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/migrate-codex-config/catalog.mjs +16 -7
- package/packages/omo-codex/plugin/scripts/sync-skills.mjs +8 -2
- package/packages/omo-codex/plugin/skills/init-deep/SKILL.md +2 -2
- package/packages/omo-codex/plugin/skills/refactor/SKILL.md +2 -2
- package/packages/omo-codex/plugin/skills/remove-ai-slops/SKILL.md +4 -4
- package/packages/omo-codex/plugin/skills/review-work/SKILL.md +18 -4
- package/packages/omo-codex/plugin/skills/start-work/SKILL.md +6 -3
- package/packages/omo-codex/plugin/skills/teammode/SKILL.md +33 -16
- package/packages/omo-codex/plugin/skills/teammode/scripts/team.mjs +2 -1
- package/packages/omo-codex/plugin/skills/ultimate-browsing/ATTRIBUTION.md +2 -2
- package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/templates/package.json +1 -1
- package/packages/omo-codex/plugin/skills/ultimate-browsing/references/chrome-stealth.md +11 -11
- package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +76 -37
- package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +14 -15
- package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +30 -28
- package/packages/omo-codex/plugin/skills/ulw-plan/SKILL.md +2 -1
- package/packages/omo-codex/plugin/skills/ulw-plan/references/full-workflow.md +1 -1
- package/packages/omo-codex/plugin/skills/ulw-plan/references/intent-unclear.md +4 -4
- package/packages/omo-codex/plugin/skills/ulw-plan/scripts/scaffold-plan.mjs +2 -2
- package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +2 -2
- package/packages/omo-codex/plugin/skills/visual-qa/SKILL.md +11 -7
- package/packages/omo-codex/plugin/test/aggregate-agents.test.mjs +76 -16
- package/packages/omo-codex/plugin/test/aggregate-hooks.test.mjs +23 -2
- package/packages/omo-codex/plugin/test/aggregate-manifest.test.mjs +1 -1
- package/packages/omo-codex/plugin/test/aggregate-model-catalog.test.mjs +4 -4
- package/packages/omo-codex/plugin/test/auto-update.test.mjs +4 -4
- package/packages/omo-codex/plugin/test/component-hook-contract-cases.mjs +2 -2
- package/packages/omo-codex/plugin/test/lcx-bug-skills.test.mjs +4 -101
- package/packages/omo-codex/plugin/test/migrate-codex-config.test.mjs +14 -14
- package/packages/omo-codex/plugin/test/sync-skills-orchestration.test.mjs +11 -0
- package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +1 -1
- package/packages/omo-codex/plugin/test/sync-skills.test.mjs +5 -3
- package/packages/omo-codex/plugin/test/teammode-transport.test.mjs +25 -0
- package/packages/omo-codex/plugin/test/ulw-plan-scope-contract.test.mjs +24 -0
- package/packages/omo-codex/plugin/test/ulw-plan-skill-contract.test.mjs +9 -40
- package/packages/omo-codex/plugin/test/ulw-research-skill-contract.test.mjs +4 -277
- package/packages/omo-codex/scripts/install-config-reasoning.test.mjs +5 -5
- package/packages/omo-codex/scripts/install-config.test.mjs +22 -3
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +98 -30
- package/packages/shared-skills/skills/remove-ai-slops/SKILL.md +2 -2
- package/packages/shared-skills/skills/review-work/SKILL.md +10 -2
- package/packages/shared-skills/skills/start-work/SKILL.md +6 -3
- package/packages/shared-skills/skills/ultimate-browsing/ATTRIBUTION.md +2 -2
- package/packages/shared-skills/skills/ultimate-browsing/engine/templates/package.json +1 -1
- package/packages/shared-skills/skills/ultimate-browsing/references/chrome-stealth.md +11 -11
- package/packages/shared-skills/skills/ulw-plan/SKILL.md +2 -1
- package/packages/shared-skills/skills/ulw-plan/references/full-workflow.md +1 -1
- package/packages/shared-skills/skills/ulw-plan/references/intent-unclear.md +4 -4
- package/packages/shared-skills/skills/ulw-plan/scripts/scaffold-plan.mjs +2 -2
- package/packages/shared-skills/skills/visual-qa/SKILL.md +9 -5
- package/dist/skills/visual-qa/scripts/skill-prompt-contract.test.ts +0 -296
- package/packages/omo-codex/plugin/components/ulw-loop/test/skill-contract.test.ts +0 -70
- package/packages/omo-codex/plugin/test/ulw-research-epistemic-contract.test.mjs +0 -98
- package/packages/shared-skills/skills/visual-qa/scripts/skill-prompt-contract.test.ts +0 -296
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { normalizeUlwLoopSessionId, ulwLoopDir } from "./paths.js";
|
|
5
|
+
import type { UlwLoopItem, UlwLoopPlan } from "./types.js";
|
|
6
|
+
|
|
7
|
+
// Turn-death recovery only: Codex emits Stop when a turn ends, so a run that
|
|
8
|
+
// dies mid-turn (crash, kill, context blowup before any Stop) never reaches
|
|
9
|
+
// this handler. Mid-turn stalls are out of scope by design.
|
|
10
|
+
|
|
11
|
+
const RESUME_CAP = 2;
|
|
12
|
+
|
|
13
|
+
// Mirrors start-work-continuation's context-pressure bail-out: injecting a
|
|
14
|
+
// resume directive into an already-overflowing context makes things worse.
|
|
15
|
+
const CONTEXT_PRESSURE_MARKERS = [
|
|
16
|
+
"context compacted",
|
|
17
|
+
"context_length_exceeded",
|
|
18
|
+
"skill descriptions were shortened",
|
|
19
|
+
"context_too_large",
|
|
20
|
+
"codex ran out of room in the model's context window",
|
|
21
|
+
"your input exceeds the context window",
|
|
22
|
+
"long threads and multiple compactions",
|
|
23
|
+
] as const;
|
|
24
|
+
|
|
25
|
+
interface StopPayload {
|
|
26
|
+
readonly session_id: string;
|
|
27
|
+
readonly cwd: string;
|
|
28
|
+
readonly transcript_path: string;
|
|
29
|
+
readonly stop_hook_active: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function runStopResumeHook(input: unknown): string {
|
|
33
|
+
const payload = parseStopPayload(input);
|
|
34
|
+
if (payload === null || payload.stop_hook_active) return "";
|
|
35
|
+
if (transcriptShowsContextPressure(payload.transcript_path)) return "";
|
|
36
|
+
if (boulderContinuationWillFire(payload.cwd, payload.session_id)) return "";
|
|
37
|
+
const stateDir = ulwLoopDir(payload.cwd, { sessionId: payload.session_id });
|
|
38
|
+
const plan = readPlan(join(stateDir, "goals.json"));
|
|
39
|
+
if (plan === null || plan.aggregateCompletion?.status === "complete") return "";
|
|
40
|
+
const goal = resumableGoal(plan);
|
|
41
|
+
if (goal === undefined) return "";
|
|
42
|
+
if (!consumeResumeBudget(stateDir, goal.id)) return "";
|
|
43
|
+
const output: { decision: "block"; reason: string } = {
|
|
44
|
+
decision: "block",
|
|
45
|
+
reason: renderResumeDirective(plan, goal, payload.session_id),
|
|
46
|
+
};
|
|
47
|
+
return JSON.stringify(output);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function runStopResumeHookCli(stdin: NodeJS.ReadableStream, stdout: NodeJS.WritableStream): Promise<void> {
|
|
51
|
+
try {
|
|
52
|
+
const chunks: Buffer[] = [];
|
|
53
|
+
for await (const chunk of stdin) chunks.push(Buffer.from(chunk));
|
|
54
|
+
const output = runStopResumeHook(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
55
|
+
if (output.length > 0) stdout.write(output);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (error instanceof Error) return;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function resumableGoal(plan: UlwLoopPlan): UlwLoopItem | undefined {
|
|
62
|
+
const active = plan.goals.find((goal) => goal.id === plan.activeGoalId);
|
|
63
|
+
if (active !== undefined && isResumableStatus(active.status)) return active;
|
|
64
|
+
return plan.goals.find((goal) => isResumableStatus(goal.status));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isResumableStatus(status: UlwLoopItem["status"]): boolean {
|
|
68
|
+
return status === "pending" || status === "in_progress";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Two-strike cap keyed on ledger movement: an unchanged ledger.jsonl line
|
|
72
|
+
// count across resumes means the loop is not progressing. The stuck marker is
|
|
73
|
+
// a separate file — a ledger append would change the count and self-reset.
|
|
74
|
+
function consumeResumeBudget(stateDir: string, goalId: string): boolean {
|
|
75
|
+
const ledgerLineCount = countLedgerLines(join(stateDir, "ledger.jsonl"));
|
|
76
|
+
const counterPath = join(stateDir, `auto-resume-${goalId}.json`);
|
|
77
|
+
const previous = readCounter(counterPath);
|
|
78
|
+
const count = previous !== null && previous.ledgerLineCount === ledgerLineCount ? previous.count : 0;
|
|
79
|
+
if (count >= RESUME_CAP) {
|
|
80
|
+
writeFileSync(join(stateDir, `auto-resume-${goalId}.stuck`), `no ledger progress after ${count} resumes\n`);
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
writeFileSync(counterPath, JSON.stringify({ count: count + 1, ledgerLineCount }));
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function renderResumeDirective(plan: UlwLoopPlan, goal: UlwLoopItem, sessionId: string): string {
|
|
88
|
+
const normalized = normalizeUlwLoopSessionId(sessionId);
|
|
89
|
+
const option =
|
|
90
|
+
normalized !== null && plan.goalsPath.includes(`/${normalized}/`) ? ` --session-id ${normalized}` : "";
|
|
91
|
+
return [
|
|
92
|
+
`The ulw-loop run in this session still has unfinished goals (next: ${goal.id} — ${goal.title}).`,
|
|
93
|
+
"The turn ended before the loop completed. Resume it now:",
|
|
94
|
+
`1. Run \`omo ulw-loop status${option} --json\` to reload the plan, the active goal, and currentAttemptDir.`,
|
|
95
|
+
"2. Continue the active goal's remaining success criteria, recording evidence with record-evidence.",
|
|
96
|
+
`3. Checkpoint through \`omo ulw-loop checkpoint${option}\` when the goal's criteria are proven.`,
|
|
97
|
+
"If the loop is genuinely blocked on the user, checkpoint the goal as blocked with the reason instead.",
|
|
98
|
+
].join("\n");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function readPlan(goalsPath: string): UlwLoopPlan | null {
|
|
102
|
+
try {
|
|
103
|
+
return JSON.parse(readFileSync(goalsPath, "utf8")) as UlwLoopPlan;
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (error instanceof Error) return null;
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function countLedgerLines(ledgerPath: string): number {
|
|
111
|
+
try {
|
|
112
|
+
return readFileSync(ledgerPath, "utf8").split("\n").filter(Boolean).length;
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (error instanceof Error) return 0;
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function readCounter(counterPath: string): { count: number; ledgerLineCount: number } | null {
|
|
120
|
+
try {
|
|
121
|
+
if (!existsSync(counterPath)) return null;
|
|
122
|
+
const parsed = JSON.parse(readFileSync(counterPath, "utf8")) as Record<string, unknown>;
|
|
123
|
+
if (typeof parsed["count"] !== "number" || typeof parsed["ledgerLineCount"] !== "number") return null;
|
|
124
|
+
return { count: parsed["count"], ledgerLineCount: parsed["ledgerLineCount"] };
|
|
125
|
+
} catch (error) {
|
|
126
|
+
if (error instanceof Error) return null;
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Local ~10-LOC approximation of start-work-continuation's boulder check (no
|
|
132
|
+
// cross-component import allowed): any continuable work for this session means
|
|
133
|
+
// that hook owns the Stop event, so this one stays silent.
|
|
134
|
+
function boulderContinuationWillFire(cwd: string, sessionId: string): boolean {
|
|
135
|
+
try {
|
|
136
|
+
const raw = JSON.parse(readFileSync(join(cwd, ".omo", "boulder.json"), "utf8")) as Record<string, unknown>;
|
|
137
|
+
const works = raw["works"];
|
|
138
|
+
// The flat legacy shape has no works map: the top level is the single work.
|
|
139
|
+
const entries = typeof works === "object" && works !== null ? Object.values(works) : [raw];
|
|
140
|
+
return entries.some((work) => {
|
|
141
|
+
if (typeof work !== "object" || work === null) return false;
|
|
142
|
+
const entry = work as Record<string, unknown>;
|
|
143
|
+
const sessionIds = Array.isArray(entry["session_ids"]) ? entry["session_ids"] : [];
|
|
144
|
+
const continuable = entry["status"] === "active" || entry["status"] === "paused";
|
|
145
|
+
return continuable && sessionIds.includes(`codex:${sessionId}`) && boulderPlanHasRemainingTask(cwd, entry);
|
|
146
|
+
});
|
|
147
|
+
} catch (error) {
|
|
148
|
+
if (error instanceof Error) return false;
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function transcriptShowsContextPressure(transcriptPath: string): boolean {
|
|
154
|
+
try {
|
|
155
|
+
const transcript = readFileSync(transcriptPath, "utf8").toLowerCase();
|
|
156
|
+
return CONTEXT_PRESSURE_MARKERS.some((marker) => transcript.includes(marker));
|
|
157
|
+
} catch (error) {
|
|
158
|
+
if (error instanceof Error) return false;
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// start-work-continuation only fires while its plan checklist has remaining
|
|
164
|
+
// items; a boulder work whose plan is exhausted (or unreadable) leaves the
|
|
165
|
+
// Stop event to this hook.
|
|
166
|
+
function boulderPlanHasRemainingTask(cwd: string, entry: Record<string, unknown>): boolean {
|
|
167
|
+
const activePlan = entry["active_plan"];
|
|
168
|
+
if (typeof activePlan !== "string" || activePlan.trim().length === 0) return false;
|
|
169
|
+
const planPath = isAbsolute(activePlan) ? activePlan : join(cwd, activePlan);
|
|
170
|
+
const worktree = entry["worktree_path"];
|
|
171
|
+
const candidates =
|
|
172
|
+
typeof worktree === "string" && worktree.trim().length > 0 && !isAbsolute(activePlan)
|
|
173
|
+
? [join(isAbsolute(worktree) ? worktree : join(cwd, worktree), activePlan), planPath]
|
|
174
|
+
: [planPath];
|
|
175
|
+
for (const candidate of candidates) {
|
|
176
|
+
try {
|
|
177
|
+
return readFileSync(candidate, "utf8")
|
|
178
|
+
.split(/\r?\n/)
|
|
179
|
+
.some((line) => line.startsWith("- [ ] "));
|
|
180
|
+
} catch (error) {
|
|
181
|
+
if (!(error instanceof Error)) throw error;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function parseStopPayload(value: unknown): StopPayload | null {
|
|
188
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
189
|
+
const record = value as Record<string, unknown>;
|
|
190
|
+
const optionalMessage = record["last_assistant_message"];
|
|
191
|
+
const valid =
|
|
192
|
+
record["hook_event_name"] === "Stop" &&
|
|
193
|
+
typeof record["session_id"] === "string" &&
|
|
194
|
+
typeof record["turn_id"] === "string" &&
|
|
195
|
+
typeof record["transcript_path"] === "string" &&
|
|
196
|
+
typeof record["cwd"] === "string" &&
|
|
197
|
+
typeof record["model"] === "string" &&
|
|
198
|
+
typeof record["permission_mode"] === "string" &&
|
|
199
|
+
typeof record["stop_hook_active"] === "boolean" &&
|
|
200
|
+
(optionalMessage === undefined || typeof optionalMessage === "string");
|
|
201
|
+
if (!valid) return null;
|
|
202
|
+
return {
|
|
203
|
+
session_id: record["session_id"] as string,
|
|
204
|
+
cwd: record["cwd"] as string,
|
|
205
|
+
transcript_path: record["transcript_path"] as string,
|
|
206
|
+
stop_hook_active: record["stop_hook_active"] as boolean,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
@@ -103,6 +103,8 @@ describe("ulwLoopCommand create-goals", () => {
|
|
|
103
103
|
|
|
104
104
|
it("#given completed default aggregate #when creating another default plan #then guides to a fresh session", async () => {
|
|
105
105
|
await createPlan("- Finished");
|
|
106
|
+
expect(await ulwLoopCommand(["complete-goals"])).toBe(0);
|
|
107
|
+
resetOutput();
|
|
106
108
|
for (const criterionId of ["C001", "C002", "C003"]) await passCriterion("G001-finished", criterionId);
|
|
107
109
|
expect(
|
|
108
110
|
await ulwLoopCommand([
|
|
@@ -128,6 +130,16 @@ describe("ulwLoopCommand create-goals", () => {
|
|
|
128
130
|
expect(err.join("")).toContain("--force only");
|
|
129
131
|
});
|
|
130
132
|
|
|
133
|
+
it("#given an active goal on a v2 plan #when reading status --json #then exposes the current attempt dir", async () => {
|
|
134
|
+
await createPlan();
|
|
135
|
+
expect(await ulwLoopCommand(["complete-goals"])).toBe(0);
|
|
136
|
+
resetOutput();
|
|
137
|
+
|
|
138
|
+
expect(await ulwLoopCommand(["status", "--json"])).toBe(0);
|
|
139
|
+
|
|
140
|
+
expect(stdoutJson()).toMatchObject({ currentAttemptDir: ".omo/evidence/ulw/session/G001-goal-a/a1" });
|
|
141
|
+
});
|
|
142
|
+
|
|
131
143
|
it("#given two session ids #when creating goals #then writes isolated session-scoped plans", async () => {
|
|
132
144
|
expect(await ulwLoopCommand(["create-goals", "--session-id", "session-A", "--brief", "- Alpha", "--json"])).toBe(
|
|
133
145
|
0,
|
|
@@ -57,7 +57,10 @@ async function runCli(args: readonly string[], input = ""): Promise<CliResult> {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
beforeAll(async () => {
|
|
60
|
-
const build =
|
|
60
|
+
const build =
|
|
61
|
+
process.platform === "win32"
|
|
62
|
+
? await runProcess("cmd.exe", ["/c", "npm", "run", "build"], componentRoot)
|
|
63
|
+
: await runProcess("npm", ["run", "build"], componentRoot);
|
|
61
64
|
expect(build.code, `npm run build failed:\n${build.stderr}`).toBe(0);
|
|
62
65
|
}, 120_000);
|
|
63
66
|
|
|
@@ -58,53 +58,28 @@ describe("buildCodexGoalInstruction aggregate mode", () => {
|
|
|
58
58
|
expect(text).toContain(".omo/ulw-loop/goals.json");
|
|
59
59
|
});
|
|
60
60
|
|
|
61
|
-
it("given aggregate mode when rendering create_goal payload then
|
|
62
|
-
const { json
|
|
61
|
+
it("given aggregate mode when rendering create_goal payload then payload is the aggregate objective only", () => {
|
|
62
|
+
const { json } = buildCodexGoalInstruction({
|
|
63
63
|
plan: makePlan({ codexGoalMode: "aggregate" }),
|
|
64
64
|
goal: makeGoal(),
|
|
65
65
|
});
|
|
66
66
|
expect(json).toEqual({
|
|
67
67
|
objective: ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE,
|
|
68
68
|
});
|
|
69
|
-
expect(text).toContain("objective only");
|
|
70
|
-
expect(text).not.toContain('"status"');
|
|
71
|
-
expect(text).toContain("Goals are unlimited");
|
|
72
|
-
expect(text).not.toMatch(/token[_-]?budget/i);
|
|
73
69
|
});
|
|
74
70
|
|
|
75
|
-
it("
|
|
76
|
-
const { text } = buildCodexGoalInstruction({
|
|
77
|
-
plan: makePlan({ codexGoalMode: "aggregate" }),
|
|
78
|
-
goal: makeGoal(),
|
|
79
|
-
isFinal: false,
|
|
80
|
-
});
|
|
81
|
-
expect(text).toContain("do not call update_goal mid-aggregate");
|
|
82
|
-
expect(text).toContain("checkpoint this OMO ledger story");
|
|
83
|
-
expect(text).toContain("update_goal is reserved for the final story after the mandatory quality gate passes");
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it("#given a non-final aggregate story #when rendering instructions #then defers the current final quality gate", () => {
|
|
71
|
+
it("#given a non-final aggregate story #when rendering instructions #then omits the final quality gate tokens", () => {
|
|
87
72
|
const { text } = buildCodexGoalInstruction({
|
|
88
73
|
plan: makePlan({ codexGoalMode: "aggregate" }),
|
|
89
74
|
goal: makeGoal(),
|
|
90
75
|
isFinal: false,
|
|
91
76
|
});
|
|
92
77
|
|
|
93
|
-
expect(text).
|
|
94
|
-
expect(text).toContain("
|
|
95
|
-
expect(text).toContain("continue the remaining stories");
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it("includes quality gate instruction when isFinal", () => {
|
|
99
|
-
const { text } = buildCodexGoalInstruction({
|
|
100
|
-
plan: makePlan({ codexGoalMode: "aggregate" }),
|
|
101
|
-
goal: makeGoal(),
|
|
102
|
-
isFinal: true,
|
|
103
|
-
});
|
|
104
|
-
expect(text).toMatch(/quality gate/i);
|
|
78
|
+
for (const role of FINAL_REVIEW_ROLES) expect(text).not.toContain(role);
|
|
79
|
+
expect(text).not.toContain("record-review-blockers");
|
|
105
80
|
});
|
|
106
81
|
|
|
107
|
-
it("#given a final aggregate story #when rendering instructions #then
|
|
82
|
+
it("#given a final aggregate story #when rendering instructions #then includes the quality gate roles, fields, and commands", () => {
|
|
108
83
|
const { text } = buildCodexGoalInstruction({
|
|
109
84
|
plan: makePlan({ codexGoalMode: "aggregate" }),
|
|
110
85
|
goal: makeGoal(),
|
|
@@ -113,10 +88,8 @@ describe("buildCodexGoalInstruction aggregate mode", () => {
|
|
|
113
88
|
|
|
114
89
|
expectTextToContainAll(text, FINAL_REVIEW_ROLES);
|
|
115
90
|
expectTextToContainAll(text, QUALITY_GATE_SECTIONS);
|
|
116
|
-
|
|
117
|
-
expect(text).
|
|
118
|
-
expectTextToContainAll(text, ["original brief", "desired user-visible outcome", "userOutcomeReview"]);
|
|
119
|
-
expect(text).toMatch(/not clean.*do not call update_goal/i);
|
|
91
|
+
expectTextToContainAll(text, ["originalIntent", "desiredOutcome", "userOutcomeReview"]);
|
|
92
|
+
expect(text).toContain("update_goal");
|
|
120
93
|
expect(text).toContain("record-review-blockers");
|
|
121
94
|
expect(text).toContain("checkpoint");
|
|
122
95
|
});
|
|
@@ -1,29 +1,40 @@
|
|
|
1
1
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
// v2 evidence layout: artifacts must live inside the goal's current attempt dir
|
|
5
|
+
export function qaDirFor(goalId: string, attempt = 1, sessionId = "session"): string {
|
|
6
|
+
return `.omo/evidence/ulw/${sessionId}/${goalId}/a${attempt}`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const QA_DIR = qaDirFor("G001-finished");
|
|
5
10
|
export const CODE_REVIEW_PATH = `${QA_DIR}/code-review.md`;
|
|
6
11
|
export const GATE_REVIEW_PATH = `${QA_DIR}/gate-review.md`;
|
|
7
12
|
export const CLI_PASS_PATH = `${QA_DIR}/cli-pass.txt`;
|
|
8
13
|
export const REJECTION_LOG_PATH = `${QA_DIR}/rejection.txt`;
|
|
9
14
|
export const MISSING_ARTIFACT_PATH = `${QA_DIR}/missing.txt`;
|
|
10
15
|
|
|
11
|
-
export async function writeQualityGateArtifacts(repoRoot: string): Promise<void> {
|
|
12
|
-
await mkdir(join(repoRoot,
|
|
13
|
-
await writeFile(join(repoRoot,
|
|
14
|
-
await writeFile(join(repoRoot,
|
|
15
|
-
await writeFile(join(repoRoot,
|
|
16
|
-
await writeFile(join(repoRoot,
|
|
16
|
+
export async function writeQualityGateArtifacts(repoRoot: string, qaDir = QA_DIR): Promise<void> {
|
|
17
|
+
await mkdir(join(repoRoot, qaDir), { recursive: true });
|
|
18
|
+
await writeFile(join(repoRoot, `${qaDir}/code-review.md`), "code review approved\n", "utf8");
|
|
19
|
+
await writeFile(join(repoRoot, `${qaDir}/gate-review.md`), "gate review approved\n", "utf8");
|
|
20
|
+
await writeFile(join(repoRoot, `${qaDir}/cli-pass.txt`), "cli scenario passed\n", "utf8");
|
|
21
|
+
await writeFile(join(repoRoot, `${qaDir}/rejection.txt`), "invalid gate rejected\n", "utf8");
|
|
17
22
|
}
|
|
18
23
|
|
|
19
|
-
export async function qualityGateJson(
|
|
20
|
-
|
|
24
|
+
export async function qualityGateJson(
|
|
25
|
+
repoRoot: string,
|
|
26
|
+
cliArtifactPath?: string,
|
|
27
|
+
goalId = "G001-finished",
|
|
28
|
+
): Promise<string> {
|
|
29
|
+
const qaDir = qaDirFor(goalId);
|
|
30
|
+
await writeQualityGateArtifacts(repoRoot, qaDir);
|
|
31
|
+
const cliPath = cliArtifactPath ?? `${qaDir}/cli-pass.txt`;
|
|
21
32
|
return JSON.stringify({
|
|
22
33
|
codeReview: {
|
|
23
34
|
by: "lazycodex-code-reviewer",
|
|
24
35
|
recommendation: "APPROVE",
|
|
25
36
|
codeQualityStatus: "CLEAR",
|
|
26
|
-
reportPath:
|
|
37
|
+
reportPath: `${qaDir}/code-review.md`,
|
|
27
38
|
evidence: "Reviewed implementation and tests; no blockers remain.",
|
|
28
39
|
blockers: [],
|
|
29
40
|
},
|
|
@@ -56,20 +67,20 @@ export async function qualityGateJson(repoRoot: string, cliArtifactPath = CLI_PA
|
|
|
56
67
|
id: "artifact-cli-pass",
|
|
57
68
|
kind: "cli-transcript",
|
|
58
69
|
description: "CLI transcript for valid final checkpoint.",
|
|
59
|
-
path:
|
|
70
|
+
path: cliPath,
|
|
60
71
|
},
|
|
61
72
|
{
|
|
62
73
|
id: "artifact-cli-reject",
|
|
63
74
|
kind: "log",
|
|
64
75
|
description: "Log proving invalid final checkpoint rejection.",
|
|
65
|
-
path:
|
|
76
|
+
path: `${qaDir}/rejection.txt`,
|
|
66
77
|
},
|
|
67
78
|
],
|
|
68
79
|
},
|
|
69
80
|
gateReview: {
|
|
70
81
|
by: "lazycodex-gate-reviewer",
|
|
71
82
|
recommendation: "APPROVE",
|
|
72
|
-
reportPath:
|
|
83
|
+
reportPath: `${qaDir}/gate-review.md`,
|
|
73
84
|
evidence: "Verified all criteria and artifact evidence.",
|
|
74
85
|
blockers: [],
|
|
75
86
|
},
|
|
@@ -141,7 +141,9 @@ describe("skills/ulw-loop/SKILL.md", () => {
|
|
|
141
141
|
expect(text).toContain('- "ulw-loop"');
|
|
142
142
|
});
|
|
143
143
|
|
|
144
|
-
it(
|
|
144
|
+
it.skipIf(process.platform === "win32")(
|
|
145
|
+
"#given PATH omo lacks ulw-loop #when bootstrap runs #then falls back to cached ulw-loop CLI",
|
|
146
|
+
async () => {
|
|
145
147
|
const text = await readText("skills/ulw-loop/references/full-workflow.md");
|
|
146
148
|
const bootstrap = bootstrapScriptFrom(text);
|
|
147
149
|
const root = await mkdtemp(join(tmpdir(), "omo-ulw-loop-bootstrap-"));
|
|
@@ -191,7 +193,8 @@ describe("source LOC budget", () => {
|
|
|
191
193
|
it("every source file stays at or under 250 pure LOC", async () => {
|
|
192
194
|
const files = [
|
|
193
195
|
"src/types.ts", "src/paths.ts", "src/plan-io.ts", "src/plan-crud.ts", "src/goal-status.ts",
|
|
194
|
-
"src/evidence.ts", "src/quality-gate.ts", "src/checkpoint.ts", "src/review-blockers.ts",
|
|
196
|
+
"src/evidence.ts", "src/quality-gate.ts", "src/quality-gate-verdicts.ts", "src/checkpoint.ts", "src/review-blockers.ts",
|
|
197
|
+
"src/stop-resume-hook.ts", "src/spawn-guard.ts",
|
|
195
198
|
"src/steering.ts", "src/codex-goal-instruction.ts", "src/codex-goal-snapshot.ts", "src/codex-hook.ts",
|
|
196
199
|
"src/cli.ts", "src/cli-arg-parser.ts", "src/cli-output.ts", "src/cli-steering.ts", "src/cli-commands.ts",
|
|
197
200
|
];
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { join, posix, win32 } from "node:path";
|
|
2
|
+
|
|
1
3
|
import { describe, expect, it } from "vitest";
|
|
2
4
|
|
|
3
5
|
import {
|
|
6
|
+
isWithinAttemptDir,
|
|
4
7
|
normalizeUlwLoopSessionId,
|
|
5
8
|
repoRelative,
|
|
6
9
|
ulwLoopBriefPath,
|
|
@@ -12,28 +15,28 @@ import {
|
|
|
12
15
|
describe("ulwLoopDir(repo)", () => {
|
|
13
16
|
it("returns repo + '/.omo/ulw-loop'", () => {
|
|
14
17
|
// when/then
|
|
15
|
-
expect(ulwLoopDir("/repo")).toBe("/repo
|
|
18
|
+
expect(ulwLoopDir("/repo")).toBe(join("/repo", ".omo", "ulw-loop"));
|
|
16
19
|
});
|
|
17
20
|
|
|
18
21
|
it("#given a session id #when resolving the loop dir #then scopes artifacts under that session", () => {
|
|
19
22
|
// when/then
|
|
20
|
-
expect(ulwLoopDir("/repo", { sessionId: "sess_abc" })).toBe("/repo
|
|
23
|
+
expect(ulwLoopDir("/repo", { sessionId: "sess_abc" })).toBe(join("/repo", ".omo", "ulw-loop", "sess_abc"));
|
|
21
24
|
});
|
|
22
25
|
});
|
|
23
26
|
|
|
24
27
|
describe("ulw-loop*Path helpers", () => {
|
|
25
28
|
it("compose artifact filenames under ulwLoopDir", () => {
|
|
26
29
|
// when/then
|
|
27
|
-
expect(ulwLoopBriefPath("/r")).toBe("/r
|
|
28
|
-
expect(ulwLoopGoalsPath("/r")).toBe("/r
|
|
29
|
-
expect(ulwLoopLedgerPath("/r")).toBe("/r
|
|
30
|
+
expect(ulwLoopBriefPath("/r")).toBe(join("/r", ".omo", "ulw-loop", "brief.md"));
|
|
31
|
+
expect(ulwLoopGoalsPath("/r")).toBe(join("/r", ".omo", "ulw-loop", "goals.json"));
|
|
32
|
+
expect(ulwLoopLedgerPath("/r")).toBe(join("/r", ".omo", "ulw-loop", "ledger.jsonl"));
|
|
30
33
|
});
|
|
31
34
|
|
|
32
35
|
it("#given a session id #when composing artifact filenames #then returns session-scoped paths", () => {
|
|
33
36
|
// when/then
|
|
34
|
-
expect(ulwLoopBriefPath("/r", { sessionId: "session-A" })).toBe("/r
|
|
35
|
-
expect(ulwLoopGoalsPath("/r", { sessionId: "session-A" })).toBe("/r
|
|
36
|
-
expect(ulwLoopLedgerPath("/r", { sessionId: "session-A" })).toBe("/r
|
|
37
|
+
expect(ulwLoopBriefPath("/r", { sessionId: "session-A" })).toBe(join("/r", ".omo", "ulw-loop", "session-A", "brief.md"));
|
|
38
|
+
expect(ulwLoopGoalsPath("/r", { sessionId: "session-A" })).toBe(join("/r", ".omo", "ulw-loop", "session-A", "goals.json"));
|
|
39
|
+
expect(ulwLoopLedgerPath("/r", { sessionId: "session-A" })).toBe(join("/r", ".omo", "ulw-loop", "session-A", "ledger.jsonl"));
|
|
37
40
|
});
|
|
38
41
|
});
|
|
39
42
|
|
|
@@ -60,3 +63,35 @@ describe("repoRelative", () => {
|
|
|
60
63
|
expect(repoRelative("/elsewhere/file", "/repo")).toBe("/elsewhere/file");
|
|
61
64
|
});
|
|
62
65
|
});
|
|
66
|
+
|
|
67
|
+
describe("isWithinAttemptDir", () => {
|
|
68
|
+
const posixRoot = "/repo/.omo/evidence/ulw/s1/g1/a1";
|
|
69
|
+
const win32Root = "C:\\repo\\.omo\\evidence\\ulw\\s1\\g1\\a1";
|
|
70
|
+
|
|
71
|
+
it("#given a child artifact #when checked on posix #then it is contained", () => {
|
|
72
|
+
expect(isWithinAttemptDir(`${posixRoot}/cli-pass.txt`, posixRoot, posix)).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("#given a child artifact #when checked with win32 separators #then it is contained", () => {
|
|
76
|
+
expect(isWithinAttemptDir(`${win32Root}\\cli-pass.txt`, win32Root, win32)).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("#given the attempt root itself #when checked #then it is contained", () => {
|
|
80
|
+
expect(isWithinAttemptDir(posixRoot, posixRoot, posix)).toBe(true);
|
|
81
|
+
expect(isWithinAttemptDir(win32Root, win32Root, win32)).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("#given a sibling dir sharing the prefix #when checked #then it is outside", () => {
|
|
85
|
+
expect(isWithinAttemptDir("/repo/.omo/evidence/ulw/s1/g1/a1x/f.txt", posixRoot, posix)).toBe(false);
|
|
86
|
+
expect(isWithinAttemptDir(`${win32Root}x\\f.txt`, win32Root, win32)).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("#given a prior-attempt artifact #when checked #then it is outside", () => {
|
|
90
|
+
expect(isWithinAttemptDir("/repo/.omo/evidence/ulw/s1/g1/a0/f.txt", posixRoot, posix)).toBe(false);
|
|
91
|
+
expect(isWithinAttemptDir("C:\\repo\\.omo\\evidence\\ulw\\s1\\g1\\a0\\f.txt", win32Root, win32)).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("#given a different-drive path #when checked on win32 #then it is outside", () => {
|
|
95
|
+
expect(isWithinAttemptDir("D:\\elsewhere\\f.txt", win32Root, win32)).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -224,8 +224,8 @@ describe("validateQualityGate", () => {
|
|
|
224
224
|
}),
|
|
225
225
|
);
|
|
226
226
|
|
|
227
|
-
// then
|
|
228
|
-
expect(error.message).toContain("
|
|
227
|
+
// then — a reasonless not_applicable now fails on the missing reason field
|
|
228
|
+
expect(error.message).toContain("reason");
|
|
229
229
|
});
|
|
230
230
|
|
|
231
231
|
it("#given criteria coverage misses required criteria #when validated #then it is rejected", () => {
|
|
@@ -250,3 +250,56 @@ describe("validateQualityGate", () => {
|
|
|
250
250
|
expect(error.message).toContain("criteriaCoverage.userOutcomeReview");
|
|
251
251
|
});
|
|
252
252
|
});
|
|
253
|
+
|
|
254
|
+
describe("quality gate middle states (WATCH / reasoned not_applicable)", () => {
|
|
255
|
+
it("#given codeQualityStatus WATCH with APPROVE #when validating #then the gate accepts", () => {
|
|
256
|
+
const gate = structuredClone(VALID_GATE);
|
|
257
|
+
(gate.codeReview as { codeQualityStatus: string }).codeQualityStatus = "WATCH";
|
|
258
|
+
expect(() => validateQualityGate(gate)).not.toThrow();
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("#given codeQualityStatus BLOCK #when validating #then the gate rejects", () => {
|
|
262
|
+
const gate = structuredClone(VALID_GATE);
|
|
263
|
+
(gate.codeReview as { codeQualityStatus: string }).codeQualityStatus = "BLOCK";
|
|
264
|
+
expect(() => validateQualityGate(gate)).toThrow(/codeQualityStatus/);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("#given a reasoned not_applicable adversarial case #when validating #then the gate accepts", () => {
|
|
268
|
+
const gate = structuredClone(VALID_GATE);
|
|
269
|
+
(gate.manualQa.adversarialCases[0] as { verdict: string; reason?: string }).verdict = "not_applicable";
|
|
270
|
+
(gate.manualQa.adversarialCases[0] as { verdict: string; reason?: string }).reason = "doc-only change";
|
|
271
|
+
expect(() => validateQualityGate(gate)).not.toThrow();
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("#given a reasonless not_applicable adversarial case #when validating #then the gate rejects", () => {
|
|
275
|
+
const gate = structuredClone(VALID_GATE);
|
|
276
|
+
(gate.manualQa.adversarialCases[0] as { verdict: string }).verdict = "not_applicable";
|
|
277
|
+
expect(() => validateQualityGate(gate)).toThrow(/reason/);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("#given a not_applicable surface evidence verdict #when validating #then the gate still rejects", () => {
|
|
281
|
+
const gate = structuredClone(VALID_GATE);
|
|
282
|
+
(gate.manualQa.surfaceEvidence[0] as { verdict: string }).verdict = "not_applicable";
|
|
283
|
+
expect(() => validateQualityGate(gate)).toThrow(/not_applicable/);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
describe("validateQualityGate attempt containment", () => {
|
|
288
|
+
const ATTEMPT_OPTS = { ...FS_OPTS, currentAttemptDir: "test/fixtures/artifacts" } as const;
|
|
289
|
+
|
|
290
|
+
it("#given artifacts inside the current attempt dir #when validating #then the gate accepts", () => {
|
|
291
|
+
expect(() => validateQualityGate(makeGate(), ATTEMPT_OPTS)).not.toThrow();
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("#given an artifact outside the current attempt dir #when validating #then the gate rejects naming the path", () => {
|
|
295
|
+
const opts = { ...FS_OPTS, currentAttemptDir: "test/fixtures/elsewhere" } as const;
|
|
296
|
+
expect(() => validateQualityGate(makeGate(), opts)).toThrow(
|
|
297
|
+
/\(test\/fixtures\/artifacts\/cli-pass\.txt\) must point to an artifact from the current attempt \(test\/fixtures\/elsewhere\)/,
|
|
298
|
+
);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("#given a sibling dir sharing the attempt dir prefix #when validating #then the gate still rejects", () => {
|
|
302
|
+
const opts = { ...FS_OPTS, currentAttemptDir: "test/fixtures/artifact" } as const;
|
|
303
|
+
expect(() => validateQualityGate(makeGate(), opts)).toThrow(/current attempt/);
|
|
304
|
+
});
|
|
305
|
+
});
|