specfact-cli 0.26.0__tar.gz → 0.26.1__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 (234) hide show
  1. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/PKG-INFO +1 -1
  2. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/pyproject.toml +1 -1
  3. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/prompts/specfact.backlog-refine.md +30 -1
  4. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/__init__.py +1 -1
  5. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/__init__.py +1 -1
  6. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/adapters/ado.py +91 -5
  7. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/adapters/backlog_base.py +66 -0
  8. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/adapters/github.py +143 -8
  9. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/commands/backlog_commands.py +39 -2
  10. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/sync/bridge_sync.py +20 -0
  11. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/.gitignore +0 -0
  12. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/LICENSE.md +0 -0
  13. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/README.md +0 -0
  14. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/mappings/node-async.yaml +0 -0
  15. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/mappings/python-async.yaml +0 -0
  16. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/mappings/speckit-default.yaml +0 -0
  17. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/prompts/shared/cli-enforcement.md +0 -0
  18. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/prompts/specfact.01-import.md +0 -0
  19. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/prompts/specfact.02-plan.md +0 -0
  20. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/prompts/specfact.03-review.md +0 -0
  21. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/prompts/specfact.04-sdd.md +0 -0
  22. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/prompts/specfact.05-enforce.md +0 -0
  23. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/prompts/specfact.06-sync.md +0 -0
  24. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/prompts/specfact.07-contracts.md +0 -0
  25. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/prompts/specfact.compare.md +0 -0
  26. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/prompts/specfact.sync-backlog.md +0 -0
  27. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/prompts/specfact.validate.md +0 -0
  28. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/schemas/deviation.schema.json +0 -0
  29. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/schemas/plan.schema.json +0 -0
  30. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/schemas/protocol.schema.json +0 -0
  31. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/templates/backlog/defaults/defect_v1.yaml +0 -0
  32. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/templates/backlog/defaults/enabler_v1.yaml +0 -0
  33. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/templates/backlog/defaults/spike_v1.yaml +0 -0
  34. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/templates/backlog/defaults/user_story_v1.yaml +0 -0
  35. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/templates/backlog/frameworks/safe/safe_feature_v1.yaml +0 -0
  36. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/templates/backlog/frameworks/scrum/user_story_v1.yaml +0 -0
  37. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/templates/backlog/personas/developer/developer_task_v1.yaml +0 -0
  38. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/templates/backlog/personas/product-owner/user_story_v1.yaml +0 -0
  39. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/templates/backlog/providers/ado/work_item_v1.yaml +0 -0
  40. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/templates/github-action.yml.j2 +0 -0
  41. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/templates/persona/architect.md.j2 +0 -0
  42. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/templates/persona/developer.md.j2 +0 -0
  43. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/templates/persona/product-owner.md.j2 +0 -0
  44. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/templates/plan.bundle.yaml.j2 +0 -0
  45. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/templates/pr-template.md.j2 +0 -0
  46. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/templates/protocol.yaml.j2 +0 -0
  47. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/resources/templates/telemetry.yaml.example +0 -0
  48. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/adapters/__init__.py +0 -0
  49. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/adapters/base.py +0 -0
  50. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/adapters/openspec.py +0 -0
  51. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/adapters/openspec_parser.py +0 -0
  52. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/adapters/registry.py +0 -0
  53. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/adapters/speckit.py +0 -0
  54. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/agents/__init__.py +0 -0
  55. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/agents/analyze_agent.py +0 -0
  56. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/agents/base.py +0 -0
  57. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/agents/plan_agent.py +0 -0
  58. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/agents/registry.py +0 -0
  59. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/agents/sync_agent.py +0 -0
  60. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/analyzers/__init__.py +0 -0
  61. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/analyzers/ambiguity_scanner.py +0 -0
  62. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/analyzers/code_analyzer.py +0 -0
  63. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/analyzers/constitution_evidence_extractor.py +0 -0
  64. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/analyzers/contract_extractor.py +0 -0
  65. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/analyzers/control_flow_analyzer.py +0 -0
  66. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/analyzers/graph_analyzer.py +0 -0
  67. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/analyzers/relationship_mapper.py +0 -0
  68. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/analyzers/requirement_extractor.py +0 -0
  69. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/analyzers/test_pattern_extractor.py +0 -0
  70. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/backlog/__init__.py +0 -0
  71. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/backlog/adapters/__init__.py +0 -0
  72. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/backlog/adapters/base.py +0 -0
  73. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/backlog/adapters/local_yaml_adapter.py +0 -0
  74. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/backlog/ai_refiner.py +0 -0
  75. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/backlog/converter.py +0 -0
  76. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/backlog/filters.py +0 -0
  77. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/backlog/format_detector.py +0 -0
  78. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/backlog/formats/__init__.py +0 -0
  79. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/backlog/formats/base.py +0 -0
  80. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/backlog/formats/markdown_format.py +0 -0
  81. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/backlog/formats/structured_format.py +0 -0
  82. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/backlog/template_detector.py +0 -0
  83. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/cli.py +0 -0
  84. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/commands/__init__.py +0 -0
  85. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/commands/analyze.py +0 -0
  86. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/commands/auth.py +0 -0
  87. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/commands/contract_cmd.py +0 -0
  88. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/commands/drift.py +0 -0
  89. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/commands/enforce.py +0 -0
  90. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/commands/generate.py +0 -0
  91. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/commands/import_cmd.py +0 -0
  92. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/commands/init.py +0 -0
  93. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/commands/migrate.py +0 -0
  94. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/commands/plan.py +0 -0
  95. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/commands/project_cmd.py +0 -0
  96. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/commands/repro.py +0 -0
  97. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/commands/sdd.py +0 -0
  98. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/commands/spec.py +0 -0
  99. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/commands/sync.py +0 -0
  100. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/commands/validate.py +0 -0
  101. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/common/__init__.py +0 -0
  102. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/common/logger_setup.py +0 -0
  103. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/common/logging_utils.py +0 -0
  104. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/common/text_utils.py +0 -0
  105. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/common/utils.py +0 -0
  106. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/comparators/__init__.py +0 -0
  107. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/comparators/plan_comparator.py +0 -0
  108. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/contracts/__init__.py +0 -0
  109. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/contracts/crosshair_props.py +0 -0
  110. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/enrichers/constitution_enricher.py +0 -0
  111. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/enrichers/plan_enricher.py +0 -0
  112. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/generators/__init__.py +0 -0
  113. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/generators/contract_generator.py +0 -0
  114. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/generators/openapi_extractor.py +0 -0
  115. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/generators/persona_exporter.py +0 -0
  116. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/generators/plan_generator.py +0 -0
  117. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/generators/protocol_generator.py +0 -0
  118. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/generators/report_generator.py +0 -0
  119. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/generators/task_generator.py +0 -0
  120. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/generators/test_to_openapi.py +0 -0
  121. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/generators/workflow_generator.py +0 -0
  122. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/importers/__init__.py +0 -0
  123. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/importers/speckit_converter.py +0 -0
  124. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/importers/speckit_scanner.py +0 -0
  125. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/integrations/__init__.py +0 -0
  126. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/integrations/specmatic.py +0 -0
  127. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/merge/__init__.py +0 -0
  128. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/merge/resolver.py +0 -0
  129. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/migrations/__init__.py +0 -0
  130. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/migrations/plan_migrator.py +0 -0
  131. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/models/__init__.py +0 -0
  132. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/models/backlog_item.py +0 -0
  133. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/models/bridge.py +0 -0
  134. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/models/capabilities.py +0 -0
  135. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/models/change.py +0 -0
  136. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/models/contract.py +0 -0
  137. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/models/deviation.py +0 -0
  138. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/models/dor_config.py +0 -0
  139. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/models/enforcement.py +0 -0
  140. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/models/persona_template.py +0 -0
  141. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/models/plan.py +0 -0
  142. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/models/project.py +0 -0
  143. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/models/protocol.py +0 -0
  144. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/models/quality.py +0 -0
  145. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/models/sdd.py +0 -0
  146. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/models/source_tracking.py +0 -0
  147. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/models/task.py +0 -0
  148. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/modes/__init__.py +0 -0
  149. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/modes/detector.py +0 -0
  150. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/modes/router.py +0 -0
  151. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/parsers/__init__.py +0 -0
  152. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/parsers/persona_importer.py +0 -0
  153. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/resources/semgrep/async.yml +0 -0
  154. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/resources/semgrep/code-quality.yml +0 -0
  155. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/resources/semgrep/feature-detection.yml +0 -0
  156. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/runtime.py +0 -0
  157. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/sync/__init__.py +0 -0
  158. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/sync/bridge_probe.py +0 -0
  159. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/sync/bridge_watch.py +0 -0
  160. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/sync/change_detector.py +0 -0
  161. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/sync/code_to_spec.py +0 -0
  162. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/sync/drift_detector.py +0 -0
  163. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/sync/repository_sync.py +0 -0
  164. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/sync/spec_to_code.py +0 -0
  165. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/sync/spec_to_tests.py +0 -0
  166. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/sync/watcher.py +0 -0
  167. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/sync/watcher_enhanced.py +0 -0
  168. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/telemetry.py +0 -0
  169. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/templates/__init__.py +0 -0
  170. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/templates/bridge_templates.py +0 -0
  171. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/templates/defaults/defect_v1.yaml +0 -0
  172. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/templates/defaults/enabler_v1.yaml +0 -0
  173. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/templates/defaults/spike_v1.yaml +0 -0
  174. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/templates/defaults/user_story_v1.yaml +0 -0
  175. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/templates/frameworks/scrum/user_story_v1.yaml +0 -0
  176. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/templates/personas/product-owner/user_story_v1.yaml +0 -0
  177. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/templates/providers/ado/work_item_v1.yaml +0 -0
  178. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/templates/registry.py +0 -0
  179. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/templates/specification_templates.py +0 -0
  180. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/__init__.py +0 -0
  181. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/acceptance_criteria.py +0 -0
  182. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/auth_tokens.py +0 -0
  183. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/bundle_loader.py +0 -0
  184. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/code_change_detector.py +0 -0
  185. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/console.py +0 -0
  186. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/content_sanitizer.py +0 -0
  187. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/context_detection.py +0 -0
  188. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/enrichment_context.py +0 -0
  189. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/enrichment_parser.py +0 -0
  190. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/env_manager.py +0 -0
  191. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/feature_keys.py +0 -0
  192. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/git.py +0 -0
  193. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/github_annotations.py +0 -0
  194. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/ide_setup.py +0 -0
  195. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/incremental_check.py +0 -0
  196. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/optional_deps.py +0 -0
  197. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/performance.py +0 -0
  198. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/progress.py +0 -0
  199. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/progressive_disclosure.py +0 -0
  200. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/prompts.py +0 -0
  201. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/sdd_discovery.py +0 -0
  202. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/source_scanner.py +0 -0
  203. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/structure.py +0 -0
  204. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/structured_io.py +0 -0
  205. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/suggestions.py +0 -0
  206. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/terminal.py +0 -0
  207. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/utils/yaml_utils.py +0 -0
  208. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/validators/__init__.py +0 -0
  209. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/validators/agile_validation.py +0 -0
  210. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/validators/change_proposal_integration.py +0 -0
  211. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/validators/cli_first_validator.py +0 -0
  212. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/validators/contract_validator.py +0 -0
  213. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/validators/fsm.py +0 -0
  214. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/validators/repro_checker.py +0 -0
  215. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/validators/schema.py +0 -0
  216. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/validators/sidecar/__init__.py +0 -0
  217. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/validators/sidecar/contract_populator.py +0 -0
  218. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/validators/sidecar/crosshair_runner.py +0 -0
  219. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/validators/sidecar/crosshair_summary.py +0 -0
  220. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/validators/sidecar/dependency_installer.py +0 -0
  221. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/validators/sidecar/framework_detector.py +0 -0
  222. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/validators/sidecar/frameworks/__init__.py +0 -0
  223. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/validators/sidecar/frameworks/base.py +0 -0
  224. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/validators/sidecar/frameworks/django.py +0 -0
  225. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/validators/sidecar/frameworks/drf.py +0 -0
  226. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/validators/sidecar/frameworks/fastapi.py +0 -0
  227. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/validators/sidecar/frameworks/flask.py +0 -0
  228. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/validators/sidecar/harness_generator.py +0 -0
  229. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/validators/sidecar/models.py +0 -0
  230. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/validators/sidecar/orchestrator.py +0 -0
  231. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/validators/sidecar/specmatic_runner.py +0 -0
  232. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/validators/sidecar/unannotated_detector.py +0 -0
  233. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/src/specfact_cli/versioning/__init__.py +0 -0
  234. {specfact_cli-0.26.0 → specfact_cli-0.26.1}/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.0
