ai-dev-workflow 0.1.0__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.0 → ai_dev_workflow-0.2.0}/PKG-INFO +1 -1
  2. {ai_dev_workflow-0.1.0 → ai_dev_workflow-0.2.0}/pyproject.toml +2 -1
  3. {ai_dev_workflow-0.1.0 → ai_dev_workflow-0.2.0}/tests/test_ai_workflow_cli.py +1 -1
  4. ai_dev_workflow-0.2.0/tests/test_instruction_adapter.py +54 -0
  5. {ai_dev_workflow-0.1.0 → ai_dev_workflow-0.2.0}/tests/test_setup_wizard.py +74 -1
  6. {ai_dev_workflow-0.1.0 → ai_dev_workflow-0.2.0}/tools/ai_dev_workflow.egg-info/PKG-INFO +1 -1
  7. {ai_dev_workflow-0.1.0 → ai_dev_workflow-0.2.0}/tools/ai_dev_workflow.egg-info/SOURCES.txt +2 -0
  8. {ai_dev_workflow-0.1.0 → ai_dev_workflow-0.2.0}/tools/ai_dev_workflow.egg-info/top_level.txt +1 -0
  9. {ai_dev_workflow-0.1.0 → ai_dev_workflow-0.2.0}/tools/ai_workflow_cli.py +85 -21
  10. ai_dev_workflow-0.2.0/tools/instruction_adapter.py +77 -0
  11. ai_dev_workflow-0.2.0/tools/setup_wizard.py +446 -0
  12. ai_dev_workflow-0.1.0/tools/setup_wizard.py +0 -221
  13. {ai_dev_workflow-0.1.0 → ai_dev_workflow-0.2.0}/README.md +0 -0
  14. {ai_dev_workflow-0.1.0 → ai_dev_workflow-0.2.0}/setup.cfg +0 -0
  15. {ai_dev_workflow-0.1.0 → ai_dev_workflow-0.2.0}/tests/test_ado_provider.py +0 -0
  16. {ai_dev_workflow-0.1.0 → ai_dev_workflow-0.2.0}/tests/test_ai_provider.py +0 -0
  17. {ai_dev_workflow-0.1.0 → ai_dev_workflow-0.2.0}/tests/test_bootstrap_project.py +0 -0
  18. {ai_dev_workflow-0.1.0 → ai_dev_workflow-0.2.0}/tests/test_codespaces_environment.py +0 -0
  19. {ai_dev_workflow-0.1.0 → ai_dev_workflow-0.2.0}/tests/test_codex_cloud_executor.py +0 -0
  20. {ai_dev_workflow-0.1.0 → ai_dev_workflow-0.2.0}/tests/test_composite_work_item.py +0 -0
  21. {ai_dev_workflow-0.1.0 → ai_dev_workflow-0.2.0}/tests/test_connection_and_mcp.py +0 -0
  22. {ai_dev_workflow-0.1.0 → ai_dev_workflow-0.2.0}/tests/test_framework_update.py +0 -0
  23. {ai_dev_workflow-0.1.0 → ai_dev_workflow-0.2.0}/tests/test_general_init.py +0 -0
  24. {ai_dev_workflow-0.1.0 → ai_dev_workflow-0.2.0}/tests/test_issue_publisher.py +0 -0
  25. {ai_dev_workflow-0.1.0 → ai_dev_workflow-0.2.0}/tests/test_package_install.py +0 -0
  26. {ai_dev_workflow-0.1.0 → ai_dev_workflow-0.2.0}/tests/test_planning_context.py +0 -0
  27. {ai_dev_workflow-0.1.0 → ai_dev_workflow-0.2.0}/tests/test_queue_dispatcher.py +0 -0
  28. {ai_dev_workflow-0.1.0 → ai_dev_workflow-0.2.0}/tests/test_worker_registry.py +0 -0
  29. {ai_dev_workflow-0.1.0 → ai_dev_workflow-0.2.0}/tools/ado_mcp.py +0 -0
  30. {ai_dev_workflow-0.1.0 → ai_dev_workflow-0.2.0}/tools/ado_provider.py +0 -0
  31. {ai_dev_workflow-0.1.0 → ai_dev_workflow-0.2.0}/tools/ai_dev_workflow.egg-info/dependency_links.txt +0 -0
  32. {ai_dev_workflow-0.1.0 → ai_dev_workflow-0.2.0}/tools/ai_dev_workflow.egg-info/entry_points.txt +0 -0
  33. {ai_dev_workflow-0.1.0 → ai_dev_workflow-0.2.0}/tools/ai_dev_workflow.egg-info/requires.txt +0 -0
  34. {ai_dev_workflow-0.1.0 → ai_dev_workflow-0.2.0}/tools/ai_provider.py +0 -0
  35. {ai_dev_workflow-0.1.0 → ai_dev_workflow-0.2.0}/tools/composite_work_item.py +0 -0
  36. {ai_dev_workflow-0.1.0 → ai_dev_workflow-0.2.0}/tools/framework_update.py +0 -0
  37. {ai_dev_workflow-0.1.0 → ai_dev_workflow-0.2.0}/tools/github_work_item_provider.py +0 -0
  38. {ai_dev_workflow-0.1.0 → ai_dev_workflow-0.2.0}/tools/issue_publisher.py +0 -0
  39. {ai_dev_workflow-0.1.0 → ai_dev_workflow-0.2.0}/tools/planning_context.py +0 -0
  40. {ai_dev_workflow-0.1.0 → ai_dev_workflow-0.2.0}/tools/setup_context.py +0 -0
  41. {ai_dev_workflow-0.1.0 → ai_dev_workflow-0.2.0}/tools/source_connection.py +0 -0
  42. {ai_dev_workflow-0.1.0 → ai_dev_workflow-0.2.0}/tools/work_item_provider.py +0 -0
  43. {ai_dev_workflow-0.1.0 → ai_dev_workflow-0.2.0}/tools/work_item_registry.py +0 -0
  44. {ai_dev_workflow-0.1.0 → 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.0
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.0"
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"
@@ -37,6 +37,7 @@ py-modules = [
37
37
  "composite_work_item",
38
38
  "framework_update",
39
39
  "github_work_item_provider",
40
+ "instruction_adapter",
40
41
  "issue_publisher",
41
42
  "planning_context",
42
43
  "setup_context",
@@ -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.0")
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)
@@ -0,0 +1,54 @@
1
+ import tempfile, unittest
2
+ from pathlib import Path
3
+
4
+ from instruction_adapter import (
5
+ START, END, adapter_for_provider, ensure_provider_instruction_file, merge_managed_block
6
+ )
7
+
8
+ class Tests(unittest.TestCase):
9
+ def test_codex_uses_agents_directly(self):
10
+ adapter = adapter_for_provider("chatgpt-codex")
11
+ self.assertEqual(adapter["shared"], "AGENTS.md")
12
+ self.assertIsNone(adapter["provider_file"])
13
+
14
+ def test_claude_adapter_targets_claude_md(self):
15
+ adapter = adapter_for_provider("anthropic")
16
+ self.assertEqual(adapter["provider_file"], "CLAUDE.md")
17
+ self.assertEqual(adapter["merge_policy"], "preserve-existing")
18
+
19
+ def test_existing_user_rules_are_preserved(self):
20
+ original = "# Claude rules\n\nNever modify generated files.\n"
21
+ merged = merge_managed_block(original)
22
+ self.assertTrue(merged.startswith(original))
23
+ self.assertIn(START, merged)
24
+ self.assertIn("AGENTS.md", merged)
25
+ self.assertIn("CURRENT.md", merged)
26
+
27
+ def test_existing_managed_block_is_replaced_not_duplicated(self):
28
+ original = f"before\n{START}\nold\n{END}\nafter\n"
29
+ merged = merge_managed_block(original)
30
+ self.assertEqual(merged.count(START), 1)
31
+ self.assertEqual(merged.count(END), 1)
32
+ self.assertTrue(merged.startswith("before\n"))
33
+ self.assertTrue(merged.endswith("\nafter\n"))
34
+
35
+ def test_idempotent_file_update(self):
36
+ with tempfile.TemporaryDirectory() as td:
37
+ path = Path(td) / "CLAUDE.md"
38
+ path.write_text("# Mine\n", encoding="utf-8")
39
+ first = ensure_provider_instruction_file(td, "anthropic")
40
+ once = path.read_text(encoding="utf-8")
41
+ second = ensure_provider_instruction_file(td, "anthropic")
42
+ twice = path.read_text(encoding="utf-8")
43
+ self.assertTrue(first["changed"])
44
+ self.assertFalse(second["changed"])
45
+ self.assertEqual(once, twice)
46
+ self.assertIn("# Mine", twice)
47
+
48
+ def test_current_is_conditional_context(self):
49
+ merged = merge_managed_block("")
50
+ self.assertIn("active ai-dev-workflow task", merged)
51
+ self.assertIn("not required for unrelated sessions", merged)
52
+
53
+ if __name__ == "__main__":
54
+ unittest.main()
@@ -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.0
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
@@ -10,6 +10,7 @@ tests/test_composite_work_item.py
10
10
  tests/test_connection_and_mcp.py
11
11
  tests/test_framework_update.py
12
12
  tests/test_general_init.py
13
+ tests/test_instruction_adapter.py
13
14
  tests/test_issue_publisher.py
14
15
  tests/test_package_install.py
15
16
  tests/test_planning_context.py
@@ -23,6 +24,7 @@ tools/ai_workflow_cli.py
23
24
  tools/composite_work_item.py
24
25
  tools/framework_update.py
25
26
  tools/github_work_item_provider.py
27
+ tools/instruction_adapter.py
26
28
  tools/issue_publisher.py
27
29
  tools/planning_context.py
28
30
  tools/setup_context.py
@@ -5,6 +5,7 @@ ai_workflow_cli
5
5
  composite_work_item
6
6
  framework_update
7
7
  github_work_item_provider
8
+ instruction_adapter
8
9
  issue_publisher
9
10
  planning_context
10
11
  setup_context
@@ -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
@@ -21,6 +21,8 @@ from work_item_registry import validate_work_item_provider
21
21
  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
+ from instruction_adapter import adapter_for_provider, ensure_provider_instruction_file
25
+ from planning_context import normalize_workspace
24
26
 
25
27
 
26
28
  DEFAULT_ROLE_PROVIDERS = {
@@ -32,7 +34,7 @@ DEFAULT_ROLE_PROVIDERS = {
32
34
  "RuntimeVerifierAgent": "project-defined",
33
35
  }
34
36
 
35
- PACKAGE_VERSION = "0.1.0"
37
+ PACKAGE_VERSION = "0.2.0"
36
38
 
37
39
  ROLE_KEYS = {
38
40
  "SetupAgent": "setup",
@@ -385,6 +387,7 @@ def build_environment(environment: str, codespaces_mode: str) -> dict:
385
387
 
386
388
 
387
389
  def perform_init(args, ado_api_factory=AzureDevOps) -> tuple[dict, dict | None]:
390
+ root = Path(getattr(args, "root", "."))
388
391
  roles = parse_role_provider(args.role_provider)
389
392
  discovery = None
390
393
  if args.work_items == "azure-devops":
@@ -406,6 +409,8 @@ def perform_init(args, ado_api_factory=AzureDevOps) -> tuple[dict, dict | None]:
406
409
  )
407
410
 
408
411
  output = Path(args.output)
412
+ if not output.is_absolute():
413
+ output = root / output
409
414
  existing = _load_yaml(output) if output.exists() else {}
410
415
  config = generated if args.force or not existing else merge_owned(existing, generated)
411
416
  output.write_text(_dump_yaml(config), encoding="utf-8")
@@ -418,17 +423,17 @@ def perform_init(args, ado_api_factory=AzureDevOps) -> tuple[dict, dict | None]:
418
423
  )
419
424
 
420
425
  if not args.no_files:
421
- ensure_file(Path("AGENTS.md"), generated_agents_md(), args.force)
422
- 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)
423
428
  if args.environment == "codespaces":
