ai-dev-workflow 0.1.1__tar.gz → 0.2.0__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 (44) hide show
  1. {ai_dev_workflow-0.1.1 → ai_dev_workflow-0.2.0}/PKG-INFO +1 -1
  2. {ai_dev_workflow-0.1.1 → ai_dev_workflow-0.2.0}/pyproject.toml +1 -1
  3. {ai_dev_workflow-0.1.1 → ai_dev_workflow-0.2.0}/tests/test_ai_workflow_cli.py +1 -1
  4. {ai_dev_workflow-0.1.1 → ai_dev_workflow-0.2.0}/tests/test_setup_wizard.py +74 -1
  5. {ai_dev_workflow-0.1.1 → ai_dev_workflow-0.2.0}/tools/ai_dev_workflow.egg-info/PKG-INFO +1 -1
  6. {ai_dev_workflow-0.1.1 → ai_dev_workflow-0.2.0}/tools/ai_workflow_cli.py +70 -21
  7. ai_dev_workflow-0.2.0/tools/setup_wizard.py +446 -0
  8. ai_dev_workflow-0.1.1/tools/setup_wizard.py +0 -221
  9. {ai_dev_workflow-0.1.1 → ai_dev_workflow-0.2.0}/README.md +0 -0
  10. {ai_dev_workflow-0.1.1 → ai_dev_workflow-0.2.0}/setup.cfg +0 -0
  11. {ai_dev_workflow-0.1.1 → ai_dev_workflow-0.2.0}/tests/test_ado_provider.py +0 -0
  12. {ai_dev_workflow-0.1.1 → ai_dev_workflow-0.2.0}/tests/test_ai_provider.py +0 -0
  13. {ai_dev_workflow-0.1.1 → ai_dev_workflow-0.2.0}/tests/test_bootstrap_project.py +0 -0
  14. {ai_dev_workflow-0.1.1 → ai_dev_workflow-0.2.0}/tests/test_codespaces_environment.py +0 -0
  15. {ai_dev_workflow-0.1.1 → ai_dev_workflow-0.2.0}/tests/test_codex_cloud_executor.py +0 -0
  16. {ai_dev_workflow-0.1.1 → ai_dev_workflow-0.2.0}/tests/test_composite_work_item.py +0 -0
  17. {ai_dev_workflow-0.1.1 → ai_dev_workflow-0.2.0}/tests/test_connection_and_mcp.py +0 -0
  18. {ai_dev_workflow-0.1.1 → ai_dev_workflow-0.2.0}/tests/test_framework_update.py +0 -0
  19. {ai_dev_workflow-0.1.1 → ai_dev_workflow-0.2.0}/tests/test_general_init.py +0 -0
  20. {ai_dev_workflow-0.1.1 → ai_dev_workflow-0.2.0}/tests/test_instruction_adapter.py +0 -0
  21. {ai_dev_workflow-0.1.1 → ai_dev_workflow-0.2.0}/tests/test_issue_publisher.py +0 -0
  22. {ai_dev_workflow-0.1.1 → ai_dev_workflow-0.2.0}/tests/test_package_install.py +0 -0
  23. {ai_dev_workflow-0.1.1 → ai_dev_workflow-0.2.0}/tests/test_planning_context.py +0 -0
  24. {ai_dev_workflow-0.1.1 → ai_dev_workflow-0.2.0}/tests/test_queue_dispatcher.py +0 -0
  25. {ai_dev_workflow-0.1.1 → ai_dev_workflow-0.2.0}/tests/test_worker_registry.py +0 -0
  26. {ai_dev_workflow-0.1.1 → ai_dev_workflow-0.2.0}/tools/ado_mcp.py +0 -0
  27. {ai_dev_workflow-0.1.1 → ai_dev_workflow-0.2.0}/tools/ado_provider.py +0 -0
  28. {ai_dev_workflow-0.1.1 → ai_dev_workflow-0.2.0}/tools/ai_dev_workflow.egg-info/SOURCES.txt +0 -0
  29. {ai_dev_workflow-0.1.1 → ai_dev_workflow-0.2.0}/tools/ai_dev_workflow.egg-info/dependency_links.txt +0 -0
  30. {ai_dev_workflow-0.1.1 → ai_dev_workflow-0.2.0}/tools/ai_dev_workflow.egg-info/entry_points.txt +0 -0
  31. {ai_dev_workflow-0.1.1 → ai_dev_workflow-0.2.0}/tools/ai_dev_workflow.egg-info/requires.txt +0 -0
  32. {ai_dev_workflow-0.1.1 → ai_dev_workflow-0.2.0}/tools/ai_dev_workflow.egg-info/top_level.txt +0 -0
  33. {ai_dev_workflow-0.1.1 → ai_dev_workflow-0.2.0}/tools/ai_provider.py +0 -0
  34. {ai_dev_workflow-0.1.1 → ai_dev_workflow-0.2.0}/tools/composite_work_item.py +0 -0
  35. {ai_dev_workflow-0.1.1 → ai_dev_workflow-0.2.0}/tools/framework_update.py +0 -0
  36. {ai_dev_workflow-0.1.1 → ai_dev_workflow-0.2.0}/tools/github_work_item_provider.py +0 -0
  37. {ai_dev_workflow-0.1.1 → ai_dev_workflow-0.2.0}/tools/instruction_adapter.py +0 -0
  38. {ai_dev_workflow-0.1.1 → ai_dev_workflow-0.2.0}/tools/issue_publisher.py +0 -0
  39. {ai_dev_workflow-0.1.1 → ai_dev_workflow-0.2.0}/tools/planning_context.py +0 -0
  40. {ai_dev_workflow-0.1.1 → ai_dev_workflow-0.2.0}/tools/setup_context.py +0 -0
  41. {ai_dev_workflow-0.1.1 → ai_dev_workflow-0.2.0}/tools/source_connection.py +0 -0
  42. {ai_dev_workflow-0.1.1 → ai_dev_workflow-0.2.0}/tools/work_item_provider.py +0 -0
  43. {ai_dev_workflow-0.1.1 → ai_dev_workflow-0.2.0}/tools/work_item_registry.py +0 -0
  44. {ai_dev_workflow-0.1.1 → ai_dev_workflow-0.2.0}/tools/worker_registry.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ai-dev-workflow
3
- Version: 0.1.1
3
+ Version: 0.2.0
4
4
  Summary: Provider-neutral AI-assisted development workflow and CLI
5
5
  Author: vuthethienlong
