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.
Files changed (23) hide show
  1. package/dist/cli/index.js +5334 -5150
  2. package/dist/index.js +3447 -3334
  3. package/package.json +13 -13
  4. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +13 -0
  5. package/packages/omo-codex/plugin/components/lsp/src/cli.ts +6 -2
  6. package/packages/omo-codex/plugin/components/lsp/src/codex-hook-cli.ts +13 -2
  7. package/packages/omo-codex/plugin/components/lsp/src/codex-hook.ts +30 -79
  8. package/packages/omo-codex/plugin/components/lsp/src/lsp-session-state.ts +116 -0
  9. package/packages/omo-codex/plugin/components/lsp/src/mutated-file-paths.ts +88 -0
  10. package/packages/omo-codex/plugin/components/lsp/test/codex-hook-unavailable.test.ts +206 -0
  11. package/packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts +5 -3
  12. package/packages/omo-codex/plugin/components/rules/src/codex-hook-options.ts +1 -0
  13. package/packages/omo-codex/plugin/components/rules/src/rules/finder.ts +15 -2
  14. package/packages/omo-codex/plugin/components/rules/src/rules-engine-factory.ts +4 -1
  15. package/packages/omo-codex/plugin/components/rules/test/windows-git-bash-bundled-rule.test.ts +28 -5
  16. package/packages/omo-codex/plugin/hooks/hooks.json +13 -2
  17. package/packages/omo-codex/plugin/scripts/sync-hook-status-messages.mjs +1 -8
  18. package/packages/omo-codex/plugin/test/aggregate.test.mjs +16 -0
  19. package/packages/omo-codex/plugin/test/hook-status-message.test.mjs +6 -28
  20. package/packages/omo-codex/plugin/test/sync-hook-status-messages.test.mjs +26 -1
  21. package/packages/omo-codex/scripts/install/permissions.mjs +11 -0
  22. package/packages/omo-codex/scripts/install-config-autonomous-features.test.mjs +83 -0
  23. 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 command = hooksJson.hooks["PostToolUse"]?.[0]?.hooks[0]?.command;
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(command).toBe(`node "${pluginRoot}/dist/cli.js" hook post-tool-use`);
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");
@@ -1,4 +1,5 @@
1
1
  export interface CodexRulesHookOptions {
2
2
  env?: NodeJS.ProcessEnv;
3
3
  pluginDataRoot?: string;
4
+ platform?: NodeJS.Platform;
4
5
  }
@@ -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
- candidates.push({
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 {
@@ -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 { runSessionStartHook, type CodexSessionStartInput } from "../src/codex-hook.js";
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 enabled #when SessionStart runs #then Windows Git Bash guidance is injected once", async () => {
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 project rule with same description #when static rules load #then project guidance overrides bundled guidance", async () => {
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("\n"),
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.1): Checking Comments"
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.2.0): Checking LSP Diagnostics"
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, (command) => componentVersionForCommand(command, componentVersions, aggregateVersion));
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", "0.1.0");
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(componentVersions.get("comment-checker"), "Checking Comments"));
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).map((hook) => ({
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
- assert.equal(aggregateHooks.hooks.PostToolUse[0].hooks[0].statusMessage, "LazyCodex(0.1.1): Checking Comments");
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
  }