sillyspec 3.26.11 → 3.26.13

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 (58) hide show
  1. package/.claude/skills/sillyspec-archive/SKILL.md +1 -1
  2. package/.claude/skills/sillyspec-execute/SKILL.md +1 -1
  3. package/.claude/skills/sillyspec-knowledge/SKILL.md +1 -1
  4. package/.claude/skills/sillyspec-plan/SKILL.md +5 -5
  5. package/.claude/skills/sillyspec-quick/SKILL.md +5 -1
  6. package/README.md +7 -5
  7. package/SKILL.md +5 -4
  8. package/package.json +1 -1
  9. package/packages/dashboard/server/executor.js +10 -1
  10. package/packages/dashboard/server/index.js +108 -46
  11. package/src/config-schema.js +3 -0
  12. package/src/contract-matrix.js +22 -13
  13. package/src/db.js +24 -4
  14. package/src/dispatch/probe.js +99 -63
  15. package/src/docs-check.js +58 -13
  16. package/src/docs-debt.js +6 -2
  17. package/src/docs-gate.js +1 -0
  18. package/src/doctor-diagnostics.js +208 -21
  19. package/src/fs-atomic.js +3 -1
  20. package/src/git-helper.js +7 -4
  21. package/src/hooks/worktree-guard.js +8 -2
  22. package/src/index.js +161 -12
  23. package/src/init.js +3 -2
  24. package/src/local-detect.js +9 -1
  25. package/src/modules.js +7 -5
  26. package/src/progress/stage-machine.js +589 -578
  27. package/src/progress/step-store.js +1 -1
  28. package/src/progress.js +184 -30
  29. package/src/quicklog.js +47 -16
  30. package/src/run/command.js +31 -8
  31. package/src/run/complete-handlers.js +21 -14
  32. package/src/run/complete.js +1181 -1121
  33. package/src/run/gates.js +148 -25
  34. package/src/run/prompt.js +9 -5
  35. package/src/run/shared.js +138 -58
  36. package/src/run/stage.js +41 -1
  37. package/src/setup.js +8 -10
  38. package/src/sillyhub-mcp/client.js +74 -6
  39. package/src/spec-dir-typo.js +3 -24
  40. package/src/spec-sync.js +43 -8
  41. package/src/stage-contract.js +12 -38
  42. package/src/stages/archive.js +4 -3
  43. package/src/stages/brainstorm-auto.js +1 -1
  44. package/src/stages/brainstorm.js +2 -1
  45. package/src/stages/doctor.js +28 -13
  46. package/src/stages/execute.js +164 -153
  47. package/src/stages/plan-postcheck.js +49 -28
  48. package/src/stages/plan.js +57 -40
  49. package/src/stages/quick.js +3 -1
  50. package/src/stages/verify.js +1 -1
  51. package/src/sync.js +323 -221
  52. package/src/task-review.js +246 -61
  53. package/src/taskcard.js +178 -0
  54. package/src/verify-postcheck.js +48 -35
  55. package/src/worktree-apply.js +58 -34
  56. package/src/worktree-deps.js +120 -20
  57. package/src/worktree.js +55 -9
  58. package/.npmrc-publish +0 -2
@@ -64,7 +64,7 @@ sillyspec run archive --continue --answer "确认写入"
64
64
 
65
65
  ### 归档结果
66
66
 
67
- 归档后变更目录从 `changes/<名>/` 移到 `changes/archive/YYYY-MM-DD-<名>/`,并从活跃列表注销。后续用 `/sillyspec:commit` 提交。
67
+ 归档后变更目录从 `changes/<名>/` 移到 `changes/archive/<名>/`,并从活跃列表注销。后续用 `/sillyspec:commit` 提交。
68
68
 
69
69
  ## 阶段流转
70
70
 
@@ -187,7 +187,7 @@ execute 完成后(所有 Wave/task 完成 + Task Review Gate 通过),运
187
187
 
188
188
  ### 批量完成(一次 --done 收尾)
189
189
 
190
- 当 `plan.md` 所有 task checkbox 已勾(人工勾或基于各 task `review.json` pass 由 CLI 自动勾)且代码客观核验通过(`checkExecuteCodeEvidence` 非"零变更")时,任一 execute `--done` 会**一次性补完所有剩余 step** 直达阶段完成,不必逐次 +1 推进。日志会打印 `🚀 execute 批量完成:plan 全勾 + 代码核验通过,一次性补完 N 个剩余 step`。条件不满足(plan 未全勾 / 代码零变更)时仍按单步推进,要求补 review 后重跑 `--done` 直至满足。
190
+ 当 `tasks.md`(任务注册表唯一真相)所有 task checkbox 已勾(agent 按 review gate 手动勾或基于各 task `review.json` pass 由 CLI `autoCheckPlanFromReviews` 自动勾,双路都写 tasks.md、文件锁 `.tasks.md.lock` 串行化)且代码客观核验通过(`checkExecuteCodeEvidence` 非"零变更")时,任一 execute `--done` 会**一次性补完所有剩余 step** 直达阶段完成,不必逐次 +1 推进。日志会打印 `🚀 execute 批量完成` 提示。条件不满足(tasks.md 未全勾 / 代码零变更)时仍按单步推进,要求补 review 后重跑 `--done` 直至满足。
191
191
 
