@xulthekl/team-flow 0.50.0 → 0.51.0

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 (37) hide show
  1. package/.claude/always/phase-guard.md +1 -1
  2. package/.claude-plugin/marketplace.json +1 -1
  3. package/.claude-plugin/plugin.json +1 -1
  4. package/.codex-plugin/plugin.json +1 -1
  5. package/.cursor-plugin/marketplace.json +1 -1
  6. package/.cursor-plugin/plugin.json +1 -1
  7. package/.github/plugin/marketplace.json +2 -2
  8. package/CHANGELOG.md +31 -0
  9. package/GEMINI.md +1 -1
  10. package/INSTALL.md +1 -1
  11. package/README.md +1 -1
  12. package/docs/README_en.md +1 -1
  13. package/gemini-extension.json +1 -1
  14. package/hooks/session-start +2 -2
  15. package/llms.txt +1 -1
  16. package/package.json +1 -1
  17. package/plugin.json +1 -1
  18. package/scripts/ensure-branch.mjs +58 -32
  19. package/scripts/lib/arch-merge.mjs +32 -9
  20. package/scripts/lib/cmd-deisolate.mjs +15 -16
  21. package/scripts/lib/cmd-publish.mjs +4 -7
  22. package/scripts/lib/conventions-generator.mjs +9 -7
  23. package/scripts/lib/execution-plan.mjs +18 -3
  24. package/scripts/lib/git-utils.mjs +172 -14
  25. package/scripts/lib/glaf4-delegation.mjs +5 -2
  26. package/scripts/lib/severity.mjs +71 -0
  27. package/scripts/lib/solutions-capture.mjs +14 -0
  28. package/scripts/lib/solutions-index-gen.mjs +2 -2
  29. package/scripts/lib/solutions-inject.mjs +2 -3
  30. package/scripts/lib/solutions-promote.mjs +16 -8
  31. package/skills/ce-compound/references/promotion-rules.md +9 -2
  32. package/skills/ce-compound/references/three-tier-index.md +1 -1
  33. package/skills/ce-compound/references/write-flow.md +1 -1
  34. package/skills/release-archivist/SKILL.md +3 -1
  35. package/skills/release-archivist/references/closing-procedures.md +1 -1
  36. package/skills/release-archivist/references/worktree-merge.md +1 -1
  37. package/templates/learnings.md +2 -2
@@ -1,3 +1,3 @@
1
- # team-flow v0.50.0 | 阶段: {{state}} | 工作流: {{workflow}}
1
+ # team-flow v0.51.0 | 阶段: {{state}} | 工作流: {{workflow}}
2
2
  当前阶段允许的操作由 workflow-start 路由规则定义。
3
3
  禁止跨越 DP gate 进入下一阶段。变更范围以 execution-contract.md 的 Intent Lock 为准。
