@xulthekl/team-flow 0.47.0 → 0.49.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 (60) 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/AGENTS.md +2 -2
  9. package/CHANGELOG.md +54 -0
  10. package/GEMINI.md +1 -1
  11. package/INSTALL.md +2 -2
  12. package/README.md +3 -3
  13. package/docs/README_en.md +1 -1
  14. package/docs/solutions/INDEX.md +0 -14
  15. package/gemini-extension.json +1 -1
  16. package/hooks/session-start +2 -2
  17. package/llms.txt +1 -1
  18. package/package.json +1 -1
  19. package/plugin.json +1 -1
  20. package/scripts/ensure-branch.mjs +94 -4
  21. package/scripts/guard/checks/compound-captured.mjs +6 -3
  22. package/scripts/lib/cmd-repo-layout.mjs +51 -0
  23. package/scripts/lib/cmd-runtime.mjs +1 -0
  24. package/scripts/lib/config-loader.mjs +17 -1
  25. package/scripts/lib/conventions-generator.mjs +61 -2
  26. package/scripts/lib/execution-plan.mjs +82 -32
  27. package/scripts/lib/execution-recommendation.mjs +4 -18
  28. package/scripts/lib/glaf4-delegation.mjs +64 -26
  29. package/scripts/lib/hash.mjs +41 -2
  30. package/scripts/lib/prototype-sync.mjs +2 -1
  31. package/scripts/lib/sdd-overlay.mjs +3 -2
  32. package/scripts/lib/solutions-capture.mjs +2 -1
  33. package/scripts/lib/solutions-index-gen.mjs +2 -1
  34. package/scripts/lib/solutions-inject.mjs +2 -1
  35. package/scripts/lib/solutions-promote.mjs +90 -12
  36. package/scripts/lib/test-record.mjs +67 -21
  37. package/scripts/team-flow.mjs +3 -0
  38. package/skills/build-executor/SKILL.md +8 -2
  39. package/skills/ce-compound/references/promotion-rules.md +1 -1
  40. package/skills/ce-compound/references/schema.yaml +10 -1
  41. package/skills/contract-builder/references/glaf4-delegation.md +11 -7
  42. package/skills/release-archivist/SKILL.md +7 -8
  43. package/skills/release-archivist/references/closing-procedures.md +7 -2
  44. package/skills/workflow-orchestrator/SKILL.md +7 -0
  45. package/skills/workflow-start/SKILL.md +12 -1
  46. package/templates/learnings.md +51 -0
  47. package/docs/solutions/cross-phase/2026-07-28-no-summary.md +0 -17
  48. package/docs/solutions/cross-phase/2026-07-29-no-summary.md +0 -17
  49. package/docs/solutions/cross-phase/2026-07-30-no-summary.md +0 -17
  50. package/docs/solutions/cross-phase/2026-07-31-no-summary.md +0 -17
  51. package/docs/solutions/cross-phase/2026-08-01-no-summary.md +0 -17
  52. package/docs/solutions/cross-phase/2026-08-03-no-summary.md +0 -17
  53. package/docs/solutions/cross-phase/2026-08-04-no-summary.md +0 -17
  54. package/docs/solutions/cross-phase/2026-08-05-no-summary.md +0 -17
  55. package/docs/solutions/cross-phase/2026-08-06-no-summary.md +0 -17
  56. package/docs/solutions/cross-phase/2026-08-07-no-summary.md +0 -17
  57. package/docs/solutions/cross-phase/2026-08-17-no-summary.md +0 -17
  58. package/docs/solutions/cross-phase/2026-08-18-no-summary.md +0 -17
  59. package/docs/solutions/cross-phase/2026-08-19-no-summary.md +0 -17
  60. package/docs/solutions/cross-phase/2026-08-21-no-summary.md +0 -17
@@ -2,7 +2,7 @@ import { createHash, randomUUID } from 'node:crypto';
2
2
  import { execFileSync } from 'node:child_process';
3
3
  import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from 'node:fs';
4
4
  import { isAbsolute, join, relative, resolve, sep } from 'node:path';
5
- import { computeArtifactsHash, computeContractHash } from './hash.mjs';
5
+ import { computeArtifactsHash, computeContractHash, hashObject, stableJson } from './hash.mjs';
6
6
  import { getOverlayPaths } from './sdd-overlay.mjs';
7
7
  import { readState } from './state-loader.mjs';
8
8
  import { detectWorkspaceRoot, findSubRepo } from './git-utils.mjs';
