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
@@ -25,6 +25,7 @@ const AGGREGATE_EXPECTED_LABELS = new Map([
25
25
  ["hooks/hooks.json:PreToolUse:1:0", "Enforcing Unlimited Goal Budget"],
26
26
  ["hooks/hooks.json:PostToolUse:0:0", "Checking Comments"],
27
27
  ["hooks/hooks.json:PostToolUse:0:1", "Checking LSP Diagnostics"],
28
+ ["hooks/hooks.json:PostToolUse:0:2", "Checking CodeGraph Init Guidance"],
28
29
  ["hooks/hooks.json:PostToolUse:1:0", "Matching Project Rules"],
29
30
  ["hooks/hooks.json:PostCompact:0:0", "Resetting Git Bash MCP Reminder"],
30
31
  ["hooks/hooks.json:PostCompact:1:0", "Resetting Project Rule Cache"],
@@ -68,6 +69,19 @@ async function readPluginVersion() {
68
69
  return (await readJson(".codex-plugin/plugin.json")).version;
69
70
  }
70
71
 
72
+ async function readAggregateHookManifests() {
73
+ const manifest = await readJson(".codex-plugin/plugin.json");
74
+ const hookPaths = Array.isArray(manifest.hooks) ? manifest.hooks : [manifest.hooks];
75
+ return Promise.all(
76
+ hookPaths
77
+ .filter((hookPath) => typeof hookPath === "string")
78
+ .map(async (hookPath) => {
79
+ const source = hookPath.replace(/^\.\//, "");
80
+ return { source, version: await readPluginVersion(), hooks: await readJson(source) };
81
+ }),
82
+ );
83
+ }
84
+
71
85
  async function readComponentVersion(componentName) {
72
86
  return (await readJson(join("components", componentName, "package.json"))).version;
73
87
  }
@@ -114,7 +128,7 @@ function collectCommandHooks(hooks, source, version) {
114
128
  return commandHooks;
115
129
  }
116
130
 
117
- test("#given hook status label #when formatting #then prefixes LazyCodex with current version", async () => {
131
+ test("#given hook status label #when formatting #then prefixes OmO display namespace", async () => {
118
132
  // given
119
133
  const version = (await readRepoJson("package.json")).version;
120
134
  const label = "Checking Comments";
@@ -123,10 +137,10 @@ test("#given hook status label #when formatting #then prefixes LazyCodex with cu
123
137
  const message = formatLazyCodexHookStatusMessage(version, label);
124
138
 
125
139
  // then
126
- assert.equal(message, `LazyCodex(${version}): Checking Comments`);
140
+ assert.equal(message, "(OmO) Checking Comments");
127
141
  });
128
142
 
129
- test("#given hook status label with blank version #when formatting #then prefixes LazyCodex with local version", () => {
143
+ test("#given hook status label with blank version #when formatting #then still prefixes OmO display namespace", () => {
130
144
  // given
131
145
  const version = " ";
132
146
  const label = "Checking Comments";
@@ -135,7 +149,7 @@ test("#given hook status label with blank version #when formatting #then prefixe
135
149
  const message = formatLazyCodexHookStatusMessage(version, label);
136
150
 
137
151
  // then
138
- assert.equal(message, "LazyCodex(local): Checking Comments");
152
+ assert.equal(message, "(OmO) Checking Comments");
139
153
  });
140
154
 
141
155
  test("#given loose legacy status label #when normalizing #then removes OMO wording and title-cases label", async () => {
@@ -149,7 +163,7 @@ test("#given loose legacy status label #when normalizing #then removes OMO wordi
149
163
 
150
164
  // then
151
165
  assert.equal(normalized, "Checking Comments");
152
- assert.equal(message, `LazyCodex(${version}): Checking Comments`);
166
+ assert.equal(message, "(OmO) Checking Comments");
153
167
  });
154
168
 
155
169
  test("#given LazyCodex appears inside hook label #when normalizing #then product casing is preserved", async () => {
@@ -163,7 +177,7 @@ test("#given LazyCodex appears inside hook label #when normalizing #then product
163
177
 
164
178
  // then
165
179
  assert.equal(normalized, "Verifying LazyCodex Executor Evidence");
166
- assert.equal(message, `LazyCodex(${version}): Verifying LazyCodex Executor Evidence`);
180
+ assert.equal(message, "(OmO) Verifying LazyCodex Executor Evidence");
167
181
  });
168
182
 
169
183
  test("#given MCP appears inside hook label #when normalizing #then protocol casing is preserved", () => {
@@ -176,31 +190,29 @@ test("#given MCP appears inside hook label #when normalizing #then protocol casi
176
190
 
177
191
  // then
178
192
  assert.equal(normalized, "Recommending Git Bash MCP");
179
- assert.equal(message, "LazyCodex(4.10.0): Recommending Git Bash MCP");
193
+ assert.equal(message, "(OmO) Recommending Git Bash MCP");
180
194
  });
181
- test("#given aggregate comment-checker hook #when status is inspected #then it uses LazyCodex comments label", async () => {
195
+ test("#given aggregate comment-checker hook #when status is inspected #then it uses OmO comments label", async () => {
182
196
  // given
183
- const aggregateVersion = await readPluginVersion();
184
- const aggregateHooks = await readJson("hooks/hooks.json");
197
+ const aggregateManifests = await readAggregateHookManifests();
185
198
 
186
199
  // when
187
- const hooks = collectCommandHooks(aggregateHooks, "hooks/hooks.json", aggregateVersion);
188
- const commentCheckerHook = hooks.find((hook) => hook.id === "hooks/hooks.json:PostToolUse:0:0");
200
+ const hooks = aggregateManifests.flatMap((manifest) => collectCommandHooks(manifest.hooks, manifest.source, manifest.version));
201
+ const commentCheckerHook = hooks.find((hook) => hook.command.includes("components/comment-checker/dist/cli.js"));
189
202
 
190
203
  // then
191
- assert.equal(commentCheckerHook?.statusMessage, formatLazyCodexHookStatusMessage(aggregateVersion, "Checking Comments"));
192
- assert.doesNotMatch(JSON.stringify(aggregateHooks), /checking\s+OMO\s+comments/i);
204
+ assert.equal(commentCheckerHook?.statusMessage, formatLazyCodexHookStatusMessage("", "Checking Comments"));
205
+ assert.doesNotMatch(JSON.stringify(aggregateManifests), /checking\s+OMO\s+comments/i);
193
206
  });
194
207
 
195
- test("#given aggregate and component hooks #when status messages are inspected #then all use the LazyCodex formatter", async () => {
208
+ test("#given aggregate and component hooks #when status messages are inspected #then all use the OmO formatter", async () => {
196
209
  // given
197
- const aggregateVersion = await readPluginVersion();
198
- const aggregateHooks = await readJson("hooks/hooks.json");
210
+ const aggregateManifests = await readAggregateHookManifests();
199
211
  const componentManifests = await readComponentHookManifests();
200
212
 
201
213
  // when
202
214
  const commandHooks = [
203
- ...collectCommandHooks(aggregateHooks, "hooks/hooks.json", aggregateVersion),
215
+ ...aggregateManifests.flatMap((manifest) => collectCommandHooks(manifest.hooks, manifest.source, manifest.version)),
204
216
  ...componentManifests.flatMap((manifest) => collectCommandHooks(manifest.hooks, manifest.source, manifest.version)),
205
217
  ];
206
218
  const expectedLabels = new Map([...AGGREGATE_EXPECTED_LABELS, ...COMPONENT_EXPECTED_LABELS]);
@@ -219,6 +231,6 @@ test("#given aggregate and component hooks #when status messages are inspected #
219
231
  const actualLabels = new Set(commandHooks.map((hook) => parseLazyCodexHookStatusMessage(hook.statusMessage)?.label));
220
232
  assert.deepEqual([...expectedLabels.values()].filter((label) => !actualLabels.has(label)), []);
221
233
  for (const hook of commandHooks) {
222
- assert.doesNotMatch(hook.statusMessage, /\bOMO\b/i);
234
+ assert.match(hook.statusMessage, /^\(OmO\) /);
223
235
  }
224
236
  });
@@ -20,7 +20,7 @@ test("#given synced lcx-report-bug skill #when inspected #then it files LazyCode
20
20
  assert.match(skill, /gh pr create --repo openai\/codex/);
21
21
  assert.doesNotMatch(skill, /gh pr create --repo "\$TARGET_REPO"/);
22
22
  assert.doesNotMatch(skill, /gh pr create --repo code-yeongyu\/lazycodex/);
23
- assert.match(interfaceMetadata, /display_name: "lcx-report-bug \(omo\)"/);
23
+ assert.match(interfaceMetadata, /display_name: "\(OmO\) lcx-report-bug"/);
24
24
  assert.match(interfaceMetadata, /- "lazycodex bug"/);
25
25
  assert.match(interfaceMetadata, /- "openai codex bug"/);
26
26
  });
@@ -40,7 +40,7 @@ test("#given synced lcx-contribute-bug-fix skill #when inspected #then it delive
40
40
  assert.match(skill, /gh pr create --repo openai\/codex/);
41
41
  assert.doesNotMatch(skill, /gh pr create --repo "\$TARGET_REPO"/);
42
42
  assert.doesNotMatch(skill, /gh pr create --repo code-yeongyu\/lazycodex/);
43
- assert.match(interfaceMetadata, /display_name: "lcx-contribute-bug-fix \(omo\)"/);
43
+ assert.match(interfaceMetadata, /display_name: "\(OmO\) lcx-contribute-bug-fix"/);
44
44
  assert.match(interfaceMetadata, /- "contribute a bug fix"/);
45
45
  assert.match(interfaceMetadata, /- "fix bug pr"/);
46
46
  });
@@ -55,7 +55,7 @@ test("#given synced lcx-doctor skill #when inspected #then it diagnoses installs
55
55
 
56
56
  // then
57
57
  assert.match(skill, /^---\r?\nname: lcx-doctor\r?\n/m);
58
- assert.match(interfaceMetadata, /display_name: "lcx-doctor \(omo\)"/);
58
+ assert.match(interfaceMetadata, /display_name: "\(OmO\) lcx-doctor"/);
59
59
  assert.match(interfaceMetadata, /- "lazycodex doctor"/);
60
60
  assert.match(interfaceMetadata, /- "lazycodex health check"/);
61
61
  });
@@ -88,10 +88,10 @@ test("#given source package versions and component without hooks #when hook stat
88
88
  const aggregateHooks = await readJson(join(root, "hooks", "hooks.json"));
89
89
  const componentHooks = await readJson(join(root, "components", "comment-checker", "hooks", "hooks.json"));
90
90
  const lspHooks = await readJson(join(root, "components", "lsp", "hooks", "hooks.json"));
91
- assert.equal(aggregateHooks.hooks.PostToolUse[0].hooks[0].statusMessage, "LazyCodex(0.1.0): Checking Comments");
92
- assert.equal(aggregateHooks.hooks.PostToolUse[0].hooks[1].statusMessage, "LazyCodex(0.1.0): Checking LSP Diagnostics");
93
- assert.equal(componentHooks.hooks.PostToolUse[0].hooks[0].statusMessage, "LazyCodex(0.1.1): Checking Comments");
94
- assert.equal(lspHooks.hooks.PostToolUse[0].hooks[0].statusMessage, "LazyCodex(0.2.0): Checking LSP Diagnostics");
91
+ assert.equal(aggregateHooks.hooks.PostToolUse[0].hooks[0].statusMessage, "(OmO) Checking Comments");
92
+ assert.equal(aggregateHooks.hooks.PostToolUse[0].hooks[1].statusMessage, "(OmO) Checking LSP Diagnostics");
93
+ assert.equal(componentHooks.hooks.PostToolUse[0].hooks[0].statusMessage, "(OmO) Checking Comments");
94
+ assert.equal(lspHooks.hooks.PostToolUse[0].hooks[0].statusMessage, "(OmO) Checking LSP Diagnostics");
95
95
  });
96
96
 
97
97
  test("#given release version override #when hook status messages sync #then aggregate hooks use release version", async () => {
@@ -139,6 +139,6 @@ test("#given release version override #when hook status messages sync #then aggr
139
139
  // then
140
140
  const aggregateHooks = await readJson(join(root, "hooks", "hooks.json"));
141
141
  const componentHooks = await readJson(join(root, "components", "comment-checker", "hooks", "hooks.json"));
142
- assert.equal(aggregateHooks.hooks.PostToolUse[0].hooks[0].statusMessage, "LazyCodex(4.8.0): Checking Comments");
143
- assert.equal(componentHooks.hooks.PostToolUse[0].hooks[0].statusMessage, "LazyCodex(4.8.0): Checking Comments");
142
+ assert.equal(aggregateHooks.hooks.PostToolUse[0].hooks[0].statusMessage, "(OmO) Checking Comments");
143
+ assert.equal(componentHooks.hooks.PostToolUse[0].hooks[0].statusMessage, "(OmO) Checking Comments");
144
144
  });
@@ -22,6 +22,7 @@ export const expectedSkills = [
22
22
  "review-work",
23
23
  "rules",
24
24
  "start-work",
25
+ "teammode",
25
26
  "ultraresearch",
26
27
  "ulw-loop",
27
28
  "ulw-plan",
@@ -32,6 +33,7 @@ export const componentSkillSources = [
32
33
  ["comment-checker", "components/comment-checker/skills/comment-checker"],
33
34
  ["lsp", "components/lsp/skills/lsp"],
34
35
  ["rules", "components/rules/skills/rules"],
36
+ ["teammode", "components/teammode/skills/teammode"],
35
37
  ["ulw-loop", "components/ulw-loop/skills/ulw-loop"],
36
38
  ["ulw-plan", "components/ultrawork/skills/ulw-plan"],
37
39
  ];
@@ -17,6 +17,7 @@ import {
17
17
  const root = dirname(dirname(fileURLToPath(import.meta.url)));
18
18
  const repoRoot = join(root, "..", "..", "..");
19
19
  const opencodeOnlyToolPattern = /\b(?:call_omo_agent|background_output|team_[a-z_]+|task)\s*\(/;
20
+ const generatedSkillMetadataFiles = new Set(["agents/openai.yaml"]);
20
21
 
21
22
  async function readPackagedSkillFile(...segments) {
22
23
  const path = join(root, "skills", ...segments);
@@ -24,6 +25,10 @@ async function readPackagedSkillFile(...segments) {
24
25
  return { path, content };
25
26
  }
26
27
 
28
+ function excludeGeneratedSkillMetadata(files) {
29
+ return files.filter((file) => !generatedSkillMetadataFiles.has(file.replaceAll("\\", "/")));
30
+ }
31
+
27
32
  test("#given synced aggregate Codex skills #when inspected #then component and shared skills are present", async () => {
28
33
  // given
29
34
  const skillsRoot = join(root, "skills");
@@ -121,8 +126,8 @@ test("#given component skill sources #when aggregate Codex component skills are
121
126
  for (const [skillName, sourcePath] of componentSkillSources) {
122
127
  const sourceDir = join(root, sourcePath);
123
128
  const aggregateDir = join(aggregateSkillsRoot, skillName);
124
- const sourceFiles = await listSkillFiles(sourceDir);
125
- const aggregateFiles = await listSkillFiles(aggregateDir);
129
+ const sourceFiles = excludeGeneratedSkillMetadata(await listSkillFiles(sourceDir));
130
+ const aggregateFiles = excludeGeneratedSkillMetadata(await listSkillFiles(aggregateDir));
126
131
  assert.deepEqual(aggregateFiles, sourceFiles, `${skillName} resource set drifted from its component skill source`);
127
132
  for (const relativePath of sourceFiles) {
128
133
  const sourceContent = await readFile(join(sourceDir, relativePath), "utf8");
@@ -146,7 +151,7 @@ test("#given synced ulw-loop skill #when Codex hint metadata is inspected #then
146
151
 
147
152
  // then
148
153
  assert.match(skill, /^---\r?\nname: ulw-loop\r?\n/m);
149
- assert.match(interfaceMetadata, /display_name: "ulw-loop \(omo\)"/);
154
+ assert.match(interfaceMetadata, /display_name: "\(OmO\) ulw-loop"/);
150
155
  assert.doesNotMatch(interfaceMetadata, /ulw-loop \/ ulw-loop/);
151
156
  assert.match(interfaceMetadata, /short_description: "Goal-like ultrawork loop for systematic decomposition"/);
152
157
  assert.match(interfaceMetadata, /default_prompt: "Use \$ulw-loop/);
@@ -179,7 +184,7 @@ test("#given synced git-master skill #when inspected #then commits and git histo
179
184
  assert.match(skill, /Choose the Git tool by the question/);
180
185
  assert.match(skill, /git log -S "text"/);
181
186
  assert.match(skill, /git blame -L start,end -- file/);
182
- assert.match(interfaceMetadata, /display_name: "git-master \(omo\)"/);
187
+ assert.match(interfaceMetadata, /display_name: "\(OmO\) git-master"/);
183
188
  assert.match(interfaceMetadata, /- "git commit"/);
184
189
  assert.match(interfaceMetadata, /- "history search"/);
185
190
  });
@@ -0,0 +1,88 @@
1
+ import assert from "node:assert/strict";
2
+ import { spawnSync } from "node:child_process";
3
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+
7
+ import { root } from "./aggregate-plugin-fixture.mjs";
8
+
9
+ const teamScript = join(root, "components", "teammode", "skills", "teammode", "scripts", "team.mjs");
10
+
11
+ export function createTeamRoot(prefix) {
12
+ return mkdtempSync(join(tmpdir(), prefix));
13
+ }
14
+
15
+ export function cleanupTeamRoot(tempRoot) {
16
+ rmSync(tempRoot, { recursive: true, force: true });
17
+ }
18
+
19
+ export function teamDir(tempRoot, sessionId) {
20
+ return join(tempRoot, ".omo", "teams", sessionId);
21
+ }
22
+
23
+ export function teamJsonPath(tempRoot, sessionId) {
24
+ return join(teamDir(tempRoot, sessionId), "team.json");
25
+ }
26
+
27
+ export function readTeamJson(tempRoot, sessionId) {
28
+ return JSON.parse(readFileSync(teamJsonPath(tempRoot, sessionId), "utf8"));
29
+ }
30
+
31
+ export function runTeam(cwd, ...args) {
32
+ const result = runTeamRaw(cwd, ...args);
33
+ assert.equal(result.status, 0, `team.mjs ${args.join(" ")} failed: ${result.stderr}`);
34
+ return result;
35
+ }
36
+
37
+ export function runTeamRaw(cwd, ...args) {
38
+ return spawnSync(process.execPath, [teamScript, ...args], {
39
+ cwd,
40
+ encoding: "utf8",
41
+ timeout: 10_000,
42
+ });
43
+ }
44
+
45
+ export function symlinkOrSkip(t, target, path, type) {
46
+ try {
47
+ symlinkSync(target, path, type);
48
+ return true;
49
+ } catch (error) {
50
+ if (isUnavailableSymlinkError(error)) {
51
+ t.skip(`symlink unavailable on this filesystem: ${error.code}`);
52
+ return false;
53
+ }
54
+ throw error;
55
+ }
56
+ }
57
+
58
+ function isUnavailableSymlinkError(error) {
59
+ return error?.code === "EPERM" || error?.code === "EACCES" || error?.code === "EINVAL";
60
+ }
61
+
62
+ export function createArchivedOutsideTeam(tempRoot, sessionId) {
63
+ const outsideTeams = join(tempRoot, "outside-teams");
64
+ const outsideTeamDir = join(outsideTeams, sessionId);
65
+ mkdirSync(outsideTeamDir, { recursive: true });
66
+ writeFileSync(
67
+ join(outsideTeamDir, "team.json"),
68
+ `${JSON.stringify(
69
+ {
70
+ schemaVersion: 2,
71
+ teamId: "outside-team",
72
+ teamName: "Outside",
73
+ sessionName: "Escape",
74
+ leader: { kind: "main-session", sessionId },
75
+ status: "archived",
76
+ members: [],
77
+ },
78
+ null,
79
+ 2,
80
+ )}\n`,
81
+ );
82
+ return { outsideTeams, outsideTeamDir };
83
+ }
84
+
85
+ export function assertOutsideTeamIntact(outsideTeamDir) {
86
+ assert.equal(existsSync(outsideTeamDir), true);
87
+ assert.equal(JSON.parse(readFileSync(join(outsideTeamDir, "team.json"), "utf8")).teamId, "outside-team");
88
+ }
@@ -0,0 +1,240 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdirSync, readFileSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import test from "node:test";
5
+
6
+ import {
7
+ assertOutsideTeamIntact,
8
+ cleanupTeamRoot,
9
+ createArchivedOutsideTeam,
10
+ createTeamRoot,
11
+ readTeamJson,
12
+ runTeam,
13
+ runTeamRaw,
14
+ symlinkOrSkip,
15
+ teamDir,
16
+ teamJsonPath,
17
+ } from "./teammode-safety-fixture.mjs";
18
+
19
+ test("#given guide.md is a symlink #when guide is regenerated #then the outside target stays untouched", (t) => {
20
+ const tempRoot = createTeamRoot("omo-codex-teammode-guide-");
21
+ try {
22
+ runTeam(tempRoot, "init", "--name", "Symlink", "--session-name", "Escape", "--session", "safe-guide");
23
+ const guidePath = join(teamDir(tempRoot, "safe-guide"), "guide.md");
24
+ const outsidePath = join(tempRoot, "outside-guide-target.md");
25
+ writeFileSync(outsidePath, "ORIGINAL_OUTSIDE\n");
26
+ unlinkSync(guidePath);
27
+ if (!symlinkOrSkip(t, outsidePath, guidePath)) return;
28
+
29
+ const result = runTeamRaw(tempRoot, "guide", "--team", "safe-guide");
30
+
31
+ assert.notEqual(result.status, 0);
32
+ assert.match(result.stderr, /guide\.md is a symlink|persist target escapes/);
33
+ assert.equal(readFileSync(outsidePath, "utf8"), "ORIGINAL_OUTSIDE\n");
34
+ } finally {
35
+ cleanupTeamRoot(tempRoot);
36
+ }
37
+ });
38
+
39
+ test("#given member A exists #when add-member receives A with trailing space #then state is not partially mutated", () => {
40
+ const tempRoot = createTeamRoot("omo-codex-teammode-duplicate-");
41
+ try {
42
+ runTeam(tempRoot, "init", "--name", "Duplicate", "--session-name", "Members", "--session", "safe-duplicate");
43
+ runTeam(
44
+ tempRoot,
45
+ "add-member",
46
+ "--team",
47
+ "safe-duplicate",
48
+ "--id",
49
+ "A",
50
+ "--focus",
51
+ "alpha",
52
+ "--lens",
53
+ "area",
54
+ "--deliverable",
55
+ "first",
56
+ );
57
+
58
+ const result = runTeamRaw(
59
+ tempRoot,
60
+ "add-member",
61
+ "--team",
62
+ "safe-duplicate",
63
+ "--id",
64
+ "A ",
65
+ "--focus",
66
+ "beta",
67
+ "--lens",
68
+ "ownership",
69
+ "--deliverable",
70
+ "second",
71
+ );
72
+ const team = readTeamJson(tempRoot, "safe-duplicate");
73
+
74
+ assert.notEqual(result.status, 0);
75
+ assert.match(result.stderr, /member id "A" already exists/);
76
+ assert.deepEqual(
77
+ team.members.map((member) => member.id),
78
+ ["A"],
79
+ );
80
+ } finally {
81
+ cleanupTeamRoot(tempRoot);
82
+ }
83
+ });
84
+
85
+ test("#given member focus already exists #when add-member receives same focus with different spacing and case #then state is not partially mutated", () => {
86
+ const tempRoot = createTeamRoot("omo-codex-teammode-duplicate-focus-");
87
+ try {
88
+ runTeam(tempRoot, "init", "--name", "DuplicateFocus", "--session-name", "Members", "--session", "safe-duplicate-focus");
89
+ runTeam(
90
+ tempRoot,
91
+ "add-member",
92
+ "--team",
93
+ "safe-duplicate-focus",
94
+ "--id",
95
+ "A",
96
+ "--focus",
97
+ "Plugin Hook Packaging",
98
+ "--lens",
99
+ "ownership",
100
+ "--deliverable",
101
+ "first",
102
+ );
103
+
104
+ const result = runTeamRaw(
105
+ tempRoot,
106
+ "add-member",
107
+ "--team",
108
+ "safe-duplicate-focus",
109
+ "--id",
110
+ "B",
111
+ "--focus",
112
+ " plugin hook packaging ",
113
+ "--lens",
114
+ "area",
115
+ "--deliverable",
116
+ "second",
117
+ );
118
+ const team = readTeamJson(tempRoot, "safe-duplicate-focus");
119
+
120
+ assert.notEqual(result.status, 0);
121
+ assert.match(result.stderr, /member focus "plugin hook packaging" duplicates "Plugin Hook Packaging"/);
122
+ assert.deepEqual(
123
+ team.members.map((member) => member.id),
124
+ ["A"],
125
+ );
126
+ } finally {
127
+ cleanupTeamRoot(tempRoot);
128
+ }
129
+ });
130
+
131
+ test("#given only one member exists #when bind-thread runs #then member is not activated", () => {
132
+ const tempRoot = createTeamRoot("omo-codex-teammode-understaffed-bind-");
133
+ try {
134
+ runTeam(tempRoot, "init", "--name", "Understaffed", "--session-name", "Members", "--session", "safe-understaffed");
135
+ runTeam(
136
+ tempRoot,
137
+ "add-member",
138
+ "--team",
139
+ "safe-understaffed",
140
+ "--id",
141
+ "A",
142
+ "--focus",
143
+ "installer",
144
+ "--lens",
145
+ "area",
146
+ "--deliverable",
147
+ "first",
148
+ );
149
+
150
+ const result = runTeamRaw(tempRoot, "bind-thread", "--team", "safe-understaffed", "--id", "A", "--thread", "thread-a");
151
+ let team = readTeamJson(tempRoot, "safe-understaffed");
152
+
153
+ assert.notEqual(result.status, 0);
154
+ assert.match(result.stderr, /at least 2 distinct members/);
155
+ assert.equal(team.members[0].threadId, null);
156
+ assert.equal(team.members[0].status, "pending");
157
+
158
+ runTeam(
159
+ tempRoot,
160
+ "add-member",
161
+ "--team",
162
+ "safe-understaffed",
163
+ "--id",
164
+ "B",
165
+ "--focus",
166
+ "runtime qa",
167
+ "--lens",
168
+ "perspective",
169
+ "--deliverable",
170
+ "second",
171
+ );
172
+ runTeam(tempRoot, "bind-thread", "--team", "safe-understaffed", "--id", "A", "--thread", "thread-a");
173
+ team = readTeamJson(tempRoot, "safe-understaffed");
174
+
175
+ assert.equal(team.members[0].threadId, "thread-a");
176
+ assert.equal(team.members[0].status, "active");
177
+ } finally {
178
+ cleanupTeamRoot(tempRoot);
179
+ }
180
+ });
181
+
182
+ test("#given persisted paths are mutated outside trusted team dir #when guide is regenerated #then outside file stays untouched", () => {
183
+ const tempRoot = createTeamRoot("omo-codex-teammode-mutated-paths-");
184
+ try {
185
+ runTeam(tempRoot, "init", "--name", "Mutated", "--session-name", "Paths", "--session", "safe-mutated");
186
+ const teamPath = teamJsonPath(tempRoot, "safe-mutated");
187
+ const outsideDir = join(tempRoot, "outside");
188
+ const outsideGuide = join(outsideDir, "guide.md");
189
+ mkdirSync(outsideDir);
190
+ writeFileSync(outsideGuide, "ORIGINAL_OUTSIDE\n");
191
+ const team = JSON.parse(readFileSync(teamPath, "utf8"));
192
+ team.paths.dir = outsideDir;
193
+ team.paths.guide = outsideGuide;
194
+ writeFileSync(teamPath, `${JSON.stringify(team, null, 2)}\n`);
195
+
196
+ const result = runTeamRaw(tempRoot, "guide", "--team", "safe-mutated");
197
+
198
+ assert.notEqual(result.status, 0);
199
+ assert.match(result.stderr, /persisted team dir does not match trusted team dir/);
200
+ assert.equal(readFileSync(outsideGuide, "utf8"), "ORIGINAL_OUTSIDE\n");
201
+ } finally {
202
+ cleanupTeamRoot(tempRoot);
203
+ }
204
+ });
205
+
206
+ test("#given team dir is swapped to a symlink #when status reads team state #then command refuses the symlink", (t) => {
207
+ const tempRoot = createTeamRoot("omo-codex-teammode-dir-symlink-");
208
+ try {
209
+ runTeam(tempRoot, "init", "--name", "SymlinkDir", "--session-name", "Paths", "--session", "safe-dir");
210
+ const targetTeamDir = teamDir(tempRoot, "safe-dir");
211
+ const outsideDir = join(tempRoot, "outside-team-dir");
212
+ mkdirSync(outsideDir);
213
+ rmSync(targetTeamDir, { recursive: true, force: true });
214
+ if (!symlinkOrSkip(t, outsideDir, targetTeamDir, "dir")) return;
215
+
216
+ const result = runTeamRaw(tempRoot, "status", "--team", "safe-dir");
217
+
218
+ assert.notEqual(result.status, 0);
219
+ assert.match(result.stderr, /path component is a symlink|team dir is a symlink/);
220
+ } finally {
221
+ cleanupTeamRoot(tempRoot);
222
+ }
223
+ });
224
+
225
+ test("#given teams root is a symlink #when delete runs #then outside team state stays untouched", (t) => {
226
+ const tempRoot = createTeamRoot("omo-codex-teammode-delete-root-symlink-");
227
+ try {
228
+ const { outsideTeams, outsideTeamDir } = createArchivedOutsideTeam(tempRoot, "escape");
229
+ mkdirSync(join(tempRoot, ".omo"), { recursive: true });
230
+ if (!symlinkOrSkip(t, outsideTeams, join(tempRoot, ".omo", "teams"), "dir")) return;
231
+
232
+ const result = runTeamRaw(tempRoot, "delete", "--team", "escape", "--force");
233
+
234
+ assert.notEqual(result.status, 0);
235
+ assert.match(result.stderr, /path component is a symlink/);
236
+ assertOutsideTeamIntact(outsideTeamDir);
237
+ } finally {
238
+ cleanupTeamRoot(tempRoot);
239
+ }
240
+ });
@@ -124,3 +124,51 @@ test("#given ultraresearch worker sizing #when spawn guidance is inspected #then
124
124
  );
125
125
  }
126
126
  });
127
+
128
+ test("#given ultraresearch execution substrate #when team usage is inspected #then it prefers a cooperating team with harness-native team tools", async () => {
129
+ for (const copy of await readUltraresearchCopies()) {
130
+ assert.match(
131
+ copy.content,
132
+ /cooperating team/i,
133
+ `${copy.label}: body must encourage running the swarm as a cooperating team`,
134
+ );
135
+ assert.match(copy.content, /\bteammode\b/i, `${copy.label}: body must name the Codex teammode skill`);
136
+ assert.match(copy.content, /team_mode/i, `${copy.label}: body must name the OpenCode team_mode path`);
137
+ }
138
+ });
139
+
140
+ test("#given ultraresearch team composition #when member slicing is inspected #then members map to part/ownership/perspective, never job titles", async () => {
141
+ for (const copy of await readUltraresearchCopies()) {
142
+ assert.match(
143
+ copy.content,
144
+ /by part, ownership, or perspective/i,
145
+ `${copy.label}: body must compose members by part, ownership, or perspective`,
146
+ );
147
+ assert.match(
148
+ copy.content,
149
+ /never a job title|not (?:a |by )?job title/i,
150
+ `${copy.label}: body must forbid vague job-title members`,
151
+ );
152
+ }
153
+ });
154
+
155
+ test("#given ultraresearch team communication #when the raise law is inspected #then members broadcast every lead immediately rather than hoarding", async () => {
156
+ for (const copy of await readUltraresearchCopies()) {
157
+ assert.match(
158
+ copy.content,
159
+ /raise law|broadcast every lead/i,
160
+ `${copy.label}: body must state the raise/broadcast law`,
161
+ );
162
+ assert.match(copy.content, /over-communicate/i, `${copy.label}: body must demand over-communication`);
163
+ assert.match(
164
+ copy.content,
165
+ /the (?:moment|instant) it (?:surfaces|appears|lands)/i,
166
+ `${copy.label}: body must require raising leads the moment they surface`,
167
+ );
168
+ assert.match(
169
+ copy.content,
170
+ /hoard/i,
171
+ `${copy.label}: body must reject hoarding leads for a final dump`,
172
+ );
173
+ }
174
+ });