asia-aidlc 1.14.2 → 1.14.5

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 +71 -4
  2. package/CHANGELOG.md +124 -0
  3. package/README.md +29 -0
  4. package/bin/aidlc.js +2 -1
  5. package/lib/engine.js +29 -16
  6. package/lib/index.js +16 -5
  7. package/lib/runner.js +25 -1
  8. package/package.json +2 -4
  9. package/.trae/commands/flow-audit.md +0 -13
  10. package/.trae/commands/flow-branch.md +0 -16
  11. package/.trae/commands/flow-e2e.md +0 -6
  12. package/.trae/commands/flow-gate.md +0 -28
  13. package/.trae/commands/flow-matrix.md +0 -6
  14. package/.trae/commands/flow-status.md +0 -6
  15. package/.trae/commands/flow-transition.md +0 -13
  16. package/.trae/commands/flow-validate.md +0 -8
  17. package/.trae/hooks/gate_hook.py +0 -334
  18. package/.trae/hooks.json +0 -60
  19. package/.trae/mcp.json +0 -11
  20. package/.trae/rules/flow-orchestration.md +0 -128
  21. package/.trae/skills/audit-logger/SKILL.md +0 -60
  22. package/.trae/skills/audit-logger/scripts/append.py +0 -107
  23. package/.trae/skills/compliance-checker/SKILL.md +0 -40
  24. package/.trae/skills/compliance-checker/scripts/check.py +0 -32
  25. package/.trae/skills/design-doc-generator/SKILL.md +0 -38
  26. package/.trae/skills/gate-verifier/SKILL.md +0 -61
  27. package/.trae/skills/gate-verifier/scripts/verify.py +0 -228
  28. package/.trae/skills/observation-doc-generator/SKILL.md +0 -36
  29. package/.trae/skills/openspec-executor/SKILL.md +0 -61
  30. package/.trae/skills/release-doc-generator/SKILL.md +0 -38
  31. package/.trae/skills/render-views/SKILL.md +0 -73
  32. package/.trae/skills/req-doc-generator/SKILL.md +0 -60
  33. package/.trae/skills/req-doc-generator/scripts/create_req.py +0 -222
  34. package/.trae/skills/state-validator/SKILL.md +0 -47
  35. package/.trae/skills/state-validator/scripts/validate.py +0 -312
  36. package/.trae/skills/task-split/SKILL.md +0 -114
  37. package/.trae/skills/task-split/scripts/tasks.py +0 -511
  38. package/.trae/skills/team-init/SKILL.md +0 -99
  39. package/.trae/skills/team-init/scripts/init_owners.py +0 -614
  40. package/.trae/skills/test-manager/SKILL.md +0 -38
  41. package/.trae/skills/traceability-updater/SKILL.md +0 -34
  42. package/.trae/skills/traceability-updater/scripts/update.py +0 -98
@@ -426,6 +426,8 @@ ROLE_NODE_HINT = (
426
426
  )
427
427
  NODE_LABEL = (("N0", "任务受理"), ("N1", "需求生成"), ("N2", "方案生成"),
428
428
  ("N3", "代码生成"), ("N4", "测试执行"), ("N5", "交付发布"))
429
+ # 名单里的「角色」是自由文本,但给几个示例用户才知道该写什么(角色会用于自动推断阶段归属)
430
+ ROLE_EXAMPLES = ("产品负责人", "技术负责人", "研发负责人", "QA负责人", "交付负责人")
429
431
 
430
432
 
431
433
  def parse_members(text):
@@ -526,6 +528,10 @@ def setup_team_wizard(system, state):
526
528
  if admin is None:
527
529
  return NO_TTY_HINT
528
530
  print("团队名单(每行一条,格式 名字:角色;空行结束)")
531
+ print(" 角色示例:" + " / ".join(ROLE_EXAMPLES))
532
+ print(" 也可以写:架构师 / 项目经理 / 测试工程师 / 运维 —— 角色是自由文本,用于自动推断阶段归属")
533
+ print(" 只写名字也行(用这条命令会按名字建成员、阶段归属再单独指定)")
534
+ print(" 例:张敏:产品负责人 / 李强:技术负责人")
529
535
  roster = []
530
536
  while True:
531
537
  try:
@@ -610,6 +616,8 @@ def next_steps_block(state, team_done=False, hooks_done=False):
610
616
  lines += [
611
617
  "[next] step=team 状态=未配置 影响=门控审批人校验/视图「配置负责人」列/日历按人筛选",
612
618
  "[next] step=team 一条命令配好=flow init --team \"张敏:产品负责人,李强:技术负责人\" --admin 张敏",
619
+ "[next] step=team 角色示例=" + " / ".join(ROLE_EXAMPLES) +
620
+ "(角色是自由文本,用于自动推断阶段归属)",
613
621
  "[next] step=team 跳过=什么都不做即可(不影响 init 结果;随时可再配)",
614
622
  ]
615
623
  if not hooks_done:
