flower-trellis 0.6.6 → 0.6.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +12 -4
  2. package/enhancements/0.6/.agents/skills/trellis-check-all/SKILL.md +6 -31
  3. package/enhancements/0.6/.agents/skills/trellis-check-all/references/full-profile.md +10 -49
  4. package/enhancements/0.6/.agents/skills/trellis-check-all/references/light-profile.md +9 -28
  5. package/enhancements/0.6/.agents/skills/trellis-check-all/references/maven-evidence.md +14 -0
  6. package/enhancements/0.6/.agents/skills/trellis-check-all/references/reporting-and-disposition.md +10 -1
  7. package/enhancements/0.6/.agents/skills/trellis-check-all/references/verification.md +29 -0
  8. package/enhancements/0.6/.agents/skills/trellis-flower-update/SKILL.md +8 -5
  9. package/enhancements/0.6/.agents/skills/trellis-route/SKILL.md +1 -1
  10. package/enhancements/0.6/.agents/skills/trellis-worktree/SKILL.md +20 -8
  11. package/enhancements/0.6/.claude/skills/trellis-check-all/SKILL.md +6 -31
  12. package/enhancements/0.6/.claude/skills/trellis-check-all/references/full-profile.md +10 -49
  13. package/enhancements/0.6/.claude/skills/trellis-check-all/references/light-profile.md +9 -28
  14. package/enhancements/0.6/.claude/skills/trellis-check-all/references/maven-evidence.md +14 -0
  15. package/enhancements/0.6/.claude/skills/trellis-check-all/references/reporting-and-disposition.md +10 -1
  16. package/enhancements/0.6/.claude/skills/trellis-check-all/references/verification.md +29 -0
  17. package/enhancements/0.6/.claude/skills/trellis-flower-update/SKILL.md +8 -5
  18. package/enhancements/0.6/.claude/skills/trellis-route/SKILL.md +1 -1
  19. package/enhancements/0.6/.claude/skills/trellis-worktree/SKILL.md +20 -8
  20. package/enhancements/0.6/scripts/worktree_setup.py +119 -17
  21. package/enhancements/MANIFEST.json +2 -2
  22. package/package.json +3 -3
  23. package/src/assets/flower_update_hook.py +135 -14
  24. package/src/builtin-plugins/skill-garden/content-adapter.js +4 -0
  25. package/src/builtin-plugins/skill-garden/project-metadata.js +41 -0
  26. package/src/commands/self-check.js +3 -1
  27. package/src/commands/self-update.js +3 -1
  28. package/src/commands/update-check.js +3 -3
  29. package/src/commands/worktree.js +36 -5
  30. package/src/lib/apply-enhancements.js +1 -1
  31. package/src/lib/manifest.js +46 -43
  32. package/src/lib/self-check.js +8 -4
  33. package/src/lib/worktree-flower-state.js +347 -0
  34. package/src/plugin/application-service.js +1 -44
  35. package/src/plugin/install/state-validation.js +47 -0
  36. package/src/plugin/install/transaction-writer.js +23 -2
  37. package/src/plugin/state/ignore-rules.js +40 -0
  38. package/src/plugin/state/project-store.js +3 -25
