specfact-cli 0.26.2__tar.gz → 0.26.5__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.2 → specfact_cli-0.26.5}/PKG-INFO +1 -1
  2. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/pyproject.toml +1 -1
  3. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/__init__.py +1 -1
  4. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/__init__.py +1 -1
  5. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/adapters/ado.py +140 -17
  6. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/cli.py +9 -1
  7. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/commands/auth.py +190 -6
  8. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/commands/init.py +20 -19
  9. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/runtime.py +31 -0
  10. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/.gitignore +0 -0
  11. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/LICENSE.md +0 -0
  12. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/README.md +0 -0
  13. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/mappings/node-async.yaml +0 -0
  14. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/mappings/python-async.yaml +0 -0
  15. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/mappings/speckit-default.yaml +0 -0
  16. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/prompts/shared/cli-enforcement.md +0 -0
  17. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/prompts/specfact.01-import.md +0 -0
  18. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/prompts/specfact.02-plan.md +0 -0
  19. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/prompts/specfact.03-review.md +0 -0
  20. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/prompts/specfact.04-sdd.md +0 -0
  21. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/prompts/specfact.05-enforce.md +0 -0
  22. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/prompts/specfact.06-sync.md +0 -0
  23. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/prompts/specfact.07-contracts.md +0 -0
  24. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/prompts/specfact.backlog-refine.md +0 -0
  25. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/prompts/specfact.compare.md +0 -0
  26. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/prompts/specfact.sync-backlog.md +0 -0
  27. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/prompts/specfact.validate.md +0 -0
  28. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/schemas/deviation.schema.json +0 -0
  29. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/schemas/plan.schema.json +0 -0
  30. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/schemas/protocol.schema.json +0 -0
  31. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/templates/backlog/defaults/defect_v1.yaml +0 -0
  32. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/templates/backlog/defaults/enabler_v1.yaml +0 -0
  33. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/templates/backlog/defaults/spike_v1.yaml +0 -0
  34. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/templates/backlog/defaults/user_story_v1.yaml +0 -0
  35. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/templates/backlog/frameworks/safe/safe_feature_v1.yaml +0 -0
  36. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/templates/backlog/frameworks/scrum/user_story_v1.yaml +0 -0
  37. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/templates/backlog/personas/developer/developer_task_v1.yaml +0 -0
  38. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/templates/backlog/personas/product-owner/user_story_v1.yaml +0 -0
  39. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/templates/backlog/providers/ado/work_item_v1.yaml +0 -0
  40. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/templates/github-action.yml.j2 +0 -0
  41. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/templates/persona/architect.md.j2 +0 -0
  42. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/templates/persona/developer.md.j2 +0 -0
  43. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/templates/persona/product-owner.md.j2 +0 -0
  44. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/templates/plan.bundle.yaml.j2 +0 -0
  45. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/templates/pr-template.md.j2 +0 -0
  46. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/templates/protocol.yaml.j2 +0 -0
  47. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/templates/telemetry.yaml.example +0 -0
  48. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/adapters/__init__.py +0 -0
  49. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/adapters/backlog_base.py +0 -0
  50. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/adapters/base.py +0 -0
  51. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/adapters/github.py +0 -0
  52. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/adapters/openspec.py +0 -0
  53. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/adapters/openspec_parser.py +0 -0
  54. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/adapters/registry.py +0 -0
  55. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/adapters/speckit.py +0 -0
  56. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/agents/__init__.py +0 -0
  57. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/agents/analyze_agent.py +0 -0
  58. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/agents/base.py +0 -0
  59. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/agents/plan_agent.py +0 -0
  60. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/agents/registry.py +0 -0
  61. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/agents/sync_agent.py +0 -0
  62. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/analyzers/__init__.py +0 -0
  63. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/analyzers/ambiguity_scanner.py +0 -0
  64. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/analyzers/code_analyzer.py +0 -0
  65. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/analyzers/constitution_evidence_extractor.py +0 -0
  66. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/analyzers/contract_extractor.py +0 -0
  67. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/analyzers/control_flow_analyzer.py +0 -0
  68. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/analyzers/graph_analyzer.py +0 -0
  69. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/analyzers/relationship_mapper.py +0 -0
  70. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/analyzers/requirement_extractor.py +0 -0
  71. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/analyzers/test_pattern_extractor.py +0 -0
  72. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/backlog/__init__.py +0 -0
  73. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/backlog/adapters/__init__.py +0 -0
  74. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/backlog/adapters/base.py +0 -0
  75. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/backlog/adapters/local_yaml_adapter.py +0 -0
  76. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/backlog/ai_refiner.py +0 -0
  77. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/backlog/converter.py +0 -0
  78. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/backlog/filters.py +0 -0
  79. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/backlog/format_detector.py +0 -0
  80. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/backlog/formats/__init__.py +0 -0
  81. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/backlog/formats/base.py +0 -0
  82. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/backlog/formats/markdown_format.py +0 -0
  83. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/backlog/formats/structured_format.py +0 -0
  84. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/backlog/template_detector.py +0 -0
  85. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/commands/__init__.py +0 -0
  86. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/commands/analyze.py +0 -0
  87. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/commands/backlog_commands.py +0 -0
  88. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/commands/contract_cmd.py +0 -0
  89. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/commands/drift.py +0 -0
  90. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/commands/enforce.py +0 -0
  91. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/commands/generate.py +0 -0
  92. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/commands/import_cmd.py +0 -0
  93. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/commands/migrate.py +0 -0
  94. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/commands/plan.py +0 -0
  95. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/commands/project_cmd.py +0 -0
  96. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/commands/repro.py +0 -0
  97. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/commands/sdd.py +0 -0
  98. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/commands/spec.py +0 -0
  99. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/commands/sync.py +0 -0
  100. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/commands/validate.py +0 -0
  101. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/common/__init__.py +0 -0
  102. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/common/logger_setup.py +0 -0
  103. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/common/logging_utils.py +0 -0
  104. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/common/text_utils.py +0 -0
  105. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/common/utils.py +0 -0
  106. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/comparators/__init__.py +0 -0
  107. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/comparators/plan_comparator.py +0 -0
  108. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/contracts/__init__.py +0 -0
  109. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/contracts/crosshair_props.py +0 -0
  110. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/enrichers/constitution_enricher.py +0 -0
  111. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/enrichers/plan_enricher.py +0 -0
  112. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/generators/__init__.py +0 -0
  113. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/generators/contract_generator.py +0 -0
  114. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/generators/openapi_extractor.py +0 -0
  115. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/generators/persona_exporter.py +0 -0
  116. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/generators/plan_generator.py +0 -0
  117. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/generators/protocol_generator.py +0 -0
  118. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/generators/report_generator.py +0 -0
  119. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/generators/task_generator.py +0 -0
  120. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/generators/test_to_openapi.py +0 -0
  121. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/generators/workflow_generator.py +0 -0
  122. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/importers/__init__.py +0 -0
  123. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/importers/speckit_converter.py +0 -0
  124. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/importers/speckit_scanner.py +0 -0
  125. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/integrations/__init__.py +0 -0
  126. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/integrations/specmatic.py +0 -0
  127. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/merge/__init__.py +0 -0
  128. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/merge/resolver.py +0 -0
  129. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/migrations/__init__.py +0 -0
  130. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/migrations/plan_migrator.py +0 -0
  131. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/models/__init__.py +0 -0
  132. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/models/backlog_item.py +0 -0
  133. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/models/bridge.py +0 -0
  134. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/models/capabilities.py +0 -0
  135. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/models/change.py +0 -0
  136. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/models/contract.py +0 -0
  137. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/models/deviation.py +0 -0
  138. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/models/dor_config.py +0 -0
  139. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/models/enforcement.py +0 -0
  140. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/models/persona_template.py +0 -0
  141. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/models/plan.py +0 -0
  142. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/models/project.py +0 -0
  143. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/models/protocol.py +0 -0
  144. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/models/quality.py +0 -0
  145. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/models/sdd.py +0 -0
  146. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/models/source_tracking.py +0 -0
  147. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/models/task.py +0 -0
  148. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/modes/__init__.py +0 -0
  149. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/modes/detector.py +0 -0
  150. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/modes/router.py +0 -0
  151. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/parsers/__init__.py +0 -0
  152. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/parsers/persona_importer.py +0 -0
  153. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/resources/semgrep/async.yml +0 -0
  154. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/resources/semgrep/code-quality.yml +0 -0
  155. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/resources/semgrep/feature-detection.yml +0 -0
  156. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/sync/__init__.py +0 -0
  157. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/sync/bridge_probe.py +0 -0
  158. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/sync/bridge_sync.py +0 -0
  159. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/sync/bridge_watch.py +0 -0
  160. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/sync/change_detector.py +0 -0
  161. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/sync/code_to_spec.py +0 -0
  162. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/sync/drift_detector.py +0 -0
  163. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/sync/repository_sync.py +0 -0
  164. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/sync/spec_to_code.py +0 -0
  165. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/sync/spec_to_tests.py +0 -0
  166. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/sync/watcher.py +0 -0
  167. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/sync/watcher_enhanced.py +0 -0
  168. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/telemetry.py +0 -0
  169. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/templates/__init__.py +0 -0
  170. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/templates/bridge_templates.py +0 -0
  171. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/templates/defaults/defect_v1.yaml +0 -0
  172. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/templates/defaults/enabler_v1.yaml +0 -0
  173. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/templates/defaults/spike_v1.yaml +0 -0
  174. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/templates/defaults/user_story_v1.yaml +0 -0
  175. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/templates/frameworks/scrum/user_story_v1.yaml +0 -0
  176. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/templates/personas/product-owner/user_story_v1.yaml +0 -0
  177. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/templates/providers/ado/work_item_v1.yaml +0 -0
  178. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/templates/registry.py +0 -0
  179. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/templates/specification_templates.py +0 -0
  180. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/__init__.py +0 -0
  181. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/acceptance_criteria.py +0 -0
  182. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/auth_tokens.py +0 -0
  183. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/bundle_loader.py +0 -0
  184. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/code_change_detector.py +0 -0
  185. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/console.py +0 -0
  186. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/content_sanitizer.py +0 -0
  187. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/context_detection.py +0 -0
  188. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/enrichment_context.py +0 -0
  189. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/enrichment_parser.py +0 -0
  190. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/env_manager.py +0 -0
  191. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/feature_keys.py +0 -0
  192. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/git.py +0 -0
  193. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/github_annotations.py +0 -0
  194. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/ide_setup.py +0 -0
  195. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/incremental_check.py +0 -0
  196. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/optional_deps.py +0 -0
  197. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/performance.py +0 -0
  198. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/progress.py +0 -0
  199. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/progressive_disclosure.py +0 -0
  200. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/prompts.py +0 -0
  201. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/sdd_discovery.py +0 -0
  202. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/source_scanner.py +0 -0
  203. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/structure.py +0 -0
  204. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/structured_io.py +0 -0
  205. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/suggestions.py +0 -0
  206. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/terminal.py +0 -0
  207. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/yaml_utils.py +0 -0
  208. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/validators/__init__.py +0 -0
  209. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/validators/agile_validation.py +0 -0
  210. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/validators/change_proposal_integration.py +0 -0
  211. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/validators/cli_first_validator.py +0 -0
  212. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/validators/contract_validator.py +0 -0
  213. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/validators/fsm.py +0 -0
  214. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/validators/repro_checker.py +0 -0
  215. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/validators/schema.py +0 -0
  216. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/validators/sidecar/__init__.py +0 -0
  217. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/validators/sidecar/contract_populator.py +0 -0
  218. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/validators/sidecar/crosshair_runner.py +0 -0
  219. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/validators/sidecar/crosshair_summary.py +0 -0
  220. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/validators/sidecar/dependency_installer.py +0 -0
  221. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/validators/sidecar/framework_detector.py +0 -0
  222. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/validators/sidecar/frameworks/__init__.py +0 -0
  223. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/validators/sidecar/frameworks/base.py +0 -0
  224. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/validators/sidecar/frameworks/django.py +0 -0
  225. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/validators/sidecar/frameworks/drf.py +0 -0
  226. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/validators/sidecar/frameworks/fastapi.py +0 -0
  227. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/validators/sidecar/frameworks/flask.py +0 -0
  228. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/validators/sidecar/harness_generator.py +0 -0
  229. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/validators/sidecar/models.py +0 -0
  230. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/validators/sidecar/orchestrator.py +0 -0
  231. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/validators/sidecar/specmatic_runner.py +0 -0
  232. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/validators/sidecar/unannotated_detector.py +0 -0
  233. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/versioning/__init__.py +0 -0
  234. {specfact_cli-0.26.2 → specfact_cli-0.26.5}/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.2
