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.
- package/.claude/skills/sillyspec-execute/SKILL.md +1 -1
- package/.claude/skills/sillyspec-quick/SKILL.md +3 -1
- package/.sillyspec-platform-cleaned +1 -0
- package/.sillyspec-platform.json +7 -0
- package/README.md +3 -5
- package/package.json +2 -3
- package/src/db-engine.js +55 -0
- package/src/db.js +53 -35
- package/src/dispatch/probe.js +11 -9
- package/src/doctor-diagnostics.js +6 -6
- package/src/hooks/worktree-guard.js +18 -26
- package/src/index.js +151 -3
- package/src/local-detect.js +120 -110
- package/src/progress/change-registry.js +43 -2
- package/src/progress/shared.js +2 -2
- package/src/progress/stage-machine.js +6 -0
- package/src/progress/step-store.js +10 -0
- package/src/progress.js +302 -10
- package/src/quicklog.js +48 -9
- package/src/run/command.js +13 -2
- package/src/run/complete-handlers.js +37 -8
- package/src/run/complete.js +17 -4
- package/src/run/gates.js +13 -2
- package/src/run/shared.js +74 -0
- package/src/run/stage.js +6 -1
- package/src/sillyhub-mcp/client.js +12 -7
- package/src/sillyhub-mcp/config.js +62 -0
- package/src/stage-review.js +105 -1
- package/src/stages/doctor.js +1 -1
- package/src/stages/execute.js +6 -5
- package/src/stages/quick.js +24 -8
- package/src/stages/scan.js +44 -5
- package/src/stages/verify.js +2 -2
- package/src/sync.js +430 -13
- package/src/task-review.js +29 -0
- package/src/verify-postcheck.js +7 -2
- package/src/worktree-apply.js +135 -27
|
@@ -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
|
|
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
|
|
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
|
package/README.md
CHANGED
|
@@ -59,12 +59,10 @@ npx sillyspec init --interactive # 完整引导
|
|
|
59
59
|
|
|
60
60
|
## 平台支持
|
|
61
61
|
|
|
62
|
-
SillySpec 进度库使用 SQLite 持久化(基于
|
|
62
|
+
SillySpec 进度库使用 SQLite 持久化(基于 Node.js 内置的 `node:sqlite` 模块,纯 JS 无原生编译依赖)。
|
|
63
63
|
|
|
64
|
-
-
|
|
65
|
-
-
|
|
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.
|
|
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": ">=
|
|
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",
|
package/src/db-engine.js
ADDED
|
@@ -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
|
|
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
|
-
//
|
|
8
|
+
// node:sqlite(DatabaseSync)是原生 SQLite 引擎,打开即持久化(不像 sql.js 纯内存需整库 export 落盘),
|
|
9
9
|
// _createSchema 内 DDL 直接落盘,无需额外 _save。
|
|
10
|
-
const DB_SCHEMA_VERSION =
|
|
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(
|
|
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
|
-
* 同步初始化(
|
|
45
|
-
* -
|
|
46
|
-
* - PRAGMA:journal_mode=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
|
-
* -
|
|
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(
|
|
64
|
-
this.db
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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 开销。
|
|
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,但
|
|
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
|
-
*
|
|
94
|
-
* .bak 作为恢复源只要存在且能打开即用(含 0 字节空库——
|
|
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:
|
|
106
|
+
// tryOpen: openDatabase(path) + prepare 探测双保险(防打开成功但内容非 SQLite)
|
|
105
107
|
const tryOpen = (p) => {
|
|
106
108
|
let db;
|
|
107
|
-
try { db =
|
|
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 对不存在的路径触发
|
|
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
|
-
//
|
|
134
|
+
// node:sqlite 绑定路径,无 _save:把 .bak 内容 copy 回主库,统一用 dbPath
|
|
133
135
|
copyFileSync(bakPath, this.dbPath);
|
|
134
|
-
return
|
|
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
|
|
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
|
-
* 关闭数据库。
|
|
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
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
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
|
|
178
|
+
return runTransaction(this.db, fn);
|
|
177
179
|
} catch (err) {
|
|
178
|
-
// 仅 SQLITE_BUSY 重试(
|
|
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 对象(
|
|
191
|
+
/** 获取底层 db 对象(node:sqlite DatabaseSync 实例,供 progress.js 直接使用) */
|
|
190
192
|
getDb() {
|
|
191
193
|
return this.db;
|
|
192
194
|
}
|
|
193
195
|
|
|
194
196
|
_createSchema() {
|
|
195
|
-
//
|
|
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
|
|
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
|
-
*
|
|
327
|
+
* node:sqlite:exec() 执行 ALTER TABLE DDL;列已存在抛错被 catch 静默跳过。
|
|
310
328
|
* @private
|
|
311
329
|
*/
|
|
312
330
|
_migrateAddColumn(table, column, type) {
|
package/src/dispatch/probe.js
CHANGED
|
@@ -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 身份;
|
|
61
|
-
* token 轮换后最迟 TTL 内自动失效重探。
|
|
61
|
+
* 配置指纹(负面缓存 key)。用 local.yaml mcp 段或 env 的 URL 标识 daemon 身份;
|
|
62
|
+
* token 不入 key(避免敏感信息驻留缓存,保密语义不变)。token 轮换后最迟 TTL 内自动失效重探。
|
|
62
63
|
*/
|
|
63
|
-
function configFingerprint() {
|
|
64
|
-
return
|
|
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()
|
|
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.
|
|
145
|
-
|
|
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 以
|
|
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
|
|
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 =
|
|
71
|
+
db = openDatabase(dbPath, { readOnly: true });
|
|
72
72
|
const pick = (sql, fallback = null) => {
|
|
73
73
|
try {
|
|
74
|
-
const r = db
|
|
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
|
|
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 =
|
|
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
|
-
* 用子进程跑
|
|
229
|
-
* -
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
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:
|
|
240
|
-
*
|
|
241
|
-
|
|
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
|
-
|
|
252
|
-
|
|
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
|
|
252
|
+
"const {DatabaseSync}=require('node:sqlite')," +
|
|
262
253
|
"dbPath=" + JSON.stringify(dbPath) + ",sql=" + JSON.stringify(sql) + ";" +
|
|
263
254
|
"let db;" +
|
|
264
|
-
"try{db=new
|
|
265
|
-
"const
|
|
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 损坏 /
|
|
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 +
|
|
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 +
|
|
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
|
|