sillyspec 3.25.11 → 3.26.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -114,7 +114,7 @@ execute 还有**第二道**独立的 stage 级审查:除逐 task review.json
114
114
 
115
115
  ## 派发模式(SillyHub MCP,可选)
116
116
 
117
- `execute` Wave 内的子代理默认用本机 Agent tool 执行。若消费方配置了 SillyHub MCP(`SILLYHUB_MCP_URL` / `SILLYHUB_MCP_TOKEN`),Wave 步骤 prompt 运行时可能注入一段 SillyHub 派发指令——**出现就照其中的指令执行**(创建 mission / dispatch_worker / 轮询结果 / 本机兜底),**没出现就用本机 Agent tool**。未配置 MCP 时完全不注入,行为与无此机制一致。
117
+ `execute` Wave 内的子代理默认用本机 Agent tool 执行。若消费方配置了 SillyHub MCP(`local.yaml` 的 `mcp` 段写 `mcp.url` / `mcp.token`——可由 `sillyspec platform connect` 同源写入或手填;或环境变量 `SILLYHUB_MCP_URL` / `SILLYHUB_MCP_TOKEN` 作回退),Wave 步骤 prompt 运行时可能注入一段 SillyHub 派发指令——**出现就照其中的指令执行**(创建 mission / dispatch_worker / 轮询结果 / 本机兜底),**没出现就用本机 Agent tool**。未配置 MCP 时完全不注入,行为与无此机制一致。
118
118
 
119
119
  可选:`sillyspec dispatch probe` 查看 SillyHub 是否可用。
120
120
 
@@ -49,6 +49,7 @@ sillyspec run quick --reopen --from-step N # 重新打开已完成阶段修
49
49
  |---|---|
50
50
  | `--linked-changes none\|a,b` | **显式关联变更(取代 `--change`,推荐)**。none=不关联,a,b=关联列表 |
51
51
  | `--files a.js,b.js` | 显式声明本次允许修改的文件(边界保护) |
52
+ | `--file-notes "p::注 \|\| p::注"` | quick `--done` 用:QUICKLOG「文件:」行落盘为多行带括注 bullet(省事后手改文件行)。格式 `path::括注`,`\|\|` 分隔多条 |
52
53
  | `--allow-new` | 允许新增文件(默认禁止,防意外创建) |
53
54
  | `--force-baseline` | 允许覆盖 baseline 受保护文件 / 压制 `.sillyspec/` 危险判定(危险,慎用) |
54
55
  | `--confirm` | ⚠️ 仅打印变更概览,**不解锁 blocked**(blocked 仍 exit 1)。真正解锁用 `--force-baseline`/`--allow-new` |
@@ -88,7 +89,8 @@ sillyspec run quick --files src/phone.ts,src/phone.test.ts
88
89
  - quick 直接在主工作区改代码(不创建 worktree),范围必须小且明确
89
90
  - 完成后立即 `--done`,不跳过
90
91
  - QUICKLOG 记录的**骨架由 CLI 接管**:启动时 CLI 自动分配 ql-ID 并在 `.sillyspec/quicklog/QUICKLOG-<user>.md` 写「进行中」条目(含关联变更 tasks.md),`--done` 时 CLI 自动翻「已完成」+ 勾选 task + 回填文件路径。ql-ID 分配/状态/task 你无需手写,只需用注入的 `<quicklog-id>` 在模块文档变更索引引用
91
- - **QUICKLOG 正文精修(`--done` 后必做,不可跳过)**:CLI 落盘的是简版骨架(标题可能是 `(quick 任务)`、文件是单行逗号无括注),**不合格**。`--done` 成功后必须立即编辑本次 `<quicklog-id>` 条目,手动精修成丰富格式:① 标题改真实需求摘要(禁 `(quick 任务)`);② 文件改多行 bullet、每行带关键改动括注(禁 `(见实际改动)` 或无括注纯路径);③ 正文 `需求:`/`根因:`/`方案:`/`结果:` 四段逐项充实(禁只留一段「结果:」)。参照同文件早期丰富条目。一条 quick = 一条独立 ql,不追加到旧条目
92
+ - **QUICKLOG 落盘已结构化(`--done` 后按需核对,多数无需手改)**:CLI 已落盘结构化条目——标题从 `--output` 的「需求:」自动提取、正文四字段自动分行、文件行用 `--file-notes` 时为多行带括注。`--done` 后只需核对:标题弱才改(禁留 `(quick 任务)` 占位);没用 `--file-notes` 时文件行是单行、可事后补括注(参照同文件早期丰富条目);正文 `需求:`/`根因:`/`方案:`/`结果:` 四段按需充实(禁只留一段「结果:」)。一条 quick = 一条独立 ql,不追加到旧条目
93
+ - **收尾顺序(模块文档在 `--done` 前,QUICKLOG 在 `--done` 后,别记混)**:① 命中模块→改模块文档→`git add`;② `sillyspec run quick --done --change <id> --output "四字段" [--file-notes "..."]`(CLI 自动翻完成 + 勾 task + 落盘 QUICKLOG 标题/文件/正文);③ 核对 QUICKLOG 标题(CLI 已从「需求:」提取,仅弱标题才改)→若改了再 `git add`
92
94
  - **最后一步 `--done --output` 必须按结构化结果模板给全四字段**(逐项一句话):`需求:… 根因:… 方案:… 结果:…`。这是 QUICKLOG「结果:」归档的唯一来源;CLI 校验缺字段会拒绝 `--done`(exit 1),补全后重跑即可。前两个 step 的 `--output` 是中间摘要,不用此模板
