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.
- package/.claude/skills/sillyspec-archive/SKILL.md +1 -1
- package/.claude/skills/sillyspec-execute/SKILL.md +1 -1
- package/.claude/skills/sillyspec-knowledge/SKILL.md +1 -1
- package/.claude/skills/sillyspec-plan/SKILL.md +5 -5
- package/.claude/skills/sillyspec-quick/SKILL.md +5 -1
- package/README.md +7 -5
- package/SKILL.md +5 -4
- package/package.json +1 -1
- package/packages/dashboard/server/executor.js +10 -1
- package/packages/dashboard/server/index.js +108 -46
- package/src/config-schema.js +3 -0
- package/src/contract-matrix.js +22 -13
- package/src/db.js +24 -4
- package/src/dispatch/probe.js +99 -63
- package/src/docs-check.js +58 -13
- package/src/docs-debt.js +6 -2
- package/src/docs-gate.js +1 -0
- package/src/doctor-diagnostics.js +208 -21
- package/src/fs-atomic.js +3 -1
- package/src/git-helper.js +7 -4
- package/src/hooks/worktree-guard.js +8 -2
- package/src/index.js +161 -12
- package/src/init.js +3 -2
- package/src/local-detect.js +9 -1
- package/src/modules.js +7 -5
- package/src/progress/stage-machine.js +589 -578
- package/src/progress/step-store.js +1 -1
- package/src/progress.js +184 -30
- package/src/quicklog.js +47 -16
- package/src/run/command.js +31 -8
- package/src/run/complete-handlers.js +21 -14
- package/src/run/complete.js +1181 -1121
- package/src/run/gates.js +148 -25
- package/src/run/prompt.js +9 -5
- package/src/run/shared.js +138 -58
- package/src/run/stage.js +41 -1
- package/src/setup.js +8 -10
- package/src/sillyhub-mcp/client.js +74 -6
- package/src/spec-dir-typo.js +3 -24
- package/src/spec-sync.js +43 -8
- package/src/stage-contract.js +12 -38
- package/src/stages/archive.js +4 -3
- package/src/stages/brainstorm-auto.js +1 -1
- package/src/stages/brainstorm.js +2 -1
- package/src/stages/doctor.js +28 -13
- package/src/stages/execute.js +164 -153
- package/src/stages/plan-postcheck.js +49 -28
- package/src/stages/plan.js +57 -40
- package/src/stages/quick.js +3 -1
- package/src/stages/verify.js +1 -1
- package/src/sync.js +323 -221
- package/src/task-review.js +246 -61
- package/src/taskcard.js +178 -0
- package/src/verify-postcheck.js +48 -35
- package/src/worktree-apply.js +58 -34
- package/src/worktree-deps.js +120 -20
- package/src/worktree.js +55 -9
- package/.npmrc-publish +0 -2
|
@@ -64,7 +64,7 @@ sillyspec run archive --continue --answer "确认写入"
|
|
|
64
64
|
|
|
65
65
|
### 归档结果
|
|
66
66
|
|
|
67
|
-
归档后变更目录从 `changes/<名>/` 移到 `changes/archive
|
|
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
|
-
当 `
|
|
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": "
|
|
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 分组 +
|
|
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 分组 +
|
|
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 从 `
|
|
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
|
|
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
|
|
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
|
-
-
|
|
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 粒度强制流转,
|
|
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(
|
|
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、
|
|
84
|
+
- **MCP 增强** — 一键安装 Context7、Chrome DevTools、PinchTab
|
|
84
85
|
|
|
85
86
|
## MCP 工具
|
|
86
87
|
|
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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:
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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 (!
|
|
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
|
|
package/src/config-schema.js
CHANGED
|
@@ -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
|
}
|
package/src/contract-matrix.js
CHANGED
|
@@ -46,18 +46,24 @@ export function classifyTask(taskContent) {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
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:
|
|
59
|
-
//
|
|
60
|
-
const
|
|
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
|
-
*
|
|
104
|
-
*
|
|
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(
|
|
115
|
+
function parseTaskDependencies(registryContent) {
|
|
108
116
|
const deps = {}
|
|
109
117
|
|
|
110
|
-
// 方式 1:
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
169
|
-
|
|
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
|
|