@openhands/extensions 0.20.0 → 0.22.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 (305) hide show
  1. package/.claude-plugin/marketplace.json +881 -0
  2. package/.codex-plugin/marketplace.json +881 -0
  3. package/.github/workflows/check-extensions.yml +4 -0
  4. package/.github/workflows/npm-publish.yml +9 -3
  5. package/.github/workflows/pr-artifacts.yml +294 -0
  6. package/.github/workflows/pr-description-check.yml +4 -2
  7. package/.plugin/marketplace.json +881 -0
  8. package/.release-please-manifest.json +1 -1
  9. package/AGENTS.md +27 -3
  10. package/README.md +6 -2
  11. package/automations/bundle-index.js +22 -3
  12. package/automations/catalog/github-agents-md-maintainer/manifest.json +2 -1
  13. package/automations/catalog/github-delivery-watchdog/manifest.json +79 -0
  14. package/automations/catalog/github-issue-to-pr/manifest.json +24 -15
  15. package/automations/catalog/github-issue-triage/manifest.json +73 -0
  16. package/automations/catalog/github-pr-reviewer/manifest.json +23 -15
  17. package/automations/catalog/gitlab-issue-to-mr/manifest.json +124 -0
  18. package/automations/catalog-index.js +17 -11
  19. package/integrations/catalog/reportportal.json +76 -0
  20. package/integrations/catalog-index.js +6 -4
  21. package/marketplaces/openhands-extensions.json +42 -0
  22. package/package.json +1 -1
  23. package/plugins/city-weather/.claude-plugin/plugin.json +13 -0
  24. package/plugins/city-weather/.codex-plugin/plugin.json +13 -0
  25. package/plugins/cobol-modernization/.claude-plugin/plugin.json +19 -0
  26. package/plugins/cobol-modernization/.codex-plugin/plugin.json +19 -0
  27. package/plugins/issue-duplicate-checker/.claude-plugin/plugin.json +13 -0
  28. package/plugins/issue-duplicate-checker/.codex-plugin/plugin.json +13 -0
  29. package/plugins/magic-test/.claude-plugin/plugin.json +13 -0
  30. package/plugins/magic-test/.codex-plugin/plugin.json +13 -0
  31. package/plugins/migration-scoring/.claude-plugin/plugin.json +19 -0
  32. package/plugins/migration-scoring/.codex-plugin/plugin.json +19 -0
  33. package/plugins/onboarding/.claude-plugin/plugin.json +20 -0
  34. package/plugins/onboarding/.codex-plugin/plugin.json +20 -0
  35. package/plugins/onboarding/skills/agent-readiness-report/references/criteria.md +144 -0
  36. package/plugins/onboarding/skills/improve-agent-readiness/references/criteria.md +144 -0
  37. package/plugins/openhands/.claude-plugin/plugin.json +13 -0
  38. package/plugins/openhands/.codex-plugin/plugin.json +13 -0
  39. package/plugins/openhands/skills/openhands-api/.claude-plugin/plugin.json +22 -0
  40. package/plugins/openhands/skills/openhands-api/.codex-plugin/plugin.json +22 -0
  41. package/plugins/openhands/skills/openhands-api/.plugin/plugin.json +22 -0
  42. package/plugins/openhands/skills/openhands-api/README.md +52 -0
  43. package/plugins/openhands/skills/openhands-api/SKILL.md +469 -0
  44. package/plugins/openhands/skills/openhands-api/references/README.md +33 -0
  45. package/plugins/openhands/skills/openhands-api/references/TROUBLESHOOTING.md +81 -0
  46. package/plugins/openhands/skills/openhands-api/references/example_prompt.md +12 -0
  47. package/plugins/openhands/skills/openhands-api/references/windows.md +44 -0
  48. package/plugins/openhands/skills/openhands-api/scripts/openhands_api.py +606 -0
  49. package/plugins/openhands/skills/openhands-api/scripts/openhands_api.ts +252 -0
  50. package/plugins/openhands/skills/openhands-automation/.claude-plugin/plugin.json +19 -0
  51. package/plugins/openhands/skills/openhands-automation/.codex-plugin/plugin.json +19 -0
  52. package/plugins/openhands/skills/openhands-automation/.plugin/plugin.json +19 -0
  53. package/plugins/openhands/skills/openhands-automation/README.md +89 -0
  54. package/plugins/openhands/skills/openhands-automation/SKILL.md +899 -0
  55. package/plugins/openhands/skills/openhands-automation/commands/automation-create.md +8 -0
  56. package/plugins/openhands/skills/openhands-automation/references/ab-testing.md +185 -0
  57. package/plugins/openhands/skills/openhands-automation/references/custom-automation.md +832 -0
  58. package/plugins/openhands/skills/openhands-automation/references/security.md +84 -0
  59. package/plugins/openhands/skills/openhands-automation/references/windows.md +57 -0
  60. package/plugins/openhands/skills/openhands-sdk/.claude-plugin/plugin.json +20 -0
  61. package/plugins/openhands/skills/openhands-sdk/.codex-plugin/plugin.json +20 -0
  62. package/plugins/openhands/skills/openhands-sdk/.plugin/plugin.json +20 -0
  63. package/plugins/openhands/skills/openhands-sdk/README.md +22 -0
  64. package/plugins/openhands/skills/openhands-sdk/SKILL.md +245 -0
  65. package/plugins/openhands/skills/openhands-sdk/commands/sdk.md +8 -0
  66. package/plugins/pr-review/.claude-plugin/plugin.json +13 -0
  67. package/plugins/pr-review/.codex-plugin/plugin.json +13 -0
  68. package/plugins/pr-review/skills/code-review/.claude-plugin/plugin.json +19 -0
  69. package/plugins/pr-review/skills/code-review/.codex-plugin/plugin.json +19 -0
  70. package/plugins/pr-review/skills/code-review/.plugin/plugin.json +19 -0
  71. package/plugins/pr-review/skills/code-review/README.md +18 -0
  72. package/plugins/pr-review/skills/code-review/SKILL.md +209 -0
  73. package/plugins/pr-review/skills/code-review/commands/codereview-roasted.md +8 -0
  74. package/plugins/pr-review/skills/code-review/commands/codereview.md +8 -0
  75. package/plugins/pr-review/skills/code-review/references/risk-evaluation.md +41 -0
  76. package/plugins/pr-review/skills/code-review/references/supply-chain-security.md +31 -0
  77. package/plugins/pr-review/skills/github-pr-review/.claude-plugin/plugin.json +18 -0
  78. package/plugins/pr-review/skills/github-pr-review/.codex-plugin/plugin.json +18 -0
  79. package/plugins/pr-review/skills/github-pr-review/.plugin/plugin.json +18 -0
  80. package/plugins/pr-review/skills/github-pr-review/README.md +145 -0
  81. package/plugins/pr-review/skills/github-pr-review/SKILL.md +187 -0
  82. package/plugins/pr-review/skills/github-pr-review/commands/github-pr-review.md +8 -0
  83. package/plugins/pr-review/skills/github-pr-review/references/windows.md +47 -0
  84. package/plugins/qa-changes/.claude-plugin/plugin.json +11 -0
  85. package/plugins/qa-changes/.codex-plugin/plugin.json +11 -0
  86. package/plugins/qa-changes/skills/github-pr-review/.claude-plugin/plugin.json +18 -0
  87. package/plugins/qa-changes/skills/github-pr-review/.codex-plugin/plugin.json +18 -0
  88. package/plugins/qa-changes/skills/github-pr-review/.plugin/plugin.json +18 -0
  89. package/plugins/qa-changes/skills/github-pr-review/README.md +145 -0
  90. package/plugins/qa-changes/skills/github-pr-review/SKILL.md +187 -0
  91. package/plugins/qa-changes/skills/github-pr-review/commands/github-pr-review.md +8 -0
  92. package/plugins/qa-changes/skills/github-pr-review/references/windows.md +47 -0
  93. package/plugins/qa-changes/skills/qa-changes/README.md +18 -0
  94. package/plugins/qa-changes/skills/qa-changes/SKILL.md +229 -0
  95. package/plugins/qa-changes/skills/qa-changes/commands/qa-changes.md +8 -0
  96. package/plugins/release-notes/.claude-plugin/plugin.json +19 -0
  97. package/plugins/release-notes/.codex-plugin/plugin.json +19 -0
  98. package/plugins/release-notes/skills/release-notes/README.md +24 -0
  99. package/plugins/release-notes/skills/release-notes/SKILL.md +19 -0
  100. package/plugins/release-notes/skills/release-notes/commands/release-notes.md +8 -0
  101. package/plugins/vulnerability-remediation/.claude-plugin/plugin.json +19 -0
  102. package/plugins/vulnerability-remediation/.codex-plugin/plugin.json +19 -0
  103. package/plugins/vulnerability-remediation/skills/github/.claude-plugin/plugin.json +19 -0
  104. package/plugins/vulnerability-remediation/skills/github/.codex-plugin/plugin.json +19 -0
  105. package/plugins/vulnerability-remediation/skills/github/.plugin/plugin.json +19 -0
  106. package/plugins/vulnerability-remediation/skills/github/README.md +42 -0
  107. package/plugins/vulnerability-remediation/skills/github/SKILL.md +107 -0
  108. package/plugins/vulnerability-remediation/skills/github/references/windows.md +48 -0
  109. package/plugins/vulnerability-remediation/skills/github/scripts/agent_conversation.py +143 -0
  110. package/plugins/vulnerability-remediation/skills/github/scripts/github_client.py +230 -0
  111. package/plugins/vulnerability-remediation/skills/security/.claude-plugin/plugin.json +18 -0
  112. package/plugins/vulnerability-remediation/skills/security/.codex-plugin/plugin.json +18 -0
  113. package/plugins/vulnerability-remediation/skills/security/.plugin/plugin.json +18 -0
  114. package/plugins/vulnerability-remediation/skills/security/README.md +38 -0
  115. package/plugins/vulnerability-remediation/skills/security/SKILL.md +33 -0
  116. package/pyproject.toml +2 -2
  117. package/python/openhands_extensions/_version.py +1 -1
  118. package/skills/add-javadoc/.claude-plugin/plugin.json +18 -0
  119. package/skills/add-javadoc/.codex-plugin/plugin.json +18 -0
  120. package/skills/add-skill/.claude-plugin/plugin.json +18 -0
  121. package/skills/add-skill/.codex-plugin/plugin.json +18 -0
  122. package/skills/agent-canvas-environment/.claude-plugin/plugin.json +19 -0
  123. package/skills/agent-canvas-environment/.codex-plugin/plugin.json +19 -0
  124. package/skills/agent-creator/.claude-plugin/plugin.json +20 -0
  125. package/skills/agent-creator/.codex-plugin/plugin.json +20 -0
  126. package/skills/agent-memory/.claude-plugin/plugin.json +18 -0
  127. package/skills/agent-memory/.codex-plugin/plugin.json +18 -0
  128. package/skills/agent-sdk-builder/.claude-plugin/plugin.json +18 -0
  129. package/skills/agent-sdk-builder/.codex-plugin/plugin.json +18 -0
  130. package/skills/azure-devops/.claude-plugin/plugin.json +18 -0
  131. package/skills/azure-devops/.codex-plugin/plugin.json +18 -0
  132. package/skills/bitbucket/.claude-plugin/plugin.json +17 -0
  133. package/skills/bitbucket/.codex-plugin/plugin.json +17 -0
  134. package/skills/bitbucket-cloud/.claude-plugin/plugin.json +18 -0
  135. package/skills/bitbucket-cloud/.codex-plugin/plugin.json +18 -0
  136. package/skills/bitbucket-data-center/.claude-plugin/plugin.json +19 -0
  137. package/skills/bitbucket-data-center/.codex-plugin/plugin.json +19 -0
  138. package/skills/code-review/.claude-plugin/plugin.json +19 -0
  139. package/skills/code-review/.codex-plugin/plugin.json +19 -0
  140. package/skills/code-simplifier/.claude-plugin/plugin.json +21 -0
  141. package/skills/code-simplifier/.codex-plugin/plugin.json +21 -0
  142. package/skills/datadog/.claude-plugin/plugin.json +19 -0
  143. package/skills/datadog/.codex-plugin/plugin.json +19 -0
  144. package/skills/deno/.claude-plugin/plugin.json +18 -0
  145. package/skills/deno/.codex-plugin/plugin.json +18 -0
  146. package/skills/discord/.claude-plugin/plugin.json +18 -0
  147. package/skills/discord/.codex-plugin/plugin.json +18 -0
  148. package/skills/docker/.claude-plugin/plugin.json +17 -0
  149. package/skills/docker/.codex-plugin/plugin.json +17 -0
  150. package/skills/evidence-based-citations/.claude-plugin/plugin.json +20 -0
  151. package/skills/evidence-based-citations/.codex-plugin/plugin.json +20 -0
  152. package/skills/flarglebargle/.claude-plugin/plugin.json +16 -0
  153. package/skills/flarglebargle/.codex-plugin/plugin.json +16 -0
  154. package/skills/frontend-design/.claude-plugin/plugin.json +21 -0
  155. package/skills/frontend-design/.codex-plugin/plugin.json +21 -0
  156. package/skills/github/.claude-plugin/plugin.json +19 -0
  157. package/skills/github/.codex-plugin/plugin.json +19 -0
  158. package/skills/github/scripts/agent_conversation.py +143 -0
  159. package/skills/github/scripts/github_client.py +230 -0
  160. package/skills/github-actions/.claude-plugin/plugin.json +19 -0
  161. package/skills/github-actions/.codex-plugin/plugin.json +19 -0
  162. package/skills/github-agents-md-maintainer/.claude-plugin/plugin.json +19 -0
  163. package/skills/github-agents-md-maintainer/.codex-plugin/plugin.json +19 -0
  164. package/skills/github-agents-md-maintainer/SKILL.md +7 -1
  165. package/skills/github-agents-md-maintainer/scripts/github_client.py +230 -0
  166. package/skills/github-agents-md-maintainer/scripts/main.py +4 -40
  167. package/skills/github-delivery-watchdog/.claude-plugin/plugin.json +9 -0
  168. package/skills/github-delivery-watchdog/.codex-plugin/plugin.json +9 -0
  169. package/skills/github-delivery-watchdog/.plugin/plugin.json +9 -0
  170. package/skills/github-delivery-watchdog/README.md +10 -0
  171. package/skills/github-delivery-watchdog/SKILL.md +35 -0
  172. package/skills/github-delivery-watchdog/commands/github-delivery-watchdog.md +8 -0
  173. package/skills/github-delivery-watchdog/scripts/github_client.py +230 -0
  174. package/skills/github-delivery-watchdog/scripts/worker.py +96 -0
  175. package/skills/github-issue-to-pr/.claude-plugin/plugin.json +19 -0
  176. package/skills/github-issue-to-pr/.codex-plugin/plugin.json +19 -0
  177. package/skills/github-issue-to-pr/SKILL.md +15 -1
  178. package/skills/github-issue-to-pr/scripts/agent_conversation.py +143 -0
  179. package/skills/github-issue-to-pr/scripts/github_client.py +230 -0
  180. package/skills/github-issue-to-pr/scripts/main.py +19 -50
  181. package/skills/github-issue-to-pr/scripts/worker.py +172 -0
  182. package/skills/github-issue-triage/.claude-plugin/plugin.json +9 -0
  183. package/skills/github-issue-triage/.codex-plugin/plugin.json +9 -0
  184. package/skills/github-issue-triage/.plugin/plugin.json +9 -0
  185. package/skills/github-issue-triage/README.md +9 -0
  186. package/skills/github-issue-triage/SKILL.md +34 -0
  187. package/skills/github-issue-triage/commands/github-issue-triage.md +8 -0
  188. package/skills/github-issue-triage/scripts/agent_conversation.py +143 -0
  189. package/skills/github-issue-triage/scripts/github_client.py +230 -0
  190. package/skills/github-issue-triage/scripts/worker.py +106 -0
  191. package/skills/github-pr-review/.claude-plugin/plugin.json +18 -0
  192. package/skills/github-pr-review/.codex-plugin/plugin.json +18 -0
  193. package/skills/github-pr-reviewer/.claude-plugin/plugin.json +20 -0
  194. package/skills/github-pr-reviewer/.codex-plugin/plugin.json +20 -0
  195. package/skills/github-pr-reviewer/SKILL.md +15 -1
  196. package/skills/github-pr-reviewer/scripts/agent_conversation.py +143 -0
  197. package/skills/github-pr-reviewer/scripts/github_client.py +230 -0
  198. package/skills/github-pr-reviewer/scripts/main.py +23 -49
  199. package/skills/github-pr-reviewer/scripts/worker.py +94 -0
  200. package/skills/github-repo-monitor/.claude-plugin/plugin.json +22 -0
  201. package/skills/github-repo-monitor/.codex-plugin/plugin.json +22 -0
  202. package/skills/github-repo-monitor/SKILL.md +7 -1
  203. package/skills/github-repo-monitor/scripts/github_client.py +230 -0
  204. package/skills/github-repo-monitor/scripts/main.py +4 -46
  205. package/skills/gitlab/.claude-plugin/plugin.json +17 -0
  206. package/skills/gitlab/.codex-plugin/plugin.json +17 -0
  207. package/skills/gitlab-issue-to-mr/.claude-plugin/plugin.json +19 -0
  208. package/skills/gitlab-issue-to-mr/.codex-plugin/plugin.json +19 -0
  209. package/skills/gitlab-issue-to-mr/.plugin/plugin.json +19 -0
  210. package/skills/gitlab-issue-to-mr/README.md +92 -0
  211. package/skills/gitlab-issue-to-mr/SKILL.md +408 -0
  212. package/skills/gitlab-issue-to-mr/commands/issue-to-mr-setup.md +8 -0
  213. package/skills/gitlab-issue-to-mr/references/state-schema.md +140 -0
  214. package/skills/gitlab-issue-to-mr/scripts/main.py +1448 -0
  215. package/skills/incident-retrospective/.claude-plugin/plugin.json +21 -0
  216. package/skills/incident-retrospective/.codex-plugin/plugin.json +21 -0
  217. package/skills/index.js +52 -6
  218. package/skills/iterate/.claude-plugin/plugin.json +13 -0
  219. package/skills/iterate/.codex-plugin/plugin.json +13 -0
  220. package/skills/jira-issue-to-pr/.claude-plugin/plugin.json +19 -0
  221. package/skills/jira-issue-to-pr/.codex-plugin/plugin.json +19 -0
  222. package/skills/jupyter/.claude-plugin/plugin.json +18 -0
  223. package/skills/jupyter/.codex-plugin/plugin.json +18 -0
  224. package/skills/kubernetes/.claude-plugin/plugin.json +18 -0
  225. package/skills/kubernetes/.codex-plugin/plugin.json +18 -0
  226. package/skills/learn-from-code-review/.claude-plugin/plugin.json +19 -0
  227. package/skills/learn-from-code-review/.codex-plugin/plugin.json +19 -0
  228. package/skills/linear/.claude-plugin/plugin.json +19 -0
  229. package/skills/linear/.codex-plugin/plugin.json +19 -0
  230. package/skills/linear-triage/.claude-plugin/plugin.json +21 -0
  231. package/skills/linear-triage/.codex-plugin/plugin.json +21 -0
  232. package/skills/news-digest/.claude-plugin/plugin.json +19 -0
  233. package/skills/news-digest/.codex-plugin/plugin.json +19 -0
  234. package/skills/notion/.claude-plugin/plugin.json +17 -0
  235. package/skills/notion/.codex-plugin/plugin.json +17 -0
  236. package/skills/npm/.claude-plugin/plugin.json +17 -0
  237. package/skills/npm/.codex-plugin/plugin.json +17 -0
  238. package/skills/openhands-api/.claude-plugin/plugin.json +22 -0
  239. package/skills/openhands-api/.codex-plugin/plugin.json +22 -0
  240. package/skills/openhands-automation/.claude-plugin/plugin.json +19 -0
  241. package/skills/openhands-automation/.codex-plugin/plugin.json +19 -0
  242. package/skills/openhands-automation/README.md +1 -1
  243. package/skills/openhands-automation/SKILL.md +18 -8
  244. package/skills/openhands-enterprise-troubleshooting/.claude-plugin/plugin.json +20 -0
  245. package/skills/openhands-enterprise-troubleshooting/.codex-plugin/plugin.json +20 -0
  246. package/skills/openhands-enterprise-troubleshooting/.plugin/plugin.json +20 -0
  247. package/skills/openhands-enterprise-troubleshooting/README.md +79 -0
  248. package/skills/openhands-enterprise-troubleshooting/SKILL.md +339 -0
  249. package/skills/openhands-enterprise-troubleshooting/references/diagnostics.md +698 -0
  250. package/skills/openhands-enterprise-troubleshooting/references/support-bundle-analysis.md +570 -0
  251. package/skills/openhands-enterprise-troubleshooting/scripts/bundle_triage.py +532 -0
  252. package/skills/openhands-sdk/.claude-plugin/plugin.json +20 -0
  253. package/skills/openhands-sdk/.codex-plugin/plugin.json +20 -0
  254. package/skills/openhands-sdk/SKILL.md +2 -0
  255. package/skills/pdflatex/.claude-plugin/plugin.json +18 -0
  256. package/skills/pdflatex/.codex-plugin/plugin.json +18 -0
  257. package/skills/plain-english-content/.claude-plugin/plugin.json +15 -0
  258. package/skills/plain-english-content/.codex-plugin/plugin.json +15 -0
  259. package/skills/prd/.claude-plugin/plugin.json +19 -0
  260. package/skills/prd/.codex-plugin/plugin.json +19 -0
  261. package/skills/research-brief/.claude-plugin/plugin.json +20 -0
  262. package/skills/research-brief/.codex-plugin/plugin.json +20 -0
  263. package/skills/security/.claude-plugin/plugin.json +18 -0
  264. package/skills/security/.codex-plugin/plugin.json +18 -0
  265. package/skills/skill-creator/.claude-plugin/plugin.json +17 -0
  266. package/skills/skill-creator/.codex-plugin/plugin.json +17 -0
  267. package/skills/slack-channel-monitor/.claude-plugin/plugin.json +21 -0
  268. package/skills/slack-channel-monitor/.codex-plugin/plugin.json +21 -0
  269. package/skills/slack-standup-digest/.claude-plugin/plugin.json +21 -0
  270. package/skills/slack-standup-digest/.codex-plugin/plugin.json +21 -0
  271. package/skills/spark-version-upgrade/.claude-plugin/plugin.json +20 -0
  272. package/skills/spark-version-upgrade/.codex-plugin/plugin.json +20 -0
  273. package/skills/ssh/.claude-plugin/plugin.json +18 -0
  274. package/skills/ssh/.codex-plugin/plugin.json +18 -0
  275. package/skills/swift-linux/.claude-plugin/plugin.json +17 -0
  276. package/skills/swift-linux/.codex-plugin/plugin.json +17 -0
  277. package/skills/technical-writing/.claude-plugin/plugin.json +15 -0
  278. package/skills/technical-writing/.codex-plugin/plugin.json +15 -0
  279. package/skills/theme-factory/.claude-plugin/plugin.json +19 -0
  280. package/skills/theme-factory/.codex-plugin/plugin.json +19 -0
  281. package/skills/ticket-to-code-change/.claude-plugin/plugin.json +21 -0
  282. package/skills/ticket-to-code-change/.codex-plugin/plugin.json +21 -0
  283. package/skills/upstream-fork-sync/.claude-plugin/plugin.json +21 -0
  284. package/skills/upstream-fork-sync/.codex-plugin/plugin.json +21 -0
  285. package/skills/uv/.claude-plugin/plugin.json +18 -0
  286. package/skills/uv/.codex-plugin/plugin.json +18 -0
  287. package/skills/vercel/.claude-plugin/plugin.json +18 -0
  288. package/skills/vercel/.codex-plugin/plugin.json +18 -0
  289. package/tests/fixtures/automations/capabilities.json +6 -3
  290. package/tests/fixtures/automations/github-pr-reviewer.json +64 -43
  291. package/tests/github_automation_helpers.py +31 -0
  292. package/tests/test_agent_conversation_dispatch.py +180 -0
  293. package/tests/test_automation_setup.py +3 -3
  294. package/tests/test_github_automation_foundation.py +73 -0
  295. package/tests/test_github_developer_delivery.py +150 -0
  296. package/tests/test_github_reviewer_delivery.py +110 -0
  297. package/tests/test_github_skill_installation.py +63 -0
  298. package/tests/test_github_triage_delivery.py +90 -0
  299. package/tests/test_github_watchdog_delivery.py +233 -0
  300. package/tests/test_gitlab_issue_to_mr.py +823 -0
  301. package/.pr/custom-automation-event-trigger.png +0 -0
  302. package/.pr/custom-automation-plugin-fields.png +0 -0
  303. package/.pr/custom-automation-prompt-review.png +0 -0
  304. package/.pr/custom-automation-prompt-scheduled.png +0 -0
  305. package/.pr/custom-automation-upload-fields.png +0 -0
