oh-my-copilot 5.6.2 → 5.8.0

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 (264) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +2 -0
  4. package/bridge/__pycache__/gyoshu_bridge.cpython-312.pyc +0 -0
  5. package/bridge/claude-md-coordinator.cjs +2 -2
  6. package/bridge/cli.cjs +81405 -76559
  7. package/bridge/mcp-server.cjs +40048 -32187
  8. package/bridge/runtime-cli.cjs +943 -199
  9. package/bridge/team-mcp.cjs +716 -125
  10. package/bridge/team.js +1066 -290
  11. package/dist/__tests__/cli-win32-warning.test.js +7 -0
  12. package/dist/__tests__/cli-win32-warning.test.js.map +1 -1
  13. package/dist/__tests__/run-cjs-copilot-worker-routing.test.d.ts +2 -0
  14. package/dist/__tests__/run-cjs-copilot-worker-routing.test.d.ts.map +1 -0
  15. package/dist/__tests__/run-cjs-copilot-worker-routing.test.js +294 -0
  16. package/dist/__tests__/run-cjs-copilot-worker-routing.test.js.map +1 -0
  17. package/dist/__tests__/run-cjs-fail-closed-timeout.test.d.ts +2 -0
  18. package/dist/__tests__/run-cjs-fail-closed-timeout.test.d.ts.map +1 -0
  19. package/dist/__tests__/run-cjs-fail-closed-timeout.test.js +68 -0
  20. package/dist/__tests__/run-cjs-fail-closed-timeout.test.js.map +1 -0
  21. package/dist/__tests__/run-cjs-session-end-budget.test.d.ts +2 -0
  22. package/dist/__tests__/run-cjs-session-end-budget.test.d.ts.map +1 -0
  23. package/dist/__tests__/run-cjs-session-end-budget.test.js +171 -0
  24. package/dist/__tests__/run-cjs-session-end-budget.test.js.map +1 -0
  25. package/dist/__tests__/session-end-process-exit.test.js +70 -2
  26. package/dist/__tests__/session-end-process-exit.test.js.map +1 -1
  27. package/dist/cli/__tests__/doctor-conflicts-json.test.d.ts +2 -0
  28. package/dist/cli/__tests__/doctor-conflicts-json.test.d.ts.map +1 -0
  29. package/dist/cli/__tests__/doctor-conflicts-json.test.js +46 -0
  30. package/dist/cli/__tests__/doctor-conflicts-json.test.js.map +1 -0
  31. package/dist/cli/__tests__/team.test.js +67 -0
  32. package/dist/cli/__tests__/team.test.js.map +1 -1
  33. package/dist/cli/__tests__/wait-teleport-json.test.d.ts +2 -0
  34. package/dist/cli/__tests__/wait-teleport-json.test.d.ts.map +1 -0
  35. package/dist/cli/__tests__/wait-teleport-json.test.js +64 -0
  36. package/dist/cli/__tests__/wait-teleport-json.test.js.map +1 -0
  37. package/dist/cli/commands/__tests__/smoke-tier2.test.d.ts +2 -0
  38. package/dist/cli/commands/__tests__/smoke-tier2.test.d.ts.map +1 -0
  39. package/dist/cli/commands/__tests__/smoke-tier2.test.js +110 -0
  40. package/dist/cli/commands/__tests__/smoke-tier2.test.js.map +1 -0
  41. package/dist/cli/commands/__tests__/team-status-json.test.d.ts +2 -0
  42. package/dist/cli/commands/__tests__/team-status-json.test.d.ts.map +1 -0
  43. package/dist/cli/commands/__tests__/team-status-json.test.js +141 -0
  44. package/dist/cli/commands/__tests__/team-status-json.test.js.map +1 -0
  45. package/dist/cli/commands/factory.d.ts +2 -0
  46. package/dist/cli/commands/factory.d.ts.map +1 -1
  47. package/dist/cli/commands/factory.js +8 -1
  48. package/dist/cli/commands/factory.js.map +1 -1
  49. package/dist/cli/commands/smoke.d.ts +36 -0
  50. package/dist/cli/commands/smoke.d.ts.map +1 -0
  51. package/dist/cli/commands/smoke.js +169 -0
  52. package/dist/cli/commands/smoke.js.map +1 -0
  53. package/dist/cli/commands/team.d.ts +2 -0
  54. package/dist/cli/commands/team.d.ts.map +1 -1
  55. package/dist/cli/commands/team.js +124 -10
  56. package/dist/cli/commands/team.js.map +1 -1
  57. package/dist/cli/index.d.ts.map +1 -1
  58. package/dist/cli/index.js +70 -10
  59. package/dist/cli/index.js.map +1 -1
  60. package/dist/cli/win32-warning.d.ts +7 -1
  61. package/dist/cli/win32-warning.d.ts.map +1 -1
  62. package/dist/cli/win32-warning.js +9 -1
  63. package/dist/cli/win32-warning.js.map +1 -1
  64. package/dist/constants/names.d.ts +1 -0
  65. package/dist/constants/names.d.ts.map +1 -1
  66. package/dist/constants/names.js +1 -0
  67. package/dist/constants/names.js.map +1 -1
  68. package/dist/factory/__tests__/check-run.test.js +2 -2
  69. package/dist/factory/__tests__/check-run.test.js.map +1 -1
  70. package/dist/factory/__tests__/listener.test.js +1 -1
  71. package/dist/factory/__tests__/listener.test.js.map +1 -1
  72. package/dist/factory/__tests__/status.test.js +42 -0
  73. package/dist/factory/__tests__/status.test.js.map +1 -1
  74. package/dist/factory/listener.d.ts.map +1 -1
  75. package/dist/factory/listener.js +7 -8
  76. package/dist/factory/listener.js.map +1 -1
  77. package/dist/factory/status.d.ts +19 -1
  78. package/dist/factory/status.d.ts.map +1 -1
  79. package/dist/factory/status.js +49 -8
  80. package/dist/factory/status.js.map +1 -1
  81. package/dist/factory/watchdog.d.ts.map +1 -1
  82. package/dist/factory/watchdog.js +14 -0
  83. package/dist/factory/watchdog.js.map +1 -1
  84. package/dist/factory/watchdog.test.js +45 -0
  85. package/dist/factory/watchdog.test.js.map +1 -1
  86. package/dist/hooks/session-end/__tests__/action-runner.test.js +5 -0
  87. package/dist/hooks/session-end/__tests__/action-runner.test.js.map +1 -1
  88. package/dist/hooks/session-end/__tests__/chain-enqueuer.test.js +186 -1
  89. package/dist/hooks/session-end/__tests__/chain-enqueuer.test.js.map +1 -1
  90. package/dist/hooks/session-end/__tests__/chain-link-env-passthrough.test.d.ts +2 -0
  91. package/dist/hooks/session-end/__tests__/chain-link-env-passthrough.test.d.ts.map +1 -0
  92. package/dist/hooks/session-end/__tests__/chain-link-env-passthrough.test.js +49 -0
  93. package/dist/hooks/session-end/__tests__/chain-link-env-passthrough.test.js.map +1 -0
  94. package/dist/hooks/session-end/__tests__/foreground-bootstrap.test.js +30 -1
  95. package/dist/hooks/session-end/__tests__/foreground-bootstrap.test.js.map +1 -1
  96. package/dist/hooks/session-end/__tests__/spawn-next.test.js +104 -3
  97. package/dist/hooks/session-end/__tests__/spawn-next.test.js.map +1 -1
  98. package/dist/hooks/session-end/__tests__/worker.test.js +6 -0
  99. package/dist/hooks/session-end/__tests__/worker.test.js.map +1 -1
  100. package/dist/hooks/session-end/action-runner.d.ts.map +1 -1
  101. package/dist/hooks/session-end/action-runner.js +2 -1
  102. package/dist/hooks/session-end/action-runner.js.map +1 -1
  103. package/dist/hooks/session-end/chain-enqueuer.d.ts +70 -7
  104. package/dist/hooks/session-end/chain-enqueuer.d.ts.map +1 -1
  105. package/dist/hooks/session-end/chain-enqueuer.js +354 -82
  106. package/dist/hooks/session-end/chain-enqueuer.js.map +1 -1
  107. package/dist/hooks/session-end/foreground-bootstrap.d.ts.map +1 -1
  108. package/dist/hooks/session-end/foreground-bootstrap.js +24 -16
  109. package/dist/hooks/session-end/foreground-bootstrap.js.map +1 -1
  110. package/dist/hooks/session-end/index.d.ts +2 -3
  111. package/dist/hooks/session-end/index.d.ts.map +1 -1
  112. package/dist/hooks/session-end/index.js +11 -13
  113. package/dist/hooks/session-end/index.js.map +1 -1
  114. package/dist/hooks/session-end/spawn-next.d.ts +81 -4
  115. package/dist/hooks/session-end/spawn-next.d.ts.map +1 -1
  116. package/dist/hooks/session-end/spawn-next.js +141 -14
  117. package/dist/hooks/session-end/spawn-next.js.map +1 -1
  118. package/dist/hooks/session-end/wiki-foreground-bootstrap.d.ts +17 -0
  119. package/dist/hooks/session-end/wiki-foreground-bootstrap.d.ts.map +1 -0
  120. package/dist/hooks/session-end/wiki-foreground-bootstrap.js +20 -0
  121. package/dist/hooks/session-end/wiki-foreground-bootstrap.js.map +1 -0
  122. package/dist/hooks/session-end/worker.d.ts.map +1 -1
  123. package/dist/hooks/session-end/worker.js +11 -6
  124. package/dist/hooks/session-end/worker.js.map +1 -1
  125. package/dist/lib/__tests__/atomic-write.test.js +32 -1
  126. package/dist/lib/__tests__/atomic-write.test.js.map +1 -1
  127. package/dist/lib/__tests__/mode-state-lock.test.js +121 -12
  128. package/dist/lib/__tests__/mode-state-lock.test.js.map +1 -1
  129. package/dist/lib/atomic-write.d.ts +13 -3
  130. package/dist/lib/atomic-write.d.ts.map +1 -1
  131. package/dist/lib/atomic-write.js +29 -15
  132. package/dist/lib/atomic-write.js.map +1 -1
  133. package/dist/lib/mode-state-io.d.ts +2 -2
  134. package/dist/lib/mode-state-io.d.ts.map +1 -1
  135. package/dist/lib/mode-state-io.js +16 -10
  136. package/dist/lib/mode-state-io.js.map +1 -1
  137. package/dist/mcp/__tests__/host-smoke-tool.test.d.ts +2 -0
  138. package/dist/mcp/__tests__/host-smoke-tool.test.d.ts.map +1 -0
  139. package/dist/mcp/__tests__/host-smoke-tool.test.js +174 -0
  140. package/dist/mcp/__tests__/host-smoke-tool.test.js.map +1 -0
  141. package/dist/mcp/disable-tools.d.ts.map +1 -1
  142. package/dist/mcp/disable-tools.js +1 -0
  143. package/dist/mcp/disable-tools.js.map +1 -1
  144. package/dist/mcp/tool-registry.d.ts.map +1 -1
  145. package/dist/mcp/tool-registry.js +2 -0
  146. package/dist/mcp/tool-registry.js.map +1 -1
  147. package/dist/shared/types.d.ts +11 -0
  148. package/dist/shared/types.d.ts.map +1 -1
  149. package/dist/smoke/__tests__/copilot-chain-scenario.test.d.ts +2 -0
  150. package/dist/smoke/__tests__/copilot-chain-scenario.test.d.ts.map +1 -0
  151. package/dist/smoke/__tests__/copilot-chain-scenario.test.js +154 -0
  152. package/dist/smoke/__tests__/copilot-chain-scenario.test.js.map +1 -0
  153. package/dist/smoke/__tests__/copilot-sdk-driver.test.d.ts +2 -0
  154. package/dist/smoke/__tests__/copilot-sdk-driver.test.d.ts.map +1 -0
  155. package/dist/smoke/__tests__/copilot-sdk-driver.test.js +591 -0
  156. package/dist/smoke/__tests__/copilot-sdk-driver.test.js.map +1 -0
  157. package/dist/smoke/__tests__/copilot-sdk-scenarios.test.d.ts +2 -0
  158. package/dist/smoke/__tests__/copilot-sdk-scenarios.test.d.ts.map +1 -0
  159. package/dist/smoke/__tests__/copilot-sdk-scenarios.test.js +362 -0
  160. package/dist/smoke/__tests__/copilot-sdk-scenarios.test.js.map +1 -0
  161. package/dist/smoke/__tests__/copilot-session-eval.test.d.ts +2 -0
  162. package/dist/smoke/__tests__/copilot-session-eval.test.d.ts.map +1 -0
  163. package/dist/smoke/__tests__/copilot-session-eval.test.js +264 -0
  164. package/dist/smoke/__tests__/copilot-session-eval.test.js.map +1 -0
  165. package/dist/smoke/__tests__/copilot-smoke.test.d.ts +2 -0
  166. package/dist/smoke/__tests__/copilot-smoke.test.d.ts.map +1 -0
  167. package/dist/smoke/__tests__/copilot-smoke.test.js +482 -0
  168. package/dist/smoke/__tests__/copilot-smoke.test.js.map +1 -0
  169. package/dist/smoke/copilot-binary.d.ts +21 -0
  170. package/dist/smoke/copilot-binary.d.ts.map +1 -0
  171. package/dist/smoke/copilot-binary.js +46 -0
  172. package/dist/smoke/copilot-binary.js.map +1 -0
  173. package/dist/smoke/copilot-chain-scenario.d.ts +61 -0
  174. package/dist/smoke/copilot-chain-scenario.d.ts.map +1 -0
  175. package/dist/smoke/copilot-chain-scenario.js +253 -0
  176. package/dist/smoke/copilot-chain-scenario.js.map +1 -0
  177. package/dist/smoke/copilot-sdk-driver.d.ts +189 -0
  178. package/dist/smoke/copilot-sdk-driver.d.ts.map +1 -0
  179. package/dist/smoke/copilot-sdk-driver.js +637 -0
  180. package/dist/smoke/copilot-sdk-driver.js.map +1 -0
  181. package/dist/smoke/copilot-sdk-scenarios.d.ts +200 -0
  182. package/dist/smoke/copilot-sdk-scenarios.d.ts.map +1 -0
  183. package/dist/smoke/copilot-sdk-scenarios.js +620 -0
  184. package/dist/smoke/copilot-sdk-scenarios.js.map +1 -0
  185. package/dist/smoke/copilot-session-env.d.ts +75 -0
  186. package/dist/smoke/copilot-session-env.d.ts.map +1 -0
  187. package/dist/smoke/copilot-session-env.js +154 -0
  188. package/dist/smoke/copilot-session-env.js.map +1 -0
  189. package/dist/smoke/copilot-session-eval.d.ts +90 -0
  190. package/dist/smoke/copilot-session-eval.d.ts.map +1 -0
  191. package/dist/smoke/copilot-session-eval.js +323 -0
  192. package/dist/smoke/copilot-session-eval.js.map +1 -0
  193. package/dist/smoke/copilot-smoke.d.ts +166 -0
  194. package/dist/smoke/copilot-smoke.d.ts.map +1 -0
  195. package/dist/smoke/copilot-smoke.js +688 -0
  196. package/dist/smoke/copilot-smoke.js.map +1 -0
  197. package/dist/smoke/process-utils.d.ts +47 -0
  198. package/dist/smoke/process-utils.d.ts.map +1 -0
  199. package/dist/smoke/process-utils.js +99 -0
  200. package/dist/smoke/process-utils.js.map +1 -0
  201. package/dist/team/__tests__/runtime-v2.monitor.test.js +35 -0
  202. package/dist/team/__tests__/runtime-v2.monitor.test.js.map +1 -1
  203. package/dist/team/__tests__/scaling.test.js +13 -0
  204. package/dist/team/__tests__/scaling.test.js.map +1 -1
  205. package/dist/team/__tests__/sdk-transport.runtime.test.d.ts +2 -0
  206. package/dist/team/__tests__/sdk-transport.runtime.test.d.ts.map +1 -0
  207. package/dist/team/__tests__/sdk-transport.runtime.test.js +96 -0
  208. package/dist/team/__tests__/sdk-transport.runtime.test.js.map +1 -0
  209. package/dist/team/__tests__/sdk-transport.test.d.ts +2 -0
  210. package/dist/team/__tests__/sdk-transport.test.d.ts.map +1 -0
  211. package/dist/team/__tests__/sdk-transport.test.js +650 -0
  212. package/dist/team/__tests__/sdk-transport.test.js.map +1 -0
  213. package/dist/team/__tests__/tmux-session.pane-env.test.d.ts +2 -0
  214. package/dist/team/__tests__/tmux-session.pane-env.test.d.ts.map +1 -0
  215. package/dist/team/__tests__/tmux-session.pane-env.test.js +52 -0
  216. package/dist/team/__tests__/tmux-session.pane-env.test.js.map +1 -0
  217. package/dist/team/api-interop.d.ts.map +1 -1
  218. package/dist/team/api-interop.js +8 -0
  219. package/dist/team/api-interop.js.map +1 -1
  220. package/dist/team/runtime-v2.d.ts +33 -0
  221. package/dist/team/runtime-v2.d.ts.map +1 -1
  222. package/dist/team/runtime-v2.js +338 -24
  223. package/dist/team/runtime-v2.js.map +1 -1
  224. package/dist/team/scaling.d.ts.map +1 -1
  225. package/dist/team/scaling.js +5 -0
  226. package/dist/team/scaling.js.map +1 -1
  227. package/dist/team/sdk-host.d.ts +48 -0
  228. package/dist/team/sdk-host.d.ts.map +1 -0
  229. package/dist/team/sdk-host.js +604 -0
  230. package/dist/team/sdk-host.js.map +1 -0
  231. package/dist/team/sdk-policy.d.ts +79 -0
  232. package/dist/team/sdk-policy.d.ts.map +1 -0
  233. package/dist/team/sdk-policy.js +186 -0
  234. package/dist/team/sdk-policy.js.map +1 -0
  235. package/dist/team/sdk-transport.d.ts +164 -0
  236. package/dist/team/sdk-transport.d.ts.map +1 -0
  237. package/dist/team/sdk-transport.js +266 -0
  238. package/dist/team/sdk-transport.js.map +1 -0
  239. package/dist/team/tmux-session.d.ts +7 -0
  240. package/dist/team/tmux-session.d.ts.map +1 -1
  241. package/dist/team/tmux-session.js +80 -18
  242. package/dist/team/tmux-session.js.map +1 -1
  243. package/dist/team/types.d.ts +2 -0
  244. package/dist/team/types.d.ts.map +1 -1
  245. package/dist/team/types.js.map +1 -1
  246. package/dist/tools/host-smoke-tool.d.ts +34 -0
  247. package/dist/tools/host-smoke-tool.d.ts.map +1 -0
  248. package/dist/tools/host-smoke-tool.js +133 -0
  249. package/dist/tools/host-smoke-tool.js.map +1 -0
  250. package/docs/CLAUDE.md +1 -1
  251. package/docs/DEVELOPERS.md +122 -0
  252. package/docs/HOOKS.md +34 -2
  253. package/docs/MIGRATION.md +194 -3
  254. package/docs/REFERENCE.md +95 -1
  255. package/docs/TOOLS.md +32 -1
  256. package/package.json +11 -2
  257. package/plugin.json +1 -1
  258. package/scripts/dev/repro-state-lock.mjs +1 -1
  259. package/scripts/lib/atomic-write.mjs +4 -2
  260. package/scripts/lib/state-lock.mjs +21 -12
  261. package/scripts/run.cjs +141 -18
  262. package/scripts/session-start.mjs +13 -0
  263. package/scripts/wiki-session-end.mjs +2 -1
  264. package/templates/hooks/lib/atomic-write.mjs +4 -2
