oh-my-opencode 4.5.12 → 4.7.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 +194 -0
- package/.agents/skills/opencode-qa/references/cli-commands.md +188 -0
- package/.agents/skills/opencode-qa/references/db-investigation.md +197 -0
- package/.agents/skills/opencode-qa/references/events-hooks.md +110 -0
- package/.agents/skills/opencode-qa/references/sdk.md +96 -0
- package/.agents/skills/opencode-qa/references/server-api.md +200 -0
- package/.agents/skills/opencode-qa/references/testing-harness.md +218 -0
- package/.agents/skills/opencode-qa/references/tui-tmux.md +52 -0
- package/.agents/skills/opencode-qa/scripts/db-session-by-id.sh +53 -0
- package/.agents/skills/opencode-qa/scripts/db-session-by-name.sh +57 -0
- package/.agents/skills/opencode-qa/scripts/db-session-by-text.sh +158 -0
- package/.agents/skills/opencode-qa/scripts/export-roundtrip.sh +57 -0
- package/.agents/skills/opencode-qa/scripts/lib/common.sh +216 -0
- package/.agents/skills/opencode-qa/scripts/server-smoke.sh +64 -0
- package/.agents/skills/opencode-qa/scripts/sse-hook-probe.sh +106 -0
- package/.agents/skills/opencode-qa/scripts/tui-smoke.sh +89 -0
- package/README.ja.md +13 -3
- package/README.ko.md +13 -3
- package/README.md +24 -14
- package/README.ru.md +13 -3
- package/README.zh-cn.md +13 -3
- package/bin/oh-my-opencode.js +4 -3
- package/bin/oh-my-opencode.test.ts +35 -7
- package/bin/platform.d.ts +1 -1
- package/bin/platform.js +4 -4
- package/bin/platform.test.ts +31 -9
- package/bin/version-mismatch.js +47 -0
- package/bin/version-mismatch.test.ts +120 -0
- package/dist/cli/cleanup-command.d.ts +4 -0
- package/dist/cli/cleanup.d.ts +11 -0
- package/dist/cli/cli-program.d.ts +2 -1
- package/dist/cli/codex-ulw-loop.d.ts +12 -0
- package/dist/cli/doctor/checks/tui-plugin-config.d.ts +2 -0
- package/dist/cli/index.js +2189 -529
- package/dist/cli/install-codex/codex-cache.d.ts +1 -0
- package/dist/cli/install-codex/codex-cleanup-config.d.ts +6 -0
- package/dist/cli/install-codex/codex-cleanup.d.ts +21 -0
- package/dist/cli/install-codex/codex-config-permissions.d.ts +1 -0
- package/dist/cli/install-codex/codex-config-reasoning.d.ts +2 -0
- package/dist/cli/install-codex/codex-config-toml.d.ts +2 -1
- package/dist/cli/install-codex/codex-installation-detection.d.ts +36 -0
- package/dist/cli/install-codex/codex-model-catalog.d.ts +13 -0
- package/dist/cli/install-codex/codex-package-layout.d.ts +1 -0
- package/dist/cli/install-codex/codex-project-local-cleanup-best-effort.d.ts +7 -0
- package/dist/cli/install-codex/codex-project-local-cleanup.d.ts +35 -0
- package/dist/cli/install-codex/git-bash.d.ts +35 -0
- package/dist/cli/install-codex/index.d.ts +4 -0
- package/dist/cli/install-codex/toml-section-editor.d.ts +2 -0
- package/dist/cli/install-codex/types.d.ts +20 -0
- package/dist/cli/run/event-state.d.ts +1 -0
- package/dist/cli/run/poll-for-completion.d.ts +1 -0
- package/dist/cli/run/prompt-start.d.ts +7 -0
- package/dist/cli/star-request.d.ts +9 -0
- package/dist/config/schema/hooks.d.ts +0 -1
- package/dist/create-hooks.d.ts +0 -1
- package/dist/features/background-agent/concurrency.d.ts +1 -0
- package/dist/features/background-agent/process-cleanup.d.ts +6 -0
- package/dist/features/builtin-skills/skills/debugging.d.ts +2 -0
- package/dist/features/builtin-skills/skills/index.d.ts +1 -0
- package/dist/features/claude-code-session-state/state.d.ts +1 -0
- package/dist/features/opencode-skill-loader/index.d.ts +1 -0
- package/dist/features/opencode-skill-loader/opencode-config-skills-reader.d.ts +5 -0
- package/dist/features/tmux-subagent/attachable-session-status.d.ts +1 -1
- package/dist/features/tmux-subagent/session-status-parser.d.ts +1 -0
- package/dist/hooks/comment-checker/cli.d.ts +1 -0
- package/dist/hooks/index.d.ts +0 -1
- package/dist/hooks/tasks-todowrite-disabler/constants.d.ts +1 -1
- package/dist/index.js +1077 -563
- package/dist/plugin/hooks/create-core-hooks.d.ts +0 -1
- package/dist/plugin/hooks/create-session-hooks.d.ts +1 -2
- package/dist/plugin/messages-transform.d.ts +8 -1
- package/dist/plugin/user-abort-interrupted-recovery-guard.d.ts +6 -0
- package/dist/shared/command-executor/execute-hook-command.d.ts +2 -0
- package/dist/shared/prompt-async-gate/recent-dispatches.d.ts +14 -0
- package/dist/shared/prompt-async-gate/semantic-dedupe.d.ts +7 -0
- package/dist/shared/prompt-async-gate/session-idle-dispatch.d.ts +1 -0
- package/dist/shared/prompt-async-gate/timing.d.ts +1 -0
- package/dist/shared/prompt-async-gate/types.d.ts +2 -0
- package/dist/shared/prompt-async-gate.d.ts +1 -1
- package/dist/tools/skill/description-formatter.d.ts +5 -1
- package/dist/tools/skill/types.d.ts +1 -0
- package/package.json +22 -18
- package/packages/ast-grep-mcp/dist/cli.js +53 -9
- package/packages/git-bash-mcp/dist/cli.js +367 -0
- package/packages/lsp-tools-mcp/dist/lsp/process.js +1 -1
- package/packages/omo-codex/plugin/.mcp.json +11 -0
- package/packages/omo-codex/plugin/components/comment-checker/README.md +1 -1
- package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +29 -0
- package/packages/omo-codex/plugin/components/git-bash/package.json +23 -0
- package/packages/omo-codex/plugin/components/git-bash/src/cli.ts +33 -0
- package/packages/omo-codex/plugin/components/git-bash/src/codex-hook.ts +180 -0
- package/packages/omo-codex/plugin/components/git-bash/src/index.ts +10 -0
- package/packages/omo-codex/plugin/components/git-bash/test/codex-hook.test.ts +195 -0
- package/packages/omo-codex/plugin/components/git-bash/tsconfig.build.json +13 -0
- package/packages/omo-codex/plugin/components/git-bash/tsconfig.json +25 -0
- package/packages/omo-codex/plugin/components/lsp/README.md +1 -1
- package/packages/omo-codex/plugin/components/lsp/src/cli.ts +5 -5
- package/packages/omo-codex/plugin/components/lsp/src/codex-hook-cli.ts +33 -0
- package/packages/omo-codex/plugin/components/lsp/src/codex-hook.ts +19 -27
- package/packages/omo-codex/plugin/components/lsp/test/codex-hook-cli.test.ts +28 -0
- package/packages/omo-codex/plugin/components/lsp/test/codex-hook-errors.test.ts +55 -0
- package/packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts +7 -5
- package/packages/omo-codex/plugin/components/rules/README.md +1 -1
- package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus.md +6 -4
- package/packages/omo-codex/plugin/components/rules/bundled-rules/windows-git-bash.md +10 -0
- package/packages/omo-codex/plugin/components/rules/src/post-compact-budget.ts +0 -2
- package/packages/omo-codex/plugin/components/rules/test/package-smoke.test.ts +3 -1
- package/packages/omo-codex/plugin/components/rules/test/windows-git-bash-bundled-rule.test.ts +97 -0
- package/packages/omo-codex/plugin/components/start-work-continuation/directive.md +6 -5
- package/packages/omo-codex/plugin/components/start-work-continuation/test/codex-hook.test.ts +22 -0
- package/packages/omo-codex/plugin/components/ultrawork/CHANGELOG.md +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/README.md +3 -3
- package/packages/omo-codex/plugin/components/ultrawork/agents/codex-ultrawork-reviewer.toml +4 -1
- package/packages/omo-codex/plugin/components/ultrawork/agents/librarian.toml +8 -7
- package/packages/omo-codex/plugin/components/ultrawork/agents/plan.toml +9 -8
- package/packages/omo-codex/plugin/components/ultrawork/directive.md +32 -6
- package/packages/omo-codex/plugin/components/ultrawork/test/codex-hook.test.ts +27 -4
- package/packages/omo-codex/plugin/components/ultrawork/test/package-smoke.test.ts +25 -0
- package/packages/omo-codex/plugin/components/ulw-loop/README.md +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +28 -205
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +231 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +12 -1
- package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint.test.ts +19 -1
- package/packages/omo-codex/plugin/components/ulw-loop/test/package-smoke.test.ts +102 -5
- package/packages/omo-codex/plugin/hooks/hooks.json +35 -2
- package/packages/omo-codex/plugin/model-catalog.json +49 -0
- package/packages/omo-codex/plugin/package-lock.json +19 -0
- package/packages/omo-codex/plugin/package.json +3 -1
- package/packages/omo-codex/plugin/scripts/auto-update.mjs +159 -0
- package/packages/omo-codex/plugin/scripts/build-bundled-mcp-runtimes.mjs +16 -1
- package/packages/omo-codex/plugin/scripts/build-components.mjs +2 -1
- package/packages/omo-codex/plugin/scripts/migrate-codex-config.mjs +269 -0
- package/packages/omo-codex/plugin/scripts/sync-hook-status-messages.mjs +89 -0
- package/packages/omo-codex/plugin/scripts/sync-skills.mjs +6 -6
- package/packages/omo-codex/plugin/skills/init-deep/SKILL.md +6 -6
- package/packages/omo-codex/plugin/skills/lcx-report-bug/SKILL.md +127 -0
- package/packages/omo-codex/plugin/skills/lcx-report-bug/agents/openai.yaml +9 -0
- package/packages/omo-codex/plugin/skills/refactor/SKILL.md +6 -6
- package/packages/omo-codex/plugin/skills/remove-ai-slops/SKILL.md +6 -6
- package/packages/omo-codex/plugin/skills/review-work/SKILL.md +33 -8
- package/packages/omo-codex/plugin/skills/start-work/SKILL.md +25 -5
- package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +28 -205
- package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +231 -0
- package/packages/omo-codex/plugin/skills/ulw-plan/SKILL.md +17 -17
- package/packages/omo-codex/plugin/test/aggregate.test.mjs +188 -20
- package/packages/omo-codex/plugin/test/auto-update.test.mjs +129 -0
- package/packages/omo-codex/plugin/test/hook-status-message.test.mjs +58 -11
- package/packages/omo-codex/plugin/test/install-time-build-runtime.test.mjs +34 -0
- package/packages/omo-codex/plugin/test/mcp-research-servers.test.mjs +21 -0
- package/packages/omo-codex/plugin/test/migrate-codex-config.test.mjs +146 -0
- package/packages/omo-codex/plugin/test/node-install-surface.test.mjs +48 -0
- package/packages/omo-codex/plugin/test/subagent-guidance.test.mjs +76 -0
- package/packages/omo-codex/plugin/test/sync-hook-status-messages.test.mjs +67 -0
- package/packages/omo-codex/plugin/test/sync-skills.test.mjs +54 -2
- package/packages/omo-codex/scripts/install/cache.mjs +5 -3
- package/packages/omo-codex/scripts/install/cli-args.mjs +112 -0
- package/packages/omo-codex/scripts/install/config.mjs +23 -1
- package/packages/omo-codex/scripts/install/delegated-command.mjs +25 -0
- package/packages/omo-codex/scripts/install/git-bash.mjs +99 -0
- package/packages/omo-codex/scripts/install/git-bash.test.mjs +174 -0
- package/packages/omo-codex/scripts/install/legacy-bins.mjs +1 -0
- package/packages/omo-codex/scripts/install/mcp-runtime-cache.mjs +5 -1
- package/packages/omo-codex/scripts/install/model-catalog.mjs +66 -0
- package/packages/omo-codex/scripts/install/multi-agent-v2-config.mjs +7 -1
- package/packages/omo-codex/scripts/install/permissions.d.mts +1 -0
- package/packages/omo-codex/scripts/install/permissions.mjs +26 -0
- package/packages/omo-codex/scripts/install/project-local-cleanup.mjs +229 -0
- package/packages/omo-codex/scripts/install/reasoning-config.mjs +72 -0
- package/packages/omo-codex/scripts/install/source-package-build.mjs +20 -0
- package/packages/omo-codex/scripts/install/toml-editor.mjs +19 -2
- package/packages/omo-codex/scripts/install-bin-links.test.mjs +23 -0
- package/packages/omo-codex/scripts/install-cli-args.test.mjs +146 -0
- package/packages/omo-codex/scripts/install-config-autonomous.test.mjs +48 -0
- package/packages/omo-codex/scripts/install-config-reasoning.test.mjs +141 -0
- package/packages/omo-codex/scripts/install-config.test.mjs +205 -0
- package/packages/omo-codex/scripts/install-local-entrypoint.test.mjs +157 -0
- package/packages/omo-codex/scripts/install-local-git-bash-preflight.test.mjs +145 -0
- package/packages/omo-codex/scripts/install-local.mjs +91 -8
- package/packages/omo-codex/scripts/install-local.test.mjs +15 -0
- package/packages/omo-codex/scripts/install-mcp-runtime.test.mjs +60 -0
- package/packages/omo-codex/scripts/install-packaged-local.test.mjs +67 -0
- package/packages/omo-codex/scripts/install-project-local-cleanup.test.mjs +277 -0
- package/packages/shared-skills/skills/lcx-report-bug/SKILL.md +127 -0
- package/packages/shared-skills/skills/lcx-report-bug/agents/openai.yaml +9 -0
- package/packages/shared-skills/skills/review-work/SKILL.md +33 -8
- package/packages/shared-skills/skills/start-work/SKILL.md +25 -5
- package/packages/shared-skills/skills/ulw-plan/SKILL.md +11 -11
- package/postinstall.mjs +36 -3
- package/dist/hooks/context-window-monitor.d.ts +0 -19
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "bun:test";
|
|
2
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { Readable, Writable } from "node:stream";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
applyGitBashPostCompactReset,
|
|
9
|
+
applyGitBashPreToolUseReminder,
|
|
10
|
+
runGitBashHookCli,
|
|
11
|
+
type PostCompactPayload,
|
|
12
|
+
type PreToolUsePayload,
|
|
13
|
+
} from "../src/codex-hook.js";
|
|
14
|
+
|
|
15
|
+
const temporaryDirectories: string[] = [];
|
|
16
|
+
|
|
17
|
+
function createTemporaryDirectory(prefix: string): string {
|
|
18
|
+
const directory = mkdtempSync(join(tmpdir(), prefix));
|
|
19
|
+
temporaryDirectories.push(directory);
|
|
20
|
+
return directory;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
for (const directory of temporaryDirectories.splice(0)) {
|
|
25
|
+
rmSync(directory, { recursive: true, force: true });
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
function preToolPayload(toolName: string, sessionId = "session-1"): PreToolUsePayload {
|
|
30
|
+
return {
|
|
31
|
+
cwd: "/repo",
|
|
32
|
+
hook_event_name: "PreToolUse",
|
|
33
|
+
model: "gpt-5.5",
|
|
34
|
+
permission_mode: "default",
|
|
35
|
+
session_id: sessionId,
|
|
36
|
+
tool_input: { command: "pwd" },
|
|
37
|
+
tool_name: toolName,
|
|
38
|
+
tool_use_id: "call-1",
|
|
39
|
+
transcript_path: null,
|
|
40
|
+
turn_id: "turn-1",
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function postCompactPayload(sessionId = "session-1"): PostCompactPayload {
|
|
45
|
+
return {
|
|
46
|
+
hook_event_name: "PostCompact",
|
|
47
|
+
session_id: sessionId,
|
|
48
|
+
transcript_path: null,
|
|
49
|
+
trigger: "manual",
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function windowsEnv(): NodeJS.ProcessEnv {
|
|
54
|
+
return { OS: "Windows_NT", ComSpec: "C:\\Windows\\System32\\cmd.exe" };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function captureStdout(): { readonly stdout: Writable; readonly read: () => string } {
|
|
58
|
+
let captured = "";
|
|
59
|
+
const stdout = new Writable({
|
|
60
|
+
write(chunk: unknown, _encoding: BufferEncoding, callback: (error?: Error | null) => void): void {
|
|
61
|
+
captured += chunk instanceof Buffer ? chunk.toString() : String(chunk);
|
|
62
|
+
callback();
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
return { stdout, read: () => captured };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
describe("applyGitBashPreToolUseReminder", () => {
|
|
69
|
+
it("#given first Windows Bash call #when hook runs #then emits non-blocking git_bash guidance", () => {
|
|
70
|
+
// given
|
|
71
|
+
const pluginDataRoot = createTemporaryDirectory("omo-git-bash-hook-");
|
|
72
|
+
|
|
73
|
+
// when
|
|
74
|
+
const output = applyGitBashPreToolUseReminder(preToolPayload("Bash"), {
|
|
75
|
+
env: windowsEnv(),
|
|
76
|
+
platform: "linux",
|
|
77
|
+
pluginDataRoot,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// then
|
|
81
|
+
const parsed = JSON.parse(output);
|
|
82
|
+
expect(parsed.hookSpecificOutput).toEqual({
|
|
83
|
+
hookEventName: "PreToolUse",
|
|
84
|
+
additionalContext:
|
|
85
|
+
"On Windows, prefer the OMO git_bash MCP for shell commands before using built-in exec_command. Use exec_command only when git_bash is unavailable or for non-shell operations.",
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("#given second Windows Bash call in same session #when hook runs #then it stays silent", () => {
|
|
90
|
+
// given
|
|
91
|
+
const pluginDataRoot = createTemporaryDirectory("omo-git-bash-hook-");
|
|
92
|
+
const payload = preToolPayload("Bash");
|
|
93
|
+
|
|
94
|
+
// when
|
|
95
|
+
const first = applyGitBashPreToolUseReminder(payload, { env: windowsEnv(), platform: "linux", pluginDataRoot });
|
|
96
|
+
const second = applyGitBashPreToolUseReminder(payload, { env: windowsEnv(), platform: "linux", pluginDataRoot });
|
|
97
|
+
|
|
98
|
+
// then
|
|
99
|
+
expect(first).toContain("git_bash");
|
|
100
|
+
expect(second).toBe("");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("#given non-Windows Bash call #when hook runs #then it stays silent", () => {
|
|
104
|
+
// given
|
|
105
|
+
const pluginDataRoot = createTemporaryDirectory("omo-git-bash-hook-");
|
|
106
|
+
|
|
107
|
+
// when
|
|
108
|
+
const output = applyGitBashPreToolUseReminder(preToolPayload("Bash"), {
|
|
109
|
+
env: {},
|
|
110
|
+
platform: "darwin",
|
|
111
|
+
pluginDataRoot,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// then
|
|
115
|
+
expect(output).toBe("");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("#given non-Bash tool call #when hook runs #then it stays silent", () => {
|
|
119
|
+
// given
|
|
120
|
+
const pluginDataRoot = createTemporaryDirectory("omo-git-bash-hook-");
|
|
121
|
+
|
|
122
|
+
// when
|
|
123
|
+
const output = applyGitBashPreToolUseReminder(preToolPayload("exec_command"), {
|
|
124
|
+
env: windowsEnv(),
|
|
125
|
+
platform: "linux",
|
|
126
|
+
pluginDataRoot,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// then
|
|
130
|
+
expect(output).toBe("");
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("applyGitBashPostCompactReset", () => {
|
|
135
|
+
it("#given reminder already emitted #when PostCompact runs #then next Windows Bash call emits reminder again", () => {
|
|
136
|
+
// given
|
|
137
|
+
const pluginDataRoot = createTemporaryDirectory("omo-git-bash-hook-");
|
|
138
|
+
const payload = preToolPayload("Bash");
|
|
139
|
+
const first = applyGitBashPreToolUseReminder(payload, { env: windowsEnv(), platform: "linux", pluginDataRoot });
|
|
140
|
+
const second = applyGitBashPreToolUseReminder(payload, { env: windowsEnv(), platform: "linux", pluginDataRoot });
|
|
141
|
+
|
|
142
|
+
// when
|
|
143
|
+
applyGitBashPostCompactReset(postCompactPayload(), { pluginDataRoot });
|
|
144
|
+
const afterCompact = applyGitBashPreToolUseReminder(payload, {
|
|
145
|
+
env: windowsEnv(),
|
|
146
|
+
platform: "linux",
|
|
147
|
+
pluginDataRoot,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// then
|
|
151
|
+
expect(first).toContain("git_bash");
|
|
152
|
+
expect(second).toBe("");
|
|
153
|
+
expect(afterCompact).toContain("git_bash");
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe("runGitBashHookCli", () => {
|
|
158
|
+
it("#given Codex PreToolUse stdin on Windows #when CLI hook runs #then it writes reminder JSON", async () => {
|
|
159
|
+
// given
|
|
160
|
+
const pluginDataRoot = createTemporaryDirectory("omo-git-bash-hook-");
|
|
161
|
+
const stdin = Readable.from([JSON.stringify(preToolPayload("Bash"))]);
|
|
162
|
+
const capture = captureStdout();
|
|
163
|
+
|
|
164
|
+
// when
|
|
165
|
+
await runGitBashHookCli(stdin, capture.stdout, "pre-tool-use", {
|
|
166
|
+
env: windowsEnv(),
|
|
167
|
+
platform: "linux",
|
|
168
|
+
pluginDataRoot,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// then
|
|
172
|
+
expect(capture.read()).toContain("git_bash MCP");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("#given PostCompact stdin #when CLI hook runs #then it resets the one-shot reminder", async () => {
|
|
176
|
+
// given
|
|
177
|
+
const pluginDataRoot = createTemporaryDirectory("omo-git-bash-hook-");
|
|
178
|
+
const payload = preToolPayload("Bash");
|
|
179
|
+
applyGitBashPreToolUseReminder(payload, { env: windowsEnv(), platform: "linux", pluginDataRoot });
|
|
180
|
+
const resetStdin = Readable.from([JSON.stringify(postCompactPayload())]);
|
|
181
|
+
const capture = captureStdout();
|
|
182
|
+
|
|
183
|
+
// when
|
|
184
|
+
await runGitBashHookCli(resetStdin, capture.stdout, "post-compact", { pluginDataRoot });
|
|
185
|
+
const afterCompact = applyGitBashPreToolUseReminder(payload, {
|
|
186
|
+
env: windowsEnv(),
|
|
187
|
+
platform: "linux",
|
|
188
|
+
pluginDataRoot,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// then
|
|
192
|
+
expect(capture.read()).toBe("");
|
|
193
|
+
expect(afterCompact).toContain("git_bash");
|
|
194
|
+
});
|
|
195
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"allowImportingTsExtensions": false,
|
|
5
|
+
"declaration": true,
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"rootDir": "src",
|
|
8
|
+
"noEmit": false,
|
|
9
|
+
"types": ["node"]
|
|
10
|
+
},
|
|
11
|
+
"include": ["src/**/*"],
|
|
12
|
+
"exclude": ["test/**/*"]
|
|
13
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "Node16",
|
|
5
|
+
"moduleResolution": "Node16",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"strict": true,
|
|
8
|
+
"exactOptionalPropertyTypes": true,
|
|
9
|
+
"noUncheckedIndexedAccess": true,
|
|
10
|
+
"noPropertyAccessFromIndexSignature": true,
|
|
11
|
+
"verbatimModuleSyntax": true,
|
|
12
|
+
"noImplicitOverride": true,
|
|
13
|
+
"noImplicitReturns": true,
|
|
14
|
+
"noFallthroughCasesInSwitch": true,
|
|
15
|
+
"noUnusedLocals": true,
|
|
16
|
+
"noUnusedParameters": true,
|
|
17
|
+
"esModuleInterop": true,
|
|
18
|
+
"allowImportingTsExtensions": true,
|
|
19
|
+
"skipLibCheck": true,
|
|
20
|
+
"forceConsistentCasingInFileNames": true,
|
|
21
|
+
"types": ["node", "bun-types"],
|
|
22
|
+
"noEmit": true
|
|
23
|
+
},
|
|
24
|
+
"include": ["src/**/*", "test/**/*"]
|
|
25
|
+
}
|
|
@@ -118,7 +118,7 @@ printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | node dist/cli.j
|
|
|
118
118
|
## Local Codex Installation
|
|
119
119
|
|
|
120
120
|
```bash
|
|
121
|
-
|
|
121
|
+
npx lazycodex-ai install
|
|
122
122
|
```
|
|
123
123
|
|
|
124
124
|
The installer builds and copies the plugin into `~/.codex/plugins/cache/sisyphuslabs/omo/0.1.0`, registers the `sisyphuslabs` marketplace from the `lazycodex` Git repository, and enables:
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
-
import {
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
4
|
import { argv, execPath, stderr } from "node:process";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
5
|
|
|
7
|
-
import { runPostToolUseHookCli } from "./codex-hook.js";
|
|
6
|
+
import { runPostToolUseHookCli } from "./codex-hook-cli.js";
|
|
8
7
|
|
|
9
|
-
const
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
const PACKAGE_LSP_MCP_CLI = "@code-yeongyu/lsp-tools-mcp/dist/cli.js";
|
|
10
10
|
|
|
11
11
|
async function main(): Promise<void> {
|
|
12
12
|
const [command = "mcp", subcommand = ""] = argv.slice(2);
|
|
@@ -31,7 +31,7 @@ main().catch((error: unknown) => {
|
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
async function runPackageLspMcpCli(): Promise<void> {
|
|
34
|
-
const cliPath = resolve(
|
|
34
|
+
const cliPath = require.resolve(PACKAGE_LSP_MCP_CLI);
|
|
35
35
|
const child = spawn(execPath, [cliPath, "mcp"], { stdio: "inherit" });
|
|
36
36
|
await new Promise<void>((resolve, reject) => {
|
|
37
37
|
child.once("error", reject);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { stdin as processStdin } from "node:process";
|
|
2
|
+
|
|
3
|
+
import { disposeDefaultLspManager } from "@code-yeongyu/lsp-tools-mcp/dist/lsp/manager.js";
|
|
4
|
+
|
|
5
|
+
import { isRecord, runLspPostToolUseHook } from "./codex-hook.js";
|
|
6
|
+
|
|
7
|
+
export async function runPostToolUseHookCli(stdin: NodeJS.ReadStream = processStdin): Promise<void> {
|
|
8
|
+
try {
|
|
9
|
+
const raw = await readStdin(stdin);
|
|
10
|
+
if (!raw.trim()) return;
|
|
11
|
+
let parsed: unknown;
|
|
12
|
+
try {
|
|
13
|
+
parsed = JSON.parse(raw);
|
|
14
|
+
} catch (error) {
|
|
15
|
+
if (error instanceof SyntaxError) return;
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
const input = isRecord(parsed) ? parsed : {};
|
|
19
|
+
const output = await runLspPostToolUseHook(input);
|
|
20
|
+
if (output) process.stdout.write(output);
|
|
21
|
+
} finally {
|
|
22
|
+
await disposeDefaultLspManager();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function readStdin(stdin: NodeJS.ReadStream): Promise<string> {
|
|
27
|
+
stdin.setEncoding("utf8");
|
|
28
|
+
let raw = "";
|
|
29
|
+
for await (const chunk of stdin) {
|
|
30
|
+
raw += chunk;
|
|
31
|
+
}
|
|
32
|
+
return raw;
|
|
33
|
+
}
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
import { stdin as processStdin } from "node:process";
|
|
3
2
|
|
|
4
|
-
import {
|
|
5
|
-
import { executeLspDiagnostics } from "../../../../../lsp-tools-mcp/dist/tools.js";
|
|
3
|
+
import { executeLspDiagnostics } from "@code-yeongyu/lsp-tools-mcp/dist/tools.js";
|
|
6
4
|
|
|
7
5
|
export type DiagnosticsRunner = (filePath: string) => Promise<string>;
|
|
8
6
|
|
|
@@ -91,13 +89,29 @@ async function collectDiagnostics(
|
|
|
91
89
|
nextIndex += 1;
|
|
92
90
|
const filePath = filePaths[index];
|
|
93
91
|
if (filePath === undefined) return;
|
|
94
|
-
results[index] = { filePath, diagnostics:
|
|
92
|
+
results[index] = { filePath, diagnostics: await collectFileDiagnostics(filePath, runDiagnostics) };
|
|
95
93
|
}
|
|
96
94
|
}
|
|
97
95
|
await Promise.all(Array.from({ length: workerCount }, () => worker()));
|
|
98
96
|
return results;
|
|
99
97
|
}
|
|
100
98
|
|
|
99
|
+
async function collectFileDiagnostics(filePath: string, runDiagnostics: DiagnosticsRunner): Promise<string> {
|
|
100
|
+
try {
|
|
101
|
+
return (await runDiagnostics(filePath)).trim();
|
|
102
|
+
} catch (error) {
|
|
103
|
+
return formatDiagnosticsError(error);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function formatDiagnosticsError(error: unknown): string {
|
|
108
|
+
if (error instanceof Error) {
|
|
109
|
+
const message = error.message.trim();
|
|
110
|
+
if (message.length > 0) return message;
|
|
111
|
+
}
|
|
112
|
+
return String(error).trim();
|
|
113
|
+
}
|
|
114
|
+
|
|
101
115
|
function formatDiagnosticBlock({ filePath, diagnostics }: DiagnosticBlock): string {
|
|
102
116
|
return `LSP diagnostics after editing ${filePath}:\n\n${formatDiagnosticsForDisplay(diagnostics)}`;
|
|
103
117
|
}
|
|
@@ -191,19 +205,6 @@ export function extractMutatedFilePaths(input: CodexPostToolUseInput): string[]
|
|
|
191
205
|
return [...paths];
|
|
192
206
|
}
|
|
193
207
|
|
|
194
|
-
export async function runPostToolUseHookCli(stdin: NodeJS.ReadStream = processStdin): Promise<void> {
|
|
195
|
-
try {
|
|
196
|
-
const raw = await readStdin(stdin);
|
|
197
|
-
if (!raw.trim()) return;
|
|
198
|
-
const parsed: unknown = JSON.parse(raw);
|
|
199
|
-
const input = isRecord(parsed) ? parsed : {};
|
|
200
|
-
const output = await runLspPostToolUseHook(input);
|
|
201
|
-
if (output) process.stdout.write(output);
|
|
202
|
-
} finally {
|
|
203
|
-
await disposeDefaultLspManager();
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
208
|
function isMutationTool(value: unknown): boolean {
|
|
208
209
|
if (typeof value !== "string") return false;
|
|
209
210
|
return MUTATION_TOOL_NAMES.has(value.toLowerCase());
|
|
@@ -271,15 +272,6 @@ function addPatchFiles(paths: Set<string>, value: unknown): void {
|
|
|
271
272
|
}
|
|
272
273
|
}
|
|
273
274
|
|
|
274
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
275
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
275
276
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
276
277
|
}
|
|
277
|
-
|
|
278
|
-
async function readStdin(stdin: NodeJS.ReadStream): Promise<string> {
|
|
279
|
-
stdin.setEncoding("utf8");
|
|
280
|
-
let raw = "";
|
|
281
|
-
for await (const chunk of stdin) {
|
|
282
|
-
raw += chunk;
|
|
283
|
-
}
|
|
284
|
-
return raw;
|
|
285
|
-
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
|
|
7
|
+
describe("codex PostToolUse hook CLI", () => {
|
|
8
|
+
it("#given malformed post-tool-use stdin #when hook CLI runs #then it no-ops without stderr", () => {
|
|
9
|
+
// given
|
|
10
|
+
const input = "break;\n";
|
|
11
|
+
|
|
12
|
+
// when
|
|
13
|
+
const result = runBuiltHookCli(input);
|
|
14
|
+
|
|
15
|
+
// then
|
|
16
|
+
expect(result.status).toBe(0);
|
|
17
|
+
expect(result.stderr).toBe("");
|
|
18
|
+
expect(result.stdout).toBe("");
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
function runBuiltHookCli(input: string): ReturnType<typeof spawnSync> {
|
|
23
|
+
const cliPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../dist/cli.js");
|
|
24
|
+
return spawnSync(process.execPath, [cliPath, "hook", "post-tool-use"], {
|
|
25
|
+
input,
|
|
26
|
+
encoding: "utf8",
|
|
27
|
+
});
|
|
28
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { runLspPostToolUseHook } from "../src/codex-hook.js";
|
|
4
|
+
|
|
5
|
+
describe("codex PostToolUse diagnostics errors", () => {
|
|
6
|
+
it("#given diagnostics runner throws for a mutated file #when the hook evaluates diagnostics #then it returns blocked output with the thrown message", async () => {
|
|
7
|
+
// given
|
|
8
|
+
const output = await runLspPostToolUseHook(
|
|
9
|
+
{
|
|
10
|
+
tool_name: "write",
|
|
11
|
+
tool_input: { path: "src/missing.ts" },
|
|
12
|
+
tool_response: { ok: true },
|
|
13
|
+
},
|
|
14
|
+
async (filePath) => {
|
|
15
|
+
expect(filePath).toBe("src/missing.ts");
|
|
16
|
+
throw new Error("ENOENT: no such file or directory, open 'src/missing.ts'");
|
|
17
|
+
},
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
// when
|
|
21
|
+
const parsed: unknown = JSON.parse(output);
|
|
22
|
+
if (!isPostToolUseHookOutput(parsed)) throw new TypeError("Expected PostToolUse hook output");
|
|
23
|
+
|
|
24
|
+
// then
|
|
25
|
+
expect(parsed.reason).toBe(
|
|
26
|
+
"LSP diagnostics after editing src/missing.ts:\n\nENOENT: no such file or directory, open 'src/missing.ts'",
|
|
27
|
+
);
|
|
28
|
+
expect(parsed.hookSpecificOutput.additionalContext).toBe(parsed.reason);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
interface PostToolUseHookOutput {
|
|
33
|
+
readonly decision: "block";
|
|
34
|
+
readonly reason: string;
|
|
35
|
+
readonly hookSpecificOutput: {
|
|
36
|
+
readonly hookEventName: "PostToolUse";
|
|
37
|
+
readonly additionalContext: string;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isPostToolUseHookOutput(value: unknown): value is PostToolUseHookOutput {
|
|
42
|
+
if (!isRecord(value)) return false;
|
|
43
|
+
const hookSpecificOutput = value["hookSpecificOutput"];
|
|
44
|
+
return (
|
|
45
|
+
value["decision"] === "block" &&
|
|
46
|
+
typeof value["reason"] === "string" &&
|
|
47
|
+
isRecord(hookSpecificOutput) &&
|
|
48
|
+
hookSpecificOutput["hookEventName"] === "PostToolUse" &&
|
|
49
|
+
typeof hookSpecificOutput["additionalContext"] === "string"
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
54
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
55
|
+
}
|
|
@@ -56,6 +56,7 @@ describe("plugin package metadata", () => {
|
|
|
56
56
|
const hooksJson = readHooksJson("hooks/hooks.json");
|
|
57
57
|
const mcpJson = readMcpJson(".mcp.json");
|
|
58
58
|
const cliSource = readFileSync("src/cli.ts", "utf8");
|
|
59
|
+
const codexHookCliSource = readFileSync("src/codex-hook-cli.ts", "utf8");
|
|
59
60
|
const codexHookSource = readFileSync("src/codex-hook.ts", "utf8");
|
|
60
61
|
const sourceFiles = readdirSync("src");
|
|
61
62
|
|
|
@@ -79,11 +80,12 @@ describe("plugin package metadata", () => {
|
|
|
79
80
|
expect(lspServer?.command).toBe("node");
|
|
80
81
|
expect(lspServer?.args).toEqual(["../../../../lsp-tools-mcp/dist/cli.js", "mcp"]);
|
|
81
82
|
expect(cliSource).not.toContain("./lazy-lsp-mcp.js");
|
|
82
|
-
expect(cliSource).
|
|
83
|
-
expect(cliSource).toContain("../../../../../lsp-tools-mcp/dist/cli.js");
|
|
84
|
-
expect(
|
|
85
|
-
expect(codexHookSource).toContain("
|
|
86
|
-
expect(
|
|
83
|
+
expect(cliSource).toContain("@code-yeongyu/lsp-tools-mcp/dist/cli.js");
|
|
84
|
+
expect(cliSource).not.toContain("../../../../../lsp-tools-mcp/dist/cli.js");
|
|
85
|
+
expect(codexHookCliSource).toContain("@code-yeongyu/lsp-tools-mcp/dist/lsp/manager.js");
|
|
86
|
+
expect(codexHookSource).toContain("@code-yeongyu/lsp-tools-mcp/dist/tools.js");
|
|
87
|
+
expect(codexHookCliSource).not.toContain("../../../../../lsp-tools-mcp/dist/lsp/manager.js");
|
|
88
|
+
expect(codexHookSource).not.toContain("../../../../../lsp-tools-mcp/dist/tools.js");
|
|
87
89
|
expect(sourceFiles.filter((name) => name.startsWith("lazy-mcp") || name === "lazy-lsp-mcp.ts")).toEqual([]);
|
|
88
90
|
});
|
|
89
91
|
|
|
@@ -79,13 +79,15 @@ omo-codex bundles three read-only Codex subagent roles in `CODEX_HOME/agents/`:
|
|
|
79
79
|
|
|
80
80
|
**Routing:**
|
|
81
81
|
|
|
82
|
-
- "Where is X?" / "Find code that does Y" -> `spawn_agent(agent_type="explorer", ...)`
|
|
83
|
-
- "How does library Z work?" / "What's the API contract?" -> `spawn_agent(agent_type="librarian", ...)`
|
|
84
|
-
- 5+ interdependent steps, ambiguous scope, multi-module work -> `spawn_agent(agent_type="plan", ...)`
|
|
85
|
-
- Heavy verification of a finished change -> `spawn_agent(agent_type="codex-ultrawork-reviewer", ...)`
|
|
82
|
+
- "Where is X?" / "Find code that does Y" -> `spawn_agent(agent_type="explorer", fork_turns="none", ...)`
|
|
83
|
+
- "How does library Z work?" / "What's the API contract?" -> `spawn_agent(agent_type="librarian", fork_turns="none", ...)`
|
|
84
|
+
- 5+ interdependent steps, ambiguous scope, multi-module work -> `spawn_agent(agent_type="plan", fork_turns="none", ...)`
|
|
85
|
+
- Heavy verification of a finished change -> `spawn_agent(agent_type="codex-ultrawork-reviewer", fork_turns="none", ...)`
|
|
86
86
|
|
|
87
87
|
**Don't duplicate.** Once a subagent is dispatched for a question, do not re-do the same search yourself. Once results return, do not re-verify by repeating their tool calls; integrate and move on.
|
|
88
88
|
|
|
89
|
+
**Keep parent liveness visible.** While any child is active, keep the parent visibly alive with brief status updates that include active subagent count, agent names, last heartbeat, and whether the parent is waiting for mailbox updates. Do this during long `wait_agent` cycles so the session does not look idle while children are still running.
|
|
90
|
+
|
|
89
91
|
# Operating Loop
|
|
90
92
|
|
|
91
93
|
**Explore -> Plan -> Implement -> Verify -> Manually QA.** Loops are short and tight; do not loop back with a draft when the work is yours to do.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Windows Git Bash guidance for Codex
|
|
3
|
+
alwaysApply: true
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
On Windows native Codex sessions, prefer Git Bash for shell commands.
|
|
7
|
+
|
|
8
|
+
Use `shell: "bash"` when `bash.exe` is on PATH. Otherwise use the absolute Git Bash path from `OMO_CODEX_GIT_BASH_PATH` or `C:\Program Files\Git\bin\bash.exe`.
|
|
9
|
+
|
|
10
|
+
Use PowerShell only for Windows-native operations that need PowerShell.
|
|
@@ -24,8 +24,6 @@ const MODEL_CONTEXT_BUDGETS: readonly ModelContextBudget[] = [
|
|
|
24
24
|
{ slug: "gpt-5.5", contextWindowTokens: 272_000, effectivePercent: DEFAULT_EFFECTIVE_CONTEXT_WINDOW_PERCENT },
|
|
25
25
|
{ slug: "gpt-5.4", contextWindowTokens: 272_000, effectivePercent: DEFAULT_EFFECTIVE_CONTEXT_WINDOW_PERCENT },
|
|
26
26
|
{ slug: "gpt-5.4-mini", contextWindowTokens: 272_000, effectivePercent: DEFAULT_EFFECTIVE_CONTEXT_WINDOW_PERCENT },
|
|
27
|
-
{ slug: "gpt-5.3-codex", contextWindowTokens: 272_000, effectivePercent: DEFAULT_EFFECTIVE_CONTEXT_WINDOW_PERCENT },
|
|
28
|
-
{ slug: "gpt-5.2", contextWindowTokens: 272_000, effectivePercent: DEFAULT_EFFECTIVE_CONTEXT_WINDOW_PERCENT },
|
|
29
27
|
{
|
|
30
28
|
slug: "codex-auto-review",
|
|
31
29
|
contextWindowTokens: 272_000,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
1
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
2
2
|
import { describe, expect, it } from "vitest";
|
|
3
3
|
|
|
4
4
|
type PackageJson = {
|
|
@@ -51,6 +51,7 @@ describe("plugin package metadata", () => {
|
|
|
51
51
|
const pluginJson = readPluginJson(".codex-plugin/plugin.json");
|
|
52
52
|
const hooksJson = readHooksJson("hooks/hooks.json");
|
|
53
53
|
const cliSource = readFileSync("src/cli.ts", "utf8");
|
|
54
|
+
const bundledRules = readdirSync("bundled-rules").sort();
|
|
54
55
|
|
|
55
56
|
// when
|
|
56
57
|
const hookConfig = hooksJson.hooks;
|
|
@@ -69,6 +70,7 @@ describe("plugin package metadata", () => {
|
|
|
69
70
|
expect(packageJson.dependencies ?? {}).toEqual({ picomatch: "^4.0.3" });
|
|
70
71
|
expect(packageJson.bin["omo-rules"]).toBe("./dist/cli.js");
|
|
71
72
|
expect(packageJson.files).toContain("bundled-rules");
|
|
73
|
+
expect(bundledRules).toContain("windows-git-bash.md");
|
|
72
74
|
expect(pluginJson.hooks).toBe("./hooks/hooks.json");
|
|
73
75
|
expect(cliSource.startsWith("#!/usr/bin/env node")).toBe(true);
|
|
74
76
|
expect(commands).toEqual([
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
+
|
|
6
|
+
import { runSessionStartHook, type CodexSessionStartInput } from "../src/codex-hook.js";
|
|
7
|
+
import { findPluginBundledCandidates } from "../src/rules/finder.js";
|
|
8
|
+
|
|
9
|
+
const WINDOWS_RULE_DESCRIPTION = "Windows Git Bash guidance for Codex";
|
|
10
|
+
const WINDOWS_RULE_PATH = "bundled-rules/windows-git-bash.md";
|
|
11
|
+
const WINDOWS_GUIDANCE = "On Windows native Codex sessions, prefer Git Bash for shell commands.";
|
|
12
|
+
const BUNDLED_ONLY_ENV = {
|
|
13
|
+
CODEX_RULES_ENABLED_SOURCES: "plugin-bundled",
|
|
14
|
+
};
|
|
15
|
+
const PROJECT_AND_BUNDLED_ENV = {
|
|
16
|
+
CODEX_RULES_ENABLED_SOURCES: ".omo/rules,plugin-bundled",
|
|
17
|
+
};
|
|
18
|
+
const tempDirectories: string[] = [];
|
|
19
|
+
let originalPluginRoot: string | undefined;
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
restoreEnv("PLUGIN_ROOT", originalPluginRoot);
|
|
23
|
+
for (const directory of tempDirectories.splice(0)) {
|
|
24
|
+
rmSync(directory, { recursive: true, force: true });
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
function makeProject(): { readonly root: string; readonly pluginData: string } {
|
|
29
|
+
originalPluginRoot = process.env["PLUGIN_ROOT"];
|
|
30
|
+
process.env["PLUGIN_ROOT"] = process.cwd();
|
|
31
|
+
const root = mkdtempSync(join(tmpdir(), "codex-rules-windows-git-bash-project-"));
|
|
32
|
+
const pluginData = mkdtempSync(join(tmpdir(), "codex-rules-windows-git-bash-data-"));
|
|
33
|
+
tempDirectories.push(root, pluginData);
|
|
34
|
+
writeFileSync(join(root, "package.json"), JSON.stringify({ name: "fixture" }));
|
|
35
|
+
return { root, pluginData };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function sessionStartInput(root: string): CodexSessionStartInput {
|
|
39
|
+
return {
|
|
40
|
+
session_id: "session-1",
|
|
41
|
+
transcript_path: null,
|
|
42
|
+
cwd: root,
|
|
43
|
+
hook_event_name: "SessionStart",
|
|
44
|
+
model: "gpt-5.5",
|
|
45
|
+
permission_mode: "default",
|
|
46
|
+
source: "startup",
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function restoreEnv(name: string, value: string | undefined): void {
|
|
51
|
+
if (value === undefined) {
|
|
52
|
+
delete process.env[name];
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
process.env[name] = value;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function occurrenceCount(value: string, search: string): number {
|
|
59
|
+
return value.split(search).length - 1;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
describe("Windows Git Bash bundled rule", () => {
|
|
63
|
+
it("#given packaged bundled rules #when discovering plugin-bundled candidates #then Windows Git Bash rule is included", () => {
|
|
64
|
+
const candidates = findPluginBundledCandidates({ pluginRoot: process.cwd() });
|
|
65
|
+
|
|
66
|
+
expect(candidates.map((candidate) => candidate.relativePath)).toContain(WINDOWS_RULE_PATH);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("#given bundled rules enabled #when SessionStart runs #then Windows Git Bash guidance is injected once", async () => {
|
|
70
|
+
const { root, pluginData } = makeProject();
|
|
71
|
+
|
|
72
|
+
const output = await runSessionStartHook(sessionStartInput(root), {
|
|
73
|
+
pluginDataRoot: pluginData,
|
|
74
|
+
env: BUNDLED_ONLY_ENV,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
expect(occurrenceCount(output, WINDOWS_GUIDANCE)).toBe(1);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("#given project rule with same description #when static rules load #then project guidance overrides bundled guidance", async () => {
|
|
81
|
+
const { root, pluginData } = makeProject();
|
|
82
|
+
const projectGuidance = "Project-specific Windows shell policy.";
|
|
83
|
+
mkdirSync(join(root, ".omo", "rules"), { recursive: true });
|
|
84
|
+
writeFileSync(
|
|
85
|
+
join(root, ".omo", "rules", "windows-git-bash.md"),
|
|
86
|
+
["---", `description: ${WINDOWS_RULE_DESCRIPTION}`, "alwaysApply: true", "---", "", projectGuidance].join("\n"),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const output = await runSessionStartHook(sessionStartInput(root), {
|
|
90
|
+
pluginDataRoot: pluginData,
|
|
91
|
+
env: PROJECT_AND_BUNDLED_ENV,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
expect(output).toContain(projectGuidance);
|
|
95
|
+
expect(output).not.toContain(WINDOWS_GUIDANCE);
|
|
96
|
+
});
|
|
97
|
+
});
|