@xulthekl/team-flow 0.63.0 → 0.64.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 +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/.github/workflows/ci.yml +2 -0
  9. package/CHANGELOG.md +28 -0
  10. package/GEMINI.md +1 -1
  11. package/INSTALL.md +1 -1
  12. package/README.md +1 -1
  13. package/docs/README_en.md +1 -1
  14. package/docs/state-machine.md +4 -1
  15. package/docs/team-flow /344/275/277/347/224/250/350/257/264/346/230/216/357/274/210/347/240/224/345/217/221/345/233/242/351/230/237/347/211/210/357/274/211.md" +2 -2
  16. package/gemini-extension.json +1 -1
  17. package/hooks/session-start +2 -2
  18. package/llms.txt +1 -1
  19. package/package.json +1 -1
  20. package/plugin.json +1 -1
  21. package/scripts/guard/checks/_fs-utils.mjs +18 -0
  22. package/scripts/guard/checks/arch-design-light.mjs +39 -0
  23. package/scripts/guard/checks/arch-merged-light.mjs +67 -0
  24. package/scripts/guard/checks/arch-snapshot-light.mjs +30 -0
  25. package/scripts/guard/checks/artifacts-planned.mjs +38 -0
  26. package/scripts/guard/checks/compound-writeback-light.mjs +45 -0
  27. package/scripts/guard/checks/cross-change-consistency-light.mjs +75 -0
  28. package/scripts/guard/checks/direct-short-path.mjs +52 -0
  29. package/scripts/guard/checks/direct-test-result.mjs +30 -0
  30. package/scripts/guard/checks/execution-plan-ready.mjs +7 -1
  31. package/scripts/guard/checks/execution-reviews-passed-light.mjs +28 -0
  32. package/scripts/guard/checks/lightweight-completion-evidence.mjs +27 -0
  33. package/scripts/guard/checks/specs-merged.mjs +25 -1
  34. package/scripts/guard/checks/test-matrix-complete.mjs +27 -1
  35. package/scripts/guard/checks/test-matrix-ready.mjs +28 -1
  36. package/scripts/guard/checks/test-merged-light.mjs +36 -0
  37. package/scripts/guard/guard.mjs +102 -12
  38. package/scripts/infer-workflow.mjs +35 -4
  39. package/scripts/lib/arch-merge.mjs +20 -4
  40. package/scripts/lib/cmd-execution.mjs +44 -1
  41. package/scripts/lib/cmd-state.mjs +94 -4
  42. package/scripts/lib/execution-plan.mjs +3 -1
  43. package/scripts/lib/state-loader.mjs +43 -0
  44. package/scripts/lib/surface-scan.mjs +156 -0
  45. package/scripts/lib/test-merge.mjs +10 -2
  46. package/scripts/team-flow.mjs +3 -3
  47. package/skills/clean-code/SKILL.md +1 -1
  48. package/skills/jarvis/SKILL.md +2 -0
  49. package/skills/release-archivist/SKILL.md +39 -13
  50. package/skills/session-handoff/SKILL.md +1 -0
  51. package/skills/test-strategy/SKILL.md +1 -1
  52. package/skills/workflow-start/SKILL.md +63 -5
  53. package/skills/workflow-start/references/routing-rules.md +4 -4
@@ -1,4 +1,6 @@
1
1
  import { parseArgs } from 'node:util';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
2
4
  import { createPlan, describeWaves, EXECUTION_MODES, readPlan, recordReview, refreshPlanHash, validatePlan, writePlan } from './execution-plan.mjs';
