oh-my-opencode 4.9.1 → 4.10.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 (220) hide show
  1. package/.agents/skills/opencode-qa/SKILL.md +1 -0
  2. package/.agents/skills/opencode-qa/scripts/lib/common.sh +39 -1
  3. package/.agents/skills/opencode-qa/scripts/lib/fake-openai-branches.mjs +39 -0
  4. package/.agents/skills/opencode-qa/scripts/lib/fake-openai-events.mjs +106 -0
  5. package/.agents/skills/opencode-qa/scripts/lib/fake-openai-server.mjs +117 -0
  6. package/.agents/skills/opencode-qa/scripts/serve-wake-split-probe.sh +716 -0
  7. package/.agents/skills/tech-debt-audit/SKILL.md +277 -0
  8. package/.agents/skills/work-with-pr-workspace/iteration-1/eval-5/with_skill/outputs/execution-plan.md +1 -1
  9. package/.opencode/skills/work-with-pr-workspace/iteration-1/eval-5/with_skill/outputs/execution-plan.md +1 -1
  10. package/bin/platform.js +5 -0
  11. package/bin/platform.test.ts +56 -0
  12. package/dist/agents/atlas/agent.d.ts +4 -3
  13. package/dist/agents/gpt-apply-patch-guard.d.ts +2 -2
  14. package/dist/agents/hephaestus/agent.d.ts +5 -0
  15. package/dist/agents/hephaestus/index.d.ts +1 -1
  16. package/dist/agents/metis.d.ts +1 -0
  17. package/dist/agents/prometheus/system-prompt.d.ts +1 -1
  18. package/dist/agents/sisyphus/kimi-k2-7.d.ts +17 -0
  19. package/dist/agents/sisyphus-junior/agent.d.ts +1 -1
  20. package/dist/agents/sisyphus-junior/kimi-k2-7.d.ts +11 -0
  21. package/dist/agents/types.d.ts +2 -2
  22. package/dist/cli/doctor/checks/codex-components.d.ts +13 -0
  23. package/dist/cli/doctor/checks/tui-plugin-config.d.ts +1 -0
  24. package/dist/cli/doctor/constants.d.ts +1 -1
  25. package/dist/cli/index.js +32329 -31437
  26. package/dist/cli/install-codex/codex-cleanup.d.ts +4 -0
  27. package/dist/cli/install-codex/install-codex-test-fixtures.d.ts +34 -0
  28. package/dist/cli/install-codex/link-cached-plugin-agents.d.ts +4 -0
  29. package/dist/cli/model-fallback.d.ts +1 -0
  30. package/dist/cli/provider-availability.d.ts +2 -0
  31. package/dist/cli-node/index.js +32329 -31437
  32. package/dist/config/schema/agent-overrides.d.ts +80 -16
  33. package/dist/config/schema/experimental.d.ts +1 -1
  34. package/dist/config/schema/hooks.d.ts +0 -1
  35. package/dist/config/schema/internal/permission.d.ts +5 -1
  36. package/dist/config/schema/oh-my-opencode-config.d.ts +76 -16
  37. package/dist/create-hooks.d.ts +0 -1
  38. package/dist/features/background-agent/index.d.ts +1 -1
  39. package/dist/features/background-agent/manager.d.ts +6 -0
  40. package/dist/features/background-agent/types.d.ts +2 -0
  41. package/dist/features/claude-code-plugin-loader/types.d.ts +3 -0
  42. package/dist/features/claude-code-session-state/state.d.ts +1 -0
  43. package/dist/features/skill-mcp-manager/manager.d.ts +11 -7
  44. package/dist/features/team-mode/team-mailbox/pending-delivery-recovery.d.ts +31 -0
  45. package/dist/features/team-mode/team-runtime/delete-team.d.ts +2 -1
  46. package/dist/features/team-mode/tools/lifecycle-inline-spec.d.ts +2 -2
  47. package/dist/features/tmux-subagent/stale-tmux-resource-sweeper.d.ts +12 -0
  48. package/dist/features/tool-metadata-store/store.d.ts +5 -0
  49. package/dist/hooks/anthropic-context-window-limit-recovery/storage/constants.d.ts +3 -0
  50. package/dist/hooks/{session-recovery → anthropic-context-window-limit-recovery}/storage/messages-reader.d.ts +1 -1
  51. package/dist/hooks/{session-recovery → anthropic-context-window-limit-recovery}/storage/part-content.d.ts +1 -1
  52. package/dist/hooks/{session-recovery → anthropic-context-window-limit-recovery}/storage/parts-reader.d.ts +1 -1
  53. package/dist/hooks/{session-recovery → anthropic-context-window-limit-recovery/storage}/types.d.ts +0 -13
  54. package/dist/hooks/auto-update-checker/checker/bundled-version.d.ts +1 -0
  55. package/dist/hooks/auto-update-checker/checker.d.ts +1 -0
  56. package/dist/hooks/auto-update-checker/constants.d.ts +3 -3
  57. package/dist/hooks/auto-update-checker/hook.d.ts +2 -1
  58. package/dist/hooks/claude-code-hooks/types.d.ts +4 -0
  59. package/dist/hooks/index.d.ts +0 -1
  60. package/dist/hooks/team-session-events/team-idle-wake-hint.d.ts +5 -0
  61. package/dist/index.js +6061 -3714
  62. package/dist/oh-my-opencode.schema.json +123 -18
  63. package/dist/plugin/build-team-idle-wake-hint-client.d.ts +2 -0
  64. package/dist/plugin/event-session-lifecycle.d.ts +0 -3
  65. package/dist/plugin/hooks/create-continuation-hooks.d.ts +0 -6
  66. package/dist/plugin/hooks/create-core-hooks.d.ts +0 -1
  67. package/dist/plugin/hooks/create-session-hooks.d.ts +1 -2
  68. package/dist/shared/command-executor/execute-hook-command.d.ts +7 -0
  69. package/dist/shared/internal-initiator-marker.d.ts +7 -0
  70. package/dist/shared/live-server-route.d.ts +24 -0
  71. package/dist/shared/plugin-identity.d.ts +2 -2
  72. package/dist/shared/prompt-async-gate/prompt-message-state.d.ts +1 -0
  73. package/dist/shared/tmux/tmux-utils/server-health.d.ts +2 -1
  74. package/dist/shared/tmux/tmux-utils/stale-attach-pane-sweep.d.ts +16 -0
  75. package/dist/shared/tmux/tmux-utils.d.ts +1 -0
  76. package/dist/testing/create-plugin-module.d.ts +4 -0
  77. package/dist/tools/background-task/clients.d.ts +2 -0
  78. package/dist/tools/background-task/full-session-format.d.ts +1 -0
  79. package/dist/tools/background-task/types.d.ts +1 -0
  80. package/dist/tools/delegate-task/sync-prompt-sender.d.ts +1 -1
  81. package/dist/tools/delegate-task/sync-session-lifecycle.d.ts +2 -1
  82. package/dist/tools/look-at/look-at-input-preparer.d.ts +6 -2
  83. package/dist/tools/look-at/look-at-prompt.d.ts +2 -1
  84. package/dist/tools/look-at/look-at-session-runner.d.ts +3 -4
  85. package/dist/tools/look-at/types.d.ts +2 -0
  86. package/dist/tools/session-manager/types.d.ts +1 -0
  87. package/dist/tools/skill-mcp/types.d.ts +1 -0
  88. package/package.json +14 -13
  89. package/packages/ast-grep-mcp/dist/cli.js +50 -17
  90. package/packages/lsp-daemon/dist/cli.js +8 -5
  91. package/packages/lsp-daemon/dist/index.js +8 -5
  92. package/packages/lsp-tools-mcp/dist/lsp/connection.js +1 -1
  93. package/packages/lsp-tools-mcp/dist/lsp/server-definitions.js +2 -2
  94. package/packages/lsp-tools-mcp/dist/lsp/transport.d.ts +10 -1
  95. package/packages/lsp-tools-mcp/dist/lsp/transport.js +6 -3
  96. package/packages/omo-codex/lazycodex-repository/.github/workflows/pr-source-guidance.yml +11 -12
  97. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  98. package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +2583 -0
  99. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +17 -0
  100. package/packages/omo-codex/plugin/components/bootstrap/manifests/ast-grep.json +22 -0
  101. package/packages/omo-codex/plugin/components/bootstrap/manifests/node.json +10 -0
  102. package/packages/omo-codex/plugin/components/bootstrap/package.json +20 -0
  103. package/packages/omo-codex/plugin/components/bootstrap/scripts/bootstrap.ps1 +310 -0
  104. package/packages/omo-codex/plugin/components/bootstrap/scripts/build.mjs +35 -0
  105. package/packages/omo-codex/plugin/components/bootstrap/scripts/generate-manifests.mjs +115 -0
  106. package/packages/omo-codex/plugin/components/bootstrap/src/cli.ts +153 -0
  107. package/packages/omo-codex/plugin/components/bootstrap/src/download.ts +212 -0
  108. package/packages/omo-codex/plugin/components/bootstrap/src/environment.ts +286 -0
  109. package/packages/omo-codex/plugin/components/bootstrap/src/hook.ts +108 -0
  110. package/packages/omo-codex/plugin/components/bootstrap/src/provision.ts +243 -0
  111. package/packages/omo-codex/plugin/components/bootstrap/src/setup.ts +294 -0
  112. package/packages/omo-codex/plugin/components/bootstrap/src/worker.ts +279 -0
  113. package/packages/omo-codex/plugin/components/bootstrap/test/download.test.ts +295 -0
  114. package/packages/omo-codex/plugin/components/bootstrap/test/environment.test.ts +375 -0
  115. package/packages/omo-codex/plugin/components/bootstrap/test/provision.test.ts +464 -0
  116. package/packages/omo-codex/plugin/components/bootstrap/tsconfig.json +25 -0
  117. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  118. package/packages/omo-codex/plugin/components/comment-checker/package.json +4 -4
  119. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  120. package/packages/omo-codex/plugin/components/git-bash/package.json +2 -2
  121. package/packages/omo-codex/plugin/components/lsp/dist/codex-hook-cli.js +6 -10
  122. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  123. package/packages/omo-codex/plugin/components/lsp/package.json +4 -4
  124. package/packages/omo-codex/plugin/components/lsp/scripts/build-lsp-tools.test.mjs +8 -3
  125. package/packages/omo-codex/plugin/components/lsp/src/codex-hook-cli.ts +5 -8
  126. package/packages/omo-codex/plugin/components/lsp/test/codex-hook-cli.test.ts +24 -1
  127. package/packages/omo-codex/plugin/components/rules/bundled-rules/windows-git-bash.md +3 -1
  128. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  129. package/packages/omo-codex/plugin/components/rules/package.json +4 -4
  130. package/packages/omo-codex/plugin/components/rules/test/windows-git-bash-bundled-rule.test.ts +35 -1
  131. package/packages/omo-codex/plugin/components/start-work-continuation/hooks/hooks.json +2 -2
  132. package/packages/omo-codex/plugin/components/start-work-continuation/package.json +4 -4
  133. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  134. package/packages/omo-codex/plugin/components/telemetry/package.json +4 -4
  135. package/packages/omo-codex/plugin/components/ultrawork/biome.json +1 -1
  136. package/packages/omo-codex/plugin/components/ultrawork/directive.md +155 -99
  137. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  138. package/packages/omo-codex/plugin/components/ultrawork/package.json +4 -4
  139. package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/SKILL.md +19 -51
  140. package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/references/full-workflow.md +46 -51
  141. package/packages/omo-codex/plugin/components/ultrawork/test/codex-hook.test.ts +19 -0
  142. package/packages/omo-codex/plugin/components/ultrawork/test/package-smoke.test.ts +0 -1
  143. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-commands.js +9 -1
  144. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-output.d.ts +1 -0
  145. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-output.js +18 -0
  146. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-crud.js +1 -3
  147. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +2 -2
  148. package/packages/omo-codex/plugin/components/ulw-loop/package.json +4 -4
  149. package/packages/omo-codex/plugin/components/ulw-loop/src/cli-commands.ts +6 -2
  150. package/packages/omo-codex/plugin/components/ulw-loop/src/cli-output.ts +19 -0
  151. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-crud.ts +1 -1
  152. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-commands.test.ts +6 -0
  153. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-complete-goals.test.ts +26 -1
  154. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-json-errors.test.ts +89 -0
  155. package/packages/omo-codex/plugin/hooks/hooks.json +27 -16
  156. package/packages/omo-codex/plugin/package-lock.json +193 -193
  157. package/packages/omo-codex/plugin/package.json +1 -1
  158. package/packages/omo-codex/plugin/scripts/auto-update-state.d.mts +20 -0
  159. package/packages/omo-codex/plugin/scripts/auto-update.mjs +28 -8
  160. package/packages/omo-codex/plugin/scripts/build-components.mjs +36 -5
  161. package/packages/omo-codex/plugin/scripts/install-flow.mjs +43 -0
  162. package/packages/omo-codex/plugin/skills/lcx-contribute-bug-fix/SKILL.md +79 -28
  163. package/packages/omo-codex/plugin/skills/lcx-contribute-bug-fix/agents/openai.yaml +2 -2
  164. package/packages/omo-codex/plugin/skills/lcx-report-bug/SKILL.md +7 -6
  165. package/packages/omo-codex/plugin/skills/lcx-report-bug/agents/openai.yaml +1 -1
  166. package/packages/omo-codex/plugin/skills/ulw-plan/SKILL.md +19 -51
  167. package/packages/omo-codex/plugin/skills/ulw-plan/references/full-workflow.md +46 -51
  168. package/packages/omo-codex/plugin/test/aggregate-manifest.test.mjs +1 -0
  169. package/packages/omo-codex/plugin/test/auto-update.test.mjs +145 -0
  170. package/packages/omo-codex/plugin/test/bootstrap-binlinks.test.mjs +250 -0
  171. package/packages/omo-codex/plugin/test/bootstrap-hooks.test.mjs +166 -0
  172. package/packages/omo-codex/plugin/test/bootstrap-orchestration.test.mjs +371 -0
  173. package/packages/omo-codex/plugin/test/bootstrap-ps-guard.test.mjs +134 -0
  174. package/packages/omo-codex/plugin/test/bootstrap-setup.test.mjs +249 -0
  175. package/packages/omo-codex/plugin/test/lcx-bug-skills.test.mjs +10 -1
  176. package/packages/omo-codex/plugin/test/ulw-plan-skill.test.mjs +46 -0
  177. package/packages/omo-codex/scripts/atomic-write.test.mjs +82 -0
  178. package/packages/omo-codex/scripts/install/agents.d.mts +18 -0
  179. package/packages/omo-codex/scripts/install/agents.mjs +78 -5
  180. package/packages/omo-codex/scripts/install/atomic-write.mjs +59 -0
  181. package/packages/omo-codex/scripts/install/bin-dir.d.mts +7 -0
  182. package/packages/omo-codex/scripts/install/bin-links.d.mts +18 -0
  183. package/packages/omo-codex/scripts/install/config.d.mts +35 -0
  184. package/packages/omo-codex/scripts/install/config.mjs +13 -3
  185. package/packages/omo-codex/scripts/install/git-bash-mcp-env.d.mts +5 -0
  186. package/packages/omo-codex/scripts/install/git-bash.d.mts +23 -0
  187. package/packages/omo-codex/scripts/install/hook-trust.d.mts +10 -0
  188. package/packages/omo-codex/scripts/install-agent-links.test.mjs +41 -0
  189. package/packages/omo-codex/scripts/install-local.mjs +3 -2
  190. package/packages/shared-skills/skills/lcx-contribute-bug-fix/SKILL.md +79 -28
  191. package/packages/shared-skills/skills/lcx-contribute-bug-fix/agents/openai.yaml +2 -2
  192. package/packages/shared-skills/skills/lcx-report-bug/SKILL.md +7 -6
  193. package/packages/shared-skills/skills/lcx-report-bug/agents/openai.yaml +1 -1
  194. package/dist/hooks/session-recovery/constants.d.ts +0 -4
  195. package/dist/hooks/session-recovery/detect-error-type.d.ts +0 -4
  196. package/dist/hooks/session-recovery/error-recovery.d.ts +0 -4
  197. package/dist/hooks/session-recovery/hook-types.d.ts +0 -22
  198. package/dist/hooks/session-recovery/hook.d.ts +0 -4
  199. package/dist/hooks/session-recovery/index.d.ts +0 -5
  200. package/dist/hooks/session-recovery/interrupted-idle-message-fetch-timeout.d.ts +0 -7
  201. package/dist/hooks/session-recovery/interrupted-tool-results.d.ts +0 -3
  202. package/dist/hooks/session-recovery/message-state.d.ts +0 -4
  203. package/dist/hooks/session-recovery/recover-thinking-block-order.d.ts +0 -5
  204. package/dist/hooks/session-recovery/recover-thinking-disabled-violation.d.ts +0 -5
  205. package/dist/hooks/session-recovery/recover-tool-result-missing.d.ts +0 -10
  206. package/dist/hooks/session-recovery/recover-unavailable-tool.d.ts +0 -5
  207. package/dist/hooks/session-recovery/resume.d.ts +0 -7
  208. package/dist/hooks/session-recovery/storage/latest-assistant-message.d.ts +0 -5
  209. package/dist/hooks/session-recovery/storage/orphan-thinking-search.d.ts +0 -2
  210. package/dist/hooks/session-recovery/storage/thinking-block-search.d.ts +0 -2
  211. package/dist/hooks/session-recovery/storage/thinking-prepend.d.ts +0 -33
  212. package/dist/hooks/session-recovery/storage/thinking-strip.d.ts +0 -11
  213. package/dist/hooks/session-recovery/storage.d.ts +0 -20
  214. package/dist/plugin/event-session-recovery.d.ts +0 -9
  215. package/dist/plugin/user-abort-interrupted-recovery-guard.d.ts +0 -6
  216. /package/dist/hooks/{session-recovery → anthropic-context-window-limit-recovery}/storage/empty-messages.d.ts +0 -0
  217. /package/dist/hooks/{session-recovery → anthropic-context-window-limit-recovery}/storage/empty-text.d.ts +0 -0
  218. /package/dist/hooks/{session-recovery → anthropic-context-window-limit-recovery}/storage/message-dir.d.ts +0 -0
  219. /package/dist/hooks/{session-recovery → anthropic-context-window-limit-recovery}/storage/part-id.d.ts +0 -0
  220. /package/dist/hooks/{session-recovery → anthropic-context-window-limit-recovery}/storage/text-part-injector.d.ts +0 -0
