specfact-cli 0.25.1__tar.gz → 0.25.3__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.
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/PKG-INFO +3 -1
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/pyproject.toml +3 -1
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/__init__.py +1 -1
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/__init__.py +1 -1
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/adapters/ado.py +62 -16
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/adapters/github.py +61 -12
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/agents/analyze_agent.py +1 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/cli.py +5 -1
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/commands/__init__.py +2 -0
- specfact_cli-0.25.3/src/specfact_cli/commands/auth.py +341 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/commands/import_cmd.py +4 -4
- specfact_cli-0.25.3/src/specfact_cli/contracts/__init__.py +3 -0
- specfact_cli-0.25.3/src/specfact_cli/contracts/crosshair_props.py +25 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/sync/bridge_sync.py +1582 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/telemetry.py +1 -0
- specfact_cli-0.25.3/src/specfact_cli/utils/auth_tokens.py +182 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/validators/repro_checker.py +76 -15
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/validators/sidecar/orchestrator.py +2 -2
- specfact_cli-0.25.1/resources/templates/sidecar/STRUCTURE.md +0 -70
- specfact_cli-0.25.1/resources/templates/sidecar/__init__.py +0 -0
- specfact_cli-0.25.1/resources/templates/sidecar/common/README.md +0 -208
- specfact_cli-0.25.1/resources/templates/sidecar/common/adapters.py +0 -926
- specfact_cli-0.25.1/resources/templates/sidecar/common/bindings.yaml.example +0 -42
- specfact_cli-0.25.1/resources/templates/sidecar/common/crosshair_plugin.py +0 -34
- specfact_cli-0.25.1/resources/templates/sidecar/common/generate_harness.py +0 -561
- specfact_cli-0.25.1/resources/templates/sidecar/common/harness_contracts.py.example +0 -26
- specfact_cli-0.25.1/resources/templates/sidecar/common/populate_contracts.py +0 -1013
- specfact_cli-0.25.1/resources/templates/sidecar/common/run_sidecar.sh +0 -579
- specfact_cli-0.25.1/resources/templates/sidecar/common/sidecar-init.sh +0 -108
- specfact_cli-0.25.1/resources/templates/sidecar/frameworks/django/__init__.py +0 -0
- specfact_cli-0.25.1/resources/templates/sidecar/frameworks/django/crosshair_django_wrapper.py +0 -140
- specfact_cli-0.25.1/resources/templates/sidecar/frameworks/django/django_form_extractor.py +0 -440
- specfact_cli-0.25.1/resources/templates/sidecar/frameworks/django/django_url_extractor.py +0 -338
- specfact_cli-0.25.1/resources/templates/sidecar/frameworks/drf/__init__.py +0 -0
- specfact_cli-0.25.1/resources/templates/sidecar/frameworks/drf/drf_serializer_extractor.py +0 -372
- specfact_cli-0.25.1/resources/templates/sidecar/frameworks/fastapi/__init__.py +0 -0
- specfact_cli-0.25.1/resources/templates/sidecar/frameworks/fastapi/fastapi_route_extractor.py +0 -643
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/.gitignore +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/LICENSE.md +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/README.md +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/resources/mappings/node-async.yaml +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/resources/mappings/python-async.yaml +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/resources/mappings/speckit-default.yaml +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/resources/prompts/shared/cli-enforcement.md +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/resources/prompts/specfact.01-import.md +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/resources/prompts/specfact.02-plan.md +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/resources/prompts/specfact.03-review.md +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/resources/prompts/specfact.04-sdd.md +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/resources/prompts/specfact.05-enforce.md +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/resources/prompts/specfact.06-sync.md +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/resources/prompts/specfact.07-contracts.md +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/resources/prompts/specfact.compare.md +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/resources/prompts/specfact.sync-backlog.md +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/resources/prompts/specfact.validate.md +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/resources/schemas/deviation.schema.json +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/resources/schemas/plan.schema.json +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/resources/schemas/protocol.schema.json +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/resources/templates/github-action.yml.j2 +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/resources/templates/persona/architect.md.j2 +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/resources/templates/persona/developer.md.j2 +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/resources/templates/persona/product-owner.md.j2 +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/resources/templates/plan.bundle.yaml.j2 +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/resources/templates/pr-template.md.j2 +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/resources/templates/protocol.yaml.j2 +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/resources/templates/telemetry.yaml.example +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/adapters/__init__.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/adapters/backlog_base.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/adapters/base.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/adapters/openspec.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/adapters/openspec_parser.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/adapters/registry.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/adapters/speckit.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/agents/__init__.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/agents/base.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/agents/plan_agent.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/agents/registry.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/agents/sync_agent.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/analyzers/__init__.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/analyzers/ambiguity_scanner.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/analyzers/code_analyzer.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/analyzers/constitution_evidence_extractor.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/analyzers/contract_extractor.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/analyzers/control_flow_analyzer.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/analyzers/graph_analyzer.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/analyzers/relationship_mapper.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/analyzers/requirement_extractor.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/analyzers/test_pattern_extractor.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/commands/analyze.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/commands/contract_cmd.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/commands/drift.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/commands/enforce.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/commands/generate.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/commands/init.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/commands/migrate.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/commands/plan.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/commands/project_cmd.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/commands/repro.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/commands/sdd.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/commands/spec.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/commands/sync.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/commands/validate.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/common/__init__.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/common/logger_setup.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/common/logging_utils.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/common/text_utils.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/common/utils.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/comparators/__init__.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/comparators/plan_comparator.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/enrichers/constitution_enricher.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/enrichers/plan_enricher.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/generators/__init__.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/generators/contract_generator.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/generators/openapi_extractor.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/generators/persona_exporter.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/generators/plan_generator.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/generators/protocol_generator.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/generators/report_generator.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/generators/task_generator.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/generators/test_to_openapi.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/generators/workflow_generator.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/importers/__init__.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/importers/speckit_converter.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/importers/speckit_scanner.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/integrations/__init__.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/integrations/specmatic.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/merge/__init__.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/merge/resolver.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/migrations/__init__.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/migrations/plan_migrator.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/models/__init__.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/models/bridge.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/models/capabilities.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/models/change.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/models/contract.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/models/deviation.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/models/enforcement.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/models/persona_template.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/models/plan.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/models/project.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/models/protocol.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/models/quality.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/models/sdd.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/models/source_tracking.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/models/task.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/modes/__init__.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/modes/detector.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/modes/router.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/parsers/__init__.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/parsers/persona_importer.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/resources/semgrep/async.yml +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/resources/semgrep/code-quality.yml +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/resources/semgrep/feature-detection.yml +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/runtime.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/sync/__init__.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/sync/bridge_probe.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/sync/bridge_watch.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/sync/change_detector.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/sync/code_to_spec.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/sync/drift_detector.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/sync/repository_sync.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/sync/spec_to_code.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/sync/spec_to_tests.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/sync/watcher.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/sync/watcher_enhanced.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/templates/__init__.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/templates/bridge_templates.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/templates/specification_templates.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/utils/__init__.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/utils/acceptance_criteria.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/utils/bundle_loader.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/utils/code_change_detector.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/utils/console.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/utils/content_sanitizer.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/utils/context_detection.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/utils/enrichment_context.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/utils/enrichment_parser.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/utils/env_manager.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/utils/feature_keys.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/utils/git.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/utils/github_annotations.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/utils/ide_setup.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/utils/incremental_check.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/utils/optional_deps.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/utils/performance.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/utils/progress.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/utils/progressive_disclosure.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/utils/prompts.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/utils/sdd_discovery.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/utils/source_scanner.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/utils/structure.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/utils/structured_io.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/utils/suggestions.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/utils/terminal.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/utils/yaml_utils.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/validators/__init__.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/validators/agile_validation.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/validators/change_proposal_integration.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/validators/cli_first_validator.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/validators/contract_validator.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/validators/fsm.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/validators/schema.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/validators/sidecar/__init__.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/validators/sidecar/contract_populator.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/validators/sidecar/crosshair_runner.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/validators/sidecar/crosshair_summary.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/validators/sidecar/dependency_installer.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/validators/sidecar/framework_detector.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/validators/sidecar/frameworks/__init__.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/validators/sidecar/frameworks/base.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/validators/sidecar/frameworks/django.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/validators/sidecar/frameworks/drf.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/validators/sidecar/frameworks/fastapi.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/validators/sidecar/frameworks/flask.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/validators/sidecar/harness_generator.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/validators/sidecar/models.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/validators/sidecar/specmatic_runner.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/validators/sidecar/unannotated_detector.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/src/specfact_cli/versioning/__init__.py +0 -0
- {specfact_cli-0.25.1 → specfact_cli-0.25.3}/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.25.
|
|
3
|
+
Version: 0.25.3
|
|
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
|
|
@@ -223,6 +223,7 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
223
223
|
Classifier: Topic :: Software Development :: Quality Assurance
|
|
224
224
|
Classifier: Topic :: Software Development :: Testing
|
|
225
225
|
Requires-Python: >=3.11
|
|
226
|
+
Requires-Dist: azure-identity>=1.17.1
|
|
226
227
|
Requires-Dist: beartype>=0.22.4
|
|
227
228
|
Requires-Dist: crosshair-tool>=0.0.97
|
|
228
229
|
Requires-Dist: gitpython>=3.1.45
|
|
@@ -237,6 +238,7 @@ Requires-Dist: opentelemetry-sdk>=1.27.0
|
|
|
237
238
|
Requires-Dist: pydantic>=2.12.3
|
|
238
239
|
Requires-Dist: python-dotenv>=1.2.1
|
|
239
240
|
Requires-Dist: pyyaml>=6.0.3
|
|
241
|
+
Requires-Dist: requests>=2.32.3
|
|
240
242
|
Requires-Dist: rich<13.6.0,>=13.5.2
|
|
241
243
|
Requires-Dist: ruamel-yaml>=0.18.16
|
|
242
244
|
Requires-Dist: ruff>=0.14.2
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "specfact-cli"
|
|
7
|
-
version = "0.25.
|
|
7
|
+
version = "0.25.3"
|
|
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"
|
|
@@ -31,6 +31,8 @@ dependencies = [
|
|
|
31
31
|
"python-dotenv>=1.2.1",
|
|
32
32
|
"typing-extensions>=4.15.0",
|
|
33
33
|
"PyYAML>=6.0.3",
|
|
34
|
+
"requests>=2.32.3",
|
|
35
|
+
"azure-identity>=1.17.1",
|
|
34
36
|
|
|
35
37
|
# CLI framework
|
|
36
38
|
"typer>=0.20.0",
|
|
@@ -26,6 +26,7 @@ from specfact_cli.adapters.base import BridgeAdapter
|
|
|
26
26
|
from specfact_cli.models.bridge import BridgeConfig
|
|
27
27
|
from specfact_cli.models.capabilities import ToolCapabilities
|
|
28
28
|
from specfact_cli.models.change import ChangeProposal, ChangeTracking
|
|
29
|
+
from specfact_cli.utils.auth_tokens import get_token
|
|
29
30
|
|
|
30
31
|
|
|
31
32
|
console = Console()
|
|
@@ -57,19 +58,27 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin):
|
|
|
57
58
|
org: Azure DevOps organization name (optional, can be provided via env/CLI)
|
|
58
59
|
project: Azure DevOps project name (optional, can be provided via env/CLI)
|
|
59
60
|
base_url: Azure DevOps base URL (optional, defaults to https://dev.azure.com)
|
|
60
|
-
api_token: Azure DevOps PAT (optional, uses AZURE_DEVOPS_TOKEN env var)
|
|
61
|
+
api_token: Azure DevOps PAT (optional, uses AZURE_DEVOPS_TOKEN env var or stored auth token)
|
|
61
62
|
work_item_type: Work item type (optional, derived from process template if not provided)
|
|
62
63
|
"""
|
|
63
64
|
self.org = org
|
|
64
65
|
self.project = project
|
|
66
|
+
self.auth_scheme: str | None = None
|
|
65
67
|
|
|
66
|
-
# Token resolution: explicit token > env var
|
|
68
|
+
# Token resolution: explicit token > env var > stored token
|
|
67
69
|
if api_token:
|
|
68
70
|
self.api_token = api_token
|
|
71
|
+
self.auth_scheme = "basic"
|
|
69
72
|
elif os.environ.get("AZURE_DEVOPS_TOKEN"):
|
|
70
73
|
self.api_token = os.environ.get("AZURE_DEVOPS_TOKEN")
|
|
74
|
+
self.auth_scheme = "basic"
|
|
75
|
+
elif stored_token := get_token("azure-devops"):
|
|
76
|
+
self.api_token = stored_token.get("access_token")
|
|
77
|
+
token_type = (stored_token.get("token_type") or "bearer").lower()
|
|
78
|
+
self.auth_scheme = "bearer" if token_type == "bearer" else "basic"
|
|
71
79
|
else:
|
|
72
80
|
self.api_token = None
|
|
81
|
+
self.auth_scheme = None
|
|
73
82
|
|
|
74
83
|
# Base URL defaults to Azure DevOps Services (cloud)
|
|
75
84
|
self.base_url = base_url or "https://dev.azure.com"
|
|
@@ -210,12 +219,13 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin):
|
|
|
210
219
|
|
|
211
220
|
description = ""
|
|
212
221
|
rationale = ""
|
|
222
|
+
impact = ""
|
|
213
223
|
|
|
214
224
|
# Parse markdown sections (Why, What Changes)
|
|
215
225
|
if description_raw:
|
|
216
226
|
# Extract "Why" section (stop at What Changes or OpenSpec footer)
|
|
217
227
|
why_match = re.search(
|
|
218
|
-
r"##\s+Why\s*\n(.*?)(?=\n##\s+What\s+Changes\s|\n---\s*\n\*OpenSpec Change Proposal:|\Z)",
|
|
228
|
+
r"##\s+Why\s*\n(.*?)(?=\n##\s+What\s+Changes\s|\n##\s+Impact\s|\n---\s*\n\*OpenSpec Change Proposal:|\Z)",
|
|
219
229
|
description_raw,
|
|
220
230
|
re.DOTALL | re.IGNORECASE,
|
|
221
231
|
)
|
|
@@ -224,7 +234,7 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin):
|
|
|
224
234
|
|
|
225
235
|
# Extract "What Changes" section (stop at OpenSpec footer)
|
|
226
236
|
what_match = re.search(
|
|
227
|
-
r"##\s+What\s+Changes\s*\n(.*?)(?=\n---\s*\n\*OpenSpec Change Proposal:|\Z)",
|
|
237
|
+
r"##\s+What\s+Changes\s*\n(.*?)(?=\n##\s+Impact\s|\n---\s*\n\*OpenSpec Change Proposal:|\Z)",
|
|
228
238
|
description_raw,
|
|
229
239
|
re.DOTALL | re.IGNORECASE,
|
|
230
240
|
)
|
|
@@ -235,6 +245,14 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin):
|
|
|
235
245
|
body_clean = re.sub(r"\n---\s*\n\*OpenSpec Change Proposal:.*", "", description_raw, flags=re.DOTALL)
|
|
236
246
|
description = body_clean.strip()
|
|
237
247
|
|
|
248
|
+
impact_match = re.search(
|
|
249
|
+
r"##\s+Impact\s*\n(.*?)(?=\n---\s*\n\*OpenSpec Change Proposal:|\Z)",
|
|
250
|
+
description_raw,
|
|
251
|
+
re.DOTALL | re.IGNORECASE,
|
|
252
|
+
)
|
|
253
|
+
if impact_match:
|
|
254
|
+
impact = impact_match.group(1).strip()
|
|
255
|
+
|
|
238
256
|
# Extract change ID from OpenSpec metadata footer or work item ID
|
|
239
257
|
change_id = None
|
|
240
258
|
if description_raw:
|
|
@@ -306,6 +324,7 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin):
|
|
|
306
324
|
"title": title,
|
|
307
325
|
"description": description,
|
|
308
326
|
"rationale": rationale,
|
|
327
|
+
"impact": impact,
|
|
309
328
|
"status": status,
|
|
310
329
|
"created_at": created_at,
|
|
311
330
|
"timeline": timeline,
|
|
@@ -505,7 +524,8 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin):
|
|
|
505
524
|
msg = (
|
|
506
525
|
"Azure DevOps API token required. Options:\n"
|
|
507
526
|
" 1. Set AZURE_DEVOPS_TOKEN environment variable\n"
|
|
508
|
-
" 2. Provide via --ado-token option"
|
|
527
|
+
" 2. Provide via --ado-token option\n"
|
|
528
|
+
" 3. Run `specfact auth azure-devops` for device code authentication"
|
|
509
529
|
)
|
|
510
530
|
raise ValueError(msg)
|
|
511
531
|
|
|
@@ -946,8 +966,8 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin):
|
|
|
946
966
|
# Get process template from project
|
|
947
967
|
url = f"{self.base_url}/{org}/_apis/projects/{project}?api-version=7.1"
|
|
948
968
|
headers = {
|
|
949
|
-
"Authorization": f"Basic {self._encode_pat(self.api_token)}",
|
|
950
969
|
"Content-Type": "application/json",
|
|
970
|
+
**self._auth_headers(),
|
|
951
971
|
}
|
|
952
972
|
response = requests.get(url, headers=headers, timeout=30)
|
|
953
973
|
response.raise_for_status()
|
|
@@ -1054,6 +1074,14 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin):
|
|
|
1054
1074
|
|
|
1055
1075
|
return base64.b64encode(f":{token}".encode()).decode()
|
|
1056
1076
|
|
|
1077
|
+
def _auth_headers(self) -> dict[str, str]:
|
|
1078
|
+
"""Return authorization headers based on token type."""
|
|
1079
|
+
if not self.api_token:
|
|
1080
|
+
return {}
|
|
1081
|
+
if self.auth_scheme == "bearer":
|
|
1082
|
+
return {"Authorization": f"Bearer {self.api_token}"}
|
|
1083
|
+
return {"Authorization": f"Basic {self._encode_pat(self.api_token)}"}
|
|
1084
|
+
|
|
1057
1085
|
def _work_item_exists(self, work_item_id: int | str, org: str, project: str) -> bool:
|
|
1058
1086
|
"""
|
|
1059
1087
|
Check if a work item exists in Azure DevOps.
|
|
@@ -1078,8 +1106,8 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin):
|
|
|
1078
1106
|
|
|
1079
1107
|
url = f"{self.base_url}/{org}/{project}/_apis/wit/workitems/{work_item_id}?api-version=7.1"
|
|
1080
1108
|
headers = {
|
|
1081
|
-
"Authorization": f"Basic {self._encode_pat(self.api_token)}",
|
|
1082
1109
|
"Accept": "application/json",
|
|
1110
|
+
**self._auth_headers(),
|
|
1083
1111
|
}
|
|
1084
1112
|
|
|
1085
1113
|
try:
|
|
@@ -1121,8 +1149,8 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin):
|
|
|
1121
1149
|
|
|
1122
1150
|
url = f"{self.base_url}/{org}/{project}/_apis/wit/workitems/{work_item_id}?api-version=7.1"
|
|
1123
1151
|
headers = {
|
|
1124
|
-
"Authorization": f"Basic {self._encode_pat(self.api_token)}",
|
|
1125
1152
|
"Accept": "application/json",
|
|
1153
|
+
**self._auth_headers(),
|
|
1126
1154
|
}
|
|
1127
1155
|
|
|
1128
1156
|
try:
|
|
@@ -1165,8 +1193,8 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin):
|
|
|
1165
1193
|
}
|
|
1166
1194
|
url = f"{self.base_url}/{org}/{project}/_apis/wit/wiql?api-version=7.1"
|
|
1167
1195
|
headers = {
|
|
1168
|
-
"Authorization": f"Basic {self._encode_pat(self.api_token)}",
|
|
1169
1196
|
"Content-Type": "application/json",
|
|
1197
|
+
**self._auth_headers(),
|
|
1170
1198
|
}
|
|
1171
1199
|
|
|
1172
1200
|
try:
|
|
@@ -1208,6 +1236,7 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin):
|
|
|
1208
1236
|
title = proposal_data.get("title", "Untitled Change Proposal")
|
|
1209
1237
|
description = proposal_data.get("description", "")
|
|
1210
1238
|
rationale = proposal_data.get("rationale", "")
|
|
1239
|
+
impact = proposal_data.get("impact", "")
|
|
1211
1240
|
status = proposal_data.get("status", "proposed")
|
|
1212
1241
|
change_id = proposal_data.get("change_id", "unknown")
|
|
1213
1242
|
raw_title, raw_body = self._extract_raw_fields(proposal_data)
|
|
@@ -1243,8 +1272,16 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin):
|
|
|
1243
1272
|
body_parts.append(line)
|
|
1244
1273
|
body_parts.append("") # Blank line
|
|
1245
1274
|
|
|
1275
|
+
if impact:
|
|
1276
|
+
body_parts.append("## Impact")
|
|
1277
|
+
body_parts.append("")
|
|
1278
|
+
impact_lines = impact.strip().split("\n")
|
|
1279
|
+
for line in impact_lines:
|
|
1280
|
+
body_parts.append(line)
|
|
1281
|
+
body_parts.append("")
|
|
1282
|
+
|
|
1246
1283
|
# If no content, add placeholder
|
|
1247
|
-
if not body_parts or (not rationale and not description):
|
|
1284
|
+
if not body_parts or (not rationale and not description and not impact):
|
|
1248
1285
|
body_parts.append("No description provided.")
|
|
1249
1286
|
body_parts.append("")
|
|
1250
1287
|
|
|
@@ -1268,8 +1305,8 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin):
|
|
|
1268
1305
|
# Create work item via Azure DevOps API
|
|
1269
1306
|
url = f"{self.base_url}/{org}/{project}/_apis/wit/workitems/${work_item_type}?api-version=7.1"
|
|
1270
1307
|
headers = {
|
|
1271
|
-
"Authorization": f"Basic {self._encode_pat(self.api_token)}",
|
|
1272
1308
|
"Content-Type": "application/json-patch+json",
|
|
1309
|
+
**self._auth_headers(),
|
|
1273
1310
|
}
|
|
1274
1311
|
|
|
1275
1312
|
# Build JSON Patch document for work item creation
|
|
@@ -1406,8 +1443,8 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin):
|
|
|
1406
1443
|
# Update work item state via Azure DevOps API
|
|
1407
1444
|
url = f"{self.base_url}/{org}/{project}/_apis/wit/workitems/{work_item_id}?api-version=7.1"
|
|
1408
1445
|
headers = {
|
|
1409
|
-
"Authorization": f"Basic {self._encode_pat(self.api_token)}",
|
|
1410
1446
|
"Content-Type": "application/json-patch+json",
|
|
1447
|
+
**self._auth_headers(),
|
|
1411
1448
|
}
|
|
1412
1449
|
patch_document = [{"op": "replace", "path": "/fields/System.State", "value": ado_state}]
|
|
1413
1450
|
|
|
@@ -1450,6 +1487,7 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin):
|
|
|
1450
1487
|
title = proposal_data.get("title", "Untitled Change Proposal")
|
|
1451
1488
|
description = proposal_data.get("description", "")
|
|
1452
1489
|
rationale = proposal_data.get("rationale", "")
|
|
1490
|
+
impact = proposal_data.get("impact", "")
|
|
1453
1491
|
status = proposal_data.get("status", "proposed")
|
|
1454
1492
|
change_id = proposal_data.get("change_id", "unknown")
|
|
1455
1493
|
raw_title, raw_body = self._extract_raw_fields(proposal_data)
|
|
@@ -1485,8 +1523,16 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin):
|
|
|
1485
1523
|
body_parts.append(line)
|
|
1486
1524
|
body_parts.append("") # Blank line
|
|
1487
1525
|
|
|
1526
|
+
if impact:
|
|
1527
|
+
body_parts.append("## Impact")
|
|
1528
|
+
body_parts.append("")
|
|
1529
|
+
impact_lines = impact.strip().split("\n")
|
|
1530
|
+
for line in impact_lines:
|
|
1531
|
+
body_parts.append(line)
|
|
1532
|
+
body_parts.append("")
|
|
1533
|
+
|
|
1488
1534
|
# If no content, add placeholder
|
|
1489
|
-
if not body_parts or (not rationale and not description):
|
|
1535
|
+
if not body_parts or (not rationale and not description and not impact):
|
|
1490
1536
|
body_parts.append("No description provided.")
|
|
1491
1537
|
body_parts.append("")
|
|
1492
1538
|
|
|
@@ -1507,8 +1553,8 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin):
|
|
|
1507
1553
|
# Update work item body and state via Azure DevOps API
|
|
1508
1554
|
url = f"{self.base_url}/{org}/{project}/_apis/wit/workitems/{work_item_id}?api-version=7.1"
|
|
1509
1555
|
headers = {
|
|
1510
|
-
"Authorization": f"Basic {self._encode_pat(self.api_token)}",
|
|
1511
1556
|
"Content-Type": "application/json-patch+json",
|
|
1557
|
+
**self._auth_headers(),
|
|
1512
1558
|
}
|
|
1513
1559
|
|
|
1514
1560
|
# Build JSON Patch document for work item update
|
|
@@ -1623,8 +1669,8 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin):
|
|
|
1623
1669
|
# Update work item state via Azure DevOps API
|
|
1624
1670
|
url = f"{self.base_url}/{org}/{project}/_apis/wit/workitems/{work_item_id}?api-version=7.1"
|
|
1625
1671
|
headers = {
|
|
1626
|
-
"Authorization": f"Basic {self._encode_pat(self.api_token)}",
|
|
1627
1672
|
"Content-Type": "application/json-patch+json",
|
|
1673
|
+
**self._auth_headers(),
|
|
1628
1674
|
}
|
|
1629
1675
|
|
|
1630
1676
|
# Build JSON Patch document for state update
|
|
@@ -1971,8 +2017,8 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin):
|
|
|
1971
2017
|
# Azure DevOps API for adding comments to work items
|
|
1972
2018
|
url = f"{self.base_url}/{org}/{project}/_apis/wit/workitems/{work_item_id}/comments?api-version=7.1"
|
|
1973
2019
|
headers = {
|
|
1974
|
-
"Authorization": f"Basic {self._encode_pat(self.api_token)}",
|
|
1975
2020
|
"Content-Type": "application/json",
|
|
2021
|
+
**self._auth_headers(),
|
|
1976
2022
|
}
|
|
1977
2023
|
|
|
1978
2024
|
# Build request body for comment
|
|
@@ -30,6 +30,7 @@ from specfact_cli.adapters.base import BridgeAdapter
|
|
|
30
30
|
from specfact_cli.models.bridge import BridgeConfig
|
|
31
31
|
from specfact_cli.models.capabilities import ToolCapabilities
|
|
32
32
|
from specfact_cli.models.change import ChangeProposal, ChangeTracking
|
|
33
|
+
from specfact_cli.utils.auth_tokens import get_token
|
|
33
34
|
|
|
34
35
|
|
|
35
36
|
console = Console()
|
|
@@ -96,23 +97,38 @@ class GitHubAdapter(BridgeAdapter, BacklogAdapterMixin):
|
|
|
96
97
|
Args:
|
|
97
98
|
repo_owner: GitHub repository owner (optional, can be auto-detected)
|
|
98
99
|
repo_name: GitHub repository name (optional, can be auto-detected)
|
|
99
|
-
api_token: GitHub API token (optional, uses GITHUB_TOKEN env var or gh CLI)
|
|
100
|
+
api_token: GitHub API token (optional, uses GITHUB_TOKEN env var, stored auth token, or gh CLI)
|
|
100
101
|
use_gh_cli: If True, try to get token from GitHub CLI (`gh auth token`)
|
|
101
102
|
"""
|
|
102
103
|
self.repo_owner = repo_owner
|
|
103
104
|
self.repo_name = repo_name
|
|
104
105
|
|
|
105
|
-
|
|
106
|
+
stored_token = get_token("github")
|
|
107
|
+
|
|
108
|
+
# Token resolution order: explicit token > env var > stored token > gh CLI (if enabled)
|
|
109
|
+
token_source = "none"
|
|
106
110
|
if api_token:
|
|
107
111
|
self.api_token = api_token
|
|
112
|
+
token_source = "explicit"
|
|
108
113
|
elif os.environ.get("GITHUB_TOKEN"):
|
|
109
114
|
self.api_token = os.environ.get("GITHUB_TOKEN")
|
|
115
|
+
token_source = "env"
|
|
116
|
+
elif stored_token:
|
|
117
|
+
self.api_token = stored_token.get("access_token")
|
|
118
|
+
token_source = "stored"
|
|
110
119
|
elif use_gh_cli:
|
|
111
120
|
self.api_token = _get_github_token_from_gh_cli()
|
|
121
|
+
if self.api_token:
|
|
122
|
+
token_source = "gh_cli"
|
|
112
123
|
else:
|
|
113
124
|
self.api_token = None
|
|
114
125
|
|
|
115
|
-
|
|
126
|
+
env_api_url = os.environ.get("GITHUB_API_URL")
|
|
127
|
+
stored_api_url = stored_token.get("api_base_url") if stored_token else None
|
|
128
|
+
if token_source == "stored":
|
|
129
|
+
self.base_url = stored_api_url or env_api_url or "https://api.github.com"
|
|
130
|
+
else:
|
|
131
|
+
self.base_url = env_api_url or stored_api_url or "https://api.github.com"
|
|
116
132
|
|
|
117
133
|
# BacklogAdapterMixin abstract method implementations
|
|
118
134
|
|
|
@@ -225,12 +241,13 @@ class GitHubAdapter(BridgeAdapter, BacklogAdapterMixin):
|
|
|
225
241
|
body = item_data.get("body", "") or ""
|
|
226
242
|
description = ""
|
|
227
243
|
rationale = ""
|
|
244
|
+
impact = ""
|
|
228
245
|
|
|
229
246
|
# Parse markdown sections (Why, What Changes)
|
|
230
247
|
if body:
|
|
231
248
|
# Extract "Why" section (stop at What Changes or OpenSpec footer)
|
|
232
249
|
why_match = re.search(
|
|
233
|
-
r"##\s+Why\s*\n(.*?)(?=\n##\s+What\s+Changes\s|\n---\s*\n\*OpenSpec Change Proposal:|\Z)",
|
|
250
|
+
r"##\s+Why\s*\n(.*?)(?=\n##\s+What\s+Changes\s|\n##\s+Impact\s|\n---\s*\n\*OpenSpec Change Proposal:|\Z)",
|
|
234
251
|
body,
|
|
235
252
|
re.DOTALL | re.IGNORECASE,
|
|
236
253
|
)
|
|
@@ -239,7 +256,7 @@ class GitHubAdapter(BridgeAdapter, BacklogAdapterMixin):
|
|
|
239
256
|
|
|
240
257
|
# Extract "What Changes" section (stop at OpenSpec footer)
|
|
241
258
|
what_match = re.search(
|
|
242
|
-
r"##\s+What\s+Changes\s*\n(.*?)(?=\n---\s*\n\*OpenSpec Change Proposal:|\Z)",
|
|
259
|
+
r"##\s+What\s+Changes\s*\n(.*?)(?=\n##\s+Impact\s|\n---\s*\n\*OpenSpec Change Proposal:|\Z)",
|
|
243
260
|
body,
|
|
244
261
|
re.DOTALL | re.IGNORECASE,
|
|
245
262
|
)
|
|
@@ -250,6 +267,14 @@ class GitHubAdapter(BridgeAdapter, BacklogAdapterMixin):
|
|
|
250
267
|
body_clean = re.sub(r"\n---\s*\n\*OpenSpec Change Proposal:.*", "", body, flags=re.DOTALL)
|
|
251
268
|
description = body_clean.strip()
|
|
252
269
|
|
|
270
|
+
impact_match = re.search(
|
|
271
|
+
r"##\s+Impact\s*\n(.*?)(?=\n---\s*\n\*OpenSpec Change Proposal:|\Z)",
|
|
272
|
+
body,
|
|
273
|
+
re.DOTALL | re.IGNORECASE,
|
|
274
|
+
)
|
|
275
|
+
if impact_match:
|
|
276
|
+
impact = impact_match.group(1).strip()
|
|
277
|
+
|
|
253
278
|
# Extract change ID from OpenSpec metadata footer or issue number
|
|
254
279
|
change_id = None
|
|
255
280
|
if body:
|
|
@@ -330,6 +355,7 @@ class GitHubAdapter(BridgeAdapter, BacklogAdapterMixin):
|
|
|
330
355
|
"title": title,
|
|
331
356
|
"description": description,
|
|
332
357
|
"rationale": rationale,
|
|
358
|
+
"impact": impact,
|
|
333
359
|
"status": status,
|
|
334
360
|
"created_at": created_at,
|
|
335
361
|
"timeline": timeline,
|
|
@@ -529,7 +555,8 @@ class GitHubAdapter(BridgeAdapter, BacklogAdapterMixin):
|
|
|
529
555
|
" 1. Set GITHUB_TOKEN environment variable\n"
|
|
530
556
|
" 2. Provide via --github-token option\n"
|
|
531
557
|
" 3. Use GitHub CLI: `gh auth login` (auto-detected if available)\n"
|
|
532
|
-
" 4. Use --use-gh-cli flag to explicitly use GitHub CLI token"
|
|
558
|
+
" 4. Use --use-gh-cli flag to explicitly use GitHub CLI token\n"
|
|
559
|
+
" 5. Run `specfact auth github` for device code authentication"
|
|
533
560
|
)
|
|
534
561
|
raise ValueError(msg)
|
|
535
562
|
|
|
@@ -922,6 +949,7 @@ class GitHubAdapter(BridgeAdapter, BacklogAdapterMixin):
|
|
|
922
949
|
title = proposal_data.get("title", "Untitled Change Proposal")
|
|
923
950
|
description = proposal_data.get("description", "")
|
|
924
951
|
rationale = proposal_data.get("rationale", "")
|
|
952
|
+
impact = proposal_data.get("impact", "")
|
|
925
953
|
status = proposal_data.get("status", "proposed")
|
|
926
954
|
change_id = proposal_data.get("change_id", "unknown")
|
|
927
955
|
raw_title, raw_body = self._extract_raw_fields(proposal_data)
|
|
@@ -959,14 +987,24 @@ class GitHubAdapter(BridgeAdapter, BacklogAdapterMixin):
|
|
|
959
987
|
body_parts.append(line)
|
|
960
988
|
body_parts.append("") # Blank line
|
|
961
989
|
|
|
990
|
+
# Add Impact section if present
|
|
991
|
+
if impact:
|
|
992
|
+
body_parts.append("## Impact")
|
|
993
|
+
body_parts.append("")
|
|
994
|
+
impact_lines = impact.strip().split("\n")
|
|
995
|
+
for line in impact_lines:
|
|
996
|
+
body_parts.append(line)
|
|
997
|
+
body_parts.append("")
|
|
998
|
+
|
|
962
999
|
# If no content, add placeholder
|
|
963
1000
|
if not body_parts or (not rationale and not description):
|
|
964
1001
|
body_parts.append("No description provided.")
|
|
965
1002
|
body_parts.append("")
|
|
966
1003
|
|
|
967
|
-
# Add OpenSpec metadata footer
|
|
968
|
-
|
|
969
|
-
|
|
1004
|
+
# Add OpenSpec metadata footer (avoid duplicates)
|
|
1005
|
+
if not any("OpenSpec Change Proposal:" in line for line in body_parts):
|
|
1006
|
+
body_parts.append("---")
|
|
1007
|
+
body_parts.append(f"*OpenSpec Change Proposal: `{change_id}`*")
|
|
970
1008
|
|
|
971
1009
|
body = "\n".join(body_parts)
|
|
972
1010
|
|
|
@@ -1172,6 +1210,7 @@ class GitHubAdapter(BridgeAdapter, BacklogAdapterMixin):
|
|
|
1172
1210
|
title = proposal_data.get("title", "Untitled Change Proposal")
|
|
1173
1211
|
description = proposal_data.get("description", "")
|
|
1174
1212
|
rationale = proposal_data.get("rationale", "")
|
|
1213
|
+
impact = proposal_data.get("impact", "")
|
|
1175
1214
|
change_id = proposal_data.get("change_id", "unknown")
|
|
1176
1215
|
status = proposal_data.get("status", "proposed")
|
|
1177
1216
|
raw_title, raw_body = self._extract_raw_fields(proposal_data)
|
|
@@ -1238,6 +1277,15 @@ class GitHubAdapter(BridgeAdapter, BacklogAdapterMixin):
|
|
|
1238
1277
|
body_parts.append(line)
|
|
1239
1278
|
body_parts.append("") # Blank line
|
|
1240
1279
|
|
|
1280
|
+
# Add Impact section if present
|
|
1281
|
+
if impact:
|
|
1282
|
+
body_parts.append("## Impact")
|
|
1283
|
+
body_parts.append("")
|
|
1284
|
+
impact_lines = impact.strip().split("\n")
|
|
1285
|
+
for line in impact_lines:
|
|
1286
|
+
body_parts.append(line)
|
|
1287
|
+
body_parts.append("") # Blank line
|
|
1288
|
+
|
|
1241
1289
|
# If no content, add placeholder
|
|
1242
1290
|
if not body_parts or (not rationale and not description):
|
|
1243
1291
|
body_parts.append("No description provided.")
|
|
@@ -1252,9 +1300,10 @@ class GitHubAdapter(BridgeAdapter, BacklogAdapterMixin):
|
|
|
1252
1300
|
body_parts.append("") # Blank line before preserved section
|
|
1253
1301
|
body_parts.append(preserved_clean)
|
|
1254
1302
|
|
|
1255
|
-
# Add OpenSpec metadata footer
|
|
1256
|
-
|
|
1257
|
-
|
|
1303
|
+
# Add OpenSpec metadata footer (avoid duplicates)
|
|
1304
|
+
if not any("OpenSpec Change Proposal:" in line for line in body_parts):
|
|
1305
|
+
body_parts.append("---")
|
|
1306
|
+
body_parts.append(f"*OpenSpec Change Proposal: `{change_id}`*")
|
|
1258
1307
|
|
|
1259
1308
|
body = "\n".join(body_parts)
|
|
1260
1309
|
|
|
@@ -400,6 +400,7 @@ Dependencies: {len(dependencies)} dependency files found
|
|
|
400
400
|
|
|
401
401
|
|
|
402
402
|
# CrossHair property-based test functions
|
|
403
|
+
# CrossHair: skip (side-effectful imports via GitPython)
|
|
403
404
|
# These functions are designed for CrossHair symbolic execution analysis
|
|
404
405
|
@beartype
|
|
405
406
|
def test_generate_prompt_property(command: str, context: dict[str, Any] | None) -> None:
|
|
@@ -55,6 +55,7 @@ from specfact_cli import __version__, runtime
|
|
|
55
55
|
# Import command modules
|
|
56
56
|
from specfact_cli.commands import (
|
|
57
57
|
analyze,
|
|
58
|
+
auth,
|
|
58
59
|
contract_cmd,
|
|
59
60
|
drift,
|
|
60
61
|
enforce,
|
|
@@ -303,11 +304,14 @@ def main(
|
|
|
303
304
|
# 1. Setup & Initialization
|
|
304
305
|
app.add_typer(init.app, name="init", help="Initialize SpecFact for IDE integration")
|
|
305
306
|
|
|
307
|
+
# 1.5. Authentication
|
|
308
|
+
app.add_typer(auth.app, name="auth", help="Authenticate with DevOps providers (GitHub, Azure DevOps)")
|
|
309
|
+
|
|
306
310
|
# 2. Import & Analysis
|
|
307
311
|
app.add_typer(
|
|
308
312
|
import_cmd.app,
|
|
309
313
|
name="import",
|
|
310
|
-
help="Import codebases and external tool projects (e.g., Spec-Kit, OpenSpec,
|
|
314
|
+
help="Import codebases and external tool projects (e.g., Spec-Kit, OpenSpec, generic-markdown)",
|
|
311
315
|
)
|
|
312
316
|
|
|
313
317
|
# 2.5. Migration
|
|
@@ -6,6 +6,7 @@ This package contains all CLI command implementations.
|
|
|
6
6
|
|
|
7
7
|
from specfact_cli.commands import (
|
|
8
8
|
analyze,
|
|
9
|
+
auth,
|
|
9
10
|
contract_cmd,
|
|
10
11
|
drift,
|
|
11
12
|
enforce,
|
|
@@ -25,6 +26,7 @@ from specfact_cli.commands import (
|
|
|
25
26
|
|
|
26
27
|
__all__ = [
|
|
27
28
|
"analyze",
|
|
29
|
+
"auth",
|
|
28
30
|
"contract_cmd",
|
|
29
31
|
"drift",
|
|
30
32
|
"enforce",
|