specfact-cli 0.26.11__tar.gz → 0.26.14__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 (248) hide show
  1. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/PKG-INFO +2 -1
  2. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/README.md +1 -0
  3. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/pyproject.toml +1 -1
  4. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/__init__.py +1 -1
  5. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/__init__.py +1 -1
  6. specfact_cli-0.26.14/src/specfact_cli/__main__.py +6 -0
  7. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/adapters/ado.py +189 -92
  8. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/adapters/github.py +8 -0
  9. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/cli.py +4 -2
  10. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/commands/analyze.py +28 -0
  11. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/commands/auth.py +57 -1
  12. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/commands/backlog_commands.py +16 -6
  13. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/commands/drift.py +26 -0
  14. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/commands/enforce.py +71 -0
  15. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/commands/generate.py +142 -1
  16. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/commands/import_cmd.py +64 -1
  17. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/commands/init.py +10 -1
  18. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/commands/migrate.py +69 -0
  19. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/commands/plan.py +34 -0
  20. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/commands/project_cmd.py +50 -0
  21. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/commands/repro.py +34 -0
  22. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/commands/sdd.py +11 -1
  23. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/commands/spec.py +35 -0
  24. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/commands/sync.py +44 -4
  25. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/commands/update.py +37 -0
  26. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/commands/validate.py +42 -1
  27. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/common/logger_setup.py +68 -0
  28. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/runtime.py +126 -4
  29. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/sync/bridge_sync.py +11 -6
  30. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/.gitignore +0 -0
  31. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/LICENSE.md +0 -0
  32. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/mappings/node-async.yaml +0 -0
  33. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/mappings/python-async.yaml +0 -0
  34. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/mappings/speckit-default.yaml +0 -0
  35. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/prompts/shared/cli-enforcement.md +0 -0
  36. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/prompts/specfact.01-import.md +0 -0
  37. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/prompts/specfact.02-plan.md +0 -0
  38. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/prompts/specfact.03-review.md +0 -0
  39. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/prompts/specfact.04-sdd.md +0 -0
  40. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/prompts/specfact.05-enforce.md +0 -0
  41. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/prompts/specfact.06-sync.md +0 -0
  42. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/prompts/specfact.07-contracts.md +0 -0
  43. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/prompts/specfact.backlog-refine.md +0 -0
  44. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/prompts/specfact.compare.md +0 -0
  45. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/prompts/specfact.sync-backlog.md +0 -0
  46. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/prompts/specfact.validate.md +0 -0
  47. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/schemas/deviation.schema.json +0 -0
  48. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/schemas/plan.schema.json +0 -0
  49. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/schemas/protocol.schema.json +0 -0
  50. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/templates/backlog/defaults/defect_v1.yaml +0 -0
  51. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/templates/backlog/defaults/enabler_v1.yaml +0 -0
  52. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/templates/backlog/defaults/spike_v1.yaml +0 -0
  53. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/templates/backlog/defaults/user_story_v1.yaml +0 -0
  54. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/templates/backlog/field_mappings/ado_agile.yaml +0 -0
  55. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/templates/backlog/field_mappings/ado_default.yaml +0 -0
  56. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/templates/backlog/field_mappings/ado_kanban.yaml +0 -0
  57. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/templates/backlog/field_mappings/ado_safe.yaml +0 -0
  58. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/templates/backlog/field_mappings/ado_scrum.yaml +0 -0
  59. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/templates/backlog/frameworks/safe/safe_feature_v1.yaml +0 -0
  60. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/templates/backlog/frameworks/scrum/user_story_v1.yaml +0 -0
  61. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/templates/backlog/personas/developer/developer_task_v1.yaml +0 -0
  62. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/templates/backlog/personas/product-owner/user_story_v1.yaml +0 -0
  63. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/templates/backlog/providers/ado/work_item_v1.yaml +0 -0
  64. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/templates/github-action.yml.j2 +0 -0
  65. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/templates/persona/architect.md.j2 +0 -0
  66. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/templates/persona/developer.md.j2 +0 -0
  67. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/templates/persona/product-owner.md.j2 +0 -0
  68. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/templates/plan.bundle.yaml.j2 +0 -0
  69. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/templates/pr-template.md.j2 +0 -0
  70. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/templates/protocol.yaml.j2 +0 -0
  71. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/resources/templates/telemetry.yaml.example +0 -0
  72. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/adapters/__init__.py +0 -0
  73. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/adapters/backlog_base.py +0 -0
  74. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/adapters/base.py +0 -0
  75. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/adapters/openspec.py +0 -0
  76. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/adapters/openspec_parser.py +0 -0
  77. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/adapters/registry.py +0 -0
  78. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/adapters/speckit.py +0 -0
  79. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/agents/__init__.py +0 -0
  80. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/agents/analyze_agent.py +0 -0
  81. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/agents/base.py +0 -0
  82. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/agents/plan_agent.py +0 -0
  83. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/agents/registry.py +0 -0
  84. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/agents/sync_agent.py +0 -0
  85. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/analyzers/__init__.py +0 -0
  86. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/analyzers/ambiguity_scanner.py +0 -0
  87. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/analyzers/code_analyzer.py +0 -0
  88. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/analyzers/constitution_evidence_extractor.py +0 -0
  89. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/analyzers/contract_extractor.py +0 -0
  90. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/analyzers/control_flow_analyzer.py +0 -0
  91. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/analyzers/graph_analyzer.py +0 -0
  92. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/analyzers/relationship_mapper.py +0 -0
  93. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/analyzers/requirement_extractor.py +0 -0
  94. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/analyzers/test_pattern_extractor.py +0 -0
  95. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/backlog/__init__.py +0 -0
  96. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/backlog/adapters/__init__.py +0 -0
  97. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/backlog/adapters/base.py +0 -0
  98. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/backlog/adapters/local_yaml_adapter.py +0 -0
  99. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/backlog/ai_refiner.py +0 -0
  100. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/backlog/converter.py +0 -0
  101. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/backlog/filters.py +0 -0
  102. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/backlog/format_detector.py +0 -0
  103. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/backlog/formats/__init__.py +0 -0
  104. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/backlog/formats/base.py +0 -0
  105. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/backlog/formats/markdown_format.py +0 -0
  106. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/backlog/formats/structured_format.py +0 -0
  107. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/backlog/mappers/__init__.py +0 -0
  108. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/backlog/mappers/ado_mapper.py +0 -0
  109. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/backlog/mappers/base.py +0 -0
  110. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/backlog/mappers/github_mapper.py +0 -0
  111. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/backlog/mappers/template_config.py +0 -0
  112. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/backlog/template_detector.py +0 -0
  113. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/commands/__init__.py +0 -0
  114. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/commands/contract_cmd.py +0 -0
  115. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/common/__init__.py +0 -0
  116. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/common/logging_utils.py +0 -0
  117. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/common/text_utils.py +0 -0
  118. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/common/utils.py +0 -0
  119. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/comparators/__init__.py +0 -0
  120. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/comparators/plan_comparator.py +0 -0
  121. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/contracts/__init__.py +0 -0
  122. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/contracts/crosshair_props.py +0 -0
  123. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/enrichers/constitution_enricher.py +0 -0
  124. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/enrichers/plan_enricher.py +0 -0
  125. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/generators/__init__.py +0 -0
  126. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/generators/contract_generator.py +0 -0
  127. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/generators/openapi_extractor.py +0 -0
  128. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/generators/persona_exporter.py +0 -0
  129. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/generators/plan_generator.py +0 -0
  130. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/generators/protocol_generator.py +0 -0
  131. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/generators/report_generator.py +0 -0
  132. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/generators/task_generator.py +0 -0
  133. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/generators/test_to_openapi.py +0 -0
  134. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/generators/workflow_generator.py +0 -0
  135. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/importers/__init__.py +0 -0
  136. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/importers/speckit_converter.py +0 -0
  137. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/importers/speckit_scanner.py +0 -0
  138. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/integrations/__init__.py +0 -0
  139. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/integrations/specmatic.py +0 -0
  140. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/merge/__init__.py +0 -0
  141. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/merge/resolver.py +0 -0
  142. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/migrations/__init__.py +0 -0
  143. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/migrations/plan_migrator.py +0 -0
  144. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/models/__init__.py +0 -0
  145. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/models/backlog_item.py +0 -0
  146. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/models/bridge.py +0 -0
  147. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/models/capabilities.py +0 -0
  148. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/models/change.py +0 -0
  149. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/models/contract.py +0 -0
  150. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/models/deviation.py +0 -0
  151. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/models/dor_config.py +0 -0
  152. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/models/enforcement.py +0 -0
  153. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/models/persona_template.py +0 -0
  154. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/models/plan.py +0 -0
  155. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/models/project.py +0 -0
  156. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/models/protocol.py +0 -0
  157. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/models/quality.py +0 -0
  158. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/models/sdd.py +0 -0
  159. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/models/source_tracking.py +0 -0
  160. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/models/task.py +0 -0
  161. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/modes/__init__.py +0 -0
  162. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/modes/detector.py +0 -0
  163. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/modes/router.py +0 -0
  164. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/parsers/__init__.py +0 -0
  165. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/parsers/persona_importer.py +0 -0
  166. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/resources/semgrep/async.yml +0 -0
  167. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/resources/semgrep/code-quality.yml +0 -0
  168. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/resources/semgrep/feature-detection.yml +0 -0
  169. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/sync/__init__.py +0 -0
  170. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/sync/bridge_probe.py +0 -0
  171. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/sync/bridge_watch.py +0 -0
  172. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/sync/change_detector.py +0 -0
  173. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/sync/code_to_spec.py +0 -0
  174. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/sync/drift_detector.py +0 -0
  175. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/sync/repository_sync.py +0 -0
  176. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/sync/spec_to_code.py +0 -0
  177. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/sync/spec_to_tests.py +0 -0
  178. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/sync/watcher.py +0 -0
  179. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/sync/watcher_enhanced.py +0 -0
  180. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/telemetry.py +0 -0
  181. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/templates/__init__.py +0 -0
  182. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/templates/bridge_templates.py +0 -0
  183. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/templates/defaults/defect_v1.yaml +0 -0
  184. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/templates/defaults/enabler_v1.yaml +0 -0
  185. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/templates/defaults/spike_v1.yaml +0 -0
  186. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/templates/defaults/user_story_v1.yaml +0 -0
  187. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/templates/frameworks/scrum/user_story_v1.yaml +0 -0
  188. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/templates/personas/product-owner/user_story_v1.yaml +0 -0
  189. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/templates/providers/ado/work_item_v1.yaml +0 -0
  190. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/templates/registry.py +0 -0
  191. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/templates/specification_templates.py +0 -0
  192. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/__init__.py +0 -0
  193. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/acceptance_criteria.py +0 -0
  194. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/auth_tokens.py +0 -0
  195. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/bundle_loader.py +0 -0
  196. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/code_change_detector.py +0 -0
  197. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/console.py +0 -0
  198. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/content_sanitizer.py +0 -0
  199. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/context_detection.py +0 -0
  200. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/enrichment_context.py +0 -0
  201. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/enrichment_parser.py +0 -0
  202. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/env_manager.py +0 -0
  203. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/feature_keys.py +0 -0
  204. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/git.py +0 -0
  205. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/github_annotations.py +0 -0
  206. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/ide_setup.py +0 -0
  207. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/incremental_check.py +0 -0
  208. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/metadata.py +0 -0
  209. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/optional_deps.py +0 -0
  210. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/performance.py +0 -0
  211. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/progress.py +0 -0
  212. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/progressive_disclosure.py +0 -0
  213. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/prompts.py +0 -0
  214. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/sdd_discovery.py +0 -0
  215. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/source_scanner.py +0 -0
  216. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/startup_checks.py +0 -0
  217. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/structure.py +0 -0
  218. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/structured_io.py +0 -0
  219. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/suggestions.py +0 -0
  220. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/terminal.py +0 -0
  221. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/utils/yaml_utils.py +0 -0
  222. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/validators/__init__.py +0 -0
  223. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/validators/agile_validation.py +0 -0
  224. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/validators/change_proposal_integration.py +0 -0
  225. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/validators/cli_first_validator.py +0 -0
  226. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/validators/contract_validator.py +0 -0
  227. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/validators/fsm.py +0 -0
  228. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/validators/repro_checker.py +0 -0
  229. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/validators/schema.py +0 -0
  230. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/validators/sidecar/__init__.py +0 -0
  231. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/validators/sidecar/contract_populator.py +0 -0
  232. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/validators/sidecar/crosshair_runner.py +0 -0
  233. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/validators/sidecar/crosshair_summary.py +0 -0
  234. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/validators/sidecar/dependency_installer.py +0 -0
  235. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/validators/sidecar/framework_detector.py +0 -0
  236. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/validators/sidecar/frameworks/__init__.py +0 -0
  237. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/validators/sidecar/frameworks/base.py +0 -0
  238. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/validators/sidecar/frameworks/django.py +0 -0
  239. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/validators/sidecar/frameworks/drf.py +0 -0
  240. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/validators/sidecar/frameworks/fastapi.py +0 -0
  241. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/validators/sidecar/frameworks/flask.py +0 -0
  242. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/validators/sidecar/harness_generator.py +0 -0
  243. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/validators/sidecar/models.py +0 -0
  244. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/validators/sidecar/orchestrator.py +0 -0
  245. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/validators/sidecar/specmatic_runner.py +0 -0
  246. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/validators/sidecar/unannotated_detector.py +0 -0
  247. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/versioning/__init__.py +0 -0
  248. {specfact_cli-0.26.11 → specfact_cli-0.26.14}/src/specfact_cli/versioning/analyzer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: specfact-cli
