lazycodex-ai 5.0.0-beta.45 → 5.0.0-beta.47
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +43 -27
- package/dist/cli-node/index.js +43 -27
- package/package.json +1 -1
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
- package/packages/omo-codex/plugin/components/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/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-commands.js +15 -7
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-output.d.ts +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-output.js +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +294 -96
- package/packages/omo-codex/plugin/components/ulw-loop/dist/constants.d.ts +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/constants.js +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/paths.d.ts +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/paths.js +12 -3
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-io.d.ts +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-io.js +21 -6
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-missing-recovery.d.ts +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-missing-recovery.js +10 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-guard.d.ts +4 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-guard.js +25 -57
- package/packages/omo-codex/plugin/components/ulw-loop/dist/state-lock.d.ts +9 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/state-lock.js +227 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/stop-resume-hook.js +17 -3
- package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +2 -1
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +10 -7
- package/packages/omo-codex/plugin/components/ulw-loop/src/cli-commands.ts +18 -7
- package/packages/omo-codex/plugin/components/ulw-loop/src/cli-output.ts +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/src/constants.ts +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/paths.ts +24 -3
- package/packages/omo-codex/plugin/components/ulw-loop/src/plan-io.ts +25 -5
- package/packages/omo-codex/plugin/components/ulw-loop/src/plan-missing-recovery.ts +11 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-guard.ts +34 -64
- package/packages/omo-codex/plugin/components/ulw-loop/src/state-lock.ts +239 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/stop-resume-hook.ts +16 -3
- package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint-template.test.ts +14 -4
- package/packages/omo-codex/plugin/components/ulw-loop/test/cli-checkpoint-continuation.test.ts +4 -2
- package/packages/omo-codex/plugin/components/ulw-loop/test/cli-checkpoint.test.ts +4 -2
- package/packages/omo-codex/plugin/components/ulw-loop/test/cli-commands.test.ts +2 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/cli-complete-goals.test.ts +3 -1
- package/packages/omo-codex/plugin/components/ulw-loop/test/cli-create-goals.test.ts +10 -8
- package/packages/omo-codex/plugin/components/ulw-loop/test/cli-entrypoint.test.ts +12 -2
- package/packages/omo-codex/plugin/components/ulw-loop/test/cli-json-errors.test.ts +2 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/cli-scope-required.test.ts +144 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/cli-status-next-actions.test.ts +7 -5
- package/packages/omo-codex/plugin/components/ulw-loop/test/cli-steering-batch.test.ts +8 -3
- package/packages/omo-codex/plugin/components/ulw-loop/test/cli-validation-batch.test.ts +6 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/cli-session.ts +4 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/quality-gate-builder.ts +2 -1
- package/packages/omo-codex/plugin/components/ulw-loop/test/plan-io-cross-process.test.ts +106 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/plan-io.test.ts +36 -2
- package/packages/omo-codex/plugin/components/ulw-loop/test/spawn-guard.test.ts +37 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/state-lock.test.ts +218 -0
- 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/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-checking-ulw-execute-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/package-lock.json +12 -12
- package/packages/omo-codex/plugin/package.json +1 -1
- package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +2 -1
- package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +10 -7
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +2 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// components/ulw-loop/src/checkpoint.ts
|
|
4
|
-
import { existsSync as existsSync4, statSync } from "node:fs";
|
|
4
|
+
import { existsSync as existsSync4, statSync as statSync2 } from "node:fs";
|
|
5
5
|
import { readFile as readFile5 } from "node:fs/promises";
|
|
6
6
|
import { resolve as resolve3 } from "node:path";
|
|
7
7
|
|
|
@@ -123,6 +123,7 @@ var ULW_LOOP_DIR = ".omo/ulw-loop";
|
|
|
123
123
|
var ULW_LOOP_BRIEF = "brief.md";
|
|
124
124
|
var ULW_LOOP_GOALS = "goals.json";
|
|
125
125
|
var ULW_LOOP_LEDGER = "ledger.jsonl";
|
|
126
|
+
var ULW_LOOP_STATE_LOCK = ".state.lock";
|
|
126
127
|
var ULW_LOOP_STEERING_MUTATION_KINDS = [
|
|
127
128
|
"add_subgoal",
|
|
128
129
|
"split_subgoal",
|
|
@@ -195,6 +196,9 @@ function ulwLoopGoalsPath(repoRoot, scope) {
|
|
|
195
196
|
function ulwLoopLedgerPath(repoRoot, scope) {
|
|
196
197
|
return join(ulwLoopDir(repoRoot, scope), ULW_LOOP_LEDGER);
|
|
197
198
|
}
|
|
199
|
+
function ulwLoopStateLockPath(repoRoot, scope) {
|
|
200
|
+
return join(ulwLoopDir(repoRoot, scope), ULW_LOOP_STATE_LOCK);
|
|
201
|
+
}
|
|
198
202
|
function repoRelative(absolutePath, repoRoot) {
|
|
199
203
|
const slashPrefix = `${repoRoot}/`;
|
|
200
204
|
const backslashPrefix = `${repoRoot}\\`;
|
|
@@ -205,7 +209,10 @@ function repoRelative(absolutePath, repoRoot) {
|
|
|
205
209
|
return absolutePath.split("\\").join("/");
|
|
206
210
|
}
|
|
207
211
|
function ulwLoopAttemptEvidenceDir(goalId, attempt, scope) {
|
|
208
|
-
const sessionId = normalizeUlwLoopSessionId(scope?.sessionId)
|
|
212
|
+
const sessionId = normalizeUlwLoopSessionId(scope?.sessionId);
|
|
213
|
+
if (sessionId === null) {
|
|
214
|
+
throw new UlwLoopError(`Evidence for ${goalId} attempt ${attempt} needs a session scope; pass --session-id <id> so the attempt directory lives under .omo/evidence/ulw/<id>/.`, "ULW_LOOP_SESSION_SCOPE_REQUIRED", { details: { goalId, attempt } });
|
|
215
|
+
}
|
|
209
216
|
return `.omo/evidence/ulw/${sessionId}/${goalId}/a${attempt}`;
|
|
210
217
|
}
|
|
211
218
|
var PLATFORM_PATH_API = { relative, isAbsolute, sep };
|
|
@@ -418,6 +425,7 @@ ${gateError.message}`, "ULW_LOOP_QUALITY_GATE_INVALID", {
|
|
|
418
425
|
}
|
|
419
426
|
|
|
420
427
|
// components/ulw-loop/src/plan-io.ts
|
|
428
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
421
429
|
import { createReadStream, readdirSync } from "node:fs";
|
|
422
430
|
import { appendFile, mkdir, readFile as readFile3, rename, writeFile } from "node:fs/promises";
|
|
423
431
|
import { createInterface } from "node:readline";
|
|
@@ -433,6 +441,17 @@ function planMissingRecovery(existingSessionIds) {
|
|
|
433
441
|
return { message: lines.join(`
|
|
434
442
|
`), details: { existingSessionIds } };
|
|
435
443
|
}
|
|
444
|
+
function sessionScopeRequiredMessage(flag, existingSessionIds) {
|
|
445
|
+
const lines = [
|
|
446
|
+
"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.",
|
|
447
|
+
`Recovery: pass the scope explicitly: \`${flag} <id>\` (subprocess, eval, and hook contexts do not inherit the session env).`
|
|
448
|
+
];
|
|
449
|
+
if (existingSessionIds.length > 0) {
|
|
450
|
+
lines.push(`Existing ulw-loop session ids under .omo/ulw-loop/: ${existingSessionIds.join(", ")}.`);
|
|
451
|
+
}
|
|
452
|
+
return lines.join(`
|
|
453
|
+
`);
|
|
454
|
+
}
|
|
436
455
|
function sessionIdRequiredMessage(flag) {
|
|
437
456
|
return [
|
|
438
457
|
`${flag} requires a non-empty value.`,
|
|
@@ -442,11 +461,205 @@ function sessionIdRequiredMessage(flag) {
|
|
|
442
461
|
`);
|
|
443
462
|
}
|
|
444
463
|
|
|
464
|
+
// components/ulw-loop/src/state-lock.ts
|
|
465
|
+
import { randomUUID } from "node:crypto";
|
|
466
|
+
import { closeSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, writeSync } from "node:fs";
|
|
467
|
+
import { dirname } from "node:path";
|
|
468
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
469
|
+
var ULW_LOOP_LOCK_TIMEOUT_CODE = "ULW_LOOP_LOCK_TIMEOUT";
|
|
470
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
471
|
+
var DEFAULT_STALE_MS = 60000;
|
|
472
|
+
var MIN_DELAY_MS = 5;
|
|
473
|
+
var MAX_DELAY_MS = 100;
|
|
474
|
+
var SLEEP_CELL = new Int32Array(new SharedArrayBuffer(4));
|
|
475
|
+
async function withStateLock(lockPath, fn, options = {}) {
|
|
476
|
+
const token = await acquireAsync(lockPath, options);
|
|
477
|
+
try {
|
|
478
|
+
return await fn();
|
|
479
|
+
} finally {
|
|
480
|
+
release(lockPath, token);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
function withStateLockSync(lockPath, fn, options = {}) {
|
|
484
|
+
const token = acquireSync(lockPath, options);
|
|
485
|
+
try {
|
|
486
|
+
return fn();
|
|
487
|
+
} finally {
|
|
488
|
+
release(lockPath, token);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
function isStateLockTimeout(error) {
|
|
492
|
+
return error instanceof UlwLoopError && error.code === ULW_LOOP_LOCK_TIMEOUT_CODE;
|
|
493
|
+
}
|
|
494
|
+
async function acquireAsync(lockPath, options) {
|
|
495
|
+
const deadline = Date.now() + (options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
496
|
+
const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
|
|
497
|
+
mkdirSync(dirname(lockPath), { recursive: true });
|
|
498
|
+
for (let attempt = 0;; ) {
|
|
499
|
+
const outcome = attemptOnce(lockPath, staleMs);
|
|
500
|
+
if (outcome.kind === "acquired")
|
|
501
|
+
return outcome.token;
|
|
502
|
+
if (outcome.kind === "retry")
|
|
503
|
+
continue;
|
|
504
|
+
if (Date.now() >= deadline)
|
|
505
|
+
throw lockTimeout(lockPath, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
506
|
+
await sleep(backoffMs(attempt));
|
|
507
|
+
attempt += 1;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
function acquireSync(lockPath, options) {
|
|
511
|
+
const deadline = Date.now() + (options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
512
|
+
const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
|
|
513
|
+
mkdirSync(dirname(lockPath), { recursive: true });
|
|
514
|
+
for (let attempt = 0;; ) {
|
|
515
|
+
const outcome = attemptOnce(lockPath, staleMs);
|
|
516
|
+
if (outcome.kind === "acquired")
|
|
517
|
+
return outcome.token;
|
|
518
|
+
if (outcome.kind === "retry")
|
|
519
|
+
continue;
|
|
520
|
+
if (Date.now() >= deadline)
|
|
521
|
+
throw lockTimeout(lockPath, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
522
|
+
Atomics.wait(SLEEP_CELL, 0, 0, backoffMs(attempt));
|
|
523
|
+
attempt += 1;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
function attemptOnce(lockPath, staleMs) {
|
|
527
|
+
try {
|
|
528
|
+
const token = tryCreate(lockPath);
|
|
529
|
+
if (token !== null)
|
|
530
|
+
return { kind: "acquired", token };
|
|
531
|
+
const snapshot = readSnapshot(lockPath);
|
|
532
|
+
if (snapshot === null)
|
|
533
|
+
return { kind: "retry" };
|
|
534
|
+
if (isStale(snapshot, staleMs) && reclaim(lockPath, snapshot.raw))
|
|
535
|
+
return { kind: "retry" };
|
|
536
|
+
return { kind: "wait" };
|
|
537
|
+
} catch (error) {
|
|
538
|
+
if (hasCode(error, "EINTR"))
|
|
539
|
+
return { kind: "wait" };
|
|
540
|
+
throw error;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
function tryCreate(lockPath) {
|
|
544
|
+
let fd;
|
|
545
|
+
try {
|
|
546
|
+
fd = openSync(lockPath, "wx");
|
|
547
|
+
} catch (error) {
|
|
548
|
+
if (hasCode(error, "EEXIST"))
|
|
549
|
+
return null;
|
|
550
|
+
throw error;
|
|
551
|
+
}
|
|
552
|
+
const record = { pid: process.pid, createdAt: new Date().toISOString(), token: randomUUID() };
|
|
553
|
+
try {
|
|
554
|
+
writeSync(fd, JSON.stringify(record));
|
|
555
|
+
} catch (error) {
|
|
556
|
+
closeSync(fd);
|
|
557
|
+
try {
|
|
558
|
+
unlinkSync(lockPath);
|
|
559
|
+
} catch {}
|
|
560
|
+
throw error;
|
|
561
|
+
}
|
|
562
|
+
closeSync(fd);
|
|
563
|
+
return record.token;
|
|
564
|
+
}
|
|
565
|
+
function readSnapshot(lockPath) {
|
|
566
|
+
try {
|
|
567
|
+
const raw = readFileSync(lockPath, "utf8");
|
|
568
|
+
const ageMs = Date.now() - statSync(lockPath).mtimeMs;
|
|
569
|
+
return { raw, record: parseRecord(raw), ageMs };
|
|
570
|
+
} catch (error) {
|
|
571
|
+
if (hasCode(error, "ENOENT"))
|
|
572
|
+
return null;
|
|
573
|
+
throw error;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
function parseRecord(raw) {
|
|
577
|
+
try {
|
|
578
|
+
const parsed = JSON.parse(raw);
|
|
579
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
580
|
+
return null;
|
|
581
|
+
const record = parsed;
|
|
582
|
+
const pid = record["pid"];
|
|
583
|
+
const createdAt = record["createdAt"];
|
|
584
|
+
const token = record["token"];
|
|
585
|
+
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0 || typeof createdAt !== "string")
|
|
586
|
+
return null;
|
|
587
|
+
if (typeof token !== "string" || token.length === 0)
|
|
588
|
+
return null;
|
|
589
|
+
return { pid, createdAt, token };
|
|
590
|
+
} catch (error) {
|
|
591
|
+
if (error instanceof SyntaxError)
|
|
592
|
+
return null;
|
|
593
|
+
throw error;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
function isStale(snapshot, staleMs) {
|
|
597
|
+
if (snapshot.record === null)
|
|
598
|
+
return snapshot.ageMs > staleMs;
|
|
599
|
+
return !isProcessAlive(snapshot.record.pid);
|
|
600
|
+
}
|
|
601
|
+
function isProcessAlive(pid) {
|
|
602
|
+
try {
|
|
603
|
+
process.kill(pid, 0);
|
|
604
|
+
return true;
|
|
605
|
+
} catch (error) {
|
|
606
|
+
if (hasCode(error, "ESRCH"))
|
|
607
|
+
return false;
|
|
608
|
+
if (hasCode(error, "EPERM"))
|
|
609
|
+
return true;
|
|
610
|
+
throw error;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
function reclaim(lockPath, expectedRaw) {
|
|
614
|
+
const current = readSnapshot(lockPath);
|
|
615
|
+
if (current === null)
|
|
616
|
+
return true;
|
|
617
|
+
if (current.raw !== expectedRaw)
|
|
618
|
+
return false;
|
|
619
|
+
try {
|
|
620
|
+
unlinkSync(lockPath);
|
|
621
|
+
} catch (error) {
|
|
622
|
+
if (!hasCode(error, "ENOENT"))
|
|
623
|
+
throw error;
|
|
624
|
+
}
|
|
625
|
+
return true;
|
|
626
|
+
}
|
|
627
|
+
function release(lockPath, token) {
|
|
628
|
+
const current = readSnapshot(lockPath);
|
|
629
|
+
if (current === null || current.record?.token !== token)
|
|
630
|
+
return;
|
|
631
|
+
try {
|
|
632
|
+
unlinkSync(lockPath);
|
|
633
|
+
} catch (error) {
|
|
634
|
+
if (!hasCode(error, "ENOENT"))
|
|
635
|
+
throw error;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
function backoffMs(attempt) {
|
|
639
|
+
const exponential = Math.min(MAX_DELAY_MS, MIN_DELAY_MS * 2 ** attempt);
|
|
640
|
+
return exponential + Math.random() * MIN_DELAY_MS;
|
|
641
|
+
}
|
|
642
|
+
function lockTimeout(lockPath, timeoutMs) {
|
|
643
|
+
const holder = readSnapshot(lockPath)?.record;
|
|
644
|
+
const owner = holder === undefined || holder === null ? "another process" : `pid ${holder.pid}`;
|
|
645
|
+
return new UlwLoopError(`ulw-loop state lock ${lockPath} is held by ${owner} for more than ${timeoutMs}ms; retry once that process finishes, or delete the lock file if that process is gone.`, ULW_LOOP_LOCK_TIMEOUT_CODE, {
|
|
646
|
+
details: {
|
|
647
|
+
lockPath,
|
|
648
|
+
timeoutMs,
|
|
649
|
+
...holder === undefined || holder === null ? {} : { holderPid: holder.pid }
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
function hasCode(error, code) {
|
|
654
|
+
return error instanceof Error && "code" in error && error.code === code;
|
|
655
|
+
}
|
|
656
|
+
|
|
445
657
|
// components/ulw-loop/src/plan-io.ts
|
|
446
658
|
var LEGACY_OBJECTIVE_PREFIX = `Complete all ulw-loop stories in ${ULW_LOOP_DIR}/${ULW_LOOP_GOALS}: `;
|
|
447
659
|
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.`;
|
|
448
660
|
var locks = new Map;
|
|
449
|
-
|
|
661
|
+
var heldLocks = new AsyncLocalStorage;
|
|
662
|
+
function hasCode2(error, code) {
|
|
450
663
|
return error instanceof Error && "code" in error && error.code === code;
|
|
451
664
|
}
|
|
452
665
|
function isLegacyEnumeratedAggregateObjective(objective) {
|
|
@@ -461,8 +674,10 @@ async function withUlwLoopMutationLock(repoRoot, scopeOrFn, maybeFn) {
|
|
|
461
674
|
if (fn === undefined)
|
|
462
675
|
throw new UlwLoopError("Missing ulw-loop mutation body.", "ULW_LOOP_LOCK_BODY_MISSING");
|
|
463
676
|
const lockKey = `${repoRoot}\x00${ulwLoopRelativeDir(scope)}`;
|
|
677
|
+
const lockPath = ulwLoopStateLockPath(repoRoot, scope);
|
|
678
|
+
const locked = () => withStateLock(lockPath, () => heldLocks.run(new Set([...heldLocks.getStore() ?? [], lockKey]), fn));
|
|
464
679
|
const prior = locks.get(lockKey) ?? Promise.resolve(undefined);
|
|
465
|
-
const run = prior.then(
|
|
680
|
+
const run = prior.then(locked, locked);
|
|
466
681
|
const gate = run.then(() => {
|
|
467
682
|
return;
|
|
468
683
|
}, () => {
|
|
@@ -481,9 +696,9 @@ async function readUlwLoopPlan(repoRoot, scope) {
|
|
|
481
696
|
try {
|
|
482
697
|
raw = await readFile3(path, "utf8");
|
|
483
698
|
} catch (error) {
|
|
484
|
-
if (!
|
|
699
|
+
if (!hasCode2(error, "ENOENT"))
|
|
485
700
|
throw error;
|
|
486
|
-
const recovery = planMissingRecovery(
|
|
701
|
+
const recovery = planMissingRecovery(listUlwLoopSessionIds(repoRoot));
|
|
487
702
|
throw new UlwLoopError(`No ulw-loop plan found at ${repoRelative(path, repoRoot)}.
|
|
488
703
|
${recovery.message}`, "ULW_LOOP_PLAN_MISSING", { cause: error, ...recovery.details === undefined ? {} : { details: recovery.details } });
|
|
489
704
|
}
|
|
@@ -493,6 +708,9 @@ ${recovery.message}`, "ULW_LOOP_PLAN_MISSING", { cause: error, ...recovery.detai
|
|
|
493
708
|
}
|
|
494
709
|
const previousObjective = parsed.codexObjective;
|
|
495
710
|
if ((parsed.codexGoalMode ?? "per_story") === "aggregate" && isLegacyEnumeratedAggregateObjective(previousObjective)) {
|
|
711
|
+
if (!(heldLocks.getStore()?.has(`${repoRoot}\x00${ulwLoopRelativeDir(scope)}`) ?? false)) {
|
|
712
|
+
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");
|
|
713
|
+
}
|
|
496
714
|
const now = iso();
|
|
497
715
|
parsed.codexObjective = aggregateCodexObjectiveForScope(scope);
|
|
498
716
|
parsed.codexObjectiveAliases = [...new Set([...parsed.codexObjectiveAliases ?? [], previousObjective])];
|
|
@@ -508,7 +726,7 @@ ${recovery.message}`, "ULW_LOOP_PLAN_MISSING", { cause: error, ...recovery.detai
|
|
|
508
726
|
}
|
|
509
727
|
return parsed;
|
|
510
728
|
}
|
|
511
|
-
function
|
|
729
|
+
function listUlwLoopSessionIds(repoRoot) {
|
|
512
730
|
try {
|
|
513
731
|
return readdirSync(ulwLoopDir(repoRoot), { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
|
|
514
732
|
} catch {
|
|
@@ -543,7 +761,7 @@ async function* ledgerLines(repoRoot, scope) {
|
|
|
543
761
|
yield line;
|
|
544
762
|
}
|
|
545
763
|
} catch (error) {
|
|
546
|
-
if (!
|
|
764
|
+
if (!hasCode2(error, "ENOENT"))
|
|
547
765
|
throw error;
|
|
548
766
|
} finally {
|
|
549
767
|
lines.close();
|
|
@@ -895,8 +1113,8 @@ function adversarialVerdict(row, field) {
|
|
|
895
1113
|
}
|
|
896
1114
|
|
|
897
1115
|
// components/ulw-loop/src/surface.ts
|
|
898
|
-
import { existsSync as existsSync3, readFileSync } from "node:fs";
|
|
899
|
-
import { dirname, join as join2 } from "node:path";
|
|
1116
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "node:fs";
|
|
1117
|
+
import { dirname as dirname2, join as join2 } from "node:path";
|
|
900
1118
|
import { fileURLToPath } from "node:url";
|
|
901
1119
|
var REVIEWER_ROLES_BY_SURFACE = {
|
|
902
1120
|
lazycodex: {
|
|
@@ -935,7 +1153,7 @@ function resolveToolkitSurface(options) {
|
|
|
935
1153
|
const fromEnv = parseSurface(env[SURFACE_ENV_KEY]);
|
|
936
1154
|
if (fromEnv !== null)
|
|
937
1155
|
return fromEnv;
|
|
938
|
-
const entryDir = options?.entryDir ??
|
|
1156
|
+
const entryDir = options?.entryDir ?? dirname2(fileURLToPath(import.meta.url));
|
|
939
1157
|
const markerPath = join2(entryDir, SURFACE_MARKER_FILENAME);
|
|
940
1158
|
return readMarkerSurface(markerPath) ?? "lazycodex";
|
|
941
1159
|
}
|
|
@@ -943,7 +1161,7 @@ function readMarkerSurface(markerPath) {
|
|
|
943
1161
|
try {
|
|
944
1162
|
if (!existsSync3(markerPath))
|
|
945
1163
|
return null;
|
|
946
|
-
const parsed = JSON.parse(
|
|
1164
|
+
const parsed = JSON.parse(readFileSync2(markerPath, "utf8"));
|
|
947
1165
|
return parseSurface(parsed["surface"]);
|
|
948
1166
|
} catch (error) {
|
|
949
1167
|
if (error instanceof Error)
|
|
@@ -1356,7 +1574,7 @@ function fail(message, code = "ULW_LOOP_VALIDATION_BATCH_INVALID") {
|
|
|
1356
1574
|
}
|
|
1357
1575
|
|
|
1358
1576
|
// components/ulw-loop/src/checkpoint.ts
|
|
1359
|
-
var QUALITY_GATE_FS = { existsSync: existsSync4, statSync };
|
|
1577
|
+
var QUALITY_GATE_FS = { existsSync: existsSync4, statSync: statSync2 };
|
|
1360
1578
|
function ulwLoopFail2(message, code) {
|
|
1361
1579
|
throw new UlwLoopError(message, code);
|
|
1362
1580
|
}
|
|
@@ -1659,7 +1877,7 @@ var ULW_LOOP_HELP = `Usage:
|
|
|
1659
1877
|
omo-agent-toolkit ulw-loop add-goal --title "..." --objective "..." [--json]
|
|
1660
1878
|
omo-agent-toolkit ulw-loop record-review-blockers --goal-id <id> --title "..." --objective "..." --evidence "..." --codex-goal-json <...> [--json]
|
|
1661
1879
|
|
|
1662
|
-
|
|
1880
|
+
Every state subcommand needs a session scope: [--session-id <id>] or the session env (OMO_ULW_LOOP_SESSION_ID / CODEX_SESSION_ID / CODEX_THREAD_ID / PI_SESSION_ID); state lives under .omo/ulw-loop/<id>/ and the unscoped root is never used implicitly.
|
|
1663
1881
|
Every subcommand accepts --help | -h to print its own usage line.`;
|
|
1664
1882
|
function subcommandHelp(subcommand) {
|
|
1665
1883
|
const lines = ULW_LOOP_HELP.split(`
|
|
@@ -3171,7 +3389,6 @@ async function ulwLoopCommand(argv) {
|
|
|
3171
3389
|
const repoRoot = process.cwd();
|
|
3172
3390
|
const json = hasFlag(rest, "--json");
|
|
3173
3391
|
try {
|
|
3174
|
-
const scope = commandScope(rest);
|
|
3175
3392
|
if (!isUlwLoopSubcommand(command)) {
|
|
3176
3393
|
if (json) {
|
|
3177
3394
|
printJsonError(new UlwLoopError(`Unknown ulw-loop subcommand: ${command}.`, "ULW_LOOP_SUBCOMMAND_UNKNOWN", {
|
|
@@ -3188,11 +3405,13 @@ async function ulwLoopCommand(argv) {
|
|
|
3188
3405
|
`);
|
|
3189
3406
|
return 0;
|
|
3190
3407
|
}
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
process.stdout.write(`${ULW_LOOP_HELP}
|
|
3408
|
+
if (command === "help") {
|
|
3409
|
+
process.stdout.write(`${ULW_LOOP_HELP}
|
|
3194
3410
|
`);
|
|
3195
|
-
|
|
3411
|
+
return 0;
|
|
3412
|
+
}
|
|
3413
|
+
const scope = commandScope(repoRoot, rest);
|
|
3414
|
+
switch (command) {
|
|
3196
3415
|
case "create-goals":
|
|
3197
3416
|
return await createGoals(repoRoot, rest, json, scope);
|
|
3198
3417
|
case "status":
|
|
@@ -3238,7 +3457,7 @@ var SESSION_ID_FLAG = "--session-id";
|
|
|
3238
3457
|
function sessionIdFlagPresent(argv) {
|
|
3239
3458
|
return hasFlag(argv, SESSION_ID_FLAG) || argv.some((arg) => arg.startsWith(`${SESSION_ID_FLAG}=`));
|
|
3240
3459
|
}
|
|
3241
|
-
function commandScope(argv) {
|
|
3460
|
+
function commandScope(repoRoot, argv) {
|
|
3242
3461
|
if (sessionIdFlagPresent(argv)) {
|
|
3243
3462
|
const sessionId2 = readValue(argv, SESSION_ID_FLAG)?.trim();
|
|
3244
3463
|
if (!sessionId2) {
|
|
@@ -3249,14 +3468,17 @@ function commandScope(argv) {
|
|
|
3249
3468
|
return { sessionId: sessionId2 };
|
|
3250
3469
|
}
|
|
3251
3470
|
const sessionId = resolveUlwLoopSessionIdFromEnv();
|
|
3252
|
-
|
|
3471
|
+
if (sessionId !== null)
|
|
3472
|
+
return { sessionId };
|
|
3473
|
+
const existingSessionIds = listUlwLoopSessionIds(repoRoot);
|
|
3474
|
+
throw new UlwLoopError(sessionScopeRequiredMessage(SESSION_ID_FLAG, existingSessionIds), "ULW_LOOP_SESSION_SCOPE_REQUIRED", { details: { flag: SESSION_ID_FLAG, existingSessionIds } });
|
|
3253
3475
|
}
|
|
3254
3476
|
|
|
3255
3477
|
// components/ulw-loop/src/ultrawork-directive.ts
|
|
3256
|
-
import { readFileSync as
|
|
3478
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
3257
3479
|
|
|
3258
3480
|
// components/ulw-loop/src/ultrawork-skill-pointer.ts
|
|
3259
|
-
import { existsSync as existsSync6, readFileSync as
|
|
3481
|
+
import { existsSync as existsSync6, readFileSync as readFileSync3 } from "node:fs";
|
|
3260
3482
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
3261
3483
|
var ULTRAWORK_SKILL_POINTER_TEMPLATE = `<ultrawork-mode>
|
|
3262
3484
|
ULTRAWORK MODE IS ACTIVE FOR THIS TASK.
|
|
@@ -3288,7 +3510,7 @@ Do not start the requested work until all three steps are complete.
|
|
|
3288
3510
|
`;
|
|
3289
3511
|
var ULTRAWORK_SKILL_PATH_PLACEHOLDER = "{{ULTRAWORK_SKILL_PATH}}";
|
|
3290
3512
|
var ULTRAWORK_SKILL_FILE_URL = new URL("../../../skills/ultrawork/SKILL.md", import.meta.url);
|
|
3291
|
-
var ULTRAWORK_DIRECTIVE =
|
|
3513
|
+
var ULTRAWORK_DIRECTIVE = readFileSync3(new URL("../directive.md", import.meta.url), "utf8");
|
|
3292
3514
|
function resolveUltraworkSkillFilePath() {
|
|
3293
3515
|
return fileURLToPath2(ULTRAWORK_SKILL_FILE_URL);
|
|
3294
3516
|
}
|
|
@@ -3351,7 +3573,7 @@ function hasUltraworkDirectiveAlreadyInTranscript(transcriptPath) {
|
|
|
3351
3573
|
return false;
|
|
3352
3574
|
}
|
|
3353
3575
|
function readTranscriptTail(transcriptPath) {
|
|
3354
|
-
const rawTranscript =
|
|
3576
|
+
const rawTranscript = readFileSync4(transcriptPath);
|
|
3355
3577
|
return rawTranscript.subarray(Math.max(0, rawTranscript.byteLength - TRANSCRIPT_SEARCH_BYTES)).toString("utf8");
|
|
3356
3578
|
}
|
|
3357
3579
|
function isUltraworkPrompt(prompt) {
|
|
@@ -3545,18 +3767,8 @@ function readAll(stdin) {
|
|
|
3545
3767
|
|
|
3546
3768
|
// components/ulw-loop/src/spawn-guard.ts
|
|
3547
3769
|
import { randomBytes } from "node:crypto";
|
|
3548
|
-
import {
|
|
3549
|
-
|
|
3550
|
-
mkdirSync,
|
|
3551
|
-
openSync,
|
|
3552
|
-
readdirSync as readdirSync2,
|
|
3553
|
-
readFileSync as readFileSync4,
|
|
3554
|
-
renameSync,
|
|
3555
|
-
statSync as statSync2,
|
|
3556
|
-
unlinkSync,
|
|
3557
|
-
writeFileSync
|
|
3558
|
-
} from "node:fs";
|
|
3559
|
-
import { dirname as dirname2, join as join3 } from "node:path";
|
|
3770
|
+
import { existsSync as existsSync7, readdirSync as readdirSync2, readFileSync as readFileSync5, renameSync, statSync as statSync3, writeFileSync } from "node:fs";
|
|
3771
|
+
import { dirname as dirname3, join as join3 } from "node:path";
|
|
3560
3772
|
var SPAWN_TOOL_TOKENS = new Set(["spawn_agent", "collaborationspawn_agent", "collaboration.spawn_agent"]);
|
|
3561
3773
|
var DEFAULT_FANOUT_LIMIT = 60;
|
|
3562
3774
|
var DEFAULT_REVIEW_SPAWN_LIMIT = 3;
|
|
@@ -3567,13 +3779,24 @@ var REVIEW_AGENT_TYPES = [
|
|
|
3567
3779
|
...Object.values(REVIEWER_ROLES_BY_SURFACE).map((roles) => roles.manualQa)
|
|
3568
3780
|
];
|
|
3569
3781
|
var REVIEW_AGENT_TYPE_SET = new Set(REVIEW_AGENT_TYPES);
|
|
3570
|
-
function applySpawnGuards(payload) {
|
|
3782
|
+
function applySpawnGuards(payload, options = {}) {
|
|
3571
3783
|
if (payload.hook_event_name !== "PreToolUse" || !SPAWN_TOOL_TOKENS.has(payload.tool_name))
|
|
3572
3784
|
return "";
|
|
3573
|
-
const
|
|
3785
|
+
const scope = { sessionId: payload.session_id };
|
|
3786
|
+
const stateDir = ulwLoopDir(payload.cwd, scope);
|
|
3574
3787
|
const plan = readPlan(join3(stateDir, "goals.json"));
|
|
3575
3788
|
if (plan === null)
|
|
3576
3789
|
return "";
|
|
3790
|
+
const lockOptions = options.lockTimeoutMs === undefined ? {} : { timeoutMs: options.lockTimeoutMs };
|
|
3791
|
+
try {
|
|
3792
|
+
return withStateLockSync(ulwLoopStateLockPath(payload.cwd, scope), () => evaluateGuards(payload, plan, stateDir), lockOptions);
|
|
3793
|
+
} catch (error) {
|
|
3794
|
+
if (isStateLockTimeout(error))
|
|
3795
|
+
return deny(`ulw-loop spawn guard could not take the session state lock: ${error.message}`);
|
|
3796
|
+
throw error;
|
|
3797
|
+
}
|
|
3798
|
+
}
|
|
3799
|
+
function evaluateGuards(payload, plan, stateDir) {
|
|
3577
3800
|
const fanOutPeek = peekFanOutBudget(stateDir);
|
|
3578
3801
|
if (fanOutPeek !== null)
|
|
3579
3802
|
return deny(fanOutPeek);
|
|
@@ -3629,18 +3852,15 @@ function consumeReviewSpawnBudget(payload, plan, stateDir) {
|
|
|
3629
3852
|
if (goal3 === undefined)
|
|
3630
3853
|
return null;
|
|
3631
3854
|
const counterPath = join3(stateDir, "review-spawn-counts.json");
|
|
3632
|
-
const lockPath = `${counterPath}.lock`;
|
|
3633
3855
|
const limit = reviewSpawnLimit();
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
return null;
|
|
3643
|
-
});
|
|
3856
|
+
const counts = readCounts(counterPath);
|
|
3857
|
+
const key = `${agentType}:${goal3.id}:a${goal3.attempt}`;
|
|
3858
|
+
const count = (counts[key] ?? 0) + 1;
|
|
3859
|
+
if (count > limit)
|
|
3860
|
+
return `ulw-loop reviewer no-progress cap reached (${agentType} ${count}/${limit}) for ${goal3.id} attempt ${goal3.attempt}. Consolidate existing review findings, or checkpoint and start a new attempt after concrete progress.`;
|
|
3861
|
+
counts[key] = count;
|
|
3862
|
+
atomicWriteJson(counterPath, counts);
|
|
3863
|
+
return null;
|
|
3644
3864
|
}
|
|
3645
3865
|
function missingGateArtifact(payload, plan) {
|
|
3646
3866
|
if (!isGateReviewerSpawn(payload.tool_input))
|
|
@@ -3718,40 +3938,8 @@ function activeSurfaceReviewerAlias(reviewer) {
|
|
|
3718
3938
|
}
|
|
3719
3939
|
return reviewer;
|
|
3720
3940
|
}
|
|
3721
|
-
function withExclusiveLock(lockPath, fn) {
|
|
3722
|
-
mkdirSync(dirname2(lockPath), { recursive: true });
|
|
3723
|
-
const maxAttempts = 10;
|
|
3724
|
-
const baseDelayMs = 10;
|
|
3725
|
-
for (let attempt = 0;attempt < maxAttempts; attempt++) {
|
|
3726
|
-
let fd = null;
|
|
3727
|
-
try {
|
|
3728
|
-
fd = openSync(lockPath, "wx");
|
|
3729
|
-
writeFileSync(fd, process.pid.toString());
|
|
3730
|
-
try {
|
|
3731
|
-
return fn();
|
|
3732
|
-
} finally {
|
|
3733
|
-
try {
|
|
3734
|
-
unlinkSync(lockPath);
|
|
3735
|
-
} catch {}
|
|
3736
|
-
}
|
|
3737
|
-
} catch (error) {
|
|
3738
|
-
if (fd !== null) {
|
|
3739
|
-
try {
|
|
3740
|
-
unlinkSync(lockPath);
|
|
3741
|
-
} catch {}
|
|
3742
|
-
}
|
|
3743
|
-
if (error.code !== "EEXIST") {
|
|
3744
|
-
return fn();
|
|
3745
|
-
}
|
|
3746
|
-
const delayMs = baseDelayMs * 2 ** attempt + Math.random() * baseDelayMs;
|
|
3747
|
-
const deadline = Date.now() + delayMs;
|
|
3748
|
-
while (Date.now() < deadline) {}
|
|
3749
|
-
}
|
|
3750
|
-
}
|
|
3751
|
-
return fn();
|
|
3752
|
-
}
|
|
3753
3941
|
function atomicWriteJson(targetPath, data) {
|
|
3754
|
-
const tmp = join3(
|
|
3942
|
+
const tmp = join3(dirname3(targetPath), `.tmp-${randomBytes(6).toString("hex")}`);
|
|
3755
3943
|
writeFileSync(tmp, JSON.stringify(data));
|
|
3756
3944
|
renameSync(tmp, targetPath);
|
|
3757
3945
|
}
|
|
@@ -3782,7 +3970,7 @@ function reviewSpawnLimit() {
|
|
|
3782
3970
|
}
|
|
3783
3971
|
function isNonEmptyFile(path) {
|
|
3784
3972
|
try {
|
|
3785
|
-
return existsSync7(path) &&
|
|
3973
|
+
return existsSync7(path) && statSync3(path).size > 0;
|
|
3786
3974
|
} catch (error) {
|
|
3787
3975
|
if (error instanceof Error)
|
|
3788
3976
|
return false;
|
|
@@ -3800,7 +3988,7 @@ function hasOtherEvidenceFile(evidenceDir, excludedName) {
|
|
|
3800
3988
|
}
|
|
3801
3989
|
function readCount(counterPath) {
|
|
3802
3990
|
try {
|
|
3803
|
-
const parsed = JSON.parse(
|
|
3991
|
+
const parsed = JSON.parse(readFileSync5(counterPath, "utf8"));
|
|
3804
3992
|
return typeof parsed["count"] === "number" && parsed["count"] >= 0 ? parsed["count"] : 0;
|
|
3805
3993
|
} catch (error) {
|
|
3806
3994
|
if (error instanceof Error)
|
|
@@ -3810,7 +3998,7 @@ function readCount(counterPath) {
|
|
|
3810
3998
|
}
|
|
3811
3999
|
function readCounts(counterPath) {
|
|
3812
4000
|
try {
|
|
3813
|
-
const parsed = JSON.parse(
|
|
4001
|
+
const parsed = JSON.parse(readFileSync5(counterPath, "utf8"));
|
|
3814
4002
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
3815
4003
|
return {};
|
|
3816
4004
|
const counts = {};
|
|
@@ -3827,7 +4015,7 @@ function readCounts(counterPath) {
|
|
|
3827
4015
|
}
|
|
3828
4016
|
function readPlan(goalsPath) {
|
|
3829
4017
|
try {
|
|
3830
|
-
return JSON.parse(
|
|
4018
|
+
return JSON.parse(readFileSync5(goalsPath, "utf8"));
|
|
3831
4019
|
} catch (error) {
|
|
3832
4020
|
if (error instanceof Error)
|
|
3833
4021
|
return null;
|
|
@@ -3836,7 +4024,7 @@ function readPlan(goalsPath) {
|
|
|
3836
4024
|
}
|
|
3837
4025
|
|
|
3838
4026
|
// components/ulw-loop/src/stop-resume-hook.ts
|
|
3839
|
-
import { existsSync as existsSync8, readFileSync as
|
|
4027
|
+
import { existsSync as existsSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "node:fs";
|
|
3840
4028
|
import { isAbsolute as isAbsolute2, join as join4, resolve as resolve4, sep as sep2 } from "node:path";
|
|
3841
4029
|
var RESUME_CAP = 2;
|
|
3842
4030
|
var CONTEXT_PRESSURE_MARKERS2 = [
|
|
@@ -3856,14 +4044,15 @@ function runStopResumeHook(input) {
|
|
|
3856
4044
|
return "";
|
|
3857
4045
|
if (boulderContinuationWillFire(payload.cwd, payload.session_id))
|
|
3858
4046
|
return "";
|
|
3859
|
-
const
|
|
4047
|
+
const scope = { sessionId: payload.session_id };
|
|
4048
|
+
const stateDir = ulwLoopDir(payload.cwd, scope);
|
|
3860
4049
|
const plan = readPlan2(join4(stateDir, "goals.json"));
|
|
3861
4050
|
if (plan === null || plan.aggregateCompletion?.status === "complete")
|
|
3862
4051
|
return "";
|
|
3863
4052
|
const goal3 = resumableGoal(plan);
|
|
3864
4053
|
if (goal3 === undefined)
|
|
3865
4054
|
return "";
|
|
3866
|
-
if (!
|
|
4055
|
+
if (!consumeResumeBudgetLocked(ulwLoopStateLockPath(payload.cwd, scope), stateDir, goal3.id))
|
|
3867
4056
|
return "";
|
|
3868
4057
|
const output = {
|
|
3869
4058
|
decision: "block",
|
|
@@ -3893,6 +4082,15 @@ function resumableGoal(plan) {
|
|
|
3893
4082
|
function isResumableStatus(status2) {
|
|
3894
4083
|
return status2 === "pending" || status2 === "in_progress";
|
|
3895
4084
|
}
|
|
4085
|
+
function consumeResumeBudgetLocked(lockPath, stateDir, goalId) {
|
|
4086
|
+
try {
|
|
4087
|
+
return withStateLockSync(lockPath, () => consumeResumeBudget(stateDir, goalId));
|
|
4088
|
+
} catch (error) {
|
|
4089
|
+
if (isStateLockTimeout(error))
|
|
4090
|
+
return false;
|
|
4091
|
+
throw error;
|
|
4092
|
+
}
|
|
4093
|
+
}
|
|
3896
4094
|
function consumeResumeBudget(stateDir, goalId) {
|
|
3897
4095
|
const ledgerLineCount = countLedgerLines(join4(stateDir, "ledger.jsonl"));
|
|
3898
4096
|
const counterPath = resolve4(stateDir, `auto-resume-${goalId}.json`);
|
|
@@ -3927,7 +4125,7 @@ function renderResumeDirective(plan, goal3, sessionId) {
|
|
|
3927
4125
|
}
|
|
3928
4126
|
function readPlan2(goalsPath) {
|
|
3929
4127
|
try {
|
|
3930
|
-
return JSON.parse(
|
|
4128
|
+
return JSON.parse(readFileSync6(goalsPath, "utf8"));
|
|
3931
4129
|
} catch (error) {
|
|
3932
4130
|
if (error instanceof Error)
|
|
3933
4131
|
return null;
|
|
@@ -3936,7 +4134,7 @@ function readPlan2(goalsPath) {
|
|
|
3936
4134
|
}
|
|
3937
4135
|
function countLedgerLines(ledgerPath) {
|
|
3938
4136
|
try {
|
|
3939
|
-
return
|
|
4137
|
+
return readFileSync6(ledgerPath, "utf8").split(`
|
|
3940
4138
|
`).filter(Boolean).length;
|
|
3941
4139
|
} catch (error) {
|
|
3942
4140
|
if (error instanceof Error)
|
|
@@ -3948,7 +4146,7 @@ function readCounter(counterPath) {
|
|
|
3948
4146
|
try {
|
|
3949
4147
|
if (!existsSync8(counterPath))
|
|
3950
4148
|
return null;
|
|
3951
|
-
const parsed = JSON.parse(
|
|
4149
|
+
const parsed = JSON.parse(readFileSync6(counterPath, "utf8"));
|
|
3952
4150
|
if (typeof parsed["count"] !== "number" || typeof parsed["ledgerLineCount"] !== "number")
|
|
3953
4151
|
return null;
|
|
3954
4152
|
return { count: parsed["count"], ledgerLineCount: parsed["ledgerLineCount"] };
|
|
@@ -3960,7 +4158,7 @@ function readCounter(counterPath) {
|
|
|
3960
4158
|
}
|
|
3961
4159
|
function boulderContinuationWillFire(cwd, sessionId) {
|
|
3962
4160
|
try {
|
|
3963
|
-
const raw = JSON.parse(
|
|
4161
|
+
const raw = JSON.parse(readFileSync6(join4(cwd, ".omo", "boulder.json"), "utf8"));
|
|
3964
4162
|
const works = raw["works"];
|
|
3965
4163
|
const entries = typeof works === "object" && works !== null ? Object.values(works) : [raw];
|
|
3966
4164
|
return entries.some((work) => {
|
|
@@ -3979,7 +4177,7 @@ function boulderContinuationWillFire(cwd, sessionId) {
|
|
|
3979
4177
|
}
|
|
3980
4178
|
function transcriptShowsContextPressure(transcriptPath) {
|
|
3981
4179
|
try {
|
|
3982
|
-
const transcript =
|
|
4180
|
+
const transcript = readFileSync6(transcriptPath, "utf8").toLowerCase();
|
|
3983
4181
|
return CONTEXT_PRESSURE_MARKERS2.some((marker) => transcript.includes(marker));
|
|
3984
4182
|
} catch (error) {
|
|
3985
4183
|
if (error instanceof Error)
|
|
@@ -3996,7 +4194,7 @@ function boulderPlanHasChecklist(cwd, entry) {
|
|
|
3996
4194
|
const candidates = typeof worktree === "string" && worktree.trim().length > 0 && !isAbsolute2(activePlan) ? [join4(isAbsolute2(worktree) ? worktree : join4(cwd, worktree), activePlan), planPath] : [planPath];
|
|
3997
4195
|
for (const candidate of candidates) {
|
|
3998
4196
|
try {
|
|
3999
|
-
return
|
|
4197
|
+
return readFileSync6(candidate, "utf8").split(/\r?\n/).some((line) => line.startsWith("- [ ] ") || line.startsWith("- [x] ") || line.startsWith("- [X] "));
|
|
4000
4198
|
} catch (error) {
|
|
4001
4199
|
if (!(error instanceof Error))
|
|
4002
4200
|
throw error;
|