424
429
  ensure_file(
425
- Path(".devcontainer/devcontainer.json"),
430
+ root / ".devcontainer/devcontainer.json",
426
431
  generated_devcontainer(),
427
432
  args.force,
428
433
  )
429
434
  if args.work_items == "github-issues":
430
435
  ensure_file(
431
- Path(".github/workflows/ai-workflow.yml"),
436
+ root / ".github/workflows/ai-workflow.yml",
432
437
  generated_github_caller(args.framework_ref),
433
438
  args.force,
434
439
  )
@@ -479,6 +484,20 @@ def init_ado(args) -> int:
479
484
 
480
485
 
481
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
+
482
501
  github_auth = None
483
502
  if args.github_repository:
484
503
  _, github_auth = resolve_github_token(args.github_auth, args.github_token_env)
@@ -536,12 +555,18 @@ def setup_cmd(args) -> int:
536
555
  ado_field_mapping=ado_field_mapping,
537
556
  )
538
557
  plan["work_items"]["mapping_confirmed"] = not args.plan_only
539
- 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")
540
562
  print(render_user_steps(plan))
541
- print(f"\nWrote setup plan: {args.plan_output}")
563
+ print(f"\nWrote setup plan: {plan_output}")
542
564
 
543
565
  if args.plan_only:
