flower-trellis 0.6.4 → 0.6.6

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 (54) hide show
  1. package/README.md +20 -1
  2. package/enhancements/0.6/overrides/bundles/control-plane-integrity.json +2 -0
  3. package/enhancements/0.6/overrides/bundles/intent-routing.json +2 -1
  4. package/enhancements/0.6/overrides/bundles/task-reference-contract.json +24 -0
  5. package/enhancements/0.6/overrides/conflicts.json +37 -4
  6. package/enhancements/0.6/overrides/patches/scripts/task-current-query-contract/patch.json +19 -0
  7. package/enhancements/0.6/overrides/patches/scripts/task-current-query-contract/task-current-content.py +39 -0
  8. package/enhancements/0.6/overrides/patches/scripts/task-current-query-contract/task-current-selector.py +38 -0
  9. package/enhancements/0.6/overrides/patches/scripts/task-reference-resolution/patch.json +20 -0
  10. package/enhancements/0.6/overrides/patches/scripts/task-reference-resolution/task-reference-content.py +64 -0
  11. package/enhancements/0.6/overrides/patches/scripts/task-reference-resolution/task-reference-selector.py +2 -0
  12. package/enhancements/0.6/overrides/patches/skills/trellis-brainstorm/planning-handoff/content.md +2 -4
  13. package/enhancements/0.6/overrides/patches/skills/trellis-brainstorm/planning-handoff/planning-contract-content.md +1 -1
  14. package/enhancements/0.6/overrides/patches/skills/trellis-brainstorm/planning-handoff/readiness-content.md +2 -2
  15. package/enhancements/0.6/overrides/patches/skills/trellis-brainstorm/planning-handoff/review-content.md +2 -2
  16. package/enhancements/0.6/scripts/decision_log.py +29 -24
  17. package/enhancements/0.6/scripts/task_progress.py +54 -13
  18. package/enhancements/0.6/scripts/untracked_flow.py +13 -5
  19. package/enhancements/MANIFEST.json +9 -2
  20. package/package.json +4 -4
  21. package/src/assets/flower_session_start.py +195 -0
  22. package/src/assets/flower_telemetry_hook.py +98 -0
  23. package/src/builtin-plugins/skill-garden/content-adapter.js +15 -1
  24. package/src/cli.js +3 -1
  25. package/src/commands/init.js +36 -1
  26. package/src/commands/plugin-remote.js +61 -14
  27. package/src/commands/plugin.js +14 -1
  28. package/src/commands/self-check.js +20 -0
  29. package/src/commands/self-update.js +38 -3
  30. package/src/commands/telemetry.js +30 -1
  31. package/src/commands/uninstall.js +20 -0
  32. package/src/commands/update-check.js +19 -0
  33. package/src/commands/update.js +52 -9
  34. package/src/commands/worktree.js +114 -0
  35. package/src/lib/cli-args.js +10 -0
  36. package/src/lib/developer.js +3 -2
  37. package/src/lib/flower-assets.js +21 -6
  38. package/src/lib/platform-patch-adapters.js +46 -8
  39. package/src/lib/telemetry-context.js +275 -0
  40. package/src/lib/telemetry-files.js +144 -0
  41. package/src/lib/telemetry-operation.js +84 -0
  42. package/src/lib/telemetry-queue.js +310 -0
  43. package/src/lib/telemetry-sender.js +7 -0
  44. package/src/lib/telemetry.js +12 -361
  45. package/src/patches/bundles/flower-platform-integration.json +4 -1
  46. package/src/patches/platforms/claude/session-start-hooks/patch.json +28 -0
  47. package/src/patches/platforms/claude/telemetry-hooks/patch.json +59 -0
  48. package/src/patches/platforms/codex/session-start-hooks/patch.json +3 -2
  49. package/src/patches/platforms/codex/telemetry-hooks/patch.json +59 -0
  50. package/src/plugin/application-service.js +5 -22
  51. package/src/plugin/contracts.js +1 -0
  52. package/src/plugin/lock-reachability.js +37 -0
  53. package/src/plugin/schemas/project-files.js +1 -0
  54. package/src/plugin/sources/gitlab-provider.js +50 -9
