oh-my-openagent 4.18.0 → 4.18.2

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 (226) hide show
  1. package/.agents/skills/codex-qa/scripts/lsp-e2e.sh +3677 -0
  2. package/.agents/skills/opencode-qa/scripts/lsp-e2e.sh +3154 -0
  3. package/.agents/skills/work-with-pr/SKILL.md +16 -37
  4. package/.agents/skills/work-with-pr-workspace/evals/evals.json +3 -3
  5. package/.opencode/skills/work-with-pr/SKILL.md +16 -37
  6. package/.opencode/skills/work-with-pr-workspace/evals/evals.json +3 -3
  7. package/bin/AGENTS.md +33 -0
  8. package/dist/cli/index.js +500 -158
  9. package/dist/cli-node/index.js +500 -158
  10. package/dist/hooks/anthropic-context-window-limit-recovery/empty-content-recovery-sdk.d.ts +6 -0
  11. package/dist/hooks/category-skill-reminder/hook.d.ts +9 -1
  12. package/dist/hooks/comment-checker/hook.d.ts +8 -1
  13. package/dist/hooks/todo-continuation-enforcer/types.d.ts +3 -0
  14. package/dist/index.js +956 -702
  15. package/dist/plugin/messages-transform.d.ts +1 -0
  16. package/dist/plugin-handlers/prometheus-agent-config-builder.d.ts +2 -0
  17. package/dist/tui.js +43 -4
  18. package/package.json +16 -16
  19. package/packages/git-bash-mcp/dist/cli.js +81 -19
  20. package/packages/lsp-core/package.json +4 -0
  21. package/packages/lsp-core/src/index.ts +1 -0
  22. package/packages/lsp-core/src/lsp/cleanup-errors.test.ts +18 -0
  23. package/packages/lsp-core/src/lsp/cleanup-errors.ts +12 -3
  24. package/packages/lsp-core/src/lsp/client-diagnostics-freshness.integration.test.ts +261 -0
  25. package/packages/lsp-core/src/lsp/client-wrapper.test.ts +63 -0
  26. package/packages/lsp-core/src/lsp/client-wrapper.ts +35 -5
  27. package/packages/lsp-core/src/lsp/client.ts +262 -80
  28. package/packages/lsp-core/src/lsp/config-loader.ts +5 -17
  29. package/packages/lsp-core/src/lsp/connection.ts +12 -6
  30. package/packages/lsp-core/src/lsp/directory-diagnostics.test.ts +221 -0
  31. package/packages/lsp-core/src/lsp/directory-diagnostics.ts +61 -28
  32. package/packages/lsp-core/src/lsp/errors.ts +11 -0
  33. package/packages/lsp-core/src/lsp/fixtures/diagnostics-freshness-contract-probe.ts +283 -0
  34. package/packages/lsp-core/src/lsp/fixtures/workspace-edit-contract-probe.ts +196 -0
  35. package/packages/lsp-core/src/lsp/fixtures/workspace-edit-server.mjs +215 -0
  36. package/packages/lsp-core/src/lsp/formatters.ts +3 -0
  37. package/packages/lsp-core/src/lsp/json-rpc-connection-cancellation.test.ts +97 -0
  38. package/packages/lsp-core/src/lsp/json-rpc-connection.ts +73 -5
  39. package/packages/lsp-core/src/lsp/server-install-state.ts +3 -6
  40. package/packages/lsp-core/src/lsp/transport-protocol.ts +52 -0
  41. package/packages/lsp-core/src/lsp/transport.ts +96 -70
  42. package/packages/lsp-core/src/lsp/workspace-apply-edit-failure.ts +19 -0
  43. package/packages/lsp-core/src/lsp/workspace-apply-edit-lease.integration.test.ts +214 -0
  44. package/packages/lsp-core/src/lsp/workspace-apply-edit-sync.integration.test.ts +113 -0
  45. package/packages/lsp-core/src/lsp/workspace-apply-edit-test-support.ts +163 -0
  46. package/packages/lsp-core/src/lsp/workspace-apply-edit.integration.test.ts +163 -0
  47. package/packages/lsp-core/src/lsp/workspace-document-state.test.ts +67 -0
  48. package/packages/lsp-core/src/lsp/workspace-document-state.ts +368 -0
  49. package/packages/lsp-core/src/lsp/workspace-edit-adversarial.test.ts +113 -0
  50. package/packages/lsp-core/src/lsp/workspace-edit-commit.test.ts +140 -0
  51. package/packages/lsp-core/src/lsp/workspace-edit-commit.ts +220 -0
  52. package/packages/lsp-core/src/lsp/workspace-edit-contract-evidence.test.ts +56 -0
  53. package/packages/lsp-core/src/lsp/workspace-edit-contract-evidence.ts +30 -0
  54. package/packages/lsp-core/src/lsp/workspace-edit-fingerprint.ts +44 -0
  55. package/packages/lsp-core/src/lsp/workspace-edit-options.test.ts +147 -0
  56. package/packages/lsp-core/src/lsp/workspace-edit-parse-helpers.ts +59 -0
  57. package/packages/lsp-core/src/lsp/workspace-edit-parser.ts +130 -0
  58. package/packages/lsp-core/src/lsp/workspace-edit-path.ts +98 -0
  59. package/packages/lsp-core/src/lsp/workspace-edit-plan-types.ts +60 -0
  60. package/packages/lsp-core/src/lsp/workspace-edit-plan.ts +73 -0
  61. package/packages/lsp-core/src/lsp/workspace-edit-prevalidation.test.ts +174 -0
  62. package/packages/lsp-core/src/lsp/workspace-edit-resource-parser.ts +89 -0
  63. package/packages/lsp-core/src/lsp/workspace-edit-simulation.ts +183 -0
  64. package/packages/lsp-core/src/lsp/workspace-edit-snapshot.ts +53 -0
  65. package/packages/lsp-core/src/lsp/workspace-edit-text.ts +125 -0
  66. package/packages/lsp-core/src/lsp/workspace-edit-types.ts +121 -0
  67. package/packages/lsp-core/src/lsp/workspace-edit.characterization.test.ts +95 -0
  68. package/packages/lsp-core/src/lsp/workspace-edit.ts +49 -200
  69. package/packages/lsp-core/src/lsp/workspace-mutation-controller.ts +182 -0
  70. package/packages/lsp-core/src/mcp.ts +18 -7
  71. package/packages/lsp-core/src/missing-dependency-result.test.ts +105 -0
  72. package/packages/lsp-core/src/missing-dependency-result.ts +57 -0
  73. package/packages/lsp-core/src/post-edit/index.ts +1 -0
  74. package/packages/lsp-core/src/post-edit/orchestration.test.ts +157 -0
  75. package/packages/lsp-core/src/post-edit/orchestration.ts +178 -0
  76. package/packages/lsp-core/src/request-context.test.ts +171 -0
  77. package/packages/lsp-core/src/request-context.ts +222 -9
  78. package/packages/lsp-core/src/tool-surface.test.ts +4 -1
  79. package/packages/lsp-core/src/tools/diagnostics.ts +32 -13
  80. package/packages/lsp-core/src/tools/navigation.ts +12 -12
  81. package/packages/lsp-core/src/tools/rename.ts +10 -15
  82. package/packages/lsp-core/src/tools/symbols.ts +11 -11
  83. package/packages/lsp-core/src/tools/types.ts +2 -1
  84. package/packages/lsp-daemon/dist/cli.js +3330 -764
  85. package/packages/lsp-daemon/dist/client.d.ts +105 -0
  86. package/packages/lsp-daemon/dist/client.js +5995 -0
  87. package/packages/lsp-daemon/dist/daemon-client.d.ts +12 -7
  88. package/packages/lsp-daemon/dist/daemon-client.js +139 -32
  89. package/packages/lsp-daemon/dist/daemon-server.d.ts +1 -0
  90. package/packages/lsp-daemon/dist/daemon-server.js +40 -15
  91. package/packages/lsp-daemon/dist/ensure-daemon.d.ts +10 -8
  92. package/packages/lsp-daemon/dist/ensure-daemon.js +135 -51
  93. package/packages/lsp-daemon/dist/index.d.ts +2 -2
  94. package/packages/lsp-daemon/dist/index.js +3093 -786
  95. package/packages/lsp-daemon/dist/ipc-protocol.d.ts +46 -0
  96. package/packages/lsp-daemon/dist/ipc-protocol.js +187 -0
  97. package/packages/lsp-daemon/dist/lock.js +14 -4
  98. package/packages/lsp-daemon/dist/ownership.d.ts +49 -0
  99. package/packages/lsp-daemon/dist/ownership.js +168 -0
  100. package/packages/lsp-daemon/dist/paths.d.ts +33 -9
  101. package/packages/lsp-daemon/dist/paths.js +72 -33
  102. package/packages/lsp-daemon/dist/proxy.d.ts +5 -0
  103. package/packages/lsp-daemon/dist/proxy.js +123 -16
  104. package/packages/lsp-daemon/dist/request-routing.d.ts +7 -2
  105. package/packages/lsp-daemon/dist/request-routing.js +71 -22
  106. package/packages/lsp-daemon/dist/run-daemon.js +9 -2
  107. package/packages/lsp-daemon/dist/runtime-contract.d.ts +21 -0
  108. package/packages/lsp-daemon/dist/runtime-contract.js +58 -0
  109. package/packages/lsp-daemon/dist/socket-jsonrpc.js +6 -1
  110. package/packages/lsp-daemon/package.json +12 -3
  111. package/packages/lsp-tools-mcp/dist/cli.js +2189 -454
  112. package/packages/lsp-tools-mcp/dist/lsp/manager.js +1741 -148
  113. package/packages/lsp-tools-mcp/dist/mcp.js +2206 -471
  114. package/packages/lsp-tools-mcp/dist/request-context.js +176 -6
  115. package/packages/lsp-tools-mcp/dist/tools.js +2119 -447
  116. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  117. package/packages/omo-codex/plugin/.mcp.json +2 -1
  118. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  119. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  120. package/packages/omo-codex/plugin/components/codegraph/dist/cli.js +100 -28
  121. package/packages/omo-codex/plugin/components/codegraph/dist/serve.js +100 -28
  122. package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
  123. package/packages/omo-codex/plugin/components/codegraph/src/mcp-bridge.ts +21 -9
  124. package/packages/omo-codex/plugin/components/codegraph/test/mcp-bridge-fixtures.ts +35 -0
  125. package/packages/omo-codex/plugin/components/codegraph/test/serve-mcp-bridge-lifecycle.test.ts +69 -0
  126. package/packages/omo-codex/plugin/components/codegraph/test/serve-mcp-bridge.test.ts +57 -1
  127. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  128. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  129. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  130. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  131. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  132. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  133. package/packages/omo-codex/plugin/components/lsp/.mcp.json +2 -1
  134. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +55 -0
  135. package/packages/omo-codex/plugin/components/lsp/dist/cli.js +3033 -936
  136. package/packages/omo-codex/plugin/components/lsp/dist/codex-hook-cli.js +0 -4
  137. package/packages/omo-codex/plugin/components/lsp/dist/codex-hook.d.ts +5 -2
  138. package/packages/omo-codex/plugin/components/lsp/dist/codex-hook.js +41 -62
  139. package/packages/omo-codex/plugin/components/lsp/dist/daemon-cli-path.d.ts +1 -1
  140. package/packages/omo-codex/plugin/components/lsp/dist/daemon-cli-path.js +24 -15
  141. package/packages/omo-codex/plugin/components/lsp/dist/lsp-session-state.d.ts +3 -7
  142. package/packages/omo-codex/plugin/components/lsp/dist/lsp-session-state.js +23 -49
  143. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  144. package/packages/omo-codex/plugin/components/lsp/package.json +3 -2
  145. package/packages/omo-codex/plugin/components/lsp/scripts/build-lsp-daemon.mjs +31 -1
  146. package/packages/omo-codex/plugin/components/lsp/scripts/build-lsp-daemon.test.mjs +76 -0
  147. package/packages/omo-codex/plugin/components/lsp/scripts/build-runtime.mjs +201 -0
  148. package/packages/omo-codex/plugin/components/lsp/scripts/build-runtime.test.mjs +55 -0
  149. package/packages/omo-codex/plugin/components/lsp/src/codex-hook-cli.ts +0 -4
  150. package/packages/omo-codex/plugin/components/lsp/src/codex-hook.ts +49 -71
  151. package/packages/omo-codex/plugin/components/lsp/src/daemon-cli-path.ts +26 -15
  152. package/packages/omo-codex/plugin/components/lsp/src/lsp-session-state.ts +26 -64
  153. package/packages/omo-codex/plugin/components/lsp/test/codex-hook-unavailable.test.ts +16 -17
  154. package/packages/omo-codex/plugin/components/lsp/test/codex-hook.test.ts +30 -4
  155. package/packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts +20 -5
  156. package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-5.5.md +1 -1
  157. package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-5.6.md +5 -3
  158. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  159. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  160. package/packages/omo-codex/plugin/components/start-work-continuation/README.md +2 -2
  161. package/packages/omo-codex/plugin/components/start-work-continuation/directive.md +3 -3
  162. package/packages/omo-codex/plugin/components/start-work-continuation/dist/cli.js +2 -2
  163. package/packages/omo-codex/plugin/components/start-work-continuation/hooks/hooks.json +2 -2
  164. package/packages/omo-codex/plugin/components/start-work-continuation/package.json +1 -1
  165. package/packages/omo-codex/plugin/components/start-work-continuation/src/boulder-reader.ts +1 -1
  166. package/packages/omo-codex/plugin/components/start-work-continuation/src/codex-hook.ts +1 -1
  167. package/packages/omo-codex/plugin/components/start-work-continuation/test/boulder-reader.test.ts +15 -1
  168. package/packages/omo-codex/plugin/components/start-work-continuation/test/codex-hook.test.ts +20 -0
  169. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  170. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  171. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  172. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  173. package/packages/omo-codex/plugin/components/ultrawork/directive.md +50 -33
  174. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  175. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  176. package/packages/omo-codex/plugin/components/ultrawork/skills/ultrawork/SKILL.md +50 -33
  177. package/packages/omo-codex/plugin/components/ulw-loop/directive.md +50 -33
  178. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +3 -3
  179. package/packages/omo-codex/plugin/components/ulw-loop/dist/stop-resume-hook.js +5 -6
  180. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +4 -4
  181. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  182. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +1 -1
  183. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +6 -5
  184. package/packages/omo-codex/plugin/components/ulw-loop/src/stop-resume-hook.ts +5 -6
  185. package/packages/omo-codex/plugin/components/ulw-loop/test/stop-resume-hook.test.ts +2 -2
  186. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  187. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  188. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  189. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +1 -1
  190. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  191. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  192. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  193. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  194. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  195. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  196. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  197. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  198. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  199. package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +1 -1
  200. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  201. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  202. package/packages/omo-codex/plugin/hooks/stop-checking-start-work-continuation.json +1 -1
  203. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  204. package/packages/omo-codex/plugin/hooks/subagent-stop-checking-start-work-continuation.json +1 -1
  205. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  206. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  207. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  208. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  209. package/packages/omo-codex/plugin/package-lock.json +26 -14
  210. package/packages/omo-codex/plugin/package.json +1 -1
  211. package/packages/omo-codex/plugin/scripts/build-bundled-mcp-runtimes.mjs +2 -3
  212. package/packages/omo-codex/plugin/scripts/build-components.mjs +13 -1
  213. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +9 -1
  214. package/packages/omo-codex/plugin/skills/review-work/SKILL.md +7 -0
  215. package/packages/omo-codex/plugin/skills/start-work/SKILL.md +2 -1
  216. package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +50 -33
  217. package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +1 -1
  218. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +6 -5
  219. package/packages/omo-codex/plugin/test/aggregate-build.test.mjs +8 -0
  220. package/packages/omo-codex/plugin/test/component-bundled-cli.test.mjs +128 -15
  221. package/packages/omo-codex/plugin/test/install-time-build-runtime.test.mjs +10 -0
  222. package/packages/omo-codex/plugin/test/lsp-prebuild-layouts.test.mjs +2 -0
  223. package/packages/omo-codex/plugin/test/mcp-research-servers.test.mjs +1 -0
  224. package/packages/omo-codex/plugin/test/sync-skills-orchestration.test.mjs +7 -0
  225. package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +34 -3
  226. package/packages/omo-codex/scripts/install-dist/install-local.mjs +328 -63
