specfact-cli 0.46.17__tar.gz → 0.46.19__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.
Files changed (278) hide show
  1. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/.gitignore +1 -0
  2. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/PKG-INFO +1 -1
  3. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/pyproject.toml +2 -1
  4. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/__init__.py +1 -1
  5. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/__init__.py +1 -1
  6. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/cli.py +14 -2
  7. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/modules/init/module-package.yaml +3 -3
  8. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/modules/init/src/commands.py +29 -3
  9. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/modules/module_registry/module-package.yaml +3 -3
  10. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/modules/module_registry/src/commands.py +4 -0
  11. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/registry/module_availability.py +11 -1
  12. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/registry/module_packages.py +137 -10
  13. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/utils/env_manager.py +157 -48
  14. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/utils/ide_setup.py +5 -1
  15. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/LICENSE +0 -0
  16. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/README.md +0 -0
  17. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/resources/bundled-module-registry/index.json +0 -0
  18. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/resources/keys/README.md +0 -0
  19. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/resources/keys/module-signing-public.pem +0 -0
  20. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/resources/mappings/node-async.yaml +0 -0
  21. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/resources/mappings/python-async.yaml +0 -0
  22. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/resources/mappings/speckit-default.yaml +0 -0
  23. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/resources/schemas/deviation.schema.json +0 -0
  24. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/resources/schemas/plan.schema.json +0 -0
  25. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/resources/schemas/protocol.schema.json +0 -0
  26. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/resources/templates/github-action.yml.j2 +0 -0
  27. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/resources/templates/persona/architect.md.j2 +0 -0
  28. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/resources/templates/persona/developer.md.j2 +0 -0
  29. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/resources/templates/persona/product-owner.md.j2 +0 -0
  30. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/resources/templates/plan.bundle.yaml.j2 +0 -0
  31. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/resources/templates/policies/kanban.yaml +0 -0
  32. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/resources/templates/policies/mixed.yaml +0 -0
  33. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/resources/templates/policies/safe.yaml +0 -0
  34. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/resources/templates/policies/scrum.yaml +0 -0
  35. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/resources/templates/pr-template.md.j2 +0 -0
  36. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/resources/templates/protocol.yaml.j2 +0 -0
  37. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/resources/templates/telemetry.yaml.example +0 -0
  38. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/__main__.py +0 -0
  39. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/adapters/__init__.py +0 -0
  40. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/adapters/ado.py +0 -0
  41. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/adapters/backlog_base.py +0 -0
  42. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/adapters/base.py +0 -0
  43. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/adapters/github.py +0 -0
  44. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/adapters/openspec.py +0 -0
  45. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/adapters/openspec_parser.py +0 -0
  46. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/adapters/registry.py +0 -0
  47. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/adapters/speckit.py +0 -0
  48. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/agents/__init__.py +0 -0
  49. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/agents/analyze_agent.py +0 -0
  50. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/agents/base.py +0 -0
  51. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/agents/plan_agent.py +0 -0
  52. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/agents/registry.py +0 -0
  53. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/agents/sync_agent.py +0 -0
  54. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/analyzers/__init__.py +0 -0
  55. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/analyzers/ambiguity_scanner.py +0 -0
  56. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/analyzers/code_analyzer.py +0 -0
  57. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/analyzers/constitution_evidence_extractor.py +0 -0
  58. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/analyzers/contract_extractor.py +0 -0
  59. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/analyzers/control_flow_analyzer.py +0 -0
  60. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/analyzers/graph_analyzer.py +0 -0
  61. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/analyzers/relationship_mapper.py +0 -0
  62. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/analyzers/requirement_extractor.py +0 -0
  63. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/analyzers/test_pattern_extractor.py +0 -0
  64. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/backlog/__init__.py +0 -0
  65. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/backlog/adapters/__init__.py +0 -0
  66. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/backlog/adapters/base.py +0 -0
  67. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/backlog/converter.py +0 -0
  68. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/backlog/filters.py +0 -0
  69. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/backlog/mappers/__init__.py +0 -0
  70. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/backlog/mappers/ado_mapper.py +0 -0
  71. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/backlog/mappers/base.py +0 -0
  72. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/backlog/mappers/github_mapper.py +0 -0
  73. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/backlog/mappers/template_config.py +0 -0
  74. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/commands/__init__.py +0 -0
  75. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/commands/_bundle_shim.py +0 -0
  76. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/commands/analyze.py +0 -0
  77. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/commands/contract_cmd.py +0 -0
  78. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/commands/drift.py +0 -0
  79. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/commands/enforce.py +0 -0
  80. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/commands/generate.py +0 -0
  81. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/commands/import_cmd.py +0 -0
  82. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/commands/init.py +0 -0
  83. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/commands/migrate.py +0 -0
  84. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/commands/plan.py +0 -0
  85. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/commands/project_cmd.py +0 -0
  86. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/commands/repro.py +0 -0
  87. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/commands/sdd.py +0 -0
  88. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/commands/spec.py +0 -0
  89. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/commands/sync.py +0 -0
  90. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/commands/update.py +0 -0
  91. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/commands/validate.py +0 -0
  92. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/common/__init__.py +0 -0
  93. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/common/bundle_factory.py +0 -0
  94. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/common/logger_setup.py +0 -0
  95. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/common/logging_utils.py +0 -0
  96. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/common/text_utils.py +0 -0
  97. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/common/utils.py +0 -0
  98. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/comparators/__init__.py +0 -0
  99. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/comparators/plan_comparator.py +0 -0
  100. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/contracts/__init__.py +0 -0
  101. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/contracts/crosshair_props.py +0 -0
  102. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/contracts/module_interface.py +0 -0
  103. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/enrichers/constitution_enricher.py +0 -0
  104. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/enrichers/plan_enricher.py +0 -0
  105. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/generators/__init__.py +0 -0
  106. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/generators/contract_generator.py +0 -0
  107. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/generators/openapi_extractor.py +0 -0
  108. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/generators/persona_exporter.py +0 -0
  109. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/generators/plan_generator.py +0 -0
  110. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/generators/protocol_generator.py +0 -0
  111. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/generators/report_generator.py +0 -0
  112. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/generators/task_generator.py +0 -0
  113. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/generators/test_to_openapi.py +0 -0
  114. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/generators/workflow_generator.py +0 -0
  115. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/groups/__init__.py +0 -0
  116. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/groups/codebase_group.py +0 -0
  117. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/groups/govern_group.py +0 -0
  118. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/groups/member_group.py +0 -0
  119. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/groups/project_group.py +0 -0
  120. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/groups/spec_group.py +0 -0
  121. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/importers/__init__.py +0 -0
  122. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/importers/speckit_converter.py +0 -0
  123. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/importers/speckit_scanner.py +0 -0
  124. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/integrations/__init__.py +0 -0
  125. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/integrations/specmatic.py +0 -0
  126. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/merge/__init__.py +0 -0
  127. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/merge/resolver.py +0 -0
  128. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/migrations/__init__.py +0 -0
  129. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/migrations/plan_migrator.py +0 -0
  130. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/models/__init__.py +0 -0
  131. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/models/backlog_item.py +0 -0
  132. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/models/bridge.py +0 -0
  133. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/models/capabilities.py +0 -0
  134. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/models/change.py +0 -0
  135. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/models/contract.py +0 -0
  136. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/models/deviation.py +0 -0
  137. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/models/dor_config.py +0 -0
  138. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/models/enforcement.py +0 -0
  139. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/models/module_package.py +0 -0
  140. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/models/persona_template.py +0 -0
  141. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/models/plan.py +0 -0
  142. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/models/project.py +0 -0
  143. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/models/protocol.py +0 -0
  144. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/models/quality.py +0 -0
  145. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/models/sdd.py +0 -0
  146. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/models/source_tracking.py +0 -0
  147. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/models/task.py +0 -0
  148. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/models/validation.py +0 -0
  149. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/modes/__init__.py +0 -0
  150. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/modes/detector.py +0 -0
  151. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/modes/router.py +0 -0
  152. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/modules/__init__.py +0 -0
  153. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/modules/_bundle_import.py +0 -0
  154. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/modules/init/src/__init__.py +0 -0
  155. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/modules/init/src/app.py +0 -0
  156. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/modules/init/src/first_run_selection.py +0 -0
  157. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/modules/module_io_shim.py +0 -0
  158. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/modules/module_registry/src/__init__.py +0 -0
  159. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/modules/module_registry/src/app.py +0 -0
  160. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/modules/upgrade/module-package.yaml +0 -0
  161. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/modules/upgrade/src/__init__.py +0 -0
  162. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/modules/upgrade/src/app.py +0 -0
  163. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/modules/upgrade/src/commands.py +0 -0
  164. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/parsers/__init__.py +0 -0
  165. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/parsers/persona_importer.py +0 -0
  166. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/registry/__init__.py +0 -0
  167. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/registry/alias_manager.py +0 -0
  168. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/registry/bootstrap.py +0 -0
  169. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/registry/bridge_registry.py +0 -0
  170. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/registry/crypto_validator.py +0 -0
  171. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/registry/custom_registries.py +0 -0
  172. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/registry/dependency_resolver.py +0 -0
  173. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/registry/extension_registry.py +0 -0
  174. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/registry/help_cache.py +0 -0
  175. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/registry/marketplace_client.py +0 -0
  176. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/registry/metadata.py +0 -0
  177. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/registry/module_discovery.py +0 -0
  178. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/registry/module_grouping.py +0 -0
  179. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/registry/module_installer.py +0 -0
  180. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/registry/module_lifecycle.py +0 -0
  181. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/registry/module_security.py +0 -0
  182. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/registry/module_state.py +0 -0
  183. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/registry/registry.py +0 -0
  184. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/resources/semgrep/async.yml +0 -0
  185. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/resources/semgrep/code-quality.yml +0 -0
  186. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/resources/semgrep/feature-detection.yml +0 -0
  187. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/runtime.py +0 -0
  188. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/sync/__init__.py +0 -0
  189. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/sync/bridge_probe.py +0 -0
  190. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/sync/bridge_sync.py +0 -0
  191. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/sync/bridge_sync_openspec_md_parse.py +0 -0
  192. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/sync/bridge_sync_requirement_from_proposal.py +0 -0
  193. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/sync/bridge_sync_requirement_helpers.py +0 -0
  194. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/sync/bridge_sync_tasks_from_proposal.py +0 -0
  195. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/sync/bridge_sync_what_changes_format.py +0 -0
  196. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/sync/bridge_sync_write_openspec_from_proposal.py +0 -0
  197. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/sync/bridge_watch.py +0 -0
  198. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/sync/change_detector.py +0 -0
  199. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/sync/code_to_spec.py +0 -0
  200. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/sync/drift_detector.py +0 -0
  201. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/sync/repository_sync.py +0 -0
  202. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/sync/spec_to_code.py +0 -0
  203. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/sync/spec_to_tests.py +0 -0
  204. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/sync/watcher.py +0 -0
  205. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/sync/watcher_enhanced.py +0 -0
  206. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/telemetry.py +0 -0
  207. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/templates/__init__.py +0 -0
  208. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/templates/defaults/defect_v1.yaml +0 -0
  209. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/templates/defaults/enabler_v1.yaml +0 -0
  210. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/templates/defaults/spike_v1.yaml +0 -0
  211. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/templates/defaults/user_story_v1.yaml +0 -0
  212. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/templates/frameworks/scrum/user_story_v1.yaml +0 -0
  213. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/templates/personas/product-owner/user_story_v1.yaml +0 -0
  214. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/templates/providers/ado/work_item_v1.yaml +0 -0
  215. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/templates/registry.py +0 -0
  216. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/templates/specification_templates.py +0 -0
  217. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/utils/__init__.py +0 -0
  218. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/utils/acceptance_criteria.py +0 -0
  219. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/utils/auth_tokens.py +0 -0
  220. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/utils/bundle_converters.py +0 -0
  221. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/utils/bundle_loader.py +0 -0
  222. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/utils/code_change_detector.py +0 -0
  223. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/utils/console.py +0 -0
  224. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/utils/content_sanitizer.py +0 -0
  225. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/utils/context_detection.py +0 -0
  226. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/utils/contract_predicates.py +0 -0
  227. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/utils/enrichment_context.py +0 -0
  228. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/utils/enrichment_parser.py +0 -0
  229. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/utils/feature_keys.py +0 -0
  230. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/utils/git.py +0 -0
  231. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/utils/github_annotations.py +0 -0
  232. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/utils/icontract_helpers.py +0 -0
  233. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/utils/incremental_check.py +0 -0
  234. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/utils/metadata.py +0 -0
  235. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/utils/optional_deps.py +0 -0
  236. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/utils/performance.py +0 -0
  237. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/utils/persona_ownership.py +0 -0
  238. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/utils/progress.py +0 -0
  239. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/utils/progressive_disclosure.py +0 -0
  240. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/utils/project_artifact_write.py +0 -0
  241. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/utils/prompts.py +0 -0
  242. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/utils/sdd_discovery.py +0 -0
  243. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/utils/source_scanner.py +0 -0
  244. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/utils/startup_checks.py +0 -0
  245. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/utils/structure.py +0 -0
  246. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/utils/structured_io.py +0 -0
  247. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/utils/suggestions.py +0 -0
  248. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/utils/terminal.py +0 -0
  249. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/utils/yaml_utils.py +0 -0
  250. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/validation/__init__.py +0 -0
  251. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/validation/command_audit.py +0 -0
  252. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/validators/__init__.py +0 -0
  253. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/validators/agile_validation.py +0 -0
  254. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/validators/change_proposal_integration.py +0 -0
  255. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/validators/cli_first_validator.py +0 -0
  256. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/validators/contract_validator.py +0 -0
  257. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/validators/fsm.py +0 -0
  258. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/validators/repro_checker.py +0 -0
  259. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/validators/schema.py +0 -0
  260. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/validators/sidecar/__init__.py +0 -0
  261. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/validators/sidecar/contract_populator.py +0 -0
  262. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/validators/sidecar/crosshair_runner.py +0 -0
  263. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/validators/sidecar/crosshair_summary.py +0 -0
  264. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/validators/sidecar/dependency_installer.py +0 -0
  265. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/validators/sidecar/framework_detector.py +0 -0
  266. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/validators/sidecar/frameworks/__init__.py +0 -0
  267. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/validators/sidecar/frameworks/base.py +0 -0
  268. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/validators/sidecar/frameworks/django.py +0 -0
  269. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/validators/sidecar/frameworks/drf.py +0 -0
  270. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/validators/sidecar/frameworks/fastapi.py +0 -0
  271. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/validators/sidecar/frameworks/flask.py +0 -0
  272. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/validators/sidecar/harness_generator.py +0 -0
  273. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/validators/sidecar/models.py +0 -0
  274. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/validators/sidecar/orchestrator.py +0 -0
  275. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/validators/sidecar/specmatic_runner.py +0 -0
  276. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/validators/sidecar/unannotated_detector.py +0 -0
  277. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/versioning/__init__.py +0 -0
  278. {specfact_cli-0.46.17 → specfact_cli-0.46.19}/src/specfact_cli/versioning/analyzer.py +0 -0