@@ -55,11 +55,11 @@ export function writePlan(changeDir, plan) {
55
55
  mkdirSync(paths.root, { recursive: true });
56
56
  const previousPlan = readPlan(changeDir);
57
57
  if (previousPlan && (previousPlan.revision !== plan.revision || previousPlan.hash !== plan.hash)) {
58
- // v0.35.0: Review receipts are RETAINED across revisions — readCurrentReview
59
- // validates plan_hash/plan_revision matching, so stale receipts are
60
- // automatically invalidated without losing evidence (base/head/report SHA).
61
- // This prevents legitimate artifact updates (e.g., design.md 横展结论) from
62
- // causing cascading receipt loss. See workflow-feedback 20260805.
58
+ // v0.35.0: Review receipts are RETAINED across revisions.
59
+ // v0.49.0 §83.3.2: 有效性判据改为 wave 定义指纹 —— receipt 只在该 wave 的定义
60
+ // 变化时失效,与 plan revision/artifacts_hash 无关(历史 receipt 因此可跨
61
+ // revise/refresh-hash 继续生效);无 wave_fingerprint 的存量 receipt 回退旧判据。
62
+ // See workflow-feedback 20260805 / 20260909.
63
63
  //
64
64
  // Recommendation receipt is revision-scoped and must be cleaned:
65
65
  const recommendationFile = paths.executionRecommendation;
@@ -171,6 +171,8 @@ export function recordReview(changeDir, waveId, receipt, repoPath) {
171
171
  // Persist repo path when SHA was resolved in a sub-repository, so
172
172
  // readCurrentReview can re-validate without re-scanning.
173
173
  ...(subRepoPath ? { repo: relative(changeDir, subRepoPath) } : {}),
174
+ // v0.49.0 §83.3.2:主判据 —— wave 定义指纹;plan_hash/plan_revision 降级为审计信息
175
+ wave_fingerprint: computeWaveFingerprint(wave),
174
176
  plan_hash: plan.hash,
175
177
  plan_revision: plan.revision,
176
178
  recorded_at: new Date().toISOString(),
@@ -182,16 +184,25 @@ export function recordReview(changeDir, waveId, receipt, repoPath) {
182
184
  }
183
185
 
184
186
  /**
185
- * Returns the current plan's receipt for one wave. Receipts from a previous
186
- * revision/hash are never evidence for the current plan.
187
+ * Returns the current plan's receipt for one wave.
188
+ *
189
+ * v0.49.0 §83.3.2:有效性判据为 **wave 定义指纹**(id/strategy/tasks/depends_on)——
190
+ * receipt 只在该 wave 的定义变化时失效,与 plan 的 revision/artifacts_hash 无关。
191
+ * 无 wave_fingerprint 的存量 receipt(v0.48.0 及以前)回退旧的 plan 全内容判据。
187
192
  */
188
193
  export function readCurrentReview(changeDir, waveId, plan = readPlan(changeDir)) {
189
194
  if (!plan) return null;
195
+ const wave = Array.isArray(plan.waves) ? plan.waves.find(candidate => candidate?.id === waveId) : null;
196
+ if (!wave) return null;
190
197
  const filePath = join(getOverlayPaths(changeDir).reviews, `${safeFileName(waveId)}.json`);
191
198
  if (!existsSync(filePath)) return null;
192
199
  try {
193
200
  const receipt = JSON.parse(readFileSync(filePath, 'utf8'));
194
- if (receipt?.plan_hash !== plan.hash || receipt?.plan_revision !== plan.revision) return null;
201
+ if (receipt?.wave_fingerprint !== undefined) {
202
+ if (receipt.wave_fingerprint !== computeWaveFingerprint(wave)) return null;
203
+ } else if (receipt?.plan_hash !== plan.hash || receipt?.plan_revision !== plan.revision) {
204
+ return null;
205
+ }
195
206
  // When the receipt records a repo path (sub-repo scenario), use it for
196
207
  // SHA re-validation. Otherwise fall back to the default main-repo path.
197
208
  const repoPath = receipt?.repo ? resolve(changeDir, receipt.repo) : undefined;
@@ -218,6 +229,8 @@ export function describeWaves(changeDir, plan = readPlan(changeDir)) {
218
229
  const receipt = readCurrentReview(changeDir, wave.id, plan);
219
230
  const blockers = blockedDependencies(changeDir, plan, wave);
220
231
  const retryable = receipt?.status === 'fail';
232
+ // v0.49.0 §83.3.2:receipt 文件存在但未被采纳时给出审计线索(来源:workflow-feedback 20260909)
233
+ const staleReceipt = receipt === null ? readStaleReview(changeDir, wave.id) : null;
221
234
  return {
222
235
  id: wave.id,
223
236
  strategy: wave.strategy,
@@ -226,11 +239,32 @@ export function describeWaves(changeDir, plan = readPlan(changeDir)) {
226
239
  eligible: (receipt === null || retryable) && blockers.length === 0,
227
240
  retryable,
228
241
  receipt,
242
+ ...(staleReceipt ? { stale_receipt: staleReceipt } : {}),
229
243
  blockers,
230
244
  };
231
245
  });
232
246
  }
233
247
 
248
+ /**
249
+ * v0.49.0 §83.3.2:诊断信息 —— receipt 文件存在但因判据失配而未被采纳时返回审计线索。
250
+ * 不参与任何门禁判定(门禁只看 readCurrentReview)。
251
+ */
252
+ function readStaleReview(changeDir, waveId) {
253
+ const filePath = join(getOverlayPaths(changeDir).reviews, `${safeFileName(waveId)}.json`);
254
+ if (!existsSync(filePath)) return null;
255
+ try {
256
+ const receipt = JSON.parse(readFileSync(filePath, 'utf8'));
257
+ return {
258
+ status: receipt?.status ?? null,
259
+ plan_revision: receipt?.plan_revision ?? null,
260
+ recorded_at: receipt?.recorded_at ?? null,
261
+ reason: 'the wave definition changed after this receipt was recorded',
262
+ };
263
+ } catch {
264
+ return null;
265
+ }
266
+ }
267
+
234
268
  function validateReviewReportEvidence(changeDir, report) {
235
269
  requireText(report, 'receipt.report');
236
270
  if (/[\p{Cc}\p{Zl}\p{Zp}]/u.test(report)) {
@@ -271,12 +305,12 @@ function getPhysicalReviewsDirectory(changeDir) {
271
305
  let directory = changeRoot;
272
306
  for (const component of ['.superpowers', 'sdd', 'reviews']) {
273
307
  directory = join(directory, component);
274
- let metadata;
275
- try {
276
- metadata = lstatSync(directory);
277
- } catch (error) {
278
- throw new Error(`Review report evidence cannot read the ${component} overlay directory: ${error.message}`);
308
+ // multi-repo-support v1.0 §4.2(cli-command-2 修复):overlay 目录自动创建,修 ENOENT
309
+ // (首次 review 时 .superpowers/sdd/reviews/ 尚不存在,recordReview 写入前先建)
310
+ if (!existsSync(directory)) {
311
+ mkdirSync(directory, { recursive: true });
279
312
  }
313
+ const metadata = lstatSync(directory);
280
314
  if (metadata.isSymbolicLink() || !metadata.isDirectory()) {
281
315
  throw new Error('Review report evidence requires physical .superpowers/sdd/reviews overlay directories');
282
316
  }
@@ -312,8 +346,11 @@ function validateReviewRange(changeDir, base, head, repoPath) {
312
346
  // This happens either through automatic fallback (subRepoPath set above)
313
347
  // or through explicit --repo pointing to a different repository.
314
348
  if (!subRepoPath && repoPath) {
315
- const defaultRoot = resolveGitRoot(changeDir);
316
- if (gitRoot !== defaultRoot) subRepoPath = gitRoot;
349
+ // multi-repo-support v1.0 §4.2(cli-command-2 修复):--repo 场景下 change 目录可能无 git
350
+ //(多仓库 changes/ 未版本化),defaultRoot 比较容错返回 null,不因 change 目录无 git 报错
351
+ let defaultRoot = null;
352
+ try { defaultRoot = resolveGitRoot(changeDir); } catch {}
353
+ if (defaultRoot && gitRoot !== defaultRoot) subRepoPath = gitRoot;
317
354
  }
318
355
 
319
356
  const effectiveRoot = subRepoPath || gitRoot;
@@ -513,13 +550,35 @@ function hasDependencyCycle(waves) {
513
550
  }
514
551
 
515
552
  function hashPlan(plan) {
516
- const { hash, ...content } = plan;
517
- return `sha256:${createHash('sha256').update(stableJson(content)).digest('hex')}`;
553
+ // v0.49.0 §83.3.3:stableJson/hashObject 抽到 hash.mjs 共享(原为本文件与
554
+ // execution-recommendation.mjs 各一份副本)
555
+ return hashObject(plan);
556
+ }
557
+
558
+ /**
559
+ * v0.49.0 §83.3.2(来源:workflow-feedback 2026-09-09):wave 定义指纹 —— review receipt 的有效性判据。
560
+ *
561
+ * receipt 证明的是"该 wave 的实现在 base..head 间完成并已审查",其语义依赖仅为该 wave 的定义
562
+ * (id/strategy/tasks/depends_on)+ git 事实 + 审查报告,**不含** plan 的其余内容
563
+ * (rationale / artifacts_hash / contract_hash / revision / recommendation_receipt)。
564
+ * 绑定 plan 全内容会使任何与 wave 无关的变更(refresh-hash 刷新制品 hash、revise 升号)
565
+ * 误杀全部历史 receipt,且无补录路径。
566
+ */
567
+ function computeWaveFingerprint(wave) {
568
+ return `sha256:${createHash('sha256').update(stableJson({
569
+ id: wave?.id,
570
+ strategy: wave?.strategy,
571
+ tasks: wave?.tasks,
572
+ depends_on: wave?.depends_on,
573
+ })).digest('hex')}`;
518
574
  }
519
575
 
520
576
  /**
521
577
  * v0.35.0: Refresh plan's artifacts_hash and contract_hash without bumping revision.
522
578
  * Returns the updated plan (already persisted via writePlan), or null if no change needed.
579
+ *
580
+ * v0.49.0 §83.3.3:同步刷新内嵌 recommendation_receipt 的两个 hash(两者语义恒等),
581
+ * 否则 refresh-hash 之后 validatePlan 立即 FAIL(来源:workflow-feedback 20260909)。
523
582
  */
524
583
  export function refreshPlanHash(changeDir) {
525
584
  const plan = readPlan(changeDir);
@@ -531,6 +590,11 @@ export function refreshPlanHash(changeDir) {
531
590
  }
532
591
  plan.artifacts_hash = newArtifactsHash;
533
592
  plan.contract_hash = newContractHash;
593
+ if (plan.recommendation_receipt) {
594
+ plan.recommendation_receipt.artifacts_hash = newArtifactsHash;
595
+ plan.recommendation_receipt.contract_hash = newContractHash;
596
+ plan.recommendation_receipt.hash = hashObject(plan.recommendation_receipt);
597
+ }
534
598
  plan.hash = hashPlan(plan);
535
599
  return writePlan(changeDir, plan);
536
600
  }
@@ -544,20 +608,6 @@ function tryHashPlan(plan) {
544
608
  }
545
609
  }
546
610
 
547
- function stableJson(value, seen = new WeakSet()) {
548
- if (value === null || typeof value !== 'object') return JSON.stringify(value);
549
- if (seen.has(value)) throw new Error('circular plan data');
550
- seen.add(value);
551
- if (Array.isArray(value)) {
552
- const result = `[${value.map(item => stableJson(item, seen)).join(',')}]`;
553
- seen.delete(value);
554
- return result;
555
- }
556
- const result = `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${stableJson(value[key], seen)}`).join(',')}}`;
557
- seen.delete(value);
558
- return result;
559
- }
560
-
561
611
  function writeExecutionPlanSummary(changeDir, plan) {
562
612
  const statePath = join(changeDir, '.team-flow.yaml');
563
613
  const state = readState(changeDir);
@@ -1,10 +1,10 @@
1
1
  // Evidence-based execution-mode recommendation for DP-4.
2
2
 
3
- import { createHash, randomUUID } from 'node:crypto';
3
+ import { randomUUID } from 'node:crypto';
4
4
  import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
5
5
  import { join } from 'node:path';
6
6
  import { loadConfig } from './config-loader.mjs';
7
- import { computeArtifactsHash, computeContractHash } from './hash.mjs';
7
+ import { computeArtifactsHash, computeContractHash, hashObject, stableJson } from './hash.mjs';
8
8
  import { getOverlayPaths } from './sdd-overlay.mjs';
9
9
  import { readState } from './state-loader.mjs';
10
10
  import { EXECUTION_MODES } from './execution-plan.mjs';
@@ -205,22 +205,8 @@ function normalizeWaves(waves) {
205
205
  }
206
206
 
207
207
  function hashReceipt(receipt) {
208
- const { hash, ...content } = receipt || {};
209
- return `sha256:${createHash('sha256').update(stableJson(content)).digest('hex')}`;
210
- }
211
-
212
- function stableJson(value, seen = new WeakSet()) {
213
- if (value === null || typeof value !== 'object') return JSON.stringify(value);
214
- if (seen.has(value)) throw new Error('circular recommendation data');
215
- seen.add(value);
216
- if (Array.isArray(value)) {
217
- const result = `[${value.map(item => stableJson(item, seen)).join(',')}]`;
218
- seen.delete(value);
219
- return result;
220
- }
221
- const result = `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${stableJson(value[key], seen)}`).join(',')}}`;
222
- seen.delete(value);
223
- return result;
208
+ // v0.49.0 §83.3.3:stableJson/hashObject 抽到 hash.mjs 共享
209
+ return hashObject(receipt);
224
210
  }
225
211
 
226
212
  function atomicWrite(targetPath, content) {
@@ -3,8 +3,10 @@
3
3
  * glaf4-delegation — GLAF4 委托协议 CLI(v2.1 §6.6/§6.3/§6.7,P2 实施委托全链路)
4
4
  *
5
5
  * 用法:
6
- * tf glaf4-delegation detect <root> [--delegation-mode <mode>] [--json]
7
- * 运行时探测(v2.1 §4 接口①,C1 修复接线):三态检测 + 写 .team-flow/team-flow.config.json 的 glaf4_dev 段。
6
+ * tf glaf4-delegation detect <root> [--delegation-mode <mode>] [--target-repo <模块路径>] [--json]
7
+ * 运行时探测(v2.1 §4 接口①,C1 修复接线 + multi-repo-support v1.0 §4.1):
8
+ * root 解析(change 目录→项目根)+ 写 .team-flow/team-flow.config.json 的 glaf4_dev 段(项目根)。
9
+ * multi-repo 下按 repo_layout 清单聚合判定 is_glaf4_java;--target-repo 精确探测指定模块。
8
10
  * contract-builder 产出 execution-contract 时(DP-4 前)显式调用此命令,recommend 才能读到 glaf4_dev。
9
11
  * tf glaf4-delegation confirm <change-dir> --mode <mode> --write-set <json> [--json]
10
12
  * DP-4 委托方案确认:mode 枚举标量入 state,write_set_boundary 落 .superpowers/glaf4/write-set.json,
@@ -28,7 +30,9 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync
28
30
  import { join, isAbsolute, relative, resolve, normalize, sep } from 'node:path';
29
31
  import { readState, writeState } from './state-loader.mjs';
30
32
  import { getOverlayPaths } from './sdd-overlay.mjs';
31
- import { detectGlaf4Delegation, writeGlaf4DevConfig } from './conventions-generator.mjs';
33
+ import { detectGlaf4Delegation, detectTechStack, writeGlaf4DevConfig } from './conventions-generator.mjs';
34
+ import { detectWorkspaceRoot, getGitRoot } from './git-utils.mjs';
35
+ import { loadConfig } from './config-loader.mjs';
32
36
 
33
37
  // DP-4 委托模式合法枚举(对应 glaf4-dev 七模式中可委托 change 的六种)
34
38
  export const GLAF4_DELEGATION_MODES = [
@@ -72,26 +76,42 @@ export function hashWriteSet(writeSet) {
72
76
  * @param {string} [delegationMode] 覆盖 delegation_mode(auto/manual/off)
73
77
  * @returns {object} { ok, is_glaf4_java, glaf4_dev_available, glaf4_dev_version, delegation_mode, configPath }
74
78
  */
75
- export function detectAndWriteConfig(root, delegationMode) {
76
- const detection = detectGlaf4Delegation(root, { delegationMode });
79
+ export function detectAndWriteConfig(root, delegationMode, targetRepo) {
80
+ // multi-repo-support-design v1.0 §4.1(cli-command-1 修复):root 解析——change 目录(含 changes/)→ 项目根;否则 git 根
81
+ const projectRoot = resolveProjectRoot(root);
82
+
83
+ // 读 repo_layout(config 已固化时)
84
+ const config = loadConfig(projectRoot);
85
+ const repoLayout = config?.repo_layout || null;
86
+
87
+ // 探测目标构造(P3 自查补全 + P4 validator W1 放宽,2026-08-21):
88
+ // --target-repo 精确 > repo_layout 清单聚合(repos 非空即聚合,不限 multi-repo——
89
+ // 壳仓库含 package.json 误判 monorepo 时也应聚合,否则 cli-command-1 在真实项目失效)>
90
+ // root 有技术栈(探测 root,如 detect(svcA))>
91
+ // root 无技术栈(change 文档目录/容器根)→ 探测解析后项目根(单仓库项目根有 pom.xml)
92
+ let repos = null;
93
+ let probeRoot = root;
94
+ if (targetRepo) {
95
+ probeRoot = targetRepo;
96
+ } else if (repoLayout?.repos && Object.keys(repoLayout.repos).length > 0) {
97
+ repos = Object.values(repoLayout.repos);
98
+ } else if (!detectTechStack(root).language) {
99
+ probeRoot = projectRoot;
100
+ }
101
+ const detection = detectGlaf4Delegation(probeRoot, { delegationMode, repos });
77
102
 
78
- // 复评 C1 嵌套工程根盲区(2026-08-18):git root target_root(monorepo 布局)时,
79
- // loadConfig(changeDir) 只查 changeDir→git root→home,不查 target_root → 委托链静默断裂。
80
- // 写 git root 冗余副本保证从任意 changeDir 都能读到探测结果。gitRoot 提前解析供 mode 优先级使用。
103
+ // git root(冗余副本判断 + mode 优先级)
81
104
  let gitRoot = null;
82
105
  try {
83
- gitRoot = execFileSync('git', ['rev-parse', '--show-toplevel'], {
84
- cwd: root, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'],
85
- }).trim();
106
+ gitRoot = getGitRoot(projectRoot);
86
107
  } catch {
87
108
  // 非 git 仓库,跳过
88
109
  }
89
110
 
90
- // 第四轮复评 Important(2026-08-18):delegation_mode 优先级链补全——显式 > 目标 root 已有 > git root 已有 > auto
91
- // 上轮只查目标 root,非 Java 服务无本地 config → 默认 auto → 覆盖 git root 副本的 off(用户 off 意图被静默翻转)。
111
+ // delegation_mode 优先级链:显式 > 目标 root 已有 > git root 已有 > auto
92
112
  let effectiveMode = delegationMode;
93
113
  if (!effectiveMode) {
94
- effectiveMode = readGlaf4DelegationMode(root)
114
+ effectiveMode = readGlaf4DelegationMode(projectRoot)
95
115
  || (gitRoot ? readGlaf4DelegationMode(gitRoot) : null)
96
116
  || 'auto';
97
117
  }
@@ -100,16 +120,17 @@ export function detectAndWriteConfig(root, delegationMode) {
100
120
  version: detection.glaf4_dev_version,
101
121
  delegationMode: effectiveMode,
102
122
  };
103
- writeGlaf4DevConfig(root, info);
123
+ // config 主副本写项目根(cli-command-1 修复:不再写 change 目录)
124
+ // multi-repo-support v1.0:OR 聚合保留已有 enabled=true——项目级 GLAF4 能力一旦声明,
125
+ // 不因后续非 Java 目标探测而覆盖丢失(非 Java change detect 不应禁用项目委托)
126
+ const existingProjEnabled = readGitRootGlaf4Enabled(projectRoot);
127
+ const projEnabled = info.enabled || existingProjEnabled;
128
+ writeGlaf4DevConfig(projectRoot, { ...info, enabled: projEnabled, delegationMode: effectiveMode });
104
129
 
105
130
  let gitRootConfig = null;
106
- if (gitRoot && gitRoot !== root) {
107
- // 第三轮复评 Important①(2026-08-18):多服务混合 monorepo 下 git root 副本 last-write-wins 污染——
108
- // 非 Java 服务 detect 会把 Java 服务的 enabled=true 覆盖成 false。改为 OR 聚合:
109
- // 项目级能力一旦声明(任一服务 enabled=true),不因后续非 Java 服务探测而丢失。
131
+ if (gitRoot && gitRoot !== projectRoot) {
110
132
  const existingEnabled = readGitRootGlaf4Enabled(gitRoot);
111
133
  const aggregatedEnabled = info.enabled || existingEnabled;
112
- // 第四轮复评:git root 副本 mode 保留已有值(显式传参才覆盖)——用户 off 意图不因新探测静默翻转。
113
134
  const gitRootMode = readGlaf4DelegationMode(gitRoot);
114
135
  const gitRootEffectiveMode = delegationMode || gitRootMode || effectiveMode;
115
136
  writeGlaf4DevConfig(gitRoot, { ...info, enabled: aggregatedEnabled, delegationMode: gitRootEffectiveMode });
@@ -119,11 +140,27 @@ export function detectAndWriteConfig(root, delegationMode) {
119
140
  return {
120
141
  ok: true,
121
142
  ...detection,
122
- configPath: join(root, '.team-flow', 'team-flow.config.json'),
143
+ configPath: join(projectRoot, '.team-flow', 'team-flow.config.json'),
123
144
  gitRootConfig,
145
+ projectRoot,
124
146
  };
125
147
  }
126
148
 
149
+ /**
150
+ * root 解析(multi-repo-support-design v1.0 §4.1):change 目录(含 changes/)→ 项目根;否则 git 根;兜底直接用 root
151
+ * @param {string} root
152
+ * @returns {string}
153
+ */
154
+ function resolveProjectRoot(root) {
155
+ const ws = detectWorkspaceRoot(root);
156
+ if (ws) return ws;
157
+ try {
158
+ return getGitRoot(root);
159
+ } catch {
160
+ return root;
161
+ }
162
+ }
163
+
127
164
  /**
128
165
  * 读 git root 现有 config 的 glaf4_dev.enabled(第三轮复评 Important① OR 聚合用)
129
166
  * @param {string} gitRoot
@@ -394,7 +431,7 @@ export function resetDelegation(changeDir) {
394
431
 
395
432
  export async function run(args) {
396
433
  const positionals = args.filter(a => !a.startsWith('--'));
397
- const flags = new Set(args.filter(a => a.startsWith('--') && a !== '--mode' && a !== '--write-set' && a !== '--run-dir' && a !== '--target-root' && a !== '--json'));
434
+ const flags = new Set(args.filter(a => a.startsWith('--') && a !== '--mode' && a !== '--write-set' && a !== '--run-dir' && a !== '--target-root' && a !== '--target-repo' && a !== '--delegation-mode' && a !== '--json'));
398
435
  const opt = (name) => {
399
436
  const i = args.indexOf(`--${name}`);
400
437
  return i >= 0 ? args[i + 1] : undefined;
@@ -404,7 +441,7 @@ export async function run(args) {
404
441
  const json = flags.has('--json') || args.includes('--json');
405
442
 
406
443
  const usage = `Usage:
407
- tf glaf4-delegation detect <root> [--delegation-mode <auto|manual|off>]
444
+ tf glaf4-delegation detect <root> [--delegation-mode <auto|manual|off>] [--target-repo <模块路径>]
408
445
  tf glaf4-delegation confirm <change-dir> --mode <mode> --write-set <json-file>
409
446
  tf glaf4-delegation verify-write-set <change-dir> --run-dir <run-dir>
410
447
  tf glaf4-delegation verify-tasks <change-dir>
@@ -424,15 +461,16 @@ export async function run(args) {
424
461
  try {
425
462
  if (subcommand === 'detect') {
426
463
  const mode = opt('delegation-mode');
464
+ const targetRepo = opt('target-repo');
427
465
  // 第四轮复评 Minor-1(2026-08-18):枚举校验——非法值(如 'foo')等同 auto 放行会让用户配置失真
428
466
  if (mode !== undefined && !['auto', 'manual', 'off'].includes(mode)) {
429
467
  console.error(`--delegation-mode 必须为 auto/manual/off 之一(实际: ${mode})`);
430
468
  process.exit(2);
431
469
  }
432
- const result = detectAndWriteConfig(changeDir, mode);
470
+ const result = detectAndWriteConfig(changeDir, mode, targetRepo);
433
471
  const msg = result.glaf4_dev_available
434
- ? `探测: is_glaf4_java=${result.is_glaf4_java}, glaf4-dev v${result.glaf4_dev_version} 可用, delegation_mode=${result.delegation_mode} → config 已写 ${result.configPath}`
435
- : `探测: is_glaf4_java=${result.is_glaf4_java}, glaf4-dev 不可用 → config 已写 ${result.configPath}`;
472
+ ? `探测: is_glaf4_java=${result.is_glaf4_java}, glaf4-dev v${result.glaf4_dev_version} 可用, delegation_mode=${result.delegation_mode} → config 已写 ${result.configPath}${result.projectRoot ? `(项目根 ${result.projectRoot})` : ''}`
473
+ : `探测: is_glaf4_java=${result.is_glaf4_java}, glaf4-dev 不可用 → config 已写 ${result.configPath}${result.projectRoot ? `(项目根 ${result.projectRoot})` : ''}`;
436
474
  print(result.ok, result, msg);
437
475
  } else if (subcommand === 'confirm') {
438
476
  const mode = opt('mode');
@@ -4,6 +4,45 @@ import fs from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import { findCanonicalSpecFiles } from './spec-paths.mjs';
6
6
 
7
+ /**
8
+ * Normalize tasks.md checkbox state: `- [x]` / `- [X]` → `- [ ]`.
9
+ *
10
+ * v0.49.0 §83.3.1(来源:workflow-feedback 2026-09-09):tasks.md 同时是规划制品
11
+ * (artifacts_hash 的冻结对象)与执行台账(tasks-complete guard 要求执行期回写勾选)。
12
+ * 勾选状态属执行进度、不属规划内容 —— 归一化后入 hash,使"执行期勾选"不再使 plan
13
+ * 过期(否则勾选后 recordReview 因 plan stale 抛错,形成 closing 死锁)。
14
+ * 任务文本、数量与结构的变更仍会改变 hash。
15
+ */
16
+ export function normalizeCheckboxes(content) {
17
+ return content.replace(/^([ \t]*- )\[[xX]\]/gm, '$1[ ]');
18
+ }
19
+
20
+ /**
21
+ * 稳定序列化:对象键排序后递归序列化,保证同一内容产生同一字符串。
22
+ *
23
+ * v0.49.0 §83.3.3:从 execution-plan.mjs / execution-recommendation.mjs 抽取共享
24
+ * (原为两份实现),供 plan hash 与 recommendation receipt hash 共用。
25
+ */
26
+ export function stableJson(value, seen = new WeakSet()) {
27
+ if (value === null || typeof value !== 'object') return JSON.stringify(value);
28
+ if (seen.has(value)) throw new Error('circular data');
29
+ seen.add(value);
30
+ if (Array.isArray(value)) {
31
+ const result = `[${value.map(item => stableJson(item, seen)).join(',')}]`;
32
+ seen.delete(value);
33
+ return result;
34
+ }
35
+ const result = `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${stableJson(value[key], seen)}`).join(',')}}`;
36
+ seen.delete(value);
37
+ return result;
38
+ }
39
+
40
+ /** 通用对象 hash:剔除顶层 `hash` 字段后取 sha256。 */
41
+ export function hashObject(value) {
42
+ const { hash, ...content } = value ?? {};
43
+ return `sha256:${crypto.createHash('sha256').update(stableJson(content)).digest('hex')}`;
44
+ }
45
+
7
46
  // Compute a joint SHA256 hash of all 4 planning artifacts.
8
47
  // Input: proposal.md + specs/*/spec.md (sorted) + design.md + tasks.md
9
48
  export function computeArtifactsHash(changeDir) {
@@ -30,10 +69,10 @@ export function computeArtifactsHash(changeDir) {
30
69
  hasContent = true;
31
70
  }
32
71
 
33
- // tasks.md
72
+ // tasks.md(v0.49.0 §83.3.1:勾选状态归一化后入 hash —— 执行进度不属规划内容)
34
73
  const tasks = path.join(changeDir, 'tasks.md');
35
74
  if (fs.existsSync(tasks)) {
36
- hash.update(fs.readFileSync(tasks, 'utf-8'));
75
+ hash.update(normalizeCheckboxes(fs.readFileSync(tasks, 'utf-8')));
37
76
  hasContent = true;
38
77
  }
39
78
 
@@ -16,6 +16,7 @@
16
16
 
17
17
  import { readFileSync, writeFileSync, existsSync, cpSync, mkdirSync } from 'node:fs';
18
18
  import { join, basename, relative, sep, resolve } from 'node:path';
19
+ import { pathToFileURL } from 'node:url';
19
20
 
20
21
  /**
21
22
  * 解析 CLI 参数数组为结构化对象
@@ -216,7 +217,7 @@ export function run(args = {}) {
216
217
  }
217
218
 
218
219
  // 支持直接执行:node prototype-sync.mjs <change-dir> --source <path>
219
- if (process.argv[1]?.includes('prototype-sync')) {
220
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
220
221
  const args = { _: [] };
221
222
  for (let i = 2; i < process.argv.length; i++) {
222
223
  if (process.argv[i].startsWith('--')) {
@@ -3,7 +3,7 @@ import {
3
3
  existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync,
4
4
  } from 'node:fs';
5
5
  import { dirname, join } from 'node:path';
6
- import { computeArtifactsHash } from './hash.mjs';
6
+ import { computeArtifactsHash, normalizeCheckboxes } from './hash.mjs';
7
7
  import { readState } from './state-loader.mjs';
8
8
 
9
9
  export const HANDOFF_TYPES = new Set(['prototype', 'research', 'experiment']);
@@ -30,7 +30,8 @@ export function computeTaskHash(changeDir, taskId) {
30
30
  const escaped = taskId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
31
31
  const match = tasks.match(new RegExp(`^- \\[([ xX])\\] ${escaped}\\s+.+$`, 'm'));
32
32
  if (!match) throw new Error(`Task '${taskId}' was not found in tasks.md`);
33
- return `sha256:${createHash('sha256').update(match[0]).digest('hex')}`;
33
+ // v0.49.0 §83.3.1:勾选状态归一化 —— 勾选是执行进度,不应使 checkpoint 变 stale
34
+ return `sha256:${createHash('sha256').update(normalizeCheckboxes(match[0])).digest('hex')}`;
34
35
  }
35
36
 
36
37
  export function saveCheckpoint(changeDir, input) {
@@ -13,6 +13,7 @@
13
13
 
14
14
  import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync } from 'node:fs';
15
15
  import { join } from 'node:path';
16
+ import { pathToFileURL } from 'node:url';
16
17
 
17
18
  const PHASES = ['prd', 'plan', 'architecture', 'prototype', 'spec', 'build', 'review', 'cross-phase'];
18
19
  const MAX_INDEX_LINES = 150;
@@ -89,7 +90,7 @@ ${summary}
89
90
  return { file: `${phase}/${fileName}`, phase, domain, type, severity };
90
91
  }
91
92
 
92
- if (process.argv[1]?.includes('solutions-capture')) {
93
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
93
94
  const args = {};
94
95
  for (let i = 2; i < process.argv.length; i++) {
95
96
  if (process.argv[i].startsWith('--')) {
@@ -15,6 +15,7 @@
15
15
 
16
16
  import { readFileSync, writeFileSync, readdirSync, statSync, existsSync } from 'node:fs';
17
17
  import { join } from 'node:path';
18
+ import { pathToFileURL } from 'node:url';
18
19
 
19
20
  const PHASES = ['prd', 'plan', 'prototype', 'spec', 'build', 'review', 'cross-phase'];
20
21
  const SEVERITY_ORDER = { high: 0, medium: 1, low: 2 };
@@ -104,7 +105,7 @@ export function run(args = {}) {
104
105
  }
105
106
 
106
107
  // CLI 直接执行
107
- if (process.argv[1]?.includes('solutions-index-gen')) {
108
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
108
109
  const args = {};
109
110
  const dirIdx = process.argv.indexOf('--dir');
110
111
  if (dirIdx > -1 && process.argv[dirIdx + 1]) args.dir = process.argv[dirIdx + 1];
@@ -14,6 +14,7 @@
14
14
 
15
15
  import { readFileSync, existsSync } from 'node:fs';
16
16
  import { join } from 'node:path';
17
+ import { pathToFileURL } from 'node:url';
17
18
 
18
19
  const SEVERITY_ORDER = { high: 0, medium: 1, low: 2 };
19
20
 
@@ -66,7 +67,7 @@ export function run(args = {}) {
66
67
  return { entries: top5 };
67
68
  }
68
69
 
69
- if (process.argv[1]?.includes('solutions-inject')) {
70
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
70
71
  const args = {};
71
72
  for (let i = 2; i < process.argv.length; i += 2) {
72
73
  const key = process.argv[i]?.replace('--', '');