@@ -1,7 +1,7 @@
1
1
  {
2
- "syncedAt": "2026-09-07T02:30:42.961Z",
2
+ "syncedAt": "2026-09-07T07:30:55.059Z",
3
3
  "syncedFrom": "vendor/skill-garden",
4
- "sourceCommit": "ad64c8ab30cb14184895aa31d672cf1b846e5726",
4
+ "sourceCommit": "3e08ded44107ae8d3e1c04e5f1cdb3014655f038",
5
5
  "common": {
6
6
  "codexSkills": [
7
7
  "aliyun-ops",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flower-trellis",
3
- "version": "0.6.6",
3
+ "version": "0.6.7",
4
4
  "description": "一键安装/升级 Trellis 并自动融合 skill-garden 强化包(默认 Claude + agents)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -70,9 +70,9 @@
70
70
  "commit-and-tag-version": "^12.7.3"
71
71
  },
72
72
  "flowerReleaseNotes": {
73
- "version": "0.6.6",
73
+ "version": "0.6.7",
74
74
  "source": "CHANGELOG.md",
75
- "body": "### 🐛 修复 Bug Fixes\n\n* **codex:** 明确 Astra 规划授权边界 ([07428f8](https://github.com/SilentFlower/flower-trellis/commit/07428f8c4406449656edfe2e6dd6463d4540370a))",
75
+ "body": "### ✨ 新功能 Features\n\n* **bootstrap:** 完善团队版本记录与 CLI 安装引导 ([e86217c](https://github.com/SilentFlower/flower-trellis/commit/e86217c5a121b35509ad972540c2c0adf3763b64))\n\n\n### 🐛 修复 Bug Fixes\n\n* **worktree:** 补齐 Flower 安装记录继承与版本诊断 ([17ad63e](https://github.com/SilentFlower/flower-trellis/commit/17ad63efd9f56ecf491c9402ef92469c70db3ea1))\n\n\n### ♻️ 重构 Refactor\n\n* **check-all:** 同步检查优化与回归验证 ([1409d42](https://github.com/SilentFlower/flower-trellis/commit/1409d421884cfe35a95342b6e68b3b7b80496e29))\n\n\n### 🧰 维护 Maintenance\n\n* **trellis:** 同步 Flower 0.6.6 工作流与 hooks ([11ae2b9](https://github.com/SilentFlower/flower-trellis/commit/11ae2b96b543da1a01e8a1871471d0752f1a04af))\n* **trellis:** 同步本地安装产物与 Flower 记录 ([5a78ca0](https://github.com/SilentFlower/flower-trellis/commit/5a78ca01434e26287caaed8b3e68321a1efd7cc7))",
76
76
  "truncated": false
77
77
  },
78
78
  "optionalDependencies": {
@@ -1,16 +1,17 @@
1
1
  #!/usr/bin/env python3
2
2
  # -*- coding: utf-8 -*-
3
- """SessionStart 启动更新检查 hook。
3
+ """复用 SessionStart 升级入口,检测 CLI 缺失并提供对话安装引导。
4
4
 
5
- 脚本只做只读检查和上下文注入:调用 `flower-trellis self-check --json`,
6
- 发现可执行更新时输出 `<flower-update>` 块。失败、离线、无更新或关闭检查时静默退出,
7
- 避免影响 Codex / Claude Code 正常启动。
5
+ 只读检测,不执行 npm;CLI 已安装时沿用 self-check 更新检查。
8
6
  """
9
7
 
10
8
  from __future__ import annotations
11
9
 
10
+ import argparse
12
11
  import json
13
12
  import os
13
+ import re
14
+ import shutil
14
15
  import subprocess
15
16
  import sys
16
17
  from pathlib import Path
@@ -18,6 +19,105 @@ from pathlib import Path
18
19
 
19
20
  ACTIONABLE_STATUSES = {"update_available", "project_out_of_sync"}
20
21
  SELF_CHECK_TIMEOUT_SECONDS = 30
22
+ BOOTSTRAP_STATUSES = {"cli_missing", "cli_unavailable", "project_version_unavailable", "prerequisites_missing"}
23
+ MAX_LOCK_BYTES = 2 * 1024 * 1024
24
+
25
+
26
+ def _executable_status(name: str) -> str:
27
+ """检查 PATH,区分缺失与已有但不可执行的入口。"""
28
+ if shutil.which(name):
29
+ return "available"
30
+ names = [name]
31
+ if os.name == "nt":
32
+ names.extend(name + suffix for suffix in os.environ.get("PATHEXT", ".COM;.EXE;.BAT;.CMD").split(";"))
33
+ for directory in os.get_exec_path():
34
+ if any(os.path.lexists(os.path.join(directory, candidate)) for candidate in names):
35
+ return "unavailable"
36
+ return "missing"
37
+
38
+
39
+ def _locked_flower_version(project_dir: Path) -> str:
40
+ """只读取受支持的内置锁条目,拒绝把未知版本拼入安装命令。"""
41
+ lock_path = project_dir / ".flower" / "plugin-lock.json"
42
+ if lock_path.parent.is_symlink() or lock_path.is_symlink() or not lock_path.is_file():
43
+ raise ValueError("项目缺少普通文件形式的 .flower/plugin-lock.json")
44
+ if lock_path.stat().st_size > MAX_LOCK_BYTES:
45
+ raise ValueError("项目锁超过读取上限")
46
+ lock = json.loads(lock_path.read_text(encoding="utf-8"))
47
+ if not isinstance(lock, dict) or type(lock.get("schemaVersion")) is not int or lock["schemaVersion"] != 1:
48
+ raise ValueError("项目锁 schema 不受支持")
49
+ plugins, roots = lock.get("plugins"), lock.get("roots")
50
+ if not isinstance(plugins, list) or not isinstance(roots, list) or any(not isinstance(root, str) for root in roots):
51
+ raise ValueError("项目锁 plugins/roots 结构无效")
52
+ if any(not isinstance(plugin, dict) for plugin in plugins):
53
+ raise ValueError("项目锁 Plugin 条目无效")
54
+ matches = [plugin for plugin in plugins if plugin.get("id") == "flower/skill-garden"]
55
+ if len(matches) != 1:
56
+ raise ValueError("项目锁缺少唯一的 flower/skill-garden 条目")
57
+ plugin = matches[0]
58
+ source = plugin.get("source")
59
+ if (
60
+ not isinstance(source, dict)
61
+ or source.get("id") != "flower"
62
+ or source.get("type") != "builtin"
63
+ or not isinstance(source.get("reference"), str)
64
+ or source.get("reference") not in {
65
+ "package:skill-garden:old", "package:skill-garden:0.5", "package:skill-garden:0.6",
66
+ }
67
+ ):
68
+ raise ValueError("项目锁的 Skill-Garden 来源不是受支持的内置来源")
69
+ version = plugin.get("version")
70
+ match = re.fullmatch(r"(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?", version) if isinstance(version, str) and len(version) <= 256 else None
71
+ if not match or any(int(part) > 9007199254740991 for part in match.groups()[:3]):
72
+ raise ValueError("项目 Flower 版本不是完整有效的 SemVer")
73
+ if match[4] and any(part.isdigit() and len(part) > 1 and part.startswith("0") for part in match[4].split(".")):
74
+ raise ValueError("项目 Flower 预发布版本包含非法数字标识")
75
+ return version
76
+
77
+
78
+ def _check_cli(project_dir: Path) -> dict | None:
79
+ """只读检查本机入口,返回安装或环境诊断;可用时返回空值。"""
80
+ if not project_dir.is_dir():
81
+ return {"status": "cli_unavailable", "reason": "目标项目目录不存在"}
82
+ cli_status = _executable_status("flower-trellis")
83
+ if cli_status == "available":
84
+ return None
85
+ if cli_status == "unavailable":
86
+ return {"status": "cli_unavailable", "reason": "PATH 中已有 Flower 入口但不可执行,请检查权限、链接与解释器"}
87
+ try:
88
+ version = _locked_flower_version(project_dir)
89
+ except (OSError, ValueError) as exc:
90
+ return {"status": "project_version_unavailable", "reason": f"Flower CLI 缺失,项目版本无法确认:{exc}"}
91
+ missing = [name for name in ("node", "npm") if _executable_status(name) != "available"]
92
+ if missing:
93
+ return {"status": "prerequisites_missing", "version": version, "reason": f"Flower CLI 缺失,请先准备可执行的 {', '.join(missing)}"}
94
+ return {"status": "cli_missing", "version": version, "command": f"npm install -g flower-trellis@{version}"}
95
+
96
+
97
+ def _format_bootstrap(data: dict) -> str:
98
+ """生成唯一的对话安装协议,检测脚本自身不执行安装。"""
99
+ lines = [
100
+ "<flower-cli-bootstrap>",
101
+ "priority: blocking_confirmation_required",
102
+ f"status: {data['status']}",
103
+ ]
104
+ if data.get("version"):
105
+ lines.append(f"project_flower: {data['version']}")
106
+ if data.get("reason"):
107
+ lines.append("reason: " + json.dumps(data["reason"], ensure_ascii=False))
108
+ if data.get("command"):
109
+ lines.extend([
110
+ f"recommended_command: {data['command']}",
111
+ "ai_instruction: 告知成员本机缺少 Flower CLI,展示锁定版本和 recommended_command;说明全局 npm 安装会同步本机 Trellis 命令。先征得当前成员确认,确认前禁止执行 recommended_command;当前对话已明确授权这次安装时不重复确认。",
112
+ "verification: 安装返回成功后,在目标项目执行 flower-trellis -v 核对 CLI 首行版本,再执行 flower-trellis self-check --json --target <目标项目>;参数按所在 shell 引用。确认 CLI 可用后继续原请求,不把项目锁视为内容完整性证明,不隐式执行 init/update/self-update。",
113
+ ])
114
+ else:
115
+ lines.append("ai_instruction: 说明诊断原因与缺失前提;版本未知时请维护者补齐有效项目锁,不依据旧 manifest 或猜测 latest 安装;入口异常先修复 PATH、权限或解释器。")
116
+ lines.extend([
117
+ "failure_policy: 成员拒绝后本次对话不重复追问;安装或验证失败如实报告,不自动提权、不循环安装。依赖 CLI 的操作说明尚未满足的前提;本次已处理的相同引导不重复执行。",
118
+ "</flower-cli-bootstrap>",
119
+ ])
120
+ return "\n".join(lines)
21
121
 
22
122
 
23
123
  def _debug(message: str) -> None:
@@ -45,6 +145,9 @@ def _project_dir(hook_input: dict) -> Path:
45
145
 
46
146
  def _run_self_check(project_dir: Path) -> dict | None:
47
147
  """执行 flower-trellis self-check 并解析 JSON。"""
148
+ bootstrap = _check_cli(project_dir)
149
+ if bootstrap:
150
+ return bootstrap
48
151
  try:
49
152
  result = subprocess.run(
50
153
  [
@@ -61,18 +164,19 @@ def _run_self_check(project_dir: Path) -> dict | None:
61
164
  timeout=SELF_CHECK_TIMEOUT_SECONDS,
62
165
  cwd=str(project_dir),
63
166
  )
64
- except (subprocess.TimeoutExpired, FileNotFoundError, PermissionError) as exc:
65
- _debug(str(exc))
66
- return None
167
+ except subprocess.TimeoutExpired:
168
+ return {"status": "cli_unavailable", "reason": "Flower CLI 检查超时,不能据此判定未安装"}
169
+ except OSError as exc:
170
+ return {"status": "cli_unavailable", "reason": f"Flower 入口存在但执行失败,请检查 PATH、权限与解释器:{exc}"}
67
171
  if result.returncode != 0:
68
- _debug(result.stderr.strip() or f"退出码 {result.returncode}")
69
- return None
172
+ _debug(result.stderr.strip())
173
+ return {"status": "cli_unavailable", "reason": f"Flower CLI 检查退出码 {result.returncode},请先诊断现有安装"}
70
174
  try:
71
175
  data = json.loads(result.stdout)
72
176
  except json.JSONDecodeError as exc:
73
177
  _debug(f"JSON 解析失败:{exc}")
74
- return None
75
- return data if isinstance(data, dict) else None
178
+ return {"status": "cli_unavailable", "reason": "Flower CLI 返回的检查结果不是有效 JSON"}
179
+ return data if isinstance(data, dict) and isinstance(data.get("status"), str) else {"status": "cli_unavailable", "reason": "Flower CLI 检查结果结构无效"}
76
180
 
77
181
 
78
182
  def _json_bool(value: object) -> str:
@@ -219,15 +323,32 @@ def _emit_context(context: str, system_message: str) -> None:
219
323
  print(json.dumps(result, ensure_ascii=False), flush=True)
220
324
 
221
325
 
222
- def main() -> None:
326
+ def main(argv: list[str] | None = None) -> None:
327
+ """处理宿主启动或手动只读检测入口并输出上下文。
328
+
329
+ Args:
330
+ argv: 显式参数列表,省略时读取进程参数。
331
+ Returns:
332
+ 无返回值;通过标准输出返回宿主 JSON。
333
+ """
334
+ parser = argparse.ArgumentParser(description="Flower CLI 安装与升级只读检测")
335
+ parser.add_argument("--bootstrap-only", action="store_true")
336
+ parser.add_argument("--target", type=Path)
337
+ args = parser.parse_args(argv)
338
+ if not args.bootstrap_only and (os.environ.get("TRELLIS_HOOKS") == "0" or os.environ.get("TRELLIS_DISABLE_HOOKS") == "1" or os.environ.get("CODEX_NON_INTERACTIVE") == "1"):
339
+ return
223
340
  try:
224
- hook_input = json.loads(sys.stdin.read() or "{}")
341
+ hook_input = {} if args.bootstrap_only else json.loads(sys.stdin.read() or "{}")
225
342
  if not isinstance(hook_input, dict):
226
343
  hook_input = {}
227
344
  except json.JSONDecodeError:
228
345
  hook_input = {}
229
346
 
230
- data = _run_self_check(_project_dir(hook_input))
347
+ project_dir = args.target.resolve() if args.target else _project_dir(hook_input)
348
+ data = _check_cli(project_dir) if args.bootstrap_only else _run_self_check(project_dir)
349
+ if data and data.get("status") in BOOTSTRAP_STATUSES:
350
+ _emit_context(_format_bootstrap(data), "Flower CLI 需要安装确认或环境诊断")
351
+ return
231
352
  if not data or data.get("status") not in ACTIONABLE_STATUSES:
232
353
  return
233
354
  _emit_context(_format_context(data), _system_message(data))
@@ -6,6 +6,7 @@ import {
6
6
  ENHANCEMENT_SKILL_TARGETS,
7
7
  } from "../../constants.js";
8
8
  import { readLegacyManifestStatus } from "../../lib/manifest.js";
9
+ import { projectFlowerMetadata } from "./project-metadata.js";
9
10
  import { shouldInstallName } from "../../lib/skill-filter.js";
10
11
  import { listCanonicalTreeFiles, isVolatileTreeArtifact } from "../../plugin/integrity/canonical-tree.js";
11
12
  import {
@@ -893,6 +894,9 @@ export function projectSkillGardenContent(options) {
893
894
  }
894
895
  }
895
896
  }
897
+ const metadata = projectFlowerMetadata(projectRoot, resolved.id);
898
+ mutations.push(...metadata.mutations);
899
+ for (const [key, value] of metadata.payloads) payloads.set(key, value);
896
900
  return {
897
901
  mutations: mutations.sort((left, right) => compareUtf8(left.target, right.target)),
898
902
  payloads,
@@ -0,0 +1,41 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { planLegacyManifestMigration } from "../../lib/manifest.js";
4
+ import { contentMutationKey } from "../../plugin/install/content-projector.js";
5
+ import { hashContent, hashFileIfExists } from "../../plugin/install/content-hash.js";
6
+ import { mergeFlowerIgnoreRules } from "../../plugin/state/ignore-rules.js";
7
+
8
+ /**
9
+ * 规划 Flower 共享记录与旧配置迁移,不声明用户配置的独占 ownership。
10
+ *
11
+ * @param {string} projectRoot 目标项目根
12
+ * @param {string} owner 内置 Plugin ID
13
+ * @returns {{mutations:object[],payloads:Map<string,Buffer>}} 事务目标及写入内容
14
+ */
15
+ export function projectFlowerMetadata(projectRoot, owner) {
16
+ const writes = planLegacyManifestMigration(projectRoot);
17
+ const localIgnore = path.join(projectRoot, ".flower/.gitignore");
18
+ const localText = hashFileIfExists(localIgnore) === null ? "" : fs.readFileSync(localIgnore, "utf8");
19
+ writes.push({ path: ".flower/.gitignore", content: Buffer.from(mergeFlowerIgnoreRules(localText)) });
20
+ const rootIgnore = path.join(projectRoot, ".gitignore");
21
+ if (hashFileIfExists(rootIgnore) !== null) {
22
+ writes.push({ path: ".gitignore", content: Buffer.from(mergeFlowerIgnoreRules(fs.readFileSync(rootIgnore, "utf8"), { root: true })) });
23
+ }
24
+ const mutations = [];
25
+ const payloads = new Map();
26
+ for (const entry of writes) {
27
+ const mutation = {
28
+ owner,
29
+ target: entry.path,
30
+ operation: entry.content === null ? "remove" : "write",
31
+ beforeHash: hashFileIfExists(path.join(projectRoot, ...entry.path.split("/"))),
32
+ afterHash: entry.content === null ? null : hashContent(entry.content),
33
+ source: "flower:project-metadata",
34
+ allowUnownedWrite: true,
35
+ allowUnownedRemove: true,
36
+ };
37
+ mutations.push(mutation);
38
+ if (entry.content !== null) payloads.set(contentMutationKey(mutation), entry.content);
39
+ }
40
+ return { mutations, payloads };
41
+ }
@@ -14,7 +14,9 @@ function printSelfCheckHelp() {
14
14
  --force-remote 强制检查远端版本
15
15
  --target <dir> 要检查的项目目录
16
16
 
17
- 该命令供 SessionStart hook 和自动化读取,成功时始终输出 JSON。`);
17
+ 该命令供 SessionStart hook 和自动化读取,成功时始终输出 JSON。
18
+ project.flowerVersionStatus 区分 known/unknown;记录缺失且无已知更新时返回
19
+ project_unknown,不能以 CLI 最新推断项目也已更新。`);
18
20
  }
19
21
 
20
22
  /**
@@ -199,7 +199,9 @@ async function executeSelfUpdate(ctx) {
199
199
  }
200
200
 
201
201
  if (!shouldInstallFlower && !projectOnly && check.status !== "project_out_of_sync") {
202
- console.log(` · 当前状态:${check.status},无需执行 self-update`);
202
+ console.log(check.status === "project_unknown"
203
+ ? " · 项目 Flower 版本无法确认,未执行更新;worktree 可先通过 prepare --inherit-flower 补齐安装记录"
204
+ : ` · 当前状态:${check.status},无需执行 self-update`);
203
205
  return;
204
206
  }
205
207
 
@@ -106,11 +106,11 @@ export async function updateCheck(ctx) {
106
106
  const cachePath = updateCheckCachePath(ctx.target);
107
107
  console.log(`settings: ${settingsPath(ctx.target)}`);
108
108
  console.log(`cache: ${cachePath}`);
109
- console.log(`legacy manifest: ${manifestPath(ctx.target)}`);
109
+ if (manifest) console.log(`legacy manifest (待迁移): ${manifestPath(ctx.target)}`);
110
110
  console.log(JSON.stringify(readUpdateCheck(ctx.target), null, 2));
111
- if (!manifest) console.log(" · manifest 不存在,当前显示默认策略");
111
+ if (!manifest && !fs.existsSync(settingsPath(ctx.target))) console.log(" · settings 不存在,当前显示默认策略");
112
112
  if (!fs.existsSync(cachePath)) {
113
- console.log(" · cache 不存在,当前显示默认或旧 manifest 兼容缓存");
113
+ console.log(" · cache 不存在,当前显示默认或旧记录兼容缓存");
114
114
  }
115
115
  return;
116
116
  }
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { spawnSync } from "node:child_process";
4
+ import { fileURLToPath } from "node:url";
4
5
  import { ENHANCEMENTS_ROOT } from "../lib/paths.js";
5
6
  import {
6
7
  resolveTrellisPythonCommand,
@@ -17,8 +18,9 @@ const VALUE_OPTIONS = new Set([
17
18
  "--task-description",
18
19
  "--developer",
19
20
  "--plan-fingerprint",
21
+ "--source",
20
22
  ]);
21
- const BOOLEAN_OPTIONS = new Set(["--json", "--dry-run", "--yes", "--inherit-route-prefs"]);
23
+ const BOOLEAN_OPTIONS = new Set(["--json", "--dry-run", "--yes", "--inherit-route-prefs", "--inherit-flower"]);
22
24
 
23
25
  /**
24
26
  * 识别 worktree 根级或子命令级帮助请求。
@@ -64,9 +66,12 @@ needs-migration 状态选择下一步。`);
64
66
  用法:
65
67
  flower-trellis worktree prepare [--target <dir>] [--developer <name>]
66
68
  [--inherit-route-prefs] [--json]
69
+ [--inherit-flower --source <dir>]
67
70
 
68
71
  prepare 不创建 Git worktree,只补齐目标分支本地的 developer、runtime 和 registry 状态。
69
- 仅在确实需要继承当前控制 worktree 的个人路由偏好时使用 --inherit-route-prefs。`);
72
+ 仅在确实需要继承当前控制 worktree 的个人路由偏好时使用 --inherit-route-prefs。
73
+ Flower 记录缺失时,用 --inherit-flower --source <同仓 worktree> 补齐可验证的独立安装信息。
74
+ 即使 Trellis 已 ready-local,也可以运行此补配;已有目标记录不会被覆盖。`);
70
75
  return;
71
76
  }
72
77
  if (command === "migrate") {
@@ -96,7 +101,9 @@ prepare 不创建 Git worktree,只补齐目标分支本地的 developer、runt
96
101
  git worktree add <target> <existing-branch>
97
102
  flower-trellis worktree status --target <target>
98
103
  若 status 返回 needs-prepare,再运行:
99
- flower-trellis worktree prepare --target <target> [--developer <name>]`);
104
+ flower-trellis worktree prepare --target <target> [--developer <name>]
105
+ create 自动将缺失的 Flower 安装信息纳入计划;目标内容不匹配时停止并回滚。
106
+ 已有 worktree 缺少 Flower 记录时,使用 prepare --inherit-flower --source <source>。`);
100
107
  return;
101
108
  }
102
109
  if (command === "remove") {
@@ -177,6 +184,15 @@ export function parseWorktreeArgs(args) {
177
184
  if (command !== "prepare" && options.has("--inherit-route-prefs")) {
178
185
  throw new Error("--inherit-route-prefs 只用于 worktree prepare");
179
186
  }
187
+ if (command !== "prepare" && (options.has("--inherit-flower") || options.has("--source"))) {
188
+ throw new Error("--inherit-flower 和 --source 只用于 worktree prepare");
189
+ }
190
+ if (options.has("--inherit-flower") && !options.has("--source")) {
191
+ throw new Error("--inherit-flower 需要 --source 指定同仓来源");
192
+ }
193
+ if (options.has("--source") && !options.has("--inherit-flower")) {
194
+ throw new Error("--source 需要显式 --inherit-flower");
195
+ }
180
196
  return { command, options, json: options.has("--json") };
181
197
  }
182
198
 
@@ -194,7 +210,7 @@ export function worktreeEngineArgs(parsed, target, source) {
194
210
  args.push("--source", source, "--target", target);
195
211
  } else {
196
212
  args.push("--target", target);
197
- if (parsed.command === "prepare" && parsed.options.has("--inherit-route-prefs")) {
213
+ if (parsed.command === "prepare" && parsed.options.has("--inherit-route-prefs") && !parsed.options.has("--source")) {
198
214
  args.push("--source", source);
199
215
  }
200
216
  }
@@ -244,6 +260,17 @@ export function printWorktreeResult(payload) {
244
260
  if (payload.localStateTransfer?.routePreferences?.action) {
245
261
  console.log(`route preferences: ${payload.localStateTransfer.routePreferences.action}`);
246
262
  }
263
+ if (payload.localStateTransfer?.flower) {
264
+ const flower = payload.localStateTransfer.flower;
265
+ console.log(`flower transfer: ${flower.action}${flower.reason ? ` (${flower.reason})` : ""}`);
266
+ if (flower.source) console.log(`flower source: ${flower.source}`);
267
+ if (flower.paths?.length) console.log(`flower paths: ${flower.paths.join(", ")}`);
268
+ if (flower.validationPending) console.log("flower validation: checkout 后核验目标内容,失败回滚创建");
269
+ }
270
+ if (payload.flower) {
271
+ console.log(`flower: ${payload.flower.installation}; version: ${payload.flower.version || "unknown"}`);
272
+ if (payload.flower.reason) console.log(`flower reason: ${payload.flower.reason}`);
273
+ }
247
274
  if (payload.requiresConfirmation && payload.confirmation?.fingerprint) {
248
275
  console.log(`confirmation: 使用 --yes --plan-fingerprint ${payload.confirmation.fingerprint}`);
249
276
  }
@@ -284,7 +311,11 @@ export async function worktree(ctx) {
284
311
  [...invocation.args, script, ...worktreeEngineArgs(parsed, ctx.target, source)],
285
312
  {
286
313
  cwd: source,
287
- env: process.env,
314
+ env: {
315
+ ...process.env,
316
+ FLOWER_WORKTREE_NODE: process.execPath,
317
+ FLOWER_WORKTREE_HELPER: fileURLToPath(new URL("../lib/worktree-flower-state.js", import.meta.url)),
318
+ },
288
319
  encoding: "utf8",
289
320
  windowsHide: true,
290
321
  },
@@ -6,7 +6,7 @@ import { formatPatchDiagnostic } from "./patch-conflicts.js";
6
6
  * 叠加强化包的兼容 facade。
7
7
  *
8
8
  * 成功状态统一由 Plugin Runtime 写入 `.flower/`;旧
9
- * `.trellis/.flower-manifest.json` 只作为迁移证据读取,不再更新。
9
+ * `.trellis/.flower-manifest.json` 作为迁移证据读取,迁移成功后由同一事务删除。
10
10
  *
11
11
  * @param {string} target 目标项目根
12
12
  * @param {{variant?:string|null,skills?:string[],dryRun?:boolean}} [opts] 兼容参数
@@ -4,13 +4,7 @@ import crypto from "node:crypto";
4
4
  import { ProjectStore } from "../plugin/state/project-store.js";
5
5
 
6
6
  /**
7
- * flower-trellis 自己的安装清单。
8
- *
9
- * 记录「flower 上一次为该项目铺设了哪些强化文件」,使升级(如 0.5/old → 0.6)时
10
- * 能精确删除当前变体不再包含的过期 skill / command —— 只删自己铺过的路径,
11
- * 绝不误删用户或 Trellis 本体的文件。
12
- *
13
- * 放在 .trellis/ 下,随项目的 Trellis 生命周期存在(uninstall 删 .trellis 时一并消失)。
7
+ * 旧安装清单的兼容读取入口;成功迁移后删除,现代状态由 ProjectStore 管理。
14
8
  */
15
9
  const MANIFEST_REL = path.join(".trellis", ".flower-manifest.json");
16
10
  const LEGACY_UPDATE_CHECK_CACHE_REL = path.join(".trellis", ".flower-update-check.tmp");
@@ -148,7 +142,7 @@ function normalizeLastRemote(value) {
148
142
  * @param {object|null|undefined} value 原始 updateCheck 字段
149
143
  * @returns {{enabled:boolean,policy:string,intervalHours:number}} 归一化后的策略
150
144
  */
151
- function normalizeUpdateCheckPolicy(value) {
145
+ export function normalizeUpdateCheckPolicy(value) {
152
146
  const raw = value && typeof value === "object" ? value : {};
153
147
  const policy = UPDATE_POLICIES.has(raw.policy) ? raw.policy : DEFAULT_UPDATE_CHECK.policy;
154
148
  const interval = Number(raw.intervalHours);
@@ -374,18 +368,51 @@ export function readManifest(target) {
374
368
  * @returns {{status:"missing"|"valid"|"corrupt",manifest:object|null,error?:Error}} 迁移证据
375
369
  */
376
370
  export function readLegacyManifestStatus(target) {
377
- const filePath = manifestPath(target);
378
- try {
379
- const text = fs.readFileSync(filePath, "utf8");
380
- const manifest = JSON.parse(text);
381
- if (!isPlainObject(manifest)) {
382
- return { status: "corrupt", manifest: null, error: new Error("旧 manifest 必须是对象") };
371
+ const result = readJsonFileStatus(manifestPath(target));
372
+ if (result.status === "valid" && !isPlainObject(result.value)) {
373
+ return { status: "corrupt", manifest: null, error: new Error("旧 manifest 必须是对象") };
374
+ }
375
+ return { status: result.status, manifest: result.value, ...(result.error ? { error: result.error } : {}) };
376
+ }
377
+
378
+ /**
379
+ * 规划旧清单退出所需的配置写入及文件删除,保持 preflight 只读。
380
+ *
381
+ * @param {string} target 项目根
382
+ * @returns {Array<{path:string,content:Buffer|null}>} 固定目标及内容,null 表示删除
383
+ */
384
+ export function planLegacyManifestMigration(target) {
385
+ const legacy = readLegacyManifestStatus(target);
386
+ if (legacy.status === "missing") return [];
387
+ if (legacy.status === "corrupt") throw new Error(`旧 flower manifest 损坏,拒绝删除:${manifestPath(target)}`, { cause: legacy.error });
388
+ const writes = [];
389
+ const settings = readSettingsStatus(target);
390
+ if (settings.status === "corrupt") throw new Error(`update-check settings 损坏,拒绝迁移:${settingsPath(target)}`, { cause: settings.error });
391
+ const updateCheck = legacy.manifest.updateCheck;
392
+ if (settings.status === "missing" && hasAnyOwn(updateCheck, UPDATE_CHECK_POLICY_KEYS)) {
393
+ writes.push({
394
+ path: ".flower/settings.json",
395
+ content: Buffer.from(JSON.stringify({ schemaVersion: 1, updateCheck: normalizeUpdateCheckPolicy(updateCheck) }, null, 2) + "\n"),
396
+ });
397
+ }
398
+ const cache = readJsonFileStatus(updateCheckCachePath(target));
399
+ if (cache.status === "corrupt" || (cache.status === "valid" && !isPlainObject(cache.value))) {
400
+ throw new Error(`update-check cache 损坏,拒绝迁移:${updateCheckCachePath(target)}`);
401
+ }
402
+ if (cache.status === "missing") {
403
+ const oldCache = readJsonFileStatus(path.join(target, LEGACY_UPDATE_CHECK_CACHE_REL));
404
+ if (oldCache.status === "corrupt" || (oldCache.status === "valid" && !isPlainObject(oldCache.value))) {
405
+ throw new Error("旧 update-check cache 损坏,拒绝丢失迁移证据");
406
+ }
407
+ if (oldCache.status === "valid" || hasLegacyUpdateCheckCache(updateCheck)) {
408
+ writes.push({
409
+ path: ".flower/update-check.tmp",
410
+ content: Buffer.from(JSON.stringify(normalizeUpdateCheckCache(oldCache.value || updateCheck), null, 2) + "\n"),
411
+ });
383
412
  }
384
- return { status: "valid", manifest };
385
- } catch (error) {
386
- if (error?.code === "ENOENT") return { status: "missing", manifest: null };
387
- return { status: "corrupt", manifest: null, error };
388
413
  }
414
+ writes.push({ path: ".trellis/.flower-manifest.json", content: null });
415
+ return writes;
389
416
  }
390
417
 
391
418
  /**
@@ -406,7 +433,7 @@ export function normalizeUpdateCheck(value) {
406
433
  /**
407
434
  * 读取启动更新检查配置。
408
435
  *
409
- * 返回 manifest 策略与 tmp 运行缓存的合并视图;tmp 不存在时兼容读取旧 manifest 缓存字段。
436
+ * 返回现代 settings 策略与 tmp 缓存的合并视图,尚未迁移时兼容旧记录。
410
437
  *
411
438
  * @param {string} target 目标项目根
412
439
  * @returns {ReturnType<typeof normalizeUpdateCheck>} 归一化后的配置
@@ -464,27 +491,3 @@ export function writeUpdateCheck(target, patch) {
464
491
  }
465
492
  return readUpdateCheck(target);
466
493
  }
467
-
468
- /**
469
- * 写入 manifest。
470
- *
471
- * 若调用方没有显式传入 `updateCheck`,保留目标项目已有策略,避免全装重写时覆盖用户选择。
472
- * 运行缓存会迁移到 `.flower-update-check.tmp`,不得继续写入 manifest。
473
- *
474
- * @param {string} target 目标项目根
475
- * @param {object} data manifest 新内容
476
- */
477
- export function writeManifest(target, data) {
478
- const current = readManifest(target);
479
- const next = { ...data };
480
- if (hasLegacyUpdateCheckCache(current?.updateCheck)) {
481
- migrateLegacyUpdateCheckCache(target, current.updateCheck);
482
- }
483
- if (Object.prototype.hasOwnProperty.call(data, "updateCheck")) {
484
- migrateLegacyUpdateCheckCache(target, data.updateCheck);
485
- next.updateCheck = normalizeUpdateCheckPolicy(data.updateCheck);
486
- } else {
487
- next.updateCheck = normalizeUpdateCheckPolicy(current?.updateCheck);
488
- }
489
- fs.writeFileSync(manifestPath(target), JSON.stringify(next, null, 2) + "\n");
490
- }
@@ -572,8 +572,9 @@ export async function buildSelfCheck(target, options = {}) {
572
572
  const projectTrellis = readProjectTrellisVersion(absoluteTarget);
573
573
  const skillGardenLock = pluginLock?.plugins.find(({ id }) => id === SKILL_GARDEN_PLUGIN_ID);
574
574
  const projectFlower = skillGardenLock?.version || (
575
- typeof manifest?.flowerVersion === "string" ? manifest.flowerVersion : null
575
+ typeof manifest?.flowerVersion === "string" && manifest.flowerVersion.trim() ? manifest.flowerVersion : null
576
576
  );
577
+ const flowerVersionSource = skillGardenLock?.version ? "plugin-lock" : projectFlower ? "legacy-manifest" : null;
577
578
  const projectOutOfSyncReasons = [];
578
579
  if (projectFlower && projectFlower !== currentFlower) {
579
580
  projectOutOfSyncReasons.push("flower_version_mismatch");
@@ -602,6 +603,8 @@ export async function buildSelfCheck(target, options = {}) {
602
603
  },
603
604
  project: {
604
605
  flowerVersion: projectFlower,
606
+ flowerVersionStatus: projectFlower ? "known" : "unknown",
607
+ flowerVersionSource,
605
608
  trellisVersion: projectTrellis,
606
609
  manifestPresent: Boolean(manifest),
607
610
  pluginStatePresent: Boolean(pluginLock && pluginState),
@@ -689,8 +692,8 @@ export async function buildSelfCheck(target, options = {}) {
689
692
  }
690
693
  return {
691
694
  ...base,
692
- status: "skipped",
693
- reason: "interval_not_elapsed",
695
+ status: projectFlower ? "skipped" : "project_unknown",
696
+ reason: projectFlower ? "interval_not_elapsed" : "project_flower_version_unknown",
694
697
  remote: { ...base.remote, tags, fromCache: true, skipped: true },
695
698
  };
696
699
  }
@@ -744,7 +747,8 @@ export async function buildSelfCheck(target, options = {}) {
744
747
  }
745
748
  return {
746
749
  ...resultBase,
747
- status: remoteStatus,
750
+ status: projectFlower ? remoteStatus : "project_unknown",
751
+ reason: projectFlower ? null : "project_flower_version_unknown",
748
752
  remote: { ...resultBase.remote, tags },
749
753
  };
750
754
  }