@@ -113,6 +113,7 @@ docs/internal/
113
113
  .claude/commands/specfact.*.md
114
114
  .claude/skills/openspec-*/
115
115
  !.claude/skills/openspec-workflows/
116
+ .claude/settings.json
116
117
 
117
118
  .codex/skills/openspec-*/
118
119
  !.codex/skills/openspec-workflows/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: specfact-cli
3
- Version: 0.46.17
3
+ Version: 0.46.19
4
4
  Summary: The swiss knife CLI for agile DevOps teams. Keep backlog, specs, tests, and code in sync with validation and contract enforcement for new projects and long-lived codebases.
5
5
  Project-URL: Homepage, https://github.com/nold-ai/specfact-cli
6
6
  Project-URL: Repository, https://github.com/nold-ai/specfact-cli.git
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "specfact-cli"
7
- version = "0.46.17"
7
+ version = "0.46.19"
8
8
  description = "The swiss knife CLI for agile DevOps teams. Keep backlog, specs, tests, and code in sync with validation and contract enforcement for new projects and long-lived codebases."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -283,6 +283,7 @@ verify-removal-gate = [
283
283
  "hatch run verify-modules-signature",
284
284
  ]
285
285
  export-change-github = "python scripts/export-change-to-github.py {args}"
286
+ runtime-discovery-smoke = "python scripts/runtime_discovery_smoke.py {args}"
286
287
 
