fde-framework 0.1.44__tar.gz → 0.1.45__tar.gz
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.
- {fde_framework-0.1.44 → fde_framework-0.1.45}/CHANGELOG.md +45 -1
- {fde_framework-0.1.44 → fde_framework-0.1.45}/PKG-INFO +17 -8
- {fde_framework-0.1.44 → fde_framework-0.1.45}/README.md +16 -7
- {fde_framework-0.1.44 → fde_framework-0.1.45}/pyproject.toml +1 -1
- {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/cli.py +7 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/mcp_server.py +70 -22
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_mcp.py +86 -3
- fde_framework-0.1.45/tests/test_mcp_stdio.py +119 -0
- fde_framework-0.1.45/tests/test_release_consistency.py +38 -0
- fde_framework-0.1.44/tests/test_release_consistency.py +0 -18
- {fde_framework-0.1.44 → fde_framework-0.1.45}/.gitignore +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/LICENSE +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/examples/invoice-extraction/README.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/examples/policy-qa/README.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/examples/support-triage/README.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/ansible-playbook.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/assisted-deterministic.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/audit-only.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/boundary-and-audit.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/cascade.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/classical-ml.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/compose.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/decision-log.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/deterministic-masking.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/deterministic.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/direct-call.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/episodic-store.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/explainability-record.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/field-match.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/finetune.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/fixed-sequence.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/gitops.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/governed-tools.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/graph-expanded-retrieval.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/graph-retrieval.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/hybrid-search.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/judged.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/keyword-search.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/kubernetes-manifests.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/labelled-decision.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/labelled-metrics.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/llm-extraction.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/llm-scrubbing.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/llm.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/local-embedding.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/managed-api.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/managed-embedding.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/manual-runbook.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/model-planner.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/ocr-pipeline.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/optimisation-reasoning.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/optimisation.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/passthrough.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/reranked-retrieval.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/role-scoped-authority.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/segmentation.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/self-hosted.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/serverless-gpu.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/speech-transcription.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/structured-logs.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/systemd-unit.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/terraform-module.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/text-extraction.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/traced.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/vector-search.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/video-ingestion.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/windowed-ingestion.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/approaches/working-state.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/cases/churn-scoring.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/cases/route-planning.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/cases/structured-extraction.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/cases/studio-style.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/components/accountability.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/components/deployment.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/components/embedding.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/components/evaluation.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/components/governance.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/components/integration.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/components/memory.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/components/observability.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/components/perception.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/components/planning.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/components/provisioning.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/components/reasoning.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/components/redaction.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/components/representation.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/components/retrieval.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/components/serving.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/dimensions/accelerator.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/dimensions/access_model.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/dimensions/arrival_rate.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/dimensions/availability_target.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/dimensions/cheap_path_coverage.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/dimensions/confidence_calibrated.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/dimensions/container_competence.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/dimensions/corpus_churn.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/dimensions/corpus_size.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/dimensions/data_residency.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/dimensions/environment_lifetime.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/dimensions/existing_cluster.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/dimensions/existing_iac_tool.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/dimensions/external_systems.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/dimensions/hosting.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/dimensions/human_waiting.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/dimensions/input_format.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/dimensions/interpretability_required.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/dimensions/labelled_count.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/dimensions/latency_budget_ms.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/dimensions/licence_posture.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/dimensions/operates_after_handover.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/dimensions/output_shape.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/dimensions/provisioning_api.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/dimensions/query_pattern.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/dimensions/recall_span.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/dimensions/sensitivity_present.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/interfaces/Generator.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/interfaces/Guard.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/interfaces/Mapper.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/interfaces/ModelServer.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/interfaces/Parser.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/interfaces/Planner.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/interfaces/Retriever.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/interfaces/Scorer.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/interfaces/Store.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/interfaces/ToolBoundary.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/interfaces/Tracer.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/locales/eu-gdpr.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/locales/in-dpdp.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/ansible-playbook.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/assisted-deterministic.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/audit-only.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/boundary-and-audit.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/cascade-reasoning.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/cascade-representation.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/cascade-retrieval.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/classical-ml-reasoning.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/classical-ml.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/compose.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/decision-log.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/deterministic-masking.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/deterministic.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/direct-call.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/episodic-store.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/explainability-record.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/field-match.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/finetune-representation.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/finetune.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/fixed-sequence.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/gitops.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/governed-tools.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/graph-expanded-retrieval.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/graph-retrieval.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/hybrid-search.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/judged.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/keyword-search.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/kubernetes-manifests.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/labelled-decision.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/labelled-metrics.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/llm-representation.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/llm-scrubbing.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/llm.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/local-embedding.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/managed-api.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/managed-embedding.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/manual-runbook.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/model-planner.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/ocr-pipeline.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/optimisation-reasoning.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/optimisation.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/passthrough.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/reranked-retrieval.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/role-scoped-authority.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/segmentation.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/self-hosted.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/serverless-gpu.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/speech-transcription.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/structured-logs.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/systemd-unit.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/terraform-module.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/text-extraction.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/traced.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/vector-search.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/video-ingestion.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/windowed-ingestion.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/patterns/working-state.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/stacks/langgraph.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/stacks/llamaindex.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/stacks/local-judge.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/stacks/mcp.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/stacks/ollama.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/stacks/openai-judge.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/stacks/opentelemetry.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/stacks/ortools.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/stacks/peft.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/stacks/pgvector.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/stacks/plain-python.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/stacks/qdrant.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/stacks/supermemory.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/stacks/tesseract.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/stacks/vllm.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/stacks/whisper.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/stacks/xgboost.md +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/accountability/decision-log.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/accountability/explainability-record.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/deployment/compose.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/deployment/kubernetes-manifests.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/deployment/systemd-unit.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/embedding/local-embedding.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/embedding/managed-embedding.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/evaluation/field-match.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/evaluation/judged.local-judge.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/evaluation/judged.openai-judge.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/evaluation/judged.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/evaluation/labelled-metrics.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/evaluation/labelled-metrics.xgboost.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/governance/audit-only.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/governance/boundary-and-audit.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/governance/role-scoped-authority.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/integration/direct-call.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/integration/governed-tools.mcp.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/integration/governed-tools.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/memory/episodic-store.pgvector.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/memory/episodic-store.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/memory/episodic-store.supermemory.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/memory/working-state.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/observability/structured-logs.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/observability/traced.opentelemetry.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/observability/traced.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/perception/ocr-pipeline.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/perception/ocr-pipeline.tesseract.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/perception/passthrough.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/perception/speech-transcription.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/perception/speech-transcription.whisper.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/perception/text-extraction.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/perception/video-ingestion.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/perception/windowed-ingestion.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/planning/fixed-sequence.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/planning/model-planner.langgraph.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/planning/model-planner.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/planning/optimisation.ortools.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/planning/optimisation.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/provisioning/ansible-playbook.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/provisioning/gitops.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/provisioning/manual-runbook.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/provisioning/terraform-module.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/reasoning/cascade.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/reasoning/classical-ml.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/reasoning/classical-ml.xgboost.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/reasoning/finetune.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/reasoning/labelled-decision.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/reasoning/llm.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/reasoning/optimisation.ortools.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/reasoning/optimisation.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/redaction/deterministic-masking.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/redaction/llm-scrubbing.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/representation/assisted.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/representation/cascade.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/representation/classical-ml.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/representation/classical-ml.xgboost.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/representation/deterministic.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/representation/finetune.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/representation/llm.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/representation/segmentation.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/retrieval/cascade.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/retrieval/graph-expanded-retrieval.pgvector.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/retrieval/graph-expanded-retrieval.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/retrieval/graph-expanded-retrieval.qdrant.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/retrieval/graph-retrieval.pgvector.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/retrieval/graph-retrieval.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/retrieval/graph-retrieval.qdrant.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/retrieval/hybrid-search.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/retrieval/keyword-search.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/retrieval/reranked-retrieval.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/retrieval/vector-search.llamaindex.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/retrieval/vector-search.pgvector.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/retrieval/vector-search.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/retrieval/vector-search.qdrant.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/serving/managed-api.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/serving/self-hosted.ollama.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/serving/self-hosted.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/serving/self-hosted.vllm.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/framework/templates/serving/serverless-gpu.plain.py.j2 +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/__init__.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/architect.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/bench.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/costing.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/debt.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/decide.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/decompose.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/deploy.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/drift.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/emit.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/evolution.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/experiment.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/factlog.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/forecast.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/gates.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/graph.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/history.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/impact.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/implement.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/importer.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/intake/__init__.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/intake/answers.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/intake/documents.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/intake/interview.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/intake/llm_reader.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/intake/prose.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/intake/samples.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/lifecycle.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/models/__init__.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/models/base.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/models/fact.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/models/profile.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/models/respondent.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/models/schema.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/moves.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/ops.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/predicate.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/realization.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/registry.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/sandbox.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/scan.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/scorecard.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/space.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/stakeholders.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/stop.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/training.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/value.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/src/fde/workflow.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/__init__.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/conftest.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/models/__init__.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/models/test_profile.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_acceptance.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_access_and_sensitivity.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_answers.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_audit_fixes.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_bench.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_build_receipt.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_cascade.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_cli_engagement.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_cli_evolution.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_cli_gates.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_cli_intake.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_cli_kb.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_cli_robustness.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_cli_samples.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_costing.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_coverage.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_dead_zones.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_debt.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_decide.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_decompose.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_deploy.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_documents.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_drift.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_embedding.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_emit.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_evolution.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_example_transcript.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_experiment.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_factlog.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_finetune.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_finetune_real.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_forecast.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_gates.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_history.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_impact.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_implement.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_importer.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_interview.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_lifecycle.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_llm_reader.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_locales.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_memory_retrieval.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_more_templates.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_moves.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_multimodal.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_next.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_ops.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_outcome_contract.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_package.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_prose.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_realization.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_registry.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_remaining.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_retrieval_eval.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_reversibility.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_samples.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_sandbox.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_sanitisation.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_scan.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_schema.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_scorecard.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_serving.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_shipped_registry.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_space.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_stack_swap.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_stakeholders.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_stop.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_supermemory.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_templates.py +0 -0
- {fde_framework-0.1.44 → fde_framework-0.1.45}/tests/test_value.py +0 -0
|
@@ -5,7 +5,51 @@ the project is pre-release, so everything sits under 0.1.0 until the first tag.
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
-
## [0.1.
|
|
8
|
+
## [0.1.45] — 2026-10-11
|
|
9
|
+
|
|
10
|
+
A twelfth outside reading, and a coding agent driving the server for the
|
|
11
|
+
first time, found three things between them: a release page that said
|
|
12
|
+
nothing about what it was, a date a day ahead, and an MCP server that lost
|
|
13
|
+
the agent's second call two different ways.
|
|
14
|
+
|
|
15
|
+
- **The agent's second call finds the first call's work.**
|
|
16
|
+
`start(name="acme")` puts the record at engagements/acme;
|
|
17
|
+
`status(engagement="acme")` a moment later looked at `<workspace>/acme`
|
|
18
|
+
and said there was no engagement. The in-process test had passed the
|
|
19
|
+
path `start` returned, so it never asked by name. A bare name is now
|
|
20
|
+
looked for where `start` puts it, as `fde status acme` does at the
|
|
21
|
+
terminal; a path is taken as given; a name that climbs out is refused
|
|
22
|
+
before anything is looked for.
|
|
23
|
+
- **Calls issued at once all come back.** Claude Code asked for `status`
|
|
24
|
+
and `next` in the same breath and the second came back "Connection
|
|
25
|
+
closed". The server runs each tool on its own thread, and the capture
|
|
26
|
+
that collects a command's output does so by swapping the process's
|
|
27
|
+
stdout; two swaps interleaved left one command printing straight onto
|
|
28
|
+
the JSON-RPC channel. Commands now run one at a time inside the server.
|
|
29
|
+
A new suite drives the server as a separate process over stdio with the
|
|
30
|
+
official client: six calls at once for three rounds, a call abandoned
|
|
31
|
+
midway, a record changed on disk between calls, and every tool's input
|
|
32
|
+
schema published. The hang reproduced before the fix and fails within
|
|
33
|
+
ninety seconds instead of hanging CI.
|
|
34
|
+
- **The envelope's contract is a JSON Schema.** `fde mcp --schema` prints
|
|
35
|
+
it; every reply is validated against it; the schema's list of tools is
|
|
36
|
+
the server's; a field it does not name or a status it does not name
|
|
37
|
+
fails. The README says when the version moves: a field changing meaning
|
|
38
|
+
or going away, never one being added.
|
|
39
|
+
- **A release page means verified.** It is written only after the wheel
|
|
40
|
+
PyPI serves has been installed on Python 3.11, 3.12 and 3.13, hashed
|
|
41
|
+
against the wheel the tests ran on, and walked through the first five
|
|
42
|
+
minutes. It carries the wheel and the sdist, their SHA-256, the commit,
|
|
43
|
+
the run and the install line.
|
|
44
|
+
- **One date.** 0.1.44 shipped on October 11 dated October 12 in both
|
|
45
|
+
CHANGELOG and CITATION. Corrected; the release workflow now holds the
|
|
46
|
+
CHANGELOG date, CITATION's date-released and the tagged commit's own day
|
|
47
|
+
in the author's time zone to one value, and a test holds the two files
|
|
48
|
+
to each other.
|
|
49
|
+
- A corrupt record is a sentence naming the file, not a traceback, through
|
|
50
|
+
the server as at the terminal; now pinned by a test.
|
|
51
|
+
|
|
52
|
+
## [0.1.44] — 2026-10-11
|
|
9
53
|
|
|
10
54
|
An eleventh outside reading found the README contradicting itself on its
|
|
11
55
|
own test count, and asked for the published wheel to be the thing that
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: fde-framework
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.45
|
|
4
4
|
Summary: The decision before the AI build: evidence in, an architecture with receipts out, a scored build or a stop. For Forward Deployed Engineers and anyone deciding whether an AI system should exist.
|
|
5
5
|
Project-URL: Homepage, https://github.com/atulkapoor/fde-framework
|
|
6
6
|
Project-URL: Documentation, https://atulkapoor.github.io/fde-framework/
|
|
@@ -159,15 +159,24 @@ It serves `triage`, `start`, `frame`, `status`, `next`, `stage`, `debt`,
|
|
|
159
159
|
refusals included, beside a small envelope an agent can branch on without
|
|
160
160
|
parsing prose: `schema_version`, `status` (ok, blocked, refused, failed),
|
|
161
161
|
`mutated`, and the workspace. The envelope follows the command's exit,
|
|
162
|
-
never a reading of its text
|
|
163
|
-
|
|
164
|
-
|
|
162
|
+
never a reading of its text. Its contract is a JSON Schema, `fde mcp
|
|
163
|
+
--schema`, that every reply is tested against; a client pins
|
|
164
|
+
`schema_version`, which moves only when a field changes meaning or goes
|
|
165
|
+
away, never when one is added. An engagement is named as `start` named
|
|
166
|
+
it (`start(name="acme")`, then `status(engagement="acme")`), or by its
|
|
167
|
+
path. An agent is not a person, so every path it supplies must stay
|
|
168
|
+
inside one workspace, the directory the server started in or
|
|
165
169
|
`FDE_WORKSPACE`; a path outside it, a traversal, or a symlink that points
|
|
166
170
|
out is refused in words, and engagement names are plain names. The two
|
|
167
171
|
tools that write to the record, `start` and `frame`, say so and carry the
|
|
168
|
-
protocol's hints so a client can ask first.
|
|
169
|
-
|
|
170
|
-
|
|
172
|
+
protocol's hints so a client can ask first. Calls run one at a time
|
|
173
|
+
inside the server, so an agent that asks for `status` and `next` in the
|
|
174
|
+
same breath gets both. Exercised against two real clients: the official
|
|
175
|
+
MCP Python client over stdio, in the test suite, with concurrent calls,
|
|
176
|
+
an abandoned call and a record changed on disk between calls; and Claude
|
|
177
|
+
Code 2.1, by hand, from the configuration above. Building, implementing
|
|
178
|
+
and deploying are not served; they stay on the command line, where the
|
|
179
|
+
person who runs them is accountable for them.
|
|
171
180
|
|
|
172
181
|
> ### Five founding engagements
|
|
173
182
|
> One real workflow, your real examples, a measured baseline, and someone
|
|
@@ -232,7 +241,7 @@ ladder, so nothing here can be over-read:
|
|
|
232
241
|
|
|
233
242
|
| Claim | Status | The evidence, and where it stops |
|
|
234
243
|
|---|---|---|
|
|
235
|
-
| The framework is implemented | verified | 1,
|
|
244
|
+
| The framework is implemented | verified | 1,316 tests on three Pythons in CI; lint clean; every audit finding pinned as a check before it was fixed; every release installed from the published wheel into a fresh interpreter and walked through the first five minutes |
|
|
236
245
|
| A build is reproducible | verified | byte-identical rebuild pinned by test; each public demo carries a Reproduce section and its digests |
|
|
237
246
|
| The decisions generalise | partial | four shapes (extraction, freeform QA, a labelled decision, seventy-seven-way routing) on real data; one industry set; `fde kb sweep` names the shapes nothing serves |
|
|
238
247
|
| The deliverable is production-reliable | unverified | no production engagement; the banking operating loop ran on a laptop and its record says so |
|
|
@@ -115,15 +115,24 @@ It serves `triage`, `start`, `frame`, `status`, `next`, `stage`, `debt`,
|
|
|
115
115
|
refusals included, beside a small envelope an agent can branch on without
|
|
116
116
|
parsing prose: `schema_version`, `status` (ok, blocked, refused, failed),
|
|
117
117
|
`mutated`, and the workspace. The envelope follows the command's exit,
|
|
118
|
-
never a reading of its text
|
|
119
|
-
|
|
120
|
-
|
|
118
|
+
never a reading of its text. Its contract is a JSON Schema, `fde mcp
|
|
119
|
+
--schema`, that every reply is tested against; a client pins
|
|
120
|
+
`schema_version`, which moves only when a field changes meaning or goes
|
|
121
|
+
away, never when one is added. An engagement is named as `start` named
|
|
122
|
+
it (`start(name="acme")`, then `status(engagement="acme")`), or by its
|
|
123
|
+
path. An agent is not a person, so every path it supplies must stay
|
|
124
|
+
inside one workspace, the directory the server started in or
|
|
121
125
|
`FDE_WORKSPACE`; a path outside it, a traversal, or a symlink that points
|
|
122
126
|
out is refused in words, and engagement names are plain names. The two
|
|
123
127
|
tools that write to the record, `start` and `frame`, say so and carry the
|
|
124
|
-
protocol's hints so a client can ask first.
|
|
125
|
-
|
|
126
|
-
|
|
128
|
+
protocol's hints so a client can ask first. Calls run one at a time
|
|
129
|
+
inside the server, so an agent that asks for `status` and `next` in the
|
|
130
|
+
same breath gets both. Exercised against two real clients: the official
|
|
131
|
+
MCP Python client over stdio, in the test suite, with concurrent calls,
|
|
132
|
+
an abandoned call and a record changed on disk between calls; and Claude
|
|
133
|
+
Code 2.1, by hand, from the configuration above. Building, implementing
|
|
134
|
+
and deploying are not served; they stay on the command line, where the
|
|
135
|
+
person who runs them is accountable for them.
|
|
127
136
|
|
|
128
137
|
> ### Five founding engagements
|
|
129
138
|
> One real workflow, your real examples, a measured baseline, and someone
|
|
@@ -188,7 +197,7 @@ ladder, so nothing here can be over-read:
|
|
|
188
197
|
|
|
189
198
|
| Claim | Status | The evidence, and where it stops |
|
|
190
199
|
|---|---|---|
|
|
191
|
-
| The framework is implemented | verified | 1,
|
|
200
|
+
| The framework is implemented | verified | 1,316 tests on three Pythons in CI; lint clean; every audit finding pinned as a check before it was fixed; every release installed from the published wheel into a fresh interpreter and walked through the first five minutes |
|
|
192
201
|
| A build is reproducible | verified | byte-identical rebuild pinned by test; each public demo carries a Reproduce section and its digests |
|
|
193
202
|
| The decisions generalise | partial | four shapes (extraction, freeform QA, a labelled decision, seventy-seven-way routing) on real data; one industry set; `fde kb sweep` names the shapes nothing serves |
|
|
194
203
|
| The deliverable is production-reliable | unverified | no production engagement; the banking operating loop ran on a laptop and its record says so |
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# Distribution name matches the repository and stays unique on any index;
|
|
3
3
|
# the import package and the CLI command remain the short `fde`.
|
|
4
4
|
name = "fde-framework"
|
|
5
|
-
version = "0.1.
|
|
5
|
+
version = "0.1.45"
|
|
6
6
|
description = "The decision before the AI build: evidence in, an architecture with receipts out, a scored build or a stop. For Forward Deployed Engineers and anyone deciding whether an AI system should exist."
|
|
7
7
|
readme = "README.md"
|
|
8
8
|
license = {text = "Apache-2.0"}
|
|
@@ -2407,12 +2407,19 @@ def mcp_cmd(
|
|
|
2407
2407
|
check: Annotated[bool, typer.Option(
|
|
2408
2408
|
"--check", help="Build the server and list what it serves, without serving it."
|
|
2409
2409
|
)] = False,
|
|
2410
|
+
schema: Annotated[bool, typer.Option(
|
|
2411
|
+
"--schema", help="Print the JSON Schema of the envelope beside every reply."
|
|
2412
|
+
)] = False,
|
|
2410
2413
|
) -> None:
|
|
2411
2414
|
"""Serve the framework's conversational commands to a coding agent over
|
|
2412
2415
|
stdio (Model Context Protocol): triage, start, frame, status, next,
|
|
2413
2416
|
stage, debt, stop_when, history. Every path the agent supplies must be
|
|
2414
2417
|
inside the workspace (FDE_WORKSPACE, else the current directory). Add
|
|
2415
2418
|
it with, for example, `claude mcp add fde -- fde mcp`."""
|
|
2419
|
+
if schema:
|
|
2420
|
+
from fde.mcp_server import ENVELOPE_SCHEMA
|
|
2421
|
+
typer.echo(json.dumps(ENVELOPE_SCHEMA, indent=2))
|
|
2422
|
+
return
|
|
2416
2423
|
try:
|
|
2417
2424
|
from fde.mcp_server import describe, serve
|
|
2418
2425
|
if check:
|
|
@@ -30,22 +30,41 @@ from __future__ import annotations
|
|
|
30
30
|
|
|
31
31
|
import os
|
|
32
32
|
import re
|
|
33
|
+
import threading
|
|
33
34
|
from pathlib import Path
|
|
34
35
|
|
|
35
36
|
NAME = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$")
|
|
36
37
|
|
|
37
|
-
# The envelope beside every reply
|
|
38
|
-
#
|
|
38
|
+
# The envelope beside every reply, as the JSON Schema a client can validate
|
|
39
|
+
# against (`fde mcp --schema` prints it). Bump the version when a field
|
|
40
|
+
# changes meaning or goes away; adding a field keeps the version.
|
|
39
41
|
SCHEMA_VERSION = 1
|
|
40
42
|
STATUSES = ("ok", "blocked", "refused", "failed")
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
43
|
+
TOOLS = ("triage", "start", "frame", "status", "next", "stage", "debt", "stop_when", "history")
|
|
44
|
+
ENVELOPE_SCHEMA = {
|
|
45
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
46
|
+
"$id": f"https://github.com/atulkapoor/fde-framework/schemas/mcp-envelope/v{SCHEMA_VERSION}",
|
|
47
|
+
"title": f"fde MCP reply envelope, version {SCHEMA_VERSION}",
|
|
48
|
+
"description": "The structured content beside the text of every tool reply. The text "
|
|
49
|
+
"is what the command printed; the envelope is what an agent branches on "
|
|
50
|
+
"without parsing prose. It follows the command's exit, not its wording.",
|
|
51
|
+
"type": "object",
|
|
52
|
+
"properties": {
|
|
53
|
+
"schema_version": {"const": SCHEMA_VERSION,
|
|
54
|
+
"description": "Bumped when a field changes meaning or goes away."},
|
|
55
|
+
"tool": {"enum": list(TOOLS), "description": "The tool that answered."},
|
|
56
|
+
"status": {"enum": list(STATUSES),
|
|
57
|
+
"description": "ok: the command exited 0. blocked: it refused or a "
|
|
58
|
+
"condition triggered (exit 1); the text says which. "
|
|
59
|
+
"refused: a path or name was outside what the agent may "
|
|
60
|
+
"touch; nothing ran. failed: an exception; a bug to report."},
|
|
61
|
+
"mutated": {"type": "boolean",
|
|
62
|
+
"description": "True only when a writing tool ran and exited 0."},
|
|
63
|
+
"workspace": {"type": "string",
|
|
64
|
+
"description": "The directory every path was confined to."},
|
|
65
|
+
},
|
|
66
|
+
"required": ["schema_version", "tool", "status", "mutated", "workspace"],
|
|
67
|
+
"additionalProperties": False,
|
|
49
68
|
}
|
|
50
69
|
|
|
51
70
|
THIRTY_SECONDS = """\
|
|
@@ -68,10 +87,12 @@ brief into typed facts; `status` for the gates and what is still unknown;
|
|
|
68
87
|
for where the engagement stands on its record; `debt` for what nobody has
|
|
69
88
|
settled; `stop_when` for whether the evidence says stop; `history` for the
|
|
70
89
|
whole record in order. Every tool returns the command's own output,
|
|
71
|
-
refusals included
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
90
|
+
refusals included, with a status beside it (ok, blocked, refused, failed)
|
|
91
|
+
to branch on instead of the prose. `start` and `frame` write to the
|
|
92
|
+
record; the rest only read it. An engagement is named as `start` named
|
|
93
|
+
it, or by its path; every path must be inside the server's workspace.
|
|
94
|
+
Nothing here builds, implements or deploys; those are run by a person on
|
|
95
|
+
the command line.
|
|
75
96
|
"""
|
|
76
97
|
|
|
77
98
|
|
|
@@ -100,6 +121,21 @@ def confine(path: str | None, what: str, root: Path | None = None) -> Path | Non
|
|
|
100
121
|
return resolved
|
|
101
122
|
|
|
102
123
|
|
|
124
|
+
def locate(engagement: str) -> Path:
|
|
125
|
+
"""The engagement as the command will see it. `start(name="acme")` puts
|
|
126
|
+
the record at engagements/acme; `status(engagement="acme")` a moment
|
|
127
|
+
later must find it, as `fde status acme` does on the command line. So a
|
|
128
|
+
bare name is looked for at <workspace>/<name> and then where `start`
|
|
129
|
+
puts it; both candidates are confined, so a name that climbs out is
|
|
130
|
+
refused before anything is looked for. A path is taken as given."""
|
|
131
|
+
path = confine(engagement, "engagement")
|
|
132
|
+
if path is not None and not (path / "facts").is_dir() and not Path(engagement).is_absolute():
|
|
133
|
+
under = confine(str(Path("engagements") / engagement), "engagement")
|
|
134
|
+
if under is not None and (under / "facts").is_dir():
|
|
135
|
+
return under
|
|
136
|
+
return path
|
|
137
|
+
|
|
138
|
+
|
|
103
139
|
def plain_name(name: str) -> str:
|
|
104
140
|
if not NAME.match(name or ""):
|
|
105
141
|
raise Outside(f"an engagement name is a plain name (letters, digits, '.', '_', '-'), "
|
|
@@ -113,6 +149,16 @@ def _registry() -> str:
|
|
|
113
149
|
return os.environ.get("FDE_REGISTRY") or str(default_root())
|
|
114
150
|
|
|
115
151
|
|
|
152
|
+
# One command at a time. The server runs each tool on a worker thread, and
|
|
153
|
+
# a coding agent will issue `status` and `next` in the same breath; the
|
|
154
|
+
# runner that captures a command's output does so by swapping the
|
|
155
|
+
# process's stdout, and two swaps interleaved left one command printing
|
|
156
|
+
# straight onto the JSON-RPC channel. The protocol broke, the client saw
|
|
157
|
+
# "Connection closed", and the record was not at fault. Commands take
|
|
158
|
+
# milliseconds; the record has one writer at a time, as it should.
|
|
159
|
+
_ONE_AT_A_TIME = threading.Lock()
|
|
160
|
+
|
|
161
|
+
|
|
116
162
|
def run_command(args: list[str], registry: bool = True) -> tuple[str, str]:
|
|
117
163
|
"""One command, in-process, as the terminal would run it: the text it
|
|
118
164
|
printed, and a status an agent can branch on without parsing prose --
|
|
@@ -123,7 +169,8 @@ def run_command(args: list[str], registry: bool = True) -> tuple[str, str]:
|
|
|
123
169
|
from fde.cli import app
|
|
124
170
|
|
|
125
171
|
argv = [*args, "--registry", _registry()] if registry else list(args)
|
|
126
|
-
|
|
172
|
+
with _ONE_AT_A_TIME:
|
|
173
|
+
result = CliRunner().invoke(app, argv)
|
|
127
174
|
text = (result.output or "").strip()
|
|
128
175
|
if result.exception and not isinstance(result.exception, SystemExit):
|
|
129
176
|
return (f"{text}\n\nfde: {type(result.exception).__name__}: {result.exception}".strip(),
|
|
@@ -211,14 +258,14 @@ def build_server():
|
|
|
211
258
|
"""WRITES: reads a brief, pasted as text, into typed facts appended to
|
|
212
259
|
the engagement's record, played back for correction with the
|
|
213
260
|
follow-up questions. The engagement must be inside the workspace."""
|
|
214
|
-
return run_command(["frame", str(
|
|
261
|
+
return run_command(["frame", str(locate(engagement)), "--text", text])
|
|
215
262
|
|
|
216
263
|
@server.tool(annotations=reads)
|
|
217
264
|
@guarded
|
|
218
265
|
def status(engagement: str):
|
|
219
266
|
"""The engagement's gates, what is known by scope and who said it, what
|
|
220
267
|
is still missing, and which roles have not been heard."""
|
|
221
|
-
return run_command(["status", str(
|
|
268
|
+
return run_command(["status", str(locate(engagement))])
|
|
222
269
|
|
|
223
270
|
@server.tool(annotations=reads)
|
|
224
271
|
@guarded
|
|
@@ -227,7 +274,7 @@ def build_server():
|
|
|
227
274
|
and -- when it is a question -- the evidence already on record and
|
|
228
275
|
the decisions that turn on each candidate answer. Reads only; the
|
|
229
276
|
action it names is for a person to run."""
|
|
230
|
-
return run_command(["next", str(
|
|
277
|
+
return run_command(["next", str(locate(engagement))])
|
|
231
278
|
|
|
232
279
|
@server.tool(annotations=reads)
|
|
233
280
|
@guarded
|
|
@@ -236,7 +283,7 @@ def build_server():
|
|
|
236
283
|
validation, prototype, pilot, production, adoption, retrospective,
|
|
237
284
|
or stopped -- with what would move it to the next stage. Reads only:
|
|
238
285
|
a person runs `fde stage` to append the transition to the record."""
|
|
239
|
-
args = ["stage", str(
|
|
286
|
+
args = ["stage", str(locate(engagement)), "--no-record"]
|
|
240
287
|
target = confine(project, "project")
|
|
241
288
|
return run_command(args + (["--project", str(target)] if target else []))
|
|
242
289
|
|
|
@@ -247,7 +294,7 @@ def build_server():
|
|
|
247
294
|
settled -- failing gates, standing waivers, guessed or merely stated
|
|
248
295
|
facts, disagreements, unsigned attestations, unheard roles, open
|
|
249
296
|
incidents, undecided components -- each with an owner and an age."""
|
|
250
|
-
return run_command(["debt", str(
|
|
297
|
+
return run_command(["debt", str(locate(engagement))])
|
|
251
298
|
|
|
252
299
|
@server.tool(annotations=reads)
|
|
253
300
|
@guarded
|
|
@@ -256,7 +303,7 @@ def build_server():
|
|
|
256
303
|
"""Judge the engagement's stop conditions against what the record has
|
|
257
304
|
measured: the scorecard, the field journal, the recorded outcomes.
|
|
258
305
|
Reads only; recording a condition is for a person."""
|
|
259
|
-
args = ["stop-when", str(
|
|
306
|
+
args = ["stop-when", str(locate(engagement))]
|
|
260
307
|
target = confine(project, "project")
|
|
261
308
|
if target:
|
|
262
309
|
args += ["--project", str(target)]
|
|
@@ -270,7 +317,7 @@ def build_server():
|
|
|
270
317
|
def history(engagement: str):
|
|
271
318
|
"""Every dated entry on the engagement's record, in order, the undated
|
|
272
319
|
ones above it, one line each."""
|
|
273
|
-
return run_command(["history", str(
|
|
320
|
+
return run_command(["history", str(locate(engagement))],
|
|
274
321
|
registry=False)
|
|
275
322
|
|
|
276
323
|
@server.resource("fde://thirty-seconds", name="fde in thirty seconds",
|
|
@@ -293,6 +340,7 @@ def describe() -> str:
|
|
|
293
340
|
else "reads"
|
|
294
341
|
lines.append(f" {tool.name:10} {hint}")
|
|
295
342
|
lines.append(" resources: fde://thirty-seconds")
|
|
343
|
+
lines.append(f" envelope: schema_version {SCHEMA_VERSION} (`fde mcp --schema`)")
|
|
296
344
|
return "\n".join(lines)
|
|
297
345
|
|
|
298
346
|
|
|
@@ -224,10 +224,11 @@ def test_check_mode_creates_nothing(tmp_path, monkeypatch):
|
|
|
224
224
|
|
|
225
225
|
|
|
226
226
|
def test_the_envelope_is_versioned_and_every_status_is_reachable(tmp_path, monkeypatch):
|
|
227
|
+
import jsonschema
|
|
228
|
+
|
|
227
229
|
from fde import mcp_server
|
|
228
|
-
from fde.mcp_server import
|
|
230
|
+
from fde.mcp_server import ENVELOPE_SCHEMA, SCHEMA_VERSION, STATUSES, reply, run_command
|
|
229
231
|
|
|
230
|
-
assert set(ENVELOPE) == {"schema_version", "tool", "status", "mutated", "workspace"}
|
|
231
232
|
monkeypatch.setenv("FDE_WORKSPACE", str(tmp_path))
|
|
232
233
|
server = build_server()
|
|
233
234
|
seen = {}
|
|
@@ -243,7 +244,7 @@ def test_the_envelope_is_versioned_and_every_status_is_reachable(tmp_path, monke
|
|
|
243
244
|
for status in STATUSES:
|
|
244
245
|
assert seen[status]["status"] == status
|
|
245
246
|
assert seen[status]["schema_version"] == SCHEMA_VERSION
|
|
246
|
-
|
|
247
|
+
jsonschema.validate(seen[status], ENVELOPE_SCHEMA)
|
|
247
248
|
|
|
248
249
|
class Broken:
|
|
249
250
|
output = "half-written"
|
|
@@ -270,3 +271,85 @@ def test_doctor_measures_the_install_and_fails_only_on_what_the_core_needs(tmp_p
|
|
|
270
271
|
sick = runner.invoke(app, ["doctor", "--registry", str(tmp_path / "nowhere")])
|
|
271
272
|
assert sick.exit_code == 1 and "NO registry" in sick.output
|
|
272
273
|
assert "something the core needs is missing" in sick.output
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
# --- the interface as a product: the second call finds the first call's work
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def test_an_engagement_is_found_by_the_name_start_gave_it(tmp_path, monkeypatch):
|
|
280
|
+
"""`start(name="acme")` then `status(engagement="acme")` is the sequence
|
|
281
|
+
an agent writes first. Before this test the second call looked at
|
|
282
|
+
<workspace>/acme, found nothing, and said so; the record was at
|
|
283
|
+
engagements/acme, where `start` had put it and where `fde status acme`
|
|
284
|
+
finds it on the command line."""
|
|
285
|
+
monkeypatch.setenv("FDE_WORKSPACE", str(tmp_path))
|
|
286
|
+
server = build_server()
|
|
287
|
+
opened = asyncio.run(server.call_tool("start", {
|
|
288
|
+
"name": "acme", "statement": "Route each support message to one of 77 queues."}))
|
|
289
|
+
assert opened.structured_content["status"] == "ok"
|
|
290
|
+
assert (tmp_path / "engagements" / "acme" / "facts").is_dir()
|
|
291
|
+
for tool, expected in (("status", "blocked by"), ("next", "next: fde "),
|
|
292
|
+
("stage", "acme: discovery"), ("debt", "decision debt as of"),
|
|
293
|
+
("history", "acme: history"),
|
|
294
|
+
("stop_when", "no stop conditions on record")):
|
|
295
|
+
result = asyncio.run(server.call_tool(tool, {"engagement": "acme"}))
|
|
296
|
+
assert expected in result.content[0].text, (tool, result.content[0].text)
|
|
297
|
+
assert result.structured_content["status"] == "ok", (tool, result.structured_content)
|
|
298
|
+
framed = asyncio.run(server.call_tool("frame", {
|
|
299
|
+
"engagement": "acme",
|
|
300
|
+
"text": "Two agents triage 9,000 emails a month by hand and misroute about 12 percent."}))
|
|
301
|
+
assert framed.structured_content["status"] == "ok" and framed.structured_content["mutated"]
|
|
302
|
+
# The path form still works, and a name that climbs is still refused
|
|
303
|
+
# before anything is looked for.
|
|
304
|
+
by_path = asyncio.run(server.call_tool("status", {"engagement": "engagements/acme"}))
|
|
305
|
+
assert by_path.structured_content["status"] == "ok"
|
|
306
|
+
climbing = asyncio.run(server.call_tool("status", {"engagement": "../acme"}))
|
|
307
|
+
assert climbing.structured_content["status"] == "refused"
|
|
308
|
+
assert "outside the workspace" in climbing.content[0].text
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def test_a_corrupt_record_is_a_sentence_naming_the_file_not_a_traceback(tmp_path, monkeypatch):
|
|
312
|
+
monkeypatch.setenv("FDE_WORKSPACE", str(tmp_path))
|
|
313
|
+
server = build_server()
|
|
314
|
+
asyncio.run(server.call_tool("start", {
|
|
315
|
+
"name": "acme", "statement": "Route each support message to one of 77 queues."}))
|
|
316
|
+
asyncio.run(server.call_tool("frame", {
|
|
317
|
+
"engagement": "acme", "text": "Two agents triage 9,000 emails a month by hand."}))
|
|
318
|
+
record = next((tmp_path / "engagements" / "acme" / "facts").glob("*.yaml"))
|
|
319
|
+
with record.open("a") as fh:
|
|
320
|
+
fh.write('{"this is": not yaml\n')
|
|
321
|
+
for tool in ("status", "next", "debt"):
|
|
322
|
+
result = asyncio.run(server.call_tool(tool, {"engagement": "acme"}))
|
|
323
|
+
text = result.content[0].text
|
|
324
|
+
assert result.structured_content["status"] != "ok", tool
|
|
325
|
+
assert record.stem in text and "cannot read" in text, (tool, text)
|
|
326
|
+
assert "Traceback" not in text, (tool, text)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def test_the_schema_is_published_and_is_what_every_reply_follows(tmp_path, monkeypatch):
|
|
330
|
+
import json
|
|
331
|
+
|
|
332
|
+
import jsonschema
|
|
333
|
+
from typer.testing import CliRunner
|
|
334
|
+
|
|
335
|
+
from fde.cli import app
|
|
336
|
+
from fde.mcp_server import ENVELOPE_SCHEMA, SCHEMA_VERSION, TOOLS
|
|
337
|
+
|
|
338
|
+
result = CliRunner().invoke(app, ["mcp", "--schema"])
|
|
339
|
+
assert result.exit_code == 0, result.output
|
|
340
|
+
published = json.loads(result.output)
|
|
341
|
+
assert published == ENVELOPE_SCHEMA
|
|
342
|
+
jsonschema.Draft202012Validator.check_schema(published)
|
|
343
|
+
assert published["$id"].endswith(f"/v{SCHEMA_VERSION}")
|
|
344
|
+
# The schema's list of tools is the server's list of tools.
|
|
345
|
+
monkeypatch.setenv("FDE_WORKSPACE", str(tmp_path))
|
|
346
|
+
server = build_server()
|
|
347
|
+
assert {t.name for t in asyncio.run(server.list_tools())} == set(TOOLS)
|
|
348
|
+
# And an envelope with a field it does not name, or a status it does not
|
|
349
|
+
# name, fails validation: a client can trust the schema to be the contract.
|
|
350
|
+
good = asyncio.run(server.call_tool("triage", {"statements": ["Route."]})).structured_content
|
|
351
|
+
jsonschema.validate(good, published)
|
|
352
|
+
with pytest.raises(jsonschema.ValidationError):
|
|
353
|
+
jsonschema.validate({**good, "extra": 1}, published)
|
|
354
|
+
with pytest.raises(jsonschema.ValidationError):
|
|
355
|
+
jsonschema.validate({**good, "status": "done"}, published)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""The server as a client sees it: a separate process over stdio, driven by
|
|
2
|
+
the official MCP client, the way a coding agent drives it. The in-process
|
|
3
|
+
tests check each tool; these check the transport under what agents
|
|
4
|
+
actually do -- several calls at once, a call abandoned midway, a record
|
|
5
|
+
that changed on disk between calls."""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
pytest.importorskip("mcp", reason="the MCP extra is not installed")
|
|
16
|
+
|
|
17
|
+
from mcp import ClientSession, StdioServerParameters # noqa: E402
|
|
18
|
+
from mcp.client.stdio import stdio_client # noqa: E402
|
|
19
|
+
|
|
20
|
+
from fde.mcp_server import TOOLS # noqa: E402
|
|
21
|
+
|
|
22
|
+
STATEMENT = "Route each support message to one of 77 queues."
|
|
23
|
+
PATIENCE = 90 # seconds; a regression hangs the server, and the test must fail, not hang
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def serve_in(workspace):
|
|
27
|
+
return StdioServerParameters(command=sys.executable, args=["-m", "fde.cli", "mcp"],
|
|
28
|
+
env={**os.environ, "FDE_WORKSPACE": str(workspace)})
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def drive(workspace, scenario):
|
|
32
|
+
async def main():
|
|
33
|
+
async with stdio_client(serve_in(workspace)) as (read, write):
|
|
34
|
+
async with ClientSession(read, write) as session:
|
|
35
|
+
await asyncio.wait_for(session.initialize(), PATIENCE)
|
|
36
|
+
return await asyncio.wait_for(scenario(session), PATIENCE)
|
|
37
|
+
return asyncio.run(main())
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_a_real_client_lists_the_tools_with_their_input_schemas_and_walks_by_name(tmp_path):
|
|
41
|
+
async def scenario(session):
|
|
42
|
+
listed = await session.list_tools()
|
|
43
|
+
schemas = {t.name: t.input_schema for t in listed.tools}
|
|
44
|
+
opened = await session.call_tool("start", {"name": "acme", "statement": STATEMENT})
|
|
45
|
+
status = await session.call_tool("status", {"engagement": "acme"})
|
|
46
|
+
return schemas, opened, status
|
|
47
|
+
|
|
48
|
+
schemas, opened, status = drive(tmp_path, scenario)
|
|
49
|
+
assert set(schemas) == set(TOOLS)
|
|
50
|
+
for name, schema in schemas.items():
|
|
51
|
+
assert schema["type"] == "object" and schema["properties"], name
|
|
52
|
+
if name not in ("triage", "start"):
|
|
53
|
+
assert "engagement" in schema["required"], name
|
|
54
|
+
assert opened.structured_content["status"] == "ok" and opened.structured_content["mutated"]
|
|
55
|
+
assert status.structured_content["status"] == "ok"
|
|
56
|
+
assert "blocked by" in status.content[0].text
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_calls_issued_at_once_all_come_back(tmp_path):
|
|
60
|
+
"""Claude Code issued `status` and `next` in the same breath and the
|
|
61
|
+
second came back "Connection closed": the server ran them on two
|
|
62
|
+
threads, and the output capture that each swaps in leaked one
|
|
63
|
+
command's text onto the JSON-RPC channel. Commands are now one at a
|
|
64
|
+
time inside the server."""
|
|
65
|
+
async def scenario(session):
|
|
66
|
+
await session.call_tool("start", {"name": "acme", "statement": STATEMENT})
|
|
67
|
+
rounds = []
|
|
68
|
+
for _ in range(3):
|
|
69
|
+
replies = await asyncio.gather(
|
|
70
|
+
*(session.call_tool(tool, {"engagement": "acme"})
|
|
71
|
+
for tool in ("status", "next", "debt", "stage", "history", "stop_when")),
|
|
72
|
+
return_exceptions=True)
|
|
73
|
+
rounds.append(replies)
|
|
74
|
+
after = await session.call_tool("history", {"engagement": "acme"})
|
|
75
|
+
return rounds, after
|
|
76
|
+
|
|
77
|
+
rounds, after = drive(tmp_path, scenario)
|
|
78
|
+
for replies in rounds:
|
|
79
|
+
for reply in replies:
|
|
80
|
+
assert not isinstance(reply, BaseException), repr(reply)
|
|
81
|
+
assert reply.structured_content["status"] == "ok", reply.content[0].text
|
|
82
|
+
assert reply.content[0].text.strip(), reply.structured_content
|
|
83
|
+
assert after.structured_content["status"] == "ok"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_a_call_abandoned_midway_leaves_the_server_answering(tmp_path):
|
|
87
|
+
async def scenario(session):
|
|
88
|
+
await session.call_tool("start", {"name": "acme", "statement": STATEMENT})
|
|
89
|
+
abandoned = asyncio.ensure_future(session.call_tool("next", {"engagement": "acme"}))
|
|
90
|
+
await asyncio.sleep(0)
|
|
91
|
+
abandoned.cancel()
|
|
92
|
+
try:
|
|
93
|
+
await abandoned
|
|
94
|
+
except asyncio.CancelledError:
|
|
95
|
+
pass
|
|
96
|
+
return await session.call_tool("status", {"engagement": "acme"})
|
|
97
|
+
|
|
98
|
+
status = drive(tmp_path, scenario)
|
|
99
|
+
assert status.structured_content["status"] == "ok"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_the_record_is_read_fresh_so_a_change_on_disk_shows_in_the_next_call(tmp_path):
|
|
103
|
+
from typer.testing import CliRunner
|
|
104
|
+
|
|
105
|
+
from fde.cli import app
|
|
106
|
+
|
|
107
|
+
async def scenario(session):
|
|
108
|
+
await session.call_tool("start", {"name": "acme", "statement": STATEMENT})
|
|
109
|
+
before = await session.call_tool("history", {"engagement": "acme"})
|
|
110
|
+
# Someone at the terminal frames a brief while the server is up.
|
|
111
|
+
result = CliRunner().invoke(app, ["frame", str(tmp_path / "engagements" / "acme"),
|
|
112
|
+
"--text", "Two agents triage 9,000 emails a month "
|
|
113
|
+
"by hand and misroute about 12 percent."])
|
|
114
|
+
assert result.exit_code == 0, result.output
|
|
115
|
+
after = await session.call_tool("history", {"engagement": "acme"})
|
|
116
|
+
return before.content[0].text, after.content[0].text
|
|
117
|
+
|
|
118
|
+
before, after = drive(tmp_path, scenario)
|
|
119
|
+
assert "0002-frame" not in before and "0002-frame" in after
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""One version, in every place that states it. Two tags this week had to
|
|
2
|
+
be moved after the fact; this fails before the tag instead."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
import tomllib
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
REPO = Path(__file__).resolve().parent.parent
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_pyproject_citation_and_changelog_agree_on_the_version():
|
|
14
|
+
version = tomllib.loads((REPO / "pyproject.toml").read_text())["project"]["version"]
|
|
15
|
+
citation = re.search(r"^version: (\S+)$", (REPO / "CITATION.cff").read_text(), re.M)
|
|
16
|
+
assert citation and citation.group(1) == version, "CITATION.cff lags pyproject.toml"
|
|
17
|
+
latest = re.search(r"^## \[(\d+\.\d+\.\d+)\]", (REPO / "CHANGELOG.md").read_text(), re.M)
|
|
18
|
+
assert latest and latest.group(1) == version, "CHANGELOG.md's newest entry lags the version"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_citation_and_changelog_agree_on_the_date_and_it_is_not_ahead_of_today():
|
|
22
|
+
"""0.1.44 shipped on October 11 dated October 12, in both files, and an
|
|
23
|
+
outside reader noticed before I did. The two files must agree, and the
|
|
24
|
+
date may not be in the future; the release workflow goes further and
|
|
25
|
+
holds it to the day the tagged commit was authored."""
|
|
26
|
+
from datetime import date, timedelta
|
|
27
|
+
|
|
28
|
+
changelog = re.search(r"^## \[\d+\.\d+\.\d+\] — (\d{4}-\d{2}-\d{2})",
|
|
29
|
+
(REPO / "CHANGELOG.md").read_text(), re.M)
|
|
30
|
+
assert changelog, "the newest CHANGELOG entry carries no date"
|
|
31
|
+
citation = re.search(r"^date-released: '(\d{4}-\d{2}-\d{2})'$",
|
|
32
|
+
(REPO / "CITATION.cff").read_text(), re.M)
|
|
33
|
+
assert citation and citation.group(1) == changelog.group(1), \
|
|
34
|
+
"CITATION.cff date-released != the newest CHANGELOG date"
|
|
35
|
+
released = date.fromisoformat(changelog.group(1))
|
|
36
|
+
# A day of slack: the author's calendar and the test runner's need not
|
|
37
|
+
# agree at midnight.
|
|
38
|
+
assert released <= date.today() + timedelta(days=1), f"{released} has not happened yet"
|