flower-trellis 0.6.5 → 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 (44) 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/overrides/conflicts.json +4 -4
  21. package/enhancements/0.6/overrides/patches/skills/trellis-brainstorm/planning-handoff/content.md +2 -4
  22. package/enhancements/0.6/overrides/patches/skills/trellis-brainstorm/planning-handoff/planning-contract-content.md +1 -1
  23. package/enhancements/0.6/overrides/patches/skills/trellis-brainstorm/planning-handoff/readiness-content.md +2 -2
  24. package/enhancements/0.6/overrides/patches/skills/trellis-brainstorm/planning-handoff/review-content.md +2 -2
  25. package/enhancements/0.6/scripts/worktree_setup.py +119 -17
  26. package/enhancements/MANIFEST.json +2 -2
  27. package/package.json +3 -3
  28. package/src/assets/flower_session_start.py +1 -1
  29. package/src/assets/flower_update_hook.py +135 -14
  30. package/src/builtin-plugins/skill-garden/content-adapter.js +4 -0
  31. package/src/builtin-plugins/skill-garden/project-metadata.js +41 -0
  32. package/src/commands/self-check.js +3 -1
  33. package/src/commands/self-update.js +3 -1
  34. package/src/commands/update-check.js +3 -3
  35. package/src/commands/worktree.js +36 -5
  36. package/src/lib/apply-enhancements.js +1 -1
  37. package/src/lib/manifest.js +46 -43
  38. package/src/lib/self-check.js +8 -4
  39. package/src/lib/worktree-flower-state.js +347 -0
  40. package/src/plugin/application-service.js +1 -44
  41. package/src/plugin/install/state-validation.js +47 -0
  42. package/src/plugin/install/transaction-writer.js +23 -2
  43. package/src/plugin/state/ignore-rules.js +40 -0
  44. package/src/plugin/state/project-store.js +3 -25
@@ -35,7 +35,7 @@ ROUTE_PREFERENCE_MODES = {
35
35
  NOT_INHERITED_LOCAL_STATE = (
36
36
  "session-state",
37
37
  "auto-loop",
38
- "flower-local-state",
38
+ "flower-control-state",
39
39
  "platform-local-settings",
40
40
  "cache-and-transaction-state",
41
41
  )
@@ -496,6 +496,60 @@ def _registry_registration_conflicts(
496
496
  return conflicts
497
497
 
498
498
 
499
+ def _flower_call(operation: str, **values: Any) -> dict[str, Any]:
500
+ """调用 Flower 随包适配器,保持 Plugin schema 和写入逻辑的单一归属。"""
501
+ executable = os.environ.get("FLOWER_WORKTREE_NODE")
502
+ helper = os.environ.get("FLOWER_WORKTREE_HELPER")
503
+ if not executable or not helper:
504
+ if operation == "status":
505
+ return {"installation": "unchecked", "version": None, "source": None, "reason": "flower-cli-required"}
506
+ source = Path(values.get("source", "."))
507
+ flower_directory = source / ".flower"
508
+ if operation == "plan" and not flower_directory.is_symlink() and not any(
509
+ (flower_directory / name).exists() for name in ("plugins.json", "plugin-lock.json", "state.json")
510
+ ):
511
+ return {"action": "unavailable", "paths": [], "reason": "source-installation-missing"}
512
+ raise WorktreeSetupError("flower-cli-required", "Flower 继承需要通过外部 flower-trellis worktree 命令执行")
513
+ try:
514
+ result = subprocess.run(
515
+ [executable, helper],
516
+ input=json.dumps({"operation": operation, **values}, ensure_ascii=False),
517
+ capture_output=True,
518
+ text=True,
519
+ timeout=60,
520
+ check=False,
521
+ )
522
+ payload = json.loads(result.stdout)
523
+ except (OSError, subprocess.SubprocessError, ValueError) as error:
524
+ raise WorktreeSetupError("flower-adapter-failed", "Flower worktree 适配器执行失败", error=str(error)) from error
525
+ if not isinstance(payload, dict) or payload.get("ok") is not True or result.returncode != 0:
526
+ details = payload if isinstance(payload, dict) else {}
527
+ raise WorktreeSetupError(
528
+ details.get("reason", "flower-adapter-failed"),
529
+ details.get("message", "Flower worktree 适配器返回非法结果"),
530
+ path=details.get("path"),
531
+ )
532
+ if not isinstance(payload.get("result"), dict):
533
+ raise WorktreeSetupError("flower-adapter-failed", "Flower worktree 适配器缺少结构化结果")
534
+ value = payload["result"]
535
+ if operation == "status":
536
+ valid = value.get("installation") in {"complete", "incomplete", "invalid"}
537
+ elif operation in {"plan", "apply"}:
538
+ valid = (
539
+ value.get("action") in {"inherited", "preserved", "unavailable"}
540
+ and isinstance(value.get("paths"), list)
541
+ and all(isinstance(item, str) for item in value["paths"])
542
+ and isinstance(value.get("digest"), str)
543
+ and value["digest"].startswith("sha256:")
544
+ and len(value["digest"]) == 71
545
+ )
546
+ else:
547
+ valid = value.get("action") == "rolledBack"
548
+ if not valid:
549
+ raise WorktreeSetupError("flower-adapter-failed", "Flower worktree 适配器结果不符合操作契约")
550
+ return value
551
+
552
+
499
553
  def _analyze(target_arg: str | None) -> dict[str, Any]:
500
554
  """生成 target-local readiness 诊断,且不写盘。"""
501
555
  context = _worktree_context(_resolve_start(target_arg))
@@ -537,7 +591,7 @@ def _analyze(target_arg: str | None) -> dict[str, Any]:
537
591
  conflicts.append({"path": relative, "reason": item["reason"]})
538
592
  else:
539
593
  item["state"] = "missing"
540
- if relative in configured:
594
+ if relative in configured and relative != ".flower":
541
595
  missing_configured.append(relative)
542
596
  entries.append(item)
543
597
 
@@ -610,11 +664,14 @@ def _analyze(target_arg: str | None) -> dict[str, Any]:
610
664
  },
611
665
  "actions": actions,
612
666
  "conflicts": conflicts,
667
+ "flower": _flower_call("status", target=str(target_root)),
613
668
  }