192
192
  ## 铁律
193
193
 
@@ -39,7 +39,7 @@ sillyspec knowledge search --query "GLM usage metadata"
39
39
  "id": "patterns",
40
40
  "path": "knowledge/patterns.md",
41
41
  "title": "Patterns",
42
- "summary": "每个阶段对应 `src/stages/` 下的独立模块...",
42
+ "summary": "每个阶段一个独立模块定义(含步骤与提示词)...",
43
43
  "score": 3,
44
44
  "tags": ["worktree", "git-worktree", "WorktreeManager"],
45
45
  "category": "Patterns"
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  name: sillyspec:plan
3
- description: 用于把 design 拆解为可执行的实现计划。适合用户说"拆任务、做计划、排 wave、规划实现步骤"。产出 plan.md(Wave 分组 + Task 列表 + 依赖关系)。
3
+ description: 用于把 design 拆解为可执行的实现计划。适合用户说"拆任务、做计划、排 wave、规划实现步骤"。产出 plan.md(Wave 分组 + 任务 ID 引用 + 依赖关系)+ tasks.md 任务清单(唯一真相,plan 阶段展开写回)。
4
4
  ---
5
5
 
6
6
  ## 何时使用
7
7
 
8
8
  - 用户说"拆任务、做计划、排 wave、规划实现步骤"
9
9
  - 把 brainstorm 的 design.md 拆成可执行的 Wave + Task
10
- - 产出:`plan.md`(Wave 分组 + Task 列表 + 依赖关系),可能含 `tasks/task-NN.md` 任务蓝图
10
+ - 产出:`plan.md`(Wave 分组 + 任务 ID 引用行 + 依赖关系)+ `tasks.md` 任务清单展开写回(唯一真相)+ `tasks/task-NN.md` 任务蓝图
11
11
 
12
12
  ## 多变更说明
13
13
 
@@ -43,7 +43,7 @@ sillyspec run plan --done --answer "..." --output "..." # 一步完成 wait+d
43
43
 
44
44
  ### 动态步骤
45
45
 
46
- plan 的步骤是动态的:`generate_plan`(生成分级计划)→ `review_plan`(审查计划,按规模分级:tier=self 当前 agent 自审 / tier=independent 启动独立审查子代理产出 stage review.json,避免生成+自审同一次输出的偏差)→ CLI 从 `plan.md` 解析出 task 自动插入"任务蓝图协调器"步骤(per-task)。这是正常行为,不要手动添加。
46
+ plan 的步骤是动态的:`generate_plan`(生成分级计划)→ `review_plan`(审查计划,按规模分级:tier=self 当前 agent 自审 / tier=independent 启动独立审查子代理产出 stage review.json,避免生成+自审同一次输出的偏差)→ CLI 从 `tasks.md` 注册表解析出 task 自动插入"任务蓝图协调器"步骤(per-task;plan.md 只提供 Wave 引用分组)。这是正常行为,不要手动添加。
47
47
 
48
48
  ### Stage Review Gate(plan 的 design review.json)
49
49
 
@@ -75,7 +75,7 @@ plan 的步骤是动态的:`generate_plan`(生成分级计划)→ `review_
75
75
  ### 契约门控(阻断完成)
76
76
 
77
77
  - **plan 启动前**:CLI 校验 `design.md` 是否满足 plan 契约(缺文件变更清单/风险登记/自审章节会阻断)。若失败需先 `sillyspec run brainstorm --reopen --from-step N` 修订设计。
78
- - **plan 完成时**:CLI 校验 `plan.md` 是否满足 execute 契约(Wave 结构、task 引用等)。失败会阻断完成,提示修复后重新 `--done`。
78
+ - **plan 完成时**:CLI 校验 `tasks.md`(任务注册表)× `plan.md`(Wave ID 引用)是否满足 execute 契约(引用存在性/覆盖恰一次/编号连续/旧格式拦)。失败会阻断完成,提示修复后重新 `--done`。
79
79
 
80
80
  ### 生产接线路径检查
81
81
 
@@ -141,7 +141,7 @@ plan 完成后(plan.md 通过 execute 契约校验),运行 `sillyspec run
141
141
 
142
142
  - **必须用 exec 工具(shell)执行 CLI,不要自己编造流程**
143
143
  - 只做当前步骤 prompt 描述的操作,不跳过、不自行扩展
144
- - plan.md 是任务完成的唯一真相源,task 拆解粒度要均匀、依赖要明确
144
+ - tasks.md 是任务清单唯一真相(checkbox 注册表,plan 阶段展开写回——保留 ql-xxx 等非 task-XX 行);plan.md Wave 段只写纯 ID 引用行(`- task-XX`),不重抄任务名
145
145
  - 完成后立即 `--done`,不跳过
146
146
 
147
147
  ## 用户指令
@@ -50,6 +50,7 @@ sillyspec run quick --done --change quick-<hash> --output "…" # 完成该会
50
50
  | 参数 | 说明 |
51
51
  |---|---|
52
52
  | `--spec-dir <path>` | 指定规范目录(默认 `<项目>/.sillyspec`) |
