@xulthekl/team-flow 0.49.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 (61) 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 +67 -0
  9. package/GEMINI.md +1 -1
  10. package/INSTALL.md +2 -2
  11. package/README.md +1 -1
  12. package/agents/architecture-design.md +1 -1
  13. package/agents/code-reviewer.md +1 -1
  14. package/docs/README_en.md +2 -2
  15. package/docs/artifact-contract.md +2 -0
  16. package/docs/decision-points.md +23 -0
  17. package/docs/state-machine.md +1 -1
  18. package/docs/usage-guide.md +7 -4
  19. package/gemini-extension.json +1 -1
  20. package/hooks/session-start +2 -2
  21. package/llms.txt +1 -1
  22. package/package.json +2 -1
  23. package/plugin.json +1 -1
  24. package/scripts/ensure-branch.mjs +58 -32
  25. package/scripts/guard/checks/tasks-complete.mjs +39 -3
  26. package/scripts/guard/checks/tasks-gate-exemptions.mjs +49 -0
  27. package/scripts/lib/arch-merge.mjs +32 -9
  28. package/scripts/lib/arch-precheck.mjs +190 -0
  29. package/scripts/lib/cmd-arch.mjs +5 -1
  30. package/scripts/lib/cmd-deisolate.mjs +15 -16
  31. package/scripts/lib/cmd-doctor.mjs +9 -2
  32. package/scripts/lib/cmd-publish.mjs +4 -7
  33. package/scripts/lib/cmd-state.mjs +2 -0
  34. package/scripts/lib/conventions-generator.mjs +9 -7
  35. package/scripts/lib/execution-plan.mjs +18 -3
  36. package/scripts/lib/git-utils.mjs +172 -14
  37. package/scripts/lib/glaf4-delegation.mjs +5 -2
  38. package/scripts/lib/severity.mjs +71 -0
  39. package/scripts/lib/solutions-capture.mjs +14 -0
  40. package/scripts/lib/solutions-index-gen.mjs +2 -2
  41. package/scripts/lib/solutions-inject.mjs +2 -3
  42. package/scripts/lib/solutions-promote.mjs +16 -8
  43. package/scripts/lib/state-loader.mjs +7 -0
  44. package/scripts/lint/rules/behavior-consistency.mjs +3 -1
  45. package/scripts/team-flow.mjs +8 -1
  46. package/skills/architecture-design/SKILL.md +17 -3
  47. package/skills/build-executor/SKILL.md +2 -2
  48. package/skills/build-executor/references/execution-modes.md +1 -1
  49. package/skills/build-executor/task-reviewer-prompt.md +5 -4
  50. package/skills/ce-compound/references/promotion-rules.md +9 -2
  51. package/skills/ce-compound/references/three-tier-index.md +1 -1
  52. package/skills/ce-compound/references/write-flow.md +1 -1
  53. package/skills/code-reviewer/SKILL.md +1 -1
  54. package/skills/code-reviewer/code-reviewer-prompt.md +5 -4
  55. package/skills/contract-builder/SKILL.md +35 -4
  56. package/skills/release-archivist/SKILL.md +14 -3
  57. package/skills/release-archivist/references/closing-procedures.md +1 -1
  58. package/skills/release-archivist/references/worktree-merge.md +1 -1
  59. package/skills/workflow-start/SKILL.md +18 -7
  60. package/skills/workflow-start/references/routing-rules.md +3 -1
  61. package/templates/learnings.md +2 -2
