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.
- package/README.md +20 -1
- package/enhancements/0.6/overrides/bundles/control-plane-integrity.json +2 -0
- package/enhancements/0.6/overrides/bundles/intent-routing.json +2 -1
- package/enhancements/0.6/overrides/bundles/task-reference-contract.json +24 -0
- package/enhancements/0.6/overrides/conflicts.json +37 -4
- package/enhancements/0.6/overrides/patches/scripts/task-current-query-contract/patch.json +19 -0
- package/enhancements/0.6/overrides/patches/scripts/task-current-query-contract/task-current-content.py +39 -0
- package/enhancements/0.6/overrides/patches/scripts/task-current-query-contract/task-current-selector.py +38 -0
- package/enhancements/0.6/overrides/patches/scripts/task-reference-resolution/patch.json +20 -0
- package/enhancements/0.6/overrides/patches/scripts/task-reference-resolution/task-reference-content.py +64 -0
- package/enhancements/0.6/overrides/patches/scripts/task-reference-resolution/task-reference-selector.py +2 -0
- package/enhancements/0.6/overrides/patches/skills/trellis-brainstorm/planning-handoff/content.md +2 -4
- package/enhancements/0.6/overrides/patches/skills/trellis-brainstorm/planning-handoff/planning-contract-content.md +1 -1
- package/enhancements/0.6/overrides/patches/skills/trellis-brainstorm/planning-handoff/readiness-content.md +2 -2
- package/enhancements/0.6/overrides/patches/skills/trellis-brainstorm/planning-handoff/review-content.md +2 -2
- package/enhancements/0.6/scripts/decision_log.py +29 -24
- package/enhancements/0.6/scripts/task_progress.py +54 -13
- package/enhancements/0.6/scripts/untracked_flow.py +13 -5
- package/enhancements/MANIFEST.json +9 -2
- package/package.json +4 -4
- package/src/assets/flower_session_start.py +195 -0
- package/src/assets/flower_telemetry_hook.py +98 -0
- package/src/builtin-plugins/skill-garden/content-adapter.js +15 -1
- package/src/cli.js +3 -1
- package/src/commands/init.js +36 -1
- package/src/commands/plugin-remote.js +61 -14
- package/src/commands/plugin.js +14 -1
- package/src/commands/self-check.js +20 -0
- package/src/commands/self-update.js +38 -3
- package/src/commands/telemetry.js +30 -1
- package/src/commands/uninstall.js +20 -0
- package/src/commands/update-check.js +19 -0
- package/src/commands/update.js +52 -9
- package/src/commands/worktree.js +114 -0
- package/src/lib/cli-args.js +10 -0
- package/src/lib/developer.js +3 -2
- package/src/lib/flower-assets.js +21 -6
- package/src/lib/platform-patch-adapters.js +46 -8
- package/src/lib/telemetry-context.js +275 -0
- package/src/lib/telemetry-files.js +144 -0
- package/src/lib/telemetry-operation.js +84 -0
- package/src/lib/telemetry-queue.js +310 -0
- package/src/lib/telemetry-sender.js +7 -0
- package/src/lib/telemetry.js +12 -361
- package/src/patches/bundles/flower-platform-integration.json +4 -1
- package/src/patches/platforms/claude/session-start-hooks/patch.json +28 -0
- package/src/patches/platforms/claude/telemetry-hooks/patch.json +59 -0
- package/src/patches/platforms/codex/session-start-hooks/patch.json +3 -2
- package/src/patches/platforms/codex/telemetry-hooks/patch.json +59 -0
- package/src/plugin/application-service.js +5 -22
- package/src/plugin/contracts.js +1 -0
- package/src/plugin/lock-reachability.js +37 -0
- package/src/plugin/schemas/project-files.js +1 -0
- package/src/plugin/sources/gitlab-provider.js +50 -9
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"syncedAt": "2026-
|
|
2
|
+
"syncedAt": "2026-09-07T02:30:42.961Z",
|
|
3
3
|
"syncedFrom": "vendor/skill-garden",
|
|
4
|
-
"sourceCommit": "
|
|
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.
|
|
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
|
|
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.
|
|
73
|
+
"version": "0.6.6",
|
|
74
74
|
"source": "CHANGELOG.md",
|
|
75
|
-
"body": "### 🐛 修复 Bug Fixes\n\n* **
|
|
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
|
|
142
|
+
// 顶层 -v / -h 仅在作为首个参数时拦截;Flower 子命令各自处理帮助,其它命令透传 Trellis。
|
|
141
143
|
if (argv[0] === "-v" || argv[0] === "--version") {
|
|
142
144
|
printVersion(process.cwd());
|
|
143
145
|
return;
|
package/src/commands/init.js
CHANGED
|
@@ -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({
|
|
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 }) =>
|
|
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
|
-
|
|
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({
|
|
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 || [])
|
|
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)
|