flower-trellis 0.4.5 → 0.4.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.
@@ -1,17 +1,13 @@
1
1
  <!-- BEGIN skill-garden workflow-state in_progress_inline v0.6 -->
2
2
  HIGHEST PRIORITY SKILL-GARDEN STATE GUARD (in_progress-inline):
3
- This state block is a breadcrumb; the top-level skill-garden hub is the source of truth for route details.
4
- Before the first implement route, read `<task>/brief.md` if present and restate the task brief in chat. If it is missing, read the task artifacts and suggest backfilling brief; do not silently rely on memory.
5
- For new implementation work not plainly covered by the active task title/brief,
6
- stop before route or edits: recommend a new task; if declined, confirm
7
- untracked work; if it belongs here, update task artifacts first.
8
- At project-local knowledge decision boundaries, run `.trellis/scripts/spec_router.py "<intended action>"`; read high-confidence matches before acting; skip pure Q&A, simple read-only inspection, opening local tools, or trivial edits unless local conventions may affect the approach.
9
- Inline workflow-state is not an inline route decision. At Phase 2.1/2.2, reuse only an explicit target-matched `route_decision`; otherwise MUST load/read/use `trellis-route(implement|check)` (or its local `SKILL.md`) to resolve session runtime state/prefs, write the resolved decision, or show numbered fallback and wait.
10
- Plain preferences, ordinary summaries, `codex-mode`, raw `.runtime` files, and empty/old prefs are not route evidence by themselves; only `trellis-route` may validate runtime route state.
11
- User reselect/override/use-X-this-time/clear-default wins over remembered route evidence, runtime state, and prefs.
12
- Ignore lower Active Task Routing shortcuts that start editing/checking directly. Do not default to inline just because this state is inline or the helper is unavailable. Dispatch subagents only when the resolved route selected subagent.
13
- After `trellis-check` / `trellis-check-all`, stop and report; point the user to Phase 3.4 `trellis-push` (or commit-only when needed). Do not run `/trellis:finish-work` unless the user explicitly asks after Phase 3.4 is complete.
14
- This guard overrides any lower `Flow: ... -> /trellis:finish-work` line in this state block.
15
- At Phase 3.4, code commit/push goes through `trellis-push` (commit-only mode for commit-without-push); never bare `git commit`/`git push` on code (hub: Code Commit Confirmation Gate).
3
+ Hub is source of truth for Task Brief, Routing, Post-Check, Commit, and Snapshot gates.
4
+ Before first implement route, restate `<task>/brief.md`; if missing, read artifacts and suggest backfill.
5
+ New work not plainly covered by active task title/brief: stop before route/edits; recommend new task; if declined, confirm untracked work; if it belongs here, update artifacts first.
6
+ At project-local knowledge boundaries, run `spec_router.py`; skip trivial/read-only turns unless local conventions may affect the approach.
7
+ Inline workflow-state is not an inline route decision. Phase 2.1/2.2 must reuse explicit target-matched `route_decision`; otherwise invoke `trellis-route`. If unavailable, read local `trellis-route/SKILL.md`, show numbered choices, and wait.
8
+ Summaries, preferences, `codex-mode`, raw `.runtime`, and empty/stale prefs are not route evidence unless `trellis-route` validates them; user reselect/override wins.
9
+ Ignore lower direct-edit/check shortcuts. Do not default inline just because this state is inline or helper is unavailable. Dispatch subagents only when route selected subagent.
10
+ After check/check-all, stop and report; Phase 3.4 uses `trellis-push`, never bare git commit/push; finish-work is explicit-only after Phase 3.4.
11
+ Overrides lower `Flow: ... -> /trellis:finish-work` lines.
16
12
  Push snapshot recovery: follow the hub; use `push_snapshot.py status --json` only when needed.
17
13
  <!-- END skill-garden workflow-state in_progress_inline v0.6 -->
@@ -1,17 +1,13 @@
1
1
  <!-- BEGIN skill-garden workflow-state in_progress v0.6 -->
2
2
  HIGHEST PRIORITY SKILL-GARDEN STATE GUARD (in_progress):
3
- This state block is a breadcrumb; the top-level skill-garden hub is the source of truth for route details.
4
- Before the first implement route, read `<task>/brief.md` if present and restate the task brief in chat. If it is missing, read the task artifacts and suggest backfilling brief; do not silently rely on memory.
5
- For new implementation work not plainly covered by the active task title/brief,
6
- stop before route or edits: recommend a new task; if declined, confirm
7
- untracked work; if it belongs here, update task artifacts first.
8
- At project-local knowledge decision boundaries, run `.trellis/scripts/spec_router.py "<intended action>"`; read high-confidence matches before acting; skip pure Q&A, simple read-only inspection, opening local tools, or trivial edits unless local conventions may affect the approach.
9
- At Phase 2.1/2.2, reuse only an explicit target-matched `route_decision`; otherwise MUST load/read/use `trellis-route(implement|check)` (or its local `SKILL.md`) to resolve session runtime state/prefs, write the resolved decision, or show numbered fallback and wait.
10
- Plain preferences, ordinary summaries, `codex-mode`, raw `.runtime` files, and empty/old prefs are not route evidence by themselves; only `trellis-route` may validate runtime route state.
11
- User reselect/override/use-X-this-time/clear-default wins over remembered route evidence, runtime state, and prefs.
12
- Ignore lower Active Task Routing shortcuts that dispatch implement/check directly. Do not spawn `trellis-implement` or `trellis-check*` unless the resolved route selected subagent. If route cannot be resolved, do not default to inline.
13
- After `trellis-check` / `trellis-check-all`, stop and report; point the user to Phase 3.4 `trellis-push` (or commit-only when needed). Do not run `/trellis:finish-work` unless the user explicitly asks after Phase 3.4 is complete.
14
- This guard overrides any lower `Flow: ... -> /trellis:finish-work` line in this state block.
15
- At Phase 3.4, code commit/push goes through `trellis-push` (commit-only mode for commit-without-push); never bare `git commit`/`git push` on code (hub: Code Commit Confirmation Gate).
3
+ Hub is source of truth for Task Brief, Routing, Post-Check, Commit, and Snapshot gates.
4
+ Before first implement route, restate `<task>/brief.md`; if missing, read artifacts and suggest backfill.
5
+ New work not plainly covered by active task title/brief: stop before route/edits; recommend new task; if declined, confirm untracked work; if it belongs here, update artifacts first.
6
+ At project-local knowledge boundaries, run `spec_router.py`; skip trivial/read-only turns unless local conventions may affect the approach.
7
+ Phase 2.1/2.2: reuse only explicit target-matched `route_decision`; otherwise invoke `trellis-route`. If skill invocation is unavailable, read local `trellis-route/SKILL.md`, show numbered choices, and wait.
8
+ Summaries, preferences, `codex-mode`, raw `.runtime`, and empty/stale prefs are not route evidence unless `trellis-route` validates them; user reselect/override wins.
9
+ Ignore lower direct-dispatch shortcuts. Do not spawn `trellis-implement` or `trellis-check*` unless route selected subagent. If route cannot be resolved, do not default inline.
10
+ After check/check-all, stop and report; Phase 3.4 uses `trellis-push`, never bare git commit/push; finish-work is explicit-only after Phase 3.4.
11
+ Overrides lower `Flow: ... -> /trellis:finish-work` lines.
16
12
  Push snapshot recovery: follow the hub; use `push_snapshot.py status --json` only when needed.