3
- Version: 0.26.11
3
+ Version: 0.26.14
4
4
  Summary: Brownfield-first CLI: Reverse engineer legacy Python → specs → enforced contracts. Automate legacy code documentation and prevent modernization regressions.
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
@@ -617,6 +617,7 @@ hatch run contract-test-full
617
617
 
618
618
  - 💬 **Questions?** [GitHub Discussions](https://github.com/nold-ai/specfact-cli/discussions)
619
619
  - 🐛 **Found a bug?** [GitHub Issues](https://github.com/nold-ai/specfact-cli/issues)
620
+ - 🔍 **Debugging I/O or API issues?** Run with `--debug`; logs are written to `~/.specfact/logs/specfact-debug.log`. With `--debug`, ADO API errors include response snippet and patch paths in the log. See [Debug Logging](docs/reference/debug-logging.md).
620
621
  - 📧 **Need help?** [hello@noldai.com](mailto:hello@noldai.com)
621
622
  - 🌐 **Learn more:** [specfact.com](https://specfact.com) • [specfact.io](https://specfact.io) • [specfact.dev](https://specfact.dev)
622
623
 
@@ -339,6 +339,7 @@ hatch run contract-test-full
339
339
 
340
340
  - 💬 **Questions?** [GitHub Discussions](https://github.com/nold-ai/specfact-cli/discussions)
341
341
  - 🐛 **Found a bug?** [GitHub Issues](https://github.com/nold-ai/specfact-cli/issues)
342
+ - 🔍 **Debugging I/O or API issues?** Run with `--debug`; logs are written to `~/.specfact/logs/specfact-debug.log`. With `--debug`, ADO API errors include response snippet and patch paths in the log. See [Debug Logging](docs/reference/debug-logging.md).
342
343
  - 📧 **Need help?** [hello@noldai.com](mailto:hello@noldai.com)
343
344
  - 🌐 **Learn more:** [specfact.com](https://specfact.com) • [specfact.io](https://specfact.io) • [specfact.dev](https://specfact.dev)
344
345
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "specfact-cli"
7
- version = "0.26.11"
7
+ version = "0.26.14"
8
8
  description = "Brownfield-first CLI: Reverse engineer legacy Python → specs → enforced contracts. Automate legacy code documentation and prevent modernization regressions."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -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.26.11"
6
+ __version__ = "0.26.14"
@@ -9,6 +9,6 @@ This package provides command-line tools for:
9
9
  - Validating reproducibility
10
10
  """
11
11
 
12
- __version__ = "0.26.11"
12
+ __version__ = "0.26.14"
13
13
 
14
14
  __all__ = ["__version__"]
@@ -0,0 +1,6 @@
1
+ """Allow running the CLI as python -m specfact_cli."""
2
+
3
+ if __name__ == "__main__":
4
+ from specfact_cli.cli import cli_main
5
+
6
+ cli_main()
@@ -27,17 +27,77 @@ from specfact_cli.adapters.base import BridgeAdapter
27
27
  from specfact_cli.backlog.adapters.base import BacklogAdapter
28
28
  from specfact_cli.backlog.filters import BacklogFilters
29
29
  from specfact_cli.backlog.mappers.ado_mapper import AdoFieldMapper
30
+ from specfact_cli.common.logger_setup import LoggerSetup
30
31
  from specfact_cli.models.backlog_item import BacklogItem
31
32
  from specfact_cli.models.bridge import BridgeConfig
32
33
  from specfact_cli.models.capabilities import ToolCapabilities
33
34
  from specfact_cli.models.change import ChangeProposal, ChangeTracking
34
- from specfact_cli.runtime import debug_print
35
+ from specfact_cli.runtime import debug_log_operation, debug_print, is_debug_mode
35
36
  from specfact_cli.utils.auth_tokens import get_token, set_token
36
37
 
37
38
 
39
+ _MAX_RESPONSE_BODY_LOG = 2048
40
+
38
41
  console = Console()
39
42
 
40
43
 
44
+ def _log_ado_patch_failure(
45
+ response: requests.Response | None,
46
+ operations: list[dict[str, Any]],
47
+ url: str,
48
+ context: str = "",
49
+ ) -> str:
50
+ """
51
+ Log ADO PATCH failure to debug log (when debug on) and return user-facing message.
52
+
53
+ Parses response body (JSON message or truncated text), extracts patch paths,
54
+ redacts/truncates for debug log, and builds a user message with ADO text and hint.
55
+ """
56
+ paths = [op.get("path", "") for op in operations if isinstance(op, dict)]
57
+ snippet = ""
58
+ if response is not None:
59
+ try:
60
+ body = response.json()
61
+ snippet = str(body.get("message", response.text[:500]))
62
+ except Exception:
63
+ snippet = (response.text or "")[:_MAX_RESPONSE_BODY_LOG]
64
+ snippet = snippet[:_MAX_RESPONSE_BODY_LOG]
65
+ snippet = str(LoggerSetup.redact_secrets(snippet))
66
+
67
+ if is_debug_mode():
68
+ debug_log_operation(
69
+ "ado_patch",
70
+ url,
71
+ "failed",
72
+ error=context or snippet[:500],
73
+ extra={"response_body": snippet, "patch_paths": paths},
74
+ )
75
+
76
+ return _build_ado_user_message(response)
77
+
78
+
79
+ def _build_ado_user_message(response: requests.Response | None) -> str:
80
+ """Build user-facing error message from ADO response and append mapping hint."""
81
+ hint = " Check custom field mapping; see ado_custom.yaml or documentation."
82
+ if response is None:
83
+ return f"Azure DevOps request failed.{hint}"
84
+ try:
85
+ body = response.json()
86
+ msg = body.get("message", "") or (response.text or "")[:500]
87
+ except Exception:
88
+ msg = (response.text or "")[:500]
89
+ if not msg:
90
+ return f"Azure DevOps request failed (HTTP {getattr(response, 'status_code', '')}).{hint}"
91
+
92
+ m = re.search(r"Cannot find field\s+([^\s]+)", msg, re.IGNORECASE)
93
+ if m:
94
+ field = m.group(1).strip().rstrip(".")
95
+ user_msg = f"Field '{field}' not found.{hint}"
96
+ else:
97
+ user_msg = f"{msg}{hint}"
98
+ return user_msg
99
+
100
+
41
101
  class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
42
102
  """
43
103
  Azure DevOps bridge adapter implementing BridgeAdapter interface.
@@ -372,6 +432,8 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
372
432
  rationale = ""
373
433
  impact = ""
374
434
 
435
+ import re
436
+
375
437
  # Parse markdown sections (Why, What Changes)
376
438
  if description_raw:
377
439
  # Extract "Why" section (stop at What Changes or OpenSpec footer)
@@ -696,6 +758,8 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
696
758
  ValueError: If required configuration is missing
697
759
  requests.RequestException: If Azure DevOps API call fails
698
760
  """
761
+ import re as _re
762
+
699
763
  if not self.api_token:
700
764
  msg = (
701
765
  "Azure DevOps API token required. Options:\n"
@@ -751,7 +815,7 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
751
815
  parsed = urlparse(source_url)
752
816
  if parsed.hostname and parsed.hostname.lower() == "dev.azure.com":
753
817
  target_org = target_repo.split("/")[0]
754
- ado_org_match = re.search(r"dev\.azure\.com/([^/]+)/", source_url)
818
+ ado_org_match = _re.search(r"dev\.azure\.com/([^/]+)/", source_url)
755
819
  if ado_org_match and ado_org_match.group(1) == target_org:
756
820
  # Org matches - this is likely the same ADO organization
757
821
  work_item_id = entry.get("source_id")
@@ -773,8 +837,8 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
773
837
  # 3. AND (project is unknown in entry OR project is unknown in target OR both contain GUIDs)
774
838
  # This prevents matching org/project-a with org/project-b when both have known project names
775
839
  source_url = entry.get("source_url", "")
776
- entry_has_guid = source_url and re.search(
777
- r"dev\.azure\.com/[^/]+/[0-9a-f-]{36}", source_url, re.IGNORECASE
840
+ entry_has_guid = source_url and _re.search(
841
+ r"dev\.azure\.com/[^/]+/[0-9a-f-]{36}", source_url, _re.IGNORECASE
778
842
  )
779
843
  project_unknown = (
780
844
  not entry_project # Entry has no project part
@@ -967,8 +1031,10 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
967
1031
  Returns:
968
1032
  Tuple of (org, project, work_item_id)
969
1033
  """
1034
+ import re as _re
1035
+
970
1036
  cleaned = item_ref.strip().lstrip("#")
971
- url_match = re.search(r"dev\.azure\.com/([^/]+)/([^/]+)/.*?/(\d+)", cleaned, re.IGNORECASE)
1037
+ url_match = _re.search(r"dev\.azure\.com/([^/]+)/([^/]+)/.*?/(\d+)", cleaned, _re.IGNORECASE)
972
1038
  if url_match:
973
1039
  return url_match.group(1), url_match.group(2), int(url_match.group(3))
974
1040
 
@@ -1439,6 +1505,13 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
1439
1505
 
1440
1506
  try:
1441
1507
  response = requests.post(url, json=wiql, headers=headers, timeout=10)
1508
+ if is_debug_mode():
1509
+ debug_log_operation(
1510
+ "ado_wiql",
1511
+ url,
1512
+ str(response.status_code),
1513
+ error=None if response.ok else (response.text[:200] if response.text else None),
1514
+ )
1442
1515
  if response.status_code != 200:
1443
1516
  return None
1444
1517
  work_items = response.json().get("workItems", [])
@@ -1453,7 +1526,9 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
1453
1526
  "source_type": "ado",
1454
1527
  "source_repo": f"{org}/{project}",
1455
1528
  }
1456
- except requests.RequestException:
1529
+ except requests.RequestException as e:
1530
+ if is_debug_mode():
1531
+ debug_log_operation("ado_wiql", url, "error", error=str(e))
1457
1532
  return None
1458
1533
 
1459
1534
  def _create_work_item_from_proposal(
@@ -1473,6 +1548,8 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
1473
1548
  Returns:
1474
1549
  Dict with work item data: {"work_item_id": int, "work_item_url": str, "state": str}
1475
1550
  """
1551
+ import re as _re
1552
+
1476
1553
  title = proposal_data.get("title", "Untitled Change Proposal")
1477
1554
  description = proposal_data.get("description", "")
1478
1555
  rationale = proposal_data.get("rationale", "")
@@ -1489,7 +1566,7 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
1489
1566
  else:
1490
1567
  body_parts = []
1491
1568
 
1492
- display_title = re.sub(r"^\[change\]\s*", "", title, flags=re.IGNORECASE).strip()
1569
+ display_title = _re.sub(r"^\[change\]\s*", "", title, flags=_re.IGNORECASE).strip()
1493
1570
  if display_title:
1494
1571
  body_parts.append(f"# {display_title}")
1495
1572
  body_parts.append("")
@@ -1572,6 +1649,13 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
1572
1649
 
1573
1650
  try:
1574
1651
  response = requests.patch(url, json=patch_document, headers=headers, timeout=30)
1652
+ if is_debug_mode():
1653
+ debug_log_operation(
1654
+ "ado_patch",
1655
+ url,
1656
+ str(response.status_code),
1657
+ error=None if response.ok else (response.text[:200] if response.text else None),
1658
+ )
1575
1659
  response.raise_for_status()
1576
1660
  work_item_data = response.json()
1577
1661
 
@@ -1617,8 +1701,10 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
1617
1701
  "state": ado_state,
1618
1702
  }
1619
1703
  except requests.RequestException as e:
1620
- msg = f"Failed to create Azure DevOps work item: {e}"
1621
- console.print(f"[bold red]✗[/bold red] {msg}")
1704
+ resp = getattr(e, "response", None)
1705
+ user_msg = _log_ado_patch_failure(resp, patch_document, url)
1706
+ e.ado_user_message = user_msg
1707
+ console.print(f"[bold red]✗[/bold red] {user_msg}")
1622
1708
  raise
1623
1709
 
1624
1710
  def _update_work_item_status(
@@ -1717,8 +1803,9 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
1717
1803
  "state": ado_state,
1718
1804
  }
1719
1805
  except requests.RequestException as e:
1720
- msg = f"Failed to update Azure DevOps work item #{work_item_id}: {e}"
1721
- console.print(f"[bold red]✗[/bold red] {msg}")
1806
+ resp = getattr(e, "response", None)
1807
+ user_msg = _log_ado_patch_failure(resp, patch_document, url)
1808
+ console.print(f"[bold red]✗[/bold red] {user_msg}")
1722
1809
  raise
1723
1810
 
1724
1811
  def _update_work_item_body(
@@ -1740,6 +1827,8 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
1740
1827
  Returns:
1741
1828
  Dict with updated work item data: {"work_item_id": int, "work_item_url": str, "state": str}
1742
1829
  """
1830
+ import re as _re
1831
+
1743
1832
  title = proposal_data.get("title", "Untitled Change Proposal")
1744
1833
  description = proposal_data.get("description", "")
1745
1834
  rationale = proposal_data.get("rationale", "")
@@ -1756,7 +1845,7 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
1756
1845
  else:
1757
1846
  body_parts = []
1758
1847
 
1759
- display_title = re.sub(r"^\[change\]\s*", "", title, flags=re.IGNORECASE).strip()
1848
+ display_title = _re.sub(r"^\[change\]\s*", "", title, flags=_re.IGNORECASE).strip()
1760
1849
  if display_title:
1761
1850
  body_parts.append(f"# {display_title}")
1762
1851
  body_parts.append("")
@@ -1847,8 +1936,9 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
1847
1936
  "state": ado_state,
1848
1937
  }
1849
1938
  except requests.RequestException as e:
1850
- msg = f"Failed to update Azure DevOps work item #{work_item_id}: {e}"
1851
- console.print(f"[bold red]✗[/bold red] {msg}")
1939
+ resp = getattr(e, "response", None)
1940
+ user_msg = _log_ado_patch_failure(resp, patch_document, url)
1941
+ console.print(f"[bold red]✗[/bold red] {user_msg}")
1852
1942
  raise
1853
1943
 
1854
1944
  @beartype
@@ -1954,8 +2044,10 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
1954
2044
  "new_state": ado_state,
1955
2045
  }
1956
2046
  except requests.RequestException as e:
1957
- msg = f"Failed to sync status to Azure DevOps work item #{work_item_id}: {e}"
1958
- console.print(f"[bold red]✗[/bold red] {msg}")
2047
+ resp = getattr(e, "response", None)
2048
+ user_msg = _log_ado_patch_failure(resp, patch_document, url)
2049
+ e.ado_user_message = user_msg
2050
+ console.print(f"[bold red]✗[/bold red] {user_msg}")
1959
2051
  raise
1960
2052
 
1961
2053
  @beartype
@@ -2332,8 +2424,10 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
2332
2424
  "comment_added": True,
2333
2425
  }
2334
2426
  except requests.RequestException as e:
2335
- msg = f"Failed to add comment to Azure DevOps work item #{work_item_id}: {e}"
2336
- console.print(f"[bold red]✗[/bold red] {msg}")
2427
+ resp = getattr(e, "response", None)
2428
+ user_msg = _log_ado_patch_failure(resp, [], url)
2429
+ e.ado_user_message = user_msg
2430
+ console.print(f"[bold red]✗[/bold red] {user_msg}")
2337
2431
  raise
2338
2432
 
2339
2433
  @beartype
@@ -2908,8 +3002,22 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
2908
3002
 
2909
3003
  try:
2910
3004
  response = requests.get(url, headers=workitems_headers, params=params, timeout=30)
3005
+ if is_debug_mode():
3006
+ debug_log_operation(
3007
+ "ado_workitems_get",
3008
+ url,
3009
+ str(response.status_code),
3010
+ error=None if response.ok else (response.text[:200] if response.text else None),
3011
+ )
2911
3012
  response.raise_for_status()
2912
3013
  except requests.HTTPError as e:
3014
+ if is_debug_mode():
3015
+ debug_log_operation(
3016
+ "ado_workitems_get",
3017
+ url,
3018
+ "error",
3019
+ error=str(e.response.status_code) if e.response is not None else str(e),
3020
+ )
2913
3021
  # Provide better error message with URL details
2914
3022
  error_detail = ""
2915
3023
  if e.response is not None:
@@ -3095,11 +3203,12 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
3095
3203
 
3096
3204
  # Update description (body_markdown) - always use System.Description
3097
3205
  if update_fields is None or "body" in update_fields or "body_markdown" in update_fields:
3098
- # Convert TODO markers to proper Markdown checkboxes for ADO rendering
3099
3206
  import re
3100
3207
 
3101
- markdown_content = item.body_markdown
3102
- # Pattern matches: * [TODO: ...] or - [TODO: ...] or *[TODO: ...] or -[TODO: ...]
3208
+ # Never send null: ADO rejects null for /fields/System.Description (HTTP 400)
3209
+ raw_body = item.body_markdown
3210
+ markdown_content = raw_body if raw_body is not None else ""
3211
+ # Convert TODO markers to proper Markdown checkboxes for ADO rendering
3103
3212
  todo_pattern = r"^(\s*)[-*]\s*\[TODO[:\s]+([^\]]+)\](.*)$"
3104
3213
  markdown_content = re.sub(
3105
3214
  todo_pattern,
@@ -3108,11 +3217,9 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
3108
3217
  flags=re.MULTILINE | re.IGNORECASE,
3109
3218
  )
3110
3219
 
3111
- # Get mapped description field name (honors custom mappings)
3112
3220
  description_field = reverse_mappings.get("description", "System.Description")
3113
- # Set multiline field format to Markdown FIRST (before setting content)
3221
+ # Set multiline field format to Markdown first (optional; many ADO instances return 400 for this path)
3114
3222
  operations.append({"op": "add", "path": f"/multilineFieldsFormat/{description_field}", "value": "Markdown"})
3115
- # Then set description content with Markdown format
3116
3223
  operations.append({"op": "replace", "path": f"/fields/{description_field}", "value": markdown_content})
3117
3224
 
3118
3225
  # Update acceptance criteria using mapped field name (honors custom mappings)
@@ -3158,7 +3265,9 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
3158
3265
  response = requests.patch(url, headers=headers, json=operations, timeout=30)
3159
3266
  response.raise_for_status()
3160
3267
  except requests.HTTPError as e:
3161
- # Handle various error cases
3268
+ user_msg = _log_ado_patch_failure(e.response, operations, url)
3269
+ e.ado_user_message = user_msg
3270
+ response = None
3162
3271
  if e.response and e.response.status_code in (400, 422):
3163
3272
  error_message = ""
3164
3273
  try:
@@ -3167,91 +3276,79 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
3167
3276
  except Exception:
3168
3277
  pass
3169
3278
 
3170
- # Check if error is about multilineFieldsFormat already existing (use "replace" instead)
3171
- if "already exists" in error_message.lower() or "cannot add" in error_message.lower():
3172
- # Try with "replace" operation for multilineFieldsFormat
3279
+ # First retry: omit multilineFieldsFormat entirely (only /fields/ updates).
3280
+ # Many ADO instances reject /multilineFieldsFormat/ path with 400 Bad Request.
3281
+ operations_no_format = [
3282
+ op for op in operations if not (op.get("path") or "").startswith("/multilineFieldsFormat/")
3283
+ ]
3284
+ if operations_no_format != operations:
3285
+ try:
3286
+ resp = requests.patch(url, headers=headers, json=operations_no_format, timeout=30)
3287
+ resp.raise_for_status()
3288
+ response = resp
3289
+ except requests.HTTPError as retry_error:
3290
+ _log_ado_patch_failure(
3291
+ retry_error.response,
3292
+ operations_no_format,
3293
+ url,
3294
+ context=str(retry_error),
3295
+ )
3296
+
3297
+ if response is None and (
3298
+ "already exists" in error_message.lower() or "cannot add" in error_message.lower()
3299
+ ):
3300
+ # Second: try "replace" instead of "add" for multilineFieldsFormat
3173
3301
  operations_replace = []
3174
3302
  for op in operations:
3175
- if op.get("path") == "/multilineFieldsFormat/System.Description":
3176
- # Change to replace operation
3177
- operations_replace.append({"op": "replace", "path": op["path"], "value": op["value"]})
3303
+ path = op.get("path") or ""
3304
+ if path.startswith("/multilineFieldsFormat/"):
3305
+ operations_replace.append({"op": "replace", "path": path, "value": op["value"]})
3178
3306
  else:
3179
3307
  operations_replace.append(op)
3180
-
3181
3308
  try:
3182
- response = requests.patch(url, headers=headers, json=operations_replace, timeout=30)
3183
- response.raise_for_status()
3309
+ resp = requests.patch(url, headers=headers, json=operations_replace, timeout=30)
3310
+ resp.raise_for_status()
3311
+ response = resp
3184
3312
  except requests.HTTPError:
3185
- # If replace also fails, fallback to HTML conversion
3186
- console.print("[yellow]⚠ Markdown format not supported, converting to HTML[/yellow]")
3187
- operations_html = [
3188
- op for op in operations if "/multilineFieldsFormat/" not in op.get("path", "")
3189
- ]
3190
- # Find description operation and convert markdown to HTML
3191
- for op in operations_html:
3192
- if op.get("path") == "/fields/System.Description":
3193
- # Convert TODO markers to HTML checkboxes before converting to HTML
3194
- import re
3195
-
3196
- markdown_for_html = op["value"]
3197
- # Convert TODO markers to checkboxes first
3198
- todo_pattern = r"^(\s*)[-*]\s*\[TODO[:\s]+([^\]]+)\](.*)$"
3199
- markdown_for_html = re.sub(
3200
- todo_pattern,
3201
- r"\1- [ ] \2",
3202
- markdown_for_html,
3203
- flags=re.MULTILINE | re.IGNORECASE,
3204
- )
3205
- # Simple markdown to HTML conversion (basic)
3206
- try:
3207
- import markdown
3208
-
3209
- html_body = markdown.markdown(
3210
- markdown_for_html, extensions=["fenced_code", "tables"]
3211
- )
3212
- op["value"] = html_body
3213
- except ImportError:
3214
- # markdown library not available - use raw text
3215
- console.print("[yellow]⚠ markdown library not available, using raw text[/yellow]")
3216
- # Keep original markdown as-is (ADO may still render it)
3217
- break
3218
-
3219
- response = requests.patch(url, headers=headers, json=operations_html, timeout=30)
3220
- response.raise_for_status()
3221
- else:
3222
- # Other 400/422 errors - try HTML fallback
3223
- console.print("[yellow]⚠ Markdown format not supported, converting to HTML[/yellow]")
3224
- operations_html = [op for op in operations if "/multilineFieldsFormat/" not in op.get("path", "")]
3225
- # Find description operation and convert markdown to HTML
3313
+ pass
3314
+
3315
+ if response is None:
3316
+ # Third: HTML fallback (no multilineFieldsFormat, description as HTML)
3317
+ import re as _re
3318
+
3319
+ console.print("[yellow]⚠ Markdown format not supported, converting description to HTML[/yellow]")
3320
+ operations_html = [
3321
+ op for op in operations if not (op.get("path") or "").startswith("/multilineFieldsFormat/")
3322
+ ]
3323
+ description_field = reverse_mappings.get("description", "System.Description")
3324
+ desc_path = f"/fields/{description_field}"
3226
3325
  for op in operations_html:
3227
- if op.get("path") == "/fields/System.Description":
3228
- # Convert TODO markers to HTML checkboxes before converting to HTML
3229
- import re
3230
-
3231
- markdown_for_html = op["value"]
3232
- # Convert TODO markers to checkboxes first
3326
+ if op.get("path") == desc_path:
3327
+ markdown_for_html = op.get("value") or ""
3233
3328
  todo_pattern = r"^(\s*)[-*]\s*\[TODO[:\s]+([^\]]+)\](.*)$"
3234
- markdown_for_html = re.sub(
3329
+ markdown_for_html = _re.sub(
3235
3330
  todo_pattern,
3236
3331
  r"\1- [ ] \2",
3237
3332
  markdown_for_html,
3238
- flags=re.MULTILINE | re.IGNORECASE,
3333
+ flags=_re.MULTILINE | _re.IGNORECASE,
3239
3334
  )
3240
- # Simple markdown to HTML conversion (basic)
3241
3335
  try:
3242
3336
  import markdown
3243
3337
 
3244
- html_body = markdown.markdown(markdown_for_html, extensions=["fenced_code", "tables"])
3245
- op["value"] = html_body
3338
+ op["value"] = markdown.markdown(markdown_for_html, extensions=["fenced_code", "tables"])
3246
3339
  except ImportError:
3247
- # markdown library not available - use raw text
3248
- console.print("[yellow]⚠ markdown library not available, using raw text[/yellow]")
3249
- # Keep original markdown as-is (ADO may still render it)
3340
+ pass
3250
3341
  break
3342
+ try:
3343
+ resp = requests.patch(url, headers=headers, json=operations_html, timeout=30)
3344
+ resp.raise_for_status()
3345
+ response = resp
3346
+ except requests.HTTPError:
3347
+ console.print(f"[bold red]✗[/bold red] {user_msg}")
3348
+ raise
3251
3349
 
3252
- response = requests.patch(url, headers=headers, json=operations_html, timeout=30)
3253
- response.raise_for_status()
3254
- else:
3350
+ if response is None:
3351
+ console.print(f"[bold red]✗[/bold red] {user_msg}")
3255
3352
  raise
3256
3353
 
3257
3354
  updated_work_item = response.json()
@@ -35,6 +35,7 @@ from specfact_cli.models.backlog_item import BacklogItem
35
35
  from specfact_cli.models.bridge import BridgeConfig
36
36
  from specfact_cli.models.capabilities import ToolCapabilities
37
37
  from specfact_cli.models.change import ChangeProposal, ChangeTracking
38
+ from specfact_cli.runtime import debug_log_operation, is_debug_mode
38
39
  from specfact_cli.utils.auth_tokens import get_token
39
40
 
40
41
 
@@ -817,6 +818,13 @@ class GitHubAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
817
818
  "Accept": "application/vnd.github.v3+json",
818
819
  }
819
820
  response = requests.get(url, headers=headers, timeout=30)
821
+ if is_debug_mode():
822
+ debug_log_operation(
823
+ "github_api_get",
824
+ url,
825
+ str(response.status_code),
826
+ error=None if response.ok else (response.text[:200] if response.text else None),
827
+ )
820
828
  response.raise_for_status()
821
829
  return response.json()
822
830
 
@@ -75,7 +75,7 @@ from specfact_cli.commands import (
75
75
  validate,
76
76
  )
77
77
  from specfact_cli.modes import OperationalMode, detect_mode
78
- from specfact_cli.runtime import get_configured_console, set_debug_mode
78
+ from specfact_cli.runtime import get_configured_console, init_debug_log_file, set_debug_mode
79
79
  from specfact_cli.utils.progressive_disclosure import ProgressiveDisclosureGroup
80
80
  from specfact_cli.utils.structured_io import StructuredFormat
81
81
 
@@ -246,7 +246,7 @@ def main(
246
246
  debug: bool = typer.Option(
247
247
  False,
248
248
  "--debug",
249
- help="Enable debug output (shows detailed logging and diagnostic information)",
249
+ help="Enable debug output: console diagnostics and log file at ~/.specfact/logs/specfact-debug.log (operation metadata for file I/O and API calls)",
250
250
  ),
251
251
  skip_checks: bool = typer.Option(
252
252
  False,
@@ -297,6 +297,8 @@ def main(
297
297
 
298
298
  # Set debug mode
299
299
  set_debug_mode(debug)
300
+ if debug:
301
+ init_debug_log_file()
300
302
 
301
303
  runtime.configure_io_formats(input_format=input_format, output_format=output_format)
302
304
  # Invert logic: --interactive means not non-interactive, --no-interactive means non-interactive
@@ -17,6 +17,7 @@ from rich.console import Console
17
17
  from rich.table import Table
18
18
 
19
19
  from specfact_cli.models.quality import CodeQuality, QualityTracking
20
+ from specfact_cli.runtime import debug_log_operation, debug_print, is_debug_mode
20
21
  from specfact_cli.telemetry import telemetry
21
22
  from specfact_cli.utils import print_error, print_success
22
23
  from specfact_cli.utils.progress import load_bundle_with_progress
@@ -63,12 +64,23 @@ def analyze_contracts(
63
64
  **Examples:**
64
65
  specfact analyze contracts --repo . --bundle legacy-api
65
66
  """
67
+ if is_debug_mode():
68
+ debug_log_operation("command", "analyze contracts", "started", extra={"repo": str(repo), "bundle": bundle})
69
+ debug_print("[dim]analyze contracts: started[/dim]")
66
70
  console = Console()
67
71
 
68
72
  # Use active plan as default if bundle not provided
69
73
  if bundle is None:
70
74
  bundle = SpecFactStructure.get_active_bundle_name(repo)
71
75
  if bundle is None:
76
+ if is_debug_mode():
77
+ debug_log_operation(
78
+ "command",
79
+ "analyze contracts",
80
+ "failed",
81
+ error="Bundle name required",
82
+ extra={"reason": "no_bundle"},
83
+ )
72
84
  console.print("[bold red]✗[/bold red] Bundle name required")
73
85
  console.print("[yellow]→[/yellow] Use --bundle option or run 'specfact plan select' to set active plan")
74
86
  raise typer.Exit(1)
@@ -78,6 +90,14 @@ def analyze_contracts(
78
90
  bundle_dir = SpecFactStructure.project_dir(base_path=repo_path, bundle_name=bundle)
79
91
 
80
92
  if not bundle_dir.exists():
93
+ if is_debug_mode():
94
+ debug_log_operation(
95
+ "command",
96
+ "analyze contracts",
97
+ "failed",
98
+ error=f"Bundle not found: {bundle_dir}",
99
+ extra={"reason": "bundle_missing"},
100
+ )
81
101
  print_error(f"Project bundle not found: {bundle_dir}")
82
102
  raise typer.Exit(1)
83
103
 
@@ -200,6 +220,14 @@ def analyze_contracts(
200
220
  "files_with_crosshair": files_with_crosshair,
201
221
  }
202
222
  )
223
+ if is_debug_mode():
224
+ debug_log_operation(
225
+ "command",
226
+ "analyze contracts",
227
+ "success",
228
+ extra={"files_analyzed": files_analyzed, "bundle": bundle},
229
+ )
230
+ debug_print("[dim]analyze contracts: success[/dim]")
203
231
 
204
232
 
205
233
  def _analyze_file_quality(file_path: Path) -> CodeQuality: