oh-my-opencode 4.7.0 → 4.7.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/dist/cli/index.js +5334 -5150
- package/dist/index.js +3447 -3334
- package/package.json +13 -13
- package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +13 -0
- package/packages/omo-codex/plugin/components/lsp/src/cli.ts +6 -2
- package/packages/omo-codex/plugin/components/lsp/src/codex-hook-cli.ts +13 -2
- package/packages/omo-codex/plugin/components/lsp/src/codex-hook.ts +30 -79
- package/packages/omo-codex/plugin/components/lsp/src/lsp-session-state.ts +116 -0
- package/packages/omo-codex/plugin/components/lsp/src/mutated-file-paths.ts +88 -0
- package/packages/omo-codex/plugin/components/lsp/test/codex-hook-unavailable.test.ts +206 -0
- package/packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts +5 -3
- package/packages/omo-codex/plugin/components/rules/src/codex-hook-options.ts +1 -0
- package/packages/omo-codex/plugin/components/rules/src/rules/finder.ts +15 -2
- package/packages/omo-codex/plugin/components/rules/src/rules-engine-factory.ts +4 -1
- package/packages/omo-codex/plugin/components/rules/test/windows-git-bash-bundled-rule.test.ts +28 -5
- package/packages/omo-codex/plugin/hooks/hooks.json +13 -2
- package/packages/omo-codex/plugin/scripts/sync-hook-status-messages.mjs +1 -8
- package/packages/omo-codex/plugin/test/aggregate.test.mjs +16 -0
- package/packages/omo-codex/plugin/test/hook-status-message.test.mjs +6 -28
- package/packages/omo-codex/plugin/test/sync-hook-status-messages.test.mjs +26 -1
- package/packages/omo-codex/scripts/install/permissions.mjs +11 -0
- package/packages/omo-codex/scripts/install-config-autonomous-features.test.mjs +83 -0
- package/packages/omo-codex/scripts/install-local.test.mjs +3 -1
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
6
|
+
|
|
7
|
+
import { runLspPostCompactHook, runLspPostToolUseHook } from "../src/codex-hook.js";
|
|
8
|
+
|
|
9
|
+
const MARKSMAN_INITIALIZE_TIMEOUT = [
|
|
10
|
+
"LSP request timeout (method: initialize)",
|
|
11
|
+
'recent stderr: [01:16:41 INF] <LSP Entry> Starting Marksman LSP server: {"arch":"Arm64"}',
|
|
12
|
+
'[01:16:41 INF] <Folder> Loading folder documents: {"uri":"file:///repo"}',
|
|
13
|
+
].join("\n");
|
|
14
|
+
|
|
15
|
+
const tempDirs: string[] = [];
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
for (const tempDir of tempDirs.splice(0)) {
|
|
19
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe("codex PostToolUse unavailable LSP suppression", () => {
|
|
24
|
+
it("#given unavailable markdown LSP in one session #when PostToolUse repeats #then suppresses feedback and skips the cached extension", async () => {
|
|
25
|
+
// given
|
|
26
|
+
const pluginData = tempPluginData();
|
|
27
|
+
const input = postToolUseInput("session-unavailable", ".omo/ulw-loop/evidence/note.md");
|
|
28
|
+
let calls = 0;
|
|
29
|
+
|
|
30
|
+
await withPluginData(pluginData, async () => {
|
|
31
|
+
// when
|
|
32
|
+
const firstOutput = await runLspPostToolUseHook(input, async () => {
|
|
33
|
+
calls += 1;
|
|
34
|
+
return MARKSMAN_INITIALIZE_TIMEOUT;
|
|
35
|
+
});
|
|
36
|
+
const secondOutput = await runLspPostToolUseHook(input, async () => {
|
|
37
|
+
calls += 1;
|
|
38
|
+
return "error[markdown] (1000) at 1:1: second call should have been skipped.";
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// then
|
|
42
|
+
expect(firstOutput).toBe("");
|
|
43
|
+
expect(secondOutput).toBe("");
|
|
44
|
+
expect(calls).toBe(1);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("#given cached unavailable LSP after PostCompact #when the next PostToolUse runs #then probes once and suppresses again", async () => {
|
|
49
|
+
// given
|
|
50
|
+
const pluginData = tempPluginData();
|
|
51
|
+
const input = postToolUseInput("session-compact", ".omo/ulw-loop/evidence/note.md");
|
|
52
|
+
let calls = 0;
|
|
53
|
+
|
|
54
|
+
await withPluginData(pluginData, async () => {
|
|
55
|
+
await runLspPostToolUseHook(input, async () => {
|
|
56
|
+
calls += 1;
|
|
57
|
+
return MARKSMAN_INITIALIZE_TIMEOUT;
|
|
58
|
+
});
|
|
59
|
+
await runLspPostToolUseHook(input, async () => {
|
|
60
|
+
calls += 1;
|
|
61
|
+
return "error[markdown] (1000) at 1:1: cached call should have been skipped.";
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// when
|
|
65
|
+
const compactInput = {
|
|
66
|
+
cwd: "/repo",
|
|
67
|
+
hook_event_name: "PostCompact",
|
|
68
|
+
model: "gpt-5.5",
|
|
69
|
+
session_id: "session-compact",
|
|
70
|
+
transcript_path: null,
|
|
71
|
+
trigger: "manual",
|
|
72
|
+
turn_id: "turn-compact",
|
|
73
|
+
};
|
|
74
|
+
const compactOutput = await runLspPostCompactHook(compactInput);
|
|
75
|
+
const afterCompactOutput = await runLspPostToolUseHook(input, async () => {
|
|
76
|
+
calls += 1;
|
|
77
|
+
return MARKSMAN_INITIALIZE_TIMEOUT;
|
|
78
|
+
});
|
|
79
|
+
await runLspPostToolUseHook(input, async () => {
|
|
80
|
+
calls += 1;
|
|
81
|
+
return "error[markdown] (1000) at 1:1: post-compact cached call should have been skipped.";
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// then
|
|
85
|
+
expect(compactOutput).toBe("");
|
|
86
|
+
expect(afterCompactOutput).toBe("");
|
|
87
|
+
expect(calls).toBe(2);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("#given cached unavailable LSP after PostCompact #when the probe is clean #then clears the unavailable cache", async () => {
|
|
92
|
+
// given
|
|
93
|
+
const pluginData = tempPluginData();
|
|
94
|
+
const input = postToolUseInput("session-compact-clean", ".omo/ulw-loop/evidence/note.md");
|
|
95
|
+
let calls = 0;
|
|
96
|
+
|
|
97
|
+
await withPluginData(pluginData, async () => {
|
|
98
|
+
await runLspPostToolUseHook(input, async () => {
|
|
99
|
+
calls += 1;
|
|
100
|
+
return MARKSMAN_INITIALIZE_TIMEOUT;
|
|
101
|
+
});
|
|
102
|
+
await runLspPostCompactHook({ session_id: "session-compact-clean" });
|
|
103
|
+
|
|
104
|
+
// when
|
|
105
|
+
const cleanProbeOutput = await runLspPostToolUseHook(input, async () => {
|
|
106
|
+
calls += 1;
|
|
107
|
+
return "No diagnostics found";
|
|
108
|
+
});
|
|
109
|
+
const laterDiagnosticOutput = await runLspPostToolUseHook(input, async () => {
|
|
110
|
+
calls += 1;
|
|
111
|
+
return "error[markdown] (1000) at 1:1: recovered markdown diagnostic.";
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// then
|
|
115
|
+
expect(cleanProbeOutput).toBe("");
|
|
116
|
+
expect(laterDiagnosticOutput).toContain("recovered markdown diagnostic");
|
|
117
|
+
expect(calls).toBe(3);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("#given markdown LSP is cached unavailable #when TypeScript diagnostics run #then real diagnostics still block", async () => {
|
|
122
|
+
// given
|
|
123
|
+
const pluginData = tempPluginData();
|
|
124
|
+
const markdownInput = postToolUseInput("session-real-diagnostics", "README.md");
|
|
125
|
+
const typescriptInput = postToolUseInput("session-real-diagnostics", "src/broken.ts");
|
|
126
|
+
|
|
127
|
+
await withPluginData(pluginData, async () => {
|
|
128
|
+
await runLspPostToolUseHook(markdownInput, async () => MARKSMAN_INITIALIZE_TIMEOUT);
|
|
129
|
+
|
|
130
|
+
// when
|
|
131
|
+
const output = await runLspPostToolUseHook(
|
|
132
|
+
typescriptInput,
|
|
133
|
+
async () => "error[typescript] (2304) at 1:1: Cannot find name 'missing'.",
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
// then
|
|
137
|
+
const parsed: unknown = JSON.parse(output);
|
|
138
|
+
if (!isPostToolUseHookOutput(parsed)) throw new TypeError("Expected PostToolUse hook output");
|
|
139
|
+
expect(parsed.reason).toBe(
|
|
140
|
+
"LSP diagnostics after editing src/broken.ts:\n\n" +
|
|
141
|
+
"- error[typescript] (2304) at 1:1: Cannot find name 'missing'.",
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
function postToolUseInput(sessionId: string, filePath: string) {
|
|
148
|
+
return {
|
|
149
|
+
cwd: "/repo",
|
|
150
|
+
hook_event_name: "PostToolUse",
|
|
151
|
+
model: "gpt-5.5",
|
|
152
|
+
permission_mode: "default",
|
|
153
|
+
session_id: sessionId,
|
|
154
|
+
tool_input: { path: filePath },
|
|
155
|
+
tool_name: "write",
|
|
156
|
+
tool_response: { ok: true },
|
|
157
|
+
tool_use_id: "tool-use-1",
|
|
158
|
+
transcript_path: null,
|
|
159
|
+
turn_id: "turn-1",
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function withPluginData(pluginData: string, fn: () => Promise<void>): Promise<void> {
|
|
164
|
+
const previous = process.env["PLUGIN_DATA"];
|
|
165
|
+
process.env["PLUGIN_DATA"] = pluginData;
|
|
166
|
+
try {
|
|
167
|
+
await fn();
|
|
168
|
+
} finally {
|
|
169
|
+
if (previous === undefined) {
|
|
170
|
+
delete process.env["PLUGIN_DATA"];
|
|
171
|
+
} else {
|
|
172
|
+
process.env["PLUGIN_DATA"] = previous;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function tempPluginData(): string {
|
|
178
|
+
const dir = mkdtempSync(path.join(tmpdir(), "codex-lsp-unavailable-"));
|
|
179
|
+
tempDirs.push(dir);
|
|
180
|
+
return dir;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
interface PostToolUseHookOutput {
|
|
184
|
+
readonly decision: "block";
|
|
185
|
+
readonly reason: string;
|
|
186
|
+
readonly hookSpecificOutput: {
|
|
187
|
+
readonly hookEventName: "PostToolUse";
|
|
188
|
+
readonly additionalContext: string;
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function isPostToolUseHookOutput(value: unknown): value is PostToolUseHookOutput {
|
|
193
|
+
if (!isRecord(value)) return false;
|
|
194
|
+
const hookSpecificOutput = value["hookSpecificOutput"];
|
|
195
|
+
return (
|
|
196
|
+
value["decision"] === "block" &&
|
|
197
|
+
typeof value["reason"] === "string" &&
|
|
198
|
+
isRecord(hookSpecificOutput) &&
|
|
199
|
+
hookSpecificOutput["hookEventName"] === "PostToolUse" &&
|
|
200
|
+
typeof hookSpecificOutput["additionalContext"] === "string"
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
205
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
206
|
+
}
|
|
@@ -61,7 +61,8 @@ describe("plugin package metadata", () => {
|
|
|
61
61
|
const sourceFiles = readdirSync("src");
|
|
62
62
|
|
|
63
63
|
// when
|
|
64
|
-
const
|
|
64
|
+
const postToolUseCommand = hooksJson.hooks["PostToolUse"]?.[0]?.hooks[0]?.command;
|
|
65
|
+
const postCompactCommand = hooksJson.hooks["PostCompact"]?.[0]?.hooks[0]?.command;
|
|
65
66
|
const lspServer = mcpJson.mcpServers["lsp"];
|
|
66
67
|
const pluginRoot = ["$", "{PLUGIN_ROOT}"].join("");
|
|
67
68
|
|
|
@@ -75,8 +76,9 @@ describe("plugin package metadata", () => {
|
|
|
75
76
|
expect(packageJson.bin["codex-lsp"]).toBeUndefined();
|
|
76
77
|
expect(packageJson.scripts["build"]).toBe("node scripts/clean-dist.mjs && tsc -p tsconfig.build.json");
|
|
77
78
|
expect(cliSource.startsWith("#!/usr/bin/env node")).toBe(true);
|
|
78
|
-
expect(cliSource).toContain("Usage: omo-lsp [mcp | hook post-tool-use]");
|
|
79
|
-
expect(
|
|
79
|
+
expect(cliSource).toContain("Usage: omo-lsp [mcp | hook post-tool-use | hook post-compact]");
|
|
80
|
+
expect(postToolUseCommand).toBe(`node "${pluginRoot}/dist/cli.js" hook post-tool-use`);
|
|
81
|
+
expect(postCompactCommand).toBe(`node "${pluginRoot}/dist/cli.js" hook post-compact`);
|
|
80
82
|
expect(lspServer?.command).toBe("node");
|
|
81
83
|
expect(lspServer?.args).toEqual(["../../../../lsp-tools-mcp/dist/cli.js", "mcp"]);
|
|
82
84
|
expect(cliSource).not.toContain("./lazy-lsp-mcp.js");
|
|
@@ -36,6 +36,7 @@ export interface FinderOptions {
|
|
|
36
36
|
skipUserHome?: boolean;
|
|
37
37
|
/** Plugin root directory. Defaults to PLUGIN_ROOT env or this package root. */
|
|
38
38
|
pluginRoot?: string;
|
|
39
|
+
platform?: NodeJS.Platform;
|
|
39
40
|
cache?: RuleDiscoveryCache;
|
|
40
41
|
}
|
|
41
42
|
|
|
@@ -43,8 +44,11 @@ interface PluginBundledFinderOptions {
|
|
|
43
44
|
readonly disabledSources?: ReadonlySet<string>;
|
|
44
45
|
readonly cache?: RuleDiscoveryCache;
|
|
45
46
|
readonly pluginRoot?: string;
|
|
47
|
+
readonly platform?: NodeJS.Platform;
|
|
46
48
|
}
|
|
47
49
|
|
|
50
|
+
const WINDOWS_GIT_BASH_BUNDLED_RULE_PATH = "bundled-rules/windows-git-bash.md";
|
|
51
|
+
|
|
48
52
|
export function findRuleCandidates(options: FinderOptions): RuleCandidate[] {
|
|
49
53
|
const skipUserHome = options.skipUserHome ?? false;
|
|
50
54
|
const disabledSources = options.disabledSources ?? new Set<string>();
|
|
@@ -61,6 +65,7 @@ export function findRuleCandidates(options: FinderOptions): RuleCandidate[] {
|
|
|
61
65
|
disabledSources,
|
|
62
66
|
...(options.cache === undefined ? {} : { cache: options.cache }),
|
|
63
67
|
...(options.pluginRoot === undefined ? {} : { pluginRoot: options.pluginRoot }),
|
|
68
|
+
...(options.platform === undefined ? {} : { platform: options.platform }),
|
|
64
69
|
};
|
|
65
70
|
candidates.push(...findPluginBundledCandidates(pluginBundledOptions));
|
|
66
71
|
|
|
@@ -78,9 +83,10 @@ export function findPluginBundledCandidates(options: PluginBundledFinderOptions
|
|
|
78
83
|
|
|
79
84
|
const pluginRoot = resolvePluginRulesRoot(options.pluginRoot);
|
|
80
85
|
const ruleDirectory = join(pluginRoot, BUNDLED_RULE_SUBDIR);
|
|
86
|
+
const platform = options.platform ?? process.platform;
|
|
81
87
|
const candidates: RuleCandidate[] = [];
|
|
82
88
|
for (const scannedFile of scanRuleFilesCached(ruleDirectory, options.cache)) {
|
|
83
|
-
|
|
89
|
+
const candidate: RuleCandidate = {
|
|
84
90
|
path: scannedFile.path,
|
|
85
91
|
realPath: scannedFile.realPath,
|
|
86
92
|
source: "plugin-bundled",
|
|
@@ -88,11 +94,18 @@ export function findPluginBundledCandidates(options: PluginBundledFinderOptions
|
|
|
88
94
|
isGlobal: true,
|
|
89
95
|
isSingleFile: false,
|
|
90
96
|
relativePath: toRelativePath(pluginRoot, scannedFile.path),
|
|
91
|
-
}
|
|
97
|
+
};
|
|
98
|
+
if (isPluginBundledCandidateEnabled(candidate, platform)) {
|
|
99
|
+
candidates.push(candidate);
|
|
100
|
+
}
|
|
92
101
|
}
|
|
93
102
|
return candidates;
|
|
94
103
|
}
|
|
95
104
|
|
|
105
|
+
function isPluginBundledCandidateEnabled(candidate: RuleCandidate, platform: NodeJS.Platform): boolean {
|
|
106
|
+
return candidate.relativePath !== WINDOWS_GIT_BASH_BUNDLED_RULE_PATH || platform === "win32";
|
|
107
|
+
}
|
|
108
|
+
|
|
96
109
|
function findProjectCandidates(
|
|
97
110
|
projectRoot: string,
|
|
98
111
|
targetFile: string | null,
|
|
@@ -7,11 +7,14 @@ import { findProjectRoot } from "./rules/project-root.js";
|
|
|
7
7
|
|
|
8
8
|
interface RulesEngineFactoryOptions {
|
|
9
9
|
env?: NodeJS.ProcessEnv;
|
|
10
|
+
platform?: NodeJS.Platform;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
export function createRulesEngine(options: RulesEngineFactoryOptions, config = configFromEnvironment(options.env)) {
|
|
14
|
+
const platform = options.platform ?? process.platform;
|
|
15
|
+
|
|
13
16
|
return createEngine(config, {
|
|
14
|
-
findCandidates: findRuleCandidates,
|
|
17
|
+
findCandidates: (finderOptions) => findRuleCandidates({ ...finderOptions, platform }),
|
|
15
18
|
findProjectRoot,
|
|
16
19
|
readFile: (path) => {
|
|
17
20
|
try {
|
package/packages/omo-codex/plugin/components/rules/test/windows-git-bash-bundled-rule.test.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { tmpdir } from "node:os";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { afterEach, describe, expect, it } from "vitest";
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { type CodexSessionStartInput, runSessionStartHook } from "../src/codex-hook.js";
|
|
7
7
|
import { findPluginBundledCandidates } from "../src/rules/finder.js";
|
|
8
8
|
|
|
9
9
|
const WINDOWS_RULE_DESCRIPTION = "Windows Git Bash guidance for Codex";
|
|
@@ -61,34 +61,57 @@ function occurrenceCount(value: string, search: string): number {
|
|
|
61
61
|
|
|
62
62
|
describe("Windows Git Bash bundled rule", () => {
|
|
63
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() });
|
|
64
|
+
const candidates = findPluginBundledCandidates({ pluginRoot: process.cwd(), platform: "win32" });
|
|
65
65
|
|
|
66
66
|
expect(candidates.map((candidate) => candidate.relativePath)).toContain(WINDOWS_RULE_PATH);
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
-
it("#given bundled rules
|
|
69
|
+
it("#given packaged bundled rules off Windows #when discovering plugin-bundled candidates #then Windows Git Bash rule is excluded", () => {
|
|
70
|
+
const candidates = findPluginBundledCandidates({ pluginRoot: process.cwd(), platform: "darwin" });
|
|
71
|
+
|
|
72
|
+
expect(candidates.map((candidate) => candidate.relativePath)).not.toContain(WINDOWS_RULE_PATH);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("#given bundled rules enabled on Windows #when SessionStart runs #then Windows Git Bash guidance is injected once", async () => {
|
|
70
76
|
const { root, pluginData } = makeProject();
|
|
71
77
|
|
|
72
78
|
const output = await runSessionStartHook(sessionStartInput(root), {
|
|
73
79
|
pluginDataRoot: pluginData,
|
|
74
80
|
env: BUNDLED_ONLY_ENV,
|
|
81
|
+
platform: "win32",
|
|
75
82
|
});
|
|
76
83
|
|
|
77
84
|
expect(occurrenceCount(output, WINDOWS_GUIDANCE)).toBe(1);
|
|
78
85
|
});
|
|
79
86
|
|
|
80
|
-
it("#given
|
|
87
|
+
it("#given bundled rules enabled off Windows #when SessionStart runs #then Windows Git Bash guidance is not injected", async () => {
|
|
88
|
+
const { root, pluginData } = makeProject();
|
|
89
|
+
|
|
90
|
+
const output = await runSessionStartHook(sessionStartInput(root), {
|
|
91
|
+
pluginDataRoot: pluginData,
|
|
92
|
+
env: BUNDLED_ONLY_ENV,
|
|
93
|
+
platform: "darwin",
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
expect(output).not.toContain(WINDOWS_GUIDANCE);
|
|
97
|
+
expect(output).not.toContain(WINDOWS_RULE_PATH);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("#given project rule with same description on Windows #when static rules load #then project guidance overrides bundled guidance", async () => {
|
|
81
101
|
const { root, pluginData } = makeProject();
|
|
82
102
|
const projectGuidance = "Project-specific Windows shell policy.";
|
|
83
103
|
mkdirSync(join(root, ".omo", "rules"), { recursive: true });
|
|
84
104
|
writeFileSync(
|
|
85
105
|
join(root, ".omo", "rules", "windows-git-bash.md"),
|
|
86
|
-
["---", `description: ${WINDOWS_RULE_DESCRIPTION}`, "alwaysApply: true", "---", "", projectGuidance].join(
|
|
106
|
+
["---", `description: ${WINDOWS_RULE_DESCRIPTION}`, "alwaysApply: true", "---", "", projectGuidance].join(
|
|
107
|
+
"\n",
|
|
108
|
+
),
|
|
87
109
|
);
|
|
88
110
|
|
|
89
111
|
const output = await runSessionStartHook(sessionStartInput(root), {
|
|
90
112
|
pluginDataRoot: pluginData,
|
|
91
113
|
env: PROJECT_AND_BUNDLED_ENV,
|
|
114
|
+
platform: "win32",
|
|
92
115
|
});
|
|
93
116
|
|
|
94
117
|
expect(output).toContain(projectGuidance);
|
|
@@ -97,13 +97,13 @@
|
|
|
97
97
|
"type": "command",
|
|
98
98
|
"command": "node \"${PLUGIN_ROOT}/components/comment-checker/dist/cli.js\" hook post-tool-use",
|
|
99
99
|
"timeout": 30,
|
|
100
|
-
"statusMessage": "LazyCodex(0.1.
|
|
100
|
+
"statusMessage": "LazyCodex(0.1.0): Checking Comments"
|
|
101
101
|
},
|
|
102
102
|
{
|
|
103
103
|
"type": "command",
|
|
104
104
|
"command": "node \"${PLUGIN_ROOT}/components/lsp/dist/cli.js\" hook post-tool-use",
|
|
105
105
|
"timeout": 60,
|
|
106
|
-
"statusMessage": "LazyCodex(0.
|
|
106
|
+
"statusMessage": "LazyCodex(0.1.0): Checking LSP Diagnostics"
|
|
107
107
|
}
|
|
108
108
|
]
|
|
109
109
|
},
|
|
@@ -141,6 +141,17 @@
|
|
|
141
141
|
"statusMessage": "LazyCodex(0.1.0): Resetting Project Rule Cache"
|
|
142
142
|
}
|
|
143
143
|
]
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
"matcher": "manual|auto",
|
|
147
|
+
"hooks": [
|
|
148
|
+
{
|
|
149
|
+
"type": "command",
|
|
150
|
+
"command": "node \"${PLUGIN_ROOT}/components/lsp/dist/cli.js\" hook post-compact",
|
|
151
|
+
"timeout": 5,
|
|
152
|
+
"statusMessage": "LazyCodex(0.1.0): Resetting LSP Diagnostics Cache"
|
|
153
|
+
}
|
|
154
|
+
]
|
|
144
155
|
}
|
|
145
156
|
],
|
|
146
157
|
"Stop": [
|
|
@@ -44,13 +44,6 @@ async function readComponentVersions(root) {
|
|
|
44
44
|
return versions;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
function componentVersionForCommand(command, componentVersions, fallbackVersion) {
|
|
48
|
-
for (const [componentName, version] of componentVersions.entries()) {
|
|
49
|
-
if (command.includes(`/components/${componentName}/dist/cli.js`)) return version;
|
|
50
|
-
}
|
|
51
|
-
return fallbackVersion;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
47
|
function syncHooksJson(hooksJson, versionForCommand) {
|
|
55
48
|
for (const groups of Object.values(hooksJson.hooks)) {
|
|
56
49
|
for (const group of groups) {
|
|
@@ -76,7 +69,7 @@ export async function syncHookStatusMessages(root = defaultRoot) {
|
|
|
76
69
|
const componentVersions = await readComponentVersions(root);
|
|
77
70
|
const aggregateHooksPath = join(root, "hooks", "hooks.json");
|
|
78
71
|
const aggregateHooks = await readJson(aggregateHooksPath);
|
|
79
|
-
syncHooksJson(aggregateHooks, (
|
|
72
|
+
syncHooksJson(aggregateHooks, () => aggregateVersion);
|
|
80
73
|
await writeJson(aggregateHooksPath, aggregateHooks);
|
|
81
74
|
|
|
82
75
|
for (const [componentName, version] of componentVersions.entries()) {
|
|
@@ -135,6 +135,22 @@ test("#given isolated components #when hooks are inspected #then commands stay i
|
|
|
135
135
|
assert.equal(await exists("scripts/migrate-codex-config.mjs"), true);
|
|
136
136
|
});
|
|
137
137
|
|
|
138
|
+
test("#given aggregate PostCompact hooks #when hooks are inspected #then LSP diagnostics cache reset is registered", async () => {
|
|
139
|
+
// given
|
|
140
|
+
const hooks = await readJson("hooks/hooks.json");
|
|
141
|
+
|
|
142
|
+
// when
|
|
143
|
+
const lspPostCompactHooks = collectCommandHooks(hooks, "hooks/hooks.json").filter(
|
|
144
|
+
(hook) =>
|
|
145
|
+
hook.eventName === "PostCompact" &&
|
|
146
|
+
hook.handler.command === 'node "${PLUGIN_ROOT}/components/lsp/dist/cli.js" hook post-compact',
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
// then
|
|
150
|
+
assert.equal(lspPostCompactHooks.length, 1);
|
|
151
|
+
assert.equal(lspPostCompactHooks[0]?.handler.statusMessage, "LazyCodex(0.1.0): Resetting LSP Diagnostics Cache");
|
|
152
|
+
});
|
|
153
|
+
|
|
138
154
|
test("#given aggregate hook commands #when inspected #then every command exposes a Codex status message", async () => {
|
|
139
155
|
// given
|
|
140
156
|
const hooks = await readJson("hooks/hooks.json");
|
|
@@ -24,6 +24,7 @@ const AGGREGATE_EXPECTED_LABELS = new Map([
|
|
|
24
24
|
["hooks/hooks.json:PostToolUse:0:1", "Checking LSP Diagnostics"],
|
|
25
25
|
["hooks/hooks.json:PostToolUse:1:0", "Matching Project Rules"],
|
|
26
26
|
["hooks/hooks.json:PostCompact:0:0", "Resetting Project Rule Cache"],
|
|
27
|
+
["hooks/hooks.json:PostCompact:2:0", "Resetting LSP Diagnostics Cache"],
|
|
27
28
|
["hooks/hooks.json:Stop:0:0", "Checking Start-Work Continuation"],
|
|
28
29
|
["hooks/hooks.json:SubagentStop:0:0", "Checking Start-Work Continuation"],
|
|
29
30
|
]);
|
|
@@ -31,6 +32,7 @@ const AGGREGATE_EXPECTED_LABELS = new Map([
|
|
|
31
32
|
const COMPONENT_EXPECTED_LABELS = new Map([
|
|
32
33
|
["components/comment-checker/hooks/hooks.json:PostToolUse:0:0", "Checking Comments"],
|
|
33
34
|
["components/lsp/hooks/hooks.json:PostToolUse:0:0", "Checking LSP Diagnostics"],
|
|
35
|
+
["components/lsp/hooks/hooks.json:PostCompact:0:0", "Resetting LSP Diagnostics Cache"],
|
|
34
36
|
["components/rules/hooks/hooks.json:SessionStart:0:0", "Loading Project Rules"],
|
|
35
37
|
["components/rules/hooks/hooks.json:UserPromptSubmit:0:0", "Loading Project Rules"],
|
|
36
38
|
["components/rules/hooks/hooks.json:PostToolUse:0:0", "Matching Project Rules"],
|
|
@@ -70,26 +72,6 @@ async function readComponentHookManifests() {
|
|
|
70
72
|
return manifests.sort((left, right) => left.source.localeCompare(right.source));
|
|
71
73
|
}
|
|
72
74
|
|
|
73
|
-
async function readComponentVersions() {
|
|
74
|
-
const components = await readdir(join(root, "components"), { withFileTypes: true });
|
|
75
|
-
const versions = new Map();
|
|
76
|
-
for (const entry of components) {
|
|
77
|
-
if (!entry.isDirectory()) continue;
|
|
78
|
-
if (!(await exists(join("components", entry.name, "package.json")))) continue;
|
|
79
|
-
const packageJson = await readJson(join("components", entry.name, "package.json"));
|
|
80
|
-
versions.set(entry.name, packageJson.version);
|
|
81
|
-
}
|
|
82
|
-
return versions;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function hookOwnerVersion(hook, aggregateVersion, componentVersions) {
|
|
86
|
-
const command = hook.command;
|
|
87
|
-
for (const [componentName, version] of componentVersions.entries()) {
|
|
88
|
-
if (command.includes(`/components/${componentName}/dist/cli.js`)) return version;
|
|
89
|
-
}
|
|
90
|
-
return aggregateVersion;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
75
|
function collectCommandHooks(hooks, source, version) {
|
|
94
76
|
const commandHooks = [];
|
|
95
77
|
const normalizedSource = source.replaceAll("\\", "/");
|
|
@@ -149,15 +131,15 @@ test("#given loose legacy status label #when normalizing #then removes OMO wordi
|
|
|
149
131
|
|
|
150
132
|
test("#given aggregate comment-checker hook #when status is inspected #then it uses LazyCodex comments label", async () => {
|
|
151
133
|
// given
|
|
134
|
+
const aggregateVersion = (await readJson(".codex-plugin/plugin.json")).version;
|
|
152
135
|
const aggregateHooks = await readJson("hooks/hooks.json");
|
|
153
|
-
const componentVersions = await readComponentVersions();
|
|
154
136
|
|
|
155
137
|
// when
|
|
156
|
-
const hooks = collectCommandHooks(aggregateHooks, "hooks/hooks.json",
|
|
138
|
+
const hooks = collectCommandHooks(aggregateHooks, "hooks/hooks.json", aggregateVersion);
|
|
157
139
|
const commentCheckerHook = hooks.find((hook) => hook.id === "hooks/hooks.json:PostToolUse:0:0");
|
|
158
140
|
|
|
159
141
|
// then
|
|
160
|
-
assert.equal(commentCheckerHook?.statusMessage, formatLazyCodexHookStatusMessage(
|
|
142
|
+
assert.equal(commentCheckerHook?.statusMessage, formatLazyCodexHookStatusMessage(aggregateVersion, "Checking Comments"));
|
|
161
143
|
assert.doesNotMatch(JSON.stringify(aggregateHooks), /checking\s+OMO\s+comments/i);
|
|
162
144
|
});
|
|
163
145
|
|
|
@@ -166,14 +148,10 @@ test("#given aggregate and component hooks #when status messages are inspected #
|
|
|
166
148
|
const aggregateVersion = (await readJson(".codex-plugin/plugin.json")).version;
|
|
167
149
|
const aggregateHooks = await readJson("hooks/hooks.json");
|
|
168
150
|
const componentManifests = await readComponentHookManifests();
|
|
169
|
-
const componentVersions = await readComponentVersions();
|
|
170
151
|
|
|
171
152
|
// when
|
|
172
153
|
const commandHooks = [
|
|
173
|
-
...collectCommandHooks(aggregateHooks, "hooks/hooks.json", aggregateVersion)
|
|
174
|
-
...hook,
|
|
175
|
-
version: hookOwnerVersion(hook, aggregateVersion, componentVersions),
|
|
176
|
-
})),
|
|
154
|
+
...collectCommandHooks(aggregateHooks, "hooks/hooks.json", aggregateVersion),
|
|
177
155
|
...componentManifests.flatMap((manifest) => collectCommandHooks(manifest.hooks, manifest.source, manifest.version)),
|
|
178
156
|
];
|
|
179
157
|
const expectedLabels = new Map([...AGGREGATE_EXPECTED_LABELS, ...COMPONENT_EXPECTED_LABELS]);
|
|
@@ -20,10 +20,12 @@ test("#given a component without hooks #when hook status messages sync #then bui
|
|
|
20
20
|
await mkdir(join(root, ".codex-plugin"), { recursive: true });
|
|
21
21
|
await mkdir(join(root, "hooks"), { recursive: true });
|
|
22
22
|
await mkdir(join(root, "components", "comment-checker", "hooks"), { recursive: true });
|
|
23
|
+
await mkdir(join(root, "components", "lsp", "hooks"), { recursive: true });
|
|
23
24
|
await mkdir(join(root, "components", "git-bash"), { recursive: true });
|
|
24
25
|
await mkdir(join(root, "components", "stale-build-output", "dist"), { recursive: true });
|
|
25
26
|
await writeJson(join(root, ".codex-plugin", "plugin.json"), { version: "0.1.0" });
|
|
26
27
|
await writeJson(join(root, "components", "comment-checker", "package.json"), { version: "0.1.1" });
|
|
28
|
+
await writeJson(join(root, "components", "lsp", "package.json"), { version: "0.2.0" });
|
|
27
29
|
await writeJson(join(root, "components", "git-bash", "package.json"), { version: "0.3.0" });
|
|
28
30
|
await writeJson(join(root, "hooks", "hooks.json"), {
|
|
29
31
|
hooks: {
|
|
@@ -35,6 +37,11 @@ test("#given a component without hooks #when hook status messages sync #then bui
|
|
|
35
37
|
command: 'node "${PLUGIN_ROOT}/components/comment-checker/dist/cli.js" hook post-tool-use',
|
|
36
38
|
statusMessage: "LazyCodex(0.1.0): Checking Comments",
|
|
37
39
|
},
|
|
40
|
+
{
|
|
41
|
+
type: "command",
|
|
42
|
+
command: 'node "${PLUGIN_ROOT}/components/lsp/dist/cli.js" hook post-tool-use',
|
|
43
|
+
statusMessage: "LazyCodex(0.1.0): Checking LSP Diagnostics",
|
|
44
|
+
},
|
|
38
45
|
],
|
|
39
46
|
},
|
|
40
47
|
],
|
|
@@ -55,6 +62,21 @@ test("#given a component without hooks #when hook status messages sync #then bui
|
|
|
55
62
|
],
|
|
56
63
|
},
|
|
57
64
|
});
|
|
65
|
+
await writeJson(join(root, "components", "lsp", "hooks", "hooks.json"), {
|
|
66
|
+
hooks: {
|
|
67
|
+
PostToolUse: [
|
|
68
|
+
{
|
|
69
|
+
hooks: [
|
|
70
|
+
{
|
|
71
|
+
type: "command",
|
|
72
|
+
command: 'node "${PLUGIN_ROOT}/dist/cli.js" hook post-tool-use',
|
|
73
|
+
statusMessage: "LazyCodex(0.1.0): Checking LSP Diagnostics",
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
});
|
|
58
80
|
|
|
59
81
|
// when
|
|
60
82
|
await syncHookStatusMessages(root);
|
|
@@ -62,6 +84,9 @@ test("#given a component without hooks #when hook status messages sync #then bui
|
|
|
62
84
|
// then
|
|
63
85
|
const aggregateHooks = await readJson(join(root, "hooks", "hooks.json"));
|
|
64
86
|
const componentHooks = await readJson(join(root, "components", "comment-checker", "hooks", "hooks.json"));
|
|
65
|
-
|
|
87
|
+
const lspHooks = await readJson(join(root, "components", "lsp", "hooks", "hooks.json"));
|
|
88
|
+
assert.equal(aggregateHooks.hooks.PostToolUse[0].hooks[0].statusMessage, "LazyCodex(0.1.0): Checking Comments");
|
|
89
|
+
assert.equal(aggregateHooks.hooks.PostToolUse[0].hooks[1].statusMessage, "LazyCodex(0.1.0): Checking LSP Diagnostics");
|
|
66
90
|
assert.equal(componentHooks.hooks.PostToolUse[0].hooks[0].statusMessage, "LazyCodex(0.1.1): Checking Comments");
|
|
91
|
+
assert.equal(lspHooks.hooks.PostToolUse[0].hooks[0].statusMessage, "LazyCodex(0.2.0): Checking LSP Diagnostics");
|
|
67
92
|
});
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { appendBlock, findTomlSection, removeSetting, replaceOrInsertRootSetting, replaceOrInsertSetting } from "./toml-editor.mjs";
|
|
2
2
|
|
|
3
|
+
const AUTONOMOUS_FEATURES = ["multi_agent", "child_agents_md", "unified_exec", "goals"];
|
|
4
|
+
|
|
3
5
|
export function ensureAutonomousPermissions(config) {
|
|
4
6
|
let next = replaceOrInsertRootSetting(config, "approval_policy", JSON.stringify("never"));
|
|
5
7
|
next = replaceOrInsertRootSetting(next, "sandbox_mode", JSON.stringify("danger-full-access"));
|
|
6
8
|
next = replaceOrInsertRootSetting(next, "network_access", JSON.stringify("enabled"));
|
|
9
|
+
for (const featureName of AUTONOMOUS_FEATURES) {
|
|
10
|
+
next = ensureFeatureEnabled(next, featureName);
|
|
11
|
+
}
|
|
7
12
|
next = removeWindowsSandboxSetting(next);
|
|
8
13
|
next = ensureNoticeEnabled(next, "hide_full_access_warning");
|
|
9
14
|
return ensureNoticeEnabled(next, "hide_world_writable_warning");
|
|
@@ -21,6 +26,12 @@ function ensureNoticeEnabled(config, key) {
|
|
|
21
26
|
return replaceOrInsertSetting(config, section, key, "true");
|
|
22
27
|
}
|
|
23
28
|
|
|
29
|
+
function ensureFeatureEnabled(config, key) {
|
|
30
|
+
const section = findTomlSection(config, "features");
|
|
31
|
+
if (!section) return appendBlock(config, `[features]\n${key} = true\n`);
|
|
32
|
+
return replaceOrInsertSetting(config, section, key, "true");
|
|
33
|
+
}
|
|
34
|
+
|
|
24
35
|
function appendNoticeBlock(config, key) {
|
|
25
36
|
return appendBlock(config, `[notice]\n${key} = true\n`);
|
|
26
37
|
}
|