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 +17 -6
- package/enhancements/0.6/.agents/skills/trellis-check-all/references/document-drift-auto-remediation.md +7 -3
- package/enhancements/0.6/.claude/skills/trellis-check-all/references/document-drift-auto-remediation.md +7 -3
- package/enhancements/0.6/overrides/conflicts.json +4 -1
- package/enhancements/0.6/overrides/patches/workflow/intent-routing/request-triage/content.md +1 -0
- package/enhancements/0.6/overrides/patches/workflow/state-no-task/content.md +2 -1
- package/enhancements/0.6/overrides/patches/workflow/state-untracked/content.md +1 -1
- package/enhancements/0.6/scripts/untracked_flow.py +76 -3
- package/enhancements/MANIFEST.json +2 -2
- package/package.json +3 -3
- package/src/assets/flower_update_hook.py +13 -4
- package/src/cli.js +4 -2
- package/src/commands/self-check.js +1 -0
- package/src/commands/update-check.js +87 -1
- package/src/lib/manifest.js +24 -2
- package/src/lib/self-check.js +209 -7
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` | 管理 `.
|
|
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
|
-
策略保存在 `.
|
|
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
|
-
|
|
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
|
-
"
|
|
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.",
|
package/enhancements/0.6/overrides/patches/workflow/intent-routing/request-triage/content.md
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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":
|
|
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(
|
|
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-
|
|
2
|
+
"syncedAt": "2026-08-04T02:44:38.844Z",
|
|
3
3
|
"syncedFrom": "vendor/skill-garden",
|
|
4
|
-
"sourceCommit": "
|
|
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.
|
|
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.
|
|
71
|
+
"version": "0.6.0-beta.3",
|
|
72
72
|
"source": "CHANGELOG.md",
|
|
73
|
-
"body": "### 🐛 修复 Bug Fixes\n\n*
|
|
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
|
|
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 自身是否有新版(联网、带超时,失败静默)。
|
|
@@ -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
|
-
|
|
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
|
}
|
package/src/lib/manifest.js
CHANGED
|
@@ -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 {
|
package/src/lib/self-check.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|