lazycodex-ai 5.0.0-beta.61 → 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,11 +1,12 @@
|
|
|
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
|
|
|
5
4
|
import type { PreToolUsePayload } from "./codex-hook.js";
|
|
6
5
|
import { parsePreToolUsePayload } from "./codex-hook.js";
|
|
7
6
|
import { isFinalRunCompletionCandidate } from "./goal-status.js";
|
|
8
7
|
import { ulwLoopAttemptEvidenceDir, ulwLoopDir, ulwLoopStateLockPath } from "./paths.js";
|
|
8
|
+
import { readUlwLoopPlanSync } from "./plan-io.js";
|
|
9
|
+
import { atomicWriteJson, isNonEmptyFile, readAdmissionBreaker, readCount, readCounts } from "./spawn-budget-io.js";
|
|
9
10
|
import { spawnRoleDenial } from "./spawn-role-guard.js";
|
|
10
11
|
import { isStateLockTimeout, type StateLockOptions, withStateLockSync } from "./state-lock.js";
|
|
11
12
|
import {
|
|
@@ -57,7 +58,7 @@ export function applySpawnBudgetGuards(payload: PreToolUsePayload, options: Spaw
|
|
|
57
58
|
);
|
|
58
59
|
const scope = { sessionId: payload.session_id } as const;
|
|
59
60
|
const stateDir = ulwLoopDir(payload.cwd, scope);
|
|
60
|
-
const plan = readPlan(
|
|
61
|
+
const plan = readPlan(payload.cwd, payload.session_id);
|
|
61
62
|
if (plan === null) return "";
|
|
62
63
|
const lockOptions: StateLockOptions =
|
|
63
64
|
options.lockTimeoutMs === undefined ? {} : { timeoutMs: options.lockTimeoutMs };
|
|
@@ -148,6 +149,7 @@ function peekFanOutBudget(stateDir: string): string | null {
|
|
|
148
149
|
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.`;
|
|
149
150
|
}
|
|
150
151
|
|
|
152
|
+
// Hook budget counters are exempt from plan/audit commits.
|
|
151
153
|
// Per-session spawn counter; depth/lineage tracking is descoped — this is a
|
|
152
154
|
// total-volume backstop against fan-out explosions, not a recursion tracker.
|
|
153
155
|
function consumeFanOutBudget(stateDir: string): string | null {
|
|
@@ -245,12 +247,6 @@ function activeSurfaceReviewerAlias(reviewer: string): string {
|
|
|
245
247
|
return reviewer;
|
|
246
248
|
}
|
|
247
249
|
|
|
248
|
-
function atomicWriteJson(targetPath: string, data: unknown): void {
|
|
249
|
-
const tmp = join(dirname(targetPath), `.tmp-${randomBytes(6).toString("hex")}`);
|
|
250
|
-
writeFileSync(tmp, JSON.stringify(data));
|
|
251
|
-
renameSync(tmp, targetPath);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
250
|
function deny(reason: string): string {
|
|
255
251
|
return `${JSON.stringify({
|
|
256
252
|
hookSpecificOutput: {
|
|
@@ -262,19 +258,6 @@ function deny(reason: string): string {
|
|
|
262
258
|
})}\n`;
|
|
263
259
|
}
|
|
264
260
|
|
|
265
|
-
function readAdmissionBreaker(sessionId: string): string | null {
|
|
266
|
-
const dataDir = process.env["PLUGIN_DATA"];
|
|
267
|
-
if (typeof dataDir !== "string") return null;
|
|
268
|
-
try {
|
|
269
|
-
const value = JSON.parse(readFileSync(join(dataDir, "spawn-breaker", `${sessionId}.json`), "utf8")) as {
|
|
270
|
-
reason?: unknown;
|
|
271
|
-
};
|
|
272
|
-
return typeof value.reason === "string" ? value.reason : "capacity limit";
|
|
273
|
-
} catch {
|
|
274
|
-
return null;
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
261
|
function fanOutLimit(): number {
|
|
279
262
|
const raw = process.env["OMO_SPAWN_FANOUT_LIMIT"];
|
|
280
263
|
if (raw === undefined) return DEFAULT_FANOUT_LIMIT;
|
|
@@ -289,43 +272,9 @@ function reviewSpawnLimit(): number {
|
|
|
289
272
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_REVIEW_SPAWN_LIMIT;
|
|
290
273
|
}
|
|
291
274
|
|
|
292
|
-
function
|
|
293
|
-
try {
|
|
294
|
-
return existsSync(path) && statSync(path).size > 0;
|
|
295
|
-
} catch (error) {
|
|
296
|
-
if (error instanceof Error) return false;
|
|
297
|
-
throw error;
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
function readCount(counterPath: string): number {
|
|
302
|
-
try {
|
|
303
|
-
const parsed = JSON.parse(readFileSync(counterPath, "utf8")) as Record<string, unknown>;
|
|
304
|
-
return typeof parsed["count"] === "number" && parsed["count"] >= 0 ? parsed["count"] : 0;
|
|
305
|
-
} catch (error) {
|
|
306
|
-
if (error instanceof Error) return 0;
|
|
307
|
-
throw error;
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
function readCounts(counterPath: string): Record<string, number> {
|
|
312
|
-
try {
|
|
313
|
-
const parsed: unknown = JSON.parse(readFileSync(counterPath, "utf8"));
|
|
314
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
|
|
315
|
-
const counts: Record<string, number> = {};
|
|
316
|
-
for (const [key, value] of Object.entries(parsed)) {
|
|
317
|
-
if (typeof value === "number" && value >= 0) counts[key] = value;
|
|
318
|
-
}
|
|
319
|
-
return counts;
|
|
320
|
-
} catch (error) {
|
|
321
|
-
if (error instanceof Error) return {};
|
|
322
|
-
throw error;
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
function readPlan(goalsPath: string): UlwLoopPlan | null {
|
|
275
|
+
function readPlan(repoRoot: string, sessionId: string): UlwLoopPlan | null {
|
|
327
276
|
try {
|
|
328
|
-
return
|
|
277
|
+
return readUlwLoopPlanSync(repoRoot, { sessionId });
|
|
329
278
|
} catch (error) {
|
|
330
279
|
if (error instanceof Error) return null;
|
|
331
280
|
throw error;
|
|
@@ -1,120 +1,159 @@
|
|
|
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
|
-
|
|
6
4
|
import { UlwLoopError } from "./types.js";
|
|
7
5
|
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// owner (pid + a per-acquisition token). A waiter reclaims it only when the
|
|
15
|
-
// owner is provably gone (pid dead) or the body never became a record within
|
|
16
|
-
// `staleMs` (a creator that died mid-write); a live owner is never reclaimed by
|
|
17
|
-
// age alone, because overlapping two bodies is exactly the lost update this lock
|
|
18
|
-
// exists to prevent. Waiters back off and fail closed at `timeoutMs`. Release
|
|
19
|
-
// unlinks only a lock that still carries the releaser's own token.
|
|
20
|
-
|
|
6
|
+
// O_EXCL establishes ownership. Async holders keep the fd open and refresh a
|
|
7
|
+
// lease through that fd, never through the pathname: a reclaimed inode is invisible.
|
|
8
|
+
// Sync/legacy holders are lease-less and only dead pids retire their records.
|
|
9
|
+
// Expiry permits overlapping bodies; immutable revision publication fences them.
|
|
10
|
+
// Reclaim's reread/unlink is not atomic. On Windows an open inode may not unlink:
|
|
11
|
+
// treat that refusal as a live owner and fail closed. Release checks its token.
|
|
21
12
|
export const ULW_LOOP_LOCK_TIMEOUT_CODE = "ULW_LOOP_LOCK_TIMEOUT";
|
|
22
|
-
|
|
13
|
+
export interface StateLockClock {
|
|
14
|
+
readonly now: () => number;
|
|
15
|
+
readonly schedule: (fn: () => void, ms: number) => { unref(): void; cancel(): void };
|
|
16
|
+
}
|
|
23
17
|
export interface StateLockOptions {
|
|
24
18
|
readonly timeoutMs?: number;
|
|
25
19
|
readonly staleMs?: number;
|
|
20
|
+
readonly leaseMs?: number;
|
|
21
|
+
readonly heartbeatMs?: number;
|
|
22
|
+
readonly clock?: StateLockClock;
|
|
26
23
|
}
|
|
27
|
-
|
|
28
24
|
interface LockRecord {
|
|
29
25
|
readonly pid: number;
|
|
30
26
|
readonly createdAt: string;
|
|
31
27
|
readonly token: string;
|
|
28
|
+
leaseUntil?: number;
|
|
29
|
+
}
|
|
30
|
+
interface Holder {
|
|
31
|
+
readonly fd: number;
|
|
32
|
+
readonly record: LockRecord;
|
|
32
33
|
}
|
|
33
|
-
|
|
34
34
|
interface LockSnapshot {
|
|
35
35
|
readonly raw: string;
|
|
36
36
|
readonly record: LockRecord | null;
|
|
37
37
|
readonly ageMs: number;
|
|
38
38
|
}
|
|
39
|
-
|
|
40
|
-
type AttemptOutcome = { readonly kind: "acquired"; readonly token: string } | { readonly kind: "retry" | "wait" };
|
|
41
|
-
|
|
39
|
+
type AttemptOutcome = { readonly kind: "acquired"; readonly holder: Holder } | { readonly kind: "retry" | "wait" };
|
|
42
40
|
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
43
41
|
const DEFAULT_STALE_MS = 60_000;
|
|
44
|
-
const
|
|
45
|
-
const MAX_DELAY_MS = 100;
|
|
42
|
+
const DEFAULT_LEASE_MS = 30_000;
|
|
46
43
|
const SLEEP_CELL = new Int32Array(new SharedArrayBuffer(4));
|
|
44
|
+
const systemClock: StateLockClock = {
|
|
45
|
+
now: Date.now,
|
|
46
|
+
schedule(fn, ms) {
|
|
47
|
+
const timer = setTimeout(fn, ms);
|
|
48
|
+
return {
|
|
49
|
+
unref: () => {
|
|
50
|
+
timer.unref();
|
|
51
|
+
},
|
|
52
|
+
cancel: () => clearTimeout(timer),
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
};
|
|
47
56
|
|
|
48
57
|
export async function withStateLock<T>(
|
|
49
58
|
lockPath: string,
|
|
50
|
-
fn: () => Promise<T>,
|
|
59
|
+
fn: (token: string) => Promise<T>,
|
|
51
60
|
options: StateLockOptions = {},
|
|
52
61
|
): Promise<T> {
|
|
53
|
-
const
|
|
62
|
+
const holder = await acquireAsync(lockPath, options);
|
|
63
|
+
const clock = options.clock ?? systemClock;
|
|
64
|
+
let timer: ReturnType<StateLockClock["schedule"]> | undefined;
|
|
65
|
+
let heartbeatError: unknown;
|
|
66
|
+
const beat = () => {
|
|
67
|
+
try {
|
|
68
|
+
holder.record.leaseUntil = clock.now() + (options.leaseMs ?? DEFAULT_LEASE_MS);
|
|
69
|
+
writeRecord(holder);
|
|
70
|
+
schedule();
|
|
71
|
+
} catch (error) {
|
|
72
|
+
heartbeatError = error;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
const schedule = () => {
|
|
76
|
+
if (options.heartbeatMs === 0) return;
|
|
77
|
+
timer = clock.schedule(beat, options.heartbeatMs ?? (options.leaseMs ?? DEFAULT_LEASE_MS) / 3);
|
|
78
|
+
timer.unref();
|
|
79
|
+
};
|
|
80
|
+
schedule();
|
|
54
81
|
try {
|
|
55
|
-
|
|
82
|
+
const result = await fn(holder.record.token);
|
|
83
|
+
if (heartbeatError !== undefined) throw heartbeatError;
|
|
84
|
+
return result;
|
|
56
85
|
} finally {
|
|
57
|
-
|
|
86
|
+
timer?.cancel();
|
|
87
|
+
closeSync(holder.fd);
|
|
88
|
+
release(lockPath, holder.record.token);
|
|
58
89
|
}
|
|
59
90
|
}
|
|
60
|
-
|
|
61
91
|
export function withStateLockSync<T>(lockPath: string, fn: () => T, options: StateLockOptions = {}): T {
|
|
62
|
-
const
|
|
92
|
+
const holder = acquireSync(lockPath, options);
|
|
63
93
|
try {
|
|
64
94
|
return fn();
|
|
65
95
|
} finally {
|
|
66
|
-
|
|
96
|
+
closeSync(holder.fd);
|
|
97
|
+
release(lockPath, holder.record.token);
|
|
67
98
|
}
|
|
68
99
|
}
|
|
69
|
-
|
|
70
100
|
export function isStateLockTimeout(error: unknown): error is UlwLoopError {
|
|
71
101
|
return error instanceof UlwLoopError && error.code === ULW_LOOP_LOCK_TIMEOUT_CODE;
|
|
72
102
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const deadline =
|
|
76
|
-
const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
|
|
103
|
+
async function acquireAsync(lockPath: string, options: StateLockOptions): Promise<Holder> {
|
|
104
|
+
const clock = options.clock ?? systemClock;
|
|
105
|
+
const deadline = clock.now() + (options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
77
106
|
mkdirSync(dirname(lockPath), { recursive: true });
|
|
78
107
|
for (let attempt = 0; ; ) {
|
|
79
|
-
const outcome = attemptOnce(lockPath,
|
|
80
|
-
if (outcome.kind === "acquired") return outcome.
|
|
108
|
+
const outcome = attemptOnce(lockPath, options, clock.now(), options.leaseMs ?? DEFAULT_LEASE_MS);
|
|
109
|
+
if (outcome.kind === "acquired") return outcome.holder;
|
|
81
110
|
if (outcome.kind === "retry") continue;
|
|
82
|
-
if (
|
|
83
|
-
await
|
|
111
|
+
if (clock.now() >= deadline) throw lockTimeout(lockPath, options);
|
|
112
|
+
await new Promise<void>((resolve) => clock.schedule(resolve, backoffMs(attempt)));
|
|
84
113
|
attempt += 1;
|
|
85
114
|
}
|
|
86
115
|
}
|
|
87
|
-
|
|
88
|
-
function acquireSync(lockPath: string, options: StateLockOptions): string {
|
|
116
|
+
function acquireSync(lockPath: string, options: StateLockOptions): Holder {
|
|
89
117
|
const deadline = Date.now() + (options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
90
|
-
const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
|
|
91
118
|
mkdirSync(dirname(lockPath), { recursive: true });
|
|
92
119
|
for (let attempt = 0; ; ) {
|
|
93
|
-
const outcome = attemptOnce(lockPath,
|
|
94
|
-
if (outcome.kind === "acquired") return outcome.
|
|
120
|
+
const outcome = attemptOnce(lockPath, options, Date.now());
|
|
121
|
+
if (outcome.kind === "acquired") return outcome.holder;
|
|
95
122
|
if (outcome.kind === "retry") continue;
|
|
96
|
-
if (Date.now() >= deadline) throw lockTimeout(lockPath, options
|
|
123
|
+
if (Date.now() >= deadline) throw lockTimeout(lockPath, options);
|
|
97
124
|
Atomics.wait(SLEEP_CELL, 0, 0, backoffMs(attempt));
|
|
98
125
|
attempt += 1;
|
|
99
126
|
}
|
|
100
127
|
}
|
|
101
|
-
|
|
102
|
-
// EINTR surfaces raw from macOS fs syscalls under some runtimes; it is a retry, never a verdict.
|
|
103
|
-
function attemptOnce(lockPath: string, staleMs: number): AttemptOutcome {
|
|
128
|
+
function attemptOnce(lockPath: string, options: StateLockOptions, now: number, leaseMs?: number): AttemptOutcome {
|
|
104
129
|
try {
|
|
105
|
-
const
|
|
106
|
-
if (
|
|
107
|
-
const snapshot = readSnapshot(lockPath);
|
|
130
|
+
const holder = tryCreate(lockPath, now, leaseMs);
|
|
131
|
+
if (holder !== null) return { kind: "acquired", holder };
|
|
132
|
+
const snapshot = readSnapshot(lockPath, now);
|
|
108
133
|
if (snapshot === null) return { kind: "retry" };
|
|
109
|
-
|
|
134
|
+
const record = snapshot.record;
|
|
135
|
+
const stale =
|
|
136
|
+
record === null
|
|
137
|
+
? snapshot.ageMs > (options.staleMs ?? DEFAULT_STALE_MS)
|
|
138
|
+
: !isProcessAlive(record.pid) || (record.leaseUntil !== undefined && now > record.leaseUntil);
|
|
139
|
+
if (stale && reclaim(lockPath, snapshot.raw)) return { kind: "retry" };
|
|
110
140
|
return { kind: "wait" };
|
|
111
141
|
} catch (error) {
|
|
112
142
|
if (hasCode(error, "EINTR")) return { kind: "wait" };
|
|
113
143
|
throw error;
|
|
114
144
|
}
|
|
115
145
|
}
|
|
116
|
-
|
|
117
|
-
|
|
146
|
+
function writeRecord(holder: Holder): void {
|
|
147
|
+
const buf = Buffer.from(JSON.stringify(holder.record));
|
|
148
|
+
ftruncateSync(holder.fd, 0);
|
|
149
|
+
let offset = 0;
|
|
150
|
+
while (offset < buf.length) {
|
|
151
|
+
const written = writeSync(holder.fd, buf, offset, buf.length - offset, offset);
|
|
152
|
+
if (written === 0) throw new Error("State lock write made no progress.");
|
|
153
|
+
offset += written;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function tryCreate(lockPath: string, now: number, leaseMs?: number): Holder | null {
|
|
118
157
|
let fd: number;
|
|
119
158
|
try {
|
|
120
159
|
fd = openSync(lockPath, "wx");
|
|
@@ -122,60 +161,54 @@ function tryCreate(lockPath: string): string | null {
|
|
|
122
161
|
if (hasCode(error, "EEXIST")) return null;
|
|
123
162
|
throw error;
|
|
124
163
|
}
|
|
125
|
-
const record: LockRecord = {
|
|
164
|
+
const record: LockRecord = {
|
|
165
|
+
pid: process.pid,
|
|
166
|
+
createdAt: new Date(now).toISOString(),
|
|
167
|
+
token: randomUUID(),
|
|
168
|
+
...(leaseMs === undefined ? {} : { leaseUntil: now + leaseMs }),
|
|
169
|
+
};
|
|
170
|
+
const holder = { fd, record };
|
|
126
171
|
try {
|
|
127
|
-
|
|
172
|
+
writeRecord(holder);
|
|
128
173
|
} catch (error) {
|
|
129
174
|
closeSync(fd);
|
|
130
|
-
// A failed write (e.g. EINTR) must not leave a partial lock the owner never recorded;
|
|
131
|
-
// otherwise later readers see a young ownerless lock and only age can retire it.
|
|
132
175
|
try {
|
|
133
176
|
unlinkSync(lockPath);
|
|
134
|
-
} catch {
|
|
135
|
-
|
|
177
|
+
} catch (cleanup) {
|
|
178
|
+
if (!hasCode(cleanup, "ENOENT")) throw cleanup;
|
|
136
179
|
}
|
|
137
180
|
throw error;
|
|
138
181
|
}
|
|
139
|
-
|
|
140
|
-
return record.token;
|
|
182
|
+
return holder;
|
|
141
183
|
}
|
|
142
|
-
|
|
143
|
-
function readSnapshot(lockPath: string): LockSnapshot | null {
|
|
184
|
+
function readSnapshot(lockPath: string, now: number): LockSnapshot | null {
|
|
144
185
|
try {
|
|
145
186
|
const raw = readFileSync(lockPath, "utf8");
|
|
146
|
-
|
|
147
|
-
return { raw, record: parseRecord(raw), ageMs };
|
|
187
|
+
return { raw, record: parseRecord(raw), ageMs: now - statSync(lockPath).mtimeMs };
|
|
148
188
|
} catch (error) {
|
|
149
189
|
if (hasCode(error, "ENOENT")) return null;
|
|
150
190
|
throw error;
|
|
151
191
|
}
|
|
152
192
|
}
|
|
153
|
-
|
|
154
193
|
function parseRecord(raw: string): LockRecord | null {
|
|
155
194
|
try {
|
|
156
|
-
const
|
|
157
|
-
if (typeof
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
195
|
+
const record: unknown = JSON.parse(raw);
|
|
196
|
+
if (typeof record !== "object" || record === null) return null;
|
|
197
|
+
if (!("pid" in record) || typeof record.pid !== "number" || !Number.isInteger(record.pid) || record.pid <= 0)
|
|
198
|
+
return null;
|
|
199
|
+
if (!("createdAt" in record) || typeof record.createdAt !== "string") return null;
|
|
200
|
+
if (!("token" in record) || typeof record.token !== "string" || record.token.length === 0) return null;
|
|
201
|
+
return {
|
|
202
|
+
pid: record.pid,
|
|
203
|
+
createdAt: record.createdAt,
|
|
204
|
+
token: record.token,
|
|
205
|
+
...("leaseUntil" in record && typeof record.leaseUntil === "number" ? { leaseUntil: record.leaseUntil } : {}),
|
|
206
|
+
};
|
|
165
207
|
} catch (error) {
|
|
166
208
|
if (error instanceof SyntaxError) return null;
|
|
167
209
|
throw error;
|
|
168
210
|
}
|
|
169
211
|
}
|
|
170
|
-
|
|
171
|
-
// A body that is not a record is a lock mid-write (or a foreign/legacy file); only
|
|
172
|
-
// age retires it. A parsable record is stale only when its owner is dead: a live
|
|
173
|
-
// owner running past staleMs is slow, not gone, and the waiter fails closed instead.
|
|
174
|
-
function isStale(snapshot: LockSnapshot, staleMs: number): boolean {
|
|
175
|
-
if (snapshot.record === null) return snapshot.ageMs > staleMs;
|
|
176
|
-
return !isProcessAlive(snapshot.record.pid);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
212
|
function isProcessAlive(pid: number): boolean {
|
|
180
213
|
try {
|
|
181
214
|
process.kill(pid, 0);
|
|
@@ -186,54 +219,39 @@ function isProcessAlive(pid: number): boolean {
|
|
|
186
219
|
throw error;
|
|
187
220
|
}
|
|
188
221
|
}
|
|
189
|
-
|
|
190
|
-
// Re-read right before unlinking so a lock that changed hands since the stale
|
|
191
|
-
// verdict (a sibling waiter reclaimed and re-acquired it) is left alone.
|
|
192
222
|
function reclaim(lockPath: string, expectedRaw: string): boolean {
|
|
193
|
-
const current = readSnapshot(lockPath);
|
|
223
|
+
const current = readSnapshot(lockPath, Date.now());
|
|
194
224
|
if (current === null) return true;
|
|
195
225
|
if (current.raw !== expectedRaw) return false;
|
|
196
226
|
try {
|
|
197
227
|
unlinkSync(lockPath);
|
|
198
228
|
} catch (error) {
|
|
229
|
+
if (hasCode(error, "EPERM") || hasCode(error, "EACCES")) return false;
|
|
199
230
|
if (!hasCode(error, "ENOENT")) throw error;
|
|
200
231
|
}
|
|
201
232
|
return true;
|
|
202
233
|
}
|
|
203
|
-
|
|
204
|
-
// Only the acquisition that wrote this token may unlink: if the file now carries
|
|
205
|
-
// another token, a waiter has legitimately taken over and its lock must stand.
|
|
206
234
|
function release(lockPath: string, token: string): void {
|
|
207
|
-
|
|
208
|
-
if (current === null || current.record?.token !== token) return;
|
|
235
|
+
if (readSnapshot(lockPath, Date.now())?.record?.token !== token) return;
|
|
209
236
|
try {
|
|
210
237
|
unlinkSync(lockPath);
|
|
211
238
|
} catch (error) {
|
|
212
239
|
if (!hasCode(error, "ENOENT")) throw error;
|
|
213
240
|
}
|
|
214
241
|
}
|
|
215
|
-
|
|
216
242
|
function backoffMs(attempt: number): number {
|
|
217
|
-
|
|
218
|
-
return exponential + Math.random() * MIN_DELAY_MS;
|
|
243
|
+
return Math.min(100, 5 * 2 ** attempt) + Math.random() * 5;
|
|
219
244
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const
|
|
223
|
-
const owner = holder
|
|
245
|
+
function lockTimeout(lockPath: string, options: StateLockOptions): UlwLoopError {
|
|
246
|
+
const holder = readSnapshot(lockPath, Date.now())?.record;
|
|
247
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
248
|
+
const owner = holder == null ? "another process" : `pid ${holder.pid}`;
|
|
224
249
|
return new UlwLoopError(
|
|
225
|
-
`ulw-loop state lock ${lockPath} is held by ${owner} for more than ${timeoutMs}ms
|
|
250
|
+
`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.`,
|
|
226
251
|
ULW_LOOP_LOCK_TIMEOUT_CODE,
|
|
227
|
-
{
|
|
228
|
-
details: {
|
|
229
|
-
lockPath,
|
|
230
|
-
timeoutMs,
|
|
231
|
-
...(holder === undefined || holder === null ? {} : { holderPid: holder.pid }),
|
|
232
|
-
},
|
|
233
|
-
},
|
|
252
|
+
{ details: { lockPath, timeoutMs, ...(holder == null ? {} : { holderPid: holder.pid }) } },
|
|
234
253
|
);
|
|
235
254
|
}
|
|
236
|
-
|
|
237
255
|
function hasCode(error: unknown, code: string): boolean {
|
|
238
256
|
return error instanceof Error && "code" in error && error.code === code;
|
|
239
257
|
}
|
|
@@ -1,36 +1,45 @@
|
|
|
1
1
|
import { hasAllCriteriaPass, isFinalRunCompletionCandidate } from "./goal-status.js";
|
|
2
|
-
import {
|
|
2
|
+
import { createGoalsAction } from "./plan-missing-recovery.js";
|
|
3
|
+
import type { UlwLoopToolkitSurface } from "./surface.js";
|
|
3
4
|
import type { UlwLoopItem, UlwLoopPlan } from "./types.js";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
|
-
*
|
|
7
|
+
* Status is the one call every agent already makes between steps, so it is
|
|
7
8
|
* also the cheapest place to answer "what now?" without a second round trip.
|
|
8
9
|
*/
|
|
9
|
-
export function statusNextActions(plan: UlwLoopPlan): readonly string[] {
|
|
10
|
+
export function statusNextActions(plan: UlwLoopPlan, surface: UlwLoopToolkitSurface = "lazycodex"): readonly string[] {
|
|
10
11
|
const actions: string[] = [];
|
|
11
12
|
const active = plan.goals.find((goal) => goal.id === plan.activeGoalId);
|
|
12
|
-
if (plan.goals.length === 0) actions.push(`No goals yet: bootstrap with \`${
|
|
13
|
-
else if (active !== undefined) actions.push(...activeGoalActions(plan, active));
|
|
13
|
+
if (plan.goals.length === 0) actions.push(`No goals yet: bootstrap with \`${createGoalsAction(surface)}\`.`);
|
|
14
|
+
else if (active !== undefined) actions.push(...activeGoalActions(plan, active, surface));
|
|
14
15
|
for (const goal of plan.goals.filter((candidate) => candidate.status === "review_blocked"))
|
|
15
16
|
actions.push(
|
|
16
|
-
|
|
17
|
+
surface === "omo-senpi"
|
|
18
|
+
? `${goal.id} is review_blocked: capture the reviewer verdict with \`agentToolkit.recordReviewBlockers({ goalId: "${goal.id}", title: "<title>", objective: "<objective>", evidence: "<verdict>" })\`.`
|
|
19
|
+
: `${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>'\`.`,
|
|
17
20
|
);
|
|
18
21
|
if (plan.evidenceLayoutVersion !== 2 || active === undefined)
|
|
19
22
|
actions.push("plan is evidence-layout v1; artifacts go under .omo/evidence/");
|
|
20
23
|
return actions;
|
|
21
24
|
}
|
|
22
25
|
|
|
23
|
-
function activeGoalActions(plan: UlwLoopPlan, active: UlwLoopItem): readonly string[] {
|
|
26
|
+
function activeGoalActions(plan: UlwLoopPlan, active: UlwLoopItem, surface: UlwLoopToolkitSurface): readonly string[] {
|
|
24
27
|
const unresolved = active.successCriteria.filter((criterion) => criterion.status !== "pass");
|
|
25
28
|
if (unresolved.length > 0)
|
|
26
29
|
return [
|
|
27
|
-
|
|
30
|
+
surface === "omo-senpi"
|
|
31
|
+
? `${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>" })\`.`
|
|
32
|
+
: `${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>"\`.`,
|
|
28
33
|
];
|
|
29
34
|
if (hasAllCriteriaPass(active) && isFinalRunCompletionCandidate(plan, active))
|
|
30
35
|
return [
|
|
31
|
-
|
|
36
|
+
surface === "omo-senpi"
|
|
37
|
+
? `${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.`
|
|
38
|
+
: `${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.`,
|
|
32
39
|
];
|
|
33
40
|
return [
|
|
34
|
-
|
|
41
|
+
surface === "omo-senpi"
|
|
42
|
+
? `${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.`
|
|
43
|
+
: `${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.`,
|
|
35
44
|
];
|
|
36
45
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// biome-ignore-all format: compact batch steering module stays below the pure LOC budget.
|
|
2
2
|
import type { UlwLoopScope } from "./paths.js";
|
|
3
|
-
import {
|
|
3
|
+
import { commit } from "./plan-commit.js";
|
|
4
|
+
import { findAcceptedSteeringLedgerEntry, readUlwLoopPlan, withUlwLoopMutationLock } from "./plan-io.js";
|
|
4
5
|
import { applySteeringMutation, validateUlwLoopSteeringProposal } from "./steering.js";
|
|
5
6
|
import { buildSteeringPlanSnapshot, changedGoalIdsBetween } from "./steering-snapshot.js";
|
|
6
7
|
import type { UlwLoopLedgerEntry, UlwLoopPlan, UlwLoopSteeringAudit, UlwLoopSteeringProposal } from "./types.js";
|
|
@@ -43,17 +44,16 @@ export async function steerUlwLoopBatch(
|
|
|
43
44
|
const failed = prepared.results.find((item) => !item.accepted);
|
|
44
45
|
if (failed !== undefined) {
|
|
45
46
|
const entry = rejectedLedgerEntry(prepared.results);
|
|
46
|
-
await
|
|
47
|
+
await commit(repoRoot, scope, { plan, entries: [entry] });
|
|
47
48
|
return rejected(plan, prepared.results, failed.rejectedReasons);
|
|
48
49
|
}
|
|
49
50
|
let next = plan;
|
|
50
51
|
for (const item of prepared.items) if (item.kind === "fresh") next = item.prepared.next;
|
|
51
52
|
const fresh = prepared.items.filter((item): item is Extract<PreparedItem, { readonly kind: "fresh" }> => item.kind === "fresh");
|
|
52
53
|
if (fresh.length > 0) {
|
|
53
|
-
await writePlan(repoRoot, next, scope);
|
|
54
54
|
const entries = fresh.map((item) => ledgerEntry(item.prepared.proposal, item.prepared.audit, item.prepared.proposal.now?.toISOString() ?? iso()));
|
|
55
55
|
const batchEntry = batchUpdateLedgerEntry(plan, next, iso());
|
|
56
|
-
await
|
|
56
|
+
await commit(repoRoot, scope, { plan: next, entries: batchEntry === null ? entries : [...entries, batchEntry] });
|
|
57
57
|
}
|
|
58
58
|
return { plan: next, accepted: true, results: prepared.results, rejectedReasons: [] };
|
|
59
59
|
});
|
|
@@ -1,7 +1,8 @@
|
|
|
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
3
|
import type { UlwLoopScope } from "./paths.js";
|
|
4
|
-
import {
|
|
4
|
+
import { commit } from "./plan-commit.js";
|
|
5
|
+
import { findAcceptedSteeringLedgerEntry, readUlwLoopPlan, withUlwLoopMutationLock } from "./plan-io.js";
|
|
5
6
|
import { makeGoal, reviseCriterion, reviseWording, splitOrBlock } from "./steering-mutations.js";
|
|
6
7
|
import { buildSteeringPlanSnapshot, changedGoalIdsBetween } from "./steering-snapshot.js";
|
|
7
8
|
import type {
|
|
@@ -209,9 +210,9 @@ export async function steerUlwLoop(repoRoot: string, proposal: UlwLoopSteeringPr
|
|
|
209
210
|
}
|
|
210
211
|
const at = proposal.now?.toISOString() ?? iso();
|
|
211
212
|
const batchEntry = accepted ? batchUpdateLedgerEntry(plan, next, at) : null;
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
213
|
+
const entries = [ledgerEntry(proposal, finalAudit, at)];
|
|
214
|
+
if (batchEntry !== null) entries.push(batchEntry);
|
|
215
|
+
await commit(repoRoot, scope, { plan: next, entries });
|
|
215
216
|
return { plan: next, accepted, audit: finalAudit, rejectedReasons: audit.invariant.rejectedReasons, deduped: false };
|
|
216
217
|
});
|
|
217
218
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { isAbsolute, join, resolve, sep } from "node:path";
|
|
3
3
|
|
|
4
|
+
import { readLedgerAt } from "./ledger.js";
|
|
4
5
|
import { normalizeUlwLoopSessionId, ulwLoopDir, ulwLoopStateLockPath } from "./paths.js";
|
|
6
|
+
import { readUlwLoopPlanSync } from "./plan-io.js";
|
|
5
7
|
import { isStateLockTimeout, withStateLockSync } from "./state-lock.js";
|
|
6
8
|
import type { UlwLoopItem, UlwLoopPlan } from "./types.js";
|
|
7
9
|
|
|
@@ -37,7 +39,7 @@ export function runStopResumeHook(input: unknown): string {
|
|
|
37
39
|
if (boulderContinuationWillFire(payload.cwd, payload.session_id)) return "";
|
|
38
40
|
const scope = { sessionId: payload.session_id } as const;
|
|
39
41
|
const stateDir = ulwLoopDir(payload.cwd, scope);
|
|
40
|
-
const plan = readPlan(
|
|
42
|
+
const plan = readPlan(payload.cwd, payload.session_id);
|
|
41
43
|
if (plan === null || plan.aggregateCompletion?.status === "complete") return "";
|
|
42
44
|
const goal = resumableGoal(plan);
|
|
43
45
|
if (goal === undefined) return "";
|
|
@@ -81,11 +83,10 @@ function consumeResumeBudgetLocked(lockPath: string, stateDir: string, goalId: s
|
|
|
81
83
|
}
|
|
82
84
|
}
|
|
83
85
|
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
// a separate file — a ledger append would change the count and self-reset.
|
|
86
|
+
// Hook budget counters are exempt from plan/audit commits. Count reconciled
|
|
87
|
+
// entries, not cache lines: a writer dying after link still made progress.
|
|
87
88
|
function consumeResumeBudget(stateDir: string, goalId: string): boolean {
|
|
88
|
-
const ledgerLineCount =
|
|
89
|
+
const ledgerLineCount = readLedgerAt(stateDir).length;
|
|
89
90
|
const counterPath = resolve(stateDir, `auto-resume-${goalId}.json`);
|
|
90
91
|
const stuckPath = resolve(stateDir, `auto-resume-${goalId}.stuck`);
|
|
91
92
|
// goals.json is untrusted input: a crafted goal id (e.g. `../../x`) must
|
|
@@ -119,24 +120,15 @@ function renderResumeDirective(plan: UlwLoopPlan, goal: UlwLoopItem, sessionId:
|
|
|
119
120
|
].join("\n");
|
|
120
121
|
}
|
|
121
122
|
|
|
122
|
-
function readPlan(
|
|
123
|
+
function readPlan(repoRoot: string, sessionId: string): UlwLoopPlan | null {
|
|
123
124
|
try {
|
|
124
|
-
return
|
|
125
|
+
return readUlwLoopPlanSync(repoRoot, { sessionId });
|
|
125
126
|
} catch (error) {
|
|
126
127
|
if (error instanceof Error) return null;
|
|
127
128
|
throw error;
|
|
128
129
|
}
|
|
129
130
|
}
|
|
130
131
|
|
|
131
|
-
function countLedgerLines(ledgerPath: string): number {
|
|
132
|
-
try {
|
|
133
|
-
return readFileSync(ledgerPath, "utf8").split("\n").filter(Boolean).length;
|
|
134
|
-
} catch (error) {
|
|
135
|
-
if (error instanceof Error) return 0;
|
|
136
|
-
throw error;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
132
|
function readCounter(counterPath: string): { count: number; ledgerLineCount: number } | null {
|
|
141
133
|
try {
|
|
142
134
|
if (!existsSync(counterPath)) return null;
|