544
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
545
570
 
546
571
  if args.work_items == "github-issues":
547
572
  discovery, mapping, created = apply_github_status_bootstrap(args, mapping)
@@ -551,6 +576,7 @@ def setup_cmd(args) -> int:
551
576
  class InitArgs:
552
577
  pass
553
578
  init = InitArgs()
579
+ init.root = args.root
554
580
  init.work_items = "azure-devops" if args.work_items == "composite" else args.work_items
555
581
  init.ado_organization = args.ado_organization
556
582
  init.ado_project = args.ado_project
@@ -567,6 +593,20 @@ def setup_cmd(args) -> int:
567
593
  config["workflow"] = workflow
568
594
  config["providers"] = providers
569
595
  config["workers"] = workers
596
+ instruction_adapters = {}
597
+ if not args.no_files:
598
+ for provider_id, provider_config in providers.items():
599
+ provider_type = provider_config.get("type")
600
+ adapter = ensure_provider_instruction_file(args.root, provider_type)
601
+ instruction_adapters[provider_id] = {
602
+ key: value for key, value in adapter.items() if key != "changed"
603
+ }
604
+ if adapter.get("changed"):
605
+ print(f"Updated provider instruction adapter: {adapter.get('provider_file')}")
606
+ else:
607
+ for provider_id, provider_config in providers.items():
608
+ instruction_adapters[provider_id] = adapter_for_provider(provider_config.get("type"))
609
+ config["instruction_adapters"] = instruction_adapters
570
610
  config["source_connection"] = {"github": github_auth or {}}
