@yorz/cli 0.3.5 → 0.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +17 -17
  2. package/dist/cli/index.js +679 -209
  3. package/dist/gui/assets/{abnfDiagram-VRR7QNED-DQM-IUdR.js → abnfDiagram-VRR7QNED-P8M0mKNL.js} +1 -1
  4. package/dist/gui/assets/{arc-WoQS9inO.js → arc-DlQjdRG1.js} +1 -1
  5. package/dist/gui/assets/{architectureDiagram-ZJ3FMSHR-BSJfE6nY.js → architectureDiagram-ZJ3FMSHR-CWjQFrBc.js} +1 -1
  6. package/dist/gui/assets/{blockDiagram-677ZJIJ3-BXk0h_Na.js → blockDiagram-677ZJIJ3-BKVLTUWU.js} +1 -1
  7. package/dist/gui/assets/{c4Diagram-LMCZKHZV-Sgnopqnh.js → c4Diagram-LMCZKHZV-Dc-LyufI.js} +1 -1
  8. package/dist/gui/assets/channel-Donye8KV.js +1 -0
  9. package/dist/gui/assets/{chunk-2Q5K7J3B-sWwRHI31.js → chunk-2Q5K7J3B-N67wx4l2.js} +1 -1
  10. package/dist/gui/assets/{chunk-32BRIVSS-6HJXOWs9.js → chunk-32BRIVSS-DTgkNatU.js} +1 -1
  11. package/dist/gui/assets/{chunk-5VM5RSS4-De_Y0Itz.js → chunk-5VM5RSS4-CvwJ-FXX.js} +1 -1
  12. package/dist/gui/assets/{chunk-EX3LRPZG-CTtRnKWx.js → chunk-EX3LRPZG-B_ZbidKZ.js} +1 -1
  13. package/dist/gui/assets/{chunk-JWPE2WC7-Dp9usWZX.js → chunk-JWPE2WC7-CBOi6DIW.js} +1 -1
  14. package/dist/gui/assets/{chunk-MOJQB5TN-OoM3V1FE.js → chunk-MOJQB5TN-CW-x6_OV.js} +1 -1
  15. package/dist/gui/assets/{chunk-RYQCIY6F-M8me5HDP.js → chunk-RYQCIY6F-BmdlZ9KS.js} +1 -1
  16. package/dist/gui/assets/{chunk-V7JOEXUC-CLdvRo84.js → chunk-V7JOEXUC-B4f5PF42.js} +1 -1
  17. package/dist/gui/assets/{chunk-VR4S4FIN-BMEciG-B.js → chunk-VR4S4FIN-DKcqnznN.js} +1 -1
  18. package/dist/gui/assets/{chunk-XXDRQBXY-DT8b8q8Q.js → chunk-XXDRQBXY--4FGYA_u.js} +1 -1
  19. package/dist/gui/assets/classDiagram-OUVF2IWQ-8onVfRQK.js +1 -0
  20. package/dist/gui/assets/classDiagram-v2-EOCWNBFH-8onVfRQK.js +1 -0
  21. package/dist/gui/assets/{cose-bilkent-JH36ORCC-C1w9npQD.js → cose-bilkent-JH36ORCC-B2f6Upao.js} +1 -1
  22. package/dist/gui/assets/{cynefin-VYW2F7L2-buyB6Ypi.js → cynefin-VYW2F7L2-CxS20C5H.js} +1 -1
  23. package/dist/gui/assets/{cynefinDiagram-TSTJHNR4-DPQmwOX9.js → cynefinDiagram-TSTJHNR4-hyW_UYqU.js} +1 -1
  24. package/dist/gui/assets/{dagre-VKFMJZFB-DfbmQsg3.js → dagre-VKFMJZFB-BW-oRpvd.js} +1 -1
  25. package/dist/gui/assets/{diagram-FQU43EPY-DZZkoYxF.js → diagram-FQU43EPY-B-2gzyIy.js} +1 -1
  26. package/dist/gui/assets/{diagram-G47NLZAW-DJyFDLgi.js → diagram-G47NLZAW-DF0y134D.js} +1 -1
  27. package/dist/gui/assets/{diagram-NH7WQ7WH-CeIoika9.js → diagram-NH7WQ7WH-CejbGVF2.js} +1 -1
  28. package/dist/gui/assets/{diagram-OA4YK3LP-D3bJmzbA.js → diagram-OA4YK3LP-D5e5A6IV.js} +1 -1
  29. package/dist/gui/assets/{diagram-WEI45ONY-5whYnN6Z.js → diagram-WEI45ONY-CAISCqpz.js} +1 -1
  30. package/dist/gui/assets/{ebnfDiagram-CCIWWBDH-fRcOahPq.js → ebnfDiagram-CCIWWBDH-BYSESG5t.js} +1 -1
  31. package/dist/gui/assets/{erDiagram-Q63AITRT-h22ShxWi.js → erDiagram-Q63AITRT-DpW8WS2S.js} +1 -1
  32. package/dist/gui/assets/{flowDiagram-23GEKE2U-CImBEPKl.js → flowDiagram-23GEKE2U-C_v7g6xp.js} +1 -1
  33. package/dist/gui/assets/{ganttDiagram-NO4QXBWP-D7GfEoL1.js → ganttDiagram-NO4QXBWP-CeaE4SQt.js} +1 -1
  34. package/dist/gui/assets/{gitGraphDiagram-IHSO6WYX-BQlPqwV3.js → gitGraphDiagram-IHSO6WYX-DHiPQjRQ.js} +1 -1
  35. package/dist/gui/assets/{index-BnMXI8QL.js → index-BESUyU3O.js} +27 -27
  36. package/dist/gui/assets/index-CTkTlp_v.css +1 -0
  37. package/dist/gui/assets/{infoDiagram-FWYZ7A6U-dQgAvdfE.js → infoDiagram-FWYZ7A6U-tbR2h7uU.js} +1 -1
  38. package/dist/gui/assets/{ishikawaDiagram-FXEZZL3T-Dsb8okiC.js → ishikawaDiagram-FXEZZL3T-TFTW7kkJ.js} +1 -1
  39. package/dist/gui/assets/{journeyDiagram-5HDEW3XC-BVaJ8kFx.js → journeyDiagram-5HDEW3XC-DOV8E7Kx.js} +1 -1
  40. package/dist/gui/assets/{kanban-definition-HUTT4EX6-CtRRLwap.js → kanban-definition-HUTT4EX6-BmQnZixr.js} +1 -1
  41. package/dist/gui/assets/{linear-CtTzoPCE.js → linear-BM4IZgpi.js} +1 -1
  42. package/dist/gui/assets/{mermaid.core-BO-VJXe-.js → mermaid.core-LupX_4le.js} +4 -4
  43. package/dist/gui/assets/{mindmap-definition-LN4V7U3C-Djl0w9Df.js → mindmap-definition-LN4V7U3C-Ci25EAsL.js} +1 -1
  44. package/dist/gui/assets/{pegDiagram-2B236MQR-D2VRUT1H.js → pegDiagram-2B236MQR-De8kn5Bk.js} +1 -1
  45. package/dist/gui/assets/{pieDiagram-ENE6RG2P-Bp7Ytwe1.js → pieDiagram-ENE6RG2P-Q1uRLnK9.js} +1 -1
  46. package/dist/gui/assets/{quadrantDiagram-ABIIQ3AL-Df4S5Uw8.js → quadrantDiagram-ABIIQ3AL-Dox-lxWY.js} +1 -1
  47. package/dist/gui/assets/{railroadDiagram-RFXS5EU6-1E9T3-eU.js → railroadDiagram-RFXS5EU6-5PHbvzzM.js} +1 -1
  48. package/dist/gui/assets/{requirementDiagram-TGXJPOKE-ClWnTEWR.js → requirementDiagram-TGXJPOKE-BOlMALKo.js} +1 -1
  49. package/dist/gui/assets/{sankeyDiagram-HTMAVEWB-kV-szT-S.js → sankeyDiagram-HTMAVEWB-Dhl4t_ZC.js} +1 -1
  50. package/dist/gui/assets/{sequenceDiagram-DBY2YBRQ-C17izAQ5.js → sequenceDiagram-DBY2YBRQ-DWCe9xy7.js} +1 -1
  51. package/dist/gui/assets/{sizeCapture-X5ZJPWSS-Qa6aDRpG.js → sizeCapture-X5ZJPWSS-BUM02Me6.js} +1 -1
  52. package/dist/gui/assets/{stateDiagram-2N3HPSRC-C84qUVRJ.js → stateDiagram-2N3HPSRC-ryQbw4Q_.js} +1 -1
  53. package/dist/gui/assets/stateDiagram-v2-6OUMAXLB-CkZ3MMxP.js +1 -0
  54. package/dist/gui/assets/{swimlanes-5IMT3BWC-BByVALRE.js → swimlanes-5IMT3BWC-CrP6qLyo.js} +2 -2
  55. package/dist/gui/assets/swimlanesDiagram-G3AALYLV-CioGa7m6.js +8 -0
  56. package/dist/gui/assets/{timeline-definition-FHXFAJF6-BD9jOAZN.js → timeline-definition-FHXFAJF6-CXm-_myR.js} +1 -1
  57. package/dist/gui/assets/{vennDiagram-L72KCM5P-BqTRSqDu.js → vennDiagram-L72KCM5P-C2ONmkxz.js} +1 -1
  58. package/dist/gui/assets/{wardleyDiagram-EHGQE667-DqSSn_TX.js → wardleyDiagram-EHGQE667-DG3WuRlC.js} +1 -1
  59. package/dist/gui/assets/{xychartDiagram-FW5EYKEG-B5GDvD8r.js → xychartDiagram-FW5EYKEG-CChHjXHR.js} +1 -1
  60. package/dist/gui/index.html +2 -2
  61. package/package.json +1 -1
  62. package/dist/gui/assets/channel-DAAPjLkv.js +0 -1
  63. package/dist/gui/assets/classDiagram-OUVF2IWQ-DU6pj67q.js +0 -1
  64. package/dist/gui/assets/classDiagram-v2-EOCWNBFH-DU6pj67q.js +0 -1
  65. package/dist/gui/assets/index-B_NGA1xd.css +0 -1
  66. package/dist/gui/assets/stateDiagram-v2-6OUMAXLB-Ct4b-n_-.js +0 -1
  67. package/dist/gui/assets/swimlanesDiagram-G3AALYLV-0XnzonY2.js +0 -8
package/dist/cli/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import { homedir, networkInterfaces } from "node:os";
2
+ import { homedir, platform, networkInterfaces } from "node:os";
3
3
  import { Command, Option } from "commander";
4
- import { stat, readFile, writeFile, rm, mkdir, access, rename, readdir, copyFile, appendFile } from "node:fs/promises";
4
+ import { stat, readFile, writeFile, rm, mkdir, access, rename, readdir, appendFile, copyFile } from "node:fs/promises";
5
5
  import { join, dirname, extname, resolve, sep, isAbsolute, basename as basename$1, relative, normalize } from "node:path";
6
6
  import { createHash, randomUUID } from "node:crypto";
7
7
  import { spawn, execFile } from "node:child_process";
8
- import { existsSync, statSync, mkdirSync, watch, createReadStream, readFileSync } from "node:fs";
8
+ import { existsSync, statSync, mkdirSync, watch, createReadStream, openSync } from "node:fs";
9
9
  import { serve } from "@hono/node-server";
10
10
  import { Hono } from "hono";
11
11
  import { promisify } from "node:util";
@@ -21,7 +21,7 @@ import { Codex } from "@openai/codex-sdk";
21
21
  import { createOpencode } from "@opencode-ai/sdk";
22
22
  import { createInterface as createInterface$1 } from "node:readline/promises";
23
23
  import MarkdownIt from "markdown-it";
24
- const version = "0.3.5";
24
+ const version = "0.3.7";
25
25
  const pkg = {
26
26
  version
27
27
  };
@@ -55,9 +55,9 @@ function getAdapter(name) {
55
55
  if (name in ADAPTERS) return ADAPTERS[name];
56
56
  throw new Error(`Unknown agent: ${name}. Supported: ${Object.keys(ADAPTERS).join(", ")}`);
57
57
  }