@@ -1,7 +1,7 @@
1
1
  {
2
- "syncedAt": "2026-08-28T18:19:48.301Z",
2
+ "syncedAt": "2026-09-07T02:30:42.961Z",
3
3
  "syncedFrom": "vendor/skill-garden",
4
- "sourceCommit": "124b592037eef542d7faf7ecb3be93b4bfd3aed0",
4
+ "sourceCommit": "ad64c8ab30cb14184895aa31d672cf1b846e5726",
5
5
  "common": {
6
6
  "codexSkills": [
7
7
  "aliyun-ops",
@@ -216,6 +216,12 @@
216
216
  "scripts/session-context-update-boundary/output-content.py",
217
217
  "scripts/session-context-update-boundary/output-selector.py",
218
218
  "scripts/session-context-update-boundary/patch.json",
219
+ "scripts/task-current-query-contract/patch.json",
220
+ "scripts/task-current-query-contract/task-current-content.py",
221
+ "scripts/task-current-query-contract/task-current-selector.py",
222
+ "scripts/task-reference-resolution/patch.json",
223
+ "scripts/task-reference-resolution/task-reference-content.py",
224
+ "scripts/task-reference-resolution/task-reference-selector.py",
219
225
  "scripts/task-start-brief-gate/degraded-content.py",
220
226
  "scripts/task-start-brief-gate/degraded-selector.py",
221
227
  "scripts/task-start-brief-gate/guard-content.py",
@@ -451,6 +457,7 @@
451
457
  "maven-verification.json",
452
458
  "shared-hook-runtime.json",
453
459
  "task-progress-recovery.json",
460
+ "task-reference-contract.json",
454
461
  "trellis-meta.json",
455
462
  "trellis-session-insight.json",
456
463
  "untracked-execution.json",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flower-trellis",
3
- "version": "0.6.4",
3
+ "version": "0.6.6",
4
4
  "description": "一键安装/升级 Trellis 并自动融合 skill-garden 强化包(默认 Claude + agents)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -33,7 +33,7 @@
33
33
  "README.md"
34
34
  ],
35
35
  "scripts": {
36
- "test": "node --test test/js/*.test.js && python3 -m unittest discover -s test/python -p 'test_*.py' && node scripts/check-patch-conflicts.mjs && npm run patch:targets:check && node scripts/check-ai-context-budget.mjs && node scripts/check-output-templates.mjs",
36
+ "test": "node scripts/run-js-tests.mjs && python3 -m unittest discover -s test/python -p 'test_*.py' && node scripts/check-patch-conflicts.mjs && npm run patch:targets:check && node scripts/check-ai-context-budget.mjs && node scripts/check-output-templates.mjs",
37
37
  "sync": "node scripts/sync-enhancements.mjs",
38
38
  "patch:targets": "node scripts/run-skill-garden-compiled-targets.mjs",
39
39
  "patch:targets:check": "node scripts/run-skill-garden-compiled-targets.mjs --check",
@@ -70,9 +70,9 @@
70
70
  "commit-and-tag-version": "^12.7.3"
71
71
  },
72
72
  "flowerReleaseNotes": {
73
- "version": "0.6.4",
73
+ "version": "0.6.6",
74
74
  "source": "CHANGELOG.md",
75
- "body": "### 🐛 修复 Bug Fixes\n\n* **plugin:** 支持离线冻结未更新的外部 Plugin ([6210cfa](https://github.com/SilentFlower/flower-trellis/commit/6210cfae8d459a3e7b105043e4ecbc23d1b5bed9))",
75
+ "body": "### 🐛 修复 Bug Fixes\n\n* **codex:** 明确 Astra 规划授权边界 ([07428f8](https://github.com/SilentFlower/flower-trellis/commit/07428f8c4406449656edfe2e6dd6463d4540370a))",
76
76
  "truncated": false
77
77
  },
78
78
  "optionalDependencies": {
@@ -0,0 +1,195 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """复用 Trellis 原生 SessionStart,将启动上下文分成三个独立输出。"""
4
+
5
+ from __future__ import annotations
6
+
7
+ import argparse
8
+ import json
9
+ import os
10
+ import re
11
+ import sys
12
+ from contextlib import redirect_stdout
13
+ from importlib.util import module_from_spec, spec_from_file_location
14
+ from io import StringIO
15
+ from pathlib import Path
16
+
17
+
18
+ PARTS = ("state", "rules", "stages")
19
+ HOOKS = (".codex/hooks/session-start.py", ".claude/hooks/session-start.py")
20
+ MAX_PART_CHARS = 8000
21
+ WORKFLOW_BLOCK = re.compile(r"<trellis-workflow>\n(.*?)\n</trellis-workflow>\n*", re.DOTALL)
22
+ ASTRA_MODEL = "gpt-6-astra"
23
+ ASTRA_HINT_MAX_BYTES = 2048
24
+ ASTRA_WORKFLOW_HINT = """<trellis-astra-workflow-hint model="gpt-6-astra" version="1">
25
+ Applies only while the active model is gpt-6-astra; it does not apply after switching models. Perform checks internally, without a routine checklist report. Keep ordinary answers brief.
26
+ When executing the current task:
27
+ - Treat required steps, required references, phase boundaries, and output templates in applicable SKILL and WORKFLOW instructions as execution and delivery checks.
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
+ - 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. 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
+ - Before claiming "read", "checked", or "complete", verify actual tool records and artifacts. Successful reading and compliant execution are separate facts.
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
+ </trellis-astra-workflow-hint>"""
34
+
35
+
36
+ def _astra_workflow_hint(root: Path) -> str:
37
+ """读取项目开关,返回唯一来源的 Astra 提示或空串。
38
+
39
+ @param root: 当前部署项目根目录。
40
+ @return: 完整提示块;显式关闭时为空串,非法配置或超预算时抛出异常。
41
+ """
42
+ scripts_dir = str(root / ".trellis" / "scripts")
43
+ if scripts_dir not in sys.path:
44
+ sys.path.insert(0, scripts_dir)
45
+ from common.trellis_config import read_trellis_config
46
+
47
+ config = read_trellis_config(root)
48
+ codex = config.get("codex", {})
49
+ if not isinstance(codex, dict):
50
+ raise ValueError("codex 配置必须为映射")
51
+ enabled = codex.get("astra_workflow_hint", True)
52
+ # 上游无依赖 YAML 读取器返回字符串,不能把字符串 false 当作真值。
53
+ if isinstance(enabled, str) and enabled.lower() in ("true", "false"):
54
+ enabled = enabled.lower() == "true"
55
+ if not isinstance(enabled, bool):
56
+ raise ValueError("codex.astra_workflow_hint 必须为 true 或 false")
57
+ if not enabled:
58
+ return ""
59
+ if len(ASTRA_WORKFLOW_HINT.encode("utf-8")) > ASTRA_HINT_MAX_BYTES:
60
+ raise ValueError("Astra 工作流提示超过 2048 字节预算")
61
+ return ASTRA_WORKFLOW_HINT
62
+
63
+
64
+ def split_workflow(summary: str) -> dict[str, str]:
65
+ """按完整章节拆分工作流,保持原文和顺序。
66
+
67
+ @param summary: 原生 hook 生成的工作流摘要。
68
+ @return: rules 与 stages 的无损分段。
69
+ """
70
+ boundary = re.search(r"^### Planning Artifacts\s*$", summary, re.MULTILINE)
71
+ if boundary is None:
72
+ # 不能猜测新模板的边界,否则可能静默遗漏未知的工作流规则。
73
+ raise ValueError("工作流摘要缺少 Planning Artifacts 分段边界")
74
+ return {"rules": summary[:boundary.start()], "stages": summary[boundary.start():]}
75
+
76
+
77
+ def _load_hook(root: Path, hook: str):
78
+ """加载已部署的原生 hook,避免复制平台状态与会话绑定逻辑。"""
79
+ path = root / hook
80
+ spec = spec_from_file_location("flower_native_session_start", path)
81
+ if spec is None or spec.loader is None:
82
+ raise ValueError(f"无法加载原生 hook:{hook}")
83
+ module = module_from_spec(spec)
84
+ spec.loader.exec_module(module)
85
+ return module
86
+
87
+
88
+ def render_part(root: Path, hook: str, part: str, hook_input: dict) -> dict | None:
89
+ """生成指定分段,只有 state 执行原生主入口的副作用。
90
+
91
+ @param root: 目标项目根目录。
92
+ @param hook: 已验证的原生平台 hook 相对路径。
93
+ @param part: state、rules 或 stages。
94
+ @param hook_input: 宿主传入的事件 JSON。
95
+ @return: 标准 SessionStart 输出;原生 hook 禁用时返回 None。
96
+ """
97
+ if hook not in HOOKS or part not in PARTS:
98
+ raise ValueError("不支持的 SessionStart hook 或分段")
99
+ module = _load_hook(root, hook)
100
+ if module.should_skip_injection():
101
+ return None
102
+ if part == "state":
103
+ output = StringIO()
104
+ original_stdin = sys.stdin
105
+ try:
106
+ sys.stdin = StringIO(json.dumps(hook_input, ensure_ascii=False))
107
+ with redirect_stdout(output):
108
+ module.main()
109
+ finally:
110
+ sys.stdin = original_stdin
111
+ if not output.getvalue().strip():
112
+ return None
113
+ result = json.loads(output.getvalue())
114
+ context = result["hookSpecificOutput"]["additionalContext"]
115
+ if len(WORKFLOW_BLOCK.findall(context)) != 1:
116
+ raise ValueError("原生启动输出必须包含且仅包含一个 trellis-workflow 块")
117
+ context = WORKFLOW_BLOCK.sub("", context)
118
+ # Claude 原生文件同时兼容其他平台;这里仅输出当前宿主的标准通道。
119
+ result.pop("additional_context", None)
120
+ if re.fullmatch(r"Trellis context injected \(\d+ chars\)", result.get("systemMessage", "")):
121
+ # 原计数对应拆分前的全文;保留其他原生诊断,避免以后吞掉重要提示。
122
+ result.pop("systemMessage")
123
+ if (hook == HOOKS[0] and hook_input.get("model") == ASTRA_MODEL
124
+ and hook_input.get("source") in ("startup", "clear", "compact")):
125
+ try:
126
+ hint = _astra_workflow_hint(root)
127
+ if hint:
128
+ context = f"{context}\n{hint}"
129
+ except Exception as error:
130
+ # 提示是可选增强;失败不能吞掉已成功生成的原生启动上下文。
131
+ message = f"Astra 工作流提示未注入:{error}"
132
+ result["systemMessage"] = "\n".join(filter(None, [result.get("systemMessage"), message]))
133
+ print(message, file=sys.stderr)
134
+ else:
135
+ builder = module._build_workflow_toc if hook == HOOKS[0] else module._build_workflow_overview
136
+ context = split_workflow(builder(root / ".trellis/workflow.md"))[part]
137
+ result = {"hookSpecificOutput": {"hookEventName": "SessionStart"}}
138
+
139
+ context = f'<trellis-session-part name="{part}">\n{context}\n</trellis-session-part>'
140
+ result["hookSpecificOutput"]["additionalContext"] = context
141
+ if len(context) > MAX_PART_CHARS:
142
+ # 保留正文供宿主落盘补读,不能为了满足预算再次静默截断规则。
143
+ message = (
144
+ f"Trellis {part} 注入为 {len(context)} 字符,超过 {MAX_PART_CHARS} 字符预算;"
145
+ "请检查分段大小,并补读宿主保存的全文及 .trellis/workflow.md。"
146
+ )
147
+ result["systemMessage"] = "\n".join(filter(None, [result.get("systemMessage"), message]))
148
+ return result
149
+
150
+
151
+ def main() -> int:
152
+ """读取事件并输出一个分段,异常以可见诊断交付。
153
+
154
+ @return: 成功或已输出诊断时为 0。
155
+ """
156
+ parser = argparse.ArgumentParser(description=__doc__)
157
+ parser.add_argument("--hook", required=True, choices=HOOKS)
158
+ parser.add_argument("--part", required=True, choices=PARTS)
159
+ args = parser.parse_args()
160
+ if os.environ.get("TRELLIS_HOOKS") == "0" or os.environ.get("TRELLIS_DISABLE_HOOKS") == "1":
161
+ return 0
162
+ if args.hook == HOOKS[0] and os.environ.get("CODEX_NON_INTERACTIVE") == "1":
163
+ return 0
164
+ try:
165
+ hook_input = json.load(sys.stdin)
166
+ if not isinstance(hook_input, dict):
167
+ raise ValueError("hook 输入必须为 JSON 对象")
168
+ if hook_input.get("source") == "resume":
169
+ return 0
170
+ # 脚本随项目部署;使用自身位置,避免宿主 cwd 进入子目录时加载另一份 hook。
171
+ root = Path(__file__).resolve().parents[2]
172
+ hook_input = {**hook_input, "cwd": str(root)}
173
+ result = render_part(root, args.hook, args.part, hook_input)
174
+ except Exception as error:
175
+ message = f"Trellis SessionStart {args.part} 注入失败:{error}"
176
+ print(message, file=sys.stderr)
177
+ result = {
178
+ "systemMessage": message,
179
+ "hookSpecificOutput": {
180
+ "hookEventName": "SessionStart",
181
+ "additionalContext": (
182
+ f"<trellis-injection-error part=\"{args.part}\">\n"
183
+ "Startup context is incomplete. Read .trellis/workflow.md and run "
184
+ "python3 ./.trellis/scripts/get_context.py before continuing.\n"
185
+ "</trellis-injection-error>"
186
+ ),
187
+ },
188
+ }
189
+ if result is not None:
190
+ print(json.dumps(result, ensure_ascii=False), flush=True)
191
+ return 0
192
+
193
+
194
+ if __name__ == "__main__":
195
+ sys.exit(main())
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """将明确的平台活动交给 Flower 本地采集器;不输出或传递会话内容。"""
4
+
5
+ from __future__ import annotations
6
+
7
+ import json
8
+ import os
9
+ import shutil
10
+ import subprocess
11
+ import sys
12
+ import time
13
+ from datetime import datetime, timezone
14
+ from pathlib import Path
15
+
16
+
17
+ def config_directory(env: dict, platform: str, home: Path) -> Path:
18
+ """按 Node flowerConfigDirectory 相同规则定位用户目录。
19
+
20
+ @param env: 环境变量。
21
+ @param platform: win32 或其他平台。
22
+ @param home: 用户主目录。
23
+ @return: 用户配置路径。
24
+ """
25
+ if env.get("XDG_CONFIG_HOME"):
26
+ return Path(env["XDG_CONFIG_HOME"]) / "flower-trellis"
27
+ if platform == "win32" and env.get("APPDATA"):
28
+ return Path(env["APPDATA"]) / "flower-trellis"
29
+ return home / ".config" / "flower-trellis"
30
+
31
+
32
+ def _read_json(file: Path) -> dict:
33
+ """只读普通小文件;损坏提示由 Node 最终判断。"""
34
+ if any(parent.is_symlink() for parent in [file, *file.parents]):
35
+ raise ValueError("symlink")
36
+ if not file.is_file() or file.stat().st_size > 16384:
37
+ raise ValueError("invalid file")
38
+ value = json.loads(file.read_text(encoding="utf-8"))
39
+ if not isinstance(value, dict):
40
+ raise ValueError("invalid json")
41
+ return value
42
+
43
+
44
+ def _can_skip(platform: str) -> bool:
45
+ """同日提示命中后仍为到期 pending 留出唤醒机会。"""
46
+ try:
47
+ directory = config_directory(os.environ, sys.platform, Path.home())
48
+ state = _read_json(directory / "telemetry.json")
49
+ if state.get("enabled") is False:
50
+ return True
51
+ meta = _read_json(directory / "telemetry-v2" / "meta.json")
52
+ key = datetime.now(timezone.utc).date().isoformat() + ":" + platform
53
+ hint = meta.get("hints", {}).get(key)
54
+ if not isinstance(hint, dict) or not hint.get("event_id"):
55
+ return False
56
+ if meta.get("pending") == 0:
57
+ return hint.get("delivered") is True
58
+ retry = meta.get("nextRetryAt")
59
+ return bool(retry and datetime.fromisoformat(retry.replace("Z", "+00:00")).timestamp() > time.time())
60
+ except (OSError, ValueError, TypeError, AttributeError):
61
+ return False
62
+
63
+
64
+ def main() -> None:
65
+ """适配 SessionStart/UserPromptSubmit,所有失败静默退出。
66
+
67
+ @return: 无输出。
68
+ """
69
+ try:
70
+ args = sys.argv[1:]
71
+ if len(args) != 2 or args[0] != "--platform" or args[1] not in ("claude", "codex"):
72
+ return
73
+ platform = args[1]
74
+ if os.environ.get("FLOWER_NO_TELEMETRY") or os.environ.get("TRELLIS_HOOKS") == "0" or os.environ.get("TRELLIS_DISABLE_HOOKS") == "1":
75
+ return
76
+ if platform == "codex" and os.environ.get("CODEX_NON_INTERACTIVE") == "1":
77
+ return
78
+ hook_input = json.loads(sys.stdin.read(1048577) or "{}")
79
+ if not isinstance(hook_input, dict) or hook_input.get("hook_event_name") not in ("SessionStart", "UserPromptSubmit"):
80
+ return
81
+ if _can_skip(platform):
82
+ return
83
+ target = os.environ.get("CLAUDE_PROJECT_DIR" if platform == "claude" else "CODEX_PROJECT_DIR") or hook_input.get("cwd")
84
+ if not isinstance(target, str) or not Path(target).is_dir():
85
+ return
86
+ command = shutil.which("flower-trellis")
87
+ if not command:
88
+ return
89
+ # 只转交固定事件类型、平台和本地定位参数;Node 独占身份创建及队列写入。
90
+ subprocess.run([command, "telemetry", "record-activity", platform, "--target", target],
91
+ cwd=target, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
92
+ stderr=subprocess.DEVNULL, timeout=3, check=False)
93
+ except (OSError, ValueError, TypeError, subprocess.SubprocessError):
94
+ return
95
+
96
+
97
+ if __name__ == "__main__":
98
+ main()
@@ -23,7 +23,7 @@ import {
23
23
  hashDirectoryIfExists,
24
24
  hashFileIfExists,
25
25
  } from "../../plugin/install/content-hash.js";
26
- import { FLOWER_UPDATE_HOOK, FLOWER_UPDATE_HOOK_REL } from "../../lib/flower-assets.js";
26
+ import { FLOWER_UPDATE_HOOK, FLOWER_UPDATE_HOOK_REL, FLOWER_SESSION_HOOK, FLOWER_SESSION_HOOK_REL, FLOWER_TELEMETRY_HOOK, FLOWER_TELEMETRY_HOOK_REL } from "../../lib/flower-assets.js";
27
27
  import { PKG_ROOT } from "../../lib/paths.js";
28
28
  import { injectWorkflow } from "../../lib/workflow-inject.js";
29
29
  import { applyCodexTweaks } from "../../lib/codex-tweaks.js";
@@ -746,6 +746,20 @@ export function projectSkillGardenContent(options) {
746
746
  `flower:asset:${FLOWER_UPDATE_HOOK}`,
747
747
  );
748
748
  installed.add(`script:${FLOWER_UPDATE_HOOK}`);
749
+ if (pluginPackage.skillGarden.variant === "0.6") {
750
+ addFile(
751
+ FLOWER_SESSION_HOOK_REL,
752
+ fs.readFileSync(path.join(PKG_ROOT, "src", "assets", FLOWER_SESSION_HOOK)),
753
+ `flower:asset:${FLOWER_SESSION_HOOK}`,
754
+ );
755
+ installed.add(`script:${FLOWER_SESSION_HOOK}`);
756
+ addFile(
757
+ FLOWER_TELEMETRY_HOOK_REL,
758
+ fs.readFileSync(path.join(PKG_ROOT, "src", "assets", FLOWER_TELEMETRY_HOOK)),
759
+ `flower:asset:${FLOWER_TELEMETRY_HOOK}`,
760
+ );
761
+ installed.add(`script:${FLOWER_TELEMETRY_HOOK}`);
762
+ }
749
763
  const commonSync = describeInstalledCommonSkillSync(projectRoot);
750
764
  for (const common of commonSync.refreshes) {
751
765
  addTree(
package/src/cli.js CHANGED
@@ -88,6 +88,7 @@ function printHelp() {
88
88
  管理分支本地化 Git worktree
89
89
  flower-trellis uninstall [-y | --dry-run] 卸载 + 清理强化残留
90
90
  flower-trellis <其它命令> [...] 透传给 trellis(面向未来)
91
+ flower-trellis <命令> --help 查看命令级用法与下一步引导
91
92
  flower-trellis -v 打印版本
92
93
 
93
94
  flower 自有 flag:
@@ -107,6 +108,7 @@ flower 自有 flag:
107
108
  update-check skip|reset 跳过当前提示或清空提示节流状态
108
109
 
109
110
  Worktree:
111
+ worktree --help 查看子命令、推荐流程和已有分支引导
110
112
  worktree create --target <dir> --branch <name> --task-title <title> --task-slug <slug>
111
113
  只读预检;默认 base 为当前来源分支
112
114
  worktree create ... --yes --plan-fingerprint <sha256>
@@ -137,7 +139,7 @@ async function main() {
137
139
 
138
140
  const argv = process.argv.slice(2);
139
141
 
140
- // 顶层 -v / -h 仅在作为首个参数时拦截;子命令的 --help/--version 透传给 trellis
142
+ // 顶层 -v / -h 仅在作为首个参数时拦截;Flower 子命令各自处理帮助,其它命令透传 Trellis。
141
143
  if (argv[0] === "-v" || argv[0] === "--version") {
142
144
  printVersion(process.cwd());
143
145
  return;
@@ -1,3 +1,4 @@
1
+ import { observeTelemetryOperation, beginTelemetryOperation, completeTelemetryOperation } from "../lib/telemetry-operation.js";
1
2
  import fs from "node:fs";
2
3
  import { runTrellisPty } from "../lib/trellis-runner.js";
3
4
  import { plugin } from "./plugin.js";
@@ -9,6 +10,26 @@ import { ProjectStore } from "../plugin/state/project-store.js";
9
10
  import { SKILL_GARDEN_PLUGIN_ID } from "../builtin-plugins/skill-garden/provider.js";
10
11
  import { showCommandCompletion } from "../lib/command-completion.js";
11
12
  import { reportTelemetry } from "../lib/telemetry.js";
13
+ import { hasHelpFlag } from "../lib/cli-args.js";
14
+
15
+ /** 打印 init 命令帮助。 */
16
+ function printInitHelp() {
17
+ console.log(`flower-trellis init — 初始化 Trellis 并安装 Skill Garden 强化
18
+
19
+ 用法:
20
+ flower-trellis init [trellis flags] [flower flags]
21
+
22
+ 常用选项:
23
+ -y, --yes 非交互执行,默认启用 Codex + Claude Code
24
+ --target <dir> 目标目录,必须已经存在
25
+ --no-enhance 只运行 Trellis init
26
+ --enhance-only 只安装强化包
27
+ --skills <a,b,...> 仅安装指定技能
28
+ --variant <old|0.5|0.6> 指定强化包变体
29
+ --no-update-check 本次跳过版本检查
30
+
31
+ 需要选择模板、monorepo 或平台时,移除 -y 进入交互流程。`);
32
+ }
12
33
 
13
34
  /**
14
35
  * flower-trellis init:驱动 `trellis init`,随后叠加强化包。
@@ -23,6 +44,18 @@ import { reportTelemetry } from "../lib/telemetry.js";
23
44
  * @returns {Promise<void>} Trellis 初始化、强化叠加与完成交互结束后返回
24
45
  */
25
46
  export async function init(ctx) {
47
+ return observeTelemetryOperation(ctx, "init", executeInit);
48
+ }
49
+
50
+ /** 执行已建立外部操作上下文的命令。
51
+ * @param {object} ctx 命令上下文
52
+ * @returns {Promise<void>} 完成
53
+ */
54
+ async function executeInit(ctx) {
55
+ if (hasHelpFlag(ctx.passthrough)) {
56
+ printInitHelp();
57
+ return;
58
+ }
26
59
  const { target } = ctx;
27
60
  if (!fs.existsSync(target) || !fs.statSync(target).isDirectory()) {
28
61
  throw new Error(`目标目录不存在:${target}`);
@@ -62,13 +95,14 @@ export async function init(ctx) {
62
95
  }
63
96
  }
64
97
 
98
+ if (!ctx.passthrough.includes("--dry-run")) beginTelemetryOperation(ctx);
65
99
  if (!ctx.enhanceOnly) {
66
100
  // 伪终端运行:保留 trellis 的模板 / monorepo 交互 + 过滤其重复 banner
67
101
  const code = await runTrellisPty(["init", ...passthrough], target, {
68
102
  stripBanner: true,
69
103
  });
70
104
  if (code !== 0) {
71
- throw new Error(`trellis init 失败(退出码 ${code}),已中止,未叠加强化包`);
105
+ throw Object.assign(new Error(`trellis init 失败(退出码 ${code}),已中止,未叠加强化包`), { code: code === 130 ? "FLOWER_OPERATION_CANCELLED" : "FLOWER_UPSTREAM_FAILED" });
72
106
  }
73
107
  } else {
74
108
  console.log("· --enhance-only:跳过 trellis init,仅叠加强化包");
@@ -89,6 +123,7 @@ export async function init(ctx) {
89
123
  console.log("· --no-enhance:跳过强化包叠加");
90
124
  }
91
125
 
126
+ completeTelemetryOperation(ctx, "init");
92
127
  const telemetryPromise = reportTelemetry(target, "init_completed", { force: true });
93
128
  await showCommandCompletion("init", target, { passthrough });
94
129
  await telemetryPromise;
@@ -19,6 +19,22 @@ import { SourceRegistry } from "../plugin/sources/source-registry.js";
19
19
  import { UserSourceStore } from "../plugin/sources/user-source-store.js";
20
20
  import { listContentSkillChoices } from "../plugin/content-selection.js";
21
21
  import { compareUtf8 } from "../plugin/stable-order.js";
22
+ import { classifyLockReachability } from "../plugin/lock-reachability.js";
23
+
24
+ /**
25
+ * 计算远程准备允许读取的既有 lock 节点。
26
+ *
27
+ * 兼容直接调用测试时没有传入 `pluginsFile` 的旧签名,此时才回退到 lock roots;真实 CLI
28
+ * 始终传入当前声明,避免半清理项目的陈旧 roots 触发外部来源访问。
29
+ *
30
+ * @param {import("../plugin/contracts.js").ProjectPluginsFile|undefined} pluginsFile 当前声明
31
+ * @param {import("../plugin/contracts.js").PluginLock|null} lock 既有 lock
32
+ * @returns {Set<string>} 可准备的既有 lock ID
33
+ */
34
+ function reachableRemoteLockIds(pluginsFile, lock) {
35
+ const declarations = pluginsFile?.plugins || (lock?.roots || []).map((id) => ({ id }));
36
+ return classifyLockReachability(declarations, lock).reachableIds;
37
+ }
22
38
 
23
39
  /**
24
40
  * 输出 source、auth 与 search 管理命令结果。
@@ -537,22 +553,35 @@ export async function runPluginManagementCommand(parsed, ctx, options, output) {
537
553
  /**
538
554
  * 为生命周期命令登记所需的远程 Source Provider。
539
555
  *
540
- * @param {{parsed:object,projectRoot:string,options:object,registry:object,lock:object|null,preserveIds?:string[]}} context 远程准备上下文
556
+ * @param {{parsed:object,projectRoot:string,options:object,registry:object,lock:object|null,pluginsFile?:import("../plugin/contracts.js").ProjectPluginsFile,preserveIds?:string[]}} context 远程准备上下文
541
557
  * @returns {Promise<void>} 登记完成
542
558
  */
543
- export async function registerRemotePluginSources({ parsed, projectRoot, options, registry, lock, preserveIds = [] }) {
559
+ export async function registerRemotePluginSources({
560
+ parsed,
561
+ projectRoot,
562
+ options,
563
+ registry,
564
+ lock,
565
+ pluginsFile,
566
+ preserveIds = [],
567
+ }) {
544
568
  const preserved = new Set(preserveIds);
569
+ const reachableLockedIds = reachableRemoteLockIds(pluginsFile, lock);
545
570
  const remoteIds = new Set(["add", "update", "remove", "verify", "replay"].includes(parsed.command)
546
571
  ? (lock?.plugins || [])
547
- .filter(({ id, source }) => !preserved.has(id) && ["gitlab", "github"].includes(source.type))
572
+ .filter(({ id, source }) => (
573
+ reachableLockedIds.has(id) &&
574
+ !preserved.has(id) &&
575
+ ["gitlab", "github"].includes(source.type)
576
+ ))
548
577
  .map(({ source }) => source.id)
549
578
  : []);
579
+ const declaredSourceId = parsed.pluginId?.includes("/")
580
+ ? parseCanonicalPluginId(parsed.pluginId).sourceId
581
+ : null;
550
582
  let sourceStore = null;
551
583
  let configuredSources = null;
552
584
  if (parsed.command === "add") {
553
- const declaredSourceId = parsed.pluginId?.includes("/")
554
- ? parseCanonicalPluginId(parsed.pluginId).sourceId
555
- : null;
556
585
  const simpleSourceOption = parsed.source && !parsed.source.includes("/") && !parsed.source.includes(".")
557
586
  ? parsed.source
558
587
  : null;
@@ -587,7 +616,13 @@ export async function registerRemotePluginSources({ parsed, projectRoot, options
587
616
  remoteIds.add(simpleSourceOption);
588
617
  }
589
618
  }
590
- if (parsed.command === "update" && parsed.pluginId?.includes("/")) {
619
+ const shouldCollectDependencySources = parsed.command === "update" || (
620
+ parsed.command === "add" &&
621
+ declaredSourceId &&
622
+ registry.has(declaredSourceId) &&
623
+ registry.get(declaredSourceId).type === "builtin"
624
+ );
625
+ if (shouldCollectDependencySources && parsed.pluginId?.includes("/")) {
591
626
  for (const sourceId of collectUnregisteredDependencySources(
592
627
  registry,
593
628
  [parsed.pluginId],
@@ -640,24 +675,34 @@ export async function registerRemotePluginSources({ parsed, projectRoot, options
640
675
  /**
641
676
  * 准备远程 Plugin 候选及固定 lock 缓存。
642
677
  *
643
- * @param {{parsed:object,canonicalId:string|null,registry:object,lock:object|null,preserveIds?:string[]}} context 候选准备上下文
678
+ * @param {{parsed:object,canonicalId:string|null,registry:object,lock:object|null,pluginsFile?:import("../plugin/contracts.js").ProjectPluginsFile,preserveIds?:string[]}} context 候选准备上下文
644
679
  * @returns {Promise<void>} 准备完成
645
680
  */
646
- export async function prepareRemotePluginCandidates({ parsed, canonicalId, registry, lock, preserveIds = [] }) {
681
+ export async function prepareRemotePluginCandidates({
682
+ parsed,
683
+ canonicalId,
684
+ registry,
685
+ lock,
686
+ pluginsFile,
687
+ preserveIds = [],
688
+ }) {
689
+ const reachableLockedIds = reachableRemoteLockIds(pluginsFile, lock);
647
690
  if (parsed.command === "add" && registry.has(parseCanonicalPluginId(canonicalId).sourceId)) {
648
- await prepareRemoteLock(registry, lock, preserveIds);
691
+ await prepareRemoteLock(registry, lock, preserveIds, reachableLockedIds);
649
692
  await prepareRemoteClosure(registry, [canonicalId], preserveIds);
650
693
  } else if (parsed.command === "update") {
651
694
  // 更新单一 Plugin 时,解析器仍会 lock-first 重放完整图;先恢复全部远程固定包,
652
695
  // 再只为本轮显式更新目标加载新候选,避免顺带升级其它外部 Plugin。
653
- await prepareRemoteLock(registry, lock, preserveIds);
696
+ await prepareRemoteLock(registry, lock, preserveIds, reachableLockedIds);
654
697
  const updateIds = canonicalId
655
698
  ? [canonicalId]
656
- : (lock?.plugins || []).filter(({ source }) => ["gitlab", "github"].includes(source.type)).map(({ id }) => id);
699
+ : (lock?.plugins || [])
700
+ .filter(({ id, source }) => reachableLockedIds.has(id) && ["gitlab", "github"].includes(source.type))
701
+ .map(({ id }) => id);
657
702
  await prepareRemoteClosure(registry, updateIds, preserveIds);
658
703
  assertExternalVersionsNotReused(registry, lock, updateIds);
659
704
  } else if (["remove", "verify", "replay"].includes(parsed.command)) {
660
- await prepareRemoteLock(registry, lock, preserveIds);
705
+ await prepareRemoteLock(registry, lock, preserveIds, reachableLockedIds);
661
706
  }
662
707
  }
663
708
 
@@ -725,12 +770,14 @@ async function prepareRemoteClosure(registry, initialIds, preserveIds = []) {
725
770
  * @param {object} registry Provider 注册表
726
771
  * @param {import("../plugin/contracts.js").PluginLock|null} lock 当前 lock
727
772
  * @param {string[]} [preserveIds] 不读取来源的冻结 Plugin ID
773
+ * @param {Set<string>|null} [reachableIds] 当前声明可达的既有 lock ID
728
774
  * @returns {Promise<void>} 恢复完成
729
775
  */
730
- async function prepareRemoteLock(registry, lock, preserveIds = []) {
776
+ async function prepareRemoteLock(registry, lock, preserveIds = [], reachableIds = null) {
731
777
  const preserved = new Set(preserveIds);
732
778
  for (const plugin of lock?.plugins || []) {
733
779
  if (
780
+ (reachableIds && !reachableIds.has(plugin.id)) ||
734
781
  preserved.has(plugin.id) ||
735
782
  !["gitlab", "github"].includes(plugin.source.type) ||
736
783
  !registry.has(plugin.source.id)