oh-my-opencode 4.9.1 → 4.10.0
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/opencode-qa/SKILL.md +1 -0
- package/.agents/skills/opencode-qa/scripts/lib/common.sh +39 -1
- package/.agents/skills/opencode-qa/scripts/lib/fake-openai-branches.mjs +39 -0
- package/.agents/skills/opencode-qa/scripts/lib/fake-openai-events.mjs +106 -0
- package/.agents/skills/opencode-qa/scripts/lib/fake-openai-server.mjs +117 -0
- package/.agents/skills/opencode-qa/scripts/serve-wake-split-probe.sh +716 -0
- package/.agents/skills/tech-debt-audit/SKILL.md +277 -0
- package/.agents/skills/work-with-pr-workspace/iteration-1/eval-5/with_skill/outputs/execution-plan.md +1 -1
- package/.opencode/skills/work-with-pr-workspace/iteration-1/eval-5/with_skill/outputs/execution-plan.md +1 -1
- package/bin/platform.js +5 -0
- package/bin/platform.test.ts +56 -0
- package/dist/agents/atlas/agent.d.ts +4 -3
- package/dist/agents/gpt-apply-patch-guard.d.ts +2 -2
- package/dist/agents/hephaestus/agent.d.ts +5 -0
- package/dist/agents/hephaestus/index.d.ts +1 -1
- package/dist/agents/metis.d.ts +1 -0
- package/dist/agents/prometheus/system-prompt.d.ts +1 -1
- package/dist/agents/sisyphus/kimi-k2-7.d.ts +17 -0
- package/dist/agents/sisyphus-junior/agent.d.ts +1 -1
- package/dist/agents/sisyphus-junior/kimi-k2-7.d.ts +11 -0
- package/dist/agents/types.d.ts +2 -2
- package/dist/cli/doctor/checks/codex-components.d.ts +13 -0
- package/dist/cli/doctor/checks/tui-plugin-config.d.ts +1 -0
- package/dist/cli/doctor/constants.d.ts +1 -1
- package/dist/cli/index.js +32329 -31437
- package/dist/cli/install-codex/codex-cleanup.d.ts +4 -0
- package/dist/cli/install-codex/install-codex-test-fixtures.d.ts +34 -0
- package/dist/cli/install-codex/link-cached-plugin-agents.d.ts +4 -0
- package/dist/cli/model-fallback.d.ts +1 -0
- package/dist/cli/provider-availability.d.ts +2 -0
- package/dist/cli-node/index.js +32329 -31437
- package/dist/config/schema/agent-overrides.d.ts +80 -16
- package/dist/config/schema/experimental.d.ts +1 -1
- package/dist/config/schema/hooks.d.ts +0 -1
- package/dist/config/schema/internal/permission.d.ts +5 -1
- package/dist/config/schema/oh-my-opencode-config.d.ts +76 -16
- package/dist/create-hooks.d.ts +0 -1
- package/dist/features/background-agent/index.d.ts +1 -1
- package/dist/features/background-agent/manager.d.ts +6 -0
- package/dist/features/background-agent/types.d.ts +2 -0
- package/dist/features/claude-code-plugin-loader/types.d.ts +3 -0
- package/dist/features/claude-code-session-state/state.d.ts +1 -0
- package/dist/features/skill-mcp-manager/manager.d.ts +11 -7
- package/dist/features/team-mode/team-mailbox/pending-delivery-recovery.d.ts +31 -0
- package/dist/features/team-mode/team-runtime/delete-team.d.ts +2 -1
- package/dist/features/team-mode/tools/lifecycle-inline-spec.d.ts +2 -2
- package/dist/features/tmux-subagent/stale-tmux-resource-sweeper.d.ts +12 -0
- package/dist/features/tool-metadata-store/store.d.ts +5 -0
- package/dist/hooks/anthropic-context-window-limit-recovery/storage/constants.d.ts +3 -0
- package/dist/hooks/{session-recovery → anthropic-context-window-limit-recovery}/storage/messages-reader.d.ts +1 -1
- package/dist/hooks/{session-recovery → anthropic-context-window-limit-recovery}/storage/part-content.d.ts +1 -1
- package/dist/hooks/{session-recovery → anthropic-context-window-limit-recovery}/storage/parts-reader.d.ts +1 -1
- package/dist/hooks/{session-recovery → anthropic-context-window-limit-recovery/storage}/types.d.ts +0 -13
- package/dist/hooks/auto-update-checker/checker/bundled-version.d.ts +1 -0
- package/dist/hooks/auto-update-checker/checker.d.ts +1 -0
- package/dist/hooks/auto-update-checker/constants.d.ts +3 -3
- package/dist/hooks/auto-update-checker/hook.d.ts +2 -1
- package/dist/hooks/claude-code-hooks/types.d.ts +4 -0
- package/dist/hooks/index.d.ts +0 -1
- package/dist/hooks/team-session-events/team-idle-wake-hint.d.ts +5 -0
- package/dist/index.js +6061 -3714
- package/dist/oh-my-opencode.schema.json +123 -18
- package/dist/plugin/build-team-idle-wake-hint-client.d.ts +2 -0
- package/dist/plugin/event-session-lifecycle.d.ts +0 -3
- package/dist/plugin/hooks/create-continuation-hooks.d.ts +0 -6
- package/dist/plugin/hooks/create-core-hooks.d.ts +0 -1
- package/dist/plugin/hooks/create-session-hooks.d.ts +1 -2
- package/dist/shared/command-executor/execute-hook-command.d.ts +7 -0
- package/dist/shared/internal-initiator-marker.d.ts +7 -0
- package/dist/shared/live-server-route.d.ts +24 -0
- package/dist/shared/plugin-identity.d.ts +2 -2
- package/dist/shared/prompt-async-gate/prompt-message-state.d.ts +1 -0
- package/dist/shared/tmux/tmux-utils/server-health.d.ts +2 -1
- package/dist/shared/tmux/tmux-utils/stale-attach-pane-sweep.d.ts +16 -0
- package/dist/shared/tmux/tmux-utils.d.ts +1 -0
- package/dist/testing/create-plugin-module.d.ts +4 -0
- package/dist/tools/background-task/clients.d.ts +2 -0
- package/dist/tools/background-task/full-session-format.d.ts +1 -0
- package/dist/tools/background-task/types.d.ts +1 -0
- package/dist/tools/delegate-task/sync-prompt-sender.d.ts +1 -1
- package/dist/tools/delegate-task/sync-session-lifecycle.d.ts +2 -1
- package/dist/tools/look-at/look-at-input-preparer.d.ts +6 -2
- package/dist/tools/look-at/look-at-prompt.d.ts +2 -1
- package/dist/tools/look-at/look-at-session-runner.d.ts +3 -4
- package/dist/tools/look-at/types.d.ts +2 -0
- package/dist/tools/session-manager/types.d.ts +1 -0
- package/dist/tools/skill-mcp/types.d.ts +1 -0
- package/package.json +14 -13
- package/packages/ast-grep-mcp/dist/cli.js +50 -17
- package/packages/lsp-daemon/dist/cli.js +8 -5
- package/packages/lsp-daemon/dist/index.js +8 -5
- package/packages/lsp-tools-mcp/dist/lsp/connection.js +1 -1
- package/packages/lsp-tools-mcp/dist/lsp/server-definitions.js +2 -2
- package/packages/lsp-tools-mcp/dist/lsp/transport.d.ts +10 -1
- package/packages/lsp-tools-mcp/dist/lsp/transport.js +6 -3
- package/packages/omo-codex/lazycodex-repository/.github/workflows/pr-source-guidance.yml +11 -12
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +2583 -0
- package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +17 -0
- package/packages/omo-codex/plugin/components/bootstrap/manifests/ast-grep.json +22 -0
- package/packages/omo-codex/plugin/components/bootstrap/manifests/node.json +10 -0
- package/packages/omo-codex/plugin/components/bootstrap/package.json +20 -0
- package/packages/omo-codex/plugin/components/bootstrap/scripts/bootstrap.ps1 +310 -0
- package/packages/omo-codex/plugin/components/bootstrap/scripts/build.mjs +35 -0
- package/packages/omo-codex/plugin/components/bootstrap/scripts/generate-manifests.mjs +115 -0
- package/packages/omo-codex/plugin/components/bootstrap/src/cli.ts +153 -0
- package/packages/omo-codex/plugin/components/bootstrap/src/download.ts +212 -0
- package/packages/omo-codex/plugin/components/bootstrap/src/environment.ts +286 -0
- package/packages/omo-codex/plugin/components/bootstrap/src/hook.ts +108 -0
- package/packages/omo-codex/plugin/components/bootstrap/src/provision.ts +243 -0
- package/packages/omo-codex/plugin/components/bootstrap/src/setup.ts +294 -0
- package/packages/omo-codex/plugin/components/bootstrap/src/worker.ts +279 -0
- package/packages/omo-codex/plugin/components/bootstrap/test/download.test.ts +295 -0
- package/packages/omo-codex/plugin/components/bootstrap/test/environment.test.ts +375 -0
- package/packages/omo-codex/plugin/components/bootstrap/test/provision.test.ts +464 -0
- package/packages/omo-codex/plugin/components/bootstrap/tsconfig.json +25 -0
- package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/package.json +4 -4
- package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/git-bash/package.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/dist/codex-hook-cli.js +6 -10
- package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/package.json +4 -4
- package/packages/omo-codex/plugin/components/lsp/scripts/build-lsp-tools.test.mjs +8 -3
- package/packages/omo-codex/plugin/components/lsp/src/codex-hook-cli.ts +5 -8
- package/packages/omo-codex/plugin/components/lsp/test/codex-hook-cli.test.ts +24 -1
- package/packages/omo-codex/plugin/components/rules/bundled-rules/windows-git-bash.md +3 -1
- package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/rules/package.json +4 -4
- package/packages/omo-codex/plugin/components/rules/test/windows-git-bash-bundled-rule.test.ts +35 -1
- 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 +4 -4
- package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/package.json +4 -4
- package/packages/omo-codex/plugin/components/ultrawork/biome.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/directive.md +155 -99
- package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/package.json +4 -4
- package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/SKILL.md +19 -51
- package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/references/full-workflow.md +46 -51
- package/packages/omo-codex/plugin/components/ultrawork/test/codex-hook.test.ts +19 -0
- package/packages/omo-codex/plugin/components/ultrawork/test/package-smoke.test.ts +0 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-commands.js +9 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-output.d.ts +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-output.js +18 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-crud.js +1 -3
- package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/ulw-loop/package.json +4 -4
- package/packages/omo-codex/plugin/components/ulw-loop/src/cli-commands.ts +6 -2
- package/packages/omo-codex/plugin/components/ulw-loop/src/cli-output.ts +19 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/plan-crud.ts +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/test/cli-commands.test.ts +6 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/cli-complete-goals.test.ts +26 -1
- package/packages/omo-codex/plugin/components/ulw-loop/test/cli-json-errors.test.ts +89 -0
- package/packages/omo-codex/plugin/hooks/hooks.json +27 -16
- package/packages/omo-codex/plugin/package-lock.json +193 -193
- package/packages/omo-codex/plugin/package.json +1 -1
- package/packages/omo-codex/plugin/scripts/auto-update-state.d.mts +20 -0
- package/packages/omo-codex/plugin/scripts/auto-update.mjs +28 -8
- package/packages/omo-codex/plugin/scripts/build-components.mjs +36 -5
- package/packages/omo-codex/plugin/scripts/install-flow.mjs +43 -0
- package/packages/omo-codex/plugin/skills/lcx-contribute-bug-fix/SKILL.md +79 -28
- package/packages/omo-codex/plugin/skills/lcx-contribute-bug-fix/agents/openai.yaml +2 -2
- package/packages/omo-codex/plugin/skills/lcx-report-bug/SKILL.md +7 -6
- package/packages/omo-codex/plugin/skills/lcx-report-bug/agents/openai.yaml +1 -1
- package/packages/omo-codex/plugin/skills/ulw-plan/SKILL.md +19 -51
- package/packages/omo-codex/plugin/skills/ulw-plan/references/full-workflow.md +46 -51
- package/packages/omo-codex/plugin/test/aggregate-manifest.test.mjs +1 -0
- package/packages/omo-codex/plugin/test/auto-update.test.mjs +145 -0
- package/packages/omo-codex/plugin/test/bootstrap-binlinks.test.mjs +250 -0
- package/packages/omo-codex/plugin/test/bootstrap-hooks.test.mjs +166 -0
- package/packages/omo-codex/plugin/test/bootstrap-orchestration.test.mjs +371 -0
- package/packages/omo-codex/plugin/test/bootstrap-ps-guard.test.mjs +134 -0
- package/packages/omo-codex/plugin/test/bootstrap-setup.test.mjs +249 -0
- package/packages/omo-codex/plugin/test/lcx-bug-skills.test.mjs +10 -1
- package/packages/omo-codex/plugin/test/ulw-plan-skill.test.mjs +46 -0
- package/packages/omo-codex/scripts/atomic-write.test.mjs +82 -0
- package/packages/omo-codex/scripts/install/agents.d.mts +18 -0
- package/packages/omo-codex/scripts/install/agents.mjs +78 -5
- package/packages/omo-codex/scripts/install/atomic-write.mjs +59 -0
- package/packages/omo-codex/scripts/install/bin-dir.d.mts +7 -0
- package/packages/omo-codex/scripts/install/bin-links.d.mts +18 -0
- package/packages/omo-codex/scripts/install/config.d.mts +35 -0
- package/packages/omo-codex/scripts/install/config.mjs +13 -3
- package/packages/omo-codex/scripts/install/git-bash-mcp-env.d.mts +5 -0
- package/packages/omo-codex/scripts/install/git-bash.d.mts +23 -0
- package/packages/omo-codex/scripts/install/hook-trust.d.mts +10 -0
- package/packages/omo-codex/scripts/install-agent-links.test.mjs +41 -0
- package/packages/omo-codex/scripts/install-local.mjs +3 -2
- package/packages/shared-skills/skills/lcx-contribute-bug-fix/SKILL.md +79 -28
- package/packages/shared-skills/skills/lcx-contribute-bug-fix/agents/openai.yaml +2 -2
- package/packages/shared-skills/skills/lcx-report-bug/SKILL.md +7 -6
- package/packages/shared-skills/skills/lcx-report-bug/agents/openai.yaml +1 -1
- package/dist/hooks/session-recovery/constants.d.ts +0 -4
- package/dist/hooks/session-recovery/detect-error-type.d.ts +0 -4
- package/dist/hooks/session-recovery/error-recovery.d.ts +0 -4
- package/dist/hooks/session-recovery/hook-types.d.ts +0 -22
- package/dist/hooks/session-recovery/hook.d.ts +0 -4
- package/dist/hooks/session-recovery/index.d.ts +0 -5
- package/dist/hooks/session-recovery/interrupted-idle-message-fetch-timeout.d.ts +0 -7
- package/dist/hooks/session-recovery/interrupted-tool-results.d.ts +0 -3
- package/dist/hooks/session-recovery/message-state.d.ts +0 -4
- package/dist/hooks/session-recovery/recover-thinking-block-order.d.ts +0 -5
- package/dist/hooks/session-recovery/recover-thinking-disabled-violation.d.ts +0 -5
- package/dist/hooks/session-recovery/recover-tool-result-missing.d.ts +0 -10
- package/dist/hooks/session-recovery/recover-unavailable-tool.d.ts +0 -5
- package/dist/hooks/session-recovery/resume.d.ts +0 -7
- package/dist/hooks/session-recovery/storage/latest-assistant-message.d.ts +0 -5
- package/dist/hooks/session-recovery/storage/orphan-thinking-search.d.ts +0 -2
- package/dist/hooks/session-recovery/storage/thinking-block-search.d.ts +0 -2
- package/dist/hooks/session-recovery/storage/thinking-prepend.d.ts +0 -33
- package/dist/hooks/session-recovery/storage/thinking-strip.d.ts +0 -11
- package/dist/hooks/session-recovery/storage.d.ts +0 -20
- package/dist/plugin/event-session-recovery.d.ts +0 -9
- package/dist/plugin/user-abort-interrupted-recovery-guard.d.ts +0 -6
- /package/dist/hooks/{session-recovery → anthropic-context-window-limit-recovery}/storage/empty-messages.d.ts +0 -0
- /package/dist/hooks/{session-recovery → anthropic-context-window-limit-recovery}/storage/empty-text.d.ts +0 -0
- /package/dist/hooks/{session-recovery → anthropic-context-window-limit-recovery}/storage/message-dir.d.ts +0 -0
- /package/dist/hooks/{session-recovery → anthropic-context-window-limit-recovery}/storage/part-id.d.ts +0 -0
- /package/dist/hooks/{session-recovery → anthropic-context-window-limit-recovery}/storage/text-part-injector.d.ts +0 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { chmod, mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
const CLI_URL = new URL("../components/bootstrap/dist/cli.js", import.meta.url);
|
|
9
|
+
const { runBootstrapWorker, runWorkerSetup } = await import(CLI_URL.href);
|
|
10
|
+
|
|
11
|
+
const REPO_MODEL_CATALOG_PATH = fileURLToPath(new URL("../model-catalog.json", import.meta.url));
|
|
12
|
+
const MARKETPLACE_SOURCE_LINE = 'source = "https://github.com/code-yeongyu/lazycodex.git"';
|
|
13
|
+
const PERMISSIONS_KEY_PATTERN = /approval_policy|sandbox_mode|network_access/;
|
|
14
|
+
const PLUGIN_VERSION = "9.9.9";
|
|
15
|
+
|
|
16
|
+
const BUNDLED_EXPLORER_TOML = 'description = "Explorer agent"\nmodel_reasoning_effort = "medium"\n';
|
|
17
|
+
const BUNDLED_METIS_TOML = 'description = "Metis agent"\nmodel_reasoning_effort = "high"\n';
|
|
18
|
+
|
|
19
|
+
async function withSetupFixture(run) {
|
|
20
|
+
const root = await mkdtemp(join(tmpdir(), "omo-bootstrap-setup-"));
|
|
21
|
+
try {
|
|
22
|
+
const pluginRoot = join(root, "plugin");
|
|
23
|
+
const pluginData = join(root, "plugin-data");
|
|
24
|
+
const codexHome = join(root, "codex-home");
|
|
25
|
+
await mkdir(join(pluginRoot, ".codex-plugin"), { recursive: true });
|
|
26
|
+
await mkdir(join(pluginRoot, "hooks"), { recursive: true });
|
|
27
|
+
await mkdir(join(pluginRoot, "components", "ultrawork", "agents"), { recursive: true });
|
|
28
|
+
// A complete npx-style payload ships dist/cli; the marketplace-payload
|
|
29
|
+
// (no dist/cli -> degraded omo-cli) path is covered by
|
|
30
|
+
// bootstrap-binlinks.test.mjs.
|
|
31
|
+
await mkdir(join(pluginRoot, "dist", "cli"), { recursive: true });
|
|
32
|
+
await writeFile(join(pluginRoot, "dist", "cli", "index.js"), "");
|
|
33
|
+
await mkdir(pluginData, { recursive: true });
|
|
34
|
+
await mkdir(codexHome, { recursive: true });
|
|
35
|
+
await writeFile(
|
|
36
|
+
join(pluginRoot, ".codex-plugin", "plugin.json"),
|
|
37
|
+
`${JSON.stringify({ hooks: "./hooks/hooks.json", name: "omo", version: PLUGIN_VERSION })}\n`,
|
|
38
|
+
);
|
|
39
|
+
await writeFile(
|
|
40
|
+
join(pluginRoot, "hooks", "hooks.json"),
|
|
41
|
+
`${JSON.stringify({
|
|
42
|
+
hooks: {
|
|
43
|
+
SessionStart: [
|
|
44
|
+
{
|
|
45
|
+
hooks: [
|
|
46
|
+
{
|
|
47
|
+
command: 'node "${PLUGIN_ROOT}/components/bootstrap/dist/cli.js" hook session-start',
|
|
48
|
+
statusMessage: "LazyCodex(9.9.9): Checking Bootstrap Provisioning",
|
|
49
|
+
timeout: 30,
|
|
50
|
+
type: "command",
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
})}\n`,
|
|
57
|
+
);
|
|
58
|
+
await writeFile(
|
|
59
|
+
join(pluginRoot, ".mcp.json"),
|
|
60
|
+
`${JSON.stringify({ mcpServers: { git_bash: { args: ["serve"], command: "node", env: {} } } }, null, "\t")}\n`,
|
|
61
|
+
);
|
|
62
|
+
await writeFile(join(pluginRoot, "components", "ultrawork", "agents", "explorer.toml"), BUNDLED_EXPLORER_TOML);
|
|
63
|
+
await writeFile(join(pluginRoot, "components", "ultrawork", "agents", "metis.toml"), BUNDLED_METIS_TOML);
|
|
64
|
+
await writeFile(join(codexHome, "config.toml"), `[marketplaces.sisyphuslabs]\n${MARKETPLACE_SOURCE_LINE}\n`);
|
|
65
|
+
await run({ codexHome, pluginData, pluginRoot, root });
|
|
66
|
+
} finally {
|
|
67
|
+
await rm(root, { force: true, recursive: true });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function setupOptions(fixture, overrides = {}) {
|
|
72
|
+
return {
|
|
73
|
+
codexHome: fixture.codexHome,
|
|
74
|
+
env: {},
|
|
75
|
+
platform: "darwin",
|
|
76
|
+
pluginData: fixture.pluginData,
|
|
77
|
+
pluginRoot: fixture.pluginRoot,
|
|
78
|
+
...overrides,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function readConfig(fixture) {
|
|
83
|
+
return readFile(join(fixture.codexHome, "config.toml"), "utf8");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
test("#given a marketplace-flow CODEX_HOME #when the worker setup runs #then config gains plugin, hook-state, and agent blocks without permissions keys", async () => {
|
|
87
|
+
await withSetupFixture(async (fixture) => {
|
|
88
|
+
const outcome = await runWorkerSetup(setupOptions(fixture));
|
|
89
|
+
|
|
90
|
+
assert.deepEqual(outcome.degraded, []);
|
|
91
|
+
const config = await readConfig(fixture);
|
|
92
|
+
assert.match(config, /\[plugins\."omo@sisyphuslabs"\]\nenabled = true/);
|
|
93
|
+
assert.match(config, /\[plugins\."omo@sisyphuslabs"\.mcp_servers\.context7\]\nenabled = true/);
|
|
94
|
+
assert.match(config, /\[plugins\."omo@sisyphuslabs"\.mcp_servers\.git_bash\]\nenabled = false/);
|
|
95
|
+
assert.match(
|
|
96
|
+
config,
|
|
97
|
+
/\[hooks\.state\."omo@sisyphuslabs:hooks\/hooks\.json:session_start:0:0"\]\ntrusted_hash = "sha256:[0-9a-f]{64}"/,
|
|
98
|
+
);
|
|
99
|
+
assert.match(config, /\[agents\.explorer\]\nconfig_file = "\.\/agents\/explorer\.toml"/);
|
|
100
|
+
assert.match(config, /\[agents\.metis\]\nconfig_file = "\.\/agents\/metis\.toml"/);
|
|
101
|
+
assert.doesNotMatch(config, PERMISSIONS_KEY_PATTERN);
|
|
102
|
+
assert.equal(await readFile(join(fixture.codexHome, "agents", "explorer.toml"), "utf8"), BUNDLED_EXPLORER_TOML);
|
|
103
|
+
assert.equal(await readFile(join(fixture.codexHome, "agents", "metis.toml"), "utf8"), BUNDLED_METIS_TOML);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("#given an existing git marketplace source #when the worker setup runs #then the [marketplaces.sisyphuslabs] block stays byte-identical", async () => {
|
|
108
|
+
await withSetupFixture(async (fixture) => {
|
|
109
|
+
await runWorkerSetup(setupOptions(fixture));
|
|
110
|
+
|
|
111
|
+
const config = await readConfig(fixture);
|
|
112
|
+
assert.ok(config.includes(`[marketplaces.sisyphuslabs]\n${MARKETPLACE_SOURCE_LINE}`), "git source line must stay verbatim");
|
|
113
|
+
assert.doesNotMatch(config, /source_type/);
|
|
114
|
+
assert.doesNotMatch(config, /last_updated/);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("#given a completed first run #when the worker setup runs again #then config.toml is byte-identical (idempotent)", async () => {
|
|
119
|
+
await withSetupFixture(async (fixture) => {
|
|
120
|
+
await runWorkerSetup(setupOptions(fixture));
|
|
121
|
+
const firstRun = await readConfig(fixture);
|
|
122
|
+
|
|
123
|
+
const outcome = await runWorkerSetup(setupOptions(fixture));
|
|
124
|
+
|
|
125
|
+
assert.deepEqual(outcome.degraded, []);
|
|
126
|
+
assert.equal(await readConfig(fixture), firstRun);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("#given bootstrap-managed staging #when agents are linked #then nothing is persisted under PLUGIN_ROOT", async () => {
|
|
131
|
+
await withSetupFixture(async (fixture) => {
|
|
132
|
+
await runWorkerSetup(setupOptions(fixture));
|
|
133
|
+
|
|
134
|
+
await assert.rejects(() => stat(join(fixture.pluginRoot, ".installed-agents.json")));
|
|
135
|
+
const manifest = JSON.parse(
|
|
136
|
+
await readFile(join(fixture.pluginData, "bootstrap", "agents-stage", ".installed-agents.json"), "utf8"),
|
|
137
|
+
);
|
|
138
|
+
assert.equal(manifest.agents.length, 2);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("#given user-tuned reasoning and service tier on an installed agent #when agents are re-linked #then both are preserved", async () => {
|
|
143
|
+
await withSetupFixture(async (fixture) => {
|
|
144
|
+
await mkdir(join(fixture.codexHome, "agents"), { recursive: true });
|
|
145
|
+
await writeFile(
|
|
146
|
+
join(fixture.codexHome, "agents", "explorer.toml"),
|
|
147
|
+
'description = "Explorer agent"\nmodel_reasoning_effort = "low"\nservice_tier = "flex"\n',
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
await runWorkerSetup(setupOptions(fixture));
|
|
151
|
+
|
|
152
|
+
const linked = await readFile(join(fixture.codexHome, "agents", "explorer.toml"), "utf8");
|
|
153
|
+
assert.match(linked, /model_reasoning_effort = "low"/);
|
|
154
|
+
assert.match(linked, /service_tier = "flex"/);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("#given an unwritable config.toml #when the worker setup runs #then it degrades naming config.toml and leaves the file untouched", async () => {
|
|
159
|
+
await withSetupFixture(async (fixture) => {
|
|
160
|
+
const configPath = join(fixture.codexHome, "config.toml");
|
|
161
|
+
const before = await readFile(configPath, "utf8");
|
|
162
|
+
await chmod(configPath, 0o444);
|
|
163
|
+
try {
|
|
164
|
+
const outcome = await runWorkerSetup(setupOptions(fixture));
|
|
165
|
+
|
|
166
|
+
const configEntries = outcome.degraded.filter((entry) => entry.component === "config");
|
|
167
|
+
assert.equal(configEntries.length, 1);
|
|
168
|
+
assert.match(configEntries[0].reason, /config\.toml/);
|
|
169
|
+
assert.equal(await readFile(configPath, "utf8"), before);
|
|
170
|
+
assert.equal((await stat(configPath)).mode & 0o777, 0o444);
|
|
171
|
+
} finally {
|
|
172
|
+
await chmod(configPath, 0o644);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("#given win32 without Git Bash and auto-install skipped #when the worker setup runs #then it degrades instead of throwing and disables the git_bash MCP", async () => {
|
|
178
|
+
await withSetupFixture(async (fixture) => {
|
|
179
|
+
const commands = [];
|
|
180
|
+
const outcome = await runWorkerSetup(
|
|
181
|
+
setupOptions(fixture, {
|
|
182
|
+
env: { OMO_CODEX_SKIP_GIT_BASH_AUTO_INSTALL: "1" },
|
|
183
|
+
platform: "win32",
|
|
184
|
+
resolveGitBash: () => ({ checkedPaths: [], found: false, installHint: "install git bash" }),
|
|
185
|
+
runCommand: async (command, args) => {
|
|
186
|
+
commands.push([command, ...args]);
|
|
187
|
+
},
|
|
188
|
+
}),
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
assert.deepEqual(commands, []);
|
|
192
|
+
const gitBashEntries = outcome.degraded.filter((entry) => entry.component === "git-bash");
|
|
193
|
+
assert.equal(gitBashEntries.length, 1);
|
|
194
|
+
const config = await readConfig(fixture);
|
|
195
|
+
assert.match(config, /\[plugins\."omo@sisyphuslabs"\.mcp_servers\.git_bash\]\nenabled = false/);
|
|
196
|
+
assert.match(config, /\[plugins\."omo@sisyphuslabs"\]\nenabled = true/, "setup must continue past a missing Git Bash");
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("#given win32 with Git Bash and OMO_CODEX_GIT_BASH_PATH #when the worker setup runs #then git_bash is enabled and the MCP env is stamped", async () => {
|
|
201
|
+
await withSetupFixture(async (fixture) => {
|
|
202
|
+
const bashPath = "C:\\Tools\\Git\\bin\\bash.exe";
|
|
203
|
+
const outcome = await runWorkerSetup(
|
|
204
|
+
setupOptions(fixture, {
|
|
205
|
+
env: { OMO_CODEX_GIT_BASH_PATH: bashPath },
|
|
206
|
+
platform: "win32",
|
|
207
|
+
resolveGitBash: () => ({ checkedPaths: [bashPath], found: true, path: bashPath, source: "env" }),
|
|
208
|
+
}),
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
assert.deepEqual(outcome.degraded, []);
|
|
212
|
+
const config = await readConfig(fixture);
|
|
213
|
+
assert.match(config, /\[plugins\."omo@sisyphuslabs"\.mcp_servers\.git_bash\]\nenabled = true/);
|
|
214
|
+
const manifest = JSON.parse(await readFile(join(fixture.pluginRoot, ".mcp.json"), "utf8"));
|
|
215
|
+
assert.equal(manifest.mcpServers.git_bash.env.OMO_CODEX_GIT_BASH_PATH, bashPath);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("#given the shipped plugin model catalog #when the worker setup stamps reasoning config #then the bundled fallback has not drifted from it", async () => {
|
|
220
|
+
await withSetupFixture(async (fixture) => {
|
|
221
|
+
const catalog = JSON.parse(await readFile(REPO_MODEL_CATALOG_PATH, "utf8"));
|
|
222
|
+
|
|
223
|
+
await runWorkerSetup(setupOptions(fixture));
|
|
224
|
+
|
|
225
|
+
const config = await readConfig(fixture);
|
|
226
|
+
assert.ok(config.includes(`model = ${JSON.stringify(catalog.current.model)}`));
|
|
227
|
+
assert.ok(config.includes(`model_context_window = ${catalog.current.model_context_window}`));
|
|
228
|
+
assert.ok(config.includes(`model_reasoning_effort = ${JSON.stringify(catalog.current.model_reasoning_effort)}`));
|
|
229
|
+
assert.ok(config.includes(`plan_mode_reasoning_effort = ${JSON.stringify(catalog.current.plan_mode_reasoning_effort)}`));
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("#given the default worker step list #when the worker runs end to end #then the setup step updates config and records a success marker", async () => {
|
|
234
|
+
await withSetupFixture(async (fixture) => {
|
|
235
|
+
const result = await runBootstrapWorker({
|
|
236
|
+
argv: ["--codex-home", fixture.codexHome, "--only", "setup"],
|
|
237
|
+
env: { PLUGIN_DATA: fixture.pluginData, PLUGIN_ROOT: fixture.pluginRoot },
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
assert.equal(result.ran, true);
|
|
241
|
+
assert.equal(result.status, "success");
|
|
242
|
+
const state = JSON.parse(await readFile(join(fixture.pluginData, "bootstrap", "state.json"), "utf8"));
|
|
243
|
+
assert.equal(state.completedForVersion, PLUGIN_VERSION);
|
|
244
|
+
assert.equal(state.lastStatus, "success");
|
|
245
|
+
const config = await readConfig(fixture);
|
|
246
|
+
assert.match(config, /\[plugins\."omo@sisyphuslabs"\]\nenabled = true/);
|
|
247
|
+
assert.ok(config.includes(`[marketplaces.sisyphuslabs]\n${MARKETPLACE_SOURCE_LINE}`));
|
|
248
|
+
});
|
|
249
|
+
});
|
|
@@ -16,12 +16,16 @@ test("#given synced lcx-report-bug skill #when inspected #then it files LazyCode
|
|
|
16
16
|
|
|
17
17
|
// then
|
|
18
18
|
assert.match(skill, /^---\r?\nname: lcx-report-bug\r?\n/m);
|
|
19
|
+
assert.match(skill, /Never create a PR or push a branch against `code-yeongyu\/lazycodex`/);
|
|
20
|
+
assert.match(skill, /gh pr create --repo openai\/codex/);
|
|
21
|
+
assert.doesNotMatch(skill, /gh pr create --repo "\$TARGET_REPO"/);
|
|
22
|
+
assert.doesNotMatch(skill, /gh pr create --repo code-yeongyu\/lazycodex/);
|
|
19
23
|
assert.match(interfaceMetadata, /display_name: "lcx-report-bug \(omo\)"/);
|
|
20
24
|
assert.match(interfaceMetadata, /- "lazycodex bug"/);
|
|
21
25
|
assert.match(interfaceMetadata, /- "openai codex bug"/);
|
|
22
26
|
});
|
|
23
27
|
|
|
24
|
-
test("#given synced lcx-contribute-bug-fix skill #when inspected #then it
|
|
28
|
+
test("#given synced lcx-contribute-bug-fix skill #when inspected #then it delivers LazyCodex fixes as issues and upstream fixes as fork PRs", async () => {
|
|
25
29
|
// given
|
|
26
30
|
const skillRoot = join(root, "skills", "lcx-contribute-bug-fix");
|
|
27
31
|
|
|
@@ -31,6 +35,11 @@ test("#given synced lcx-contribute-bug-fix skill #when inspected #then it contri
|
|
|
31
35
|
|
|
32
36
|
// then
|
|
33
37
|
assert.match(skill, /^---\r?\nname: lcx-contribute-bug-fix\r?\n/m);
|
|
38
|
+
assert.match(skill, /NEVER open a PR or push a branch against this repo/);
|
|
39
|
+
assert.match(skill, /gh issue create --repo code-yeongyu\/lazycodex/);
|
|
40
|
+
assert.match(skill, /gh pr create --repo openai\/codex/);
|
|
41
|
+
assert.doesNotMatch(skill, /gh pr create --repo "\$TARGET_REPO"/);
|
|
42
|
+
assert.doesNotMatch(skill, /gh pr create --repo code-yeongyu\/lazycodex/);
|
|
34
43
|
assert.match(interfaceMetadata, /display_name: "lcx-contribute-bug-fix \(omo\)"/);
|
|
35
44
|
assert.match(interfaceMetadata, /- "contribute a bug fix"/);
|
|
36
45
|
assert.match(interfaceMetadata, /- "fix bug pr"/);
|
|
@@ -30,6 +30,30 @@ test("#given ulw-plan skill #when the planning gate is inspected #then it explor
|
|
|
30
30
|
assert.doesNotMatch(skill, /Proceeding to plan generation/);
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
+
test("#given ulw-plan skill #when the execution gate is inspected #then plan mode is sticky, execution needs an explicit start, and the high-accuracy ask is mandatory", async () => {
|
|
34
|
+
// given
|
|
35
|
+
const skill = await readFile(skillPath, "utf8");
|
|
36
|
+
|
|
37
|
+
// then
|
|
38
|
+
assert.match(skill, /plan mode is sticky/i);
|
|
39
|
+
assert.match(skill, /never\s+(?:start|begin)[^.]{0,80}(?:implement|execut)/i);
|
|
40
|
+
assert.match(skill, /ask[^.]{0,160}high[- ]accuracy/i);
|
|
41
|
+
assert.match(skill, /two filters/i);
|
|
42
|
+
assert.match(skill, /\bWHY\b/);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("#given ulw-plan full workflow reference #when the delivery phase is inspected #then it mandates the start-or-high-accuracy question and forbids self-started execution", async () => {
|
|
46
|
+
// given
|
|
47
|
+
const workflow = await readFile(workflowPath, "utf8");
|
|
48
|
+
|
|
49
|
+
// then
|
|
50
|
+
assert.match(workflow, /plan mode is sticky/i);
|
|
51
|
+
assert.match(workflow, /two filters/i);
|
|
52
|
+
assert.match(workflow, /ask[^.]{0,160}high[- ]accuracy/i);
|
|
53
|
+
assert.match(workflow, /execution belongs to the worker/i);
|
|
54
|
+
assert.doesNotMatch(workflow, /High-accuracy review \(optional\)/);
|
|
55
|
+
});
|
|
56
|
+
|
|
33
57
|
test("#given ulw-plan full workflow reference #when inspected #then it documents the approval gate and .omo plan output with Codex-native tools only", async () => {
|
|
34
58
|
// given
|
|
35
59
|
const workflow = await readFile(workflowPath, "utf8");
|
|
@@ -41,3 +65,25 @@ test("#given ulw-plan full workflow reference #when inspected #then it documents
|
|
|
41
65
|
assert.doesNotMatch(workflow, opencodeOnlyToolPattern);
|
|
42
66
|
assert.doesNotMatch(workflow, /Proceeding to plan generation/);
|
|
43
67
|
});
|
|
68
|
+
|
|
69
|
+
test("#given ulw-plan approval gate #when inspected #then it is a durable decision rule that ends the approval loop (lazycodex #48)", async () => {
|
|
70
|
+
// given
|
|
71
|
+
const skill = await readFile(skillPath, "utf8");
|
|
72
|
+
const workflow = await readFile(workflowPath, "utf8");
|
|
73
|
+
const combined = `${skill}\n${workflow}`;
|
|
74
|
+
|
|
75
|
+
// then — a durable approval-pending checkpoint guards against re-running exploration after compaction
|
|
76
|
+
assert.match(combined, /awaiting-approval/);
|
|
77
|
+
assert.match(combined, /instead of re-running exploration/i);
|
|
78
|
+
|
|
79
|
+
// then — approval is decided from intent, not a fixed passphrase
|
|
80
|
+
assert.match(combined, /"proceed"/);
|
|
81
|
+
assert.match(combined, /"write the plan"/);
|
|
82
|
+
|
|
83
|
+
// then — approval authorizes writing the plan, never implementation (resolves sticky-mode ambiguity)
|
|
84
|
+
assert.match(combined, /authoriz[^.]{0,80}writing the plan/i);
|
|
85
|
+
assert.match(combined, /never authorization to implement/i);
|
|
86
|
+
|
|
87
|
+
// then — the still-unclear path is a single prompt, never a re-exploration loop
|
|
88
|
+
assert.match(combined, /do not re-explore/i);
|
|
89
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { lstat, mkdir, mkdtemp, readFile, readdir, symlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
|
|
7
|
+
import { isRetriableRenameError, writeFileAtomic } from "./install/atomic-write.mjs";
|
|
8
|
+
|
|
9
|
+
test("#given a fresh target path #when writeFileAtomic writes content #then the file holds exactly that content with no temp residue", async () => {
|
|
10
|
+
// given
|
|
11
|
+
const root = await mkdtemp(join(tmpdir(), "omo-atomic-write-fresh-"));
|
|
12
|
+
const target = join(root, "config.toml");
|
|
13
|
+
|
|
14
|
+
// when
|
|
15
|
+
await writeFileAtomic(target, "alpha = 1\n");
|
|
16
|
+
|
|
17
|
+
// then
|
|
18
|
+
assert.equal(await readFile(target, "utf8"), "alpha = 1\n");
|
|
19
|
+
assert.deepEqual(await readdir(root), ["config.toml"]);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("#given an existing file #when writeFileAtomic overwrites it #then the content is replaced with no temp residue", async () => {
|
|
23
|
+
// given
|
|
24
|
+
const root = await mkdtemp(join(tmpdir(), "omo-atomic-write-replace-"));
|
|
25
|
+
const target = join(root, "config.toml");
|
|
26
|
+
await writeFile(target, "old = true\n");
|
|
27
|
+
|
|
28
|
+
// when
|
|
29
|
+
await writeFileAtomic(target, "new = true\n");
|
|
30
|
+
|
|
31
|
+
// then
|
|
32
|
+
assert.equal(await readFile(target, "utf8"), "new = true\n");
|
|
33
|
+
const residue = (await readdir(root)).filter((entry) => entry.startsWith(".tmp-"));
|
|
34
|
+
assert.deepEqual(residue, []);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("#given the rename target cannot be replaced #when writeFileAtomic fails #then the original is untouched and the temp file is cleaned up", async () => {
|
|
38
|
+
// given
|
|
39
|
+
const root = await mkdtemp(join(tmpdir(), "omo-atomic-write-fail-"));
|
|
40
|
+
const target = join(root, "config.toml");
|
|
41
|
+
await mkdir(target);
|
|
42
|
+
await writeFile(join(target, "sentinel"), "keep\n");
|
|
43
|
+
|
|
44
|
+
// when
|
|
45
|
+
await assert.rejects(writeFileAtomic(target, "should not land\n"));
|
|
46
|
+
|
|
47
|
+
// then
|
|
48
|
+
assert.equal(await readFile(join(target, "sentinel"), "utf8"), "keep\n");
|
|
49
|
+
const residue = (await readdir(root)).filter((entry) => entry.startsWith(".tmp-"));
|
|
50
|
+
assert.deepEqual(residue, []);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("#given the target is a symlink #when writeFileAtomic writes #then it writes through to the link target and preserves the symlink", async () => {
|
|
54
|
+
// given
|
|
55
|
+
const root = await mkdtemp(join(tmpdir(), "omo-atomic-write-symlink-"));
|
|
56
|
+
const realTarget = join(root, "actual.toml");
|
|
57
|
+
const link = join(root, "config.toml");
|
|
58
|
+
await writeFile(realTarget, "old = true\n");
|
|
59
|
+
await symlink(realTarget, link);
|
|
60
|
+
|
|
61
|
+
// when
|
|
62
|
+
await writeFileAtomic(link, "new = true\n");
|
|
63
|
+
|
|
64
|
+
// then
|
|
65
|
+
assert.equal(await readFile(realTarget, "utf8"), "new = true\n");
|
|
66
|
+
assert.equal((await lstat(link)).isSymbolicLink(), true);
|
|
67
|
+
const residue = (await readdir(root)).filter((entry) => entry.startsWith(".tmp-"));
|
|
68
|
+
assert.deepEqual(residue, []);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("#given various rename errors #when classifying retriability #then only Windows file-lock codes are retriable", () => {
|
|
72
|
+
// given
|
|
73
|
+
const busy = Object.assign(new Error("busy"), { code: "EBUSY" });
|
|
74
|
+
const perm = Object.assign(new Error("perm"), { code: "EPERM" });
|
|
75
|
+
const missing = Object.assign(new Error("missing"), { code: "ENOENT" });
|
|
76
|
+
|
|
77
|
+
// when / then
|
|
78
|
+
assert.equal(isRetriableRenameError(busy), true);
|
|
79
|
+
assert.equal(isRetriableRenameError(perm), true);
|
|
80
|
+
assert.equal(isRetriableRenameError(missing), false);
|
|
81
|
+
assert.equal(isRetriableRenameError("not an error"), false);
|
|
82
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface LinkedPluginAgent {
|
|
2
|
+
name: string;
|
|
3
|
+
path: string;
|
|
4
|
+
target: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export declare function capturePreservedAgentReasoning(options: { codexHome: string }): Promise<Map<string, string>>;
|
|
8
|
+
|
|
9
|
+
export declare function capturePreservedAgentServiceTier(options: {
|
|
10
|
+
codexHome: string;
|
|
11
|
+
}): Promise<Map<string, string | null>>;
|
|
12
|
+
|
|
13
|
+
export declare function linkCachedPluginAgents(options: {
|
|
14
|
+
codexHome: string;
|
|
15
|
+
pluginRoot: string;
|
|
16
|
+
preservedReasoning?: Map<string, string>;
|
|
17
|
+
preservedServiceTier?: Map<string, string | null>;
|
|
18
|
+
}): Promise<LinkedPluginAgent[]>;
|
|
@@ -22,7 +22,22 @@ export async function capturePreservedAgentReasoning({ codexHome }) {
|
|
|
22
22
|
return preserved;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
export async function
|
|
25
|
+
export async function capturePreservedAgentServiceTier({ codexHome }) {
|
|
26
|
+
const agentsDir = join(codexHome, "agents");
|
|
27
|
+
if (!(await exists(agentsDir))) return new Map();
|
|
28
|
+
|
|
29
|
+
const preserved = new Map();
|
|
30
|
+
const agentEntries = await readdir(agentsDir, { withFileTypes: true });
|
|
31
|
+
for (const entry of agentEntries) {
|
|
32
|
+
if (!entry.name.endsWith(".toml")) continue;
|
|
33
|
+
const content = await readTextIfExists(join(agentsDir, entry.name));
|
|
34
|
+
if (content === null) continue;
|
|
35
|
+
preserved.set(agentNameFromToml(entry.name), extractServiceTier(content));
|
|
36
|
+
}
|
|
37
|
+
return preserved;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function linkCachedPluginAgents({ codexHome, pluginRoot, preservedReasoning = new Map(), preservedServiceTier = new Map() }) {
|
|
26
41
|
const bundledAgents = await discoverBundledAgents(pluginRoot);
|
|
27
42
|
if (bundledAgents.length === 0) {
|
|
28
43
|
await writeManifest(pluginRoot, []);
|
|
@@ -38,12 +53,26 @@ export async function linkCachedPluginAgents({ codexHome, pluginRoot, preservedR
|
|
|
38
53
|
const linkPath = join(agentsDir, agentFileName);
|
|
39
54
|
await replaceWithCopy(linkPath, agentPath);
|
|
40
55
|
await restorePreservedReasoning({ linkPath, target: agentPath, value: preservedReasoning.get(agentName) });
|
|
56
|
+
await restorePreservedServiceTier({
|
|
57
|
+
linkPath,
|
|
58
|
+
preserved: preservedServiceTier.has(agentName),
|
|
59
|
+
value: preservedServiceTier.get(agentName) ?? null,
|
|
60
|
+
});
|
|
41
61
|
linked.push({ name: agentFileName, path: linkPath, target: agentPath });
|
|
42
62
|
}
|
|
43
63
|
await writeManifest(pluginRoot, linked.map((entry) => entry.path));
|
|
44
64
|
return linked;
|
|
45
65
|
}
|
|
46
66
|
|
|
67
|
+
async function restorePreservedServiceTier({ linkPath, preserved, value }) {
|
|
68
|
+
if (!preserved) return;
|
|
69
|
+
const content = await readFile(linkPath, "utf8");
|
|
70
|
+
if (extractServiceTier(content) === value) return;
|
|
71
|
+
const replacement = replaceServiceTier(content, value);
|
|
72
|
+
if (!replacement.replaced) return;
|
|
73
|
+
await writeFile(linkPath, replacement.content);
|
|
74
|
+
}
|
|
75
|
+
|
|
47
76
|
async function discoverBundledAgents(pluginRoot) {
|
|
48
77
|
const componentsRoot = join(pluginRoot, "components");
|
|
49
78
|
if (!(await exists(componentsRoot))) return [];
|
|
@@ -106,29 +135,73 @@ async function readTextIfExists(path) {
|
|
|
106
135
|
}
|
|
107
136
|
|
|
108
137
|
function extractReasoningEffort(content) {
|
|
138
|
+
return extractTopLevelStringSetting(content, "model_reasoning_effort");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function extractServiceTier(content) {
|
|
142
|
+
return extractTopLevelStringSetting(content, "service_tier");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function extractTopLevelStringSetting(content, key) {
|
|
109
146
|
for (const line of content.split(/\n/)) {
|
|
110
147
|
if (isSectionHeader(line)) return null;
|
|
111
|
-
const
|
|
112
|
-
if (
|
|
113
|
-
return JSON.parse(
|
|
148
|
+
const rawValue = topLevelStringSettingRawValue(line, key);
|
|
149
|
+
if (rawValue === undefined) continue;
|
|
150
|
+
return JSON.parse(rawValue);
|
|
114
151
|
}
|
|
115
152
|
return null;
|
|
116
153
|
}
|
|
117
154
|
|
|
118
155
|
function replaceReasoningEffort(content, value) {
|
|
156
|
+
return replaceTopLevelStringSetting(content, "model_reasoning_effort", value, { insertIfMissing: false });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function replaceServiceTier(content, value) {
|
|
160
|
+
return replaceTopLevelStringSetting(content, "service_tier", value, { insertIfMissing: true });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function replaceTopLevelStringSetting(content, key, value, options) {
|
|
119
164
|
let replaced = false;
|
|
120
165
|
const lines = content.split(/\n/);
|
|
121
166
|
for (let index = 0; index < lines.length; index += 1) {
|
|
122
167
|
const line = lines[index];
|
|
123
168
|
if (isSectionHeader(line)) break;
|
|
124
|
-
if (
|
|
169
|
+
if (topLevelStringSettingRawValue(line, key) === undefined) continue;
|
|
170
|
+
if (value === null) {
|
|
171
|
+
lines.splice(index, 1);
|
|
172
|
+
replaced = true;
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
125
175
|
lines[index] = line.replace(/=\s*"(?:[^"\\]|\\.)*"/, `= ${JSON.stringify(value)}`);
|
|
126
176
|
replaced = true;
|
|
127
177
|
break;
|
|
128
178
|
}
|
|
179
|
+
if (!replaced && value !== null && options.insertIfMissing) {
|
|
180
|
+
lines.splice(topLevelInsertionIndex(lines), 0, `${key} = ${JSON.stringify(value)}`);
|
|
181
|
+
replaced = true;
|
|
182
|
+
}
|
|
129
183
|
return { content: lines.join("\n"), replaced };
|
|
130
184
|
}
|
|
131
185
|
|
|
186
|
+
function topLevelStringSettingRawValue(line, key) {
|
|
187
|
+
const match = line.match(/^\s*([A-Za-z0-9_]+)\s*=\s*("(?:[^"\\]|\\.)*")/);
|
|
188
|
+
if (match === null) return undefined;
|
|
189
|
+
const settingKey = match[1];
|
|
190
|
+
const rawValue = match[2];
|
|
191
|
+
if (settingKey !== key || rawValue === undefined) return undefined;
|
|
192
|
+
return rawValue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function topLevelInsertionIndex(lines) {
|
|
196
|
+
const sectionIndex = lines.findIndex((line) => isSectionHeader(line));
|
|
197
|
+
const topLevelEnd = sectionIndex === -1 ? lines.length : sectionIndex;
|
|
198
|
+
let insertionIndex = topLevelEnd;
|
|
199
|
+
while (insertionIndex > 0 && lines[insertionIndex - 1] === "") {
|
|
200
|
+
insertionIndex -= 1;
|
|
201
|
+
}
|
|
202
|
+
return insertionIndex;
|
|
203
|
+
}
|
|
204
|
+
|
|
132
205
|
function isSectionHeader(line) {
|
|
133
206
|
const trimmed = line.trim();
|
|
134
207
|
return trimmed.startsWith("[") && trimmed.endsWith("]");
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { lstat, readlink, realpath, rename, unlink, writeFile } from "node:fs/promises";
|
|
2
|
+
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
const RENAME_RETRY_DELAYS_MS = [10, 25, 50];
|
|
5
|
+
const RETRIABLE_RENAME_CODES = new Set(["EPERM", "EBUSY"]);
|
|
6
|
+
|
|
7
|
+
export function isRetriableRenameError(error) {
|
|
8
|
+
if (!(error instanceof Error)) return false;
|
|
9
|
+
return RETRIABLE_RENAME_CODES.has(Reflect.get(error, "code"));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function writeFileAtomic(targetPath, data) {
|
|
13
|
+
const writeTarget = await resolveSymlinkTarget(targetPath);
|
|
14
|
+
const temporaryPath = join(
|
|
15
|
+
dirname(writeTarget),
|
|
16
|
+
`.tmp-${basename(writeTarget)}-${process.pid}-${Date.now()}`,
|
|
17
|
+
);
|
|
18
|
+
await writeFile(temporaryPath, data);
|
|
19
|
+
try {
|
|
20
|
+
await renameWithRetry(temporaryPath, writeTarget);
|
|
21
|
+
} catch (renameError) {
|
|
22
|
+
await unlink(temporaryPath).catch(() => {});
|
|
23
|
+
throw renameError;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function resolveSymlinkTarget(targetPath) {
|
|
28
|
+
let linkStats;
|
|
29
|
+
try {
|
|
30
|
+
linkStats = await lstat(targetPath);
|
|
31
|
+
} catch {
|
|
32
|
+
return targetPath;
|
|
33
|
+
}
|
|
34
|
+
if (!linkStats.isSymbolicLink()) return targetPath;
|
|
35
|
+
try {
|
|
36
|
+
return await realpath(targetPath);
|
|
37
|
+
} catch {
|
|
38
|
+
const linkValue = await readlink(targetPath);
|
|
39
|
+
return isAbsolute(linkValue) ? linkValue : resolve(dirname(targetPath), linkValue);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function renameWithRetry(fromPath, toPath) {
|
|
44
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
45
|
+
try {
|
|
46
|
+
await rename(fromPath, toPath);
|
|
47
|
+
return;
|
|
48
|
+
} catch (renameError) {
|
|
49
|
+
if (!isRetriableRenameError(renameError) || attempt >= RENAME_RETRY_DELAYS_MS.length) {
|
|
50
|
+
throw renameError;
|
|
51
|
+
}
|
|
52
|
+
await delay(RENAME_RETRY_DELAYS_MS[attempt]);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function delay(milliseconds) {
|
|
58
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
59
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function resolveCodexInstallerBinDir(options?: {
|
|
2
|
+
codexHome?: string;
|
|
3
|
+
env?: Record<string, string | undefined>;
|
|
4
|
+
homeDir?: string;
|
|
5
|
+
}): string;
|
|
6
|
+
|
|
7
|
+
export declare function nonEmptyEnvValue(env: Record<string, string | undefined>, key: string): string | undefined;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface LinkedPluginBin {
|
|
2
|
+
name: string;
|
|
3
|
+
path: string;
|
|
4
|
+
target: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export declare function linkCachedPluginBins(options: {
|
|
8
|
+
binDir: string;
|
|
9
|
+
pluginRoot: string;
|
|
10
|
+
platform?: string;
|
|
11
|
+
}): Promise<LinkedPluginBin[]>;
|
|
12
|
+
|
|
13
|
+
export declare function linkRootRuntimeBin(options: {
|
|
14
|
+
binDir: string;
|
|
15
|
+
codexHome: string;
|
|
16
|
+
repoRoot: string;
|
|
17
|
+
platform?: string;
|
|
18
|
+
}): Promise<LinkedPluginBin | null>;
|