-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy path__main__.py
More file actions
4785 lines (4358 loc) · 203 KB
/
Copy path__main__.py
File metadata and controls
4785 lines (4358 loc) · 203 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""__main__.py — CLI エントリポイント
使い方:
# (A) GUI モード(引数なし時の既定。PySide6 未導入時は自動で CLI へフォールバック)
python -m hve
python -m hve gui # 明示指定(後方互換)
# (B) CLI 対話ウィザードモード
python -m hve cli
# (C) python -m で直接実行(サブコマンド指定)
python -m hve orchestrate --workflow aad
# (C) ディレクトリに移動して __main__.py を直接実行
cd hve
python __main__.py orchestrate --workflow aad
# (D) フルパス指定
python hve/__main__.py orchestrate --workflow aad
# 基本実行 (デフォルト: Auto, 並列15, compact, Issue/PR作成なし)
python -m hve orchestrate --workflow aad
# QA + Review 有効
python -m hve orchestrate --workflow aad --auto-qa --auto-contents-review
# Issue 作成あり + MCP Server 設定ファイル指定
python -m hve orchestrate --workflow asdw \\
--create-issues --mcp-config mcp-servers.json
# 並列数変更 + モデル変更
python -m hve orchestrate --workflow aad \\
--max-parallel 5 --model gpt-5.4
# 出力抑制
python -m hve orchestrate --workflow aad --quiet
# 外部 CLI サーバー接続
python -m hve orchestrate --workflow aad --cli-url localhost:4321
# ドライラン
python -m hve orchestrate --workflow aad --dry-run
# 追加プロンプト付き
python -m hve orchestrate --workflow aad \\
--additional-prompt "Azure Japan East リージョンを前提にしてください"
# Issue タイトル指定
python -m hve orchestrate --workflow aad \\
--create-issues --issue-title "Sprint 42: AAD 全ステップ実行"
# Knowledge Management(デフォルト設定: sources=qa, target_files=qa/*.md, force_refresh=false)
python -m hve orchestrate --workflow akm
# original-docs 起点
python -m hve orchestrate --workflow akm --sources original-docs
# 両方 + custom source dir
python -m hve orchestrate --workflow akm --sources both --custom-source-dir docs/specs
# ADI(docs-original/ の設計書を目録化・質問票生成・選別)
python -m hve orchestrate --workflow adi --purpose "EC 倉庫の取り置き算出バッチを再構築する"
python -m hve orchestrate --workflow adi --target-scope docs-original/ --depth lightweight
# ARD(要求定義の自動化)
python -m hve orchestrate --workflow ard --company-name "株式会社サンプル"
python -m hve orchestrate --workflow ard --company-name "株式会社サンプル" \\
--target-business "ロイヤルティプログラム事業"
"""
from __future__ import annotations
import argparse
import asyncio
import json
import os
import shutil
import subprocess
import sys
from datetime import datetime
from pathlib import Path
from typing import Any, List, Optional
def _configure_stdio_encoding() -> None:
"""stdout/stderr を UTF-8 に再設定する(パイプ経由起動時の cp932 対策)。
`hve gui` などから ``subprocess.Popen(..., stdout=PIPE)`` で起動された場合、
Python の標準出力ストリームはコンソール直結時の UTF-8 ではなく OS ロケール
(Windows なら cp932)にフォールバックする。この状態で console.py が出力する
``▸`` (U+25B8) 等の Unicode 記号を ``print()`` すると ``UnicodeEncodeError``
で落ちる。本関数で起動時に UTF-8 を強制し、全モードで一貫した出力にする。
- ``errors="replace"`` により、万一エンコード不能文字が混入しても例外で
落ちず置換文字に置き換える。
- コンソール直結時(既に UTF-8 ベース)でも冪等に動作する。
- ``reconfigure`` が無い環境(Python 3.6 以前等)では no-op。
"""
for stream_name in ("stdout", "stderr"):
stream = getattr(sys, stream_name, None)
reconfigure = getattr(stream, "reconfigure", None)
if reconfigure is None:
continue
try:
reconfigure(encoding="utf-8", errors="replace")
except Exception: # pragma: no cover - top-level guard
pass
_configure_stdio_encoding()
def _reexec_in_venv_if_needed() -> None:
"""``python -m hve`` がリポジトリの ``.venv`` 外の Python で起動された場合に、
同梱 ``.venv`` の Python へ自動的に再 exec する。
セットアップ (``hve/setup-hve.*``) は全依存を ``<repo>/.venv`` に導入する。
しかし activate 漏れや、システム Python から ``python -m hve gui`` を直接実行
した場合は ``ModuleNotFoundError: No module named 'PySide6'`` 等で起動に失敗する。
本関数はこれを吸収し、セットアップ直後でも ``python -m hve gui`` / ``cli`` が
そのまま動作するようにする。
挙動:
- 既に ``.venv`` の Python で動作している場合は何もしない(冪等)。
- ``.venv`` が存在しない場合は何もしない(現在の Python で続行)。
- ``HVE_NO_VENV_REEXEC=1`` でオプトアウト(再帰防止にも使用)。
- 検出・再 exec に失敗した場合は現在の Python で続行(フォールバック)。
呼び出し箇所は 2 か所に限定する:
- ``__name__ == "__main__"`` の module level(重い import より前)
- ``_console_main()``(``hve`` console script 経路)
``import hve.__main__`` 等のライブラリ利用時には発火させない。
"""
if os.environ.get("HVE_NO_VENV_REEXEC", "").strip().lower() in {"1", "true", "yes"}:
return
try:
repo_root = Path(__file__).resolve().parent.parent
venv_py = (
repo_root / ".venv" / "Scripts" / "python.exe"
if os.name == "nt"
else repo_root / ".venv" / "bin" / "python"
)
if not venv_py.exists():
return
# 既に .venv の Python で動作しているなら再 exec は不要。
try:
already_in_venv = os.path.samefile(sys.executable, str(venv_py))
except OSError:
already_in_venv = Path(sys.executable).resolve() == venv_py.resolve()
if already_in_venv:
return
new_argv = [str(venv_py), "-m", "hve", *sys.argv[1:]]
new_env = dict(os.environ)
new_env["HVE_NO_VENV_REEXEC"] = "1" # 再帰防止フラグ
except Exception: # pragma: no cover - 検出失敗時は従来挙動へフォールバック
return
print(
# NOTE: この通知は環境正規化前(.venv 外の Python・cp932 等のコンソール)に
# 出力される可能性があるため、文字化けを避けて ASCII のみで記述する。
f"[hve] Detected non-.venv Python; re-executing with .venv: {venv_py}",
file=sys.stderr,
)
if os.name == "nt":
# Windows では os.execv の挙動が不安定なため subprocess + 終了コード継承を用いる。
import subprocess
try:
completed = subprocess.run(new_argv, env=new_env)
except Exception: # pragma: no cover - 起動失敗時は現在の Python で続行
return
sys.exit(completed.returncode)
else:
try:
os.execve(str(venv_py), new_argv, new_env)
except Exception: # pragma: no cover - exec 失敗時は現在の Python で続行
return
if __name__ == "__main__":
# 重い依存 (`.config` -> `cq`) を読み込む前に .venv へ再 exec する。
# `python -m hve` では以下の module level import がファイル末尾の
# `if __name__ == "__main__":` ブロックより先に評価されるため、
# ガードをここに置かないと依存欠落で先に落ちる。
_reexec_in_venv_if_needed()
try:
from .config import DEFAULT_MODEL, MODEL_AUTO_VALUE, MODEL_CHOICES, SDKConfig
from .workflow_registry import canonicalize_workflow_id, get_workflow
except ImportError:
# 平坦 import への退避は `cd hve && python __main__.py` のような
# パッケージ文脈なし実行のときだけ。パッケージとして import されて
# いる場合の失敗は依存欠落 (例: cq 未導入) なので真因を握り潰さず再送出する。
if __package__:
raise
from config import DEFAULT_MODEL, MODEL_AUTO_VALUE, MODEL_CHOICES, SDKConfig # type: ignore[no-redef]
_workflow_registry_module = __import__("workflow_" + "registry")
canonicalize_workflow_id = getattr(
_workflow_registry_module,
"canonicalize_workflow_id",
)
get_workflow = getattr(_workflow_registry_module, "get_workflow")
def _ts() -> str:
"""現在時刻のプレフィックス文字列を返す。"""
return f"[{datetime.now().strftime('%H:%M:%S')}]"
# -----------------------------------------------------------------------
# Auto モデル定数
# -----------------------------------------------------------------------
MODEL_AUTO = MODEL_AUTO_VALUE
# AKM デフォルト値
# Work IQ を入力ソースとして任意追加できるよう、既定は qa + original-docs のマルチ値(カンマ区切り)。
_AKM_DEFAULT_SOURCES = "qa,original-docs"
_AKM_DEFAULT_TARGET_FILES = "qa/*.md"
_AKM_SOURCES_OPTIONS = [
"qa のみ",
"original-docs のみ",
"両方",
]
_AKM_SOURCES_MAP = {
"qa のみ": "qa",
"original-docs のみ": "original-docs",
"両方": "both",
}
# Work IQ を含むマルチ選択用のソース一覧(C-1 で使用)
_AKM_SOURCES_MULTI_OPTIONS = [
"qa(質問票)",
"original-docs(原資料)",
"workiq(Microsoft 365 Copilot Work IQ)",
]
_AKM_SOURCES_MULTI_VALUES = ["qa", "original-docs", "workiq"]
_ADI_DEFAULT_TARGET_SCOPE = "docs-original/"
_ADI_DEFAULT_DEPTH = "standard"
_ADI_DEPTH_CHOICES = ("standard", "lightweight")
_ADI_DEPTH_MENU_OPTIONS = (
"standard — 全カテゴリ",
"lightweight — 不明瞭/矛盾のみ",
)
_ADOC_DOC_PURPOSE_CHOICES = ("all", "onboarding", "refactoring", "migration")
_ADOC_DOC_PURPOSE_MENU_OPTIONS = (
"all — 全用途",
"onboarding — 新規参画者向け",
"refactoring — 改善・保守向け",
"migration — 移行計画向け",
)
_ADOC_DEFAULT_DOC_PURPOSE = "all"
_ADOC_MAX_FILE_LINES_CHOICES = (300, 500, 1000)
_ADOC_MAX_FILE_LINES_MENU_OPTIONS = (
"300 行 — 小さめに分割",
"500 行 — 既定",
"1000 行 — 大きめに分割",
)
_ADOC_DEFAULT_MAX_FILE_LINES = 500
_ADOC_DEFAULT_EXCLUDE_PATTERNS = "node_modules/,vendor/,dist/,*.lock,__pycache__/"
# ARD デフォルト値
_ARD_DEFAULT_SURVEY_PERIOD_YEARS = 30
_ARD_DEFAULT_TARGET_REGION = "グローバル全体"
_ARD_DEFAULT_ANALYSIS_PURPOSE = "中長期成長戦略の立案"
_APP_ID_AUTO_HINTS = {
"aad-web": "Webフロントエンド + クラウドの APP-ID を自動選択",
"asdw-web": "Webフロントエンド + クラウドの APP-ID を自動選択",
"adfd": "データデータフロー処理 / バッチの APP-ID を自動選択",
"adfdv": "データデータフロー処理 / バッチの APP-ID を自動選択",
}
_PARAM_PROMPT_LABELS = {
"app_ids": "対象 APP-ID",
"app_id": "対象 APP-ID(単一)",
"resource_group": "Azure リソースグループ名(任意)",
"usecase_id": "対象ユースケースID(任意)",
"app_id": "対象データフローアプリID(カンマ区切り・任意)",
"target_scope": "対象スコープ",
"focus_areas": "重点観点(任意)",
"purpose": "選別の目的(任意)",
"target_dirs": "ドキュメント生成対象ディレクトリ(カンマ区切り。省略 = 全体)",
"exclude_patterns": "除外パターン(カンマ区切り)",
"issue_title": "GitHub Issue タイトル(任意)",
"sources": "取り込みソース",
"target_files": "対象ファイルパス",
"force_refresh": "knowledge/ 完全再生成",
"custom_source_dir": "追加ソースディレクトリ",
"enable_auto_merge": "PR 自動 Approve & Auto-merge",
"doc_purpose": "ドキュメント主目的",
"max_file_lines": "大規模ファイル分割閾値",
"create_remote_mcp_server": "Remote MCP Server を作成する",
# ARD 固有
"company_name": "対象企業名(Step 1 選択時は必須)",
"target_business": "対象業務名", # サフィックスは _build_target_business_label で動的付与
"survey_base_date": "調査基準日(YYYY-MM-DD、任意)",
"survey_period_years": "調査期間年数(任意)",
"target_region": "対象地域(任意)",
"analysis_purpose": "分析目的(任意)",
"attached_docs": "添付資料のファイルパス(カンマ区切り・任意)",
}
_PARAM_DEFAULTS = {
"resource_group": "",
"usecase_id": "",
"app_id": "",
"target_scope": _ADI_DEFAULT_TARGET_SCOPE,
"depth": _ADI_DEFAULT_DEPTH,
"focus_areas": "",
"purpose": "",
"target_dirs": "",
"exclude_patterns": _ADOC_DEFAULT_EXCLUDE_PATTERNS,
"create_remote_mcp_server": True,
# ARD 固有
"company_name": "",
"target_business": "",
"survey_base_date": "", # 空 → orchestrator 側で today() を採用
"survey_period_years": _ARD_DEFAULT_SURVEY_PERIOD_YEARS,
"target_region": _ARD_DEFAULT_TARGET_REGION,
"analysis_purpose": _ARD_DEFAULT_ANALYSIS_PURPOSE,
"attached_docs": "",
}
def _split_csv(value: str) -> List[str]:
"""カンマ区切り文字列を空要素なしのリストに変換する。"""
return [part.strip() for part in value.split(",") if part.strip()]
def _prompt_app_ids(con, wf_id: str) -> dict:
"""APP-ID を 1 回だけ尋ね、単一指定時は app_id も派生させる。"""
auto_hint = _APP_ID_AUTO_HINTS.get(wf_id)
if auto_hint:
label = f"対象アプリケーション (APP-ID、カンマ区切り・任意。未指定時は {auto_hint})"
else:
label = "対象アプリケーション (APP-ID、カンマ区切り・任意)"
raw = con.prompt_input(label, default="", required=False)
app_ids = _split_csv(raw or "")
if not app_ids:
return {}
params = {"app_ids": app_ids}
if len(app_ids) == 1:
params["app_id"] = app_ids[0]
return params
def _prompt_param_input(con, param_name: str) -> str:
"""ワークフロー固有パラメータを内部名ではなく表示ラベルで入力させる。"""
label = _PARAM_PROMPT_LABELS.get(param_name, param_name)
default = _PARAM_DEFAULTS.get(param_name, "")
return con.prompt_input(label, default=default, required=False)
def _build_target_business_label(con, selected_steps) -> str:
"""ARD ウィザードの target_business ラベルを Step 1 選択有無で切り替える。
- Step 1 選択時: 補足説明を灰色(DIM)で付記し任意入力。
- Step 1 非選択時: 必須マークは prompt_input 側の `required=True` で赤色付与。
"""
s = getattr(con, "s", None)
if "1" in selected_steps:
if s is not None:
return f"対象業務名 {s.DIM}既定値: Step 1で作成されたドキュメントの「戦略的提言」から、LLMで自動生成){s.RESET}"
return "対象業務名既定値: Step 1で作成されたドキュメントの「戦略的提言」から、LLMで自動生成)"
return "対象業務名"
def _default_param_value(param_name: str):
"""クイック全自動で使うワークフロー固有パラメータ既定値。"""
if param_name == "doc_purpose":
return _ADOC_DEFAULT_DOC_PURPOSE
if param_name == "max_file_lines":
return _ADOC_DEFAULT_MAX_FILE_LINES
return _PARAM_DEFAULTS.get(param_name, "")
def _format_param_value(value) -> str:
"""確認パネル用にパラメータ値を読みやすく整形する。"""
if isinstance(value, bool):
return "ON" if value else "OFF"
if isinstance(value, list):
return ", ".join(str(v) for v in value) if value else "(なし)"
return str(value) if value else "(なし)"
def _format_param_label(param_name: str) -> str:
"""確認パネル用の表示名を返す。"""
return _PARAM_PROMPT_LABELS.get(param_name, param_name)
def _step_options_with_groups(wf) -> tuple:
"""コンテナステップの見出しを使ってステップ選択肢を整形する。"""
container_titles = {s.id: s.title for s in wf.steps if s.is_container}
non_container_steps = [s for s in wf.steps if not s.is_container]
options = []
for step in non_container_steps:
parent_id = step.id.split(".", 1)[0] if "." in step.id else ""
parent_title = container_titles.get(parent_id)
if parent_title:
options.append(f"{parent_title} > [{step.id}] {step.title}")
else:
options.append(f"[{step.id}] {step.title}")
return non_container_steps, options
def _collect_ard_wizard_params(con, *, is_quick_auto: bool) -> tuple[dict, list[str]]:
"""ARD ワークフロー固有のパラメータ収集と selected_steps 計算。
Returns:
(params, selected_steps) のタプル。
- selected_steps: ウィザードでユーザーが選択したグループ ID 一覧("1" / "2" / "3" / "4")。
Enter 時の初期値は ["2", "3", "4"]。
グループ ID は registry 侧の `_WORKFLOW_GROUP_MAPS["ard"]` で実 Step ID に展開される:
"1" → ["1", "1.1", "1.2"](企業の事業分析)
"2" → ["2"](要求定義書作成)
"3" → ["2.1"](KPI/OKR 定義・任意)
"4" → ["3.1", "3.2", "3.3"](ユースケース作成)
"""
from datetime import date
params: dict = {}
# 4 グループ体系。各グループは内部で複数 Step を順次実行する。
# Step 3(KPI/OKR)も既定で選択に含める(Step 2 / 4 と同時実行することで戦略的記述から
# KPI/OKR・計測データ・データ収集設計まで一気通貫で生成する運用に合わせる)。
_ard_step_ids = ["1", "2", "3", "4"]
_ard_step_options = [
"[1] 企業の事業分析(事業分野候補列挙 → 分野別深掘り → 統合)",
"[2] 要求定義書作成(Step 1 の出力があれば参考にし、無くてもよい)",
"[3] KPI/OKR 定義(任意・戦略的記述から KPI/OKR・計測データ・データ収集設計を生成)",
"[4] ユースケース作成(骨格抽出 → 詳細生成 → カタログ統合)",
]
selected_indices = con.prompt_multi_select(
"ARD で実行するステップを選択",
_ard_step_options,
default_indices=[1, 2, 3],
)
selected_steps = [_ard_step_ids[i] for i in selected_indices if 0 <= i < len(_ard_step_ids)]
if not selected_steps:
selected_steps = ["2", "3", "4"]
requires_company_name = "1" in selected_steps
requires_target_business = ("2" in selected_steps) and ("1" not in selected_steps)
target_business_label = _build_target_business_label(con, selected_steps)
if is_quick_auto:
# Step 1 を選択した場合のみ company_name を必須とする。
params["company_name"] = con.prompt_input(
_PARAM_PROMPT_LABELS["company_name"],
default="",
required=requires_company_name,
)
if "2" in selected_steps:
params["target_business"] = con.prompt_input(
target_business_label,
default="",
required=requires_target_business,
)
else:
params["target_business"] = ""
params["survey_base_date"] = date.today().isoformat()
params["survey_period_years"] = _ARD_DEFAULT_SURVEY_PERIOD_YEARS
params["target_region"] = _ARD_DEFAULT_TARGET_REGION
params["analysis_purpose"] = _ARD_DEFAULT_ANALYSIS_PURPOSE
params["attached_docs"] = []
# quick-auto モードでも Step 3 (KPI/OKR 定義) を既定で有効化する(GUI/CLI 対話ウィザードと整合)。
# orchestrator 側で selected_steps に "3" が含まれれば自動同期されるが、明示的に True を記録する。
params["include_kpi_okr"] = True
else:
params["company_name"] = con.prompt_input(
_PARAM_PROMPT_LABELS["company_name"],
default="",
required=requires_company_name,
)
if "2" in selected_steps:
params["target_business"] = con.prompt_input(
target_business_label,
default="",
required=requires_target_business,
)
else:
params["target_business"] = ""
survey_base = con.prompt_input(
_PARAM_PROMPT_LABELS["survey_base_date"],
default=date.today().isoformat(), required=False,
)
params["survey_base_date"] = survey_base or date.today().isoformat()
survey_years = con.prompt_input(
_PARAM_PROMPT_LABELS["survey_period_years"],
default=str(_ARD_DEFAULT_SURVEY_PERIOD_YEARS), required=False,
)
try:
params["survey_period_years"] = int(survey_years)
except (TypeError, ValueError):
params["survey_period_years"] = _ARD_DEFAULT_SURVEY_PERIOD_YEARS
params["target_region"] = con.prompt_input(
_PARAM_PROMPT_LABELS["target_region"],
default=_ARD_DEFAULT_TARGET_REGION, required=False,
)
params["analysis_purpose"] = con.prompt_input(
_PARAM_PROMPT_LABELS["analysis_purpose"],
default=_ARD_DEFAULT_ANALYSIS_PURPOSE, required=False,
)
attached_raw = con.prompt_input(
_PARAM_PROMPT_LABELS["attached_docs"], default="", required=False,
)
params["attached_docs"] = _split_csv(attached_raw or "")
# Step 3 (KPI/OKR 定義・任意): Step 2 または Step 4 が選択時のみプロンプト。
# 他の場合は自動的に False(DAG にも組み込まれない)。
if ("2" in selected_steps or "4" in selected_steps):
params["include_kpi_okr"] = con.prompt_yes_no(
"KPI/OKR 定義 (Step 3・任意) を実行しますか?"
"(戦略的記述から KPI/OKR・計測データ・データ収集設計を生成)",
default=False,
)
else:
params["include_kpi_okr"] = False
# グループ ID ("1"/"2"/"3"/"4") は registry 侧の _WORKFLOW_GROUP_MAPS で実 Step ID に展開される。
# (旧仕様の "1.1 選択時に Step '1' を自動前提" は撤廃。グループ "1" 自体が 1,1.1,1.2 を包含する。)
return params, selected_steps
def _asdw_data_deploy_is_selected(selected_step_ids: list[str]) -> bool:
"""Return whether an ASDW selection can reach Step 1.3."""
return not selected_step_ids or any(
step_id in {"1", "1.3"} or step_id.startswith("1.3/")
for step_id in selected_step_ids
)
# ASDW-WEB Step 1.3 の必須パラメータの表示ラベル(FR-CLI-14)。
# キー集合の正本は workflow_registry の StepDef.required_params(FR-DAG-07)。
_ASDW_DATA_DEPLOY_PARAM_LABELS = {
"resource_group": "Azure リソースグループ名",
"data_location": "DataDeploy location",
"data_resource_suffix": "DataDeploy resource suffix",
"data_vnet_cidr": "DataDeploy VNet CIDR",
"data_private_endpoint_subnet_cidr": "DataDeploy private endpoint subnet CIDR",
"data_aci_subnet_cidr": "DataDeploy ACI subnet CIDR",
}
def _asdw_data_deploy_param_keys() -> frozenset:
"""ASDW-WEB Step 1.3 が宣言する必須パラメータ名を返す(FR-DAG-07)。"""
wf = get_workflow("asdw-web")
step = wf.get_step("1.3") if wf is not None else None
return frozenset(step.required_params) if step is not None else frozenset()
def _prompt_asdw_data_deploy_params(con, collected: dict) -> dict:
"""ASDW-WEB Step 1.3 の必須パラメータを宣言(FR-DAG-07)から収集する。
既定値があるキーは既定値を提示し、Enter のみで採用できる。
先行の汎用ループで非空値を収集済みのキーは再質問しない。
"""
wf = get_workflow("asdw-web")
step = wf.get_step("1.3") if wf is not None else None
if step is None:
return {}
values: dict = {}
for key in step.required_params:
existing = collected.get(key)
if isinstance(existing, str) and existing.strip():
continue
values[key] = con.prompt_input(
_ASDW_DATA_DEPLOY_PARAM_LABELS.get(key, key),
default=step.default_params.get(key, ""),
required=True,
)
return values
def _collect_generic_workflow_params(
con,
wf,
*,
is_quick_auto: bool,
selected_step_ids: Optional[list[str]] = None,
) -> dict:
"""AKM/ARD 以外のワークフロー固有パラメータを収集する。"""
params: dict = {}
# FR-CLI-14: Step 1.3 の required_params は DataDeploy ブロックが宣言由来の
# ラベル・既定値で尋ねるため、汎用ループ側では尋ねない(二重質問防止)。
asdw_data_param_keys: frozenset = frozenset()
collect_asdw_data_params = (
wf.id == "asdw-web"
and not is_quick_auto
and _asdw_data_deploy_is_selected(selected_step_ids or [])
)
if collect_asdw_data_params:
asdw_data_param_keys = _asdw_data_deploy_param_keys()
if "app_ids" in wf.params or "app_id" in wf.params:
if not is_quick_auto:
params.update(_prompt_app_ids(con, wf.id))
for param_name in wf.params:
if param_name in ("app_ids", "app_id"):
continue
if param_name in asdw_data_param_keys:
continue
if is_quick_auto:
params[param_name] = _default_param_value(param_name)
elif param_name == "doc_purpose":
params[param_name] = _prompt_valid_doc_purpose(con)
elif param_name == "max_file_lines":
params[param_name] = _prompt_valid_max_file_lines(con)
elif wf.id == "adi" and param_name == "depth":
params[param_name] = _prompt_valid_adi_depth(con)
elif param_name == "create_remote_mcp_server":
params[param_name] = con.prompt_yes_no(
_PARAM_PROMPT_LABELS["create_remote_mcp_server"],
default=_PARAM_DEFAULTS["create_remote_mcp_server"],
)
else:
params[param_name] = _prompt_param_input(con, param_name)
if collect_asdw_data_params:
params.update(_prompt_asdw_data_deploy_params(con, params))
return params
def _default_akm_target_files(sources) -> str:
"""AKM の sources に応じた target_files 既定値を返す。
``sources`` は文字列(カンマ/空白区切り)または ``list[str]`` を受け付ける。
Work IQ のみ、または非 Work IQ ソースが複数の場合は既定パターンなし(``""``)。
後方互換: 旧 ``"original-docs"`` / ``"both"`` 等の単一文字列も受理する。
"""
# orchestrator 側の正規化/既定算出を再利用し、本モジュール内の重複ロジックを避ける。
try:
from .orchestrator import _default_akm_target_files as _impl # type: ignore
except ImportError:
from orchestrator import _default_akm_target_files as _impl # type: ignore[no-redef]
return _impl(sources)
def _normalize_akm_sources(value) -> list:
"""``orchestrator._normalize_akm_sources`` への薄いラッパー。"""
try:
from .orchestrator import _normalize_akm_sources as _impl # type: ignore
except ImportError:
from orchestrator import _normalize_akm_sources as _impl # type: ignore[no-redef]
return _impl(value)
def _prompt_valid_doc_purpose(con) -> str:
"""ADOC の doc_purpose をメニュー選択させる。"""
default_idx = _ADOC_DOC_PURPOSE_CHOICES.index(_ADOC_DEFAULT_DOC_PURPOSE)
selected_idx = con.menu_select(
"ドキュメントの主目的を選択してください",
list(_ADOC_DOC_PURPOSE_MENU_OPTIONS),
allow_empty=True,
default_index=default_idx,
)
return _ADOC_DOC_PURPOSE_CHOICES[default_idx if selected_idx == -1 else selected_idx]
def _prompt_valid_adi_depth(con) -> str:
"""ADI の depth をメニュー選択させる。"""
default_idx = _ADI_DEPTH_CHOICES.index(_ADI_DEFAULT_DEPTH)
selected_idx = con.menu_select(
"分析の深さを選択してください",
list(_ADI_DEPTH_MENU_OPTIONS),
allow_empty=True,
default_index=default_idx,
)
return _ADI_DEPTH_CHOICES[default_idx if selected_idx == -1 else selected_idx]
def _prompt_valid_max_file_lines(con) -> int:
"""ADOC の max_file_lines をメニュー選択させる。"""
default_idx = _ADOC_MAX_FILE_LINES_CHOICES.index(_ADOC_DEFAULT_MAX_FILE_LINES)
selected_idx = con.menu_select(
"大規模ファイル分割閾値を選択してください",
list(_ADOC_MAX_FILE_LINES_MENU_OPTIONS),
allow_empty=True,
default_index=default_idx,
)
return _ADOC_MAX_FILE_LINES_CHOICES[default_idx if selected_idx == -1 else selected_idx]
def _collect_agentic_retrieval_wizard_answers(con, wf_id: str, *, is_quick_auto: bool) -> dict:
"""Agentic Retrieval 関連の質問(Q1〜Q6)をウィザードで収集する。
AAD-WEB は Q1・Q3 のみ(設計フェーズ)。ASDW-WEB は Q1〜Q6 全て。
`is_quick_auto=True` のときは既定値をそのまま返す。
Returns:
``normalize_agentic_retrieval_answers`` への入力に対応するキー辞書。
"""
try:
from .template_engine import _AGENTIC_RETRIEVAL_QUESTIONS, _AGENTIC_RETRIEVAL_KEYS_FOR
except ImportError:
from template_engine import _AGENTIC_RETRIEVAL_QUESTIONS, _AGENTIC_RETRIEVAL_KEYS_FOR # type: ignore[no-redef]
_wf_id = canonicalize_workflow_id(wf_id)
keys = _AGENTIC_RETRIEVAL_KEYS_FOR.get(_wf_id, [])
if not keys:
return {}
answers: dict = {}
if is_quick_auto:
for key in keys:
q = _AGENTIC_RETRIEVAL_QUESTIONS[key]
kind = q["kind"]
default = q["default"]
if kind == "dropdown":
opts = q["options"]
answers[key] = opts[default]
elif kind == "checkboxes":
answers[key] = list(default) if isinstance(default, list) else [default] if default else []
elif kind == "checkbox":
answers[key] = default
else:
answers[key] = default
return answers
con._print(
"\n ─── Agentic Retrieval 設定 ───────────────────────────",
ts=False,
)
for key in keys:
q = _AGENTIC_RETRIEVAL_QUESTIONS[key]
label = q["label"]
desc = q["description"]
kind = q["kind"]
default = q["default"]
prompt_text = f"{label}\n {desc}"
if kind == "dropdown":
opts = q["options"]
sel_idx = con.menu_select(prompt_text, opts, allow_empty=True, default_index=default)
if sel_idx == -1:
sel_idx = default
answers[key] = opts[sel_idx]
elif kind == "checkboxes":
opts = q["options"]
defaults_list = default if isinstance(default, list) else []
sel_indices = con.prompt_multi_select(prompt_text, opts)
if not sel_indices:
# 未選択時は既定値を使用
defaults_set = set(defaults_list)
sel_indices = [i for i, o in enumerate(opts) if o in defaults_set] or [0]
answers[key] = [opts[i] for i in sel_indices]
elif kind == "checkbox":
answers[key] = con.prompt_yes_no(prompt_text, default=default)
else:
answers[key] = con.prompt_input(prompt_text, default=str(default) if default else "")
return answers
def _prompt_akm_params(
con,
is_quick_auto: bool,
*,
will_create_pr: bool = False,
) -> dict:
"""AKM ワークフローのパラメータを収集する。
Args:
con: Console インスタンス。
is_quick_auto: クイック全自動モードの場合 True。
will_create_pr: GitHub Issue または PR を作成する場合 True。
False のときは `enable_auto_merge` プロンプトを表示せず False を採用する。
"""
params: dict = {}
if is_quick_auto:
params["sources"] = _AKM_DEFAULT_SOURCES
params["target_files"] = _default_akm_target_files(params["sources"])
params["force_refresh"] = False
params["custom_source_dir"] = ""
params["enable_auto_merge"] = False
# Work IQ 入力フェーズはクイック全自動モードでは既定 OFF(明示要求がない限り)。
params["workiq_akm_ingest_dxx"] = []
return params
# 取り込みソースをマルチ選択(qa / original-docs / workiq)。
# 既定は qa + original-docs。空選択は既定にフォールバックする。
_default_indices = [0, 1] # qa, original-docs
selected_indices = con.prompt_multi_select(
"取り込みソースを選択してください(複数選択可)",
_AKM_SOURCES_MULTI_OPTIONS,
default_indices=_default_indices,
)
if not selected_indices:
selected_indices = list(_default_indices)
selected_values = [_AKM_SOURCES_MULTI_VALUES[i] for i in selected_indices]
# _normalize_akm_sources を経由して順序固定化(workiq, qa, original-docs)。
normalized = _normalize_akm_sources(selected_values)
params["sources"] = ",".join(normalized)
default_target = _default_akm_target_files(normalized)
target_input = con.prompt_input(
"対象ファイルパス(スペース区切り、省略時: デフォルト)",
default=default_target,
)
target_input_strip = (target_input or "").strip()
params["target_files"] = target_input_strip if target_input_strip else default_target
params["force_refresh"] = con.prompt_yes_no(
"既存 knowledge/ 出力を完全に再生成する?",
default=False,
)
params["custom_source_dir"] = con.prompt_input(
"追加ソースディレクトリ(スペース区切り・任意)",
default="",
)
params["enable_auto_merge"] = False
# Work IQ が選択されている場合のみ、取り込み対象 Dxx の絞り込みを尋ねる(Sub-C-4)。
if "workiq" in normalized:
dxx_input = con.prompt_input(
"Work IQ 取り込み対象 Dxx(カンマ区切り、例: D01,D04。省略=全件 D01〜D21)",
default="",
)
# config 側のヘルパで正規化(無効パターンは除外、空 → [])。
try:
from .config import _parse_workiq_akm_ingest_dxx # type: ignore
except ImportError:
from config import _parse_workiq_akm_ingest_dxx # type: ignore[no-redef]
params["workiq_akm_ingest_dxx"] = _parse_workiq_akm_ingest_dxx(dxx_input or "")
else:
params["workiq_akm_ingest_dxx"] = []
return params
def _resolve_model(model: str) -> tuple:
"""モデル名を解決する。
Args:
model: 入力モデル名。空文字または MODEL_AUTO の場合は Auto を返す。
Returns:
(resolved_model, display_name) のタプル。
"""
if model in ("", MODEL_AUTO):
return MODEL_AUTO_VALUE, MODEL_AUTO
return model, model
# -----------------------------------------------------------------------
# argparse セットアップ
# -----------------------------------------------------------------------
def _build_parser() -> argparse.ArgumentParser:
"""メイン ArgumentParser を構築する。"""
parser = argparse.ArgumentParser(
prog="hve",
description="GitHub Copilot SDK ワークフローオーケストレーター",
)
sub = parser.add_subparsers(dest="command")
# --- run サブコマンド (インタラクティブモード) ---
run_parser = sub.add_parser(
"run",
help="インタラクティブモードでワークフローを実行する (デフォルト)",
)
run_parser.add_argument(
"--banner",
action=argparse.BooleanOptionalAction,
default=None,
dest="banner",
help="起動時バナー表示を制御する (--banner: 表示, --no-banner: 抑止, 省略時: 表示)",
)
# --- orchestrate サブコマンド ---
orch = sub.add_parser(
"orchestrate",
help="ワークフローを選択し、DAG に従って各ステップをローカル実行する",
)
# 必須(--autopilot-chain 指定時は省略可)
orch.add_argument(
"--workflow", "-w",
required=False,
default=None,
metavar="WORKFLOW_ID",
help=(
"ワークフロー ID: "
"aas(App Architecture Design) / "
"aad(App Detail Design) / "
"asdw(App Dev Microservice Azure) / "
"adfd(Dataflow Design) / "
"adfdv(Dataflow Dev) / "
"akm(Knowledge Management) / "
"adi(Auto Design-doc Ingestion) / "
"adoc(Source Codeからのドキュメント作成)"
" — `--autopilot-chain` 指定時は省略可(排他)"
),
)
# Autopilot チェーン実行(GUI Autopilot と同等の挙動を CLI で再現)
orch.add_argument(
"--autopilot-chain",
default=None,
metavar="WORKFLOW_IDS",
help=(
"Autopilot チェーン実行で許可する Workflow ID をカンマ区切りで指定する "
"(例: --autopilot-chain aad-web,asdw-web)。"
"`docs/catalog/app-arch-catalog.md` から APP 一覧を取得し、APP 単位の"
"並列レーン × チェーン内直列で `python -m hve orchestrate` を起動する。"
"※ 実際の実行順序は APP アーキ種別ごとの固定チェーン(web-cloud=aad-web→asdw-web、"
"batch=adfd→adfdv)で決まり、本引数はその中から実行する workflow をフィルタする。"
"未対応 ID は警告のみで無視される。`--workflow` とは排他指定。"
),
)
orch.add_argument(
"--autopilot-dry-run",
action="store_true",
default=False,
help="--autopilot-chain の実行計画のみ表示して終了する(サブプロセスは起動しない)。",
)
orch.add_argument(
"--autopilot-catalog",
default=None,
metavar="PATH",
help="--autopilot-chain で参照するカタログファイル(既定: docs/catalog/app-arch-catalog.md)。",
)
orch.add_argument(
"--autopilot-max-parallel",
type=int,
default=4,
metavar="N",
help="--autopilot-chain の APP 並列度(既定: 4、1〜16 にクリップ)。",
)
orch.add_argument(
"--enable-agentic-retrieval",
choices=["auto", "yes", "no"],
default=None,
help=(
"Agentic Retrieval Step(AAD-WEB 2.6 / ASDW-WEB 2.5・2.6)の有効化。"
"no を指定すると当該 Step を実行対象から外す。"
"省略時はウィザード回答、それも無ければ auto。"
),
)
orch.add_argument(
"--agentic-data-source-modes",
nargs="+",
choices=["indexer", "push"],
default=None,
metavar="MODE",
help="Agentic Retrieval のデータソース投入方式(indexer / push、複数指定可。既定: indexer)。",
)
orch.add_argument(
"--foundry-mcp-integration",
action=argparse.BooleanOptionalAction,
default=None,
help="Microsoft Foundry 連携(Remote MCP Server)の有無(既定: 有効)。",
)
orch.add_argument(
"--agentic-data-sources-hint",
default=None,
metavar="TEXT",
help="想定データソースのヒント(自由記述)。Knowledge Source 選定の根拠に使う。",
)
orch.add_argument(
"--agentic-existing-design-diff-only",
action=argparse.BooleanOptionalAction,
default=None,
help="既存の Agentic Retrieval 設計を上書きせず差分更新する(既定: 無効)。",
)
orch.add_argument(
"--foundry-sku-fallback-policy",
choices=["standard_allowed", "global_required"],
default=None,
help=(
"Foundry モデル SKU のフォールバック方針。"
"standard_allowed = Standard へのフォールバックを許容(既定)、"
"global_required = Global Standard 必須。"
),
)
orch.add_argument(
"--enable-tool-search",
choices=["auto", "yes", "no"],
default=None,
help=(
"Foundry Toolbox の tool search(Tool 定義の遅延公開)の使用方針。"
"auto = Tool 総数が 15 を超えたら有効化(既定)、"
"yes = 常に有効、no = 使わない。"
),
)
# モデル
orch.add_argument(
"--model", "-m",
default=None,
metavar="MODEL",
help="使用するモデル名 (デフォルト: Auto)。Auto を指定すると GitHub が最適モデルを自動選択します",
)