3
+ Version: 0.26.1
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "specfact-cli"
7
- version = "0.26.0"
7
+ version = "0.26.1"
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"
@@ -155,20 +155,39 @@ Display refinement results:
155
155
 
156
156
  - `assignees`: Preserved
157
157
  - `tags`: Preserved
158
- - `state`: Preserved
158
+ - `state`: Preserved (original state maintained)
159
159
  - `priority`: Preserved (if present in provider_fields)
160
160
  - `due_date`: Preserved (if present in provider_fields)
161
161
  - `story_points`: Preserved (if present in provider_fields)
162
162
  - `sprint`: Preserved (if present)
163
163
  - `release`: Preserved (if present)
164
+ - `source_state`: Preserved for cross-adapter state mapping (stored in bundle entries)
164
165
  - All other metadata: Preserved in provider_fields
165
166
 
167
+ **Cross-Adapter State Preservation**:
168
+
169
+ - When items are imported into bundles, the original `source_state` (e.g., "open", "closed", "New", "Active") is stored in `source_metadata["source_state"]`
170
+ - During cross-adapter export (e.g., GitHub → ADO), the `source_state` is used to determine the correct target state
171
+ - Generic state mapping ensures state is correctly translated between any adapter pair using OpenSpec as intermediate format
172
+ - This ensures closed GitHub issues sync to ADO as "Closed", and open GitHub issues sync to ADO as "New"
173
+
166
174
  **OpenSpec Comment Integration**:
