lazycodex-ai 5.0.0-beta.62 → 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/dist/cli/doctor/checks/browser-provider.d.ts +2 -0
- package/dist/cli/index.js +64 -35
- package/dist/cli-node/index.js +64 -35
- package/package.json +2 -6
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +2 -0
- 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/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 +5 -4
- 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 +13 -0
- package/packages/omo-codex/plugin/components/ulw-loop/directive.md +5 -4
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.js +5 -4
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +582 -322
- 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 +12 -5
- 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.js +8 -62
- 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 +5 -5
- 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 +30 -5
- 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 +8 -59
- 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-session-context.test.ts +21 -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 +15 -7
- 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/review-work/SKILL.md +1 -1
- 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 +5 -4
- package/packages/omo-codex/plugin/skills/ulw-execute/SKILL.md +1 -1
- package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +2 -2
- package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +3 -3
- package/packages/omo-codex/plugin/skills/visual-qa/AGENTS.md +1 -1
- package/packages/omo-codex/plugin/skills/visual-qa/SKILL.md +1 -1
- package/packages/omo-codex/plugin/skills/visual-qa/references/browser-setup.md +75 -0
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +4 -2
- package/packages/prompts-core/prompts/ultrawork/codex.md +5 -4
- 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/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,9 +1,10 @@
|
|
|
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";
|
|
7
8
|
import { spawnRoleDenial } from "./spawn-role-guard.js";
|
|
8
9
|
import { isStateLockTimeout, withStateLockSync } from "./state-lock.js";
|
|
9
10
|
import { GATE_REVIEWER_AGENT_NAMES, REVIEWER_ROLES_BY_SURFACE, resolveToolkitSurface, reviewerRolesFor, } from "./surface.js";
|
|
@@ -43,7 +44,7 @@ export function applySpawnBudgetGuards(payload, options = {}) {
|
|
|
43
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.`);
|
|
44
45
|
const scope = { sessionId: payload.session_id };
|
|
45
46
|
const stateDir = ulwLoopDir(payload.cwd, scope);
|
|
46
|
-
const plan = readPlan(
|
|
47
|
+
const plan = readPlan(payload.cwd, payload.session_id);
|
|
47
48
|
if (plan === null)
|
|
48
49
|
return "";
|
|
49
50
|
const lockOptions = options.lockTimeoutMs === undefined ? {} : { timeoutMs: options.lockTimeoutMs };
|
|
@@ -133,6 +134,7 @@ function peekFanOutBudget(stateDir) {
|
|
|
133
134
|
return null;
|
|
134
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.`;
|
|
135
136
|
}
|
|
137
|
+
// Hook budget counters are exempt from plan/audit commits.
|
|
136
138
|
// Per-session spawn counter; depth/lineage tracking is descoped — this is a
|
|
137
139
|
// total-volume backstop against fan-out explosions, not a recursion tracker.
|
|
138
140
|
function consumeFanOutBudget(stateDir) {
|
|
@@ -239,11 +241,6 @@ function activeSurfaceReviewerAlias(reviewer) {
|
|
|
239
241
|
}
|
|
240
242
|
return reviewer;
|
|
241
243
|
}
|
|
242
|
-
function atomicWriteJson(targetPath, data) {
|
|
243
|
-
const tmp = join(dirname(targetPath), `.tmp-${randomBytes(6).toString("hex")}`);
|
|
244
|
-
writeFileSync(tmp, JSON.stringify(data));
|
|
245
|
-
renameSync(tmp, targetPath);
|
|
246
|
-
}
|
|
247
244
|
function deny(reason) {
|
|
248
245
|
return `${JSON.stringify({
|
|
249
246
|
hookSpecificOutput: {
|
|
@@ -254,18 +251,6 @@ function deny(reason) {
|
|
|
254
251
|
},
|
|
255
252
|
})}\n`;
|
|
256
253
|
}
|
|
257
|
-
function readAdmissionBreaker(sessionId) {
|
|
258
|
-
const dataDir = process.env["PLUGIN_DATA"];
|
|
259
|
-
if (typeof dataDir !== "string")
|
|
260
|
-
return null;
|
|
261
|
-
try {
|
|
262
|
-
const value = JSON.parse(readFileSync(join(dataDir, "spawn-breaker", `${sessionId}.json`), "utf8"));
|
|
263
|
-
return typeof value.reason === "string" ? value.reason : "capacity limit";
|
|
264
|
-
}
|
|
265
|
-
catch {
|
|
266
|
-
return null;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
254
|
function fanOutLimit() {
|
|
270
255
|
const raw = process.env["OMO_SPAWN_FANOUT_LIMIT"];
|
|
271
256
|
if (raw === undefined)
|
|
@@ -280,48 +265,9 @@ function reviewSpawnLimit() {
|
|
|
280
265
|
const parsed = Number.parseInt(raw, 10);
|
|
281
266
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_REVIEW_SPAWN_LIMIT;
|
|
282
267
|
}
|
|
283
|
-
function
|
|
284
|
-
try {
|
|
285
|
-
return existsSync(path) && statSync(path).size > 0;
|
|
286
|
-
}
|
|
287
|
-
catch (error) {
|
|
288
|
-
if (error instanceof Error)
|
|
289
|
-
return false;
|
|
290
|
-
throw error;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
function readCount(counterPath) {
|
|
294
|
-
try {
|
|
295
|
-
const parsed = JSON.parse(readFileSync(counterPath, "utf8"));
|
|
296
|
-
return typeof parsed["count"] === "number" && parsed["count"] >= 0 ? parsed["count"] : 0;
|
|
297
|
-
}
|
|
298
|
-
catch (error) {
|
|
299
|
-
if (error instanceof Error)
|
|
300
|
-
return 0;
|
|
301
|
-
throw error;
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
function readCounts(counterPath) {
|
|
305
|
-
try {
|
|
306
|
-
const parsed = JSON.parse(readFileSync(counterPath, "utf8"));
|
|
307
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
308
|
-
return {};
|
|
309
|
-
const counts = {};
|
|
310
|
-
for (const [key, value] of Object.entries(parsed)) {
|
|
311
|
-
if (typeof value === "number" && value >= 0)
|
|
312
|
-
counts[key] = value;
|
|
313
|
-
}
|
|
314
|
-
return counts;
|
|
315
|
-
}
|
|
316
|
-
catch (error) {
|
|
317
|
-
if (error instanceof Error)
|
|
318
|
-
return {};
|
|
319
|
-
throw error;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
function readPlan(goalsPath) {
|
|
268
|
+
function readPlan(repoRoot, sessionId) {
|
|
323
269
|
try {
|
|
324
|
-
return
|
|
270
|
+
return readUlwLoopPlanSync(repoRoot, { sessionId });
|
|
325
271
|
}
|
|
326
272
|
catch (error) {
|
|
327
273
|
if (error instanceof Error)
|
|
@@ -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
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// biome-ignore-all format: compact steering module must stay below the 240 pure-LOC budget
|
|
2
2
|
import { isUlwLoopDone } from "./goal-status.js";
|
|
3
|
-
import {
|
|
3
|
+
import { commit } from "./plan-commit.js";
|
|
4
|
+
import { findAcceptedSteeringLedgerEntry, readUlwLoopPlan, withUlwLoopMutationLock } from "./plan-io.js";
|
|
4
5
|
import { makeGoal, reviseCriterion, reviseWording, splitOrBlock } from "./steering-mutations.js";
|
|
5
6
|
import { buildSteeringPlanSnapshot, changedGoalIdsBetween } from "./steering-snapshot.js";
|
|
6
7
|
import { iso, ULW_LOOP_STEERING_MUTATION_KINDS, ULW_LOOP_SUCCESS_CRITERION_USER_MODELS } from "./types.js";
|
|
@@ -223,11 +224,10 @@ export async function steerUlwLoop(repoRoot, proposal, scope) {
|
|
|
223
224
|
}
|
|
224
225
|
const at = proposal.now?.toISOString() ?? iso();
|
|
225
226
|
const batchEntry = accepted ? batchUpdateLedgerEntry(plan, next, at) : null;
|
|
226
|
-
|
|
227
|
-
await writePlan(repoRoot, next, scope);
|
|
228
|
-
await appendLedger(repoRoot, ledgerEntry(proposal, finalAudit, at), scope);
|
|
227
|
+
const entries = [ledgerEntry(proposal, finalAudit, at)];
|
|
229
228
|
if (batchEntry !== null)
|
|
230
|
-
|
|
229
|
+
entries.push(batchEntry);
|
|
230
|
+
await commit(repoRoot, scope, { plan: next, entries });
|
|
231
231
|
return { plan: next, accepted, audit: finalAudit, rejectedReasons: audit.invariant.rejectedReasons, deduped: false };
|
|
232
232
|
});
|
|
233
233
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { isAbsolute, join, resolve, sep } from "node:path";
|
|
3
|
+
import { readLedgerAt } from "./ledger.js";
|
|
3
4
|
import { normalizeUlwLoopSessionId, ulwLoopDir, ulwLoopStateLockPath } from "./paths.js";
|
|
5
|
+
import { readUlwLoopPlanSync } from "./plan-io.js";
|
|
4
6
|
import { isStateLockTimeout, withStateLockSync } from "./state-lock.js";
|
|
5
7
|
// Turn-death recovery only: Codex emits Stop when a turn ends, so a run that
|
|
6
8
|
// dies mid-turn (crash, kill, context blowup before any Stop) never reaches
|
|
@@ -27,7 +29,7 @@ export function runStopResumeHook(input) {
|
|
|
27
29
|
return "";
|
|
28
30
|
const scope = { sessionId: payload.session_id };
|
|
29
31
|
const stateDir = ulwLoopDir(payload.cwd, scope);
|
|
30
|
-
const plan = readPlan(
|
|
32
|
+
const plan = readPlan(payload.cwd, payload.session_id);
|
|
31
33
|
if (plan === null || plan.aggregateCompletion?.status === "complete")
|
|
32
34
|
return "";
|
|
33
35
|
const goal = resumableGoal(plan);
|
|
@@ -76,11 +78,10 @@ function consumeResumeBudgetLocked(lockPath, stateDir, goalId) {
|
|
|
76
78
|
throw error;
|
|
77
79
|
}
|
|
78
80
|
}
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
// a separate file — a ledger append would change the count and self-reset.
|
|
81
|
+
// Hook budget counters are exempt from plan/audit commits. Count reconciled
|
|
82
|
+
// entries, not cache lines: a writer dying after link still made progress.
|
|
82
83
|
function consumeResumeBudget(stateDir, goalId) {
|
|
83
|
-
const ledgerLineCount =
|
|
84
|
+
const ledgerLineCount = readLedgerAt(stateDir).length;
|
|
84
85
|
const counterPath = resolve(stateDir, `auto-resume-${goalId}.json`);
|
|
85
86
|
const stuckPath = resolve(stateDir, `auto-resume-${goalId}.stuck`);
|
|
86
87
|
// goals.json is untrusted input: a crafted goal id (e.g. `../../x`) must
|
|
@@ -111,9 +112,9 @@ function renderResumeDirective(plan, goal, sessionId) {
|
|
|
111
112
|
"If the loop is genuinely blocked on the user, checkpoint the goal as blocked with the reason instead.",
|
|
112
113
|
].join("\n");
|
|
113
114
|
}
|
|
114
|
-
function readPlan(
|
|
115
|
+
function readPlan(repoRoot, sessionId) {
|
|
115
116
|
try {
|
|
116
|
-
return
|
|
117
|
+
return readUlwLoopPlanSync(repoRoot, { sessionId });
|
|
117
118
|
}
|
|
118
119
|
catch (error) {
|
|
119
120
|
if (error instanceof Error)
|
|
@@ -121,16 +122,6 @@ function readPlan(goalsPath) {
|
|
|
121
122
|
throw error;
|
|
122
123
|
}
|
|
123
124
|
}
|
|
124
|
-
function countLedgerLines(ledgerPath) {
|
|
125
|
-
try {
|
|
126
|
-
return readFileSync(ledgerPath, "utf8").split("\n").filter(Boolean).length;
|
|
127
|
-
}
|
|
128
|
-
catch (error) {
|
|
129
|
-
if (error instanceof Error)
|
|
130
|
-
return 0;
|
|
131
|
-
throw error;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
125
|
function readCounter(counterPath) {
|
|
135
126
|
try {
|
|
136
127
|
if (!existsSync(counterPath))
|