6
6
  Project-URL: Homepage, https://github.com/vuthethienlong/ai-dev-workflow
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ai-dev-workflow"
7
- version = "0.1.1"
7
+ version = "0.2.0"
8
8
  description = "Provider-neutral AI-assisted development workflow and CLI"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -16,7 +16,7 @@ DISCOVERY={
16
16
 
17
17
  class Tests(unittest.TestCase):
18
18
  def test_package_version(self):
19
- self.assertEqual(PACKAGE_VERSION, "0.1.1")
19
+ self.assertEqual(PACKAGE_VERSION, "0.2.0")
20
20
 
21
21
  def test_init_generates_explicit_mapping_and_agents(self):
22
22
  cfg=build_ado_config(DISCOVERY)
@@ -4,7 +4,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools"))
4
4
 
5
5
  from github_work_item_provider import build_github_mapping, suggest_github_state, validate_github_mapping
6
6
  from setup_context import analyze_repository, setup_agent_context
7
- from setup_wizard import build_setup_plan, credential_requirements
7
+ from setup_wizard import build_setup_plan, credential_requirements, interactive_setup_options, interactive_workspace_options, confirm_setup_plan
8
8
 
9
9
  class Tests(unittest.TestCase):
10
10
  def test_context_analysis_detects_project_signals(self):
@@ -61,4 +61,77 @@ class Tests(unittest.TestCase):
61
61
  self.assertEqual(suggest_github_state("Awaiting runtime verification"),"runtime-pending")
62
62
  self.assertIsNone(suggest_github_state("customer-impact"))
63
63
 
64
+
65
+ def test_full_wizard_collects_github_and_multi_repo(self):
66
+ class Args:
67
+ root="."
68
+ work_items=None
69
+ github_repository=None
70
+ github_auth=None
71
+ github_token_env="GH_TOKEN"
72
+ ado_organization=None
73
+ ado_project=None
74
+ ado_token_env="AZURE_DEVOPS_TOKEN"
75
+ ado_mcp_mode=None
76
+ ado_mcp_auth=None
77
+ ado_team=None
78
+ ai_provider=[]
79
+ role_provider=[]
80
+ worker=[]
81
+ environment=None
82
+ codespaces_mode=None
83
+ tdd=None
84
+ reviewer_mode=None
85
+ runtime_mode=None
86
+
87
+ answers=iter([
88
+ ".", # root
89
+ "1", # GitHub Issues
90
+ "owner/main", # repository
91
+ "1", # gh-cli
92
+ "y", # multi-repo
93
+ "dragon-workspace", # workspace name
94
+ "main", # primary ref
95
+ "primary-game", # primary purpose
96
+ "y", # add repo
97
+ "owner/shared", # second repo
98
+ "shared-lib", # purpose
99
+ "integration", # ref
100
+ "1", # read
101
+ "AGENTS.md|docs/**", # context
102
+ "n", # no more repos
103
+ "owner/main", # default repo
104
+ "n", # no Claude
105
+ "", "", "", "", "", "", # role defaults
106
+ "n", # no workers
107
+ "1", # env none
108
+ "", # TDD default yes
109
+ "1", # reviewer manual
110
+ "1", # runtime project-defined
111
+ ])
112
+ args=interactive_setup_options(Args(), input_fn=lambda _: next(answers), output_fn=lambda *_: None)
113
+ self.assertEqual(args.work_items,"github-issues")
114
+ self.assertEqual(args.github_repository,"owner/main")
115
+ self.assertEqual(args.workspace_config["workspace"]["name"],"dragon-workspace")
116
+ self.assertEqual(len(args.workspace_config["repositories"]),2)
117
+ self.assertEqual(args.workspace_config["repositories"][1]["repository"],"owner/shared")
118
+ self.assertEqual(args.workspace_config["planning"]["default_repository"],"owner/main")
119
+ self.assertTrue(args.tdd)
120
+
121
+ def test_workspace_can_be_skipped(self):
122
+ class Args:
123
+ github_repository="owner/repo"
124
+ answers=iter(["n"])
125
+ self.assertIsNone(interactive_workspace_options(Args(), input_fn=lambda _: next(answers), output_fn=lambda *_: None))
126
+
127
+ def test_review_can_cancel_apply(self):
128
+ plan={
129
+ "work_items":{"provider":"github-issues"},
130
+ "providers":{"codex":{}},
131
+ "workers":[],
132
+ "execution_environment":{"type":"none"},
133
+ "workflow":{"tdd":True,"reviewer":{"mode":"manual"},"runtime_verification":{"mode":"project-defined"}},
134
+ }
135
+ self.assertFalse(confirm_setup_plan(plan,input_fn=lambda _:"n",output_fn=lambda *_:None))
136
+
64
137
  if __name__=="__main__": unittest.main()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ai-dev-workflow
3
- Version: 0.1.1
3
+ Version: 0.2.0
4
4
  Summary: Provider-neutral AI-assisted development workflow and CLI
5
5
  Author: vuthethienlong
6
6
  Project-URL: Homepage, https://github.com/vuthethienlong/ai-dev-workflow
@@ -13,7 +13,7 @@ except ImportError:
13
13
 
14
14
  from ado_provider import AzureDevOps, build_state_mapping, validate_mapping, validate_field_mapping
15
15
  from work_item_provider import REQUIRED_AGENT_ROLES, REQUIRED_CANONICAL_STATES
16
- from setup_wizard import build_setup_plan, discover_provider, interactive_mapping, interactive_ado_field_mapping, render_user_steps, apply_github_status_bootstrap
16
+ from setup_wizard import build_setup_plan, discover_provider, interactive_mapping, interactive_ado_field_mapping, render_user_steps, apply_github_status_bootstrap, interactive_setup_options, confirm_setup_plan
17
17
  from github_work_item_provider import validate_github_mapping
18
18
  from ai_provider import provider_template, validate_provider_registry
19
19
  from source_connection import resolve_github_token
@@ -22,6 +22,7 @@ from ado_mcp import build_ado_mcp
22
22
  from worker_registry import build_worker, validate_worker_registry
23
23
  from framework_update import DEFAULT_FRAMEWORK_SOURCE, ensure_framework_metadata, build_update_plan, apply_update
24
24
  from instruction_adapter import adapter_for_provider, ensure_provider_instruction_file
25
+ from planning_context import normalize_workspace
25
26
 
26
27
 
27
28
  DEFAULT_ROLE_PROVIDERS = {
@@ -33,7 +34,7 @@ DEFAULT_ROLE_PROVIDERS = {
33
34
  "RuntimeVerifierAgent": "project-defined",
34
35
  }
35
36
 
36
- PACKAGE_VERSION = "0.1.1"
37
+ PACKAGE_VERSION = "0.2.0"
37
38
 
38
39
  ROLE_KEYS = {
39
40
  "SetupAgent": "setup",
@@ -386,6 +387,7 @@ def build_environment(environment: str, codespaces_mode: str) -> dict:
386
387
 
387
388
 
388
389
  def perform_init(args, ado_api_factory=AzureDevOps) -> tuple[dict, dict | None]:
390
+ root = Path(getattr(args, "root", "."))
389
391
  roles = parse_role_provider(args.role_provider)
390
392
  discovery = None
391
393
  if args.work_items == "azure-devops":
@@ -407,6 +409,8 @@ def perform_init(args, ado_api_factory=AzureDevOps) -> tuple[dict, dict | None]:
407
409
  )
408
410
 
409
411
  output = Path(args.output)
412
+ if not output.is_absolute():
413
+ output = root / output
410
414
  existing = _load_yaml(output) if output.exists() else {}
411
415
  config = generated if args.force or not existing else merge_owned(existing, generated)
412
416
  output.write_text(_dump_yaml(config), encoding="utf-8")
@@ -419,17 +423,17 @@ def perform_init(args, ado_api_factory=AzureDevOps) -> tuple[dict, dict | None]:
419
423
  )
420
424
 
421
425
  if not args.no_files:
422
- ensure_file(Path("AGENTS.md"), generated_agents_md(), args.force)
423
- ensure_file(Path("CURRENT.md"), generated_current_md(), args.force)
426
+ ensure_file(root / "AGENTS.md", generated_agents_md(), args.force)
427
+ ensure_file(root / "CURRENT.md", generated_current_md(), args.force)
424
428
  if args.environment == "codespaces":
425
429
  ensure_file(
426
- Path(".devcontainer/devcontainer.json"),
430
+ root / ".devcontainer/devcontainer.json",
427
431
  generated_devcontainer(),
428
432
  args.force,
429
433
  )
430
434
  if args.work_items == "github-issues":
431
435
  ensure_file(
432
- Path(".github/workflows/ai-workflow.yml"),
436
+ root / ".github/workflows/ai-workflow.yml",
433
437
  generated_github_caller(args.framework_ref),
434
438
  args.force,
435
439
  )
@@ -480,6 +484,20 @@ def init_ado(args) -> int:
480
484
 
481
485
 
482
486
  def setup_cmd(args) -> int:
487
+ if not args.non_interactive:
488
+ args = interactive_setup_options(args)
489
+
490
+ required = []
491
+ if args.work_items in ("github-issues", "composite") and not args.github_repository:
492
+ required.append("GitHub repository")
493
+ if args.work_items in ("azure-devops", "composite"):
494
+ if not args.ado_organization:
495
+ required.append("ADO organization")
496
+ if not args.ado_project:
497
+ required.append("ADO project")
498
+ if required:
499
+ raise RuntimeError("missing setup values: " + ", ".join(required))
500
+
483
501
  github_auth = None
484
502
  if args.github_repository:
485
503
  _, github_auth = resolve_github_token(args.github_auth, args.github_token_env)
@@ -537,12 +555,18 @@ def setup_cmd(args) -> int:
537
555
  ado_field_mapping=ado_field_mapping,
538
556
  )
