oh-my-opencode 4.15.1 → 4.16.1
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/.agents/skills/codex-qa/SKILL.md +9 -7
- package/.agents/skills/codex-qa/references/logging-debug.md +6 -4
- package/.agents/skills/opencode-qa/SKILL.md +9 -9
- package/.agents/skills/opencode-qa/references/tui-tmux.md +10 -8
- package/dist/cli/codex-ulw-loop.d.ts +8 -0
- package/dist/cli/doctor/checks/tui-plugin-config.d.ts +4 -4
- package/dist/cli/get-local-version/types.d.ts +1 -1
- package/dist/cli/index.js +792 -744
- package/dist/cli/install-senpi/index.d.ts +1 -0
- package/dist/cli/run/poll-for-completion.d.ts +4 -0
- package/dist/cli/run/session-resolver.d.ts +1 -0
- package/dist/cli/senpi-platform-flag.d.ts +4 -0
- package/dist/cli/types.d.ts +2 -1
- package/dist/cli-node/index.js +792 -744
- package/dist/features/background-agent/manager.d.ts +16 -0
- package/dist/features/builtin-commands/templates/start-work.d.ts +1 -1
- package/dist/features/monitor/process.d.ts +17 -1
- package/dist/hooks/anthropic-context-window-limit-recovery/aggressive-truncation-strategy.d.ts +1 -0
- package/dist/index.js +2206 -1971
- package/dist/skills/debugging/references/methodology/06-fix.md +6 -0
- package/dist/skills/frontend/ATTRIBUTION.md +10 -1
- package/dist/skills/frontend/SKILL.md +12 -7
- package/dist/skills/frontend/references/design/README.md +8 -4
- package/dist/skills/frontend/references/design/_INDEX.md +2 -2
- package/dist/skills/frontend/references/design/design-system-architecture.md +24 -2
- package/dist/skills/frontend/references/design/lazyweb.md +77 -0
- package/dist/skills/frontend/references/design/stitch-design-example.md +121 -0
- package/dist/skills/frontend/references/designpowers/README.md +2 -2
- package/dist/skills/frontend/references/designpowers/lane-b-execution.md +1 -1
- package/dist/skills/programming/SKILL.md +10 -1
- package/dist/skills/programming/references/logging.md +91 -0
- package/dist/skills/start-work/SKILL.md +2 -2
- package/dist/skills/ulw-plan/SKILL.md +5 -5
- package/dist/skills/ulw-plan/references/full-workflow.md +7 -7
- package/dist/skills/ulw-plan/references/intent-unclear.md +3 -3
- package/dist/skills/ulw-research/ATTRIBUTION.md +2 -2
- package/dist/skills/ulw-research/SKILL.md +28 -7
- package/dist/skills/visual-qa/SKILL.md +13 -17
- package/dist/tools/skill/description-formatter.d.ts +1 -0
- package/dist/tools/skill/description-formatter.test-support.d.ts +9 -0
- package/dist/tools/skill/zauc-mocks-skill-tools/test-support.d.ts +25 -0
- package/dist/tui.js +121 -419
- package/docs/reference/web-terminal-visual-qa.md +39 -45
- package/package.json +48 -25
- package/packages/git-bash-mcp/dist/cli.js +2 -2
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/AGENTS.md +55 -0
- package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +19 -16
- 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/codegraph/AGENTS.md +51 -0
- package/packages/omo-codex/plugin/components/codegraph/dist/cli.js +936 -248
- package/packages/omo-codex/plugin/components/codegraph/dist/serve.js +233 -135
- package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
- package/packages/omo-codex/plugin/components/codegraph/src/cache-gc.ts +29 -0
- package/packages/omo-codex/plugin/components/codegraph/src/cli.ts +13 -0
- package/packages/omo-codex/plugin/components/codegraph/src/hook-sweep.ts +25 -0
- package/packages/omo-codex/plugin/components/codegraph/src/hook-types.ts +3 -1
- package/packages/omo-codex/plugin/components/codegraph/src/hook.ts +30 -1
- package/packages/omo-codex/plugin/components/codegraph/src/serve.ts +16 -2
- package/packages/omo-codex/plugin/components/codegraph/src/sweep-cli.ts +80 -0
- package/packages/omo-codex/plugin/components/codegraph/test/cache-gc.test.ts +34 -0
- package/packages/omo-codex/plugin/components/codegraph/test/hook-exclusion.test.ts +107 -0
- package/packages/omo-codex/plugin/components/codegraph/test/hook-registration.test.ts +26 -0
- package/packages/omo-codex/plugin/components/codegraph/test/hook-sweep.test.ts +29 -0
- package/packages/omo-codex/plugin/components/codegraph/test/hook.test.ts +8 -21
- package/packages/omo-codex/plugin/components/codegraph/test/mcp-bridge-fixtures.ts +108 -0
- package/packages/omo-codex/plugin/components/codegraph/test/serve-built-wrapper.test.ts +76 -0
- package/packages/omo-codex/plugin/components/codegraph/test/serve-mcp-bridge.test.ts +17 -111
- package/packages/omo-codex/plugin/components/codegraph/test/serve-provision.test.ts +1 -1
- package/packages/omo-codex/plugin/components/codegraph/test/serve.test.ts +45 -67
- package/packages/omo-codex/plugin/components/codegraph/test/sweep-cli.test.ts +56 -0
- 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/AGENTS.md +31 -0
- 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/AGENTS.md +45 -0
- 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/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
- package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus.md +37 -102
- 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/start-work-continuation/directive.md +2 -2
- package/packages/omo-codex/plugin/components/start-work-continuation/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/start-work-continuation/package.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/AGENTS.md +46 -0
- 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/dist/cli.js +6 -0
- package/packages/omo-codex/plugin/components/telemetry/dist/posthog.js +6 -0
- 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.md +5 -2
- package/packages/omo-codex/plugin/components/ultrawork/CHANGELOG.md +2 -0
- package/packages/omo-codex/plugin/components/ultrawork/README.md +3 -3
- package/packages/omo-codex/plugin/components/ultrawork/directive.md +18 -10
- package/packages/omo-codex/plugin/components/ultrawork/dist/cli.js +51 -2
- package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/package.json +2 -1
- package/packages/omo-codex/plugin/components/ultrawork/scripts/sync-directive.mjs +14 -1
- package/packages/omo-codex/plugin/components/ultrawork/skills/ultrawork/SKILL.md +388 -0
- package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/SKILL.md +16 -2
- package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/references/full-workflow.md +4 -4
- package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/references/intent-unclear.md +2 -2
- package/packages/omo-codex/plugin/components/ultrawork/src/codex-hook.ts +5 -3
- package/packages/omo-codex/plugin/components/ultrawork/src/skill-pointer.ts +56 -0
- package/packages/omo-codex/plugin/components/ultrawork/test/codex-hook.test.ts +26 -5
- package/packages/omo-codex/plugin/components/ultrawork/test/directive-source.test.ts +12 -0
- package/packages/omo-codex/plugin/components/ultrawork/test/package-smoke.test.ts +1 -0
- package/packages/omo-codex/plugin/components/ultrawork/test/skill-pointer.test.ts +82 -0
- package/packages/omo-codex/plugin/components/ulw-loop/CHANGELOG.md +5 -0
- package/packages/omo-codex/plugin/components/ulw-loop/directive.md +18 -10
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-commands.js +15 -2
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-steering.js +2 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +145 -32
- package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-hook.d.ts +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-hook.js +5 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-io.d.ts +6 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-io.js +55 -9
- package/packages/omo-codex/plugin/components/ulw-loop/dist/steering-snapshot.d.ts +15 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/steering-snapshot.js +33 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/steering-types.d.ts +10 -3
- package/packages/omo-codex/plugin/components/ulw-loop/dist/steering.js +15 -11
- package/packages/omo-codex/plugin/components/ulw-loop/dist/ultrawork-directive.d.ts +2 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/ultrawork-directive.js +5 -3
- package/packages/omo-codex/plugin/components/ulw-loop/dist/ultrawork-skill-pointer.d.ts +7 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/ultrawork-skill-pointer.js +46 -0
- package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +7 -7
- package/packages/omo-codex/plugin/components/ulw-loop/src/cli-commands.ts +17 -2
- package/packages/omo-codex/plugin/components/ulw-loop/src/cli-steering.ts +2 -1
- package/packages/omo-codex/plugin/components/ulw-loop/src/codex-hook.ts +5 -1
- package/packages/omo-codex/plugin/components/ulw-loop/src/plan-io.ts +59 -11
- package/packages/omo-codex/plugin/components/ulw-loop/src/steering-snapshot.ts +38 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/steering-types.ts +11 -3
- package/packages/omo-codex/plugin/components/ulw-loop/src/steering.ts +15 -7
- package/packages/omo-codex/plugin/components/ulw-loop/src/ultrawork-directive.ts +9 -3
- package/packages/omo-codex/plugin/components/ulw-loop/src/ultrawork-skill-pointer.ts +55 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/cli-create-goals.test.ts +16 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/plan-io.test.ts +260 -2
- package/packages/omo-codex/plugin/components/ulw-loop/test/steering-snapshot.test.ts +124 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/steering.test.ts +101 -2
- package/packages/omo-codex/plugin/components/ulw-loop/test/ultrawork-directive.test.ts +49 -0
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-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-checking-codegraph-bootstrap.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-start-work-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-checking-start-work-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/package-lock.json +13 -13
- package/packages/omo-codex/plugin/package.json +1 -1
- package/packages/omo-codex/plugin/scripts/build-components.mjs +63 -17
- package/packages/omo-codex/plugin/scripts/hook-status-message.mjs +10 -6
- package/packages/omo-codex/plugin/scripts/materialize-shared-upstreams.mjs +10 -3
- package/packages/omo-codex/plugin/scripts/sync-skills.mjs +1 -0
- package/packages/omo-codex/plugin/shared/test/config-loader.test.ts +18 -0
- package/packages/omo-codex/plugin/skills/debugging/references/methodology/06-fix.md +6 -0
- package/packages/omo-codex/plugin/skills/frontend/ATTRIBUTION.md +10 -1
- package/packages/omo-codex/plugin/skills/frontend/SKILL.md +12 -7
- package/packages/omo-codex/plugin/skills/frontend/references/design/README.md +8 -4
- package/packages/omo-codex/plugin/skills/frontend/references/design/_INDEX.md +2 -2
- package/packages/omo-codex/plugin/skills/frontend/references/design/design-system-architecture.md +24 -2
- package/packages/omo-codex/plugin/skills/frontend/references/design/lazyweb.md +77 -0
- package/packages/omo-codex/plugin/skills/frontend/references/design/stitch-design-example.md +121 -0
- package/packages/omo-codex/plugin/skills/frontend/references/designpowers/README.md +2 -2
- package/packages/omo-codex/plugin/skills/frontend/references/designpowers/lane-b-execution.md +1 -1
- package/packages/omo-codex/plugin/skills/programming/SKILL.md +10 -1
- package/packages/omo-codex/plugin/skills/programming/references/logging.md +91 -0
- package/packages/omo-codex/plugin/skills/start-work/SKILL.md +2 -2
- package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +388 -0
- package/packages/omo-codex/plugin/skills/ultrawork/agents/openai.yaml +2 -0
- package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +1 -1
- package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +7 -7
- package/packages/omo-codex/plugin/skills/ulw-plan/SKILL.md +16 -2
- package/packages/omo-codex/plugin/skills/ulw-plan/references/full-workflow.md +4 -4
- package/packages/omo-codex/plugin/skills/ulw-plan/references/intent-unclear.md +2 -2
- package/packages/omo-codex/plugin/skills/ulw-research/ATTRIBUTION.md +2 -2
- package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +28 -7
- package/packages/omo-codex/plugin/skills/visual-qa/SKILL.md +13 -17
- package/packages/omo-codex/plugin/test/aggregate-hooks.test.mjs +4 -4
- package/packages/omo-codex/plugin/test/bootstrap-hooks.test.mjs +1 -1
- package/packages/omo-codex/plugin/test/hook-status-message.test.mjs +22 -9
- package/packages/omo-codex/plugin/test/install-time-build-runtime.test.mjs +1 -1
- package/packages/omo-codex/plugin/test/sync-hook-status-messages.test.mjs +6 -6
- package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +2 -0
- package/packages/omo-codex/plugin/test/ultrawork-skill-pointer.test.mjs +56 -0
- package/packages/omo-codex/plugin/test/ulw-plan-skill-contract.test.mjs +52 -0
- package/packages/omo-codex/plugin/test/ulw-research-epistemic-contract.test.mjs +98 -0
- package/packages/omo-codex/plugin/test/ulw-research-skill-contract.test.mjs +6 -6
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +310 -3979
- package/packages/omo-codex/scripts/install-lazycodex-version-stamp.test.mjs +2 -2
- package/packages/shared-skills/skills/debugging/references/methodology/06-fix.md +6 -0
- package/packages/shared-skills/skills/frontend/ATTRIBUTION.md +10 -1
- package/packages/shared-skills/skills/frontend/SKILL.md +12 -7
- package/packages/shared-skills/skills/frontend/references/design/README.md +8 -4
- package/packages/shared-skills/skills/frontend/references/design/_INDEX.md +2 -2
- package/packages/shared-skills/skills/frontend/references/design/design-system-architecture.md +24 -2
- package/packages/shared-skills/skills/frontend/references/design/lazyweb.md +77 -0
- package/packages/shared-skills/skills/frontend/references/design/stitch-design-example.md +121 -0
- package/packages/shared-skills/skills/frontend/references/designpowers/README.md +2 -2
- package/packages/shared-skills/skills/frontend/references/designpowers/lane-b-execution.md +1 -1
- package/packages/shared-skills/skills/programming/SKILL.md +10 -1
- package/packages/shared-skills/skills/programming/references/logging.md +91 -0
- package/packages/shared-skills/skills/start-work/SKILL.md +2 -2
- package/packages/shared-skills/skills/ulw-plan/SKILL.md +5 -5
- package/packages/shared-skills/skills/ulw-plan/references/full-workflow.md +7 -7
- package/packages/shared-skills/skills/ulw-plan/references/intent-unclear.md +3 -3
- package/packages/shared-skills/skills/ulw-research/ATTRIBUTION.md +2 -2
- package/packages/shared-skills/skills/ulw-research/SKILL.md +28 -7
- package/packages/shared-skills/skills/visual-qa/SKILL.md +13 -17
- package/script/qa/strip-ansi.mjs +10 -0
- package/script/qa/web-terminal-visual-qa.mjs +112 -195
- package/script/qa/xterm-live-terminal.mjs +180 -0
- package/script/qa/web-terminal-renderer.mjs +0 -218
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
import { isUlwLoopDone } from "./goal-status.js";
|
|
3
3
|
import type { UlwLoopScope } from "./paths.js";
|
|
4
4
|
import { seedDefaultSuccessCriteria } from "./plan-crud.js";
|
|
5
|
-
import { appendLedger,
|
|
5
|
+
import { appendLedger, findAcceptedSteeringLedgerEntry, readUlwLoopPlan, withUlwLoopMutationLock, writePlan } from "./plan-io.js";
|
|
6
|
+
import { buildSteeringPlanSnapshot, changedGoalIdsBetween } from "./steering-snapshot.js";
|
|
6
7
|
import type {
|
|
7
8
|
SteerUlwLoopResult,
|
|
8
9
|
UlwLoopItem,
|
|
@@ -245,13 +246,22 @@ export async function steerUlwLoop(repoRoot: string, proposal: UlwLoopSteeringPr
|
|
|
245
246
|
return withUlwLoopMutationLock(repoRoot, scope, async () => {
|
|
246
247
|
const plan = await readUlwLoopPlan(repoRoot, scope);
|
|
247
248
|
const key = proposal.idempotencyKey ?? proposal.promptSignature;
|
|
248
|
-
const prior = key === undefined ? undefined :
|
|
249
|
-
if (prior?.steering !== undefined)
|
|
249
|
+
const prior = key === undefined ? undefined : await findAcceptedSteeringLedgerEntry(repoRoot, key, scope);
|
|
250
|
+
if (prior?.steering !== undefined) {
|
|
251
|
+
// Legacy entries embedded full-plan before/after snapshots; never
|
|
252
|
+
// re-surface those multi-MB payloads on the dedup path.
|
|
253
|
+
const { before: _before, after: _after, ...compactPrior } = prior.steering;
|
|
254
|
+
return { plan, accepted: true, audit: { ...compactPrior, deduped: true }, rejectedReasons: [], deduped: true };
|
|
255
|
+
}
|
|
250
256
|
const audit = validateUlwLoopSteeringProposal(plan, proposal);
|
|
251
257
|
const accepted = audit.invariant.accepted;
|
|
252
258
|
const next = accepted ? applySteeringMutation(plan, proposal, audit) : plan;
|
|
253
|
-
const finalAudit: UlwLoopSteeringAudit = { ...audit
|
|
254
|
-
if (accepted)
|
|
259
|
+
const finalAudit: UlwLoopSteeringAudit = { ...audit };
|
|
260
|
+
if (accepted) {
|
|
261
|
+
const changed = changedGoalIdsBetween(plan, next);
|
|
262
|
+
finalAudit.before = buildSteeringPlanSnapshot(plan, changed);
|
|
263
|
+
finalAudit.after = buildSteeringPlanSnapshot(next, changed);
|
|
264
|
+
}
|
|
255
265
|
if (accepted) await writePlan(repoRoot, next, scope);
|
|
256
266
|
await appendLedger(repoRoot, ledgerEntry(proposal, finalAudit, proposal.now?.toISOString() ?? iso()), scope);
|
|
257
267
|
return { plan: next, accepted, audit: finalAudit, rejectedReasons: audit.invariant.rejectedReasons, deduped: false };
|
|
@@ -264,7 +274,5 @@ function ledgerEntry(proposal: UlwLoopSteeringProposal, audit: UlwLoopSteeringAu
|
|
|
264
274
|
if (goalId !== undefined) entry.goalId = goalId;
|
|
265
275
|
if (proposal.criterionId !== undefined) entry.criterionId = proposal.criterionId;
|
|
266
276
|
if (proposal.idempotencyKey !== undefined) entry.idempotencyKey = proposal.idempotencyKey;
|
|
267
|
-
if (audit.before !== undefined) entry.before = audit.before;
|
|
268
|
-
if (audit.after !== undefined) entry.after = audit.after;
|
|
269
277
|
return entry;
|
|
270
278
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
|
|
3
|
+
import { buildUltraworkAdditionalContext, type UltraworkAdditionalContextOptions } from "./ultrawork-skill-pointer.js";
|
|
4
|
+
|
|
3
5
|
export interface UltraworkDirectiveInput {
|
|
4
6
|
readonly prompt: string;
|
|
5
7
|
readonly transcript_path?: string | null;
|
|
@@ -12,7 +14,6 @@ interface UserPromptSubmitHookOutput {
|
|
|
12
14
|
};
|
|
13
15
|
}
|
|
14
16
|
|
|
15
|
-
const ULTRAWORK_DIRECTIVE = readFileSync(new URL("../directive.md", import.meta.url), "utf8");
|
|
16
17
|
const ULTRAWORK_CURRENT_PROMPT_PATTERN = /(?:ultrawork|ulw)/i;
|
|
17
18
|
const ULTRAWORK_DIRECTIVE_MARKER = "<ultrawork-mode>";
|
|
18
19
|
const TRANSCRIPT_SEARCH_BYTES = 512_000;
|
|
@@ -26,11 +27,16 @@ const CONTEXT_PRESSURE_MARKERS = [
|
|
|
26
27
|
"long threads and multiple compactions",
|
|
27
28
|
] as const;
|
|
28
29
|
|
|
29
|
-
export function buildUltraworkDirectiveOutput(
|
|
30
|
+
export function buildUltraworkDirectiveOutput(
|
|
31
|
+
input: UltraworkDirectiveInput,
|
|
32
|
+
options: UltraworkAdditionalContextOptions = {},
|
|
33
|
+
): string {
|
|
30
34
|
if (isContextPressureRecoveryPrompt(input.prompt)) return "";
|
|
31
35
|
if (hasUltraworkDirectiveAlreadyInTranscript(input.transcript_path)) return "";
|
|
32
36
|
if (isContextPressureTranscript(input.transcript_path)) return "";
|
|
33
|
-
return isUltraworkPrompt(input.prompt)
|
|
37
|
+
return isUltraworkPrompt(input.prompt)
|
|
38
|
+
? formatAdditionalContextOutput(buildUltraworkAdditionalContext(options))
|
|
39
|
+
: "";
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
function hasUltraworkDirectiveAlreadyInTranscript(transcriptPath: string | null | undefined): boolean {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
|
|
4
|
+
export const ULTRAWORK_SKILL_POINTER_TEMPLATE = `<ultrawork-mode>
|
|
5
|
+
ULTRAWORK MODE IS ACTIVE FOR THIS TASK.
|
|
6
|
+
|
|
7
|
+
MANDATORY BOOTSTRAP: do all three steps, in order, before anything else.
|
|
8
|
+
|
|
9
|
+
1. First user-visible line this turn MUST be exactly:
|
|
10
|
+
\`ULTRAWORK MODE ENABLED!\`
|
|
11
|
+
|
|
12
|
+
2. Call \`create_goal\` NOW with \`objective\` set to the user's request.
|
|
13
|
+
Send \`objective\` only: no \`status\`, no budget fields. If the
|
|
14
|
+
\`create_goal\` tool is unavailable, open your reply with a binding
|
|
15
|
+
\`# Goal\` block instead. Never skip this step.
|
|
16
|
+
|
|
17
|
+
3. Read the FULL ultrawork directive NOW, before any other tool call,
|
|
18
|
+
plan, or edit. It is the \`ultrawork\` skill, stored at:
|
|
19
|
+
|
|
20
|
+
{{ULTRAWORK_SKILL_PATH}}
|
|
21
|
+
|
|
22
|
+
Read the whole file. If a read result comes back truncated, keep
|
|
23
|
+
reading the remaining line ranges until you have seen every line.
|
|
24
|
+
Every rule in that file is binding for this entire task: no
|
|
25
|
+
compromise, no summarizing from memory, no skipping. If the file does
|
|
26
|
+
not exist, tell the user the omo ultrawork skill is missing and
|
|
27
|
+
continue with steps 1 and 2 plus evidence-bound execution.
|
|
28
|
+
|
|
29
|
+
Do not start the requested work until all three steps are complete.
|
|
30
|
+
</ultrawork-mode>
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
const ULTRAWORK_SKILL_PATH_PLACEHOLDER = "{{ULTRAWORK_SKILL_PATH}}";
|
|
34
|
+
const ULTRAWORK_SKILL_FILE_URL = new URL("../../../skills/ultrawork/SKILL.md", import.meta.url);
|
|
35
|
+
const ULTRAWORK_DIRECTIVE = readFileSync(new URL("../directive.md", import.meta.url), "utf8");
|
|
36
|
+
|
|
37
|
+
export interface UltraworkAdditionalContextOptions {
|
|
38
|
+
readonly skillFilePath?: string | null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function resolveUltraworkSkillFilePath(): string {
|
|
42
|
+
return fileURLToPath(ULTRAWORK_SKILL_FILE_URL);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function buildUltraworkSkillPointer(skillFilePath: string): string {
|
|
46
|
+
return ULTRAWORK_SKILL_POINTER_TEMPLATE.replace(ULTRAWORK_SKILL_PATH_PLACEHOLDER, skillFilePath);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function buildUltraworkAdditionalContext(options: UltraworkAdditionalContextOptions = {}): string {
|
|
50
|
+
const skillFilePath = options.skillFilePath === undefined ? resolveUltraworkSkillFilePath() : options.skillFilePath;
|
|
51
|
+
if (skillFilePath !== null && existsSync(skillFilePath)) {
|
|
52
|
+
return buildUltraworkSkillPointer(skillFilePath);
|
|
53
|
+
}
|
|
54
|
+
return ULTRAWORK_DIRECTIVE;
|
|
55
|
+
}
|
|
@@ -169,4 +169,20 @@ describe("ulwLoopCommand create-goals", () => {
|
|
|
169
169
|
|
|
170
170
|
expect(await readFile(join(testDir, ".omo/ulw-loop/manual-456/goals.json"), "utf8")).toContain("Manual scoped");
|
|
171
171
|
});
|
|
172
|
+
|
|
173
|
+
it("#given a valueless --session-id #when creating goals #then it fails and writes no plan", async () => {
|
|
174
|
+
const code = await ulwLoopCommand(["create-goals", "--session-id", "--brief", "- Alpha"]);
|
|
175
|
+
|
|
176
|
+
expect(code).toBe(1);
|
|
177
|
+
expect(err.join("")).toContain("--session-id requires a non-empty value");
|
|
178
|
+
await expect(readFile(join(testDir, ".omo/ulw-loop/goals.json"), "utf8")).rejects.toThrow();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("#given an empty --session-id= #when creating goals #then it fails and writes no plan", async () => {
|
|
182
|
+
const code = await ulwLoopCommand(["create-goals", "--session-id=", "--brief", "- Alpha"]);
|
|
183
|
+
|
|
184
|
+
expect(code).toBe(1);
|
|
185
|
+
expect(err.join("")).toContain("--session-id requires a non-empty value");
|
|
186
|
+
await expect(readFile(join(testDir, ".omo/ulw-loop/goals.json"), "utf8")).rejects.toThrow();
|
|
187
|
+
});
|
|
172
188
|
});
|
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
import { copyFile, mkdir, mkdtemp, readdir, readFile, writeFile } from "node:fs/promises";
|
|
1
|
+
import { appendFile, copyFile, mkdir, mkdtemp, readdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { setImmediate as tick } from "node:timers/promises";
|
|
4
5
|
import { beforeEach, describe, expect, it } from "vitest";
|
|
5
6
|
import { ulwLoopDir, ulwLoopGoalsPath, ulwLoopLedgerPath } from "../src/paths.js";
|
|
6
7
|
import {
|
|
7
8
|
appendLedger,
|
|
9
|
+
findAcceptedSteeringLedgerEntry,
|
|
8
10
|
readSteeringLedgerEntries,
|
|
9
11
|
readUlwLoopPlan,
|
|
10
12
|
withUlwLoopMutationLock,
|
|
11
13
|
writePlan,
|
|
12
14
|
} from "../src/plan-io.js";
|
|
13
|
-
import type { UlwLoopItem, UlwLoopLedgerEntry, UlwLoopPlan } from "../src/types.js";
|
|
15
|
+
import type { UlwLoopItem, UlwLoopLedgerEntry, UlwLoopPlan, UlwLoopSteeringAudit } from "../src/types.js";
|
|
14
16
|
import { UlwLoopError } from "../src/types.js";
|
|
15
17
|
|
|
16
18
|
const NOW = "2026-05-23T00:00:00.000Z";
|
|
@@ -51,6 +53,45 @@ function entry(kind: UlwLoopLedgerEntry["kind"], goalId = "G001"): UlwLoopLedger
|
|
|
51
53
|
return { at: NOW, kind, goalId };
|
|
52
54
|
}
|
|
53
55
|
|
|
56
|
+
function steeringAudit(overrides: Partial<UlwLoopSteeringAudit> = {}): UlwLoopSteeringAudit {
|
|
57
|
+
return {
|
|
58
|
+
kind: "revise_pending_wording",
|
|
59
|
+
source: "cli",
|
|
60
|
+
targetGoalIds: ["G001"],
|
|
61
|
+
evidence: "observed evidence",
|
|
62
|
+
rationale: "needed change",
|
|
63
|
+
invariant: {
|
|
64
|
+
accepted: true,
|
|
65
|
+
structuralInvariantAccepted: true,
|
|
66
|
+
evidenceBackedNecessity: true,
|
|
67
|
+
noEasierCompletion: true,
|
|
68
|
+
rejectedReasons: [],
|
|
69
|
+
},
|
|
70
|
+
...overrides,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function rejectedSteeringAudit(overrides: Partial<UlwLoopSteeringAudit> = {}): UlwLoopSteeringAudit {
|
|
75
|
+
return steeringAudit({
|
|
76
|
+
invariant: {
|
|
77
|
+
accepted: false,
|
|
78
|
+
structuralInvariantAccepted: false,
|
|
79
|
+
evidenceBackedNecessity: false,
|
|
80
|
+
noEasierCompletion: true,
|
|
81
|
+
rejectedReasons: ["missing evidence"],
|
|
82
|
+
},
|
|
83
|
+
...overrides,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function steeringEntry(
|
|
88
|
+
kind: UlwLoopLedgerEntry["kind"],
|
|
89
|
+
audit: UlwLoopSteeringAudit,
|
|
90
|
+
overrides: Partial<UlwLoopLedgerEntry> = {},
|
|
91
|
+
): UlwLoopLedgerEntry {
|
|
92
|
+
return { at: NOW, kind, goalId: "G001", steering: audit, ...overrides };
|
|
93
|
+
}
|
|
94
|
+
|
|
54
95
|
async function makeRepo(): Promise<string> {
|
|
55
96
|
return mkdtemp(join(tmpdir(), "ug-io-"));
|
|
56
97
|
}
|
|
@@ -209,6 +250,112 @@ describe("readSteeringLedgerEntries", () => {
|
|
|
209
250
|
});
|
|
210
251
|
});
|
|
211
252
|
|
|
253
|
+
describe("findAcceptedSteeringLedgerEntry", () => {
|
|
254
|
+
it.each([
|
|
255
|
+
[
|
|
256
|
+
"top-level idempotencyKey",
|
|
257
|
+
steeringEntry("steering_accepted", steeringAudit(), { idempotencyKey: "needle-key" }),
|
|
258
|
+
],
|
|
259
|
+
["steering.idempotencyKey", steeringEntry("steering_accepted", steeringAudit({ idempotencyKey: "needle-key" }))],
|
|
260
|
+
[
|
|
261
|
+
"steering.promptSignature",
|
|
262
|
+
steeringEntry("steering_accepted", steeringAudit({ promptSignature: "needle-key" })),
|
|
263
|
+
],
|
|
264
|
+
] as const)("finds an accepted entry by %s", async (_name, target) => {
|
|
265
|
+
// given
|
|
266
|
+
const repoRoot = await makeRepo();
|
|
267
|
+
await appendLedger(repoRoot, steeringEntry("steering_accepted", steeringAudit({ idempotencyKey: "other-key" })));
|
|
268
|
+
await appendLedger(repoRoot, target);
|
|
269
|
+
|
|
270
|
+
// when
|
|
271
|
+
const found = await findAcceptedSteeringLedgerEntry(repoRoot, "needle-key");
|
|
272
|
+
|
|
273
|
+
// then
|
|
274
|
+
expect(found).toEqual(target);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("skips a rejected entry carrying the same key", async () => {
|
|
278
|
+
// given
|
|
279
|
+
const repoRoot = await makeRepo();
|
|
280
|
+
await appendLedger(
|
|
281
|
+
repoRoot,
|
|
282
|
+
steeringEntry("steering_rejected", rejectedSteeringAudit({ idempotencyKey: "needle-key" })),
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
// when/then
|
|
286
|
+
await expect(findAcceptedSteeringLedgerEntry(repoRoot, "needle-key")).resolves.toBeUndefined();
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("returns the accepted entry even when a rejected one with the same key precedes it", async () => {
|
|
290
|
+
// given
|
|
291
|
+
const repoRoot = await makeRepo();
|
|
292
|
+
const accepted = steeringEntry("steering_accepted", steeringAudit({ idempotencyKey: "needle-key" }));
|
|
293
|
+
await appendLedger(
|
|
294
|
+
repoRoot,
|
|
295
|
+
steeringEntry("steering_rejected", rejectedSteeringAudit({ idempotencyKey: "needle-key" })),
|
|
296
|
+
);
|
|
297
|
+
await appendLedger(repoRoot, accepted);
|
|
298
|
+
|
|
299
|
+
// when/then
|
|
300
|
+
await expect(findAcceptedSteeringLedgerEntry(repoRoot, "needle-key")).resolves.toEqual(accepted);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("returns undefined when the ledger file is missing", async () => {
|
|
304
|
+
// given
|
|
305
|
+
const repoRoot = await makeRepo();
|
|
306
|
+
|
|
307
|
+
// when/then
|
|
308
|
+
await expect(findAcceptedSteeringLedgerEntry(repoRoot, "needle-key")).resolves.toBeUndefined();
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("ignores the key when it only appears in non-steering entries", async () => {
|
|
312
|
+
// given
|
|
313
|
+
const repoRoot = await makeRepo();
|
|
314
|
+
await appendLedger(repoRoot, { at: NOW, kind: "goal_completed", goalId: "G001", idempotencyKey: "needle-key" });
|
|
315
|
+
await appendLedger(repoRoot, { at: NOW, kind: "evidence_captured", goalId: "G001", evidence: "needle-key" });
|
|
316
|
+
|
|
317
|
+
// when/then
|
|
318
|
+
await expect(findAcceptedSteeringLedgerEntry(repoRoot, "needle-key")).resolves.toBeUndefined();
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it("ignores an accepted entry whose key only appears in evidence text", async () => {
|
|
322
|
+
// given
|
|
323
|
+
const repoRoot = await makeRepo();
|
|
324
|
+
await appendLedger(repoRoot, steeringEntry("steering_accepted", steeringAudit({ evidence: "needle-key" })));
|
|
325
|
+
|
|
326
|
+
// when/then
|
|
327
|
+
await expect(findAcceptedSteeringLedgerEntry(repoRoot, "needle-key")).resolves.toBeUndefined();
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("finds the key inside a legacy entry embedding full before/after plans", async () => {
|
|
331
|
+
// given
|
|
332
|
+
const repoRoot = await makeRepo();
|
|
333
|
+
const bulkyGoals = Array.from({ length: 20 }, (_, index) =>
|
|
334
|
+
makeGoal({ id: `G${String(index + 1).padStart(3, "0")}`, objective: `evidence detail ${index} `.repeat(200) }),
|
|
335
|
+
);
|
|
336
|
+
const bulkyPlan = makePlan({ goals: bulkyGoals });
|
|
337
|
+
const legacyLine = JSON.stringify({
|
|
338
|
+
at: NOW,
|
|
339
|
+
kind: "steering_accepted",
|
|
340
|
+
goalId: "G001",
|
|
341
|
+
idempotencyKey: "needle-key",
|
|
342
|
+
before: bulkyPlan,
|
|
343
|
+
after: bulkyPlan,
|
|
344
|
+
steering: { ...steeringAudit({ idempotencyKey: "needle-key" }), before: bulkyPlan, after: bulkyPlan },
|
|
345
|
+
});
|
|
346
|
+
await mkdir(ulwLoopDir(repoRoot), { recursive: true });
|
|
347
|
+
await appendFile(ulwLoopLedgerPath(repoRoot), `${legacyLine}\n`, "utf8");
|
|
348
|
+
|
|
349
|
+
// when
|
|
350
|
+
const found = await findAcceptedSteeringLedgerEntry(repoRoot, "needle-key");
|
|
351
|
+
|
|
352
|
+
// then
|
|
353
|
+
expect(legacyLine.length).toBeGreaterThan(100_000);
|
|
354
|
+
expect(found?.kind).toBe("steering_accepted");
|
|
355
|
+
expect(found?.idempotencyKey).toBe("needle-key");
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
212
359
|
describe("withUlwLoopMutationLock", () => {
|
|
213
360
|
it("serializes concurrent invocations", async () => {
|
|
214
361
|
// given
|
|
@@ -236,4 +383,115 @@ describe("withUlwLoopMutationLock", () => {
|
|
|
236
383
|
expect(maxActive).toBe(1);
|
|
237
384
|
expect(await readFile(counterPath, "utf8")).toBe("3");
|
|
238
385
|
});
|
|
386
|
+
|
|
387
|
+
it("propagates the body rejection and keeps later calls working", async () => {
|
|
388
|
+
// given
|
|
389
|
+
const repoRoot = await makeRepo();
|
|
390
|
+
|
|
391
|
+
// when
|
|
392
|
+
const failure = withUlwLoopMutationLock(repoRoot, async () => {
|
|
393
|
+
throw new Error("body exploded");
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
// then
|
|
397
|
+
await expect(failure).rejects.toThrow("body exploded");
|
|
398
|
+
await expect(withUlwLoopMutationLock(repoRoot, async () => "recovered")).resolves.toBe("recovered");
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it("still serializes a second batch after the first batch's gate settles", async () => {
|
|
402
|
+
// given
|
|
403
|
+
const repoRoot = await makeRepo();
|
|
404
|
+
let active = 0;
|
|
405
|
+
let maxActive = 0;
|
|
406
|
+
const body = async (): Promise<void> => {
|
|
407
|
+
active += 1;
|
|
408
|
+
maxActive = Math.max(maxActive, active);
|
|
409
|
+
await tick();
|
|
410
|
+
active -= 1;
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
// when
|
|
414
|
+
await Promise.all([1, 2, 3].map(() => withUlwLoopMutationLock(repoRoot, body)));
|
|
415
|
+
await tick();
|
|
416
|
+
await Promise.all([1, 2].map(() => withUlwLoopMutationLock(repoRoot, body)));
|
|
417
|
+
|
|
418
|
+
// then
|
|
419
|
+
expect(maxActive).toBe(1);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it("keeps an in-flight successor locked when an earlier gate settles", async () => {
|
|
423
|
+
// given
|
|
424
|
+
const repoRoot = await makeRepo();
|
|
425
|
+
const events: string[] = [];
|
|
426
|
+
let releaseSecond = false;
|
|
427
|
+
|
|
428
|
+
// when
|
|
429
|
+
const first = withUlwLoopMutationLock(repoRoot, async () => {
|
|
430
|
+
events.push("first");
|
|
431
|
+
});
|
|
432
|
+
const second = withUlwLoopMutationLock(repoRoot, async () => {
|
|
433
|
+
events.push("second:start");
|
|
434
|
+
while (!releaseSecond) await tick();
|
|
435
|
+
events.push("second:end");
|
|
436
|
+
});
|
|
437
|
+
await first;
|
|
438
|
+
await tick();
|
|
439
|
+
const third = withUlwLoopMutationLock(repoRoot, async () => {
|
|
440
|
+
events.push("third");
|
|
441
|
+
});
|
|
442
|
+
releaseSecond = true;
|
|
443
|
+
await Promise.all([second, third]);
|
|
444
|
+
|
|
445
|
+
// then
|
|
446
|
+
expect(events).toEqual(["first", "second:start", "second:end", "third"]);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it("does not serialize distinct repo roots against each other", async () => {
|
|
450
|
+
// given
|
|
451
|
+
const rootA = await makeRepo();
|
|
452
|
+
const rootB = await makeRepo();
|
|
453
|
+
const events: string[] = [];
|
|
454
|
+
let releaseA = false;
|
|
455
|
+
|
|
456
|
+
// when
|
|
457
|
+
const held = withUlwLoopMutationLock(rootA, async () => {
|
|
458
|
+
events.push("a:start");
|
|
459
|
+
while (!releaseA) await tick();
|
|
460
|
+
events.push("a:end");
|
|
461
|
+
});
|
|
462
|
+
await withUlwLoopMutationLock(rootB, async () => {
|
|
463
|
+
events.push("b");
|
|
464
|
+
});
|
|
465
|
+
releaseA = true;
|
|
466
|
+
await held;
|
|
467
|
+
|
|
468
|
+
// then
|
|
469
|
+
expect(events).toEqual(["a:start", "b", "a:end"]);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it("serializes per scope while distinct scopes stay independent", async () => {
|
|
473
|
+
// given
|
|
474
|
+
const repoRoot = await makeRepo();
|
|
475
|
+
const scoped = { sessionId: "session-a" };
|
|
476
|
+
const events: string[] = [];
|
|
477
|
+
let releaseScoped = false;
|
|
478
|
+
|
|
479
|
+
// when
|
|
480
|
+
const heldScoped = withUlwLoopMutationLock(repoRoot, scoped, async () => {
|
|
481
|
+
events.push("scoped-1:start");
|
|
482
|
+
while (!releaseScoped) await tick();
|
|
483
|
+
events.push("scoped-1:end");
|
|
484
|
+
});
|
|
485
|
+
const queuedScoped = withUlwLoopMutationLock(repoRoot, scoped, async () => {
|
|
486
|
+
events.push("scoped-2");
|
|
487
|
+
});
|
|
488
|
+
await withUlwLoopMutationLock(repoRoot, async () => {
|
|
489
|
+
events.push("root-scope");
|
|
490
|
+
});
|
|
491
|
+
releaseScoped = true;
|
|
492
|
+
await Promise.all([heldScoped, queuedScoped]);
|
|
493
|
+
|
|
494
|
+
// then
|
|
495
|
+
expect(events).toEqual(["scoped-1:start", "root-scope", "scoped-1:end", "scoped-2"]);
|
|
496
|
+
});
|
|
239
497
|
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { buildSteeringPlanSnapshot, changedGoalIdsBetween } from "../src/steering-snapshot.js";
|
|
3
|
+
import type { UlwLoopItem, UlwLoopPlan } from "../src/types.js";
|
|
4
|
+
|
|
5
|
+
const NOW = "2026-05-23T00:00:00.000Z";
|
|
6
|
+
|
|
7
|
+
function goal(id: string, overrides: Partial<UlwLoopItem> = {}): UlwLoopItem {
|
|
8
|
+
return {
|
|
9
|
+
id,
|
|
10
|
+
title: `Goal ${id}`,
|
|
11
|
+
objective: `Objective for ${id}`,
|
|
12
|
+
status: "pending",
|
|
13
|
+
successCriteria: [],
|
|
14
|
+
attempt: 0,
|
|
15
|
+
createdAt: NOW,
|
|
16
|
+
updatedAt: NOW,
|
|
17
|
+
...overrides,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function plan(goals: UlwLoopItem[], overrides: Partial<UlwLoopPlan> = {}): UlwLoopPlan {
|
|
22
|
+
return {
|
|
23
|
+
version: 1,
|
|
24
|
+
createdAt: NOW,
|
|
25
|
+
updatedAt: NOW,
|
|
26
|
+
briefPath: ".omo/ulw-loop/brief.md",
|
|
27
|
+
goalsPath: ".omo/ulw-loop/goals.json",
|
|
28
|
+
ledgerPath: ".omo/ulw-loop/ledger.jsonl",
|
|
29
|
+
goals,
|
|
30
|
+
...overrides,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe("changedGoalIdsBetween", () => {
|
|
35
|
+
it.each([
|
|
36
|
+
{
|
|
37
|
+
name: "added goal",
|
|
38
|
+
before: [goal("G001"), goal("G002")],
|
|
39
|
+
after: [goal("G001"), goal("G002"), goal("G003")],
|
|
40
|
+
expected: ["G003"],
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: "removed goal",
|
|
44
|
+
before: [goal("G001"), goal("G002")],
|
|
45
|
+
after: [goal("G001")],
|
|
46
|
+
expected: ["G002"],
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: "mutated goal",
|
|
50
|
+
before: [goal("G001"), goal("G002")],
|
|
51
|
+
after: [goal("G001"), goal("G002", { title: "Rewritten" })],
|
|
52
|
+
expected: ["G002"],
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: "reordered-only goals",
|
|
56
|
+
before: [goal("G001"), goal("G002"), goal("G003")],
|
|
57
|
+
after: [goal("G003"), goal("G001"), goal("G002")],
|
|
58
|
+
expected: [],
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: "identical plans",
|
|
62
|
+
before: [goal("G001"), goal("G002")],
|
|
63
|
+
after: [goal("G001"), goal("G002")],
|
|
64
|
+
expected: [],
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: "add + remove + mutate at once",
|
|
68
|
+
before: [goal("G001"), goal("G002"), goal("G003")],
|
|
69
|
+
after: [goal("G001", { status: "blocked" }), goal("G003"), goal("G004")],
|
|
70
|
+
expected: ["G001", "G002", "G004"],
|
|
71
|
+
},
|
|
72
|
+
])("detects $name", ({ before, after, expected }) => {
|
|
73
|
+
// when
|
|
74
|
+
const changed = changedGoalIdsBetween(plan(before), plan(after));
|
|
75
|
+
|
|
76
|
+
// then
|
|
77
|
+
expect([...changed].sort()).toEqual(expected);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("buildSteeringPlanSnapshot", () => {
|
|
82
|
+
it("includes only changed goals while keeping full goalIds order and goalCount", () => {
|
|
83
|
+
// given
|
|
84
|
+
const goals = [goal("G001"), goal("G002"), goal("G003"), goal("G004")];
|
|
85
|
+
const source = plan(goals);
|
|
86
|
+
|
|
87
|
+
// when
|
|
88
|
+
const snapshot = buildSteeringPlanSnapshot(source, new Set(["G003"]));
|
|
89
|
+
|
|
90
|
+
// then
|
|
91
|
+
expect(snapshot.goals.map((item) => item.id)).toEqual(["G003"]);
|
|
92
|
+
expect(snapshot.goals[0]).toEqual(goals[2]);
|
|
93
|
+
expect(snapshot.goalIds).toEqual(["G001", "G002", "G003", "G004"]);
|
|
94
|
+
expect(snapshot.goalCount).toBe(4);
|
|
95
|
+
expect(snapshot.updatedAt).toBe(NOW);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("omits the activeGoalId key entirely when the plan has none", () => {
|
|
99
|
+
// when
|
|
100
|
+
const snapshot = buildSteeringPlanSnapshot(plan([goal("G001")]), new Set(["G001"]));
|
|
101
|
+
|
|
102
|
+
// then
|
|
103
|
+
expect("activeGoalId" in snapshot).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("carries activeGoalId when the plan has one", () => {
|
|
107
|
+
// when
|
|
108
|
+
const snapshot = buildSteeringPlanSnapshot(plan([goal("G001")], { activeGoalId: "G001" }), new Set());
|
|
109
|
+
|
|
110
|
+
// then
|
|
111
|
+
expect(snapshot.activeGoalId).toBe("G001");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("keeps plan order for changed goals regardless of set insertion order", () => {
|
|
115
|
+
// given
|
|
116
|
+
const source = plan([goal("G001"), goal("G002"), goal("G003")]);
|
|
117
|
+
|
|
118
|
+
// when
|
|
119
|
+
const snapshot = buildSteeringPlanSnapshot(source, new Set(["G003", "G001"]));
|
|
120
|
+
|
|
121
|
+
// then
|
|
122
|
+
expect(snapshot.goals.map((item) => item.id)).toEqual(["G001", "G003"]);
|
|
123
|
+
});
|
|
124
|
+
});
|