53
+ | `--input "<一句话任务描述>"` | 通用参数,**quick 启动时强烈建议带**:作为 QUICKLOG 条目标题,条目从第一分钟即语义可读。不带则落「(quick 任务)」占位标题——平台「快速修复」列表默认隐藏进行中的占位条目,语义标题要到最终 `--done` 才回填(关联变更有 proposal/design 标题时可自动提取,免传) |
53
54
  | `--non-interactive` | CI/脚本下禁用交互式 prompt |
54
55
  | `--interactive` | 强制交互(即便 stdin 非 TTY) |
55
56
  | `--skip-approval` | 跳过阶段转换/审批检查(不能跳产物校验 gate——review.json/文档产物硬校验仍在) |
@@ -59,7 +60,7 @@ sillyspec run quick --done --change quick-<hash> --output "…" # 完成该会
59
60
  | 参数 | 说明 |
60
61
  |---|---|
61
62
  | `--linked-changes none\|a,b` | **显式关联变更(取代 `--change`,推荐)**。none=不关联,a,b=关联列表 |
62
- | `--files a.js,b.js` | 显式声明本次允许修改的文件(边界保护 + 声明即归属:多 agent 并发仓防他者窗口文件混入 QUICKLOG 文件行,未声明窗口文件进「审计:」行追溯) |
63
+ | `--files a.js,b.js` | 显式声明本次允许修改的文件(边界保护 + 声明即归属:多 agent 并发仓防他者窗口文件混入 QUICKLOG 文件行,未声明窗口文件进「审计:」行追溯)。**中途可追加**:发现要改声明外文件(如新写的测试)时,带新增文件恢复会话即并入边界(追加不替换)——`sillyspec run quick --files <新增> --change <sessionId>`;不带 `--files` 恢复则边界原样保留 |
63
64
  | `--file-notes "p::注 \|\| p::注"` | quick `--done` 用:QUICKLOG「文件:」行落盘为多行带括注 bullet(省事后手改文件行)。格式 `path::括注`,`\|\|` 分隔多条;**只随 step3 --done 同命令传**(CLI 短进程,step1/step2 传无效,不带到 step3) |
64
65
  | `--allow-new` | 允许新增文件(默认禁止,防意外创建) |
65
66
  | `--allow-delete` | 允许删除文件(默认 fail-closed,删除是破坏性操作;确认删除带此 flag 显式解锁) |
@@ -79,6 +80,9 @@ sillyspec run quick --done --change quick-<hash> --output "…" # 完成该会
79
80
  ## 典型用法
80
81
 