@@ -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({
@@ -74,6 +74,9 @@ const BUILTIN_DEFAULTS = {
74
74
  test_matrix_skip_reason: null,
75
75
  // Test evidence (v0.13 §50:tf test record 落盘的 runner 输出证据路径)
76
76
  test_evidence_path: null,
77
+ // Tasks gate (v0.22 §85:hotfix/tweak 跳过 spec-writer 时显式跳过 tasks.md)
78
+ tasks_skipped: null,
79
+ tasks_skip_reason: null,
77
80
  // 注意:schema_version 故意不在 BUILTIN_DEFAULTS 中(v0.13 §48.1)——
78
81
  // 它只由 `tf state init` 在 change 创建时打戳,字段缺失本身就是"存量 change"信号。
79
82
  };
@@ -196,6 +199,10 @@ export function writeState(changeDir, state) {
196
199
  lines.push('');
197
200
  lines.push('# === Test evidence (v0.13 §50) ===');
198
201
  lines.push(`test_evidence_path: ${state.test_evidence_path ?? 'null'}`);
202
+ lines.push('');
203
+ lines.push('# === Tasks gate (v0.22 §85) ===');
204
+ lines.push(`tasks_skipped: ${state.tasks_skipped ?? 'null'}`);
205
+ lines.push(`tasks_skip_reason: ${state.tasks_skip_reason ?? 'null'}`);
199
206
 
200
207
  fs.writeFileSync(filePath, lines.join('\n') + '\n', 'utf-8');
201
208
  }
@@ -25,7 +25,9 @@ export default {
25
25
 
26
26
  // Check 2: "route to" consistency with workflow-start
27
27
  if (skillName !== 'workflow-start') {
28
- const routeRefs = [...content.matchAll(/[Rr]oute to [`""]?([a-z-]+)[`""]?/g)];
28
+ // v0.22 §90:加 `(?![a-z\/-])` 排除路径式目标——原正则把 "route to `references/xxx.md`"
29
+ // 捕获为 skill 名 "references",产生 error 级误报(ce-plan SKILL.md:64)。
30
+ const routeRefs = [...content.matchAll(/[Rr]oute to [`""]?([a-z-]+)[`""]?(?![a-z\/-])/g)];
29
31
  for (const [, target] of routeRefs) {
30
32
  // This skill says "route to X" — workflow-start should have a corresponding rule
31
33
  // We flag this for manual verification (can't easily cross-check without loading workflow-start)
@@ -67,6 +67,8 @@ Commands:
67
67
  arch init [--mode reconstruction|design] [--baseline-ref <prd/vN/>]
68
68
  Stamp project-level arch_baseline into .team-flow/arch-state.json (v0.35.0 §59.4)
69
69
  arch show Show current project architecture baseline state
70
+ arch precheck <change-dir> [--json]
71
+ Emit deterministic architecture-gate evidence (v0.22 §88; evidence only, exit 0)
70
72
  arch-merge <change-dir> [--project-root <path>] [--dry-run]
71
73
  Merge architecture delta into global docs/architecture/
72
74
  test-merge <change-dir> [--project-root <path>] [--dry-run]
@@ -81,6 +83,9 @@ Commands:
81
83
  Detect project layout (single/monorepo/multi-repo) + code repos, write repo_layout config (v1.0 §3)
82
84
  test record <dir> --from <runner-output> [--runner auto|maven-surefire|jest|pytest]
83
85
  Record programmatic test evidence (v0.13 §50; feeds tests-passing gate)
86
+ jest requires --json output (e.g. npx jest --json --outputFile=<path>);
87
+ maven-surefire: console summary or surefire XML dir/file;
88
+ pytest: terminal summary or junit XML (--junitxml)
84
89
  config [options] Display or modify configuration
85
90
  config --resolve-model <profile> Resolve a configured model profile without switching models
86
91
  state <sub> <dir> Manage .team-flow.yaml state (init|check|transition|get|rebuild)
@@ -109,7 +114,9 @@ Commands:
109
114
  execution revise <change-dir> --mode sdd --confirm --reason <text> --wave <id>:<strategy>:<task,...> [--acknowledge-recommendation]
110
115
  Upgrade inline/batch to SDD, or replan existing SDD waves, as a new revision
111
116
  execution review <change-dir> --wave <id> --base <sha> --head <sha> --report <path> --verdict pass|fail
112
- Record one review receipt for a planned wave
117
+ Record one review receipt for a planned wave.
118
+ --report must resolve inside <change-dir>/.superpowers/sdd/reviews/
119
+ (the change review overlay; other paths are rejected)
113
120
  deisolate <change-dir> [--merge] [--force] [--clean] [--json]
114
121
  Show worktree status / merge branch back / clean worktree
115
122
  runtime check-update Run a portable update check for canonical skills
@@ -67,7 +67,7 @@ description: 基于 4A 企业架构 + DDD 领域驱动设计的架构/API/DB 设
67
67
 
68
68
  - `change-brief.md`(scope / AC / 技术方向)
69
69
  - `requirement/vN/plan.md` 高阶技术设计段(模块边界/技术选型/数据流/关键聚合划分)
70
- - `docs/architecture/iterations/vN/architecture.md`(产品级架构快照,**主输入**,v0.35.0)——BC 边界/聚合所有权/全局契约的唯一事实源
70
+ - `docs/architecture/iterations/vN/architecture.md`(产品级架构快照,**主输入**,v0.35.0)——BC 边界/聚合所有权/全局契约的唯一事实源;**Fast Path 下不读**(见 `### Fast Path`)
71
71
  - 全局 `docs/architecture/`(As-Is 实际态基线,已落地部分)
72
72
  - 现有 `specs/`(若有)
73
73
 
@@ -81,6 +81,20 @@ description: 基于 4A 企业架构 + DDD 领域驱动设计的架构/API/DB 设
81
81
  4. **API 变更**:是否涉及 API 新增/变更(端点、方法签名、请求响应 schema)
82
82
  5. **DB schema 变更**:是否涉及数据库表结构、字段、索引变更
83
83
 
84
+ ### Fast Path:precheck 证据驱动(v0.22 §88.3.2)
85
+
86
+ > **术语区分**:本节的 Fast Path 专指**架构判断门的快路径**,与 workflow-start 的 Fast-Path Routing(hotfix/tweak 路由)无关。
87
+ > **优先级**:本节条件优先于下文「执行流程」与「上下文加载协议」——Fast Path 命中时,那两节的架构文件读取要求一律不适用。
88
+
89
+ **先跑证据工具**:`tf arch precheck <change-dir> --json` —— 输出确定性证据(架构关键词的**变更语境**命中 / 否定命中 / 仅提及;改动文件按表现层/后端/配置/脚本/测试分类)。**正常调用退出码恒 0(缺参数属用法错误 exit 2),它是证据不是判断**。
90
+
91
+ | precheck 输出 | 路径 | 行为 |
92
+ |---|---|---|
93
+ | `signal: none`(无正向架构关键词 ∧ 无后端文件 ∧ 无 `.sql` ∧ 有表现层文件) | **Fast Path** | 只读 `change-brief.md` + precheck 输出;本路径**不读 `docs/architecture/` 下任何文件(含 `INDEX.md`)**;执行五项检查后返回 `skipped`,reason 引用 precheck 证据 |
94
+ | `signal: weak` / `strong` | 完整路径 | 现状(读快照 + 全局基线 + 完整五项检查) |
95
+
96
+ **红线**:Fast Path 下若发现 precheck 证据与 brief 不符(如 brief 明确提到新增聚合但 precheck 未命中),**必须回退完整路径**——判断权始终在本子代理,precheck 只用于缩小输入范围。
97
+
84
98
  ### 路由分流(v0.35.0 新增)
85
99
 
86
100
  当产品级架构快照存在时(已建档项目),判定结果再按"产品级决策 vs change 内实现细节"分流:
@@ -103,7 +117,7 @@ description: 基于 4A 企业架构 + DDD 领域驱动设计的架构/API/DB 设
103
117
  ### 执行流程
104
118
 
105
119
  ```
106
- 1. 读取输入(brief + plan + specs + 全局 ARCHITECTURE.md
120
+ 1. 读取输入(brief + plan + specs + 全局 ARCHITECTURE.md)——**Fast Path 裁剪为「brief + precheck 输出」**
107
121
  2. 执行五项检查
108
122
  3. 全部为否:
109
123
  → decision: skipped
@@ -196,7 +210,7 @@ architecture-design 执行时的上下文组装:
196
210
  1. 执行 `tf solutions inject --phase architecture`(失败静默)——注入 `docs/solutions/` 中 phase=architecture 的历史架构决策经验(BC 边界取舍/聚合划分理由/事件投影设计踩坑)
197
211
  2. 读取失败 / 无条目 → 静默跳过,不阻断(advisory 级,与 S1 复利注入同语义)
198
212
 
199
- **始终加载**:
213
+ **始终加载**(**Fast Path 除外**——见上文 `### Fast Path`,该路径不读 `docs/architecture/` 下任何文件):
200
214
  1. `Read docs/architecture/INDEX.md`(~50行摘要)
201
215
  2. `Read changes/<name>/change-brief.md`(如有)
202
216
  3. `Read changes/<name>/proposal.md`(如有)
@@ -142,9 +142,9 @@ For full/hotfix by default. Execute waves as dispatched by workflow-start.
142
142
 
143
143
  每个 wave 完成、通知 workflow-start 审查前,验证测试**实际执行**的数量(设计增强方案 v0.18 §76,来源:workflow-feedback 2026-08-06——64% 测试静默跳过但报告全绿):
144
144
 
145
- 1. 运行测试套件后,用 `tf test record <change-dir> --from <runner-output-file>` 解析**实际执行数量**(`Tests run: N`)。
145
+ 1. 运行测试套件后,用 `tf test record <change-dir> --from <runner-output-file>` 解析**实际执行数量**。**证据文件的产出形态随 runner 而异(v0.22 §86)**:jest 必须 `--json`(如 `npx jest --json --outputFile=<path>`;控制台汇总行 `Tests: N passed` 不被解析);maven-surefire 用控制台汇总行(`Tests run: N`)或 surefire XML 目录/文件;pytest 用 terminal summary 或 junit XML(`--junitxml=<path>`)。解析规则以 `scripts/lib/test-record.mjs` 为准(新增 runner 需先在该处注册)。
146
146
  2. 对照 test-matrix 当前 wave 覆盖的用例数(**分母排除 `test_tier=e2e`**——E2E case 由 Playwright 执行,不进入 `mvn test`/`npm test` 的 `Tests run: N`,口径与 code-reviewer Step 5b / release-archivist Step 2b 一致):实际执行数明显低于预期(< 70%)→ **警告 + 调查**(@Nested 静默跳过、测试未被发现、编译期跳过等),未查明前不得报告 "N tests pass"。
147
- 3. 报告引用实际执行数(`Tests run: N`),而非 BUILD SUCCESS 或编译通过数量。
147
+ 3. 报告引用实际执行数(按 runner 的计数口径),而非 BUILD SUCCESS 或编译通过数量。
148
148
 
149
149
  ### Per-Task Loop
150
150
  1. **Dispatch implementer**: Load the template with `tf runtime asset read skills/build-executor/implementer-prompt.md`. Extract task brief with `scripts/task-brief PLAN_FILE N`. Include: where task fits, brief path, interfaces from prior tasks, report file path.
@@ -35,7 +35,7 @@ Dispatch according to the persisted plan, review each planned wave, and run a fi
35
35
  1. Read the current plan with `tf execution show <change-dir> --json`; only waves with `current: true` and `eligible: true` may start.
36
36
  2. A `parallel` wave may dispatch independent tasks simultaneously only when the platform supports concurrent dispatch.
37
37
  3. A `serial` wave dispatches one task at a time in listed order.
38
- 4. After every wave, write a review report, then record one receipt:
38
+ 4. After every wave, write a review report **inside the change review overlay** `<change-dir>/.superpowers/sdd/reviews/` (v0.22 §86: other paths are rejected; the overlay is auto-created), then record one receipt:
39
39
  ```bash
40
40
  tf execution review <change-dir> \
41
41
  --wave <wave-id> --base <sha> --head <sha> --report <review-report-path> --verdict <pass|fail>
@@ -141,9 +141,10 @@ Subagent (general-purpose):
141
141
  ## Output Format
142
142
 
143
143
  Write your full review to [REVIEW_REPORT_FILE]. This distinct review report
144
- path must point to a non-empty, persisted review report before the
145
- controller records a receipt. After the verdict, provide the exact receipt
146
- command for the controller:
144
+ path must resolve inside `<change-dir>/.superpowers/sdd/reviews/` and point
145
+ to a non-empty, persisted review report before the controller records a
146
+ receipt. After the verdict, provide the exact receipt command for the
147
+ controller:
147
148
 
148
149
  ```bash
149
150
  tf execution review <change-dir> --wave [WAVE_ID] --base [BASE_SHA] --head [HEAD_SHA] --report [REVIEW_REPORT_FILE] --verdict <pass|fail>
@@ -184,7 +185,7 @@ Subagent (general-purpose):
184
185
  - `[BRIEF_FILE]` — REQUIRED: the task brief file (`scripts/task-brief PLAN N` prints the path; same file the implementer worked from)
185
186
  - `[GLOBAL_CONSTRAINTS]` — the binding requirements copied verbatim from the plan's Global Constraints section or the spec: exact values, formats, and stated relationships between components (not process rules — those are already in this template)
186
187
  - `[IMPLEMENTER_REPORT_FILE]` — REQUIRED: the file the implementer wrote its detailed report to
187
- - `[REVIEW_REPORT_FILE]` — REQUIRED: a distinct, persisted, non-empty file where the reviewer writes this review; this exact path is stored in the receipt
188
+ - `[REVIEW_REPORT_FILE]` — REQUIRED: a distinct, persisted, non-empty file under `<change-dir>/.superpowers/sdd/reviews/` (the overlay) where the reviewer writes this review; this exact path is stored in the receipt
188
189
  - `[BASE_SHA]` — commit before this task
189
190
  - `[HEAD_SHA]` — current commit
190
191
  - `[DIFF_FILE]` — REQUIRED: the path the controller wrote the review package to (`scripts/review-package BASE HEAD` prints the unique path it wrote; the package never enters the controller's context)
@@ -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
  ---