oh-my-opencode 4.11.1 → 4.12.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 (170) hide show
  1. package/.agents/skills/codex-qa/scripts/lib/app-server-client.mjs +132 -82
  2. package/.agents/skills/codex-qa/scripts/lib/app-server-client.test.js +48 -0
  3. package/.agents/skills/opencode-qa/scripts/serve-wake-split-probe.sh +361 -84
  4. package/.agents/skills/work-with-pr/SKILL.md +19 -5
  5. package/.opencode/skills/work-with-pr/SKILL.md +19 -5
  6. package/dist/agents/types.d.ts +7 -6
  7. package/dist/cli/index.js +460 -309
  8. package/dist/cli-node/index.js +460 -309
  9. package/dist/features/background-agent/parent-wake-flush-runner.d.ts +1 -0
  10. package/dist/features/background-agent/parent-wake-history-state.d.ts +1 -0
  11. package/dist/features/background-agent/parent-wake-session-history.d.ts +1 -47
  12. package/dist/features/background-agent/parent-wake-session-message.d.ts +47 -0
  13. package/dist/index.js +593 -310
  14. package/dist/plugin/tool-execute-before.d.ts +2 -0
  15. package/dist/plugin-handlers/plugin-components-loader.d.ts +1 -0
  16. package/dist/skills/ultraresearch/SKILL.md +11 -2
  17. package/dist/skills/ulw-plan/SKILL.md +2 -1
  18. package/dist/skills/ulw-plan/references/full-workflow.md +1 -1
  19. package/dist/skills/ulw-plan/references/intent-clear.md +2 -2
  20. package/dist/tools/background-task/constants.d.ts +1 -1
  21. package/dist/tools/delegate-task/sync-session-poller.d.ts +1 -2
  22. package/dist/tools/delegate-task/sync-session-turns.d.ts +3 -0
  23. package/dist/tui.js +25 -10
  24. package/package.json +13 -13
  25. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +24 -2
  26. package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +25 -9
  27. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  28. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  29. package/packages/omo-codex/plugin/components/codegraph/dist/cli.js +336 -173
  30. package/packages/omo-codex/plugin/components/codegraph/dist/serve.js +284 -33
  31. package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
  32. package/packages/omo-codex/plugin/components/codegraph/src/cli.ts +11 -0
  33. package/packages/omo-codex/plugin/components/codegraph/src/hook-types.ts +12 -0
  34. package/packages/omo-codex/plugin/components/codegraph/src/hook.ts +34 -0
  35. package/packages/omo-codex/plugin/components/codegraph/src/serve.ts +47 -5
  36. package/packages/omo-codex/plugin/components/codegraph/src/session-start-worker.ts +7 -5
  37. package/packages/omo-codex/plugin/components/codegraph/test/hook.test.ts +58 -96
  38. package/packages/omo-codex/plugin/components/codegraph/test/provisioned-node-guard.test.ts +94 -0
  39. package/packages/omo-codex/plugin/components/codegraph/test/serve-node-support.test.ts +33 -0
  40. package/packages/omo-codex/plugin/components/codegraph/test/serve-provision.test.ts +48 -0
  41. package/packages/omo-codex/plugin/components/codegraph/test/serve.test.ts +1 -0
  42. package/packages/omo-codex/plugin/components/codegraph/test/session-start-node-support.test.ts +142 -0
  43. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  44. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  45. package/packages/omo-codex/plugin/components/comment-checker/test/package-smoke.test.ts +2 -71
  46. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  47. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  48. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  49. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  50. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  51. package/packages/omo-codex/plugin/components/lsp/package.json +2 -2
  52. package/packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts +26 -104
  53. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  54. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  55. package/packages/omo-codex/plugin/components/rules/test/package-smoke.test.ts +12 -92
  56. package/packages/omo-codex/plugin/components/start-work-continuation/dist/cli.js +157 -234
  57. package/packages/omo-codex/plugin/components/start-work-continuation/hooks/hooks.json +2 -2
  58. package/packages/omo-codex/plugin/components/start-work-continuation/package.json +1 -2
  59. package/packages/omo-codex/plugin/components/start-work-continuation/src/boulder-reader.ts +227 -15
  60. package/packages/omo-codex/plugin/components/start-work-continuation/test/boulder-reader.test.ts +62 -2
  61. package/packages/omo-codex/plugin/components/start-work-continuation/test/cli.test.ts +34 -3
  62. package/packages/omo-codex/plugin/components/teammode/dist/cli.js +94 -0
  63. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +17 -0
  64. package/packages/omo-codex/plugin/components/teammode/package.json +26 -0
  65. package/packages/omo-codex/plugin/components/teammode/skills/teammode/SKILL.md +139 -0
  66. package/packages/omo-codex/plugin/components/teammode/skills/teammode/scripts/team-guide.mjs +111 -0
  67. package/packages/omo-codex/plugin/components/teammode/skills/teammode/scripts/team-state.mjs +250 -0
  68. package/packages/omo-codex/plugin/components/teammode/skills/teammode/scripts/team.mjs +220 -0
  69. package/packages/omo-codex/plugin/components/teammode/src/cli.ts +12 -0
  70. package/packages/omo-codex/plugin/components/teammode/src/codex-hook.ts +125 -0
  71. package/packages/omo-codex/plugin/components/teammode/test/thread-title-hook.test.ts +116 -0
  72. package/packages/omo-codex/plugin/components/teammode/tsconfig.json +25 -0
  73. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  74. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  75. package/packages/omo-codex/plugin/components/test-support/package-smoke-fixture.ts +158 -0
  76. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  77. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  78. package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/SKILL.md +2 -1
  79. package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/agents/openai.yaml +1 -1
  80. package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/references/full-workflow.md +1 -1
  81. package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/references/intent-clear.md +2 -2
  82. package/packages/omo-codex/plugin/components/ultrawork/test/package-smoke.test.ts +76 -49
  83. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +2 -2
  84. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  85. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/agents/openai.yaml +1 -1
  86. package/packages/omo-codex/plugin/components/ulw-loop/test/package-smoke.test.ts +1 -1
  87. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +17 -0
  88. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +17 -0
  89. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +17 -0
  90. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +17 -0
  91. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +17 -0
  92. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +17 -0
  93. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +17 -0
  94. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +17 -0
  95. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +17 -0
  96. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +17 -0
  97. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +17 -0
  98. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +17 -0
  99. package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +16 -0
  100. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +16 -0
  101. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +16 -0
  102. package/packages/omo-codex/plugin/hooks/stop-checking-start-work-continuation.json +16 -0
  103. package/packages/omo-codex/plugin/hooks/subagent-stop-checking-start-work-continuation.json +16 -0
  104. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +17 -0
  105. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +16 -0
  106. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +16 -0
  107. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +16 -0
  108. package/packages/omo-codex/plugin/package-lock.json +30 -22
  109. package/packages/omo-codex/plugin/package.json +2 -1
  110. package/packages/omo-codex/plugin/scripts/hook-status-message.mjs +15 -11
  111. package/packages/omo-codex/plugin/scripts/sync-hook-status-messages.mjs +15 -4
  112. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +37 -0
  113. package/packages/omo-codex/plugin/skills/ast-grep/agents/openai.yaml +2 -0
  114. package/packages/omo-codex/plugin/skills/comment-checker/agents/openai.yaml +2 -0
  115. package/packages/omo-codex/plugin/skills/debugging/agents/openai.yaml +2 -0
  116. package/packages/omo-codex/plugin/skills/frontend/agents/openai.yaml +2 -0
  117. package/packages/omo-codex/plugin/skills/git-master/agents/openai.yaml +1 -1
  118. package/packages/omo-codex/plugin/skills/init-deep/agents/openai.yaml +2 -0
  119. package/packages/omo-codex/plugin/skills/lcx-contribute-bug-fix/agents/openai.yaml +1 -1
  120. package/packages/omo-codex/plugin/skills/lcx-doctor/agents/openai.yaml +1 -1
  121. package/packages/omo-codex/plugin/skills/lcx-report-bug/agents/openai.yaml +1 -1
  122. package/packages/omo-codex/plugin/skills/lsp/agents/openai.yaml +2 -0
  123. package/packages/omo-codex/plugin/skills/lsp-setup/agents/openai.yaml +2 -0
  124. package/packages/omo-codex/plugin/skills/programming/agents/openai.yaml +2 -0
  125. package/packages/omo-codex/plugin/skills/refactor/agents/openai.yaml +2 -0
  126. package/packages/omo-codex/plugin/skills/remove-ai-slops/agents/openai.yaml +2 -0
  127. package/packages/omo-codex/plugin/skills/review-work/agents/openai.yaml +2 -0
  128. package/packages/omo-codex/plugin/skills/rules/agents/openai.yaml +2 -0
  129. package/packages/omo-codex/plugin/skills/start-work/agents/openai.yaml +2 -0
  130. package/packages/omo-codex/plugin/skills/teammode/SKILL.md +139 -0
  131. package/packages/omo-codex/plugin/skills/teammode/agents/openai.yaml +2 -0
  132. package/packages/omo-codex/plugin/skills/teammode/scripts/team-guide.mjs +111 -0
  133. package/packages/omo-codex/plugin/skills/teammode/scripts/team-state.mjs +250 -0
  134. package/packages/omo-codex/plugin/skills/teammode/scripts/team.mjs +220 -0
  135. package/packages/omo-codex/plugin/skills/ultraresearch/SKILL.md +11 -2
  136. package/packages/omo-codex/plugin/skills/ultraresearch/agents/openai.yaml +2 -0
  137. package/packages/omo-codex/plugin/skills/ulw-loop/agents/openai.yaml +1 -1
  138. package/packages/omo-codex/plugin/skills/ulw-plan/SKILL.md +2 -1
  139. package/packages/omo-codex/plugin/skills/ulw-plan/agents/openai.yaml +1 -1
  140. package/packages/omo-codex/plugin/skills/ulw-plan/references/full-workflow.md +1 -1
  141. package/packages/omo-codex/plugin/skills/ulw-plan/references/intent-clear.md +2 -2
  142. package/packages/omo-codex/plugin/skills/visual-qa/agents/openai.yaml +2 -0
  143. package/packages/omo-codex/plugin/test/aggregate-hooks.test.mjs +70 -33
  144. package/packages/omo-codex/plugin/test/aggregate-manifest.test.mjs +6 -2
  145. package/packages/omo-codex/plugin/test/aggregate-plugin-fixture.mjs +14 -1
  146. package/packages/omo-codex/plugin/test/bootstrap-binlinks.test.mjs +1 -1
  147. package/packages/omo-codex/plugin/test/bootstrap-hooks.test.mjs +14 -6
  148. package/packages/omo-codex/plugin/test/bootstrap-ps-guard.test.mjs +7 -3
  149. package/packages/omo-codex/plugin/test/component-bundled-cli.test.mjs +4 -3
  150. package/packages/omo-codex/plugin/test/component-hook-contract-cases.mjs +24 -0
  151. package/packages/omo-codex/plugin/test/display-metadata.test.mjs +83 -0
  152. package/packages/omo-codex/plugin/test/hook-status-message.test.mjs +31 -19
  153. package/packages/omo-codex/plugin/test/lcx-bug-skills.test.mjs +3 -3
  154. package/packages/omo-codex/plugin/test/sync-hook-status-messages.test.mjs +6 -6
  155. package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +2 -0
  156. package/packages/omo-codex/plugin/test/sync-skills.test.mjs +9 -4
  157. package/packages/omo-codex/plugin/test/teammode-safety-fixture.mjs +88 -0
  158. package/packages/omo-codex/plugin/test/teammode-safety.test.mjs +240 -0
  159. package/packages/omo-codex/plugin/test/ultraresearch-skill-contract.test.mjs +48 -0
  160. package/packages/omo-codex/scripts/install-dist/install-local.mjs +398 -206
  161. package/packages/omo-codex/scripts/install-hook-targets.test.mjs +93 -0
  162. package/packages/omo-codex/scripts/install-lazycodex-version-stamp.test.mjs +2 -2
  163. package/packages/omo-codex/scripts/install-local-bun-global-update.test.mjs +63 -0
  164. package/packages/omo-codex/scripts/install-local-entrypoint.test.mjs +109 -1
  165. package/packages/omo-codex/scripts/install-local.mjs +1 -0
  166. package/packages/shared-skills/skills/ultraresearch/SKILL.md +11 -2
  167. package/packages/shared-skills/skills/ulw-plan/SKILL.md +2 -1
  168. package/packages/shared-skills/skills/ulw-plan/references/full-workflow.md +1 -1
  169. package/packages/shared-skills/skills/ulw-plan/references/intent-clear.md +2 -2
  170. package/packages/omo-codex/plugin/hooks/hooks.json +0 -214
