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.
Files changed (42) hide show
  1. package/.aidlc-engine/cli/flow.py +335 -6
  2. package/.aidlc-engine/cli/flow_mcp.py +20 -5
  3. package/.aidlc-engine/docs/design.md +2 -2
  4. package/.aidlc-engine/skills/team-init/SKILL.md +6 -1
  5. package/.tmp-init-team-test.py +119 -0
  6. package/.tmp-mcp-init-test.py +53 -0
  7. package/.trae/commands/flow-audit.md +13 -0
  8. package/.trae/commands/flow-branch.md +16 -0
  9. package/.trae/commands/flow-e2e.md +6 -0
  10. package/.trae/commands/flow-gate.md +28 -0
  11. package/.trae/commands/flow-matrix.md +6 -0
  12. package/.trae/commands/flow-status.md +6 -0
  13. package/.trae/commands/flow-transition.md +13 -0
  14. package/.trae/commands/flow-validate.md +8 -0
  15. package/.trae/hooks/gate_hook.py +334 -0
  16. package/.trae/hooks.json +60 -0
  17. package/.trae/mcp.json +11 -0
  18. package/.trae/rules/flow-orchestration.md +128 -0
  19. package/.trae/skills/audit-logger/SKILL.md +60 -0
  20. package/.trae/skills/audit-logger/scripts/append.py +107 -0
  21. package/.trae/skills/compliance-checker/SKILL.md +40 -0
  22. package/.trae/skills/compliance-checker/scripts/check.py +32 -0
  23. package/.trae/skills/design-doc-generator/SKILL.md +38 -0
  24. package/.trae/skills/gate-verifier/SKILL.md +61 -0
  25. package/.trae/skills/gate-verifier/scripts/verify.py +228 -0
  26. package/.trae/skills/observation-doc-generator/SKILL.md +36 -0
  27. package/.trae/skills/openspec-executor/SKILL.md +61 -0
  28. package/.trae/skills/release-doc-generator/SKILL.md +38 -0
  29. package/.trae/skills/render-views/SKILL.md +73 -0
  30. package/.trae/skills/req-doc-generator/SKILL.md +60 -0
  31. package/.trae/skills/req-doc-generator/scripts/create_req.py +222 -0
  32. package/.trae/skills/state-validator/SKILL.md +47 -0
  33. package/.trae/skills/state-validator/scripts/validate.py +312 -0
  34. package/.trae/skills/task-split/SKILL.md +114 -0
  35. package/.trae/skills/task-split/scripts/tasks.py +511 -0
  36. package/.trae/skills/team-init/SKILL.md +99 -0
  37. package/.trae/skills/team-init/scripts/init_owners.py +614 -0
  38. package/.trae/skills/test-manager/SKILL.md +38 -0
  39. package/.trae/skills/traceability-updater/SKILL.md +34 -0
  40. package/.trae/skills/traceability-updater/scripts/update.py +98 -0
  41. package/README.md +106 -152
  42. 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 合并、斜杠命令同步、11 个 Skill 复制到
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.loader.exec_module(mod)
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="生成 workflow-state.json 骨架 + 仓库目录脚手架 + Trae 接入件/技能安装")
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 → 回退旧布局 cwd/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
- return os.path.join(os.getcwd(), "workflow-state.json")
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 钩子(见下)。**维护模式**:`--git-hooks` / `--sync-skills` 在状态文件已存在时不再 `[FAIL]` 退出,而是打印 `[skip] … 不覆盖(维护模式)` 并只执行所请求的安装动作(补钩子 / 补 Skill),供已初始化仓库补装新 Skill | 无 |
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)