@galaxyproject/orbit 0.0.0 → 0.9.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 (451) hide show
  1. package/CHANGELOG.md +123 -0
  2. package/LICENSE +21 -0
  3. package/README.md +760 -5
  4. package/bin/legacy-mcp-config.js +363 -0
  5. package/bin/orbit.js +810 -0
  6. package/bin/pi-extension-path.d.ts +4 -0
  7. package/bin/pi-extension-path.js +44 -0
  8. package/bin/provider-auth.d.ts +22 -0
  9. package/bin/provider-auth.js +77 -0
  10. package/bin/thinking-pref.js +38 -0
  11. package/bin/update-check.d.ts +15 -0
  12. package/bin/update-check.js +137 -0
  13. package/extensions/cli-update/index.ts +32 -0
  14. package/extensions/orbit/activity-hooks.ts +141 -0
  15. package/extensions/orbit/activity.ts +116 -0
  16. package/extensions/orbit/agent-dir.ts +11 -0
  17. package/extensions/orbit/auto-resume.ts +352 -0
  18. package/extensions/orbit/config.ts +3 -0
  19. package/extensions/orbit/confusables.ts +1 -0
  20. package/extensions/orbit/context.ts +957 -0
  21. package/extensions/orbit/dashboard-commands.ts +191 -0
  22. package/extensions/orbit/dashboard-store.ts +473 -0
  23. package/extensions/orbit/dashboard-tools.ts +1185 -0
  24. package/extensions/orbit/empty-completion.ts +99 -0
  25. package/extensions/orbit/evidence-gate.ts +1276 -0
  26. package/extensions/orbit/evidence-override-command.ts +294 -0
  27. package/extensions/orbit/exec-guard/bash-risk.ts +1408 -0
  28. package/extensions/orbit/exec-guard/gate.ts +181 -0
  29. package/extensions/orbit/exec-guard/guardian-config.ts +54 -0
  30. package/extensions/orbit/exec-guard/index.ts +1 -0
  31. package/extensions/orbit/exec-guard/model-tier.ts +36 -0
  32. package/extensions/orbit/exec-guard/path-jail.ts +54 -0
  33. package/extensions/orbit/exec-guard/policy.ts +342 -0
  34. package/extensions/orbit/exec-guard/runtime-state.ts +15 -0
  35. package/extensions/orbit/exec-guard/sensitive-read.ts +525 -0
  36. package/extensions/orbit/exec-guard/types.ts +42 -0
  37. package/extensions/orbit/execution-commands.ts +134 -0
  38. package/extensions/orbit/feedback-command.ts +81 -0
  39. package/extensions/orbit/feedback.ts +96 -0
  40. package/extensions/orbit/galaxy-api.test.ts +122 -0
  41. package/extensions/orbit/galaxy-api.ts +606 -0
  42. package/extensions/orbit/galaxy-artifact-link-guidance.ts +5 -0
  43. package/extensions/orbit/galaxy-destructive-gate.ts +113 -0
  44. package/extensions/orbit/galaxy-enrich.ts +1108 -0
  45. package/extensions/orbit/galaxy-failure.ts +101 -0
  46. package/extensions/orbit/galaxy-fixture.ts +126 -0
  47. package/extensions/orbit/galaxy-history-guard.ts +233 -0
  48. package/extensions/orbit/galaxy-job-block.ts +347 -0
  49. package/extensions/orbit/galaxy-job-ops.ts +288 -0
  50. package/extensions/orbit/galaxy-live-source.ts +929 -0
  51. package/extensions/orbit/galaxy-markdown-adapter.ts +248 -0
  52. package/extensions/orbit/galaxy-op-policy.ts +332 -0
  53. package/extensions/orbit/galaxy-ops-tools.ts +326 -0
  54. package/extensions/orbit/galaxy-output-store.ts +80 -0
  55. package/extensions/orbit/galaxy-output.ts +346 -0
  56. package/extensions/orbit/galaxy-page-binding.ts +222 -0
  57. package/extensions/orbit/galaxy-page-markdown-guidance.ts +15 -0
  58. package/extensions/orbit/galaxy-page-ops.ts +159 -0
  59. package/extensions/orbit/galaxy-page-sync.test.ts +417 -0
  60. package/extensions/orbit/galaxy-page-sync.ts +318 -0
  61. package/extensions/orbit/galaxy-pages-api.ts +203 -0
  62. package/extensions/orbit/galaxy-pages-sync.ts +409 -0
  63. package/extensions/orbit/galaxy-poll-guard.ts +189 -0
  64. package/extensions/orbit/galaxy-poller.ts +706 -0
  65. package/extensions/orbit/galaxy-provenance.ts +623 -0
  66. package/extensions/orbit/galaxy-reconcile.ts +956 -0
  67. package/extensions/orbit/galaxy-recovery.ts +176 -0
  68. package/extensions/orbit/galaxy-repeat-guard.ts +55 -0
  69. package/extensions/orbit/galaxy-submission-capture.ts +707 -0
  70. package/extensions/orbit/galaxy-submission.ts +502 -0
  71. package/extensions/orbit/galaxy-tool-source.ts +78 -0
  72. package/extensions/orbit/galaxy-tool-wrap.ts +234 -0
  73. package/extensions/orbit/galaxy-udt-block.ts +127 -0
  74. package/extensions/orbit/galaxy-upload-tus.ts +334 -0
  75. package/extensions/orbit/galaxy-upload.ts +300 -0
  76. package/extensions/orbit/galaxy-version-capture.ts +65 -0
  77. package/extensions/orbit/git.ts +173 -0
  78. package/extensions/orbit/harness-block-fields.ts +597 -0
  79. package/extensions/orbit/index.ts +593 -0
  80. package/extensions/orbit/init-gate.ts +284 -0
  81. package/extensions/orbit/instructions-command.ts +136 -0
  82. package/extensions/orbit/invocation-failure-hint.ts +57 -0
  83. package/extensions/orbit/iwc-plan-check.ts +94 -0
  84. package/extensions/orbit/lesson-command.ts +303 -0
  85. package/extensions/orbit/lesson-hint.ts +245 -0
  86. package/extensions/orbit/lesson-nudge.ts +106 -0
  87. package/extensions/orbit/lessons/compose.ts +177 -0
  88. package/extensions/orbit/lessons/counters.ts +120 -0
  89. package/extensions/orbit/lessons/enabled.ts +116 -0
  90. package/extensions/orbit/lessons/matcher.ts +346 -0
  91. package/extensions/orbit/lessons/paths.ts +286 -0
  92. package/extensions/orbit/lessons/pi-event-contract.ts +105 -0
  93. package/extensions/orbit/lessons/propose-replay.ts +103 -0
  94. package/extensions/orbit/lessons/propose-tool.ts +222 -0
  95. package/extensions/orbit/lessons/propose.ts +502 -0
  96. package/extensions/orbit/lessons/replay.ts +89 -0
  97. package/extensions/orbit/lessons/reproduction-index.ts +83 -0
  98. package/extensions/orbit/lessons/search-tool.ts +99 -0
  99. package/extensions/orbit/lessons/search.ts +136 -0
  100. package/extensions/orbit/lessons/store.ts +415 -0
  101. package/extensions/orbit/lessons/text.ts +36 -0
  102. package/extensions/orbit/lessons/types.ts +69 -0
  103. package/extensions/orbit/lessons/user-dir.ts +24 -0
  104. package/extensions/orbit/lessons/user-lesson.ts +88 -0
  105. package/extensions/orbit/lessons/wrapper.ts +61 -0
  106. package/extensions/orbit/lessons-command.ts +147 -0
  107. package/extensions/orbit/local-exec.ts +39 -0
  108. package/extensions/orbit/mcp-output.ts +179 -0
  109. package/extensions/orbit/mcp-servers.test.ts +24 -0
  110. package/extensions/orbit/mcp-servers.ts +25 -0
  111. package/extensions/orbit/model-script.ts +351 -0
  112. package/extensions/orbit/notebook-anchors.ts +342 -0
  113. package/extensions/orbit/notebook-writer.ts +863 -0
  114. package/extensions/orbit/observation-allowlists.ts +1109 -0
  115. package/extensions/orbit/observation-replay.ts +148 -0
  116. package/extensions/orbit/observation-triggers.ts +563 -0
  117. package/extensions/orbit/observation-ui.ts +175 -0
  118. package/extensions/orbit/observations-command.ts +421 -0
  119. package/extensions/orbit/observations-config.ts +200 -0
  120. package/extensions/orbit/observations.ts +1381 -0
  121. package/extensions/orbit/profiles.ts +270 -0
  122. package/extensions/orbit/progress-updates.ts +95 -0
  123. package/extensions/orbit/proposal-commands.ts +882 -0
  124. package/extensions/orbit/provenance-fs-file.ts +59 -0
  125. package/extensions/orbit/provenance-fs.ts +32 -0
  126. package/extensions/orbit/provenance-memory-fs.ts +59 -0
  127. package/extensions/orbit/record-store-file.ts +177 -0
  128. package/extensions/orbit/record-store-page.ts +77 -0
  129. package/extensions/orbit/record-store-seam.ts +47 -0
  130. package/extensions/orbit/record-store.ts +149 -0
  131. package/extensions/orbit/registry-carrier.ts +107 -0
  132. package/extensions/orbit/registry-eligibility.ts +95 -0
  133. package/extensions/orbit/registry-evaluation.ts +403 -0
  134. package/extensions/orbit/registry-evaluator.ts +519 -0
  135. package/extensions/orbit/registry-import.ts +91 -0
  136. package/extensions/orbit/registry-lock.ts +221 -0
  137. package/extensions/orbit/registry-memory-fs.ts +42 -0
  138. package/extensions/orbit/registry-page-carrier.ts +103 -0
  139. package/extensions/orbit/registry-proposal.ts +1348 -0
  140. package/extensions/orbit/registry-runtime.ts +234 -0
  141. package/extensions/orbit/registry-schema.ts +735 -0
  142. package/extensions/orbit/registry-submitter.ts +59 -0
  143. package/extensions/orbit/registry-templates.ts +346 -0
  144. package/extensions/orbit/registry.ts +476 -0
  145. package/extensions/orbit/sandbox/index.ts +120 -0
  146. package/extensions/orbit/sandbox/sandbox-bash.ts +93 -0
  147. package/extensions/orbit/sandbox/sandbox-config.ts +87 -0
  148. package/extensions/orbit/sandbox/sandbox-init-message.ts +28 -0
  149. package/extensions/orbit/secret-redaction.ts +247 -0
  150. package/extensions/orbit/session-index/cwd.ts +18 -0
  151. package/extensions/orbit/session-index/db.ts +56 -0
  152. package/extensions/orbit/session-index/indexer.ts +154 -0
  153. package/extensions/orbit/session-index/is-enabled.ts +17 -0
  154. package/extensions/orbit/session-index/parse.ts +254 -0
  155. package/extensions/orbit/session-index/query.ts +209 -0
  156. package/extensions/orbit/session-index/schema.ts +72 -0
  157. package/extensions/orbit/session-index/tools.ts +152 -0
  158. package/extensions/orbit/session-lifecycle.ts +321 -0
  159. package/extensions/orbit/skill-triggers.ts +159 -0
  160. package/extensions/orbit/skills-command.ts +48 -0
  161. package/extensions/orbit/skills-discovery.ts +435 -0
  162. package/extensions/orbit/skills.ts +84 -0
  163. package/extensions/orbit/sra-import-gate.ts +77 -0
  164. package/extensions/orbit/state.ts +265 -0
  165. package/extensions/orbit/submission-replay.ts +147 -0
  166. package/extensions/orbit/sync-command.ts +157 -0
  167. package/extensions/orbit/teams/critic-parser.ts +93 -0
  168. package/extensions/orbit/teams/dispatcher.ts +210 -0
  169. package/extensions/orbit/teams/is-enabled.ts +22 -0
  170. package/extensions/orbit/teams/tool.ts +187 -0
  171. package/extensions/orbit/teams/types.ts +57 -0
  172. package/extensions/orbit/teams/validate.ts +42 -0
  173. package/extensions/orbit/tester-id-command.ts +133 -0
  174. package/extensions/orbit/tools-sync.ts +279 -0
  175. package/extensions/orbit/tools.ts +1300 -0
  176. package/extensions/orbit/types.ts +41 -0
  177. package/extensions/orbit/ui-bridge.ts +55 -0
  178. package/extensions/orbit/ulid.ts +60 -0
  179. package/extensions/orbit/user-instructions.ts +388 -0
  180. package/extensions/orbit/vendor/skills/_catalog.json +68 -0
  181. package/extensions/orbit/vendor/skills/_manifest.json +1106 -0
  182. package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/SKILL.md +112 -0
  183. package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/_provenance.json +218 -0
  184. package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/_required_tools.json +33 -0
  185. package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/_verify.json +4 -0
  186. package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/cli/add.json +50 -0
  187. package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/cli/draft-extract.json +51 -0
  188. package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/cli/draft-next-step.json +42 -0
  189. package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/cli/draft-validate.json +140 -0
  190. package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/cli/list.json +32 -0
  191. package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/notes/galaxy-tool-job-failure-reference.md +137 -0
  192. package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/notes/open-requirements-ledger.md +173 -0
  193. package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/schemas/galaxy-tool-summary.schema.json +175 -0
  194. package/extensions/orbit/vendor/skills/advance-galaxy-draft-step/references/schemas/galaxy-workflow-draft.schema.json +2727 -0
  195. package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/SKILL.md +82 -0
  196. package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/_provenance.json +133 -0
  197. package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/_required_tools.json +16 -0
  198. package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/_verify.json +4 -0
  199. package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/references/cli/draft-validate.json +140 -0
  200. package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/references/notes/galaxy-workflow-draft-format.md +135 -0
  201. package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/references/notes/open-requirements-ledger.md +173 -0
  202. package/extensions/orbit/vendor/skills/apply-galaxy-workflow-changeset/references/schemas/galaxy-workflow-draft.schema.json +2727 -0
  203. package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/SKILL.md +84 -0
  204. package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/_provenance.json +166 -0
  205. package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/_verify.json +29 -0
  206. package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/notes/galaxy-workflow-testability-design.md +136 -0
  207. package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/notes/iwc-shortcuts-anti-patterns.md +357 -0
  208. package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/notes/iwc-test-data-conventions.md +308 -0
  209. package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/notes/planemo-asserts-idioms.md +246 -0
  210. package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/notes/planemo-workflow-test-architecture.md +151 -0
  211. package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/schemas/galaxy-workflow-test-plan.schema.json +552 -0
  212. package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/schemas/summary-galaxy-workflow.schema.json +687 -0
  213. package/extensions/orbit/vendor/skills/changeset-to-galaxy-test-plan/references/schemas/tests-format.schema.json +7328 -0
  214. package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/SKILL.md +127 -0
  215. package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/_provenance.json +198 -0
  216. package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/_required_tools.json +16 -0
  217. package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/_verify.json +4 -0
  218. package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/references/cli/convert.json +123 -0
  219. package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/references/notes/galaxy-data-flow-draft-contract.md +129 -0
  220. package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/references/notes/iwc-shortcuts-anti-patterns.md +357 -0
  221. package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/references/notes/iwc-test-data-conventions.md +308 -0
  222. package/extensions/orbit/vendor/skills/compare-against-iwc-exemplar/references/notes/open-requirements-ledger.md +173 -0
  223. package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/SKILL.md +73 -0
  224. package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/_provenance.json +160 -0
  225. package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/_verify.json +4 -0
  226. package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/galaxy-collection-semantics.md +43 -0
  227. package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/galaxy-collection-semantics.yml +1250 -0
  228. package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/galaxy-tool-job-failure-reference.md +137 -0
  229. package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/galaxy-workflow-invocation-failure-reference.md +138 -0
  230. package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/iwc-shortcuts-anti-patterns.md +357 -0
  231. package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/nextflow-operators-to-galaxy-collection-recipes.md +98 -0
  232. package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/planemo-asserts-idioms.md +246 -0
  233. package/extensions/orbit/vendor/skills/debug-galaxy-workflow-output/references/notes/planemo-workflow-test-architecture.md +151 -0
  234. package/extensions/orbit/vendor/skills/discover-shed-tool/SKILL.md +174 -0
  235. package/extensions/orbit/vendor/skills/discover-shed-tool/_provenance.json +130 -0
  236. package/extensions/orbit/vendor/skills/discover-shed-tool/_required_tools.json +18 -0
  237. package/extensions/orbit/vendor/skills/discover-shed-tool/_verify.json +17 -0
  238. package/extensions/orbit/vendor/skills/discover-shed-tool/references/cli/tool-revisions.json +48 -0
  239. package/extensions/orbit/vendor/skills/discover-shed-tool/references/cli/tool-search.json +95 -0
  240. package/extensions/orbit/vendor/skills/discover-shed-tool/references/cli/tool-versions.json +39 -0
  241. package/extensions/orbit/vendor/skills/discover-shed-tool/references/notes/component-tool-shed-search.md +230 -0
  242. package/extensions/orbit/vendor/skills/discover-shed-tool/references/schemas/galaxy-tool-discovery.schema.json +231 -0
  243. package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/SKILL.md +93 -0
  244. package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/_provenance.json +201 -0
  245. package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/_required_tools.json +28 -0
  246. package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/_verify.json +17 -0
  247. package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/cli/planemo.md +44 -0
  248. package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/cli/validate-tests.json +40 -0
  249. package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/notes/galaxy-workflow-testability-design.md +136 -0
  250. package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/notes/iwc-shortcuts-anti-patterns.md +357 -0
  251. package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/notes/iwc-test-data-conventions.md +308 -0
  252. package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/notes/planemo-asserts-idioms.md +246 -0
  253. package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/notes/planemo-workflow-test-architecture.md +151 -0
  254. package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/schemas/galaxy-workflow-test-plan.schema.json +552 -0
  255. package/extensions/orbit/vendor/skills/implement-galaxy-workflow-test/references/schemas/tests-format.schema.json +7328 -0
  256. package/extensions/orbit/vendor/skills/interview-to-galaxy-workflow-changeset/SKILL.md +81 -0
  257. package/extensions/orbit/vendor/skills/interview-to-galaxy-workflow-changeset/_provenance.json +93 -0
  258. package/extensions/orbit/vendor/skills/interview-to-galaxy-workflow-changeset/_verify.json +17 -0
  259. package/extensions/orbit/vendor/skills/interview-to-galaxy-workflow-changeset/references/notes/open-requirements-ledger.md +173 -0
  260. package/extensions/orbit/vendor/skills/interview-to-galaxy-workflow-changeset/references/schemas/summary-galaxy-workflow.schema.json +687 -0
  261. package/extensions/orbit/vendor/skills/pipeline-update-interview-to-galaxy/SKILL.md +81 -0
  262. package/extensions/orbit/vendor/skills/pipeline-update-interview-to-galaxy/_assembly.json +23 -0
  263. package/extensions/orbit/vendor/skills/run-workflow-test/SKILL.md +79 -0
  264. package/extensions/orbit/vendor/skills/run-workflow-test/_provenance.json +125 -0
  265. package/extensions/orbit/vendor/skills/run-workflow-test/_required_tools.json +28 -0
  266. package/extensions/orbit/vendor/skills/run-workflow-test/_verify.json +4 -0
  267. package/extensions/orbit/vendor/skills/run-workflow-test/references/cli/planemo.md +44 -0
  268. package/extensions/orbit/vendor/skills/run-workflow-test/references/cli/validate-tests.json +40 -0
  269. package/extensions/orbit/vendor/skills/run-workflow-test/references/notes/galaxy-workflow-invocation-failure-reference.md +138 -0
  270. package/extensions/orbit/vendor/skills/run-workflow-test/references/notes/planemo-asserts-idioms.md +246 -0
  271. package/extensions/orbit/vendor/skills/run-workflow-test/references/notes/planemo-workflow-test-architecture.md +151 -0
  272. package/extensions/orbit/vendor/skills/run-workflow-test/references/schemas/tests-format.schema.json +7328 -0
  273. package/extensions/orbit/vendor/skills/skills/LICENSE +21 -0
  274. package/extensions/orbit/vendor/skills/skills/collection-manipulation/README.md +39 -0
  275. package/extensions/orbit/vendor/skills/skills/collection-manipulation/SKILL.md +892 -0
  276. package/extensions/orbit/vendor/skills/skills/collection-manipulation/references/api-patterns.md +927 -0
  277. package/extensions/orbit/vendor/skills/skills/collection-manipulation/references/apply-rules.md +1482 -0
  278. package/extensions/orbit/vendor/skills/skills/collection-manipulation/references/test-patterns.md +996 -0
  279. package/extensions/orbit/vendor/skills/skills/collection-manipulation/references/tools.md +609 -0
  280. package/extensions/orbit/vendor/skills/skills/galaxy-data-libraries/SKILL.md +170 -0
  281. package/extensions/orbit/vendor/skills/skills/galaxy-integration/README.md +162 -0
  282. package/extensions/orbit/vendor/skills/skills/galaxy-integration/SKILL.md +99 -0
  283. package/extensions/orbit/vendor/skills/skills/galaxy-integration/examples/README.md +119 -0
  284. package/extensions/orbit/vendor/skills/skills/galaxy-integration/examples/tool-checking.md +419 -0
  285. package/extensions/orbit/vendor/skills/skills/galaxy-integration/examples/workflow-testing.md +617 -0
  286. package/extensions/orbit/vendor/skills/skills/galaxy-integration/galaxy-integration.md +614 -0
  287. package/extensions/orbit/vendor/skills/skills/galaxy-integration/jupyterlite/SKILL.md +144 -0
  288. package/extensions/orbit/vendor/skills/skills/galaxy-integration/jupyterlite/examples/average_col3.ipynb +68 -0
  289. package/extensions/orbit/vendor/skills/skills/galaxy-integration/jupyterlite/examples/extract_sample_metadata.ipynb +190 -0
  290. package/extensions/orbit/vendor/skills/skills/galaxy-integration/jupyterlite/examples/variant_annotation.ipynb +816 -0
  291. package/extensions/orbit/vendor/skills/skills/galaxy-integration/jupyterlite/examples/vcp_variant_map.ipynb +443 -0
  292. package/extensions/orbit/vendor/skills/skills/galaxy-integration/scripts/README.md +165 -0
  293. package/extensions/orbit/vendor/skills/skills/galaxy-integration/scripts/galaxy_tool_checker.py +521 -0
  294. package/extensions/orbit/vendor/skills/skills/galaxy-mcp-reference/SKILL.md +128 -0
  295. package/extensions/orbit/vendor/skills/skills/galaxy-mcp-reference/gotchas.md +135 -0
  296. package/extensions/orbit/vendor/skills/skills/galaxy-mcp-reference/history-access.md +132 -0
  297. package/extensions/orbit/vendor/skills/skills/reproduciblify/SKILL.md +146 -0
  298. package/extensions/orbit/vendor/skills/skills/reproduciblify/references/directives.yml +496 -0
  299. package/extensions/orbit/vendor/skills/skills/udt-authoring/SKILL.md +245 -0
  300. package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/01-minimal-echo.yml +27 -0
  301. package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/02-single-data-in-out.yml +27 -0
  302. package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/03-multiple-data-js.yml +28 -0
  303. package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/04-select-integer-boolean.yml +41 -0
  304. package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/05-resource-requirements.yml +25 -0
  305. package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/06-biocontainer-tool.yml +38 -0
  306. package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/07-inline-python-script.yml +56 -0
  307. package/extensions/orbit/vendor/skills/skills/udt-authoring/examples/08-configfile-script.yml +74 -0
  308. package/extensions/orbit/vendor/skills/skills/udt-authoring/references/common-mistakes.md +76 -0
  309. package/extensions/orbit/vendor/skills/skills/udt-authoring/references/schema-reference.md +155 -0
  310. package/extensions/orbit/vendor/skills/skills/udt-authoring/references/templating.md +196 -0
  311. package/extensions/orbit/vendor/skills/skills/udt-authoring/scripts/validate.py +109 -0
  312. package/extensions/orbit/vendor/skills/skills/workflow-reports/README.md +71 -0
  313. package/extensions/orbit/vendor/skills/skills/workflow-reports/SKILL.md +190 -0
  314. package/extensions/orbit/vendor/skills/skills/workflow-reports/examples/histology-staining.md +145 -0
  315. package/extensions/orbit/vendor/skills/skills/workflow-reports/examples/tissue-microarray-analysis.md +176 -0
  316. package/extensions/orbit/vendor/skills/skills/workflow-reports/references/directives.md +415 -0
  317. package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/SKILL.md +98 -0
  318. package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/_provenance.json +93 -0
  319. package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/_required_tools.json +28 -0
  320. package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/_verify.json +17 -0
  321. package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/references/cli/convert.json +123 -0
  322. package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/references/cli/foundry.md +25 -0
  323. package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/references/cli/validate.json +142 -0
  324. package/extensions/orbit/vendor/skills/summarize-galaxy-workflow/references/schemas/summary-galaxy-workflow.schema.json +687 -0
  325. package/extensions/orbit/vendor/skills/validate-galaxy-workflow/SKILL.md +74 -0
  326. package/extensions/orbit/vendor/skills/validate-galaxy-workflow/_provenance.json +71 -0
  327. package/extensions/orbit/vendor/skills/validate-galaxy-workflow/_required_tools.json +16 -0
  328. package/extensions/orbit/vendor/skills/validate-galaxy-workflow/_verify.json +4 -0
  329. package/extensions/orbit/vendor/skills/validate-galaxy-workflow/references/cli/validate.json +142 -0
  330. package/extensions/orbit/vendor/skills/validate-galaxy-workflow/references/notes/galaxy-workflow-invocation-failure-reference.md +138 -0
  331. package/extensions/orbit/vendor/skills/validate-galaxy-workflow/references/notes/galaxy-workflow-testability-design.md +136 -0
  332. package/extensions/orbit/vendor-skills.ts +283 -0
  333. package/extensions/orbit/workspace-state-dir.ts +46 -0
  334. package/extensions/orbit-handoff/index.ts +73 -0
  335. package/extensions/orbit-handoff/orbit-launcher.ts +142 -0
  336. package/extensions/whats-new/index.ts +98 -0
  337. package/lessons/LICENSE +17 -0
  338. package/lessons/README.md +117 -0
  339. package/lessons/SCHEMA.md +187 -0
  340. package/lessons/build-snapshot.mjs +311 -0
  341. package/lessons/data/downloaded-file-is-not-what-its-extension-says.md +61 -0
  342. package/lessons/galaxy-api/403-history-is-a-hard-stop.md +57 -0
  343. package/lessons/galaxy-api/collection-into-single-dataset-input.md +61 -0
  344. package/lessons/galaxy-api/connectedvalue-in-command-line.md +62 -0
  345. package/lessons/galaxy-api/hid-is-not-an-id.md +59 -0
  346. package/lessons/galaxy-api/invoke-workflow-inputs-not-params.md +60 -0
  347. package/lessons/galaxy-api/repeat-param-pipe-keys.md +63 -0
  348. package/lessons/galaxy-api/run-tool-returns-on-submit.md +61 -0
  349. package/lessons/galaxy-tools/reference-index-not-on-server.md +62 -0
  350. package/lessons/log.md +42 -0
  351. package/lessons/reproduction/input-population-mismatch.md +59 -0
  352. package/lessons/reproduction/methods-text-vs-executed-parameters.md +62 -0
  353. package/lessons/snapshot.json +1004 -0
  354. package/lessons/stats/de-contrast-direction-and-sample-labels.md +63 -0
  355. package/lessons/stats/na-coerced-to-zero-in-filters.md +59 -0
  356. package/lessons/validate.mjs +1219 -0
  357. package/package.json +103 -5
  358. package/shared/agent-prompt.d.ts +8 -0
  359. package/shared/agent-prompt.js +15 -0
  360. package/shared/approval-prompt.d.ts +4 -0
  361. package/shared/approval-prompt.js +61 -0
  362. package/shared/brain-env.d.ts +14 -0
  363. package/shared/brain-env.js +108 -0
  364. package/shared/brain-exit.d.ts +20 -0
  365. package/shared/brain-exit.js +149 -0
  366. package/shared/capability-profile.d.ts +16 -0
  367. package/shared/capability-profile.js +75 -0
  368. package/shared/config-source.d.ts +13 -0
  369. package/shared/config-source.js +17 -0
  370. package/shared/confusables.d.ts +3 -0
  371. package/shared/confusables.js +93 -0
  372. package/shared/custom-provider.d.ts +45 -0
  373. package/shared/custom-provider.js +169 -0
  374. package/shared/dashboard-contract.d.ts +123 -0
  375. package/shared/dashboard-contract.js +560 -0
  376. package/shared/dashboard-layout-store.d.ts +44 -0
  377. package/shared/dashboard-layout-store.js +281 -0
  378. package/shared/feedback-contract.d.ts +61 -0
  379. package/shared/feedback-contract.js +146 -0
  380. package/shared/galaxy-artifact-links.d.ts +30 -0
  381. package/shared/galaxy-artifact-links.js +166 -0
  382. package/shared/galaxy-credentials.d.ts +26 -0
  383. package/shared/galaxy-credentials.js +71 -0
  384. package/shared/galaxy-destructive.d.ts +21 -0
  385. package/shared/galaxy-destructive.js +269 -0
  386. package/shared/galaxy-guidance.d.ts +2 -0
  387. package/shared/galaxy-guidance.js +53 -0
  388. package/shared/galaxy-live-contract.d.ts +92 -0
  389. package/shared/galaxy-live-contract.js +180 -0
  390. package/shared/galaxy-page-cells.d.ts +2 -0
  391. package/shared/galaxy-page-cells.js +22 -0
  392. package/shared/galaxy-tool-family.d.ts +5 -0
  393. package/shared/galaxy-tool-family.js +27 -0
  394. package/shared/galaxy-url.d.ts +1 -0
  395. package/shared/galaxy-url.js +48 -0
  396. package/shared/gtn.d.ts +31 -0
  397. package/shared/gtn.js +253 -0
  398. package/shared/handled-prompt.d.ts +3 -0
  399. package/shared/handled-prompt.js +26 -0
  400. package/shared/known-provider.d.ts +11 -0
  401. package/shared/known-provider.js +89 -0
  402. package/shared/lesson-hint-marker.d.ts +7 -0
  403. package/shared/lesson-hint-marker.js +32 -0
  404. package/shared/lesson-rules.d.ts +150 -0
  405. package/shared/lesson-rules.js +203 -0
  406. package/shared/malformed-object-ids.d.ts +1 -0
  407. package/shared/malformed-object-ids.js +37 -0
  408. package/shared/markdown-sections.d.ts +10 -0
  409. package/shared/markdown-sections.js +118 -0
  410. package/shared/notebook-fences.d.ts +12 -0
  411. package/shared/notebook-fences.js +53 -0
  412. package/shared/observation-contract.d.ts +91 -0
  413. package/shared/observation-contract.js +544 -0
  414. package/shared/orbit-config.d.ts +226 -0
  415. package/shared/orbit-config.js +189 -0
  416. package/shared/orbit-env.d.ts +13 -0
  417. package/shared/orbit-env.js +124 -0
  418. package/shared/orbit-shell-contract.d.ts +74 -0
  419. package/shared/orbit-shell-contract.js +30 -0
  420. package/shared/page-content-hash.d.ts +1 -0
  421. package/shared/page-content-hash.js +13 -0
  422. package/shared/pi-default-model.d.ts +7 -0
  423. package/shared/pi-default-model.js +41 -0
  424. package/shared/prompt-blocks.d.ts +87 -0
  425. package/shared/prompt-blocks.js +898 -0
  426. package/shared/provider-auth-caps.d.ts +22 -0
  427. package/shared/provider-auth-caps.js +57 -0
  428. package/shared/random.d.ts +2 -0
  429. package/shared/random.js +15 -0
  430. package/shared/record-excerpt.d.ts +4 -0
  431. package/shared/record-excerpt.js +16 -0
  432. package/shared/redirect-guard.d.ts +36 -0
  433. package/shared/redirect-guard.js +235 -0
  434. package/shared/skills-pin.d.ts +8 -0
  435. package/shared/skills-pin.js +9 -0
  436. package/shared/skills-router.d.ts +17 -0
  437. package/shared/skills-router.js +63 -0
  438. package/shared/sra-import.d.ts +34 -0
  439. package/shared/sra-import.js +206 -0
  440. package/shared/state-dir.d.ts +21 -0
  441. package/shared/state-dir.js +240 -0
  442. package/shared/team-dispatch-contract.d.ts +28 -0
  443. package/shared/team-dispatch-contract.js +1 -0
  444. package/shared/truncate-middle.d.ts +11 -0
  445. package/shared/truncate-middle.js +68 -0
  446. package/shared/version-compare.d.ts +10 -0
  447. package/shared/version-compare.js +74 -0
  448. package/shared/whats-new.d.ts +31 -0
  449. package/shared/whats-new.js +83 -0
  450. package/shared/wsl.d.ts +10 -0
  451. package/shared/wsl.js +24 -0
