ma-agents 3.12.2 → 3.13.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 (255) hide show
  1. package/bin/cli.js +11 -6
  2. package/docs/architecture.md +18 -0
  3. package/lib/bmad-cache/bmb/.claude-plugin/marketplace.json +1 -1
  4. package/lib/bmad-cache/bmb/_git_preserved/hooks/commit-msg.sample +52 -2
  5. package/lib/bmad-cache/bmb/_git_preserved/hooks/fsmonitor-watchman.sample +2 -8
  6. package/lib/bmad-cache/bmb/_git_preserved/index +0 -0
  7. package/lib/bmad-cache/bmb/_git_preserved/objects/pack/pack-8f8b045fef5af6911495cf3b2a89f1ed75e120f7.idx +0 -0
  8. package/lib/bmad-cache/bmb/_git_preserved/objects/pack/pack-8f8b045fef5af6911495cf3b2a89f1ed75e120f7.pack +0 -0
  9. package/lib/bmad-cache/bmb/_git_preserved/objects/pack/pack-8f8b045fef5af6911495cf3b2a89f1ed75e120f7.rev +0 -0
  10. package/lib/bmad-cache/bmb/_git_preserved/packed-refs +1 -1
  11. package/lib/bmad-cache/bmb/_git_preserved/refs/heads/main +1 -1
  12. package/lib/bmad-cache/bmb/_git_preserved/shallow +1 -1
  13. package/lib/bmad-cache/bmb/package-lock.json +2 -2
  14. package/lib/bmad-cache/bmb/package.json +1 -1
  15. package/lib/bmad-cache/bmb/samples/bmad-agent-dream-weaver/assets/module-help.csv +1 -1
  16. package/lib/bmad-cache/bmb/samples/bmad-agent-dream-weaver/scripts/merge-config.py +33 -0
  17. package/lib/bmad-cache/bmb/samples/bmad-agent-dream-weaver/scripts/merge-help-csv.py +28 -0
  18. package/lib/bmad-cache/bmb/samples/sample-module-setup/assets/module-help.csv +1 -1
  19. package/lib/bmad-cache/bmb/samples/sample-module-setup/scripts/cleanup-legacy.py +28 -0
  20. package/lib/bmad-cache/bmb/samples/sample-module-setup/scripts/merge-config.py +33 -0
  21. package/lib/bmad-cache/bmb/samples/sample-module-setup/scripts/merge-help-csv.py +28 -0
  22. package/lib/bmad-cache/bmb/skills/bmad-bmb-setup/assets/module-help.csv +1 -1
  23. package/lib/bmad-cache/bmb/skills/bmad-bmb-setup/scripts/cleanup-legacy.py +28 -0
  24. package/lib/bmad-cache/bmb/skills/bmad-bmb-setup/scripts/merge-config.py +33 -0
  25. package/lib/bmad-cache/bmb/skills/bmad-bmb-setup/scripts/merge-help-csv.py +28 -0
  26. package/lib/bmad-cache/bmb/skills/bmad-eval-runner/assets/Dockerfile +29 -0
  27. package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/docker_setup.py +115 -0
  28. package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/generate_report.py +184 -0
  29. package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/pty_runner.py +171 -0
  30. package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/run_evals.py +492 -0
  31. package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/run_triggers.py +366 -0
  32. package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/utils.py +260 -0
  33. package/lib/bmad-cache/bmb/skills/bmad-module-builder/assets/setup-skill-template/assets/module-help.csv +1 -1
  34. package/lib/bmad-cache/bmb/skills/bmad-module-builder/assets/setup-skill-template/scripts/cleanup-legacy.py +28 -0
  35. package/lib/bmad-cache/bmb/skills/bmad-module-builder/assets/setup-skill-template/scripts/merge-config.py +33 -0
  36. package/lib/bmad-cache/bmb/skills/bmad-module-builder/assets/setup-skill-template/scripts/merge-help-csv.py +28 -0
  37. package/lib/bmad-cache/bmb/skills/bmad-module-builder/assets/standalone-module-template/merge-config.py +33 -0
  38. package/lib/bmad-cache/bmb/skills/bmad-module-builder/assets/standalone-module-template/merge-help-csv.py +28 -0
  39. package/lib/bmad-cache/bmb/skills/bmad-module-builder/scripts/tests/test-validate-module.py +74 -1
  40. package/lib/bmad-cache/bmb/skills/bmad-module-builder/scripts/validate-module.py +24 -13
  41. package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/assets/sample-customize-product-brief.toml +48 -33
  42. package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/extract-report-json.py +287 -0
  43. package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/generate-html-report.py +57 -8
  44. package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/prepass-prompt-metrics.py +7 -7
  45. package/lib/bmad-cache/bmb/skills/module-help.csv +1 -1
  46. package/lib/bmad-cache/bmb/website/public/img/eval-test-types.png +0 -0
  47. package/lib/bmad-cache/cache-manifest.json +17 -18
  48. package/lib/bmad-cache/cis/_git_preserved/hooks/commit-msg.sample +52 -2
  49. package/lib/bmad-cache/cis/_git_preserved/hooks/fsmonitor-watchman.sample +2 -8
  50. package/lib/bmad-cache/cis/_git_preserved/index +0 -0
  51. package/lib/bmad-cache/cis/_git_preserved/objects/pack/pack-18c8290560a98bcb7bf0676e6cc9b2ac5ca2823e.idx +0 -0
  52. package/lib/bmad-cache/cis/_git_preserved/objects/pack/{pack-42ffc048f54e58ce94c6331bc6be97ebbb7936f2.pack → pack-18c8290560a98bcb7bf0676e6cc9b2ac5ca2823e.pack} +0 -0
  53. package/lib/bmad-cache/cis/_git_preserved/objects/pack/pack-18c8290560a98bcb7bf0676e6cc9b2ac5ca2823e.rev +0 -0
  54. package/lib/bmad-cache/cis/_git_preserved/packed-refs +1 -1
  55. package/lib/bmad-cache/cis/_git_preserved/refs/heads/main +1 -1
  56. package/lib/bmad-cache/cis/_git_preserved/refs/tags/v0.2.1 +1 -0
  57. package/lib/bmad-cache/cis/_git_preserved/shallow +1 -1
  58. package/lib/bmad-cache/cis/package-lock.json +2 -2
  59. package/lib/bmad-cache/cis/package.json +1 -1
  60. package/lib/bmad-cache/cis/src/module-help.csv +1 -1
  61. package/lib/bmad-cache/gds/.claude-plugin/marketplace.json +4 -7
  62. package/lib/bmad-cache/gds/README.md +3 -1
  63. package/lib/bmad-cache/gds/_git_preserved/hooks/commit-msg.sample +52 -2
  64. package/lib/bmad-cache/gds/_git_preserved/hooks/fsmonitor-watchman.sample +2 -8
  65. package/lib/bmad-cache/gds/_git_preserved/index +0 -0
  66. package/lib/bmad-cache/gds/_git_preserved/objects/pack/pack-dcb7c556d9bb6b6b70d2301e094eaac6d7300552.idx +0 -0
  67. package/lib/bmad-cache/gds/_git_preserved/objects/pack/{pack-9427a146a90c00bb542cba038874bf9671ba4dc0.pack → pack-dcb7c556d9bb6b6b70d2301e094eaac6d7300552.pack} +0 -0
  68. package/lib/bmad-cache/gds/_git_preserved/objects/pack/pack-dcb7c556d9bb6b6b70d2301e094eaac6d7300552.rev +0 -0
  69. package/lib/bmad-cache/gds/_git_preserved/packed-refs +1 -1
  70. package/lib/bmad-cache/gds/_git_preserved/refs/heads/main +1 -1
  71. package/lib/bmad-cache/gds/_git_preserved/shallow +1 -1
  72. package/lib/bmad-cache/gds/package.json +1 -1
  73. package/lib/bmad-cache/gds/src/agents/gds-agent-game-designer/customize.toml +5 -5
  74. package/lib/bmad-cache/gds/src/agents/gds-agent-game-dev/customize.toml +5 -5
  75. package/lib/bmad-cache/gds/src/agents/gds-agent-game-solo-dev/customize.toml +0 -5
  76. package/lib/bmad-cache/gds/src/module-help.csv +6 -12
  77. package/lib/bmad-cache/gds/src/module.yaml +1 -1
  78. package/lib/bmad-cache/gds/src/workflows/1-preproduction/gds-create-game-brief/customize.toml +97 -22
  79. package/lib/bmad-cache/gds/src/workflows/2-design/gds-gdd/assets/validation-report-template.html +190 -0
  80. package/lib/bmad-cache/gds/src/workflows/2-design/gds-gdd/customize.toml +99 -0
  81. package/lib/bmad-cache/gds/src/workflows/2-design/gds-gdd/scripts/render-validation-html.py +290 -0
  82. package/lib/bmad-cache/gds/src/workflows/2-design/gds-prd/assets/validation-report-template.html +190 -0
  83. package/lib/bmad-cache/gds/src/workflows/2-design/gds-prd/customize.toml +84 -0
  84. package/lib/bmad-cache/gds/src/workflows/2-design/gds-ux/assets/validation-report-template.html +319 -0
  85. package/lib/bmad-cache/gds/src/workflows/2-design/gds-ux/customize.toml +101 -0
  86. package/lib/bmad-cache/gds/src/workflows/3-technical/gds-game-architecture/architecture-patterns.yaml +1 -0
  87. package/lib/bmad-cache/gds/src/workflows/3-technical/gds-game-architecture/decision-catalog.yaml +88 -0
  88. package/lib/bmad-cache/gds/src/workflows/3-technical/gds-game-architecture/engine-mcps.yaml +124 -2
  89. package/lib/bmad-cache/gds/src/workflows/4-production/gds-investigate/customize.toml +62 -0
  90. package/lib/bmad-cache/tea/.claude-plugin/marketplace.json +1 -1
  91. package/lib/bmad-cache/tea/.github/workflows/docs.yaml +3 -3
  92. package/lib/bmad-cache/tea/.github/workflows/quality.yaml +10 -10
  93. package/lib/bmad-cache/tea/AGENTS.md +31 -0
  94. package/lib/bmad-cache/tea/CHANGELOG.md +42 -1
  95. package/lib/bmad-cache/tea/README.md +8 -5
  96. package/lib/bmad-cache/tea/_git_preserved/hooks/commit-msg.sample +52 -2
  97. package/lib/bmad-cache/tea/_git_preserved/hooks/fsmonitor-watchman.sample +2 -8
  98. package/lib/bmad-cache/tea/_git_preserved/index +0 -0
  99. package/lib/bmad-cache/tea/_git_preserved/objects/pack/pack-9e4197e37df7763dd7a05c2965ee921dfd2eb617.idx +0 -0
  100. package/lib/bmad-cache/tea/_git_preserved/objects/pack/{pack-f0df537f2649464ff6c5aee241165eb9c8664227.pack → pack-9e4197e37df7763dd7a05c2965ee921dfd2eb617.pack} +0 -0
  101. package/lib/bmad-cache/tea/_git_preserved/objects/pack/pack-9e4197e37df7763dd7a05c2965ee921dfd2eb617.rev +0 -0
  102. package/lib/bmad-cache/tea/_git_preserved/packed-refs +1 -1
  103. package/lib/bmad-cache/tea/_git_preserved/refs/heads/main +1 -1
  104. package/lib/bmad-cache/tea/_git_preserved/refs/tags/v1.19.0 +1 -0
  105. package/lib/bmad-cache/tea/_git_preserved/shallow +1 -1
  106. package/lib/bmad-cache/tea/docs/explanation/engagement-models.md +15 -16
  107. package/lib/bmad-cache/tea/docs/explanation/knowledge-base-system.md +2 -0
  108. package/lib/bmad-cache/tea/docs/explanation/risk-based-testing.md +1 -1
  109. package/lib/bmad-cache/tea/docs/explanation/tea-overview.md +88 -52
  110. package/lib/bmad-cache/tea/docs/explanation/testing-as-engineering.md +13 -12
  111. package/lib/bmad-cache/tea/docs/glossary/index.md +2 -2
  112. package/lib/bmad-cache/tea/docs/how-to/brownfield/use-tea-for-enterprise.md +19 -18
  113. package/lib/bmad-cache/tea/docs/how-to/brownfield/use-tea-with-existing-tests.md +1 -1
  114. package/lib/bmad-cache/tea/docs/how-to/workflows/run-nfr-assess.md +32 -26
  115. package/lib/bmad-cache/tea/docs/how-to/workflows/run-test-design.md +20 -14
  116. package/lib/bmad-cache/tea/docs/how-to/workflows/run-trace.md +3 -3
  117. package/lib/bmad-cache/tea/docs/index.md +13 -11
  118. package/lib/bmad-cache/tea/docs/reference/commands.md +37 -13
  119. package/lib/bmad-cache/tea/docs/reference/knowledge-base.md +2 -2
  120. package/lib/bmad-cache/tea/package-lock.json +2 -2
  121. package/lib/bmad-cache/tea/package.json +1 -1
  122. package/lib/bmad-cache/tea/src/agents/bmad-tea/customize.toml +20 -15
  123. package/lib/bmad-cache/tea/src/agents/bmad-tea/resources/knowledge/confidence-gate.md +73 -0
  124. package/lib/bmad-cache/tea/src/agents/bmad-tea/resources/knowledge/test-quality.md +1 -0
  125. package/lib/bmad-cache/tea/src/agents/bmad-tea/resources/tea-index.csv +2 -1
  126. package/lib/bmad-cache/tea/src/module-help.csv +2 -2
  127. package/lib/bmad-cache/tea/src/workflows/testarch/README.md +5 -4
  128. package/lib/bmad-cache/tea/src/workflows/testarch/bmad-teach-me-testing/data/role-paths.yaml +1 -1
  129. package/lib/bmad-cache/tea/src/workflows/testarch/bmad-teach-me-testing/data/tea-resources-index.yaml +1 -1
  130. package/lib/bmad-cache/tea/src/workflows/testarch/bmad-teach-me-testing/steps-c/step-04-session-01.md +2 -2
  131. package/lib/bmad-cache/tea/src/workflows/testarch/bmad-teach-me-testing/steps-c/step-04-session-07.md +1 -1
  132. package/lib/bmad-cache/tea/src/workflows/testarch/bmad-teach-me-testing/templates/certificate-template.md +1 -1
  133. package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-atdd/resources/tea-index.csv +1 -1
  134. package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-automate/resources/tea-index.csv +1 -1
  135. package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-ci/resources/tea-index.csv +1 -1
  136. package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-framework/resources/tea-index.csv +1 -1
  137. package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-nfr/SKILL.md +3 -3
  138. package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-nfr/checklist.md +11 -11
  139. package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-nfr/instructions.md +4 -2
  140. package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-nfr/nfr-report-template.md +5 -5
  141. package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-nfr/resources/tea-index.csv +1 -1
  142. package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-nfr/steps-c/step-01-load-context.md +1 -1
  143. package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-nfr/steps-c/step-01b-resume.md +1 -1
  144. package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-nfr/steps-c/step-02-define-thresholds.md +14 -3
  145. package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-nfr/steps-c/step-04-evaluate-and-score.md +7 -7
  146. package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-nfr/steps-c/step-04a-subagent-security.md +4 -4
  147. package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-nfr/steps-c/step-04b-subagent-performance.md +4 -4
  148. package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-nfr/steps-c/step-04c-subagent-reliability.md +4 -4
  149. package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-nfr/steps-c/step-04d-subagent-scalability.md +4 -4
  150. package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-nfr/steps-c/step-04e-aggregate-nfr.md +4 -4
  151. package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-nfr/steps-c/step-05-generate-report.md +1 -1
  152. package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-nfr/workflow-plan.md +1 -1
  153. package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-nfr/workflow.yaml +3 -3
  154. package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-test-design/checklist.md +23 -3
  155. package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-test-design/resources/tea-index.csv +1 -1
  156. package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-test-design/steps-c/step-02-load-context.md +7 -0
  157. package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-test-design/steps-c/step-03-risk-and-testability.md +16 -2
  158. package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-test-design/steps-c/step-04-coverage-plan.md +20 -4
  159. package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-test-design/steps-c/step-05-generate-output.md +2 -0
  160. package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-test-design/test-design-architecture-template.md +17 -0
  161. package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-test-design/test-design-qa-template.md +15 -0
  162. package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-test-design/test-design-template.md +16 -0
  163. package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-test-review/resources/tea-index.csv +1 -1
  164. package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-trace/checklist.md +1 -1
  165. package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-trace/resources/tea-index.csv +1 -1
  166. package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-trace/trace-template.md +1 -1
  167. package/lib/bmad-cache/tea/test/test-installation-components.js +49 -0
  168. package/lib/bmad-cache/tea/website/astro.config.mjs +2 -2
  169. package/lib/bmad-cache/wds/README.md +1 -1
  170. package/lib/bmad-cache/wds/_git_preserved/hooks/commit-msg.sample +52 -2
  171. package/lib/bmad-cache/wds/_git_preserved/hooks/fsmonitor-watchman.sample +2 -8
  172. package/lib/bmad-cache/wds/_git_preserved/index +0 -0
  173. package/lib/bmad-cache/wds/_git_preserved/objects/pack/pack-656c3d8d5426e73043b6a7f45eedaab74e3c419e.idx +0 -0
  174. package/lib/bmad-cache/wds/_git_preserved/objects/pack/{pack-96877c1c09123cccb1f91c1412184b11d2b492ad.pack → pack-656c3d8d5426e73043b6a7f45eedaab74e3c419e.pack} +0 -0
  175. package/lib/bmad-cache/wds/_git_preserved/objects/pack/pack-656c3d8d5426e73043b6a7f45eedaab74e3c419e.rev +0 -0
  176. package/lib/bmad-cache/wds/_git_preserved/packed-refs +1 -1
  177. package/lib/bmad-cache/wds/_git_preserved/refs/heads/main +1 -1
  178. package/lib/bmad-cache/wds/_git_preserved/refs/tags/v0.4.3 +1 -0
  179. package/lib/bmad-cache/wds/_git_preserved/shallow +1 -1
  180. package/lib/bmad-cache/wds/eslint.config.mjs +1 -1
  181. package/lib/bmad-cache/wds/package.json +1 -1
  182. package/lib/bmad-cache/wds/src/agents/wds-agent-freya-ux/customize.toml +80 -0
  183. package/lib/bmad-cache/wds/src/agents/wds-agent-mimir-builder/customize.toml +52 -0
  184. package/lib/bmad-cache/wds/src/agents/wds-agent-saga-analyst/customize.toml +70 -0
  185. package/lib/bmad-cache/wds/src/module-help.csv +19 -19
  186. package/lib/bmad-cache/wds/src/module.yaml +28 -0
  187. package/lib/bmad-cache/wds/src/scripts/README.md +155 -0
  188. package/lib/bmad-cache/wds/src/scripts/wds-add-object.js +202 -0
  189. package/lib/bmad-cache/wds/src/scripts/wds-add-spacing.js +158 -0
  190. package/lib/bmad-cache/wds/src/scripts/wds-init-page.js +229 -0
  191. package/lib/bmad-cache/wds/src/scripts/wds-init-scenario.js +120 -0
  192. package/lib/bmad-cache/wds/src/scripts/wds-nav.js +201 -0
  193. package/lib/bmad-cache/wds/src/scripts/wds-validate.js +301 -0
  194. package/lib/bmad-cache/wds/src/workflows/wds-3-scenarios/workflow.xml +450 -0
  195. package/lib/bmad-cache/wds/src/workflows/wds-4-ux-design/workflow-specify.xml +387 -0
  196. package/lib/bmad-extension/.claude-plugin/marketplace.json.template +1 -1
  197. package/lib/bmad-extension-plugin/.claude-plugin/marketplace.json +2 -2
  198. package/lib/bmad.js +91 -7
  199. package/lib/installer.js +28 -6
  200. package/lib/mil498-templates/OCD.md +169 -169
  201. package/lib/mil498-templates/README.md +4 -4
  202. package/lib/mil498-templates/SDD.md +163 -163
  203. package/lib/mil498-templates/SDP.md +307 -307
  204. package/lib/mil498-templates/SRS.md +219 -219
  205. package/lib/mil498-templates/SSDD.md +154 -154
  206. package/lib/mil498-templates/SSS.md +225 -225
  207. package/lib/mil498-templates/STD.md +188 -188
  208. package/lib/templates/instruction-block-git.template.md +25 -0
  209. package/package.json +5 -4
  210. package/scripts/build-bmad-cache.js +143 -42
  211. package/skills/git-workflow-skill/skill.json +21 -21
  212. package/lib/bmad-cache/bmb/_git_preserved/objects/pack/pack-6ecd9fc6445b1281449c5ec49a6c5794708e662e.idx +0 -0
  213. package/lib/bmad-cache/bmb/_git_preserved/objects/pack/pack-6ecd9fc6445b1281449c5ec49a6c5794708e662e.pack +0 -0
  214. package/lib/bmad-cache/bmb/_git_preserved/objects/pack/pack-6ecd9fc6445b1281449c5ec49a6c5794708e662e.rev +0 -0
  215. package/lib/bmad-cache/bmb/_git_preserved/refs/remotes/origin/HEAD +0 -1
  216. package/lib/bmad-cache/bmb/_git_preserved/refs/tags/v1.7.0 +0 -1
  217. package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/generate-convert-report.py +0 -406
  218. package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/tests/test_generate_convert_report.py +0 -243
  219. package/lib/bmad-cache/cis/_git_preserved/objects/pack/pack-42ffc048f54e58ce94c6331bc6be97ebbb7936f2.idx +0 -0
  220. package/lib/bmad-cache/cis/_git_preserved/objects/pack/pack-42ffc048f54e58ce94c6331bc6be97ebbb7936f2.rev +0 -0
  221. package/lib/bmad-cache/cis/_git_preserved/refs/remotes/origin/HEAD +0 -1
  222. package/lib/bmad-cache/gds/_git_preserved/objects/pack/pack-9427a146a90c00bb542cba038874bf9671ba4dc0.idx +0 -0
  223. package/lib/bmad-cache/gds/_git_preserved/objects/pack/pack-9427a146a90c00bb542cba038874bf9671ba4dc0.rev +0 -0
  224. package/lib/bmad-cache/gds/_git_preserved/refs/remotes/origin/HEAD +0 -1
  225. package/lib/bmad-cache/gds/src/workflows/2-design/gds-create-gdd/customize.toml +0 -41
  226. package/lib/bmad-cache/gds/src/workflows/2-design/gds-create-prd/customize.toml +0 -41
  227. package/lib/bmad-cache/gds/src/workflows/2-design/gds-create-prd/data/domain-complexity.csv +0 -15
  228. package/lib/bmad-cache/gds/src/workflows/2-design/gds-create-prd/data/project-types.csv +0 -11
  229. package/lib/bmad-cache/gds/src/workflows/2-design/gds-create-ux-design/customize.toml +0 -41
  230. package/lib/bmad-cache/gds/src/workflows/2-design/gds-edit-gdd/customize.toml +0 -41
  231. package/lib/bmad-cache/gds/src/workflows/2-design/gds-edit-prd/customize.toml +0 -41
  232. package/lib/bmad-cache/gds/src/workflows/2-design/gds-validate-gdd/customize.toml +0 -41
  233. package/lib/bmad-cache/gds/src/workflows/2-design/gds-validate-prd/customize.toml +0 -41
  234. package/lib/bmad-cache/gds/src/workflows/2-design/gds-validate-prd/data/domain-complexity.csv +0 -15
  235. package/lib/bmad-cache/gds/src/workflows/2-design/gds-validate-prd/data/project-types.csv +0 -11
  236. package/lib/bmad-cache/tea/_git_preserved/objects/pack/pack-f0df537f2649464ff6c5aee241165eb9c8664227.idx +0 -0
  237. package/lib/bmad-cache/tea/_git_preserved/objects/pack/pack-f0df537f2649464ff6c5aee241165eb9c8664227.rev +0 -0
  238. package/lib/bmad-cache/tea/_git_preserved/refs/remotes/origin/HEAD +0 -1
  239. package/lib/bmad-cache/wds/_git_preserved/objects/pack/pack-96877c1c09123cccb1f91c1412184b11d2b492ad.idx +0 -0
  240. package/lib/bmad-cache/wds/_git_preserved/objects/pack/pack-96877c1c09123cccb1f91c1412184b11d2b492ad.rev +0 -0
  241. package/lib/bmad-cache/wds/_git_preserved/refs/remotes/origin/HEAD +0 -1
  242. package/lib/bmad-cache/wds/src/agents/wds-agent-freya-ux/bmad-skill-manifest.yaml +0 -12
  243. package/lib/bmad-cache/wds/src/agents/wds-agent-saga-analyst/bmad-skill-manifest.yaml +0 -12
  244. package/lib/bmad-cache/wds/src/workflows/wds-0-alignment-signoff/bmad-skill-manifest.yaml +0 -1
  245. package/lib/bmad-cache/wds/src/workflows/wds-0-project-setup/bmad-skill-manifest.yaml +0 -1
  246. package/lib/bmad-cache/wds/src/workflows/wds-1-project-brief/bmad-skill-manifest.yaml +0 -1
  247. package/lib/bmad-cache/wds/src/workflows/wds-2-trigger-mapping/bmad-skill-manifest.yaml +0 -1
  248. package/lib/bmad-cache/wds/src/workflows/wds-3-scenarios/bmad-skill-manifest.yaml +0 -1
  249. package/lib/bmad-cache/wds/src/workflows/wds-4-ux-design/bmad-skill-manifest.yaml +0 -1
  250. package/lib/bmad-cache/wds/src/workflows/wds-5-agentic-development/bmad-skill-manifest.yaml +0 -1
  251. package/lib/bmad-cache/wds/src/workflows/wds-6-asset-generation/bmad-skill-manifest.yaml +0 -1
  252. package/lib/bmad-cache/wds/src/workflows/wds-7-design-system/bmad-skill-manifest.yaml +0 -1
  253. package/lib/bmad-cache/wds/src/workflows/wds-8-product-evolution/bmad-skill-manifest.yaml +0 -1
  254. /package/lib/bmad-cache/gds/src/workflows/2-design/{gds-create-gdd → gds-gdd/assets}/game-types.csv +0 -0
  255. /package/lib/bmad-cache/gds/src/workflows/2-design/{gds-validate-gdd/data → gds-gdd/assets}/genre-complexity.csv +0 -0
