flower-trellis 0.2.5-beta.2 → 0.3.0-beta.1

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
@@ -96,7 +96,7 @@ flower banner → 平台多选菜单 → Trellis 原生交互(模板 / monorepo
96
96
  ```
97
97
 
98
98
  - **统一品牌头部**:Trellis 子进程在伪终端(`node-pty`)中运行,其原生的模板 / monorepo / 冲突等交互完整保留,但重复打印的启动 banner 被过滤,全程只呈现一个 flower banner。
99
- - **按平台铺设技能**:Claude 铺到 `.claude/skills`,Codex / Gemini 等铺到 `.agents/skills`;并对 codex 做后处理(注释 `config.toml` 的 `[features.multi_agent_v2]`、补全 `hooks.json` `SessionStart`)。
99
+ - **按平台铺设技能**:Claude 铺到 `.claude/skills`,Codex / Gemini 等铺到 `.agents/skills`;并对 codex 做后处理(兼容清理旧 `config.toml` 的 `[features.multi_agent_v2]`,在保留上游 hooks 的基础上补全 `SessionStart`)。
100
100
  - **幂等执行**:`workflow.md` 注入前先按 `BEGIN/END` 标记清除旧块再重注入(块数恒定,不会翻倍,首次注入前备份 `.bak`);技能文件覆盖式铺设,并通过 `.trellis/.flower-manifest.json` 记录已铺路径,升级时删除已淘汰项。
101
101
  - **安全中止**:`Ctrl+C` 取消后不会继续叠加。
102
102
 
@@ -44,7 +44,7 @@ Step 5 写入已确认的任务进度快照,并补齐运行后字段
44
44
  Step 6 输出结果
45
45
  ```
46
46
 
47
- 除非出现“计划变化、执行失败重试、用户调整 snapshot、父仓出现额外 dirty 文件、merge 冲突”等情况,否则不要在执行中途追加新的确认问题。
47
+ 除非出现“计划变化、执行失败重试、用户调整 snapshot、父仓 staged/冲突/task 文件预脏等 bookkeeping 安全条件不满足、merge 冲突”等情况,否则不要在执行中途追加新的确认问题。
48
48
 
49
49
  ---
50
50
 
@@ -120,7 +120,12 @@ git log <base_branch>..HEAD --oneline
120
120
  - 有未合并状态、rebase 状态或 merge 冲突残留:立即停止,展示状态;不要继续生成执行计划。
121
121
  - 当前分支为空、detached HEAD、或无法确认分支:停止并说明原因。
122
122
  - dirty 文件必须按来源分组:**AI 本轮编辑** 与 **未识别 dirty 文件**。未识别 dirty 文件默认不纳入提交计划。
123
- - 如果父仓(含 `.trellis/` 的仓库)后续要写 snapshot,先记录父仓当前 `git status --short`,供 Step 5 判断是否有额外 dirty 文件。
123
+ - 如果父仓(含 `.trellis/` 的仓库)后续要写 snapshot,先记录父仓当前 `git status --porcelain`,供 Step 5 判断:
124
+ - 是否存在未合并 / 冲突状态;
125
+ - 是否存在与本次 bookkeeping 无关的 staged 文件;
126
+ - `<task_dir>/task.json` 是否在写入前已经 dirty;
127
+ - reconfigure 场景下 `.trellis/config.yaml` 是否在写入前已经 dirty 且未在统一计划中确认。
128
+ 父仓存在无关、未暂存 dirty 文件本身不阻塞 snapshot;这些文件只需要在计划或结果中提示会保留未提交。
124
129
 
125
130
  ---
126
131
 
@@ -225,6 +230,8 @@ commit-only 模式下,字段名仍保持 `pushed_commits` 以兼容恢复逻
225
230
  ### 父仓 bookkeeping
226
231
 
227
232
  - <跳过原因,或仅提交 `<task_dir>/task.json`>
233
+ - 无关未暂存 dirty:<list 或 无>(保留未提交,不阻塞 snapshot commit)
234
+ - 无关 staged / 冲突 / 目标文件预脏:<list 或 无>(有则停止,需处理后重新计划)
228
235
 
229
236
  确认后将按上述计划执行。回复 `ok` / `行` / `确认` 执行;回复 `skip snapshot` 跳过快照;回复修改意见则先更新计划;回复 `manual` / `我自己来` 则停止。
230
237
  ```
@@ -334,6 +341,15 @@ git checkout <current_branch>
334
341
  - `pushed_commits`:各 package 的短 hash
335
342
  - `notes`:保留已确认 notes;commit-only 模式追加“本地已提交,未推送”
336
343
 
344
+ 写入前先复核目标文件:
345
+
346
+ ```bash
347
+ git status --porcelain -- <task_json_path> [".trellis/config.yaml"]
348
+ ```
349
+
350
+ - `<task_json_path>` 如果在本次写入前已经 dirty,且该 dirty 未在统一计划中确认:立即停止并说明原因,避免覆盖或混入别人对同一任务文件的修改。
351
+ - reconfigure 场景下,`.trellis/config.yaml` 如果在本次回写前已经 dirty,且未在统一计划中确认:立即停止并说明原因。
352
+
337
353
  写入方式:
338
354
 
339
355
  1. 读 `<task_dir>/task.json`
@@ -349,15 +365,21 @@ git checkout <current_branch>
349
365
  写完 snapshot 后,在父仓根目录执行:
350
366
 
351
367
  ```bash
352
- git status --short
368
+ git status --porcelain
369
+ git diff --name-only --cached
353
370
  ```
354
371
 
355
- 只允许自动处理统一计划中确认过的文件:
372
+ bookkeeping 的自动处理范围只允许统一计划中确认过的文件:
356
373
 
357
374
  - `<task_dir>/task.json`
358
375
  - 如果 Step 4 选择了 reconfigure:`.trellis/config.yaml`
359
376
 
360
- 如果父仓 status 显示其它改动,立即停止并询问用户如何处理。不要静默打包。
377
+ 检查规则:
378
+
379
+ - 如果存在未合并路径、rebase 状态或 merge 冲突残留:立即停止,展示状态。
380
+ - 如果 `git diff --name-only --cached` 显示除上述允许文件之外的 staged 文件:立即停止,说明这些 staged 文件会被普通 commit 混入,必须先处理或重新确认。
381
+ - 如果存在除上述允许文件之外的**未暂存** dirty 文件:不要阻塞;保留它们未暂存,并在结果中提示“未纳入 snapshot commit”。
382
+ - 如果允许文件之外的 dirty 同时被 staged 和 unstaged 修改,按 staged 风险处理:立即停止。
361
383
 
362
384
  如果父仓也是本次业务仓库,snapshot / config bookkeeping 仍然使用单独 commit,不和业务 commit 混合。
363
385
 
@@ -365,9 +387,11 @@ git status --short
365
387
 
366
388
  ```bash
367
389
  git add <task_json_path> [".trellis/config.yaml"]
368
- git commit -m "chore(task): update <task_name> push snapshot"
390
+ git commit --only -m "chore(task): update <task_name> push snapshot" -- <task_json_path> [".trellis/config.yaml"]
369
391
  ```
370
392
 
393
+ `git commit --only ... -- <paths>` 的目的是把 bookkeeping commit 限定到当前任务的 `task.json`(以及已确认的 `config.yaml`),即使父仓还有无关未暂存 dirty 文件,也不会把它们带入本次提交。执行前仍必须确认 staged 区没有无关文件;如果本地 Git 对 `--only` 参数不兼容,只有在 staged 区确认仅包含上述允许文件时,才可退回普通 `git commit -m ...`。
394
+
371
395
  检查父仓是否配置 remote:
372
396
 
373
397
  ```bash
@@ -393,6 +417,7 @@ git remote -v | grep -E "^origin\s+"
393
417
 
394
418
  任务进度快照:已写入 `<task_dir>/task.json`,完成 Step 1-3,下一步 Step 5。
395
419
  父仓同步:已提交并推送 `chore(task): update <task_name> push snapshot`。
420
+ 父仓存在未暂存 dirty 时补充:这些文件已保留未暂存,未纳入 snapshot commit:<list>
396
421
  ```
397
422
 
398
423
  如果部分仓库已成功、后续失败,必须明确列出:
@@ -410,7 +435,7 @@ git remote -v | grep -E "^origin\s+"
410
435
  2. **未识别 dirty 文件隔离** — 默认不纳入提交,除非用户明确确认。
411
436
  3. **执行前复核** — git 状态与计划不一致时停止,不临时扩展范围。
412
437
  4. **snapshot 合并确认** — 语义字段执行前确认,运行后字段执行后补齐。
413
- 5. **父仓 bookkeeping 隔离** — 只提交已确认的 `task.json` / `config.yaml`,其它 dirty 文件必须询问。
438
+ 5. **父仓 bookkeeping 隔离** — 只提交已确认的 `task.json` / `config.yaml`;无关未暂存 dirty 文件保留未提交并提示,不阻塞;无关 staged 文件、冲突状态、目标文件预脏必须停止。
414
439
  6. **merge 冲突处理** — 冲突时暂停,不静默跳过。
415
440
  7. **主分支保护** — 目标分支是 `master` / `main` 时必须在计划中额外警告确认。
416
441
  8. **不使用 force push** — 始终使用普通 push。
@@ -426,6 +451,8 @@ git remote -v | grep -E "^origin\s+"
426
451
  - ❌ push 后临时追问 merge,而不是在计划中提前确认
427
452
  - ❌ snapshot 语义内容未确认就写入 task.json
428
453
  - ❌ 父仓 snapshot commit 混入业务代码提交
454
+ - ❌ 父仓存在无关未暂存 dirty 就跳过 snapshot,尽管可以只提交目标 `task.json`
455
+ - ❌ 父仓已有无关 staged 文件时仍执行 snapshot commit
429
456
  - ❌ merge 冲突后静默 abort 或跳过
430
457
  - ❌ force push 到当前分支或目标分支
431
458
  - ❌ 在目标分支上直接开发(只 merge,不在目标分支上改代码)
@@ -20,6 +20,8 @@ description: |
20
20
 
21
21
  个人 route 配置只决定“已获准执行后的模式”,不是开工授权。读取 `.trellis/.route-prefs.tmp` 前,必须确认当前 workflow 已允许进入对应 target:implement 需要任务已完成规划确认并处于 `in_progress`;check 需要已有本轮实现变更或用户明确要求检查。如果仍在 planning、等待用户确认,或用户表达“等一下 / 我再想想”,停止,不读取个人配置。
22
22
 
23
+ Codex inline mode 只表示主会话默认直接执行,不是 route 选项过滤器。即使当前上下文出现 `<codex-mode>inline...do not dispatch...</codex-mode>` 或 `workflow-state:in_progress-inline`,也不能推断“只能 inline”或跳过 subagent 选项;仍必须读取 `.trellis/.route-prefs.tmp`,或在无有效配置时展示正常 inline/subagent 选项。若本 skill 的紧邻路由决定是 subagent,本步骤允许主 agent dispatch 对应 implement/check sub-agent;禁止的是绕过 `trellis-route` 直接 dispatch。
24
+
23
25
  先判断本次路由目标:
24
26
 
25
27
  - `target=implement`:决定 `inline` / `subagent`。
@@ -239,6 +241,7 @@ OLD_CHECK=$(awk -F= '$1=="check"{print $2}' "$PREF_FILE" 2>/dev/null | tail -n 1
239
241
  5. **轻量 check 是隐藏逃生口**:只有用户明确请求 `light check` / `轻量检查` 时才可走轻量 `trellis-check`。
240
242
  6. **决策与执行分离**:本 skill 只输出指令,下一轮由主 agent 调工具。
241
243
  7. **严格执行用户选择**:路由结论一旦输出,主 agent 必须按指令执行,不可“出于谨慎”再换路径。
244
+ 8. **Codex inline 不裁剪选项**:Codex inline 是默认执行模式,不是只能 inline 的强制模式;route 明确选中 subagent 时,本步骤可按 subagent 路径执行。
242
245
 
243
246
  ---
244
247
 
@@ -252,6 +255,7 @@ OLD_CHECK=$(awk -F= '$1=="check"{print $2}' "$PREF_FILE" 2>/dev/null | tail -n 1
252
255
  - `AskUserQuestion` / `request_user_input` 不可用时,记录为 inline 或 subagent 路径并继续。
253
256
  - 给 check 任何模式附加“跳过编译”指令。
254
257
  - 询问后忽视用户答案默认 subagent。
258
+ - 因 `<codex-mode>` 或 `in_progress-inline` 提到 inline,就自行把无配置 route 结果改成 inline 或隐藏 subagent 选项。
255
259
 
256
260
  ---
257
261
 
@@ -44,7 +44,7 @@ Step 5 写入已确认的任务进度快照,并补齐运行后字段
44
44
  Step 6 输出结果
45
45
  ```
46
46
 
47
- 除非出现“计划变化、执行失败重试、用户调整 snapshot、父仓出现额外 dirty 文件、merge 冲突”等情况,否则不要在执行中途追加新的确认问题。
47
+ 除非出现“计划变化、执行失败重试、用户调整 snapshot、父仓 staged/冲突/task 文件预脏等 bookkeeping 安全条件不满足、merge 冲突”等情况,否则不要在执行中途追加新的确认问题。
48
48
 
49
49
  ---
50
50
 
@@ -120,7 +120,12 @@ git log <base_branch>..HEAD --oneline
120
120
  - 有未合并状态、rebase 状态或 merge 冲突残留:立即停止,展示状态;不要继续生成执行计划。
121
121
  - 当前分支为空、detached HEAD、或无法确认分支:停止并说明原因。
122
122
  - dirty 文件必须按来源分组:**AI 本轮编辑** 与 **未识别 dirty 文件**。未识别 dirty 文件默认不纳入提交计划。
123
- - 如果父仓(含 `.trellis/` 的仓库)后续要写 snapshot,先记录父仓当前 `git status --short`,供 Step 5 判断是否有额外 dirty 文件。
123
+ - 如果父仓(含 `.trellis/` 的仓库)后续要写 snapshot,先记录父仓当前 `git status --porcelain`,供 Step 5 判断:
124
+ - 是否存在未合并 / 冲突状态;
125
+ - 是否存在与本次 bookkeeping 无关的 staged 文件;
126
+ - `<task_dir>/task.json` 是否在写入前已经 dirty;
127
+ - reconfigure 场景下 `.trellis/config.yaml` 是否在写入前已经 dirty 且未在统一计划中确认。
128
+ 父仓存在无关、未暂存 dirty 文件本身不阻塞 snapshot;这些文件只需要在计划或结果中提示会保留未提交。
124
129
 
125
130
  ---
126
131
 
@@ -225,6 +230,8 @@ commit-only 模式下,字段名仍保持 `pushed_commits` 以兼容恢复逻
225
230
  ### 父仓 bookkeeping
226
231
 
227
232
  - <跳过原因,或仅提交 `<task_dir>/task.json`>
233
+ - 无关未暂存 dirty:<list 或 无>(保留未提交,不阻塞 snapshot commit)
234
+ - 无关 staged / 冲突 / 目标文件预脏:<list 或 无>(有则停止,需处理后重新计划)
228
235
 
229
236
  确认后将按上述计划执行。回复 `ok` / `行` / `确认` 执行;回复 `skip snapshot` 跳过快照;回复修改意见则先更新计划;回复 `manual` / `我自己来` 则停止。
230
237
  ```
@@ -334,6 +341,15 @@ git checkout <current_branch>
334
341
  - `pushed_commits`:各 package 的短 hash
335
342
  - `notes`:保留已确认 notes;commit-only 模式追加“本地已提交,未推送”
336
343
 
344
+ 写入前先复核目标文件:
345
+
346
+ ```bash
347
+ git status --porcelain -- <task_json_path> [".trellis/config.yaml"]
348
+ ```
349
+
350
+ - `<task_json_path>` 如果在本次写入前已经 dirty,且该 dirty 未在统一计划中确认:立即停止并说明原因,避免覆盖或混入别人对同一任务文件的修改。
351
+ - reconfigure 场景下,`.trellis/config.yaml` 如果在本次回写前已经 dirty,且未在统一计划中确认:立即停止并说明原因。
352
+
337
353
  写入方式:
338
354
 
339
355
  1. 读 `<task_dir>/task.json`
@@ -349,15 +365,21 @@ git checkout <current_branch>
349
365
  写完 snapshot 后,在父仓根目录执行:
350
366
 
351
367
  ```bash
352
- git status --short
368
+ git status --porcelain
369
+ git diff --name-only --cached
353
370
  ```
354
371
 
355
- 只允许自动处理统一计划中确认过的文件:
372
+ bookkeeping 的自动处理范围只允许统一计划中确认过的文件:
356
373
 
357
374
  - `<task_dir>/task.json`
358
375
  - 如果 Step 4 选择了 reconfigure:`.trellis/config.yaml`
359
376
 
360
- 如果父仓 status 显示其它改动,立即停止并询问用户如何处理。不要静默打包。
377
+ 检查规则:
378
+
379
+ - 如果存在未合并路径、rebase 状态或 merge 冲突残留:立即停止,展示状态。
380
+ - 如果 `git diff --name-only --cached` 显示除上述允许文件之外的 staged 文件:立即停止,说明这些 staged 文件会被普通 commit 混入,必须先处理或重新确认。
381
+ - 如果存在除上述允许文件之外的**未暂存** dirty 文件:不要阻塞;保留它们未暂存,并在结果中提示“未纳入 snapshot commit”。
382
+ - 如果允许文件之外的 dirty 同时被 staged 和 unstaged 修改,按 staged 风险处理:立即停止。
361
383
 
362
384
  如果父仓也是本次业务仓库,snapshot / config bookkeeping 仍然使用单独 commit,不和业务 commit 混合。
363
385
 
@@ -365,9 +387,11 @@ git status --short
365
387
 
366
388
  ```bash
367
389
  git add <task_json_path> [".trellis/config.yaml"]
368
- git commit -m "chore(task): update <task_name> push snapshot"
390
+ git commit --only -m "chore(task): update <task_name> push snapshot" -- <task_json_path> [".trellis/config.yaml"]
369
391
  ```
370
392
 
393
+ `git commit --only ... -- <paths>` 的目的是把 bookkeeping commit 限定到当前任务的 `task.json`(以及已确认的 `config.yaml`),即使父仓还有无关未暂存 dirty 文件,也不会把它们带入本次提交。执行前仍必须确认 staged 区没有无关文件;如果本地 Git 对 `--only` 参数不兼容,只有在 staged 区确认仅包含上述允许文件时,才可退回普通 `git commit -m ...`。
394
+
371
395
  检查父仓是否配置 remote:
372
396
 
373
397
  ```bash
@@ -393,6 +417,7 @@ git remote -v | grep -E "^origin\s+"
393
417
 
394
418
  任务进度快照:已写入 `<task_dir>/task.json`,完成 Step 1-3,下一步 Step 5。
395
419
  父仓同步:已提交并推送 `chore(task): update <task_name> push snapshot`。
420
+ 父仓存在未暂存 dirty 时补充:这些文件已保留未暂存,未纳入 snapshot commit:<list>
396
421
  ```
397
422
 
398
423
  如果部分仓库已成功、后续失败,必须明确列出:
@@ -410,7 +435,7 @@ git remote -v | grep -E "^origin\s+"
410
435
  2. **未识别 dirty 文件隔离** — 默认不纳入提交,除非用户明确确认。
411
436
  3. **执行前复核** — git 状态与计划不一致时停止,不临时扩展范围。
412
437
  4. **snapshot 合并确认** — 语义字段执行前确认,运行后字段执行后补齐。
413
- 5. **父仓 bookkeeping 隔离** — 只提交已确认的 `task.json` / `config.yaml`,其它 dirty 文件必须询问。
438
+ 5. **父仓 bookkeeping 隔离** — 只提交已确认的 `task.json` / `config.yaml`;无关未暂存 dirty 文件保留未提交并提示,不阻塞;无关 staged 文件、冲突状态、目标文件预脏必须停止。
414
439
  6. **merge 冲突处理** — 冲突时暂停,不静默跳过。
415
440
  7. **主分支保护** — 目标分支是 `master` / `main` 时必须在计划中额外警告确认。
416
441
  8. **不使用 force push** — 始终使用普通 push。
@@ -426,6 +451,8 @@ git remote -v | grep -E "^origin\s+"
426
451
  - ❌ push 后临时追问 merge,而不是在计划中提前确认
427
452
  - ❌ snapshot 语义内容未确认就写入 task.json
428
453
  - ❌ 父仓 snapshot commit 混入业务代码提交
454
+ - ❌ 父仓存在无关未暂存 dirty 就跳过 snapshot,尽管可以只提交目标 `task.json`
455
+ - ❌ 父仓已有无关 staged 文件时仍执行 snapshot commit
429
456
  - ❌ merge 冲突后静默 abort 或跳过
430
457
  - ❌ force push 到当前分支或目标分支
431
458
  - ❌ 在目标分支上直接开发(只 merge,不在目标分支上改代码)
@@ -20,6 +20,8 @@ description: |
20
20
 
21
21
  个人 route 配置只决定“已获准执行后的模式”,不是开工授权。读取 `.trellis/.route-prefs.tmp` 前,必须确认当前 workflow 已允许进入对应 target:implement 需要任务已完成规划确认并处于 `in_progress`;check 需要已有本轮实现变更或用户明确要求检查。如果仍在 planning、等待用户确认,或用户表达“等一下 / 我再想想”,停止,不读取个人配置。
22
22
 
23
+ Codex inline mode 只表示主会话默认直接执行,不是 route 选项过滤器。即使当前上下文出现 `<codex-mode>inline...do not dispatch...</codex-mode>` 或 `workflow-state:in_progress-inline`,也不能推断“只能 inline”或跳过 subagent 选项;仍必须读取 `.trellis/.route-prefs.tmp`,或在无有效配置时展示正常 inline/subagent 选项。若本 skill 的紧邻路由决定是 subagent,本步骤允许主 agent dispatch 对应 implement/check sub-agent;禁止的是绕过 `trellis-route` 直接 dispatch。
24
+
23
25
  先判断本次路由目标:
24
26
 
25
27
  - `target=implement`:决定 `inline` / `subagent`。
@@ -239,6 +241,7 @@ OLD_CHECK=$(awk -F= '$1=="check"{print $2}' "$PREF_FILE" 2>/dev/null | tail -n 1
239
241
  5. **轻量 check 是隐藏逃生口**:只有用户明确请求 `light check` / `轻量检查` 时才可走轻量 `trellis-check`。
240
242
  6. **决策与执行分离**:本 skill 只输出指令,下一轮由主 agent 调工具。
241
243
  7. **严格执行用户选择**:路由结论一旦输出,主 agent 必须按指令执行,不可“出于谨慎”再换路径。
244
+ 8. **Codex inline 不裁剪选项**:Codex inline 是默认执行模式,不是只能 inline 的强制模式;route 明确选中 subagent 时,本步骤可按 subagent 路径执行。
242
245
 
243
246
  ---
244
247
 
@@ -252,6 +255,7 @@ OLD_CHECK=$(awk -F= '$1=="check"{print $2}' "$PREF_FILE" 2>/dev/null | tail -n 1
252
255
  - `AskUserQuestion` / `request_user_input` 不可用时,记录为 inline 或 subagent 路径并继续。
253
256
  - 给 check 任何模式附加“跳过编译”指令。
254
257
  - 询问后忽视用户答案默认 subagent。
258
+ - 因 `<codex-mode>` 或 `in_progress-inline` 提到 inline,就自行把无配置 route 结果改成 inline 或隐藏 subagent 选项。
255
259
 
256
260
  ---
257
261
 
@@ -1,6 +1,6 @@
1
1
  <!-- BEGIN skill-garden workflow-state in_progress_inline v0.6 -->
2
2
  HIGHEST PRIORITY SKILL-GARDEN STATE GUARD (in_progress-inline):
3
- Inline mode does not skip `trellis-route`: before implementation, route `implement` first; before checking, route `check` first. Only after the route selects inline should the main session run `trellis-before-dev`, edit code, or run `trellis-check` / `trellis-check-all`.
3
+ Inline mode does not skip or constrain `trellis-route`: before implementation, route `implement` first; before checking, route `check` first. The default inline flow is direct main-session work, but if the immediate route decision selects subagent, that decision permits dispatch for this step.
4
4
  After `trellis-check` / `trellis-check-all`, stop and report; do not run `/trellis:finish-work` unless the user explicitly asks after Phase 3.4 is complete.
5
5
  This guard overrides any lower `Flow: ... -> /trellis:finish-work` line in this state block.
6
6
  At Phase 3.4, code commit/push still goes through `trellis-push` (commit-only for commit-without-push); never bare `git commit`/`git push` on code (hub: Code Commit Confirmation Gate).
@@ -1,7 +1,7 @@
1
1
  {
2
- "syncedAt": "2026-06-16T03:01:51.011Z",
2
+ "syncedAt": "2026-06-16T11:04:55.690Z",
3
3
  "syncedFrom": "vendor/skill-garden/.trellis",
4
- "sourceCommit": "427b088bc8f07a93d366cb2128da0bdf7b2e35e1",
4
+ "sourceCommit": "987263bc8fce1e6057ba5624a9f8d1c8d4d40fe4",
5
5
  "variants": {
6
6
  "old": {
7
7
  "claudeSkills": [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flower-trellis",
3
- "version": "0.2.5-beta.2",
3
+ "version": "0.3.0-beta.1",
4
4
  "description": "一键安装/升级 Trellis 并自动融合 skill-garden 强化包(默认 Claude + agents)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,7 +13,7 @@
13
13
  },
14
14
  "dependencies": {
15
15
  "@inquirer/prompts": "^8.5.2",
16
- "@mindfoldhq/trellis": "0.6.0-beta.8",
16
+ "@mindfoldhq/trellis": "0.6.0",
17
17
  "chalk": "^5.6.2",
18
18
  "figlet": "^1.11.0",
19
19
  "node-pty": "^1.1.0"
@@ -129,13 +129,16 @@ export function applyEnhancements(target, opts = {}) {
129
129
  }
130
130
  }
131
131
 
132
- // codex 平台后处理:注释 config.toml multi_agent_v2 + 挂上 SessionStart hook(仅当 .codex/ 存在)
132
+ // codex 平台后处理:旧 multi_agent_v2 兼容清理 + 合并 SessionStart hook + 强制 sub-agent 调度
133
133
  const codex = applyCodexTweaks(target);
134
134
  if (codex.applied) {
135
135
  const seg = codex.tomlChanged
136
- ? "config.toml 已注释 multi_agent_v2"
137
- : "config.toml(multi_agent_v2 已是注释态)";
138
- console.log(` ✓ codex 调整:${seg};hooks.json = SessionStart + UserPromptSubmit`);
136
+ ? "config.toml 已清理旧 multi_agent_v2"
137
+ : "config.toml 无需清理 multi_agent_v2";
138
+ const dispatch = codex.dispatchModeChanged
139
+ ? "dispatch_mode 已强制为 sub-agent"
140
+ : "dispatch_mode 已是 sub-agent";
141
+ console.log(` ✓ codex 调整:${seg};hooks.json 已合并 SessionStart;${dispatch}`);
139
142
  }
140
143
 
141
144
  return { variant, installed };
@@ -5,38 +5,153 @@ import path from "node:path";
5
5
  * flower-trellis 对 Trellis 生成的 codex 配置的定制后处理。
6
6
  *
7
7
  * 仅当目标项目已配置 codex 平台(存在 .codex/)时生效;在 init / update 叠加阶段调用,幂等。
8
- * 做两件事:
9
- * 1. 注释掉 .codex/config.toml 的 [features.multi_agent_v2] 段;
10
- * 2. 用指定内容覆盖 .codex/hooks.json —— Trellis 默认(仅 UserPromptSubmit)基础上挂 SessionStart。
8
+ * 做三件事:
9
+ * 1. 兼容旧 Trellis:注释掉 .codex/config.toml 的 [features.multi_agent_v2] 段;
10
+ * 2. 合并 .codex/hooks.json —— 保留 Trellis 上游 hook 设置,只补 flower 需要的 SessionStart。
11
+ * 3. 强制 .trellis/config.yaml 的 codex.dispatch_mode 为 sub-agent。
11
12
  */
12
13
 
13
- // 期望的 .codex/hooks.json。两个脚本均由 Trellis codex configurator 写入 .codex/hooks/。
14
- const CODEX_HOOKS = {
15
- hooks: {
16
- SessionStart: [
17
- {
18
- hooks: [
19
- {
20
- type: "command",
21
- command: "python3 .codex/hooks/session-start.py",
22
- timeout: 5,
23
- },
24
- ],
25
- },
26
- ],
27
- UserPromptSubmit: [
28
- {
29
- hooks: [
30
- {
31
- type: "command",
32
- command: "python3 .codex/hooks/inject-workflow-state.py",
33
- timeout: 5,
34
- },
35
- ],
36
- },
37
- ],
38
- },
39
- };
14
+ const WORKFLOW_HOOK_SCRIPT = ".codex/hooks/inject-workflow-state.py";
15
+ const SESSION_START_SCRIPT = ".codex/hooks/session-start.py";
16
+ const CODEX_DISPATCH_MODE = "sub-agent";
17
+
18
+ /** 返回一行开头的空白缩进。 */
19
+ function leadingWhitespace(line) {
20
+ return line.match(/^\s*/)?.[0] || "";
21
+ }
22
+
23
+ /** 判断一行是否是未注释的顶层 YAML key。 */
24
+ function isTopLevelKey(line) {
25
+ const trimmed = line.trim();
26
+ return Boolean(trimmed && !trimmed.startsWith("#") && !leadingWhitespace(line) && /^[^:#]+:/.test(trimmed));
27
+ }
28
+
29
+ /** 找到未注释的顶层 `codex:` 块行号。 */
30
+ function findCodexBlockStart(lines) {
31
+ return lines.findIndex((line) => {
32
+ const trimmed = line.trim();
33
+ return isTopLevelKey(line) && /^codex\s*:/.test(trimmed);
34
+ });
35
+ }
36
+
37
+ /** 找到 YAML 顶层块结束位置,注释和空行不结束当前块。 */
38
+ function findTopLevelBlockEnd(lines, start) {
39
+ for (let i = start + 1; i < lines.length; i += 1) {
40
+ if (isTopLevelKey(lines[i])) return i;
41
+ }
42
+ return lines.length;
43
+ }
44
+
45
+ /** 按逗号拆分 YAML inline map 内容,保留引号内逗号。 */
46
+ function splitInlineMapItems(content) {
47
+ const items = [];
48
+ let quote = "";
49
+ let token = "";
50
+ for (const ch of content) {
51
+ if (quote) {
52
+ token += ch;
53
+ if (ch === quote) quote = "";
54
+ continue;
55
+ }
56
+ if (ch === "\"" || ch === "'") {
57
+ quote = ch;
58
+ token += ch;
59
+ continue;
60
+ }
61
+ if (ch === ",") {
62
+ if (token.trim()) items.push(token.trim());
63
+ token = "";
64
+ continue;
65
+ }
66
+ token += ch;
67
+ }
68
+ if (token.trim()) items.push(token.trim());
69
+ return items;
70
+ }
71
+
72
+ /** 解析简单 YAML inline map,失败时返回 null。 */
73
+ function parseInlineMapEntries(value) {
74
+ if (!value.startsWith("{") || !value.endsWith("}")) return null;
75
+ const body = value.slice(1, -1).trim();
76
+ if (!body) return [];
77
+ const entries = [];
78
+ for (const item of splitInlineMapItems(body)) {
79
+ const [key, ...rest] = item.split(":");
80
+ if (!key || rest.length === 0) return null;
81
+ entries.push({ key: key.trim(), value: rest.join(":").trim() });
82
+ }
83
+ return entries;
84
+ }
85
+
86
+ /**
87
+ * 强制目标项目 `.trellis/config.yaml` 使用 Codex sub-agent 调度。
88
+ *
89
+ * Trellis 默认缺失该字段时按 inline 注入 `<codex-mode>`,会让 route 误判 subagent
90
+ * 不可执行。flower 在 Codex 目标上直接写真实配置,避免依赖注释示例或模型推断。
91
+ *
92
+ * @param {string} configPath 目标项目 `.trellis/config.yaml` 路径
93
+ * @returns {boolean} 是否写入
94
+ */
95
+ function forceCodexDispatchMode(configPath) {
96
+ const current = fs.existsSync(configPath) ? fs.readFileSync(configPath, "utf8") : "";
97
+ const normalized = current.replace(/\r\n/g, "\n");
98
+ const hadFinalNewline = normalized.endsWith("\n");
99
+ const lines = normalized.length === 0
100
+ ? []
101
+ : normalized.replace(/\n$/, "").split("\n");
102
+
103
+ const codexStart = findCodexBlockStart(lines);
104
+ if (codexStart === -1) {
105
+ if (lines.length > 0 && lines[lines.length - 1].trim()) lines.push("");
106
+ lines.push("codex:", ` dispatch_mode: ${CODEX_DISPATCH_MODE}`);
107
+ } else {
108
+ const blockEnd = findTopLevelBlockEnd(lines, codexStart);
109
+ const codexMatch = lines[codexStart].match(/^(\s*)codex\s*:\s*(.*)$/);
110
+ const codexIndent = codexMatch?.[1] || "";
111
+ const codexValue = (codexMatch?.[2] || "").trim();
112
+ const inlineEntries = parseInlineMapEntries(codexValue);
113
+ if (inlineEntries) {
114
+ const nextLines = [`${codexIndent}codex:`];
115
+ let hasDispatchMode = false;
116
+ for (const entry of inlineEntries) {
117
+ if (entry.key === "dispatch_mode") {
118
+ hasDispatchMode = true;
119
+ nextLines.push(`${codexIndent} dispatch_mode: ${CODEX_DISPATCH_MODE}`);
120
+ } else {
121
+ nextLines.push(`${codexIndent} ${entry.key}: ${entry.value}`);
122
+ }
123
+ }
124
+ if (!hasDispatchMode) {
125
+ nextLines.splice(1, 0, `${codexIndent} dispatch_mode: ${CODEX_DISPATCH_MODE}`);
126
+ }
127
+ lines.splice(codexStart, 1, ...nextLines);
128
+ } else if (codexValue && !codexValue.startsWith("#")) {
129
+ lines[codexStart] = `${codexIndent}codex:`;
130
+ }
131
+
132
+ const nextBlockEnd = inlineEntries
133
+ ? findTopLevelBlockEnd(lines, codexStart)
134
+ : blockEnd;
135
+ const dispatchIndex = lines.findIndex((line, index) => {
136
+ if (index <= codexStart || index >= nextBlockEnd) return false;
137
+ const trimmed = line.trim();
138
+ return Boolean(trimmed && !trimmed.startsWith("#") && /^dispatch_mode\s*:/.test(trimmed));
139
+ });
140
+
141
+ if (dispatchIndex === -1) {
142
+ lines.splice(codexStart + 1, 0, `${codexIndent} dispatch_mode: ${CODEX_DISPATCH_MODE}`);
143
+ } else {
144
+ const indent = leadingWhitespace(lines[dispatchIndex]);
145
+ lines[dispatchIndex] = `${indent}dispatch_mode: ${CODEX_DISPATCH_MODE}`;
146
+ }
147
+ }
148
+
149
+ const desired = lines.join("\n") + (hadFinalNewline || lines.length > 0 ? "\n" : "");
150
+ if (current === desired) return false;
151
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
152
+ fs.writeFileSync(configPath, desired);
153
+ return true;
154
+ }
40
155
 
41
156
  /**
42
157
  * 注释掉 config.toml 里的 [features.multi_agent_v2] 段(段头 + 段内键,直到下一个 section)。
@@ -82,17 +197,82 @@ function commentMultiAgentV2(tomlPath) {
82
197
  }
83
198
 
84
199
  /**
85
- * flower 指定内容覆盖 .codex/hooks.json。幂等:内容一致则不写。
86
- * @returns {boolean} 是否写入
200
+ * 容错读取 JSON 文件;缺失或格式异常时返回空 hooks 壳。
201
+ * @param {string} hooksPath .codex/hooks.json 路径
202
+ * @returns {{hooks: object}}
87
203
  */
88
- function rewriteHooks(hooksPath) {
89
- const desired = JSON.stringify(CODEX_HOOKS, null, 2) + "\n";
90
- let current = null;
204
+ function readHooksConfig(hooksPath) {
91
205
  try {
92
- current = fs.readFileSync(hooksPath, "utf8");
206
+ const parsed = JSON.parse(fs.readFileSync(hooksPath, "utf8"));
207
+ if (parsed && typeof parsed === "object") {
208
+ const hooks = parsed.hooks && typeof parsed.hooks === "object" ? parsed.hooks : {};
209
+ return { ...parsed, hooks };
210
+ }
93
211
  } catch {
94
- // 文件不存在,视为需要写
212
+ // 缺失或损坏时用空壳重建,避免 init/update 后处理失败中断主流程
95
213
  }
214
+ return { hooks: {} };
215
+ }
216
+
217
+ /**
218
+ * 从上游 UserPromptSubmit hook 推导 SessionStart 命令。
219
+ *
220
+ * Trellis 会按平台写入 Python 命令前缀和 UTF-8 参数;复用该命令能避免 flower
221
+ * 在 Windows / Linux / 未来模板之间写死不同的 Python 调用方式。
222
+ *
223
+ * @param {{hooks: object}} config hooks.json 配置
224
+ * @returns {string} SessionStart command
225
+ */
226
+ function sessionStartCommand(config) {
227
+ const groups = Array.isArray(config.hooks.UserPromptSubmit)
228
+ ? config.hooks.UserPromptSubmit
229
+ : [];
230
+ for (const group of groups) {
231
+ const hooks = Array.isArray(group?.hooks) ? group.hooks : [];
232
+ for (const hook of hooks) {
233
+ if (hook?.type !== "command" || typeof hook.command !== "string") continue;
234
+ if (hook.command.includes(WORKFLOW_HOOK_SCRIPT)) {
235
+ return hook.command.replace(WORKFLOW_HOOK_SCRIPT, SESSION_START_SCRIPT);
236
+ }
237
+ }
238
+ }
239
+ return `python3 -X utf8 ${SESSION_START_SCRIPT}`;
240
+ }
241
+
242
+ /**
243
+ * 构造 flower 额外需要的 SessionStart hook。UserPromptSubmit 由 Trellis 上游维护,这里不覆盖。
244
+ * @param {{hooks: object}} config hooks.json 配置
245
+ * @returns {Array<object>} SessionStart hook 数组
246
+ */
247
+ function sessionStartHook(config) {
248
+ return [
249
+ {
250
+ hooks: [
251
+ {
252
+ type: "command",
253
+ command: sessionStartCommand(config),
254
+ timeout: 30,
255
+ },
256
+ ],
257
+ },
258
+ ];
259
+ }
260
+
261
+ /**
262
+ * 合并 flower 需要的 SessionStart hook,保留 Trellis 上游 UserPromptSubmit 设置。
263
+ *
264
+ * Trellis 0.6.0 已把 UserPromptSubmit 的 UTF-8 与 timeout 策略写进模板;flower 只负责
265
+ * 补上 SessionStart,否则整文件覆盖会让上游模板改进在 update 后丢失。
266
+ *
267
+ * @param {string} hooksPath .codex/hooks.json 路径
268
+ * @returns {boolean} 是否写入
269
+ */
270
+ function mergeHooks(hooksPath) {
271
+ const config = readHooksConfig(hooksPath);
272
+ config.hooks.SessionStart = sessionStartHook(config);
273
+
274
+ const desired = JSON.stringify(config, null, 2) + "\n";
275
+ const current = fs.existsSync(hooksPath) ? fs.readFileSync(hooksPath, "utf8") : "";
96
276
  if (current === desired) return false;
97
277
  fs.writeFileSync(hooksPath, desired);
98
278
  return true;
@@ -101,12 +281,13 @@ function rewriteHooks(hooksPath) {
101
281
  /**
102
282
  * codex 平台后处理入口。仅当 .codex/ 存在时执行。
103
283
  * @param {string} target 目标项目根
104
- * @returns {{applied: boolean, tomlChanged?: boolean, hooksWritten?: boolean}}
284
+ * @returns {{applied: boolean, tomlChanged?: boolean, hooksWritten?: boolean, dispatchModeChanged?: boolean}}
105
285
  */
106
286
  export function applyCodexTweaks(target) {
107
287
  const codexDir = path.join(target, ".codex");
108
288
  if (!fs.existsSync(codexDir)) return { applied: false };
109
289
  const tomlChanged = commentMultiAgentV2(path.join(codexDir, "config.toml"));
110
- const hooksWritten = rewriteHooks(path.join(codexDir, "hooks.json"));
111
- return { applied: true, tomlChanged, hooksWritten };
290
+ const hooksWritten = mergeHooks(path.join(codexDir, "hooks.json"));
291
+ const dispatchModeChanged = forceCodexDispatchMode(path.join(target, ".trellis", "config.yaml"));
292
+ return { applied: true, tomlChanged, hooksWritten, dispatchModeChanged };
112
293
  }