93
95
  - **禁止**在没有运行 CLI 的情况下自行决定流程
94
96
 
@@ -0,0 +1 @@
1
+ 2026-08-10T17:08:53.237Z
@@ -0,0 +1,7 @@
1
+ {
2
+ "specRoot": "C:\\Users\\qinyi\\IdeaProjects\\sillyspec\\.sillyspec",
3
+ "runtimeRoot": null,
4
+ "workspaceId": null,
5
+ "scanRunId": null,
6
+ "savedAt": "2026-08-11T04:01:27.160Z"
7
+ }
package/README.md CHANGED
@@ -59,12 +59,10 @@ npx sillyspec init --interactive # 完整引导
59
59
 
60
60
  ## 平台支持
61
61
 
62
- SillySpec 进度库使用 SQLite 持久化(基于 better-sqlite3 原生绑定)。安装支持情况如下:
62
+ SillySpec 进度库使用 SQLite 持久化(基于 Node.js 内置的 `node:sqlite` 模块,纯 JS 无原生编译依赖)。
63
63
 
64
- - **主流平台(prebuilt 零编译安装)**:Node.js >= 18 的 Linux x64、macOS x64 / macOS arm64、Windows x64。安装时直接下载预编译二进制,无需本地 C++ 工具链。
65
- - **边缘平台不保证**:Alpine(musl libc)、Windows arm64、BSD 等平台可能没有对应的 prebuilt 二进制,安装时可能触发 node-gyp 从源码编译(此时需要本机具备 C++ 编译工具链)。
66
-
67
- 如遇边缘平台编译失败,建议在受支持的平台或容器(如 Debian/Ubuntu 基镜像)中运行。
64
+ - **Node.js 版本要求 >= 22.11.0**:`node:sqlite` 自 v22.11.0 起免 `--experimental-sqlite` flag 直接可用。支持 Linux x64、macOS x64 / arm64、Windows x64 等所有 Node.js 官方支持平台。
65
+ - **零编译安装**:不依赖任何原生绑定 / node-gyp / C++ 工具链,npm install 即装即用,无平台编译失败风险。
68
66
 
69
67
  ## 从哪里开始
70
68
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sillyspec",
3
- "version": "3.25.11",
3
+ "version": "3.26.1",
4
4
  "description": "SillySpec CLI — 流程状态机,让 AI 严格按步骤来",
5
5
  "icon": "logo.jpg",
6
6
  "homepage": "https://sillyspec.ppdmq.top/",
@@ -13,7 +13,7 @@
13
13
  "sillyspec": "bin/sillyspec.js"
14
14
  },
15
15
  "engines": {
16
- "node": ">=18"
16
+ "node": ">=22.11.0"
17
17
  },