539
557
  plan["work_items"]["mapping_confirmed"] = not args.plan_only
540
- Path(args.plan_output).write_text(_dump_yaml(plan), encoding="utf-8")
558
+ plan_output = Path(args.plan_output)
559
+ if not plan_output.is_absolute():
560
+ plan_output = Path(args.root) / plan_output
561
+ plan_output.write_text(_dump_yaml(plan), encoding="utf-8")
541
562
  print(render_user_steps(plan))
542
- print(f"\nWrote setup plan: {args.plan_output}")
563
+ print(f"\nWrote setup plan: {plan_output}")
543
564
 
544
565
  if args.plan_only:
545
566
  return 0
567
+ if not args.non_interactive and not confirm_setup_plan(plan):
568
+ print("Setup cancelled; no project configuration was applied.")
569
+ return 0
546
570
 
547
571
  if args.work_items == "github-issues":
548
572
  discovery, mapping, created = apply_github_status_bootstrap(args, mapping)
@@ -552,6 +576,7 @@ def setup_cmd(args) -> int:
552
576
  class InitArgs:
553
577
  pass
554
578
  init = InitArgs()
579
+ init.root = args.root
555
580
  init.work_items = "azure-devops" if args.work_items == "composite" else args.work_items
556
581
  init.ado_organization = args.ado_organization
557
582
  init.ado_project = args.ado_project
@@ -607,7 +632,23 @@ def setup_cmd(args) -> int:
607
632
  )
608
633
  config["framework"]["source"] = framework_source
609
634
  config["framework"]["ref"] = framework_version
610
- Path(args.output).write_text(_dump_yaml(config), encoding="utf-8")
635
+ config_output = Path(args.output)
636
+ if not config_output.is_absolute():
637
+ config_output = Path(args.root) / config_output
638
+ config_output.write_text(_dump_yaml(config), encoding="utf-8")
639
+
640
+ workspace_config = getattr(args, "workspace_config", None)
641
+ if workspace_config:
642
+ normalized_workspace = normalize_workspace(workspace_config)
643
+ workspace_path = Path(args.root) / ".ai-workspace.yml"
644
+ workspace_path.write_text(_dump_yaml(workspace_config), encoding="utf-8")
645
+ config["planning_context"] = {
646
+ "workspace_file": ".ai-workspace.yml",
647
+ **normalized_workspace["planning_context"],
648
+ }
649
+ config_output.write_text(_dump_yaml(config), encoding="utf-8")
650
+ print(f"Wrote workspace registry: {workspace_path}")
651
+
611
652
  errors = doctor(config, discovery)
612
653
  if errors:
613
654
  print("Doctor findings:")
@@ -721,25 +762,25 @@ def main() -> int:
721
762
 
722
763
  setup = sub.add_parser("setup")
723
764
  setup.add_argument("--root", default=".")
724
- setup.add_argument("--work-items", choices=["github-issues", "azure-devops", "composite"], default="github-issues")
765
+ setup.add_argument("--work-items", choices=["github-issues", "azure-devops", "composite"])
725
766
  setup.add_argument("--github-repository")
726
767
  setup.add_argument("--github-token-env", default="GH_TOKEN")
727
- setup.add_argument("--github-auth", choices=["gh-cli","token-env"], default="gh-cli")
768
+ setup.add_argument("--github-auth", choices=["gh-cli","token-env"])
728
769
  setup.add_argument("--ado-organization")
729
770
  setup.add_argument("--ado-project")
730
771
  setup.add_argument("--ado-token-env", default="AZURE_DEVOPS_TOKEN")
731
- setup.add_argument("--ado-mcp-mode", choices=["none","remote","local"], default="remote")
732
- setup.add_argument("--ado-mcp-auth", choices=["interactive","azcli","env","envvar","pat"], default="interactive")
772
+ setup.add_argument("--ado-mcp-mode", choices=["none","remote","local"])
773
+ setup.add_argument("--ado-mcp-auth", choices=["interactive","azcli","env","envvar","pat"])
733
774
  setup.add_argument("--ado-team")