81
82
  ```bash
83
+ # 推荐启动:带一句话语义标题(QUICKLOG/平台快速修复列表进行中即可见可读)
84
+ sillyspec run quick --input "修复登录限流 INCR 计数误清" --linked-changes none --files src/auth.ts
85
+
82
86
  # 单变更项目,直接开始
83
87
  sillyspec run quick
84
88
 
package/README.md CHANGED
@@ -60,7 +60,7 @@ npx sillyspec init --interactive # 完整引导
60
60
  SillySpec 进度库使用 SQLite 持久化(基于 Node.js 内置的 `node:sqlite` 模块,纯 JS 无原生编译依赖)。
61
61
 
62
62
  - **Node.js 版本要求 >= 22.13.0**:`node:sqlite` 自 v22.13.0 起免 `--experimental-sqlite` flag 直接可用。支持 Linux x64、macOS x64 / arm64、Windows x64 等所有 Node.js 官方支持平台。
63
- - **零编译安装**:不依赖任何原生绑定 / node-gyp / C++ 工具链,npm install 即装即用,无平台编译失败风险。
63
+ - **零编译安装**:`node:sqlite` 是 Node.js 内置模块(官方预编译发行,非纯 JS 实现),不依赖 node-gyp / C++ 工具链或任何第三方原生绑定,npm install 即装即用,无平台编译失败风险。
64
64
 
65
65
  ## 从哪里开始
66
66
 
@@ -133,12 +133,12 @@ sillyspec doctor 全量自检 + 修复进度
133
133
  ## 核心特性
134
134
 
135
135
  - **规范驱动** — 所有代码产出先有设计文档支撑,文档是 AI 的记忆
136
- - **阶段状态机** — 以 stage + step 粒度强制流转,gate-status + progress.db 双轨记录状态
136
+ - **阶段状态机** — 以 stage + step 粒度强制流转,sillyspec.db(SQLite)单一进度源记录状态
137
137
  - **TDD 强制** — execute 阶段先写测试再写实现
138
138
  - **子代理并行** — 同一 Wave 内任务并行执行,加快交付
139
139
  - **Worktree 隔离** — execute 在独立 git worktree 中工作,不污染主分支
140
140
  - **拓扑排序 Wave** — plan 阶段按蓝图依赖关系自动重排 Wave 分组
141
- - **进度持久化** — SQLite(sql.js WASM)持久化,支持断点恢复
141
+ - **进度持久化** — SQLite(node:sqlite)持久化,支持断点恢复
142
142
  - **模块文档** — 模块级知识库,AI 执行时按需加载相关上下文
143
143
  - **浏览器自动化** — 可选 MCP(Playwright / Chrome DevTools),供集成/E2E 冒烟验证
144
144
  - **平台同步** — 可选对接 SillyHub,文档同步 + 团队审批
@@ -175,12 +175,14 @@ sillyspec/
175
175
  ├── bin/sillyspec.js # CLI 入口(shebang)
176
176
  ├── src/
177
177
  │ ├── index.js # 命令分发
178
- │ ├── run.js # 阶段状态机引擎
178
+ │ ├── run.js + run/ # 阶段状态机引擎(barrel + 实现模块)
179
179
  │ ├── init.js / setup.js # 初始化 + MCP 工具安装
180
- │ ├── progress.js / db.js # SQLite 进度存储
180
+ │ ├── progress.js / db.js # SQLite 进度存储(facade + 实现)
181
181
  │ ├── sync.js # SillyHub 平台同步
182
182
  │ ├── workflow.js # 工作流编排 + postcheck
183
183
  │ ├── worktree*.js # worktree 隔离执行
184
+ │ ├── dispatch/ # 任务派发(本地 agent / SillyHub MCP)
185
+ │ ├── sillyhub-mcp/ # SillyHub MCP 客户端
184
186
  │ ├── hooks/ # worktree 守卫等钩子
185
187
  │ └── stages/ # 各阶段定义(scan/brainstorm/plan/execute/verify/...)
186
188
  ├── templates/workflows/ # 工作流定义(scan-docs / archive-impact)
package/SKILL.md CHANGED
@@ -1,12 +1,12 @@
1
1
  ---
2
2
  name: sillyspec
3
- description: "规范驱动开发工具包。绿地用 /sillyspec:init,棕地用 /sillyspec:scan,全自动用 /sillyspec:auto。完整流程:scan → brainstorm → plan → execute → verify → archive。支持 TDD、子代理并行、worktree 隔离、E2E 验证。兼容 Claude Code / Cursor / Codex / OpenCode / OpenClaw。"
3
+ description: "规范驱动开发工具包。绿地用 /sillyspec:init,棕地用 /sillyspec:scan,全自动用 /sillyspec:auto。完整流程:scan → brainstorm → plan → execute → verify → archive。支持 TDD、子代理并行、worktree 隔离、E2E 验证。兼容 Claude Code / Cursor / Codex / OpenCode / OpenClaw / Gemini。"
4
4
  ---
5
5
 
6
6
  # SillySpec
7
7
 
8
8
  从"你说要啥"到"代码能跑"的规范驱动开发工具包。
9
- Claude Code / Cursor / Codex / OpenCode / OpenClaw 通用。
9
+ Claude Code / Cursor / Codex / OpenCode / OpenClaw / Gemini 通用。
10
10
 
11
11
  ## 快速开始
12
12
 
@@ -43,13 +43,14 @@ Claude Code / Cursor / Codex / OpenCode / OpenClaw 通用。
43
43
 
44
44
  | 命令 | 用途 |
45
45
  |---|---|
46
- | `/sillyspec:status` | 查看项目进度和状态 |
46
+ | `/sillyspec:status` · `/sillyspec:state` | 查看项目进度和状态 |
47
47
  | `/sillyspec:continue` | 自动判断并执行下一步 |
48
48
  | `/sillyspec:explore` | 自由思考模式 |
49
49
  | `/sillyspec:quick` | 快速任务,跳过完整流程 |
50
50
  | `/sillyspec:resume` | 恢复工作 |
51
51
  | `/sillyspec:doctor` | 项目自检 |
52
52
  | `/sillyspec:commit` | 智能提交 |
53
+ | `/sillyspec:knowledge` | 知识库检索 / 沉淀 |
53
54
  | `/sillyspec:export` | 导出成功方案为可复用模板 |
54
55
  | `/sillyspec:workspace` | 多项目工作区管理 |
55
56
 
@@ -80,7 +81,7 @@ sillyspec init 初始化(零交互,自动检测工具)
80
81
  - **E2E 验证** — 内置 E2E 测试流程,支持 Playwright / 浏览器 MCP
81
82
  - **模块文档** — 支持模块级知识库,AI 执行时按需加载相关模块上下文
82
83
  - **进度管理** — SQLite 持久化进度,断点恢复
83
- - **MCP 增强** — 一键安装 Context7、grep.app、Chrome DevTools
84
+ - **MCP 增强** — 一键安装 Context7、Chrome DevTools、PinchTab
84
85
 
85
86
  ## MCP 工具
86
87
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sillyspec",
3
- "version": "3.26.11",
3
+ "version": "3.26.13",
4
4
  "description": "SillySpec CLI — 流程状态机,让 AI 严格按步骤来",
5
5
  "icon": "logo.jpg",
6
6
  "homepage": "https://sillyspec.ppdmq.top/",
@@ -1,4 +1,10 @@
1
1
  import { spawn } from 'child_process'
2
+ import { fileURLToPath } from 'url'
3
+
4
+ // sillyspec CLI 入口(包根/bin/sillyspec.js):dashboard server 随 sillyspec npm 包分发
5
+ // (src/index.js 的 dashboard 分支 import 本目录),该相对位置在源码仓与 npm 包内一致
6
+ // (packages/dashboard/server/ → 包根/bin/)。
7
+ const SILLYSPEC_ENTRY = fileURLToPath(new URL('../../../bin/sillyspec.js', import.meta.url))
2
8
 
3
9
  /**
4
10
  * Execute a SillySpec CLI command in the given project directory
@@ -10,7 +16,10 @@ import { spawn } from 'child_process'
10
16
  */
11
17
  export function executeCommand(projectPath, command, onOutput, onComplete) {
12
18
  const args = String(command || '').trim().split(/\s+/).filter(Boolean)
13
- const proc = spawn('npx', ['sillyspec', ...args], {
19
+ // process.execPath 直接指定解释器跑包内入口:spawn('npx') 在 Windows 无法解析
20
+ // npx.cmd(体检 BUG-11,CLI 执行功能全灭);数组参数无 shell,同时杜绝未来为兼容
21
+ // .cmd 加 shell:true 而引入命令注入(体检 SEC-08)
22
+ const proc = spawn(process.execPath, [SILLYSPEC_ENTRY, ...args], {
14
23
  cwd: projectPath,
15
24
  env: { ...process.env }
16
25
  })
@@ -114,11 +114,39 @@ function isViewableDocPath(filePath) {
114
114
  return /\.(md|markdown|mdx|html?|txt|log|json|ya?ml|toml|xml|csv)$/i.test(filePath)
115
115
  }
116
116
 
117
- function isLocalOrigin(origin) {
117
+ // ── 项目发现缓存 + 路径校验(体检 SEC-06;TTL 缓存兼避免每请求全量扫 HOME 级目录)──
118
+ let _projectsCache = { at: 0, data: null }
119
+ async function getDiscoveredProjects() {
120
+ const TTL_MS = 10_000
121
+ if (_projectsCache.data && Date.now() - _projectsCache.at < TTL_MS) return _projectsCache.data
122
+ const data = await discoverProjects()
123
+ _projectsCache = { at: Date.now(), data }
124
+ return data
125
+ }
126
+
127
+ /**
128
+ * 目标路径是否位于某个已发现项目内。此前 /api/docs/content 只查"路径含 .sillyspec 组件",
129
+ * 磁盘上任意 .sillyspec 树(含 .runtime 运行时文件)都可读;detail API 还以任意路径为
130
+ * cwd 跑 git。收紧为"必须位于已发现项目的目录内"(与 WS cli:execute 的项目校验同范式)。
131
+ */
132
+ async function isKnownProjectPath(targetPath) {
133
+ const projects = await getDiscoveredProjects()
134
+ return projects.some(p => isInside(p.path, targetPath))
135
+ }
136
+
137
+ /**
138
+ * 跨站防护(体检 SEC-03 收紧):dashboard 前端由本服务自身托管,合法浏览器请求的
139
+ * Origin 端口必然等于服务端口。此前只校验 hostname,本机任意其他端口的 web 页面
140
+ * (被攻陷的 dev server / 恶意 npm 包起的页面)也能通过检查,驱动 WS cli:execute 等
141
+ * 完整 API。无 Origin 头(curl / 本地非浏览器进程)保持放行——与 localhost 服务
142
+ * "本地进程可信"的固有级别一致,浏览器攻击面才是本检查能挡的部分。
143
+ */
144
+ function isSameDashboardOrigin(origin, ownPort) {
118
145
  if (!origin) return true
119
146
  try {
120
- const { hostname } = new URL(origin)
121
- return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1'
147
+ const { hostname, port: originPort } = new URL(origin)
148
+ if (hostname !== 'localhost' && hostname !== '127.0.0.1' && hostname !== '::1') return false
149
+ return String(originPort) === String(ownPort)
122
150
  } catch {
123
151
  return false
124
152
  }
@@ -287,7 +315,7 @@ function handleCliExecute(ws, data) {
287
315
  function startServer({ port = 3456, open: openBrowser = true } = {}) {
288
316
  const server = createServer((req, res) => {
289
317
  const origin = req.headers.origin
290
- if (!isLocalOrigin(origin)) {
318
+ if (!isSameDashboardOrigin(origin, port)) {
291
319
  res.writeHead(403)
292
320
  res.end('Forbidden')
293
321
  return
@@ -315,41 +343,61 @@ function startServer({ port = 3456, open: openBrowser = true } = {}) {
315
343
  return
316
344
  }
317
345
 
318
- // Detail API
346
+ // Detail API(SEC-06:projectPath 须位于已发现项目内——parseGitDetail 以其为 cwd 跑 git)
319
347
  const detailMatch = req.url?.match(/^\/api\/projects\/(.+)\/detail(\?|$)/)
320
348
  if (detailMatch) {
321
349
  const projectPath = decodeURIComponent(detailMatch[1])
322
350
  const url = new URL(req.url, `http://${req.headers.host}`)
