@xulthekl/team-flow 0.29.2 → 0.31.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 (53) hide show
  1. package/.claude/always/phase-guard.md +1 -1
  2. package/.claude-plugin/marketplace.json +3 -3
  3. package/.claude-plugin/plugin.json +2 -2
  4. package/.codex-plugin/plugin.json +1 -1
  5. package/.cursor-plugin/marketplace.json +1 -1
  6. package/.cursor-plugin/plugin.json +2 -2
  7. package/.github/plugin/marketplace.json +2 -2
  8. package/AGENTS.md +10 -4
  9. package/CHANGELOG.md +88 -0
  10. package/GEMINI.md +1 -1
  11. package/HANDOFF.md +4 -4
  12. package/INSTALL.md +1 -1
  13. package/README.md +5 -5
  14. package/agents/architecture-design.md +1 -0
  15. package/agents/build-executor.md +81 -0
  16. package/agents/contract-builder.md +78 -0
  17. package/agents/cross-change-consistency-checker.md +1 -1
  18. package/agents/need-explorer.md +67 -0
  19. package/agents/release-archivist.md +82 -0
  20. package/agents/spec-writer.md +83 -0
  21. package/docs/README_en.md +1 -1
  22. package/gemini-extension.json +1 -1
  23. package/hooks/session-start +2 -2
  24. package/llms.txt +1 -1
  25. package/package.json +2 -2
  26. package/plugin.json +2 -2
  27. package/scripts/ensure-branch.mjs +200 -42
  28. package/scripts/guard/checks/test-matrix-complete.mjs +67 -0
  29. package/scripts/guard/guard.mjs +5 -1
  30. package/scripts/lib/cmd-doctor.mjs +40 -1
  31. package/scripts/lib/cmd-state.mjs +33 -13
  32. package/scripts/lib/hash.mjs +11 -0
  33. package/scripts/lib/state-loader.mjs +34 -1
  34. package/scripts/lib/test-matrix-export.mjs +231 -0
  35. package/scripts/lib/test-merge.mjs +540 -0
  36. package/scripts/team-flow.mjs +6 -0
  37. package/skills/build-executor/SKILL.md +14 -4
  38. package/skills/build-executor/implementer-prompt.md +38 -3
  39. package/skills/code-reviewer/SKILL.md +28 -1
  40. package/skills/code-reviewer/code-reviewer-prompt.md +10 -0
  41. package/skills/contract-builder/SKILL.md +78 -0
  42. package/skills/need-explorer/SKILL.md +2 -0
  43. package/skills/release-archivist/SKILL.md +37 -2
  44. package/skills/spec-writer/SKILL.md +5 -1
  45. package/skills/test-strategy/SKILL.md +70 -0
  46. package/skills/test-strategy/references/adversarial-patterns.md +0 -0
  47. package/skills/test-strategy/references/complexity-grading.md +137 -0
  48. package/skills/test-strategy/references/design-methods-detail.md +183 -0
  49. package/skills/workflow-orchestrator/references/s1-path-router.md +4 -0
  50. package/skills/workflow-start/SKILL.md +29 -15
  51. package/skills/workflow-start/references/routing-rules.md +29 -6
  52. package/tests/lib/cmd-install-workbuddy.test.mjs +1 -1
  53. package/tests/lib/ensure-branch.test.mjs +52 -1