58
- const __vite_glob_0_0 = '---\nname: yorz-debug\ndescription: 以资深工程师的调试纪律定位并修复疑难 bug——假设→取证→验证的证据闭环,靠 debug.md 活文档与 git 快照兜住污染。\n---\n\n# YorZ Debug 深度调试 Skill\n\n指导 Agent 以**资深工程师的调试纪律**逐步逼近疑难 bug 的根因:不凭猜测改代码,坚持「假设 → 取证 → 验证 → 缩小范围 → 再假设」的证据闭环。调试过程赋予更大的自由度(临时改代码、Mock 数据、加日志、建临时验证入口),同时用 `debug.md` 活文档 + `git stash create` 快照兜住"污染性改动不进入最终提交"。\n\n> 本 skill **不复用** yorz-spec 的 plan/tasks/execute 状态机。真相以 `debug.md` 为单一载体;Agent 持续推进直至需要用户回传证据(人在环路),或根因确认并收尾。\n\n## 何时进入本 skill\n\n- **独立触发(无需 spec / UI)**:用户在 Agent 对话中直接点名「用 yorz-debug 调这个 bug」。此时可能没有任何 spec 上下文——按下方「无 spec 场景」处理 `debug.md` 落点。\n- **追加任务勾选 Debug**:用户在 SpecDetail 追加 `fix` 任务时勾选「Debug 模式」,后端 prompt 指向本 skill。\n- **重入**:spec 目录已存在 `status: debugging` 的 `debug.md`(有未收尾记录块),run/append 路由自动切到本 skill。此时**不新建记录块**,定位活跃记录块(frontmatter `active: NNN`)续跑。\n\n## 输入约定\n\n- `spec_dir`(**可选**):spec 目录路径(含 `spec.md`)。给出时 `debug.md` 建在此目录,与 `spec.md` 同级。\n- 待调试 bug 的描述:来自追加任务行 / 用户 prompt。\n\n### debug.md 落点\n\n- **有 spec 上下文**:`debug.md` 落 `spec_dir`(与 `spec.md` 同级)。\n- **无 spec 场景(独立触发)**:没有 `spec_dir` 时,`debug.md` 落**当前工作目录**;若用户在 prompt 中显式指定了路径,则以用户指定为准。其余多记录模型、快照、脚手架、收尾流程与有 spec 时**完全一致**。\n\n## debug.md:多记录活文档\n\n**一个 `debug.md` 承载同一 spec 生命周期内的多次调试**(某次收尾后又发现新 bug,就追加下一条记录,不覆盖历史)。它同时是:调试活文档、**Debug 模式的持久化标记**(存在未收尾记录 = 处于 Debug 模式)、脚手架是否清理干净的守卫。\n\n### 文件级 frontmatter\n\n```yaml\n---\nstatus: debugging # debugging(存在未收尾记录)| resolved(全部记录已收尾)\nactive: 2 # 当前活跃记录编号;resolved 时留空\nupdated_at: \'2026-07-19 15:54:37\' # 本地秒级,单引号包裹\n---\n```\n\n### 正文按「记录块」组织\n\n每次进入 Debug **在文末追加**一个记录块,编号 `NNN` 从 1 递增、**不复用**、不覆盖既有块:\n\n```markdown\n## Debug 1 · <bug 一句话简述>\n\n- 状态:debugging # debugging | resolved\n- 快照:<git stash create 得到的 SHA>\n- 进入时间:\'2026-07-19 15:54:37\'\n\n### 1. Bug 现象与复现\n\n...\n\n### 2. 关联链路分析\n\n...\n\n### 3. Debug 基线\n\n快照 SHA + 进入时间;`git diff <SHA>` 为退出闸门基准。\n\n### 4. 假设看板\n\n...\n\n### 5. 证据\n\n...\n\n### 6. 脚手架清单\n\n...\n\n### 7. 收尾核对\n\n...\n```\n\n### 生命周期\n\n- **创建 / 进入**:`debug.md` 不存在则创建(写文件 frontmatter + 第 1 个记录块);已存在则追加 `## Debug <max+1>`,置文件 `status: debugging`、`active: <新编号>`。进入后**立刻**打快照(见「污染防线」)。\n- **单块收尾**:把该记录块块头 `状态` 置 `resolved`,脚手架逐条核销,`git diff <SNAP>` 校验只剩合法修复。\n- **文件 status 收敛**:单块收尾后,若**所有**记录块均 `resolved`,置文件 `status: resolved`、清空 `active`;否则保持 `debugging` 且 `active` 指向仍在调试的块。\n- **归档**:收尾**保留** `debug.md` 供复盘,绝不删除。\n\n## 核心调试循环\n\n**分析 → 规划 → 假设 → 实施 → 验证 → 缩小范围 → 再假设**,直至根因被硬证据锁定。\n\n```mermaid\nflowchart TD\n Start["接手 bug"] --> Repro{"能稳定复现?"}\n Repro -->|否| GetRepro["补齐复现步骤<br/>(问用户 / 自行构造)"]\n GetRepro --> Repro\n Repro -->|是| Analyze["分析关联链路<br/>写入 debug.md"]\n Analyze --> Hypo["提出可证伪假设<br/>成立看到 X / 不成立看到 Y"]\n Hypo --> Probe["写验证性代码 / 加日志<br/>登记脚手架清单"]\n Probe --> Eval{"证据指向?"}\n Eval -->|证伪| Exclude["记入已排除假设 + 依据"]\n Exclude --> Narrow["缩小范围(二分/控制变量)"]\n Narrow --> Hypo\n Eval -->|坐实根因| Fix["写最终修复"]\n Fix --> Verify{"重跑复现步骤<br/>问题消失?"}\n Verify -->|否| Hypo\n Verify -->|是| Close["进入收尾"]\n```\n\n### 方法层\n\n- **二分法**:在可疑区间(提交历史 / 代码路径 / 数据范围)二分定位,每次砍掉一半。\n- **控制变量法**:一次只改一个变量,隔离单一因素的影响。\n- **遍历可能分支**:对有限的分支/枚举/状态逐一验证,排除法收敛。\n\n## 硬约束(资深工程师容易做、Agent 容易漏)\n\n1. **先复现,再调试**:无法稳定复现,**不得进入修改阶段**——否则无从验证修没修好。复现不了就先构造复现路径或请用户提供。\n2. **假设必须可证伪**:提出假设时,同时写出"若成立会看到 X,若不成立会看到 Y",再去取证。写进「假设看板」。\n3. **记录已排除假设 + 依据**:每否定一个假设,连同证据写入看板,**避免兜圈子反复验证同一已否定项**。\n4. **禁止无证据修复**:拿到指向根因的**硬证据**(日志 / 截图 / 可复现的最小用例)前,**只许写验证性代码,不许写最终修复**。\n5. **人在环路**:加日志后**停下**,请用户复现并回传日志 / 截图(MVP 复用现有 Chat 会话承接回传,无需新 UI)。回传前不臆断。\n6. **退出双条件**:根因有硬证据 **且** 修复后重跑复现步骤、证据显示问题消失——**缺一不可**。\n\n### 日志纪律\n\n- **可读可复制**:打印结构化字符串而非整个 object(如 拼接关键字段 k1=v1&k2=v2 或 `JSON.stringify(x)`),避免直接打印对象或复杂结构变量的引用。\n- **避免刷屏**:不要在循环/高频路径里打印大量日志;聚焦怀疑环节,必要时带唯一标记便于检索。\n- **可定位**:日志带上下文(哪个函数 / 哪个分支 / 关键变量值),让用户回传的一段就能判定假设成立与否。\n\n## 更大的自由度(Debug 模式内允许)\n\n- 编写临时代码(条件短路)或注释有意义代码,控制程序进入 / 屏蔽特定分支;\n- 临时篡改变量值、Mock API 返回,让程序走到特定分支;\n- 编写临时独立测试页面 / 入口,简化复现路径、直接验证怀疑环节;\n- 联网查询第三方 SDK 文档、向用户询问必要信息;\n- 允许 lint / typecheck / build / 单测出现错误,允许**临时**跳过这些检查。\n\n> 每一处临时改动都**必须**在「脚手架清单」登记一行(文件 + 位置 + 类型:短路 / Mock / 注释 / 临时页面 / 临时日志),收尾逐条核销。**未登记 = 会漏还原**。\n\n## 污染防线(强制流程)\n\n在**当前项目与分支就地调试**,不开 worktree;靠快照 + 脚手架清单 + diff 兜住污染。\n\n```mermaid\nflowchart TD\n Enter["进入 Debug(新记录块)"] --> Snap["SNAP = git stash create<br/>零副作用快照当前脏工作区"]\n Snap --> Record["把 SNAP 写入记录块 · Debug 基线"]\n Record --> Loop["调试循环<br/>照常改代码 + 登记脚手架清单"]\n Loop --> Fixed{"根因确认 + 硬证据?"}\n Fixed -->|否| Loop\n Fixed -->|是| Revert["按脚手架清单逐条还原"]\n Revert --> Diff["git diff SNAP"]\n Diff --> Gate{"仅剩合法修复?"}\n Gate -->|否| Revert\n Gate -->|是| Done["置本块 status: resolved<br/>跑变更文件单测/build/typecheck"]\n```\n\n- **进入即打快照**:`git stash create`(**不是** `git stash` / 临时 commit)生成一个记录当前脏工作区的 commit 对象,但**不动工作区 / index / HEAD**。它天然区分"进入前的既有未提交改动"与"调试引入的改动",避免临时 commit 的还原风险。把返回的 SHA 写入记录块「Debug 基线」。\n - 若 `git stash create` 无输出(工作区干净),以当前 `HEAD`(`git rev-parse HEAD`)作为快照基准并注明。\n- **git diff 是退出闸门**:宣告完成前,`git diff <SNAP>` 必须**只剩合法修复**——所有短路 / Mock / 注释 / 临时页面 / 临时日志都已还原。diff 里还有脚手架残留 = 不许收尾。\n\n## 收尾(退出 Debug 模式)\n\n根因确认且修复通过复现验证后:\n\n1. **还原脚手架**:按「脚手架清单」逐条还原临时代码与被注释代码,确保原有逻辑正确;清理不需要的 debug 日志。\n2. **退出闸门**:`git diff <SNAP>` 只剩合法修复(脚手架清单全部核销)。\n3. **常规完整性检查**:跑变更文件关联单测、构建、类型检查。\n4. **落状态**:把该记录块块头 `状态` 置 `resolved`;按「文件 status 收敛」更新文件 frontmatter(全部 resolved → 文件 `status: resolved`、清空 `active`)。\n5. **保留归档**:`debug.md` 保留,供复盘。\n\n> 收尾后如又发现**新** bug,回到本 skill 追加下一条 `## Debug NNN` 记录块(不覆盖历史),重新走一遍循环。\n\n## 写回纪律\n\n- 每次写回 `debug.md` 都更新文件 frontmatter 的 `updated_at`(本地秒级,单引号)。\n- `updated_at` 用 `YYYY-MM-DD HH:mm:ss`。\n- 记录块编号连续递增、不复用、不重排既有块。\n';
58
+ const __vite_glob_0_0 = '---\nname: yorz-debug\ndescription: 以资深工程师的调试纪律定位并修复疑难 bug——假设→取证→验证的证据闭环,靠 debug.md 活文档与 git 快照兜住污染。\n---\n\n# YorZ Debug 深度调试 Skill\n\n指导 Agent 以**资深工程师的调试纪律**逐步逼近疑难 bug 的根因:不凭猜测改代码,坚持「假设 → 取证 → 验证 → 缩小范围 → 再假设」的证据闭环。调试过程赋予更大的自由度(临时改代码、Mock 数据、加日志、建临时验证入口),同时用 `debug.md` 活文档 + `git stash create` 快照兜住"污染性改动不进入最终提交"。\n\n> 本 skill **不复用** yorz-spec 的 plan/tasks/execute 状态机。真相以 `debug.md` 为单一载体;Agent 持续推进直至需要用户回传证据(人在环路),或根因确认并收尾。\n\n## 何时进入本 skill\n\n- **独立触发(无需 spec / UI)**:用户在 Agent 对话中直接点名「用 yorz-debug 调这个 bug」。此时可能没有任何 spec 上下文——按下方「无 spec 场景」处理 `debug.md` 落点。\n- **追加任务勾选 Debug**:用户在 SpecDetail 追加 `fix` 任务时勾选「Debug 模式」,后端 prompt 指向本 skill。\n- **重入**:spec 目录已存在 `status: debugging` 的 `debug.md`(有未收尾记录块),run/append 路由自动切到本 skill。此时**不新建记录块**,定位活跃记录块(frontmatter `active: NNN`)续跑。\n\n## 输入约定\n\n- `spec_dir`(**可选**):spec 目录路径(含 `spec.md`)。给出时 `debug.md` 建在此目录,与 `spec.md` 同级。\n- 待调试 bug 的描述:来自追加任务行 / 用户 prompt。\n\n### debug.md 落点\n\n- **有 spec 上下文**:`debug.md` 落 `spec_dir`(与 `spec.md` 同级)。\n- **无 spec 场景(独立触发)**:没有 `spec_dir` 时,`debug.md` 落**当前工作目录**;若用户在 prompt 中显式指定了路径,则以用户指定为准。其余多记录模型、快照、脚手架、收尾流程与有 spec 时**完全一致**。\n\n## debug.md:多记录活文档\n\n**一个 `debug.md` 承载同一 spec 生命周期内的多次调试**(某次收尾后又发现新 bug,就追加下一条记录,不覆盖历史)。它同时是:调试活文档、**Debug 模式的持久化标记**(存在未收尾记录 = 处于 Debug 模式)、脚手架是否清理干净的守卫。\n\n### 文件级 frontmatter\n\n```yaml\n---\nstatus: debugging # debugging(存在未收尾记录)| resolved(全部记录已收尾)\nactive: 2 # 当前活跃记录编号;resolved 时留空\nupdated_at: \'2026-07-19 15:54:37\' # 本地秒级字符串\n---\n```\n\n### 正文按「记录块」组织\n\n每次进入 Debug **在文末追加**一个记录块,编号 `NNN` 从 1 递增、**不复用**、不覆盖既有块:\n\n```markdown\n## Debug 1 · <bug 一句话简述>\n\n- 状态:debugging # debugging | resolved\n- 快照:<git stash create 得到的 SHA>\n- 进入时间:\'2026-07-19 15:54:37\'\n\n### 1. Bug 现象与复现\n\n...\n\n### 2. 关联链路分析\n\n...\n\n### 3. Debug 基线\n\n快照 SHA + 进入时间;`git diff <SHA>` 为退出闸门基准。\n\n### 4. 假设看板\n\n...\n\n### 5. 证据\n\n...\n\n### 6. 脚手架清单\n\n...\n\n### 7. 收尾核对\n\n...\n```\n\n### 生命周期\n\n- **创建 / 进入**:`debug.md` 不存在则创建(写文件 frontmatter + 第 1 个记录块);已存在则追加 `## Debug <max+1>`,置文件 `status: debugging`、`active: <新编号>`。进入后**立刻**打快照(见「污染防线」)。\n- **单块收尾**:把该记录块块头 `状态` 置 `resolved`,脚手架逐条核销,`git diff <SNAP>` 校验只剩合法修复。\n- **文件 status 收敛**:单块收尾后,若**所有**记录块均 `resolved`,置文件 `status: resolved`、清空 `active`;否则保持 `debugging` 且 `active` 指向仍在调试的块。\n- **归档**:收尾**保留** `debug.md` 供复盘,绝不删除。\n\n## 核心调试循环\n\n**分析 → 规划 → 假设 → 实施 → 验证 → 缩小范围 → 再假设**,直至根因被硬证据锁定。\n\n```mermaid\nflowchart TD\n Start["接手 bug"] --> Repro{"能稳定复现?"}\n Repro -->|否| GetRepro["补齐复现步骤<br/>(问用户 / 自行构造)"]\n GetRepro --> Repro\n Repro -->|是| Analyze["分析关联链路<br/>写入 debug.md"]\n Analyze --> Hypo["提出可证伪假设<br/>成立看到 X / 不成立看到 Y"]\n Hypo --> Probe["写验证性代码 / 加日志<br/>登记脚手架清单"]\n Probe --> Eval{"证据指向?"}\n Eval -->|证伪| Exclude["记入已排除假设 + 依据"]\n Exclude --> Narrow["缩小范围(二分/控制变量)"]\n Narrow --> Hypo\n Eval -->|坐实根因| Fix["写最终修复"]\n Fix --> Verify{"重跑复现步骤<br/>问题消失?"}\n Verify -->|否| Hypo\n Verify -->|是| Close["进入收尾"]\n```\n\n### 方法层\n\n- **二分法**:在可疑区间(提交历史 / 代码路径 / 数据范围)二分定位,每次砍掉一半。\n- **控制变量法**:一次只改一个变量,隔离单一因素的影响。\n- **遍历可能分支**:对有限的分支/枚举/状态逐一验证,排除法收敛。\n\n## 硬约束(资深工程师容易做、Agent 容易漏)\n\n1. **先复现,再调试**:无法稳定复现,**不得进入修改阶段**——否则无从验证修没修好。复现不了就先构造复现路径或请用户提供。\n2. **假设必须可证伪**:提出假设时,同时写出"若成立会看到 X,若不成立会看到 Y",再去取证。写进「假设看板」。\n3. **记录已排除假设 + 依据**:每否定一个假设,连同证据写入看板,**避免兜圈子反复验证同一已否定项**。\n4. **禁止无证据修复**:拿到指向根因的**硬证据**(日志 / 截图 / 可复现的最小用例)前,**只许写验证性代码,不许写最终修复**。\n5. **人在环路**:加日志后**停下**,请用户复现并回传日志 / 截图(MVP 复用现有 Chat 会话承接回传,无需新 UI)。回传前不臆断。\n6. **退出双条件**:根因有硬证据 **且** 修复后重跑复现步骤、证据显示问题消失——**缺一不可**。\n\n### 日志纪律\n\n- **可读可复制**:打印结构化字符串而非整个 object(如 拼接关键字段 k1=v1&k2=v2 或 `JSON.stringify(x)`),避免直接打印对象或复杂结构变量的引用。\n- **避免刷屏**:不要在循环/高频路径里打印大量日志;聚焦怀疑环节,必要时带唯一标记便于检索。\n- **可定位**:日志带上下文(哪个函数 / 哪个分支 / 关键变量值),让用户回传的一段就能判定假设成立与否。\n\n## 更大的自由度(Debug 模式内允许)\n\n- 编写临时代码(条件短路)或注释有意义代码,控制程序进入 / 屏蔽特定分支;\n- 临时篡改变量值、Mock API 返回,让程序走到特定分支;\n- 编写临时独立测试页面 / 入口,简化复现路径、直接验证怀疑环节;\n- 联网查询第三方 SDK 文档、向用户询问必要信息;\n- 允许 lint / typecheck / build / 单测出现错误,允许**临时**跳过这些检查。\n\n> 每一处临时改动都**必须**在「脚手架清单」登记一行(文件 + 位置 + 类型:短路 / Mock / 注释 / 临时页面 / 临时日志),收尾逐条核销。**未登记 = 会漏还原**。\n\n## 污染防线(强制流程)\n\n在**当前项目与分支就地调试**,不开 worktree;靠快照 + 脚手架清单 + diff 兜住污染。\n\n```mermaid\nflowchart TD\n Enter["进入 Debug(新记录块)"] --> Snap["SNAP = git stash create<br/>零副作用快照当前脏工作区"]\n Snap --> Record["把 SNAP 写入记录块 · Debug 基线"]\n Record --> Loop["调试循环<br/>照常改代码 + 登记脚手架清单"]\n Loop --> Fixed{"根因确认 + 硬证据?"}\n Fixed -->|否| Loop\n Fixed -->|是| Revert["按脚手架清单逐条还原"]\n Revert --> Diff["git diff SNAP"]\n Diff --> Gate{"仅剩合法修复?"}\n Gate -->|否| Revert\n Gate -->|是| Done["置本块 status: resolved<br/>跑变更文件单测/build/typecheck"]\n```\n\n- **进入即打快照**:`git stash create`(**不是** `git stash` / 临时 commit)生成一个记录当前脏工作区的 commit 对象,但**不动工作区 / index / HEAD**。它天然区分"进入前的既有未提交改动"与"调试引入的改动",避免临时 commit 的还原风险。把返回的 SHA 写入记录块「Debug 基线」。\n - 若 `git stash create` 无输出(工作区干净),以当前 `HEAD`(`git rev-parse HEAD`)作为快照基准并注明。\n- **git diff 是退出闸门**:宣告完成前,`git diff <SNAP>` 必须**只剩合法修复**——所有短路 / Mock / 注释 / 临时页面 / 临时日志都已还原。diff 里还有脚手架残留 = 不许收尾。\n\n## 收尾(退出 Debug 模式)\n\n根因确认且修复通过复现验证后:\n\n1. **还原脚手架**:按「脚手架清单」逐条还原临时代码与被注释代码,确保原有逻辑正确;清理不需要的 debug 日志。\n2. **退出闸门**:`git diff <SNAP>` 只剩合法修复(脚手架清单全部核销)。\n3. **常规完整性检查**:跑变更文件关联单测、构建、类型检查。\n4. **落状态**:把该记录块块头 `状态` 置 `resolved`;按「文件 status 收敛」更新文件 frontmatter(全部 resolved → 文件 `status: resolved`、清空 `active`)。\n5. **保留归档**:`debug.md` 保留,供复盘。\n\n> 收尾后如又发现**新** bug,回到本 skill 追加下一条 `## Debug NNN` 记录块(不覆盖历史),重新走一遍循环。\n\n## 写回纪律\n\n- 每次写回 `debug.md` 都更新文件 frontmatter 的 `updated_at`。\n- `updated_at` 用本地秒级 `YYYY-MM-DD HH:mm:ss` 字符串。\n- 记录块编号连续递增、不复用、不重排既有块。\n';
59
59
  const __vite_glob_0_1 = '{\n "$schema": "https://json-schema.org/draft-07/schema#",\n "version": 2,\n "modules": [\n {\n "module": "SKILL",\n "file": "SKILL.md",\n "relatedCases": []\n }\n ]\n}\n';