323
351
  const type = url.searchParams.get('type')
324
- let data
325
- try {
326
- if (type === 'git') data = parseGitDetail(projectPath)
327
- else if (type === 'tech') data = parseTechStackDetail(projectPath)
328
- else if (type === 'docs') data = parseSillyspecDocsTree(projectPath).groups
329
- else { res.writeHead(400); res.end(JSON.stringify({ error: 'Invalid type' })); return }
330
- res.setHeader('Content-Type', 'application/json')
331
- res.writeHead(200)
332
- res.end(JSON.stringify(data))
333
- } catch (err) {
352
+ isKnownProjectPath(projectPath).then(known => {
353
+ if (!known) {
354
+ res.writeHead(403)
355
+ res.end(JSON.stringify({ error: 'Unknown project path' }))
356
+ return
357
+ }
358
+ let data
359
+ try {
360
+ if (type === 'git') data = parseGitDetail(projectPath)
361
+ else if (type === 'tech') data = parseTechStackDetail(projectPath)
362
+ else if (type === 'docs') data = parseSillyspecDocsTree(projectPath).groups
363
+ else { res.writeHead(400); res.end(JSON.stringify({ error: 'Invalid type' })); return }
364
+ res.setHeader('Content-Type', 'application/json')
365
+ res.writeHead(200)
366
+ res.end(JSON.stringify(data))
367
+ } catch (err) {
368
+ res.writeHead(500)
369
+ res.end(JSON.stringify({ error: err.message }))
370
+ }
371
+ }).catch(() => {
334
372
  res.writeHead(500)
335
- res.end(JSON.stringify({ error: err.message }))
336
- }
373
+ res.end(JSON.stringify({ error: 'Project discovery failed' }))
374
+ })
337
375
  return