167
175
 
168
176
  - When `--openspec-comment` is used, a structured comment is added to the backlog item
169
177
  - The comment includes: Change ID, template used, confidence score, refinement timestamp
170
178
  - Original body is preserved; comment provides OpenSpec reference for cross-sync
171
179
 
180
+ **Cross-Adapter State Mapping**:
181
+
182
+ - When refining items that will be synced across adapters (e.g., GitHub ↔ ADO), state is preserved using generic mapping
183
+ - Generic state mapping uses OpenSpec as intermediate format:
184
+ - Source adapter state → OpenSpec status → Target adapter state
185
+ - Example: GitHub "open" → OpenSpec "proposed" → ADO "New"
186
+ - Example: GitHub "closed" → OpenSpec "applied" → ADO "Closed"
187
+ - State preservation: Original `source_state` is stored in bundle entries and used during cross-adapter export
188
+ - Bidirectional mapping: Works in both directions (GitHub → ADO and ADO → GitHub)
189
+ - State mapping is automatic during `sync bridge` operations when `source_state` and `source_type` are present
190
+
172
191
  ## Architecture Note
173
192
 
174
193
  SpecFact CLI follows a CLI-first architecture:
@@ -232,6 +251,16 @@ Items updated in remote backlog:
232
251
 
233
252
  # Refine and import to OpenSpec bundle
234
253
  /specfact.backlog-refine --adapter github --repo-owner nold-ai --repo-name specfact-cli --bundle my-project --auto-bundle --state open