@@ -0,0 +1,1185 @@
1
+ /**
2
+ * Dashboard tools -- how "show me my running jobs next to the plan" becomes a
3
+ * layout.
4
+ *
5
+ * Two tools: one reads the layout document, one changes it. Both are
6
+ * shell-neutral. The document is the same JSON the shells persist, validated by
7
+ * the same `shared/dashboard-contract` validator, and writing it IS the
8
+ * transport (see `dashboard-store.ts`) -- there is no separate widget message
9
+ * for layout, and in the CLI the write simply happens with nothing attached to
10
+ * read it.
11
+ *
12
+ * Two rules shape everything here, both from the curation design:
13
+ *
14
+ * 1. The agent changes the dashboard when the user asks, and never on its own
15
+ * initiative. That is stated in the tool description, where the model
16
+ * reads it.
17
+ * 2. A panel the user placed or pinned is not the agent's to remove, move or
18
+ * rewrite. `provenanceViolations` enforces that on the finished document,
19
+ * so it holds for a wholesale replace exactly as it does for a patch.
20
+ */
21
+
22
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
23
+ import { Text } from "@earendil-works/pi-tui";
24
+ import { Type } from "@sinclair/typebox";
25
+ import * as path from "node:path";
26
+ import {
27
+ DASHBOARD_PRESETS,
28
+ KNOWN_WIDGET_TYPES,
29
+ MAX_DASHBOARDS,
30
+ MAX_PANELS,
31
+ dashboardFromPreset,
32
+ parseDashboardDocument,
33
+ validateDashboardDocument,
34
+ } from "../../shared/dashboard-contract.js";
35
+ import type {
36
+ Dashboard,
37
+ DashboardDocument,
38
+ DashboardPanel,
39
+ DashboardProblem,
40
+ } from "../../shared/dashboard-contract.js";
41
+ import { appendActivityEvent } from "./activity";
42
+ import { readDashboardDocument, updateDashboardDocument } from "./dashboard-store";
43
+ import { capabilityProfile } from "../../shared/capability-profile.js";
44
+
45
+ /** More than this in one call is a rewrite, and a rewrite should say so. */
46
+ const MAX_ACTIONS = 20;
47
+
48
+ /**
49
+ * How much of the document `dashboard_read` will put in front of the model.
50
+ *
51
+ * The file may be up to 256 KB and a panel config can hold whatever a previous
52
+ * write put there, so returning it whole would let one layout eat the context
53
+ * window. Past this the summary carries the panel ids, which is what a write
54
+ * actually needs.
55
+ */
56
+ export const MAX_READ_CHARS = 20_000;
57
+
58
+ /**
59
+ * And how much of everything else.
60
+ *
61
+ * Swapping the document out for the summary bounds nothing on its own: the
62
+ * summary is built from ids and titles the agent itself writes into the file,
63
+ * and nothing caps their length, so a 261 KB layout came back as a 170,866-
64
+ * character tool result of which 170,014 was summary. The validation problems
65
+ * are worse -- one of them quotes the rejected `activeId` back, so a 240,000-
66
+ * character id becomes a 240,000-character diagnostic about a call that failed.
67
+ *
68
+ * Each of these is one budget spent across its whole field rather than a
69
+ * per-entry cap: twenty problems at a thousand characters each is twenty
70
+ * thousand characters, which is the cap over again.
71
+ */
72
+ const MAX_SUMMARY_CHARS = 4_000;
73
+ const MAX_PROBLEM_CHARS = 2_000;
74
+ const MAX_PROBLEMS = 20;
75
+ const MAX_ERROR_CHARS = 2_000;
76
+ /** No dashboard's line is squeezed below this, however many there are. */
77
+ const MIN_SUMMARY_LINE_CHARS = 200;
78
+
79
+ /** Keep the head of one string, and say how much was dropped. */
80
+ function capText(text: string, max: number): string {
81
+ if (text.length <= max) return text;
82
+ return `${text.slice(0, max)}... (${text.length - max} more characters)`;
83
+ }
84
+
85
+ /** Keep whole lines until the budget runs out, then say how many were dropped. */
86
+ function capLines(lines: string[], budget: number, noun: string): string[] {
87
+ const kept: string[] = [];
88
+ let used = 0;
89
+ for (const line of lines) {
90
+ if (used >= budget) {
91
+ kept.push(`... ${lines.length - kept.length} more ${noun} not shown`);
92
+ return kept;
93
+ }
94
+ const capped = capText(line, budget - used);
95
+ kept.push(capped);
96
+ used += capped.length;
97
+ }
98
+ return kept;
99
+ }
100
+
101
+ /**
102
+ * The summary as the model should see it: every dashboard, and a bound.
103
+ *
104
+ * Every dashboard, deliberately. Spending one budget head-first lets a single
105
+ * crowded dashboard eat all of it and collapses the rest into "9 more not
106
+ * shown" -- and when the document has been omitted too, the response tells the
107
+ * model to work from the summary while `dashboard_read` takes no arguments, so
108
+ * there is no second call that would fetch the rest. Each line gets its share
109
+ * and loses its own tail instead.
110
+ */
111
+ function cappedSummary(document: DashboardDocument): string[] {
112
+ const lines = summarizeDocument(document);
113
+ const share = Math.max(
114
+ MIN_SUMMARY_LINE_CHARS,
115
+ Math.floor(MAX_SUMMARY_CHARS / Math.max(1, lines.length)),
116
+ );
117
+ return capLines(
118
+ lines.map((line) => capText(line, share)),
119
+ MAX_SUMMARY_CHARS,
120
+ "dashboard(s)",
121
+ );
122
+ }
123
+
124
+ /**
125
+ * Validation diagnostics as the model should see them, inside one shared
126
+ * budget. Both fields are capped: a problem's `path` is structural, but its
127
+ * `message` quotes the input.
128
+ */
129
+ function cappedProblems(problems: DashboardProblem[]): DashboardProblem[] {
130
+ const kept: DashboardProblem[] = [];
131
+ let used = 0;
132
+ for (const problem of problems.slice(0, MAX_PROBLEMS)) {
133
+ if (used >= MAX_PROBLEM_CHARS) break;
134
+ const left = MAX_PROBLEM_CHARS - used;
135
+ const path = capText(problem.path, left);
136
+ const message = capText(problem.message, Math.max(0, left - path.length));
137
+ kept.push({ path, message });
138
+ used += path.length + message.length;
139
+ }
140
+ const dropped = problems.length - kept.length;
141
+ if (dropped > 0) kept.push({ path: "", message: `... ${dropped} more problem(s)` });
142
+ return kept;
143
+ }
144
+
145
+ /** Widget types the agent is allowed to create. */
146
+ function creatableWidgetTypes(): string[] {
147
+ return KNOWN_WIDGET_TYPES.slice();
148
+ }
149
+
150
+ /**
151
+ * The widget vocabulary the tool description advertises, derived from the
152
+ * shared contract rather than typed out a second time: the types come from
153
+ * `KNOWN_WIDGET_TYPES` and each one's example config from the first shipped
154
+ * preset that uses it. Anything richer -- a human label, a description of what
155
+ * a widget draws -- lives only in the renderer's registry, which the brain
156
+ * cannot see.
157
+ */
158
+ export function widgetCatalogLines(): string[] {
159
+ const example = new Map<string, string>();
160
+ for (const preset of DASHBOARD_PRESETS) {
161
+ for (const panel of preset.dashboard.panels) {
162
+ if (!example.has(panel.widget)) example.set(panel.widget, JSON.stringify(panel.config));
163
+ }
164
+ }
165
+ return creatableWidgetTypes().map((type) => `${type} (config ${example.get(type) ?? "{}"})`);
166
+ }
167
+
168
+ /** The shipped presets, for the tool description and for `create_dashboard`. */
169
+ export function presetLines(): string[] {
170
+ return DASHBOARD_PRESETS.map((preset) => `${preset.id} -- ${preset.description}`);
171
+ }
172
+
173
+ // ─────────────────────────────────────────────────────────────────────────────
174
+ // Provenance
175
+ // ─────────────────────────────────────────────────────────────────────────────
176
+
177
+ /**
178
+ * A panel the agent must leave alone.
179
+ *
180
+ * `pinned` is the user saying so outright. Absent provenance counts as the
181
+ * user's too: the only panels that arrive without it are hand-written ones, and
182
+ * treating an unlabelled panel as the agent's own would make a hand-edited
183
+ * layout the one case where curation quietly deletes work.
184
+ */
185
+ export function isProtectedPanel(panel: DashboardPanel): boolean {
186
+ if (panel.pinned === true) return true;
187
+ return panel.addedBy !== "agent" && panel.addedBy !== "preset";
188
+ }
189
+
190
+ /** Key-order-independent comparison, so a merged config is not a false change. */
191
+ function stableJson(value: unknown): string {
192
+ if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
193
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
194
+ const entries = Object.keys(value as Record<string, unknown>)
195
+ .sort()
196
+ .map((key) => `${JSON.stringify(key)}:${stableJson((value as Record<string, unknown>)[key])}`);
197
+ return `{${entries.join(",")}}`;
198
+ }
199
+
200
+ function samePanel(a: DashboardPanel, b: DashboardPanel): boolean {
201
+ return (
202
+ a.widget === b.widget &&
203
+ (a.title ?? "") === (b.title ?? "") &&
204
+ a.layout.span === b.layout.span &&
205
+ a.layout.rows === b.layout.rows &&
206
+ a.pinned === b.pinned &&
207
+ a.addedBy === b.addedBy &&
208
+ // `reason` is the user-visible "why is this here". Leaving it out of the
209
+ // comparison let an update_panel rewrite it on a panel the user pinned.
210
+ (a.reason ?? "") === (b.reason ?? "") &&
211
+ stableJson(a.config) === stableJson(b.config)
212
+ );
213
+ }
214
+
215
+ function reasonProtected(panel: DashboardPanel): string {
216
+ if (panel.pinned === true) return "is pinned";
217
+ return panel.addedBy === "user" ? "was placed by the user" : "was not added by the agent";
218
+ }
219
+
220
+ /**
221
+ * What an agent-proposed document would do to panels that are not its to touch.
222
+ *
223
+ * Checked on the finished document rather than per action, so the wholesale
224
+ * replace and the patch path get the same guarantee and a future action cannot
225
+ * quietly escape it. Adding panels is always fine; curation appends.
226
+ */
227
+ export function provenanceViolations(
228
+ before: DashboardDocument,
229
+ after: DashboardDocument,
230
+ ): string[] {
231
+ const violations: string[] = [];
232
+ for (const dashboard of before.dashboards) {
233
+ const protectedPanels = dashboard.panels.filter(isProtectedPanel);
234
+ if (protectedPanels.length === 0) continue;
235
+
236
+ const target = after.dashboards.find((d) => d.id === dashboard.id);
237
+ if (!target) {
238
+ violations.push(
239
+ `dashboard "${dashboard.id}" holds ${protectedPanels.length} panel(s) the user placed or pinned, so it cannot be removed`,
240
+ );
241
+ continue;
242
+ }
243
+
244
+ for (const panel of protectedPanels) {
245
+ const kept = target.panels.find((p) => p.id === panel.id);
246
+ if (!kept) {
247
+ violations.push(`panel "${panel.id}" ${reasonProtected(panel)}, so it cannot be removed`);
248
+ } else if (!samePanel(panel, kept)) {
249
+ violations.push(`panel "${panel.id}" ${reasonProtected(panel)}, so it cannot be changed`);
250
+ }
251
+ }
252
+
253
+ // Relative order of the protected panels, compared as a subsequence: new
254
+ // panels may land between them, but they may not be shuffled past each
255
+ // other. Displacing a panel the user merely placed is allowed -- placing is
256
+ // not pinning.
257
+ const expected = protectedPanels
258
+ .map((p) => p.id)
259
+ .filter((id) => target.panels.some((p) => p.id === id));
260
+ const actual = target.panels.map((p) => p.id).filter((id) => expected.includes(id));
261
+ if (JSON.stringify(expected) !== JSON.stringify(actual)) {
262
+ violations.push(
263
+ `reordering the panels the user placed in "${dashboard.id}" is not the agent's to do`,
264
+ );
265
+ }
266
+
267
+ // A pin, though, holds a panel's place as well as its content.
268
+ //
269
+ // The subsequence check above compares protected panels only against each
270
+ // other, so with one pinned panel and three agent panels it saw the same
271
+ // one-element sequence however far the pinned panel moved: `move_panel`
272
+ // pushed a pinned panel from the top to the bottom and reported success.
273
+ // A pin that does not survive one tool call is not much of a pin, and
274
+ // "keep this where I can see it" is most of why someone pins a panel.
275
+ //
276
+ // So for a pinned panel, the already-existing panels ahead of it must stay
277
+ // the same. New panels may still be inserted anywhere, and unprotected
278
+ // panels may still be removed.
279
+ const surviving = new Set(
280
+ dashboard.panels.map((p) => p.id).filter((id) => target.panels.some((q) => q.id === id)),
281
+ );
282
+ const aheadIn = (panels: { id: string }[], id: string): Set<string> => {
283
+ const ahead = new Set<string>();
284
+ for (const p of panels) {
285
+ if (p.id === id) break;
286
+ if (surviving.has(p.id)) ahead.add(p.id);
287
+ }
288
+ return ahead;
289
+ };
290
+ const sameSet = (a: Set<string>, b: Set<string>): boolean =>
291
+ a.size === b.size && [...a].every((id) => b.has(id));
292
+
293
+ for (const panel of protectedPanels) {
294
+ if (panel.pinned !== true || !surviving.has(panel.id)) continue;
295
+ if (!sameSet(aheadIn(dashboard.panels, panel.id), aheadIn(target.panels, panel.id))) {
296
+ violations.push(`panel "${panel.id}" is pinned, so the agent cannot move it`);
297
+ }
298
+ }
299
+ }
300
+ return violations;
301
+ }
302
+
303
+ /**
304
+ * Take provenance out of the model's hands.
305
+ *
306
+ * `actions` cannot set `addedBy` or `pinned` -- the schema has no field for
307
+ * them -- but a whole-document replace hands the model the raw JSON, and a
308
+ * panel it stamps `addedBy: "user"` or `pinned: true` is a panel it has
309
+ * laundered into one nothing here will ever touch again, including the user's
310
+ * own reading of who put it there. So: a panel that was already in this
311
+ * dashboard keeps exactly the provenance it had, and a panel this write
312
+ * introduces is the agent's, with the agent's reason.
313
+ */
314
+ export function reassertProvenance(
315
+ before: DashboardDocument,
316
+ candidate: DashboardDocument,
317
+ reason: string,
318
+ ): DashboardDocument {
319
+ for (const dashboard of candidate.dashboards) {
320
+ const previous = before.dashboards.find((d) => d.id === dashboard.id);
321
+ for (const panel of dashboard.panels) {
322
+ const existing = previous?.panels.find((p) => p.id === panel.id);
323
+ if (existing && existing.widget === panel.widget) {
324
+ if (existing.addedBy === undefined) delete panel.addedBy;
325
+ else panel.addedBy = existing.addedBy;
326
+ if (existing.pinned === undefined) delete panel.pinned;
327
+ else panel.pinned = existing.pinned;
328
+ // The reason is the one provenance field the agent may legitimately
329
+ // refresh -- it is the sentence the user reads to decide whether the
330
+ // panel belongs there -- but only on a panel that is the agent's to
331
+ // change at all. On a protected one it is the user's words.
332
+ if (isProtectedPanel(existing)) {
333
+ if (existing.reason === undefined) delete panel.reason;
334
+ else panel.reason = existing.reason;
335
+ } else if (panel.reason === undefined && existing.reason !== undefined) {
336
+ panel.reason = existing.reason;
337
+ }
338
+ } else {
339
+ panel.addedBy = "agent";
340
+ panel.reason = reason;
341
+ delete panel.pinned;
342
+ }
343
+ }
344
+ }
345
+ return candidate;
346
+ }
347
+
348
+ /**
349
+ * Widget types this change brings into the document: a panel id the dashboard
350
+ * did not have, or one whose widget type changed. Panels already on disk are
351
+ * left alone even when their type is unknown to this build, because that is how
352
+ * a layout written by a newer build survives being opened by an older one.
353
+ */
354
+ export function introducedWidgetTypes(
355
+ before: DashboardDocument,
356
+ after: DashboardDocument,
357
+ ): string[] {
358
+ const introduced = new Set<string>();
359
+ for (const dashboard of after.dashboards) {
360
+ const previous = before.dashboards.find((d) => d.id === dashboard.id);
361
+ for (const panel of dashboard.panels) {
362
+ const existing = previous?.panels.find((p) => p.id === panel.id);
363
+ if (!existing || existing.widget !== panel.widget) introduced.add(panel.widget);
364
+ }
365
+ }
366
+ return [...introduced];
367
+ }
368
+
369
+ /**
370
+ * What the validator had to throw away to fit its own caps.
371
+ *
372
+ * The build step produces a well-formed document either way -- the action path
373
+ * from typed inputs, the document path from an already-validated one -- so a
374
+ * panel or dashboard that comes out the far side missing was truncated at the
375
+ * 20-dashboard or 40-panel ceiling, not repaired. Counts rather than ids,
376
+ * because the validator also renames duplicates and a by-id comparison would
377
+ * call that a loss.
378
+ *
379
+ * It matters because truncation keeps the FIRST N: an insert at position 0 into
380
+ * a full dashboard pushes the last panel off the end, and the model would
381
+ * otherwise be told its add succeeded.
382
+ */
383
+ export function truncatedByValidation(
384
+ proposed: DashboardDocument,
385
+ validated: DashboardDocument,
386
+ ): string | null {
387
+ if (validated.dashboards.length < proposed.dashboards.length) {
388
+ return `this layout already holds the most dashboards a layout can have (${validated.dashboards.length}), so the new one would not fit`;
389
+ }
390
+ const count = (document: DashboardDocument): number =>
391
+ document.dashboards.reduce((total, d) => total + d.panels.length, 0);
392
+ const kept = count(validated);
393
+ if (kept < count(proposed)) {
394
+ return `that dashboard is full (${kept} panels), so adding to it would push another panel off the end`;
395
+ }
396
+ return null;
397
+ }
398
+
399
+ /**
400
+ * Did the model's document exceed a cap, so that saving it would drop panels?
401
+ *
402
+ * The actions path catches this by comparing the proposal with the validated
403
+ * result, but a whole-document replace is validated on the way in, so there is
404
+ * nothing un-truncated left to compare against -- `commitDashboardChange` ends
405
+ * up comparing the truncated document with itself and finding nothing missing.
406
+ * That is why prepending one panel to a full dashboard reported success and
407
+ * deleted the last one.
408
+ *
409
+ * Tested against the caps rather than by counting survivors, because the
410
+ * validator also drops genuinely malformed panels, and that is a repair to
411
+ * report rather than a refusal to raise.
412
+ */
413
+ export function documentOverflow(documentText: string): string | null {
414
+ let raw: unknown;
415
+ try {
416
+ raw = JSON.parse(documentText);
417
+ } catch {
418
+ return null; // Unparseable text never got this far.
419
+ }
420
+ const dashboards = (raw as { dashboards?: unknown } | null)?.dashboards;
421
+ if (!Array.isArray(dashboards)) return null;
422
+
423
+ if (dashboards.length > MAX_DASHBOARDS) {
424
+ return `Nothing was changed: a layout holds at most ${MAX_DASHBOARDS} dashboards and that one has ${dashboards.length}, so ${dashboards.length - MAX_DASHBOARDS} would have been dropped.`;
425
+ }
426
+ for (const dashboard of dashboards) {
427
+ const panels = (dashboard as { panels?: unknown } | null)?.panels;
428
+ if (!Array.isArray(panels) || panels.length <= MAX_PANELS) continue;
429
+ const id = (dashboard as { id?: unknown })?.id;
430
+ const name = typeof id === "string" ? `"${id}"` : "a dashboard";
431
+ return `Nothing was changed: ${name} holds at most ${MAX_PANELS} panels and that one has ${panels.length}, so ${panels.length - MAX_PANELS} would have been dropped. Remove some first.`;
432
+ }
433
+ return null;
434
+ }
435
+
436
+ /** Refuse a widget type the agent is not allowed to create. */
437
+ export function unsupportedWidgetMessage(type: string): string | null {
438
+ if (creatableWidgetTypes().includes(type)) return null;
439
+ return `"${type}" is not a widget this build can draw. Available: ${creatableWidgetTypes().join(", ")}.`;
440
+ }
441
+
442
+ // ─────────────────────────────────────────────────────────────────────────────
443
+ // Actions
444
+ // ─────────────────────────────────────────────────────────────────────────────
445
+
446
+ /**
447
+ * One change, as the model sent it.
448
+ *
449
+ * Every field is `unknown` on purpose. The schema asks for strings and
450
+ * integers, but nothing between the model and this function enforces that, and
451
+ * a `null` where a string was promised must come back as a refusal the model
452
+ * can read rather than as a TypeError out of the tool.
453
+ */
454
+ export type DashboardAction = {
455
+ action?: unknown;
456
+ dashboardId?: unknown;
457
+ panelId?: unknown;
458
+ widget?: unknown;
459
+ title?: unknown;
460
+ config?: unknown;
461
+ span?: unknown;
462
+ rows?: unknown;
463
+ position?: unknown;
464
+ preset?: unknown;
465
+ };
466
+
467
+ export type ApplyResult =
468
+ | { ok: true; document: DashboardDocument; notes: string[]; problems?: DashboardProblem[] }
469
+ | { ok: false; error: string };
470
+
471
+ function fail(error: string): ApplyResult {
472
+ return { ok: false, error };
473
+ }
474
+
475
+ function cloneDocument(document: DashboardDocument): DashboardDocument {
476
+ return JSON.parse(JSON.stringify(document)) as DashboardDocument;
477
+ }
478
+
479
+ function findDashboard(document: DashboardDocument, id: unknown): Dashboard | null {
480
+ const wanted = asText(id) || document.activeId;
481
+ return document.dashboards.find((d) => d.id === wanted) ?? null;
482
+ }
483
+
484
+ function dashboardIds(document: DashboardDocument): string {
485
+ return document.dashboards.map((d) => d.id).join(", ");
486
+ }
487
+
488
+ function slugify(value: string): string {
489
+ return value
490
+ .toLowerCase()
491
+ .replace(/[^a-z0-9]+/g, "-")
492
+ .replace(/^-+|-+$/g, "")
493
+ .slice(0, 40);
494
+ }
495
+
496
+ function uniquePanelId(dashboard: Dashboard, base: string): string {
497
+ const slug = slugify(base) || "panel";
498
+ if (!dashboard.panels.some((p) => p.id === `p-${slug}`)) return `p-${slug}`;
499
+ let n = 2;
500
+ while (dashboard.panels.some((p) => p.id === `p-${slug}-${n}`)) n++;
501
+ return `p-${slug}-${n}`;
502
+ }
503
+
504
+ function uniqueDashboardId(document: DashboardDocument, base: string): string {
505
+ const slug = slugify(base) || "dashboard";
506
+ if (!document.dashboards.some((d) => d.id === slug)) return slug;
507
+ let n = 2;
508
+ while (document.dashboards.some((d) => d.id === `${slug}-${n}`)) n++;
509
+ return `${slug}-${n}`;
510
+ }
511
+
512
+ /**
513
+ * A model-supplied string field, or "" for anything else.
514
+ *
515
+ * The schema says these are strings, but nothing between the model and here
516
+ * enforces that: a `null` title or a numeric reason arrives as-is and used to
517
+ * take `.trim()` with it, throwing out of `execute` instead of coming back as
518
+ * something the model could correct.
519
+ */
520
+ function asText(value: unknown): string {
521
+ return typeof value === "string" ? value.trim() : "";
522
+ }
523
+
524
+ /** Whether the model supplied this field at all, ignoring how badly. */
525
+ function given(value: unknown): boolean {
526
+ return value !== undefined && value !== null;
527
+ }
528
+
529
+ function asInteger(value: unknown): number | undefined {
530
+ if (typeof value === "number" && Number.isFinite(value)) return Math.round(value);
531
+ if (typeof value === "string" && value.trim() !== "" && Number.isFinite(Number(value))) {
532
+ return Math.round(Number(value));
533
+ }
534
+ return undefined;
535
+ }
536
+
537
+ type ParsedConfig = { ok: true; config: Record<string, unknown> } | { ok: false; error: string };
538
+
539
+ function isConfigObject(value: unknown): value is Record<string, unknown> {
540
+ return value !== null && typeof value === "object" && !Array.isArray(value);
541
+ }
542
+
543
+ function parseConfig(raw: unknown): ParsedConfig {
544
+ if (raw === undefined || raw === null) return { ok: true, config: {} };
545
+ // The schema asks for JSON text because a free-form object parameter is not
546
+ // portable across every provider's function-calling schema, but a model that
547
+ // sends the object itself is being reasonable and should not be punished for
548
+ // it.
549
+ if (isConfigObject(raw)) return { ok: true, config: raw };
550
+ if (typeof raw !== "string") {
551
+ return { ok: false, error: 'config must be a JSON object, e.g. {"follow":false}' };
552
+ }
553
+ if (raw.trim() === "") return { ok: true, config: {} };
554
+ let parsed: unknown;
555
+ try {
556
+ parsed = JSON.parse(raw);
557
+ } catch (err) {
558
+ return {
559
+ ok: false,
560
+ error: `config is not valid JSON: ${err instanceof Error ? err.message : String(err)}`,
561
+ };
562
+ }
563
+ if (!isConfigObject(parsed)) {
564
+ return { ok: false, error: 'config must be a JSON object, e.g. {"follow":false}' };
565
+ }
566
+ return { ok: true, config: parsed };
567
+ }
568
+
569
+ function clampRows(rows: unknown): number {
570
+ const value = asInteger(rows);
571
+ return value === undefined ? 2 : Math.min(6, Math.max(1, value));
572
+ }
573
+
574
+ /**
575
+ * Where an insert lands, clamped to the grid and to the panels the user pinned.
576
+ *
577
+ * `pinned` is the user saying "leave this one alone", and a panel that keeps
578
+ * its contents but loses the top-left slot has not been left alone. So an agent
579
+ * insert never goes above a pinned panel, however the model numbers it. Panels
580
+ * merely placed by the user are not protected this way -- they did not ask for
581
+ * a fixed spot, and a dashboard that can only ever grow at the bottom is worse.
582
+ */
583
+ function clampPosition(position: unknown, panels: DashboardPanel[]): number {
584
+ const floor = panels.reduce((last, panel, i) => (panel.pinned === true ? i + 1 : last), 0);
585
+ const value = asInteger(position);
586
+ if (value === undefined) return panels.length;
587
+ return Math.min(panels.length, Math.max(floor, value));
588
+ }
589
+
590
+ type Located = { dashboard: Dashboard; panel: DashboardPanel; index: number } | { error: string };
591
+
592
+ function locatePanel(document: DashboardDocument, action: DashboardAction): Located {
593
+ const panelId = asText(action.panelId);
594
+ if (!panelId) return { error: `${action.action} needs a panelId.` };
595
+ const wanted = asText(action.dashboardId) || undefined;
596
+ if (wanted && !document.dashboards.some((d) => d.id === wanted)) {
597
+ return { error: `no dashboard "${wanted}". Have: ${dashboardIds(document)}.` };
598
+ }
599
+ // Panel ids are unique per dashboard, not per document, and the shipped
600
+ // presets reuse "p-jobs" and "p-notebook" deliberately. Without the active
601
+ // dashboard first, an unqualified action edits whichever dashboard happens to
602
+ // be first in the file -- one the user is not even looking at.
603
+ const scoped = wanted
604
+ ? document.dashboards.filter((d) => d.id === wanted)
605
+ : [...document.dashboards].sort((a, b) => {
606
+ if (a.id === document.activeId) return -1;
607
+ if (b.id === document.activeId) return 1;
608
+ return 0;
609
+ });
610
+ for (const dashboard of scoped) {
611
+ const index = dashboard.panels.findIndex((p) => p.id === panelId);
612
+ if (index >= 0) return { dashboard, panel: dashboard.panels[index], index };
613
+ }
614
+ return { error: `no panel "${panelId}". Call dashboard_read to see the panel ids.` };
615
+ }
616
+
617
+ /**
618
+ * Apply the agent's actions to a copy of the document.
619
+ *
620
+ * Nothing here enforces provenance or the widget allowlist; both are checked
621
+ * against the finished document by the caller, so the patch path and the
622
+ * wholesale-replace path cannot diverge.
623
+ */
624
+ export function applyDashboardActions(
625
+ document: DashboardDocument,
626
+ actions: DashboardAction[],
627
+ reason: string,
628
+ ): ApplyResult {
629
+ if (!Array.isArray(actions)) return fail("actions must be a list of changes.");
630
+ if (actions.length === 0) return fail("No actions given.");
631
+ if (actions.length > MAX_ACTIONS) {
632
+ return fail(`${actions.length} actions in one call; at most ${MAX_ACTIONS}.`);
633
+ }
634
+
635
+ const next = cloneDocument(document);
636
+ const notes: string[] = [];
637
+
638
+ for (const [index, action] of actions.entries()) {
639
+ const at = `actions[${index}]`;
640
+ if (action === null || typeof action !== "object") {
641
+ return fail(`${at}: expected an object describing one change.`);
642
+ }
643
+ switch (asText(action.action)) {
644
+ case "add_panel": {
645
+ const dashboard = findDashboard(next, action.dashboardId);
646
+ if (!dashboard) {
647
+ return fail(
648
+ `${at}: no dashboard "${asText(action.dashboardId)}". Have: ${dashboardIds(next)}.`,
649
+ );
650
+ }
651
+ const widget = asText(action.widget);
652
+ if (!widget) return fail(`${at}: add_panel needs a widget type.`);
653
+ const config = parseConfig(action.config);
654
+ if (!config.ok) return fail(`${at}: ${config.error}`);
655
+ const panel: DashboardPanel = {
656
+ id: uniquePanelId(dashboard, widget),
657
+ widget,
658
+ config: config.config,
659
+ layout: { span: asInteger(action.span) === 2 ? 2 : 1, rows: clampRows(action.rows) },
660
+ addedBy: "agent",
661
+ reason,
662
+ };
663
+ const title = asText(action.title);
664
+ if (title) panel.title = title;
665
+ const insertAt = clampPosition(action.position, dashboard.panels);
666
+ dashboard.panels.splice(insertAt, 0, panel);
667
+ notes.push(`added ${widget} as "${panel.id}" in "${dashboard.id}"`);
668
+ break;
669
+ }
670
+
671
+ case "remove_panel": {
672
+ const found = locatePanel(next, action);
673
+ if ("error" in found) return fail(`${at}: ${found.error}`);
674
+ found.dashboard.panels.splice(found.index, 1);
675
+ notes.push(`removed "${found.panel.id}" from "${found.dashboard.id}"`);
676
+ break;
677
+ }
678
+
679
+ case "update_panel": {
680
+ const found = locatePanel(next, action);
681
+ if ("error" in found) return fail(`${at}: ${found.error}`);
682
+ if (
683
+ !given(action.widget) &&
684
+ !given(action.title) &&
685
+ !given(action.config) &&
686
+ !given(action.span) &&
687
+ !given(action.rows)
688
+ ) {
689
+ // Reporting "updated" for a call that changed nothing would have the
690
+ // model tell the user their dashboard moved when it did not.
691
+ return fail(`${at}: update_panel needs something to change.`);
692
+ }
693
+ const panel = found.panel;
694
+ const newWidget = asText(action.widget);
695
+ if (newWidget) panel.widget = newWidget;
696
+ if (given(action.title)) {
697
+ const title = asText(action.title);
698
+ if (title) panel.title = title;
699
+ else delete panel.title;
700
+ }
701
+ if (given(action.config)) {
702
+ const config = parseConfig(action.config);
703
+ if (!config.ok) return fail(`${at}: ${config.error}`);
704
+ // Merge: a model changing one setting should not silently drop the
705
+ // rest of a panel's config.
706
+ panel.config = { ...panel.config, ...config.config };
707
+ }
708
+ if (given(action.span)) panel.layout.span = asInteger(action.span) === 2 ? 2 : 1;
709
+ if (given(action.rows)) panel.layout.rows = clampRows(action.rows);
710
+ panel.reason = reason;
711
+ notes.push(`updated "${panel.id}" in "${found.dashboard.id}"`);
712
+ break;
713
+ }
714
+
715
+ case "move_panel": {
716
+ const found = locatePanel(next, action);
717
+ if ("error" in found) return fail(`${at}: ${found.error}`);
718
+ if (asInteger(action.position) === undefined) {
719
+ return fail(`${at}: move_panel needs a position.`);
720
+ }
721
+ const [panel] = found.dashboard.panels.splice(found.index, 1);
722
+ const to = clampPosition(action.position, found.dashboard.panels);
723
+ found.dashboard.panels.splice(to, 0, panel);
724
+ notes.push(`moved "${panel.id}" to position ${to} in "${found.dashboard.id}"`);
725
+ break;
726
+ }
727
+
728
+ case "create_dashboard": {
729
+ const fromPreset = asText(action.preset) || undefined;
730
+ let dashboard: Dashboard;
731
+ if (fromPreset) {
732
+ const preset = dashboardFromPreset(fromPreset);
733
+ if (!preset) {
734
+ return fail(
735
+ `${at}: no preset "${fromPreset}". Have: ${DASHBOARD_PRESETS.map((p) => p.id).join(", ")}.`,
736
+ );
737
+ }
738
+ dashboard = preset;
739
+ } else {
740
+ dashboard = { id: "", title: "", panels: [] };
741
+ }
742
+ const title = asText(action.title) || asText(dashboard.title);
743
+ if (!title) return fail(`${at}: create_dashboard needs a title or a preset.`);
744
+ dashboard.title = title;
745
+ dashboard.id = uniqueDashboardId(next, asText(action.dashboardId) || fromPreset || title);
746
+ next.dashboards.push(dashboard);
747
+ notes.push(`created dashboard "${dashboard.id}"`);
748
+ break;
749
+ }
750
+
751
+ case "switch_dashboard": {
752
+ const dashboard = findDashboard(next, action.dashboardId);
753
+ if (!dashboard) {
754
+ return fail(
755
+ `${at}: no dashboard "${asText(action.dashboardId)}". Have: ${dashboardIds(next)}.`,
756
+ );
757
+ }
758
+ next.activeId = dashboard.id;
759
+ notes.push(`showing "${dashboard.id}"`);
760
+ break;
761
+ }
762
+
763
+ default:
764
+ return fail(
765
+ `${at}: unknown action "${asText(action.action)}". Use add_panel, remove_panel, update_panel, move_panel, create_dashboard or switch_dashboard.`,
766
+ );
767
+ }
768
+ }
769
+
770
+ return { ok: true, document: next, notes };
771
+ }
772
+
773
+ // ─────────────────────────────────────────────────────────────────────────────
774
+ // The write, end to end
775
+ // ─────────────────────────────────────────────────────────────────────────────
776
+
777
+ export type CommitOutcome =
778
+ | {
779
+ ok: true;
780
+ document: DashboardDocument;
781
+ problems: DashboardProblem[];
782
+ notes: string[];
783
+ path: string;
784
+ }
785
+ | { ok: false; error: string; problems?: DashboardProblem[] };
786
+
787
+ type Refusal = { error: string; problems?: DashboardProblem[] };
788
+
789
+ /**
790
+ * Validate the candidate, check it against what it is replacing, and persist it.
791
+ *
792
+ * Shared by the tool's patch path, the tool's replace path and the parts of
793
+ * `/dashboard` that write, so every one of them gets the same refusals. The
794
+ * checks run inside the store's compare-and-swap callback, so they are made
795
+ * against the document as it is on disk at the moment of the write, not against
796
+ * a copy read earlier.
797
+ */
798
+ export async function commitDashboardChange(
799
+ build: (current: DashboardDocument, exists: boolean) => ApplyResult,
800
+ options: { asUser?: boolean; reason?: string } = {},
801
+ ): Promise<CommitOutcome> {
802
+ // `asUser` means a person typed this, so the panels they placed are theirs to
803
+ // discard. It does NOT relax the widget allowlist: whether this build can
804
+ // draw a widget is a fact about the build, not about who asked, and a preset
805
+ // that one day carries a flag-gated widget must not install it through the
806
+ // slash command either.
807
+ const enforce = options.asUser !== true;
808
+ const buildReason = options.reason ?? "";
809
+ // A holder rather than plain locals: the callback below runs inside the
810
+ // store's retry loop, and what it learns has to survive back out here.
811
+ const seen: { problems: DashboardProblem[]; notes: string[]; refusal: Refusal | null } = {
812
+ problems: [],
813
+ notes: [],
814
+ refusal: null,
815
+ };
816
+
817
+ const written = await updateDashboardDocument((current, exists) => {
818
+ seen.problems = [];
819
+ seen.notes = [];
820
+ seen.refusal = null;
821
+
822
+ const built = build(current, exists);
823
+ if (!built.ok) {
824
+ seen.refusal = { error: built.error };
825
+ return { ok: false, error: built.error };
826
+ }
827
+
828
+ const proposed = enforce
829
+ ? reassertProvenance(current, built.document, buildReason)
830
+ : built.document;
831
+ const validated = validateDashboardDocument(proposed);
832
+ if (!validated.ok) {
833
+ seen.refusal = {
834
+ error: "That layout is not a valid dashboard document.",
835
+ problems: validated.problems,
836
+ };
837
+ return { ok: false, error: seen.refusal.error };
838
+ }
839
+ // Repairs the build step already made -- a panel the model's document was
840
+ // missing a widget for, say -- are the model's to hear about, and they are
841
+ // gone by the time the result is re-validated.
842
+ seen.problems = [...(built.problems ?? []), ...validated.problems];
843
+ seen.notes = built.notes;
844
+
845
+ const truncated = truncatedByValidation(proposed, validated.document);
846
+ if (truncated) {
847
+ seen.refusal = {
848
+ error: `Nothing was changed: ${truncated}. Remove something first.`,
849
+ };
850
+ return { ok: false, error: seen.refusal.error };
851
+ }
852
+
853
+ for (const type of introducedWidgetTypes(current, validated.document)) {
854
+ const message = unsupportedWidgetMessage(type);
855
+ if (message) {
856
+ seen.refusal = { error: `${message} Nothing was changed.` };
857
+ return { ok: false, error: seen.refusal.error };
858
+ }
859
+ }
860
+
861
+ if (enforce) {
862
+ const violations = provenanceViolations(current, validated.document);
863
+ if (violations.length > 0) {
864
+ seen.refusal = {
865
+ error: `Nothing was changed: ${violations.join("; ")}. Ask the user to make that change in the dashboard's own controls.`,
866
+ };
867
+ return { ok: false, error: seen.refusal.error };
868
+ }
869
+ }
870
+
871
+ return { ok: true, document: validated.document };
872
+ });
873
+
874
+ if (!written.ok) {
875
+ return seen.refusal
876
+ ? { ok: false, error: seen.refusal.error, problems: seen.refusal.problems }
877
+ : { ok: false, error: written.error };
878
+ }
879
+ return {
880
+ ok: true,
881
+ document: written.document,
882
+ problems: seen.problems,
883
+ notes: seen.notes,
884
+ path: written.path,
885
+ };
886
+ }
887
+
888
+ /** One line per dashboard, for a tool result or a slash command. */
889
+ export function summarizeDocument(document: DashboardDocument): string[] {
890
+ return document.dashboards.map((dashboard) => {
891
+ const mark = dashboard.id === document.activeId ? "* " : " ";
892
+ const panels = dashboard.panels.length
893
+ ? dashboard.panels.map(describePanel).join(", ")
894
+ : "no panels";
895
+ return `${mark}${dashboard.id} (${dashboard.title}): ${panels}`;
896
+ });
897
+ }
898
+
899
+ function describePanel(panel: DashboardPanel): string {
900
+ const marks: string[] = [];
901
+ if (panel.pinned) marks.push("pinned");
902
+ if (panel.addedBy && panel.addedBy !== "preset") marks.push(`added by ${panel.addedBy}`);
903
+ return `${panel.id} [${panel.widget}]${marks.length ? ` (${marks.join(", ")})` : ""}`;
904
+ }
905
+
906
+ /** True when a shell with a dashboard pane is attached. */
907
+ function hasPane(): boolean {
908
+ return capabilityProfile().ui === "desktop";
909
+ }
910
+
911
+ /** Where the change landed, said in one line, honestly, in either shell. */
912
+ export function landedLine(): string {
913
+ return hasPane()
914
+ ? "The Dashboard tab picks this up within a few seconds."
915
+ : "There is no dashboard pane in the terminal; the layout file is updated and the next Orbit session will show it.";
916
+ }
917
+
918
+ /**
919
+ * Note the change in the activity log. The only durable trace that the agent,
920
+ * rather than the user, rearranged what they are looking at.
921
+ */
922
+ export function logDashboardChange(filePath: string, notes: string[], source: string): void {
923
+ try {
924
+ appendActivityEvent(path.dirname(filePath), {
925
+ timestamp: new Date().toISOString(),
926
+ kind: "dashboard.changed",
927
+ source,
928
+ payload: { changes: notes },
929
+ });
930
+ } catch {
931
+ // The layout is written; failing to note it is not worth failing the call.
932
+ }
933
+ }
934
+
935
+ export function countPanels(document: DashboardDocument): number {
936
+ return document.dashboards.reduce((total, d) => total + d.panels.length, 0);
937
+ }
938
+
939
+ // ─────────────────────────────────────────────────────────────────────────────
940
+ // Registration
941
+ // ─────────────────────────────────────────────────────────────────────────────
942
+
943
+ const UPDATE_DESCRIPTION = [
944
+ "Change the analysis dashboard: the panels the user sees beside the chat.",
945
+ "",
946
+ "Only when the user asks. Do not add, remove or rearrange panels on your own",
947
+ "initiative -- not to be helpful, not because a run started, not because you",
948
+ "think another view would suit them better. A panel the user placed or pinned",
949
+ "is refused outright; ask them to change that one in the dashboard's own controls.",
950
+ "",
951
+ "Call dashboard_read first so you use the real panel ids. Pass EITHER `actions`",
952
+ "(add_panel, remove_panel, update_panel, move_panel, create_dashboard,",
953
+ "switch_dashboard) or `document` (the whole layout as JSON text), never both.",
954
+ "`reason` is required and is recorded on every panel you add or change, so the",
955
+ 'user can see why it is there -- name the fact, e.g. "you asked to watch the',
956
+ 'alignment run".',
957
+ "",
958
+ `Widget types: ${widgetCatalogLines().join("; ")}.`,
959
+ "Presets for create_dashboard:",
960
+ ...presetLines().map((line) => ` ${line}`),
961
+ "Panels sit in a two-column grid: span is 1 or 2 columns, rows is 1-6 height",
962
+ "units, position is the index within the dashboard (omit it to append).",
963
+ ].join("\n");
964
+
965
+ const ActionSchema = Type.Object({
966
+ action: Type.Union(
967
+ [
968
+ Type.Literal("add_panel"),
969
+ Type.Literal("remove_panel"),
970
+ Type.Literal("update_panel"),
971
+ Type.Literal("move_panel"),
972
+ Type.Literal("create_dashboard"),
973
+ Type.Literal("switch_dashboard"),
974
+ ],
975
+ { description: "What to do." },
976
+ ),
977
+ dashboardId: Type.Optional(
978
+ Type.String({ description: "Which dashboard. Defaults to the one on screen." }),
979
+ ),
980
+ panelId: Type.Optional(
981
+ Type.String({ description: "Panel to act on, for remove_panel/update_panel/move_panel." }),
982
+ ),
983
+ widget: Type.Optional(Type.String({ description: "Widget type, for add_panel." })),
984
+ title: Type.Optional(
985
+ Type.String({ description: "Panel or dashboard title. Omit to use the widget's own label." }),
986
+ ),
987
+ config: Type.Optional(
988
+ Type.String({
989
+ description:
990
+ 'Widget config as a JSON object in a string, e.g. {"follow":false}. On update_panel it is merged into the panel\'s existing config.',
991
+ }),
992
+ ),
993
+ span: Type.Optional(Type.Integer({ minimum: 1, maximum: 2, description: "Columns: 1 or 2." })),
994
+ rows: Type.Optional(Type.Integer({ minimum: 1, maximum: 6, description: "Height units: 1-6." })),
995
+ position: Type.Optional(
996
+ Type.Integer({ minimum: 0, description: "Index within the dashboard. Omit to append." }),
997
+ ),
998
+ preset: Type.Optional(
999
+ Type.String({ description: "Preset id to build a new dashboard from, for create_dashboard." }),
1000
+ ),
1001
+ });
1002
+
1003
+ function toolFailure(error: string, problems?: DashboardProblem[]) {
1004
+ return {
1005
+ content: [
1006
+ {
1007
+ type: "text" as const,
1008
+ text: JSON.stringify(
1009
+ {
1010
+ success: false,
1011
+ error: capText(error, MAX_ERROR_CHARS),
1012
+ ...(problems ? { problems: cappedProblems(problems) } : {}),
1013
+ },
1014
+ null,
1015
+ 2,
1016
+ ),
1017
+ },
1018
+ ],
1019
+ details: { error: true } as Record<string, unknown>,
1020
+ };
1021
+ }
1022
+
1023
+ export function registerDashboardTools(pi: ExtensionAPI): void {
1024
+ pi.registerTool({
1025
+ name: "dashboard_read",
1026
+ label: "Read Dashboard Layout",
1027
+ description:
1028
+ "Read the analysis dashboard: the named dashboards, their panels, which one is on screen " +
1029
+ "and who put each panel there. Call this before dashboard_update so you change panels by " +
1030
+ "their real ids.",
1031
+ parameters: Type.Object({}),
1032
+ async execute() {
1033
+ const read = await readDashboardDocument();
1034
+ if (!read.ok) return toolFailure(read.error);
1035
+ // Measured the way it is emitted. The response is written with two-space
1036
+ // indentation a few lines below, which roughly doubles it, so comparing
1037
+ // the compact form against the cap let a document that just squeaked
1038
+ // under land in front of the model at twice the size.
1039
+ const serialized = JSON.stringify(read.document, null, 2);
1040
+ const tooBig = serialized.length > MAX_READ_CHARS;
1041
+ return {
1042
+ content: [
1043
+ {
1044
+ type: "text",
1045
+ text: JSON.stringify(
1046
+ {
1047
+ success: true,
1048
+ exists: read.exists,
1049
+ ...(tooBig
1050
+ ? {
1051
+ documentOmitted: `The layout is ${serialized.length} characters, too large to show in full. Work from the summary below -- it carries the panel ids -- and change panels with actions rather than replacing the whole document.`,
1052
+ }
1053
+ : { document: read.document }),
1054
+ summary: cappedSummary(read.document),
1055
+ widgetTypes: widgetCatalogLines(),
1056
+ presets: presetLines(),
1057
+ ...(read.problems.length ? { problems: cappedProblems(read.problems) } : {}),
1058
+ ...(read.exists
1059
+ ? {}
1060
+ : { note: "No layout file yet; this is the default the user sees." }),
1061
+ },
1062
+ null,
1063
+ 2,
1064
+ ),
1065
+ },
1066
+ ],
1067
+ details: { panels: countPanels(read.document) },
1068
+ };
1069
+ },
1070
+ renderResult: (result) => {
1071
+ const d = result.details as { panels?: number; error?: boolean } | undefined;
1072
+ if (d?.error) return new Text("Could not read the dashboard");
1073
+ return new Text(`Dashboard: ${d?.panels ?? 0} panel(s)`);
1074
+ },
1075
+ });
1076
+
1077
+ pi.registerTool({
1078
+ name: "dashboard_update",
1079
+ label: "Update Dashboard Layout",
1080
+ description: UPDATE_DESCRIPTION,
1081
+ parameters: Type.Object({
1082
+ reason: Type.String({
1083
+ minLength: 1,
1084
+ description:
1085
+ "Why this change, in the user's terms. Recorded on every panel you add or change.",
1086
+ }),
1087
+ actions: Type.Optional(
1088
+ Type.Array(ActionSchema, {
1089
+ minItems: 1,
1090
+ maxItems: MAX_ACTIONS,
1091
+ description: "The changes to make, applied in order.",
1092
+ }),
1093
+ ),
1094
+ document: Type.Optional(
1095
+ Type.String({
1096
+ description:
1097
+ "The whole layout as JSON text, replacing what is there. Use actions instead unless the user asked for a wholesale rebuild.",
1098
+ }),
1099
+ ),
1100
+ }),
1101
+ async execute(_toolCallId, params) {
1102
+ const raw = params as Record<string, unknown>;
1103
+ const reason = asText(raw.reason);
1104
+ if (!reason) return toolFailure("reason is required: say why the user wants this change.");
1105
+
1106
+ if (given(raw.actions) && !Array.isArray(raw.actions)) {
1107
+ return toolFailure("actions must be a list of changes.");
1108
+ }
1109
+ if (given(raw.document) && typeof raw.document !== "string") {
1110
+ return toolFailure("document must be the whole layout as JSON text.");
1111
+ }
1112
+ const actions = (Array.isArray(raw.actions) ? raw.actions : []) as DashboardAction[];
1113
+ const documentText = typeof raw.document === "string" ? raw.document : undefined;
1114
+
1115
+ if (actions.length > 0 && documentText !== undefined) {
1116
+ return toolFailure("Pass either actions or document, not both.");
1117
+ }
1118
+ if (actions.length === 0 && documentText === undefined) {
1119
+ return toolFailure("Nothing to do: pass actions or document.");
1120
+ }
1121
+
1122
+ const outcome = await commitDashboardChange(
1123
+ (current) => {
1124
+ if (documentText !== undefined) {
1125
+ const parsed = parseDashboardDocument(documentText);
1126
+ if (!parsed.ok) {
1127
+ return {
1128
+ ok: false,
1129
+ error: `document could not be read: ${parsed.problems
1130
+ .map((p) => `${p.path || "document"}: ${p.message}`)
1131
+ .join("; ")}`,
1132
+ };
1133
+ }
1134
+ // The caps have already been applied by the time we get here: the
1135
+ // validator enforces them by keeping the first N and calling it a
1136
+ // repair, so `commitDashboardChange` would compare this truncated
1137
+ // document against itself and find nothing missing. That is why
1138
+ // prepending one panel to a full dashboard reported success and
1139
+ // deleted the last one -- the guard that exists for exactly this
1140
+ // is unreachable on this path. Count what the model actually sent.
1141
+ const overflow = documentOverflow(documentText);
1142
+ if (overflow) return { ok: false, error: overflow };
1143
+ return {
1144
+ ok: true,
1145
+ document: parsed.document,
1146
+ notes: ["replaced the whole layout"],
1147
+ problems: parsed.problems,
1148
+ };
1149
+ }
1150
+ return applyDashboardActions(current, actions, reason);
1151
+ },
1152
+ { reason },
1153
+ );
1154
+
1155
+ if (!outcome.ok) return toolFailure(outcome.error, outcome.problems);
1156
+
1157
+ logDashboardChange(outcome.path, outcome.notes, "dashboard_update");
1158
+ return {
1159
+ content: [
1160
+ {
1161
+ type: "text",
1162
+ text: JSON.stringify(
1163
+ {
1164
+ success: true,
1165
+ changes: capLines(outcome.notes, MAX_SUMMARY_CHARS, "change(s)"),
1166
+ summary: cappedSummary(outcome.document),
1167
+ where: landedLine(),
1168
+ undo: "The user can put this back with /dashboard undo.",
1169
+ ...(outcome.problems.length ? { repairs: cappedProblems(outcome.problems) } : {}),
1170
+ },
1171
+ null,
1172
+ 2,
1173
+ ),
1174
+ },
1175
+ ],
1176
+ details: { changes: outcome.notes.length },
1177
+ };
1178
+ },
1179
+ renderResult: (result) => {
1180
+ const d = result.details as { changes?: number; error?: boolean } | undefined;
1181
+ if (d?.error) return new Text("Dashboard unchanged");
1182
+ return new Text(`Dashboard updated (${d?.changes ?? 0} change(s))`);
1183
+ },
1184
+ });
1185
+ }