@@ -7,8 +7,9 @@
7
7
  # or routes correctly through the live listener (FIXED).
8
8
  #
9
9
  # Two assertion modes:
10
- # --expect reproduced exit 0 if stops>1 OR children>1 OR mechanism arm true
11
- # --expect fixed exit 0 if children==1 AND stops==1
10
+ # --expect reproduced exit 0 if terminal_stops>1 OR child_task_sessions>1 OR mechanism arm true
11
+ # --expect fixed exit 0 if terminal_stops==1, child_task_sessions==1,
12
+ # fixed branch counts hold, and route logs show live dispatch
12
13
  #
13
14
  # Usage:
14
15
  # serve-wake-split-probe.sh [--expect reproduced|fixed] [--evidence-dir DIR]
@@ -45,10 +46,18 @@ FAKE_LLM_LOG="" # set after evidence dir is known
45
46
  while [ $# -gt 0 ]; do
46
47
  case "$1" in
47
48
  --expect)
49
+ if [ $# -lt 2 ] || [ "${2#--}" != "$2" ]; then
50
+ printf 'error: --expect requires reproduced or fixed\n' >&2
51
+ exit 2
52
+ fi
48
53
  EXPECT_MODE="$2"
49
54
  shift 2
50
55
  ;;
51
56
  --evidence-dir)