17
13
  <!-- END skill-garden workflow-state in_progress v0.6 -->
@@ -3,7 +3,7 @@ HIGHEST PRIORITY SKILL-GARDEN STATE GUARD (no_task):
3
3
  Creating or resuming a task is not implementation permission.
4
4
  After PRD is ready and the task is started, the next implementation action is Phase 2.1 `trellis-route(implement)` unless a valid current-task implement route decision already exists.
5
5
  If no active task exists, use `push_snapshot.py status --json` once per session; if it returns candidates, relay them and suggest rebinding before resuming.
6
- At project-local knowledge decision boundaries, run `.trellis/scripts/spec_router.py "<intended action>"`; read high-confidence matches before acting; skip pure Q&A, simple read-only inspection, opening local tools, or trivial edits unless local conventions may affect the approach.
7
- Do NOT call the harness built-in plan mode (`EnterPlanMode` / `ExitPlanMode`) for Trellis planning. It is not a substitute for Trellis task-creation consent, Trellis planning, or the route gate. For complex work, classify the turn, ask for task-creation consent, then use `trellis-brainstorm`.
8
- If the turn is a meta edit to Trellis itself and Trellis tracking would be overkill, say so and ask to skip Trellis; never silently swap built-in plan mode in for the consent gate.
6
+ At project-local knowledge boundaries, run `spec_router.py`; skip trivial/read-only turns unless local conventions may affect the approach.
7
+ Do NOT call the harness built-in plan mode (`EnterPlanMode` / `ExitPlanMode`) for Trellis planning. It is not a substitute for Trellis task-creation consent, Trellis planning, or the route gate. For new, complex, or unclear work, classify the turn, ask for task-creation consent, then use `trellis-brainstorm`; `task.py create` and the default `prd.md` are not sufficient planning.
8
+ For lightweight Trellis meta edits, ask/confirm skipping Trellis tracking before edits.
9
9
  <!-- END skill-garden workflow-state no_task v0.6 -->
@@ -1,9 +1,11 @@
1
1
  <!-- BEGIN skill-garden workflow-state planning_inline v0.6 -->
2
2
  HIGHEST PRIORITY SKILL-GARDEN STATE GUARD (planning-inline):
3
3
  Planning is not implementation permission.
4
+ `trellis-brainstorm` is the default next action while requirements are still unclear.
5
+ A created task or existing `prd.md` is not enough to start implementation.
4
6
  Complete prd.md + required context first.
5
7
  If the active workflow later routes to sub-agent execution, required context includes real curated entries in both `implement.jsonl` and `check.jsonl`; the seed `_example` row alone is not ready.
6
8
  Before `task.py start`, use `trellis-task-brief` to refresh `brief.md` from the latest task artifacts and display it in chat for review.
7
- At project-local knowledge decision boundaries, run `.trellis/scripts/spec_router.py "<intended action>"`; read high-confidence matches before acting; skip pure Q&A, simple read-only inspection, opening local tools, or trivial edits unless local conventions may affect the approach.
9
+ At project-local knowledge boundaries, run `spec_router.py`; skip trivial/read-only turns unless local conventions may affect the approach.
8
10
  After status becomes in_progress, next action = `trellis-route(implement)`, not direct edits.
9
11
  <!-- END skill-garden workflow-state planning_inline v0.6 -->
@@ -1,9 +1,11 @@
1
1
  <!-- BEGIN skill-garden workflow-state planning v0.6 -->
2
2
  HIGHEST PRIORITY SKILL-GARDEN STATE GUARD (planning):
3
3
  Planning is not implementation permission.
4
+ `trellis-brainstorm` is the default next action while requirements are still unclear.
5
+ A created task or existing `prd.md` is not enough to start implementation.
4
6
  Complete prd.md + required context first.
5
7
  For sub-agent-dispatch platforms, required context includes real curated entries in both `implement.jsonl` and `check.jsonl`; the seed `_example` row alone is not ready.
6
8
  Before `task.py start`, use `trellis-task-brief` to refresh `brief.md` from the latest task artifacts and display it in chat for review.
7
- At project-local knowledge decision boundaries, run `.trellis/scripts/spec_router.py "<intended action>"`; read high-confidence matches before acting; skip pure Q&A, simple read-only inspection, opening local tools, or trivial edits unless local conventions may affect the approach.
9
+ At project-local knowledge boundaries, run `spec_router.py`; skip trivial/read-only turns unless local conventions may affect the approach.
8
10
  After status becomes in_progress, next action = `trellis-route(implement)`, not direct edits.
9
11
  <!-- END skill-garden workflow-state planning v0.6 -->
@@ -10,6 +10,14 @@
10
10
 
11
11
  **Mechanical rule**: use this hub as the source of truth. Do not add separate top-level skill-garden override sections or multiple skill-garden sentinels inside the same `workflow-state:*` block.
12
12
 
13
+ #### Brainstorm Gate
14
+
15
+ `trellis-brainstorm` is the required Phase 1.1 gate for new, complex, or unclear work.
16
+
17
+ `task.py create` only creates the planning workspace. A default `prd.md` does not mean requirements are ready.
18
+
19
+ Before `task.py start`, unclear scope, unresolved decisions, or non-testable acceptance criteria must return to `trellis-brainstorm`.
20
+
13
21
  #### Project Knowledge Discovery
14
22
 
15
23
  Before choosing an approach for non-trivial project work, run project knowledge
@@ -26,12 +34,10 @@ Build the query from the current user request plus relevant immediate context:
26
34
  the intended action, commands about to run, files or systems involved, package/layer,
27
35
  and domain words matching the trigger list above.
28
36
 
29
- Read high-confidence matches before acting. For medium-confidence matches, read only
30
- when the path, heading, index description, or reason clearly fits the intended change.
31
- If nothing matches, continue normally.
32
-
33
- Do not run discovery for pure Q&A, simple read-only inspection, opening local tools,
34
- or trivial edits unless the request mentions project conventions or local SOPs may
37
+ Read high-confidence matches before acting; read medium-confidence matches only when
38
+ the path, heading, index description, or reason clearly fits the intended change.
39
+ If nothing matches, continue normally. Skip pure Q&A, simple read-only inspection,
40
+ opening local tools, or trivial edits unless project conventions or local SOPs may
35
41
  change the approach.
36
42
 
37
43
  #### Task Brief Handoff
