oh-my-opencode 5.0.0-beta.60 → 5.0.0-beta.63
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/THIRD-PARTY-NOTICES.md +8 -6
- package/dist/cli/doctor/checks/browser-provider.d.ts +2 -0
- package/dist/cli/index.js +465 -348
- package/dist/cli-node/index.js +465 -348
- package/dist/config/schema/agent-names.d.ts +0 -1
- package/dist/config/schema/browser-automation.d.ts +0 -2
- package/dist/config/schema/oh-my-opencode-config.d.ts +0 -1
- package/dist/hooks/comment-checker/__fixtures__/download-probe.fixture.d.ts +1 -0
- package/dist/index.js +61 -535
- package/dist/oh-my-opencode.schema.json +0 -1
- package/dist/skills/ast-grep/SKILL.md +12 -0
- package/dist/skills/debugging/references/tools/playwright-cli.md +79 -161
- package/dist/skills/frontend/SKILL.md +1 -1
- package/dist/skills/frontend/references/design/aside.md +1 -1
- package/dist/skills/frontend/references/design/clone-from-url.md +1 -1
- package/dist/skills/frontend/scripts/perfection/lighthouse-audit.py +2 -2
- package/dist/skills/review-work/SKILL.md +1 -1
- package/dist/skills/ultimate-browsing/ATTRIBUTION.md +1 -82
- package/dist/skills/ultimate-browsing/SKILL.md +17 -29
- package/dist/skills/ultimate-browsing/engine/AGENTS.md +2 -1
- package/dist/skills/ultimate-browsing/engine/templates/package.json +2 -2
- package/dist/skills/ultimate-browsing/engine/templates/playwright_mobile_chrome.js +5 -2
- package/dist/skills/ultimate-browsing/engine/templates/playwright_real_chrome.js +8 -6
- package/dist/skills/ultimate-browsing/engine/tests/test_playwright_stealth.py +86 -0
- package/dist/skills/ultimate-browsing/engine/tests/test_playwright_templates.py +4 -7
- package/dist/skills/ultimate-browsing/references/chrome-stealth.md +86 -100
- package/dist/skills/ultimate-browsing/references/insane-search/README.md +9 -3
- package/dist/skills/ultimate-browsing/references/insane-search/playwright.md +16 -13
- package/dist/skills/ultimate-browsing/scripts/extract_cookies.py +3 -3
- package/dist/skills/ulw-execute/SKILL.md +1 -1
- package/dist/skills/ulw-plan/scripts/scaffold-plan.mjs +1 -1
- package/dist/skills/ulw-research/SKILL.md +3 -3
- package/dist/skills/visual-qa/AGENTS.md +1 -1
- package/dist/skills/visual-qa/SKILL.md +1 -1
- package/dist/skills/visual-qa/references/browser-setup.md +75 -0
- package/dist/tui.js +12 -5
- package/package.json +16 -23
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +6887 -249
- 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/bootstrap/src/setup.ts +7 -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 +2 -2
- package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
- package/packages/omo-codex/plugin/components/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/plan.toml +2 -2
- package/packages/omo-codex/plugin/components/ultrawork/dist/cli.js +25 -15
- package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/src/directive-content.ts +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/directive.md +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/CHANGELOG.md +15 -0
- package/packages/omo-codex/plugin/components/ulw-loop/directive.md +25 -15
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.js +5 -4
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +640 -327
- package/packages/omo-codex/plugin/components/ulw-loop/dist/domain-types.d.ts +5 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/evidence.js +16 -12
- package/packages/omo-codex/plugin/components/ulw-loop/dist/ledger.d.ts +4 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/ledger.js +34 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-commit.d.ts +19 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-commit.js +107 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-crud.d.ts +2 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-crud.js +48 -22
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-io.d.ts +9 -7
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-io.js +105 -114
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-log.d.ts +15 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-log.js +86 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-missing-recovery.d.ts +5 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-missing-recovery.js +15 -3
- package/packages/omo-codex/plugin/components/ulw-loop/dist/review-blockers.js +3 -4
- package/packages/omo-codex/plugin/components/ulw-loop/dist/sdk/factory.js +29 -6
- package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-budget-io.d.ts +5 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-budget-io.js +67 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-guard.d.ts +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-guard.js +29 -66
- package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-role-guard.d.ts +2 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-role-guard.js +22 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/state-lock.d.ts +11 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/state-lock.js +120 -88
- package/packages/omo-codex/plugin/components/ulw-loop/dist/status-next-actions.d.ts +3 -2
- package/packages/omo-codex/plugin/components/ulw-loop/dist/status-next-actions.js +18 -10
- package/packages/omo-codex/plugin/components/ulw-loop/dist/steering-batch.js +4 -4
- package/packages/omo-codex/plugin/components/ulw-loop/dist/steering.js +5 -5
- package/packages/omo-codex/plugin/components/ulw-loop/dist/stop-resume-hook.js +8 -17
- package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +7 -7
- 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 +2 -2
- package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +5 -4
- package/packages/omo-codex/plugin/components/ulw-loop/src/domain-types.ts +5 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/evidence.ts +16 -16
- package/packages/omo-codex/plugin/components/ulw-loop/src/ledger.ts +33 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/plan-commit.ts +124 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/plan-crud.ts +58 -51
- package/packages/omo-codex/plugin/components/ulw-loop/src/plan-io.ts +126 -140
- package/packages/omo-codex/plugin/components/ulw-loop/src/plan-log.ts +84 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/plan-missing-recovery.ts +26 -3
- package/packages/omo-codex/plugin/components/ulw-loop/src/review-blockers.ts +3 -3
- package/packages/omo-codex/plugin/components/ulw-loop/src/sdk/factory.ts +55 -6
- package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-budget-io.ts +59 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-guard.ts +29 -62
- package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-role-guard.ts +22 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/state-lock.ts +129 -111
- package/packages/omo-codex/plugin/components/ulw-loop/src/status-next-actions.ts +19 -10
- package/packages/omo-codex/plugin/components/ulw-loop/src/steering-batch.ts +4 -4
- package/packages/omo-codex/plugin/components/ulw-loop/src/steering.ts +5 -4
- package/packages/omo-codex/plugin/components/ulw-loop/src/stop-resume-hook.ts +8 -16
- package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/barrier.ts +13 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-child.ts +65 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-cli-qa.mjs +76 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-crash.ts +23 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-mutations.mjs +58 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/commit-writer.ts +65 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/lease-clock.ts +23 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/plan-commit-log.test.ts +252 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/plan-commit-recovery.test.ts +190 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/plan-io.test.ts +50 -35
- package/packages/omo-codex/plugin/components/ulw-loop/test/plan-log-newest.test.ts +72 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/sdk-completed-plan.test.ts +43 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/sdk-driver-states.test.ts +145 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/sdk-session-context.test.ts +21 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/spawn-guard.test.ts +7 -1
- package/packages/omo-codex/plugin/components/ulw-loop/test/spawn-role-matrix.test.ts +113 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/state-lock.test.ts +92 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/status-next-actions-surface.test.ts +133 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/steering-batch.test.ts +12 -12
- package/packages/omo-codex/plugin/components/ulw-loop/test/steering.test.ts +4 -1
- 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-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/post-tool-use-recording-spawn-admission.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-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-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 +12 -12
- package/packages/omo-codex/plugin/package.json +1 -1
- package/packages/omo-codex/plugin/scripts/sync-skills.d.mts +4 -0
- package/packages/omo-codex/plugin/scripts/sync-skills.mjs +18 -10
- package/packages/omo-codex/plugin/skills/ast-grep/SKILL.md +12 -0
- package/packages/omo-codex/plugin/skills/debugging/references/tools/playwright-cli.md +79 -161
- package/packages/omo-codex/plugin/skills/frontend/SKILL.md +1 -1
- package/packages/omo-codex/plugin/skills/frontend/references/design/aside.md +1 -1
- package/packages/omo-codex/plugin/skills/frontend/references/design/clone-from-url.md +1 -1
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/lighthouse-audit.py +2 -2
- package/packages/omo-codex/plugin/skills/init-deep/SKILL.md +3 -3
- package/packages/omo-codex/plugin/skills/refactor/SKILL.md +3 -3
- package/packages/omo-codex/plugin/skills/remove-ai-slops/SKILL.md +3 -3
- package/packages/omo-codex/plugin/skills/review-work/SKILL.md +4 -4
- package/packages/omo-codex/plugin/skills/ultimate-browsing/ATTRIBUTION.md +1 -82
- package/packages/omo-codex/plugin/skills/ultimate-browsing/SKILL.md +17 -29
- package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/AGENTS.md +2 -1
- package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/templates/package.json +2 -2
- package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/templates/playwright_mobile_chrome.js +5 -2
- package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/templates/playwright_real_chrome.js +8 -6
- package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/tests/test_playwright_stealth.py +86 -0
- package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/tests/test_playwright_templates.py +4 -7
- package/packages/omo-codex/plugin/skills/ultimate-browsing/references/chrome-stealth.md +86 -100
- package/packages/omo-codex/plugin/skills/ultimate-browsing/references/insane-search/README.md +9 -3
- package/packages/omo-codex/plugin/skills/ultimate-browsing/references/insane-search/playwright.md +16 -13
- package/packages/omo-codex/plugin/skills/ultimate-browsing/scripts/extract_cookies.py +3 -3
- package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +25 -15
- package/packages/omo-codex/plugin/skills/ulw-execute/SKILL.md +4 -4
- package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +3 -3
- package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +2 -2
- package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +6 -6
- package/packages/omo-codex/plugin/skills/visual-qa/AGENTS.md +1 -1
- package/packages/omo-codex/plugin/skills/visual-qa/SKILL.md +4 -4
- package/packages/omo-codex/plugin/skills/visual-qa/references/browser-setup.md +75 -0
- package/packages/omo-codex/plugin/test/bootstrap-default-role.test.mjs +66 -0
- package/packages/omo-codex/plugin/test/component-hook-contract-cases.mjs +13 -1
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +7060 -417
- package/packages/prompts-core/prompts/ultrawork/codex.md +25 -15
- package/packages/shared-skills/skills/ast-grep/SKILL.md +12 -0
- package/packages/shared-skills/skills/debugging/references/tools/playwright-cli.md +79 -161
- package/packages/shared-skills/skills/frontend/SKILL.md +1 -1
- package/packages/shared-skills/skills/frontend/references/design/aside.md +1 -1
- package/packages/shared-skills/skills/frontend/references/design/clone-from-url.md +1 -1
- package/packages/shared-skills/skills/frontend/scripts/perfection/lighthouse-audit.py +2 -2
- package/packages/shared-skills/skills/review-work/SKILL.md +1 -1
- package/packages/shared-skills/skills/ultimate-browsing/ATTRIBUTION.md +1 -82
- package/packages/shared-skills/skills/ultimate-browsing/SKILL.md +17 -29
- package/packages/shared-skills/skills/ultimate-browsing/engine/AGENTS.md +2 -1
- package/packages/shared-skills/skills/ultimate-browsing/engine/templates/package.json +2 -2
- package/packages/shared-skills/skills/ultimate-browsing/engine/templates/playwright_mobile_chrome.js +5 -2
- package/packages/shared-skills/skills/ultimate-browsing/engine/templates/playwright_real_chrome.js +8 -6
- package/packages/shared-skills/skills/ultimate-browsing/engine/tests/test_playwright_stealth.py +86 -0
- package/packages/shared-skills/skills/ultimate-browsing/engine/tests/test_playwright_templates.py +4 -7
- package/packages/shared-skills/skills/ultimate-browsing/references/chrome-stealth.md +86 -100
- package/packages/shared-skills/skills/ultimate-browsing/references/insane-search/README.md +9 -3
- package/packages/shared-skills/skills/ultimate-browsing/references/insane-search/playwright.md +16 -13
- package/packages/shared-skills/skills/ultimate-browsing/scripts/extract_cookies.py +3 -3
- package/packages/shared-skills/skills/ulw-execute/SKILL.md +1 -1
- package/packages/shared-skills/skills/ulw-plan/scripts/scaffold-plan.mjs +1 -1
- package/packages/shared-skills/skills/ulw-research/SKILL.md +3 -3
- package/packages/shared-skills/skills/visual-qa/AGENTS.md +1 -1
- package/packages/shared-skills/skills/visual-qa/SKILL.md +1 -1
- package/packages/shared-skills/skills/visual-qa/references/browser-setup.md +75 -0
- package/dist/skills/visual-qa/references/agent-browser-setup.md +0 -45
- package/packages/omo-codex/plugin/skills/visual-qa/references/agent-browser-setup.md +0 -45
- package/packages/shared-skills/skills/visual-qa/references/agent-browser-setup.md +0 -45
|
@@ -1,15 +1,22 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
4
3
|
import { parsePreToolUsePayload } from "./codex-hook.js";
|
|
5
4
|
import { isFinalRunCompletionCandidate } from "./goal-status.js";
|
|
6
5
|
import { ulwLoopAttemptEvidenceDir, ulwLoopDir, ulwLoopStateLockPath } from "./paths.js";
|
|
6
|
+
import { readUlwLoopPlanSync } from "./plan-io.js";
|
|
7
|
+
import { atomicWriteJson, isNonEmptyFile, readAdmissionBreaker, readCount, readCounts } from "./spawn-budget-io.js";
|
|
8
|
+
import { spawnRoleDenial } from "./spawn-role-guard.js";
|
|
7
9
|
import { isStateLockTimeout, withStateLockSync } from "./state-lock.js";
|
|
8
10
|
import { GATE_REVIEWER_AGENT_NAMES, REVIEWER_ROLES_BY_SURFACE, resolveToolkitSurface, reviewerRolesFor, } from "./surface.js";
|
|
9
11
|
// spawn_agent = v1; collaborationspawn_agent = the delimiter-free flattened v2
|
|
10
12
|
// hook token from codex-rs hook_names.rs; collaboration.spawn_agent = the
|
|
11
13
|
// dotted token observed live in the task-1 probe (hook-tool-tokens.txt).
|
|
12
|
-
const SPAWN_TOOL_TOKENS = new Set([
|
|
14
|
+
const SPAWN_TOOL_TOKENS = new Set([
|
|
15
|
+
"spawn_agent",
|
|
16
|
+
"multi_agent_v1.spawn_agent",
|
|
17
|
+
"collaborationspawn_agent",
|
|
18
|
+
"collaboration.spawn_agent",
|
|
19
|
+
]);
|
|
13
20
|
export const DEFAULT_FANOUT_LIMIT = 24;
|
|
14
21
|
const DEFAULT_REVIEW_SPAWN_LIMIT = 3;
|
|
15
22
|
const GATE_MESSAGE_PATTERN = /lazycodex-gate-reviewer|omo-senpi-gate-reviewer|final gate review/i;
|
|
@@ -20,6 +27,16 @@ const REVIEW_AGENT_TYPES = [
|
|
|
20
27
|
];
|
|
21
28
|
const REVIEW_AGENT_TYPE_SET = new Set(REVIEW_AGENT_TYPES);
|
|
22
29
|
export function applySpawnGuards(payload, options = {}) {
|
|
30
|
+
if (payload.hook_event_name !== "PreToolUse" || !SPAWN_TOOL_TOKENS.has(payload.tool_name))
|
|
31
|
+
return "";
|
|
32
|
+
if (resolveToolkitSurface() === "lazycodex") {
|
|
33
|
+
const reason = spawnRoleDenial(payload.tool_input);
|
|
34
|
+
if (reason !== null)
|
|
35
|
+
return deny(reason);
|
|
36
|
+
}
|
|
37
|
+
return applySpawnBudgetGuards(payload, options);
|
|
38
|
+
}
|
|
39
|
+
export function applySpawnBudgetGuards(payload, options = {}) {
|
|
23
40
|
if (payload.hook_event_name !== "PreToolUse" || !SPAWN_TOOL_TOKENS.has(payload.tool_name))
|
|
24
41
|
return "";
|
|
25
42
|
const breaker = readAdmissionBreaker(payload.session_id);
|
|
@@ -27,7 +44,7 @@ export function applySpawnGuards(payload, options = {}) {
|
|
|
27
44
|
return deny(`Subagent admission failed earlier in this session (${breaker}). Do not spawn more workers or reviewers; report the capacity block and wait for the user.`);
|
|
28
45
|
const scope = { sessionId: payload.session_id };
|
|
29
46
|
const stateDir = ulwLoopDir(payload.cwd, scope);
|
|
30
|
-
const plan = readPlan(
|
|
47
|
+
const plan = readPlan(payload.cwd, payload.session_id);
|
|
31
48
|
if (plan === null)
|
|
32
49
|
return "";
|
|
33
50
|
const lockOptions = options.lockTimeoutMs === undefined ? {} : { timeoutMs: options.lockTimeoutMs };
|
|
@@ -93,15 +110,16 @@ export async function runSpawnGuardCli(stdin, stdout) {
|
|
|
93
110
|
for await (const chunk of stdin)
|
|
94
111
|
chunks.push(Buffer.from(chunk));
|
|
95
112
|
const payload = parsePreToolUsePayload(Buffer.concat(chunks).toString("utf8"));
|
|
96
|
-
if (payload === null)
|
|
113
|
+
if (payload === null) {
|
|
114
|
+
stdout.write(deny("LazyCodex spawn guard received an invalid hook payload; role routing was not verified."));
|
|
97
115
|
return;
|
|
116
|
+
}
|
|
98
117
|
const output = applySpawnGuards(payload);
|
|
99
118
|
if (output.length > 0)
|
|
100
119
|
stdout.write(output);
|
|
101
120
|
}
|
|
102
121
|
catch (error) {
|
|
103
|
-
|
|
104
|
-
return;
|
|
122
|
+
stdout.write(deny(`LazyCodex spawn guard failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
105
123
|
}
|
|
106
124
|
}
|
|
107
125
|
// Read-only fan-out eligibility check. Returns a denial reason when the next
|
|
@@ -116,6 +134,7 @@ function peekFanOutBudget(stateDir) {
|
|
|
116
134
|
return null;
|
|
117
135
|
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.`;
|
|
118
136
|
}
|
|
137
|
+
// Hook budget counters are exempt from plan/audit commits.
|
|
119
138
|
// Per-session spawn counter; depth/lineage tracking is descoped — this is a
|
|
120
139
|
// total-volume backstop against fan-out explosions, not a recursion tracker.
|
|
121
140
|
function consumeFanOutBudget(stateDir) {
|
|
@@ -222,11 +241,6 @@ function activeSurfaceReviewerAlias(reviewer) {
|
|
|
222
241
|
}
|
|
223
242
|
return reviewer;
|
|
224
243
|
}
|
|
225
|
-
function atomicWriteJson(targetPath, data) {
|
|
226
|
-
const tmp = join(dirname(targetPath), `.tmp-${randomBytes(6).toString("hex")}`);
|
|
227
|
-
writeFileSync(tmp, JSON.stringify(data));
|
|
228
|
-
renameSync(tmp, targetPath);
|
|
229
|
-
}
|
|
230
244
|
function deny(reason) {
|
|
231
245
|
return `${JSON.stringify({
|
|
232
246
|
hookSpecificOutput: {
|
|
@@ -237,18 +251,6 @@ function deny(reason) {
|
|
|
237
251
|
},
|
|
238
252
|
})}\n`;
|
|
239
253
|
}
|
|
240
|
-
function readAdmissionBreaker(sessionId) {
|
|
241
|
-
const dataDir = process.env["PLUGIN_DATA"];
|
|
242
|
-
if (typeof dataDir !== "string")
|
|
243
|
-
return null;
|
|
244
|
-
try {
|
|
245
|
-
const value = JSON.parse(readFileSync(join(dataDir, "spawn-breaker", `${sessionId}.json`), "utf8"));
|
|
246
|
-
return typeof value.reason === "string" ? value.reason : "capacity limit";
|
|
247
|
-
}
|
|
248
|
-
catch {
|
|
249
|
-
return null;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
254
|
function fanOutLimit() {
|
|
253
255
|
const raw = process.env["OMO_SPAWN_FANOUT_LIMIT"];
|
|
254
256
|
if (raw === undefined)
|
|
@@ -263,48 +265,9 @@ function reviewSpawnLimit() {
|
|
|
263
265
|
const parsed = Number.parseInt(raw, 10);
|
|
264
266
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_REVIEW_SPAWN_LIMIT;
|
|
265
267
|
}
|
|
266
|
-
function
|
|
267
|
-
try {
|
|
268
|
-
return existsSync(path) && statSync(path).size > 0;
|
|
269
|
-
}
|
|
270
|
-
catch (error) {
|
|
271
|
-
if (error instanceof Error)
|
|
272
|
-
return false;
|
|
273
|
-
throw error;
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
function readCount(counterPath) {
|
|
277
|
-
try {
|
|
278
|
-
const parsed = JSON.parse(readFileSync(counterPath, "utf8"));
|
|
279
|
-
return typeof parsed["count"] === "number" && parsed["count"] >= 0 ? parsed["count"] : 0;
|
|
280
|
-
}
|
|
281
|
-
catch (error) {
|
|
282
|
-
if (error instanceof Error)
|
|
283
|
-
return 0;
|
|
284
|
-
throw error;
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
function readCounts(counterPath) {
|
|
288
|
-
try {
|
|
289
|
-
const parsed = JSON.parse(readFileSync(counterPath, "utf8"));
|
|
290
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
291
|
-
return {};
|
|
292
|
-
const counts = {};
|
|
293
|
-
for (const [key, value] of Object.entries(parsed)) {
|
|
294
|
-
if (typeof value === "number" && value >= 0)
|
|
295
|
-
counts[key] = value;
|
|
296
|
-
}
|
|
297
|
-
return counts;
|
|
298
|
-
}
|
|
299
|
-
catch (error) {
|
|
300
|
-
if (error instanceof Error)
|
|
301
|
-
return {};
|
|
302
|
-
throw error;
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
function readPlan(goalsPath) {
|
|
268
|
+
function readPlan(repoRoot, sessionId) {
|
|
306
269
|
try {
|
|
307
|
-
return
|
|
270
|
+
return readUlwLoopPlanSync(repoRoot, { sessionId });
|
|
308
271
|
}
|
|
309
272
|
catch (error) {
|
|
310
273
|
if (error instanceof Error)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Explicit selectors, pinned by a test to the bundled role TOMLs. The generated
|
|
2
|
+
// default is a non-fork fallback, not a selector: a user can opt out of owning it.
|
|
3
|
+
export const LAZYCODEX_SPAWN_ROLES = new Set([
|
|
4
|
+
"explorer",
|
|
5
|
+
"lazycodex-clone-fidelity-reviewer",
|
|
6
|
+
"lazycodex-code-reviewer",
|
|
7
|
+
"lazycodex-gate-reviewer",
|
|
8
|
+
"lazycodex-qa-executor",
|
|
9
|
+
"lazycodex-worker-high",
|
|
10
|
+
"lazycodex-worker-low",
|
|
11
|
+
"lazycodex-worker-medium",
|
|
12
|
+
"librarian",
|
|
13
|
+
"metis",
|
|
14
|
+
"momus",
|
|
15
|
+
"plan",
|
|
16
|
+
]);
|
|
17
|
+
export function spawnRoleDenial(input) {
|
|
18
|
+
const role = typeof input === "object" && input !== null && "agent_type" in input ? input.agent_type : undefined;
|
|
19
|
+
if (typeof role === "string" && LAZYCODEX_SPAWN_ROLES.has(role))
|
|
20
|
+
return null;
|
|
21
|
+
return `LazyCodex requires an explicit registered agent_type: ${[...LAZYCODEX_SPAWN_ROLES].join(", ")}. Received ${JSON.stringify(role) ?? "no agent_type"}. Use the matching role and fork_turns: "none" (V2) or fork_context: false (V1), unless full history is deliberately required. The hook cannot see the tool schema; if agent_type is unavailable, stop and report incompatible role routing rather than spawning a generic agent. Describing a role in message does not select its TOML.`;
|
|
22
|
+
}
|
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
import { UlwLoopError } from "./types.js";
|
|
2
2
|
export declare const ULW_LOOP_LOCK_TIMEOUT_CODE = "ULW_LOOP_LOCK_TIMEOUT";
|
|
3
|
+
export interface StateLockClock {
|
|
4
|
+
readonly now: () => number;
|
|
5
|
+
readonly schedule: (fn: () => void, ms: number) => {
|
|
6
|
+
unref(): void;
|
|
7
|
+
cancel(): void;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
3
10
|
export interface StateLockOptions {
|
|
4
11
|
readonly timeoutMs?: number;
|
|
5
12
|
readonly staleMs?: number;
|
|
13
|
+
readonly leaseMs?: number;
|
|
14
|
+
readonly heartbeatMs?: number;
|
|
15
|
+
readonly clock?: StateLockClock;
|
|
6
16
|
}
|
|
7
|
-
export declare function withStateLock<T>(lockPath: string, fn: () => Promise<T>, options?: StateLockOptions): Promise<T>;
|
|
17
|
+
export declare function withStateLock<T>(lockPath: string, fn: (token: string) => Promise<T>, options?: StateLockOptions): Promise<T>;
|
|
8
18
|
export declare function withStateLockSync<T>(lockPath: string, fn: () => T, options?: StateLockOptions): T;
|
|
9
19
|
export declare function isStateLockTimeout(error: unknown): error is UlwLoopError;
|
|
@@ -1,89 +1,121 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { closeSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, writeSync } from "node:fs";
|
|
2
|
+
import { closeSync, ftruncateSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, writeSync } from "node:fs";
|
|
3
3
|
import { dirname } from "node:path";
|
|
4
|
-
import { setTimeout as sleep } from "node:timers/promises";
|
|
5
4
|
import { UlwLoopError } from "./types.js";
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// owner (pid + a per-acquisition token). A waiter reclaims it only when the
|
|
13
|
-
// owner is provably gone (pid dead) or the body never became a record within
|
|
14
|
-
// `staleMs` (a creator that died mid-write); a live owner is never reclaimed by
|
|
15
|
-
// age alone, because overlapping two bodies is exactly the lost update this lock
|
|
16
|
-
// exists to prevent. Waiters back off and fail closed at `timeoutMs`. Release
|
|
17
|
-
// unlinks only a lock that still carries the releaser's own token.
|
|
5
|
+
// O_EXCL establishes ownership. Async holders keep the fd open and refresh a
|
|
6
|
+
// lease through that fd, never through the pathname: a reclaimed inode is invisible.
|
|
7
|
+
// Sync/legacy holders are lease-less and only dead pids retire their records.
|
|
8
|
+
// Expiry permits overlapping bodies; immutable revision publication fences them.
|
|
9
|
+
// Reclaim's reread/unlink is not atomic. On Windows an open inode may not unlink:
|
|
10
|
+
// treat that refusal as a live owner and fail closed. Release checks its token.
|
|
18
11
|
export const ULW_LOOP_LOCK_TIMEOUT_CODE = "ULW_LOOP_LOCK_TIMEOUT";
|
|
19
12
|
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
20
13
|
const DEFAULT_STALE_MS = 60_000;
|
|
21
|
-
const
|
|
22
|
-
const MAX_DELAY_MS = 100;
|
|
14
|
+
const DEFAULT_LEASE_MS = 30_000;
|
|
23
15
|
const SLEEP_CELL = new Int32Array(new SharedArrayBuffer(4));
|
|
16
|
+
const systemClock = {
|
|
17
|
+
now: Date.now,
|
|
18
|
+
schedule(fn, ms) {
|
|
19
|
+
const timer = setTimeout(fn, ms);
|
|
20
|
+
return {
|
|
21
|
+
unref: () => {
|
|
22
|
+
timer.unref();
|
|
23
|
+
},
|
|
24
|
+
cancel: () => clearTimeout(timer),
|
|
25
|
+
};
|
|
26
|
+
},
|
|
27
|
+
};
|
|
24
28
|
export async function withStateLock(lockPath, fn, options = {}) {
|
|
25
|
-
const
|
|
29
|
+
const holder = await acquireAsync(lockPath, options);
|
|
30
|
+
const clock = options.clock ?? systemClock;
|
|
31
|
+
let timer;
|
|
32
|
+
let heartbeatError;
|
|
33
|
+
const beat = () => {
|
|
34
|
+
try {
|
|
35
|
+
holder.record.leaseUntil = clock.now() + (options.leaseMs ?? DEFAULT_LEASE_MS);
|
|
36
|
+
writeRecord(holder);
|
|
37
|
+
schedule();
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
heartbeatError = error;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
const schedule = () => {
|
|
44
|
+
if (options.heartbeatMs === 0)
|
|
45
|
+
return;
|
|
46
|
+
timer = clock.schedule(beat, options.heartbeatMs ?? (options.leaseMs ?? DEFAULT_LEASE_MS) / 3);
|
|
47
|
+
timer.unref();
|
|
48
|
+
};
|
|
49
|
+
schedule();
|
|
26
50
|
try {
|
|
27
|
-
|
|
51
|
+
const result = await fn(holder.record.token);
|
|
52
|
+
if (heartbeatError !== undefined)
|
|
53
|
+
throw heartbeatError;
|
|
54
|
+
return result;
|
|
28
55
|
}
|
|
29
56
|
finally {
|
|
30
|
-
|
|
57
|
+
timer?.cancel();
|
|
58
|
+
closeSync(holder.fd);
|
|
59
|
+
release(lockPath, holder.record.token);
|
|
31
60
|
}
|
|
32
61
|
}
|
|
33
62
|
export function withStateLockSync(lockPath, fn, options = {}) {
|
|
34
|
-
const
|
|
63
|
+
const holder = acquireSync(lockPath, options);
|
|
35
64
|
try {
|
|
36
65
|
return fn();
|
|
37
66
|
}
|
|
38
67
|
finally {
|
|
39
|
-
|
|
68
|
+
closeSync(holder.fd);
|
|
69
|
+
release(lockPath, holder.record.token);
|
|
40
70
|
}
|
|
41
71
|
}
|
|
42
72
|
export function isStateLockTimeout(error) {
|
|
43
73
|
return error instanceof UlwLoopError && error.code === ULW_LOOP_LOCK_TIMEOUT_CODE;
|
|
44
74
|
}
|
|
45
75
|
async function acquireAsync(lockPath, options) {
|
|
46
|
-
const
|
|
47
|
-
const
|
|
76
|
+
const clock = options.clock ?? systemClock;
|
|
77
|
+
const deadline = clock.now() + (options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
48
78
|
mkdirSync(dirname(lockPath), { recursive: true });
|
|
49
79
|
for (let attempt = 0;;) {
|
|
50
|
-
const outcome = attemptOnce(lockPath,
|
|
80
|
+
const outcome = attemptOnce(lockPath, options, clock.now(), options.leaseMs ?? DEFAULT_LEASE_MS);
|
|
51
81
|
if (outcome.kind === "acquired")
|
|
52
|
-
return outcome.
|
|
82
|
+
return outcome.holder;
|
|
53
83
|
if (outcome.kind === "retry")
|
|
54
84
|
continue;
|
|
55
|
-
if (
|
|
56
|
-
throw lockTimeout(lockPath, options
|
|
57
|
-
await
|
|
85
|
+
if (clock.now() >= deadline)
|
|
86
|
+
throw lockTimeout(lockPath, options);
|
|
87
|
+
await new Promise((resolve) => clock.schedule(resolve, backoffMs(attempt)));
|
|
58
88
|
attempt += 1;
|
|
59
89
|
}
|
|
60
90
|
}
|
|
61
91
|
function acquireSync(lockPath, options) {
|
|
62
92
|
const deadline = Date.now() + (options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
63
|
-
const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
|
|
64
93
|
mkdirSync(dirname(lockPath), { recursive: true });
|
|
65
94
|
for (let attempt = 0;;) {
|
|
66
|
-
const outcome = attemptOnce(lockPath,
|
|
95
|
+
const outcome = attemptOnce(lockPath, options, Date.now());
|
|
67
96
|
if (outcome.kind === "acquired")
|
|
68
|
-
return outcome.
|
|
97
|
+
return outcome.holder;
|
|
69
98
|
if (outcome.kind === "retry")
|
|
70
99
|
continue;
|
|
71
100
|
if (Date.now() >= deadline)
|
|
72
|
-
throw lockTimeout(lockPath, options
|
|
101
|
+
throw lockTimeout(lockPath, options);
|
|
73
102
|
Atomics.wait(SLEEP_CELL, 0, 0, backoffMs(attempt));
|
|
74
103
|
attempt += 1;
|
|
75
104
|
}
|
|
76
105
|
}
|
|
77
|
-
|
|
78
|
-
function attemptOnce(lockPath, staleMs) {
|
|
106
|
+
function attemptOnce(lockPath, options, now, leaseMs) {
|
|
79
107
|
try {
|
|
80
|
-
const
|
|
81
|
-
if (
|
|
82
|
-
return { kind: "acquired",
|
|
83
|
-
const snapshot = readSnapshot(lockPath);
|
|
108
|
+
const holder = tryCreate(lockPath, now, leaseMs);
|
|
109
|
+
if (holder !== null)
|
|
110
|
+
return { kind: "acquired", holder };
|
|
111
|
+
const snapshot = readSnapshot(lockPath, now);
|
|
84
112
|
if (snapshot === null)
|
|
85
113
|
return { kind: "retry" };
|
|
86
|
-
|
|
114
|
+
const record = snapshot.record;
|
|
115
|
+
const stale = record === null
|
|
116
|
+
? snapshot.ageMs > (options.staleMs ?? DEFAULT_STALE_MS)
|
|
117
|
+
: !isProcessAlive(record.pid) || (record.leaseUntil !== undefined && now > record.leaseUntil);
|
|
118
|
+
if (stale && reclaim(lockPath, snapshot.raw))
|
|
87
119
|
return { kind: "retry" };
|
|
88
120
|
return { kind: "wait" };
|
|
89
121
|
}
|
|
@@ -93,7 +125,18 @@ function attemptOnce(lockPath, staleMs) {
|
|
|
93
125
|
throw error;
|
|
94
126
|
}
|
|
95
127
|
}
|
|
96
|
-
function
|
|
128
|
+
function writeRecord(holder) {
|
|
129
|
+
const buf = Buffer.from(JSON.stringify(holder.record));
|
|
130
|
+
ftruncateSync(holder.fd, 0);
|
|
131
|
+
let offset = 0;
|
|
132
|
+
while (offset < buf.length) {
|
|
133
|
+
const written = writeSync(holder.fd, buf, offset, buf.length - offset, offset);
|
|
134
|
+
if (written === 0)
|
|
135
|
+
throw new Error("State lock write made no progress.");
|
|
136
|
+
offset += written;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function tryCreate(lockPath, now, leaseMs) {
|
|
97
140
|
let fd;
|
|
98
141
|
try {
|
|
99
142
|
fd = openSync(lockPath, "wx");
|
|
@@ -103,30 +146,33 @@ function tryCreate(lockPath) {
|
|
|
103
146
|
return null;
|
|
104
147
|
throw error;
|
|
105
148
|
}
|
|
106
|
-
const record = {
|
|
149
|
+
const record = {
|
|
150
|
+
pid: process.pid,
|
|
151
|
+
createdAt: new Date(now).toISOString(),
|
|
152
|
+
token: randomUUID(),
|
|
153
|
+
...(leaseMs === undefined ? {} : { leaseUntil: now + leaseMs }),
|
|
154
|
+
};
|
|
155
|
+
const holder = { fd, record };
|
|
107
156
|
try {
|
|
108
|
-
|
|
157
|
+
writeRecord(holder);
|
|
109
158
|
}
|
|
110
159
|
catch (error) {
|
|
111
160
|
closeSync(fd);
|
|
112
|
-
// A failed write (e.g. EINTR) must not leave a partial lock the owner never recorded;
|
|
113
|
-
// otherwise later readers see a young ownerless lock and only age can retire it.
|
|
114
161
|
try {
|
|
115
162
|
unlinkSync(lockPath);
|
|
116
163
|
}
|
|
117
|
-
catch {
|
|
118
|
-
|
|
164
|
+
catch (cleanup) {
|
|
165
|
+
if (!hasCode(cleanup, "ENOENT"))
|
|
166
|
+
throw cleanup;
|
|
119
167
|
}
|
|
120
168
|
throw error;
|
|
121
169
|
}
|
|
122
|
-
|
|
123
|
-
return record.token;
|
|
170
|
+
return holder;
|
|
124
171
|
}
|
|
125
|
-
function readSnapshot(lockPath) {
|
|
172
|
+
function readSnapshot(lockPath, now) {
|
|
126
173
|
try {
|
|
127
174
|
const raw = readFileSync(lockPath, "utf8");
|
|
128
|
-
|
|
129
|
-
return { raw, record: parseRecord(raw), ageMs };
|
|
175
|
+
return { raw, record: parseRecord(raw), ageMs: now - statSync(lockPath).mtimeMs };
|
|
130
176
|
}
|
|
131
177
|
catch (error) {
|
|
132
178
|
if (hasCode(error, "ENOENT"))
|
|
@@ -136,18 +182,21 @@ function readSnapshot(lockPath) {
|
|
|
136
182
|
}
|
|
137
183
|
function parseRecord(raw) {
|
|
138
184
|
try {
|
|
139
|
-
const
|
|
140
|
-
if (typeof
|
|
185
|
+
const record = JSON.parse(raw);
|
|
186
|
+
if (typeof record !== "object" || record === null)
|
|
187
|
+
return null;
|
|
188
|
+
if (!("pid" in record) || typeof record.pid !== "number" || !Number.isInteger(record.pid) || record.pid <= 0)
|
|
141
189
|
return null;
|
|
142
|
-
|
|
143
|
-
const pid = record["pid"];
|
|
144
|
-
const createdAt = record["createdAt"];
|
|
145
|
-
const token = record["token"];
|
|
146
|
-
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0 || typeof createdAt !== "string")
|
|
190
|
+
if (!("createdAt" in record) || typeof record.createdAt !== "string")
|
|
147
191
|
return null;
|
|
148
|
-
if (typeof token !== "string" || token.length === 0)
|
|
192
|
+
if (!("token" in record) || typeof record.token !== "string" || record.token.length === 0)
|
|
149
193
|
return null;
|
|
150
|
-
return {
|
|
194
|
+
return {
|
|
195
|
+
pid: record.pid,
|
|
196
|
+
createdAt: record.createdAt,
|
|
197
|
+
token: record.token,
|
|
198
|
+
...("leaseUntil" in record && typeof record.leaseUntil === "number" ? { leaseUntil: record.leaseUntil } : {}),
|
|
199
|
+
};
|
|
151
200
|
}
|
|
152
201
|
catch (error) {
|
|
153
202
|
if (error instanceof SyntaxError)
|
|
@@ -155,14 +204,6 @@ function parseRecord(raw) {
|
|
|
155
204
|
throw error;
|
|
156
205
|
}
|
|
157
206
|
}
|
|
158
|
-
// A body that is not a record is a lock mid-write (or a foreign/legacy file); only
|
|
159
|
-
// age retires it. A parsable record is stale only when its owner is dead: a live
|
|
160
|
-
// owner running past staleMs is slow, not gone, and the waiter fails closed instead.
|
|
161
|
-
function isStale(snapshot, staleMs) {
|
|
162
|
-
if (snapshot.record === null)
|
|
163
|
-
return snapshot.ageMs > staleMs;
|
|
164
|
-
return !isProcessAlive(snapshot.record.pid);
|
|
165
|
-
}
|
|
166
207
|
function isProcessAlive(pid) {
|
|
167
208
|
try {
|
|
168
209
|
process.kill(pid, 0);
|
|
@@ -176,10 +217,8 @@ function isProcessAlive(pid) {
|
|
|
176
217
|
throw error;
|
|
177
218
|
}
|
|
178
219
|
}
|
|
179
|
-
// Re-read right before unlinking so a lock that changed hands since the stale
|
|
180
|
-
// verdict (a sibling waiter reclaimed and re-acquired it) is left alone.
|
|
181
220
|
function reclaim(lockPath, expectedRaw) {
|
|
182
|
-
const current = readSnapshot(lockPath);
|
|
221
|
+
const current = readSnapshot(lockPath, Date.now());
|
|
183
222
|
if (current === null)
|
|
184
223
|
return true;
|
|
185
224
|
if (current.raw !== expectedRaw)
|
|
@@ -188,16 +227,15 @@ function reclaim(lockPath, expectedRaw) {
|
|
|
188
227
|
unlinkSync(lockPath);
|
|
189
228
|
}
|
|
190
229
|
catch (error) {
|
|
230
|
+
if (hasCode(error, "EPERM") || hasCode(error, "EACCES"))
|
|
231
|
+
return false;
|
|
191
232
|
if (!hasCode(error, "ENOENT"))
|
|
192
233
|
throw error;
|
|
193
234
|
}
|
|
194
235
|
return true;
|
|
195
236
|
}
|
|
196
|
-
// Only the acquisition that wrote this token may unlink: if the file now carries
|
|
197
|
-
// another token, a waiter has legitimately taken over and its lock must stand.
|
|
198
237
|
function release(lockPath, token) {
|
|
199
|
-
|
|
200
|
-
if (current === null || current.record?.token !== token)
|
|
238
|
+
if (readSnapshot(lockPath, Date.now())?.record?.token !== token)
|
|
201
239
|
return;
|
|
202
240
|
try {
|
|
203
241
|
unlinkSync(lockPath);
|
|
@@ -208,19 +246,13 @@ function release(lockPath, token) {
|
|
|
208
246
|
}
|
|
209
247
|
}
|
|
210
248
|
function backoffMs(attempt) {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const
|
|
216
|
-
const owner = holder
|
|
217
|
-
return new UlwLoopError(`ulw-loop state lock ${lockPath} is held by ${owner} for more than ${timeoutMs}ms
|
|
218
|
-
details: {
|
|
219
|
-
lockPath,
|
|
220
|
-
timeoutMs,
|
|
221
|
-
...(holder === undefined || holder === null ? {} : { holderPid: holder.pid }),
|
|
222
|
-
},
|
|
223
|
-
});
|
|
249
|
+
return Math.min(100, 5 * 2 ** attempt) + Math.random() * 5;
|
|
250
|
+
}
|
|
251
|
+
function lockTimeout(lockPath, options) {
|
|
252
|
+
const holder = readSnapshot(lockPath, Date.now())?.record;
|
|
253
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
254
|
+
const owner = holder == null ? "another process" : `pid ${holder.pid}`;
|
|
255
|
+
return new UlwLoopError(`ulw-loop state lock ${lockPath} is held by ${owner} for more than ${timeoutMs}ms. The lock owner is still alive. If a JS eval kernel was interrupted while writing, wait for its lease to expire (${options.leaseMs ?? DEFAULT_LEASE_MS} ms) or restart the owning senpi process; never delete a lock owned by a live process.`, ULW_LOOP_LOCK_TIMEOUT_CODE, { details: { lockPath, timeoutMs, ...(holder == null ? {} : { holderPid: holder.pid }) } });
|
|
224
256
|
}
|
|
225
257
|
function hasCode(error, code) {
|
|
226
258
|
return error instanceof Error && "code" in error && error.code === code;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import type { UlwLoopToolkitSurface } from "./surface.js";
|
|
1
2
|
import type { UlwLoopPlan } from "./types.js";
|
|
2
3
|
/**
|
|
3
|
-
*
|
|
4
|
+
* Status is the one call every agent already makes between steps, so it is
|
|
4
5
|
* also the cheapest place to answer "what now?" without a second round trip.
|
|
5
6
|
*/
|
|
6
|
-
export declare function statusNextActions(plan: UlwLoopPlan): readonly string[];
|
|
7
|
+
export declare function statusNextActions(plan: UlwLoopPlan, surface?: UlwLoopToolkitSurface): readonly string[];
|
|
@@ -1,33 +1,41 @@
|
|
|
1
1
|
import { hasAllCriteriaPass, isFinalRunCompletionCandidate } from "./goal-status.js";
|
|
2
|
-
import {
|
|
2
|
+
import { createGoalsAction } from "./plan-missing-recovery.js";
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* Status is the one call every agent already makes between steps, so it is
|
|
5
5
|
* also the cheapest place to answer "what now?" without a second round trip.
|
|
6
6
|
*/
|
|
7
|
-
export function statusNextActions(plan) {
|
|
7
|
+
export function statusNextActions(plan, surface = "lazycodex") {
|
|
8
8
|
const actions = [];
|
|
9
9
|
const active = plan.goals.find((goal) => goal.id === plan.activeGoalId);
|
|
10
10
|
if (plan.goals.length === 0)
|
|
11
|
-
actions.push(`No goals yet: bootstrap with \`${
|
|
11
|
+
actions.push(`No goals yet: bootstrap with \`${createGoalsAction(surface)}\`.`);
|
|
12
12
|
else if (active !== undefined)
|
|
13
|
-
actions.push(...activeGoalActions(plan, active));
|
|
13
|
+
actions.push(...activeGoalActions(plan, active, surface));
|
|
14
14
|
for (const goal of plan.goals.filter((candidate) => candidate.status === "review_blocked"))
|
|
15
|
-
actions.push(
|
|
15
|
+
actions.push(surface === "omo-senpi"
|
|
16
|
+
? `${goal.id} is review_blocked: capture the reviewer verdict with \`agentToolkit.recordReviewBlockers({ goalId: "${goal.id}", title: "<title>", objective: "<objective>", evidence: "<verdict>" })\`.`
|
|
17
|
+
: `${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>'\`.`);
|
|
16
18
|
if (plan.evidenceLayoutVersion !== 2 || active === undefined)
|
|
17
19
|
actions.push("plan is evidence-layout v1; artifacts go under .omo/evidence/");
|
|
18
20
|
return actions;
|
|
19
21
|
}
|
|
20
|
-
function activeGoalActions(plan, active) {
|
|
22
|
+
function activeGoalActions(plan, active, surface) {
|
|
21
23
|
const unresolved = active.successCriteria.filter((criterion) => criterion.status !== "pass");
|
|
22
24
|
if (unresolved.length > 0)
|
|
23
25
|
return [
|
|
24
|
-
|
|
26
|
+
surface === "omo-senpi"
|
|
27
|
+
? `${active.id} has ${unresolved.length} unresolved criterion(s) (${unresolved.map((criterion) => criterion.id).join(", ")}): record proof with \`agentToolkit.recordEvidence({ goalId: "${active.id}", criterionId: "<id>", status: "pass", evidence: "<observable proof>" })\`.`
|
|
28
|
+
: `${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>"\`.`,
|
|
25
29
|
];
|
|
26
30
|
if (hasAllCriteriaPass(active) && isFinalRunCompletionCandidate(plan, active))
|
|
27
31
|
return [
|
|
28
|
-
|
|
32
|
+
surface === "omo-senpi"
|
|
33
|
+
? `${active.id} passes every criterion and is the final story: complete the driver goal, then call \`agentToolkit.checkpoint({ goalId: "${active.id}", printTemplate: true })\` to build the final quality gate. Use status().result.currentAttemptDir for all quality-gate artifacts.`
|
|
34
|
+
: `${active.id} passes every criterion and is the final story: update_goal complete, then checkpoint --print-template to build the final quality gate. Use status --json's currentAttemptDir for all quality-gate artifacts.`,
|
|
29
35
|
];
|
|
30
36
|
return [
|
|
31
|
-
|
|
37
|
+
surface === "omo-senpi"
|
|
38
|
+
? `${active.id} passes every criterion: close it with \`agentToolkit.checkpoint({ goalId: "${active.id}", status: "complete", evidence: "<proof>" })\`. Put quality-gate artifacts under status().result.currentAttemptDir.`
|
|
39
|
+
: `${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>'\`. Put quality-gate artifacts under the currentAttemptDir shown by status --json.`,
|
|
32
40
|
];
|
|
33
41
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { commit } from "./plan-commit.js";
|
|
2
|
+
import { findAcceptedSteeringLedgerEntry, readUlwLoopPlan, withUlwLoopMutationLock } from "./plan-io.js";
|
|
2
3
|
import { applySteeringMutation, validateUlwLoopSteeringProposal } from "./steering.js";
|
|
3
4
|
import { buildSteeringPlanSnapshot, changedGoalIdsBetween } from "./steering-snapshot.js";
|
|
4
5
|
import { iso } from "./types.js";
|
|
@@ -10,7 +11,7 @@ export async function steerUlwLoopBatch(repoRoot, proposals, scope) {
|
|
|
10
11
|
const failed = prepared.results.find((item) => !item.accepted);
|
|
11
12
|
if (failed !== undefined) {
|
|
12
13
|
const entry = rejectedLedgerEntry(prepared.results);
|
|
13
|
-
await
|
|
14
|
+
await commit(repoRoot, scope, { plan, entries: [entry] });
|
|
14
15
|
return rejected(plan, prepared.results, failed.rejectedReasons);
|
|
15
16
|
}
|
|
16
17
|
let next = plan;
|
|
@@ -19,10 +20,9 @@ export async function steerUlwLoopBatch(repoRoot, proposals, scope) {
|
|
|
19
20
|
next = item.prepared.next;
|
|
20
21
|
const fresh = prepared.items.filter((item) => item.kind === "fresh");
|
|
21
22
|
if (fresh.length > 0) {
|
|
22
|
-
await writePlan(repoRoot, next, scope);
|
|
23
23
|
const entries = fresh.map((item) => ledgerEntry(item.prepared.proposal, item.prepared.audit, item.prepared.proposal.now?.toISOString() ?? iso()));
|
|
24
24
|
const batchEntry = batchUpdateLedgerEntry(plan, next, iso());
|
|
25
|
-
await
|
|
25
|
+
await commit(repoRoot, scope, { plan: next, entries: batchEntry === null ? entries : [...entries, batchEntry] });
|
|
26
26
|
}
|
|
27
27
|
return { plan: next, accepted: true, results: prepared.results, rejectedReasons: [] };
|
|
28
28
|
});
|