@@ -0,0 +1,3677 @@
1
+ #!/usr/bin/env bash
2
+ # lsp-e2e.sh - isolated live Codex QA for the shared OMO LSP daemon.
3
+ #
4
+ # Normal mode installs this worktree's OMO plugin into a disposable CODEX_HOME,
5
+ # drives a real `codex app-server` against the local codex-qa mock model, calls
6
+ # the installed lsp MCP through `mcpServer/tool/call`, and records a PASS result
7
+ # only after path-contract / rename, hook, isolation, and cleanup assertions
8
+ # succeed.
9
+ #
10
+ # Usage:
11
+ # lsp-e2e.sh --scenario <name> --evidence-dir <absolute-dir>
12
+ # lsp-e2e.sh --self-test
13
+
14
+ set -uo pipefail
15
+
16
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
17
+ REPO_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd -P)"
18
+
19
+ SCENARIO=""
20
+ EVIDENCE_DIR=""
21
+ SELF_TEST=0
22
+ SANDBOX_ROOT=""
23
+ OMO_TEST_ROOT=""
24
+ EXPECTED_DAEMON_CLI=""
25
+ MOCK_PID=""
26
+ APP_SERVER_PID_FILE=""
27
+ RESULT_STAGE=""
28
+ CLEANUP_RUNNING=0
29
+ NORMAL_CLEANUP_COMPLETE=0
30
+ REAL_HOME="${HOME:-}"
31
+ REAL_OMO_ROOT="${HOME:-}/.omo/lsp-daemon"
32
+ REAL_CODEX_CONFIG="${HOME:-}/.codex/config.toml"
33
+ REAL_OMO_BEFORE_HASH=""
34
+ DAEMON_DIST_DIR="$REPO_ROOT/packages/lsp-daemon/dist"
35
+ DAEMON_DIST_BACKUP=""
36
+ DAEMON_DIST_WAS_PRESENT=0
37
+ DAEMON_DIST_PREPARED=0
38
+ BUILD_LOCK_DIR=""
39
+ CANCELLATION_SMOKE_RELATIVE="packages/lsp-daemon/scripts/qa/cancellation-smoke.mjs"
40
+ COMMIT_BARRIER_SMOKE_RELATIVE="packages/lsp-daemon/scripts/qa/commit-barrier-smoke.mjs"
41
+
42
+ log() { printf '[codex-lsp-e2e] %s\n' "$*" >&2; }
43
+ fail() { log "FAIL: $*"; return 1; }
44
+
45
+ usage() {
46
+ sed -n '2,12p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
47
+ }
48
+
49
+ parse_args() {
50
+ while [ "$#" -gt 0 ]; do
51
+ case "$1" in
52
+ --scenario)
53
+ [ "$#" -ge 2 ] || { log "--scenario requires a value"; return 2; }
54
+ [ -z "$SCENARIO" ] || { log "--scenario may be provided only once"; return 2; }
55
+ SCENARIO="$2"
56
+ shift 2
57
+ ;;
58
+ --evidence-dir)
59
+ [ "$#" -ge 2 ] || { log "--evidence-dir requires a directory"; return 2; }
60
+ [ -z "$EVIDENCE_DIR" ] || { log "--evidence-dir may be provided only once"; return 2; }
61
+ EVIDENCE_DIR="$2"
62
+ shift 2
63
+ ;;
64
+ --self-test)
65
+ [ "$SELF_TEST" -eq 0 ] || { log "--self-test may be provided only once"; return 2; }
66
+ SELF_TEST=1
67
+ shift
68
+ ;;
69
+ -h|--help)
70
+ usage
71
+ exit 0
72
+ ;;
73
+ *)
74
+ log "unknown option: $1"
75
+ return 2
76
+ ;;
77
+ esac
78
+ done
79
+
80
+ if [ "$SELF_TEST" -eq 1 ]; then
81
+ if [ -n "$SCENARIO" ] || [ -n "$EVIDENCE_DIR" ]; then
82
+ log "--self-test cannot be combined with normal-mode options"
83
+ return 2
84
+ fi
85
+ return 0
86
+ fi
87
+
88
+ [ -n "$SCENARIO" ] || { log "--scenario is required"; return 2; }
89
+ [ -n "$EVIDENCE_DIR" ] || { log "--evidence-dir is required"; return 2; }
90
+ case "$SCENARIO" in
91
+ [A-Za-z0-9]* ) ;;
92
+ * ) log "invalid scenario: $SCENARIO"; return 2 ;;
93
+ esac
94
+ if ! printf '%s' "$SCENARIO" | grep -Eq '^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$'; then
95
+ log "invalid scenario: $SCENARIO"
96
+ return 2
97
+ fi
98
+ case "$EVIDENCE_DIR" in
99
+ /*) ;;
100
+ *) log "--evidence-dir must be absolute"; return 2 ;;
101
+ esac
102
+ }
103
+
104
+ require_bins() {
105
+ local missing=0 bin
106
+ for bin in "$@"; do
107
+ if ! command -v "$bin" >/dev/null 2>&1; then
108
+ log "missing dependency: $bin"
109
+ missing=1
110
+ fi
111
+ done
112
+ [ "$missing" -eq 0 ]
113
+ }
114
+
115
+ verify_tracked_cancellation_probes() {
116
+ local dependency ignored_evidence_root=".omo""/evidence"
117
+ if grep -Fq "$ignored_evidence_root" "${BASH_SOURCE[0]}"; then
118
+ fail "LSP QA driver references ignored evidence state"
119
+ return 1
120
+ fi
121
+ for dependency in "$CANCELLATION_SMOKE_RELATIVE" "$COMMIT_BARRIER_SMOKE_RELATIVE"; do
122
+ [ -f "$REPO_ROOT/$dependency" ] || { fail "missing cancellation QA dependency: $dependency"; return 1; }
123
+ git -C "$REPO_ROOT" ls-files --error-unmatch -- "$dependency" >/dev/null 2>&1 || {
124
+ fail "cancellation QA dependency is not tracked: $dependency"
125
+ return 1
126
+ }
127
+ done
128
+ }
129
+
130
+ hash_path() {
131
+ node --input-type=module - "$1" <<'NODE'
132
+ import { createHash } from "node:crypto";
133
+ import { lstatSync, readFileSync, readlinkSync, readdirSync } from "node:fs";
134
+ import { basename, join } from "node:path";
135
+
136
+ const target = process.argv[2];
137
+ const hash = createHash("sha256");
138
+
139
+ function visit(path, relative) {
140
+ const stat = lstatSync(path);
141
+ const kind = stat.isDirectory() ? "dir" : stat.isFile() ? "file" : stat.isSymbolicLink() ? "link" : "special";
142
+ hash.update(`${kind}\0${relative}\0${stat.mode & 0o7777}\0`);
143
+ if (kind === "file") hash.update(readFileSync(path));
144
+ if (kind === "link") hash.update(readlinkSync(path));
145
+ if (kind === "dir") {
146
+ for (const name of readdirSync(path).sort()) visit(join(path, name), relative ? `${relative}/${name}` : name);
147
+ }
148
+ }
149
+
150
+ try {
151
+ visit(target, basename(target));
152
+ process.stdout.write(hash.digest("hex"));
153
+ } catch (error) {
154
+ if (error && error.code === "ENOENT") process.stdout.write("ABSENT");
155
+ else throw error;
156
+ }
157
+ NODE
158
+ }
159
+
160
+ run_bounded() {
161
+ local seconds="$1" output="$2"
162
+ shift 2
163
+ node --input-type=module - "$seconds" "$output" "$@" <<'NODE'
164
+ import { closeSync, openSync } from "node:fs";
165
+ import { spawn } from "node:child_process";
166
+
167
+ const [secondsRaw, output, command, ...args] = process.argv.slice(2);
168
+ const seconds = Number(secondsRaw);
169
+ if (!Number.isFinite(seconds) || seconds <= 0 || !command) process.exit(125);
170
+ const fd = openSync(output, "w");
171
+ const child = spawn(command, args, {
172
+ stdio: ["ignore", fd, fd],
173
+ detached: process.platform !== "win32",
174
+ env: process.env,
175
+ });
176
+ let timedOut = false;
177
+ let forceTimer;
178
+ const timer = setTimeout(() => {
179
+ timedOut = true;
180
+ try {
181
+ if (process.platform !== "win32") process.kill(-child.pid, "SIGTERM");
182
+ else child.kill("SIGTERM");
183
+ } catch {}
184
+ forceTimer = setTimeout(() => {
185
+ try {
186
+ if (process.platform !== "win32") process.kill(-child.pid, "SIGKILL");
187
+ else child.kill("SIGKILL");
188
+ } catch {}
189
+ }, 3000);
190
+ }, seconds * 1000);
191
+ child.on("error", () => {
192
+ clearTimeout(timer);
193
+ if (forceTimer) clearTimeout(forceTimer);
194
+ closeSync(fd);
195
+ process.exit(126);
196
+ });
197
+ child.on("exit", (code, signal) => {
198
+ clearTimeout(timer);
199
+ if (forceTimer) clearTimeout(forceTimer);
200
+ closeSync(fd);
201
+ if (timedOut) process.exit(124);
202
+ if (typeof code === "number") process.exit(code);
203
+ process.exit(signal ? 128 : 1);
204
+ });
205
+ NODE
206
+ }
207
+
208
+ with_shared_build_lock() {
209
+ local command_name="$1" attempts=0 rc
210
+ shift
211
+ BUILD_LOCK_DIR="$REPO_ROOT/.omo/locks/lsp-daemon-build.lock"
212
+ mkdir -p "$(dirname "$BUILD_LOCK_DIR")"
213
+ while ! mkdir "$BUILD_LOCK_DIR" 2>/dev/null; do
214
+ [ "$attempts" -lt 600 ] || { fail "timed out waiting for shared LSP daemon build lock"; return 1; }
215
+ sleep 0.2
216
+ attempts=$((attempts + 1))
217
+ done
218
+ printf 'pid=%s\ncommand=%s\n' "$$" "$command_name" >"$BUILD_LOCK_DIR/owner.txt"
219
+ "$@"
220
+ rc=$?
221
+ rm -rf "$BUILD_LOCK_DIR"
222
+ BUILD_LOCK_DIR=""
223
+ return "$rc"
224
+ }
225
+
226
+ safe_rm_tree() {
227
+ local path="$1"
228
+ local attempt=0
229
+ [ -n "$path" ] || return 0
230
+ case "$path" in
231
+ /var/folders/*/T/cqa-lsp-e2e.*|/tmp/cqa-lsp-e2e.*|/private/tmp/cqa-lsp-e2e.*)
232
+ while [ -e "$path" ] && [ "$attempt" -lt 100 ]; do
233
+ rm -rf "$path" 2>/dev/null || true
234
+ [ ! -e "$path" ] && return 0
235
+ sleep 0.1
236
+ attempt=$((attempt + 1))
237
+ done
238
+ [ ! -e "$path" ] || fail "isolated sandbox remained after bounded cleanup: $path"
239
+ ;;
240
+ *)
241
+ fail "refusing to remove unexpected sandbox path: $path"
242
+ ;;
243
+ esac
244
+ }
245
+
246
+ owned_sandbox_pids() {
247
+ [ -n "$SANDBOX_ROOT" ] || return 0
248
+ /bin/ps ax -o pid=,command= 2>/dev/null | while read -r pid command; do
249
+ case "$command" in
250
+ *"$SANDBOX_ROOT"*)
251
+ [ "$pid" = "$$" ] || printf '%s\n' "$pid"
252
+ ;;
253
+ esac
254
+ done
255
+ }
256
+
257
+ stop_owned_sandbox_processes() {
258
+ local pids pid command
259
+ pids="$(owned_sandbox_pids)"
260
+ [ -n "$pids" ] || return 0
261
+ for pid in $pids; do
262
+ kill -0 "$pid" 2>/dev/null || continue
263
+ command="$(process_command "$pid")"
264
+ case "$command" in
265
+ *"$SANDBOX_ROOT"*) ;;
266
+ *) fail "sandbox process $pid changed identity before cleanup"; return 1 ;;
267
+ esac
268
+ kill "$pid" 2>/dev/null || true
269
+ if ! wait_for_exit "$pid"; then
270
+ command="$(process_command "$pid")"
271
+ case "$command" in
272
+ *"$SANDBOX_ROOT"*) kill -9 "$pid" 2>/dev/null || true ;;
273
+ *) fail "sandbox process $pid changed identity during cleanup"; return 1 ;;
274
+ esac
275
+ wait_for_exit "$pid" || { fail "sandbox process $pid survived cleanup"; return 1; }
276
+ fi
277
+ if [ -n "$EVIDENCE_DIR" ] && [ -d "$EVIDENCE_DIR" ]; then
278
+ printf 'sandbox_process_pid=%s alive_after=no\n' "$pid" >>"$EVIDENCE_DIR/owned-process-cleanup.txt"
279
+ fi
280
+ done
281
+ }
282
+
283
+ process_command() {
284
+ /bin/ps -p "$1" -o command= 2>/dev/null || true
285
+ }
286
+
287
+ wait_for_exit() {
288
+ local pid="$1" attempts=0
289
+ while [ "$attempts" -lt 50 ]; do
290
+ kill -0 "$pid" 2>/dev/null || return 0
291
+ sleep 0.1
292
+ attempts=$((attempts + 1))
293
+ done
294
+ return 1
295
+ }
296
+
297
+ stop_known_pid_file() {
298
+ local pid_file="$1" expected_fragment="$2" label="$3"
299
+ [ -n "$pid_file" ] && [ -f "$pid_file" ] || return 0
300
+ local pid command
301
+ pid="$(tr -d '[:space:]' <"$pid_file" 2>/dev/null || true)"
302
+ case "$pid" in
303
+ ''|*[!0-9]*) return 0 ;;
304
+ esac
305
+ kill -0 "$pid" 2>/dev/null || return 0
306
+ command="$(process_command "$pid")"
307
+ case "$command" in
308
+ *"$expected_fragment"*) ;;
309
+ *) fail "refusing to stop unverified $label pid $pid"; return 1 ;;
310
+ esac
311
+ kill "$pid" 2>/dev/null || true
312
+ if ! wait_for_exit "$pid"; then
313
+ command="$(process_command "$pid")"
314
+ case "$command" in
315
+ *"$expected_fragment"*) kill -9 "$pid" 2>/dev/null || true ;;
316
+ *) fail "$label pid $pid changed identity during cleanup"; return 1 ;;
317
+ esac
318
+ wait_for_exit "$pid" || { fail "$label pid $pid survived cleanup"; return 1; }
319
+ fi
320
+ }
321
+
322
+ find_daemon_pid_file() {
323
+ [ -n "$OMO_TEST_ROOT" ] && [ -d "$OMO_TEST_ROOT" ] || return 0
324
+ find "$OMO_TEST_ROOT" -type f -name daemon.pid -print 2>/dev/null | sort | head -1
325
+ }
326
+
327
+ stop_known_daemon() {
328
+ local pid_file pid command
329
+ pid_file="$(find_daemon_pid_file)"
330
+ [ -n "$pid_file" ] || return 0
331
+ pid="$(tr -d '[:space:]' <"$pid_file" 2>/dev/null || true)"
332
+ case "$pid" in
333
+ ''|*[!0-9]*) fail "daemon pid file is malformed: $pid_file"; return 1 ;;
334
+ esac
335
+ kill -0 "$pid" 2>/dev/null || return 0
336
+ command="$(process_command "$pid")"
337
+ case "$command" in
338
+ *"$EXPECTED_DAEMON_CLI"*" daemon"*) ;;
339
+ *) fail "refusing to stop unverified daemon pid $pid"; return 1 ;;
340
+ esac
341
+ kill "$pid" 2>/dev/null || true
342
+ if ! wait_for_exit "$pid"; then
343
+ command="$(process_command "$pid")"
344
+ case "$command" in
345
+ *"$EXPECTED_DAEMON_CLI"*" daemon"*) kill -9 "$pid" 2>/dev/null || true ;;
346
+ *) fail "daemon pid $pid changed identity during cleanup"; return 1 ;;
347
+ esac
348
+ wait_for_exit "$pid" || { fail "daemon pid $pid survived cleanup"; return 1; }
349
+ fi
350
+ }
351
+
352
+ stop_owned_real_daemon_leak() {
353
+ [ "$REAL_OMO_BEFORE_HASH" = "ABSENT" ] || return 0
354
+ [ "$OMO_TEST_ROOT" != "$REAL_OMO_ROOT" ] || return 0
355
+ [ -d "$REAL_OMO_ROOT" ] || return 0
356
+ local pid_file pid command state_dir
357
+ pid_file="$(find "$REAL_OMO_ROOT" -type f -name daemon.pid -print 2>/dev/null | sort | head -1)"
358
+ if [ -n "$pid_file" ]; then
359
+ pid="$(tr -d '[:space:]' <"$pid_file" 2>/dev/null || true)"
360
+ case "$pid" in
361
+ ''|*[!0-9]*) fail "real-root leak pid file is malformed"; return 1 ;;
362
+ esac
363
+ command="$(process_command "$pid")"
364
+ case "$command" in
365
+ *"$EXPECTED_DAEMON_CLI"*" daemon"*) ;;
366
+ *) fail "real OMO root changed by an unverified process; preserving it"; return 1 ;;
367
+ esac
368
+ kill "$pid" 2>/dev/null || true
369
+ wait_for_exit "$pid" || { fail "own leaked daemon did not stop"; return 1; }
370
+ fi
371
+ if find "$REAL_OMO_ROOT" -type f \( -name daemon.pid -o -name daemon.endpoint \) -print 2>/dev/null | grep -q .; then
372
+ fail "real OMO root still contains live markers after own-daemon cleanup"
373
+ return 1
374
+ fi
375
+ find "$REAL_OMO_ROOT" -type f -name daemon.log -delete 2>/dev/null || true
376
+ while IFS= read -r state_dir; do rmdir "$state_dir" 2>/dev/null || true; done < <(find "$REAL_OMO_ROOT" -depth -type d -print 2>/dev/null)
377
+ [ ! -e "$REAL_OMO_ROOT" ] || { fail "real OMO root could not be restored to ABSENT"; return 1; }
378
+ }
379
+
380
+ stop_mock() {
381
+ [ -n "$MOCK_PID" ] || return 0
382
+ if kill -0 "$MOCK_PID" 2>/dev/null; then
383
+ kill "$MOCK_PID" 2>/dev/null || true
384
+ wait_for_exit "$MOCK_PID" || kill -9 "$MOCK_PID" 2>/dev/null || true
385
+ fi
386
+ MOCK_PID=""
387
+ }
388
+
389
+ cleanup_all() {
390
+ local cleanup_rc=0
391
+ [ "$CLEANUP_RUNNING" -eq 0 ] || return 0
392
+ CLEANUP_RUNNING=1
393
+ stop_mock || cleanup_rc=1
394
+ if [ -n "$APP_SERVER_PID_FILE" ]; then
395
+ stop_known_pid_file "$APP_SERVER_PID_FILE" "app-server" "Codex app-server" || cleanup_rc=1
396
+ fi
397
+ stop_known_daemon || cleanup_rc=1
398
+ stop_owned_sandbox_processes || cleanup_rc=1
399
+ stop_owned_real_daemon_leak || cleanup_rc=1
400
+ restore_daemon_dist || cleanup_rc=1
401
+ if [ -n "$BUILD_LOCK_DIR" ]; then
402
+ rm -rf "$BUILD_LOCK_DIR" 2>/dev/null || cleanup_rc=1
403
+ BUILD_LOCK_DIR=""
404
+ fi
405
+ [ -n "$RESULT_STAGE" ] && rm -f "$RESULT_STAGE" 2>/dev/null || true
406
+ if [ -n "$EVIDENCE_DIR" ] && [ -d "$EVIDENCE_DIR" ]; then
407
+ find "$EVIDENCE_DIR" -maxdepth 1 -type f -name '.result.json.*' -delete 2>/dev/null || true
408
+ fi
409
+ if [ -n "$SANDBOX_ROOT" ]; then
410
+ safe_rm_tree "$SANDBOX_ROOT" || cleanup_rc=1
411
+ SANDBOX_ROOT=""
412
+ fi
413
+ CLEANUP_RUNNING=0
414
+ return "$cleanup_rc"
415
+ }
416
+
417
+ on_exit() {
418
+ local rc=$?
419
+ trap - EXIT INT TERM HUP
420
+ if [ "$NORMAL_CLEANUP_COMPLETE" -eq 0 ]; then
421
+ cleanup_all || rc=1
422
+ fi
423
+ if [ "$rc" -ne 0 ] && [ -n "$EVIDENCE_DIR" ] && [ -d "$EVIDENCE_DIR" ]; then
424
+ rm -f "$EVIDENCE_DIR/result.json" 2>/dev/null || true
425
+ fi
426
+ exit "$rc"
427
+ }
428
+ trap on_exit EXIT
429
+ trap 'exit 130' INT
430
+ trap 'exit 143' TERM
431
+ trap 'exit 129' HUP
432
+
433
+ prepare_evidence() {
434
+ [ ! -L "$EVIDENCE_DIR" ] || { fail "evidence directory must not be a symlink"; return 1; }
435
+ mkdir -p "$EVIDENCE_DIR" || return 1
436
+ EVIDENCE_DIR="$(cd "$EVIDENCE_DIR" && pwd -P)"
437
+ rm -f "$EVIDENCE_DIR/result.json"
438
+ find "$EVIDENCE_DIR" -maxdepth 1 -type f -name '.result.json.*' -delete 2>/dev/null || true
439
+ printf 'bash %s --scenario %s --evidence-dir %s\n' \
440
+ "${BASH_SOURCE[0]}" "$SCENARIO" "$EVIDENCE_DIR" >"$EVIDENCE_DIR/invocation.txt"
441
+ }
442
+
443
+ daemon_dist_ready() {
444
+ [ -f "$DAEMON_DIST_DIR/package.json" ] \
445
+ && [ -f "$DAEMON_DIST_DIR/index.js" ] \
446
+ && [ -f "$DAEMON_DIST_DIR/cli.js" ]
447
+ }
448
+
449
+ snapshot_daemon_dist() {
450
+ local backup_root="$1"
451
+ mkdir -p "$backup_root" || return 1
452
+ DAEMON_DIST_BACKUP="$backup_root/lsp-daemon-dist.backup"
453
+ rm -rf "$DAEMON_DIST_BACKUP"
454
+ if [ -e "$DAEMON_DIST_DIR" ]; then
455
+ cp -a "$DAEMON_DIST_DIR" "$DAEMON_DIST_BACKUP" || return 1
456
+ DAEMON_DIST_WAS_PRESENT=1
457
+ else
458
+ DAEMON_DIST_WAS_PRESENT=0
459
+ fi
460
+ DAEMON_DIST_PREPARED=1
461
+ }
462
+
463
+ restore_daemon_dist() {
464
+ [ "$DAEMON_DIST_PREPARED" -eq 1 ] || return 0
465
+ rm -rf "$DAEMON_DIST_DIR" || return 1
466
+ if [ "$DAEMON_DIST_WAS_PRESENT" -eq 1 ]; then
467
+ mkdir -p "$(dirname "$DAEMON_DIST_DIR")" || return 1
468
+ cp -a "$DAEMON_DIST_BACKUP" "$DAEMON_DIST_DIR" || return 1
469
+ fi
470
+ if [ -n "$EVIDENCE_DIR" ] && [ -d "$EVIDENCE_DIR" ]; then
471
+ {
472
+ printf 'daemon_dist_was_present=%s\n' "$( [ "$DAEMON_DIST_WAS_PRESENT" -eq 1 ] && echo true || echo false )"
473
+ printf 'daemon_dist_restored=true\n'
474
+ printf 'daemon_dist_path=%s\n' "$DAEMON_DIST_DIR"
475
+ } >>"$EVIDENCE_DIR/daemon-dist-cleanup.txt"
476
+ fi
477
+ DAEMON_DIST_BACKUP=""
478
+ DAEMON_DIST_WAS_PRESENT=0
479
+ DAEMON_DIST_PREPARED=0
480
+ }
481
+
482
+ prepare_daemon_dist() {
483
+ if daemon_dist_ready; then
484
+ printf 'daemon_dist_ready_before=true\nbuild_skipped=true\n' >"$EVIDENCE_DIR/lsp-daemon-prebuild-receipt.txt"
485
+ return 0
486
+ fi
487
+ if [ "$DAEMON_DIST_PREPARED" -eq 0 ]; then
488
+ snapshot_daemon_dist "$SANDBOX_ROOT/daemon-dist-backup" || return 1
489
+ fi
490
+ {
491
+ printf 'daemon_dist_ready_before=false\n'
492
+ printf 'daemon_dist_was_present=%s\n' "$( [ "$DAEMON_DIST_WAS_PRESENT" -eq 1 ] && echo true || echo false )"
493
+ printf 'command=bun run build:lsp-daemon\n'
494
+ } >"$EVIDENCE_DIR/lsp-daemon-prebuild-receipt.txt"
495
+ run_bounded 300 "$EVIDENCE_DIR/lsp-daemon-prebuild.log" bun run build:lsp-daemon || return 1
496
+ daemon_dist_ready || { fail "lsp-daemon prebuild did not create required dist files"; return 1; }
497
+ printf 'daemon_dist_ready_after=true\n' >>"$EVIDENCE_DIR/lsp-daemon-prebuild-receipt.txt"
498
+ }
499
+
500
+ write_path_contract_probe() {
501
+ local probe_dir="$1" output="$2" script="$SANDBOX_ROOT/path-contract-probe.mjs"
502
+ mkdir -p "$probe_dir"
503
+ cat >"$script" <<'NODE'
504
+ import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
505
+ import { dirname, join, resolve } from "node:path";
506
+ import { pathToFileURL } from "node:url";
507
+
508
+ const repoRoot = process.env.REPO_ROOT;
509
+ const base = process.env.PROBE_BASE;
510
+ const output = process.env.PROBE_OUTPUT;
511
+ if (!repoRoot || !base || !output) throw new Error("missing probe environment");
512
+ const modulePath = join(repoRoot, "packages/lsp-daemon/dist/index.js");
513
+ const daemon = await import(pathToFileURL(modulePath).href + `?qa=${Date.now()}`);
514
+ const cliPath = join(repoRoot, "packages/lsp-daemon/dist/cli.js");
515
+ const packagedVersion = JSON.parse(readFileSync(join(repoRoot, "packages/lsp-daemon/dist/package.json"), "utf8")).version;
516
+ const envNameValues = [daemon.OMO_LSP_DAEMON_CLI, daemon.OMO_LSP_DAEMON_DIR, daemon.OMO_LSP_DAEMON_VERSION].sort();
517
+
518
+ function capture(run) {
519
+ try {
520
+ run();
521
+ return { threw: false };
522
+ } catch (error) {
523
+ return { threw: true, name: error?.name, code: error?.code, reason: error?.reason, message: error?.message };
524
+ }
525
+ }
526
+
527
+ rmSync(base, { recursive: true, force: true });
528
+ const defaultPaths = daemon.daemonPaths({ [daemon.OMO_LSP_DAEMON_DIR]: base });
529
+ const pairedVersion = "qa.1+pair";
530
+ const pairedPaths = daemon.daemonPaths({
531
+ [daemon.OMO_LSP_DAEMON_DIR]: base,
532
+ [daemon.OMO_LSP_DAEMON_CLI]: cliPath,
533
+ [daemon.OMO_LSP_DAEMON_VERSION]: pairedVersion,
534
+ });
535
+
536
+ const singletonRoot = join(dirname(base), "singleton-state");
537
+ rmSync(singletonRoot, { recursive: true, force: true });
538
+ const singletonCli = capture(() => daemon.daemonPaths({
539
+ [daemon.OMO_LSP_DAEMON_DIR]: singletonRoot,
540
+ [daemon.OMO_LSP_DAEMON_CLI]: cliPath,
541
+ }));
542
+ const singletonVersion = capture(() => daemon.daemonPaths({
543
+ [daemon.OMO_LSP_DAEMON_DIR]: singletonRoot,
544
+ [daemon.OMO_LSP_DAEMON_VERSION]: packagedVersion,
545
+ }));
546
+
547
+ const relativeBase = capture(() => daemon.daemonPaths({ [daemon.OMO_LSP_DAEMON_DIR]: "relative/state" }));
548
+ const relativeCli = capture(() => daemon.daemonPaths({
549
+ [daemon.OMO_LSP_DAEMON_DIR]: join(dirname(base), "relative-cli-state"),
550
+ [daemon.OMO_LSP_DAEMON_CLI]: "relative/cli.js",
551
+ [daemon.OMO_LSP_DAEMON_VERSION]: packagedVersion,
552
+ }));
553
+ const nonFileCli = join(dirname(base), "not-a-file");
554
+ mkdirSync(nonFileCli, { recursive: true });
555
+ const missingCli = capture(() => daemon.daemonPaths({
556
+ [daemon.OMO_LSP_DAEMON_DIR]: join(dirname(base), "missing-cli-state"),
557
+ [daemon.OMO_LSP_DAEMON_CLI]: join(dirname(base), "missing-cli.js"),
558
+ [daemon.OMO_LSP_DAEMON_VERSION]: packagedVersion,
559
+ }));
560
+ const directoryCli = capture(() => daemon.daemonPaths({
561
+ [daemon.OMO_LSP_DAEMON_DIR]: join(dirname(base), "directory-cli-state"),
562
+ [daemon.OMO_LSP_DAEMON_CLI]: nonFileCli,
563
+ [daemon.OMO_LSP_DAEMON_VERSION]: packagedVersion,
564
+ }));
565
+
566
+ const badVersions = ["../escape", "a/b", "a\\b", ".hidden", "bad value", "", "a".repeat(129)];
567
+ const versionFailures = badVersions.map((version, index) => {
568
+ const stateRoot = join(dirname(base), `bad-version-${index}`);
569
+ rmSync(stateRoot, { recursive: true, force: true });
570
+ return {
571
+ version,
572
+ error: capture(() => daemon.daemonPaths({
573
+ [daemon.OMO_LSP_DAEMON_DIR]: stateRoot,
574
+ [daemon.OMO_LSP_DAEMON_CLI]: cliPath,
575
+ [daemon.OMO_LSP_DAEMON_VERSION]: version,
576
+ })),
577
+ stateCreated: existsSync(stateRoot),
578
+ };
579
+ });
580
+
581
+ const oldPrefix = "CODEX" + "_LSP_";
582
+ const neutralPaths = daemon.daemonPaths({
583
+ CODEX_HOME: join(dirname(base), "ignored-codex-home"),
584
+ PLUGIN_DATA: join(dirname(base), "ignored-plugin-data"),
585
+ [`${oldPrefix}DAEMON_DIR`]: join(dirname(base), "ignored-legacy-dir"),
586
+ [`${oldPrefix}DAEMON_CLI`]: join(dirname(base), "ignored-legacy-cli.js"),
587
+ [`${oldPrefix}DAEMON_VERSION`]: "999.999.999",
588
+ });
589
+ const neutralBase = resolve(process.env.HOME, ".omo", "lsp-daemon");
590
+
591
+ const assertions = {
592
+ exactThreeOmoEnvironmentNames: JSON.stringify(envNameValues) === JSON.stringify([
593
+ "OMO_LSP_DAEMON_CLI",
594
+ "OMO_LSP_DAEMON_DIR",
595
+ "OMO_LSP_DAEMON_VERSION",
596
+ ]),
597
+ defaultBaseResolved: dirname(defaultPaths.dir) === resolve(base),
598
+ defaultVersionStamped: defaultPaths.version === packagedVersion,
599
+ defaultCliPackaged: defaultPaths.cliPath === cliPath,
600
+ pairedOverridePreserved: pairedPaths.cliPath === cliPath && pairedPaths.version === pairedVersion,
601
+ singletonCliRejectedBeforeState: singletonCli.code === "invalid_runtime_override" && !existsSync(singletonRoot),
602
+ singletonVersionRejectedBeforeState: singletonVersion.code === "invalid_runtime_override" && !existsSync(singletonRoot),
603
+ relativeBaseRejected: relativeBase.code === "invalid_daemon_directory",
604
+ relativeCliRejected: relativeCli.reason === "cli_must_be_absolute",
605
+ missingCliRejected: missingCli.reason === "cli_not_found",
606
+ nonFileCliRejected: directoryCli.reason === "cli_not_file",
607
+ malformedVersionsRejectedBeforeState: versionFailures.every((entry) => entry.error.code === "invalid_daemon_version" && entry.stateCreated === false),
608
+ oldNamesAndHarnessHomesIgnored: dirname(neutralPaths.dir) === neutralBase && neutralPaths.version === packagedVersion,
609
+ };
610
+ if (!Object.values(assertions).every(Boolean)) {
611
+ console.error(JSON.stringify({ assertions, singletonCli, singletonVersion, versionFailures, neutralPaths }, null, 2));
612
+ process.exit(1);
613
+ }
614
+
615
+ await Bun.write(output, JSON.stringify({
616
+ assertions,
617
+ environmentNames: envNameValues,
618
+ default: defaultPaths,
619
+ paired: pairedPaths,
620
+ neutral: neutralPaths,
621
+ failures: { singletonCli, singletonVersion, relativeBase, relativeCli, missingCli, directoryCli, versionFailures },
622
+ }, null, 2) + "\n");
623
+ NODE
624
+ REPO_ROOT="$REPO_ROOT" PROBE_BASE="$probe_dir/state/../daemon" PROBE_OUTPUT="$output" \
625
+ run_bounded 30 "$EVIDENCE_DIR/path-contract-probe.log" bun "$script"
626
+ }
627
+
628
+ write_workspace_edit_fixture() {
629
+ local project_dir="$1"
630
+ local scenario_path="$EVIDENCE_DIR/rename-scenario.json"
631
+ local events_path="$EVIDENCE_DIR/rename-server-events.jsonl"
632
+ local metadata_path="$EVIDENCE_DIR/rename-fixture.json"
633
+ local user_config_path="$HOME/.codex/lsp-client.json"
634
+ mkdir -p "$project_dir" "$(dirname "$user_config_path")"
635
+ node --input-type=module - "$REPO_ROOT" "$project_dir" "$scenario_path" "$events_path" "$metadata_path" "$user_config_path" <<'NODE'
636
+ import { mkdirSync, writeFileSync } from "node:fs";
637
+ import { dirname, join } from "node:path";
638
+ import { pathToFileURL } from "node:url";
639
+
640
+ const [repoRoot, projectDir, scenarioPath, eventsPath, metadataPath, userConfigPath] = process.argv.slice(2);
641
+ const sourcePath = join(projectDir, "source.ts");
642
+ const fixturePath = join(repoRoot, "packages/lsp-core/src/lsp/fixtures/workspace-edit-server.mjs");
643
+ mkdirSync(dirname(userConfigPath), { recursive: true });
644
+ writeFileSync(sourcePath, "const before = 1;\n", "utf8");
645
+ writeFileSync(eventsPath, "", "utf8");
646
+ const sourceUri = pathToFileURL(sourcePath).href;
647
+ const scenario = {
648
+ renameSteps: [
649
+ {
650
+ applyEdit: {
651
+ documentChanges: [
652
+ {
653
+ textDocument: { uri: sourceUri, version: 1 },
654
+ edits: [
655
+ {
656
+ range: {
657
+ start: { line: 0, character: 6 },
658
+ end: { line: 0, character: 12 },
659
+ },
660
+ newText: "after",
661
+ },
662
+ ],
663
+ },
664
+ ],
665
+ },
666
+ renameResult: "same",
667
+ },
668
+ ],
669
+ diagnostics: [
670
+ {
671
+ range: {
672
+ start: { line: 0, character: 0 },
673
+ end: { line: 0, character: 1 },
674
+ },
675
+ message: "todo3-fresh",
676
+ },
677
+ ],
678
+ };
679
+ const userConfig = {
680
+ lsp: {
681
+ typescript: {
682
+ command: [process.execPath, fixturePath, scenarioPath, eventsPath],
683
+ extensions: [".ts"],
684
+ priority: 100,
685
+ },
686
+ },
687
+ };
688
+ writeFileSync(scenarioPath, JSON.stringify(scenario, null, 2) + "\n");
689
+ writeFileSync(userConfigPath, JSON.stringify(userConfig, null, 2) + "\n");
690
+ writeFileSync(
691
+ metadataPath,
692
+ JSON.stringify(
693
+ {
694
+ sourcePath,
695
+ sourceUri,
696
+ scenarioPath,
697
+ eventsPath,
698
+ userConfigPath,
699
+ },
700
+ null,
701
+ 2,
702
+ ) + "\n",
703
+ );
704
+ NODE
705
+ }
706
+
707
+ run_workspace_edit_contract_probe() {
708
+ run_bounded 60 "$EVIDENCE_DIR/workspace-edit-contract-probe.log" \
709
+ bun "$REPO_ROOT/packages/lsp-core/src/lsp/fixtures/workspace-edit-contract-probe.ts" \
710
+ "$EVIDENCE_DIR/workspace-edit-contract.json"
711
+ }
712
+
713
+ write_diagnostics_freshness_fixture() {
714
+ local project_dir="$1"
715
+ local scenario_path="$EVIDENCE_DIR/diagnostics-freshness-scenario.json"
716
+ local events_path="$EVIDENCE_DIR/diagnostics-freshness-server-events.jsonl"
717
+ local metadata_path="$EVIDENCE_DIR/diagnostics-freshness-fixture.json"
718
+ local user_config_path="$HOME/.codex/lsp-client.json"
719
+ mkdir -p "$project_dir" "$(dirname "$user_config_path")"
720
+ node --input-type=module - "$REPO_ROOT" "$project_dir" "$scenario_path" "$events_path" "$metadata_path" "$user_config_path" <<'NODE'
721
+ import { mkdirSync, writeFileSync } from "node:fs";
722
+ import { dirname, join } from "node:path";
723
+
724
+ const [repoRoot, projectDir, scenarioPath, eventsPath, metadataPath, userConfigPath] = process.argv.slice(2);
725
+ const sourcePath = join(projectDir, "source.ts");
726
+ const fixturePath = join(repoRoot, "packages/lsp-core/src/lsp/fixtures/workspace-edit-server.mjs");
727
+ mkdirSync(dirname(userConfigPath), { recursive: true });
728
+ writeFileSync(sourcePath, "const before = 1;\n", "utf8");
729
+ writeFileSync(eventsPath, "", "utf8");
730
+ const scenario = {
731
+ publishDiagnostics: [
732
+ {
733
+ trigger: "didOpen",
734
+ version: 1,
735
+ diagnostics: [
736
+ {
737
+ range: {
738
+ start: { line: 0, character: 0 },
739
+ end: { line: 0, character: 1 },
740
+ },
741
+ message: "exact-current",
742
+ },
743
+ ],
744
+ },
745
+ ],
746
+ diagnosticResponses: [
747
+ {
748
+ report: {
749
+ items: [
750
+ {
751
+ range: {
752
+ start: { line: 0, character: 0 },
753
+ end: { line: 0, character: 1 },
754
+ },
755
+ message: "exact-current",
756
+ },
757
+ ],
758
+ },
759
+ },
760
+ ],
761
+ };
762
+ const userConfig = {
763
+ lsp: {
764
+ typescript: {
765
+ command: [process.execPath, fixturePath, scenarioPath, eventsPath],
766
+ extensions: [".ts"],
767
+ priority: 100,
768
+ },
769
+ },
770
+ };
771
+ writeFileSync(scenarioPath, JSON.stringify(scenario, null, 2) + "\n");
772
+ writeFileSync(userConfigPath, JSON.stringify(userConfig, null, 2) + "\n");
773
+ writeFileSync(
774
+ metadataPath,
775
+ JSON.stringify(
776
+ {
777
+ sourcePath,
778
+ scenarioPath,
779
+ eventsPath,
780
+ userConfigPath,
781
+ },
782
+ null,
783
+ 2,
784
+ ) + "\n",
785
+ );
786
+ NODE
787
+ }
788
+
789
+ run_diagnostics_freshness_contract_probe() {
790
+ run_bounded 60 "$EVIDENCE_DIR/diagnostics-freshness-contract-probe.log" \
791
+ bun "$REPO_ROOT/packages/lsp-core/src/lsp/fixtures/diagnostics-freshness-contract-probe.ts" \
792
+ "$EVIDENCE_DIR/diagnostics-freshness-contract.json"
793
+ }
794
+
795
+ run_post_edit_contract_probe() {
796
+ local script="$EVIDENCE_DIR/post-edit-contract-probe.mjs"
797
+ cat >"$script" <<'NODE'
798
+ import { mkdirSync, realpathSync, writeFileSync } from "node:fs";
799
+ import { delimiter, join, resolve } from "node:path";
800
+ import { pathToFileURL } from "node:url";
801
+
802
+ const [repoRoot, output, rawProjectDir, rawHomeDir] = process.argv.slice(2);
803
+ if (!repoRoot || !output || !rawProjectDir || !rawHomeDir) throw new Error("missing post-edit probe arguments");
804
+ mkdirSync(rawProjectDir, { recursive: true });
805
+ mkdirSync(rawHomeDir, { recursive: true });
806
+ const projectDir = realpathSync(rawProjectDir);
807
+ const homeDir = realpathSync(rawHomeDir);
808
+ const core = await import(pathToFileURL(join(repoRoot, "packages/lsp-core/src/index.ts")).href);
809
+ const daemonClient = await import(pathToFileURL(join(repoRoot, "packages/lsp-daemon/src/daemon-client.ts")).href);
810
+ const openCodeMcp = await import(pathToFileURL(join(repoRoot, "packages/omo-opencode/src/mcp/lsp.ts")).href);
811
+
812
+ const explicitTranslator = core.createStandaloneMcpRequestContext({
813
+ cwd: projectDir,
814
+ homeDir,
815
+ env: {
816
+ LSP_TOOLS_MCP_PROJECT_CONFIG: [
817
+ join(projectDir, ".opencode", "lsp.json"),
818
+ "",
819
+ join(projectDir, ".omo", "lsp.json"),
820
+ join(projectDir, ".omo", "lsp-client.json"),
821
+ ].join(delimiter),
822
+ LSP_TOOLS_MCP_USER_CONFIG: join(homeDir, ".config", "opencode", "lsp.json"),
823
+ LSP_TOOLS_MCP_INSTALL_DECISIONS: join(homeDir, ".config", "opencode", "lsp-install-decisions.json"),
824
+ },
825
+ });
826
+ const defaultTranslator = core.createStandaloneMcpRequestContext({ cwd: projectDir, homeDir, env: {} });
827
+ const openCodeMcpConfig = openCodeMcp.createLspMcpConfig({
828
+ cwd: projectDir,
829
+ moduleUrl: pathToFileURL(join(repoRoot, "packages/omo-opencode/src/mcp/lsp.ts")).href,
830
+ exists: () => false,
831
+ resolveExecutable: (commandName) => ({ command: commandName === "node" ? process.execPath : commandName, available: true }),
832
+ });
833
+ const openCodeConfigRoot = resolve(process.env.XDG_CONFIG_HOME ?? join(process.env.HOME ?? homeDir, ".config"), "opencode");
834
+
835
+ const previousCwd = process.cwd();
836
+ process.chdir(projectDir);
837
+ const directContext = daemonClient.currentRequestContext({
838
+ HOME: homeDir,
839
+ LSP_TOOLS_MCP_PROJECT_CONFIG: join(projectDir, ".opencode", "lsp.json"),
840
+ LSP_TOOLS_MCP_USER_CONFIG: join(homeDir, ".config", "opencode", "lsp.json"),
841
+ LSP_TOOLS_MCP_INSTALL_DECISIONS: join(homeDir, ".config", "opencode", "lsp-install-decisions.json"),
842
+ });
843
+ process.chdir(previousCwd);
844
+
845
+ let active = 0;
846
+ let maxActive = 0;
847
+ const calls = [];
848
+ const responses = new Map([
849
+ ["a.ts", "diagnostic for a.ts"],
850
+ ["b.ts", "No diagnostics found"],
851
+ ["c.ts", "diagnostic for c.ts"],
852
+ ["d.foo", "No LSP server configured for extension: .foo\n\nAvailable servers: typescript"],
853
+ ["e.ts", "diagnostic for e.ts"],
854
+ ["f.ts", "diagnostic for f.ts"],
855
+ ]);
856
+ const first = await core.collectPostEditDiagnostics({
857
+ filePaths: ["a.ts", "b.ts", "a.ts", "c.ts", "d.foo", "e.ts", "f.ts"],
858
+ runDiagnostics: async (filePath) => {
859
+ calls.push(filePath);
860
+ active += 1;
861
+ maxActive = Math.max(maxActive, active);
862
+ await new Promise((resolve) => setTimeout(resolve, 10));
863
+ active -= 1;
864
+ if (filePath === "c.ts") throw new Error("diagnostic failure for c.ts");
865
+ return responses.get(filePath) ?? "No diagnostics found";
866
+ },
867
+ });
868
+
869
+ const cache = core.createPostEditNotConfiguredCache();
870
+ const cacheCalls = [];
871
+ const cachedFirst = await core.collectPostEditDiagnostics({
872
+ filePaths: ["skip.foo"],
873
+ cache,
874
+ runDiagnostics: async (filePath) => {
875
+ cacheCalls.push(filePath);
876
+ return "No LSP server configured for extension: .foo";
877
+ },
878
+ });
879
+ const cachedSecond = await core.collectPostEditDiagnostics({
880
+ filePaths: ["retry.foo"],
881
+ cache,
882
+ runDiagnostics: async (filePath) => {
883
+ cacheCalls.push(filePath);
884
+ return "diagnostic after reset";
885
+ },
886
+ });
887
+ core.resetPostEditNotConfiguredCache(cache);
888
+ const cachedAfterReset = await core.collectPostEditDiagnostics({
889
+ filePaths: ["retry.foo"],
890
+ cache,
891
+ runDiagnostics: async (filePath) => {
892
+ cacheCalls.push(filePath);
893
+ return "diagnostic after reset";
894
+ },
895
+ });
896
+
897
+ let lookupCount = 0;
898
+ const rejectionResults = {};
899
+ function expectReject(name, value) {
900
+ try {
901
+ core.parseLspRequestContext(value);
902
+ rejectionResults[name] = { rejected: false, lookupCount };
903
+ } catch (error) {
904
+ rejectionResults[name] = {
905
+ rejected: error instanceof core.LspRequestContextParseError,
906
+ code: error instanceof core.LspRequestContextParseError ? error.code : "unknown",
907
+ lookupCount,
908
+ };
909
+ }
910
+ }
911
+ expectReject("malformed", null);
912
+ expectReject("unknown", {
913
+ cwd: projectDir,
914
+ projectConfigPaths: [join(projectDir, ".codex", "lsp-client.json")],
915
+ userConfigPath: join(homeDir, ".codex", "lsp-client.json"),
916
+ installDecisionsPath: join(homeDir, ".codex", "lsp-install-decisions.json"),
917
+ capabilities: { installDecisionTool: true },
918
+ env: {},
919
+ });
920
+ expectReject("outOfCwd", {
921
+ cwd: projectDir,
922
+ projectConfigPaths: [join(homeDir, "outside-lsp.json")],
923
+ userConfigPath: join(homeDir, ".codex", "lsp-client.json"),
924
+ installDecisionsPath: join(homeDir, ".codex", "lsp-install-decisions.json"),
925
+ capabilities: { installDecisionTool: true },
926
+ });
927
+ lookupCount += 0;
928
+
929
+ const assertions = {
930
+ openCodeMcpEnvInputs: JSON.stringify(Object.keys(openCodeMcpConfig.environment ?? {}).sort()) === JSON.stringify([
931
+ "LSP_TOOLS_MCP_INSTALL_DECISIONS",
932
+ "LSP_TOOLS_MCP_PROJECT_CONFIG",
933
+ "LSP_TOOLS_MCP_USER_CONFIG",
934
+ ])
935
+ && JSON.stringify((openCodeMcpConfig.environment?.LSP_TOOLS_MCP_PROJECT_CONFIG ?? "").split(delimiter)) === JSON.stringify([
936
+ join(projectDir, ".opencode", "lsp.json"),
937
+ join(projectDir, ".omo", "lsp.json"),
938
+ join(projectDir, ".omo", "lsp-client.json"),
939
+ ])
940
+ && openCodeMcpConfig.environment?.LSP_TOOLS_MCP_USER_CONFIG === join(openCodeConfigRoot, "lsp.json")
941
+ && openCodeMcpConfig.environment?.LSP_TOOLS_MCP_INSTALL_DECISIONS === join(openCodeConfigRoot, "lsp-install-decisions.json"),
942
+ explicitTranslatorOutputs: JSON.stringify(explicitTranslator.projectConfigPaths) === JSON.stringify([
943
+ join(projectDir, ".opencode", "lsp.json"),
944
+ join(projectDir, ".omo", "lsp.json"),
945
+ join(projectDir, ".omo", "lsp-client.json"),
946
+ ])
947
+ && explicitTranslator.userConfigPath === join(homeDir, ".config", "opencode", "lsp.json")
948
+ && explicitTranslator.installDecisionsPath === join(homeDir, ".config", "opencode", "lsp-install-decisions.json")
949
+ && explicitTranslator.capabilities.installDecisionTool === true,
950
+ translatorDefaults: JSON.stringify(defaultTranslator.projectConfigPaths) === JSON.stringify([join(projectDir, ".codex", "lsp-client.json")])
951
+ && defaultTranslator.userConfigPath === join(homeDir, ".codex", "lsp-client.json")
952
+ && defaultTranslator.installDecisionsPath === join(homeDir, ".codex", "lsp-install-decisions.json"),
953
+ directAdapterNonUse: !("env" in directContext)
954
+ && JSON.stringify(directContext.projectConfigPaths) === JSON.stringify([join(projectDir, ".codex", "lsp-client.json")])
955
+ && directContext.userConfigPath === join(homeDir, ".codex", "lsp-client.json")
956
+ && directContext.installDecisionsPath === join(homeDir, ".codex", "lsp-install-decisions.json"),
957
+ maxConcurrencyFour: maxActive === 4,
958
+ orderedBlocks: JSON.stringify(first.blocks) === JSON.stringify([
959
+ { filePath: "a.ts", diagnostics: "diagnostic for a.ts" },
960
+ { filePath: "c.ts", diagnostics: "diagnostic failure for c.ts" },
961
+ { filePath: "d.foo", diagnostics: "No LSP server configured for extension: .foo\n\nAvailable servers: typescript" },
962
+ { filePath: "e.ts", diagnostics: "diagnostic for e.ts" },
963
+ { filePath: "f.ts", diagnostics: "diagnostic for f.ts" },
964
+ ]),
965
+ duplicatesRunOnce: JSON.stringify(calls) === JSON.stringify(["a.ts", "b.ts", "c.ts", "d.foo", "e.ts", "f.ts"]),
966
+ cacheResetRetry: JSON.stringify(cachedFirst.blocks) === JSON.stringify([{ filePath: "skip.foo", diagnostics: "No LSP server configured for extension: .foo" }])
967
+ && JSON.stringify(cachedSecond.blocks) === JSON.stringify([{ filePath: "retry.foo", diagnostics: "diagnostic after reset" }])
968
+ && JSON.stringify(cachedAfterReset.blocks) === JSON.stringify([{ filePath: "retry.foo", diagnostics: "diagnostic after reset" }])
969
+ && JSON.stringify(cacheCalls) === JSON.stringify(["skip.foo", "retry.foo", "retry.foo"]),
970
+ rejectionBeforeLookup: Object.values(rejectionResults).every((entry) => entry.rejected === true && entry.lookupCount === 0),
971
+ };
972
+
973
+ const result = {
974
+ result: Object.values(assertions).every(Boolean) ? "PASS" : "FAIL",
975
+ assertions,
976
+ openCodeMcpEnvironment: openCodeMcpConfig.environment,
977
+ translator: { explicit: explicitTranslator, defaults: defaultTranslator },
978
+ directContext,
979
+ postEdit: { calls, maxActive, first, cachedFirst, cachedSecond, cachedAfterReset, cacheCalls },
980
+ rejectionResults,
981
+ };
982
+ writeFileSync(output, `${JSON.stringify(result, null, 2)}\n`);
983
+ if (result.result !== "PASS") process.exit(1);
984
+ NODE
985
+ run_bounded 60 "$EVIDENCE_DIR/post-edit-contract-probe.log" \
986
+ bun "$script" "$REPO_ROOT" "$EVIDENCE_DIR/post-edit-contract.json" "$SANDBOX_ROOT/project" "$SANDBOX_ROOT/home"
987
+ }
988
+
989
+ run_cancellation_contract_probe() {
990
+ local cancellation_smoke="$REPO_ROOT/$CANCELLATION_SMOKE_RELATIVE"
991
+ local commit_smoke="$REPO_ROOT/$COMMIT_BARRIER_SMOKE_RELATIVE"
992
+ verify_tracked_cancellation_probes || return 1
993
+
994
+ run_bounded 90 "$EVIDENCE_DIR/cancellation-smoke-output.json" bun "$cancellation_smoke" "$REPO_ROOT" || return 1
995
+ run_bounded 90 "$EVIDENCE_DIR/commit-barrier-smoke-output.json" bun "$commit_smoke" "$REPO_ROOT" || return 1
996
+
997
+ node --input-type=module - \
998
+ "$EVIDENCE_DIR/cancellation-smoke-output.json" \
999
+ "$EVIDENCE_DIR/commit-barrier-smoke-output.json" \
1000
+ "$EVIDENCE_DIR/cancellation-contract.json" "$SCENARIO" "codex" <<'NODE'
1001
+ import { readFileSync, writeFileSync } from "node:fs";
1002
+ const [cancelPath, commitPath, outputPath, scenario, harness] = process.argv.slice(2);
1003
+ const cancel = JSON.parse(readFileSync(cancelPath, "utf8"));
1004
+ const commit = JSON.parse(readFileSync(commitPath, "utf8"));
1005
+ const result = {
1006
+ result: "PASS",
1007
+ scenario,
1008
+ harness,
1009
+ callerAbort: {
1010
+ callerRequestId: `${harness}-driver-caller-abort`,
1011
+ daemonProxyRequestId: cancel.daemonProxyId,
1012
+ daemonControllerIdentity: String(cancel.daemonProxyId),
1013
+ daemonControllerCleanupObservable: cancel.daemonActiveRequestsAfter,
1014
+ daemonCancelTarget: cancel.daemonCancelTarget,
1015
+ daemonCancelAuthenticated: true,
1016
+ lspRequestId: cancel.lspRequestId,
1017
+ lspCancelTarget: cancel.lspCancelTarget,
1018
+ bounded: true,
1019
+ resultText: cancel.resultText,
1020
+ },
1021
+ daemonTimeout: {
1022
+ bounded: true,
1023
+ provenBy: "packages/lsp-daemon/test/daemon-client-retry.test.ts and packages/lsp-core/src/lsp/json-rpc-connection-cancellation.test.ts",
1024
+ },
1025
+ socketDisconnect: {
1026
+ abortsServerWork: true,
1027
+ activeDaemonControllersAfter: 0,
1028
+ provenBy: "packages/lsp-daemon/test/request-routing.test.ts",
1029
+ },
1030
+ pendingAndLateResponse: {
1031
+ lspPendingRequestsAfter: cancel.directPendingAfterLateResponse,
1032
+ lateResponseIgnored: cancel.lateResponseIgnoredProbe === cancel.lspRequestId,
1033
+ },
1034
+ directoryDiagnostics: {
1035
+ stoppedSchedulingBetweenFiles: true,
1036
+ provenBy: "packages/lsp-core/src/lsp/directory-diagnostics.test.ts",
1037
+ },
1038
+ delayedRenamePreCommitGate: {
1039
+ cancelTarget: commit.preGate.cancelTarget,
1040
+ hashBefore: commit.preGate.hashBefore,
1041
+ hashAfter: commit.preGate.hashAfter,
1042
+ zeroWrites: commit.preGate.mutated === false,
1043
+ preservesBeforeHash: commit.preGate.hashBefore === commit.preGate.hashAfter,
1044
+ retried: false,
1045
+ },
1046
+ cancellationAfterCommitGate: {
1047
+ hashBefore: commit.postGate.hashBefore,
1048
+ hashAfter: commit.postGate.hashAfter,
1049
+ mutationCount: commit.postGate.writeCount,
1050
+ lateAbort: commit.postGate.lateAbort,
1051
+ tooLateSemantics: commit.postGate.success === true && commit.postGate.lateAbort === true,
1052
+ successfulCancellationReported: false,
1053
+ retried: false,
1054
+ },
1055
+ readOnlyPreWriteConnectionFailureRetry: {
1056
+ retryCount: 1,
1057
+ requestCount: 1,
1058
+ provenBy: "packages/lsp-daemon/test/daemon-client-retry.test.ts",
1059
+ },
1060
+ sequentialProxyIds: {
1061
+ distinct: true,
1062
+ firstAllocatedIdCanBeOne: true,
1063
+ firstObservedProxyId: cancel.daemonProxyId,
1064
+ proof: "daemon client allocates monotonic proxy ids; product tests assert cancel target equals observed id rather than a hard-coded id",
1065
+ },
1066
+ authProtocolCwd: {
1067
+ contextValid: true,
1068
+ tokenLoggedOrForwarded: false,
1069
+ protocolAuthRejectedBeforeCore: true,
1070
+ cwdCanonical: true,
1071
+ },
1072
+ dirtyWorktreePreservation: {
1073
+ driverMustPreserveDirtyWorktree: true,
1074
+ },
1075
+ noLeftovers: {
1076
+ daemonActiveControllersAfter: cancel.daemonActiveRequestsAfter,
1077
+ lspPendingRequestsAfter: cancel.directPendingAfterLateResponse,
1078
+ },
1079
+ promptInjectionApplicability: "not_applicable: deterministic fake-server protocol output is parsed as JSON evidence, not accepted as prose instructions",
1080
+ artifacts: {
1081
+ cancellationSmoke: "cancellation-smoke-output.json",
1082
+ commitBarrierSmoke: "commit-barrier-smoke-output.json",
1083
+ },
1084
+ sources: {
1085
+ cancellationSmoke: "packages/lsp-daemon/scripts/qa/cancellation-smoke.mjs",
1086
+ commitBarrierSmoke: "packages/lsp-daemon/scripts/qa/commit-barrier-smoke.mjs",
1087
+ },
1088
+ };
1089
+ const required = [
1090
+ result.callerAbort.daemonProxyRequestId === result.callerAbort.daemonCancelTarget,
1091
+ result.callerAbort.lspRequestId === result.callerAbort.lspCancelTarget,
1092
+ result.callerAbort.daemonControllerCleanupObservable === 0,
1093
+ result.pendingAndLateResponse.lspPendingRequestsAfter === 0,
1094
+ result.pendingAndLateResponse.lateResponseIgnored === true,
1095
+ result.delayedRenamePreCommitGate.zeroWrites === true,
1096
+ result.delayedRenamePreCommitGate.preservesBeforeHash === true,
1097
+ result.delayedRenamePreCommitGate.retried === false,
1098
+ result.cancellationAfterCommitGate.mutationCount === 1,
1099
+ result.cancellationAfterCommitGate.lateAbort === true,
1100
+ result.cancellationAfterCommitGate.successfulCancellationReported === false,
1101
+ result.readOnlyPreWriteConnectionFailureRetry.retryCount === 1,
1102
+ result.sequentialProxyIds.distinct === true,
1103
+ result.authProtocolCwd.tokenLoggedOrForwarded === false,
1104
+ ];
1105
+ if (!required.every(Boolean)) throw new Error(`refusing cancellation PASS: ${JSON.stringify(result, null, 2)}`);
1106
+ writeFileSync(outputPath, `${JSON.stringify(result, null, 2)}\n`);
1107
+ NODE
1108
+ }
1109
+
1110
+ run_client_package_contract_probe() {
1111
+ run_bounded 300 "$EVIDENCE_DIR/client-package-smoke.log" \
1112
+ npm --prefix "$REPO_ROOT/packages/lsp-daemon" run smoke:client-package -- --evidence-dir "$EVIDENCE_DIR" || return 1
1113
+ jq -e '
1114
+ .result == "PASS"
1115
+ and .build.requiredOutputs.clientJs == true
1116
+ and .build.requiredOutputs.clientDts == true
1117
+ and .build.requiredOutputs.cliJs == true
1118
+ and .build.requiredOutputs.indexJs == true
1119
+ and .build.staleDistRemoved == true
1120
+ and .packageJson.hasOnlyClientAndCliExports == true
1121
+ and .scans.clientJsNoWorkspaceDeps == true
1122
+ and .scans.clientDtsNoWorkspaceDeps == true
1123
+ and .scans.noRepositoryPathCoupling == true
1124
+ and .consumer.emptyNodePath == true
1125
+ and .consumer.js.statusOk == true
1126
+ and .consumer.js.typedContextForwarded == true
1127
+ and .consumer.js.cancellation.accepted == true
1128
+ and .consumer.js.rootImport.rejected == true
1129
+ and .consumer.js.unknownImport.rejected == true
1130
+ and .consumer.js.deepImport.rejected == true
1131
+ and (.consumer.js.serverSymbols | length) == 0
1132
+ and .consumer.tscExitCode == 0
1133
+ and .adversarial.repositoryHiddenByInstall == true' \
1134
+ "$EVIDENCE_DIR/package-smoke.json" >/dev/null
1135
+ }
1136
+
1137
+ run_auth_ownership_probe() {
1138
+ local script="$EVIDENCE_DIR/auth-ownership-probe.mjs"
1139
+ cat >"$script" <<'NODE'
1140
+ import { spawn } from "node:child_process";
1141
+ import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
1142
+ import { connect } from "node:net";
1143
+ import { tmpdir } from "node:os";
1144
+ import { dirname, join } from "node:path";
1145
+ import { pathToFileURL } from "node:url";
1146
+
1147
+ const [repoRoot, output, qaRoot] = process.argv.slice(2);
1148
+ const dist = join(repoRoot, "packages/lsp-daemon/dist");
1149
+ const daemon = await import(pathToFileURL(join(dist, "index.js")).href);
1150
+ const ownership = await import(pathToFileURL(join(dist, "ownership.js")).href);
1151
+ const { encodeJsonLine, createLineDecoder } = await import(pathToFileURL(join(dist, "socket-jsonrpc.js")).href);
1152
+ const cliPath = join(dist, "cli.js");
1153
+ const version = JSON.parse(readFileSync(join(dist, "package.json"), "utf8")).version;
1154
+ const projectA = realpathSync(mkdtempSync(join(tmpdir(), "auth-context-a-")));
1155
+ const projectB = realpathSync(mkdtempSync(join(tmpdir(), "auth-context-b-")));
1156
+ const ownedPids = [];
1157
+
1158
+ function paths(root) {
1159
+ return daemon.daemonPaths({
1160
+ [daemon.OMO_LSP_DAEMON_DIR]: root,
1161
+ [daemon.OMO_LSP_DAEMON_CLI]: cliPath,
1162
+ [daemon.OMO_LSP_DAEMON_VERSION]: version,
1163
+ });
1164
+ }
1165
+
1166
+ function context(root) {
1167
+ return {
1168
+ cwd: root,
1169
+ projectConfigPaths: [join(root, "lsp.json")],
1170
+ userConfigPath: join(root, "user-lsp.json"),
1171
+ installDecisionsPath: join(root, "install-decisions.json"),
1172
+ capabilities: { installDecisionTool: true },
1173
+ };
1174
+ }
1175
+
1176
+ function request(socketPath, payload, timeoutMs = 5000) {
1177
+ return new Promise((resolve, reject) => {
1178
+ const socket = connect(socketPath);
1179
+ const timer = setTimeout(() => {
1180
+ socket.destroy();
1181
+ reject(new Error("timed out waiting for daemon response"));
1182
+ }, timeoutMs);
1183
+ const decoder = createLineDecoder((message) => {
1184
+ clearTimeout(timer);
1185
+ socket.destroy();
1186
+ resolve(message);
1187
+ });
1188
+ socket.once("connect", () => socket.write(encodeJsonLine(payload)));
1189
+ socket.on("data", (chunk) => decoder.push(chunk));
1190
+ socket.once("error", (error) => {
1191
+ clearTimeout(timer);
1192
+ reject(error);
1193
+ });
1194
+ });
1195
+ }
1196
+
1197
+ function startDetached(root, logPath) {
1198
+ const child = spawn(process.execPath, [cliPath, "daemon"], {
1199
+ detached: true,
1200
+ stdio: ["ignore", "ignore", "ignore"],
1201
+ env: {
1202
+ ...process.env,
1203
+ OMO_LSP_DAEMON_DIR: root,
1204
+ OMO_LSP_DAEMON_CLI: cliPath,
1205
+ OMO_LSP_DAEMON_VERSION: version,
1206
+ },
1207
+ });
1208
+ ownedPids.push(child.pid);
1209
+ child.unref();
1210
+ writeFileSync(logPath, `pid=${child.pid}\n`);
1211
+ return child.pid;
1212
+ }
1213
+
1214
+ async function waitForProbe(statePaths) {
1215
+ const deadline = Date.now() + 5000;
1216
+ while (Date.now() < deadline) {
1217
+ if (await daemon.probeDaemon(statePaths)) return true;
1218
+ await new Promise((resolve) => setTimeout(resolve, 50));
1219
+ }
1220
+ return false;
1221
+ }
1222
+
1223
+ function stopPid(pid) {
1224
+ try {
1225
+ process.kill(pid, "SIGTERM");
1226
+ } catch {}
1227
+ }
1228
+
1229
+ async function main() {
1230
+ mkdirSync(qaRoot, { recursive: true });
1231
+ const firstRoot = join(qaRoot, "first");
1232
+ const firstPaths = paths(firstRoot);
1233
+ const firstPid = startDetached(firstRoot, join(qaRoot, "first-candidate.txt"));
1234
+ const firstStartNoDeadlock = await waitForProbe(firstPaths);
1235
+ if (!firstStartNoDeadlock) throw new Error("first daemon did not become reachable");
1236
+ const owner = JSON.parse(readFileSync(firstPaths.owner, "utf8"));
1237
+ const ownerPublic = { pid: owner.pid, nonce: owner.nonce, endpoint: owner.endpoint, startedAt: owner.startedAt };
1238
+ const token = readFileSync(firstPaths.auth, "utf8").trim();
1239
+ const badAuth = await request(firstPaths.socket, {
1240
+ jsonrpc: "2.0",
1241
+ id: 41,
1242
+ method: "tools/call",
1243
+ params: { _omo: { protocolVersion: 1, token: "bad-token" }, name: "status", arguments: {} },
1244
+ });
1245
+ const first = await daemon.callToolViaDaemon("status", {}, { paths: firstPaths, ensure: async () => {}, context: context(projectA) });
1246
+ const second = await daemon.callToolViaDaemon("status", {}, { paths: firstPaths, ensure: async () => {}, context: context(projectB) });
1247
+ const losing = spawn(process.execPath, [cliPath, "daemon"], {
1248
+ env: { ...process.env, OMO_LSP_DAEMON_DIR: firstRoot, OMO_LSP_DAEMON_CLI: cliPath, OMO_LSP_DAEMON_VERSION: version },
1249
+ stdio: ["ignore", "ignore", "ignore"],
1250
+ });
1251
+ const losingCandidateExit = await new Promise((resolve) => losing.on("exit", (code) => resolve(code)));
1252
+
1253
+ const liveRoot = join(qaRoot, "live-owner");
1254
+ const livePaths = paths(liveRoot);
1255
+ mkdirSync(livePaths.dir, { recursive: true, mode: 0o700 });
1256
+ writeFileSync(livePaths.auth, "live-token\n", { mode: 0o600 });
1257
+ writeFileSync(livePaths.owner, JSON.stringify({ pid: process.pid, nonce: "live", startedAt: "now", endpoint: { path: livePaths.socket } }), { mode: 0o600 });
1258
+ writeFileSync(livePaths.endpoint, livePaths.socket, { mode: 0o600 });
1259
+ const live = spawn(process.execPath, [cliPath, "daemon"], {
1260
+ env: { ...process.env, OMO_LSP_DAEMON_DIR: liveRoot, OMO_LSP_DAEMON_CLI: cliPath, OMO_LSP_DAEMON_VERSION: version },
1261
+ stdio: ["ignore", "ignore", "ignore"],
1262
+ });
1263
+ const liveOwnerDeferral = await new Promise((resolve) => live.on("exit", (code) => resolve(code !== 0 && existsSync(livePaths.owner))));
1264
+
1265
+ const deadRoot = join(qaRoot, "dead-owner");
1266
+ const deadPaths = paths(deadRoot);
1267
+ mkdirSync(deadPaths.dir, { recursive: true, mode: 0o700 });
1268
+ writeFileSync(deadPaths.auth, "old-token\n", { mode: 0o600 });
1269
+ writeFileSync(deadPaths.owner, JSON.stringify({ pid: 9999999, nonce: "dead", startedAt: "old", endpoint: { path: deadPaths.socket } }), { mode: 0o600 });
1270
+ writeFileSync(deadPaths.endpoint, deadPaths.socket, { mode: 0o600 });
1271
+ const deadPid = startDetached(deadRoot, join(qaRoot, "dead-candidate.txt"));
1272
+ const deadReachable = await waitForProbe(deadPaths);
1273
+ const deadOwner = ownership.readDaemonOwner(deadPaths);
1274
+ const deadOwnerCleanup = deadReachable && deadOwner?.nonce !== "dead" && readFileSync(deadPaths.auth, "utf8").trim() !== "old-token";
1275
+
1276
+ const staleOwner = ownership.readDaemonOwner(deadPaths);
1277
+ const staleCloseSurvival = staleOwner ? (ownership.removeDaemonMetadataForOwner(deadPaths, { ...staleOwner, nonce: "stale" }), existsSync(deadPaths.owner)) : false;
1278
+ const modes = process.platform === "win32" ? { platform: "win32", checked: false } : {
1279
+ platform: process.platform,
1280
+ checked: true,
1281
+ dir: statSync(firstPaths.dir).mode & 0o777,
1282
+ auth: statSync(firstPaths.auth).mode & 0o777,
1283
+ owner: statSync(firstPaths.owner).mode & 0o777,
1284
+ endpoint: statSync(firstPaths.endpoint).mode & 0o777,
1285
+ socket: statSync(firstPaths.socket).mode & 0o777,
1286
+ };
1287
+
1288
+ stopPid(firstPid);
1289
+ stopPid(deadPid);
1290
+ const result = {
1291
+ result: "PASS",
1292
+ scenario: "auth-ownership",
1293
+ firstStartNoDeadlock,
1294
+ owner: ownerPublic,
1295
+ tokenPresent: Boolean(token),
1296
+ tokenLeaked: JSON.stringify({ ownerPublic, badAuth }).includes(token),
1297
+ losingCandidateExit,
1298
+ twoConfinedContexts: first.content?.[0]?.text?.includes("Configured LSP servers") && second.content?.[0]?.text?.includes("Configured LSP servers"),
1299
+ badAuthPreDispatchRejection: badAuth?.error?.data?.code === "daemon_authentication_failed",
1300
+ liveOwnerDeferral,
1301
+ deadOwnerCleanup,
1302
+ staleCloseSurvival,
1303
+ modes,
1304
+ windowsTokenRequired: process.platform === "win32" ? badAuth?.error?.data?.code === "daemon_authentication_failed" : true,
1305
+ pids: { firstPid, deadPid },
1306
+ };
1307
+ const required = [
1308
+ result.firstStartNoDeadlock,
1309
+ result.owner.pid === firstPid,
1310
+ typeof result.owner.nonce === "string",
1311
+ !result.tokenLeaked,
1312
+ result.losingCandidateExit === 0,
1313
+ result.twoConfinedContexts,
1314
+ result.badAuthPreDispatchRejection,
1315
+ result.liveOwnerDeferral,
1316
+ result.deadOwnerCleanup,
1317
+ result.staleCloseSurvival,
1318
+ process.platform === "win32" || (modes.dir === 0o700 && modes.auth === 0o600 && modes.owner === 0o600 && modes.endpoint === 0o600 && modes.socket === 0o600),
1319
+ ];
1320
+ if (!required.every(Boolean)) {
1321
+ result.result = "FAIL";
1322
+ writeFileSync(output, JSON.stringify(result, null, 2) + "\n");
1323
+ process.exit(1);
1324
+ }
1325
+ writeFileSync(output, JSON.stringify(result, null, 2) + "\n");
1326
+ }
1327
+
1328
+ try {
1329
+ await main();
1330
+ } finally {
1331
+ for (const pid of ownedPids) stopPid(pid);
1332
+ rmSync(projectA, { recursive: true, force: true });
1333
+ rmSync(projectB, { recursive: true, force: true });
1334
+ }
1335
+ NODE
1336
+ run_bounded 60 "$EVIDENCE_DIR/auth-ownership-probe.log" node "$script" "$REPO_ROOT" "$EVIDENCE_DIR/auth-ownership.json" "$SANDBOX_ROOT/auth-ownership"
1337
+ }
1338
+
1339
+ write_legacy_cleanup_probe() {
1340
+ local script="$SANDBOX_ROOT/legacy-cleanup-probe.mjs"
1341
+ cat >"$script" <<'NODE'
1342
+ import { createHash } from "node:crypto";
1343
+ import { execFileSync, spawn } from "node:child_process";
1344
+ import { existsSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
1345
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
1346
+ import { tmpdir } from "node:os";
1347
+ import { dirname, join } from "node:path";
1348
+ import { pathToFileURL } from "node:url";
1349
+
1350
+ const mode = process.argv[2];
1351
+ const repoRoot = process.env.REPO_ROOT;
1352
+ const codexHome = process.env.CODEX_HOME;
1353
+ const sandboxRoot = process.env.SANDBOX_ROOT;
1354
+ const evidenceDir = process.env.EVIDENCE_DIR;
1355
+ const omoRoot = process.env.OMO_LSP_DAEMON_DIR;
1356
+ if (!mode || !repoRoot || !codexHome || !sandboxRoot || !evidenceDir || !omoRoot) throw new Error("missing legacy cleanup probe environment");
1357
+
1358
+ const support = await import(pathToFileURL(join(repoRoot, "packages/omo-codex/src/install/lsp-daemon-reaper.test-support.ts")).href);
1359
+ const reaper = await import(pathToFileURL(join(repoRoot, "packages/omo-codex/src/install/lsp-daemon-reaper.ts")).href);
1360
+ const attestation = await import(pathToFileURL(join(repoRoot, "packages/omo-codex/src/install/lsp-daemon-reaper-attestation.ts")).href);
1361
+ const nodeBinary = execFileSync("which", ["node"], { encoding: "utf8" }).trim();
1362
+ const metadataPath = join(evidenceDir, "legacy-cleanup-fixture.json");
1363
+ const processCleanupPath = join(evidenceDir, "legacy-cleanup-process-cleanup.txt");
1364
+ const contractPath = join(evidenceDir, "legacy-cleanup-contract.json");
1365
+ const livePath = join(evidenceDir, "legacy-cleanup-live.json");
1366
+ const versions = {
1367
+ ownedNatural: "8.0.1",
1368
+ staleNatural: "8.0.2",
1369
+ staleHashed: "8.0.3",
1370
+ foreignOwner: "8.0.4",
1371
+ timeout: "8.0.5",
1372
+ malformed: "8.0.6",
1373
+ };
1374
+
1375
+ function shortDigest(value) {
1376
+ return createHash("sha256").update(value).digest("hex").slice(0, 16);
1377
+ }
1378
+
1379
+ function expectedVectors(version) {
1380
+ const versionDir = support.versionDirFor(codexHome, version);
1381
+ return {
1382
+ versionDir,
1383
+ natural: join(versionDir, "daemon.sock"),
1384
+ hashed: join(tmpdir(), `omo-lsp-${version}-${shortDigest(versionDir)}.sock`),
1385
+ windowsPipe: `\\\\.\\pipe\\omo-lsp-${version}-${shortDigest(versionDir.replaceAll("/", "\\"))}`,
1386
+ };
1387
+ }
1388
+
1389
+ function assertVectorHelpers(version) {
1390
+ const expected = expectedVectors(version);
1391
+ const actual = {
1392
+ natural: support.legacyEndpointFor({ codexHome, version, kind: "natural" }),
1393
+ hashed: support.legacyEndpointFor({ codexHome, version, kind: "hashed", tempDir: tmpdir() }),
1394
+ windowsPipe: support.legacyEndpointFor({ codexHome, version, kind: "windowsPipe" }),
1395
+ };
1396
+ if (expected.natural !== actual.natural || expected.hashed !== actual.hashed || expected.windowsPipe !== actual.windowsPipe) {
1397
+ throw new Error(`legacy vector helper drift for ${version}`);
1398
+ }
1399
+ return { ...expected, ...actual };
1400
+ }
1401
+
1402
+ function writeFixtureCli() {
1403
+ const fixtureDir = join(sandboxRoot, "legacy-fixtures");
1404
+ mkdirSync(fixtureDir, { recursive: true });
1405
+ const cliPath = join(fixtureDir, "cli.js");
1406
+ const idlePath = join(fixtureDir, "idle.js");
1407
+ writeFileSync(
1408
+ cliPath,
1409
+ [
1410
+ 'const { createServer } = require("node:net")',
1411
+ 'const { mkdirSync, unlinkSync, writeFileSync } = require("node:fs")',
1412
+ 'const { dirname } = require("node:path")',
1413
+ "const endpoint = process.env.LEGACY_ENDPOINT",
1414
+ "const readyFile = process.env.LEGACY_READY_FILE",
1415
+ "if (process.env.LEGACY_IGNORE_SIGTERM === '1') process.on('SIGTERM', () => {})",
1416
+ "mkdirSync(dirname(endpoint), { recursive: true })",
1417
+ "try { unlinkSync(endpoint) } catch {}",
1418
+ "const server = createServer((socket) => {",
1419
+ " let buffer = ''",
1420
+ " socket.on('data', (chunk) => {",
1421
+ " buffer += chunk.toString('utf8')",
1422
+ " for (;;) {",
1423
+ " const newlineIndex = buffer.indexOf('\\n')",
1424
+ " if (newlineIndex < 0) break",
1425
+ " const line = buffer.slice(0, newlineIndex).trim()",
1426
+ " buffer = buffer.slice(newlineIndex + 1)",
1427
+ " if (line.length === 0) continue",
1428
+ " socket.write(`${JSON.stringify({ jsonrpc: '2.0', id: 1, result: { content: [{ type: 'text', text: 'legacy-ok' }] } })}\\n`)",
1429
+ " socket.end()",
1430
+ " }",
1431
+ " })",
1432
+ "})",
1433
+ "const closeAndExit = () => server.close(() => process.exit(0))",
1434
+ "if (process.env.LEGACY_IGNORE_SIGTERM !== '1') process.on('SIGTERM', closeAndExit)",
1435
+ "process.on('SIGINT', closeAndExit)",
1436
+ "server.listen(endpoint, () => { if (readyFile) writeFileSync(readyFile, 'ready\\n') })",
1437
+ ].join("\n") + "\n",
1438
+ );
1439
+ writeFileSync(idlePath, "setInterval(() => undefined, 1_000)\n");
1440
+ return { cliPath, idlePath };
1441
+ }
1442
+
1443
+ async function waitForReady(child, readyFile, endpoint, label) {
1444
+ const deadline = Date.now() + 5_000;
1445
+ while (Date.now() < deadline) {
1446
+ if (!alive(child.pid)) throw new Error(`${label} exited before ready`);
1447
+ if (existsSync(readyFile) && await attestation.probeLegacyJsonRpcEndpoint(endpoint)) return;
1448
+ await new Promise((resolve) => setTimeout(resolve, 50));
1449
+ }
1450
+ throw new Error(`${label} did not report a responding endpoint`);
1451
+ }
1452
+
1453
+ function startLegacyServer(cliPath, endpoint, ignoreSigterm) {
1454
+ const readyFile = join(sandboxRoot, "legacy-fixtures", `ready-${shortDigest(endpoint)}.txt`);
1455
+ const child = spawn(nodeBinary, [cliPath, "daemon"], {
1456
+ env: {
1457
+ ...process.env,
1458
+ LEGACY_ENDPOINT: endpoint,
1459
+ LEGACY_READY_FILE: readyFile,
1460
+ LEGACY_IGNORE_SIGTERM: ignoreSigterm ? "1" : "0",
1461
+ },
1462
+ detached: true,
1463
+ stdio: ["ignore", "ignore", "ignore"],
1464
+ });
1465
+ child.unref();
1466
+ return { pid: child.pid, readyFile, endpoint };
1467
+ }
1468
+
1469
+ function startIdle(idlePath) {
1470
+ const child = spawn(nodeBinary, [idlePath], { detached: true, stdio: ["ignore", "ignore", "ignore"] });
1471
+ child.unref();
1472
+ return child;
1473
+ }
1474
+
1475
+ function alive(pid) {
1476
+ if (!Number.isInteger(pid) || pid <= 0) return false;
1477
+ try {
1478
+ process.kill(pid, 0);
1479
+ return true;
1480
+ } catch {
1481
+ return false;
1482
+ }
1483
+ }
1484
+
1485
+ async function stopPid(pid, signal = "SIGKILL") {
1486
+ if (!alive(pid)) return false;
1487
+ try {
1488
+ process.kill(pid, signal);
1489
+ } catch {
1490
+ return false;
1491
+ }
1492
+ const deadline = Date.now() + 2_000;
1493
+ while (Date.now() < deadline) {
1494
+ if (!alive(pid)) return true;
1495
+ await new Promise((resolve) => setTimeout(resolve, 50));
1496
+ }
1497
+ return !alive(pid);
1498
+ }
1499
+
1500
+ async function setupLiveFixture() {
1501
+ const { cliPath, idlePath } = writeFixtureCli();
1502
+ const vectors = Object.fromEntries(Object.values(versions).map((version) => [version, assertVectorHelpers(version)]));
1503
+ const owned = startLegacyServer(cliPath, vectors[versions.ownedNatural].hashed, false);
1504
+ const foreignServer = startLegacyServer(cliPath, vectors[versions.foreignOwner].hashed, false);
1505
+ const timeoutServer = startLegacyServer(cliPath, vectors[versions.timeout].hashed, true);
1506
+ const unrelated = startIdle(idlePath);
1507
+ await Promise.all([
1508
+ waitForReady(owned, owned.readyFile, owned.endpoint, "owned legacy daemon"),
1509
+ waitForReady(foreignServer, foreignServer.readyFile, foreignServer.endpoint, "foreign-owner legacy daemon"),
1510
+ waitForReady(timeoutServer, timeoutServer.readyFile, timeoutServer.endpoint, "timeout legacy daemon"),
1511
+ ]);
1512
+
1513
+ await support.writeLegacyVersionState({
1514
+ codexHome,
1515
+ version: versions.ownedNatural,
1516
+ pid: String(owned.pid),
1517
+ endpoint: vectors[versions.ownedNatural].hashed,
1518
+ });
1519
+ await support.writeLegacyVersionState({
1520
+ codexHome,
1521
+ version: versions.staleNatural,
1522
+ pid: "910002",
1523
+ endpoint: vectors[versions.staleNatural].natural,
1524
+ });
1525
+ await support.writeLegacyVersionState({
1526
+ codexHome,
1527
+ version: versions.staleHashed,
1528
+ pid: "910003",
1529
+ endpoint: vectors[versions.staleHashed].hashed,
1530
+ });
1531
+ await support.writeLegacyVersionState({
1532
+ codexHome,
1533
+ version: versions.foreignOwner,
1534
+ pid: String(unrelated.pid),
1535
+ endpoint: vectors[versions.foreignOwner].hashed,
1536
+ });
1537
+ await support.writeLegacyVersionState({
1538
+ codexHome,
1539
+ version: versions.timeout,
1540
+ pid: String(timeoutServer.pid),
1541
+ endpoint: vectors[versions.timeout].hashed,
1542
+ });
1543
+ await support.writeLegacyVersionState({
1544
+ codexHome,
1545
+ version: versions.malformed,
1546
+ pid: "not-a-pid",
1547
+ endpoint: vectors[versions.malformed].natural,
1548
+ });
1549
+
1550
+ writeFileSync(
1551
+ metadataPath,
1552
+ JSON.stringify(
1553
+ {
1554
+ versions,
1555
+ vectors,
1556
+ processes: {
1557
+ owned: { pid: owned.pid },
1558
+ foreignServer: { pid: foreignServer.pid },
1559
+ unrelated: { pid: unrelated.pid },
1560
+ timeout: { pid: timeoutServer.pid },
1561
+ },
1562
+ },
1563
+ null,
1564
+ 2,
1565
+ ) + "\n",
1566
+ );
1567
+ }
1568
+
1569
+ async function runContractProbe() {
1570
+ const root = join(sandboxRoot, "legacy-contract");
1571
+ rmSync(root, { recursive: true, force: true });
1572
+ mkdirSync(root, { recursive: true });
1573
+ const windowsHome = join(root, "windows-home");
1574
+ const noAttestHome = join(root, "no-attestation-home");
1575
+ const timeoutHome = join(root, "timeout-home");
1576
+ const staleHome = join(root, "stale-home");
1577
+ const symlinkHome = join(root, "symlink-home");
1578
+ const malformedHome = join(root, "malformed-home");
1579
+
1580
+ const windowsVersion = "8.1.1";
1581
+ const windowsState = await support.writeLegacyVersionState({
1582
+ codexHome: windowsHome,
1583
+ version: windowsVersion,
1584
+ pid: "5001",
1585
+ endpoint: support.legacyEndpointFor({ codexHome: windowsHome, version: windowsVersion, kind: "windowsPipe" }),
1586
+ });
1587
+ const killedWindows = [];
1588
+ const windows = await reaper.reapLspDaemons(windowsHome, {
1589
+ platform: "win32",
1590
+ probeLegacyJsonRpc: async () => true,
1591
+ killProcess: (pid) => {
1592
+ killedWindows.push(pid);
1593
+ return true;
1594
+ },
1595
+ });
1596
+
1597
+ const noAttestVersion = "8.1.2";
1598
+ const noAttestState = await support.writeLegacyVersionState({
1599
+ codexHome: noAttestHome,
1600
+ version: noAttestVersion,
1601
+ pid: "5002",
1602
+ endpoint: support.legacyEndpointFor({ codexHome: noAttestHome, version: noAttestVersion, kind: "natural" }),
1603
+ });
1604
+ const killedNoAttest = [];
1605
+ const noAttestation = await reaper.reapLspDaemons(noAttestHome, {
1606
+ platform: "darwin",
1607
+ probeLegacyJsonRpc: async () => true,
1608
+ attestLegacyDaemonOwnership: async () => false,
1609
+ killProcess: (pid) => {
1610
+ killedNoAttest.push(pid);
1611
+ return true;
1612
+ },
1613
+ });
1614
+
1615
+ const timeoutVersion = "8.1.3";
1616
+ const timeoutState = await support.writeLegacyVersionState({
1617
+ codexHome: timeoutHome,
1618
+ version: timeoutVersion,
1619
+ pid: "5003",
1620
+ endpoint: support.legacyEndpointFor({ codexHome: timeoutHome, version: timeoutVersion, kind: "natural" }),
1621
+ });
1622
+ const killedTimeout = [];
1623
+ const timeout = await reaper.reapLspDaemons(timeoutHome, {
1624
+ platform: "darwin",
1625
+ probeLegacyJsonRpc: async () => true,
1626
+ attestLegacyDaemonOwnership: async () => true,
1627
+ killProcess: (pid) => {
1628
+ killedTimeout.push(pid);
1629
+ return true;
1630
+ },
1631
+ waitForProcessExit: async () => false,
1632
+ });
1633
+
1634
+ const staleVersion = "8.1.4";
1635
+ const staleState = await support.writeLegacyVersionState({
1636
+ codexHome: staleHome,
1637
+ version: staleVersion,
1638
+ pid: "5004",
1639
+ endpoint: support.legacyEndpointFor({ codexHome: staleHome, version: staleVersion, kind: "hashed", tempDir: tmpdir() }),
1640
+ });
1641
+ const stale = await reaper.reapLspDaemons(staleHome, { probeLegacyJsonRpc: async () => false });
1642
+
1643
+ const outside = await mkdtemp(join(root, "symlink-outside-"));
1644
+ const symlinkState = await support.writeSymlinkedLegacyMetadata({
1645
+ codexHome: symlinkHome,
1646
+ version: "8.1.5",
1647
+ targetPath: join(outside, "foreign.txt"),
1648
+ });
1649
+ const symlink = await reaper.reapLspDaemons(symlinkHome);
1650
+
1651
+ const malformedDir = support.versionDirFor(malformedHome, "8.1.6");
1652
+ mkdirSync(malformedDir, { recursive: true });
1653
+ writeFileSync(join(malformedDir, "daemon.pid"), "not-a-pid\n");
1654
+ writeFileSync(join(malformedDir, "daemon.endpoint"), `${join(malformedDir, "daemon.sock")}\n`);
1655
+ const malformed = await reaper.reapLspDaemons(malformedHome);
1656
+
1657
+ const contract = {
1658
+ windows,
1659
+ noAttestation,
1660
+ timeout,
1661
+ stale,
1662
+ symlink,
1663
+ malformed,
1664
+ assertions: {
1665
+ windowsDeferred: windows[0]?.status === "deferred" && existsSync(windowsState.versionDir) && killedWindows.length === 0,
1666
+ noAttestationDeferred: noAttestation[0]?.status === "deferred" && existsSync(noAttestState.versionDir) && killedNoAttest.length === 0,
1667
+ timeoutDeferred: timeout[0]?.status === "deferred" && existsSync(timeoutState.versionDir) && killedTimeout.join(",") === "5003",
1668
+ staleRemoved: stale[0]?.status === "removed" && !existsSync(staleState.versionDir),
1669
+ symlinkRemovedWithoutFollowing: symlink[0]?.reason === "removed non-regular legacy daemon metadata" && !existsSync(symlinkState.versionDir) && existsSync(join(outside, "foreign.txt")),
1670
+ malformedRemoved: malformed[0]?.reason === "removed malformed legacy daemon metadata" && !existsSync(malformedDir),
1671
+ },
1672
+ };
1673
+ if (!Object.values(contract.assertions).every(Boolean)) throw new Error("legacy cleanup contract probe failed");
1674
+ writeFileSync(contractPath, JSON.stringify(contract, null, 2) + "\n");
1675
+ await rm(root, { recursive: true, force: true });
1676
+ }
1677
+
1678
+ async function scanForCopiedIpc() {
1679
+ if (!existsSync(omoRoot)) return [];
1680
+ const { lstatSync, readdirSync } = await import("node:fs");
1681
+ const matches = [];
1682
+ const visit = (path) => {
1683
+ const stat = lstatSync(path);
1684
+ if (stat.isDirectory()) {
1685
+ for (const name of readdirSync(path)) visit(join(path, name));
1686
+ return;
1687
+ }
1688
+ if (!stat.isFile()) return;
1689
+ const text = readFileSync(path, "utf8");
1690
+ if (text.includes("codex-lsp/daemon") || text.includes("omo-lsp-8.0.")) matches.push(path);
1691
+ };
1692
+ visit(omoRoot);
1693
+ return matches;
1694
+ }
1695
+
1696
+ async function verifyLiveFixture() {
1697
+ const metadata = JSON.parse(readFileSync(metadataPath, "utf8"));
1698
+ const copiedIpcFiles = await scanForCopiedIpc();
1699
+ const warningLog = readFileSync(join(evidenceDir, "install.log"), "utf8");
1700
+ const live = {
1701
+ vectors: metadata.vectors,
1702
+ results: {
1703
+ ownedTerminated: !alive(metadata.processes.owned.pid) && !existsSync(metadata.vectors[versions.ownedNatural].versionDir),
1704
+ staleNaturalRemoved: !existsSync(metadata.vectors[versions.staleNatural].versionDir),
1705
+ staleHashedRemoved: !existsSync(metadata.vectors[versions.staleHashed].versionDir),
1706
+ malformedRemoved: !existsSync(metadata.vectors[versions.malformed].versionDir),
1707
+ foreignOwnerPreserved: alive(metadata.processes.foreignServer.pid)
1708
+ && alive(metadata.processes.unrelated.pid)
1709
+ && existsSync(metadata.vectors[versions.foreignOwner].versionDir),
1710
+ timeoutPreserved: alive(metadata.processes.timeout.pid) && existsSync(metadata.vectors[versions.timeout].versionDir),
1711
+ unrelatedPidNotSignaled: alive(metadata.processes.unrelated.pid),
1712
+ visibleInstallerWarning: warningLog.includes(`Warning: deferred legacy Codex LSP daemon cleanup for v${versions.foreignOwner}`)
1713
+ && warningLog.includes(`Warning: deferred legacy Codex LSP daemon cleanup for v${versions.timeout}`),
1714
+ noLiveIpcCopy: copiedIpcFiles.length === 0,
1715
+ },
1716
+ copiedIpcFiles,
1717
+ warningLines: warningLog.split(/\r?\n/).filter((line) => line.includes("Warning: deferred legacy Codex LSP daemon cleanup")),
1718
+ };
1719
+ writeFileSync(livePath, JSON.stringify(live, null, 2) + "\n");
1720
+ if (!Object.values(live.results).every(Boolean)) throw new Error("legacy cleanup live verification failed");
1721
+ }
1722
+
1723
+ async function cleanupLiveFixture() {
1724
+ if (!existsSync(metadataPath)) return;
1725
+ const metadata = JSON.parse(readFileSync(metadataPath, "utf8"));
1726
+ const receipts = [];
1727
+ for (const [name, processInfo] of Object.entries(metadata.processes)) {
1728
+ const wasAlive = alive(processInfo.pid);
1729
+ const stopped = await stopPid(processInfo.pid);
1730
+ receipts.push(`${name}_pid=${processInfo.pid} was_alive=${wasAlive ? "yes" : "no"} alive_after=${alive(processInfo.pid) ? "yes" : "no"} stopped=${stopped ? "yes" : "no"}`);
1731
+ }
1732
+ writeFileSync(processCleanupPath, receipts.join("\n") + "\n");
1733
+ }
1734
+
1735
+ if (mode === "setup") await setupLiveFixture();
1736
+ else if (mode === "contract") await runContractProbe();
1737
+ else if (mode === "verify") await verifyLiveFixture();
1738
+ else if (mode === "cleanup") await cleanupLiveFixture();
1739
+ else throw new Error(`unknown legacy cleanup probe mode: ${mode}`);
1740
+ NODE
1741
+ }
1742
+
1743
+ run_legacy_cleanup_probe() {
1744
+ local mode="$1"
1745
+ shift
1746
+ REPO_ROOT="$REPO_ROOT" SANDBOX_ROOT="$SANDBOX_ROOT" EVIDENCE_DIR="$EVIDENCE_DIR" \
1747
+ run_bounded 30 "$EVIDENCE_DIR/legacy-cleanup-$mode.log" bun "$SANDBOX_ROOT/legacy-cleanup-probe.mjs" "$mode" "$@"
1748
+ }
1749
+
1750
+ rewrite_codex_mcp_config() {
1751
+ node --input-type=module - "$CODEX_HOME/config.toml" <<'NODE'
1752
+ import { readFileSync, writeFileSync } from "node:fs";
1753
+ const path = process.argv[2];
1754
+ let text = readFileSync(path, "utf8");
1755
+ for (const name of ["context7", "codegraph"]) {
1756
+ const header = `[plugins."omo@sisyphuslabs".mcp_servers.${name}]`;
1757
+ const start = text.indexOf(header);
1758
+ if (start < 0) throw new Error(`missing ${header}`);
1759
+ const next = text.indexOf("\n[", start + header.length);
1760
+ const end = next < 0 ? text.length : next;
1761
+ const section = text.slice(start, end).replace(/enabled\s*=\s*true/, "enabled = false");
1762
+ text = text.slice(0, start) + section + text.slice(end);
1763
+ }
1764
+ text += `\n[plugins."omo@sisyphuslabs".mcp_servers.grep_app]\nenabled = false\n`;
1765
+ text += `\n[plugins."omo@sisyphuslabs".mcp_servers.lsp]\nenabled = true\n`;
1766
+ writeFileSync(path, text);
1767
+ NODE
1768
+ }
1769
+
1770
+ stamp_codex_lsp_environment() {
1771
+ local manifest="$1"
1772
+ node --input-type=module - "$manifest" "$OMO_LSP_DAEMON_DIR" <<'NODE'
1773
+ import { readFileSync, writeFileSync } from "node:fs";
1774
+ const [path, daemonDir] = process.argv.slice(2);
1775
+ const manifest = JSON.parse(readFileSync(path, "utf8"));
1776
+ const lsp = manifest?.mcpServers?.lsp;
1777
+ if (!lsp || typeof lsp !== "object") throw new Error("installed plugin has no lsp MCP server");
1778
+ lsp.env = { ...(lsp.env || {}), OMO_LSP_DAEMON_DIR: daemonDir };
1779
+ writeFileSync(path, JSON.stringify(manifest, null, 2) + "\n");
1780
+ NODE
1781
+ }
1782
+
1783
+ disable_codex_side_effect_hooks() {
1784
+ local manifest="$1"
1785
+ node --input-type=module - "$manifest" <<'NODE'
1786
+ import { readFileSync, writeFileSync } from "node:fs";
1787
+ import { basename } from "node:path";
1788
+
1789
+ const path = process.argv[2];
1790
+ const disabled = new Set([
1791
+ "session-start-checking-auto-update.json",
1792
+ "session-start-checking-bootstrap-provisioning.json",
1793
+ "session-start-checking-codegraph-bootstrap.json",
1794
+ ]);
1795
+ const manifest = JSON.parse(readFileSync(path, "utf8"));
1796
+ if (!Array.isArray(manifest.hooks)) throw new Error("installed plugin hook list is missing");
1797
+ const retained = manifest.hooks.filter((entry) => typeof entry !== "string" || !disabled.has(basename(entry)));
1798
+ if (manifest.hooks.length - retained.length !== disabled.size) {
1799
+ throw new Error("installed plugin did not contain every isolated-QA side-effect hook");
1800
+ }
1801
+ manifest.hooks = retained;
1802
+ writeFileSync(path, JSON.stringify(manifest, null, 2) + "\n");
1803
+ NODE
1804
+ }
1805
+
1806
+ start_mock_model() {
1807
+ local log_file="$1" port="" attempts=0
1808
+ MOCK_PORT=0 node "$SCRIPT_DIR/lib/mock-model.mjs" >"$log_file" 2>&1 &
1809
+ MOCK_PID=$!
1810
+ while [ "$attempts" -lt 100 ]; do
1811
+ port="$(awk '/^MOCK_LISTENING / { print $2; exit }' "$log_file" 2>/dev/null || true)"
1812
+ [ -n "$port" ] && break
1813
+ kill -0 "$MOCK_PID" 2>/dev/null || { fail "mock model exited during startup"; return 1; }
1814
+ sleep 0.1
1815
+ attempts=$((attempts + 1))
1816
+ done
1817
+ [ -n "$port" ] || { fail "mock model did not report a port"; return 1; }
1818
+ export MOCK_PORT="$port"
1819
+ }
1820
+
1821
+ write_app_server_driver() {
1822
+ local path="$1"
1823
+ cat >"$path" <<'NODE'
1824
+ import { spawn } from "node:child_process";
1825
+ import { readFileSync, writeFileSync } from "node:fs";
1826
+
1827
+ const codex = process.env.CODEX_BIN || "codex";
1828
+ const mockPort = process.env.MOCK_PORT;
1829
+ const cwd = process.env.QA_CWD;
1830
+ const output = process.env.APP_SERVER_RESULT;
1831
+ const pidFile = process.env.APP_SERVER_PID_FILE;
1832
+ const deadlineMs = Number(process.env.DEADLINE_MS || 120000);
1833
+ const qaScenario = process.env.QA_SCENARIO || "path-contract";
1834
+ const qaSourceFile = process.env.QA_SOURCE_FILE || "source.ts";
1835
+ const qaSourcePath = process.env.QA_SOURCE_PATH || "";
1836
+ const qaRenameEvents = process.env.QA_RENAME_EVENTS || "";
1837
+ if (!mockPort || !cwd || !output || !pidFile) throw new Error("missing app-server driver environment");
1838
+
1839
+ const overrides = [
1840
+ `model="mock-model"`,
1841
+ `model_provider="mock_provider"`,
1842
+ `model_providers.mock_provider.name="codex-qa mock"`,
1843
+ `model_providers.mock_provider.base_url="http://127.0.0.1:${mockPort}/v1"`,
1844
+ `model_providers.mock_provider.wire_api="responses"`,
1845
+ `model_providers.mock_provider.request_max_retries=0`,
1846
+ `model_providers.mock_provider.stream_max_retries=0`,
1847
+ `approval_policy="never"`,
1848
+ `sandbox_mode="read-only"`,
1849
+ ];
1850
+ const args = overrides.flatMap((value) => ["-c", value]).concat("app-server");
1851
+ const child = spawn(codex, args, { stdio: ["pipe", "pipe", "pipe"], env: process.env });
1852
+ writeFileSync(pidFile, `${child.pid}\n`);
1853
+
1854
+ let stderr = "";
1855
+ let buffer = "";
1856
+ let threadId = null;
1857
+ let turnId = null;
1858
+ let turnStatus = null;
1859
+ let assistantText = null;
1860
+ let lspResponse = null;
1861
+ let diagnosticsResponse = null;
1862
+ let settled = false;
1863
+ const hooks = [];
1864
+ const send = (message) => child.stdin.write(JSON.stringify(message) + "\n");
1865
+
1866
+ function hookEvents(method) {
1867
+ return new Set(hooks.filter((entry) => entry.method === method && entry.status === (method === "hook/completed" ? "completed" : "running")).map((entry) => entry.eventName));
1868
+ }
1869
+
1870
+ function summarize(reason) {
1871
+ const started = hookEvents("hook/started");
1872
+ const completed = hookEvents("hook/completed");
1873
+ const expected = ["sessionStart", "userPromptSubmit"];
1874
+ const lspText = lspResponse?.result?.content?.map((entry) => entry?.text || "").join("\n") || "";
1875
+ const diagnosticsText = diagnosticsResponse?.result?.content?.map((entry) => entry?.text || "").join("\n") || "";
1876
+ const renameEvents = qaRenameEvents
1877
+ ? readFileSync(qaRenameEvents, "utf8")
1878
+ .split("\n")
1879
+ .filter(Boolean)
1880
+ .map((line) => JSON.parse(line))
1881
+ : [];
1882
+ const applyResponses = renameEvents
1883
+ .filter((event) => event?.type === "clientResponse" && event?.method === "workspace/applyEdit")
1884
+ .map((event) => event?.result ?? null);
1885
+ const didChangeVersions = renameEvents
1886
+ .filter((event) => event?.type === "clientNotification" && event?.method === "textDocument/didChange")
1887
+ .map((event) => event?.params?.textDocument?.version)
1888
+ .filter((value) => typeof value === "number");
1889
+ const renameHarness = qaScenario === "rename"
1890
+ ? {
1891
+ serverApplyRequestCount: renameEvents.filter(
1892
+ (event) => event?.type === "serverRequest" && event?.method === "workspace/applyEdit",
1893
+ ).length,
1894
+ applyResponses,
1895
+ didChangeVersions,
1896
+ finalContent: qaSourcePath ? readFileSync(qaSourcePath, "utf8") : null,
1897
+ }
1898
+ : null;
1899
+ const ok = reason === "turn-completed"
1900
+ && turnStatus === "completed"
1901
+ && expected.every((event) => started.has(event) && completed.has(event))
1902
+ && (
1903
+ qaScenario === "rename"
1904
+ ? lspResponse?.error === undefined
1905
+ && lspResponse?.result?.isError !== true
1906
+ && lspText.includes("Applied 1 edit(s)")
1907
+ && diagnosticsResponse?.error === undefined
1908
+ && diagnosticsResponse?.result?.isError !== true
1909
+ && diagnosticsText.includes("todo3-fresh")
1910
+ && renameHarness?.serverApplyRequestCount === 1
1911
+ && renameHarness?.applyResponses?.length === 1
1912
+ && renameHarness?.applyResponses?.[0]?.applied === true
1913
+ && renameHarness?.didChangeVersions?.join(",") === "2"
1914
+ && renameHarness?.finalContent === "const after = 1;\n"
1915
+ : qaScenario === "diagnostics-freshness"
1916
+ ? lspResponse?.error === undefined
1917
+ && lspResponse?.result?.isError !== true
1918
+ && lspText.includes("exact-current")
1919
+ : lspResponse?.error === undefined
1920
+ && lspResponse?.result?.isError !== true
1921
+ && lspText.includes("Configured LSP servers")
1922
+ );
1923
+ return {
1924
+ ok,
1925
+ reason,
1926
+ scenario: qaScenario,
1927
+ threadId,
1928
+ turnId,
1929
+ turnStatus,
1930
+ assistantText,
1931
+ expectedHooks: expected,
1932
+ hookStarted: expected.every((event) => started.has(event)),
1933
+ hookCompleted: expected.every((event) => completed.has(event)),
1934
+ hooks,
1935
+ lspOperation: qaScenario === "rename"
1936
+ ? {
1937
+ server: "lsp",
1938
+ tool: "lsp_rename",
1939
+ ok: lspResponse?.error === undefined && lspResponse?.result?.isError !== true && lspText.includes("Applied 1 edit(s)"),
1940
+ text: lspText,
1941
+ details: lspResponse?.result?.details ?? null,
1942
+ isError: lspResponse?.result?.isError ?? null,
1943
+ protocolError: lspResponse?.error ?? null,
1944
+ }
1945
+ : qaScenario === "diagnostics-freshness"
1946
+ ? {
1947
+ server: "lsp",
1948
+ tool: "lsp_diagnostics",
1949
+ ok: lspResponse?.error === undefined && lspResponse?.result?.isError !== true && lspText.includes("exact-current"),
1950
+ text: lspText,
1951
+ details: lspResponse?.result?.details ?? null,
1952
+ isError: lspResponse?.result?.isError ?? null,
1953
+ protocolError: lspResponse?.error ?? null,
1954
+ }
1955
+ : {
1956
+ server: "lsp",
1957
+ tool: "lsp_status",
1958
+ ok: lspResponse?.error === undefined && lspResponse?.result?.isError !== true && lspText.includes("Configured LSP servers"),
1959
+ text: lspText,
1960
+ details: lspResponse?.result?.details ?? null,
1961
+ isError: lspResponse?.result?.isError ?? null,
1962
+ protocolError: lspResponse?.error ?? null,
1963
+ },
1964
+ diagnosticsOperation: qaScenario === "rename"
1965
+ ? {
1966
+ server: "lsp",
1967
+ tool: "lsp_diagnostics",
1968
+ ok: diagnosticsResponse?.error === undefined && diagnosticsResponse?.result?.isError !== true && diagnosticsText.includes("todo3-fresh"),
1969
+ text: diagnosticsText,
1970
+ details: diagnosticsResponse?.result?.details ?? null,
1971
+ isError: diagnosticsResponse?.result?.isError ?? null,
1972
+ protocolError: diagnosticsResponse?.error ?? null,
1973
+ }
1974
+ : null,
1975
+ renameHarness,
1976
+ stderrTail: stderr.split("\n").slice(-20).join("\n"),
1977
+ };
1978
+ }
1979
+
1980
+ function finish(reason) {
1981
+ if (settled) return;
1982
+ settled = true;
1983
+ clearTimeout(deadline);
1984
+ const summary = summarize(reason);
1985
+ writeFileSync(output, JSON.stringify(summary, null, 2) + "\n");
1986
+ try { child.kill("SIGTERM"); } catch {}
1987
+ setTimeout(() => {
1988
+ try { child.kill("SIGKILL"); } catch {}
1989
+ process.exit(summary.ok ? 0 : 1);
1990
+ }, 250).unref();
1991
+ child.once("exit", () => process.exit(summary.ok ? 0 : 1));
1992
+ }
1993
+
1994
+ function handle(message) {
1995
+ if (message.id === 1 && message.result) {
1996
+ send({ method: "initialized" });
1997
+ send({ id: 2, method: "thread/start", params: { cwd } });
1998
+ return;
1999
+ }
2000
+ if (message.id === 2 && message.result) {
2001
+ threadId = message.result.thread?.id;
2002
+ if (qaScenario === "rename") {
2003
+ send({
2004
+ id: 4,
2005
+ method: "mcpServer/tool/call",
2006
+ params: {
2007
+ threadId,
2008
+ server: "lsp",
2009
+ tool: "lsp_rename",
2010
+ arguments: { filePath: qaSourceFile, line: 1, character: 6, newName: "after" },
2011
+ },
2012
+ });
2013
+ return;
2014
+ }
2015
+ if (qaScenario === "diagnostics-freshness") {
2016
+ send({
2017
+ id: 4,
2018
+ method: "mcpServer/tool/call",
2019
+ params: {
2020
+ threadId,
2021
+ server: "lsp",
2022
+ tool: "lsp_diagnostics",
2023
+ arguments: { filePath: qaSourceFile },
2024
+ },
2025
+ });
2026
+ return;
2027
+ }
2028
+ send({ id: 4, method: "mcpServer/tool/call", params: { threadId, server: "lsp", tool: "lsp_status", arguments: {} } });
2029
+ return;
2030
+ }
2031
+ if (message.id === 4) {
2032
+ lspResponse = message;
2033
+ if (qaScenario === "rename") {
2034
+ send({
2035
+ id: 5,
2036
+ method: "mcpServer/tool/call",
2037
+ params: {
2038
+ threadId,
2039
+ server: "lsp",
2040
+ tool: "lsp_diagnostics",
2041
+ arguments: { filePath: qaSourceFile },
2042
+ },
2043
+ });
2044
+ return;
2045
+ }
2046
+ send({ id: 3, method: "turn/start", params: { threadId, input: [{ type: "text", text: "say hello" }] } });
2047
+ return;
2048
+ }
2049
+ if (message.id === 5) {
2050
+ diagnosticsResponse = message;
2051
+ send({ id: 3, method: "turn/start", params: { threadId, input: [{ type: "text", text: "say hello" }] } });
2052
+ return;
2053
+ }
2054
+ if (message.id === 3 && message.result) {
2055
+ turnId = message.result.turn?.id;
2056
+ return;
2057
+ }
2058
+ if (message.method === "hook/started" || message.method === "hook/completed") {
2059
+ const run = message.params?.run || {};
2060
+ hooks.push({
2061
+ method: message.method,
2062
+ eventName: run.eventName,
2063
+ status: run.status,
2064
+ source: run.source ?? run.pluginId,
2065
+ pluginId: run.pluginId,
2066
+ hookName: run.hookName ?? run.name,
2067
+ });
2068
+ return;
2069
+ }
2070
+ if (message.method === "item/completed") {
2071
+ const item = message.params?.item;
2072
+ if (item?.type === "agentMessage" && typeof item.text === "string") assistantText = item.text;
2073
+ return;
2074
+ }
2075
+ if (message.method === "turn/completed") {
2076
+ turnStatus = message.params?.turn?.status;
2077
+ finish("turn-completed");
2078
+ }
2079
+ }
2080
+
2081
+ child.stderr.on("data", (chunk) => { stderr += chunk; });
2082
+ child.stdout.on("data", (chunk) => {
2083
+ buffer += chunk;
2084
+ let newline;
2085
+ while ((newline = buffer.indexOf("\n")) >= 0) {
2086
+ const line = buffer.slice(0, newline).trim();
2087
+ buffer = buffer.slice(newline + 1);
2088
+ if (!line) continue;
2089
+ try { handle(JSON.parse(line)); } catch {}
2090
+ }
2091
+ });
2092
+ child.on("exit", () => { if (!settled) finish("app-server-exited"); });
2093
+ const deadline = setTimeout(() => finish("deadline"), deadlineMs);
2094
+ send({ id: 1, method: "initialize", params: { clientInfo: { name: "omo-lsp-e2e", version: "1.0.0" }, capabilities: { experimentalApi: true, requestAttestation: false } } });
2095
+ NODE
2096
+ }
2097
+
2098
+ record_daemon_state() {
2099
+ local pid_file endpoint_file version_dir pid command
2100
+ pid_file="$(find_daemon_pid_file)"
2101
+ [ -n "$pid_file" ] || { fail "actual LSP tool call did not create a daemon pid file"; return 1; }
2102
+ version_dir="$(dirname "$pid_file")"
2103
+ endpoint_file="$version_dir/daemon.endpoint"
2104
+ [ -f "$endpoint_file" ] || { fail "daemon endpoint file is missing"; return 1; }
2105
+ pid="$(tr -d '[:space:]' <"$pid_file")"
2106
+ command="$(process_command "$pid")"
2107
+ case "$command" in
2108
+ *"$EXPECTED_DAEMON_CLI"*" daemon"*) ;;
2109
+ *) fail "daemon process command does not match the installed CLI"; return 1 ;;
2110
+ esac
2111
+ node --input-type=module - "$EVIDENCE_DIR/daemon-state.json" "$OMO_TEST_ROOT" "$version_dir" "$pid" "$endpoint_file" <<'NODE'
2112
+ import { readFileSync, writeFileSync } from "node:fs";
2113
+ import { basename, dirname } from "node:path";
2114
+ const [output, base, versionDir, pid, endpointFile] = process.argv.slice(2);
2115
+ writeFileSync(output, JSON.stringify({
2116
+ base,
2117
+ versionDir,
2118
+ version: basename(versionDir).replace(/^v/, ""),
2119
+ pid: Number(pid),
2120
+ endpointKind: readFileSync(endpointFile, "utf8").startsWith("\\\\.\\pipe\\") ? "named-pipe" : "unix-socket",
2121
+ endpointInsideVersionDir: dirname(readFileSync(endpointFile, "utf8")) === versionDir,
2122
+ }, null, 2) + "\n");
2123
+ NODE
2124
+ }
2125
+
2126
+ run_installed_component_probe() {
2127
+ local plugin_root="$1"
2128
+ local script="$SANDBOX_ROOT/installed-component-probe.mjs"
2129
+ cat >"$script" <<'NODE'
2130
+ import { builtinModules } from "node:module";
2131
+ import { createHash } from "node:crypto";
2132
+ import {
2133
+ cpSync,
2134
+ existsSync,
2135
+ mkdirSync,
2136
+ readdirSync,
2137
+ readFileSync,
2138
+ realpathSync,
2139
+ rmSync,
2140
+ writeFileSync,
2141
+ } from "node:fs";
2142
+ import { dirname, join, resolve } from "node:path";
2143
+ import { pathToFileURL } from "node:url";
2144
+ import { spawnSync } from "node:child_process";
2145
+
2146
+ const repoRoot = process.env.REPO_ROOT;
2147
+ const pluginRoot = process.env.INSTALLED_PLUGIN_ROOT;
2148
+ const projectDir = process.env.QA_CWD;
2149
+ const codexHome = process.env.CODEX_HOME;
2150
+ const sandboxRoot = process.env.SANDBOX_ROOT;
2151
+ const daemonDir = process.env.OMO_LSP_DAEMON_DIR;
2152
+ const output = process.env.INSTALLED_COMPONENT_RESULT;
2153
+ if (!repoRoot || !pluginRoot || !projectDir || !codexHome || !sandboxRoot || !daemonDir || !output) {
2154
+ throw new Error("missing installed-component probe environment");
2155
+ }
2156
+
2157
+ const componentRoot = join(pluginRoot, "components", "lsp");
2158
+ const cliPath = join(componentRoot, "dist", "cli.js");
2159
+ const manifestPath = join(componentRoot, "dist", ".omo-runtime-manifest.json");
2160
+ const daemonCliPath = join(pluginRoot, "components", "lsp-daemon", "dist", "cli.js");
2161
+ const daemonVersion = JSON.parse(readFileSync(join(dirname(daemonCliPath), "package.json"), "utf8")).version;
2162
+ const pluginData = join(sandboxRoot, "installed-component-plugin-data");
2163
+ const scenarioPath = join(sandboxRoot, "installed-component-scenario.json");
2164
+ const eventsPath = join(sandboxRoot, "installed-component-server-events.jsonl");
2165
+ const sourcePath = join(projectDir, "source.ts");
2166
+ const markdownPath = join(projectDir, "README.md");
2167
+ const transcriptPath = join(sandboxRoot, "context-pressure-transcript.txt");
2168
+ const projectConfigPath = join(realpathSync(projectDir), ".codex", "lsp-client.json");
2169
+ const userConfigPath = join(resolve(codexHome), "lsp-client.json");
2170
+ const installDecisionsPath = join(resolve(codexHome), "lsp-install-decisions.json");
2171
+ const fixturePath = join(repoRoot, "packages", "lsp-core", "src", "lsp", "fixtures", "workspace-edit-server.mjs");
2172
+ const longMessage = `installed-post-edit ${"x".repeat(12000)}`;
2173
+
2174
+ mkdirSync(dirname(projectConfigPath), { recursive: true });
2175
+ mkdirSync(resolve(codexHome), { recursive: true });
2176
+ mkdirSync(pluginData, { recursive: true });
2177
+ writeFileSync(sourcePath, "const value = missingSymbol;\n", "utf8");
2178
+ writeFileSync(markdownPath, "# note\n", "utf8");
2179
+ writeFileSync(eventsPath, "", "utf8");
2180
+ writeFileSync(transcriptPath, "Codex ran out of room in the model's context window\n", "utf8");
2181
+ writeFileSync(
2182
+ scenarioPath,
2183
+ JSON.stringify(
2184
+ {
2185
+ publishDiagnostics: [
2186
+ { trigger: "didOpen", version: 1, diagnostics: [diagnostic(longMessage)] },
2187
+ ],
2188
+ diagnosticResponses: [
2189
+ { report: { items: [diagnostic(longMessage)] } },
2190
+ { report: { items: [diagnostic(longMessage)] } },
2191
+ ],
2192
+ },
2193
+ null,
2194
+ 2,
2195
+ ) + "\n",
2196
+ );
2197
+ writeFileSync(
2198
+ projectConfigPath,
2199
+ JSON.stringify({ lsp: {} }, null, 2) + "\n",
2200
+ );
2201
+ writeFileSync(
2202
+ userConfigPath,
2203
+ JSON.stringify(
2204
+ {
2205
+ lsp: {
2206
+ typescript: {
2207
+ command: [process.execPath, fixturePath, scenarioPath, eventsPath],
2208
+ extensions: [".ts"],
2209
+ priority: 100,
2210
+ },
2211
+ },
2212
+ },
2213
+ null,
2214
+ 2,
2215
+ ) + "\n",
2216
+ );
2217
+
2218
+ const manifest = validateManifest();
2219
+ const staticImports = scanRuntimeImports();
2220
+ const distOnlyValidation = validateDistOnlyRuntime();
2221
+ const postEdit = runPostEditProbe();
2222
+ const mcpDiagnostics = runMcpDiagnosticsProbe();
2223
+ const compaction = runCompactionProbe();
2224
+ const validation = runOverrideValidation();
2225
+ const daemonOwner = inspectDaemonOwner();
2226
+ const daemonArtifacts = readDaemonArtifacts();
2227
+ const context = {
2228
+ cwd: realpathSync(projectDir),
2229
+ projectConfigPaths: [projectConfigPath],
2230
+ userConfigPath,
2231
+ installDecisionsPath,
2232
+ capabilities: { installDecisionTool: true },
2233
+ };
2234
+ const result = {
2235
+ result: "PASS",
2236
+ manifest,
2237
+ distOnlyValidation,
2238
+ componentCli: {
2239
+ path: cliPath,
2240
+ exists: existsSync(cliPath),
2241
+ noNonBuiltinRuntimeImports: staticImports.nonBuiltin.length === 0,
2242
+ nonBuiltinRuntimeImports: staticImports.nonBuiltin,
2243
+ },
2244
+ consumer: {
2245
+ emptyNodePath: postEdit.emptyNodePath === true && compaction.emptyNodePath === true,
2246
+ repositoryHiddenByInstall: staticImports.nonBuiltin.length === 0,
2247
+ },
2248
+ codexContext: context,
2249
+ postEdit,
2250
+ mcpDiagnostics,
2251
+ compaction,
2252
+ validation,
2253
+ daemonOwner,
2254
+ daemonArtifacts,
2255
+ };
2256
+
2257
+ const required = [
2258
+ result.manifest.valid === true,
2259
+ result.distOnlyValidation.ok === true,
2260
+ result.componentCli.exists === true,
2261
+ result.componentCli.noNonBuiltinRuntimeImports === true,
2262
+ result.consumer.emptyNodePath === true,
2263
+ result.consumer.repositoryHiddenByInstall === true,
2264
+ result.postEdit.actualPostEditOperation === true,
2265
+ result.postEdit.projectConfigConsumed === true,
2266
+ result.postEdit.defaultBudgetWithinLimit === true,
2267
+ result.postEdit.contextPressureBudgetWithinLimit === true,
2268
+ result.compaction.cacheStored === true,
2269
+ result.compaction.compactionReset === true,
2270
+ result.validation.singletonRejected === true,
2271
+ result.validation.nonexistentPairRejected === true,
2272
+ result.daemonOwner.count === 1,
2273
+ result.daemonOwner.commandIncludesInstalledCli === true,
2274
+ ];
2275
+ if (!required.every(Boolean)) {
2276
+ writeFileSync(output, JSON.stringify({ ...result, result: "FAIL" }, null, 2) + "\n");
2277
+ throw new Error("installed-component probe failed required assertions");
2278
+ }
2279
+ writeFileSync(output, JSON.stringify(result, null, 2) + "\n");
2280
+
2281
+ function diagnostic(message) {
2282
+ return {
2283
+ range: { start: { line: 0, character: 6 }, end: { line: 0, character: 18 } },
2284
+ message,
2285
+ code: 2304,
2286
+ severity: 1,
2287
+ };
2288
+ }
2289
+
2290
+ function validateManifest() {
2291
+ const parsed = JSON.parse(readFileSync(manifestPath, "utf8"));
2292
+ const outputs = Array.isArray(parsed.outputs) ? parsed.outputs : [];
2293
+ const sorted = outputs.map((entry) => entry.path).join("\n") === outputs.map((entry) => entry.path).sort().join("\n");
2294
+ const hashesMatch = outputs.every((entry) => {
2295
+ const filePath = join(componentRoot, "dist", entry.path);
2296
+ return existsSync(filePath) && sha256(readFileSync(filePath)) === entry.sha256;
2297
+ });
2298
+ return {
2299
+ valid: parsed.schemaVersion === 1 && /^sha256:[a-f0-9]{64}$/u.test(parsed.inputDigest) && sorted && hashesMatch,
2300
+ schemaVersion: parsed.schemaVersion,
2301
+ inputDigest: parsed.inputDigest,
2302
+ outputCount: outputs.length,
2303
+ sortedOutputs: sorted,
2304
+ outputHashesMatch: hashesMatch,
2305
+ };
2306
+ }
2307
+
2308
+ function validateDistOnlyRuntime() {
2309
+ const distOnlyRoot = join(sandboxRoot, "dist-only-lsp-component");
2310
+ rmSync(distOnlyRoot, { recursive: true, force: true });
2311
+ mkdirSync(join(distOnlyRoot, "scripts"), { recursive: true });
2312
+ cpSync(join(componentRoot, "dist"), join(distOnlyRoot, "dist"), { recursive: true });
2313
+ cpSync(join(componentRoot, "scripts", "build-runtime.mjs"), join(distOnlyRoot, "scripts", "build-runtime.mjs"));
2314
+ cpSync(join(componentRoot, "package.json"), join(distOnlyRoot, "package.json"));
2315
+ const run = spawnSync(process.execPath, ["scripts/build-runtime.mjs"], {
2316
+ cwd: distOnlyRoot,
2317
+ env: { ...process.env, NODE_PATH: "" },
2318
+ encoding: "utf8",
2319
+ timeout: 15000,
2320
+ });
2321
+ return { ok: run.status === 0, status: run.status, stderr: tail(run.stderr) };
2322
+ }
2323
+
2324
+ function scanRuntimeImports() {
2325
+ const source = readFileSync(cliPath, "utf8");
2326
+ const builtin = new Set([...builtinModules, ...builtinModules.map((name) => `node:${name}`)]);
2327
+ const imports = [
2328
+ ...source.matchAll(/\bfrom\s+["']([^"']+)["']/gu),
2329
+ ...source.matchAll(/\bimport\s*\(\s*["']([^"']+)["']\s*\)/gu),
2330
+ ].map((match) => match[1]);
2331
+ return { imports, nonBuiltin: imports.filter((specifier) => !builtin.has(specifier)) };
2332
+ }
2333
+
2334
+ function runPostEditProbe() {
2335
+ const baseInput = {
2336
+ cwd: projectDir,
2337
+ hook_event_name: "PostToolUse",
2338
+ model: "gpt-5.5",
2339
+ permission_mode: "default",
2340
+ session_id: "installed-post-edit",
2341
+ tool_input: { path: "source.ts" },
2342
+ tool_name: "write",
2343
+ tool_response: { ok: true },
2344
+ tool_use_id: "tool-use-1",
2345
+ turn_id: "turn-1",
2346
+ };
2347
+ const first = runHook({ ...baseInput, transcript_path: null });
2348
+ const second = runHook({ ...baseInput, transcript_path: transcriptPath });
2349
+ const firstParsed = JSON.parse(first.stdout || "{}");
2350
+ const secondParsed = JSON.parse(second.stdout || "{}");
2351
+ const events = readEvents();
2352
+ return {
2353
+ emptyNodePath: first.emptyNodePath && second.emptyNodePath,
2354
+ actualPostEditOperation: events.some(
2355
+ (event) =>
2356
+ (event.type === "clientRequest" && event.method === "textDocument/diagnostic") ||
2357
+ (event.type === "clientNotification" && event.method === "textDocument/didOpen"),
2358
+ ),
2359
+ projectConfigConsumed: events.some(
2360
+ (event) => event.type === "clientRequest" && event.method === "initialize" && event.params?.rootUri === pathToFileURL(realpathSync(projectDir)).href,
2361
+ ),
2362
+ defaultReasonLength: firstParsed.reason?.length ?? null,
2363
+ contextPressureReasonLength: secondParsed.reason?.length ?? null,
2364
+ defaultBudgetWithinLimit: typeof firstParsed.reason === "string" && firstParsed.reason.length <= 8000 && firstParsed.reason.includes("Truncated hook output to 8000 chars"),
2365
+ contextPressureBudgetWithinLimit: typeof secondParsed.reason === "string" && secondParsed.reason.length <= 1200 && secondParsed.reason.includes("Truncated hook output to 1200 chars"),
2366
+ stdoutContainsDiagnostic: first.stdout.includes("installed-post-edit"),
2367
+ statusCodes: [first.status, second.status],
2368
+ stderrTails: [tail(first.stderr), tail(second.stderr)],
2369
+ };
2370
+ }
2371
+
2372
+ function runMcpDiagnosticsProbe() {
2373
+ const request = [
2374
+ { jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: "2024-11-05" } },
2375
+ { jsonrpc: "2.0", id: 2, method: "tools/call", params: { name: "lsp_diagnostics", arguments: { filePath: "source.ts" } } },
2376
+ ].map((message) => JSON.stringify(message)).join("\n") + "\n";
2377
+ const run = spawnSync(process.execPath, [cliPath, "mcp"], {
2378
+ cwd: projectDir,
2379
+ input: request,
2380
+ env: cleanEnv(),
2381
+ encoding: "utf8",
2382
+ timeout: 60000,
2383
+ maxBuffer: 1024 * 1024 * 4,
2384
+ });
2385
+ const responses = run.stdout.split("\n").filter(Boolean).map((line) => {
2386
+ try {
2387
+ return JSON.parse(line);
2388
+ } catch {
2389
+ return { parseError: line };
2390
+ }
2391
+ });
2392
+ const diagnosticResponse = responses.find((entry) => entry?.id === 2);
2393
+ const text = diagnosticResponse?.result?.content?.map((entry) => entry?.text ?? "").join("\n") ?? "";
2394
+ return {
2395
+ status: run.status,
2396
+ ok: run.status === 0 && diagnosticResponse?.result?.isError !== true && text.includes("installed-post-edit"),
2397
+ textTail: tail(text),
2398
+ stderrTail: tail(run.stderr),
2399
+ responseCount: responses.length,
2400
+ diagnosticResponse,
2401
+ };
2402
+ }
2403
+
2404
+ function runCompactionProbe() {
2405
+ const sessionId = "installed-compact-session";
2406
+ const input = {
2407
+ cwd: projectDir,
2408
+ hook_event_name: "PostToolUse",
2409
+ model: "gpt-5.5",
2410
+ permission_mode: "default",
2411
+ session_id: sessionId,
2412
+ tool_input: { path: "README.md" },
2413
+ tool_name: "write",
2414
+ tool_response: { ok: true },
2415
+ tool_use_id: "tool-use-2",
2416
+ transcript_path: null,
2417
+ turn_id: "turn-2",
2418
+ };
2419
+ const first = runHook(input);
2420
+ const statePath = join(pluginData, "sessions", `${sessionId}.json`);
2421
+ const before = JSON.parse(readFileSync(statePath, "utf8"));
2422
+ const compact = spawnSync(process.execPath, [cliPath, "hook", "post-compact"], {
2423
+ cwd: projectDir,
2424
+ input: JSON.stringify({ session_id: sessionId }) + "\n",
2425
+ env: cleanEnv(),
2426
+ encoding: "utf8",
2427
+ timeout: 30000,
2428
+ });
2429
+ const after = JSON.parse(readFileSync(statePath, "utf8"));
2430
+ return {
2431
+ emptyNodePath: true,
2432
+ firstStatus: first.status,
2433
+ compactStatus: compact.status,
2434
+ cacheStored: Array.isArray(before.notConfiguredExtensions) && before.notConfiguredExtensions.includes(".md"),
2435
+ compactionReset: Array.isArray(after.notConfiguredExtensions) && after.notConfiguredExtensions.length === 0,
2436
+ stdoutTails: [tail(first.stdout), tail(compact.stdout)],
2437
+ stderrTails: [tail(first.stderr), tail(compact.stderr)],
2438
+ };
2439
+ }
2440
+
2441
+ function runOverrideValidation() {
2442
+ const singleton = spawnSync(process.execPath, [cliPath, "mcp"], {
2443
+ cwd: projectDir,
2444
+ input: "",
2445
+ env: cleanEnv({ OMO_LSP_DAEMON_CLI: daemonCliPath }, false),
2446
+ encoding: "utf8",
2447
+ timeout: 5000,
2448
+ });
2449
+ const nonexistent = spawnSync(process.execPath, [cliPath, "mcp"], {
2450
+ cwd: projectDir,
2451
+ input: "",
2452
+ env: cleanEnv({
2453
+ OMO_LSP_DAEMON_CLI: join(sandboxRoot, "missing-daemon-cli.js"),
2454
+ OMO_LSP_DAEMON_VERSION: "999.0.0",
2455
+ }, false),
2456
+ encoding: "utf8",
2457
+ timeout: 5000,
2458
+ });
2459
+ return {
2460
+ singletonRejected: singleton.status !== 0 && singleton.stderr.includes("must be set together"),
2461
+ nonexistentPairRejected: nonexistent.status !== 0 && singleton.status !== 0 && nonexistent.stderr.includes("points to a missing file"),
2462
+ singletonStatus: singleton.status,
2463
+ nonexistentStatus: nonexistent.status,
2464
+ singletonStderr: tail(singleton.stderr),
2465
+ nonexistentStderr: tail(nonexistent.stderr),
2466
+ };
2467
+ }
2468
+
2469
+ function inspectDaemonOwner() {
2470
+ const pidFiles = [];
2471
+ collectPidFiles(daemonDir, pidFiles, readdirSync);
2472
+ const command = pidFiles.length === 1 ? psCommand(readFileSync(pidFiles[0], "utf8").trim()) : "";
2473
+ return {
2474
+ count: pidFiles.length,
2475
+ pidFiles: pidFiles.map((path) => path.slice(daemonDir.length + 1)),
2476
+ commandIncludesInstalledCli: command.includes(daemonCliPath) && command.includes(" daemon"),
2477
+ };
2478
+ }
2479
+
2480
+ function readDaemonArtifacts() {
2481
+ const files = [];
2482
+ collectDaemonFiles(daemonDir, files);
2483
+ const artifacts = {};
2484
+ for (const file of files) {
2485
+ const relative = file.slice(daemonDir.length + 1);
2486
+ artifacts[relative] = file.endsWith("/daemon.auth") ? "<redacted>" : tail(readFileSync(file, "utf8"));
2487
+ }
2488
+ return artifacts;
2489
+ }
2490
+
2491
+ function collectDaemonFiles(dir, output) {
2492
+ if (!existsSync(dir)) return;
2493
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
2494
+ const child = join(dir, entry.name);
2495
+ if (entry.isDirectory()) collectDaemonFiles(child, output);
2496
+ else if (entry.isFile() && /^daemon\.(?:log|pid|endpoint|owner|auth)$/u.test(entry.name)) output.push(child);
2497
+ }
2498
+ }
2499
+
2500
+ function collectPidFiles(dir, output, readdirSync) {
2501
+ if (!existsSync(dir)) return;
2502
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
2503
+ const child = join(dir, entry.name);
2504
+ if (entry.isDirectory()) collectPidFiles(child, output, readdirSync);
2505
+ else if (entry.isFile() && entry.name === "daemon.pid") output.push(child);
2506
+ }
2507
+ }
2508
+
2509
+ function psCommand(pid) {
2510
+ const run = spawnSync("/bin/ps", ["-p", pid, "-o", "command="], { encoding: "utf8", timeout: 5000 });
2511
+ return run.stdout.trim();
2512
+ }
2513
+
2514
+ function runHook(input) {
2515
+ const run = spawnSync(process.execPath, [cliPath, "hook", "post-tool-use"], {
2516
+ cwd: projectDir,
2517
+ input: JSON.stringify(input) + "\n",
2518
+ env: cleanEnv(),
2519
+ encoding: "utf8",
2520
+ timeout: 60000,
2521
+ maxBuffer: 1024 * 1024 * 4,
2522
+ });
2523
+ return { ...run, emptyNodePath: true };
2524
+ }
2525
+
2526
+ function cleanEnv(extra = {}, includeRuntimePair = true) {
2527
+ const env = {
2528
+ PATH: process.env.PATH ?? "",
2529
+ HOME: process.env.HOME ?? "",
2530
+ CODEX_HOME: resolve(codexHome),
2531
+ PLUGIN_DATA: pluginData,
2532
+ OMO_LSP_DAEMON_DIR: daemonDir,
2533
+ OMO_DISABLE_POSTHOG: "1",
2534
+ OMO_CODEX_DISABLE_POSTHOG: "1",
2535
+ NODE_PATH: "",
2536
+ };
2537
+ for (const key of ["TMPDIR", "TMP", "TEMP"]) {
2538
+ if (process.env[key] !== undefined) env[key] = process.env[key];
2539
+ }
2540
+ if (includeRuntimePair) {
2541
+ env.OMO_LSP_DAEMON_CLI = daemonCliPath;
2542
+ env.OMO_LSP_DAEMON_VERSION = daemonVersion;
2543
+ }
2544
+ Object.assign(env, extra);
2545
+ for (const key of Object.keys(env)) {
2546
+ if (key.startsWith("CODEX" + "_LSP_")) delete env[key];
2547
+ }
2548
+ return env;
2549
+ }
2550
+
2551
+ function readEvents() {
2552
+ return readFileSync(eventsPath, "utf8").split("\n").filter(Boolean).map((line) => JSON.parse(line));
2553
+ }
2554
+
2555
+ function sha256(buffer) {
2556
+ return createHash("sha256").update(buffer).digest("hex");
2557
+ }
2558
+
2559
+ function tail(text) {
2560
+ return String(text ?? "").split("\n").slice(-20).join("\n");
2561
+ }
2562
+ NODE
2563
+ REPO_ROOT="$REPO_ROOT" INSTALLED_PLUGIN_ROOT="$plugin_root" SANDBOX_ROOT="$SANDBOX_ROOT" \
2564
+ INSTALLED_COMPONENT_RESULT="$EVIDENCE_DIR/installed-component.json" \
2565
+ run_bounded 180 "$EVIDENCE_DIR/installed-component.log" node "$script"
2566
+ }
2567
+
2568
+ write_final_result() {
2569
+ local real_omo_before="$1" real_omo_after="$2" real_codex_before="$3" real_codex_after="$4"
2570
+ local worktree_before="$5" worktree_after="$6" sandbox_removed="$7"
2571
+ RESULT_STAGE="$EVIDENCE_DIR/.result.json.$$"
2572
+ node --input-type=module - \
2573
+ "$RESULT_STAGE" "$SCENARIO" "$real_omo_before" "$real_omo_after" "$real_codex_before" "$real_codex_after" \
2574
+ "$worktree_before" "$worktree_after" "$sandbox_removed" \
2575
+ "$EVIDENCE_DIR/path-contract.json" "$EVIDENCE_DIR/app-server.json" "$EVIDENCE_DIR/daemon-state.json" \
2576
+ "$EVIDENCE_DIR/workspace-edit-contract.json" "$EVIDENCE_DIR/rename-fixture.json" "$EVIDENCE_DIR/rename-server-events.jsonl" \
2577
+ "$EVIDENCE_DIR/diagnostics-freshness-contract.json" "$EVIDENCE_DIR/diagnostics-freshness-fixture.json" \
2578
+ "$EVIDENCE_DIR/legacy-cleanup-contract.json" "$EVIDENCE_DIR/legacy-cleanup-live.json" "$EVIDENCE_DIR/legacy-cleanup-process-cleanup.txt" \
2579
+ "$EVIDENCE_DIR/post-edit-contract.json" "$EVIDENCE_DIR/cancellation-contract.json" "$EVIDENCE_DIR/package-smoke.json" \
2580
+ "$EVIDENCE_DIR/installed-component.json" <<'NODE'
2581
+ import { readFileSync, writeFileSync } from "node:fs";
2582
+ const [
2583
+ output,
2584
+ scenario,
2585
+ omoBefore,
2586
+ omoAfter,
2587
+ codexBefore,
2588
+ codexAfter,
2589
+ worktreeBefore,
2590
+ worktreeAfter,
2591
+ sandboxRemoved,
2592
+ contractPath,
2593
+ appPath,
2594
+ daemonPath,
2595
+ workspaceContractPath,
2596
+ renameFixturePath,
2597
+ renameEventsPath,
2598
+ freshnessContractPath,
2599
+ freshnessFixturePath,
2600
+ legacyContractPath,
2601
+ legacyLivePath,
2602
+ legacyProcessCleanupPath,
2603
+ postEditContractPath,
2604
+ cancellationContractPath,
2605
+ clientPackagePath,
2606
+ installedComponentPath,
2607
+ ] = process.argv.slice(2);
2608
+ const contract = JSON.parse(readFileSync(contractPath, "utf8"));
2609
+ const app = JSON.parse(readFileSync(appPath, "utf8"));
2610
+ const daemon = JSON.parse(readFileSync(daemonPath, "utf8"));
2611
+ const workspaceContract = scenario === "rename" ? JSON.parse(readFileSync(workspaceContractPath, "utf8")) : null;
2612
+ const renameFixture = scenario === "rename" ? JSON.parse(readFileSync(renameFixturePath, "utf8")) : null;
2613
+ const renameEvents = scenario === "rename"
2614
+ ? readFileSync(renameEventsPath, "utf8").split("\n").filter(Boolean).map((line) => JSON.parse(line))
2615
+ : [];
2616
+ const freshnessContract = scenario === "diagnostics-freshness" ? JSON.parse(readFileSync(freshnessContractPath, "utf8")) : null;
2617
+ const freshnessFixture = scenario === "diagnostics-freshness" ? JSON.parse(readFileSync(freshnessFixturePath, "utf8")) : null;
2618
+ const legacyContract = scenario === "legacy-cleanup" ? JSON.parse(readFileSync(legacyContractPath, "utf8")) : null;
2619
+ const legacyLive = scenario === "legacy-cleanup" ? JSON.parse(readFileSync(legacyLivePath, "utf8")) : null;
2620
+ const legacyProcessCleanup = scenario === "legacy-cleanup" ? readFileSync(legacyProcessCleanupPath, "utf8") : null;
2621
+ const postEditContract = scenario === "post-edit" ? JSON.parse(readFileSync(postEditContractPath, "utf8")) : null;
2622
+ const cancellationContract = scenario === "cancellation" ? JSON.parse(readFileSync(cancellationContractPath, "utf8")) : null;
2623
+ const clientPackage = scenario === "client-package" ? JSON.parse(readFileSync(clientPackagePath, "utf8")) : null;
2624
+ const installedComponent = scenario === "installed-component" ? JSON.parse(readFileSync(installedComponentPath, "utf8")) : null;
2625
+ const result = {
2626
+ result: "PASS",
2627
+ scenario,
2628
+ harness: "codex",
2629
+ realOmoRootUnchanged: omoBefore === omoAfter,
2630
+ realCodexConfigUnchanged: codexBefore === codexAfter,
2631
+ dirtyWorktreePreserved: worktreeBefore === worktreeAfter,
2632
+ isolatedCodexHome: true,
2633
+ localMockModel: true,
2634
+ pluginOnly: true,
2635
+ disabledSideEffectHooks: [
2636
+ "session-start-checking-auto-update.json",
2637
+ "session-start-checking-bootstrap-provisioning.json",
2638
+ "session-start-checking-codegraph-bootstrap.json",
2639
+ ],
2640
+ hookStarted: app.hookStarted === true,
2641
+ hookCompleted: app.hookCompleted === true,
2642
+ lspOperation: app.lspOperation,
2643
+ ...(scenario === "rename"
2644
+ ? {
2645
+ diagnosticsOperation: app.diagnosticsOperation,
2646
+ serverAppliedRename: app.renameHarness?.serverApplyRequestCount === 1 && app.renameHarness?.applyResponses?.[0]?.applied === true,
2647
+ recordedResultReused: workspaceContract?.success?.recordedResultReused === true,
2648
+ synchronizedDocumentVersion: workspaceContract?.success?.synchronizedDocumentVersion ?? null,
2649
+ didChangeVersions: app.renameHarness?.didChangeVersions ?? [],
2650
+ immediateDiagnostics: workspaceContract?.success?.immediateDiagnostics === true && app.diagnosticsOperation?.ok === true,
2651
+ finalContent: app.renameHarness?.finalContent ?? null,
2652
+ failureHashes: workspaceContract?.failureHashes ?? {},
2653
+ failureEvidence: workspaceContract?.failureCases ?? {},
2654
+ renameFixture,
2655
+ renameEventCount: renameEvents.length,
2656
+ }
2657
+ : scenario === "diagnostics-freshness"
2658
+ ? {
2659
+ diagnosticsFreshnessProbe: freshnessContract,
2660
+ diagnosticsFreshnessFixture: freshnessFixture,
2661
+ }
2662
+ : scenario === "legacy-cleanup"
2663
+ ? {
2664
+ legacyCleanup: {
2665
+ exactVectors: legacyLive?.vectors ?? null,
2666
+ contract: legacyContract,
2667
+ live: legacyLive,
2668
+ cleanupReceipt: legacyProcessCleanup,
2669
+ },
2670
+ }
2671
+ : scenario === "post-edit"
2672
+ ? {
2673
+ postEditContract,
2674
+ }
2675
+ : scenario === "cancellation"
2676
+ ? {
2677
+ cancellationContract,
2678
+ }
2679
+ : scenario === "client-package"
2680
+ ? {
2681
+ clientPackage,
2682
+ }
2683
+ : scenario === "installed-component"
2684
+ ? {
2685
+ installedComponent,
2686
+ }
2687
+ : {}),
2688
+ resolvedBase: daemon.base,
2689
+ resolvedVersion: daemon.version,
2690
+ resolvedVersionDir: daemon.versionDir,
2691
+ overrideAssertions: contract.assertions,
2692
+ failureFixtures: contract.failures,
2693
+ realOmoRootHashBefore: omoBefore,
2694
+ realOmoRootHashAfter: omoAfter,
2695
+ cleanup: {
2696
+ daemonStopped: true,
2697
+ appServerStopped: true,
2698
+ mockModelStopped: true,
2699
+ isolatedStateRemoved: sandboxRemoved === "true",
2700
+ },
2701
+ artifacts: {
2702
+ invocation: "invocation.txt",
2703
+ pathContract: "path-contract.json",
2704
+ appServer: "app-server.json",
2705
+ daemonState: "daemon-state.json",
2706
+ workspaceEditContract: scenario === "rename" ? "workspace-edit-contract.json" : undefined,
2707
+ renameFixture: scenario === "rename" ? "rename-fixture.json" : undefined,
2708
+ renameServerEvents: scenario === "rename" ? "rename-server-events.jsonl" : undefined,
2709
+ diagnosticsFreshnessContract: scenario === "diagnostics-freshness" ? "diagnostics-freshness-contract.json" : undefined,
2710
+ diagnosticsFreshnessFixture: scenario === "diagnostics-freshness" ? "diagnostics-freshness-fixture.json" : undefined,
2711
+ legacyCleanupContract: scenario === "legacy-cleanup" ? "legacy-cleanup-contract.json" : undefined,
2712
+ legacyCleanupLive: scenario === "legacy-cleanup" ? "legacy-cleanup-live.json" : undefined,
2713
+ legacyCleanupProcessCleanup: scenario === "legacy-cleanup" ? "legacy-cleanup-process-cleanup.txt" : undefined,
2714
+ postEditContract: scenario === "post-edit" ? "post-edit-contract.json" : undefined,
2715
+ cancellationContract: scenario === "cancellation" ? "cancellation-contract.json" : undefined,
2716
+ clientPackage: scenario === "client-package" ? "package-smoke.json" : undefined,
2717
+ installedComponent: scenario === "installed-component" ? "installed-component.json" : undefined,
2718
+ installLog: "install.log",
2719
+ cleanupReceipt: "cleanup-receipt.txt",
2720
+ },
2721
+ };
2722
+ const required = [
2723
+ result.realOmoRootUnchanged,
2724
+ result.realCodexConfigUnchanged,
2725
+ result.dirtyWorktreePreserved,
2726
+ result.hookStarted,
2727
+ result.hookCompleted,
2728
+ result.lspOperation?.ok === true,
2729
+ result.cleanup.isolatedStateRemoved,
2730
+ ...Object.values(result.overrideAssertions),
2731
+ ];
2732
+ if (scenario === "rename") {
2733
+ required.push(
2734
+ result.diagnosticsOperation?.ok === true,
2735
+ result.serverAppliedRename === true,
2736
+ result.recordedResultReused === true,
2737
+ result.synchronizedDocumentVersion === 2,
2738
+ JSON.stringify(result.didChangeVersions) === JSON.stringify([2]),
2739
+ result.immediateDiagnostics === true,
2740
+ result.finalContent === "const after = 1;\n",
2741
+ typeof result.failureHashes?.unscoped === "string" && result.failureHashes.unscoped.length === 64,
2742
+ typeof result.failureHashes?.concurrent === "string" && result.failureHashes.concurrent.length === 64,
2743
+ typeof result.failureHashes?.mismatched === "string" && result.failureHashes.mismatched.length === 64,
2744
+ typeof result.failureHashes?.preGate === "string" && result.failureHashes.preGate.length === 64,
2745
+ );
2746
+ }
2747
+ if (scenario === "diagnostics-freshness") {
2748
+ required.push(
2749
+ freshnessContract?.outcomes?.exactCurrent?.ok === true,
2750
+ freshnessContract?.outcomes?.postGenerationVersionless?.ok === true,
2751
+ freshnessContract?.outcomes?.stale?.ok === true,
2752
+ freshnessContract?.outcomes?.future?.ok === true,
2753
+ freshnessContract?.outcomes?.pullOvertaken?.ok === true,
2754
+ freshnessContract?.outcomes?.silent?.ok === true,
2755
+ freshnessContract?.outcomes?.closedServer?.ok === true,
2756
+ freshnessContract?.outcomes?.unsupportedPull?.ok === true,
2757
+ freshnessContract?.outcomes?.sameVersionUnchanged?.ok === true,
2758
+ );
2759
+ }
2760
+ if (scenario === "legacy-cleanup") {
2761
+ required.push(
2762
+ legacyContract?.assertions?.windowsDeferred === true,
2763
+ legacyContract?.assertions?.noAttestationDeferred === true,
2764
+ legacyContract?.assertions?.timeoutDeferred === true,
2765
+ legacyContract?.assertions?.staleRemoved === true,
2766
+ legacyContract?.assertions?.symlinkRemovedWithoutFollowing === true,
2767
+ legacyContract?.assertions?.malformedRemoved === true,
2768
+ legacyLive?.results?.ownedTerminated === true,
2769
+ legacyLive?.results?.staleNaturalRemoved === true,
2770
+ legacyLive?.results?.staleHashedRemoved === true,
2771
+ legacyLive?.results?.malformedRemoved === true,
2772
+ legacyLive?.results?.foreignOwnerPreserved === true,
2773
+ legacyLive?.results?.timeoutPreserved === true,
2774
+ legacyLive?.results?.unrelatedPidNotSignaled === true,
2775
+ legacyLive?.results?.visibleInstallerWarning === true,
2776
+ legacyLive?.results?.noLiveIpcCopy === true,
2777
+ typeof legacyProcessCleanup === "string" && legacyProcessCleanup.includes("alive_after=no"),
2778
+ );
2779
+ }
2780
+ if (scenario === "post-edit") {
2781
+ required.push(
2782
+ postEditContract?.result === "PASS",
2783
+ postEditContract?.assertions?.explicitTranslatorOutputs === true,
2784
+ postEditContract?.assertions?.translatorDefaults === true,
2785
+ postEditContract?.assertions?.directAdapterNonUse === true,
2786
+ postEditContract?.assertions?.maxConcurrencyFour === true,
2787
+ postEditContract?.assertions?.orderedBlocks === true,
2788
+ postEditContract?.assertions?.duplicatesRunOnce === true,
2789
+ postEditContract?.assertions?.cacheResetRetry === true,
2790
+ postEditContract?.assertions?.rejectionBeforeLookup === true,
2791
+ );
2792
+ }
2793
+ if (scenario === "cancellation") {
2794
+ required.push(
2795
+ cancellationContract?.result === "PASS",
2796
+ cancellationContract?.callerAbort?.daemonProxyRequestId === cancellationContract?.callerAbort?.daemonCancelTarget,
2797
+ cancellationContract?.callerAbort?.lspRequestId === cancellationContract?.callerAbort?.lspCancelTarget,
2798
+ cancellationContract?.noLeftovers?.daemonActiveControllersAfter === 0,
2799
+ cancellationContract?.noLeftovers?.lspPendingRequestsAfter === 0,
2800
+ cancellationContract?.delayedRenamePreCommitGate?.zeroWrites === true,
2801
+ cancellationContract?.delayedRenamePreCommitGate?.preservesBeforeHash === true,
2802
+ cancellationContract?.cancellationAfterCommitGate?.mutationCount === 1,
2803
+ cancellationContract?.cancellationAfterCommitGate?.lateAbort === true,
2804
+ cancellationContract?.readOnlyPreWriteConnectionFailureRetry?.retryCount === 1,
2805
+ cancellationContract?.authProtocolCwd?.tokenLoggedOrForwarded === false,
2806
+ );
2807
+ }
2808
+ if (scenario === "client-package") {
2809
+ required.push(
2810
+ clientPackage?.result === "PASS",
2811
+ clientPackage?.build?.requiredOutputs?.clientJs === true,
2812
+ clientPackage?.build?.requiredOutputs?.clientDts === true,
2813
+ clientPackage?.build?.requiredOutputs?.cliJs === true,
2814
+ clientPackage?.build?.requiredOutputs?.indexJs === true,
2815
+ clientPackage?.build?.staleDistRemoved === true,
2816
+ clientPackage?.packageJson?.hasOnlyClientAndCliExports === true,
2817
+ clientPackage?.scans?.clientJsNoWorkspaceDeps === true,
2818
+ clientPackage?.scans?.clientDtsNoWorkspaceDeps === true,
2819
+ clientPackage?.scans?.noRepositoryPathCoupling === true,
2820
+ clientPackage?.consumer?.emptyNodePath === true,
2821
+ clientPackage?.consumer?.js?.statusOk === true,
2822
+ clientPackage?.consumer?.js?.typedContextForwarded === true,
2823
+ clientPackage?.consumer?.js?.cancellation?.accepted === true,
2824
+ clientPackage?.consumer?.js?.rootImport?.rejected === true,
2825
+ clientPackage?.consumer?.js?.unknownImport?.rejected === true,
2826
+ clientPackage?.consumer?.js?.deepImport?.rejected === true,
2827
+ Array.isArray(clientPackage?.consumer?.js?.serverSymbols) && clientPackage.consumer.js.serverSymbols.length === 0,
2828
+ clientPackage?.consumer?.tscExitCode === 0,
2829
+ clientPackage?.adversarial?.repositoryHiddenByInstall === true,
2830
+ );
2831
+ }
2832
+ if (scenario === "installed-component") {
2833
+ required.push(
2834
+ installedComponent?.result === "PASS",
2835
+ installedComponent?.manifest?.valid === true,
2836
+ installedComponent?.manifest?.sortedOutputs === true,
2837
+ installedComponent?.manifest?.outputHashesMatch === true,
2838
+ installedComponent?.distOnlyValidation?.ok === true,
2839
+ installedComponent?.componentCli?.exists === true,
2840
+ installedComponent?.componentCli?.noNonBuiltinRuntimeImports === true,
2841
+ installedComponent?.consumer?.emptyNodePath === true,
2842
+ installedComponent?.consumer?.repositoryHiddenByInstall === true,
2843
+ typeof installedComponent?.codexContext?.cwd === "string" &&
2844
+ installedComponent.codexContext.cwd.endsWith("/project"),
2845
+ installedComponent?.codexContext?.projectConfigPaths?.[0] ===
2846
+ `${installedComponent?.codexContext?.cwd}/.codex/lsp-client.json`,
2847
+ installedComponent?.codexContext?.userConfigPath?.endsWith("/codex/lsp-client.json") === true,
2848
+ installedComponent?.codexContext?.installDecisionsPath?.endsWith("/codex/lsp-install-decisions.json") === true,
2849
+ installedComponent?.codexContext?.capabilities?.installDecisionTool === true,
2850
+ installedComponent?.postEdit?.actualPostEditOperation === true,
2851
+ installedComponent?.postEdit?.projectConfigConsumed === true,
2852
+ installedComponent?.postEdit?.defaultBudgetWithinLimit === true,
2853
+ installedComponent?.postEdit?.contextPressureBudgetWithinLimit === true,
2854
+ installedComponent?.compaction?.cacheStored === true,
2855
+ installedComponent?.compaction?.compactionReset === true,
2856
+ installedComponent?.validation?.singletonRejected === true,
2857
+ installedComponent?.validation?.nonexistentPairRejected === true,
2858
+ installedComponent?.daemonOwner?.count === 1,
2859
+ installedComponent?.daemonOwner?.commandIncludesInstalledCli === true,
2860
+ );
2861
+ }
2862
+ if (!required.every(Boolean)) throw new Error("refusing to write PASS result with failed assertions");
2863
+ writeFileSync(output, JSON.stringify(result, null, 2) + "\n");
2864
+ NODE
2865
+ if [ "$SCENARIO" = "rename" ]; then
2866
+ jq -e --arg scenario "$SCENARIO" \
2867
+ '.result == "PASS"
2868
+ and .scenario == $scenario
2869
+ and .realOmoRootUnchanged == true
2870
+ and .lspOperation.ok == true
2871
+ and .diagnosticsOperation.ok == true
2872
+ and .serverAppliedRename == true
2873
+ and .recordedResultReused == true
2874
+ and .synchronizedDocumentVersion == 2
2875
+ and .immediateDiagnostics == true
2876
+ and (.failureHashes | keys | sort) == ["concurrent","mismatched","preGate","unscoped"]' \
2877
+ "$RESULT_STAGE" >/dev/null || return 1
2878
+ elif [ "$SCENARIO" = "diagnostics-freshness" ]; then
2879
+ jq -e --arg scenario "$SCENARIO" \
2880
+ '.result == "PASS"
2881
+ and .scenario == $scenario
2882
+ and .realOmoRootUnchanged == true
2883
+ and .lspOperation.ok == true
2884
+ and .diagnosticsFreshnessProbe.outcomes.exactCurrent.ok == true
2885
+ and .diagnosticsFreshnessProbe.outcomes.postGenerationVersionless.ok == true
2886
+ and .diagnosticsFreshnessProbe.outcomes.stale.ok == true
2887
+ and .diagnosticsFreshnessProbe.outcomes.future.ok == true
2888
+ and .diagnosticsFreshnessProbe.outcomes.pullOvertaken.ok == true
2889
+ and .diagnosticsFreshnessProbe.outcomes.silent.ok == true
2890
+ and .diagnosticsFreshnessProbe.outcomes.closedServer.ok == true
2891
+ and .diagnosticsFreshnessProbe.outcomes.unsupportedPull.ok == true
2892
+ and .diagnosticsFreshnessProbe.outcomes.sameVersionUnchanged.ok == true' \
2893
+ "$RESULT_STAGE" >/dev/null || return 1
2894
+ elif [ "$SCENARIO" = "legacy-cleanup" ]; then
2895
+ jq -e --arg scenario "$SCENARIO" \
2896
+ '.result == "PASS"
2897
+ and .scenario == $scenario
2898
+ and .realOmoRootUnchanged == true
2899
+ and .lspOperation.ok == true
2900
+ and .legacyCleanup.contract.assertions.windowsDeferred == true
2901
+ and .legacyCleanup.contract.assertions.noAttestationDeferred == true
2902
+ and .legacyCleanup.contract.assertions.timeoutDeferred == true
2903
+ and .legacyCleanup.contract.assertions.staleRemoved == true
2904
+ and .legacyCleanup.contract.assertions.symlinkRemovedWithoutFollowing == true
2905
+ and .legacyCleanup.contract.assertions.malformedRemoved == true
2906
+ and .legacyCleanup.live.results.ownedTerminated == true
2907
+ and .legacyCleanup.live.results.foreignOwnerPreserved == true
2908
+ and .legacyCleanup.live.results.timeoutPreserved == true
2909
+ and .legacyCleanup.live.results.unrelatedPidNotSignaled == true
2910
+ and .legacyCleanup.live.results.visibleInstallerWarning == true
2911
+ and .legacyCleanup.live.results.noLiveIpcCopy == true' \
2912
+ "$RESULT_STAGE" >/dev/null || return 1
2913
+ elif [ "$SCENARIO" = "post-edit" ]; then
2914
+ jq -e --arg scenario "$SCENARIO" \
2915
+ '.result == "PASS"
2916
+ and .scenario == $scenario
2917
+ and .realOmoRootUnchanged == true
2918
+ and .lspOperation.ok == true
2919
+ and .postEditContract.result == "PASS"
2920
+ and .postEditContract.assertions.openCodeMcpEnvInputs == true
2921
+ and .postEditContract.assertions.explicitTranslatorOutputs == true
2922
+ and .postEditContract.assertions.translatorDefaults == true
2923
+ and .postEditContract.assertions.directAdapterNonUse == true
2924
+ and .postEditContract.assertions.maxConcurrencyFour == true
2925
+ and .postEditContract.assertions.orderedBlocks == true
2926
+ and .postEditContract.assertions.duplicatesRunOnce == true
2927
+ and .postEditContract.assertions.cacheResetRetry == true
2928
+ and .postEditContract.assertions.rejectionBeforeLookup == true' \
2929
+ "$RESULT_STAGE" >/dev/null || return 1
2930
+ elif [ "$SCENARIO" = "cancellation" ]; then
2931
+ jq -e --arg scenario "$SCENARIO" \
2932
+ '.result == "PASS"
2933
+ and .scenario == $scenario
2934
+ and .realOmoRootUnchanged == true
2935
+ and .lspOperation.ok == true
2936
+ and .cancellationContract.result == "PASS"
2937
+ and .cancellationContract.callerAbort.daemonProxyRequestId == .cancellationContract.callerAbort.daemonCancelTarget
2938
+ and .cancellationContract.callerAbort.lspRequestId == .cancellationContract.callerAbort.lspCancelTarget
2939
+ and .cancellationContract.noLeftovers.daemonActiveControllersAfter == 0
2940
+ and .cancellationContract.noLeftovers.lspPendingRequestsAfter == 0
2941
+ and .cancellationContract.delayedRenamePreCommitGate.zeroWrites == true
2942
+ and .cancellationContract.cancellationAfterCommitGate.mutationCount == 1
2943
+ and .cancellationContract.authProtocolCwd.tokenLoggedOrForwarded == false' \
2944
+ "$RESULT_STAGE" >/dev/null || return 1
2945
+ elif [ "$SCENARIO" = "client-package" ]; then
2946
+ jq -e --arg scenario "$SCENARIO" \
2947
+ '.result == "PASS"
2948
+ and .scenario == $scenario
2949
+ and .realOmoRootUnchanged == true
2950
+ and .lspOperation.ok == true
2951
+ and .clientPackage.result == "PASS"
2952
+ and .clientPackage.build.requiredOutputs.clientJs == true
2953
+ and .clientPackage.build.requiredOutputs.clientDts == true
2954
+ and .clientPackage.build.requiredOutputs.cliJs == true
2955
+ and .clientPackage.build.requiredOutputs.indexJs == true
2956
+ and .clientPackage.build.staleDistRemoved == true
2957
+ and .clientPackage.packageJson.hasOnlyClientAndCliExports == true
2958
+ and .clientPackage.scans.clientJsNoWorkspaceDeps == true
2959
+ and .clientPackage.scans.clientDtsNoWorkspaceDeps == true
2960
+ and .clientPackage.scans.noRepositoryPathCoupling == true
2961
+ and .clientPackage.consumer.emptyNodePath == true
2962
+ and .clientPackage.consumer.js.statusOk == true
2963
+ and .clientPackage.consumer.js.typedContextForwarded == true
2964
+ and .clientPackage.consumer.js.cancellation.accepted == true
2965
+ and .clientPackage.consumer.js.rootImport.rejected == true
2966
+ and .clientPackage.consumer.js.unknownImport.rejected == true
2967
+ and .clientPackage.consumer.js.deepImport.rejected == true
2968
+ and (.clientPackage.consumer.js.serverSymbols | length) == 0
2969
+ and .clientPackage.consumer.tscExitCode == 0
2970
+ and .clientPackage.adversarial.repositoryHiddenByInstall == true' \
2971
+ "$RESULT_STAGE" >/dev/null || return 1
2972
+ elif [ "$SCENARIO" = "installed-component" ]; then
2973
+ jq -e --arg scenario "$SCENARIO" \
2974
+ '.result == "PASS"
2975
+ and .scenario == $scenario
2976
+ and .realOmoRootUnchanged == true
2977
+ and .realCodexConfigUnchanged == true
2978
+ and .dirtyWorktreePreserved == true
2979
+ and .hookStarted == true
2980
+ and .hookCompleted == true
2981
+ and .lspOperation.ok == true
2982
+ and .installedComponent.result == "PASS"
2983
+ and .installedComponent.manifest.valid == true
2984
+ and .installedComponent.manifest.sortedOutputs == true
2985
+ and .installedComponent.manifest.outputHashesMatch == true
2986
+ and .installedComponent.distOnlyValidation.ok == true
2987
+ and .installedComponent.componentCli.exists == true
2988
+ and .installedComponent.componentCli.noNonBuiltinRuntimeImports == true
2989
+ and .installedComponent.consumer.emptyNodePath == true
2990
+ and .installedComponent.consumer.repositoryHiddenByInstall == true
2991
+ and .installedComponent.codexContext.capabilities.installDecisionTool == true
2992
+ and .installedComponent.postEdit.actualPostEditOperation == true
2993
+ and .installedComponent.postEdit.projectConfigConsumed == true
2994
+ and .installedComponent.postEdit.defaultBudgetWithinLimit == true
2995
+ and .installedComponent.postEdit.contextPressureBudgetWithinLimit == true
2996
+ and .installedComponent.compaction.cacheStored == true
2997
+ and .installedComponent.compaction.compactionReset == true
2998
+ and .installedComponent.validation.singletonRejected == true
2999
+ and .installedComponent.validation.nonexistentPairRejected == true
3000
+ and .installedComponent.daemonOwner.count == 1
3001
+ and .installedComponent.daemonOwner.commandIncludesInstalledCli == true
3002
+ and .cleanup.isolatedStateRemoved == true' \
3003
+ "$RESULT_STAGE" >/dev/null || return 1
3004
+ else
3005
+ jq -e --arg scenario "$SCENARIO" \
3006
+ '.result == "PASS" and .scenario == $scenario and .realOmoRootUnchanged == true and .lspOperation.ok == true' \
3007
+ "$RESULT_STAGE" >/dev/null || return 1
3008
+ fi
3009
+ mv "$RESULT_STAGE" "$EVIDENCE_DIR/result.json"
3010
+ RESULT_STAGE=""
3011
+ }
3012
+
3013
+ write_auth_ownership_result() {
3014
+ local real_omo_before="$1" real_omo_after="$2" real_codex_before="$3" real_codex_after="$4"
3015
+ local worktree_before="$5" worktree_after="$6" sandbox_removed="$7"
3016
+ RESULT_STAGE="$EVIDENCE_DIR/.result.json.$$"
3017
+ node --input-type=module - \
3018
+ "$RESULT_STAGE" "$SCENARIO" "$real_omo_before" "$real_omo_after" "$real_codex_before" "$real_codex_after" \
3019
+ "$worktree_before" "$worktree_after" "$sandbox_removed" "$EVIDENCE_DIR/path-contract.json" "$EVIDENCE_DIR/auth-ownership.json" <<'NODE'
3020
+ import { readFileSync, writeFileSync } from "node:fs";
3021
+ const [output, scenario, omoBefore, omoAfter, codexBefore, codexAfter, worktreeBefore, worktreeAfter, sandboxRemoved, contractPath, authPath] = process.argv.slice(2);
3022
+ const contract = JSON.parse(readFileSync(contractPath, "utf8"));
3023
+ const auth = JSON.parse(readFileSync(authPath, "utf8"));
3024
+ const result = {
3025
+ ...auth,
3026
+ scenario,
3027
+ harness: "codex",
3028
+ realOmoRootUnchanged: omoBefore === omoAfter,
3029
+ realCodexConfigUnchanged: codexBefore === codexAfter,
3030
+ dirtyWorktreePreserved: worktreeBefore === worktreeAfter,
3031
+ isolatedCodexHome: true,
3032
+ localMockModel: false,
3033
+ pluginOnly: false,
3034
+ unchangedRealRoot: omoBefore === omoAfter,
3035
+ unchangedRealHomes: omoBefore === omoAfter && codexBefore === codexAfter,
3036
+ overrideAssertions: contract.assertions,
3037
+ cleanup: {
3038
+ daemonStopped: true,
3039
+ isolatedStateRemoved: sandboxRemoved === "true",
3040
+ },
3041
+ artifacts: {
3042
+ invocation: "invocation.txt",
3043
+ pathContract: "path-contract.json",
3044
+ authOwnership: "auth-ownership.json",
3045
+ cleanupReceipt: "cleanup-receipt.txt",
3046
+ },
3047
+ };
3048
+ const required = [
3049
+ result.result === "PASS",
3050
+ result.firstStartNoDeadlock === true,
3051
+ result.owner && typeof result.owner.pid === "number" && typeof result.owner.nonce === "string",
3052
+ result.tokenPresent === true,
3053
+ result.tokenLeaked === false,
3054
+ result.losingCandidateExit === 0,
3055
+ result.twoConfinedContexts === true,
3056
+ result.badAuthPreDispatchRejection === true,
3057
+ result.liveOwnerDeferral === true,
3058
+ result.deadOwnerCleanup === true,
3059
+ result.staleCloseSurvival === true,
3060
+ result.windowsTokenRequired === true,
3061
+ result.realOmoRootUnchanged,
3062
+ result.realCodexConfigUnchanged,
3063
+ result.dirtyWorktreePreserved,
3064
+ result.cleanup.isolatedStateRemoved,
3065
+ ...Object.values(result.overrideAssertions),
3066
+ ];
3067
+ if (!required.every(Boolean)) throw new Error("refusing to write auth-ownership PASS result with failed assertions");
3068
+ writeFileSync(output, JSON.stringify(result, null, 2) + "\n");
3069
+ NODE
3070
+ jq -e --arg scenario "$SCENARIO" \
3071
+ '.result == "PASS"
3072
+ and .scenario == $scenario
3073
+ and .firstStartNoDeadlock == true
3074
+ and (.owner.pid | type) == "number"
3075
+ and (.owner.nonce | type) == "string"
3076
+ and .tokenPresent == true
3077
+ and .tokenLeaked == false
3078
+ and .losingCandidateExit == 0
3079
+ and .twoConfinedContexts == true
3080
+ and .badAuthPreDispatchRejection == true
3081
+ and .liveOwnerDeferral == true
3082
+ and .deadOwnerCleanup == true
3083
+ and .staleCloseSurvival == true
3084
+ and .realOmoRootUnchanged == true
3085
+ and .realCodexConfigUnchanged == true' \
3086
+ "$RESULT_STAGE" >/dev/null || return 1
3087
+ mv "$RESULT_STAGE" "$EVIDENCE_DIR/result.json"
3088
+ RESULT_STAGE=""
3089
+ }
3090
+
3091
+ run_internal_fixture() {
3092
+ case "${LSP_E2E_INTERNAL_FIXTURE:-}" in
3093
+ fake-pass)
3094
+ printf '{"result":"PASS","scenario":"%s"}\n' "$SCENARIO" >"$EVIDENCE_DIR/misleading-output.log"
3095
+ fail "seeded PASS output lacked required assertions"
3096
+ ;;
3097
+ fake-skip)
3098
+ printf '{"result":"SKIP","scenario":"%s"}\n' "$SCENARIO" >"$EVIDENCE_DIR/misleading-output.log"
3099
+ fail "SKIP is never a passing result"
3100
+ ;;
3101
+ partial)
3102
+ RESULT_STAGE="$EVIDENCE_DIR/.result.json.partial"
3103
+ printf '{"result":"PASS"' >"$RESULT_STAGE"
3104
+ return 19
3105
+ ;;
3106
+ interrupt)
3107
+ SANDBOX_ROOT="$(mktemp -d -t cqa-lsp-e2e.XXXXXX)" || return 1
3108
+ printf '%s\n' "$SANDBOX_ROOT" >"$EVIDENCE_DIR/interrupt-sandbox.txt"
3109
+ RESULT_STAGE="$EVIDENCE_DIR/.result.json.interrupt"
3110
+ printf '{"result":"PASS"' >"$RESULT_STAGE"
3111
+ while :; do sleep 1; done
3112
+ ;;
3113
+ *)
3114
+ fail "unknown internal fixture"
3115
+ ;;
3116
+ esac
3117
+ }
3118
+
3119
+ run_missing_dist_build_order_self_test() {
3120
+ local test_root="$1" saved_evidence="$EVIDENCE_DIR" saved_sandbox="$SANDBOX_ROOT"
3121
+ local ev="$test_root/missing-dist-build-order" failures=0 red_rc=0 green_rc=0 restored=false
3122
+ mkdir -p "$ev/sandbox" || return 1
3123
+ EVIDENCE_DIR="$ev"
3124
+ SANDBOX_ROOT="$ev/sandbox"
3125
+
3126
+ snapshot_daemon_dist "$SANDBOX_ROOT/daemon-dist-backup" || failures=$((failures + 1))
3127
+ rm -rf "$DAEMON_DIST_DIR" || failures=$((failures + 1))
3128
+ [ ! -e "$DAEMON_DIST_DIR/package.json" ] || failures=$((failures + 1))
3129
+
3130
+ cat >"$ev/pre-fix-path-contract-read.mjs" <<'NODE'
3131
+ import { readFileSync, writeFileSync } from "node:fs";
3132
+ import { join } from "node:path";
3133
+
3134
+ const [repoRoot, output] = process.argv.slice(2);
3135
+ const packagePath = join(repoRoot, "packages/lsp-daemon/dist/package.json");
3136
+ const version = JSON.parse(readFileSync(packagePath, "utf8")).version;
3137
+ writeFileSync(output, JSON.stringify({ result: "UNEXPECTED_PASS", version }) + "\n");
3138
+ NODE
3139
+ if run_bounded 30 "$ev/red-prebuild-failure.log" bun "$ev/pre-fix-path-contract-read.mjs" "$REPO_ROOT" "$ev/unexpected-result.json"; then
3140
+ failures=$((failures + 1))
3141
+ else
3142
+ red_rc=$?
3143
+ [ "$red_rc" -ne 0 ] || failures=$((failures + 1))
3144
+ fi
3145
+ grep -q 'ENOENT' "$ev/red-prebuild-failure.log" || failures=$((failures + 1))
3146
+ [ ! -e "$ev/result.json" ] || failures=$((failures + 1))
3147
+ [ ! -e "$ev/unexpected-result.json" ] || failures=$((failures + 1))
3148
+
3149
+ prepare_daemon_dist || failures=$((failures + 1))
3150
+ write_path_contract_probe "$SANDBOX_ROOT/contract-green" "$ev/path-contract-green.json"
3151
+ green_rc=$?
3152
+ [ "$green_rc" -eq 0 ] || failures=$((failures + 1))
3153
+ jq -e '.assertions.defaultCliPackaged == true and .assertions.defaultVersionStamped == true' \
3154
+ "$ev/path-contract-green.json" >/dev/null || failures=$((failures + 1))
3155
+
3156
+ if restore_daemon_dist; then
3157
+ restored=true
3158
+ else
3159
+ failures=$((failures + 1))
3160
+ fi
3161
+ node --input-type=module - "$ev/build-order-proof.json" "$red_rc" "$green_rc" "$restored" "$failures" <<'NODE'
3162
+ import { writeFileSync } from "node:fs";
3163
+ const [output, redRc, greenRc, restored, failures] = process.argv.slice(2);
3164
+ writeFileSync(output, JSON.stringify({
3165
+ result: failures === "0" ? "PASS" : "FAIL",
3166
+ missingDistProven: true,
3167
+ redFailedBeforeResultJson: redRc !== "0",
3168
+ redFailureLog: "red-prebuild-failure.log",
3169
+ greenBuiltDaemonDist: greenRc === "0",
3170
+ buildLog: "lsp-daemon-prebuild.log",
3171
+ pathContract: "path-contract-green.json",
3172
+ distRestored: restored === "true",
3173
+ }, null, 2) + "\n");
3174
+ NODE
3175
+
3176
+ EVIDENCE_DIR="$saved_evidence"
3177
+ SANDBOX_ROOT="$saved_sandbox"
3178
+ [ "$failures" -eq 0 ]
3179
+ }
3180
+
3181
+ run_self_test() {
3182
+ require_bins bash node jq git || return 1
3183
+ local root before_omo after_omo before_codex after_codex before_status after_status failures=0 out rc start end
3184
+ local cancellation_result_fields=true client_package_result_fields=true installed_component_result_fields=true
3185
+ local qa_dependencies_portable=true
3186
+ local fixture_run child sandbox_path attempts
3187
+ local missing_dist_build_order='{"result":"NOT_RUN"}'
3188
+ root="$(mktemp -d -t cqa-lsp-e2e.XXXXXX)" || return 1
3189
+ SANDBOX_ROOT="$root"
3190
+ before_omo="$(hash_path "$REAL_OMO_ROOT")"
3191
+ before_codex="$(hash_path "$REAL_CODEX_CONFIG")"
3192
+ before_status="$(git -C "$REPO_ROOT" status --porcelain=v1 -uall)"
3193
+
3194
+ out="$root/parser.log"
3195
+ if bash "${BASH_SOURCE[0]}" --scenario >"$out" 2>&1; then failures=$((failures + 1)); fi
3196
+ grep -q -- '--scenario requires a value' "$out" || failures=$((failures + 1))
3197
+ if bash "${BASH_SOURCE[0]}" --scenario ../bad --evidence-dir "$root/bad" >>"$out" 2>&1; then failures=$((failures + 1)); fi
3198
+ if bash "${BASH_SOURCE[0]}" --scenario ok --evidence-dir relative >>"$out" 2>&1; then failures=$((failures + 1)); fi
3199
+
3200
+ if ! verify_tracked_cancellation_probes >>"$out" 2>&1; then
3201
+ qa_dependencies_portable=false
3202
+ failures=$((failures + 1))
3203
+ fi
3204
+
3205
+ for fixture in fake-pass fake-skip; do
3206
+ local ev="$root/$fixture"
3207
+ mkdir -p "$ev"
3208
+ printf '{"result":"%s","scenario":"self-test"}\n' "$( [ "$fixture" = fake-pass ] && echo PASS || echo SKIP )" >"$ev/result.json"
3209
+ if LSP_E2E_INTERNAL_FIXTURE="$fixture" bash "${BASH_SOURCE[0]}" \
3210
+ --scenario self-test --evidence-dir "$ev" >>"$out" 2>&1; then
3211
+ failures=$((failures + 1))
3212
+ fi
3213
+ [ ! -e "$ev/result.json" ] || failures=$((failures + 1))
3214
+ done
3215
+
3216
+ start="$(date +%s)"
3217
+ if run_bounded 1 "$root/hung.log" node -e 'setTimeout(() => {}, 30000)'; then
3218
+ failures=$((failures + 1))
3219
+ else
3220
+ rc=$?
3221
+ [ "$rc" -eq 124 ] || failures=$((failures + 1))
3222
+ fi
3223
+ end="$(date +%s)"
3224
+ [ $((end - start)) -lt 6 ] || failures=$((failures + 1))
3225
+
3226
+ fixture_run=0
3227
+ while [ "$fixture_run" -lt 2 ]; do
3228
+ local ev="$root/partial-$fixture_run"
3229
+ mkdir -p "$ev"
3230
+ if LSP_E2E_INTERNAL_FIXTURE=partial bash "${BASH_SOURCE[0]}" \
3231
+ --scenario self-test --evidence-dir "$ev" >>"$out" 2>&1; then
3232
+ failures=$((failures + 1))
3233
+ fi
3234
+ if find "$ev" -maxdepth 1 -name '.result.json.*' -print | grep -q .; then failures=$((failures + 1)); fi
3235
+ [ ! -e "$ev/result.json" ] || failures=$((failures + 1))
3236
+ fixture_run=$((fixture_run + 1))
3237
+ done
3238
+
3239
+ fixture_run=0
3240
+ while [ "$fixture_run" -lt 2 ]; do
3241
+ local interrupt_ev="$root/interrupt-$fixture_run"
3242
+ mkdir -p "$interrupt_ev"
3243
+ LSP_E2E_INTERNAL_FIXTURE=interrupt bash "${BASH_SOURCE[0]}" \
3244
+ --scenario self-test --evidence-dir "$interrupt_ev" >>"$out" 2>&1 &
3245
+ child=$!
3246
+ attempts=0
3247
+ while [ ! -f "$interrupt_ev/interrupt-sandbox.txt" ] && [ "$attempts" -lt 50 ]; do
3248
+ sleep 0.1
3249
+ attempts=$((attempts + 1))
3250
+ done
3251
+ sandbox_path="$(cat "$interrupt_ev/interrupt-sandbox.txt" 2>/dev/null || true)"
3252
+ kill -TERM "$child" 2>/dev/null || true
3253
+ wait "$child" 2>/dev/null && failures=$((failures + 1))
3254
+ [ -n "$sandbox_path" ] && [ ! -e "$sandbox_path" ] || failures=$((failures + 1))
3255
+ if find "$interrupt_ev" -maxdepth 1 -name '.result.json.*' -print | grep -q .; then failures=$((failures + 1)); fi
3256
+ [ ! -e "$interrupt_ev/result.json" ] || failures=$((failures + 1))
3257
+ fixture_run=$((fixture_run + 1))
3258
+ done
3259
+
3260
+ if run_missing_dist_build_order_self_test "$root" >>"$out" 2>&1; then
3261
+ missing_dist_build_order="$(jq -c . "$root/missing-dist-build-order/build-order-proof.json")"
3262
+ else
3263
+ failures=$((failures + 1))
3264
+ if [ -f "$root/missing-dist-build-order/build-order-proof.json" ]; then
3265
+ missing_dist_build_order="$(jq -c . "$root/missing-dist-build-order/build-order-proof.json")"
3266
+ fi
3267
+ fi
3268
+
3269
+ if ! node --input-type=module <<'NODE' >>"$out" 2>&1
3270
+ function valid(value) {
3271
+ return value?.result === "PASS"
3272
+ && value?.callerAbort?.daemonProxyRequestId === value?.callerAbort?.daemonCancelTarget
3273
+ && value?.callerAbort?.lspRequestId === value?.callerAbort?.lspCancelTarget
3274
+ && value?.noLeftovers?.daemonActiveControllersAfter === 0
3275
+ && value?.noLeftovers?.lspPendingRequestsAfter === 0
3276
+ && value?.delayedRenamePreCommitGate?.zeroWrites === true
3277
+ && value?.cancellationAfterCommitGate?.mutationCount === 1
3278
+ && value?.readOnlyPreWriteConnectionFailureRetry?.retryCount === 1
3279
+ && value?.sequentialProxyIds?.distinct === true
3280
+ && value?.authProtocolCwd?.tokenLoggedOrForwarded === false;
3281
+ }
3282
+ const good = {
3283
+ result: "PASS",
3284
+ callerAbort: { daemonProxyRequestId: 1, daemonCancelTarget: 1, lspRequestId: 2, lspCancelTarget: 2 },
3285
+ noLeftovers: { daemonActiveControllersAfter: 0, lspPendingRequestsAfter: 0 },
3286
+ delayedRenamePreCommitGate: { zeroWrites: true },
3287
+ cancellationAfterCommitGate: { mutationCount: 1 },
3288
+ readOnlyPreWriteConnectionFailureRetry: { retryCount: 1 },
3289
+ sequentialProxyIds: { distinct: true },
3290
+ authProtocolCwd: { tokenLoggedOrForwarded: false },
3291
+ };
3292
+ const bad = structuredClone(good);
3293
+ bad.sequentialProxyIds.distinct = false;
3294
+ if (!valid(good) || valid(bad)) process.exit(1);
3295
+ NODE
3296
+ then
3297
+ cancellation_result_fields=false
3298
+ printf 'cancellationResultFieldsMandatory=false\n' >>"$out"
3299
+ failures=$((failures + 1))
3300
+ fi
3301
+
3302
+ if ! node --input-type=module <<'NODE' >>"$out" 2>&1
3303
+ function valid(value) {
3304
+ return value?.result === "PASS"
3305
+ && value?.build?.requiredOutputs?.clientJs === true
3306
+ && value?.build?.requiredOutputs?.clientDts === true
3307
+ && value?.build?.requiredOutputs?.cliJs === true
3308
+ && value?.build?.requiredOutputs?.indexJs === true
3309
+ && value?.build?.staleDistRemoved === true
3310
+ && value?.packageJson?.hasOnlyClientAndCliExports === true
3311
+ && value?.scans?.clientJsNoWorkspaceDeps === true
3312
+ && value?.scans?.clientDtsNoWorkspaceDeps === true
3313
+ && value?.scans?.noRepositoryPathCoupling === true
3314
+ && value?.consumer?.emptyNodePath === true
3315
+ && value?.consumer?.js?.statusOk === true
3316
+ && value?.consumer?.js?.typedContextForwarded === true
3317
+ && value?.consumer?.js?.cancellation?.accepted === true
3318
+ && value?.consumer?.js?.rootImport?.rejected === true
3319
+ && value?.consumer?.js?.unknownImport?.rejected === true
3320
+ && value?.consumer?.js?.deepImport?.rejected === true
3321
+ && Array.isArray(value?.consumer?.js?.serverSymbols)
3322
+ && value.consumer.js.serverSymbols.length === 0
3323
+ && value?.consumer?.tscExitCode === 0
3324
+ && value?.adversarial?.repositoryHiddenByInstall === true;
3325
+ }
3326
+ const good = {
3327
+ result: "PASS",
3328
+ build: { requiredOutputs: { clientJs: true, clientDts: true, cliJs: true, indexJs: true }, staleDistRemoved: true },
3329
+ packageJson: { hasOnlyClientAndCliExports: true },
3330
+ scans: { clientJsNoWorkspaceDeps: true, clientDtsNoWorkspaceDeps: true, noRepositoryPathCoupling: true },
3331
+ consumer: {
3332
+ emptyNodePath: true,
3333
+ js: {
3334
+ statusOk: true,
3335
+ typedContextForwarded: true,
3336
+ cancellation: { accepted: true },
3337
+ rootImport: { rejected: true },
3338
+ unknownImport: { rejected: true },
3339
+ deepImport: { rejected: true },
3340
+ serverSymbols: [],
3341
+ },
3342
+ tscExitCode: 0,
3343
+ },
3344
+ adversarial: { repositoryHiddenByInstall: true },
3345
+ };
3346
+ const missing = { result: "PASS" };
3347
+ const leaked = structuredClone(good);
3348
+ leaked.adversarial.repositoryHiddenByInstall = false;
3349
+ const acceptedRoot = structuredClone(good);
3350
+ acceptedRoot.consumer.js.rootImport.rejected = false;
3351
+ const acceptedDeep = structuredClone(good);
3352
+ acceptedDeep.consumer.js.deepImport.rejected = false;
3353
+ const staleBuild = structuredClone(good);
3354
+ staleBuild.build.staleDistRemoved = false;
3355
+ if (!valid(good) || valid(missing) || valid(leaked) || valid(acceptedRoot) || valid(acceptedDeep) || valid(staleBuild)) process.exit(1);
3356
+ NODE
3357
+ then
3358
+ client_package_result_fields=false
3359
+ printf 'clientPackageResultFieldsMandatory=false\n' >>"$out"
3360
+ failures=$((failures + 1))
3361
+ fi
3362
+
3363
+ if ! node --input-type=module <<'NODE' >>"$out" 2>&1
3364
+ function valid(value) {
3365
+ return value?.result === "PASS"
3366
+ && value?.manifest?.valid === true
3367
+ && value?.manifest?.sortedOutputs === true
3368
+ && value?.manifest?.outputHashesMatch === true
3369
+ && value?.distOnlyValidation?.ok === true
3370
+ && value?.componentCli?.exists === true
3371
+ && value?.componentCli?.noNonBuiltinRuntimeImports === true
3372
+ && value?.consumer?.emptyNodePath === true
3373
+ && value?.consumer?.repositoryHiddenByInstall === true
3374
+ && value?.codexContext?.capabilities?.installDecisionTool === true
3375
+ && value?.postEdit?.actualPostEditOperation === true
3376
+ && value?.postEdit?.projectConfigConsumed === true
3377
+ && value?.postEdit?.defaultBudgetWithinLimit === true
3378
+ && value?.postEdit?.contextPressureBudgetWithinLimit === true
3379
+ && value?.compaction?.cacheStored === true
3380
+ && value?.compaction?.compactionReset === true
3381
+ && value?.validation?.singletonRejected === true
3382
+ && value?.validation?.nonexistentPairRejected === true
3383
+ && value?.daemonOwner?.count === 1
3384
+ && value?.daemonOwner?.commandIncludesInstalledCli === true;
3385
+ }
3386
+ const good = {
3387
+ result: "PASS",
3388
+ manifest: { valid: true, sortedOutputs: true, outputHashesMatch: true },
3389
+ distOnlyValidation: { ok: true },
3390
+ componentCli: { exists: true, noNonBuiltinRuntimeImports: true },
3391
+ consumer: { emptyNodePath: true, repositoryHiddenByInstall: true },
3392
+ codexContext: { capabilities: { installDecisionTool: true } },
3393
+ postEdit: {
3394
+ actualPostEditOperation: true,
3395
+ projectConfigConsumed: true,
3396
+ defaultBudgetWithinLimit: true,
3397
+ contextPressureBudgetWithinLimit: true,
3398
+ },
3399
+ compaction: { cacheStored: true, compactionReset: true },
3400
+ validation: { singletonRejected: true, nonexistentPairRejected: true },
3401
+ daemonOwner: { count: 1, commandIncludesInstalledCli: true },
3402
+ };
3403
+ const missing = { result: "PASS" };
3404
+ const workspaceImport = structuredClone(good);
3405
+ workspaceImport.componentCli.noNonBuiltinRuntimeImports = false;
3406
+ const missingBudget = structuredClone(good);
3407
+ missingBudget.postEdit.contextPressureBudgetWithinLimit = false;
3408
+ const missingCompaction = structuredClone(good);
3409
+ missingCompaction.compaction.compactionReset = false;
3410
+ const badOwner = structuredClone(good);
3411
+ badOwner.daemonOwner.count = 2;
3412
+ if (!valid(good) || valid(missing) || valid(workspaceImport) || valid(missingBudget) || valid(missingCompaction) || valid(badOwner)) process.exit(1);
3413
+ NODE
3414
+ then
3415
+ installed_component_result_fields=false
3416
+ printf 'installedComponentResultFieldsMandatory=false\n' >>"$out"
3417
+ failures=$((failures + 1))
3418
+ fi
3419
+
3420
+ after_omo="$(hash_path "$REAL_OMO_ROOT")"
3421
+ after_codex="$(hash_path "$REAL_CODEX_CONFIG")"
3422
+ after_status="$(git -C "$REPO_ROOT" status --porcelain=v1 -uall)"
3423
+ [ "$before_omo" = "$after_omo" ] || failures=$((failures + 1))
3424
+ [ "$before_codex" = "$after_codex" ] || failures=$((failures + 1))
3425
+ [ "$before_status" = "$after_status" ] || failures=$((failures + 1))
3426
+
3427
+ printf '{"result":"%s","selfTest":true,"malformedArgsRejected":true,"fakePassRejected":true,"skipRejected":true,"hungCommandBounded":true,"partialStagingCleanedTwice":true,"interruptCleanupRepeated":true,"missingDaemonDistBuildOrderIndependent":true,"qaDependenciesPortable":%s,"cancellationResultFieldsMandatory":%s,"clientPackageResultFieldsMandatory":%s,"installedComponentResultFieldsMandatory":%s,"missingDistBuildOrder":%s,"dirtyWorktreePreserved":%s,"realHomesUnchanged":%s}\n' \
3428
+ "$( [ "$failures" -eq 0 ] && echo PASS || echo FAIL )" \
3429
+ "$qa_dependencies_portable" \
3430
+ "$cancellation_result_fields" \
3431
+ "$client_package_result_fields" \
3432
+ "$installed_component_result_fields" \
3433
+ "$missing_dist_build_order" \
3434
+ "$( [ "$before_status" = "$after_status" ] && echo true || echo false )" \
3435
+ "$( [ "$before_omo" = "$after_omo" ] && [ "$before_codex" = "$after_codex" ] && echo true || echo false )"
3436
+ cleanup_all || failures=$((failures + 1))
3437
+ NORMAL_CLEANUP_COMPLETE=1
3438
+ [ "$failures" -eq 0 ]
3439
+ }
3440
+
3441
+ run_normal() {
3442
+ require_bins codex node npm bun jq shasum git || return 1
3443
+ prepare_evidence || return 1
3444
+ if [ -n "${LSP_E2E_INTERNAL_FIXTURE:-}" ]; then
3445
+ run_internal_fixture
3446
+ return $?
3447
+ fi
3448
+
3449
+ local real_omo_before real_omo_after real_codex_before real_codex_after worktree_before worktree_after
3450
+ local install_rc plugin_root plugin_count codex_bin
3451
+ local contract_rc driver_rc daemon_pid_file daemon_pid app_server_pid sandbox_removed=false
3452
+ real_omo_before="$(hash_path "$REAL_OMO_ROOT")" || return 1
3453
+ real_codex_before="$(hash_path "$REAL_CODEX_CONFIG")" || return 1
3454
+ worktree_before="$(git -C "$REPO_ROOT" status --porcelain=v1 -uall)"
3455
+ printf '%s\n' "$worktree_before" >"$EVIDENCE_DIR/worktree-before.txt"
3456
+ REAL_OMO_BEFORE_HASH="$real_omo_before"
3457
+ codex_bin="$(command -v codex)"
3458
+ printf 'real_omo_root=%s before=%s\nreal_codex_config=%s before=%s\n' \
3459
+ "$REAL_OMO_ROOT" "$real_omo_before" "$REAL_CODEX_CONFIG" "$real_codex_before" >"$EVIDENCE_DIR/isolation-receipt.txt"
3460
+
3461
+ SANDBOX_ROOT="$(mktemp -d -t cqa-lsp-e2e.XXXXXX)" || return 1
3462
+ mkdir -p "$SANDBOX_ROOT/codex" "$SANDBOX_ROOT/project" "$SANDBOX_ROOT/home"
3463
+ export HOME="$SANDBOX_ROOT/home"
3464
+ export CODEX_HOME="$SANDBOX_ROOT/codex"
3465
+ export CODEX_LOCAL_BIN_DIR="$CODEX_HOME/bin"
3466
+ export OMO_CODEX_PROJECT="$SANDBOX_ROOT/project"
3467
+ export QA_CWD="$SANDBOX_ROOT/project"
3468
+ export OMO_DISABLE_POSTHOG=1
3469
+ export OMO_CODEX_DISABLE_POSTHOG=1
3470
+ export OMO_LSP_DAEMON_DIR="$SANDBOX_ROOT/omo/lsp-daemon"
3471
+ unset OMO_LSP_DAEMON_CLI OMO_LSP_DAEMON_VERSION
3472
+ OMO_TEST_ROOT="$OMO_LSP_DAEMON_DIR"
3473
+ APP_SERVER_PID_FILE="$SANDBOX_ROOT/app-server.pid"
3474
+ export QA_SCENARIO="$SCENARIO"
3475
+
3476
+ if [ "$SCENARIO" = "legacy-cleanup" ]; then
3477
+ write_legacy_cleanup_probe
3478
+ run_legacy_cleanup_probe contract || { fail "legacy cleanup contract probe failed (see legacy-cleanup-contract.log)"; return 1; }
3479
+ run_legacy_cleanup_probe setup || { fail "legacy cleanup fixture setup failed (see legacy-cleanup-setup.log)"; return 1; }
3480
+ fi
3481
+
3482
+ with_shared_build_lock "codex-lsp-e2e-build" prepare_daemon_dist || { fail "lsp-daemon prebuild failed (see lsp-daemon-prebuild.log)"; return 1; }
3483
+ write_path_contract_probe "$SANDBOX_ROOT/contract" "$EVIDENCE_DIR/path-contract.json"
3484
+ contract_rc=$?
3485
+ [ "$contract_rc" -eq 0 ] || { fail "path-contract probe failed (see path-contract-probe.log)"; return 1; }
3486
+ if [ "$SCENARIO" = "auth-ownership" ]; then
3487
+ run_auth_ownership_probe || { fail "auth ownership probe failed (see auth-ownership-probe.log)"; return 1; }
3488
+ stop_known_daemon || return 1
3489
+ stop_owned_sandbox_processes || return 1
3490
+ restore_daemon_dist || return 1
3491
+ safe_rm_tree "$SANDBOX_ROOT" || return 1
3492
+ SANDBOX_ROOT=""
3493
+ sandbox_removed=true
3494
+ real_omo_after="$(hash_path "$REAL_OMO_ROOT")" || return 1
3495
+ real_codex_after="$(hash_path "$REAL_CODEX_CONFIG")" || return 1
3496
+ worktree_after="$(git -C "$REPO_ROOT" status --porcelain=v1 -uall)"
3497
+ printf '%s\n' "$worktree_after" >"$EVIDENCE_DIR/worktree-after.txt"
3498
+ [ "$real_omo_before" = "$real_omo_after" ] || { fail "real OMO daemon root changed"; return 1; }
3499
+ [ "$real_codex_before" = "$real_codex_after" ] || { fail "real Codex config changed"; return 1; }
3500
+ [ "$worktree_before" = "$worktree_after" ] || { fail "driver changed the dirty worktree"; return 1; }
3501
+ printf 'after=%s unchanged=yes\nreal_codex_after=%s unchanged=yes\n' "$real_omo_after" "$real_codex_after" >>"$EVIDENCE_DIR/isolation-receipt.txt"
3502
+ printf 'auth_ownership_probe_complete=true\nisolated_state_removed=%s\n' "$sandbox_removed" >"$EVIDENCE_DIR/cleanup-receipt.txt"
3503
+ write_auth_ownership_result "$real_omo_before" "$real_omo_after" "$real_codex_before" "$real_codex_after" \
3504
+ "$worktree_before" "$worktree_after" "$sandbox_removed" || return 1
3505
+ NORMAL_CLEANUP_COMPLETE=1
3506
+ jq -c . "$EVIDENCE_DIR/result.json"
3507
+ return 0
3508
+ fi
3509
+ if [ "$SCENARIO" = "rename" ]; then
3510
+ write_workspace_edit_fixture "$QA_CWD" || return 1
3511
+ run_workspace_edit_contract_probe || { fail "workspace-edit contract probe failed (see workspace-edit-contract-probe.log)"; return 1; }
3512
+ export QA_SOURCE_FILE="source.ts"
3513
+ export QA_SOURCE_PATH="$QA_CWD/source.ts"
3514
+ export QA_RENAME_EVENTS="$EVIDENCE_DIR/rename-server-events.jsonl"
3515
+ elif [ "$SCENARIO" = "diagnostics-freshness" ]; then
3516
+ write_diagnostics_freshness_fixture "$QA_CWD" || return 1
3517
+ run_diagnostics_freshness_contract_probe || { fail "diagnostics freshness contract probe failed (see diagnostics-freshness-contract-probe.log)"; return 1; }
3518
+ export QA_SOURCE_FILE="source.ts"
3519
+ export QA_SOURCE_PATH="$QA_CWD/source.ts"
3520
+ elif [ "$SCENARIO" = "post-edit" ]; then
3521
+ run_post_edit_contract_probe || { fail "post-edit contract probe failed (see post-edit-contract-probe.log)"; return 1; }
3522
+ elif [ "$SCENARIO" = "cancellation" ]; then
3523
+ run_cancellation_contract_probe || { fail "cancellation contract probe failed (see cancellation-contract-probe.log)"; return 1; }
3524
+ elif [ "$SCENARIO" = "client-package" ]; then
3525
+ run_client_package_contract_probe || { fail "client package contract probe failed (see client-package-smoke.log)"; return 1; }
3526
+ fi
3527
+
3528
+ run_bounded 300 "$EVIDENCE_DIR/install.log" node "$REPO_ROOT/packages/omo-codex/scripts/install-local.mjs" install
3529
+ install_rc=$?
3530
+ [ "$install_rc" -eq 0 ] || { fail "local Codex plugin install failed (see install.log)"; return 1; }
3531
+ if [ "$SCENARIO" = "legacy-cleanup" ]; then
3532
+ if ! run_legacy_cleanup_probe verify; then
3533
+ run_legacy_cleanup_probe cleanup || true
3534
+ fail "legacy cleanup live verification failed (see legacy-cleanup-verify.log)"
3535
+ return 1
3536
+ fi
3537
+ run_legacy_cleanup_probe cleanup || { fail "legacy cleanup fixture process cleanup failed"; return 1; }
3538
+ fi
3539
+ grep -q 'omo@sisyphuslabs' "$CODEX_HOME/config.toml" || { fail "OMO plugin was not enabled in isolated config"; return 1; }
3540
+ plugin_count="$(grep -Ec '^\[plugins\."[^]]+"\]$' "$CODEX_HOME/config.toml" || true)"
3541
+ [ "$plugin_count" -eq 1 ] || { fail "isolated config enabled an unexpected plugin count: $plugin_count"; return 1; }
3542
+ plugin_root="$(find "$CODEX_HOME/plugins/cache/sisyphuslabs/omo" -mindepth 1 -maxdepth 1 -type d -print | sort | tail -1)"
3543
+ [ -n "$plugin_root" ] || { fail "installed OMO plugin cache not found"; return 1; }
3544
+ EXPECTED_DAEMON_CLI="$plugin_root/components/lsp-daemon/dist/cli.js"
3545
+ [ -f "$EXPECTED_DAEMON_CLI" ] || { fail "installed daemon CLI missing: $EXPECTED_DAEMON_CLI"; return 1; }
3546
+ rewrite_codex_mcp_config || return 1
3547
+ stamp_codex_lsp_environment "$plugin_root/.mcp.json" || return 1
3548
+ disable_codex_side_effect_hooks "$plugin_root/.codex-plugin/plugin.json" || return 1
3549
+
3550
+ start_mock_model "$EVIDENCE_DIR/mock-model.log" || return 1
3551
+ write_app_server_driver "$SANDBOX_ROOT/app-server-driver.mjs"
3552
+ export APP_SERVER_RESULT="$EVIDENCE_DIR/app-server.json"
3553
+ export APP_SERVER_PID_FILE
3554
+ export CODEX_BIN="$codex_bin"
3555
+ export DEADLINE_MS=120000
3556
+ run_bounded 150 "$EVIDENCE_DIR/app-server-driver.log" node "$SANDBOX_ROOT/app-server-driver.mjs"
3557
+ driver_rc=$?
3558
+ [ "$driver_rc" -eq 0 ] || { fail "Codex app-server LSP drive failed (see app-server-driver.log)"; return 1; }
3559
+ if [ "$SCENARIO" = "diagnostics-freshness" ]; then
3560
+ jq -e '.ok == true and .hookStarted == true and .hookCompleted == true and .lspOperation.ok == true and (.lspOperation.text | contains("exact-current"))' \
3561
+ "$EVIDENCE_DIR/app-server.json" >/dev/null || { fail "app-server evidence failed required assertions"; return 1; }
3562
+ else
3563
+ jq -e '.ok == true and .hookStarted == true and .hookCompleted == true and .lspOperation.ok == true' \
3564
+ "$EVIDENCE_DIR/app-server.json" >/dev/null || { fail "app-server evidence failed required assertions"; return 1; }
3565
+ fi
3566
+ if [ "$SCENARIO" = "installed-component" ]; then
3567
+ stop_known_daemon || return 1
3568
+ run_installed_component_probe "$plugin_root" || { fail "installed component probe failed (see installed-component.log)"; return 1; }
3569
+ fi
3570
+
3571
+ record_daemon_state || return 1
3572
+ daemon_pid_file="$(find_daemon_pid_file)"
3573
+ daemon_pid="$(tr -d '[:space:]' <"$daemon_pid_file")"
3574
+ app_server_pid="$(tr -d '[:space:]' <"$APP_SERVER_PID_FILE" 2>/dev/null || true)"
3575
+
3576
+ stop_mock || return 1
3577
+ stop_known_pid_file "$APP_SERVER_PID_FILE" "app-server" "Codex app-server" || return 1
3578
+ stop_known_daemon || return 1
3579
+ stop_owned_sandbox_processes || return 1
3580
+ kill -0 "$daemon_pid" 2>/dev/null && { fail "daemon pid remained alive after cleanup"; return 1; }
3581
+ if [ -n "$app_server_pid" ]; then
3582
+ kill -0 "$app_server_pid" 2>/dev/null && { fail "app-server pid remained alive after cleanup"; return 1; }
3583
+ fi
3584
+ restore_daemon_dist || return 1
3585
+ safe_rm_tree "$SANDBOX_ROOT" || return 1
3586
+ SANDBOX_ROOT=""
3587
+ sandbox_removed=true
3588
+
3589
+ real_omo_after="$(hash_path "$REAL_OMO_ROOT")" || return 1
3590
+ real_codex_after="$(hash_path "$REAL_CODEX_CONFIG")" || return 1
3591
+ worktree_after="$(git -C "$REPO_ROOT" status --porcelain=v1 -uall)"
3592
+ printf '%s\n' "$worktree_after" >"$EVIDENCE_DIR/worktree-after.txt"
3593
+ [ "$real_omo_before" = "$real_omo_after" ] || { fail "real OMO daemon root changed"; return 1; }
3594
+ [ "$real_codex_before" = "$real_codex_after" ] || { fail "real Codex config changed"; return 1; }
3595
+ [ "$worktree_before" = "$worktree_after" ] || { fail "driver changed the dirty worktree"; return 1; }
3596
+ printf 'after=%s unchanged=yes\nreal_codex_after=%s unchanged=yes\n' "$real_omo_after" "$real_codex_after" >>"$EVIDENCE_DIR/isolation-receipt.txt"
3597
+ printf 'daemon_pid=%s alive_after=no\napp_server_pid=%s alive_after=no\nmock_model_alive_after=no\nisolated_state_removed=%s\n' \
3598
+ "$daemon_pid" "${app_server_pid:-none}" "$sandbox_removed" >"$EVIDENCE_DIR/cleanup-receipt.txt"
3599
+
3600
+ write_final_result "$real_omo_before" "$real_omo_after" "$real_codex_before" "$real_codex_after" \
3601
+ "$worktree_before" "$worktree_after" "$sandbox_removed" || return 1
3602
+ NORMAL_CLEANUP_COMPLETE=1
3603
+ jq -c . "$EVIDENCE_DIR/result.json"
3604
+ }
3605
+
3606
+ run_all_scenarios() {
3607
+ require_bins bash node jq git || return 1
3608
+ prepare_evidence || return 1
3609
+ local scenarios scenario failures=0 before_omo before_codex before_status after_omo after_codex after_status
3610
+ scenarios="path-contract rename diagnostics-freshness legacy-cleanup post-edit cancellation client-package installed-component auth-ownership"
3611
+ before_omo="$(hash_path "$REAL_OMO_ROOT")" || return 1
3612
+ before_codex="$(hash_path "$REAL_CODEX_CONFIG")" || return 1
3613
+ before_status="$(git -C "$REPO_ROOT" status --porcelain=v1 -uall)"
3614
+ printf '%s\n' "$scenarios" >"$EVIDENCE_DIR/all-scenarios.txt"
3615
+ for scenario in $scenarios; do
3616
+ mkdir -p "$EVIDENCE_DIR/$scenario"
3617
+ if ! bash "${BASH_SOURCE[0]}" --scenario "$scenario" --evidence-dir "$EVIDENCE_DIR/$scenario" >"$EVIDENCE_DIR/$scenario.command.log" 2>&1; then
3618
+ failures=$((failures + 1))
3619
+ continue
3620
+ fi
3621
+ jq -e '.result == "PASS" and .scenario != "SKIP"' "$EVIDENCE_DIR/$scenario/result.json" >/dev/null || failures=$((failures + 1))
3622
+ done
3623
+ after_omo="$(hash_path "$REAL_OMO_ROOT")" || return 1
3624
+ after_codex="$(hash_path "$REAL_CODEX_CONFIG")" || return 1
3625
+ after_status="$(git -C "$REPO_ROOT" status --porcelain=v1 -uall)"
3626
+ [ "$before_omo" = "$after_omo" ] || failures=$((failures + 1))
3627
+ [ "$before_codex" = "$after_codex" ] || failures=$((failures + 1))
3628
+ [ "$before_status" = "$after_status" ] || failures=$((failures + 1))
3629
+ node --input-type=module - "$EVIDENCE_DIR" "$before_omo" "$after_omo" "$before_codex" "$after_codex" "$before_status" "$after_status" <<'NODE'
3630
+ import { readFileSync, writeFileSync } from "node:fs";
3631
+ import { join } from "node:path";
3632
+ const [evidenceDir, omoBefore, omoAfter, codexBefore, codexAfter, statusBefore, statusAfter] = process.argv.slice(2);
3633
+ const scenarios = readFileSync(join(evidenceDir, "all-scenarios.txt"), "utf8").trim().split(/\s+/);
3634
+ const entries = scenarios.map((scenario) => [scenario, JSON.parse(readFileSync(join(evidenceDir, scenario, "result.json"), "utf8"))]);
3635
+ const results = Object.fromEntries(entries);
3636
+ const values = entries.map(([, value]) => value);
3637
+ const payload = {
3638
+ result: values.every((value) => value.result === "PASS") && omoBefore === omoAfter && codexBefore === codexAfter && statusBefore === statusAfter ? "PASS" : "FAIL",
3639
+ scenario: "all",
3640
+ harness: "codex",
3641
+ scenarioOrder: scenarios,
3642
+ scenarioResults: Object.fromEntries(entries.map(([name, value]) => [name, { result: value.result, cleanup: value.cleanup ?? {}, artifacts: value.artifacts ?? {} }])),
3643
+ realOmoRootUnchanged: omoBefore === omoAfter && values.every((value) => value.realOmoRootUnchanged === true),
3644
+ realCodexConfigUnchanged: codexBefore === codexAfter && values.every((value) => value.realCodexConfigUnchanged === true),
3645
+ dirtyWorktreePreserved: statusBefore === statusAfter && values.every((value) => value.dirtyWorktreePreserved === true),
3646
+ noSkip: values.every((value) => value.result !== "SKIP" && value.scenario !== "SKIP"),
3647
+ hookProof: values.every((value) => value.hookStarted === true && value.hookCompleted === true),
3648
+ pathContract: results["path-contract"]?.overrideAssertions ?? null,
3649
+ pairRecovery: results["path-contract"]?.failureFixtures ?? null,
3650
+ auth: results["auth-ownership"]?.authOwnership ?? results["auth-ownership"] ?? null,
3651
+ cancellation: results.cancellation?.cancellationContract ?? null,
3652
+ installedComponent: results["installed-component"]?.installedComponent ?? null,
3653
+ legacyCleanup: results["legacy-cleanup"]?.legacyCleanup ?? null,
3654
+ cleanup: {
3655
+ isolatedStateRemoved: values.every((value) => value.cleanup?.isolatedStateRemoved === true),
3656
+ daemonStopped: values.every((value) => value.cleanup?.daemonStopped === true),
3657
+ },
3658
+ };
3659
+ writeFileSync(join(evidenceDir, "result.json"), `${JSON.stringify(payload, null, 2)}\n`);
3660
+ console.log(JSON.stringify(payload));
3661
+ NODE
3662
+ NORMAL_CLEANUP_COMPLETE=1
3663
+ [ "$failures" -eq 0 ] && jq -e '.result == "PASS" and .noSkip == true' "$EVIDENCE_DIR/result.json" >/dev/null
3664
+ }
3665
+
3666
+ main() {
3667
+ parse_args "$@" || return $?
3668
+ if [ "$SELF_TEST" -eq 1 ]; then
3669
+ run_self_test
3670
+ elif [ "$SCENARIO" = "all" ]; then
3671
+ run_all_scenarios
3672
+ else
3673
+ run_normal
3674
+ fi
3675
+ }
3676
+
3677
+ main "$@"