287
288
  # Contract-First Smart Test System Scripts
288
289
  contract-test = "python tools/contract_first_smart_test.py run --level auto {args}"
@@ -3,4 +3,4 @@ SpecFact CLI - Spec→Contract→Sentinel tool for contract-driven development.
3
3
  """
4
4
 
5
5
  # Package version: keep in sync with pyproject.toml, setup.py, src/specfact_cli/__init__.py
6
- __version__ = "0.46.17"
6
+ __version__ = "0.46.19"
@@ -45,6 +45,6 @@ def _bootstrap_bundle_paths() -> None:
45
45
 
46
46
  _bootstrap_bundle_paths()
47
47
 
48
- __version__ = "0.46.17"
48
+ __version__ = "0.46.19"
49
49
 
50
50
  __all__ = ["__version__"]
@@ -178,6 +178,11 @@ class _RootCLIGroup(ProgressiveDisclosureGroup):
178
178
  _print_missing_bundle_command_help(invoked)
179
179
  raise SystemExit(1) from None
180
180
  raise
181
+ except ValueError as exc:
182
+ if invoked in KNOWN_BUNDLE_GROUP_OR_SHIM_NAMES:
183
+ _print_missing_bundle_command_help(invoked)
184
+ raise SystemExit(1) from exc
185
+ raise
181
186
  _name, cmd, remaining = result
182
187
  if cmd is not None or not remaining:
183
188
  return result
@@ -610,6 +615,12 @@ def _strip_redundant_single_command_arg(click_cmd: click.Command, args: tuple[st
610
615
  return args_list
611
616
 
612
617
 
618
+ def _lazy_delegate_remaining_args(ctx: click.Context) -> list[str]:
619
+ ctx_state = vars(ctx)
620
+ protected_args = ctx_state.get("_protected_args") or ctx_state.get("protected_args") or ()
621
+ return [str(arg) for arg in (*protected_args, *ctx.args)]
622
+
623
+
613
624
  class _LazyDelegateGroup(click.Group):
614
625
  """Click Group that delegates all args to the real command (lazy-loaded)."""
615
626
 
@@ -657,9 +668,10 @@ class _LazyDelegateGroup(click.Group):
657
668
  @require(lambda ctx: ctx is not None, "ctx must not be None")
658
669
  @ensure(lambda result: result is None or isinstance(result, int), "result must be None or an exit code")
659
670
  def invoke(self, ctx: click.Context) -> Any:
660
- if ctx.invoked_subcommand is None and not ctx.args:
671
+ if ctx.invoked_subcommand is None:
672
+ args = _lazy_delegate_remaining_args(ctx)
661
673
  ctx.meta["original_prog_name"] = ctx.command_path
662
- return self._delegate_cmd.main(args=[], prog_name=ctx.command_path, standalone_mode=False)
674
+ return self._delegate_cmd.main(args=args, prog_name=ctx.command_path, standalone_mode=False)
663
675
  return super().invoke(ctx)
664
676
 
665
677
  @require(_lazy_delegate_cmd_name_ready, "lazy command name must be set")
@@ -1,5 +1,5 @@
1
1
  name: init
2
- version: 0.1.31
2
+ version: 0.1.33
3
3
  commands:
4
4
  - init
5
5
  category: core
@@ -17,5 +17,5 @@ publisher:
17
17
  description: Initialize SpecFact workspace and bootstrap local configuration.
18
18
  license: Apache-2.0
19
19
  integrity:
20
- checksum: sha256:0f7bc54a823bea14033fcb143ecb6c83d2bca2b5da661f03a0b545100acebe5b
21
- signature: dTatkqgBUtti4tL/pmcFBZY9bsJ61gY/V0lP9gZU8Y5W3YWK+wpgRx1oewlAmfKzkxca2NhalKcjLACQjTNvAA==
20
+ checksum: sha256:0086a4fa3e1f8744deee21640e73e6e2f24daf2b4a680b9157feed5487afc208
21
+ signature: L7DtVqvg+zqc/CgJu/mOkvxYpfH9HuddbLS3EK5Uvnf6K/j/BofqopamZDdHVlevOGAb+dbsk06kskNOIJfkDQ==
@@ -26,7 +26,13 @@ from specfact_cli.registry.module_state import write_modules_state
26
26
  from specfact_cli.runtime import debug_print, is_non_interactive
27
27
  from specfact_cli.telemetry import telemetry
28
28
  from specfact_cli.utils.contract_predicates import repo_path_exists, repo_path_is_dir
29
- from specfact_cli.utils.env_manager import EnvManager, EnvManagerInfo, build_tool_command, detect_env_manager
29
+ from specfact_cli.utils.env_manager import (
30
+ EnvManager,
31
+ EnvManagerInfo,
32
+ build_tool_command,
33
+ detect_env_manager,
34
+ env_info_from_tool_choice,
35
+ )
30
36
  from specfact_cli.utils.ide_setup import (
31
37
  IDE_CONFIG,
32
38
  PROMPT_SOURCE_CORE,
@@ -610,6 +616,11 @@ def init_ide(
610
616
  "--ide",
611
617
  help="IDE type (cursor, vscode, copilot, claude, gemini, qwen, opencode, windsurf, kilocode, auggie, roo, codebuddy, amp, q, auto)",
612
618
  ),
619
+ env_manager: EnvManager = typer.Option(
620
+ EnvManager.AUTO,
621
+ "--env-manager",
622
+ help="Environment manager override: auto, uv, hatch, poetry, or pip",
623
+ ),
613
624
  prompts: str | None = typer.Option(
614
625
  None,
615
626
  "--prompts",
@@ -638,7 +649,17 @@ def init_ide(
638
649
  console.print(f"[cyan]IDE:[/cyan] {ide_name} ({selected_ide})")
639
650
  console.print()
640
651
 
641
- env_info = detect_env_manager(repo_path)
652
+ env_info = (
653
+ detect_env_manager(repo_path)
654
+ if env_manager is EnvManager.AUTO
655
+ else env_info_from_tool_choice(env_manager, repo_path)
656
+ )
657
+ if env_manager is not EnvManager.AUTO and not env_info.available:
658
+ console.print(Panel(f"[bold red]{env_info.message}[/bold red]", border_style="red"))
659
+ raise typer.Exit(1)
660
+ if env_info.manager is not EnvManager.UNKNOWN:
661
+ console.print(f"[cyan]Environment manager:[/cyan] {env_info.manager.value}")
662
+ console.print()
642
663
  if env_info.manager == EnvManager.UNKNOWN:
643
664
  console.print(
644
665
  Panel(
@@ -673,7 +694,12 @@ def init_ide(
673
694
  copied_files, settings_path = copy_templates_to_ide(
674
695
  repo_path, selected_ide, force, prompts_by_source=selected_catalog
675
696
  )
676
- write_ide_prompt_export_state(repo_path, selected_ide, sorted(selected_catalog.keys()))
697
+ write_ide_prompt_export_state(
698
+ repo_path,
699
+ selected_ide,
700
+ sorted(selected_catalog.keys()),
701
+ env_manager=env_info.manager.value,
702
+ )
677
703
  _copy_backlog_field_mapping_templates(repo_path, force, console)
678
704
 
679
705
  console.print()
@@ -1,5 +1,5 @@
1
1
  name: module-registry
2
- version: 0.1.23
2
+ version: 0.1.24
3
3
  commands:
4
4
  - module
5
5
  category: core
@@ -17,5 +17,5 @@ publisher:
17
17
  description: 'Manage modules: search, list, show, install, and upgrade.'
18
18
  license: Apache-2.0
19
19
  integrity:
20
- checksum: sha256:f500281d2249d712be23a1b25b5660374694dfa47634f60ae4378eb2cdb753ca
21
- signature: cunuat95bD44IcNUBs35NTlHYOR8atydVz4ZyZTjIzwJL6Wz5YOfBYGq/WtgDnOFv3KplfvhRZbTo3CwmS/KBQ==
20
+ checksum: sha256:bc1293efa20676657f440f5c8f21583fcf7c5c2138d754e0262934417c92cfbb
21
+ signature: 7hALhs1hTjtnz3SdYmzKIqoniyvJDk5LyFcauv/wnVJRyZEQyPv42sb0Zk30zXAZ8QPE/g43/g4l78nv0AqNBQ==
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  import inspect
6
6
  import os
7
7
  import shutil
8
+ import tempfile
8
9
  from collections.abc import Callable, Iterator
9
10
  from contextlib import contextmanager
10
11
  from dataclasses import dataclass
@@ -187,7 +188,10 @@ def _normalize_project_repo(repo: Path | None) -> Path | None:
187
188
  if repo is None:
188
189
  return None
189
190
  repo_path = repo.resolve()
191
+ temp_root = Path(tempfile.gettempdir()).resolve()
190
192
  for candidate in [repo_path, *repo_path.parents]:
193
+ if candidate == temp_root and candidate != repo_path:
194
+ return repo_path
191
195
  if (candidate / ".git").exists():
192
196
  return candidate
193
197
  return repo_path
@@ -14,6 +14,7 @@ from specfact_cli.registry.module_discovery import DiscoveredModule, discover_al
14
14
  from specfact_cli.registry.module_packages import (
15
15
  _check_core_compatibility,
16
16
  _validate_module_dependencies,
17
+ get_module_load_failure_reason,
17
18
  merge_module_state,
18
19
  )
19
20
  from specfact_cli.registry.module_state import read_modules_state
@@ -104,6 +105,9 @@ def _recovery_command(status: ModuleAvailabilityStatus, module_id: str) -> str:
104
105
 
105
106
  def _skip_reason(entry: DiscoveredModule, enabled_map: dict[str, bool]) -> str:
106
107
  meta = entry.metadata
108
+ load_failure = get_module_load_failure_reason(meta.name, None)
109
+ if load_failure:
110
+ return load_failure
107
111
  if not _check_core_compatibility(meta, cli_version):
108
112
  return f"requires {meta.core_compatibility}, cli is {cli_version}"
109
113
  deps_ok, missing = _validate_module_dependencies(meta, enabled_map)
@@ -193,7 +197,13 @@ def classify_module_availability(
193
197
  command_name: str | None = None,
194
198
  base_path: Path | None = None,
195
199
  ) -> ModuleAvailability:
196
- """Classify module availability using manifests and modules.json only."""
200
+ """
201
+ Classify module availability from discovery state and process load failures.
202
+
203
+ Decisions use manifests/modules.json plus the process-scoped lazy-load failure
204
+ registry exposed by get_module_load_failure_reason and backed by
205
+ _MODULE_LOAD_FAILURES.
206
+ """
197
207
  discovered = discover_all_modules_for_project_with_shadowed(base_path)
198
208
  matches = _availability_matches(discovered, module_id=module_id, command_name=command_name)
199
209
  requested_id = module_id or command_name or ""
@@ -13,7 +13,12 @@ import ast
13
13
  import importlib
14
14
  import importlib.util
15
15
  import os
16
+ import re
17
+ import site
16
18
  import sys
19
+ import sysconfig
20
+ import tempfile
21
+ from contextlib import suppress
17
22
  from dataclasses import dataclass
18
23
  from pathlib import Path
19
24
  from typing import Any, cast
@@ -106,6 +111,8 @@ PROTOCOL_METHODS: dict[str, str] = {
106
111
  PROTOCOL_INTERFACE_BINDINGS: tuple[str, ...] = ("runtime_interface", "commands_interface", "commands")
107
112
  BRIDGE_REGISTRY = BridgeRegistry()
108
113
  BUILTIN_MODULES_ROOT = (Path(__file__).resolve().parents[1] / "modules").resolve()
114
+ _ACTIVE_MODULE_SRC_DIRS: list[Path] = []
115
+ _MODULE_LOAD_FAILURES: dict[tuple[str, str], str] = {}
109
116
 
110
117
 
111
118
  def _normalized_module_name(package_name: str) -> str:
@@ -141,6 +148,51 @@ def _is_builtin_module_package(package_dir: Path) -> bool:
141
148
  return False
142
149
 
143
150
 
151
+ def _is_under_directory(path: Path, parent: Path) -> bool:
152
+ try:
153
+ path.resolve().relative_to(parent.resolve())
154
+ return True
155
+ except ValueError:
156
+ return False
157
+
158
+
159
+ def _installed_package_search_roots() -> tuple[Path, ...]:
160
+ """Return interpreter-managed roots that can contain installed packages."""
161
+ roots: list[Path] = []
162
+ for key in ("purelib", "platlib"):
163
+ raw_path = sysconfig.get_paths().get(key)
164
+ if raw_path:
165
+ roots.append(Path(raw_path))
166
+ with suppress(AttributeError):
167
+ roots.extend(Path(path) for path in site.getsitepackages())
168
+ return tuple(dict.fromkeys(root.resolve() for root in roots if root.exists()))
169
+
170
+
171
+ def _has_installed_distribution_metadata(package_dir: Path) -> bool:
172
+ """Return True when package_dir has adjacent Python distribution metadata."""
173
+ parent = package_dir.parent
174
+ normalized = re.sub(r"[-_.]+", "_", package_dir.name).lower()
175
+ metadata_dirs = [*parent.glob("*.dist-info"), *parent.glob("*.egg-info")]
176
+ return any(re.sub(r"[-_.]+", "_", path.name).lower().startswith(f"{normalized}_") for path in metadata_dirs)
177
+
178
+
179
+ def _is_managed_specfact_module_package(package_dir: Path) -> bool:
180
+ """Return True for modules installed under a SpecFact-managed modules root."""
181
+ parts = package_dir.resolve().parts
182
+ return ".specfact" in parts and "modules" in parts
183
+
184
+
185
+ def _is_installed_module_package(package_dir: Path) -> bool:
186
+ """Return True when package_dir represents an installed package, not a source checkout."""
187
+ if _is_builtin_module_package(package_dir):
188
+ return False
189
+ if _is_managed_specfact_module_package(package_dir):
190
+ return True
191
+ if _has_installed_distribution_metadata(package_dir):
192
+ return True
193
+ return any(_is_under_directory(package_dir, root) for root in _installed_package_search_roots())
194
+
195
+
144
196
  @beartype
145
197
  @ensure(lambda result: isinstance(result, list), "Must return a list of paths")
146
198
  def get_modules_roots() -> list[Path]:
@@ -180,16 +232,16 @@ def get_modules_roots() -> list[Path]:
180
232
  def get_workspace_modules_root(base_path: Path | None = None) -> Path | None:
181
233
  """Return nearest workspace-local .specfact/modules root from base path upward."""
182
234
  start = base_path.resolve() if base_path is not None else Path.cwd().resolve()
235
+ temp_root = Path(tempfile.gettempdir()).resolve()
183
236
  for candidate in [start, *start.parents]:
237
+ if candidate == temp_root and candidate != start:
238
+ return None
239
+ workspace_modules_root = candidate / ".specfact" / "modules"
240
+ if workspace_modules_root.exists():
241
+ return workspace_modules_root
184
242
  git_dir = candidate / ".git"
185
243
  if git_dir.exists():
186
- workspace_modules_root = candidate / ".specfact" / "modules"
187
- if workspace_modules_root.exists():
188
- return workspace_modules_root
189
244
  return None
190
- workspace_modules_root = start / ".specfact" / "modules"
191
- if workspace_modules_root.exists():
192
- return workspace_modules_root
193
245
  return None
194
246
 
195
247
 
@@ -601,10 +653,77 @@ def _resolve_command_loader_path(
601
653
  return load_path, submodule_locations
602
654
 
603
655
 
656
+ def _remember_active_module_src(package_dir: Path) -> None:
657
+ """Remember an eligible installed module source root for lazy cross-module imports."""
658
+ if not _is_installed_module_package(package_dir):
659
+ return
660
+ src_dir = package_dir / "src"
661
+ if not src_dir.is_dir():
662
+ return
663
+ resolved = src_dir.resolve()
664
+ if resolved not in _ACTIVE_MODULE_SRC_DIRS:
665
+ _ACTIVE_MODULE_SRC_DIRS.insert(0, resolved)
666
+
667
+
668
+ def _prepend_active_module_src_roots() -> None:
669
+ """Prepend eligible installed module source roots before loading a command app."""
670
+ for src_dir in reversed(_ACTIVE_MODULE_SRC_DIRS):
671
+ src = str(src_dir)
672
+ if src not in sys.path:
673
+ sys.path.insert(0, src)
674
+
675
+
676
+ def _record_module_load_failure(package_name: str, command_name: str, reason: str) -> None:
677
+ _MODULE_LOAD_FAILURES[(package_name, command_name)] = reason
678
+ _MODULE_LOAD_FAILURES[(package_name, "*")] = reason
679
+
680
+
681
+ def _clear_module_load_failure(package_name: str, command_name: str) -> None:
682
+ _MODULE_LOAD_FAILURES.pop((package_name, command_name), None)
683
+ has_remaining_failure = any(
684
+ registered_package == package_name and registered_command != "*"
685
+ for registered_package, registered_command in _MODULE_LOAD_FAILURES
686
+ )
687
+ if not has_remaining_failure:
688
+ _MODULE_LOAD_FAILURES.pop((package_name, "*"), None)
689
+
690
+
691
+ def _clear_active_module_src_dirs() -> None:
692
+ for src_dir in _ACTIVE_MODULE_SRC_DIRS:
693
+ src = str(src_dir)
694
+ while src in sys.path:
695
+ sys.path.remove(src)
696
+ _ACTIVE_MODULE_SRC_DIRS.clear()
697
+
698
+
699
+ @beartype
700
+ @ensure(lambda result: result is None, "must return None")
701
+ def clear_module_load_failures() -> None:
702
+ """Clear process-scoped lazy module load failure diagnostics."""
703
+ _MODULE_LOAD_FAILURES.clear()
704
+
705
+
706
+ def _package_name_non_empty(package_name: str) -> bool:
707
+ return bool(package_name.strip())
708
+
709
+
710
+ @beartype
711
+ @require(_package_name_non_empty, "package name must be non-empty")
712
+ @ensure(lambda result: result is None or isinstance(result, str), "result must be a string or None")
713
+ def get_module_load_failure_reason(package_name: str, command_name: str | None = None) -> str | None:
714
+ """Return the latest lazy-load failure for a module, if one was captured."""
715
+ if command_name is not None:
716
+ specific = _MODULE_LOAD_FAILURES.get((package_name, command_name))
717
+ if specific:
718
+ return specific
719
+ return _MODULE_LOAD_FAILURES.get((package_name, "*"))
720
+
721
+
604
722
  def _make_package_loader(package_dir: Path, package_name: str, command_name: str) -> Any:
605
723
  """Return a callable that loads the package's app (from src/app.py or src/<name>/__init__.py)."""
