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
|
@@ -6,3 +6,9 @@ export declare function readUlwLoopPlan(repoRoot: string, scope?: UlwLoopScope):
|
|
|
6
6
|
export declare function writePlan(repoRoot: string, plan: UlwLoopPlan, scope?: UlwLoopScope): Promise<void>;
|
|
7
7
|
export declare function appendLedger(repoRoot: string, entry: UlwLoopLedgerEntry, scope?: UlwLoopScope): Promise<void>;
|
|
8
8
|
export declare function readSteeringLedgerEntries(repoRoot: string, scope?: UlwLoopScope): Promise<UlwLoopLedgerEntry[]>;
|
|
9
|
+
/**
|
|
10
|
+
* First accepted steering entry matching an idempotency key/prompt signature.
|
|
11
|
+
* A cheap substring probe on the raw line skips JSON.parse for the vast
|
|
12
|
+
* majority of entries, so dedup stays flat even on legacy multi-MB ledgers.
|
|
13
|
+
*/
|
|
14
|
+
export declare function findAcceptedSteeringLedgerEntry(repoRoot: string, key: string, scope?: UlwLoopScope): Promise<UlwLoopLedgerEntry | undefined>;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { createReadStream } from "node:fs";
|
|
1
2
|
import { appendFile, mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
3
|
+
import { createInterface } from "node:readline";
|
|
2
4
|
import { aggregateCodexObjectiveForScope } from "./goal-status.js";
|
|
3
5
|
import { repoRelative, ulwLoopDir, ulwLoopGoalsPath, ulwLoopLedgerPath, ulwLoopRelativeDir, } from "./paths.js";
|
|
4
6
|
import { iso, ULW_LOOP_DIR, ULW_LOOP_GOALS, ULW_LOOP_LEDGER, UlwLoopError } from "./types.js";
|
|
@@ -20,9 +22,17 @@ export async function withUlwLoopMutationLock(repoRoot, scopeOrFn, maybeFn) {
|
|
|
20
22
|
if (fn === undefined)
|
|
21
23
|
throw new UlwLoopError("Missing ulw-loop mutation body.", "ULW_LOOP_LOCK_BODY_MISSING");
|
|
22
24
|
const lockKey = `${repoRoot}\0${ulwLoopRelativeDir(scope)}`;
|
|
23
|
-
const prior = locks.get(lockKey) ?? Promise.resolve();
|
|
25
|
+
const prior = locks.get(lockKey) ?? Promise.resolve(undefined);
|
|
24
26
|
const run = prior.then(fn, fn);
|
|
25
|
-
|
|
27
|
+
// The stored gate resolves to undefined so the map never retains fn's result
|
|
28
|
+
// (plans/audits), and it removes itself once no newer waiter replaced it —
|
|
29
|
+
// otherwise a long-lived host leaks one entry per (repo, scope) forever.
|
|
30
|
+
const gate = run.then(() => undefined, () => undefined);
|
|
31
|
+
locks.set(lockKey, gate);
|
|
32
|
+
void gate.then(() => {
|
|
33
|
+
if (locks.get(lockKey) === gate)
|
|
34
|
+
locks.delete(lockKey);
|
|
35
|
+
});
|
|
26
36
|
return run;
|
|
27
37
|
}
|
|
28
38
|
export async function readUlwLoopPlan(repoRoot, scope) {
|
|
@@ -69,21 +79,57 @@ export async function appendLedger(repoRoot, entry, scope) {
|
|
|
69
79
|
await mkdir(ulwLoopDir(repoRoot, scope), { recursive: true });
|
|
70
80
|
await appendFile(ulwLoopLedgerPath(repoRoot, scope), `${JSON.stringify(entry)}\n`, "utf8");
|
|
71
81
|
}
|
|
72
|
-
|
|
73
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Streams raw ledger lines without materializing the file. Real ledgers reach
|
|
84
|
+
* many MB (legacy entries embedded full-plan snapshots), so `readFile` here
|
|
85
|
+
* ballooned every steer/dedup path; line-at-a-time keeps memory O(longest line).
|
|
86
|
+
*/
|
|
87
|
+
async function* ledgerLines(repoRoot, scope) {
|
|
88
|
+
const stream = createReadStream(ulwLoopLedgerPath(repoRoot, scope), { encoding: "utf8" });
|
|
89
|
+
const lines = createInterface({ input: stream, crlfDelay: Number.POSITIVE_INFINITY });
|
|
74
90
|
try {
|
|
75
|
-
|
|
91
|
+
for await (const line of lines) {
|
|
92
|
+
if (line.trim().length > 0)
|
|
93
|
+
yield line;
|
|
94
|
+
}
|
|
76
95
|
}
|
|
77
96
|
catch (error) {
|
|
78
|
-
if (hasCode(error, "ENOENT"))
|
|
79
|
-
|
|
80
|
-
|
|
97
|
+
if (!hasCode(error, "ENOENT"))
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
lines.close();
|
|
102
|
+
stream.destroy();
|
|
81
103
|
}
|
|
104
|
+
}
|
|
105
|
+
export async function readSteeringLedgerEntries(repoRoot, scope) {
|
|
82
106
|
const entries = [];
|
|
83
|
-
for (const line of
|
|
107
|
+
for await (const line of ledgerLines(repoRoot, scope)) {
|
|
84
108
|
const entry = JSON.parse(line);
|
|
85
109
|
if (isSteeringKind(entry.kind))
|
|
86
110
|
entries.push(entry);
|
|
87
111
|
}
|
|
88
112
|
return entries;
|
|
89
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* First accepted steering entry matching an idempotency key/prompt signature.
|
|
116
|
+
* A cheap substring probe on the raw line skips JSON.parse for the vast
|
|
117
|
+
* majority of entries, so dedup stays flat even on legacy multi-MB ledgers.
|
|
118
|
+
*/
|
|
119
|
+
export async function findAcceptedSteeringLedgerEntry(repoRoot, key, scope) {
|
|
120
|
+
const probe = JSON.stringify(key);
|
|
121
|
+
for await (const line of ledgerLines(repoRoot, scope)) {
|
|
122
|
+
if (!line.includes(probe))
|
|
123
|
+
continue;
|
|
124
|
+
const entry = JSON.parse(line);
|
|
125
|
+
if (!isSteeringKind(entry.kind))
|
|
126
|
+
continue;
|
|
127
|
+
if (entry.steering?.invariant.accepted !== true)
|
|
128
|
+
continue;
|
|
129
|
+
if (entry.idempotencyKey === key ||
|
|
130
|
+
entry.steering.idempotencyKey === key ||
|
|
131
|
+
entry.steering.promptSignature === key)
|
|
132
|
+
return entry;
|
|
133
|
+
}
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { UlwLoopPlan } from "./domain-types.js";
|
|
2
|
+
import type { UlwLoopSteeringPlanSnapshot } from "./steering-types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Compact before/after snapshots for steering ledger entries.
|
|
5
|
+
*
|
|
6
|
+
* Ledger entries used to embed the FULL plan (every goal, criterion, and
|
|
7
|
+
* evidence string) twice per audit and twice more at the entry top level.
|
|
8
|
+
* That made each accepted steer O(plan size) on disk, so the ledger grew
|
|
9
|
+
* quadratically over a run, and every dedup scan re-hydrated all of it into
|
|
10
|
+
* memory. A snapshot instead records plan-level counters plus only the goals
|
|
11
|
+
* the mutation actually touched, keeping each entry O(changed goals).
|
|
12
|
+
*/
|
|
13
|
+
export declare function buildSteeringPlanSnapshot(plan: UlwLoopPlan, changedGoalIds: ReadonlySet<string>): UlwLoopSteeringPlanSnapshot;
|
|
14
|
+
/** Ids of goals that differ between two plans, including added or removed goals. */
|
|
15
|
+
export declare function changedGoalIdsBetween(before: UlwLoopPlan, after: UlwLoopPlan): Set<string>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compact before/after snapshots for steering ledger entries.
|
|
3
|
+
*
|
|
4
|
+
* Ledger entries used to embed the FULL plan (every goal, criterion, and
|
|
5
|
+
* evidence string) twice per audit and twice more at the entry top level.
|
|
6
|
+
* That made each accepted steer O(plan size) on disk, so the ledger grew
|
|
7
|
+
* quadratically over a run, and every dedup scan re-hydrated all of it into
|
|
8
|
+
* memory. A snapshot instead records plan-level counters plus only the goals
|
|
9
|
+
* the mutation actually touched, keeping each entry O(changed goals).
|
|
10
|
+
*/
|
|
11
|
+
export function buildSteeringPlanSnapshot(plan, changedGoalIds) {
|
|
12
|
+
const snapshot = {
|
|
13
|
+
updatedAt: plan.updatedAt,
|
|
14
|
+
goalCount: plan.goals.length,
|
|
15
|
+
goalIds: plan.goals.map((goal) => goal.id),
|
|
16
|
+
goals: plan.goals.filter((goal) => changedGoalIds.has(goal.id)),
|
|
17
|
+
};
|
|
18
|
+
return plan.activeGoalId === undefined ? snapshot : { ...snapshot, activeGoalId: plan.activeGoalId };
|
|
19
|
+
}
|
|
20
|
+
/** Ids of goals that differ between two plans, including added or removed goals. */
|
|
21
|
+
export function changedGoalIdsBetween(before, after) {
|
|
22
|
+
const beforeById = new Map(before.goals.map((goal) => [goal.id, goal]));
|
|
23
|
+
const changed = new Set();
|
|
24
|
+
for (const goal of after.goals) {
|
|
25
|
+
const prior = beforeById.get(goal.id);
|
|
26
|
+
if (prior === undefined || JSON.stringify(prior) !== JSON.stringify(goal))
|
|
27
|
+
changed.add(goal.id);
|
|
28
|
+
beforeById.delete(goal.id);
|
|
29
|
+
}
|
|
30
|
+
for (const id of beforeById.keys())
|
|
31
|
+
changed.add(id);
|
|
32
|
+
return changed;
|
|
33
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { UlwLoopSteeringMutationKind, UlwLoopSteeringSource } from "./constants.js";
|
|
2
|
-
import type { UlwLoopPlan } from "./domain-types.js";
|
|
2
|
+
import type { UlwLoopItem, UlwLoopPlan } from "./domain-types.js";
|
|
3
3
|
export interface UlwLoopSteeringInvariantResult {
|
|
4
4
|
accepted: boolean;
|
|
5
5
|
structuralInvariantAccepted: boolean;
|
|
@@ -39,13 +39,20 @@ export interface UlwLoopSteeringProposal {
|
|
|
39
39
|
idempotencyKey?: string;
|
|
40
40
|
now?: Date;
|
|
41
41
|
}
|
|
42
|
+
export interface UlwLoopSteeringPlanSnapshot {
|
|
43
|
+
readonly updatedAt: string;
|
|
44
|
+
readonly activeGoalId?: string;
|
|
45
|
+
readonly goalCount: number;
|
|
46
|
+
readonly goalIds: readonly string[];
|
|
47
|
+
readonly goals: readonly UlwLoopItem[];
|
|
48
|
+
}
|
|
42
49
|
export interface UlwLoopSteeringAudit {
|
|
43
50
|
kind: UlwLoopSteeringMutationKind;
|
|
44
51
|
source: UlwLoopSteeringSource;
|
|
45
52
|
targetGoalIds: string[];
|
|
46
53
|
criterionId?: string;
|
|
47
|
-
before?:
|
|
48
|
-
after?:
|
|
54
|
+
before?: UlwLoopSteeringPlanSnapshot;
|
|
55
|
+
after?: UlwLoopSteeringPlanSnapshot;
|
|
49
56
|
evidence: string;
|
|
50
57
|
rationale: string;
|
|
51
58
|
invariant: UlwLoopSteeringInvariantResult;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// biome-ignore-all format: compact steering module must stay below the 240 pure-LOC budget
|
|
2
2
|
import { isUlwLoopDone } from "./goal-status.js";
|
|
3
3
|
import { seedDefaultSuccessCriteria } from "./plan-crud.js";
|
|
4
|
-
import { appendLedger,
|
|
4
|
+
import { appendLedger, findAcceptedSteeringLedgerEntry, readUlwLoopPlan, withUlwLoopMutationLock, writePlan } from "./plan-io.js";
|
|
5
|
+
import { buildSteeringPlanSnapshot, changedGoalIdsBetween } from "./steering-snapshot.js";
|
|
5
6
|
import { iso, ULW_LOOP_STEERING_MUTATION_KINDS, ULW_LOOP_SUCCESS_CRITERION_USER_MODELS } from "./types.js";
|
|
6
7
|
const SOURCES = ["user_prompt_submit", "finding", "cli"];
|
|
7
8
|
const PROTECTED = new Set(["aggregateCompletion", "codexObjective", "codexObjectiveAliases", "originalConstraints", "qualityGate", "status", "completedAt", "completionStatus"]);
|
|
@@ -260,15 +261,22 @@ export async function steerUlwLoop(repoRoot, proposal, scope) {
|
|
|
260
261
|
return withUlwLoopMutationLock(repoRoot, scope, async () => {
|
|
261
262
|
const plan = await readUlwLoopPlan(repoRoot, scope);
|
|
262
263
|
const key = proposal.idempotencyKey ?? proposal.promptSignature;
|
|
263
|
-
const prior = key === undefined ? undefined :
|
|
264
|
-
if (prior?.steering !== undefined)
|
|
265
|
-
|
|
264
|
+
const prior = key === undefined ? undefined : await findAcceptedSteeringLedgerEntry(repoRoot, key, scope);
|
|
265
|
+
if (prior?.steering !== undefined) {
|
|
266
|
+
// Legacy entries embedded full-plan before/after snapshots; never
|
|
267
|
+
// re-surface those multi-MB payloads on the dedup path.
|
|
268
|
+
const { before: _before, after: _after, ...compactPrior } = prior.steering;
|
|
269
|
+
return { plan, accepted: true, audit: { ...compactPrior, deduped: true }, rejectedReasons: [], deduped: true };
|
|
270
|
+
}
|
|
266
271
|
const audit = validateUlwLoopSteeringProposal(plan, proposal);
|
|
267
272
|
const accepted = audit.invariant.accepted;
|
|
268
273
|
const next = accepted ? applySteeringMutation(plan, proposal, audit) : plan;
|
|
269
|
-
const finalAudit = { ...audit
|
|
270
|
-
if (accepted)
|
|
271
|
-
|
|
274
|
+
const finalAudit = { ...audit };
|
|
275
|
+
if (accepted) {
|
|
276
|
+
const changed = changedGoalIdsBetween(plan, next);
|
|
277
|
+
finalAudit.before = buildSteeringPlanSnapshot(plan, changed);
|
|
278
|
+
finalAudit.after = buildSteeringPlanSnapshot(next, changed);
|
|
279
|
+
}
|
|
272
280
|
if (accepted)
|
|
273
281
|
await writePlan(repoRoot, next, scope);
|
|
274
282
|
await appendLedger(repoRoot, ledgerEntry(proposal, finalAudit, proposal.now?.toISOString() ?? iso()), scope);
|
|
@@ -284,9 +292,5 @@ function ledgerEntry(proposal, audit, at) {
|
|
|
284
292
|
entry.criterionId = proposal.criterionId;
|
|
285
293
|
if (proposal.idempotencyKey !== undefined)
|
|
286
294
|
entry.idempotencyKey = proposal.idempotencyKey;
|
|
287
|
-
if (audit.before !== undefined)
|
|
288
|
-
entry.before = audit.before;
|
|
289
|
-
if (audit.after !== undefined)
|
|
290
|
-
entry.after = audit.after;
|
|
291
295
|
return entry;
|
|
292
296
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { type UltraworkAdditionalContextOptions } from "./ultrawork-skill-pointer.js";
|
|
1
2
|
export interface UltraworkDirectiveInput {
|
|
2
3
|
readonly prompt: string;
|
|
3
4
|
readonly transcript_path?: string | null;
|
|
4
5
|
}
|
|
5
|
-
export declare function buildUltraworkDirectiveOutput(input: UltraworkDirectiveInput): string;
|
|
6
|
+
export declare function buildUltraworkDirectiveOutput(input: UltraworkDirectiveInput, options?: UltraworkAdditionalContextOptions): string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
|
|
2
|
+
import { buildUltraworkAdditionalContext } from "./ultrawork-skill-pointer.js";
|
|
3
3
|
const ULTRAWORK_CURRENT_PROMPT_PATTERN = /(?:ultrawork|ulw)/i;
|
|
4
4
|
const ULTRAWORK_DIRECTIVE_MARKER = "<ultrawork-mode>";
|
|
5
5
|
const TRANSCRIPT_SEARCH_BYTES = 512_000;
|
|
@@ -12,14 +12,16 @@ const CONTEXT_PRESSURE_MARKERS = [
|
|
|
12
12
|
"your input exceeds the context window",
|
|
13
13
|
"long threads and multiple compactions",
|
|
14
14
|
];
|
|
15
|
-
export function buildUltraworkDirectiveOutput(input) {
|
|
15
|
+
export function buildUltraworkDirectiveOutput(input, options = {}) {
|
|
16
16
|
if (isContextPressureRecoveryPrompt(input.prompt))
|
|
17
17
|
return "";
|
|
18
18
|
if (hasUltraworkDirectiveAlreadyInTranscript(input.transcript_path))
|
|
19
19
|
return "";
|
|
20
20
|
if (isContextPressureTranscript(input.transcript_path))
|
|
21
21
|
return "";
|
|
22
|
-
return isUltraworkPrompt(input.prompt)
|
|
22
|
+
return isUltraworkPrompt(input.prompt)
|
|
23
|
+
? formatAdditionalContextOutput(buildUltraworkAdditionalContext(options))
|
|
24
|
+
: "";
|
|
23
25
|
}
|
|
24
26
|
function hasUltraworkDirectiveAlreadyInTranscript(transcriptPath) {
|
|
25
27
|
if (transcriptPath === undefined || transcriptPath === null)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const ULTRAWORK_SKILL_POINTER_TEMPLATE = "<ultrawork-mode>\nULTRAWORK MODE IS ACTIVE FOR THIS TASK.\n\nMANDATORY BOOTSTRAP: do all three steps, in order, before anything else.\n\n1. First user-visible line this turn MUST be exactly:\n`ULTRAWORK MODE ENABLED!`\n\n2. Call `create_goal` NOW with `objective` set to the user's request.\nSend `objective` only: no `status`, no budget fields. If the\n`create_goal` tool is unavailable, open your reply with a binding\n`# Goal` block instead. Never skip this step.\n\n3. Read the FULL ultrawork directive NOW, before any other tool call,\nplan, or edit. It is the `ultrawork` skill, stored at:\n\n{{ULTRAWORK_SKILL_PATH}}\n\nRead the whole file. If a read result comes back truncated, keep\nreading the remaining line ranges until you have seen every line.\nEvery rule in that file is binding for this entire task: no\ncompromise, no summarizing from memory, no skipping. If the file does\nnot exist, tell the user the omo ultrawork skill is missing and\ncontinue with steps 1 and 2 plus evidence-bound execution.\n\nDo not start the requested work until all three steps are complete.\n</ultrawork-mode>\n";
|
|
2
|
+
export interface UltraworkAdditionalContextOptions {
|
|
3
|
+
readonly skillFilePath?: string | null;
|
|
4
|
+
}
|
|
5
|
+
export declare function resolveUltraworkSkillFilePath(): string;
|
|
6
|
+
export declare function buildUltraworkSkillPointer(skillFilePath: string): string;
|
|
7
|
+
export declare function buildUltraworkAdditionalContext(options?: UltraworkAdditionalContextOptions): string;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
export const ULTRAWORK_SKILL_POINTER_TEMPLATE = `<ultrawork-mode>
|
|
4
|
+
ULTRAWORK MODE IS ACTIVE FOR THIS TASK.
|
|
5
|
+
|
|
6
|
+
MANDATORY BOOTSTRAP: do all three steps, in order, before anything else.
|
|
7
|
+
|
|
8
|
+
1. First user-visible line this turn MUST be exactly:
|
|
9
|
+
\`ULTRAWORK MODE ENABLED!\`
|
|
10
|
+
|
|
11
|
+
2. Call \`create_goal\` NOW with \`objective\` set to the user's request.
|
|
12
|
+
Send \`objective\` only: no \`status\`, no budget fields. If the
|
|
13
|
+
\`create_goal\` tool is unavailable, open your reply with a binding
|
|
14
|
+
\`# Goal\` block instead. Never skip this step.
|
|
15
|
+
|
|
16
|
+
3. Read the FULL ultrawork directive NOW, before any other tool call,
|
|
17
|
+
plan, or edit. It is the \`ultrawork\` skill, stored at:
|
|
18
|
+
|
|
19
|
+
{{ULTRAWORK_SKILL_PATH}}
|
|
20
|
+
|
|
21
|
+
Read the whole file. If a read result comes back truncated, keep
|
|
22
|
+
reading the remaining line ranges until you have seen every line.
|
|
23
|
+
Every rule in that file is binding for this entire task: no
|
|
24
|
+
compromise, no summarizing from memory, no skipping. If the file does
|
|
25
|
+
not exist, tell the user the omo ultrawork skill is missing and
|
|
26
|
+
continue with steps 1 and 2 plus evidence-bound execution.
|
|
27
|
+
|
|
28
|
+
Do not start the requested work until all three steps are complete.
|
|
29
|
+
</ultrawork-mode>
|
|
30
|
+
`;
|
|
31
|
+
const ULTRAWORK_SKILL_PATH_PLACEHOLDER = "{{ULTRAWORK_SKILL_PATH}}";
|
|
32
|
+
const ULTRAWORK_SKILL_FILE_URL = new URL("../../../skills/ultrawork/SKILL.md", import.meta.url);
|
|
33
|
+
const ULTRAWORK_DIRECTIVE = readFileSync(new URL("../directive.md", import.meta.url), "utf8");
|
|
34
|
+
export function resolveUltraworkSkillFilePath() {
|
|
35
|
+
return fileURLToPath(ULTRAWORK_SKILL_FILE_URL);
|
|
36
|
+
}
|
|
37
|
+
export function buildUltraworkSkillPointer(skillFilePath) {
|
|
38
|
+
return ULTRAWORK_SKILL_POINTER_TEMPLATE.replace(ULTRAWORK_SKILL_PATH_PLACEHOLDER, skillFilePath);
|
|
39
|
+
}
|
|
40
|
+
export function buildUltraworkAdditionalContext(options = {}) {
|
|
41
|
+
const skillFilePath = options.skillFilePath === undefined ? resolveUltraworkSkillFilePath() : options.skillFilePath;
|
|
42
|
+
if (skillFilePath !== null && existsSync(skillFilePath)) {
|
|
43
|
+
return buildUltraworkSkillPointer(skillFilePath);
|
|
44
|
+
}
|
|
45
|
+
return ULTRAWORK_DIRECTIVE;
|
|
46
|
+
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"type": "command",
|
|
8
8
|
"command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook user-prompt-submit --with-ultrawork",
|
|
9
9
|
"timeout": 10,
|
|
10
|
-
"statusMessage": "(OmO) Checking Ulw-Loop Steering"
|
|
10
|
+
"statusMessage": "(OmO 4.16.1) Checking Ulw-Loop Steering"
|
|
11
11
|
}
|
|
12
12
|
]
|
|
13
13
|
}
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"type": "command",
|
|
21
21
|
"command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook pre-tool-use",
|
|
22
22
|
"timeout": 5,
|
|
23
|
-
"statusMessage": "(OmO) Enforcing Unlimited Ulw-Loop Budget"
|
|
23
|
+
"statusMessage": "(OmO 4.16.1) Enforcing Unlimited Ulw-Loop Budget"
|
|
24
24
|
}
|
|
25
25
|
]
|
|
26
26
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@code-yeongyu/codex-ulw-loop",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.16.1",
|
|
4
4
|
"description": "Codex plugin: durable repo-native multi-goal orchestration with embedded success criteria and observable evidence audit.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "npm@11.12.1",
|
|
@@ -22,7 +22,7 @@ This skill is intentionally compact. The full workflow lives in `references/full
|
|
|
22
22
|
- Use the ulw-loop CLI state under `.omo/ulw-loop`; do not hand-edit goal state.
|
|
23
23
|
- After any compaction or context loss, re-read brief + goals + ledger FIRST plus `omo ulw-loop status --json`, then resume; never re-plan from scratch.
|
|
24
24
|
- If `omo ulw-loop create-goals` says the existing aggregate is already complete, start unrelated new work with a fresh `--session-id <new-id>` instead of steering or forcing the completed default state. Use `--force` only to intentionally overwrite completed evidence.
|
|
25
|
-
- Every success criterion needs observable evidence from a real surface: a channel (
|
|
25
|
+
- Every success criterion needs observable evidence from a real surface: a channel (terminal/TUI via the xterm.js web terminal, HTTP, browser, computer-use) or, for CLI- or data-shaped criteria, an auxiliary surface (CLI stdout, DB diff, parsed config dump).
|
|
26
26
|
- Record evidence through the CLI only after cleanup receipts are available.
|
|
27
27
|
- Delegate code edits, test writes, fixes, and QA execution to right-sized Codex subagents when the workflow requires it.
|
|
28
28
|
- Every `multi_agent_v1.spawn_agent` message starts with `TASK:`, then names `DELIVERABLE`, `SCOPE`, and `VERIFY`; put role and specialty instructions inside `message`; use `fork_context: false` unless full history is truly required.
|
package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md
CHANGED
|
@@ -19,16 +19,16 @@ Audit each pass, fail, block, steering change, and checkpoint in `.omo/ulw-loop/
|
|
|
19
19
|
Run each criterion's real-surface proof yourself through the channel that faithfully exercises it; capture the artifact before recording PASS.
|
|
20
20
|
|
|
21
21
|
1. **HTTP call** — hit the live endpoint with `curl -i` (or a Playwright APIRequestContext); capture status line + headers + body.
|
|
22
|
-
2. **
|
|
22
|
+
2. **Terminal / TUI** - prove it through the xterm.js web terminal; tmux `send-keys` is fine for a boot smoke, but NEVER `tmux capture-pane` for color/layout/CJK evidence (it degrades truecolor).
|
|
23
23
|
3. **Browser use** — in Codex, use `browser:control-in-app-browser` first when available and the scenario does not need an authenticated or persistent user browser profile. Otherwise use Chrome to drive the REAL page; if unavailable, use agent-browser. Capture action log + screenshot path. Never downgrade a browser-facing criterion.
|
|
24
24
|
4. **Computer use** — for desktop/GUI apps, drive the running app via OS automation (computer-use, AppleScript, xdotool, etc.); capture action log + screenshot.
|
|
25
25
|
|
|
26
|
-
For TUI visual QA,
|
|
27
|
-
screenshot
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
26
|
+
For TUI visual QA, render the terminal through the real xterm.js web terminal and
|
|
27
|
+
screenshot it - NEVER a `tmux capture-pane` dump (it degrades color and wide-glyph
|
|
28
|
+
width). In this repo run `node script/qa/web-terminal-visual-qa.mjs --command
|
|
29
|
+
"<cmd>" --input "{Enter}" --evidence-dir <dir>` (live pty + xterm.js in Chrome;
|
|
30
|
+
`--from-file` replays a raw stream) and record `terminal.png`, `terminal.txt`, and
|
|
31
|
+
`metadata.json`. Mandatory when a PR or review must inspect the terminal screen.
|
|
32
32
|
|
|
33
33
|
Auxiliary surfaces (CLI stdout / DB state diff / parsed config dump) are first-class evidence for CLI- or data-shaped criteria; use a channel scenario when the behavior is user-facing. `--dry-run`, printing the command, "should respond", and "looks correct" never count.
|
|
34
34
|
|
|
@@ -39,8 +39,8 @@ export async function ulwLoopCommand(argv: readonly string[]): Promise<number> {
|
|
|
39
39
|
const rest = argv.slice(1);
|
|
40
40
|
const repoRoot = process.cwd();
|
|
41
41
|
const json = hasFlag(rest, "--json");
|
|
42
|
-
const scope = commandScope(rest);
|
|
43
42
|
try {
|
|
43
|
+
const scope = commandScope(rest);
|
|
44
44
|
if (!isUlwLoopSubcommand(command)) {
|
|
45
45
|
if (json) {
|
|
46
46
|
printJsonError(
|
|
@@ -94,7 +94,22 @@ function unhandledSubcommand(command: never): never {
|
|
|
94
94
|
throw new UlwLoopError(`Unhandled ulw-loop subcommand: ${String(command)}.`, "ULW_LOOP_SUBCOMMAND_UNHANDLED");
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
const SESSION_ID_FLAG = "--session-id";
|
|
98
|
+
|
|
99
|
+
function sessionIdFlagPresent(argv: readonly string[]): boolean {
|
|
100
|
+
return hasFlag(argv, SESSION_ID_FLAG) || argv.some((arg) => arg.startsWith(`${SESSION_ID_FLAG}=`));
|
|
101
|
+
}
|
|
102
|
+
|
|
97
103
|
function commandScope(argv: readonly string[]): UlwLoopScope | undefined {
|
|
98
|
-
|
|
104
|
+
if (sessionIdFlagPresent(argv)) {
|
|
105
|
+
const sessionId = readValue(argv, SESSION_ID_FLAG)?.trim();
|
|
106
|
+
if (!sessionId) {
|
|
107
|
+
throw new UlwLoopError(`${SESSION_ID_FLAG} requires a non-empty value.`, "ULW_LOOP_SESSION_ID_REQUIRED", {
|
|
108
|
+
details: { flag: SESSION_ID_FLAG },
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return { sessionId };
|
|
112
|
+
}
|
|
113
|
+
const sessionId = resolveUlwLoopSessionIdFromEnv();
|
|
99
114
|
return sessionId === null ? undefined : { sessionId };
|
|
100
115
|
}
|
|
@@ -73,7 +73,8 @@ function model(value: string | undefined): UlwLoopSuccessCriterionUserModel | un
|
|
|
73
73
|
function neverKind(kind: never): never { return fail(`Unsupported steering kind: ${String(kind)}.`, "ULW_LOOP_STEERING_KIND_UNSUPPORTED", { kind }); }
|
|
74
74
|
|
|
75
75
|
export async function parseSteeringProposal(argv: readonly string[]): Promise<CliSteeringProposal> {
|
|
76
|
-
const kind = parseSteeringKind(argv); const source = parseSteeringSource(argv); const
|
|
76
|
+
const kind = parseSteeringKind(argv); const source = parseSteeringSource(argv); const idempotencyKey = text(readValue(argv, "--idempotency-key"), "--idempotency-key");
|
|
77
|
+
const base = { kind, source, evidence: required(argv, "--evidence"), rationale: required(argv, "--rationale"), ...(idempotencyKey === undefined ? {} : { idempotencyKey }) };
|
|
77
78
|
switch (kind) {
|
|
78
79
|
case "add_subgoal": return normalizeSteeringProposal({ ...base, title: required(argv, "--title"), objective: required(argv, "--objective") });
|
|
79
80
|
case "split_subgoal": { const goalId = requiredGoal(argv); return normalizeSteeringProposal({ ...base, goalId, targetGoalId: goalId, childGoals: await children(argv, "--children", true) }); }
|
|
@@ -15,6 +15,7 @@ export interface UserPromptSubmitPayload {
|
|
|
15
15
|
|
|
16
16
|
export interface UserPromptSubmitHookOptions {
|
|
17
17
|
readonly includeUltraworkDirective?: boolean;
|
|
18
|
+
readonly ultraworkSkillFilePath?: string | null;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export interface PreToolUsePayload {
|
|
@@ -74,7 +75,10 @@ export async function applyUserPromptUlwLoopSteering(
|
|
|
74
75
|
const proposal = parseUlwLoopSteeringDirective(payload.prompt);
|
|
75
76
|
if (proposal === null) {
|
|
76
77
|
if (hasSteeringDirectiveMarker(payload.prompt)) return "";
|
|
77
|
-
|
|
78
|
+
if (!options.includeUltraworkDirective) return "";
|
|
79
|
+
return options.ultraworkSkillFilePath === undefined
|
|
80
|
+
? buildUltraworkDirectiveOutput(payload)
|
|
81
|
+
: buildUltraworkDirectiveOutput(payload, { skillFilePath: options.ultraworkSkillFilePath });
|
|
78
82
|
}
|
|
79
83
|
const result = await steerUlwLoop(payload.cwd, proposal, payloadScope(payload));
|
|
80
84
|
if (!result.accepted) return "";
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { createReadStream } from "node:fs";
|
|
1
2
|
import { appendFile, mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
3
|
+
import { createInterface } from "node:readline";
|
|
2
4
|
|
|
3
5
|
import { aggregateCodexObjectiveForScope } from "./goal-status.js";
|
|
4
6
|
import {
|
|
@@ -14,7 +16,7 @@ import { iso, ULW_LOOP_DIR, ULW_LOOP_GOALS, ULW_LOOP_LEDGER, UlwLoopError } from
|
|
|
14
16
|
|
|
15
17
|
const LEGACY_OBJECTIVE_PREFIX = `Complete all ulw-loop stories in ${ULW_LOOP_DIR}/${ULW_LOOP_GOALS}: `;
|
|
16
18
|
const LEGACY_OBJECTIVE = `Complete all ulw-loop stories listed in ${ULW_LOOP_DIR}/${ULW_LOOP_GOALS}. Use ${ULW_LOOP_DIR}/${ULW_LOOP_LEDGER} as the durable audit trail.`;
|
|
17
|
-
const locks = new Map<string, Promise<
|
|
19
|
+
const locks = new Map<string, Promise<undefined>>();
|
|
18
20
|
|
|
19
21
|
function hasCode(error: unknown, code: string): boolean {
|
|
20
22
|
return error instanceof Error && "code" in error && error.code === code;
|
|
@@ -43,12 +45,19 @@ export async function withUlwLoopMutationLock<T>(
|
|
|
43
45
|
const fn = typeof scopeOrFn === "function" ? scopeOrFn : maybeFn;
|
|
44
46
|
if (fn === undefined) throw new UlwLoopError("Missing ulw-loop mutation body.", "ULW_LOOP_LOCK_BODY_MISSING");
|
|
45
47
|
const lockKey = `${repoRoot}\0${ulwLoopRelativeDir(scope)}`;
|
|
46
|
-
const prior = locks.get(lockKey) ?? Promise.resolve();
|
|
48
|
+
const prior = locks.get(lockKey) ?? Promise.resolve(undefined);
|
|
47
49
|
const run = prior.then(fn, fn);
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
// The stored gate resolves to undefined so the map never retains fn's result
|
|
51
|
+
// (plans/audits), and it removes itself once no newer waiter replaced it —
|
|
52
|
+
// otherwise a long-lived host leaks one entry per (repo, scope) forever.
|
|
53
|
+
const gate: Promise<undefined> = run.then(
|
|
54
|
+
() => undefined,
|
|
55
|
+
() => undefined,
|
|
51
56
|
);
|
|
57
|
+
locks.set(lockKey, gate);
|
|
58
|
+
void gate.then(() => {
|
|
59
|
+
if (locks.get(lockKey) === gate) locks.delete(lockKey);
|
|
60
|
+
});
|
|
52
61
|
return run;
|
|
53
62
|
}
|
|
54
63
|
|
|
@@ -107,18 +116,57 @@ export async function appendLedger(repoRoot: string, entry: UlwLoopLedgerEntry,
|
|
|
107
116
|
await appendFile(ulwLoopLedgerPath(repoRoot, scope), `${JSON.stringify(entry)}\n`, "utf8");
|
|
108
117
|
}
|
|
109
118
|
|
|
110
|
-
|
|
111
|
-
|
|
119
|
+
/**
|
|
120
|
+
* Streams raw ledger lines without materializing the file. Real ledgers reach
|
|
121
|
+
* many MB (legacy entries embedded full-plan snapshots), so `readFile` here
|
|
122
|
+
* ballooned every steer/dedup path; line-at-a-time keeps memory O(longest line).
|
|
123
|
+
*/
|
|
124
|
+
async function* ledgerLines(repoRoot: string, scope?: UlwLoopScope): AsyncGenerator<string> {
|
|
125
|
+
const stream = createReadStream(ulwLoopLedgerPath(repoRoot, scope), { encoding: "utf8" });
|
|
126
|
+
const lines = createInterface({ input: stream, crlfDelay: Number.POSITIVE_INFINITY });
|
|
112
127
|
try {
|
|
113
|
-
|
|
128
|
+
for await (const line of lines) {
|
|
129
|
+
if (line.trim().length > 0) yield line;
|
|
130
|
+
}
|
|
114
131
|
} catch (error) {
|
|
115
|
-
if (hasCode(error, "ENOENT"))
|
|
116
|
-
|
|
132
|
+
if (!hasCode(error, "ENOENT")) throw error;
|
|
133
|
+
} finally {
|
|
134
|
+
lines.close();
|
|
135
|
+
stream.destroy();
|
|
117
136
|
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function readSteeringLedgerEntries(repoRoot: string, scope?: UlwLoopScope): Promise<UlwLoopLedgerEntry[]> {
|
|
118
140
|
const entries: UlwLoopLedgerEntry[] = [];
|
|
119
|
-
for (const line of
|
|
141
|
+
for await (const line of ledgerLines(repoRoot, scope)) {
|
|
120
142
|
const entry: UlwLoopLedgerEntry = JSON.parse(line);
|
|
121
143
|
if (isSteeringKind(entry.kind)) entries.push(entry);
|
|
122
144
|
}
|
|
123
145
|
return entries;
|
|
124
146
|
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* First accepted steering entry matching an idempotency key/prompt signature.
|
|
150
|
+
* A cheap substring probe on the raw line skips JSON.parse for the vast
|
|
151
|
+
* majority of entries, so dedup stays flat even on legacy multi-MB ledgers.
|
|
152
|
+
*/
|
|
153
|
+
export async function findAcceptedSteeringLedgerEntry(
|
|
154
|
+
repoRoot: string,
|
|
155
|
+
key: string,
|
|
156
|
+
scope?: UlwLoopScope,
|
|
157
|
+
): Promise<UlwLoopLedgerEntry | undefined> {
|
|
158
|
+
const probe = JSON.stringify(key);
|
|
159
|
+
for await (const line of ledgerLines(repoRoot, scope)) {
|
|
160
|
+
if (!line.includes(probe)) continue;
|
|
161
|
+
const entry: UlwLoopLedgerEntry = JSON.parse(line);
|
|
162
|
+
if (!isSteeringKind(entry.kind)) continue;
|
|
163
|
+
if (entry.steering?.invariant.accepted !== true) continue;
|
|
164
|
+
if (
|
|
165
|
+
entry.idempotencyKey === key ||
|
|
166
|
+
entry.steering.idempotencyKey === key ||
|
|
167
|
+
entry.steering.promptSignature === key
|
|
168
|
+
)
|
|
169
|
+
return entry;
|
|
170
|
+
}
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { UlwLoopItem, UlwLoopPlan } from "./domain-types.js";
|
|
2
|
+
import type { UlwLoopSteeringPlanSnapshot } from "./steering-types.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Compact before/after snapshots for steering ledger entries.
|
|
6
|
+
*
|
|
7
|
+
* Ledger entries used to embed the FULL plan (every goal, criterion, and
|
|
8
|
+
* evidence string) twice per audit and twice more at the entry top level.
|
|
9
|
+
* That made each accepted steer O(plan size) on disk, so the ledger grew
|
|
10
|
+
* quadratically over a run, and every dedup scan re-hydrated all of it into
|
|
11
|
+
* memory. A snapshot instead records plan-level counters plus only the goals
|
|
12
|
+
* the mutation actually touched, keeping each entry O(changed goals).
|
|
13
|
+
*/
|
|
14
|
+
export function buildSteeringPlanSnapshot(
|
|
15
|
+
plan: UlwLoopPlan,
|
|
16
|
+
changedGoalIds: ReadonlySet<string>,
|
|
17
|
+
): UlwLoopSteeringPlanSnapshot {
|
|
18
|
+
const snapshot: UlwLoopSteeringPlanSnapshot = {
|
|
19
|
+
updatedAt: plan.updatedAt,
|
|
20
|
+
goalCount: plan.goals.length,
|
|
21
|
+
goalIds: plan.goals.map((goal) => goal.id),
|
|
22
|
+
goals: plan.goals.filter((goal) => changedGoalIds.has(goal.id)),
|
|
23
|
+
};
|
|
24
|
+
return plan.activeGoalId === undefined ? snapshot : { ...snapshot, activeGoalId: plan.activeGoalId };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Ids of goals that differ between two plans, including added or removed goals. */
|
|
28
|
+
export function changedGoalIdsBetween(before: UlwLoopPlan, after: UlwLoopPlan): Set<string> {
|
|
29
|
+
const beforeById = new Map<string, UlwLoopItem>(before.goals.map((goal) => [goal.id, goal]));
|
|
30
|
+
const changed = new Set<string>();
|
|
31
|
+
for (const goal of after.goals) {
|
|
32
|
+
const prior = beforeById.get(goal.id);
|
|
33
|
+
if (prior === undefined || JSON.stringify(prior) !== JSON.stringify(goal)) changed.add(goal.id);
|
|
34
|
+
beforeById.delete(goal.id);
|
|
35
|
+
}
|
|
36
|
+
for (const id of beforeById.keys()) changed.add(id);
|
|
37
|
+
return changed;
|
|
38
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { UlwLoopSteeringMutationKind, UlwLoopSteeringSource } from "./constants.js";
|
|
2
|
-
import type { UlwLoopPlan } from "./domain-types.js";
|
|
2
|
+
import type { UlwLoopItem, UlwLoopPlan } from "./domain-types.js";
|
|
3
3
|
|
|
4
4
|
export interface UlwLoopSteeringInvariantResult {
|
|
5
5
|
accepted: boolean;
|
|
@@ -44,13 +44,21 @@ export interface UlwLoopSteeringProposal {
|
|
|
44
44
|
now?: Date;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
export interface UlwLoopSteeringPlanSnapshot {
|
|
48
|
+
readonly updatedAt: string;
|
|
49
|
+
readonly activeGoalId?: string;
|
|
50
|
+
readonly goalCount: number;
|
|
51
|
+
readonly goalIds: readonly string[];
|
|
52
|
+
readonly goals: readonly UlwLoopItem[];
|
|
53
|
+
}
|
|
54
|
+
|
|
47
55
|
export interface UlwLoopSteeringAudit {
|
|
48
56
|
kind: UlwLoopSteeringMutationKind;
|
|
49
57
|
source: UlwLoopSteeringSource;
|
|
50
58
|
targetGoalIds: string[];
|
|
51
59
|
criterionId?: string;
|
|
52
|
-
before?:
|
|
53
|
-
after?:
|
|
60
|
+
before?: UlwLoopSteeringPlanSnapshot;
|
|
61
|
+
after?: UlwLoopSteeringPlanSnapshot;
|
|
54
62
|
evidence: string;
|
|
55
63
|
rationale: string;
|
|
56
64
|
invariant: UlwLoopSteeringInvariantResult;
|