57
+ if [ $# -lt 2 ] || [ "${2#--}" != "$2" ]; then
58
+ printf 'error: --evidence-dir requires a directory\n' >&2
59
+ exit 2
60
+ fi
52
61
  EVIDENCE_DIR="$2"
53
62
  shift 2
54
63
  ;;
@@ -81,6 +90,22 @@ fi
81
90
  swsp_log() { printf '%s\n' "$*" >&2; }
82
91
  swsp_info() { printf '[swsp] %s\n' "$*" >&2; }
83
92
 
93
+ swsp_tail_log_since_offset() {
94
+ local offset="$1"
95
+ local log_path="$2"
96
+ if [ ! -f "$log_path" ]; then
97
+ return 0
98
+ fi
99
+ local current_size
100
+ current_size="$(wc -c <"$log_path" 2>/dev/null | tr -d ' ')" || current_size=0
101
+ current_size="${current_size:-0}"
102
+ if [ "$offset" -gt "$current_size" ] 2>/dev/null; then
103
+ return 0
104
+ else
105
+ tail -c "+$((offset + 1))" "$log_path" 2>/dev/null || true
106
+ fi
107
+ }
108
+
84
109
  # Start the fake-LLM server; sets FAKE_SERVER_PID + FAKE_SERVER_PORT.
85
110
  swsp_start_fake_llm() {
86
111
  local log_file="$1"
@@ -203,9 +228,9 @@ JSONC
203
228
  swsp_info "opencode.jsonc written to $cfg_dir/opencode/opencode.jsonc"
204
229
  }
205
230
 
206
- # Poll the sandbox DB for children/stops on a message matching a LIKE pattern.
231
+ # Poll the sandbox DB for parent assistant step metrics on a message matching a LIKE pattern.
207
232
  # Args: db_path like_pattern timeout_s
208
- # Outputs: "<children> <stops>" on stdout
233
+ # Outputs: "<parent_assistant_messages> <parent_tool_call_turns> <terminal_stops> <child_task_sessions>" on stdout
209
234
  swsp_poll_db_metrics() {
210
235
  local db="$1"
211
236
  local like_pat="$2"
@@ -222,17 +247,28 @@ swsp_poll_db_metrics() {
222
247
  ),
223
248
  counts AS (
224
249
  SELECT
225
- count(a.id) AS children,
226
- sum(CASE WHEN json_extract(a.data, '\$.finish') = 'stop' THEN 1 ELSE 0 END) AS stops
250
+ count(a.id) AS parent_assistant_messages,
251
+ sum(CASE WHEN json_extract(a.data, '\$.finish') = 'tool-calls' THEN 1 ELSE 0 END) AS parent_tool_call_turns,
252
+ sum(CASE WHEN json_extract(a.data, '\$.finish') = 'stop' THEN 1 ELSE 0 END) AS terminal_stops
227
253
  FROM target t
228
254
  LEFT JOIN message a
229
255
  ON a.session_id = t.session_id
230
256
  AND json_extract(a.data, '\$.parentID') = t.user_id
231
257
  GROUP BY t.user_id
258
+ ),
259
+ child_task_sessions AS (
260
+ SELECT count(DISTINCT m.session_id) AS child_task_sessions
261
+ FROM message m
262
+ JOIN part p ON p.message_id = m.id
263
+ WHERE json_extract(m.data, '\$.role') = 'user'
264
+ AND json_extract(p.data, '\$.type') = 'text'
265
+ AND json_extract(p.data, '\$.text') LIKE '%SPLIT_CHILD_TASK:%'
232
266
  )
233
- SELECT printf('%d %d',
234
- coalesce((SELECT max(children) FROM counts), 0),
235
- coalesce((SELECT max(stops) FROM counts), 0)
267
+ SELECT printf('%d %d %d %d',
268
+ coalesce((SELECT max(parent_assistant_messages) FROM counts), 0),
269
+ coalesce((SELECT max(parent_tool_call_turns) FROM counts), 0),
270
+ coalesce((SELECT max(terminal_stops) FROM counts), 0),
271
+ coalesce((SELECT child_task_sessions FROM child_task_sessions), 0)
236
272
  );
237
273
  "
238
274
 
@@ -245,13 +281,15 @@ swsp_poll_db_metrics() {
245
281
  local result
246
282
  result="$(sqlite3 "$db" "$metrics_query" 2>/dev/null)" || true
247
283
 
248
- local children stops
249
- children="$(printf '%s' "$result" | awk '{print $1}')"
250
- stops="$(printf '%s' "$result" | awk '{print $2}')"
284
+ local parent_assistant_messages parent_tool_call_turns terminal_stops child_task_sessions
285
+ parent_assistant_messages="$(printf '%s' "$result" | awk '{print $1}')"
286
+ parent_tool_call_turns="$(printf '%s' "$result" | awk '{print $2}')"
287
+ terminal_stops="$(printf '%s' "$result" | awk '{print $3}')"
288
+ child_task_sessions="$(printf '%s' "$result" | awk '{print $4}')"
251
289
 
252
290
  # Return once we have at least 1 stop (parent session finished)
253
- if [ -n "$stops" ] && [ "${stops:-0}" -ge 1 ] 2>/dev/null; then
254
- printf '%s %s' "$children" "$stops"
291
+ if [ -n "$terminal_stops" ] && [ "${terminal_stops:-0}" -ge 1 ] 2>/dev/null; then
292
+ printf '%s %s %s %s' "$parent_assistant_messages" "$parent_tool_call_turns" "$terminal_stops" "$child_task_sessions"
255
293
  return 0
256
294
  fi
257
295
  sleep 0.5
@@ -260,7 +298,7 @@ swsp_poll_db_metrics() {
260
298
  # Return whatever we have on timeout
261
299
  local result
262
300
  result="$(sqlite3 "$db" "$metrics_query" 2>/dev/null)" || true
263
- printf '%s' "${result:-0 0}"
301
+ printf '%s' "${result:-0 0 0 0}"
264
302
  }
265
303
 
266
304
  # Wait until a session is no longer in the server's active status map.
@@ -291,11 +329,9 @@ swsp_count_plugin_inits() {
291
329
  printf '0'
292
330
  return 0
293
331
  fi
294
- # tail from byte offset
295
- tail -c "+$((offset + 1))" "$log_path" 2>/dev/null \
332
+ swsp_tail_log_since_offset "$offset" "$log_path" \
296
333
  | grep "ENTRY - plugin loading" \
297
- | grep -c "$sandbox_dir" 2>/dev/null \
298
- || printf '0'
334
+ | awk -v sandbox_dir="$sandbox_dir" 'index($0, sandbox_dir) { count += 1 } END { print count + 0 }'
299
335
  }
300
336
 
301
337
  # Detect WAKE_DISPATCHED_DURING_PARENT_TURN:
@@ -321,7 +357,7 @@ swsp_detect_wake_during_parent() {
321
357
 
322
358
  # Check for gate dispatch log line with parent-wake source since offset
323
359
  local dispatch_line
324
- dispatch_line="$(tail -c "+$((offset + 1))" "$omo_log" 2>/dev/null \
360
+ dispatch_line="$(swsp_tail_log_since_offset "$offset" "$omo_log" \
325
361
  | grep "promptAsync dispatching" \
326
362
  | grep -i "parent-wake\|background-agent-parent-wake" \
327
363
  | head -1)" || true
@@ -350,10 +386,97 @@ swsp_detect_wake_during_parent() {
350
386
  fi
351
387
  }
352
388
 
389
+ swsp_has_session_live_dispatch() {
390
+ local route_prov="$1"
391
+ local session_id="$2"
392
+ [ -n "$session_id" ] || return 1
393
+ printf '%s\n' "$route_prov" \
394
+ | grep -F "dispatch via live listener" \
395
+ | grep -F "\"sessionID\":\"${session_id}\"" >/dev/null 2>&1
396
+ }
397
+
398
+ swsp_is_nonnegative_int() {
399
+ case "${1:-}" in
400
+ ''|*[!0-9]*) return 1 ;;
401
+ *) return 0 ;;
402
+ esac
403
+ }
404
+
405
+ swsp_fixed_topology_observed() {
406
+ local parent_assistant_messages="$1"
407
+ local parent_tool_call_turns="$2"
408
+ local terminal_stops="$3"
409
+ local child_task_sessions="$4"
410
+ local parent_tool_call_branches="$5"
411
+ local parent_hold_branches="$6"
412
+ local child_branches="$7"
413
+ local wake_branches="$8"
414
+ local default_branches="$9"
415
+ local has_live_dispatch="${10}"
416
+
417
+ swsp_is_nonnegative_int "$parent_assistant_messages" || return 1
418
+ swsp_is_nonnegative_int "$parent_tool_call_turns" || return 1
419
+ swsp_is_nonnegative_int "$terminal_stops" || return 1
420
+ swsp_is_nonnegative_int "$child_task_sessions" || return 1
421
+ swsp_is_nonnegative_int "$parent_tool_call_branches" || return 1
422
+ swsp_is_nonnegative_int "$parent_hold_branches" || return 1
423
+ swsp_is_nonnegative_int "$child_branches" || return 1
424
+ swsp_is_nonnegative_int "$wake_branches" || return 1
425
+ swsp_is_nonnegative_int "$default_branches" || return 1
426
+
427
+ [ "$has_live_dispatch" = "true" ] || return 1
428
+
429
+ if [ "${terminal_stops:-0}" -ne 1 ] \
430
+ || [ "${child_task_sessions:-0}" -ne 1 ] \
431
+ || [ "${parent_tool_call_turns:-0}" -ne 2 ] \
432
+ || [ "${parent_assistant_messages:-0}" -ne 3 ] \
433
+ || [ "${parent_tool_call_branches:-0}" -ne 1 ] \
434
+ || [ "${parent_hold_branches:-0}" -ne 1 ] \
435
+ || [ "${child_branches:-0}" -ne 1 ] \
436
+ || [ "${default_branches:-0}" -lt 1 ] \
437
+ || [ "${wake_branches:-0}" -ne 0 ] 2>/dev/null; then
438
+ return 1
439
+ fi
440
+
441
+ return 0
442
+ }
443
+
444
+ swsp_collect_route_provenance() {
445
+ local offset="$1"
446
+ local log_path="$2"
447
+ local project_dir="$3"
448
+ local session_id="$4"
449
+ local output_file="$5"
450
+ local all_output_file="$6"
451
+ local timeout_s="${7:-0}"
452
+ local deadline
453
+ deadline=$(( $(date +%s) + timeout_s ))
454
+
455
+ while :; do
456
+ local route_prov_all="" route_prov=""
457
+ if [ -f "$log_path" ]; then
458
+ route_prov_all="$(swsp_tail_log_since_offset "$offset" "$log_path" \
459
+ | grep -E "live-server-route" || true)"
460
+ route_prov="$(printf '%s\n' "$route_prov_all" \
461
+ | awk -v dir="$project_dir" -v sid="\"sessionID\":\"${session_id}\"" 'index($0, dir) || index($0, sid)' || true)"
462
+ fi
463
+ printf '%s' "$route_prov" >"$all_output_file"
464
+ printf '%s' "$route_prov" >"$output_file"
465
+ if swsp_has_session_live_dispatch "$route_prov" "$session_id"; then
466
+ return 0
467
+ fi
468
+ if [ "$timeout_s" -le 0 ] || [ "$(date +%s)" -ge "$deadline" ]; then
469
+ return 1
470
+ fi
471
+ sleep 0.25
472
+ done
473
+ }
474
+
353
475
  # Verify branch-count guard: all required branches fired.