606
724
 
607
725
  def loader() -> Any:
726
+ _prepend_active_module_src_roots()
608
727
  src_dir = package_dir / "src"
609
728
  if str(src_dir) not in sys.path:
610
729
  sys.path.insert(0, str(src_dir))
@@ -621,18 +740,23 @@ def _make_package_loader(package_dir: Path, package_name: str, command_name: str
621
740
  sys.modules[spec.name] = mod
622
741
  try:
623
742
  spec.loader.exec_module(mod)
624
- except (ImportError, ModuleNotFoundError, OSError) as exc:
625
- raise ValueError(
743
+ except Exception as exc:
744
+ message = (
626
745
  "Runtime compatibility error while loading "
627
746
  f"module '{package_name}' command '{command_name}' from {package_dir}: {exc}. "
628
747
  f"Reinstall the module and run SpecFact with the same Python interpreter ({sys.executable})."
629
- ) from exc
748
+ )
749
+ _record_module_load_failure(package_name, command_name, message)
750
+ raise ValueError(message) from exc
630
751
  command_attr = f"{_normalized_module_name(command_name)}_app"
631
752
  app = getattr(mod, command_attr, None)
632
753
  if app is None:
633
754
  app = getattr(mod, "app", None)
634
755
  if app is None:
635
- raise ValueError(f"Package {package_dir.name} has no '{command_attr}' or 'app' attribute")
756
+ message = f"Package {package_dir.name} has no '{command_attr}' or 'app' attribute"
757
+ _record_module_load_failure(package_name, command_name, message)
758
+ raise ValueError(message)
759
+ _clear_module_load_failure(package_name, command_name)
636
760
  return app
637
761
 
638
762
  return loader
@@ -1331,6 +1455,7 @@ def _register_one_package_if_eligible(package_dir: Path, meta: Any, reg: _Packag
1331
1455
  _register_schema_extensions_safe(meta, reg.logger)
1332
1456
  _register_service_bridges_safe(meta, reg.bridge_owner_map, reg.logger)
1333
1457
  _record_protocol_compliance_result(package_dir, meta, reg.logger, reg.counters)
1458
+ _remember_active_module_src(package_dir)
1334
1459
  _register_commands_for_package(package_dir, meta, reg.category_grouping_enabled, reg.logger)
1335
1460
 
1336
1461
 
@@ -1382,6 +1507,8 @@ def register_module_package_commands(
1382
1507
  disable_ids = disable_ids or []
1383
1508
  if allow_unsigned is None:
1384
1509
  allow_unsigned = os.environ.get("SPECFACT_ALLOW_UNSIGNED", "").strip().lower() in ("1", "true", "yes")
1510
+ _clear_active_module_src_dirs()
1511
+ _MODULE_LOAD_FAILURES.clear()
1385
1512
  is_test_mode = os.environ.get("TEST_MODE") == "true" or os.environ.get("PYTEST_CURRENT_TEST") is not None
1386
1513
  packages = discover_all_package_metadata()
1387
1514
  packages = sorted(packages, key=_package_sort_key)