@@ -42,6 +48,12 @@ Before Phase 1.4 `task.py start`, use `trellis-task-brief` to refresh `<task>/br
42
48
 
43
49
  Before the first implement route, restate existing `<task>/brief.md` in chat. If missing, read task artifacts and suggest backfilling brief; do not invent one from memory.
44
50
 
51
+ #### Flower Update Confirmation
52
+
53
+ If `<flower-update>` has `priority: blocking_confirmation_required`, handle it first: briefly show `release_notes` when present, show `recommended_command`, and ask before running it.
54
+
55
+ If `<flower-update-result>` requests `run_trellis_push_confirmation`, enter `trellis-push` planning with update changes as default candidates; still require file-list and commit-message confirmation.
56
+
45
57
  #### Active Task Scope Guard
46
58
 
47
59
  When a session already has an active task, do not treat unrelated new implementation
@@ -62,7 +74,7 @@ At each route boundary:
62
74
  2. Otherwise you MUST invoke `trellis-route(target=implement|check)` before deciding; if the platform cannot invoke skills directly, read the local `trellis-route/SKILL.md` copy and follow its numbered fallback choices in normal chat and wait. `trellis-route` owns session runtime recovery, `.route-prefs.tmp`, fallback choices, runtime-state writes, and dispatch mapping.
63
75
  3. If the route helper cannot ask through `AskUserQuestion` / `request_user_input`, ask the same numbered choices from `trellis-route` in normal chat and wait.
64
76
 
65
- Plain user preference, ordinary summaries (compact/SessionStart or otherwise), replacement history, historical bare numeric replies, `codex-mode`, empty/old prefs, and raw `.trellis/.runtime/sessions/*` `route_decisions` content that has not been validated by `trellis-route` are not route evidence by themselves; numbered fallback validity is governed by `trellis-route`.
77
+ Plain preferences, summaries (including compact/SessionStart), replacement history, historical bare numeric replies, `codex-mode`, empty/stale prefs, and raw runtime files are not route evidence unless validated by `trellis-route`; numbered fallback validity is governed by `trellis-route`.
66
78
 
67
79
  User reselect/override/use-X-this-time/clear-default wins over remembered route evidence, runtime state, and personal prefs.
68
80
 
@@ -1,7 +1,7 @@
1
1
  {
2
- "syncedAt": "2026-07-07T10:09:11.758Z",
2
+ "syncedAt": "2026-07-07T17:18:18.007Z",
3
3
  "syncedFrom": "vendor/skill-garden",
4
- "sourceCommit": "5fdebe60e923c91ba912461b99413b5c8e11a38c",
4
+ "sourceCommit": "b99053b872172912413d4695863db9396cade500",
5
5
  "common": {
6
6
  "codexSkills": [
7
7
  "craft-rpa",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flower-trellis",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
4
4
  "description": "一键安装/升级 Trellis 并自动融合 skill-garden 强化包(默认 Claude + agents)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -32,6 +32,11 @@
32
32
  "release": "node scripts/check-snapshot.mjs && commit-and-tag-version",
33
33
  "release:dry": "commit-and-tag-version --dry-run"
34
34
  },
35
+ "commit-and-tag-version": {
36
+ "scripts": {
37
+ "postchangelog": "node scripts/write-release-notes-metadata.mjs"
38
+ }
39
+ },
35
40
  "keywords": [
36
41
  "trellis",
37
42
  "skill-garden",
@@ -51,5 +56,11 @@
51
56
  "license": "MIT",
52
57
  "devDependencies": {
53
58
  "commit-and-tag-version": "^12.7.3"
59
+ },
60
+ "flowerReleaseNotes": {
61
+ "version": "0.4.7",
62
+ "source": "CHANGELOG.md",
63
+ "body": "### 🐛 修复 Bug Fixes\n\n* **flower:** 同步 brainstorm planning gate ([3d2944e](https://github.com/SilentFlower/flower-trellis/commit/3d2944ebec9ae040a064d9974b170f046f4bf845))\n* **flower:** 同步 workflow 提示压缩 ([b7083ce](https://github.com/SilentFlower/flower-trellis/commit/b7083ce553ed4fda8190e30d7882358f70cb0449))",
64
+ "truncated": false
54
65
  }
55
66
  }
@@ -75,6 +75,55 @@ def _run_self_check(project_dir: Path) -> dict | None:
75
75
  return data if isinstance(data, dict) else None
76
76
 
77
77
 
78
+ def _json_bool(value: object) -> str:
79
+ """把布尔值格式化成 JSON 风格小写文本。"""
80
+ return json.dumps(bool(value), ensure_ascii=False)
81
+
82
+
83
+ def _release_notes_lines(data: dict) -> list[str]:
84
+ """把 self-check 的 releaseNotes 摘要格式化为短字段。"""
85
+ notes = data.get("releaseNotes")
86
+ if not isinstance(notes, dict):
87
+ return []
88
+ versions = notes.get("versions") or []
89
+ if not isinstance(versions, list):
90
+ versions = []
91
+ safe_versions = []
92
+ for entry in versions:
93
+ if not isinstance(entry, dict):
94
+ continue
95
+ version = entry.get("version")
96
+ body = entry.get("body")
97
+ if isinstance(version, str) and isinstance(body, str) and body.strip():
98
+ safe_versions.append(
99
+ {
100
+ "version": version,
101
+ "body": body,
102
+ "truncated": bool(entry.get("truncated")),
103
+ }
104
+ )
105
+
106
+ lines: list[str] = []
107
+ note_range = notes.get("range") or {}
108
+ if isinstance(note_range, dict) and (note_range.get("from") or note_range.get("to")):
109
+ range_text = f"{note_range.get('from')} -> {note_range.get('to')}"
110
+ if note_range.get("channel"):
111
+ range_text += f" ({note_range.get('channel')})"
112
+ if note_range.get("reason"):
113
+ range_text += f" reason={note_range.get('reason')}"
114
+ lines.append(f"release_notes_range: {range_text}")
115
+ if safe_versions:
116
+ lines.append(
117
+ "release_notes: "
118
+ + json.dumps(safe_versions, ensure_ascii=False, separators=(",", ":"))
119
+ )
120
+ elif notes.get("unavailable"):
121
+ lines.append("release_notes_unavailable: true")
122
+ lines.append(f"release_notes_truncated: {_json_bool(notes.get('truncated'))}")
123
+ lines.append(f"release_notes_more_versions: {_json_bool(notes.get('moreVersions'))}")
124
+ return lines
125
+
126
+
78
127
  def _format_context(data: dict) -> str:
79
128
  """把 self-check JSON 转成给 AI 读取的短上下文块。"""
80
129
  current = data.get("current") or {}
@@ -102,6 +151,7 @@ def _format_context(data: dict) -> str:
102
151
  lines.append(f"remote: {json.dumps(remote.get('tags'), ensure_ascii=False)}")
103
152
  if remote.get("errorCode"):
104
153
  lines.append(f"remote_error_code: {remote.get('errorCode')}")
154
+ lines.extend(_release_notes_lines(data))
105
155
  command = (data.get("commands") or {}).get("recommended") or ai.get("command")
106
156
  if command:
107
157
  lines.append(f"recommended_command: {command}")
@@ -110,7 +160,9 @@ def _format_context(data: dict) -> str:
110
160
  if ai.get("mode"):
111
161
  lines.append(f"ai_mode: {ai.get('mode')}")
112
162
  if ai.get("mode") == "ask":
113
- lines.append("ai_required_action: 必须先向用户提出明确确认问题;用户确认前禁止执行 recommended_command。")
163
+ lines.append(
164
+ "ai_required_action: 必须先向用户展示 release_notes 摘要和 recommended_command,再提出明确确认问题;用户确认前禁止执行 recommended_command。"
165
+ )
114
166
  if ai.get("instruction"):
115
167
  lines.append(f"ai_instruction: {ai.get('instruction')}")
116
168
  lines.append("</flower-update>")
@@ -121,7 +173,11 @@ def _system_message(data: dict) -> str:
121
173
  """生成 Codex / Claude Code 更容易注意到的短系统提示。"""
122
174
  ai = data.get("ai") or {}
123
175
  command = (data.get("commands") or {}).get("recommended") or ai.get("command")
176
+ notes = data.get("releaseNotes") or {}
177
+ has_notes = isinstance(notes, dict) and bool(notes.get("versions"))
124
178
  if ai.get("mode") == "ask" and command:
179
+ if has_notes:
180
+ return "flower-trellis 发现可执行更新;必须先展示更新摘要并询问用户是否执行 recommended_command,确认前禁止运行。"
125
181
  return "flower-trellis 发现可执行更新;必须先询问用户是否执行 recommended_command,确认前禁止运行。"
126
182
  if command:
127
183
  return "flower-trellis 发现可执行更新;已注入 recommended_command。"
@@ -24,6 +24,80 @@ function runCommand(command, args, cwd, failureMessage) {
24
24
  }
25
25
  }
26
26
 
27
+ /** 读取目标 git 工作区变动数量,只作为 self-update 后续动作提示。 */
28
+ function gitDirtySummary(target) {
29
+ const common = {
30
+ cwd: target,
31
+ encoding: "utf8",
32
+ shell: process.platform === "win32",
33
+ stdio: ["ignore", "pipe", "pipe"],
34
+ timeout: 1500,
35
+ };
36
+ const root = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], common);
37
+ if (root.error || root.status !== 0 || String(root.stdout || "").trim() !== "true") {
38
+ return {
39
+ checked: false,
40
+ reason: "not_git_repo",
41
+ dirtyCount: 0,
42
+ changedFilesDetected: false,
43
+ files: [],
44
+ };
45
+ }
46
+ const status = spawnSync("git", ["status", "--porcelain"], common);
47
+ if (status.error || status.status !== 0) {
48
+ return {
49
+ checked: false,
50
+ reason: "git_status_failed",
51
+ dirtyCount: 0,
52
+ changedFilesDetected: false,
53
+ files: [],
54
+ };
55
+ }
56
+ const files = String(status.stdout || "")
57
+ .split(/\r?\n/)
58
+ .map((line) => line.trim())
59
+ .filter(Boolean);
60
+ return {
61
+ checked: true,
62
+ reason: null,
63
+ dirtyCount: files.length,
64
+ changedFilesDetected: files.length > 0,
65
+ files: files.slice(0, 20),
66
+ };
67
+ }
68
+
69
+ /** 打印 release notes 预览行。 */
70
+ function printReleaseNotesPreview(releaseNotes) {
71
+ if (!releaseNotes) return;
72
+ const versions = Array.isArray(releaseNotes.versions) ? releaseNotes.versions : [];
73
+ if (!versions.length) {
74
+ if (releaseNotes.unavailable) console.log(" · 更新内容:未获取到可用 release notes");
75
+ return;
76
+ }
77
+ console.log(" · 更新内容:");
78
+ for (const entry of versions) {
79
+ const suffix = entry.truncated ? " (已截断)" : "";
80
+ console.log(` - ${entry.version}${suffix}:${entry.body}`);
81
+ }
82
+ if (releaseNotes.moreVersions) {
83
+ console.log(" - 还有更多版本变更未展示");
84
+ }
85
+ }
86
+
87
+ /** 打印结构化 self-update 结果块。 */
88
+ function printFlowerUpdateResult(fields) {
89
+ console.log("<flower-update-result>");
90
+ for (const [key, value] of Object.entries(fields)) {
91
+ if (value === undefined) continue;
92
+ if (typeof value === "object" && value !== null) {
93
+ console.log(`${key}: ${JSON.stringify(value)}`);
94
+ } else {
95
+ console.log(`${key}: ${value}`);
96
+ }
97
+ }
98
+ console.log("</flower-update-result>");
99
+ }
100
+
27
101
  /**
28
102
  * flower-trellis self-update:执行受控自更新与项目重叠加。
29
103
  *
@@ -73,7 +147,16 @@ export async function selfUpdate(ctx) {
73
147
  console.log(
74
148
  ` · 安全检查:${effectiveSafety.safe ? "通过" : `需确认(${effectiveSafety.reasons.join(", ") || "无可用结果"})`}`,
75
149
  );
150
+ printReleaseNotesPreview(check.releaseNotes);
76
151
  console.log(` · 写入:否`);
152
+ printFlowerUpdateResult({
153
+ status: "dry_run",
154
+ target: ctx.target,
155
+ write: false,
156
+ current_status: check.status,
157
+ post_action_preview: "run_trellis_push_after_real_update",
158
+ release_notes: check.releaseNotes || null,
159
+ });
77
160
  return;
78
161
  }
79
162
 
@@ -104,5 +187,19 @@ export async function selfUpdate(ctx) {
104
187
  "目标项目重叠加失败,请手动运行:" + projectUpdateCommand(ctx.target, forwarded),
105
188
  );
106
189
 
190
+ const dirty = gitDirtySummary(ctx.target);
107
191
  console.log(`\n🌸 flower-trellis self-update 完成 → ${ctx.target}`);
192
+ printFlowerUpdateResult({
193
+ status: "completed",
194
+ target: ctx.target,
195
+ write: true,
196
+ git_dirty_count: dirty.dirtyCount,
197
+ changed_files_detected: dirty.changedFilesDetected,
198
+ changed_files_sample: dirty.files,
199
+ git_check_reason: dirty.reason,
200
+ post_action: "run_trellis_push_confirmation",
201
+ release_notes: check.releaseNotes || null,
202
+ ai_instruction:
203
+ "汇总本次升级产生的文件变动,进入 trellis-push 执行计划,展示具体文件列表和 commit message 后等待用户确认。",
204
+ });
108
205
  }
@@ -18,10 +18,61 @@ const DEFAULT_UPDATE_CHECK = {
18
18
  intervalHours: 8,
19
19
  lastCheckedAt: null,
20
20
  lastRemote: null,
21
+ lastReleaseNotes: null,
21
22
  lastStatus: null,
22
23
  lastErrorCode: null,
23
24
  };
24
25
 
26
+ /**
27
+ * 归一化 release notes 范围字段。
28
+ *
29
+ * @param {object|null|undefined} value 原始 range 字段
30
+ * @returns {{from:string|null,to:string|null,channel:string|null,reason:string|null}} 归一化 range
31
+ */
32
+ function normalizeReleaseNotesRange(value) {
33
+ const raw = value && typeof value === "object" ? value : {};
34
+ return {
35
+ from: typeof raw.from === "string" ? raw.from : null,
36
+ to: typeof raw.to === "string" ? raw.to : null,
37
+ channel: typeof raw.channel === "string" ? raw.channel : null,
38
+ reason: typeof raw.reason === "string" ? raw.reason : null,
39
+ };
40
+ }
41
+
42
+ /**
43
+ * 归一化最近一次可用 release notes 摘要。
44
+ *
45
+ * @param {object|null|undefined} value 原始 lastReleaseNotes 字段
46
+ * @returns {{source:string,range:object,versions:Array<{version:string,body:string,truncated:boolean}>,truncated:boolean,moreVersions:boolean,unavailable:boolean}|null} 归一化摘要
47
+ */
48
+ function normalizeLastReleaseNotes(value) {
49
+ if (!value || typeof value !== "object") return null;
50
+ const versions = Array.isArray(value.versions)
51
+ ? value.versions
52
+ .map((entry) => {
53
+ if (!entry || typeof entry !== "object") return null;
54
+ const version = typeof entry.version === "string" ? entry.version : null;
55
+ const body = typeof entry.body === "string" ? entry.body : null;
56
+ if (!version || !body) return null;
57
+ return {
58
+ version,
59
+ body,
60
+ truncated: entry.truncated === true,
61
+ };
62
+ })
63
+ .filter(Boolean)
64
+ : [];
65
+ if (!versions.length && value.unavailable !== true) return null;
66
+ return {
67
+ source: typeof value.source === "string" ? value.source : "npm-metadata",
68
+ range: normalizeReleaseNotesRange(value.range),
69
+ versions,
70
+ truncated: value.truncated === true,
71
+ moreVersions: value.moreVersions === true,
72
+ unavailable: value.unavailable === true,
73
+ };
74
+ }
75
+
25
76
  /** manifest 文件的绝对路径。 */