18
18
  "keywords": [
19
19
  "sillyspec",
@@ -29,7 +29,6 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "@inquirer/prompts": "^7.10.1",
32
- "better-sqlite3": "^11.0.0",
33
32
  "chalk": "^5.6.2",
34
33
  "chokidar": "^4.0",
35
34
  "js-yaml": "^4.2.0",
@@ -0,0 +1,55 @@
1
+ // db-engine.js — node:sqlite DatabaseSync 引擎抽象层
2
+ // 封装 node:sqlite 内置模块 + 消解 better-sqlite3 → node:sqlite 三缺口:
3
+ // pragma → exec / transaction → 手写 SAVEPOINT / pluck → Object.values 取首列
4
+ // 供 db.js 与 doctor-diagnostics 共用(方案 B 单一换引擎点,D-002)。
5
+ // node:sqlite 在 node v22.11.0+ 无需 --experimental-sqlite flag(仍发 ExperimentalWarning)。
6
+ // Author: sillyspec execute W1 task-02
7
+ // Created: 2026-08-11
8
+ import { DatabaseSync } from 'node:sqlite';
9
+
10
+ // 打开数据库。opts.readOnly(驼峰)透传 node:sqlite。
11
+ // fileMustExist 语义由调用方 existsSync 前置门实现(node:sqlite 不拒缺失文件,不凭空建库由调用方保证)。
12
+ export function openDatabase(dbPath, opts = {}) {
13
+ return new DatabaseSync(dbPath, { readOnly: !!opts.readOnly });
14
+ }
15
+
16
+ // 逐条 exec PRAGMA。entries: [['journal_mode','WAL'], ['busy_timeout','5000'], ...]
17
+ // node:sqlite 无 .pragma(),统一走 .exec('PRAGMA key = value')。
18
+ export function applyPragmas(db, entries) {
19
+ for (const [key, value] of entries) {
20
+ db.exec(`PRAGMA ${key} = ${value}`);
21
+ }
22
+ }
23
+
24
+ // 手写事务(node:sqlite 无 .transaction())。
25
+ // 统一用 SAVEPOINT/RELEASE/ROLLBACK TO —— SQLite 允许顶层 SAVEPOINT(等价事务),
26
+ // 嵌套调用自动形成 savepoint 栈(better-sqlite3 嵌套自动 SAVEPOINT 的兼容实现)。
27
+ // fn 抛错自动 ROLLBACK TO + RELEASE 且原错误上抛不吞。
28
+ // 本函数不含 BUSY 重试(由 db.js wrapper 外层包裹)。
29
+ export function runTransaction(db, fn) {
30
+ const sp = 'tx_sp_' + Math.random().toString(36).slice(2);
31
+ db.exec(`SAVEPOINT ${sp}`);
32
+ try {
33
+ const result = fn();
34
+ db.exec(`RELEASE ${sp}`);
35
+ return result;
36
+ } catch (err) {
37
+ try {
38
+ db.exec(`ROLLBACK TO ${sp}`);
39
+ db.exec(`RELEASE ${sp}`);
40
+ } catch { /* 回滚失败不掩盖原错误 */ }
41
+ throw err;
42
+ }
43
+ }
44
+
45
+ // pluck 替代:取第一行第一列,无行 undefined。
46
+ // node:sqlite .get() 返回行对象(不像 better-sqlite3 pluck 返标量),故 Object.values 取首列。
47
+ export function pluckGet(db, sql, ...params) {
48
+ const row = db.prepare(sql).get(...params);
49
+ return row === undefined ? undefined : Object.values(row)[0];
50
+ }
51
+
52
+ // pluck 替代:取所有行第一列成数组。
53
+ export function pluckAll(db, sql, ...params) {
54
+ return db.prepare(sql).all(...params).map((row) => Object.values(row)[0]);
55
+ }
package/src/db.js CHANGED
@@ -1,13 +1,13 @@
1
- import Database from 'better-sqlite3';
1
+ import { openDatabase, applyPragmas, runTransaction } from './db-engine.js';
2
2
  import { copyFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'fs';
3
3
  import { dirname } from 'path';
4
4
 
5
5
  // DB schema 版本(与 project.schema_version DEFAULT 对齐)。_createSchema 改动(加表/列/migration)时 bump,
6
6
  // 触发 .schema-version 戳失效 → 下次 init 重跑 _createSchema(W4-H:高频读路径 gate/derive/status
7
7
  // 每次 new ProgressManager 都过 init,靠版本戳跳过建表省开销)。
8
- // better-sqlite3 是原生 SQLite 引擎,打开即持久化(不像 sql.js 纯内存需整库 export 落盘),
8
+ // node:sqlite(DatabaseSync)是原生 SQLite 引擎,打开即持久化(不像 sql.js 纯内存需整库 export 落盘),
9
9
  // _createSchema 内 DDL 直接落盘,无需额外 _save。
10
- const DB_SCHEMA_VERSION = 3;
10
+ const DB_SCHEMA_VERSION = 5;
11
11
 
12
12
  // SQLITE_BUSY 应用层有限重试(R-08 / NFR-03):WAL 单写者模型,并发写第二者在
13
13
  // busy_timeout=5000(init PRAGMA)后抛 SQLITE_BUSY。busy_timeout 已在引擎层处理大部分等待;
@@ -20,7 +20,7 @@ const MAX_BUSY_RETRIES = 3;
20
20
  const BUSY_BACKOFF_MS = [50, 100, 200];
21
21
 
22
22
  /**
23
- * 同步阻塞 sleep(better-sqlite3 同步 API,BUSY 退避不能用 await)。
23
+ * 同步阻塞 sleep(node:sqlite 同步 API,BUSY 退避不能用 await)。
24
24
  * Atomics.wait 阻塞当前线程不占 CPU;SharedArrayBuffer 不可用的环境(受限 CSP / 旧 Node)
25
25
  * 退化为 busy-wait(退避时长 50-200ms 短,CPU 占用可接受)。仅在 transaction() 的 BUSY
26
26
  * 重试路径调用。
@@ -41,13 +41,13 @@ export class DB {
41
41
  }
42
42
 
43
43
  /**
44
- * 同步初始化(better-sqlite3 是同步 API,无 async/await,与 sql.js 的 initSqlJs() 异步加载不同):
45
- * - new Database(path) 同步打开/创建库文件(better-sqlite3 原生绑定,非 WASM)
46
- * - PRAGMA:journal_mode=WAL 真生效(better-sqlite3 直连原生 SQLite,WAL 落地;
44
+ * 同步初始化(node:sqlite DatabaseSync 是同步 API,无 async/await,与 sql.js 的 initSqlJs() 异步加载不同):
45
+ * - openDatabase(path) 同步打开/创建库文件(node:sqlite 原生绑定,非 WASM)
46
+ * - PRAGMA:journal_mode=WAL 真生效(node:sqlite 直连原生 SQLite,WAL 落地;
47
47
  * sql.js 是纯内存库,WAL 对它无意义),busy_timeout=5000 让写锁竞争时自动等待,
48
48
  * foreign_keys=ON 启用外键级联,synchronous=NORMAL 在 WAL 下兼顾安全与性能
49
49
  * - schema 戳检查:内容匹配 DB_SCHEMA_VERSION 则跳过 _createSchema,省建表开销
50
- * - better-sqlite3 打开即持久化,DDL/事务提交直接落盘,不需要 sql.js 时代的 _save()
50
+ * - node:sqlite 打开即持久化,DDL/事务提交直接落盘,不需要 sql.js 时代的 _save()
51
51
  *
52
52
  * .bak 损坏回退由 _openWithFallback() 承担(task-04):主库 → .bak → 全新/报错 逐级回退,
53
53
  * 语义对齐原 sql.js 的 _loadDatabase。
@@ -60,14 +60,16 @@ export class DB {
60
60
  // 2. 同步打开/创建数据库(主库 → .bak → 全新/报错 逐级回退,归 task-04)
61
61
  this.db = this._openWithFallback();
62
62
 
63
- // 3. 设置 PRAGMA(better-sqlite3 走真 SQLite,journal_mode=WAL 真生效)
64
- this.db.pragma('journal_mode = WAL');
65
- this.db.pragma('busy_timeout = 5000');
66
- this.db.pragma('foreign_keys = ON');
67
- this.db.pragma('synchronous = NORMAL');
63
+ // 3. 设置 PRAGMA(node:sqlite 走真 SQLite,journal_mode=WAL 真生效;经 applyPragmas 逐条 exec)
64
+ applyPragmas(this.db, [
65
+ ['journal_mode', 'WAL'],
66
+ ['busy_timeout', '5000'],
67
+ ['foreign_keys', 'ON'],
68
+ ['synchronous', 'NORMAL'],
69
+ ]);
68
70
 
69
71
  // 4. 创建表结构(仅当 schema 版本戳不匹配时——W4-H):
70
- // schema 已最新则跳过建表,省 DDL 开销。better-sqlite3 打开即持久化,
72
+ // schema 已最新则跳过建表,省 DDL 开销。node:sqlite 打开即持久化,
71
73
  // _createSchema 内 DDL 直接落盘,无需 _save。
72
74
  // 戳在但 db 缺表属"手动改 db"边角(SillySpec 不支持),不在本层兜底。
73
75
  const versionPath = `${this.dbPath}.schema-version`;
@@ -85,13 +87,13 @@ export class DB {
85
87
 
86
88
  /**
87
89
  * 打开数据库(主库 → .bak → 全新/报错 逐级回退)。
88
- * 语义对齐原 sql.js 的 _loadDatabase,但 better-sqlite3 绑定文件路径无 _save,
90
+ * 语义对齐原 sql.js 的 _loadDatabase,但 node:sqlite 绑定文件路径无 _save,
89
91
  * .bak 回退时 copyFileSync 把 .bak 内容恢复到主库路径,统一用 dbPath。
90
92
  * - 主库存在且有内容且可打开:直接用。
91
93
  * - 主库缺失/为空/损坏:尝试 .bak;成功则 warn 原因后 copy 回主库使用。
92
94
  * - 都不可用:主库不存在→建全新空库;主库曾存在(空/损坏)→fail-loud 抛错(不静默建空库吞进度)。
93
- * better-sqlite3 打开 0 字节文件不抛错:主库须 statSync 显式判空(截断信号→走回退,防静默吞进度);
94
- * .bak 作为恢复源只要存在且能打开即用(含 0 字节空库——better-sqlite3 视其为合法空库,
95
+ * node:sqlite 打开 0 字节文件不抛错:主库须 statSync 显式判空(截断信号→走回退,防静默吞进度);
96
+ * .bak 作为恢复源只要存在且能打开即用(含 0 字节空库——node:sqlite 视其为合法空库,
95
97
  * 生产环境 .bak 由原子写改名上一完整主库得来,永不为 0 字节,0 字节态仅出现在测试构造)。
96
98
  */
97
99
  _openWithFallback() {
@@ -101,10 +103,10 @@ export class DB {
101
103
  if (!existsSync(p)) return null;
102
104
  return statSync(p).size === 0 ? 'empty' : 'ok';
103
105
  };
104
- // tryOpen: new Database(path) + prepare 探测双保险(防打开成功但内容非 SQLite)
106
+ // tryOpen: openDatabase(path) + prepare 探测双保险(防打开成功但内容非 SQLite)
105
107
  const tryOpen = (p) => {
106
108
  let db;
107
- try { db = new Database(p); }
109
+ try { db = openDatabase(p); }
108
110
  catch { return null; }
109
111
  try { db.prepare('SELECT count(*) FROM sqlite_master').get(); return db; }
110
112
  catch { try { db.close(); } catch { /* 关闭失败忽略 */ } return null; }
@@ -117,7 +119,7 @@ export class DB {
117
119
  }
118
120
 
119
121
  // 2. .bak 回退(.bak 是恢复源:存在且能打开即用,含 0 字节空库;门禁只挡"不存在",
120
- // 避免 tryOpen 对不存在的路径触发 new Database 的副作用——会凭空创建文件)
122
+ // 避免 tryOpen 对不存在的路径触发 openDatabase 的副作用——会凭空创建文件)
121
123
  if (readValid(bakPath) !== null) {
122
124
  const bakDb = tryOpen(bakPath);
123
125
  if (bakDb) {
@@ -129,23 +131,23 @@ export class DB {
129
131
  : 'sillyspec.db 损坏';
130
132
  console.warn(`⚠️ ${reason},已从 .bak 备份恢复。`);
131
133
  bakDb.close();
132
- // better-sqlite3 绑定路径,无 _save:把 .bak 内容 copy 回主库,统一用 dbPath
134
+ // node:sqlite 绑定路径,无 _save:把 .bak 内容 copy 回主库,统一用 dbPath
133
135
  copyFileSync(bakPath, this.dbPath);
134
- return new Database(this.dbPath);
136
+ return openDatabase(this.dbPath);
135
137
  }
136
138
  }
137
139
 
138
140
  // 3. 都不可用
139
141
  if (readValid(this.dbPath) === null) {
140
142
  // 主库与备份都不存在 → 全新项目,建空库
141
- return new Database(this.dbPath);
143
+ return openDatabase(this.dbPath);
142
144
  }
143
145
  // 主库曾存在(空/损坏)但无法恢复 → 数据丢失,必须 fail-loud(不静默建空库吞进度)
144
146
  throw new Error('sillyspec.db 损坏且 .bak 备份不可用,无法恢复。请从版本控制或其他备份恢复 .sillyspec/.runtime/ 后重试。');
145
147
  }
146
148
 
147
149
  /**
148
- * 关闭数据库。better-sqlite3 的 close() 自动做 WAL checkpoint(把 -wal/-shm 日志合并回主库),
150
+ * 关闭数据库。node:sqlite 的 close() 自动做 WAL checkpoint(把 -wal/-shm 日志合并回主库),
149
151
  * 不需要 sql.js 时代的 _save() 整库 export 落盘。
150
152
  */
151
153
  close() {
@@ -158,9 +160,10 @@ export class DB {
158
160
  /**
159
161
  * 原生事务 + SQLITE_BUSY 应用层有限重试(R-08 / NFR-03)。
160
162
  *
161
- * better-sqlite3 的 db.transaction(fn) 返回一个包装函数,调用时自动
162
- * BEGIN/COMMIT/ROLLBACK(fn 抛错自动 ROLLBACK 且不吞错),嵌套调用自动用 SAVEPOINT。
163
- * 无需手动 BEGIN/COMMIT/ROLLBACK,也无需 _save(better-sqlite3 提交即持久化)。
163
+ * node:sqlite 无 .transaction(),runTransaction(fn)(db-engine)用 SAVEPOINT/RELEASE/
164
+ * ROLLBACK TO 手写事务:fn 抛错自动回滚且不吞错,嵌套调用自动形成 savepoint 栈
165
+ * (对齐旧引擎嵌套自动 SAVEPOINT 的兼容实现)。无需手动 BEGIN/COMMIT/ROLLBACK,
166
+ * 也无需 _save(node:sqlite 提交即持久化)。
164
167
  *
165
168
  * SQLITE_BUSY 重试:WAL 单写者模型,并发写第二者在 busy_timeout=5000 后抛 BUSY。此处对
166
169
  * BUSY 加 MAX_BUSY_RETRIES 次递增退避(BUSY_BACKOFF_MS);仅 BUSY 重试(其它异常直接上抛),
@@ -169,14 +172,13 @@ export class DB {
169
172
  */
170
173
  transaction(fn) {
171
174
  if (!this.db) throw new Error('DB not initialized');
172
- const tx = this.db.transaction(fn);
173
175
  let attempt = 0;
174
176
  while (true) {
175
177
  try {
176
- return tx();
178
+ return runTransaction(this.db, fn);
177
179
  } catch (err) {
178
- // 仅 SQLITE_BUSY 重试(better-sqlite3 err.code 为 'SQLITE_BUSY'/5,message 含 'database is locked')
179
- const isBusy = err && (err.code === 'SQLITE_BUSY' || err.code === 5
180
+ // 仅 SQLITE_BUSY 重试(node:sqlite err.errcode 为 5 / err.code 为 'ERR_SQLITE_ERROR',message 含 'database is locked')
181
+ const isBusy = err && (err.code === 'SQLITE_BUSY' || err.code === 5 || err.errcode === 5
180
182
  || (typeof err.message === 'string' && /database is locked|SQLITE_BUSY/i.test(err.message)));
181
183
  if (!isBusy) throw err;
182
184
  if (attempt >= MAX_BUSY_RETRIES) throw err; // 达上限 fail-loud(不静默吞,防死循环)
@@ -186,13 +188,13 @@ export class DB {
186
188
  }
187
189
  }
188
190
 
189
- /** 获取底层 db 对象(better-sqlite3 Database 实例,供 progress.js 直接使用) */
191
+ /** 获取底层 db 对象(node:sqlite DatabaseSync 实例,供 progress.js 直接使用) */
190
192
  getDb() {
191
193
  return this.db;
192
194
  }
193
195
 
194
196
  _createSchema() {
195
- // better-sqlite3:exec() 执行多条 SQL/DDL(无返回);prepare().run/get/all 用于 DML。
197
+ // node:sqlite:exec() 执行多条 SQL/DDL(无返回);prepare().run/get/all 用于 DML。
196
198
  // sql.js 的 .run(sql) 在此换为 .exec(sql)。
197
199
 
198
200
  // project 表
@@ -200,7 +202,7 @@ export class DB {
200
202
  CREATE TABLE IF NOT EXISTS project (
201
203
  id INTEGER PRIMARY KEY DEFAULT 1,
202
204
  name TEXT NOT NULL,
203
- schema_version INTEGER DEFAULT 3,
205
+ schema_version INTEGER DEFAULT 4,
204
206
  created_at TEXT NOT NULL,
205
207
  updated_at TEXT NOT NULL
206
208
  )
@@ -302,11 +304,27 @@ export class DB {
302
304
  this._migrateAddColumn('stages', 'reopened_from_step', 'TEXT');
303
305
  this._migrateAddColumn('stages', 'reopened_at', 'TEXT');
304
306
  this._migrateAddColumn('stages', 'stale_reason', 'TEXT');
307
+
308
+ // Platform sync support(2026-08-10-platform-progress-sync §8 / D-012):
309
+ // changes 表加 base_ts(last_synced_platform_ts)与本地脏度(last_local_modified_ts)两列,
310
+ // 为 serializeForSync()/import() 序列化与 base_ts 乐观锁冲突检测提供数据载体。
311
+ // NULL 语义(gap8):last_local_modified_ts NULL=本地无脏度(pull 不判冲突直接 import);
312
+ // last_synced_platform_ts NULL=首次同步(base_ts NULL 平台接受首次 push)。
313
+ this._migrateAddColumn('changes', 'last_synced_platform_ts', 'TEXT');
314
+ this._migrateAddColumn('changes', 'last_local_modified_ts', 'TEXT');
315
+
316
+ // changes 表加 title(人类可读中文标题)+ quicklog_id(关联 QUICKLOG ql-ID)两列:
317
+ // 让 quick-<hex> 这种机器 hash sessionId 命名的 change 行可读,DB↔QUICKLOG 可对账。
318
+ // quick 启动时回填(stage.js:title 从任务描述提取、quicklog_id 用分配的 qlId),
319
+ // --done 时从 step3「需求:」刷新 title(agent 可改)。完整流程 change 的 name 本身
320
+ // 已语义化,title/quicklog_id 留空。不纳入平台同步脏度(本地展示用元信息)。
321
+ this._migrateAddColumn('changes', 'title', 'TEXT');
322
+ this._migrateAddColumn('changes', 'quicklog_id', 'TEXT');
305
323
  }
306
324
 
307
325
  /**
308
326
  * 幂等地给表添加列(列已存在则跳过)。
309
- * better-sqlite3:exec() 执行 ALTER TABLE DDL;列已存在抛错被 catch 静默跳过。
327
+ * node:sqlite:exec() 执行 ALTER TABLE DDL;列已存在抛错被 catch 静默跳过。
310
328
  * @private
311
329
  */
312
330
  _migrateAddColumn(table, column, type) {
@@ -16,6 +16,7 @@ import { existsSync, readFileSync } from 'node:fs';
16
16
  import { join, relative, isAbsolute, sep } from 'node:path';
17
17
  import jsYaml from 'js-yaml';
18
18
  import { SillyHubMcpClient } from '../sillyhub-mcp/client.js';
19
+ import { readMcpConfig } from '../sillyhub-mcp/config.js';
19
20
  import { setPathAProbeResult } from './backends/sillyhub-mcp.js';
20
21
 
21
22
  /** 默认负面缓存 TTL(毫秒)。可被 local.yaml dispatch.probe_ttl_ms 或 ttlMs 参数覆盖。 */
@@ -57,11 +58,11 @@ function resolveTtl(ttlMs, cwd) {
57
58
  }
58
59
 
59
60
  /**
60
- * 配置指纹(负面缓存 key)。用 env URL 标识 daemon 身份;token 不入 key(避免敏感信息驻留缓存)。
61
- * token 轮换后最迟 TTL 内自动失效重探。
61
+ * 配置指纹(负面缓存 key)。用 local.yaml mcp 段或 env 的 URL 标识 daemon 身份;
62
+ * token 不入 key(避免敏感信息驻留缓存,保密语义不变)。token 轮换后最迟 TTL 内自动失效重探。
62
63
  */
63
- function configFingerprint() {
64
- return process.env.SILLYHUB_MCP_URL || '';
64
+ function configFingerprint(cwd) {
65
+ return readMcpConfig(cwd)?.url || '';
65
66
  }
66
67
 
67
68
  /**
@@ -133,7 +134,7 @@ async function preheatPathAProbe(cli) {
133
134
  * 5. 全通过 → available=true(不缓存正面结果)
134
135
  *
135
136
  * @param {object} [opts]
136
- * @param {object} [opts.client] - SillyHubMcpClient 实例;缺省 new SillyHubMcpClient()(读 env)
137
+ * @param {object} [opts.client] - SillyHubMcpClient 实例;缺省 new SillyHubMcpClient()(经 readMcpConfig 读 local.yaml mcp 段 + env)
137
138
  * @param {string} [opts.worktreePath] - SillySpec worktree 绝对路径(root_path 校验用)
138
139
  * @param {string} [opts.rootPath] - daemon ws.root_path;未传(undefined)时 best-effort 从 daemon
139
140
  * 拿(task-12 #5),拿不到则跳过越界校验(不判 unavailable)
@@ -141,13 +142,14 @@ async function preheatPathAProbe(cli) {
141
142
  * @returns {Promise<{ available: boolean, reason?: string }>}
142
143
  */
143
144
  export async function probeSillyHub({ client, worktreePath, rootPath, ttlMs } = {}) {
144
- // 1. 同步快速路径:env 缺任一即 unavailable(不发网络,零回归关键)
145
- if (!process.env.SILLYHUB_MCP_URL || !process.env.SILLYHUB_MCP_TOKEN) {
145
+ // 1. 同步快速路径:无 local.yaml mcp 段且 env 缺凭据即 unavailable(不发网络,零回归关键)。
146
+ // readMcpConfig 纯 fs + env 读(best-effort 不抛不发网络),返回 null = 两源都缺 → no-config。
147
+ if (!readMcpConfig(process.cwd())) {
146
148
  return { available: false, reason: 'no-config' };
147
149
  }
148
150
 
149
151
  // 2. 负面缓存命中(未过 TTL)→ 直接返回,不发网络(R-06 抖动期免反复探测)
150
- const fp = configFingerprint();
152
+ const fp = configFingerprint(process.cwd());
151
153
  const cached = negativeCache.get(fp);
152
154
  if (cached && cached.expiresAt > Date.now()) {
153
155
  return cached.result;
@@ -155,7 +157,7 @@ export async function probeSillyHub({ client, worktreePath, rootPath, ttlMs } =
155
157
 
156
158
  // 3. 连通性探测:client.probeDaemon 自身 best-effort 不抛;再兜一层 try/catch,
157
159
  // 即便 client 契约异常抛出也保守判 unavailable(铁律:绝不抛穿 execute)
158
- const cli = client || new SillyHubMcpClient();
160
+ const cli = client || new SillyHubMcpClient({ cwd: process.cwd() });
159
161
  let reachable;
160
162
  try {
161
163
  reachable = await cli.probeDaemon();
@@ -11,7 +11,7 @@
11
11
  * - safe_actions:只描述建议动作与风险等级,绝不自动执行
12
12
  *
13
13
  * 安全约束(硬性):
14
- * - 所有检测只读。DB 以 better-sqlite3 只读连接打开(readonly + fileMustExist),不调用
14
+ * - 所有检测只读。DB 以 node:sqlite 只读连接打开(readOnly + existsSync 前置门),不调用
15
15
  * export/writeFileSync,不跑建表/迁移,close 后丢弃——绝不写回原 db 文件。
16
16
  * - 不删除/移动任何文件;orphan db 仅报告,处理交给后续 --dump-db / --confirm 流程。
17
17
  * - execute-progress-plan-mismatch 维度同样只读:仅读 plan.md checkbox + 只读查 stages 表,
@@ -20,7 +20,7 @@
20
20
  * 风格对齐 scan-postcheck.js:checks 用 CHECK_SEVERITY,formatter 产出 schema_version JSON,
21
21
  * writer 落盘到 <authoritySpecDir>/.runtime/。
22
22
  */
23
- import Database from 'better-sqlite3';
23
+ import { openDatabase, pluckGet, pluckAll } from './db-engine.js';
24
24
  import { existsSync, statSync, readFileSync, readdirSync, mkdirSync, writeFileSync, unlinkSync } from 'fs';
25
25
  import { join } from 'path';
26
26
  import { CHECK_SEVERITY } from './constants.js';
@@ -68,16 +68,16 @@ function probeDb(dbPath) {
68
68
  }
69
69
  let db = null;
70
70
  try {
71
- db = new Database(dbPath, { readonly: true, fileMustExist: true });
71
+ db = openDatabase(dbPath, { readOnly: true });
72
72
  const pick = (sql, fallback = null) => {
73
73
  try {
74
- const r = db.prepare(sql).pluck().get();
74
+ const r = pluckGet(db, sql);
75
75
  return r === undefined ? fallback : r;
76
76
  } catch { return fallback; }
77
77
  };
78
78
  const pickCol = (sql) => {
79
79
  try {
80
- return db.prepare(sql).pluck().all();
80
+ return pluckAll(db, sql);
81
81
  } catch { return []; }
82
82
  };
83
83
  // 每个 active change 的 execute stage status(只读查询;stages 表的 status 列)。
@@ -657,7 +657,7 @@ export async function dumpDb({ dbPath, cwd }) {
657
657
  }
658
658
  let db = null;
659
659
  try {
660
- db = new Database(dbPath, { readonly: true, fileMustExist: true });
660
+ db = openDatabase(dbPath, { readOnly: true });
661
661
  const rows = (sql) => {
662
662
  try { return db.prepare(sql).all(); } catch { return []; }
663
663
  };
@@ -12,7 +12,6 @@
12
12
  import { existsSync, readFileSync, readdirSync } from 'fs'
13
13
  import path from 'path'
14
14
  import { execFileSync } from 'child_process'
15
- import { createRequire } from 'module'
16
15
 
17
16
  // ── 常量 ──
18
17
 
@@ -225,20 +224,19 @@ function isInsideWorktreeStorage(filePath, cwd) {
225
224
  /**
226
225
  * 同步只读查询 sillyspec.db 第一行第一列(不依赖外部 sqlite3 CLI)。
227
226
  *
228
- * 用子进程跑 better-sqlite3 同步原生绑定(execFileSync 等待):
229
- * - 原生绑定经 hook 进程的 createRequire(import.meta.url).resolve('better-sqlite3') 解析出绝对路径,
230
- * JSON.stringify 嵌入子进程脚本——子进程 cwd 是项目目录,裸 require('better-sqlite3') 找不到包
231
- * (用户项目未必直接装了它);绝对路径 Windows/Linux/macOS 均 require 直接可用,无需 file:// URL。
232
- * - readonly + fileMustExist 双 true 打开只读连接:绝不写库;WAL 并发读不阻塞主进程写,
233
- * 且原生绑定可看到已 commit 但未 checkpoint 的过渡态(修复旧 WASM 内存库裸读 .db
234
- * 看不见 WAL 的过渡态可见性窗口)。
227
+ * 用子进程跑 node:sqlite 的 DatabaseSync(execFileSync 等待):
228
+ * - node:sqlite 是 Node 内置模块,子进程 require('node:sqlite') 直接可用,无需先 resolve 出
229
+ * 绝对路径再注入(旧方案需手动解析原生绑定路径,node:sqlite 无此步骤)。
230
+ * - readOnly 打开只读连接(替代旧方案 readonly/fileMustExist 语义):绝不写库;WAL 并发读不阻塞主进程写,
231
+ * 且只读连接可看到已 commit 但未 checkpoint 的过渡态(修复旧 WASM 内存库裸读 .db
232
+ * 看不见 WAL 的过渡态可见性窗口)。db 缺失/损坏由 open 异常自然触发 fail-closed(外层 catch)。
235
233
  *
236
234
  * 环境假设(R-09):WAL 模式下只读连接仍需在 .runtime/ 目录建/更新 -shm 索引文件,故 .runtime
237
235
  * 必须可写(与主进程一致)——“只读”指不改 DB 数据,不等于不对 -shm 加索引。
238
236
  *
239
- * fail-closed:better-sqlite3 resolve 失败 / db 文件不存在 / fileMustExist 打开失败 / 查询异常 /
240
- * 子进程超时或崩溃——一律 console.warn(含 e.stderr 详情)并返回 null,调用方
241
- *(readCurrentStage/isNoWorktreeMode)对 null 走 fail-closed,禁止 fail-open。
237
+ * fail-closed:db 文件不存在 / 子进程打开、查询异常 / 子进程超时或崩溃——一律 console.warn
238
+ * (含 e.stderr 详情)并返回 null,调用方(readCurrentStage/isNoWorktreeMode)对 null 走
239
+ * fail-closed,禁止 fail-open。
242
240
  */
243
241
  function queryDbFirstCell(cwd, sql) {
244
242
  const dbPath = path.join(cwd, '.sillyspec', '.runtime', 'sillyspec.db')
@@ -248,21 +246,15 @@ function queryDbFirstCell(cwd, sql) {
248
246
  console.warn('⚠️ sillyspec.db 不存在,hook 降级 fail-closed: ' + dbPath)
249
247
  return null
250
248
  }
251
- let libPath
252
- try {
253
- libPath = createRequire(import.meta.url).resolve('better-sqlite3')
254
- } catch (e) {
255
- console.warn('⚠️ sillyspec.db 查询失败(better-sqlite3 未解析),hook 降级 fail-closed: ' + ((e && e.message) || e))
256
- return null
257
- }
258
- // 子进程同步 require 绝对路径(better-sqlite3 是同步原生 API,无 async/import().then 包装)。
259
- // pluck().get() 取第一行第一列原值,无行返回 undefined(写空 stdout → 父侧 trim 后 null)。
249
+ // 子进程同步内置 require('node:sqlite')(DatabaseSync 是同步 API,无 async/import().then 包装)。
250
+ // prepare(sql).get() 取第一行整行后取首列值,无行返回 undefined(写空 stdout → 父侧 trim 后 null)。
260
251
  const script =
261
- "const D=require(" + JSON.stringify(libPath) + ")," +
252
+ "const {DatabaseSync}=require('node:sqlite')," +
262
253
  "dbPath=" + JSON.stringify(dbPath) + ",sql=" + JSON.stringify(sql) + ";" +
263
254
  "let db;" +
264
- "try{db=new D(dbPath,{readonly:true,fileMustExist:true});" +
265
- "const v=db.prepare(sql).pluck().get();" +
255
+ "try{db=new DatabaseSync(dbPath,{readOnly:true});" +
256
+ "const row=db.prepare(sql).get();" +
257
+ "const v=row===undefined?undefined:Object.values(row)[0];" +
266
258
  "if(v!==undefined)process.stdout.write(String(v??''));db.close()}" +
267
259
  "catch(e){process.stderr.write('hook db query failed: '+String(e&&e.message||e));process.exit(1)};"
268
260
  try {
@@ -270,7 +262,7 @@ function queryDbFirstCell(cwd, sql) {
270
262
  encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'pipe'],
271
263
  }).trim() || null
272
264
  } catch (e) {
273
- // DB 查询失败(db 损坏 / fileMustExist 打开失败 / 子进程超时或崩溃):降级返回 null,调用方
265
+ // DB 查询失败(db 损坏 / readOnly 打开失败 / 子进程超时或崩溃):降级返回 null,调用方
274
266
  //(readCurrentStage 等)对 null 走 fail-closed。补 warn 让 doctor/agent 能看到根因,
275
267
  // 而非完全静默(子进程把诊断写到了 stderr,execFileSync 抛错时挂在 e.stderr 上)。
276
268
  const detail = (e && e.stderr ? String(e.stderr).trim() : '') || (e && e.message) || 'timeout/crash'
@@ -291,7 +283,7 @@ export {
291
283
  * @returns {string|null} 阶段名,null 表示无法确定
292
284
  */
293
285
  function readCurrentStage(cwd) {
294
- // 直读 sillyspec.db(node + better-sqlite3 只读子进程,不依赖外部 sqlite3 CLI——Windows 默认没有)
286
+ // 直读 sillyspec.db(node + node:sqlite 只读子进程,不依赖外部 sqlite3 CLI——Windows 默认没有)
295
287
  const v = queryDbFirstCell(cwd, "SELECT current_stage FROM changes WHERE status='active' AND current_stage IN ('execute','quick') ORDER BY last_active DESC LIMIT 1")
296
288
  return v || null
297
289
  }
@@ -302,7 +294,7 @@ function readCurrentStage(cwd) {
302
294
  * @returns {boolean}
303
295
  */
304
296
  function isNoWorktreeMode(cwd) {
305
- // 直读 sillyspec.db(node + better-sqlite3,同 readCurrentStage)
297
+ // 直读 sillyspec.db(node + node:sqlite,同 readCurrentStage)
306
298
  return queryDbFirstCell(cwd, "SELECT no_worktree FROM changes WHERE status='active' AND current_stage IN ('execute','quick') LIMIT 1") === '1'
307
299
  }
308
300