@@ -0,0 +1,570 @@
1
+ # Support Bundle Analysis
2
+
3
+ How to read a Replicated support bundle from an OpenHands Enterprise install, offline and without
4
+ cluster access. A bundle is a point-in-time snapshot collected by
5
+ [troubleshoot.sh](https://troubleshoot.sh) — it contains nearly everything you would get from
6
+ `kubectl` against the live cluster, but the data is reorganized, partly redacted, and has specific
7
+ gaps you must know about before you trust a negative result.
8
+
9
+ Use this after generating a bundle (see "Support Bundle Generation" in `SKILL.md`), or when someone
10
+ hands you a `support-bundle-*` directory.
11
+
12
+ ## Unpack first
13
+
14
+ The download is a `.tar.gz` and nothing below works until it is extracted.
15
+
16
+ ```bash
17
+ tar -xzf support-bundle-2026-07-28T06_54_18.tar.gz
18
+ cd support-bundle-2026-07-28T06_54_18
19
+
20
+ # Sanity check: a real bundle has these at its root
21
+ ls analysis.json version.yaml cluster-resources/
22
+ ```
23
+
24
+ The archive expands to a single `support-bundle-<UTC timestamp>/` directory. Every path in this
25
+ document is relative to that directory. Note that macOS Safari may have auto-extracted it already,
26
+ in which case the directory is present and the archive is not.
27
+
28
+ ## Establish when the bundle was taken
29
+
30
+ Every age, "X ago", and staleness judgment depends on this, and getting it wrong is the single most
31
+ common mistake.
32
+
33
+ **Do not use file mtimes.** They record when *you* extracted the archive, in *your* local timezone —
34
+ a silent multi-hour error in every age you compute.
35
+
36
+ Use, in order of preference:
37
+
38
+ 1. The bundle directory name — `support-bundle-2026-07-28T06_54_18` is a UTC timestamp.
39
+ 2. The newest timestamp inside `node-metrics/<node>.json`.
40
+ 3. `.status.conditions[].lastTransitionTime` on a recently-updated object.
41
+
42
+ ## Fast path: the triage script
43
+
44
+ `scripts/bundle_triage.py` reconstructs the whole standard first pass in one command. Standard
45
+ library only, no dependencies.
46
+
47
+ ```bash
48
+ python3 scripts/bundle_triage.py /path/to/support-bundle-2026-07-28T06_54_18
49
+ ```
50
+
51
+ It prints, in order: bundle metadata and node capacity, analyzer verdicts, a
52
+ `kubectl get pods -o wide` table, an OOM/restart scan across every namespace, a `kubectl top pods`
53
+ equivalent, allocated-vs-allocatable resources, and an events summary.
54
+
55
+ The script reports; it does not rank or diagnose. Reading the output for which of its observations
56
+ explains the symptom the user reported is the part that needs judgement, and it is yours to do. An
57
+ alarming-looking line can be long-standing and irrelevant, and the thing that actually broke may not
58
+ appear in the output at all (see below).
59
+
60
+ ```bash
61
+ # Focus one namespace, or one section at a time
62
+ python3 scripts/bundle_triage.py <bundle> --namespace openhands
63
+ python3 scripts/bundle_triage.py <bundle> --section pods --section events
64
+
65
+ # Sandbox pods are collapsed by default; there can be hundreds
66
+ python3 scripts/bundle_triage.py <bundle> --expand-runtimes
67
+ ```
68
+
69
+ Use it to orient, then go to the raw files for anything it surfaces.
70
+
71
+ ## Read these five first
72
+
73
+ | File | Gives you |
74
+ |---|---|
75
+ | `analysis.json` | Pre-computed analyzer verdicts — often answers the question outright |
76
+ | `version.yaml` | Troubleshoot spec version |
77
+ | `cluster-info/cluster_version.json` | Kubernetes version |
78
+ | `cluster-resources/nodes.json` | Nodes, their labels and roles, capacity, conditions, taints |
79
+ | `kots/admin_console/app-info.json` | App status, channel, sequence, KOTS + embedded-cluster versions |
80
+
81
+ `analysis.json` is the fastest orientation in the bundle and the most frequently skipped. It ships
82
+ verdicts like `event.oom.check: "No OOMKilling event detected"` and
83
+ `node.resources.for.openhands: "Node resources are sufficient"` already computed.
84
+
85
+ Read it first, but do not stop there. Its analyzers are narrow — the OOM analyzer keys on *events*,
86
+ so it reports "No OOMKilling event detected" on a bundle whose pod objects clearly record
87
+ `OOMKilled` containers, simply because the events aged out of the TTL window. **A clean
88
+ `analysis.json` is not evidence of a clean cluster**, only that no analyzer's specific trigger
89
+ fired.
90
+
91
+ ```bash
92
+ # Failing analyzers, keeping severity and the distinct message
93
+ jq -r '.[] | select(.severity|test("error|warn|fail"))
94
+ | "\(.severity)\t\(.name)\t\(.insight.detail)"' analysis.json | sort
95
+
96
+ # Collapsing per-instance analyzers: replace only the identifier segments (long
97
+ # digit runs, hashes), never the whole middle -- wildcarding `a.b.*.status` merges
98
+ # unrelated subsystems and hides real failures behind an unrelated example.
99
+ jq -r '.[] | select(.severity|test("error|warn|fail")) | .name' analysis.json \
100
+ | sed -E 's/\.[0-9]{4,}\./.*./g' | sort | uniq -c | sort -rn
101
+
102
+ # Passing analyzers (what the collector already confirmed is fine)
103
+ jq -r '.[] | select(.severity=="debug") | "\(.name): \(.insight.detail)"' analysis.json
104
+ ```
105
+
106
+ ## The kubectl → bundle map
107
+
108
+ | What you would run | Where it lives | Notes |
109
+ |---|---|---|
110
+ | `get pods -n <ns> -o wide` | `cluster-resources/pods/<ns>.json` | Complete; pod IPs redacted |
111
+ | `get <kind> -n <ns> -o yaml` | `cluster-resources/<kind>/<ns>.json` | Live API objects incl. `.status` |
112
+ | `describe pod` | `cluster-resources/pods/<ns>.json` | Reconstruct it — no `describe` output is ever stored |
113
+ | `top pods -n <ns>` | `node-metrics/<node>.json` | Raw kubelet summary API; richer than `top` |
114
+ | `describe node` (capacity, conditions) | `cluster-resources/nodes.json` + `node-metrics/` | Complete |
115
+ | `describe node` (**Events**) | — | **Not collected.** See Known gaps |
116
+ | `get events -n <ns>` | `cluster-resources/events/<ns>.json` | Short TTL window only |
117
+ | `get ingress -n <ns>` | `cluster-resources/ingress/<ns>.json` | Spec + status; no events |
118
+ | `logs <pod> [-c <container>]` | `cluster-resources/pods/logs/<ns>/<pod>/<container>.log` | Filename is the **container** name |
119
+ | `logs <pod> --previous` | `…/<container>-previous.log` | |
120
+ | `get pv,pvc,sc` | `cluster-resources/pvs.json`, `pvcs/<ns>.json`, `storage-classes.json` | |
121
+ | `helm get values` | — | **Not collected.** See Config and Helm values |
122
+
123
+ Cluster-scoped resources are single files at `cluster-resources/*.json` (`nodes`, `pvs`,
124
+ `namespaces`, `clusterroles`, `storage-classes`, `custom-resource-definitions`, …). Namespaced
125
+ resources are a directory per kind with one file per namespace.
126
+
127
+ ## Namespaces and workloads
128
+
129
+ The namespaces that carry workloads worth reading. This is a curated subset, not the full list — a
130
+ bundle also contains `default`, `kube-public`, `kube-node-lease`, and `k0s-autopilot` (k0s's
131
+ in-place updater). Enumerate `cluster-resources/pods/*.json` rather than assuming this table is
132
+ exhaustive.
133
+
134
+ | Namespace | Contents |
135
+ |---|---|
136
+ | `openhands` | The application — all OpenHands services plus `runtime-*` sandbox pods |
137
+ | `kotsadm` | KOTS admin console, rqlite, kurl-proxy |
138
+ | `kube-system` | calico, coredns, kube-proxy, metrics-server |
139
+ | `cert-manager` | cert-manager, cainjector, webhook, trust-manager |
140
+ | `traefik` | Ingress controller |
141
+ | `openebs` | Local PV provisioner (backs `openebs-hostpath` PVCs) |
142
+ | `embedded-cluster` | Embedded-cluster operator |
143
+
144
+ Platform pods in `openhands`, roughly: `openhands` (frontend/API), `openhands-integrations`,
145
+ `openhands-mcp`, `openhands-litellm`, `openhands-runtime-api`, `openhands-minio`,
146
+ `openhands-redis-master`, `keycloak`, `automation`, `plugin-directory`, `replicated` (SDK), plus
147
+ CronJobs (`openhands-runtime-api-cleanup`, `-db-cleanup`, `-warm-runtimes`, …).
148
+
149
+ `runtime-*` pods are per-conversation agent sandboxes. There can be dozens to hundreds, each with
150
+ its own Deployment, Service, and PVC. They dominate pod counts, event counts, and analyzer output —
151
+ filter them out before drawing conclusions about platform health, then look at them separately.
152
+
153
+ Postgres may be **external** (a `DB_HOST` outside the cluster) rather than the in-cluster
154
+ StatefulSet. Check the deployment env before assuming a `postgresql` pod should exist.
155
+
156
+ ## Reading pod state
157
+
158
+ `.status.phase` is not what `kubectl` prints in the STATUS column. To get CrashLoopBackOff,
159
+ ImagePullBackOff, `Init:Error`, or Terminating you must walk `.status.containerStatuses[].state`
160
+ and `.status.initContainerStatuses[]`, exactly as kubectl's printer does. `bundle_triage.py`
161
+ implements this — reuse it rather than reimplementing.
162
+
163
+ ```bash
164
+ NS=openhands
165
+
166
+ # Restart counts, highest first
167
+ jq -r '.items[] | {n:.metadata.name,
168
+ r:([(.status.containerStatuses//[])[].restartCount]|add // 0)}
169
+ | select(.r>0) | "\(.r)\t\(.n)"' cluster-resources/pods/$NS.json | sort -rn
170
+
171
+ # Containers currently waiting (CrashLoopBackOff, ImagePullBackOff, …)
172
+ jq -r '.items[] | .metadata.name as $p
173
+ | (.status.containerStatuses//[])[]
174
+ | select(.state.waiting)
175
+ | "\($p)\t\(.name)\t\(.state.waiting.reason)\t\(.state.waiting.message//"")"' \
176
+ cluster-resources/pods/$NS.json
177
+
178
+ # Why every Pending pod is Pending
179
+ jq -r '.items[] | select(.status.phase=="Pending")
180
+ | (.status.conditions//[])[] | select(.type=="PodScheduled" and .status!="True")
181
+ | .message' cluster-resources/pods/$NS.json | sort | uniq -c | sort -rn
182
+ ```
183
+
184
+ **Why a pod is Pending** lives in the `PodScheduled` condition — the scheduler's message names the
185
+ actual blocking predicate (insufficient CPU, unbound PVC, taint). Read it before assuming resource
186
+ pressure.
187
+
188
+ ### Checking for OOMKills — three independent sources
189
+
190
+ Check all three. A clean result from any one of them alone is weak evidence.
191
+
192
+ ```bash
193
+ # 1. Container state on any pod, any namespace
194
+ grep -l OOMKilled cluster-resources/pods/*.json || echo "no OOMKilled in any pod object"
195
+
196
+ # 2. Events (only within the events TTL window)
197
+ jq -r '[(.items//[])[] | select((.reason+.message) | test("OOM|Evict|MemoryPressure";"i"))] | length' \
198
+ cluster-resources/events/openhands.json
199
+
200
+ # 3. The pre-computed analyzer
201
+ jq -r '.[] | select(.name|test("oom")) | "\(.name): \(.insight.detail)"' analysis.json
202
+ ```
203
+
204
+ ### Simultaneous restarts are a node reboot, not a crash
205
+
206
+ If many containers across unrelated namespaces share one `lastState.terminated.finishedAt` with
207
+ `exitCode: 255` / `reason: Unknown`, the host restarted. Reading those as per-workload failures
208
+ sends you down the wrong path. `bundle_triage.py` flags this automatically.
209
+
210
+ ```bash
211
+ # Abnormal terminations across every namespace, sorted by finish time
212
+ for f in cluster-resources/pods/*.json; do
213
+ jq -r --arg ns "$(basename "$f" .json)" '
214
+ (.items//[])[] | .metadata.name as $p
215
+ | ((.status.containerStatuses//[]) + (.status.initContainerStatuses//[]))[]
216
+ | . as $c | (.lastState.terminated // .state.terminated)
217
+ | select(. != null and .reason != "Completed")
218
+ | "\($ns)\t\($p)\t\($c.name)\t\(.reason)\texit=\(.exitCode)\t\(.finishedAt)"
219
+ ' "$f"
220
+ done | sort -t$'\t' -k6
221
+ ```
222
+
223
+ ## Resource pressure
224
+
225
+ Two different questions, two different sources:
226
+
227
+ - **Actual usage** — `node-metrics/<node>.json`, the kubelet summary API. Per-pod and per-container
228
+ CPU, memory working set, and ephemeral storage, plus node-level `fs` / `imageFs` capacity.
229
+ - **Scheduling headroom** — sum each pod's *effective* request over pods that have a
230
+ `.spec.nodeName` **and are still `Running` or `Pending`**, then compare to `.status.allocatable`
231
+ in `nodes.json`. This is what `kubectl describe node` shows under "Allocated resources", and it is
232
+ what the scheduler acts on. Two things make this easy to get wrong:
233
+
234
+ - A pod's effective request is `max(sum(regular containers), max(init container))`, **not** the sum
235
+ of both — init containers finish before the regular ones start. Native sidecars
236
+ (`initContainers` with `restartPolicy: Always`) keep running, so they belong in the regular sum.
237
+ - `Succeeded` pods still carry a `nodeName`. Counting completed Jobs inflates the total against a
238
+ node that has long since reclaimed their capacity.
239
+
240
+ `bundle_triage.py --section alloc` applies neither rule — it adds init-container requests to the
241
+ regular sum, so any pod with init steps is over-charged. Treat its percentages as a starting point
242
+ and recompute by hand before concluding a node is short of capacity.
243
+
244
+ Requests, not usage, decide whether the next pod schedules. A node at 17% memory usage can still
245
+ refuse to schedule anything. Check `ephemeral-storage` too — it is a real and frequently-hit
246
+ ceiling that nobody thinks to look at.
247
+
248
+ A `Pending` pod is not always short of resources. If it carries a `spec.nodeSelector` or a
249
+ `spec.affinity`, the scheduler will only consider nodes matching it, and free capacity elsewhere
250
+ counts for nothing. Check the constraint against the labels actually present before reading the
251
+ allocation numbers as the answer:
252
+
253
+ ```bash
254
+ # What the pod demands of a node
255
+ jq -r '.items[] | select(.status.phase == "Pending")
256
+ | .metadata.name, (.spec.nodeSelector // {}), (.spec.affinity // {})' \
257
+ cluster-resources/pods/openhands.json
258
+
259
+ # What the nodes actually offer
260
+ jq -r '.items[] | .metadata.name + "\t" + ((.metadata.labels // {}) | to_entries
261
+ | map(select(.key | test("node-role|openhands.dev"))) | map(.key) | join(","))' \
262
+ cluster-resources/nodes.json
263
+ ```
264
+
265
+ Node labels are applied at join time and are not reconciled afterwards, so an absent label is not
266
+ evidence the role was never intended. `Pending` with no `nodeName` and no scheduling event is the
267
+ signature of a constraint nothing satisfies — see the events caveat below, since the bundle may not
268
+ have captured the reason.
269
+
270
+ ```bash
271
+ NODE=$(ls node-metrics/*.json | head -1)
272
+
273
+ # Node totals
274
+ jq '{cpu_cores: (.node.cpu.usageNanoCores/1e9),
275
+ mem_workingset_gib: (.node.memory.workingSetBytes/1073741824),
276
+ mem_available_gib: (.node.memory.availableBytes/1073741824),
277
+ nodefs_used_gib: (.node.fs.usedBytes/1073741824),
278
+ nodefs_cap_gib: (.node.fs.capacityBytes/1073741824)}' "$NODE"
279
+
280
+ # kubectl top pods, sorted by memory
281
+ jq -r '.pods[] | "\(.podRef.namespace)\t\(.podRef.name)\t\(((.memory.workingSetBytes//0)/1048576)|floor)Mi"' \
282
+ "$NODE" | sort -t$'\t' -k3 -rn | head -20
283
+ ```
284
+
285
+ Allocated requests vs allocatable needs quantity parsing — use
286
+ `python3 scripts/bundle_triage.py <bundle> --section alloc`.
287
+
288
+ ## Events
289
+
290
+ `cluster-resources/events/<ns>.json` covers **only the API server's event TTL window** — often as
291
+ little as one hour. Anything older is gone.
292
+
293
+ An empty file is `{"kind":"EventList", …, "items": null}` and means "no events in the window", not
294
+ "nothing happened". Most namespaces in a healthy bundle are empty.
295
+
296
+ ```bash
297
+ NS=openhands
298
+
299
+ # The window the bundle actually covers — state this in any conclusion you draw
300
+ jq -r '(.items//[])[] | (.lastTimestamp // .eventTime // .metadata.creationTimestamp)' \
301
+ cluster-resources/events/$NS.json | sort | sed -n '1p;$p'
302
+
303
+ # Warnings, deduped
304
+ jq -r '(.items//[])[] | select(.type=="Warning") | "\(.reason)\t\(.message)"' \
305
+ cluster-resources/events/$NS.json | sed -E 's/"[^"]*"/"X"/g' | sort | uniq -c | sort -rn
306
+ ```
307
+
308
+ ## Logs
309
+
310
+ One canonical location. Everything else is symlinks into it.
311
+
312
+ ```
313
+ cluster-resources/pods/logs/<namespace>/<pod-name>/<container-name>.log
314
+ ```
315
+
316
+ ```bash
317
+ find cluster-resources/pods/logs -path "*runtime-api*"
318
+ ```
319
+
320
+ **The filename is the container name, not the pod name.** Every init container gets its own file,
321
+ so a pod directory may look like:
322
+
323
+ ```
324
+ openhands-runtime-api-6b55f68bb-g6kb6/
325
+ ├── wait-for-db.log # init container
326
+ ├── create-db.log # init container
327
+ ├── migrate-db.log # init container ← failures here are invisible to `kubectl logs <pod>`
328
+ ├── runtime-api.log
329
+ └── runtime-api-previous.log # kubectl logs --previous
330
+ ```
331
+
332
+ Init-container logs are frequently where the real failure is, and are the easiest thing in the
333
+ bundle to miss.
334
+
335
+ Convenience directories (`podlogs/`, `runtime-sandboxes/logs/`, `replicated/logs/`, parts of `app/`)
336
+ are **symlink farms** pointing back at the canonical path. Do not treat them as separate evidence.
337
+ `find -type f` shows them as empty; use `find -type l` or `find -L`.
338
+
339
+ Host-level journald logs live outside the pod tree: `k0scontroller/<node>.log`,
340
+ `k0sworker/<node>.log`, `local-artifact-mirror/<node>.log`. These partly compensate for the missing
341
+ node events.
342
+
343
+ ### Triaging a log file
344
+
345
+ Logs are the largest thing in a bundle and the easiest to skim badly. Three moves, in order.
346
+
347
+ The same three moves apply to logs a user pastes into the conversation, with no bundle involved —
348
+ read them directly rather than reaching for the commands. Check the format before filtering, group
349
+ by message shape rather than counting lines, and find the burst before reading line by line. A
350
+ pasted excerpt carries two extra hazards worth naming: it is a *selection*, chosen by someone who
351
+ already had a theory, so the cause may sit in the lines just before what you were given — ask for
352
+ more context around the interesting entry rather than reasoning from the fragment. And it usually
353
+ arrives with no filename, so you do not know which container produced it; ask, because a stack trace
354
+ from an init container and the same trace from the main container mean different things. Where a
355
+ timestamp is present, place it against when the symptom started before treating it as the cause.
356
+
357
+ **1. Check the format before filtering.** Log files are a mix of JSON-per-line and plain text, and
358
+ the same file often contains both. Most files in a bundle are entirely plain text; only the OpenHands
359
+ application services log JSON. A `jq`- or `.severity`-based filter **silently skips every non-JSON
360
+ line**, which is where a lot of real failures live — an uncaught Python exception is printed bare,
361
+ with no `severity` field and often without the word "error" at all.
362
+
363
+ ```bash
364
+ # What am I dealing with? (JSON lines vs total)
365
+ for f in <logs>; do
366
+ total=$(wc -l < "$f"); js=$(grep -c '^{' "$f")
367
+ echo "$((total-js))\tnon-JSON\t$js\tJSON\t$f"
368
+ done | sort -rn
369
+ ```
370
+
371
+ Always make a separate pass over the non-JSON lines:
372
+
373
+ ```bash
374
+ grep -v '^{' app.log | grep -vE '^\s*$' | sort | uniq -c | sort -rn | head -30
375
+ ```
376
+
377
+ **2. Cluster by message shape, not by count.** Raw counts mislead badly: 107 warnings in one
378
+ observed log were a single issue repeated inside a four-second window, and 224 of 228 non-JSON lines
379
+ in another were one repeated message. Normalise the volatile parts (UUIDs, numbers, addresses) and
380
+ count the shapes. Key on the *message*, not a logger or module field — some records have neither.
381
+
382
+ Note the `grep '^{'` in front of every `jq`: without it `jq` hits the first plain-text line and
383
+ aborts with `Invalid numeric literal`, printing nothing. A silent empty result from a file you know
384
+ has content means the filter died, not that the cluster is clean.
385
+
386
+ ```bash
387
+ grep '^{' app.log | jq -r 'select(.severity=="WARNING") | .message' \
388
+ | sed -E 's/[0-9a-f-]{36}/<uuid>/g; s/[0-9]+/<n>/g' \
389
+ | cut -c1-110 | sort | uniq -c | sort -rn
390
+ ```
391
+
392
+ Typical OpenHands service records carry `ts`, `severity`, `message`, `module`, `funcName`, `lineno`.
393
+
394
+ **3. Bucket by time before concluding anything.** A flat rate is usually health probes; incidents are
395
+ bursts. Compare the log's own span against the bundle capture time — steady low-volume traffic across
396
+ hours with one dense cluster tells you where to look, and a count alone does not.
397
+
398
+ ```bash
399
+ grep '^{' app.log | jq -r '.ts[:16]' | sort | uniq -c
400
+ ```
401
+
402
+ Then read the burst, not the file.
403
+
404
+ Three traps:
405
+
406
+ - **Truncation.** Some logs are capped and keep only the *tail*. Find them by looking for files that
407
+ share one exact large size: `find cluster-resources/pods/logs -type f -size +1000000c -printf '%s\n' | sort | uniq -c`.
408
+ Absence of an early error in a capped file proves nothing.
409
+ - **Empty ≠ silent.** Pods that never started still get 2-byte `.log` files. Cross-check
410
+ `.status.phase` before concluding a pod logged nothing.
411
+ - **Missing pods.** Succeeded Job pods and occasionally a Running pod have no log directory at all.
412
+
413
+ ```bash
414
+ # Pods with no logs at all
415
+ comm -23 \
416
+ <(jq -r '.items[].metadata.name' cluster-resources/pods/openhands.json | sort) \
417
+ <(ls cluster-resources/pods/logs/openhands | sort)
418
+ ```
419
+
420
+ ## Redaction
421
+
422
+ Redacted values appear as `***HIDDEN***`. The redactor is heuristic and **over-redacts**: it hides
423
+ pod IPs, CIDRs inside `NO_PROXY`, and non-secret values that merely pattern-match.
424
+
425
+ `***HIDDEN***` therefore means "a value exists here that was redacted" — **not** "this is a secret"
426
+ and **not** "this is unset". You can always confirm that an env var is set and where its value comes
427
+ from; you cannot always read the value.
428
+
429
+ Over-redaction does not imply completeness. The redactor is pattern-based, so identifying data it
430
+ has no pattern for — hostnames, user and installation identifiers, internal URLs — routinely
431
+ survives into the bundle. Treat the archive as confidential regardless of how much of it is masked,
432
+ and redact identifiers yourself before quoting bundle contents anywhere public.
433
+
434
+ ## Config and Helm values
435
+
436
+ **Not in the bundle:** rendered Helm values, `sh.helm.release.v1.*` secrets, and the KOTS
437
+ `ConfigValues` object holding the config answers. A grep for `ConfigValues` returns nothing.
438
+
439
+ **In the bundle**, inside the KOTS app archive:
440
+
441
+ ```bash
442
+ tar -xf "$(find kots -name '*.tar')" -C /tmp/kotsapp && ls /tmp/kotsapp
443
+ ```
444
+
445
+ - `openhands.yaml` — the KOTS `HelmChart` CR with the full `spec.values`, but **still templated**:
446
+ hundreds of unrendered `repl{{ ConfigOption "…" }}` expressions. You get the rule, not the input.
447
+ - `config.yaml` — the KOTS `Config` *schema*, with no answers.
448
+ - `<chart>-<version>.tgz` — the complete chart and subcharts, including every default `values.yaml`.
449
+ - `kots/admin_console/license.yaml` — license and entitlements.
450
+
451
+ **Backing out the effective config.** Because the live objects hold the *rendered* result, you can
452
+ solve most template expressions backwards: read the template in `openhands.yaml`, then find the
453
+ corresponding rendered value in the live object.
454
+
455
+ | ConfigOption | Evidence | Inferred |
456
+ |---|---|---|
457
+ | `hostname_mode`, `base_domain` | `WEB_HOST` matches the `app.{{base_domain}}` branch | `derive` + the base domain |
458
+ | `postgres_type` | `DB_HOST` ≠ the chart's internal service name | `external_postgres` |
459
+ | `external_postgres_host`, `_ssl_mode` | `DB_HOST`, `DB_SSL_MODE` | direct read |
460
+ | `log_level` | `LOG_LEVEL` | direct read |
461
+ | `proxy_enabled`, `https_proxy` | `HTTPS_PROXY` | on + the proxy URL |
462
+ | `ssl_verify` | `SSL_VERIFY=False` | disabled |
463
+ | `runtime_routing_mode` | `RUNTIME_ROUTING_MODE` | direct read |
464
+ | `custom_sandbox_image_*` | `AGENT_SERVER_IMAGE_REPOSITORY` + `_TAG` | registry/repo/tag |
465
+ | `sandbox_cpu_limit`, `_memory_limit` | a `runtime-*` pod's `.spec.containers[0].resources` | direct read |
466
+
467
+ Where it breaks: any option whose rendered output the redactor hides, and every `*_password` /
468
+ `*_secret` / `*_api_key`. Those are not recoverable — read them from your own config instead.
469
+
470
+ Useful for the auth, LLM, and certificate failure modes in `diagnostics.md`, where the question is
471
+ usually "is this configured the way we think it is":
472
+
473
+ ```bash
474
+ NS=openhands
475
+
476
+ # All env vars on a deployment, resolving valueFrom references
477
+ jq -r '.items[] | select(.metadata.name=="openhands")
478
+ | .spec.template.spec.containers[0].env[]
479
+ | "\(.name)\t\(
480
+ if .value then .value
481
+ elif .valueFrom.secretKeyRef then "<secret \(.valueFrom.secretKeyRef.name)/\(.valueFrom.secretKeyRef.key)>"
482
+ elif .valueFrom.configMapKeyRef then "<cm \(.valueFrom.configMapKeyRef.name)/\(.valueFrom.configMapKeyRef.key)>"
483
+ else "<other>" end)"' cluster-resources/deployments/$NS.json | column -t -s $'\t'
484
+
485
+ # Not every workload is a Deployment, and StatefulSet capture is version-dependent.
486
+ # Keycloak, redis and rqlite are StatefulSets; older bundle specs omit them
487
+ # entirely, so an absent statefulsets/ means "not collected", not "not deployed".
488
+ # Fall back to the pod list, which is always present.
489
+ ls cluster-resources/statefulsets/ 2>/dev/null || \
490
+ jq -r '.items[].metadata.name' cluster-resources/pods/$NS.json
491
+
492
+ # LiteLLM model config
493
+ jq -r '.items[] | select(.metadata.name=="openhands-litellm-config") | .data["config.yaml"]' \
494
+ cluster-resources/configmaps/$NS.json
495
+
496
+ # Secret keys (values redacted, but the keys tell you the wiring)
497
+ ls secrets/$NS/
498
+
499
+ # Ingress overview — hosts and TLS wiring for certificate issues
500
+ jq -r '.items[] | "\(.metadata.name)\t\(.spec.ingressClassName)\t\([.spec.rules[].host]|join(","))"' \
501
+ cluster-resources/ingress/$NS.json | column -t -s $'\t'
502
+ ```
503
+
504
+ Also diff the **pod** against the **deployment**. If a rollout never completed, the running
505
+ container's env differs from the deployment spec, and only the pod tells you what is actually live.
506
+
507
+ ## Known gaps
508
+
509
+ These are the things a bundle cannot tell you. Say so explicitly rather than inferring.
510
+
511
+ - **No node-scoped events.** No event of `kind: Node` exists anywhere. `SystemOOM`, `NodeNotReady`,
512
+ and kubelet eviction events are absent. Node-level incidents must be inferred from correlated pod
513
+ restart timestamps or the k0s journald logs.
514
+ - **No `describe` output** for anything. Reconstruct from JSON, which has strictly more — minus events.
515
+ - **One instant only.** `node-metrics` is a single sample. No history, so "was it spiking an hour
516
+ ago" is unanswerable. Partial mitigation:
517
+ `kots/admin_console/kotsadm/*/kotsadm/tmp/last-preflight-result/` holds a second full snapshot
518
+ from the last preflight run, giving you two points to compare.
519
+ - **Events are TTL-limited** (see above).
520
+ - **Collector-error paths are usually directories.** `collector-errors/` and `secrets-errors/` are
521
+ directories recording collector failures, not data — but the `-errors` suffix is not a reliable
522
+ signal on its own: some genuine `*-errors.json` files exist (for example under `kots/goldpinger/`).
523
+ Stat the path rather than assuming from the name.
524
+
525
+ ## When the bundle looks clean but the user has a problem
526
+
527
+ This is the normal hard case, not an edge case. Nobody sends a support bundle because things are
528
+ working, so "no findings" means the failure is somewhere the cluster-resources objects do not
529
+ reach — not that the install is fine. What a clean triage run has actually ruled out is narrow:
530
+ pod objects, analyzer verdicts, node conditions, and resource totals.
531
+
532
+ What it has *not* looked at, roughly in order of how often it pays off:
533
+
534
+ 1. **Application logs.** The script reads none. A component can be `Running`, `1/1 Ready`, zero
535
+ restarts, and failing every request. Start with the logs for whatever the user's symptom points
536
+ at, and see the log triage section above — a severity filter that silently skips non-JSON lines
537
+ will show you nothing on a file full of errors.
538
+ 2. **Anything outside the capture window.** Events have a short TTL and the bundle is one instant.
539
+ An incident that resolved before capture leaves almost nothing behind; `lastState`, restart
540
+ counts, and pod age are the only real memory the bundle has.
541
+ 3. **Config, not runtime.** A wrong secret, a bad URL, a disabled feature flag, an expired
542
+ licence — all render as a healthy pod. Check the ConfigMaps and the app config, and remember
543
+ `***HIDDEN***` means redacted, not unset.
544
+ 4. **Whatever the collectors missed.** Check `collector-errors/` before concluding a subsystem is
545
+ healthy: an empty or absent file is indistinguishable from a clean one, and a failed collector
546
+ produces exactly that.
547
+ 5. **Off-cluster.** Ingress, DNS, TLS, an external database, an egress proxy, the licence endpoint.
548
+ Nothing outside the cluster appears in these objects at all.
549
+
550
+ If you find nothing, say so plainly and ask for what would actually settle it — the exact user-facing
551
+ symptom, a timestamp, and the component involved. "The bundle looks healthy" is an unhelpful answer
552
+ on its own, and a confident wrong diagnosis drawn from a clean bundle is worse than none.
553
+
554
+ ## Anti-patterns
555
+
556
+ - Concluding "no OOM" from the pod objects alone — check events and `analysis.json` too, and state
557
+ that the events window is short.
558
+ - Reading a simultaneous mass restart as a crash loop.
559
+ - Trusting file mtimes for timing.
560
+ - Treating `***HIDDEN***` as "secret" or "unset".
561
+ - Counting the symlink farms as additional log coverage.
562
+ - Reporting "the pod logged nothing" from an empty 2-byte file.
563
+ - Assuming a Pending pod is resource-starved without reading the `PodScheduled` condition message —
564
+ and then assuming `PodScheduled` is where the answer lives. **Pending does not mean unschedulable.**
565
+ The common case in practice is a pod that scheduled fine and is stuck starting
566
+ (`CreateContainerConfigError`, `ImagePullBackOff`), which has a `nodeName` and `PodScheduled=True`;
567
+ its reason is in `.status.initContainerStatuses[].state.waiting` / `containerStatuses[]`, not in
568
+ the conditions. Check both.
569
+ - Drawing platform-health conclusions from pod counts inflated by `runtime-*` sandboxes.
570
+ - Running a severity filter over a log without first checking whether the file is even JSON.