614
669
 
615
670
 
616
671
  def _developer_from_file(target_root: Path) -> str | None:
617
672
  """从目标本地 `.developer` 读取开发者名。"""
673
+ if (target_root / ".trellis").is_symlink() or (target_root / ".trellis/.developer").is_symlink():
674
+ return None
618
675
  try:
619
676
  lines = (target_root / ".trellis/.developer").read_text(encoding="utf-8").splitlines()
620
677
  except OSError:
@@ -721,6 +778,7 @@ def _prepare_local(
721
778
  *,
722
779
  source: str | None = None,
723
780
  inherit_route_prefs: bool = False,
781
+ inherit_flower: bool = False,
724
782
  ) -> dict[str, Any]:
725
783
  """初始化当前 worktree 自己的 gitignored 运行态并注册。"""
726
784
  if plan["status"] == "needs-migration":
@@ -734,22 +792,23 @@ def _prepare_local(
734
792
  context = _worktree_context(target_root)
735
793
  source_context = None
736
794
  source_developer = None
737
- if inherit_route_prefs:
795
+ if inherit_route_prefs or inherit_flower:
738
796
  if not source:
739
797
  raise WorktreeSetupError(
740
798
  "route-preferences-source-required",
741
- "显式继承 route 偏好时需要 --source",
799
+ "显式继承本地信息时需要 --source",
742
800
  )
743
801
  source_context = _worktree_context(_resolve_start(source))
744
802
  if source_context["gitCommonDir"] != context["gitCommonDir"]:
745
803
  raise WorktreeSetupError(
746
804
  "route-preferences-repository-mismatch",
747
- "route 偏好来源与目标不属于同一 Git 仓库",
805
+ "继承来源与目标不属于同一 Git 仓库",
748
806
  )
749
807
  source_developer = _developer_from_file(source_context["targetRoot"])
750
808
  changed_paths: list[str] = []
751
809
  route_transfer = {"action": "notRequested", "values": {}}
752
- with _registry_lock(context["gitCommonDir"]):
810
+ flower_transfer: dict[str, Any] = {"action": "notRequested", "paths": []}
811
+ with _registry_lock(context["gitCommonDir"]), tempfile.TemporaryDirectory(prefix="flower-worktree-") as temporary:
753
812
  current_developer = _developer_from_file(target_root)
754
813
  registry = _load_registry(context["gitCommonDir"])
755
814
  registration_conflicts = _registry_registration_conflicts(context, registry, task=None)
@@ -781,17 +840,42 @@ def _prepare_local(
781
840
  source_developer,
782
841
  normalized_developer,
783
842
  )
784
- runtime_sessions = target_root / ".trellis/.runtime/sessions"
785
- if not runtime_sessions.is_dir():
786
- runtime_sessions.mkdir(parents=True, exist_ok=True)
787
- changed_paths.append(".trellis/.runtime/sessions")
788
- if current_developer is None:
789
- _write_developer(target_root, normalized_developer)
790
- changed_paths.append(".trellis/.developer")
791
- if route_transfer["action"] == "inherited":
792
- _write_route_preferences(target_root, route_transfer["values"])
793
- changed_paths.append(ROUTE_PREFERENCES_PATH)
794
- registry_changed = _register_worktree(context, developer=normalized_developer)
843
+ journal = str(Path(temporary) / "flower-receipt.json")
844
+ try:
845
+ if inherit_flower:
846
+ flower_input = {
847
+ "source": str(source_context["targetRoot"]),
848
+ "target": str(target_root),
849
+ "sourceDeveloper": source_developer,
850
+ "targetDeveloper": normalized_developer,
851
+ }
852
+ flower_transfer = _flower_call("plan", **flower_input)
853
+ flower_transfer = _flower_call(
854
+ "apply", **flower_input, expectedDigest=flower_transfer.get("digest"), journal=journal,
855
+ )
856
+ flower_transfer.pop("receipt", None)
857
+ changed_paths.extend(flower_transfer.get("paths", []))
858
+ runtime_sessions = target_root / ".trellis/.runtime/sessions"
859
+ if not runtime_sessions.is_dir():
860
+ runtime_sessions.mkdir(parents=True, exist_ok=True)
861
+ changed_paths.append(".trellis/.runtime/sessions")
862
+ if current_developer is None:
863
+ _write_developer(target_root, normalized_developer)
864
+ changed_paths.append(".trellis/.developer")
865
+ if route_transfer["action"] == "inherited":
866
+ _write_route_preferences(target_root, route_transfer["values"])
867
+ changed_paths.append(ROUTE_PREFERENCES_PATH)
868
+ registry_changed = _register_worktree(context, developer=normalized_developer)
869
+ except Exception as error:
870
+ if Path(journal).is_file():
871
+ try:
872
+ _flower_call("rollback", target=str(target_root), journal=journal)
873
+ except WorktreeSetupError as rollback_error:
874
+ raise WorktreeSetupError(
875
+ "flower-rollback-failed", "prepare 失败且 Flower 记录回滚不完整",
876
+ error=str(error), rollbackError=str(rollback_error),
877
+ ) from error
878
+ raise
795
879
  result = {
796
880
  **_analyze(str(target_root)),
797
881
  "status": "prepared" if changed_paths or registry_changed else "ready-local",
@@ -801,6 +885,7 @@ def _prepare_local(
801
885
  }
802
886
  result["localStateTransfer"] = {
803
887
  "routePreferences": route_transfer,
888
+ "flower": flower_transfer,
804
889
  "notInherited": list(NOT_INHERITED_LOCAL_STATE),
805
890
  }
806
891
  return result
@@ -1121,6 +1206,10 @@ def _create_plan(args: argparse.Namespace) -> dict[str, Any]:
1121
1206
  if not developer:
1122
1207
  raise WorktreeSetupError("developer-required", "create 需要 --developer 或来源 worktree 的本地身份")
1123
1208
  route_transfer = _route_preference_transfer(source_root, target, source_developer, developer)
1209
+ flower_transfer = _flower_call(
1210
+ "plan", source=str(source_root), target=str(target), baseCommit=resolved_commit,
1211
+ sourceDeveloper=source_developer, targetDeveloper=developer,
1212
+ )
1124
1213
  plan: dict[str, Any] = {
1125
1214
  "status": "confirmation-required",
1126
1215
  "changed": False,
@@ -1161,6 +1250,7 @@ def _create_plan(args: argparse.Namespace) -> dict[str, Any]:
1161
1250
  "sourceName": source_developer,
1162
1251
  },
1163
1252
  "routePreferences": route_transfer,
1253
+ "flower": flower_transfer,
1164
1254
  "initialized": ["session-runtime"],
1165
1255
  "notInherited": list(NOT_INHERITED_LOCAL_STATE),
1166
1256
  },
@@ -1239,6 +1329,16 @@ def _create(args: argparse.Namespace) -> dict[str, Any]:
1239
1329
  if route_transfer["action"] == "inherited":
1240
1330
  _write_route_preferences(target, route_transfer["values"])
1241
1331
 
1332
+ flower_transfer = plan["localStateTransfer"]["flower"]
1333
+ if flower_transfer.get("digest"):
1334
+ flower_transfer = _flower_call(
1335
+ "apply", source=str(source_root), target=str(target), baseCommit=base_commit,
1336
+ sourceDeveloper=plan["localStateTransfer"]["developer"]["sourceName"],
1337
+ targetDeveloper=developer, expectedDigest=flower_transfer["digest"],
1338
+ )
1339
+ flower_transfer.pop("receipt", None)
1340
+ plan["localStateTransfer"]["flower"] = flower_transfer
1341
+
1242
1342
  task_script = target / ".trellis/scripts/task.py"
1243
1343
  if not task_script.is_file():
1244
1344
  raise WorktreeSetupError("task-script-missing", "目标分支缺少 `.trellis/scripts/task.py`")
@@ -1561,6 +1661,7 @@ def build_parser() -> argparse.ArgumentParser:
1561
1661
  action="store_true",
1562
1662
  help="inherit normalized route preferences from --source",
1563
1663
  )