3
+ Version: 0.26.5
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.2"
7
+ version = "0.26.5"
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
  # Define the package version (kept in sync with pyproject.toml and setup.py)
6
- __version__ = "0.26.2"
6
+ __version__ = "0.26.5"
@@ -9,6 +9,6 @@ This package provides command-line tools for:
9
9
  - Validating reproducibility
10
10
  """
11
11
 
12
- __version__ = "0.26.2"
12
+ __version__ = "0.26.5"
13
13
 
14
14
  __all__ = ["__version__"]
@@ -29,7 +29,8 @@ from specfact_cli.models.backlog_item import BacklogItem
29
29
  from specfact_cli.models.bridge import BridgeConfig
30
30
  from specfact_cli.models.capabilities import ToolCapabilities
31
31
  from specfact_cli.models.change import ChangeProposal, ChangeTracking
32
- from specfact_cli.utils.auth_tokens import get_token
32
+ from specfact_cli.runtime import debug_print
33
+ from specfact_cli.utils.auth_tokens import get_token, set_token
33
34
 
34
35
 
35
36
  console = Console()
@@ -75,10 +76,45 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
75
76
  elif os.environ.get("AZURE_DEVOPS_TOKEN"):
76
77
  self.api_token = os.environ.get("AZURE_DEVOPS_TOKEN")
77
78
  self.auth_scheme = "basic"
78
- elif stored_token := get_token("azure-devops"):
79
+ elif stored_token := get_token("azure-devops", allow_expired=False):
80
+ # Valid, non-expired token found
79
81
  self.api_token = stored_token.get("access_token")
80
82
  token_type = (stored_token.get("token_type") or "bearer").lower()
81
83
  self.auth_scheme = "bearer" if token_type == "bearer" else "basic"
84
+ elif stored_token_expired := get_token("azure-devops", allow_expired=True):
85
+ # Token exists but is expired - try to refresh using persistent cache
86
+ expires_at = stored_token_expired.get("expires_at", "unknown")
87
+ token_type = (stored_token_expired.get("token_type") or "bearer").lower()
88
+ if token_type == "bearer":
89
+ # OAuth token expired - try automatic refresh using persistent cache (like Azure CLI)
90
+ refreshed_token = self._try_refresh_oauth_token()
91
+ if refreshed_token:
92
+ self.api_token = refreshed_token.get("access_token")
93
+ self.auth_scheme = "bearer"
94
+ # Update stored token with refreshed token
95
+ set_token("azure-devops", refreshed_token)
96
+ debug_print(f"[dim]OAuth token automatically refreshed (was expired at {expires_at})[/dim]")
97
+ else:
98
+ # Refresh failed - provide helpful guidance
99
+ console.print(
100
+ f"[yellow]⚠[/yellow] Stored OAuth token expired at {expires_at}. "
101
+ "Attempting automatic refresh..."
102
+ )
103
+ console.print("[yellow]⚠[/yellow] Automatic refresh failed. OAuth tokens expire after ~1 hour.")
104
+ console.print(
105
+ "[dim]Options:[/dim]\n"
106
+ " 1. Use a Personal Access Token (PAT) with longer expiration (up to 1 year):\n"
107
+ " - Create PAT: https://dev.azure.com/{org}/_usersSettings/tokens\n"
108
+ " - Store PAT: specfact auth azure-devops --pat your_pat_token\n"
109
+ " 2. Re-authenticate: specfact auth azure-devops\n"
110
+ " 3. Use --ado-token option with a valid token"
111
+ )
112
+ self.api_token = None
113
+ self.auth_scheme = None
114
+ else:
115
+ # PAT token - no expiration tracking, assume still valid
116
+ self.api_token = stored_token_expired.get("access_token")
117
+ self.auth_scheme = "basic"
82
118
  else:
83
119
  self.api_token = None
84
120
  self.auth_scheme = None
@@ -117,8 +153,9 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
117
153
  Full URL with proper format based on cloud vs on-premise
118
154
 
119
155
  Note:
120
- For on-premise, if base_url already includes /tfs/{collection} or /{collection},
121
- it won't add org again. For cloud, always adds {org}/{project}.
156
+ For project-based permissions in larger organizations, org must be part of the
157
+ _apis URL path before the project. This ensures proper permission scoping.
158
+ Format: {base_url}/{org}/{project}/_apis/...
122
159
  """
