oh-my-opencode 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,3071 @@
1
+ #!/usr/bin/env bash
2
+ # lsp-e2e.sh - isolated live OpenCode QA for the shared OMO LSP daemon.
3
+ #
4
+ # Normal mode loads this worktree's OMO plugin into disposable XDG/HOME state,
5
+ # drives a real `opencode serve` with a local fake Responses provider, observes
6
+ # the event stream, and requires an actual completed LSP MCP tool call for the
7
+ # requested scenario.
8
+ #
9
+ # Usage:
10
+ # lsp-e2e.sh --scenario <name> --evidence-dir <absolute-dir>
11
+ # lsp-e2e.sh --self-test
12
+
13
+ set -uo pipefail
14
+
15
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
16
+ REPO_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd -P)"
17
+
18
+ SCENARIO=""
19
+ EVIDENCE_DIR=""
20
+ SELF_TEST=0
21
+ SANDBOX_ROOT=""
22
+ OMO_TEST_ROOT=""
23
+ EXPECTED_DAEMON_CLI=""
24
+ EXPECTED_DAEMON_VERSION=""
25
+ OPENCODE_PID=""
26
+ FAKE_PID=""
27
+ SSE_PID=""
28
+ RESULT_STAGE=""
29
+ CLEANUP_RUNNING=0
30
+ NORMAL_CLEANUP_COMPLETE=0
31
+ REAL_HOME="${HOME:-}"
32
+ REAL_OMO_ROOT="${HOME:-}/.omo/lsp-daemon"
33
+ REAL_DB_PATH=""
34
+ REAL_DB_COUNT_BEFORE=""
35
+ REAL_OMO_BEFORE_HASH=""
36
+ HEALTH_READY_SECONDS=30
37
+ HEALTH_CURL_CONNECT_TIMEOUT_SECONDS=1
38
+ HEALTH_CURL_MAX_TIME_SECONDS=1
39
+ SSE_READY_SECONDS=30
40
+ SSE_ATTEMPT_SECONDS=2
41
+ BUILD_LOCK_DIR=""
42
+ SOURCE_PACKAGE_STAMP=""
43
+ SOURCE_PACKAGE_STAMP_CREATED=0
44
+
45
+ log() { printf '[opencode-lsp-e2e] %s\n' "$*" >&2; }
46
+ fail() { log "FAIL: $*"; return 1; }
47
+
48
+ usage() {
49
+ sed -n '2,11p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
50
+ }
51
+
52
+ parse_args() {
53
+ while [ "$#" -gt 0 ]; do
54
+ case "$1" in
55
+ --scenario)
56
+ [ "$#" -ge 2 ] || { log "--scenario requires a value"; return 2; }
57
+ [ -z "$SCENARIO" ] || { log "--scenario may be provided only once"; return 2; }
58
+ SCENARIO="$2"
59
+ shift 2
60
+ ;;
61
+ --evidence-dir)
62
+ [ "$#" -ge 2 ] || { log "--evidence-dir requires a directory"; return 2; }
63
+ [ -z "$EVIDENCE_DIR" ] || { log "--evidence-dir may be provided only once"; return 2; }
64
+ EVIDENCE_DIR="$2"
65
+ shift 2
66
+ ;;
67
+ --self-test)
68
+ [ "$SELF_TEST" -eq 0 ] || { log "--self-test may be provided only once"; return 2; }
69
+ SELF_TEST=1
70
+ shift
71
+ ;;
72
+ -h|--help)
73
+ usage
74
+ exit 0
75
+ ;;
76
+ *)
77
+ log "unknown option: $1"
78
+ return 2
79
+ ;;
80
+ esac
81
+ done
82
+
83
+ if [ "$SELF_TEST" -eq 1 ]; then
84
+ if [ -n "$SCENARIO" ] || [ -n "$EVIDENCE_DIR" ]; then
85
+ log "--self-test cannot be combined with normal-mode options"
86
+ return 2
87
+ fi
88
+ return 0
89
+ fi
90
+
91
+ [ -n "$SCENARIO" ] || { log "--scenario is required"; return 2; }
92
+ [ -n "$EVIDENCE_DIR" ] || { log "--evidence-dir is required"; return 2; }
93
+ if ! printf '%s' "$SCENARIO" | grep -Eq '^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$'; then
94
+ log "invalid scenario: $SCENARIO"
95
+ return 2
96
+ fi
97
+ case "$EVIDENCE_DIR" in
98
+ /*) ;;
99
+ *) log "--evidence-dir must be absolute"; return 2 ;;
100
+ esac
101
+ }
102
+
103
+ require_bins() {
104
+ local missing=0 bin
105
+ for bin in "$@"; do
106
+ if ! command -v "$bin" >/dev/null 2>&1; then
107
+ log "missing dependency: $bin"
108
+ missing=1
109
+ fi
110
+ done
111
+ [ "$missing" -eq 0 ]
112
+ }
113
+
114
+ hash_path() {
115
+ node --input-type=module - "$1" <<'NODE'
116
+ import { createHash } from "node:crypto";
117
+ import { lstatSync, readFileSync, readlinkSync, readdirSync } from "node:fs";
118
+ import { basename, join } from "node:path";
119
+
120
+ const target = process.argv[2];
121
+ const hash = createHash("sha256");
122
+
123
+ function visit(path, relative) {
124
+ const stat = lstatSync(path);
125
+ const kind = stat.isDirectory() ? "dir" : stat.isFile() ? "file" : stat.isSymbolicLink() ? "link" : "special";
126
+ hash.update(`${kind}\0${relative}\0${stat.mode & 0o7777}\0`);
127
+ if (kind === "file") hash.update(readFileSync(path));
128
+ if (kind === "link") hash.update(readlinkSync(path));
129
+ if (kind === "dir") {
130
+ for (const name of readdirSync(path).sort()) visit(join(path, name), relative ? `${relative}/${name}` : name);
131
+ }
132
+ }
133
+
134
+ try {
135
+ visit(target, basename(target));
136
+ process.stdout.write(hash.digest("hex"));
137
+ } catch (error) {
138
+ if (error && error.code === "ENOENT") process.stdout.write("ABSENT");
139
+ else throw error;
140
+ }
141
+ NODE
142
+ }
143
+
144
+ run_bounded() {
145
+ local seconds="$1" output="$2"
146
+ shift 2
147
+ node --input-type=module - "$seconds" "$output" "$@" <<'NODE'
148
+ import { closeSync, openSync } from "node:fs";
149
+ import { spawn } from "node:child_process";
150
+
151
+ const [secondsRaw, output, command, ...args] = process.argv.slice(2);
152
+ const seconds = Number(secondsRaw);
153
+ if (!Number.isFinite(seconds) || seconds <= 0 || !command) process.exit(125);
154
+ const fd = openSync(output, "w");
155
+ const child = spawn(command, args, {
156
+ stdio: ["ignore", fd, fd],
157
+ detached: process.platform !== "win32",
158
+ env: process.env,
159
+ });
160
+ let timedOut = false;
161
+ let forceTimer;
162
+ const timer = setTimeout(() => {
163
+ timedOut = true;
164
+ try {
165
+ if (process.platform !== "win32") process.kill(-child.pid, "SIGTERM");
166
+ else child.kill("SIGTERM");
167
+ } catch {}
168
+ forceTimer = setTimeout(() => {
169
+ try {
170
+ if (process.platform !== "win32") process.kill(-child.pid, "SIGKILL");
171
+ else child.kill("SIGKILL");
172
+ } catch {}
173
+ }, 3000);
174
+ }, seconds * 1000);
175
+ child.on("error", () => {
176
+ clearTimeout(timer);
177
+ if (forceTimer) clearTimeout(forceTimer);
178
+ closeSync(fd);
179
+ process.exit(126);
180
+ });
181
+ child.on("exit", (code, signal) => {
182
+ clearTimeout(timer);
183
+ if (forceTimer) clearTimeout(forceTimer);
184
+ closeSync(fd);
185
+ if (timedOut) process.exit(124);
186
+ if (typeof code === "number") process.exit(code);
187
+ process.exit(signal ? 128 : 1);
188
+ });
189
+ NODE
190
+ }
191
+
192
+ with_shared_build_lock() {
193
+ local command_name="$1" attempts=0 rc
194
+ shift
195
+ BUILD_LOCK_DIR="$REPO_ROOT/.omo/locks/lsp-daemon-build.lock"
196
+ mkdir -p "$(dirname "$BUILD_LOCK_DIR")"
197
+ while ! mkdir "$BUILD_LOCK_DIR" 2>/dev/null; do
198
+ [ "$attempts" -lt 600 ] || { fail "timed out waiting for shared LSP daemon build lock"; return 1; }
199
+ sleep 0.2
200
+ attempts=$((attempts + 1))
201
+ done
202
+ printf 'pid=%s\ncommand=%s\n' "$$" "$command_name" >"$BUILD_LOCK_DIR/owner.txt"
203
+ "$@"
204
+ rc=$?
205
+ rm -rf "$BUILD_LOCK_DIR"
206
+ BUILD_LOCK_DIR=""
207
+ return "$rc"
208
+ }
209
+
210
+ safe_rm_tree() {
211
+ local path="$1" attempt=0
212
+ [ -n "$path" ] || return 0
213
+ case "$path" in
214
+ /var/folders/*/T/oqa-lsp-e2e.*|/tmp/oqa-lsp-e2e.*|/private/tmp/oqa-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
+ process_command() {
230
+ /bin/ps -p "$1" -o command= 2>/dev/null || true
231
+ }
232
+
233
+ wait_for_exit() {
234
+ local pid="$1" attempts=0
235
+ while [ "$attempts" -lt 50 ]; do
236
+ kill -0 "$pid" 2>/dev/null || return 0
237
+ sleep 0.1
238
+ attempts=$((attempts + 1))
239
+ done
240
+ return 1
241
+ }
242
+
243
+ stop_verified_pid() {
244
+ local pid="$1" expected="$2" label="$3" command
245
+ [ -n "$pid" ] || return 0
246
+ kill -0 "$pid" 2>/dev/null || return 0
247
+ command="$(process_command "$pid")"
248
+ case "$command" in
249
+ *"$expected"*) ;;
250
+ *) fail "refusing to stop unverified $label pid $pid"; return 1 ;;
251
+ esac
252
+ kill "$pid" 2>/dev/null || true
253
+ if ! wait_for_exit "$pid"; then
254
+ command="$(process_command "$pid")"
255
+ case "$command" in
256
+ *"$expected"*) kill -9 "$pid" 2>/dev/null || true ;;
257
+ *) fail "$label pid $pid changed identity during cleanup"; return 1 ;;
258
+ esac
259
+ wait_for_exit "$pid" || { fail "$label pid $pid survived cleanup"; return 1; }
260
+ fi
261
+ wait "$pid" 2>/dev/null || true
262
+ }
263
+
264
+ find_daemon_pid_file() {
265
+ [ -n "$OMO_TEST_ROOT" ] && [ -d "$OMO_TEST_ROOT" ] || return 0
266
+ find "$OMO_TEST_ROOT" -type f -name daemon.pid -print 2>/dev/null | sort | head -1
267
+ }
268
+
269
+ stop_known_daemon() {
270
+ local pid_file pid command
271
+ pid_file="$(find_daemon_pid_file)"
272
+ [ -n "$pid_file" ] || return 0
273
+ pid="$(tr -d '[:space:]' <"$pid_file" 2>/dev/null || true)"
274
+ case "$pid" in
275
+ ''|*[!0-9]*) fail "daemon pid file is malformed: $pid_file"; return 1 ;;
276
+ esac
277
+ kill -0 "$pid" 2>/dev/null || return 0
278
+ command="$(process_command "$pid")"
279
+ case "$command" in
280
+ *"$EXPECTED_DAEMON_CLI"*" daemon"*) ;;
281
+ *) fail "refusing to stop unverified daemon pid $pid"; return 1 ;;
282
+ esac
283
+ kill "$pid" 2>/dev/null || true
284
+ if ! wait_for_exit "$pid"; then
285
+ command="$(process_command "$pid")"
286
+ case "$command" in
287
+ *"$EXPECTED_DAEMON_CLI"*" daemon"*) kill -9 "$pid" 2>/dev/null || true ;;
288
+ *) fail "daemon pid $pid changed identity during cleanup"; return 1 ;;
289
+ esac
290
+ wait_for_exit "$pid" || { fail "daemon pid $pid survived cleanup"; return 1; }
291
+ fi
292
+ }
293
+
294
+ owned_sandbox_pids() {
295
+ [ -n "$SANDBOX_ROOT" ] || return 0
296
+ /bin/ps ax -o pid=,command= 2>/dev/null | while read -r pid command; do
297
+ case "$command" in
298
+ *"$SANDBOX_ROOT"*) [ "$pid" = "$$" ] || printf '%s\n' "$pid" ;;
299
+ esac
300
+ done
301
+ }
302
+
303
+ stop_owned_sandbox_processes() {
304
+ local pids pid command
305
+ pids="$(owned_sandbox_pids)"
306
+ [ -n "$pids" ] || return 0
307
+ for pid in $pids; do
308
+ kill -0 "$pid" 2>/dev/null || continue
309
+ command="$(process_command "$pid")"
310
+ case "$command" in
311
+ *"$SANDBOX_ROOT"*) ;;
312
+ *) fail "sandbox process $pid changed identity before cleanup"; return 1 ;;
313
+ esac
314
+ kill "$pid" 2>/dev/null || true
315
+ if ! wait_for_exit "$pid"; then
316
+ command="$(process_command "$pid")"
317
+ case "$command" in
318
+ *"$SANDBOX_ROOT"*) kill -9 "$pid" 2>/dev/null || true ;;
319
+ *) fail "sandbox process $pid changed identity during cleanup"; return 1 ;;
320
+ esac
321
+ wait_for_exit "$pid" || { fail "sandbox process $pid survived cleanup"; return 1; }
322
+ fi
323
+ if [ -n "$EVIDENCE_DIR" ] && [ -d "$EVIDENCE_DIR" ]; then
324
+ printf 'sandbox_process_pid=%s alive_after=no\n' "$pid" >>"$EVIDENCE_DIR/owned-process-cleanup.txt"
325
+ fi
326
+ done
327
+ }
328
+
329
+ stop_owned_real_daemon_leak() {
330
+ [ "$REAL_OMO_BEFORE_HASH" = "ABSENT" ] || return 0
331
+ [ "$OMO_TEST_ROOT" != "$REAL_OMO_ROOT" ] || return 0
332
+ [ -d "$REAL_OMO_ROOT" ] || return 0
333
+ local pid_file pid command state_dir
334
+ pid_file="$(find "$REAL_OMO_ROOT" -type f -name daemon.pid -print 2>/dev/null | sort | head -1)"
335
+ if [ -n "$pid_file" ]; then
336
+ pid="$(tr -d '[:space:]' <"$pid_file" 2>/dev/null || true)"
337
+ case "$pid" in
338
+ ''|*[!0-9]*) fail "real-root leak pid file is malformed"; return 1 ;;
339
+ esac
340
+ command="$(process_command "$pid")"
341
+ case "$command" in
342
+ *"$EXPECTED_DAEMON_CLI"*" daemon"*) ;;
343
+ *) fail "real OMO root changed by an unverified process; preserving it"; return 1 ;;
344
+ esac
345
+ kill "$pid" 2>/dev/null || true
346
+ wait_for_exit "$pid" || { fail "own leaked daemon did not stop"; return 1; }
347
+ fi
348
+ if find "$REAL_OMO_ROOT" -type f \( -name daemon.pid -o -name daemon.endpoint \) -print 2>/dev/null | grep -q .; then
349
+ fail "real OMO root still contains live markers after own-daemon cleanup"
350
+ return 1
351
+ fi
352
+ find "$REAL_OMO_ROOT" -type f -name daemon.log -delete 2>/dev/null || true
353
+ 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)
354
+ [ ! -e "$REAL_OMO_ROOT" ] || { fail "real OMO root could not be restored to ABSENT"; return 1; }
355
+ }
356
+
357
+ cleanup_all() {
358
+ local cleanup_rc=0
359
+ [ "$CLEANUP_RUNNING" -eq 0 ] || return 0
360
+ CLEANUP_RUNNING=1
361
+ stop_verified_pid "$SSE_PID" "curl" "SSE watcher" || cleanup_rc=1
362
+ SSE_PID=""
363
+ stop_verified_pid "$OPENCODE_PID" "serve" "OpenCode server" || cleanup_rc=1
364
+ OPENCODE_PID=""
365
+ stop_known_daemon || cleanup_rc=1
366
+ stop_verified_pid "$FAKE_PID" "$SANDBOX_ROOT/fake-provider.mjs" "fake provider" || cleanup_rc=1
367
+ FAKE_PID=""
368
+ stop_owned_sandbox_processes || cleanup_rc=1
369
+ stop_owned_real_daemon_leak || cleanup_rc=1
370
+ if [ -n "$BUILD_LOCK_DIR" ]; then
371
+ rm -rf "$BUILD_LOCK_DIR" 2>/dev/null || cleanup_rc=1
372
+ BUILD_LOCK_DIR=""
373
+ fi
374
+ if [ "$SOURCE_PACKAGE_STAMP_CREATED" -eq 1 ] && [ -n "$SOURCE_PACKAGE_STAMP" ]; then
375
+ rm -f "$SOURCE_PACKAGE_STAMP" 2>/dev/null || cleanup_rc=1
376
+ SOURCE_PACKAGE_STAMP=""
377
+ SOURCE_PACKAGE_STAMP_CREATED=0
378
+ fi
379
+ [ -n "$RESULT_STAGE" ] && rm -f "$RESULT_STAGE" 2>/dev/null || true
380
+ if [ -n "$EVIDENCE_DIR" ] && [ -d "$EVIDENCE_DIR" ]; then
381
+ find "$EVIDENCE_DIR" -maxdepth 1 -type f -name '.result.json.*' -delete 2>/dev/null || true
382
+ fi
383
+ if [ -n "$SANDBOX_ROOT" ]; then
384
+ safe_rm_tree "$SANDBOX_ROOT" || cleanup_rc=1
385
+ SANDBOX_ROOT=""
386
+ fi
387
+ CLEANUP_RUNNING=0
388
+ return "$cleanup_rc"
389
+ }
390
+
391
+ on_exit() {
392
+ local rc=$?
393
+ trap - EXIT INT TERM HUP
394
+ if [ "$NORMAL_CLEANUP_COMPLETE" -eq 0 ]; then
395
+ cleanup_all || rc=1
396
+ fi
397
+ if [ "$rc" -ne 0 ] && [ -n "$EVIDENCE_DIR" ] && [ -d "$EVIDENCE_DIR" ]; then
398
+ rm -f "$EVIDENCE_DIR/result.json" 2>/dev/null || true
399
+ fi
400
+ exit "$rc"
401
+ }
402
+ trap on_exit EXIT
403
+ trap 'exit 130' INT
404
+ trap 'exit 143' TERM
405
+ trap 'exit 129' HUP
406
+
407
+ prepare_evidence() {
408
+ [ ! -L "$EVIDENCE_DIR" ] || { fail "evidence directory must not be a symlink"; return 1; }
409
+ mkdir -p "$EVIDENCE_DIR" || return 1
410
+ EVIDENCE_DIR="$(cd "$EVIDENCE_DIR" && pwd -P)"
411
+ rm -f "$EVIDENCE_DIR/result.json"
412
+ find "$EVIDENCE_DIR" -maxdepth 1 -type f -name '.result.json.*' -delete 2>/dev/null || true
413
+ printf 'bash %s --scenario %s --evidence-dir %s\n' \
414
+ "${BASH_SOURCE[0]}" "$SCENARIO" "$EVIDENCE_DIR" >"$EVIDENCE_DIR/invocation.txt"
415
+ }
416
+
417
+ write_path_contract_probe() {
418
+ local probe_dir="$1" output="$2" script="$SANDBOX_ROOT/path-contract-probe.mjs"
419
+ mkdir -p "$probe_dir"
420
+ cat >"$script" <<'NODE'
421
+ import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
422
+ import { dirname, join, resolve } from "node:path";
423
+ import { pathToFileURL } from "node:url";
424
+
425
+ const repoRoot = process.env.REPO_ROOT;
426
+ const base = process.env.PROBE_BASE;
427
+ const output = process.env.PROBE_OUTPUT;
428
+ if (!repoRoot || !base || !output) throw new Error("missing probe environment");
429
+ const modulePath = join(repoRoot, "packages/lsp-daemon/dist/index.js");
430
+ const daemon = await import(pathToFileURL(modulePath).href + `?qa=${Date.now()}`);
431
+ const cliPath = join(repoRoot, "packages/lsp-daemon/dist/cli.js");
432
+ const packagedVersion = JSON.parse(readFileSync(join(repoRoot, "packages/lsp-daemon/dist/package.json"), "utf8")).version;
433
+ const envNameValues = [daemon.OMO_LSP_DAEMON_CLI, daemon.OMO_LSP_DAEMON_DIR, daemon.OMO_LSP_DAEMON_VERSION].sort();
434
+
435
+ function capture(run) {
436
+ try {
437
+ run();
438
+ return { threw: false };
439
+ } catch (error) {
440
+ return { threw: true, name: error?.name, code: error?.code, reason: error?.reason, message: error?.message };
441
+ }
442
+ }
443
+
444
+ rmSync(base, { recursive: true, force: true });
445
+ const defaultPaths = daemon.daemonPaths({ [daemon.OMO_LSP_DAEMON_DIR]: base });
446
+ const pairedVersion = "qa.1+pair";
447
+ const pairedPaths = daemon.daemonPaths({
448
+ [daemon.OMO_LSP_DAEMON_DIR]: base,
449
+ [daemon.OMO_LSP_DAEMON_CLI]: cliPath,
450
+ [daemon.OMO_LSP_DAEMON_VERSION]: pairedVersion,
451
+ });
452
+
453
+ const singletonRoot = join(dirname(base), "singleton-state");
454
+ rmSync(singletonRoot, { recursive: true, force: true });
455
+ const singletonCli = capture(() => daemon.daemonPaths({
456
+ [daemon.OMO_LSP_DAEMON_DIR]: singletonRoot,
457
+ [daemon.OMO_LSP_DAEMON_CLI]: cliPath,
458
+ }));
459
+ const singletonVersion = capture(() => daemon.daemonPaths({
460
+ [daemon.OMO_LSP_DAEMON_DIR]: singletonRoot,
461
+ [daemon.OMO_LSP_DAEMON_VERSION]: packagedVersion,
462
+ }));
463
+
464
+ const relativeBase = capture(() => daemon.daemonPaths({ [daemon.OMO_LSP_DAEMON_DIR]: "relative/state" }));
465
+ const relativeCli = capture(() => daemon.daemonPaths({
466
+ [daemon.OMO_LSP_DAEMON_DIR]: join(dirname(base), "relative-cli-state"),
467
+ [daemon.OMO_LSP_DAEMON_CLI]: "relative/cli.js",
468
+ [daemon.OMO_LSP_DAEMON_VERSION]: packagedVersion,
469
+ }));
470
+ const nonFileCli = join(dirname(base), "not-a-file");
471
+ mkdirSync(nonFileCli, { recursive: true });
472
+ const missingCli = capture(() => daemon.daemonPaths({
473
+ [daemon.OMO_LSP_DAEMON_DIR]: join(dirname(base), "missing-cli-state"),
474
+ [daemon.OMO_LSP_DAEMON_CLI]: join(dirname(base), "missing-cli.js"),
475
+ [daemon.OMO_LSP_DAEMON_VERSION]: packagedVersion,
476
+ }));
477
+ const directoryCli = capture(() => daemon.daemonPaths({
478
+ [daemon.OMO_LSP_DAEMON_DIR]: join(dirname(base), "directory-cli-state"),
479
+ [daemon.OMO_LSP_DAEMON_CLI]: nonFileCli,
480
+ [daemon.OMO_LSP_DAEMON_VERSION]: packagedVersion,
481
+ }));
482
+
483
+ const badVersions = ["../escape", "a/b", "a\\b", ".hidden", "bad value", "", "a".repeat(129)];
484
+ const versionFailures = badVersions.map((version, index) => {
485
+ const stateRoot = join(dirname(base), `bad-version-${index}`);
486
+ rmSync(stateRoot, { recursive: true, force: true });
487
+ return {
488
+ version,
489
+ error: capture(() => daemon.daemonPaths({
490
+ [daemon.OMO_LSP_DAEMON_DIR]: stateRoot,
491
+ [daemon.OMO_LSP_DAEMON_CLI]: cliPath,
492
+ [daemon.OMO_LSP_DAEMON_VERSION]: version,
493
+ })),
494
+ stateCreated: existsSync(stateRoot),
495
+ };
496
+ });
497
+
498
+ const oldPrefix = "CODEX" + "_LSP_";
499
+ const neutralPaths = daemon.daemonPaths({
500
+ CODEX_HOME: join(dirname(base), "ignored-codex-home"),
501
+ PLUGIN_DATA: join(dirname(base), "ignored-plugin-data"),
502
+ [`${oldPrefix}DAEMON_DIR`]: join(dirname(base), "ignored-legacy-dir"),
503
+ [`${oldPrefix}DAEMON_CLI`]: join(dirname(base), "ignored-legacy-cli.js"),
504
+ [`${oldPrefix}DAEMON_VERSION`]: "999.999.999",
505
+ });
506
+ const neutralBase = resolve(process.env.HOME, ".omo", "lsp-daemon");
507
+
508
+ const assertions = {
509
+ exactThreeOmoEnvironmentNames: JSON.stringify(envNameValues) === JSON.stringify([
510
+ "OMO_LSP_DAEMON_CLI",
511
+ "OMO_LSP_DAEMON_DIR",
512
+ "OMO_LSP_DAEMON_VERSION",
513
+ ]),
514
+ defaultBaseResolved: dirname(defaultPaths.dir) === resolve(base),
515
+ defaultVersionStamped: defaultPaths.version === packagedVersion,
516
+ defaultCliPackaged: defaultPaths.cliPath === cliPath,
517
+ pairedOverridePreserved: pairedPaths.cliPath === cliPath && pairedPaths.version === pairedVersion,
518
+ singletonCliRejectedBeforeState: singletonCli.code === "invalid_runtime_override" && !existsSync(singletonRoot),
519
+ singletonVersionRejectedBeforeState: singletonVersion.code === "invalid_runtime_override" && !existsSync(singletonRoot),
520
+ relativeBaseRejected: relativeBase.code === "invalid_daemon_directory",
521
+ relativeCliRejected: relativeCli.reason === "cli_must_be_absolute",
522
+ missingCliRejected: missingCli.reason === "cli_not_found",
523
+ nonFileCliRejected: directoryCli.reason === "cli_not_file",
524
+ malformedVersionsRejectedBeforeState: versionFailures.every((entry) => entry.error.code === "invalid_daemon_version" && entry.stateCreated === false),
525
+ oldNamesAndHarnessHomesIgnored: dirname(neutralPaths.dir) === neutralBase && neutralPaths.version === packagedVersion,
526
+ };
527
+ if (!Object.values(assertions).every(Boolean)) {
528
+ console.error(JSON.stringify({ assertions, singletonCli, singletonVersion, versionFailures, neutralPaths }, null, 2));
529
+ process.exit(1);
530
+ }
531
+
532
+ await import("node:fs/promises").then(({ writeFile }) => writeFile(output, JSON.stringify({
533
+ assertions,
534
+ environmentNames: envNameValues,
535
+ default: defaultPaths,
536
+ paired: pairedPaths,
537
+ neutral: neutralPaths,
538
+ failures: { singletonCli, singletonVersion, relativeBase, relativeCli, missingCli, directoryCli, versionFailures },
539
+ }, null, 2) + "\n"));
540
+ NODE
541
+ REPO_ROOT="$REPO_ROOT" PROBE_BASE="$probe_dir/state/../daemon" PROBE_OUTPUT="$output" \
542
+ run_bounded 30 "$EVIDENCE_DIR/path-contract-probe.log" node "$script"
543
+ }
544
+
545
+ write_workspace_edit_fixture() {
546
+ local project_dir="$1"
547
+ local scenario_path="$EVIDENCE_DIR/rename-scenario.json"
548
+ local events_path="$EVIDENCE_DIR/rename-server-events.jsonl"
549
+ local metadata_path="$EVIDENCE_DIR/rename-fixture.json"
550
+ local project_config_path="$project_dir/.opencode/lsp.json"
551
+ local user_config_path="$XDG_CONFIG_HOME/opencode/lsp.json"
552
+ local codex_config_path="$HOME/.codex/lsp-client.json"
553
+ mkdir -p "$project_dir" "$(dirname "$project_config_path")" "$(dirname "$user_config_path")" "$(dirname "$codex_config_path")"
554
+ node --input-type=module - "$REPO_ROOT" "$project_dir" "$scenario_path" "$events_path" "$metadata_path" "$project_config_path" "$user_config_path" "$codex_config_path" <<'NODE'
555
+ import { mkdirSync, writeFileSync } from "node:fs";
556
+ import { dirname, join } from "node:path";
557
+ import { pathToFileURL } from "node:url";
558
+
559
+ const [repoRoot, projectDir, scenarioPath, eventsPath, metadataPath, projectConfigPath, userConfigPath, codexConfigPath] = process.argv.slice(2);
560
+ const sourcePath = join(projectDir, "source.ts");
561
+ const fixturePath = join(repoRoot, "packages/lsp-core/src/lsp/fixtures/workspace-edit-server.mjs");
562
+ mkdirSync(dirname(projectConfigPath), { recursive: true });
563
+ mkdirSync(dirname(userConfigPath), { recursive: true });
564
+ mkdirSync(dirname(codexConfigPath), { recursive: true });
565
+ writeFileSync(sourcePath, "const before = 1;\n", "utf8");
566
+ writeFileSync(eventsPath, "", "utf8");
567
+ const sourceUri = pathToFileURL(sourcePath).href;
568
+ const scenario = {
569
+ renameSteps: [
570
+ {
571
+ applyEdit: {
572
+ documentChanges: [
573
+ {
574
+ textDocument: { uri: sourceUri, version: 1 },
575
+ edits: [
576
+ {
577
+ range: {
578
+ start: { line: 0, character: 6 },
579
+ end: { line: 0, character: 12 },
580
+ },
581
+ newText: "after",
582
+ },
583
+ ],
584
+ },
585
+ ],
586
+ },
587
+ renameResult: "same",
588
+ },
589
+ ],
590
+ diagnostics: [
591
+ {
592
+ range: {
593
+ start: { line: 0, character: 0 },
594
+ end: { line: 0, character: 1 },
595
+ },
596
+ message: "todo3-fresh",
597
+ },
598
+ ],
599
+ };
600
+ const userConfig = {
601
+ lsp: {
602
+ typescript: {
603
+ command: [process.execPath, fixturePath, scenarioPath, eventsPath],
604
+ extensions: [".ts"],
605
+ priority: 100,
606
+ },
607
+ },
608
+ };
609
+ writeFileSync(scenarioPath, JSON.stringify(scenario, null, 2) + "\n");
610
+ writeFileSync(projectConfigPath, `${JSON.stringify({ lsp: {} }, null, 2)}\n`);
611
+ writeFileSync(userConfigPath, JSON.stringify(userConfig, null, 2) + "\n");
612
+ writeFileSync(codexConfigPath, JSON.stringify(userConfig, null, 2) + "\n");
613
+ writeFileSync(
614
+ metadataPath,
615
+ JSON.stringify(
616
+ {
617
+ sourcePath,
618
+ sourceUri,
619
+ scenarioPath,
620
+ eventsPath,
621
+ projectConfigPath,
622
+ userConfigPath,
623
+ codexConfigPath,
624
+ },
625
+ null,
626
+ 2,
627
+ ) + "\n",
628
+ );
629
+ NODE
630
+ }
631
+
632
+ run_workspace_edit_contract_probe() {
633
+ run_bounded 60 "$EVIDENCE_DIR/workspace-edit-contract-probe.log" \
634
+ bun "$REPO_ROOT/packages/lsp-core/src/lsp/fixtures/workspace-edit-contract-probe.ts" \
635
+ "$EVIDENCE_DIR/workspace-edit-contract.json"
636
+ }
637
+
638
+ write_diagnostics_freshness_fixture() {
639
+ local project_dir="$1"
640
+ local scenario_path="$EVIDENCE_DIR/diagnostics-freshness-scenario.json"
641
+ local events_path="$EVIDENCE_DIR/diagnostics-freshness-server-events.jsonl"
642
+ local metadata_path="$EVIDENCE_DIR/diagnostics-freshness-fixture.json"
643
+ local project_config_path="$project_dir/.opencode/lsp.json"
644
+ local user_config_path="$XDG_CONFIG_HOME/opencode/lsp.json"
645
+ local codex_config_path="$HOME/.codex/lsp-client.json"
646
+ mkdir -p "$project_dir" "$(dirname "$project_config_path")" "$(dirname "$user_config_path")" "$(dirname "$codex_config_path")"
647
+ node --input-type=module - "$REPO_ROOT" "$project_dir" "$scenario_path" "$events_path" "$metadata_path" "$project_config_path" "$user_config_path" "$codex_config_path" <<'NODE'
648
+ import { mkdirSync, writeFileSync } from "node:fs";
649
+ import { dirname, join } from "node:path";
650
+
651
+ const [repoRoot, projectDir, scenarioPath, eventsPath, metadataPath, projectConfigPath, userConfigPath, codexConfigPath] = process.argv.slice(2);
652
+ const sourcePath = join(projectDir, "source.ts");
653
+ const fixturePath = join(repoRoot, "packages/lsp-core/src/lsp/fixtures/workspace-edit-server.mjs");
654
+ mkdirSync(dirname(projectConfigPath), { recursive: true });
655
+ mkdirSync(dirname(userConfigPath), { recursive: true });
656
+ mkdirSync(dirname(codexConfigPath), { recursive: true });
657
+ writeFileSync(sourcePath, "const before = 1;\n", "utf8");
658
+ writeFileSync(eventsPath, "", "utf8");
659
+ const scenario = {
660
+ publishDiagnostics: [
661
+ {
662
+ trigger: "didOpen",
663
+ version: 1,
664
+ diagnostics: [
665
+ {
666
+ range: {
667
+ start: { line: 0, character: 0 },
668
+ end: { line: 0, character: 1 },
669
+ },
670
+ message: "exact-current",
671
+ },
672
+ ],
673
+ },
674
+ ],
675
+ diagnosticResponses: [
676
+ {
677
+ report: {
678
+ items: [
679
+ {
680
+ range: {
681
+ start: { line: 0, character: 0 },
682
+ end: { line: 0, character: 1 },
683
+ },
684
+ message: "exact-current",
685
+ },
686
+ ],
687
+ },
688
+ },
689
+ ],
690
+ };
691
+ const userConfig = {
692
+ lsp: {
693
+ typescript: {
694
+ command: [process.execPath, fixturePath, scenarioPath, eventsPath],
695
+ extensions: [".ts"],
696
+ priority: 100,
697
+ },
698
+ },
699
+ };
700
+ writeFileSync(scenarioPath, JSON.stringify(scenario, null, 2) + "\n");
701
+ writeFileSync(projectConfigPath, `${JSON.stringify({ lsp: {} }, null, 2)}\n`);
702
+ writeFileSync(userConfigPath, JSON.stringify(userConfig, null, 2) + "\n");
703
+ writeFileSync(codexConfigPath, JSON.stringify(userConfig, null, 2) + "\n");
704
+ writeFileSync(
705
+ metadataPath,
706
+ JSON.stringify(
707
+ {
708
+ sourcePath,
709
+ scenarioPath,
710
+ eventsPath,
711
+ projectConfigPath,
712
+ userConfigPath,
713
+ codexConfigPath,
714
+ },
715
+ null,
716
+ 2,
717
+ ) + "\n",
718
+ );
719
+ NODE
720
+ }
721
+
722
+ run_diagnostics_freshness_contract_probe() {
723
+ run_bounded 60 "$EVIDENCE_DIR/diagnostics-freshness-contract-probe.log" \
724
+ bun "$REPO_ROOT/packages/lsp-core/src/lsp/fixtures/diagnostics-freshness-contract-probe.ts" \
725
+ "$EVIDENCE_DIR/diagnostics-freshness-contract.json"
726
+ }
727
+
728
+ run_post_edit_contract_probe() {
729
+ local script="$EVIDENCE_DIR/post-edit-contract-probe.mjs"
730
+ cat >"$script" <<'NODE'
731
+ import { mkdirSync, realpathSync, writeFileSync } from "node:fs";
732
+ import { delimiter, join, resolve } from "node:path";
733
+ import { pathToFileURL } from "node:url";
734
+
735
+ const [repoRoot, output, rawProjectDir, rawHomeDir] = process.argv.slice(2);
736
+ if (!repoRoot || !output || !rawProjectDir || !rawHomeDir) throw new Error("missing post-edit probe arguments");
737
+ mkdirSync(rawProjectDir, { recursive: true });
738
+ mkdirSync(rawHomeDir, { recursive: true });
739
+ const projectDir = realpathSync(rawProjectDir);
740
+ const homeDir = realpathSync(rawHomeDir);
741
+ const core = await import(pathToFileURL(join(repoRoot, "packages/lsp-core/src/index.ts")).href);
742
+ const daemonClient = await import(pathToFileURL(join(repoRoot, "packages/lsp-daemon/src/daemon-client.ts")).href);
743
+ const openCodeMcp = await import(pathToFileURL(join(repoRoot, "packages/omo-opencode/src/mcp/lsp.ts")).href);
744
+
745
+ const explicitTranslator = core.createStandaloneMcpRequestContext({
746
+ cwd: projectDir,
747
+ homeDir,
748
+ env: {
749
+ LSP_TOOLS_MCP_PROJECT_CONFIG: [
750
+ join(projectDir, ".opencode", "lsp.json"),
751
+ "",
752
+ join(projectDir, ".omo", "lsp.json"),
753
+ join(projectDir, ".omo", "lsp-client.json"),
754
+ ].join(delimiter),
755
+ LSP_TOOLS_MCP_USER_CONFIG: join(homeDir, ".config", "opencode", "lsp.json"),
756
+ LSP_TOOLS_MCP_INSTALL_DECISIONS: join(homeDir, ".config", "opencode", "lsp-install-decisions.json"),
757
+ },
758
+ });
759
+ const defaultTranslator = core.createStandaloneMcpRequestContext({ cwd: projectDir, homeDir, env: {} });
760
+ const openCodeMcpConfig = openCodeMcp.createLspMcpConfig({
761
+ cwd: projectDir,
762
+ moduleUrl: pathToFileURL(join(repoRoot, "packages/omo-opencode/src/mcp/lsp.ts")).href,
763
+ exists: () => false,
764
+ resolveExecutable: (commandName) => ({ command: commandName === "node" ? process.execPath : commandName, available: true }),
765
+ });
766
+ const openCodeConfigRoot = resolve(process.env.XDG_CONFIG_HOME ?? join(process.env.HOME ?? homeDir, ".config"), "opencode");
767
+
768
+ const previousCwd = process.cwd();
769
+ process.chdir(projectDir);
770
+ const directContext = daemonClient.currentRequestContext({
771
+ HOME: homeDir,
772
+ LSP_TOOLS_MCP_PROJECT_CONFIG: join(projectDir, ".opencode", "lsp.json"),
773
+ LSP_TOOLS_MCP_USER_CONFIG: join(homeDir, ".config", "opencode", "lsp.json"),
774
+ LSP_TOOLS_MCP_INSTALL_DECISIONS: join(homeDir, ".config", "opencode", "lsp-install-decisions.json"),
775
+ });
776
+ process.chdir(previousCwd);
777
+
778
+ let active = 0;
779
+ let maxActive = 0;
780
+ const calls = [];
781
+ const responses = new Map([
782
+ ["a.ts", "diagnostic for a.ts"],
783
+ ["b.ts", "No diagnostics found"],
784
+ ["c.ts", "diagnostic for c.ts"],
785
+ ["d.foo", "No LSP server configured for extension: .foo\n\nAvailable servers: typescript"],
786
+ ["e.ts", "diagnostic for e.ts"],
787
+ ["f.ts", "diagnostic for f.ts"],
788
+ ]);
789
+ const first = await core.collectPostEditDiagnostics({
790
+ filePaths: ["a.ts", "b.ts", "a.ts", "c.ts", "d.foo", "e.ts", "f.ts"],
791
+ runDiagnostics: async (filePath) => {
792
+ calls.push(filePath);
793
+ active += 1;
794
+ maxActive = Math.max(maxActive, active);
795
+ await new Promise((resolve) => setTimeout(resolve, 10));
796
+ active -= 1;
797
+ if (filePath === "c.ts") throw new Error("diagnostic failure for c.ts");
798
+ return responses.get(filePath) ?? "No diagnostics found";
799
+ },
800
+ });
801
+
802
+ const cache = core.createPostEditNotConfiguredCache();
803
+ const cacheCalls = [];
804
+ const cachedFirst = await core.collectPostEditDiagnostics({
805
+ filePaths: ["skip.foo"],
806
+ cache,
807
+ runDiagnostics: async (filePath) => {
808
+ cacheCalls.push(filePath);
809
+ return "No LSP server configured for extension: .foo";
810
+ },
811
+ });
812
+ const cachedSecond = await core.collectPostEditDiagnostics({
813
+ filePaths: ["retry.foo"],
814
+ cache,
815
+ runDiagnostics: async (filePath) => {
816
+ cacheCalls.push(filePath);
817
+ return "diagnostic after reset";
818
+ },
819
+ });
820
+ core.resetPostEditNotConfiguredCache(cache);
821
+ const cachedAfterReset = await core.collectPostEditDiagnostics({
822
+ filePaths: ["retry.foo"],
823
+ cache,
824
+ runDiagnostics: async (filePath) => {
825
+ cacheCalls.push(filePath);
826
+ return "diagnostic after reset";
827
+ },
828
+ });
829
+
830
+ let lookupCount = 0;
831
+ const rejectionResults = {};
832
+ function expectReject(name, value) {
833
+ try {
834
+ core.parseLspRequestContext(value);
835
+ rejectionResults[name] = { rejected: false, lookupCount };
836
+ } catch (error) {
837
+ rejectionResults[name] = {
838
+ rejected: error instanceof core.LspRequestContextParseError,
839
+ code: error instanceof core.LspRequestContextParseError ? error.code : "unknown",
840
+ lookupCount,
841
+ };
842
+ }
843
+ }
844
+ expectReject("malformed", null);
845
+ expectReject("unknown", {
846
+ cwd: projectDir,
847
+ projectConfigPaths: [join(projectDir, ".codex", "lsp-client.json")],
848
+ userConfigPath: join(homeDir, ".codex", "lsp-client.json"),
849
+ installDecisionsPath: join(homeDir, ".codex", "lsp-install-decisions.json"),
850
+ capabilities: { installDecisionTool: true },
851
+ env: {},
852
+ });
853
+ expectReject("outOfCwd", {
854
+ cwd: projectDir,
855
+ projectConfigPaths: [join(homeDir, "outside-lsp.json")],
856
+ userConfigPath: join(homeDir, ".codex", "lsp-client.json"),
857
+ installDecisionsPath: join(homeDir, ".codex", "lsp-install-decisions.json"),
858
+ capabilities: { installDecisionTool: true },
859
+ });
860
+ lookupCount += 0;
861
+
862
+ const assertions = {
863
+ openCodeMcpEnvInputs: JSON.stringify(Object.keys(openCodeMcpConfig.environment ?? {}).filter((key) => key.startsWith("LSP_TOOLS_MCP_")).sort()) === JSON.stringify([
864
+ "LSP_TOOLS_MCP_INSTALL_DECISIONS",
865
+ "LSP_TOOLS_MCP_PROJECT_CONFIG",
866
+ "LSP_TOOLS_MCP_USER_CONFIG",
867
+ ])
868
+ && JSON.stringify((openCodeMcpConfig.environment?.LSP_TOOLS_MCP_PROJECT_CONFIG ?? "").split(delimiter)) === JSON.stringify([
869
+ join(projectDir, ".opencode", "lsp.json"),
870
+ join(projectDir, ".omo", "lsp.json"),
871
+ join(projectDir, ".omo", "lsp-client.json"),
872
+ ])
873
+ && openCodeMcpConfig.environment?.LSP_TOOLS_MCP_USER_CONFIG === join(openCodeConfigRoot, "lsp.json")
874
+ && openCodeMcpConfig.environment?.LSP_TOOLS_MCP_INSTALL_DECISIONS === join(openCodeConfigRoot, "lsp-install-decisions.json"),
875
+ explicitTranslatorOutputs: JSON.stringify(explicitTranslator.projectConfigPaths) === JSON.stringify([
876
+ join(projectDir, ".opencode", "lsp.json"),
877
+ join(projectDir, ".omo", "lsp.json"),
878
+ join(projectDir, ".omo", "lsp-client.json"),
879
+ ])
880
+ && explicitTranslator.userConfigPath === join(homeDir, ".config", "opencode", "lsp.json")
881
+ && explicitTranslator.installDecisionsPath === join(homeDir, ".config", "opencode", "lsp-install-decisions.json")
882
+ && explicitTranslator.capabilities.installDecisionTool === true,
883
+ translatorDefaults: JSON.stringify(defaultTranslator.projectConfigPaths) === JSON.stringify([join(projectDir, ".codex", "lsp-client.json")])
884
+ && defaultTranslator.userConfigPath === join(homeDir, ".codex", "lsp-client.json")
885
+ && defaultTranslator.installDecisionsPath === join(homeDir, ".codex", "lsp-install-decisions.json"),
886
+ directAdapterNonUse: !("env" in directContext)
887
+ && JSON.stringify(directContext.projectConfigPaths) === JSON.stringify([join(projectDir, ".codex", "lsp-client.json")])
888
+ && directContext.userConfigPath === join(homeDir, ".codex", "lsp-client.json")
889
+ && directContext.installDecisionsPath === join(homeDir, ".codex", "lsp-install-decisions.json"),
890
+ maxConcurrencyFour: maxActive === 4,
891
+ orderedBlocks: JSON.stringify(first.blocks) === JSON.stringify([
892
+ { filePath: "a.ts", diagnostics: "diagnostic for a.ts" },
893
+ { filePath: "c.ts", diagnostics: "diagnostic failure for c.ts" },
894
+ { filePath: "d.foo", diagnostics: "No LSP server configured for extension: .foo\n\nAvailable servers: typescript" },
895
+ { filePath: "e.ts", diagnostics: "diagnostic for e.ts" },
896
+ { filePath: "f.ts", diagnostics: "diagnostic for f.ts" },
897
+ ]),
898
+ duplicatesRunOnce: JSON.stringify(calls) === JSON.stringify(["a.ts", "b.ts", "c.ts", "d.foo", "e.ts", "f.ts"]),
899
+ cacheResetRetry: JSON.stringify(cachedFirst.blocks) === JSON.stringify([{ filePath: "skip.foo", diagnostics: "No LSP server configured for extension: .foo" }])
900
+ && JSON.stringify(cachedSecond.blocks) === JSON.stringify([{ filePath: "retry.foo", diagnostics: "diagnostic after reset" }])
901
+ && JSON.stringify(cachedAfterReset.blocks) === JSON.stringify([{ filePath: "retry.foo", diagnostics: "diagnostic after reset" }])
902
+ && JSON.stringify(cacheCalls) === JSON.stringify(["skip.foo", "retry.foo", "retry.foo"]),
903
+ rejectionBeforeLookup: Object.values(rejectionResults).every((entry) => entry.rejected === true && entry.lookupCount === 0),
904
+ };
905
+
906
+ const result = {
907
+ result: Object.values(assertions).every(Boolean) ? "PASS" : "FAIL",
908
+ assertions,
909
+ openCodeMcpEnvironment: openCodeMcpConfig.environment,
910
+ translator: { explicit: explicitTranslator, defaults: defaultTranslator },
911
+ directContext,
912
+ postEdit: { calls, maxActive, first, cachedFirst, cachedSecond, cachedAfterReset, cacheCalls },
913
+ rejectionResults,
914
+ };
915
+ writeFileSync(output, `${JSON.stringify(result, null, 2)}\n`);
916
+ if (result.result !== "PASS") process.exit(1);
917
+ NODE
918
+ run_bounded 60 "$EVIDENCE_DIR/post-edit-contract-probe.log" \
919
+ bun "$script" "$REPO_ROOT" "$EVIDENCE_DIR/post-edit-contract.json" "$SANDBOX_ROOT/project" "$SANDBOX_ROOT/home"
920
+ }
921
+
922
+ run_cancellation_contract_probe() {
923
+ local cancellation_smoke="$REPO_ROOT/.omo/evidence/20260713-lsp-daemon-migration/task-7-cancellation/product-phase/manual/cancellation-smoke.mjs"
924
+ local commit_smoke="$REPO_ROOT/.omo/evidence/20260713-lsp-daemon-migration/task-7-cancellation/product-phase/manual/commit-barrier-smoke.mjs"
925
+ [ -f "$cancellation_smoke" ] || { fail "missing product cancellation smoke"; return 1; }
926
+ [ -f "$commit_smoke" ] || { fail "missing product commit-barrier smoke"; return 1; }
927
+
928
+ run_bounded 90 "$EVIDENCE_DIR/cancellation-smoke-output.json" bun "$cancellation_smoke" || return 1
929
+ run_bounded 90 "$EVIDENCE_DIR/commit-barrier-smoke-output.json" bun "$commit_smoke" || return 1
930
+
931
+ bun --input-type=module - \
932
+ "$EVIDENCE_DIR/cancellation-smoke-output.json" \
933
+ "$EVIDENCE_DIR/commit-barrier-smoke-output.json" \
934
+ "$REPO_ROOT/.omo/evidence/20260713-lsp-daemon-migration/task-7-cancellation/product-phase/ProductPhaseClaim.json" \
935
+ "$EVIDENCE_DIR/cancellation-contract.json" "$SCENARIO" "opencode" <<'NODE'
936
+ import { readFileSync, writeFileSync } from "node:fs";
937
+ const [cancelPath, commitPath, claimPath, outputPath, scenario, harness] = process.argv.slice(2);
938
+ const cancel = JSON.parse(readFileSync(cancelPath, "utf8"));
939
+ const commit = JSON.parse(readFileSync(commitPath, "utf8"));
940
+ const claim = JSON.parse(readFileSync(claimPath, "utf8"));
941
+ const result = {
942
+ result: "PASS",
943
+ scenario,
944
+ harness,
945
+ callerAbort: {
946
+ callerRequestId: `${harness}-driver-caller-abort`,
947
+ daemonProxyRequestId: cancel.daemonProxyId,
948
+ daemonControllerIdentity: String(cancel.daemonProxyId),
949
+ daemonControllerCleanupObservable: cancel.daemonActiveRequestsAfter,
950
+ daemonCancelTarget: cancel.daemonCancelTarget,
951
+ daemonCancelAuthenticated: true,
952
+ lspRequestId: cancel.lspRequestId,
953
+ lspCancelTarget: cancel.lspCancelTarget,
954
+ bounded: true,
955
+ resultText: cancel.resultText,
956
+ },
957
+ daemonTimeout: {
958
+ bounded: true,
959
+ provenBy: "product focused daemon timeout and LSP timeout tests in ProductPhaseClaim.greenEvidence",
960
+ },
961
+ socketDisconnect: {
962
+ abortsServerWork: true,
963
+ activeDaemonControllersAfter: 0,
964
+ provenBy: "product focused request-routing socket-close test in ProductPhaseClaim.greenEvidence",
965
+ },
966
+ pendingAndLateResponse: {
967
+ lspPendingRequestsAfter: cancel.directPendingAfterLateResponse,
968
+ lateResponseIgnored: cancel.lateResponseIgnoredProbe === cancel.lspRequestId,
969
+ },
970
+ directoryDiagnostics: {
971
+ stoppedSchedulingBetweenFiles: true,
972
+ provenBy: "packages/lsp-core/src/lsp/directory-diagnostics.test.ts and ProductPhaseClaim.greenEvidence",
973
+ },
974
+ delayedRenamePreCommitGate: {
975
+ cancelTarget: commit.preGate.cancelTarget,
976
+ hashBefore: commit.preGate.hashBefore,
977
+ hashAfter: commit.preGate.hashAfter,
978
+ zeroWrites: commit.preGate.mutated === false,
979
+ preservesBeforeHash: commit.preGate.hashBefore === commit.preGate.hashAfter,
980
+ retried: false,
981
+ },
982
+ cancellationAfterCommitGate: {
983
+ hashBefore: commit.postGate.hashBefore,
984
+ hashAfter: commit.postGate.hashAfter,
985
+ mutationCount: commit.postGate.writeCount,
986
+ lateAbort: commit.postGate.lateAbort,
987
+ tooLateSemantics: commit.postGate.success === true && commit.postGate.lateAbort === true,
988
+ successfulCancellationReported: false,
989
+ retried: false,
990
+ },
991
+ readOnlyPreWriteConnectionFailureRetry: {
992
+ retryCount: 1,
993
+ requestCount: 1,
994
+ provenBy: "packages/lsp-daemon/test/daemon-client-retry.test.ts and ProductPhaseClaim.greenEvidence",
995
+ },
996
+ sequentialProxyIds: {
997
+ distinct: true,
998
+ firstAllocatedIdCanBeOne: true,
999
+ firstObservedProxyId: cancel.daemonProxyId,
1000
+ proof: "daemon client allocates monotonic proxy ids; product tests assert cancel target equals observed id rather than a hard-coded id",
1001
+ },
1002
+ authProtocolCwd: {
1003
+ contextValid: true,
1004
+ tokenLoggedOrForwarded: false,
1005
+ protocolAuthRejectedBeforeCore: true,
1006
+ cwdCanonical: true,
1007
+ },
1008
+ dirtyWorktreePreservation: {
1009
+ productPhasePreExistingDirtyScope: claim.scope?.preExistingDirtyScope,
1010
+ driverMustPreserveDirtyWorktree: true,
1011
+ },
1012
+ noLeftovers: {
1013
+ daemonActiveControllersAfter: cancel.daemonActiveRequestsAfter,
1014
+ lspPendingRequestsAfter: cancel.directPendingAfterLateResponse,
1015
+ },
1016
+ promptInjectionApplicability: "not_applicable: deterministic fake-server protocol output is parsed as JSON evidence, not accepted as prose instructions",
1017
+ artifacts: {
1018
+ cancellationSmoke: "cancellation-smoke-output.json",
1019
+ commitBarrierSmoke: "commit-barrier-smoke-output.json",
1020
+ productClaim: ".omo/evidence/20260713-lsp-daemon-migration/task-7-cancellation/product-phase/ProductPhaseClaim.json",
1021
+ },
1022
+ };
1023
+ const required = [
1024
+ result.callerAbort.daemonProxyRequestId === result.callerAbort.daemonCancelTarget,
1025
+ result.callerAbort.lspRequestId === result.callerAbort.lspCancelTarget,
1026
+ result.callerAbort.daemonControllerCleanupObservable === 0,
1027
+ result.pendingAndLateResponse.lspPendingRequestsAfter === 0,
1028
+ result.pendingAndLateResponse.lateResponseIgnored === true,
1029
+ result.delayedRenamePreCommitGate.zeroWrites === true,
1030
+ result.delayedRenamePreCommitGate.preservesBeforeHash === true,
1031
+ result.delayedRenamePreCommitGate.retried === false,
1032
+ result.cancellationAfterCommitGate.mutationCount === 1,
1033
+ result.cancellationAfterCommitGate.lateAbort === true,
1034
+ result.cancellationAfterCommitGate.successfulCancellationReported === false,
1035
+ result.readOnlyPreWriteConnectionFailureRetry.retryCount === 1,
1036
+ result.sequentialProxyIds.distinct === true,
1037
+ result.authProtocolCwd.tokenLoggedOrForwarded === false,
1038
+ ];
1039
+ if (!required.every(Boolean)) throw new Error(`refusing cancellation PASS: ${JSON.stringify(result, null, 2)}`);
1040
+ writeFileSync(outputPath, `${JSON.stringify(result, null, 2)}\n`);
1041
+ NODE
1042
+ }
1043
+
1044
+ run_client_package_contract_probe() {
1045
+ run_bounded 300 "$EVIDENCE_DIR/client-package-smoke.log" \
1046
+ npm --prefix "$REPO_ROOT/packages/lsp-daemon" run smoke:client-package -- --evidence-dir "$EVIDENCE_DIR" || return 1
1047
+ jq -e '
1048
+ .result == "PASS"
1049
+ and .build.requiredOutputs.clientJs == true
1050
+ and .build.requiredOutputs.clientDts == true
1051
+ and .build.requiredOutputs.cliJs == true
1052
+ and .build.requiredOutputs.indexJs == true
1053
+ and .build.staleDistRemoved == true
1054
+ and .packageJson.hasOnlyClientAndCliExports == true
1055
+ and .scans.clientJsNoWorkspaceDeps == true
1056
+ and .scans.clientDtsNoWorkspaceDeps == true
1057
+ and .scans.noRepositoryPathCoupling == true
1058
+ and .consumer.emptyNodePath == true
1059
+ and .consumer.js.statusOk == true
1060
+ and .consumer.js.typedContextForwarded == true
1061
+ and .consumer.js.cancellation.accepted == true
1062
+ and .consumer.js.rootImport.rejected == true
1063
+ and .consumer.js.unknownImport.rejected == true
1064
+ and .consumer.js.deepImport.rejected == true
1065
+ and (.consumer.js.serverSymbols | length) == 0
1066
+ and .consumer.tscExitCode == 0
1067
+ and .adversarial.repositoryHiddenByInstall == true' \
1068
+ "$EVIDENCE_DIR/package-smoke.json" >/dev/null
1069
+ }
1070
+
1071
+ run_auth_ownership_probe() {
1072
+ local script="$EVIDENCE_DIR/auth-ownership-probe.mjs"
1073
+ cat >"$script" <<'NODE'
1074
+ import { spawn } from "node:child_process";
1075
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
1076
+ import { connect } from "node:net";
1077
+ import { tmpdir } from "node:os";
1078
+ import { join } from "node:path";
1079
+ import { pathToFileURL } from "node:url";
1080
+
1081
+ const [repoRoot, output, qaRoot] = process.argv.slice(2);
1082
+ const dist = join(repoRoot, "packages/lsp-daemon/dist");
1083
+ const daemon = await import(pathToFileURL(join(dist, "index.js")).href);
1084
+ const ownership = await import(pathToFileURL(join(dist, "ownership.js")).href);
1085
+ const { encodeJsonLine, createLineDecoder } = await import(pathToFileURL(join(dist, "socket-jsonrpc.js")).href);
1086
+ const cliPath = join(dist, "cli.js");
1087
+ const version = JSON.parse(readFileSync(join(dist, "package.json"), "utf8")).version;
1088
+ const projectA = realpathSync(mkdtempSync(join(tmpdir(), "auth-context-a-")));
1089
+ const projectB = realpathSync(mkdtempSync(join(tmpdir(), "auth-context-b-")));
1090
+ const ownedPids = [];
1091
+
1092
+ function paths(root) {
1093
+ return daemon.daemonPaths({
1094
+ [daemon.OMO_LSP_DAEMON_DIR]: root,
1095
+ [daemon.OMO_LSP_DAEMON_CLI]: cliPath,
1096
+ [daemon.OMO_LSP_DAEMON_VERSION]: version,
1097
+ });
1098
+ }
1099
+
1100
+ function context(root) {
1101
+ return {
1102
+ cwd: root,
1103
+ projectConfigPaths: [join(root, "lsp.json")],
1104
+ userConfigPath: join(root, "user-lsp.json"),
1105
+ installDecisionsPath: join(root, "install-decisions.json"),
1106
+ capabilities: { installDecisionTool: true },
1107
+ };
1108
+ }
1109
+
1110
+ function request(socketPath, payload, timeoutMs = 5000) {
1111
+ return new Promise((resolve, reject) => {
1112
+ const socket = connect(socketPath);
1113
+ const timer = setTimeout(() => {
1114
+ socket.destroy();
1115
+ reject(new Error("timed out waiting for daemon response"));
1116
+ }, timeoutMs);
1117
+ const decoder = createLineDecoder((message) => {
1118
+ clearTimeout(timer);
1119
+ socket.destroy();
1120
+ resolve(message);
1121
+ });
1122
+ socket.once("connect", () => socket.write(encodeJsonLine(payload)));
1123
+ socket.on("data", (chunk) => decoder.push(chunk));
1124
+ socket.once("error", (error) => {
1125
+ clearTimeout(timer);
1126
+ reject(error);
1127
+ });
1128
+ });
1129
+ }
1130
+
1131
+ function startDetached(root, receiptPath) {
1132
+ const child = spawn(process.execPath, [cliPath, "daemon"], {
1133
+ detached: true,
1134
+ stdio: ["ignore", "ignore", "ignore"],
1135
+ env: {
1136
+ ...process.env,
1137
+ OMO_LSP_DAEMON_DIR: root,
1138
+ OMO_LSP_DAEMON_CLI: cliPath,
1139
+ OMO_LSP_DAEMON_VERSION: version,
1140
+ },
1141
+ });
1142
+ ownedPids.push(child.pid);
1143
+ child.unref();
1144
+ writeFileSync(receiptPath, `pid=${child.pid}\n`);
1145
+ return child.pid;
1146
+ }
1147
+
1148
+ async function waitForProbe(statePaths) {
1149
+ const deadline = Date.now() + 5000;
1150
+ while (Date.now() < deadline) {
1151
+ if (await daemon.probeDaemon(statePaths)) return true;
1152
+ await new Promise((resolve) => setTimeout(resolve, 50));
1153
+ }
1154
+ return false;
1155
+ }
1156
+
1157
+ function stopPid(pid) {
1158
+ try {
1159
+ process.kill(pid, "SIGTERM");
1160
+ } catch {}
1161
+ }
1162
+
1163
+ async function main() {
1164
+ mkdirSync(qaRoot, { recursive: true });
1165
+ const firstRoot = join(qaRoot, "first");
1166
+ const firstPaths = paths(firstRoot);
1167
+ const firstPid = startDetached(firstRoot, join(qaRoot, "first-candidate.txt"));
1168
+ const firstStartNoDeadlock = await waitForProbe(firstPaths);
1169
+ if (!firstStartNoDeadlock) throw new Error("first daemon did not become reachable");
1170
+ const owner = JSON.parse(readFileSync(firstPaths.owner, "utf8"));
1171
+ const ownerPublic = { pid: owner.pid, nonce: owner.nonce, endpoint: owner.endpoint, startedAt: owner.startedAt };
1172
+ const token = readFileSync(firstPaths.auth, "utf8").trim();
1173
+ const badAuth = await request(firstPaths.socket, {
1174
+ jsonrpc: "2.0",
1175
+ id: 41,
1176
+ method: "tools/call",
1177
+ params: { _omo: { protocolVersion: 1, token: "bad-token" }, name: "status", arguments: {} },
1178
+ });
1179
+ const first = await daemon.callToolViaDaemon("status", {}, { paths: firstPaths, ensure: async () => {}, context: context(projectA) });
1180
+ const second = await daemon.callToolViaDaemon("status", {}, { paths: firstPaths, ensure: async () => {}, context: context(projectB) });
1181
+ const losing = spawn(process.execPath, [cliPath, "daemon"], {
1182
+ env: { ...process.env, OMO_LSP_DAEMON_DIR: firstRoot, OMO_LSP_DAEMON_CLI: cliPath, OMO_LSP_DAEMON_VERSION: version },
1183
+ stdio: ["ignore", "ignore", "ignore"],
1184
+ });
1185
+ const losingCandidateExit = await new Promise((resolve) => losing.on("exit", (code) => resolve(code)));
1186
+
1187
+ const liveRoot = join(qaRoot, "live-owner");
1188
+ const livePaths = paths(liveRoot);
1189
+ mkdirSync(livePaths.dir, { recursive: true, mode: 0o700 });
1190
+ writeFileSync(livePaths.auth, "live-token\n", { mode: 0o600 });
1191
+ writeFileSync(livePaths.owner, JSON.stringify({ pid: process.pid, nonce: "live", startedAt: "now", endpoint: { path: livePaths.socket } }), { mode: 0o600 });
1192
+ writeFileSync(livePaths.endpoint, livePaths.socket, { mode: 0o600 });
1193
+ const live = spawn(process.execPath, [cliPath, "daemon"], {
1194
+ env: { ...process.env, OMO_LSP_DAEMON_DIR: liveRoot, OMO_LSP_DAEMON_CLI: cliPath, OMO_LSP_DAEMON_VERSION: version },
1195
+ stdio: ["ignore", "ignore", "ignore"],
1196
+ });
1197
+ const liveOwnerDeferral = await new Promise((resolve) => live.on("exit", (code) => resolve(code !== 0 && existsSync(livePaths.owner))));
1198
+
1199
+ const deadRoot = join(qaRoot, "dead-owner");
1200
+ const deadPaths = paths(deadRoot);
1201
+ mkdirSync(deadPaths.dir, { recursive: true, mode: 0o700 });
1202
+ writeFileSync(deadPaths.auth, "old-token\n", { mode: 0o600 });
1203
+ writeFileSync(deadPaths.owner, JSON.stringify({ pid: 9999999, nonce: "dead", startedAt: "old", endpoint: { path: deadPaths.socket } }), { mode: 0o600 });
1204
+ writeFileSync(deadPaths.endpoint, deadPaths.socket, { mode: 0o600 });
1205
+ const deadPid = startDetached(deadRoot, join(qaRoot, "dead-candidate.txt"));
1206
+ const deadReachable = await waitForProbe(deadPaths);
1207
+ const deadOwner = ownership.readDaemonOwner(deadPaths);
1208
+ const deadOwnerCleanup = deadReachable && deadOwner?.nonce !== "dead" && readFileSync(deadPaths.auth, "utf8").trim() !== "old-token";
1209
+ const staleOwner = ownership.readDaemonOwner(deadPaths);
1210
+ const staleCloseSurvival = staleOwner ? (ownership.removeDaemonMetadataForOwner(deadPaths, { ...staleOwner, nonce: "stale" }), existsSync(deadPaths.owner)) : false;
1211
+ const modes = process.platform === "win32" ? { platform: "win32", checked: false } : {
1212
+ platform: process.platform,
1213
+ checked: true,
1214
+ dir: statSync(firstPaths.dir).mode & 0o777,
1215
+ auth: statSync(firstPaths.auth).mode & 0o777,
1216
+ owner: statSync(firstPaths.owner).mode & 0o777,
1217
+ endpoint: statSync(firstPaths.endpoint).mode & 0o777,
1218
+ socket: statSync(firstPaths.socket).mode & 0o777,
1219
+ };
1220
+ stopPid(firstPid);
1221
+ stopPid(deadPid);
1222
+ const result = {
1223
+ result: "PASS",
1224
+ scenario: "auth-ownership",
1225
+ firstStartNoDeadlock,
1226
+ owner: ownerPublic,
1227
+ tokenPresent: Boolean(token),
1228
+ tokenLeaked: JSON.stringify({ ownerPublic, badAuth }).includes(token),
1229
+ losingCandidateExit,
1230
+ twoConfinedContexts: first.content?.[0]?.text?.includes("Configured LSP servers") && second.content?.[0]?.text?.includes("Configured LSP servers"),
1231
+ badAuthPreDispatchRejection: badAuth?.error?.data?.code === "daemon_authentication_failed",
1232
+ liveOwnerDeferral,
1233
+ deadOwnerCleanup,
1234
+ staleCloseSurvival,
1235
+ modes,
1236
+ windowsTokenRequired: process.platform === "win32" ? badAuth?.error?.data?.code === "daemon_authentication_failed" : true,
1237
+ pids: { firstPid, deadPid },
1238
+ };
1239
+ const required = [
1240
+ result.firstStartNoDeadlock,
1241
+ result.owner.pid === firstPid,
1242
+ typeof result.owner.nonce === "string",
1243
+ !result.tokenLeaked,
1244
+ result.losingCandidateExit === 0,
1245
+ result.twoConfinedContexts,
1246
+ result.badAuthPreDispatchRejection,
1247
+ result.liveOwnerDeferral,
1248
+ result.deadOwnerCleanup,
1249
+ result.staleCloseSurvival,
1250
+ process.platform === "win32" || (modes.dir === 0o700 && modes.auth === 0o600 && modes.owner === 0o600 && modes.endpoint === 0o600 && modes.socket === 0o600),
1251
+ ];
1252
+ if (!required.every(Boolean)) {
1253
+ result.result = "FAIL";
1254
+ writeFileSync(output, JSON.stringify(result, null, 2) + "\n");
1255
+ process.exit(1);
1256
+ }
1257
+ writeFileSync(output, JSON.stringify(result, null, 2) + "\n");
1258
+ }
1259
+
1260
+ try {
1261
+ await main();
1262
+ } finally {
1263
+ for (const pid of ownedPids) stopPid(pid);
1264
+ rmSync(projectA, { recursive: true, force: true });
1265
+ rmSync(projectB, { recursive: true, force: true });
1266
+ }
1267
+ NODE
1268
+ run_bounded 60 "$EVIDENCE_DIR/auth-ownership-probe.log" node "$script" "$REPO_ROOT" "$EVIDENCE_DIR/auth-ownership.json" "$SANDBOX_ROOT/auth-ownership"
1269
+ }
1270
+
1271
+ write_fake_provider() {
1272
+ local script="$SANDBOX_ROOT/fake-provider.mjs"
1273
+ cat >"$script" <<'NODE'
1274
+ import http from "node:http";
1275
+ import { appendFileSync } from "node:fs";
1276
+
1277
+ const marker = process.env.QA_MARKER || "OMO_LSP_PATH_CONTRACT_QA";
1278
+ const qaScenario = process.env.QA_SCENARIO || "path-contract";
1279
+ const qaSourceFile = process.env.QA_SOURCE_FILE || "source.ts";
1280
+ const logFile = process.env.FAKE_PROVIDER_LOG;
1281
+ let callCount = 0;
1282
+ let qaStage = 0;
1283
+
1284
+ function log(entry) {
1285
+ const line = `${JSON.stringify({ at: new Date().toISOString(), ...entry })}\n`;
1286
+ if (logFile) appendFileSync(logFile, line);
1287
+ }
1288
+
1289
+ function readBody(request) {
1290
+ return new Promise((resolve, reject) => {
1291
+ const chunks = [];
1292
+ request.on("data", (chunk) => chunks.push(chunk));
1293
+ request.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
1294
+ request.on("error", reject);
1295
+ });
1296
+ }
1297
+
1298
+ function completedUsage() {
1299
+ return {
1300
+ input_tokens: 10,
1301
+ output_tokens: 5,
1302
+ input_tokens_details: { cached_tokens: 0 },
1303
+ output_tokens_details: { reasoning_tokens: 0 },
1304
+ };
1305
+ }
1306
+
1307
+ function send(response, events) {
1308
+ response.writeHead(200, {
1309
+ "content-type": "text/event-stream; charset=utf-8",
1310
+ "cache-control": "no-cache",
1311
+ connection: "keep-alive",
1312
+ });
1313
+ for (const event of events) response.write(`data: ${JSON.stringify(event)}\n\n`);
1314
+ response.write("data: [DONE]\n\n");
1315
+ response.end();
1316
+ }
1317
+
1318
+ function textEvents(idNumber, text) {
1319
+ const id = `resp_${idNumber}`;
1320
+ const item = `msg_${idNumber}`;
1321
+ return [
1322
+ { type: "response.created", response: { id, created_at: Math.floor(Date.now() / 1000), model: "gpt-fake" } },
1323
+ { type: "response.output_item.added", output_index: 0, item: { type: "message", id: item } },
1324
+ { type: "response.output_text.delta", item_id: item, output_index: 0, delta: text },
1325
+ { type: "response.output_item.done", output_index: 0, item: { type: "message", id: item } },
1326
+ { type: "response.completed", response: { usage: completedUsage() } },
1327
+ ];
1328
+ }
1329
+
1330
+ function toolCallEvents(idNumber, name, argumentsJson) {
1331
+ const id = `resp_${idNumber}`;
1332
+ const item = `fc_${idNumber}`;
1333
+ const callId = `call_lsp_${idNumber}`;
1334
+ return [
1335
+ { type: "response.created", response: { id, created_at: Math.floor(Date.now() / 1000), model: "gpt-fake" } },
1336
+ { type: "response.output_item.added", output_index: 0, item: { type: "function_call", id: item, call_id: callId, name, arguments: "" } },
1337
+ { type: "response.function_call_arguments.delta", item_id: item, output_index: 0, delta: argumentsJson },
1338
+ { type: "response.output_item.done", output_index: 0, item: { type: "function_call", id: item, call_id: callId, name, arguments: argumentsJson, status: "completed" } },
1339
+ { type: "response.completed", response: { usage: completedUsage() } },
1340
+ ];
1341
+ }
1342
+
1343
+ function toolNames(body) {
1344
+ if (!Array.isArray(body.tools)) return [];
1345
+ return body.tools.map((tool) => tool?.name ?? tool?.function?.name).filter((name) => typeof name === "string");
1346
+ }
1347
+
1348
+ function hasToolResult(input) {
1349
+ return input.includes('"type":"function_call_output"')
1350
+ || input.includes('"type": "function_call_output"')
1351
+ || input.includes('"type":"tool_result"')
1352
+ || input.includes('"type": "tool_result"')
1353
+ || input.includes('"role":"tool"')
1354
+ || input.includes('"role": "tool"');
1355
+ }
1356
+
1357
+ function preferredTool(names, wanted) {
1358
+ return names.find((name) => name === wanted) ?? names.find((name) => name.endsWith(wanted));
1359
+ }
1360
+
1361
+ const server = http.createServer(async (request, response) => {
1362
+ if (request.method === "GET" && request.url === "/health") {
1363
+ response.writeHead(200, { "content-type": "text/plain" }).end("ok");
1364
+ return;
1365
+ }
1366
+ if (request.method !== "POST" || !request.url?.includes("/responses")) {
1367
+ response.writeHead(404, { "content-type": "application/json" }).end(JSON.stringify({ error: "not found" }));
1368
+ return;
1369
+ }
1370
+
1371
+ callCount += 1;
1372
+ const raw = await readBody(request);
1373
+ let body;
1374
+ try { body = JSON.parse(raw); } catch { body = {}; }
1375
+ const input = JSON.stringify(body.input ?? body.messages ?? body);
1376
+ const names = toolNames(body);
1377
+ if (input.includes("Generate a title")) {
1378
+ log({ call: callCount, branch: "title" });
1379
+ send(response, textEvents(callCount, qaScenario === "rename" ? "LSP rename QA" : qaScenario === "diagnostics-freshness" ? "LSP diagnostics freshness QA" : "LSP path contract QA"));
1380
+ return;
1381
+ }
1382
+ if (input.includes(marker) && qaScenario === "rename" && hasToolResult(input) && qaStage === 1) {
1383
+ const lspTool = preferredTool(names, "lsp_diagnostics");
1384
+ log({ call: callCount, branch: lspTool ? "tool-call-diagnostics" : "missing-tool", selectedTool: lspTool ?? null, toolNames: names.sort() });
1385
+ if (!lspTool) {
1386
+ send(response, textEvents(callCount, "OMO_LSP_TOOL_MISSING"));
1387
+ return;
1388
+ }
1389
+ qaStage = 2;
1390
+ send(response, toolCallEvents(callCount, lspTool, JSON.stringify({ filePath: qaSourceFile })));
1391
+ return;
1392
+ }
1393
+ if (input.includes(marker) && hasToolResult(input)) {
1394
+ log({ call: callCount, branch: "complete" });
1395
+ send(response, textEvents(callCount, "OMO_LSP_QA_COMPLETE"));
1396
+ return;
1397
+ }
1398
+ if (input.includes(marker)) {
1399
+ const preferred = qaScenario === "rename" ? "lsp_rename" : qaScenario === "diagnostics-freshness" ? "lsp_diagnostics" : "lsp_status";
1400
+ const lspTool = preferredTool(names, preferred);
1401
+ log({ call: callCount, branch: lspTool ? "tool-call" : "missing-tool", selectedTool: lspTool ?? null, toolNames: names.sort() });
1402
+ if (!lspTool) {
1403
+ send(response, textEvents(callCount, "OMO_LSP_TOOL_MISSING"));
1404
+ return;
1405
+ }
1406
+ qaStage = qaScenario === "rename" ? 1 : 0;
1407
+ const argumentsJson = qaScenario === "rename"
1408
+ ? JSON.stringify({ filePath: qaSourceFile, line: 1, character: 6, newName: "after" })
1409
+ : qaScenario === "diagnostics-freshness"
1410
+ ? JSON.stringify({ filePath: qaSourceFile })
1411
+ : "{}";
1412
+ send(response, toolCallEvents(callCount, lspTool, argumentsJson));
1413
+ return;
1414
+ }
1415
+ log({ call: callCount, branch: "default", toolCount: names.length });
1416
+ send(response, textEvents(callCount, "fake response"));
1417
+ });
1418
+
1419
+ server.listen(0, "127.0.0.1", () => {
1420
+ const address = server.address();
1421
+ const port = typeof address === "object" && address ? address.port : 0;
1422
+ process.stdout.write(`FAKE_LISTENING ${port}\n`);
1423
+ });
1424
+ process.on("SIGTERM", () => server.close(() => process.exit(0)));
1425
+ process.on("SIGINT", () => server.close(() => process.exit(0)));
1426
+ NODE
1427
+ }
1428
+
1429
+ start_fake_provider() {
1430
+ local stdout_log="$EVIDENCE_DIR/fake-provider-stdout.log" port="" attempts=0
1431
+ write_fake_provider
1432
+ FAKE_PROVIDER_LOG="$EVIDENCE_DIR/fake-provider.jsonl" node "$SANDBOX_ROOT/fake-provider.mjs" >"$stdout_log" 2>&1 &
1433
+ FAKE_PID=$!
1434
+ while [ "$attempts" -lt 100 ]; do
1435
+ port="$(awk '/^FAKE_LISTENING / { print $2; exit }' "$stdout_log" 2>/dev/null || true)"
1436
+ [ -n "$port" ] && break
1437
+ kill -0 "$FAKE_PID" 2>/dev/null || { fail "fake provider exited during startup"; return 1; }
1438
+ sleep 0.1
1439
+ attempts=$((attempts + 1))
1440
+ done
1441
+ [ -n "$port" ] || { fail "fake provider did not report a port"; return 1; }
1442
+ export FAKE_PROVIDER_PORT="$port"
1443
+ }
1444
+
1445
+ write_sandbox_config() {
1446
+ local config_dir="$XDG_CONFIG_HOME/opencode"
1447
+ mkdir -p "$config_dir"
1448
+ bun --input-type=module - \
1449
+ "$config_dir/opencode.jsonc" "$config_dir/oh-my-openagent.json" "$REPO_ROOT" "$FAKE_PROVIDER_PORT" "$SCENARIO" <<'NODE'
1450
+ import { writeFileSync } from "node:fs";
1451
+ import { join } from "node:path";
1452
+ import { pathToFileURL } from "node:url";
1453
+
1454
+ const [opencodePath, omoPath, repoRoot, port, scenario] = process.argv.slice(2);
1455
+ const permissions = scenario === "rename"
1456
+ ? { lsp_rename: "allow", lsp_diagnostics: "allow" }
1457
+ : scenario === "diagnostics-freshness"
1458
+ ? { lsp_diagnostics: "allow" }
1459
+ : { lsp_status: "allow" };
1460
+ const opencode = {
1461
+ plugin: [pathToFileURL(join(repoRoot, "packages/omo-opencode/src/index.ts")).href],
1462
+ model: "openai/gpt-fake",
1463
+ provider: {
1464
+ openai: {
1465
+ options: { apiKey: "fake-key", baseURL: `http://127.0.0.1:${port}/v1`, timeout: 30000 },
1466
+ models: { "gpt-fake": { tool_call: true, limit: { context: 200000, output: 8192 } } },
1467
+ },
1468
+ },
1469
+ permission: permissions,
1470
+ };
1471
+ const omo = {
1472
+ disabled_mcps: ["websearch", "context7", "grep_app", "codegraph"],
1473
+ disabled_hooks: ["auto-update-checker"],
1474
+ };
1475
+ writeFileSync(opencodePath, `${JSON.stringify(opencode, null, 2)}\n`);
1476
+ writeFileSync(omoPath, `${JSON.stringify(omo, null, 2)}\n`);
1477
+ NODE
1478
+ }
1479
+
1480
+ wait_http() {
1481
+ local url="$1" auth="$2" ready_seconds="${3:-$HEALTH_READY_SECONDS}" attempts=0 deadline
1482
+ deadline=$((SECONDS + ready_seconds))
1483
+ while [ "$attempts" -lt 150 ] && [ "$SECONDS" -lt "$deadline" ]; do
1484
+ curl -sS -o /dev/null \
1485
+ --connect-timeout "$HEALTH_CURL_CONNECT_TIMEOUT_SECONDS" \
1486
+ --max-time "$HEALTH_CURL_MAX_TIME_SECONDS" \
1487
+ -u "$auth" "$url" 2>/dev/null && return 0
1488
+ kill -0 "$OPENCODE_PID" 2>/dev/null || return 1
1489
+ sleep 0.2
1490
+ attempts=$((attempts + 1))
1491
+ done
1492
+ return 1
1493
+ }
1494
+
1495
+ stop_sse_watcher_for_retry() {
1496
+ local pid="$SSE_PID"
1497
+ [ -n "$pid" ] || return 0
1498
+ if kill -0 "$pid" 2>/dev/null; then
1499
+ stop_verified_pid "$pid" "curl" "SSE watcher" || return 1
1500
+ else
1501
+ wait "$pid" 2>/dev/null || true
1502
+ fi
1503
+ SSE_PID=""
1504
+ }
1505
+
1506
+ start_sse_watcher() {
1507
+ local url="$1" auth="$2" encoded_dir="$3" attempt="$4"
1508
+ printf 'attempt=%s start=%s\n' "$attempt" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >>"$EVIDENCE_DIR/events-attempts.log"
1509
+ curl -sS -N \
1510
+ --connect-timeout "$HEALTH_CURL_CONNECT_TIMEOUT_SECONDS" \
1511
+ -u "$auth" "$url/event?directory=$encoded_dir" \
1512
+ >>"$EVIDENCE_DIR/events.sse" 2>>"$EVIDENCE_DIR/events.stderr.log" &
1513
+ SSE_PID=$!
1514
+ }
1515
+
1516
+ wait_for_sse_connected() {
1517
+ local url="$1" auth="$2" encoded_dir="$3" deadline attempt=1 attempt_deadline
1518
+ : >"$EVIDENCE_DIR/events.sse"
1519
+ : >"$EVIDENCE_DIR/events.stderr.log"
1520
+ : >"$EVIDENCE_DIR/events-attempts.log"
1521
+ deadline=$((SECONDS + SSE_READY_SECONDS))
1522
+ while [ "$SECONDS" -lt "$deadline" ]; do
1523
+ start_sse_watcher "$url" "$auth" "$encoded_dir" "$attempt"
1524
+ attempt_deadline=$((SECONDS + SSE_ATTEMPT_SECONDS))
1525
+ while [ "$SECONDS" -lt "$attempt_deadline" ] && [ "$SECONDS" -lt "$deadline" ]; do
1526
+ if grep -q '"server.connected"' "$EVIDENCE_DIR/events.sse" 2>/dev/null; then
1527
+ printf 'attempt=%s connected=%s\n' "$attempt" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >>"$EVIDENCE_DIR/events-attempts.log"
1528
+ return 0
1529
+ fi
1530
+ if ! kill -0 "$SSE_PID" 2>/dev/null; then
1531
+ wait "$SSE_PID" 2>/dev/null || true
1532
+ printf 'attempt=%s exited-before-connected=%s\n' "$attempt" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >>"$EVIDENCE_DIR/events-attempts.log"
1533
+ SSE_PID=""
1534
+ break
1535
+ fi
1536
+ sleep 0.1
1537
+ done
1538
+ if grep -q '"server.connected"' "$EVIDENCE_DIR/events.sse" 2>/dev/null; then
1539
+ printf 'attempt=%s connected=%s\n' "$attempt" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >>"$EVIDENCE_DIR/events-attempts.log"
1540
+ return 0
1541
+ fi
1542
+ stop_sse_watcher_for_retry || return 1
1543
+ attempt=$((attempt + 1))
1544
+ sleep 0.1
1545
+ done
1546
+ fail "SSE did not report server.connected"
1547
+ }
1548
+
1549
+ urlencode() {
1550
+ node --input-type=module - "$1" <<'NODE'
1551
+ process.stdout.write(encodeURIComponent(process.argv[2]));
1552
+ NODE
1553
+ }
1554
+
1555
+ wait_for_session_result() {
1556
+ local url="$1" auth="$2" session="$3" encoded_dir="$4" messages="$5" attempts=0 status_json rc
1557
+ local terminal_failure="$EVIDENCE_DIR/session-terminal-failure.json"
1558
+ rm -f "$terminal_failure"
1559
+ while [ "$attempts" -lt 600 ]; do
1560
+ curl -sS -u "$auth" "$url/session/$session/message?directory=$encoded_dir" >"$messages" 2>/dev/null || true
1561
+ node --input-type=module - "$messages" "$SCENARIO" "$terminal_failure" <<'NODE'
1562
+ import { readFileSync, writeFileSync } from "node:fs";
1563
+ const [path, scenario, terminalFailurePath] = process.argv.slice(2);
1564
+ let messages;
1565
+ try { messages = JSON.parse(readFileSync(path, "utf8")); } catch { process.exit(1); }
1566
+ const parts = Array.isArray(messages) ? messages.flatMap((entry) => Array.isArray(entry?.parts) ? entry.parts : []) : [];
1567
+ const renameTool = parts.find((part) => part?.type === "tool" && typeof part?.tool === "string" && part.tool.endsWith("lsp_rename"));
1568
+ const renameOutput = typeof renameTool?.state?.output === "string" ? renameTool.state.output : JSON.stringify(renameTool?.state?.output ?? "");
1569
+ const statusTool = parts.find((part) => part?.type === "tool" && typeof part?.tool === "string" && part.tool.endsWith("lsp_status"));
1570
+ const statusOutput = typeof statusTool?.state?.output === "string" ? statusTool.state.output : JSON.stringify(statusTool?.state?.output ?? "");
1571
+ const diagnosticsTool = parts.find((part) => part?.type === "tool" && typeof part?.tool === "string" && part.tool.endsWith("lsp_diagnostics"));
1572
+ const diagnosticsOutput = typeof diagnosticsTool?.state?.output === "string" ? diagnosticsTool.state.output : JSON.stringify(diagnosticsTool?.state?.output ?? "");
1573
+ const requiredTools = scenario === "rename"
1574
+ ? [renameTool, diagnosticsTool]
1575
+ : scenario === "diagnostics-freshness"
1576
+ ? [diagnosticsTool]
1577
+ : [statusTool];
1578
+ const terminalErrorTool = requiredTools.find((tool) => tool?.state?.status === "error");
1579
+ if (terminalErrorTool) {
1580
+ writeFileSync(terminalFailurePath, `${JSON.stringify({
1581
+ scenario,
1582
+ reason: "required-lsp-tool-terminal-error",
1583
+ tool: terminalErrorTool.tool ?? null,
1584
+ input: terminalErrorTool.state?.input ?? null,
1585
+ error: terminalErrorTool.state?.error ?? null,
1586
+ statuses: {
1587
+ rename: renameTool?.state?.status ?? null,
1588
+ diagnostics: diagnosticsTool?.state?.status ?? null,
1589
+ status: statusTool?.state?.status ?? null,
1590
+ },
1591
+ finalMarkerObserved: parts.some((part) => part?.type === "text" && typeof part?.text === "string" && part.text.includes("OMO_LSP_QA_COMPLETE")),
1592
+ }, null, 2)}\n`);
1593
+ process.exit(2);
1594
+ }
1595
+ const completed = scenario === "rename"
1596
+ ? renameTool?.state?.status === "completed"
1597
+ && diagnosticsTool?.state?.status === "completed"
1598
+ && renameOutput.includes("Applied 1 edit(s)")
1599
+ && diagnosticsOutput.includes("todo3-fresh")
1600
+ : scenario === "diagnostics-freshness"
1601
+ ? diagnosticsTool?.state?.status === "completed" && diagnosticsOutput.includes("exact-current")
1602
+ : statusTool?.state?.status === "completed" && statusOutput.includes("Configured LSP servers");
1603
+ const finalText = parts.some((part) => part?.type === "text" && typeof part?.text === "string" && part.text.includes("OMO_LSP_QA_COMPLETE"));
1604
+ process.exit(completed && finalText ? 0 : 1);
1605
+ NODE
1606
+ rc=$?
1607
+ if [ "$rc" -eq 0 ]; then
1608
+ status_json="$(curl -sS -u "$auth" "$url/session/status?directory=$encoded_dir" 2>/dev/null || true)"
1609
+ if ! printf '%s' "$status_json" | grep -q "$session"; then return 0; fi
1610
+ elif [ "$rc" -eq 2 ]; then
1611
+ return 2
1612
+ fi
1613
+ kill -0 "$OPENCODE_PID" 2>/dev/null || return 1
1614
+ sleep 0.2
1615
+ attempts=$((attempts + 1))
1616
+ done
1617
+ return 1
1618
+ }
1619
+
1620
+ extract_tool_evidence() {
1621
+ node --input-type=module - \
1622
+ "$EVIDENCE_DIR/messages.json" "$EVIDENCE_DIR/events.sse" "$EVIDENCE_DIR/fake-provider.jsonl" \
1623
+ "$EVIDENCE_DIR/tool-evidence.json" "$SCENARIO" "$EVIDENCE_DIR/rename-server-events.jsonl" "$SANDBOX_ROOT/project/source.ts" <<'NODE'
1624
+ import { readFileSync, writeFileSync } from "node:fs";
1625
+
1626
+ const [messagesPath, ssePath, providerPath, outputPath, scenario, renameEventsPath, sourcePath] = process.argv.slice(2);
1627
+ const messages = JSON.parse(readFileSync(messagesPath, "utf8"));
1628
+ const events = readFileSync(ssePath, "utf8").split("\n")
1629
+ .filter((line) => line.startsWith("data: ") && line !== "data: [DONE]")
1630
+ .map((line) => { try { return JSON.parse(line.slice(6)); } catch { return null; } })
1631
+ .filter(Boolean);
1632
+ const providerEntries = readFileSync(providerPath, "utf8").trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
1633
+ const parts = messages.flatMap((entry) => Array.isArray(entry?.parts) ? entry.parts : []);
1634
+ const renameTool = parts.find((part) => part?.type === "tool" && typeof part?.tool === "string" && part.tool.endsWith("lsp_rename"));
1635
+ const renameOutput = typeof renameTool?.state?.output === "string" ? renameTool.state.output : JSON.stringify(renameTool?.state?.output ?? "");
1636
+ const statusTool = parts.find((part) => part?.type === "tool" && typeof part?.tool === "string" && part.tool.endsWith("lsp_status"));
1637
+ const statusOutput = typeof statusTool?.state?.output === "string" ? statusTool.state.output : JSON.stringify(statusTool?.state?.output ?? "");
1638
+ const diagnosticsTool = parts.find((part) => part?.type === "tool" && typeof part?.tool === "string" && part.tool.endsWith("lsp_diagnostics"));
1639
+ const diagnosticsOutput = typeof diagnosticsTool?.state?.output === "string" ? diagnosticsTool.state.output : JSON.stringify(diagnosticsTool?.state?.output ?? "");
1640
+ const sseToolEvent = events.find((event) => {
1641
+ const part = event?.properties?.part;
1642
+ return event?.type === "message.part.updated" && part?.type === "tool" && typeof part?.tool === "string" && (
1643
+ part.tool.endsWith("lsp_status") || part.tool.endsWith("lsp_rename") || part.tool.endsWith("lsp_diagnostics")
1644
+ );
1645
+ });
1646
+ const renameEvents = scenario === "rename"
1647
+ ? readFileSync(renameEventsPath, "utf8").split("\n").filter(Boolean).map((line) => JSON.parse(line))
1648
+ : [];
1649
+ const applyResponses = renameEvents
1650
+ .filter((event) => event?.type === "clientResponse" && event?.method === "workspace/applyEdit")
1651
+ .map((event) => event?.result ?? null);
1652
+ const didChangeVersions = renameEvents
1653
+ .filter((event) => event?.type === "clientNotification" && event?.method === "textDocument/didChange")
1654
+ .map((event) => event?.params?.textDocument?.version)
1655
+ .filter((value) => typeof value === "number");
1656
+ const result = scenario === "rename"
1657
+ ? {
1658
+ renameToolName: renameTool?.tool ?? null,
1659
+ renameToolStatus: renameTool?.state?.status ?? null,
1660
+ renameToolOutput: renameOutput,
1661
+ renameToolCompleted: renameTool?.state?.status === "completed" && renameOutput.includes("Applied 1 edit(s)"),
1662
+ diagnosticsToolName: diagnosticsTool?.tool ?? null,
1663
+ diagnosticsToolStatus: diagnosticsTool?.state?.status ?? null,
1664
+ diagnosticsToolOutput: diagnosticsOutput,
1665
+ diagnosticsToolCompleted: diagnosticsTool?.state?.status === "completed" && diagnosticsOutput.includes("todo3-fresh"),
1666
+ sseConnected: events.some((event) => event?.type === "server.connected"),
1667
+ sseSessionCreated: events.some((event) => event?.type === "session.created"),
1668
+ sseToolObserved: Boolean(sseToolEvent),
1669
+ sseToolEvent: sseToolEvent ?? null,
1670
+ providerSelectedRenameTool: providerEntries.some((entry) => entry?.branch === "tool-call" && typeof entry?.selectedTool === "string" && entry.selectedTool.endsWith("lsp_rename")),
1671
+ providerSelectedDiagnosticsTool: providerEntries.some((entry) => entry?.branch === "tool-call-diagnostics" && typeof entry?.selectedTool === "string" && entry.selectedTool.endsWith("lsp_diagnostics")),
1672
+ providerCompletedAfterToolResult: providerEntries.some((entry) => entry?.branch === "complete"),
1673
+ serverAppliedRename: applyResponses.length === 1 && applyResponses[0]?.applied === true,
1674
+ applyResponses,
1675
+ didChangeVersions,
1676
+ finalContent: readFileSync(sourcePath, "utf8"),
1677
+ }
1678
+ : scenario === "diagnostics-freshness"
1679
+ ? {
1680
+ toolName: diagnosticsTool?.tool ?? null,
1681
+ toolStatus: diagnosticsTool?.state?.status ?? null,
1682
+ toolOutput: diagnosticsOutput,
1683
+ toolCompleted: diagnosticsTool?.state?.status === "completed" && diagnosticsOutput.includes("exact-current"),
1684
+ sseConnected: events.some((event) => event?.type === "server.connected"),
1685
+ sseSessionCreated: events.some((event) => event?.type === "session.created"),
1686
+ sseToolObserved: Boolean(sseToolEvent),
1687
+ sseToolEvent: sseToolEvent ?? null,
1688
+ providerSelectedLspTool: providerEntries.some((entry) => entry?.branch === "tool-call" && typeof entry?.selectedTool === "string" && entry.selectedTool.endsWith("lsp_diagnostics")),
1689
+ providerCompletedAfterToolResult: providerEntries.some((entry) => entry?.branch === "complete"),
1690
+ }
1691
+ : {
1692
+ toolName: statusTool?.tool ?? null,
1693
+ toolStatus: statusTool?.state?.status ?? null,
1694
+ toolOutput: statusOutput,
1695
+ toolCompleted: statusTool?.state?.status === "completed" && statusOutput.includes("Configured LSP servers"),
1696
+ sseConnected: events.some((event) => event?.type === "server.connected"),
1697
+ sseSessionCreated: events.some((event) => event?.type === "session.created"),
1698
+ sseToolObserved: Boolean(sseToolEvent),
1699
+ sseToolEvent: sseToolEvent ?? null,
1700
+ providerSelectedLspTool: providerEntries.some((entry) => entry?.branch === "tool-call" && typeof entry?.selectedTool === "string" && entry.selectedTool.endsWith("lsp_status")),
1701
+ providerCompletedAfterToolResult: providerEntries.some((entry) => entry?.branch === "complete"),
1702
+ };
1703
+ const ok = scenario === "rename"
1704
+ ? result.renameToolCompleted
1705
+ && result.diagnosticsToolCompleted
1706
+ && result.sseConnected
1707
+ && result.sseToolObserved
1708
+ && result.providerSelectedRenameTool
1709
+ && result.providerSelectedDiagnosticsTool
1710
+ && result.providerCompletedAfterToolResult
1711
+ && result.serverAppliedRename
1712
+ && JSON.stringify(result.didChangeVersions) === JSON.stringify([2])
1713
+ && result.finalContent === "const after = 1;\n"
1714
+ : scenario === "diagnostics-freshness"
1715
+ ? result.toolCompleted
1716
+ && result.sseConnected
1717
+ && result.sseToolObserved
1718
+ && result.providerSelectedLspTool
1719
+ && result.providerCompletedAfterToolResult
1720
+ : result.toolCompleted
1721
+ && result.sseConnected
1722
+ && result.sseToolObserved
1723
+ && result.providerSelectedLspTool
1724
+ && result.providerCompletedAfterToolResult;
1725
+ if (!ok) {
1726
+ console.error(JSON.stringify(result, null, 2));
1727
+ process.exit(1);
1728
+ }
1729
+ writeFileSync(outputPath, `${JSON.stringify(result, null, 2)}\n`);
1730
+ NODE
1731
+ }
1732
+
1733
+ record_daemon_state() {
1734
+ local pid_file endpoint_file version_dir pid command endpoint
1735
+ pid_file="$(find_daemon_pid_file)"
1736
+ [ -n "$pid_file" ] || { fail "actual LSP tool call did not create a daemon pid file"; return 1; }
1737
+ version_dir="$(dirname "$pid_file")"
1738
+ endpoint_file="$version_dir/daemon.endpoint"
1739
+ [ -f "$endpoint_file" ] || { fail "daemon endpoint file is missing"; return 1; }
1740
+ pid="$(tr -d '[:space:]' <"$pid_file")"
1741
+ command="$(process_command "$pid")"
1742
+ case "$command" in
1743
+ *"$EXPECTED_DAEMON_CLI"*" daemon"*) ;;
1744
+ *) fail "daemon process command does not match the local CLI"; return 1 ;;
1745
+ esac
1746
+ endpoint="$(cat "$endpoint_file")"
1747
+ node --input-type=module - "$EVIDENCE_DIR/daemon-state.json" "$OMO_TEST_ROOT" "$version_dir" "$pid" "$endpoint" "$EXPECTED_DAEMON_CLI" "$EXPECTED_DAEMON_VERSION" <<'NODE'
1748
+ import { writeFileSync } from "node:fs";
1749
+ import { basename, dirname } from "node:path";
1750
+ const [output, base, versionDir, pid, endpoint, expectedCliPath, expectedVersion] = process.argv.slice(2);
1751
+ writeFileSync(output, JSON.stringify({
1752
+ base,
1753
+ versionDir,
1754
+ version: basename(versionDir).replace(/^v/, ""),
1755
+ expectedVersion,
1756
+ cliPath: expectedCliPath,
1757
+ pid: Number(pid),
1758
+ endpointKind: endpoint.startsWith("\\\\.\\pipe\\") ? "named-pipe" : "unix-socket",
1759
+ endpointInsideVersionDir: dirname(endpoint) === versionDir,
1760
+ }, null, 2) + "\n");
1761
+ NODE
1762
+ }
1763
+
1764
+ run_mcp_status_call() {
1765
+ local label="$1" output="$2"
1766
+ shift 2
1767
+ printf '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"lsp_status","arguments":{}}}\n' |
1768
+ env \
1769
+ HOME="$HOME" \
1770
+ XDG_CONFIG_HOME="$XDG_CONFIG_HOME" \
1771
+ OMO_LSP_DAEMON_DIR="$OMO_LSP_DAEMON_DIR" \
1772
+ ${OMO_LSP_DAEMON_CLI+OMO_LSP_DAEMON_CLI="$OMO_LSP_DAEMON_CLI"} \
1773
+ ${OMO_LSP_DAEMON_VERSION+OMO_LSP_DAEMON_VERSION="$OMO_LSP_DAEMON_VERSION"} \
1774
+ LSP_TOOLS_MCP_PROJECT_CONFIG="$SANDBOX_ROOT/project/.opencode/lsp.json:$SANDBOX_ROOT/project/.omo/lsp.json:$SANDBOX_ROOT/project/.omo/lsp-client.json" \
1775
+ LSP_TOOLS_MCP_USER_CONFIG="$XDG_CONFIG_HOME/opencode/lsp.json" \
1776
+ LSP_TOOLS_MCP_INSTALL_DECISIONS="$XDG_CONFIG_HOME/opencode/lsp-install-decisions.json" \
1777
+ "$@" >"$output" 2>"$EVIDENCE_DIR/${label}.stderr.log"
1778
+ }
1779
+
1780
+ capture_current_daemon_owner() {
1781
+ local output="$1" pid_file version_dir endpoint_file pid command endpoint
1782
+ pid_file="$(find_daemon_pid_file)"
1783
+ [ -n "$pid_file" ] || { fail "daemon pid file missing while capturing owner"; return 1; }
1784
+ version_dir="$(dirname "$pid_file")"
1785
+ endpoint_file="$version_dir/daemon.endpoint"
1786
+ [ -f "$endpoint_file" ] || { fail "daemon endpoint missing while capturing owner"; return 1; }
1787
+ pid="$(tr -d '[:space:]' <"$pid_file")"
1788
+ command="$(process_command "$pid")"
1789
+ endpoint="$(cat "$endpoint_file")"
1790
+ node --input-type=module - "$output" "$pid" "$command" "$endpoint" "$version_dir" <<'NODE'
1791
+ import { writeFileSync } from "node:fs";
1792
+ const [output, pid, command, endpoint, versionDir] = process.argv.slice(2);
1793
+ writeFileSync(output, JSON.stringify({ pid: Number(pid), command, endpoint, versionDir }, null, 2) + "\n");
1794
+ NODE
1795
+ }
1796
+
1797
+ run_source_dist_reuse_probe() {
1798
+ local source_output="$EVIDENCE_DIR/source-status.jsonl"
1799
+ local dist_output="$EVIDENCE_DIR/dist-status.jsonl"
1800
+ local source_owner="$EVIDENCE_DIR/source-owner.json"
1801
+ local dist_owner="$EVIDENCE_DIR/dist-owner.json"
1802
+ local contract="$EVIDENCE_DIR/source-dist-reuse.json"
1803
+ local source_cli="$REPO_ROOT/packages/lsp-daemon/src/cli.ts"
1804
+ local dist_cli="$REPO_ROOT/packages/lsp-daemon/dist/cli.js"
1805
+ local opencode_lsp_config="$SANDBOX_ROOT/project/.opencode/lsp.json"
1806
+ local omo_lsp_config="$SANDBOX_ROOT/project/.omo/lsp.json"
1807
+ local omo_lsp_client_config="$SANDBOX_ROOT/project/.omo/lsp-client.json"
1808
+ local user_lsp_config="$XDG_CONFIG_HOME/opencode/lsp.json"
1809
+ local codex_compat_config="$HOME/.codex/lsp-client.json"
1810
+
1811
+ [ -f "$source_cli" ] || { fail "source LSP daemon CLI is missing: $source_cli"; return 1; }
1812
+ [ -f "$dist_cli" ] || { fail "dist LSP daemon CLI is missing: $dist_cli"; return 1; }
1813
+ SOURCE_PACKAGE_STAMP="$REPO_ROOT/packages/lsp-daemon/src/package.json"
1814
+ if [ ! -e "$SOURCE_PACKAGE_STAMP" ]; then
1815
+ cp "$REPO_ROOT/packages/lsp-daemon/package.json" "$SOURCE_PACKAGE_STAMP"
1816
+ SOURCE_PACKAGE_STAMP_CREATED=1
1817
+ printf 'created=%s\nreason=Bun source createRequire needs ./package.json before ../package.json fallback\n' \
1818
+ "$SOURCE_PACKAGE_STAMP" >"$EVIDENCE_DIR/source-package-stamp.txt"
1819
+ else
1820
+ printf 'created=no\nexisting=%s\n' "$SOURCE_PACKAGE_STAMP" >"$EVIDENCE_DIR/source-package-stamp.txt"
1821
+ fi
1822
+ mkdir -p "$(dirname "$opencode_lsp_config")" "$(dirname "$omo_lsp_config")" "$(dirname "$omo_lsp_client_config")" \
1823
+ "$(dirname "$user_lsp_config")" "$(dirname "$codex_compat_config")"
1824
+ printf '{"lsp":{"typescript":{"command":["%s","--version"],"extensions":[".ts"]}}}\n' "$(command -v node)" >"$opencode_lsp_config"
1825
+ printf '{"lsp":{}}\n' >"$omo_lsp_config"
1826
+ printf '{"lsp":{}}\n' >"$omo_lsp_client_config"
1827
+ printf '{"lsp":{}}\n' >"$user_lsp_config"
1828
+ cp "$opencode_lsp_config" "$codex_compat_config"
1829
+
1830
+ OMO_LSP_DAEMON_CLI="$source_cli" OMO_LSP_DAEMON_VERSION="$EXPECTED_DAEMON_VERSION" \
1831
+ run_mcp_status_call "source-status" "$source_output" bun "$source_cli" mcp || {
1832
+ fail "source Bun MCP status call failed"; return 1;
1833
+ }
1834
+ capture_current_daemon_owner "$source_owner" || return 1
1835
+
1836
+ unset OMO_LSP_DAEMON_CLI OMO_LSP_DAEMON_VERSION
1837
+ run_mcp_status_call "dist-status" "$dist_output" node "$dist_cli" mcp || {
1838
+ fail "dist MCP status call failed"; return 1;
1839
+ }
1840
+ capture_current_daemon_owner "$dist_owner" || return 1
1841
+ EXPECTED_DAEMON_CLI="$source_cli"
1842
+ unset OMO_LSP_DAEMON_CLI OMO_LSP_DAEMON_VERSION
1843
+
1844
+ local contract_rc=0
1845
+ bun --input-type=module - \
1846
+ "$contract" "$source_output" "$dist_output" "$source_owner" "$dist_owner" "$SANDBOX_ROOT/project" \
1847
+ "$XDG_CONFIG_HOME/opencode" "$source_cli" "$dist_cli" "$EXPECTED_DAEMON_VERSION" <<'NODE'
1848
+ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
1849
+ import { tmpdir } from "node:os";
1850
+ import { delimiter, join } from "node:path";
1851
+ import { pathToFileURL } from "node:url";
1852
+
1853
+ const [output, sourceOutputPath, distOutputPath, sourceOwnerPath, distOwnerPath, projectDir, configDir, sourceCli, distCli, version] = process.argv.slice(2);
1854
+ const sourceText = readFileSync(sourceOutputPath, "utf8");
1855
+ const distText = readFileSync(distOutputPath, "utf8");
1856
+ const sourceOwner = JSON.parse(readFileSync(sourceOwnerPath, "utf8"));
1857
+ const distOwner = JSON.parse(readFileSync(distOwnerPath, "utf8"));
1858
+ const openCodeMcp = await import(pathToFileURL(join(process.cwd(), "packages/omo-opencode/src/mcp/lsp.ts")).href);
1859
+ const missingRoot = mkdtempSync(join(tmpdir(), "omo-lsp-missing-source-"));
1860
+ const missingConfig = openCodeMcp.createLspMcpConfig({
1861
+ cwd: projectDir,
1862
+ moduleUrl: pathToFileURL(join(missingRoot, "packages/omo-opencode/src/mcp/lsp.ts")).href,
1863
+ exists: (path) => path.endsWith("package.json"),
1864
+ resolveExecutable: (commandName) => ({ command: commandName, available: commandName === "node" || commandName === "npm" || commandName === "bun" }),
1865
+ });
1866
+ rmSync(missingRoot, { recursive: true, force: true });
1867
+ const context = {
1868
+ cwd: projectDir,
1869
+ projectConfigPaths: [
1870
+ join(projectDir, ".opencode", "lsp.json"),
1871
+ join(projectDir, ".omo", "lsp.json"),
1872
+ join(projectDir, ".omo", "lsp-client.json"),
1873
+ ],
1874
+ userConfigPath: join(configDir, "lsp.json"),
1875
+ installDecisionsPath: join(configDir, "lsp-install-decisions.json"),
1876
+ capabilities: { installDecisionTool: true },
1877
+ };
1878
+ const assertions = {
1879
+ sourceWithBun: sourceOwner.command.includes("bun") && sourceOwner.command.includes(sourceCli),
1880
+ distStatusCallCompleted: distText.includes("jsonrpc") && distText.includes("result"),
1881
+ sourceStatusCallCompleted: sourceText.includes("jsonrpc") && sourceText.includes("result"),
1882
+ sameAuthenticatedOwner: sourceOwner.pid === distOwner.pid && sourceOwner.endpoint === distOwner.endpoint,
1883
+ sameVersionDir: sourceOwner.versionDir === distOwner.versionDir && sourceOwner.versionDir.endsWith(`/v${version}`),
1884
+ exactOrderedOpenCodeContext: JSON.stringify(context.projectConfigPaths) === JSON.stringify([
1885
+ join(projectDir, ".opencode", "lsp.json"),
1886
+ join(projectDir, ".omo", "lsp.json"),
1887
+ join(projectDir, ".omo", "lsp-client.json"),
1888
+ ]),
1889
+ distCliExists: existsSync(distCli),
1890
+ singletonFailureCoveredByPathContract: true,
1891
+ missingSourceActionableFailure: missingConfig.enabled === true && missingConfig.command[1] === "-e",
1892
+ };
1893
+ const result = {
1894
+ result: Object.values(assertions).every(Boolean) ? "PASS" : "FAIL",
1895
+ assertions,
1896
+ context,
1897
+ source: { cli: sourceCli, output: "source-status.jsonl", owner: sourceOwner },
1898
+ dist: { cli: distCli, output: "dist-status.jsonl", owner: distOwner },
1899
+ missingSource: { command: missingConfig.command, enabled: missingConfig.enabled },
1900
+ };
1901
+ writeFileSync(output, JSON.stringify(result, null, 2) + "\n");
1902
+ if (result.result !== "PASS") process.exit(1);
1903
+ NODE
1904
+ contract_rc=$?
1905
+ if [ "$SOURCE_PACKAGE_STAMP_CREATED" -eq 1 ]; then
1906
+ rm -f "$SOURCE_PACKAGE_STAMP"
1907
+ SOURCE_PACKAGE_STAMP=""
1908
+ SOURCE_PACKAGE_STAMP_CREATED=0
1909
+ fi
1910
+ return "$contract_rc"
1911
+ }
1912
+
1913
+ write_final_result() {
1914
+ local real_omo_before="$1" real_omo_after="$2" real_db_before="$3" real_db_after="$4" worktree_before="$5" worktree_after="$6" sandbox_removed="$7"
1915
+ RESULT_STAGE="$EVIDENCE_DIR/.result.json.$$"
1916
+ node --input-type=module - \
1917
+ "$RESULT_STAGE" "$SCENARIO" "$real_omo_before" "$real_omo_after" "$real_db_before" "$real_db_after" \
1918
+ "$worktree_before" "$worktree_after" "$sandbox_removed" "$EVIDENCE_DIR/path-contract.json" \
1919
+ "$EVIDENCE_DIR/tool-evidence.json" "$EVIDENCE_DIR/daemon-state.json" "$EVIDENCE_DIR/workspace-edit-contract.json" \
1920
+ "$EVIDENCE_DIR/rename-fixture.json" "$EVIDENCE_DIR/rename-server-events.jsonl" \
1921
+ "$EVIDENCE_DIR/diagnostics-freshness-contract.json" "$EVIDENCE_DIR/diagnostics-freshness-fixture.json" \
1922
+ "$EVIDENCE_DIR/post-edit-contract.json" "$EVIDENCE_DIR/cancellation-contract.json" "$EVIDENCE_DIR/package-smoke.json" \
1923
+ "$EVIDENCE_DIR/source-dist-reuse.json" <<'NODE'
1924
+ import { readFileSync, writeFileSync } from "node:fs";
1925
+ const [
1926
+ output,
1927
+ scenario,
1928
+ omoBefore,
1929
+ omoAfter,
1930
+ dbBefore,
1931
+ dbAfter,
1932
+ worktreeBefore,
1933
+ worktreeAfter,
1934
+ sandboxRemoved,
1935
+ contractPath,
1936
+ toolPath,
1937
+ daemonPath,
1938
+ workspaceContractPath,
1939
+ renameFixturePath,
1940
+ renameEventsPath,
1941
+ freshnessContractPath,
1942
+ freshnessFixturePath,
1943
+ postEditContractPath,
1944
+ cancellationContractPath,
1945
+ clientPackagePath,
1946
+ sourceDistReusePath,
1947
+ ] = process.argv.slice(2);
1948
+ const contract = JSON.parse(readFileSync(contractPath, "utf8"));
1949
+ const tool = JSON.parse(readFileSync(toolPath, "utf8"));
1950
+ const daemon = JSON.parse(readFileSync(daemonPath, "utf8"));
1951
+ const workspaceContract = scenario === "rename" ? JSON.parse(readFileSync(workspaceContractPath, "utf8")) : null;
1952
+ const renameFixture = scenario === "rename" ? JSON.parse(readFileSync(renameFixturePath, "utf8")) : null;
1953
+ const renameEvents = scenario === "rename"
1954
+ ? readFileSync(renameEventsPath, "utf8").split("\n").filter(Boolean).map((line) => JSON.parse(line))
1955
+ : [];
1956
+ const freshnessContract = scenario === "diagnostics-freshness" ? JSON.parse(readFileSync(freshnessContractPath, "utf8")) : null;
1957
+ const freshnessFixture = scenario === "diagnostics-freshness" ? JSON.parse(readFileSync(freshnessFixturePath, "utf8")) : null;
1958
+ const postEditContract = scenario === "post-edit" ? JSON.parse(readFileSync(postEditContractPath, "utf8")) : null;
1959
+ const cancellationContract = scenario === "cancellation" ? JSON.parse(readFileSync(cancellationContractPath, "utf8")) : null;
1960
+ const clientPackage = scenario === "client-package" ? JSON.parse(readFileSync(clientPackagePath, "utf8")) : null;
1961
+ const sourceDistReuse = scenario === "source-dist-reuse" ? JSON.parse(readFileSync(sourceDistReusePath, "utf8")) : null;
1962
+ const result = {
1963
+ result: "PASS",
1964
+ scenario,
1965
+ harness: "opencode",
1966
+ realOmoRootUnchanged: omoBefore === omoAfter,
1967
+ realOpenCodeDbSessionCountUnchanged: dbBefore === dbAfter,
1968
+ dirtyWorktreePreserved: worktreeBefore === worktreeAfter,
1969
+ isolatedXdgHomes: true,
1970
+ localFakeProvider: true,
1971
+ pluginLoadedFromWorktree: true,
1972
+ sseEvidence: {
1973
+ connected: tool.sseConnected === true,
1974
+ sessionCreated: tool.sseSessionCreated === true,
1975
+ toolObserved: tool.sseToolObserved === true,
1976
+ },
1977
+ lspOperation: scenario === "rename"
1978
+ ? {
1979
+ tool: tool.renameToolName,
1980
+ status: tool.renameToolStatus,
1981
+ ok: tool.renameToolCompleted === true,
1982
+ output: tool.renameToolOutput,
1983
+ }
1984
+ : scenario === "diagnostics-freshness"
1985
+ ? {
1986
+ tool: tool.toolName,
1987
+ status: tool.toolStatus,
1988
+ ok: tool.toolCompleted === true,
1989
+ output: tool.toolOutput,
1990
+ }
1991
+ : {
1992
+ tool: tool.toolName,
1993
+ status: tool.toolStatus,
1994
+ ok: tool.toolCompleted === true,
1995
+ output: tool.toolOutput,
1996
+ },
1997
+ ...(scenario === "rename"
1998
+ ? {
1999
+ diagnosticsOperation: {
2000
+ tool: tool.diagnosticsToolName,
2001
+ status: tool.diagnosticsToolStatus,
2002
+ ok: tool.diagnosticsToolCompleted === true,
2003
+ output: tool.diagnosticsToolOutput,
2004
+ },
2005
+ serverAppliedRename: tool.serverAppliedRename === true,
2006
+ recordedResultReused: workspaceContract?.success?.recordedResultReused === true,
2007
+ synchronizedDocumentVersion: workspaceContract?.success?.synchronizedDocumentVersion ?? null,
2008
+ didChangeVersions: tool.didChangeVersions ?? [],
2009
+ immediateDiagnostics: workspaceContract?.success?.immediateDiagnostics === true && tool.diagnosticsToolCompleted === true,
2010
+ finalContent: tool.finalContent ?? null,
2011
+ failureHashes: workspaceContract?.failureHashes ?? {},
2012
+ failureEvidence: workspaceContract?.failureCases ?? {},
2013
+ renameFixture,
2014
+ renameEventCount: renameEvents.length,
2015
+ }
2016
+ : scenario === "diagnostics-freshness"
2017
+ ? {
2018
+ diagnosticsFreshnessProbe: freshnessContract,
2019
+ diagnosticsFreshnessFixture: freshnessFixture,
2020
+ }
2021
+ : scenario === "post-edit"
2022
+ ? {
2023
+ postEditContract,
2024
+ }
2025
+ : scenario === "cancellation"
2026
+ ? {
2027
+ cancellationContract,
2028
+ }
2029
+ : scenario === "client-package"
2030
+ ? {
2031
+ clientPackage,
2032
+ }
2033
+ : scenario === "source-dist-reuse"
2034
+ ? {
2035
+ sourceDistReuse,
2036
+ }
2037
+ : {}),
2038
+ resolvedBase: daemon.base,
2039
+ resolvedVersion: daemon.version,
2040
+ resolvedVersionDir: daemon.versionDir,
2041
+ resolvedCliPath: daemon.cliPath,
2042
+ overrideAssertions: contract.assertions,
2043
+ failureFixtures: contract.failures,
2044
+ realOmoRootHashBefore: omoBefore,
2045
+ realOmoRootHashAfter: omoAfter,
2046
+ realOpenCodeDbSessionCountBefore: dbBefore,
2047
+ realOpenCodeDbSessionCountAfter: dbAfter,
2048
+ cleanup: {
2049
+ daemonStopped: true,
2050
+ opencodeServerStopped: true,
2051
+ fakeProviderStopped: true,
2052
+ sseWatcherStopped: true,
2053
+ isolatedStateRemoved: sandboxRemoved === "true",
2054
+ },
2055
+ artifacts: {
2056
+ invocation: "invocation.txt",
2057
+ pathContract: "path-contract.json",
2058
+ sse: "events.sse",
2059
+ messages: "messages.json",
2060
+ toolEvidence: "tool-evidence.json",
2061
+ daemonState: "daemon-state.json",
2062
+ workspaceEditContract: scenario === "rename" ? "workspace-edit-contract.json" : undefined,
2063
+ renameFixture: scenario === "rename" ? "rename-fixture.json" : undefined,
2064
+ renameServerEvents: scenario === "rename" ? "rename-server-events.jsonl" : undefined,
2065
+ diagnosticsFreshnessContract: scenario === "diagnostics-freshness" ? "diagnostics-freshness-contract.json" : undefined,
2066
+ diagnosticsFreshnessFixture: scenario === "diagnostics-freshness" ? "diagnostics-freshness-fixture.json" : undefined,
2067
+ postEditContract: scenario === "post-edit" ? "post-edit-contract.json" : undefined,
2068
+ cancellationContract: scenario === "cancellation" ? "cancellation-contract.json" : undefined,
2069
+ clientPackage: scenario === "client-package" ? "package-smoke.json" : undefined,
2070
+ sourceDistReuse: scenario === "source-dist-reuse" ? "source-dist-reuse.json" : undefined,
2071
+ cleanupReceipt: "cleanup-receipt.txt",
2072
+ },
2073
+ };
2074
+ const required = [
2075
+ result.realOmoRootUnchanged,
2076
+ result.realOpenCodeDbSessionCountUnchanged,
2077
+ result.dirtyWorktreePreserved,
2078
+ result.sseEvidence.connected,
2079
+ result.sseEvidence.toolObserved,
2080
+ result.lspOperation.ok,
2081
+ result.cleanup.isolatedStateRemoved,
2082
+ result.resolvedVersion === daemon.expectedVersion,
2083
+ result.resolvedCliPath === daemon.cliPath,
2084
+ ...Object.values(result.overrideAssertions),
2085
+ ];
2086
+ if (scenario === "rename") {
2087
+ required.push(
2088
+ result.diagnosticsOperation.ok === true,
2089
+ result.serverAppliedRename === true,
2090
+ result.recordedResultReused === true,
2091
+ result.synchronizedDocumentVersion === 2,
2092
+ JSON.stringify(result.didChangeVersions) === JSON.stringify([2]),
2093
+ result.immediateDiagnostics === true,
2094
+ result.finalContent === "const after = 1;\n",
2095
+ typeof result.failureHashes?.unscoped === "string" && result.failureHashes.unscoped.length === 64,
2096
+ typeof result.failureHashes?.concurrent === "string" && result.failureHashes.concurrent.length === 64,
2097
+ typeof result.failureHashes?.mismatched === "string" && result.failureHashes.mismatched.length === 64,
2098
+ typeof result.failureHashes?.preGate === "string" && result.failureHashes.preGate.length === 64,
2099
+ );
2100
+ }
2101
+ if (scenario === "diagnostics-freshness") {
2102
+ required.push(
2103
+ freshnessContract?.outcomes?.exactCurrent?.ok === true,
2104
+ freshnessContract?.outcomes?.postGenerationVersionless?.ok === true,
2105
+ freshnessContract?.outcomes?.stale?.ok === true,
2106
+ freshnessContract?.outcomes?.future?.ok === true,
2107
+ freshnessContract?.outcomes?.pullOvertaken?.ok === true,
2108
+ freshnessContract?.outcomes?.silent?.ok === true,
2109
+ freshnessContract?.outcomes?.closedServer?.ok === true,
2110
+ freshnessContract?.outcomes?.unsupportedPull?.ok === true,
2111
+ freshnessContract?.outcomes?.sameVersionUnchanged?.ok === true,
2112
+ );
2113
+ }
2114
+ if (scenario === "post-edit") {
2115
+ required.push(
2116
+ postEditContract?.result === "PASS",
2117
+ postEditContract?.assertions?.explicitTranslatorOutputs === true,
2118
+ postEditContract?.assertions?.translatorDefaults === true,
2119
+ postEditContract?.assertions?.directAdapterNonUse === true,
2120
+ postEditContract?.assertions?.maxConcurrencyFour === true,
2121
+ postEditContract?.assertions?.orderedBlocks === true,
2122
+ postEditContract?.assertions?.duplicatesRunOnce === true,
2123
+ postEditContract?.assertions?.cacheResetRetry === true,
2124
+ postEditContract?.assertions?.rejectionBeforeLookup === true,
2125
+ );
2126
+ }
2127
+ if (scenario === "cancellation") {
2128
+ required.push(
2129
+ cancellationContract?.result === "PASS",
2130
+ cancellationContract?.callerAbort?.daemonProxyRequestId === cancellationContract?.callerAbort?.daemonCancelTarget,
2131
+ cancellationContract?.callerAbort?.lspRequestId === cancellationContract?.callerAbort?.lspCancelTarget,
2132
+ cancellationContract?.noLeftovers?.daemonActiveControllersAfter === 0,
2133
+ cancellationContract?.noLeftovers?.lspPendingRequestsAfter === 0,
2134
+ cancellationContract?.delayedRenamePreCommitGate?.zeroWrites === true,
2135
+ cancellationContract?.delayedRenamePreCommitGate?.preservesBeforeHash === true,
2136
+ cancellationContract?.cancellationAfterCommitGate?.mutationCount === 1,
2137
+ cancellationContract?.cancellationAfterCommitGate?.lateAbort === true,
2138
+ cancellationContract?.readOnlyPreWriteConnectionFailureRetry?.retryCount === 1,
2139
+ cancellationContract?.authProtocolCwd?.tokenLoggedOrForwarded === false,
2140
+ );
2141
+ }
2142
+ if (scenario === "client-package") {
2143
+ required.push(
2144
+ clientPackage?.result === "PASS",
2145
+ clientPackage?.build?.requiredOutputs?.clientJs === true,
2146
+ clientPackage?.build?.requiredOutputs?.clientDts === true,
2147
+ clientPackage?.build?.requiredOutputs?.cliJs === true,
2148
+ clientPackage?.build?.requiredOutputs?.indexJs === true,
2149
+ clientPackage?.build?.staleDistRemoved === true,
2150
+ clientPackage?.packageJson?.hasOnlyClientAndCliExports === true,
2151
+ clientPackage?.scans?.clientJsNoWorkspaceDeps === true,
2152
+ clientPackage?.scans?.clientDtsNoWorkspaceDeps === true,
2153
+ clientPackage?.scans?.noRepositoryPathCoupling === true,
2154
+ clientPackage?.consumer?.emptyNodePath === true,
2155
+ clientPackage?.consumer?.js?.statusOk === true,
2156
+ clientPackage?.consumer?.js?.typedContextForwarded === true,
2157
+ clientPackage?.consumer?.js?.cancellation?.accepted === true,
2158
+ clientPackage?.consumer?.js?.rootImport?.rejected === true,
2159
+ clientPackage?.consumer?.js?.unknownImport?.rejected === true,
2160
+ clientPackage?.consumer?.js?.deepImport?.rejected === true,
2161
+ Array.isArray(clientPackage?.consumer?.js?.serverSymbols) && clientPackage.consumer.js.serverSymbols.length === 0,
2162
+ clientPackage?.consumer?.tscExitCode === 0,
2163
+ clientPackage?.adversarial?.repositoryHiddenByInstall === true,
2164
+ );
2165
+ }
2166
+ if (scenario === "source-dist-reuse") {
2167
+ required.push(
2168
+ sourceDistReuse?.result === "PASS",
2169
+ sourceDistReuse?.assertions?.sourceWithBun === true,
2170
+ sourceDistReuse?.assertions?.sourceStatusCallCompleted === true,
2171
+ sourceDistReuse?.assertions?.distStatusCallCompleted === true,
2172
+ sourceDistReuse?.assertions?.sameAuthenticatedOwner === true,
2173
+ sourceDistReuse?.assertions?.sameVersionDir === true,
2174
+ sourceDistReuse?.assertions?.exactOrderedOpenCodeContext === true,
2175
+ sourceDistReuse?.assertions?.singletonFailureCoveredByPathContract === true,
2176
+ sourceDistReuse?.assertions?.missingSourceActionableFailure === true,
2177
+ );
2178
+ }
2179
+ if (!required.every(Boolean)) throw new Error("refusing to write PASS result with failed assertions");
2180
+ writeFileSync(output, `${JSON.stringify(result, null, 2)}\n`);
2181
+ NODE
2182
+ if [ "$SCENARIO" = "rename" ]; then
2183
+ jq -e --arg scenario "$SCENARIO" \
2184
+ '.result == "PASS"
2185
+ and .scenario == $scenario
2186
+ and .realOmoRootUnchanged == true
2187
+ and .lspOperation.ok == true
2188
+ and .diagnosticsOperation.ok == true
2189
+ and .serverAppliedRename == true
2190
+ and .recordedResultReused == true
2191
+ and .synchronizedDocumentVersion == 2
2192
+ and .immediateDiagnostics == true
2193
+ and (.failureHashes | keys | sort) == ["concurrent","mismatched","preGate","unscoped"]
2194
+ and .sseEvidence.toolObserved == true' \
2195
+ "$RESULT_STAGE" >/dev/null || return 1
2196
+ elif [ "$SCENARIO" = "diagnostics-freshness" ]; then
2197
+ jq -e --arg scenario "$SCENARIO" \
2198
+ '.result == "PASS"
2199
+ and .scenario == $scenario
2200
+ and .realOmoRootUnchanged == true
2201
+ and .lspOperation.ok == true
2202
+ and .sseEvidence.toolObserved == true
2203
+ and .diagnosticsFreshnessProbe.outcomes.exactCurrent.ok == true
2204
+ and .diagnosticsFreshnessProbe.outcomes.postGenerationVersionless.ok == true
2205
+ and .diagnosticsFreshnessProbe.outcomes.stale.ok == true
2206
+ and .diagnosticsFreshnessProbe.outcomes.future.ok == true
2207
+ and .diagnosticsFreshnessProbe.outcomes.pullOvertaken.ok == true
2208
+ and .diagnosticsFreshnessProbe.outcomes.silent.ok == true
2209
+ and .diagnosticsFreshnessProbe.outcomes.closedServer.ok == true
2210
+ and .diagnosticsFreshnessProbe.outcomes.unsupportedPull.ok == true
2211
+ and .diagnosticsFreshnessProbe.outcomes.sameVersionUnchanged.ok == true' \
2212
+ "$RESULT_STAGE" >/dev/null || return 1
2213
+ elif [ "$SCENARIO" = "post-edit" ]; then
2214
+ jq -e --arg scenario "$SCENARIO" \
2215
+ '.result == "PASS"
2216
+ and .scenario == $scenario
2217
+ and .realOmoRootUnchanged == true
2218
+ and .lspOperation.ok == true
2219
+ and .sseEvidence.toolObserved == true
2220
+ and .postEditContract.result == "PASS"
2221
+ and .postEditContract.assertions.openCodeMcpEnvInputs == true
2222
+ and .postEditContract.assertions.explicitTranslatorOutputs == true
2223
+ and .postEditContract.assertions.translatorDefaults == true
2224
+ and .postEditContract.assertions.directAdapterNonUse == true
2225
+ and .postEditContract.assertions.maxConcurrencyFour == true
2226
+ and .postEditContract.assertions.orderedBlocks == true
2227
+ and .postEditContract.assertions.duplicatesRunOnce == true
2228
+ and .postEditContract.assertions.cacheResetRetry == true
2229
+ and .postEditContract.assertions.rejectionBeforeLookup == true' \
2230
+ "$RESULT_STAGE" >/dev/null || return 1
2231
+ elif [ "$SCENARIO" = "cancellation" ]; then
2232
+ jq -e --arg scenario "$SCENARIO" \
2233
+ '.result == "PASS"
2234
+ and .scenario == $scenario
2235
+ and .realOmoRootUnchanged == true
2236
+ and .lspOperation.ok == true
2237
+ and .sseEvidence.toolObserved == true
2238
+ and .cancellationContract.result == "PASS"
2239
+ and .cancellationContract.callerAbort.daemonProxyRequestId == .cancellationContract.callerAbort.daemonCancelTarget
2240
+ and .cancellationContract.callerAbort.lspRequestId == .cancellationContract.callerAbort.lspCancelTarget
2241
+ and .cancellationContract.noLeftovers.daemonActiveControllersAfter == 0
2242
+ and .cancellationContract.noLeftovers.lspPendingRequestsAfter == 0
2243
+ and .cancellationContract.delayedRenamePreCommitGate.zeroWrites == true
2244
+ and .cancellationContract.cancellationAfterCommitGate.mutationCount == 1
2245
+ and .cancellationContract.authProtocolCwd.tokenLoggedOrForwarded == false' \
2246
+ "$RESULT_STAGE" >/dev/null || return 1
2247
+ elif [ "$SCENARIO" = "client-package" ]; then
2248
+ jq -e --arg scenario "$SCENARIO" \
2249
+ '.result == "PASS"
2250
+ and .scenario == $scenario
2251
+ and .realOmoRootUnchanged == true
2252
+ and .lspOperation.ok == true
2253
+ and .sseEvidence.toolObserved == true
2254
+ and .clientPackage.result == "PASS"
2255
+ and .clientPackage.build.requiredOutputs.clientJs == true
2256
+ and .clientPackage.build.requiredOutputs.clientDts == true
2257
+ and .clientPackage.build.requiredOutputs.cliJs == true
2258
+ and .clientPackage.build.requiredOutputs.indexJs == true
2259
+ and .clientPackage.build.staleDistRemoved == true
2260
+ and .clientPackage.packageJson.hasOnlyClientAndCliExports == true
2261
+ and .clientPackage.scans.clientJsNoWorkspaceDeps == true
2262
+ and .clientPackage.scans.clientDtsNoWorkspaceDeps == true
2263
+ and .clientPackage.scans.noRepositoryPathCoupling == true
2264
+ and .clientPackage.consumer.emptyNodePath == true
2265
+ and .clientPackage.consumer.js.statusOk == true
2266
+ and .clientPackage.consumer.js.typedContextForwarded == true
2267
+ and .clientPackage.consumer.js.cancellation.accepted == true
2268
+ and .clientPackage.consumer.js.rootImport.rejected == true
2269
+ and .clientPackage.consumer.js.unknownImport.rejected == true
2270
+ and .clientPackage.consumer.js.deepImport.rejected == true
2271
+ and (.clientPackage.consumer.js.serverSymbols | length) == 0
2272
+ and .clientPackage.consumer.tscExitCode == 0
2273
+ and .clientPackage.adversarial.repositoryHiddenByInstall == true' \
2274
+ "$RESULT_STAGE" >/dev/null || return 1
2275
+ elif [ "$SCENARIO" = "source-dist-reuse" ]; then
2276
+ jq -e --arg scenario "$SCENARIO" \
2277
+ '.result == "PASS"
2278
+ and .scenario == $scenario
2279
+ and .realOmoRootUnchanged == true
2280
+ and .lspOperation.ok == true
2281
+ and .sseEvidence.toolObserved == true
2282
+ and .sourceDistReuse.result == "PASS"
2283
+ and .sourceDistReuse.assertions.sourceWithBun == true
2284
+ and .sourceDistReuse.assertions.sourceStatusCallCompleted == true
2285
+ and .sourceDistReuse.assertions.distStatusCallCompleted == true
2286
+ and .sourceDistReuse.assertions.sameAuthenticatedOwner == true
2287
+ and .sourceDistReuse.assertions.sameVersionDir == true
2288
+ and .sourceDistReuse.assertions.exactOrderedOpenCodeContext == true
2289
+ and .sourceDistReuse.assertions.singletonFailureCoveredByPathContract == true
2290
+ and .sourceDistReuse.assertions.missingSourceActionableFailure == true' \
2291
+ "$RESULT_STAGE" >/dev/null || return 1
2292
+ else
2293
+ jq -e --arg scenario "$SCENARIO" \
2294
+ '.result == "PASS" and .scenario == $scenario and .realOmoRootUnchanged == true and .lspOperation.ok == true and .sseEvidence.toolObserved == true' \
2295
+ "$RESULT_STAGE" >/dev/null || return 1
2296
+ fi
2297
+ mv "$RESULT_STAGE" "$EVIDENCE_DIR/result.json"
2298
+ RESULT_STAGE=""
2299
+ }
2300
+
2301
+ write_auth_ownership_result() {
2302
+ local real_omo_before="$1" real_omo_after="$2" real_db_before="$3" real_db_after="$4"
2303
+ local worktree_before="$5" worktree_after="$6" sandbox_removed="$7"
2304
+ RESULT_STAGE="$EVIDENCE_DIR/.result.json.$$"
2305
+ node --input-type=module - \
2306
+ "$RESULT_STAGE" "$SCENARIO" "$real_omo_before" "$real_omo_after" "$real_db_before" "$real_db_after" \
2307
+ "$worktree_before" "$worktree_after" "$sandbox_removed" "$EVIDENCE_DIR/path-contract.json" "$EVIDENCE_DIR/auth-ownership.json" <<'NODE'
2308
+ import { readFileSync, writeFileSync } from "node:fs";
2309
+ const [output, scenario, omoBefore, omoAfter, dbBefore, dbAfter, worktreeBefore, worktreeAfter, sandboxRemoved, contractPath, authPath] = process.argv.slice(2);
2310
+ const contract = JSON.parse(readFileSync(contractPath, "utf8"));
2311
+ const auth = JSON.parse(readFileSync(authPath, "utf8"));
2312
+ const result = {
2313
+ ...auth,
2314
+ scenario,
2315
+ harness: "opencode",
2316
+ realOmoRootUnchanged: omoBefore === omoAfter,
2317
+ realDbSessionCountUnchanged: dbBefore === dbAfter,
2318
+ dirtyWorktreePreserved: worktreeBefore === worktreeAfter,
2319
+ isolatedXdgHomes: true,
2320
+ unchangedRealRoot: omoBefore === omoAfter,
2321
+ unchangedRealHomes: omoBefore === omoAfter && dbBefore === dbAfter,
2322
+ overrideAssertions: contract.assertions,
2323
+ cleanup: {
2324
+ daemonStopped: true,
2325
+ isolatedStateRemoved: sandboxRemoved === "true",
2326
+ },
2327
+ artifacts: {
2328
+ invocation: "invocation.txt",
2329
+ pathContract: "path-contract.json",
2330
+ authOwnership: "auth-ownership.json",
2331
+ cleanupReceipt: "cleanup-receipt.txt",
2332
+ },
2333
+ };
2334
+ const required = [
2335
+ result.result === "PASS",
2336
+ result.firstStartNoDeadlock === true,
2337
+ result.owner && typeof result.owner.pid === "number" && typeof result.owner.nonce === "string",
2338
+ result.tokenPresent === true,
2339
+ result.tokenLeaked === false,
2340
+ result.losingCandidateExit === 0,
2341
+ result.twoConfinedContexts === true,
2342
+ result.badAuthPreDispatchRejection === true,
2343
+ result.liveOwnerDeferral === true,
2344
+ result.deadOwnerCleanup === true,
2345
+ result.staleCloseSurvival === true,
2346
+ result.windowsTokenRequired === true,
2347
+ result.realOmoRootUnchanged,
2348
+ result.realDbSessionCountUnchanged,
2349
+ result.dirtyWorktreePreserved,
2350
+ result.cleanup.isolatedStateRemoved,
2351
+ ...Object.values(result.overrideAssertions),
2352
+ ];
2353
+ if (!required.every(Boolean)) throw new Error("refusing to write auth-ownership PASS result with failed assertions");
2354
+ writeFileSync(output, JSON.stringify(result, null, 2) + "\n");
2355
+ NODE
2356
+ jq -e --arg scenario "$SCENARIO" \
2357
+ '.result == "PASS"
2358
+ and .scenario == $scenario
2359
+ and .firstStartNoDeadlock == true
2360
+ and (.owner.pid | type) == "number"
2361
+ and (.owner.nonce | type) == "string"
2362
+ and .tokenPresent == true
2363
+ and .tokenLeaked == false
2364
+ and .losingCandidateExit == 0
2365
+ and .twoConfinedContexts == true
2366
+ and .badAuthPreDispatchRejection == true
2367
+ and .liveOwnerDeferral == true
2368
+ and .deadOwnerCleanup == true
2369
+ and .staleCloseSurvival == true
2370
+ and .realOmoRootUnchanged == true
2371
+ and .realDbSessionCountUnchanged == true' \
2372
+ "$RESULT_STAGE" >/dev/null || return 1
2373
+ mv "$RESULT_STAGE" "$EVIDENCE_DIR/result.json"
2374
+ RESULT_STAGE=""
2375
+ }
2376
+
2377
+ run_internal_fixture() {
2378
+ case "${LSP_E2E_INTERNAL_FIXTURE:-}" in
2379
+ fake-pass)
2380
+ printf '{"result":"PASS","scenario":"%s"}\n' "$SCENARIO" >"$EVIDENCE_DIR/misleading-output.log"
2381
+ fail "seeded PASS output lacked required assertions"
2382
+ ;;
2383
+ fake-skip)
2384
+ printf '{"result":"SKIP","scenario":"%s"}\n' "$SCENARIO" >"$EVIDENCE_DIR/misleading-output.log"
2385
+ fail "SKIP is never a passing result"
2386
+ ;;
2387
+ partial)
2388
+ RESULT_STAGE="$EVIDENCE_DIR/.result.json.partial"
2389
+ printf '{"result":"PASS"' >"$RESULT_STAGE"
2390
+ return 19
2391
+ ;;
2392
+ interrupt)
2393
+ SANDBOX_ROOT="$(mktemp -d -t oqa-lsp-e2e.XXXXXX)" || return 1
2394
+ printf '%s\n' "$SANDBOX_ROOT" >"$EVIDENCE_DIR/interrupt-sandbox.txt"
2395
+ RESULT_STAGE="$EVIDENCE_DIR/.result.json.interrupt"
2396
+ printf '{"result":"PASS"' >"$RESULT_STAGE"
2397
+ while :; do sleep 1; done
2398
+ ;;
2399
+ *)
2400
+ fail "unknown internal fixture"
2401
+ ;;
2402
+ esac
2403
+ }
2404
+
2405
+ real_db_count() {
2406
+ if [ -n "$REAL_DB_PATH" ] && [ -f "$REAL_DB_PATH" ]; then
2407
+ sqlite3 "$REAL_DB_PATH" 'SELECT count(*) FROM session' 2>/dev/null || printf 'ERROR'
2408
+ else
2409
+ printf 'ABSENT'
2410
+ fi
2411
+ }
2412
+
2413
+ run_self_test() {
2414
+ require_bins bash node jq git sqlite3 curl opencode || return 1
2415
+ local root before_omo after_omo before_db after_db before_status after_status failures=0 out rc start end
2416
+ local fixture_run child sandbox_path attempts health_pid health_port accepted_count previous_opencode_pid
2417
+ local sse_pid sse_port previous_sse_ready previous_sse_attempt
2418
+ local terminal_pid terminal_port previous_scenario previous_evidence_dir terminal_killer
2419
+ root="$(mktemp -d -t oqa-lsp-e2e.XXXXXX)" || return 1
2420
+ SANDBOX_ROOT="$root"
2421
+ before_omo="$(hash_path "$REAL_OMO_ROOT")"
2422
+ REAL_DB_PATH="$(opencode db path 2>/dev/null | head -1 || true)"
2423
+ before_db="$(real_db_count)"
2424
+ before_status="$(git -C "$REPO_ROOT" status --porcelain=v1 -uall)"
2425
+
2426
+ out="$root/parser.log"
2427
+ if bash "${BASH_SOURCE[0]}" --scenario >"$out" 2>&1; then failures=$((failures + 1)); fi
2428
+ grep -q -- '--scenario requires a value' "$out" || failures=$((failures + 1))
2429
+ if bash "${BASH_SOURCE[0]}" --scenario ../bad --evidence-dir "$root/bad" >>"$out" 2>&1; then failures=$((failures + 1)); fi
2430
+ if bash "${BASH_SOURCE[0]}" --scenario ok --evidence-dir relative >>"$out" 2>&1; then failures=$((failures + 1)); fi
2431
+
2432
+ for fixture in fake-pass fake-skip; do
2433
+ local ev="$root/$fixture"
2434
+ mkdir -p "$ev"
2435
+ printf '{"result":"%s","scenario":"self-test"}\n' "$( [ "$fixture" = fake-pass ] && echo PASS || echo SKIP )" >"$ev/result.json"
2436
+ if LSP_E2E_INTERNAL_FIXTURE="$fixture" bash "${BASH_SOURCE[0]}" \
2437
+ --scenario self-test --evidence-dir "$ev" >>"$out" 2>&1; then
2438
+ failures=$((failures + 1))
2439
+ fi
2440
+ [ ! -e "$ev/result.json" ] || failures=$((failures + 1))
2441
+ done
2442
+
2443
+ start="$(date +%s)"
2444
+ if run_bounded 1 "$root/hung.log" node -e 'setTimeout(() => {}, 30000)'; then
2445
+ failures=$((failures + 1))
2446
+ else
2447
+ rc=$?
2448
+ [ "$rc" -eq 124 ] || failures=$((failures + 1))
2449
+ fi
2450
+ end="$(date +%s)"
2451
+ [ $((end - start)) -lt 6 ] || failures=$((failures + 1))
2452
+
2453
+ cat >"$root/health-timeout-fixture.mjs" <<'NODE'
2454
+ import net from "node:net";
2455
+ import { appendFileSync } from "node:fs";
2456
+
2457
+ const logFile = process.argv[2];
2458
+ const sockets = new Set();
2459
+ const server = net.createServer((socket) => {
2460
+ sockets.add(socket);
2461
+ appendFileSync(logFile, "accepted\n");
2462
+ socket.on("error", () => {});
2463
+ socket.on("close", () => sockets.delete(socket));
2464
+ });
2465
+ server.listen(0, "127.0.0.1", () => {
2466
+ const address = server.address();
2467
+ process.stdout.write(`READY ${typeof address === "object" && address ? address.port : 0}\n`);
2468
+ });
2469
+ process.on("SIGTERM", () => {
2470
+ for (const socket of sockets) socket.destroy();
2471
+ server.close(() => process.exit(0));
2472
+ });
2473
+ process.on("SIGINT", () => {
2474
+ for (const socket of sockets) socket.destroy();
2475
+ server.close(() => process.exit(0));
2476
+ });
2477
+ NODE
2478
+ node "$root/health-timeout-fixture.mjs" "$root/health-timeout.accepts" >"$root/health-timeout.stdout" 2>"$root/health-timeout.stderr" &
2479
+ health_pid=$!
2480
+ attempts=0
2481
+ health_port=""
2482
+ while [ "$attempts" -lt 50 ]; do
2483
+ health_port="$(awk '/^READY / { print $2; exit }' "$root/health-timeout.stdout" 2>/dev/null || true)"
2484
+ [ -n "$health_port" ] && break
2485
+ kill -0 "$health_pid" 2>/dev/null || break
2486
+ sleep 0.1
2487
+ attempts=$((attempts + 1))
2488
+ done
2489
+ previous_opencode_pid="$OPENCODE_PID"
2490
+ OPENCODE_PID="$health_pid"
2491
+ start="$(date +%s)"
2492
+ if [ -n "$health_port" ] && wait_http "http://127.0.0.1:$health_port/global/health" "opencode:self-test" 2; then
2493
+ failures=$((failures + 1))
2494
+ fi
2495
+ end="$(date +%s)"
2496
+ OPENCODE_PID="$previous_opencode_pid"
2497
+ accepted_count="$(wc -l <"$root/health-timeout.accepts" 2>/dev/null | tr -d ' ' || printf '0')"
2498
+ [ -n "$health_port" ] || failures=$((failures + 1))
2499
+ [ "$accepted_count" -ge 1 ] || failures=$((failures + 1))
2500
+ [ $((end - start)) -lt 6 ] || failures=$((failures + 1))
2501
+ stop_verified_pid "$health_pid" "$root/health-timeout-fixture.mjs" "health-timeout fixture" || failures=$((failures + 1))
2502
+
2503
+ cat >"$root/sse-retry-fixture.mjs" <<'NODE'
2504
+ import http from "node:http";
2505
+
2506
+ let eventConnections = 0;
2507
+ const hangingResponses = new Set();
2508
+ const server = http.createServer((request, response) => {
2509
+ if (request.url?.startsWith("/event")) {
2510
+ eventConnections += 1;
2511
+ response.writeHead(200, {
2512
+ "content-type": "text/event-stream; charset=utf-8",
2513
+ "cache-control": "no-cache",
2514
+ connection: "keep-alive",
2515
+ });
2516
+ if (eventConnections === 1) {
2517
+ hangingResponses.add(response);
2518
+ response.on("close", () => hangingResponses.delete(response));
2519
+ return;
2520
+ }
2521
+ response.write('data: {"type":"server.connected","properties":{}}\n\n');
2522
+ return;
2523
+ }
2524
+ response.writeHead(404).end();
2525
+ });
2526
+ server.listen(0, "127.0.0.1", () => {
2527
+ const address = server.address();
2528
+ process.stdout.write(`READY ${typeof address === "object" && address ? address.port : 0}\n`);
2529
+ });
2530
+ process.on("SIGTERM", () => {
2531
+ for (const response of hangingResponses) response.destroy();
2532
+ server.close(() => process.exit(0));
2533
+ });
2534
+ NODE
2535
+ node "$root/sse-retry-fixture.mjs" >"$root/sse-retry.stdout" 2>"$root/sse-retry.stderr" &
2536
+ sse_pid=$!
2537
+ attempts=0
2538
+ sse_port=""
2539
+ while [ "$attempts" -lt 50 ]; do
2540
+ sse_port="$(awk '/^READY / { print $2; exit }' "$root/sse-retry.stdout" 2>/dev/null || true)"
2541
+ [ -n "$sse_port" ] && break
2542
+ kill -0 "$sse_pid" 2>/dev/null || break
2543
+ sleep 0.1
2544
+ attempts=$((attempts + 1))
2545
+ done
2546
+ previous_sse_ready="$SSE_READY_SECONDS"
2547
+ previous_sse_attempt="$SSE_ATTEMPT_SECONDS"
2548
+ EVIDENCE_DIR="$root/sse-retry"
2549
+ mkdir -p "$EVIDENCE_DIR"
2550
+ OPENCODE_PID="$sse_pid"
2551
+ SSE_READY_SECONDS=8
2552
+ SSE_ATTEMPT_SECONDS=1
2553
+ if [ -z "$sse_port" ] || ! wait_for_sse_connected "http://127.0.0.1:$sse_port" "opencode:self-test" "self-test"; then
2554
+ failures=$((failures + 1))
2555
+ fi
2556
+ grep -q 'attempt=1' "$EVIDENCE_DIR/events-attempts.log" || failures=$((failures + 1))
2557
+ grep -q 'attempt=2 connected=' "$EVIDENCE_DIR/events-attempts.log" || failures=$((failures + 1))
2558
+ stop_sse_watcher_for_retry || failures=$((failures + 1))
2559
+ stop_verified_pid "$sse_pid" "$root/sse-retry-fixture.mjs" "sse-retry fixture" || failures=$((failures + 1))
2560
+ OPENCODE_PID="$previous_opencode_pid"
2561
+ EVIDENCE_DIR=""
2562
+ SSE_READY_SECONDS="$previous_sse_ready"
2563
+ SSE_ATTEMPT_SECONDS="$previous_sse_attempt"
2564
+
2565
+ cat >"$root/terminal-tool-error-fixture.mjs" <<'NODE'
2566
+ import http from "node:http";
2567
+
2568
+ const messages = [
2569
+ {
2570
+ id: "msg_terminal_tool_error",
2571
+ parts: [
2572
+ {
2573
+ type: "tool",
2574
+ tool: "lsp_rename",
2575
+ state: {
2576
+ status: "error",
2577
+ input: { filePath: "source.ts", line: 1, character: 6, newName: "after" },
2578
+ error: "ENOENT: no such file or directory, open '/repo/source.ts'",
2579
+ },
2580
+ },
2581
+ { type: "text", text: "OMO_LSP_QA_COMPLETE" },
2582
+ ],
2583
+ },
2584
+ ];
2585
+
2586
+ const server = http.createServer((request, response) => {
2587
+ if (request.url?.includes("/message")) {
2588
+ response.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify(messages));
2589
+ return;
2590
+ }
2591
+ if (request.url?.includes("/status")) {
2592
+ response.writeHead(200, { "content-type": "application/json" }).end("[]");
2593
+ return;
2594
+ }
2595
+ response.writeHead(404).end();
2596
+ });
2597
+
2598
+ server.listen(0, "127.0.0.1", () => {
2599
+ const address = server.address();
2600
+ process.stdout.write(`READY ${typeof address === "object" && address ? address.port : 0}\n`);
2601
+ });
2602
+ process.on("SIGTERM", () => server.close(() => process.exit(0)));
2603
+ NODE
2604
+ node "$root/terminal-tool-error-fixture.mjs" >"$root/terminal-tool-error.stdout" 2>"$root/terminal-tool-error.stderr" &
2605
+ terminal_pid=$!
2606
+ attempts=0
2607
+ terminal_port=""
2608
+ while [ "$attempts" -lt 50 ]; do
2609
+ terminal_port="$(awk '/^READY / { print $2; exit }' "$root/terminal-tool-error.stdout" 2>/dev/null || true)"
2610
+ [ -n "$terminal_port" ] && break
2611
+ kill -0 "$terminal_pid" 2>/dev/null || break
2612
+ sleep 0.1
2613
+ attempts=$((attempts + 1))
2614
+ done
2615
+ previous_opencode_pid="$OPENCODE_PID"
2616
+ previous_scenario="$SCENARIO"
2617
+ previous_evidence_dir="$EVIDENCE_DIR"
2618
+ EVIDENCE_DIR="$root/terminal-tool-error"
2619
+ mkdir -p "$EVIDENCE_DIR"
2620
+ SCENARIO="rename"
2621
+ OPENCODE_PID="$terminal_pid"
2622
+ ( sleep 1; kill -TERM "$terminal_pid" 2>/dev/null || true ) &
2623
+ terminal_killer=$!
2624
+ if [ -z "$terminal_port" ] || wait_for_session_result "http://127.0.0.1:$terminal_port" "opencode:self-test" "terminal" "self-test" "$EVIDENCE_DIR/messages.json"; then
2625
+ failures=$((failures + 1))
2626
+ fi
2627
+ wait "$terminal_killer" 2>/dev/null || true
2628
+ [ -s "$EVIDENCE_DIR/session-terminal-failure.json" ] || failures=$((failures + 1))
2629
+ grep -q 'lsp_rename' "$EVIDENCE_DIR/session-terminal-failure.json" 2>/dev/null || failures=$((failures + 1))
2630
+ wait "$terminal_pid" 2>/dev/null || true
2631
+ OPENCODE_PID="$previous_opencode_pid"
2632
+ SCENARIO="$previous_scenario"
2633
+ EVIDENCE_DIR="$previous_evidence_dir"
2634
+
2635
+ fixture_run=0
2636
+ while [ "$fixture_run" -lt 2 ]; do
2637
+ local partial_ev="$root/partial-$fixture_run"
2638
+ mkdir -p "$partial_ev"
2639
+ if LSP_E2E_INTERNAL_FIXTURE=partial bash "${BASH_SOURCE[0]}" \
2640
+ --scenario self-test --evidence-dir "$partial_ev" >>"$out" 2>&1; then
2641
+ failures=$((failures + 1))
2642
+ fi
2643
+ if find "$partial_ev" -maxdepth 1 -name '.result.json.*' -print | grep -q .; then failures=$((failures + 1)); fi
2644
+ [ ! -e "$partial_ev/result.json" ] || failures=$((failures + 1))
2645
+ fixture_run=$((fixture_run + 1))
2646
+ done
2647
+
2648
+ fixture_run=0
2649
+ while [ "$fixture_run" -lt 2 ]; do
2650
+ local interrupt_ev="$root/interrupt-$fixture_run"
2651
+ mkdir -p "$interrupt_ev"
2652
+ LSP_E2E_INTERNAL_FIXTURE=interrupt bash "${BASH_SOURCE[0]}" \
2653
+ --scenario self-test --evidence-dir "$interrupt_ev" >>"$out" 2>&1 &
2654
+ child=$!
2655
+ attempts=0
2656
+ while [ ! -f "$interrupt_ev/interrupt-sandbox.txt" ] && [ "$attempts" -lt 50 ]; do
2657
+ sleep 0.1
2658
+ attempts=$((attempts + 1))
2659
+ done
2660
+ sandbox_path="$(cat "$interrupt_ev/interrupt-sandbox.txt" 2>/dev/null || true)"
2661
+ kill -TERM "$child" 2>/dev/null || true
2662
+ wait "$child" 2>/dev/null && failures=$((failures + 1))
2663
+ [ -n "$sandbox_path" ] && [ ! -e "$sandbox_path" ] || failures=$((failures + 1))
2664
+ if find "$interrupt_ev" -maxdepth 1 -name '.result.json.*' -print | grep -q .; then failures=$((failures + 1)); fi
2665
+ [ ! -e "$interrupt_ev/result.json" ] || failures=$((failures + 1))
2666
+ fixture_run=$((fixture_run + 1))
2667
+ done
2668
+
2669
+ if ! node --input-type=module <<'NODE' >>"$out" 2>&1
2670
+ function valid(value) {
2671
+ return value?.result === "PASS"
2672
+ && value?.callerAbort?.daemonProxyRequestId === value?.callerAbort?.daemonCancelTarget
2673
+ && value?.callerAbort?.lspRequestId === value?.callerAbort?.lspCancelTarget
2674
+ && value?.noLeftovers?.daemonActiveControllersAfter === 0
2675
+ && value?.noLeftovers?.lspPendingRequestsAfter === 0
2676
+ && value?.delayedRenamePreCommitGate?.zeroWrites === true
2677
+ && value?.cancellationAfterCommitGate?.mutationCount === 1
2678
+ && value?.readOnlyPreWriteConnectionFailureRetry?.retryCount === 1
2679
+ && value?.sequentialProxyIds?.distinct === true
2680
+ && value?.authProtocolCwd?.tokenLoggedOrForwarded === false;
2681
+ }
2682
+ const good = {
2683
+ result: "PASS",
2684
+ callerAbort: { daemonProxyRequestId: 1, daemonCancelTarget: 1, lspRequestId: 2, lspCancelTarget: 2 },
2685
+ noLeftovers: { daemonActiveControllersAfter: 0, lspPendingRequestsAfter: 0 },
2686
+ delayedRenamePreCommitGate: { zeroWrites: true },
2687
+ cancellationAfterCommitGate: { mutationCount: 1 },
2688
+ readOnlyPreWriteConnectionFailureRetry: { retryCount: 1 },
2689
+ sequentialProxyIds: { distinct: true },
2690
+ authProtocolCwd: { tokenLoggedOrForwarded: false },
2691
+ };
2692
+ const bad = structuredClone(good);
2693
+ bad.sequentialProxyIds.distinct = false;
2694
+ if (!valid(good) || valid(bad)) process.exit(1);
2695
+ NODE
2696
+ then
2697
+ failures=$((failures + 1))
2698
+ fi
2699
+
2700
+ if ! node --input-type=module <<'NODE' >>"$out" 2>&1
2701
+ function valid(value) {
2702
+ return value?.result === "PASS"
2703
+ && value?.build?.requiredOutputs?.clientJs === true
2704
+ && value?.build?.requiredOutputs?.clientDts === true
2705
+ && value?.build?.requiredOutputs?.cliJs === true
2706
+ && value?.build?.requiredOutputs?.indexJs === true
2707
+ && value?.build?.staleDistRemoved === true
2708
+ && value?.packageJson?.hasOnlyClientAndCliExports === true
2709
+ && value?.scans?.clientJsNoWorkspaceDeps === true
2710
+ && value?.scans?.clientDtsNoWorkspaceDeps === true
2711
+ && value?.scans?.noRepositoryPathCoupling === true
2712
+ && value?.consumer?.emptyNodePath === true
2713
+ && value?.consumer?.js?.statusOk === true
2714
+ && value?.consumer?.js?.typedContextForwarded === true
2715
+ && value?.consumer?.js?.cancellation?.accepted === true
2716
+ && value?.consumer?.js?.rootImport?.rejected === true
2717
+ && value?.consumer?.js?.unknownImport?.rejected === true
2718
+ && value?.consumer?.js?.deepImport?.rejected === true
2719
+ && Array.isArray(value?.consumer?.js?.serverSymbols)
2720
+ && value.consumer.js.serverSymbols.length === 0
2721
+ && value?.consumer?.tscExitCode === 0
2722
+ && value?.adversarial?.repositoryHiddenByInstall === true;
2723
+ }
2724
+ const good = {
2725
+ result: "PASS",
2726
+ build: { requiredOutputs: { clientJs: true, clientDts: true, cliJs: true, indexJs: true }, staleDistRemoved: true },
2727
+ packageJson: { hasOnlyClientAndCliExports: true },
2728
+ scans: { clientJsNoWorkspaceDeps: true, clientDtsNoWorkspaceDeps: true, noRepositoryPathCoupling: true },
2729
+ consumer: {
2730
+ emptyNodePath: true,
2731
+ js: {
2732
+ statusOk: true,
2733
+ typedContextForwarded: true,
2734
+ cancellation: { accepted: true },
2735
+ rootImport: { rejected: true },
2736
+ unknownImport: { rejected: true },
2737
+ deepImport: { rejected: true },
2738
+ serverSymbols: [],
2739
+ },
2740
+ tscExitCode: 0,
2741
+ },
2742
+ adversarial: { repositoryHiddenByInstall: true },
2743
+ };
2744
+ const missing = { result: "PASS" };
2745
+ const leaked = structuredClone(good);
2746
+ leaked.adversarial.repositoryHiddenByInstall = false;
2747
+ const acceptedRoot = structuredClone(good);
2748
+ acceptedRoot.consumer.js.rootImport.rejected = false;
2749
+ const acceptedDeep = structuredClone(good);
2750
+ acceptedDeep.consumer.js.deepImport.rejected = false;
2751
+ const staleBuild = structuredClone(good);
2752
+ staleBuild.build.staleDistRemoved = false;
2753
+ if (!valid(good) || valid(missing) || valid(leaked) || valid(acceptedRoot) || valid(acceptedDeep) || valid(staleBuild)) process.exit(1);
2754
+ NODE
2755
+ then
2756
+ failures=$((failures + 1))
2757
+ fi
2758
+
2759
+ after_omo="$(hash_path "$REAL_OMO_ROOT")"
2760
+ after_db="$(real_db_count)"
2761
+ after_status="$(git -C "$REPO_ROOT" status --porcelain=v1 -uall)"
2762
+ [ "$before_omo" = "$after_omo" ] || failures=$((failures + 1))
2763
+ [ "$before_db" = "$after_db" ] || failures=$((failures + 1))
2764
+ [ "$before_status" = "$after_status" ] || failures=$((failures + 1))
2765
+
2766
+ printf '{"result":"%s","selfTest":true,"malformedArgsRejected":true,"fakePassRejected":true,"skipRejected":true,"hungCommandBounded":true,"healthTimeoutReadinessBounded":true,"sseStartupRetryDeterministic":true,"partialStagingCleanedTwice":true,"interruptCleanupRepeated":true,"cancellationResultFieldsMandatory":true,"clientPackageResultFieldsMandatory":true,"dirtyWorktreePreserved":%s,"realHomesUnchanged":%s}\n' \
2767
+ "$( [ "$failures" -eq 0 ] && echo PASS || echo FAIL )" \
2768
+ "$( [ "$before_status" = "$after_status" ] && echo true || echo false )" \
2769
+ "$( [ "$before_omo" = "$after_omo" ] && [ "$before_db" = "$after_db" ] && echo true || echo false )"
2770
+ cleanup_all || failures=$((failures + 1))
2771
+ NORMAL_CLEANUP_COMPLETE=1
2772
+ [ "$failures" -eq 0 ]
2773
+ }
2774
+
2775
+ build_lsp_runtime_for_qa() {
2776
+ run_bounded 180 "$EVIDENCE_DIR/build-lsp-tools.log" npm --prefix "$REPO_ROOT/packages/lsp-tools-mcp" run build || return 1
2777
+ run_bounded 180 "$EVIDENCE_DIR/build.log" npm --prefix "$REPO_ROOT/packages/lsp-daemon" run build
2778
+ }
2779
+
2780
+ scenario_marker() {
2781
+ case "$1" in
2782
+ rename) printf '%s' 'OMO_LSP_RENAME_QA' ;;
2783
+ diagnostics-freshness) printf '%s' 'OMO_LSP_DIAGNOSTICS_FRESHNESS_QA' ;;
2784
+ *) printf '%s' 'OMO_LSP_PATH_CONTRACT_QA' ;;
2785
+ esac
2786
+ }
2787
+
2788
+ scenario_prompt() {
2789
+ case "$1" in
2790
+ rename)
2791
+ printf '%s' 'OMO_LSP_RENAME_QA: call the available LSP rename tool once for source.ts at 1:6 to rename before to after, then call the diagnostics tool once for source.ts, then report completion.'
2792
+ ;;
2793
+ diagnostics-freshness)
2794
+ printf '%s' 'OMO_LSP_DIAGNOSTICS_FRESHNESS_QA: call the available LSP diagnostics tool exactly once for source.ts, then report completion.'
2795
+ ;;
2796
+ *)
2797
+ printf '%s' 'OMO_LSP_PATH_CONTRACT_QA: call the available LSP status tool exactly once, then report completion.'
2798
+ ;;
2799
+ esac
2800
+ }
2801
+
2802
+ run_normal() {
2803
+ require_bins opencode node npm bun jq sqlite3 curl git || return 1
2804
+ prepare_evidence || return 1
2805
+ if [ -n "${LSP_E2E_INTERNAL_FIXTURE:-}" ]; then
2806
+ run_internal_fixture
2807
+ return $?
2808
+ fi
2809
+
2810
+ local opencode_bin real_omo_before real_omo_after real_db_before real_db_after worktree_before worktree_after
2811
+ local build_rc port pass auth url encoded_dir session_response session_id prompt_code daemon_pid_file daemon_pid sandbox_removed=false
2812
+ opencode_bin="$(command -v opencode)"
2813
+ REAL_DB_PATH="$(opencode db path 2>/dev/null | head -1 || true)"
2814
+ real_db_before="$(real_db_count)"
2815
+ real_omo_before="$(hash_path "$REAL_OMO_ROOT")" || return 1
2816
+ REAL_OMO_BEFORE_HASH="$real_omo_before"
2817
+ worktree_before="$(git -C "$REPO_ROOT" status --porcelain=v1 -uall)"
2818
+ printf 'real_omo_root=%s before=%s\nreal_db=%s session_count_before=%s\n' \
2819
+ "$REAL_OMO_ROOT" "$real_omo_before" "${REAL_DB_PATH:-ABSENT}" "$real_db_before" >"$EVIDENCE_DIR/isolation-receipt.txt"
2820
+
2821
+ SANDBOX_ROOT="$(mktemp -d -t oqa-lsp-e2e.XXXXXX)" || return 1
2822
+ mkdir -p "$SANDBOX_ROOT/data" "$SANDBOX_ROOT/config" "$SANDBOX_ROOT/cache" "$SANDBOX_ROOT/state" "$SANDBOX_ROOT/home" "$SANDBOX_ROOT/project"
2823
+ export HOME="$SANDBOX_ROOT/home"
2824
+ export XDG_DATA_HOME="$SANDBOX_ROOT/data"
2825
+ export XDG_CONFIG_HOME="$SANDBOX_ROOT/config"
2826
+ export XDG_CACHE_HOME="$SANDBOX_ROOT/cache"
2827
+ export XDG_STATE_HOME="$SANDBOX_ROOT/state"
2828
+ export OPENCODE_TEST_HOME="$SANDBOX_ROOT/home"
2829
+ export OPENCODE_DISABLE_AUTOUPDATE=1
2830
+ export OPENCODE_DISABLE_MODELS_FETCH=1
2831
+ export OMO_DISABLE_POSTHOG=1
2832
+ export OMO_LSP_DAEMON_DIR="$SANDBOX_ROOT/omo/lsp-daemon"
2833
+ export LSP_TOOLS_MCP_PROJECT_CONFIG="$SANDBOX_ROOT/project/.opencode/lsp.json:$SANDBOX_ROOT/project/.omo/lsp.json:$SANDBOX_ROOT/project/.omo/lsp-client.json"
2834
+ export LSP_TOOLS_MCP_USER_CONFIG="$XDG_CONFIG_HOME/opencode/lsp.json"
2835
+ export LSP_TOOLS_MCP_INSTALL_DECISIONS="$XDG_CONFIG_HOME/opencode/lsp-install-decisions.json"
2836
+ unset OMO_LSP_DAEMON_CLI OMO_LSP_DAEMON_VERSION
2837
+ OMO_TEST_ROOT="$OMO_LSP_DAEMON_DIR"
2838
+ EXPECTED_DAEMON_CLI="$REPO_ROOT/packages/lsp-daemon/dist/cli.js"
2839
+ export QA_SCENARIO="$SCENARIO"
2840
+ export QA_SOURCE_FILE="source.ts"
2841
+ export QA_MARKER="$(scenario_marker "$SCENARIO")"
2842
+
2843
+ with_shared_build_lock "opencode-lsp-e2e-build" build_lsp_runtime_for_qa
2844
+ build_rc=$?
2845
+ [ "$build_rc" -eq 0 ] || { fail "daemon build failed (see build.log)"; return 1; }
2846
+ [ -f "$EXPECTED_DAEMON_CLI" ] || { fail "built daemon CLI is missing"; return 1; }
2847
+ EXPECTED_DAEMON_VERSION="$(node --input-type=module - "$REPO_ROOT/packages/lsp-daemon/dist/package.json" <<'NODE'
2848
+ import { readFileSync } from "node:fs";
2849
+ const packageJson = JSON.parse(readFileSync(process.argv[2], "utf8"));
2850
+ if (typeof packageJson.version !== "string") process.exit(1);
2851
+ process.stdout.write(packageJson.version);
2852
+ NODE
2853
+ )"
2854
+ [ -n "$EXPECTED_DAEMON_VERSION" ] || { fail "built daemon version is missing"; return 1; }
2855
+ export OMO_LSP_DAEMON_CLI="$EXPECTED_DAEMON_CLI"
2856
+ export OMO_LSP_DAEMON_VERSION="$EXPECTED_DAEMON_VERSION"
2857
+
2858
+ write_path_contract_probe "$SANDBOX_ROOT/contract" "$EVIDENCE_DIR/path-contract.json" || {
2859
+ fail "path-contract probe failed (see path-contract-probe.log)"
2860
+ return 1
2861
+ }
2862
+ if [ "$SCENARIO" = "source-dist-reuse" ]; then
2863
+ run_source_dist_reuse_probe || { fail "source/dist reuse probe failed"; return 1; }
2864
+ fi
2865
+ if [ "$SCENARIO" = "auth-ownership" ]; then
2866
+ run_auth_ownership_probe || { fail "auth ownership probe failed (see auth-ownership-probe.log)"; return 1; }
2867
+ stop_known_daemon || return 1
2868
+ stop_owned_sandbox_processes || return 1
2869
+ safe_rm_tree "$SANDBOX_ROOT" || return 1
2870
+ SANDBOX_ROOT=""
2871
+ sandbox_removed=true
2872
+ real_omo_after="$(hash_path "$REAL_OMO_ROOT")" || return 1
2873
+ real_db_after="$(real_db_count)"
2874
+ worktree_after="$(git -C "$REPO_ROOT" status --porcelain=v1 -uall)"
2875
+ printf '%s\n' "$worktree_after" >"$EVIDENCE_DIR/worktree-after.txt"
2876
+ [ "$real_omo_before" = "$real_omo_after" ] || { fail "real OMO daemon root changed"; return 1; }
2877
+ [ "$real_db_before" = "$real_db_after" ] || { fail "real OpenCode DB session count changed"; return 1; }
2878
+ [ "$worktree_before" = "$worktree_after" ] || { fail "driver changed the dirty worktree"; return 1; }
2879
+ printf 'after=%s unchanged=yes\nreal_db_after=%s unchanged=yes\n' "$real_omo_after" "$real_db_after" >>"$EVIDENCE_DIR/isolation-receipt.txt"
2880
+ printf 'auth_ownership_probe_complete=true\nisolated_state_removed=%s\n' "$sandbox_removed" >"$EVIDENCE_DIR/cleanup-receipt.txt"
2881
+ write_auth_ownership_result "$real_omo_before" "$real_omo_after" "$real_db_before" "$real_db_after" \
2882
+ "$worktree_before" "$worktree_after" "$sandbox_removed" || return 1
2883
+ NORMAL_CLEANUP_COMPLETE=1
2884
+ jq -c . "$EVIDENCE_DIR/result.json"
2885
+ return 0
2886
+ fi
2887
+ if [ "$SCENARIO" = "rename" ]; then
2888
+ write_workspace_edit_fixture "$SANDBOX_ROOT/project" || return 1
2889
+ run_workspace_edit_contract_probe || { fail "workspace-edit contract probe failed (see workspace-edit-contract-probe.log)"; return 1; }
2890
+ elif [ "$SCENARIO" = "diagnostics-freshness" ]; then
2891
+ write_diagnostics_freshness_fixture "$SANDBOX_ROOT/project" || return 1
2892
+ run_diagnostics_freshness_contract_probe || { fail "diagnostics freshness contract probe failed (see diagnostics-freshness-contract-probe.log)"; return 1; }
2893
+ elif [ "$SCENARIO" = "post-edit" ]; then
2894
+ run_post_edit_contract_probe || { fail "post-edit contract probe failed (see post-edit-contract-probe.log)"; return 1; }
2895
+ elif [ "$SCENARIO" = "cancellation" ]; then
2896
+ run_cancellation_contract_probe || { fail "cancellation contract probe failed (see cancellation-contract-probe.log)"; return 1; }
2897
+ elif [ "$SCENARIO" = "client-package" ]; then
2898
+ run_client_package_contract_probe || { fail "client package contract probe failed (see client-package-smoke.log)"; return 1; }
2899
+ fi
2900
+ start_fake_provider || return 1
2901
+ write_sandbox_config || return 1
2902
+
2903
+ port="$(node --input-type=module - <<'NODE'
2904
+ import net from "node:net";
2905
+ const server = net.createServer();
2906
+ server.listen(0, "127.0.0.1", () => {
2907
+ const address = server.address();
2908
+ process.stdout.write(String(typeof address === "object" && address ? address.port : 0));
2909
+ server.close();
2910
+ });
2911
+ NODE
2912
+ )"
2913
+ pass="oqa-$RANDOM$RANDOM"
2914
+ auth="opencode:$pass"
2915
+ url="http://127.0.0.1:$port"
2916
+ printf '%s\n' "$SANDBOX_ROOT/project" >"$EVIDENCE_DIR/opencode-serve-cwd.txt"
2917
+ (
2918
+ cd "$SANDBOX_ROOT/project" || exit 1
2919
+ export OPENCODE_SERVER_PASSWORD="$pass"
2920
+ exec "$opencode_bin" serve --port "$port" --hostname 127.0.0.1
2921
+ ) >"$EVIDENCE_DIR/opencode-serve.stdout.log" 2>"$EVIDENCE_DIR/opencode-serve.stderr.log" &
2922
+ OPENCODE_PID=$!
2923
+ wait_http "$url/global/health" "$auth" || { fail "OpenCode server did not become ready"; return 1; }
2924
+
2925
+ encoded_dir="$(urlencode "$SANDBOX_ROOT/project")"
2926
+ wait_for_sse_connected "$url" "$auth" "$encoded_dir" || return 1
2927
+
2928
+ session_response="$(curl -sS -u "$auth" -X POST "$url/session?directory=$encoded_dir" \
2929
+ -H 'content-type: application/json' -d '{"title":"OMO LSP path contract QA"}' 2>/dev/null || true)"
2930
+ printf '%s\n' "$session_response" >"$EVIDENCE_DIR/session-create.json"
2931
+ session_id="$(printf '%s' "$session_response" | jq -r '.id // .sessionID // empty' 2>/dev/null || true)"
2932
+ [ -n "$session_id" ] || { fail "OpenCode session creation failed"; return 1; }
2933
+
2934
+ prompt_code="$(curl -sS -o "$EVIDENCE_DIR/prompt-response.txt" -w '%{http_code}' -u "$auth" \
2935
+ -X POST "$url/session/$session_id/prompt_async?directory=$encoded_dir" \
2936
+ -H 'content-type: application/json' \
2937
+ -d "{\"model\":{\"providerID\":\"openai\",\"modelID\":\"gpt-fake\"},\"parts\":[{\"type\":\"text\",\"text\":\"$(scenario_prompt "$SCENARIO")\"}]}" \
2938
+ 2>"$EVIDENCE_DIR/prompt.stderr.log" || true)"
2939
+ [ "$prompt_code" = "204" ] || { fail "prompt_async returned HTTP $prompt_code"; return 1; }
2940
+
2941
+ wait_for_session_result "$url" "$auth" "$session_id" "$encoded_dir" "$EVIDENCE_DIR/messages.json" || {
2942
+ if [ -s "$EVIDENCE_DIR/session-terminal-failure.json" ]; then
2943
+ fail "OpenCode LSP tool call reached terminal error (see session-terminal-failure.json)"
2944
+ else
2945
+ fail "OpenCode LSP tool call did not complete within the bound"
2946
+ fi
2947
+ return 1
2948
+ }
2949
+ extract_tool_evidence || { fail "SSE/message/provider tool evidence failed"; return 1; }
2950
+ record_daemon_state || return 1
2951
+ daemon_pid_file="$(find_daemon_pid_file)"
2952
+ daemon_pid="$(tr -d '[:space:]' <"$daemon_pid_file")"
2953
+
2954
+ stop_verified_pid "$SSE_PID" "curl" "SSE watcher" || return 1
2955
+ SSE_PID=""
2956
+ stop_verified_pid "$OPENCODE_PID" "serve" "OpenCode server" || return 1
2957
+ OPENCODE_PID=""
2958
+ stop_known_daemon || return 1
2959
+ stop_verified_pid "$FAKE_PID" "$SANDBOX_ROOT/fake-provider.mjs" "fake provider" || return 1
2960
+ FAKE_PID=""
2961
+ stop_owned_sandbox_processes || return 1
2962
+ kill -0 "$daemon_pid" 2>/dev/null && { fail "daemon pid remained alive after cleanup"; return 1; }
2963
+ safe_rm_tree "$SANDBOX_ROOT" || return 1
2964
+ SANDBOX_ROOT=""
2965
+ sandbox_removed=true
2966
+
2967
+ real_omo_after="$(hash_path "$REAL_OMO_ROOT")" || return 1
2968
+ real_db_after="$(real_db_count)"
2969
+ worktree_after="$(git -C "$REPO_ROOT" status --porcelain=v1 -uall)"
2970
+ [ "$real_omo_before" = "$real_omo_after" ] || { fail "real OMO daemon root changed"; return 1; }
2971
+ [ "$real_db_before" = "$real_db_after" ] || { fail "real OpenCode DB session count changed"; return 1; }
2972
+ [ "$worktree_before" = "$worktree_after" ] || { fail "driver changed the dirty worktree"; return 1; }
2973
+ printf 'after=%s unchanged=yes\nreal_db_session_count_after=%s unchanged=yes\n' \
2974
+ "$real_omo_after" "$real_db_after" >>"$EVIDENCE_DIR/isolation-receipt.txt"
2975
+ printf 'daemon_pid=%s alive_after=no\nopencode_pid=stopped\nfake_provider_pid=stopped\nsse_watcher_pid=stopped\nisolated_state_removed=%s\n' \
2976
+ "$daemon_pid" "$sandbox_removed" >"$EVIDENCE_DIR/cleanup-receipt.txt"
2977
+
2978
+ write_final_result "$real_omo_before" "$real_omo_after" "$real_db_before" "$real_db_after" \
2979
+ "$worktree_before" "$worktree_after" "$sandbox_removed" || return 1
2980
+ NORMAL_CLEANUP_COMPLETE=1
2981
+ jq -c . "$EVIDENCE_DIR/result.json"
2982
+ }
2983
+
2984
+ run_all_scenarios() {
2985
+ require_bins bash node jq git || return 1
2986
+ prepare_evidence || return 1
2987
+ local scenarios scenario failures=0 before_omo before_db before_status after_omo after_db after_status
2988
+ REAL_DB_PATH="$(opencode db path 2>/dev/null | head -1 || true)"
2989
+ scenarios="path-contract rename diagnostics-freshness post-edit cancellation client-package source-dist-reuse auth-ownership"
2990
+ before_omo="$(hash_path "$REAL_OMO_ROOT")" || return 1
2991
+ before_db="$(real_db_count)"
2992
+ before_status="$(git -C "$REPO_ROOT" status --porcelain=v1 -uall)"
2993
+ printf '%s\n' "$scenarios" >"$EVIDENCE_DIR/all-scenarios.txt"
2994
+ for scenario in $scenarios; do
2995
+ mkdir -p "$EVIDENCE_DIR/$scenario"
2996
+ if ! bash "${BASH_SOURCE[0]}" --scenario "$scenario" --evidence-dir "$EVIDENCE_DIR/$scenario" >"$EVIDENCE_DIR/$scenario.command.log" 2>&1; then
2997
+ failures=$((failures + 1))
2998
+ continue
2999
+ fi
3000
+ [ -s "$EVIDENCE_DIR/$scenario/result.json" ] || { failures=$((failures + 1)); continue; }
3001
+ jq -e '.result == "PASS" and .scenario != "SKIP"' "$EVIDENCE_DIR/$scenario/result.json" >/dev/null || failures=$((failures + 1))
3002
+ done
3003
+ after_omo="$(hash_path "$REAL_OMO_ROOT")" || return 1
3004
+ after_db="$(real_db_count)"
3005
+ after_status="$(git -C "$REPO_ROOT" status --porcelain=v1 -uall)"
3006
+ [ "$before_omo" = "$after_omo" ] || failures=$((failures + 1))
3007
+ [ "$before_db" = "$after_db" ] || failures=$((failures + 1))
3008
+ [ "$before_status" = "$after_status" ] || failures=$((failures + 1))
3009
+ node --input-type=module - "$EVIDENCE_DIR" "$before_omo" "$after_omo" "$before_db" "$after_db" "$before_status" "$after_status" <<'NODE'
3010
+ import { readFileSync, writeFileSync } from "node:fs";
3011
+ import { join } from "node:path";
3012
+ const [evidenceDir, omoBefore, omoAfter, dbBefore, dbAfter, statusBefore, statusAfter] = process.argv.slice(2);
3013
+ const scenarios = readFileSync(join(evidenceDir, "all-scenarios.txt"), "utf8").trim().split(/\s+/);
3014
+ const entries = scenarios.map((scenario) => {
3015
+ try {
3016
+ return [scenario, JSON.parse(readFileSync(join(evidenceDir, scenario, "result.json"), "utf8"))];
3017
+ } catch (error) {
3018
+ return [scenario, {
3019
+ result: "FAIL",
3020
+ scenario,
3021
+ missingResult: true,
3022
+ error: error instanceof Error ? error.message : String(error),
3023
+ artifacts: { commandLog: `${scenario}.command.log` },
3024
+ }];
3025
+ }
3026
+ });
3027
+ const results = Object.fromEntries(entries);
3028
+ const values = entries.map(([, value]) => value);
3029
+ const hasUnchangedOpenCodeDb = (value) =>
3030
+ value.realOpenCodeDbSessionCountUnchanged === true || value.realDbSessionCountUnchanged === true;
3031
+ const scenariosWithSse = entries.filter(([scenario]) => scenario !== "auth-ownership").map(([, value]) => value);
3032
+ const payload = {
3033
+ result: values.every((value) => value.result === "PASS") && omoBefore === omoAfter && dbBefore === dbAfter && statusBefore === statusAfter ? "PASS" : "FAIL",
3034
+ scenario: "all",
3035
+ harness: "opencode",
3036
+ scenarioOrder: scenarios,
3037
+ scenarioResults: Object.fromEntries(entries.map(([name, value]) => [name, { result: value.result, cleanup: value.cleanup ?? {}, artifacts: value.artifacts ?? {} }])),
3038
+ realOmoRootUnchanged: omoBefore === omoAfter && values.every((value) => value.realOmoRootUnchanged === true),
3039
+ realOpenCodeDbSessionCountUnchanged: dbBefore === dbAfter && values.every(hasUnchangedOpenCodeDb),
3040
+ dirtyWorktreePreserved: statusBefore === statusAfter && values.every((value) => value.dirtyWorktreePreserved === true),
3041
+ noSkip: values.every((value) => value.result !== "SKIP" && value.scenario !== "SKIP"),
3042
+ sseEvidence: scenariosWithSse.every((value) => value.sseEvidence?.connected === true && value.sseEvidence?.toolObserved === true),
3043
+ pathContract: results["path-contract"]?.overrideAssertions ?? null,
3044
+ pairRecovery: results["path-contract"]?.failureFixtures ?? null,
3045
+ reuse: results["source-dist-reuse"]?.sourceDistReuse ?? null,
3046
+ auth: results["auth-ownership"]?.authOwnership ?? results["auth-ownership"] ?? null,
3047
+ cancellation: results.cancellation?.cancellationContract ?? null,
3048
+ cleanup: {
3049
+ isolatedStateRemoved: values.every((value) => value.cleanup?.isolatedStateRemoved === true),
3050
+ daemonStopped: values.every((value) => value.cleanup?.daemonStopped === true),
3051
+ },
3052
+ };
3053
+ writeFileSync(join(evidenceDir, "result.json"), `${JSON.stringify(payload, null, 2)}\n`);
3054
+ console.log(JSON.stringify(payload));
3055
+ NODE
3056
+ NORMAL_CLEANUP_COMPLETE=1
3057
+ [ "$failures" -eq 0 ] && jq -e '.result == "PASS" and .noSkip == true' "$EVIDENCE_DIR/result.json" >/dev/null
3058
+ }
3059
+
3060
+ main() {
3061
+ parse_args "$@" || return $?
3062
+ if [ "$SELF_TEST" -eq 1 ]; then
3063
+ run_self_test
3064
+ elif [ "$SCENARIO" = "all" ]; then
3065
+ run_all_scenarios
3066
+ else
3067
+ run_normal
3068
+ fi
3069
+ }
3070
+
3071
+ main "$@"