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.
- package/bin/cli.js +11 -6
- package/docs/architecture.md +18 -0
- package/lib/bmad-cache/bmb/.claude-plugin/marketplace.json +1 -1
- package/lib/bmad-cache/bmb/_git_preserved/hooks/commit-msg.sample +52 -2
- package/lib/bmad-cache/bmb/_git_preserved/hooks/fsmonitor-watchman.sample +2 -8
- package/lib/bmad-cache/bmb/_git_preserved/index +0 -0
- package/lib/bmad-cache/bmb/_git_preserved/objects/pack/pack-8f8b045fef5af6911495cf3b2a89f1ed75e120f7.idx +0 -0
- package/lib/bmad-cache/bmb/_git_preserved/objects/pack/pack-8f8b045fef5af6911495cf3b2a89f1ed75e120f7.pack +0 -0
- package/lib/bmad-cache/bmb/_git_preserved/objects/pack/pack-8f8b045fef5af6911495cf3b2a89f1ed75e120f7.rev +0 -0
- package/lib/bmad-cache/bmb/_git_preserved/packed-refs +1 -1
- package/lib/bmad-cache/bmb/_git_preserved/refs/heads/main +1 -1
- package/lib/bmad-cache/bmb/_git_preserved/shallow +1 -1
- package/lib/bmad-cache/bmb/package-lock.json +2 -2
- package/lib/bmad-cache/bmb/package.json +1 -1
- package/lib/bmad-cache/bmb/samples/bmad-agent-dream-weaver/assets/module-help.csv +1 -1
- package/lib/bmad-cache/bmb/samples/bmad-agent-dream-weaver/scripts/merge-config.py +33 -0
- package/lib/bmad-cache/bmb/samples/bmad-agent-dream-weaver/scripts/merge-help-csv.py +28 -0
- package/lib/bmad-cache/bmb/samples/sample-module-setup/assets/module-help.csv +1 -1
- package/lib/bmad-cache/bmb/samples/sample-module-setup/scripts/cleanup-legacy.py +28 -0
- package/lib/bmad-cache/bmb/samples/sample-module-setup/scripts/merge-config.py +33 -0
- package/lib/bmad-cache/bmb/samples/sample-module-setup/scripts/merge-help-csv.py +28 -0
- package/lib/bmad-cache/bmb/skills/bmad-bmb-setup/assets/module-help.csv +1 -1
- package/lib/bmad-cache/bmb/skills/bmad-bmb-setup/scripts/cleanup-legacy.py +28 -0
- package/lib/bmad-cache/bmb/skills/bmad-bmb-setup/scripts/merge-config.py +33 -0
- package/lib/bmad-cache/bmb/skills/bmad-bmb-setup/scripts/merge-help-csv.py +28 -0
- package/lib/bmad-cache/bmb/skills/bmad-eval-runner/assets/Dockerfile +29 -0
- package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/docker_setup.py +115 -0
- package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/generate_report.py +184 -0
- package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/pty_runner.py +171 -0
- package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/run_evals.py +492 -0
- package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/run_triggers.py +366 -0
- package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/utils.py +260 -0
- package/lib/bmad-cache/bmb/skills/bmad-module-builder/assets/setup-skill-template/assets/module-help.csv +1 -1
- package/lib/bmad-cache/bmb/skills/bmad-module-builder/assets/setup-skill-template/scripts/cleanup-legacy.py +28 -0
- package/lib/bmad-cache/bmb/skills/bmad-module-builder/assets/setup-skill-template/scripts/merge-config.py +33 -0
- package/lib/bmad-cache/bmb/skills/bmad-module-builder/assets/setup-skill-template/scripts/merge-help-csv.py +28 -0
- package/lib/bmad-cache/bmb/skills/bmad-module-builder/assets/standalone-module-template/merge-config.py +33 -0
- package/lib/bmad-cache/bmb/skills/bmad-module-builder/assets/standalone-module-template/merge-help-csv.py +28 -0
- package/lib/bmad-cache/bmb/skills/bmad-module-builder/scripts/tests/test-validate-module.py +74 -1
- package/lib/bmad-cache/bmb/skills/bmad-module-builder/scripts/validate-module.py +24 -13
- package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/assets/sample-customize-product-brief.toml +48 -33
- package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/extract-report-json.py +287 -0
- package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/generate-html-report.py +57 -8
- package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/prepass-prompt-metrics.py +7 -7
- package/lib/bmad-cache/bmb/skills/module-help.csv +1 -1
- package/lib/bmad-cache/bmb/website/public/img/eval-test-types.png +0 -0
- package/lib/bmad-cache/cache-manifest.json +17 -18
- package/lib/bmad-cache/cis/_git_preserved/hooks/commit-msg.sample +52 -2
- package/lib/bmad-cache/cis/_git_preserved/hooks/fsmonitor-watchman.sample +2 -8
- package/lib/bmad-cache/cis/_git_preserved/index +0 -0
- package/lib/bmad-cache/cis/_git_preserved/objects/pack/pack-18c8290560a98bcb7bf0676e6cc9b2ac5ca2823e.idx +0 -0
- package/lib/bmad-cache/cis/_git_preserved/objects/pack/{pack-42ffc048f54e58ce94c6331bc6be97ebbb7936f2.pack → pack-18c8290560a98bcb7bf0676e6cc9b2ac5ca2823e.pack} +0 -0
- package/lib/bmad-cache/cis/_git_preserved/objects/pack/pack-18c8290560a98bcb7bf0676e6cc9b2ac5ca2823e.rev +0 -0
- package/lib/bmad-cache/cis/_git_preserved/packed-refs +1 -1
- package/lib/bmad-cache/cis/_git_preserved/refs/heads/main +1 -1
- package/lib/bmad-cache/cis/_git_preserved/refs/tags/v0.2.1 +1 -0
- package/lib/bmad-cache/cis/_git_preserved/shallow +1 -1
- package/lib/bmad-cache/cis/package-lock.json +2 -2
- package/lib/bmad-cache/cis/package.json +1 -1
- package/lib/bmad-cache/cis/src/module-help.csv +1 -1
- package/lib/bmad-cache/gds/.claude-plugin/marketplace.json +4 -7
- package/lib/bmad-cache/gds/README.md +3 -1
- package/lib/bmad-cache/gds/_git_preserved/hooks/commit-msg.sample +52 -2
- package/lib/bmad-cache/gds/_git_preserved/hooks/fsmonitor-watchman.sample +2 -8
- package/lib/bmad-cache/gds/_git_preserved/index +0 -0
- package/lib/bmad-cache/gds/_git_preserved/objects/pack/pack-dcb7c556d9bb6b6b70d2301e094eaac6d7300552.idx +0 -0
- package/lib/bmad-cache/gds/_git_preserved/objects/pack/{pack-9427a146a90c00bb542cba038874bf9671ba4dc0.pack → pack-dcb7c556d9bb6b6b70d2301e094eaac6d7300552.pack} +0 -0
- package/lib/bmad-cache/gds/_git_preserved/objects/pack/pack-dcb7c556d9bb6b6b70d2301e094eaac6d7300552.rev +0 -0
- package/lib/bmad-cache/gds/_git_preserved/packed-refs +1 -1
- package/lib/bmad-cache/gds/_git_preserved/refs/heads/main +1 -1
- package/lib/bmad-cache/gds/_git_preserved/shallow +1 -1
- package/lib/bmad-cache/gds/package.json +1 -1
- package/lib/bmad-cache/gds/src/agents/gds-agent-game-designer/customize.toml +5 -5
- package/lib/bmad-cache/gds/src/agents/gds-agent-game-dev/customize.toml +5 -5
- package/lib/bmad-cache/gds/src/agents/gds-agent-game-solo-dev/customize.toml +0 -5
- package/lib/bmad-cache/gds/src/module-help.csv +6 -12
- package/lib/bmad-cache/gds/src/module.yaml +1 -1
- package/lib/bmad-cache/gds/src/workflows/1-preproduction/gds-create-game-brief/customize.toml +97 -22
- package/lib/bmad-cache/gds/src/workflows/2-design/gds-gdd/assets/validation-report-template.html +190 -0
- package/lib/bmad-cache/gds/src/workflows/2-design/gds-gdd/customize.toml +99 -0
- package/lib/bmad-cache/gds/src/workflows/2-design/gds-gdd/scripts/render-validation-html.py +290 -0
- package/lib/bmad-cache/gds/src/workflows/2-design/gds-prd/assets/validation-report-template.html +190 -0
- package/lib/bmad-cache/gds/src/workflows/2-design/gds-prd/customize.toml +84 -0
- package/lib/bmad-cache/gds/src/workflows/2-design/gds-ux/assets/validation-report-template.html +319 -0
- package/lib/bmad-cache/gds/src/workflows/2-design/gds-ux/customize.toml +101 -0
- package/lib/bmad-cache/gds/src/workflows/3-technical/gds-game-architecture/architecture-patterns.yaml +1 -0
- package/lib/bmad-cache/gds/src/workflows/3-technical/gds-game-architecture/decision-catalog.yaml +88 -0
- package/lib/bmad-cache/gds/src/workflows/3-technical/gds-game-architecture/engine-mcps.yaml +124 -2
- package/lib/bmad-cache/gds/src/workflows/4-production/gds-investigate/customize.toml +62 -0
- package/lib/bmad-cache/tea/.claude-plugin/marketplace.json +1 -1
- package/lib/bmad-cache/tea/.github/workflows/docs.yaml +3 -3
- package/lib/bmad-cache/tea/.github/workflows/quality.yaml +10 -10
- package/lib/bmad-cache/tea/AGENTS.md +31 -0
- package/lib/bmad-cache/tea/CHANGELOG.md +42 -1
- package/lib/bmad-cache/tea/README.md +8 -5
- package/lib/bmad-cache/tea/_git_preserved/hooks/commit-msg.sample +52 -2
- package/lib/bmad-cache/tea/_git_preserved/hooks/fsmonitor-watchman.sample +2 -8
- package/lib/bmad-cache/tea/_git_preserved/index +0 -0
- package/lib/bmad-cache/tea/_git_preserved/objects/pack/pack-9e4197e37df7763dd7a05c2965ee921dfd2eb617.idx +0 -0
- package/lib/bmad-cache/tea/_git_preserved/objects/pack/{pack-f0df537f2649464ff6c5aee241165eb9c8664227.pack → pack-9e4197e37df7763dd7a05c2965ee921dfd2eb617.pack} +0 -0
- package/lib/bmad-cache/tea/_git_preserved/objects/pack/pack-9e4197e37df7763dd7a05c2965ee921dfd2eb617.rev +0 -0
- package/lib/bmad-cache/tea/_git_preserved/packed-refs +1 -1
- package/lib/bmad-cache/tea/_git_preserved/refs/heads/main +1 -1
- package/lib/bmad-cache/tea/_git_preserved/refs/tags/v1.19.0 +1 -0
- package/lib/bmad-cache/tea/_git_preserved/shallow +1 -1
- package/lib/bmad-cache/tea/docs/explanation/engagement-models.md +15 -16
- package/lib/bmad-cache/tea/docs/explanation/knowledge-base-system.md +2 -0
- package/lib/bmad-cache/tea/docs/explanation/risk-based-testing.md +1 -1
- package/lib/bmad-cache/tea/docs/explanation/tea-overview.md +88 -52
- package/lib/bmad-cache/tea/docs/explanation/testing-as-engineering.md +13 -12
- package/lib/bmad-cache/tea/docs/glossary/index.md +2 -2
- package/lib/bmad-cache/tea/docs/how-to/brownfield/use-tea-for-enterprise.md +19 -18
- package/lib/bmad-cache/tea/docs/how-to/brownfield/use-tea-with-existing-tests.md +1 -1
- package/lib/bmad-cache/tea/docs/how-to/workflows/run-nfr-assess.md +32 -26
- package/lib/bmad-cache/tea/docs/how-to/workflows/run-test-design.md +20 -14
- package/lib/bmad-cache/tea/docs/how-to/workflows/run-trace.md +3 -3
- package/lib/bmad-cache/tea/docs/index.md +13 -11
- package/lib/bmad-cache/tea/docs/reference/commands.md +37 -13
- package/lib/bmad-cache/tea/docs/reference/knowledge-base.md +2 -2
- package/lib/bmad-cache/tea/package-lock.json +2 -2
- package/lib/bmad-cache/tea/package.json +1 -1
- package/lib/bmad-cache/tea/src/agents/bmad-tea/customize.toml +20 -15
- package/lib/bmad-cache/tea/src/agents/bmad-tea/resources/knowledge/confidence-gate.md +73 -0
- package/lib/bmad-cache/tea/src/agents/bmad-tea/resources/knowledge/test-quality.md +1 -0
- package/lib/bmad-cache/tea/src/agents/bmad-tea/resources/tea-index.csv +2 -1
- package/lib/bmad-cache/tea/src/module-help.csv +2 -2
- package/lib/bmad-cache/tea/src/workflows/testarch/README.md +5 -4
- package/lib/bmad-cache/tea/src/workflows/testarch/bmad-teach-me-testing/data/role-paths.yaml +1 -1
- package/lib/bmad-cache/tea/src/workflows/testarch/bmad-teach-me-testing/data/tea-resources-index.yaml +1 -1
- package/lib/bmad-cache/tea/src/workflows/testarch/bmad-teach-me-testing/steps-c/step-04-session-01.md +2 -2
- package/lib/bmad-cache/tea/src/workflows/testarch/bmad-teach-me-testing/steps-c/step-04-session-07.md +1 -1
- package/lib/bmad-cache/tea/src/workflows/testarch/bmad-teach-me-testing/templates/certificate-template.md +1 -1
- package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-atdd/resources/tea-index.csv +1 -1
- package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-automate/resources/tea-index.csv +1 -1
- package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-ci/resources/tea-index.csv +1 -1
- package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-framework/resources/tea-index.csv +1 -1
- package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-nfr/SKILL.md +3 -3
- package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-nfr/checklist.md +11 -11
- package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-nfr/instructions.md +4 -2
- package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-nfr/nfr-report-template.md +5 -5
- package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-nfr/resources/tea-index.csv +1 -1
- package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-nfr/steps-c/step-01-load-context.md +1 -1
- package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-nfr/steps-c/step-01b-resume.md +1 -1
- package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-nfr/steps-c/step-02-define-thresholds.md +14 -3
- package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-nfr/steps-c/step-04-evaluate-and-score.md +7 -7
- package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-nfr/steps-c/step-04a-subagent-security.md +4 -4
- package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-nfr/steps-c/step-04b-subagent-performance.md +4 -4
- package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-nfr/steps-c/step-04c-subagent-reliability.md +4 -4
- package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-nfr/steps-c/step-04d-subagent-scalability.md +4 -4
- package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-nfr/steps-c/step-04e-aggregate-nfr.md +4 -4
- package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-nfr/steps-c/step-05-generate-report.md +1 -1
- package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-nfr/workflow-plan.md +1 -1
- package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-nfr/workflow.yaml +3 -3
- package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-test-design/checklist.md +23 -3
- package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-test-design/resources/tea-index.csv +1 -1
- package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-test-design/steps-c/step-02-load-context.md +7 -0
- package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-test-design/steps-c/step-03-risk-and-testability.md +16 -2
- package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-test-design/steps-c/step-04-coverage-plan.md +20 -4
- package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-test-design/steps-c/step-05-generate-output.md +2 -0
- package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-test-design/test-design-architecture-template.md +17 -0
- package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-test-design/test-design-qa-template.md +15 -0
- package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-test-design/test-design-template.md +16 -0
- package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-test-review/resources/tea-index.csv +1 -1
- package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-trace/checklist.md +1 -1
- package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-trace/resources/tea-index.csv +1 -1
- package/lib/bmad-cache/tea/src/workflows/testarch/bmad-testarch-trace/trace-template.md +1 -1
- package/lib/bmad-cache/tea/test/test-installation-components.js +49 -0
- package/lib/bmad-cache/tea/website/astro.config.mjs +2 -2
- package/lib/bmad-cache/wds/README.md +1 -1
- package/lib/bmad-cache/wds/_git_preserved/hooks/commit-msg.sample +52 -2
- package/lib/bmad-cache/wds/_git_preserved/hooks/fsmonitor-watchman.sample +2 -8
- package/lib/bmad-cache/wds/_git_preserved/index +0 -0
- package/lib/bmad-cache/wds/_git_preserved/objects/pack/pack-656c3d8d5426e73043b6a7f45eedaab74e3c419e.idx +0 -0
- package/lib/bmad-cache/wds/_git_preserved/objects/pack/{pack-96877c1c09123cccb1f91c1412184b11d2b492ad.pack → pack-656c3d8d5426e73043b6a7f45eedaab74e3c419e.pack} +0 -0
- package/lib/bmad-cache/wds/_git_preserved/objects/pack/pack-656c3d8d5426e73043b6a7f45eedaab74e3c419e.rev +0 -0
- package/lib/bmad-cache/wds/_git_preserved/packed-refs +1 -1
- package/lib/bmad-cache/wds/_git_preserved/refs/heads/main +1 -1
- package/lib/bmad-cache/wds/_git_preserved/refs/tags/v0.4.3 +1 -0
- package/lib/bmad-cache/wds/_git_preserved/shallow +1 -1
- package/lib/bmad-cache/wds/eslint.config.mjs +1 -1
- package/lib/bmad-cache/wds/package.json +1 -1
- package/lib/bmad-cache/wds/src/agents/wds-agent-freya-ux/customize.toml +80 -0
- package/lib/bmad-cache/wds/src/agents/wds-agent-mimir-builder/customize.toml +52 -0
- package/lib/bmad-cache/wds/src/agents/wds-agent-saga-analyst/customize.toml +70 -0
- package/lib/bmad-cache/wds/src/module-help.csv +19 -19
- package/lib/bmad-cache/wds/src/module.yaml +28 -0
- package/lib/bmad-cache/wds/src/scripts/README.md +155 -0
- package/lib/bmad-cache/wds/src/scripts/wds-add-object.js +202 -0
- package/lib/bmad-cache/wds/src/scripts/wds-add-spacing.js +158 -0
- package/lib/bmad-cache/wds/src/scripts/wds-init-page.js +229 -0
- package/lib/bmad-cache/wds/src/scripts/wds-init-scenario.js +120 -0
- package/lib/bmad-cache/wds/src/scripts/wds-nav.js +201 -0
- package/lib/bmad-cache/wds/src/scripts/wds-validate.js +301 -0
- package/lib/bmad-cache/wds/src/workflows/wds-3-scenarios/workflow.xml +450 -0
- package/lib/bmad-cache/wds/src/workflows/wds-4-ux-design/workflow-specify.xml +387 -0
- package/lib/bmad-extension/.claude-plugin/marketplace.json.template +1 -1
- package/lib/bmad-extension-plugin/.claude-plugin/marketplace.json +2 -2
- package/lib/bmad.js +91 -7
- package/lib/installer.js +28 -6
- package/lib/mil498-templates/OCD.md +169 -169
- package/lib/mil498-templates/README.md +4 -4
- package/lib/mil498-templates/SDD.md +163 -163
- package/lib/mil498-templates/SDP.md +307 -307
- package/lib/mil498-templates/SRS.md +219 -219
- package/lib/mil498-templates/SSDD.md +154 -154
- package/lib/mil498-templates/SSS.md +225 -225
- package/lib/mil498-templates/STD.md +188 -188
- package/lib/templates/instruction-block-git.template.md +25 -0
- package/package.json +5 -4
- package/scripts/build-bmad-cache.js +143 -42
- package/skills/git-workflow-skill/skill.json +21 -21
- package/lib/bmad-cache/bmb/_git_preserved/objects/pack/pack-6ecd9fc6445b1281449c5ec49a6c5794708e662e.idx +0 -0
- package/lib/bmad-cache/bmb/_git_preserved/objects/pack/pack-6ecd9fc6445b1281449c5ec49a6c5794708e662e.pack +0 -0
- package/lib/bmad-cache/bmb/_git_preserved/objects/pack/pack-6ecd9fc6445b1281449c5ec49a6c5794708e662e.rev +0 -0
- package/lib/bmad-cache/bmb/_git_preserved/refs/remotes/origin/HEAD +0 -1
- package/lib/bmad-cache/bmb/_git_preserved/refs/tags/v1.7.0 +0 -1
- package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/generate-convert-report.py +0 -406
- package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/tests/test_generate_convert_report.py +0 -243
- package/lib/bmad-cache/cis/_git_preserved/objects/pack/pack-42ffc048f54e58ce94c6331bc6be97ebbb7936f2.idx +0 -0
- package/lib/bmad-cache/cis/_git_preserved/objects/pack/pack-42ffc048f54e58ce94c6331bc6be97ebbb7936f2.rev +0 -0
- package/lib/bmad-cache/cis/_git_preserved/refs/remotes/origin/HEAD +0 -1
- package/lib/bmad-cache/gds/_git_preserved/objects/pack/pack-9427a146a90c00bb542cba038874bf9671ba4dc0.idx +0 -0
- package/lib/bmad-cache/gds/_git_preserved/objects/pack/pack-9427a146a90c00bb542cba038874bf9671ba4dc0.rev +0 -0
- package/lib/bmad-cache/gds/_git_preserved/refs/remotes/origin/HEAD +0 -1
- package/lib/bmad-cache/gds/src/workflows/2-design/gds-create-gdd/customize.toml +0 -41
- package/lib/bmad-cache/gds/src/workflows/2-design/gds-create-prd/customize.toml +0 -41
- package/lib/bmad-cache/gds/src/workflows/2-design/gds-create-prd/data/domain-complexity.csv +0 -15
- package/lib/bmad-cache/gds/src/workflows/2-design/gds-create-prd/data/project-types.csv +0 -11
- package/lib/bmad-cache/gds/src/workflows/2-design/gds-create-ux-design/customize.toml +0 -41
- package/lib/bmad-cache/gds/src/workflows/2-design/gds-edit-gdd/customize.toml +0 -41
- package/lib/bmad-cache/gds/src/workflows/2-design/gds-edit-prd/customize.toml +0 -41
- package/lib/bmad-cache/gds/src/workflows/2-design/gds-validate-gdd/customize.toml +0 -41
- package/lib/bmad-cache/gds/src/workflows/2-design/gds-validate-prd/customize.toml +0 -41
- package/lib/bmad-cache/gds/src/workflows/2-design/gds-validate-prd/data/domain-complexity.csv +0 -15
- package/lib/bmad-cache/gds/src/workflows/2-design/gds-validate-prd/data/project-types.csv +0 -11
- package/lib/bmad-cache/tea/_git_preserved/objects/pack/pack-f0df537f2649464ff6c5aee241165eb9c8664227.idx +0 -0
- package/lib/bmad-cache/tea/_git_preserved/objects/pack/pack-f0df537f2649464ff6c5aee241165eb9c8664227.rev +0 -0
- package/lib/bmad-cache/tea/_git_preserved/refs/remotes/origin/HEAD +0 -1
- package/lib/bmad-cache/wds/_git_preserved/objects/pack/pack-96877c1c09123cccb1f91c1412184b11d2b492ad.idx +0 -0
- package/lib/bmad-cache/wds/_git_preserved/objects/pack/pack-96877c1c09123cccb1f91c1412184b11d2b492ad.rev +0 -0
- package/lib/bmad-cache/wds/_git_preserved/refs/remotes/origin/HEAD +0 -1
- package/lib/bmad-cache/wds/src/agents/wds-agent-freya-ux/bmad-skill-manifest.yaml +0 -12
- package/lib/bmad-cache/wds/src/agents/wds-agent-saga-analyst/bmad-skill-manifest.yaml +0 -12
- package/lib/bmad-cache/wds/src/workflows/wds-0-alignment-signoff/bmad-skill-manifest.yaml +0 -1
- package/lib/bmad-cache/wds/src/workflows/wds-0-project-setup/bmad-skill-manifest.yaml +0 -1
- package/lib/bmad-cache/wds/src/workflows/wds-1-project-brief/bmad-skill-manifest.yaml +0 -1
- package/lib/bmad-cache/wds/src/workflows/wds-2-trigger-mapping/bmad-skill-manifest.yaml +0 -1
- package/lib/bmad-cache/wds/src/workflows/wds-3-scenarios/bmad-skill-manifest.yaml +0 -1
- package/lib/bmad-cache/wds/src/workflows/wds-4-ux-design/bmad-skill-manifest.yaml +0 -1
- package/lib/bmad-cache/wds/src/workflows/wds-5-agentic-development/bmad-skill-manifest.yaml +0 -1
- package/lib/bmad-cache/wds/src/workflows/wds-6-asset-generation/bmad-skill-manifest.yaml +0 -1
- package/lib/bmad-cache/wds/src/workflows/wds-7-design-system/bmad-skill-manifest.yaml +0 -1
- package/lib/bmad-cache/wds/src/workflows/wds-8-product-evolution/bmad-skill-manifest.yaml +0 -1
- /package/lib/bmad-cache/gds/src/workflows/2-design/{gds-create-gdd → gds-gdd/assets}/game-types.csv +0 -0
- /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,
|
|
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
|
|