354
476
  # Returns 0 if OK, 1 if any required branch missing (also sets RESULT=HARNESS_ERROR).
355
477
  swsp_check_branch_counts() {
356
478
  local fake_log="$1"
479
+ local mode="${2:-}"
357
480
  local ptc pc cc wc
358
481
  ptc="$(grep -c "branch=parent-tool-call" "$fake_log" 2>/dev/null | tr -d '[:space:]')"; ptc="${ptc:-0}"
359
482
  pc="$(grep -c "branch=parent-hold" "$fake_log" 2>/dev/null | tr -d '[:space:]')"; pc="${pc:-0}"
@@ -364,7 +487,7 @@ swsp_check_branch_counts() {
364
487
 
365
488
  swsp_info "branch counts: parent-tool-call=$ptc parent-hold=$pc child=$cc wake=$wc"
366
489
 
367
- if [ "$ptc" -lt 1 ] || [ "$pc" -lt 1 ] || [ "$cc" -lt 1 ] || [ "$wc" -lt 1 ]; then
490
+ if [ "$ptc" -lt 1 ] || [ "$pc" -lt 1 ] || [ "$cc" -lt 1 ]; then
368
491
  printf 'RESULT=HARNESS_ERROR branch_counts parent-tool-call=%s parent-hold=%s child=%s wake=%s\n' \
369
492
  "$ptc" "$pc" "$cc" "$wc"
370
493
  return 1
@@ -400,6 +523,33 @@ swsp_self_test() {
400
523
  fi
401
524
  fi
402
525
 
526
+ local missing_expect_out missing_expect_err missing_evidence_out missing_evidence_err
527
+ missing_expect_out="$(mktemp -t swsp-missing-expect-out.XXXXXX)"
528
+ missing_expect_err="$(mktemp -t swsp-missing-expect-err.XXXXXX)"
529
+ missing_evidence_out="$(mktemp -t swsp-missing-evidence-out.XXXXXX)"
530
+ missing_evidence_err="$(mktemp -t swsp-missing-evidence-err.XXXXXX)"
531
+ OQA_TMPDIRS+=("$missing_expect_out" "$missing_expect_err" "$missing_evidence_out" "$missing_evidence_err")
532
+
533
+ if bash "${BASH_SOURCE[0]}" --expect >"$missing_expect_out" 2>"$missing_expect_err"; then
534
+ swsp_log "FAIL: missing --expect operand unexpectedly succeeded"
535
+ fails=$((fails+1))
536
+ elif grep -q "error: --expect requires reproduced or fixed" "$missing_expect_err"; then
537
+ swsp_info "PASS: missing --expect operand fails with usage error"
538
+ else
539
+ swsp_log "FAIL: missing --expect operand did not emit usage error"
540
+ fails=$((fails+1))
541
+ fi
542
+
543
+ if bash "${BASH_SOURCE[0]}" --evidence-dir --self-test >"$missing_evidence_out" 2>"$missing_evidence_err"; then
544
+ swsp_log "FAIL: missing --evidence-dir operand unexpectedly succeeded"
545
+ fails=$((fails+1))
546
+ elif grep -q "error: --evidence-dir requires a directory" "$missing_evidence_err"; then
547
+ swsp_info "PASS: missing --evidence-dir operand fails with usage error"
548
+ else
549
+ swsp_log "FAIL: missing --evidence-dir operand did not emit usage error"
550
+ fails=$((fails+1))
551
+ fi
552
+
403
553
  # Sandbox + opencode serve
404
554
  if ! oqa_start_server; then
405
555
  swsp_log "FAIL: opencode serve did not start"
@@ -433,16 +583,117 @@ swsp_self_test() {
433
583
  fi
434
584
  fi
435
585
 
586
+ local route_fixture
587
+ route_fixture='[2026-06-19T00:00:00.000Z] [live-server-route] dispatch via live listener {"sessionID":"ses_other","source":"background-agent-parent-wake"}
588
+ [2026-06-19T00:00:01.000Z] [live-server-route] dispatch via live listener {"sessionID":"ses_probe","source":"background-agent-parent-wake"}'
589
+ if swsp_has_session_live_dispatch "$route_fixture" "ses_probe" \
590
+ && ! swsp_has_session_live_dispatch "$route_fixture" "ses_missing"; then
591
+ swsp_info "PASS: live dispatch detection is scoped to the probe session"
592
+ else
593
+ swsp_log "FAIL: live dispatch detection accepted an unrelated session"
594
+ fails=$((fails+1))
595
+ fi
596
+
597
+ local branch_log
598
+ branch_log="$(mktemp -t swsp-branch-log.XXXXXX)"
599
+ OQA_TMPDIRS+=("$branch_log")
600
+ {
601
+ printf '[2026-06-19T00:00:00.000Z] branch=parent-tool-call\n'
602
+ printf '[2026-06-19T00:00:01.000Z] branch=parent-hold\n'
603
+ printf '[2026-06-19T00:00:02.000Z] branch=child\n'
604
+ } >"$branch_log"
605
+ if swsp_check_branch_counts "$branch_log" reproduced >/dev/null 2>&1; then
606
+ swsp_info "PASS: reproduced branch guard accepts mechanism-only evidence"
607
+ else
608
+ swsp_log "FAIL: reproduced branch guard still requires wake branch"
609
+ fails=$((fails+1))
610
+ fi
611
+
612
+ if swsp_fixed_topology_observed 3 2 1 1 1 1 1 0 1 true; then
613
+ swsp_info "PASS: fixed topology accepts scoped live dispatch plus deterministic DB/provider evidence"
614
+ else
615
+ swsp_log "FAIL: fixed topology rejected scoped live dispatch plus deterministic DB/provider evidence"
616
+ fails=$((fails+1))
617
+ fi
618
+
619
+ if swsp_fixed_topology_observed 3 2 1 1 1 1 1 0 1 false; then
620
+ swsp_log "FAIL: fixed topology accepted missing scoped live dispatch"
621
+ fails=$((fails+1))
622
+ else
623
+ swsp_info "PASS: fixed topology rejects missing scoped live dispatch"
624
+ fi
625
+
626
+ if swsp_fixed_topology_observed bad 2 1 1 1 1 1 0 1 true; then
627
+ swsp_log "FAIL: fixed topology accepted malformed numeric evidence"
628
+ fails=$((fails+1))
629
+ else
630
+ swsp_info "PASS: fixed topology rejects malformed numeric evidence"
631
+ fi
632
+
633
+ if swsp_fixed_topology_observed 3 2 2 1 1 1 1 0 1 true; then
634
+ swsp_log "FAIL: fixed topology accepted duplicate terminal stop"
635
+ fails=$((fails+1))
636
+ else
637
+ swsp_info "PASS: fixed topology rejects duplicate terminal stop"
638
+ fi
639
+
640
+ local stale_log stale_scoped stale_all
641
+ stale_log="$(mktemp -t swsp-stale-log.XXXXXX)"
642
+ stale_scoped="$(mktemp -t swsp-stale-scoped.XXXXXX)"
643
+ stale_all="$(mktemp -t swsp-stale-all.XXXXXX)"
644
+ OQA_TMPDIRS+=("$stale_log" "$stale_scoped" "$stale_all")
645
+ printf '[2026-06-19T00:00:00.000Z] [live-server-route] dispatch via live listener {"sessionID":"ses_unrelated","source":"background-agent-parent-wake"}\n' >"$stale_log"
646
+ swsp_collect_route_provenance 999999 "$stale_log" "/probe" "ses_probe" "$stale_scoped" "$stale_all" 0 || true
647
+ if [ ! -s "$stale_scoped" ] && [ ! -s "$stale_all" ]; then
648
+ swsp_info "PASS: stale log offset does not persist unrelated route provenance"
649
+ else
650
+ swsp_log "FAIL: stale log offset persisted unrelated route provenance"
651
+ fails=$((fails+1))
652
+ fi
653
+
654
+ local scoped_log scoped_out scoped_all
655
+ scoped_log="$(mktemp -t swsp-scoped-log.XXXXXX)"
656
+ scoped_out="$(mktemp -t swsp-scoped-out.XXXXXX)"
657
+ scoped_all="$(mktemp -t swsp-scoped-all.XXXXXX)"
658
+ OQA_TMPDIRS+=("$scoped_log" "$scoped_out" "$scoped_all")
659
+ {
660
+ printf '[2026-06-19T00:00:00.000Z] [live-server-route] dispatch via live listener {"sessionID":"ses_other","source":"background-agent-parent-wake"}\n'
661
+ printf '[2026-06-19T00:00:01.000Z] [live-server-route] dispatch via live listener {"sessionID":"ses_probe","source":"background-agent-parent-wake"}\n'
662
+ } >"$scoped_log"
663
+ swsp_collect_route_provenance 0 "$scoped_log" "/probe" "ses_probe" "$scoped_out" "$scoped_all" 0 || true
664
+ if grep -q "ses_probe" "$scoped_all" && ! grep -q "ses_other" "$scoped_all"; then
665
+ swsp_info "PASS: route provenance artifact excludes unrelated sessions"
666
+ else
667
+ swsp_log "FAIL: route provenance artifact included unrelated sessions"
668
+ fails=$((fails+1))
669
+ fi
670
+
671
+ local route_wait_log route_wait_scoped route_wait_all
672
+ route_wait_log="$(mktemp -t swsp-route-wait-log.XXXXXX)"
673
+ route_wait_scoped="$(mktemp -t swsp-route-wait-scoped.XXXXXX)"
674
+ route_wait_all="$(mktemp -t swsp-route-wait-all.XXXXXX)"
675
+ OQA_TMPDIRS+=("$route_wait_log" "$route_wait_scoped" "$route_wait_all")
676
+ printf '[2026-06-19T00:00:00.000Z] [live-server-route] registered {"directory":"/probe","hasServerUrl":true}\n' >"$route_wait_log"
677
+ (
678
+ sleep 0.5
679
+ printf '[2026-06-19T00:00:01.000Z] [live-server-route] dispatch via live listener {"sessionID":"ses_wait","source":"background-agent-parent-wake"}\n' >>"$route_wait_log"
680
+ ) &
681
+ local route_wait_pid=$!
682
+ if swsp_collect_route_provenance 0 "$route_wait_log" "/probe" "ses_wait" "$route_wait_scoped" "$route_wait_all" 3; then
683
+ swsp_info "PASS: route provenance waits for delayed session dispatch"
684
+ else
685
+ swsp_log "FAIL: route provenance did not wait for delayed session dispatch"
686
+ fails=$((fails+1))
687
+ fi
688
+ wait "$route_wait_pid" 2>/dev/null || true
689
+
690
+ local st_fake_pid="$FAKE_SERVER_PID"
436
691
  swsp_stop_fake_llm
437
692
 
438
- # Orphan check
439
- local orphan_count
440
- orphan_count="$(pgrep -f "fake-openai-server" 2>/dev/null | wc -l | tr -d ' ')" || orphan_count=0
441
- if [ "${orphan_count:-0}" -eq 0 ]; then
442
- swsp_info "PASS: no orphan fake-openai-server processes"
693
+ if [ -n "$st_fake_pid" ] && ! kill -0 "$st_fake_pid" 2>/dev/null; then
694
+ swsp_info "PASS: fake-openai server process stopped"
443
695
  else
444
- swsp_log "FAIL: $orphan_count orphan fake-openai-server process(es) remain"
445
- pkill -f "fake-openai-server" 2>/dev/null || true
696
+ swsp_log "FAIL: fake-openai server process still running"
446
697
  fails=$((fails+1))
447
698
  fi
448
699
 
@@ -573,29 +824,38 @@ swsp_run_probe() {
573
824
  2>/dev/null)" || prompt_response=""
574
825
  swsp_info "prompt_async response: $prompt_response"
575
826
 
576
- # Step 7: Poll sandbox DB for children/stops; also wait for session idle
577
827
  swsp_info "polling DB for wake-split metrics (up to 120s)..."
578
828
  local metrics
579
- metrics="$(swsp_poll_db_metrics "$sandbox_db" '%[BACKGROUND TASK%' 120)"
580
- local children stops
581
- children="$(printf '%s' "$metrics" | awk '{print $1}')"
582
- stops="$(printf '%s' "$metrics" | awk '{print $2}')"
583
- children="${children:-0}"
584
- stops="${stops:-0}"
585
- swsp_info "DB metrics: children=$children stops=$stops"
829
+ metrics="$(swsp_poll_db_metrics "$sandbox_db" '%Run the split probe:%' 120)"
830
+ local parent_assistant_messages parent_tool_call_turns terminal_stops child_task_sessions
831
+ parent_assistant_messages="$(printf '%s' "$metrics" | awk '{print $1}')"
832
+ parent_tool_call_turns="$(printf '%s' "$metrics" | awk '{print $2}')"
833
+ terminal_stops="$(printf '%s' "$metrics" | awk '{print $3}')"
834
+ child_task_sessions="$(printf '%s' "$metrics" | awk '{print $4}')"
835
+ parent_assistant_messages="${parent_assistant_messages:-0}"
836
+ parent_tool_call_turns="${parent_tool_call_turns:-0}"
837
+ terminal_stops="${terminal_stops:-0}"
838
+ child_task_sessions="${child_task_sessions:-0}"
839
+ swsp_info "DB metrics: parent_assistant_messages=$parent_assistant_messages parent_tool_call_turns=$parent_tool_call_turns terminal_stops=$terminal_stops child_task_sessions=$child_task_sessions"
586
840
 
587
841
  # Wait for parent session to go idle
588
842
  swsp_info "waiting for parent session to go idle..."
589
843
  swsp_wait_session_idle "$OQA_SERVER_URL" "$pass" "$ses_id" 60
590
844
 
591
845
  # Re-read metrics after idle
592
- metrics="$(swsp_poll_db_metrics "$sandbox_db" '%[BACKGROUND TASK%' 10)"
593
- children="$(printf '%s' "$metrics" | awk '{print $1}')"
594
- stops="$(printf '%s' "$metrics" | awk '{print $2}')"
595
- children="${children:-0}"
596
- stops="${stops:-0}"
597
- swsp_info "final DB metrics: children=$children stops=$stops"
598
- printf 'children=%s stops=%s\n' "$children" "$stops" >"$evidence_dir/marker-metrics.txt"
846
+ metrics="$(swsp_poll_db_metrics "$sandbox_db" '%Run the split probe:%' 10)"
847
+ parent_assistant_messages="$(printf '%s' "$metrics" | awk '{print $1}')"
848
+ parent_tool_call_turns="$(printf '%s' "$metrics" | awk '{print $2}')"
849
+ terminal_stops="$(printf '%s' "$metrics" | awk '{print $3}')"
850
+ child_task_sessions="$(printf '%s' "$metrics" | awk '{print $4}')"
851
+ parent_assistant_messages="${parent_assistant_messages:-0}"
852
+ parent_tool_call_turns="${parent_tool_call_turns:-0}"
853
+ terminal_stops="${terminal_stops:-0}"
854
+ child_task_sessions="${child_task_sessions:-0}"
855
+ swsp_info "final DB metrics: parent_assistant_messages=$parent_assistant_messages parent_tool_call_turns=$parent_tool_call_turns terminal_stops=$terminal_stops child_task_sessions=$child_task_sessions"
856
+ printf 'parent_assistant_messages=%s parent_tool_call_turns=%s terminal_stops=%s child_task_sessions=%s\n' \
857
+ "$parent_assistant_messages" "$parent_tool_call_turns" "$terminal_stops" "$child_task_sessions" \
858
+ >"$evidence_dir/marker-metrics.txt"
599
859
 
600
860
  # Step 8: Plugin-init count
601
861
  local plugin_inits
@@ -606,11 +866,15 @@ swsp_run_probe() {
606
866
 
607
867
  # Step 9: Route provenance
608
868
  local route_prov=""
609
- if [ -f "$omo_log" ]; then
610
- route_prov="$(tail -c "+$((omo_log_offset + 1))" "$omo_log" 2>/dev/null \
611
- | grep -E "live-server-route" || true)"
612
- fi
613
- printf '%s\n' "$route_prov" >"$evidence_dir/route-provenance.log"
869
+ swsp_collect_route_provenance \
870
+ "$omo_log_offset" \
871
+ "$omo_log" \
872
+ "$OQA_PROJ" \
873
+ "$ses_id" \
874
+ "$evidence_dir/route-provenance.log" \
875
+ "$evidence_dir/route-provenance-all.log" \
876
+ 10 || true
877
+ route_prov="$(cat "$evidence_dir/route-provenance.log" 2>/dev/null || true)"
614
878
  swsp_info "route-provenance lines: $(printf '%s' "$route_prov" | wc -l | tr -d ' ')"
615
879
 
616
880
  # WAKE_DISPATCHED_DURING_PARENT_TURN mechanism signal
@@ -618,16 +882,30 @@ swsp_run_probe() {
618
882
  wake_during_parent="$(swsp_detect_wake_during_parent "$omo_log_offset" "$fake_llm_log" "$OQA_PROJ")"
619
883
  swsp_info "WAKE_DISPATCHED_DURING_PARENT_TURN=$wake_during_parent"
620
884
 
885
+ local real_db_count_after=""
886
+ if [ -n "$real_db_path" ] && [ "$real_db_path" != "(not found)" ] && [ -f "$real_db_path" ]; then
887
+ real_db_count_after="$(sqlite3 "$real_db_path" 'SELECT count(*) FROM session' 2>/dev/null || echo "0")"
888
+ else
889
+ real_db_count_after="$real_db_count_before"
890
+ fi
891
+ printf 'after=%s unchanged=%s\n' \
892
+ "$real_db_count_after" \
893
+ "$([ "$real_db_count_after" = "$real_db_count_before" ] && echo yes || echo NO)" \
894
+ >>"$evidence_dir/isolation-receipt.txt"
895
+ swsp_info "isolation: real DB before=$real_db_count_before after=$real_db_count_after"
896
+
897
+ sqlite3 "$sandbox_db" ".backup '$evidence_dir/sandbox-opencode.db'" 2>/dev/null || true
898
+
621
899
  # Step 10: Branch-count guard
622
- if ! swsp_check_branch_counts "$fake_llm_log" >&2; then
900
+ if ! swsp_check_branch_counts "$fake_llm_log" "$EXPECT_MODE" >&2; then
623
901
  # Branch counts not met — HARNESS_ERROR
624
902
  local ptc pc cc wc
625
- ptc="$(grep -c "branch=parent-tool-call" "$fake_llm_log" 2>/dev/null || printf '0')"
626
- pc="$(grep -c "branch=parent-hold" "$fake_llm_log" 2>/dev/null || printf '0')"
627
- cc="$(grep -c "branch=child" "$fake_llm_log" 2>/dev/null || printf '0')"
628
- wc="$(grep -c "branch=wake" "$fake_llm_log" 2>/dev/null || printf '0')"
903
+ ptc="$(grep -c "branch=parent-tool-call" "$fake_llm_log" 2>/dev/null || true)"; ptc="${ptc:-0}"
904
+ pc="$(grep -c "branch=parent-hold" "$fake_llm_log" 2>/dev/null || true)"; pc="${pc:-0}"
905
+ cc="$(grep -c "branch=child" "$fake_llm_log" 2>/dev/null || true)"; cc="${cc:-0}"
906
+ wc="$(grep -c "branch=wake" "$fake_llm_log" 2>/dev/null || true)"; wc="${wc:-0}"
629
907
  local verdict_line
630
- verdict_line="RESULT=HARNESS_ERROR children=${children} stops=${stops} plugin_inits=${plugin_inits} WAKE_DISPATCHED_DURING_PARENT_TURN=${wake_during_parent} branch_counts=parent-tool-call:${ptc},parent-hold:${pc},child:${cc},wake:${wc}"
908
+ verdict_line="RESULT=HARNESS_ERROR parent_assistant_messages=${parent_assistant_messages} parent_tool_call_turns=${parent_tool_call_turns} terminal_stops=${terminal_stops} child_task_sessions=${child_task_sessions} plugin_inits=${plugin_inits} WAKE_DISPATCHED_DURING_PARENT_TURN=${wake_during_parent} branch_counts=parent-tool-call:${ptc},parent-hold:${pc},child:${cc},wake:${wc}"
631
909
  printf '%s\n' "$verdict_line" | tee -a "$harness_log"
632
910
  swsp_stop_fake_llm
633
911
  return 1
@@ -636,29 +914,45 @@ swsp_run_probe() {
636
914
  # Step 11: Determine verdict
637
915
  local result="INCONCLUSIVE"
638
916
  local exit_code=1
639
-
640
- # Arm 1: SQLite evidence (stops>1 or children>1)
641
- if [ "${stops:-0}" -gt 1 ] || [ "${children:-0}" -gt 1 ] 2>/dev/null; then
917
+ local ptc pc cc wc
918
+ ptc="$(grep -c "branch=parent-tool-call" "$fake_llm_log" 2>/dev/null || true)"; ptc="${ptc:-0}"
919
+ pc="$(grep -c "branch=parent-hold" "$fake_llm_log" 2>/dev/null || true)"; pc="${pc:-0}"
920
+ cc="$(grep -c "branch=child" "$fake_llm_log" 2>/dev/null || true)"; cc="${cc:-0}"
921
+ wc="$(grep -c "branch=wake" "$fake_llm_log" 2>/dev/null || true)"; wc="${wc:-0}"
922
+ local dc
923
+ dc="$(grep -c "branch=default" "$fake_llm_log" 2>/dev/null || true)"; dc="${dc:-0}"
924
+
925
+ if [ "${terminal_stops:-0}" -gt 1 ] || [ "${child_task_sessions:-0}" -gt 1 ] 2>/dev/null; then
642
926
  result="REPRODUCED"
643
927
  fi
644
928
 
645
929
  # Arm 2: Mechanism signal (wake dispatched during parent turn + in-process path)
646
930
  # in-process path: no live-server-route dispatch line for this wake
647
931
  local has_live_dispatch=false
648
- if printf '%s' "$route_prov" | grep -q "dispatch via live listener" 2>/dev/null; then
932
+ if swsp_has_session_live_dispatch "$route_prov" "$ses_id"; then
649
933
  has_live_dispatch=true
650
934
  fi
651
935
  if [ "$wake_during_parent" = "true" ] && [ "$has_live_dispatch" = "false" ]; then
652
936
  result="REPRODUCED"
653
937
  fi
654
938
 
655
- # FIXED: exactly 1 child and 1 stop, and neither REPRODUCED arm held
656
- if [ "$result" = "INCONCLUSIVE" ] && [ "${children:-0}" -eq 1 ] && [ "${stops:-0}" -eq 1 ] 2>/dev/null; then
939
+ if [ "$result" = "INCONCLUSIVE" ] \
940
+ && swsp_fixed_topology_observed \
941
+ "$parent_assistant_messages" \
942
+ "$parent_tool_call_turns" \
943
+ "$terminal_stops" \
944
+ "$child_task_sessions" \
945
+ "$ptc" \
946
+ "$pc" \
947
+ "$cc" \
948
+ "$wc" \
949
+ "$dc" \
950
+ "$has_live_dispatch"; then
657
951
  result="FIXED"
658
952
  fi
659
953
 
660
954
  local verdict_line
661
- verdict_line="RESULT=${result} children=${children} stops=${stops} plugin_inits=${plugin_inits} WAKE_DISPATCHED_DURING_PARENT_TURN=${wake_during_parent}"
955
+ verdict_line="RESULT=${result} parent_assistant_messages=${parent_assistant_messages} parent_tool_call_turns=${parent_tool_call_turns} terminal_stops=${terminal_stops} child_task_sessions=${child_task_sessions} plugin_inits=${plugin_inits} WAKE_DISPATCHED_DURING_PARENT_TURN=${wake_during_parent} route_live_dispatch=${has_live_dispatch} branch_counts=parent-tool-call:${ptc},parent-hold:${pc},child:${cc},wake:${wc},default:${dc}"
662
956
  printf '%s\n' "$verdict_line" | tee -a "$harness_log"
663
957
 
664
958
  # Determine exit code based on expected mode
@@ -674,34 +968,17 @@ swsp_run_probe() {
674
968
  exit_code=0 # no expectation: just report
675
969
  fi
676
970
 
677
- # Step 12: Isolation receipt — verify real DB count unchanged
678
- local real_db_count_after=""
679
- if [ -n "$real_db_path" ] && [ "$real_db_path" != "(not found)" ] && [ -f "$real_db_path" ]; then
680
- real_db_count_after="$(sqlite3 "$real_db_path" 'SELECT count(*) FROM session' 2>/dev/null || echo "0")"
681
- else
682
- real_db_count_after="$real_db_count_before"
683
- fi
684
- printf 'after=%s unchanged=%s\n' \
685
- "$real_db_count_after" \
686
- "$([ "$real_db_count_after" = "$real_db_count_before" ] && echo yes || echo NO)" \
687
- >>"$evidence_dir/isolation-receipt.txt"
688
- swsp_info "isolation: real DB before=$real_db_count_before after=$real_db_count_after"
689
-
690
- # Preserve the sandbox DB for post-hoc inspection before the trap removes it
691
- sqlite3 "$sandbox_db" ".backup '$evidence_dir/sandbox-opencode.db'" 2>/dev/null || true
692
-
693
971
  # Cleanup receipt
972
+ local stopped_fake_pid="$FAKE_SERVER_PID"
694
973
  swsp_stop_fake_llm
695
974
  printf 'fake_llm=stopped opencode_serve=stopping\n' >"$evidence_dir/cleanup-receipt.txt"
696
975
 
697
- # Orphan check
698
- local orphan_count
699
- orphan_count="$(pgrep -f "fake-openai-server" 2>/dev/null | wc -l | tr -d ' ')" || orphan_count=0
700
- if [ "${orphan_count:-0}" -gt 0 ]; then
701
- swsp_log "WARNING: $orphan_count orphan fake-openai-server process(es); killing"
702
- pkill -f "fake-openai-server" 2>/dev/null || true
976
+ if [ -n "$stopped_fake_pid" ] && kill -0 "$stopped_fake_pid" 2>/dev/null; then
977
+ swsp_log "WARNING: fake-openai server process $stopped_fake_pid still running after cleanup"
978
+ printf 'fake_llm_pid_alive=yes pid=%s\n' "$stopped_fake_pid" >>"$evidence_dir/cleanup-receipt.txt"
979
+ else
980
+ printf 'fake_llm_pid_alive=no pid=%s\n' "$stopped_fake_pid" >>"$evidence_dir/cleanup-receipt.txt"
703
981
  fi
704
- printf 'orphan_fake_llm=%s\n' "${orphan_count:-0}" >>"$evidence_dir/cleanup-receipt.txt"
705
982
 
706
983
  return "$exit_code"
707
984
  }