oh-my-opencode 5.0.0-beta.31 → 5.0.0-beta.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +136 -44
- package/dist/cli-node/index.js +136 -44
- package/dist/features/background-agent/parent-wake-notifier-types.d.ts +1 -1
- package/dist/features/background-agent/parent-wake-prompt-dispatch.d.ts +1 -1
- package/dist/features/background-agent/parent-wake-session-inspector.d.ts +1 -1
- package/dist/features/monitor/manager-internals.d.ts +1 -1
- package/dist/features/monitor/output-injector-types.d.ts +1 -1
- package/dist/features/team-mode/tools/messaging-live-delivery-recipient.d.ts +1 -0
- package/dist/features/team-mode/tools/messaging-runtime.d.ts +1 -0
- package/dist/hooks/anthropic-context-window-limit-recovery/tool-result-storage.d.ts +11 -0
- package/dist/index.js +286 -99
- package/dist/skills/ultimate-browsing/references/insane-search/README.md +9 -2
- package/dist/tools/session-manager/directory-filter.d.ts +2 -0
- package/dist/tui.js +109 -23
- package/package.json +16 -15
- package/packages/lsp-core/src/lsp/client-diagnostics-freshness.integration.test.ts +47 -19
- package/packages/lsp-core/src/lsp/client-diagnostics-pull-timeout.test.ts +64 -0
- package/packages/lsp-core/src/lsp/client.ts +6 -3
- package/packages/lsp-core/src/lsp/controlled-clock-test-support.ts +61 -0
- package/packages/lsp-core/src/lsp/fixtures/workspace-edit-server.mjs +10 -3
- package/packages/lsp-core/src/lsp/timer-provider.ts +13 -0
- package/packages/lsp-core/src/lsp/transport-request-timeout.test.ts +73 -0
- package/packages/lsp-core/src/lsp/transport.ts +7 -2
- package/packages/lsp-core/src/lsp/workspace-document-state.test.ts +84 -1
- package/packages/lsp-core/src/lsp/workspace-document-state.ts +30 -11
- package/packages/lsp-core/src/tools/parameters.test.ts +51 -0
- package/packages/lsp-daemon/dist/cli.js +41 -16
- package/packages/lsp-daemon/dist/client.js +41 -16
- package/packages/lsp-daemon/dist/index.js +41 -16
- package/packages/lsp-tools-mcp/dist/cli.js +41 -16
- package/packages/lsp-tools-mcp/dist/lsp/manager.js +41 -16
- package/packages/lsp-tools-mcp/dist/mcp.js +41 -16
- package/packages/lsp-tools-mcp/dist/tools.js +41 -16
- package/packages/lsp-tools-mcp/package.json +1 -1
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
- package/packages/omo-codex/plugin/components/codegraph/dist/cli.js +3 -17
- package/packages/omo-codex/plugin/components/codegraph/dist/serve.js +3 -17
- package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
- package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
- package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +3 -3
- package/packages/omo-codex/plugin/components/lsp/dist/cli.js +39 -16
- package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
- package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/rules/package.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/AGENTS.md +5 -4
- package/packages/omo-codex/plugin/components/ultrawork/dist/cli.js +489 -4
- package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -2
- package/packages/omo-codex/plugin/components/ultrawork/scripts/sync-directive.mjs +30 -14
- package/packages/omo-codex/plugin/components/ultrawork/src/directive-content.ts +4 -0
- package/packages/omo-codex/plugin/components/ultrawork/src/directive.ts +5 -2
- package/packages/omo-codex/plugin/components/ultrawork/test/directive-source.test.ts +14 -8
- package/packages/omo-codex/plugin/components/ultrawork/test/package-smoke.test.ts +12 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/AGENTS.md +4 -2
- package/packages/omo-codex/plugin/components/ulw-loop/README.md +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-continuation.js +9 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-reconciliation.d.ts +14 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-reconciliation.js +10 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-template.d.ts +8 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-template.js +113 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.js +7 -5
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-commands.js +2 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-output.d.ts +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-output.js +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-subcommands.js +2 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +775 -312
- package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-instruction.js +18 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-snapshot.d.ts +13 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-snapshot.js +14 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/domain-types.d.ts +17 -9
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-io.js +17 -2
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-missing-recovery.d.ts +13 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-missing-recovery.js +19 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-aggregate.d.ts +2 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-aggregate.js +53 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-artifacts.d.ts +9 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-artifacts.js +85 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-fields.js +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate.js +41 -99
- package/packages/omo-codex/plugin/components/ulw-loop/dist/review-blockers.js +4 -2
- package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-guard.js +184 -14
- package/packages/omo-codex/plugin/components/ulw-loop/dist/status-next-actions.d.ts +6 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/status-next-actions.js +33 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/surface.d.ts +3 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/surface.js +15 -0
- package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +17 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-continuation.ts +7 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-reconciliation.ts +25 -1
- package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-template.ts +127 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +8 -12
- package/packages/omo-codex/plugin/components/ulw-loop/src/cli-commands.ts +2 -1
- package/packages/omo-codex/plugin/components/ulw-loop/src/cli-output.ts +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/cli-subcommands.ts +2 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-instruction.ts +18 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-snapshot.ts +23 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/domain-types.ts +19 -9
- package/packages/omo-codex/plugin/components/ulw-loop/src/plan-io.ts +18 -3
- package/packages/omo-codex/plugin/components/ulw-loop/src/plan-missing-recovery.ts +27 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-aggregate.ts +57 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-artifacts.ts +103 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-fields.ts +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate.ts +50 -116
- package/packages/omo-codex/plugin/components/ulw-loop/src/review-blockers.ts +4 -2
- package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-guard.ts +186 -12
- package/packages/omo-codex/plugin/components/ulw-loop/src/status-next-actions.ts +36 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/surface.ts +22 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint-final.test.ts +66 -3
- package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint-template.test.ts +92 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/cli-status-next-actions.test.ts +183 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/codex-goal-instruction.test.ts +13 -3
- package/packages/omo-codex/plugin/components/ulw-loop/test/guided-recovery.test.ts +105 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-aggregate.test.ts +101 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-roles.test.ts +16 -15
- package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-senpi-surface.test.ts +131 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate.test.ts +5 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/spawn-guard.test.ts +300 -3
- package/packages/omo-codex/plugin/components/ulw-loop/test/ultrawork-directive.test.ts +9 -3
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-checking-ulw-execute-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/package-lock.json +13 -13
- package/packages/omo-codex/plugin/package.json +1 -1
- package/packages/omo-codex/plugin/scripts/sync-skills.mjs +31 -2
- package/packages/omo-codex/plugin/skills/ultimate-browsing/references/insane-search/README.md +9 -2
- package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +17 -0
- package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +11 -1
- package/packages/omo-codex/plugin/test/sync-skills.test.mjs +13 -0
- package/packages/omo-codex/plugin/test/ultrawork-skill-pointer.test.mjs +5 -2
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +2 -2
- package/packages/shared-skills/skills/ultimate-browsing/references/insane-search/README.md +9 -2
- package/dist/features/builtin-skills/index.d.ts +0 -1
- package/dist/features/builtin-skills/skill-file-loader.d.ts +0 -1
- package/dist/features/builtin-skills/skills/agent-browser-skill.d.ts +0 -1
- package/dist/features/builtin-skills/skills/agent-browser-template.d.ts +0 -1
- package/dist/features/builtin-skills/skills/debugging.d.ts +0 -1
- package/dist/features/builtin-skills/skills/dev-browser.d.ts +0 -1
- package/dist/features/builtin-skills/skills/frontend.d.ts +0 -1
- package/dist/features/builtin-skills/skills/git-master-sections/commit-atomic-planning.d.ts +0 -1
- package/dist/features/builtin-skills/skills/git-master-sections/commit-context-analysis.d.ts +0 -1
- package/dist/features/builtin-skills/skills/git-master-sections/commit-execution-verification.d.ts +0 -1
- package/dist/features/builtin-skills/skills/git-master-sections/commit-workflow.d.ts +0 -1
- package/dist/features/builtin-skills/skills/git-master-sections/history-search-workflow.d.ts +0 -1
- package/dist/features/builtin-skills/skills/git-master-sections/overview.d.ts +0 -1
- package/dist/features/builtin-skills/skills/git-master-sections/quick-reference.d.ts +0 -1
- package/dist/features/builtin-skills/skills/git-master-sections/rebase-workflow.d.ts +0 -1
- package/dist/features/builtin-skills/skills/git-master-skill-metadata.d.ts +0 -1
- package/dist/features/builtin-skills/skills/git-master.d.ts +0 -1
- package/dist/features/builtin-skills/skills/index.d.ts +0 -1
- package/dist/features/builtin-skills/skills/init-deep.d.ts +0 -1
- package/dist/features/builtin-skills/skills/playwright-cli.d.ts +0 -1
- package/dist/features/builtin-skills/skills/playwright-mcp-skill.d.ts +0 -1
- package/dist/features/builtin-skills/skills/playwright.d.ts +0 -1
- package/dist/features/builtin-skills/skills/remove-ai-slops.d.ts +0 -1
- package/dist/features/builtin-skills/skills/review-work.d.ts +0 -1
- package/dist/features/builtin-skills/skills/security-research.d.ts +0 -1
- package/dist/features/builtin-skills/skills/security-review.d.ts +0 -1
- package/dist/features/builtin-skills/skills/team-mode.d.ts +0 -1
- package/dist/features/builtin-skills/skills/visual-qa.d.ts +0 -1
- package/dist/features/builtin-skills/skills.d.ts +0 -1
- package/dist/features/builtin-skills/types.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/agent-loader.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/command-loader.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/discovery-core.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/discovery-paths.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/discovery.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/hook-loader.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/index.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/install-path-resolver.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/installed-plugin-database.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/loaded-plugin.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/loader.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/mcp-server-loader.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/plugin-key.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/plugin-manifest.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/plugin-path-resolver.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/plugin-settings.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/scope-filter.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/skill-loader.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/types.d.ts +0 -1
- package/dist/shared/prompt-async-gate/message-inspection-error.d.ts +0 -1
- package/dist/shared/prompt-async-gate/pending-tool-turn.d.ts +0 -1
- package/dist/shared/prompt-async-gate/prompt-message-state.d.ts +0 -1
- package/dist/shared/prompt-async-gate/queue.d.ts +0 -1
- package/dist/shared/prompt-async-gate/recent-dispatches.d.ts +0 -1
- package/dist/shared/prompt-async-gate/reservations.d.ts +0 -1
- package/dist/shared/prompt-async-gate/semantic-dedupe.d.ts +0 -1
- package/dist/shared/prompt-async-gate/session-idle-dispatch.d.ts +0 -1
- package/dist/shared/prompt-async-gate/timing.d.ts +0 -1
- package/dist/shared/prompt-async-gate/types.d.ts +0 -1
- package/packages/omo-codex/plugin/components/ultrawork/directive.md +0 -483
- package/packages/omo-codex/plugin/components/ultrawork/skills/ultrawork/SKILL.md +0 -490
|
@@ -1,17 +1,124 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
//
|
|
3
|
+
// components/ulw-loop/src/checkpoint.ts
|
|
4
4
|
import { existsSync as existsSync4, statSync } from "node:fs";
|
|
5
5
|
import { readFile as readFile5 } from "node:fs/promises";
|
|
6
|
-
import { resolve as
|
|
6
|
+
import { resolve as resolve4 } from "node:path";
|
|
7
|
+
|
|
8
|
+
// components/ulw-loop/src/checkpoint-reconciliation.ts
|
|
9
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
10
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
7
11
|
|
|
8
|
-
//
|
|
12
|
+
// components/ulw-loop/src/codex-goal-snapshot.ts
|
|
9
13
|
import { existsSync } from "node:fs";
|
|
10
14
|
import { readFile } from "node:fs/promises";
|
|
15
|
+
import { resolve } from "node:path";
|
|
16
|
+
|
|
17
|
+
class CodexGoalSnapshotError extends Error {
|
|
18
|
+
}
|
|
19
|
+
function safeObject(value) {
|
|
20
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
21
|
+
}
|
|
22
|
+
function safeString(value) {
|
|
23
|
+
return typeof value === "string" ? value.trim() : "";
|
|
24
|
+
}
|
|
25
|
+
function normalizeStatus(value) {
|
|
26
|
+
const status = safeString(value).toLowerCase();
|
|
27
|
+
if (status === "complete" || status === "completed" || status === "done")
|
|
28
|
+
return "complete";
|
|
29
|
+
if (status === "cancelled" || status === "canceled")
|
|
30
|
+
return "cancelled";
|
|
31
|
+
if (status === "failed" || status === "failure")
|
|
32
|
+
return "failed";
|
|
33
|
+
if (status === "active" || status === "in_progress" || status === "pending" || status === "running")
|
|
34
|
+
return "active";
|
|
35
|
+
return "unknown";
|
|
36
|
+
}
|
|
37
|
+
function normalizeObjective(value) {
|
|
38
|
+
return value.replace(/\s+/g, " ").trim();
|
|
39
|
+
}
|
|
40
|
+
function parseCodexGoalSnapshot(value) {
|
|
41
|
+
const root = safeObject(value);
|
|
42
|
+
const goalValue = Object.hasOwn(root, "goal") ? root["goal"] : value;
|
|
43
|
+
if (goalValue === null || goalValue === undefined || goalValue === false) {
|
|
44
|
+
return { available: false, raw: value };
|
|
45
|
+
}
|
|
46
|
+
const goal = safeObject(goalValue);
|
|
47
|
+
const objective = safeString(goal["objective"] ?? goal["goal"] ?? goal["description"] ?? goal["title"] ?? root["objective"] ?? root["title"]);
|
|
48
|
+
const status = normalizeStatus(goal["status"] ?? root["status"]);
|
|
49
|
+
return {
|
|
50
|
+
available: Boolean(objective || status !== "unknown"),
|
|
51
|
+
...objective ? { objective } : {},
|
|
52
|
+
status,
|
|
53
|
+
raw: value
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
async function readCodexGoalSnapshotInput(raw, cwd = process.cwd()) {
|
|
57
|
+
if (!raw?.trim())
|
|
58
|
+
return null;
|
|
59
|
+
const trimmed = raw.trim();
|
|
60
|
+
try {
|
|
61
|
+
return parseCodexGoalSnapshot(JSON.parse(trimmed));
|
|
62
|
+
} catch {
|
|
63
|
+
const path = resolve(cwd, trimmed);
|
|
64
|
+
if (!existsSync(path)) {
|
|
65
|
+
throw new CodexGoalSnapshotError(`Codex goal snapshot is neither valid JSON nor a readable path: ${trimmed}`);
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
return parseCodexGoalSnapshot(JSON.parse(await readFile(path, "utf-8")));
|
|
69
|
+
} catch (error) {
|
|
70
|
+
throw new CodexGoalSnapshotError(`Codex goal snapshot path does not contain valid JSON: ${trimmed}${error instanceof Error ? ` (${error.message})` : ""}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function reconcileCodexGoalSnapshot(snapshot, options) {
|
|
75
|
+
const effectiveSnapshot = snapshot ?? { available: false, raw: null };
|
|
76
|
+
const errors = [];
|
|
77
|
+
const warnings = [];
|
|
78
|
+
if (!effectiveSnapshot.available) {
|
|
79
|
+
const message = "Codex goal snapshot is absent or reports no active goal; call get_goal and pass its JSON with --codex-goal-json.";
|
|
80
|
+
if (options.requireSnapshot)
|
|
81
|
+
errors.push(message);
|
|
82
|
+
else
|
|
83
|
+
warnings.push(message);
|
|
84
|
+
return { ok: errors.length === 0, snapshot: effectiveSnapshot, warnings, errors };
|
|
85
|
+
}
|
|
86
|
+
const expected = normalizeObjective(options.expectedObjective);
|
|
87
|
+
const accepted = new Set([expected, ...(options.acceptedObjectives ?? []).map((objective) => normalizeObjective(objective))].filter(Boolean));
|
|
88
|
+
const actual = normalizeObjective(effectiveSnapshot.objective ?? "");
|
|
89
|
+
if (!actual) {
|
|
90
|
+
errors.push("Codex goal snapshot is missing objective text.");
|
|
91
|
+
} else if (!accepted.has(actual)) {
|
|
92
|
+
errors.push(`Codex goal objective mismatch: expected "${expected}", got "${actual}".`);
|
|
93
|
+
}
|
|
94
|
+
const allowed = options.allowedStatuses ?? (options.requireComplete ? ["complete"] : ["active", "complete"]);
|
|
95
|
+
const actualStatus = effectiveSnapshot.status ?? "unknown";
|
|
96
|
+
if (!allowed.includes(actualStatus)) {
|
|
97
|
+
errors.push(`Codex goal status mismatch: expected ${allowed.join(" or ")}, got ${actualStatus}.`);
|
|
98
|
+
}
|
|
99
|
+
if (options.requireComplete && actualStatus !== "complete") {
|
|
100
|
+
errors.push('Codex goal is not complete; call update_goal({status: "complete"}) only after the objective is actually complete, then pass the fresh get_goal JSON.');
|
|
101
|
+
}
|
|
102
|
+
return { ok: errors.length === 0, snapshot: effectiveSnapshot, warnings, errors };
|
|
103
|
+
}
|
|
104
|
+
function formatCodexGoalReconciliation(reconciliation) {
|
|
105
|
+
const parts = [...reconciliation.errors, ...reconciliation.warnings];
|
|
106
|
+
return parts.join(" ");
|
|
107
|
+
}
|
|
108
|
+
function codexGoalMismatchRecovery(expectedObjective, snapshot) {
|
|
109
|
+
const receivedObjective = snapshot?.objective ?? "";
|
|
110
|
+
const message = [
|
|
111
|
+
"Recovery: the Codex goal objective must equal the plan's codexObjective exactly — copy the expected value below into update_goal and re-run get_goal.",
|
|
112
|
+
`expected codexObjective: ${expectedObjective}`,
|
|
113
|
+
`received objective: ${receivedObjective || "(none)"}`
|
|
114
|
+
].join(`
|
|
115
|
+
`);
|
|
116
|
+
return { message, details: { expectedObjective, receivedObjective } };
|
|
117
|
+
}
|
|
11
118
|
|
|
12
|
-
//
|
|
119
|
+
// components/ulw-loop/src/paths.ts
|
|
13
120
|
import { isAbsolute, join, relative, sep } from "node:path";
|
|
14
|
-
//
|
|
121
|
+
// components/ulw-loop/src/constants.ts
|
|
15
122
|
var ULW_LOOP_DIR = ".omo/ulw-loop";
|
|
16
123
|
var ULW_LOOP_BRIEF = "brief.md";
|
|
17
124
|
var ULW_LOOP_GOALS = "goals.json";
|
|
@@ -31,7 +138,7 @@ var ULW_LOOP_SUCCESS_CRITERION_USER_MODELS = [
|
|
|
31
138
|
"regression",
|
|
32
139
|
"adversarial"
|
|
33
140
|
];
|
|
34
|
-
//
|
|
141
|
+
// components/ulw-loop/src/runtime.ts
|
|
35
142
|
class UlwLoopError extends Error {
|
|
36
143
|
constructor(message, code, opts) {
|
|
37
144
|
super(message, opts?.cause === undefined ? undefined : { cause: opts.cause });
|
|
@@ -45,7 +152,7 @@ class UlwLoopError extends Error {
|
|
|
45
152
|
function iso() {
|
|
46
153
|
return new Date().toISOString();
|
|
47
154
|
}
|
|
48
|
-
//
|
|
155
|
+
// components/ulw-loop/src/paths.ts
|
|
49
156
|
var SESSION_ENV_KEYS = ["OMO_ULW_LOOP_SESSION_ID", "CODEX_SESSION_ID", "CODEX_THREAD_ID", "PI_SESSION_ID"];
|
|
50
157
|
function normalizeUlwLoopSessionId(sessionId) {
|
|
51
158
|
const trimmed = sessionId?.trim();
|
|
@@ -111,7 +218,7 @@ function isWithinAttemptDir(absolutePath, attemptRoot, pathApi = PLATFORM_PATH_A
|
|
|
111
218
|
return !pathApi.isAbsolute(relativePath);
|
|
112
219
|
}
|
|
113
220
|
|
|
114
|
-
//
|
|
221
|
+
// components/ulw-loop/src/goal-status.ts
|
|
115
222
|
var ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE = aggregateCodexObjectiveForScope();
|
|
116
223
|
function aggregateCodexObjectiveForScope(scope) {
|
|
117
224
|
return `Complete the durable ulw-loop plan in ${ulwLoopGoalsRelativePath(scope)}, including later accepted/appended stories, under the original brief constraints; use ${ulwLoopLedgerRelativePath(scope)} as the audit trail.`;
|
|
@@ -194,8 +301,15 @@ function hasEssentialCriteriaPass(goal) {
|
|
|
194
301
|
return criteria.length > 0 && criteria.every((criterion) => criterion.status === "pass");
|
|
195
302
|
}
|
|
196
303
|
|
|
197
|
-
//
|
|
198
|
-
function
|
|
304
|
+
// components/ulw-loop/src/checkpoint-reconciliation.ts
|
|
305
|
+
function codexSnapshotMismatchError(input) {
|
|
306
|
+
const { reconciliation, snapshot, taskScopedHint } = input;
|
|
307
|
+
const hint = taskScopedHint?.aggregate === true && snapshot?.status === "complete" && snapshot.objective !== undefined ? buildTaskScopedAggregateReconciliationHint(taskScopedHint.goal, taskScopedHint.final) : "";
|
|
308
|
+
const recovery = codexGoalMismatchRecovery(input.expectedObjective, snapshot);
|
|
309
|
+
return new UlwLoopError(`${formatCodexGoalReconciliation(reconciliation)}${hint}
|
|
310
|
+
${recovery.message}`, "ulw_loop_codex_snapshot_mismatch", { details: recovery.details });
|
|
311
|
+
}
|
|
312
|
+
function normalizeObjective2(value) {
|
|
199
313
|
return value.replace(/\s+/g, " ").trim();
|
|
200
314
|
}
|
|
201
315
|
function textMentionsUlwLoopPlanArtifact(value) {
|
|
@@ -212,13 +326,13 @@ function textHasCompletionValidationEvidence(value) {
|
|
|
212
326
|
return done && verified;
|
|
213
327
|
}
|
|
214
328
|
async function snapshotObjectiveMapsToUlwLoopPlan(repoRoot, snapshotObjective, scope) {
|
|
215
|
-
const actual =
|
|
329
|
+
const actual = normalizeObjective2(snapshotObjective).toLowerCase();
|
|
216
330
|
if (textMentionsUlwLoopPlanArtifact(actual))
|
|
217
331
|
return true;
|
|
218
|
-
if (actual.length < 24 || !
|
|
332
|
+
if (actual.length < 24 || !existsSync2(ulwLoopBriefPath(repoRoot, scope)))
|
|
219
333
|
return false;
|
|
220
334
|
try {
|
|
221
|
-
const brief =
|
|
335
|
+
const brief = normalizeObjective2(await readFile2(ulwLoopBriefPath(repoRoot, scope), "utf8")).toLowerCase();
|
|
222
336
|
return brief.length >= 24 && (brief.includes(actual) || actual.includes(brief));
|
|
223
337
|
} catch (error) {
|
|
224
338
|
if (error instanceof Error)
|
|
@@ -265,107 +379,32 @@ function buildTaskScopedAggregateReconciliationHint(goal, final) {
|
|
|
265
379
|
return ` Completed task-scoped aggregate reconciliation requires the checkpoint goal to be the active in-progress OMO goal, evidence that names that active OMO goal id, names .omo/ulw-loop/goals.json or ledger.jsonl, includes completed implementation plus validation/review evidence, and a get_goal objective that maps to the ulw-loop brief/artifact. ${buildCompletedLegacyGoalRemediation(goal)}`;
|
|
266
380
|
}
|
|
267
381
|
|
|
268
|
-
//
|
|
269
|
-
import {
|
|
270
|
-
import { readFile as
|
|
271
|
-
import {
|
|
382
|
+
// components/ulw-loop/src/plan-io.ts
|
|
383
|
+
import { createReadStream, readdirSync } from "node:fs";
|
|
384
|
+
import { appendFile, mkdir, readFile as readFile3, rename, writeFile } from "node:fs/promises";
|
|
385
|
+
import { createInterface } from "node:readline";
|
|
272
386
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
function
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
return "unknown";
|
|
292
|
-
}
|
|
293
|
-
function normalizeObjective2(value) {
|
|
294
|
-
return value.replace(/\s+/g, " ").trim();
|
|
295
|
-
}
|
|
296
|
-
function parseCodexGoalSnapshot(value) {
|
|
297
|
-
const root = safeObject(value);
|
|
298
|
-
const goalValue = Object.hasOwn(root, "goal") ? root["goal"] : value;
|
|
299
|
-
if (goalValue === null || goalValue === undefined || goalValue === false) {
|
|
300
|
-
return { available: false, raw: value };
|
|
301
|
-
}
|
|
302
|
-
const goal = safeObject(goalValue);
|
|
303
|
-
const objective = safeString(goal["objective"] ?? goal["goal"] ?? goal["description"] ?? goal["title"] ?? root["objective"] ?? root["title"]);
|
|
304
|
-
const status = normalizeStatus(goal["status"] ?? root["status"]);
|
|
305
|
-
return {
|
|
306
|
-
available: Boolean(objective || status !== "unknown"),
|
|
307
|
-
...objective ? { objective } : {},
|
|
308
|
-
status,
|
|
309
|
-
raw: value
|
|
310
|
-
};
|
|
311
|
-
}
|
|
312
|
-
async function readCodexGoalSnapshotInput(raw, cwd = process.cwd()) {
|
|
313
|
-
if (!raw?.trim())
|
|
314
|
-
return null;
|
|
315
|
-
const trimmed = raw.trim();
|
|
316
|
-
try {
|
|
317
|
-
return parseCodexGoalSnapshot(JSON.parse(trimmed));
|
|
318
|
-
} catch {
|
|
319
|
-
const path = resolve(cwd, trimmed);
|
|
320
|
-
if (!existsSync2(path)) {
|
|
321
|
-
throw new CodexGoalSnapshotError(`Codex goal snapshot is neither valid JSON nor a readable path: ${trimmed}`);
|
|
322
|
-
}
|
|
323
|
-
try {
|
|
324
|
-
return parseCodexGoalSnapshot(JSON.parse(await readFile2(path, "utf-8")));
|
|
325
|
-
} catch (error) {
|
|
326
|
-
throw new CodexGoalSnapshotError(`Codex goal snapshot path does not contain valid JSON: ${trimmed}${error instanceof Error ? ` (${error.message})` : ""}`);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
function reconcileCodexGoalSnapshot(snapshot, options) {
|
|
331
|
-
const effectiveSnapshot = snapshot ?? { available: false, raw: null };
|
|
332
|
-
const errors = [];
|
|
333
|
-
const warnings = [];
|
|
334
|
-
if (!effectiveSnapshot.available) {
|
|
335
|
-
const message = "Codex goal snapshot is absent or reports no active goal; call get_goal and pass its JSON with --codex-goal-json.";
|
|
336
|
-
if (options.requireSnapshot)
|
|
337
|
-
errors.push(message);
|
|
338
|
-
else
|
|
339
|
-
warnings.push(message);
|
|
340
|
-
return { ok: errors.length === 0, snapshot: effectiveSnapshot, warnings, errors };
|
|
341
|
-
}
|
|
342
|
-
const expected = normalizeObjective2(options.expectedObjective);
|
|
343
|
-
const accepted = new Set([expected, ...(options.acceptedObjectives ?? []).map((objective) => normalizeObjective2(objective))].filter(Boolean));
|
|
344
|
-
const actual = normalizeObjective2(effectiveSnapshot.objective ?? "");
|
|
345
|
-
if (!actual) {
|
|
346
|
-
errors.push("Codex goal snapshot is missing objective text.");
|
|
347
|
-
} else if (!accepted.has(actual)) {
|
|
348
|
-
errors.push(`Codex goal objective mismatch: expected "${expected}", got "${actual}".`);
|
|
349
|
-
}
|
|
350
|
-
const allowed = options.allowedStatuses ?? (options.requireComplete ? ["complete"] : ["active", "complete"]);
|
|
351
|
-
const actualStatus = effectiveSnapshot.status ?? "unknown";
|
|
352
|
-
if (!allowed.includes(actualStatus)) {
|
|
353
|
-
errors.push(`Codex goal status mismatch: expected ${allowed.join(" or ")}, got ${actualStatus}.`);
|
|
354
|
-
}
|
|
355
|
-
if (options.requireComplete && actualStatus !== "complete") {
|
|
356
|
-
errors.push('Codex goal is not complete; call update_goal({status: "complete"}) only after the objective is actually complete, then pass the fresh get_goal JSON.');
|
|
357
|
-
}
|
|
358
|
-
return { ok: errors.length === 0, snapshot: effectiveSnapshot, warnings, errors };
|
|
359
|
-
}
|
|
360
|
-
function formatCodexGoalReconciliation(reconciliation) {
|
|
361
|
-
const parts = [...reconciliation.errors, ...reconciliation.warnings];
|
|
362
|
-
return parts.join(" ");
|
|
387
|
+
// components/ulw-loop/src/plan-missing-recovery.ts
|
|
388
|
+
var ULW_LOOP_CREATE_GOALS_COMMAND = 'omo-agent-toolkit ulw-loop create-goals --brief "<brief>" --json';
|
|
389
|
+
function planMissingRecovery(existingSessionIds) {
|
|
390
|
+
const lines = [`Recovery: bootstrap the plan with \`${ULW_LOOP_CREATE_GOALS_COMMAND}\`.`];
|
|
391
|
+
if (existingSessionIds.length === 0)
|
|
392
|
+
return { message: lines.join(`
|
|
393
|
+
`) };
|
|
394
|
+
lines.push(`Existing ulw-loop session ids under .omo/ulw-loop/: ${existingSessionIds.join(", ")}. Re-run with \`--session-id <id>\` to target one of them.`);
|
|
395
|
+
return { message: lines.join(`
|
|
396
|
+
`), details: { existingSessionIds } };
|
|
397
|
+
}
|
|
398
|
+
function sessionIdRequiredMessage(flag) {
|
|
399
|
+
return [
|
|
400
|
+
`${flag} requires a non-empty value.`,
|
|
401
|
+
"Recovery: subprocess, eval, and hook contexts do not inherit the ulw-loop session env (OMO_ULW_LOOP_SESSION_ID / CODEX_SESSION_ID / CODEX_THREAD_ID / PI_SESSION_ID),",
|
|
402
|
+
`so pass the scope explicitly: \`${flag} <id>\` (for example \`${flag} 01a05b8a-6763-7780-834f-319423b071ce\`).`
|
|
403
|
+
].join(`
|
|
404
|
+
`);
|
|
363
405
|
}
|
|
364
406
|
|
|
365
|
-
//
|
|
366
|
-
import { createReadStream } from "node:fs";
|
|
367
|
-
import { appendFile, mkdir, readFile as readFile3, rename, writeFile } from "node:fs/promises";
|
|
368
|
-
import { createInterface } from "node:readline";
|
|
407
|
+
// components/ulw-loop/src/plan-io.ts
|
|
369
408
|
var LEGACY_OBJECTIVE_PREFIX = `Complete all ulw-loop stories in ${ULW_LOOP_DIR}/${ULW_LOOP_GOALS}: `;
|
|
370
409
|
var LEGACY_OBJECTIVE = `Complete all ulw-loop stories listed in ${ULW_LOOP_DIR}/${ULW_LOOP_GOALS}. Use ${ULW_LOOP_DIR}/${ULW_LOOP_LEDGER} as the durable audit trail.`;
|
|
371
410
|
var locks = new Map;
|
|
@@ -406,7 +445,9 @@ async function readUlwLoopPlan(repoRoot, scope) {
|
|
|
406
445
|
} catch (error) {
|
|
407
446
|
if (!hasCode(error, "ENOENT"))
|
|
408
447
|
throw error;
|
|
409
|
-
|
|
448
|
+
const recovery = planMissingRecovery(readSessionDirs(repoRoot));
|
|
449
|
+
throw new UlwLoopError(`No ulw-loop plan found at ${repoRelative(path, repoRoot)}.
|
|
450
|
+
${recovery.message}`, "ULW_LOOP_PLAN_MISSING", { cause: error, ...recovery.details === undefined ? {} : { details: recovery.details } });
|
|
410
451
|
}
|
|
411
452
|
const parsed = JSON.parse(raw);
|
|
412
453
|
if (parsed.version !== 1 || !Array.isArray(parsed.goals)) {
|
|
@@ -429,6 +470,13 @@ async function readUlwLoopPlan(repoRoot, scope) {
|
|
|
429
470
|
}
|
|
430
471
|
return parsed;
|
|
431
472
|
}
|
|
473
|
+
function readSessionDirs(repoRoot) {
|
|
474
|
+
try {
|
|
475
|
+
return readdirSync(ulwLoopDir(repoRoot), { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
|
|
476
|
+
} catch {
|
|
477
|
+
return [];
|
|
478
|
+
}
|
|
479
|
+
}
|
|
432
480
|
async function writePlan(repoRoot, plan, scope) {
|
|
433
481
|
await mkdir(ulwLoopDir(repoRoot, scope), { recursive: true });
|
|
434
482
|
const path = ulwLoopGoalsPath(repoRoot, scope);
|
|
@@ -480,7 +528,7 @@ async function findAcceptedSteeringLedgerEntry(repoRoot, key, scope) {
|
|
|
480
528
|
return;
|
|
481
529
|
}
|
|
482
530
|
|
|
483
|
-
//
|
|
531
|
+
// components/ulw-loop/src/evidence.ts
|
|
484
532
|
function ulwLoopFail(message, code, details) {
|
|
485
533
|
throw new UlwLoopError(message, code, { details });
|
|
486
534
|
}
|
|
@@ -586,11 +634,11 @@ function requireEssentialCriteriaPass(goal) {
|
|
|
586
634
|
});
|
|
587
635
|
}
|
|
588
636
|
|
|
589
|
-
//
|
|
637
|
+
// components/ulw-loop/src/quality-gate-aggregate.ts
|
|
590
638
|
import { resolve as resolve2 } from "node:path";
|
|
591
639
|
|
|
592
|
-
//
|
|
593
|
-
var PLACEHOLDER_PATTERN = /^(
|
|
640
|
+
// components/ulw-loop/src/quality-gate-fields.ts
|
|
641
|
+
var PLACEHOLDER_PATTERN = /^(?:<replace:[^>]+>|placeholder|todo|tbd|n\/a|stub)$/i;
|
|
594
642
|
function invalid(message, field) {
|
|
595
643
|
throw new UlwLoopError(message, "ULW_LOOP_QUALITY_GATE_INVALID", { details: { field } });
|
|
596
644
|
}
|
|
@@ -627,7 +675,135 @@ function literal(value, expected, field) {
|
|
|
627
675
|
invalid(`${field} must be ${String(expected)}.`, field);
|
|
628
676
|
}
|
|
629
677
|
|
|
630
|
-
//
|
|
678
|
+
// components/ulw-loop/src/quality-gate-aggregate.ts
|
|
679
|
+
var PLACEHOLDER = /^(?:<replace:[^>]+>|placeholder|todo|tbd|n\/a|stub)$/i;
|
|
680
|
+
function add(defects, field, message) {
|
|
681
|
+
defects.push({ field, message });
|
|
682
|
+
}
|
|
683
|
+
function text(value, field, defects) {
|
|
684
|
+
if (typeof value !== "string" || value.trim() === "")
|
|
685
|
+
add(defects, field, `Final quality gate requires non-empty ${field}.`);
|
|
686
|
+
else if (PLACEHOLDER.test(value.trim()))
|
|
687
|
+
add(defects, field, `Final quality gate rejects placeholder ${field}.`);
|
|
688
|
+
}
|
|
689
|
+
function section2(value, field, defects) {
|
|
690
|
+
if (!isRecord(value)) {
|
|
691
|
+
add(defects, field, `Final quality gate is missing ${field} evidence.`);
|
|
692
|
+
return {};
|
|
693
|
+
}
|
|
694
|
+
return value;
|
|
695
|
+
}
|
|
696
|
+
function aggregateQualityGateDefects(input, opts) {
|
|
697
|
+
if (!isRecord(input))
|
|
698
|
+
return;
|
|
699
|
+
const defects = [];
|
|
700
|
+
const gate = input;
|
|
701
|
+
const manual = section2(gate["manualQa"], "manualQa", defects);
|
|
702
|
+
const review = section2(gate["gateReview"], "gateReview", defects);
|
|
703
|
+
text(manual["evidence"], "manualQa.evidence", defects);
|
|
704
|
+
text(review["evidence"], "gateReview.evidence", defects);
|
|
705
|
+
if (review["recommendation"] !== "APPROVE")
|
|
706
|
+
add(defects, "gateReview.recommendation", "gateReview.recommendation must be APPROVE.");
|
|
707
|
+
const artifacts = Array.isArray(manual["artifactRefs"]) ? manual["artifactRefs"] : [];
|
|
708
|
+
for (const [index, item] of artifacts.entries()) {
|
|
709
|
+
if (!isRecord(item))
|
|
710
|
+
continue;
|
|
711
|
+
const path = item["path"];
|
|
712
|
+
if (typeof path !== "string" || path.trim() === "")
|
|
713
|
+
continue;
|
|
714
|
+
if (opts?.repoRoot !== undefined && opts.fs !== undefined && !opts.fs.existsSync(resolve2(opts.repoRoot, path)))
|
|
715
|
+
add(defects, `manualQa.artifactRefs[${index}].path`, `manualQa.artifactRefs[${index}].path must point to an existing artifact.`);
|
|
716
|
+
}
|
|
717
|
+
if (defects.length === 0)
|
|
718
|
+
return;
|
|
719
|
+
const truncated = defects.length > 25;
|
|
720
|
+
const fields = defects.slice(0, 25).map(({ field, message: message2 }) => ({ field, message: message2 }));
|
|
721
|
+
const message = [
|
|
722
|
+
`Final quality gate has ${fields.length}${truncated ? "+" : ""} validation defects:`,
|
|
723
|
+
...fields.map((item) => `- ${item.field}: ${item.message}`)
|
|
724
|
+
].join(`
|
|
725
|
+
`);
|
|
726
|
+
throw new UlwLoopError(message, "ULW_LOOP_QUALITY_GATE_INVALID", {
|
|
727
|
+
details: { field: fields[0]?.field, fields, ...truncated ? { truncated: true } : {} }
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// components/ulw-loop/src/quality-gate-artifacts.ts
|
|
732
|
+
import { resolve as resolve3 } from "node:path";
|
|
733
|
+
function surfaceField(value, field) {
|
|
734
|
+
if (value === "cli" || value === "http" || value === "tmux" || value === "browser" || value === "gui" || value === "data")
|
|
735
|
+
return value;
|
|
736
|
+
invalid(`${field} must be a supported manual QA surface.`, field);
|
|
737
|
+
}
|
|
738
|
+
function kindField(value, field) {
|
|
739
|
+
if (value === "cli-transcript" || value === "log" || value === "screenshot" || value === "image" || value === "http-dump" || value === "data-diff")
|
|
740
|
+
return value;
|
|
741
|
+
invalid(`${field} must be a supported artifact kind.`, field);
|
|
742
|
+
}
|
|
743
|
+
function artifactCompatible(surface, kind) {
|
|
744
|
+
switch (surface) {
|
|
745
|
+
case "cli":
|
|
746
|
+
case "tmux":
|
|
747
|
+
return kind === "cli-transcript" || kind === "log";
|
|
748
|
+
case "http":
|
|
749
|
+
return kind === "http-dump";
|
|
750
|
+
case "browser":
|
|
751
|
+
case "gui":
|
|
752
|
+
return kind === "screenshot" || kind === "image";
|
|
753
|
+
case "data":
|
|
754
|
+
return kind === "data-diff";
|
|
755
|
+
default:
|
|
756
|
+
invalid("manualQa.surfaceEvidence has an unsupported surface.", "manualQa.surfaceEvidence.surface");
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
function checkFile(path, field, opts) {
|
|
760
|
+
if (opts?.repoRoot === undefined || opts.fs === undefined)
|
|
761
|
+
return;
|
|
762
|
+
const absolute = resolve3(opts.repoRoot, path);
|
|
763
|
+
if (!opts.fs.existsSync(absolute))
|
|
764
|
+
invalid(`${field} must point to an existing artifact.`, field);
|
|
765
|
+
if (opts.fs.statSync(absolute).size <= 0)
|
|
766
|
+
invalid(`${field} must point to a non-empty artifact.`, field);
|
|
767
|
+
if (opts.currentAttemptDir !== undefined && opts.repoRoot !== undefined) {
|
|
768
|
+
const attemptRoot = resolve3(opts.repoRoot, opts.currentAttemptDir);
|
|
769
|
+
if (!isWithinAttemptDir(absolute, attemptRoot))
|
|
770
|
+
invalid(`${field} (${path}) must point to an artifact from the current attempt (${opts.currentAttemptDir}).`, field);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
function artifactMap(refs) {
|
|
774
|
+
const byId = new Map;
|
|
775
|
+
for (const ref of refs) {
|
|
776
|
+
if (byId.has(ref.id))
|
|
777
|
+
invalid(`manualQa.artifactRefs contains duplicate ${ref.id}.`, "manualQa.artifactRefs");
|
|
778
|
+
byId.set(ref.id, ref);
|
|
779
|
+
}
|
|
780
|
+
return byId;
|
|
781
|
+
}
|
|
782
|
+
function parseArtifactRefs(value, opts) {
|
|
783
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
784
|
+
invalid("manualQa.artifactRefs must not be empty.", "manualQa.artifactRefs");
|
|
785
|
+
return value.map((item, index) => {
|
|
786
|
+
const ref = section(item, `manualQa.artifactRefs[${index}]`);
|
|
787
|
+
const path = textField(ref["path"], `manualQa.artifactRefs[${index}].path`);
|
|
788
|
+
checkFile(path, `manualQa.artifactRefs[${index}].path`, opts);
|
|
789
|
+
return {
|
|
790
|
+
id: textField(ref["id"], `manualQa.artifactRefs[${index}].id`),
|
|
791
|
+
kind: kindField(ref["kind"], `manualQa.artifactRefs[${index}].kind`),
|
|
792
|
+
description: textField(ref["description"], `manualQa.artifactRefs[${index}].description`),
|
|
793
|
+
path
|
|
794
|
+
};
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
function referencedArtifacts(value, field, byId) {
|
|
798
|
+
return stringArray(value, field).map((id) => {
|
|
799
|
+
const artifact = byId.get(id);
|
|
800
|
+
if (artifact === undefined)
|
|
801
|
+
invalid(`${field} references unknown artifact ${id}.`, field);
|
|
802
|
+
return artifact;
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// components/ulw-loop/src/quality-gate-verdicts.ts
|
|
631
807
|
function passedVerdict(value, field) {
|
|
632
808
|
if (value === "not_applicable")
|
|
633
809
|
invalid(`${field} must not be not_applicable.`, field);
|
|
@@ -648,7 +824,7 @@ function adversarialVerdict(row, field) {
|
|
|
648
824
|
invalid(`${field} must be passed or not_applicable with a reason.`, field);
|
|
649
825
|
}
|
|
650
826
|
|
|
651
|
-
//
|
|
827
|
+
// components/ulw-loop/src/surface.ts
|
|
652
828
|
import { existsSync as existsSync3, readFileSync } from "node:fs";
|
|
653
829
|
import { dirname, join as join2 } from "node:path";
|
|
654
830
|
import { fileURLToPath } from "node:url";
|
|
@@ -665,6 +841,21 @@ var REVIEWER_ROLES_BY_SURFACE = {
|
|
|
665
841
|
}
|
|
666
842
|
};
|
|
667
843
|
var GATE_REVIEWER_AGENT_NAMES = new Set(Object.values(REVIEWER_ROLES_BY_SURFACE).map((roles) => roles.gateReview));
|
|
844
|
+
var REQUIRED_GATE_SECTIONS_BY_SURFACE = {
|
|
845
|
+
lazycodex: ["codeReview", "manualQa", "gateReview", "iteration", "criteriaCoverage"],
|
|
846
|
+
"omo-senpi": ["manualQa", "gateReview", "iteration", "criteriaCoverage"]
|
|
847
|
+
};
|
|
848
|
+
var GATE_SECTION_BY_ACCEPTOR = {
|
|
849
|
+
lazycodex: {
|
|
850
|
+
codeReview: [REVIEWER_ROLES_BY_SURFACE.lazycodex.codeReview],
|
|
851
|
+
manualQa: [REVIEWER_ROLES_BY_SURFACE.lazycodex.manualQa],
|
|
852
|
+
gateReview: [REVIEWER_ROLES_BY_SURFACE.lazycodex.gateReview]
|
|
853
|
+
},
|
|
854
|
+
"omo-senpi": {
|
|
855
|
+
manualQa: ["main-session"],
|
|
856
|
+
gateReview: ["category:deep", "category:unspecified-high", "category:unspecified-low"]
|
|
857
|
+
}
|
|
858
|
+
};
|
|
668
859
|
function reviewerRolesFor(surface) {
|
|
669
860
|
return REVIEWER_ROLES_BY_SURFACE[surface];
|
|
670
861
|
}
|
|
@@ -695,7 +886,7 @@ function readMarkerSurface(markerPath) {
|
|
|
695
886
|
}
|
|
696
887
|
}
|
|
697
888
|
|
|
698
|
-
//
|
|
889
|
+
// components/ulw-loop/src/quality-gate-blockers.ts
|
|
699
890
|
var BLOCKER_FIELD_KEYS = "blocker blockerSignature blockerEvidence blockerOccurrences blockedAt".split(" ");
|
|
700
891
|
var URL_PATTERN = /https?:\/\/\S+/g;
|
|
701
892
|
var PUNCTUATION_PATTERN = /[`"'()[\]{}:,;]/g;
|
|
@@ -737,93 +928,38 @@ function clearGoalBlockerFields(goal) {
|
|
|
737
928
|
Reflect.deleteProperty(goal, key);
|
|
738
929
|
}
|
|
739
930
|
|
|
740
|
-
//
|
|
931
|
+
// components/ulw-loop/src/quality-gate.ts
|
|
741
932
|
function reviewerRoleField(value, expected, field) {
|
|
742
933
|
const actual = textField(value, field);
|
|
743
934
|
if (actual !== expected)
|
|
744
935
|
invalid(`${field} must be ${expected}.`, field);
|
|
745
936
|
return expected;
|
|
746
937
|
}
|
|
747
|
-
function
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
return value;
|
|
755
|
-
invalid(`${field} must be a supported artifact kind.`, field);
|
|
756
|
-
}
|
|
757
|
-
function artifactCompatible(surface, kind) {
|
|
758
|
-
switch (surface) {
|
|
759
|
-
case "cli":
|
|
760
|
-
case "tmux":
|
|
761
|
-
return kind === "cli-transcript" || kind === "log";
|
|
762
|
-
case "http":
|
|
763
|
-
return kind === "http-dump";
|
|
764
|
-
case "browser":
|
|
765
|
-
case "gui":
|
|
766
|
-
return kind === "screenshot" || kind === "image";
|
|
767
|
-
case "data":
|
|
768
|
-
return kind === "data-diff";
|
|
769
|
-
default:
|
|
770
|
-
invalid("manualQa.surfaceEvidence has an unsupported surface.", "manualQa.surfaceEvidence.surface");
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
function checkFile(path, field, opts) {
|
|
774
|
-
if (opts?.repoRoot === undefined || opts.fs === undefined)
|
|
775
|
-
return;
|
|
776
|
-
const absolute = resolve2(opts.repoRoot, path);
|
|
777
|
-
if (!opts.fs.existsSync(absolute))
|
|
778
|
-
invalid(`${field} must point to an existing artifact.`, field);
|
|
779
|
-
if (opts.fs.statSync(absolute).size <= 0)
|
|
780
|
-
invalid(`${field} must point to a non-empty artifact.`, field);
|
|
781
|
-
if (opts.currentAttemptDir !== undefined && opts.repoRoot !== undefined) {
|
|
782
|
-
const attemptRoot = resolve2(opts.repoRoot, opts.currentAttemptDir);
|
|
783
|
-
if (!isWithinAttemptDir(absolute, attemptRoot))
|
|
784
|
-
invalid(`${field} (${path}) must point to an artifact from the current attempt (${opts.currentAttemptDir}).`, field);
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
function artifactMap(refs) {
|
|
788
|
-
const byId = new Map;
|
|
789
|
-
for (const ref of refs) {
|
|
790
|
-
if (byId.has(ref.id))
|
|
791
|
-
invalid(`manualQa.artifactRefs contains duplicate ${ref.id}.`, "manualQa.artifactRefs");
|
|
792
|
-
byId.set(ref.id, ref);
|
|
793
|
-
}
|
|
794
|
-
return byId;
|
|
795
|
-
}
|
|
796
|
-
function parseArtifactRefs(value, opts) {
|
|
797
|
-
if (!Array.isArray(value) || value.length === 0)
|
|
798
|
-
invalid("manualQa.artifactRefs must not be empty.", "manualQa.artifactRefs");
|
|
799
|
-
return value.map((item, index) => {
|
|
800
|
-
const ref = section(item, `manualQa.artifactRefs[${index}]`);
|
|
801
|
-
const path = textField(ref["path"], `manualQa.artifactRefs[${index}].path`);
|
|
802
|
-
checkFile(path, `manualQa.artifactRefs[${index}].path`, opts);
|
|
803
|
-
return {
|
|
804
|
-
id: textField(ref["id"], `manualQa.artifactRefs[${index}].id`),
|
|
805
|
-
kind: kindField(ref["kind"], `manualQa.artifactRefs[${index}].kind`),
|
|
806
|
-
description: textField(ref["description"], `manualQa.artifactRefs[${index}].description`),
|
|
807
|
-
path
|
|
808
|
-
};
|
|
809
|
-
});
|
|
810
|
-
}
|
|
811
|
-
function referencedArtifacts(value, field, byId) {
|
|
812
|
-
return stringArray(value, field).map((id) => {
|
|
813
|
-
const artifact = byId.get(id);
|
|
814
|
-
if (artifact === undefined)
|
|
815
|
-
invalid(`${field} references unknown artifact ${id}.`, field);
|
|
816
|
-
return artifact;
|
|
817
|
-
});
|
|
938
|
+
function reviewerAcceptorField(value, surface, sectionName) {
|
|
939
|
+
const field = `${sectionName}.by`;
|
|
940
|
+
const actual = textField(value, field);
|
|
941
|
+
const accepted = GATE_SECTION_BY_ACCEPTOR[surface][sectionName];
|
|
942
|
+
if (accepted === undefined || !accepted.includes(actual))
|
|
943
|
+
invalid(`${field} must be one of ${accepted?.join(", ") ?? "the configured reviewers"}.`, field);
|
|
944
|
+
return actual;
|
|
818
945
|
}
|
|
819
946
|
function validateQualityGate(input, opts) {
|
|
820
|
-
const
|
|
947
|
+
const surface = opts?.reviewerSurface ?? "lazycodex";
|
|
948
|
+
aggregateQualityGateDefects(input, opts);
|
|
821
949
|
const gate = section(input, "qualityGate");
|
|
822
|
-
const
|
|
950
|
+
const requiredSections = REQUIRED_GATE_SECTIONS_BY_SURFACE[surface];
|
|
951
|
+
for (const sectionName of requiredSections)
|
|
952
|
+
section(gate[sectionName], sectionName);
|
|
953
|
+
if (surface === "omo-senpi" && gate["codeReview"] !== undefined)
|
|
954
|
+
invalid("omo-senpi gate has no codeReview lane.", "codeReview");
|
|
823
955
|
const manualQa = section(gate["manualQa"], "manualQa");
|
|
824
956
|
const gateReview = section(gate["gateReview"], "gateReview");
|
|
825
957
|
const iteration = section(gate["iteration"], "iteration");
|
|
826
958
|
const coverage = section(gate["criteriaCoverage"], "criteriaCoverage");
|
|
959
|
+
const manualQaBy = reviewerAcceptorField(manualQa["by"], surface, "manualQa");
|
|
960
|
+
const gateReviewBy = reviewerAcceptorField(gateReview["by"], surface, "gateReview");
|
|
961
|
+
if (surface === "lazycodex")
|
|
962
|
+
reviewerRoleField(section(gate["codeReview"], "codeReview")["by"], "lazycodex-code-reviewer", "codeReview.by");
|
|
827
963
|
const totalCriteria = numberField(coverage["totalCriteria"], "criteriaCoverage.totalCriteria");
|
|
828
964
|
const passCount = numberField(coverage["passCount"], "criteriaCoverage.passCount");
|
|
829
965
|
if (passCount < totalCriteria)
|
|
@@ -832,21 +968,11 @@ function validateQualityGate(input, opts) {
|
|
|
832
968
|
const byId = artifactMap(artifactRefs);
|
|
833
969
|
const surfaceEvidence = parseSurfaceEvidence(manualQa["surfaceEvidence"], byId);
|
|
834
970
|
const adversarialCases = parseAdversarialCases(manualQa["adversarialCases"], byId);
|
|
835
|
-
const codeReportPath = textField(codeReview["reportPath"], "codeReview.reportPath");
|
|
836
971
|
const gateReportPath = textField(gateReview["reportPath"], "gateReview.reportPath");
|
|
837
|
-
checkFile(codeReportPath, "codeReview.reportPath", opts);
|
|
838
972
|
checkFile(gateReportPath, "gateReview.reportPath", opts);
|
|
839
|
-
|
|
840
|
-
codeReview: {
|
|
841
|
-
by: reviewerRoleField(codeReview["by"], reviewerRoles.codeReview, "codeReview.by"),
|
|
842
|
-
recommendation: literal(codeReview["recommendation"], "APPROVE", "codeReview.recommendation"),
|
|
843
|
-
codeQualityStatus: codeQualityStatusField(codeReview["codeQualityStatus"], "codeReview.codeQualityStatus"),
|
|
844
|
-
reportPath: codeReportPath,
|
|
845
|
-
evidence: textField(codeReview["evidence"], "codeReview.evidence"),
|
|
846
|
-
blockers: emptyBlockers(codeReview["blockers"], "codeReview.blockers")
|
|
847
|
-
},
|
|
973
|
+
const common = {
|
|
848
974
|
manualQa: {
|
|
849
|
-
by:
|
|
975
|
+
by: manualQaBy,
|
|
850
976
|
status: literal(manualQa["status"], "passed", "manualQa.status"),
|
|
851
977
|
evidence: textField(manualQa["evidence"], "manualQa.evidence"),
|
|
852
978
|
surfaceEvidence,
|
|
@@ -854,7 +980,7 @@ function validateQualityGate(input, opts) {
|
|
|
854
980
|
artifactRefs
|
|
855
981
|
},
|
|
856
982
|
gateReview: {
|
|
857
|
-
by:
|
|
983
|
+
by: gateReviewBy,
|
|
858
984
|
recommendation: literal(gateReview["recommendation"], "APPROVE", "gateReview.recommendation"),
|
|
859
985
|
reportPath: gateReportPath,
|
|
860
986
|
evidence: textField(gateReview["evidence"], "gateReview.evidence"),
|
|
@@ -875,6 +1001,23 @@ function validateQualityGate(input, opts) {
|
|
|
875
1001
|
adversarialClassesCovered: stringArray(coverage["adversarialClassesCovered"], "criteriaCoverage.adversarialClassesCovered")
|
|
876
1002
|
}
|
|
877
1003
|
};
|
|
1004
|
+
if (surface === "omo-senpi")
|
|
1005
|
+
return { surface, ...common };
|
|
1006
|
+
const codeReview = section(gate["codeReview"], "codeReview");
|
|
1007
|
+
const codeReportPath = textField(codeReview["reportPath"], "codeReview.reportPath");
|
|
1008
|
+
checkFile(codeReportPath, "codeReview.reportPath", opts);
|
|
1009
|
+
return {
|
|
1010
|
+
surface,
|
|
1011
|
+
...common,
|
|
1012
|
+
codeReview: {
|
|
1013
|
+
by: "lazycodex-code-reviewer",
|
|
1014
|
+
recommendation: literal(codeReview["recommendation"], "APPROVE", "codeReview.recommendation"),
|
|
1015
|
+
codeQualityStatus: codeQualityStatusField(codeReview["codeQualityStatus"], "codeReview.codeQualityStatus"),
|
|
1016
|
+
reportPath: codeReportPath,
|
|
1017
|
+
evidence: textField(codeReview["evidence"], "codeReview.evidence"),
|
|
1018
|
+
blockers: emptyBlockers(codeReview["blockers"], "codeReview.blockers")
|
|
1019
|
+
}
|
|
1020
|
+
};
|
|
878
1021
|
}
|
|
879
1022
|
function parseSurfaceEvidence(value, byId) {
|
|
880
1023
|
if (!Array.isArray(value) || value.length === 0)
|
|
@@ -917,7 +1060,7 @@ function parseAdversarialCases(value, byId) {
|
|
|
917
1060
|
});
|
|
918
1061
|
}
|
|
919
1062
|
|
|
920
|
-
//
|
|
1063
|
+
// components/ulw-loop/src/cli-arg-parser.ts
|
|
921
1064
|
import { readFile as readFile4 } from "node:fs/promises";
|
|
922
1065
|
var VALUE_FLAGS = new Set("--brief --brief-file --session-id --codex-goal-mode --validation-batch-json --goal --goal-id --criterion-id --status --evidence --notes --codex-goal-json --quality-gate-json --kind --rationale --title --objective --target-goal-id --source --after-json --directive-json --directive-file --idempotency-key --proposals-json".split(" "));
|
|
923
1066
|
var SUBCOMMANDS = new Set("create-goals status complete-goals criteria record-evidence checkpoint steer add-goal record-review-blockers".split(" "));
|
|
@@ -1008,14 +1151,14 @@ function parseRecordEvidenceArgs(argv) {
|
|
|
1008
1151
|
return notes ? { ...result, notes } : result;
|
|
1009
1152
|
}
|
|
1010
1153
|
|
|
1011
|
-
//
|
|
1154
|
+
// components/ulw-loop/src/validation-batch.ts
|
|
1012
1155
|
function isObject(value) {
|
|
1013
1156
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1014
1157
|
}
|
|
1015
1158
|
function read(value, key) {
|
|
1016
1159
|
return Object.entries(value).find(([name]) => name === key)?.[1];
|
|
1017
1160
|
}
|
|
1018
|
-
function
|
|
1161
|
+
function text2(value, key) {
|
|
1019
1162
|
const candidate = read(value, key);
|
|
1020
1163
|
return typeof candidate === "string" && candidate.trim().length > 0 ? candidate.trim() : undefined;
|
|
1021
1164
|
}
|
|
@@ -1036,9 +1179,9 @@ async function parseValidationBatches(input, goals) {
|
|
|
1036
1179
|
function batchFromObject(value) {
|
|
1037
1180
|
if (!isObject(value))
|
|
1038
1181
|
fail("validation batch entries must be objects.");
|
|
1039
|
-
const batchId =
|
|
1182
|
+
const batchId = text2(value, "batchId");
|
|
1040
1183
|
const memberIds = strings(value, "memberIds");
|
|
1041
|
-
const finalGoalId =
|
|
1184
|
+
const finalGoalId = text2(value, "finalGoalId");
|
|
1042
1185
|
if (batchId === undefined)
|
|
1043
1186
|
fail("validation batch requires batchId.");
|
|
1044
1187
|
if (memberIds === undefined || memberIds.length < 2)
|
|
@@ -1126,7 +1269,7 @@ function fail(message, code = "ULW_LOOP_VALIDATION_BATCH_INVALID") {
|
|
|
1126
1269
|
throw new UlwLoopError(message, code);
|
|
1127
1270
|
}
|
|
1128
1271
|
|
|
1129
|
-
//
|
|
1272
|
+
// components/ulw-loop/src/checkpoint.ts
|
|
1130
1273
|
var QUALITY_GATE_FS = { existsSync: existsSync4, statSync };
|
|
1131
1274
|
function ulwLoopFail2(message, code) {
|
|
1132
1275
|
throw new UlwLoopError(message, code);
|
|
@@ -1152,7 +1295,7 @@ async function readJsonInput2(raw, repoRoot) {
|
|
|
1152
1295
|
if (!(error instanceof SyntaxError))
|
|
1153
1296
|
throw error;
|
|
1154
1297
|
}
|
|
1155
|
-
const path =
|
|
1298
|
+
const path = resolve4(repoRoot, trimmed);
|
|
1156
1299
|
if (!existsSync4(path))
|
|
1157
1300
|
return ulwLoopFail2("Quality gate JSON is neither valid JSON nor a readable path.", "ulw_loop_json_input_invalid");
|
|
1158
1301
|
try {
|
|
@@ -1196,13 +1339,13 @@ function ledgerKind2(status, goal, aggregateCompletion) {
|
|
|
1196
1339
|
return status === "blocked" ? "goal_blocked" : "goal_failed";
|
|
1197
1340
|
}
|
|
1198
1341
|
function buildLedger(now, args, goal, qualityGate, codexGoal, aggregateCompletion) {
|
|
1199
|
-
const watch = qualityGate?.codeReview.codeQualityStatus === "WATCH";
|
|
1342
|
+
const watch = qualityGate?.surface === "lazycodex" && qualityGate.codeReview.codeQualityStatus === "WATCH";
|
|
1200
1343
|
const entry = {
|
|
1201
1344
|
at: now,
|
|
1202
1345
|
kind: ledgerKind2(args.status, goal, aggregateCompletion),
|
|
1203
1346
|
goalId: goal.id,
|
|
1204
1347
|
status: goal.status,
|
|
1205
|
-
evidence: watch ? `${args.evidence} | codeQuality=WATCH: ${qualityGate.codeReview.evidence}` : args.evidence
|
|
1348
|
+
evidence: watch && qualityGate.surface === "lazycodex" ? `${args.evidence} | codeQuality=WATCH: ${qualityGate.codeReview.evidence}` : args.evidence
|
|
1206
1349
|
};
|
|
1207
1350
|
if (codexGoal !== undefined)
|
|
1208
1351
|
entry.codexGoal = codexGoal;
|
|
@@ -1253,7 +1396,7 @@ async function checkpointUlwLoop(repoRoot, args, scope) {
|
|
|
1253
1396
|
const activeFinalTaskScoped = mismatchedTaskObjective && snapshot.status === "active" && await canReconcileActiveFinalTaskScopedAggregateSnapshot(repoRoot, plan, goal, objective, evidence, scope);
|
|
1254
1397
|
const taskScoped = completedTaskScoped || activeFinalTaskScoped;
|
|
1255
1398
|
if (!taskScoped)
|
|
1256
|
-
throw
|
|
1399
|
+
throw codexSnapshotMismatchError({ reconciliation, snapshot, expectedObjective: expectedCodexObjective(plan, goal), taskScopedHint: { goal, aggregate, final } });
|
|
1257
1400
|
}
|
|
1258
1401
|
if (closesBatch)
|
|
1259
1402
|
requireBatchFinalReady(plan, goal);
|
|
@@ -1294,7 +1437,116 @@ async function checkpointUlwLoop(repoRoot, args, scope) {
|
|
|
1294
1437
|
});
|
|
1295
1438
|
}
|
|
1296
1439
|
|
|
1297
|
-
//
|
|
1440
|
+
// components/ulw-loop/src/checkpoint-template.ts
|
|
1441
|
+
function artifactPath(base, name) {
|
|
1442
|
+
return `${base}/${name}`;
|
|
1443
|
+
}
|
|
1444
|
+
function gateTemplate(surface, base) {
|
|
1445
|
+
const artifacts = [
|
|
1446
|
+
{
|
|
1447
|
+
id: "artifact-cli",
|
|
1448
|
+
kind: "cli-transcript",
|
|
1449
|
+
description: "<replace:artifact description>",
|
|
1450
|
+
path: artifactPath(base, "cli-transcript.txt")
|
|
1451
|
+
},
|
|
1452
|
+
{
|
|
1453
|
+
id: "artifact-data",
|
|
1454
|
+
kind: "data-diff",
|
|
1455
|
+
description: "<replace:artifact description>",
|
|
1456
|
+
path: artifactPath(base, "data-diff.txt")
|
|
1457
|
+
}
|
|
1458
|
+
];
|
|
1459
|
+
const manualQa = {
|
|
1460
|
+
by: surface === "omo-senpi" ? "main-session" : "lazycodex-qa-executor",
|
|
1461
|
+
status: "passed",
|
|
1462
|
+
evidence: "<replace:manual QA evidence>",
|
|
1463
|
+
surfaceEvidence: [
|
|
1464
|
+
{
|
|
1465
|
+
id: "surface-cli",
|
|
1466
|
+
criterionRef: "<replace:criterion id>",
|
|
1467
|
+
surface: "cli",
|
|
1468
|
+
invocation: "<replace:command>",
|
|
1469
|
+
verdict: "passed",
|
|
1470
|
+
artifactRefs: ["artifact-cli"]
|
|
1471
|
+
},
|
|
1472
|
+
{
|
|
1473
|
+
id: "surface-data",
|
|
1474
|
+
criterionRef: "<replace:criterion id>",
|
|
1475
|
+
surface: "data",
|
|
1476
|
+
invocation: "<replace:command>",
|
|
1477
|
+
verdict: "passed",
|
|
1478
|
+
artifactRefs: ["artifact-data"]
|
|
1479
|
+
}
|
|
1480
|
+
],
|
|
1481
|
+
adversarialCases: [
|
|
1482
|
+
{
|
|
1483
|
+
id: "<replace:adversarial case id>",
|
|
1484
|
+
criterionRef: "<replace:criterion id>",
|
|
1485
|
+
scenario: "<replace:scenario>",
|
|
1486
|
+
expectedBehavior: "<replace:expected behavior>",
|
|
1487
|
+
verdict: "not_applicable",
|
|
1488
|
+
reason: "<replace:reason>",
|
|
1489
|
+
artifactRefs: ["artifact-cli"]
|
|
1490
|
+
}
|
|
1491
|
+
],
|
|
1492
|
+
artifactRefs: artifacts
|
|
1493
|
+
};
|
|
1494
|
+
const common = {
|
|
1495
|
+
manualQa,
|
|
1496
|
+
gateReview: {
|
|
1497
|
+
by: surface === "omo-senpi" ? "category:deep" : "lazycodex-gate-reviewer",
|
|
1498
|
+
recommendation: "APPROVE",
|
|
1499
|
+
reportPath: artifactPath(base, "gate-review.md"),
|
|
1500
|
+
evidence: "<replace:gate review evidence>",
|
|
1501
|
+
blockers: []
|
|
1502
|
+
},
|
|
1503
|
+
iteration: {
|
|
1504
|
+
fullRerun: true,
|
|
1505
|
+
status: "passed",
|
|
1506
|
+
rerunCommands: ["<replace:verification command>"],
|
|
1507
|
+
evidence: "<replace:iteration evidence>"
|
|
1508
|
+
},
|
|
1509
|
+
criteriaCoverage: {
|
|
1510
|
+
totalCriteria: 0,
|
|
1511
|
+
passCount: 0,
|
|
1512
|
+
originalIntent: "<replace:original intent>",
|
|
1513
|
+
desiredOutcome: "<replace:desired outcome>",
|
|
1514
|
+
userOutcomeReview: "<replace:user outcome review>",
|
|
1515
|
+
adversarialClassesCovered: ["<replace:adversarial class>"]
|
|
1516
|
+
}
|
|
1517
|
+
};
|
|
1518
|
+
if (surface === "omo-senpi")
|
|
1519
|
+
return common;
|
|
1520
|
+
return {
|
|
1521
|
+
codeReview: {
|
|
1522
|
+
by: "lazycodex-code-reviewer",
|
|
1523
|
+
recommendation: "APPROVE",
|
|
1524
|
+
codeQualityStatus: "CLEAR",
|
|
1525
|
+
reportPath: artifactPath(base, "code-review.md"),
|
|
1526
|
+
evidence: "<replace:code review evidence>",
|
|
1527
|
+
blockers: []
|
|
1528
|
+
},
|
|
1529
|
+
...common
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
async function checkpointTemplate(repoRoot, scope, goalId) {
|
|
1533
|
+
const plan = await readUlwLoopPlan(repoRoot, scope);
|
|
1534
|
+
const targetId = goalId ?? plan.activeGoalId;
|
|
1535
|
+
const active = plan.goals.find((goal) => goal.id === targetId);
|
|
1536
|
+
if (goalId !== undefined && active === undefined)
|
|
1537
|
+
throw new UlwLoopError(`Unknown ulw-loop id: ${goalId}.`, "ULW_LOOP_GOAL_NOT_FOUND", { details: { goalId } });
|
|
1538
|
+
const hasAttempt = plan.evidenceLayoutVersion === 2 && active !== undefined;
|
|
1539
|
+
const attemptDir = hasAttempt ? ulwLoopAttemptEvidenceDir(active.id, active.attempt, scope) : ".omo/evidence";
|
|
1540
|
+
return {
|
|
1541
|
+
qualityGateTemplate: gateTemplate(resolveToolkitSurface(), attemptDir),
|
|
1542
|
+
codexGoalTemplate: {
|
|
1543
|
+
goal: { objective: plan.codexObjective ?? "<replace:codex objective>", status: "complete" }
|
|
1544
|
+
},
|
|
1545
|
+
...hasAttempt ? { attemptDir } : { guidance: "This plan is evidence-layout v1; artifacts go under .omo/evidence/." }
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
// components/ulw-loop/src/cli-output.ts
|
|
1298
1550
|
var ULW_LOOP_HELP = `Usage:
|
|
1299
1551
|
omo-agent-toolkit hook user-prompt-submit [--with-ultrawork] (Codex UserPromptSubmit hook)
|
|
1300
1552
|
omo-agent-toolkit help | --help | -h (this message)
|
|
@@ -1304,6 +1556,7 @@ var ULW_LOOP_HELP = `Usage:
|
|
|
1304
1556
|
omo-agent-toolkit ulw-loop complete-goals [--retry-failed] [--json]
|
|
1305
1557
|
omo-agent-toolkit ulw-loop criteria --goal-id <id> [--json]
|
|
1306
1558
|
omo-agent-toolkit ulw-loop record-evidence --goal-id <id> --criterion-id <id> --status pass|fail|blocked --evidence "..." [--notes "..."] [--json]
|
|
1559
|
+
omo-agent-toolkit ulw-loop checkpoint --print-template [--goal-id <id>] [--json]
|
|
1307
1560
|
omo-agent-toolkit ulw-loop checkpoint --goal-id <id> --status complete|failed|blocked --evidence "..." --codex-goal-json <...> [--quality-gate-json <...>] [--no-advance] [--json]
|
|
1308
1561
|
omo-agent-toolkit ulw-loop steer --kind <kind> ... --evidence "..." --rationale "..." [--proposals-json <json-or-path>] [--json]
|
|
1309
1562
|
omo-agent-toolkit ulw-loop add-goal --title "..." --objective "..." [--json]
|
|
@@ -1384,7 +1637,7 @@ function normalizeCodexGoalMode(value) {
|
|
|
1384
1637
|
throw new UlwLoopError("Invalid --codex-goal-mode; expected aggregate or per_story.", "ULW_LOOP_CODEX_GOAL_MODE_INVALID", { details: { value } });
|
|
1385
1638
|
}
|
|
1386
1639
|
|
|
1387
|
-
//
|
|
1640
|
+
// components/ulw-loop/src/codex-goal-instruction.ts
|
|
1388
1641
|
function buildCodexGoalInstruction(args) {
|
|
1389
1642
|
const mode = codexGoalMode(args.plan);
|
|
1390
1643
|
const createGoal = buildCreateGoalPayload(args.plan, args.goal);
|
|
@@ -1469,6 +1722,8 @@ function finalSection(plan, goal, isFinal, aggregate, surface) {
|
|
|
1469
1722
|
if (!isFinal)
|
|
1470
1723
|
return "- This is not the final ulw-loop story; do not run the final reviewer/manual-QA/gate-review quality gate yet.";
|
|
1471
1724
|
const option = sessionOption(plan);
|
|
1725
|
+
if (surface === "omo-senpi")
|
|
1726
|
+
return senpiFinalSection(plan, goal, aggregate);
|
|
1472
1727
|
const blockerCommand = `omo-agent-toolkit ulw-loop record-review-blockers${option} --goal-id ${goal.id} --title "Resolve final code-review blockers" --objective "<blocker-resolution objective>" --evidence "<review findings>" --codex-goal-json "<active get_goal JSON or path>"`;
|
|
1473
1728
|
const checkpointCommand = `omo-agent-toolkit ulw-loop checkpoint${option} --goal-id ${goal.id} --status complete --evidence "<targeted verification/manualQa/gateReview evidence>" --codex-goal-json "<fresh complete get_goal JSON or path>" --quality-gate-json "<quality gate JSON or path>"`;
|
|
1474
1729
|
return joinLines([
|
|
@@ -1486,6 +1741,20 @@ function finalSection(plan, goal, isFinal, aggregate, surface) {
|
|
|
1486
1741
|
` ${checkpointCommand}`
|
|
1487
1742
|
]);
|
|
1488
1743
|
}
|
|
1744
|
+
function senpiFinalSection(plan, goal, aggregate) {
|
|
1745
|
+
const option = sessionOption(plan);
|
|
1746
|
+
const checkpointCommand = `omo-agent-toolkit ulw-loop checkpoint${option} --goal-id ${goal.id} --status complete --evidence "<manualQa/gateReview evidence>" --codex-goal-json "<fresh complete get_goal JSON or path>" --quality-gate-json "$(omo-agent-toolkit ulw-loop checkpoint${option} --print-template)"`;
|
|
1747
|
+
return joinLines([
|
|
1748
|
+
"Final story — run the single-reviewer quality gate before update_goal:",
|
|
1749
|
+
'- Run manual QA yourself and write the non-empty artifact under currentAttemptDir; set manualQa.by to "main-session".',
|
|
1750
|
+
'- Spawn exactly one gate reviewer with task(category: "deep").',
|
|
1751
|
+
"- If that task fails with any model_unavailable failure, retry with category:unspecified-high, then category:unspecified-low.",
|
|
1752
|
+
"- Set gateReview.by to the exact category:<name> literal used for the successful task.",
|
|
1753
|
+
"- Build the gate JSON with omo-agent-toolkit ulw-loop checkpoint --print-template, then fill manualQa, gateReview, iteration, and criteriaCoverage.",
|
|
1754
|
+
'- Require passed manualQa, approved gateReview, passed iteration, and complete criteriaCoverage before update_goal({status: "complete"}).',
|
|
1755
|
+
aggregate ? `- If the gate is clean, call update_goal({status: "complete"}), call get_goal again, then checkpoint the aggregate story: ${checkpointCommand}` : `- If the gate is clean, call update_goal({status: "complete"}), call get_goal again, then checkpoint: ${checkpointCommand}`
|
|
1756
|
+
]);
|
|
1757
|
+
}
|
|
1489
1758
|
function sessionOption(plan) {
|
|
1490
1759
|
const prefix = ".omo/ulw-loop/";
|
|
1491
1760
|
const suffix = "/goals.json";
|
|
@@ -1499,11 +1768,11 @@ function joinLines(lines) {
|
|
|
1499
1768
|
`);
|
|
1500
1769
|
}
|
|
1501
1770
|
|
|
1502
|
-
//
|
|
1771
|
+
// components/ulw-loop/src/plan-crud.ts
|
|
1503
1772
|
import { existsSync as existsSync5 } from "node:fs";
|
|
1504
1773
|
import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
|
|
1505
1774
|
|
|
1506
|
-
//
|
|
1775
|
+
// components/ulw-loop/src/plan-goal-factory.ts
|
|
1507
1776
|
function cleanLine(line) {
|
|
1508
1777
|
return line.replace(/^\s*(?:[-*+]\s+|\d+[.)]\s+)/, "").trim();
|
|
1509
1778
|
}
|
|
@@ -1592,7 +1861,7 @@ function appendGoalToPlan(plan, title, objective, now) {
|
|
|
1592
1861
|
return goal;
|
|
1593
1862
|
}
|
|
1594
1863
|
|
|
1595
|
-
//
|
|
1864
|
+
// components/ulw-loop/src/plan-crud.ts
|
|
1596
1865
|
function isScheduleEligible(goal) {
|
|
1597
1866
|
return goal.steeringStatus !== "superseded" && goal.steeringStatus !== "blocked";
|
|
1598
1867
|
}
|
|
@@ -1719,7 +1988,7 @@ function summarizeUlwLoopPlan(plan) {
|
|
|
1719
1988
|
};
|
|
1720
1989
|
}
|
|
1721
1990
|
|
|
1722
|
-
//
|
|
1991
|
+
// components/ulw-loop/src/checkpoint-continuation.ts
|
|
1723
1992
|
async function checkpointAndContinue(repoRoot, args, scope) {
|
|
1724
1993
|
const result = await checkpointUlwLoop(repoRoot, args, scope);
|
|
1725
1994
|
if (args.status !== "complete" || result.aggregateCompletion !== undefined || !args.advance)
|
|
@@ -1731,6 +2000,14 @@ async function checkpointAndContinue(repoRoot, args, scope) {
|
|
|
1731
2000
|
return { ...result, plan: next.plan, next: { resumed: next.resumed, goal: next.goal, instruction } };
|
|
1732
2001
|
}
|
|
1733
2002
|
async function checkpoint(repoRoot, argv, json, scope) {
|
|
2003
|
+
if (hasFlag(argv, "--print-template")) {
|
|
2004
|
+
const template = await checkpointTemplate(repoRoot, scope, readValue(argv, "--goal-id"));
|
|
2005
|
+
if (json)
|
|
2006
|
+
printJson({ ok: true, ...template });
|
|
2007
|
+
else
|
|
2008
|
+
printJson(template);
|
|
2009
|
+
return 0;
|
|
2010
|
+
}
|
|
1734
2011
|
const goalId = required2(argv, "--goal-id");
|
|
1735
2012
|
const statusValue = checkpointStatus(required2(argv, "--status"));
|
|
1736
2013
|
const evidence = required2(argv, "--evidence");
|
|
@@ -1782,10 +2059,10 @@ function checkpointStatus(value) {
|
|
|
1782
2059
|
throw new UlwLoopError("Missing or invalid --status; expected complete, failed, or blocked.", "ULW_LOOP_STATUS_INVALID", { details: { status: value } });
|
|
1783
2060
|
}
|
|
1784
2061
|
|
|
1785
|
-
//
|
|
2062
|
+
// components/ulw-loop/src/cli-subcommands.ts
|
|
1786
2063
|
import { readFile as readFile6 } from "node:fs/promises";
|
|
1787
2064
|
|
|
1788
|
-
//
|
|
2065
|
+
// components/ulw-loop/src/cli-steering.ts
|
|
1789
2066
|
var SOURCES = ["user_prompt_submit", "finding", "cli"];
|
|
1790
2067
|
var STEERING_KIND_HELP = [
|
|
1791
2068
|
`Allowed --kind values: ${ULW_LOOP_STEERING_MUTATION_KINDS.join(", ")}`,
|
|
@@ -1817,7 +2094,7 @@ function kindMessage(prefix) {
|
|
|
1817
2094
|
|
|
1818
2095
|
${STEERING_KIND_HELP}`;
|
|
1819
2096
|
}
|
|
1820
|
-
function
|
|
2097
|
+
function text3(value, field) {
|
|
1821
2098
|
if (value === undefined)
|
|
1822
2099
|
return;
|
|
1823
2100
|
const trimmed = value.trim();
|
|
@@ -1826,11 +2103,11 @@ function text2(value, field) {
|
|
|
1826
2103
|
return fail2(`Empty ${field}.`, "ULW_LOOP_STEERING_FIELD_EMPTY", { field });
|
|
1827
2104
|
}
|
|
1828
2105
|
function required3(argv, flag) {
|
|
1829
|
-
const value =
|
|
2106
|
+
const value = text3(readValue(argv, flag), flag);
|
|
1830
2107
|
return value ?? fail2(`Missing ${flag}.`, "ULW_LOOP_STEERING_FIELD_REQUIRED", { flag });
|
|
1831
2108
|
}
|
|
1832
2109
|
function requiredGoal(argv) {
|
|
1833
|
-
const value =
|
|
2110
|
+
const value = text3(parseGoalArg(argv), "--goal-id");
|
|
1834
2111
|
return value ?? fail2("Missing --goal-id.", "ULW_LOOP_GOAL_ID_REQUIRED", { flag: "--goal-id" });
|
|
1835
2112
|
}
|
|
1836
2113
|
function readObject(value, key) {
|
|
@@ -1881,14 +2158,14 @@ function parseSteeringSource(argv) {
|
|
|
1881
2158
|
function child(value) {
|
|
1882
2159
|
if (!isPlain(value))
|
|
1883
2160
|
return null;
|
|
1884
|
-
const title =
|
|
1885
|
-
const objective =
|
|
2161
|
+
const title = text3(objectText(value, "title"), "title");
|
|
2162
|
+
const objective = text3(objectText(value, "objective"), "objective");
|
|
1886
2163
|
if (title === undefined || objective === undefined)
|
|
1887
2164
|
return null;
|
|
1888
2165
|
return { title, objective };
|
|
1889
2166
|
}
|
|
1890
2167
|
async function children(argv, flag, needed) {
|
|
1891
|
-
const input = needed ? required3(argv, flag) :
|
|
2168
|
+
const input = needed ? required3(argv, flag) : text3(readValue(argv, flag), flag);
|
|
1892
2169
|
if (input === undefined)
|
|
1893
2170
|
return [];
|
|
1894
2171
|
const raw = await readJsonInput(input);
|
|
@@ -1911,12 +2188,12 @@ async function stringArray2(argv, flag) {
|
|
|
1911
2188
|
for (const item of raw) {
|
|
1912
2189
|
if (typeof item !== "string")
|
|
1913
2190
|
return fail2(`${flag} entries must be strings.`, "ULW_LOOP_STEERING_STRING_ARRAY_REQUIRED", { flag });
|
|
1914
|
-
values.push(
|
|
2191
|
+
values.push(text3(item, flag) ?? "");
|
|
1915
2192
|
}
|
|
1916
2193
|
return values;
|
|
1917
2194
|
}
|
|
1918
2195
|
function model(value) {
|
|
1919
|
-
const trimmed =
|
|
2196
|
+
const trimmed = text3(value, "--user-model");
|
|
1920
2197
|
if (trimmed === undefined)
|
|
1921
2198
|
return;
|
|
1922
2199
|
return isModel(trimmed) ? trimmed : fail2(`Invalid --user-model: ${trimmed}.`, "ULW_LOOP_STEERING_USER_MODEL_INVALID", { value: trimmed, expected: ULW_LOOP_SUCCESS_CRITERION_USER_MODELS });
|
|
@@ -1927,7 +2204,7 @@ function neverKind(kind) {
|
|
|
1927
2204
|
async function parseSteeringProposal(argv) {
|
|
1928
2205
|
const kind = parseSteeringKind(argv);
|
|
1929
2206
|
const source = parseSteeringSource(argv);
|
|
1930
|
-
const idempotencyKey =
|
|
2207
|
+
const idempotencyKey = text3(readValue(argv, "--idempotency-key"), "--idempotency-key");
|
|
1931
2208
|
const base = { kind, source, evidence: required3(argv, "--evidence"), rationale: required3(argv, "--rationale"), ...idempotencyKey === undefined ? {} : { idempotencyKey } };
|
|
1932
2209
|
switch (kind) {
|
|
1933
2210
|
case "add_subgoal":
|
|
@@ -1970,36 +2247,36 @@ async function parseSteeringProposal(argv) {
|
|
|
1970
2247
|
function normalizedChildren(values) {
|
|
1971
2248
|
if (values === undefined)
|
|
1972
2249
|
return;
|
|
1973
|
-
return values.map((item) => ({ title:
|
|
2250
|
+
return values.map((item) => ({ title: text3(item.title, "child.title") ?? "", objective: text3(item.objective, "child.objective") ?? "" }));
|
|
1974
2251
|
}
|
|
1975
2252
|
function normalizedStrings(values, field) {
|
|
1976
2253
|
if (values === undefined)
|
|
1977
2254
|
return;
|
|
1978
|
-
return values.map((value) =>
|
|
2255
|
+
return values.map((value) => text3(value, field) ?? "");
|
|
1979
2256
|
}
|
|
1980
2257
|
function normalizeSteeringProposal(proposal) {
|
|
1981
|
-
const evidence =
|
|
1982
|
-
const rationale =
|
|
1983
|
-
const goalId =
|
|
1984
|
-
const targetGoalId =
|
|
2258
|
+
const evidence = text3(proposal.evidence, "evidence") ?? "";
|
|
2259
|
+
const rationale = text3(proposal.rationale, "rationale") ?? "";
|
|
2260
|
+
const goalId = text3(proposal.goalId, "goalId");
|
|
2261
|
+
const targetGoalId = text3(proposal.targetGoalId, "targetGoalId");
|
|
1985
2262
|
const targetGoalIds = normalizedStrings(proposal.targetGoalIds, "targetGoalIds");
|
|
1986
|
-
const criterionId =
|
|
1987
|
-
const title =
|
|
1988
|
-
const objective =
|
|
1989
|
-
const revisedTitle =
|
|
1990
|
-
const revisedObjective =
|
|
1991
|
-
const blockedReason =
|
|
1992
|
-
const directiveText =
|
|
1993
|
-
const promptSignature =
|
|
1994
|
-
const idempotencyKey =
|
|
1995
|
-
const scenario =
|
|
1996
|
-
const expectedEvidence =
|
|
2263
|
+
const criterionId = text3(proposal.criterionId, "criterionId");
|
|
2264
|
+
const title = text3(proposal.title, "title");
|
|
2265
|
+
const objective = text3(proposal.objective, "objective");
|
|
2266
|
+
const revisedTitle = text3(proposal.revisedTitle, "revisedTitle");
|
|
2267
|
+
const revisedObjective = text3(proposal.revisedObjective, "revisedObjective");
|
|
2268
|
+
const blockedReason = text3(proposal.blockedReason, "blockedReason");
|
|
2269
|
+
const directiveText = text3(proposal.directiveText, "directiveText");
|
|
2270
|
+
const promptSignature = text3(proposal.promptSignature, "promptSignature");
|
|
2271
|
+
const idempotencyKey = text3(proposal.idempotencyKey, "idempotencyKey");
|
|
2272
|
+
const scenario = text3(proposal.scenario, "scenario");
|
|
2273
|
+
const expectedEvidence = text3(proposal.expectedEvidence, "expectedEvidence");
|
|
1997
2274
|
const childGoals = normalizedChildren(proposal.childGoals);
|
|
1998
2275
|
const pendingOrder = normalizedStrings(proposal.pendingOrder, "pendingOrder");
|
|
1999
2276
|
return { kind: proposal.kind, source: proposal.source, evidence, rationale, ...goalId === undefined ? {} : { goalId }, ...targetGoalId === undefined ? {} : { targetGoalId }, ...targetGoalIds === undefined ? {} : { targetGoalIds }, ...criterionId === undefined ? {} : { criterionId }, ...title === undefined ? {} : { title }, ...objective === undefined ? {} : { objective }, ...childGoals === undefined ? {} : { childGoals }, ...revisedTitle === undefined ? {} : { revisedTitle }, ...revisedObjective === undefined ? {} : { revisedObjective }, ...pendingOrder === undefined ? {} : { pendingOrder }, ...blockedReason === undefined ? {} : { blockedReason }, ...proposal.after === undefined ? {} : { after: proposal.after }, ...directiveText === undefined ? {} : { directiveText }, ...promptSignature === undefined ? {} : { promptSignature }, ...idempotencyKey === undefined ? {} : { idempotencyKey }, ...proposal.now === undefined ? {} : { now: proposal.now }, ...scenario === undefined ? {} : { scenario }, ...expectedEvidence === undefined ? {} : { expectedEvidence }, ...proposal.userModel === undefined ? {} : { userModel: proposal.userModel } };
|
|
2000
2277
|
}
|
|
2001
2278
|
async function parseSteeringProposals(argv) {
|
|
2002
|
-
const input =
|
|
2279
|
+
const input = text3(readValue(argv, "--proposals-json"), "--proposals-json");
|
|
2003
2280
|
if (input === undefined)
|
|
2004
2281
|
return [await parseSteeringProposal(argv)];
|
|
2005
2282
|
if (readValue(argv, "--kind") !== undefined)
|
|
@@ -2092,7 +2369,7 @@ function printSteerBatchResult(result, json) {
|
|
|
2092
2369
|
printStatus(result.plan);
|
|
2093
2370
|
}
|
|
2094
2371
|
|
|
2095
|
-
//
|
|
2372
|
+
// components/ulw-loop/src/review-blockers.ts
|
|
2096
2373
|
var BLOCKER_FIELDS = "blockedReason blockerSignature blockerOccurrenceCount requiredExternalDecision nonRetriable failedAt failureReason completedAt blocker blockerEvidence blockerOccurrences blockedAt".split(" ");
|
|
2097
2374
|
function ulwLoopError(message, code) {
|
|
2098
2375
|
throw new UlwLoopError(message, code);
|
|
@@ -2131,9 +2408,10 @@ async function recordFinalReviewBlockers(repoRoot, args, scope) {
|
|
|
2131
2408
|
ulwLoopError(`${goal.id} is not final.`, "ulw_loop_not_final_story");
|
|
2132
2409
|
const snapshot = await readCodexGoalSnapshotInput(args.codexGoalJson, repoRoot);
|
|
2133
2410
|
const aggregate = codexGoalMode(plan) === "aggregate";
|
|
2134
|
-
const
|
|
2411
|
+
const expected = expectedCodexObjective(plan, goal);
|
|
2412
|
+
const reconciliation = reconcileCodexGoalSnapshot(snapshot, { expectedObjective: expected, ...aggregate ? { acceptedObjectives: compatibleCodexObjectives(plan) } : {}, allowedStatuses: ["active"], requireSnapshot: true, requireComplete: false });
|
|
2135
2413
|
if (!reconciliation.ok)
|
|
2136
|
-
|
|
2414
|
+
throw codexSnapshotMismatchError({ reconciliation, snapshot, expectedObjective: expected });
|
|
2137
2415
|
const now = iso();
|
|
2138
2416
|
for (const field of BLOCKER_FIELDS)
|
|
2139
2417
|
Reflect.deleteProperty(goal, field);
|
|
@@ -2158,10 +2436,39 @@ async function recordFinalReviewBlockers(repoRoot, args, scope) {
|
|
|
2158
2436
|
});
|
|
2159
2437
|
}
|
|
2160
2438
|
|
|
2161
|
-
//
|
|
2439
|
+
// components/ulw-loop/src/status-next-actions.ts
|
|
2440
|
+
function statusNextActions(plan) {
|
|
2441
|
+
const actions = [];
|
|
2442
|
+
const active = plan.goals.find((goal) => goal.id === plan.activeGoalId);
|
|
2443
|
+
if (plan.goals.length === 0)
|
|
2444
|
+
actions.push(`No goals yet: bootstrap with \`${ULW_LOOP_CREATE_GOALS_COMMAND}\`.`);
|
|
2445
|
+
else if (active !== undefined)
|
|
2446
|
+
actions.push(...activeGoalActions(plan, active));
|
|
2447
|
+
for (const goal of plan.goals.filter((candidate) => candidate.status === "review_blocked"))
|
|
2448
|
+
actions.push(`${goal.id} is review_blocked: capture the reviewer verdict with \`omo-agent-toolkit ulw-loop record-review-blockers --goal-id ${goal.id} --title "<title>" --objective "<objective>" --evidence "<verdict>" --codex-goal-json '<get_goal json>'\`.`);
|
|
2449
|
+
if (plan.evidenceLayoutVersion !== 2 || active === undefined)
|
|
2450
|
+
actions.push("plan is evidence-layout v1; artifacts go under .omo/evidence/");
|
|
2451
|
+
return actions;
|
|
2452
|
+
}
|
|
2453
|
+
function activeGoalActions(plan, active) {
|
|
2454
|
+
const unresolved = active.successCriteria.filter((criterion) => criterion.status !== "pass");
|
|
2455
|
+
if (unresolved.length > 0)
|
|
2456
|
+
return [
|
|
2457
|
+
`${active.id} has ${unresolved.length} unresolved criterion(s) (${unresolved.map((criterion) => criterion.id).join(", ")}): record proof with \`omo-agent-toolkit ulw-loop record-evidence --goal-id ${active.id} --criterion-id <id> --status pass --evidence "<observable proof>"\`.`
|
|
2458
|
+
];
|
|
2459
|
+
if (hasAllCriteriaPass(active) && isFinalRunCompletionCandidate(plan, active))
|
|
2460
|
+
return [
|
|
2461
|
+
`${active.id} passes every criterion and is the final story: update_goal complete, then checkpoint --print-template to build the final quality gate.`
|
|
2462
|
+
];
|
|
2463
|
+
return [
|
|
2464
|
+
`${active.id} passes every criterion: close it with \`omo-agent-toolkit ulw-loop checkpoint --goal-id ${active.id} --status complete --evidence "<proof>" --codex-goal-json '<get_goal json>'\`.`
|
|
2465
|
+
];
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
// components/ulw-loop/src/steering-mutations.ts
|
|
2162
2469
|
var read2 = (value, key) => Object.entries(value).find(([name]) => name === key)?.[1];
|
|
2163
2470
|
var isText = (value) => typeof value === "string" && value.trim().length > 0;
|
|
2164
|
-
var
|
|
2471
|
+
var text4 = (value, key) => {
|
|
2165
2472
|
const candidate = read2(value, key);
|
|
2166
2473
|
return isText(candidate) ? candidate.trim() : undefined;
|
|
2167
2474
|
};
|
|
@@ -2170,13 +2477,13 @@ var after = (proposal) => {
|
|
|
2170
2477
|
const candidate = read2(proposal, "after");
|
|
2171
2478
|
return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate) ? candidate : undefined;
|
|
2172
2479
|
};
|
|
2173
|
-
var revised = (proposal, direct, nested) =>
|
|
2174
|
-
var targets = (proposal) => proposal.targetGoalIds ?? [proposal.targetGoalId ??
|
|
2480
|
+
var revised = (proposal, direct, nested) => text4(proposal, direct) ?? text4(after(proposal) ?? proposal, nested);
|
|
2481
|
+
var targets = (proposal) => proposal.targetGoalIds ?? [proposal.targetGoalId ?? text4(proposal, "goalId") ?? ""].filter(Boolean);
|
|
2175
2482
|
var child2 = (value) => {
|
|
2176
2483
|
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
2177
2484
|
return null;
|
|
2178
|
-
const title =
|
|
2179
|
-
const objective =
|
|
2485
|
+
const title = text4(value, "title");
|
|
2486
|
+
const objective = text4(value, "objective");
|
|
2180
2487
|
return title === undefined || objective === undefined ? null : { title, objective };
|
|
2181
2488
|
};
|
|
2182
2489
|
var children2 = (proposal) => {
|
|
@@ -2241,11 +2548,11 @@ function reviseCriterion(plan, proposal, now) {
|
|
|
2241
2548
|
if (target === undefined || current === undefined)
|
|
2242
2549
|
return;
|
|
2243
2550
|
const model2 = read2(proposal, "userModel");
|
|
2244
|
-
target.successCriteria[index] = { ...current, scenario:
|
|
2551
|
+
target.successCriteria[index] = { ...current, scenario: text4(proposal, "scenario") ?? current.scenario, expectedEvidence: text4(proposal, "expectedEvidence") ?? current.expectedEvidence, userModel: isModel2(model2) ? model2 : current.userModel };
|
|
2245
2552
|
target.updatedAt = now;
|
|
2246
2553
|
}
|
|
2247
2554
|
|
|
2248
|
-
//
|
|
2555
|
+
// components/ulw-loop/src/steering-snapshot.ts
|
|
2249
2556
|
function buildSteeringPlanSnapshot(plan, changedGoalIds) {
|
|
2250
2557
|
const snapshot = {
|
|
2251
2558
|
updatedAt: plan.updatedAt,
|
|
@@ -2269,14 +2576,14 @@ function changedGoalIdsBetween(before, after2) {
|
|
|
2269
2576
|
return changed;
|
|
2270
2577
|
}
|
|
2271
2578
|
|
|
2272
|
-
//
|
|
2579
|
+
// components/ulw-loop/src/steering.ts
|
|
2273
2580
|
var SOURCES2 = ["user_prompt_submit", "finding", "cli"];
|
|
2274
2581
|
var PROTECTED = new Set(["aggregateCompletion", "codexObjective", "codexObjectiveAliases", "originalConstraints", "qualityGate", "status", "completedAt", "completionStatus"]);
|
|
2275
2582
|
var isObject2 = (value) => typeof value === "object" && value !== null;
|
|
2276
2583
|
var isPlain2 = (value) => isObject2(value) && !Array.isArray(value);
|
|
2277
2584
|
var read3 = (value, key) => Object.entries(value).find(([name]) => name === key)?.[1];
|
|
2278
2585
|
var isText2 = (value) => typeof value === "string" && value.trim().length > 0;
|
|
2279
|
-
var
|
|
2586
|
+
var text5 = (value, key) => {
|
|
2280
2587
|
const candidate = read3(value, key);
|
|
2281
2588
|
return isText2(candidate) ? candidate.trim() : undefined;
|
|
2282
2589
|
};
|
|
@@ -2289,19 +2596,19 @@ var texts = (value, key) => {
|
|
|
2289
2596
|
};
|
|
2290
2597
|
function targets2(proposal) {
|
|
2291
2598
|
const many = texts(proposal, "targetGoalIds");
|
|
2292
|
-
const one =
|
|
2599
|
+
const one = text5(proposal, "targetGoalId") ?? text5(proposal, "goalId");
|
|
2293
2600
|
return many.length > 0 ? many : one === undefined ? [] : [one];
|
|
2294
2601
|
}
|
|
2295
2602
|
var after2 = (proposal) => {
|
|
2296
2603
|
const candidate = read3(proposal, "after");
|
|
2297
2604
|
return isPlain2(candidate) ? candidate : undefined;
|
|
2298
2605
|
};
|
|
2299
|
-
var revised2 = (proposal, direct, nested) =>
|
|
2606
|
+
var revised2 = (proposal, direct, nested) => text5(proposal, direct) ?? text5(after2(proposal) ?? proposal, nested);
|
|
2300
2607
|
function child3(value) {
|
|
2301
2608
|
if (!isPlain2(value))
|
|
2302
2609
|
return null;
|
|
2303
|
-
const title =
|
|
2304
|
-
const objective =
|
|
2610
|
+
const title = text5(value, "title");
|
|
2611
|
+
const objective = text5(value, "objective");
|
|
2305
2612
|
if (title === undefined || objective === undefined)
|
|
2306
2613
|
return null;
|
|
2307
2614
|
return { title, objective };
|
|
@@ -2340,15 +2647,15 @@ function auditFor(proposal, reasons) {
|
|
|
2340
2647
|
const object = isPlain2(proposal) ? proposal : undefined;
|
|
2341
2648
|
const kindRaw = object === undefined ? undefined : read3(object, "kind");
|
|
2342
2649
|
const sourceRaw = object === undefined ? undefined : read3(object, "source");
|
|
2343
|
-
const evidence = object === undefined ? "" :
|
|
2344
|
-
const rationale = object === undefined ? "" :
|
|
2650
|
+
const evidence = object === undefined ? "" : text5(object, "evidence") ?? "";
|
|
2651
|
+
const rationale = object === undefined ? "" : text5(object, "rationale") ?? "";
|
|
2345
2652
|
const audit = { kind: isKind2(kindRaw) ? kindRaw : "annotate_ledger", source: isSource2(sourceRaw) ? sourceRaw : "cli", targetGoalIds: object === undefined ? [] : targets2(object), evidence, rationale, invariant: { accepted: reasons.length === 0, structuralInvariantAccepted: reasons.length === 0, evidenceBackedNecessity: evidence.length > 0 && rationale.length > 0, noEasierCompletion: !weakens(proposal), rejectedReasons: reasons, reasons } };
|
|
2346
2653
|
if (object === undefined)
|
|
2347
2654
|
return audit;
|
|
2348
|
-
const criterionId =
|
|
2349
|
-
const directiveText =
|
|
2350
|
-
const promptSignature =
|
|
2351
|
-
const idempotencyKey =
|
|
2655
|
+
const criterionId = text5(object, "criterionId");
|
|
2656
|
+
const directiveText = text5(object, "directiveText");
|
|
2657
|
+
const promptSignature = text5(object, "promptSignature");
|
|
2658
|
+
const idempotencyKey = text5(object, "idempotencyKey");
|
|
2352
2659
|
if (criterionId !== undefined)
|
|
2353
2660
|
audit.criterionId = criterionId;
|
|
2354
2661
|
if (directiveText !== undefined)
|
|
@@ -2369,9 +2676,9 @@ function validateUlwLoopSteeringProposal(plan, proposal) {
|
|
|
2369
2676
|
reasons.push(`invalid kind: ${String(kind)}`);
|
|
2370
2677
|
if (!isSource2(read3(object, "source")))
|
|
2371
2678
|
reasons.push(`invalid source: ${String(read3(object, "source"))}`);
|
|
2372
|
-
if (
|
|
2679
|
+
if (text5(object, "evidence") === undefined)
|
|
2373
2680
|
reasons.push("missing evidence");
|
|
2374
|
-
if (
|
|
2681
|
+
if (text5(object, "rationale") === undefined)
|
|
2375
2682
|
reasons.push("missing rationale");
|
|
2376
2683
|
if (hasProtected(proposal))
|
|
2377
2684
|
reasons.push("protected payload");
|
|
@@ -2388,7 +2695,7 @@ function goal2(plan, id) {
|
|
|
2388
2695
|
}
|
|
2389
2696
|
function validateKind(plan, proposal, kind, reasons) {
|
|
2390
2697
|
const target = goal2(plan, targets2(proposal)[0]);
|
|
2391
|
-
if (kind === "add_subgoal" && (
|
|
2698
|
+
if (kind === "add_subgoal" && (text5(proposal, "title") === undefined || text5(proposal, "objective") === undefined))
|
|
2392
2699
|
reasons.push("add_subgoal requires title/objective");
|
|
2393
2700
|
if ((kind === "split_subgoal" || kind === "revise_pending_wording" || kind === "mark_blocked_superseded") && target === undefined)
|
|
2394
2701
|
reasons.push(`${kind} requires target`);
|
|
@@ -2418,7 +2725,7 @@ function validateOrder(plan, proposal, reasons) {
|
|
|
2418
2725
|
}
|
|
2419
2726
|
function validateCriterion(plan, proposal, reasons) {
|
|
2420
2727
|
const target = goal2(plan, targets2(proposal)[0]);
|
|
2421
|
-
const criterionId =
|
|
2728
|
+
const criterionId = text5(proposal, "criterionId");
|
|
2422
2729
|
if (target === undefined)
|
|
2423
2730
|
reasons.push("revise_criterion requires goalId");
|
|
2424
2731
|
else if (criterionId === undefined || target.successCriteria.every((item) => item.id !== criterionId))
|
|
@@ -2453,8 +2760,8 @@ function applySteeringMutation(plan, proposal, audit) {
|
|
|
2453
2760
|
function isProposal(value) {
|
|
2454
2761
|
return isPlain2(value) && isKind2(read3(value, "kind")) && isSource2(read3(value, "source")) && isText2(read3(value, "evidence")) && isText2(read3(value, "rationale"));
|
|
2455
2762
|
}
|
|
2456
|
-
function parseUlwLoopSteeringDirective(
|
|
2457
|
-
const match = /(?:^|\s)(?:OMO_ULW_LOOP_STEER|omo\.ulw-loop\.steer|omo ulw-loop steer|omo-agent-toolkit ulw-loop steer):\s*([\s\S]+)$/u.exec(
|
|
2763
|
+
function parseUlwLoopSteeringDirective(text6) {
|
|
2764
|
+
const match = /(?:^|\s)(?:OMO_ULW_LOOP_STEER|omo\.ulw-loop\.steer|omo ulw-loop steer|omo-agent-toolkit ulw-loop steer):\s*([\s\S]+)$/u.exec(text6);
|
|
2458
2765
|
if (match?.[1] === undefined)
|
|
2459
2766
|
return null;
|
|
2460
2767
|
try {
|
|
@@ -2506,7 +2813,7 @@ function ledgerEntry(proposal, audit, at) {
|
|
|
2506
2813
|
return entry;
|
|
2507
2814
|
}
|
|
2508
2815
|
|
|
2509
|
-
//
|
|
2816
|
+
// components/ulw-loop/src/steering-batch.ts
|
|
2510
2817
|
async function steerUlwLoopBatch(repoRoot, proposals, scope) {
|
|
2511
2818
|
return withUlwLoopMutationLock(repoRoot, scope, async () => {
|
|
2512
2819
|
const plan = await readUlwLoopPlan(repoRoot, scope);
|
|
@@ -2587,7 +2894,7 @@ function ledgerEntry2(proposal, audit, at) {
|
|
|
2587
2894
|
return entry;
|
|
2588
2895
|
}
|
|
2589
2896
|
|
|
2590
|
-
//
|
|
2897
|
+
// components/ulw-loop/src/cli-subcommands.ts
|
|
2591
2898
|
async function createGoals(repoRoot, argv, json, scope) {
|
|
2592
2899
|
const briefFile = readValue(argv, "--brief-file");
|
|
2593
2900
|
const brief = readValue(argv, "--brief") ?? (briefFile === undefined ? undefined : await readFile6(briefFile, "utf8")) ?? (hasFlag(argv, "--from-stdin") ? await readStdin() : undefined) ?? positionalText(argv);
|
|
@@ -2621,7 +2928,8 @@ async function status(repoRoot, json, scope) {
|
|
|
2621
2928
|
ok: true,
|
|
2622
2929
|
plan,
|
|
2623
2930
|
summary: summarizeUlwLoopPlan(plan),
|
|
2624
|
-
...currentAttemptDir === undefined ? {} : { currentAttemptDir }
|
|
2931
|
+
...currentAttemptDir === undefined ? {} : { currentAttemptDir },
|
|
2932
|
+
nextActions: statusNextActions(plan)
|
|
2625
2933
|
});
|
|
2626
2934
|
} else
|
|
2627
2935
|
printStatus(plan);
|
|
@@ -2743,7 +3051,7 @@ function findGoal3(plan, goalId) {
|
|
|
2743
3051
|
throw new UlwLoopError(`Unknown ulw-loop id: ${goalId}.`, "ULW_LOOP_GOAL_NOT_FOUND", { details: { goalId } });
|
|
2744
3052
|
}
|
|
2745
3053
|
|
|
2746
|
-
//
|
|
3054
|
+
// components/ulw-loop/src/cli-commands.ts
|
|
2747
3055
|
var ULW_LOOP_SUBCOMMANDS = [
|
|
2748
3056
|
"help",
|
|
2749
3057
|
"create-goals",
|
|
@@ -2837,7 +3145,7 @@ function commandScope(argv) {
|
|
|
2837
3145
|
if (sessionIdFlagPresent(argv)) {
|
|
2838
3146
|
const sessionId2 = readValue(argv, SESSION_ID_FLAG)?.trim();
|
|
2839
3147
|
if (!sessionId2) {
|
|
2840
|
-
throw new UlwLoopError(
|
|
3148
|
+
throw new UlwLoopError(sessionIdRequiredMessage(SESSION_ID_FLAG), "ULW_LOOP_SESSION_ID_REQUIRED", {
|
|
2841
3149
|
details: { flag: SESSION_ID_FLAG }
|
|
2842
3150
|
});
|
|
2843
3151
|
}
|
|
@@ -2847,10 +3155,10 @@ function commandScope(argv) {
|
|
|
2847
3155
|
return sessionId === null ? undefined : { sessionId };
|
|
2848
3156
|
}
|
|
2849
3157
|
|
|
2850
|
-
//
|
|
3158
|
+
// components/ulw-loop/src/ultrawork-directive.ts
|
|
2851
3159
|
import { readFileSync as readFileSync3 } from "node:fs";
|
|
2852
3160
|
|
|
2853
|
-
//
|
|
3161
|
+
// components/ulw-loop/src/ultrawork-skill-pointer.ts
|
|
2854
3162
|
import { existsSync as existsSync6, readFileSync as readFileSync2 } from "node:fs";
|
|
2855
3163
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
2856
3164
|
var ULTRAWORK_SKILL_POINTER_TEMPLATE = `<ultrawork-mode>
|
|
@@ -2898,7 +3206,7 @@ function buildUltraworkAdditionalContext(options = {}) {
|
|
|
2898
3206
|
return ULTRAWORK_DIRECTIVE;
|
|
2899
3207
|
}
|
|
2900
3208
|
|
|
2901
|
-
//
|
|
3209
|
+
// components/ulw-loop/src/ultrawork-directive.ts
|
|
2902
3210
|
var ULTRAWORK_CURRENT_PROMPT_PATTERN = /(?:ultrawork|ulw)/i;
|
|
2903
3211
|
var ULTRAWORK_DIRECTIVE_MARKER = "<ultrawork-mode>";
|
|
2904
3212
|
var TRANSCRIPT_SEARCH_BYTES = 512000;
|
|
@@ -3001,7 +3309,7 @@ function isRecord3(value) {
|
|
|
3001
3309
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3002
3310
|
}
|
|
3003
3311
|
|
|
3004
|
-
//
|
|
3312
|
+
// components/ulw-loop/src/codex-hook.ts
|
|
3005
3313
|
var CREATE_GOAL_TOOL_NAME = "create_goal";
|
|
3006
3314
|
var CREATE_GOAL_PAYLOAD_WARNING = "Use create_goal with objective only. Omit token_budget so the goal stays unlimited, and put lifecycle status changes on update_goal.";
|
|
3007
3315
|
function parseUserPromptSubmitPayload(raw) {
|
|
@@ -3127,23 +3435,41 @@ function optionalString(value) {
|
|
|
3127
3435
|
return value === undefined || typeof value === "string";
|
|
3128
3436
|
}
|
|
3129
3437
|
function readAll(stdin) {
|
|
3130
|
-
return new Promise((
|
|
3438
|
+
return new Promise((resolve5, reject) => {
|
|
3131
3439
|
let data = "";
|
|
3132
3440
|
stdin.setEncoding("utf8");
|
|
3133
3441
|
stdin.on("data", (chunk) => {
|
|
3134
3442
|
data += chunk instanceof Buffer ? chunk.toString() : String(chunk);
|
|
3135
3443
|
});
|
|
3136
3444
|
stdin.once("error", reject);
|
|
3137
|
-
stdin.once("end", () =>
|
|
3445
|
+
stdin.once("end", () => resolve5(data));
|
|
3138
3446
|
});
|
|
3139
3447
|
}
|
|
3140
3448
|
|
|
3141
|
-
//
|
|
3142
|
-
import {
|
|
3143
|
-
import {
|
|
3449
|
+
// components/ulw-loop/src/spawn-guard.ts
|
|
3450
|
+
import { randomBytes } from "node:crypto";
|
|
3451
|
+
import {
|
|
3452
|
+
existsSync as existsSync7,
|
|
3453
|
+
mkdirSync,
|
|
3454
|
+
openSync,
|
|
3455
|
+
readdirSync as readdirSync2,
|
|
3456
|
+
readFileSync as readFileSync4,
|
|
3457
|
+
renameSync,
|
|
3458
|
+
statSync as statSync2,
|
|
3459
|
+
unlinkSync,
|
|
3460
|
+
writeFileSync
|
|
3461
|
+
} from "node:fs";
|
|
3462
|
+
import { dirname as dirname2, join as join3 } from "node:path";
|
|
3144
3463
|
var SPAWN_TOOL_TOKENS = new Set(["spawn_agent", "collaborationspawn_agent", "collaboration.spawn_agent"]);
|
|
3145
3464
|
var DEFAULT_FANOUT_LIMIT = 60;
|
|
3465
|
+
var DEFAULT_REVIEW_SPAWN_LIMIT = 3;
|
|
3146
3466
|
var GATE_MESSAGE_PATTERN = /lazycodex-gate-reviewer|omo-senpi-gate-reviewer|final gate review/i;
|
|
3467
|
+
var REVIEW_AGENT_TYPES = [
|
|
3468
|
+
...Object.values(REVIEWER_ROLES_BY_SURFACE).map((roles) => roles.gateReview),
|
|
3469
|
+
...Object.values(REVIEWER_ROLES_BY_SURFACE).map((roles) => roles.codeReview),
|
|
3470
|
+
...Object.values(REVIEWER_ROLES_BY_SURFACE).map((roles) => roles.manualQa)
|
|
3471
|
+
];
|
|
3472
|
+
var REVIEW_AGENT_TYPE_SET = new Set(REVIEW_AGENT_TYPES);
|
|
3147
3473
|
function applySpawnGuards(payload) {
|
|
3148
3474
|
if (payload.hook_event_name !== "PreToolUse" || !SPAWN_TOOL_TOKENS.has(payload.tool_name))
|
|
3149
3475
|
return "";
|
|
@@ -3151,12 +3477,18 @@ function applySpawnGuards(payload) {
|
|
|
3151
3477
|
const plan = readPlan(join3(stateDir, "goals.json"));
|
|
3152
3478
|
if (plan === null)
|
|
3153
3479
|
return "";
|
|
3154
|
-
const
|
|
3155
|
-
if (
|
|
3156
|
-
return deny(
|
|
3480
|
+
const fanOutPeek = peekFanOutBudget(stateDir);
|
|
3481
|
+
if (fanOutPeek !== null)
|
|
3482
|
+
return deny(fanOutPeek);
|
|
3157
3483
|
const missingArtifact = missingGateArtifact(payload, plan);
|
|
3158
3484
|
if (missingArtifact !== null)
|
|
3159
3485
|
return deny(`spawn code-review + QA first; gate audits their artifacts: missing ${missingArtifact}`);
|
|
3486
|
+
const reviewDenial = consumeReviewSpawnBudget(payload, plan, stateDir);
|
|
3487
|
+
if (reviewDenial !== null)
|
|
3488
|
+
return deny(reviewDenial);
|
|
3489
|
+
const fanOutDenial = consumeFanOutBudget(stateDir);
|
|
3490
|
+
if (fanOutDenial !== null)
|
|
3491
|
+
return deny(fanOutDenial);
|
|
3160
3492
|
return "";
|
|
3161
3493
|
}
|
|
3162
3494
|
async function runSpawnGuardCli(stdin, stdout) {
|
|
@@ -3175,15 +3507,44 @@ async function runSpawnGuardCli(stdin, stdout) {
|
|
|
3175
3507
|
return;
|
|
3176
3508
|
}
|
|
3177
3509
|
}
|
|
3510
|
+
function peekFanOutBudget(stateDir) {
|
|
3511
|
+
const counterPath = join3(stateDir, "spawn-count.json");
|
|
3512
|
+
const count = readCount(counterPath) + 1;
|
|
3513
|
+
const limit = fanOutLimit();
|
|
3514
|
+
if (count <= limit)
|
|
3515
|
+
return null;
|
|
3516
|
+
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.`;
|
|
3517
|
+
}
|
|
3178
3518
|
function consumeFanOutBudget(stateDir) {
|
|
3179
3519
|
const counterPath = join3(stateDir, "spawn-count.json");
|
|
3180
3520
|
const count = readCount(counterPath) + 1;
|
|
3181
|
-
|
|
3521
|
+
atomicWriteJson(counterPath, { count });
|
|
3182
3522
|
const limit = fanOutLimit();
|
|
3183
3523
|
if (count <= limit)
|
|
3184
3524
|
return null;
|
|
3185
3525
|
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.`;
|
|
3186
3526
|
}
|
|
3527
|
+
function consumeReviewSpawnBudget(payload, plan, stateDir) {
|
|
3528
|
+
const agentType = reviewAgentType(payload.tool_input);
|
|
3529
|
+
if (agentType === null)
|
|
3530
|
+
return null;
|
|
3531
|
+
const goal3 = plan.goals.find((candidate) => candidate.id === plan.activeGoalId) ?? plan.goals.find((candidate) => isFinalRunCompletionCandidate(plan, candidate));
|
|
3532
|
+
if (goal3 === undefined)
|
|
3533
|
+
return null;
|
|
3534
|
+
const counterPath = join3(stateDir, "review-spawn-counts.json");
|
|
3535
|
+
const lockPath = `${counterPath}.lock`;
|
|
3536
|
+
const limit = reviewSpawnLimit();
|
|
3537
|
+
return withExclusiveLock(lockPath, () => {
|
|
3538
|
+
const counts = readCounts(counterPath);
|
|
3539
|
+
const key = `${agentType}:${goal3.id}:a${goal3.attempt}`;
|
|
3540
|
+
const count = (counts[key] ?? 0) + 1;
|
|
3541
|
+
if (count > limit)
|
|
3542
|
+
return `ulw-loop reviewer no-progress cap reached (${agentType} ${count}/${limit}) for ${goal3.id} attempt ${goal3.attempt}. Consolidate existing review findings, or checkpoint and start a new attempt after concrete progress.`;
|
|
3543
|
+
counts[key] = count;
|
|
3544
|
+
atomicWriteJson(counterPath, counts);
|
|
3545
|
+
return null;
|
|
3546
|
+
});
|
|
3547
|
+
}
|
|
3187
3548
|
function missingGateArtifact(payload, plan) {
|
|
3188
3549
|
if (!isGateReviewerSpawn(payload.tool_input))
|
|
3189
3550
|
return null;
|
|
@@ -3193,9 +3554,11 @@ function missingGateArtifact(payload, plan) {
|
|
|
3193
3554
|
if (!goal3.successCriteria.every((criterion) => criterion.status === "pass"))
|
|
3194
3555
|
return null;
|
|
3195
3556
|
const scope = { sessionId: payload.session_id };
|
|
3557
|
+
const surface = resolveToolkitSurface();
|
|
3558
|
+
const requiredArtifacts = surface === "omo-senpi" ? [`${goal3.id}-manual-qa.md`] : [`${goal3.id}-code-review.md`, `${goal3.id}-manual-qa.md`];
|
|
3196
3559
|
if (plan.evidenceLayoutVersion === 2) {
|
|
3197
3560
|
const attemptDir = ulwLoopAttemptEvidenceDir(goal3.id, goal3.attempt, scope);
|
|
3198
|
-
for (const name of
|
|
3561
|
+
for (const name of requiredArtifacts) {
|
|
3199
3562
|
const relative2 = `${attemptDir}/${name}`;
|
|
3200
3563
|
if (!isNonEmptyFile(join3(payload.cwd, relative2)))
|
|
3201
3564
|
return relative2;
|
|
@@ -3203,21 +3566,97 @@ function missingGateArtifact(payload, plan) {
|
|
|
3203
3566
|
return null;
|
|
3204
3567
|
}
|
|
3205
3568
|
const flatReport = `.omo/evidence/${goal3.id}-code-review.md`;
|
|
3206
|
-
if (!isNonEmptyFile(join3(payload.cwd, flatReport)))
|
|
3569
|
+
if (surface !== "omo-senpi" && !isNonEmptyFile(join3(payload.cwd, flatReport)))
|
|
3207
3570
|
return flatReport;
|
|
3571
|
+
if (surface === "omo-senpi") {
|
|
3572
|
+
const manualQa = `.omo/evidence/${goal3.id}-manual-qa.md`;
|
|
3573
|
+
return isNonEmptyFile(join3(payload.cwd, manualQa)) ? null : manualQa;
|
|
3574
|
+
}
|
|
3208
3575
|
if (!hasOtherEvidenceFile(join3(payload.cwd, ".omo", "evidence"), `${goal3.id}-code-review.md`))
|
|
3209
|
-
return `.omo/evidence
|
|
3576
|
+
return `.omo/evidence/${goal3.id}-manual-qa.md`;
|
|
3210
3577
|
return null;
|
|
3211
3578
|
}
|
|
3212
3579
|
function isGateReviewerSpawn(toolInput) {
|
|
3580
|
+
const agentType = reviewAgentType(toolInput);
|
|
3581
|
+
return agentType !== null && GATE_REVIEWER_AGENT_NAMES.has(agentType);
|
|
3582
|
+
}
|
|
3583
|
+
function reviewAgentType(toolInput) {
|
|
3213
3584
|
if (typeof toolInput !== "object" || toolInput === null)
|
|
3214
|
-
return
|
|
3585
|
+
return null;
|
|
3215
3586
|
const record = toolInput;
|
|
3216
3587
|
const agentType = record["agent_type"];
|
|
3217
|
-
if (typeof agentType === "string")
|
|
3218
|
-
|
|
3588
|
+
if (typeof agentType === "string") {
|
|
3589
|
+
if (!REVIEW_AGENT_TYPE_SET.has(agentType))
|
|
3590
|
+
return null;
|
|
3591
|
+
return activeSurfaceReviewerAlias(agentType);
|
|
3592
|
+
}
|
|
3219
3593
|
const message = record["message"];
|
|
3220
|
-
|
|
3594
|
+
if (typeof message !== "string")
|
|
3595
|
+
return null;
|
|
3596
|
+
const normalizedMessage = message.toLowerCase();
|
|
3597
|
+
const allRoleNames = [...REVIEW_AGENT_TYPES];
|
|
3598
|
+
const explicitAssignment = allRoleNames.map((name) => ({
|
|
3599
|
+
name,
|
|
3600
|
+
index: normalizedMessage.search(new RegExp(`\\bact as (?:an? )?${name}\\b`))
|
|
3601
|
+
})).filter(({ index }) => index >= 0).sort((left, right) => left.index - right.index)[0];
|
|
3602
|
+
if (explicitAssignment !== undefined)
|
|
3603
|
+
return activeSurfaceReviewerAlias(explicitAssignment.name);
|
|
3604
|
+
const nonReviewerActAs = /\bact as (?:an? )?\S+/.test(normalizedMessage);
|
|
3605
|
+
if (nonReviewerActAs)
|
|
3606
|
+
return null;
|
|
3607
|
+
const namedReviewer = REVIEW_AGENT_TYPES.find((name) => normalizedMessage.includes(name));
|
|
3608
|
+
if (namedReviewer !== undefined)
|
|
3609
|
+
return activeSurfaceReviewerAlias(namedReviewer);
|
|
3610
|
+
return GATE_MESSAGE_PATTERN.test(message) ? reviewerRolesFor(resolveToolkitSurface()).gateReview : null;
|
|
3611
|
+
}
|
|
3612
|
+
function activeSurfaceReviewerAlias(reviewer) {
|
|
3613
|
+
const activeRoles = reviewerRolesFor(resolveToolkitSurface());
|
|
3614
|
+
for (const roles of Object.values(REVIEWER_ROLES_BY_SURFACE)) {
|
|
3615
|
+
if (reviewer === roles.codeReview)
|
|
3616
|
+
return activeRoles.codeReview;
|
|
3617
|
+
if (reviewer === roles.manualQa)
|
|
3618
|
+
return activeRoles.manualQa;
|
|
3619
|
+
if (reviewer === roles.gateReview)
|
|
3620
|
+
return activeRoles.gateReview;
|
|
3621
|
+
}
|
|
3622
|
+
return reviewer;
|
|
3623
|
+
}
|
|
3624
|
+
function withExclusiveLock(lockPath, fn) {
|
|
3625
|
+
mkdirSync(dirname2(lockPath), { recursive: true });
|
|
3626
|
+
const maxAttempts = 10;
|
|
3627
|
+
const baseDelayMs = 10;
|
|
3628
|
+
for (let attempt = 0;attempt < maxAttempts; attempt++) {
|
|
3629
|
+
let fd = null;
|
|
3630
|
+
try {
|
|
3631
|
+
fd = openSync(lockPath, "wx");
|
|
3632
|
+
writeFileSync(fd, process.pid.toString());
|
|
3633
|
+
try {
|
|
3634
|
+
return fn();
|
|
3635
|
+
} finally {
|
|
3636
|
+
try {
|
|
3637
|
+
unlinkSync(lockPath);
|
|
3638
|
+
} catch {}
|
|
3639
|
+
}
|
|
3640
|
+
} catch (error) {
|
|
3641
|
+
if (fd !== null) {
|
|
3642
|
+
try {
|
|
3643
|
+
unlinkSync(lockPath);
|
|
3644
|
+
} catch {}
|
|
3645
|
+
}
|
|
3646
|
+
if (error.code !== "EEXIST") {
|
|
3647
|
+
return fn();
|
|
3648
|
+
}
|
|
3649
|
+
const delayMs = baseDelayMs * 2 ** attempt + Math.random() * baseDelayMs;
|
|
3650
|
+
const deadline = Date.now() + delayMs;
|
|
3651
|
+
while (Date.now() < deadline) {}
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3654
|
+
return fn();
|
|
3655
|
+
}
|
|
3656
|
+
function atomicWriteJson(targetPath, data) {
|
|
3657
|
+
const tmp = join3(dirname2(targetPath), `.tmp-${randomBytes(6).toString("hex")}`);
|
|
3658
|
+
writeFileSync(tmp, JSON.stringify(data));
|
|
3659
|
+
renameSync(tmp, targetPath);
|
|
3221
3660
|
}
|
|
3222
3661
|
function deny(reason) {
|
|
3223
3662
|
return `${JSON.stringify({
|
|
@@ -3237,6 +3676,13 @@ function fanOutLimit() {
|
|
|
3237
3676
|
const parsed = Number.parseInt(raw, 10);
|
|
3238
3677
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_FANOUT_LIMIT;
|
|
3239
3678
|
}
|
|
3679
|
+
function reviewSpawnLimit() {
|
|
3680
|
+
const raw = process.env["OMO_ULW_LOOP_REVIEW_SPAWN_LIMIT"];
|
|
3681
|
+
if (raw === undefined)
|
|
3682
|
+
return DEFAULT_REVIEW_SPAWN_LIMIT;
|
|
3683
|
+
const parsed = Number.parseInt(raw, 10);
|
|
3684
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_REVIEW_SPAWN_LIMIT;
|
|
3685
|
+
}
|
|
3240
3686
|
function isNonEmptyFile(path) {
|
|
3241
3687
|
try {
|
|
3242
3688
|
return existsSync7(path) && statSync2(path).size > 0;
|
|
@@ -3248,7 +3694,7 @@ function isNonEmptyFile(path) {
|
|
|
3248
3694
|
}
|
|
3249
3695
|
function hasOtherEvidenceFile(evidenceDir, excludedName) {
|
|
3250
3696
|
try {
|
|
3251
|
-
return
|
|
3697
|
+
return readdirSync2(evidenceDir).some((name) => name !== excludedName && isNonEmptyFile(join3(evidenceDir, name)));
|
|
3252
3698
|
} catch (error) {
|
|
3253
3699
|
if (error instanceof Error)
|
|
3254
3700
|
return false;
|
|
@@ -3265,6 +3711,23 @@ function readCount(counterPath) {
|
|
|
3265
3711
|
throw error;
|
|
3266
3712
|
}
|
|
3267
3713
|
}
|
|
3714
|
+
function readCounts(counterPath) {
|
|
3715
|
+
try {
|
|
3716
|
+
const parsed = JSON.parse(readFileSync4(counterPath, "utf8"));
|
|
3717
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
3718
|
+
return {};
|
|
3719
|
+
const counts = {};
|
|
3720
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
3721
|
+
if (typeof value === "number" && value >= 0)
|
|
3722
|
+
counts[key] = value;
|
|
3723
|
+
}
|
|
3724
|
+
return counts;
|
|
3725
|
+
} catch (error) {
|
|
3726
|
+
if (error instanceof Error)
|
|
3727
|
+
return {};
|
|
3728
|
+
throw error;
|
|
3729
|
+
}
|
|
3730
|
+
}
|
|
3268
3731
|
function readPlan(goalsPath) {
|
|
3269
3732
|
try {
|
|
3270
3733
|
return JSON.parse(readFileSync4(goalsPath, "utf8"));
|
|
@@ -3275,9 +3738,9 @@ function readPlan(goalsPath) {
|
|
|
3275
3738
|
}
|
|
3276
3739
|
}
|
|
3277
3740
|
|
|
3278
|
-
//
|
|
3741
|
+
// components/ulw-loop/src/stop-resume-hook.ts
|
|
3279
3742
|
import { existsSync as existsSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "node:fs";
|
|
3280
|
-
import { isAbsolute as isAbsolute2, join as join4, resolve as
|
|
3743
|
+
import { isAbsolute as isAbsolute2, join as join4, resolve as resolve5, sep as sep2 } from "node:path";
|
|
3281
3744
|
var RESUME_CAP = 2;
|
|
3282
3745
|
var CONTEXT_PRESSURE_MARKERS2 = [
|
|
3283
3746
|
"context compacted",
|
|
@@ -3335,8 +3798,8 @@ function isResumableStatus(status2) {
|
|
|
3335
3798
|
}
|
|
3336
3799
|
function consumeResumeBudget(stateDir, goalId) {
|
|
3337
3800
|
const ledgerLineCount = countLedgerLines(join4(stateDir, "ledger.jsonl"));
|
|
3338
|
-
const counterPath =
|
|
3339
|
-
const stuckPath =
|
|
3801
|
+
const counterPath = resolve5(stateDir, `auto-resume-${goalId}.json`);
|
|
3802
|
+
const stuckPath = resolve5(stateDir, `auto-resume-${goalId}.stuck`);
|
|
3340
3803
|
if (!isInsideDir(stateDir, counterPath) || !isInsideDir(stateDir, stuckPath))
|
|
3341
3804
|
return false;
|
|
3342
3805
|
const previous = readCounter(counterPath);
|
|
@@ -3350,7 +3813,7 @@ function consumeResumeBudget(stateDir, goalId) {
|
|
|
3350
3813
|
return true;
|
|
3351
3814
|
}
|
|
3352
3815
|
function isInsideDir(dir, candidate) {
|
|
3353
|
-
return candidate.startsWith(
|
|
3816
|
+
return candidate.startsWith(resolve5(dir) + sep2);
|
|
3354
3817
|
}
|
|
3355
3818
|
function renderResumeDirective(plan, goal3, sessionId) {
|
|
3356
3819
|
const normalized = normalizeUlwLoopSessionId(sessionId);
|
|
@@ -3460,7 +3923,7 @@ function parseStopPayload(value) {
|
|
|
3460
3923
|
};
|
|
3461
3924
|
}
|
|
3462
3925
|
|
|
3463
|
-
//
|
|
3926
|
+
// components/ulw-loop/src/cli.ts
|
|
3464
3927
|
async function main() {
|
|
3465
3928
|
const argv = process.argv.slice(2);
|
|
3466
3929
|
const command = argv[0];
|