@xulthekl/team-flow 0.39.1 → 0.40.1
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/GEMINI.md +1 -1
- package/INSTALL.md +1 -1
- package/README.md +1 -1
- package/docs/README_en.md +1 -1
- package/docs/solutions/INDEX.md +1 -0
- package/docs/solutions/cross-phase/2026-08-07-no-summary.md +17 -0
- 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 +2 -8
- package/scripts/lib/arch-merge.mjs +11 -10
- package/scripts/lib/cmd-deisolate.mjs +1 -5
- package/scripts/lib/cmd-execution.mjs +3 -2
- package/scripts/lib/cmd-prototype.mjs +30 -13
- package/scripts/lib/cmd-publish.mjs +16 -4
- package/scripts/lib/execution-plan.mjs +78 -18
- package/scripts/lib/git-utils.mjs +90 -0
- package/skills/ce-brainstorm/SKILL.md +65 -2
- package/skills/ce-brainstorm/references/brainstorm-sections.md +53 -9
- package/skills/ce-brainstorm/references/business-processes.md +140 -0
- package/skills/ce-brainstorm/references/business-scenarios.md +122 -0
- package/skills/ce-brainstorm/references/evidence-chain-validation.md +114 -0
- package/skills/ce-brainstorm/references/phase0-routing.md +7 -1
- package/skills/ce-brainstorm/references/prd-mapping.md +36 -8
- package/skills/ce-brainstorm/references/synthesis-summary.md +21 -0
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
{
|
|
10
10
|
"name": "team-flow",
|
|
11
11
|
"description": "8-state spec workflow + compound global compounding + architecture-design (4A/DDD) + local HTML prototype + product-level orchestration + bootstrap + e2e + session handoff + workflow feedback. 24 skills + 15 agents with embedded TDD, SDD, code review, debugging, delta spec sync, and design-system-driven prototyping.",
|
|
12
|
-
"version": "0.
|
|
12
|
+
"version": "0.40.1",
|
|
13
13
|
"source": "./",
|
|
14
14
|
"author": {
|
|
15
15
|
"name": "LT",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "team-flow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.1",
|
|
4
4
|
"description": "Unified workflow plugin: team-flow (spec-driven dev: TDD/SDD/code-review/debugging) + compound-engineering core subset (brainstorm/plan/compound/strategy/ideate/proof, global compounding) + architecture-design (4A+DDD incremental design & global compounding) + prototype (local HTML prototype, zero external deps) + e2e (Playwright E2E, AC-driven) + workflow-orchestrator (product-level workflow orchestration) + workflow-bootstrap (existing project onboarding) + session-handoff (context transfer) + workflow-feedback (issue tracking). 24 skills + 15 agents, one install.",
|
|
5
5
|
"source": "./",
|
|
6
6
|
"author": {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "team-flow",
|
|
3
3
|
"displayName": "team-flow",
|
|
4
4
|
"description": "Unified workflow plugin: team-flow (spec-driven dev: TDD/SDD/code-review/debugging) + compound-engineering core subset (brainstorm/plan/compound/strategy/ideate/proof, global compounding) + architecture-design (4A+DDD incremental design & global compounding) + prototype (local HTML prototype, zero external deps) + e2e (Playwright E2E, AC-driven) + workflow-orchestrator (product-level workflow orchestration) + workflow-bootstrap (existing project onboarding) + session-handoff (context transfer) + workflow-feedback (issue tracking). 24 skills + 15 agents, one install.",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.40.1",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "LT",
|
|
8
8
|
"url": "https://github.com/LT"
|
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "Unified workflow plugins and skills for AI coding agents (team-flow: team-flow + compound + architecture-design + prototype).",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.40.1"
|
|
10
10
|
},
|
|
11
11
|
"plugins": [
|
|
12
12
|
{
|
|
13
13
|
"name": "team-flow",
|
|
14
14
|
"description": "Unified workflow with planning artifacts, execution contracts, TDD, review gates, systematic debugging, delta spec sync, architecture-design, and local HTML prototyping.",
|
|
15
|
-
"version": "0.
|
|
15
|
+
"version": "0.40.1",
|
|
16
16
|
"source": ".",
|
|
17
17
|
"author": {
|
|
18
18
|
"name": "LT",
|
package/GEMINI.md
CHANGED
|
@@ -8,7 +8,7 @@ The workflow is self-contained and does not require OpenSpec or Superpowers at r
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
<!-- team-flow-phase-guard-start -->
|
|
11
|
-
# team-flow v0.
|
|
11
|
+
# team-flow v0.40.1 | 阶段: {{state}} | 工作流: {{workflow}}
|
|
12
12
|
当前阶段允许的操作由 workflow-start 路由规则定义。
|
|
13
13
|
禁止跨越 DP gate 进入下一阶段。变更范围以 execution-contract.md 的 Intent Lock 为准。
|
|
14
14
|
<!-- team-flow-phase-guard-end -->
|
package/INSTALL.md
CHANGED
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# team-flow
|
|
2
2
|
|
|
3
|
-
> 当前版本:`v0.
|
|
3
|
+
> 当前版本:`v0.40.1`
|
|
4
4
|
|
|
5
5
|
> 统一插件:**team-flow**(spec 驱动开发)+ **compound-engineering 核心子集**(全局复利)+ **architecture-design**(4A+DDD 增量设计)+ **prototype**(本地 HTML 原型)+ **e2e**(AC 驱动 E2E)+ **workflow-orchestrator**(产品级编排)+ **workflow-bootstrap**(既有项目接入)。一次安装,七套能力协同。
|
|
6
6
|
|
package/docs/README_en.md
CHANGED
|
@@ -126,7 +126,7 @@ npm install -g team-flow
|
|
|
126
126
|
|
|
127
127
|
### Version
|
|
128
128
|
|
|
129
|
-
- Current: `v0.
|
|
129
|
+
- Current: `v0.40.1`
|
|
130
130
|
- v0.9.1 highlights: DP-4 execution-mode recommendations, a portable runtime across 17 platforms, and a raw-package smoke with no plugin-root variable.
|
|
131
131
|
- Self-contained — no OpenSpec or Superpowers runtime required
|
|
132
132
|
- Upstream: [Fission-AI/OpenSpec](https://github.com/Fission-AI/OpenSpec), [obra/superpowers](https://github.com/obra/superpowers)
|
package/docs/solutions/INDEX.md
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
<!-- 每条一行,按 severity 降序,≤150 行硬上限 -->
|
|
3
3
|
| date | phase | domain | type | severity | summary | file |
|
|
4
4
|
|------|-------|--------|------|----------|---------|------|
|
|
5
|
+
| 2026-08-07 | cross-phase | general | insight | medium | (no summary) | cross-phase/2026-08-07-no-summary.md |
|
|
5
6
|
| 2026-08-06 | cross-phase | general | insight | medium | (no summary) | cross-phase/2026-08-06-no-summary.md |
|
|
6
7
|
| 2026-08-05 | cross-phase | general | insight | medium | (no summary) | cross-phase/2026-08-05-no-summary.md |
|
|
7
8
|
| 2026-08-04 | cross-phase | general | insight | medium | (no summary) | cross-phase/2026-08-04-no-summary.md |
|
package/gemini-extension.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "team-flow",
|
|
3
3
|
"description": "Unified workflow plugin: team-flow (spec-driven dev) + compound-engineering core subset + architecture-design (4A/DDD) + prototype (local HTML). 24 skills, one install.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.40.1",
|
|
5
5
|
"contextFileName": "GEMINI.md"
|
|
6
6
|
}
|
package/hooks/session-start
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# v0.
|
|
2
|
+
# v0.40.1: auto-sync CLI version with plugin version
|
|
3
3
|
set -e
|
|
4
4
|
|
|
5
5
|
# ═══════════════════════════════════════════════════════════════
|
|
6
6
|
# Plugin version (update this when releasing new versions)
|
|
7
7
|
# ═══════════════════════════════════════════════════════════════
|
|
8
|
-
PLUGIN_VERSION="0.
|
|
8
|
+
PLUGIN_VERSION="0.40.1"
|
|
9
9
|
|
|
10
10
|
# ═══════════════════════════════════════════════════════════════
|
|
11
11
|
# Step 1: Auto-sync CLI version with plugin version
|
package/llms.txt
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
## Overview
|
|
4
4
|
spec-superflow is a self-contained workflow integration plugin for Claude Code, Cursor, OpenAI Codex CLI/App, GitHub Copilot CLI, Gemini CLI, OpenCode, WorkBuddy, and Trae. It merges spec-driven planning artifacts (proposal, specs, design, tasks) with disciplined execution guardrails (TDD, review gates, controlled handoff) into one unified workflow.
|
|
5
5
|
|
|
6
|
-
Current version: v0.
|
|
6
|
+
Current version: v0.40.1.
|
|
7
7
|
|
|
8
8
|
## Key Documents
|
|
9
9
|
- README.md: Chinese homepage with full usage guide and FAQ
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xulthekl/team-flow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.1",
|
|
4
4
|
"description": "Unified plugin (24 skills + 15 agents) integrating team-flow, compound-engineering, architecture-design, prototype, design-system, workflow-orchestrator, workflow-bootstrap, e2e, session-handoff, workflow-feedback for multi-agent coding tools.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
package/plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "team-flow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.1",
|
|
4
4
|
"description": "Unified workflow plugin: team-flow (spec-driven dev: TDD/SDD/code-review/debugging) + compound-engineering core subset (brainstorm/plan/compound/strategy/ideate/proof, global compounding) + architecture-design (4A+DDD incremental design & global compounding) + prototype (local HTML prototype, zero external deps) + e2e (Playwright E2E, AC-driven) + workflow-orchestrator (product-level workflow orchestration) + workflow-bootstrap (existing project onboarding) + session-handoff (context transfer) + workflow-feedback (issue tracking). 24 skills + 15 agents, one install.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "LT"
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
// names) and are passed as array elements, never interpolated into a shell string;
|
|
25
25
|
// branch names are additionally sanitized to git-legal characters before use.
|
|
26
26
|
import { execFileSync } from 'node:child_process';
|
|
27
|
-
import { resolve, basename,
|
|
27
|
+
import { resolve, basename, join } from 'node:path';
|
|
28
|
+
import { detectWorkspaceRoot } from './lib/git-utils.mjs';
|
|
28
29
|
import { readdirSync, existsSync, appendFileSync, readFileSync } from 'node:fs';
|
|
29
30
|
|
|
30
31
|
const rawChangeDir = process.argv[2];
|
|
@@ -59,13 +60,6 @@ function sanitizeBranchName(raw) {
|
|
|
59
60
|
return cleaned || 'change';
|
|
60
61
|
}
|
|
61
62
|
|
|
62
|
-
// 若 change-dir 形如 <root>/changes/<name>,返回 <root>;否则返回 null。
|
|
63
|
-
function detectWorkspaceRoot(absChangeDir) {
|
|
64
|
-
const parent = dirname(absChangeDir); // <root>/changes
|
|
65
|
-
if (basename(parent) !== 'changes') return null;
|
|
66
|
-
return dirname(parent); // <root>
|
|
67
|
-
}
|
|
68
|
-
|
|
69
63
|
// 枚举 root 下含 .git 的直接子目录(= 代码仓库),按目录名排序保证确定性。
|
|
70
64
|
// .git 可能是目录(普通仓库)或文件(worktree/submodule 链接),existsSync 均覆盖。
|
|
71
65
|
function listCodeRepos(root) {
|
|
@@ -18,13 +18,14 @@
|
|
|
18
18
|
import {
|
|
19
19
|
readFileSync, writeFileSync as fsWriteFileSync, existsSync, mkdirSync, readdirSync, rmdirSync, cpSync,
|
|
20
20
|
} from 'node:fs';
|
|
21
|
-
import { join, basename, dirname, resolve
|
|
22
|
-
import {
|
|
21
|
+
import { join, basename, dirname, resolve } from 'node:path';
|
|
22
|
+
import { execFileSync } from 'node:child_process';
|
|
23
23
|
import { pathToFileURL } from 'node:url';
|
|
24
24
|
import {
|
|
25
25
|
parseTableAfter, extractEndpoints, extractAggregates,
|
|
26
26
|
extractTablesFromSql, extractTablesFromDatabaseMd, readFrontmatter,
|
|
27
27
|
} from './arch-parse.mjs';
|
|
28
|
+
import { detectWorkspaceRoot, getGitRoot } from './git-utils.mjs';
|
|
28
29
|
|
|
29
30
|
const MARKER_BEGIN = '<!-- arch:current-state:begin -->';
|
|
30
31
|
const MARKER_END = '<!-- arch:current-state:end -->';
|
|
@@ -417,9 +418,9 @@ function gitCommit(projectRoot, globalArchDir, touchedFiles, changeName, dryRun)
|
|
|
417
418
|
try {
|
|
418
419
|
// 白名单:只 add 本次 touch 的文件(防卷走他人未提交工作)
|
|
419
420
|
for (const f of touchedFiles) {
|
|
420
|
-
|
|
421
|
+
execFileSync('git', ['add', f], { cwd: projectRoot, stdio: 'pipe' });
|
|
421
422
|
}
|
|
422
|
-
|
|
423
|
+
execFileSync('git', ['commit', '-m', commitMsg], { cwd: projectRoot, stdio: 'pipe' });
|
|
423
424
|
return { committed: true, message: commitMsg };
|
|
424
425
|
} catch (e) {
|
|
425
426
|
return { committed: false, error: e.message };
|
|
@@ -427,9 +428,11 @@ function gitCommit(projectRoot, globalArchDir, touchedFiles, changeName, dryRun)
|
|
|
427
428
|
}
|
|
428
429
|
|
|
429
430
|
/** 检测 docs/architecture/ 下非本次 touch 的脏文件(团队协作安全底线)。 */
|
|
430
|
-
function detectUntouchedDirtyFiles(globalArchDir, touchedFiles) {
|
|
431
|
+
function detectUntouchedDirtyFiles(projectRoot, globalArchDir, touchedFiles) {
|
|
431
432
|
try {
|
|
432
|
-
const status =
|
|
433
|
+
const status = execFileSync('git', ['status', '--porcelain', '--', globalArchDir], {
|
|
434
|
+
cwd: projectRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
|
|
435
|
+
});
|
|
433
436
|
const dirty = status.split('\n').filter(Boolean)
|
|
434
437
|
.map(line => line.replace(/^\S+\s+/, '').trim())
|
|
435
438
|
.filter(p => p && !touchedFiles.includes(resolve(p)) && !touchedFiles.includes(p));
|
|
@@ -461,9 +464,7 @@ export function run(args = {}) {
|
|
|
461
464
|
|
|
462
465
|
let projectRoot = args.projectRoot;
|
|
463
466
|
if (!projectRoot) {
|
|
464
|
-
|
|
465
|
-
const changesIdx = abs.lastIndexOf(sep + 'changes' + sep);
|
|
466
|
-
projectRoot = changesIdx > 0 ? abs.slice(0, changesIdx) : process.cwd();
|
|
467
|
+
projectRoot = detectWorkspaceRoot(changeDir) || getGitRoot(changeDir);
|
|
467
468
|
}
|
|
468
469
|
|
|
469
470
|
const changeName = basename(changeDir.replace(/\/$/, ''));
|
|
@@ -530,7 +531,7 @@ export function run(args = {}) {
|
|
|
530
531
|
join(globalArchDir, 'INDEX.md'),
|
|
531
532
|
...(archiveResult.files || []), // v0.36.3:changelog/ 复制文件入白名单
|
|
532
533
|
].filter(p => existsSync(p));
|
|
533
|
-
const dirty = detectUntouchedDirtyFiles(globalArchDir, touchedFiles);
|
|
534
|
+
const dirty = detectUntouchedDirtyFiles(projectRoot, globalArchDir, touchedFiles);
|
|
534
535
|
if (dirty.length > 0) {
|
|
535
536
|
console.warn(` [WARN] docs/architecture/ 存在非本次 touch 的脏文件(${dirty.length} 个):${dirty.slice(0, 5).join(', ')} — 白名单 git add 不卷走它们,但请确认是否他人未提交工作`);
|
|
536
537
|
}
|
|
@@ -18,6 +18,7 @@ import { execFileSync } from 'node:child_process';
|
|
|
18
18
|
import { existsSync, readdirSync, rmSync } from 'node:fs';
|
|
19
19
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
20
20
|
import { parseArgs } from 'node:util';
|
|
21
|
+
import { detectWorkspaceRoot } from './git-utils.mjs';
|
|
21
22
|
|
|
22
23
|
export async function run(args) {
|
|
23
24
|
const { positionals, values } = parseArgs({
|
|
@@ -70,11 +71,6 @@ export async function run(args) {
|
|
|
70
71
|
}
|
|
71
72
|
}
|
|
72
73
|
|
|
73
|
-
function detectWorkspaceRoot(absChangeDir) {
|
|
74
|
-
const parent = dirname(absChangeDir);
|
|
75
|
-
if (basename(parent) !== 'changes') return null;
|
|
76
|
-
return dirname(parent);
|
|
77
|
-
}
|
|
78
74
|
|
|
79
75
|
// Case A: <root>/.worktrees/<change>/<repo>/
|
|
80
76
|
function detectCaseA(root, changeName) {
|
|
@@ -28,6 +28,7 @@ export async function run(args) {
|
|
|
28
28
|
'tests-failed': { type: 'string' },
|
|
29
29
|
json: { type: 'boolean', default: false },
|
|
30
30
|
help: { type: 'boolean', default: false },
|
|
31
|
+
repo: { type: 'string' },
|
|
31
32
|
},
|
|
32
33
|
allowPositionals: true,
|
|
33
34
|
});
|
|
@@ -144,7 +145,7 @@ function recordAndPrintReview(changeDir, values) {
|
|
|
144
145
|
head: values.head,
|
|
145
146
|
report: values.report,
|
|
146
147
|
tests: parseTestsStats(values),
|
|
147
|
-
});
|
|
148
|
+
}, values.repo);
|
|
148
149
|
print(values.json, { ok: true, wave: values.wave[0], receipt }, `Review for ${values.wave[0]} recorded: ${receipt.status}.`);
|
|
149
150
|
}
|
|
150
151
|
|
|
@@ -237,6 +238,6 @@ function printHelp() {
|
|
|
237
238
|
tf execution plan <dir> --mode <mode> --confirm --reason <text> --wave <id>:<strategy>:<task,...>[:<depends-on,...>] [--acknowledge-recommendation]
|
|
238
239
|
tf execution show <dir> [--json]
|
|
239
240
|
tf execution revise <dir> --mode sdd --confirm --reason <text> --wave <id>:<strategy>:<task,...>[:<depends-on,...>] [--acknowledge-recommendation]
|
|
240
|
-
tf execution review <dir> --wave <id> --base <sha> --head <sha> --report <path> --verdict pass|fail [--tests-total N --tests-passed N --tests-failed N]
|
|
241
|
+
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]
|
|
241
242
|
tf execution refresh-hash <dir> [--json] # refresh artifacts/contract hash without bumping revision`);
|
|
242
243
|
}
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import { execFileSync } from 'node:child_process';
|
|
21
21
|
import { existsSync, readdirSync, rmSync, appendFileSync, readFileSync } from 'node:fs';
|
|
22
22
|
import { join, resolve, basename } from 'node:path';
|
|
23
|
+
import { detectWorkspaceRoot } from './git-utils.mjs';
|
|
23
24
|
|
|
24
25
|
const PROTECTED = ['main', 'master'];
|
|
25
26
|
|
|
@@ -39,12 +40,20 @@ function normalizeBranchName(raw) {
|
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
/** 定位 workspace root(cwd 在主仓库内)与原型仓库路径。 */
|
|
42
|
-
function resolveLayout() {
|
|
43
|
+
function resolveLayout(projectRootArg) {
|
|
43
44
|
let workspaceRoot;
|
|
44
|
-
|
|
45
|
-
workspaceRoot =
|
|
46
|
-
}
|
|
47
|
-
|
|
45
|
+
if (projectRootArg) {
|
|
46
|
+
workspaceRoot = resolve(projectRootArg);
|
|
47
|
+
} else {
|
|
48
|
+
// 优先从 changes/ 布局推断(worktree 中 cwd 可能不是主仓库)
|
|
49
|
+
workspaceRoot = detectWorkspaceRoot(process.cwd());
|
|
50
|
+
if (!workspaceRoot) {
|
|
51
|
+
try {
|
|
52
|
+
workspaceRoot = git(process.cwd(), 'rev-parse', '--show-toplevel');
|
|
53
|
+
} catch {
|
|
54
|
+
throw new Error('无法定位 workspace root:cwd 不在 git 仓库内?请使用 --project-root 指定');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
48
57
|
}
|
|
49
58
|
const protoRepo = join(workspaceRoot, 'prototype');
|
|
50
59
|
if (!existsSync(join(protoRepo, '.git'))) {
|
|
@@ -84,8 +93,8 @@ function localBranchExists(protoRepo, branch) {
|
|
|
84
93
|
}
|
|
85
94
|
|
|
86
95
|
/** 子命令 branch:创建/复用原型版本 worktree。 */
|
|
87
|
-
function cmdBranch(branchName) {
|
|
88
|
-
const { workspaceRoot, protoRepo } = resolveLayout();
|
|
96
|
+
function cmdBranch(branchName, projectRootArg) {
|
|
97
|
+
const { workspaceRoot, protoRepo } = resolveLayout(projectRootArg);
|
|
89
98
|
const wtPath = join(workspaceRoot, '.worktrees', branchName, basename(protoRepo));
|
|
90
99
|
|
|
91
100
|
// 幂等:worktree 已存在 → 复用
|
|
@@ -118,8 +127,8 @@ function cmdBranch(branchName) {
|
|
|
118
127
|
}
|
|
119
128
|
|
|
120
129
|
/** 子命令 deisolate:版本收尾 merge 回主干 + 清理 worktree。 */
|
|
121
|
-
function cmdDeisolate(branchName, opts) {
|
|
122
|
-
const { workspaceRoot, protoRepo } = resolveLayout();
|
|
130
|
+
function cmdDeisolate(branchName, opts, projectRootArg) {
|
|
131
|
+
const { workspaceRoot, protoRepo } = resolveLayout(projectRootArg);
|
|
123
132
|
const wtPath = join(workspaceRoot, '.worktrees', branchName, basename(protoRepo));
|
|
124
133
|
const result = { branch: branchName, worktreePath: wtPath };
|
|
125
134
|
|
|
@@ -181,8 +190,9 @@ function usage() {
|
|
|
181
190
|
console.log(`tf prototype — 原型仓库版本 worktree 隔离(v0.37.0 §68.4)
|
|
182
191
|
|
|
183
192
|
用法:
|
|
184
|
-
tf prototype branch <prd-vN>
|
|
185
|
-
|
|
193
|
+
tf prototype branch <prd-vN> [--project-root <path>]
|
|
194
|
+
# 创建/复用原型版本 worktree(隔离 + 团队拉取)
|
|
195
|
+
tf prototype deisolate <prd-vN> [--merge] [--clean] [--project-root <path>]
|
|
186
196
|
# 版本收尾:merge 回主干 + 清理 worktree
|
|
187
197
|
|
|
188
198
|
示例:
|
|
@@ -210,13 +220,20 @@ export async function run(args = []) {
|
|
|
210
220
|
try { branchName = normalizeBranchName(rawVersion); }
|
|
211
221
|
catch (e) { console.error(e.message); process.exit(2); }
|
|
212
222
|
|
|
223
|
+
// Extract --project-root from remaining args
|
|
224
|
+
let projectRootArg;
|
|
225
|
+
const projectRootIdx = rest.indexOf('--project-root');
|
|
226
|
+
if (projectRootIdx !== -1 && rest[projectRootIdx + 1]) {
|
|
227
|
+
projectRootArg = rest[projectRootIdx + 1];
|
|
228
|
+
}
|
|
229
|
+
|
|
213
230
|
try {
|
|
214
231
|
if (sub === 'branch') {
|
|
215
|
-
return cmdBranch(branchName);
|
|
232
|
+
return cmdBranch(branchName, projectRootArg);
|
|
216
233
|
}
|
|
217
234
|
if (sub === 'deisolate') {
|
|
218
235
|
const opts = { merge: rest.includes('--merge'), clean: rest.includes('--clean') };
|
|
219
|
-
return cmdDeisolate(branchName, opts);
|
|
236
|
+
return cmdDeisolate(branchName, opts, projectRootArg);
|
|
220
237
|
}
|
|
221
238
|
console.error(`未知子命令:${sub}(可用:branch / deisolate)`);
|
|
222
239
|
process.exit(2);
|
|
@@ -19,16 +19,18 @@
|
|
|
19
19
|
import { execFileSync } from 'node:child_process';
|
|
20
20
|
import { existsSync, mkdirSync, readdirSync, rmdirSync } from 'node:fs';
|
|
21
21
|
import { join, resolve, isAbsolute } from 'node:path';
|
|
22
|
+
import { detectWorkspaceRoot } from './git-utils.mjs';
|
|
22
23
|
|
|
23
24
|
const LOCK_PATH = '.publish.lock';
|
|
24
25
|
|
|
25
26
|
/** 解析 CLI 参数。 */
|
|
26
27
|
function parseArgv(argv) {
|
|
27
|
-
const parsed = { targets: [], push: false, dryRun: false, changeDir: null };
|
|
28
|
+
const parsed = { targets: [], push: false, dryRun: false, changeDir: null, projectRoot: null };
|
|
28
29
|
for (let i = 0; i < argv.length; i++) {
|
|
29
30
|
const a = argv[i];
|
|
30
31
|
if (a === '--push') parsed.push = true;
|
|
31
32
|
else if (a === '--dry-run') parsed.dryRun = true;
|
|
33
|
+
else if (a === '--project-root') { parsed.projectRoot = argv[++i]; }
|
|
32
34
|
else if (a === '--prd') parsed.targets.push('prd');
|
|
33
35
|
else if (a === '--arch') parsed.targets.push('arch');
|
|
34
36
|
else if (a === '--changes') {
|
|
@@ -179,17 +181,27 @@ export async function run(args = []) {
|
|
|
179
181
|
if (parsed.help) {
|
|
180
182
|
console.log(`tf publish — 阶段产物白名单提交 + 可选推送(v0.37.0 §68.3)
|
|
181
183
|
|
|
182
|
-
用法:tf publish <target> [--push] [--dry-run]
|
|
184
|
+
用法:tf publish <target> [--push] [--dry-run] [--project-root <path>]
|
|
183
185
|
--prd 提交 PRD + orchestrator.yaml + registry.yaml + 主仓库 prototype/
|
|
184
186
|
--arch 提交 docs/architecture/(迭代快照)
|
|
185
187
|
--changes 提交整个 changes/(或 --changes <change-dir> 指定单个 change)
|
|
186
188
|
--all 提交全部上述
|
|
187
189
|
--push 同时推送到远程当前分支(默认只提交不推送)
|
|
188
|
-
--dry-run 只打印将执行的 git 动作,不落盘不 commit
|
|
190
|
+
--dry-run 只打印将执行的 git 动作,不落盘不 commit
|
|
191
|
+
--project-root <path>
|
|
192
|
+
显式指定主仓库根目录(多仓库 worktree 场景下自动检测失败时使用)`);
|
|
189
193
|
return;
|
|
190
194
|
}
|
|
191
195
|
|
|
192
|
-
|
|
196
|
+
let projectRoot;
|
|
197
|
+
if (parsed.projectRoot) {
|
|
198
|
+
projectRoot = resolve(parsed.projectRoot);
|
|
199
|
+
} else {
|
|
200
|
+
projectRoot = detectWorkspaceRoot(process.cwd());
|
|
201
|
+
if (!projectRoot) {
|
|
202
|
+
projectRoot = git(process.cwd(), 'rev-parse', '--show-toplevel');
|
|
203
|
+
}
|
|
204
|
+
}
|
|
193
205
|
const whitelist = collectWhitelist(projectRoot, parsed.targets, parsed.changeDir);
|
|
194
206
|
|
|
195
207
|
if (whitelist.length === 0) {
|
|
@@ -5,6 +5,7 @@ import { isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
|
5
5
|
import { computeArtifactsHash, computeContractHash } from './hash.mjs';
|
|
6
6
|
import { getOverlayPaths } from './sdd-overlay.mjs';
|
|
7
7
|
import { readState } from './state-loader.mjs';
|
|
8
|
+
import { detectWorkspaceRoot, findSubRepo } from './git-utils.mjs';
|
|
8
9
|
|
|
9
10
|
export const EXECUTION_MODES = ['inline', 'batch-inline', 'sdd'];
|
|
10
11
|
|
|
@@ -130,7 +131,7 @@ export function validatePlan(changeDir, plan) {
|
|
|
130
131
|
return { valid: failures.length === 0, failures, plan };
|
|
131
132
|
}
|
|
132
133
|
|
|
133
|
-
export function recordReview(changeDir, waveId, receipt) {
|
|
134
|
+
export function recordReview(changeDir, waveId, receipt, repoPath) {
|
|
134
135
|
const plan = readPlan(changeDir);
|
|
135
136
|
const validation = validatePlan(changeDir, plan);
|
|
136
137
|
if (!validation.valid) throw new Error(`Cannot record a review for an invalid execution plan: ${validation.failures.join('; ')}`);
|
|
@@ -145,7 +146,7 @@ export function recordReview(changeDir, waveId, receipt) {
|
|
|
145
146
|
}
|
|
146
147
|
for (const field of ['base', 'head']) requireText(receipt?.[field], `receipt.${field}`);
|
|
147
148
|
const report = validateReviewReportEvidence(changeDir, receipt?.report);
|
|
148
|
-
const { base, head } = validateReviewRange(changeDir, receipt.base, receipt.head);
|
|
149
|
+
const { base, head, subRepoPath } = validateReviewRange(changeDir, receipt.base, receipt.head, repoPath);
|
|
149
150
|
// v0.13 §51.2:测试统计为可选证据字段(提供时必须为非负整数)。
|
|
150
151
|
// 硬门禁在入口(test-matrix-ready)与 closing(tests-passing),此处只沉淀证据,
|
|
151
152
|
// 避免单 wave 零测试造成 BUG-A 式过严死锁(DDL/脚手架类 wave 可能合理无测试)。
|
|
@@ -166,6 +167,9 @@ export function recordReview(changeDir, waveId, receipt) {
|
|
|
166
167
|
head,
|
|
167
168
|
report,
|
|
168
169
|
...(tests ? { tests } : {}),
|
|
170
|
+
// Persist repo path when SHA was resolved in a sub-repository, so
|
|
171
|
+
// readCurrentReview can re-validate without re-scanning.
|
|
172
|
+
...(subRepoPath ? { repo: relative(changeDir, subRepoPath) } : {}),
|
|
169
173
|
plan_hash: plan.hash,
|
|
170
174
|
plan_revision: plan.revision,
|
|
171
175
|
recorded_at: new Date().toISOString(),
|
|
@@ -187,7 +191,10 @@ export function readCurrentReview(changeDir, waveId, plan = readPlan(changeDir))
|
|
|
187
191
|
try {
|
|
188
192
|
const receipt = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
189
193
|
if (receipt?.plan_hash !== plan.hash || receipt?.plan_revision !== plan.revision) return null;
|
|
190
|
-
|
|
194
|
+
// When the receipt records a repo path (sub-repo scenario), use it for
|
|
195
|
+
// SHA re-validation. Otherwise fall back to the default main-repo path.
|
|
196
|
+
const repoPath = receipt?.repo ? resolve(changeDir, receipt.repo) : undefined;
|
|
197
|
+
const range = validateReviewRange(changeDir, receipt?.base, receipt?.head, repoPath);
|
|
191
198
|
if (receipt.base !== range.base || receipt.head !== range.head) return null;
|
|
192
199
|
// A passing receipt is current evidence only while its referenced report
|
|
193
200
|
// remains safe and readable. Recheck it here because reports can be
|
|
@@ -276,10 +283,74 @@ function getPhysicalReviewsDirectory(changeDir) {
|
|
|
276
283
|
return { changeRoot, reviewsDir: directory };
|
|
277
284
|
}
|
|
278
285
|
|
|
279
|
-
function validateReviewRange(changeDir, base, head) {
|
|
280
|
-
const gitRoot =
|
|
281
|
-
|
|
282
|
-
|
|
286
|
+
function validateReviewRange(changeDir, base, head, repoPath) {
|
|
287
|
+
const gitRoot = resolveGitRoot(changeDir, repoPath);
|
|
288
|
+
|
|
289
|
+
let resolvedBase, resolvedHead, subRepoPath;
|
|
290
|
+
try {
|
|
291
|
+
resolvedBase = resolveGitCommit(gitRoot, base, 'base');
|
|
292
|
+
resolvedHead = resolveGitCommit(gitRoot, head, 'head');
|
|
293
|
+
} catch (primaryError) {
|
|
294
|
+
// Sub-repo fallback: when SHA is not in the main repo, scan workspace
|
|
295
|
+
// children for a sub-repository that contains it (e.g. bff/, ui/).
|
|
296
|
+
// Only attempt when --repo was not explicitly provided.
|
|
297
|
+
if (repoPath) throw primaryError;
|
|
298
|
+
|
|
299
|
+
const workspaceRoot = detectWorkspaceRoot(changeDir);
|
|
300
|
+
if (!workspaceRoot) throw primaryError;
|
|
301
|
+
|
|
302
|
+
const subRoot = findSubRepo(workspaceRoot, head);
|
|
303
|
+
if (!subRoot) throw primaryError;
|
|
304
|
+
|
|
305
|
+
resolvedBase = resolveGitCommit(subRoot, base, 'base');
|
|
306
|
+
resolvedHead = resolveGitCommit(subRoot, head, 'head');
|
|
307
|
+
subRepoPath = subRoot;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Track when SHA was resolved outside the changeDir's own repo.
|
|
311
|
+
// This happens either through automatic fallback (subRepoPath set above)
|
|
312
|
+
// or through explicit --repo pointing to a different repository.
|
|
313
|
+
if (!subRepoPath && repoPath) {
|
|
314
|
+
const defaultRoot = resolveGitRoot(changeDir);
|
|
315
|
+
if (gitRoot !== defaultRoot) subRepoPath = gitRoot;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const effectiveRoot = subRepoPath || gitRoot;
|
|
319
|
+
const result = validateResolvedRange(effectiveRoot, resolvedBase, resolvedHead);
|
|
320
|
+
return { ...result, subRepoPath };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Resolve the git root for SHA verification. Honors an explicit --repo path;
|
|
325
|
+
* otherwise uses the changeDir's own git repository.
|
|
326
|
+
*/
|
|
327
|
+
function resolveGitRoot(changeDir, repoPath) {
|
|
328
|
+
if (repoPath) {
|
|
329
|
+
const absRepo = isAbsolute(repoPath) ? repoPath : resolve(changeDir, repoPath);
|
|
330
|
+
try {
|
|
331
|
+
return execFileSync('git', ['-C', absRepo, 'rev-parse', '--show-toplevel'], {
|
|
332
|
+
encoding: 'utf8',
|
|
333
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
334
|
+
}).trim();
|
|
335
|
+
} catch {
|
|
336
|
+
throw new Error(`--repo path '${repoPath}' is not inside a Git work tree`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
try {
|
|
340
|
+
return execFileSync('git', ['-C', changeDir, 'rev-parse', '--show-toplevel'], {
|
|
341
|
+
encoding: 'utf8',
|
|
342
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
343
|
+
}).trim();
|
|
344
|
+
} catch {
|
|
345
|
+
throw new Error('Review receipts require the change directory to be inside a Git work tree');
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Validate a resolved SHA range: base must differ from head, and base must
|
|
351
|
+
* be an ancestor of head. Shared by both primary and sub-repo paths.
|
|
352
|
+
*/
|
|
353
|
+
function validateResolvedRange(gitRoot, resolvedBase, resolvedHead) {
|
|
283
354
|
// v0.13 §51.1:禁止空 diff review(base === head)。
|
|
284
355
|
// C1-domain-policy 现场曾出现 6 个 receipt 全部 base==head==初始 commit,
|
|
285
356
|
// 即 review 对空 diff 进行。本校验同时强制"每 wave 落 commit"的纪律。
|
|
@@ -299,17 +370,6 @@ function validateReviewRange(changeDir, base, head) {
|
|
|
299
370
|
return { base: resolvedBase, head: resolvedHead };
|
|
300
371
|
}
|
|
301
372
|
|
|
302
|
-
function getGitRoot(changeDir) {
|
|
303
|
-
try {
|
|
304
|
-
return execFileSync('git', ['-C', changeDir, 'rev-parse', '--show-toplevel'], {
|
|
305
|
-
encoding: 'utf8',
|
|
306
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
307
|
-
}).trim();
|
|
308
|
-
} catch {
|
|
309
|
-
throw new Error('Review receipts require the change directory to be inside a Git work tree');
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
373
|
function resolveGitCommit(gitRoot, revision, field) {
|
|
314
374
|
try {
|
|
315
375
|
return execFileSync('git', ['-C', gitRoot, 'rev-parse', '--verify', `${revision}^{commit}`], {
|