@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.
- package/.claude/always/phase-guard.md +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/marketplace.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/.github/plugin/marketplace.json +2 -2
- package/AGENTS.md +2 -2
- package/CHANGELOG.md +54 -0
- package/GEMINI.md +1 -1
- package/INSTALL.md +2 -2
- package/README.md +3 -3
- package/docs/README_en.md +1 -1
- package/docs/solutions/INDEX.md +0 -14
- package/gemini-extension.json +1 -1
- package/hooks/session-start +2 -2
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/scripts/ensure-branch.mjs +94 -4
- package/scripts/guard/checks/compound-captured.mjs +6 -3
- package/scripts/lib/cmd-repo-layout.mjs +51 -0
- package/scripts/lib/cmd-runtime.mjs +1 -0
- package/scripts/lib/config-loader.mjs +17 -1
- package/scripts/lib/conventions-generator.mjs +61 -2
- package/scripts/lib/execution-plan.mjs +82 -32
- package/scripts/lib/execution-recommendation.mjs +4 -18
- package/scripts/lib/glaf4-delegation.mjs +64 -26
- package/scripts/lib/hash.mjs +41 -2
- package/scripts/lib/prototype-sync.mjs +2 -1
- package/scripts/lib/sdd-overlay.mjs +3 -2
- package/scripts/lib/solutions-capture.mjs +2 -1
- package/scripts/lib/solutions-index-gen.mjs +2 -1
- package/scripts/lib/solutions-inject.mjs +2 -1
- package/scripts/lib/solutions-promote.mjs +90 -12
- package/scripts/lib/test-record.mjs +67 -21
- package/scripts/team-flow.mjs +3 -0
- package/skills/build-executor/SKILL.md +8 -2
- package/skills/ce-compound/references/promotion-rules.md +1 -1
- package/skills/ce-compound/references/schema.yaml +10 -1
- package/skills/contract-builder/references/glaf4-delegation.md +11 -7
- package/skills/release-archivist/SKILL.md +7 -8
- package/skills/release-archivist/references/closing-procedures.md +7 -2
- package/skills/workflow-orchestrator/SKILL.md +7 -0
- package/skills/workflow-start/SKILL.md +12 -1
- package/templates/learnings.md +51 -0
- package/docs/solutions/cross-phase/2026-07-28-no-summary.md +0 -17
- package/docs/solutions/cross-phase/2026-07-29-no-summary.md +0 -17
- package/docs/solutions/cross-phase/2026-07-30-no-summary.md +0 -17
- package/docs/solutions/cross-phase/2026-07-31-no-summary.md +0 -17
- package/docs/solutions/cross-phase/2026-08-01-no-summary.md +0 -17
- package/docs/solutions/cross-phase/2026-08-03-no-summary.md +0 -17
- package/docs/solutions/cross-phase/2026-08-04-no-summary.md +0 -17
- package/docs/solutions/cross-phase/2026-08-05-no-summary.md +0 -17
- package/docs/solutions/cross-phase/2026-08-06-no-summary.md +0 -17
- package/docs/solutions/cross-phase/2026-08-07-no-summary.md +0 -17
- package/docs/solutions/cross-phase/2026-08-17-no-summary.md +0 -17
- package/docs/solutions/cross-phase/2026-08-18-no-summary.md +0 -17
- package/docs/solutions/cross-phase/2026-08-19-no-summary.md +0 -17
- 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
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
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.
|
|
186
|
-
*
|
|
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?.
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
316
|
-
|
|
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
|
-
|
|
517
|
-
|
|
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 {
|
|
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
|
-
|
|
209
|
-
return
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
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 =
|
|
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
|
-
//
|
|
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(
|
|
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
|
-
|
|
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 !==
|
|
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(
|
|
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');
|
package/scripts/lib/hash.mjs
CHANGED
|
@@ -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]
|
|
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
|
-
|
|
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]
|
|
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]
|
|
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]
|
|
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('--', '');
|