26
77
  export function manifestPath(target) {
27
78
  return path.join(target, MANIFEST_REL);
@@ -42,7 +93,7 @@ export function readManifest(target) {
42
93
  * 用户策略字段启用保守默认值;缓存字段只保留结构化摘要,避免把网络错误细节写入项目。
43
94
  *
44
95
  * @param {object|null|undefined} value 原始 updateCheck 字段
45
- * @returns {{enabled:boolean,policy:string,intervalHours:number,lastCheckedAt:string|null,lastRemote:object|null,lastStatus:string|null,lastErrorCode:string|null}} 归一化后的 updateCheck
96
+ * @returns {{enabled:boolean,policy:string,intervalHours:number,lastCheckedAt:string|null,lastRemote:object|null,lastReleaseNotes:object|null,lastStatus:string|null,lastErrorCode:string|null}} 归一化后的 updateCheck
46
97
  */
47
98
  export function normalizeUpdateCheck(value) {
48
99
  const raw = value && typeof value === "object" ? value : {};
@@ -64,6 +115,7 @@ export function normalizeUpdateCheck(value) {
64
115
  intervalHours,
65
116
  lastCheckedAt: typeof raw.lastCheckedAt === "string" ? raw.lastCheckedAt : null,
66
117
  lastRemote,
118
+ lastReleaseNotes: normalizeLastReleaseNotes(raw.lastReleaseNotes),
67
119
  lastStatus: typeof raw.lastStatus === "string" ? raw.lastStatus : null,
68
120
  lastErrorCode: typeof raw.lastErrorCode === "string" ? raw.lastErrorCode : null,
69
121
  };
@@ -2,7 +2,12 @@ import { spawnSync } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { readManifest, readUpdateCheck, writeUpdateCheck } from "./manifest.js";
5
- import { fetchPackageDistTags, getUpdateRecommendation } from "./update-check.js";
5
+ import {
6
+ buildReleaseNotesSummary,
7
+ fetchPackageUpdateMetadata,
8
+ getUpdateRecommendation,
9
+ isPrerelease,
10
+ } from "./update-check.js";
6
11
  import { isRunningViaNpx } from "./runtime-env.js";
7
12
  import { flowerVersion, trellisVersion } from "./versions.js";
8
13
 
@@ -36,6 +41,74 @@ function isRemoteCacheFresh(updateCheck, now = new Date()) {
36
41
  return now.getTime() - checkedAt < updateCheck.intervalHours * 60 * 60 * 1000;
37
42
  }
38
43
 
44
+ /**
45
+ * 判断缓存的 release notes 是否匹配本次检查范围。
46
+ *
47
+ * @param {object} updateCheck 归一化 updateCheck 配置
48
+ * @param {{from:string|null,to:string|null,channel:string,reason:string}|null} range 本次期望范围
49
+ * @returns {object|null} 可复用的缓存摘要
50
+ */
51
+ function cachedReleaseNotes(updateCheck, range) {
52
+ const cached = updateCheck.lastReleaseNotes;
53
+ if (!cached || !range || cached.unavailable) return null;
54
+ const cachedRange = cached.range || {};
55
+ if (
56
+ cachedRange.from !== range.from ||
57
+ cachedRange.to !== range.to ||
58
+ cachedRange.channel !== range.channel ||
59
+ cachedRange.reason !== range.reason
60
+ ) {
61
+ return null;
62
+ }
63
+ return Array.isArray(cached.versions) && cached.versions.length ? cached : null;
64
+ }
65
+
66
+ /**
67
+ * 构造新版升级 release notes 范围。
68
+ *
69
+ * @param {string} currentFlower 当前 flower 版本
70
+ * @param {{version:string,tag:string}|null} recommendation 升级推荐
71
+ * @returns {{from:string,to:string,channel:string,reason:string}|null} release notes 范围
72
+ */
73
+ function updateReleaseNotesRange(currentFlower, recommendation) {
74
+ if (!recommendation?.version) return null;
75
+ return {
76
+ from: currentFlower,
77
+ to: recommendation.version,
78
+ channel: recommendation.tag,
79
+ reason: "update_available",
80
+ };
81
+ }
82
+
83
+ /**
84
+ * 构造项目追平 release notes 范围。
85
+ *
86
+ * @param {string|null} projectFlower 项目 manifest 记录的 flower 版本
87
+ * @param {string} currentFlower 当前 flower 版本
88
+ * @returns {{from:string,to:string,channel:string,reason:string}|null} release notes 范围
89
+ */
90
+ function projectReleaseNotesRange(projectFlower, currentFlower) {
91
+ if (!projectFlower || !currentFlower) return null;
92
+ return {
93
+ from: projectFlower,
94
+ to: currentFlower,
95
+ channel: isPrerelease(currentFlower) ? "beta" : "latest",
96
+ reason: "project_out_of_sync",
97
+ };
98
+ }
99
+
100
+ /**
101
+ * 从 registry metadata 构造 release notes 摘要。
102
+ *
103
+ * @param {object|null} metadata registry metadata
104
+ * @param {object|null} range release notes 范围
105
+ * @returns {object|null} release notes 摘要
106
+ */
107
+ function releaseNotesFromMetadata(metadata, range) {
108
+ if (!range) return null;
109
+ return buildReleaseNotesSummary(metadata?.releaseNotesByVersion, range);
110
+ }
111
+
39
112
  /** 检查目标目录 git 工作区是否 clean。 */
40
113
  function gitSafety(target) {
41
114
  const common = {
@@ -184,7 +257,7 @@ function withAction(target, policy, result, command) {
184
257
  }
185
258
 
186
259
  /** 生成项目重叠加建议结果。 */
187
- function projectOutOfSyncResult(base, target, remotePatch = {}) {
260
+ function projectOutOfSyncResult(base, target, remotePatch = {}, releaseNotes = null) {
188
261
  const command = selfUpdateCommand(target, { projectOnly: true });
189
262
  return withAction(
190
263
  target,
@@ -198,6 +271,7 @@ function projectOutOfSyncResult(base, target, remotePatch = {}) {
198
271
  recommended: command,
199
272
  projectUpdate: projectUpdateCommand(target),
200
273
  },
274
+ releaseNotes,
201
275
  },
202
276
  command,
203
277
  );
@@ -276,6 +350,7 @@ export async function buildSelfCheck(target, options = {}) {
276
350
  errorCode: null,
277
351
  },
278
352
  recommendation: null,
353
+ releaseNotes: null,
279
354
  commands: {},
280
355
  safety: null,
281
356
  ai: null,
@@ -297,6 +372,7 @@ export async function buildSelfCheck(target, options = {}) {
297
372
  const recommendation = getUpdateRecommendation(currentFlower, tags);
298
373
  if (recommendation) {
299
374
  const command = selfUpdateCommand(absoluteTarget);
375
+ const releaseNotesRange = updateReleaseNotesRange(currentFlower, recommendation);
300
376
  return withAction(
301
377
  absoluteTarget,
302
378
  updateCheck.policy,
@@ -311,16 +387,18 @@ export async function buildSelfCheck(target, options = {}) {
311
387
  npm: recommendation.command,
312
388
  projectUpdate: projectUpdateCommand(absoluteTarget),
313
389
  },
390
+ releaseNotes: cachedReleaseNotes(updateCheck, releaseNotesRange),
314
391
  },
315
392
  command,
316
393
  );
317
394
  }
318
395
  if (projectOutOfSync) {
396
+ const releaseNotesRange = projectReleaseNotesRange(projectFlower, currentFlower);
319
397
  return projectOutOfSyncResult(base, absoluteTarget, {
320
398
  tags,
321
399
  fromCache: true,
322
400
  skipped: true,
323
- });
401
+ }, cachedReleaseNotes(updateCheck, releaseNotesRange));
324
402
  }
325
403
  return {
326
404
  ...base,
@@ -330,8 +408,8 @@ export async function buildSelfCheck(target, options = {}) {
330
408
  };
331
409
  }
332
410
 
333
- tags = await fetchPackageDistTags();
334
- if (!tags) {
411
+ const metadata = await fetchPackageUpdateMetadata();
412
+ if (!metadata) {
335
413
  if (writeCache && manifest) {
336
414
  writeUpdateCheck(absoluteTarget, {
337
415
  lastStatus: "offline",
@@ -340,7 +418,13 @@ export async function buildSelfCheck(target, options = {}) {
340
418
  }
341
419
  const remotePatch = { tags: updateCheck.lastRemote, errorCode: "fetch_failed" };
342
420
  if (projectOutOfSync) {
343
- return projectOutOfSyncResult(base, absoluteTarget, remotePatch);
421
+ const releaseNotesRange = projectReleaseNotesRange(projectFlower, currentFlower);
422
+ return projectOutOfSyncResult(
423
+ base,
424
+ absoluteTarget,
425
+ remotePatch,
426
+ cachedReleaseNotes(updateCheck, releaseNotesRange),
427
+ );
344
428
  }
345
429
  return {
346
430
  ...base,
@@ -350,12 +434,18 @@ export async function buildSelfCheck(target, options = {}) {
350
434
  };
351
435
  }
352
436
 
437
+ tags = metadata.tags;
353
438
  const recommendation = getUpdateRecommendation(currentFlower, tags);
354
439
  const remoteStatus = recommendation ? "update_available" : "up_to_date";
440
+ const releaseNotesRange = recommendation
441
+ ? updateReleaseNotesRange(currentFlower, recommendation)
442
+ : projectReleaseNotesRange(projectFlower, currentFlower);
443
+ const releaseNotes = releaseNotesFromMetadata(metadata, releaseNotesRange);
355
444
  if (writeCache && manifest) {
356
445
  writeUpdateCheck(absoluteTarget, {
357
446
  lastCheckedAt: now.toISOString(),
358
447
  lastRemote: tags,
448
+ lastReleaseNotes: releaseNotes && !releaseNotes.unavailable ? releaseNotes : null,
359
449
  lastStatus: remoteStatus,
360
450
  lastErrorCode: null,
361
451
  });
@@ -363,7 +453,7 @@ export async function buildSelfCheck(target, options = {}) {
363
453
 
364
454
  if (!recommendation) {
365
455
  if (projectOutOfSync) {
366
- return projectOutOfSyncResult(base, absoluteTarget, { tags });
456
+ return projectOutOfSyncResult(base, absoluteTarget, { tags }, releaseNotes);
367
457
  }
368
458
  return {
369
459
  ...base,
@@ -386,6 +476,7 @@ export async function buildSelfCheck(target, options = {}) {
386
476
  npm: recommendation.command,
387
477
  projectUpdate: projectUpdateCommand(absoluteTarget),
388
478
  },
479
+ releaseNotes,
389
480
  },
390
481
  command,
391
482
  );
@@ -31,7 +31,8 @@ function stripSkillOverride(value, name) {
31
31
  }
32
32
 
33
33
  /**
34
- * 把 override 块插入到 frontmatter 后、正文前;无 frontmatter 时插到文件顶部。
34
+ * 把 override 块插入到 frontmatter 后、正文前;无 frontmatter 时优先插到首个 H1 后,
35
+ * 都没有才插到文件顶部。
35
36
  *
36
37
  * @param {string} value 已清理旧块的目标文本
37
38
  * @param {string} block override 块
@@ -44,6 +45,13 @@ function injectAfterFrontmatter(value, block) {
44
45
  const after = value.slice(m[0].length).replace(/^\n+/, "");
45
46
  return value.slice(0, m[0].length) + "\n" + b + "\n\n" + after;
46
47
  }
48
+ // 无 frontmatter 的 command 文件首行标题常被平台当作命令描述来源;
49
+ // override 放到标题后,避免把高优先级 override 标题暴露成命令名。
50
+ const h1 = /^# [^\n]*\n/.exec(value);
51
+ if (h1) {
52
+ const after = value.slice(h1[0].length).replace(/^\n+/, "");
53
+ return value.slice(0, h1[0].length) + "\n" + b + "\n\n" + after;
54
+ }
47
55
  return b + "\n\n" + value.replace(/^\n+/, "");
48
56
  }
49
57
 
@@ -19,15 +19,75 @@ const REGISTRY = "https://registry.npmjs.org";
19
19
  const PKG = "flower-trellis";
20
20
  /** 网络探测超时(毫秒)—— 启动检查有 hook 总预算兜底,5s 可降低 registry 偶发慢响应误判。 */
21
21
  const TIMEOUT_MS = 5000;
22
+ /** npm package metadata 中保存 flower 内部发布说明的字段名。 */
23
+ const RELEASE_NOTES_FIELD = "flowerReleaseNotes";
24
+ /** 单次注入最多展示的版本数。 */
25
+ const RELEASE_NOTES_MAX_VERSIONS = 5;
26
+ /** 单个版本发布说明最多保留的字符数。 */
27
+ const RELEASE_NOTES_MAX_VERSION_CHARS = 500;
28
+ /** 所有版本发布说明合计最多保留的字符数。 */
29
+ const RELEASE_NOTES_MAX_TOTAL_CHARS = 1600;
22
30
 
23
31
  /**
24
- * npm flower-trellis dist-tags;任何失败(离线/超时/非 200/解析异常)一律
25
- * 返回 null —— 调用方据此「拿不到就当没这回事」继续主流程。
32
+ * npm registry 根文档解析 dist-tags
26
33
  *
27
- * AbortController 给内置 fetch 加超时,finally 清除定时器防句柄泄漏。
28
- * @returns {Promise<{latest:string|null,beta:string|null}|null>} 可用 dist-tags,或失败时 null
34
+ * @param {object} json registry 根文档 JSON
35
+ * @returns {{latest:string|null,beta:string|null}|null} 可用 dist-tags
29
36
  */
30
- export async function fetchPackageDistTags() {
37
+ function parseDistTags(json) {
38
+ const tags = json && typeof json === "object" ? json["dist-tags"] : null;
39
+ const latest = typeof tags?.latest === "string" ? tags.latest : null;
40
+ const beta = typeof tags?.beta === "string" ? tags.beta : null;
41
+ return latest || beta ? { latest, beta } : null;
42
+ }
43
+
44
+ /**
45
+ * 解析单个版本的 flowerReleaseNotes metadata。
46
+ *
47
+ * @param {string} version registry versions 字典里的版本号
48
+ * @param {object} pkgVersion 对应版本 package metadata
49
+ * @returns {{version:string,body:string,truncated:boolean,source:string}|null} 发布说明 metadata
50
+ */
51
+ function parseReleaseNotesMetadata(version, pkgVersion) {
52
+ const raw = pkgVersion?.[RELEASE_NOTES_FIELD];
53
+ if (!raw || typeof raw !== "object") return null;
54
+ if (raw.version !== version) return null;
55
+ const body = typeof raw.body === "string" ? raw.body.trim() : "";
56
+ if (!body) return null;
57
+ return {
58
+ version,
59
+ body,
60
+ truncated: raw.truncated === true,
61
+ source: "npm-metadata",
62
+ };
63
+ }
64
+
65
+ /**
66
+ * 从 npm registry 根文档解析每个版本的发布说明 metadata。
67
+ *
68
+ * @param {object} json registry 根文档 JSON
69
+ * @returns {Record<string,{version:string,body:string,truncated:boolean,source:string}>} 按版本号索引的发布说明
70
+ */
71
+ function parseReleaseNotesByVersion(json) {
72
+ const versions = json && typeof json === "object" ? json.versions : null;
73
+ if (!versions || typeof versions !== "object") return {};
74
+ const notes = {};
75
+ for (const [version, pkgVersion] of Object.entries(versions)) {
76
+ const parsed = parseReleaseNotesMetadata(version, pkgVersion);
77
+ if (parsed) notes[version] = parsed;
78
+ }
79
+ return notes;
80
+ }
81
+
82
+ /**
83
+ * 取 npm 上 flower-trellis 的更新 metadata。
84
+ *
85
+ * 一次请求 registry 根文档,同时解析 dist-tags 与各版本的 flowerReleaseNotes。任何失败
86
+ * (离线/超时/非 200/解析异常)一律返回 null,调用方继续主流程。
87
+ *
88
+ * @returns {Promise<{tags:{latest:string|null,beta:string|null},releaseNotesByVersion:Record<string,{version:string,body:string,truncated:boolean,source:string}>}|null>} 更新 metadata
89
+ */
90
+ export async function fetchPackageUpdateMetadata() {
31
91
  const ac = new AbortController();
32
92
  const timer = setTimeout(() => ac.abort(), TIMEOUT_MS);
33
93
  try {
@@ -37,10 +97,12 @@ export async function fetchPackageDistTags() {
37
97
  });
38
98
  if (!res.ok) return null; // 非 200(404/5xx 等)→ 静默跳过
39
99
  const json = await res.json();
40
- const tags = json && typeof json === "object" ? json["dist-tags"] : null;
41
- const latest = typeof tags?.latest === "string" ? tags.latest : null;
42
- const beta = typeof tags?.beta === "string" ? tags.beta : null;
43
- return latest || beta ? { latest, beta } : null;
100
+ const tags = parseDistTags(json);
101
+ if (!tags) return null;
102
+ return {
103
+ tags,
104
+ releaseNotesByVersion: parseReleaseNotesByVersion(json),
105
+ };
44
106
  } catch {
45
107
  return null; // AbortError(超时)/ fetch failed(离线)/ JSON 解析失败 → 静默
46
108
  } finally {
@@ -48,10 +110,22 @@ export async function fetchPackageDistTags() {
48
110
  }
49
111
  }
50
112
 
113
+ /**
114
+ * 取 npm 上 flower-trellis 的 dist-tags;任何失败(离线/超时/非 200/解析异常)一律
115
+ * 返回 null —— 调用方据此「拿不到就当没这回事」继续主流程。
116
+ *
117
+ * 用 AbortController 给内置 fetch 加超时,finally 清除定时器防句柄泄漏。
118
+ * @returns {Promise<{latest:string|null,beta:string|null}|null>} 可用 dist-tags,或失败时 null
119
+ */
120
+ export async function fetchPackageDistTags() {
121
+ const metadata = await fetchPackageUpdateMetadata();
122
+ return metadata?.tags ?? null;
123
+ }
124
+
51
125
  /**
52
126
  * 取 npm 上 flower-trellis 的 latest 版本号。
53
127
  *
54
- * 保留这个导出是为了兼容已有调用方;新逻辑应优先使用 `fetchPackageDistTags()`。
128
+ * 保留这个导出是为了兼容已有调用方;新逻辑应优先使用 `fetchPackageUpdateMetadata()`。
55
129
  * @returns {Promise<string|null>} latest 版本号,或失败时 null
56
130
  */
57
131
  export async function fetchLatestVersion() {
@@ -147,6 +221,106 @@ export function compareVersions(a, b) {
147
221
  return comparePrerelease(av.prerelease, bv.prerelease);
148
222
  }
149
223
 
224
+ /**
225
+ * 判断版本是否属于目标更新通道。
226
+ *
227
+ * @param {string} version 版本号
228
+ * @param {"latest"|"beta"|string} channel 目标通道
229
+ * @returns {boolean}
230
+ */
231
+ function matchesReleaseNotesChannel(version, channel) {
232
+ if (channel === "beta") return isPrerelease(version);
233
+ return !isPrerelease(version);
234
+ }
235
+
236
+ /**
237
+ * 把单版本发布说明裁剪到注入上限。
238
+ *
239
+ * @param {{body:string,truncated?:boolean}} note 原始版本发布说明
240
+ * @param {number} remainingTotal 总字符剩余额度
241
+ * @returns {{body:string,truncated:boolean,used:number}|null} 裁剪后的发布说明
242
+ */
243
+ function limitReleaseNoteBody(note, remainingTotal) {
244
+ if (remainingTotal <= 0) return null;
245
+ let body = String(note.body || "").trim();
246
+ let truncated = note.truncated === true;
247
+ if (!body) return null;
248
+ if (body.length > RELEASE_NOTES_MAX_VERSION_CHARS) {
249
+ body = body.slice(0, RELEASE_NOTES_MAX_VERSION_CHARS).trimEnd();
250
+ truncated = true;
251
+ }
252
+ if (body.length > remainingTotal) {
253
+ body = body.slice(0, remainingTotal).trimEnd();
254
+ truncated = true;
255
+ }
256
+ if (!body) return null;
257
+ return { body, truncated, used: body.length };
258
+ }
259
+
260
+ /**
261
+ * 为 self-check / hook 构造跨版本发布说明摘要。
262
+ *
263
+ * @param {Record<string,{version:string,body:string,truncated:boolean,source:string}>|null|undefined} releaseNotesByVersion 按版本索引的 npm metadata
264
+ * @param {{from:string|null,to:string|null,channel:"latest"|"beta"|string,reason:string}} range 聚合范围
265
+ * @returns {{source:string,range:object,versions:Array<{version:string,body:string,truncated:boolean}>,truncated:boolean,moreVersions:boolean,unavailable:boolean}} 可注入的发布说明摘要
266
+ */
267
+ export function buildReleaseNotesSummary(releaseNotesByVersion, range) {
268
+ const summaryRange = {
269
+ from: range?.from ?? null,
270
+ to: range?.to ?? null,
271
+ channel: range?.channel ?? "latest",
272
+ reason: range?.reason ?? null,
273
+ };
274
+ const empty = {
275
+ source: "npm-metadata",
276
+ range: summaryRange,
277
+ versions: [],
278
+ truncated: false,
279
+ moreVersions: false,
280
+ unavailable: true,
281
+ };
282
+ if (!summaryRange.from || !summaryRange.to || !releaseNotesByVersion) return empty;
283
+
284
+ const candidates = Object.keys(releaseNotesByVersion)
285
+ .filter((version) => compareVersions(version, summaryRange.from) === 1)
286
+ .filter((version) => compareVersions(version, summaryRange.to) <= 0)
287
+ .filter((version) => matchesReleaseNotesChannel(version, summaryRange.channel))
288
+ .sort(compareVersions);
289
+
290
+ if (!candidates.length) return empty;
291
+
292
+ const limitedCandidates = candidates.slice(-RELEASE_NOTES_MAX_VERSIONS);
293
+ const versions = [];
294
+ let remainingTotal = RELEASE_NOTES_MAX_TOTAL_CHARS;
295
+ let truncated = false;
296
+ for (const version of limitedCandidates) {
297
+ const limited = limitReleaseNoteBody(releaseNotesByVersion[version], remainingTotal);
298
+ if (!limited) {
299
+ truncated = true;
300
+ break;
301
+ }
302
+ versions.push({
303
+ version,
304
+ body: limited.body,
305
+ truncated: limited.truncated,
306
+ });
307
+ remainingTotal -= limited.used;
308
+ if (limited.truncated) truncated = true;
309
+ }
310
+
311
+ const moreVersions =
312
+ candidates.length > limitedCandidates.length ||
313
+ versions.length < limitedCandidates.length;
314
+ return {
315
+ source: "npm-metadata",
316
+ range: summaryRange,
317
+ versions,
318
+ truncated,
319
+ moreVersions,
320
+ unavailable: versions.length === 0,
321
+ };
322
+ }
323
+
150
324
  /**
151
325
  * 根据当前版本和 npm dist-tags 生成升级推荐。
152
326
  *
@@ -175,15 +349,20 @@ export function getUpdateRecommendation(current, tags) {
175
349
  }
176
350
 
177
351
  /** 尽力而为刷新目标项目 manifest 里的远端探测缓存。 */
178
- function rememberRemoteTags(target, tags, status) {
352
+ function rememberRemoteTags(target, tags, status, releaseNotes = null) {
179
353
  try {
180
354
  if (!readManifest(target)) return;
181
- writeUpdateCheck(target, {
355
+ const patch = {
182
356
  lastCheckedAt: new Date().toISOString(),
183
357
  lastRemote: tags,
184
358
  lastStatus: status,
185
359
  lastErrorCode: null,
186
- });
360
+ lastReleaseNotes: null,
361
+ };
362
+ if (releaseNotes && !releaseNotes.unavailable) {
363
+ patch.lastReleaseNotes = releaseNotes;
364
+ }
365
+ writeUpdateCheck(target, patch);
187
366
  } catch {
188
367
  // 缓存写入只是优化后续启动提示,失败不能影响 init/update 主流程。
189
368
  }
@@ -217,13 +396,27 @@ export async function checkForUpdate(ctx, commandLabel) {
217
396
  if (isRunningViaNpx(import.meta.url)) return;
218
397
 
219
398
  // 3. 尽力而为取 dist-tags;拿不到就静默退出
220
- const tags = await fetchPackageDistTags();
221
- if (!tags) return;
399
+ const metadata = await fetchPackageUpdateMetadata();
400
+ if (!metadata) return;
401
+ const tags = metadata.tags;
222
402
 
223
403
  // 4. 根据本地版本通道生成推荐;无推荐时不打扰
224
404
  const current = flowerVersion();
225
405
  const recommendation = getUpdateRecommendation(current, tags);
226
- rememberRemoteTags(ctx.target, tags, recommendation ? "update_available" : "up_to_date");
406
+ const releaseNotes = recommendation
407
+ ? buildReleaseNotesSummary(metadata.releaseNotesByVersion, {
408
+ from: current,
409
+ to: recommendation.version,
410
+ channel: recommendation.tag,
411
+ reason: "update_available",
412
+ })
413
+ : null;
414
+ rememberRemoteTags(
415
+ ctx.target,
416
+ tags,
417
+ recommendation ? "update_available" : "up_to_date",
418
+ releaseNotes,
419
+ );
227
420
  if (!recommendation) return;
228
421
 
229
422
  // 5. 打印发现新版本通知(粉色品牌色,与 banner 一致)