oh-my-opencode 4.16.2 → 4.17.0
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/skills/hyperplan/SKILL.md +3 -3
- package/.agents/skills/omomomo/SKILL.md +1 -1
- package/.agents/skills/pre-publish-review/SKILL.md +3 -0
- package/.opencode/command/omomomo.md +1 -1
- package/.opencode/skills/hyperplan/SKILL.md +3 -3
- package/.opencode/skills/pre-publish-review/SKILL.md +3 -0
- package/README.ja.md +4 -4
- package/README.ko.md +4 -4
- package/README.md +4 -4
- package/README.ru.md +4 -4
- package/README.zh-cn.md +4 -4
- package/dist/agents/hephaestus/agent.d.ts +1 -1
- package/dist/agents/hephaestus/gpt-5-6.d.ts +2 -0
- package/dist/agents/momus-gpt-5-6.d.ts +1 -0
- package/dist/agents/types.d.ts +2 -0
- package/dist/cli/index.js +947 -567
- package/dist/cli-node/index.js +947 -567
- package/dist/features/background-agent/error-classifier.d.ts +5 -0
- package/dist/features/builtin-commands/templates/refactor-sections/intro-and-analysis.d.ts +1 -1
- package/dist/features/builtin-commands/templates/remove-ai-slops.d.ts +1 -1
- package/dist/features/team-mode/test-support/async-test-helpers.d.ts +2 -2
- package/dist/index.js +523 -81
- 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/shared/live-server-route.d.ts +1 -1
- package/dist/skills/remove-ai-slops/SKILL.md +2 -2
- package/dist/skills/review-work/SKILL.md +1 -1
- package/dist/skills/start-work/SKILL.md +5 -2
- package/dist/skills/ulw-plan/references/full-workflow.md +1 -1
- package/dist/skills/ulw-plan/scripts/scaffold-plan.mjs +2 -2
- package/dist/skills/ulw-research/SKILL.md +2 -2
- package/dist/skills/visual-qa/SKILL.md +1 -1
- package/dist/tui.js +195 -41
- package/package.json +15 -14
- package/packages/git-bash-mcp/package.json +27 -0
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +3 -1
- package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +465 -248
- 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/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.6.md +93 -0
- package/packages/omo-codex/plugin/components/rules/dist/cli.js +43 -24
- 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/rules/src/codex-hook.ts +2 -1
- package/packages/omo-codex/plugin/components/rules/src/dynamic-target-fingerprints.ts +15 -6
- package/packages/omo-codex/plugin/components/rules/src/rules-engine-factory.ts +7 -2
- package/packages/omo-codex/plugin/components/rules/src/static-injection.ts +2 -2
- package/packages/omo-codex/plugin/components/rules/test/codex-hook.test.ts +32 -0
- package/packages/omo-codex/plugin/components/rules/test/hephaestus-model-variant.test.ts +106 -0
- 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 +12 -9
- 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 +136 -76
- package/packages/omo-codex/plugin/components/teammode/skills/teammode/scripts/team-guide.mjs +94 -23
- package/packages/omo-codex/plugin/components/teammode/skills/teammode/scripts/team-state.mjs +94 -14
- package/packages/omo-codex/plugin/components/teammode/skills/teammode/scripts/team-transport.mjs +55 -0
- package/packages/omo-codex/plugin/components/teammode/skills/teammode/scripts/team.mjs +72 -18
- package/packages/omo-codex/plugin/components/teammode/test/thread-title-hook.test.ts +23 -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 +23 -40
- package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-clone-fidelity-reviewer.toml +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-code-reviewer.toml +3 -3
- package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-gate-reviewer.toml +7 -7
- 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} +7 -5
- package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-worker-medium.toml +26 -0
- package/packages/omo-codex/plugin/components/ultrawork/agents/librarian.toml +20 -48
- package/packages/omo-codex/plugin/components/ultrawork/agents/metis.toml +17 -29
- package/packages/omo-codex/plugin/components/ultrawork/agents/momus.toml +20 -50
- package/packages/omo-codex/plugin/components/ultrawork/agents/plan.toml +22 -43
- package/packages/omo-codex/plugin/components/ultrawork/directive.md +55 -28
- 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 +55 -28
- 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/scripts/scaffold-plan.mjs +2 -2
- package/packages/omo-codex/plugin/components/ultrawork/test/codex-hook.test.ts +22 -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 +55 -28
- 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 +29 -27
- 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/auto-update.mjs +9 -2
- package/packages/omo-codex/plugin/scripts/entry-guard.mjs +26 -0
- package/packages/omo-codex/plugin/scripts/materialize-shared-upstreams.mjs +3 -1
- package/packages/omo-codex/plugin/scripts/migrate-codex-config/catalog.mjs +16 -7
- package/packages/omo-codex/plugin/scripts/migrate-codex-config/multi-agent-v2-guard.mjs +39 -4
- package/packages/omo-codex/plugin/scripts/migrate-codex-config/subagent-limit-guard.mjs +24 -4
- package/packages/omo-codex/plugin/scripts/migrate-codex-config.mjs +3 -3
- package/packages/omo-codex/plugin/scripts/migrate-omo-sot.mjs +2 -2
- package/packages/omo-codex/plugin/scripts/sync-hook-status-messages.mjs +2 -2
- package/packages/omo-codex/plugin/scripts/sync-skills.mjs +14 -6
- package/packages/omo-codex/plugin/scripts/sync-version.mjs +4 -2
- package/packages/omo-codex/plugin/skills/init-deep/SKILL.md +3 -3
- package/packages/omo-codex/plugin/skills/refactor/SKILL.md +3 -3
- package/packages/omo-codex/plugin/skills/remove-ai-slops/SKILL.md +5 -5
- package/packages/omo-codex/plugin/skills/review-work/SKILL.md +9 -3
- package/packages/omo-codex/plugin/skills/start-work/SKILL.md +6 -3
- package/packages/omo-codex/plugin/skills/teammode/SKILL.md +136 -76
- package/packages/omo-codex/plugin/skills/teammode/scripts/team-guide.mjs +94 -23
- package/packages/omo-codex/plugin/skills/teammode/scripts/team-state.mjs +94 -14
- package/packages/omo-codex/plugin/skills/teammode/scripts/team-transport.mjs +55 -0
- package/packages/omo-codex/plugin/skills/teammode/scripts/team.mjs +72 -18
- package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +55 -28
- package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +14 -15
- package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +29 -27
- package/packages/omo-codex/plugin/skills/ulw-plan/references/full-workflow.md +1 -1
- package/packages/omo-codex/plugin/skills/ulw-plan/scripts/scaffold-plan.mjs +2 -2
- package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +4 -4
- package/packages/omo-codex/plugin/skills/visual-qa/SKILL.md +4 -4
- package/packages/omo-codex/plugin/test/aggregate-agents.test.mjs +116 -10
- 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/aggregate-skills.test.mjs +4 -2
- package/packages/omo-codex/plugin/test/auto-update-restart-notice.test.mjs +33 -0
- 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 +160 -23
- package/packages/omo-codex/plugin/test/multi-agent-v2-regression.test.mjs +71 -0
- package/packages/omo-codex/plugin/test/subagent-limit-migration.test.mjs +32 -0
- package/packages/omo-codex/plugin/test/sync-skills-orchestration.test.mjs +20 -2
- package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +1 -1
- package/packages/omo-codex/plugin/test/sync-skills.test.mjs +4 -2
- package/packages/omo-codex/plugin/test/teammode-transport.test.mjs +345 -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 +508 -286
- package/packages/omo-codex/scripts/install-generated-bundle.test.mjs +111 -1
- package/packages/shared-skills/skills/remove-ai-slops/SKILL.md +2 -2
- package/packages/shared-skills/skills/review-work/SKILL.md +1 -1
- package/packages/shared-skills/skills/start-work/SKILL.md +5 -2
- package/packages/shared-skills/skills/ulw-plan/references/full-workflow.md +1 -1
- package/packages/shared-skills/skills/ulw-plan/scripts/scaffold-plan.mjs +2 -2
- package/packages/shared-skills/skills/ulw-research/SKILL.md +2 -2
- package/packages/shared-skills/skills/visual-qa/SKILL.md +1 -1
- 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
- /package/packages/omo-codex/plugin/components/rules/bundled-rules/{hephaestus.md → hephaestus/gpt-5.5.md} +0 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ulwLoopAttemptEvidenceDir } from "./paths.js";
|
|
1
2
|
import { existsSync, statSync } from "node:fs";
|
|
2
3
|
import { readFile } from "node:fs/promises";
|
|
3
4
|
import { resolve } from "node:path";
|
|
@@ -79,12 +80,13 @@ function ledgerKind(status, goal, aggregateCompletion) {
|
|
|
79
80
|
return status === "blocked" ? "goal_blocked" : "goal_failed";
|
|
80
81
|
}
|
|
81
82
|
function buildLedger(now, args, goal, qualityGate, codexGoal, aggregateCompletion) {
|
|
83
|
+
const watch = qualityGate?.codeReview.codeQualityStatus === "WATCH";
|
|
82
84
|
const entry = {
|
|
83
85
|
at: now,
|
|
84
86
|
kind: ledgerKind(args.status, goal, aggregateCompletion),
|
|
85
87
|
goalId: goal.id,
|
|
86
88
|
status: goal.status,
|
|
87
|
-
evidence: args.evidence,
|
|
89
|
+
evidence: watch ? `${args.evidence} | codeQuality=WATCH: ${qualityGate.codeReview.evidence}` : args.evidence,
|
|
88
90
|
};
|
|
89
91
|
if (codexGoal !== undefined)
|
|
90
92
|
entry.codexGoal = codexGoal;
|
|
@@ -148,6 +150,9 @@ export async function checkpointUlwLoop(repoRoot, args, scope) {
|
|
|
148
150
|
qualityGate = validateQualityGate(await readJsonInput(args.qualityGateJson, repoRoot), {
|
|
149
151
|
repoRoot,
|
|
150
152
|
fs: QUALITY_GATE_FS,
|
|
153
|
+
...(plan.evidenceLayoutVersion === 2
|
|
154
|
+
? { currentAttemptDir: ulwLoopAttemptEvidenceDir(goal.id, goal.attempt, scope) }
|
|
155
|
+
: {}),
|
|
151
156
|
});
|
|
152
157
|
goal.status = "complete";
|
|
153
158
|
goal.completedAt = now;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type UlwLoopScope } from "./paths.js";
|
|
2
2
|
export declare function createGoals(repoRoot: string, argv: readonly string[], json: boolean, scope?: UlwLoopScope): Promise<number>;
|
|
3
3
|
export declare function status(repoRoot: string, json: boolean, scope?: UlwLoopScope): Promise<number>;
|
|
4
4
|
export declare function completeGoals(repoRoot: string, argv: readonly string[], json: boolean, scope?: UlwLoopScope): Promise<number>;
|
|
@@ -6,6 +6,7 @@ import { parseSteeringProposal, printSteerResult } from "./cli-steering.js";
|
|
|
6
6
|
import { buildCodexGoalInstruction } from "./codex-goal-instruction.js";
|
|
7
7
|
import { recordEvidence } from "./evidence.js";
|
|
8
8
|
import { isEssentialCriterion } from "./goal-status.js";
|
|
9
|
+
import { ulwLoopAttemptEvidenceDir } from "./paths.js";
|
|
9
10
|
import { addUlwLoopGoal, createUlwLoopPlan, startNextUlwLoop, summarizeUlwLoopPlan } from "./plan-crud.js";
|
|
10
11
|
import { readUlwLoopPlan } from "./plan-io.js";
|
|
11
12
|
import { recordFinalReviewBlockers } from "./review-blockers.js";
|
|
@@ -34,8 +35,18 @@ export async function createGoals(repoRoot, argv, json, scope) {
|
|
|
34
35
|
}
|
|
35
36
|
export async function status(repoRoot, json, scope) {
|
|
36
37
|
const plan = await readUlwLoopPlan(repoRoot, scope);
|
|
37
|
-
if (json)
|
|
38
|
-
|
|
38
|
+
if (json) {
|
|
39
|
+
const active = plan.goals.find((goal) => goal.id === plan.activeGoalId);
|
|
40
|
+
const currentAttemptDir = plan.evidenceLayoutVersion === 2 && active
|
|
41
|
+
? ulwLoopAttemptEvidenceDir(active.id, active.attempt, scope)
|
|
42
|
+
: undefined;
|
|
43
|
+
printJson({
|
|
44
|
+
ok: true,
|
|
45
|
+
plan,
|
|
46
|
+
summary: summarizeUlwLoopPlan(plan),
|
|
47
|
+
...(currentAttemptDir === undefined ? {} : { currentAttemptDir }),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
39
50
|
else
|
|
40
51
|
printStatus(plan);
|
|
41
52
|
return 0;
|
|
@@ -209,17 +209,8 @@ function normalizeCodexGoalMode(value) {
|
|
|
209
209
|
// components/ulw-loop/src/cli-subcommands.ts
|
|
210
210
|
import { readFile as readFile6 } from "node:fs/promises";
|
|
211
211
|
|
|
212
|
-
// components/ulw-loop/src/checkpoint.ts
|
|
213
|
-
import { existsSync as existsSync3, statSync } from "node:fs";
|
|
214
|
-
import { readFile as readFile5 } from "node:fs/promises";
|
|
215
|
-
import { resolve as resolve3 } from "node:path";
|
|
216
|
-
|
|
217
|
-
// components/ulw-loop/src/checkpoint-reconciliation.ts
|
|
218
|
-
import { existsSync } from "node:fs";
|
|
219
|
-
import { readFile as readFile2 } from "node:fs/promises";
|
|
220
|
-
|
|
221
212
|
// components/ulw-loop/src/paths.ts
|
|
222
|
-
import { join } from "node:path";
|
|
213
|
+
import { isAbsolute, join, relative, sep } from "node:path";
|
|
223
214
|
var SESSION_ENV_KEYS = ["OMO_ULW_LOOP_SESSION_ID", "CODEX_SESSION_ID", "CODEX_THREAD_ID"];
|
|
224
215
|
function normalizeUlwLoopSessionId(sessionId) {
|
|
225
216
|
const trimmed = sessionId?.trim();
|
|
@@ -271,6 +262,28 @@ function repoRelative(absolutePath, repoRoot) {
|
|
|
271
262
|
return absolutePath.slice(backslashPrefix.length).split("\\").join("/");
|
|
272
263
|
return absolutePath.split("\\").join("/");
|
|
273
264
|
}
|
|
265
|
+
function ulwLoopAttemptEvidenceDir(goalId, attempt, scope) {
|
|
266
|
+
const sessionId = normalizeUlwLoopSessionId(scope?.sessionId) ?? resolveUlwLoopSessionIdFromEnv() ?? "session";
|
|
267
|
+
return `.omo/evidence/ulw/${sessionId}/${goalId}/a${attempt}`;
|
|
268
|
+
}
|
|
269
|
+
var PLATFORM_PATH_API = { relative, isAbsolute, sep };
|
|
270
|
+
function isWithinAttemptDir(absolutePath, attemptRoot, pathApi = PLATFORM_PATH_API) {
|
|
271
|
+
const relativePath = pathApi.relative(attemptRoot, absolutePath);
|
|
272
|
+
if (relativePath === "")
|
|
273
|
+
return true;
|
|
274
|
+
if (relativePath === ".." || relativePath.startsWith(`..${pathApi.sep}`))
|
|
275
|
+
return false;
|
|
276
|
+
return !pathApi.isAbsolute(relativePath);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// components/ulw-loop/src/checkpoint.ts
|
|
280
|
+
import { existsSync as existsSync3, statSync } from "node:fs";
|
|
281
|
+
import { readFile as readFile5 } from "node:fs/promises";
|
|
282
|
+
import { resolve as resolve3 } from "node:path";
|
|
283
|
+
|
|
284
|
+
// components/ulw-loop/src/checkpoint-reconciliation.ts
|
|
285
|
+
import { existsSync } from "node:fs";
|
|
286
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
274
287
|
|
|
275
288
|
// components/ulw-loop/src/goal-status.ts
|
|
276
289
|
var ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE = aggregateCodexObjectiveForScope();
|
|
@@ -779,6 +792,27 @@ function literal(value, expected, field) {
|
|
|
779
792
|
invalid(`${field} must be ${String(expected)}.`, field);
|
|
780
793
|
}
|
|
781
794
|
|
|
795
|
+
// components/ulw-loop/src/quality-gate-verdicts.ts
|
|
796
|
+
function passedVerdict(value, field) {
|
|
797
|
+
if (value === "not_applicable")
|
|
798
|
+
invalid(`${field} must not be not_applicable.`, field);
|
|
799
|
+
return literal(value, "passed", field);
|
|
800
|
+
}
|
|
801
|
+
function codeQualityStatusField(value, field) {
|
|
802
|
+
if (value === "CLEAR" || value === "WATCH")
|
|
803
|
+
return value;
|
|
804
|
+
invalid(`${field} must be CLEAR or WATCH.`, field);
|
|
805
|
+
}
|
|
806
|
+
function adversarialVerdict(row, field) {
|
|
807
|
+
const value = row["verdict"];
|
|
808
|
+
if (value === "passed")
|
|
809
|
+
return { verdict: "passed" };
|
|
810
|
+
if (value === "not_applicable") {
|
|
811
|
+
return { verdict: "not_applicable", reason: textField(row["reason"], `${field}.reason`) };
|
|
812
|
+
}
|
|
813
|
+
invalid(`${field} must be passed or not_applicable with a reason.`, field);
|
|
814
|
+
}
|
|
815
|
+
|
|
782
816
|
// components/ulw-loop/src/quality-gate-blockers.ts
|
|
783
817
|
var BLOCKER_FIELD_KEYS = "blocker blockerSignature blockerEvidence blockerOccurrences blockedAt".split(" ");
|
|
784
818
|
var URL_PATTERN = /https?:\/\/\S+/g;
|
|
@@ -843,11 +877,6 @@ function kindField(value, field) {
|
|
|
843
877
|
return value;
|
|
844
878
|
invalid(`${field} must be a supported artifact kind.`, field);
|
|
845
879
|
}
|
|
846
|
-
function passedVerdict(value, field) {
|
|
847
|
-
if (value === "not_applicable")
|
|
848
|
-
invalid(`${field} must not be not_applicable.`, field);
|
|
849
|
-
return literal(value, "passed", field);
|
|
850
|
-
}
|
|
851
880
|
function artifactCompatible(surface, kind) {
|
|
852
881
|
switch (surface) {
|
|
853
882
|
case "cli":
|
|
@@ -870,9 +899,13 @@ function checkFile(path, field, opts) {
|
|
|
870
899
|
const absolute = resolve2(opts.repoRoot, path);
|
|
871
900
|
if (!opts.fs.existsSync(absolute))
|
|
872
901
|
invalid(`${field} must point to an existing artifact.`, field);
|
|
873
|
-
|
|
874
|
-
if (stat.size <= 0)
|
|
902
|
+
if (opts.fs.statSync(absolute).size <= 0)
|
|
875
903
|
invalid(`${field} must point to a non-empty artifact.`, field);
|
|
904
|
+
if (opts.currentAttemptDir !== undefined) {
|
|
905
|
+
const attemptRoot = resolve2(opts.repoRoot, opts.currentAttemptDir);
|
|
906
|
+
if (!isWithinAttemptDir(absolute, attemptRoot))
|
|
907
|
+
invalid(`${field} (${path}) must point to an artifact from the current attempt (${opts.currentAttemptDir}).`, field);
|
|
908
|
+
}
|
|
876
909
|
}
|
|
877
910
|
function artifactMap(refs) {
|
|
878
911
|
const byId = new Map;
|
|
@@ -929,7 +962,7 @@ function validateQualityGate(input, opts) {
|
|
|
929
962
|
codeReview: {
|
|
930
963
|
by: reviewerRoleField(codeReview["by"], REVIEWER_ROLES.codeReview, "codeReview.by"),
|
|
931
964
|
recommendation: literal(codeReview["recommendation"], "APPROVE", "codeReview.recommendation"),
|
|
932
|
-
codeQualityStatus:
|
|
965
|
+
codeQualityStatus: codeQualityStatusField(codeReview["codeQualityStatus"], "codeReview.codeQualityStatus"),
|
|
933
966
|
reportPath: codeReportPath,
|
|
934
967
|
evidence: textField(codeReview["evidence"], "codeReview.evidence"),
|
|
935
968
|
blockers: emptyBlockers(codeReview["blockers"], "codeReview.blockers")
|
|
@@ -993,12 +1026,14 @@ function parseAdversarialCases(value, byId) {
|
|
|
993
1026
|
return value.map((item, index) => {
|
|
994
1027
|
const row = section(item, `manualQa.adversarialCases[${index}]`);
|
|
995
1028
|
const artifacts = referencedArtifacts(row["artifactRefs"], `manualQa.adversarialCases[${index}].artifactRefs`, byId);
|
|
1029
|
+
const verdictInfo = adversarialVerdict(row, `manualQa.adversarialCases[${index}]`);
|
|
996
1030
|
return {
|
|
997
1031
|
id: textField(row["id"], `manualQa.adversarialCases[${index}].id`),
|
|
998
1032
|
criterionRef: textField(row["criterionRef"], `manualQa.adversarialCases[${index}].criterionRef`),
|
|
999
1033
|
scenario: textField(row["scenario"], `manualQa.adversarialCases[${index}].scenario`),
|
|
1000
1034
|
expectedBehavior: textField(row["expectedBehavior"], `manualQa.adversarialCases[${index}].expectedBehavior`),
|
|
1001
|
-
verdict:
|
|
1035
|
+
verdict: verdictInfo.verdict,
|
|
1036
|
+
...verdictInfo.reason === undefined ? {} : { reason: verdictInfo.reason },
|
|
1002
1037
|
artifactRefs: artifacts.map((artifact) => artifact.id)
|
|
1003
1038
|
};
|
|
1004
1039
|
});
|
|
@@ -1074,12 +1109,13 @@ function ledgerKind2(status, goal, aggregateCompletion) {
|
|
|
1074
1109
|
return status === "blocked" ? "goal_blocked" : "goal_failed";
|
|
1075
1110
|
}
|
|
1076
1111
|
function buildLedger(now, args, goal, qualityGate, codexGoal, aggregateCompletion) {
|
|
1112
|
+
const watch = qualityGate?.codeReview.codeQualityStatus === "WATCH";
|
|
1077
1113
|
const entry = {
|
|
1078
1114
|
at: now,
|
|
1079
1115
|
kind: ledgerKind2(args.status, goal, aggregateCompletion),
|
|
1080
1116
|
goalId: goal.id,
|
|
1081
1117
|
status: goal.status,
|
|
1082
|
-
evidence: args.evidence
|
|
1118
|
+
evidence: watch ? `${args.evidence} | codeQuality=WATCH: ${qualityGate.codeReview.evidence}` : args.evidence
|
|
1083
1119
|
};
|
|
1084
1120
|
if (codexGoal !== undefined)
|
|
1085
1121
|
entry.codexGoal = codexGoal;
|
|
@@ -1135,7 +1171,8 @@ async function checkpointUlwLoop(repoRoot, args, scope) {
|
|
|
1135
1171
|
if (final || aggregateCompletion !== undefined)
|
|
1136
1172
|
qualityGate = validateQualityGate(await readJsonInput2(args.qualityGateJson, repoRoot), {
|
|
1137
1173
|
repoRoot,
|
|
1138
|
-
fs: QUALITY_GATE_FS
|
|
1174
|
+
fs: QUALITY_GATE_FS,
|
|
1175
|
+
...plan.evidenceLayoutVersion === 2 ? { currentAttemptDir: ulwLoopAttemptEvidenceDir(goal.id, goal.attempt, scope) } : {}
|
|
1139
1176
|
});
|
|
1140
1177
|
goal.status = "complete";
|
|
1141
1178
|
goal.completedAt = now;
|
|
@@ -1391,6 +1428,7 @@ function buildText(mode, plan, goal, createGoal, isFinal) {
|
|
|
1391
1428
|
"- Use the create_goal payload exactly as rendered: objective only.",
|
|
1392
1429
|
"- Goals are unlimited. Do not add numeric limits.",
|
|
1393
1430
|
...modeConstraintLines(mode, isFinal),
|
|
1431
|
+
...evidenceLayoutLines(plan),
|
|
1394
1432
|
finalSection(plan, goal, isFinal, mode === "aggregate"),
|
|
1395
1433
|
...checkpointLines(plan, mode),
|
|
1396
1434
|
"",
|
|
@@ -1436,6 +1474,13 @@ function formatCriterionLine(criterion) {
|
|
|
1436
1474
|
const marker = isEssentialCriterion(criterion) ? "essential" : "non-essential";
|
|
1437
1475
|
return `-${remainingWork} [${criterion.id}] [${marker}] (${criterion.userModel}) ${criterion.scenario} — expect: ${criterion.expectedEvidence} — status: ${criterion.status}`;
|
|
1438
1476
|
}
|
|
1477
|
+
function evidenceLayoutLines(plan) {
|
|
1478
|
+
if (plan.evidenceLayoutVersion !== 2)
|
|
1479
|
+
return [];
|
|
1480
|
+
return [
|
|
1481
|
+
"- Evidence layout v2: write every artifact for the active goal (QA matrix, review reports, receipts) under the current attempt directory — read currentAttemptDir from `omo ulw-loop status --json` (.omo/evidence/ulw/<session>/<goalId>/a<attempt>). The final checkpoint rejects quality-gate artifacts outside that directory."
|
|
1482
|
+
];
|
|
1483
|
+
}
|
|
1439
1484
|
function finalSection(plan, goal, isFinal, aggregate) {
|
|
1440
1485
|
if (!isFinal)
|
|
1441
1486
|
return "- This is not the final ulw-loop story; do not run the final reviewer/manual-QA/gate-review quality gate yet.";
|
|
@@ -1446,8 +1491,11 @@ function finalSection(plan, goal, isFinal, aggregate) {
|
|
|
1446
1491
|
"Final story — run mandatory quality gate before update_goal:",
|
|
1447
1492
|
"- Run targeted verification for changed behavior.",
|
|
1448
1493
|
"- Confirm every manualQa artifact path exists and has non-zero size.",
|
|
1449
|
-
|
|
1494
|
+
'- First spawn lazycodex-code-reviewer and lazycodex-qa-executor 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).',
|
|
1495
|
+
"- Only then spawn lazycodex-gate-reviewer (same fork settings), passing those artifact paths.",
|
|
1450
1496
|
"- Require clean codeReview, manualQa, gateReview, iteration, and criteriaCoverage. criteriaCoverage must summarize originalIntent, desiredOutcome, and userOutcomeReview; counts alone are not approval.",
|
|
1497
|
+
"- 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.",
|
|
1498
|
+
"- If codeQualityStatus is WATCH, include the WATCH notes verbatim in your final user-facing message.",
|
|
1451
1499
|
"- If any reviewer is blocked/inconclusive or the quality gate is not clean, do not call update_goal. Record blocker work first:",
|
|
1452
1500
|
` ${blockerCommand}`,
|
|
1453
1501
|
aggregate ? '- If the quality gate is clean, call update_goal({status: "complete"}), call get_goal again, then checkpoint the aggregate story:' : '- If the quality gate is clean, call update_goal({status: "complete"}), call get_goal again, then checkpoint:',
|
|
@@ -1588,6 +1636,7 @@ async function createUlwLoopPlan(repoRoot, args, scope) {
|
|
|
1588
1636
|
const goals = deriveGoalCandidates(args.brief).map((goal, index) => makeGoal(goal.title, goal.objective, index, now));
|
|
1589
1637
|
const plan = {
|
|
1590
1638
|
version: 1,
|
|
1639
|
+
evidenceLayoutVersion: 2,
|
|
1591
1640
|
createdAt: now,
|
|
1592
1641
|
updatedAt: now,
|
|
1593
1642
|
briefPath: ulwLoopBriefRelativePath(scope),
|
|
@@ -2087,9 +2136,16 @@ ledger: ${plan.ledgerPath}
|
|
|
2087
2136
|
}
|
|
2088
2137
|
async function status(repoRoot, json, scope) {
|
|
2089
2138
|
const plan = await readUlwLoopPlan(repoRoot, scope);
|
|
2090
|
-
if (json)
|
|
2091
|
-
|
|
2092
|
-
|
|
2139
|
+
if (json) {
|
|
2140
|
+
const active = plan.goals.find((goal2) => goal2.id === plan.activeGoalId);
|
|
2141
|
+
const currentAttemptDir = plan.evidenceLayoutVersion === 2 && active ? ulwLoopAttemptEvidenceDir(active.id, active.attempt, scope) : undefined;
|
|
2142
|
+
printJson({
|
|
2143
|
+
ok: true,
|
|
2144
|
+
plan,
|
|
2145
|
+
summary: summarizeUlwLoopPlan(plan),
|
|
2146
|
+
...currentAttemptDir === undefined ? {} : { currentAttemptDir }
|
|
2147
|
+
});
|
|
2148
|
+
} else
|
|
2093
2149
|
printStatus(plan);
|
|
2094
2150
|
return 0;
|
|
2095
2151
|
}
|
|
@@ -2622,6 +2678,322 @@ function readAll(stdin) {
|
|
|
2622
2678
|
});
|
|
2623
2679
|
}
|
|
2624
2680
|
|
|
2681
|
+
// components/ulw-loop/src/spawn-guard.ts
|
|
2682
|
+
import { existsSync as existsSync6, readdirSync, readFileSync as readFileSync3, statSync as statSync2, writeFileSync } from "node:fs";
|
|
2683
|
+
import { join as join2 } from "node:path";
|
|
2684
|
+
var SPAWN_TOOL_TOKENS = new Set(["spawn_agent", "collaborationspawn_agent", "collaboration.spawn_agent"]);
|
|
2685
|
+
var DEFAULT_FANOUT_LIMIT = 60;
|
|
2686
|
+
var GATE_MESSAGE_PATTERN = /lazycodex-gate-reviewer|final gate review/i;
|
|
2687
|
+
function applySpawnGuards(payload) {
|
|
2688
|
+
if (payload.hook_event_name !== "PreToolUse" || !SPAWN_TOOL_TOKENS.has(payload.tool_name))
|
|
2689
|
+
return "";
|
|
2690
|
+
const stateDir = ulwLoopDir(payload.cwd, { sessionId: payload.session_id });
|
|
2691
|
+
const plan = readPlan(join2(stateDir, "goals.json"));
|
|
2692
|
+
if (plan === null)
|
|
2693
|
+
return "";
|
|
2694
|
+
const fanOutDenial = consumeFanOutBudget(stateDir);
|
|
2695
|
+
if (fanOutDenial !== null)
|
|
2696
|
+
return deny(fanOutDenial);
|
|
2697
|
+
const missingArtifact = missingGateArtifact(payload, plan);
|
|
2698
|
+
if (missingArtifact !== null)
|
|
2699
|
+
return deny(`spawn code-review + QA first; gate audits their artifacts: missing ${missingArtifact}`);
|
|
2700
|
+
return "";
|
|
2701
|
+
}
|
|
2702
|
+
async function runSpawnGuardCli(stdin, stdout) {
|
|
2703
|
+
try {
|
|
2704
|
+
const chunks = [];
|
|
2705
|
+
for await (const chunk of stdin)
|
|
2706
|
+
chunks.push(Buffer.from(chunk));
|
|
2707
|
+
const payload = parsePreToolUsePayload(Buffer.concat(chunks).toString("utf8"));
|
|
2708
|
+
if (payload === null)
|
|
2709
|
+
return;
|
|
2710
|
+
const output = applySpawnGuards(payload);
|
|
2711
|
+
if (output.length > 0)
|
|
2712
|
+
stdout.write(output);
|
|
2713
|
+
} catch (error) {
|
|
2714
|
+
if (error instanceof Error)
|
|
2715
|
+
return;
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
function consumeFanOutBudget(stateDir) {
|
|
2719
|
+
const counterPath = join2(stateDir, "spawn-count.json");
|
|
2720
|
+
const count = readCount(counterPath) + 1;
|
|
2721
|
+
writeFileSync(counterPath, JSON.stringify({ count }));
|
|
2722
|
+
const limit = fanOutLimit();
|
|
2723
|
+
if (count <= limit)
|
|
2724
|
+
return null;
|
|
2725
|
+
return `ulw-loop spawn fan-out cap reached (${count}/${limit}). Consolidate work into the agents already running, or raise OMO_SPAWN_FANOUT_LIMIT if this volume is intentional.`;
|
|
2726
|
+
}
|
|
2727
|
+
function missingGateArtifact(payload, plan) {
|
|
2728
|
+
if (!isGateReviewerSpawn(payload.tool_input))
|
|
2729
|
+
return null;
|
|
2730
|
+
const goal2 = plan.goals.find((candidate) => isFinalRunCompletionCandidate(plan, candidate));
|
|
2731
|
+
if (goal2 === undefined || goal2.status === "complete")
|
|
2732
|
+
return null;
|
|
2733
|
+
if (!goal2.successCriteria.every((criterion) => criterion.status === "pass"))
|
|
2734
|
+
return null;
|
|
2735
|
+
const scope = { sessionId: payload.session_id };
|
|
2736
|
+
if (plan.evidenceLayoutVersion === 2) {
|
|
2737
|
+
const attemptDir = ulwLoopAttemptEvidenceDir(goal2.id, goal2.attempt, scope);
|
|
2738
|
+
for (const name of [`${goal2.id}-code-review.md`, `${goal2.id}-manual-qa.md`]) {
|
|
2739
|
+
const relative2 = `${attemptDir}/${name}`;
|
|
2740
|
+
if (!isNonEmptyFile(join2(payload.cwd, relative2)))
|
|
2741
|
+
return relative2;
|
|
2742
|
+
}
|
|
2743
|
+
return null;
|
|
2744
|
+
}
|
|
2745
|
+
const flatReport = `.omo/evidence/${goal2.id}-code-review.md`;
|
|
2746
|
+
if (!isNonEmptyFile(join2(payload.cwd, flatReport)))
|
|
2747
|
+
return flatReport;
|
|
2748
|
+
if (!hasOtherEvidenceFile(join2(payload.cwd, ".omo", "evidence"), `${goal2.id}-code-review.md`))
|
|
2749
|
+
return `.omo/evidence/<any manual-QA artifact besides ${goal2.id}-code-review.md>`;
|
|
2750
|
+
return null;
|
|
2751
|
+
}
|
|
2752
|
+
function isGateReviewerSpawn(toolInput) {
|
|
2753
|
+
if (typeof toolInput !== "object" || toolInput === null)
|
|
2754
|
+
return false;
|
|
2755
|
+
const record = toolInput;
|
|
2756
|
+
const agentType = record["agent_type"];
|
|
2757
|
+
if (typeof agentType === "string")
|
|
2758
|
+
return agentType === "lazycodex-gate-reviewer";
|
|
2759
|
+
const message = record["message"];
|
|
2760
|
+
return typeof message === "string" && GATE_MESSAGE_PATTERN.test(message);
|
|
2761
|
+
}
|
|
2762
|
+
function deny(reason) {
|
|
2763
|
+
return `${JSON.stringify({
|
|
2764
|
+
hookSpecificOutput: {
|
|
2765
|
+
hookEventName: "PreToolUse",
|
|
2766
|
+
permissionDecision: "deny",
|
|
2767
|
+
permissionDecisionReason: reason,
|
|
2768
|
+
additionalContext: reason
|
|
2769
|
+
}
|
|
2770
|
+
})}
|
|
2771
|
+
`;
|
|
2772
|
+
}
|
|
2773
|
+
function fanOutLimit() {
|
|
2774
|
+
const raw = process.env["OMO_SPAWN_FANOUT_LIMIT"];
|
|
2775
|
+
if (raw === undefined)
|
|
2776
|
+
return DEFAULT_FANOUT_LIMIT;
|
|
2777
|
+
const parsed = Number.parseInt(raw, 10);
|
|
2778
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_FANOUT_LIMIT;
|
|
2779
|
+
}
|
|
2780
|
+
function isNonEmptyFile(path) {
|
|
2781
|
+
try {
|
|
2782
|
+
return existsSync6(path) && statSync2(path).size > 0;
|
|
2783
|
+
} catch (error) {
|
|
2784
|
+
if (error instanceof Error)
|
|
2785
|
+
return false;
|
|
2786
|
+
throw error;
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
function hasOtherEvidenceFile(evidenceDir, excludedName) {
|
|
2790
|
+
try {
|
|
2791
|
+
return readdirSync(evidenceDir).some((name) => name !== excludedName && isNonEmptyFile(join2(evidenceDir, name)));
|
|
2792
|
+
} catch (error) {
|
|
2793
|
+
if (error instanceof Error)
|
|
2794
|
+
return false;
|
|
2795
|
+
throw error;
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2798
|
+
function readCount(counterPath) {
|
|
2799
|
+
try {
|
|
2800
|
+
const parsed = JSON.parse(readFileSync3(counterPath, "utf8"));
|
|
2801
|
+
return typeof parsed["count"] === "number" && parsed["count"] >= 0 ? parsed["count"] : 0;
|
|
2802
|
+
} catch (error) {
|
|
2803
|
+
if (error instanceof Error)
|
|
2804
|
+
return 0;
|
|
2805
|
+
throw error;
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
function readPlan(goalsPath) {
|
|
2809
|
+
try {
|
|
2810
|
+
return JSON.parse(readFileSync3(goalsPath, "utf8"));
|
|
2811
|
+
} catch (error) {
|
|
2812
|
+
if (error instanceof Error)
|
|
2813
|
+
return null;
|
|
2814
|
+
throw error;
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
|
|
2818
|
+
// components/ulw-loop/src/stop-resume-hook.ts
|
|
2819
|
+
import { existsSync as existsSync7, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2820
|
+
import { isAbsolute as isAbsolute2, join as join3 } from "node:path";
|
|
2821
|
+
var RESUME_CAP = 2;
|
|
2822
|
+
var CONTEXT_PRESSURE_MARKERS2 = [
|
|
2823
|
+
"context compacted",
|
|
2824
|
+
"context_length_exceeded",
|
|
2825
|
+
"skill descriptions were shortened",
|
|
2826
|
+
"context_too_large",
|
|
2827
|
+
"codex ran out of room in the model's context window",
|
|
2828
|
+
"your input exceeds the context window",
|
|
2829
|
+
"long threads and multiple compactions"
|
|
2830
|
+
];
|
|
2831
|
+
function runStopResumeHook(input) {
|
|
2832
|
+
const payload = parseStopPayload(input);
|
|
2833
|
+
if (payload === null || payload.stop_hook_active)
|
|
2834
|
+
return "";
|
|
2835
|
+
if (transcriptShowsContextPressure(payload.transcript_path))
|
|
2836
|
+
return "";
|
|
2837
|
+
if (boulderContinuationWillFire(payload.cwd, payload.session_id))
|
|
2838
|
+
return "";
|
|
2839
|
+
const stateDir = ulwLoopDir(payload.cwd, { sessionId: payload.session_id });
|
|
2840
|
+
const plan = readPlan2(join3(stateDir, "goals.json"));
|
|
2841
|
+
if (plan === null || plan.aggregateCompletion?.status === "complete")
|
|
2842
|
+
return "";
|
|
2843
|
+
const goal2 = resumableGoal(plan);
|
|
2844
|
+
if (goal2 === undefined)
|
|
2845
|
+
return "";
|
|
2846
|
+
if (!consumeResumeBudget(stateDir, goal2.id))
|
|
2847
|
+
return "";
|
|
2848
|
+
const output = {
|
|
2849
|
+
decision: "block",
|
|
2850
|
+
reason: renderResumeDirective(plan, goal2, payload.session_id)
|
|
2851
|
+
};
|
|
2852
|
+
return JSON.stringify(output);
|
|
2853
|
+
}
|
|
2854
|
+
async function runStopResumeHookCli(stdin, stdout) {
|
|
2855
|
+
try {
|
|
2856
|
+
const chunks = [];
|
|
2857
|
+
for await (const chunk of stdin)
|
|
2858
|
+
chunks.push(Buffer.from(chunk));
|
|
2859
|
+
const output = runStopResumeHook(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
2860
|
+
if (output.length > 0)
|
|
2861
|
+
stdout.write(output);
|
|
2862
|
+
} catch (error) {
|
|
2863
|
+
if (error instanceof Error)
|
|
2864
|
+
return;
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
function resumableGoal(plan) {
|
|
2868
|
+
const active = plan.goals.find((goal2) => goal2.id === plan.activeGoalId);
|
|
2869
|
+
if (active !== undefined && isResumableStatus(active.status))
|
|
2870
|
+
return active;
|
|
2871
|
+
return plan.goals.find((goal2) => isResumableStatus(goal2.status));
|
|
2872
|
+
}
|
|
2873
|
+
function isResumableStatus(status2) {
|
|
2874
|
+
return status2 === "pending" || status2 === "in_progress";
|
|
2875
|
+
}
|
|
2876
|
+
function consumeResumeBudget(stateDir, goalId) {
|
|
2877
|
+
const ledgerLineCount = countLedgerLines(join3(stateDir, "ledger.jsonl"));
|
|
2878
|
+
const counterPath = join3(stateDir, `auto-resume-${goalId}.json`);
|
|
2879
|
+
const previous = readCounter(counterPath);
|
|
2880
|
+
const count = previous !== null && previous.ledgerLineCount === ledgerLineCount ? previous.count : 0;
|
|
2881
|
+
if (count >= RESUME_CAP) {
|
|
2882
|
+
writeFileSync2(join3(stateDir, `auto-resume-${goalId}.stuck`), `no ledger progress after ${count} resumes
|
|
2883
|
+
`);
|
|
2884
|
+
return false;
|
|
2885
|
+
}
|
|
2886
|
+
writeFileSync2(counterPath, JSON.stringify({ count: count + 1, ledgerLineCount }));
|
|
2887
|
+
return true;
|
|
2888
|
+
}
|
|
2889
|
+
function renderResumeDirective(plan, goal2, sessionId) {
|
|
2890
|
+
const normalized = normalizeUlwLoopSessionId(sessionId);
|
|
2891
|
+
const option = normalized !== null && plan.goalsPath.includes(`/${normalized}/`) ? ` --session-id ${normalized}` : "";
|
|
2892
|
+
return [
|
|
2893
|
+
`The ulw-loop run in this session still has unfinished goals (next: ${goal2.id} — ${goal2.title}).`,
|
|
2894
|
+
"The turn ended before the loop completed. Resume it now:",
|
|
2895
|
+
`1. Run \`omo ulw-loop status${option} --json\` to reload the plan, the active goal, and currentAttemptDir.`,
|
|
2896
|
+
"2. Continue the active goal's remaining success criteria, recording evidence with record-evidence.",
|
|
2897
|
+
`3. Checkpoint through \`omo ulw-loop checkpoint${option}\` when the goal's criteria are proven.`,
|
|
2898
|
+
"If the loop is genuinely blocked on the user, checkpoint the goal as blocked with the reason instead."
|
|
2899
|
+
].join(`
|
|
2900
|
+
`);
|
|
2901
|
+
}
|
|
2902
|
+
function readPlan2(goalsPath) {
|
|
2903
|
+
try {
|
|
2904
|
+
return JSON.parse(readFileSync4(goalsPath, "utf8"));
|
|
2905
|
+
} catch (error) {
|
|
2906
|
+
if (error instanceof Error)
|
|
2907
|
+
return null;
|
|
2908
|
+
throw error;
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
function countLedgerLines(ledgerPath) {
|
|
2912
|
+
try {
|
|
2913
|
+
return readFileSync4(ledgerPath, "utf8").split(`
|
|
2914
|
+
`).filter(Boolean).length;
|
|
2915
|
+
} catch (error) {
|
|
2916
|
+
if (error instanceof Error)
|
|
2917
|
+
return 0;
|
|
2918
|
+
throw error;
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
function readCounter(counterPath) {
|
|
2922
|
+
try {
|
|
2923
|
+
if (!existsSync7(counterPath))
|
|
2924
|
+
return null;
|
|
2925
|
+
const parsed = JSON.parse(readFileSync4(counterPath, "utf8"));
|
|
2926
|
+
if (typeof parsed["count"] !== "number" || typeof parsed["ledgerLineCount"] !== "number")
|
|
2927
|
+
return null;
|
|
2928
|
+
return { count: parsed["count"], ledgerLineCount: parsed["ledgerLineCount"] };
|
|
2929
|
+
} catch (error) {
|
|
2930
|
+
if (error instanceof Error)
|
|
2931
|
+
return null;
|
|
2932
|
+
throw error;
|
|
2933
|
+
}
|
|
2934
|
+
}
|
|
2935
|
+
function boulderContinuationWillFire(cwd, sessionId) {
|
|
2936
|
+
try {
|
|
2937
|
+
const raw = JSON.parse(readFileSync4(join3(cwd, ".omo", "boulder.json"), "utf8"));
|
|
2938
|
+
const works = raw["works"];
|
|
2939
|
+
const entries = typeof works === "object" && works !== null ? Object.values(works) : [raw];
|
|
2940
|
+
return entries.some((work) => {
|
|
2941
|
+
if (typeof work !== "object" || work === null)
|
|
2942
|
+
return false;
|
|
2943
|
+
const entry = work;
|
|
2944
|
+
const sessionIds = Array.isArray(entry["session_ids"]) ? entry["session_ids"] : [];
|
|
2945
|
+
const continuable = entry["status"] === "active" || entry["status"] === "paused";
|
|
2946
|
+
return continuable && sessionIds.includes(`codex:${sessionId}`) && boulderPlanHasRemainingTask(cwd, entry);
|
|
2947
|
+
});
|
|
2948
|
+
} catch (error) {
|
|
2949
|
+
if (error instanceof Error)
|
|
2950
|
+
return false;
|
|
2951
|
+
throw error;
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
function transcriptShowsContextPressure(transcriptPath) {
|
|
2955
|
+
try {
|
|
2956
|
+
const transcript = readFileSync4(transcriptPath, "utf8").toLowerCase();
|
|
2957
|
+
return CONTEXT_PRESSURE_MARKERS2.some((marker) => transcript.includes(marker));
|
|
2958
|
+
} catch (error) {
|
|
2959
|
+
if (error instanceof Error)
|
|
2960
|
+
return false;
|
|
2961
|
+
throw error;
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
function boulderPlanHasRemainingTask(cwd, entry) {
|
|
2965
|
+
const activePlan = entry["active_plan"];
|
|
2966
|
+
if (typeof activePlan !== "string" || activePlan.trim().length === 0)
|
|
2967
|
+
return false;
|
|
2968
|
+
const planPath = isAbsolute2(activePlan) ? activePlan : join3(cwd, activePlan);
|
|
2969
|
+
const worktree = entry["worktree_path"];
|
|
2970
|
+
const candidates = typeof worktree === "string" && worktree.trim().length > 0 && !isAbsolute2(activePlan) ? [join3(isAbsolute2(worktree) ? worktree : join3(cwd, worktree), activePlan), planPath] : [planPath];
|
|
2971
|
+
for (const candidate of candidates) {
|
|
2972
|
+
try {
|
|
2973
|
+
return readFileSync4(candidate, "utf8").split(/\r?\n/).some((line) => line.startsWith("- [ ] "));
|
|
2974
|
+
} catch (error) {
|
|
2975
|
+
if (!(error instanceof Error))
|
|
2976
|
+
throw error;
|
|
2977
|
+
}
|
|
2978
|
+
}
|
|
2979
|
+
return false;
|
|
2980
|
+
}
|
|
2981
|
+
function parseStopPayload(value) {
|
|
2982
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
2983
|
+
return null;
|
|
2984
|
+
const record = value;
|
|
2985
|
+
const optionalMessage = record["last_assistant_message"];
|
|
2986
|
+
const valid = record["hook_event_name"] === "Stop" && typeof record["session_id"] === "string" && typeof record["turn_id"] === "string" && typeof record["transcript_path"] === "string" && typeof record["cwd"] === "string" && typeof record["model"] === "string" && typeof record["permission_mode"] === "string" && typeof record["stop_hook_active"] === "boolean" && (optionalMessage === undefined || typeof optionalMessage === "string");
|
|
2987
|
+
if (!valid)
|
|
2988
|
+
return null;
|
|
2989
|
+
return {
|
|
2990
|
+
session_id: record["session_id"],
|
|
2991
|
+
cwd: record["cwd"],
|
|
2992
|
+
transcript_path: record["transcript_path"],
|
|
2993
|
+
stop_hook_active: record["stop_hook_active"]
|
|
2994
|
+
};
|
|
2995
|
+
}
|
|
2996
|
+
|
|
2625
2997
|
// components/ulw-loop/src/cli.ts
|
|
2626
2998
|
var TOP_LEVEL_HELP = `Usage:
|
|
2627
2999
|
omo ulw-loop <subcommand> [args]
|
|
@@ -2651,6 +3023,14 @@ async function main() {
|
|
|
2651
3023
|
await runPreToolUseGoalBudgetGuardCli(process.stdin, process.stdout);
|
|
2652
3024
|
return 0;
|
|
2653
3025
|
}
|
|
3026
|
+
if (sub === "stop") {
|
|
3027
|
+
await runStopResumeHookCli(process.stdin, process.stdout);
|
|
3028
|
+
return 0;
|
|
3029
|
+
}
|
|
3030
|
+
if (sub === "pre-tool-use-spawn") {
|
|
3031
|
+
await runSpawnGuardCli(process.stdin, process.stdout);
|
|
3032
|
+
return 0;
|
|
3033
|
+
}
|
|
2654
3034
|
process.stderr.write(`[omo] unknown hook subcommand: ${sub ?? "(none)"}
|
|
2655
3035
|
`);
|
|
2656
3036
|
return 1;
|
|
@@ -24,6 +24,7 @@ function buildText(mode, plan, goal, createGoal, isFinal) {
|
|
|
24
24
|
"- Use the create_goal payload exactly as rendered: objective only.",
|
|
25
25
|
"- Goals are unlimited. Do not add numeric limits.",
|
|
26
26
|
...modeConstraintLines(mode, isFinal),
|
|
27
|
+
...evidenceLayoutLines(plan),
|
|
27
28
|
finalSection(plan, goal, isFinal, mode === "aggregate"),
|
|
28
29
|
...checkpointLines(plan, mode),
|
|
29
30
|
"",
|
|
@@ -71,6 +72,13 @@ function formatCriterionLine(criterion) {
|
|
|
71
72
|
const marker = isEssentialCriterion(criterion) ? "essential" : "non-essential";
|
|
72
73
|
return `-${remainingWork} [${criterion.id}] [${marker}] (${criterion.userModel}) ${criterion.scenario} — expect: ${criterion.expectedEvidence} — status: ${criterion.status}`;
|
|
73
74
|
}
|
|
75
|
+
function evidenceLayoutLines(plan) {
|
|
76
|
+
if (plan.evidenceLayoutVersion !== 2)
|
|
77
|
+
return [];
|
|
78
|
+
return [
|
|
79
|
+
"- Evidence layout v2: write every artifact for the active goal (QA matrix, review reports, receipts) under the current attempt directory — read currentAttemptDir from `omo ulw-loop status --json` (.omo/evidence/ulw/<session>/<goalId>/a<attempt>). The final checkpoint rejects quality-gate artifacts outside that directory.",
|
|
80
|
+
];
|
|
81
|
+
}
|
|
74
82
|
function finalSection(plan, goal, isFinal, aggregate) {
|
|
75
83
|
if (!isFinal)
|
|
76
84
|
return "- This is not the final ulw-loop story; do not run the final reviewer/manual-QA/gate-review quality gate yet.";
|
|
@@ -81,8 +89,11 @@ function finalSection(plan, goal, isFinal, aggregate) {
|
|
|
81
89
|
"Final story — run mandatory quality gate before update_goal:",
|
|
82
90
|
"- Run targeted verification for changed behavior.",
|
|
83
91
|
"- Confirm every manualQa artifact path exists and has non-zero size.",
|
|
84
|
-
"-
|
|
92
|
+
"- First spawn lazycodex-code-reviewer and lazycodex-qa-executor 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).",
|
|
93
|
+
"- Only then spawn lazycodex-gate-reviewer (same fork settings), passing those artifact paths.",
|
|
85
94
|
"- Require clean codeReview, manualQa, gateReview, iteration, and criteriaCoverage. criteriaCoverage must summarize originalIntent, desiredOutcome, and userOutcomeReview; counts alone are not approval.",
|
|
95
|
+
"- 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.",
|
|
96
|
+
"- If codeQualityStatus is WATCH, include the WATCH notes verbatim in your final user-facing message.",
|
|
86
97
|
"- If any reviewer is blocked/inconclusive or the quality gate is not clean, do not call update_goal. Record blocker work first:",
|
|
87
98
|
` ${blockerCommand}`,
|
|
88
99
|
aggregate
|