60
- const __vite_glob_0_2 = '---\nname: yorz-spec\ndescription: Drive YorZ spec docs through plan / tasks / execute stages with deterministic state updates.\n---\n\n# YorZ Spec Drive Skill\n\n将单个 YorZ spec 文档作为状态机执行,围绕 `plan` / `tasks` / `execute` 三阶段推进,全部任务完成后进入终止态 `done`,把状态持续写回文档。md 是单一真相;Agent 持续推进直至阻塞(待确认项、决策、Review)或收尾为 `done` 才退出。\n\n## 如何使用本 skill\n\n本 skill 文档结构精简为 3 个文件;每次接到 spec 任务时,**按以下顺序**按需 Read:\n\n1. **本文档(SKILL.md)**:输入约定、frontmatter 规范、格式约定、自动模式判定、全局硬约束、lint 硬约束、持续推进约束。\n2. **[stages.md](./stages.md)**:plan / tasks / execute / new-spec 四阶段流程与正面示例。\n3. **[review.md](./review.md)**:Review / Git Ops 阶段(独立路径,低频使用)。\n\n[mermaid.md](./mermaid.md) 与 `references/` 目录**仅按需 Read**:只有在判断当前阶段需要输出 mermaid 图表「升维」时才加载,纯状态推进任务不必读取,以节省 context。**plan 阶段的「图形化补充」收尾子步骤**(见 stages.md)会针对 `现状分析`/`技术方案` 两节强制加载 mermaid.md 补图;补图须遵循其核心原则**「图优先、精确信息折叠」**——表层给图,精确细节折叠进 `<details>` 精确层。\n\n> 当 Agent 以 `mode=review` / `mode=git-ops` 启动时,按 [review.md](./review.md) 执行;该路径不进入 plan/tasks/execute 状态机,也不修改 spec.md 的 frontmatter。\n\n## 输入约定\n\n- `spec_path`:**可选**,目标 spec 文档路径。\n - 显式给出时:典型位置 `docs/specs/*.md` 或 `.yorz/specs/<id>/spec.md`,直接更新该 spec。\n - 缺省时按以下顺序解析:\n 1. **从 session 上下文恢复**:扫描当前会话历史中已出现/已读写过的 spec 文档路径;存在多个时优先选取最近一次被读写的;若仍有歧义,向用户确认。\n 2. **从当前 prompt 创建**:若上下文中没有任何 spec 文档,按 [新建 spec 流程](./stages.md#new-spec) 生成路径与骨架。\n- spec 目录默认为 `.yorz/specs`,可由 `<ProjectRoot>/.yorz/config.json` 的 `specsDir` 字段覆盖。\n- `mode`:可选,`plan|tasks|execute|auto`,默认 `auto`。\n- 批注前缀:`!!!`\n\n## frontmatter 规范\n\n每个 spec md 顶部必须有 YAML frontmatter:\n\n```yaml\n---\nstage: plan # plan | tasks | execute | done\nlast_action: 简述上一次动作\nupdated_at: \'2026-06-14 15:42:07\' # 秒级 + 单引号\nsummary: 一句话概要,≤ 200 字符\n---\n```\n\n- 字段顺序固定为 `stage` → `last_action` → `updated_at` → `summary`,每个字段独占一行,禁止嵌套与额外字段。\n- `updated_at` 使用本地秒级 `YYYY-MM-DD HH:mm:ss`,**写入时必须用单引号包裹**。\n- 缺失任意字段时,先补齐再继续。\n\n## Markdown 格式化约定\n\n- 每次写回 spec md 后,**应在仓库支持的条件下运行 Markdown formatter**(优先 `npx prettier --write <spec_path>`);未配置则跳过。\n- formatter 必须保留 YAML frontmatter 不变。\n- 任务清单仅使用单层 `- [ ]` / `- [x]`,避免缩进与嵌套。\n- 二、三级标题必须带层级编号,便于外部按章节号定位:\n - 每次写回前按 body 中 `## ` / `### ` 出现顺序重新编号。\n - 二级标题 `## N. 标题`(N 从 1 起);三级标题 `### N.M 标题`(M 在所属二级下从 1 起)。\n - 该规则不影响 frontmatter 与一级标题(`# `)。\n\n## 自动模式判定顺序(严格按顺序)\n\n`mode=auto` 时,**严格按顺序**判定,命中即停:\n\n1. 若 `spec_path` 缺省:从 session 上下文恢复 spec 路径;恢复失败进入 [新建 spec 流程](./stages.md#new-spec) 并以 `plan` 起步。\n2. **优先扫描 `## 追加任务` 中是否存在 `[open]` 条目**:存在即视为"新输入"信号,进入 `plan`(重开流程)。\n3. 若识别到新增/扩展需求或新增 bug,进入 `plan`(重开流程)。\n4. 若文档存在任意 `!!!` 批注,进入 [tasks](./stages.md#tasks)。\n5. 若 `## 待确认项` 下存在有效条目(非空态 `_暂无_`),停止推进并等待人工批注。\n6. 若 `stage` 已是 `done` 且上述 1–5 均未命中(无新输入/批注/待确认项),直接停止:终止态不再自动推进。\n7. 若存在未完成的**非 `[manual]`** 任务(`- [ ]` 且非 `- [ ] [manual]`),进入 [execute](./stages.md#execute)。\n8. 若任务清单已就绪且不存在未完成的非 manual 任务(走到此处已排除 `[open]`/新需求/批注/待确认项):将 `stage` 置为 `done` 并停止推进(终止态)。\n9. 若 `## 技术实现方案` 为空或明显不完整,进入 [plan](./stages.md#plan)。\n10. 进入 [tasks](./stages.md#tasks) 生成或刷新详细任务清单。\n\n> 章节名 `## 待确认项` 为新名,parser/lint 兼容旧名 `## 待确认问题`;存量 spec 保留旧名仍可识别,新建/重写一律用 `待确认项`。\n>\n> 待确认项判定:章节内存在任一 `### ` 三级标题即视为未决;仅有 `_暂无_` 或整章为空即视为无未决条目;存在未决条目时禁止进入 execute。\n>\n> `done` 判定忽略 `- [ ] [manual]` 人工确认项:即使 manual 项仍未勾选,只要其余任务全部完成、且无待确认项/批注/`[open]`,即可收尾为 `done`。\n\n## 全局硬约束\n\n### 通用\n\n- 仅更新目标 spec 文档,不修改无关文档。\n- 每次写回文档都必须更新 frontmatter 元信息。\n- 执行任务时若涉及代码改动,只改动与任务相关的最小范围。\n\n### 章节建议\n\n初始化 spec 时**建议**创建以下章节(`## 追加任务` `## 用户批注` 为可选,由用户触发追加时懒插入):\n\n- `## 现状分析` / `## 技术实现方案` / `## 待确认项` / `## 任务清单` / `## 执行记录`\n\n> lint 不再强制校验章节齐全;但 routing 按章节名查找时,章节不存在视为空态。\n\n### 变更重开流程\n\n**触发条件:**\n\n- 在已有 spec 中识别到"新增/扩展需求"或"新增 bug",将 `stage` 切回 `plan`,`last_action` 记录"变更重开流程"。\n- `## 追加任务` 存在 `- [open] [feat|refct|fix] ...` 条目时,将 `stage` 切回 `plan`。\n\n**重开后必须做的事:**\n\n- 仅针对新增内容重新走完 `plan → tasks → execute`。\n- 未受影响的既有结论/任务可保留,受影响任务必须重新评估。\n- plan 阶段消费追加任务条目时,**不修改** `[open]` 状态标记。\n\n## 输出优先级\n\n1. 先保证状态正确(frontmatter 元信息更新)。\n2. 再保证结构正确(列表格式、任务格式、候选项)。\n3. 最后保证内容质量(可执行、可验收、可追踪)。\n\n## 写回后的 lint 硬约束\n\n任何阶段完成对 `spec.md` / `review.md` 的写入后,Agent **必须**通过 Bash 运行 `yorz lint <path> --format json`,并 parse stdout。\n\n- `errorCount === 0` 时视为通过。\n- 存在 `severity: error` 时按 `ruleId` + `message` + `line` 定位并修改,然后重新运行 lint。\n- 同一文件 lint 连续失败达到 **3 次**,将偏差作为 `## 待确认项` 条目写入 spec.md 后退出,等待人工干预。\n- `warn` 级 finding 不阻断推进,但应尽力消除。\n\n## 持续推进硬约束\n\nAgent 在 plan / tasks / execute 任意阶段,**只允许**因以下"合法阻塞"退出当轮:\n\n1. `## 待确认项` 章节存在非 `暂无` 条目;\n2. spec md 内存在任何 `!!!` 批注;\n3. 识别到新增/扩展需求或新增 bug,按变更重开流程切回 `plan`;\n4. 执行外部命令 / 工具调用失败,需要用户做不可推断的决断。\n\n**显式禁止**以下"元确认"行为:\n\n- 以"任务很多 / 改动较大 / 范围广 / 时间长 / 风险高"为由询问"是否继续 / 是否执行 / 是否同意此方案"。\n- 在 execute 阶段中段汇报进度并要求用户确认下一步。\n- 反问类如"要不要我开始 / 是否继续下一项"等问句。\n\n替代约定:tasks/execute 阶段若产生新疑问,**必须**作为新条目写回 `## 待确认项` 并按变更重开流程退出。\n\n兜底:对外部世界有副作用、不可逆的命令(如 `git push`、`git reset --hard`、`rm -rf`、修改 CI 配置等破坏性操作)仍按系统默认安全准则处理。\n\n## 与 YorZ 工作流的关联\n\n- 本 skill 不依赖 YorZ Service / GUI 也能独立工作:Agent 直接读写 `spec_path` 即可。\n- 当 YorZ Service 在线时,Agent 写回 md 会被 Service 的 FS Watcher 感知并推送给 GUI。\n- 如需用户介入,Agent 把信息写回文档对应章节后立即退出,由用户人工补齐 `!!!` 批注,再由 CLI / Service 重新拉起 Agent 继续执行。\n';
60
+ const __vite_glob_0_2 = '---\nname: yorz-spec\ndescription: Drive YorZ spec docs through plan / tasks / execute stages with deterministic state updates.\n---\n\n# YorZ Spec Drive Skill\n\n将单个 YorZ spec 文档作为状态机执行,围绕 `plan` / `tasks` / `execute` 三阶段推进,全部任务完成后进入终止态 `done`,把状态持续写回文档。md 是单一真相;Agent 持续推进直至阻塞(待确认项、决策、Review)或收尾为 `done` 才退出。\n\n## 如何使用本 skill\n\n本 skill 文档结构精简为 3 个文件;每次接到 spec 任务时,**按以下顺序**按需 Read:\n\n1. **本文档(SKILL.md)**:输入约定、frontmatter 规范、格式约定、自动模式判定、全局硬约束、lint 硬约束、持续推进约束。\n2. **[stages.md](./stages.md)**:plan / tasks / execute / new-spec 四阶段流程与正面示例。\n3. **[review.md](./review.md)**:Review / Git Ops 阶段(独立路径,低频使用)。\n\n[mermaid.md](./mermaid.md) 与 `references/` 目录**仅按需 Read**:只有在判断当前阶段需要输出 mermaid 图表「升维」时才加载,纯状态推进任务不必读取,以节省 context。**plan 阶段的「图形化补充」收尾子步骤**(见 stages.md)会针对 `现状分析`/`技术方案` 两节强制加载 mermaid.md 补图;补图须遵循其核心原则**「图优先、精确信息折叠」**——表层给图,精确细节折叠进 `<details>` 精确层。\n\n> 当 Agent 以 `mode=review` / `mode=git-ops` 启动时,按 [review.md](./review.md) 执行;该路径不进入 plan/tasks/execute 状态机,也不修改 spec.md 的 frontmatter。\n\n## 输入约定\n\n- `spec_path`:**可选**,目标 spec 文档路径。\n - 显式给出时:典型位置 `docs/specs/*.md` 或 `.yorz/specs/<id>/spec.md`,直接更新该 spec。\n - 缺省时按以下顺序解析:\n 1. **从 session 上下文恢复**:扫描当前会话历史中已出现/已读写过的 spec 文档路径;存在多个时优先选取最近一次被读写的;若仍有歧义,向用户确认。\n 2. **从当前 prompt 创建**:若上下文中没有任何 spec 文档,按 [新建 spec 流程](./stages.md#new-spec) 生成路径与骨架。\n- spec 目录默认为 `.yorz/specs`,可由 `<ProjectRoot>/.yorz/config.json` 的 `specsDir` 字段覆盖。\n- `mode`:可选,`plan|tasks|execute|auto`,默认 `auto`。\n- 批注前缀:`!!!`\n\n## frontmatter 规范\n\n每个 spec md 顶部必须有 YAML frontmatter:\n\n```yaml\n---\nstage: plan # plan | tasks | execute | done\nlast_action: 简述上一次动作\nupdated_at: \'2026-06-14 15:42:07\' # 本地秒级字符串\nsummary: 一句话概要,≤ 200 字符\n---\n```\n\n- 字段顺序固定为 `stage` → `last_action` → `updated_at` → `summary`,每个字段独占一行,禁止嵌套与额外字段。\n- `updated_at` 使用本地秒级 `YYYY-MM-DD HH:mm:ss` 字符串。\n- 缺失任意字段时,先补齐再继续。\n\n## Markdown 格式化约定\n\n- 每次写回 spec md 后,**应在仓库支持的条件下运行 Markdown formatter**(优先 `npx prettier --write <spec_path>`);未配置则跳过。\n- formatter 必须保留 YAML frontmatter 不变。\n- 任务清单仅使用单层 `- [ ]` / `- [x]`,避免缩进与嵌套。\n- 二、三级标题必须带层级编号,便于外部按章节号定位:\n - 每次写回前按 body 中 `## ` / `### ` 出现顺序重新编号。\n - 二级标题 `## N. 标题`(N 从 1 起);三级标题 `### N.M 标题`(M 在所属二级下从 1 起)。\n - 该规则不影响 frontmatter 与一级标题(`# `)。\n\n## 自动模式判定顺序(严格按顺序)\n\n`mode=auto` 时,**严格按顺序**判定,命中即停:\n\n1. 若 `spec_path` 缺省:从 session 上下文恢复 spec 路径;恢复失败进入 [新建 spec 流程](./stages.md#new-spec) 并以 `plan` 起步。\n2. **优先扫描 `## 追加任务` 中是否存在 `[open]` 条目**:存在即视为"新输入"信号,进入 `plan`(重开流程)。\n3. 若识别到新增/扩展需求或新增 bug,进入 `plan`(重开流程)。\n4. 若文档存在任意 `!!!` 批注,进入 [tasks](./stages.md#tasks)。\n5. 若 `## 待确认项` 下存在有效条目(非空态 `_暂无_`),停止推进并等待人工批注。\n6. 若 `stage` 已是 `done` 且上述 1–5 均未命中(无新输入/批注/待确认项),直接停止:终止态不再自动推进。\n7. 若存在未完成的**非 `[manual]`** 任务(`- [ ]` 且非 `- [ ] [manual]`),进入 [execute](./stages.md#execute)。\n8. 若任务清单已就绪且不存在未完成的非 manual 任务(走到此处已排除 `[open]`/新需求/批注/待确认项):将 `stage` 置为 `done` 并停止推进(终止态)。\n9. 若 `## 技术实现方案` 为空或明显不完整,进入 [plan](./stages.md#plan)。\n10. 进入 [tasks](./stages.md#tasks) 生成或刷新详细任务清单。\n\n> 章节名 `## 待确认项` 为新名,parser/lint 兼容旧名 `## 待确认问题`;存量 spec 保留旧名仍可识别,新建/重写一律用 `待确认项`。\n>\n> 待确认项判定:章节内存在任一 `### ` 三级标题即视为未决;仅有 `_暂无_` 或整章为空即视为无未决条目;存在未决条目时禁止进入 execute。\n>\n> `done` 判定忽略 `- [ ] [manual]` 人工确认项:即使 manual 项仍未勾选,只要其余任务全部完成、且无待确认项/批注/`[open]`,即可收尾为 `done`。\n\n## 全局硬约束\n\n### 通用\n\n- 仅更新目标 spec 文档,不修改无关文档。\n- 每次写回文档都必须更新 frontmatter 元信息。\n- 执行任务时若涉及代码改动,只改动与任务相关的最小范围。\n\n### 章节建议\n\n初始化 spec 时**建议**创建以下章节(`## 追加任务` `## 用户批注` 为可选,由用户触发追加时懒插入):\n\n- `## 现状分析` / `## 技术实现方案` / `## 待确认项` / `## 任务清单` / `## 执行记录`\n\n> lint 不再强制校验章节齐全;但 routing 按章节名查找时,章节不存在视为空态。\n\n### 变更重开流程\n\n**触发条件:**\n\n- 在已有 spec 中识别到"新增/扩展需求"或"新增 bug",将 `stage` 切回 `plan`,`last_action` 记录"变更重开流程"。\n- `## 追加任务` 存在 `- [open] [feat|refct|fix] ...` 条目时,将 `stage` 切回 `plan`。\n\n**重开后必须做的事:**\n\n- 仅针对新增内容重新走完 `plan → tasks → execute`。\n- 未受影响的既有结论/任务可保留,受影响任务必须重新评估。\n- plan 阶段消费追加任务条目时,**不修改** `[open]` 状态标记。\n\n## 输出优先级\n\n1. 先保证状态正确(frontmatter 元信息更新)。\n2. 再保证结构正确(列表格式、任务格式、候选项)。\n3. 最后保证内容质量(可执行、可验收、可追踪)。\n\n## 写回后的 lint 硬约束\n\n任何阶段完成对 `spec.md` / `review.md` 的写入后,Agent **必须**通过 Bash 运行 `yorz lint <path> --format json`,并 parse stdout。\n\n- `errorCount === 0` 时视为通过。\n- 存在 `severity: error` 时按 `ruleId` + `message` + `line` 定位并修改,然后重新运行 lint。\n- 同一文件 lint 连续失败达到 **3 次**,将偏差作为 `## 待确认项` 条目写入 spec.md 后退出,等待人工干预。\n- `warn` 级 finding 不阻断推进,但应尽力消除。\n\n## 持续推进硬约束\n\nAgent 在 plan / tasks / execute 任意阶段,**只允许**因以下"合法阻塞"退出当轮:\n\n1. `## 待确认项` 章节存在非 `暂无` 条目;\n2. spec md 内存在任何 `!!!` 批注;\n3. 识别到新增/扩展需求或新增 bug,按变更重开流程切回 `plan`;\n4. 执行外部命令 / 工具调用失败,需要用户做不可推断的决断。\n\n**显式禁止**以下"元确认"行为:\n\n- 以"任务很多 / 改动较大 / 范围广 / 时间长 / 风险高"为由询问"是否继续 / 是否执行 / 是否同意此方案"。\n- 在 execute 阶段中段汇报进度并要求用户确认下一步。\n- 反问类如"要不要我开始 / 是否继续下一项"等问句。\n\n替代约定:tasks/execute 阶段若产生新疑问,**必须**作为新条目写回 `## 待确认项` 并按变更重开流程退出。\n\n兜底:对外部世界有副作用、不可逆的命令(如 `git push`、`git reset --hard`、`rm -rf`、修改 CI 配置等破坏性操作)仍按系统默认安全准则处理。\n\n## 与 YorZ 工作流的关联\n\n- 本 skill 不依赖 YorZ Service / GUI 也能独立工作:Agent 直接读写 `spec_path` 即可。\n- 当 YorZ Service 在线时,Agent 写回 md 会被 Service 的 FS Watcher 感知并推送给 GUI。\n- 如需用户介入,Agent 把信息写回文档对应章节后立即退出,由用户人工补齐 `!!!` 批注,再由 CLI / Service 重新拉起 Agent 继续执行。\n';
61
61
  const __vite_glob_0_3 = "---\nstage: execute\nlast_action: 主任务完成,追加 1 项 [open] 待处理\nupdated_at: 2026-06-20\nsummary: HELLO.txt 已经创建;追加任务:在 HELLO.txt 末尾追加一行 `goodbye world\\n`。\n---\n\n# HELLO 占位\n\n## 1. 背景\n\n希望仓库根有占位文件,便于演示。\n\n## 2. 需求\n\n- 在仓库根创建 `HELLO.txt`,内容 `hello world\\n`\n\n## 3. 现状分析\n\n`HELLO.txt` 已经存在,内容 `hello world\\n`。\n\n## 4. 技术实现方案\n\n- 直接写入 `HELLO.txt`,内容 `hello world\\n`。\n\n## 5. 待确认问题\n\n- 暂无\n\n## 6. 任务清单\n\n- [x] 在仓库根新建 `HELLO.txt`,内容 `hello world\\n`,验收点:文件存在且字节数 = 12。\n\n## 7. 追加任务\n\n- [open] 在 `HELLO.txt` 末尾追加一行 `goodbye world\\n`,验收点:最终文件含 2 行、共 26 字节。\n\n## 8. 执行记录\n\n- 2026-06-20 创建 `HELLO.txt`,内容 `hello world\\n`。\n";
62
62
  const __vite_glob_0_4 = '{\n "module": "execute"\n}\n';
63
63
  const __vite_glob_0_5 = "---\nstage: execute\nlast_action: 任务已就绪,等待执行\nupdated_at: 2026-06-20\nsummary: 在仓库根新增 HELLO.txt 占位文件,内容为 hello world。\n---\n\n# HELLO 占位\n\n## 1. 背景\n\n希望在仓库根有一个 HELLO.txt 占位文件,用于演示 execute 阶段的勾选行为。\n\n## 2. 需求\n\n- 在仓库根创建 `HELLO.txt`,内容固定为 `hello world\\n`\n\n## 3. 现状分析\n\n仓库根目前没有 `HELLO.txt`,需新建。\n\n## 4. 技术实现方案\n\n- 直接写入 `HELLO.txt`,内容 `hello world\\n`,无 BOM、无尾随空行。\n\n## 5. 待确认问题\n\n- 暂无\n\n## 6. 任务清单\n\n- [ ] 在仓库根新建 `HELLO.txt`,内容为 `hello world\\n`,验收点:文件存在且内容字节数 = 12。\n\n## 7. 追加任务\n\n- 暂无\n\n## 8. 执行记录\n\n- 暂无\n";
@@ -97,7 +97,7 @@ const __vite_glob_0_38 = '> **Warning**\n>\n> ## THIS IS AN AUTOGENERATED FILE.
97
97
  const __vite_glob_0_39 = '> **Warning**\n>\n> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT.\n>\n> ## Please edit the corresponding file in [/packages/mermaid/src/docs/syntax/treemap.md](../../packages/mermaid/src/docs/syntax/treemap.md).\n\n# Treemap Diagram\n\n> A treemap diagram displays hierarchical data as a set of nested rectangles. Each branch of the tree is represented by a rectangle, which is then tiled with smaller rectangles representing sub-branches.\n\n> **Warning**\n> This is a new diagram type in Mermaid. Its syntax may evolve in future versions.\n\n## Introduction\n\nTreemap diagrams are an effective way to visualize hierarchical data and show proportions between categories and subcategories. The size of each rectangle is proportional to the value it represents, making it easy to compare different parts of a hierarchy.\n\nTreemap diagrams are particularly useful for:\n\n- Visualizing hierarchical data structures\n- Comparing proportions between categories\n- Displaying large amounts of hierarchical data in a limited space\n- Identifying patterns and outliers in hierarchical data\n\n## Syntax\n\n```\ntreemap-beta\n"Section 1"\n "Leaf 1.1": 12\n "Section 1.2"\n "Leaf 1.2.1": 12\n"Section 2"\n "Leaf 2.1": 20\n "Leaf 2.2": 25\n```\n\n### Node Definition\n\nNodes in a treemap are defined using the following syntax:\n\n- **Section/Parent nodes**: Defined with quoted text `"Section Name"`\n- **Leaf nodes with values**: Defined with quoted text followed by a colon and value `"Leaf Name": value`\n- **Hierarchy**: Created using indentation (spaces or tabs)\n- **Styling**: Nodes can be styled using the `:::class` syntax\n\n## Examples\n\n### Basic Treemap\n\n```mermaid-example\ntreemap-beta\n"Category A"\n "Item A1": 10\n "Item A2": 20\n"Category B"\n "Item B1": 15\n "Item B2": 25\n```\n\n```mermaid\ntreemap-beta\n"Category A"\n "Item A1": 10\n "Item A2": 20\n"Category B"\n "Item B1": 15\n "Item B2": 25\n```\n\n### Hierarchical Treemap\n\n```mermaid-example\ntreemap-beta\n"Products"\n "Electronics"\n "Phones": 50\n "Computers": 30\n "Accessories": 20\n "Clothing"\n "Men\'s": 40\n "Women\'s": 40\n```\n\n```mermaid\ntreemap-beta\n"Products"\n "Electronics"\n "Phones": 50\n "Computers": 30\n "Accessories": 20\n "Clothing"\n "Men\'s": 40\n "Women\'s": 40\n```\n\n### Treemap with Styling\n\n```mermaid-example\ntreemap-beta\n"Section 1"\n "Leaf 1.1": 12\n "Section 1.2":::class1\n "Leaf 1.2.1": 12\n"Section 2"\n "Leaf 2.1": 20:::class1\n "Leaf 2.2": 25\n "Leaf 2.3": 12\n\nclassDef class1 fill:red,color:blue,stroke:#FFD600;\n```\n\n```mermaid\ntreemap-beta\n"Section 1"\n "Leaf 1.1": 12\n "Section 1.2":::class1\n "Leaf 1.2.1": 12\n"Section 2"\n "Leaf 2.1": 20:::class1\n "Leaf 2.2": 25\n "Leaf 2.3": 12\n\nclassDef class1 fill:red,color:blue,stroke:#FFD600;\n```\n\n## Styling and Configuration\n\nTreemap diagrams can be customized using Mermaid\'s styling and configuration options.\n\n### Using classDef for Styling\n\nYou can define custom styles for nodes using the `classDef` syntax, which is a standard feature across many Mermaid diagram types:\n\n```mermaid-example\ntreemap-beta\n"Main"\n "A": 20\n "B":::important\n "B1": 10\n "B2": 15\n "C": 5\n\nclassDef important fill:#f96,stroke:#333,stroke-width:2px;\n```\n\n```mermaid\ntreemap-beta\n"Main"\n "A": 20\n "B":::important\n "B1": 10\n "B2": 15\n "C": 5\n\nclassDef important fill:#f96,stroke:#333,stroke-width:2px;\n```\n\n### Theme Configuration\n\nYou can customize the colors of your treemap using the theme configuration:\n\n```mermaid-example\n---\nconfig:\n theme: \'forest\'\n---\ntreemap-beta\n"Category A"\n "Item A1": 10\n "Item A2": 20\n"Category B"\n "Item B1": 15\n "Item B2": 25\n```\n\n```mermaid\n---\nconfig:\n theme: \'forest\'\n---\ntreemap-beta\n"Category A"\n "Item A1": 10\n "Item A2": 20\n"Category B"\n "Item B1": 15\n "Item B2": 25\n```\n\n### Diagram Padding\n\nYou can adjust the padding around the treemap diagram using the `diagramPadding` configuration option:\n\n```mermaid-example\n---\nconfig:\n treemap:\n diagramPadding: 200\n---\ntreemap-beta\n"Category A"\n "Item A1": 10\n "Item A2": 20\n"Category B"\n "Item B1": 15\n "Item B2": 25\n```\n\n```mermaid\n---\nconfig:\n treemap:\n diagramPadding: 200\n---\ntreemap-beta\n"Category A"\n "Item A1": 10\n "Item A2": 20\n"Category B"\n "Item B1": 15\n "Item B2": 25\n```\n\n## Configuration Options\n\nThe treemap diagram supports the following configuration options:\n\n| Option | Description | Default |\n| -------------- | --------------------------------------------------------------------------- | ------- |\n| useMaxWidth | When true, the diagram width is set to 100% and scales with available space | true |\n| padding | Internal padding between nodes | 10 |\n| diagramPadding | Padding around the entire diagram | 8 |\n| showValues | Whether to show values in the treemap | true |\n| nodeWidth | Width of nodes | 100 |\n| nodeHeight | Height of nodes | 40 |\n| borderWidth | Width of borders | 1 |\n| valueFontSize | Font size for values | 12 |\n| labelFontSize | Font size for labels | 14 |\n| valueFormat | Format for values (see Value Formatting section) | \',\' |\n\n## Advanced Features\n\n### Value Formatting\n\nValues in treemap diagrams can be formatted to display in different ways using the `valueFormat` configuration option. This option primarily uses [D3\'s format specifiers](https://github.com/d3/d3-format#locale_format) to control how numbers are displayed, with some additional special cases for common formats.\n\nSome common format patterns:\n\n- `,` - Thousands separator (default)\n- `$` - Add dollar sign\n- `.1f` - Show one decimal place\n- `.1%` - Show as percentage with one decimal place\n- `$0,0` - Dollar sign with thousands separator\n- `$.2f` - Dollar sign with 2 decimal places\n- `$,.2f` - Dollar sign with thousands separator and 2 decimal places\n\nThe treemap diagram supports both standard D3 format specifiers and some common currency formats that combine the dollar sign with other formatting options.\n\nExample with currency formatting:\n\n```mermaid-example\n---\nconfig:\n treemap:\n valueFormat: \'$0,0\'\n---\ntreemap-beta\n"Budget"\n "Operations"\n "Salaries": 700000\n "Equipment": 200000\n "Supplies": 100000\n "Marketing"\n "Advertising": 400000\n "Events": 100000\n```\n\n```mermaid\n---\nconfig:\n treemap:\n valueFormat: \'$0,0\'\n---\ntreemap-beta\n"Budget"\n "Operations"\n "Salaries": 700000\n "Equipment": 200000\n "Supplies": 100000\n "Marketing"\n "Advertising": 400000\n "Events": 100000\n```\n\nExample with percentage formatting:\n\n```mermaid-example\n---\nconfig:\n treemap:\n valueFormat: \'$.1%\'\n---\ntreemap-beta\n"Market Share"\n "Company A": 0.35\n "Company B": 0.25\n "Company C": 0.15\n "Others": 0.25\n```\n\n```mermaid\n---\nconfig:\n treemap:\n valueFormat: \'$.1%\'\n---\ntreemap-beta\n"Market Share"\n "Company A": 0.35\n "Company B": 0.25\n "Company C": 0.15\n "Others": 0.25\n```\n\n## Common Use Cases\n\nTreemap diagrams are commonly used for:\n\n1. **Financial Data**: Visualizing budget allocations, market shares, or portfolio compositions\n2. **File System Analysis**: Showing disk space usage by folders and files\n3. **Population Demographics**: Displaying population distribution across regions and subregions\n4. **Product Hierarchies**: Visualizing product categories and their sales volumes\n5. **Organizational Structures**: Representing departments and team sizes in a company\n\n## Limitations\n\n- Treemap diagrams work best when the data has a natural hierarchy\n- Very small values may be difficult to see or label in a treemap diagram\n- Deep hierarchies (many levels) can be challenging to represent clearly\n- Treemap diagrams are not well suited for representing data with negative values\n\n## Related Diagrams\n\nIf treemap diagrams don\'t suit your needs, consider these alternatives:\n\n- [**Pie Charts**](./pie.md): For simple proportion comparisons without hierarchy\n- **Sunburst Diagrams**: For hierarchical data with a radial layout (yet to be released in Mermaid).\n- [**Sankey Diagrams**](./sankey.md): For flow-based hierarchical data\n\n## Notes\n\nThe treemap diagram implementation in Mermaid is designed to be simple to use while providing powerful visualization capabilities. As this is a newer diagram type, feedback and feature requests are welcome through the Mermaid GitHub repository.\n';
