asia-aidlc 1.14.0 → 1.14.1
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.
- package/.aidlc-engine/cli/flow.py +335 -6
- package/.aidlc-engine/cli/flow_mcp.py +20 -5
- package/.aidlc-engine/docs/design.md +2 -2
- package/.aidlc-engine/skills/team-init/SKILL.md +6 -1
- package/.tmp-init-team-test.py +119 -0
- package/.tmp-mcp-init-test.py +53 -0
- package/.trae/commands/flow-audit.md +13 -0
- package/.trae/commands/flow-branch.md +16 -0
- package/.trae/commands/flow-e2e.md +6 -0
- package/.trae/commands/flow-gate.md +28 -0
- package/.trae/commands/flow-matrix.md +6 -0
- package/.trae/commands/flow-status.md +6 -0
- package/.trae/commands/flow-transition.md +13 -0
- package/.trae/commands/flow-validate.md +8 -0
- package/.trae/hooks/gate_hook.py +334 -0
- package/.trae/hooks.json +60 -0
- package/.trae/mcp.json +11 -0
- package/.trae/rules/flow-orchestration.md +128 -0
- package/.trae/skills/audit-logger/SKILL.md +60 -0
- package/.trae/skills/audit-logger/scripts/append.py +107 -0
- package/.trae/skills/compliance-checker/SKILL.md +40 -0
- package/.trae/skills/compliance-checker/scripts/check.py +32 -0
- package/.trae/skills/design-doc-generator/SKILL.md +38 -0
- package/.trae/skills/gate-verifier/SKILL.md +61 -0
- package/.trae/skills/gate-verifier/scripts/verify.py +228 -0
- package/.trae/skills/observation-doc-generator/SKILL.md +36 -0
- package/.trae/skills/openspec-executor/SKILL.md +61 -0
- package/.trae/skills/release-doc-generator/SKILL.md +38 -0
- package/.trae/skills/render-views/SKILL.md +73 -0
- package/.trae/skills/req-doc-generator/SKILL.md +60 -0
- package/.trae/skills/req-doc-generator/scripts/create_req.py +222 -0
- package/.trae/skills/state-validator/SKILL.md +47 -0
- package/.trae/skills/state-validator/scripts/validate.py +312 -0
- package/.trae/skills/task-split/SKILL.md +114 -0
- package/.trae/skills/task-split/scripts/tasks.py +511 -0
- package/.trae/skills/team-init/SKILL.md +99 -0
- package/.trae/skills/team-init/scripts/init_owners.py +614 -0
- package/.trae/skills/test-manager/SKILL.md +38 -0
- package/.trae/skills/traceability-updater/SKILL.md +34 -0
- package/.trae/skills/traceability-updater/scripts/update.py +98 -0
- package/README.md +106 -152
- package/package.json +1 -1
|
@@ -5,10 +5,14 @@
|
|
|
5
5
|
子命令:
|
|
6
6
|
init 从 example 生成 workflow-state.json 骨架 + 按目录规约创建仓库脚手架
|
|
7
7
|
(产物目录 / 根级登记册 / .gitignore 合并,全部幂等;不动 openspec/ 与 .ai-sdd/)
|
|
8
|
-
+ Trae 接入:.trae/mcp.json 合并、斜杠命令同步、
|
|
8
|
+
+ Trae 接入:.trae/mcp.json 合并、斜杠命令同步、14 个 Skill 复制到
|
|
9
9
|
.trae/skills(--skills-dir 可改目标,--no-skills 跳过;脚本兼容复制安装)
|
|
10
10
|
+ --git-hooks:安装 git 钩子,pull/切分支/commit 后自动重建 viewer 视图
|
|
11
11
|
+ --sync-skills:维护模式,只补装缺失的 Skill(已初始化的仓库也能跑)
|
|
12
|
+
**收尾会问「是否配置团队成员」**(TTY 交互;它决定门控审批人校验、视图「配置负责人」
|
|
13
|
+
列、日历按人筛选)。非 TTY / --no-input 下不提问,改为打印 [next] 待办清单——
|
|
14
|
+
Agent 据此在对话里问用户。一行带全:--team "张敏:产品负责人,…" --admin 张敏
|
|
15
|
+
(按角色自动推断阶段归属并跑一次 validate;未配完只报 [info],不影响退出码)
|
|
12
16
|
hooks 安装/查看/卸载 git 钩子(install / status / uninstall)——让 .aidlc-engine/viewer/
|
|
13
17
|
的派生视图保持最新:钩子只跑 `render.py --if-stale`(新鲜即跳过、不阻断 git);
|
|
14
18
|
自己的钩子可重复安装,他人同名钩子一律不覆盖
|
|
@@ -138,6 +142,21 @@ def _repo_root_from_state(state):
|
|
|
138
142
|
VIEW_OUT_NAME = {"calendar": "calendar.html", "console": "workflow-view.html", "trace": "trace.html"}
|
|
139
143
|
_RENDER_MOD = None # None=未尝试 / False=加载失败 / module=已加载
|
|
140
144
|
_RENDER_NOTE = {} # 最近一次自动重建的结果,供 CLI/MCP 回给用户
|
|
145
|
+
_PENDING_SETUP = {} # init 的交互式收尾(团队/钩子):由 main() 在汇总打印之后执行
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _exec_no_pycache(spec, mod):
|
|
149
|
+
"""执行模块但**不写 __pycache__**。
|
|
150
|
+
|
|
151
|
+
引擎脚本是"源码"而非依赖:在 .aidlc-engine/ 下留 .pyc 会污染仓库,
|
|
152
|
+
而且会被 _install_skills 连带复制进 .trae/skills/。
|
|
153
|
+
"""
|
|
154
|
+
prev = sys.dont_write_bytecode
|
|
155
|
+
sys.dont_write_bytecode = True
|
|
156
|
+
try:
|
|
157
|
+
spec.loader.exec_module(mod)
|
|
158
|
+
finally:
|
|
159
|
+
sys.dont_write_bytecode = prev
|
|
141
160
|
|
|
142
161
|
|
|
143
162
|
def _load_render(system=None):
|
|
@@ -153,7 +172,7 @@ def _load_render(system=None):
|
|
|
153
172
|
import importlib.util
|
|
154
173
|
spec = importlib.util.spec_from_file_location("aidlc_viewer_render", cand)
|
|
155
174
|
mod = importlib.util.module_from_spec(spec)
|
|
156
|
-
spec
|
|
175
|
+
_exec_no_pycache(spec, mod)
|
|
157
176
|
_RENDER_MOD = mod
|
|
158
177
|
except Exception as exc: # fail-open:渲染器坏了也不能拖垮 flow
|
|
159
178
|
LOG.warning("加载 viewer/render.py 失败:%s", exc)
|
|
@@ -332,8 +351,241 @@ GITIGNORE_ENTRIES = ["aidlc-data/traceability-matrix.md", "*.tmp", "aidlc-data/g
|
|
|
332
351
|
"aidlc-data/trace.html", "aidlc-data/req/"]
|
|
333
352
|
|
|
334
353
|
|
|
354
|
+
# ---------------- 团队配置(team-init 的**进程内**调用,同 _load_render 的理由) ----------------
|
|
355
|
+
# Pyodide(npm 包)下没有子进程,进程内 import 才两边都能用;team.json 与状态文件同目录。
|
|
356
|
+
_TEAM_MOD = None
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _load_team_init(system=None):
|
|
360
|
+
"""惰性加载 <system>/skills/team-init/scripts/init_owners.py;失败返回 None。"""
|
|
361
|
+
global _TEAM_MOD
|
|
362
|
+
if _TEAM_MOD is not None:
|
|
363
|
+
return _TEAM_MOD or None
|
|
364
|
+
cand = os.path.join(os.path.abspath(system or DEFAULT_SYSTEM),
|
|
365
|
+
"skills", "team-init", "scripts", "init_owners.py")
|
|
366
|
+
if not os.path.exists(cand):
|
|
367
|
+
_TEAM_MOD = False
|
|
368
|
+
return None
|
|
369
|
+
try:
|
|
370
|
+
import importlib.util
|
|
371
|
+
spec = importlib.util.spec_from_file_location("aidlc_team_init", cand)
|
|
372
|
+
mod = importlib.util.module_from_spec(spec)
|
|
373
|
+
_exec_no_pycache(spec, mod)
|
|
374
|
+
_TEAM_MOD = mod
|
|
375
|
+
except Exception as exc: # fail-open
|
|
376
|
+
LOG.warning("加载 team-init 失败:%s", exc)
|
|
377
|
+
_TEAM_MOD = False
|
|
378
|
+
return None
|
|
379
|
+
return _TEAM_MOD
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def team_path_of(state):
|
|
383
|
+
return os.path.join(os.path.dirname(os.path.abspath(state)), "team.json")
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def team_configured(state):
|
|
387
|
+
"""team.json 是否存在且至少配了一个阶段负责人。"""
|
|
388
|
+
p = team_path_of(state)
|
|
389
|
+
if not os.path.exists(p):
|
|
390
|
+
return False
|
|
391
|
+
try:
|
|
392
|
+
with open(p, encoding="utf-8") as f:
|
|
393
|
+
team = json.load(f)
|
|
394
|
+
except (OSError, ValueError):
|
|
395
|
+
return False
|
|
396
|
+
return any((n.get("owner") or "").strip() for n in (team.get("nodes") or []))
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def run_team_init(system, argv_list):
|
|
400
|
+
"""跑一次 team-init 子命令(进程内、捕获 stdout+stderr)。返回 (ok, 输出文本)。"""
|
|
401
|
+
mod = _load_team_init(system)
|
|
402
|
+
if mod is None:
|
|
403
|
+
return False, "找不到 skills/team-init/scripts/init_owners.py"
|
|
404
|
+
import io
|
|
405
|
+
buf, real_out, real_err = io.StringIO(), sys.stdout, sys.stderr
|
|
406
|
+
try:
|
|
407
|
+
sys.stdout = sys.stderr = buf
|
|
408
|
+
try:
|
|
409
|
+
rc = mod.main(list(argv_list))
|
|
410
|
+
except SystemExit as exc:
|
|
411
|
+
rc = exc.code if isinstance(exc.code, int) else 0
|
|
412
|
+
except Exception as exc:
|
|
413
|
+
return False, "team-init 执行失败:%s" % exc
|
|
414
|
+
finally:
|
|
415
|
+
sys.stdout, sys.stderr = real_out, real_err
|
|
416
|
+
text = buf.getvalue().strip()
|
|
417
|
+
return (not isinstance(rc, int)) or rc == 0, text
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
ROLE_NODE_HINT = (
|
|
421
|
+
("产品", ("N0", "N1")), ("需求", ("N0", "N1")), ("项目", ("N0", "N1")),
|
|
422
|
+
("技术", ("N2",)), ("架构", ("N2", "N3")),
|
|
423
|
+
("研发", ("N3",)), ("开发", ("N3",)),
|
|
424
|
+
("测试", ("N4",)), ("qa", ("N4",)), ("质量", ("N4",)),
|
|
425
|
+
("交付", ("N5",)), ("发布", ("N5",)), ("运维", ("N5",)),
|
|
426
|
+
)
|
|
427
|
+
NODE_LABEL = (("N0", "任务受理"), ("N1", "需求生成"), ("N2", "方案生成"),
|
|
428
|
+
("N3", "代码生成"), ("N4", "测试执行"), ("N5", "交付发布"))
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def parse_members(text):
|
|
432
|
+
"""把「张敏:产品负责人 / 李强:技术负责人 / 王磊」解析成 [(名字, 角色)];换行与逗号都当分隔。"""
|
|
433
|
+
items = []
|
|
434
|
+
for chunk in re.split(r"[\n,,;;]+", text or ""):
|
|
435
|
+
chunk = chunk.strip()
|
|
436
|
+
if not chunk:
|
|
437
|
+
continue
|
|
438
|
+
name, _, role = chunk.partition(":")
|
|
439
|
+
if not role:
|
|
440
|
+
name, _, role = chunk.partition(":")
|
|
441
|
+
name, role = name.strip(), role.strip()
|
|
442
|
+
if name:
|
|
443
|
+
items.append((name, role))
|
|
444
|
+
return items
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def suggest_owners(members):
|
|
448
|
+
"""按角色名猜阶段归属(只作**默认值**给用户确认,不静默落盘)。"""
|
|
449
|
+
owners = {}
|
|
450
|
+
for name, role in members:
|
|
451
|
+
low = (role or "").lower()
|
|
452
|
+
for kw, nodes in ROLE_NODE_HINT:
|
|
453
|
+
if kw in role or kw in low:
|
|
454
|
+
for nid in nodes:
|
|
455
|
+
owners.setdefault(nid, name)
|
|
456
|
+
break
|
|
457
|
+
return owners
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def _ask(prompt, default=""):
|
|
461
|
+
"""读一行输入;非 TTY / EOF / Ctrl-C 一律回落到默认值(绝不因提问把 init 弄挂)。"""
|
|
462
|
+
try:
|
|
463
|
+
shown = (" [%s]" % default) if default else ""
|
|
464
|
+
line = input("%s%s: " % (prompt, shown))
|
|
465
|
+
except (EOFError, KeyboardInterrupt, OSError):
|
|
466
|
+
print()
|
|
467
|
+
return default
|
|
468
|
+
return line.strip() or default
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def setup_team_wizard(system, state):
|
|
472
|
+
"""交互式配团队(最简版:单负责人)。返回要打印的文本。
|
|
473
|
+
|
|
474
|
+
**由 main() 在 init 汇总打印之后调用**——否则提问会插在汇总前面,看起来很乱。
|
|
475
|
+
"""
|
|
476
|
+
data_root = os.path.dirname(os.path.abspath(state))
|
|
477
|
+
tpath = team_path_of(state)
|
|
478
|
+
try:
|
|
479
|
+
project = (read_state(state).get("meta") or {}).get("project") or ""
|
|
480
|
+
except Exception:
|
|
481
|
+
project = ""
|
|
482
|
+
project = project or os.path.basename(os.path.dirname(data_root)) or "未命名项目"
|
|
483
|
+
|
|
484
|
+
print("\n" + "─" * 8 + " 下一步:配置团队成员 " + "─" * 8)
|
|
485
|
+
print(" 它决定:门控审批人校验、视图里的「配置负责人」、日历按人筛选。")
|
|
486
|
+
print(" [1] 现在配(约 30 秒) [2] 先跳过(随时可配:flow init --team \"张敏:产品负责人\")")
|
|
487
|
+
if _ask("选择", "1") not in ("1", "y", "Y", "yes", ""):
|
|
488
|
+
return " 已跳过。之后可用:flow init --team \"张敏:产品负责人,…\" --admin 张敏"
|
|
489
|
+
|
|
490
|
+
proj = _ask("项目名", project)
|
|
491
|
+
admin = _ask("管理员 / 产品负责人(填入 N0/N1 与 req_review)", "")
|
|
492
|
+
print("团队名单(每行一条,格式 名字:角色;空行结束)")
|
|
493
|
+
roster = []
|
|
494
|
+
while True:
|
|
495
|
+
try:
|
|
496
|
+
ln = input(" > ").strip()
|
|
497
|
+
except (EOFError, KeyboardInterrupt, OSError):
|
|
498
|
+
print()
|
|
499
|
+
break
|
|
500
|
+
if not ln:
|
|
501
|
+
break
|
|
502
|
+
roster += parse_members(ln)
|
|
503
|
+
if admin and all(n != admin for n, _ in roster):
|
|
504
|
+
roster.insert(0, (admin, "项目管理员"))
|
|
505
|
+
if not roster:
|
|
506
|
+
return " 名单为空,已跳过。随时可配:flow init --team \"张敏:产品负责人\" --admin 张敏"
|
|
507
|
+
|
|
508
|
+
sug = suggest_owners(roster)
|
|
509
|
+
print("六个阶段分别归谁?(格式 N0=张敏,N1=张敏,…;回车用下面的建议值;留空 = 暂不指定)")
|
|
510
|
+
for nid, label in NODE_LABEL:
|
|
511
|
+
print(" %s %-6s → %s" % (nid, label, sug.get(nid) or "(待定)"))
|
|
512
|
+
mapping = _ask("阶段归属", ",".join("%s=%s" % (nid, sug.get(nid, "")) for nid, _ in NODE_LABEL))
|
|
513
|
+
|
|
514
|
+
base = ["--file", tpath, "--state", os.path.abspath(state)]
|
|
515
|
+
init_argv = base + (["--project", proj] if proj else [])
|
|
516
|
+
outs = []
|
|
517
|
+
if not os.path.exists(tpath):
|
|
518
|
+
ok, t = run_team_init(system, ["init"] + init_argv +
|
|
519
|
+
(["--admin", admin] if admin else ["--force"]))
|
|
520
|
+
outs.append(t)
|
|
521
|
+
if not ok:
|
|
522
|
+
return "\n".join(outs + [" [warn] 初始化 team.json 失败,可重试:flow init --team \"…\""])
|
|
523
|
+
args = ["add-members"] + base
|
|
524
|
+
for name, role in roster:
|
|
525
|
+
args += ["--member", ("%s:%s" % (name, role)) if role else name]
|
|
526
|
+
ok, t = run_team_init(system, args)
|
|
527
|
+
outs.append(t)
|
|
528
|
+
if not ok:
|
|
529
|
+
return "\n".join(outs + [" [warn] 写入成员失败,已保留已写入的部分"])
|
|
530
|
+
pairs = [p.strip() for p in mapping.split(",") if "=" in p and p.split("=", 1)[1].strip()]
|
|
531
|
+
if pairs:
|
|
532
|
+
ok, t = run_team_init(system, ["apply"] + base + sum([["--pair", p] for p in pairs], []))
|
|
533
|
+
outs.append(t)
|
|
534
|
+
okv, tv = run_team_init(system, ["validate"] + base)
|
|
535
|
+
outs.append(" 项目:%s | 管理员:%s | 成员:%s" %
|
|
536
|
+
(proj, admin or "(未指定)", "、".join(n for n, _ in roster)))
|
|
537
|
+
outs += _team_report(tv)
|
|
538
|
+
outs.append(" 随时可改:flow init --team \"…\" / 直接编辑 aidlc-data/team.json")
|
|
539
|
+
return "\n".join(outs)
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def _team_report(validate_text):
|
|
543
|
+
"""把 team-init validate 的结果整理成「硬问题 / 待补 / 通过」,并给出可执行的补救命令。
|
|
544
|
+
|
|
545
|
+
注意:**不要把 validate 的原始 [FAIL] 行直接拼进 init 的输出**——init 本身是成功的,
|
|
546
|
+
带着 [FAIL] 会让 `flow init` 退出码变成 1,CI 与 Agent 的错误处理都会被误触发。
|
|
547
|
+
"""
|
|
548
|
+
problems = [ln.strip()[2:].strip() for ln in (validate_text or "").splitlines()
|
|
549
|
+
if ln.strip().startswith("- ")]
|
|
550
|
+
hard = [p for p in problems if ("DUTY_CONFLICT" in p or "不在成员名单" in p)]
|
|
551
|
+
todo = [p for p in problems if p not in hard]
|
|
552
|
+
lines = []
|
|
553
|
+
if hard:
|
|
554
|
+
lines.append("[warn] 团队配置有硬问题(必须修):")
|
|
555
|
+
lines += [" - %s" % p for p in hard]
|
|
556
|
+
if todo:
|
|
557
|
+
lines.append("[info] 还有 %d 项待补(不影响现在继续用):" % len(todo))
|
|
558
|
+
lines += [" - %s" % p for p in todo]
|
|
559
|
+
if any("代码评审" in p or "code_review" in p for p in todo):
|
|
560
|
+
lines.append(" 代码评审需要**独立于 N3 负责人**的评审人(作者≠评审人),例如:")
|
|
561
|
+
lines.append(" aidlc skill team-init set --gate code_review --approver <独立的人>")
|
|
562
|
+
if any("未配置负责人" in p for p in todo):
|
|
563
|
+
lines.append(" 补齐阶段负责人:flow init --team \"名字:角色,…\""
|
|
564
|
+
"(或直接编辑 aidlc-data/team.json)")
|
|
565
|
+
if not hard and not todo:
|
|
566
|
+
lines.append("[ok] 团队配置校验通过")
|
|
567
|
+
return lines
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def next_steps_block(state, team_done=False, hooks_done=False):
|
|
571
|
+
"""init 之后的待办清单:人类可读、Agent 可解析(`[next] key=value`)。"""
|
|
572
|
+
lines = []
|
|
573
|
+
if not team_done and not team_configured(state):
|
|
574
|
+
lines += [
|
|
575
|
+
"[next] step=team 状态=未配置 影响=门控审批人校验/视图「配置负责人」列/日历按人筛选",
|
|
576
|
+
"[next] step=team 一条命令配好=flow init --team \"张敏:产品负责人,李强:技术负责人\" --admin 张敏",
|
|
577
|
+
"[next] step=team 跳过=回答「先跳过」即可(不影响 init 结果;随时可再配)",
|
|
578
|
+
]
|
|
579
|
+
if not hooks_done:
|
|
580
|
+
lines.append("[next] step=hooks 状态=未安装 影响=commit/切分支后视图不自动重建 "
|
|
581
|
+
"cmd=\"flow init --git-hooks\"")
|
|
582
|
+
if lines:
|
|
583
|
+
lines.append("[next] step=view cmd=\"flow view\"(渲染日历 + 每个需求一个文件,并给出可点开的路径)")
|
|
584
|
+
return "\n".join(lines)
|
|
585
|
+
|
|
586
|
+
|
|
335
587
|
def do_init(system, state, skills_dir=".trae/skills", no_skills=False, empty=False, git_hooks=False,
|
|
336
|
-
sync_skills=False):
|
|
588
|
+
sync_skills=False, team="", admin="", no_input=False):
|
|
337
589
|
src = os.path.join(STATE_DIR, "workflow-state.example.json")
|
|
338
590
|
# 钩子装到状态文件所在仓库(脚本自身会 git rev-parse 定位仓库根,传目录即可)
|
|
339
591
|
hook_start = os.path.dirname(os.path.abspath(state)) or os.getcwd()
|
|
@@ -390,6 +642,52 @@ def do_init(system, state, skills_dir=".trae/skills", no_skills=False, empty=Fal
|
|
|
390
642
|
out += "\n" + dep
|
|
391
643
|
if hint:
|
|
392
644
|
out += hint
|
|
645
|
+
|
|
646
|
+
# ---- 收尾:团队 + 下一步(让「init 完不知道该干嘛」这件事消失)----
|
|
647
|
+
global _PENDING_SETUP
|
|
648
|
+
team_done = team_configured(state)
|
|
649
|
+
if team.strip(): # 一行带全:调用方已知道答案(Agent / 脚本)
|
|
650
|
+
proj = ""
|
|
651
|
+
try:
|
|
652
|
+
proj = (data.get("meta") or {}).get("project") or ""
|
|
653
|
+
except Exception:
|
|
654
|
+
proj = ""
|
|
655
|
+
base = ["--file", team_path_of(state), "--state", os.path.abspath(state)]
|
|
656
|
+
init_argv = base + (["--project", proj] if proj else [])
|
|
657
|
+
members = parse_members(team)
|
|
658
|
+
if not team_done:
|
|
659
|
+
run_team_init(system, ["init"] + init_argv + (["--admin", admin] if admin else ["--force"]))
|
|
660
|
+
args2 = ["add-members"] + base
|
|
661
|
+
for name, role in members:
|
|
662
|
+
args2 += ["--member", ("%s:%s" % (name, role)) if role else name]
|
|
663
|
+
ok1, t2 = run_team_init(system, args2)
|
|
664
|
+
sug = suggest_owners(members)
|
|
665
|
+
pairs = [["--pair", "%s=%s" % (nid, sug[nid])] for nid, _ in NODE_LABEL if sug.get(nid)]
|
|
666
|
+
ok2, t3 = (run_team_init(system, ["apply"] + base + sum(pairs, [])) if pairs else (True, ""))
|
|
667
|
+
okv, t4 = run_team_init(system, ["validate"] + base)
|
|
668
|
+
out += "\n" + "\n".join(x for x in (t2, t3) if x)
|
|
669
|
+
out += "\n" + "\n".join(_team_report(t4))
|
|
670
|
+
if pairs:
|
|
671
|
+
out += "\n 阶段归属按角色推断:" + "、".join(
|
|
672
|
+
"%s=%s" % (p[1].split("=")[0], p[1].split("=")[1]) for p in pairs)
|
|
673
|
+
team_done = team_configured(state)
|
|
674
|
+
if admin:
|
|
675
|
+
out += "\nOK: 管理员 %s 已填入 N0/N1 与 req_review" % admin
|
|
676
|
+
if not (ok1 and ok2):
|
|
677
|
+
out += "\n[warn] 写入成员/阶段归属未成功,可重试:flow init --team \"…\" --admin <名字>"
|
|
678
|
+
elif not team_done and not no_input and sys.stdin.isatty():
|
|
679
|
+
# 提问放在 main() 打印完汇总之后(否则提示会插在 OK 行前面)
|
|
680
|
+
_PENDING_SETUP = {"system": system, "state": state}
|
|
681
|
+
|
|
682
|
+
if not git_hooks and not no_input and sys.stdin.isatty() and not team.strip():
|
|
683
|
+
_PENDING_SETUP.setdefault("hooks_dir", hook_start)
|
|
684
|
+
elif not git_hooks:
|
|
685
|
+
pass
|
|
686
|
+
|
|
687
|
+
blk = next_steps_block(state, team_done=team_done or bool(team.strip()),
|
|
688
|
+
hooks_done=git_hooks or bool(_PENDING_SETUP.get("hooks_dir")))
|
|
689
|
+
if blk:
|
|
690
|
+
out += "\n\n下一步:\n" + blk
|
|
393
691
|
return out
|
|
394
692
|
|
|
395
693
|
|
|
@@ -568,7 +866,7 @@ def _install_skills(system, root, target_rel=".trae/skills"):
|
|
|
568
866
|
if os.path.exists(dst):
|
|
569
867
|
skipped.append(name)
|
|
570
868
|
continue
|
|
571
|
-
shutil.copytree(src, dst)
|
|
869
|
+
shutil.copytree(src, dst, ignore=shutil.ignore_patterns("__pycache__", "*.pyc", "*.pyo"))
|
|
572
870
|
copied.append(name)
|
|
573
871
|
msg = "技能 %d 个 → %s(%s)" % (len(copied), target_rel, ",".join(copied) or "无新增")
|
|
574
872
|
if skipped:
|
|
@@ -1703,7 +2001,7 @@ def main():
|
|
|
1703
2001
|
ap = argparse.ArgumentParser(prog="flow", description="OpenSpec+AI-DLC 研发流程统一 CLI")
|
|
1704
2002
|
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
1705
2003
|
|
|
1706
|
-
p = sub.add_parser("init", help="
|
|
2004
|
+
p = sub.add_parser("init", help="初始化:状态骨架 + 脚手架 + Trae 接入件/技能;结束时会询问是否配团队")
|
|
1707
2005
|
p.add_argument("--state", default=DEFAULT_STATE)
|
|
1708
2006
|
p.add_argument("--system", default=DEFAULT_SYSTEM, help=".aidlc-engine 引擎目录(含 skills/ 与 state/)")
|
|
1709
2007
|
p.add_argument("--skills-dir", default=".trae/skills",
|
|
@@ -1716,6 +2014,12 @@ def main():
|
|
|
1716
2014
|
p.add_argument("--sync-skills", dest="sync_skills", action="store_true",
|
|
1717
2015
|
help="只补装缺失的 Skill 到 --skills-dir(已初始化的仓库也能跑;"
|
|
1718
2016
|
"新增 Skill 后用它在 .trae/skills 落地,不动状态与脚手架)")
|
|
2017
|
+
p.add_argument("--team", default="",
|
|
2018
|
+
help="一行配好团队:\"张敏:产品负责人,李强:技术负责人\"(逗号/换行分隔;"
|
|
2019
|
+
"按角色自动猜阶段归属,配完跑一次 validate)")
|
|
2020
|
+
p.add_argument("--admin", default="", help="项目管理员(填入 N0/N1 负责人与 req_review 审批人)")
|
|
2021
|
+
p.add_argument("--no-input", dest="no_input", action="store_true",
|
|
2022
|
+
help="不提问(CI/无头环境;只打印 [next] 待办清单,不读 stdin)")
|
|
1719
2023
|
|
|
1720
2024
|
p = sub.add_parser("hooks", help="git 钩子:让 .aidlc-engine/viewer/ 派生视图保持实时")
|
|
1721
2025
|
hsub = p.add_subparsers(dest="hooks_cmd", required=True)
|
|
@@ -1863,7 +2167,8 @@ def main():
|
|
|
1863
2167
|
|
|
1864
2168
|
dispatch = {
|
|
1865
2169
|
"init": lambda: do_init(system, a.state, a.skills_dir, a.no_skills, a.empty, a.git_hooks,
|
|
1866
|
-
a.sync_skills),
|
|
2170
|
+
a.sync_skills, getattr(a, "team", ""), getattr(a, "admin", ""),
|
|
2171
|
+
getattr(a, "no_input", False)),
|
|
1867
2172
|
"hooks install": lambda: install_viewer_hooks(os.getcwd()),
|
|
1868
2173
|
"hooks status": lambda: viewer_hooks_status(os.getcwd()),
|
|
1869
2174
|
"hooks uninstall": lambda: uninstall_viewer_hooks(os.getcwd()),
|
|
@@ -1902,6 +2207,30 @@ def main():
|
|
|
1902
2207
|
if note:
|
|
1903
2208
|
out = (out.rstrip() + "\n\n" + note) if out else note
|
|
1904
2209
|
print(out)
|
|
2210
|
+
|
|
2211
|
+
# init 的交互式收尾放在汇总之后:否则提问会插在 OK 行前面,读起来是乱的
|
|
2212
|
+
global _PENDING_SETUP
|
|
2213
|
+
if _PENDING_SETUP.get("system"):
|
|
2214
|
+
st = _PENDING_SETUP.get("state")
|
|
2215
|
+
try:
|
|
2216
|
+
print(setup_team_wizard(_PENDING_SETUP["system"], st))
|
|
2217
|
+
except (EOFError, KeyboardInterrupt):
|
|
2218
|
+
print("\n 已取消。随时可配:flow init --team \"张敏:产品负责人\" --admin 张敏")
|
|
2219
|
+
except Exception as exc: # 向导失败绝不拖垮 init
|
|
2220
|
+
LOG.warning("配团队向导失败:%s", exc)
|
|
2221
|
+
print(" [warn] 交互式配置团队失败(%s);可改用:"
|
|
2222
|
+
"flow init --team \"张敏:产品负责人\" --admin 张敏" % exc)
|
|
2223
|
+
else:
|
|
2224
|
+
_auto_render(st) # 团队变了,让视图跟上
|
|
2225
|
+
extra = view_note()
|
|
2226
|
+
if extra:
|
|
2227
|
+
print("\n" + extra)
|
|
2228
|
+
if _PENDING_SETUP.get("hooks_dir"):
|
|
2229
|
+
if _ask("是否安装 git 钩子(commit/切分支后自动重建视图)", "y").lower() in ("y", "yes", "1"):
|
|
2230
|
+
print("OK: " + install_viewer_hooks(_PENDING_SETUP["hooks_dir"]))
|
|
2231
|
+
else:
|
|
2232
|
+
print(" 已跳过。之后可装:flow init --git-hooks / flow hooks install")
|
|
2233
|
+
_PENDING_SETUP = {}
|
|
1905
2234
|
# 进程退出码反映判定结果(供 shell/CI 消费):失败标记 → 1
|
|
1906
2235
|
fail_markers = (
|
|
1907
2236
|
"[FAIL]", "[REJECT]", "[ERROR]", "Traceback",
|
|
@@ -40,11 +40,17 @@ DEFAULT_STATE = None # 由调用方 arguments 传 state,缺省用 cwd
|
|
|
40
40
|
def resolve_state(arg):
|
|
41
41
|
if arg and os.path.exists(arg):
|
|
42
42
|
return arg
|
|
43
|
-
# 两目录布局优先:cwd/aidlc-data/workflow-state.json
|
|
43
|
+
# 两目录布局优先:cwd/aidlc-data/workflow-state.json
|
|
44
44
|
new = os.path.join(os.getcwd(), flow.DATA_DIR, "workflow-state.json")
|
|
45
45
|
if os.path.exists(new):
|
|
46
46
|
return new
|
|
47
|
-
|
|
47
|
+
# 旧布局(根目录平铺)**只在真的存在时**才用;否则一律走两目录布局——
|
|
48
|
+
# 否则全新项目里 flow_init 会把 workflow-state.json / team.json 建到仓库根,
|
|
49
|
+
# 与 `flow init` 的缺省路径(aidlc-data/)不一致。
|
|
50
|
+
legacy = os.path.join(os.getcwd(), "workflow-state.json")
|
|
51
|
+
if os.path.exists(legacy):
|
|
52
|
+
return legacy
|
|
53
|
+
return new
|
|
48
54
|
|
|
49
55
|
|
|
50
56
|
TOOLS = [
|
|
@@ -81,10 +87,17 @@ TOOLS = [
|
|
|
81
87
|
"required": ["actor", "action", "entity", "result"]}},
|
|
82
88
|
{"name": "flow_matrix", "description": "渲染追溯矩阵(输出到状态文件同目录 traceability-matrix.md)",
|
|
83
89
|
"inputSchema": {"type": "object", "properties": {"state": {"type": "string"}}}},
|
|
84
|
-
{"name": "flow_init", "description": "初始化 workflow-state.json + 仓库脚手架 + Trae
|
|
90
|
+
{"name": "flow_init", "description": "初始化 workflow-state.json + 仓库脚手架 + Trae 接入件/技能/编排规则安装。"
|
|
91
|
+
"【初始化后请主动问用户是否配置团队成员】——它决定门控审批人校验、视图里的「配置负责人」列、日历按人筛选。"
|
|
92
|
+
"用户给了名单就用 team=\"张敏:产品负责人,李强:技术负责人\" + admin 一步配好(会按角色自动推断阶段归属);"
|
|
93
|
+
"用户说「先跳过」就直接不传(返回值里的 [next] 清单会说明后果与随时补配的命令),不要反复追问。"
|
|
94
|
+
"注意:本工具**不会**在返回前提问(MCP 无人机交互),提问由你在对话里完成。",
|
|
85
95
|
"inputSchema": {"type": "object",
|
|
86
96
|
"properties": {"state": {"type": "string"},
|
|
87
|
-
"empty": {"type": "boolean", "description": "空白初始化:清空示例实体,仅保留骨架"}
|
|
97
|
+
"empty": {"type": "boolean", "description": "空白初始化:清空示例实体,仅保留骨架"},
|
|
98
|
+
"team": {"type": "string",
|
|
99
|
+
"description": "团队成员名单,格式 \"名字:角色,名字:角色\"(用户确认后传入;不传则只输出待办)"},
|
|
100
|
+
"admin": {"type": "string", "description": "项目管理员(填入 N0/N1 负责人与 req_review 审批人)"}}}},
|
|
88
101
|
{"name": "flow_branch_create", "description": "建执行分支 feature/chg-<NNN> + 登记 base_commit/owned + 审计 branch.create(分支做容器,状态推进仍走 transition)",
|
|
89
102
|
"inputSchema": {"type": "object",
|
|
90
103
|
"properties": {"chg": {"type": "string", "description": "change 编号,如 CHG-003"},
|
|
@@ -147,7 +160,9 @@ HANDLERS = {
|
|
|
147
160
|
a["actor"], a["action"], a["entity"], a["result"], a.get("detail", "")),
|
|
148
161
|
"flow_matrix": lambda a: flow.do_matrix(DEFAULT_SYSTEM, resolve_state(a.get("state"))),
|
|
149
162
|
"flow_init": lambda a: flow.do_init(DEFAULT_SYSTEM, resolve_state(a.get("state")),
|
|
150
|
-
empty=bool(a.get("empty"))
|
|
163
|
+
empty=bool(a.get("empty")),
|
|
164
|
+
team=a.get("team", ""), admin=a.get("admin", ""),
|
|
165
|
+
no_input=True),
|
|
151
166
|
"flow_branch_create": lambda a: flow.do_branch_create(DEFAULT_SYSTEM, resolve_state(a.get("state")),
|
|
152
167
|
a["chg"], a.get("actor", "编排Agent")),
|
|
153
168
|
"flow_branch_merge": lambda a: flow.do_branch_merge(DEFAULT_SYSTEM, resolve_state(a.get("state")),
|
|
@@ -369,7 +369,7 @@ hook 一览(全部 fail-open:读盘失败放行,硬校验由 flow CLI 兜
|
|
|
369
369
|
|
|
370
370
|
| 子命令 | 行为 | 审计 |
|
|
371
371
|
|---|---|---|
|
|
372
|
-
| `flow init` | 从 example 生成 workflow-state.json 骨架 + 仓库脚手架 + Trae 接入安装。`_scaffold`:按目录规约创建 10 个产物目录;`risk-register.md`/`dependency-map.md` 模板落仓库根;派生状态条目合并进 `.gitignore`(逐行去重)。`_install_trae`:`.aidlc-engine/cli/.trae/mcp.json` 合并到项目根 `.trae/`(保留既有 server;utf-8-sig 兼容 BOM;python 路径含空格自动改写绝对路径)、斜杠命令覆盖式同步。`_install_agent_rule`:编排 Agent 提示词写入 `.trae/rules/flow-orchestration.md`(alwaysApply,已存在跳过)。`_install_skills`:14 个 Skill 复制到 `.trae/skills/`(默认,`--skills-dir` 可改、`--no-skills` 跳过;已存在跳过)。`--empty` 空白初始化(清空示例实体)。init 检测 jsonschema 缺失并提示;flow.py/flow_mcp.py 启动时 reconfigure stdout/stderr 为 UTF-8(消除 GBK 乱码)。全部幂等;不创建 `openspec/` 与 `.ai-sdd/`(SDD 工作区层)。复制安装的脚本(verify/validate/append)以 `_resolve_state_dir()` 解析 state:`__file__` 上溯 → `FLOW_SYSTEM_DIR` 环境变量 → cwd 向上查找 `.aidlc-engine/state`。`--git-hooks` 顺带安装 viewer
|
|
372
|
+
| `flow init` | 从 example 生成 workflow-state.json 骨架 + 仓库脚手架 + Trae 接入安装。`_scaffold`:按目录规约创建 10 个产物目录;`risk-register.md`/`dependency-map.md` 模板落仓库根;派生状态条目合并进 `.gitignore`(逐行去重)。`_install_trae`:`.aidlc-engine/cli/.trae/mcp.json` 合并到项目根 `.trae/`(保留既有 server;utf-8-sig 兼容 BOM;python 路径含空格自动改写绝对路径)、斜杠命令覆盖式同步。`_install_agent_rule`:编排 Agent 提示词写入 `.trae/rules/flow-orchestration.md`(alwaysApply,已存在跳过)。`_install_skills`:14 个 Skill 复制到 `.trae/skills/`(默认,`--skills-dir` 可改、`--no-skills` 跳过;已存在跳过)。`--empty` 空白初始化(清空示例实体)。init 检测 jsonschema 缺失并提示;flow.py/flow_mcp.py 启动时 reconfigure stdout/stderr 为 UTF-8(消除 GBK 乱码)。全部幂等;不创建 `openspec/` 与 `.ai-sdd/`(SDD 工作区层)。复制安装的脚本(verify/validate/append)以 `_resolve_state_dir()` 解析 state:`__file__` 上溯 → `FLOW_SYSTEM_DIR` 环境变量 → cwd 向上查找 `.aidlc-engine/state`。`--git-hooks` 顺带安装 viewer 钩子(见下)。**收尾询问团队**:TTY 下问「是否配置团队成员」(`_PENDING_SETUP` 延后到 main() 打印完汇总再交互,避免提问插在 OK 行前面),走 `setup_team_wizard()` → 进程内调 team-init 的 `init/add-members/apply/validate`;非 TTY(Agent/CI/管道)或 `--no-input` **绝不读 stdin**,改为输出 `[next] step=…` 结构化待办,由编排 Agent 在对话里问用户。`--team "名字:角色,…" --admin X` 一行带全(按角色经 `ROLE_NODE_HINT` 推断阶段归属)。**汇报分级**:`_team_report()` 把 validate 结果分成「硬问题(DUTY_CONFLICT/不在成员名单)」与「待补(未配置负责人/审批人)」,后者只报 `[info]`——**不把 validate 的原始 `[FAIL]` 拼进 init 输出**,否则 init 明明成功也会被 `fail_markers` 判成退出码 1。`code_review` 需独立于 N3 负责人的评审人,init 猜不出来,故给出补救命令。**维护模式**:`--git-hooks` / `--sync-skills` 在状态文件已存在时不再 `[FAIL]` 退出,而是打印 `[skip] … 不覆盖(维护模式)` 并只执行所请求的安装动作(补钩子 / 补 Skill),供已初始化仓库补装新 Skill。`_install_skills` 复制时忽略 `__pycache__`/`*.pyc` | 无 |
|
|
373
373
|
| `flow hooks install/status/uninstall` | **viewer 派生视图实时化**(`.aidlc-engine/viewer/README.md`「让视图保持最新」):把 `render.py --if-stale` 挂到 `post-merge`/`post-checkout`/`post-commit`/`post-rewrite`,pull / 切分支 / commit / rebase 后自动重建视图。脚本为 POSIX sh(Git for Windows 自带 sh 执行),内容恒为 `_viewer_hook_script()` 生成并带 `aidlc-viewer-hook` 标记。契约:**幂等**(本工具的钩子覆盖更新,他人同名钩子一律跳过不覆盖)、**fail-open**(任何失败都放行 git,末尾恒 `exit 0`)、**新鲜即跳过**(`--if-stale`)、**无真相不渲染**(`aidlc-data/workflow-state.json` 缺失时不动作,避免 render.py 回退样例快照污染仓库)、`post-checkout` 仅 `$3=1` 分支切换触发;逃生开关 `AIDLC_SKIP_VIEWER_HOOK=1`、`AIDLC_VIEWER_FORCE=1`、追加参数 `AIDLC_VIEWER_HOOK_ARGS`。钩子目录尊重 `core.hooksPath`,`.git` 为文件(worktree/submodule)按 `gitdir:` 解析;git 不可用时向上查找 `.git` 降级 | 无 |
|
|
374
374
|
| `flow status` | 输出需求看板/实体统计/覆盖率 | 无 |
|
|
375
375
|
| `flow view` | **交付可视化视图的唯一入口**:`kind` ∈ calendar(默认,研发日历 + 每个需求一个文件)/ console / trace。渲染后输出**可点开的绝对路径**(+ 单需求文件目录),`--open` 用系统默认程序打开;**只产出静态 HTML,不起任何服务**(UI 状态存 URL hash,刷新/分享不丢筛选)。与 MCP `flow_render`、Skill `render-views` 同源(都走 `do_view()`)。**状态写盘后自动重建**:`write_state()` 末尾调 `render_views(if_stale=True)`(幂等 + fail-open + 静默),并在命令输出末尾附 `[视图]` 提示行;`AIDLC_RENDER=off` 关闭 | 无 |
|
|
@@ -406,7 +406,7 @@ MCP stdio Server(JSON-RPC 2.0),暴露 14 个工具供 Trae 调用:
|
|
|
406
406
|
| `flow_compliance` | do_compliance |
|
|
407
407
|
| `flow_audit` | do_audit |
|
|
408
408
|
| `flow_matrix` | do_matrix |
|
|
409
|
-
| `flow_init` | do_init |
|
|
409
|
+
| `flow_init` | do_init。除 `empty` 外新增 **`team` / `admin`**:用户在对话里确认名单后一步配好(MCP 侧强制 `no_input=True`——**绝不在返回前提问**,提问由 Agent 在对话里完成;不传 `team` 则返回值带 `[next]` 待办清单,含「初始化后请主动问用户是否配团队」的触发指引)。`resolve_state` 两目录布局优先,旧布局**只在真的存在时才用**,避免全新项目把数据建到仓库根 |
|
|
410
410
|
| `flow_branch_create` / `flow_branch_merge` / `flow_branch_discard` | do_branch_create/merge/discard |
|
|
411
411
|
| `flow_req_create` | do_req_create |
|
|
412
412
|
| `flow_e2e` | do_e2e |
|
|
@@ -11,11 +11,16 @@ description: 初始化与维护「六阶段节点负责人 + 四门控审批人
|
|
|
11
11
|
|
|
12
12
|
| 场景 | 入口 |
|
|
13
13
|
|---|---|
|
|
14
|
+
| **`flow init` 收尾时被问到** | init 会主动问「是否配置团队成员」(TTY 交互;非 TTY 打印 `[next]` 待办让编排 Agent 在对话里问)。这时就是本 Skill 的活 |
|
|
14
15
|
| 项目开工,一次把团队配好 | 对话:"初始化团队,管理员张敏,方案李强,开发王磊,测试赵倩,发布陈昊" |
|
|
15
16
|
| 中途换人 | 对话:"把测试负责人改成赵倩" / "代码评审加上赵倩" |
|
|
16
17
|
| 查看现状 | 对话:"现在谁负责发布?" → `list` |
|
|
17
18
|
| 开工前体检 | 对话:"负责人配齐了吗?" → `validate` |
|
|
18
|
-
| 无对话环境(CI / 脚本 / 手工) | 直接跑下面的 CLI |
|
|
19
|
+
| 无对话环境(CI / 脚本 / 手工) | 直接跑下面的 CLI;Agent 已拿到名单时可用 `flow init --team "名字:角色,…" --admin X` 一步配好 |
|
|
20
|
+
|
|
21
|
+
> **多人**:当前模型是**每个阶段一个负责人**(`nodes[].owner` 单值);门控审批人已经是列表
|
|
22
|
+
> (`--approver` 可多次)。多负责人(`owners` 列表)是规划中的改动,尚未实现——需要时先用
|
|
23
|
+
> 门控审批人(可多人)表达"这条事多人参与"。
|
|
19
24
|
|
|
20
25
|
## 六个节点 / 四个门控(固定,来自 `directory-convention.md` 与 `agent-roles.md`)
|
|
21
26
|
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""临时验证:init 的团队向导(打桩 input)+ 待办块 + MCP 入参。"""
|
|
3
|
+
import builtins
|
|
4
|
+
import importlib.util
|
|
5
|
+
import io
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
import contextlib
|
|
10
|
+
|
|
11
|
+
ROOT = os.path.abspath(os.path.dirname(os.path.abspath(__file__)))
|
|
12
|
+
CLI = os.path.join(ROOT, ".aidlc-engine", "cli")
|
|
13
|
+
sys.path.insert(0, CLI)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def load(name, path):
|
|
17
|
+
spec = importlib.util.spec_from_file_location(name, path)
|
|
18
|
+
mod = importlib.util.module_from_spec(spec)
|
|
19
|
+
spec.loader.exec_module(mod)
|
|
20
|
+
return mod
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
flow = load("flow_t", os.path.join(CLI, "flow.py"))
|
|
24
|
+
fails = []
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def ck(name, cond, extra=""):
|
|
28
|
+
print((" OK " if cond else " FAIL ") + name + ("" if cond else " " + str(extra)))
|
|
29
|
+
if not cond:
|
|
30
|
+
fails.append(name)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
state = os.path.join(ROOT, ".tmp-wz", "aidlc-data", "workflow-state.json")
|
|
34
|
+
os.makedirs(os.path.dirname(state), exist_ok=True)
|
|
35
|
+
with open(state, "w", encoding="utf-8") as f:
|
|
36
|
+
json.dump({"meta": {"project": "订单中心", "updated_at": "2026-10-09T00:00:00+08:00",
|
|
37
|
+
"owner": "张敏"}}, f, ensure_ascii=False)
|
|
38
|
+
|
|
39
|
+
# ---- 1) 名单解析 + 角色推断 ----
|
|
40
|
+
ck("parse_members 支持逗号/换行/中英文冒号",
|
|
41
|
+
flow.parse_members("张敏:产品负责人, 李强:技术负责人\n王磊") ==
|
|
42
|
+
[("张敏", "产品负责人"), ("李强", "技术负责人"), ("王磊", "")],
|
|
43
|
+
flow.parse_members("张敏:产品负责人, 李强:技术负责人\n王磊"))
|
|
44
|
+
sug = flow.suggest_owners([("张敏", "产品负责人"), ("李强", "技术负责人"),
|
|
45
|
+
("王磊", "研发负责人"), ("赵倩", "QA负责人"), ("陈昊", "交付负责人")])
|
|
46
|
+
ck("角色推断铺满六阶段", sorted(sug) == ["N0", "N1", "N2", "N3", "N4", "N5"], sug)
|
|
47
|
+
ck("研发负责人 → N3", sug.get("N3") == "王磊", sug)
|
|
48
|
+
|
|
49
|
+
# ---- 2) 打桩 input 跑向导 ----
|
|
50
|
+
answers = iter(["1", # 现在配
|
|
51
|
+
"订单中心", # 项目名
|
|
52
|
+
"张敏", # 管理员
|
|
53
|
+
"张敏:产品负责人", "李强:技术负责人", "王磊:研发负责人",
|
|
54
|
+
"赵倩:QA负责人", "陈昊:交付负责人", "", # 名单 + 空行结束
|
|
55
|
+
""]) # 阶段归属:回车用建议值
|
|
56
|
+
asked = []
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def fake_input(prompt=""):
|
|
60
|
+
asked.append(prompt)
|
|
61
|
+
try:
|
|
62
|
+
return next(answers)
|
|
63
|
+
except StopIteration:
|
|
64
|
+
raise EOFError
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
builtins.input = fake_input
|
|
68
|
+
buf = io.StringIO()
|
|
69
|
+
with contextlib.redirect_stdout(buf):
|
|
70
|
+
text = flow.setup_team_wizard(os.path.join(ROOT, ".aidlc-engine"), state)
|
|
71
|
+
out = buf.getvalue()
|
|
72
|
+
print("--- 向导输出 ---")
|
|
73
|
+
print(out + "\n" + text)
|
|
74
|
+
|
|
75
|
+
team_p = os.path.join(os.path.dirname(state), "team.json")
|
|
76
|
+
team = json.load(open(team_p, encoding="utf-8")) if os.path.exists(team_p) else {}
|
|
77
|
+
owners = {n["id"]: n.get("owner") for n in team.get("nodes", [])}
|
|
78
|
+
ck("向导写出 team.json", os.path.exists(team_p))
|
|
79
|
+
ck("六阶段负责人齐备", all(owners.get("N%d" % i) for i in range(6)), owners)
|
|
80
|
+
ck("N3 = 王磊", owners.get("N3") == "王磊", owners)
|
|
81
|
+
ck("成员 5 人", len(team.get("members", [])) == 5, [m["name"] for m in team.get("members", [])])
|
|
82
|
+
ck("req_review 审批人随 N1 联动",
|
|
83
|
+
next((g["approvers"] for n in team["nodes"] if n["id"] == "N1" for g in n["gates"]
|
|
84
|
+
if g["type"] == "req_review"), None) == ["张敏"])
|
|
85
|
+
ck("code_review 未被自动同步(职责分离例外)",
|
|
86
|
+
next((g["approvers"] for n in team["nodes"] if n["id"] == "N3" for g in n["gates"]
|
|
87
|
+
if g["type"] == "code_review"), None) == [], "应为空并要求另行指定")
|
|
88
|
+
ck("向导给出了 code_review 的补救命令",
|
|
89
|
+
"code_review" in text and "set --gate code_review" in text, text[-300:])
|
|
90
|
+
ck("向导问了「是否现在配」", any("选择" in a for a in asked), asked[:2])
|
|
91
|
+
|
|
92
|
+
# ---- 3) 待办块 ----
|
|
93
|
+
blk = flow.next_steps_block(state, team_done=True, hooks_done=False)
|
|
94
|
+
ck("钩子未装 → [next] step=hooks", "step=hooks" in blk, blk)
|
|
95
|
+
ck("团队已配 → 不再提 step=team", "step=team" not in blk, blk)
|
|
96
|
+
# 用一份「还没有 team.json」的状态验证未配分支
|
|
97
|
+
fresh = os.path.join(ROOT, ".tmp-wz2", "aidlc-data", "workflow-state.json")
|
|
98
|
+
os.makedirs(os.path.dirname(fresh), exist_ok=True)
|
|
99
|
+
with open(fresh, "w", encoding="utf-8") as f:
|
|
100
|
+
json.dump({"meta": {"project": "p"}}, f, ensure_ascii=False)
|
|
101
|
+
blk2 = flow.next_steps_block(fresh, team_done=False, hooks_done=True)
|
|
102
|
+
ck("团队未配 → [next] step=team 且含一行命令",
|
|
103
|
+
"step=team" in blk2 and "--team" in blk2, blk2)
|
|
104
|
+
ck("已配置的仓库再跑 init 不重复唠叨",
|
|
105
|
+
"step=team" not in flow.next_steps_block(state, team_done=False, hooks_done=True))
|
|
106
|
+
|
|
107
|
+
# ---- 4) MCP 入参 ----
|
|
108
|
+
mcp = load("mcp_t", os.path.join(CLI, "flow_mcp.py"))
|
|
109
|
+
tool = next(t for t in mcp.TOOLS if t["name"] == "flow_init")
|
|
110
|
+
props = set(tool["inputSchema"]["properties"])
|
|
111
|
+
ck("MCP flow_init 增加 team/admin", {"team", "admin"} <= props, props)
|
|
112
|
+
ck("MCP flow_init description 要求主动问团队", "主动问" in tool["description"], "")
|
|
113
|
+
import inspect
|
|
114
|
+
src = inspect.getsource(mcp)
|
|
115
|
+
ck("MCP 侧强制 no_input=True(不会在返回前提问)",
|
|
116
|
+
"no_input=True" in src, "")
|
|
117
|
+
|
|
118
|
+
print("\n" + ("失败 %d 项" % len(fails) if fails else "INIT-TEAM TEST PASS"))
|
|
119
|
+
sys.exit(1 if fails else 0)
|