oh-my-opencode 4.19.2 → 4.19.3

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 (157) hide show
  1. package/.agents/command/publish.md +95 -28
  2. package/.agents/skills/publish/SKILL.md +20 -6
  3. package/.opencode/command/publish.md +95 -28
  4. package/THIRD-PARTY-NOTICES.md +7 -7
  5. package/dist/agents/sisyphus-runtime-prompt-reconciler.d.ts +1 -1
  6. package/dist/cli/config-migrate.d.ts +8 -0
  7. package/dist/cli/doctor/checks/legacy-config-leftovers.d.ts +10 -0
  8. package/dist/cli/doctor/checks/model-resolution-config.d.ts +1 -1
  9. package/dist/cli/doctor/checks/tools-lsp.d.ts +1 -1
  10. package/dist/cli/doctor/framework/constants.d.ts +1 -1
  11. package/dist/cli/index.js +95557 -92944
  12. package/dist/cli-node/index.js +95539 -92926
  13. package/dist/config/validate.d.ts +2 -1
  14. package/dist/config-migration/deep-diff.d.ts +1 -0
  15. package/dist/config-migration/discovery-paths.d.ts +11 -0
  16. package/dist/config-migration/discovery-roots.d.ts +7 -0
  17. package/dist/config-migration/discovery.d.ts +4 -0
  18. package/dist/config-migration/index.d.ts +9 -0
  19. package/dist/config-migration/legacy-history.d.ts +3 -0
  20. package/dist/config-migration/migration-executor.d.ts +4 -0
  21. package/dist/config-migration/migration-plans.d.ts +14 -0
  22. package/dist/config-migration/record-values.d.ts +4 -0
  23. package/dist/config-migration/schema-url.d.ts +1 -0
  24. package/dist/config-migration/transform-config-jsonc.d.ts +2 -0
  25. package/dist/config-migration/transform-opencode.d.ts +2 -0
  26. package/dist/config-migration/transform-types.d.ts +24 -0
  27. package/dist/config-migration/types.d.ts +39 -0
  28. package/dist/features/builtin-commands/templates/hyperplan.d.ts +1 -1
  29. package/dist/hooks/auto-update-checker/constants.d.ts +3 -3
  30. package/dist/index.js +10424 -8161
  31. package/dist/plugin-config/omo-config-chain.d.ts +14 -0
  32. package/dist/plugin-config/unknown-key-diagnostics.d.ts +5 -0
  33. package/dist/plugin-config.d.ts +2 -2
  34. package/dist/shared/agent-display-names.d.ts +1 -1
  35. package/dist/shared/jsonc-parser.d.ts +1 -1
  36. package/dist/shared/migration.d.ts +5 -1
  37. package/dist/shared/opencode-config-dir-types.d.ts +0 -1
  38. package/dist/shared/plugin-identity.d.ts +6 -8
  39. package/dist/shared/project-discovery-dirs.d.ts +0 -1
  40. package/dist/skills/frontend/ATTRIBUTION.md +7 -0
  41. package/dist/skills/frontend/SKILL.md +4 -1
  42. package/dist/skills/frontend/references/design/README.md +8 -0
  43. package/dist/skills/frontend/references/design/_INDEX.md +14 -1
  44. package/dist/skills/frontend/references/design/interaction-skill.md +144 -0
  45. package/dist/startup-migration.d.ts +28 -0
  46. package/dist/testing/create-plugin-module.d.ts +4 -0
  47. package/dist/tui.js +5356 -4643
  48. package/package.json +13 -13
  49. package/packages/lsp-core/src/lsp/client-diagnostics-freshness.integration.test.ts +19 -3
  50. package/packages/lsp-core/src/lsp/fixtures/workspace-edit-server.mjs +11 -2
  51. package/packages/omo-codex/THIRD-PARTY-NOTICES.md +2 -2
  52. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  53. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  54. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  55. package/packages/omo-codex/plugin/components/codegraph/AGENTS.md +11 -5
  56. package/packages/omo-codex/plugin/components/codegraph/NOTICE +1 -1
  57. package/packages/omo-codex/plugin/components/codegraph/dist/cli.js +10153 -2571
  58. package/packages/omo-codex/plugin/components/codegraph/dist/serve.js +7311 -572
  59. package/packages/omo-codex/plugin/components/codegraph/package.json +2 -2
  60. package/packages/omo-codex/plugin/components/codegraph/src/hook-input.ts +33 -0
  61. package/packages/omo-codex/plugin/components/codegraph/src/hook-types.ts +36 -10
  62. package/packages/omo-codex/plugin/components/codegraph/src/hook.ts +164 -159
  63. package/packages/omo-codex/plugin/components/codegraph/src/post-tool-use-hook.ts +34 -0
  64. package/packages/omo-codex/plugin/components/codegraph/src/serve.ts +20 -2
  65. package/packages/omo-codex/plugin/components/codegraph/src/session-start-command.ts +106 -0
  66. package/packages/omo-codex/plugin/components/codegraph/src/session-start-cooldown.ts +145 -0
  67. package/packages/omo-codex/plugin/components/codegraph/src/session-start-hook-runtime.ts +21 -0
  68. package/packages/omo-codex/plugin/components/codegraph/src/session-start-lock.ts +139 -0
  69. package/packages/omo-codex/plugin/components/codegraph/src/session-start-outcome.ts +15 -0
  70. package/packages/omo-codex/plugin/components/codegraph/src/session-start-paths.ts +32 -0
  71. package/packages/omo-codex/plugin/components/codegraph/src/session-start-project.ts +109 -0
  72. package/packages/omo-codex/plugin/components/codegraph/src/session-start-worker-result.ts +148 -0
  73. package/packages/omo-codex/plugin/components/codegraph/src/session-start-worker.ts +126 -175
  74. package/packages/omo-codex/plugin/components/codegraph/test/hook-exclusion.test.ts +4 -4
  75. package/packages/omo-codex/plugin/components/codegraph/test/hook-session-start-guard.test.ts +160 -0
  76. package/packages/omo-codex/plugin/components/codegraph/test/hook-store-upgrade.test.ts +12 -138
  77. package/packages/omo-codex/plugin/components/codegraph/test/hook.test.ts +28 -24
  78. package/packages/omo-codex/plugin/components/codegraph/test/mcp-bridge-fixtures.ts +3 -3
  79. package/packages/omo-codex/plugin/components/codegraph/test/package-runtime.test.ts +2 -2
  80. package/packages/omo-codex/plugin/components/codegraph/test/provisioned-node-guard.test.ts +5 -5
  81. package/packages/omo-codex/plugin/components/codegraph/test/serve-mcp-bridge.test.ts +3 -3
  82. package/packages/omo-codex/plugin/components/codegraph/test/serve-mcp-facade.test.ts +1 -1
  83. package/packages/omo-codex/plugin/components/codegraph/test/serve-provision.test.ts +45 -0
  84. package/packages/omo-codex/plugin/components/codegraph/test/serve-unavailable.test.ts +2 -2
  85. package/packages/omo-codex/plugin/components/codegraph/test/session-start-node-support.test.ts +70 -13
  86. package/packages/omo-codex/plugin/components/codegraph/test/session-start-project.test.ts +58 -0
  87. package/packages/omo-codex/plugin/components/codegraph/test/session-start-state.test.ts +104 -0
  88. package/packages/omo-codex/plugin/components/codegraph/test/session-start-trust-boundary.test.ts +4 -2
  89. package/packages/omo-codex/plugin/components/codegraph/test/session-start-worker-cooldown.test.ts +116 -0
  90. package/packages/omo-codex/plugin/components/codegraph/test/session-start-worker-flow.test.ts +58 -57
  91. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  92. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  93. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  94. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  95. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  96. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  97. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +2 -2
  98. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  99. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  100. package/packages/omo-codex/plugin/components/rules/dist/cli.js +31 -14
  101. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  102. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  103. package/packages/omo-codex/plugin/components/start-work-continuation/hooks/hooks.json +2 -2
  104. package/packages/omo-codex/plugin/components/start-work-continuation/package.json +1 -1
  105. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  106. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  107. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  108. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  109. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  110. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  111. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +4 -4
  112. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  113. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +17 -0
  114. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  115. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  116. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  117. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +1 -1
  118. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  119. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  120. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  121. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  122. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  123. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  124. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  125. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  126. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  127. package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +1 -1
  128. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  129. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  130. package/packages/omo-codex/plugin/hooks/stop-checking-start-work-continuation.json +1 -1
  131. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  132. package/packages/omo-codex/plugin/hooks/subagent-stop-checking-start-work-continuation.json +1 -1
  133. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  134. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  135. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  136. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  137. package/packages/omo-codex/plugin/package-lock.json +42 -41
  138. package/packages/omo-codex/plugin/package.json +1 -1
  139. package/packages/omo-codex/plugin/shared/src/config-loader.ts +165 -16
  140. package/packages/omo-codex/plugin/shared/src/config-migration.ts +162 -0
  141. package/packages/omo-codex/plugin/shared/test/config-loader.test.ts +186 -9
  142. package/packages/omo-codex/plugin/skills/frontend/ATTRIBUTION.md +7 -0
  143. package/packages/omo-codex/plugin/skills/frontend/SKILL.md +4 -1
  144. package/packages/omo-codex/plugin/skills/frontend/references/design/README.md +8 -0
  145. package/packages/omo-codex/plugin/skills/frontend/references/design/_INDEX.md +14 -1
  146. package/packages/omo-codex/plugin/skills/frontend/references/design/interaction-skill.md +144 -0
  147. package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +17 -0
  148. package/packages/omo-codex/scripts/install-dist/install-local.mjs +1 -1
  149. package/packages/shared-skills/skills/frontend/ATTRIBUTION.md +7 -0
  150. package/packages/shared-skills/skills/frontend/SKILL.md +4 -1
  151. package/packages/shared-skills/skills/frontend/references/design/README.md +8 -0
  152. package/packages/shared-skills/skills/frontend/references/design/_INDEX.md +14 -1
  153. package/packages/shared-skills/skills/frontend/references/design/interaction-skill.md +144 -0
  154. package/dist/plugin-config/layered-config-loader.d.ts +0 -2
  155. package/dist/plugin-config/single-config-loader.d.ts +0 -4
  156. package/dist/shared/migrate-legacy-config-file.d.ts +0 -1
  157. package/dist/shared/migration/config-migration.d.ts +0 -1