98
98
  const __vite_glob_0_40 = '> **Warning**\n>\n> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT.\n>\n> ## Please edit the corresponding file in [/packages/mermaid/src/docs/syntax/xyChart.md](../../packages/mermaid/src/docs/syntax/xyChart.md).\n\n# XY Chart\n\n> In the context of mermaid-js, the XY chart is a comprehensive charting module that encompasses various types of charts that utilize both x-axis and y-axis for data representation. Presently, it includes two fundamental chart types: the bar chart and the line chart. These charts are designed to visually display and analyze data that involve two numerical variables.\n\n> It\'s important to note that while the current implementation of mermaid-js includes these two chart types, the framework is designed to be dynamic and adaptable. Therefore, it has the capacity for expansion and the inclusion of additional chart types in the future. This means that users can expect an evolving suite of charting options within the XY chart module, catering to various data visualization needs as new chart types are introduced over time.\n\n## Example\n\n```mermaid-example\nxychart\n title "Sales Revenue"\n x-axis [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]\n y-axis "Revenue (in $)" 4000 --> 11000\n bar [5000, 6000, 7500, 8200, 9500, 10500, 11000, 10200, 9200, 8500, 7000, 6000]\n line [5000, 6000, 7500, 8200, 9500, 10500, 11000, 10200, 9200, 8500, 7000, 6000]\n```\n\n```mermaid\nxychart\n title "Sales Revenue"\n x-axis [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]\n y-axis "Revenue (in $)" 4000 --> 11000\n bar [5000, 6000, 7500, 8200, 9500, 10500, 11000, 10200, 9200, 8500, 7000, 6000]\n line [5000, 6000, 7500, 8200, 9500, 10500, 11000, 10200, 9200, 8500, 7000, 6000]\n```\n\n## Syntax\n\n> **Note**\n> All text values that contain only one word can be written without `"`. If a text value has many words in it, specifically if it contains spaces, enclose the value in `"`\n\n### Orientations\n\nThe chart can be drawn horizontal or vertical, default value is vertical.\n\n```\nxychart horizontal\n...\n```\n\n### Title\n\nThe title is a short description of the chart and it will always render on top of the chart.\n\n#### Example\n\n```\nxychart\n title "This is a simple example"\n ...\n```\n\n> **Note**\n> If the title is a single word one no need to use `"`, but if it has space `"` is needed\n\n### x-axis\n\nThe x-axis primarily serves as a categorical value, although it can also function as a numeric range value when needed.\n\n#### Example\n\n1. `x-axis title min --> max` x-axis will function as numeric with the given range\n2. `x-axis "title with space" [cat1, "cat2 with space", cat3]` x-axis if categorical, categories are text type\n\n### y-axis\n\nThe y-axis is employed to represent numerical range values, it cannot have categorical values.\n\n#### Example\n\n1. `y-axis title min --> max`\n2. `y-axis title` it will only add the title, the range will be auto generated from data.\n\n> **Note**\n> Both x and y axis are optional if not provided we will try to create the range\n\n### Line chart\n\nA line chart offers the capability to graphically depict lines.\n\n#### Example\n\n1. `line [2.3, 45, .98, -3.4]` it can have all valid numeric values.\n2. `line "series name" [2.3, 45, .98, -3.4]` adds the line to the legend.\n\n### Bar chart\n\nA bar chart offers the capability to graphically depict bars.\n\n#### Example\n\n1. `bar [2.3, 45, .98, -3.4]` it can have all valid numeric values.\n2. `bar "series name" [2.3, 45, .98, -3.4]` adds the bar to the legend.\n\n### Legend (v\\<MERMAID_RELEASE_VERSION>+)\n\nNamed line and bar plots are automatically shown in a legend. Unnamed plots are omitted from the legend.\n\n```mermaid-example\nxychart-beta\n title "An Example Chart"\n x-axis ["90d", "60d", "30d", "7d", "1d", "Current"]\n y-axis "Seconds" 0 --> 198.2\n line "avg" [48.1, 41.5, 45.7, 72.8, 67.7, 59.9]\n line "p50" [38.2, 36.8, 39.7, 54.5, 49.0, 38.4]\n line "p95" [112.2, 75.3, 103.0, 177.0, 180.2, 109.4]\n```\n\n```mermaid\nxychart-beta\n title "An Example Chart"\n x-axis ["90d", "60d", "30d", "7d", "1d", "Current"]\n y-axis "Seconds" 0 --> 198.2\n line "avg" [48.1, 41.5, 45.7, 72.8, 67.7, 59.9]\n line "p50" [38.2, 36.8, 39.7, 54.5, 49.0, 38.4]\n line "p95" [112.2, 75.3, 103.0, 177.0, 180.2, 109.4]\n```\n\n#### Simplest example\n\nThe only two things required are the chart name (`xychart`) and one data set. So you will be able to draw a chart with a simple config like\n\n```\nxychart\n line [+1.3, .6, 2.4, -.34]\n```\n\n## Chart Configurations\n\n| Parameter | Description | Default value |\n| ------------------------ | ------------------------------------------------------------- | :-----------: |\n| width | Width of the chart | 700 |\n| height | Height of the chart | 500 |\n| titlePadding | Top and Bottom padding of the title | 10 |\n| titleFontSize | Title font size | 20 |\n| showTitle | Title to be shown or not | true |\n| showLegend | Legend to be shown for named plots or not | true |\n| legendFontSize | Legend font size | 14 |\n| legendPadding | Padding around the legend | 10 |\n| xAxis | xAxis configuration | AxisConfig |\n| yAxis | yAxis configuration | AxisConfig |\n| chartOrientation | \'vertical\' or \'horizontal\' | \'vertical\' |\n| plotReservedSpacePercent | Minimum space plots will take inside the chart | 50 |\n| showDataLabel | Should show the value corresponding to the bar within the bar | false |\n| showDataLabelOutsideBar | If showing data label then show it outside the bar. | false |\n\n### AxisConfig\n\n| Parameter | Description | Default value |\n| ------------- | ------------------------------------------------------------ | :-----------: |\n| showLabel | Show axis labels or tick values | true |\n| labelFontSize | Font size of the label to be drawn | 14 |\n| labelPadding | Top and Bottom padding of the label | 5 |\n| showTitle | Axis title to be shown or not | true |\n| titleFontSize | Axis title font size | 16 |\n| titlePadding | Top and Bottom padding of Axis title | 5 |\n| showTick | Tick to be shown or not | true |\n| tickLength | How long the tick will be | 5 |\n| tickWidth | How width the tick will be | 2 |\n| showAxisLine | Axis line to be shown or not | true |\n| axisLineWidth | Thickness of the axis line | 2 |\n| labelRotation | Label rotation in degrees (only applicable on bottom X axis) | 0 |\n\n## Chart Theme Variables\n\nThemes for xychart reside inside the `xychart` attribute, allowing customization through the following syntax:\n\n```yaml\n---\nconfig:\n themeVariables:\n xyChart:\n titleColor: \'#ff0000\'\n---\n```\n\n| Parameter | Description |\n| ---------------- | --------------------------------------------------------- |\n| backgroundColor | Background color of the whole chart |\n| titleColor | Color of the Title text |\n| dataLabelColor | Color of the Data labels (if shown) |\n| legendTextColor | Color of the legend text |\n| xAxisLabelColor | Color of the x-axis labels |\n| xAxisTitleColor | Color of the x-axis title |\n| xAxisTickColor | Color of the x-axis tick |\n| xAxisLineColor | Color of the x-axis line |\n| yAxisLabelColor | Color of the y-axis labels |\n| yAxisTitleColor | Color of the y-axis title |\n| yAxisTickColor | Color of the y-axis tick |\n| yAxisLineColor | Color of the y-axis line |\n| plotColorPalette | String of colors separated by comma e.g. "#f3456, #43445" |\n\n### Setting Colors for Lines and Bars\n\nTo set the color for lines and bars, use the `plotColorPalette` parameter. Colors in the palette will correspond sequentially to the elements in your chart (e.g., first bar/line will use the first color specified in the palette).\n\n```mermaid-example\n---\nconfig:\n themeVariables:\n xyChart:\n plotColorPalette: \'#000000, #0000FF, #00FF00, #FF0000\'\n---\nxychart\ntitle "Different Colors in xyChart"\nx-axis "categoriesX" ["Category 1", "Category 2", "Category 3", "Category 4"]\ny-axis "valuesY" 0 --> 50\n%% Black line\nline [10,20,30,40]\n%% Blue bar\nbar [20,30,25,35]\n%% Green bar\nbar [15,25,20,30]\n%% Red line\nline [5,15,25,35]\n```\n\n```mermaid\n---\nconfig:\n themeVariables:\n xyChart:\n plotColorPalette: \'#000000, #0000FF, #00FF00, #FF0000\'\n---\nxychart\ntitle "Different Colors in xyChart"\nx-axis "categoriesX" ["Category 1", "Category 2", "Category 3", "Category 4"]\ny-axis "valuesY" 0 --> 50\n%% Black line\nline [10,20,30,40]\n%% Blue bar\nbar [20,30,25,35]\n%% Green bar\nbar [15,25,20,30]\n%% Red line\nline [5,15,25,35]\n```\n\n## Displaying individual values on a bar chart (v11.14.0+)\n\nTo show the value corresponding to a bar specify `showDataLabel: true`.\n\n```mermaid-example\n---\nconfig:\n xyChart:\n showDataLabel: true\n---\nxychart\n title "Genres in top 100 book survey of 2025"\n x-axis [comedy, romance, mystery, crime, "non fiction", other]\n y-axis "Number of Books" 0 --> 30\n bar [12,2,20,25,17,24]\n```\n\n```mermaid\n---\nconfig:\n xyChart:\n showDataLabel: true\n---\nxychart\n title "Genres in top 100 book survey of 2025"\n x-axis [comedy, romance, mystery, crime, "non fiction", other]\n y-axis "Number of Books" 0 --> 30\n bar [12,2,20,25,17,24]\n```\n\nLabels are shown within the bar by default. To show the labels outside the bar, specify `showDataLabelOutsideBar: true`.\n\n```mermaid-example\n---\nconfig:\n xyChart:\n showDataLabel: true\n showDataLabelOutsideBar: true\n---\nxychart\n title "Genres in top 100 book survey of 2025"\n x-axis [comedy, romance, mystery, crime, "non fiction", other]\n y-axis "Number of Books" 0 --> 30\n bar [12,2,20,25,17,24]\n```\n\n```mermaid\n---\nconfig:\n xyChart:\n showDataLabel: true\n showDataLabelOutsideBar: true\n---\nxychart\n title "Genres in top 100 book survey of 2025"\n x-axis [comedy, romance, mystery, crime, "non fiction", other]\n y-axis "Number of Books" 0 --> 30\n bar [12,2,20,25,17,24]\n```\n\n## Per-point text labels for line charts (v\\<MERMAID_RELEASE_VERSION>+)\n\nEach data point in a `line` can optionally include a quoted string label after the numeric value. Labels render above points in vertical orientation, or to the right in horizontal orientation, using the line\'s stroke color.\n\n```mermaid-example\nxychart\n title "Smallest AI models scoring above 60% on MMLU"\n x-axis "Date" ["Apr 2022", "Feb 2023", "Jul 2023", "Sep 2023", "Apr 2024"]\n y-axis "Parameters (B)" 0 --> 600\n line [540 "PaLM", 65 "LLaMA-65B", 34 "Llama 2 34B", 7 "Mistral 7B", 3.8 "Phi-3-mini"]\n```\n\n```mermaid\nxychart\n title "Smallest AI models scoring above 60% on MMLU"\n x-axis "Date" ["Apr 2022", "Feb 2023", "Jul 2023", "Sep 2023", "Apr 2024"]\n y-axis "Parameters (B)" 0 --> 600\n line [540 "PaLM", 65 "LLaMA-65B", 34 "Llama 2 34B", 7 "Mistral 7B", 3.8 "Phi-3-mini"]\n```\n\nLabels are optional per point — you can mix labeled and unlabeled values:\n\n```mermaid-example\nxychart\n title "Quarterly Performance"\n x-axis [Q1, Q2, Q3, Q4]\n y-axis "Revenue ($M)" 0 --> 100\n line [25 "Launch", 45, 72, 90 "Target Hit"]\n```\n\n```mermaid\nxychart\n title "Quarterly Performance"\n x-axis [Q1, Q2, Q3, Q4]\n y-axis "Revenue ($M)" 0 --> 100\n line [25 "Launch", 45, 72, 90 "Target Hit"]\n```\n\nExisting syntax without labels continues to work unchanged.\n\n> **Note**\n> Point labels use a fixed font size of 12px. In vertical charts, labels appear above each point. In horizontal charts, labels appear to the right. Labels are currently supported on `line` plots only; the syntax is accepted on `bar` plots but labels are ignored.\n\n## Example on config and theme\n\n```mermaid-example\n---\nconfig:\n xyChart:\n width: 900\n height: 600\n showDataLabel: true\n themeVariables:\n xyChart:\n titleColor: "#ff0000"\n---\nxychart\n title "Sales Revenue"\n x-axis [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]\n y-axis "Revenue (in $)" 4000 --> 11000\n bar [5000, 6000, 7500, 8200, 9500, 10500, 11000, 10200, 9200, 8500, 7000, 6000]\n line [5000, 6000, 7500, 8200, 9500, 10500, 11000, 10200, 9200, 8500, 7000, 6000]\n```\n\n```mermaid\n---\nconfig:\n xyChart:\n width: 900\n height: 600\n showDataLabel: true\n themeVariables:\n xyChart:\n titleColor: "#ff0000"\n---\nxychart\n title "Sales Revenue"\n x-axis [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]\n y-axis "Revenue (in $)" 4000 --> 11000\n bar [5000, 6000, 7500, 8200, 9500, 10500, 11000, 10200, 9200, 8500, 7000, 6000]\n line [5000, 6000, 7500, 8200, 9500, 10500, 11000, 10200, 9200, 8500, 7000, 6000]\n```\n';
99
99
  const __vite_glob_0_41 = '# Review / Git Ops 阶段说明\n\n本文件描述 service 端拉起 `mode=review` / `mode=git-ops`(带 `action`)时,Agent 应遵守的输入解析、输出格式与 git 安全约束。**这条路径不进入 plan/tasks/execute 状态机**,亦不修改 spec.md 的 frontmatter `stage`。\n\n## mode=review\n\n输入:\n\n- spec 文档路径(`<spec_path>`),同目录可能已存在 `review.md`。\n- 当前 git 仓库的未提交变更(`git status -sb` / `git diff` / `git diff --staged`)。\n\n行为:\n\n1. 读取 `<spec_path>` 与 git 变更,理解本次改动相对该 spec 的语义。\n2. 将结构化 review 报告**按时间降序插入**到 `<spec_dir>/review.md`(与 spec.md 同级),使**最新条目位于文件顶部**。\n - 若 `review.md` 不存在:先写入一行 `# Review · <spec-id>` 作为一级标题,然后再追加本次条目。\n - 若已存在:**禁止覆盖**历史条目;将本次二级标题及其正文块**插入到 `# Review · <spec-id>` 一级标题之后、既有第一个 `## ` 二级标题之前**,使 review 条目按时间降序(最新在顶)排列。一级标题与新条目之间保留 1 个空行,新条目与后续既有条目之间保留 1 个空行。\n3. 每次 review 的二级标题使用本机当前时间 `## YYYY-MM-DD HH:mm:ss`,时间格式与 spec frontmatter 一致。\n4. 每个二级标题下必须依次包含 4 个三级小节,顺序固定:\n\n ```markdown\n ## 2026-06-30 14:23:01\n\n ### 变更总结\n\n <内容>\n\n ### 影响范围\n\n <内容>\n\n ### 风险提醒\n\n <内容>\n\n ### 变更文件清单\n\n - path/to/a.ts\n - path/to/b.tsx\n ```\n\n5. 报告写完即结束本轮;不要修改 `spec.md`、不要执行任何 git 写操作。\n\n## mode=git-ops + action=commit\n\n- 输入:spec 文档 + 最近一次 `review.md` 条目(若存在)+ 当前 `git status`。\n- 由 Agent **自主决定**本次要提交的 spec 相关变更文件清单;优先以最近 review 的"变更文件清单"为锚,结合 `git status` 中实际存在的未提交文件做交集。\n- commit message 由 Agent 基于 review 总结自行生成;不附加 `[spec:<id>]` 锚点,不带 scope(遵循 `260630.refct.review-commit-msg-remove-scope`)。\n- 仅允许使用 `git add <paths>` + `git commit -m <message>`;**禁止** `git push` / `git reset --hard` / `git rebase` / 修改任何已提交历史。\n\n## mode=git-ops + action=discard\n\n- 使用 `git restore --staged --worktree -- <paths>` 丢弃已 tracked 的修改;untracked 新文件使用 `git clean -fd -- <paths>` 清理。\n- 对 untracked 文件先在终端输出待清理列表,再执行 clean,便于日志回溯。\n- **不**预先 `git stash` 作为"保险备份"(遵循用户显式意图,保持轻量)。\n- **禁止** `git reset --hard`、`git push`。\n\n## mode=git-ops + action=stash\n\n- 使用 `git stash push -m "yorz:<spec-id>" -- <paths>`,仅暂存 spec 相关变更文件。\n- 不要使用 `git stash --all` 之类会影响 untracked / ignored 文件的形式,除非明确需要。\n- **禁止** `git push`。\n\n## 通用硬约束\n\n- 仅操作 `<spec_path>`、`<spec_dir>/review.md` 与 git 工作区文件;不修改无关 spec、不修改本 skill 文件。\n- 任一 git 子命令失败时,把错误信息写入终端日志(service 端会落盘到 agent-logs),不要静默吞掉。\n- 元确认禁令同 SKILL.md「持续推进硬约束」:不向用户回问"是否继续 / 是否执行"等问题(GUI 已在按钮触发前做过必要的二次确认)。\n';