@@ -0,0 +1,688 @@
1
+ /**
2
+ * `omg smoke copilot` — load-check the oh-my-copilot plugin inside the real
3
+ * GitHub Copilot CLI.
4
+ *
5
+ * Tier 0 makes no model call: manifest/version, generated hooks/agents drift,
6
+ * `plugin list`/`skill list` against `--plugin-dir`, and the standalone MCP
7
+ * server's tools/list. Tier 1 adds one cheap non-interactive session in a
8
+ * throwaway COPILOT_HOME and asserts on its persisted events, debug log and
9
+ * the `.omg/` state our hooks write. Tier 2 replaces that session with
10
+ * `@github/copilot-sdk` static checks and scripted scenarios
11
+ * (src/smoke/copilot-sdk-driver.ts).
12
+ */
13
+ import { spawn, spawnSync } from 'child_process';
14
+ import { randomUUID } from 'crypto';
15
+ import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from 'fs';
16
+ import { tmpdir } from 'os';
17
+ import { join, resolve, sep } from 'path';
18
+ import { buildHostBinarySpawn } from '../cli/tmux-utils.js';
19
+ import { getCopilotConfigDir } from '../utils/config-dir.js';
20
+ import { parseCopilotVersion, resolveCopilotBinary } from './copilot-binary.js';
21
+ import { EVIDENCE_MAX, evaluateAdapterErrors, evaluateEventsLog, evaluateHooks, evaluateMcpLoaded, evaluatePluginsLoaded, evaluateSessionExit, evaluateStateWritten, evaluateSubagent, excerpt, parseJsonl, REQUIRED_HOOK_TYPES, TOOL_HOOK_TYPES, } from './copilot-session-eval.js';
22
+ import { hasExited, killProcessTree, runAsync, useProcessGroup } from './process-utils.js';
23
+ import { DEFAULT_SCENARIO_TIMEOUT_MS, loadCopilotSdk, runSdkTier } from './copilot-sdk-driver.js';
24
+ import { runChainScenario } from './copilot-chain-scenario.js';
25
+ import { DEFAULT_SCENARIOS, failedTier2Checks, KNOWN_SCENARIOS, RUNTIME_MIN_MAX_CREDITS } from './copilot-sdk-scenarios.js';
26
+ import { buildSessionEnv, loginIdentity, resolveDefaultPluginRoot } from './copilot-session-env.js';
27
+ export { buildSessionEnv, isStrippedEnvKey, LOGIN_SHADOWING_TOKENS, loginIdentity, resolveDefaultPluginRoot, resolvePackageRoot, SESSION_SET_ENV, SMOKE_SET_ENV, STRIPPED_ENV_EXACT, STRIPPED_ENV_PREFIXES, } from './copilot-session-env.js';
28
+ export { ALL_SCENARIOS, DEFAULT_SCENARIOS, KNOWN_SCENARIOS, OPT_IN_SCENARIOS } from './copilot-sdk-scenarios.js';
29
+ /**
30
+ * No default model id: on Copilot CLI 1.0.91 every explicit `--model` tried
31
+ * (gpt-5-mini, gpt-6-luna, claude-sonnet-5) failed with `Model "<id>" from
32
+ * --model flag is not available` before any model call, while omitting the
33
+ * flag lets the CLI auto-pick (it chose mai-code-1.1-flash, a low-price flash
34
+ * model). Cost stays bounded by --max-ai-credits. Pass --model to pin one.
35
+ */
36
+ export const DEFAULT_SMOKE_MODEL = undefined;
37
+ export const DEFAULT_MAX_CREDITS = 30;
38
+ /** Copilot CLI rejects `--max-ai-credits` below this. */
39
+ export const MIN_MAX_CREDITS = RUNTIME_MIN_MAX_CREDITS;
40
+ export const DEFAULT_TIER0_TIMEOUT_MS = 60_000;
41
+ export const DEFAULT_TIER1_TIMEOUT_MS = 180_000;
42
+ export const MCP_LIST_TIMEOUT_MS = 10_000;
43
+ export const PLUGIN_NAME = 'oh-my-copilot';
44
+ export const DELEGATE_AGENT = 'oh-my-copilot:architect';
45
+ export const DEFAULT_SMOKE_PROMPT = 'Reply with exactly: SMOKE_OK. Do not use tools.';
46
+ export const DELEGATE_SMOKE_PROMPT = `Delegate one task to the ${DELEGATE_AGENT} agent: ask it to reply with one line. `
47
+ + 'Then reply with exactly: SMOKE_OK. Do not edit any files.';
48
+ /**
49
+ * Copy of the non-interactive permission flags from COPILOT_WORKER_BASE_FLAGS
50
+ * (src/team/model-contract.ts), minus the path/url grants the smoke does not
51
+ * need. Copied rather than imported so the smoke does not pull in team code.
52
+ */
53
+ const SMOKE_PERMISSION_FLAGS = ['--allow-all-tools', '--no-ask-user'];
54
+ /**
55
+ * Tier 1 session sandboxing (spellings verified against `copilot --help` and
56
+ * `copilot help permissions`, CLI 1.0.91; deny rules beat --allow-all-tools):
57
+ * - never let the model call our own `host_smoke` tool (server `t`) recursively;
58
+ * - no built-in MCP servers (github-mcp-server, githubiq) in the smoke;
59
+ * - the default prompt forbids tools, so also deny shell and file writes.
60
+ * `--delegate` keeps them, because the subagent hand-off is a tool call.
61
+ */
62
+ export function sessionSandboxFlags(delegate) {
63
+ return [
64
+ '--deny-tool', 't(host_smoke)',
65
+ '--disable-builtin-mcps',
66
+ ...(delegate ? [] : ['--deny-tool', 'shell', '--deny-tool', 'write']),
67
+ ];
68
+ }
69
+ const TIER0_COPILOT_IDS = ['copilot.binary', 'copilot.plugin_list', 'copilot.skill_list'];
70
+ function tier1Ids(delegate) {
71
+ return [
72
+ 'session.exit',
73
+ 'session.events',
74
+ ...REQUIRED_HOOK_TYPES.map((t) => `hooks.${t}`),
75
+ ...TOOL_HOOK_TYPES.map((t) => `hooks.${t}`),
76
+ 'hooks.adapter_errors',
77
+ 'plugins.loaded',
78
+ 'mcp.server_loaded',
79
+ 'state.written',
80
+ ...(delegate ? ['subagent.selected'] : []),
81
+ ];
82
+ }
83
+ function readJson(path) {
84
+ try {
85
+ return JSON.parse(readFileSync(path, 'utf-8'));
86
+ }
87
+ catch {
88
+ return null;
89
+ }
90
+ }
91
+ function countDirsWith(dir, file) {
92
+ try {
93
+ return readdirSync(dir).filter((name) => existsSync(join(dir, name, file))).sort();
94
+ }
95
+ catch {
96
+ return [];
97
+ }
98
+ }
99
+ function countFiles(dir, ext) {
100
+ try {
101
+ return readdirSync(dir).filter((name) => name.endsWith(ext)).length;
102
+ }
103
+ catch {
104
+ return -1;
105
+ }
106
+ }
107
+ /** Extract the first JSON array from CLI output that may carry warnings around it. */
108
+ export function extractJsonArray(text) {
109
+ const start = text.indexOf('[');
110
+ const end = text.lastIndexOf(']');
111
+ if (start === -1 || end < start)
112
+ return null;
113
+ try {
114
+ const parsed = JSON.parse(text.slice(start, end + 1));
115
+ return Array.isArray(parsed) ? parsed : null;
116
+ }
117
+ catch {
118
+ return null;
119
+ }
120
+ }
121
+ function makeTempDir(prefix) {
122
+ return mkdtempSync(join(tmpdir(), prefix));
123
+ }
124
+ function removeDir(path) {
125
+ if (!path)
126
+ return;
127
+ try {
128
+ rmSync(path, { recursive: true, force: true, maxRetries: 3, retryDelay: 200 });
129
+ }
130
+ catch { /* best effort */ }
131
+ }
132
+ // ---------------------------------------------------------------------------
133
+ // Tier 0
134
+ // ---------------------------------------------------------------------------
135
+ function checkManifest(root) {
136
+ const manifest = readJson(join(root, 'plugin.json'));
137
+ const pkg = readJson(join(root, 'package.json'));
138
+ const pluginVersion = typeof manifest?.version === 'string' ? manifest.version : null;
139
+ const packageVersion = typeof pkg?.version === 'string' ? pkg.version : null;
140
+ const problems = [];
141
+ if (!manifest)
142
+ problems.push('plugin.json missing or not JSON');
143
+ else {
144
+ if (manifest.name !== PLUGIN_NAME)
145
+ problems.push(`name is ${JSON.stringify(manifest.name)}, expected ${PLUGIN_NAME}`);
146
+ if (!packageVersion)
147
+ problems.push('package.json version missing');
148
+ else if (pluginVersion !== packageVersion)
149
+ problems.push(`version ${pluginVersion} != package.json ${packageVersion}`);
150
+ }
151
+ return {
152
+ check: {
153
+ id: 'plugin.manifest',
154
+ ok: problems.length === 0,
155
+ detail: problems.length === 0 ? `${PLUGIN_NAME}@${pluginVersion}` : problems.join('; '),
156
+ },
157
+ pluginVersion,
158
+ packageVersion,
159
+ };
160
+ }
161
+ function checkGeneratedHooks(ctx) {
162
+ const scripts = ['scripts/copilot/build-hooks.mjs', 'scripts/copilot/build-agents.mjs'];
163
+ const failures = [];
164
+ const evidence = [];
165
+ for (const script of scripts) {
166
+ const res = ctx.spawnSyncFn(process.execPath, [script, '--verify'], {
167
+ cwd: ctx.root,
168
+ env: ctx.env,
169
+ encoding: 'utf8',
170
+ timeout: ctx.timeoutMs,
171
+ windowsHide: true,
172
+ });
173
+ const out = `${res.stdout ?? ''}${res.stderr ?? ''}`.trim();
174
+ if (res.status !== 0) {
175
+ failures.push(`${script} --verify exit ${res.status ?? res.error?.message ?? 'null'}`);
176
+ evidence.push(out);
177
+ }
178
+ }
179
+ return {
180
+ id: 'plugin.hooks',
181
+ ok: failures.length === 0,
182
+ detail: failures.length === 0 ? 'build-hooks and build-agents --verify pass' : failures.join('; '),
183
+ ...(failures.length ? { evidence: excerpt(evidence.join('\n')) } : {}),
184
+ };
185
+ }
186
+ function checkAgents(root) {
187
+ const source = countFiles(join(root, 'agents'), '.md');
188
+ const generated = countFiles(join(root, 'copilot', 'agents'), '.md');
189
+ const ok = source > 0 && source === generated;
190
+ return {
191
+ id: 'copilot.agents',
192
+ ok,
193
+ detail: `copilot/agents ${generated} vs agents ${source} *.md`,
194
+ };
195
+ }
196
+ function runCopilotSync(ctx, bin, args, env, cwd) {
197
+ const plan = buildHostBinarySpawn(bin, args);
198
+ return ctx.spawnSyncFn(plan.command, plan.args, {
199
+ cwd,
200
+ env,
201
+ encoding: 'utf8',
202
+ timeout: ctx.timeoutMs,
203
+ windowsHide: true,
204
+ windowsVerbatimArguments: plan.windowsVerbatimArguments,
205
+ maxBuffer: 32 * 1024 * 1024,
206
+ });
207
+ }
208
+ function checkPluginList(ctx, bin, env, cwd, packageVersion) {
209
+ const res = runCopilotSync(ctx, bin, ['--plugin-dir', ctx.root, '--no-auto-update', 'plugin', 'list', '--json'], env, cwd);
210
+ const stdout = String(res.stdout ?? '');
211
+ const list = extractJsonArray(stdout);
212
+ if (res.status !== 0 || !list) {
213
+ return { id: 'copilot.plugin_list', ok: false, detail: `plugin list exit ${res.status}${list ? '' : ', no JSON array'}`, evidence: excerpt(`${stdout}${res.stderr ?? ''}`) };
214
+ }
215
+ // A bad --plugin-dir only warns and returns [] with exit 0: assert the exact entry.
216
+ const entry = list.find((p) => p.name === PLUGIN_NAME);
217
+ const ok = !!entry && entry.version === packageVersion && entry.source === 'external';
218
+ return {
219
+ id: 'copilot.plugin_list',
220
+ ok,
221
+ detail: entry
222
+ ? `${PLUGIN_NAME}@${String(entry.version)} source=${String(entry.source)}${ok ? '' : ` (expected ${packageVersion}, external)`}`
223
+ : `${PLUGIN_NAME} not listed (${list.length} plugin(s))`,
224
+ ...(ok ? {} : { evidence: excerpt(`${stdout}${res.stderr ?? ''}`) }),
225
+ };
226
+ }
227
+ /** Canonical path for comparisons: realpath (symlinked roots), case-folded on win32. */
228
+ function normalizePath(p) {
229
+ let out = resolve(p);
230
+ try {
231
+ out = realpathSync.native(out);
232
+ }
233
+ catch { /* missing path: keep the resolved form */ }
234
+ return process.platform === 'win32' ? out.toLowerCase() : out;
235
+ }
236
+ function checkSkillList(ctx, bin, env, cwd) {
237
+ const expected = countDirsWith(join(ctx.root, 'skills'), 'SKILL.md');
238
+ const res = runCopilotSync(ctx, bin, ['--plugin-dir', ctx.root, '--no-auto-update', 'skill', 'list', '--json'], env, cwd);
239
+ const stdout = String(res.stdout ?? '');
240
+ const list = extractJsonArray(stdout);
241
+ if (res.status !== 0 || !list) {
242
+ return { id: 'copilot.skill_list', ok: false, detail: `skill list exit ${res.status}${list ? '' : ', no JSON array'}`, evidence: excerpt(`${stdout}${res.stderr ?? ''}`) };
243
+ }
244
+ // Copilot also surfaces plugin `commands/*.md` as plugin skills, so count only
245
+ // plugin-sourced entries whose path is a skills/<dir>.
246
+ const skillsDir = normalizePath(join(ctx.root, 'skills')) + sep;
247
+ const pluginSkills = list.filter((s) => s.source === 'plugin');
248
+ const fromSkillsDir = new Set(pluginSkills
249
+ .filter((s) => typeof s.path === 'string' && normalizePath(s.path).startsWith(skillsDir))
250
+ .map((s) => normalizePath(s.path).slice(skillsDir.length).split(/[\\/]/)[0]));
251
+ const missing = expected.filter((d) => !fromSkillsDir.has(process.platform === 'win32' ? d.toLowerCase() : d));
252
+ const ok = expected.length > 0 && missing.length === 0 && fromSkillsDir.size === expected.length;
253
+ return {
254
+ id: 'copilot.skill_list',
255
+ ok,
256
+ detail: `${fromSkillsDir.size}/${expected.length} skills/*/SKILL.md loaded as source=plugin (${pluginSkills.length} plugin entries incl. commands)`,
257
+ ...(ok ? {} : { evidence: excerpt(missing.length ? `missing: ${missing.join(', ')}` : stdout) }),
258
+ };
259
+ }
260
+ async function defaultExpectedToolCount() {
261
+ const registry = await import('../mcp/tool-registry.js');
262
+ return registry.allTools.length;
263
+ }
264
+ async function checkMcpListTools(ctx) {
265
+ const id = 'mcp.list_tools';
266
+ const server = join(ctx.root, 'dist', 'mcp', 'standalone-server.js');
267
+ if (!existsSync(server))
268
+ return { id, ok: false, detail: `${server} missing; run npm run build` };
269
+ let expected;
270
+ try {
271
+ expected = await (ctx.deps.loadExpectedToolCount ?? defaultExpectedToolCount)();
272
+ }
273
+ catch (err) {
274
+ return { id, ok: false, detail: `could not load tool registry: ${err.message}` };
275
+ }
276
+ const env = { ...ctx.env };
277
+ delete env.OMC_DISABLE_TOOLS; // compare against the full registry
278
+ return new Promise((resolveCheck) => {
279
+ let settled = false;
280
+ let buffer = '';
281
+ let stderr = '';
282
+ const child = ctx.spawnFn(process.execPath, [server], {
283
+ cwd: ctx.root,
284
+ env,
285
+ stdio: ['pipe', 'pipe', 'pipe'],
286
+ windowsHide: true,
287
+ detached: useProcessGroup(),
288
+ });
289
+ const done = (check) => {
290
+ if (settled)
291
+ return;
292
+ settled = true;
293
+ clearTimeout(timer);
294
+ try {
295
+ child.stdin?.end();
296
+ }
297
+ catch { /* closed */ }
298
+ if (!hasExited(child))
299
+ killProcessTree(child.pid, ctx.spawnSyncFn);
300
+ resolveCheck(check);
301
+ };
302
+ const timer = setTimeout(() => done({ id, ok: false, detail: `no tools/list response within ${MCP_LIST_TIMEOUT_MS} ms`, evidence: excerpt(stderr) }), MCP_LIST_TIMEOUT_MS);
303
+ const send = (msg) => { try {
304
+ child.stdin?.write(`${JSON.stringify(msg)}\n`);
305
+ }
306
+ catch { /* closed */ } };
307
+ child.on('error', (err) => done({ id, ok: false, detail: `spawn failed: ${err.message}` }));
308
+ child.on('close', (code) => done({ id, ok: false, detail: `server exited (${code}) before tools/list`, evidence: excerpt(stderr) }));
309
+ child.stdin?.on('error', () => { });
310
+ child.stderr?.setEncoding('utf8');
311
+ child.stderr?.on('data', (c) => { if (stderr.length < 64_000)
312
+ stderr += c; });
313
+ child.stdout?.setEncoding('utf8');
314
+ child.stdout?.on('data', (chunk) => {
315
+ buffer += chunk;
316
+ let nl;
317
+ while ((nl = buffer.indexOf('\n')) !== -1) {
318
+ const line = buffer.slice(0, nl).trim();
319
+ buffer = buffer.slice(nl + 1);
320
+ if (!line)
321
+ continue;
322
+ let msg;
323
+ try {
324
+ msg = JSON.parse(line);
325
+ }
326
+ catch {
327
+ continue;
328
+ }
329
+ if (msg.id === 1) {
330
+ if (msg.error) {
331
+ done({ id, ok: false, detail: 'initialize failed', evidence: excerpt(JSON.stringify(msg.error)) });
332
+ return;
333
+ }
334
+ send({ jsonrpc: '2.0', method: 'notifications/initialized' });
335
+ send({ jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} });
336
+ }
337
+ else if (msg.id === 2) {
338
+ const tools = msg.result?.tools;
339
+ if (!Array.isArray(tools)) {
340
+ done({ id, ok: false, detail: 'tools/list returned no tools array', evidence: excerpt(line) });
341
+ return;
342
+ }
343
+ const ok = tools.length === expected;
344
+ done({ id, ok, detail: `tools/list returned ${tools.length}, registry allTools ${expected}${ok ? '' : ' (stale dist? run npm run build)'}` });
345
+ return;
346
+ }
347
+ }
348
+ });
349
+ send({
350
+ jsonrpc: '2.0',
351
+ id: 1,
352
+ method: 'initialize',
353
+ params: { protocolVersion: '2025-06-18', capabilities: {}, clientInfo: { name: 'omg-smoke', version: '1' } },
354
+ });
355
+ });
356
+ }
357
+ // ---------------------------------------------------------------------------
358
+ // Tier 1
359
+ // ---------------------------------------------------------------------------
360
+ function initProject(ctx, projectDir, env) {
361
+ const res = ctx.spawnSyncFn('git', ['init', '-q'], { cwd: projectDir, env, encoding: 'utf8', timeout: 30_000, windowsHide: true });
362
+ if (res.status !== 0 || !existsSync(join(projectDir, '.git')))
363
+ mkdirSync(join(projectDir, '.git'), { recursive: true });
364
+ }
365
+ function readText(path) {
366
+ try {
367
+ return readFileSync(path, 'utf-8');
368
+ }
369
+ catch {
370
+ return null;
371
+ }
372
+ }
373
+ /** All process logs under the log dir, newest last, joined; plus the newest path. */
374
+ function readDebugLogs(logDir) {
375
+ let files;
376
+ try {
377
+ files = readdirSync(logDir).filter((f) => f.endsWith('.log')).map((f) => join(logDir, f));
378
+ }
379
+ catch {
380
+ return { text: null };
381
+ }
382
+ if (files.length === 0)
383
+ return { text: null };
384
+ files.sort((a, b) => statSync(a).mtimeMs - statSync(b).mtimeMs);
385
+ return { text: files.map((f) => readText(f) ?? '').join('\n'), path: files[files.length - 1] };
386
+ }
387
+ function mcpServerNames(root) {
388
+ const mcp = readJson(join(root, '.mcp.json'));
389
+ const servers = mcp?.mcpServers;
390
+ return servers && typeof servers === 'object' ? Object.keys(servers) : [];
391
+ }
392
+ /**
393
+ * Run the live session. Registers its temp dir in `cleanup` and its paths in
394
+ * `artifacts` as soon as they exist, so the caller's `finally` removes them
395
+ * even when this throws midway.
396
+ */
397
+ async function runTier1(ctx, bin, opts, cleanup, artifacts) {
398
+ // One parent so --keep-home leaves home/ and project/ side by side.
399
+ const parent = makeTempDir('omg-smoke-');
400
+ cleanup.push(parent);
401
+ const home = join(parent, 'home');
402
+ artifacts.copilotHome = home;
403
+ const projectDir = join(parent, 'project');
404
+ mkdirSync(home);
405
+ mkdirSync(projectDir);
406
+ const logDir = join(home, 'logs');
407
+ const sessionId = (ctx.deps.randomUUID ?? randomUUID)();
408
+ const identity = loginIdentity(ctx.deps.userConfigDir ?? getCopilotConfigDir());
409
+ const env = buildSessionEnv(ctx.env, home, { session: true, hasLogin: identity.loggedInUsers !== undefined });
410
+ const config = { ...identity, trustedFolders: [projectDir] };
411
+ writeFileSync(join(home, 'config.json'), `${JSON.stringify(config, null, 2)}\n`);
412
+ initProject(ctx, projectDir, env);
413
+ const prompt = opts.prompt ?? (opts.delegate ? DELEGATE_SMOKE_PROMPT : DEFAULT_SMOKE_PROMPT);
414
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIER1_TIMEOUT_MS;
415
+ const args = [
416
+ '--plugin-dir', ctx.root,
417
+ '--session-id', sessionId,
418
+ '--output-format', 'json',
419
+ ...SMOKE_PERMISSION_FLAGS,
420
+ ...sessionSandboxFlags(!!opts.delegate),
421
+ '--no-auto-update',
422
+ '--log-level', 'debug',
423
+ '--log-dir', logDir,
424
+ ...((opts.model ?? DEFAULT_SMOKE_MODEL) ? ['--model', (opts.model ?? DEFAULT_SMOKE_MODEL)] : []),
425
+ '--max-ai-credits', String(opts.maxCredits ?? DEFAULT_MAX_CREDITS),
426
+ ];
427
+ // No -p: `-p <text>` would put the prompt on argv. Piped stdin is the
428
+ // CLI's other non-interactive entry (verified on 1.0.91).
429
+ const plan = buildHostBinarySpawn(bin, args);
430
+ const run = await runAsync(ctx.spawnFn, ctx.spawnSyncFn, plan.command, plan.args, {
431
+ cwd: projectDir,
432
+ env,
433
+ input: `${prompt}\n`,
434
+ timeoutMs,
435
+ windowsVerbatimArguments: plan.windowsVerbatimArguments,
436
+ });
437
+ const stdoutPath = join(home, 'smoke-stdout.jsonl');
438
+ try {
439
+ writeFileSync(stdoutPath, run.stdout);
440
+ }
441
+ catch { /* best effort */ }
442
+ const eventsPath = join(home, 'session-state', sessionId, 'events.jsonl');
443
+ const logs = readDebugLogs(logDir);
444
+ Object.assign(artifacts, { eventsLog: eventsPath, debugLog: logs.path, stdout: stdoutPath });
445
+ return evaluateTier1({
446
+ exitCode: run.code,
447
+ timedOut: run.timedOut,
448
+ error: run.error,
449
+ stdout: run.stdout,
450
+ stderr: run.stderr,
451
+ timeoutMs,
452
+ eventsPath,
453
+ eventsText: readText(eventsPath),
454
+ logText: logs.text,
455
+ projectDir,
456
+ mcpServerNames: mcpServerNames(ctx.root),
457
+ delegate: !!opts.delegate,
458
+ });
459
+ }
460
+ /** Evaluate captured Tier 1 artifacts (exported for fixture-driven tests). */
461
+ export function evaluateTier1(input) {
462
+ const checks = [];
463
+ checks.push(evaluateSessionExit(input));
464
+ const { check: eventsCheck, events } = evaluateEventsLog(input.eventsPath, input.eventsText);
465
+ checks.push(eventsCheck);
466
+ // Ephemeral events (e.g. MCP status changes) only reach stdout, so evaluate both streams.
467
+ const stdoutEvents = parseJsonl(input.stdout).events;
468
+ const allEvents = [...events, ...stdoutEvents.filter((e) => !e.id || !events.some((p) => p.id === e.id))];
469
+ checks.push(...evaluateHooks(events.length ? events : stdoutEvents));
470
+ checks.push(evaluateAdapterErrors([
471
+ { name: 'debug log', text: input.logText },
472
+ { name: 'session stderr', text: input.stderr },
473
+ { name: 'events.jsonl', text: input.eventsText },
474
+ { name: 'stdout', text: input.stdout },
475
+ ]));
476
+ checks.push(evaluatePluginsLoaded(input.logText));
477
+ checks.push(evaluateMcpLoaded(allEvents, input.logText, input.mcpServerNames.length ? input.mcpServerNames : ['t']));
478
+ checks.push(evaluateStateWritten(input.projectDir));
479
+ if (input.delegate)
480
+ checks.push(evaluateSubagent(allEvents, DELEGATE_AGENT));
481
+ return checks.map((c) => (c.evidence && c.evidence.length > EVIDENCE_MAX ? { ...c, evidence: excerpt(c.evidence) } : c));
482
+ }
483
+ // ---------------------------------------------------------------------------
484
+ // Entry point
485
+ // ---------------------------------------------------------------------------
486
+ export const LIVE_RUN_REFUSED_DETAIL = 'live run refused under vitest — set OMC_LIVE_SMOKE=1 (tier 1) / =2 (tier 2 scenarios)';
487
+ /**
488
+ * Hard no-billing guard: under a test runner (`VITEST` or `NODE_ENV=test` in
489
+ * the real process env) a run that would call a model (tier 1; tier 2 with
490
+ * any scenario, the default included) is refused before any subprocess or SDK
491
+ * client, unless `OMC_LIVE_SMOKE` names that tier or the test injected the
492
+ * seam that would otherwise bill (`deps.spawn` at tier 1, `deps.loadSdk` at
493
+ * tier 2). Returns the refusal detail, or null to proceed.
494
+ */
495
+ export function liveRunRefusal(tier, scenarios, deps, env = process.env) {
496
+ const underTest = !!env.VITEST || env.NODE_ENV === 'test';
497
+ if (!underTest)
498
+ return null;
499
+ const billable = tier === 1 || (tier === 2 && (scenarios ?? DEFAULT_SCENARIOS).length > 0);
500
+ if (!billable)
501
+ return null;
502
+ if (env.OMC_LIVE_SMOKE === String(tier))
503
+ return null;
504
+ if ((tier === 1 && deps.spawn) || (tier === 2 && deps.loadSdk))
505
+ return null;
506
+ return LIVE_RUN_REFUSED_DETAIL;
507
+ }
508
+ export async function runCopilotSmoke(input = {}) {
509
+ const started = Date.now();
510
+ const env = input.env ?? process.env;
511
+ const deps = input.deps ?? {};
512
+ const tier = input.tier === 1 || input.tier === 2 ? input.tier : 0;
513
+ const root = resolve(input.pluginRoot ?? resolveDefaultPluginRoot(env));
514
+ const opts = { ...input, pluginRoot: root, tier };
515
+ if (opts.prompt !== undefined && /\0/.test(opts.prompt))
516
+ throw new Error('smoke prompt must not contain NUL');
517
+ const unknown = (opts.scenarios ?? []).filter((s) => !KNOWN_SCENARIOS.includes(s));
518
+ if (unknown.length)
519
+ throw new Error(`unknown smoke scenario(s): ${unknown.join(', ')} (known: ${KNOWN_SCENARIOS.join(', ')})`);
520
+ const refusal = liveRunRefusal(tier, opts.scenarios, deps);
521
+ if (refusal) {
522
+ return {
523
+ ok: false,
524
+ tier,
525
+ pluginRoot: root,
526
+ pluginVersion: null,
527
+ copilot: { bin: null, version: null },
528
+ checks: [{ id: 'cli.guard', ok: false, detail: refusal }],
529
+ artifacts: {},
530
+ durationMs: Date.now() - started,
531
+ skipped: refusal,
532
+ };
533
+ }
534
+ const ctx = {
535
+ root,
536
+ env: { ...env },
537
+ spawnFn: deps.spawn ?? spawn,
538
+ spawnSyncFn: deps.spawnSync ?? spawnSync,
539
+ deps,
540
+ timeoutMs: tier === 0 ? (opts.timeoutMs ?? DEFAULT_TIER0_TIMEOUT_MS) : DEFAULT_TIER0_TIMEOUT_MS,
541
+ };
542
+ const checks = [];
543
+ const artifacts = {};
544
+ const cleanup = [];
545
+ try {
546
+ return await runChecks(ctx, opts, started, checks, artifacts, cleanup);
547
+ }
548
+ finally {
549
+ if (!opts.keepHome) {
550
+ for (const dir of cleanup)
551
+ removeDir(dir);
552
+ for (const key of Object.keys(artifacts))
553
+ delete artifacts[key];
554
+ }
555
+ }
556
+ }
557
+ async function runChecks(ctx, opts, started, checks, artifacts, cleanup) {
558
+ const { root, deps, env } = ctx;
559
+ const tier = opts.tier;
560
+ let skipped;
561
+ const manifest = checkManifest(root);
562
+ checks.push(manifest.check);
563
+ checks.push(checkGeneratedHooks(ctx));
564
+ const resolution = resolveCopilotBinary(opts.copilotBin, env, deps);
565
+ let version = null;
566
+ if (!resolution.bin) {
567
+ skipped = opts.copilotBin
568
+ ? `copilot binary not found: ${opts.copilotBin}`
569
+ : (deps.platform ?? process.platform) === 'win32'
570
+ ? 'copilot binary not found on PATH, COPILOT_CLI_PATH, or the WinGet package dir'
571
+ : 'copilot binary not found on PATH or COPILOT_CLI_PATH (npm i -g @github/copilot)';
572
+ checks.push({ id: 'copilot.binary', ok: false, detail: skipped });
573
+ for (const id of TIER0_COPILOT_IDS.slice(1))
574
+ checks.push({ id, ok: false, detail: 'skipped' });
575
+ }
576
+ else {
577
+ const bin = resolution.bin;
578
+ // cwd is tmpdir, not the plugin root: a missing --plugin-root must not
579
+ // surface as an ENOENT on the binary probe.
580
+ const res = runCopilotSync(ctx, bin, ['--version'], ctx.env, tmpdir());
581
+ version = parseCopilotVersion(String(res.stdout ?? ''));
582
+ const binaryOk = res.status === 0 && version !== null;
583
+ checks.push({
584
+ id: 'copilot.binary',
585
+ ok: binaryOk,
586
+ detail: binaryOk ? `${bin} (${resolution.source}) v${version}` : `${bin} --version exit ${res.status ?? res.error?.message}`,
587
+ ...(binaryOk ? {} : { evidence: excerpt(`${res.stdout ?? ''}${res.stderr ?? ''}`) }),
588
+ });
589
+ const listHome = makeTempDir('omg-smoke-list-');
590
+ cleanup.push(listHome);
591
+ artifacts.listHome = listHome;
592
+ const listEnv = buildSessionEnv(ctx.env, listHome);
593
+ checks.push(checkPluginList(ctx, bin, listEnv, listHome, manifest.packageVersion));
594
+ checks.push(checkSkillList(ctx, bin, listEnv, listHome));
595
+ }
596
+ checks.push(checkAgents(root));
597
+ checks.push(await checkMcpListTools(ctx));
598
+ if (tier === 1) {
599
+ if (!resolution.bin) {
600
+ for (const id of tier1Ids(!!opts.delegate))
601
+ checks.push({ id, ok: false, detail: 'skipped' });
602
+ }
603
+ else {
604
+ checks.push(...await runTier1(ctx, resolution.bin, opts, cleanup, artifacts));
605
+ }
606
+ }
607
+ let tier2;
608
+ if (tier === 2) {
609
+ const scenarios = [...new Set(opts.scenarios ?? DEFAULT_SCENARIOS)];
610
+ if (!resolution.bin) {
611
+ checks.push(...failedTier2Checks(scenarios, 'skipped'));
612
+ }
613
+ else {
614
+ tier2 = await runTier2(ctx, resolution.bin, version, manifest.packageVersion, opts, scenarios, cleanup, artifacts);
615
+ checks.push(...tier2.checks);
616
+ skipped ??= tier2.skipped;
617
+ }
618
+ }
619
+ return {
620
+ ok: checks.every((c) => c.ok),
621
+ tier,
622
+ pluginRoot: root,
623
+ pluginVersion: manifest.pluginVersion,
624
+ copilot: { bin: resolution.bin, version },
625
+ checks,
626
+ artifacts,
627
+ durationMs: Date.now() - started,
628
+ ...(skipped ? { skipped } : {}),
629
+ ...(tier2?.sdk ? { sdk: tier2.sdk } : {}),
630
+ ...(tier2?.cost ? { cost: tier2.cost } : {}),
631
+ };
632
+ }
633
+ // ---------------------------------------------------------------------------
634
+ // Tier 2
635
+ // ---------------------------------------------------------------------------
636
+ /**
637
+ * Same isolation as tier 1 (throwaway COPILOT_HOME with the copied login
638
+ * identity and trustedFolders, {@link buildSessionEnv} with
639
+ * OMC_HOOK_FAIL_CLOSED=1), driven through one SDK client. Env is per client,
640
+ * so OMC_GIT_GUARDRAILS=1 holds for every scenario; only the guardrail
641
+ * scenario is permitted a shell, so the others never reach the guardrail.
642
+ */
643
+ async function runTier2(ctx, bin, binVersion, packageVersion, opts, scenarios, cleanup, artifacts) {
644
+ const parent = makeTempDir('omg-smoke-sdk-');
645
+ cleanup.push(parent);
646
+ const home = join(parent, 'home');
647
+ const projectDir = join(parent, 'project');
648
+ mkdirSync(home);
649
+ artifacts.copilotHome = home;
650
+ const identity = loginIdentity(ctx.deps.userConfigDir ?? getCopilotConfigDir());
651
+ const env = buildSessionEnv(ctx.env, home, { session: true, hasLogin: identity.loggedInUsers !== undefined });
652
+ env.OMC_GIT_GUARDRAILS = '1';
653
+ writeFileSync(join(home, 'config.json'), `${JSON.stringify({ ...identity, trustedFolders: [projectDir] }, null, 2)}\n`);
654
+ let agentFiles = [];
655
+ try {
656
+ agentFiles = readdirSync(join(ctx.root, 'agents')).filter((f) => f.endsWith('.md')).sort();
657
+ }
658
+ catch { /* evaluated as 0 */ }
659
+ const result = await runSdkTier({
660
+ root: ctx.root,
661
+ bin,
662
+ binVersion,
663
+ packageVersion,
664
+ home,
665
+ projectDir,
666
+ remoteDir: join(parent, 'remote.git'),
667
+ env,
668
+ scenarios,
669
+ model: opts.model,
670
+ timeoutMs: opts.timeoutMs ?? DEFAULT_SCENARIO_TIMEOUT_MS,
671
+ maxCredits: opts.maxCredits ?? DEFAULT_MAX_CREDITS,
672
+ keepHome: !!opts.keepHome,
673
+ loadSdk: ctx.deps.loadSdk ?? (() => loadCopilotSdk(ctx.env, ctx.spawnSyncFn)),
674
+ loadExpectedToolCount: ctx.deps.loadExpectedToolCount ?? defaultExpectedToolCount,
675
+ spawnSync: ctx.spawnSyncFn,
676
+ skillDirs: countDirsWith(join(ctx.root, 'skills'), 'SKILL.md'),
677
+ agentFiles,
678
+ mcpServer: mcpServerNames(ctx.root)[0] ?? 't',
679
+ runChain: runChainScenario,
680
+ });
681
+ if (Object.keys(result.events).length)
682
+ artifacts.events = result.events;
683
+ const logs = readDebugLogs(join(home, 'logs'));
684
+ if (logs.path)
685
+ artifacts.debugLog = logs.path;
686
+ return result;
687
+ }
688
+ //# sourceMappingURL=copilot-smoke.js.map