338
376
  }
339
377
 
340
- // Overview API
378
+ // Overview API(SEC-06:同 detail,projectPath 须位于已发现项目内)
341
379
  if (req.url?.startsWith('/api/projects/') && req.url.endsWith('/overview')) {
342
380
  const parts = req.url.replace('/api/projects/', '').replace('/overview', '').split('/')
343
381
  const projectPath = decodeURIComponent(parts.join('/'))
344
- try {
345
- const overview = parseProjectOverview(projectPath)
346
- res.setHeader('Content-Type', 'application/json')
347
- res.writeHead(200)
348
- res.end(JSON.stringify(overview))
349
- } catch (err) {
382
+ isKnownProjectPath(projectPath).then(known => {
383
+ if (!known) {
384
+ res.writeHead(403)
385
+ res.end(JSON.stringify({ error: 'Unknown project path' }))
386
+ return
387
+ }
388
+ try {
389
+ const overview = parseProjectOverview(projectPath)
390
+ res.setHeader('Content-Type', 'application/json')
391
+ res.writeHead(200)
392
+ res.end(JSON.stringify(overview))
393
+ } catch (err) {
394
+ res.writeHead(500)
395
+ res.end(JSON.stringify({ error: err.message }))
396
+ }
397
+ }).catch(() => {
350
398
  res.writeHead(500)
351
- res.end(JSON.stringify({ error: err.message }))
352
- }
399
+ res.end(JSON.stringify({ error: 'Project discovery failed' }))
400
+ })
353
401
  return
354
402
  }
355
403
 
@@ -382,27 +430,33 @@ function startServer({ port = 3456, open: openBrowser = true } = {}) {
382
430
  res.end(JSON.stringify({ error: 'Missing path parameter' }))
383
431
  return
384
432
  }
385
- // Security: only allow reading viewable text documents under a .sillyspec tree.
433
+ // Security: only allow reading viewable text documents under a discovered
434
+ // project's .sillyspec tree(SEC-06:不限在已发现项目内则磁盘上任意 .sillyspec 树可读).
386
435
  const normalizedPath = resolve(filePath)
387
- if (!isSillyspecPath(normalizedPath) || !isViewableDocPath(normalizedPath) || isCredentialPath(normalizedPath)) {
388
- res.writeHead(403)
389
- res.end(JSON.stringify({ error: 'Access denied' }))
390
- return
391
- }
392
- if (existsSync(normalizedPath)) {
393
- try {
394
- const content = readFileSync(normalizedPath, 'utf-8')
395
- res.setHeader('Content-Type', 'text/plain; charset=utf-8')
396
- res.writeHead(200)
397
- res.end(content)
398
- } catch (err) {
399
- res.writeHead(500)
400
- res.end(JSON.stringify({ error: err.message }))
436
+ isKnownProjectPath(normalizedPath).then(known => {
437
+ if (!known || !isSillyspecPath(normalizedPath) || !isViewableDocPath(normalizedPath) || isCredentialPath(normalizedPath)) {
438
+ res.writeHead(403)
439
+ res.end(JSON.stringify({ error: 'Access denied' }))
440
+ return
401
441
  }
402
- } else {
403
- res.writeHead(404)
404
- res.end(JSON.stringify({ error: 'File not found' }))
405
- }
442
+ if (existsSync(normalizedPath)) {
443
+ try {
444
+ const content = readFileSync(normalizedPath, 'utf-8')
445
+ res.setHeader('Content-Type', 'text/plain; charset=utf-8')
446
+ res.writeHead(200)
447
+ res.end(content)
448
+ } catch (err) {
449
+ res.writeHead(500)
450
+ res.end(JSON.stringify({ error: err.message }))
451
+ }
452
+ } else {
453
+ res.writeHead(404)
454
+ res.end(JSON.stringify({ error: 'File not found' }))
455
+ }
456
+ }).catch(() => {
457
+ res.writeHead(500)
458
+ res.end(JSON.stringify({ error: 'Project discovery failed' }))
459
+ })
406
460
  return
407
461
  }
408
462
 
@@ -460,7 +514,7 @@ function startServer({ port = 3456, open: openBrowser = true } = {}) {
460
514
  })
461
515
 
