bmad-method-test-architecture-enterprise 2.0.4-next.3 → 3.0.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/.claude-plugin/marketplace.json +10 -10
- package/README.md +29 -13
- package/cli/atdd-red-check.js +1 -1
- package/cli/atdd-runner.js +1 -1
- package/cli/automate.js +3 -3
- package/cli/ci-runner.js +1 -1
- package/cli/lib/automate-prompt.js +1 -1
- package/cli/lib/framework-setup.js +2 -2
- package/cli/lib/resolve-skill.js +2 -2
- package/cli/lib/resolve-tea-config.js +54 -9
- package/cli/lib/teach-turn.js +558 -0
- package/cli/lib/test-design-parser.js +1 -1
- package/cli/lib/workflow-cli.js +5 -1
- package/cli/migrate-customizations.js +254 -0
- package/cli/nfr-runner.js +1 -1
- package/cli/nfr.js +2 -2
- package/cli/teach.js +433 -0
- package/cli/test-design.js +2 -2
- package/cli/test-review.js +3 -3
- package/cli/trace.js +2 -2
- package/cli/transcript-runner.js +1 -1
- package/package.json +8 -4
- package/skills/bmad-tea/SKILL.md +4 -2
- package/skills/bmad-tea/customize.toml +10 -10
- package/skills/{bmad-testarch-automate → bmad-tea-automate}/SKILL.md +3 -1
- package/skills/{bmad-testarch-automate → bmad-tea-automate}/checklist.md +1 -1
- package/skills/{bmad-testarch-nfr → bmad-tea-automate}/customize.toml +1 -1
- package/skills/{bmad-testarch-automate → bmad-tea-automate}/red/customize.toml +1 -1
- package/skills/{bmad-testarch-automate → bmad-tea-automate}/red/instructions.md +1 -1
- package/skills/{bmad-testarch-automate → bmad-tea-automate}/red/workflow.yaml +2 -2
- package/skills/{bmad-testarch-automate → bmad-tea-automate}/resources/test-generation-routing.md +14 -14
- package/skills/{bmad-testarch-automate → bmad-tea-automate}/workflow.yaml +2 -2
- package/skills/{bmad-testarch-evaluate → bmad-tea-evaluate}/SKILL.md +4 -2
- package/skills/{bmad-testarch-evaluate → bmad-tea-evaluate}/assets/inspection-record.md +1 -1
- package/skills/{bmad-testarch-evaluate → bmad-tea-evaluate}/customize.toml +1 -1
- package/skills/{bmad-testarch-evaluate → bmad-tea-evaluate}/references/ci.md +4 -4
- package/skills/{bmad-testarch-framework → bmad-tea-framework}/SKILL.md +5 -3
- package/skills/bmad-tea-framework/ci/customize.toml +10 -0
- package/skills/{bmad-testarch-framework → bmad-tea-framework}/ci/instructions.md +1 -1
- package/skills/{bmad-testarch-framework → bmad-tea-framework}/customize.toml +2 -2
- package/skills/{bmad-testarch-framework → bmad-tea-framework}/resources/framework-setup-progress.example.md +1 -1
- package/skills/{bmad-testarch-framework → bmad-tea-framework}/resources/hooks/tea-enforce.cjs +2 -2
- package/skills/{bmad-testarch-framework → bmad-tea-framework}/resources/setup-phase-completion.md +1 -1
- package/skills/{bmad-testarch-framework → bmad-tea-framework}/resources/setup-routing.md +3 -3
- package/skills/{bmad-testarch-framework → bmad-tea-framework}/resources/setup-run-progress.example.md +1 -1
- package/skills/{bmad-testarch-framework → bmad-tea-framework}/steps-c/step-04-docs-and-scripts.md +2 -2
- package/skills/{bmad-testarch-framework → bmad-tea-framework}/workflow.yaml +2 -2
- package/skills/{bmad-testarch-nfr → bmad-tea-nfr}/SKILL.md +3 -1
- package/skills/{bmad-testarch-nfr → bmad-tea-nfr}/checklist.md +3 -3
- package/skills/{bmad-testarch-trace → bmad-tea-nfr}/customize.toml +1 -1
- package/skills/{bmad-testarch-nfr → bmad-tea-nfr}/instructions.md +1 -1
- package/skills/{bmad-testarch-nfr → bmad-tea-nfr}/nfr-report-template.md +3 -3
- package/skills/{bmad-testarch-nfr → bmad-tea-nfr}/resources/nfr-assessment.example.md +4 -4
- package/skills/{bmad-testarch-nfr → bmad-tea-nfr}/workflow.yaml +2 -2
- package/skills/{bmad-teach-me-testing → bmad-tea-teach-me-testing}/SKILL.md +3 -1
- package/skills/{bmad-teach-me-testing → bmad-tea-teach-me-testing}/customize.toml +1 -1
- package/skills/{bmad-teach-me-testing → bmad-tea-teach-me-testing}/instructions.md +2 -2
- package/skills/{bmad-teach-me-testing → bmad-tea-teach-me-testing}/steps-c/step-01-init.md +1 -2
- package/skills/{bmad-teach-me-testing → bmad-tea-teach-me-testing}/steps-c/step-01b-continue.md +1 -1
- package/skills/{bmad-teach-me-testing → bmad-tea-teach-me-testing}/steps-c/step-04-session-01.md +2 -2
- package/skills/{bmad-teach-me-testing → bmad-tea-teach-me-testing}/steps-c/step-04-session-02.md +1 -1
- package/skills/{bmad-teach-me-testing → bmad-tea-teach-me-testing}/steps-c/step-04-session-03.md +2 -2
- package/skills/{bmad-teach-me-testing → bmad-tea-teach-me-testing}/steps-c/step-04-session-05.md +2 -2
- package/skills/{bmad-teach-me-testing → bmad-tea-teach-me-testing}/steps-e/step-e-02-apply-edits.md +2 -2
- package/skills/{bmad-testarch-test-design → bmad-tea-test-design}/SKILL.md +3 -1
- package/skills/{bmad-testarch-test-design → bmad-tea-test-design}/checklist.md +3 -3
- package/skills/{bmad-testarch-automate → bmad-tea-test-design}/customize.toml +1 -1
- package/skills/{bmad-testarch-test-design → bmad-tea-test-design}/resources/test-design-epic-3.example.md +5 -5
- package/skills/{bmad-testarch-test-design → bmad-tea-test-design}/test-design-qa-template.md +1 -1
- package/skills/{bmad-testarch-test-design → bmad-tea-test-design}/test-design-template.md +3 -3
- package/skills/{bmad-testarch-test-design → bmad-tea-test-design}/workflow.yaml +2 -2
- package/skills/{bmad-testarch-test-review → bmad-tea-test-review}/SKILL.md +3 -1
- package/skills/{bmad-testarch-test-review → bmad-tea-test-review}/customize.toml +1 -1
- package/skills/{bmad-testarch-test-review → bmad-tea-test-review}/instructions.md +1 -1
- package/skills/{bmad-testarch-test-review → bmad-tea-test-review}/workflow.yaml +2 -2
- package/skills/{bmad-testarch-trace → bmad-tea-trace}/SKILL.md +3 -1
- package/skills/{bmad-testarch-trace → bmad-tea-trace}/checklist.md +2 -2
- package/skills/bmad-tea-trace/customize.toml +38 -0
- package/skills/{bmad-testarch-trace → bmad-tea-trace}/instructions.md +1 -1
- package/skills/{bmad-testarch-trace → bmad-tea-trace}/resources/traceability-matrix.example.md +1 -1
- package/skills/{bmad-testarch-trace → bmad-tea-trace}/steps-c/step-04-analyze-gaps.md +3 -3
- package/skills/{bmad-testarch-trace → bmad-tea-trace}/trace-template.md +2 -2
- package/skills/{bmad-testarch-trace → bmad-tea-trace}/workflow.yaml +2 -2
- package/skills/bmod-tea/bmod.toml +11 -11
- package/skills/bmod-tea/help/help.md +10 -10
- package/skills/bmod-tea/help/integrations.md +2 -2
- package/skills/bmod-tea/knowledge/burn-in.md +1 -1
- package/skills/bmod-tea/knowledge/risk-governance.md +1 -1
- package/skills/bmod-tea/knowledge/test-healing-patterns.md +1 -1
- package/skills/bmod-tea/migration-1.toml +11 -11
- package/skills/bmad-testarch-test-design/customize.toml +0 -38
- /package/skills/{bmad-teach-me-testing → bmad-tea-automate}/bmod.toml +0 -0
- /package/skills/{bmad-testarch-automate → bmad-tea-automate}/instructions.md +0 -0
- /package/skills/{bmad-testarch-automate → bmad-tea-automate}/red/atdd-checklist-template.md +0 -0
- /package/skills/{bmad-testarch-automate → bmad-tea-automate}/red/checklist.md +0 -0
- /package/skills/{bmad-testarch-automate → bmad-tea-automate}/red/resources/atdd-checklist-2-4-guest-checkout.example.md +0 -0
- /package/skills/{bmad-testarch-automate → bmad-tea-automate}/red/steps-c/step-01-preflight-and-context.md +0 -0
- /package/skills/{bmad-testarch-automate → bmad-tea-automate}/red/steps-c/step-01b-resume.md +0 -0
- /package/skills/{bmad-testarch-automate → bmad-tea-automate}/red/steps-c/step-02-generation-mode.md +0 -0
- /package/skills/{bmad-testarch-automate → bmad-tea-automate}/red/steps-c/step-03-test-strategy.md +0 -0
- /package/skills/{bmad-testarch-automate → bmad-tea-automate}/red/steps-c/step-04-generate-tests.md +0 -0
- /package/skills/{bmad-testarch-automate → bmad-tea-automate}/red/steps-c/step-04a-subagent-api-failing.md +0 -0
- /package/skills/{bmad-testarch-automate → bmad-tea-automate}/red/steps-c/step-04b-subagent-e2e-failing.md +0 -0
- /package/skills/{bmad-testarch-automate → bmad-tea-automate}/red/steps-c/step-04c-aggregate.md +0 -0
- /package/skills/{bmad-testarch-automate → bmad-tea-automate}/red/steps-c/step-05-validate-and-complete.md +0 -0
- /package/skills/{bmad-testarch-automate → bmad-tea-automate}/red/steps-e/step-01-assess.md +0 -0
- /package/skills/{bmad-testarch-automate → bmad-tea-automate}/red/steps-e/step-02-apply-edit.md +0 -0
- /package/skills/{bmad-testarch-automate → bmad-tea-automate}/red/steps-v/step-01-validate.md +0 -0
- /package/skills/{bmad-testarch-automate → bmad-tea-automate}/resources/automation-summary.example.md +0 -0
- /package/skills/{bmad-testarch-automate → bmad-tea-automate}/resources/run-and-heal.md +0 -0
- /package/skills/{bmad-testarch-automate → bmad-tea-automate}/steps-c/step-01-preflight-and-context.md +0 -0
- /package/skills/{bmad-testarch-automate → bmad-tea-automate}/steps-c/step-01b-resume.md +0 -0
- /package/skills/{bmad-testarch-automate → bmad-tea-automate}/steps-c/step-02-identify-targets.md +0 -0
- /package/skills/{bmad-testarch-automate → bmad-tea-automate}/steps-c/step-03-generate-tests.md +0 -0
- /package/skills/{bmad-testarch-automate → bmad-tea-automate}/steps-c/step-03a-subagent-api.md +0 -0
- /package/skills/{bmad-testarch-automate → bmad-tea-automate}/steps-c/step-03b-subagent-backend.md +0 -0
- /package/skills/{bmad-testarch-automate → bmad-tea-automate}/steps-c/step-03b-subagent-e2e.md +0 -0
- /package/skills/{bmad-testarch-automate → bmad-tea-automate}/steps-c/step-03b-subagent-mobile.md +0 -0
- /package/skills/{bmad-testarch-automate → bmad-tea-automate}/steps-c/step-03c-aggregate.md +0 -0
- /package/skills/{bmad-testarch-automate → bmad-tea-automate}/steps-c/step-04-validate-and-summarize.md +0 -0
- /package/skills/{bmad-testarch-automate → bmad-tea-automate}/steps-e/step-01-assess.md +0 -0
- /package/skills/{bmad-testarch-automate → bmad-tea-automate}/steps-e/step-02-apply-edit.md +0 -0
- /package/skills/{bmad-testarch-automate → bmad-tea-automate}/steps-v/step-01-validate.md +0 -0
- /package/skills/{bmad-testarch-evaluate → bmad-tea-evaluate}/assets/README.md +0 -0
- /package/skills/{bmad-testarch-evaluate → bmad-tea-evaluate}/assets/contract.skeleton.json +0 -0
- /package/skills/{bmad-testarch-evaluate → bmad-tea-evaluate}/assets/evaluation-ci-plan.template.json +0 -0
- /package/skills/{bmad-testarch-evaluate → bmad-tea-evaluate}/assets/evaluation-folder.gitignore +0 -0
- /package/skills/{bmad-testarch-evaluate → bmad-tea-evaluate}/assets/evaluation.json +0 -0
- /package/skills/{bmad-testarch-evaluate → bmad-tea-evaluate}/assets/evaluator-conditions.template.json +0 -0
- /package/skills/{bmad-testarch-evaluate → bmad-tea-evaluate}/assets/evaluators/LEARNED.md +0 -0
- /package/skills/{bmad-testarch-evaluate → bmad-tea-evaluate}/assets/evaluators/agentevals-frameworks.json +0 -0
- /package/skills/{bmad-testarch-evaluate → bmad-tea-evaluate}/assets/evaluators/agentevals-trajectory.mjs +0 -0
- /package/skills/{bmad-testarch-evaluate → bmad-tea-evaluate}/assets/evaluators/command-evaluator.mjs +0 -0
- /package/skills/{bmad-testarch-evaluate → bmad-tea-evaluate}/assets/evaluators/frameworks.json +0 -0
- /package/skills/{bmad-testarch-evaluate → bmad-tea-evaluate}/assets/evaluators/installed-version.mjs +0 -0
- /package/skills/{bmad-testarch-evaluate → bmad-tea-evaluate}/assets/evaluators/mapping.json +0 -0
- /package/skills/{bmad-testarch-evaluate → bmad-tea-evaluate}/assets/evaluators/promptfoo-assertions.mjs +0 -0
- /package/skills/{bmad-testarch-evaluate → bmad-tea-evaluate}/assets/evaluators/promptfoo-frameworks.json +0 -0
- /package/skills/{bmad-testarch-evaluate → bmad-tea-evaluate}/assets/http-probe-port.conformance.mjs +0 -0
- /package/skills/{bmad-testarch-evaluate → bmad-tea-evaluate}/assets/http-probe-port.mjs +0 -0
- /package/skills/{bmad-testarch-evaluate → bmad-tea-evaluate}/assets/requirements-statement.md +0 -0
- /package/skills/{bmad-testarch-evaluate → bmad-tea-evaluate}/assets/scoring-policy.template.json +0 -0
- /package/skills/{bmad-testarch-automate → bmad-tea-evaluate}/bmod.toml +0 -0
- /package/skills/{bmad-testarch-evaluate → bmad-tea-evaluate}/references/adapters.md +0 -0
- /package/skills/{bmad-testarch-evaluate → bmad-tea-evaluate}/references/authoring-routing.md +0 -0
- /package/skills/{bmad-testarch-evaluate → bmad-tea-evaluate}/references/contract.md +0 -0
- /package/skills/{bmad-testarch-evaluate → bmad-tea-evaluate}/references/corpus-agent.md +0 -0
- /package/skills/{bmad-testarch-evaluate → bmad-tea-evaluate}/references/corpus-ai-feature.md +0 -0
- /package/skills/{bmad-testarch-evaluate → bmad-tea-evaluate}/references/corpus-skill.md +0 -0
- /package/skills/{bmad-testarch-evaluate → bmad-tea-evaluate}/references/corpus-test-review-mechanism.md +0 -0
- /package/skills/{bmad-testarch-evaluate → bmad-tea-evaluate}/references/corpus-tool-use-system.md +0 -0
- /package/skills/{bmad-testarch-evaluate → bmad-tea-evaluate}/references/corpus-workflow.md +0 -0
- /package/skills/{bmad-testarch-evaluate → bmad-tea-evaluate}/references/corpus.md +0 -0
- /package/skills/{bmad-testarch-evaluate → bmad-tea-evaluate}/references/evaluator.md +0 -0
- /package/skills/{bmad-testarch-evaluate → bmad-tea-evaluate}/references/gaps.md +0 -0
- /package/skills/{bmad-testarch-evaluate → bmad-tea-evaluate}/references/harness.md +0 -0
- /package/skills/{bmad-testarch-evaluate → bmad-tea-evaluate}/references/inspection.md +0 -0
- /package/skills/{bmad-testarch-evaluate → bmad-tea-evaluate}/references/intake.md +0 -0
- /package/skills/{bmad-testarch-evaluate → bmad-tea-evaluate}/references/mutation.md +0 -0
- /package/skills/{bmad-testarch-evaluate → bmad-tea-evaluate}/references/oracles.md +0 -0
- /package/skills/{bmad-testarch-evaluate → bmad-tea-evaluate}/references/run.md +0 -0
- /package/skills/{bmad-testarch-evaluate → bmad-tea-framework}/bmod.toml +0 -0
- /package/skills/{bmad-testarch-framework → bmad-tea-framework}/checklist.md +0 -0
- /package/skills/{bmad-testarch-framework → bmad-tea-framework}/ci/azure-pipelines-template.yaml +0 -0
- /package/skills/{bmad-testarch-framework → bmad-tea-framework}/ci/checklist.md +0 -0
- /package/skills/{bmad-testarch-framework → bmad-tea-framework}/ci/github-actions-template.yaml +0 -0
- /package/skills/{bmad-testarch-framework → bmad-tea-framework}/ci/gitlab-ci-template.yaml +0 -0
- /package/skills/{bmad-testarch-framework → bmad-tea-framework}/ci/harness-pipeline-template.yaml +0 -0
- /package/skills/{bmad-testarch-framework → bmad-tea-framework}/ci/jenkins-pipeline-template.groovy +0 -0
- /package/skills/{bmad-testarch-framework → bmad-tea-framework}/ci/resources/ci-pipeline-progress.example.md +0 -0
- /package/skills/{bmad-testarch-framework → bmad-tea-framework}/ci/steps-c/step-01-preflight.md +0 -0
- /package/skills/{bmad-testarch-framework → bmad-tea-framework}/ci/steps-c/step-01b-resume.md +0 -0
- /package/skills/{bmad-testarch-framework → bmad-tea-framework}/ci/steps-c/step-02-generate-pipeline.md +0 -0
- /package/skills/{bmad-testarch-framework → bmad-tea-framework}/ci/steps-c/step-03-configure-quality-gates.md +0 -0
- /package/skills/{bmad-testarch-framework → bmad-tea-framework}/ci/steps-c/step-03b-render-evaluation-plans.md +0 -0
- /package/skills/{bmad-testarch-framework → bmad-tea-framework}/ci/steps-c/step-04-validate-and-summary.md +0 -0
- /package/skills/{bmad-testarch-framework → bmad-tea-framework}/ci/steps-e/step-01-assess.md +0 -0
- /package/skills/{bmad-testarch-framework → bmad-tea-framework}/ci/steps-e/step-02-apply-edit.md +0 -0
- /package/skills/{bmad-testarch-framework → bmad-tea-framework}/ci/steps-v/step-01-validate.md +0 -0
- /package/skills/{bmad-testarch-framework → bmad-tea-framework}/instructions.md +0 -0
- /package/skills/{bmad-testarch-framework → bmad-tea-framework}/resources/setup-parallel.md +0 -0
- /package/skills/{bmad-testarch-framework → bmad-tea-framework}/resources/setup-state.md +0 -0
- /package/skills/{bmad-testarch-framework → bmad-tea-framework}/steps-c/step-01-preflight.md +0 -0
- /package/skills/{bmad-testarch-framework → bmad-tea-framework}/steps-c/step-01b-resume.md +0 -0
- /package/skills/{bmad-testarch-framework → bmad-tea-framework}/steps-c/step-02-select-framework.md +0 -0
- /package/skills/{bmad-testarch-framework → bmad-tea-framework}/steps-c/step-03-scaffold-framework.md +0 -0
- /package/skills/{bmad-testarch-framework → bmad-tea-framework}/steps-c/step-05-validate-and-summary.md +0 -0
- /package/skills/{bmad-testarch-framework → bmad-tea-framework}/steps-e/step-01-assess.md +0 -0
- /package/skills/{bmad-testarch-framework → bmad-tea-framework}/steps-e/step-02-apply-edit.md +0 -0
- /package/skills/{bmad-testarch-framework → bmad-tea-framework}/steps-v/step-01-validate.md +0 -0
- /package/skills/{bmad-testarch-framework → bmad-tea-nfr}/bmod.toml +0 -0
- /package/skills/{bmad-testarch-nfr → bmad-tea-nfr}/steps-c/nfr-status-definitions.md +0 -0
- /package/skills/{bmad-testarch-nfr → bmad-tea-nfr}/steps-c/step-01-load-context.md +0 -0
- /package/skills/{bmad-testarch-nfr → bmad-tea-nfr}/steps-c/step-01b-resume.md +0 -0
- /package/skills/{bmad-testarch-nfr → bmad-tea-nfr}/steps-c/step-02-define-thresholds.md +0 -0
- /package/skills/{bmad-testarch-nfr → bmad-tea-nfr}/steps-c/step-03-gather-evidence.md +0 -0
- /package/skills/{bmad-testarch-nfr → bmad-tea-nfr}/steps-c/step-04-evaluate-and-score.md +0 -0
- /package/skills/{bmad-testarch-nfr → bmad-tea-nfr}/steps-c/step-04a-subagent-security.md +0 -0
- /package/skills/{bmad-testarch-nfr → bmad-tea-nfr}/steps-c/step-04b-subagent-performance.md +0 -0
- /package/skills/{bmad-testarch-nfr → bmad-tea-nfr}/steps-c/step-04c-subagent-reliability.md +0 -0
- /package/skills/{bmad-testarch-nfr → bmad-tea-nfr}/steps-c/step-04d-subagent-maintainability.md +0 -0
- /package/skills/{bmad-testarch-nfr → bmad-tea-nfr}/steps-c/step-04e-aggregate-nfr.md +0 -0
- /package/skills/{bmad-testarch-nfr → bmad-tea-nfr}/steps-c/step-05-generate-report.md +0 -0
- /package/skills/{bmad-testarch-nfr → bmad-tea-nfr}/steps-e/step-01-assess.md +0 -0
- /package/skills/{bmad-testarch-nfr → bmad-tea-nfr}/steps-e/step-02-apply-edit.md +0 -0
- /package/skills/{bmad-testarch-nfr → bmad-tea-nfr}/steps-v/step-01-validate.md +0 -0
- /package/skills/{bmad-testarch-nfr → bmad-tea-teach-me-testing}/bmod.toml +0 -0
- /package/skills/{bmad-teach-me-testing → bmad-tea-teach-me-testing}/checklist.md +0 -0
- /package/skills/{bmad-teach-me-testing → bmad-tea-teach-me-testing}/data/curriculum.yaml +0 -0
- /package/skills/{bmad-teach-me-testing → bmad-tea-teach-me-testing}/data/quiz-questions.yaml +0 -0
- /package/skills/{bmad-teach-me-testing → bmad-tea-teach-me-testing}/data/role-paths.yaml +0 -0
- /package/skills/{bmad-teach-me-testing → bmad-tea-teach-me-testing}/data/session-content-map.yaml +0 -0
- /package/skills/{bmad-teach-me-testing → bmad-tea-teach-me-testing}/data/tea-resources-index.yaml +0 -0
- /package/skills/{bmad-teach-me-testing → bmad-tea-teach-me-testing}/resources/tea-completion-summary.example.md +0 -0
- /package/skills/{bmad-teach-me-testing → bmad-tea-teach-me-testing}/steps-c/step-02-assess.md +0 -0
- /package/skills/{bmad-teach-me-testing → bmad-tea-teach-me-testing}/steps-c/step-03-session-menu.md +0 -0
- /package/skills/{bmad-teach-me-testing → bmad-tea-teach-me-testing}/steps-c/step-04-session-04.md +0 -0
- /package/skills/{bmad-teach-me-testing → bmad-tea-teach-me-testing}/steps-c/step-04-session-06.md +0 -0
- /package/skills/{bmad-teach-me-testing → bmad-tea-teach-me-testing}/steps-c/step-04-session-07.md +0 -0
- /package/skills/{bmad-teach-me-testing → bmad-tea-teach-me-testing}/steps-c/step-05-completion.md +0 -0
- /package/skills/{bmad-teach-me-testing → bmad-tea-teach-me-testing}/steps-e/step-e-01-assess-workflow.md +0 -0
- /package/skills/{bmad-teach-me-testing → bmad-tea-teach-me-testing}/steps-v/step-v-01-validate.md +0 -0
- /package/skills/{bmad-teach-me-testing → bmad-tea-teach-me-testing}/templates/completion-summary-template.md +0 -0
- /package/skills/{bmad-teach-me-testing → bmad-tea-teach-me-testing}/templates/progress-template.yaml +0 -0
- /package/skills/{bmad-teach-me-testing → bmad-tea-teach-me-testing}/templates/session-notes-template.md +0 -0
- /package/skills/{bmad-testarch-test-design → bmad-tea-test-design}/bmod.toml +0 -0
- /package/skills/{bmad-testarch-test-design → bmad-tea-test-design}/instructions.md +0 -0
- /package/skills/{bmad-testarch-test-design → bmad-tea-test-design}/steps-c/step-01-detect-mode.md +0 -0
- /package/skills/{bmad-testarch-test-design → bmad-tea-test-design}/steps-c/step-01b-resume.md +0 -0
- /package/skills/{bmad-testarch-test-design → bmad-tea-test-design}/steps-c/step-02-load-context.md +0 -0
- /package/skills/{bmad-testarch-test-design → bmad-tea-test-design}/steps-c/step-03-risk-and-testability.md +0 -0
- /package/skills/{bmad-testarch-test-design → bmad-tea-test-design}/steps-c/step-04-coverage-plan.md +0 -0
- /package/skills/{bmad-testarch-test-design → bmad-tea-test-design}/steps-c/step-05-generate-output.md +0 -0
- /package/skills/{bmad-testarch-test-design → bmad-tea-test-design}/steps-e/step-01-assess.md +0 -0
- /package/skills/{bmad-testarch-test-design → bmad-tea-test-design}/steps-e/step-02-apply-edit.md +0 -0
- /package/skills/{bmad-testarch-test-design → bmad-tea-test-design}/steps-v/step-01-validate.md +0 -0
- /package/skills/{bmad-testarch-test-design → bmad-tea-test-design}/test-design-architecture-template.md +0 -0
- /package/skills/{bmad-testarch-test-design → bmad-tea-test-design}/test-design-handoff-template.md +0 -0
- /package/skills/{bmad-testarch-test-review → bmad-tea-test-review}/bmod.toml +0 -0
- /package/skills/{bmad-testarch-test-review → bmad-tea-test-review}/checklist.md +0 -0
- /package/skills/{bmad-testarch-test-review → bmad-tea-test-review}/resources/test-review.example.md +0 -0
- /package/skills/{bmad-testarch-test-review → bmad-tea-test-review}/steps-c/criteria-registry.md +0 -0
- /package/skills/{bmad-testarch-test-review → bmad-tea-test-review}/steps-c/step-01-load-context.md +0 -0
- /package/skills/{bmad-testarch-test-review → bmad-tea-test-review}/steps-c/step-01b-resume.md +0 -0
- /package/skills/{bmad-testarch-test-review → bmad-tea-test-review}/steps-c/step-02-discover-tests.md +0 -0
- /package/skills/{bmad-testarch-test-review → bmad-tea-test-review}/steps-c/step-03-quality-evaluation.md +0 -0
- /package/skills/{bmad-testarch-test-review → bmad-tea-test-review}/steps-c/step-03a-subagent-determinism.md +0 -0
- /package/skills/{bmad-testarch-test-review → bmad-tea-test-review}/steps-c/step-03b-subagent-isolation.md +0 -0
- /package/skills/{bmad-testarch-test-review → bmad-tea-test-review}/steps-c/step-03c-subagent-maintainability.md +0 -0
- /package/skills/{bmad-testarch-test-review → bmad-tea-test-review}/steps-c/step-03e-subagent-performance.md +0 -0
- /package/skills/{bmad-testarch-test-review → bmad-tea-test-review}/steps-c/step-03f-aggregate-scores.md +0 -0
- /package/skills/{bmad-testarch-test-review → bmad-tea-test-review}/steps-c/step-04-generate-report.md +0 -0
- /package/skills/{bmad-testarch-test-review → bmad-tea-test-review}/steps-e/step-01-assess.md +0 -0
- /package/skills/{bmad-testarch-test-review → bmad-tea-test-review}/steps-e/step-02-apply-edit.md +0 -0
- /package/skills/{bmad-testarch-test-review → bmad-tea-test-review}/steps-v/step-01-validate.md +0 -0
- /package/skills/{bmad-testarch-test-review → bmad-tea-test-review}/test-review-template.md +0 -0
- /package/skills/{bmad-testarch-trace → bmad-tea-trace}/bmod.toml +0 -0
- /package/skills/{bmad-testarch-trace → bmad-tea-trace}/resources/live-verification-results.example.json +0 -0
- /package/skills/{bmad-testarch-trace → bmad-tea-trace}/steps-c/step-01-load-context.md +0 -0
- /package/skills/{bmad-testarch-trace → bmad-tea-trace}/steps-c/step-01b-resume.md +0 -0
- /package/skills/{bmad-testarch-trace → bmad-tea-trace}/steps-c/step-02-discover-tests.md +0 -0
- /package/skills/{bmad-testarch-trace → bmad-tea-trace}/steps-c/step-03-map-criteria.md +0 -0
- /package/skills/{bmad-testarch-trace → bmad-tea-trace}/steps-c/step-05-gate-decision.md +0 -0
- /package/skills/{bmad-testarch-trace → bmad-tea-trace}/steps-e/step-01-assess.md +0 -0
- /package/skills/{bmad-testarch-trace → bmad-tea-trace}/steps-e/step-02-apply-edit.md +0 -0
- /package/skills/{bmad-testarch-trace → bmad-tea-trace}/steps-v/step-01-validate.md +0 -0
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
/** Durable learner state and evidence checks for one teaching turn. */
|
|
2
|
+
'use strict';
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const crypto = require('node:crypto');
|
|
6
|
+
const yaml = require('js-yaml');
|
|
7
|
+
function isDeepStrictEqual(a, b) {
|
|
8
|
+
if (Object.is(a, b)) return true;
|
|
9
|
+
if (typeof a !== 'object' || a === null || typeof b !== 'object' || b === null) return false;
|
|
10
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
11
|
+
const keysA = Object.keys(a);
|
|
12
|
+
const keysB = Object.keys(b);
|
|
13
|
+
if (keysA.length !== keysB.length) return false;
|
|
14
|
+
for (const key of keysA) {
|
|
15
|
+
if (!Object.prototype.hasOwnProperty.call(b, key) || !isDeepStrictEqual(a[key], b[key])) return false;
|
|
16
|
+
}
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
const { projectPath, WorkflowError } = require('./workflow-cli');
|
|
20
|
+
const SESSION_IDS = [
|
|
21
|
+
'session-01-quickstart',
|
|
22
|
+
'session-02-concepts',
|
|
23
|
+
'session-03-architecture',
|
|
24
|
+
'session-04-test-design',
|
|
25
|
+
'session-05-atdd-automate',
|
|
26
|
+
'session-06-quality-trace',
|
|
27
|
+
'session-07-advanced',
|
|
28
|
+
];
|
|
29
|
+
const FACTS = ['role', 'experience_level', 'learning_goals', 'pain_points'];
|
|
30
|
+
const hash = (value) => crypto.createHash('sha256').update(value).digest('hex');
|
|
31
|
+
const fail = (message) => {
|
|
32
|
+
throw new WorkflowError('environment-parser', message);
|
|
33
|
+
};
|
|
34
|
+
const table = (value) => value !== null && typeof value === 'object' && !Array.isArray(value) && !(value instanceof Date);
|
|
35
|
+
function parseProgress(text, { legacy = false } = {}) {
|
|
36
|
+
const documents = [];
|
|
37
|
+
yaml.loadAll(text, (value) => documents.push(value), { schema: yaml.JSON_SCHEMA });
|
|
38
|
+
if (documents.length !== 1 && !(legacy && documents.length === 2 && documents[1] === null))
|
|
39
|
+
fail('progress must contain one YAML document');
|
|
40
|
+
if (!table(documents[0])) fail('progress must be a YAML table');
|
|
41
|
+
return { progress: documents[0], legacyNormalized: documents.length === 2 };
|
|
42
|
+
}
|
|
43
|
+
function checkProgress(progress, learner) {
|
|
44
|
+
if (progress.user !== learner) fail('progress belongs to another learner');
|
|
45
|
+
if (
|
|
46
|
+
!Array.isArray(progress.sessions) ||
|
|
47
|
+
progress.sessions.length !== SESSION_IDS.length ||
|
|
48
|
+
progress.sessions.some((session, index) => !table(session) || session.id !== SESSION_IDS[index])
|
|
49
|
+
)
|
|
50
|
+
fail('progress must retain all seven canonical sessions in order');
|
|
51
|
+
for (const session of progress.sessions) {
|
|
52
|
+
if (!['not-started', 'in-progress', 'completed'].includes(session.status)) fail(`invalid status for ${session.id}`);
|
|
53
|
+
if (
|
|
54
|
+
session.score !== null &&
|
|
55
|
+
(typeof session.score !== 'number' || !Number.isFinite(session.score) || session.score < 0 || session.score > 100)
|
|
56
|
+
)
|
|
57
|
+
fail(`invalid score for ${session.id}`);
|
|
58
|
+
if (
|
|
59
|
+
session.status === 'completed' &&
|
|
60
|
+
(session.score === null ||
|
|
61
|
+
typeof session.completed_date !== 'string' ||
|
|
62
|
+
!session.completed_date.trim() ||
|
|
63
|
+
typeof session.notes_artifact !== 'string' ||
|
|
64
|
+
!session.notes_artifact.trim())
|
|
65
|
+
)
|
|
66
|
+
fail(`completed session lacks score, date or notes: ${session.id}`);
|
|
67
|
+
}
|
|
68
|
+
const completed = progress.sessions.filter((session) => session.status === 'completed').length;
|
|
69
|
+
if (
|
|
70
|
+
progress.sessions_completed !== completed ||
|
|
71
|
+
progress.total_sessions !== 7 ||
|
|
72
|
+
typeof progress.completion_percentage !== 'number' ||
|
|
73
|
+
Math.abs(progress.completion_percentage - (completed / 7) * 100) > 0.51
|
|
74
|
+
)
|
|
75
|
+
fail('progress completion counters contradict its sessions');
|
|
76
|
+
if (
|
|
77
|
+
!(
|
|
78
|
+
SESSION_IDS.includes(progress.next_recommended) ||
|
|
79
|
+
(completed === 7 && [null, 'completion', 'completed', 'none'].includes(progress.next_recommended))
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
fail('progress has an invalid next recommendation');
|
|
83
|
+
if (completed < 7 && progress.sessions.find((session) => session.id === progress.next_recommended)?.status === 'completed')
|
|
84
|
+
fail('next recommendation names an already completed session');
|
|
85
|
+
if (progress.role !== null && (typeof progress.role !== 'string' || !['qa', 'dev', 'lead', 'vp'].includes(progress.role.toLowerCase())))
|
|
86
|
+
fail('progress has an invalid role');
|
|
87
|
+
if (
|
|
88
|
+
progress.experience_level !== null &&
|
|
89
|
+
(typeof progress.experience_level !== 'string' ||
|
|
90
|
+
!['beginner', 'intermediate', 'experienced'].includes(progress.experience_level.toLowerCase()))
|
|
91
|
+
)
|
|
92
|
+
fail('progress has an invalid experience level');
|
|
93
|
+
for (const field of ['learning_goals', 'pain_points'])
|
|
94
|
+
if (progress[field] !== null && (typeof progress[field] !== 'string' || !progress[field].trim())) fail(`progress has invalid ${field}`);
|
|
95
|
+
if (
|
|
96
|
+
!Array.isArray(progress.stepsCompleted) ||
|
|
97
|
+
progress.stepsCompleted.some((step) => typeof step !== 'string') ||
|
|
98
|
+
typeof progress.lastStep !== 'string'
|
|
99
|
+
)
|
|
100
|
+
fail('progress has invalid continuation tracking');
|
|
101
|
+
if (typeof progress.summary_generated !== 'boolean') fail('progress has an invalid summary flag');
|
|
102
|
+
if (
|
|
103
|
+
progress.summary_generated &&
|
|
104
|
+
(completed !== 7 ||
|
|
105
|
+
typeof progress.summary_path !== 'string' ||
|
|
106
|
+
!progress.summary_path.trim() ||
|
|
107
|
+
typeof progress.completion_date !== 'string' ||
|
|
108
|
+
!progress.completion_date.trim())
|
|
109
|
+
)
|
|
110
|
+
fail('completed summary lacks its path, completion date or all seven sessions');
|
|
111
|
+
}
|
|
112
|
+
/** Every read is a fresh, singly linked regular file inside the attempt. */
|
|
113
|
+
function regularArtifact(root, value) {
|
|
114
|
+
const lexical = path.resolve(root, value);
|
|
115
|
+
let resolved;
|
|
116
|
+
try {
|
|
117
|
+
resolved = projectPath(root, value, 'generated artifact');
|
|
118
|
+
} catch (error) {
|
|
119
|
+
fail(`generated artifact is outside the attempt: ${error.message}`);
|
|
120
|
+
}
|
|
121
|
+
if (resolved !== lexical) fail('generated artifact traverses a symbolic link');
|
|
122
|
+
let stat;
|
|
123
|
+
try {
|
|
124
|
+
stat = fs.lstatSync(resolved);
|
|
125
|
+
} catch {
|
|
126
|
+
fail(`generated artifact is missing: ${value}`);
|
|
127
|
+
}
|
|
128
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1) fail(`generated artifact must be a separate regular file: ${value}`);
|
|
129
|
+
return { path: resolved, bytes: fs.readFileSync(resolved) };
|
|
130
|
+
}
|
|
131
|
+
/** Snapshot the entire CLI-owned directory. Symbolic and inode aliases are rejected. */
|
|
132
|
+
function snapshot(root) {
|
|
133
|
+
const files = new Map();
|
|
134
|
+
if (!fs.existsSync(root)) return files;
|
|
135
|
+
const walk = (directory) => {
|
|
136
|
+
if (!fs.lstatSync(directory).isDirectory() || fs.lstatSync(directory).isSymbolicLink())
|
|
137
|
+
throw new WorkflowError('usage', 'learner state must be a regular directory');
|
|
138
|
+
for (const entry of fs.readdirSync(directory)) {
|
|
139
|
+
const file = path.join(directory, entry),
|
|
140
|
+
stat = fs.lstatSync(file);
|
|
141
|
+
if (stat.isDirectory() && !stat.isSymbolicLink()) walk(file);
|
|
142
|
+
else if (stat.isFile() && stat.nlink === 1) files.set(path.relative(root, file), fs.readFileSync(file));
|
|
143
|
+
else throw new WorkflowError('usage', 'learner state contains a symbolic link or inode alias');
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
walk(root);
|
|
147
|
+
return files;
|
|
148
|
+
}
|
|
149
|
+
function sameSnapshot(left, right) {
|
|
150
|
+
return left.size === right.size && [...left].every(([file, bytes]) => right.get(file)?.equals(bytes));
|
|
151
|
+
}
|
|
152
|
+
/** Install the directory as a unit; retain recoverable backups if rollback fails. */
|
|
153
|
+
function publishState(root, files, io = fs) {
|
|
154
|
+
const parent = path.dirname(root);
|
|
155
|
+
io.mkdirSync(parent, { recursive: true });
|
|
156
|
+
const stage = io.mkdtempSync(path.join(parent, `.tea-teach-publish-${hash(root).slice(0, 16)}-`));
|
|
157
|
+
const next = path.join(stage, 'next'),
|
|
158
|
+
previous = path.join(stage, 'previous');
|
|
159
|
+
io.mkdirSync(next);
|
|
160
|
+
let moved = false,
|
|
161
|
+
installed = false,
|
|
162
|
+
keep = false;
|
|
163
|
+
try {
|
|
164
|
+
for (const [file, bytes] of files) {
|
|
165
|
+
const destination = path.join(next, file);
|
|
166
|
+
io.mkdirSync(path.dirname(destination), { recursive: true });
|
|
167
|
+
io.writeFileSync(destination, bytes, { flag: 'wx' });
|
|
168
|
+
}
|
|
169
|
+
if (io.existsSync(root)) {
|
|
170
|
+
io.renameSync(root, previous);
|
|
171
|
+
moved = true;
|
|
172
|
+
}
|
|
173
|
+
io.renameSync(next, root);
|
|
174
|
+
installed = true;
|
|
175
|
+
} catch (error) {
|
|
176
|
+
if (moved && !installed) {
|
|
177
|
+
try {
|
|
178
|
+
io.renameSync(previous, root);
|
|
179
|
+
} catch (error_) {
|
|
180
|
+
keep = true;
|
|
181
|
+
throw new WorkflowError(
|
|
182
|
+
'environment-configuration',
|
|
183
|
+
`state publication failed: ${error.message}; recovery backup: ${previous}; rollback: ${error_.message}`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
throw new WorkflowError('environment-configuration', `state publication failed: ${error.message}; previous state preserved`);
|
|
188
|
+
} finally {
|
|
189
|
+
if (!keep) io.rmSync(stage, { recursive: true, force: true });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/** Restore a prior learner directory if publication stopped between its two renames. */
|
|
193
|
+
function recoverPublishedState(root) {
|
|
194
|
+
if (fs.existsSync(root)) return;
|
|
195
|
+
const parent = path.dirname(root);
|
|
196
|
+
const prefix = `.tea-teach-publish-${hash(root).slice(0, 16)}-`;
|
|
197
|
+
const stages = fs
|
|
198
|
+
.readdirSync(parent)
|
|
199
|
+
.filter((name) => name.startsWith(prefix))
|
|
200
|
+
.map((name) => path.join(parent, name));
|
|
201
|
+
for (const stage of stages) {
|
|
202
|
+
const stat = fs.lstatSync(stage);
|
|
203
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
204
|
+
throw new WorkflowError('usage', `interrupted learner publication has an invalid stage: ${stage}`);
|
|
205
|
+
}
|
|
206
|
+
if (stages.length === 0) return;
|
|
207
|
+
if (stages.length !== 1)
|
|
208
|
+
throw new WorkflowError('usage', `multiple interrupted learner publications need manual recovery: ${stages.join(', ')}`);
|
|
209
|
+
const previous = path.join(stages[0], 'previous');
|
|
210
|
+
if (!fs.existsSync(previous) || !fs.lstatSync(previous).isDirectory() || fs.lstatSync(previous).isSymbolicLink())
|
|
211
|
+
throw new WorkflowError('usage', `interrupted learner publication has no retained state: ${stages[0]}`);
|
|
212
|
+
fs.renameSync(previous, root);
|
|
213
|
+
fs.rmSync(stages[0], { recursive: true, force: true });
|
|
214
|
+
}
|
|
215
|
+
function checkConversation(value, learner, bank) {
|
|
216
|
+
if (!table(value) || value.schema_version !== '0.1.0' || value.learner !== learner || !Array.isArray(value.turns) || !table(value.quiz))
|
|
217
|
+
fail('conversation has invalid learner identity or schema');
|
|
218
|
+
if (value.activeQuiz !== undefined && !table(value.activeQuiz)) fail('conversation contains an invalid active quiz ledger');
|
|
219
|
+
const ids = new Set();
|
|
220
|
+
for (const turn of value.turns) {
|
|
221
|
+
if (
|
|
222
|
+
!table(turn) ||
|
|
223
|
+
typeof turn.id !== 'string' ||
|
|
224
|
+
ids.has(turn.id) ||
|
|
225
|
+
typeof turn.learnerMessage !== 'string' ||
|
|
226
|
+
!turn.learnerMessage.trim() ||
|
|
227
|
+
typeof turn.tutorReply !== 'string' ||
|
|
228
|
+
!turn.tutorReply.trim() ||
|
|
229
|
+
!table(turn.waiting)
|
|
230
|
+
)
|
|
231
|
+
fail('conversation contains an invalid or repeated turn');
|
|
232
|
+
ids.add(turn.id);
|
|
233
|
+
}
|
|
234
|
+
for (const [sessionId, round] of Object.entries(value.quiz)) checkQuizRound(value, bank, sessionId, round);
|
|
235
|
+
for (const [sessionId, round] of Object.entries(value.activeQuiz ?? {})) {
|
|
236
|
+
if (value.quiz[sessionId] && value.activeQuiz?.[sessionId] && value.quiz[sessionId].id === value.activeQuiz[sessionId].id)
|
|
237
|
+
fail('recorded and active quiz rounds must differ');
|
|
238
|
+
checkQuizRound(value, bank, sessionId, round);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/** Bind one recorded quiz round to its exact preceding caller questions and answers. */
|
|
242
|
+
function checkQuizRound(value, bank, sessionId, round) {
|
|
243
|
+
if (!SESSION_IDS.includes(sessionId) || !table(round) || typeof round.id !== 'string' || !Array.isArray(round.answers))
|
|
244
|
+
fail('conversation contains an invalid quiz ledger');
|
|
245
|
+
for (const [index, answer] of round.answers.entries()) {
|
|
246
|
+
const turnIndex = value.turns.findIndex((item) => item.id === answer.messageId);
|
|
247
|
+
const turn = value.turns[turnIndex];
|
|
248
|
+
const pending = value.turns[turnIndex - 1]?.waiting;
|
|
249
|
+
const question = bank[sessionId]?.questions[index];
|
|
250
|
+
if (
|
|
251
|
+
!turn ||
|
|
252
|
+
turn.learnerMessage.trim().toUpperCase() !== answer.answer ||
|
|
253
|
+
!/^[ABCD]$/.test(answer.answer) ||
|
|
254
|
+
!question ||
|
|
255
|
+
answer.questionId !== question.id ||
|
|
256
|
+
answer.correct !== (answer.answer === question.correct) ||
|
|
257
|
+
pending?.kind !== 'quiz' ||
|
|
258
|
+
pending.sessionId !== sessionId ||
|
|
259
|
+
pending.questionId !== question.id ||
|
|
260
|
+
pending.roundId !== round.id
|
|
261
|
+
)
|
|
262
|
+
fail('quiz answer lacks its actual learner message');
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function quizForTurn(conversation, incoming, bank) {
|
|
266
|
+
const quiz = structuredClone(conversation.quiz);
|
|
267
|
+
const activeQuiz = structuredClone(conversation.activeQuiz ?? {});
|
|
268
|
+
const pending = conversation.turns.at(-1)?.waiting;
|
|
269
|
+
if (pending?.kind === 'quiz') {
|
|
270
|
+
const item = bank[pending.sessionId]?.questions.find((question) => question.id === pending.questionId);
|
|
271
|
+
if (!item) fail('saved pending quiz question is unknown');
|
|
272
|
+
const answer = incoming.learnerMessage.trim().toUpperCase();
|
|
273
|
+
if (/^[ABCD]$/.test(answer)) {
|
|
274
|
+
const round = activeQuiz[pending.sessionId]?.id === pending.roundId ? activeQuiz[pending.sessionId] : quiz[pending.sessionId];
|
|
275
|
+
if (!round || round.id !== pending.roundId || round.answers.some((entry) => entry.questionId === item.id))
|
|
276
|
+
fail('saved quiz round is inconsistent');
|
|
277
|
+
round.answers.push({ questionId: item.id, messageId: incoming.id, answer, correct: answer === item.correct });
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return { quiz, activeQuiz };
|
|
281
|
+
}
|
|
282
|
+
/** Accept only a caller assertion that answers the field's assessment question. */
|
|
283
|
+
function supportsLearnerFact(turns, cited, field, value) {
|
|
284
|
+
const index = turns.findIndex((turn) => turn.id === cited);
|
|
285
|
+
if (index === -1) return false;
|
|
286
|
+
const pending = turns[index - 1]?.waiting;
|
|
287
|
+
if (pending?.kind === 'assessment' && pending.field !== field) return false;
|
|
288
|
+
const message = turns[index].learnerMessage.toLowerCase();
|
|
289
|
+
const target = value.toLowerCase();
|
|
290
|
+
const at = message.indexOf(target);
|
|
291
|
+
if (at === -1) return false;
|
|
292
|
+
if (['role', 'experience_level'].includes(field) && /[a-z0-9_]/.test(message[at - 1] ?? '')) return false;
|
|
293
|
+
if (['role', 'experience_level'].includes(field) && /[a-z0-9_]/.test(message[at + target.length] ?? '')) return false;
|
|
294
|
+
const prefix = message
|
|
295
|
+
.slice(0, at)
|
|
296
|
+
.split(/[.!?;,]/)
|
|
297
|
+
.at(-1);
|
|
298
|
+
if (/\b(?:not|never|no|without|don't|dont|cannot|can't)\b(?:\W+\w+){0,4}\W*$/.test(prefix)) return false;
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
/** Match displayed choice letters and text to the canonical quiz bank. */
|
|
302
|
+
function hasCanonicalOptions(reply, question) {
|
|
303
|
+
const shown = new Map();
|
|
304
|
+
for (const line of reply.replaceAll(/\*\*|`/g, '').split(/\r?\n/)) {
|
|
305
|
+
const match = line.match(/^\s*(?:[-*]\s*)?([ABCD])\s*[).:-]\s*(.*?)\s*$/);
|
|
306
|
+
if (!match) continue;
|
|
307
|
+
if (shown.has(match[1]) || match[2] !== question.options[match[1]]) return false;
|
|
308
|
+
shown.set(match[1], match[2]);
|
|
309
|
+
}
|
|
310
|
+
return Object.keys(question.options).every((letter) => shown.has(letter));
|
|
311
|
+
}
|
|
312
|
+
function scoredRound(round, questions) {
|
|
313
|
+
if (
|
|
314
|
+
!round ||
|
|
315
|
+
round.answers.length !== questions.length ||
|
|
316
|
+
questions.some((question, index) => round.answers[index]?.questionId !== question.id)
|
|
317
|
+
)
|
|
318
|
+
return null;
|
|
319
|
+
const correct = round.answers.filter((answer) => answer.correct).length;
|
|
320
|
+
return { score: Math.round((correct / questions.length) * 10_000) / 100, passed: (correct / questions.length) * 100 >= 70 };
|
|
321
|
+
}
|
|
322
|
+
/** Bind trusted imported completions to specific session facts; publication relocates notes. */
|
|
323
|
+
function trustedSessionFingerprint(session) {
|
|
324
|
+
const facts = { ...session };
|
|
325
|
+
delete facts.notes_artifact;
|
|
326
|
+
return hash(JSON.stringify(Object.fromEntries(Object.entries(facts).sort(([left], [right]) => left.localeCompare(right)))));
|
|
327
|
+
}
|
|
328
|
+
/** Cross-check saved progress against caller-owned history before invoking a tutor or publishing. */
|
|
329
|
+
function checkHistory(progress, conversation, bank) {
|
|
330
|
+
for (const session of progress.sessions) {
|
|
331
|
+
if (session.status !== 'completed') continue;
|
|
332
|
+
const imported = conversation.importedProgress;
|
|
333
|
+
if (
|
|
334
|
+
imported?.completedSessions?.[session.id] === trustedSessionFingerprint(session) &&
|
|
335
|
+
typeof imported.path === 'string' &&
|
|
336
|
+
typeof imported.sha256 === 'string' &&
|
|
337
|
+
/^[a-f0-9]{64}$/.test(imported.sha256)
|
|
338
|
+
)
|
|
339
|
+
continue;
|
|
340
|
+
const questions = bank[session.id]?.questions;
|
|
341
|
+
if (!Array.isArray(questions)) fail('completed session has no canonical quiz bank');
|
|
342
|
+
let result;
|
|
343
|
+
if (questions.length === 0) {
|
|
344
|
+
const completed = conversation.turns.some((turn, index) => {
|
|
345
|
+
const pending = conversation.turns[index - 1]?.waiting;
|
|
346
|
+
return (
|
|
347
|
+
pending?.kind === 'completion' && pending.sessionId === session.id && /^(?:c|complete|done)$/i.test(turn.learnerMessage.trim())
|
|
348
|
+
);
|
|
349
|
+
});
|
|
350
|
+
if (!completed) fail('saved exploratory completion lacks its caller choice');
|
|
351
|
+
result = { score: 100, passed: null };
|
|
352
|
+
} else {
|
|
353
|
+
result = scoredRound(conversation.quiz[session.id], questions);
|
|
354
|
+
if (!result) fail(`saved completion lacks caller quiz evidence: ${session.id}`);
|
|
355
|
+
if (!result.passed) {
|
|
356
|
+
const lastAnswer = conversation.quiz[session.id].answers.at(-1).messageId;
|
|
357
|
+
const answerIndex = conversation.turns.findIndex((turn) => turn.id === lastAnswer);
|
|
358
|
+
if (
|
|
359
|
+
!conversation.turns.some(
|
|
360
|
+
(turn, index) =>
|
|
361
|
+
index > answerIndex &&
|
|
362
|
+
conversation.turns[index - 1]?.waiting.kind === 'review' &&
|
|
363
|
+
conversation.turns[index - 1].waiting.sessionId === session.id &&
|
|
364
|
+
/^\[?c\]?$/i.test(turn.learnerMessage.trim()),
|
|
365
|
+
)
|
|
366
|
+
)
|
|
367
|
+
fail('saved below-pass completion lacks its caller Continue choice');
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (
|
|
371
|
+
Math.abs(session.score - result.score) > 0.011 ||
|
|
372
|
+
session.quiz_passed !== result.passed ||
|
|
373
|
+
['passed', 'mastered'].some((field) => session[field] !== undefined && session[field] !== result.passed)
|
|
374
|
+
)
|
|
375
|
+
fail('saved completion contradicts its caller quiz grade');
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
/** Verify the tutor output against host-owned incoming messages and quiz results. */
|
|
379
|
+
function validateTurn({
|
|
380
|
+
request,
|
|
381
|
+
conversation,
|
|
382
|
+
before,
|
|
383
|
+
bank,
|
|
384
|
+
attemptDir,
|
|
385
|
+
stagedRoot,
|
|
386
|
+
progressFile,
|
|
387
|
+
responseFile,
|
|
388
|
+
inputDigests,
|
|
389
|
+
assertInputsUnchanged,
|
|
390
|
+
}) {
|
|
391
|
+
assertInputsUnchanged();
|
|
392
|
+
for (const [file, digest] of inputDigests) if (hash(fs.readFileSync(file)) !== digest) fail('the agent changed a supplied input');
|
|
393
|
+
const progressArtifact = regularArtifact(attemptDir, progressFile);
|
|
394
|
+
const progress = parseProgress(progressArtifact.bytes.toString('utf8')).progress;
|
|
395
|
+
checkProgress(progress, request.learner);
|
|
396
|
+
const response = JSON.parse(regularArtifact(attemptDir, responseFile).bytes);
|
|
397
|
+
if (
|
|
398
|
+
!table(response) ||
|
|
399
|
+
response.requestId !== request.requestId ||
|
|
400
|
+
response.learner !== request.learner ||
|
|
401
|
+
typeof response.reply !== 'string' ||
|
|
402
|
+
!response.reply.trim() ||
|
|
403
|
+
!table(response.waiting) ||
|
|
404
|
+
!table(response.factEvidence)
|
|
405
|
+
)
|
|
406
|
+
fail('response does not identify this learner turn');
|
|
407
|
+
if (Object.keys(response).some((key) => !['requestId', 'learner', 'reply', 'waiting', 'factEvidence'].includes(key)))
|
|
408
|
+
fail('response includes unsupported fields; learner messages are host-owned');
|
|
409
|
+
const incoming = { id: request.requestId, learnerMessage: request.message, tutorReply: response.reply, waiting: response.waiting };
|
|
410
|
+
const { quiz, activeQuiz } = quizForTurn(conversation, incoming, bank);
|
|
411
|
+
const messages = [...conversation.turns, incoming];
|
|
412
|
+
for (const field of FACTS) {
|
|
413
|
+
if (before?.[field] !== null && before?.[field] !== undefined && !isDeepStrictEqual(before[field], progress[field]))
|
|
414
|
+
fail(`the tutor changed established learner ${field}`);
|
|
415
|
+
if (
|
|
416
|
+
progress[field] !== null &&
|
|
417
|
+
(before?.[field] === null || before?.[field] === undefined) &&
|
|
418
|
+
!supportsLearnerFact(messages, response.factEvidence[field], field, progress[field])
|
|
419
|
+
)
|
|
420
|
+
fail(`learner ${field} lacks caller-message evidence`);
|
|
421
|
+
}
|
|
422
|
+
const artifacts = new Map();
|
|
423
|
+
const collect = (file) => {
|
|
424
|
+
const artifact = regularArtifact(stagedRoot, file);
|
|
425
|
+
artifacts.set(path.relative(stagedRoot, artifact.path), artifact.bytes);
|
|
426
|
+
return artifact;
|
|
427
|
+
};
|
|
428
|
+
for (const [index, session] of progress.sessions.entries()) {
|
|
429
|
+
const previous = before?.sessions[index];
|
|
430
|
+
if (session.score === null && ['quiz_passed', 'passed', 'mastered'].some((field) => session[field] === true))
|
|
431
|
+
fail('session claims mastery without quiz evidence');
|
|
432
|
+
if (previous?.status === 'completed' && !isDeepStrictEqual(previous, session))
|
|
433
|
+
fail(`the tutor changed completed session ${session.id}`);
|
|
434
|
+
if (session.status === 'completed') {
|
|
435
|
+
const notes = collect(session.notes_artifact);
|
|
436
|
+
const notesText = notes.bytes.toString('utf8');
|
|
437
|
+
const metadata = notesText.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
|
|
438
|
+
if (!metadata || !notesText.slice(metadata[0].length).trim()) fail('completed session notes lack metadata or content');
|
|
439
|
+
const noteState = yaml.load(metadata[1], { schema: yaml.JSON_SCHEMA });
|
|
440
|
+
if (
|
|
441
|
+
noteState?.session_id !== session.id ||
|
|
442
|
+
noteState.user !== request.learner ||
|
|
443
|
+
Math.abs(noteState.score - session.score) > 0.011 ||
|
|
444
|
+
typeof noteState.score !== 'number'
|
|
445
|
+
)
|
|
446
|
+
fail('session notes disagree with learner, session or score');
|
|
447
|
+
if (previous?.status !== 'completed') {
|
|
448
|
+
const questions = bank[session.id]?.questions;
|
|
449
|
+
if (!Array.isArray(questions)) fail('skill quiz bank is missing this session');
|
|
450
|
+
let result;
|
|
451
|
+
if (questions.length === 0) {
|
|
452
|
+
const pending = conversation.turns.at(-1)?.waiting;
|
|
453
|
+
if (
|
|
454
|
+
pending?.kind !== 'completion' ||
|
|
455
|
+
pending.sessionId !== session.id ||
|
|
456
|
+
previous?.status !== 'in-progress' ||
|
|
457
|
+
!/^(?:c|complete|done)$/i.test(request.message.trim())
|
|
458
|
+
)
|
|
459
|
+
fail('exploratory completion requires the learner’s explicit completion choice');
|
|
460
|
+
result = { score: 100, passed: null };
|
|
461
|
+
} else {
|
|
462
|
+
result = scoredRound(activeQuiz[session.id] ?? quiz[session.id], questions);
|
|
463
|
+
if (!result) fail('session completion lacks all three caller-owned quiz answers');
|
|
464
|
+
const pending = conversation.turns.at(-1)?.waiting;
|
|
465
|
+
if (
|
|
466
|
+
!result.passed &&
|
|
467
|
+
!(pending?.kind === 'review' && pending.sessionId === session.id && /^\[?c\]?$/i.test(request.message.trim()))
|
|
468
|
+
)
|
|
469
|
+
fail('below-pass completion requires the learner’s explicit Continue choice');
|
|
470
|
+
}
|
|
471
|
+
if (Math.abs(session.score - result.score) > 0.011) fail('session score disagrees with its actual quiz evidence');
|
|
472
|
+
for (const field of ['quiz_passed', 'passed', 'mastered'])
|
|
473
|
+
if (session[field] !== undefined && session[field] !== result.passed) fail('session claims unearned mastery');
|
|
474
|
+
session.quiz_passed = result.passed;
|
|
475
|
+
if (activeQuiz[session.id]) {
|
|
476
|
+
quiz[session.id] = activeQuiz[session.id];
|
|
477
|
+
delete activeQuiz[session.id];
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
} else if (session.score !== null) {
|
|
481
|
+
const activeResult = scoredRound(activeQuiz[session.id], bank[session.id]?.questions ?? []);
|
|
482
|
+
const result = activeResult ?? scoredRound(quiz[session.id], bank[session.id]?.questions ?? []);
|
|
483
|
+
if (!result || Math.abs(session.score - result.score) > 0.011) fail('session score lacks complete quiz evidence');
|
|
484
|
+
for (const field of ['quiz_passed', 'passed', 'mastered'])
|
|
485
|
+
if (session[field] !== undefined && session[field] !== result.passed) fail('session claims unearned mastery');
|
|
486
|
+
if (activeResult) {
|
|
487
|
+
quiz[session.id] = activeQuiz[session.id];
|
|
488
|
+
delete activeQuiz[session.id];
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
const waiting = response.waiting;
|
|
493
|
+
if (
|
|
494
|
+
['lesson', 'quiz', 'review', 'completion'].includes(waiting.kind) &&
|
|
495
|
+
['role', 'experience_level', 'learning_goals'].some((field) => progress[field] === null)
|
|
496
|
+
)
|
|
497
|
+
fail('teaching requires the caller-supported learner assessment');
|
|
498
|
+
if (!['assessment', 'menu', 'lesson', 'quiz', 'review', 'completion'].includes(waiting.kind))
|
|
499
|
+
fail('response has an invalid learner stop point');
|
|
500
|
+
if (['lesson', 'quiz', 'review', 'completion'].includes(waiting.kind) && !SESSION_IDS.includes(waiting.sessionId))
|
|
501
|
+
fail('response stop point has no canonical session');
|
|
502
|
+
if (waiting.kind === 'assessment' && !FACTS.includes(waiting.field)) fail('assessment stop point has no known field');
|
|
503
|
+
if (waiting.kind === 'quiz') {
|
|
504
|
+
const questions = bank[waiting.sessionId].questions;
|
|
505
|
+
const question = questions.find((item) => item.id === waiting.questionId);
|
|
506
|
+
if (!question || !response.reply.includes(question.question) || !hasCanonicalOptions(response.reply, question))
|
|
507
|
+
fail('pending quiz reply omits its canonical question or options');
|
|
508
|
+
let round = activeQuiz[waiting.sessionId] ?? quiz[waiting.sessionId];
|
|
509
|
+
if (
|
|
510
|
+
waiting.questionId === questions[0].id &&
|
|
511
|
+
(conversation.turns.at(-1)?.waiting.kind !== 'quiz' || round?.answers.length === questions.length)
|
|
512
|
+
)
|
|
513
|
+
round = (scoredRound(quiz[waiting.sessionId], questions) ? activeQuiz : quiz)[waiting.sessionId] = {
|
|
514
|
+
id: crypto.randomUUID(),
|
|
515
|
+
answers: [],
|
|
516
|
+
};
|
|
517
|
+
if (!round || questions[round.answers.length]?.id !== waiting.questionId) fail('pending quiz skips unanswered questions');
|
|
518
|
+
waiting.roundId = round.id;
|
|
519
|
+
}
|
|
520
|
+
if (
|
|
521
|
+
waiting.kind === 'review' &&
|
|
522
|
+
!scoredRound(activeQuiz[waiting.sessionId] ?? quiz[waiting.sessionId], bank[waiting.sessionId].questions)
|
|
523
|
+
)
|
|
524
|
+
fail('review stop point lacks a completed quiz');
|
|
525
|
+
if (progress.summary_generated) {
|
|
526
|
+
if (progress.sessions_completed !== 7 || typeof progress.summary_path !== 'string')
|
|
527
|
+
fail('completion summary lacks all seven completed sessions');
|
|
528
|
+
collect(progress.summary_path);
|
|
529
|
+
}
|
|
530
|
+
if (before?.summary_generated && (!progress.summary_generated || progress.summary_path !== before.summary_path))
|
|
531
|
+
fail('the tutor changed the established completion summary');
|
|
532
|
+
if (before?.completion_date != null && progress.completion_date !== before.completion_date)
|
|
533
|
+
fail('the tutor changed the established completion date');
|
|
534
|
+
if (before && before.stepsCompleted.some((step) => !progress.stepsCompleted.includes(step)))
|
|
535
|
+
fail('the tutor discarded saved workflow steps');
|
|
536
|
+
const nextConversation = { ...conversation, turns: messages, quiz, activeQuiz };
|
|
537
|
+
checkConversation(nextConversation, request.learner, bank);
|
|
538
|
+
checkHistory(progress, nextConversation, bank);
|
|
539
|
+
return { progress, conversation: nextConversation, artifacts };
|
|
540
|
+
}
|
|
541
|
+
module.exports = {
|
|
542
|
+
SESSION_IDS,
|
|
543
|
+
FACTS,
|
|
544
|
+
hash,
|
|
545
|
+
parseProgress,
|
|
546
|
+
checkProgress,
|
|
547
|
+
regularArtifact,
|
|
548
|
+
snapshot,
|
|
549
|
+
sameSnapshot,
|
|
550
|
+
publishState,
|
|
551
|
+
recoverPublishedState,
|
|
552
|
+
checkConversation,
|
|
553
|
+
trustedSessionFingerprint,
|
|
554
|
+
checkHistory,
|
|
555
|
+
quizForTurn,
|
|
556
|
+
scoredRound,
|
|
557
|
+
validateTurn,
|
|
558
|
+
};
|
|
@@ -74,7 +74,7 @@ function isReferenceHeading(heading) {
|
|
|
74
74
|
* second risk table, an explanatory sentence, an HTML comment). `Reference:` and
|
|
75
75
|
* `References:` open citations, so they label nothing; so does `For example:`.
|
|
76
76
|
*
|
|
77
|
-
* The shipped worked example, `bmad-
|
|
77
|
+
* The shipped worked example, `bmad-tea-test-design/resources/test-design-epic-3.example.md`,
|
|
78
78
|
* shows the register this rule keeps: the three band tables under `## Risk Assessment`
|
|
79
79
|
* (`### High Risks: Score 6 or Greater`, `### Medium Risks: Score 3 to 4`,
|
|
80
80
|
* `### Low Risks: Score 1 to 2`), none of whose headings or lead-ins labels anything.
|
package/cli/lib/workflow-cli.js
CHANGED
|
@@ -111,7 +111,11 @@ function resolveWorkflowSkill({ projectRoot, skillName, skillRoot }) {
|
|
|
111
111
|
if (!fs.statSync(path.join(knowledge, 'tea-index.csv')).isFile()) throw new Error('sibling bmod-tea knowledge is missing');
|
|
112
112
|
return fs.realpathSync(root);
|
|
113
113
|
} catch (error) {
|
|
114
|
-
throw new WorkflowError(
|
|
114
|
+
throw new WorkflowError(
|
|
115
|
+
'environment-configuration',
|
|
116
|
+
`${skillName} is unavailable at ${root}: ${error.message}. Install TEA with npx skills add bmad-code-org/bmad-method-test-architecture-enterprise`,
|
|
117
|
+
{ cause: error },
|
|
118
|
+
);
|
|
115
119
|
}
|
|
116
120
|
}
|
|
117
121
|
/** Activate the actual skill using authoritative config and caller run inputs. */
|