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,74 +1,122 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
-
import {
|
|
3
|
-
import { appendFile, mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
4
|
-
import { createInterface } from "node:readline";
|
|
5
|
-
|
|
2
|
+
import { readdirSync } from "node:fs";
|
|
6
3
|
import { aggregateCodexObjectiveForScope } from "./goal-status.js";
|
|
4
|
+
import { readLedger as reconcileLedger } from "./ledger.js";
|
|
7
5
|
import {
|
|
8
6
|
repoRelative,
|
|
9
7
|
type UlwLoopScope,
|
|
10
8
|
ulwLoopDir,
|
|
11
9
|
ulwLoopGoalsPath,
|
|
12
|
-
ulwLoopLedgerPath,
|
|
13
10
|
ulwLoopRelativeDir,
|
|
14
11
|
ulwLoopStateLockPath,
|
|
15
12
|
} from "./paths.js";
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import
|
|
19
|
-
import {
|
|
13
|
+
import { commit, materialize, materializeSync } from "./plan-commit.js";
|
|
14
|
+
import { readOptional, reconcilePlan } from "./plan-log.js";
|
|
15
|
+
import { planMissingError } from "./plan-missing-recovery.js";
|
|
16
|
+
import { type StateLockOptions, withStateLock } from "./state-lock.js";
|
|
17
|
+
import {
|
|
18
|
+
iso,
|
|
19
|
+
ULW_LOOP_DIR,
|
|
20
|
+
ULW_LOOP_GOALS,
|
|
21
|
+
ULW_LOOP_LEDGER,
|
|
22
|
+
UlwLoopError,
|
|
23
|
+
type UlwLoopLedgerEntry,
|
|
24
|
+
type UlwLoopPlan,
|
|
25
|
+
} from "./types.js";
|
|
26
|
+
|
|
27
|
+
export function readLedger(repoRoot: string, scope?: UlwLoopScope): UlwLoopLedgerEntry[] {
|
|
28
|
+
const lockPath = ulwLoopStateLockPath(repoRoot, scope);
|
|
29
|
+
if (heldLocks.getStore()?.has(lockPath)) {
|
|
30
|
+
assertStateLockOwned(lockPath);
|
|
31
|
+
materializeSync(ulwLoopDir(repoRoot, scope));
|
|
32
|
+
}
|
|
33
|
+
return reconcileLedger(repoRoot, scope);
|
|
34
|
+
}
|
|
35
|
+
export { planExists } from "./plan-log.js";
|
|
20
36
|
|
|
21
37
|
const LEGACY_OBJECTIVE_PREFIX = `Complete all ulw-loop stories in ${ULW_LOOP_DIR}/${ULW_LOOP_GOALS}: `;
|
|
22
38
|
const 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.`;
|
|
23
39
|
const locks = new Map<string, Promise<undefined>>();
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const heldLocks = new AsyncLocalStorage<ReadonlySet<string>>();
|
|
28
|
-
|
|
29
|
-
function hasCode(error: unknown, code: string): boolean {
|
|
30
|
-
return error instanceof Error && "code" in error && error.code === code;
|
|
40
|
+
interface LockContext {
|
|
41
|
+
readonly token: string;
|
|
42
|
+
readonly migrations: Map<string, UlwLoopLedgerEntry[]>;
|
|
31
43
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
44
|
+
const heldLocks = new AsyncLocalStorage<ReadonlyMap<string, LockContext>>();
|
|
45
|
+
const lockOptions = new AsyncLocalStorage<StateLockOptions>();
|
|
46
|
+
export function withMutationLockOptions<T>(options: StateLockOptions, fn: () => Promise<T>): Promise<T> {
|
|
47
|
+
return lockOptions.run(options, fn);
|
|
35
48
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
49
|
+
function tokenAt(lockPath: string): string | undefined {
|
|
50
|
+
const raw = readOptional(lockPath);
|
|
51
|
+
if (raw === undefined) return undefined;
|
|
52
|
+
try {
|
|
53
|
+
const record: unknown = JSON.parse(raw);
|
|
54
|
+
return typeof record === "object" && record !== null && "token" in record && typeof record.token === "string"
|
|
55
|
+
? record.token
|
|
56
|
+
: undefined;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if (error instanceof SyntaxError) return undefined;
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export function assertStateLockOwned(lockPath: string): void {
|
|
63
|
+
const context = heldLocks.getStore()?.get(lockPath);
|
|
64
|
+
if (context === undefined) return;
|
|
65
|
+
if (tokenAt(lockPath) !== context.token)
|
|
66
|
+
throw new UlwLoopError("The ulw-loop mutation lock changed owners.", "ULW_LOOP_LOCK_LOST");
|
|
67
|
+
}
|
|
68
|
+
export function migrationEntries(plan: UlwLoopPlan): readonly UlwLoopLedgerEntry[] {
|
|
69
|
+
for (const context of heldLocks.getStore()?.values() ?? []) {
|
|
70
|
+
const entries = context.migrations.get(plan.goalsPath);
|
|
71
|
+
if (entries !== undefined) {
|
|
72
|
+
context.migrations.delete(plan.goalsPath);
|
|
73
|
+
return entries;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return [];
|
|
44
77
|
}
|
|
45
|
-
|
|
46
78
|
export async function withUlwLoopMutationLock<T>(repoRoot: string, fn: () => Promise<T>): Promise<T>;
|
|
47
79
|
export async function withUlwLoopMutationLock<T>(
|
|
48
80
|
repoRoot: string,
|
|
49
81
|
scope: UlwLoopScope | undefined,
|
|
50
82
|
fn: () => Promise<T>,
|
|
83
|
+
options?: StateLockOptions,
|
|
51
84
|
): Promise<T>;
|
|
52
85
|
export async function withUlwLoopMutationLock<T>(
|
|
53
86
|
repoRoot: string,
|
|
54
87
|
scopeOrFn: UlwLoopScope | (() => Promise<T>) | undefined,
|
|
55
88
|
maybeFn?: () => Promise<T>,
|
|
89
|
+
options: StateLockOptions = {},
|
|
56
90
|
): Promise<T> {
|
|
57
91
|
const scope = typeof scopeOrFn === "function" ? undefined : scopeOrFn;
|
|
58
92
|
const fn = typeof scopeOrFn === "function" ? scopeOrFn : maybeFn;
|
|
59
93
|
if (fn === undefined) throw new UlwLoopError("Missing ulw-loop mutation body.", "ULW_LOOP_LOCK_BODY_MISSING");
|
|
60
94
|
const lockKey = `${repoRoot}\0${ulwLoopRelativeDir(scope)}`;
|
|
61
95
|
const lockPath = ulwLoopStateLockPath(repoRoot, scope);
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
96
|
+
const locked = () =>
|
|
97
|
+
withStateLock(
|
|
98
|
+
lockPath,
|
|
99
|
+
async (token) => {
|
|
100
|
+
return heldLocks.run(
|
|
101
|
+
new Map([...(heldLocks.getStore() ?? []), [lockPath, { token, migrations: new Map() }]]),
|
|
102
|
+
async () => {
|
|
103
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
104
|
+
try {
|
|
105
|
+
return await fn();
|
|
106
|
+
} catch (error) {
|
|
107
|
+
if (!(error instanceof UlwLoopError) || error.code !== "ULW_LOOP_PUBLISH_CONFLICT") throw error;
|
|
108
|
+
assertStateLockOwned(lockPath);
|
|
109
|
+
if (attempt !== 0) throw error;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
);
|
|
114
|
+
},
|
|
115
|
+
{ ...lockOptions.getStore(), ...options },
|
|
116
|
+
);
|
|
66
117
|
const prior = locks.get(lockKey) ?? Promise.resolve(undefined);
|
|
67
118
|
const run = prior.then(locked, locked);
|
|
68
|
-
|
|
69
|
-
// (plans/audits), and it removes itself once no newer waiter replaced it —
|
|
70
|
-
// otherwise a long-lived host leaks one entry per (repo, scope) forever.
|
|
71
|
-
const gate: Promise<undefined> = run.then(
|
|
119
|
+
const gate = run.then(
|
|
72
120
|
() => undefined,
|
|
73
121
|
() => undefined,
|
|
74
122
|
);
|
|
@@ -78,60 +126,41 @@ export async function withUlwLoopMutationLock<T>(
|
|
|
78
126
|
});
|
|
79
127
|
return run;
|
|
80
128
|
}
|
|
81
|
-
|
|
82
|
-
export async function readUlwLoopPlan(repoRoot: string, scope?: UlwLoopScope): Promise<UlwLoopPlan> {
|
|
129
|
+
export function readUlwLoopPlanSync(repoRoot: string, scope?: UlwLoopScope): UlwLoopPlan {
|
|
83
130
|
const path = ulwLoopGoalsPath(repoRoot, scope);
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
} catch (error) {
|
|
88
|
-
if (!hasCode(error, "ENOENT")) throw error;
|
|
89
|
-
const recovery = planMissingRecovery(listUlwLoopSessionIds(repoRoot));
|
|
90
|
-
throw new UlwLoopError(
|
|
91
|
-
`No ulw-loop plan found at ${repoRelative(path, repoRoot)}.\n${recovery.message}`,
|
|
92
|
-
"ULW_LOOP_PLAN_MISSING",
|
|
93
|
-
{ cause: error, ...(recovery.details === undefined ? {} : { details: recovery.details }) },
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
const parsed: UlwLoopPlan = JSON.parse(raw);
|
|
97
|
-
if (parsed.version !== 1 || !Array.isArray(parsed.goals)) {
|
|
131
|
+
const parsed = reconcilePlan(ulwLoopDir(repoRoot, scope));
|
|
132
|
+
if (parsed === undefined) throw planMissingError(repoRelative(path, repoRoot), listUlwLoopSessionIds(repoRoot));
|
|
133
|
+
if (parsed.version !== 1 || !Array.isArray(parsed.goals))
|
|
98
134
|
throw new UlwLoopError(`Invalid ulw-loop plan at ${repoRelative(path, repoRoot)}.`, "ULW_LOOP_PLAN_INVALID");
|
|
99
|
-
}
|
|
100
135
|
const previousObjective = parsed.codexObjective;
|
|
101
136
|
if (
|
|
102
137
|
(parsed.codexGoalMode ?? "per_story") === "aggregate" &&
|
|
103
|
-
|
|
138
|
+
previousObjective !== undefined &&
|
|
139
|
+
(previousObjective === LEGACY_OBJECTIVE || previousObjective.startsWith(LEGACY_OBJECTIVE_PREFIX))
|
|
104
140
|
) {
|
|
105
|
-
if (!(heldLocks.getStore()?.has(`${repoRoot}\0${ulwLoopRelativeDir(scope)}`) ?? false)) {
|
|
106
|
-
// A read path (status/criteria) must not mutate state: mutating here runs
|
|
107
|
-
// unlocked and a second reader could write a partially-migrated plan.
|
|
108
|
-
throw new UlwLoopError(
|
|
109
|
-
`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.`,
|
|
110
|
-
"ULW_LOOP_MIGRATION_REQUIRED",
|
|
111
|
-
);
|
|
112
|
-
}
|
|
113
|
-
const now = iso();
|
|
114
141
|
parsed.codexObjective = aggregateCodexObjectiveForScope(scope);
|
|
115
142
|
parsed.codexObjectiveAliases = [...new Set([...(parsed.codexObjectiveAliases ?? []), previousObjective])];
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
scope,
|
|
128
|
-
);
|
|
143
|
+
heldLocks
|
|
144
|
+
.getStore()
|
|
145
|
+
?.get(ulwLoopStateLockPath(repoRoot, scope))
|
|
146
|
+
?.migrations.set(parsed.goalsPath, [
|
|
147
|
+
{
|
|
148
|
+
at: iso(),
|
|
149
|
+
kind: "aggregate_objective_migrated",
|
|
150
|
+
before: { codexObjective: previousObjective },
|
|
151
|
+
after: { codexObjective: parsed.codexObjective },
|
|
152
|
+
},
|
|
153
|
+
]);
|
|
129
154
|
}
|
|
130
155
|
return parsed;
|
|
131
156
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
157
|
+
export async function readUlwLoopPlan(repoRoot: string, scope?: UlwLoopScope): Promise<UlwLoopPlan> {
|
|
158
|
+
if (heldLocks.getStore()?.has(ulwLoopStateLockPath(repoRoot, scope))) {
|
|
159
|
+
assertStateLockOwned(ulwLoopStateLockPath(repoRoot, scope));
|
|
160
|
+
await materialize(ulwLoopDir(repoRoot, scope));
|
|
161
|
+
}
|
|
162
|
+
return readUlwLoopPlanSync(repoRoot, scope);
|
|
163
|
+
}
|
|
135
164
|
export function listUlwLoopSessionIds(repoRoot: string): readonly string[] {
|
|
136
165
|
try {
|
|
137
166
|
return readdirSync(ulwLoopDir(repoRoot), { withFileTypes: true })
|
|
@@ -142,84 +171,41 @@ export function listUlwLoopSessionIds(repoRoot: string): readonly string[] {
|
|
|
142
171
|
return [];
|
|
143
172
|
}
|
|
144
173
|
}
|
|
145
|
-
|
|
146
174
|
export async function writePlan(repoRoot: string, plan: UlwLoopPlan, scope?: UlwLoopScope): Promise<void> {
|
|
147
|
-
await
|
|
148
|
-
const path = ulwLoopGoalsPath(repoRoot, scope);
|
|
149
|
-
const tmpPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
150
|
-
await writeFile(tmpPath, `${JSON.stringify(plan, null, 2)}\n`, "utf8");
|
|
151
|
-
await rename(tmpPath, path);
|
|
175
|
+
await commit(repoRoot, scope, { plan, entries: [] });
|
|
152
176
|
}
|
|
153
|
-
|
|
154
177
|
export async function appendLedger(repoRoot: string, entry: UlwLoopLedgerEntry, scope?: UlwLoopScope): Promise<void> {
|
|
155
178
|
await appendLedgerEntries(repoRoot, [entry], scope);
|
|
156
179
|
}
|
|
157
|
-
|
|
158
180
|
export async function appendLedgerEntries(
|
|
159
181
|
repoRoot: string,
|
|
160
182
|
entries: readonly UlwLoopLedgerEntry[],
|
|
161
183
|
scope?: UlwLoopScope,
|
|
162
184
|
): Promise<void> {
|
|
163
|
-
if (entries.length
|
|
164
|
-
await mkdir(ulwLoopDir(repoRoot, scope), { recursive: true });
|
|
165
|
-
await appendFile(
|
|
166
|
-
ulwLoopLedgerPath(repoRoot, scope),
|
|
167
|
-
`${entries.map((entry) => JSON.stringify(entry)).join("\n")}\n`,
|
|
168
|
-
"utf8",
|
|
169
|
-
);
|
|
185
|
+
if (entries.length > 0) await commit(repoRoot, scope, { plan: await readUlwLoopPlan(repoRoot, scope), entries });
|
|
170
186
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const stream = createReadStream(ulwLoopLedgerPath(repoRoot, scope), { encoding: "utf8" });
|
|
179
|
-
const lines = createInterface({ input: stream, crlfDelay: Number.POSITIVE_INFINITY });
|
|
180
|
-
try {
|
|
181
|
-
for await (const line of lines) {
|
|
182
|
-
if (line.trim().length > 0) yield line;
|
|
183
|
-
}
|
|
184
|
-
} catch (error) {
|
|
185
|
-
if (!hasCode(error, "ENOENT")) throw error;
|
|
186
|
-
} finally {
|
|
187
|
-
lines.close();
|
|
188
|
-
stream.destroy();
|
|
189
|
-
}
|
|
187
|
+
function isSteeringKind(value: unknown): boolean {
|
|
188
|
+
return (
|
|
189
|
+
value === "steering_accepted" ||
|
|
190
|
+
value === "steering_rejected" ||
|
|
191
|
+
value === "criteria_revised" ||
|
|
192
|
+
value === "batch_updated"
|
|
193
|
+
);
|
|
190
194
|
}
|
|
191
|
-
|
|
192
195
|
export async function readSteeringLedgerEntries(repoRoot: string, scope?: UlwLoopScope): Promise<UlwLoopLedgerEntry[]> {
|
|
193
|
-
|
|
194
|
-
for await (const line of ledgerLines(repoRoot, scope)) {
|
|
195
|
-
const entry: UlwLoopLedgerEntry = JSON.parse(line);
|
|
196
|
-
if (isSteeringKind(entry.kind)) entries.push(entry);
|
|
197
|
-
}
|
|
198
|
-
return entries;
|
|
196
|
+
return readLedger(repoRoot, scope).filter((entry) => isSteeringKind(entry.kind));
|
|
199
197
|
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* First accepted steering entry matching an idempotency key/prompt signature.
|
|
203
|
-
* A cheap substring probe on the raw line skips JSON.parse for the vast
|
|
204
|
-
* majority of entries, so dedup stays flat even on legacy multi-MB ledgers.
|
|
205
|
-
*/
|
|
206
198
|
export async function findAcceptedSteeringLedgerEntry(
|
|
207
199
|
repoRoot: string,
|
|
208
200
|
key: string,
|
|
209
201
|
scope?: UlwLoopScope,
|
|
210
202
|
): Promise<UlwLoopLedgerEntry | undefined> {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
entry.steering.idempotencyKey === key ||
|
|
220
|
-
entry.steering.promptSignature === key
|
|
221
|
-
)
|
|
222
|
-
return entry;
|
|
223
|
-
}
|
|
224
|
-
return undefined;
|
|
203
|
+
return readLedger(repoRoot, scope).find(
|
|
204
|
+
(entry) =>
|
|
205
|
+
isSteeringKind(entry.kind) &&
|
|
206
|
+
entry.steering?.invariant.accepted === true &&
|
|
207
|
+
(entry.idempotencyKey === key ||
|
|
208
|
+
entry.steering.idempotencyKey === key ||
|
|
209
|
+
entry.steering.promptSignature === key),
|
|
210
|
+
);
|
|
225
211
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { type UlwLoopScope, ulwLoopDir } from "./paths.js";
|
|
4
|
+
import type { UlwLoopLedgerEntry, UlwLoopPlan } from "./types.js";
|
|
5
|
+
|
|
6
|
+
export interface PlanCommitRecord {
|
|
7
|
+
readonly version: 1;
|
|
8
|
+
readonly revision: number;
|
|
9
|
+
readonly plan: UlwLoopPlan;
|
|
10
|
+
readonly ledger: readonly UlwLoopLedgerEntry[];
|
|
11
|
+
}
|
|
12
|
+
export function hasCode(error: unknown, code: string): boolean {
|
|
13
|
+
return error instanceof Error && "code" in error && error.code === code;
|
|
14
|
+
}
|
|
15
|
+
export function readOptional(path: string): string | undefined {
|
|
16
|
+
try {
|
|
17
|
+
return readFileSync(path, "utf8");
|
|
18
|
+
} catch (error) {
|
|
19
|
+
if (hasCode(error, "ENOENT")) return undefined;
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function logNames(dir: string): string[] {
|
|
24
|
+
try {
|
|
25
|
+
return readdirSync(join(dir, "revisions"))
|
|
26
|
+
.filter((name) => /^\d{8,}\.json$/.test(name))
|
|
27
|
+
.sort();
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if (hasCode(error, "ENOENT")) return [];
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function readRecord(dir: string, name: string): PlanCommitRecord | undefined {
|
|
34
|
+
try {
|
|
35
|
+
const record: PlanCommitRecord = JSON.parse(readFileSync(join(dir, "revisions", name), "utf8"));
|
|
36
|
+
return record.version === 1 &&
|
|
37
|
+
Number.isInteger(record.revision) &&
|
|
38
|
+
record.plan?.version === 1 &&
|
|
39
|
+
Array.isArray(record.plan.goals) &&
|
|
40
|
+
Array.isArray(record.ledger)
|
|
41
|
+
? record
|
|
42
|
+
: undefined;
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (!(error instanceof SyntaxError)) throw error;
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Only the audit trail needs every record; plan reads use readNewestRecord.
|
|
49
|
+
export function readRecords(dir: string): PlanCommitRecord[] {
|
|
50
|
+
const records: PlanCommitRecord[] = [];
|
|
51
|
+
for (const name of logNames(dir)) {
|
|
52
|
+
const record = readRecord(dir, name);
|
|
53
|
+
if (record !== undefined) records.push(record);
|
|
54
|
+
}
|
|
55
|
+
return records.sort((a, b) => a.revision - b.revision);
|
|
56
|
+
}
|
|
57
|
+
// Every ulw-loop status probe reads the plan, each record embeds a full plan copy, and records are never
|
|
58
|
+
// deleted - so a plan read parses the newest record only, newest first, and stops there. Names are ordered
|
|
59
|
+
// by their parsed revision because a wider zero-padding would break lexicographic order.
|
|
60
|
+
export function readNewestRecord(dir: string): PlanCommitRecord | undefined {
|
|
61
|
+
const names = logNames(dir).sort((a, b) => Number.parseInt(b, 10) - Number.parseInt(a, 10));
|
|
62
|
+
for (const name of names) {
|
|
63
|
+
const record = readRecord(dir, name);
|
|
64
|
+
if (record !== undefined) return record;
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
export function reconcilePlan(dir: string): UlwLoopPlan | undefined {
|
|
69
|
+
const raw = readOptional(join(dir, "goals.json"));
|
|
70
|
+
let cached: UlwLoopPlan | undefined;
|
|
71
|
+
if (raw !== undefined) {
|
|
72
|
+
try {
|
|
73
|
+
cached = JSON.parse(raw);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (!(error instanceof SyntaxError)) throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const latest = readNewestRecord(dir);
|
|
79
|
+
return latest !== undefined && latest.revision >= (cached?.revision ?? 0) ? latest.plan : cached;
|
|
80
|
+
}
|
|
81
|
+
export function planExists(repoRoot: string, scope?: UlwLoopScope): boolean {
|
|
82
|
+
const dir = ulwLoopDir(repoRoot, scope);
|
|
83
|
+
return existsSync(join(dir, "goals.json")) || logNames(dir).length > 0;
|
|
84
|
+
}
|
|
@@ -1,5 +1,12 @@
|
|
|
1
|
+
import { UlwLoopError } from "./runtime.js";
|
|
2
|
+
import type { UlwLoopToolkitSurface } from "./surface.js";
|
|
3
|
+
|
|
1
4
|
export const ULW_LOOP_CREATE_GOALS_COMMAND = 'omo-agent-toolkit ulw-loop create-goals --brief "<brief>" --json';
|
|
2
5
|
|
|
6
|
+
export function createGoalsAction(surface: UlwLoopToolkitSurface): string {
|
|
7
|
+
return surface === "omo-senpi" ? 'agentToolkit.createGoals({ brief: "<brief>" })' : ULW_LOOP_CREATE_GOALS_COMMAND;
|
|
8
|
+
}
|
|
9
|
+
|
|
3
10
|
export interface PlanMissingRecovery {
|
|
4
11
|
readonly message: string;
|
|
5
12
|
readonly details?: { readonly existingSessionIds: readonly string[] };
|
|
@@ -9,15 +16,31 @@ export interface PlanMissingRecovery {
|
|
|
9
16
|
* A missing plan is either "never bootstrapped" or "bootstrapped under a different
|
|
10
17
|
* session id"; the recovery text has to answer both without the caller guessing.
|
|
11
18
|
*/
|
|
12
|
-
export function planMissingRecovery(
|
|
13
|
-
|
|
19
|
+
export function planMissingRecovery(
|
|
20
|
+
existingSessionIds: readonly string[],
|
|
21
|
+
surface: UlwLoopToolkitSurface = "lazycodex",
|
|
22
|
+
): PlanMissingRecovery {
|
|
23
|
+
const lines = [`Recovery: bootstrap the plan with \`${createGoalsAction(surface)}\`.`];
|
|
14
24
|
if (existingSessionIds.length === 0) return { message: lines.join("\n") };
|
|
15
25
|
lines.push(
|
|
16
|
-
|
|
26
|
+
surface === "omo-senpi"
|
|
27
|
+
? `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.`
|
|
28
|
+
: `Existing ulw-loop session ids under .omo/ulw-loop/: ${existingSessionIds.join(", ")}. Re-run with \`--session-id <id>\` to target one of them.`,
|
|
17
29
|
);
|
|
18
30
|
return { message: lines.join("\n"), details: { existingSessionIds } };
|
|
19
31
|
}
|
|
20
32
|
|
|
33
|
+
export function planMissingError(
|
|
34
|
+
planPath: string,
|
|
35
|
+
existingSessionIds: readonly string[],
|
|
36
|
+
surface: UlwLoopToolkitSurface = "lazycodex",
|
|
37
|
+
): UlwLoopError {
|
|
38
|
+
const recovery = planMissingRecovery(existingSessionIds, surface);
|
|
39
|
+
return new UlwLoopError(`No ulw-loop plan found at ${planPath}.\n${recovery.message}`, "ULW_LOOP_PLAN_MISSING", {
|
|
40
|
+
...(recovery.details === undefined ? {} : { details: recovery.details }),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
21
44
|
export function sessionScopeRequiredMessage(flag: string, existingSessionIds: readonly string[]): string {
|
|
22
45
|
const lines = [
|
|
23
46
|
"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.",
|
|
@@ -8,8 +8,9 @@ import {
|
|
|
8
8
|
} from "./codex-goal-snapshot.js";
|
|
9
9
|
import { compatibleCodexObjectives, expectedCodexObjective, isFinalRunCompletionCandidate } from "./goal-status.js";
|
|
10
10
|
import type { UlwLoopScope } from "./paths.js";
|
|
11
|
+
import { commit } from "./plan-commit.js";
|
|
11
12
|
import { seedDefaultSuccessCriteria } from "./plan-crud.js";
|
|
12
|
-
import {
|
|
13
|
+
import { readUlwLoopPlan, withUlwLoopMutationLock } from "./plan-io.js";
|
|
13
14
|
import type { UlwLoopItem, UlwLoopLedgerEntry, UlwLoopPlan } from "./types.js";
|
|
14
15
|
import { iso, UlwLoopError } from "./types.js";
|
|
15
16
|
|
|
@@ -88,8 +89,7 @@ export async function recordFinalReviewBlockers(
|
|
|
88
89
|
const summaryEntry: UlwLoopLedgerEntry = { at: now, kind: "goal_review_blocked", goalId: goal.id, status: goal.status, evidence: args.evidence, codexGoal, message: `Review blockers recorded; appended ${newGoal.id}.` };
|
|
89
90
|
Reflect.set(summaryEntry, "kind", "blocker_recorded");
|
|
90
91
|
const ledgerEntries = [blockedEntry, addedEntry, summaryEntry];
|
|
91
|
-
await
|
|
92
|
-
for (const entry of ledgerEntries) await appendLedger(repoRoot, entry, scope);
|
|
92
|
+
await commit(repoRoot, scope, { plan, entries: ledgerEntries });
|
|
93
93
|
return {
|
|
94
94
|
plan,
|
|
95
95
|
blockedGoal: goal,
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { checkpointUlwLoop } from "../checkpoint.js";
|
|
2
2
|
import { checkpointTemplate } from "../checkpoint-template.js";
|
|
3
3
|
import { recordEvidence } from "../evidence.js";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
normalizeUlwLoopSessionId,
|
|
6
|
+
type UlwLoopScope,
|
|
7
|
+
ulwLoopAttemptEvidenceDir,
|
|
8
|
+
ulwLoopGoalsRelativePath,
|
|
9
|
+
} from "../paths.js";
|
|
5
10
|
import { addUlwLoopGoal, createUlwLoopPlan, startNextUlwLoop, summarizeUlwLoopPlan } from "../plan-crud.js";
|
|
6
|
-
import { readUlwLoopPlan } from "../plan-io.js";
|
|
11
|
+
import { listUlwLoopSessionIds, readUlwLoopPlan } from "../plan-io.js";
|
|
12
|
+
import { planMissingError } from "../plan-missing-recovery.js";
|
|
7
13
|
import { recordFinalReviewBlockers } from "../review-blockers.js";
|
|
8
14
|
import { UlwLoopError } from "../runtime.js";
|
|
9
15
|
import { statusNextActions } from "../status-next-actions.js";
|
|
@@ -41,8 +47,15 @@ function validateCodexGoalJson(raw: string | undefined): void {
|
|
|
41
47
|
|
|
42
48
|
function validateContext(context: ToolkitContext): void {
|
|
43
49
|
if (!context.cwd.trim()) throw new UlwLoopError("cwd is required.", "ULW_LOOP_CWD_REQUIRED");
|
|
44
|
-
|
|
50
|
+
const sessionId = context.sessionId.trim();
|
|
51
|
+
if (!sessionId)
|
|
45
52
|
throw new UlwLoopError("ULW_LOOP_SESSION_ID_REQUIRED: sessionId is required.", "ULW_LOOP_SESSION_ID_REQUIRED");
|
|
53
|
+
const normalizedSessionId = normalizeUlwLoopSessionId(sessionId);
|
|
54
|
+
if (normalizedSessionId === null || /(?:^|[\\/])\.\.(?:[\\/]|$)/.test(sessionId))
|
|
55
|
+
throw new UlwLoopError(
|
|
56
|
+
"ULW_LOOP_SESSION_ID_INVALID: sessionId normalizes to null.",
|
|
57
|
+
"ULW_LOOP_SESSION_ID_INVALID",
|
|
58
|
+
);
|
|
46
59
|
if (context.surface !== "omo-senpi" && context.surface !== "lazycodex")
|
|
47
60
|
throw new UlwLoopError("surface must be omo-senpi or lazycodex.", "ULW_LOOP_SURFACE_INVALID");
|
|
48
61
|
}
|
|
@@ -106,6 +119,18 @@ export function createAgentToolkit(context: ToolkitContext, deps: AgentToolkitDe
|
|
|
106
119
|
const nextActions = typeof result === "object" && result !== null ? nextActionsFrom(result) : [];
|
|
107
120
|
return await notify(operation, { ok: true, operation, result, nextActions });
|
|
108
121
|
} catch (error) {
|
|
122
|
+
if (error instanceof UlwLoopError && error.code === "ULW_LOOP_PLAN_MISSING" && context.surface === "omo-senpi")
|
|
123
|
+
return notify(
|
|
124
|
+
operation,
|
|
125
|
+
failure(
|
|
126
|
+
operation,
|
|
127
|
+
planMissingError(
|
|
128
|
+
ulwLoopGoalsRelativePath(scope),
|
|
129
|
+
listUlwLoopSessionIds(context.cwd),
|
|
130
|
+
context.surface,
|
|
131
|
+
),
|
|
132
|
+
),
|
|
133
|
+
);
|
|
109
134
|
const response = caught(operation, error instanceof Error ? error : new Error("ULW_LOOP_ERROR"));
|
|
110
135
|
return notify(operation, response);
|
|
111
136
|
}
|
|
@@ -143,7 +168,7 @@ export function createAgentToolkit(context: ToolkitContext, deps: AgentToolkitDe
|
|
|
143
168
|
}
|
|
144
169
|
},
|
|
145
170
|
help: () => invoke("help", async () => ULW_LOOP_MANIFEST),
|
|
146
|
-
createGoals: (args) => invoke("create-goals", () => createUlwLoopPlan(context.cwd, args, scope)),
|
|
171
|
+
createGoals: (args) => invoke("create-goals", () => createUlwLoopPlan(context.cwd, args, scope, context.surface)),
|
|
147
172
|
status: () =>
|
|
148
173
|
invoke("status", async () => {
|
|
149
174
|
const plan = await readUlwLoopPlan(context.cwd, scope);
|
|
@@ -151,7 +176,7 @@ export function createAgentToolkit(context: ToolkitContext, deps: AgentToolkitDe
|
|
|
151
176
|
return {
|
|
152
177
|
plan,
|
|
153
178
|
summary: summarizeUlwLoopPlan(plan),
|
|
154
|
-
nextActions: statusNextActions(plan),
|
|
179
|
+
nextActions: statusNextActions(plan, context.surface),
|
|
155
180
|
// Attempt directories are an evidence-layout v2 concept; a v1 plan must not advertise one.
|
|
156
181
|
...(active === undefined || plan.evidenceLayoutVersion !== 2
|
|
157
182
|
? {}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { existsSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
|
|
5
|
+
export function readAdmissionBreaker(sessionId: string): string | null {
|
|
6
|
+
const dataDir = process.env["PLUGIN_DATA"];
|
|
7
|
+
if (typeof dataDir !== "string") return null;
|
|
8
|
+
try {
|
|
9
|
+
const value: unknown = JSON.parse(readFileSync(join(dataDir, "spawn-breaker", `${sessionId}.json`), "utf8"));
|
|
10
|
+
return typeof value === "object" && value !== null && "reason" in value && typeof value.reason === "string"
|
|
11
|
+
? value.reason
|
|
12
|
+
: "capacity limit";
|
|
13
|
+
} catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// These hook budget files are not plan/audit state and remain outside the log.
|
|
19
|
+
export function atomicWriteJson(targetPath: string, data: unknown): void {
|
|
20
|
+
const tmp = join(dirname(targetPath), `.tmp-${randomBytes(6).toString("hex")}`);
|
|
21
|
+
writeFileSync(tmp, JSON.stringify(data));
|
|
22
|
+
renameSync(tmp, targetPath);
|
|
23
|
+
}
|
|
24
|
+
export function isNonEmptyFile(path: string): boolean {
|
|
25
|
+
try {
|
|
26
|
+
return existsSync(path) && statSync(path).size > 0;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (error instanceof Error) return false;
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function readCount(counterPath: string): number {
|
|
33
|
+
try {
|
|
34
|
+
const parsed: unknown = JSON.parse(readFileSync(counterPath, "utf8"));
|
|
35
|
+
return typeof parsed === "object" &&
|
|
36
|
+
parsed !== null &&
|
|
37
|
+
"count" in parsed &&
|
|
38
|
+
typeof parsed.count === "number" &&
|
|
39
|
+
parsed.count >= 0
|
|
40
|
+
? parsed.count
|
|
41
|
+
: 0;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (error instanceof Error) return 0;
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export function readCounts(counterPath: string): Record<string, number> {
|
|
48
|
+
try {
|
|
49
|
+
const parsed: unknown = JSON.parse(readFileSync(counterPath, "utf8"));
|
|
50
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
|
|
51
|
+
const counts: Record<string, number> = {};
|
|
52
|
+
for (const [key, value] of Object.entries(parsed))
|
|
53
|
+
if (typeof value === "number" && value >= 0) counts[key] = value;
|
|
54
|
+
return counts;
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (error instanceof Error) return {};
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
}
|