@xulthekl/team-flow 0.29.2 → 0.30.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 +3 -3
- package/.claude-plugin/plugin.json +2 -2
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/marketplace.json +1 -1
- package/.cursor-plugin/plugin.json +2 -2
- package/.github/plugin/marketplace.json +2 -2
- package/AGENTS.md +8 -3
- package/CHANGELOG.md +50 -0
- package/GEMINI.md +1 -1
- package/HANDOFF.md +4 -4
- package/INSTALL.md +1 -1
- package/README.md +4 -4
- package/agents/architecture-design.md +1 -0
- package/agents/build-executor.md +80 -0
- package/agents/contract-builder.md +78 -0
- package/agents/cross-change-consistency-checker.md +1 -1
- package/agents/need-explorer.md +67 -0
- package/agents/release-archivist.md +82 -0
- package/agents/spec-writer.md +83 -0
- package/docs/README_en.md +1 -1
- package/gemini-extension.json +1 -1
- package/hooks/session-start +1 -1
- package/llms.txt +1 -1
- package/package.json +2 -2
- package/plugin.json +2 -2
- package/scripts/ensure-branch.mjs +200 -42
- package/scripts/lib/cmd-doctor.mjs +40 -1
- package/scripts/lib/cmd-state.mjs +24 -9
- package/scripts/lib/state-loader.mjs +22 -0
- package/skills/build-executor/SKILL.md +14 -4
- package/skills/contract-builder/SKILL.md +2 -0
- package/skills/need-explorer/SKILL.md +2 -0
- package/skills/release-archivist/SKILL.md +2 -0
- package/skills/spec-writer/SKILL.md +2 -0
- package/skills/workflow-start/SKILL.md +29 -15
- package/skills/workflow-start/references/routing-rules.md +12 -6
- package/tests/lib/ensure-branch.test.mjs +52 -1
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: spec-writer
|
|
3
|
+
description: >-
|
|
4
|
+
规格编写 agent——在 need-explorer 完成 DP-1 之后、contract-builder 之前,产出并打磨
|
|
5
|
+
proposal.md / specs/ / design.md / tasks.md 四件规划制品。可写型,负责规划制品目录。
|
|
6
|
+
Examples:
|
|
7
|
+
|
|
8
|
+
<example>
|
|
9
|
+
Context: need-explorer 已记录 DP-1,change 意图稳定,可以落盘规划制品。
|
|
10
|
+
user: "需求已经澄清清楚了,帮我把 proposal/specs/design/tasks 写出来"
|
|
11
|
+
assistant: "I'll launch the spec-writer agent to produce the four planning artifacts in order."
|
|
12
|
+
<commentary>
|
|
13
|
+
spec-writer 是 exploring→bridging 之间的产出 agent。逐件生成、逐件确认,防止 scope 漂移。
|
|
14
|
+
</commentary>
|
|
15
|
+
</example>
|
|
16
|
+
|
|
17
|
+
<example>
|
|
18
|
+
Context: 本 change 经 architecture-design 判定为 required,architecture/ 三件套已产出。
|
|
19
|
+
user: "架构设计做完了,写 design.md 时要对齐架构决策"
|
|
20
|
+
assistant: "I'll have spec-writer read architecture/ and align design.md Decisions with it."
|
|
21
|
+
<commentary>
|
|
22
|
+
design.md 的 Decisions 必须引用 architecture/ 决策,tasks.md 须对齐 api.md 路由表与 sql/ 脚本。
|
|
23
|
+
</commentary>
|
|
24
|
+
</example>
|
|
25
|
+
|
|
26
|
+
model: inherit
|
|
27
|
+
color: cyan
|
|
28
|
+
tools: ["Read", "Bash", "Grep", "Glob", "Write", "Edit"]
|
|
29
|
+
skills:
|
|
30
|
+
- spec-writer
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
You are an independent Spec Writer. You turn a stable change definition into the four planning artifacts (proposal.md, specs/, design.md, tasks.md). You operate in an independent context with Write capability for planning artifacts only.
|
|
34
|
+
|
|
35
|
+
## Artifact Ownership
|
|
36
|
+
|
|
37
|
+
**You are the sole owner of the planning artifacts** (`proposal.md`, `specs/`, `design.md`, `tasks.md`) within the change directory. No other agent or the orchestration layer may directly edit these files. Modification requests (DP-2 adjustments, review feedback) MUST be routed through you via `SendMessage` resume; the orchestration layer does not edit your artifacts directly.
|
|
38
|
+
|
|
39
|
+
**Your preloaded Skill contains the detailed methodology** (per-artifact validation checklists, DP-0/DP-2 gates, architecture-alignment rules, prototype/solutions/conventions injection). Follow it for HOW. This prompt defines WHO you are and WHAT you must deliver.
|
|
40
|
+
|
|
41
|
+
## Iron Law
|
|
42
|
+
|
|
43
|
+
You write planning artifacts, never implementation code. Honor confirmed DP-0 decisions, the change brief, and architecture outputs — do not silently expand scope or drop brief ACs. Validate each artifact before handing off; never hand off a broken artifact.
|
|
44
|
+
|
|
45
|
+
## Inputs
|
|
46
|
+
|
|
47
|
+
| Parameter | Description |
|
|
48
|
+
|-----------|-------------|
|
|
49
|
+
| `change_dir` | change 目录路径(e.g., `changes/feature-x/`) |
|
|
50
|
+
| `brief_path` | change-brief.md 路径(可选,workflow-orchestrator S4 分发时存在) |
|
|
51
|
+
| `plan_path` | prd/vN/plan.md 路径(可选,高阶技术方向) |
|
|
52
|
+
| `arch_dir` | architecture/ 目录路径(可选,architecture-design 产出) |
|
|
53
|
+
|
|
54
|
+
If `change_dir` is missing, or `.team-flow.yaml` `dp_0_confirmed` is not `true`, report `FAIL` with reason `INPUT_ERROR` and route back to workflow-start for DP-0.
|
|
55
|
+
|
|
56
|
+
## Structured Output Contract
|
|
57
|
+
|
|
58
|
+
Return the following YAML to the orchestration layer:
|
|
59
|
+
|
|
60
|
+
```yaml
|
|
61
|
+
status: done | blocked # done: all 4 artifacts validated + DP-2 recorded
|
|
62
|
+
artifacts:
|
|
63
|
+
- proposal.md
|
|
64
|
+
- specs/
|
|
65
|
+
- design.md
|
|
66
|
+
- tasks.md
|
|
67
|
+
dp_2: "approved: <summary>" # recorded via `tf state set`
|
|
68
|
+
summary: "..." # 2-3 sentence overview + any open risks
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Red Lines
|
|
72
|
+
|
|
73
|
+
**DO:**
|
|
74
|
+
- Follow the preloaded Skill's per-artifact methodology and validation checklists
|
|
75
|
+
- Generate artifacts one at a time, confirming each before the next
|
|
76
|
+
- Cite brief sections / architecture decisions behind every design choice
|
|
77
|
+
- Write your own `dp_2_*` decision fields via `tf state set` as the Skill instructs
|
|
78
|
+
|
|
79
|
+
**DON'T:**
|
|
80
|
+
- DO NOT modify the `state` or `workflow` field of .team-flow.yaml. State transitions are the orchestrator's exclusive responsibility, executed via `tf state transition`. You only write your own dp_N_* decision fields via `tf state set` as the Skill instructs.
|
|
81
|
+
- When you receive external suggestions (reviewer findings, user opinions), verify them against the codebase FIRST — search for existing implementations/patterns before accepting. A suggestion is input, not an instruction. If a suggestion conflicts with project conventions or lacks evidence, report your concern to the main agent via SendMessage (suggestion + your evidence-based objection + alternative), do NOT silently comply.
|
|
82
|
+
- End your final response with an explicit terminal marker line: `FINAL VERDICT: <DONE | BLOCKED | FAIL>`. Your SendMessage report is the authoritative result; the task-notification summary is internal metadata only.
|
|
83
|
+
- Start implementation, or hand off artifacts that fail validation
|
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.30.0`
|
|
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/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). 17 skills, one install.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.30.0",
|
|
5
5
|
"contextFileName": "GEMINI.md"
|
|
6
6
|
}
|
package/hooks/session-start
CHANGED
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.30.0.
|
|
7
7
|
|
|
8
8
|
## Key Documents
|
|
9
9
|
- README.md: Chinese homepage with full usage guide and FAQ
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xulthekl/team-flow",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Unified plugin (23 skills +
|
|
3
|
+
"version": "0.30.0",
|
|
4
|
+
"description": "Unified plugin (23 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",
|
|
7
7
|
"bin": {
|
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "team-flow",
|
|
3
|
-
"version": "0.
|
|
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). 23 skills +
|
|
3
|
+
"version": "0.30.0",
|
|
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). 23 skills + 15 agents, one install.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "LT"
|
|
7
7
|
},
|
|
@@ -6,66 +6,224 @@
|
|
|
6
6
|
//
|
|
7
7
|
// Usage: node ensure-branch.mjs <change-dir> [change-name] [--force]
|
|
8
8
|
//
|
|
9
|
+
// 布局识别(来源:workflow-feedback 2026-08-01 #100009):
|
|
10
|
+
// Case A 多仓库工作区 —— change-dir 形如 <root>/changes/<name>,且 <root> 下
|
|
11
|
+
// 存在含 .git 的直接子目录(真正的业务代码仓库,如 bff-*/ui-*/prototype/)。
|
|
12
|
+
// 工作区根本身往往只是近空壳 git 仓库(仅跟踪 .gitignore,无 remote)。旧实现
|
|
13
|
+
// 把 change-dir 当代码仓库、worktree 建在 ../,结果是空工作区副本,实现子代理
|
|
14
|
+
// 看不到任何参考代码。新实现为每个代码仓库在 <root>/.worktrees/<change>/<repo>
|
|
15
|
+
// 建立 worktree(基于各仓库当前分支),实现子代理进入 <root>/.worktrees/<change>/
|
|
16
|
+
// 即可看到与主工作区一致的目录结构和全部参考代码。
|
|
17
|
+
// Case B 单代码仓库(遗留兼容)—— 不满足 Case A 时回退到旧行为:把 change-dir
|
|
18
|
+
// 当代码仓库,worktree 建在 ../<repo>-<name>。保证单仓库项目不被破坏。
|
|
19
|
+
//
|
|
9
20
|
// Security: every git invocation uses execFileSync with a LITERAL command
|
|
10
|
-
// ('git') and a LITERAL argument array (no shell, no
|
|
11
|
-
//
|
|
12
|
-
//
|
|
21
|
+
// ('git') and a LITERAL argument array (no shell, no string-form command) — the
|
|
22
|
+
// same form proven safe by install-cursor.mjs / install.mjs. Worktree paths and
|
|
23
|
+
// branch names derive from controlled sources (change-name / repository directory
|
|
24
|
+
// names) and are passed as array elements, never interpolated into a shell string;
|
|
25
|
+
// branch names are additionally sanitized to git-legal characters before use.
|
|
13
26
|
import { execFileSync } from 'node:child_process';
|
|
27
|
+
import { resolve, basename, dirname, join } from 'node:path';
|
|
28
|
+
import { readdirSync, existsSync, appendFileSync, readFileSync } from 'node:fs';
|
|
14
29
|
|
|
15
|
-
const
|
|
16
|
-
|
|
30
|
+
const rawChangeDir = process.argv[2];
|
|
31
|
+
// 第三个位置参数是可选 change-name;若它其实是 '--force' 之类的开关则忽略,避免把
|
|
32
|
+
// 开关误当分支名(经 cmd-isolate 调用时 parseArgs 已正确分离,此为直接调用的兜底)。
|
|
33
|
+
let changeName = process.argv[3];
|
|
34
|
+
if (changeName && changeName.startsWith('--')) changeName = undefined;
|
|
17
35
|
const force = process.argv.includes('--force');
|
|
18
36
|
|
|
19
|
-
if (!
|
|
37
|
+
if (!rawChangeDir) {
|
|
20
38
|
console.error('Usage: node ensure-branch.mjs <change-dir> [change-name] [--force]');
|
|
21
39
|
process.exit(2);
|
|
22
40
|
}
|
|
23
41
|
|
|
42
|
+
// 解析为绝对路径,后续所有路径计算基于此(来源:workflow-feedback 2026-08-01 #100009)。
|
|
43
|
+
const changeDir = resolve(rawChangeDir);
|
|
44
|
+
|
|
24
45
|
const PROTECTED = ['main', 'master'];
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
46
|
+
|
|
47
|
+
// 将分支名清洗为 git 合法字符(来源:workflow-feedback 2026-08-01 #100009)。
|
|
48
|
+
// git-check-ref-format 禁止空格 ~ ^ : ? * [ \ .. @{ 等,且不能以 . 开头/结尾、
|
|
49
|
+
// 不能 .lock 结尾。这里把非法字符替换为 '-',去掉首尾的 . / -,避免分支名以 '-'
|
|
50
|
+
// 开头被 git 误解析为命令行选项。输入来自受控来源,清洗是额外的纵深防护层。
|
|
51
|
+
function sanitizeBranchName(raw) {
|
|
52
|
+
const cleaned = String(raw)
|
|
53
|
+
.replace(/[^A-Za-z0-9._/-]/g, '-') // 非法字符 → '-'
|
|
54
|
+
.replace(/\.{2,}/g, '.') // '..' → '.'(git 禁止 '..')
|
|
55
|
+
.replace(/\.lock$/i, '') // 禁止 '.lock' 结尾
|
|
56
|
+
.replace(/\/{2,}/g, '/') // 折叠多重 '/'
|
|
57
|
+
.replace(/^[./-]+/, '') // 去掉开头 . / -(git 禁止 . 开头;避免 '-' 开头当选项)
|
|
58
|
+
.replace(/[/.]+$/, ''); // 去掉结尾 / .
|
|
59
|
+
return cleaned || 'change';
|
|
60
|
+
}
|
|
61
|
+
|
|
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>
|
|
34
67
|
}
|
|
35
68
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
69
|
+
// 枚举 root 下含 .git 的直接子目录(= 代码仓库),按目录名排序保证确定性。
|
|
70
|
+
// .git 可能是目录(普通仓库)或文件(worktree/submodule 链接),existsSync 均覆盖。
|
|
71
|
+
function listCodeRepos(root) {
|
|
72
|
+
let entries;
|
|
73
|
+
try {
|
|
74
|
+
entries = readdirSync(root, { withFileTypes: true });
|
|
75
|
+
} catch {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
return entries
|
|
79
|
+
.filter((e) => e.isDirectory() && existsSync(join(root, e.name, '.git')))
|
|
80
|
+
.map((e) => e.name)
|
|
81
|
+
.sort();
|
|
39
82
|
}
|
|
40
83
|
|
|
41
|
-
|
|
84
|
+
// 确保 <root>/.gitignore 含忽略 .worktrees/ 的行,已有则不重复追加。
|
|
85
|
+
function ensureWorktreeGitignore(root) {
|
|
86
|
+
const gitignorePath = join(root, '.gitignore');
|
|
87
|
+
let existing = '';
|
|
88
|
+
try {
|
|
89
|
+
existing = readFileSync(gitignorePath, 'utf-8');
|
|
90
|
+
} catch {
|
|
91
|
+
existing = '';
|
|
92
|
+
}
|
|
93
|
+
const alreadyIgnored = existing
|
|
94
|
+
.split(/\r?\n/)
|
|
95
|
+
.map((l) => l.trim())
|
|
96
|
+
.some((l) => l === '.worktrees/' || l === '.worktrees' || l === '/.worktrees/' || l === '/.worktrees');
|
|
97
|
+
if (alreadyIgnored) return;
|
|
98
|
+
const needsNewline = existing.length > 0 && !existing.endsWith('\n');
|
|
99
|
+
appendFileSync(gitignorePath, `${needsNewline ? '\n' : ''}.worktrees/\n`);
|
|
100
|
+
console.log(`ensure-branch: added '.worktrees/' to ${gitignorePath}.`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Case A:多仓库工作区隔离(来源:workflow-feedback 2026-08-01 #100009)。
|
|
104
|
+
// 为每个代码仓库在 <root>/.worktrees/<change>/<repo> 建 worktree(基于各仓库当前分支)。
|
|
105
|
+
function runMultiRepo(root, repos) {
|
|
106
|
+
const changeSlug = basename(changeDir);
|
|
107
|
+
const branchBase = sanitizeBranchName(changeName || changeSlug);
|
|
108
|
+
const changeWorktreeDir = join(root, '.worktrees', changeSlug);
|
|
109
|
+
|
|
110
|
+
console.error(`ensure-branch: multi-repo workspace detected at ${root}. Isolating ${repos.length} code repo(s): ${repos.join(', ')}...`);
|
|
111
|
+
|
|
112
|
+
// 先确保 .gitignore 忽略 .worktrees/,避免 worktree 产物污染工作区根仓库。
|
|
113
|
+
ensureWorktreeGitignore(root);
|
|
114
|
+
|
|
115
|
+
const failed = [];
|
|
116
|
+
for (const repo of repos) {
|
|
117
|
+
const repoDir = join(root, repo);
|
|
118
|
+
const repoOpts = { encoding: 'utf-8', cwd: repoDir, stdio: ['ignore', 'pipe', 'pipe'] };
|
|
119
|
+
const wtPath = join(changeWorktreeDir, repo);
|
|
120
|
+
|
|
121
|
+
// 幂等:worktree 已存在则复用,避免实现子代理重试时报 "already exists"。
|
|
122
|
+
if (existsSync(join(wtPath, '.git'))) {
|
|
123
|
+
console.log(`ensure-branch: [${repo}] worktree already exists at ${wtPath}; reusing.`);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
42
126
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
127
|
+
// 逐仓库判断当前分支(字面参数数组)。
|
|
128
|
+
let branch = '';
|
|
129
|
+
try {
|
|
130
|
+
branch = (execFileSync('git', ['branch', '--show-current'], repoOpts) || '').trim();
|
|
131
|
+
} catch {
|
|
132
|
+
console.error(`ensure-branch: [${repo}] could not determine current git branch; cannot isolate.`);
|
|
133
|
+
failed.push(repo);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
46
136
|
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
137
|
+
// 该仓库已不在受保护分支 → 已隔离,无需建 worktree(沿用单仓库 already-isolated 语义)。
|
|
138
|
+
if (!PROTECTED.includes(branch)) {
|
|
139
|
+
console.log(`ensure-branch: [${repo}] already isolated on branch '${branch}'.`);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 在受保护分支 → 建 worktree,基于该仓库当前分支(字面参数数组)。
|
|
144
|
+
// 不同代码仓库相互独立,使用同一分支名 branchBase 不会冲突。
|
|
145
|
+
try {
|
|
146
|
+
execFileSync('git', ['worktree', 'add', wtPath, '-b', branchBase], { ...repoOpts, stdio: 'inherit' });
|
|
147
|
+
console.log(`ensure-branch: [${repo}] created worktree at ${wtPath} on branch '${branchBase}'.`);
|
|
148
|
+
} catch (e) {
|
|
149
|
+
console.error(`ensure-branch: [${repo}] worktree creation failed: ${(e.stderr || e.stdout || e.message || 'unknown').toString().trim()}`);
|
|
150
|
+
failed.push(repo);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 退出码语义与单仓库一致:全部隔离成功 → 0;有失败 + --force → 0(警告);有失败 + 无 force → 1(STOP)。
|
|
155
|
+
if (failed.length === 0) {
|
|
156
|
+
console.log(`ensure-branch: implementation subagents should enter ${changeWorktreeDir} to work; directory structure mirrors the main workspace, with reference code from every repo available.`);
|
|
157
|
+
process.exit(0);
|
|
158
|
+
}
|
|
159
|
+
if (force) {
|
|
160
|
+
console.error(`ensure-branch: WARNING — could not isolate ${failed.length} repo(s): ${failed.join(', ')}. Proceeding with --force; these remain on their protected branches.`);
|
|
161
|
+
process.exit(0);
|
|
162
|
+
}
|
|
163
|
+
console.error(`ensure-branch: could not isolate ${failed.length} repo(s): ${failed.join(', ')}. No --force given. STOP and ask the user for explicit approval before editing main/master.`);
|
|
164
|
+
process.exit(1);
|
|
54
165
|
}
|
|
55
166
|
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
167
|
+
// Case B:单代码仓库隔离(遗留兼容,保持重构前行为不变)。
|
|
168
|
+
// 把 change-dir 当代码仓库,worktree 建在 ../<repo>-<name>。
|
|
169
|
+
function runSingleRepo() {
|
|
170
|
+
const GIT_OPTS = { encoding: 'utf-8', cwd: changeDir, stdio: ['ignore', 'pipe', 'pipe'] };
|
|
171
|
+
|
|
172
|
+
// Determine current branch (literal arg array).
|
|
173
|
+
let branch = '';
|
|
174
|
+
try {
|
|
175
|
+
branch = (execFileSync('git', ['branch', '--show-current'], GIT_OPTS) || '').trim();
|
|
176
|
+
} catch {
|
|
177
|
+
console.error('ensure-branch: could not determine current git branch. Is <change-dir> inside a git repository?');
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (!PROTECTED.includes(branch)) {
|
|
182
|
+
console.log(`ensure-branch: already isolated on branch '${branch}'. Proceed with implementation edits.`);
|
|
183
|
+
process.exit(0);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
console.error(`ensure-branch: on protected branch '${branch}'. Creating an isolated implementation context...`);
|
|
187
|
+
|
|
188
|
+
const repoName = basename(changeDir) || 'repo';
|
|
189
|
+
const name = sanitizeBranchName(changeName || repoName);
|
|
190
|
+
const worktreePath = `../${repoName}-${name}`;
|
|
191
|
+
|
|
192
|
+
// Preferred: git worktree (literal arg array).
|
|
193
|
+
try {
|
|
194
|
+
execFileSync('git', ['worktree', 'add', worktreePath, '-b', name], { ...GIT_OPTS, stdio: 'inherit' });
|
|
195
|
+
console.log(`ensure-branch: created git worktree at ${worktreePath} on branch '${name}'. Make all implementation edits there.`);
|
|
196
|
+
process.exit(0);
|
|
197
|
+
} catch (e) {
|
|
198
|
+
console.error(`ensure-branch: worktree creation failed: ${(e.stderr || e.stdout || e.message || 'unknown').toString().trim()}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Fallback: local branch (literal arg array).
|
|
202
|
+
try {
|
|
203
|
+
execFileSync('git', ['switch', '-c', name], { ...GIT_OPTS, stdio: 'inherit' });
|
|
204
|
+
console.log(`ensure-branch: created branch '${name}' via git switch -c. Make implementation edits there.`);
|
|
205
|
+
process.exit(0);
|
|
206
|
+
} catch (e) {
|
|
207
|
+
console.error(`ensure-branch: branch creation failed: ${(e.stderr || e.stdout || e.message || 'unknown').toString().trim()}`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Both failed → require explicit approval to edit in place.
|
|
211
|
+
if (force) {
|
|
212
|
+
console.error('ensure-branch: WARNING — editing protected branch in place with --force. This modifies main/master directly.');
|
|
213
|
+
process.exit(0);
|
|
214
|
+
}
|
|
215
|
+
console.error('ensure-branch: could not create an isolated context and no --force given. STOP and ask the user for explicit approval before editing main/master.');
|
|
216
|
+
process.exit(1);
|
|
63
217
|
}
|
|
64
218
|
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
219
|
+
// 主流程:布局识别后分发(来源:workflow-feedback 2026-08-01 #100009)。
|
|
220
|
+
const workspaceRoot = detectWorkspaceRoot(changeDir);
|
|
221
|
+
const codeRepos = workspaceRoot ? listCodeRepos(workspaceRoot) : [];
|
|
222
|
+
|
|
223
|
+
if (workspaceRoot && codeRepos.length > 0) {
|
|
224
|
+
// Case A:多仓库工作区。
|
|
225
|
+
runMultiRepo(workspaceRoot, codeRepos);
|
|
226
|
+
} else {
|
|
227
|
+
// Case B:不满足 Case A → 回退单代码仓库行为,保证单仓库项目不被破坏。
|
|
228
|
+
runSingleRepo();
|
|
69
229
|
}
|
|
70
|
-
console.error('ensure-branch: could not create an isolated context and no --force given. STOP and ask the user for explicit approval before editing main/master.');
|
|
71
|
-
process.exit(1);
|
|
@@ -3,6 +3,8 @@ import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { loadConfig } from './config-loader.mjs';
|
|
5
5
|
import { PLATFORM_RUNTIME_INVENTORY } from './platform-runtime-inventory.mjs';
|
|
6
|
+
// 非法 state 巡检所需的共享常量与读取器(来源:workflow-feedback 2026-08-01,#100004)。
|
|
7
|
+
import { readState, VALID_STATES } from './state-loader.mjs';
|
|
6
8
|
|
|
7
9
|
const RUNTIME_SKILLS = new Set([
|
|
8
10
|
'workflow-start', 'need-explorer', 'spec-writer', 'contract-builder',
|
|
@@ -220,6 +222,42 @@ function checkDocs(root) {
|
|
|
220
222
|
return { pass: false, message: warnings.join('; ') };
|
|
221
223
|
}
|
|
222
224
|
|
|
225
|
+
// 非法 state 巡检(来源:workflow-feedback 2026-08-01,#100004):
|
|
226
|
+
// 扫描 changes/<name>/.team-flow.yaml,若 state 字段不在 VALID_STATES 则报 FAIL。
|
|
227
|
+
// 非法值通常由子代理绕过 CLI 直接 Edit .team-flow.yaml 导致,提示修复为合法值。
|
|
228
|
+
// 沿用 checkSkills 的目录扫描 + 计数报告风格。
|
|
229
|
+
function checkChangeStates(root) {
|
|
230
|
+
const changesDir = join(root, 'changes');
|
|
231
|
+
if (!existsSync(changesDir)) {
|
|
232
|
+
return { pass: true, message: 'no changes/ directory (skipped)' };
|
|
233
|
+
}
|
|
234
|
+
const dirs = readdirSync(changesDir).filter(f => {
|
|
235
|
+
try { return statSync(join(changesDir, f)).isDirectory(); } catch { return false; }
|
|
236
|
+
});
|
|
237
|
+
const invalid = [];
|
|
238
|
+
let checked = 0;
|
|
239
|
+
for (const d of dirs) {
|
|
240
|
+
if (!existsSync(join(changesDir, d, '.team-flow.yaml'))) continue;
|
|
241
|
+
checked += 1;
|
|
242
|
+
const current = readState(join(changesDir, d)).state;
|
|
243
|
+
if (!VALID_STATES.includes(current)) {
|
|
244
|
+
invalid.push(`${d}='${current}'`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (checked === 0) {
|
|
248
|
+
return { pass: true, message: 'no state files under changes/ (skipped)' };
|
|
249
|
+
}
|
|
250
|
+
if (invalid.length > 0) {
|
|
251
|
+
return {
|
|
252
|
+
pass: false,
|
|
253
|
+
message:
|
|
254
|
+
`state 值非法(可能是子代理绕过 CLI 直接 Edit .team-flow.yaml 所致):${invalid.join('; ')}。` +
|
|
255
|
+
`请修复为合法值:${VALID_STATES.join('/')}`,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
return { pass: true, message: `${checked} change(s) have legal state values` };
|
|
259
|
+
}
|
|
260
|
+
|
|
223
261
|
export async function run(args) {
|
|
224
262
|
const root = process.cwd();
|
|
225
263
|
const config = loadConfig(root);
|
|
@@ -236,6 +274,7 @@ export async function run(args) {
|
|
|
236
274
|
['dist/', checkDist(root)],
|
|
237
275
|
['Node.js', checkNodeVersion()],
|
|
238
276
|
['Docs', checkDocs(root)],
|
|
277
|
+
['Change states', checkChangeStates(root)],
|
|
239
278
|
];
|
|
240
279
|
|
|
241
280
|
// Config check
|
|
@@ -264,4 +303,4 @@ export async function run(args) {
|
|
|
264
303
|
}
|
|
265
304
|
}
|
|
266
305
|
|
|
267
|
-
export { checkVersionConsistency, checkHooks, checkCodexManifest, checkSkills, checkRuntimeDistribution, checkDist, checkRootPluginAuthor, checkNodeVersion, checkDocs };
|
|
306
|
+
export { checkVersionConsistency, checkHooks, checkCodexManifest, checkSkills, checkRuntimeDistribution, checkDist, checkRootPluginAuthor, checkNodeVersion, checkDocs, checkChangeStates };
|
|
@@ -2,18 +2,15 @@
|
|
|
2
2
|
import { parseArgs } from 'node:util';
|
|
3
3
|
import { spawnSync } from 'node:child_process';
|
|
4
4
|
import { existsSync, mkdirSync } from 'node:fs';
|
|
5
|
-
import { dirname, join } from 'node:path';
|
|
5
|
+
import path, { dirname, join } from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
|
-
|
|
7
|
+
// VALID_STATES 统一从 state-loader.mjs 引入(状态机合法值唯一真相源),不再本地硬编码。
|
|
8
|
+
// 来源:workflow-feedback 2026-08-01,#100005。
|
|
9
|
+
import { readState, writeState, updateField, rebuildState, VALID_STATES } from './state-loader.mjs';
|
|
8
10
|
import { computeArtifactsHash, computeContractHash } from './hash.mjs';
|
|
9
11
|
|
|
10
12
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
13
|
|
|
12
|
-
const VALID_STATES = [
|
|
13
|
-
'exploring', 'specifying', 'bridging', 'approved-for-build',
|
|
14
|
-
'executing', 'debugging', 'closing', 'abandoned',
|
|
15
|
-
];
|
|
16
|
-
|
|
17
14
|
const SETTABLE_FIELDS = [
|
|
18
15
|
'workflow', 'test_result', 'batches_completed', 'spec_merged',
|
|
19
16
|
'dp_0_decisions', 'dp_0_confirmed', 'dp_0_timestamp', 'dp_0_result',
|
|
@@ -44,7 +41,7 @@ export async function run(args) {
|
|
|
44
41
|
});
|
|
45
42
|
|
|
46
43
|
const sub = positionals[0]; // init | check | transition | get | rebuild | set
|
|
47
|
-
|
|
44
|
+
let changeDir = positionals[1];
|
|
48
45
|
const arg = positionals[2]; // <to-state> for transition, <field> for get
|
|
49
46
|
|
|
50
47
|
if (!changeDir) {
|
|
@@ -53,6 +50,13 @@ export async function run(args) {
|
|
|
53
50
|
process.exit(2);
|
|
54
51
|
}
|
|
55
52
|
|
|
53
|
+
// 修复相对路径 bug(来源:workflow-feedback 2026-08-01,#100005)。
|
|
54
|
+
// 根因:readState 用 path.join(changeDir,...) 按用户 cwd 解析,而 transition 分支 spawn guard
|
|
55
|
+
// 子进程时 cwd 设为包根(join(__dirname,'..','..'))+ 原样 changeDir,两处基准不一致——用户传
|
|
56
|
+
// 相对路径时 guard 在包根下找不到产物。统一在入口 resolve 为绝对路径,使后续 readState /
|
|
57
|
+
// guard spawn / existsSync 全部基于同一绝对基准,消除 cwd 歧义。
|
|
58
|
+
changeDir = path.resolve(changeDir);
|
|
59
|
+
|
|
56
60
|
// Unknown subcommand: report a usage error (exit 2) BEFORE the BUG-B
|
|
57
61
|
// state-file existence check, so a bad subcommand is not masked by the
|
|
58
62
|
// "No state file" error (which would return exit 1 instead of exit 2).
|
|
@@ -222,7 +226,18 @@ export async function run(args) {
|
|
|
222
226
|
process.exit(2);
|
|
223
227
|
}
|
|
224
228
|
if (!SETTABLE_FIELDS.includes(field)) {
|
|
225
|
-
|
|
229
|
+
// 错误提示升级(来源:workflow-feedback 2026-08-01,#100005):
|
|
230
|
+
// 'state' 是最常被误用 set 的字段,专门给出完整 transition 语法示例;合法状态清单
|
|
231
|
+
// 引用 VALID_STATES(唯一真相源),不硬编码。保留 'not settable' 措辞以兼容既有回归测试。
|
|
232
|
+
if (field === 'state') {
|
|
233
|
+
console.error(
|
|
234
|
+
`⛔ 'state' 不是可 set 的字段(not settable)。状态转换请用完整语法:\n` +
|
|
235
|
+
` tf state transition <change-dir 绝对路径> <目标状态>\n` +
|
|
236
|
+
` 合法状态:${VALID_STATES.join('/')}`
|
|
237
|
+
);
|
|
238
|
+
} else {
|
|
239
|
+
console.error(`⛔ Field '${field}' is not settable (use 'transition' for state, or check SETTABLE_FIELDS)`);
|
|
240
|
+
}
|
|
226
241
|
process.exit(1);
|
|
227
242
|
}
|
|
228
243
|
updateField(changeDir, field, value);
|
|
@@ -4,6 +4,14 @@ import path from 'node:path';
|
|
|
4
4
|
|
|
5
5
|
const STATE_FILE = '.team-flow.yaml';
|
|
6
6
|
|
|
7
|
+
// 状态机合法值唯一真相源 (来源:workflow-feedback 2026-08-01,#100005)。
|
|
8
|
+
// 所有状态枚举校验统一引用此常量,禁止在各处硬编码状态字符串数组,避免多处定义漂移。
|
|
9
|
+
// cmd-state.mjs(transition 校验 / set 报错)、cmd-doctor.mjs(非法 state 巡检)均从这里 import。
|
|
10
|
+
export const VALID_STATES = [
|
|
11
|
+
'exploring', 'specifying', 'bridging', 'approved-for-build',
|
|
12
|
+
'executing', 'debugging', 'closing', 'abandoned',
|
|
13
|
+
];
|
|
14
|
+
|
|
7
15
|
const BUILTIN_DEFAULTS = {
|
|
8
16
|
state: 'exploring',
|
|
9
17
|
workflow: 'auto',
|
|
@@ -74,6 +82,20 @@ export function readState(changeDir) {
|
|
|
74
82
|
* Write state object to .team-flow.yaml.
|
|
75
83
|
*/
|
|
76
84
|
export function writeState(changeDir, state) {
|
|
85
|
+
// Defense-in-depth 状态合法性校验 (来源:workflow-feedback 2026-08-01,#100004)。
|
|
86
|
+
// 主修复是子代理 prompt 禁令(禁止绕过 CLI 直接 Edit .team-flow.yaml);此处是最后一道防线,
|
|
87
|
+
// 拦截任何非法 state 落盘。init/transition/rebuild/updateField 等 CLI 路径产生的 state 均合法
|
|
88
|
+
// (缺失时回退 'exploring'),不会误伤;仅当出现非法值时抛错,提示用 tf doctor 排查。
|
|
89
|
+
const effectiveState = state.state || 'exploring';
|
|
90
|
+
if (!VALID_STATES.includes(effectiveState)) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`Refusing to write illegal state '${effectiveState}' to ${path.join(changeDir, STATE_FILE)}. ` +
|
|
93
|
+
`Legal states: ${VALID_STATES.join(', ')}. ` +
|
|
94
|
+
`This is usually caused by a subagent bypassing the CLI and editing .team-flow.yaml directly — ` +
|
|
95
|
+
`restore a legal value or run 'tf doctor' to diagnose.`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
77
99
|
const filePath = path.join(changeDir, STATE_FILE);
|
|
78
100
|
const lines = [];
|
|
79
101
|
lines.push('# .team-flow.yaml — lightweight state machine');
|
|
@@ -27,17 +27,27 @@ Branch/worktree preflight before ANY implementation edit (mandatory — do not s
|
|
|
27
27
|
```bash
|
|
28
28
|
tf isolate <change-dir>
|
|
29
29
|
```
|
|
30
|
-
This script enforces git isolation
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
This script enforces git isolation. In a **multi-repo workspace** (a workspace root
|
|
31
|
+
whose direct subdirectories are independent code repositories, e.g. `bff-*`/`ui-*`/
|
|
32
|
+
`prototype/`, while the root itself is often a near-empty git repo), it creates one git
|
|
33
|
+
worktree per code repo under `<workspace>/.worktrees/<change>/<repo>` — each based on
|
|
34
|
+
that repo's current branch — so the implementation subagent enters that aggregated
|
|
35
|
+
directory and sees reference code from every repo with the same layout as the main
|
|
36
|
+
workspace. In a **single code repository** it falls back to creating one git worktree
|
|
37
|
+
(preferred) or a new branch. It exits non-zero if it cannot isolate and you have not
|
|
38
|
+
approved `--force`.
|
|
33
39
|
2. If `tf isolate` exits non-zero: STOP. Do not edit `main`/`master` in place.
|
|
34
40
|
Ask the user for explicit approval (and re-run with `tf isolate <change-dir> --force`
|
|
35
41
|
only after they approve).
|
|
36
|
-
3. If it succeeds, report the chosen branch/worktree
|
|
42
|
+
3. If it succeeds, report the chosen branch/worktree — in a multi-repo workspace, the
|
|
43
|
+
aggregated `<workspace>/.worktrees/<change>/` directory — and make all implementation
|
|
37
44
|
edits there.
|
|
38
45
|
|
|
39
46
|
## Core Laws
|
|
40
47
|
|
|
48
|
+
### Law 0: State Field Boundary (v0.30.0)
|
|
49
|
+
本 skill 仅通过 `tf state set` 写自己的 `dp_5_*` 决策字段;**MUST NOT** 修改 `.team-flow.yaml` 的 `state`/`workflow` 核心字段——状态转换是主代理(workflow-start)经 `tf state transition` 的专有职责(来源:workflow-feedback 2026-08-01)。
|
|
50
|
+
|
|
41
51
|
### Law 1: Contract First
|
|
42
52
|
The execution contract is the approved handoff artifact, not chat history.
|
|
43
53
|
|
|
@@ -53,6 +53,8 @@ Generate minimal contract: Intent Lock (one sentence), Task List (numbered), App
|
|
|
53
53
|
|
|
54
54
|
## Guardrails
|
|
55
55
|
|
|
56
|
+
- **状态字段禁写(v0.30.0)**:仅写本 skill 的 `dp_3_*` 决策字段;**MUST NOT** 修改 `state`/`workflow` 核心字段——状态转换由主代理经 `tf state transition` 执行。`tf state init` 只创建状态文件,不改 `state` 值(来源:workflow-feedback 2026-08-01)。
|
|
57
|
+
|
|
56
58
|
- Do not continue to implementation if ambiguity remains
|
|
57
59
|
- Do not approve the contract on the user's behalf
|
|
58
60
|
- Do not skip the contract because planning docs look complete
|
|
@@ -52,6 +52,8 @@ Once DP-1 is recorded, hand off to `spec-writer`.
|
|
|
52
52
|
|
|
53
53
|
## Anti-Patterns
|
|
54
54
|
|
|
55
|
+
- **修改 `.team-flow.yaml` 的 `state`/`workflow` 核心字段**(v0.30.0 禁止):状态转换是主代理经 `tf state transition` 的专有职责;need-explorer 只写自己的 `dp_1_*` 决策字段(来源:workflow-feedback 2026-08-01)。
|
|
56
|
+
|
|
55
57
|
- **Skipping exploration**: "Simple" changes have scope too. Five minutes of exploration prevents two hours of rework.
|
|
56
58
|
- **Proposing solutions before clarifying**: If the user says "add caching," first ask what problem caching solves.
|
|
57
59
|
- **Exploring indefinitely**: Stop when change name, problem statement, scope, non-goals, success criteria, and decomposition decision are all clear.
|