flower-trellis 0.6.0-beta.2 → 0.6.0-beta.3

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 CHANGED
@@ -86,7 +86,7 @@ flower-trellis -v
86
86
  | `update` | 升级 Trellis,并按新版本重新叠加强化包 |
87
87
  | `self-check` | 输出启动更新检查 JSON,供 Codex / Claude Code hook 和 AI 自动化读取 |
88
88
  | `self-update` | 受控升级 flower-trellis 并对目标项目执行完整 `flower-trellis update` 重叠加 |
89
- | `update-check` | 管理 `.trellis/.flower-manifest.json` 内的启动更新检查策略 |
89
+ | `update-check` | 管理 `.flower/settings.json` 内的启动更新策略和提示节流 |
90
90
  | `telemetry` | 查询、启用或停用用户级匿名安装遥测 |
91
91
  | `plugin` | 管理 Flower Plugin、Marketplace 来源、GitLab 授权和作者校验 |
92
92
  | `uninstall` | 移除 Trellis 本体并清理强化包残留(支持 `-y` / `--dry-run`) |
@@ -204,16 +204,17 @@ flower-trellis plugin add flower/flower-plugin-author --platform codex --json
204
204
  flower-trellis self-check --json --target .
205
205
  ```
206
206
 
207
- 发现可更新或项目已铺版本不一致时,向 AI 注入 `<flower-update>` 上下文。无更新、离线、关闭检查、npx 临时运行时默认不打扰。
207
+ 发现可更新或项目已铺版本不一致时,向 AI 注入 `<flower-update>` 上下文。无更新、离线、关闭检查、npx 临时运行时默认不打扰;同一更新提示会按本机提示节流状态降噪。
208
208
 
209
209
  检查分两层:
210
210
 
211
- - **本地一致性检查**:每次启动都会比对当前安装的 `flower-trellis` / 捆绑 Trellis 版本与项目 `.trellis/.flower-manifest.json` / `.trellis/.version`;不受 `intervalHours` 限制。
211
+ - **本地一致性检查**:每次启动都会比对当前安装的 `flower-trellis` / 捆绑 Trellis 版本与项目 `.flower/plugin-lock.json` / `.trellis/.flower-manifest.json` / `.trellis/.version`;不受 `intervalHours` 限制。
212
212
  - **远程版本探测**:访问 npm registry 读取 `flower-trellis` dist-tags;受 `intervalHours` 节流,带超时,失败静默。
213
+ - **提示节流**:同一更新提示默认 24 小时内只注入一次;用户选择稍后时默认 7 天不再提示,选择跳过时同一版本不再提示。
213
214
 
214
- 策略保存在 `.trellis/.flower-manifest.json` 的 `updateCheck`;运行缓存保存在 gitignored 的 `.trellis/.flower-update-check.tmp`,避免 `lastCheckedAt` 等字段反复污染 git。
215
+ 策略保存在 `.flower/settings.json` 的 `updateCheck`;运行缓存保存在 gitignored 的 `.flower/update-check.tmp`,避免 `lastCheckedAt` / `lastPromptedAt` 等字段反复污染 git。旧 `.trellis/.flower-manifest.json` 和 `.trellis/.flower-update-check.tmp` 只作为兼容 fallback 读取。
215
216
 
216
- manifest 中只保留用户策略:
217
+ settings 中只保留用户策略:
217
218
 
218
219
  ```json