@@ -0,0 +1,166 @@
1
+ import assert from "node:assert/strict";
2
+ import { readdir, readFile, stat } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
4
+ import test from "node:test";
5
+
6
+ import { collectCommandHooks, readComponentHookManifests, readJson, root } from "./aggregate-plugin-fixture.mjs";
7
+
8
+ const PLUGIN_ROOT_TARGET_PATTERN = /\$\{PLUGIN_ROOT\}([^"']+)/g;
9
+ const SKIPPED_DIRECTORY_NAMES = new Set([".git", "node_modules"]);
10
+ const EXPECTED_BOOTSTRAP_COMMAND = 'node "${PLUGIN_ROOT}/components/bootstrap/dist/cli.js" hook session-start';
11
+ const EXPECTED_BOOTSTRAP_COMMAND_WINDOWS =
12
+ 'powershell -NoProfile -ExecutionPolicy Bypass -File "${PLUGIN_ROOT}\\components\\bootstrap\\scripts\\bootstrap.ps1"';
13
+
14
+ async function pathExists(absolutePath) {
15
+ try {
16
+ await stat(absolutePath);
17
+ return true;
18
+ } catch (error) {
19
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return false;
20
+ throw error;
21
+ }
22
+ }
23
+
24
+ async function findHooksManifestPaths(directory, results = []) {
25
+ const entries = await readdir(directory, { withFileTypes: true });
26
+ for (const entry of entries) {
27
+ if (SKIPPED_DIRECTORY_NAMES.has(entry.name)) continue;
28
+ const path = join(directory, entry.name);
29
+ if (entry.isDirectory()) {
30
+ await findHooksManifestPaths(path, results);
31
+ continue;
32
+ }
33
+ if (entry.name === "hooks.json") results.push(path);
34
+ }
35
+ return results;
36
+ }
37
+
38
+ function collectAsyncOptIns(value, manifestPath, offenders) {
39
+ if (Array.isArray(value)) {
40
+ for (const entry of value) collectAsyncOptIns(entry, manifestPath, offenders);
41
+ return offenders;
42
+ }
43
+ if (typeof value !== "object" || value === null) return offenders;
44
+ if (value.async === true) offenders.push(manifestPath);
45
+ for (const entry of Object.values(value)) collectAsyncOptIns(entry, manifestPath, offenders);
46
+ return offenders;
47
+ }
48
+
49
+ function collectPluginRootTargets(handler) {
50
+ const targets = [];
51
+ for (const commandText of [handler.command, handler.commandWindows]) {
52
+ if (typeof commandText !== "string") continue;
53
+ for (const match of commandText.matchAll(PLUGIN_ROOT_TARGET_PATTERN)) {
54
+ targets.push(match[1].split(/[\\/]/).filter((part) => part.length > 0));
55
+ }
56
+ }
57
+ return targets;
58
+ }
59
+
60
+ async function readHookManifestsWithRoots() {
61
+ const aggregate = { source: "hooks/hooks.json", hooks: await readJson("hooks/hooks.json"), roots: [root] };
62
+ const components = (await readComponentHookManifests()).map(({ source, hooks }) => ({
63
+ source,
64
+ hooks,
65
+ roots: [dirname(dirname(join(root, source))), root],
66
+ }));
67
+ return [aggregate, ...components];
68
+ }
69
+
70
+ function findBootstrapSessionStartHandlers(hooks) {
71
+ const groups = Array.isArray(hooks.hooks?.SessionStart) ? hooks.hooks.SessionStart : [];
72
+ const located = [];
73
+ for (const group of groups) {
74
+ for (const handler of group.hooks ?? []) {
75
+ if (typeof handler.command !== "string") continue;
76
+ if (!handler.command.includes("components/bootstrap/dist/cli.js")) continue;
77
+ located.push({ group, handler });
78
+ }
79
+ }
80
+ return located;
81
+ }
82
+
83
+ test("#given every hooks.json under the plugin #when handlers are inspected #then no entry opts into unsupported async execution", async () => {
84
+ // given
85
+ const manifestPaths = await findHooksManifestPaths(root);
86
+
87
+ // when
88
+ const offenders = [];
89
+ for (const manifestPath of manifestPaths) {
90
+ collectAsyncOptIns(JSON.parse(await readFile(manifestPath, "utf8")), manifestPath, offenders);
91
+ }
92
+
93
+ // then
94
+ assert(manifestPaths.length >= 2, "expected the aggregate and component hooks manifests to be discovered");
95
+ assert.deepEqual(offenders, [], `Codex skips async hooks silently; remove "async": true from: ${offenders.join(", ")}`);
96
+ });
97
+
98
+ test("#given aggregate and component hook manifests #when command targets are resolved #then every command and commandWindows target exists", async () => {
99
+ // given
100
+ const manifests = await readHookManifestsWithRoots();
101
+
102
+ // when
103
+ const missing = [];
104
+ for (const manifest of manifests) {
105
+ for (const { handler } of collectCommandHooks(manifest.hooks, manifest.source)) {
106
+ for (const targetParts of collectPluginRootTargets(handler)) {
107
+ const candidates = manifest.roots.map((rootPath) => join(rootPath, ...targetParts));
108
+ let found = false;
109
+ for (const candidate of candidates) {
110
+ if (await pathExists(candidate)) {
111
+ found = true;
112
+ break;
113
+ }
114
+ }
115
+ if (!found) missing.push(`${manifest.source}: ${targetParts.join("/")}`);
116
+ }
117
+ }
118
+ }
119
+
120
+ // then
121
+ assert.deepEqual(missing, []);
122
+ });
123
+
124
+ test("#given the bootstrap component #when its SessionStart registration is inspected #then aggregate and component entries declare both platform commands", async () => {
125
+ // given
126
+ const aggregateHooks = await readJson("hooks/hooks.json");
127
+ const componentHooks = await readJson("components/bootstrap/hooks/hooks.json");
128
+
129
+ // when
130
+ const aggregateEntries = findBootstrapSessionStartHandlers(aggregateHooks);
131
+ const componentEntries = findBootstrapSessionStartHandlers(componentHooks);
132
+
133
+ // then
134
+ for (const [label, entries] of [
135
+ ["aggregate", aggregateEntries],
136
+ ["component", componentEntries],
137
+ ]) {
138
+ assert.equal(entries.length, 1, `${label} hooks.json must register exactly one bootstrap SessionStart handler`);
139
+ const { group, handler } = entries[0];
140
+ assert.equal(group.matcher, undefined, `${label} bootstrap SessionStart entry must be matcher-less`);
141
+ assert.equal(handler.type, "command");
142
+ assert.equal(handler.command, EXPECTED_BOOTSTRAP_COMMAND);
143
+ assert.equal(handler.commandWindows, EXPECTED_BOOTSTRAP_COMMAND_WINDOWS);
144
+ assert.equal(typeof handler.timeout, "number");
145
+ assert(handler.timeout <= 60, `${label} bootstrap timeout must stay <= 60 seconds`);
146
+ assert.equal(typeof handler.statusMessage, "string");
147
+ assert.match(handler.statusMessage, /^LazyCodex\([^)]+\): .+$/);
148
+ }
149
+ });
150
+
151
+ test("#given the built bootstrap bundle #when its module references are inspected #then it depends on Node built-ins only", async () => {
152
+ // given
153
+ const bundlePath = join(root, "components", "bootstrap", "dist", "cli.js");
154
+ assert.equal(await pathExists(bundlePath), true, "components/bootstrap/dist/cli.js must exist after the plugin build");
155
+ const bundle = await readFile(bundlePath, "utf8");
156
+
157
+ // when
158
+ const externalSpecifiers = [
159
+ ...[...bundle.matchAll(/\brequire\(["']([^"']+)["']\)/g)].map((match) => match[1]),
160
+ ...[...bundle.matchAll(/\bfrom\s*["']([^"']+)["']/g)].map((match) => match[1]),
161
+ ].filter((specifier) => !specifier.startsWith("node:"));
162
+
163
+ // then
164
+ assert(bundle.length > 0, "bootstrap bundle must not be empty");
165
+ assert.deepEqual(externalSpecifiers, [], "bootstrap dist must bundle everything except node: built-ins");
166
+ });
@@ -0,0 +1,371 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import test from "node:test";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ const CLI_URL = new URL("../components/bootstrap/dist/cli.js", import.meta.url);
9
+ const CLI_PATH = fileURLToPath(CLI_URL);
10
+ const {
11
+ BOOTSTRAP_RESTART_NOTICE,
12
+ bootstrapLocks,
13
+ executeSessionStartHook,
14
+ parseWorkerFlags,
15
+ runBootstrapWorker,
16
+ runSessionStartHook,
17
+ } = await import(CLI_URL.href);
18
+
19
+ const PLUGIN_VERSION = "9.9.9";
20
+
21
+ async function withFixture(run, options = {}) {
22
+ const root = await mkdtemp(join(tmpdir(), "omo-bootstrap-orch-"));
23
+ try {
24
+ const pluginRoot = join(root, "plugin");
25
+ const pluginData = join(root, "plugin-data");
26
+ await mkdir(join(pluginRoot, ".codex-plugin"), { recursive: true });
27
+ await mkdir(pluginData, { recursive: true });
28
+ await writeFile(
29
+ join(pluginRoot, ".codex-plugin", "plugin.json"),
30
+ `${JSON.stringify({ name: "omo", version: options.version ?? PLUGIN_VERSION })}\n`,
31
+ );
32
+ await run({
33
+ env: { PLUGIN_DATA: pluginData, PLUGIN_ROOT: pluginRoot },
34
+ pluginData,
35
+ pluginRoot,
36
+ root,
37
+ });
38
+ } finally {
39
+ await rm(root, { force: true, recursive: true });
40
+ }
41
+ }
42
+
43
+ function hookRecorder() {
44
+ const notices = [];
45
+ const spawned = [];
46
+ return {
47
+ notices,
48
+ spawned,
49
+ spawnWorker: (invocation) => {
50
+ spawned.push(invocation);
51
+ },
52
+ writeNotice: (line) => {
53
+ notices.push(line);
54
+ },
55
+ };
56
+ }
57
+
58
+ async function readStateFile(pluginData) {
59
+ return JSON.parse(await readFile(join(pluginData, "bootstrap", "state.json"), "utf8"));
60
+ }
61
+
62
+ async function writeStateFile(pluginData, state) {
63
+ await mkdir(join(pluginData, "bootstrap"), { recursive: true });
64
+ await writeFile(join(pluginData, "bootstrap", "state.json"), `${JSON.stringify(state)}\n`);
65
+ }
66
+
67
+ test("#given a fresh PLUGIN_DATA #when the session-start hook runs #then it spawns the detached worker and emits the restart notice", async () => {
68
+ await withFixture(async (fixture) => {
69
+ const recorder = hookRecorder();
70
+
71
+ const result = await executeSessionStartHook({ env: fixture.env, ...recorder });
72
+
73
+ assert.equal(result.exitCode, 0);
74
+ assert.equal(result.action, "spawned");
75
+ assert.equal(recorder.spawned.length, 1);
76
+ assert.equal(recorder.spawned[0].command, process.execPath);
77
+ assert.deepEqual(recorder.spawned[0].args, [CLI_PATH, "worker"]);
78
+ assert.equal(recorder.spawned[0].env.PLUGIN_DATA, fixture.pluginData);
79
+ assert.equal(recorder.notices.length, 1);
80
+ assert.deepEqual(JSON.parse(recorder.notices[0]), {
81
+ hookSpecificOutput: {
82
+ hookEventName: "SessionStart",
83
+ additionalContext: BOOTSTRAP_RESTART_NOTICE,
84
+ },
85
+ });
86
+ });
87
+ });
88
+
89
+ test("#given a completed marker for the current version #when the hook runs #then it exits 0 silently without spawning", async () => {
90
+ await withFixture(async (fixture) => {
91
+ await writeStateFile(fixture.pluginData, { completedForVersion: PLUGIN_VERSION, lastAttemptAt: 1, lastStatus: "success" });
92
+ const recorder = hookRecorder();
93
+
94
+ const result = await executeSessionStartHook({ env: fixture.env, ...recorder });
95
+
96
+ assert.equal(result.exitCode, 0);
97
+ assert.equal(result.action, "skip-completed");
98
+ assert.deepEqual(recorder.spawned, []);
99
+ assert.deepEqual(recorder.notices, []);
100
+ });
101
+ });
102
+
103
+ test("#given a marker stamped for an older plugin version #when the hook runs #then the version bump re-spawns the worker", async () => {
104
+ await withFixture(async (fixture) => {
105
+ await writeStateFile(fixture.pluginData, { completedForVersion: "0.0.1", lastAttemptAt: 1, lastStatus: "success" });
106
+ const recorder = hookRecorder();
107
+
108
+ const result = await executeSessionStartHook({ env: fixture.env, ...recorder });
109
+
110
+ assert.equal(result.action, "spawned");
111
+ assert.equal(recorder.spawned.length, 1);
112
+ });
113
+ });
114
+
115
+ test("#given a fresh bootstrap lock held by another process #when the hook runs #then it exits 0 without spawning", async () => {
116
+ await withFixture(async (fixture) => {
117
+ await mkdir(join(fixture.pluginData, "bootstrap"), { recursive: true });
118
+ await writeFile(join(fixture.pluginData, "bootstrap", "state.json.lock"), `${Date.now()}\n`);
119
+ const recorder = hookRecorder();
120
+
121
+ const result = await executeSessionStartHook({ env: fixture.env, ...recorder });
122
+
123
+ assert.equal(result.exitCode, 0);
124
+ assert.equal(result.action, "skip-locked");
125
+ assert.deepEqual(recorder.spawned, []);
126
+ assert.deepEqual(recorder.notices, []);
127
+ });
128
+ });
129
+
130
+ test("#given a stale bootstrap lock #when the hook runs #then it spawns anyway", async () => {
131
+ await withFixture(async (fixture) => {
132
+ await mkdir(join(fixture.pluginData, "bootstrap"), { recursive: true });
133
+ await writeFile(join(fixture.pluginData, "bootstrap", "state.json.lock"), "1\n");
134
+ const staleNow = Date.now() + 11 * 60 * 1_000;
135
+ const recorder = hookRecorder();
136
+
137
+ const result = await executeSessionStartHook({ env: fixture.env, now: staleNow, ...recorder });
138
+
139
+ assert.equal(result.action, "spawned");
140
+ });
141
+ });
142
+
143
+ test("#given PLUGIN_DATA or PLUGIN_ROOT missing #when the hook runs #then it exits 0 silently", async () => {
144
+ await withFixture(async (fixture) => {
145
+ const recorder = hookRecorder();
146
+
147
+ const noData = await executeSessionStartHook({ env: { PLUGIN_ROOT: fixture.pluginRoot }, ...recorder });
148
+ const noRoot = await executeSessionStartHook({ env: { PLUGIN_DATA: fixture.pluginData }, ...recorder });
149
+
150
+ assert.equal(noData.exitCode, 0);
151
+ assert.equal(noData.action, "skip-missing-env");
152
+ assert.equal(noRoot.action, "skip-missing-env");
153
+ assert.deepEqual(recorder.spawned, []);
154
+ });
155
+ });
156
+
157
+ test("#given an unreadable plugin version manifest #when the hook runs #then it exits 0 without spawning", async () => {
158
+ await withFixture(async (fixture) => {
159
+ await rm(join(fixture.pluginRoot, ".codex-plugin", "plugin.json"));
160
+ const recorder = hookRecorder();
161
+
162
+ const result = await executeSessionStartHook({ env: fixture.env, ...recorder });
163
+
164
+ assert.equal(result.exitCode, 0);
165
+ assert.equal(result.action, "skip-version-unresolved");
166
+ assert.deepEqual(recorder.spawned, []);
167
+ });
168
+ });
169
+
170
+ test("#given the public hook runner #when every branch resolves #then it always returns exit code 0", async () => {
171
+ await withFixture(async (fixture) => {
172
+ const recorder = hookRecorder();
173
+
174
+ assert.equal(await runSessionStartHook({ env: fixture.env, ...recorder }), 0);
175
+ assert.equal(await runSessionStartHook({ env: {}, ...recorder }), 0);
176
+ });
177
+ });
178
+
179
+ test("#given a fresh state #when the worker runs #then steps execute and the versioned success marker is persisted", async () => {
180
+ await withFixture(async (fixture) => {
181
+ const contexts = [];
182
+ const now = 42_000;
183
+
184
+ const result = await runBootstrapWorker({
185
+ argv: ["--codex-home", join(fixture.root, "codex-home")],
186
+ env: fixture.env,
187
+ now,
188
+ steps: [
189
+ {
190
+ name: "setup",
191
+ run: async (context) => {
192
+ contexts.push(context);
193
+ return { degraded: [] };
194
+ },
195
+ },
196
+ ],
197
+ });
198
+
199
+ assert.equal(result.ran, true);
200
+ assert.equal(result.status, "success");
201
+ assert.equal(contexts.length, 1);
202
+ assert.equal(contexts[0].codexHome, join(fixture.root, "codex-home"));
203
+ assert.equal(contexts[0].pluginRoot, fixture.pluginRoot);
204
+ assert.equal(contexts[0].pluginData, fixture.pluginData);
205
+ assert.equal(contexts[0].pluginVersion, PLUGIN_VERSION);
206
+ assert.deepEqual(await readStateFile(fixture.pluginData), {
207
+ completedForVersion: PLUGIN_VERSION,
208
+ degraded: [],
209
+ lastAttemptAt: now,
210
+ lastStatus: "success",
211
+ });
212
+ });
213
+ });
214
+
215
+ test("#given a marker written between hook and worker start #when the worker re-checks under lock #then it honors the marker (TOCTOU)", async () => {
216
+ await withFixture(async (fixture) => {
217
+ await writeStateFile(fixture.pluginData, { completedForVersion: PLUGIN_VERSION, lastAttemptAt: 7, lastStatus: "success" });
218
+ let stepRuns = 0;
219
+
220
+ const result = await runBootstrapWorker({
221
+ env: fixture.env,
222
+ steps: [
223
+ {
224
+ name: "setup",
225
+ run: async () => {
226
+ stepRuns += 1;
227
+ return { degraded: [] };
228
+ },
229
+ },
230
+ ],
231
+ });
232
+
233
+ assert.deepEqual(result, { ran: false, reason: "already-completed" });
234
+ assert.equal(stepRuns, 0);
235
+ assert.deepEqual(await readStateFile(fixture.pluginData), {
236
+ completedForVersion: PLUGIN_VERSION,
237
+ lastAttemptAt: 7,
238
+ lastStatus: "success",
239
+ });
240
+ });
241
+ });
242
+
243
+ test("#given both locks already held #when the worker starts #then it is refused without writing state", async () => {
244
+ await withFixture(async (fixture) => {
245
+ const locks = await bootstrapLocks({ env: fixture.env, pluginData: fixture.pluginData });
246
+ assert.notEqual(locks, null);
247
+
248
+ const result = await runBootstrapWorker({ env: fixture.env, steps: [] });
249
+
250
+ assert.deepEqual(result, { ran: false, reason: "locked" });
251
+ await assert.rejects(() => stat(join(fixture.pluginData, "bootstrap", "state.json")));
252
+ await locks.release();
253
+ await assert.rejects(() => stat(locks.bootstrapLockPath));
254
+ });
255
+ });
256
+
257
+ test("#given throwing and degraded steps #when the worker runs #then degraded reasons are persisted and the worker still exits cleanly", async () => {
258
+ await withFixture(async (fixture) => {
259
+ const now = 84_000;
260
+
261
+ const result = await runBootstrapWorker({
262
+ env: fixture.env,
263
+ now,
264
+ steps: [
265
+ {
266
+ name: "setup",
267
+ run: async () => {
268
+ throw new Error("config.toml unwritable");
269
+ },
270
+ },
271
+ {
272
+ name: "sg",
273
+ run: async () => ({
274
+ degraded: [{ component: "ast_grep", hint: "npx lazycodex-ai doctor", reason: "checksum mismatch for darwin-arm64" }],
275
+ }),
276
+ },
277
+ ],
278
+ });
279
+
280
+ assert.equal(result.ran, true);
281
+ assert.equal(result.status, "degraded");
282
+ const state = await readStateFile(fixture.pluginData);
283
+ assert.equal(state.completedForVersion, PLUGIN_VERSION);
284
+ assert.equal(state.lastAttemptAt, now);
285
+ assert.equal(state.lastStatus, "degraded");
286
+ assert.equal(state.degraded.length, 2);
287
+ assert.deepEqual(state.degraded[0], {
288
+ component: "setup",
289
+ hint: "npx lazycodex-ai doctor",
290
+ reason: "config.toml unwritable",
291
+ });
292
+ assert.deepEqual(state.degraded[1], {
293
+ component: "ast_grep",
294
+ hint: "npx lazycodex-ai doctor",
295
+ reason: "checksum mismatch for darwin-arm64",
296
+ });
297
+ });
298
+ });
299
+
300
+ test("#given a completed marker #when the worker runs with --once #then the marker is bypassed and steps run again", async () => {
301
+ await withFixture(async (fixture) => {
302
+ await writeStateFile(fixture.pluginData, { completedForVersion: PLUGIN_VERSION, lastAttemptAt: 7, lastStatus: "success" });
303
+ let stepRuns = 0;
304
+
305
+ const result = await runBootstrapWorker({
306
+ argv: ["--once"],
307
+ env: fixture.env,
308
+ steps: [
309
+ {
310
+ name: "setup",
311
+ run: async () => {
312
+ stepRuns += 1;
313
+ return { degraded: [] };
314
+ },
315
+ },
316
+ ],
317
+ });
318
+
319
+ assert.equal(result.ran, true);
320
+ assert.equal(stepRuns, 1);
321
+ });
322
+ });
323
+
324
+ test("#given --only sg #when the worker runs #then only the matching step executes", async () => {
325
+ await withFixture(async (fixture) => {
326
+ const ran = [];
327
+ const step = (name) => ({
328
+ name,
329
+ run: async () => {
330
+ ran.push(name);
331
+ return { degraded: [] };
332
+ },
333
+ });
334
+
335
+ const result = await runBootstrapWorker({
336
+ argv: ["--only", "sg"],
337
+ env: fixture.env,
338
+ steps: [step("setup"), step("sg")],
339
+ });
340
+
341
+ assert.equal(result.ran, true);
342
+ assert.deepEqual(ran, ["sg"]);
343
+ });
344
+ });
345
+
346
+ test("#given worker CLI flags #when parsed #then --codex-home/--once/--only/--manifest-dir are recognized and unknown flags throw", () => {
347
+ assert.deepEqual(parseWorkerFlags(["--codex-home", "/x", "--once", "--only", "sg", "--manifest-dir", "/m"]), {
348
+ codexHome: "/x",
349
+ manifestDir: "/m",
350
+ once: true,
351
+ only: "sg",
352
+ });
353
+ assert.deepEqual(parseWorkerFlags([]), { once: false });
354
+ assert.throws(() => parseWorkerFlags(["--bogus"]), /--bogus/);
355
+ assert.throws(() => parseWorkerFlags(["--codex-home"]), /--codex-home/);
356
+ });
357
+
358
+ test("#given a missing plugin version in the worker #when it runs #then it records a degraded state instead of crashing", async () => {
359
+ await withFixture(async (fixture) => {
360
+ await rm(join(fixture.pluginRoot, ".codex-plugin", "plugin.json"));
361
+
362
+ const result = await runBootstrapWorker({ env: fixture.env, now: 5_000, steps: [] });
363
+
364
+ assert.equal(result.ran, true);
365
+ assert.equal(result.status, "degraded");
366
+ const state = await readStateFile(fixture.pluginData);
367
+ assert.equal(state.completedForVersion, undefined);
368
+ assert.equal(state.lastStatus, "degraded");
369
+ assert.match(state.degraded[0].reason, /plugin version/i);
370
+ });
371
+ });
@@ -0,0 +1,134 @@
1
+ import assert from "node:assert/strict";
2
+ import { readFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import test from "node:test";
5
+
6
+ import { collectCommandHooks, readJson, root } from "./aggregate-plugin-fixture.mjs";
7
+
8
+ const BOOTSTRAP_SCRIPT_RELATIVE_PATH = join("components", "bootstrap", "scripts", "bootstrap.ps1");
9
+ const HOOK_MANIFEST_SOURCES = ["hooks/hooks.json", "components/bootstrap/hooks/hooks.json"];
10
+ const BOOTSTRAP_PS1_COMMAND_WINDOWS_TARGET = "\\components\\bootstrap\\scripts\\bootstrap.ps1";
11
+ const TLS12_LINE_PATTERN =
12
+ /\[Net\.ServicePointManager\]::SecurityProtocol\s*=\s*\[Net\.ServicePointManager\]::SecurityProtocol\s+-bor\s+\[Net\.SecurityProtocolType\]::Tls12/;
13
+ const PWSH7_ONLY_TOKENS = [
14
+ { token: "??", pattern: /\?\?/, reason: "null-coalescing operator requires pwsh 7" },
15
+ { token: "?.", pattern: /\?\./, reason: "null-conditional member access requires pwsh 7" },
16
+ { token: "-Parallel", pattern: /-Parallel\b/i, reason: "ForEach-Object -Parallel requires pwsh 7" },
17
+ ];
18
+
19
+ async function readBootstrapScript() {
20
+ return readFile(join(root, BOOTSTRAP_SCRIPT_RELATIVE_PATH), "utf8");
21
+ }
22
+
23
+ function numberedLines(content) {
24
+ return content.split(/\r?\n/).map((text, index) => ({ line: index + 1, text }));
25
+ }
26
+
27
+ test("#given bootstrap.ps1 #when the download preamble is inspected #then it forces TLS 1.2 on ServicePointManager", async () => {
28
+ // given
29
+ const content = await readBootstrapScript();
30
+
31
+ // then
32
+ assert.match(
33
+ content,
34
+ TLS12_LINE_PATTERN,
35
+ "bootstrap.ps1 must OR Tls12 into [Net.ServicePointManager]::SecurityProtocol before any Invoke-WebRequest",
36
+ );
37
+ });
38
+
39
+ test("#given bootstrap.ps1 #when scanned for pwsh-7-only syntax #then Windows PowerShell 5.1 can parse every line", async () => {
40
+ // given
41
+ const content = await readBootstrapScript();
42
+
43
+ // when
44
+ const offenders = [];
45
+ for (const { token, pattern, reason } of PWSH7_ONLY_TOKENS) {
46
+ for (const { line, text } of numberedLines(content)) {
47
+ if (pattern.test(text)) {
48
+ offenders.push(`pwsh-7-only token \`${token}\` (${reason}) at bootstrap.ps1:${line}: ${text.trim()}`);
49
+ }
50
+ }
51
+ }
52
+
53
+ // then
54
+ assert.deepEqual(offenders, [], "bootstrap.ps1 runs under Windows PowerShell 5.1 (System32); pwsh-7-only syntax breaks it");
55
+ });
56
+
57
+ test("#given bootstrap.ps1 #when environment writes are scanned #then no Machine-scope target appears", async () => {
58
+ // given
59
+ const content = await readBootstrapScript();
60
+
61
+ // when
62
+ const offenders = numberedLines(content)
63
+ .filter(({ text }) => /\bmachine\b/i.test(text))
64
+ .map(({ line, text }) => `Machine-scope token at bootstrap.ps1:${line}: ${text.trim()}`);
65
+
66
+ // then
67
+ assert.deepEqual(offenders, [], "bootstrap.ps1 must only touch User-scope environment values (Machine scope needs elevation)");
68
+ });
69
+
70
+ test("#given bootstrap.ps1 #when execution policy mutations are scanned #then Set-ExecutionPolicy is never invoked", async () => {
71
+ // given
72
+ const content = await readBootstrapScript();
73
+
74
+ // when
75
+ const offenders = numberedLines(content)
76
+ .filter(({ text }) => /set-executionpolicy/i.test(text))
77
+ .map(({ line, text }) => `Set-ExecutionPolicy at bootstrap.ps1:${line}: ${text.trim()}`);
78
+
79
+ // then
80
+ assert.deepEqual(offenders, [], "the hook line already passes -ExecutionPolicy Bypass; the script must not change policy");
81
+ });
82
+
83
+ test("#given bootstrap.ps1 #when the final executable statement is located #then it is `exit 0` so sessions are never blocked", async () => {
84
+ // given
85
+ const content = await readBootstrapScript();
86
+
87
+ // when
88
+ const statements = content
89
+ .split(/\r?\n/)
90
+ .map((line) => line.trim())
91
+ .filter((line) => line.length > 0 && !line.startsWith("#"));
92
+
93
+ // then
94
+ assert.equal(statements.at(-1), "exit 0", "the terminal statement of bootstrap.ps1 must be `exit 0`");
95
+ });
96
+
97
+ test("#given bootstrap.ps1 #when every character is inspected #then the script is ASCII-only for cp949-safe consoles", async () => {
98
+ // given
99
+ const content = await readBootstrapScript();
100
+
101
+ // when
102
+ const offenders = [];
103
+ for (const { line, text } of numberedLines(content)) {
104
+ for (const character of text) {
105
+ const codePoint = character.codePointAt(0);
106
+ if (codePoint > 0x7f) {
107
+ offenders.push(`non-ASCII U+${codePoint.toString(16).toUpperCase().padStart(4, "0")} at bootstrap.ps1:${line}: ${text.trim()}`);
108
+ }
109
+ }
110
+ }
111
+
112
+ // then
113
+ assert.deepEqual(offenders, [], "bootstrap.ps1 output and source must be ASCII-only (cp949 consoles mangle anything else)");
114
+ });
115
+
116
+ test("#given the aggregate and bootstrap component hook manifests #when commandWindows entries are read #then the bootstrap SessionStart hook launches bootstrap.ps1", async () => {
117
+ for (const source of HOOK_MANIFEST_SOURCES) {
118
+ // given
119
+ const hooks = await readJson(source);
120
+
121
+ // when
122
+ const launchers = collectCommandHooks(hooks, source)
123
+ .map(({ handler }) => handler.commandWindows)
124
+ .filter((command) => typeof command === "string" && command.includes(BOOTSTRAP_PS1_COMMAND_WINDOWS_TARGET));
125
+
126
+ // then
127
+ assert.equal(launchers.length, 1, `${source} must register exactly one commandWindows pointing at bootstrap.ps1`);
128
+ assert.match(
129
+ launchers[0],
130
+ /^powershell -NoProfile -ExecutionPolicy Bypass -File /,
131
+ `${source} must launch bootstrap.ps1 with -NoProfile and -File`,
132
+ );
133
+ }
134
+ });