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,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// components/ulw-loop/src/checkpoint.ts
|
|
4
|
-
import { existsSync as
|
|
5
|
-
import { readFile as
|
|
4
|
+
import { existsSync as existsSync4, statSync as statSync2 } from "node:fs";
|
|
5
|
+
import { readFile as readFile3 } from "node:fs/promises";
|
|
6
6
|
import { resolve as resolve3 } from "node:path";
|
|
7
7
|
|
|
8
8
|
// components/ulw-loop/src/codex-goal-snapshot.ts
|
|
@@ -178,15 +178,9 @@ function ulwLoopGoalsRelativePath(scope) {
|
|
|
178
178
|
function ulwLoopLedgerRelativePath(scope) {
|
|
179
179
|
return `${ulwLoopRelativeDir(scope)}/${ULW_LOOP_LEDGER}`;
|
|
180
180
|
}
|
|
181
|
-
function ulwLoopBriefPath(repoRoot, scope) {
|
|
182
|
-
return join(ulwLoopDir(repoRoot, scope), ULW_LOOP_BRIEF);
|
|
183
|
-
}
|
|
184
181
|
function ulwLoopGoalsPath(repoRoot, scope) {
|
|
185
182
|
return join(ulwLoopDir(repoRoot, scope), ULW_LOOP_GOALS);
|
|
186
183
|
}
|
|
187
|
-
function ulwLoopLedgerPath(repoRoot, scope) {
|
|
188
|
-
return join(ulwLoopDir(repoRoot, scope), ULW_LOOP_LEDGER);
|
|
189
|
-
}
|
|
190
184
|
function ulwLoopStateLockPath(repoRoot, scope) {
|
|
191
185
|
return join(ulwLoopDir(repoRoot, scope), ULW_LOOP_STATE_LOCK);
|
|
192
186
|
}
|
|
@@ -322,23 +316,142 @@ ${gateError.message}`, "ULW_LOOP_QUALITY_GATE_INVALID", {
|
|
|
322
316
|
});
|
|
323
317
|
}
|
|
324
318
|
|
|
319
|
+
// components/ulw-loop/src/plan-commit.ts
|
|
320
|
+
import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
|
|
321
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
322
|
+
import { linkSync, mkdirSync as mkdirSync2, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
323
|
+
import { rename, writeFile } from "node:fs/promises";
|
|
324
|
+
import { join as join4 } from "node:path";
|
|
325
|
+
|
|
326
|
+
// components/ulw-loop/src/ledger.ts
|
|
327
|
+
import { join as join3 } from "node:path";
|
|
328
|
+
|
|
329
|
+
// components/ulw-loop/src/plan-log.ts
|
|
330
|
+
import { existsSync as existsSync2, readdirSync, readFileSync } from "node:fs";
|
|
331
|
+
import { join as join2 } from "node:path";
|
|
332
|
+
function hasCode(error, code) {
|
|
333
|
+
return error instanceof Error && "code" in error && error.code === code;
|
|
334
|
+
}
|
|
335
|
+
function readOptional(path) {
|
|
336
|
+
try {
|
|
337
|
+
return readFileSync(path, "utf8");
|
|
338
|
+
} catch (error) {
|
|
339
|
+
if (hasCode(error, "ENOENT"))
|
|
340
|
+
return;
|
|
341
|
+
throw error;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
function logNames(dir) {
|
|
345
|
+
try {
|
|
346
|
+
return readdirSync(join2(dir, "revisions")).filter((name) => /^\d{8,}\.json$/.test(name)).sort();
|
|
347
|
+
} catch (error) {
|
|
348
|
+
if (hasCode(error, "ENOENT"))
|
|
349
|
+
return [];
|
|
350
|
+
throw error;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
function readRecord(dir, name) {
|
|
354
|
+
try {
|
|
355
|
+
const record = JSON.parse(readFileSync(join2(dir, "revisions", name), "utf8"));
|
|
356
|
+
return record.version === 1 && Number.isInteger(record.revision) && record.plan?.version === 1 && Array.isArray(record.plan.goals) && Array.isArray(record.ledger) ? record : undefined;
|
|
357
|
+
} catch (error) {
|
|
358
|
+
if (!(error instanceof SyntaxError))
|
|
359
|
+
throw error;
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
function readRecords(dir) {
|
|
364
|
+
const records = [];
|
|
365
|
+
for (const name of logNames(dir)) {
|
|
366
|
+
const record = readRecord(dir, name);
|
|
367
|
+
if (record !== undefined)
|
|
368
|
+
records.push(record);
|
|
369
|
+
}
|
|
370
|
+
return records.sort((a, b) => a.revision - b.revision);
|
|
371
|
+
}
|
|
372
|
+
function readNewestRecord(dir) {
|
|
373
|
+
const names = logNames(dir).sort((a, b) => Number.parseInt(b, 10) - Number.parseInt(a, 10));
|
|
374
|
+
for (const name of names) {
|
|
375
|
+
const record = readRecord(dir, name);
|
|
376
|
+
if (record !== undefined)
|
|
377
|
+
return record;
|
|
378
|
+
}
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
function reconcilePlan(dir) {
|
|
382
|
+
const raw = readOptional(join2(dir, "goals.json"));
|
|
383
|
+
let cached;
|
|
384
|
+
if (raw !== undefined) {
|
|
385
|
+
try {
|
|
386
|
+
cached = JSON.parse(raw);
|
|
387
|
+
} catch (error) {
|
|
388
|
+
if (!(error instanceof SyntaxError))
|
|
389
|
+
throw error;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
const latest = readNewestRecord(dir);
|
|
393
|
+
return latest !== undefined && latest.revision >= (cached?.revision ?? 0) ? latest.plan : cached;
|
|
394
|
+
}
|
|
395
|
+
function planExists(repoRoot, scope) {
|
|
396
|
+
const dir = ulwLoopDir(repoRoot, scope);
|
|
397
|
+
return existsSync2(join2(dir, "goals.json")) || logNames(dir).length > 0;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// components/ulw-loop/src/ledger.ts
|
|
401
|
+
function readLedgerAt(dir) {
|
|
402
|
+
const entries = new Map;
|
|
403
|
+
const lines = (readOptional(join3(dir, "ledger.jsonl")) ?? "").split(/\r?\n/);
|
|
404
|
+
for (const [index, line] of lines.entries()) {
|
|
405
|
+
if (line.trim().length === 0)
|
|
406
|
+
continue;
|
|
407
|
+
try {
|
|
408
|
+
const entry = JSON.parse(line);
|
|
409
|
+
entry.revision ??= 0;
|
|
410
|
+
entry.id ??= `legacy-${index + 1}`;
|
|
411
|
+
entries.set(entry.id, entry);
|
|
412
|
+
} catch (error) {
|
|
413
|
+
if (!(error instanceof SyntaxError))
|
|
414
|
+
throw error;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
for (const record of readRecords(dir))
|
|
418
|
+
for (const [seq, entry] of record.ledger.entries()) {
|
|
419
|
+
const id = entry.id ?? `${record.revision}-${seq}`;
|
|
420
|
+
entries.set(id, { ...entry, revision: record.revision, id });
|
|
421
|
+
}
|
|
422
|
+
const reset = reconcilePlan(dir)?.ledgerResetRevision ?? 0;
|
|
423
|
+
const sequence = (entry) => Number(entry.id?.split("-").at(-1) ?? 0);
|
|
424
|
+
return [...entries.values()].filter((entry) => (entry.revision ?? 0) >= reset).sort((a, b) => (a.revision ?? 0) - (b.revision ?? 0) || sequence(a) - sequence(b));
|
|
425
|
+
}
|
|
426
|
+
function readLedger(repoRoot, scope) {
|
|
427
|
+
return readLedgerAt(ulwLoopDir(repoRoot, scope));
|
|
428
|
+
}
|
|
429
|
+
|
|
325
430
|
// components/ulw-loop/src/plan-io.ts
|
|
326
431
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
327
|
-
import {
|
|
328
|
-
import { appendFile, mkdir, readFile as readFile2, rename, writeFile } from "node:fs/promises";
|
|
329
|
-
import { createInterface } from "node:readline";
|
|
432
|
+
import { readdirSync as readdirSync2 } from "node:fs";
|
|
330
433
|
|
|
331
434
|
// components/ulw-loop/src/plan-missing-recovery.ts
|
|
332
435
|
var ULW_LOOP_CREATE_GOALS_COMMAND = 'omo-agent-toolkit ulw-loop create-goals --brief "<brief>" --json';
|
|
333
|
-
function
|
|
334
|
-
|
|
436
|
+
function createGoalsAction(surface) {
|
|
437
|
+
return surface === "omo-senpi" ? 'agentToolkit.createGoals({ brief: "<brief>" })' : ULW_LOOP_CREATE_GOALS_COMMAND;
|
|
438
|
+
}
|
|
439
|
+
function planMissingRecovery(existingSessionIds, surface = "lazycodex") {
|
|
440
|
+
const lines = [`Recovery: bootstrap the plan with \`${createGoalsAction(surface)}\`.`];
|
|
335
441
|
if (existingSessionIds.length === 0)
|
|
336
442
|
return { message: lines.join(`
|
|
337
443
|
`) };
|
|
338
|
-
lines.push(`Existing ulw-loop session ids under .omo/ulw-loop/: ${existingSessionIds.join(", ")}. Re-run with \`--session-id <id>\` to target one of them.`);
|
|
444
|
+
lines.push(surface === "omo-senpi" ? `Existing ulw-loop session ids under .omo/ulw-loop/: ${existingSessionIds.join(", ")}. The SDK is bound to the current session; resume the owning session to target its plan.` : `Existing ulw-loop session ids under .omo/ulw-loop/: ${existingSessionIds.join(", ")}. Re-run with \`--session-id <id>\` to target one of them.`);
|
|
339
445
|
return { message: lines.join(`
|
|
340
446
|
`), details: { existingSessionIds } };
|
|
341
447
|
}
|
|
448
|
+
function planMissingError(planPath, existingSessionIds, surface = "lazycodex") {
|
|
449
|
+
const recovery = planMissingRecovery(existingSessionIds, surface);
|
|
450
|
+
return new UlwLoopError(`No ulw-loop plan found at ${planPath}.
|
|
451
|
+
${recovery.message}`, "ULW_LOOP_PLAN_MISSING", {
|
|
452
|
+
...recovery.details === undefined ? {} : { details: recovery.details }
|
|
453
|
+
});
|
|
454
|
+
}
|
|
342
455
|
function sessionScopeRequiredMessage(flag, existingSessionIds) {
|
|
343
456
|
const lines = [
|
|
344
457
|
"No ulw-loop session scope: neither the session env (OMO_ULW_LOOP_SESSION_ID / CODEX_SESSION_ID / CODEX_THREAD_ID / PI_SESSION_ID) nor the flag names this run, and the shared .omo/ulw-loop root is never used implicitly because every session in this directory would read and overwrite it.",
|
|
@@ -361,155 +474,207 @@ function sessionIdRequiredMessage(flag) {
|
|
|
361
474
|
|
|
362
475
|
// components/ulw-loop/src/state-lock.ts
|
|
363
476
|
import { randomUUID } from "node:crypto";
|
|
364
|
-
import { closeSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, writeSync } from "node:fs";
|
|
477
|
+
import { closeSync, ftruncateSync, mkdirSync, openSync, readFileSync as readFileSync2, statSync, unlinkSync, writeSync } from "node:fs";
|
|
365
478
|
import { dirname } from "node:path";
|
|
366
|
-
import { setTimeout as sleep } from "node:timers/promises";
|
|
367
479
|
var ULW_LOOP_LOCK_TIMEOUT_CODE = "ULW_LOOP_LOCK_TIMEOUT";
|
|
368
480
|
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
369
481
|
var DEFAULT_STALE_MS = 60000;
|
|
370
|
-
var
|
|
371
|
-
var MAX_DELAY_MS = 100;
|
|
482
|
+
var DEFAULT_LEASE_MS = 30000;
|
|
372
483
|
var SLEEP_CELL = new Int32Array(new SharedArrayBuffer(4));
|
|
484
|
+
var systemClock = {
|
|
485
|
+
now: Date.now,
|
|
486
|
+
schedule(fn, ms) {
|
|
487
|
+
const timer = setTimeout(fn, ms);
|
|
488
|
+
return {
|
|
489
|
+
unref: () => {
|
|
490
|
+
timer.unref();
|
|
491
|
+
},
|
|
492
|
+
cancel: () => clearTimeout(timer)
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
};
|
|
373
496
|
async function withStateLock(lockPath, fn, options = {}) {
|
|
374
|
-
const
|
|
497
|
+
const holder = await acquireAsync(lockPath, options);
|
|
498
|
+
const clock = options.clock ?? systemClock;
|
|
499
|
+
let timer;
|
|
500
|
+
let heartbeatError;
|
|
501
|
+
const beat = () => {
|
|
502
|
+
try {
|
|
503
|
+
holder.record.leaseUntil = clock.now() + (options.leaseMs ?? DEFAULT_LEASE_MS);
|
|
504
|
+
writeRecord(holder);
|
|
505
|
+
schedule();
|
|
506
|
+
} catch (error) {
|
|
507
|
+
heartbeatError = error;
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
const schedule = () => {
|
|
511
|
+
if (options.heartbeatMs === 0)
|
|
512
|
+
return;
|
|
513
|
+
timer = clock.schedule(beat, options.heartbeatMs ?? (options.leaseMs ?? DEFAULT_LEASE_MS) / 3);
|
|
514
|
+
timer.unref();
|
|
515
|
+
};
|
|
516
|
+
schedule();
|
|
375
517
|
try {
|
|
376
|
-
|
|
518
|
+
const result = await fn(holder.record.token);
|
|
519
|
+
if (heartbeatError !== undefined)
|
|
520
|
+
throw heartbeatError;
|
|
521
|
+
return result;
|
|
377
522
|
} finally {
|
|
378
|
-
|
|
523
|
+
timer?.cancel();
|
|
524
|
+
closeSync(holder.fd);
|
|
525
|
+
release(lockPath, holder.record.token);
|
|
379
526
|
}
|
|
380
527
|
}
|
|
381
528
|
function withStateLockSync(lockPath, fn, options = {}) {
|
|
382
|
-
const
|
|
529
|
+
const holder = acquireSync(lockPath, options);
|
|
383
530
|
try {
|
|
384
531
|
return fn();
|
|
385
532
|
} finally {
|
|
386
|
-
|
|
533
|
+
closeSync(holder.fd);
|
|
534
|
+
release(lockPath, holder.record.token);
|
|
387
535
|
}
|
|
388
536
|
}
|
|
389
537
|
function isStateLockTimeout(error) {
|
|
390
538
|
return error instanceof UlwLoopError && error.code === ULW_LOOP_LOCK_TIMEOUT_CODE;
|
|
391
539
|
}
|
|
392
540
|
async function acquireAsync(lockPath, options) {
|
|
393
|
-
const
|
|
394
|
-
const
|
|
541
|
+
const clock = options.clock ?? systemClock;
|
|
542
|
+
const deadline = clock.now() + (options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
395
543
|
mkdirSync(dirname(lockPath), { recursive: true });
|
|
396
544
|
for (let attempt = 0;; ) {
|
|
397
|
-
const outcome = attemptOnce(lockPath,
|
|
545
|
+
const outcome = attemptOnce(lockPath, options, clock.now(), options.leaseMs ?? DEFAULT_LEASE_MS);
|
|
398
546
|
if (outcome.kind === "acquired")
|
|
399
|
-
return outcome.
|
|
547
|
+
return outcome.holder;
|
|
400
548
|
if (outcome.kind === "retry")
|
|
401
549
|
continue;
|
|
402
|
-
if (
|
|
403
|
-
throw lockTimeout(lockPath, options
|
|
404
|
-
await
|
|
550
|
+
if (clock.now() >= deadline)
|
|
551
|
+
throw lockTimeout(lockPath, options);
|
|
552
|
+
await new Promise((resolve) => clock.schedule(resolve, backoffMs(attempt)));
|
|
405
553
|
attempt += 1;
|
|
406
554
|
}
|
|
407
555
|
}
|
|
408
556
|
function acquireSync(lockPath, options) {
|
|
409
557
|
const deadline = Date.now() + (options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
410
|
-
const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
|
|
411
558
|
mkdirSync(dirname(lockPath), { recursive: true });
|
|
412
559
|
for (let attempt = 0;; ) {
|
|
413
|
-
const outcome = attemptOnce(lockPath,
|
|
560
|
+
const outcome = attemptOnce(lockPath, options, Date.now());
|
|
414
561
|
if (outcome.kind === "acquired")
|
|
415
|
-
return outcome.
|
|
562
|
+
return outcome.holder;
|
|
416
563
|
if (outcome.kind === "retry")
|
|
417
564
|
continue;
|
|
418
565
|
if (Date.now() >= deadline)
|
|
419
|
-
throw lockTimeout(lockPath, options
|
|
566
|
+
throw lockTimeout(lockPath, options);
|
|
420
567
|
Atomics.wait(SLEEP_CELL, 0, 0, backoffMs(attempt));
|
|
421
568
|
attempt += 1;
|
|
422
569
|
}
|
|
423
570
|
}
|
|
424
|
-
function attemptOnce(lockPath,
|
|
571
|
+
function attemptOnce(lockPath, options, now, leaseMs) {
|
|
425
572
|
try {
|
|
426
|
-
const
|
|
427
|
-
if (
|
|
428
|
-
return { kind: "acquired",
|
|
429
|
-
const snapshot = readSnapshot(lockPath);
|
|
573
|
+
const holder = tryCreate(lockPath, now, leaseMs);
|
|
574
|
+
if (holder !== null)
|
|
575
|
+
return { kind: "acquired", holder };
|
|
576
|
+
const snapshot = readSnapshot(lockPath, now);
|
|
430
577
|
if (snapshot === null)
|
|
431
578
|
return { kind: "retry" };
|
|
432
|
-
|
|
579
|
+
const record = snapshot.record;
|
|
580
|
+
const stale = record === null ? snapshot.ageMs > (options.staleMs ?? DEFAULT_STALE_MS) : !isProcessAlive(record.pid) || record.leaseUntil !== undefined && now > record.leaseUntil;
|
|
581
|
+
if (stale && reclaim(lockPath, snapshot.raw))
|
|
433
582
|
return { kind: "retry" };
|
|
434
583
|
return { kind: "wait" };
|
|
435
584
|
} catch (error) {
|
|
436
|
-
if (
|
|
585
|
+
if (hasCode2(error, "EINTR"))
|
|
437
586
|
return { kind: "wait" };
|
|
438
587
|
throw error;
|
|
439
588
|
}
|
|
440
589
|
}
|
|
441
|
-
function
|
|
590
|
+
function writeRecord(holder) {
|
|
591
|
+
const buf = Buffer.from(JSON.stringify(holder.record));
|
|
592
|
+
ftruncateSync(holder.fd, 0);
|
|
593
|
+
let offset = 0;
|
|
594
|
+
while (offset < buf.length) {
|
|
595
|
+
const written = writeSync(holder.fd, buf, offset, buf.length - offset, offset);
|
|
596
|
+
if (written === 0)
|
|
597
|
+
throw new Error("State lock write made no progress.");
|
|
598
|
+
offset += written;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
function tryCreate(lockPath, now, leaseMs) {
|
|
442
602
|
let fd;
|
|
443
603
|
try {
|
|
444
604
|
fd = openSync(lockPath, "wx");
|
|
445
605
|
} catch (error) {
|
|
446
|
-
if (
|
|
606
|
+
if (hasCode2(error, "EEXIST"))
|
|
447
607
|
return null;
|
|
448
608
|
throw error;
|
|
449
609
|
}
|
|
450
|
-
const record = {
|
|
610
|
+
const record = {
|
|
611
|
+
pid: process.pid,
|
|
612
|
+
createdAt: new Date(now).toISOString(),
|
|
613
|
+
token: randomUUID(),
|
|
614
|
+
...leaseMs === undefined ? {} : { leaseUntil: now + leaseMs }
|
|
615
|
+
};
|
|
616
|
+
const holder = { fd, record };
|
|
451
617
|
try {
|
|
452
|
-
|
|
618
|
+
writeRecord(holder);
|
|
453
619
|
} catch (error) {
|
|
454
620
|
closeSync(fd);
|
|
455
621
|
try {
|
|
456
622
|
unlinkSync(lockPath);
|
|
457
|
-
} catch {
|
|
623
|
+
} catch (cleanup) {
|
|
624
|
+
if (!hasCode2(cleanup, "ENOENT"))
|
|
625
|
+
throw cleanup;
|
|
626
|
+
}
|
|
458
627
|
throw error;
|
|
459
628
|
}
|
|
460
|
-
|
|
461
|
-
return record.token;
|
|
629
|
+
return holder;
|
|
462
630
|
}
|
|
463
|
-
function readSnapshot(lockPath) {
|
|
631
|
+
function readSnapshot(lockPath, now) {
|
|
464
632
|
try {
|
|
465
|
-
const raw =
|
|
466
|
-
|
|
467
|
-
return { raw, record: parseRecord(raw), ageMs };
|
|
633
|
+
const raw = readFileSync2(lockPath, "utf8");
|
|
634
|
+
return { raw, record: parseRecord(raw), ageMs: now - statSync(lockPath).mtimeMs };
|
|
468
635
|
} catch (error) {
|
|
469
|
-
if (
|
|
636
|
+
if (hasCode2(error, "ENOENT"))
|
|
470
637
|
return null;
|
|
471
638
|
throw error;
|
|
472
639
|
}
|
|
473
640
|
}
|
|
474
641
|
function parseRecord(raw) {
|
|
475
642
|
try {
|
|
476
|
-
const
|
|
477
|
-
if (typeof
|
|
643
|
+
const record = JSON.parse(raw);
|
|
644
|
+
if (typeof record !== "object" || record === null)
|
|
645
|
+
return null;
|
|
646
|
+
if (!("pid" in record) || typeof record.pid !== "number" || !Number.isInteger(record.pid) || record.pid <= 0)
|
|
478
647
|
return null;
|
|
479
|
-
|
|
480
|
-
const pid = record["pid"];
|
|
481
|
-
const createdAt = record["createdAt"];
|
|
482
|
-
const token = record["token"];
|
|
483
|
-
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0 || typeof createdAt !== "string")
|
|
648
|
+
if (!("createdAt" in record) || typeof record.createdAt !== "string")
|
|
484
649
|
return null;
|
|
485
|
-
if (typeof token !== "string" || token.length === 0)
|
|
650
|
+
if (!("token" in record) || typeof record.token !== "string" || record.token.length === 0)
|
|
486
651
|
return null;
|
|
487
|
-
return {
|
|
652
|
+
return {
|
|
653
|
+
pid: record.pid,
|
|
654
|
+
createdAt: record.createdAt,
|
|
655
|
+
token: record.token,
|
|
656
|
+
..."leaseUntil" in record && typeof record.leaseUntil === "number" ? { leaseUntil: record.leaseUntil } : {}
|
|
657
|
+
};
|
|
488
658
|
} catch (error) {
|
|
489
659
|
if (error instanceof SyntaxError)
|
|
490
660
|
return null;
|
|
491
661
|
throw error;
|
|
492
662
|
}
|
|
493
663
|
}
|
|
494
|
-
function isStale(snapshot, staleMs) {
|
|
495
|
-
if (snapshot.record === null)
|
|
496
|
-
return snapshot.ageMs > staleMs;
|
|
497
|
-
return !isProcessAlive(snapshot.record.pid);
|
|
498
|
-
}
|
|
499
664
|
function isProcessAlive(pid) {
|
|
500
665
|
try {
|
|
501
666
|
process.kill(pid, 0);
|
|
502
667
|
return true;
|
|
503
668
|
} catch (error) {
|
|
504
|
-
if (
|
|
669
|
+
if (hasCode2(error, "ESRCH"))
|
|
505
670
|
return false;
|
|
506
|
-
if (
|
|
671
|
+
if (hasCode2(error, "EPERM"))
|
|
507
672
|
return true;
|
|
508
673
|
throw error;
|
|
509
674
|
}
|
|
510
675
|
}
|
|
511
676
|
function reclaim(lockPath, expectedRaw) {
|
|
512
|
-
const current = readSnapshot(lockPath);
|
|
677
|
+
const current = readSnapshot(lockPath, Date.now());
|
|
513
678
|
if (current === null)
|
|
514
679
|
return true;
|
|
515
680
|
if (current.raw !== expectedRaw)
|
|
@@ -517,63 +682,102 @@ function reclaim(lockPath, expectedRaw) {
|
|
|
517
682
|
try {
|
|
518
683
|
unlinkSync(lockPath);
|
|
519
684
|
} catch (error) {
|
|
520
|
-
if (
|
|
685
|
+
if (hasCode2(error, "EPERM") || hasCode2(error, "EACCES"))
|
|
686
|
+
return false;
|
|
687
|
+
if (!hasCode2(error, "ENOENT"))
|
|
521
688
|
throw error;
|
|
522
689
|
}
|
|
523
690
|
return true;
|
|
524
691
|
}
|
|
525
692
|
function release(lockPath, token) {
|
|
526
|
-
|
|
527
|
-
if (current === null || current.record?.token !== token)
|
|
693
|
+
if (readSnapshot(lockPath, Date.now())?.record?.token !== token)
|
|
528
694
|
return;
|
|
529
695
|
try {
|
|
530
696
|
unlinkSync(lockPath);
|
|
531
697
|
} catch (error) {
|
|
532
|
-
if (!
|
|
698
|
+
if (!hasCode2(error, "ENOENT"))
|
|
533
699
|
throw error;
|
|
534
700
|
}
|
|
535
701
|
}
|
|
536
702
|
function backoffMs(attempt) {
|
|
537
|
-
|
|
538
|
-
return exponential + Math.random() * MIN_DELAY_MS;
|
|
703
|
+
return Math.min(100, 5 * 2 ** attempt) + Math.random() * 5;
|
|
539
704
|
}
|
|
540
|
-
function lockTimeout(lockPath,
|
|
541
|
-
const holder = readSnapshot(lockPath)?.record;
|
|
542
|
-
const
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
lockPath,
|
|
546
|
-
timeoutMs,
|
|
547
|
-
...holder === undefined || holder === null ? {} : { holderPid: holder.pid }
|
|
548
|
-
}
|
|
549
|
-
});
|
|
705
|
+
function lockTimeout(lockPath, options) {
|
|
706
|
+
const holder = readSnapshot(lockPath, Date.now())?.record;
|
|
707
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
708
|
+
const owner = holder == null ? "another process" : `pid ${holder.pid}`;
|
|
709
|
+
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 } } });
|
|
550
710
|
}
|
|
551
|
-
function
|
|
711
|
+
function hasCode2(error, code) {
|
|
552
712
|
return error instanceof Error && "code" in error && error.code === code;
|
|
553
713
|
}
|
|
554
714
|
|
|
555
715
|
// components/ulw-loop/src/plan-io.ts
|
|
716
|
+
function readLedger2(repoRoot, scope) {
|
|
717
|
+
const lockPath = ulwLoopStateLockPath(repoRoot, scope);
|
|
718
|
+
if (heldLocks.getStore()?.has(lockPath)) {
|
|
719
|
+
assertStateLockOwned(lockPath);
|
|
720
|
+
materializeSync(ulwLoopDir(repoRoot, scope));
|
|
721
|
+
}
|
|
722
|
+
return readLedger(repoRoot, scope);
|
|
723
|
+
}
|
|
556
724
|
var LEGACY_OBJECTIVE_PREFIX = `Complete all ulw-loop stories in ${ULW_LOOP_DIR}/${ULW_LOOP_GOALS}: `;
|
|
557
725
|
var LEGACY_OBJECTIVE = `Complete all ulw-loop stories listed in ${ULW_LOOP_DIR}/${ULW_LOOP_GOALS}. Use ${ULW_LOOP_DIR}/${ULW_LOOP_LEDGER} as the durable audit trail.`;
|
|
558
726
|
var locks = new Map;
|
|
559
727
|
var heldLocks = new AsyncLocalStorage;
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
728
|
+
var lockOptions = new AsyncLocalStorage;
|
|
729
|
+
function tokenAt(lockPath) {
|
|
730
|
+
const raw = readOptional(lockPath);
|
|
731
|
+
if (raw === undefined)
|
|
732
|
+
return;
|
|
733
|
+
try {
|
|
734
|
+
const record = JSON.parse(raw);
|
|
735
|
+
return typeof record === "object" && record !== null && "token" in record && typeof record.token === "string" ? record.token : undefined;
|
|
736
|
+
} catch (error) {
|
|
737
|
+
if (error instanceof SyntaxError)
|
|
738
|
+
return;
|
|
739
|
+
throw error;
|
|
740
|
+
}
|
|
565
741
|
}
|
|
566
|
-
function
|
|
567
|
-
|
|
742
|
+
function assertStateLockOwned(lockPath) {
|
|
743
|
+
const context = heldLocks.getStore()?.get(lockPath);
|
|
744
|
+
if (context === undefined)
|
|
745
|
+
return;
|
|
746
|
+
if (tokenAt(lockPath) !== context.token)
|
|
747
|
+
throw new UlwLoopError("The ulw-loop mutation lock changed owners.", "ULW_LOOP_LOCK_LOST");
|
|
748
|
+
}
|
|
749
|
+
function migrationEntries(plan) {
|
|
750
|
+
for (const context of heldLocks.getStore()?.values() ?? []) {
|
|
751
|
+
const entries = context.migrations.get(plan.goalsPath);
|
|
752
|
+
if (entries !== undefined) {
|
|
753
|
+
context.migrations.delete(plan.goalsPath);
|
|
754
|
+
return entries;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
return [];
|
|
568
758
|
}
|
|
569
|
-
async function withUlwLoopMutationLock(repoRoot, scopeOrFn, maybeFn) {
|
|
759
|
+
async function withUlwLoopMutationLock(repoRoot, scopeOrFn, maybeFn, options = {}) {
|
|
570
760
|
const scope = typeof scopeOrFn === "function" ? undefined : scopeOrFn;
|
|
571
761
|
const fn = typeof scopeOrFn === "function" ? scopeOrFn : maybeFn;
|
|
572
762
|
if (fn === undefined)
|
|
573
763
|
throw new UlwLoopError("Missing ulw-loop mutation body.", "ULW_LOOP_LOCK_BODY_MISSING");
|
|
574
764
|
const lockKey = `${repoRoot}\x00${ulwLoopRelativeDir(scope)}`;
|
|
575
765
|
const lockPath = ulwLoopStateLockPath(repoRoot, scope);
|
|
576
|
-
const locked = () => withStateLock(lockPath, () =>
|
|
766
|
+
const locked = () => withStateLock(lockPath, async (token) => {
|
|
767
|
+
return heldLocks.run(new Map([...heldLocks.getStore() ?? [], [lockPath, { token, migrations: new Map }]]), async () => {
|
|
768
|
+
for (let attempt = 0;; attempt += 1) {
|
|
769
|
+
try {
|
|
770
|
+
return await fn();
|
|
771
|
+
} catch (error) {
|
|
772
|
+
if (!(error instanceof UlwLoopError) || error.code !== "ULW_LOOP_PUBLISH_CONFLICT")
|
|
773
|
+
throw error;
|
|
774
|
+
assertStateLockOwned(lockPath);
|
|
775
|
+
if (attempt !== 0)
|
|
776
|
+
throw error;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
});
|
|
780
|
+
}, { ...lockOptions.getStore(), ...options });
|
|
577
781
|
const prior = locks.get(lockKey) ?? Promise.resolve(undefined);
|
|
578
782
|
const run = prior.then(locked, locked);
|
|
579
783
|
const gate = run.then(() => {
|
|
@@ -588,98 +792,140 @@ async function withUlwLoopMutationLock(repoRoot, scopeOrFn, maybeFn) {
|
|
|
588
792
|
});
|
|
589
793
|
return run;
|
|
590
794
|
}
|
|
591
|
-
|
|
795
|
+
function readUlwLoopPlanSync(repoRoot, scope) {
|
|
592
796
|
const path = ulwLoopGoalsPath(repoRoot, scope);
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
if (!hasCode2(error, "ENOENT"))
|
|
598
|
-
throw error;
|
|
599
|
-
const recovery = planMissingRecovery(listUlwLoopSessionIds(repoRoot));
|
|
600
|
-
throw new UlwLoopError(`No ulw-loop plan found at ${repoRelative(path, repoRoot)}.
|
|
601
|
-
${recovery.message}`, "ULW_LOOP_PLAN_MISSING", { cause: error, ...recovery.details === undefined ? {} : { details: recovery.details } });
|
|
602
|
-
}
|
|
603
|
-
const parsed = JSON.parse(raw);
|
|
604
|
-
if (parsed.version !== 1 || !Array.isArray(parsed.goals)) {
|
|
797
|
+
const parsed = reconcilePlan(ulwLoopDir(repoRoot, scope));
|
|
798
|
+
if (parsed === undefined)
|
|
799
|
+
throw planMissingError(repoRelative(path, repoRoot), listUlwLoopSessionIds(repoRoot));
|
|
800
|
+
if (parsed.version !== 1 || !Array.isArray(parsed.goals))
|
|
605
801
|
throw new UlwLoopError(`Invalid ulw-loop plan at ${repoRelative(path, repoRoot)}.`, "ULW_LOOP_PLAN_INVALID");
|
|
606
|
-
}
|
|
607
802
|
const previousObjective = parsed.codexObjective;
|
|
608
|
-
if ((parsed.codexGoalMode ?? "per_story") === "aggregate" &&
|
|
609
|
-
if (!(heldLocks.getStore()?.has(`${repoRoot}\x00${ulwLoopRelativeDir(scope)}`) ?? false)) {
|
|
610
|
-
throw new UlwLoopError(`The ulw-loop plan at ${repoRelative(path, repoRoot)} carries a legacy enumerated aggregate objective that must be migrated before reads continue. Run any state-mutating ulw-loop command once (e.g. \`record-evidence\`, \`steer\`, \`checkpoint\`) to migrate it under the state lock, then retry.`, "ULW_LOOP_MIGRATION_REQUIRED");
|
|
611
|
-
}
|
|
612
|
-
const now = iso();
|
|
803
|
+
if ((parsed.codexGoalMode ?? "per_story") === "aggregate" && previousObjective !== undefined && (previousObjective === LEGACY_OBJECTIVE || previousObjective.startsWith(LEGACY_OBJECTIVE_PREFIX))) {
|
|
613
804
|
parsed.codexObjective = aggregateCodexObjectiveForScope(scope);
|
|
614
805
|
parsed.codexObjectiveAliases = [...new Set([...parsed.codexObjectiveAliases ?? [], previousObjective])];
|
|
615
|
-
parsed.
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
}, scope);
|
|
806
|
+
heldLocks.getStore()?.get(ulwLoopStateLockPath(repoRoot, scope))?.migrations.set(parsed.goalsPath, [
|
|
807
|
+
{
|
|
808
|
+
at: iso(),
|
|
809
|
+
kind: "aggregate_objective_migrated",
|
|
810
|
+
before: { codexObjective: previousObjective },
|
|
811
|
+
after: { codexObjective: parsed.codexObjective }
|
|
812
|
+
}
|
|
813
|
+
]);
|
|
624
814
|
}
|
|
625
815
|
return parsed;
|
|
626
816
|
}
|
|
817
|
+
async function readUlwLoopPlan(repoRoot, scope) {
|
|
818
|
+
if (heldLocks.getStore()?.has(ulwLoopStateLockPath(repoRoot, scope))) {
|
|
819
|
+
assertStateLockOwned(ulwLoopStateLockPath(repoRoot, scope));
|
|
820
|
+
await materialize(ulwLoopDir(repoRoot, scope));
|
|
821
|
+
}
|
|
822
|
+
return readUlwLoopPlanSync(repoRoot, scope);
|
|
823
|
+
}
|
|
627
824
|
function listUlwLoopSessionIds(repoRoot) {
|
|
628
825
|
try {
|
|
629
|
-
return
|
|
826
|
+
return readdirSync2(ulwLoopDir(repoRoot), { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
|
|
630
827
|
} catch {
|
|
631
828
|
return [];
|
|
632
829
|
}
|
|
633
830
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
const path = ulwLoopGoalsPath(repoRoot, scope);
|
|
637
|
-
const tmpPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
638
|
-
await writeFile(tmpPath, `${JSON.stringify(plan, null, 2)}
|
|
639
|
-
`, "utf8");
|
|
640
|
-
await rename(tmpPath, path);
|
|
641
|
-
}
|
|
642
|
-
async function appendLedger(repoRoot, entry, scope) {
|
|
643
|
-
await appendLedgerEntries(repoRoot, [entry], scope);
|
|
831
|
+
function isSteeringKind(value) {
|
|
832
|
+
return value === "steering_accepted" || value === "steering_rejected" || value === "criteria_revised" || value === "batch_updated";
|
|
644
833
|
}
|
|
645
|
-
async function
|
|
646
|
-
|
|
647
|
-
return;
|
|
648
|
-
await mkdir(ulwLoopDir(repoRoot, scope), { recursive: true });
|
|
649
|
-
await appendFile(ulwLoopLedgerPath(repoRoot, scope), `${entries.map((entry) => JSON.stringify(entry)).join(`
|
|
650
|
-
`)}
|
|
651
|
-
`, "utf8");
|
|
834
|
+
async function findAcceptedSteeringLedgerEntry(repoRoot, key, scope) {
|
|
835
|
+
return readLedger2(repoRoot, scope).find((entry) => isSteeringKind(entry.kind) && entry.steering?.invariant.accepted === true && (entry.idempotencyKey === key || entry.steering.idempotencyKey === key || entry.steering.promptSignature === key));
|
|
652
836
|
}
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
837
|
+
|
|
838
|
+
// components/ulw-loop/src/plan-commit.ts
|
|
839
|
+
var hooks = new AsyncLocalStorage2;
|
|
840
|
+
async function beforePlanMutation() {
|
|
841
|
+
await hooks.getStore()?.beforeMutation?.();
|
|
842
|
+
}
|
|
843
|
+
async function writeViewFile(path, content) {
|
|
844
|
+
await (hooks.getStore()?.writeView ?? writeFile)(path, content);
|
|
845
|
+
}
|
|
846
|
+
async function commit(repoRoot, scope, mutation) {
|
|
847
|
+
const { plan } = mutation;
|
|
848
|
+
const dir = ulwLoopDir(repoRoot, scope);
|
|
849
|
+
assertStateLockOwned(ulwLoopStateLockPath(repoRoot, scope));
|
|
850
|
+
await hooks.getStore()?.beforeCommit?.();
|
|
851
|
+
const next = (plan.revision ?? 0) + 1;
|
|
852
|
+
const brief = plan.brief ?? readOptional(join4(dir, "brief.md")) ?? "";
|
|
853
|
+
const entries = [...migrationEntries(plan), ...mutation.entries].map((entry, seq) => ({
|
|
854
|
+
...entry,
|
|
855
|
+
revision: next,
|
|
856
|
+
id: `${next}-${seq}`
|
|
857
|
+
}));
|
|
858
|
+
const record = {
|
|
859
|
+
version: 1,
|
|
860
|
+
revision: next,
|
|
861
|
+
plan: { ...plan, revision: next, brief },
|
|
862
|
+
ledger: entries
|
|
863
|
+
};
|
|
864
|
+
mkdirSync2(join4(dir, "revisions"), { recursive: true });
|
|
865
|
+
mkdirSync2(join4(dir, "tmp"), { recursive: true });
|
|
866
|
+
const temp = join4(dir, "tmp", `${process.pid}-${randomUUID2()}.json`);
|
|
656
867
|
try {
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
868
|
+
writeFileSync(temp, `${JSON.stringify(record)}
|
|
869
|
+
`, { flag: "wx" });
|
|
870
|
+
try {
|
|
871
|
+
(hooks.getStore()?.link ?? linkSync)(temp, join4(dir, "revisions", `${String(next).padStart(8, "0")}.json`));
|
|
872
|
+
} catch (error) {
|
|
873
|
+
if (hasCode(error, "EEXIST"))
|
|
874
|
+
throw new UlwLoopError("Another writer published the read revision.", "ULW_LOOP_PUBLISH_CONFLICT", {
|
|
875
|
+
cause: error
|
|
876
|
+
});
|
|
877
|
+
if (["EPERM", "ENOTSUP", "EXDEV"].some((code) => hasCode(error, code)))
|
|
878
|
+
throw new UlwLoopError("Immutable ulw-loop publication requires atomic hard links.", "ULW_LOOP_PUBLISH_UNSUPPORTED_FS", { cause: error });
|
|
663
879
|
throw error;
|
|
880
|
+
}
|
|
664
881
|
} finally {
|
|
665
|
-
|
|
666
|
-
stream.destroy();
|
|
882
|
+
rmSync(temp, { force: true });
|
|
667
883
|
}
|
|
884
|
+
plan.revision = next;
|
|
885
|
+
plan.brief = brief;
|
|
886
|
+
for (const [index, entry] of mutation.entries.entries())
|
|
887
|
+
Object.assign(entry, entries[index + entries.length - mutation.entries.length]);
|
|
888
|
+
await hooks.getStore()?.afterCommit?.();
|
|
889
|
+
await materialize(dir);
|
|
890
|
+
}
|
|
891
|
+
function viewContents(dir) {
|
|
892
|
+
const plan = reconcilePlan(dir);
|
|
893
|
+
if (plan === undefined || readRecords(dir).length === 0)
|
|
894
|
+
return [];
|
|
895
|
+
const ledger = readLedgerAt(dir);
|
|
896
|
+
const views = [
|
|
897
|
+
["goals.json", `${JSON.stringify(plan, null, 2)}
|
|
898
|
+
`],
|
|
899
|
+
["ledger.jsonl", ledger.length === 0 ? "" : `${ledger.map((entry) => JSON.stringify(entry)).join(`
|
|
900
|
+
`)}
|
|
901
|
+
`],
|
|
902
|
+
["brief.md", plan.brief ?? readOptional(join4(dir, "brief.md")) ?? ""]
|
|
903
|
+
];
|
|
904
|
+
return views.filter(([name, content]) => readOptional(join4(dir, name)) !== content);
|
|
668
905
|
}
|
|
669
|
-
|
|
670
|
-
const
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
906
|
+
function materializeSync(dir) {
|
|
907
|
+
for (const [name, content] of viewContents(dir)) {
|
|
908
|
+
mkdirSync2(join4(dir, "tmp"), { recursive: true });
|
|
909
|
+
const temp = join4(dir, "tmp", `${process.pid}-${randomUUID2()}-${name}`);
|
|
910
|
+
try {
|
|
911
|
+
writeFileSync(temp, content, { flag: "wx" });
|
|
912
|
+
renameSync(temp, join4(dir, name));
|
|
913
|
+
} finally {
|
|
914
|
+
rmSync(temp, { force: true });
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
async function materialize(dir) {
|
|
919
|
+
for (const [name, content] of viewContents(dir)) {
|
|
920
|
+
mkdirSync2(join4(dir, "tmp"), { recursive: true });
|
|
921
|
+
const temp = join4(dir, "tmp", `${process.pid}-${randomUUID2()}-${name}`);
|
|
922
|
+
try {
|
|
923
|
+
await writeViewFile(temp, content);
|
|
924
|
+
await rename(temp, join4(dir, name));
|
|
925
|
+
} finally {
|
|
926
|
+
rmSync(temp, { force: true });
|
|
927
|
+
}
|
|
681
928
|
}
|
|
682
|
-
return;
|
|
683
929
|
}
|
|
684
930
|
|
|
685
931
|
// components/ulw-loop/src/evidence.ts
|
|
@@ -729,7 +975,6 @@ async function recordEvidence(repoRoot, args, scope) {
|
|
|
729
975
|
criterion.notes = args.notes;
|
|
730
976
|
goal.updatedAt = capturedAt;
|
|
731
977
|
plan.updatedAt = capturedAt;
|
|
732
|
-
await writePlan(repoRoot, plan, scope);
|
|
733
978
|
const ledgerEntry = {
|
|
734
979
|
at: capturedAt,
|
|
735
980
|
kind,
|
|
@@ -741,7 +986,7 @@ async function recordEvidence(repoRoot, args, scope) {
|
|
|
741
986
|
before: { status: prevStatus },
|
|
742
987
|
after: { goalId: goal.id, criterionId: criterion.id, status: args.status, evidence, capturedAt, prevStatus }
|
|
743
988
|
};
|
|
744
|
-
await
|
|
989
|
+
await commit(repoRoot, scope, { plan, entries: [ledgerEntry] });
|
|
745
990
|
return { plan, goal, criterion, ledgerEntry };
|
|
746
991
|
});
|
|
747
992
|
}
|
|
@@ -1011,8 +1256,8 @@ function adversarialVerdict(row, field) {
|
|
|
1011
1256
|
}
|
|
1012
1257
|
|
|
1013
1258
|
// components/ulw-loop/src/surface.ts
|
|
1014
|
-
import { existsSync as
|
|
1015
|
-
import { dirname as dirname2, join as
|
|
1259
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
1260
|
+
import { dirname as dirname2, join as join5 } from "node:path";
|
|
1016
1261
|
import { fileURLToPath } from "node:url";
|
|
1017
1262
|
var REVIEWER_ROLES_BY_SURFACE = {
|
|
1018
1263
|
lazycodex: {
|
|
@@ -1066,14 +1311,14 @@ function resolveToolkitSurface(options) {
|
|
|
1066
1311
|
if (fromEnv !== null)
|
|
1067
1312
|
return fromEnv;
|
|
1068
1313
|
const entryDir = options?.entryDir ?? dirname2(fileURLToPath(import.meta.url));
|
|
1069
|
-
const markerPath =
|
|
1314
|
+
const markerPath = join5(entryDir, SURFACE_MARKER_FILENAME);
|
|
1070
1315
|
return readMarkerSurface(markerPath) ?? "lazycodex";
|
|
1071
1316
|
}
|
|
1072
1317
|
function readMarkerSurface(markerPath) {
|
|
1073
1318
|
try {
|
|
1074
|
-
if (!
|
|
1319
|
+
if (!existsSync3(markerPath))
|
|
1075
1320
|
return null;
|
|
1076
|
-
const parsed = JSON.parse(
|
|
1321
|
+
const parsed = JSON.parse(readFileSync3(markerPath, "utf8"));
|
|
1077
1322
|
return parseSurface(parsed["surface"]);
|
|
1078
1323
|
} catch (error) {
|
|
1079
1324
|
if (error instanceof Error)
|
|
@@ -1274,7 +1519,7 @@ function parseAdversarialCases(value, byId) {
|
|
|
1274
1519
|
}
|
|
1275
1520
|
|
|
1276
1521
|
// components/ulw-loop/src/cli-arg-parser.ts
|
|
1277
|
-
import { readFile as
|
|
1522
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
1278
1523
|
var VALUE_FLAGS = new Set("--brief --brief-file --session-id --codex-goal-mode --validation-batch-json --goal --goal-id --criterion-id --status --evidence --notes --codex-goal-json --quality-gate-json --kind --rationale --title --objective --target-goal-id --source --after-json --directive-json --directive-file --idempotency-key --proposals-json".split(" "));
|
|
1279
1524
|
var SUBCOMMANDS = new Set("create-goals status complete-goals criteria record-evidence checkpoint steer add-goal record-review-blockers".split(" "));
|
|
1280
1525
|
function hasFlag(argv, flag) {
|
|
@@ -1322,7 +1567,7 @@ async function readJsonInput(value) {
|
|
|
1322
1567
|
if (value === undefined)
|
|
1323
1568
|
return;
|
|
1324
1569
|
try {
|
|
1325
|
-
return JSON.parse(looksLikeJson(value) ? value : await
|
|
1570
|
+
return JSON.parse(looksLikeJson(value) ? value : await readFile2(value, "utf8"));
|
|
1326
1571
|
} catch (error) {
|
|
1327
1572
|
const message = error instanceof Error ? error.message : "unknown error";
|
|
1328
1573
|
throw new UlwLoopError(`Invalid JSON input: ${message}`, "ULW_LOOP_JSON_INPUT_INVALID", { cause: error });
|
|
@@ -1332,7 +1577,7 @@ async function parseCodexGoalJson(value) {
|
|
|
1332
1577
|
if (value === undefined)
|
|
1333
1578
|
return;
|
|
1334
1579
|
try {
|
|
1335
|
-
const raw = looksLikeJson(value) ? value : await
|
|
1580
|
+
const raw = looksLikeJson(value) ? value : await readFile2(value, "utf8");
|
|
1336
1581
|
JSON.parse(raw);
|
|
1337
1582
|
return raw;
|
|
1338
1583
|
} catch (error) {
|
|
@@ -1483,7 +1728,7 @@ function fail(message, code = "ULW_LOOP_VALIDATION_BATCH_INVALID") {
|
|
|
1483
1728
|
}
|
|
1484
1729
|
|
|
1485
1730
|
// components/ulw-loop/src/checkpoint.ts
|
|
1486
|
-
var QUALITY_GATE_FS = { existsSync:
|
|
1731
|
+
var QUALITY_GATE_FS = { existsSync: existsSync4, statSync: statSync2 };
|
|
1487
1732
|
function ulwLoopFail2(message, code) {
|
|
1488
1733
|
throw new UlwLoopError(message, code);
|
|
1489
1734
|
}
|
|
@@ -1506,10 +1751,10 @@ async function readJsonInput2(raw, repoRoot) {
|
|
|
1506
1751
|
throw error;
|
|
1507
1752
|
}
|
|
1508
1753
|
const path = resolve3(repoRoot, trimmed);
|
|
1509
|
-
if (!
|
|
1754
|
+
if (!existsSync4(path))
|
|
1510
1755
|
return ulwLoopFail2("Quality gate JSON is neither valid JSON nor a readable path.", "ulw_loop_json_input_invalid");
|
|
1511
1756
|
try {
|
|
1512
|
-
return JSON.parse(await
|
|
1757
|
+
return JSON.parse(await readFile3(path, "utf8"));
|
|
1513
1758
|
} catch (error) {
|
|
1514
1759
|
return ulwLoopFail2(`Quality gate path does not contain valid JSON${error instanceof Error ? `: ${error.message}` : "."}`, "ulw_loop_json_input_invalid");
|
|
1515
1760
|
}
|
|
@@ -1649,12 +1894,12 @@ async function checkpointUlwLoop(repoRoot, args, scope, dependencies) {
|
|
|
1649
1894
|
if (aggregateCompletion !== undefined)
|
|
1650
1895
|
nextActions = [...nextActions, 'aggregate complete — now update_goal({status:"complete"})'];
|
|
1651
1896
|
plan.updatedAt = now;
|
|
1652
|
-
await writePlan(repoRoot, plan, scope);
|
|
1653
1897
|
const ledgerEntry = buildLedger(now, args, goal, qualityGate, codexGoal, aggregateCompletion);
|
|
1654
|
-
|
|
1898
|
+
const entries = [ledgerEntry];
|
|
1655
1899
|
const closedBatch = args.status === "complete" ? batchClosedBy(plan, goal.id) : undefined;
|
|
1656
1900
|
if (closedBatch !== undefined)
|
|
1657
|
-
|
|
1901
|
+
entries.push({ at: now, kind: "batch_closed", goalId: goal.id, message: closedBatch.batchId });
|
|
1902
|
+
await commit(repoRoot, scope, { plan, entries });
|
|
1658
1903
|
return aggregateCompletion === undefined ? { plan, goal, ledgerEntry, nextActions, warnings } : { plan, goal, ledgerEntry, aggregateCompletion, nextActions, warnings };
|
|
1659
1904
|
});
|
|
1660
1905
|
}
|
|
@@ -1990,10 +2235,6 @@ function joinLines(lines) {
|
|
|
1990
2235
|
`);
|
|
1991
2236
|
}
|
|
1992
2237
|
|
|
1993
|
-
// components/ulw-loop/src/plan-crud.ts
|
|
1994
|
-
import { existsSync as existsSync4 } from "node:fs";
|
|
1995
|
-
import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
|
|
1996
|
-
|
|
1997
2238
|
// components/ulw-loop/src/plan-goal-factory.ts
|
|
1998
2239
|
function cleanLine(line) {
|
|
1999
2240
|
return line.replace(/^\s*(?:[-*+]\s+|\d+[.)]\s+)/, "").trim();
|
|
@@ -2099,18 +2340,31 @@ function clearGoalBlockerFields2(goal) {
|
|
|
2099
2340
|
])
|
|
2100
2341
|
delete goal[key];
|
|
2101
2342
|
}
|
|
2102
|
-
async function createUlwLoopPlan(repoRoot, args, scope) {
|
|
2343
|
+
async function createUlwLoopPlan(repoRoot, args, scope, surface = "lazycodex") {
|
|
2103
2344
|
return withUlwLoopMutationLock(repoRoot, scope, async () => {
|
|
2104
|
-
|
|
2105
|
-
|
|
2345
|
+
let existing;
|
|
2346
|
+
if (planExists(repoRoot, scope)) {
|
|
2347
|
+
try {
|
|
2348
|
+
existing = await readUlwLoopPlan(repoRoot, scope);
|
|
2349
|
+
} catch (error) {
|
|
2350
|
+
if (!args.force)
|
|
2351
|
+
throw error;
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
if (!args.force && existing !== undefined) {
|
|
2106
2355
|
if (isUlwLoopDone(existing))
|
|
2107
|
-
throw completedPlanExistsError(scope);
|
|
2356
|
+
throw completedPlanExistsError(scope, surface);
|
|
2108
2357
|
throw new UlwLoopError(`Refusing to overwrite existing ${ulwLoopGoalsRelativePath(scope)}; pass --force to recreate it.`, "ULW_LOOP_PLAN_EXISTS");
|
|
2109
2358
|
}
|
|
2110
2359
|
const now = iso();
|
|
2111
2360
|
const goals = deriveGoalCandidates(args.brief).map((goal, index) => makeGoal(goal.title, goal.objective, index, now));
|
|
2112
2361
|
const plan = {
|
|
2113
2362
|
version: 1,
|
|
2363
|
+
revision: existing?.revision ?? 0,
|
|
2364
|
+
ledgerResetRevision: (existing?.revision ?? 0) + 1,
|
|
2365
|
+
brief: args.brief.endsWith(`
|
|
2366
|
+
`) ? args.brief : `${args.brief}
|
|
2367
|
+
`,
|
|
2114
2368
|
evidenceLayoutVersion: 2,
|
|
2115
2369
|
createdAt: now,
|
|
2116
2370
|
updatedAt: now,
|
|
@@ -2125,21 +2379,23 @@ async function createUlwLoopPlan(repoRoot, args, scope) {
|
|
|
2125
2379
|
plan.validationBatches = validationBatches;
|
|
2126
2380
|
if (plan.codexGoalMode === "aggregate")
|
|
2127
2381
|
plan.codexObjective = aggregateCodexObjectiveForScope(scope);
|
|
2128
|
-
await
|
|
2129
|
-
await
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
await writeFile2(ulwLoopLedgerPath(repoRoot, scope), "", "utf8");
|
|
2134
|
-
await appendLedger(repoRoot, { at: now, kind: "plan_created", message: `${goals.length} goal(s) created` }, scope);
|
|
2382
|
+
await beforePlanMutation();
|
|
2383
|
+
await commit(repoRoot, scope, {
|
|
2384
|
+
plan,
|
|
2385
|
+
entries: [{ at: now, kind: "plan_created", message: `${goals.length} goal(s) created` }]
|
|
2386
|
+
});
|
|
2135
2387
|
return plan;
|
|
2136
2388
|
});
|
|
2137
2389
|
}
|
|
2138
|
-
function completedPlanExistsError(scope) {
|
|
2390
|
+
function completedPlanExistsError(scope, surface) {
|
|
2139
2391
|
return new UlwLoopError([
|
|
2140
2392
|
`Existing ulw-loop aggregate is already complete at ${ulwLoopGoalsRelativePath(scope)}.`,
|
|
2141
|
-
|
|
2142
|
-
|
|
2393
|
+
...surface === "omo-senpi" ? [
|
|
2394
|
+
"Start a new run under a fresh session id (a new senpi session) or call agentToolkit.createGoals({ brief, force: true }) to recreate."
|
|
2395
|
+
] : [
|
|
2396
|
+
"Start a new run with `omo-agent-toolkit ulw-loop create-goals --session-id <new-id> ...` to isolate fresh state.",
|
|
2397
|
+
"Use --force only when you intentionally want to overwrite the completed evidence."
|
|
2398
|
+
]
|
|
2143
2399
|
].join(" "), "ULW_LOOP_PLAN_EXISTS_COMPLETE");
|
|
2144
2400
|
}
|
|
2145
2401
|
async function addUlwLoopGoal(repoRoot, args, scope) {
|
|
@@ -2147,8 +2403,10 @@ async function addUlwLoopGoal(repoRoot, args, scope) {
|
|
|
2147
2403
|
const plan = await readUlwLoopPlan(repoRoot, scope);
|
|
2148
2404
|
const now = iso();
|
|
2149
2405
|
const goal = appendGoalToPlan(plan, args.title, args.objective, now);
|
|
2150
|
-
await
|
|
2151
|
-
|
|
2406
|
+
await commit(repoRoot, scope, {
|
|
2407
|
+
plan,
|
|
2408
|
+
entries: [{ at: now, kind: "goal_added", goalId: goal.id, status: goal.status, message: goal.title }]
|
|
2409
|
+
});
|
|
2152
2410
|
return { plan, goal };
|
|
2153
2411
|
});
|
|
2154
2412
|
}
|
|
@@ -2161,17 +2419,18 @@ async function startNextUlwLoop(repoRoot, args = {}, scope) {
|
|
|
2161
2419
|
const existing = plan.goals.find((goal) => goal.status === "in_progress" && isScheduleEligible(goal));
|
|
2162
2420
|
if (existing)
|
|
2163
2421
|
return { plan, goal: existing, resumed: true };
|
|
2422
|
+
const entries = [];
|
|
2164
2423
|
let next = plan.goals.find((goal) => goal.status === "pending" && isScheduleEligible(goal));
|
|
2165
2424
|
if (!next && args.retryFailed) {
|
|
2166
2425
|
next = plan.goals.find((goal) => goal.status === "failed" && !goal.nonRetriable && isScheduleEligible(goal));
|
|
2167
2426
|
if (next)
|
|
2168
|
-
|
|
2427
|
+
entries.push({
|
|
2169
2428
|
at: now,
|
|
2170
2429
|
kind: "goal_retried",
|
|
2171
2430
|
goalId: next.id,
|
|
2172
2431
|
status: "pending",
|
|
2173
2432
|
...next.failureReason ? { message: next.failureReason } : {}
|
|
2174
|
-
}
|
|
2433
|
+
});
|
|
2175
2434
|
}
|
|
2176
2435
|
if (!next)
|
|
2177
2436
|
return { done: true, plan };
|
|
@@ -2182,8 +2441,14 @@ async function startNextUlwLoop(repoRoot, args = {}, scope) {
|
|
|
2182
2441
|
next.updatedAt = now;
|
|
2183
2442
|
plan.activeGoalId = next.id;
|
|
2184
2443
|
plan.updatedAt = now;
|
|
2185
|
-
|
|
2186
|
-
|
|
2444
|
+
entries.push({
|
|
2445
|
+
at: now,
|
|
2446
|
+
kind: "goal_started",
|
|
2447
|
+
goalId: next.id,
|
|
2448
|
+
status: next.status,
|
|
2449
|
+
message: `Attempt ${next.attempt}`
|
|
2450
|
+
});
|
|
2451
|
+
await commit(repoRoot, scope, { plan, entries });
|
|
2187
2452
|
return { plan, goal: next, resumed: false };
|
|
2188
2453
|
});
|
|
2189
2454
|
}
|
|
@@ -2279,7 +2544,7 @@ function checkpointStatus(value) {
|
|
|
2279
2544
|
}
|
|
2280
2545
|
|
|
2281
2546
|
// components/ulw-loop/src/cli-subcommands.ts
|
|
2282
|
-
import { readFile as
|
|
2547
|
+
import { readFile as readFile4 } from "node:fs/promises";
|
|
2283
2548
|
|
|
2284
2549
|
// components/ulw-loop/src/cli-steering.ts
|
|
2285
2550
|
var SOURCES = ["user_prompt_submit", "finding", "cli"];
|
|
@@ -2649,9 +2914,7 @@ async function recordFinalReviewBlockers(repoRoot, args, scope) {
|
|
|
2649
2914
|
const summaryEntry = { at: now, kind: "goal_review_blocked", goalId: goal.id, status: goal.status, evidence: args.evidence, codexGoal, message: `Review blockers recorded; appended ${newGoal.id}.` };
|
|
2650
2915
|
Reflect.set(summaryEntry, "kind", "blocker_recorded");
|
|
2651
2916
|
const ledgerEntries = [blockedEntry, addedEntry, summaryEntry];
|
|
2652
|
-
await
|
|
2653
|
-
for (const entry of ledgerEntries)
|
|
2654
|
-
await appendLedger(repoRoot, entry, scope);
|
|
2917
|
+
await commit(repoRoot, scope, { plan, entries: ledgerEntries });
|
|
2655
2918
|
return {
|
|
2656
2919
|
plan,
|
|
2657
2920
|
blockedGoal: goal,
|
|
@@ -2664,31 +2927,31 @@ async function recordFinalReviewBlockers(repoRoot, args, scope) {
|
|
|
2664
2927
|
}
|
|
2665
2928
|
|
|
2666
2929
|
// components/ulw-loop/src/status-next-actions.ts
|
|
2667
|
-
function statusNextActions(plan) {
|
|
2930
|
+
function statusNextActions(plan, surface = "lazycodex") {
|
|
2668
2931
|
const actions = [];
|
|
2669
2932
|
const active = plan.goals.find((goal) => goal.id === plan.activeGoalId);
|
|
2670
2933
|
if (plan.goals.length === 0)
|
|
2671
|
-
actions.push(`No goals yet: bootstrap with \`${
|
|
2934
|
+
actions.push(`No goals yet: bootstrap with \`${createGoalsAction(surface)}\`.`);
|
|
2672
2935
|
else if (active !== undefined)
|
|
2673
|
-
actions.push(...activeGoalActions(plan, active));
|
|
2936
|
+
actions.push(...activeGoalActions(plan, active, surface));
|
|
2674
2937
|
for (const goal of plan.goals.filter((candidate) => candidate.status === "review_blocked"))
|
|
2675
|
-
actions.push(`${goal.id} is review_blocked: capture the reviewer verdict with \`omo-agent-toolkit ulw-loop record-review-blockers --goal-id ${goal.id} --title "<title>" --objective "<objective>" --evidence "<verdict>" --codex-goal-json '<get_goal json>'\`.`);
|
|
2938
|
+
actions.push(surface === "omo-senpi" ? `${goal.id} is review_blocked: capture the reviewer verdict with \`agentToolkit.recordReviewBlockers({ goalId: "${goal.id}", title: "<title>", objective: "<objective>", evidence: "<verdict>" })\`.` : `${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>'\`.`);
|
|
2676
2939
|
if (plan.evidenceLayoutVersion !== 2 || active === undefined)
|
|
2677
2940
|
actions.push("plan is evidence-layout v1; artifacts go under .omo/evidence/");
|
|
2678
2941
|
return actions;
|
|
2679
2942
|
}
|
|
2680
|
-
function activeGoalActions(plan, active) {
|
|
2943
|
+
function activeGoalActions(plan, active, surface) {
|
|
2681
2944
|
const unresolved = active.successCriteria.filter((criterion) => criterion.status !== "pass");
|
|
2682
2945
|
if (unresolved.length > 0)
|
|
2683
2946
|
return [
|
|
2684
|
-
`${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>"\`.`
|
|
2947
|
+
surface === "omo-senpi" ? `${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>" })\`.` : `${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>"\`.`
|
|
2685
2948
|
];
|
|
2686
2949
|
if (hasAllCriteriaPass(active) && isFinalRunCompletionCandidate(plan, active))
|
|
2687
2950
|
return [
|
|
2688
|
-
`${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.`
|
|
2951
|
+
surface === "omo-senpi" ? `${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.` : `${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.`
|
|
2689
2952
|
];
|
|
2690
2953
|
return [
|
|
2691
|
-
`${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.`
|
|
2954
|
+
surface === "omo-senpi" ? `${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.` : `${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.`
|
|
2692
2955
|
];
|
|
2693
2956
|
}
|
|
2694
2957
|
|
|
@@ -3020,11 +3283,10 @@ async function steerUlwLoop(repoRoot, proposal, scope) {
|
|
|
3020
3283
|
}
|
|
3021
3284
|
const at = proposal.now?.toISOString() ?? iso();
|
|
3022
3285
|
const batchEntry = accepted ? batchUpdateLedgerEntry(plan, next, at) : null;
|
|
3023
|
-
|
|
3024
|
-
await writePlan(repoRoot, next, scope);
|
|
3025
|
-
await appendLedger(repoRoot, ledgerEntry(proposal, finalAudit, at), scope);
|
|
3286
|
+
const entries = [ledgerEntry(proposal, finalAudit, at)];
|
|
3026
3287
|
if (batchEntry !== null)
|
|
3027
|
-
|
|
3288
|
+
entries.push(batchEntry);
|
|
3289
|
+
await commit(repoRoot, scope, { plan: next, entries });
|
|
3028
3290
|
return { plan: next, accepted, audit: finalAudit, rejectedReasons: audit.invariant.rejectedReasons, deduped: false };
|
|
3029
3291
|
});
|
|
3030
3292
|
}
|
|
@@ -3071,8 +3333,12 @@ function validateCodexGoalJson(raw) {
|
|
|
3071
3333
|
function validateContext(context) {
|
|
3072
3334
|
if (!context.cwd.trim())
|
|
3073
3335
|
throw new UlwLoopError("cwd is required.", "ULW_LOOP_CWD_REQUIRED");
|
|
3074
|
-
|
|
3336
|
+
const sessionId = context.sessionId.trim();
|
|
3337
|
+
if (!sessionId)
|
|
3075
3338
|
throw new UlwLoopError("ULW_LOOP_SESSION_ID_REQUIRED: sessionId is required.", "ULW_LOOP_SESSION_ID_REQUIRED");
|
|
3339
|
+
const normalizedSessionId = normalizeUlwLoopSessionId(sessionId);
|
|
3340
|
+
if (normalizedSessionId === null || /(?:^|[\\/])\.\.(?:[\\/]|$)/.test(sessionId))
|
|
3341
|
+
throw new UlwLoopError("ULW_LOOP_SESSION_ID_INVALID: sessionId normalizes to null.", "ULW_LOOP_SESSION_ID_INVALID");
|
|
3076
3342
|
if (context.surface !== "omo-senpi" && context.surface !== "lazycodex")
|
|
3077
3343
|
throw new UlwLoopError("surface must be omo-senpi or lazycodex.", "ULW_LOOP_SURFACE_INVALID");
|
|
3078
3344
|
}
|
|
@@ -3115,6 +3381,8 @@ function createAgentToolkit(context, deps = {}) {
|
|
|
3115
3381
|
const nextActions = typeof result === "object" && result !== null ? nextActionsFrom(result) : [];
|
|
3116
3382
|
return await notify(operation, { ok: true, operation, result, nextActions });
|
|
3117
3383
|
} catch (error) {
|
|
3384
|
+
if (error instanceof UlwLoopError && error.code === "ULW_LOOP_PLAN_MISSING" && context.surface === "omo-senpi")
|
|
3385
|
+
return notify(operation, failure(operation, planMissingError(ulwLoopGoalsRelativePath(scope), listUlwLoopSessionIds(context.cwd), context.surface)));
|
|
3118
3386
|
const response = caught(operation, error instanceof Error ? error : new Error("ULW_LOOP_ERROR"));
|
|
3119
3387
|
return notify(operation, response);
|
|
3120
3388
|
}
|
|
@@ -3149,14 +3417,14 @@ function createAgentToolkit(context, deps = {}) {
|
|
|
3149
3417
|
}
|
|
3150
3418
|
},
|
|
3151
3419
|
help: () => invoke("help", async () => ULW_LOOP_MANIFEST),
|
|
3152
|
-
createGoals: (args) => invoke("create-goals", () => createUlwLoopPlan(context.cwd, args, scope)),
|
|
3420
|
+
createGoals: (args) => invoke("create-goals", () => createUlwLoopPlan(context.cwd, args, scope, context.surface)),
|
|
3153
3421
|
status: () => invoke("status", async () => {
|
|
3154
3422
|
const plan = await readUlwLoopPlan(context.cwd, scope);
|
|
3155
3423
|
const active = plan.goals.find((goal) => goal.id === plan.activeGoalId);
|
|
3156
3424
|
return {
|
|
3157
3425
|
plan,
|
|
3158
3426
|
summary: summarizeUlwLoopPlan(plan),
|
|
3159
|
-
nextActions: statusNextActions(plan),
|
|
3427
|
+
nextActions: statusNextActions(plan, context.surface),
|
|
3160
3428
|
...active === undefined || plan.evidenceLayoutVersion !== 2 ? {} : { currentAttemptDir: ulwLoopAttemptEvidenceDir(active.id, active.attempt, scope) }
|
|
3161
3429
|
};
|
|
3162
3430
|
}),
|
|
@@ -3184,7 +3452,7 @@ async function steerUlwLoopBatch(repoRoot, proposals, scope) {
|
|
|
3184
3452
|
const failed = prepared.results.find((item) => !item.accepted);
|
|
3185
3453
|
if (failed !== undefined) {
|
|
3186
3454
|
const entry = rejectedLedgerEntry(prepared.results);
|
|
3187
|
-
await
|
|
3455
|
+
await commit(repoRoot, scope, { plan, entries: [entry] });
|
|
3188
3456
|
return rejected(plan, prepared.results, failed.rejectedReasons);
|
|
3189
3457
|
}
|
|
3190
3458
|
let next = plan;
|
|
@@ -3193,10 +3461,9 @@ async function steerUlwLoopBatch(repoRoot, proposals, scope) {
|
|
|
3193
3461
|
next = item.prepared.next;
|
|
3194
3462
|
const fresh = prepared.items.filter((item) => item.kind === "fresh");
|
|
3195
3463
|
if (fresh.length > 0) {
|
|
3196
|
-
await writePlan(repoRoot, next, scope);
|
|
3197
3464
|
const entries = fresh.map((item) => ledgerEntry2(item.prepared.proposal, item.prepared.audit, item.prepared.proposal.now?.toISOString() ?? iso()));
|
|
3198
3465
|
const batchEntry = batchUpdateLedgerEntry(plan, next, iso());
|
|
3199
|
-
await
|
|
3466
|
+
await commit(repoRoot, scope, { plan: next, entries: batchEntry === null ? entries : [...entries, batchEntry] });
|
|
3200
3467
|
}
|
|
3201
3468
|
return { plan: next, accepted: true, results: prepared.results, rejectedReasons: [] };
|
|
3202
3469
|
});
|
|
@@ -3260,7 +3527,7 @@ function ledgerEntry2(proposal, audit, at) {
|
|
|
3260
3527
|
// components/ulw-loop/src/cli-subcommands.ts
|
|
3261
3528
|
async function createGoals(repoRoot, argv, json, scope) {
|
|
3262
3529
|
const briefFile = readValue(argv, "--brief-file");
|
|
3263
|
-
const brief = readValue(argv, "--brief") ?? (briefFile === undefined ? undefined : await
|
|
3530
|
+
const brief = readValue(argv, "--brief") ?? (briefFile === undefined ? undefined : await readFile4(briefFile, "utf8")) ?? (hasFlag(argv, "--from-stdin") ? await readStdin() : undefined) ?? positionalText(argv);
|
|
3264
3531
|
if (!brief.trim()) {
|
|
3265
3532
|
throw new UlwLoopError("Missing brief text. Pass --brief, --brief-file, --from-stdin, or positional text.", "ULW_LOOP_BRIEF_REQUIRED");
|
|
3266
3533
|
}
|
|
@@ -3528,10 +3795,10 @@ function commandScope(repoRoot, argv) {
|
|
|
3528
3795
|
}
|
|
3529
3796
|
|
|
3530
3797
|
// components/ulw-loop/src/ultrawork-directive.ts
|
|
3531
|
-
import { readFileSync as
|
|
3798
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
3532
3799
|
|
|
3533
3800
|
// components/ulw-loop/src/ultrawork-skill-pointer.ts
|
|
3534
|
-
import { existsSync as existsSync5, readFileSync as
|
|
3801
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "node:fs";
|
|
3535
3802
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
3536
3803
|
var ULTRAWORK_SKILL_POINTER_TEMPLATE = `<ultrawork-mode>
|
|
3537
3804
|
ULTRAWORK MODE IS ACTIVE FOR THIS TASK.
|
|
@@ -3563,7 +3830,7 @@ Do not start the requested work until all three steps are complete.
|
|
|
3563
3830
|
`;
|
|
3564
3831
|
var ULTRAWORK_SKILL_PATH_PLACEHOLDER = "{{ULTRAWORK_SKILL_PATH}}";
|
|
3565
3832
|
var ULTRAWORK_SKILL_FILE_URL = new URL("../../../skills/ultrawork/SKILL.md", import.meta.url);
|
|
3566
|
-
var ULTRAWORK_DIRECTIVE =
|
|
3833
|
+
var ULTRAWORK_DIRECTIVE = readFileSync4(new URL("../directive.md", import.meta.url), "utf8");
|
|
3567
3834
|
function resolveUltraworkSkillFilePath() {
|
|
3568
3835
|
return fileURLToPath2(ULTRAWORK_SKILL_FILE_URL);
|
|
3569
3836
|
}
|
|
@@ -3626,7 +3893,7 @@ function hasUltraworkDirectiveAlreadyInTranscript(transcriptPath) {
|
|
|
3626
3893
|
return false;
|
|
3627
3894
|
}
|
|
3628
3895
|
function readTranscriptTail(transcriptPath) {
|
|
3629
|
-
const rawTranscript =
|
|
3896
|
+
const rawTranscript = readFileSync5(transcriptPath);
|
|
3630
3897
|
return rawTranscript.subarray(Math.max(0, rawTranscript.byteLength - TRANSCRIPT_SEARCH_BYTES)).toString("utf8");
|
|
3631
3898
|
}
|
|
3632
3899
|
function isUltraworkPrompt(prompt) {
|
|
@@ -3819,9 +4086,64 @@ function readAll(stdin) {
|
|
|
3819
4086
|
}
|
|
3820
4087
|
|
|
3821
4088
|
// components/ulw-loop/src/spawn-guard.ts
|
|
4089
|
+
import { mkdirSync as mkdirSync3 } from "node:fs";
|
|
4090
|
+
import { join as join7 } from "node:path";
|
|
4091
|
+
|
|
4092
|
+
// components/ulw-loop/src/spawn-budget-io.ts
|
|
3822
4093
|
import { randomBytes } from "node:crypto";
|
|
3823
|
-
import { existsSync as existsSync6,
|
|
3824
|
-
import { dirname as dirname3, join as
|
|
4094
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6, renameSync as renameSync2, statSync as statSync3, writeFileSync as writeFileSync2 } from "node:fs";
|
|
4095
|
+
import { dirname as dirname3, join as join6 } from "node:path";
|
|
4096
|
+
function readAdmissionBreaker(sessionId) {
|
|
4097
|
+
const dataDir = process.env["PLUGIN_DATA"];
|
|
4098
|
+
if (typeof dataDir !== "string")
|
|
4099
|
+
return null;
|
|
4100
|
+
try {
|
|
4101
|
+
const value = JSON.parse(readFileSync6(join6(dataDir, "spawn-breaker", `${sessionId}.json`), "utf8"));
|
|
4102
|
+
return typeof value === "object" && value !== null && "reason" in value && typeof value.reason === "string" ? value.reason : "capacity limit";
|
|
4103
|
+
} catch {
|
|
4104
|
+
return null;
|
|
4105
|
+
}
|
|
4106
|
+
}
|
|
4107
|
+
function atomicWriteJson(targetPath, data) {
|
|
4108
|
+
const tmp = join6(dirname3(targetPath), `.tmp-${randomBytes(6).toString("hex")}`);
|
|
4109
|
+
writeFileSync2(tmp, JSON.stringify(data));
|
|
4110
|
+
renameSync2(tmp, targetPath);
|
|
4111
|
+
}
|
|
4112
|
+
function isNonEmptyFile(path) {
|
|
4113
|
+
try {
|
|
4114
|
+
return existsSync6(path) && statSync3(path).size > 0;
|
|
4115
|
+
} catch (error) {
|
|
4116
|
+
if (error instanceof Error)
|
|
4117
|
+
return false;
|
|
4118
|
+
throw error;
|
|
4119
|
+
}
|
|
4120
|
+
}
|
|
4121
|
+
function readCount(counterPath) {
|
|
4122
|
+
try {
|
|
4123
|
+
const parsed = JSON.parse(readFileSync6(counterPath, "utf8"));
|
|
4124
|
+
return typeof parsed === "object" && parsed !== null && "count" in parsed && typeof parsed.count === "number" && parsed.count >= 0 ? parsed.count : 0;
|
|
4125
|
+
} catch (error) {
|
|
4126
|
+
if (error instanceof Error)
|
|
4127
|
+
return 0;
|
|
4128
|
+
throw error;
|
|
4129
|
+
}
|
|
4130
|
+
}
|
|
4131
|
+
function readCounts(counterPath) {
|
|
4132
|
+
try {
|
|
4133
|
+
const parsed = JSON.parse(readFileSync6(counterPath, "utf8"));
|
|
4134
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
4135
|
+
return {};
|
|
4136
|
+
const counts = {};
|
|
4137
|
+
for (const [key, value] of Object.entries(parsed))
|
|
4138
|
+
if (typeof value === "number" && value >= 0)
|
|
4139
|
+
counts[key] = value;
|
|
4140
|
+
return counts;
|
|
4141
|
+
} catch (error) {
|
|
4142
|
+
if (error instanceof Error)
|
|
4143
|
+
return {};
|
|
4144
|
+
throw error;
|
|
4145
|
+
}
|
|
4146
|
+
}
|
|
3825
4147
|
|
|
3826
4148
|
// components/ulw-loop/src/spawn-role-guard.ts
|
|
3827
4149
|
var LAZYCODEX_SPAWN_ROLES = new Set([
|
|
@@ -3879,7 +4201,7 @@ function applySpawnBudgetGuards(payload, options = {}) {
|
|
|
3879
4201
|
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.`);
|
|
3880
4202
|
const scope = { sessionId: payload.session_id };
|
|
3881
4203
|
const stateDir = ulwLoopDir(payload.cwd, scope);
|
|
3882
|
-
const plan = readPlan(
|
|
4204
|
+
const plan = readPlan(payload.cwd, payload.session_id);
|
|
3883
4205
|
if (plan === null)
|
|
3884
4206
|
return "";
|
|
3885
4207
|
const lockOptions = options.lockTimeoutMs === undefined ? {} : { timeoutMs: options.lockTimeoutMs };
|
|
@@ -3918,10 +4240,10 @@ async function runSpawnAdmissionRecorderCli(stdin, stdout) {
|
|
|
3918
4240
|
const dataDir = process.env["PLUGIN_DATA"];
|
|
3919
4241
|
if (typeof dataDir !== "string" || typeof payload["session_id"] !== "string")
|
|
3920
4242
|
return;
|
|
3921
|
-
const markerDir =
|
|
4243
|
+
const markerDir = join7(dataDir, "spawn-breaker");
|
|
3922
4244
|
try {
|
|
3923
|
-
|
|
3924
|
-
atomicWriteJson(
|
|
4245
|
+
mkdirSync3(markerDir, { recursive: true });
|
|
4246
|
+
atomicWriteJson(join7(markerDir, `${payload["session_id"]}.json`), {
|
|
3925
4247
|
reason: response,
|
|
3926
4248
|
at: new Date().toISOString()
|
|
3927
4249
|
});
|
|
@@ -3950,7 +4272,7 @@ async function runSpawnGuardCli(stdin, stdout) {
|
|
|
3950
4272
|
}
|
|
3951
4273
|
}
|
|
3952
4274
|
function peekFanOutBudget(stateDir) {
|
|
3953
|
-
const counterPath =
|
|
4275
|
+
const counterPath = join7(stateDir, "spawn-count.json");
|
|
3954
4276
|
const count = readCount(counterPath) + 1;
|
|
3955
4277
|
const limit = fanOutLimit();
|
|
3956
4278
|
if (count <= limit)
|
|
@@ -3958,7 +4280,7 @@ function peekFanOutBudget(stateDir) {
|
|
|
3958
4280
|
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.`;
|
|
3959
4281
|
}
|
|
3960
4282
|
function consumeFanOutBudget(stateDir) {
|
|
3961
|
-
const counterPath =
|
|
4283
|
+
const counterPath = join7(stateDir, "spawn-count.json");
|
|
3962
4284
|
const count = readCount(counterPath) + 1;
|
|
3963
4285
|
atomicWriteJson(counterPath, { count });
|
|
3964
4286
|
const limit = fanOutLimit();
|
|
@@ -3973,7 +4295,7 @@ function consumeReviewSpawnBudget(payload, plan, stateDir) {
|
|
|
3973
4295
|
const goal = plan.goals.find((candidate) => candidate.id === plan.activeGoalId) ?? plan.goals.find((candidate) => isFinalRunCompletionCandidate(plan, candidate));
|
|
3974
4296
|
if (goal === undefined)
|
|
3975
4297
|
return null;
|
|
3976
|
-
const counterPath =
|
|
4298
|
+
const counterPath = join7(stateDir, "review-spawn-counts.json");
|
|
3977
4299
|
const limit = reviewSpawnLimit();
|
|
3978
4300
|
const counts = readCounts(counterPath);
|
|
3979
4301
|
const key = `${agentType}:${goal.id}:a${goal.attempt}`;
|
|
@@ -3998,13 +4320,13 @@ function missingGateArtifact(payload, plan) {
|
|
|
3998
4320
|
const attemptDir = ulwLoopAttemptEvidenceDir(goal.id, goal.attempt, scope);
|
|
3999
4321
|
for (const name of requiredArtifacts) {
|
|
4000
4322
|
const relative = `${attemptDir}/${name}`;
|
|
4001
|
-
if (!isNonEmptyFile(
|
|
4323
|
+
if (!isNonEmptyFile(join7(payload.cwd, relative)))
|
|
4002
4324
|
return relative;
|
|
4003
4325
|
}
|
|
4004
4326
|
return null;
|
|
4005
4327
|
}
|
|
4006
4328
|
const manualQa = `.omo/evidence/${goal.id}-manual-qa.md`;
|
|
4007
|
-
return isNonEmptyFile(
|
|
4329
|
+
return isNonEmptyFile(join7(payload.cwd, manualQa)) ? null : manualQa;
|
|
4008
4330
|
}
|
|
4009
4331
|
function isGateReviewerSpawn(toolInput) {
|
|
4010
4332
|
const agentType = reviewAgentType(toolInput);
|
|
@@ -4051,11 +4373,6 @@ function activeSurfaceReviewerAlias(reviewer) {
|
|
|
4051
4373
|
}
|
|
4052
4374
|
return reviewer;
|
|
4053
4375
|
}
|
|
4054
|
-
function atomicWriteJson(targetPath, data) {
|
|
4055
|
-
const tmp = join3(dirname3(targetPath), `.tmp-${randomBytes(6).toString("hex")}`);
|
|
4056
|
-
writeFileSync(tmp, JSON.stringify(data));
|
|
4057
|
-
renameSync(tmp, targetPath);
|
|
4058
|
-
}
|
|
4059
4376
|
function deny(reason) {
|
|
4060
4377
|
return `${JSON.stringify({
|
|
4061
4378
|
hookSpecificOutput: {
|
|
@@ -4067,17 +4384,6 @@ function deny(reason) {
|
|
|
4067
4384
|
})}
|
|
4068
4385
|
`;
|
|
4069
4386
|
}
|
|
4070
|
-
function readAdmissionBreaker(sessionId) {
|
|
4071
|
-
const dataDir = process.env["PLUGIN_DATA"];
|
|
4072
|
-
if (typeof dataDir !== "string")
|
|
4073
|
-
return null;
|
|
4074
|
-
try {
|
|
4075
|
-
const value = JSON.parse(readFileSync5(join3(dataDir, "spawn-breaker", `${sessionId}.json`), "utf8"));
|
|
4076
|
-
return typeof value.reason === "string" ? value.reason : "capacity limit";
|
|
4077
|
-
} catch {
|
|
4078
|
-
return null;
|
|
4079
|
-
}
|
|
4080
|
-
}
|
|
4081
4387
|
function fanOutLimit() {
|
|
4082
4388
|
const raw = process.env["OMO_SPAWN_FANOUT_LIMIT"];
|
|
4083
4389
|
if (raw === undefined)
|
|
@@ -4092,45 +4398,9 @@ function reviewSpawnLimit() {
|
|
|
4092
4398
|
const parsed = Number.parseInt(raw, 10);
|
|
4093
4399
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_REVIEW_SPAWN_LIMIT;
|
|
4094
4400
|
}
|
|
4095
|
-
function
|
|
4096
|
-
try {
|
|
4097
|
-
return existsSync6(path) && statSync3(path).size > 0;
|
|
4098
|
-
} catch (error) {
|
|
4099
|
-
if (error instanceof Error)
|
|
4100
|
-
return false;
|
|
4101
|
-
throw error;
|
|
4102
|
-
}
|
|
4103
|
-
}
|
|
4104
|
-
function readCount(counterPath) {
|
|
4105
|
-
try {
|
|
4106
|
-
const parsed = JSON.parse(readFileSync5(counterPath, "utf8"));
|
|
4107
|
-
return typeof parsed["count"] === "number" && parsed["count"] >= 0 ? parsed["count"] : 0;
|
|
4108
|
-
} catch (error) {
|
|
4109
|
-
if (error instanceof Error)
|
|
4110
|
-
return 0;
|
|
4111
|
-
throw error;
|
|
4112
|
-
}
|
|
4113
|
-
}
|
|
4114
|
-
function readCounts(counterPath) {
|
|
4115
|
-
try {
|
|
4116
|
-
const parsed = JSON.parse(readFileSync5(counterPath, "utf8"));
|
|
4117
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
4118
|
-
return {};
|
|
4119
|
-
const counts = {};
|
|
4120
|
-
for (const [key, value] of Object.entries(parsed)) {
|
|
4121
|
-
if (typeof value === "number" && value >= 0)
|
|
4122
|
-
counts[key] = value;
|
|
4123
|
-
}
|
|
4124
|
-
return counts;
|
|
4125
|
-
} catch (error) {
|
|
4126
|
-
if (error instanceof Error)
|
|
4127
|
-
return {};
|
|
4128
|
-
throw error;
|
|
4129
|
-
}
|
|
4130
|
-
}
|
|
4131
|
-
function readPlan(goalsPath) {
|
|
4401
|
+
function readPlan(repoRoot, sessionId) {
|
|
4132
4402
|
try {
|
|
4133
|
-
return
|
|
4403
|
+
return readUlwLoopPlanSync(repoRoot, { sessionId });
|
|
4134
4404
|
} catch (error) {
|
|
4135
4405
|
if (error instanceof Error)
|
|
4136
4406
|
return null;
|
|
@@ -4139,8 +4409,8 @@ function readPlan(goalsPath) {
|
|
|
4139
4409
|
}
|
|
4140
4410
|
|
|
4141
4411
|
// components/ulw-loop/src/stop-resume-hook.ts
|
|
4142
|
-
import { existsSync as existsSync7, readFileSync as
|
|
4143
|
-
import { isAbsolute as isAbsolute2, join as
|
|
4412
|
+
import { existsSync as existsSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "node:fs";
|
|
4413
|
+
import { isAbsolute as isAbsolute2, join as join8, resolve as resolve4, sep as sep2 } from "node:path";
|
|
4144
4414
|
var RESUME_CAP = 2;
|
|
4145
4415
|
var CONTEXT_PRESSURE_MARKERS2 = [
|
|
4146
4416
|
"context compacted",
|
|
@@ -4161,7 +4431,7 @@ function runStopResumeHook(input) {
|
|
|
4161
4431
|
return "";
|
|
4162
4432
|
const scope = { sessionId: payload.session_id };
|
|
4163
4433
|
const stateDir = ulwLoopDir(payload.cwd, scope);
|
|
4164
|
-
const plan = readPlan2(
|
|
4434
|
+
const plan = readPlan2(payload.cwd, payload.session_id);
|
|
4165
4435
|
if (plan === null || plan.aggregateCompletion?.status === "complete")
|
|
4166
4436
|
return "";
|
|
4167
4437
|
const goal = resumableGoal(plan);
|
|
@@ -4207,7 +4477,7 @@ function consumeResumeBudgetLocked(lockPath, stateDir, goalId) {
|
|
|
4207
4477
|
}
|
|
4208
4478
|
}
|
|
4209
4479
|
function consumeResumeBudget(stateDir, goalId) {
|
|
4210
|
-
const ledgerLineCount =
|
|
4480
|
+
const ledgerLineCount = readLedgerAt(stateDir).length;
|
|
4211
4481
|
const counterPath = resolve4(stateDir, `auto-resume-${goalId}.json`);
|
|
4212
4482
|
const stuckPath = resolve4(stateDir, `auto-resume-${goalId}.stuck`);
|
|
4213
4483
|
if (!isInsideDir(stateDir, counterPath) || !isInsideDir(stateDir, stuckPath))
|
|
@@ -4215,11 +4485,11 @@ function consumeResumeBudget(stateDir, goalId) {
|
|
|
4215
4485
|
const previous = readCounter(counterPath);
|
|
4216
4486
|
const count = previous !== null && previous.ledgerLineCount === ledgerLineCount ? previous.count : 0;
|
|
4217
4487
|
if (count >= RESUME_CAP) {
|
|
4218
|
-
|
|
4488
|
+
writeFileSync3(stuckPath, `no ledger progress after ${count} resumes
|
|
4219
4489
|
`);
|
|
4220
4490
|
return false;
|
|
4221
4491
|
}
|
|
4222
|
-
|
|
4492
|
+
writeFileSync3(counterPath, JSON.stringify({ count: count + 1, ledgerLineCount }));
|
|
4223
4493
|
return true;
|
|
4224
4494
|
}
|
|
4225
4495
|
function isInsideDir(dir, candidate) {
|
|
@@ -4238,30 +4508,20 @@ function renderResumeDirective(plan, goal, sessionId) {
|
|
|
4238
4508
|
].join(`
|
|
4239
4509
|
`);
|
|
4240
4510
|
}
|
|
4241
|
-
function readPlan2(
|
|
4511
|
+
function readPlan2(repoRoot, sessionId) {
|
|
4242
4512
|
try {
|
|
4243
|
-
return
|
|
4513
|
+
return readUlwLoopPlanSync(repoRoot, { sessionId });
|
|
4244
4514
|
} catch (error) {
|
|
4245
4515
|
if (error instanceof Error)
|
|
4246
4516
|
return null;
|
|
4247
4517
|
throw error;
|
|
4248
4518
|
}
|
|
4249
4519
|
}
|
|
4250
|
-
function countLedgerLines(ledgerPath) {
|
|
4251
|
-
try {
|
|
4252
|
-
return readFileSync6(ledgerPath, "utf8").split(`
|
|
4253
|
-
`).filter(Boolean).length;
|
|
4254
|
-
} catch (error) {
|
|
4255
|
-
if (error instanceof Error)
|
|
4256
|
-
return 0;
|
|
4257
|
-
throw error;
|
|
4258
|
-
}
|
|
4259
|
-
}
|
|
4260
4520
|
function readCounter(counterPath) {
|
|
4261
4521
|
try {
|
|
4262
4522
|
if (!existsSync7(counterPath))
|
|
4263
4523
|
return null;
|
|
4264
|
-
const parsed = JSON.parse(
|
|
4524
|
+
const parsed = JSON.parse(readFileSync7(counterPath, "utf8"));
|
|
4265
4525
|
if (typeof parsed["count"] !== "number" || typeof parsed["ledgerLineCount"] !== "number")
|
|
4266
4526
|
return null;
|
|
4267
4527
|
return { count: parsed["count"], ledgerLineCount: parsed["ledgerLineCount"] };
|
|
@@ -4273,7 +4533,7 @@ function readCounter(counterPath) {
|
|
|
4273
4533
|
}
|
|
4274
4534
|
function boulderContinuationWillFire(cwd, sessionId) {
|
|
4275
4535
|
try {
|
|
4276
|
-
const raw = JSON.parse(
|
|
4536
|
+
const raw = JSON.parse(readFileSync7(join8(cwd, ".omo", "boulder.json"), "utf8"));
|
|
4277
4537
|
const works = raw["works"];
|
|
4278
4538
|
const entries = typeof works === "object" && works !== null ? Object.values(works) : [raw];
|
|
4279
4539
|
return entries.some((work) => {
|
|
@@ -4292,7 +4552,7 @@ function boulderContinuationWillFire(cwd, sessionId) {
|
|
|
4292
4552
|
}
|
|
4293
4553
|
function transcriptShowsContextPressure(transcriptPath) {
|
|
4294
4554
|
try {
|
|
4295
|
-
const transcript =
|
|
4555
|
+
const transcript = readFileSync7(transcriptPath, "utf8").toLowerCase();
|
|
4296
4556
|
return CONTEXT_PRESSURE_MARKERS2.some((marker) => transcript.includes(marker));
|
|
4297
4557
|
} catch (error) {
|
|
4298
4558
|
if (error instanceof Error)
|
|
@@ -4304,12 +4564,12 @@ function boulderPlanHasChecklist(cwd, entry) {
|
|
|
4304
4564
|
const activePlan = entry["active_plan"];
|
|
4305
4565
|
if (typeof activePlan !== "string" || activePlan.trim().length === 0)
|
|
4306
4566
|
return false;
|
|
4307
|
-
const planPath = isAbsolute2(activePlan) ? activePlan :
|
|
4567
|
+
const planPath = isAbsolute2(activePlan) ? activePlan : join8(cwd, activePlan);
|
|
4308
4568
|
const worktree = entry["worktree_path"];
|
|
4309
|
-
const candidates = typeof worktree === "string" && worktree.trim().length > 0 && !isAbsolute2(activePlan) ? [
|
|
4569
|
+
const candidates = typeof worktree === "string" && worktree.trim().length > 0 && !isAbsolute2(activePlan) ? [join8(isAbsolute2(worktree) ? worktree : join8(cwd, worktree), activePlan), planPath] : [planPath];
|
|
4310
4570
|
for (const candidate of candidates) {
|
|
4311
4571
|
try {
|
|
4312
|
-
return
|
|
4572
|
+
return readFileSync7(candidate, "utf8").split(/\r?\n/).some((line) => line.startsWith("- [ ] ") || line.startsWith("- [x] ") || line.startsWith("- [X] "));
|
|
4313
4573
|
} catch (error) {
|
|
4314
4574
|
if (!(error instanceof Error))
|
|
4315
4575
|
throw error;
|