219
220
  {
@@ -231,8 +232,14 @@ tmp 中保存本地运行缓存:
231
232
  {
232
233
  "lastCheckedAt": "2026-07-07T00:00:00.000Z",
233
234
  "lastRemote": { "latest": "0.4.2", "beta": null },
235
+ "lastReleaseNotes": null,
234
236
  "lastStatus": "update_available",
235
- "lastErrorCode": null
237
+ "lastErrorCode": null,
238
+ "lastPromptedAt": "2026-07-07T00:05:00.000Z",
239
+ "lastPromptedKey": "update:latest:0.4.2",
240
+ "promptSuppressedUntil": null,
241
+ "promptSuppressedKey": null,
242
+ "promptSuppressionReason": null
236
243
  }
237
244
  ```
238
245
 
@@ -254,6 +261,10 @@ flower-trellis update-check get --target .
254
261
  flower-trellis update-check set --policy auto --interval-hours 12 --target .
255
262
  flower-trellis update-check disable --target . # 只设置 enabled=false,不改 policy
256
263
  flower-trellis update-check enable --target . # 只设置 enabled=true,沿用原 policy
264
+ flower-trellis update-check snooze --target . # 延后当前提示,默认 7 天
265
+ flower-trellis update-check snooze --days 3 --target .
266
+ flower-trellis update-check skip --target . # 跳过当前提示 key
267
+ flower-trellis update-check reset --target . # 清空提示节流状态
257
268
  ```
258
269
 
259
270
  执行更新:
@@ -11,8 +11,8 @@
11
11
  | 字段 | 规则 |
12
12
  | --- | --- |
13
13
  | ID | 首次记录时依次分配 `DOC-001`、`DOC-002` |
14
- | 类型 | `task-status`、`brief-stale`、`implementation-note`、`check-record`、`mechanical-link` |
15
- | 文件 | 需要修改的任务或规范文档路径 |
14
+ | 类型 | `task-status`、`brief-stale`、`implementation-note`、`check-record`、`mechanical-link`、`fact-status` |
15
+ | 文件 | 需要修改的任务、规范、项目文档或本地上下文摘要路径 |
16
16
  | 漂移 | 当前文档与实际 diff / 任务状态不一致的事实 |
17
17
  | 证据 | `file:line`、命令输出或实际 diff 摘要 |
18
18
  | 修复 | 精确说明要写入、更新或删除的内容 |
@@ -26,10 +26,12 @@
26
26
 
27
27
  满足以下全部条件才可自动修复:
28
28
 
29
- - 目标是任务执行文档、brief、check 记录、实现进度记录、机械链接或明显过期的本地上下文摘要;
29
+ - 目标是任务执行文档、brief、check 记录、实现进度记录、机械链接、明显过期的本地上下文摘要,或项目文档中仅记录已验证事实状态的片段;
30
30
  - 修复内容可由实际 diff、任务状态、命令输出或已存在任务材料唯一推出;
31
31
  - 不新增需求、不改变验收标准、不改变设计取舍、不替用户选择产品语义;
32
32
  - 不修改源码、配置、测试、迁移、生产数据或外部系统;
33
+ - 可自动修复性由具体片段语义决定,不由文档类别决定;同一文档中只有可由证据唯一推出的事实状态片段可改;
34
+ - 事实状态修复只允许同步状态、URL、版本号、配置键、命令结果、验证结论或时间点,不改操作步骤、责任边界、安全提示、协议字段或对外承诺;
33
35
  - 改动小且可通过重读文件或简单静态检查验证;
34
36
  - 用户没有明确要求本轮绝对只读。
35
37
 
@@ -39,6 +41,7 @@
39
41
  - `implement.md` 中机械状态仍写“待完成”,但对应步骤已经落地且无语义争议;
40
42
  - `check.jsonl` 或任务记录中的文件路径、命令名、链接因重命名出现机械漂移;
41
43
  - 文档写的检查深度、route 或状态与 runner / task.py 的确定性状态不一致。
44
+ - 任意 Markdown / HTML / 运行手册 / 接入说明仍写“待发布 / 待回读 / 旧版本 / 旧地址”,但已读取的发布记录、环境回读、测试输出或实际 diff 唯一证明目标状态已经变化。
42
45
 
43
46
  ---
44
47
 
@@ -51,6 +54,7 @@
51
54
  - 实现与规划冲突,无法判断是代码错还是文档错;
52
55
  - 需要新增测试、改代码、改配置、改迁移或调用外部系统;
53
56
  - 证据来自推测、聊天摘要或未读取的文件;
57
+ - 文档修复需要改写用户可见操作路径、接口协议、权限/安全边界、上线承诺、责任归属或验收口径;
54
58
  - 修改会扩大任务范围或影响发布、权限、数据安全、生产行为。
55
59
 
56
60
  ---
@@ -11,8 +11,8 @@
11
11
  | 字段 | 规则 |
12
12
  | --- | --- |
13
13
  | ID | 首次记录时依次分配 `DOC-001`、`DOC-002` |
14
- | 类型 | `task-status`、`brief-stale`、`implementation-note`、`check-record`、`mechanical-link` |
15
- | 文件 | 需要修改的任务或规范文档路径 |
14
+ | 类型 | `task-status`、`brief-stale`、`implementation-note`、`check-record`、`mechanical-link`、`fact-status` |
15
+ | 文件 | 需要修改的任务、规范、项目文档或本地上下文摘要路径 |
16
16
  | 漂移 | 当前文档与实际 diff / 任务状态不一致的事实 |
17
17
  | 证据 | `file:line`、命令输出或实际 diff 摘要 |
18
18
  | 修复 | 精确说明要写入、更新或删除的内容 |
@@ -26,10 +26,12 @@
26
26
 
27
27
  满足以下全部条件才可自动修复:
28
28
 
29
- - 目标是任务执行文档、brief、check 记录、实现进度记录、机械链接或明显过期的本地上下文摘要;
29
+ - 目标是任务执行文档、brief、check 记录、实现进度记录、机械链接、明显过期的本地上下文摘要,或项目文档中仅记录已验证事实状态的片段;
30
30
  - 修复内容可由实际 diff、任务状态、命令输出或已存在任务材料唯一推出;
31
31
  - 不新增需求、不改变验收标准、不改变设计取舍、不替用户选择产品语义;
32
32
  - 不修改源码、配置、测试、迁移、生产数据或外部系统;
33
+ - 可自动修复性由具体片段语义决定,不由文档类别决定;同一文档中只有可由证据唯一推出的事实状态片段可改;
34
+ - 事实状态修复只允许同步状态、URL、版本号、配置键、命令结果、验证结论或时间点,不改操作步骤、责任边界、安全提示、协议字段或对外承诺;
33
35
  - 改动小且可通过重读文件或简单静态检查验证;
34
36
  - 用户没有明确要求本轮绝对只读。
35
37
 
@@ -39,6 +41,7 @@
39
41
  - `implement.md` 中机械状态仍写“待完成”,但对应步骤已经落地且无语义争议;
40
42
  - `check.jsonl` 或任务记录中的文件路径、命令名、链接因重命名出现机械漂移;
41
43
  - 文档写的检查深度、route 或状态与 runner / task.py 的确定性状态不一致。
44
+ - 任意 Markdown / HTML / 运行手册 / 接入说明仍写“待发布 / 待回读 / 旧版本 / 旧地址”,但已读取的发布记录、环境回读、测试输出或实际 diff 唯一证明目标状态已经变化。
42
45
 
43
46
  ---
44
47
 
@@ -51,6 +54,7 @@
51
54
  - 实现与规划冲突,无法判断是代码错还是文档错;
52
55
  - 需要新增测试、改代码、改配置、改迁移或调用外部系统;
53
56
  - 证据来自推测、聊天摘要或未读取的文件;
57
+ - 文档修复需要改写用户可见操作路径、接口协议、权限/安全边界、上线承诺、责任归属或验收口径;
54
58
  - 修改会扩大任务范围或影响发布、权限、数据安全、生产行为。
55
59
 
56
60
  ---
@@ -476,7 +476,10 @@
476
476
  "type": "required-literal",
477
477
  "values": [
478
478
  "Only an explicit current-request workflow instruction such as `直接做` / `不要任务` may override automatic `task_plan`.",
479
- "python3 ./.trellis/scripts/untracked_flow.py begin --summary \"<bounded work summary>\" --source <inferred|user-explicit>",
479
+ "Use quick direct edit for one-turn small fixes",
480
+ "Use tracked direct edit only when the user explicitly wants no task but still wants later turns to remember the current phase",
481
+ "python3 ./.trellis/scripts/untracked_flow.py begin --summary \"<bounded work summary>\" --source <inferred|user-explicit> --mode tracked-direct-edit",
482
+ "Never create `untracked_flow` for `workflow_action` itself",
480
483
  "A same-item hit resumes the existing state",
481
484
  "`active-work-conflict` blocks unrelated code writes until the current item is completed, explicitly abandoned, or adopted into a task",
482
485
  "Unrelated read-only requests may continue without mutating the state.",
@@ -1,6 +1,7 @@
1
1
  - Infer `discuss`, `inspect`, `direct_edit`, `task_plan`, or `workflow_action` from the whole current request, scope, risk, side effects, active-task state, and latest explicit switch. Do not classify from one keyword alone.
2
2
  - Treat requests for an opinion, expressions of discomfort, rejected proposals, or questions about how something should change as `discuss`; treat requests to inspect, explain, verify, or locate a cause as `inspect`. Both are read-only unless the current request explicitly authorizes a concrete edit. Selecting a repair does not authorize editing while scope is unknown or permission to skip task planning; inspect first and reclassify from evidence.
3
3
  - `direct_edit` requires known, bounded, low-risk, reversible scope, no unresolved design choice, and simple validation. Permission/authentication/data-scope/security, database/migration/release/external effects, or unknown scope remain strong `task_plan` boundaries. Shared contracts, cross-package/layer or multi-entry behavior, configuration, historical regressions, and systematic validation raise the evidence and validation bar, but do not automatically require `task_plan`. An exact rollback or mechanically synchronized known change may remain `direct_edit` or `workflow_action` when its behavior, scope, side effects, and validation are already determined.
4
+ - `direct_edit` has two execution shapes. Use quick direct edit for one-turn small fixes, wording/config tweaks, focused release-note corrections, or other changes that can be edited, validated, and reported without remembering a later phase; do not create `untracked_flow` for those. Use tracked direct edit only when the user explicitly wants no task but still wants later turns to remember the current phase, or when the work is likely to continue across turns with `下一步` / `继续` / check / push handoff.
4
5
  - When project-local SOPs or conventions may change the correct action for non-trivial work, build a short query from the request, intended commands, affected files or systems, package/layer, and domain terms; run `python3 ./.trellis/scripts/spec_router.py "<short query describing the intended action>"` once per user intent, workflow phase, or decision boundary and follow its returned `load_strategy` and `action`. Skip discovery for pure Q&A, simple read-only inspection, opening local tools, or trivial edits unless project rules may alter the approach.
5
6
  - When an active task exists, apply the Active Task Scope Guard before artifact ownership, task routing, or file edits. New implementation outside the active task title/brief requires a new task, an artifact update before continuing, or an explicit untracked path that does not reuse current progress. `task_intent.py` executes the already-decided safety boundary; it does not decide semantic ownership.
6
7
  - High-confidence reversible steps proceed without a mechanical task-creation question. Clear complex implementation intent may create a planning task and enter `trellis-brainstorm`, but never authorizes `task.py start` or implementation. Ask one focused question only when ambiguity changes material side effects or crosses an independent destructive, production, database, credential, external-system, permission, or safety boundary.
@@ -1,6 +1,7 @@
1
1
  No active task. Infer the current request intent before acting.
2
2
  Repair intent alone is not a no-task switch; inspect unknown scope and reclassify before edits. Only an explicit current-request workflow instruction such as `直接做` / `不要任务` may override automatic `task_plan`.
3
3
  For non-trivial project work, follow the `Request Triage` Project Knowledge Discovery contract before routing the action. Load a Trellis capability directly only when the user explicitly names it or the request exactly matches that capability; route project-specific workflow actions through the matched SOP instead of keyword-mapping a general release/publish request to `trellis-release`.
4
- Handle `discuss` and `inspect` silently. For non-destructive `direct_edit`, state once that work will use the session-scoped untracked flow, then run `python3 ./.trellis/scripts/untracked_flow.py begin --summary "<bounded work summary>" --source <inferred|user-explicit>`.
4
+ Handle `discuss` and `inspect` silently. For quick direct edit, make the bounded edit, run focused validation, and report without creating `untracked_flow`; small wording/config tweaks, release dry-runs, version checks, local SOP steps, and other one-turn fixes stay in this path unless the current request explicitly asks future `下一步` / `继续` / check / push handoff to remember this no-task work. For tracked direct edit, state once that work will use the session-scoped untracked flow, then run `python3 ./.trellis/scripts/untracked_flow.py begin --summary "<bounded work summary>" --source <inferred|user-explicit> --mode tracked-direct-edit`.
5
+ Never create `untracked_flow` for `workflow_action` itself, including release/publish, commit, push, finish-work, task operations, snapshot sync, auto-loop control, or read-only inspection. If a workflow action reveals a separate code/config fix, pause that workflow action, explain the separate fix, and only create tracked direct edit when the user wants later turns to remember that fix's check/spec/push handoff; otherwise keep it as quick direct edit.
5
6
  A same-item hit resumes the existing state; `active-work-conflict` blocks unrelated code writes until the current item is completed, explicitly abandoned, or adopted into a task. Unrelated read-only requests may continue without mutating the state. A successful `begin` starts at `stage=implement`; the helper is a workflow cursor and does not gate edits on file scope, Git state, or owner evidence.
6
7
  For high-confidence complex implementation, create an auto-routed planning task through `task_intent.py create`, show one non-blocking switch hint, and enter `trellis-brainstorm`. Ask only for material ambiguity or independent safety gates.
@@ -1,6 +1,6 @@
1
1
  [workflow-state:untracked]
2
2
  The current-session untracked work item is at `implement`. Run `python3 ./.trellis/scripts/untracked_flow.py status`, then enter Phase 2.1 through `trellis-route(target=implement)` without creating task artifacts or task-scoped route decisions.
3
- The helper is only a workflow cursor: it does not validate file scope, Git state, focused validation, or owner evidence. After focused validation is complete, run `python3 ./.trellis/scripts/untracked_flow.py advance --stage check` and continue through the normal Phase 2.1 completion contract.
3
+ The helper is only a workflow cursor: it does not validate file scope, Git state, focused validation, or owner evidence. Its `owner` / `remainingOwners` fields are route reminders for `trellis-check-all` -> `trellis-update-spec` -> `trellis-push`, not proof that any owner already ran. After focused validation is complete, run `python3 ./.trellis/scripts/untracked_flow.py advance --stage check` and continue through the normal Phase 2.1 completion contract.
4
4
  A different implementation request remains blocked by the single-active-work guard until this item is completed, explicitly abandoned, or adopted through `python3 ./.trellis/scripts/task_intent.py adopt "<title>" --slug <slug>`; adoption continues through planning artifacts, Brief review, and `task.py start`, and never authorizes immediate implementation. Unrelated read-only requests may continue without changing state.
5
5
  [/workflow-state:untracked]
6
6
 
@@ -20,9 +20,45 @@ STATE_KEY = "untracked_flow"
20
20
  STATE_VERSION = 2
21
21
  LEGACY_STATE_VERSION = 1
22
22
  VALID_SOURCES = {"inferred", "user-explicit"}
23
+ VALID_BEGIN_MODES = {"tracked-direct-edit"}
23
24
  VALID_STAGES = {"implement", "check", "spec", "push"}
24
25
  LEGACY_STAGES = VALID_STAGES | {"inspect"}
25
26
  VALID_CLEAR_REASONS = {"completed", "abandoned", "adopted"}
27
+ STAGE_CONTRACTS = {
28
+ "implement": {
29
+ "owner": "trellis-route(target=implement)",
30
+ "remainingOwners": [
31
+ "trellis-route(target=implement)",
32
+ "trellis-check-all",
33
+ "trellis-update-spec",
34
+ "trellis-push",
35
+ ],
36
+ "advanceAfterOwner": "python3 ./.trellis/scripts/untracked_flow.py advance --stage check",
37
+ },
38
+ "check": {
39
+ "owner": "trellis-check-all",
40
+ "remainingOwners": [
41
+ "trellis-check-all",
42
+ "trellis-update-spec",
43
+ "trellis-push",
44
+ ],
45
+ "advanceAfterOwner": "python3 ./.trellis/scripts/untracked_flow.py advance --stage spec",
46
+ "advanceOnFindings": "python3 ./.trellis/scripts/untracked_flow.py advance --stage implement",
47
+ },
48
+ "spec": {
49
+ "owner": "trellis-update-spec",
50
+ "remainingOwners": [
51
+ "trellis-update-spec",
52
+ "trellis-push",
53
+ ],
54
+ "advanceAfterOwner": "python3 ./.trellis/scripts/untracked_flow.py advance --stage push",
55
+ },
56
+ "push": {
57
+ "owner": "trellis-push",
58
+ "remainingOwners": ["trellis-push"],
59
+ "clearAfterOwner": "python3 ./.trellis/scripts/untracked_flow.py clear --reason completed",
60
+ },
61
+ }
26
62
 
27
63
 
28
64
  class UntrackedFlowError(Exception):
@@ -174,11 +210,24 @@ def _normalized_state(value: Any) -> dict[str, Any] | None:
174
210
 
175
211
  def _state_summary(state: dict[str, Any]) -> dict[str, Any]:
176
212
  """返回默认输出需要的最小状态字段。"""
177
- return {
213
+ stage = state.get("stage")
214
+ summary = {
178
215
  "workId": state.get("id"),
179
216
  "summary": state.get("summary"),
180
217
  "source": state.get("source"),
181
- "stage": state.get("stage"),
218
+ "stage": stage,
219
+ }
220
+ if isinstance(stage, str) and stage in STAGE_CONTRACTS:
221
+ summary.update(_stage_contract(stage))
222
+ return summary
223
+
224
+
225
+ def _stage_contract(stage: str) -> dict[str, Any]:
226
+ """返回当前阶段的 Trellis owner 和剩余完成链提示。"""
227
+ contract = STAGE_CONTRACTS[stage]
228
+ return {
229
+ key: list(value) if isinstance(value, list) else value
230
+ for key, value in contract.items()
182
231
  }
183
232
 
184
233
 
@@ -203,6 +252,7 @@ def begin_untracked_work(
203
252
  repo_root: Path,
204
253
  summary: str,
205
254
  source: str,
255
+ mode: str | None,
206
256
  platform_input: dict[str, Any] | None = None,
207
257
  platform: str | None = None,
208
258
  ) -> dict[str, Any]:
@@ -212,12 +262,20 @@ def begin_untracked_work(
212
262
  repo_root: Trellis 项目根目录。
213
263
  summary: 当前事项摘要。
214
264
  source: ``inferred`` 或 ``user-explicit``。
265
+ mode: 入口模式,必须是 ``tracked-direct-edit``。
215
266
  platform_input: 可选平台 hook 输入。
216
267
  platform: 可选平台名称。
217
268
 
218
269
  Returns:
219
270
  创建或命中同一事项的结构化结果。
220
271
  """
272
+ if mode not in VALID_BEGIN_MODES:
273
+ raise UntrackedFlowError(
274
+ "invalid-entry-mode",
275
+ "只有需要跨轮恢复的无任务直接修改才能创建无任务事项",
276
+ mode=mode,
277
+ allowed=sorted(VALID_BEGIN_MODES),
278
+ )
221
279
  clean_summary = summary.strip()
222
280
  if not clean_summary:
223
281
  raise UntrackedFlowError("summary-empty", "无任务事项摘要不能为空")
@@ -394,8 +452,17 @@ def session_start_hint(
394
452
  return None
395
453
  if result.get("status") != "hit":
396
454
  return None
455
+ owner = result.get("owner")
456
+ remaining_owners = result.get("remainingOwners")
457
+ remaining = (
458
+ [value for value in remaining_owners if isinstance(value, str) and value]
459
+ if isinstance(remaining_owners, list)
460
+ else []
461
+ )
462
+ remaining_text = " -> ".join(remaining) if remaining else str(owner)
397
463
  return (
398
464
  f"Untracked work: {result['workId']}; stage={result['stage']}; "
465
+ f"owner={owner}; remaining={remaining_text}; "
399
466
  f"summary={result['summary']}."
400
467
  )
401
468
 
@@ -422,6 +489,7 @@ def build_parser() -> argparse.ArgumentParser:
422
489
  begin_parser = subparsers.add_parser("begin", help="create one untracked work item")
423
490
  begin_parser.add_argument("--summary", required=True)
424
491
  begin_parser.add_argument("--source", choices=sorted(VALID_SOURCES), required=True)
492
+ begin_parser.add_argument("--mode", required=True)
425
493
 
426
494
  advance_parser = subparsers.add_parser("advance", help="set workflow stage cursor")
427
495
  advance_parser.add_argument("--stage", choices=sorted(VALID_STAGES), required=True)
@@ -452,7 +520,12 @@ def main(argv: list[str] | None = None) -> int:
452
520
  return 1
453
521
  try:
454
522
  if args.command == "begin":
455
- payload = begin_untracked_work(repo_root, args.summary, args.source)
523
+ payload = begin_untracked_work(
524
+ repo_root,
525
+ args.summary,
526
+ args.source,
527
+ args.mode,
528
+ )
456
529
  elif args.command == "advance":
457
530
  payload = advance_stage(repo_root, args.stage)
458
531
  elif args.command == "status":
@@ -1,7 +1,7 @@
1
1
  {
2
- "syncedAt": "2026-08-03T15:09:38.275Z",
2
+ "syncedAt": "2026-08-04T02:44:38.844Z",
3
3
  "syncedFrom": "vendor/skill-garden",
4
- "sourceCommit": "99505703777997be615cecb2e950fbd766398bc9",
4
+ "sourceCommit": "32477325abe35122e9b12d4cc257fab9c7576c48",
5
5
  "common": {
6
6
  "codexSkills": [
7
7
  "aliyun-sls-query",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flower-trellis",
3
- "version": "0.6.0-beta.2",
3
+ "version": "0.6.0-beta.3",
4
4
  "description": "一键安装/升级 Trellis 并自动融合 skill-garden 强化包(默认 Claude + agents)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -68,9 +68,9 @@
68
68
  "commit-and-tag-version": "^12.7.3"
69
69
  },
70
70
  "flowerReleaseNotes": {
71
- "version": "0.6.0-beta.2",
71
+ "version": "0.6.0-beta.3",
72
72
  "source": "CHANGELOG.md",
73
- "body": "### 🐛 修复 Bug Fixes\n\n* 修复 Skill-Garden update 平台状态污染 ([787d4c7](https://github.com/SilentFlower/flower-trellis/commit/787d4c797834b4a0bb909e4eb20afe0be281fde1))",
73
+ "body": "### ✨ 新功能 Features\n\n* **check-all:** 同步 Check-All fact-status 文档修复规则 ([8b48a5c](https://github.com/SilentFlower/flower-trellis/commit/8b48a5c5b074ad614351726a85525853352da990))\n* **update:** 增加启动更新提示节流控制 ([d341841](https://github.com/SilentFlower/flower-trellis/commit/d34184194e260359a427da72205621579577ab65))\n\n\n### 🐛 修复 Bug Fixes\n\n* **trellis:** 同步 untracked_flow 入口限制 ([4732835](https://github.com/SilentFlower/flower-trellis/commit/47328352035b9d4c5e9972a95dd457e4d7eb9217))\n* **trellis:** 衔接 untracked 检查、规范更新和推送链路 ([9f1e3b1](https://github.com/SilentFlower/flower-trellis/commit/9f1e3b16af627b9c95a373047a146d07c6ecda72))",
74
74
  "truncated": false
75
75
  },
76
76
  "optionalDependencies": {
@@ -139,8 +139,8 @@ def _ai_instruction(data: dict, command: str | None) -> str | None:
139
139
  instruction = ai.get("instruction")
140
140
  if ai.get("mode") == "ask" and command:
141
141
  if _has_release_notes(data):
142
- return "先展示 release_notes 摘要和 recommended_command,再询问用户确认;确认前禁止执行 recommended_command。"
143
- return "先展示 recommended_command,再询问用户确认;确认前禁止执行 recommended_command。"
142
+ return "先展示 release_notes 摘要和 recommended_command,再询问用户确认;确认前禁止执行 recommended_command;用户拒绝本次升级时执行 snooze_command,明确跳过时执行 skip_command。"
143
+ return "先展示 recommended_command,再询问用户确认;确认前禁止执行 recommended_command;用户拒绝本次升级时执行 snooze_command,明确跳过时执行 skip_command。"
144
144
  return instruction if isinstance(instruction, str) and instruction else None
145
145
 
146
146
 
@@ -150,6 +150,7 @@ def _format_context(data: dict) -> str:
150
150
  project = data.get("project") or {}
151
151
  remote = data.get("remote") or {}
152
152
  ai = data.get("ai") or {}
153
+ prompt = data.get("prompt") or {}
153
154
  safety = data.get("safety") or {}
154
155
  out_of_sync_reasons = project.get("outOfSyncReasons") or []
155
156
  current_trellis = current.get("bundledTrellisVersion")
@@ -175,6 +176,14 @@ def _format_context(data: dict) -> str:
175
176
  command = (data.get("commands") or {}).get("recommended") or ai.get("command")
176
177
  if command:
177
178
  lines.append(f"recommended_command: {command}")
179
+ prompt_commands = prompt.get("commands") if isinstance(prompt, dict) else {}
180
+ if isinstance(prompt_commands, dict):
181
+ snooze_command = prompt_commands.get("snooze")
182
+ skip_command = prompt_commands.get("skip")
183
+ if isinstance(snooze_command, str) and snooze_command:
184
+ lines.append(f"snooze_command: {snooze_command}")
185
+ if isinstance(skip_command, str) and skip_command:
186
+ lines.append(f"skip_command: {skip_command}")
178
187
  if safety.get("reasons"):
179
188
  lines.append(f"safety_reasons: {', '.join(safety.get('reasons') or [])}")
180
189
  instruction = _ai_instruction(data, command)
@@ -190,8 +199,8 @@ def _system_message(data: dict) -> str:
190
199
  command = (data.get("commands") or {}).get("recommended") or ai.get("command")
191
200
  if ai.get("mode") == "ask" and command:
192
201
  if _has_release_notes(data):
193
- return "flower-trellis 发现可执行更新;必须先展示更新摘要并询问用户是否执行 recommended_command,确认前禁止运行。"
194
- return "flower-trellis 发现可执行更新;必须先询问用户是否执行 recommended_command,确认前禁止运行。"
202
+ return "flower-trellis 发现可执行更新;必须先展示更新摘要并询问用户是否执行 recommended_command,确认前禁止运行;用户拒绝时执行 snooze_command。"
203
+ return "flower-trellis 发现可执行更新;必须先询问用户是否执行 recommended_command,确认前禁止运行;用户拒绝时执行 snooze_command。"
195
204
  if command:
196
205
  return "flower-trellis 发现可执行更新;已注入 recommended_command。"
197
206
  return "flower-trellis update context injected"
package/src/cli.js CHANGED
@@ -73,7 +73,8 @@ function printHelp() {
73
73
  flower-trellis update [trellis flags] [flower flags] 升级 + 按新版本重新叠加
74
74
  flower-trellis self-check --json [--target <dir>] 输出启动更新检查 JSON
75
75
  flower-trellis self-update --target <dir> --yes 自更新 + 项目重叠加
76
- flower-trellis update-check <get|set|disable|enable> 管理启动更新策略
76
+ flower-trellis update-check <get|set|disable|enable|snooze|skip|reset>
77
+ 管理启动更新策略
77
78
  flower-trellis telemetry <status|enable|disable> 管理匿名安装遥测
78
79
  flower-trellis plugin 交互管理 Plugin、来源与 GitLab 授权
79
80
  flower-trellis uninstall [-y | --dry-run] 卸载 + 清理强化残留
@@ -93,7 +94,8 @@ flower 自有 flag:
93
94
  self-check --json 稳定输出检查 JSON
94
95
  self-update --yes [--dry-run] 执行或预览全局更新与项目 update
95
96
  update-check set --policy <off|notify|ask|auto> [--interval-hours <n>]
96
- update-check disable|enable 只切换 enabled,不覆盖已选 policy
97
+ update-check snooze [--hours <n>|--days <n>] 延后当前更新提示(默认 7 天)
98
+ update-check skip|reset 跳过当前提示或清空提示节流状态
97
99
 
98
100
  命令别名:flower-trellis 可简写为 ftl 或 ft(三者完全等价)。
99
101
  init / update 启动时会顺带检测 flower-trellis 自身是否有新版(联网、带超时,失败静默)。
@@ -17,6 +17,7 @@ export async function selfCheck(ctx) {
17
17
  }
18
18
  const result = await buildSelfCheck(ctx.target, {
19
19
  forceRemote,
20
+ recordPrompt: true,
20
21
  onRemoteCheck: () => reportTelemetry(ctx.target, "version_check"),
21
22
  });
22
23
  console.log(JSON.stringify(result, null, 2));
@@ -8,8 +8,13 @@ import {
8
8
  updateCheckCachePath,
9
9
  writeUpdateCheck,
10
10
  } from "../lib/manifest.js";
11
+ import {
12
+ buildSelfCheck,
13
+ DEFAULT_UPDATE_PROMPT_SNOOZE_HOURS,
14
+ } from "../lib/self-check.js";
11
15
 
12
16
  const POLICIES = new Set(["off", "notify", "ask", "auto"]);
17
+ const ACTIONABLE_STATUSES = new Set(["update_available", "project_out_of_sync"]);
13
18
 
14
19
  /** 读取 flag 后面的取值。 */
15
20
  function optionValue(args, name) {
@@ -18,6 +23,48 @@ function optionValue(args, name) {
18
23
  return args[index + 1] || null;
19
24
  }
20
25
 
26
+ /** 判断是否传入了某个 option。 */
27
+ function hasOption(args, name) {
28
+ return args.includes(name);
29
+ }
30
+
31
+ /** 解析正数小时参数。 */
32
+ function positiveHours(value, label) {
33
+ const hours = Number(value);
34
+ if (!Number.isFinite(hours) || hours <= 0) {
35
+ throw new Error(`${label} 必须是正数`);
36
+ }
37
+ return hours;
38
+ }
39
+
40
+ /** 解析 snooze 默认或显式时长。 */
41
+ function snoozeHours(args) {
42
+ const hoursText = optionValue(args, "--hours");
43
+ const daysText = optionValue(args, "--days");
44
+ const hasHours = hasOption(args, "--hours");
45
+ const hasDays = hasOption(args, "--days");
46
+ if (hasHours && hoursText === null) throw new Error("--hours 缺少取值");
47
+ if (hasDays && daysText === null) throw new Error("--days 缺少取值");
48
+ if (hasHours && hasDays) {
49
+ throw new Error("update-check snooze 不能同时使用 --hours 和 --days");
50
+ }
51
+ if (hasHours) return positiveHours(hoursText, "--hours");
52
+ if (hasDays) return positiveHours(daysText, "--days") * 24;
53
+ return DEFAULT_UPDATE_PROMPT_SNOOZE_HOURS;
54
+ }
55
+
56
+ /** 当前是否有可延后或跳过的更新提示。 */
57
+ async function currentPrompt(target) {
58
+ const result = await buildSelfCheck(target, {
59
+ writeCache: false,
60
+ ignorePromptSuppression: true,
61
+ });
62
+ if (!ACTIONABLE_STATUSES.has(result.status) || !result.prompt?.key) {
63
+ throw new Error("当前没有可延后或跳过的更新提示");
64
+ }
65
+ return result.prompt;
66
+ }
67
+
21
68
  /** 确保目标是 Trellis 项目。 */
22
69
  function assertTrellisProject(target) {
23
70
  if (!fs.existsSync(path.join(target, ".trellis"))) {
@@ -90,5 +137,44 @@ export async function updateCheck(ctx) {
90
137
  return;
91
138
  }
92
139
 
93
- throw new Error("update-check 只支持 get / set / disable / enable");
140
+ if (action === "snooze") {
141
+ const prompt = await currentPrompt(ctx.target);
142
+ const hours = snoozeHours(args);
143
+ const until = new Date(Date.now() + hours * 60 * 60 * 1000).toISOString();
144
+ writeUpdateCheck(ctx.target, {
145
+ promptSuppressedKey: prompt.key,
146
+ promptSuppressedUntil: until,
147
+ promptSuppressionReason: "snooze",
148
+ });
149
+ console.log(` ✓ 当前更新提示已延后到 ${until}`);
150
+ console.log(JSON.stringify(readUpdateCheck(ctx.target), null, 2));
151
+ return;
152
+ }
153
+
154
+ if (action === "skip") {
155
+ const prompt = await currentPrompt(ctx.target);
156
+ writeUpdateCheck(ctx.target, {
157
+ promptSuppressedKey: prompt.key,
158
+ promptSuppressedUntil: null,
159
+ promptSuppressionReason: "skip",
160
+ });
161
+ console.log(" ✓ 当前更新提示已跳过;新版本或项目版本差异变化后会再次提示");
162
+ console.log(JSON.stringify(readUpdateCheck(ctx.target), null, 2));
163
+ return;
164
+ }
165
+
166
+ if (action === "reset") {
167
+ writeUpdateCheck(ctx.target, {
168
+ lastPromptedAt: null,
169
+ lastPromptedKey: null,
170
+ promptSuppressedUntil: null,
171
+ promptSuppressedKey: null,
172
+ promptSuppressionReason: null,
173
+ });
174
+ console.log(" ✓ 更新提示节流状态已清空");
175
+ console.log(JSON.stringify(readUpdateCheck(ctx.target), null, 2));
176
+ return;
177
+ }
178
+
179
+ throw new Error("update-check 只支持 get / set / disable / enable / snooze / skip / reset");
94
180
  }
@@ -24,7 +24,13 @@ const UPDATE_CHECK_CACHE_KEYS = new Set([
24
24
  "lastReleaseNotes",
25
25
  "lastStatus",
26
26
  "lastErrorCode",
27
+ "lastPromptedAt",
28
+ "lastPromptedKey",
29
+ "promptSuppressedUntil",
30
+ "promptSuppressedKey",
31
+ "promptSuppressionReason",
27
32
  ]);
33
+ const UPDATE_PROMPT_SUPPRESSION_REASONS = new Set(["snooze", "skip"]);
28
34
  const DEFAULT_UPDATE_CHECK_POLICY = {
29
35
  enabled: true,
30
36
  policy: "ask",
@@ -36,6 +42,11 @@ const DEFAULT_UPDATE_CHECK_CACHE = {
36
42
  lastReleaseNotes: null,
37
43
  lastStatus: null,
38
44
  lastErrorCode: null,
45
+ lastPromptedAt: null,
46
+ lastPromptedKey: null,
47
+ promptSuppressedUntil: null,
48
+ promptSuppressedKey: null,
49
+ promptSuppressionReason: null,
39
50
  };
40
51
  const DEFAULT_UPDATE_CHECK = {
41
52
  ...DEFAULT_UPDATE_CHECK_POLICY,
@@ -155,7 +166,7 @@ function normalizeUpdateCheckPolicy(value) {
155
166
  * 归一化启动更新检查运行缓存字段。
156
167
  *
157
168
  * @param {object|null|undefined} value 原始缓存字段
158
- * @returns {{lastCheckedAt:string|null,lastRemote:object|null,lastReleaseNotes:object|null,lastStatus:string|null,lastErrorCode:string|null}} 归一化后的缓存
169
+ * @returns {{lastCheckedAt:string|null,lastRemote:object|null,lastReleaseNotes:object|null,lastStatus:string|null,lastErrorCode:string|null,lastPromptedAt:string|null,lastPromptedKey:string|null,promptSuppressedUntil:string|null,promptSuppressedKey:string|null,promptSuppressionReason:string|null}} 归一化后的缓存
159
170
  */
160
171
  function normalizeUpdateCheckCache(value) {
161
172
  const raw = value && typeof value === "object" ? value : {};
@@ -165,6 +176,17 @@ function normalizeUpdateCheckCache(value) {
165
176
  lastReleaseNotes: normalizeLastReleaseNotes(raw.lastReleaseNotes),
166
177
  lastStatus: typeof raw.lastStatus === "string" ? raw.lastStatus : null,
167
178
  lastErrorCode: typeof raw.lastErrorCode === "string" ? raw.lastErrorCode : null,
179
+ lastPromptedAt: typeof raw.lastPromptedAt === "string" ? raw.lastPromptedAt : null,
180
+ lastPromptedKey: typeof raw.lastPromptedKey === "string" ? raw.lastPromptedKey : null,
181
+ promptSuppressedUntil: typeof raw.promptSuppressedUntil === "string"
182
+ ? raw.promptSuppressedUntil
183
+ : null,
184
+ promptSuppressedKey: typeof raw.promptSuppressedKey === "string"
185
+ ? raw.promptSuppressedKey
186
+ : null,
187
+ promptSuppressionReason: UPDATE_PROMPT_SUPPRESSION_REASONS.has(raw.promptSuppressionReason)
188
+ ? raw.promptSuppressionReason
189
+ : null,
168
190
  };
169
191
  }
170
192
 
@@ -372,7 +394,7 @@ export function readLegacyManifestStatus(target) {
372
394
  * 用户策略字段启用保守默认值;缓存字段只保留结构化摘要,避免把网络错误细节写入项目。
373
395
  *
374
396
  * @param {object|null|undefined} value 原始 updateCheck 字段
375
- * @returns {{enabled:boolean,policy:string,intervalHours:number,lastCheckedAt:string|null,lastRemote:object|null,lastReleaseNotes:object|null,lastStatus:string|null,lastErrorCode:string|null}} 归一化后的 updateCheck
397
+ * @returns {{enabled:boolean,policy:string,intervalHours:number,lastCheckedAt:string|null,lastRemote:object|null,lastReleaseNotes:object|null,lastStatus:string|null,lastErrorCode:string|null,lastPromptedAt:string|null,lastPromptedKey:string|null,promptSuppressedUntil:string|null,promptSuppressedKey:string|null,promptSuppressionReason:string|null}} 归一化后的 updateCheck
376
398
  */
377
399
  export function normalizeUpdateCheck(value) {
378
400
  return {
@@ -15,6 +15,10 @@ import { SKILL_GARDEN_PLUGIN_ID } from "../builtin-plugins/skill-garden/provider
15
15
 
16
16
  /** 上游 trellis update 支持的批量冲突处理参数。 */
17
17
  const CONFLICT_FLAGS = new Set(["-f", "--force", "-s", "--skip-all", "-n", "--create-new"]);
18
+ /** 同一更新提示默认冷却时长,避免每次会话启动都阻塞用户。 */
19
+ export const DEFAULT_UPDATE_PROMPT_COOLDOWN_HOURS = 24;
20
+ /** 用户选择稍后时的默认延后时长。 */
21
+ export const DEFAULT_UPDATE_PROMPT_SNOOZE_HOURS = 24 * 7;
18
22
 
19
23
  /** 给命令建议使用的保守 shell 引号。 */
20
24
  function shellQuote(value) {
@@ -273,6 +277,191 @@ export function selfUpdateCommand(target, options = {}) {
273
277
  return args.map(shellQuote).join(" ");
274
278
  }
275
279
 
280
+ /**
281
+ * 生成 update-check 提示管理命令。
282
+ *
283
+ * @param {string} target 目标项目根
284
+ * @returns {{snooze:string,skip:string,reset:string}} 可执行命令
285
+ */
286
+ function promptManagementCommands(target) {
287
+ const base = ["flower-trellis", "update-check"];
288
+ const targetArgs = ["--target", target];
289
+ return {
290
+ snooze: [...base, "snooze", ...targetArgs].map(shellQuote).join(" "),
291
+ skip: [...base, "skip", ...targetArgs].map(shellQuote).join(" "),
292
+ reset: [...base, "reset", ...targetArgs].map(shellQuote).join(" "),
293
+ };
294
+ }
295
+
296
+ /** 把版本差异字段转为稳定 prompt key 片段。 */
297
+ function keyPart(value) {
298
+ return String(value || "unknown").replace(/\s+/g, "_");
299
+ }
300
+
301
+ /**
302
+ * 为可执行更新结果生成提示 key。
303
+ *
304
+ * @param {object} result self-check 结果
305
+ * @returns {string|null} 当前提示 key
306
+ */
307
+ function updatePromptKey(result) {
308
+ if (result.status === "update_available" && result.recommendation?.version) {
309
+ return `update:${keyPart(result.recommendation.tag)}:${keyPart(result.recommendation.version)}`;
310
+ }
311
+ if (result.status === "project_out_of_sync") {
312
+ const current = result.current || {};
313
+ const project = result.project || {};
314
+ return [
315
+ "project",
316
+ `flower:${keyPart(project.flowerVersion)}>${keyPart(current.flowerVersion)}`,
317
+ `trellis:${keyPart(project.trellisVersion)}>${keyPart(current.bundledTrellisVersion)}`,
318
+ ].join(":");
319
+ }
320
+ return null;
321
+ }
322
+
323
+ /** 解析 ISO 时间,无效时返回 null。 */
324
+ function parseTimeMs(value) {
325
+ const ms = Date.parse(String(value || ""));
326
+ return Number.isFinite(ms) ? ms : null;
327
+ }
328
+
329
+ /** 给时间加指定小时。 */
330
+ function addHours(date, hours) {
331
+ return new Date(date.getTime() + hours * 60 * 60 * 1000);
332
+ }
333
+
334
+ /**
335
+ * 判断当前提示是否应被延后、跳过或冷却抑制。
336
+ *
337
+ * @param {object} updateCheck 归一化 updateCheck
338
+ * @param {string} promptKey 当前提示 key
339
+ * @param {Date} now 当前时间
340
+ * @returns {{suppressed:boolean,reason:string|null,until:string|null}} 抑制结果
341
+ */
342
+ function promptSuppression(updateCheck, promptKey, now) {
343
+ if (updateCheck.promptSuppressedKey === promptKey) {
344
+ if (updateCheck.promptSuppressionReason === "skip") {
345
+ return { suppressed: true, reason: "skip", until: null };
346
+ }
347
+ if (updateCheck.promptSuppressionReason === "snooze") {
348
+ const suppressedUntilMs = parseTimeMs(updateCheck.promptSuppressedUntil);
349
+ if (suppressedUntilMs && suppressedUntilMs > now.getTime()) {
350
+ return {
351
+ suppressed: true,
352
+ reason: "snooze",
353
+ until: new Date(suppressedUntilMs).toISOString(),
354
+ };
355
+ }
356
+ }
357
+ }
358
+
359
+ if (updateCheck.lastPromptedKey === promptKey) {
360
+ const lastPromptedMs = parseTimeMs(updateCheck.lastPromptedAt);
361
+ if (lastPromptedMs) {
362
+ const nextPromptAt = addHours(
363
+ new Date(lastPromptedMs),
364
+ DEFAULT_UPDATE_PROMPT_COOLDOWN_HOURS,
365
+ );
366
+ if (nextPromptAt.getTime() > now.getTime()) {
367
+ return {
368
+ suppressed: true,
369
+ reason: "cooldown",
370
+ until: nextPromptAt.toISOString(),
371
+ };
372
+ }
373
+ }
374
+ }
375
+
376
+ return { suppressed: false, reason: null, until: null };
377
+ }
378
+
379
+ /**
380
+ * 构造提示节流诊断对象。
381
+ *
382
+ * @param {string} target 目标项目根
383
+ * @param {object} updateCheck 归一化 updateCheck
384
+ * @param {string} promptKey 当前提示 key
385
+ * @param {object} suppression 抑制结果
386
+ * @returns {object} prompt 字段
387
+ */
388
+ function promptInfo(target, updateCheck, promptKey, suppression) {
389
+ return {
390
+ key: promptKey,
391
+ suppressed: suppression.suppressed,
392
+ reason: suppression.reason,
393
+ until: suppression.until,
394
+ lastPromptedAt: updateCheck.lastPromptedAt,
395
+ lastPromptedKey: updateCheck.lastPromptedKey,
396
+ suppressedKey: updateCheck.promptSuppressedKey,
397
+ suppressionReason: updateCheck.promptSuppressionReason,
398
+ cooldownHours: DEFAULT_UPDATE_PROMPT_COOLDOWN_HOURS,
399
+ snoozeHours: DEFAULT_UPDATE_PROMPT_SNOOZE_HOURS,
400
+ commands: promptManagementCommands(target),
401
+ };
402
+ }
403
+
404
+ /**
405
+ * 对可执行更新结果应用提示节流。
406
+ *
407
+ * 远程检查节流只回答“是否知道有更新”;这里单独回答“本次会话是否还要打扰用户”。
408
+ *
409
+ * @param {string} target 目标项目根
410
+ * @param {object} result 可执行 self-check 结果
411
+ * @param {{now:Date,writeCache:boolean,projectEvidence:object|null,recordPrompt:boolean,ignorePromptSuppression:boolean}} options 提示选项
412
+ * @returns {object} 应返回给 self-check 的结果
413
+ */
414
+ function applyPromptPolicy(target, result, options) {
415
+ const promptKey = updatePromptKey(result);
416
+ if (!promptKey) return result;
417
+
418
+ const bypassSuppression = options.ignorePromptSuppression || result.ai?.mode === "auto";
419
+ const suppression = bypassSuppression
420
+ ? { suppressed: false, reason: null, until: null }
421
+ : promptSuppression(result.updateCheck, promptKey, options.now);
422
+
423
+ if (suppression.suppressed) {
424
+ return {
425
+ ...result,
426
+ status: "skipped",
427
+ reason: `prompt_${suppression.reason}`,
428
+ prompt: promptInfo(target, result.updateCheck, promptKey, suppression),
429
+ suppressedAction: {
430
+ status: result.status,
431
+ reason: result.reason,
432
+ recommendation: result.recommendation,
433
+ commands: result.commands,
434
+ releaseNotes: result.releaseNotes,
435
+ },
436
+ recommendation: null,
437
+ releaseNotes: null,
438
+ commands: {},
439
+ safety: null,
440
+ ai: null,
441
+ };
442
+ }
443
+
444
+ if (options.recordPrompt && options.writeCache && options.projectEvidence) {
445
+ const updateCheck = writeUpdateCheck(target, {
446
+ lastPromptedAt: options.now.toISOString(),
447
+ lastPromptedKey: promptKey,
448
+ promptSuppressedUntil: null,
449
+ promptSuppressedKey: null,
450
+ promptSuppressionReason: null,
451
+ });
452
+ return {
453
+ ...result,
454
+ updateCheck,
455
+ prompt: promptInfo(target, updateCheck, promptKey, suppression),
456
+ };
457
+ }
458
+
459
+ return {
460
+ ...result,
461
+ prompt: promptInfo(target, result.updateCheck, promptKey, suppression),
462
+ };
463
+ }
464
+
276
465
  /** 根据 policy 和安全检查结果生成 AI 动作指令。 */
277
466
  function actionForPolicy(policy, command, safety) {
278
467
  if (policy === "notify") {
@@ -309,9 +498,9 @@ function withAction(target, policy, result, command) {
309
498
  }
310
499
 
311
500
  /** 生成项目重叠加建议结果。 */
312
- function projectOutOfSyncResult(base, target, remotePatch = {}, releaseNotes = null) {
501
+ function projectOutOfSyncResult(base, target, remotePatch = {}, releaseNotes = null, promptOptions = null) {
313
502
  const command = selfUpdateCommand(target, { projectOnly: true });
314
- return withAction(
503
+ const result = withAction(
315
504
  target,
316
505
  base.policy,
317
506
  {
@@ -327,6 +516,7 @@ function projectOutOfSyncResult(base, target, remotePatch = {}, releaseNotes = n
327
516
  },
328
517
  command,
329
518
  );
519
+ return promptOptions ? applyPromptPolicy(target, result, promptOptions) : result;
330
520
  }
331
521
 
332
522
  /** 计算 auto 策略安全门槛。 */
@@ -354,12 +544,14 @@ export function safetyState(target, status, command) {
354
544
  * 构建启动自更新检查结果。
355
545
  *
356
546
  * @param {string} target 目标项目根
357
- * @param {{writeCache?: boolean, forceRemote?: boolean, fetchMetadata?: () => Promise<object|null>,onRemoteCheck?:()=>Promise<unknown>}} options 检查选项
547
+ * @param {{writeCache?: boolean, forceRemote?: boolean, fetchMetadata?: () => Promise<object|null>,onRemoteCheck?:()=>Promise<unknown>,recordPrompt?:boolean,ignorePromptSuppression?:boolean}} options 检查选项
358
548
  * @returns {Promise<object>} 结构化检查结果
359
549
  */
360
550
  export async function buildSelfCheck(target, options = {}) {
361
551
  const writeCache = options.writeCache !== false;
362
552
  const forceRemote = options.forceRemote === true;
553
+ const recordPrompt = options.recordPrompt === true;
554
+ const ignorePromptSuppression = options.ignorePromptSuppression === true;
363
555
  const fetchMetadata = typeof options.fetchMetadata === "function"
364
556
  ? options.fetchMetadata
365
557
  : fetchPackageUpdateMetadata;
@@ -390,6 +582,13 @@ export async function buildSelfCheck(target, options = {}) {
390
582
  projectOutOfSyncReasons.push("trellis_version_mismatch");
391
583
  }
392
584
  const projectOutOfSync = projectOutOfSyncReasons.length > 0;
585
+ const promptOptions = {
586
+ now,
587
+ writeCache,
588
+ projectEvidence,
589
+ recordPrompt,
590
+ ignorePromptSuppression,
591
+ };
393
592
 
394
593
  const base = {
395
594
  status: "up_to_date",
@@ -452,7 +651,7 @@ export async function buildSelfCheck(target, options = {}) {
452
651
  if (recommendation) {
453
652
  const command = selfUpdateCommand(absoluteTarget);
454
653
  const releaseNotesRange = updateReleaseNotesRange(currentFlower, recommendation);
455
- return withAction(
654
+ const result = withAction(
456
655
  absoluteTarget,
457
656
  updateCheck.policy,
458
657
  {
@@ -470,6 +669,7 @@ export async function buildSelfCheck(target, options = {}) {
470
669
  },
471
670
  command,
472
671
  );
672
+ return applyPromptPolicy(absoluteTarget, result, promptOptions);
473
673
  }
474
674
  if (projectOutOfSync) {
475
675
  const releaseNotesRange = projectReleaseNotesRange(projectFlower, currentFlower);
@@ -485,7 +685,7 @@ export async function buildSelfCheck(target, options = {}) {
485
685
  tags,
486
686
  fromCache: true,
487
687
  skipped: true,
488
- }, releaseNotes);
688
+ }, releaseNotes, promptOptions);
489
689
  }
490
690
  return {
491
691
  ...base,
@@ -512,6 +712,7 @@ export async function buildSelfCheck(target, options = {}) {
512
712
  absoluteTarget,
513
713
  remotePatch,
514
714
  cachedReleaseNotes(updateCheck, releaseNotesRange),
715
+ promptOptions,
515
716
  );
516
717
  }
517
718
  return {
@@ -539,7 +740,7 @@ export async function buildSelfCheck(target, options = {}) {
539
740
 
540
741
  if (!recommendation) {
541
742
  if (projectOutOfSync) {
542
- return projectOutOfSyncResult(resultBase, absoluteTarget, { tags }, releaseNotes);
743
+ return projectOutOfSyncResult(resultBase, absoluteTarget, { tags }, releaseNotes, promptOptions);
543
744
  }
544
745
  return {
545
746
  ...resultBase,
@@ -549,7 +750,7 @@ export async function buildSelfCheck(target, options = {}) {
549
750
  }
550
751
 
551
752
  const command = selfUpdateCommand(absoluteTarget);
552
- return withAction(
753
+ const result = withAction(
553
754
  absoluteTarget,
554
755
  updateCheck.policy,
555
756
  {
@@ -566,4 +767,5 @@ export async function buildSelfCheck(target, options = {}) {
566
767
  },
567
768
  command,
568
769
  );
770
+ return applyPromptPolicy(absoluteTarget, result, promptOptions);
569
771
  }