100
- const __vite_glob_0_42 = '# 阶段流程(plan / tasks / execute / done / new-spec)\n\n## plan\n\n理解文档,分析现有代码,编写技术实现方案,更新到当前文档中;需要用户介入的事项以三级标题写入 `## 待确认项`,待用户后续审阅批注;暂时只需更新文档,不要实施代码。\n\n**目标:** 输出 `## 现状分析`、`## 技术实现方案`、`## 待确认项` 三节;禁止输出任务清单格式 `- [ ]`。\n\n若 `## 追加任务` 存在 `- [open] [feat|refct|fix] ...` 条目,必须将其纳入分析范围;plan 阶段不修改 `[open]` 状态标记。\n\n### 待确认项的三种类型\n\n一个待确认项,要么让用户**抉择**,要么让用户**确认**,要么请用户**自由补充**:\n\n| 类型 | 标记 | 语义 | 何时用 |\n| -------- | ------------------- | --------------------------------------------------------------------------------------- | --------------------- |\n| 抉择型 | `[choice]` | 给用户**选择权**:存在多个都可行、优劣取决于你拿不到的信息(业务优先级/外部约束)的方案 | 多方案差异大、难取舍 |\n| 确认型 | `[confirm]` | 给用户**否决权**:只有一个可行方案,但它有明显代价(技术债/影响面/不可逆) | 单方案但影响重大 |\n| 自由文本 | `(自由文本)` 后缀 | 请用户补一段开放说明 | 需要主观文案/开放意见 |\n\n> 无标记且带有序候选,向后兼容视为 `[choice]`。\n\n### 准入门槛(写入待确认项前必过的自检)\n\n**第一道过滤器先于类型判断**:这个答案,你能否通过读代码、跑测试、查项目约定自己确定?\n\n- **能** → 禁止写成待确认项,自己去查、去定,把决策+理由写进 `## 技术实现方案`(决策说明),不要打断用户。\n- **不能**(决策依赖用户独有信息,或选错的返工不可逆)→ 才写成待确认项,并归入 choice/confirm。\n\n> 待确认项是压力释放阀,不是免责声明。被你砍掉的判断不该凭空消失,要落成 `## 技术实现方案` 里的决策说明(决策 + 理由 + 被否决的备选)。\n\n### 抉择型 [choice] 正确示例\n\n```\n### 5.1 [choice] 候选答案的展现形式应采用哪种?\n1. 嵌套子列表\n2. 表格 (推荐)\n```\n\n- 候选写一级有序列表;恰 1 个候选以 ` (推荐)` / ` (推荐)` 结尾。\n- 空态与格式由 lint 的 `pending-questions/*` 规则保证。\n\n### 确认型 [confirm] 正确示例\n\n```\n### 5.2 [confirm] 将 UserID 从 int 迁移为 uuid\n\n**方案**:一次性迁移,双写过渡 2 周。\n**影响**:🔴 需停机窗口;对外 /api/user 字段类型变更,客户端需同步。\n```\n\n- 只有 `**方案**` 与 `**影响**`(或 `**代价**`)两字段,**不列候选**。\n- 影响行用红/黄语义标注严重度:🔴 breaking(破坏性/不可逆)、🟡 affected(有影响但可控)。\n- 确认型 = "我要这么干了,除非你喊停" 的知会 + 急停按钮:放行是常态,用户否决才是例外。\n\n### 确认型的否决语义(用户在 GUI 上如何回应)\n\n用户对确认型只有两种回应:**确认放行** 或 **否决**。否决必须携带理由,并细分意图(GUI 渲染为嵌套单选):\n\n| 用户决策 | 回写 `!!!选择:…` | 含义 |\n| ------------------------- | ------------------------------------ | ---------------------- |\n| 确认,按此推进 | `确认,按此推进` | 接受代价,继续 |\n| 否决·换方案 | `否决·换方案` | 原目标下另寻实现 |\n| 否决·补约束 | `否决·补约束` | 用户补充你漏掉的硬约束 |\n| 否决·弃目标·废弃当前目标 | `否决·弃目标·废弃当前目标,继续其余` | 该目标不做,其余照旧 |\n| 否决·弃目标·放弃整个 spec | `否决·弃目标·放弃整个 spec` | 整条 spec 作废 |\n\n> 决策不改动 API:编码进批注的 `选择:…`(结构化意图)+ `备注:…`(理由)。tasks 阶段按下表消费。\n\n### 图形化补充(plan 收尾子步骤)\n\n方案文字定稿后、结束 plan 前,**必须**专门回看 `## 现状分析`、`## 技术实现方案` 两节做一次 mermaid 图形化补充(仅此两节,其它章节不动):\n\n- 加载 [mermaid.md](./mermaid.md),先服从其**核心原则「图优先、精确信息折叠」**:表层给图(认知层),精确细节(路径、行号、类型/源码全文)折叠进 `<details>` 精确层,图与折叠互补不重复。\n- 按选型表与「必须用图」触发词补图:分步/分支散文→flowchart,接口/类型定义→classDiagram;含「兼容性/影响范围」时出结构图并按红=breaking、黄=affected 语义配色。\n- 这是一道独立聚焦的工序(不新增 stage):把「想清楚方案 / 写成文字」与「回看补图」分开,避免图形化被跳过。补图完成后再结束 plan。\n\n### 待确认项自检(plan 收尾子步骤)\n\n方案与图形化定稿后、结束 plan 前,**必须**专门回看 `## 待确认项` 做一次自检(与「图形化补充」同构:把"写方案"与"回看校验"分开,避免自检被跳过):\n\n1. **准入过滤**:逐条问"能否自己查证?"——能则删除该条、把决策落进 `## 技术实现方案`(决策说明)。\n2. **类型归类**:留下的每条明确标 `[choice]` 或 `[confirm]`(或 `(自由文本)`);单一类型,不兼具。\n3. **确认型补影响**:每个 `[confirm]` 必带 `**方案**` 与 `**影响**`(红/黄语义);影响写不出来,说明它其实够不上确认型,降级或删除。\n\n自检完成后再结束 plan。\n\n空态整章仅一行斜体占位:\n\n```\n## 5. 待确认项\n\n_暂无_\n```\n\n> 格式由 lint 的 `pending-questions/*` 规则保证;不确定时运行 `yorz lint` 查看报错。\n\n## tasks\n\n根据批注内容(`!!!` 前缀)更新技术方案;审查批注是否正确解答了待确认项;若引入新增/扩展需求或新增 bug,切回 plan;若存在冲突或歧义,回退 plan 并写入待确认项后退出;否则将技术方案拆解为可执行任务,写入文档,然后进入 execute。\n\n### 批注消费规则\n\n- 读取并消费所有 `!!!` 批注,将批注意图合并进 `## 技术实现方案` 与 `## 任务清单`。\n- 处理完成后**删除对应 `!!!` 文本**,然后**整段删除** `## 用户批注` 章节。\n\n### 确认型答复的消费\n\n当批注含 `!!!选择:<决策>;备注:<理由>`(引用行的前缀 `待确认项`/`待确认问题` 皆可),按 `选择:` 分派下游行为,并**无论确认或否决都留痕**——在 `## 技术实现方案` 追加一行决策记录:`> 决策记录:<条目> —— 用户<决策>,理由:<理由>。`(决策记录不随 `## 用户批注` 删除而蒸发):\n\n| `选择:` | 下游行为 |\n| ---------------------------------- | ---------------------------------------------------------------------------------------------- |\n| 确认,按此推进 | 接受代价,落决策记录,继续拆任务 |\n| 否决·换方案 | 切回 plan:原目标下另寻实现(新方案可能又产生 choice/confirm) |\n| 否决·补约束 | 切回 plan:把理由当硬约束重算方案 |\n| 否决·弃目标·废弃当前目标,继续其余 | 从 `## 需求` 剔除该目标,落决策记录,继续其余目标 |\n| 否决·弃目标·放弃整个 spec | 落决策记录(作废),将 `stage` 置 `done` 终止,`last_action` 记「用户放弃整个 spec,标记终止」 |\n\n### 任务清单正确示例\n\n```\n## 6. 任务清单\n\n- [ ] 删除 src/foo.ts 中的 deprecatedFunc(验收:grep 无残留引用)\n- [ ] 更新 index.ts 导出(验收:tsc --noEmit 通过)\n```\n\n> 每项任务包含"动作 + 对象 + 验收点";仅使用单层 `- [ ]`;禁止泛化任务。\n\n### `[manual]` 人工确认任务标记\n\n对「需人工确认、Agent 不能自动完成」的事项,用 `- [ ] [manual] ...` 标注:\n\n```\n- [ ] [manual] 联系运维确认灰度窗口(验收:人工回复确认)\n```\n\n`done` 收尾判定**忽略**未勾选的 `[manual]` 项:只要其余非 manual 任务全部完成,即可收尾为 `done`,无需等待人工项被勾选(详见 execute 的「收尾为 done」)。\n\n### 自动衔接\n\n> 任务量不构成暂停理由:以下衔接规则与任务清单长度、改动半径无关。\n\n若待确认项为 `_暂无_` 且无新冲突,同一轮继续进入 execute,**同一轮衔接 execute 前后不得**向用户追加任何"是否执行/是否继续"询问。\n\n## execute\n\n顺序执行 `## 任务清单` 中未完成项(`- [ ]`)。执行中若发现新增/扩展需求或新增 bug,立即停止,切回 plan。\n\n> **执行过程中不得对用户做整批/中段元确认**:除"新需求/新 bug → 切回 plan"这一合法阻塞外,任务清单内部的任何疑问都必须以 `## 待确认项` 形式写回触发重开,禁止追加"是否继续 / 我是否应该执行下一项"等问句停顿。\n\n### 每完成一项后立刻\n\n- 将该项从 `- [ ]` 改为 `- [x]`。\n- 在 `## 执行记录` 增加一条结果(改动点、验证方式、阻塞项若有)。\n\n### 默认执行验证\n\n若仓库存在可用测试/构建命令,默认执行并记录结果;若当前环境无法执行,记录原因。\n\n### 追加任务 `[open] → [fixed]` 状态机\n\n若本轮起因是 `## 追加任务` 中的 `[open]` 条目:每完成一个追加项,把状态标记从 `[open]` 改为 `[fixed]`(位置和正文保留不变),在 `## 执行记录` 追加一条结果。已 `[fixed]` 的条目保留作为历史,不删除。\n\n### 收尾为 done\n\n当 `## 任务清单` 不存在未完成的**非 `[manual]`** 任务(`- [ ] [manual]` 人工确认项被忽略),且 `## 待确认项` 为 `_暂无_`、无任何 `!!!` 批注、无 `## 追加任务` 的 `[open]` 条目时,在 execute 收尾同一轮:\n\n- 将 `stage` 置为 `done`,`last_action` 记「任务全部完成,标记 done」。\n- 在 `## 执行记录` 追加一条收尾条目。\n\n`done` 是终止态:机器 / GUI 可据 `stage=done` 直接判定完成。此后不再自动推进;收到新增/扩展需求、新 bug,或 `## 追加任务` 出现 `[open]` 时,按「变更重开流程」切回 `plan`。\n\n## new-spec\n\n当 `spec_path` 缺省且无法从 session 上下文恢复时,自动生成路径与初始骨架。\n\n### 步骤\n\n1. **提炼摘要**:从用户输入提炼 kebab-case `summary-name`(≤ 40 字符,不以数字开头,必须语义化)。中文为主难以提炼时使用 `untitled-<NNN>` 占位。\n2. **判定 type**:`feat` / `refct` / `fix`;prompt 中已显式给出时直接使用;不确定时询问用户,兜底取 `feat`。\n3. **生成 id**:`YYMMDD.<type>.<summary-name>`(如 `260614.feat.spec-frontmatter`)。\n4. **生成路径**:`<specsDir>/<id>/spec.md`。id 冲突时追加 `-2`、`-3` 后缀。\n5. **初始化文档**:写入 frontmatter(`stage: plan`、`last_action: 新建 spec`、`updated_at` 秒级带单引号、`summary` ≤ 200 字符);建议创建全部章节(背景 / 需求 / 现状分析 / 技术实现方案 / 待确认项 / 任务清单 / 执行记录),将用户原始需求写入背景或需求。\n6. **进入 plan**:初始化后在同一轮继续推进 plan,不要因为"文档已创建"就退出。\n\n### CLI 触发模式\n\nCLI/Service 同时给出 `类型:<type>` 与需求但未提供 `spec_path` 时,必须自动进入新建流程,不询问类型/路径;初始化后同一轮继续推进 plan,直到遇到真正阻塞才退出。\n';
100
+ const __vite_glob_0_42 = '# 阶段流程(plan / tasks / execute / done / new-spec)\n\n## plan\n\n理解文档,分析现有代码,编写技术实现方案,更新到当前文档中;需要用户介入的事项以三级标题写入 `## 待确认项`,待用户后续审阅批注;暂时只需更新文档,不要实施代码。\n\n**目标:** 输出 `## 现状分析`、`## 技术实现方案`、`## 待确认项` 三节;禁止输出任务清单格式 `- [ ]`。\n\n若 `## 追加任务` 存在 `- [open] [feat|refct|fix] ...` 条目,必须将其纳入分析范围;plan 阶段不修改 `[open]` 状态标记。\n\n### 待确认项的三种类型\n\n一个待确认项,要么让用户**抉择**,要么让用户**确认**,要么请用户**自由补充**:\n\n| 类型 | 标记 | 语义 | 何时用 |\n| -------- | ------------------- | --------------------------------------------------------------------------------------- | --------------------- |\n| 抉择型 | `[choice]` | 给用户**选择权**:存在多个都可行、优劣取决于你拿不到的信息(业务优先级/外部约束)的方案 | 多方案差异大、难取舍 |\n| 确认型 | `[confirm]` | 给用户**否决权**:只有一个可行方案,但它有明显代价(技术债/影响面/不可逆) | 单方案但影响重大 |\n| 自由文本 | `(自由文本)` 后缀 | 请用户补一段开放说明 | 需要主观文案/开放意见 |\n\n> 无标记且带有序候选,向后兼容视为 `[choice]`。\n\n### 准入门槛(写入待确认项前必过的自检)\n\n**第一道过滤器先于类型判断**:这个答案,你能否通过读代码、跑测试、查项目约定自己确定?\n\n- **能** → 禁止写成待确认项,自己去查、去定,把决策+理由写进 `## 技术实现方案`(决策说明),不要打断用户。\n- **不能**(决策依赖用户独有信息,或选错的返工不可逆)→ 才写成待确认项,并归入 choice/confirm。\n\n> 待确认项是压力释放阀,不是免责声明。被你砍掉的判断不该凭空消失,要落成 `## 技术实现方案` 里的决策说明(决策 + 理由 + 被否决的备选)。\n\n### 抉择型 [choice] 正确示例\n\n```\n### 5.1 [choice] 候选答案的展现形式应采用哪种?\n1. 嵌套子列表\n2. 表格 (推荐)\n```\n\n- 候选写一级有序列表;恰 1 个候选以 ` (推荐)` / ` (推荐)` 结尾。\n- 空态与格式由 lint 的 `pending-questions/*` 规则保证。\n\n### 确认型 [confirm] 正确示例\n\n```\n### 5.2 [confirm] 将 UserID 从 int 迁移为 uuid\n\n**方案**:一次性迁移,双写过渡 2 周。\n**影响**:🔴 需停机窗口;对外 /api/user 字段类型变更,客户端需同步。\n```\n\n- 只有 `**方案**` 与 `**影响**`(或 `**代价**`)两字段,**不列候选**。\n- 影响行用红/黄语义标注严重度:🔴 breaking(破坏性/不可逆)、🟡 affected(有影响但可控)。\n- 确认型 = "我要这么干了,除非你喊停" 的知会 + 急停按钮:放行是常态,用户否决才是例外。\n\n### 确认型的否决语义(用户在 GUI 上如何回应)\n\n用户对确认型只有两种回应:**确认放行** 或 **否决**。否决必须携带理由,并细分意图(GUI 渲染为嵌套单选):\n\n| 用户决策 | 回写 `!!!选择:…` | 含义 |\n| ------------------------- | ------------------------------------ | ---------------------- |\n| 确认,按此推进 | `确认,按此推进` | 接受代价,继续 |\n| 否决·换方案 | `否决·换方案` | 原目标下另寻实现 |\n| 否决·补约束 | `否决·补约束` | 用户补充你漏掉的硬约束 |\n| 否决·弃目标·废弃当前目标 | `否决·弃目标·废弃当前目标,继续其余` | 该目标不做,其余照旧 |\n| 否决·弃目标·放弃整个 spec | `否决·弃目标·放弃整个 spec` | 整条 spec 作废 |\n\n> 决策不改动 API:编码进批注的 `选择:…`(结构化意图)+ `备注:…`(理由)。tasks 阶段按下表消费。\n\n### 图形化补充(plan 收尾子步骤)\n\n方案文字定稿后、结束 plan 前,**必须**专门回看 `## 现状分析`、`## 技术实现方案` 两节做一次 mermaid 图形化补充(仅此两节,其它章节不动):\n\n- 加载 [mermaid.md](./mermaid.md),先服从其**核心原则「图优先、精确信息折叠」**:表层给图(认知层),精确细节(路径、行号、类型/源码全文)折叠进 `<details>` 精确层,图与折叠互补不重复。\n- 按选型表与「必须用图」触发词补图:分步/分支散文→flowchart,接口/类型定义→classDiagram;含「兼容性/影响范围」时出结构图并按红=breaking、黄=affected 语义配色。\n- 这是一道独立聚焦的工序(不新增 stage):把「想清楚方案 / 写成文字」与「回看补图」分开,避免图形化被跳过。补图完成后再结束 plan。\n\n### 待确认项自检(plan 收尾子步骤)\n\n方案与图形化定稿后、结束 plan 前,**必须**专门回看 `## 待确认项` 做一次自检(与「图形化补充」同构:把"写方案"与"回看校验"分开,避免自检被跳过):\n\n1. **准入过滤**:逐条问"能否自己查证?"——能则删除该条、把决策落进 `## 技术实现方案`(决策说明)。\n2. **类型归类**:留下的每条明确标 `[choice]` 或 `[confirm]`(或 `(自由文本)`);单一类型,不兼具。\n3. **确认型补影响**:每个 `[confirm]` 必带 `**方案**` 与 `**影响**`(红/黄语义);影响写不出来,说明它其实够不上确认型,降级或删除。\n\n自检完成后再结束 plan。\n\n空态整章仅一行斜体占位:\n\n```\n## 5. 待确认项\n\n_暂无_\n```\n\n> 格式由 lint 的 `pending-questions/*` 规则保证;不确定时运行 `yorz lint` 查看报错。\n\n## tasks\n\n根据批注内容(`!!!` 前缀)更新技术方案;审查批注是否正确解答了待确认项;若引入新增/扩展需求或新增 bug,切回 plan;若存在冲突或歧义,回退 plan 并写入待确认项后退出;否则将技术方案拆解为可执行任务,写入文档,然后进入 execute。\n\n### 批注消费规则\n\n- 读取并消费所有 `!!!` 批注,将批注意图合并进 `## 技术实现方案` 与 `## 任务清单`。\n- 处理完成后**删除对应 `!!!` 文本**,然后**整段删除** `## 用户批注` 章节。\n\n### 确认型答复的消费\n\n当批注含 `!!!选择:<决策>;备注:<理由>`(引用行的前缀 `待确认项`/`待确认问题` 皆可),按 `选择:` 分派下游行为,并**无论确认或否决都留痕**——在 `## 技术实现方案` 追加一行决策记录:`> 决策记录:<条目> —— 用户<决策>,理由:<理由>。`(决策记录不随 `## 用户批注` 删除而蒸发):\n\n| `选择:` | 下游行为 |\n| ---------------------------------- | ---------------------------------------------------------------------------------------------- |\n| 确认,按此推进 | 接受代价,落决策记录,继续拆任务 |\n| 否决·换方案 | 切回 plan:原目标下另寻实现(新方案可能又产生 choice/confirm) |\n| 否决·补约束 | 切回 plan:把理由当硬约束重算方案 |\n| 否决·弃目标·废弃当前目标,继续其余 | 从 `## 需求` 剔除该目标,落决策记录,继续其余目标 |\n| 否决·弃目标·放弃整个 spec | 落决策记录(作废),将 `stage` 置 `done` 终止,`last_action` 记「用户放弃整个 spec,标记终止」 |\n\n### 任务清单正确示例\n\n```\n## 6. 任务清单\n\n- [ ] 删除 src/foo.ts 中的 deprecatedFunc(验收:grep 无残留引用)\n- [ ] 更新 index.ts 导出(验收:tsc --noEmit 通过)\n```\n\n> 每项任务包含"动作 + 对象 + 验收点";仅使用单层 `- [ ]`;禁止泛化任务。\n\n### `[manual]` 人工确认任务标记\n\n对「需人工确认、Agent 不能自动完成」的事项,用 `- [ ] [manual] ...` 标注:\n\n```\n- [ ] [manual] 联系运维确认灰度窗口(验收:人工回复确认)\n```\n\n`done` 收尾判定**忽略**未勾选的 `[manual]` 项:只要其余非 manual 任务全部完成,即可收尾为 `done`,无需等待人工项被勾选(详见 execute 的「收尾为 done」)。\n\n### 自动衔接\n\n> 任务量不构成暂停理由:以下衔接规则与任务清单长度、改动半径无关。\n\n若待确认项为 `_暂无_` 且无新冲突,同一轮继续进入 execute,**同一轮衔接 execute 前后不得**向用户追加任何"是否执行/是否继续"询问。\n\n## execute\n\n顺序执行 `## 任务清单` 中未完成项(`- [ ]`)。执行中若发现新增/扩展需求或新增 bug,立即停止,切回 plan。\n\n> **执行过程中不得对用户做整批/中段元确认**:除"新需求/新 bug → 切回 plan"这一合法阻塞外,任务清单内部的任何疑问都必须以 `## 待确认项` 形式写回触发重开,禁止追加"是否继续 / 我是否应该执行下一项"等问句停顿。\n\n### 每完成一项后立刻\n\n- 将该项从 `- [ ]` 改为 `- [x]`。\n- 在 `## 执行记录` 增加一条结果(改动点、验证方式、阻塞项若有)。\n\n### 默认执行验证\n\n若仓库存在可用测试/构建命令,默认执行并记录结果;若当前环境无法执行,记录原因。\n\n### 追加任务 `[open] → [fixed]` 状态机\n\n若本轮起因是 `## 追加任务` 中的 `[open]` 条目:每完成一个追加项,把状态标记从 `[open]` 改为 `[fixed]`(位置和正文保留不变),在 `## 执行记录` 追加一条结果。已 `[fixed]` 的条目保留作为历史,不删除。\n\n### 收尾为 done\n\n当 `## 任务清单` 不存在未完成的**非 `[manual]`** 任务(`- [ ] [manual]` 人工确认项被忽略),且 `## 待确认项` 为 `_暂无_`、无任何 `!!!` 批注、无 `## 追加任务` 的 `[open]` 条目时,在 execute 收尾同一轮:\n\n- 将 `stage` 置为 `done`,`last_action` 记「任务全部完成,标记 done」。\n- 在 `## 执行记录` 追加一条收尾条目。\n\n`done` 是终止态:机器 / GUI 可据 `stage=done` 直接判定完成。此后不再自动推进;收到新增/扩展需求、新 bug,或 `## 追加任务` 出现 `[open]` 时,按「变更重开流程」切回 `plan`。\n\n## new-spec\n\n当 `spec_path` 缺省且无法从 session 上下文恢复时,自动生成路径与初始骨架。\n\n### 步骤\n\n1. **提炼摘要**:从用户输入提炼 kebab-case `summary-name`(≤ 40 字符,不以数字开头,必须语义化)。中文为主难以提炼时使用 `untitled-<NNN>` 占位。\n2. **判定 type**:`feat` / `refct` / `fix`;prompt 中已显式给出时直接使用;不确定时询问用户,兜底取 `feat`。\n3. **生成 id**:`YYMMDD.<type>.<summary-name>`(如 `260614.feat.spec-frontmatter`)。\n4. **生成路径**:`<specsDir>/<id>/spec.md`。id 冲突时追加 `-2`、`-3` 后缀。\n5. **初始化文档**:写入 frontmatter(`stage: plan`、`last_action: 新建 spec`、`updated_at` 本地秒级字符串、`summary` ≤ 200 字符);建议创建全部章节(背景 / 需求 / 现状分析 / 技术实现方案 / 待确认项 / 任务清单 / 执行记录),将用户原始需求写入背景或需求。\n6. **进入 plan**:初始化后在同一轮继续推进 plan,不要因为"文档已创建"就退出。\n\n### CLI 触发模式\n\nCLI/Service 同时给出 `类型:<type>` 与需求但未提供 `spec_path` 时,必须自动进入新建流程,不询问类型/路径;初始化后同一轮继续推进 plan,直到遇到真正阻塞才退出。\n';
101
101
  async function isGitRepo(cwd) {
102
102
  try {
103
103
  await stat(join(cwd, ".git"));
@@ -1096,7 +1096,7 @@ function createSessionsRoutes(resolveProject) {
1096
1096
  });
1097
1097
  return app;
1098
1098
  }