@@ -626,11 +634,16 @@ def do_init(system, state, skills_dir=".trae/skills", no_skills=False, empty=Fal
626
634
  # 钩子装到状态文件所在仓库(脚本自身会 git rev-parse 定位仓库根,传目录即可)
627
635
  hook_start = os.path.dirname(os.path.abspath(state)) or os.getcwd()
628
636
  if os.path.exists(state):
629
- # 维护模式:已初始化的仓库不能重建状态,但「补装缺失的 Skill / 钩子」应当仍可执行
637
+ # 维护模式:已初始化的仓库不能重建状态,但「补装缺失的 Skill / 钩子 / 刷新引擎」应当仍可执行
630
638
  if git_hooks or sync_skills:
631
639
  out = "[skip] %s 已存在,不覆盖(维护模式:只执行所请求的安装动作)" % state
640
+ repo_root = _repo_root_from_state(state)
641
+ n_files, eng_dest = _materialize_engine(system, repo_root)
642
+ if n_files is not None:
643
+ system = eng_dest # 后续步骤改用项目内那份引擎
644
+ out += "\nOK: 引擎已落地 → %s(新写/更新 %d 个文件)" % (eng_dest, n_files)
632
645
  if sync_skills:
633
- out += "\nOK: " + _install_skills(system, _repo_root_from_state(state), skills_dir)
646
+ out += "\nOK: " + _install_skills(system, repo_root, skills_dir)
634
647
  if git_hooks:
635
648
  out += "\nOK: " + install_viewer_hooks(hook_start)
636
649
  return out
@@ -654,6 +667,16 @@ def do_init(system, state, skills_dir=".trae/skills", no_skills=False, empty=Fal
654
667
  write_state(state, data)
655
668
  data_root = os.path.dirname(os.path.abspath(state)) or os.getcwd()
656
669
  repo_root = os.path.dirname(data_root) or data_root # .trae/ 等项目配置仍装在仓库根
670
+ # npm 形态:先把引擎落地到项目内,**后续所有步骤都改用项目内那份**——
671
+ # 这样 .trae/mcp.json 的 ${workspaceFolder}/.aidlc-engine/... 与 git 钩子的路径才是真的。
672
+ n_files, eng_dest = _materialize_engine(system, repo_root)
673
+ if n_files is not None:
674
+ system = eng_dest
675
+ engine_note = "OK: 引擎已落地 → %s(新写/更新 %d 个文件)%s" % (
676
+ eng_dest, n_files,
677
+ ";之后 Trae MCP 与 git 钩子都指向项目内这份引擎" if n_files else "(已是最新)")
678
+ else:
679
+ engine_note = "OK: 引擎:%s" % eng_dest
657
680
  made = _scaffold(system, state)
658
681
  trae = _install_trae(system, repo_root)
659
682
  rule = _install_agent_rule(system, repo_root)
@@ -668,10 +691,10 @@ def do_init(system, state, skills_dir=".trae/skills", no_skills=False, empty=Fal
668
691
  dep = "提示: 未安装 jsonschema,validate 将降级运行(pip install jsonschema 启用完整 Schema 校验)"
669
692
  prefix = "(空白,无示例实体)" if empty else ""
670
693
  out = ("OK: 已从 example 生成 %s%s(%d 需求 / %d EPIC / %d WU / %d CHG / %d REL)\n"
671
- "OK: %s\nOK: Trae 接入: %s\nOK: %s\nOK: %s" % (
694
+ "%s\nOK: %s\nOK: Trae 接入: %s\nOK: %s\nOK: %s" % (
672
695
  state, prefix, len(data.get("requirements", [])), len(data.get("epics", [])),
673
696
  len(data.get("workunits", [])), len(data.get("changes", [])), len(data.get("releases", [])),
674
- made, trae, rule, sk))
697
+ engine_note, made, trae, rule, sk))
675
698
  if git_hooks:
676
699
  out += "\nOK: " + install_viewer_hooks(hook_start)
677
700
  if dep:
@@ -730,6 +753,50 @@ def do_init(system, state, skills_dir=".trae/skills", no_skills=False, empty=Fal
730
753
  return out
731
754
 
732
755
 
756
+ def _materialize_engine(system, repo_root):
757
+ """把「包里的引擎」落地到项目内 `.aidlc-engine/`(npm 安装形态专用,仓库内形态是 no-op)。
758
+
759
+ 为什么必须做:npm 形态下引擎只存在于 `node_modules/asia-aidlc/.aidlc-engine/`,
760
+ 而项目侧的接入件全都按**项目内**路径引用它——
761
+ · `.trae/mcp.json` → `${workspaceFolder}/.aidlc-engine/cli/flow_mcp.py`
762
+ · git 钩子 → `python .aidlc-engine/viewer/render.py --if-stale`
763
+ · `.trae/rules/flow-orchestration.md` → `.aidlc-engine/cli/flow.py`
764
+ 不落地的话,这三样在项目里全是**死路径**(MCP 起不来、钩子空跑)。
765
+
766
+ 幂等:逐文件比较,只写缺失或内容不同的;跳过 `__pycache__` / `*.pyc` / `.tmp*`。
767
+ 返回 (落地文件数, 目标路径) 或 (None, 原因)。
768
+ """
769
+ src = (os.environ.get("AIDLC_ENGINE_SRC") or "").strip()
770
+ if not src:
771
+ return None, "(仓库内形态:引擎已在项目里,无需落地)"
772
+ if not os.path.isdir(src):
773
+ return None, "找不到包内引擎 %s" % src
774
+ dest = os.path.join(repo_root, ".aidlc-engine")
775
+ if os.path.abspath(src) == os.path.abspath(dest):
776
+ return None, "(引擎与项目同一目录,无需落地)"
777
+ written = skipped = 0
778
+ for root, dirs, files in os.walk(src):
779
+ dirs[:] = [d for d in dirs if d != "__pycache__" and not d.startswith(".tmp")]
780
+ rel = os.path.relpath(root, src)
781
+ target_dir = dest if rel == "." else os.path.join(dest, rel)
782
+ os.makedirs(target_dir, exist_ok=True)
783
+ for name in files:
784
+ if name.endswith((".pyc", ".pyo")) or name.startswith(".tmp"):
785
+ continue
786
+ s, d = os.path.join(root, name), os.path.join(target_dir, name)
787
+ try:
788
+ if os.path.exists(d) and os.path.getsize(d) == os.path.getsize(s):
789
+ with open(s, "rb") as fa, open(d, "rb") as fb:
790
+ if fa.read() == fb.read():
791
+ skipped += 1
792
+ continue
793
+ shutil.copy2(s, d)
794
+ written += 1
795
+ except OSError as exc:
796
+ LOG.warning("引擎落地失败 %s:%s", rel, exc)
797
+ return written, dest
798
+
799
+
733
800
  def _scaffold(system, state):
734
801
  """按目录规约创建仓库骨架:aidlc-data/ 产物目录 + 登记册 + 仓库根 .gitignore 合并。全部幂等,已存在即跳过。"""
735
802
  data_root = os.path.dirname(os.path.abspath(state)) or os.getcwd()
package/CHANGELOG.md CHANGED
@@ -5,6 +5,130 @@
5
5
 
6
6
  ---
7
7
 
8
+ ## v1.25 · 2026-10-09 — npm 形态 init 只落 `aidlc-data/`:引擎与 `.trae/` 现在真的落地
9
+
10
+ ### 背景(用户报障)
11
+
12
+ > 「从 npm 上安装完这个包之后进行初始化,只会生成 `aidlc-data` 这个文件夹,而 `.aidlc-engine`、`.trae` 没有生成,这几个也很重要」
13
+
14
+ 用户是对的,而且这是**同一个根因**的两个症状。
15
+
16
+ ### 根因(实测量化)
17
+
18
+ npm 形态的挂载表只有两个子目录,**项目根本身没挂**:
19
+
20
+ ```
21
+ mounts: /project/.aidlc-engine ← 包里的引擎(NODEFS)
22
+ /project/aidlc-data ← 项目数据(NODEFS,mustExist:false 时创建)
23
+ /project 本身 = MEMFS ← 内存盘!
24
+ ```
25
+
26
+ 于是 `flow init` 打印了 `OK: Trae 接入: … → .trae/`「技能 14 个 → .trae/skills」,
27
+ **写入全落在内存盘 `/project/.trae`,进程退出即丢**;复现脚本在宿主上只看到 `aidlc-data/`,
28
+ `.trae` 与 `.aidlc-engine` 均为 false。而 `.aidlc-engine/` 属于"引擎来自包"的既有设计,**从未被复制**。
29
+
30
+ 致命连锁:即使 `.trae/` 落了,里面的路径也全是死的——
31
+ `.trae/mcp.json` → `${workspaceFolder}/.aidlc-engine/cli/flow_mcp.py`(不存在)、
32
+ git 钩子 → `python .aidlc-engine/viewer/render.py --if-stale`(不存在)、
33
+ `.trae/rules/flow-orchestration.md` → `.aidlc-engine/cli/flow.py`(不存在)。
34
+
35
+ ### 变更明细
36
+
37
+ 1. **`lib/engine.js` 挂载表重做**:npm 形态改为
38
+ `{host: <项目根>, guest: "/project"}`(NODEFS,写入直达宿主)+
39
+ `{host: <包内引擎>, guest: "/engine-src"}`(`ENGINE_SRC_GUEST`),
40
+ `resolveMounts` 返回值新增 `engineSrc`。
41
+ 2. **`lib/index.js`**:新增 `guestPath` 二选一规则——项目内**已有** `.aidlc-engine/cli/flow.py`
42
+ 就用 `/project/.aidlc-engine/…`(与 Trae 钩子/MCP 同一份引擎,避免版本错位),
43
+ 否则用 `/engine-src/…`(首次 init 前必须能跑起来);并通过环境变量 `AIDLC_ENGINE_SRC` 告知 Python 侧。
44
+ 3. **`flow.py` 新增 `_materialize_engine()`**:把 `/engine-src` 逐文件复制到 `<项目>/.aidlc-engine/`,
45
+ 跳过 `__pycache__`/`*.pyc`/`.tmp*`,**幂等**(同尺寸先比内容,只写有变化的);
46
+ init 之后 `system` 切到项目内那份,于是 `.trae/mcp.json`、git 钩子、`.trae/skills` 的路径全部指向项目内引擎。
47
+ **维护模式也刷新引擎**(`flow init --sync-skills` 可用于 `npm update` 后同步)。
48
+ 4. **`lib/runner.js` 修一个既有 bug**:`ensureMounts` 无条件重复 `FS.mount`,
49
+ 同一 Node 进程里**第二次** `runScript` 会抛 `ErrnoError`(我的验证脚本是第一个连调两次的用例)。
50
+ 改为记录每个 guest 当前挂的宿主:同宿主跳过,不同宿主先 `unmount` 再挂(支持一个进程里先后操作不同项目)。
51
+ 5. **`bin/aidlc.js`**:`doctor` 增加一行「项目内引擎 … OK / 未落地(先 aidlc flow init)」;
52
+ `项目根` 那行措辞改为「npm 形态:引擎来自包,`flow init` 会落地到项目内」。
53
+ 6. **`README.md` 新增「从 npm 安装到自己的项目」**:三样落地物各是什么、为什么 `.aidlc-engine/` 必须落地、
54
+ 升级后用 `--sync-skills` 刷新,以及上面那段"实现要点(改引擎挂载时必看)"。此前 README **完全没写** npm 形态,
55
+ 这正是这个坑长期没被发现的原因。
56
+ 7. **`lib/engine.js` 头注释**同步(原文还写着"用 MEMFS 造一个合成项目根")。
57
+
58
+ ### 验证(合成根形态,实跑)
59
+
60
+ 7 组断言全过:
61
+ - ① `init --empty --no-input --git-hooks` 退出码 0;
62
+ - ② 宿主上 **`.aidlc-engine/`(91 个文件)+ `.trae/` + `aidlc-data/`** 三者全部存在;
63
+ - ③ `.trae/mcp.json` 的 `args` 解出来是**真实存在**的文件;
64
+ - ④ git 钩子内容是 `.aidlc-engine/viewer/render.py`,且**不泄漏** `/engine-src`;
65
+ - ⑤ 后续 `flow status` 改用项目内引擎(不再报 ErrnoError);
66
+ - ⑥ `flow init --sync-skills` 幂等刷新(第二次「新写/更新 **0** 个文件」);
67
+ - ⑦ `flow view` 用项目内 `render.py` 产出日历。
68
+
69
+ 回归:`npm test` SMOKE PASS;仓库内形态(CPython)`flow init` 仍写 `.trae/` + `aidlc-data/`,
70
+ 且**不会**多建一份 `.aidlc-engine/`(正确 no-op,输出「(仓库内形态:引擎已在项目里,无需落地)」)。
71
+
72
+ ### 说明
73
+
74
+ - 落地后项目就是标准两目录布局,`git status` 会看到 `.aidlc-engine/` 与 `.trae/` 作为新增内容——这是**预期**的,
75
+ 它们是项目侧接入件,应当提交(派生视图仍在 `.gitignore` 里)。
76
+ - Trae 的 MCP server 仍是 `python <项目>/.aidlc-engine/cli/flow_mcp.py`,**需要本机有 Python**;
77
+ `aidlc …` 命令本身走内置 Pyodide、不需要(这是既有设计,本次未改)。
78
+
79
+ ---
80
+
81
+ ## v1.24 · 2026-10-09 — 团队名单给出角色示例 + 修 `.npmignore` 夹带仓库自身 `.trae/`
82
+
83
+ ### 背景(用户报障)
84
+
85
+ 1. **团队初始化时没有角色示例**:「后面都不知道要写什么角色」——向导只写了「格式 名字:角色」,
86
+ 没告诉用户角色长什么样。
87
+ 2. **「初始化之后原本的 `.trae` 文件夹没了,这个很重要」**——查清了机制,见下。
88
+
89
+ ### 变更明细
90
+
91
+ - **团队名单提示补角色示例**(向导 + `[next]` 块 + 常量 `ROLE_EXAMPLES`):
92
+ ```
93
+ 团队名单(每行一条,格式 名字:角色;空行结束)
94
+ 角色示例:产品负责人 / 技术负责人 / 研发负责人 / QA负责人 / 交付负责人
95
+ 也可以写:架构师 / 项目经理 / 测试工程师 / 运维 —— 角色是自由文本,用于自动推断阶段归属
96
+ 只写名字也行(按名字建成员、阶段归属再单独指定)
97
+ 例:张敏:产品负责人 / 李强:技术负责人
98
+ ```
99
+ Agent 路径的 `[next] step=team 角色示例=…` 同样带上,避免 Agent 在对话里也不知道该引导用户写什么。
100
+
101
+ - **`.npmignore` 修两处夹带**:
102
+ - **`/.trae/`**:仓库自己的 Trae 接入件(`.trae/` 是 `flow init` 装到**项目侧**的派生件)被打进了包。
103
+ 实测 **1.14.0 顶层 `.trae` 条目 = 0,1.14.1/1.14.2 变成 34**(因为仓库里出现 `.trae/` 后它不再被排除)。
104
+ 修后 `npm pack` **entryCount 87**、无顶层 `.trae`。
105
+ - **`/.tmp*`(不带斜杠)**:原来的 `/.tmp*/` 只匹配**目录**,临时**文件**(如 `.tmp-xxx.js`)仍会被打进包——
106
+ 实测修复前的包里就有这个文件。
107
+
108
+ ### 关于「`.trae` 没了」的排查结论(未改策略,待定)
109
+
110
+ - **`flow init` 不会删 `.trae`**:实测预置 4 个哨兵文件(`mcp.json` + 自有 command/rule/skill)后跑 init,
111
+ **哨兵全在**,`mcp.json` 是**合并**(用户 server + flow server 都在),总条目由少变多。
112
+ - **真正机制**:本仓库 `.gitignore` **第 34 行就是 `.trae`**(落在「IDE / 编辑器」区块,`.idea/`、`.vscode/` 旁边)
113
+ → `.trae/` **不被 git 跟踪** → `git clean -xfd`、**新克隆**、部分 checkout 都会**静默删掉它**。
114
+ - **文件自相矛盾**:同一个 `.gitignore` 第 69 行注释却写着「明确保留(不忽略)→ `.trae/` Trae 接入件」。
115
+ 两者必有一错。
116
+ - **用户项目不受影响** ✓:`templates/gitignore.example`(`flow init` 写给用户项目的那份)与
117
+ `GITIGNORE_ENTRIES` **都不含 `.trae`**,所以只有本框架仓库有这个坑。
118
+ - 策略二选一(需拍板):**(A) 取消忽略**,让 `.trae/` 可提交、可随克隆恢复、跨机器共享(代价:14 个 Skill
119
+ 是 `.aidlc-engine/skills/` 的副本,提交会重复且可能漂移);**(B) 保持忽略**(承认它是派生件),
120
+ 同时把第 69 行那条矛盾的注释改对,并在 init 输出里提示「`.trae/` 是派生件,被 clean/新克隆清掉后重跑本命令即可恢复」。
121
+ 倾向 (B)——它本就是 `flow init` 从 `.aidlc-engine/cli/.trae/` + skills 装出来的派生件。
122
+
123
+ ### 验证
124
+
125
+ - 向导输出含全部 5 个角色示例 + 「自由文本 / 可只写名字」说明(7 项断言全过);`[next]` 块含 `角色示例=`。
126
+ - `npm pack --json`:`entryCount: 87`,无 `package/.trae/**`、无 `.tmp*`;`.aidlc-engine/cli/.trae/**` 10 个条目仍在
127
+ (它是**源模板**,必须随包分发,否则装完无法生成 `.trae`)。
128
+ - 预置 `.trae` 后跑 init:哨兵 4/4 存活、`mcp.json` 合并保留用户 server。
129
+
130
+ ---
131
+
8
132
  ## v1.23 · 2026-10-09 — `flow init` 收尾主动询问是否配团队(并修掉 npm 形态下的"假提示")
9
133
 
10
134
  ### 背景
package/README.md CHANGED
@@ -124,6 +124,35 @@ python .aidlc-engine/cli/flow.py code verify --chg CHG-102 # 代
124
124
  3. 编排 Agent 提示词:Trae 环境由 init 自动写入 `.trae/rules/flow-orchestration.md`(alwaysApply);其他载体手工加载 `agent/orchestration-agent-system-prompt.md`。
125
125
  4. 按 `README.md` 的"落地三步"接入第一个真实需求,跑通一条完整链路。
126
126
 
127
+ ### 从 npm 安装到自己的项目
128
+
129
+ ```bash
130
+ npm i -D asia-aidlc # 或全局:npm i -g asia-aidlc(之后可直接 aidlc …,无需 npx)
131
+ npx aidlc flow init # 在你自己的项目根执行
132
+ ```
133
+
134
+ `flow init` 会把三样东西**落到你的项目里**(不是只留在 `node_modules/`):
135
+
136
+ | 落地物 | 作用 |
137
+ |---|---|
138
+ | `.aidlc-engine/` | 引擎(从包里复制过来)。**必须落地的原因**:`.trae/mcp.json` 指向 `${workspaceFolder}/.aidlc-engine/cli/flow_mcp.py`、git 钩子跑 `.aidlc-engine/viewer/render.py`——项目里没有引擎,这两条都是死路径(MCP 起不来、钩子空跑) |
139
+ | `.trae/` | Trae 接入件:MCP server、8 个斜杠命令、14 个技能、编排规则(`rules/flow-orchestration.md`) |
140
+ | `aidlc-data/` | 流程数据(状态真相 + 需求/设计/测试/发布产物) |
141
+
142
+ 落完之后项目就是标准的**两目录布局**,`.trae/` 与 `.aidlc-engine/` 都该进版本控制
143
+ (`flow init` 写入的 `.gitignore` 只忽略派生视图 `aidlc-data/*.html`、`aidlc-data/req/` 等,不忽略它们)。
144
+
145
+ 升级包之后刷新项目内引擎(幂等:只写内容有变化的文件):
146
+
147
+ ```bash
148
+ npx aidlc flow init --sync-skills # 维护模式:不覆盖状态,只刷新引擎 + 补装缺失 Skill
149
+ ```
150
+
151
+ > 实现要点(改引擎挂载时必看):npm 形态下**项目根本身**被挂到虚拟 FS 的 `/project`(NODEFS,写入直达宿主),
152
+ > 包里的引擎另挂到 `/engine-src`;`flow init` 把 `/engine-src` 落地成 `<项目>/.aidlc-engine/`,
153
+ > 之后所有命令改用项目内那份。早期只挂了 `aidlc-data` 与包内引擎两个子目录,`/project` 是内存盘,
154
+ > 于是 `.trae/` 写进了内存、进程退出即丢——**宿主上只剩 `aidlc-data/`**。
155
+
127
156
  ## 验证清单(用户后续验收用)
128
157
 
129
158
  - [ ] `state-validator` 对 example.json 返回 EXIT=0
package/bin/aidlc.js CHANGED
@@ -109,8 +109,9 @@ async function cmdDoctor() {
109
109
  const lines = [];
110
110
  lines.push(`Node ${process.version}`);
111
111
  lines.push(`包根 ${st.packageRoot}`);
112
- lines.push(`项目根 ${st.projectRoot}${st.synthetic ? "(合成根:引擎来自包,数据来自项目)" : "(仓库内形态)"}`);
112
+ lines.push(`项目根 ${st.projectRoot}${st.synthetic ? "(npm 形态:引擎来自包,flow init 会落地到项目内)" : "(仓库内形态)"}`);
113
113
  lines.push(`引擎 ${st.engineHost} ${fs.existsSync(st.engineHost) ? "OK" : "缺失"}`);
114
+ lines.push(`项目内引擎 ${path.join(st.projectRoot, ".aidlc-engine")} ${fs.existsSync(path.join(st.projectRoot, ".aidlc-engine", "cli", "flow.py")) ? "OK" : "未落地(先 aidlc flow init)"}`);
114
115
  lines.push(`数据目录 ${path.join(st.projectRoot, "aidlc-data")} ${st.hasData ? "OK" : "不存在(先 aidlc flow init)"}`);
115
116
  let pyVer = "未加载";
116
117
  try {
package/lib/engine.js CHANGED
@@ -2,10 +2,13 @@
2
2
  * engine.js —— 引擎侧定位:包根、项目根、Skill 清单、Pyodide 可用性。
3
3
  *
4
4
  * 两种部署形态(同一套代码):
5
- * A. 仓库内(本仓库就是项目):包根 === 项目根,直接整目录挂到 /project。
6
- * B. npm 安装(node_modules/asia-aidlc):项目根在用户目录,
7
- * 用 MEMFS 造一个「合成项目根」/project:/.aidlc-engine 挂包里的引擎,/aidlc-data 挂用户数据。
8
- * 这样脚本里所有基于 __file__ 的路径解析都落到 /project,**无需改一行 Python**。
5
+ * A. 仓库内(本仓库就是项目):包根 === 项目根,整目录挂到 /project。
6
+ * B. npm 安装(node_modules/asia-aidlc):项目根本身挂到 /project(NODEFS,写入直达宿主),
7
+ * 包里的引擎另挂到 /engine-src;`flow init` 会把 /engine-src **落地**成项目内的
8
+ * `.aidlc-engine/`,之后再跑命令就改用项目内那份(环境变量 AIDLC_ENGINE_SRC 由 lib/index.js 注入)。
9
+ *
10
+ * 于是两种形态最终都是同一个「两目录布局」(`<项目>/.aidlc-engine/` + `<项目>/aidlc-data/`),
11
+ * 脚本里所有基于 __file__ 的路径解析都落到 /project,**无需改一行 Python**。
9
12
  */
10
13
  const fs = require("fs");
11
14
  const path = require("path");
@@ -92,14 +95,22 @@ const NEEDS_SUBPROCESS = [
92
95
  "cli_flow.py",
93
96
  ];
94
97
 
98
+ /** npm 形态下「包里的引擎」挂载点:`flow init` 从这里复制到项目内的 .aidlc-engine/。 */
99
+ const ENGINE_SRC_GUEST = "/engine-src";
100
+
95
101
  function hasSubprocessDependency(relPath) {
96
102
  const p = String(relPath).split(path.sep).join("/");
97
103
  return NEEDS_SUBPROCESS.some((x) => p.endsWith(x));
98
104
  }
99
105
 
100
106
  /**
101
- * 计算挂载表。返回 { synthetic, mounts:[{host, guest, mustExist}], cwd, projectRoot }。
102
- * synthetic=true 表示 /project 是 MEMFS 合成根(npm 安装形态)。
107
+ * 计算挂载表。返回 { synthetic, mounts:[{host, guest, mustExist}], cwd, projectRoot, engineSrc }。
108
+ * synthetic=true 表示 npm 安装形态(引擎来自包,需要 `flow init` 落地到项目内)。
109
+ *
110
+ * ⚠️ 关键:npm 形态下**必须把项目根本身**挂到 /project。
111
+ * 早期只挂了 `/project/.aidlc-engine`(包里的引擎)与 `/project/aidlc-data` 两个子目录,
112
+ * `/project` 本身是 MEMFS 内存盘 —— 于是 `flow init` 写出的 `.trae/`(以及任何非 aidlc-data 的路径)
113
+ * 都写进了内存、进程退出即丢:实测宿主上只留下 `aidlc-data/`,`.trae` 和 `.aidlc-engine` 都不存在。
103
114
  */
104
115
  function resolveMounts(packageRoot, projectRoot) {
105
116
  const engineHost = path.join(packageRoot, ENGINE_DIR);
@@ -109,23 +120,25 @@ function resolveMounts(packageRoot, projectRoot) {
109
120
  synthetic: false,
110
121
  projectRoot,
111
122
  cwd: "/project",
123
+ engineSrc: null, // 仓库内形态:引擎就在 /project/.aidlc-engine
112
124
  mounts: [{ host: projectRoot, guest: "/project", mustExist: true }],
113
125
  };
114
126
  }
115
- const mounts = [{ host: engineHost, guest: `/project/${ENGINE_DIR}`, mustExist: true }];
116
- for (const sub of PROJECT_SUBDIRS) {
117
- const host = path.join(projectRoot, sub);
118
- if (fs.existsSync(host)) {
119
- mounts.push({ host, guest: `/project/${sub}`, mustExist: true });
120
- } else if (sub === "aidlc-data") {
121
- mounts.push({ host, guest: `/project/${sub}`, mustExist: false }); // 需要时在宿主建出来
122
- }
123
- }
124
- return { synthetic: true, projectRoot, cwd: "/project", mounts };
127
+ return {
128
+ synthetic: true,
129
+ projectRoot,
130
+ cwd: "/project",
131
+ engineSrc: ENGINE_SRC_GUEST, // 包里的引擎:供 `flow init` 复制到项目内
132
+ mounts: [
133
+ { host: projectRoot, guest: "/project", mustExist: true },
134
+ { host: engineHost, guest: ENGINE_SRC_GUEST, mustExist: true },
135
+ ],
136
+ };
125
137
  }
126
138
 
127
139
  module.exports = {
128
140
  ENGINE_DIR,
141
+ ENGINE_SRC_GUEST,
129
142
  PROJECT_SUBDIRS,
130
143
  findPackageRoot,
131
144
  findProjectRoot,
package/lib/index.js CHANGED
@@ -5,6 +5,7 @@
5
5
  * const r = await runSkill("team-init", ["init", "--project", "订单中心", "--admin", "张敏"]);
6
6
  * console.log(r.code, r.stdout);
7
7
  */
8
+ const fs = require("fs");
8
9
  const path = require("path");
9
10
  const eng = require("./engine");
10
11
  const { runPython } = require("./runner");
@@ -12,14 +13,24 @@ const { runPython } = require("./runner");
12
13
  function projectContext(opts = {}) {
13
14
  const packageRoot = opts.packageRoot || eng.findPackageRoot(__dirname);
14
15
  const projectRoot = opts.projectRoot || eng.findProjectRoot(opts.cwd || process.cwd());
15
- const { synthetic, mounts, cwd } = eng.resolveMounts(packageRoot, projectRoot);
16
- return { packageRoot, projectRoot, synthetic, mounts, cwd };
16
+ const { synthetic, mounts, cwd, engineSrc } = eng.resolveMounts(packageRoot, projectRoot);
17
+ return { packageRoot, projectRoot, synthetic, mounts, cwd, engineSrc };
17
18
  }
18
19
 
19
- /** 把引擎相对路径(如 .aidlc-engine/skills/x/scripts/y.py)映射为虚拟 FS 路径。 */
20
+ /**
21
+ * 把引擎相对路径(如 .aidlc-engine/skills/x/scripts/y.py)映射为虚拟 FS 路径。
22
+ *
23
+ * npm 形态下项目根与包引擎是**两个挂载点**,要分两种情形:
24
+ * · 项目内**已有**引擎(`flow init` 落地之后)→ 用 `/project/.aidlc-engine/…`,
25
+ * 与项目里 Trae 钩子/MCP 用同一份引擎,避免版本错位;
26
+ * · 项目内**还没有**引擎(首次 init 之前)→ 用包里的 `/engine-src/…`,否则连 init 都跑不起来。
27
+ */
20
28
  function guestPath(ctx, relPath) {
21
29
  const rel = String(relPath).split(path.sep).join("/").replace(/^\.\//, "");
22
- if (ctx.synthetic) return "/project/" + rel.replace(/^\.aidlc-engine\//, ".aidlc-engine/");
30
+ if (ctx.synthetic && ctx.engineSrc && rel.startsWith(eng.ENGINE_DIR + "/")) {
31
+ const inProject = fs.existsSync(path.join(ctx.projectRoot, eng.ENGINE_DIR, "cli", "flow.py"));
32
+ if (!inProject) return ctx.engineSrc + "/" + rel.slice(eng.ENGINE_DIR.length + 1);
33
+ }
23
34
  return "/project/" + rel;
24
35
  }
25
36
 
@@ -34,7 +45,7 @@ async function runScript(relPath, args = [], opts = {}) {
34
45
  args,
35
46
  mounts: ctx.mounts,
36
47
  cwd: ctx.cwd,
37
- env: { AIDLC_NPM: "1", ...(opts.env || {}) },
48
+ env: { AIDLC_NPM: "1", AIDLC_ENGINE_SRC: ctx.engineSrc || "", ...(opts.env || {}) },
38
49
  onStdout: opts.onStdout,
39
50
  onStderr: opts.onStderr,
40
51
  indexURL: opts.indexURL,
package/lib/runner.js CHANGED
@@ -61,17 +61,41 @@ if "subprocess" not in sys.modules:
61
61
  sys.modules["subprocess"] = _m
62
62
  `;
63
63
 
64
- /** 把宿主目录挂进虚拟 FS(NODEFS 直通)。挂载点若在 MEMFS 里则就地建目录,不碰宿主磁盘。 */
64
+ /**
65
+ * 把宿主目录挂进虚拟 FS(NODEFS 直通)。挂载点若在 MEMFS 里则就地建目录,不碰宿主磁盘。
66
+ *
67
+ * 同一 runtime 会被多次复用(同一个 Node 进程里连调两次 runScript),而 Emscripten 对
68
+ * **已挂载**的挂载点再 mount 会抛 ErrnoError —— 所以这里记住每个 guest 当前挂的是哪个宿主:
69
+ * · 同一个宿主 → 跳过(这是同进程多命令的常态)
70
+ * · 不同宿主 → 先 unmount 再挂(允许一个进程里先后操作不同项目)
71
+ */
72
+ const mountedHosts = new WeakMap(); // py → { guest: host }
73
+
65
74
  function ensureMounts(py, mounts) {
75
+ let state = mountedHosts.get(py);
76
+ if (!state) {
77
+ state = {};
78
+ mountedHosts.set(py, state);
79
+ }
66
80
  for (const m of mounts) {
67
81
  if (m.mustExist !== false && !fs.existsSync(m.host)) {
68
82
  throw new Error(`挂载源不存在:${m.host}`);
69
83
  }
84
+ if (state[m.guest] === m.host) continue;
85
+ if (state[m.guest]) {
86
+ try {
87
+ py.FS.unmount(m.guest);
88
+ } catch (e) {
89
+ /* 缓存与实际不一致时忽略,下面照常挂 */
90
+ }
91
+ delete state[m.guest];
92
+ }
70
93
  const parent = path.posix.dirname(m.guest);
71
94
  if (parent !== "/" && !py.FS.analyzePath(parent).exists) py.FS.mkdirTree(parent);
72
95
  if (!py.FS.analyzePath(m.guest).exists) py.FS.mkdir(m.guest);
73
96
  if (m.mustExist === false && !fs.existsSync(m.host)) fs.mkdirSync(m.host, { recursive: true });
74
97
  py.FS.mount(py.FS.filesystems.NODEFS, { root: m.host }, m.guest);
98
+ state[m.guest] = m.host;
75
99
  }
76
100
  }
77
101
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "asia-aidlc",
3
- "version": "1.14.2",
3
+ "version": "1.14.5",
4
4
  "description": "OpenSpec + AI-DLC 研发流程能力包:14 个 Skill(SKILL.md + 确定性 Python 脚本)、flow CLI、状态机真相源、研发日历 / 交付控制台 / 追溯图谱三个静态视图。内置 Pyodide 运行时,装完即可执行 Python 脚本,无需本机安装 Python。",
5
5
  "keywords": [
6
6
  "aidlc",
@@ -30,8 +30,6 @@
30
30
  "test:viewer": "node test/viewer-js.js"
31
31
  },
32
32
  "dependencies": {
33
- "jsonschema": "^1.5.0",
34
- "pyodide": "^314.0.7",
35
- "validate": "^5.2.0"
33
+ "pyodide": "^314.0.7"
36
34
  }
37
35
  }
@@ -1,13 +0,0 @@
1
- ---
2
- name: flow-audit
3
- description: 追加审计日志(append-only,action 必须属于封闭事件注册表)
4
- argument-hint: --actor <执行者> --action <注册表事件> --entity <实体> --result ok|blocked|failed
5
- ---
6
- 追加一条审计日志。参数:--actor(人或Agent)、--action、--entity(如 REQ-100)、--result(ok/blocked/failed)、--detail(可选)。
7
- 若已配置 flow MCP,调用 mcp__flow__flow_audit 工具;否则运行 `python .aidlc-engine/cli/flow.py audit --actor ... --action ... --entity ... --result ...`。
8
-
9
- 硬性规则:
10
- - action 必须属于 `.aidlc-engine/state/audit-events.json` 封闭注册表(25 事件,含 emitter 归属);未注册事件会被类型化拒绝([GUARD_EVENT_NOT_REGISTERED]),禁止先使用再补注册。
11
- - 只增不删,seq 严格连续递增;禁止改历史条目。
12
- - 审计先行:状态变更的审计事件必须先于(或与状态变更同一次原子写中)写入——状态迁移/门控请优先用 `flow transition` / `flow gate --record`,它们已内置审计先行。
13
- - 新增事件类型:先改 audit-events.json(desc + emitter)+ .aidlc-engine/templates/audit-log.md,再使用。
@@ -1,16 +0,0 @@
1
- ---
2
- name: flow-branch
3
- description: 执行分支协作——建/查/合并/丢弃 Change 执行分支(feature/chg-<NNN>)
4
- ---
5
- 管理 Change 的 git 执行分支(分支归属制,CHG 实施强制前置):
6
-
7
- ```
8
- python .aidlc-engine/cli/flow.py branch create --chg CHG-xxx # 建执行分支(前置:CHG=proposed、WU gate 已过、当前在 main、工作区干净)
9
- python .aidlc-engine/cli/flow.py branch status # 只读查看各 CHG 分支归属与当前 git 分支
10
- python .aidlc-engine/cli/flow.py branch merge --chg CHG-xxx # code_review 留痕 + trailer 预检 + ff-only 合并回 main(代码冻结)
11
- python .aidlc-engine/cli/flow.py branch discard --chg CHG-xxx # 丢弃执行分支、归属回 main(状态回退另走 transition)
12
- ```
13
-
14
- 若已配置 flow MCP,优先直接调用 `mcp__flow__flow_branch_create` / `flow_branch_merge` / `flow_branch_discard` 工具;未配置则运行上面的 CLI 命令。
15
-
16
- **顺序铁律**:建分支 → `flow transition <CHG-ID> in_progress` → 分支内实施 → code_review 门留痕 → merge(或 discard 回退)→ `flow transition <CHG-ID> archived`。CHG 状态变更一律走 `flow transition`,禁止直接改 `workflow-state.json`。
@@ -1,6 +0,0 @@
1
- ---
2
- name: flow-e2e
3
- description: 运行端到端测试,验证 9 步全链路
4
- ---
5
- 运行 9 步端到端测试,验证需求从进入→评审→立项→拆WU→门控→变更→发布→部署→观测验收全链路。若已配置 flow MCP,调用 mcp__flow__flow_e2e 工具;否则运行 `python .aidlc-engine/cli/flow.py e2e`。
6
- 输出 9 步 PASS/FAIL 汇总,任一 FAIL 即流程不完整。
@@ -1,28 +0,0 @@
1
- ---
2
- name: flow-gate
3
- description: 执行门控校验并留痕(req_review / workunit_gate / release_gate),两步确认(--request → 人工确认 → --record),支持冻结 hash 与 HUMAN_TURN 证据核验
4
- argument-hint: <实体ID> <门控类型> [--request|--record|--reject] [--reviewer <人>] [--confirm <人>] [--note <说明>]
5
- ---
6
- 对指定实体执行门控(两步确认,P1-8)。参数:$1=实体ID(如 WU-03 / REL-100),$2=门控类型(req_review / workunit_gate / release_gate)。
7
-
8
- 若已配置 flow MCP,调用 mcp__flow__flow_gate 工具;否则运行 `python .aidlc-engine/cli/flow.py gate $1 $2 ...`。
9
-
10
- **第一步(开启门禁)**:`flow_gate(request=true)` / `flow gate $1 $2 --request` 登记 pending 待确认请求。
11
-
12
- **HARD STOP 铁律(登记后立即执行)**:
13
- - 向用户完整呈现:门禁检查单逐项结论 + 确认问题(批准 / 修改 / 驳回 三选项)。
14
- - 呈现后**立即结束回合等待用户回复**;未获用户回复前不得调用任何工具、不得自行推进。
15
- (Trae hooks 强制此仪式:enforce-gate 阻断一切非只读工具,stop 阻断未提问的收尾。)
16
-
17
- **第二步(人工决策后留痕)**:
18
- - 用户批准 → `flow_gate(record=true, confirm=<用户/确认人>)` / `flow gate $1 $2 --record --confirm <确认人>`。
19
- CLI 核验存在 ts>=requested_at 的用户真实输入(human.turn 证据)才接受留痕;Agent 转述无效。
20
- - 用户修改 → 按修改意见修订产物,然后重新 --request。
21
- - 用户驳回 → `flow_gate(reject=true, note=<原因>)` / `flow gate $1 $2 --reject --note <原因>`,进入修订。
22
-
23
- 硬性规则:
24
- - 门控必须由 gate-verifier 判定,不得主观放行;不通过则列出缺失项并停止推进。
25
- - 通过后必须用 `--record` 写入门控留痕 + 产物冻结 hash(gate.pass / gate.freeze 审计同一次原子写)。
26
- - pending 门禁的 `--record` 必须 `--confirm <人类确认人>` 且有 HUMAN_TURN 证据;缺任一即 [REJECT]。
27
- - 无 pending 请求的旧路径(直接 --record)仍可用但仅 WARN(兼容历史流程);新流程一律两步确认。
28
- - 被门控产物事后任何变化 → validate 报 [GATE_INVALIDATED],门控失效,修改后须重新过门。
@@ -1,6 +0,0 @@
1
- ---
2
- name: flow-matrix
3
- description: 从状态文件渲染全链路追溯矩阵
4
- ---
5
- 渲染 traceability-matrix.md(只读视图,真相在 workflow-state.json)。若已配置 flow MCP,调用 mcp__flow__flow_matrix 工具;否则运行 `python .aidlc-engine/cli/flow.py matrix`。
6
- 输出矩阵表与覆盖率统计,作为需求评审和审计的查看依据。
@@ -1,6 +0,0 @@
1
- ---
2
- name: flow-status
3
- description: 查看研发流程当前状态(需求看板、实体统计、覆盖率)
4
- ---
5
- 查看 workflow-state.json 的需求看板与统计信息,输出:总需求、Epic/WU/CHG/REL/OBS 数量、各状态需求分布、上线与验收覆盖率。
6
- 若已配置 flow MCP,直接调用 mcp__flow__flow_status 工具;否则运行 `python .aidlc-engine/cli/flow.py status`。
@@ -1,13 +0,0 @@
1
- ---
2
- name: flow-transition
3
- description: 状态迁移守卫(P0-1):合法迁移表 + 前置条件守卫 + 审计先行原子写
4
- argument-hint: <实体ID> <目标状态> [--by <操作者>]
5
- ---
6
- 执行受守卫的状态迁移。参数:$1=实体ID(REQ-xxx / WU-xx / CHG-xxx / REL-xxx),$2=目标状态,$3=操作者(可选,默认 编排Agent)。
7
- 若已配置 flow MCP,调用 mcp__flow__flow_transition 工具;否则运行 `python .aidlc-engine/cli/flow.py transition $1 $2 --by <操作者>`。
8
-
9
- 硬性规则:
10
- - 合法迁移表唯一真相源:`.aidlc-engine/state/state-machine.json`(REQ/WU/CHG/REL 四条流 + 状态-门控不变量)。
11
- - 非法跳转或前置条件不满足 → [REJECT] + 类型化错误码(GUARD_ILLEGAL_JUMP / GUARD_GATE_MISSING / GUARD_WU_GATE_MISSING / GUARD_OBS_FAIL 等)+ 可执行的补救命令,拒绝并审计(status.change blocked)。
12
- - 迁移成功时,status.change 审计与状态变更在同一次原子写中完成(审计先行不变量)。
13
- - 禁止绕过本命令手改状态文件(硬约束路径,见 directory-convention.md)。
@@ -1,8 +0,0 @@
1
- ---
2
- name: flow-validate
3
- description: 校验状态机、门控冻结、人在场、审计规约、产物完整性与追溯链
4
- ---
5
- 校验 workflow-state.json:状态机流转合法性、门控冻结是否失效([INVALIDATED])、人在场门控([HUMAN_ABSENT])、
6
- 审计规约(seq 连续 / action 属注册表 / 审计先行)、每状态必需产物完整性、REQ↔EPIC↔WU↔CHG↔REL↔OBS 追溯链。
7
- 若已配置 flow MCP,调用 mcp__flow__flow_validate 工具;否则运行 `python .aidlc-engine/cli/flow.py validate`。
8
- 输出所有 [ERROR] / [WARN],存在 ERROR 时不得继续推进流程。