3
5
  import {
4
6
  createRecommendationReceipt,
@@ -6,6 +8,7 @@ import {
6
8
  writeRecommendationReceipt,
7
9
  } from './execution-recommendation.mjs';
8
10
  import { readState, writeState } from './state-loader.mjs';
11
+ import { parseTaskLine } from './md-normalize.mjs';
9
12
 
10
13
  const SUBCOMMANDS = ['recommend', 'plan', 'show', 'revise', 'review', 'refresh-hash'];
11
14
 
@@ -17,6 +20,8 @@ export async function run(args) {
17
20
  reason: { type: 'string' },
18
21
  wave: { type: 'string', multiple: true },
19
22
  confirm: { type: 'boolean', default: false },
23
+ // v0.64.0(实施计划 D7):planned 前门的 tasks.md 派生通道——跳过 recommend/receipt 仪式
24
+ derive: { type: 'boolean', default: false },
20
25
  'acknowledge-recommendation': { type: 'boolean', default: false },
21
26
  base: { type: 'string' },
22
27
  head: { type: 'string' },
@@ -58,10 +63,47 @@ export async function run(args) {
58
63
  }
59
64
  }
60
65
 
66
+ // v0.64.0(D7 派生):tasks.md checkbox → 单 wave(serial),仅 planned --derive 通道使用
67
+ function deriveWavesFromTasks(changeDir) {
68
+ const tasksPath = join(changeDir, 'tasks.md');
69
+ if (!existsSync(tasksPath)) throw new Error('--derive requires tasks.md (planned front door: proposal.md + tasks.md)');
70
+ const tasks = readFileSync(tasksPath, 'utf-8')
71
+ .split('\n')
72
+ .map(line => parseTaskLine(line)?.text)
73
+ .map(t => (typeof t === 'string' ? t.trim() : ''))
74
+ .filter(Boolean);
75
+ if (tasks.length === 0) throw new Error('tasks.md has no checkboxes to derive a plan from');
76
+ return [{ id: 'w1', strategy: 'serial', tasks, depends_on: [] }];
77
+ }
78
+
61
79
  function createAndPrintPlan(changeDir, values, revise) {
62
- requireMode(values.mode);
63
80
  requireOption(values.reason, '--reason');
64
81
  requireSafeReason(values.reason);
82
+ // planned 派生通道(v0.64.0 D7):无 recommendation/receipt/selection——validatePlan 对
83
+ // workflow_variant=planned 豁免三件要求;state.execution_* 由 writeExecutionSummary 同步。
84
+ if (values.derive) {
85
+ if (revise) throw new Error('--derive creates the initial plan; use "tf execution revise" for revisions');
86
+ const st = readState(changeDir);
87
+ if (st.workflow_variant !== 'planned') {
88
+ throw new Error('--derive is only valid for the planned front door (workflow_variant=planned)');
89
+ }
90
+ if (!values.confirm) throw new Error('--derive requires --confirm');
91
+ const mode = values.mode || 'inline';
92
+ requireMode(mode);
93
+ const waves = values.wave?.length ? parseWaves(values.wave) : deriveWavesFromTasks(changeDir);
94
+ const plan = createPlan(changeDir, {
95
+ mode,
96
+ source: 'tasks-derived',
97
+ rationale: values.reason,
98
+ waves,
99
+ });
100
+ const saved = writePlan(changeDir, plan);
101
+ writeExecutionSummary(changeDir, saved);
102
+ print(values.json, { ok: true, plan: saved },
103
+ `Execution plan revision ${saved.revision} recorded (${saved.mode}, derived from tasks.md).`);
104
+ return;
105
+ }
106
+ requireMode(values.mode);
65
107
  const waves = parseWaves(values.wave);
66
108
  const existing = readPlan(changeDir);
67
109
  if (revise) {
@@ -236,6 +278,7 @@ function printHelp() {
236
278
  console.log(`Usage:
237
279
  tf execution recommend <dir> [--wave <id>:<strategy>:<task,...>[:<depends-on,...>]] [--json]
238
280
  tf execution plan <dir> --mode <mode> --confirm --reason <text> --wave <id>:<strategy>:<task,...>[:<depends-on,...>] [--acknowledge-recommendation]
281
+ tf execution plan <dir> --derive --confirm --reason <text> [--mode <mode>] # planned only: derive single wave from tasks.md
239
282
  tf execution show <dir> [--json]
240
283
  tf execution revise <dir> --mode sdd --confirm --reason <text> --wave <id>:<strategy>:<task,...>[:<depends-on,...>] [--acknowledge-recommendation]
241
284
  tf execution review <dir> --wave <id> --base <sha> --head <sha> --report <path> --verdict pass|fail [--repo <path>] [--tests-total N --tests-passed N --tests-failed N]
@@ -17,6 +17,13 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
17
17
  // schema_version 不可设置(仅 init 打戳,防止伪造豁免身份,v0.13 §48.1)。
18
18
  const SETTABLE_FIELDS = [
19
19
  'workflow', 'batches_completed', 'spec_merged',
20
+ // v0.64.0 P0(实施计划 §3.3):前门双维度。三环齐全(本列表 + state-loader
21
+ // BUILTIN_DEFAULTS + writeState 序列化分支),缺一即「回显成功却零写入」。
22
+ // workflow_variant 走通用通道仅 exploring 态可写(下方 set 分支强制);
23
+ // 升档豁免 = tf state upgrade 专用子命令(不经本通道),写入打
24
+ // variant_source=upgrade + variant_direction=up,guard 据此 fail-closed。
25
+ 'workflow_variant', 'variant_source', 'variant_direction',
26
+ 'planned_arch', 'model_profile',
20
27
  'dp_0_decisions', 'dp_0_confirmed', 'dp_0_timestamp', 'dp_0_result',
21
28
  // v0.59.0(P4 实测):dp_{1,2,3,5,6,7}_decisions / _confirmed 共 12 个字段已移除——
22
29
  // 它们虽在旧白名单内,但 writeState 无对应序列化分支,tf state set 会**回显成功却零写入**
@@ -49,6 +56,8 @@ const SETTABLE_FIELDS = [
49
56
  // 三键必须与 state-loader BUILTIN_DEFAULTS + writeState 序列化分支同步注册,
50
57
  // 缺任一环即「回显成功却零写入」(v0.59.0 同型教训,见本文件 :20-24 注释)。
51
58
  'gates_probed_skipped', 'gates_probed_skip_reason', 'gates_probed_na',
59
+ // v0.64.0(§4 arch-design-light):轻架构说明显式跳过键(须附理由)
60
+ 'arch_design_light_skipped', 'arch_design_light_skip_reason',
52
61
  ];
53
62
 
54
63
  export async function run(args) {
@@ -66,7 +75,7 @@ export async function run(args) {
66
75
 
67
76
  if (!changeDir) {
68
77
  console.error('Usage: tf state <subcommand> <change-dir> [arg]');
69
- console.error('Subcommands: init, check, transition, get, rebuild, set');
78
+ console.error('Subcommands: init, check, transition, get, rebuild, set, upgrade');
70
79
  process.exit(2);
71
80
  }
72
81
 
@@ -80,7 +89,7 @@ export async function run(args) {
80
89
  // Unknown subcommand: report a usage error (exit 2) BEFORE the BUG-B
81
90
  // state-file existence check, so a bad subcommand is not masked by the
82
91
  // "No state file" error (which would return exit 1 instead of exit 2).
83
- const KNOWN_SUBS = ['init', 'check', 'transition', 'get', 'rebuild', 'set'];
92
+ const KNOWN_SUBS = ['init', 'check', 'transition', 'get', 'rebuild', 'set', 'upgrade'];
84
93
  if (!KNOWN_SUBS.includes(sub)) {
85
94
  console.error(`Unknown subcommand: ${sub}. Valid: ${KNOWN_SUBS.join(', ')}`);
86
95
  process.exit(2);
@@ -112,6 +121,12 @@ export async function run(args) {
112
121
  const state = readState(changeDir);
113
122
  if (!stateFileExisted) {
114
123
  state.schema_version = 1;
124
+ // v0.64.0 P0(§4 扫描基线):init 打戳 base_sha(架构 surface 扫描的 git 基线)。
125
+ // 非 git 环境 / 尚无提交 → 保持 null,扫描时按 §4 退化参照(origin → FAIL)。
126
+ try {
127
+ const rev = spawnSync('git', ['rev-parse', 'HEAD'], { cwd: changeDir, encoding: 'utf-8', timeout: 3_000 });
128
+ if (rev.status === 0 && rev.stdout?.trim()) state.base_sha = rev.stdout.trim();
129
+ } catch { /* 保持 null */ }
115
130
  }
116
131
  state.artifacts_hash = hash;
117
132
  state.contract_hash = ch;
@@ -170,7 +185,16 @@ export async function run(args) {
170
185
  const rawWorkflow = state.workflow || 'full';
171
186
  // Normalize: guard only accepts full/hotfix/tweak, not "auto"
172
187
  const workflow = rawWorkflow === 'auto' ? 'full' : rawWorkflow;
173
- const guardResult = spawnSync('node', [guardScript, 'check', changeDir, fromState, toState, '--json', '--workflow', workflow], {
188
+ // v0.64.0 P1(§3.3/§4.5 注记①):透传前门变体——guard 据此选 quick/planned 维度表。
189
+ // 这是 workflow_variant 到达 resolveDimensions 的唯一管道(workflow-start 是 skill,不是调用方)。
190
+ const guardArgs = [guardScript, 'check', changeDir, fromState, toState, '--json', '--workflow', workflow];
191
+ if (state.workflow_variant != null && state.workflow_variant !== 'null') {
192
+ guardArgs.push('--workflow-variant', String(state.workflow_variant));
193
+ }
194
+ if (state.planned_arch === true || state.planned_arch === 'true') {
195
+ guardArgs.push('--planned-arch', 'true');
196
+ }
197
+ const guardResult = spawnSync('node', guardArgs, {
174
198
  cwd: join(__dirname, '..', '..'),
175
199
  timeout: 10_000,
176
200
  });
@@ -282,6 +306,28 @@ export async function run(args) {
282
306
  }
283
307
  process.exit(1);
284
308
  }
309
+ // v0.64.0 P0 写限(§3.3/§5.3):workflow_variant 通用通道仅 exploring 态可写,
310
+ // 防关门段自降档清屏;升档唯一豁免 = tf state upgrade 专用子命令(不走本通道)。
311
+ if (field === 'workflow_variant') {
312
+ const cur = readState(changeDir);
313
+ if (cur.state !== 'exploring') {
314
+ console.error(
315
+ `⛔ 'workflow_variant' 仅可在 exploring 态通过本通道写入(当前: ${cur.state})。\n` +
316
+ ` 升档唯一机械入口: tf state upgrade <change-dir> <planned|full>(原子改写 variant_source=upgrade + 回退 approved-for-build + 重算 hash;降档拒绝)`
317
+ );
318
+ process.exit(1);
319
+ }
320
+ if (value !== 'null' && !['direct', 'planned', 'legacy'].includes(value)) {
321
+ console.error(`⛔ Invalid workflow_variant: '${value}'. Must be one of: direct, planned, legacy, null`);
322
+ process.exit(1);
323
+ }
324
+ }
325
+ if (field === 'model_profile') {
326
+ if (!['mechanical', 'standard', 'strong', 'review'].includes(value)) {
327
+ console.error(`⛔ Invalid model_profile: '${value}'. Must be one of: mechanical, standard, strong, review`);
328
+ process.exit(1);
329
+ }
330
+ }
285
331
  updateField(changeDir, field, value);
286
332
  if (values.json) {
287
333
  console.log(JSON.stringify({ ok: true, field, value }));
@@ -290,8 +336,52 @@ export async function run(args) {
290
336
  }
291
337
  break;
292
338
  }
339
+ case 'upgrade': {
340
+ // v0.64.0(实施计划 §5.3 D9,升档唯一机械入口):
341
+ // tf state upgrade <change-dir> <planned|full>
342
+ // 校验方向向上 → 原子写 variant_source=upgrade + variant_direction=up →
343
+ // state 回退 approved-for-build(重跑目标档闸门)→ 重算 artifacts_hash(B-10)。
344
+ // 保留:磁盘代码/产物、test_result + test_evidence_path、test_matrix_hash。
345
+ const toVariant = arg;
346
+ if (!['planned', 'full'].includes(toVariant)) {
347
+ console.error('Usage: tf state upgrade <change-dir> <planned|full> (downgrades are forbidden)');
348
+ process.exit(2);
349
+ }
350
+ const ORDER = { direct: 0, planned: 1 };
351
+ const cur = readState(changeDir);
352
+ const from = cur.workflow_variant;
353
+ // 档位序:direct(0) < planned(1) < full/legacy(2)。upgrade full = 置 legacy + workflow full
354
+ //(variant 枚举无 'full',full 档由 workflow=full + legacy 全量仪式表达,方案 §5.3)。
355
+ const fromOrder = from === 'legacy' || from == null ? 2 : (ORDER[from] ?? 0);
356
+ const targetOrder = toVariant === 'full' ? 2 : ORDER[toVariant];
357
+ if (fromOrder >= targetOrder) {
358
+ console.error(`⛔ Upgrade ${from ?? 'null'} -> ${toVariant} is not upward (downgrades are forbidden)`);
359
+ process.exit(1);
360
+ }
361
+ cur.workflow_variant = toVariant === 'full' ? 'legacy' : toVariant;
362
+ cur.variant_source = 'upgrade';
363
+ cur.variant_direction = 'up';
364
+ if (toVariant === 'full') cur.workflow = 'full';
365
+ // 已过规划段则回退 approved-for-build 重跑目标档闸门;exploring 态无需回退
366
+ const ROLLBACK_STATES = ['specifying', 'bridging', 'executing', 'debugging', 'closing'];
367
+ if (ROLLBACK_STATES.includes(cur.state)) {
368
+ const prev = cur.state;
369
+ cur.state = 'approved-for-build';
370
+ cur.last_transition_from = prev;
371
+ cur.last_transition_to = 'approved-for-build';
372
+ cur.last_transition = new Date().toISOString();
373
+ }
374
+ cur.artifacts_hash = computeArtifactsHash(changeDir); // B-10 重算,防 doctor/check 恒报不一致
375
+ writeState(changeDir, cur);
376
+ if (values.json) {
377
+ console.log(JSON.stringify({ ok: true, from: from ?? 'null', to: cur.workflow_variant, state: cur.state, variant_source: 'upgrade' }));
378
+ } else {
379
+ console.log(`✅ Upgraded workflow_variant: ${from ?? 'null'} -> ${cur.workflow_variant} (source=upgrade, state=${cur.state}, artifacts_hash recomputed)`);
380
+ }
381
+ break;
382
+ }
293
383
  default:
294
- console.error(`Unknown subcommand: ${sub}. Valid: init, check, transition, get, rebuild, set`);
384
+ console.error(`Unknown subcommand: ${sub}. Valid: init, check, transition, get, rebuild, set, upgrade`);
295
385
  process.exit(2);
296
386
  }
297
387
  }
@@ -107,7 +107,9 @@ export function validatePlan(changeDir, plan) {
107
107
  if (state.revision != null && plan?.revision !== state.revision) {
108
108
  failures.push('execution plan revision does not match state');
109
109
  }
110
- if (plan?.workflow !== 'tweak') {
110
+ // v0.64.0(§4.5 注记⑤配套):planned(workflow=full+variant=planned)无 recommendation/
111
+ // selection 仪式——plan 由 tasks.md 派生,不走 execution-recommendation 流程;豁免三件要求。
112
+ if (plan?.workflow !== 'tweak' && state.workflow_variant !== 'planned') {
111
113
  if (plan?.recommendation === undefined) failures.push('execution plan recommendation is required for full/hotfix');
112
114
  if (plan?.recommendation_receipt === undefined) failures.push('execution plan recommendation receipt is required for full/hotfix');
113
115
  if (plan?.selection === undefined) failures.push('execution plan selection is required for full/hotfix');
@@ -15,6 +15,21 @@ export const VALID_STATES = [
15
15
  const BUILTIN_DEFAULTS = {
16
16
  state: 'exploring',
17
17
  workflow: 'auto',
18
+ // spec-superflow 2.0 双维度(v0.64.0 P0;实施计划 §3.1):
19
+ // workflow 仍是内部档位 full|hotfix|tweak|quick|lightweight(+auto 归一化前);
20
+ // workflow_variant 是用户前门意图 null|direct|planned|legacy——两者不可混用。
21
+ // 旧 yaml 缺字段 → null,路由语义 = legacy 分支(见 workflow-start SKILL)。
22
+ workflow_variant: null,
23
+ // 升档标记(§5.3):variant_source ∈ null|start|upgrade;direction ∈ null|up|down。
24
+ // guard fail-closed 校验数据源——非 exploring 改 variant 必须 source=upgrade 且 direction=up。
25
+ variant_source: null,
26
+ variant_direction: null,
27
+ // planned 是否走轻架构表(arch-design-light + arch-snapshot-light,G3)
28
+ planned_arch: false,
29
+ // 角色级模型分配档(config-loader MODEL_PROFILES),仅解析不改流程(§3.5)
30
+ model_profile: 'standard',
31
+ // 架构 surface 扫描基线(§4):tf state init 打戳 git HEAD;缺失时扫描退化参照
32
+ base_sha: null,
18
33
  revision: null,
19
34
  artifacts_hash: null,
20
35
  contract_hash: null,
@@ -90,6 +105,10 @@ const BUILTIN_DEFAULTS = {
90
105
  gates_probed_skipped: null,
91
106
  gates_probed_skip_reason: null,
92
107
  gates_probed_na: null,
108
+ // v0.64.0(§4 arch-design-light):轻架构说明的显式跳过键(skip-with-reason 模式,
109
+ // 与 arch_merge_skipped / test_matrix_skipped 同型:跳过必须附理由留痕)
110
+ arch_design_light_skipped: null,
111
+ arch_design_light_skip_reason: null,
93
112
  // 注意:schema_version 故意不在 BUILTIN_DEFAULTS 中(v0.13 §48.1)——
94
113
  // 它只由 `tf state init` 在 change 创建时打戳,字段缺失本身就是"存量 change"信号。
95
114
  };
@@ -135,6 +154,26 @@ export function writeState(changeDir, state) {
135
154
  lines.push('# === Core state ===');
136
155
  lines.push(`state: ${state.state || 'exploring'}`);
137
156
  lines.push(`workflow: ${state.workflow || 'auto'}`);
157
+ // v0.64.0 P0:前门双维度字段。条件序列化(同 schema_version 模式)——
158
+ // 存量 change 无这些字段时不追加,避免全量 churn;readState 缺失回退 BUILTIN_DEFAULTS。
159
+ if (state.workflow_variant != null) {
160
+ lines.push(`workflow_variant: ${state.workflow_variant}`);
161
+ }
162
+ if (state.variant_source != null) {
163
+ lines.push(`variant_source: ${state.variant_source}`);
164
+ }
165
+ if (state.variant_direction != null) {
166
+ lines.push(`variant_direction: ${state.variant_direction}`);
167
+ }
168
+ if (state.planned_arch === true || state.planned_arch === 'true') {
169
+ lines.push(`planned_arch: true`);
170
+ }
171
+ if (state.model_profile != null && state.model_profile !== 'standard') {
172
+ lines.push(`model_profile: ${state.model_profile}`);
173
+ }
174
+ if (state.base_sha != null) {
175
+ lines.push(`base_sha: ${state.base_sha}`);
176
+ }
138
177
  lines.push(`revision: ${state.revision ?? 'null'}`);
139
178
  // v0.13 §48.1:schema_version 仅由 tf state init 在 change 创建时打戳;
140
179
  // 缺失 = 存量 change(测试门禁豁免键)。rebuild/set 不追加,故条件序列化。
@@ -225,6 +264,10 @@ export function writeState(changeDir, state) {
225
264
  lines.push(`gates_probed_skipped: ${state.gates_probed_skipped ?? 'null'}`);
226
265
  lines.push(`gates_probed_skip_reason: ${state.gates_probed_skip_reason ?? 'null'}`);
227
266
  lines.push(`gates_probed_na: ${state.gates_probed_na ?? 'null'}`);
267
+ lines.push('');
268
+ lines.push('# === Light architecture design gate (v0.64.0) ===');
269
+ lines.push(`arch_design_light_skipped: ${state.arch_design_light_skipped ?? 'null'}`);
270
+ lines.push(`arch_design_light_skip_reason: ${state.arch_design_light_skip_reason ?? 'null'}`);
228
271
 
229
272
  fs.writeFileSync(filePath, lines.join('\n') + '\n', 'utf-8');
230
273
  }
@@ -0,0 +1,156 @@
1
+ // scripts/lib/surface-scan.mjs — 架构 surface 扫描共享层(v0.64.0,实施计划 §4)
2
+ //
3
+ // 唯一真相源:direct-short-path / arch-merged-light / test-merged-light /
4
+ // cross-change-consistency-light 都从这里取「本次变更碰了哪些文件、是否命中架构 surface」。
5
+ // 判据统一为「架构 surface = API / DB / 聚合」(R2 A-17 统一,禁止各 check 自造变体)。
6
+ //
7
+ // 三源合并(B-05/B-06):已提交 diff + 工作区修改 + untracked;基线 = state.base_sha,
8
+ // 缺失退化 origin/<default> merge-base,再无 → no-baseline(fail-closed,禁 merge-base HEAD HEAD 空过)。
9
+ // 聚合清单(B-04):.team-flow/aggregate-dirs.txt 机器清单(每行一个目录前缀,# 注释,
10
+ // 空文件 = 显式确认无聚合);与 docs/architecture/baseline.md 皆缺失 → missing(fail-closed)。
11
+ import { execFileSync } from 'node:child_process';
12
+ import { existsSync, readFileSync } from 'node:fs';
13
+ import { dirname, join } from 'node:path';
14
+
15
+ const GIT_TIMEOUT_MS = 2_000; // §4 扫描预算 ≤2s(cmd-state spawnSync 10s 总超时)
16
+
17
+ // API/DB surface 路径模式(§4 路径模式表)——聚合只认清单目录,不做词义解析
18
+ const SURFACE_PATH_RE = /(^|\/)(api|apis|routes|controllers?)(\/|$)|mapper|migrations?(\/|$)|(^|\/)schema[._/-]|\.(sql)$/i;
19
+ // 测试文件(test-merged-light 用)
20
+ const TEST_PATH_RE = /(^|\/)(tests?|__tests__|spec)(\/|$)|\.(test|spec)\.[jt]sx?$|Test\.java$/i;
21
+
22
+ function git(args, cwd) {
23
+ return execFileSync('git', args, {
24
+ cwd,
25
+ encoding: 'utf-8',
26
+ timeout: GIT_TIMEOUT_MS,
27
+ maxBuffer: 8 * 1024 * 1024,
28
+ stdio: ['ignore', 'pipe', 'pipe'],
29
+ });
30
+ }
31
+
32
+ /** 由 changes/<name> 推项目根:优先含 changes/ 或 docs/ 的祖先 */
33
+ export function findProjectRoot(changeDir) {
34
+ const a = dirname(dirname(changeDir));
35
+ if (existsSync(join(a, 'changes')) || existsSync(join(a, 'docs'))) return a;
36
+ const b = dirname(changeDir);
37
+ if (existsSync(join(b, 'changes')) || existsSync(join(b, 'docs'))) return b;
38
+ return a;
39
+ }
40
+
41
+ /**
42
+ * 解析聚合清单。返回 { missing, dirs[] }。
43
+ * missing=true 表示两处皆无 → 调用方 fail-closed。
44
+ */
45
+ export function readAggregateList(projectRoot) {
46
+ const txt = join(projectRoot, '.team-flow', 'aggregate-dirs.txt');
47
+ const baseline = join(projectRoot, 'docs', 'architecture', 'baseline.md');
48
+ const txtExists = existsSync(txt);
49
+ const baselineExists = existsSync(baseline);
50
+ if (!txtExists && !baselineExists) return { missing: true, dirs: [] };
51
+ if (!txtExists) return { missing: false, dirs: [] }; // 有 baseline 但无机器清单:清单维度放行,聚合匹配按空表(见设计 §4)
52
+ const dirs = readFileSync(txt, 'utf-8')
53
+ .split('\n')
54
+ .map(l => l.trim())
55
+ .filter(l => l && !l.startsWith('#'));
56
+ return { missing: false, dirs };
57
+ }
58
+
59
+ function readScanIgnore(projectRoot) {
60
+ const p = join(projectRoot, '.team-flow', 'scan-ignore');
61
+ if (!existsSync(p)) return [];
62
+ return readFileSync(p, 'utf-8')
63
+ .split('\n')
64
+ .map(l => l.trim())
65
+ .filter(l => l && !l.startsWith('#'));
66
+ }
67
+
68
+ /**
69
+ * 三源合并的变更文件清单。
70
+ * 返回 { error: null|'no-baseline'|'diff-failed', files: string[], base }
71
+ */
72
+ export function gitChangedFiles(changeDir, state) {
73
+ const files = new Set();
74
+ let base = state?.base_sha || null;
75
+
76
+ // 退化:base_sha 缺失 → origin/<default> merge-base(B-05:参照对象写死)
77
+ if (!base) {
78
+ try {
79
+ const ref = git(['symbolic-ref', 'refs/remotes/origin/HEAD'], changeDir).trim();
80
+ const remoteBranch = ref.replace('refs/remotes/', '');
81
+ base = git(['merge-base', 'HEAD', remoteBranch], changeDir).trim();
82
+ } catch {
83
+ return { error: 'no-baseline', files: [], base: null };
84
+ }
85
+ }
86
+
87
+ // 源①:已提交 diff(base...HEAD)
88
+ try {
89
+ for (const f of git(['diff', '--name-only', `${base}...HEAD`], changeDir).split('\n')) {
90
+ if (f.trim()) files.add(f.trim());
91
+ }
92
+ } catch {
93
+ return { error: 'diff-failed', files: [], base };
94
+ }
95
+
96
+ // 源②:工作区已修改
97
+ try {
98
+ for (const f of git(['diff', '--name-only', 'HEAD'], changeDir).split('\n')) {
99
+ if (f.trim()) files.add(f.trim());
100
+ }
101
+ } catch { /* 无 HEAD 时忽略 */ }
102
+
103
+ // 源③:untracked(porcelain -uall;B-06:新建未 add 的文件只有此源可见)
104
+ try {
105
+ for (const line of git(['status', '--porcelain', '-uall'], changeDir).split('\n')) {
106
+ if (!line) continue;
107
+ let p = line.slice(3);
108
+ const arrow = p.indexOf(' -> ');
109
+ if (arrow !== -1) p = p.slice(arrow + 4); // rename 取新名
110
+ p = p.trim().replace(/^"|"$/g, '');
111
+ if (p) files.add(p);
112
+ }
113
+ } catch { /* 忽略 */ }
114
+
115
+ // 排除清单(B-06:防无关 WIP 误报)
116
+ const ignore = readScanIgnore(findProjectRoot(changeDir));
117
+ const out = [...files].filter(f => !ignore.some(prefix => f.startsWith(prefix)));
118
+ return { error: null, files: out, base };
119
+ }
120
+
121
+ /**
122
+ * 文件 → 架构 surface 分类。
123
+ * 返回 { architectureSurface, apiFiles, dbFiles, aggregateFiles, testFiles }
124
+ */
125
+ export function classifyFiles(files, aggregateDirs) {
126
+ const apiFiles = [];
127
+ const dbFiles = [];
128
+ const aggregateFiles = [];
129
+ const testFiles = [];
130
+ for (const f of files) {
131
+ if (TEST_PATH_RE.test(f)) testFiles.push(f);
132
+ if (SURFACE_PATH_RE.test(f)) {
133
+ apiFiles.push(f);
134
+ if (/migrations?(\/|$)|schema[._/-]|\.(sql)$/i.test(f)) dbFiles.push(f);
135
+ }
136
+ if (aggregateDirs.some(d => {
137
+ const norm = d.replace(/\/+$/, '');
138
+ return f === norm || f.startsWith(`${norm}/`);
139
+ })) {
140
+ aggregateFiles.push(f);
141
+ }
142
+ }
143
+ const architectureSurface = [...new Set([...apiFiles, ...aggregateFiles])];
144
+ return { architectureSurface, apiFiles, dbFiles, aggregateFiles, testFiles };
145
+ }
146
+
147
+ /** 便捷组合:扫描 + 清单 + 分类(各 light runner 统一入口) */
148
+ export function scanArchitectureSurface(changeDir, state) {
149
+ const changed = gitChangedFiles(changeDir, state);
150
+ if (changed.error) return { error: changed.error, projectRoot: findProjectRoot(changeDir) };
151
+ const projectRoot = findProjectRoot(changeDir);
152
+ const agg = readAggregateList(projectRoot);
153
+ if (agg.missing) return { error: 'aggregate-list-missing', projectRoot };
154
+ const cls = classifyFiles(changed.files, agg.dirs);
155
+ return { error: null, projectRoot, files: changed.files, ...cls };
156
+ }
@@ -40,6 +40,8 @@ function parseArgv(argv) {
40
40
  parsed.projectRoot = argv[++i];
41
41
  } else if (argv[i] === '--dry-run') {
42
42
  parsed.dryRun = true;
43
+ } else if (argv[i] === '--light') {
44
+ parsed.light = true;
43
45
  } else if (!argv[i].startsWith('--')) {
44
46
  parsed._.push(argv[i]);
45
47
  }
@@ -584,11 +586,13 @@ export async function run(args) {
584
586
  async function main(argv, projectRoot) {
585
587
  const changeDir = argv._[0];
586
588
  const dryRun = argv.dryRun || false;
589
+ const light = argv.light || false; // v0.64.0 §4:planned 轻回写入口(同 rewriteIndex 单写)
587
590
 
588
591
  if (!changeDir) {
589
- console.error('Usage: tf test-merge <change-dir> [--project-root <path>] [--dry-run]');
592
+ console.error('Usage: tf test-merge <change-dir> [--project-root <path>] [--dry-run] [--light]');
590
593
  process.exit(2);
591
594
  }
595
+ if (light) console.log(' Mode: LIGHT(增量条目 + change:<name> 归因锚写入 changelog)');
592
596
 
593
597
  const absChangeDir = resolve(changeDir);
594
598
  const changeName = extractChangeName(absChangeDir);
@@ -643,7 +647,11 @@ async function main(argv, projectRoot) {
643
647
  console.log(`${mark} Step 3: resolveDeferred — ${deferredResult.resolved} items resolved`);
644
648
 
645
649
  // Step 4: appendChangelog
646
- const changelogPath = appendChangelog(ledgerDir, changeName, matrixContent, dryRun);
650
+ // v0.64.0 §4:light 模式在 changelog 首行写入归因锚 `change:<name>`——
651
+ // arch-merged-light 同型的 guard 归因判据(test-merged-light)依赖该锚,
652
+ // full 模式文件名即含 changeName,锚为显式加固不改变既有内容。
653
+ const changelogContent = light ? `<!-- change:${changeName} -->\n${matrixContent}` : matrixContent;
654
+ const changelogPath = appendChangelog(ledgerDir, changeName, changelogContent, dryRun);
647
655
  console.log(`${mark} Step 4: appendChangelog — ${relative(projectRoot, changelogPath)}`);
648
656
 
649
657
  // Step 5: rewriteIndex
@@ -71,9 +71,9 @@ Commands:
71
71
  arch scaffold Scaffold global docs/architecture/ ledger in generator format (v0.53.0 §102)
72
72
  arch precheck <change-dir> [--json]
73
73
  Emit deterministic architecture-gate evidence (v0.22 §88; evidence only, exit 0)
74
- arch-merge <change-dir> [--project-root <path>] [--dry-run]
74
+ arch-merge <change-dir> [--project-root <path>] [--dry-run] [--light]
75
75
  Merge architecture delta into global docs/architecture/
76
- test-merge <change-dir> [--project-root <path>] [--dry-run]
76
+ test-merge <change-dir> [--project-root <path>] [--dry-run] [--light]
77
77
  Merge test matrix results into global docs/test-ledger/
78
78
  test-matrix-export <input.json> <output.md> [--change-id <id>]
79
79
  Convert glaf4 test-matrix.json to team-flow test-matrix.md
@@ -90,7 +90,7 @@ Commands:
90
90
  pytest: terminal summary or junit XML (--junitxml)
91
91
  config [options] Display or modify configuration
92
92
  config --resolve-model <profile> Resolve a configured model profile without switching models
93
- state <sub> <dir> Manage .team-flow.yaml state (init|check|transition|get|rebuild)
93
+ state <sub> <dir> Manage .team-flow.yaml state (init|check|transition|get|rebuild|set|upgrade)
94
94
  inject <dir> Generate phase-guard artifacts; use --platforms <name|all> when platform is ambiguous
95
95
  audit <dir> Generate decision-point-audit.md from .team-flow.yaml
96
96
  checkpoint save <change-dir> --task <id> --next <text>
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: clean-code
3
- description: 代码结构质量判据 skill——可机械判定项阈值与计数口径、审查必答项、增量归因边界与共享层判定。
3
+ description: 代码结构质量判据 skill——可机械判定项阈值与计数口径、审查必答项、增量归因边界与共享层判定。限 team-flow 实施/审查流程内(code-reviewer、clean-code 门禁)调用,普通编码会话不触发(v0.64.0 §3.8 收紧)。
4
4
  user-invocable: false
5
5
  ---
6
6
 
@@ -16,6 +16,8 @@ description: This skill should be used when the user asks to "启动 Jarvis", "
16
16
 
17
17
  **核心边界**:Jarvis 只做决策与编排——**不写产线代码、不改 team-flow 状态文件(`.team-flow.yaml`)、不发明任务方向**。(「不改状态文件」专指 team-flow 的门禁真相源;Jarvis 自己的 `state/*.log|md` 当然要写。)
18
18
 
19
+ **前门回显(v0.64.0)**:接管/恢复任何 team-flow change 时,简报须读取并回显 `workflow_variant`(direct/planned/legacy/null)——direct/planned **轻路径无 DP-0~7 决策点可代答**(代答权限表对该两档恒 N/A);升档决策(`tf state upgrade`)属「挂起交 LT」类,不在代答权限内。
20
+
19
21
  > **与 firstmate 的分工**:Jarvis 是 **team-flow 专用**的决策代理,深度集成状态机 / 决策点 / 门禁;通用跨项目的「大副」角色由 firstmate 承担。两者不重叠——Jarvis 不做通用编码、调查或审计工作。
20
22
 
21
23
  > **命令形态以官方为准**:本文命令随 Orca 版本演进,**运行前先读一次** `orca skills get orchestration`,不要凭记忆推断子命令或参数——v0.60.0 实测暴露的 skill 缺陷之一即命令形态错误。