1099
- const execFileP = promisify(execFile);
1099
+ const execFileP$1 = promisify(execFile);
1100
1100
  class GitError extends Error {
1101
1101
  code;
1102
1102
  stderr;
@@ -1151,7 +1151,7 @@ function assertSafeRelativePath(cwd, p) {
1151
1151
  }
1152
1152
  async function runGit(cwd, args) {
1153
1153
  try {
1154
- const result = await execFileP("git", args, {
1154
+ const result = await execFileP$1("git", args, {
1155
1155
  cwd,
1156
1156
  encoding: "utf8",
1157
1157
  maxBuffer: 16 * 1024 * 1024
@@ -1170,7 +1170,7 @@ async function runGitChecked(cwd, args) {
1170
1170
  }
1171
1171
  async function runGitRaw(cwd, args) {
1172
1172
  try {
1173
- const result = await execFileP("git", args, {
1173
+ const result = await execFileP$1("git", args, {
1174
1174
  cwd,
1175
1175
  encoding: "utf8",
1176
1176
  maxBuffer: 16 * 1024 * 1024
@@ -1463,6 +1463,365 @@ function buildGitOpsPrompt(action, specId, specRel, reviewRel) {
1463
1463
  `;
1464
1464
  }
1465
1465
  }
1466
+ const CURRENT_VERSION$1 = 1;
1467
+ const DEFAULT_GLOBAL_AGENT = { defaultKind: "claude" };
1468
+ const DEFAULT_NOTIFICATIONS = {
1469
+ sessionEnd: { banner: false, sound: false }
1470
+ };
1471
+ function resolveGlobalConfigDir(env = process.env) {
1472
+ if (env.YORZ_HOME && env.YORZ_HOME.trim()) return env.YORZ_HOME.trim();
1473
+ const xdg = env.XDG_CONFIG_HOME?.trim();
1474
+ const base = xdg && xdg.length > 0 ? xdg : join(homedir(), ".config");
1475
+ return join(base, "yorz");
1476
+ }
1477
+ function resolveGlobalConfigPath(env = process.env) {
1478
+ return join(resolveGlobalConfigDir(env), "projects.json");
1479
+ }
1480
+ async function loadGlobalConfig(filePath) {
1481
+ const fp = filePath ?? resolveGlobalConfigPath();
1482
+ if (!existsSync(fp)) return defaultGlobalConfig();
1483
+ let raw;
1484
+ try {
1485
+ raw = await readFile(fp, "utf8");
1486
+ } catch {
1487
+ return defaultGlobalConfig();
1488
+ }
1489
+ if (!raw.trim()) return defaultGlobalConfig();
1490
+ let parsed;
1491
+ try {
1492
+ parsed = JSON.parse(raw);
1493
+ } catch {
1494
+ return defaultGlobalConfig();
1495
+ }
1496
+ return normalizeConfig$1(parsed);
1497
+ }
1498
+ async function saveGlobalConfig(config, filePath) {
1499
+ const fp = filePath ?? resolveGlobalConfigPath();
1500
+ await mkdir(dirname(fp), { recursive: true });
1501
+ const normalized = normalizeConfig$1(config);
1502
+ const body = `${JSON.stringify(normalized, null, 2)}
1503
+ `;
1504
+ const tmp = `${fp}.tmp-${process.pid}-${Date.now().toString(36)}`;
1505
+ await writeFile(tmp, body, "utf8");
1506
+ await rename(tmp, fp);
1507
+ }
1508
+ function normalizeConfig$1(value) {
1509
+ if (!value || typeof value !== "object") return defaultGlobalConfig();
1510
+ const obj = value;
1511
+ const projectsRaw = Array.isArray(obj.projects) ? obj.projects : [];
1512
+ const projects = [];
1513
+ for (const item of projectsRaw) {
1514
+ if (!item || typeof item !== "object") continue;
1515
+ const it = item;
1516
+ const id = typeof it.id === "string" ? it.id : "";
1517
+ const path = typeof it.path === "string" ? it.path : "";
1518
+ if (!id || !path) continue;
1519
+ const addedAt = typeof it.addedAt === "string" ? it.addedAt : "";
1520
+ const lastActivityAt = typeof it.lastActivityAt === "string" ? it.lastActivityAt : null;
1521
+ const worktree = normalizeWorktree(it.worktree);
1522
+ const entry = { id, path, addedAt, lastActivityAt };
1523
+ if (worktree) entry.worktree = worktree;
1524
+ projects.push(entry);
1525
+ }
1526
+ return {
1527
+ version: CURRENT_VERSION$1,
1528
+ projects,
1529
+ agent: normalizeAgent$1(obj.agent),
1530
+ notifications: normalizeNotifications(obj.notifications)
1531
+ };
1532
+ }
1533
+ function defaultGlobalConfig() {
1534
+ return {
1535
+ version: CURRENT_VERSION$1,
1536
+ projects: [],
1537
+ agent: { defaultKind: DEFAULT_GLOBAL_AGENT.defaultKind },
1538
+ notifications: {
1539
+ sessionEnd: {
1540
+ banner: DEFAULT_NOTIFICATIONS.sessionEnd.banner,
1541
+ sound: DEFAULT_NOTIFICATIONS.sessionEnd.sound
1542
+ }
1543
+ }
1544
+ };
1545
+ }
1546
+ function normalizeAgent$1(value) {
1547
+ if (!value || typeof value !== "object") return { defaultKind: DEFAULT_GLOBAL_AGENT.defaultKind };
1548
+ const obj = value;
1549
+ const defaultKind = obj.defaultKind;
1550
+ if (defaultKind === "opencode" || defaultKind === "codex") return { defaultKind };
1551
+ return { defaultKind: "claude" };
1552
+ }
1553
+ function normalizeNotifications(value) {
1554
+ if (!value || typeof value !== "object") return defaultGlobalConfig().notifications;
1555
+ const obj = value;
1556
+ const sessionEndRaw = obj.sessionEnd;
1557
+ if (!sessionEndRaw || typeof sessionEndRaw !== "object")
1558
+ return defaultGlobalConfig().notifications;
1559
+ const sessionEndObj = sessionEndRaw;
1560
+ return {
1561
+ sessionEnd: {
1562
+ banner: typeof sessionEndObj.banner === "boolean" ? sessionEndObj.banner : DEFAULT_NOTIFICATIONS.sessionEnd.banner,
1563
+ sound: typeof sessionEndObj.sound === "boolean" ? sessionEndObj.sound : DEFAULT_NOTIFICATIONS.sessionEnd.sound
1564
+ }
1565
+ };
1566
+ }
1567
+ function normalizeWorktree(value) {
1568
+ if (!value || typeof value !== "object") return void 0;
1569
+ const it = value;
1570
+ const mainProjectId = typeof it.mainProjectId === "string" ? it.mainProjectId : "";
1571
+ const mainPath = typeof it.mainPath === "string" ? it.mainPath : "";
1572
+ const branch = typeof it.branch === "string" ? it.branch : "";
1573
+ const specId = typeof it.specId === "string" ? it.specId : "";
1574
+ const createdAt = typeof it.createdAt === "string" ? it.createdAt : "";
1575
+ if (!mainProjectId || !mainPath || !branch) return void 0;
1576
+ const cleanSlug = typeof it.cleanSlug === "string" ? it.cleanSlug : void 0;
1577
+ const meta = { mainProjectId, mainPath, branch, specId, createdAt };
1578
+ if (cleanSlug) meta.cleanSlug = cleanSlug;
1579
+ return meta;
1580
+ }
1581
+ function generateProjectId(absPath) {
1582
+ const base = basename$1(absPath);
1583
+ const slug = slugify(base) || "proj";
1584
+ const hash = createHash("sha256").update(absPath).digest("hex").slice(0, 6);
1585
+ return `${slug}-${hash}`;
1586
+ }
1587
+ function slugify(text) {
1588
+ return text.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40).replace(/-+$/, "");
1589
+ }
1590
+ async function addProject(absPath, filePath, now = () => /* @__PURE__ */ new Date()) {
1591
+ const config = await loadGlobalConfig(filePath);
1592
+ const existing = config.projects.find((p) => p.path === absPath);
1593
+ if (existing) return { entry: existing, created: false };
1594
+ const entry = {
1595
+ id: generateProjectId(absPath),
1596
+ path: absPath,
1597
+ addedAt: now().toISOString(),
1598
+ lastActivityAt: null
1599
+ };
1600
+ config.projects.push(entry);
1601
+ await saveGlobalConfig(config, filePath);
1602
+ return { entry, created: true };
1603
+ }
1604
+ async function setProjectWorktree(id, worktree, filePath) {
1605
+ const config = await loadGlobalConfig(filePath);
1606
+ const target = config.projects.find((p) => p.id === id);
1607
+ if (!target) return false;
1608
+ if (worktree) target.worktree = worktree;
1609
+ else delete target.worktree;
1610
+ await saveGlobalConfig(config, filePath);
1611
+ return true;
1612
+ }
1613
+ async function removeProject(id, filePath) {
1614
+ const config = await loadGlobalConfig(filePath);
1615
+ const before = config.projects.length;
1616
+ config.projects = config.projects.filter((p) => p.id !== id);
1617
+ if (config.projects.length === before) return false;
1618
+ await saveGlobalConfig(config, filePath);
1619
+ return true;
1620
+ }
1621
+ async function prepareProjectDir(input, cwd) {
1622
+ if (typeof input !== "string" || !input.trim()) {
1623
+ throw new Error("path required");
1624
+ }
1625
+ const trimmed = input.trim();
1626
+ const abs = isAbsolute(trimmed) ? resolve(trimmed) : resolve(cwd ?? process.cwd(), trimmed);
1627
+ if (!existsSync(abs)) {
1628
+ throw new Error(`path does not exist: ${abs}`);
1629
+ }
1630
+ const stats = await stat(abs);
1631
+ if (!stats.isDirectory()) {
1632
+ throw new Error(`path is not a directory: ${abs}`);
1633
+ }
1634
+ await mkdir(join(abs, ".yorz", "specs"), { recursive: true });
1635
+ return abs;
1636
+ }
1637
+ const LEVEL_WEIGHT = {
1638
+ debug: 10,
1639
+ info: 20,
1640
+ warn: 30,
1641
+ error: 40
1642
+ };
1643
+ const LOG_LEVELS = Object.keys(LEVEL_WEIGHT);
1644
+ const DEFAULT_MAX_BYTES = 5 * 1024 * 1024;
1645
+ const DEFAULT_MAX_ARCHIVES = 1;
1646
+ const DEFAULT_LOG_FILE = "serve.log";
1647
+ const STDIO_LOG_FILE = "serve-stdio.log";
1648
+ function resolveLogDir(env = process.env) {
1649
+ return join(resolveGlobalConfigDir(env), "logs");
1650
+ }
1651
+ function resolveLogLevel(env = process.env) {
1652
+ const raw = env.YORZ_LOG_LEVEL?.trim().toLowerCase();
1653
+ if (raw && LOG_LEVELS.includes(raw)) return raw;
1654
+ return "info";
1655
+ }
1656
+ function defaultOptions(env = process.env) {
1657
+ return {
1658
+ dir: resolveLogDir(env),
1659
+ fileName: DEFAULT_LOG_FILE,
1660
+ maxBytes: DEFAULT_MAX_BYTES,
1661
+ maxArchives: DEFAULT_MAX_ARCHIVES,
1662
+ level: resolveLogLevel(env),
1663
+ mirrorConsole: true
1664
+ };
1665
+ }
1666
+ class RotatingFileSink {
1667
+ constructor(dir, fileName, maxBytes, maxArchives) {
1668
+ this.dir = dir;
1669
+ this.fileName = fileName;
1670
+ this.maxBytes = maxBytes;
1671
+ this.maxArchives = maxArchives;
1672
+ }
1673
+ currentSize = 0;
1674
+ initialized = false;
1675
+ queue = Promise.resolve();
1676
+ get filePath() {
1677
+ return join(this.dir, this.fileName);
1678
+ }
1679
+ archivePath(index) {
1680
+ return `${this.filePath}.${index}`;
1681
+ }
1682
+ /** Enqueue a line; returns immediately, never throws. */
1683
+ write(line) {
1684
+ this.queue = this.queue.then(() => this.writeNow(line)).catch(() => {
1685
+ });
1686
+ }
1687
+ /** Resolve once every queued line has been flushed to disk. */
1688
+ async flush() {
1689
+ await this.queue.catch(() => {
1690
+ });
1691
+ }
1692
+ async writeNow(line) {
1693
+ try {
1694
+ await this.ensureInit();
1695
+ const bytes = Buffer.byteLength(line, "utf8");
1696
+ if (this.currentSize > 0 && this.currentSize + bytes > this.maxBytes) {
1697
+ await this.rotate();
1698
+ }
1699
+ await appendFile(this.filePath, line, "utf8");
1700
+ this.currentSize += bytes;
1701
+ } catch {
1702
+ }
1703
+ }
1704
+ /** Lazily create the dir and recover `currentSize` from an existing file. */
1705
+ async ensureInit() {
1706
+ if (this.initialized) return;
1707
+ await mkdir(this.dir, { recursive: true });
1708
+ try {
1709
+ const st = await stat(this.filePath);
1710
+ this.currentSize = st.size;
1711
+ } catch {
1712
+ this.currentSize = 0;
1713
+ }
1714
+ this.initialized = true;
1715
+ }
1716
+ async rotate() {
1717
+ if (this.maxArchives <= 0) {
1718
+ await writeFile(this.filePath, "", "utf8");
1719
+ this.currentSize = 0;
1720
+ return;
1721
+ }
1722
+ for (let i = this.maxArchives - 1; i >= 1; i--) {
1723
+ try {
1724
+ await rename(this.archivePath(i), this.archivePath(i + 1));
1725
+ } catch {
1726
+ }
1727
+ }
1728
+ await rename(this.filePath, this.archivePath(1));
1729
+ this.currentSize = 0;
1730
+ }
1731
+ }
1732
+ function createSink(options) {
1733
+ return new RotatingFileSink(options.dir, options.fileName, options.maxBytes, options.maxArchives);
1734
+ }
1735
+ function formatMeta(meta) {
1736
+ if (!meta) return "";
1737
+ const keys = Object.keys(meta);
1738
+ if (keys.length === 0) return "";
1739
+ try {
1740
+ return ` ${JSON.stringify(meta, jsonReplacer)}`;
1741
+ } catch {
1742
+ return "";
1743
+ }
1744
+ }
1745
+ function jsonReplacer(_key, value) {
1746
+ if (value instanceof Error) return { message: value.message, stack: value.stack };
1747
+ if (typeof value === "bigint") return value.toString();
1748
+ return value;
1749
+ }
1750
+ class Logger {
1751
+ constructor(core, scope) {
1752
+ this.core = core;
1753
+ this.scope = scope;
1754
+ }
1755
+ get level() {
1756
+ return this.core.options.level;
1757
+ }
1758
+ get filePath() {
1759
+ return this.core.sink.filePath;
1760
+ }
1761
+ get dir() {
1762
+ return this.core.options.dir;
1763
+ }
1764
+ /** Derive a logger that prefixes every line with `[scope]`. */
1765
+ child(scope) {
1766
+ const next = this.scope && this.scope !== "yorz" ? `${this.scope}:${scope}` : scope;
1767
+ return new Logger(this.core, next);
1768
+ }
1769
+ debug(msg, meta) {
1770
+ this.emit("debug", msg, meta);
1771
+ }
1772
+ info(msg, meta) {
1773
+ this.emit("info", msg, meta);
1774
+ }
1775
+ warn(msg, meta) {
1776
+ this.emit("warn", msg, meta);
1777
+ }
1778
+ error(msg, meta) {
1779
+ this.emit("error", msg, meta);
1780
+ }
1781
+ /** Replace options in place; every existing child logger picks the change up. */
1782
+ configure(patch) {
1783
+ const prev = this.core.options;
1784
+ const next = { ...prev, ...patch };
1785
+ this.core.options = next;
1786
+ const sinkChanged = next.dir !== prev.dir || next.fileName !== prev.fileName || next.maxBytes !== prev.maxBytes || next.maxArchives !== prev.maxArchives;
1787
+ if (sinkChanged) this.core.sink = createSink(next);
1788
+ }
1789
+ async flush() {
1790
+ await this.core.sink.flush();
1791
+ }
1792
+ emit(level, msg, meta) {
1793
+ const { options } = this.core;
1794
+ if (LEVEL_WEIGHT[level] < LEVEL_WEIGHT[options.level]) return;
1795
+ const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] [${level}] [${this.scope}] ${msg}${formatMeta(meta)}
1796
+ `;
1797
+ if (options.mirrorConsole) mirror(level, line);
1798
+ this.core.sink.write(line);
1799
+ }
1800
+ }
1801
+ function mirror(level, line) {
1802
+ const text = line.endsWith("\n") ? line.slice(0, -1) : line;
1803
+ try {
1804
+ if (level === "error") console.error(text);
1805
+ else if (level === "warn") console.warn(text);
1806
+ else console.log(text);
1807
+ } catch {
1808
+ }
1809
+ }
1810
+ function createLogger(patch = {}) {
1811
+ const options = { ...defaultOptions(), ...patch };
1812
+ return new Logger({ options, sink: createSink(options) }, "yorz");
1813
+ }
1814
+ let singleton = null;
1815
+ function getLogger() {
1816
+ if (!singleton) singleton = createLogger();
1817
+ return singleton;
1818
+ }
1819
+ function configureLogger(patch) {
1820
+ const logger = getLogger();
1821
+ logger.configure(patch);
1822
+ return logger;
1823
+ }
1824
+ const sseLog = () => getLogger().child("sse");
1466
1825
  const HEARTBEAT_INTERVAL_MS = 5e3;
1467
1826
  function attachHeartbeat(stream, intervalMs = HEARTBEAT_INTERVAL_MS) {
1468
1827
  const timer = setInterval(() => {
@@ -1626,7 +1985,7 @@ class EventsHub {
1626
1985
  s.queue.push(frame);
1627
1986
  }
1628
1987
  } catch (err) {
1629
- console.error("[yorz] SSE emit error:", err);
1988
+ sseLog().warn("emit failed", { topic, err });
1630
1989
  }
1631
1990
  }
1632
1991
  async flush(s) {
@@ -1812,10 +2171,10 @@ function createProjectRoutes(registry, worktreeManager) {
1812
2171
  });
1813
2172
  return app;
1814
2173
  }
1815
- const CURRENT_VERSION$1 = 1;
2174
+ const CURRENT_VERSION = 1;
1816
2175
  const DEFAULT_SPECS_DIR = ".yorz/specs";
1817
2176
  function defaultProjectConfig() {
1818
- return { version: CURRENT_VERSION$1, agent: { kind: "claude" }, specsDir: DEFAULT_SPECS_DIR };
2177
+ return { version: CURRENT_VERSION, agent: { kind: "inherit" }, specsDir: DEFAULT_SPECS_DIR };
1819
2178
  }
1820
2179
  function configPath(cwd) {
1821
2180
  return join(cwd, ".yorz", "config.json");
@@ -1836,12 +2195,12 @@ async function loadProjectConfig(cwd) {
1836
2195
  } catch {
1837
2196
  return defaultProjectConfig();
1838
2197
  }
1839
- return normalizeConfig$1(parsed);
2198
+ return normalizeConfig(parsed);
1840
2199
  }
1841
2200
  async function saveProjectConfig(cwd, cfg) {
1842
2201
  const fp = configPath(cwd);
1843
2202
  await mkdir(dirname(fp), { recursive: true });
1844
- const normalized = normalizeConfig$1(cfg);
2203
+ const normalized = normalizeConfig(cfg);
1845
2204
  const body = `${JSON.stringify(normalized, null, 2)}
1846
2205
  `;
1847
2206
  const tmp = `${fp}.tmp-${process.pid}-${Date.now().toString(36)}`;
@@ -1861,12 +2220,12 @@ function resolveSpecsDir(cwd, cfg) {
1861
2220
  async function ensureSpecsDirExists(absSpecsDir) {
1862
2221
  await mkdir(absSpecsDir, { recursive: true });
1863
2222
  }
1864
- function normalizeConfig$1(value) {
2223
+ function normalizeConfig(value) {
1865
2224
  if (!value || typeof value !== "object") return defaultProjectConfig();
1866
2225
  const obj = value;
1867
2226
  const agent = normalizeAgent(obj.agent);
1868
2227
  const specsDir = normalizeSpecsDir(obj.specsDir);
1869
- return { version: CURRENT_VERSION$1, agent, specsDir };
2228
+ return { version: CURRENT_VERSION, agent, specsDir };
1870
2229
  }
1871
2230
  function normalizeAgent(value) {
1872
2231
  if (typeof value === "string") {
@@ -1877,6 +2236,7 @@ function normalizeAgent(value) {
1877
2236
  if (!value || typeof value !== "object") return { kind: "claude" };
1878
2237
  const obj = value;
1879
2238
  const kind = obj.kind;
2239
+ if (kind === "inherit") return { kind: "inherit" };
1880
2240
  if (kind === "opencode") return { kind: "opencode" };
1881
2241
  if (kind === "codex") return { kind: "codex" };
1882
2242
  if (kind === "custom") {
@@ -1912,7 +2272,7 @@ function createProjectConfigRoutes(registry) {
1912
2272
  } catch {
1913
2273
  return c.json({ error: "invalid JSON body" }, 400);
1914
2274
  }
1915
- const parsed = parseBody(raw);
2275
+ const parsed = parseBody$1(raw);
1916
2276
  if ("error" in parsed) return c.json({ error: parsed.error }, 400);
1917
2277
  const cfg = {
1918
2278
  version: 1,
@@ -1940,7 +2300,7 @@ function createProjectConfigRoutes(registry) {
1940
2300
  });
1941
2301
  return app;
1942
2302
  }
1943
- function parseBody(value) {
2303
+ function parseBody$1(value) {
1944
2304
  if (!value || typeof value !== "object") return { error: "body must be an object" };
1945
2305
  const obj = value;
1946
2306
  const agent = parseAgent(obj.agent);
@@ -1955,6 +2315,7 @@ function parseAgent(value) {
1955
2315
  }
1956
2316
  const obj = value;
1957
2317
  const kind = obj.kind;
2318
+ if (kind === "inherit") return { value: { kind: "inherit" } };
1958
2319
  if (kind === "claude") return { value: { kind: "claude" } };
1959
2320
  if (kind === "opencode") return { value: { kind: "opencode" } };
1960
2321
  if (kind === "codex") return { value: { kind: "codex" } };
@@ -1978,7 +2339,7 @@ function parseAgent(value) {
1978
2339
  }
1979
2340
  return { value: { kind: "custom", cmd: cmd.trim(), args } };
1980
2341
  }
1981
- return { error: "agent.kind must be claude | opencode | codex | custom" };
2342
+ return { error: "agent.kind must be inherit | claude | opencode | codex | custom" };
1982
2343
  }
1983
2344
  function parseSpecsDir(value) {
1984
2345
  const fallback = defaultProjectConfig().specsDir;
@@ -1992,6 +2353,62 @@ function parseSpecsDir(value) {
1992
2353
  }
1993
2354
  return { value: trimmed };
1994
2355
  }
2356
+ function createGlobalConfigRoutes(globalConfigPath) {
2357
+ const app = new Hono();
2358
+ app.get("/global-config", async (c) => {
2359
+ const cfg = await loadGlobalConfig(globalConfigPath);
2360
+ return c.json({ agent: cfg.agent, notifications: cfg.notifications });
2361
+ });
2362
+ app.put("/global-config", async (c) => {
2363
+ let raw;
2364
+ try {
2365
+ raw = await c.req.json();
2366
+ } catch {
2367
+ return c.json({ error: "invalid JSON body" }, 400);
2368
+ }
2369
+ const parsed = parseBody(raw);
2370
+ if ("error" in parsed) return c.json({ error: parsed.error }, 400);
2371
+ const cfg = await loadGlobalConfig(globalConfigPath);
2372
+ cfg.agent = parsed.agent;
2373
+ cfg.notifications = parsed.notifications;
2374
+ await saveGlobalConfig(cfg, globalConfigPath);
2375
+ return c.json({ ok: true, config: { agent: cfg.agent, notifications: cfg.notifications } });
2376
+ });
2377
+ return app;
2378
+ }
2379
+ function parseBody(value) {
2380
+ if (!value || typeof value !== "object") return { error: "body must be an object" };
2381
+ const obj = value;
2382
+ const agent = obj.agent;
2383
+ if (!agent || typeof agent !== "object") {
2384
+ return { error: "agent must be an object" };
2385
+ }
2386
+ const agentObj = agent;
2387
+ const defaultKind = agentObj.defaultKind;
2388
+ if (defaultKind !== "claude" && defaultKind !== "opencode" && defaultKind !== "codex") {
2389
+ return { error: "agent.defaultKind must be claude | opencode | codex" };
2390
+ }
2391
+ const notifications = obj.notifications;
2392
+ if (!notifications || typeof notifications !== "object") {
2393
+ return { error: "notifications must be an object" };
2394
+ }
2395
+ const n = notifications;
2396
+ const sessionEnd = n.sessionEnd;
2397
+ if (!sessionEnd || typeof sessionEnd !== "object") {
2398
+ return { error: "notifications.sessionEnd must be an object" };
2399
+ }
2400
+ const s = sessionEnd;
2401
+ if (typeof s.banner !== "boolean") {
2402
+ return { error: "notifications.sessionEnd.banner must be a boolean" };
2403
+ }
2404
+ if (typeof s.sound !== "boolean") {
2405
+ return { error: "notifications.sessionEnd.sound must be a boolean" };
2406
+ }
2407
+ return {
2408
+ agent: { defaultKind },
2409
+ notifications: { sessionEnd: { banner: s.banner, sound: s.sound } }
2410
+ };
2411
+ }
1995
2412
  function createSpecDraftsRoutes(resolveProject) {
1996
2413
  const app = new Hono();
1997
2414
  const need = async (c) => {
@@ -2393,6 +2810,7 @@ async function serveFile(c, file) {
2393
2810
  return c.body(body);
2394
2811
  }
2395
2812
  const DEBOUNCE_MS = 200;
2813
+ const registryLog = () => getLogger().child("registry");
2396
2814
  class RegistryEventBus {
2397
2815
  subs = /* @__PURE__ */ new Set();
2398
2816
  watcher = null;
@@ -2425,7 +2843,7 @@ class RegistryEventBus {
2425
2843
  this.scheduleEmit();
2426
2844
  });
2427
2845
  } catch (err) {
2428
- console.warn(`[registry-events] watch failed: ${err.message}`);
2846
+ registryLog().warn("projects.json watch failed", { dir, message: err.message });
2429
2847
  }
2430
2848
  }
2431
2849
  stop() {
@@ -2445,134 +2863,7 @@ class RegistryEventBus {
2445
2863
  this.debounceTimer.unref?.();
2446
2864
  }
2447
2865
  }
2448
- const CURRENT_VERSION = 1;
2449
- function resolveGlobalConfigDir(env = process.env) {
2450
- if (env.YORZ_HOME && env.YORZ_HOME.trim()) return env.YORZ_HOME.trim();
2451
- const xdg = env.XDG_CONFIG_HOME?.trim();
2452
- const base = xdg && xdg.length > 0 ? xdg : join(homedir(), ".config");
2453
- return join(base, "yorz");
2454
- }
2455
- function resolveGlobalConfigPath(env = process.env) {
2456
- return join(resolveGlobalConfigDir(env), "projects.json");
2457
- }
2458
- async function loadGlobalConfig(filePath) {
2459
- const fp = filePath ?? resolveGlobalConfigPath();
2460
- if (!existsSync(fp)) return { version: CURRENT_VERSION, projects: [] };
2461
- let raw;
2462
- try {
2463
- raw = await readFile(fp, "utf8");
2464
- } catch {
2465
- return { version: CURRENT_VERSION, projects: [] };
2466
- }
2467
- if (!raw.trim()) return { version: CURRENT_VERSION, projects: [] };
2468
- let parsed;
2469
- try {
2470
- parsed = JSON.parse(raw);
2471
- } catch {
2472
- return { version: CURRENT_VERSION, projects: [] };
2473
- }
2474
- return normalizeConfig(parsed);
2475
- }
2476
- async function saveGlobalConfig(config, filePath) {
2477
- const fp = filePath ?? resolveGlobalConfigPath();
2478
- await mkdir(dirname(fp), { recursive: true });
2479
- const normalized = normalizeConfig(config);
2480
- const body = `${JSON.stringify(normalized, null, 2)}
2481
- `;
2482
- const tmp = `${fp}.tmp-${process.pid}-${Date.now().toString(36)}`;
2483
- await writeFile(tmp, body, "utf8");
2484
- await rename(tmp, fp);
2485
- }
2486
- function normalizeConfig(value) {
2487
- if (!value || typeof value !== "object") return { version: CURRENT_VERSION, projects: [] };
2488
- const obj = value;
2489
- const projectsRaw = Array.isArray(obj.projects) ? obj.projects : [];
2490
- const projects = [];
2491
- for (const item of projectsRaw) {
2492
- if (!item || typeof item !== "object") continue;
2493
- const it = item;
2494
- const id = typeof it.id === "string" ? it.id : "";
2495
- const path = typeof it.path === "string" ? it.path : "";
2496
- if (!id || !path) continue;
2497
- const addedAt = typeof it.addedAt === "string" ? it.addedAt : "";
2498
- const lastActivityAt = typeof it.lastActivityAt === "string" ? it.lastActivityAt : null;
2499
- const worktree = normalizeWorktree(it.worktree);
2500
- const entry = { id, path, addedAt, lastActivityAt };
2501
- if (worktree) entry.worktree = worktree;
2502
- projects.push(entry);
2503
- }
2504
- return { version: CURRENT_VERSION, projects };
2505
- }
2506
- function normalizeWorktree(value) {
2507
- if (!value || typeof value !== "object") return void 0;
2508
- const it = value;
2509
- const mainProjectId = typeof it.mainProjectId === "string" ? it.mainProjectId : "";
2510
- const mainPath = typeof it.mainPath === "string" ? it.mainPath : "";
2511
- const branch = typeof it.branch === "string" ? it.branch : "";
2512
- const specId = typeof it.specId === "string" ? it.specId : "";
2513
- const createdAt = typeof it.createdAt === "string" ? it.createdAt : "";
2514
- if (!mainProjectId || !mainPath || !branch) return void 0;
2515
- const cleanSlug = typeof it.cleanSlug === "string" ? it.cleanSlug : void 0;
2516
- const meta = { mainProjectId, mainPath, branch, specId, createdAt };
2517
- if (cleanSlug) meta.cleanSlug = cleanSlug;
2518
- return meta;
2519
- }
2520
- function generateProjectId(absPath) {
2521
- const base = basename$1(absPath);
2522
- const slug = slugify(base) || "proj";
2523
- const hash = createHash("sha256").update(absPath).digest("hex").slice(0, 6);
2524
- return `${slug}-${hash}`;
2525
- }
2526
- function slugify(text) {
2527
- return text.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40).replace(/-+$/, "");
2528
- }
2529
- async function addProject(absPath, filePath, now = () => /* @__PURE__ */ new Date()) {
2530
- const config = await loadGlobalConfig(filePath);
2531
- const existing = config.projects.find((p) => p.path === absPath);
2532
- if (existing) return { entry: existing, created: false };
2533
- const entry = {
2534
- id: generateProjectId(absPath),
2535
- path: absPath,
2536
- addedAt: now().toISOString(),
2537
- lastActivityAt: null
2538
- };
2539
- config.projects.push(entry);
2540
- await saveGlobalConfig(config, filePath);
2541
- return { entry, created: true };
2542
- }
2543
- async function setProjectWorktree(id, worktree, filePath) {
2544
- const config = await loadGlobalConfig(filePath);
2545
- const target = config.projects.find((p) => p.id === id);
2546
- if (!target) return false;
2547
- if (worktree) target.worktree = worktree;
2548
- else delete target.worktree;
2549
- await saveGlobalConfig(config, filePath);
2550
- return true;
2551
- }
2552
- async function removeProject(id, filePath) {
2553
- const config = await loadGlobalConfig(filePath);
2554
- const before = config.projects.length;
2555
- config.projects = config.projects.filter((p) => p.id !== id);
2556
- if (config.projects.length === before) return false;
2557
- await saveGlobalConfig(config, filePath);
2558
- return true;
2559
- }
2560
- async function prepareProjectDir(input, cwd) {
2561
- if (typeof input !== "string" || !input.trim()) {
2562
- throw new Error("path required");
2563
- }
2564
- const trimmed = input.trim();
2565
- const abs = isAbsolute(trimmed) ? resolve(trimmed) : resolve(cwd ?? process.cwd(), trimmed);
2566
- if (!existsSync(abs)) {
2567
- throw new Error(`path does not exist: ${abs}`);
2568
- }
2569
- const stats = await stat(abs);
2570
- if (!stats.isDirectory()) {
2571
- throw new Error(`path is not a directory: ${abs}`);
2572
- }
2573
- await mkdir(join(abs, ".yorz", "specs"), { recursive: true });
2574
- return abs;
2575
- }
2866
+ const worktreeLog = () => getLogger().child("worktree");
2576
2867
  class WorktreeManager {
2577
2868
  registry;
2578
2869
  globalConfigPath;
@@ -2620,6 +2911,13 @@ class WorktreeManager {
2620
2911
  };
2621
2912
  await setProjectWorktree(entry.id, meta, this.globalConfigPath);
2622
2913
  const updated = await this.registry.findEntry(entry.id) ?? { ...entry, worktree: meta };
2914
+ worktreeLog().info("worktree created", {
2915
+ mainProjectId: main.id,
2916
+ worktreeProjectId: entry.id,
2917
+ branch,
2918
+ path: wtPath,
2919
+ baseRef
2920
+ });
2623
2921
  return { entry: updated, branch, path: wtPath, baseRef };
2624
2922
  }
2625
2923
  /** Update the `specId` on a worktree entry once the spec has been created. */
@@ -2660,6 +2958,12 @@ class WorktreeManager {
2660
2958
  globalConfigPath: this.globalConfigPath ?? resolveGlobalConfigPath(),
2661
2959
  now: input.now ?? (() => /* @__PURE__ */ new Date())
2662
2960
  });
2961
+ worktreeLog().warn("merge back hit conflicts", {
2962
+ worktreeProjectId: entry.id,
2963
+ branch,
2964
+ mainPath,
2965
+ conflictSpecId: "conflictSpecId" in conflict ? conflict.conflictSpecId : void 0
2966
+ });
2663
2967
  return conflict;
2664
2968
  }
2665
2969
  const head = await runGitChecked(mainPath, ["rev-parse", "HEAD"]);
@@ -2674,6 +2978,12 @@ class WorktreeManager {
2674
2978
  await this.registry.reload(mainEntry.id);
2675
2979
  }
2676
2980
  this.onProjectsChanged?.();
2981
+ worktreeLog().info("worktree merged back", {
2982
+ worktreeProjectId: entry.id,
2983
+ mainProjectId: mainEntry?.id ?? entry.worktree.mainProjectId,
2984
+ branch,
2985
+ mergeCommit
2986
+ });
2677
2987
  return {
2678
2988
  status: "merged",
2679
2989
  mainProjectId: mainEntry?.id ?? entry.worktree.mainProjectId,
@@ -2701,6 +3011,7 @@ class WorktreeManager {
2701
3011
  }
2702
3012
  await this.registry.remove(worktreeProjectId);
2703
3013
  this.onProjectsChanged?.();
3014
+ worktreeLog().info("worktree removed", { worktreeProjectId, branch, path: wtPath });
2704
3015
  }
2705
3016
  async listWorktreesOf(mainProjectId) {
2706
3017
  const config = await loadGlobalConfig(this.globalConfigPath);
@@ -2711,7 +3022,7 @@ class WorktreeManager {
2711
3022
  const files = unmerged.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
2712
3023
  const commitsByFile = {};
2713
3024
  for (const file of files) {
2714
- const log = await runGitRaw(mainPath, [
3025
+ const log2 = await runGitRaw(mainPath, [
2715
3026
  "log",
2716
3027
  "--since=30 days ago",
2717
3028
  "--pretty=format:%h%x09%ad%x09%an%x09%s",
@@ -2719,7 +3030,7 @@ class WorktreeManager {
2719
3030
  "--",
2720
3031
  file
2721
3032
  ]);
2722
- commitsByFile[file] = parseCommitLines(log.stdout);
3033
+ commitsByFile[file] = parseCommitLines(log2.stdout);
2723
3034
  }
2724
3035
  const merges = await runGitRaw(mainPath, [
2725
3036
  "log",
@@ -2767,9 +3078,10 @@ class WorktreeManager {
2767
3078
  if (this.triggerConflictAgent) {
2768
3079
  await this.triggerConflictAgent(args.mainProjectId, specId);
2769
3080
  } else {
2770
- console.warn(
2771
- `[worktree] triggerConflictAgent not configured; conflict spec ${specId} created at ${specPath} but no Agent was launched`
2772
- );
3081
+ worktreeLog().warn("triggerConflictAgent not configured; no Agent launched", {
3082
+ specId,
3083
+ specPath
3084
+ });
2773
3085
  }
2774
3086
  return {
2775
3087
  status: "conflict",
@@ -2916,8 +3228,25 @@ _暂无_
2916
3228
  function pad2(n) {
2917
3229
  return n < 10 ? `0${n}` : String(n);
2918
3230
  }
3231
+ const SLOW_REQUEST_MS = 1e3;
2919
3232
  function createApp(opts) {
2920
3233
  const app = new Hono();
3234
+ const httpLog = getLogger().child("http");
3235
+ const worktreeLog2 = getLogger().child("worktree");
3236
+ app.use("*", async (c, next) => {
3237
+ const startedAt = Date.now();
3238
+ await next();
3239
+ const durationMs = Date.now() - startedAt;
3240
+ const meta = {
3241
+ method: c.req.method,
3242
+ path: c.req.path,
3243
+ status: c.res.status,
3244
+ durationMs
3245
+ };
3246
+ if (c.res.status >= 400) httpLog.warn("request failed", meta);
3247
+ else if (durationMs >= SLOW_REQUEST_MS) httpLog.warn("slow request", meta);
3248
+ else httpLog.debug("request", meta);
3249
+ });
2921
3250
  const api = new Hono();
2922
3251
  const resolveProject = (id) => opts.registry.getOrCreate(id);
2923
3252
  const projectsBus = new RegistryEventBus();
@@ -2928,9 +3257,10 @@ function createApp(opts) {
2928
3257
  triggerConflictAgent: async (mainProjectId, specId) => {
2929
3258
  const main = await opts.registry.getOrCreate(mainProjectId);
2930
3259
  if (!main) {
2931
- console.warn(
2932
- `[worktree] cannot launch conflict Agent: main project ${mainProjectId} not resolvable`
2933
- );
3260
+ worktreeLog2.warn("cannot launch conflict Agent: main project not resolvable", {
3261
+ mainProjectId,
3262
+ specId
3263
+ });
2934
3264
  return;
2935
3265
  }
2936
3266
  const { sessionId } = await main.sessions.ensureSessionForSpec(specId);
@@ -2941,6 +3271,7 @@ function createApp(opts) {
2941
3271
  }
2942
3272
  });
2943
3273
  api.route("/", createProjectRoutes(opts.registry, worktreeManager));
3274
+ api.route("/", createGlobalConfigRoutes(opts.registry.configPath()));
2944
3275
  api.route("/", createProjectConfigRoutes(opts.registry));
2945
3276
  api.route("/", createSpecsRoutes(resolveProject));
2946
3277
  api.route("/", createSessionsRoutes(resolveProject));
@@ -2952,7 +3283,13 @@ function createApp(opts) {
2952
3283
  app.route("/api", api);
2953
3284
  app.route("/", createStaticRoutes(opts.guiRoot));
2954
3285
  app.onError((err, c) => {
2955
- console.error("[yorz] route error:", err);
3286
+ httpLog.error("route error", {
3287
+ method: c.req.method,
3288
+ path: c.req.path,
3289
+ status: 500,
3290
+ message: err.message,
3291
+ stack: err.stack
3292
+ });
2956
3293
  return c.json({ error: "Internal Server Error", message: err.message }, 500);
2957
3294
  });
2958
3295
  return app;
@@ -3404,6 +3741,7 @@ function placeholderSummary(requirement) {
3404
3741
  if (!firstParagraph) return "(待 Agent 补全)";
3405
3742
  return firstParagraph.length > 200 ? `${firstParagraph.slice(0, 199)}…` : firstParagraph;
3406
3743
  }
3744
+ const watcherLog = () => getLogger().child("watcher");
3407
3745
  const UNLINK_SETTLE_MS = 80;
3408
3746
  class SpecWatcher {
3409
3747
  root;
@@ -3427,13 +3765,15 @@ class SpecWatcher {
3427
3765
  depth: 2,
3428
3766
  ...usePolling ? { usePolling: true, interval: 100 } : {}
3429
3767
  });
3430
- this.watcher.on("add", (p) => this.handle(p, "updated"));
3431
- this.watcher.on("change", (p) => this.handle(p, "updated"));
3432
- this.watcher.on("unlink", (p) => this.handle(p, "removed"));
3768
+ this.watcher.on("add", (p) => this.handle(p, "updated", "add"));
3769
+ this.watcher.on("change", (p) => this.handle(p, "updated", "change"));
3770
+ this.watcher.on("unlink", (p) => this.handle(p, "removed", "unlink"));
3771
+ this.watcher.on("error", (err) => watcherLog().warn("watch error", { root: this.root, err }));
3433
3772
  await new Promise((resolve2, reject) => {
3434
3773
  this.watcher.once("ready", () => resolve2());
3435
3774
  this.watcher.once("error", (err) => reject(err));
3436
3775
  });
3776
+ watcherLog().debug("watching specs dir", { root: this.root, usePolling });
3437
3777
  }
3438
3778
  async close() {
3439
3779
  for (const timer of this.pendingUnlinks.values()) clearTimeout(timer);
@@ -3462,10 +3802,11 @@ class SpecWatcher {
3462
3802
  this.listListeners.add(cb);
3463
3803
  return () => this.listListeners.delete(cb);
3464
3804
  }
3465
- async handle(filePath, evt) {
3805
+ async handle(filePath, evt, raw) {
3466
3806
  if (basename$1(filePath) !== "spec.md") return;
3467
3807
  const id = this.idForPath(filePath);
3468
3808
  if (!id) return;
3809
+ watcherLog().debug("spec file event", { event: raw, specId: id, path: filePath });
3469
3810
  if (evt === "removed") {
3470
3811
  this.scheduleUnlink(id, filePath);
3471
3812
  return;
@@ -4072,6 +4413,7 @@ class AdapterRegistry {
4072
4413
  this.byKind.clear();
4073
4414
  }
4074
4415
  }
4416
+ const agentLog = () => getLogger().child("agent");
4075
4417
  const SESSION_LIST_LIMIT = 30;
4076
4418
  const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
4077
4419
  const TITLE_MAX_LENGTH = 64;
@@ -4086,10 +4428,11 @@ function summarizePromptForTitle(prompt) {
4086
4428
  return cleaned.length > TITLE_MAX_LENGTH ? `${cleaned.slice(0, TITLE_MAX_LENGTH - 1)}…` : cleaned;
4087
4429
  }
4088
4430
  class SessionManager {
4089
- constructor(cwd, defaultKind, store) {
4431
+ constructor(cwd, defaultKind, store, opts = {}) {
4090
4432
  this.cwd = cwd;
4091
4433
  this.defaultKind = defaultKind;
4092
4434
  this.store = store;
4435
+ this.opts = opts;
4093
4436
  this.adapters = new AdapterRegistry(cwd);
4094
4437
  this.statusEmitter.setMaxListeners(0);
4095
4438
  }
@@ -4215,9 +4558,11 @@ class SessionManager {
4215
4558
  const runId = randomUUID();
4216
4559
  const emitter = this.emitterFor(sid);
4217
4560
  let currentSid = sid;
4561
+ const startedAt = Date.now();
4218
4562
  await this.maybeUpdateTitleFromPrompt(currentSid, prompt).catch(() => {
4219
4563
  });
4220
4564
  this.setRunning(sid, true);
4565
+ agentLog().info("dispatch start", { sessionId: sid, runId, promptLength: prompt.length });
4221
4566
  void (async () => {
4222
4567
  try {
4223
4568
  const ls = await this.ensureLive(sid);
@@ -4230,14 +4575,28 @@ class SessionManager {
4230
4575
  emitter.emit("event", ev);
4231
4576
  }
4232
4577
  } catch (err) {
4578
+ agentLog().error("dispatch failed", {
4579
+ sessionId: currentSid,
4580
+ runId,
4581
+ durationMs: Date.now() - startedAt,
4582
+ message: err instanceof Error ? err.message : String(err),
4583
+ stack: err instanceof Error ? err.stack : void 0
4584
+ });
4233
4585
  emitter.emit("event", {
4234
4586
  type: "error",
4235
4587
  message: err instanceof Error ? err.message : String(err)
4236
4588
  });
4237
4589
  } finally {
4590
+ agentLog().info("dispatch end", {
4591
+ sessionId: currentSid,
4592
+ runId,
4593
+ durationMs: Date.now() - startedAt
4594
+ });
4238
4595
  await this.store.touch(currentSid).catch(() => {
4239
4596
  });
4240
4597
  this.setRunning(currentSid, false);
4598
+ void Promise.resolve(this.opts.onSessionEnd?.(currentSid)).catch(() => {
4599
+ });
4241
4600
  emitter.emit("done");
4242
4601
  }
4243
4602
  })();
@@ -4261,6 +4620,7 @@ class SessionManager {
4261
4620
  return () => emitter.off("event", onEvent);
4262
4621
  }
4263
4622
  async reconcile(oldId, newId) {
4623
+ agentLog().debug("reconcile sessionId", { from: oldId, to: newId });
4264
4624
  const ls = this.live.get(oldId);
4265
4625
  if (ls) {
4266
4626
  this.live.delete(oldId);
@@ -4365,30 +4725,89 @@ class SessionStore {
4365
4725
  await this.persist();
4366
4726
  }
4367
4727
  }
4368
- function resolveAgentKind(cwd) {
4369
- const path = join(cwd, ".yorz", "config.json");
4370
- if (!existsSync(path)) return "claude";
4371
- let raw;
4372
- try {
4373
- raw = readFileSync(path, "utf8");
4374
- } catch {
4375
- return "claude";
4728
+ const execFileP = promisify(execFile);
4729
+ const COMMAND_TIMEOUT_MS = 4e3;
4730
+ function createSessionEndNotifier(opts = {}) {
4731
+ return async function notifySessionEnded() {
4732
+ const cfg = await loadGlobalConfig(opts.globalConfigPath);
4733
+ const sessionEnd = cfg.notifications.sessionEnd;
4734
+ if (!sessionEnd.banner && !sessionEnd.sound) return;
4735
+ const os = opts.platform ?? platform();
4736
+ const run = opts.runCommand ?? defaultRunCommand;
4737
+ const title = formatBannerTitle(opts.projectName);
4738
+ const tasks = [];
4739
+ if (sessionEnd.banner) tasks.push(showBanner(os, run, title));
4740
+ if (sessionEnd.sound) tasks.push(playSound(os, run));
4741
+ await Promise.all(tasks.map((task) => task.catch(() => {
4742
+ })));
4743
+ };
4744
+ }
4745
+ async function defaultRunCommand(cmd, args) {
4746
+ await execFileP(cmd, args, {
4747
+ timeout: COMMAND_TIMEOUT_MS,
4748
+ windowsHide: true
4749
+ });
4750
+ }
4751
+ function formatBannerTitle(projectName) {
4752
+ const name = projectName?.trim();
4753
+ return name ? `YorZ · ${name}` : "YorZ";
4754
+ }
4755
+ function appleScriptString(value) {
4756
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
4757
+ }
4758
+ function powershellSingleQuoted(value) {
4759
+ return `'${value.replace(/'/g, "''")}'`;
4760
+ }
4761
+ async function showBanner(os, run, title) {
4762
+ if (os === "darwin") {
4763
+ await run("osascript", ["-e", `display notification "" with title ${appleScriptString(title)}`]);
4764
+ return;
4376
4765
  }
4377
- if (!raw.trim()) return "claude";
4378
- let data;
4379
- try {
4380
- data = JSON.parse(raw);
4381
- } catch {
4382
- return "claude";
4766
+ if (os === "win32") {
4767
+ await run("powershell.exe", [
4768
+ "-NoProfile",
4769
+ "-NonInteractive",
4770
+ "-Command",
4771
+ [
4772
+ "Add-Type -AssemblyName System.Windows.Forms",
4773
+ "Add-Type -AssemblyName System.Drawing",
4774
+ "$n = New-Object System.Windows.Forms.NotifyIcon",
4775
+ "$n.Icon = [System.Drawing.SystemIcons]::Information",
4776
+ "$n.Visible = $true",
4777
+ `$n.ShowBalloonTip(5000, ${powershellSingleQuoted(title)}, '', [System.Windows.Forms.ToolTipIcon]::Info)`,
4778
+ "Start-Sleep -Milliseconds 5500",
4779
+ "$n.Dispose()"
4780
+ ].join("; ")
4781
+ ]);
4782
+ return;
4383
4783
  }
4384
- if (!data || typeof data !== "object") return "claude";
4385
- const agent = data.agent;
4386
- if (typeof agent === "string") {
4387
- return agent === "codex" || agent === "opencode" ? agent : "claude";
4784
+ await run("notify-send", [title, ""]);
4785
+ }
4786
+ async function playSound(os, run) {
4787
+ if (os === "darwin") {
4788
+ await run("afplay", ["/System/Library/Sounds/Submarine.aiff"]);
4789
+ return;
4790
+ }
4791
+ if (os === "win32") {
4792
+ await run("powershell.exe", [
4793
+ "-NoProfile",
4794
+ "-NonInteractive",
4795
+ "-Command",
4796
+ "[console]::beep(440, 180)"
4797
+ ]);
4798
+ return;
4799
+ }
4800
+ for (const [cmd, args] of [
4801
+ ["paplay", ["/usr/share/sounds/freedesktop/stereo/complete.oga"]],
4802
+ ["canberra-gtk-play", ["-i", "complete"]],
4803
+ ["aplay", ["/usr/share/sounds/alsa/Front_Center.wav"]]
4804
+ ]) {
4805
+ try {
4806
+ await run(cmd, args);
4807
+ return;
4808
+ } catch {
4809
+ }
4388
4810
  }
4389
- if (!agent || typeof agent !== "object") return "claude";
4390
- const kind = agent.kind;
4391
- return kind === "codex" || kind === "opencode" ? kind : "claude";
4392
4811
  }
4393
4812
  class ProjectRegistry {
4394
4813
  globalConfigPath;
@@ -4488,6 +4907,7 @@ class ProjectRegistry {
4488
4907
  }
4489
4908
  async materialize(input) {
4490
4909
  const cfg = await loadProjectConfig(input.path);
4910
+ const globalCfg = await loadGlobalConfig(this.globalConfigPath);
4491
4911
  const specsDir = resolveSpecsDir(input.path, cfg);
4492
4912
  const specsDirRelative = posixRelative(input.path, specsDir);
4493
4913
  await ensureSpecsDirExists(specsDir);
@@ -4499,7 +4919,14 @@ class ProjectRegistry {
4499
4919
  });
4500
4920
  const attachments = new AttachmentStore({ cwd: input.path });
4501
4921
  const sessionStore = new SessionStore(input.path);
4502
- const sessions = new SessionManager(input.path, resolveAgentKind(input.path), sessionStore);
4922
+ const notifySessionEnded = createSessionEndNotifier({
4923
+ globalConfigPath: this.globalConfigPath,
4924
+ projectName: basename(input.path)
4925
+ });
4926
+ const defaultKind = resolveProjectAgentKind(cfg.agent, globalCfg.agent.defaultKind);
4927
+ const sessions = new SessionManager(input.path, defaultKind, sessionStore, {
4928
+ onSessionEnd: notifySessionEnded
4929
+ });
4503
4930
  let closed = false;
4504
4931
  const instance = {
4505
4932
  id: input.id,
@@ -4530,6 +4957,11 @@ class ProjectRegistry {
4530
4957
  return instance;
4531
4958
  }
4532
4959
  }
4960
+ function resolveProjectAgentKind(projectAgent, globalDefault) {
4961
+ if (projectAgent.kind === "inherit") return globalDefault;
4962
+ if (projectAgent.kind === "codex" || projectAgent.kind === "opencode") return projectAgent.kind;
4963
+ return "claude";
4964
+ }
4533
4965
  function basename(p) {
4534
4966
  const parts = p.split(/[/\\]/).filter(Boolean);
4535
4967
  return parts[parts.length - 1] ?? p;
@@ -4556,6 +4988,7 @@ async function maxSpecUpdatedAt(projectPath) {
4556
4988
  }
4557
4989
  const DEFAULT_PORT = 7423;
4558
4990
  const MAX_PORT_TRIES = 10;
4991
+ const log$1 = () => getLogger().child("serve");
4559
4992
  async function start(opts = {}) {
4560
4993
  const registry = new ProjectRegistry({ globalConfigPath: opts.globalConfigPath });
4561
4994
  const cwd = opts.cwd ?? process.cwd();
@@ -4577,6 +5010,15 @@ async function start(opts = {}) {
4577
5010
  console.log(` - ${p.name} -> ${p.path}`);
4578
5011
  }
4579
5012
  console.log(`agent heartbeat enabled (interval=${HEARTBEAT_INTERVAL_MS / 1e3}s)`);
5013
+ log$1().info("service ready", {
5014
+ pid: process.pid,
5015
+ port: port.port,
5016
+ url,
5017
+ projects: projects.length,
5018
+ node: process.version,
5019
+ cli: pkg.version,
5020
+ logFile: getLogger().filePath
5021
+ });
4580
5022
  installGlobalErrorHandlers();
4581
5023
  if (opts.open) await tryOpenBrowser(url);
4582
5024
  return {
@@ -4618,10 +5060,18 @@ async function listen(fetchHandler, preferredPort) {
4618
5060
  );
4619
5061
  } catch (err) {
4620
5062
  lastErr = err;
4621
- if (err.code !== "EADDRINUSE") break;
5063
+ const code = err.code;
5064
+ if (code === "EADDRINUSE") {
5065
+ log$1().warn("port in use, retrying", { port: tryPort, attempt: i + 1, max: MAX_PORT_TRIES });
5066
+ } else {
5067
+ log$1().warn("listen failed", { port: tryPort, code, err: lastErr });
5068
+ break;
5069
+ }
4622
5070
  }
4623
5071
  }
4624
- throw lastErr ?? new Error(`failed to bind port near ${preferredPort}`);
5072
+ const fatal = lastErr ?? new Error(`failed to bind port near ${preferredPort}`);
5073
+ log$1().error("failed to bind any port", { preferredPort, tries: MAX_PORT_TRIES, err: fatal });
5074
+ throw fatal;
4625
5075
  }
4626
5076
  function listLanUrls(port) {
4627
5077
  const urls = [];
@@ -4643,23 +5093,9 @@ async function tryOpenBrowser(url) {
4643
5093
  } catch {
4644
5094
  }
4645
5095
  }
4646
- let errorLogReady = false;
4647
- async function ensureErrorLogDir() {
4648
- const dir = join(resolveGlobalConfigDir(), "logs");
4649
- if (!errorLogReady) {
4650
- await mkdir(dir, { recursive: true });
4651
- errorLogReady = true;
4652
- }
4653
- return join(dir, "serve-errors.log");
4654
- }
4655
5096
  function logCrash(kind, payload) {
4656
- const ts = (/* @__PURE__ */ new Date()).toISOString();
4657
5097
  const body = payload instanceof Error ? payload.stack ?? payload.message : String(payload);
4658
- const line = `[${ts}] [${kind}] ${body}
4659
- `;
4660
- console.error(`[yorz] ${kind}:`, payload);
4661
- void ensureErrorLogDir().then((fp) => appendFile(fp, line, "utf8")).catch(() => {
4662
- });
5098
+ log$1().error(kind, { detail: body });
4663
5099
  }
4664
5100
  function installGlobalErrorHandlers() {
4665
5101
  if (process.listenerCount("uncaughtException") > 0) return;
@@ -4670,10 +5106,25 @@ function installGlobalErrorHandlers() {
4670
5106
  logCrash("unhandledRejection", reason);
4671
5107
  });
4672
5108
  }
5109
+ const log = () => getLogger().child("serve");
5110
+ function silenceConsoleMirror() {
5111
+ configureLogger({ mirrorConsole: false });
5112
+ }
4673
5113
  const DEFAULT_SERVE_PORT = 7423;
4674
5114
  const RUNTIME_FILE = "runtime.json";
4675
5115
  const START_LOCK_DIR = "serve.lock";
4676
5116
  const START_WAIT_MS = 5e3;
5117
+ function backgroundStdio() {
5118
+ try {
5119
+ const dir = resolveLogDir();
5120
+ mkdirSync(dir, { recursive: true });
5121
+ const filePath = join(dir, STDIO_LOG_FILE);
5122
+ const fd = openSync(filePath, "w");
5123
+ return { stdio: ["ignore", fd, fd], path: filePath };
5124
+ } catch {
5125
+ return { stdio: "ignore", path: null };
5126
+ }
5127
+ }
4677
5128
  async function runServe(opts) {
4678
5129
  if (!opts.skipSkillCheck) {
4679
5130
  await ensureSkillsInstalledWithLog(opts.cwd ?? process.cwd());
@@ -4708,6 +5159,7 @@ async function runServe(opts) {
4708
5159
  return handle;
4709
5160
  }
4710
5161
  function startBackgroundServe(opts) {
5162
+ silenceConsoleMirror();
4711
5163
  return withStartLock(async () => {
4712
5164
  const live = await readLiveProcesses();
4713
5165
  if (live.length > 0) {
@@ -4725,12 +5177,24 @@ function startBackgroundServe(opts) {
4725
5177
  }
4726
5178
  const entry = process.argv[1];
4727
5179
  if (!entry) throw new Error("Cannot resolve CLI entrypoint for background service");
5180
+ const stdio = backgroundStdio();
4728
5181
  const child = spawn(process.execPath, [entry, "serve", ...backgroundArgs(opts)], {
4729
5182
  detached: true,
4730
- stdio: "ignore"
5183
+ stdio: stdio.stdio
4731
5184
  });
4732
5185
  child.unref();
5186
+ log().info("background service spawned", {
5187
+ pid: child.pid,
5188
+ stdioFile: stdio.path,
5189
+ logFile: getLogger().filePath
5190
+ });
4733
5191
  const runtime = await waitForRuntime(child.pid, START_WAIT_MS);
5192
+ if (!runtime) {
5193
+ log().warn("timed out waiting for runtime.json", {
5194
+ pid: child.pid,
5195
+ timeoutMs: START_WAIT_MS
5196
+ });
5197
+ }
4734
5198
  const port = runtime?.port ?? opts.port ?? DEFAULT_SERVE_PORT;
4735
5199
  const url = runtime?.url ?? `http://localhost:${port}/`;
4736
5200
  console.log(`YorZ Service started in background (pid=${child.pid ?? "unknown"}).`);
@@ -4766,6 +5230,7 @@ async function ensureSkillsInstalledWithLog(cwd) {
4766
5230
  }
4767
5231
  }
4768
5232
  async function runStopServe() {
5233
+ silenceConsoleMirror();
4769
5234
  const all = await readAllProcesses();
4770
5235
  if (all.length === 0) {
4771
5236
  return {
@@ -4779,6 +5244,7 @@ async function runStopServe() {
4779
5244
  const dead = all.filter((p) => !isProcessAlive(p.pid));
4780
5245
  if (alive.length === 0) {
4781
5246
  await removeRuntime();
5247
+ log().warn("cleared stale runtime records", { pids: dead.map((p) => p.pid) });
4782
5248
  const deadPidList = dead.map((p) => `pid=${p.pid}`).join(", ");
4783
5249
  return {
4784
5250
  stopped: false,
@@ -4793,12 +5259,15 @@ async function runStopServe() {
4793
5259
  for (const proc of alive) {
4794
5260
  try {
4795
5261
  process.kill(proc.pid, "SIGTERM");
4796
- } catch {
5262
+ log().info("sent SIGTERM", { pid: proc.pid, url: proc.url });
5263
+ } catch (err) {
5264
+ log().warn("SIGTERM failed", { pid: proc.pid, err });
4797
5265
  failedPids.push(proc.pid);
4798
5266
  continue;
4799
5267
  }
4800
5268
  let stopped = await waitForProcessExit(proc.pid, 2e3);
4801
5269
  if (!stopped) {
5270
+ log().warn("still alive after SIGTERM, escalating to SIGKILL", { pid: proc.pid });
4802
5271
  try {
4803
5272
  process.kill(proc.pid, "SIGKILL");
4804
5273
  } catch {
@@ -4809,6 +5278,7 @@ async function runStopServe() {
4809
5278
  stoppedPids.push(proc.pid);
4810
5279
  stoppedUrls.push(proc.url);
4811
5280
  } else {
5281
+ log().warn("process did not exit", { pid: proc.pid });
4812
5282
  failedPids.push(proc.pid);
4813
5283
  }
4814
5284
  }
@@ -5384,7 +5854,7 @@ const frontmatterRequiredFields = {
5384
5854
  };
5385
5855
  const frontmatterUpdatedAt = {
5386
5856
  id: "frontmatter/updated-at",
5387
- description: "updated_at 值应形如 YYYY-MM-DD HH:mm:ss,原文中带单引号。",
5857
+ description: "updated_at 值应为 YAML 字符串,形如 YYYY-MM-DD HH:mm:ss。",
5388
5858
  check(ctx) {
5389
5859
  const findings = [];
5390
5860
  const fm = ctx.frontmatter;
@@ -5402,13 +5872,13 @@ const frontmatterUpdatedAt = {
5402
5872
  });
5403
5873
  }
5404
5874
  const afterColon = rawLine.slice(rawLine.indexOf(":") + 1).trim();
5405
- if (!(afterColon.startsWith("'") && afterColon.endsWith("'"))) {
5875
+ const quoted = afterColon.startsWith("'") && afterColon.endsWith("'") || afterColon.startsWith('"') && afterColon.endsWith('"');
5876
+ if (!quoted) {
5406
5877
  findings.push({
5407
5878
  ruleId: this.id,
5408
5879
  severity: "error",
5409
- message: "updated_at 写入时必须使用单引号包裹,避免 YAML 1.1 时间戳解析。",
5410
- line: lineNo,
5411
- hint: `updated_at: '${value}'`
5880
+ message: "updated_at 必须写为 YAML 字符串,避免裸时间戳被 YAML 解析。",
5881
+ line: lineNo
5412
5882
  });
5413
5883
  }
5414
5884
  return findings;