@@ -9,7 +9,7 @@
9
9
  {
10
10
  "name": "team-flow",
11
11
  "description": "8-state spec workflow + compound global compounding + architecture-design (4A/DDD) + local HTML prototype + product-level orchestration + bootstrap + e2e + session handoff + workflow feedback + independent business analysis. 26 skills + 17 agents with embedded TDD, SDD, code review, debugging, delta spec sync, and design-system-driven prototyping.",
12
- "version": "0.50.0",
12
+ "version": "0.51.0",
13
13
  "source": "./",
14
14
  "author": {
15
15
  "name": "LT",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "team-flow",
3
- "version": "0.50.0",
3
+ "version": "0.51.0",
4
4
  "description": "Unified workflow plugin: team-flow (spec-driven dev: TDD/SDD/code-review/debugging) + compound-engineering core subset (brainstorm/plan/compound/strategy/ideate/proof, global compounding) + architecture-design (4A+DDD incremental design & global compounding) + prototype (local HTML prototype, zero external deps) + e2e (Playwright E2E, AC-driven) + workflow-orchestrator (product-level workflow orchestration) + workflow-bootstrap (existing project onboarding) + session-handoff (context transfer) + workflow-feedback (issue tracking) + business-analysis (independent requirement/scenario artifact). 26 skills + 17 agents, one install.",
5
5
  "source": "./",
6
6
  "author": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "team-flow",
3
- "version": "0.50.0",
3
+ "version": "0.51.0",
4
4
  "description": "Spec-first workflow that bridges OpenSpec-style planning and Superpowers-style execution discipline.",
5
5
  "author": {
6
6
  "name": "MageByte",
@@ -5,7 +5,7 @@
5
5
  },
6
6
  "metadata": {
7
7
  "description": "Unified workflow plugin marketplace for Cursor (team-flow: team-flow + compound + architecture-design + prototype).",
8
- "version": "0.50.0"
8
+ "version": "0.51.0"
9
9
  },
10
10
  "plugins": [
11
11
  {
@@ -2,7 +2,7 @@
2
2
  "name": "team-flow",
3
3
  "displayName": "team-flow",
4
4
  "description": "Unified workflow plugin: team-flow (spec-driven dev: TDD/SDD/code-review/debugging) + compound-engineering core subset (brainstorm/plan/compound/strategy/ideate/proof, global compounding) + architecture-design (4A+DDD incremental design & global compounding) + prototype (local HTML prototype, zero external deps) + e2e (Playwright E2E, AC-driven) + workflow-orchestrator (product-level workflow orchestration) + workflow-bootstrap (existing project onboarding) + session-handoff (context transfer) + workflow-feedback (issue tracking) + business-analysis (independent requirement/scenario artifact). 26 skills + 17 agents, one install.",
5
- "version": "0.50.0",
5
+ "version": "0.51.0",
6
6
  "author": {
7
7
  "name": "LT",
8
8
  "url": "https://github.com/LT"
@@ -6,13 +6,13 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "Unified workflow plugins and skills for AI coding agents (team-flow: team-flow + compound + architecture-design + prototype).",
9
- "version": "0.50.0"
9
+ "version": "0.51.0"
10
10
  },
11
11
  "plugins": [
12
12
  {
13
13
  "name": "team-flow",
14
14
  "description": "Unified workflow with planning artifacts, execution contracts, TDD, review gates, systematic debugging, delta spec sync, architecture-design, independent business analysis, and local HTML prototyping.",
15
- "version": "0.50.0",
15
+ "version": "0.51.0",
16
16
  "source": ".",
17
17
  "author": {
18
18
  "name": "LT",
package/CHANGELOG.md CHANGED
@@ -4,6 +4,37 @@ All notable changes to `team-flow` will be documented in this file.
4
4
 
5
5
  The format loosely follows Keep a Changelog.
6
6
 
7
+ ## [0.51.0] - 2026-09-09
8
+
9
+ ### Fixed(CLI 静默失效治理:仓清单 / 白名单 / 严重度序)
10
+
11
+ 设计来源:`docs/architecture-api-db-design-enhancement-v0.23.md` §91-§93(2026-09-09,emp-auth 4 条 workflow-feedback:20260909-101814 / 101815 / 101816 / 104554)
12
+
13
+ **R1 — severity 有序等级集中化**(`tf solutions promote` 漏晋升 critical)
14
+
15
+ - **唯一真相源**:新增 `scripts/lib/severity.mjs`(`SEVERITY_ORDER` / `SEVERITY_LADDER` / `severityRank` / `meetsMinSeverity` / `nextSeverity`),取值域扩展为 `critical | high | medium | low`
16
+ - **修复三处同时失效**:晋升判定用集合相等 `severity === 'high' || 'medium'` → `meetsMinSeverity(severity, 'medium')`;升级阶梯补 critical 封顶;`SEVERITY_ORDER` 两处重复定义删除并补 critical 键(原先 critical 排序垫底、inject top-5 优先排除)
17
+ - **入口闸**:`tf solutions capture --severity` 新增取值校验(非法值报错并列出合法取值)
18
+ - **防模板污染**:`parseFrontmatter` 剥离行尾注释(模板注释被逐字复制时会污染取值)
19
+ - 规范同步:`templates/learnings.md` / `release-archivist`(SKILL + closing-procedures)/ `ce-compound`(promotion-rules + three-tier-index + write-flow)
20
+
21
+ **R2 — 仓清单解析统一**(`tf execution review` 嵌套仓找不到、`tf isolate` 静默降级为空壳)
22
+
23
+ - **统一入口**:`git-utils.resolveCodeRepos`(config `repo_layout.repos` 权威 → 两层探测 fallback)、`listCodeReposDeep`(支持 `service/<repo>` 等两层布局)、`parsePorcelainPaths`(porcelain 解析唯一实现)
24
+ - **格式兼容**:`repo_layout.repos` 的 key/value 逐序尝试(相对路径 / 绝对路径 / 人类描述),命中判据为「目录存在」+ 返回 `isGitRepo` 供消费方分流
25
+ - **禁止静默降级**:config 声明 `mode=multi-repo` 却解析不到 git 仓时 `tf isolate` 退出非 0(`--force` 可显式降级)
26
+ - **报错可操作**:review receipt 的 SHA 不在任何已扫描仓时,报错附扫描清单与 `--repo` 指引
27
+ - **消费者闭环**:`tf deisolate` 的 `detectCaseA` 复用两层探测(`excludeNonRepoDirs: false` 与创建方口径对齐),嵌套仓与怪名仓(`data`/`prototype`/`docs/*`)不再漏检
28
+ - 消费方改造:`ensure-branch` / `execution-plan` / `conventions-generator`(嵌套探测)/ `glaf4-delegation`(复用解析器,修 `Object.values(repos)` 把人类描述当路径)
29
+
30
+ **R3 — arch-merge 白名单机制修复**(告警与实际提交行为不一致)
31
+
32
+ - **判定侧**:`detectUntouchedDirtyFiles` 改用共享 `parsePorcelainPaths`(原 `^\S+\s+` 漏匹配 ` M path`,导致本次 touch 的文件被误报为「非本次 touch 的脏文件」)
33
+ - **提交侧**:`git commit -- <白名单>` 限定边界(裸 `git commit` 会卷走索引中已暂存的外部文件)
34
+ - **事后断言**:回读 `git show --name-only` 与白名单比对,不一致则 WARN
35
+
36
+ **质量**:P3 审查发现并修复 5 项(含 `glaf4-delegation` 设计承诺未落地);P4 两轮评审(skill-reviewer FAIL→PASS,8 项修复;plugin-validator FAIL→FAIL→PASS,闭环 `tf deisolate` 断链与 `NON_REPO_DIRS` 口径回归);**807/807 测试通过**。
37
+
7
38
  ## [0.50.0] - 2026-09-09
8
39
 
9
40
  ### Fixed(hotfix 路径死锁 + 轻量路径仪式成本)
package/GEMINI.md CHANGED
@@ -8,7 +8,7 @@ The workflow is self-contained and does not require OpenSpec or Superpowers at r
8
8
 
9
9
 
10
10
  <!-- team-flow-phase-guard-start -->
11
- # team-flow v0.50.0 | 阶段: {{state}} | 工作流: {{workflow}}
11
+ # team-flow v0.51.0 | 阶段: {{state}} | 工作流: {{workflow}}
12
12
  当前阶段允许的操作由 workflow-start 路由规则定义。
13
13
  禁止跨越 DP gate 进入下一阶段。变更范围以 execution-contract.md 的 Intent Lock 为准。
14
14
  <!-- team-flow-phase-guard-end -->
package/INSTALL.md CHANGED
@@ -7,7 +7,7 @@
7
7
  - [Fission-AI/OpenSpec](https://github.com/Fission-AI/OpenSpec) — 规划引擎(Schema 验证、Delta Spec、工件解析)
8
8
  - [obra/superpowers](https://github.com/obra/superpowers) — 执行纪律(TDD 铁律、SDD、系统化调试、代码审查)
9
9
 
10
- 当前发布版本:**v0.50.0**。
10
+ 当前发布版本:**v0.51.0**。
11
11
 
12
12
  ---
13
13
 
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # team-flow
2
2
 
3
- > 当前版本:`v0.50.0`
3
+ > 当前版本:`v0.51.0`
4
4
 
5
5
  > 统一插件:**team-flow**(spec 驱动开发)+ **compound-engineering 核心子集**(全局复利)+ **architecture-design**(4A+DDD 增量设计)+ **prototype**(本地 HTML 原型)+ **e2e**(AC 驱动 E2E)+ **workflow-orchestrator**(产品级编排)+ **workflow-bootstrap**(既有项目接入)。一次安装,七套能力协同。
6
6
 
package/docs/README_en.md CHANGED
@@ -126,7 +126,7 @@ npm install -g team-flow
126
126
 
127
127
  ### Version
128
128
 
129
- - Current: `v0.50.0`
129
+ - Current: `v0.51.0`
130
130
  - v0.9.1 highlights: DP-4 execution-mode recommendations, a portable runtime across 17 platforms, and a raw-package smoke with no plugin-root variable.
131
131
  - Self-contained — no OpenSpec or Superpowers runtime required
132
132
  - Upstream: [Fission-AI/OpenSpec](https://github.com/Fission-AI/OpenSpec), [obra/superpowers](https://github.com/obra/superpowers)
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "team-flow",
3
3
  "description": "Unified workflow plugin: team-flow (spec-driven dev) + compound-engineering core subset + architecture-design (4A/DDD) + prototype (local HTML) + business-analysis (independent requirement/scenario artifact). 26 skills, one install.",
4
- "version": "0.50.0",
4
+ "version": "0.51.0",
5
5
  "contextFileName": "GEMINI.md"
6
6
  }
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env bash
2
- # v0.50.0: auto-sync CLI version with plugin version
2
+ # v0.51.0: auto-sync CLI version with plugin version
3
3
  set -e
4
4
 
5
5
  # ═══════════════════════════════════════════════════════════════
6
6
  # Plugin version (update this when releasing new versions)
7
7
  # ═══════════════════════════════════════════════════════════════
8
- PLUGIN_VERSION="0.50.0"
8
+ PLUGIN_VERSION="0.51.0"
9
9
 
10
10
  # ═══════════════════════════════════════════════════════════════
11
11
  # Step 1: Auto-sync CLI version with plugin version
package/llms.txt CHANGED
@@ -3,7 +3,7 @@
3
3
  ## Overview
4
4
  spec-superflow is a self-contained workflow integration plugin for Claude Code, Cursor, OpenAI Codex CLI/App, GitHub Copilot CLI, Gemini CLI, OpenCode, WorkBuddy, and Trae. It merges spec-driven planning artifacts (proposal, specs, design, tasks) with disciplined execution guardrails (TDD, review gates, controlled handoff) into one unified workflow.
5
5
 
6
- Current version: v0.50.0.
6
+ Current version: v0.51.0.
7
7
 
8
8
  ## Key Documents
9
9
  - README.md: Chinese homepage with full usage guide and FAQ
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xulthekl/team-flow",
3
- "version": "0.50.0",
3
+ "version": "0.51.0",
4
4
  "description": "Unified plugin (26 skills + 17 agents) integrating team-flow, compound-engineering, architecture-design, prototype, design-system, workflow-orchestrator, workflow-bootstrap, e2e, session-handoff, workflow-feedback, business-analysis for multi-agent coding tools.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "team-flow",
3
- "version": "0.50.0",
3
+ "version": "0.51.0",
4
4
  "description": "Unified workflow plugin: team-flow (spec-driven dev: TDD/SDD/code-review/debugging) + compound-engineering core subset (brainstorm/plan/compound/strategy/ideate/proof, global compounding) + architecture-design (4A+DDD incremental design & global compounding) + prototype (local HTML prototype, zero external deps) + e2e (Playwright E2E, AC-driven) + workflow-orchestrator (product-level workflow orchestration) + workflow-bootstrap (existing project onboarding) + session-handoff (context transfer) + workflow-feedback (issue tracking) + business-analysis (independent requirement/scenario artifact). 26 skills + 17 agents, one install.",
5
5
  "author": {
6
6
  "name": "LT"
@@ -6,16 +6,19 @@
6
6
  //
7
7
  // Usage: node ensure-branch.mjs <change-dir> [change-name] [--force]
8
8
  //
9
- // 布局识别(来源:workflow-feedback 2026-08-01 #100009):
10
- // Case A 多仓库工作区 —— change-dir 形如 <root>/changes/<name>,且 <root> 下
11
- // 存在含 .git 的直接子目录(真正的业务代码仓库,如 bff-*/ui-*/prototype/)。
12
- // 工作区根本身往往只是近空壳 git 仓库(仅跟踪 .gitignore,无 remote)。旧实现
13
- // change-dir 当代码仓库、worktree 建在 ../,结果是空工作区副本,实现子代理
14
- // 看不到任何参考代码。新实现为每个代码仓库在 <root>/.worktrees/<change>/<repo>
15
- // 建立 worktree(基于各仓库当前分支),实现子代理进入 <root>/.worktrees/<change>/
16
- // 即可看到与主工作区一致的目录结构和全部参考代码。
17
- // Case B 单代码仓库(遗留兼容)—— 不满足 Case A 时回退到旧行为:把 change-dir
18
- // 当代码仓库,worktree 建在 ../<repo>-<name>。保证单仓库项目不被破坏。
9
+ // 布局识别(来源:workflow-feedback 2026-08-01 #100009;v0.23 §92.3.4/§92.3.6 重构):
10
+ // 仓清单统一由 git-utils.resolveCodeRepos 解析(config 权威 两层探测 fallback),
11
+ // 本文件不再自行枚举目录。
12
+ // Case A 多仓库工作区 —— change-dir 形如 <root>/changes/<name>,且解析出 ≥1 个代码仓库
13
+ // (含 <root>/<repo> <root>/service/<repo> 两种布局)。工作区根本身往往只是近空壳
14
+ // git 仓库(仅跟踪 .gitignore,无 remote)。旧实现把 change-dir 当代码仓库、worktree
15
+ // 建在 ../,结果是空工作区副本,实现子代理看不到任何参考代码。现为每个代码仓库在
16
+ // <root>/.worktrees/<change>/<repo> 建立 worktree(基于各仓库当前分支),实现子代理
17
+ // 进入 <root>/.worktrees/<change>/ 即可看到与主工作区一致的目录结构和全部参考代码。
18
+ // Case B 单代码仓库(遗留兼容)—— 无 repo_layout 声明或声明为 single 时回退旧行为:
19
+ // 把 change-dir 当代码仓库,worktree 建在 ../<repo>-<name>。保证单仓库项目不被破坏。
20
+ // 硬失败(v0.23 §92.3.6)—— config 声明 mode=multi-repo 却解析不到任何代码仓库时退出
21
+ // 非 0(此前会静默降级为 Case B,建出空壳 worktree 且代码落到共享分支)。--force 可显式降级。
19
22
  //
20
23
  // Security: every git invocation uses execFileSync with a LITERAL command
21
24
  // ('git') and a LITERAL argument array (no shell, no string-form command) — the
@@ -25,8 +28,9 @@
25
28
  // branch names are additionally sanitized to git-legal characters before use.
26
29
  import { execFileSync } from 'node:child_process';
27
30
  import { resolve, basename, join } from 'node:path';
28
- import { detectWorkspaceRoot } from './lib/git-utils.mjs';
29
- import { readdirSync, existsSync, appendFileSync, readFileSync } from 'node:fs';
31
+ import { detectWorkspaceRoot, resolveCodeRepos } from './lib/git-utils.mjs';
32
+ import { loadConfig } from './lib/config-loader.mjs';
33
+ import { existsSync, appendFileSync, readFileSync } from 'node:fs';
30
34
 
31
35
  const rawChangeDir = process.argv[2];
32
36
  // 第三个位置参数是可选 change-name;若它其实是 '--force' 之类的开关则忽略,避免把
@@ -107,21 +111,6 @@ function checkSharedBranch(repoDir) {
107
111
  }
108
112
  }
109
113
 
110
- // 枚举 root 下含 .git 的直接子目录(= 代码仓库),按目录名排序保证确定性。
111
- // .git 可能是目录(普通仓库)或文件(worktree/submodule 链接),existsSync 均覆盖。
112
- function listCodeRepos(root) {
113
- let entries;
114
- try {
115
- entries = readdirSync(root, { withFileTypes: true });
116
- } catch {
117
- return [];
118
- }
119
- return entries
120
- .filter((e) => e.isDirectory() && existsSync(join(root, e.name, '.git')))
121
- .map((e) => e.name)
122
- .sort();
123
- }
124
-
125
114
  // 确保 <root>/.gitignore 含忽略 .worktrees/ 的行,已有则不重复追加。
126
115
  function ensureWorktreeGitignore(root) {
127
116
  const gitignorePath = join(root, '.gitignore');
@@ -300,14 +289,51 @@ function runSingleRepo() {
300
289
  process.exit(1);
301
290
  }
302
291
 
303
- // 主流程:布局识别后分发(来源:workflow-feedback 2026-08-01 #100009)。
292
+ // 主流程:布局识别后分发(来源:workflow-feedback 2026-08-01 #100009
293
+ // v0.23 §92.3.4/§92.3.6:仓清单统一走 resolveCodeRepos,config 声明与实况矛盾时硬失败)。
304
294
  const workspaceRoot = detectWorkspaceRoot(changeDir);
305
- const codeRepos = workspaceRoot ? listCodeRepos(workspaceRoot) : [];
295
+ const config = workspaceRoot ? loadConfig(workspaceRoot) : null;
296
+ const { repos: codeRepos, unresolved, source } = workspaceRoot
297
+ ? resolveCodeRepos(workspaceRoot, config)
298
+ : { repos: [], unresolved: [], source: 'probe' };
299
+
300
+ // v0.23 §92.3.2:repo_layout.repos 条目可能是 monorepo 风格的 Java 模块(无 .git),
301
+ // 无法建 worktree —— 隔离只对 git 仓生效,非 git 条目跳过并告警(不静默)。
302
+ const gitRepos = codeRepos.filter(r => r.isGitRepo);
303
+ const nonGitRepos = codeRepos.filter(r => !r.isGitRepo);
306
304
 
307
- if (workspaceRoot && codeRepos.length > 0) {
305
+ if (workspaceRoot && gitRepos.length > 0) {
308
306
  // Case A:多仓库工作区。
309
- runMultiRepo(workspaceRoot, codeRepos);
307
+ if (unresolved.length > 0) {
308
+ console.error(`ensure-branch: ${unresolved.length} 条 repo_layout.repos 声明无法解析为路径(已忽略):`
309
+ + unresolved.map(u => `\n - ${u.key}`).join(''));
310
+ }
311
+ if (nonGitRepos.length > 0) {
312
+ console.error(`ensure-branch: ${nonGitRepos.length} 个声明项非 git 仓库(无法建 worktree,已跳过隔离):${nonGitRepos.map(r => r.relPath).join(', ')}`);
313
+ }
314
+ console.error(`ensure-branch: repo list resolved from ${source}(${gitRepos.length} 个): ${gitRepos.map(r => r.relPath).join(', ')}`);
315
+ runMultiRepo(workspaceRoot, gitRepos.map(r => r.relPath));
316
+ } else if (config?.repo_layout?.mode === 'multi-repo') {
317
+ // v0.23 §92.3.6:config 声明 multi-repo 却解析不到任何代码仓库 → 硬失败,禁止静默降级。
318
+ // 历史缺陷:此时会走 Case B,把 change-dir 当代码仓库、建出归属根仓的空壳 worktree,
319
+ // 实现子代理回到主工作区直接改共享分支(emp-auth feedback 2026-09-09)。
320
+ console.error('ensure-branch: config 声明 repo_layout.mode=multi-repo,但未解析出任何代码仓库 —— 拒绝按单仓库就地实施。');
321
+ console.error(` workspace root : ${workspaceRoot}`);
322
+ console.error(` config : ${join(workspaceRoot, '.team-flow', 'team-flow.config.json')}`);
323
+ if (unresolved.length > 0) {
324
+ console.error(` 无法解析的声明(${unresolved.length} 条):${unresolved.map(u => u.key).join(', ')}`);
325
+ console.error(' 提示:key 应为相对工作区根的仓库路径(如 service/infra-emp-auth),或 value 为仓库绝对路径。');
326
+ } else {
327
+ console.error(' 提示:repo_layout.repos 为空,或路径未指向含 .git 的目录;可运行 tf repo-layout detect 重建。');
328
+ }
329
+ console.error(' 如确认要在共享分支就地实施,请显式传 --force。');
330
+ if (!force) process.exit(1);
331
+ console.error('ensure-branch: WARNING — --force given; falling back to single-repo behavior.');
332
+ runSingleRepo();
310
333
  } else {
311
- // Case B:不满足 Case A → 回退单代码仓库行为,保证单仓库项目不被破坏。
334
+ // Case B:真单仓库(无 config,或 config.mode=single)→ 保持既有行为。
335
+ if (!config?.repo_layout?.mode) {
336
+ console.error('ensure-branch: 未检测到 repo_layout 配置,按单仓库处理;多仓库项目请先运行 tf repo-layout detect <root>。');
337
+ }
312
338
  runSingleRepo();
313
339
  }
@@ -18,14 +18,14 @@
18
18
  import {
19
19
  readFileSync, writeFileSync as fsWriteFileSync, existsSync, mkdirSync, readdirSync, rmdirSync, cpSync,
20
20
  } from 'node:fs';
21
- import { join, basename, dirname, resolve } from 'node:path';
21
+ import { join, basename, dirname, relative, resolve } from 'node:path';
22
22
  import { execFileSync } from 'node:child_process';
23
23
  import { pathToFileURL } from 'node:url';
24
24
  import {
25
25
  parseTableAfter, extractEndpoints, extractAggregates,
26
26
  extractTablesFromSql, extractTablesFromDatabaseMd, readFrontmatter,
27
27
  } from './arch-parse.mjs';
28
- import { detectWorkspaceRoot, getGitRoot } from './git-utils.mjs';
28
+ import { detectWorkspaceRoot, getGitRoot, parsePorcelainPaths } from './git-utils.mjs';
29
29
 
30
30
  const MARKER_BEGIN = '<!-- arch:current-state:begin -->';
31
31
  const MARKER_END = '<!-- arch:current-state:end -->';
@@ -420,8 +420,29 @@ function gitCommit(projectRoot, globalArchDir, touchedFiles, changeName, dryRun)
420
420
  for (const f of touchedFiles) {
421
421
  execFileSync('git', ['add', f], { cwd: projectRoot, stdio: 'pipe' });
422
422
  }
423
- execFileSync('git', ['commit', '-m', commitMsg], { cwd: projectRoot, stdio: 'pipe' });
424
- return { committed: true, message: commitMsg };
423
+ // v0.23 §93.3.2:commit 必须带 pathspec——裸 `git commit` 提交索引中**全部已暂存内容**,
424
+ // 索引里若有他人/前序工具(如 tf publish --arch)留下的 staged 文件会被一并卷走。
425
+ execFileSync('git', ['commit', '-m', commitMsg, '--', ...touchedFiles], { cwd: projectRoot, stdio: 'pipe' });
426
+
427
+ // v0.23 §93.3.3:回读实际提交清单与白名单比对(第二道防线,不阻断——提交已完成)
428
+ // 前提:projectRoot 即 git 仓根(`git show --name-only` 输出仓根相对路径,此处按 projectRoot
429
+ // 还原为绝对路径)。projectRoot 取自 detectWorkspaceRoot(changeDir) || getGitRoot(changeDir),
430
+ // 实践中成立;显式传 --project-root 指向子目录时该前提不成立,会误报为白名单外(已知边界)。
431
+ const committed = String(execFileSync(
432
+ 'git', ['show', '--name-only', '--format=', 'HEAD'],
433
+ { cwd: projectRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] },
434
+ )).split('\n').filter(Boolean).map(p => resolve(projectRoot, p));
435
+ const whitelist = new Set(touchedFiles.map(p => resolve(projectRoot, p)));
436
+ const unexpected = committed.filter(p => !whitelist.has(p));
437
+ if (unexpected.length > 0) {
438
+ console.warn(` [WARN] 本次 commit 含白名单外的文件(${unexpected.length} 个):${unexpected.map(p => relative(projectRoot, p)).join(', ')}`);
439
+ }
440
+ return {
441
+ committed: true,
442
+ message: commitMsg,
443
+ files: committed.map(p => relative(projectRoot, p)),
444
+ unexpected,
445
+ };
425
446
  } catch (e) {
426
447
  return { committed: false, error: e.message };
427
448
  }
@@ -433,10 +454,11 @@ function detectUntouchedDirtyFiles(projectRoot, globalArchDir, touchedFiles) {
433
454
  const status = execFileSync('git', ['status', '--porcelain', '--', globalArchDir], {
434
455
  cwd: projectRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
435
456
  });
436
- const dirty = status.split('\n').filter(Boolean)
437
- .map(line => line.replace(/^\S+\s+/, '').trim())
438
- .filter(p => p && !touchedFiles.includes(resolve(p)) && !touchedFiles.includes(p));
439
- return dirty;
457
+ // v0.23 §93.3.1:改用共享解析器。原 `^\S+\s+` 无法剥离 ` M path`(行首空格 = 未暂存修改,
458
+ // 最常见形态)→ 路径残留状态码 → 本次 touch 的文件被误报为「非本次 touch 的脏文件」。
459
+ // 路径口径统一为「相对 projectRoot 的绝对路径」后再与白名单比对,不依赖 process.cwd()
460
+ const touched = new Set(touchedFiles.map(p => resolve(projectRoot, p)));
461
+ return parsePorcelainPaths(status).filter(p => p && !touched.has(resolve(projectRoot, p)));
440
462
  } catch {
441
463
  return [];
442
464
  }
@@ -533,7 +555,8 @@ export function run(args = {}) {
533
555
  ].filter(p => existsSync(p));
534
556
  const dirty = detectUntouchedDirtyFiles(projectRoot, globalArchDir, touchedFiles);
535
557
  if (dirty.length > 0) {
536
- console.warn(` [WARN] docs/architecture/ 存在非本次 touch 的脏文件(${dirty.length} 个):${dirty.slice(0, 5).join(', ')} 白名单 git add 不卷走它们,但请确认是否他人未提交工作`);
558
+ console.warn(` [WARN] docs/architecture/ 检测到 ${dirty.length} 个非本次 touch 的改动:${dirty.slice(0, 5).join(', ')}${dirty.length > 5 ? ' …' : ''}`);
559
+ console.warn(` 本次提交范围以白名单为准(${touchedFiles.length} 个文件);上述文件不会被提交,请确认是否他人未提交工作。`);
537
560
  }
538
561
  const commitResult = gitCommit(projectRoot, globalArchDir, touchedFiles, changeName, dryRun);
539
562
 
@@ -15,10 +15,10 @@
15
15
  // Case B 单仓库:<change-dir> 同级 ../<repo>-<change-name> 存在
16
16
 
17
17
  import { execFileSync } from 'node:child_process';
18
- import { existsSync, readdirSync, rmSync } from 'node:fs';
18
+ import { existsSync, rmSync } from 'node:fs';
19
19
  import { basename, dirname, join, resolve } from 'node:path';
20
20
  import { parseArgs } from 'node:util';
21
- import { detectWorkspaceRoot } from './git-utils.mjs';
21
+ import { detectWorkspaceRoot, listCodeReposDeep } from './git-utils.mjs';
22
22
 
23
23
  export async function run(args) {
24
24
  const { positionals, values } = parseArgs({
@@ -79,24 +79,23 @@ export async function run(args) {
79
79
  }
80
80
 
81
81
 
82
- // Case A: <root>/.worktrees/<change>/<repo>/
82
+ // Case A: <root>/.worktrees/<change>/<repo>/ 或 <root>/.worktrees/<change>/<container>/<repo>/
83
+ // v0.23 §92.3.7(P4 plugin-validator M1):复用两层探测——ensure-branch 现按 relPath 建 worktree,
84
+ // 嵌套布局(service/<repo>)在 <change>/ 下的直接子项是容器目录(无 .git),旧的一层扫描会漏检
85
+ // → deisolate 报 "No worktrees found",closing 阶段代码永远停在 worktree 分支。
83
86
  function detectCaseA(root, changeName) {
84
87
  const changeWorktreeDir = join(root, '.worktrees', changeName);
85
88
  if (!existsSync(changeWorktreeDir)) return null;
86
89
 
87
- let entries;
88
- try {
89
- entries = readdirSync(changeWorktreeDir, { withFileTypes: true });
90
- } catch {
91
- return null;
92
- }
93
-
94
- const repos = entries
95
- .filter(e => e.isDirectory() && existsSync(join(changeWorktreeDir, e.name, '.git')))
96
- .map(e => ({
97
- name: e.name,
98
- worktreePath: join(changeWorktreeDir, e.name),
99
- repoPath: join(root, e.name),
90
+ // excludeNonRepoDirs: false —— worktree 目录结构由 ensure-branch 按 config 权威清单创建,
91
+ // 仓名可能恰是 NON_REPO_DIRS 中的词(data / prototype / docs / …);沿用探测期排除表会漏检
92
+ // 代码静默留在 worktree 分支(P4 plugin-validator 复审残留 M1)。
93
+ const repos = listCodeReposDeep(changeWorktreeDir, { excludeNonRepoDirs: false })
94
+ .map(repo => ({
95
+ // 用 relPath 作展示名:嵌套布局下可区分 service/a 与 service/b(扁平布局等价于目录名)
96
+ name: repo.relPath,
97
+ worktreePath: join(changeWorktreeDir, repo.relPath),
98
+ repoPath: join(root, repo.relPath),
100
99
  layout: 'A',
101
100
  }))
102
101
  .sort((a, b) => a.name.localeCompare(b.name));
@@ -19,7 +19,7 @@
19
19
  import { execFileSync } from 'node:child_process';
20
20
  import { existsSync, mkdirSync, readdirSync, rmdirSync } from 'node:fs';
21
21
  import { join, resolve, isAbsolute } from 'node:path';
22
- import { detectWorkspaceRoot } from './git-utils.mjs';
22
+ import { detectWorkspaceRoot, parsePorcelainPaths } from './git-utils.mjs';
23
23
 
24
24
  const LOCK_PATH = '.publish.lock';
25
25
 
@@ -126,12 +126,9 @@ function detectOutsideDirty(projectRoot, whitelist) {
126
126
  try {
127
127
  const status = git(projectRoot, 'status', '--porcelain');
128
128
  if (!status) return [];
129
- return status.split('\n').filter(Boolean)
130
- .map(line => {
131
- // porcelain 前 2 字符是状态,后面是路径(引号包裹的可能带空格)
132
- const raw = line.slice(3).replace(/^"|"$/g, '').replace(/\\ /g, ' ');
133
- return raw;
134
- })
129
+ // v0.23 §93.3.1:解析统一走 git-utils.parsePorcelainPaths(本文件的 slice(3) 实现
130
+ // 是正确版本,已上移共享;arch-merge 原用的 `^\S+\s+` 正则会漏掉 ` M path`)。
131
+ return parsePorcelainPaths(status)
135
132
  .filter(p => {
136
133
  if (!p) return false;
137
134
  const norm = p.replace(/\/+$/, ''); // porcelain 目录级输出带尾斜杠,先去规范化
@@ -13,6 +13,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from
13
13
  import { homedir } from 'node:os';
14
14
  import path, { join } from 'node:path';
15
15
  import { parseArgs } from 'node:util';
16
+ import { listCodeReposDeep, NON_REPO_DIRS } from './git-utils.mjs';
16
17
 
17
18
  // ── glaf4-dev 运行时探测(v2.1 §4 接口①)─────────────────────────────────────
18
19
 
@@ -109,11 +110,7 @@ export function writeGlaf4DevConfig(root, glaf4DevInfo) {
109
110
 
110
111
  // ── repo_layout 检测(multi-repo-support-design v1.0 §3,2026-08-21)─────────────
111
112
 
112
- // 非代码仓库目录(team-flow 产物/基础设施),扫描时排除
113
- const NON_REPO_DIRS = new Set([
114
- 'changes', '.team-flow', 'docs', 'doc', 'node_modules', '.worktrees',
115
- 'requirement', 'prototype', 'specs', 'data', 'path', 'template',
116
- ]);
113
+ // 非代码仓库目录集合统一在 git-utils.mjs 定义(v0.23 §92:唯一真相源),见顶部 import。
117
114
 
118
115
  /**
119
116
  * 识别项目布局(single / monorepo / multi-repo,v1.0 §3.1)
@@ -130,15 +127,20 @@ export function detectRepoLayout(root) {
130
127
  return { mode: subModules.length > 0 ? 'monorepo' : 'single', repos };
131
128
  }
132
129
  // 2. 根无技术栈特征 → 扫描子目录识别独立代码仓库(含 .git 或有技术栈语言)
130
+ // v0.23 §92.3.3:含 .git 的仓库改用 listCodeReposDeep 探测,支持 `service/<repo>` 两层布局
131
+ //(原实现只扫直接子项 → emp-auth 12 仓被误判 single,config 靠人工修正)。
133
132
  const repos = {};
133
+ for (const repo of listCodeReposDeep(root)) {
134
+ repos[repo.relPath] = repo.absPath;
135
+ }
134
136
  const entries = readdirSync(root, { withFileTypes: true });
135
137
  for (const entry of entries) {
136
138
  if (!entry.isDirectory()) continue;
137
139
  if (entry.name.startsWith('.') || NON_REPO_DIRS.has(entry.name)) continue;
140
+ if (repos[entry.name]) continue;
138
141
  const sub = join(root, entry.name);
139
- const hasGit = existsSync(join(sub, '.git'));
140
142
  const tech = detectTechStack(sub);
141
- if (hasGit || tech.language) {
143
+ if (tech.language) {
142
144
  repos[entry.name] = sub;
143
145
  }
144
146
  }
@@ -5,7 +5,8 @@ import { isAbsolute, join, relative, resolve, sep } from 'node:path';
5
5
  import { computeArtifactsHash, computeContractHash, hashObject, stableJson } from './hash.mjs';
6
6
  import { getOverlayPaths } from './sdd-overlay.mjs';
7
7
  import { readState } from './state-loader.mjs';
8
- import { detectWorkspaceRoot, findSubRepo } from './git-utils.mjs';
8
+ import { detectWorkspaceRoot, findSubRepo, resolveCodeRepos } from './git-utils.mjs';
9
+ import { loadConfig } from './config-loader.mjs';
9
10
 
10
11
  // v2.1 §6.6:扩入 glaf4-delegation(GLAF4 change 整体委托 glaf4-dev 七模式流,S1 结构统一)
11
12
  export const EXECUTION_MODES = ['inline', 'batch-inline', 'sdd', 'glaf4-delegation'];
@@ -334,8 +335,22 @@ function validateReviewRange(changeDir, base, head, repoPath) {
334
335
  const workspaceRoot = detectWorkspaceRoot(changeDir);
335
336
  if (!workspaceRoot) throw primaryError;
336
337
 
337
- const subRoot = findSubRepo(workspaceRoot, head);
338
- if (!subRoot) throw primaryError;
338
+ // v0.23 §92.3.4:传入 config(repo_layout 权威 + 两层探测)
339
+ const config = loadConfig(workspaceRoot);
340
+ const subRoot = findSubRepo(workspaceRoot, head, config);
341
+ if (!subRoot) {
342
+ // v0.23 §92.3.5:文案区分「SHA 不存在」与「不在已扫描仓中」——后者给出扫描清单与 --repo 指引
343
+ const { repos, unresolved, source } = resolveCodeRepos(workspaceRoot, config);
344
+ const scanned = repos.length ? repos.map(r => r.relPath).join(', ') : '(none)';
345
+ throw new Error(
346
+ `${primaryError.message}\n`
347
+ + ` Scanned repos (source=${source}, ${repos.length}): ${scanned}\n`
348
+ + (unresolved.length
349
+ ? ` Unresolved repo_layout.repos entries (${unresolved.length}): ${unresolved.map(u => u.key).join(', ')}\n`
350
+ : '')
351
+ + ' If the commit lives in a nested repo not listed above, pass --repo <abs-path>.',
352
+ );
353
+ }
339
354
 
340
355
  resolvedBase = resolveGitCommit(subRoot, base, 'base');
341
356
  resolvedHead = resolveGitCommit(subRoot, head, 'head');
@@ -11,12 +11,16 @@
11
11
  * - ensure-branch.mjs (workspace root detection for worktree isolation)
12
12
  * - cmd-deisolate.mjs (workspace root detection for deisolation)
13
13
  *
14
+ * v0.23 §92:仓清单解析收敛到本模块——`resolveCodeRepos` 是唯一入口
15
+ * (config `repo_layout.repos` 权威 → 两层探测 fallback),消费方不再各自枚举目录;
16
+ * `parsePorcelainPaths` 是 porcelain 路径解析的唯一实现(v0.23 §93)。
17
+ *
14
18
  * @module git-utils
15
19
  */
16
20
 
17
21
  import { execFileSync } from 'node:child_process';
18
- import { existsSync, readdirSync, realpathSync } from 'node:fs';
19
- import { isAbsolute, join, resolve, sep } from 'node:path';
22
+ import { existsSync, readdirSync, realpathSync, statSync } from 'node:fs';
23
+ import { basename, isAbsolute, join, relative, resolve, sep } from 'node:path';
20
24
 
21
25
  /**
22
26
  * Detect the workspace root from a changeDir path by looking for the
@@ -65,26 +69,180 @@ export function getGitRoot(path) {
65
69
  * @returns {string|null} Absolute path to the matching sub-repo root,
66
70
  * or null if no sub-repo contains the revision
67
71
  */
68
- export function findSubRepo(workspaceRoot, revision) {
72
+ export function findSubRepo(workspaceRoot, revision, config = null) {
73
+ // v0.23 §92.3.4:改为遍历统一解析结果(config 权威 + 两层探测),不再自行扫直接子目录。
74
+ for (const repo of resolveCodeRepos(workspaceRoot, config).repos) {
75
+ try {
76
+ execFileSync('git', ['-C', repo.absPath, 'rev-parse', '--verify', `${revision}^{commit}`], {
77
+ stdio: ['ignore', 'ignore', 'ignore'],
78
+ });
79
+ // Resolve symlinks (macOS /var → /private/var) for consistency with git paths
80
+ return realpathSync(repo.absPath);
81
+ } catch {
82
+ // Not in this repo, continue scanning
83
+ }
84
+ }
85
+ return null;
86
+ }
87
+
88
+ /* ============ v0.23 §92:仓清单统一解析 ============ */
89
+
90
+ /** 非代码仓库目录(team-flow 产物/基础设施),探测时排除(与 conventions-generator 口径一致) */
91
+ export const NON_REPO_DIRS = new Set([
92
+ 'changes', '.team-flow', 'docs', 'doc', 'node_modules', '.worktrees',
93
+ 'requirement', 'prototype', 'specs', 'data', 'path', 'template',
94
+ ]);
95
+
96
+ /**
97
+ * @param {string} name 目录名
98
+ * @param {{excludeNonRepoDirs?: boolean}} [options] 是否套用 NON_REPO_DIRS 排除表(默认 true)。
99
+ * 枚举「工作区」时须为 true(避免把 docs/changes 等产物目录误当代码仓);
100
+ * 枚举「已隔离的 worktree 目录」时应为 false——那里的结构由 ensure-branch 按 config
101
+ * 权威清单创建,仓名可能恰是 NON_REPO_DIRS 中的词(data / prototype / docs / …)。
102
+ */
103
+ function isExcludedDir(name, { excludeNonRepoDirs = true } = {}) {
104
+ if (name.startsWith('.') || name === 'node_modules') return true;
105
+ return excludeNonRepoDirs && NON_REPO_DIRS.has(name);
106
+ }
107
+
108
+ function hasGitEntry(dir) {
109
+ return existsSync(join(dir, '.git'));
110
+ }
111
+
112
+ function isDirectory(p) {
113
+ try {
114
+ return statSync(p).isDirectory();
115
+ } catch {
116
+ return false;
117
+ }
118
+ }
119
+
120
+ /**
121
+ * 探测 root 下的代码仓库,支持两层布局(v0.23 §92.3.3)。
122
+ *
123
+ * 层级 1:`root/<name>`(自身含 .git)
124
+ * 层级 2:`root/<container>/<name>`(container 自身无 .git,但含带 .git 的子目录)
125
+ * —— 覆盖 `service/<repo>`、`apps/<repo>`、`packages/<repo>` 等常见布局。
126
+ *
127
+ * 深度上限 2:现有案例均为 2 层,更深无依据且易误判(node_modules 等已排除)。
128
+ *
129
+ * @param {string} root 工作区根
130
+ * @param {{excludeNonRepoDirs?: boolean}} [options] 见 `isExcludedDir`;默认 true(工作区探测)。
131
+ * 枚举已隔离的 worktree 目录时传 false——口径须与创建方(ensure-branch 的 config 权威分支)一致。
132
+ * @returns {Array<{name: string, relPath: string, absPath: string, isGitRepo: boolean}>} 按 relPath 排序
133
+ * 探测命中的条目必然含 `.git`,故 isGitRepo 恒为 true;与 `resolveCodeRepos` 的 config 分支
134
+ * 区分——后者可能是无 `.git` 的 monorepo 模块,消费方须按 isGitRepo 判断能否做 git 操作。
135
+ */
136
+ export function listCodeReposDeep(root, { excludeNonRepoDirs = true } = {}) {
69
137
  let entries;
70
138
  try {
71
- entries = readdirSync(workspaceRoot, { withFileTypes: true });
139
+ entries = readdirSync(root, { withFileTypes: true });
72
140
  } catch {
73
- return null;
141
+ return [];
74
142
  }
143
+
144
+ const found = [];
145
+ const push = (name, relPath, absPath) => {
146
+ found.push({ name, relPath, absPath, isGitRepo: true });
147
+ };
148
+
75
149
  for (const entry of entries) {
76
- if (!entry.isDirectory()) continue;
77
- const candidate = join(workspaceRoot, entry.name);
78
- if (!existsSync(join(candidate, '.git'))) continue;
150
+ if (!entry.isDirectory() || isExcludedDir(entry.name, { excludeNonRepoDirs })) continue;
151
+ const candidate = join(root, entry.name);
152
+
153
+ if (hasGitEntry(candidate)) {
154
+ push(entry.name, entry.name, candidate);
155
+ continue;
156
+ }
157
+
158
+ // 层级 2:容器目录(自身非仓,但子目录可能是仓)
159
+ let children;
79
160
  try {
80
- execFileSync('git', ['-C', candidate, 'rev-parse', '--verify', `${revision}^{commit}`], {
81
- stdio: ['ignore', 'ignore', 'ignore'],
82
- });
83
- // Resolve symlinks (macOS /var → /private/var) for consistency with git paths
84
- return realpathSync(candidate);
161
+ children = readdirSync(candidate, { withFileTypes: true });
85
162
  } catch {
86
- // Not in this sub-repo, continue scanning
163
+ continue;
164
+ }
165
+ for (const child of children) {
166
+ if (!child.isDirectory() || isExcludedDir(child.name, { excludeNonRepoDirs })) continue;
167
+ const childPath = join(candidate, child.name);
168
+ if (hasGitEntry(childPath)) {
169
+ push(child.name, `${entry.name}/${child.name}`, childPath);
170
+ }
87
171
  }
88
172
  }
173
+
174
+ return found.sort((a, b) => a.relPath.localeCompare(b.relPath));
175
+ }
176
+
177
+ /**
178
+ * 统一仓清单解析(v0.23 §92.3.1):config 权威 → 探测 fallback。
179
+ *
180
+ * `repo_layout.repos` 的格式兼容(LT 2026-09-09:尽量都能兼容识别)——
181
+ * key 与 value 均可能是相对路径 / 绝对路径 / 人类描述(人工修正产物),逐条按序尝试:
182
+ * 1. key 作相对路径 → `<root>/<key>`
183
+ * 2. value 作绝对路径
184
+ * 3. value 作相对路径 → `<root>/<value>`
185
+ *
186
+ * 命中判据为「**目录存在**」——repos 条目既可能是独立 git 仓(含 .git),
187
+ * 也可能是 monorepo 风格的 Java 模块(仅含 pom.xml,见 multi-repo-support-design §3.1);
188
+ * 是否可做 git 操作由消费方按返回项的 `isGitRepo` 自行判断。全部落空 → 计入
189
+ * unresolved(调用方须告警,不得静默)。
190
+ *
191
+ * 解析结果为空时回退探测,避免 config 与实况矛盾时直接失效。
192
+ *
193
+ * @param {string} workspaceRoot
194
+ * @param {object|null} config 已加载的 team-flow.config.json 内容
195
+ * @returns {{repos: Array<{name,relPath,absPath,isGitRepo}>, unresolved: Array<{key,value}>, source: 'config'|'probe'}}
196
+ */
197
+ export function resolveCodeRepos(workspaceRoot, config = null) {
198
+ const declared = config?.repo_layout?.repos;
199
+ const repos = [];
200
+ const unresolved = [];
201
+
202
+ if (declared && typeof declared === 'object' && !Array.isArray(declared)) {
203
+ for (const [key, value] of Object.entries(declared)) {
204
+ const hit = resolveDeclaredRepo(workspaceRoot, key, value);
205
+ if (hit) repos.push(hit);
206
+ else unresolved.push({ key, value });
207
+ }
208
+ }
209
+
210
+ if (repos.length > 0) {
211
+ return { repos: repos.sort((a, b) => a.relPath.localeCompare(b.relPath)), unresolved, source: 'config' };
212
+ }
213
+ return { repos: listCodeReposDeep(workspaceRoot), unresolved, source: 'probe' };
214
+ }
215
+
216
+ function resolveDeclaredRepo(workspaceRoot, key, value) {
217
+ const candidates = [join(workspaceRoot, key)];
218
+ if (typeof value === 'string' && value) {
219
+ candidates.push(isAbsolute(value) ? value : join(workspaceRoot, value));
220
+ }
221
+ for (const dir of candidates) {
222
+ if (!isDirectory(dir)) continue;
223
+ return {
224
+ name: basename(dir),
225
+ relPath: relative(workspaceRoot, dir) || key,
226
+ absPath: dir,
227
+ isGitRepo: hasGitEntry(dir),
228
+ };
229
+ }
89
230
  return null;
90
231
  }
232
+
233
+ /**
234
+ * 解析 `git status --porcelain` 输出为路径数组(v0.23 §93.3.1)。
235
+ *
236
+ * 格式为 `XY <path>`——X/Y 各占一位,**未暂存修改的 X 位是空格**,因此不能用
237
+ * `^\S+\s+` 剥离(该正则会漏掉 ` M path`,正是 v0.51.0 前 arch-merge 误报的根因)。
238
+ * 以 `line.slice(3)` 按固定宽度切片,并还原引号包裹与转义空格。
239
+ *
240
+ * @param {string} output git status --porcelain 的原始输出
241
+ * @returns {string[]} 仓库相对路径
242
+ */
243
+ export function parsePorcelainPaths(output) {
244
+ return String(output || '')
245
+ .split('\n')
246
+ .filter(Boolean)
247
+ .map(line => line.slice(3).replace(/^"|"$/g, '').replace(/\\ /g, ' '));
248
+ }
@@ -31,7 +31,7 @@ import { join, isAbsolute, relative, resolve, normalize, sep } from 'node:path';
31
31
  import { readState, writeState } from './state-loader.mjs';
32
32
  import { getOverlayPaths } from './sdd-overlay.mjs';
33
33
  import { detectGlaf4Delegation, detectTechStack, writeGlaf4DevConfig } from './conventions-generator.mjs';
34
- import { detectWorkspaceRoot, getGitRoot } from './git-utils.mjs';
34
+ import { detectWorkspaceRoot, getGitRoot, resolveCodeRepos } from './git-utils.mjs';
35
35
  import { loadConfig } from './config-loader.mjs';
36
36
 
37
37
  // DP-4 委托模式合法枚举(对应 glaf4-dev 七模式中可委托 change 的六种)
@@ -94,7 +94,10 @@ export function detectAndWriteConfig(root, delegationMode, targetRepo) {
94
94
  if (targetRepo) {
95
95
  probeRoot = targetRepo;
96
96
  } else if (repoLayout?.repos && Object.keys(repoLayout.repos).length > 0) {
97
- repos = Object.values(repoLayout.repos);
97
+ // v0.23 §92.3.4:复用统一解析器——`Object.values(repos)` 在 value 为人类描述时
98
+ // 会把描述当路径去探测(emp-auth 实况),导致聚合判定静默失效。
99
+ const resolved = resolveCodeRepos(projectRoot, config).repos;
100
+ repos = resolved.length > 0 ? resolved.map(r => r.absPath) : null;
98
101
  } else if (!detectTechStack(root).language) {
99
102
  probeRoot = projectRoot;
100
103
  }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * severity — 经验严重度的唯一真相源(v0.23 §91,v0.51.0)
3
+ *
4
+ * 背景:severity 是**有序等级**,但此前"序"在 4 处各自定义——solutions-promote 用集合相等
5
+ * 表达「≥ medium」(`severity === 'high' || severity === 'medium'`)+ 本地 SEVERITY_LADDER,
6
+ * solutions-index-gen 与 solutions-inject 各自重复一份 SEVERITY_ORDER——导致 critical 在
7
+ * **晋升 / 排序 / 升级阶梯**三处同时失效。新增等级或调整序只改本文件。
8
+ *
9
+ * 约定:
10
+ * - SEVERITY_ORDER 数值升序 = 严重度降序(0 最高);比较一律走 severityRank(),禁止集合相等
11
+ * - 未知值排最后(与历史 `?? 3` 行为等价),不阻断存量数据
12
+ *
13
+ * @module severity
14
+ */
15
+
16
+ /** 合法取值,按严重度降序(供 CLI 校验与提示文本使用) */
17
+ export const SEVERITY_VALUES = Object.freeze(['critical', 'high', 'medium', 'low']);
18
+
19
+ export const SEVERITY_ORDER = Object.freeze({
20
+ critical: 0,
21
+ high: 1,
22
+ medium: 2,
23
+ low: 3,
24
+ });
25
+
26
+ /** 重复命中时的升级阶梯(v0.49.0 §83.3.5 落盘;v0.23 §91 补 critical 封顶) */
27
+ export const SEVERITY_LADDER = Object.freeze({
28
+ low: 'medium',
29
+ medium: 'high',
30
+ high: 'critical',
31
+ });
32
+
33
+ /** 未知值排序位:排在 low 之后 */
34
+ export const SEVERITY_UNKNOWN_RANK = SEVERITY_VALUES.length;
35
+
36
+ /**
37
+ * 取排序位。数值越小越严重;未知值排最后。
38
+ * @param {string} sev
39
+ * @returns {number}
40
+ */
41
+ export function severityRank(sev) {
42
+ return SEVERITY_ORDER[sev] ?? SEVERITY_UNKNOWN_RANK;
43
+ }
44
+
45
+ /**
46
+ * 是否为合法 severity 取值。
47
+ * @param {string} sev
48
+ * @returns {boolean}
49
+ */
50
+ export function isSeverity(sev) {
51
+ return Object.hasOwn(SEVERITY_ORDER, sev);
52
+ }
53
+
54
+ /**
55
+ * sev 是否达到 min 及以上(含 min 自身)。
56
+ * @param {string} sev
57
+ * @param {string} min
58
+ * @returns {boolean}
59
+ */
60
+ export function meetsMinSeverity(sev, min) {
61
+ return severityRank(sev) <= severityRank(min);
62
+ }
63
+
64
+ /**
65
+ * 升一档;已是最高档(critical)或未知值时原样返回。
66
+ * @param {string} sev
67
+ * @returns {string}
68
+ */
69
+ export function nextSeverity(sev) {
70
+ return SEVERITY_LADDER[sev] ?? sev;
71
+ }
@@ -4,6 +4,7 @@
4
4
  *
5
5
  * v0.5 复利贯穿机制脚本
6
6
  * 用法:tf solutions capture --phase <p> --domain <d> --type <t> --severity <s> --summary "<text>"
7
+ * --severity 合法取值:critical | high | medium | low(v0.23 §91.3.4 起校验,非法值报错退出)
7
8
  *
8
9
  * 功能:
9
10
  * 1. 在对应阶段目录下创建经验文件
@@ -14,6 +15,7 @@
14
15
  import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync } from 'node:fs';
15
16
  import { join } from 'node:path';
16
17
  import { pathToFileURL } from 'node:url';
18
+ import { SEVERITY_VALUES, isSeverity } from './severity.mjs';
17
19
 
18
20
  const PHASES = ['prd', 'plan', 'architecture', 'prototype', 'spec', 'build', 'review', 'cross-phase'];
19
21
  const MAX_INDEX_LINES = 150;
@@ -30,14 +32,26 @@ export function run(args = {}) {
30
32
  const phase = args.phase || 'cross-phase';
31
33
  const domain = args.domain || 'general';
32
34
  const type = args.type || 'insight';
35
+ // v0.23 §91.3.4:仅校验「显式传入」的 severity——缺省 medium 行为不变。
36
+ // 此前无校验,非法值静默流入 INDEX,再在 promote/index-gen/inject 三处各自降级。
37
+ const severityExplicit = args.severity !== undefined && args.severity !== null;
33
38
  const severity = args.severity || 'medium';
34
39
  const summary = args.summary || '(no summary)';
35
40
  const source = args.source || '';
36
41
  const dir = args.dir || 'docs/solutions';
37
42
 
43
+ // 注:process.exit 后补 return——真实 CLI 下 exit 即终止;测试环境 mock exit 时
44
+ // 不得继续执行(否则校验失败仍会写入文件,v0.23 §91.3.4 横展)。
38
45
  if (!PHASES.includes(phase)) {
39
46
  console.error(`Invalid phase: ${phase}. Valid: ${PHASES.join(', ')}`);
40
47
  process.exit(1);
48
+ return;
49
+ }
50
+
51
+ if (severityExplicit && !isSeverity(severity)) {
52
+ console.error(`Invalid severity: ${severity}. Valid: ${SEVERITY_VALUES.join(', ')}`);
53
+ process.exit(1);
54
+ return;
41
55
  }
42
56
 
43
57
  // 确保目录存在
@@ -16,9 +16,9 @@
16
16
  import { readFileSync, writeFileSync, readdirSync, statSync, existsSync } from 'node:fs';
17
17
  import { join } from 'node:path';
18
18
  import { pathToFileURL } from 'node:url';
19
+ import { severityRank } from './severity.mjs';
19
20
 
20
21
  const PHASES = ['prd', 'plan', 'prototype', 'spec', 'build', 'review', 'cross-phase'];
21
- const SEVERITY_ORDER = { high: 0, medium: 1, low: 2 };
22
22
  const MAX_INDEX_LINES = 150;
23
23
 
24
24
  function parseFrontmatter(content) {
@@ -79,7 +79,7 @@ export function run(args = {}) {
79
79
 
80
80
  // 排序:severity 降序,date 降序
81
81
  entries.sort((a, b) => {
82
- const sevDiff = (SEVERITY_ORDER[a.severity] ?? 3) - (SEVERITY_ORDER[b.severity] ?? 3);
82
+ const sevDiff = severityRank(a.severity) - severityRank(b.severity);
83
83
  if (sevDiff !== 0) return sevDiff;
84
84
  return b.date.localeCompare(a.date);
85
85
  });
@@ -15,8 +15,7 @@
15
15
  import { readFileSync, existsSync } from 'node:fs';
16
16
  import { join } from 'node:path';
17
17
  import { pathToFileURL } from 'node:url';
18
-
19
- const SEVERITY_ORDER = { high: 0, medium: 1, low: 2 };
18
+ import { severityRank } from './severity.mjs';
20
19
 
21
20
  export function run(args = {}) {
22
21
  const phase = args.phase || 'cross-phase';
@@ -49,7 +48,7 @@ export function run(args = {}) {
49
48
  }
50
49
 
51
50
  // 按 severity 降序排序,取 top-5
52
- entries.sort((a, b) => (SEVERITY_ORDER[a.severity] ?? 3) - (SEVERITY_ORDER[b.severity] ?? 3));
51
+ entries.sort((a, b) => severityRank(a.severity) - severityRank(b.severity));
53
52
  const top5 = entries.slice(0, 5);
54
53
 
55
54
  if (top5.length === 0) {
@@ -21,6 +21,16 @@
21
21
  import { readFileSync, writeFileSync, existsSync, appendFileSync, mkdirSync } from 'node:fs';
22
22
  import { join, basename } from 'node:path';
23
23
  import { pathToFileURL } from 'node:url';
24
+ import { meetsMinSeverity, nextSeverity } from './severity.mjs';
25
+
26
+ /**
27
+ * 剥离 YAML 行尾注释(空格 + `#` 起始)——v0.23 §91 硬化:
28
+ * templates/learnings.md 的取值说明写在行尾注释里,若被逐字复制进 learnings.md,
29
+ * 注释会随取值一起进入 frontmatter,导致 severity 被判为未知值而静默不晋升。
30
+ */
31
+ function stripInlineComment(raw) {
32
+ return raw.replace(/\s+#.*$/, '').trim();
33
+ }
24
34
 
25
35
  function parseFrontmatter(content) {
26
36
  // learnings.md 中每个条目以 `## 标题` 开头,frontmatter 紧跟标题行之后;
@@ -31,7 +41,7 @@ function parseFrontmatter(content) {
31
41
  for (const line of match[1].split('\n')) {
32
42
  const idx = line.indexOf(':');
33
43
  if (idx > 0) {
34
- fm[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
44
+ fm[line.slice(0, idx).trim()] = stripInlineComment(line.slice(idx + 1));
35
45
  }
36
46
  }
37
47
  return fm;
@@ -63,8 +73,6 @@ function renderEntryBody(learning) {
63
73
  return `## ${learning.title}\n\n${body}`;
64
74
  }
65
75
 
66
- const SEVERITY_LADDER = { low: 'medium', medium: 'high' };
67
-
68
76
  /**
69
77
  * v0.49.0 §83.3.5:重复命中时按 promotion-rules.md 承诺真正落盘(原实现只 `updated++`,
70
78
  * 不更新 INDEX 行、不标记条目)——升级 INDEX 行 severity + 条目文件 confirmed 计数。
@@ -77,12 +85,12 @@ function confirmExistingEntry(solutionsDir, indexContent, domain, type) {
77
85
  if (cols.length < 9 || cols[3] !== domain || cols[4] !== type) continue;
78
86
 
79
87
  const currentSeverity = cols[5];
80
- const nextSeverity = SEVERITY_LADDER[currentSeverity] ?? currentSeverity;
81
- if (nextSeverity !== currentSeverity) {
82
- lines[i] = lines[i].replace(`| ${currentSeverity} |`, `| ${nextSeverity} |`);
88
+ const upgraded = nextSeverity(currentSeverity);
89
+ if (upgraded !== currentSeverity) {
90
+ lines[i] = lines[i].replace(`| ${currentSeverity} |`, `| ${upgraded} |`);
83
91
  }
84
92
  markEntryConfirmed(join(solutionsDir, cols[7]));
85
- return { indexContent: lines.join('\n'), from: currentSeverity, to: nextSeverity, file: cols[7] };
93
+ return { indexContent: lines.join('\n'), from: currentSeverity, to: upgraded, file: cols[7] };
86
94
  }
87
95
  return null;
88
96
  }
@@ -139,7 +147,7 @@ export function run(args = {}) {
139
147
  const domain = learning.fm?.domain || 'general';
140
148
 
141
149
  // 晋升条件:severity ≥ medium 且 type = pitfall/pattern
142
- const sevOk = severity === 'high' || severity === 'medium';
150
+ const sevOk = meetsMinSeverity(severity, 'medium');
143
151
  const typeOk = type === 'pitfall' || type === 'pattern';
144
152
  if (!sevOk || !typeOk) {
145
153
  skipped.push({
@@ -25,14 +25,21 @@ tf solutions promote <change-dir>
25
25
 
26
26
  | Severity | 含义 | 晋升行为 |
27
27
  |----------|------|----------|
28
- | high | 阻塞性问题或关键模式 | 必须晋升 |
28
+ | critical | 安全 / 数据 / 合规级高危经验(凭证熵源、越权、数据损坏等) | 满足 type 条件时必然晋升;INDEX 重建后置顶 |
29
+ | high | 阻塞性问题或关键模式 | 满足 type 条件时必然晋升 |
29
30
  | medium | 有显著影响的问题或可复用模式 | 满足 type 条件时晋升 |
30
31
  | low | 轻微问题或局部洞察 | 不晋升,保留在 change 级别 |
31
32
 
33
+ > **晋升是 severity × type 的合取判定**(`severity ≥ medium` **且** `type ∈ {pitfall, pattern}`)——
34
+ > 任一维度不满足即 skipped,critical 也不例外(如 `critical` + `insight` 不晋升)。
35
+ > 「置顶」需 `tf solutions index-gen` 重建 INDEX 后生效:promote 只 append 或原地改 severity,不重排。
36
+ > 取值域与「序」由 `scripts/lib/severity.mjs` 唯一定义(v0.23 §91);新增等级只改该文件,不在消费点各自实现。
37
+ > 取值**区分大小写**,须全小写(`Critical` 会被判为未知值:排最后且不晋升)。
38
+
32
39
  ### 已确认模式
33
40
 
34
41
  当晋升的经验与全局 INDEX 中已有条目的 domain + type 匹配时:
35
42
  1. 不创建新文件
36
43
  2. 在已有条目中标记"已确认模式"
37
- 3. severity 升级(low → medium, mediumhigh
44
+ 3. severity 升级(low → medium high critical
38
45
  4. 更新 INDEX.md 中对应行的 severity 字段
@@ -32,7 +32,7 @@ docs/solutions/
32
32
  phase: prd # 阶段标签:prd | plan | prototype | spec | build | review | cross-phase
33
33
  domain: auth # 领域标签(与 PRD/change 的领域对应)
34
34
  type: pitfall # pitfall | pattern | decision | insight
35
- severity: high # high | medium | low
35
+ severity: high # critical | high | medium | low(序定义于 scripts/lib/severity.mjs)
36
36
  date: 2026-07-15
37
37
  source: change-id # 来源 change(晋升时保留)
38
38
  ---
@@ -10,7 +10,7 @@
10
10
  ## 淘汰规则(Eviction)
11
11
 
12
12
  当 INDEX.md 超过 150 行硬上限时:
13
- - 按 severity 降序排列(high > medium > low
13
+ - 按 severity 降序排列(critical > high > medium > low;序定义于 `scripts/lib/severity.mjs`)
14
14
  - 保留前 150 条
15
15
  - 淘汰 low severity 且 date 最早的条目
16
16
  - 被淘汰的经验文件保留在阶段目录中(可从文件系统找回),仅从 INDEX.md 移除
@@ -207,7 +207,9 @@ tf solutions promote <change-dir>
207
207
 
208
208
  This checks the change's `learnings.md` (change root; v0.49.0 §83.3.5 path unification) for entries meeting promotion criteria:
209
209
  - severity ≥ medium AND type = pitfall/pattern → promote to global `docs/solutions/`
210
- - domain+type matches existing global entry → mark as "confirmed pattern", upgrade severity
210
+ - domain+type matches existing global entry → mark as "confirmed pattern", upgrade severity (low → medium → high → critical)
211
+
212
+ Valid `severity` values: `critical` | `high` | `medium` | `low` (ordered; lowercase, case-sensitive; v0.23 §91 — the order is defined once in `scripts/lib/severity.mjs`).
211
213
 
212
214
  Report promotion results in the closing summary. Promotion is advisory — failures do not block closing.
213
215
 
@@ -26,7 +26,7 @@ tf solutions promote <change-dir>
26
26
 
27
27
  Promotion criteria:
28
28
  - severity ≥ medium AND type = pitfall/pattern → promote to global `docs/solutions/`
29
- - domain+type matches existing global entry → mark as "confirmed pattern", upgrade severity
29
+ - domain+type matches existing global entry → mark as "confirmed pattern", upgrade severity (low → medium → high → critical)
30
30
 
31
31
  Report promotion results in the closing summary. Promotion is advisory — failures do not block closing.
32
32
 
@@ -25,7 +25,7 @@ team-flow 的 change 实施在 worktree 隔离环境(`tf isolate` → `.worktr
25
25
 
26
26
  ## 多仓库策略(Case A)
27
27
 
28
- 多仓库工作区(bff/ui 等独立 git 仓库)中,`tf deisolate` 自动识别 Case A 布局(`.worktrees/<change>/<repo>/`),逐 repo 执行合并。每个独立仓库是一个独立 git 操作,互不依赖。
28
+ 多仓库工作区(bff/ui 等独立 git 仓库)中,`tf deisolate` 自动识别 Case A 布局(`.worktrees/<change>/<repo>/`,含 `service/<repo>` 两层嵌套,v0.23 §92.3.7),逐 repo 执行合并。每个独立仓库是一个独立 git 操作,互不依赖。
29
29
 
30
30
  ## 依赖链顺序
31
31
 
@@ -16,7 +16,7 @@
16
16
  phase: cross-phase # 阶段名(prd/plan/architecture/prototype/spec/build/review/cross-phase)或具体领域阶段
17
17
  domain: <领域> # 如 jest / sdd / spring-boot;注入时按领域过滤
18
18
  type: pitfall # pitfall | pattern | insight
19
- severity: medium # high | medium | low
19
+ severity: medium # critical | high | medium | low(小写;critical = 安全/数据/合规级高危经验)
20
20
  date: YYYY-MM-DD
21
21
  ---
22
22
 
@@ -45,7 +45,7 @@ date: YYYY-MM-DD
45
45
  | 条件 | 行为 |
46
46
  |---|---|
47
47
  | `severity` ≥ medium 且 `type` = pitfall / pattern | 晋升到全局 `docs/solutions/<phase>/`,并追加 INDEX 行 |
48
- | 与全局 INDEX 已有条目 domain + type 匹配 | 不新建文件:升级已有条目 severity(low→medium→high)+ 条目 frontmatter `confirmed` 计数 +1 + 更新 INDEX 行 |
48
+ | 与全局 INDEX 已有条目 domain + type 匹配 | 不新建文件:升级已有条目 severity(low medium high → critical)+ 条目 frontmatter `confirmed` 计数 +1 + 更新 INDEX 行 |
49
49
  | 其他 | 保留在本文件;promote 输出中列为 skipped 并附原因 |
50
50
 
51
51
  **条目质量要求**:写「正确做法」前先验证它在系统层面成立——局部观察得出的 workaround 若被沉淀为通则,会被后续 change 当经验复用,反而固化缺陷(v0.49.0 §83.2.2 的实证教训)。