571
611
  if ado_mcp:
572
612
  config["mcp"] = {"azure_devops": ado_mcp}
@@ -592,7 +632,23 @@ def setup_cmd(args) -> int:
592
632
  )
593
633
  config["framework"]["source"] = framework_source
594
634
  config["framework"]["ref"] = framework_version
595
- 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
+
596
652
  errors = doctor(config, discovery)
597
653
  if errors:
598
654
  print("Doctor findings:")
@@ -706,25 +762,25 @@ def main() -> int:
706
762
 
707
763
  setup = sub.add_parser("setup")
708
764
  setup.add_argument("--root", default=".")
709
- 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"])
710
766
  setup.add_argument("--github-repository")
711
767
  setup.add_argument("--github-token-env", default="GH_TOKEN")
712
- setup.add_argument("--github-auth", choices=["gh-cli","token-env"], default="gh-cli")
768
+ setup.add_argument("--github-auth", choices=["gh-cli","token-env"])
713
769
  setup.add_argument("--ado-organization")
714
770
  setup.add_argument("--ado-project")
715
771
  setup.add_argument("--ado-token-env", default="AZURE_DEVOPS_TOKEN")
716
- setup.add_argument("--ado-mcp-mode", choices=["none","remote","local"], default="remote")
717
- 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"])
718
774
  setup.add_argument("--ado-team")
719
775
  setup.add_argument("--ado-field", action="append", default=[])
720
- setup.add_argument("--environment", choices=["none", "codespaces"], default="none")
721
- 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"])
722
778
  setup.add_argument("--role-provider", action="append", default=[])
723
779
  setup.add_argument("--ai-provider", action="append", default=[])