1664
+ command_parser.add_argument("--inherit-flower", action="store_true", help="inherit verified Flower records from --source")
1564
1665
  if command == "migrate":
1565
1666
  command_parser.add_argument("--dry-run", action="store_true", help="validate migration without writing")
1566
1667
 
@@ -1603,6 +1704,7 @@ def main(argv: list[str] | None = None) -> int:
1603
1704
  args.developer,
1604
1705
  source=args.source,
1605
1706
  inherit_route_prefs=args.inherit_route_prefs,
1707
+ inherit_flower=args.inherit_flower,
1606
1708
  )
1607
1709
  elif args.command == "migrate":
1608
1710
  payload = _migrate(plan, dry_run=args.dry_run)
@@ -1,7 +1,7 @@
1
1
  {
2
- "syncedAt": "2026-09-05T18:24:52.904Z",
2
+ "syncedAt": "2026-09-07T07:30:55.059Z",
3
3
  "syncedFrom": "vendor/skill-garden",
4
- "sourceCommit": "6199ab8b2e0259ff695d70797943ea1ae97e099c",
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.5",
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.5",
73
+ "version": "0.6.7",
74
74
  "source": "CHANGELOG.md",
75
- "body": "### ✨ 新功能 Features\n\n* **cli:** 优化调用契约与帮助体验 ([b7a39db](https://github.com/SilentFlower/flower-trellis/commit/b7a39dbda2042643c3e15dd47caebe7c83242548))\n* **codex:** 为 Astra 添加 SessionStart 工作流提示 ([1d79835](https://github.com/SilentFlower/flower-trellis/commit/1d79835f4bea1782f5774ff8c14a6263742b1677))\n* **telemetry:** 采集平台活跃与核心操作质量 ([08598bf](https://github.com/SilentFlower/flower-trellis/commit/08598bf26638567b9183150d7c258e2f801046a4))\n\n\n### 🐛 修复 Bug Fixes\n\n* **hooks:** 分段注入启动上下文并保留额度 ([5eaf6af](https://github.com/SilentFlower/flower-trellis/commit/5eaf6afd7c7b088e8421f3b176d0d79ccb9cdba2))\n* **plugin:** 兼容旧 RD Guide 离线升级 ([fcec390](https://github.com/SilentFlower/flower-trellis/commit/fcec3905629dd31bc3f113ac28bf64139311324e))",
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": {
@@ -27,7 +27,7 @@ When executing the current task:
27
27
  - Treat required steps, required references, phase boundaries, and output templates in applicable SKILL and WORKFLOW instructions as execution and delivery checks.
28
28
  - Before a step, review its rules and required references. Reuse material already read in full and unchanged; search matches are not full reads.
29
29
  - Preserve required heading levels, section order, and conditional sections in specified templates. General brevity or no-heading preferences apply to ordinary prose and do not justify flattening, shortening, or reshaping a specified template.
30
- - Resolve conflicts by instruction hierarchy and respect the user's current explicit authorization. Do not invent additional confirmation steps or claim that this hint or a SKILL overrides all host rules.
30
+ - Resolve conflicts by instruction hierarchy. Follow the owning workflow's phase-transition and review requirements, checking the scope of prior authorization; do not infer approval of the final plan from permission to begin planning.
31
31
  - Before claiming "read", "checked", or "complete", verify actual tool records and artifacts. Successful reading and compliant execution are separate facts.
32
32
  - When corrected, review the applicable rules, execution records, and actual result before repairing it. If evidence is missing, state uncertainty. Do not invent causes such as "not read", "forgot", or "file missing", or consult unrelated rules in place of the relevant ones.
33
33
  </trellis-astra-workflow-hint>"""
@@ -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
  }