123
160
  if not self.project:
124
161
  raise ValueError(f"project required to build ADO URL (project={self.project!r})")
@@ -150,8 +187,16 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
150
187
  has_collection_in_base = has_tfs or len(parts) > 1
151
188
 
152
189
  if has_collection_in_base:
153
- # Collection already in base_url, just add project
154
- url = f"{base_url_normalized}/{self.project}/{path_normalized}?api-version={api_version}"
190
+ # Collection already in base_url, but for project-based permissions, we still need org in path
191
+ # Include org before project to ensure proper permission scoping
192
+ if self.org:
193
+ url = f"{base_url_normalized}/{self.org}/{self.project}/{path_normalized}?api-version={api_version}"
194
+ else:
195
+ # Fallback: if org not provided but collection in base_url, use project directly
196
+ console.print(
197
+ "[yellow]Warning:[/yellow] Collection in base_url but org not provided. Using project directly."
198
+ )
199
+ url = f"{base_url_normalized}/{self.project}/{path_normalized}?api-version={api_version}"
155
200
  elif self.org:
156
201
  # Collection not in base_url, need to add it
157
202
  # For on-premise, typically use /tfs/{collection} format unless explicitly newer format
@@ -1199,6 +1244,62 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
1199
1244
 
1200
1245
  return base64.b64encode(f":{token}".encode()).decode()
