oh-my-openagent 4.18.0 → 4.18.1

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