@@ -0,0 +1,540 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * test-merge — change 完成后将测试增量合并回全局 docs/test-ledger/
4
+ *
5
+ * v0.12 §43.3 定义,v0.31.0 实现为 CLI 子命令
6
+ * 用法:tf test-merge <change-dir> [--project-root <path>] [--dry-run]
7
+ *
8
+ * 功能(6 步流程):
9
+ * 1. preCheck — 校验 test-matrix.md 存在且非空
10
+ * 2. mergeBaselines — 按模块提取 case,增量合并到 baselines/{module}.md
11
+ * 3. resolveDeferred — 新 case 覆盖了旧 deferred 项 → 从 Deferred Items 移除
12
+ * 4. appendChangelog — 归档 test-matrix.md → changelog/{change-id}.md
13
+ * 5. rewriteIndex — 统计模块数/case 数/deferred 数,重写 INDEX.md
14
+ * 6. gitCommit — 单次原子提交
15
+ *
16
+ * 回写顺序:arch-merge → prototype-sync → test-merge → compound promotion
17
+ */
18
+
19
+ import { readFileSync, writeFileSync, existsSync, cpSync, mkdirSync, readdirSync } from 'node:fs';
20
+ import { join, basename, relative, resolve } from 'node:path';
21
+ import { execSync } from 'node:child_process';
22
+
23
+ /**
24
+ * 解析 CLI 参数数组为结构化对象
25
+ */
26
+ function parseArgv(argv) {
27
+ const parsed = { _: [] };
28
+ for (let i = 0; i < argv.length; i++) {
29
+ if (argv[i] === '--project-root' && argv[i + 1] && !argv[i + 1].startsWith('--')) {
30
+ parsed.projectRoot = argv[++i];
31
+ } else if (argv[i] === '--dry-run') {
32
+ parsed.dryRun = true;
33
+ } else if (!argv[i].startsWith('--')) {
34
+ parsed._.push(argv[i]);
35
+ }
36
+ }
37
+ return parsed;
38
+ }
39
+
40
+ /**
41
+ * 从 change 目录提取 change name
42
+ */
43
+ function extractChangeName(changeDir) {
44
+ return basename(changeDir.replace(/\/$/, ''));
45
+ }
46
+
47
+ /**
48
+ * Step 1: 预检 — 校验 test-matrix.md 存在且非空
49
+ */
50
+ function preCheck(changeDir) {
51
+ const matrixPath = join(changeDir, 'test-matrix.md');
52
+ if (!existsSync(matrixPath)) {
53
+ return { pass: false, reason: 'test-matrix.md not found — skipping test-merge' };
54
+ }
55
+ const content = readFileSync(matrixPath, 'utf-8');
56
+ if (content.trim().length === 0) {
57
+ return { pass: false, reason: 'test-matrix.md is empty — skipping test-merge' };
58
+ }
59
+ return { pass: true, content };
60
+ }
61
+
62
+ /**
63
+ * 从 test-matrix.md 提取 Summary 统计信息
64
+ */
65
+ function extractSummary(content) {
66
+ const summary = { totalCases: 0, modules: 0, unitCases: 0, integrationCases: 0, deferred: 0 };
67
+ const lines = content.split('\n');
68
+ for (const line of lines) {
69
+ const totalMatch = line.match(/Total cases:\s*(\d+)/i);
70
+ if (totalMatch) summary.totalCases = parseInt(totalMatch[1], 10);
71
+ const modulesMatch = line.match(/Modules covered:\s*(\d+)/i);
72
+ if (modulesMatch) summary.modules = parseInt(modulesMatch[1], 10);
73
+ const unitMatch = line.match(/Unit cases:\s*(\d+)/i);
74
+ if (unitMatch) summary.unitCases = parseInt(unitMatch[1], 10);
75
+ const intMatch = line.match(/Integration cases:\s*(\d+)/i);
76
+ if (intMatch) summary.integrationCases = parseInt(intMatch[1], 10);
77
+ const defMatch = line.match(/Deferred items:\s*(\d+)/i);
78
+ if (defMatch) summary.deferred = parseInt(defMatch[1], 10);
79
+ }
80
+ return summary;
81
+ }
82
+
83
+ /**
84
+ * 从 test-matrix.md 提取模块分段(## Cases 下的 ### 子段)
85
+ * 返回 { moduleName: sectionContent } 映射
86
+ */
87
+ function extractModuleSections(content) {
88
+ const sections = {};
89
+ const lines = content.split('\n');
90
+ let currentModule = null;
91
+ let currentLines = [];
92
+
93
+ for (const line of lines) {
94
+ if (line.startsWith('### ') && !line.startsWith('### #')) {
95
+ if (currentModule) {
96
+ sections[currentModule] = currentLines.join('\n');
97
+ }
98
+ currentModule = line.replace(/^###\s+/, '').trim();
99
+ currentLines = [line];
100
+ } else if (currentModule) {
101
+ currentLines.push(line);
102
+ }
103
+ }
104
+ if (currentModule) {
105
+ sections[currentModule] = currentLines.join('\n');
106
+ }
107
+ return sections;
108
+ }
109
+
110
+ /**
111
+ * 从 test-matrix.md 提取 Candidate Coverage Ledger
112
+ */
113
+ function extractCandidateLedger(content) {
114
+ const candidates = [];
115
+ const lines = content.split('\n');
116
+ let inLedger = false;
117
+
118
+ for (const line of lines) {
119
+ if (line.match(/## Candidate Coverage Ledger/i)) {
120
+ inLedger = true;
121
+ continue;
122
+ }
123
+ if (inLedger && line.startsWith('## ')) {
124
+ break;
125
+ }
126
+ if (inLedger && line.startsWith('|') && !line.match(/^\|\s*[-:]+/)) {
127
+ const cells = line.split('|').map(c => c.trim()).filter(Boolean);
128
+ if (cells.length >= 3 && cells[0] !== 'candidate') {
129
+ candidates.push({
130
+ candidate: cells[0],
131
+ category: cells[1] || 'unknown',
132
+ decision: cells[2] || 'unknown',
133
+ caseIds: cells[3] || '',
134
+ reason: cells[4] || '',
135
+ });
136
+ }
137
+ }
138
+ }
139
+ return candidates;
140
+ }
141
+
142
+ /**
143
+ * 从 test-matrix.md 提取 Deferred Items
144
+ */
145
+ function extractDeferredItems(content) {
146
+ const items = [];
147
+ const lines = content.split('\n');
148
+ let inDeferred = false;
149
+
150
+ for (const line of lines) {
151
+ if (line.match(/## Deferred Items/i)) {
152
+ inDeferred = true;
153
+ continue;
154
+ }
155
+ if (inDeferred && line.startsWith('## ')) {
156
+ break;
157
+ }
158
+ if (inDeferred && line.startsWith('|') && !line.match(/^\|\s*[-:]+/)) {
159
+ const cells = line.split('|').map(c => c.trim()).filter(Boolean);
160
+ if (cells.length >= 3 && cells[0] !== 'case_id') {
161
+ items.push({
162
+ caseId: cells[0],
163
+ behavior: cells[1] || '',
164
+ designMethod: cells[2] || '',
165
+ reason: cells[3] || '',
166
+ deferredSince: cells[4] || new Date().toISOString().slice(0, 10),
167
+ });
168
+ }
169
+ }
170
+ }
171
+ return items;
172
+ }
173
+
174
+ /**
175
+ * Step 2: mergeBaselines — 增量合并到 baselines/{module}.md
176
+ */
177
+ function mergeBaselines(ledgerDir, changeName, moduleSections, candidateLedger, deferredItems) {
178
+ const baselinesDir = join(ledgerDir, 'baselines');
179
+ if (!existsSync(baselinesDir)) {
180
+ mkdirSync(baselinesDir, { recursive: true });
181
+ }
182
+
183
+ const results = {};
184
+ const today = new Date().toISOString().slice(0, 10);
185
+
186
+ for (const [moduleName, sectionContent] of Object.entries(moduleSections)) {
187
+ const safeName = moduleName.toLowerCase().replace(/[^a-z0-9-]/g, '-');
188
+ const baselinePath = join(baselinesDir, `${safeName}.md`);
189
+ const moduleCandidates = candidateLedger.filter(c =>
190
+ c.candidate.toLowerCase().includes(moduleName.toLowerCase())
191
+ );
192
+ const moduleDeferred = deferredItems.filter(d =>
193
+ d.caseId.toLowerCase().startsWith(moduleName.toLowerCase().slice(0, 3))
194
+ );
195
+
196
+ if (existsSync(baselinePath)) {
197
+ // 增量合并:读取现有 baseline,追加新 case
198
+ const existing = readFileSync(baselinePath, 'utf-8');
199
+ const updated = mergeExistingBaseline(existing, sectionContent, changeName, today, moduleCandidates, moduleDeferred);
200
+ if (!updated) continue; // no changes
201
+ writeFileSync(baselinePath, updated, 'utf-8');
202
+ results[moduleName] = 'updated';
203
+ } else {
204
+ // 新建 baseline
205
+ const newBaseline = createNewBaseline(moduleName, sectionContent, changeName, today, moduleCandidates, moduleDeferred);
206
+ writeFileSync(baselinePath, newBaseline, 'utf-8');
207
+ results[moduleName] = 'created';
208
+ }
209
+ }
210
+ return results;
211
+ }
212
+
213
+ /**
214
+ * 创建新 baseline 文件
215
+ */
216
+ function createNewBaseline(moduleName, sectionContent, changeName, date, candidates, deferred) {
217
+ const lines = [];
218
+ lines.push(`# ${moduleName} Test Baseline`);
219
+ lines.push('');
220
+ lines.push(`> schema_version: 1`);
221
+ lines.push(`> last_updated_by_change: ${changeName}`);
222
+ lines.push(`> last_updated: ${date}`);
223
+ lines.push('');
224
+ lines.push('## Current Cases(当前态 — 覆盖式)');
225
+ lines.push('');
226
+ lines.push('| case_id | behavior | design_method | test_kind | test_tier | work_mode | test_file | test_method_name | source_change |');
227
+ lines.push('|---|---|---|---|---|---|---|---|---|');
228
+
229
+ // 从 sectionContent 提取 case 行并添加 source_change 列
230
+ const caseLines = sectionContent.split('\n').filter(l =>
231
+ l.startsWith('|') && !l.match(/^\|\s*[-:]+/) && !l.match(/\|\s*case_id/)
232
+ );
233
+ for (const line of caseLines) {
234
+ const cells = line.split('|').map(c => c.trim()).filter(Boolean);
235
+ if (cells.length >= 10) {
236
+ lines.push(`| ${cells[0]} | ${cells[1]} | ${cells[2]} | ${cells[5]} | ${cells[6]} | ${cells[8]} | ${cells[9]} | ${cells[10]} | ${changeName} |`);
237
+ }
238
+ }
239
+
240
+ if (deferred.length > 0) {
241
+ lines.push('');
242
+ lines.push('## Deferred Items');
243
+ lines.push('');
244
+ lines.push('| case_id | behavior | design_method | reason | deferred_since |');
245
+ lines.push('|---|---|---|---|---|');
246
+ for (const d of deferred) {
247
+ lines.push(`| ${d.caseId} | ${d.behavior} | ${d.designMethod} | ${d.reason} | ${d.deferredSince} |`);
248
+ }
249
+ }
250
+
251
+ lines.push('');
252
+ lines.push('## Evolution Log(append-only)');
253
+ lines.push('');
254
+ const caseCount = caseLines.length;
255
+ lines.push(`- **${date}** [${changeName}]: 新增 ${caseCount} case, defer ${deferred.length}`);
256
+
257
+ return lines.join('\n') + '\n';
258
+ }
259
+
260
+ /**
261
+ * 合并现有 baseline(增量更新 Current Cases + 追加 Evolution Log)
262
+ */
263
+ function mergeExistingBaseline(existing, sectionContent, changeName, date, candidates, deferred) {
264
+ const lines = [];
265
+ const existingLines = existing.split('\n');
266
+
267
+ // 更新 frontmatter
268
+ let inFrontmatter = false;
269
+ let frontmatterDone = false;
270
+ for (const line of existingLines) {
271
+ if (line.startsWith('> last_updated_by_change:')) {
272
+ lines.push(`> last_updated_by_change: ${changeName}`);
273
+ } else if (line.startsWith('> last_updated:')) {
274
+ lines.push(`> last_updated: ${date}`);
275
+ } else if (line.startsWith('## Current Cases')) {
276
+ // 插入 Current Cases header,然后用新数据替换表格
277
+ lines.push(line);
278
+ lines.push('');
279
+ lines.push('| case_id | behavior | design_method | test_kind | test_tier | work_mode | test_file | test_method_name | source_change |');
280
+ lines.push('|---|---|---|---|---|---|---|---|---|');
281
+ inFrontmatter = true;
282
+ // 跳过旧表格
283
+ continue;
284
+ } else if (inFrontmatter && (line.startsWith('## ') || line.startsWith(''))) {
285
+ inFrontmatter = false;
286
+ // 插入新的 case 行
287
+ const caseLines = sectionContent.split('\n').filter(l =>
288
+ l.startsWith('|') && !l.match(/^\|\s*[-:]+/) && !l.match(/\|\s*case_id/)
289
+ );
290
+ for (const cl of caseLines) {
291
+ const cells = cl.split('|').map(c => c.trim()).filter(Boolean);
292
+ if (cells.length >= 10) {
293
+ lines.push(`| ${cells[0]} | ${cells[1]} | ${cells[2]} | ${cells[5]} | ${cells[6]} | ${cells[8]} | ${cells[9]} | ${cells[10]} | ${changeName} |`);
294
+ }
295
+ }
296
+ lines.push(line);
297
+ } else if (inFrontmatter && line.startsWith('|')) {
298
+ // 跳过旧表格行(已替换)
299
+ continue;
300
+ } else if (line.startsWith('## Evolution Log')) {
301
+ lines.push(line);
302
+ lines.push('');
303
+ const caseCount = sectionContent.split('\n').filter(l =>
304
+ l.startsWith('|') && !l.match(/^\|\s*[-:]+/) && !l.match(/\|\s*case_id/)
305
+ ).length;
306
+ lines.push(`- **${date}** [${changeName}]: 新增 ${caseCount} case, defer ${deferred.length}`);
307
+ frontmatterDone = true;
308
+ } else if (frontmatterDone && line.startsWith('- **')) {
309
+ // 保留旧的 evolution log entries
310
+ lines.push(line);
311
+ } else if (!inFrontmatter) {
312
+ lines.push(line);
313
+ }
314
+ }
315
+
316
+ return lines.join('\n') + '\n';
317
+ }
318
+
319
+ /**
320
+ * Step 3: resolveDeferred — 新 case 覆盖了旧 deferred 项
321
+ */
322
+ function resolveDeferred(ledgerDir, changeName, newCaseIds) {
323
+ const baselinesDir = join(ledgerDir, 'baselines');
324
+ if (!existsSync(baselinesDir)) return { resolved: 0 };
325
+
326
+ let resolved = 0;
327
+ for (const file of readdirSync(baselinesDir)) {
328
+ if (!file.endsWith('.md')) continue;
329
+ const fp = join(baselinesDir, file);
330
+ let content = readFileSync(fp, 'utf-8');
331
+ let changed = false;
332
+
333
+ // 移除 Deferred Items 中被新 case 覆盖的行
334
+ for (const caseId of newCaseIds) {
335
+ const regex = new RegExp(`\\|\\s*${escapeRegex(caseId)}\\s*\\|[^\\n]*\\n`, 'g');
336
+ if (regex.test(content)) {
337
+ content = content.replace(regex, '');
338
+ changed = true;
339
+ resolved++;
340
+ }
341
+ }
342
+
343
+ if (changed) {
344
+ writeFileSync(fp, content, 'utf-8');
345
+ }
346
+ }
347
+ return { resolved };
348
+ }
349
+
350
+ function escapeRegex(str) {
351
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
352
+ }
353
+
354
+ /**
355
+ * Step 4: appendChangelog — 归档 test-matrix.md
356
+ */
357
+ function appendChangelog(ledgerDir, changeName, matrixContent) {
358
+ const changelogDir = join(ledgerDir, 'changelog');
359
+ if (!existsSync(changelogDir)) {
360
+ mkdirSync(changelogDir, { recursive: true });
361
+ }
362
+ const destPath = join(changelogDir, `${changeName}.md`);
363
+ writeFileSync(destPath, matrixContent, 'utf-8');
364
+ return destPath;
365
+ }
366
+
367
+ /**
368
+ * Step 5: rewriteIndex — 重写 INDEX.md
369
+ */
370
+ function rewriteIndex(ledgerDir) {
371
+ const baselinesDir = join(ledgerDir, 'baselines');
372
+ const indexPath = join(ledgerDir, 'INDEX.md');
373
+ const today = new Date().toISOString().slice(0, 10);
374
+
375
+ const modules = [];
376
+ if (existsSync(baselinesDir)) {
377
+ for (const file of readdirSync(baselinesDir)) {
378
+ if (!file.endsWith('.md')) continue;
379
+ const content = readFileSync(join(baselinesDir, file), 'utf-8');
380
+ const moduleName = file.replace('.md', '').replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
381
+
382
+ // 统计 case 数
383
+ const caseMatches = content.match(/\|\s*\S+-\S+-\S+-\d+/g) || [];
384
+ const caseCount = caseMatches.length;
385
+
386
+ // 统计 deferred 数
387
+ const deferredSection = content.split('## Deferred Items')[1] || '';
388
+ const deferredMatches = deferredSection.match(/\|\s*\S+-\S+-\S+-\d+/g) || [];
389
+ const deferredCount = deferredMatches.length;
390
+
391
+ // 提取 last_updated_by_change
392
+ const changeMatch = content.match(/last_updated_by_change:\s*(.+)/);
393
+ const lastChange = changeMatch ? changeMatch[1].trim() : 'unknown';
394
+ const dateMatch = content.match(/last_updated:\s*(.+)/);
395
+ const lastDate = dateMatch ? dateMatch[1].trim() : 'unknown';
396
+
397
+ // coverage_status
398
+ let status = '✅ baseline';
399
+ if (deferredCount > 0) status = '⚠️ partial';
400
+ if (caseCount === 0) status = '⏭️ not_applicable';
401
+
402
+ // test_tier_breakdown
403
+ const unitCount = (content.match(/unit/gi) || []).length;
404
+ const intCount = (content.match(/integration/gi) || []).length;
405
+
406
+ modules.push({ moduleName, caseCount, deferredCount, lastChange, lastDate, status, unitCount, intCount });
407
+ }
408
+ }
409
+
410
+ const totalCases = modules.reduce((sum, m) => sum + m.caseCount, 0);
411
+ const totalDeferred = modules.reduce((sum, m) => sum + m.deferredCount, 0);
412
+
413
+ const lines = [];
414
+ lines.push('# Test Ledger Index');
415
+ lines.push('');
416
+ lines.push('> Auto-generated by `tf test-merge`. Do not edit manually.');
417
+ lines.push('');
418
+ lines.push(`**Modules**: ${modules.length} | **Total Cases**: ${totalCases} | **Deferred**: ${totalDeferred} | **Last Updated**: ${today}`);
419
+ lines.push('');
420
+ lines.push('| module | test_tier_breakdown | case_count | deferred | last_change | last_date | coverage_status |');
421
+ lines.push('|---|---|---|---|---|---|---|');
422
+
423
+ for (const m of modules) {
424
+ lines.push(`| ${m.moduleName} | unit:${m.unitCount}, integration:${m.intCount} | ${m.caseCount} | ${m.deferredCount} | ${m.lastChange} | ${m.lastDate} | ${m.status} |`);
425
+ }
426
+
427
+ writeFileSync(indexPath, lines.join('\n') + '\n', 'utf-8');
428
+ return { modules: modules.length, totalCases, totalDeferred };
429
+ }
430
+
431
+ /**
432
+ * Step 6: gitCommit — 原子性提交
433
+ */
434
+ function gitCommit(projectRoot, changeName, dryRun) {
435
+ const msg = `test-ledger: merge test results from ${changeName}`;
436
+ if (dryRun) {
437
+ console.log(`[DRY-RUN] Would commit: git add docs/test-ledger/ && git commit -m "${msg}"`);
438
+ return;
439
+ }
440
+ try {
441
+ execSync('git add docs/test-ledger/', { cwd: projectRoot, stdio: 'pipe' });
442
+ execSync(`git commit -m "${msg}"`, { cwd: projectRoot, stdio: 'pipe' });
443
+ console.log(`✅ test-ledger committed: ${msg}`);
444
+ } catch (err) {
445
+ // Nothing to commit is acceptable (e.g., no changes)
446
+ if (err.message && err.message.includes('nothing to commit')) {
447
+ console.log('ℹ️ No changes to commit in test-ledger');
448
+ } else {
449
+ console.error(`⚠️ git commit warning: ${err.message}`);
450
+ }
451
+ }
452
+ }
453
+
454
+ /**
455
+ * CLI entry point — export as `run()` for registration
456
+ */
457
+ export async function run(args) {
458
+ const argv = parseArgv(args || process.argv.slice(2));
459
+ const projectRoot = resolve(argv.projectRoot || process.cwd());
460
+
461
+ // Delegate to main()
462
+ await main(argv, projectRoot);
463
+ }
464
+
465
+ /**
466
+ * 主入口
467
+ */
468
+ async function main(argv, projectRoot) {
469
+ const changeDir = argv._[0];
470
+ const dryRun = argv.dryRun || false;
471
+
472
+ if (!changeDir) {
473
+ console.error('Usage: tf test-merge <change-dir> [--project-root <path>] [--dry-run]');
474
+ process.exit(2);
475
+ }
476
+
477
+ const absChangeDir = resolve(changeDir);
478
+ const changeName = extractChangeName(absChangeDir);
479
+ const ledgerDir = join(projectRoot, 'docs', 'test-ledger');
480
+
481
+ console.log(`📋 test-merge: ${changeName} → ${ledgerDir}`);
482
+
483
+ // Step 1: preCheck
484
+ const preCheckResult = preCheck(absChangeDir);
485
+ if (!preCheckResult.pass) {
486
+ console.log(`⏭️ ${preCheckResult.reason}`);
487
+ process.exit(0);
488
+ }
489
+ console.log('✅ Step 1: preCheck passed');
490
+
491
+ const matrixContent = preCheckResult.content;
492
+ const summary = extractSummary(matrixContent);
493
+ const moduleSections = extractModuleSections(matrixContent);
494
+ const candidateLedger = extractCandidateLedger(matrixContent);
495
+ const deferredItems = extractDeferredItems(matrixContent);
496
+
497
+ console.log(` Summary: ${summary.totalCases} cases, ${summary.modules} modules, ${summary.deferred} deferred`);
498
+
499
+ // Ensure ledger directory exists
500
+ if (!existsSync(ledgerDir)) {
501
+ mkdirSync(ledgerDir, { recursive: true });
502
+ }
503
+
504
+ // Step 2: mergeBaselines
505
+ const mergeResults = mergeBaselines(ledgerDir, changeName, moduleSections, candidateLedger, deferredItems);
506
+ console.log(`✅ Step 2: mergeBaselines — ${Object.keys(mergeResults).length} modules processed`);
507
+
508
+ // Step 3: resolveDeferred
509
+ const newCaseIds = [];
510
+ for (const section of Object.values(moduleSections)) {
511
+ const lines = section.split('\n');
512
+ for (const line of lines) {
513
+ if (line.startsWith('|') && !line.match(/^\|\s*[-:]+/) && !line.match(/\|\s*case_id/)) {
514
+ const cells = line.split('|').map(c => c.trim()).filter(Boolean);
515
+ if (cells[0]) newCaseIds.push(cells[0]);
516
+ }
517
+ }
518
+ }
519
+ const deferredResult = resolveDeferred(ledgerDir, changeName, newCaseIds);
520
+ console.log(`✅ Step 3: resolveDeferred — ${deferredResult.resolved} items resolved`);
521
+
522
+ // Step 4: appendChangelog
523
+ const changelogPath = appendChangelog(ledgerDir, changeName, matrixContent);
524
+ console.log(`✅ Step 4: appendChangelog — ${relative(projectRoot, changelogPath)}`);
525
+
526
+ // Step 5: rewriteIndex
527
+ const indexResult = rewriteIndex(ledgerDir);
528
+ console.log(`✅ Step 5: rewriteIndex — ${indexResult.modules} modules, ${indexResult.totalCases} total cases`);
529
+
530
+ // Step 6: gitCommit
531
+ gitCommit(projectRoot, changeName, dryRun);
532
+ console.log('✅ Step 6: gitCommit');
533
+
534
+ console.log(`\n🎉 test-merge complete for ${changeName}`);
535
+ }
536
+
537
+ main().catch(err => {
538
+ console.error('test-merge error:', err.message);
539
+ process.exit(1);
540
+ });
@@ -34,6 +34,8 @@ const COMMANDS = {
34
34
  'install-zcode': () => import('./lib/cmd-install-zcode.mjs'),
35
35
  'prototype-sync': () => import('./lib/prototype-sync.mjs'),
36
36
  'arch-merge': () => import('./lib/arch-merge.mjs'),
37
+ 'test-merge': () => import('./lib/test-merge.mjs'),
38
+ 'test-matrix-export': () => import('./lib/test-matrix-export.mjs'),
37
39
  };
38
40
 
39
41
  const HELP = `team-flow (tf) — Unified workflow plugin CLI
@@ -50,6 +52,10 @@ Commands:
50
52
  Merge UX delta into global prototype/ + design-system.md
51
53
  arch-merge <change-dir> [--project-root <path>] [--dry-run]
52
54
  Merge architecture delta into global docs/architecture/
55
+ test-merge <change-dir> [--project-root <path>] [--dry-run]
56
+ Merge test matrix results into global docs/test-ledger/
57
+ test-matrix-export <input.json> <output.md> [--change-id <id>]
58
+ Convert glaf4 test-matrix.json to team-flow test-matrix.md
53
59
  config [options] Display or modify configuration
54
60
  config --resolve-model <profile> Resolve a configured model profile without switching models
55
61
  state <sub> <dir> Manage .team-flow.yaml state (init|check|transition|get|rebuild)
@@ -27,17 +27,27 @@ Branch/worktree preflight before ANY implementation edit (mandatory — do not s
27
27
  ```bash
28
28
  tf isolate <change-dir>
29
29
  ```
30
- This script enforces git isolation: if you are on `main`/`master` it creates a
31
- git worktree (preferred) or a new branch, and exits non-zero if it cannot and you
32
- have not approved `--force`.
30
+ This script enforces git isolation. In a **multi-repo workspace** (a workspace root
31
+ whose direct subdirectories are independent code repositories, e.g. `bff-*`/`ui-*`/
32
+ `prototype/`, while the root itself is often a near-empty git repo), it creates one git
33
+ worktree per code repo under `<workspace>/.worktrees/<change>/<repo>` — each based on
34
+ that repo's current branch — so the implementation subagent enters that aggregated
35
+ directory and sees reference code from every repo with the same layout as the main
36
+ workspace. In a **single code repository** it falls back to creating one git worktree
37
+ (preferred) or a new branch. It exits non-zero if it cannot isolate and you have not
38
+ approved `--force`.
33
39
  2. If `tf isolate` exits non-zero: STOP. Do not edit `main`/`master` in place.
34
40
  Ask the user for explicit approval (and re-run with `tf isolate <change-dir> --force`
35
41
  only after they approve).
36
- 3. If it succeeds, report the chosen branch/worktree and make all implementation
42
+ 3. If it succeeds, report the chosen branch/worktree in a multi-repo workspace, the
43
+ aggregated `<workspace>/.worktrees/<change>/` directory — and make all implementation
37
44
  edits there.
38
45
 
39
46
  ## Core Laws
40
47
 
48
+ ### Law 0: State Field Boundary (v0.30.0)
49
+ 本 skill 仅通过 `tf state set` 写自己的 `dp_5_*` 决策字段;**MUST NOT** 修改 `.team-flow.yaml` 的 `state`/`workflow` 核心字段——状态转换是主代理(workflow-start)经 `tf state transition` 的专有职责(来源:workflow-feedback 2026-08-01)。
50
+
41
51
  ### Law 1: Contract First
42
52
  The execution contract is the approved handoff artifact, not chat history.
43
53
 
@@ -40,13 +40,42 @@ Subagent (general-purpose):
40
40
  ## Your Job
41
41
 
42
42
  Once you're clear on requirements:
43
- 1. Implement exactly what the task specifies
44
- 2. Write tests (following TDD if task says to)
45
- 3. Verify implementation works
43
+ 1. Read test-matrix.md for this task's module section (if it exists)
44
+ 2. Implement exactly what the task specifies, following the Test Matrix Protocol below
45
+ 3. Verify implementation works (all matrix cases pass)
46
46
  4. Commit your work
47
47
  5. Self-review (see below)
48
48
  6. Report back
49
49
 
50
+ ## Test Matrix Protocol (v0.12 §44.2)
51
+
52
+ If `test-matrix.md` exists, read the module section relevant to your task.
53
+ For each case in the matrix, follow the protocol matching its `work_mode`:
54
+
55
+ **work_mode=TDD** (new behavior):
56
+ 1. RED: Write failing test matching `case_id` + `test_method_name`, confirm failure
57
+ 2. GREEN: Implement minimum code to pass
58
+ 3. REFACTOR: Clean up, suite stays green
59
+ 4. Report TDD Evidence: RED command + failure output, GREEN command + pass output
60
+
61
+ **work_mode=CHARACTERIZATION** (legacy behavior capture):
62
+ 1. Write test capturing current behavior — do NOT change production code
63
+ 2. Report: `current_behavior_note` + test output
64
+ 3. `production_change_allowed=false` — if test reveals a bug, report it but do NOT fix
65
+
66
+ **work_mode=REGRESSION** (defect reproduction + fix):
67
+ 1. Write failing test reproducing the defect
68
+ 2. Fix production code, confirm green
69
+ 3. Report: defect reproduction + fix evidence
70
+
71
+ After all cases: self-check matrix coverage = passed cases / total cases in matrix for your module.
72
+
73
+ **两不原则 (Two Prohibitions)**:
74
+ - ⛔ DO NOT write production code and test code simultaneously — RED first, then GREEN
75
+ - ⛔ DO NOT skip the RED failure confirmation — you must see the test fail before implementing
76
+
77
+ If `test-matrix.md` does NOT exist, fall back to standard TDD (Law 2): write tests first, then implement.
78
+
50
79
  Work from: [directory]
51
80
 
52
81
  **While you work:** If you encounter something unexpected or unclear, **ask questions**.
@@ -109,6 +138,8 @@ Subagent (general-purpose):
109
138
  - Did I follow TDD if required?
110
139
  - Are tests comprehensive?
111
140
  - Is the test output pristine (no stray warnings or noise)?
141
+ - If test-matrix.md exists: matrix coverage = passed cases / total cases for my module
142
+ - Each case's `design_method` matches the actual test approach (boundary case uses real boundary values, not happy-path disguised as boundary)
112
143
 
113
144
  If you find issues during self-review, fix them now before reporting.
114
145
 
@@ -126,6 +157,10 @@ Subagent (general-purpose):
126
157
  - **TDD Evidence** (if TDD was required for this task):
127
158
  - RED: command run, relevant failing output before implementation, and why the failure was expected
128
159
  - GREEN: command run and relevant passing output after implementation
160
+ - **Test Matrix Coverage** (if test-matrix.md exists):
161
+ - Cases implemented: N/M (list case_ids)
162
+ - Cases deferred: N (list case_ids + reason)
163
+ - Any case where expected_red did not match actual failure — report mismatch
129
164
  - Files changed
130
165
  - Self-review findings (if any)
131
166
  - Any issues or concerns