734
775
  setup.add_argument("--ado-field", action="append", default=[])
735
- setup.add_argument("--environment", choices=["none", "codespaces"], default="none")
736
- setup.add_argument("--codespaces-mode", choices=["off", "on-demand", "always-on"], default="on-demand")
776
+ setup.add_argument("--environment", choices=["none", "codespaces"])
777
+ setup.add_argument("--codespaces-mode", choices=["off", "on-demand", "always-on"])
737
778
  setup.add_argument("--role-provider", action="append", default=[])
738
779
  setup.add_argument("--ai-provider", action="append", default=[])
739
780
  setup.add_argument("--worker", action="append", default=[])
740
- setup.add_argument("--tdd", action=argparse.BooleanOptionalAction, default=True)
741
- setup.add_argument("--reviewer-mode", choices=["manual", "automatic", "none"], default="manual")
742
- setup.add_argument("--runtime-mode", choices=["project-defined", "batched", "none"], default="project-defined")
781
+ setup.add_argument("--tdd", action=argparse.BooleanOptionalAction, default=None)
782
+ setup.add_argument("--reviewer-mode", choices=["manual", "automatic", "none"])
783
+ setup.add_argument("--runtime-mode", choices=["project-defined", "batched", "none"])
743
784
  setup.add_argument("--output", default=".ai-workflow.yml")
744
785
  setup.add_argument("--plan-output", default=".ai-setup-plan.yml")
745
786
  setup.add_argument("--framework-ref", default="vuthethienlong/ai-dev-workflow/.github/workflows/queue-dispatcher.yml@main")
@@ -764,11 +805,19 @@ def main() -> int:
764
805
  doc.set_defaults(func=doctor_cmd)
765
806
 
766
807
  args = parser.parse_args()
767
- if args.command in ("init", "setup") and args.work_items in ("azure-devops", "composite"):
808
+ if args.command == "init" and args.work_items == "azure-devops":
768
809
  if not args.ado_organization or not args.ado_project:
769
810
  parser.error("--ado-organization and --ado-project are required for azure-devops")
770
- if args.command == "setup" and args.work_items in ("github-issues", "composite") and not args.github_repository:
771
- parser.error("--github-repository is required for GitHub status discovery")
811
+ if args.command == "setup" and args.non_interactive:
812
+ args.work_items = args.work_items or "github-issues"
813
+ args.github_auth = args.github_auth or "gh-cli"
814
+ args.ado_mcp_mode = args.ado_mcp_mode or "remote"
815
+ args.ado_mcp_auth = args.ado_mcp_auth or "interactive"
816
+ args.environment = args.environment or "none"
817
+ args.codespaces_mode = args.codespaces_mode or "on-demand"
818
+ args.tdd = True if args.tdd is None else args.tdd
819
+ args.reviewer_mode = args.reviewer_mode or "manual"
820
+ args.runtime_mode = args.runtime_mode or "project-defined"
772
821
  return args.func(args)
773
822
 
774
823
 