254
+
255
+ # Cross-adapter sync workflow: Refine GitHub → Sync to ADO (with state preservation)
256
+ /specfact.backlog-refine --adapter github --repo-owner nold-ai --repo-name specfact-cli --write --labels feature
257
+ # Then sync to ADO (state will be automatically mapped: open → New, closed → Closed)
258
+ # specfact sync bridge --adapter ado --ado-org my-org --ado-project my-project --mode bidirectional
259
+
260
+ # Cross-adapter sync workflow: Refine ADO → Sync to GitHub (with state preservation)
261
+ /specfact.backlog-refine --adapter ado --ado-org my-org --ado-project my-project --write --state Active
262
+ # Then sync to GitHub (state will be automatically mapped: New → open, Closed → closed)
263
+ # specfact sync bridge --adapter github --repo-owner my-org --repo-name my-repo --mode bidirectional
235
264
  ```
236
265
 
237
266
  ## Context
@@ -3,4 +3,4 @@ SpecFact CLI - Spec→Contract→Sentinel tool for contract-driven development.
3
3
  """
4
4
 
5
5
  # Define the package version (kept in sync with pyproject.toml and setup.py)
6
- __version__ = "0.26.0"
6
+ __version__ = "0.26.1"
@@ -9,6 +9,6 @@ This package provides command-line tools for:
9
9
  - Validating reproducibility
10
10
  """
11
11
 
12
- __version__ = "0.26.0"
12
+ __version__ = "0.26.1"
13
13
 
14
14
  __all__ = ["__version__"]
@@ -200,6 +200,12 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
200
200
 
201
201
  Note:
202
202
  This implements the tool-agnostic metadata extraction pattern for Azure DevOps.
203
+ Future backlog adapters should implement similar parsing for their tools.
204
+
205
+ Change ID extraction priority:
206
+ 1. Description footer (legacy format): *OpenSpec Change Proposal: `id`*
207
+ 2. Comments (new format): **Change ID**: `id` in OpenSpec Change Proposal Reference comment
208
+ 3. Work item ID (fallback)
203
209
  """
204
210
  if not isinstance(item_data, dict):
205
211
  msg = "ADO work item data must be dict"
@@ -256,15 +262,40 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
256
262
  if impact_match:
257
263
  impact = impact_match.group(1).strip()
258
264
 
259
- # Extract change ID from OpenSpec metadata footer or work item ID
265
+ # Extract change ID from OpenSpec metadata footer, comments, or work item ID
260
266
  change_id = None
267
+
268
+ # First, check description for OpenSpec metadata footer (legacy format)
261
269
  if description_raw:
262
270
  # Look for OpenSpec metadata footer: *OpenSpec Change Proposal: `{change_id}`*
263
271
  change_id_match = re.search(r"OpenSpec Change Proposal:\s*`([^`]+)`", description_raw, re.IGNORECASE)
264
272
  if change_id_match:
265
273
  change_id = change_id_match.group(1)
274
+
275
+ # If not found in description, check comments (new format - OpenSpec info in comments)
276
+ if not change_id:
277
+ work_item_id = item_data.get("id")
278
+ if work_item_id and self.org and self.project:
279
+ comments = self._get_work_item_comments(self.org, self.project, work_item_id)
280
+ # Look for OpenSpec Change Proposal Reference comment
281
+ openspec_patterns = [
282
+ r"\*\*Change ID\*\*[:\s]+`([a-z0-9-]+)`",
283
+ r"Change ID[:\s]+`([a-z0-9-]+)`",
284
+ r"OpenSpec Change Proposal[:\s]+`?([a-z0-9-]+)`?",
285
+ r"\*OpenSpec Change Proposal:\s*`([a-z0-9-]+)`",
286
+ ]
287
+ for comment in comments:
288
+ comment_text = comment.get("text", "") or comment.get("body", "")
289
+ for pattern in openspec_patterns:
290
+ match = re.search(pattern, comment_text, re.IGNORECASE | re.DOTALL)
291
+ if match:
292
+ change_id = match.group(1)
293
+ break
294
+ if change_id:
295
+ break
296
+
297
+ # Fallback to work item ID if still not found
266
298
  if not change_id:
267
- # Use work item ID as fallback
268
299
  change_id = str(item_data.get("id", "unknown"))
269
300
 
270
301
  # Extract status from System.State
@@ -1298,7 +1329,15 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
1298
1329
  work_item_type = self._get_work_item_type(org, project)
1299
1330
 
1300
1331
  # Map status to ADO state
1301
- ado_state = self.map_openspec_status_to_backlog(status)
1332
+ # Check if source_state and source_type are provided (from cross-adapter sync)
1333
+ source_state = proposal_data.get("source_state")
1334
+ source_type = proposal_data.get("source_type")
1335
+ if source_state and source_type and source_type != "ado":
1336
+ # Use generic cross-adapter state mapping (preserves original state from source adapter)
1337
+ ado_state = self.map_backlog_state_between_adapters(source_state, source_type, self)
1338
+ else:
1339
+ # Use OpenSpec status mapping (default behavior)
1340
+ ado_state = self.map_openspec_status_to_backlog(status)
1302
1341
 
1303
1342
  # Ensure API token is available
1304
1343
  if not self.api_token:
@@ -1436,7 +1475,15 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
1436
1475
  status = proposal_data.get("status", "proposed")
1437
1476
 
1438
1477
  # Map status to ADO state
1439
- ado_state = self.map_openspec_status_to_backlog(status)
1478
+ # Check if source_state and source_type are provided (from cross-adapter sync)
1479
+ source_state = proposal_data.get("source_state")
1480
+ source_type = proposal_data.get("source_type")
1481
+ if source_state and source_type and source_type != "ado":
1482
+ # Use generic cross-adapter state mapping (preserves original state from source adapter)
1483
+ ado_state = self.map_backlog_state_between_adapters(source_state, source_type, self)
1484
+ else:
1485
+ # Use OpenSpec status mapping (default behavior)
1486
+ ado_state = self.map_openspec_status_to_backlog(status)
1440
1487
 
1441
1488
  # Ensure API token is available
1442
1489
  if not self.api_token:
@@ -1546,7 +1593,15 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
1546
1593
  body = "\n".join(body_parts)
1547
1594
 
1548
1595
  # Map status to ADO state
1549
- ado_state = self.map_openspec_status_to_backlog(status)
1596
+ # Check if source_state and source_type are provided (from cross-adapter sync)
1597
+ source_state = proposal_data.get("source_state")
1598
+ source_type = proposal_data.get("source_type")
1599
+ if source_state and source_type and source_type != "ado":
1600
+ # Use generic cross-adapter state mapping (preserves original state from source adapter)
1601
+ ado_state = self.map_backlog_state_between_adapters(source_state, source_type, self)
1602
+ else:
1603
+ # Use OpenSpec status mapping (default behavior)
1604
+ ado_state = self.map_openspec_status_to_backlog(status)
1550
1605
 
1551
1606
  # Ensure API token is available
1552
1607
  if not self.api_token:
@@ -1980,6 +2035,37 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
1980
2035
  self.console.log(f"[bold yellow]Warning:[/bold yellow] Error checking branch existence: {e}")
1981
2036
  return False
1982
2037
 
2038
+ def _get_work_item_comments(self, org: str, project: str, work_item_id: int) -> list[dict[str, Any]]:
2039
+ """
2040
+ Fetch comments for an Azure DevOps work item.
2041
+
2042
+ Args:
2043
+ org: Azure DevOps organization
2044
+ project: Azure DevOps project
2045
+ work_item_id: Work item ID
2046
+
2047
+ Returns:
2048
+ List of comment dicts with 'text' or 'body' field, or empty list on error
2049
+ """
2050
+ if not self.api_token:
2051
+ return []
2052
+
2053
+ url = f"{self.base_url}/{org}/{project}/_apis/wit/workitems/{work_item_id}/comments?api-version=7.1"
2054
+ headers = {
2055
+ "Accept": "application/json",
2056
+ **self._auth_headers(),
2057
+ }
2058
+
2059
+ try:
2060
+ response = requests.get(url, headers=headers, timeout=30)
2061
+ response.raise_for_status()
2062
+ # ADO API returns comments in a 'comments' array within the response
2063
+ response_data = response.json()
2064
+ return response_data.get("comments", [])
2065
+ except requests.RequestException:
2066
+ # Return empty list on error - comments are optional
2067
+ return []
2068
+
1983
2069
  @beartype
1984
2070
  @require(lambda org: isinstance(org, str) and org, "Organization must be non-empty string")
1985
2071
  @require(lambda project: isinstance(project, str) and project, "Project must be non-empty string")
@@ -74,6 +74,72 @@ class BacklogAdapterMixin(ABC):
74
74
  tool-specific status mapping logic.
75
75
  """
76
76
 
77
+ @beartype
78
+ @require(
79
+ lambda source_state: isinstance(source_state, str) and len(source_state) > 0,
80
+ "Source state must be non-empty string",
81
+ )
82
+ @require(
83
+ lambda source_adapter_type: isinstance(source_adapter_type, str) and len(source_adapter_type) > 0,
84
+ "Source adapter type must be non-empty string",
85
+ )
86
+ @require(
87
+ lambda target_adapter: isinstance(target_adapter, BacklogAdapterMixin),
88
+ "Target adapter must implement BacklogAdapterMixin",
89
+ )
90
+ @ensure(lambda result: isinstance(result, str), "Must return status string")
91
+ def map_backlog_state_between_adapters(
92
+ self, source_state: str, source_adapter_type: str, target_adapter: BacklogAdapterMixin
93
+ ) -> str:
94
+ """
95
+ Map backlog state from one adapter to another using OpenSpec as intermediate format.
96
+
97
+ This method provides generic cross-adapter state mapping by:
98
+ 1. Getting the source adapter instance
99
+ 2. Mapping source state to OpenSpec status using source adapter's mapping
100
+ 3. Mapping OpenSpec status to target state using target adapter's mapping
101
+
102
+ Args:
103
+ source_state: State from source adapter (e.g., "open", "closed", "New", "Active")
104
+ source_adapter_type: Source adapter type (e.g., "github", "ado", "jira")
105
+ target_adapter: Target adapter instance (must implement BacklogAdapterMixin)
106
+
107
+ Returns:
108
+ Target adapter state string
109
+
110
+ Note:
111
+ This is a generic method that works for any adapter pair by using OpenSpec
112
+ as the intermediate format. It requires the source adapter to be registered
113
+ in AdapterRegistry to retrieve its mapping methods.
114
+ """
115
+ from specfact_cli.adapters.registry import AdapterRegistry
116
+
117
+ # Get source adapter instance to use its mapping methods
118
+ source_adapter = AdapterRegistry.get_adapter(source_adapter_type)
119
+ if not source_adapter or not isinstance(source_adapter, BacklogAdapterMixin):
120
+ # Fallback: if source adapter not found, try to map directly
121
+ # This handles cases where source adapter might not be registered
122
+ # In this case, we'll use the target adapter's default mapping
123
+ openspec_status = "proposed" # Default fallback
124
+ else:
125
+ # Step 1: Map source state to OpenSpec status using source adapter
126
+ openspec_status = source_adapter.map_backlog_status_to_openspec(source_state)
127
+
128
+ # Step 2: Map OpenSpec status to target state using target adapter
129
+ # Special handling for GitHub adapter: use issue state method instead of labels
130
+ if hasattr(target_adapter, "map_openspec_status_to_issue_state"):
131
+ # GitHub adapter: use issue state mapping (open/closed)
132
+ return target_adapter.map_openspec_status_to_issue_state(openspec_status)
133
+
134
+ target_state = target_adapter.map_openspec_status_to_backlog(openspec_status)
135
+
136
+ # Handle list return type (some adapters return lists)
137
+ if isinstance(target_state, list):
138
+ # Use first element if list (typically the primary state)
139
+ return target_state[0] if target_state else "New"
140
+
141
+ return target_state
142
+
77
143
  @abstractmethod
78
144
  @beartype
79
145
  @require(lambda item_data: isinstance(item_data, dict), "Item data must be dict")
@@ -169,6 +169,30 @@ class GitHubAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
169
169
  # Default: treat as proposed
170
170
  return "proposed"
171
171
 
172
+ @beartype
173
+ @require(lambda status: isinstance(status, str) and len(status) > 0, "Status must be non-empty string")
174
+ @ensure(lambda result: isinstance(result, str), "Must return issue state string")
175
+ def map_openspec_status_to_issue_state(self, status: str) -> str:
176
+ """
177
+ Map OpenSpec change status to GitHub issue state (open/closed).
178
+
179
+ Args:
180
+ status: OpenSpec change status (proposed, in-progress, applied, deprecated, discarded)
181
+
182
+ Returns:
183
+ GitHub issue state: "open" or "closed"
184
+
185
+ Note:
186
+ This method is used for cross-adapter state mapping where we need the
187
+ actual issue state, not labels. For label mapping, use map_openspec_status_to_backlog().
188
+ """
189
+ # Map OpenSpec status to GitHub issue state
190
+ # "applied", "deprecated", "discarded" → closed
191
+ # "proposed", "in-progress" → open
192
+ if status in ("applied", "deprecated", "discarded"):
193
+ return "closed"
194
+ return "open"
195
+
172
196
  @beartype
173
197
  @require(lambda status: isinstance(status, str) and len(status) > 0, "Status must be non-empty string")
174
198
  @ensure(lambda result: isinstance(result, list), "Must return list of label strings")
@@ -185,6 +209,8 @@ class GitHubAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
185
209
  Note:
186
210
  This implements the tool-agnostic status mapping pattern for GitHub.
187
211
  Future backlog adapters should implement similar mappings for their tools.
212
+
213
+ For cross-adapter state mapping (issue state, not labels), use map_openspec_status_to_issue_state().
188
214
  """
189
215
  labels = ["openspec"]
190
216
 
@@ -210,6 +236,7 @@ class GitHubAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
210
236
  - Title (from issue title)
211
237
  - Description (What Changes section)
212
238
  - Rationale (Why section)
239
+ - Change ID (from body footer or comments)
213
240
  - Other optional fields (timeline, owner, stakeholders, dependencies)
214
241
 
215
242
  Args:
@@ -220,6 +247,7 @@ class GitHubAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
220
247
  - title: str
221
248
  - description: str (What Changes section)
222
249
  - rationale: str (Why section)
250
+ - change_id: str (extracted from body footer or comments)
223
251
  - status: str (mapped to OpenSpec status)
224
252
  - Other optional fields
225
253
 
@@ -229,6 +257,11 @@ class GitHubAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
229
257
  Note:
230
258
  This implements the tool-agnostic metadata extraction pattern for GitHub.
231
259
  Future backlog adapters should implement similar parsing for their tools.
260
+
261
+ Change ID extraction priority:
262
+ 1. Body footer (legacy format): *OpenSpec Change Proposal: `id`*
263
+ 2. Comments (new format): **Change ID**: `id` in OpenSpec Change Proposal Reference comment
264
+ 3. Issue number (fallback)
232
265
  """
233
266
  if not isinstance(item_data, dict):
234
267
  msg = "GitHub issue data must be dict"
@@ -278,15 +311,41 @@ class GitHubAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
278
311
  if impact_match:
279
312
  impact = impact_match.group(1).strip()
280
313
 
281
- # Extract change ID from OpenSpec metadata footer or issue number
314
+ # Extract change ID from OpenSpec metadata footer, comments, or issue number
282
315
  change_id = None
316
+
317
+ # First, check body for OpenSpec metadata footer (legacy format)
283
318
  if body:
284
319
  # Look for OpenSpec metadata footer: *OpenSpec Change Proposal: `{change_id}`*
285
320
  change_id_match = re.search(r"OpenSpec Change Proposal:\s*`([^`]+)`", body, re.IGNORECASE)
286
321
  if change_id_match:
287
322
  change_id = change_id_match.group(1)
323
+
324
+ # If not found in body, check comments (new format - OpenSpec info in comments)
325
+ if not change_id:
326
+ issue_number = item_data.get("number")
327
+ if issue_number and self.repo_owner and self.repo_name:
328
+ comments = self._get_issue_comments(self.repo_owner, self.repo_name, issue_number)
329
+ # Look for OpenSpec Change Proposal Reference comment
330
+ # Pattern 1: Structured comment format with "**Change ID**: `id`"
331
+ openspec_patterns = [
332
+ r"\*\*Change ID\*\*[:\s]+`([a-z0-9-]+)`",
333
+ r"Change ID[:\s]+`([a-z0-9-]+)`",
334
+ r"OpenSpec Change Proposal[:\s]+`?([a-z0-9-]+)`?",
335
+ r"\*OpenSpec Change Proposal:\s*`([a-z0-9-]+)`",
336
+ ]
337
+ for comment in comments:
338
+ comment_body = comment.get("body", "")
339
+ for pattern in openspec_patterns:
340
+ match = re.search(pattern, comment_body, re.IGNORECASE | re.DOTALL)
341
+ if match:
342
+ change_id = match.group(1)
343
+ break
344
+ if change_id:
345
+ break
346
+
347
+ # Fallback to issue number if still not found
288
348
  if not change_id:
289
- # Use issue number as fallback
290
349
  change_id = str(item_data.get("number", "unknown"))
291
350
 
292
351
  # Extract status from labels
@@ -491,12 +550,17 @@ class GitHubAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
491
550
  source_metadata.setdefault("source_repo", source_repo)
492
551
 
493
552
  entry_id = artifact_path.get("number") or artifact_path.get("id")
553
+ # Extract GitHub issue state (open/closed) for cross-adapter sync state preservation
554
+ github_state = artifact_path.get("state", "open").lower()
494
555
  entry = {
495
556
  "source_id": str(entry_id) if entry_id is not None else None,
496
557
  "source_url": artifact_path.get("html_url") or artifact_path.get("url") or "",
497
558
  "source_type": "github",
498
559
  "source_repo": source_repo or "",
499
- "source_metadata": {"last_synced_status": proposal.status},
560
+ "source_metadata": {
561
+ "last_synced_status": proposal.status,
562
+ "source_state": github_state, # Preserve GitHub state for cross-adapter sync
563
+ },
500
564
  }
501
565
  entries = source_metadata.get("backlog_entries")
502
566
  if not isinstance(entries, list):
@@ -1011,6 +1075,17 @@ class GitHubAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
1011
1075
 
1012
1076
  body = "\n".join(body_parts)
1013
1077
 
1078
+ # Check for API token before making request
1079
+ if not self.api_token:
1080
+ msg = (
1081
+ "GitHub API token required to create issues. Options:\n"
1082
+ " 1. Set GITHUB_TOKEN environment variable\n"
1083
+ " 2. Use --github-token option\n"
1084
+ " 3. Use GitHub CLI authentication (gh auth login)\n"
1085
+ " 4. Store token via specfact auth github"
1086
+ )
1087
+ raise ValueError(msg)
1088
+
1014
1089
  # Create issue via GitHub API
1015
1090
  url = f"{self.base_url}/repos/{repo_owner}/{repo_name}/issues"
1016
1091
  headers = {
@@ -1018,9 +1093,24 @@ class GitHubAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
1018
1093
  "Accept": "application/vnd.github.v3+json",
1019
1094
  }
1020
1095
  # Determine issue state based on proposal status
1021
- # Completed proposals (applied, deprecated, discarded) should be closed
1022
- should_close = status in ("applied", "deprecated", "discarded")
1023
- issue_state = "closed" if should_close else "open"
1096
+ # Check if source_state and source_type are provided (from cross-adapter sync)
1097
+ source_state = proposal_data.get("source_state")
1098
+ source_type = proposal_data.get("source_type")
1099
+ if source_state and source_type and source_type != "github":
1100
+ # Use generic cross-adapter state mapping (preserves original state from source adapter)
1101
+ from specfact_cli.adapters.registry import AdapterRegistry
1102
+
1103
+ source_adapter = AdapterRegistry.get_adapter(source_type)
1104
+ if source_adapter and hasattr(source_adapter, "map_backlog_state_between_adapters"):
1105
+ issue_state = source_adapter.map_backlog_state_between_adapters(source_state, source_type, self)
1106
+ else:
1107
+ # Fallback: map via OpenSpec status
1108
+ should_close = status in ("applied", "deprecated", "discarded")
1109
+ issue_state = "closed" if should_close else "open"
1110
+ else:
1111
+ # Use OpenSpec status mapping (default behavior)
1112
+ should_close = status in ("applied", "deprecated", "discarded")
1113
+ issue_state = "closed" if should_close else "open"
1024
1114
 
1025
1115
  # Map status to GitHub state_reason
1026
1116
  state_reason = None
@@ -1120,7 +1210,23 @@ class GitHubAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
1120
1210
  title = proposal_data.get("title", "Untitled")
1121
1211
 
1122
1212
  # Map status to GitHub issue state and comment
1123
- should_close = status in ("applied", "deprecated", "discarded")
1213
+ # Check if source_state and source_type are provided (from cross-adapter sync)
1214
+ source_state = proposal_data.get("source_state")
1215
+ source_type = proposal_data.get("source_type")
1216
+ if source_state and source_type and source_type != "github":
1217
+ # Use generic cross-adapter state mapping (preserves original state from source adapter)
1218
+ from specfact_cli.adapters.registry import AdapterRegistry
1219
+
1220
+ source_adapter = AdapterRegistry.get_adapter(source_type)
1221
+ if source_adapter and hasattr(source_adapter, "map_backlog_state_between_adapters"):
1222
+ issue_state = source_adapter.map_backlog_state_between_adapters(source_state, source_type, self)
1223
+ should_close = issue_state == "closed"
1224
+ else:
1225
+ # Fallback: map via OpenSpec status
1226
+ should_close = status in ("applied", "deprecated", "discarded")
1227
+ else:
1228
+ # Use OpenSpec status mapping (default behavior)
1229
+ should_close = status in ("applied", "deprecated", "discarded")
1124
1230
  source_tracking = proposal_data.get("source_tracking", {})
1125
1231
  # Note: code_repo_path not available in _update_issue_status context
1126
1232
  comment_text = self._get_status_comment(status, title, source_tracking, None)
@@ -1161,6 +1267,35 @@ class GitHubAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
1161
1267
  console.print(f"[bold red]✗[/bold red] {msg}")
1162
1268
  raise
1163
1269
 
1270
+ def _get_issue_comments(self, repo_owner: str, repo_name: str, issue_number: int) -> list[dict[str, Any]]:
1271
+ """
1272
+ Fetch comments for a GitHub issue.
1273
+
1274
+ Args:
1275
+ repo_owner: GitHub repository owner
1276
+ repo_name: GitHub repository name
1277
+ issue_number: Issue number
1278
+
1279
+ Returns:
1280
+ List of comment dicts with 'body' field, or empty list on error
1281
+ """
1282
+ if not self.api_token:
1283
+ return []
1284
+
1285
+ url = f"{self.base_url}/repos/{repo_owner}/{repo_name}/issues/{issue_number}/comments"
1286
+ headers = {
1287
+ "Authorization": f"token {self.api_token}",
1288
+ "Accept": "application/vnd.github.v3+json",
1289
+ }
1290
+
1291
+ try:
1292
+ response = requests.get(url, headers=headers, timeout=30)
1293
+ response.raise_for_status()
1294
+ return response.json()
1295
+ except requests.RequestException:
1296
+ # Return empty list on error - comments are optional
1297
+ return []
1298
+
1164
1299
  def _add_issue_comment(self, repo_owner: str, repo_name: str, issue_number: int, comment: str) -> None:
1165
1300
  """
1166
1301
  Add comment to GitHub issue.
@@ -2419,7 +2554,7 @@ class GitHubAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
2419
2554
  @beartype
2420
2555
  @require(lambda item: isinstance(item, BacklogItem), "Item must be BacklogItem")
2421
2556
  @require(
2422
- lambda update_fields: update_fields is None or isinstance(update_fields, list),
2557
+ lambda item, update_fields: update_fields is None or isinstance(update_fields, list),
2423
2558
  "Update fields must be None or list",
2424
2559
  )
2425
2560
  @ensure(lambda result: isinstance(result, BacklogItem), "Must return BacklogItem")
@@ -97,6 +97,35 @@ def _apply_filters(
97
97
  return filtered
98
98
 
99
99
 
100
+ def _extract_openspec_change_id(body: str) -> str | None:
101
+ """
102
+ Extract OpenSpec change proposal ID from issue body.
103
+
104
+ Looks for patterns like:
105
+ - *OpenSpec Change Proposal: `id`*
106
+ - OpenSpec Change Proposal: `id`
107
+ - OpenSpec.*proposal: `id`
108
+
109
+ Args:
110
+ body: Issue body text
111
+
112
+ Returns:
113
+ Change proposal ID if found, None otherwise
114
+ """
115
+ import re
116
+
117
+ openspec_patterns = [
118
+ r"OpenSpec Change Proposal[:\s]+`?([a-z0-9-]+)`?",
119
+ r"\*OpenSpec Change Proposal:\s*`([a-z0-9-]+)`",
120
+ r"OpenSpec.*proposal[:\s]+`?([a-z0-9-]+)`?",
121
+ ]
122
+ for pattern in openspec_patterns:
123
+ match = re.search(pattern, body, re.IGNORECASE)
124
+ if match:
125
+ return match.group(1)
126
+ return None
127
+
128
+
100
129
  def _build_adapter_kwargs(
101
130
  adapter: str,
102
131
  repo_owner: str | None = None,
@@ -623,8 +652,12 @@ def refine(
623
652
 
624
653
  # Add OpenSpec comment if requested
625
654
  if openspec_comment:
655
+ # Extract OpenSpec change proposal ID from original body if present
656
+ original_body = item.body_markdown or ""
657
+ openspec_change_id = _extract_openspec_change_id(original_body)
658
+
626
659
  # Generate OpenSpec change proposal reference
627
- change_id = f"backlog-refine-{item.id}"
660
+ change_id = openspec_change_id or f"backlog-refine-{item.id}"
628
661
  comment_text = (
629
662
  f"## OpenSpec Change Proposal Reference\n\n"
630
663
  f"This backlog item was refined using SpecFact CLI template-driven refinement.\n\n"
@@ -671,8 +704,12 @@ def refine(
671
704
 
672
705
  # Add OpenSpec comment if requested
673
706
  if openspec_comment:
707
+ # Extract OpenSpec change proposal ID from original body if present
708
+ original_body = item.body_markdown or ""
709
+ openspec_change_id = _extract_openspec_change_id(original_body)
710
+
674
711
  # Generate OpenSpec change proposal reference
675
- change_id = f"backlog-refine-{item.id}"
712
+ change_id = openspec_change_id or f"backlog-refine-{item.id}"
676
713
  comment_text = (
677
714
  f"## OpenSpec Change Proposal Reference\n\n"
678
715
  f"This backlog item was refined using SpecFact CLI template-driven refinement.\n\n"
@@ -1912,6 +1912,26 @@ class BridgeSync:
1912
1912
  "source_tracking": entries,
1913
1913
  }
1914
1914
 
1915
+ # Extract source state from backlog entries (for cross-adapter sync state preservation)
1916
+ # Check for source backlog entry from a different adapter (generic approach)
1917
+ source_state = None
1918
+ source_type = None
1919
+ for entry in entries:
1920
+ if isinstance(entry, dict):
1921
+ entry_type = entry.get("source_type", "").lower()
1922
+ # Look for entry from a different adapter (not the target adapter)
1923
+ if entry_type and entry_type != adapter_type.lower():
1924
+ source_metadata = entry.get("source_metadata", {})
1925
+ entry_source_state = source_metadata.get("source_state")
1926
+ if entry_source_state:
1927
+ source_state = entry_source_state
1928
+ source_type = entry_type
1929
+ break
1930
+
1931
+ if source_state and source_type:
1932
+ proposal_dict["source_state"] = source_state
1933
+ proposal_dict["source_type"] = source_type
1934
+
1915
1935
  if isinstance(proposal.source_tracking.source_metadata, dict):
1916
1936
  raw_title = proposal.source_tracking.source_metadata.get("raw_title")
1917
1937
  raw_body = proposal.source_tracking.source_metadata.get("raw_body")
File without changes
File without changes
File without changes