462
516
  wss.on('connection', (ws, req) => {
463
- if (!isLocalOrigin(req.headers.origin)) {
517
+ if (!isSameDashboardOrigin(req.headers.origin, port)) {
464
518
  ws.close(1008, 'Forbidden origin')
465
519
  return
466
520
  }
@@ -589,6 +643,14 @@ function startServer({ port = 3456, open: openBrowser = true } = {}) {
589
643
  stopWatcher()
590
644
  activeProcesses.forEach(kill => kill())
591
645
  activeProcesses.clear()
646
+ // server.close() 只停止接受新连接,不终止已升级的 WS 连接——事件循环被客户端
647
+ // 持有导致 Ctrl+C 无法退出(体检 BUG-10)。先 terminate 客户端再关 wss。
648
+ if (wss) {
649
+ for (const client of wss.clients) {
650
+ try { client.terminate() } catch { /* 已断开的客户端忽略 */ }
651
+ }
652
+ try { wss.close() } catch { /* 关闭失败不阻断退出 */ }
653
+ }
592
654
  server.close()
593
655
  }
594
656
 
@@ -154,6 +154,7 @@ export const LOCAL_YAML_SCHEMA = {
154
154
  { path: 'docs-check.skip', type: 'array', optional: true, status: 'live', readers: ['walkGlob (src/docs-check.js)'], desc: '排除的路径/glob 列表,缺省空。', example: 'docs/sillyspec/archive' },
155
155
  { path: 'docs-check.keywordAssert', type: 'boolean', optional: true, status: 'live', readers: ['runDocsCheck (src/docs-check.js)'], desc: '层2 关键词断言开关,缺省 true(关闭时 warning 提示仅做存在性校验)。', example: 'true' },
156
156
  { path: 'docs-check.living-docs', type: 'array', optional: true, status: 'live', readers: ['resolveLivingDocs (src/run/shared.js) — 经 readLocalYamlRaw 直读'], desc: '活文档监控点追加列表(docs check 之外,quick 审计的 livingDocDrift 真失效提示也监控——只报校验真失败的引用,全过零输出)。只追加不覆盖缺省集合(缺省:docs/sillyspec/platform-interface-map.md)。配了是加哨兵,不该把缺省监控点挤掉。', example: 'docs/sillyspec/architecture.md' },
157
+ { path: 'docs-check.cross_repo_roots', type: 'object', optional: true, status: 'live', readers: ['readDocsCheckConfig (src/docs-check.js)'], desc: 'repo://<仓库名> 跨仓引用 → 本机仓库根目录映射。文档里 repo://name/src/x.js:12 形态的引用:未配映射默认跳过(不同设备仓库位置不同,防跨设备误报);配了映射走与本地引用相同的行号+关键词校验。local.yaml 是 gitignored,绝对路径不入库,每台设备各自配。', example: 'sillyspec: /c/Users/you/IdeaProjects/sillyspec' },
157
158
  ],
158
159
  },
159
160
  ],
@@ -306,5 +307,7 @@ docs-check:
306
307
  keywordAssert: true
307
308
  # living-docs: # 活文档监控点追加(缺省已含 docs/sillyspec/platform-interface-map.md,只加哨兵不覆盖)
308
309
  # - docs/sillyspec/architecture.md
310
+ # cross_repo_roots: # repo://<仓库名> 跨仓引用 → 本机仓库根(每台设备各自配,绝对路径勿提交)
311
+ # sillyspec: /c/Users/you/IdeaProjects/sillyspec
309
312
  `;
310
313
  }
@@ -46,18 +46,24 @@ export function classifyTask(taskContent) {
46
46
  }
47
47
 
48
48
  /**
49
- * 从 plan.md 解析 task 依赖关系,识别 provider → consumer 对
50
- * @param {string} planContent - plan.md 内容
49
+ * 解析 task 依赖关系,识别 provider → consumer 对(2026-08-20-task-truth-unify D-003@v1:
50
+ * 行内 depends_on 标注源迁 tasks.md,函数内 tasks.md 优先、plan.md 回退拼接)
51
+ * @param {string} planContent - plan.md 内容(任务总表依赖列兜底源)
51
52
  * @param {string} changeDir - changes/<name>/ 目录
52
53
  * @returns {Array<{ provider: string, consumer: string, type: string }>}
53
54
  */
54
55
  export function buildContractMatrix(planContent, changeDir) {
55
56
  const contracts = []
56
57
 
57
- // 解析 task 依赖关系
58
- // 格式: - [ ] task-04: ... (depends_on: [task-01]) 或
59
- // | task-04 | ... | 01 |
60
- const taskDeps = parseTaskDependencies(planContent)
58
+ // 解析 task 依赖关系(tasks.md 注册表行内标注优先;plan.md 总表列兜底——两源拼合去重)
59
+ // tasks.md 格式: - [ ] task-04: 名称 (depends_on: task-01,02);
60
+ // plan.md 总表: | task-04 | ... | 01 |
61
+ const tasksMdPath = changeDir ? join(changeDir, 'tasks.md') : null
62
+ let registryContent = ''
63
+ if (tasksMdPath && existsSync(tasksMdPath)) {
64
+ try { registryContent = readFileSync(tasksMdPath, 'utf8') } catch { registryContent = '' }
65
+ }
66
+ const taskDeps = parseTaskDependencies(registryContent + '\n' + String(planContent || ''))
61
67
 
62
68
  // 读取各 task 文档,分类 provider/consumer
63
69
  // classify consumers(taskDeps keys)+ providers(deps 值)。
@@ -100,25 +106,28 @@ export function buildContractMatrix(planContent, changeDir) {
100
106
  }
101
107
 
102
108
  /**
103
- * 从 plan.md 解析 task 依赖关系
104
- * @param {string} planContent
109
+ * 解析 task 依赖关系(2026-08-20-task-truth-unify D-003@v1)。
110
+ * 方式 2(depends_on 行内标注)的机器解析源随任务行迁 tasks.md——调用方 buildContractMatrix
111
+ * 读 tasks.md 传入(缺失回退 plan.md 内容,旧变更兼容);方式 1(任务总表依赖列)仍来自 plan.md。
112
+ * @param {string} registryContent - tasks.md 内容(优先)或 plan.md 内容(回退)
105
113
  * @returns {Record<string, string[]>} task → depends_on list
106
114
  */
107
- function parseTaskDependencies(planContent) {
115
+ function parseTaskDependencies(registryContent) {
108
116
  const deps = {}
109
117
 
110
- // 方式 1: 表格形式 | task-04 | ... | 01,02 |
118
+ // 方式 1: 任务总表表格形式 | task-04 | ... | 01,02 |
111
119
  // 列前缀用 [^|\d]*(不吃数字):旧 [^|]* 贪婪会吃掉 deps 列前导 0(" 01" → g2=1 → task-1),
112
120
  // 导致 provider task-1.md 不存在 → contracts 恒空,端点契约管线失效。
113
- const tableRows = planContent.matchAll(/\|[^|]*task-(\d+)[^|]*\|[^|]*\|[^|\d]*(?:task-)?(\d+(?:\s*[,,]\s*\d+)*)[^|]*\|/gi)
121
+ const tableRows = registryContent.matchAll(/\|[^|]*task-(\d+)[^|]*\|[^|]*\|[^|\d]*(?:task-)?(\d+(?:\s*[,,]\s*\d+)*)[^|]*\|/gi)
114
122
  for (const match of tableRows) {
115
123
  const task = `task-${match[1]}`
116
124
  const depList = match[2].split(/[,,\s]+/).map(d => `task-${d.trim()}`).filter(d => d.startsWith('task-'))
117
125
  deps[task] = depList
118
126
  }
119
127
 
120
- // 方式 2: depends_on 关键字
121
- const dependsPattern = planContent.matchAll(/task-(\d+).*?depends_on.*?(\d+(?:\s*[,,]\s*\d+)*)/gi)
128
+ // 方式 2: tasks.md 行内标注 "- [ ] task-04: 名称 (depends_on: task-01,02)"(新家,D-003@v1;
129
+ // 旧 plan.md checkbox 行内标注同格式兼容)
130
+ const dependsPattern = registryContent.matchAll(/task-(\d+).*?depends_on.*?(\d+(?:\s*[,,]\s*\d+)*)/gi)
122
131
  for (const match of dependsPattern) {
123
132
  const task = `task-${match[1]}`
124
133
  const depList = match[2].split(/[,,\s]+/).map(d => `task-${d.trim()}`).filter(d => d.startsWith('task-'))
package/src/db.js CHANGED
@@ -144,8 +144,22 @@ export class DB {
144
144
  : 'sillyspec.db 损坏';
145
145
  console.warn(`⚠️ ${reason},已从 .bak 备份恢复。`);
146
146
  bakDb.close();
147
- // node:sqlite 绑定路径,无 _save:把 .bak 内容 copy 回主库,统一用 dbPath
148
- copyFileSync(bakPath, this.dbPath);
147
+ // node:sqlite 绑定路径,无 _save:把 .bak 内容 copy 回主库,统一用 dbPath。
148
+ // 体检 BUG-18:主库可能正被其他进程(daemon/另一 CLI)打开——Windows 覆盖他人
149
+ // 打开的文件抛 EPERM/EBUSY,短暂退避重试(持有者通常是瞬时句柄),仍失败 fail-loud
150
+ let copied = false;
151
+ let lastErr = null;
152
+ for (let attempt = 0; attempt < 5 && !copied; attempt++) {
153
+ try {
154
+ copyFileSync(bakPath, this.dbPath);
155
+ copied = true;
156
+ } catch (e) {
157
+ if (!['EPERM', 'EBUSY', 'EACCES'].includes(e.code)) throw e;
158
+ lastErr = e;
159
+ _sleepSync(50 * (attempt + 1));
160
+ }
161
+ }
162
+ if (!copied) throw lastErr;
149
163
  return openDatabase(this.dbPath);
150
164
  }
151
165
  }
@@ -165,8 +179,14 @@ export class DB {
165
179
  */
166
180
  close() {
167
181
  if (this.db) {
168
- this.db.close();
169
- this.db = null;
182
+ try {
183
+ this.db.close();
184
+ } catch (err) {
185
+ // close 自身失败(如 WAL checkpoint 磁盘满):仍置 null 防后续操作已损坏句柄,warn 留痕不吞
186
+ console.warn(`[db] close 失败(句柄已废弃): ${err && err.message ? err.message : err}`);
187
+ } finally {
188
+ this.db = null;
189
+ }
170
190
  }
171
191
  }
172
192