@@ -0,0 +1,366 @@
1
+ #!/usr/bin/env python3
2
+ # /// script
3
+ # requires-python = ">=3.9"
4
+ # ///
5
+ """Run trigger evals: does the skill's description fire on each query?
6
+
7
+ Adapted from Anthropic skill-creator's run_eval.py
8
+ (https://github.com/anthropics/skills/tree/main/skills/skill-creator) with two
9
+ adaptations:
10
+
11
+ 1. Isolation. Each query runs in either a fresh Docker container off
12
+ bmad-eval-runner:latest, or a fresh local tmp dir under ~/bmad-evals/<run-id>/
13
+ with HOME overridden to a clean directory. This prevents the host's global
14
+ CLAUDE.md and auto-memory from biasing whether the skill fires.
15
+
16
+ 2. Output. Results are written to a run folder alongside the artifact eval
17
+ run-folder layout (so triggers and artifacts can share a single report).
18
+
19
+ Usage:
20
+ python3 run_triggers.py \\
21
+ --skill-path PATH \\
22
+ --triggers-file PATH/triggers.json \\
23
+ --output-dir PATH \\
24
+ --isolation docker|local \\
25
+ [--workers N] [--runs-per-query N] [--timeout SECS] [--threshold 0.5]
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import argparse
31
+ import json
32
+ import os
33
+ import shutil
34
+ import subprocess
35
+ import sys
36
+ import time
37
+ import uuid
38
+ from concurrent.futures import ThreadPoolExecutor, as_completed
39
+ from pathlib import Path
40
+
41
+ SCRIPT_DIR = Path(__file__).resolve().parent
42
+ sys.path.insert(0, str(SCRIPT_DIR))
43
+
44
+ from utils import ( # noqa: E402
45
+ new_run_id,
46
+ parse_skill_md,
47
+ read_json,
48
+ read_macos_keychain_credentials,
49
+ stage_credentials,
50
+ utc_now_iso,
51
+ write_json,
52
+ )
53
+
54
+ DOCKER_IMAGE = "bmad-eval-runner:latest"
55
+ _KEYCHAIN_CREDS: str | None = read_macos_keychain_credentials()
56
+
57
+
58
+ def write_synthetic_skill(skills_dir: Path, skill_name: str, description: str, unique_id: str) -> tuple[Path, str]:
59
+ """Place a synthetic skill at <skills_dir>/<clean_name>/SKILL.md.
60
+
61
+ The Skill tool only fires for entries discovered as actual skills (frontmatter
62
+ `name` + `description` under a `.claude/skills/<name>/SKILL.md`). Slash-commands
63
+ under `.claude/commands/` do not auto-invoke the Skill tool, so the previous
64
+ implementation could never observe a positive trigger. This places the synthetic
65
+ skill where Claude Code looks for skills, with a unique name so the detector
66
+ can disambiguate it from any pre-existing skill of the same display name.
67
+ """
68
+ clean_name = f"{skill_name}-skill-{unique_id}"
69
+ skill_root = skills_dir / clean_name
70
+ skill_root.mkdir(parents=True, exist_ok=True)
71
+ path = skill_root / "SKILL.md"
72
+ indented_desc = "\n ".join(description.split("\n"))
73
+ path.write_text(
74
+ f"---\n"
75
+ f"name: {clean_name}\n"
76
+ f"description: |\n"
77
+ f" {indented_desc}\n"
78
+ f"---\n\n"
79
+ f"# {skill_name}\n\n"
80
+ f"This skill handles: {description}\n",
81
+ encoding="utf-8",
82
+ )
83
+ return path, clean_name
84
+
85
+
86
+ def parse_stream_for_trigger(buffer: str, clean_name: str) -> tuple[bool | None, str]:
87
+ """Return (triggered_or_none, leftover_buffer). None means undecided yet."""
88
+ triggered: bool | None = None
89
+ pending_tool: str | None = None
90
+ accumulated_json = ""
91
+ leftover = ""
92
+
93
+ while "\n" in buffer:
94
+ line, buffer = buffer.split("\n", 1)
95
+ line = line.strip()
96
+ if not line:
97
+ continue
98
+ try:
99
+ evt = json.loads(line)
100
+ except json.JSONDecodeError:
101
+ continue
102
+
103
+ if evt.get("type") == "stream_event":
104
+ se = evt.get("event", {})
105
+ t = se.get("type", "")
106
+ if t == "content_block_start":
107
+ cb = se.get("content_block", {})
108
+ if cb.get("type") == "tool_use":
109
+ name = cb.get("name", "")
110
+ if name in ("Skill", "Read"):
111
+ pending_tool = name
112
+ accumulated_json = ""
113
+ else:
114
+ return False, ""
115
+ elif t == "content_block_delta" and pending_tool:
116
+ delta = se.get("delta", {})
117
+ if delta.get("type") == "input_json_delta":
118
+ accumulated_json += delta.get("partial_json", "")
119
+ if clean_name in accumulated_json:
120
+ return True, ""
121
+ elif t in ("content_block_stop", "message_stop"):
122
+ if pending_tool:
123
+ return clean_name in accumulated_json, ""
124
+ if t == "message_stop":
125
+ return False, ""
126
+ elif evt.get("type") == "assistant":
127
+ for item in evt.get("message", {}).get("content", []):
128
+ if item.get("type") != "tool_use":
129
+ continue
130
+ tname = item.get("name", "")
131
+ tinput = item.get("input", {})
132
+ if tname == "Skill" and clean_name in tinput.get("skill", ""):
133
+ return True, ""
134
+ if tname == "Read" and clean_name in tinput.get("file_path", ""):
135
+ return True, ""
136
+ return False, ""
137
+ elif evt.get("type") == "result":
138
+ return triggered if triggered is not None else False, ""
139
+ leftover = buffer
140
+ return triggered, leftover
141
+
142
+
143
+ def run_query_local(query: str, skill_name: str, description: str,
144
+ workspace_root: Path, timeout: int) -> bool:
145
+ workspace_root.mkdir(parents=True, exist_ok=True)
146
+ home_dir = workspace_root / ".home"
147
+ (home_dir / ".claude").mkdir(parents=True, exist_ok=True)
148
+ stage_credentials(home_dir / ".claude", _KEYCHAIN_CREDS)
149
+ project_dir = workspace_root / "project"
150
+ skills_dir = project_dir / ".claude" / "skills"
151
+ project_dir.mkdir(parents=True, exist_ok=True)
152
+
153
+ unique = uuid.uuid4().hex[:8]
154
+ cmd_file, clean_name = write_synthetic_skill(skills_dir, skill_name, description, unique)
155
+
156
+ env = {
157
+ "HOME": str(home_dir),
158
+ "CLAUDE_CONFIG_DIR": str(home_dir / ".claude"),
159
+ "PATH": os.environ.get("PATH", ""),
160
+ "ANTHROPIC_API_KEY": os.environ.get("ANTHROPIC_API_KEY", ""),
161
+ }
162
+
163
+ cmd = [
164
+ "claude", "-p", query,
165
+ "--output-format", "stream-json",
166
+ "--verbose",
167
+ "--include-partial-messages",
168
+ "--dangerously-skip-permissions",
169
+ ]
170
+
171
+ try:
172
+ proc = subprocess.Popen(
173
+ cmd,
174
+ stdout=subprocess.PIPE,
175
+ stderr=subprocess.DEVNULL,
176
+ cwd=str(project_dir),
177
+ env=env,
178
+ )
179
+ buffer = ""
180
+ triggered: bool | None = None
181
+ start = time.time()
182
+ try:
183
+ while time.time() - start < timeout:
184
+ if proc.poll() is not None:
185
+ rest = proc.stdout.read()
186
+ if rest:
187
+ buffer += rest.decode("utf-8", errors="replace")
188
+ break
189
+ chunk = proc.stdout.read1(8192) if hasattr(proc.stdout, "read1") else proc.stdout.read(8192)
190
+ if not chunk:
191
+ time.sleep(0.05)
192
+ continue
193
+ buffer += chunk.decode("utf-8", errors="replace")
194
+ decided, buffer = parse_stream_for_trigger(buffer, clean_name)
195
+ if decided is not None:
196
+ triggered = decided
197
+ break
198
+ finally:
199
+ if proc.poll() is None:
200
+ proc.kill()
201
+ proc.wait()
202
+ if triggered is None:
203
+ decided, _ = parse_stream_for_trigger(buffer + "\n", clean_name)
204
+ triggered = bool(decided)
205
+ return bool(triggered)
206
+ finally:
207
+ try:
208
+ shutil.rmtree(cmd_file.parent, ignore_errors=True)
209
+ except OSError:
210
+ pass
211
+
212
+
213
+ def run_query_docker(query: str, skill_name: str, description: str,
214
+ workspace_root: Path, timeout: int) -> bool:
215
+ workspace_root.mkdir(parents=True, exist_ok=True)
216
+ unique = uuid.uuid4().hex[:8]
217
+ skills_in = workspace_root / "skills_in"
218
+ skills_in.mkdir(parents=True, exist_ok=True)
219
+ _, clean_name = write_synthetic_skill(skills_in, skill_name, description, unique)
220
+
221
+ creds_dir: Path | None = None
222
+ if _KEYCHAIN_CREDS:
223
+ creds_dir = workspace_root / "creds_in"
224
+ creds_dir.mkdir(parents=True, exist_ok=True)
225
+ (creds_dir / ".credentials.json").write_text(_KEYCHAIN_CREDS, encoding="utf-8")
226
+
227
+ container_script = f"""
228
+ set -e
229
+ mkdir -p /workspace/.claude/skills
230
+ cp -R /skills/. /workspace/.claude/skills/ 2>/dev/null || true
231
+ if [ -f /creds/.credentials.json ]; then
232
+ mkdir -p /home/evaluator/.claude
233
+ cp /creds/.credentials.json /home/evaluator/.claude/.credentials.json
234
+ fi
235
+ cd /workspace
236
+ claude -p "$EVAL_QUERY" \\
237
+ --output-format stream-json --verbose --include-partial-messages \\
238
+ --dangerously-skip-permissions \\
239
+ > /output/stream.jsonl 2>/dev/null || true
240
+ """
241
+
242
+ output_dir = workspace_root / "output"
243
+ output_dir.mkdir(parents=True, exist_ok=True)
244
+
245
+ cmd = [
246
+ "docker", "run", "--rm",
247
+ "-v", f"{skills_in}:/skills:ro",
248
+ "-v", f"{output_dir}:/output",
249
+ "-e", "ANTHROPIC_API_KEY",
250
+ "-e", f"EVAL_QUERY={query}",
251
+ ]
252
+ if creds_dir:
253
+ cmd += ["-v", f"{creds_dir}:/creds:ro"]
254
+ cmd += [DOCKER_IMAGE, "bash", "-c", container_script]
255
+
256
+ try:
257
+ subprocess.run(cmd, capture_output=True, timeout=timeout + 30)
258
+ except subprocess.TimeoutExpired:
259
+ pass
260
+
261
+ stream_file = output_dir / "stream.jsonl"
262
+ if not stream_file.is_file():
263
+ return False
264
+ decided, _ = parse_stream_for_trigger(stream_file.read_text(encoding="utf-8", errors="replace") + "\n", clean_name)
265
+ return bool(decided)
266
+
267
+
268
+ def main() -> int:
269
+ parser = argparse.ArgumentParser(description="Run trigger evals in isolation")
270
+ parser.add_argument("--skill-path", required=True, type=Path)
271
+ parser.add_argument("--triggers-file", required=True, type=Path)
272
+ parser.add_argument("--output-dir", required=True, type=Path)
273
+ parser.add_argument("--isolation", choices=("docker", "local"), required=True)
274
+ parser.add_argument("--workers", type=int, default=8)
275
+ parser.add_argument("--runs-per-query", type=int, default=3)
276
+ parser.add_argument("--timeout", type=int, default=45)
277
+ parser.add_argument("--threshold", type=float, default=0.5)
278
+ parser.add_argument("--quiet", action="store_true")
279
+ args = parser.parse_args()
280
+
281
+ skill_path = args.skill_path.resolve()
282
+ triggers_file = args.triggers_file.resolve()
283
+ if not triggers_file.is_file():
284
+ print(f"triggers file not found: {triggers_file}", file=sys.stderr)
285
+ return 2
286
+
287
+ skill_name, description, _ = parse_skill_md(skill_path)
288
+ queries = read_json(triggers_file)
289
+
290
+ run_id = new_run_id(f"{skill_name}-triggers")
291
+ run_dir = (args.output_dir / run_id).resolve()
292
+ (run_dir / "queries").mkdir(parents=True, exist_ok=True)
293
+
294
+ write_json(run_dir / "run.json", {
295
+ "run_id": run_id,
296
+ "skill_name": skill_name,
297
+ "description": description,
298
+ "isolation": args.isolation,
299
+ "started_at": utc_now_iso(),
300
+ "query_count": len(queries),
301
+ "runs_per_query": args.runs_per_query,
302
+ "threshold": args.threshold,
303
+ })
304
+
305
+ runner = run_query_docker if args.isolation == "docker" else run_query_local
306
+
307
+ def run_one(idx: int, q: dict, run_idx: int) -> tuple[int, bool]:
308
+ ws = run_dir / "queries" / f"q{idx:03d}-r{run_idx}"
309
+ triggered = runner(q["query"], skill_name, description, ws, args.timeout)
310
+ return idx, triggered
311
+
312
+ per_query: dict[int, list[bool]] = {}
313
+ if not args.quiet:
314
+ print(f"[run_triggers] {len(queries)} queries × {args.runs_per_query} runs, isolation={args.isolation}", file=sys.stderr)
315
+
316
+ with ThreadPoolExecutor(max_workers=args.workers) as pool:
317
+ futures = []
318
+ for idx, q in enumerate(queries):
319
+ for run_idx in range(args.runs_per_query):
320
+ futures.append(pool.submit(run_one, idx, q, run_idx))
321
+ for fut in as_completed(futures):
322
+ try:
323
+ idx, triggered = fut.result()
324
+ except Exception as e:
325
+ print(f"Warning: query failed: {e}", file=sys.stderr)
326
+ continue
327
+ per_query.setdefault(idx, []).append(triggered)
328
+
329
+ results = []
330
+ for idx, q in enumerate(queries):
331
+ triggers = per_query.get(idx, [])
332
+ rate = (sum(triggers) / len(triggers)) if triggers else 0.0
333
+ should = bool(q["should_trigger"])
334
+ if should:
335
+ passed = rate >= args.threshold
336
+ else:
337
+ passed = rate < args.threshold
338
+ results.append({
339
+ "query": q["query"],
340
+ "should_trigger": should,
341
+ "trigger_rate": rate,
342
+ "triggers": int(sum(triggers)),
343
+ "runs": len(triggers),
344
+ "pass": passed,
345
+ })
346
+
347
+ output = {
348
+ "run_id": run_id,
349
+ "completed_at": utc_now_iso(),
350
+ "skill_name": skill_name,
351
+ "description": description,
352
+ "isolation": args.isolation,
353
+ "results": results,
354
+ "summary": {
355
+ "total": len(results),
356
+ "passed": sum(1 for r in results if r["pass"]),
357
+ "failed": sum(1 for r in results if not r["pass"]),
358
+ },
359
+ }
360
+ write_json(run_dir / "triggers-result.json", output)
361
+ print(json.dumps(output, indent=2))
362
+ return 0
363
+
364
+
365
+ if __name__ == "__main__":
366
+ sys.exit(main())
@@ -0,0 +1,260 @@
1
+ #!/usr/bin/env python3
2
+ # /// script
3
+ # requires-python = ">=3.9"
4
+ # ///
5
+ """Shared helpers for the eval runner."""
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import re
11
+ import shutil
12
+ import subprocess
13
+ import sys
14
+ from datetime import datetime, timezone
15
+ from pathlib import Path
16
+
17
+
18
+ def parse_skill_md(skill_path: Path) -> tuple[str, str, str]:
19
+ """Return (name, description, body) from the skill's SKILL.md frontmatter."""
20
+ text = (skill_path / "SKILL.md").read_text(encoding="utf-8")
21
+ fm_match = re.match(r"^---\s*\n(.*?)\n---\s*\n(.*)$", text, re.DOTALL)
22
+ if not fm_match:
23
+ raise ValueError(f"SKILL.md at {skill_path} is missing frontmatter")
24
+ frontmatter, body = fm_match.group(1), fm_match.group(2)
25
+
26
+ name = None
27
+ description_lines: list[str] = []
28
+ in_description = False
29
+ for line in frontmatter.splitlines():
30
+ if line.startswith("name:"):
31
+ name = line.split(":", 1)[1].strip()
32
+ in_description = False
33
+ elif line.startswith("description:"):
34
+ value = line.split(":", 1)[1].strip()
35
+ if value in ("|", ">"):
36
+ in_description = True
37
+ else:
38
+ description_lines = [value]
39
+ in_description = False
40
+ elif in_description and line.startswith((" ", "\t")):
41
+ description_lines.append(line.strip())
42
+ elif in_description:
43
+ in_description = False
44
+
45
+ if not name:
46
+ raise ValueError(f"SKILL.md at {skill_path} is missing a name")
47
+ return name, " ".join(description_lines).strip(), body
48
+
49
+
50
+ def discover_project_root(skill_path: Path) -> Path:
51
+ """Walk up from the skill looking for _bmad/ or .git; default to skill's grandparent."""
52
+ for parent in [skill_path, *skill_path.parents]:
53
+ if (parent / "_bmad").is_dir() or (parent / ".git").exists():
54
+ return parent
55
+ return skill_path.parent.parent
56
+
57
+
58
+ def discover_evals(
59
+ skill_path: Path,
60
+ project_root: Path,
61
+ explicit: Path | None,
62
+ ) -> dict[str, Path]:
63
+ """Locate evals.json and triggers.json. Return dict with keys 'evals' and/or 'triggers'."""
64
+ found: dict[str, Path] = {}
65
+
66
+ def check_dir(d: Path) -> None:
67
+ if not d.is_dir():
68
+ return
69
+ for key, fname in (("evals", "evals.json"), ("triggers", "triggers.json")):
70
+ candidate = d / fname
71
+ if candidate.is_file() and key not in found:
72
+ found[key] = candidate
73
+
74
+ if explicit is not None:
75
+ explicit = explicit.resolve()
76
+ if explicit.is_file():
77
+ if explicit.name == "evals.json":
78
+ found["evals"] = explicit
79
+ elif explicit.name == "triggers.json":
80
+ found["triggers"] = explicit
81
+ elif explicit.is_dir():
82
+ check_dir(explicit)
83
+ return found
84
+
85
+ skill_name = skill_path.name
86
+ candidates: list[Path] = [
87
+ skill_path / "evals",
88
+ skill_path.parent.parent / "evals" / skill_name,
89
+ project_root / "evals" / skill_name,
90
+ ]
91
+ for d in candidates:
92
+ check_dir(d)
93
+ if found:
94
+ break
95
+
96
+ if not found:
97
+ evals_root = project_root / "evals"
98
+ if evals_root.is_dir():
99
+ for sub in evals_root.rglob(skill_name):
100
+ if sub.is_dir():
101
+ check_dir(sub)
102
+ if found:
103
+ break
104
+
105
+ return found
106
+
107
+
108
+ def utc_now_iso() -> str:
109
+ return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
110
+
111
+
112
+ def new_run_id(skill_name: str) -> str:
113
+ return f"{datetime.now().strftime('%Y%m%d-%H%M%S')}-{skill_name}"
114
+
115
+
116
+ def have_docker() -> bool:
117
+ if shutil.which("docker") is None:
118
+ return False
119
+ try:
120
+ result = subprocess.run(
121
+ ["docker", "info"],
122
+ stdout=subprocess.DEVNULL,
123
+ stderr=subprocess.DEVNULL,
124
+ timeout=5,
125
+ )
126
+ return result.returncode == 0
127
+ except Exception:
128
+ return False
129
+
130
+
131
+ def docker_image_present(image: str = "bmad-eval-runner:latest") -> bool:
132
+ if not have_docker():
133
+ return False
134
+ try:
135
+ result = subprocess.run(
136
+ ["docker", "image", "inspect", image],
137
+ stdout=subprocess.DEVNULL,
138
+ stderr=subprocess.DEVNULL,
139
+ timeout=10,
140
+ )
141
+ return result.returncode == 0
142
+ except Exception:
143
+ return False
144
+
145
+
146
+ def read_macos_keychain_credentials() -> str | None:
147
+ """Read the Claude Code OAuth credentials JSON from the macOS Keychain.
148
+
149
+ Returns the raw JSON string stored under service "Claude Code-credentials",
150
+ or None if unavailable (non-macOS, entry missing, or access denied).
151
+
152
+ Called in the parent process — which owns the Keychain ACL — so the credential
153
+ can be staged into each isolated workspace's `.claude/.credentials.json` before
154
+ `claude -p` is launched. Without this, an isolated subprocess with HOME pointed
155
+ at an empty dir has no auth and every eval fails with "Not logged in."
156
+ """
157
+ if sys.platform != "darwin":
158
+ return None
159
+ try:
160
+ result = subprocess.run(
161
+ ["security", "find-generic-password", "-s", "Claude Code-credentials", "-w"],
162
+ capture_output=True,
163
+ timeout=5,
164
+ )
165
+ if result.returncode != 0:
166
+ return None
167
+ val = result.stdout.decode("utf-8", errors="replace").strip()
168
+ return val if val else None
169
+ except Exception:
170
+ return None
171
+
172
+
173
+ def stage_credentials(claude_dir: Path, credentials_json: str | None) -> None:
174
+ """Write credentials_json to <claude_dir>/.credentials.json. No-op if None."""
175
+ if not credentials_json:
176
+ return
177
+ claude_dir.mkdir(parents=True, exist_ok=True)
178
+ (claude_dir / ".credentials.json").write_text(credentials_json, encoding="utf-8")
179
+
180
+
181
+ def write_json(path: Path, data: object) -> None:
182
+ path.parent.mkdir(parents=True, exist_ok=True)
183
+ path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
184
+
185
+
186
+ def read_json(path: Path) -> object:
187
+ return json.loads(path.read_text(encoding="utf-8"))
188
+
189
+
190
+ def parse_skill_dependencies(skill_path: Path) -> list[str]:
191
+ """Return skill names declared under 'dependencies:' in SKILL.md frontmatter."""
192
+ try:
193
+ text = (skill_path / "SKILL.md").read_text(encoding="utf-8")
194
+ except (FileNotFoundError, OSError):
195
+ return []
196
+ fm = re.match(r"^---\s*\n(.*?)\n---", text, re.DOTALL)
197
+ if not fm:
198
+ return []
199
+ deps: list[str] = []
200
+ in_deps = False
201
+ for line in fm.group(1).splitlines():
202
+ if re.match(r"^dependencies\s*:", line):
203
+ in_deps = True
204
+ elif in_deps:
205
+ m = re.match(r"^\s+-\s+(\S+)", line)
206
+ if m:
207
+ deps.append(m.group(1))
208
+ elif not line.startswith((" ", "\t")):
209
+ break
210
+ return deps
211
+
212
+
213
+ def discover_setup_dirs(evals_file: Path, eval_id: str | None = None) -> list[Path]:
214
+ """Return ordered list of setup overlay dirs that exist.
215
+
216
+ base: <evals_dir>/setup/
217
+ per-eval: <evals_dir>/<eval_id>/setup/
218
+
219
+ Applied base-first so per-eval overlays win on conflict.
220
+ """
221
+ evals_dir = evals_file.parent
222
+ dirs: list[Path] = []
223
+ base = evals_dir / "setup"
224
+ if base.is_dir():
225
+ dirs.append(base)
226
+ if eval_id:
227
+ per_eval = evals_dir / eval_id / "setup"
228
+ if per_eval.is_dir():
229
+ dirs.append(per_eval)
230
+ return dirs
231
+
232
+
233
+ def apply_setup_overlay(setup_dirs: list[Path], dest: Path) -> None:
234
+ """Rsync each setup dir onto dest in order (base first, per-eval last)."""
235
+ dest.mkdir(parents=True, exist_ok=True)
236
+ for src in setup_dirs:
237
+ if not src.is_dir():
238
+ continue
239
+ subprocess.run(
240
+ ["rsync", "-a", f"{src}/", f"{dest}/"],
241
+ check=False,
242
+ )
243
+
244
+
245
+ __all__ = [
246
+ "parse_skill_md",
247
+ "discover_project_root",
248
+ "discover_evals",
249
+ "utc_now_iso",
250
+ "new_run_id",
251
+ "have_docker",
252
+ "docker_image_present",
253
+ "read_macos_keychain_credentials",
254
+ "stage_credentials",
255
+ "write_json",
256
+ "read_json",
257
+ "parse_skill_dependencies",
258
+ "discover_setup_dirs",
259
+ "apply_setup_overlay",
260
+ ]
@@ -1 +1 @@
1
- module,skill,display-name,menu-code,description,action,args,phase,after,before,required,output-location,outputs
1
+ module,skill,display-name,menu-code,description,action,args,phase,preceded-by,followed-by,required,output-location,outputs
@@ -197,9 +197,37 @@ def cleanup_directories(
197
197
  return removed, not_found, total_files
198
198
 
199
199
 
200
+ def reject_unresolved_paths(named_paths: list[tuple[str, str]]) -> None:
201
+ """Exit with a clear error if any path argument still contains the literal
202
+ ``{project-root}`` token. That token is meaningful only inside config
203
+ values; filesystem path arguments must be resolved by the caller. Failing
204
+ loudly here prevents silently operating on a junk ``{project-root}/`` directory.
205
+ """
206
+ for name, value in named_paths:
207
+ if value and "{project-root}" in value:
208
+ print(
209
+ json.dumps(
210
+ {
211
+ "status": "error",
212
+ "error": (
213
+ f"Unresolved '{{project-root}}' token in {name} path: {value!r}. "
214
+ "Resolve '{project-root}' to the actual project root before running "
215
+ "this script — it is a filesystem path, not a config value."
216
+ ),
217
+ },
218
+ indent=2,
219
+ )
220
+ )
221
+ sys.exit(1)
222
+
223
+
200
224
  def main():
201
225
  args = parse_args()
202
226
 
227
+ reject_unresolved_paths(
228
+ [("--bmad-dir", args.bmad_dir), ("--skills-dir", args.skills_dir)]
229
+ )
230
+
203
231
  bmad_dir = args.bmad_dir
204
232
  module_code = args.module_code
205
233