1201
1246
 
1247
+ def _try_refresh_oauth_token(self) -> dict[str, Any] | None:
1248
+ """
1249
+ Attempt to refresh expired OAuth token using persistent token cache.
1250
+
1251
+ This uses the same persistent cache as the auth command, allowing automatic
1252
+ token refresh without user interaction (like Azure CLI).
1253
+
1254
+ Returns:
1255
+ Refreshed token data dict if successful, None if refresh failed
1256
+ """
1257
+ try:
1258
+ from azure.identity import ( # type: ignore[reportMissingImports]
1259
+ DeviceCodeCredential,
1260
+ TokenCachePersistenceOptions,
1261
+ )
1262
+
1263
+ # Use the same cache name as auth command for shared cache
1264
+ # Try encrypted first, fall back to unencrypted if libsecret unavailable
1265
+ cache_options = None
1266
+ try:
1267
+ try:
1268
+ cache_options = TokenCachePersistenceOptions(
1269
+ name="specfact-azure-devops",
1270
+ allow_unencrypted_cache=False, # Prefer encrypted
1271
+ )
1272
+ except Exception:
1273
+ # Encrypted cache not available, try unencrypted
1274
+ cache_options = TokenCachePersistenceOptions(
1275
+ name="specfact-azure-devops",
1276
+ allow_unencrypted_cache=True, # Fallback: unencrypted
1277
+ )
1278
+ except Exception:
1279
+ # Persistent cache completely unavailable, can't refresh
1280
+ return None
1281
+
1282
+ # Create credential with same cache - it will use cached refresh token
1283
+ credential = DeviceCodeCredential(cache_persistence_options=cache_options)
1284
+ # Use the same resource as auth command
1285
+ azure_devops_resource = "499b84ac-1321-427f-aa17-267ca6975798/.default"
1286
+ token = credential.get_token(azure_devops_resource)
1287
+
1288
+ # Return refreshed token data
1289
+ from datetime import UTC, datetime
1290
+
1291
+ expires_at = datetime.fromtimestamp(token.expires_on, tz=UTC).isoformat()
1292
+ return {
1293
+ "access_token": token.token,
1294
+ "token_type": "bearer",
1295
+ "expires_at": expires_at,
1296
+ "resource": azure_devops_resource,
1297
+ "issued_at": datetime.now(tz=UTC).isoformat(),
1298
+ }
1299
+ except Exception:
1300
+ # Refresh failed (no cached refresh token, refresh token expired, etc.)
1301
+ return None
1302
+
1202
1303
  def _auth_headers(self) -> dict[str, str]:
1203
1304
  """Return authorization headers based on token type."""
1204
1305
  if not self.api_token:
@@ -2306,11 +2407,26 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
2306
2407
  Uses ADO Work Items API to query work items.
