lazycodex-ai 5.0.0-beta.62 → 5.0.0-beta.64
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 +70 -41
- package/dist/cli-node/index.js +70 -41
- 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 +4 -2
- 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-codex-validation.d.ts +7 -5
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-codex-validation.js +7 -2
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.js +7 -4
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +931 -442
- package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-snapshot.d.ts +6 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-snapshot.js +20 -12
- package/packages/omo-codex/plugin/components/ulw-loop/dist/domain-types.d.ts +8 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/driver-objective-ack.d.ts +4 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/driver-objective-ack.js +18 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/evidence-artifacts.d.ts +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/evidence-artifacts.js +37 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/evidence.d.ts +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/evidence.js +24 -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/paths.d.ts +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/paths.js +6 -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 +6 -2
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-crud.js +54 -25
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-goal-factory.d.ts +10 -3
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-goal-factory.js +38 -31
- 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 +11 -8
- package/packages/omo-codex/plugin/components/ulw-loop/dist/sdk/factory.js +28 -12
- package/packages/omo-codex/plugin/components/ulw-loop/dist/sdk/manifest.d.ts +335 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/sdk/manifest.js +109 -10
- package/packages/omo-codex/plugin/components/ulw-loop/dist/sdk/types.d.ts +3 -0
- 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-mutations.d.ts +3 -2
- package/packages/omo-codex/plugin/components/ulw-loop/dist/steering-mutations.js +4 -4
- package/packages/omo-codex/plugin/components/ulw-loop/dist/steering-types.d.ts +9 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/steering.d.ts +3 -2
- package/packages/omo-codex/plugin/components/ulw-loop/dist/steering.js +11 -11
- package/packages/omo-codex/plugin/components/ulw-loop/dist/stop-resume-hook.js +8 -17
- package/packages/omo-codex/plugin/components/ulw-loop/dist/success-criteria-input.d.ts +9 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/success-criteria-input.js +41 -0
- 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-codex-validation.ts +15 -3
- package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +7 -4
- package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-snapshot.ts +26 -14
- package/packages/omo-codex/plugin/components/ulw-loop/src/domain-types.ts +8 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/driver-objective-ack.ts +20 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/evidence-artifacts.ts +45 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/evidence.ts +23 -16
- package/packages/omo-codex/plugin/components/ulw-loop/src/ledger.ts +33 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/paths.ts +7 -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 +67 -54
- package/packages/omo-codex/plugin/components/ulw-loop/src/plan-goal-factory.ts +63 -31
- 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 +11 -7
- package/packages/omo-codex/plugin/components/ulw-loop/src/sdk/factory.ts +47 -12
- package/packages/omo-codex/plugin/components/ulw-loop/src/sdk/manifest.ts +124 -10
- package/packages/omo-codex/plugin/components/ulw-loop/src/sdk/types.ts +3 -0
- 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-mutations.ts +5 -4
- package/packages/omo-codex/plugin/components/ulw-loop/src/steering-types.ts +13 -1
- package/packages/omo-codex/plugin/components/ulw-loop/src/steering.ts +12 -10
- package/packages/omo-codex/plugin/components/ulw-loop/src/stop-resume-hook.ts +8 -16
- package/packages/omo-codex/plugin/components/ulw-loop/src/success-criteria-input.ts +56 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/codex-goal-snapshot.test.ts +37 -1
- 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-driver-states.test.ts +33 -4
- package/packages/omo-codex/plugin/components/ulw-loop/test/sdk-dx.test.ts +219 -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 +6 -4
- package/packages/prompts-core/prompts/ultrawork/codex.md +5 -4
- package/packages/shared-skills/skills/ast-grep/SKILL.md +12 -0
- package/packages/shared-skills/skills/debugging/references/tools/playwright-cli.md +79 -161
- package/packages/shared-skills/skills/frontend/SKILL.md +1 -1
- package/packages/shared-skills/skills/frontend/references/design/aside.md +1 -1
- package/packages/shared-skills/skills/frontend/references/design/clone-from-url.md +1 -1
- package/packages/shared-skills/skills/frontend/scripts/perfection/lighthouse-audit.py +2 -2
- package/packages/shared-skills/skills/review-work/SKILL.md +1 -1
- package/packages/shared-skills/skills/ultimate-browsing/ATTRIBUTION.md +1 -82
- package/packages/shared-skills/skills/ultimate-browsing/SKILL.md +17 -29
- package/packages/shared-skills/skills/ultimate-browsing/engine/AGENTS.md +2 -1
- package/packages/shared-skills/skills/ultimate-browsing/engine/templates/package.json +2 -2
- package/packages/shared-skills/skills/ultimate-browsing/engine/templates/playwright_mobile_chrome.js +5 -2
- package/packages/shared-skills/skills/ultimate-browsing/engine/templates/playwright_real_chrome.js +8 -6
- package/packages/shared-skills/skills/ultimate-browsing/engine/tests/test_playwright_stealth.py +86 -0
- package/packages/shared-skills/skills/ultimate-browsing/engine/tests/test_playwright_templates.py +4 -7
- package/packages/shared-skills/skills/ultimate-browsing/references/chrome-stealth.md +86 -100
- package/packages/shared-skills/skills/ultimate-browsing/references/insane-search/README.md +9 -3
- package/packages/shared-skills/skills/ultimate-browsing/references/insane-search/playwright.md +16 -13
- package/packages/shared-skills/skills/ultimate-browsing/scripts/extract_cookies.py +3 -3
- package/packages/shared-skills/skills/ulw-execute/SKILL.md +1 -1
- package/packages/shared-skills/skills/ulw-plan/scripts/scaffold-plan.mjs +1 -1
- package/packages/shared-skills/skills/ulw-research/SKILL.md +3 -3
- package/packages/shared-skills/skills/visual-qa/AGENTS.md +1 -1
- package/packages/shared-skills/skills/visual-qa/SKILL.md +1 -1
- package/packages/shared-skills/skills/visual-qa/references/browser-setup.md +75 -0
- package/packages/omo-codex/plugin/skills/visual-qa/references/agent-browser-setup.md +0 -45
- package/packages/shared-skills/skills/visual-qa/references/agent-browser-setup.md +0 -45
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// components/ulw-loop/src/checkpoint.ts
|
|
4
|
-
import { existsSync as
|
|
5
|
-
import { readFile as
|
|
6
|
-
import { resolve as
|
|
4
|
+
import { existsSync as existsSync5, statSync as statSync2 } from "node:fs";
|
|
5
|
+
import { readFile as readFile3 } from "node:fs/promises";
|
|
6
|
+
import { resolve as resolve4 } from "node:path";
|
|
7
7
|
|
|
8
8
|
// components/ulw-loop/src/codex-goal-snapshot.ts
|
|
9
9
|
import { existsSync } from "node:fs";
|
|
@@ -80,33 +80,58 @@ function reconcileCodexGoalSnapshot(snapshot, options) {
|
|
|
80
80
|
const effectiveSnapshot = snapshot ?? { available: false, raw: null };
|
|
81
81
|
const errors = [];
|
|
82
82
|
const warnings = [];
|
|
83
|
+
const nextActions = [];
|
|
83
84
|
const expected = options.expectedObjective;
|
|
84
|
-
const normalizedExpected = normalizeObjective(expected);
|
|
85
85
|
if (!effectiveSnapshot.available) {
|
|
86
|
-
|
|
87
|
-
return { ok: errors.length === 0, snapshot: effectiveSnapshot, warnings, errors };
|
|
86
|
+
nextActions.push(`call get_goal; if none, create_goal with codexObjective "${expected}" verbatim`);
|
|
87
|
+
return { ok: errors.length === 0, snapshot: effectiveSnapshot, warnings, nextActions, errors };
|
|
88
88
|
}
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
].filter(Boolean));
|
|
89
|
+
const normalized = (objectives) => new Set(objectives.map(normalizeObjective).filter(Boolean));
|
|
90
|
+
const accepted = normalized([expected, ...options.acceptedObjectives ?? []]);
|
|
91
|
+
const acknowledged = normalized(options.acknowledgedObjectives ?? []);
|
|
93
92
|
const actual = normalizeObjective(effectiveSnapshot.objective ?? "");
|
|
94
|
-
|
|
93
|
+
let unacknowledgedObjective;
|
|
94
|
+
if (actual && !accepted.has(actual) && !acknowledged.has(actual)) {
|
|
95
95
|
warnings.push(`driver_objective_differs: expected "${expected}", got "${actual}".`);
|
|
96
|
+
unacknowledgedObjective = actual;
|
|
96
97
|
}
|
|
97
98
|
const actualStatus = effectiveSnapshot.status ?? "unknown";
|
|
98
99
|
if (actualStatus === "paused" || actualStatus === "usage_limited" || actualStatus === "budget_limited") {
|
|
99
|
-
|
|
100
|
+
nextActions.push("/goal resume or raise the budget");
|
|
100
101
|
}
|
|
101
102
|
if (actualStatus === "complete")
|
|
102
|
-
|
|
103
|
-
return {
|
|
103
|
+
nextActions.push(`driver closed early: call create_goal with codexObjective "${expected}" verbatim`);
|
|
104
|
+
return {
|
|
105
|
+
ok: errors.length === 0,
|
|
106
|
+
snapshot: effectiveSnapshot,
|
|
107
|
+
warnings,
|
|
108
|
+
nextActions,
|
|
109
|
+
errors,
|
|
110
|
+
...unacknowledgedObjective === undefined ? {} : { unacknowledgedObjective }
|
|
111
|
+
};
|
|
104
112
|
}
|
|
105
113
|
function formatCodexGoalReconciliation(reconciliation) {
|
|
106
|
-
const parts = [...reconciliation.errors, ...reconciliation.warnings];
|
|
114
|
+
const parts = [...reconciliation.errors, ...reconciliation.nextActions, ...reconciliation.warnings];
|
|
107
115
|
return parts.join(" ");
|
|
108
116
|
}
|
|
109
117
|
|
|
118
|
+
// components/ulw-loop/src/driver-objective-ack.ts
|
|
119
|
+
function normalizeDriverObjective(value) {
|
|
120
|
+
return value.replace(/\s+/g, " ").trim();
|
|
121
|
+
}
|
|
122
|
+
function acknowledgedDriverObjectives(plan) {
|
|
123
|
+
return plan.acknowledgedDriverObjectives ?? [];
|
|
124
|
+
}
|
|
125
|
+
function acknowledgeDriverObjective(plan, objective) {
|
|
126
|
+
if (objective === undefined)
|
|
127
|
+
return false;
|
|
128
|
+
const normalized = normalizeDriverObjective(objective);
|
|
129
|
+
if (!normalized || acknowledgedDriverObjectives(plan).includes(normalized))
|
|
130
|
+
return false;
|
|
131
|
+
plan.acknowledgedDriverObjectives = [...acknowledgedDriverObjectives(plan), normalized];
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
|
|
110
135
|
// components/ulw-loop/src/paths.ts
|
|
111
136
|
import { isAbsolute, join, relative, sep } from "node:path";
|
|
112
137
|
// components/ulw-loop/src/constants.ts
|
|
@@ -178,15 +203,9 @@ function ulwLoopGoalsRelativePath(scope) {
|
|
|
178
203
|
function ulwLoopLedgerRelativePath(scope) {
|
|
179
204
|
return `${ulwLoopRelativeDir(scope)}/${ULW_LOOP_LEDGER}`;
|
|
180
205
|
}
|
|
181
|
-
function ulwLoopBriefPath(repoRoot, scope) {
|
|
182
|
-
return join(ulwLoopDir(repoRoot, scope), ULW_LOOP_BRIEF);
|
|
183
|
-
}
|
|
184
206
|
function ulwLoopGoalsPath(repoRoot, scope) {
|
|
185
207
|
return join(ulwLoopDir(repoRoot, scope), ULW_LOOP_GOALS);
|
|
186
208
|
}
|
|
187
|
-
function ulwLoopLedgerPath(repoRoot, scope) {
|
|
188
|
-
return join(ulwLoopDir(repoRoot, scope), ULW_LOOP_LEDGER);
|
|
189
|
-
}
|
|
190
209
|
function ulwLoopStateLockPath(repoRoot, scope) {
|
|
191
210
|
return join(ulwLoopDir(repoRoot, scope), ULW_LOOP_STATE_LOCK);
|
|
192
211
|
}
|
|
@@ -199,6 +218,10 @@ function repoRelative(absolutePath, repoRoot) {
|
|
|
199
218
|
return absolutePath.slice(backslashPrefix.length).split("\\").join("/");
|
|
200
219
|
return absolutePath.split("\\").join("/");
|
|
201
220
|
}
|
|
221
|
+
function ulwLoopEvidenceRoot(scope) {
|
|
222
|
+
const sessionId = normalizeUlwLoopSessionId(scope?.sessionId);
|
|
223
|
+
return sessionId === null ? ".omo/evidence" : `.omo/evidence/ulw/${sessionId}`;
|
|
224
|
+
}
|
|
202
225
|
function ulwLoopAttemptEvidenceDir(goalId, attempt, scope) {
|
|
203
226
|
const sessionId = normalizeUlwLoopSessionId(scope?.sessionId);
|
|
204
227
|
if (sessionId === null) {
|
|
@@ -305,14 +328,16 @@ async function validateCheckpointCodexGoal(input) {
|
|
|
305
328
|
const expected = expectedCodexObjective(input.plan, input.goal);
|
|
306
329
|
const reconciliation = reconcileCodexGoalSnapshot(snapshot, {
|
|
307
330
|
expectedObjective: expected,
|
|
331
|
+
acknowledgedObjectives: acknowledgedDriverObjectives(input.plan),
|
|
308
332
|
...codexGoalMode(input.plan) === "aggregate" ? { acceptedObjectives: compatibleCodexObjectives(input.plan) } : {}
|
|
309
333
|
});
|
|
310
334
|
if (!reconciliation.ok)
|
|
311
335
|
throw new CodexGoalSnapshotError(formatCodexGoalReconciliation(reconciliation));
|
|
312
336
|
return {
|
|
313
337
|
raw: snapshot?.raw,
|
|
314
|
-
nextActions: reconciliation.
|
|
315
|
-
warnings: reconciliation.warnings
|
|
338
|
+
nextActions: reconciliation.nextActions,
|
|
339
|
+
warnings: reconciliation.warnings,
|
|
340
|
+
...reconciliation.unacknowledgedObjective === undefined ? {} : { unacknowledgedObjective: reconciliation.unacknowledgedObjective }
|
|
316
341
|
};
|
|
317
342
|
}
|
|
318
343
|
function combineCheckpointValidationErrors(codexError, gateError) {
|
|
@@ -322,23 +347,177 @@ ${gateError.message}`, "ULW_LOOP_QUALITY_GATE_INVALID", {
|
|
|
322
347
|
});
|
|
323
348
|
}
|
|
324
349
|
|
|
350
|
+
// components/ulw-loop/src/evidence-artifacts.ts
|
|
351
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
352
|
+
import { isAbsolute as isAbsolute2, relative as relative2, resolve as resolve2, sep as sep2 } from "node:path";
|
|
353
|
+
function fail(message, code, details) {
|
|
354
|
+
throw new UlwLoopError(message, code, { details });
|
|
355
|
+
}
|
|
356
|
+
function stored(repoRoot, absolute) {
|
|
357
|
+
const rel = relative2(repoRoot, absolute);
|
|
358
|
+
const inside = rel !== "" && !rel.startsWith("..") && !isAbsolute2(rel);
|
|
359
|
+
return inside ? rel.split(sep2).join("/") : absolute;
|
|
360
|
+
}
|
|
361
|
+
function resolveEvidenceArtifacts(repoRoot, artifacts) {
|
|
362
|
+
if (artifacts === undefined)
|
|
363
|
+
return;
|
|
364
|
+
const seen = new Set;
|
|
365
|
+
const resolved = [];
|
|
366
|
+
for (const [index, candidate] of artifacts.entries()) {
|
|
367
|
+
const path = typeof candidate === "string" ? candidate.trim() : "";
|
|
368
|
+
if (!path)
|
|
369
|
+
fail(`Artifact ${index + 1} must be a non-empty path.`, "ULW_LOOP_ARGUMENT_INVALID", { index });
|
|
370
|
+
const absolute = resolve2(repoRoot, path);
|
|
371
|
+
if (!existsSync2(absolute))
|
|
372
|
+
fail(`Evidence artifact does not exist: ${path} (resolved to ${absolute}).`, "ULW_LOOP_EVIDENCE_ARTIFACT_MISSING", {
|
|
373
|
+
path,
|
|
374
|
+
resolved: absolute
|
|
375
|
+
});
|
|
376
|
+
const value = stored(repoRoot, absolute);
|
|
377
|
+
if (!seen.has(value)) {
|
|
378
|
+
seen.add(value);
|
|
379
|
+
resolved.push(value);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return resolved;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// components/ulw-loop/src/plan-commit.ts
|
|
386
|
+
import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
|
|
387
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
388
|
+
import { linkSync, mkdirSync as mkdirSync2, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
389
|
+
import { rename, writeFile } from "node:fs/promises";
|
|
390
|
+
import { join as join4 } from "node:path";
|
|
391
|
+
|
|
392
|
+
// components/ulw-loop/src/ledger.ts
|
|
393
|
+
import { join as join3 } from "node:path";
|
|
394
|
+
|
|
395
|
+
// components/ulw-loop/src/plan-log.ts
|
|
396
|
+
import { existsSync as existsSync3, readdirSync, readFileSync } from "node:fs";
|
|
397
|
+
import { join as join2 } from "node:path";
|
|
398
|
+
function hasCode(error, code) {
|
|
399
|
+
return error instanceof Error && "code" in error && error.code === code;
|
|
400
|
+
}
|
|
401
|
+
function readOptional(path) {
|
|
402
|
+
try {
|
|
403
|
+
return readFileSync(path, "utf8");
|
|
404
|
+
} catch (error) {
|
|
405
|
+
if (hasCode(error, "ENOENT"))
|
|
406
|
+
return;
|
|
407
|
+
throw error;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
function logNames(dir) {
|
|
411
|
+
try {
|
|
412
|
+
return readdirSync(join2(dir, "revisions")).filter((name) => /^\d{8,}\.json$/.test(name)).sort();
|
|
413
|
+
} catch (error) {
|
|
414
|
+
if (hasCode(error, "ENOENT"))
|
|
415
|
+
return [];
|
|
416
|
+
throw error;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
function readRecord(dir, name) {
|
|
420
|
+
try {
|
|
421
|
+
const record = JSON.parse(readFileSync(join2(dir, "revisions", name), "utf8"));
|
|
422
|
+
return record.version === 1 && Number.isInteger(record.revision) && record.plan?.version === 1 && Array.isArray(record.plan.goals) && Array.isArray(record.ledger) ? record : undefined;
|
|
423
|
+
} catch (error) {
|
|
424
|
+
if (!(error instanceof SyntaxError))
|
|
425
|
+
throw error;
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
function readRecords(dir) {
|
|
430
|
+
const records = [];
|
|
431
|
+
for (const name of logNames(dir)) {
|
|
432
|
+
const record = readRecord(dir, name);
|
|
433
|
+
if (record !== undefined)
|
|
434
|
+
records.push(record);
|
|
435
|
+
}
|
|
436
|
+
return records.sort((a, b) => a.revision - b.revision);
|
|
437
|
+
}
|
|
438
|
+
function readNewestRecord(dir) {
|
|
439
|
+
const names = logNames(dir).sort((a, b) => Number.parseInt(b, 10) - Number.parseInt(a, 10));
|
|
440
|
+
for (const name of names) {
|
|
441
|
+
const record = readRecord(dir, name);
|
|
442
|
+
if (record !== undefined)
|
|
443
|
+
return record;
|
|
444
|
+
}
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
function reconcilePlan(dir) {
|
|
448
|
+
const raw = readOptional(join2(dir, "goals.json"));
|
|
449
|
+
let cached;
|
|
450
|
+
if (raw !== undefined) {
|
|
451
|
+
try {
|
|
452
|
+
cached = JSON.parse(raw);
|
|
453
|
+
} catch (error) {
|
|
454
|
+
if (!(error instanceof SyntaxError))
|
|
455
|
+
throw error;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
const latest = readNewestRecord(dir);
|
|
459
|
+
return latest !== undefined && latest.revision >= (cached?.revision ?? 0) ? latest.plan : cached;
|
|
460
|
+
}
|
|
461
|
+
function planExists(repoRoot, scope) {
|
|
462
|
+
const dir = ulwLoopDir(repoRoot, scope);
|
|
463
|
+
return existsSync3(join2(dir, "goals.json")) || logNames(dir).length > 0;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// components/ulw-loop/src/ledger.ts
|
|
467
|
+
function readLedgerAt(dir) {
|
|
468
|
+
const entries = new Map;
|
|
469
|
+
const lines = (readOptional(join3(dir, "ledger.jsonl")) ?? "").split(/\r?\n/);
|
|
470
|
+
for (const [index, line] of lines.entries()) {
|
|
471
|
+
if (line.trim().length === 0)
|
|
472
|
+
continue;
|
|
473
|
+
try {
|
|
474
|
+
const entry = JSON.parse(line);
|
|
475
|
+
entry.revision ??= 0;
|
|
476
|
+
entry.id ??= `legacy-${index + 1}`;
|
|
477
|
+
entries.set(entry.id, entry);
|
|
478
|
+
} catch (error) {
|
|
479
|
+
if (!(error instanceof SyntaxError))
|
|
480
|
+
throw error;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
for (const record of readRecords(dir))
|
|
484
|
+
for (const [seq, entry] of record.ledger.entries()) {
|
|
485
|
+
const id = entry.id ?? `${record.revision}-${seq}`;
|
|
486
|
+
entries.set(id, { ...entry, revision: record.revision, id });
|
|
487
|
+
}
|
|
488
|
+
const reset = reconcilePlan(dir)?.ledgerResetRevision ?? 0;
|
|
489
|
+
const sequence = (entry) => Number(entry.id?.split("-").at(-1) ?? 0);
|
|
490
|
+
return [...entries.values()].filter((entry) => (entry.revision ?? 0) >= reset).sort((a, b) => (a.revision ?? 0) - (b.revision ?? 0) || sequence(a) - sequence(b));
|
|
491
|
+
}
|
|
492
|
+
function readLedger(repoRoot, scope) {
|
|
493
|
+
return readLedgerAt(ulwLoopDir(repoRoot, scope));
|
|
494
|
+
}
|
|
495
|
+
|
|
325
496
|
// components/ulw-loop/src/plan-io.ts
|
|
326
497
|
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";
|
|
498
|
+
import { readdirSync as readdirSync2 } from "node:fs";
|
|
330
499
|
|
|
331
500
|
// components/ulw-loop/src/plan-missing-recovery.ts
|
|
332
501
|
var ULW_LOOP_CREATE_GOALS_COMMAND = 'omo-agent-toolkit ulw-loop create-goals --brief "<brief>" --json';
|
|
333
|
-
function
|
|
334
|
-
|
|
502
|
+
function createGoalsAction(surface) {
|
|
503
|
+
return surface === "omo-senpi" ? 'agentToolkit.createGoals({ brief: "<brief>" })' : ULW_LOOP_CREATE_GOALS_COMMAND;
|
|
504
|
+
}
|
|
505
|
+
function planMissingRecovery(existingSessionIds, surface = "lazycodex") {
|
|
506
|
+
const lines = [`Recovery: bootstrap the plan with \`${createGoalsAction(surface)}\`.`];
|
|
335
507
|
if (existingSessionIds.length === 0)
|
|
336
508
|
return { message: lines.join(`
|
|
337
509
|
`) };
|
|
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.`);
|
|
510
|
+
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
511
|
return { message: lines.join(`
|
|
340
512
|
`), details: { existingSessionIds } };
|
|
341
513
|
}
|
|
514
|
+
function planMissingError(planPath, existingSessionIds, surface = "lazycodex") {
|
|
515
|
+
const recovery = planMissingRecovery(existingSessionIds, surface);
|
|
516
|
+
return new UlwLoopError(`No ulw-loop plan found at ${planPath}.
|
|
517
|
+
${recovery.message}`, "ULW_LOOP_PLAN_MISSING", {
|
|
518
|
+
...recovery.details === undefined ? {} : { details: recovery.details }
|
|
519
|
+
});
|
|
520
|
+
}
|
|
342
521
|
function sessionScopeRequiredMessage(flag, existingSessionIds) {
|
|
343
522
|
const lines = [
|
|
344
523
|
"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 +540,207 @@ function sessionIdRequiredMessage(flag) {
|
|
|
361
540
|
|
|
362
541
|
// components/ulw-loop/src/state-lock.ts
|
|
363
542
|
import { randomUUID } from "node:crypto";
|
|
364
|
-
import { closeSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, writeSync } from "node:fs";
|
|
543
|
+
import { closeSync, ftruncateSync, mkdirSync, openSync, readFileSync as readFileSync2, statSync, unlinkSync, writeSync } from "node:fs";
|
|
365
544
|
import { dirname } from "node:path";
|
|
366
|
-
import { setTimeout as sleep } from "node:timers/promises";
|
|
367
545
|
var ULW_LOOP_LOCK_TIMEOUT_CODE = "ULW_LOOP_LOCK_TIMEOUT";
|
|
368
546
|
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
369
547
|
var DEFAULT_STALE_MS = 60000;
|
|
370
|
-
var
|
|
371
|
-
var MAX_DELAY_MS = 100;
|
|
548
|
+
var DEFAULT_LEASE_MS = 30000;
|
|
372
549
|
var SLEEP_CELL = new Int32Array(new SharedArrayBuffer(4));
|
|
550
|
+
var systemClock = {
|
|
551
|
+
now: Date.now,
|
|
552
|
+
schedule(fn, ms) {
|
|
553
|
+
const timer = setTimeout(fn, ms);
|
|
554
|
+
return {
|
|
555
|
+
unref: () => {
|
|
556
|
+
timer.unref();
|
|
557
|
+
},
|
|
558
|
+
cancel: () => clearTimeout(timer)
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
};
|
|
373
562
|
async function withStateLock(lockPath, fn, options = {}) {
|
|
374
|
-
const
|
|
563
|
+
const holder = await acquireAsync(lockPath, options);
|
|
564
|
+
const clock = options.clock ?? systemClock;
|
|
565
|
+
let timer;
|
|
566
|
+
let heartbeatError;
|
|
567
|
+
const beat = () => {
|
|
568
|
+
try {
|
|
569
|
+
holder.record.leaseUntil = clock.now() + (options.leaseMs ?? DEFAULT_LEASE_MS);
|
|
570
|
+
writeRecord(holder);
|
|
571
|
+
schedule();
|
|
572
|
+
} catch (error) {
|
|
573
|
+
heartbeatError = error;
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
const schedule = () => {
|
|
577
|
+
if (options.heartbeatMs === 0)
|
|
578
|
+
return;
|
|
579
|
+
timer = clock.schedule(beat, options.heartbeatMs ?? (options.leaseMs ?? DEFAULT_LEASE_MS) / 3);
|
|
580
|
+
timer.unref();
|
|
581
|
+
};
|
|
582
|
+
schedule();
|
|
375
583
|
try {
|
|
376
|
-
|
|
584
|
+
const result = await fn(holder.record.token);
|
|
585
|
+
if (heartbeatError !== undefined)
|
|
586
|
+
throw heartbeatError;
|
|
587
|
+
return result;
|
|
377
588
|
} finally {
|
|
378
|
-
|
|
589
|
+
timer?.cancel();
|
|
590
|
+
closeSync(holder.fd);
|
|
591
|
+
release(lockPath, holder.record.token);
|
|
379
592
|
}
|
|
380
593
|
}
|
|
381
594
|
function withStateLockSync(lockPath, fn, options = {}) {
|
|
382
|
-
const
|
|
595
|
+
const holder = acquireSync(lockPath, options);
|
|
383
596
|
try {
|
|
384
597
|
return fn();
|
|
385
598
|
} finally {
|
|
386
|
-
|
|
599
|
+
closeSync(holder.fd);
|
|
600
|
+
release(lockPath, holder.record.token);
|
|
387
601
|
}
|
|
388
602
|
}
|
|
389
603
|
function isStateLockTimeout(error) {
|
|
390
604
|
return error instanceof UlwLoopError && error.code === ULW_LOOP_LOCK_TIMEOUT_CODE;
|
|
391
605
|
}
|
|
392
606
|
async function acquireAsync(lockPath, options) {
|
|
393
|
-
const
|
|
394
|
-
const
|
|
607
|
+
const clock = options.clock ?? systemClock;
|
|
608
|
+
const deadline = clock.now() + (options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
395
609
|
mkdirSync(dirname(lockPath), { recursive: true });
|
|
396
610
|
for (let attempt = 0;; ) {
|
|
397
|
-
const outcome = attemptOnce(lockPath,
|
|
611
|
+
const outcome = attemptOnce(lockPath, options, clock.now(), options.leaseMs ?? DEFAULT_LEASE_MS);
|
|
398
612
|
if (outcome.kind === "acquired")
|
|
399
|
-
return outcome.
|
|
613
|
+
return outcome.holder;
|
|
400
614
|
if (outcome.kind === "retry")
|
|
401
615
|
continue;
|
|
402
|
-
if (
|
|
403
|
-
throw lockTimeout(lockPath, options
|
|
404
|
-
await
|
|
616
|
+
if (clock.now() >= deadline)
|
|
617
|
+
throw lockTimeout(lockPath, options);
|
|
618
|
+
await new Promise((resolve) => clock.schedule(resolve, backoffMs(attempt)));
|
|
405
619
|
attempt += 1;
|
|
406
620
|
}
|
|
407
621
|
}
|
|
408
622
|
function acquireSync(lockPath, options) {
|
|
409
623
|
const deadline = Date.now() + (options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
410
|
-
const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
|
|
411
624
|
mkdirSync(dirname(lockPath), { recursive: true });
|
|
412
625
|
for (let attempt = 0;; ) {
|
|
413
|
-
const outcome = attemptOnce(lockPath,
|
|
626
|
+
const outcome = attemptOnce(lockPath, options, Date.now());
|
|
414
627
|
if (outcome.kind === "acquired")
|
|
415
|
-
return outcome.
|
|
628
|
+
return outcome.holder;
|
|
416
629
|
if (outcome.kind === "retry")
|
|
417
630
|
continue;
|
|
418
631
|
if (Date.now() >= deadline)
|
|
419
|
-
throw lockTimeout(lockPath, options
|
|
632
|
+
throw lockTimeout(lockPath, options);
|
|
420
633
|
Atomics.wait(SLEEP_CELL, 0, 0, backoffMs(attempt));
|
|
421
634
|
attempt += 1;
|
|
422
635
|
}
|
|
423
636
|
}
|
|
424
|
-
function attemptOnce(lockPath,
|
|
637
|
+
function attemptOnce(lockPath, options, now, leaseMs) {
|
|
425
638
|
try {
|
|
426
|
-
const
|
|
427
|
-
if (
|
|
428
|
-
return { kind: "acquired",
|
|
429
|
-
const snapshot = readSnapshot(lockPath);
|
|
639
|
+
const holder = tryCreate(lockPath, now, leaseMs);
|
|
640
|
+
if (holder !== null)
|
|
641
|
+
return { kind: "acquired", holder };
|
|
642
|
+
const snapshot = readSnapshot(lockPath, now);
|
|
430
643
|
if (snapshot === null)
|
|
431
644
|
return { kind: "retry" };
|
|
432
|
-
|
|
645
|
+
const record = snapshot.record;
|
|
646
|
+
const stale = record === null ? snapshot.ageMs > (options.staleMs ?? DEFAULT_STALE_MS) : !isProcessAlive(record.pid) || record.leaseUntil !== undefined && now > record.leaseUntil;
|
|
647
|
+
if (stale && reclaim(lockPath, snapshot.raw))
|
|
433
648
|
return { kind: "retry" };
|
|
434
649
|
return { kind: "wait" };
|
|
435
650
|
} catch (error) {
|
|
436
|
-
if (
|
|
651
|
+
if (hasCode2(error, "EINTR"))
|
|
437
652
|
return { kind: "wait" };
|
|
438
653
|
throw error;
|
|
439
654
|
}
|
|
440
655
|
}
|
|
441
|
-
function
|
|
656
|
+
function writeRecord(holder) {
|
|
657
|
+
const buf = Buffer.from(JSON.stringify(holder.record));
|
|
658
|
+
ftruncateSync(holder.fd, 0);
|
|
659
|
+
let offset = 0;
|
|
660
|
+
while (offset < buf.length) {
|
|
661
|
+
const written = writeSync(holder.fd, buf, offset, buf.length - offset, offset);
|
|
662
|
+
if (written === 0)
|
|
663
|
+
throw new Error("State lock write made no progress.");
|
|
664
|
+
offset += written;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
function tryCreate(lockPath, now, leaseMs) {
|
|
442
668
|
let fd;
|
|
443
669
|
try {
|
|
444
670
|
fd = openSync(lockPath, "wx");
|
|
445
671
|
} catch (error) {
|
|
446
|
-
if (
|
|
672
|
+
if (hasCode2(error, "EEXIST"))
|
|
447
673
|
return null;
|
|
448
674
|
throw error;
|
|
449
675
|
}
|
|
450
|
-
const record = {
|
|
676
|
+
const record = {
|
|
677
|
+
pid: process.pid,
|
|
678
|
+
createdAt: new Date(now).toISOString(),
|
|
679
|
+
token: randomUUID(),
|
|
680
|
+
...leaseMs === undefined ? {} : { leaseUntil: now + leaseMs }
|
|
681
|
+
};
|
|
682
|
+
const holder = { fd, record };
|
|
451
683
|
try {
|
|
452
|
-
|
|
684
|
+
writeRecord(holder);
|
|
453
685
|
} catch (error) {
|
|
454
686
|
closeSync(fd);
|
|
455
687
|
try {
|
|
456
688
|
unlinkSync(lockPath);
|
|
457
|
-
} catch {
|
|
689
|
+
} catch (cleanup) {
|
|
690
|
+
if (!hasCode2(cleanup, "ENOENT"))
|
|
691
|
+
throw cleanup;
|
|
692
|
+
}
|
|
458
693
|
throw error;
|
|
459
694
|
}
|
|
460
|
-
|
|
461
|
-
return record.token;
|
|
695
|
+
return holder;
|
|
462
696
|
}
|
|
463
|
-
function readSnapshot(lockPath) {
|
|
697
|
+
function readSnapshot(lockPath, now) {
|
|
464
698
|
try {
|
|
465
|
-
const raw =
|
|
466
|
-
|
|
467
|
-
return { raw, record: parseRecord(raw), ageMs };
|
|
699
|
+
const raw = readFileSync2(lockPath, "utf8");
|
|
700
|
+
return { raw, record: parseRecord(raw), ageMs: now - statSync(lockPath).mtimeMs };
|
|
468
701
|
} catch (error) {
|
|
469
|
-
if (
|
|
702
|
+
if (hasCode2(error, "ENOENT"))
|
|
470
703
|
return null;
|
|
471
704
|
throw error;
|
|
472
705
|
}
|
|
473
706
|
}
|
|
474
707
|
function parseRecord(raw) {
|
|
475
708
|
try {
|
|
476
|
-
const
|
|
477
|
-
if (typeof
|
|
709
|
+
const record = JSON.parse(raw);
|
|
710
|
+
if (typeof record !== "object" || record === null)
|
|
478
711
|
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")
|
|
712
|
+
if (!("pid" in record) || typeof record.pid !== "number" || !Number.isInteger(record.pid) || record.pid <= 0)
|
|
484
713
|
return null;
|
|
485
|
-
if (typeof
|
|
714
|
+
if (!("createdAt" in record) || typeof record.createdAt !== "string")
|
|
486
715
|
return null;
|
|
487
|
-
|
|
716
|
+
if (!("token" in record) || typeof record.token !== "string" || record.token.length === 0)
|
|
717
|
+
return null;
|
|
718
|
+
return {
|
|
719
|
+
pid: record.pid,
|
|
720
|
+
createdAt: record.createdAt,
|
|
721
|
+
token: record.token,
|
|
722
|
+
..."leaseUntil" in record && typeof record.leaseUntil === "number" ? { leaseUntil: record.leaseUntil } : {}
|
|
723
|
+
};
|
|
488
724
|
} catch (error) {
|
|
489
725
|
if (error instanceof SyntaxError)
|
|
490
726
|
return null;
|
|
491
727
|
throw error;
|
|
492
728
|
}
|
|
493
729
|
}
|
|
494
|
-
function isStale(snapshot, staleMs) {
|
|
495
|
-
if (snapshot.record === null)
|
|
496
|
-
return snapshot.ageMs > staleMs;
|
|
497
|
-
return !isProcessAlive(snapshot.record.pid);
|
|
498
|
-
}
|
|
499
730
|
function isProcessAlive(pid) {
|
|
500
731
|
try {
|
|
501
732
|
process.kill(pid, 0);
|
|
502
733
|
return true;
|
|
503
734
|
} catch (error) {
|
|
504
|
-
if (
|
|
735
|
+
if (hasCode2(error, "ESRCH"))
|
|
505
736
|
return false;
|
|
506
|
-
if (
|
|
737
|
+
if (hasCode2(error, "EPERM"))
|
|
507
738
|
return true;
|
|
508
739
|
throw error;
|
|
509
740
|
}
|
|
510
741
|
}
|
|
511
742
|
function reclaim(lockPath, expectedRaw) {
|
|
512
|
-
const current = readSnapshot(lockPath);
|
|
743
|
+
const current = readSnapshot(lockPath, Date.now());
|
|
513
744
|
if (current === null)
|
|
514
745
|
return true;
|
|
515
746
|
if (current.raw !== expectedRaw)
|
|
@@ -517,63 +748,102 @@ function reclaim(lockPath, expectedRaw) {
|
|
|
517
748
|
try {
|
|
518
749
|
unlinkSync(lockPath);
|
|
519
750
|
} catch (error) {
|
|
520
|
-
if (
|
|
751
|
+
if (hasCode2(error, "EPERM") || hasCode2(error, "EACCES"))
|
|
752
|
+
return false;
|
|
753
|
+
if (!hasCode2(error, "ENOENT"))
|
|
521
754
|
throw error;
|
|
522
755
|
}
|
|
523
756
|
return true;
|
|
524
757
|
}
|
|
525
758
|
function release(lockPath, token) {
|
|
526
|
-
|
|
527
|
-
if (current === null || current.record?.token !== token)
|
|
759
|
+
if (readSnapshot(lockPath, Date.now())?.record?.token !== token)
|
|
528
760
|
return;
|
|
529
761
|
try {
|
|
530
762
|
unlinkSync(lockPath);
|
|
531
763
|
} catch (error) {
|
|
532
|
-
if (!
|
|
764
|
+
if (!hasCode2(error, "ENOENT"))
|
|
533
765
|
throw error;
|
|
534
766
|
}
|
|
535
767
|
}
|
|
536
768
|
function backoffMs(attempt) {
|
|
537
|
-
|
|
538
|
-
return exponential + Math.random() * MIN_DELAY_MS;
|
|
769
|
+
return Math.min(100, 5 * 2 ** attempt) + Math.random() * 5;
|
|
539
770
|
}
|
|
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
|
-
});
|
|
771
|
+
function lockTimeout(lockPath, options) {
|
|
772
|
+
const holder = readSnapshot(lockPath, Date.now())?.record;
|
|
773
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
774
|
+
const owner = holder == null ? "another process" : `pid ${holder.pid}`;
|
|
775
|
+
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
776
|
}
|
|
551
|
-
function
|
|
777
|
+
function hasCode2(error, code) {
|
|
552
778
|
return error instanceof Error && "code" in error && error.code === code;
|
|
553
779
|
}
|
|
554
780
|
|
|
555
781
|
// components/ulw-loop/src/plan-io.ts
|
|
782
|
+
function readLedger2(repoRoot, scope) {
|
|
783
|
+
const lockPath = ulwLoopStateLockPath(repoRoot, scope);
|
|
784
|
+
if (heldLocks.getStore()?.has(lockPath)) {
|
|
785
|
+
assertStateLockOwned(lockPath);
|
|
786
|
+
materializeSync(ulwLoopDir(repoRoot, scope));
|
|
787
|
+
}
|
|
788
|
+
return readLedger(repoRoot, scope);
|
|
789
|
+
}
|
|
556
790
|
var LEGACY_OBJECTIVE_PREFIX = `Complete all ulw-loop stories in ${ULW_LOOP_DIR}/${ULW_LOOP_GOALS}: `;
|
|
557
791
|
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
792
|
var locks = new Map;
|
|
559
793
|
var heldLocks = new AsyncLocalStorage;
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
794
|
+
var lockOptions = new AsyncLocalStorage;
|
|
795
|
+
function tokenAt(lockPath) {
|
|
796
|
+
const raw = readOptional(lockPath);
|
|
797
|
+
if (raw === undefined)
|
|
798
|
+
return;
|
|
799
|
+
try {
|
|
800
|
+
const record = JSON.parse(raw);
|
|
801
|
+
return typeof record === "object" && record !== null && "token" in record && typeof record.token === "string" ? record.token : undefined;
|
|
802
|
+
} catch (error) {
|
|
803
|
+
if (error instanceof SyntaxError)
|
|
804
|
+
return;
|
|
805
|
+
throw error;
|
|
806
|
+
}
|
|
565
807
|
}
|
|
566
|
-
function
|
|
567
|
-
|
|
808
|
+
function assertStateLockOwned(lockPath) {
|
|
809
|
+
const context = heldLocks.getStore()?.get(lockPath);
|
|
810
|
+
if (context === undefined)
|
|
811
|
+
return;
|
|
812
|
+
if (tokenAt(lockPath) !== context.token)
|
|
813
|
+
throw new UlwLoopError("The ulw-loop mutation lock changed owners.", "ULW_LOOP_LOCK_LOST");
|
|
814
|
+
}
|
|
815
|
+
function migrationEntries(plan) {
|
|
816
|
+
for (const context of heldLocks.getStore()?.values() ?? []) {
|
|
817
|
+
const entries = context.migrations.get(plan.goalsPath);
|
|
818
|
+
if (entries !== undefined) {
|
|
819
|
+
context.migrations.delete(plan.goalsPath);
|
|
820
|
+
return entries;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
return [];
|
|
568
824
|
}
|
|
569
|
-
async function withUlwLoopMutationLock(repoRoot, scopeOrFn, maybeFn) {
|
|
825
|
+
async function withUlwLoopMutationLock(repoRoot, scopeOrFn, maybeFn, options = {}) {
|
|
570
826
|
const scope = typeof scopeOrFn === "function" ? undefined : scopeOrFn;
|
|
571
827
|
const fn = typeof scopeOrFn === "function" ? scopeOrFn : maybeFn;
|
|
572
828
|
if (fn === undefined)
|
|
573
829
|
throw new UlwLoopError("Missing ulw-loop mutation body.", "ULW_LOOP_LOCK_BODY_MISSING");
|
|
574
830
|
const lockKey = `${repoRoot}\x00${ulwLoopRelativeDir(scope)}`;
|
|
575
831
|
const lockPath = ulwLoopStateLockPath(repoRoot, scope);
|
|
576
|
-
const locked = () => withStateLock(lockPath, () =>
|
|
832
|
+
const locked = () => withStateLock(lockPath, async (token) => {
|
|
833
|
+
return heldLocks.run(new Map([...heldLocks.getStore() ?? [], [lockPath, { token, migrations: new Map }]]), async () => {
|
|
834
|
+
for (let attempt = 0;; attempt += 1) {
|
|
835
|
+
try {
|
|
836
|
+
return await fn();
|
|
837
|
+
} catch (error) {
|
|
838
|
+
if (!(error instanceof UlwLoopError) || error.code !== "ULW_LOOP_PUBLISH_CONFLICT")
|
|
839
|
+
throw error;
|
|
840
|
+
assertStateLockOwned(lockPath);
|
|
841
|
+
if (attempt !== 0)
|
|
842
|
+
throw error;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
});
|
|
846
|
+
}, { ...lockOptions.getStore(), ...options });
|
|
577
847
|
const prior = locks.get(lockKey) ?? Promise.resolve(undefined);
|
|
578
848
|
const run = prior.then(locked, locked);
|
|
579
849
|
const gate = run.then(() => {
|
|
@@ -588,98 +858,140 @@ async function withUlwLoopMutationLock(repoRoot, scopeOrFn, maybeFn) {
|
|
|
588
858
|
});
|
|
589
859
|
return run;
|
|
590
860
|
}
|
|
591
|
-
|
|
861
|
+
function readUlwLoopPlanSync(repoRoot, scope) {
|
|
592
862
|
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)) {
|
|
863
|
+
const parsed = reconcilePlan(ulwLoopDir(repoRoot, scope));
|
|
864
|
+
if (parsed === undefined)
|
|
865
|
+
throw planMissingError(repoRelative(path, repoRoot), listUlwLoopSessionIds(repoRoot));
|
|
866
|
+
if (parsed.version !== 1 || !Array.isArray(parsed.goals))
|
|
605
867
|
throw new UlwLoopError(`Invalid ulw-loop plan at ${repoRelative(path, repoRoot)}.`, "ULW_LOOP_PLAN_INVALID");
|
|
606
|
-
}
|
|
607
868
|
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();
|
|
869
|
+
if ((parsed.codexGoalMode ?? "per_story") === "aggregate" && previousObjective !== undefined && (previousObjective === LEGACY_OBJECTIVE || previousObjective.startsWith(LEGACY_OBJECTIVE_PREFIX))) {
|
|
613
870
|
parsed.codexObjective = aggregateCodexObjectiveForScope(scope);
|
|
614
871
|
parsed.codexObjectiveAliases = [...new Set([...parsed.codexObjectiveAliases ?? [], previousObjective])];
|
|
615
|
-
parsed.
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
}, scope);
|
|
872
|
+
heldLocks.getStore()?.get(ulwLoopStateLockPath(repoRoot, scope))?.migrations.set(parsed.goalsPath, [
|
|
873
|
+
{
|
|
874
|
+
at: iso(),
|
|
875
|
+
kind: "aggregate_objective_migrated",
|
|
876
|
+
before: { codexObjective: previousObjective },
|
|
877
|
+
after: { codexObjective: parsed.codexObjective }
|
|
878
|
+
}
|
|
879
|
+
]);
|
|
624
880
|
}
|
|
625
881
|
return parsed;
|
|
626
882
|
}
|
|
883
|
+
async function readUlwLoopPlan(repoRoot, scope) {
|
|
884
|
+
if (heldLocks.getStore()?.has(ulwLoopStateLockPath(repoRoot, scope))) {
|
|
885
|
+
assertStateLockOwned(ulwLoopStateLockPath(repoRoot, scope));
|
|
886
|
+
await materialize(ulwLoopDir(repoRoot, scope));
|
|
887
|
+
}
|
|
888
|
+
return readUlwLoopPlanSync(repoRoot, scope);
|
|
889
|
+
}
|
|
627
890
|
function listUlwLoopSessionIds(repoRoot) {
|
|
628
891
|
try {
|
|
629
|
-
return
|
|
892
|
+
return readdirSync2(ulwLoopDir(repoRoot), { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
|
|
630
893
|
} catch {
|
|
631
894
|
return [];
|
|
632
895
|
}
|
|
633
896
|
}
|
|
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);
|
|
897
|
+
function isSteeringKind(value) {
|
|
898
|
+
return value === "steering_accepted" || value === "steering_rejected" || value === "criteria_revised" || value === "batch_updated";
|
|
644
899
|
}
|
|
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");
|
|
900
|
+
async function findAcceptedSteeringLedgerEntry(repoRoot, key, scope) {
|
|
901
|
+
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
902
|
}
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
903
|
+
|
|
904
|
+
// components/ulw-loop/src/plan-commit.ts
|
|
905
|
+
var hooks = new AsyncLocalStorage2;
|
|
906
|
+
async function beforePlanMutation() {
|
|
907
|
+
await hooks.getStore()?.beforeMutation?.();
|
|
908
|
+
}
|
|
909
|
+
async function writeViewFile(path, content) {
|
|
910
|
+
await (hooks.getStore()?.writeView ?? writeFile)(path, content);
|
|
911
|
+
}
|
|
912
|
+
async function commit(repoRoot, scope, mutation) {
|
|
913
|
+
const { plan } = mutation;
|
|
914
|
+
const dir = ulwLoopDir(repoRoot, scope);
|
|
915
|
+
assertStateLockOwned(ulwLoopStateLockPath(repoRoot, scope));
|
|
916
|
+
await hooks.getStore()?.beforeCommit?.();
|
|
917
|
+
const next = (plan.revision ?? 0) + 1;
|
|
918
|
+
const brief = plan.brief ?? readOptional(join4(dir, "brief.md")) ?? "";
|
|
919
|
+
const entries = [...migrationEntries(plan), ...mutation.entries].map((entry, seq) => ({
|
|
920
|
+
...entry,
|
|
921
|
+
revision: next,
|
|
922
|
+
id: `${next}-${seq}`
|
|
923
|
+
}));
|
|
924
|
+
const record = {
|
|
925
|
+
version: 1,
|
|
926
|
+
revision: next,
|
|
927
|
+
plan: { ...plan, revision: next, brief },
|
|
928
|
+
ledger: entries
|
|
929
|
+
};
|
|
930
|
+
mkdirSync2(join4(dir, "revisions"), { recursive: true });
|
|
931
|
+
mkdirSync2(join4(dir, "tmp"), { recursive: true });
|
|
932
|
+
const temp = join4(dir, "tmp", `${process.pid}-${randomUUID2()}.json`);
|
|
656
933
|
try {
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
934
|
+
writeFileSync(temp, `${JSON.stringify(record)}
|
|
935
|
+
`, { flag: "wx" });
|
|
936
|
+
try {
|
|
937
|
+
(hooks.getStore()?.link ?? linkSync)(temp, join4(dir, "revisions", `${String(next).padStart(8, "0")}.json`));
|
|
938
|
+
} catch (error) {
|
|
939
|
+
if (hasCode(error, "EEXIST"))
|
|
940
|
+
throw new UlwLoopError("Another writer published the read revision.", "ULW_LOOP_PUBLISH_CONFLICT", {
|
|
941
|
+
cause: error
|
|
942
|
+
});
|
|
943
|
+
if (["EPERM", "ENOTSUP", "EXDEV"].some((code) => hasCode(error, code)))
|
|
944
|
+
throw new UlwLoopError("Immutable ulw-loop publication requires atomic hard links.", "ULW_LOOP_PUBLISH_UNSUPPORTED_FS", { cause: error });
|
|
663
945
|
throw error;
|
|
946
|
+
}
|
|
664
947
|
} finally {
|
|
665
|
-
|
|
666
|
-
stream.destroy();
|
|
948
|
+
rmSync(temp, { force: true });
|
|
667
949
|
}
|
|
950
|
+
plan.revision = next;
|
|
951
|
+
plan.brief = brief;
|
|
952
|
+
for (const [index, entry] of mutation.entries.entries())
|
|
953
|
+
Object.assign(entry, entries[index + entries.length - mutation.entries.length]);
|
|
954
|
+
await hooks.getStore()?.afterCommit?.();
|
|
955
|
+
await materialize(dir);
|
|
956
|
+
}
|
|
957
|
+
function viewContents(dir) {
|
|
958
|
+
const plan = reconcilePlan(dir);
|
|
959
|
+
if (plan === undefined || readRecords(dir).length === 0)
|
|
960
|
+
return [];
|
|
961
|
+
const ledger = readLedgerAt(dir);
|
|
962
|
+
const views = [
|
|
963
|
+
["goals.json", `${JSON.stringify(plan, null, 2)}
|
|
964
|
+
`],
|
|
965
|
+
["ledger.jsonl", ledger.length === 0 ? "" : `${ledger.map((entry) => JSON.stringify(entry)).join(`
|
|
966
|
+
`)}
|
|
967
|
+
`],
|
|
968
|
+
["brief.md", plan.brief ?? readOptional(join4(dir, "brief.md")) ?? ""]
|
|
969
|
+
];
|
|
970
|
+
return views.filter(([name, content]) => readOptional(join4(dir, name)) !== content);
|
|
668
971
|
}
|
|
669
|
-
|
|
670
|
-
const
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
972
|
+
function materializeSync(dir) {
|
|
973
|
+
for (const [name, content] of viewContents(dir)) {
|
|
974
|
+
mkdirSync2(join4(dir, "tmp"), { recursive: true });
|
|
975
|
+
const temp = join4(dir, "tmp", `${process.pid}-${randomUUID2()}-${name}`);
|
|
976
|
+
try {
|
|
977
|
+
writeFileSync(temp, content, { flag: "wx" });
|
|
978
|
+
renameSync(temp, join4(dir, name));
|
|
979
|
+
} finally {
|
|
980
|
+
rmSync(temp, { force: true });
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
async function materialize(dir) {
|
|
985
|
+
for (const [name, content] of viewContents(dir)) {
|
|
986
|
+
mkdirSync2(join4(dir, "tmp"), { recursive: true });
|
|
987
|
+
const temp = join4(dir, "tmp", `${process.pid}-${randomUUID2()}-${name}`);
|
|
988
|
+
try {
|
|
989
|
+
await writeViewFile(temp, content);
|
|
990
|
+
await rename(temp, join4(dir, name));
|
|
991
|
+
} finally {
|
|
992
|
+
rmSync(temp, { force: true });
|
|
993
|
+
}
|
|
681
994
|
}
|
|
682
|
-
return;
|
|
683
995
|
}
|
|
684
996
|
|
|
685
997
|
// components/ulw-loop/src/evidence.ts
|
|
@@ -719,6 +1031,7 @@ async function recordEvidence(repoRoot, args, scope) {
|
|
|
719
1031
|
const goal = findGoal(plan, args.goalId);
|
|
720
1032
|
const criterion = findCriterion(goal, args.criterionId);
|
|
721
1033
|
const evidence = nonEmptyEvidence(args.evidence);
|
|
1034
|
+
const artifacts = resolveEvidenceArtifacts(repoRoot, args.artifacts);
|
|
722
1035
|
const kind = ledgerKind(args.status);
|
|
723
1036
|
const prevStatus = criterion.status;
|
|
724
1037
|
const capturedAt = iso();
|
|
@@ -727,9 +1040,12 @@ async function recordEvidence(repoRoot, args, scope) {
|
|
|
727
1040
|
criterion.capturedAt = capturedAt;
|
|
728
1041
|
if (args.notes !== undefined)
|
|
729
1042
|
criterion.notes = args.notes;
|
|
1043
|
+
if (artifacts !== undefined)
|
|
1044
|
+
criterion.artifacts = artifacts;
|
|
1045
|
+
else
|
|
1046
|
+
delete criterion.artifacts;
|
|
730
1047
|
goal.updatedAt = capturedAt;
|
|
731
1048
|
plan.updatedAt = capturedAt;
|
|
732
|
-
await writePlan(repoRoot, plan, scope);
|
|
733
1049
|
const ledgerEntry = {
|
|
734
1050
|
at: capturedAt,
|
|
735
1051
|
kind,
|
|
@@ -738,10 +1054,11 @@ async function recordEvidence(repoRoot, args, scope) {
|
|
|
738
1054
|
criterionStatus: args.status,
|
|
739
1055
|
evidence,
|
|
740
1056
|
capturedEvidence: evidence,
|
|
1057
|
+
...artifacts === undefined ? {} : { artifacts },
|
|
741
1058
|
before: { status: prevStatus },
|
|
742
1059
|
after: { goalId: goal.id, criterionId: criterion.id, status: args.status, evidence, capturedAt, prevStatus }
|
|
743
1060
|
};
|
|
744
|
-
await
|
|
1061
|
+
await commit(repoRoot, scope, { plan, entries: [ledgerEntry] });
|
|
745
1062
|
return { plan, goal, criterion, ledgerEntry };
|
|
746
1063
|
});
|
|
747
1064
|
}
|
|
@@ -789,7 +1106,7 @@ function requireEssentialCriteriaPass(goal) {
|
|
|
789
1106
|
}
|
|
790
1107
|
|
|
791
1108
|
// components/ulw-loop/src/quality-gate-artifacts.ts
|
|
792
|
-
import { resolve as
|
|
1109
|
+
import { resolve as resolve3 } from "node:path";
|
|
793
1110
|
|
|
794
1111
|
// components/ulw-loop/src/quality-gate-fields.ts
|
|
795
1112
|
var PLACEHOLDER_PATTERN = /^(?:<replace:[^>]+>|placeholder|todo|tbd|n\/a|stub)$/i;
|
|
@@ -928,14 +1245,14 @@ function artifactCompatible(surface, kind) {
|
|
|
928
1245
|
function checkFile(path, field, opts) {
|
|
929
1246
|
if (opts?.repoRoot === undefined || opts.fs === undefined || isPoisoned(field))
|
|
930
1247
|
return;
|
|
931
|
-
const absolute =
|
|
1248
|
+
const absolute = resolve3(opts.repoRoot, path);
|
|
932
1249
|
if (!opts.fs.existsSync(absolute)) {
|
|
933
1250
|
invalid(`${field} must point to an existing artifact.`, field);
|
|
934
1251
|
return;
|
|
935
1252
|
}
|
|
936
1253
|
if (opts.fs.statSync(absolute).size <= 0)
|
|
937
1254
|
invalid(`${field} must point to a non-empty artifact.`, field);
|
|
938
|
-
if (opts.currentAttemptDir !== undefined && !isWithinAttemptDir(absolute,
|
|
1255
|
+
if (opts.currentAttemptDir !== undefined && !isWithinAttemptDir(absolute, resolve3(opts.repoRoot, opts.currentAttemptDir)))
|
|
939
1256
|
invalid(`${field} (${path}) must point to an artifact from the current attempt (${opts.currentAttemptDir}).`, field);
|
|
940
1257
|
}
|
|
941
1258
|
function artifactMap(refs) {
|
|
@@ -1011,8 +1328,8 @@ function adversarialVerdict(row, field) {
|
|
|
1011
1328
|
}
|
|
1012
1329
|
|
|
1013
1330
|
// components/ulw-loop/src/surface.ts
|
|
1014
|
-
import { existsSync as
|
|
1015
|
-
import { dirname as dirname2, join as
|
|
1331
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "node:fs";
|
|
1332
|
+
import { dirname as dirname2, join as join5 } from "node:path";
|
|
1016
1333
|
import { fileURLToPath } from "node:url";
|
|
1017
1334
|
var REVIEWER_ROLES_BY_SURFACE = {
|
|
1018
1335
|
lazycodex: {
|
|
@@ -1066,14 +1383,14 @@ function resolveToolkitSurface(options) {
|
|
|
1066
1383
|
if (fromEnv !== null)
|
|
1067
1384
|
return fromEnv;
|
|
1068
1385
|
const entryDir = options?.entryDir ?? dirname2(fileURLToPath(import.meta.url));
|
|
1069
|
-
const markerPath =
|
|
1386
|
+
const markerPath = join5(entryDir, SURFACE_MARKER_FILENAME);
|
|
1070
1387
|
return readMarkerSurface(markerPath) ?? "lazycodex";
|
|
1071
1388
|
}
|
|
1072
1389
|
function readMarkerSurface(markerPath) {
|
|
1073
1390
|
try {
|
|
1074
|
-
if (!
|
|
1391
|
+
if (!existsSync4(markerPath))
|
|
1075
1392
|
return null;
|
|
1076
|
-
const parsed = JSON.parse(
|
|
1393
|
+
const parsed = JSON.parse(readFileSync3(markerPath, "utf8"));
|
|
1077
1394
|
return parseSurface(parsed["surface"]);
|
|
1078
1395
|
} catch (error) {
|
|
1079
1396
|
if (error instanceof Error)
|
|
@@ -1274,7 +1591,7 @@ function parseAdversarialCases(value, byId) {
|
|
|
1274
1591
|
}
|
|
1275
1592
|
|
|
1276
1593
|
// components/ulw-loop/src/cli-arg-parser.ts
|
|
1277
|
-
import { readFile as
|
|
1594
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
1278
1595
|
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
1596
|
var SUBCOMMANDS = new Set("create-goals status complete-goals criteria record-evidence checkpoint steer add-goal record-review-blockers".split(" "));
|
|
1280
1597
|
function hasFlag(argv, flag) {
|
|
@@ -1322,7 +1639,7 @@ async function readJsonInput(value) {
|
|
|
1322
1639
|
if (value === undefined)
|
|
1323
1640
|
return;
|
|
1324
1641
|
try {
|
|
1325
|
-
return JSON.parse(looksLikeJson(value) ? value : await
|
|
1642
|
+
return JSON.parse(looksLikeJson(value) ? value : await readFile2(value, "utf8"));
|
|
1326
1643
|
} catch (error) {
|
|
1327
1644
|
const message = error instanceof Error ? error.message : "unknown error";
|
|
1328
1645
|
throw new UlwLoopError(`Invalid JSON input: ${message}`, "ULW_LOOP_JSON_INPUT_INVALID", { cause: error });
|
|
@@ -1332,7 +1649,7 @@ async function parseCodexGoalJson(value) {
|
|
|
1332
1649
|
if (value === undefined)
|
|
1333
1650
|
return;
|
|
1334
1651
|
try {
|
|
1335
|
-
const raw = looksLikeJson(value) ? value : await
|
|
1652
|
+
const raw = looksLikeJson(value) ? value : await readFile2(value, "utf8");
|
|
1336
1653
|
JSON.parse(raw);
|
|
1337
1654
|
return raw;
|
|
1338
1655
|
} catch (error) {
|
|
@@ -1384,23 +1701,23 @@ async function parseValidationBatches(input, goals) {
|
|
|
1384
1701
|
if (raw === undefined)
|
|
1385
1702
|
return;
|
|
1386
1703
|
if (!Array.isArray(raw))
|
|
1387
|
-
|
|
1704
|
+
fail2("--validation-batch-json must be a JSON array.");
|
|
1388
1705
|
const batches = raw.map(batchFromObject);
|
|
1389
1706
|
validateBatches(batches, goals);
|
|
1390
1707
|
return batches;
|
|
1391
1708
|
}
|
|
1392
1709
|
function batchFromObject(value) {
|
|
1393
1710
|
if (!isObject(value))
|
|
1394
|
-
|
|
1711
|
+
fail2("validation batch entries must be objects.");
|
|
1395
1712
|
const batchId = text(value, "batchId");
|
|
1396
1713
|
const memberIds = strings(value, "memberIds");
|
|
1397
1714
|
const finalGoalId = text(value, "finalGoalId");
|
|
1398
1715
|
if (batchId === undefined)
|
|
1399
|
-
|
|
1716
|
+
fail2("validation batch requires batchId.");
|
|
1400
1717
|
if (memberIds === undefined || memberIds.length < 2)
|
|
1401
|
-
|
|
1718
|
+
fail2("validation batch requires at least two memberIds.");
|
|
1402
1719
|
if (finalGoalId === undefined)
|
|
1403
|
-
|
|
1720
|
+
fail2("validation batch requires finalGoalId.");
|
|
1404
1721
|
return { batchId, memberIds, finalGoalId };
|
|
1405
1722
|
}
|
|
1406
1723
|
function validateBatches(batches, goals) {
|
|
@@ -1409,17 +1726,17 @@ function validateBatches(batches, goals) {
|
|
|
1409
1726
|
const members = new Set;
|
|
1410
1727
|
for (const batch of batches) {
|
|
1411
1728
|
if (batchIds.has(batch.batchId))
|
|
1412
|
-
|
|
1729
|
+
fail2(`duplicate validation batch id: ${batch.batchId}.`);
|
|
1413
1730
|
batchIds.add(batch.batchId);
|
|
1414
1731
|
if (new Set(batch.memberIds).size !== batch.memberIds.length)
|
|
1415
|
-
|
|
1732
|
+
fail2(`validation batch ${batch.batchId} has duplicate memberIds.`);
|
|
1416
1733
|
if (!batch.memberIds.includes(batch.finalGoalId))
|
|
1417
|
-
|
|
1734
|
+
fail2(`validation batch ${batch.batchId} finalGoalId must be a member.`, "ULW_LOOP_VALIDATION_BATCH_FINAL_NOT_MEMBER");
|
|
1418
1735
|
for (const memberId of batch.memberIds) {
|
|
1419
1736
|
if (!goalIds.has(memberId))
|
|
1420
|
-
|
|
1737
|
+
fail2(`validation batch ${batch.batchId} references unknown goal: ${memberId}.`, "ULW_LOOP_VALIDATION_BATCH_MEMBER_UNKNOWN");
|
|
1421
1738
|
if (members.has(memberId))
|
|
1422
|
-
|
|
1739
|
+
fail2(`goal appears in multiple validation batches: ${memberId}.`, "ULW_LOOP_VALIDATION_BATCH_OVERLAP");
|
|
1423
1740
|
members.add(memberId);
|
|
1424
1741
|
}
|
|
1425
1742
|
}
|
|
@@ -1478,12 +1795,12 @@ function memberResolved(plan, goalId) {
|
|
|
1478
1795
|
const goal = plan.goals.find((candidate) => candidate.id === goalId);
|
|
1479
1796
|
return goal !== undefined && isMemberResolved(goal, plan);
|
|
1480
1797
|
}
|
|
1481
|
-
function
|
|
1798
|
+
function fail2(message, code = "ULW_LOOP_VALIDATION_BATCH_INVALID") {
|
|
1482
1799
|
throw new UlwLoopError(message, code);
|
|
1483
1800
|
}
|
|
1484
1801
|
|
|
1485
1802
|
// components/ulw-loop/src/checkpoint.ts
|
|
1486
|
-
var QUALITY_GATE_FS = { existsSync:
|
|
1803
|
+
var QUALITY_GATE_FS = { existsSync: existsSync5, statSync: statSync2 };
|
|
1487
1804
|
function ulwLoopFail2(message, code) {
|
|
1488
1805
|
throw new UlwLoopError(message, code);
|
|
1489
1806
|
}
|
|
@@ -1505,11 +1822,11 @@ async function readJsonInput2(raw, repoRoot) {
|
|
|
1505
1822
|
if (!(error instanceof SyntaxError))
|
|
1506
1823
|
throw error;
|
|
1507
1824
|
}
|
|
1508
|
-
const path =
|
|
1509
|
-
if (!
|
|
1825
|
+
const path = resolve4(repoRoot, trimmed);
|
|
1826
|
+
if (!existsSync5(path))
|
|
1510
1827
|
return ulwLoopFail2("Quality gate JSON is neither valid JSON nor a readable path.", "ulw_loop_json_input_invalid");
|
|
1511
1828
|
try {
|
|
1512
|
-
return JSON.parse(await
|
|
1829
|
+
return JSON.parse(await readFile3(path, "utf8"));
|
|
1513
1830
|
} catch (error) {
|
|
1514
1831
|
return ulwLoopFail2(`Quality gate path does not contain valid JSON${error instanceof Error ? `: ${error.message}` : "."}`, "ulw_loop_json_input_invalid");
|
|
1515
1832
|
}
|
|
@@ -1605,6 +1922,7 @@ async function checkpointUlwLoop(repoRoot, args, scope, dependencies) {
|
|
|
1605
1922
|
codexGoal = validation.raw;
|
|
1606
1923
|
nextActions = validation.nextActions;
|
|
1607
1924
|
warnings = validation.warnings;
|
|
1925
|
+
acknowledgeDriverObjective(plan, validation.unacknowledgedObjective);
|
|
1608
1926
|
} catch (error) {
|
|
1609
1927
|
if (!(error instanceof UlwLoopError))
|
|
1610
1928
|
throw error;
|
|
@@ -1649,12 +1967,12 @@ async function checkpointUlwLoop(repoRoot, args, scope, dependencies) {
|
|
|
1649
1967
|
if (aggregateCompletion !== undefined)
|
|
1650
1968
|
nextActions = [...nextActions, 'aggregate complete — now update_goal({status:"complete"})'];
|
|
1651
1969
|
plan.updatedAt = now;
|
|
1652
|
-
await writePlan(repoRoot, plan, scope);
|
|
1653
1970
|
const ledgerEntry = buildLedger(now, args, goal, qualityGate, codexGoal, aggregateCompletion);
|
|
1654
|
-
|
|
1971
|
+
const entries = [ledgerEntry];
|
|
1655
1972
|
const closedBatch = args.status === "complete" ? batchClosedBy(plan, goal.id) : undefined;
|
|
1656
1973
|
if (closedBatch !== undefined)
|
|
1657
|
-
|
|
1974
|
+
entries.push({ at: now, kind: "batch_closed", goalId: goal.id, message: closedBatch.batchId });
|
|
1975
|
+
await commit(repoRoot, scope, { plan, entries });
|
|
1658
1976
|
return aggregateCompletion === undefined ? { plan, goal, ledgerEntry, nextActions, warnings } : { plan, goal, ledgerEntry, aggregateCompletion, nextActions, warnings };
|
|
1659
1977
|
});
|
|
1660
1978
|
}
|
|
@@ -1990,9 +2308,47 @@ function joinLines(lines) {
|
|
|
1990
2308
|
`);
|
|
1991
2309
|
}
|
|
1992
2310
|
|
|
1993
|
-
// components/ulw-loop/src/
|
|
1994
|
-
|
|
1995
|
-
|
|
2311
|
+
// components/ulw-loop/src/success-criteria-input.ts
|
|
2312
|
+
function invalid2(message, details) {
|
|
2313
|
+
throw new UlwLoopError(`Invalid successCriteria: ${message}`, "ULW_LOOP_ARGUMENT_INVALID", { details });
|
|
2314
|
+
}
|
|
2315
|
+
function isUserModel(value) {
|
|
2316
|
+
return typeof value === "string" && ULW_LOOP_SUCCESS_CRITERION_USER_MODELS.some((model) => model === value);
|
|
2317
|
+
}
|
|
2318
|
+
function requireText(value, field, index) {
|
|
2319
|
+
const trimmed = typeof value === "string" ? value.trim() : "";
|
|
2320
|
+
return trimmed || invalid2(`entry ${index + 1} needs a non-empty ${field}.`, { index, field });
|
|
2321
|
+
}
|
|
2322
|
+
function criterionId(index) {
|
|
2323
|
+
return `C${String(index + 1).padStart(3, "0")}`;
|
|
2324
|
+
}
|
|
2325
|
+
function criteriaFromInput(input) {
|
|
2326
|
+
if (input.length === 0)
|
|
2327
|
+
invalid2("provide at least one criterion or omit the field for placeholders.", { count: 0 });
|
|
2328
|
+
return input.map((entry, index) => {
|
|
2329
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry))
|
|
2330
|
+
return invalid2(`entry ${index + 1} must be an object.`, { index });
|
|
2331
|
+
const record = { ...entry };
|
|
2332
|
+
const userModel = record["userModel"] ?? "happy";
|
|
2333
|
+
if (!isUserModel(userModel))
|
|
2334
|
+
return invalid2(`entry ${index + 1} userModel must be one of ${ULW_LOOP_SUCCESS_CRITERION_USER_MODELS.join(", ")}.`, {
|
|
2335
|
+
index,
|
|
2336
|
+
userModel: String(userModel)
|
|
2337
|
+
});
|
|
2338
|
+
const essential = record["essential"];
|
|
2339
|
+
if (essential !== undefined && typeof essential !== "boolean")
|
|
2340
|
+
return invalid2(`entry ${index + 1} essential must be a boolean.`, { index });
|
|
2341
|
+
return {
|
|
2342
|
+
id: criterionId(index),
|
|
2343
|
+
scenario: requireText(record["scenario"], "scenario", index),
|
|
2344
|
+
userModel,
|
|
2345
|
+
expectedEvidence: requireText(record["expectedEvidence"], "expectedEvidence", index),
|
|
2346
|
+
essential: essential ?? true,
|
|
2347
|
+
capturedEvidence: null,
|
|
2348
|
+
status: "pending"
|
|
2349
|
+
};
|
|
2350
|
+
});
|
|
2351
|
+
}
|
|
1996
2352
|
|
|
1997
2353
|
// components/ulw-loop/src/plan-goal-factory.ts
|
|
1998
2354
|
function cleanLine(line) {
|
|
@@ -2018,40 +2374,35 @@ function assertNonEmpty(value, label) {
|
|
|
2018
2374
|
function truncateObjective(objective) {
|
|
2019
2375
|
return objective.length > 80 ? `${objective.slice(0, 77).trimEnd()}...` : objective;
|
|
2020
2376
|
}
|
|
2021
|
-
function
|
|
2377
|
+
function replaceVia(goalId, id, surface) {
|
|
2378
|
+
return surface === "omo-senpi" ? `Replace via agentToolkit.steer({ kind: "revise_criterion", source: "finding", goalId: "${goalId}", criterionId: "${id}", scenario, expectedEvidence, evidence, rationale }) (or pass successCriteria to addGoal)` : `Replace via omo-agent-toolkit ulw-loop steer --kind revise_criterion --goal-id ${goalId} --criterion-id ${id} --scenario "<scenario>" --expected-evidence "<proof>" --evidence "<why>" --rationale "<why>"`;
|
|
2379
|
+
}
|
|
2380
|
+
function seedDefaultSuccessCriteria(goalIndex, objective, options = {}) {
|
|
2022
2381
|
const subject = truncateObjective(normalizeObjective2(objective) || `Goal ${goalIndex + 1}`);
|
|
2382
|
+
const goalId = options.goalId ?? `G${String(goalIndex + 1).padStart(3, "0")}`;
|
|
2383
|
+
const surface = options.surface ?? "lazycodex";
|
|
2023
2384
|
const rows = [
|
|
2385
|
+
["happy", `happy path for: ${subject}`, `observable happy-path proof for goal ${goalIndex + 1}`, true],
|
|
2386
|
+
["edge", "edge case (boundary/empty/malformed)", `boundary or malformed-input proof for: ${subject}`, true],
|
|
2024
2387
|
[
|
|
2025
|
-
"C001",
|
|
2026
|
-
"happy",
|
|
2027
|
-
`happy path for: ${subject}`,
|
|
2028
|
-
`Replace via revise_criterion with observable happy-path proof for goal ${goalIndex + 1}.`,
|
|
2029
|
-
true
|
|
2030
|
-
],
|
|
2031
|
-
[
|
|
2032
|
-
"C002",
|
|
2033
|
-
"edge",
|
|
2034
|
-
"edge case (boundary/empty/malformed)",
|
|
2035
|
-
`Replace via revise_criterion with boundary or malformed-input proof for: ${subject}.`,
|
|
2036
|
-
true
|
|
2037
|
-
],
|
|
2038
|
-
[
|
|
2039
|
-
"C003",
|
|
2040
2388
|
"regression",
|
|
2041
2389
|
"regression: adjacent surface still works",
|
|
2042
|
-
`
|
|
2390
|
+
`regression proof for neighboring behavior after: ${subject}`,
|
|
2043
2391
|
false
|
|
2044
2392
|
]
|
|
2045
2393
|
];
|
|
2046
|
-
return rows.map(([
|
|
2047
|
-
id
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2394
|
+
return rows.map(([userModel, scenario, proof, essential], index) => {
|
|
2395
|
+
const id = criterionId(index);
|
|
2396
|
+
return {
|
|
2397
|
+
id,
|
|
2398
|
+
scenario,
|
|
2399
|
+
userModel,
|
|
2400
|
+
expectedEvidence: `${replaceVia(goalId, id, surface)} with ${proof}.`,
|
|
2401
|
+
essential,
|
|
2402
|
+
capturedEvidence: null,
|
|
2403
|
+
status: "pending"
|
|
2404
|
+
};
|
|
2405
|
+
});
|
|
2055
2406
|
}
|
|
2056
2407
|
function deriveGoalCandidates(brief) {
|
|
2057
2408
|
const bulletGoals = brief.split(/\r?\n/).map((line) => ({ original: line, cleaned: normalizeObjective2(cleanLine(line)) })).filter(({ cleaned }) => cleaned.length > 0 && cleaned.length <= 1200).filter(({ original, cleaned }, index, all) => /^\s*(?:[-*+]\s+|\d+[.)]\s+)/.test(original) && all.findIndex((candidate) => candidate.cleaned === cleaned) === index).map(({ cleaned }) => cleaned);
|
|
@@ -2062,22 +2413,27 @@ function deriveGoalCandidates(brief) {
|
|
|
2062
2413
|
objective
|
|
2063
2414
|
}));
|
|
2064
2415
|
}
|
|
2065
|
-
function makeGoal(title, objective, index, now) {
|
|
2416
|
+
function makeGoal(title, objective, index, now, options = {}) {
|
|
2066
2417
|
const cleanTitle = assertNonEmpty(title, "title");
|
|
2067
2418
|
const cleanObjective = assertNonEmpty(objective, "objective");
|
|
2419
|
+
const id = normalizeGoalId(cleanTitle, index);
|
|
2420
|
+
const successCriteria = options.successCriteria === undefined ? seedDefaultSuccessCriteria(index, cleanObjective, {
|
|
2421
|
+
goalId: id,
|
|
2422
|
+
...options.surface === undefined ? {} : { surface: options.surface }
|
|
2423
|
+
}) : criteriaFromInput(options.successCriteria);
|
|
2068
2424
|
return {
|
|
2069
|
-
id
|
|
2425
|
+
id,
|
|
2070
2426
|
title: cleanTitle,
|
|
2071
2427
|
objective: cleanObjective,
|
|
2072
2428
|
status: "pending",
|
|
2073
|
-
successCriteria
|
|
2429
|
+
successCriteria,
|
|
2074
2430
|
attempt: 0,
|
|
2075
2431
|
createdAt: now,
|
|
2076
2432
|
updatedAt: now
|
|
2077
2433
|
};
|
|
2078
2434
|
}
|
|
2079
|
-
function appendGoalToPlan(plan, title, objective, now) {
|
|
2080
|
-
const goal = makeGoal(title, objective, plan.goals.length, now);
|
|
2435
|
+
function appendGoalToPlan(plan, title, objective, now, options = {}) {
|
|
2436
|
+
const goal = makeGoal(title, objective, plan.goals.length, now, options);
|
|
2081
2437
|
plan.goals.push(goal);
|
|
2082
2438
|
plan.updatedAt = now;
|
|
2083
2439
|
return goal;
|
|
@@ -2099,18 +2455,31 @@ function clearGoalBlockerFields2(goal) {
|
|
|
2099
2455
|
])
|
|
2100
2456
|
delete goal[key];
|
|
2101
2457
|
}
|
|
2102
|
-
async function createUlwLoopPlan(repoRoot, args, scope) {
|
|
2458
|
+
async function createUlwLoopPlan(repoRoot, args, scope, surface = "lazycodex") {
|
|
2103
2459
|
return withUlwLoopMutationLock(repoRoot, scope, async () => {
|
|
2104
|
-
|
|
2105
|
-
|
|
2460
|
+
let existing;
|
|
2461
|
+
if (planExists(repoRoot, scope)) {
|
|
2462
|
+
try {
|
|
2463
|
+
existing = await readUlwLoopPlan(repoRoot, scope);
|
|
2464
|
+
} catch (error) {
|
|
2465
|
+
if (!args.force)
|
|
2466
|
+
throw error;
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
if (!args.force && existing !== undefined) {
|
|
2106
2470
|
if (isUlwLoopDone(existing))
|
|
2107
|
-
throw completedPlanExistsError(scope);
|
|
2471
|
+
throw completedPlanExistsError(scope, surface);
|
|
2108
2472
|
throw new UlwLoopError(`Refusing to overwrite existing ${ulwLoopGoalsRelativePath(scope)}; pass --force to recreate it.`, "ULW_LOOP_PLAN_EXISTS");
|
|
2109
2473
|
}
|
|
2110
2474
|
const now = iso();
|
|
2111
|
-
const goals = deriveGoalCandidates(args.brief).map((goal, index) => makeGoal(goal.title, goal.objective, index, now));
|
|
2475
|
+
const goals = deriveGoalCandidates(args.brief).map((goal, index) => makeGoal(goal.title, goal.objective, index, now, { surface }));
|
|
2112
2476
|
const plan = {
|
|
2113
2477
|
version: 1,
|
|
2478
|
+
revision: existing?.revision ?? 0,
|
|
2479
|
+
ledgerResetRevision: (existing?.revision ?? 0) + 1,
|
|
2480
|
+
brief: args.brief.endsWith(`
|
|
2481
|
+
`) ? args.brief : `${args.brief}
|
|
2482
|
+
`,
|
|
2114
2483
|
evidenceLayoutVersion: 2,
|
|
2115
2484
|
createdAt: now,
|
|
2116
2485
|
updatedAt: now,
|
|
@@ -2125,30 +2494,37 @@ async function createUlwLoopPlan(repoRoot, args, scope) {
|
|
|
2125
2494
|
plan.validationBatches = validationBatches;
|
|
2126
2495
|
if (plan.codexGoalMode === "aggregate")
|
|
2127
2496
|
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);
|
|
2497
|
+
await beforePlanMutation();
|
|
2498
|
+
await commit(repoRoot, scope, {
|
|
2499
|
+
plan,
|
|
2500
|
+
entries: [{ at: now, kind: "plan_created", message: `${goals.length} goal(s) created` }]
|
|
2501
|
+
});
|
|
2135
2502
|
return plan;
|
|
2136
2503
|
});
|
|
2137
2504
|
}
|
|
2138
|
-
function completedPlanExistsError(scope) {
|
|
2505
|
+
function completedPlanExistsError(scope, surface) {
|
|
2139
2506
|
return new UlwLoopError([
|
|
2140
2507
|
`Existing ulw-loop aggregate is already complete at ${ulwLoopGoalsRelativePath(scope)}.`,
|
|
2141
|
-
|
|
2142
|
-
|
|
2508
|
+
...surface === "omo-senpi" ? [
|
|
2509
|
+
"Start a new run under a fresh session id (a new senpi session) or call agentToolkit.createGoals({ brief, force: true }) to recreate."
|
|
2510
|
+
] : [
|
|
2511
|
+
"Start a new run with `omo-agent-toolkit ulw-loop create-goals --session-id <new-id> ...` to isolate fresh state.",
|
|
2512
|
+
"Use --force only when you intentionally want to overwrite the completed evidence."
|
|
2513
|
+
]
|
|
2143
2514
|
].join(" "), "ULW_LOOP_PLAN_EXISTS_COMPLETE");
|
|
2144
2515
|
}
|
|
2145
|
-
async function addUlwLoopGoal(repoRoot, args, scope) {
|
|
2516
|
+
async function addUlwLoopGoal(repoRoot, args, scope, surface = "lazycodex") {
|
|
2146
2517
|
return withUlwLoopMutationLock(repoRoot, scope, async () => {
|
|
2147
2518
|
const plan = await readUlwLoopPlan(repoRoot, scope);
|
|
2148
2519
|
const now = iso();
|
|
2149
|
-
const goal = appendGoalToPlan(plan, args.title, args.objective, now
|
|
2150
|
-
|
|
2151
|
-
|
|
2520
|
+
const goal = appendGoalToPlan(plan, args.title, args.objective, now, {
|
|
2521
|
+
surface,
|
|
2522
|
+
...args.successCriteria === undefined ? {} : { successCriteria: args.successCriteria }
|
|
2523
|
+
});
|
|
2524
|
+
await commit(repoRoot, scope, {
|
|
2525
|
+
plan,
|
|
2526
|
+
entries: [{ at: now, kind: "goal_added", goalId: goal.id, status: goal.status, message: goal.title }]
|
|
2527
|
+
});
|
|
2152
2528
|
return { plan, goal };
|
|
2153
2529
|
});
|
|
2154
2530
|
}
|
|
@@ -2161,17 +2537,18 @@ async function startNextUlwLoop(repoRoot, args = {}, scope) {
|
|
|
2161
2537
|
const existing = plan.goals.find((goal) => goal.status === "in_progress" && isScheduleEligible(goal));
|
|
2162
2538
|
if (existing)
|
|
2163
2539
|
return { plan, goal: existing, resumed: true };
|
|
2540
|
+
const entries = [];
|
|
2164
2541
|
let next = plan.goals.find((goal) => goal.status === "pending" && isScheduleEligible(goal));
|
|
2165
2542
|
if (!next && args.retryFailed) {
|
|
2166
2543
|
next = plan.goals.find((goal) => goal.status === "failed" && !goal.nonRetriable && isScheduleEligible(goal));
|
|
2167
2544
|
if (next)
|
|
2168
|
-
|
|
2545
|
+
entries.push({
|
|
2169
2546
|
at: now,
|
|
2170
2547
|
kind: "goal_retried",
|
|
2171
2548
|
goalId: next.id,
|
|
2172
2549
|
status: "pending",
|
|
2173
2550
|
...next.failureReason ? { message: next.failureReason } : {}
|
|
2174
|
-
}
|
|
2551
|
+
});
|
|
2175
2552
|
}
|
|
2176
2553
|
if (!next)
|
|
2177
2554
|
return { done: true, plan };
|
|
@@ -2182,8 +2559,14 @@ async function startNextUlwLoop(repoRoot, args = {}, scope) {
|
|
|
2182
2559
|
next.updatedAt = now;
|
|
2183
2560
|
plan.activeGoalId = next.id;
|
|
2184
2561
|
plan.updatedAt = now;
|
|
2185
|
-
|
|
2186
|
-
|
|
2562
|
+
entries.push({
|
|
2563
|
+
at: now,
|
|
2564
|
+
kind: "goal_started",
|
|
2565
|
+
goalId: next.id,
|
|
2566
|
+
status: next.status,
|
|
2567
|
+
message: `Attempt ${next.attempt}`
|
|
2568
|
+
});
|
|
2569
|
+
await commit(repoRoot, scope, { plan, entries });
|
|
2187
2570
|
return { plan, goal: next, resumed: false };
|
|
2188
2571
|
});
|
|
2189
2572
|
}
|
|
@@ -2279,7 +2662,7 @@ function checkpointStatus(value) {
|
|
|
2279
2662
|
}
|
|
2280
2663
|
|
|
2281
2664
|
// components/ulw-loop/src/cli-subcommands.ts
|
|
2282
|
-
import { readFile as
|
|
2665
|
+
import { readFile as readFile4 } from "node:fs/promises";
|
|
2283
2666
|
|
|
2284
2667
|
// components/ulw-loop/src/cli-steering.ts
|
|
2285
2668
|
var SOURCES = ["user_prompt_submit", "finding", "cli"];
|
|
@@ -2305,7 +2688,7 @@ function isSource(value) {
|
|
|
2305
2688
|
function isModel(value) {
|
|
2306
2689
|
return ULW_LOOP_SUCCESS_CRITERION_USER_MODELS.some((model) => model === value);
|
|
2307
2690
|
}
|
|
2308
|
-
function
|
|
2691
|
+
function fail3(message, code, details) {
|
|
2309
2692
|
throw new UlwLoopError(message, code, { details });
|
|
2310
2693
|
}
|
|
2311
2694
|
function kindMessage(prefix) {
|
|
@@ -2319,15 +2702,15 @@ function text2(value, field) {
|
|
|
2319
2702
|
const trimmed = value.trim();
|
|
2320
2703
|
if (trimmed.length > 0)
|
|
2321
2704
|
return trimmed;
|
|
2322
|
-
return
|
|
2705
|
+
return fail3(`Empty ${field}.`, "ULW_LOOP_STEERING_FIELD_EMPTY", { field });
|
|
2323
2706
|
}
|
|
2324
2707
|
function required3(argv, flag) {
|
|
2325
2708
|
const value = text2(readValue(argv, flag), flag);
|
|
2326
|
-
return value ??
|
|
2709
|
+
return value ?? fail3(`Missing ${flag}.`, "ULW_LOOP_STEERING_FIELD_REQUIRED", { flag });
|
|
2327
2710
|
}
|
|
2328
2711
|
function requiredGoal(argv) {
|
|
2329
2712
|
const value = text2(parseGoalArg(argv), "--goal-id");
|
|
2330
|
-
return value ??
|
|
2713
|
+
return value ?? fail3("Missing --goal-id.", "ULW_LOOP_GOAL_ID_REQUIRED", { flag: "--goal-id" });
|
|
2331
2714
|
}
|
|
2332
2715
|
function readObject(value, key) {
|
|
2333
2716
|
return Object.entries(value).find(([name]) => name === key)?.[1];
|
|
@@ -2351,7 +2734,7 @@ function objectChildren(value, key) {
|
|
|
2351
2734
|
for (const item of candidate) {
|
|
2352
2735
|
const next = child(item);
|
|
2353
2736
|
if (next === null)
|
|
2354
|
-
return
|
|
2737
|
+
return fail3(`${key} entries require title/objective.`, "ULW_LOOP_STEERING_CHILD_INVALID", { key });
|
|
2355
2738
|
parsed.push(next);
|
|
2356
2739
|
}
|
|
2357
2740
|
return parsed;
|
|
@@ -2366,13 +2749,13 @@ function parseSteeringKind(argv) {
|
|
|
2366
2749
|
const value = readValue(argv, "--kind");
|
|
2367
2750
|
if (isKind(value))
|
|
2368
2751
|
return value;
|
|
2369
|
-
return value === undefined ?
|
|
2752
|
+
return value === undefined ? fail3(kindMessage("Missing --kind."), "ULW_LOOP_STEERING_KIND_REQUIRED", { flag: "--kind", expected: ULW_LOOP_STEERING_MUTATION_KINDS, usage: STEERING_KIND_HELP }) : fail3(kindMessage(`Invalid --kind: ${value}.`), "ULW_LOOP_STEERING_KIND_INVALID", { value, expected: ULW_LOOP_STEERING_MUTATION_KINDS, usage: STEERING_KIND_HELP });
|
|
2370
2753
|
}
|
|
2371
2754
|
function parseSteeringSource(argv) {
|
|
2372
2755
|
const value = readValue(argv, "--source");
|
|
2373
2756
|
if (value === undefined)
|
|
2374
2757
|
return "cli";
|
|
2375
|
-
return isSource(value) ? value :
|
|
2758
|
+
return isSource(value) ? value : fail3(`Invalid --source: ${value}.`, "ULW_LOOP_STEERING_SOURCE_INVALID", { value, expected: SOURCES });
|
|
2376
2759
|
}
|
|
2377
2760
|
function child(value) {
|
|
2378
2761
|
if (!isPlain(value))
|
|
@@ -2389,12 +2772,12 @@ async function children(argv, flag, needed) {
|
|
|
2389
2772
|
return [];
|
|
2390
2773
|
const raw = await readJsonInput(input);
|
|
2391
2774
|
if (!Array.isArray(raw))
|
|
2392
|
-
return
|
|
2775
|
+
return fail3(`${flag} must be a JSON array.`, "ULW_LOOP_STEERING_JSON_ARRAY_REQUIRED", { flag });
|
|
2393
2776
|
const parsed = [];
|
|
2394
2777
|
for (const item of raw) {
|
|
2395
2778
|
const next = child(item);
|
|
2396
2779
|
if (next === null)
|
|
2397
|
-
return
|
|
2780
|
+
return fail3(`${flag} entries require title/objective.`, "ULW_LOOP_STEERING_CHILD_INVALID", { flag });
|
|
2398
2781
|
parsed.push(next);
|
|
2399
2782
|
}
|
|
2400
2783
|
return parsed;
|
|
@@ -2402,11 +2785,11 @@ async function children(argv, flag, needed) {
|
|
|
2402
2785
|
async function stringArray2(argv, flag) {
|
|
2403
2786
|
const raw = await readJsonInput(required3(argv, flag));
|
|
2404
2787
|
if (!Array.isArray(raw))
|
|
2405
|
-
return
|
|
2788
|
+
return fail3(`${flag} must be a JSON array.`, "ULW_LOOP_STEERING_JSON_ARRAY_REQUIRED", { flag });
|
|
2406
2789
|
const values = [];
|
|
2407
2790
|
for (const item of raw) {
|
|
2408
2791
|
if (typeof item !== "string")
|
|
2409
|
-
return
|
|
2792
|
+
return fail3(`${flag} entries must be strings.`, "ULW_LOOP_STEERING_STRING_ARRAY_REQUIRED", { flag });
|
|
2410
2793
|
values.push(text2(item, flag) ?? "");
|
|
2411
2794
|
}
|
|
2412
2795
|
return values;
|
|
@@ -2415,10 +2798,10 @@ function model(value) {
|
|
|
2415
2798
|
const trimmed = text2(value, "--user-model");
|
|
2416
2799
|
if (trimmed === undefined)
|
|
2417
2800
|
return;
|
|
2418
|
-
return isModel(trimmed) ? trimmed :
|
|
2801
|
+
return isModel(trimmed) ? trimmed : fail3(`Invalid --user-model: ${trimmed}.`, "ULW_LOOP_STEERING_USER_MODEL_INVALID", { value: trimmed, expected: ULW_LOOP_SUCCESS_CRITERION_USER_MODELS });
|
|
2419
2802
|
}
|
|
2420
2803
|
function neverKind(kind) {
|
|
2421
|
-
return
|
|
2804
|
+
return fail3(`Unsupported steering kind: ${String(kind)}.`, "ULW_LOOP_STEERING_KIND_UNSUPPORTED", { kind });
|
|
2422
2805
|
}
|
|
2423
2806
|
async function parseSteeringProposal(argv) {
|
|
2424
2807
|
const kind = parseSteeringKind(argv);
|
|
@@ -2439,7 +2822,7 @@ async function parseSteeringProposal(argv) {
|
|
|
2439
2822
|
const revisedTitle = readValue(argv, "--title");
|
|
2440
2823
|
const revisedObjective = readValue(argv, "--objective");
|
|
2441
2824
|
if (revisedTitle === undefined && revisedObjective === undefined)
|
|
2442
|
-
return
|
|
2825
|
+
return fail3("revise_pending_wording requires --title or --objective.", "ULW_LOOP_STEERING_UPDATE_REQUIRED", { kind });
|
|
2443
2826
|
return normalizeSteeringProposal({ ...base, goalId, targetGoalId: goalId, ...revisedTitle === undefined ? {} : { revisedTitle }, ...revisedObjective === undefined ? {} : { revisedObjective } });
|
|
2444
2827
|
}
|
|
2445
2828
|
case "revise_criterion": {
|
|
@@ -2449,7 +2832,7 @@ async function parseSteeringProposal(argv) {
|
|
|
2449
2832
|
const expectedEvidence = readValue(argv, "--expected-evidence");
|
|
2450
2833
|
const userModel = model(readValue(argv, "--user-model"));
|
|
2451
2834
|
if (scenario === undefined && expectedEvidence === undefined && userModel === undefined)
|
|
2452
|
-
return
|
|
2835
|
+
return fail3("revise_criterion requires scenario, expected-evidence, or user-model.", "ULW_LOOP_STEERING_UPDATE_REQUIRED", { kind });
|
|
2453
2836
|
return normalizeSteeringProposal({ ...base, goalId, targetGoalId: goalId, criterionId, ...scenario === undefined ? {} : { scenario }, ...expectedEvidence === undefined ? {} : { expectedEvidence }, ...userModel === undefined ? {} : { userModel } });
|
|
2454
2837
|
}
|
|
2455
2838
|
case "annotate_ledger":
|
|
@@ -2499,10 +2882,10 @@ async function parseSteeringProposals(argv) {
|
|
|
2499
2882
|
if (input === undefined)
|
|
2500
2883
|
return [await parseSteeringProposal(argv)];
|
|
2501
2884
|
if (readValue(argv, "--kind") !== undefined)
|
|
2502
|
-
return
|
|
2885
|
+
return fail3("--kind and --proposals-json are mutually exclusive.", "ULW_LOOP_STEERING_BATCH_CONFLICT", { flags: ["--kind", "--proposals-json"] });
|
|
2503
2886
|
const raw = await readJsonInput(input);
|
|
2504
2887
|
if (!Array.isArray(raw) || raw.length === 0)
|
|
2505
|
-
return
|
|
2888
|
+
return fail3("--proposals-json must be a non-empty JSON array.", "ULW_LOOP_STEERING_BATCH_ARRAY_REQUIRED", { flag: "--proposals-json" });
|
|
2506
2889
|
const proposals = [];
|
|
2507
2890
|
for (const item of raw)
|
|
2508
2891
|
proposals.push(normalizeSteeringProposal(proposalFromObject(item)));
|
|
@@ -2510,13 +2893,13 @@ async function parseSteeringProposals(argv) {
|
|
|
2510
2893
|
}
|
|
2511
2894
|
function proposalFromObject(value) {
|
|
2512
2895
|
if (!isPlain(value))
|
|
2513
|
-
return
|
|
2896
|
+
return fail3("--proposals-json entries must be objects.", "ULW_LOOP_STEERING_BATCH_ITEM_INVALID", { flag: "--proposals-json" });
|
|
2514
2897
|
const kind = readObject(value, "kind");
|
|
2515
2898
|
const source = readObject(value, "source") ?? "cli";
|
|
2516
2899
|
if (!isProposalKind(kind))
|
|
2517
|
-
return
|
|
2900
|
+
return fail3(`Invalid batch steering kind: ${String(kind)}.`, "ULW_LOOP_STEERING_KIND_INVALID", { value: kind });
|
|
2518
2901
|
if (!isProposalSource(source))
|
|
2519
|
-
return
|
|
2902
|
+
return fail3(`Invalid batch steering source: ${String(source)}.`, "ULW_LOOP_STEERING_SOURCE_INVALID", { value: source });
|
|
2520
2903
|
let proposal = { kind, source, evidence: objectText(value, "evidence") ?? "", rationale: objectText(value, "rationale") ?? "" };
|
|
2521
2904
|
const goalId = objectText(value, "goalId");
|
|
2522
2905
|
const targetGoalId = objectText(value, "targetGoalId");
|
|
@@ -2602,12 +2985,13 @@ function nextGoalId(plan) {
|
|
|
2602
2985
|
}
|
|
2603
2986
|
function appendBlockerGoal(plan, args, now) {
|
|
2604
2987
|
const index = plan.goals.length;
|
|
2988
|
+
const id = nextGoalId(plan);
|
|
2605
2989
|
const goal = {
|
|
2606
|
-
id
|
|
2990
|
+
id,
|
|
2607
2991
|
title: args.title,
|
|
2608
2992
|
objective: args.objective,
|
|
2609
2993
|
status: "pending",
|
|
2610
|
-
successCriteria: seedDefaultSuccessCriteria(index, args.objective),
|
|
2994
|
+
successCriteria: seedDefaultSuccessCriteria(index, args.objective, { goalId: id }),
|
|
2611
2995
|
attempt: 0,
|
|
2612
2996
|
createdAt: now,
|
|
2613
2997
|
updatedAt: now
|
|
@@ -2628,10 +3012,12 @@ async function recordFinalReviewBlockers(repoRoot, args, scope) {
|
|
|
2628
3012
|
const snapshot = await readCodexGoalSnapshotInput(args.codexGoalJson, repoRoot);
|
|
2629
3013
|
const reconciliation = reconcileCodexGoalSnapshot(snapshot, {
|
|
2630
3014
|
expectedObjective: expectedCodexObjective(plan, goal),
|
|
2631
|
-
acceptedObjectives: compatibleCodexObjectives(plan)
|
|
3015
|
+
acceptedObjectives: compatibleCodexObjectives(plan),
|
|
3016
|
+
acknowledgedObjectives: acknowledgedDriverObjectives(plan)
|
|
2632
3017
|
});
|
|
2633
3018
|
if (!reconciliation.ok)
|
|
2634
3019
|
throw new CodexGoalSnapshotError(formatCodexGoalReconciliation(reconciliation));
|
|
3020
|
+
acknowledgeDriverObjective(plan, reconciliation.unacknowledgedObjective);
|
|
2635
3021
|
const now = iso();
|
|
2636
3022
|
for (const field of BLOCKER_FIELDS)
|
|
2637
3023
|
Reflect.deleteProperty(goal, field);
|
|
@@ -2649,46 +3035,44 @@ async function recordFinalReviewBlockers(repoRoot, args, scope) {
|
|
|
2649
3035
|
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
3036
|
Reflect.set(summaryEntry, "kind", "blocker_recorded");
|
|
2651
3037
|
const ledgerEntries = [blockedEntry, addedEntry, summaryEntry];
|
|
2652
|
-
await
|
|
2653
|
-
for (const entry of ledgerEntries)
|
|
2654
|
-
await appendLedger(repoRoot, entry, scope);
|
|
3038
|
+
await commit(repoRoot, scope, { plan, entries: ledgerEntries });
|
|
2655
3039
|
return {
|
|
2656
3040
|
plan,
|
|
2657
3041
|
blockedGoal: goal,
|
|
2658
3042
|
newGoal,
|
|
2659
3043
|
ledgerEntries,
|
|
2660
|
-
nextActions: reconciliation.
|
|
2661
|
-
warnings: reconciliation.warnings
|
|
3044
|
+
nextActions: reconciliation.nextActions,
|
|
3045
|
+
warnings: reconciliation.warnings
|
|
2662
3046
|
};
|
|
2663
3047
|
});
|
|
2664
3048
|
}
|
|
2665
3049
|
|
|
2666
3050
|
// components/ulw-loop/src/status-next-actions.ts
|
|
2667
|
-
function statusNextActions(plan) {
|
|
3051
|
+
function statusNextActions(plan, surface = "lazycodex") {
|
|
2668
3052
|
const actions = [];
|
|
2669
3053
|
const active = plan.goals.find((goal) => goal.id === plan.activeGoalId);
|
|
2670
3054
|
if (plan.goals.length === 0)
|
|
2671
|
-
actions.push(`No goals yet: bootstrap with \`${
|
|
3055
|
+
actions.push(`No goals yet: bootstrap with \`${createGoalsAction(surface)}\`.`);
|
|
2672
3056
|
else if (active !== undefined)
|
|
2673
|
-
actions.push(...activeGoalActions(plan, active));
|
|
3057
|
+
actions.push(...activeGoalActions(plan, active, surface));
|
|
2674
3058
|
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>'\`.`);
|
|
3059
|
+
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
3060
|
if (plan.evidenceLayoutVersion !== 2 || active === undefined)
|
|
2677
3061
|
actions.push("plan is evidence-layout v1; artifacts go under .omo/evidence/");
|
|
2678
3062
|
return actions;
|
|
2679
3063
|
}
|
|
2680
|
-
function activeGoalActions(plan, active) {
|
|
3064
|
+
function activeGoalActions(plan, active, surface) {
|
|
2681
3065
|
const unresolved = active.successCriteria.filter((criterion) => criterion.status !== "pass");
|
|
2682
3066
|
if (unresolved.length > 0)
|
|
2683
3067
|
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>"\`.`
|
|
3068
|
+
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
3069
|
];
|
|
2686
3070
|
if (hasAllCriteriaPass(active) && isFinalRunCompletionCandidate(plan, active))
|
|
2687
3071
|
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.`
|
|
3072
|
+
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
3073
|
];
|
|
2690
3074
|
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.`
|
|
3075
|
+
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
3076
|
];
|
|
2693
3077
|
}
|
|
2694
3078
|
|
|
@@ -2729,11 +3113,11 @@ function nextId(plan, offset) {
|
|
|
2729
3113
|
}, 0);
|
|
2730
3114
|
return `G${String(max + offset).padStart(3, "0")}`;
|
|
2731
3115
|
}
|
|
2732
|
-
function makeGoal2(plan, childGoal, evidence, now, offset) {
|
|
3116
|
+
function makeGoal2(plan, childGoal, evidence, now, offset, surface = "lazycodex") {
|
|
2733
3117
|
const id = nextId(plan, offset);
|
|
2734
3118
|
const digits = /^G(\d+)/u.exec(id)?.[1];
|
|
2735
3119
|
const goalIndex = digits === undefined ? plan.goals.length + offset - 1 : Number(digits) - 1;
|
|
2736
|
-
return { id, title: childGoal.title, objective: childGoal.objective, status: "pending", successCriteria: seedDefaultSuccessCriteria(goalIndex, childGoal.objective), attempt: 0, createdAt: now, updatedAt: now, evidence };
|
|
3120
|
+
return { id, title: childGoal.title, objective: childGoal.objective, status: "pending", successCriteria: seedDefaultSuccessCriteria(goalIndex, childGoal.objective, { goalId: id, surface }), attempt: 0, createdAt: now, updatedAt: now, evidence };
|
|
2737
3121
|
}
|
|
2738
3122
|
function reviseWording(plan, proposal, now) {
|
|
2739
3123
|
const target = goal(plan, targets(proposal)[0]);
|
|
@@ -2745,11 +3129,11 @@ function reviseWording(plan, proposal, now) {
|
|
|
2745
3129
|
target.steeringRationale = proposal.rationale;
|
|
2746
3130
|
target.updatedAt = now;
|
|
2747
3131
|
}
|
|
2748
|
-
function splitOrBlock(plan, proposal, now) {
|
|
3132
|
+
function splitOrBlock(plan, proposal, now, surface = "lazycodex") {
|
|
2749
3133
|
const target = goal(plan, targets(proposal)[0]);
|
|
2750
3134
|
if (target === undefined)
|
|
2751
3135
|
return;
|
|
2752
|
-
const replacements = children2(proposal).map((item, index) => makeGoal2(plan, item, proposal.evidence, now, index + 1));
|
|
3136
|
+
const replacements = children2(proposal).map((item, index) => makeGoal2(plan, item, proposal.evidence, now, index + 1, surface));
|
|
2753
3137
|
target.steeringEvidence = proposal.evidence;
|
|
2754
3138
|
target.steeringRationale = proposal.rationale;
|
|
2755
3139
|
target.updatedAt = now;
|
|
@@ -2805,7 +3189,7 @@ function changedGoalIdsBetween(before, after) {
|
|
|
2805
3189
|
|
|
2806
3190
|
// components/ulw-loop/src/steering.ts
|
|
2807
3191
|
var SOURCES2 = ["user_prompt_submit", "finding", "cli"];
|
|
2808
|
-
var PROTECTED = new Set(["aggregateCompletion", "codexObjective", "codexObjectiveAliases", "originalConstraints", "qualityGate", "status", "completedAt", "completionStatus"]);
|
|
3192
|
+
var PROTECTED = new Set(["aggregateCompletion", "codexObjective", "codexObjectiveAliases", "acknowledgedDriverObjectives", "originalConstraints", "qualityGate", "status", "completedAt", "completionStatus"]);
|
|
2809
3193
|
var isObject2 = (value) => typeof value === "object" && value !== null;
|
|
2810
3194
|
var isPlain2 = (value) => isObject2(value) && !Array.isArray(value);
|
|
2811
3195
|
var read3 = (value, key) => Object.entries(value).find(([name]) => name === key)?.[1];
|
|
@@ -2963,13 +3347,13 @@ function validateCriterion(plan, proposal, reasons) {
|
|
|
2963
3347
|
if (model !== undefined && !isModel3(model))
|
|
2964
3348
|
reasons.push("invalid userModel");
|
|
2965
3349
|
}
|
|
2966
|
-
function applySteeringMutation(plan, proposal, audit) {
|
|
3350
|
+
function applySteeringMutation(plan, proposal, audit, surface = "lazycodex") {
|
|
2967
3351
|
const next = structuredClone(plan);
|
|
2968
3352
|
if (!audit.invariant.accepted)
|
|
2969
3353
|
return next;
|
|
2970
3354
|
const now = proposal.now?.toISOString() ?? iso();
|
|
2971
3355
|
if (proposal.kind === "add_subgoal")
|
|
2972
|
-
next.goals.push(makeGoal2(next, { title: proposal.title ?? "", objective: proposal.objective ?? "" }, proposal.evidence, now, 1));
|
|
3356
|
+
next.goals.push(makeGoal2(next, { title: proposal.title ?? "", objective: proposal.objective ?? "" }, proposal.evidence, now, 1, surface));
|
|
2973
3357
|
if (proposal.kind === "reorder_pending") {
|
|
2974
3358
|
const order = pendingOrder(proposal);
|
|
2975
3359
|
next.goals = [...order.map((id) => goal2(next, id)).filter((item) => item !== undefined), ...next.goals.filter((item) => !order.includes(item.id))];
|
|
@@ -2977,7 +3361,7 @@ function applySteeringMutation(plan, proposal, audit) {
|
|
|
2977
3361
|
if (proposal.kind === "revise_pending_wording")
|
|
2978
3362
|
reviseWording(next, proposal, now);
|
|
2979
3363
|
if (proposal.kind === "split_subgoal" || proposal.kind === "mark_blocked_superseded")
|
|
2980
|
-
splitOrBlock(next, proposal, now);
|
|
3364
|
+
splitOrBlock(next, proposal, now, surface);
|
|
2981
3365
|
if (proposal.kind === "revise_criterion")
|
|
2982
3366
|
reviseCriterion(next, proposal, now);
|
|
2983
3367
|
if (proposal.kind !== "annotate_ledger")
|
|
@@ -3000,7 +3384,7 @@ function parseUlwLoopSteeringDirective(text) {
|
|
|
3000
3384
|
throw error;
|
|
3001
3385
|
}
|
|
3002
3386
|
}
|
|
3003
|
-
async function steerUlwLoop(repoRoot, proposal, scope) {
|
|
3387
|
+
async function steerUlwLoop(repoRoot, proposal, scope, surface = "lazycodex") {
|
|
3004
3388
|
return withUlwLoopMutationLock(repoRoot, scope, async () => {
|
|
3005
3389
|
const plan = await readUlwLoopPlan(repoRoot, scope);
|
|
3006
3390
|
const key = proposal.idempotencyKey ?? proposal.promptSignature;
|
|
@@ -3011,7 +3395,7 @@ async function steerUlwLoop(repoRoot, proposal, scope) {
|
|
|
3011
3395
|
}
|
|
3012
3396
|
const audit = validateUlwLoopSteeringProposal(plan, proposal);
|
|
3013
3397
|
const accepted = audit.invariant.accepted;
|
|
3014
|
-
const next = accepted ? applySteeringMutation(plan, proposal, audit) : plan;
|
|
3398
|
+
const next = accepted ? applySteeringMutation(plan, proposal, audit, surface) : plan;
|
|
3015
3399
|
const finalAudit = { ...audit };
|
|
3016
3400
|
if (accepted) {
|
|
3017
3401
|
const changed = changedGoalIdsBetween(plan, next);
|
|
@@ -3020,11 +3404,10 @@ async function steerUlwLoop(repoRoot, proposal, scope) {
|
|
|
3020
3404
|
}
|
|
3021
3405
|
const at = proposal.now?.toISOString() ?? iso();
|
|
3022
3406
|
const batchEntry = accepted ? batchUpdateLedgerEntry(plan, next, at) : null;
|
|
3023
|
-
|
|
3024
|
-
await writePlan(repoRoot, next, scope);
|
|
3025
|
-
await appendLedger(repoRoot, ledgerEntry(proposal, finalAudit, at), scope);
|
|
3407
|
+
const entries = [ledgerEntry(proposal, finalAudit, at)];
|
|
3026
3408
|
if (batchEntry !== null)
|
|
3027
|
-
|
|
3409
|
+
entries.push(batchEntry);
|
|
3410
|
+
await commit(repoRoot, scope, { plan: next, entries });
|
|
3028
3411
|
return { plan: next, accepted, audit: finalAudit, rejectedReasons: audit.invariant.rejectedReasons, deduped: false };
|
|
3029
3412
|
});
|
|
3030
3413
|
}
|
|
@@ -3041,20 +3424,119 @@ function ledgerEntry(proposal, audit, at) {
|
|
|
3041
3424
|
}
|
|
3042
3425
|
|
|
3043
3426
|
// components/ulw-loop/src/sdk/manifest.ts
|
|
3427
|
+
var CRITERION_INPUT = '{ scenario: string, expectedEvidence: string, userModel?: "happy" | "edge" | "regression" | "adversarial", essential?: boolean }';
|
|
3428
|
+
var STEER_KINDS = '"add_subgoal" | "split_subgoal" | "reorder_pending" | "revise_pending_wording" | "revise_criterion" | "annotate_ledger" | "mark_blocked_superseded"';
|
|
3044
3429
|
var ULW_LOOP_MANIFEST = {
|
|
3045
3430
|
version: 1,
|
|
3046
3431
|
name: "ulw-loop",
|
|
3047
3432
|
operations: [
|
|
3048
|
-
{
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
{
|
|
3056
|
-
|
|
3057
|
-
|
|
3433
|
+
{
|
|
3434
|
+
name: "help",
|
|
3435
|
+
method: "help",
|
|
3436
|
+
mutating: false,
|
|
3437
|
+
description: "Return this manifest: every operation with its method name, argument fields, and what it does.",
|
|
3438
|
+
args: {}
|
|
3439
|
+
},
|
|
3440
|
+
{
|
|
3441
|
+
name: "create-goals",
|
|
3442
|
+
method: "createGoals",
|
|
3443
|
+
mutating: true,
|
|
3444
|
+
description: "Create the session plan from a brief; one goal per bullet, each seeded with placeholder criteria until revised.",
|
|
3445
|
+
args: {
|
|
3446
|
+
brief: "string",
|
|
3447
|
+
codexGoalMode: '"aggregate" | "per_story"?',
|
|
3448
|
+
force: "boolean?",
|
|
3449
|
+
validationBatchesJson: "string?"
|
|
3450
|
+
}
|
|
3451
|
+
},
|
|
3452
|
+
{
|
|
3453
|
+
name: "status",
|
|
3454
|
+
method: "status",
|
|
3455
|
+
mutating: false,
|
|
3456
|
+
description: "Read the plan, its summary, structured nextActions, the stable evidenceRoot, and the active goal's currentAttemptDir.",
|
|
3457
|
+
args: {}
|
|
3458
|
+
},
|
|
3459
|
+
{
|
|
3460
|
+
name: "complete-goals",
|
|
3461
|
+
method: "completeGoals",
|
|
3462
|
+
mutating: true,
|
|
3463
|
+
description: "Acquire the next eligible goal or resume the in-progress one; returns { done: true } once the aggregate is complete.",
|
|
3464
|
+
args: { retryFailed: "boolean?" }
|
|
3465
|
+
},
|
|
3466
|
+
{
|
|
3467
|
+
name: "checkpoint",
|
|
3468
|
+
method: "checkpoint",
|
|
3469
|
+
mutating: true,
|
|
3470
|
+
description: "Close the goal as complete, failed, or blocked with evidence; the final goal also needs the quality gate. printTemplate returns that gate's template instead.",
|
|
3471
|
+
args: {
|
|
3472
|
+
goalId: "string",
|
|
3473
|
+
status: '"complete" | "failed" | "blocked"',
|
|
3474
|
+
evidence: "string",
|
|
3475
|
+
codexGoalJson: "string?",
|
|
3476
|
+
qualityGateJson: "string?",
|
|
3477
|
+
printTemplate: "true? (with goalId? only)"
|
|
3478
|
+
}
|
|
3479
|
+
},
|
|
3480
|
+
{
|
|
3481
|
+
name: "steer",
|
|
3482
|
+
method: "steer",
|
|
3483
|
+
mutating: true,
|
|
3484
|
+
description: "Propose an evidence-backed plan mutation; revise_criterion replaces a criterion's scenario, expectedEvidence, or userModel.",
|
|
3485
|
+
args: {
|
|
3486
|
+
kind: STEER_KINDS,
|
|
3487
|
+
source: '"finding" | "user_prompt_submit" | "cli"',
|
|
3488
|
+
evidence: "string",
|
|
3489
|
+
rationale: "string",
|
|
3490
|
+
goalId: "string?",
|
|
3491
|
+
criterionId: "string? (revise_criterion)",
|
|
3492
|
+
scenario: "string? (revise_criterion)",
|
|
3493
|
+
expectedEvidence: "string? (revise_criterion)",
|
|
3494
|
+
userModel: '"happy" | "edge" | "regression" | "adversarial"? (revise_criterion)',
|
|
3495
|
+
title: "string? (add_subgoal)",
|
|
3496
|
+
objective: "string? (add_subgoal)",
|
|
3497
|
+
childGoals: "{ title: string, objective: string }[]? (split_subgoal)",
|
|
3498
|
+
revisedTitle: "string? (revise_pending_wording)",
|
|
3499
|
+
revisedObjective: "string? (revise_pending_wording)",
|
|
3500
|
+
pendingOrder: "string[]? (reorder_pending)",
|
|
3501
|
+
blockedReason: "string? (mark_blocked_superseded)",
|
|
3502
|
+
idempotencyKey: "string?"
|
|
3503
|
+
}
|
|
3504
|
+
},
|
|
3505
|
+
{
|
|
3506
|
+
name: "add-goal",
|
|
3507
|
+
method: "addGoal",
|
|
3508
|
+
mutating: true,
|
|
3509
|
+
description: "Append a goal; pass successCriteria to define its criteria in the same call, otherwise placeholders name the revise call.",
|
|
3510
|
+
args: { title: "string", objective: "string", successCriteria: `${CRITERION_INPUT}[]?` }
|
|
3511
|
+
},
|
|
3512
|
+
{
|
|
3513
|
+
name: "criteria",
|
|
3514
|
+
method: "criteria",
|
|
3515
|
+
mutating: false,
|
|
3516
|
+
description: "List one goal's success criteria with their status and captured evidence.",
|
|
3517
|
+
args: { goalId: "string" }
|
|
3518
|
+
},
|
|
3519
|
+
{
|
|
3520
|
+
name: "record-evidence",
|
|
3521
|
+
method: "recordEvidence",
|
|
3522
|
+
mutating: true,
|
|
3523
|
+
description: "Record a criterion's pass, fail, or blocked evidence; artifacts must exist (resolved against the session cwd) and are stored with the criterion and the ledger entry.",
|
|
3524
|
+
args: {
|
|
3525
|
+
goalId: "string",
|
|
3526
|
+
criterionId: "string",
|
|
3527
|
+
status: '"pass" | "fail" | "blocked"',
|
|
3528
|
+
evidence: "string",
|
|
3529
|
+
notes: "string?",
|
|
3530
|
+
artifacts: "string[]?"
|
|
3531
|
+
}
|
|
3532
|
+
},
|
|
3533
|
+
{
|
|
3534
|
+
name: "record-review-blockers",
|
|
3535
|
+
method: "recordReviewBlockers",
|
|
3536
|
+
mutating: true,
|
|
3537
|
+
description: "Mark the final goal review_blocked and append the blocker goal the reviewer verdict names.",
|
|
3538
|
+
args: { goalId: "string", title: "string", objective: "string", evidence: "string", codexGoalJson: "string?" }
|
|
3539
|
+
}
|
|
3058
3540
|
]
|
|
3059
3541
|
};
|
|
3060
3542
|
|
|
@@ -3071,8 +3553,12 @@ function validateCodexGoalJson(raw) {
|
|
|
3071
3553
|
function validateContext(context) {
|
|
3072
3554
|
if (!context.cwd.trim())
|
|
3073
3555
|
throw new UlwLoopError("cwd is required.", "ULW_LOOP_CWD_REQUIRED");
|
|
3074
|
-
|
|
3556
|
+
const sessionId = context.sessionId.trim();
|
|
3557
|
+
if (!sessionId)
|
|
3075
3558
|
throw new UlwLoopError("ULW_LOOP_SESSION_ID_REQUIRED: sessionId is required.", "ULW_LOOP_SESSION_ID_REQUIRED");
|
|
3559
|
+
const normalizedSessionId = normalizeUlwLoopSessionId(sessionId);
|
|
3560
|
+
if (normalizedSessionId === null || /(?:^|[\\/])\.\.(?:[\\/]|$)/.test(sessionId))
|
|
3561
|
+
throw new UlwLoopError("ULW_LOOP_SESSION_ID_INVALID: sessionId normalizes to null.", "ULW_LOOP_SESSION_ID_INVALID");
|
|
3076
3562
|
if (context.surface !== "omo-senpi" && context.surface !== "lazycodex")
|
|
3077
3563
|
throw new UlwLoopError("surface must be omo-senpi or lazycodex.", "ULW_LOOP_SURFACE_INVALID");
|
|
3078
3564
|
}
|
|
@@ -3092,10 +3578,11 @@ function isKnownRequest(request) {
|
|
|
3092
3578
|
function unreachable(value) {
|
|
3093
3579
|
throw new UlwLoopError(`Unhandled operation: ${String(value)}`, "ULW_LOOP_OPERATION_UNHANDLED");
|
|
3094
3580
|
}
|
|
3095
|
-
function
|
|
3096
|
-
if (!(
|
|
3581
|
+
function stringsFrom(result, key) {
|
|
3582
|
+
if (!(key in result))
|
|
3097
3583
|
return [];
|
|
3098
|
-
|
|
3584
|
+
const value = Object.entries(result).find(([name]) => name === key)?.[1];
|
|
3585
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === "string").slice(0, 8) : [];
|
|
3099
3586
|
}
|
|
3100
3587
|
function checkpointWithValidatedSnapshot(context, scope, args) {
|
|
3101
3588
|
validateCodexGoalJson(args.codexGoalJson);
|
|
@@ -3112,9 +3599,18 @@ function createAgentToolkit(context, deps = {}) {
|
|
|
3112
3599
|
const invoke = async (operation, fn) => {
|
|
3113
3600
|
try {
|
|
3114
3601
|
const result = await fn();
|
|
3115
|
-
const nextActions = typeof result === "object" && result !== null ?
|
|
3116
|
-
|
|
3602
|
+
const nextActions = typeof result === "object" && result !== null ? stringsFrom(result, "nextActions") : [];
|
|
3603
|
+
const warnings = typeof result === "object" && result !== null ? stringsFrom(result, "warnings") : [];
|
|
3604
|
+
return await notify(operation, {
|
|
3605
|
+
ok: true,
|
|
3606
|
+
operation,
|
|
3607
|
+
result,
|
|
3608
|
+
nextActions,
|
|
3609
|
+
...warnings.length === 0 ? {} : { warnings }
|
|
3610
|
+
});
|
|
3117
3611
|
} catch (error) {
|
|
3612
|
+
if (error instanceof UlwLoopError && error.code === "ULW_LOOP_PLAN_MISSING" && context.surface === "omo-senpi")
|
|
3613
|
+
return notify(operation, failure(operation, planMissingError(ulwLoopGoalsRelativePath(scope), listUlwLoopSessionIds(context.cwd), context.surface)));
|
|
3118
3614
|
const response = caught(operation, error instanceof Error ? error : new Error("ULW_LOOP_ERROR"));
|
|
3119
3615
|
return notify(operation, response);
|
|
3120
3616
|
}
|
|
@@ -3149,21 +3645,22 @@ function createAgentToolkit(context, deps = {}) {
|
|
|
3149
3645
|
}
|
|
3150
3646
|
},
|
|
3151
3647
|
help: () => invoke("help", async () => ULW_LOOP_MANIFEST),
|
|
3152
|
-
createGoals: (args) => invoke("create-goals", () => createUlwLoopPlan(context.cwd, args, scope)),
|
|
3648
|
+
createGoals: (args) => invoke("create-goals", () => createUlwLoopPlan(context.cwd, args, scope, context.surface)),
|
|
3153
3649
|
status: () => invoke("status", async () => {
|
|
3154
3650
|
const plan = await readUlwLoopPlan(context.cwd, scope);
|
|
3155
3651
|
const active = plan.goals.find((goal) => goal.id === plan.activeGoalId);
|
|
3156
3652
|
return {
|
|
3157
3653
|
plan,
|
|
3158
3654
|
summary: summarizeUlwLoopPlan(plan),
|
|
3159
|
-
nextActions: statusNextActions(plan),
|
|
3655
|
+
nextActions: statusNextActions(plan, context.surface),
|
|
3656
|
+
evidenceRoot: ulwLoopEvidenceRoot(scope),
|
|
3160
3657
|
...active === undefined || plan.evidenceLayoutVersion !== 2 ? {} : { currentAttemptDir: ulwLoopAttemptEvidenceDir(active.id, active.attempt, scope) }
|
|
3161
3658
|
};
|
|
3162
3659
|
}),
|
|
3163
3660
|
completeGoals: (args = {}) => invoke("complete-goals", () => startNextUlwLoop(context.cwd, args, scope)),
|
|
3164
3661
|
checkpoint: (args) => invoke("checkpoint", () => args.printTemplate === true ? checkpointTemplate(context.cwd, scope, args.goalId, { surface: context.surface }) : checkpointWithValidatedSnapshot(context, scope, args)),
|
|
3165
|
-
steer: (args) => invoke("steer", () => steerUlwLoop(context.cwd, args, scope)),
|
|
3166
|
-
addGoal: (args) => invoke("add-goal", () => addUlwLoopGoal(context.cwd, args, scope)),
|
|
3662
|
+
steer: (args) => invoke("steer", () => steerUlwLoop(context.cwd, args, scope, context.surface)),
|
|
3663
|
+
addGoal: (args) => invoke("add-goal", () => addUlwLoopGoal(context.cwd, args, scope, context.surface)),
|
|
3167
3664
|
criteria: (args) => invoke("criteria", async () => {
|
|
3168
3665
|
const plan = await readUlwLoopPlan(context.cwd, scope);
|
|
3169
3666
|
const goal = plan.goals.find((candidate) => candidate.id === args.goalId);
|
|
@@ -3184,7 +3681,7 @@ async function steerUlwLoopBatch(repoRoot, proposals, scope) {
|
|
|
3184
3681
|
const failed = prepared.results.find((item) => !item.accepted);
|
|
3185
3682
|
if (failed !== undefined) {
|
|
3186
3683
|
const entry = rejectedLedgerEntry(prepared.results);
|
|
3187
|
-
await
|
|
3684
|
+
await commit(repoRoot, scope, { plan, entries: [entry] });
|
|
3188
3685
|
return rejected(plan, prepared.results, failed.rejectedReasons);
|
|
3189
3686
|
}
|
|
3190
3687
|
let next = plan;
|
|
@@ -3193,10 +3690,9 @@ async function steerUlwLoopBatch(repoRoot, proposals, scope) {
|
|
|
3193
3690
|
next = item.prepared.next;
|
|
3194
3691
|
const fresh = prepared.items.filter((item) => item.kind === "fresh");
|
|
3195
3692
|
if (fresh.length > 0) {
|
|
3196
|
-
await writePlan(repoRoot, next, scope);
|
|
3197
3693
|
const entries = fresh.map((item) => ledgerEntry2(item.prepared.proposal, item.prepared.audit, item.prepared.proposal.now?.toISOString() ?? iso()));
|
|
3198
3694
|
const batchEntry = batchUpdateLedgerEntry(plan, next, iso());
|
|
3199
|
-
await
|
|
3695
|
+
await commit(repoRoot, scope, { plan: next, entries: batchEntry === null ? entries : [...entries, batchEntry] });
|
|
3200
3696
|
}
|
|
3201
3697
|
return { plan: next, accepted: true, results: prepared.results, rejectedReasons: [] };
|
|
3202
3698
|
});
|
|
@@ -3260,7 +3756,7 @@ function ledgerEntry2(proposal, audit, at) {
|
|
|
3260
3756
|
// components/ulw-loop/src/cli-subcommands.ts
|
|
3261
3757
|
async function createGoals(repoRoot, argv, json, scope) {
|
|
3262
3758
|
const briefFile = readValue(argv, "--brief-file");
|
|
3263
|
-
const brief = readValue(argv, "--brief") ?? (briefFile === undefined ? undefined : await
|
|
3759
|
+
const brief = readValue(argv, "--brief") ?? (briefFile === undefined ? undefined : await readFile4(briefFile, "utf8")) ?? (hasFlag(argv, "--from-stdin") ? await readStdin() : undefined) ?? positionalText(argv);
|
|
3264
3760
|
if (!brief.trim()) {
|
|
3265
3761
|
throw new UlwLoopError("Missing brief text. Pass --brief, --brief-file, --from-stdin, or positional text.", "ULW_LOOP_BRIEF_REQUIRED");
|
|
3266
3762
|
}
|
|
@@ -3528,10 +4024,10 @@ function commandScope(repoRoot, argv) {
|
|
|
3528
4024
|
}
|
|
3529
4025
|
|
|
3530
4026
|
// components/ulw-loop/src/ultrawork-directive.ts
|
|
3531
|
-
import { readFileSync as
|
|
4027
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
3532
4028
|
|
|
3533
4029
|
// components/ulw-loop/src/ultrawork-skill-pointer.ts
|
|
3534
|
-
import { existsSync as
|
|
4030
|
+
import { existsSync as existsSync6, readFileSync as readFileSync4 } from "node:fs";
|
|
3535
4031
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
3536
4032
|
var ULTRAWORK_SKILL_POINTER_TEMPLATE = `<ultrawork-mode>
|
|
3537
4033
|
ULTRAWORK MODE IS ACTIVE FOR THIS TASK.
|
|
@@ -3563,7 +4059,7 @@ Do not start the requested work until all three steps are complete.
|
|
|
3563
4059
|
`;
|
|
3564
4060
|
var ULTRAWORK_SKILL_PATH_PLACEHOLDER = "{{ULTRAWORK_SKILL_PATH}}";
|
|
3565
4061
|
var ULTRAWORK_SKILL_FILE_URL = new URL("../../../skills/ultrawork/SKILL.md", import.meta.url);
|
|
3566
|
-
var ULTRAWORK_DIRECTIVE =
|
|
4062
|
+
var ULTRAWORK_DIRECTIVE = readFileSync4(new URL("../directive.md", import.meta.url), "utf8");
|
|
3567
4063
|
function resolveUltraworkSkillFilePath() {
|
|
3568
4064
|
return fileURLToPath2(ULTRAWORK_SKILL_FILE_URL);
|
|
3569
4065
|
}
|
|
@@ -3572,7 +4068,7 @@ function buildUltraworkSkillPointer(skillFilePath) {
|
|
|
3572
4068
|
}
|
|
3573
4069
|
function buildUltraworkAdditionalContext(options = {}) {
|
|
3574
4070
|
const skillFilePath = options.skillFilePath === undefined ? resolveUltraworkSkillFilePath() : options.skillFilePath;
|
|
3575
|
-
if (skillFilePath !== null &&
|
|
4071
|
+
if (skillFilePath !== null && existsSync6(skillFilePath)) {
|
|
3576
4072
|
return buildUltraworkSkillPointer(skillFilePath);
|
|
3577
4073
|
}
|
|
3578
4074
|
return ULTRAWORK_DIRECTIVE;
|
|
@@ -3626,7 +4122,7 @@ function hasUltraworkDirectiveAlreadyInTranscript(transcriptPath) {
|
|
|
3626
4122
|
return false;
|
|
3627
4123
|
}
|
|
3628
4124
|
function readTranscriptTail(transcriptPath) {
|
|
3629
|
-
const rawTranscript =
|
|
4125
|
+
const rawTranscript = readFileSync5(transcriptPath);
|
|
3630
4126
|
return rawTranscript.subarray(Math.max(0, rawTranscript.byteLength - TRANSCRIPT_SEARCH_BYTES)).toString("utf8");
|
|
3631
4127
|
}
|
|
3632
4128
|
function isUltraworkPrompt(prompt) {
|
|
@@ -3819,9 +4315,64 @@ function readAll(stdin) {
|
|
|
3819
4315
|
}
|
|
3820
4316
|
|
|
3821
4317
|
// components/ulw-loop/src/spawn-guard.ts
|
|
4318
|
+
import { mkdirSync as mkdirSync3 } from "node:fs";
|
|
4319
|
+
import { join as join7 } from "node:path";
|
|
4320
|
+
|
|
4321
|
+
// components/ulw-loop/src/spawn-budget-io.ts
|
|
3822
4322
|
import { randomBytes } from "node:crypto";
|
|
3823
|
-
import { existsSync as
|
|
3824
|
-
import { dirname as dirname3, join as
|
|
4323
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6, renameSync as renameSync2, statSync as statSync3, writeFileSync as writeFileSync2 } from "node:fs";
|
|
4324
|
+
import { dirname as dirname3, join as join6 } from "node:path";
|
|
4325
|
+
function readAdmissionBreaker(sessionId) {
|
|
4326
|
+
const dataDir = process.env["PLUGIN_DATA"];
|
|
4327
|
+
if (typeof dataDir !== "string")
|
|
4328
|
+
return null;
|
|
4329
|
+
try {
|
|
4330
|
+
const value = JSON.parse(readFileSync6(join6(dataDir, "spawn-breaker", `${sessionId}.json`), "utf8"));
|
|
4331
|
+
return typeof value === "object" && value !== null && "reason" in value && typeof value.reason === "string" ? value.reason : "capacity limit";
|
|
4332
|
+
} catch {
|
|
4333
|
+
return null;
|
|
4334
|
+
}
|
|
4335
|
+
}
|
|
4336
|
+
function atomicWriteJson(targetPath, data) {
|
|
4337
|
+
const tmp = join6(dirname3(targetPath), `.tmp-${randomBytes(6).toString("hex")}`);
|
|
4338
|
+
writeFileSync2(tmp, JSON.stringify(data));
|
|
4339
|
+
renameSync2(tmp, targetPath);
|
|
4340
|
+
}
|
|
4341
|
+
function isNonEmptyFile(path) {
|
|
4342
|
+
try {
|
|
4343
|
+
return existsSync7(path) && statSync3(path).size > 0;
|
|
4344
|
+
} catch (error) {
|
|
4345
|
+
if (error instanceof Error)
|
|
4346
|
+
return false;
|
|
4347
|
+
throw error;
|
|
4348
|
+
}
|
|
4349
|
+
}
|
|
4350
|
+
function readCount(counterPath) {
|
|
4351
|
+
try {
|
|
4352
|
+
const parsed = JSON.parse(readFileSync6(counterPath, "utf8"));
|
|
4353
|
+
return typeof parsed === "object" && parsed !== null && "count" in parsed && typeof parsed.count === "number" && parsed.count >= 0 ? parsed.count : 0;
|
|
4354
|
+
} catch (error) {
|
|
4355
|
+
if (error instanceof Error)
|
|
4356
|
+
return 0;
|
|
4357
|
+
throw error;
|
|
4358
|
+
}
|
|
4359
|
+
}
|
|
4360
|
+
function readCounts(counterPath) {
|
|
4361
|
+
try {
|
|
4362
|
+
const parsed = JSON.parse(readFileSync6(counterPath, "utf8"));
|
|
4363
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
4364
|
+
return {};
|
|
4365
|
+
const counts = {};
|
|
4366
|
+
for (const [key, value] of Object.entries(parsed))
|
|
4367
|
+
if (typeof value === "number" && value >= 0)
|
|
4368
|
+
counts[key] = value;
|
|
4369
|
+
return counts;
|
|
4370
|
+
} catch (error) {
|
|
4371
|
+
if (error instanceof Error)
|
|
4372
|
+
return {};
|
|
4373
|
+
throw error;
|
|
4374
|
+
}
|
|
4375
|
+
}
|
|
3825
4376
|
|
|
3826
4377
|
// components/ulw-loop/src/spawn-role-guard.ts
|
|
3827
4378
|
var LAZYCODEX_SPAWN_ROLES = new Set([
|
|
@@ -3879,7 +4430,7 @@ function applySpawnBudgetGuards(payload, options = {}) {
|
|
|
3879
4430
|
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
4431
|
const scope = { sessionId: payload.session_id };
|
|
3881
4432
|
const stateDir = ulwLoopDir(payload.cwd, scope);
|
|
3882
|
-
const plan = readPlan(
|
|
4433
|
+
const plan = readPlan(payload.cwd, payload.session_id);
|
|
3883
4434
|
if (plan === null)
|
|
3884
4435
|
return "";
|
|
3885
4436
|
const lockOptions = options.lockTimeoutMs === undefined ? {} : { timeoutMs: options.lockTimeoutMs };
|
|
@@ -3918,10 +4469,10 @@ async function runSpawnAdmissionRecorderCli(stdin, stdout) {
|
|
|
3918
4469
|
const dataDir = process.env["PLUGIN_DATA"];
|
|
3919
4470
|
if (typeof dataDir !== "string" || typeof payload["session_id"] !== "string")
|
|
3920
4471
|
return;
|
|
3921
|
-
const markerDir =
|
|
4472
|
+
const markerDir = join7(dataDir, "spawn-breaker");
|
|
3922
4473
|
try {
|
|
3923
|
-
|
|
3924
|
-
atomicWriteJson(
|
|
4474
|
+
mkdirSync3(markerDir, { recursive: true });
|
|
4475
|
+
atomicWriteJson(join7(markerDir, `${payload["session_id"]}.json`), {
|
|
3925
4476
|
reason: response,
|
|
3926
4477
|
at: new Date().toISOString()
|
|
3927
4478
|
});
|
|
@@ -3950,7 +4501,7 @@ async function runSpawnGuardCli(stdin, stdout) {
|
|
|
3950
4501
|
}
|
|
3951
4502
|
}
|
|
3952
4503
|
function peekFanOutBudget(stateDir) {
|
|
3953
|
-
const counterPath =
|
|
4504
|
+
const counterPath = join7(stateDir, "spawn-count.json");
|
|
3954
4505
|
const count = readCount(counterPath) + 1;
|
|
3955
4506
|
const limit = fanOutLimit();
|
|
3956
4507
|
if (count <= limit)
|
|
@@ -3958,7 +4509,7 @@ function peekFanOutBudget(stateDir) {
|
|
|
3958
4509
|
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
4510
|
}
|
|
3960
4511
|
function consumeFanOutBudget(stateDir) {
|
|
3961
|
-
const counterPath =
|
|
4512
|
+
const counterPath = join7(stateDir, "spawn-count.json");
|
|
3962
4513
|
const count = readCount(counterPath) + 1;
|
|
3963
4514
|
atomicWriteJson(counterPath, { count });
|
|
3964
4515
|
const limit = fanOutLimit();
|
|
@@ -3973,7 +4524,7 @@ function consumeReviewSpawnBudget(payload, plan, stateDir) {
|
|
|
3973
4524
|
const goal = plan.goals.find((candidate) => candidate.id === plan.activeGoalId) ?? plan.goals.find((candidate) => isFinalRunCompletionCandidate(plan, candidate));
|
|
3974
4525
|
if (goal === undefined)
|
|
3975
4526
|
return null;
|
|
3976
|
-
const counterPath =
|
|
4527
|
+
const counterPath = join7(stateDir, "review-spawn-counts.json");
|
|
3977
4528
|
const limit = reviewSpawnLimit();
|
|
3978
4529
|
const counts = readCounts(counterPath);
|
|
3979
4530
|
const key = `${agentType}:${goal.id}:a${goal.attempt}`;
|
|
@@ -3998,13 +4549,13 @@ function missingGateArtifact(payload, plan) {
|
|
|
3998
4549
|
const attemptDir = ulwLoopAttemptEvidenceDir(goal.id, goal.attempt, scope);
|
|
3999
4550
|
for (const name of requiredArtifacts) {
|
|
4000
4551
|
const relative = `${attemptDir}/${name}`;
|
|
4001
|
-
if (!isNonEmptyFile(
|
|
4552
|
+
if (!isNonEmptyFile(join7(payload.cwd, relative)))
|
|
4002
4553
|
return relative;
|
|
4003
4554
|
}
|
|
4004
4555
|
return null;
|
|
4005
4556
|
}
|
|
4006
4557
|
const manualQa = `.omo/evidence/${goal.id}-manual-qa.md`;
|
|
4007
|
-
return isNonEmptyFile(
|
|
4558
|
+
return isNonEmptyFile(join7(payload.cwd, manualQa)) ? null : manualQa;
|
|
4008
4559
|
}
|
|
4009
4560
|
function isGateReviewerSpawn(toolInput) {
|
|
4010
4561
|
const agentType = reviewAgentType(toolInput);
|
|
@@ -4051,11 +4602,6 @@ function activeSurfaceReviewerAlias(reviewer) {
|
|
|
4051
4602
|
}
|
|
4052
4603
|
return reviewer;
|
|
4053
4604
|
}
|
|
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
4605
|
function deny(reason) {
|
|
4060
4606
|
return `${JSON.stringify({
|
|
4061
4607
|
hookSpecificOutput: {
|
|
@@ -4067,17 +4613,6 @@ function deny(reason) {
|
|
|
4067
4613
|
})}
|
|
4068
4614
|
`;
|
|
4069
4615
|
}
|
|
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
4616
|
function fanOutLimit() {
|
|
4082
4617
|
const raw = process.env["OMO_SPAWN_FANOUT_LIMIT"];
|
|
4083
4618
|
if (raw === undefined)
|
|
@@ -4092,45 +4627,9 @@ function reviewSpawnLimit() {
|
|
|
4092
4627
|
const parsed = Number.parseInt(raw, 10);
|
|
4093
4628
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_REVIEW_SPAWN_LIMIT;
|
|
4094
4629
|
}
|
|
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) {
|
|
4630
|
+
function readPlan(repoRoot, sessionId) {
|
|
4132
4631
|
try {
|
|
4133
|
-
return
|
|
4632
|
+
return readUlwLoopPlanSync(repoRoot, { sessionId });
|
|
4134
4633
|
} catch (error) {
|
|
4135
4634
|
if (error instanceof Error)
|
|
4136
4635
|
return null;
|
|
@@ -4139,8 +4638,8 @@ function readPlan(goalsPath) {
|
|
|
4139
4638
|
}
|
|
4140
4639
|
|
|
4141
4640
|
// components/ulw-loop/src/stop-resume-hook.ts
|
|
4142
|
-
import { existsSync as
|
|
4143
|
-
import { isAbsolute as
|
|
4641
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "node:fs";
|
|
4642
|
+
import { isAbsolute as isAbsolute3, join as join8, resolve as resolve5, sep as sep3 } from "node:path";
|
|
4144
4643
|
var RESUME_CAP = 2;
|
|
4145
4644
|
var CONTEXT_PRESSURE_MARKERS2 = [
|
|
4146
4645
|
"context compacted",
|
|
@@ -4161,7 +4660,7 @@ function runStopResumeHook(input) {
|
|
|
4161
4660
|
return "";
|
|
4162
4661
|
const scope = { sessionId: payload.session_id };
|
|
4163
4662
|
const stateDir = ulwLoopDir(payload.cwd, scope);
|
|
4164
|
-
const plan = readPlan2(
|
|
4663
|
+
const plan = readPlan2(payload.cwd, payload.session_id);
|
|
4165
4664
|
if (plan === null || plan.aggregateCompletion?.status === "complete")
|
|
4166
4665
|
return "";
|
|
4167
4666
|
const goal = resumableGoal(plan);
|
|
@@ -4207,23 +4706,23 @@ function consumeResumeBudgetLocked(lockPath, stateDir, goalId) {
|
|
|
4207
4706
|
}
|
|
4208
4707
|
}
|
|
4209
4708
|
function consumeResumeBudget(stateDir, goalId) {
|
|
4210
|
-
const ledgerLineCount =
|
|
4211
|
-
const counterPath =
|
|
4212
|
-
const stuckPath =
|
|
4709
|
+
const ledgerLineCount = readLedgerAt(stateDir).length;
|
|
4710
|
+
const counterPath = resolve5(stateDir, `auto-resume-${goalId}.json`);
|
|
4711
|
+
const stuckPath = resolve5(stateDir, `auto-resume-${goalId}.stuck`);
|
|
4213
4712
|
if (!isInsideDir(stateDir, counterPath) || !isInsideDir(stateDir, stuckPath))
|
|
4214
4713
|
return false;
|
|
4215
4714
|
const previous = readCounter(counterPath);
|
|
4216
4715
|
const count = previous !== null && previous.ledgerLineCount === ledgerLineCount ? previous.count : 0;
|
|
4217
4716
|
if (count >= RESUME_CAP) {
|
|
4218
|
-
|
|
4717
|
+
writeFileSync3(stuckPath, `no ledger progress after ${count} resumes
|
|
4219
4718
|
`);
|
|
4220
4719
|
return false;
|
|
4221
4720
|
}
|
|
4222
|
-
|
|
4721
|
+
writeFileSync3(counterPath, JSON.stringify({ count: count + 1, ledgerLineCount }));
|
|
4223
4722
|
return true;
|
|
4224
4723
|
}
|
|
4225
4724
|
function isInsideDir(dir, candidate) {
|
|
4226
|
-
return candidate.startsWith(
|
|
4725
|
+
return candidate.startsWith(resolve5(dir) + sep3);
|
|
4227
4726
|
}
|
|
4228
4727
|
function renderResumeDirective(plan, goal, sessionId) {
|
|
4229
4728
|
const normalized = normalizeUlwLoopSessionId(sessionId);
|
|
@@ -4238,30 +4737,20 @@ function renderResumeDirective(plan, goal, sessionId) {
|
|
|
4238
4737
|
].join(`
|
|
4239
4738
|
`);
|
|
4240
4739
|
}
|
|
4241
|
-
function readPlan2(
|
|
4740
|
+
function readPlan2(repoRoot, sessionId) {
|
|
4242
4741
|
try {
|
|
4243
|
-
return
|
|
4742
|
+
return readUlwLoopPlanSync(repoRoot, { sessionId });
|
|
4244
4743
|
} catch (error) {
|
|
4245
4744
|
if (error instanceof Error)
|
|
4246
4745
|
return null;
|
|
4247
4746
|
throw error;
|
|
4248
4747
|
}
|
|
4249
4748
|
}
|
|
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
4749
|
function readCounter(counterPath) {
|
|
4261
4750
|
try {
|
|
4262
|
-
if (!
|
|
4751
|
+
if (!existsSync8(counterPath))
|
|
4263
4752
|
return null;
|
|
4264
|
-
const parsed = JSON.parse(
|
|
4753
|
+
const parsed = JSON.parse(readFileSync7(counterPath, "utf8"));
|
|
4265
4754
|
if (typeof parsed["count"] !== "number" || typeof parsed["ledgerLineCount"] !== "number")
|
|
4266
4755
|
return null;
|
|
4267
4756
|
return { count: parsed["count"], ledgerLineCount: parsed["ledgerLineCount"] };
|
|
@@ -4273,7 +4762,7 @@ function readCounter(counterPath) {
|
|
|
4273
4762
|
}
|
|
4274
4763
|
function boulderContinuationWillFire(cwd, sessionId) {
|
|
4275
4764
|
try {
|
|
4276
|
-
const raw = JSON.parse(
|
|
4765
|
+
const raw = JSON.parse(readFileSync7(join8(cwd, ".omo", "boulder.json"), "utf8"));
|
|
4277
4766
|
const works = raw["works"];
|
|
4278
4767
|
const entries = typeof works === "object" && works !== null ? Object.values(works) : [raw];
|
|
4279
4768
|
return entries.some((work) => {
|
|
@@ -4292,7 +4781,7 @@ function boulderContinuationWillFire(cwd, sessionId) {
|
|
|
4292
4781
|
}
|
|
4293
4782
|
function transcriptShowsContextPressure(transcriptPath) {
|
|
4294
4783
|
try {
|
|
4295
|
-
const transcript =
|
|
4784
|
+
const transcript = readFileSync7(transcriptPath, "utf8").toLowerCase();
|
|
4296
4785
|
return CONTEXT_PRESSURE_MARKERS2.some((marker) => transcript.includes(marker));
|
|
4297
4786
|
} catch (error) {
|
|
4298
4787
|
if (error instanceof Error)
|
|
@@ -4304,12 +4793,12 @@ function boulderPlanHasChecklist(cwd, entry) {
|
|
|
4304
4793
|
const activePlan = entry["active_plan"];
|
|
4305
4794
|
if (typeof activePlan !== "string" || activePlan.trim().length === 0)
|
|
4306
4795
|
return false;
|
|
4307
|
-
const planPath =
|
|
4796
|
+
const planPath = isAbsolute3(activePlan) ? activePlan : join8(cwd, activePlan);
|
|
4308
4797
|
const worktree = entry["worktree_path"];
|
|
4309
|
-
const candidates = typeof worktree === "string" && worktree.trim().length > 0 && !
|
|
4798
|
+
const candidates = typeof worktree === "string" && worktree.trim().length > 0 && !isAbsolute3(activePlan) ? [join8(isAbsolute3(worktree) ? worktree : join8(cwd, worktree), activePlan), planPath] : [planPath];
|
|
4310
4799
|
for (const candidate of candidates) {
|
|
4311
4800
|
try {
|
|
4312
|
-
return
|
|
4801
|
+
return readFileSync7(candidate, "utf8").split(/\r?\n/).some((line) => line.startsWith("- [ ] ") || line.startsWith("- [x] ") || line.startsWith("- [X] "));
|
|
4313
4802
|
} catch (error) {
|
|
4314
4803
|
if (!(error instanceof Error))
|
|
4315
4804
|
throw error;
|