@@ -0,0 +1,446 @@
1
+ """Step-by-step repository setup for ai-dev-workflow."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ from pathlib import Path
6
+
7
+ from ado_provider import AzureDevOps, build_state_mapping, available_fields
8
+ from github_work_item_provider import GitHubIssues, build_github_mapping, ensure_managed_status_labels
9
+ from setup_context import analyze_repository, setup_agent_context
10
+ from ai_provider import credential_requirements_for_providers, provider_template
11
+ from source_connection import resolve_github_token
12
+ from ado_mcp import build_ado_mcp, ado_mcp_credentials
13
+ from worker_registry import worker_credential_requirements
14
+
15
+ CANONICAL_HELP = (
16
+ "draft, ready, in-progress, in-review, runtime-pending, "
17
+ "release-pending, blocked, completed"
18
+ )
19
+
20
+ DEFAULT_WORKFLOW = {
21
+ "tdd": True,
22
+ "reviewer": {"mode": "manual"},
23
+ "runtime_verification": {"mode": "project-defined"},
24
+ }
25
+
26
+
27
+ def credential_requirements(work_items: str, environment: str, agents: dict, providers: dict | None = None, github_auth=None, ado_mcp=None, workers: list[dict] | None = None) -> list[dict]:
28
+ requirements = []
29
+ if work_items in ("github-issues", "composite"):
30
+ if (github_auth or {}).get("type") == "gh-cli-oauth":
31
+ requirements.append({
32
+ "name": "GitHub CLI OAuth session",
33
+ "purpose": "Authenticate GitHub API through existing gh auth login session.",
34
+ "required_for_setup": True,
35
+ "store": "GitHub CLI credential store",
36
+ })
37
+ else:
38
+ requirements.append({
39
+ "name": (github_auth or {}).get("env", "GH_TOKEN"),
40
+ "purpose": "Authenticate GitHub API using token environment fallback.",
41
+ "required_for_setup": True,
42
+ "store": "environment or GitHub Actions secret; never commit",
43
+ })
44
+ if work_items in ("azure-devops", "composite"):
45
+ requirements.append({
46
+ "name": "AZURE_DEVOPS_TOKEN",
47
+ "purpose": "Discover ADO process/WIT states and manage work items.",
48
+ "required_for_setup": True,
49
+ "store": "environment/secret store; never commit",
50
+ })
51
+ if environment == "codespaces":
52
+ requirements.append({
53
+ "name": "CODESPACES_TOKEN",
54
+ "purpose": "Provision/start/stop GitHub Codespaces worker environments.",
55
+ "required_for_setup": False,
56
+ "store": "GitHub Actions repository secret",
57
+ })
58
+ requirements.extend(credential_requirements_for_providers(providers or {}))
59
+ requirements.extend(ado_mcp_credentials(ado_mcp or {}))
60
+ requirements.extend(worker_credential_requirements(workers or [], work_items))
61
+ return requirements
62
+
63
+
64
+ def build_setup_plan(
65
+ root=".",
66
+ work_items="github-issues",
67
+ discovery=None,
68
+ mapping=None,
69
+ workflow=None,
70
+ agents=None,
71
+ environment=None,
72
+ providers=None,
73
+ github_auth=None,
74
+ ado_mcp=None,
75
+ workers=None,
76
+ ado_field_mapping=None,
77
+ ) -> dict:
78
+ analysis = analyze_repository(root)
79
+ if providers is None:
80
+ providers = {}
81
+ referenced = {item.get("provider") for item in (agents or {}).values() if isinstance(item, dict)}
82
+ if "codex" in referenced:
83
+ providers["codex"] = provider_template("chatgpt-codex")
84
+ if "manual" in referenced:
85
+ providers["manual"] = provider_template("interactive")
86
+ return {
87
+ "version": 1,
88
+ "status": "review-required",
89
+ "phases": [
90
+ {"id": 1, "name": "connect-source-host", "status": "complete"},
91
+ {"id": 2, "name": "analyze-context", "status": "complete"},
92
+ {"id": 3, "name": "select-work-item-provider", "status": "complete"},
93
+ {"id": 4, "name": "connect-work-item-provider", "status": "complete"},
94
+ {"id": 5, "name": "discover-existing-statuses", "status": "complete" if discovery is not None else "pending"},
95
+ {"id": 6, "name": "confirm-status-mapping", "status": "review-required"},
96
+ {"id": 7, "name": "configure-workflow-and-agents", "status": "review-required"},
97
+ {"id": 8, "name": "register-workers", "status": "review-required"},
98
+ {"id": 9, "name": "configure-environment-and-credentials", "status": "review-required"},
99
+ {"id": 10, "name": "doctor-and-apply", "status": "pending"},
100
+ ],
101
+ "context": analysis,
102
+ "setup_agent": setup_agent_context(analysis),
103
+ "source_connection": {"github": github_auth or {}},
104
+ "work_items": {
105
+ "provider": work_items,
106
+ "discovery": discovery,
107
+ "suggested_mapping": mapping or {},
108
+ "mapping_confirmed": False,
109
+ "ado_field_mapping": ado_field_mapping or {},
110
+ },
111
+ "workflow": workflow or DEFAULT_WORKFLOW,
112
+ "agents": agents or {},
113
+ "providers": providers or {},
114
+ "ado_mcp": ado_mcp or {},
115
+ "workers": workers or [],
116
+ "execution_environment": environment or {"type": "none"},
117
+ "credential_requirements": credential_requirements(
118
+ work_items,
119
+ (environment or {"type": "none"}).get("type", "none"),
120
+ agents or {},
121
+ providers or {},
122
+ github_auth,
123
+ ado_mcp,
124
+ workers or [],
125
+ ),
126
+ }
127
+
128
+
129
+ def discover_provider(args):
130
+ if args.work_items in ("azure-devops", "composite"):
131
+ token = os.environ.get(args.ado_token_env)
132
+ if not token:
133
+ raise RuntimeError(f"{args.ado_token_env} is required for ADO discovery")
134
+ discovery = AzureDevOps(args.ado_organization, token).discover(args.ado_project)
135
+ return discovery, build_state_mapping(discovery)
136
+
137
+ token, _ = resolve_github_token(args.github_auth, args.github_token_env)
138
+ discovery = GitHubIssues(args.github_repository, token).discover()
139
+ return discovery, build_github_mapping(discovery)
140
+
141
+
142
+ def interactive_ado_field_mapping(discovery: dict, input_fn=input, output_fn=print) -> dict:
143
+ result = {}
144
+ output_fn("\nADO existing-field mapping (existing fields only; setup will not create fields)")
145
+ for wit in discovery.get("work_item_types", []):
146
+ ref = wit["reference_name"]
147
+ fields = available_fields(discovery, ref)
148
+ if not fields:
149
+ continue
150
+ output_fn(f"\nWork Item Type: {wit['name']} ({ref})")
151
+ by_ref = {f["reference_name"]: f for f in fields}
152
+ configured = {}
153
+ for key in ("workflow_phase","github_issue","worker","agent_role","pull_request","test_status","runtime_status","workflow_profile"):
154
+ field_ref = input_fn(f" Map {key} to existing field reference (blank=skip): ").strip()
155
+ if not field_ref:
156
+ continue
157
+ if field_ref not in by_ref:
158
+ raise ValueError(f"ADO field does not exist for {ref}: {field_ref}")
159
+ default = input_fn(f" Default value for {key} (blank=no default): ")
160
+ item = {"field": field_ref}
161
+ if default != "":
162
+ item["default"] = default
163
+ configured[key] = item
164
+ if configured:
165
+ result[ref] = configured
166
+ return result
167
+
168
+
169
+ def interactive_mapping(provider: str, discovery: dict, suggestions: dict, input_fn=input, output_fn=print):
170
+ output_fn(f"Canonical states: {CANONICAL_HELP}")
171
+ if provider == "azure-devops":
172
+ result = {}
173
+ for wit in discovery.get("work_item_types", []):
174
+ ref = wit["reference_name"]
175
+ result[ref] = {}
176
+ output_fn(f"\nWork Item Type: {wit['name']} ({ref})")
177
+ for state in wit.get("states", []):
178
+ if state.get("hidden"):
179
+ continue
180
+ name = state["name"]
181
+ default = suggestions.get(ref, {}).get(name, "draft")
182
+ value = input_fn(f" {name} [{default}]: ").strip() or default
183
+ result[ref][name] = value
184
+ return result
185
+
186
+ result = {}
187
+ labels = discovery.get("labels", [])
188
+ for item in labels:
189
+ name = item["name"]
190
+ default = suggestions.get(name)
191
+ prompt_default = default or "skip"
192
+ value = input_fn(f" {name} [{prompt_default}]: ").strip()
193
+ if not value:
194
+ value = default
195
+ if value and value != "skip":
196
+ result[name] = value
197
+ return result
198
+
199
+
200
+ def apply_github_status_bootstrap(args, mapping: dict) -> tuple[dict, dict, list[str]]:
201
+ token, _ = resolve_github_token(args.github_auth, args.github_token_env)
202
+ api = GitHubIssues(args.github_repository, token)
203
+ before = api.discover()
204
+ created = ensure_managed_status_labels(api, before)
205
+ after = api.discover() if created else before
206
+ refreshed = build_github_mapping(after)
207
+ refreshed.update(mapping)
208
+ return after, refreshed, created
209
+
210
+
211
+ def render_user_steps(plan: dict) -> str:
212
+ lines = ["Setup phases:"]
213
+ for phase in plan["phases"]:
214
+ lines.append(f"{phase['id']}. {phase['name']} — {phase['status']}")
215
+ lines.append("")
216
+ lines.append("Manual credential/settings checklist:")
217
+ if not plan["credential_requirements"]:
218
+ lines.append("- none")
219
+ for item in plan["credential_requirements"]:
220
+ lines.append(f"- {item['name']}: {item['purpose']} Store: {item['store']}.")
221
+ return "\n".join(lines)
222
+
223
+
224
+ def _prompt(input_fn, text: str, default: str | None = None) -> str:
225
+ suffix = f" [{default}]" if default not in (None, "") else ""
226
+ value = input_fn(f"{text}{suffix}: ").strip()
227
+ return value if value else (default or "")
228
+
229
+
230
+ def _choice(input_fn, output_fn, title: str, options: list[tuple[str, str]], default: str) -> str:
231
+ output_fn(title)
232
+ by_number = {}
233
+ allowed = set()
234
+ for index, (value, label) in enumerate(options, 1):
235
+ by_number[str(index)] = value
236
+ allowed.add(value)
237
+ marker = " (default)" if value == default else ""
238
+ output_fn(f" {index}. {label}{marker}")
239
+ while True:
240
+ raw = _prompt(input_fn, "Choose", default)
241
+ value = by_number.get(raw, raw)
242
+ if value in allowed:
243
+ return value
244
+ output_fn("Invalid choice. Try again.")
245
+
246
+
247
+ def _yes_no(input_fn, text: str, default: bool = False) -> bool:
248
+ hint = "Y/n" if default else "y/N"
249
+ raw = input_fn(f"{text} [{hint}]: ").strip().lower()
250
+ if not raw:
251
+ return default
252
+ return raw in ("y", "yes", "1", "true", "on")
253
+
254
+
255
+ def interactive_setup_options(args, input_fn=input, output_fn=print):
256
+ """Collect a complete setup configuration while preserving explicit CLI flags."""
257
+ output_fn("ai-dev-workflow setup wizard")
258
+ output_fn("Press Enter to accept the value shown in brackets.\n")
259
+
260
+ args.root = _prompt(input_fn, "1/9 Project root", args.root or ".")
261
+ args.work_items = args.work_items or _choice(
262
+ input_fn, output_fn, "2/9 Work Item Provider",
263
+ [
264
+ ("github-issues", "GitHub Issues only"),
265
+ ("azure-devops", "Azure DevOps only"),
266
+ ("composite", "Azure DevOps business items + GitHub execution issues"),
267
+ ],
268
+ "github-issues",
269
+ )
270
+
271
+ if args.work_items in ("github-issues", "composite"):
272
+ args.github_repository = args.github_repository or _prompt(
273
+ input_fn, "3/9 GitHub repository (owner/repo)"
274
+ )
275
+ args.github_auth = args.github_auth or _choice(
276
+ input_fn, output_fn, "GitHub authentication",
277
+ [("gh-cli", "GitHub CLI login (recommended)"), ("token-env", "Token environment variable")],
278
+ "gh-cli",
279
+ )
280
+ if args.github_auth == "token-env":
281
+ args.github_token_env = _prompt(input_fn, "GitHub token env", args.github_token_env or "GH_TOKEN")
282
+
283
+ if args.work_items in ("azure-devops", "composite"):
284
+ output_fn("4/9 Azure DevOps settings")
285
+ args.ado_organization = args.ado_organization or _prompt(input_fn, "ADO organization")
286
+ args.ado_project = args.ado_project or _prompt(input_fn, "ADO project")
287
+ args.ado_token_env = _prompt(input_fn, "ADO token env", args.ado_token_env or "AZURE_DEVOPS_TOKEN")
288
+ args.ado_mcp_mode = args.ado_mcp_mode or _choice(
289
+ input_fn, output_fn, "ADO MCP mode",
290
+ [("none", "Disabled"), ("remote", "Remote MCP"), ("local", "Local MCP")],
291
+ "remote",
292
+ )
293
+ if args.ado_mcp_mode != "none":
294
+ args.ado_mcp_auth = args.ado_mcp_auth or _choice(
295
+ input_fn, output_fn, "ADO MCP authentication",
296
+ [
297
+ ("interactive", "Interactive"),
298
+ ("azcli", "Azure CLI"),
299
+ ("env", "Environment"),
300
+ ("envvar", "Environment variable"),
301
+ ("pat", "PAT"),
302
+ ],
303
+ "interactive",
304
+ )
305
+
306
+ workspace = interactive_workspace_options(args, input_fn=input_fn, output_fn=output_fn)
307
+ args.workspace_config = workspace
308
+
309
+ output_fn("5/10 AI providers")
310
+ if not args.ai_provider and _yes_no(input_fn, "Add Claude Code / Anthropic provider?", False):
311
+ provider_id = _prompt(input_fn, "Provider id", "claude")
312
+ env = _prompt(input_fn, "Anthropic API key env", "ANTHROPIC_API_KEY")
313
+ args.ai_provider.append(f"{provider_id}=anthropic,auth=api-key-env,env={env}")
314
+
315
+ output_fn("6/10 Agent role assignment")
316
+ role_defaults = {
317
+ "SetupAgent": "manual",
318
+ "ArchitectAgent": "manual",
319
+ "TesterAgent": "codex",
320
+ "ImplementerAgent": "codex",
321
+ "ReviewerAgent": "manual",
322
+ "RuntimeVerifierAgent": "project-defined",
323
+ }
324
+ if not args.role_provider:
325
+ for role, default in role_defaults.items():
326
+ provider = _prompt(input_fn, f"{role} provider", default)
327
+ args.role_provider.append(f"{role}={provider}")
328
+
329
+ output_fn("7/10 Workers")
330
+ if not args.worker:
331
+ while _yes_no(input_fn, "Register a worker?", False):
332
+ worker_id = _prompt(input_fn, "Worker id")
333
+ mode = _choice(
334
+ input_fn, output_fn, "Worker mode",
335
+ [("planner", "Planner"), ("executor", "Executor"), ("hybrid", "Hybrid")],
336
+ "hybrid",
337
+ )
338
+ provider = _prompt(input_fn, "Provider id", "codex")
339
+ environment = _choice(
340
+ input_fn, output_fn, "Worker environment",
341
+ [("local", "Local"), ("codespaces", "GitHub Codespaces"), ("cloud", "Cloud")],
342
+ "local",
343
+ )
344
+ if mode == "planner":
345
+ default_roles = "SetupAgent|ArchitectAgent|ReviewerAgent"
346
+ elif mode == "executor":
347
+ default_roles = "TesterAgent|ImplementerAgent|RuntimeVerifierAgent"
348
+ else:
349
+ default_roles = "ArchitectAgent|TesterAgent|ImplementerAgent|ReviewerAgent"
350
+ roles = _prompt(input_fn, "Roles separated by |", default_roles)
351
+ capacity = _prompt(input_fn, "Capacity", "1")
352
+ parts = [mode, f"provider={provider}", f"environment={environment}", f"roles={roles}", f"capacity={capacity}"]
353
+ if environment == "local" and mode in ("executor", "hybrid"):
354
+ assignee = _prompt(input_fn, "GitHub/Work Item assignee for local claim ownership")
355
+ if assignee:
356
+ parts.append(f"assignee={assignee}")
357
+ parts.append("managed=false")
358
+ args.worker.append(f"{worker_id}=" + ",".join(parts))
359
+
360
+ args.environment = args.environment or _choice(
361
+ input_fn, output_fn, "8/10 Framework execution environment",
362
+ [("none", "No managed environment"), ("codespaces", "GitHub Codespaces")],
363
+ "none",
364
+ )
365
+ if args.environment == "codespaces":
366
+ args.codespaces_mode = args.codespaces_mode or _choice(
367
+ input_fn, output_fn, "Codespaces mode",
368
+ [("off", "Off"), ("on-demand", "On demand"), ("always-on", "Always on")],
369
+ "on-demand",
370
+ )
371
+
372
+ output_fn("9/10 Workflow policy")
373
+ if args.tdd is None:
374
+ args.tdd = _yes_no(input_fn, "Require TDD/test design", True)
375
+ args.reviewer_mode = args.reviewer_mode or _choice(
376
+ input_fn, output_fn, "Reviewer mode",
377
+ [("manual", "Manual"), ("automatic", "Automatic"), ("none", "Disabled")],
378
+ "manual",
379
+ )
380
+ args.runtime_mode = args.runtime_mode or _choice(
381
+ input_fn, output_fn, "Runtime verification",
382
+ [("project-defined", "Project-defined"), ("batched", "Batched runtime verification"), ("none", "Disabled")],
383
+ "project-defined",
384
+ )
385
+ output_fn("10/10 Review will be shown after provider discovery and status mapping.")
386
+ return args
387
+
388
+
389
+ def confirm_setup_plan(plan: dict, input_fn=input, output_fn=print) -> bool:
390
+ output_fn("\nReview")
391
+ output_fn(f"- Work Item Provider: {plan['work_items']['provider']}")
392
+ output_fn(f"- AI providers: {', '.join(plan.get('providers', {}).keys()) or 'none'}")
393
+ output_fn(f"- Workers: {len(plan.get('workers', []))}")
394
+ output_fn(f"- Environment: {plan.get('execution_environment', {}).get('type', 'none')}")
395
+ workflow = plan.get("workflow", {})
396
+ output_fn(f"- TDD: {workflow.get('tdd')}")
397
+ output_fn(f"- Reviewer: {(workflow.get('reviewer') or {}).get('mode')}")
398
+ output_fn(f"- Runtime verification: {(workflow.get('runtime_verification') or {}).get('mode')}")
399
+ return _yes_no(input_fn, "Apply this configuration?", True)
400
+
401
+
402
+ def interactive_workspace_options(args, input_fn=input, output_fn=print) -> dict | None:
403
+ """Collect optional multi-repository workspace configuration."""
404
+ if not _yes_no(input_fn, "Use a multi-repository workspace?", False):
405
+ return None
406
+
407
+ output_fn("\nMulti-repository workspace")
408
+ name = _prompt(input_fn, "Workspace name", "ai-workspace")
409
+ repositories = []
410
+
411
+ primary_repo = args.github_repository or _prompt(input_fn, "Primary repository (owner/repo)")
412
+ primary_ref = _prompt(input_fn, "Primary repository ref", "main")
413
+ repositories.append({
414
+ "repository": primary_repo,
415
+ "purpose": _prompt(input_fn, "Primary repository purpose", "primary-application"),
416
+ "ref": primary_ref,
417
+ "access": "read-write",
418
+ "context": ["AGENTS.md", "CURRENT.md", "docs/**"],
419
+ })
420
+
421
+ while _yes_no(input_fn, "Add another repository?", False):
422
+ repository = _prompt(input_fn, "Repository (owner/repo)")
423
+ purpose = _prompt(input_fn, "Purpose", "shared-dependency")
424
+ ref = _prompt(input_fn, "Ref", "main")
425
+ access = _choice(
426
+ input_fn, output_fn, "Access",
427
+ [("read", "Read only"), ("read-write", "Read/write")],
428
+ "read",
429
+ )
430
+ context_raw = _prompt(input_fn, "Context paths separated by |", "AGENTS.md|docs/**")
431
+ repositories.append({
432
+ "repository": repository,
433
+ "purpose": purpose,
434
+ "ref": ref,
435
+ "access": access,
436
+ "context": [item for item in context_raw.split("|") if item],
437
+ })
438
+
439
+ default_repository = _prompt(input_fn, "Default planning repository", primary_repo)
440
+ return {
441
+ "version": 1,
442
+ "workspace": {"name": name},
443
+ "repositories": repositories,
444
+ "planning": {"default_repository": default_repository},
445
+ "initiatives": {},
446
+ }
@@ -1,221 +0,0 @@
1
- """Step-by-step repository setup for ai-dev-workflow."""
2
- from __future__ import annotations
3
-
4
- import os
5
- from pathlib import Path
6
-
7
- from ado_provider import AzureDevOps, build_state_mapping, available_fields
8
- from github_work_item_provider import GitHubIssues, build_github_mapping, ensure_managed_status_labels
9
- from setup_context import analyze_repository, setup_agent_context
10
- from ai_provider import credential_requirements_for_providers, provider_template
11
- from source_connection import resolve_github_token
12
- from ado_mcp import build_ado_mcp, ado_mcp_credentials
13
- from worker_registry import worker_credential_requirements
14
-
15
- CANONICAL_HELP = (
16
- "draft, ready, in-progress, in-review, runtime-pending, "
17
- "release-pending, blocked, completed"
18
- )
19
-
20
- DEFAULT_WORKFLOW = {
21
- "tdd": True,
22
- "reviewer": {"mode": "manual"},
23
- "runtime_verification": {"mode": "project-defined"},
24
- }
25
-
26
-
27
- def credential_requirements(work_items: str, environment: str, agents: dict, providers: dict | None = None, github_auth=None, ado_mcp=None, workers: list[dict] | None = None) -> list[dict]:
28
- requirements = []
29
- if work_items in ("github-issues", "composite"):
30
- if (github_auth or {}).get("type") == "gh-cli-oauth":
31
- requirements.append({
32
- "name": "GitHub CLI OAuth session",
33
- "purpose": "Authenticate GitHub API through existing gh auth login session.",
34
- "required_for_setup": True,
35
- "store": "GitHub CLI credential store",
36
- })
37
- else:
38
- requirements.append({
39
- "name": (github_auth or {}).get("env", "GH_TOKEN"),
40
- "purpose": "Authenticate GitHub API using token environment fallback.",
41
- "required_for_setup": True,
42
- "store": "environment or GitHub Actions secret; never commit",
43
- })
44
- if work_items in ("azure-devops", "composite"):
45
- requirements.append({
46
- "name": "AZURE_DEVOPS_TOKEN",
47
- "purpose": "Discover ADO process/WIT states and manage work items.",
48
- "required_for_setup": True,
49
- "store": "environment/secret store; never commit",
50
- })
51
- if environment == "codespaces":
52
- requirements.append({
53
- "name": "CODESPACES_TOKEN",
54
- "purpose": "Provision/start/stop GitHub Codespaces worker environments.",
55
- "required_for_setup": False,
56
- "store": "GitHub Actions repository secret",
57
- })
58
- requirements.extend(credential_requirements_for_providers(providers or {}))
59
- requirements.extend(ado_mcp_credentials(ado_mcp or {}))
60
- requirements.extend(worker_credential_requirements(workers or [], work_items))
61
- return requirements
62
-
63
-
64
- def build_setup_plan(
65
- root=".",
66
- work_items="github-issues",
67
- discovery=None,
68
- mapping=None,
69
- workflow=None,
70
- agents=None,
71
- environment=None,
72
- providers=None,
73
- github_auth=None,
74
- ado_mcp=None,
75
- workers=None,
76
- ado_field_mapping=None,
77
- ) -> dict:
78
- analysis = analyze_repository(root)
79
- if providers is None:
80
- providers = {}
81
- referenced = {item.get("provider") for item in (agents or {}).values() if isinstance(item, dict)}
82
- if "codex" in referenced:
83
- providers["codex"] = provider_template("chatgpt-codex")
84
- if "manual" in referenced:
85
- providers["manual"] = provider_template("interactive")
86
- return {
87
- "version": 1,
88
- "status": "review-required",
89
- "phases": [
90
- {"id": 1, "name": "connect-source-host", "status": "complete"},
91
- {"id": 2, "name": "analyze-context", "status": "complete"},
92
- {"id": 3, "name": "select-work-item-provider", "status": "complete"},
93
- {"id": 4, "name": "connect-work-item-provider", "status": "complete"},
94
- {"id": 5, "name": "discover-existing-statuses", "status": "complete" if discovery is not None else "pending"},
95
- {"id": 6, "name": "confirm-status-mapping", "status": "review-required"},
96
- {"id": 7, "name": "configure-workflow-and-agents", "status": "review-required"},
97
- {"id": 8, "name": "register-workers", "status": "review-required"},
98
- {"id": 9, "name": "configure-environment-and-credentials", "status": "review-required"},
99
- {"id": 10, "name": "doctor-and-apply", "status": "pending"},
100
- ],
101
- "context": analysis,
102
- "setup_agent": setup_agent_context(analysis),
103
- "source_connection": {"github": github_auth or {}},
104
- "work_items": {
105
- "provider": work_items,
106
- "discovery": discovery,
107
- "suggested_mapping": mapping or {},
108
- "mapping_confirmed": False,
109
- "ado_field_mapping": ado_field_mapping or {},
110
- },
111
- "workflow": workflow or DEFAULT_WORKFLOW,
112
- "agents": agents or {},
113
- "providers": providers or {},
114
- "ado_mcp": ado_mcp or {},
115
- "workers": workers or [],
116
- "execution_environment": environment or {"type": "none"},
117
- "credential_requirements": credential_requirements(
118
- work_items,
119
- (environment or {"type": "none"}).get("type", "none"),
120
- agents or {},
121
- providers or {},
122
- github_auth,
123
- ado_mcp,
124
- workers or [],
125
- ),
126
- }
127
-
128
-
129
- def discover_provider(args):
130
- if args.work_items in ("azure-devops", "composite"):
131
- token = os.environ.get(args.ado_token_env)
132
- if not token:
133
- raise RuntimeError(f"{args.ado_token_env} is required for ADO discovery")
134
- discovery = AzureDevOps(args.ado_organization, token).discover(args.ado_project)
135
- return discovery, build_state_mapping(discovery)
136
-
137
- token, _ = resolve_github_token(args.github_auth, args.github_token_env)
138
- discovery = GitHubIssues(args.github_repository, token).discover()
139
- return discovery, build_github_mapping(discovery)
140
-
141
-
142
- def interactive_ado_field_mapping(discovery: dict, input_fn=input, output_fn=print) -> dict:
143
- result = {}
144
- output_fn("\nADO existing-field mapping (existing fields only; setup will not create fields)")
145
- for wit in discovery.get("work_item_types", []):
146
- ref = wit["reference_name"]
147
- fields = available_fields(discovery, ref)
148
- if not fields:
149
- continue
150
- output_fn(f"\nWork Item Type: {wit['name']} ({ref})")
151
- by_ref = {f["reference_name"]: f for f in fields}
152
- configured = {}
153
- for key in ("workflow_phase","github_issue","worker","agent_role","pull_request","test_status","runtime_status","workflow_profile"):
154
- field_ref = input_fn(f" Map {key} to existing field reference (blank=skip): ").strip()
155
- if not field_ref:
156
- continue
157
- if field_ref not in by_ref:
158
- raise ValueError(f"ADO field does not exist for {ref}: {field_ref}")
159
- default = input_fn(f" Default value for {key} (blank=no default): ")
160
- item = {"field": field_ref}
161
- if default != "":
162
- item["default"] = default
163
- configured[key] = item
164
- if configured:
165
- result[ref] = configured
166
- return result
167
-
168
-
169
- def interactive_mapping(provider: str, discovery: dict, suggestions: dict, input_fn=input, output_fn=print):
170
- output_fn(f"Canonical states: {CANONICAL_HELP}")
171
- if provider == "azure-devops":
172
- result = {}
173
- for wit in discovery.get("work_item_types", []):
174
- ref = wit["reference_name"]
175
- result[ref] = {}
176
- output_fn(f"\nWork Item Type: {wit['name']} ({ref})")
177
- for state in wit.get("states", []):
178
- if state.get("hidden"):
179
- continue
180
- name = state["name"]
181
- default = suggestions.get(ref, {}).get(name, "draft")
182
- value = input_fn(f" {name} [{default}]: ").strip() or default
183
- result[ref][name] = value
184
- return result
185
-
186
- result = {}
187
- labels = discovery.get("labels", [])
188
- for item in labels:
189
- name = item["name"]
190
- default = suggestions.get(name)
191
- prompt_default = default or "skip"
192
- value = input_fn(f" {name} [{prompt_default}]: ").strip()
193
- if not value:
194
- value = default
195
- if value and value != "skip":
196
- result[name] = value
197
- return result
198
-
199
-
200
- def apply_github_status_bootstrap(args, mapping: dict) -> tuple[dict, dict, list[str]]:
201
- token, _ = resolve_github_token(args.github_auth, args.github_token_env)
202
- api = GitHubIssues(args.github_repository, token)
203
- before = api.discover()
204
- created = ensure_managed_status_labels(api, before)
205
- after = api.discover() if created else before
206
- refreshed = build_github_mapping(after)
207
- refreshed.update(mapping)
208
- return after, refreshed, created
209
-
210
-
211
- def render_user_steps(plan: dict) -> str:
212
- lines = ["Setup phases:"]
213
- for phase in plan["phases"]:
214
- lines.append(f"{phase['id']}. {phase['name']} — {phase['status']}")
215
- lines.append("")
216
- lines.append("Manual credential/settings checklist:")
217
- if not plan["credential_requirements"]:
218
- lines.append("- none")
219
- for item in plan["credential_requirements"]:
220
- lines.append(f"- {item['name']}: {item['purpose']} Store: {item['store']}.")
221
- return "\n".join(lines)