2307
2408
  """
2308
2409
  if not self.api_token:
2309
- msg = "Azure DevOps API token required to fetch backlog items"
2410
+ msg = (
2411
+ "Azure DevOps API token required to fetch backlog items.\n"
2412
+ "Options:\n"
2413
+ " 1. Set AZURE_DEVOPS_TOKEN environment variable\n"
2414
+ " 2. Use --ado-token option\n"
2415
+ " 3. Store token via specfact auth azure-devops"
2416
+ )
2310
2417
  raise ValueError(msg)
2311
2418
 
2312
- if not self.org or not self.project:
2313
- msg = "org and project required to fetch backlog items"
2419
+ if not self.org:
2420
+ msg = (
2421
+ "org (organization) required to fetch backlog items.\n"
2422
+ "For Azure DevOps Services (cloud), org is always required.\n"
2423
+ "For Azure DevOps Server (on-premise), org is the collection name.\n"
2424
+ "Provide via --ado-org option or ensure it's set in adapter configuration."
2425
+ )
2426
+ raise ValueError(msg)
2427
+
2428
+ if not self.project:
2429
+ msg = "project required to fetch backlog items. Provide via --ado-project option."
2314
2430
  raise ValueError(msg)
2315
2431
 
2316
2432
  # Build WIQL (Work Item Query Language) query
@@ -2344,14 +2460,23 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
2344
2460
  # POST to project-level endpoint: {org}/{project}/_apis/wit/wiql?api-version=7.1
2345
2461
  url = self._build_ado_url("_apis/wit/wiql", api_version="7.1")
2346
2462
  headers = {
2347
- "Authorization": f"{self.auth_scheme} {self.api_token}" if self.auth_scheme else f"Basic {self.api_token}",
2463
+ **self._auth_headers(),
2348
2464
  "Content-Type": "application/json",
2349
2465
  "Accept": "application/json",
2350
2466
  }
2351
2467
  payload = {"query": wiql}
2352
2468
 
2353
- # Debug: Log URL construction for troubleshooting
2354
- console.print(f"[dim]ADO WIQL URL: {url}[/dim]")
2469
+ # Debug: Log URL construction and auth status for troubleshooting
2470
+ debug_print(f"[dim]ADO WIQL URL: {url}[/dim]")
2471
+ if "Authorization" in headers:
2472
+ auth_header_preview = (
2473
+ headers["Authorization"][:20] + "..."
2474
+ if len(headers["Authorization"]) > 20
2475
+ else headers["Authorization"]
2476
+ )
2477
+ debug_print(f"[dim]ADO Auth: {auth_header_preview}[/dim]")
2478
+ else:
2479
+ debug_print("[yellow]Warning: No Authorization header in request[/yellow]")
2355
2480
 
2356
2481
  try:
2357
2482
  response = requests.post(url, headers=headers, json=payload, timeout=30)
@@ -2430,14 +2555,12 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
2430
2555
 
2431
2556
  # Headers for work items batch GET (organization-level endpoint)
2432
2557
  workitems_headers = {
2433
- "Authorization": f"{self.auth_scheme} {self.api_token}"
2434
- if self.auth_scheme
2435
- else f"Basic {self.api_token}",
2558
+ **self._auth_headers(),
2436
2559
  "Accept": "application/json",
2437
2560
  }
2438
2561
 
2439
2562
  # Debug: Log URL construction for troubleshooting
2440
- console.print(f"[dim]ADO WorkItems URL: {url}&ids={ids_str}[/dim]")
2563
+ debug_print(f"[dim]ADO WorkItems URL: {url}&ids={ids_str}[/dim]")
2441
2564
 
2442
2565
  try:
2443
2566
  response = requests.get(url, headers=workitems_headers, params=params, timeout=30)
@@ -2545,7 +2668,7 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
2545
2668
  work_item_id = int(item.id)
2546
2669
  url = self._build_ado_url(f"_apis/wit/workitems/{work_item_id}", api_version="7.1")
2547
2670
  headers = {
2548
- "Authorization": f"{self.auth_scheme} {self.api_token}" if self.auth_scheme else f"Basic {self.api_token}",
2671
+ **self._auth_headers(),
2549
2672
  "Content-Type": "application/json-patch+json",
2550
2673
  }
2551
2674
 
@@ -73,7 +73,7 @@ from specfact_cli.commands import (
73
73
  validate,
74
74
  )
75
75
  from specfact_cli.modes import OperationalMode, detect_mode
76
- from specfact_cli.runtime import get_configured_console
76
+ from specfact_cli.runtime import get_configured_console, set_debug_mode
77
77
  from specfact_cli.utils.progressive_disclosure import ProgressiveDisclosureGroup
78
78
  from specfact_cli.utils.structured_io import StructuredFormat
79
79
 
@@ -236,6 +236,11 @@ def main(
236
236
  callback=mode_callback,
237
237
  help="Operational mode: cicd (fast, deterministic) or copilot (enhanced, interactive)",
238
238
  ),
239
+ debug: bool = typer.Option(
240
+ False,
241
+ "--debug",
242
+ help="Enable debug output (shows detailed logging and diagnostic information)",
243
+ ),
239
244
  input_format: Annotated[
240
245
  StructuredFormat,
241
246
  typer.Option(
@@ -278,6 +283,9 @@ def main(
278
283
  # Set banner flag based on --no-banner option
279
284
  _show_banner = not no_banner
280
285
 
286
+ # Set debug mode
287
+ set_debug_mode(debug)
288
+
281
289
  runtime.configure_io_formats(input_format=input_format, output_format=output_format)
282
290
  # Invert logic: --interactive means not non-interactive, --no-interactive means non-interactive
283
291
  if interaction is not None:
@@ -153,10 +153,54 @@ def _poll_github_device_token(
153
153
 
154
154
 
155
155
  @app.command("azure-devops")
156
- def auth_azure_devops() -> None:
157
- """Authenticate to Azure DevOps using device code flow."""
156
+ def auth_azure_devops(
157
+ pat: str | None = typer.Option(
158
+ None,
159
+ "--pat",
160
+ help="Store a Personal Access Token (PAT) directly. PATs can have expiration up to 1 year, "
161
+ "unlike OAuth tokens which expire after ~1 hour. Create PAT at: "
162
+ "https://dev.azure.com/{org}/_usersSettings/tokens",
163
+ ),
164
+ use_device_code: bool = typer.Option(
165
+ False,
166
+ "--use-device-code",
167
+ help="Force device code flow instead of trying interactive browser first. "
168
+ "Useful for SSH/headless environments where browser cannot be opened.",
169
+ ),
170
+ ) -> None:
171
+ """
172
+ Authenticate to Azure DevOps using OAuth (device code or interactive browser) or Personal Access Token (PAT).
173
+
174
+ **Token Options:**
175
+
176
+ 1. **Personal Access Token (PAT)** - Recommended for long-lived authentication:
177
+ - Use --pat option to store a PAT directly
178
+ - PATs can have expiration up to 1 year (maximum allowed)
179
+ - Create PAT at: https://dev.azure.com/{org}/_usersSettings/tokens
180
+ - Select required scopes (e.g., "Work Items: Read & Write")
181
+ - Example: specfact auth azure-devops --pat your_pat_token
182
+
183
+ 2. **OAuth Flow** (default, when no PAT provided):
184
+ - **First tries interactive browser** (opens browser automatically, better UX)
185
+ - **Falls back to device code** if browser unavailable (SSH/headless environments)
186
+ - Access tokens expire after ~1 hour, refresh tokens last 90 days
187
+ - Automatic token refresh via persistent cache (no re-authentication needed)
188
+ - Example: specfact auth azure-devops
189
+
190
+ 3. **Force Device Code Flow** (--use-device-code):
191
+ - Skip interactive browser, use device code directly
192
+ - Useful for SSH/headless environments or when browser cannot be opened
193
+ - Example: specfact auth azure-devops --use-device-code
194
+
195
+ **For Long-Lived Tokens:**
196
+ Use a PAT with 90 days or 1 year expiration instead of OAuth tokens to avoid
197
+ frequent re-authentication. PATs are stored securely and work the same way as OAuth tokens.
198
+ """
158
199
  try:
159
- from azure.identity import DeviceCodeCredential # type: ignore[reportMissingImports]
200
+ from azure.identity import ( # type: ignore[reportMissingImports]
201
+ DeviceCodeCredential,
202
+ InteractiveBrowserCredential,
203
+ )
160
204
  except ImportError:
161
205
  console.print("[bold red]✗[/bold red] azure-identity is not installed.")
162
206
  console.print("Install dependencies with: pip install specfact-cli")
@@ -171,9 +215,145 @@ def auth_azure_devops() -> None:
171
215
  console.print(f"Enter the code: [bold]{user_code}[/bold]")
172
216
  console.print(f"Code expires at: {expires_at.isoformat()}")
173
217
 
174
- console.print("[bold]Starting Azure DevOps device code authentication...[/bold]")
175
- credential = DeviceCodeCredential(prompt_callback=prompt_callback)
176
- token = credential.get_token(AZURE_DEVOPS_RESOURCE)
218
+ # If PAT is provided, store it directly (no expiration for PATs stored as Basic auth)
219
+ if pat:
220
+ console.print("[bold]Storing Personal Access Token (PAT)...[/bold]")
221
+ # PATs are stored as Basic auth tokens (no expiration date set by default)
222
+ # Users can create PATs with up to 1 year expiration in Azure DevOps UI
223
+ token_data = {
224
+ "access_token": pat,
225
+ "token_type": "basic", # PATs use Basic authentication
226
+ "issued_at": datetime.now(tz=UTC).isoformat(),
227
+ # Note: PAT expiration is managed by Azure DevOps, not stored locally
228
+ # Users should set expiration when creating PAT (up to 1 year)
229
+ }
230
+ set_token("azure-devops", token_data)
231
+ console.print("[bold green]✓[/bold green] Personal Access Token stored")
232
+ console.print(
233
+ "[dim]PAT stored successfully. PATs can have expiration up to 1 year when created in Azure DevOps.[/dim]"
234
+ )
235
+ console.print("[dim]Create/manage PATs at: https://dev.azure.com/{org}/_usersSettings/tokens[/dim]")
236
+ return
237
+
238
+ # OAuth flow with persistent token cache (automatic refresh)
239
+ # Try interactive browser first, fall back to device code if it fails
240
+ console.print("[bold]Starting Azure DevOps OAuth authentication...[/bold]")
241
+
242
+ # Enable persistent token cache for automatic token refresh (like Azure CLI)
243
+ # This allows tokens to be refreshed automatically without re-authentication
244
+ cache_options = None
245
+ use_unencrypted_cache = False
246
+ try:
247
+ from azure.identity import TokenCachePersistenceOptions # type: ignore[reportMissingImports]
248
+
249
+ # Try encrypted cache first (secure), fall back to unencrypted if libsecret unavailable
250
+ try:
251
+ cache_options = TokenCachePersistenceOptions(
252
+ name="specfact-azure-devops", # Shared cache name across processes
253
+ allow_unencrypted_cache=False, # Prefer encrypted storage
254
+ )
255
+ console.print(
256
+ "[dim]Persistent token cache enabled (encrypted) - tokens will refresh automatically (like Azure CLI)[/dim]"
257
+ )
258
+ except Exception:
259
+ # Encrypted cache not available (e.g., libsecret missing on Linux), try unencrypted
260
+ try:
261
+ cache_options = TokenCachePersistenceOptions(
262
+ name="specfact-azure-devops",
263
+ allow_unencrypted_cache=True, # Fallback: unencrypted storage
264
+ )
265
+ use_unencrypted_cache = True
266
+ console.print(
267
+ "[yellow]Note:[/yellow] Using unencrypted token cache (libsecret unavailable). "
268
+ "Tokens will refresh automatically but stored without encryption."
269
+ )
270
+ except Exception:
271
+ # Persistent cache completely unavailable, use in-memory only
272
+ console.print(
273
+ "[yellow]Note:[/yellow] Persistent cache not available, using in-memory cache only. "
274
+ "Tokens will need to be refreshed manually after ~1 hour."
275
+ )
276
+ except ImportError:
277
+ # TokenCachePersistenceOptions not available in this version
278
+ pass
279
+
280
+ # Helper function to try authentication with fallback to unencrypted cache or no cache
281
+ def try_authenticate_with_fallback(credential_class, credential_kwargs):
282
+ """Try authentication, falling back to unencrypted cache or no cache if encrypted cache fails."""
283
+ nonlocal cache_options, use_unencrypted_cache
284
+ # First try with current cache_options
285
+ try:
286
+ credential = credential_class(cache_persistence_options=cache_options, **credential_kwargs)
287
+ return credential.get_token(AZURE_DEVOPS_RESOURCE)
288
+ except Exception as e:
289
+ error_msg = str(e).lower()
290
+ # Check if error is about cache encryption and we haven't already tried unencrypted
291
+ if (
292
+ ("cache encryption" in error_msg or "libsecret" in error_msg)
293
+ and cache_options
294
+ and not use_unencrypted_cache
295
+ ):
296
+ # Try again with unencrypted cache
297
+ console.print("[yellow]Note:[/yellow] Encrypted cache unavailable, trying unencrypted cache...")
298
+ try:
299
+ from azure.identity import TokenCachePersistenceOptions # type: ignore[reportMissingImports]
300
+
301
+ unencrypted_cache = TokenCachePersistenceOptions(
302
+ name="specfact-azure-devops",
303
+ allow_unencrypted_cache=True,
304
+ )
305
+ credential = credential_class(cache_persistence_options=unencrypted_cache, **credential_kwargs)
306
+ token = credential.get_token(AZURE_DEVOPS_RESOURCE)
307
+ console.print(
308
+ "[yellow]Note:[/yellow] Using unencrypted token cache (libsecret unavailable). "
309
+ "Tokens will refresh automatically but stored without encryption."
310
+ )
311
+ # Update global cache_options for future use
312
+ cache_options = unencrypted_cache
313
+ use_unencrypted_cache = True
314
+ return token
315
+ except Exception as e2:
316
+ # Unencrypted cache also failed - check if it's the same error
317
+ error_msg2 = str(e2).lower()
318
+ if "cache encryption" in error_msg2 or "libsecret" in error_msg2:
319
+ # Still failing on cache, try without cache entirely
320
+ console.print("[yellow]Note:[/yellow] Persistent cache unavailable, trying without cache...")
321
+ try:
322
+ credential = credential_class(**credential_kwargs)
323
+ token = credential.get_token(AZURE_DEVOPS_RESOURCE)
324
+ console.print(
325
+ "[yellow]Note:[/yellow] Using in-memory cache only. "
326
+ "Tokens will need to be refreshed manually after ~1 hour."
327
+ )
328
+ return token
329
+ except Exception:
330
+ # Even without cache it failed, re-raise original
331
+ raise e from e2
332
+ # Different error, re-raise
333
+ raise e2 from e
334
+ # Not a cache encryption error, re-raise
335
+ raise
336
+
337
+ # Try interactive browser first (better UX), fall back to device code if it fails
338
+ token = None
339
+ if not use_device_code:
340
+ try:
341
+ console.print("[dim]Trying interactive browser authentication...[/dim]")
342
+ token = try_authenticate_with_fallback(InteractiveBrowserCredential, {})
343
+ console.print("[bold green]✓[/bold green] Interactive browser authentication successful")
344
+ except Exception as e:
345
+ # Interactive browser failed (no display, headless environment, etc.)
346
+ console.print(f"[yellow]⚠[/yellow] Interactive browser unavailable: {type(e).__name__}")
347
+ console.print("[dim]Falling back to device code flow...[/dim]")
348
+
349
+ # Use device code flow if interactive browser failed or was explicitly requested
350
+ if token is None:
351
+ console.print("[bold]Using device code authentication...[/bold]")
352
+ try:
353
+ token = try_authenticate_with_fallback(DeviceCodeCredential, {"prompt_callback": prompt_callback})
354
+ except Exception as e:
355
+ console.print(f"[bold red]✗[/bold red] Authentication failed: {e}")
356
+ raise typer.Exit(1) from e
177
357
 
178
358
  expires_at = datetime.fromtimestamp(token.expires_on, tz=UTC).isoformat()
179
359
  token_data = {
@@ -187,6 +367,10 @@ def auth_azure_devops() -> None:
187
367
 
188
368
  console.print("[bold green]✓[/bold green] Azure DevOps authentication complete")
189
369
  console.print("Stored token for provider: azure-devops")
370
+ console.print(
371
+ f"[yellow]⚠[/yellow] Token expires at: {expires_at}\n"
372
+ "[dim]For longer-lived tokens (up to 1 year), use --pat option with a Personal Access Token.[/dim]"
373
+ )
190
374
 
191
375
 
192
376
  @app.command("github")
@@ -17,6 +17,7 @@ from icontract import ensure, require
17
17
  from rich.console import Console
18
18
  from rich.panel import Panel
19
19
 
20
+ from specfact_cli.runtime import debug_print
20
21
  from specfact_cli.telemetry import telemetry
21
22
  from specfact_cli.utils.env_manager import EnvManager, build_tool_command, detect_env_manager
22
23
  from specfact_cli.utils.ide_setup import (
@@ -258,12 +259,12 @@ def init(
258
259
  # Try 1: Development mode - relative to repo root
259
260
  dev_templates_dir = (repo_path / "resources" / "prompts").resolve()
260
261
  tried_locations.append(dev_templates_dir)
261
- console.print(f"[dim]Debug:[/dim] Trying development path: {dev_templates_dir}")
262
+ debug_print(f"[dim]Debug:[/dim] Trying development path: {dev_templates_dir}")
262
263
  if dev_templates_dir.exists():
263
264
  templates_dir = dev_templates_dir
264
265
  console.print(f"[green]✓[/green] Found templates at: {templates_dir}")
265
266
  else:
266
- console.print("[dim]Debug:[/dim] Development path not found, trying installed package...")
267
+ debug_print("[dim]Debug:[/dim] Development path not found, trying installed package...")
267
268
  # Try 2: Installed package - use importlib.resources
268
269
  # Note: importlib is part of Python's standard library (since Python 3.1)
269
270
  # importlib.resources.files() is available since Python 3.9
@@ -273,7 +274,7 @@ def init(
273
274
  try:
274
275
  import importlib.resources
275
276
 
276
- console.print("[dim]Debug:[/dim] Using importlib.resources.files() API...")
277
+ debug_print("[dim]Debug:[/dim] Using importlib.resources.files() API...")
277
278
  # Use files() API (Python 3.9+) - recommended approach
278
279
  resources_ref = importlib.resources.files("specfact_cli")
279
280
  templates_ref = resources_ref / "resources" / "prompts"
@@ -282,7 +283,7 @@ def init(
282
283
  # Use resolve() to handle Windows/Linux/macOS path differences
283
284
  package_templates_dir = Path(str(templates_ref)).resolve()
284
285
  tried_locations.append(package_templates_dir)
285
- console.print(f"[dim]Debug:[/dim] Package templates path: {package_templates_dir}")
286
+ debug_print(f"[dim]Debug:[/dim] Package templates path: {package_templates_dir}")
286
287
  if package_templates_dir.exists():
287
288
  templates_dir = package_templates_dir
288
289
  console.print(f"[green]✓[/green] Found templates at: {templates_dir}")
@@ -292,20 +293,20 @@ def init(
292
293
  console.print(
293
294
  f"[yellow]⚠[/yellow] importlib.resources not available or module not found: {type(e).__name__}: {e}"
294
295
  )
295
- console.print("[dim]Debug:[/dim] Falling back to importlib.util.find_spec()...")
296
+ debug_print("[dim]Debug:[/dim] Falling back to importlib.util.find_spec()...")
296
297
  except (TypeError, AttributeError, ValueError) as e:
297
298
  console.print(f"[yellow]⚠[/yellow] Error converting Traversable to Path: {e}")
298
- console.print("[dim]Debug:[/dim] Falling back to importlib.util.find_spec()...")
299
+ debug_print("[dim]Debug:[/dim] Falling back to importlib.util.find_spec()...")
299
300
  except Exception as e:
300
301
  console.print(f"[yellow]⚠[/yellow] Unexpected error with importlib.resources: {type(e).__name__}: {e}")
301
- console.print("[dim]Debug:[/dim] Falling back to importlib.util.find_spec()...")
302
+ debug_print("[dim]Debug:[/dim] Falling back to importlib.util.find_spec()...")
302
303
 
303
304
  # Fallback: importlib.util.find_spec() + comprehensive package location search
304
305
  if not templates_dir or not templates_dir.exists():
305
306
  try:
306
307
  import importlib.util
307
308
 
308
- console.print("[dim]Debug:[/dim] Using importlib.util.find_spec() fallback...")
309
+ debug_print("[dim]Debug:[/dim] Using importlib.util.find_spec() fallback...")
309
310
  spec = importlib.util.find_spec("specfact_cli")
310
311
  if spec and spec.origin:
311
312
  # spec.origin points to __init__.py
@@ -314,8 +315,8 @@ def init(
314
315
  package_root = Path(spec.origin).parent.resolve()
315
316
  package_templates_dir = (package_root / "resources" / "prompts").resolve()
316
317
  tried_locations.append(package_templates_dir)
317
- console.print(f"[dim]Debug:[/dim] Package root from spec.origin: {package_root}")
318
- console.print(f"[dim]Debug:[/dim] Templates path from spec: {package_templates_dir}")
318
+ debug_print(f"[dim]Debug:[/dim] Package root from spec.origin: {package_root}")
319
+ debug_print(f"[dim]Debug:[/dim] Templates path from spec: {package_templates_dir}")
319
320
  if package_templates_dir.exists():
320
321
  templates_dir = package_templates_dir
321
322
  console.print(f"[green]✓[/green] Found templates at: {templates_dir}")
@@ -324,20 +325,20 @@ def init(
324
325
  else:
325
326
  console.print("[yellow]⚠[/yellow] Could not find specfact_cli module spec")
326
327
  if spec is None:
327
- console.print("[dim]Debug:[/dim] spec is None")
328
+ debug_print("[dim]Debug:[/dim] spec is None")
328
329
  elif not spec.origin:
329
- console.print("[dim]Debug:[/dim] spec.origin is None or empty")
330
+ debug_print("[dim]Debug:[/dim] spec.origin is None or empty")
330
331
  except Exception as e:
331
332
  console.print(f"[yellow]⚠[/yellow] Error with importlib.util.find_spec(): {type(e).__name__}: {e}")
332
333
 
333
334
  # Fallback: Comprehensive package location search (cross-platform)
334
335
  if not templates_dir or not templates_dir.exists():
335
336
  try:
336
- console.print("[dim]Debug:[/dim] Searching all package installation locations...")
337
+ debug_print("[dim]Debug:[/dim] Searching all package installation locations...")
337
338
  package_locations = get_package_installation_locations("specfact_cli")
338
- console.print(f"[dim]Debug:[/dim] Found {len(package_locations)} possible package location(s)")
339
+ debug_print(f"[dim]Debug:[/dim] Found {len(package_locations)} possible package location(s)")
339
340
  for i, loc in enumerate(package_locations, 1):
340
- console.print(f"[dim]Debug:[/dim] {i}. {loc}")
341
+ debug_print(f"[dim]Debug:[/dim] {i}. {loc}")
341
342
  # Check for resources/prompts in this package location
342
343
  resource_path = (loc / "resources" / "prompts").resolve()
343
344
  tried_locations.append(resource_path)
@@ -347,7 +348,7 @@ def init(
347
348
  break
348
349
  if not templates_dir or not templates_dir.exists():
349
350
  # Try using the helper function as a final attempt
350
- console.print("[dim]Debug:[/dim] Trying find_package_resources_path() helper...")
351
+ debug_print("[dim]Debug:[/dim] Trying find_package_resources_path() helper...")
351
352
  resource_path = find_package_resources_path("specfact_cli", "resources/prompts")
352
353
  if resource_path and resource_path.exists():
353
354
  tried_locations.append(resource_path)
@@ -361,15 +362,15 @@ def init(
361
362
  # Try 3: Fallback - relative to this file (for edge cases)
362
363
  if not templates_dir or not templates_dir.exists():
363
364
  try:
364
- console.print("[dim]Debug:[/dim] Trying fallback: relative to __file__...")
365
+ debug_print("[dim]Debug:[/dim] Trying fallback: relative to __file__...")
365
366
  # Get the directory containing this file (init.py)
366
367
  # init.py is in: src/specfact_cli/commands/init.py
367
368
  # Go up: commands -> specfact_cli -> src -> project root
368
369
  current_file = Path(__file__).resolve()
369
370
  fallback_dir = (current_file.parent.parent.parent.parent / "resources" / "prompts").resolve()
370
371
  tried_locations.append(fallback_dir)
371
- console.print(f"[dim]Debug:[/dim] Current file: {current_file}")
372
- console.print(f"[dim]Debug:[/dim] Fallback templates path: {fallback_dir}")
372
+ debug_print(f"[dim]Debug:[/dim] Current file: {current_file}")
373
+ debug_print(f"[dim]Debug:[/dim] Fallback templates path: {fallback_dir}")
373
374
  if fallback_dir.exists():
374
375
  templates_dir = fallback_dir
375
376
  console.print(f"[green]✓[/green] Found templates at: {templates_dir}")