724
780
  setup.add_argument("--worker", action="append", default=[])
725
- setup.add_argument("--tdd", action=argparse.BooleanOptionalAction, default=True)
726
- setup.add_argument("--reviewer-mode", choices=["manual", "automatic", "none"], default="manual")
727
- 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"])
728
784
  setup.add_argument("--output", default=".ai-workflow.yml")
729
785
  setup.add_argument("--plan-output", default=".ai-setup-plan.yml")
730
786
  setup.add_argument("--framework-ref", default="vuthethienlong/ai-dev-workflow/.github/workflows/queue-dispatcher.yml@main")
@@ -749,11 +805,19 @@ def main() -> int:
749
805
  doc.set_defaults(func=doctor_cmd)
750
806
 
751
807
  args = parser.parse_args()
752
- 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":
753
809
  if not args.ado_organization or not args.ado_project:
754
810
  parser.error("--ado-organization and --ado-project are required for azure-devops")
755
- if args.command == "setup" and args.work_items in ("github-issues", "composite") and not args.github_repository:
756
- 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"
757
821
  return args.func(args)
758
822
 
759
823
 
@@ -0,0 +1,77 @@
1
+ """Provider-specific instruction file adapters.
2
+
3
+ AGENTS.md is the shared provider-neutral source of truth.
4
+ Provider-native files keep user-owned content; ai-dev-workflow only owns the
5
+ bounded managed block below.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+
11
+ START = "<!-- ai-dev-workflow:start -->"
12
+ END = "<!-- ai-dev-workflow:end -->"
13
+
14
+ ADAPTERS = {
15
+ "chatgpt-codex": {
16
+ "shared": "AGENTS.md",
17
+ "provider_file": None,
18
+ "merge_policy": "direct",
19
+ },
20
+ "anthropic": {
21
+ "shared": "AGENTS.md",
22
+ "provider_file": "CLAUDE.md",
23
+ "merge_policy": "preserve-existing",
24
+ },
25
+ }
26
+
27
+
28
+ def adapter_for_provider(provider_type: str) -> dict:
29
+ return dict(ADAPTERS.get(provider_type, {
30
+ "shared": "AGENTS.md",
31
+ "provider_file": None,
32
+ "merge_policy": "direct",
33
+ }))
34
+
35
+
36
+ def managed_block() -> str:
37
+ return (
38
+ f"{START}\n"
39
+ "## Shared workflow instructions\n\n"
40
+ "Read and follow `AGENTS.md` as the shared project instruction source.\n\n"
41
+ "When you are executing an active ai-dev-workflow task and `CURRENT.md` "
42
+ "exists, read it as the short-lived task/context router. `CURRENT.md` is "
43
+ "not a permanent provider rule and is not required for unrelated sessions.\n"
44
+ f"{END}"
45
+ )
46
+
47
+
48
+ def merge_managed_block(existing: str) -> str:
49
+ block = managed_block()
50
+ if START in existing or END in existing:
51
+ if START not in existing or END not in existing:
52
+ raise ValueError("instruction adapter managed block is malformed")
53
+ before, rest = existing.split(START, 1)
54
+ _, after = rest.split(END, 1)
55
+ return before + block + after
56
+
57
+ if not existing:
58
+ return block + "\n"
59
+ separator = "" if existing.endswith("\n\n") else ("\n" if existing.endswith("\n") else "\n\n")
60
+ return existing + separator + block + "\n"
61
+
62
+
63
+ def ensure_provider_instruction_file(root: str | Path, provider_type: str) -> dict:
64
+ adapter = adapter_for_provider(provider_type)
65
+ provider_file = adapter.get("provider_file")
66
+ if not provider_file:
67
+ return {**adapter, "changed": False}
68
+
69
+ path = Path(root) / provider_file
70
+ existing = path.read_text(encoding="utf-8") if path.exists() else ""
71
+ merged = merge_managed_block(existing)
72
+ changed = merged != existing
73
+ if changed:
74
+ path.parent.mkdir(parents=True, exist_ok=True)
75
+ path.write_text(merged, encoding="utf-8")
76
+
77
+ return {**adapter, "changed": changed}
@@ -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)