@@ -1,10 +1,15 @@
1
1
  import { describe, expect, it } from "bun:test";
2
- import { existsSync, mkdtempSync, rmSync } from "node:fs";
2
+ import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
 
6
6
  import { runCodegraphSessionStartWorker } from "../src/hook.ts";
7
7
 
8
+ function writeProjectDatabase(workspace: string): void {
9
+ mkdirSync(join(workspace, ".codegraph"), { recursive: true });
10
+ writeFileSync(join(workspace, ".codegraph", "codegraph.db"), "fixture");
11
+ }
12
+
8
13
  describe("CodeGraph SessionStart worker Node support", () => {
9
14
  it("#given an unsupported local Node and a PATH CodeGraph command with auto provisioning disabled #when worker runs #then it skips without touching the workspace", async () => {
10
15
  // given
@@ -52,7 +57,7 @@ describe("CodeGraph SessionStart worker Node support", () => {
52
57
  const homeDir = mkdtempSync(join(tmpdir(), "omo-codegraph-worker-node-provision-home-"));
53
58
  const binPath = join(homeDir, ".omo", "codegraph", "bin", "codegraph");
54
59
  const calls: Array<{ readonly args: readonly string[]; readonly command: string }> = [];
55
- const provisionCalls: Array<{ readonly installDir?: string; readonly lockDir: string; readonly version: "1.4.1" }> = [];
60
+ const provisionCalls: Array<{ readonly installDir?: string; readonly lockDir: string; readonly version: "1.5.0" }> = [];
56
61
  const outcomes: unknown[] = [];
57
62
 
58
63
  try {
@@ -78,19 +83,17 @@ describe("CodeGraph SessionStart worker Node support", () => {
78
83
  resolveCommand: () => ({ argsPrefix: [], command: "/usr/local/bin/codegraph", exists: true, source: "path" }),
79
84
  runCommand: (_projectRoot, command, args) => {
80
85
  calls.push({ args, command });
81
- return Promise.resolve({ exitCode: 0, stdout: calls.length === 1 ? '{"initialized":false}' : "", timedOut: false });
86
+ writeProjectDatabase(workspace);
87
+ return Promise.resolve({ exitCode: 0, stdout: "", timedOut: false });
82
88
  },
83
89
  },
84
90
  });
85
91
 
86
92
  // then
87
93
  expect(result).toEqual({ action: "initialized" });
88
- expect(calls).toEqual([
89
- { args: ["status", "--json"], command: binPath },
90
- { args: ["init"], command: binPath },
91
- ]);
94
+ expect(calls).toEqual([{ args: ["init"], command: binPath }]);
92
95
  expect(provisionCalls).toEqual([
93
- { installDir: join(homeDir, ".omo", "codegraph"), lockDir: join(homeDir, ".omo", "codegraph", ".locks"), version: "1.4.1" },
96
+ { installDir: join(homeDir, ".omo", "codegraph"), lockDir: join(homeDir, ".omo", "codegraph", ".locks"), version: "1.5.0" },
94
97
  ]);
95
98
  expect(outcomes).toEqual([{ action: "initialized", exitCode: 0, projectRoot: workspace, source: "provisioned", timedOut: false }]);
96
99
  } finally {
@@ -99,6 +102,62 @@ describe("CodeGraph SessionStart worker Node support", () => {
99
102
  }
100
103
  });
101
104
 
105
+ it("#given bundled CodeGraph and a stale managed install #when the worker runs #then it upgrades and uses the managed runtime", async () => {
106
+ // given
107
+ const workspace = mkdtempSync(join(tmpdir(), "omo-codegraph-worker-managed-upgrade-"));
108
+ const homeDir = mkdtempSync(join(tmpdir(), "omo-codegraph-worker-managed-upgrade-home-"));
109
+ const installDir = join(homeDir, ".omo", "codegraph");
110
+ const binPath = join(installDir, "bin", "codegraph");
111
+ mkdirSync(join(installDir, "bin"), { recursive: true });
112
+ mkdirSync(join(installDir, ".provisioned"), { recursive: true });
113
+ writeFileSync(binPath, "stale codegraph\n");
114
+ writeFileSync(
115
+ join(installDir, ".provisioned", "codegraph-1.4.1.json"),
116
+ `${JSON.stringify({ binPath, version: "1.4.1" })}\n`,
117
+ );
118
+ const provisionCalls: unknown[] = [];
119
+ const commandCalls: Array<{ readonly args: readonly string[]; readonly command: string }> = [];
120
+
121
+ try {
122
+ // when
123
+ const result = await runCodegraphSessionStartWorker({
124
+ cwd: workspace,
125
+ env: { HOME: homeDir },
126
+ nodeVersion: "22.14.0",
127
+ deps: {
128
+ ensureGitignored: () => true,
129
+ ensureProvisioned: (options) => {
130
+ provisionCalls.push(options);
131
+ return Promise.resolve({ binPath, provisioned: true });
132
+ },
133
+ managedInstallExists: () => true,
134
+ prepareWorkspace: () => ({
135
+ dataDir: join(homeDir, ".omo/codegraph/projects/test"),
136
+ dataRoot: join(homeDir, ".omo/codegraph"),
137
+ linked: true,
138
+ mode: "global-linked",
139
+ projectLink: join(workspace, ".codegraph"),
140
+ }),
141
+ resolveCommand: () => ({ argsPrefix: ["codegraph.js"], command: "/opt/node22/bin/node", exists: true, source: "bundled" }),
142
+ resolveManagedBin: () => null,
143
+ runCommand: (_projectRoot, command, args) => {
144
+ commandCalls.push({ args, command });
145
+ writeProjectDatabase(workspace);
146
+ return Promise.resolve({ exitCode: 0, stdout: "", timedOut: false });
147
+ },
148
+ },
149
+ });
150
+
151
+ // then
152
+ expect(result).toEqual({ action: "initialized" });
153
+ expect(provisionCalls).toEqual([{ installDir, lockDir: join(installDir, ".locks"), version: "1.5.0" }]);
154
+ expect(commandCalls).toEqual([{ args: ["init"], command: binPath }]);
155
+ } finally {
156
+ rmSync(workspace, { recursive: true, force: true });
157
+ rmSync(homeDir, { recursive: true, force: true });
158
+ }
159
+ });
160
+
102
161
  it("#given an unsupported local Node but bundled CodeGraph resolves through CODEGRAPH_NODE_BIN #when worker runs #then it bootstraps with the compatible runtime", async () => {
103
162
  // given
104
163
  const workspace = mkdtempSync(join(tmpdir(), "omo-codegraph-worker-compatible-node-"));
@@ -129,17 +188,15 @@ describe("CodeGraph SessionStart worker Node support", () => {
129
188
  resolveCommand: () => ({ argsPrefix: ["codegraph.js"], command: nodeBin, exists: true, source: "bundled" }),
130
189
  runCommand: (_projectRoot, command, args) => {
131
190
  calls.push({ args, command });
132
- return Promise.resolve({ exitCode: 0, stdout: calls.length === 1 ? '{"initialized":false}' : "", timedOut: false });
191
+ writeProjectDatabase(workspace);
192
+ return Promise.resolve({ exitCode: 0, stdout: "", timedOut: false });
133
193
  },
134
194
  },
135
195
  });
136
196
 
137
197
  // then
138
198
  expect(result).toEqual({ action: "initialized" });
139
- expect(calls).toEqual([
140
- { args: ["codegraph.js", "status", "--json"], command: nodeBin },
141
- { args: ["codegraph.js", "init"], command: nodeBin },
142
- ]);
199
+ expect(calls).toEqual([{ args: ["codegraph.js", "init"], command: nodeBin }]);
143
200
  expect(outcomes).toEqual([{ action: "initialized", exitCode: 0, projectRoot: workspace, source: "bundled", timedOut: false }]);
144
201
  } finally {
145
202
  rmSync(workspace, { recursive: true, force: true });
@@ -0,0 +1,58 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+
6
+ import { probeCodegraphProject } from "../src/session-start-project.ts";
7
+
8
+ describe("CodeGraph SessionStart project probe", () => {
9
+ it("#given the exact project directory has a CodeGraph database #when probing #then it is initialized", () => {
10
+ // given
11
+ const projectRoot = mkdtempSync(join(tmpdir(), "omo-codegraph-probe-exact-"));
12
+ try {
13
+ mkdirSync(join(projectRoot, ".codegraph"), { recursive: true });
14
+ writeFileSync(join(projectRoot, ".codegraph", "codegraph.db"), "fixture");
15
+
16
+ // when
17
+ const result = probeCodegraphProject(projectRoot);
18
+
19
+ // then
20
+ expect(result).toEqual({ kind: "initialized" });
21
+ } finally {
22
+ rmSync(projectRoot, { recursive: true, force: true });
23
+ }
24
+ });
25
+
26
+ it("#given an ancestor directory has a CodeGraph database #when probing a nested project #then the ancestor covers it", () => {
27
+ // given
28
+ const ancestorRoot = mkdtempSync(join(tmpdir(), "omo-codegraph-probe-ancestor-"));
29
+ const projectRoot = join(ancestorRoot, "apps", "server");
30
+ try {
31
+ mkdirSync(join(ancestorRoot, ".codegraph"), { recursive: true });
32
+ mkdirSync(projectRoot, { recursive: true });
33
+ writeFileSync(join(ancestorRoot, ".codegraph", "codegraph.db"), "fixture");
34
+
35
+ // when
36
+ const result = probeCodegraphProject(projectRoot);
37
+
38
+ // then
39
+ expect(result).toEqual({ ancestorRoot: realpathSync(ancestorRoot), kind: "nested-root" });
40
+ } finally {
41
+ rmSync(ancestorRoot, { recursive: true, force: true });
42
+ }
43
+ });
44
+
45
+ it("#given neither the project nor its ancestors have a CodeGraph database #when probing #then it is definitively uninitialized", () => {
46
+ // given
47
+ const projectRoot = mkdtempSync(join(tmpdir(), "omo-codegraph-probe-missing-"));
48
+ try {
49
+ // when
50
+ const result = probeCodegraphProject(projectRoot);
51
+
52
+ // then
53
+ expect(result).toEqual({ kind: "uninitialized" });
54
+ } finally {
55
+ rmSync(projectRoot, { recursive: true, force: true });
56
+ }
57
+ });
58
+ });
@@ -0,0 +1,104 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { mkdtempSync, rmSync, utimesSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+
6
+ import {
7
+ clearSessionStartCooldown,
8
+ readSessionStartCooldown,
9
+ recordSessionStartFailure,
10
+ } from "../src/session-start-cooldown.ts";
11
+ import {
12
+ acquireSessionStartLock,
13
+ releaseSessionStartLock,
14
+ } from "../src/session-start-lock.ts";
15
+
16
+ describe("CodeGraph SessionStart lock", () => {
17
+ it("#given a live project lock #when a second hook acquires it #then the second hook is deduplicated", () => {
18
+ // given
19
+ const homeDir = mkdtempSync(join(tmpdir(), "omo-codegraph-lock-home-"));
20
+ const projectRoot = mkdtempSync(join(tmpdir(), "omo-codegraph-lock-project-"));
21
+ try {
22
+ const first = acquireSessionStartLock({ homeDir, nowMs: 10_000, projectRoot, staleMs: 60_000 });
23
+ expect(first.kind).toBe("acquired");
24
+
25
+ // when
26
+ const second = acquireSessionStartLock({ homeDir, nowMs: 10_001, projectRoot, staleMs: 60_000 });
27
+
28
+ // then
29
+ expect(second).toEqual({ kind: "locked" });
30
+ if (first.kind === "acquired") releaseSessionStartLock(first.lock);
31
+ } finally {
32
+ rmSync(homeDir, { recursive: true, force: true });
33
+ rmSync(projectRoot, { recursive: true, force: true });
34
+ }
35
+ });
36
+
37
+ it("#given a project lock older than the stale threshold #when another hook acquires it #then the stale lock is recovered", () => {
38
+ // given
39
+ const homeDir = mkdtempSync(join(tmpdir(), "omo-codegraph-stale-home-"));
40
+ const projectRoot = mkdtempSync(join(tmpdir(), "omo-codegraph-stale-project-"));
41
+ try {
42
+ const first = acquireSessionStartLock({ homeDir, nowMs: 1_000, projectRoot, staleMs: 500 });
43
+ expect(first.kind).toBe("acquired");
44
+ if (first.kind !== "acquired") return;
45
+ const staleDate = new Date(1_000);
46
+ utimesSync(first.lock.path, staleDate, staleDate);
47
+
48
+ // when
49
+ const second = acquireSessionStartLock({ homeDir, nowMs: 2_000, projectRoot, staleMs: 500 });
50
+
51
+ // then
52
+ expect(second.kind).toBe("acquired");
53
+ if (second.kind === "acquired") {
54
+ expect(second.recoveredStale).toBe(true);
55
+ releaseSessionStartLock(second.lock);
56
+ }
57
+ } finally {
58
+ rmSync(homeDir, { recursive: true, force: true });
59
+ rmSync(projectRoot, { recursive: true, force: true });
60
+ }
61
+ });
62
+ });
63
+
64
+ describe("CodeGraph SessionStart cooldown", () => {
65
+ it("#given a failed worker outcome #when the cooldown is checked #then retries are suppressed until expiry and the next failure backs off", () => {
66
+ // given
67
+ const homeDir = mkdtempSync(join(tmpdir(), "omo-codegraph-cooldown-home-"));
68
+ const projectRoot = mkdtempSync(join(tmpdir(), "omo-codegraph-cooldown-project-"));
69
+ try {
70
+ const first = recordSessionStartFailure({
71
+ action: "failed",
72
+ baseCooldownMs: 1_000,
73
+ homeDir,
74
+ nowMs: 1_000,
75
+ projectRoot,
76
+ });
77
+
78
+ // when
79
+ const active = readSessionStartCooldown({ baseCooldownMs: 1_000, homeDir, nowMs: 1_999, projectRoot });
80
+ const expired = readSessionStartCooldown({ baseCooldownMs: 1_000, homeDir, nowMs: 2_000, projectRoot });
81
+ const second = recordSessionStartFailure({
82
+ action: "skipped-status",
83
+ baseCooldownMs: 1_000,
84
+ homeDir,
85
+ nowMs: 2_000,
86
+ projectRoot,
87
+ });
88
+
89
+ // then
90
+ expect(first).toMatchObject({ cooldownUntil: 2_000, failureCount: 1 });
91
+ expect(active).toMatchObject({ cooldownUntil: 2_000, failureCount: 1, kind: "active" });
92
+ expect(expired).toEqual({ failureCount: 1, kind: "inactive" });
93
+ expect(second).toMatchObject({ cooldownUntil: 4_000, failureCount: 2 });
94
+ clearSessionStartCooldown({ homeDir, projectRoot });
95
+ expect(readSessionStartCooldown({ baseCooldownMs: 1_000, homeDir, nowMs: 2_001, projectRoot })).toEqual({
96
+ failureCount: 0,
97
+ kind: "inactive",
98
+ });
99
+ } finally {
100
+ rmSync(homeDir, { recursive: true, force: true });
101
+ rmSync(projectRoot, { recursive: true, force: true });
102
+ }
103
+ });
104
+ });
@@ -15,7 +15,7 @@ describe("CodeGraph SessionStart trust boundary", () => {
15
15
  const calls: Array<{ readonly env: Record<string, string>; readonly installDir?: string; readonly lockDir?: string }> = [];
16
16
  try {
17
17
  mkdirSync(join(workspace, ".omo"), { recursive: true });
18
- writeFileSync(join(workspace, ".omo", "config.jsonc"), JSON.stringify({ codegraph: { enabled: true, install_dir: attackerInstallDir } }));
18
+ writeFileSync(join(workspace, ".omo", "omo.jsonc"), JSON.stringify({ codegraph: { enabled: true, install_dir: attackerInstallDir } }));
19
19
 
20
20
  // when
21
21
  const result = await runCodegraphSessionStartWorker({
@@ -45,7 +45,9 @@ describe("CodeGraph SessionStart trust boundary", () => {
45
45
  },
46
46
  runCommand: (_projectRoot, _command, _args, options) => {
47
47
  calls.push({ env: options.env });
48
- return Promise.resolve({ exitCode: 0, stdout: calls.length === 2 ? '{"initialized":false}' : "", timedOut: false });
48
+ mkdirSync(join(workspace, ".codegraph"), { recursive: true });
49
+ writeFileSync(join(workspace, ".codegraph", "codegraph.db"), "fixture");
50
+ return Promise.resolve({ exitCode: 0, stdout: "", timedOut: false });
49
51
  },
50
52
  },
51
53
  });
@@ -0,0 +1,116 @@
1
+ import { 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
+
6
+ import type { CodegraphSessionStartOutcome } from "../src/hook.ts";
7
+ import { runCodegraphSessionStartWorker } from "../src/hook.ts";
8
+ import { readSessionStartCooldown } from "../src/session-start-cooldown.ts";
9
+
10
+ describe("CodeGraph SessionStart worker cooldown", () => {
11
+ it("#given codegraph init times out #when the worker finishes #then it records a failed outcome and cooldown", async () => {
12
+ // given
13
+ const workspace = mkdtempSync(join(tmpdir(), "omo-codegraph-worker-timeout-"));
14
+ const homeDir = mkdtempSync(join(tmpdir(), "omo-codegraph-worker-timeout-home-"));
15
+ const outcomes: CodegraphSessionStartOutcome[] = [];
16
+ try {
17
+ // when
18
+ const result = await runCodegraphSessionStartWorker({
19
+ config: { codegraph: { enabled: true, session_start_cooldown_ms: 60_000 }, sources: [], warnings: [] },
20
+ cwd: workspace,
21
+ env: { HOME: homeDir },
22
+ logOutcome: (outcome) => outcomes.push(outcome),
23
+ nodeVersion: "22.14.0",
24
+ nowMs: 1_000,
25
+ projectProbe: () => ({ kind: "uninitialized" }),
26
+ deps: {
27
+ ensureGitignored: () => true,
28
+ ensureProvisioned: () => Promise.resolve({ binPath: "/tmp/codegraph", provisioned: true }),
29
+ prepareWorkspace: () => ({
30
+ dataDir: join(homeDir, ".omo/codegraph/projects/test"),
31
+ dataRoot: join(homeDir, ".omo/codegraph"),
32
+ linked: true,
33
+ mode: "global-linked",
34
+ projectLink: join(workspace, ".codegraph"),
35
+ }),
36
+ resolveCommand: () => ({ argsPrefix: [], command: "/tmp/codegraph", exists: true, source: "path" }),
37
+ runCommand: () => Promise.resolve({ exitCode: 1, stdout: "", timedOut: true }),
38
+ },
39
+ });
40
+ const cooldown = readSessionStartCooldown({
41
+ baseCooldownMs: 60_000,
42
+ homeDir,
43
+ nowMs: 2_000,
44
+ projectRoot: workspace,
45
+ });
46
+
47
+ // then
48
+ expect(result).toEqual({ action: "failed" });
49
+ expect(outcomes).toEqual([{
50
+ action: "failed",
51
+ error: "codegraph init timed out",
52
+ exitCode: 1,
53
+ projectRoot: workspace,
54
+ source: "path",
55
+ timedOut: true,
56
+ }]);
57
+ expect(cooldown).toEqual({ cooldownUntil: 61_000, failureCount: 1, kind: "active" });
58
+ } finally {
59
+ rmSync(homeDir, { recursive: true, force: true });
60
+ rmSync(workspace, { recursive: true, force: true });
61
+ }
62
+ });
63
+
64
+ it("#given codegraph init exits zero without an exact database #when the worker verifies the result #then it records a failure cooldown", async () => {
65
+ // given
66
+ const workspace = mkdtempSync(join(tmpdir(), "omo-codegraph-worker-no-db-"));
67
+ const homeDir = mkdtempSync(join(tmpdir(), "omo-codegraph-worker-no-db-home-"));
68
+ const outcomes: CodegraphSessionStartOutcome[] = [];
69
+ try {
70
+ // when
71
+ const result = await runCodegraphSessionStartWorker({
72
+ config: { codegraph: { enabled: true, session_start_cooldown_ms: 60_000 }, sources: [], warnings: [] },
73
+ cwd: workspace,
74
+ env: { HOME: homeDir },
75
+ logOutcome: (outcome) => outcomes.push(outcome),
76
+ nodeVersion: "22.14.0",
77
+ nowMs: 1_000,
78
+ projectProbe: () => ({ kind: "uninitialized" }),
79
+ deps: {
80
+ ensureGitignored: () => true,
81
+ ensureProvisioned: () => Promise.resolve({ binPath: "/tmp/codegraph", provisioned: true }),
82
+ prepareWorkspace: () => ({
83
+ dataDir: join(homeDir, ".omo/codegraph/projects/test"),
84
+ dataRoot: join(homeDir, ".omo/codegraph"),
85
+ linked: true,
86
+ mode: "global-linked",
87
+ projectLink: join(workspace, ".codegraph"),
88
+ }),
89
+ resolveCommand: () => ({ argsPrefix: [], command: "/tmp/codegraph", exists: true, source: "path" }),
90
+ runCommand: () => Promise.resolve({ exitCode: 0, stdout: "", timedOut: false }),
91
+ },
92
+ });
93
+ const cooldown = readSessionStartCooldown({
94
+ baseCooldownMs: 60_000,
95
+ homeDir,
96
+ nowMs: 2_000,
97
+ projectRoot: workspace,
98
+ });
99
+
100
+ // then
101
+ expect(result).toEqual({ action: "failed" });
102
+ expect(outcomes).toEqual([{
103
+ action: "failed",
104
+ error: "codegraph init completed without creating an exact project database",
105
+ exitCode: 0,
106
+ projectRoot: workspace,
107
+ source: "path",
108
+ timedOut: false,
109
+ }]);
110
+ expect(cooldown).toEqual({ cooldownUntil: 61_000, failureCount: 1, kind: "active" });
111
+ } finally {
112
+ rmSync(homeDir, { recursive: true, force: true });
113
+ rmSync(workspace, { recursive: true, force: true });
114
+ }
115
+ });
116
+ });
@@ -6,6 +6,11 @@ import { join } from "node:path";
6
6
  import { CODEGRAPH_PINNED_VERSION } from "../../../../../utils/src/codegraph/manifest.ts";
7
7
  import { resolveCodegraphCommandInvocation, runCodegraphSessionStartWorker } from "../src/hook.ts";
8
8
 
9
+ function writeProjectDatabase(workspace: string): void {
10
+ mkdirSync(join(workspace, ".codegraph"), { recursive: true });
11
+ writeFileSync(join(workspace, ".codegraph", "codegraph.db"), "fixture");
12
+ }
13
+
9
14
  describe("CodeGraph SessionStart worker flow", () => {
10
15
  it("#given install_dir has the pinned platform binary #when worker resolves provisioned CodeGraph #then it uses that launcher", async () => {
11
16
  // given
@@ -47,9 +52,11 @@ describe("CodeGraph SessionStart worker flow", () => {
47
52
  const provisioned = options?.provisioned?.() ?? null;
48
53
  return { argsPrefix: [], command: provisioned ?? "missing-codegraph", exists: provisioned !== null, source: provisioned === null ? "path" : "provisioned" };
49
54
  },
55
+ resolveManagedBin: () => binPath,
50
56
  runCommand: (_projectRoot, command, args, options) => {
51
57
  calls.push({ args, command, env: options.env });
52
- return Promise.resolve({ exitCode: 0, stdout: calls.length === 1 ? '{"initialized":false}' : "", timedOut: false });
58
+ writeProjectDatabase(workspace);
59
+ return Promise.resolve({ exitCode: 0, stdout: "", timedOut: false });
53
60
  },
54
61
  },
55
62
  });
@@ -57,7 +64,6 @@ describe("CodeGraph SessionStart worker flow", () => {
57
64
  // then
58
65
  expect(result).toEqual({ action: "initialized" });
59
66
  expect(calls.map((call) => ({ args: [...call.args], command: call.command }))).toEqual([
60
- { args: ["status", "--json"], command: binPath },
61
67
  { args: ["init"], command: binPath },
62
68
  ]);
63
69
  expect(calls[0]?.env["CODEGRAPH_INSTALL_DIR"]).toBe(installDir);
@@ -74,11 +80,11 @@ describe("CodeGraph SessionStart worker flow", () => {
74
80
  const command = "C:\\Users\\test\\.omo\\codegraph\\bin\\codegraph.cmd";
75
81
 
76
82
  // when
77
- const invocation = resolveCodegraphCommandInvocation(command, ["status", "--json"], "win32");
83
+ const invocation = resolveCodegraphCommandInvocation(command, ["init"], "win32");
78
84
 
79
85
  // then
80
86
  expect(invocation).toEqual({
81
- args: ["/d", "/s", "/c", command, "status", "--json"],
87
+ args: ["/d", "/s", "/c", command, "init"],
82
88
  command: "cmd.exe",
83
89
  });
84
90
  });
@@ -88,11 +94,11 @@ describe("CodeGraph SessionStart worker flow", () => {
88
94
  const command = "C:\\Users\\test\\.omo\\codegraph\\bin\\codegraph.cjs";
89
95
 
90
96
  // when
91
- const invocation = resolveCodegraphCommandInvocation(command, ["status", "--json"], "win32");
97
+ const invocation = resolveCodegraphCommandInvocation(command, ["init"], "win32");
92
98
 
93
99
  // then
94
100
  expect(invocation).toEqual({
95
- args: [command, "status", "--json"],
101
+ args: [command, "init"],
96
102
  command: process.execPath,
97
103
  });
98
104
  });
@@ -108,51 +114,47 @@ describe("CodeGraph SessionStart worker flow", () => {
108
114
  expect(invocation).toEqual({ args: ["sync"], command });
109
115
  });
110
116
 
111
- it("#given resolved CodeGraph status #when worker runs #then it runs status before init or sync", async () => {
112
- for (const scenario of [
113
- { action: "initialized", args: [["status", "--json"], ["init"]], stdout: '{"initialized":false}' },
114
- { action: "synced", args: [["status", "--json"], ["sync"]], stdout: '{"initialized":true}' },
115
- ] as const) {
116
- // given
117
- const workspace = mkdtempSync(join(tmpdir(), "omo-codegraph-status-"));
118
- const homeDir = mkdtempSync(join(tmpdir(), "omo-codegraph-status-home-"));
119
- const calls: { readonly args: readonly string[]; readonly command: string; readonly env: Record<string, string> }[] = [];
120
- const outcomes: unknown[] = [];
121
- try {
122
- // when
123
- const result = await runCodegraphSessionStartWorker({
124
- config: { codegraph: { enabled: true, install_dir: "/tmp/codegraph-install" }, sources: [], trustedCodegraphInstallDir: "/tmp/codegraph-install", warnings: [] },
125
- nodeVersion: "22.14.0",
126
- cwd: workspace,
127
- env: { HOME: homeDir },
128
- logOutcome: (outcome) => outcomes.push(outcome),
129
- deps: {
130
- ensureGitignored: () => true,
131
- ensureProvisioned: () => Promise.resolve({ binPath: "/tmp/codegraph", provisioned: true }),
132
- prepareWorkspace: () => ({
133
- dataDir: join(homeDir, ".omo/codegraph/projects/test"),
134
- dataRoot: join(homeDir, ".omo/codegraph"),
135
- linked: true,
136
- mode: "global-linked",
137
- projectLink: join(workspace, ".codegraph"),
138
- }),
139
- resolveCommand: () => ({ argsPrefix: [], command: "/tmp/codegraph", exists: true, source: "path" }),
140
- runCommand: (_projectRoot, command, args, options) => {
141
- calls.push({ args, command, env: options.env });
142
- return Promise.resolve({ exitCode: 0, stdout: calls.length === 1 ? scenario.stdout : "", timedOut: false });
143
- },
117
+ it("#given an uninitialized exact project #when worker runs #then it invokes init directly without a status command", async () => {
118
+ // given
119
+ const workspace = mkdtempSync(join(tmpdir(), "omo-codegraph-init-direct-"));
120
+ const homeDir = mkdtempSync(join(tmpdir(), "omo-codegraph-init-direct-home-"));
121
+ const calls: { readonly args: readonly string[]; readonly env: Record<string, string> }[] = [];
122
+ const outcomes: unknown[] = [];
123
+ try {
124
+ // when
125
+ const result = await runCodegraphSessionStartWorker({
126
+ config: { codegraph: { enabled: true, install_dir: "/tmp/codegraph-install" }, sources: [], trustedCodegraphInstallDir: "/tmp/codegraph-install", warnings: [] },
127
+ nodeVersion: "22.14.0",
128
+ cwd: workspace,
129
+ env: { HOME: homeDir },
130
+ logOutcome: (outcome) => outcomes.push(outcome),
131
+ deps: {
132
+ ensureGitignored: () => true,
133
+ ensureProvisioned: () => Promise.resolve({ binPath: "/tmp/codegraph", provisioned: true }),
134
+ prepareWorkspace: () => ({
135
+ dataDir: join(homeDir, ".omo/codegraph/projects/test"),
136
+ dataRoot: join(homeDir, ".omo/codegraph"),
137
+ linked: true,
138
+ mode: "global-linked",
139
+ projectLink: join(workspace, ".codegraph"),
140
+ }),
141
+ resolveCommand: () => ({ argsPrefix: [], command: "/tmp/codegraph", exists: true, source: "path" }),
142
+ runCommand: (_projectRoot, _command, args, options) => {
143
+ calls.push({ args, env: options.env });
144
+ writeProjectDatabase(workspace);
145
+ return Promise.resolve({ exitCode: 0, stdout: "", timedOut: false });
144
146
  },
145
- });
147
+ },
148
+ });
146
149
 
147
- // then
148
- expect(result).toEqual({ action: scenario.action });
149
- expect(calls.map((call) => [...call.args])).toEqual(scenario.args.map((args) => [...args]));
150
- expect(calls[0]?.env["CODEGRAPH_INSTALL_DIR"]).toBe("/tmp/codegraph-install");
151
- expect(outcomes).toEqual([{ action: scenario.action, exitCode: 0, projectRoot: workspace, source: "path", timedOut: false }]);
152
- } finally {
153
- rmSync(workspace, { recursive: true, force: true });
154
- rmSync(homeDir, { recursive: true, force: true });
155
- }
150
+ // then
151
+ expect(result).toEqual({ action: "initialized" });
152
+ expect(calls.map((call) => [...call.args])).toEqual([["init"]]);
153
+ expect(calls[0]?.env["CODEGRAPH_INSTALL_DIR"]).toBe("/tmp/codegraph-install");
154
+ expect(outcomes).toEqual([{ action: "initialized", exitCode: 0, projectRoot: workspace, source: "path", timedOut: false }]);
155
+ } finally {
156
+ rmSync(workspace, { recursive: true, force: true });
157
+ rmSync(homeDir, { recursive: true, force: true });
156
158
  }
157
159
  });
158
160
 
@@ -182,13 +184,14 @@ describe("CodeGraph SessionStart worker flow", () => {
182
184
  resolveCommand: () => ({ argsPrefix: [], command: "/tmp/codegraph", exists: true, source: "path" }),
183
185
  runCommand: (_projectRoot, command, args, options) => {
184
186
  calls.push({ args, command, env: options.env });
185
- return Promise.resolve({ exitCode: 0, stdout: calls.length === 1 ? '{"initialized":true}' : "", timedOut: false });
187
+ writeProjectDatabase(workspace);
188
+ return Promise.resolve({ exitCode: 0, stdout: "", timedOut: false });
186
189
  },
187
190
  },
188
191
  });
189
192
 
190
193
  // then
191
- expect(result).toEqual({ action: "synced" });
194
+ expect(result).toEqual({ action: "initialized" });
192
195
  expect(calls.length).toBeGreaterThan(0);
193
196
  for (const call of calls) {
194
197
  expect(call.env["CODEGRAPH_NO_DAEMON"]).toBe("1");
@@ -211,7 +214,8 @@ describe("CodeGraph SessionStart worker flow", () => {
211
214
  const fakeCodegraphScript = [
212
215
  "const fs = require('node:fs');",
213
216
  `fs.appendFileSync(${JSON.stringify(logPath)}, JSON.stringify({install:process.env.CODEGRAPH_INSTALL_DIR,openai:process.env.OPENAI_API_KEY}) + '\\n');`,
214
- "process.stdout.write('{\"initialized\":true}');",
217
+ "fs.mkdirSync('.codegraph', { recursive: true });",
218
+ "fs.writeFileSync('.codegraph/codegraph.db', 'fixture');",
215
219
  ].join("");
216
220
 
217
221
  // when
@@ -238,15 +242,12 @@ describe("CodeGraph SessionStart worker flow", () => {
238
242
  });
239
243
 
240
244
  // then
241
- expect(result).toEqual({ action: "synced" });
245
+ expect(result).toEqual({ action: "initialized" });
242
246
  const captured = readFileSync(logPath, "utf8")
243
247
  .trim()
244
248
  .split("\n")
245
249
  .map((line) => JSON.parse(line));
246
- expect(captured).toEqual([
247
- { install: join(homeDir, ".omo", "codegraph") },
248
- { install: join(homeDir, ".omo", "codegraph") },
249
- ]);
250
+ expect(captured).toEqual([{ install: join(homeDir, ".omo", "codegraph") }]);
250
251
  } finally {
251
252
  if (originalOpenAiKey === undefined) delete process.env["OPENAI_API_KEY"];
252
253
  else process.env["OPENAI_API_KEY"] = originalOpenAiKey;
@@ -8,7 +8,7 @@
8
8
  "type": "command",
9
9
  "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook post-tool-use",
10
10
  "timeout": 30,
11
- "statusMessage": "(OmO 4.19.2) Checking Comments"
11
+ "statusMessage": "(OmO 4.19.3) Checking Comments"
12
12
  }
13
13
  ]
14
14
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/codex-comment-checker",
3
- "version": "4.19.2",
3
+ "version": "4.19.3",
4
4
  "description": "Codex plugin that runs comment-checker after edit-like PostToolUse hooks.",
5
5
  "type": "module",
6
6
  "packageManager": "npm@11.12.1",