sillyspec 3.25.9 → 3.25.11
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-auto/SKILL.md +8 -13
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +8 -6
- package/.claude/skills/sillyspec-continue/SKILL.md +3 -3
- package/.claude/skills/sillyspec-doctor/SKILL.md +1 -1
- package/.claude/skills/sillyspec-execute/SKILL.md +10 -4
- package/.claude/skills/sillyspec-plan/SKILL.md +1 -1
- package/.claude/skills/sillyspec-verify/SKILL.md +1 -1
- package/README.md +9 -0
- package/package.json +2 -2
- package/src/change-list.js +11 -8
- package/src/change-risk-profile.js +22 -0
- package/src/db.js +160 -159
- package/src/dispatch/backends/local-agent.js +152 -0
- package/src/dispatch/backends/sillyhub-mcp.js +242 -0
- package/src/dispatch/probe.js +208 -0
- package/src/dispatch/strategy.js +121 -0
- package/src/doctor-diagnostics.js +19 -25
- package/src/fs-atomic.js +69 -69
- package/src/git-helper.js +66 -0
- package/src/hooks/worktree-guard.js +64 -55
- package/src/index.js +176 -34
- package/src/init.js +7 -14
- package/src/machine-interface.js +54 -19
- package/src/migrate.js +14 -4
- package/src/modules.js +16 -5
- package/src/progress/change-registry.js +72 -70
- package/src/progress/consistency-doctor.js +71 -14
- package/src/progress/shared.js +6 -3
- package/src/progress/stage-machine.js +61 -65
- package/src/progress/step-store.js +62 -67
- package/src/progress.js +155 -233
- package/src/quicklog.js +13 -5
- package/src/run/command.js +65 -37
- package/src/run/complete-handlers.js +1005 -889
- package/src/run/complete.js +115 -208
- package/src/run/concurrent-detect.js +132 -0
- package/src/run/gates.js +289 -17
- package/src/run/prompt.js +22 -68
- package/src/run/quick-audit.js +10 -9
- package/src/run/shared.js +648 -600
- package/src/run/stage.js +43 -24
- package/src/scan-postcheck.js +9 -31
- package/src/sillyhub-mcp/client.js +389 -0
- package/src/spec-dir-typo.js +71 -0
- package/src/stage-contract-engine.js +22 -1
- package/src/stage-contract-spec.js +11 -4
- package/src/stage-contract.js +26 -3
- package/src/stage-review.js +13 -9
- package/src/stages/archive.js +4 -6
- package/src/stages/brainstorm-auto.js +26 -20
- package/src/stages/brainstorm.js +29 -5
- package/src/stages/cmd-existence.js +145 -0
- package/src/stages/doctor.js +9 -6
- package/src/stages/execute.js +61 -20
- package/src/stages/plan-postcheck.js +152 -0
- package/src/stages/plan.js +16 -31
- package/src/stages/quick.js +1 -1
- package/src/stages/scan.js +9 -3
- package/src/stages/status.js +0 -1
- package/src/stages/verify.js +12 -7
- package/src/sync.js +4 -4
- package/src/task-review.js +30 -1
- package/src/verify-postcheck.js +933 -862
- package/src/version.js +19 -0
- package/src/worktree-apply.js +85 -68
- package/src/worktree-deps.js +89 -5
- package/src/worktree.js +213 -111
- package/templates/prompts/taskcard-rules.md +27 -0
- package/templates/prompts/verify-probes.md +3 -3
- package/.claude/skills/sillyspec-propose/SKILL.md +0 -87
- package/src/stages/propose.js +0 -181
package/src/db.js
CHANGED
|
@@ -1,38 +1,75 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync,
|
|
3
|
-
import {
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'fs';
|
|
3
|
+
import { dirname } from 'path';
|
|
4
4
|
|
|
5
5
|
// DB schema 版本(与 project.schema_version DEFAULT 对齐)。_createSchema 改动(加表/列/migration)时 bump,
|
|
6
|
-
// 触发 .schema-version 戳失效 → 下次 init 重跑 _createSchema
|
|
7
|
-
// 每次 new ProgressManager 都过 init
|
|
6
|
+
// 触发 .schema-version 戳失效 → 下次 init 重跑 _createSchema(W4-H:高频读路径 gate/derive/status
|
|
7
|
+
// 每次 new ProgressManager 都过 init,靠版本戳跳过建表省开销)。
|
|
8
|
+
// better-sqlite3 是原生 SQLite 引擎,打开即持久化(不像 sql.js 纯内存需整库 export 落盘),
|
|
9
|
+
// _createSchema 内 DDL 直接落盘,无需额外 _save。
|
|
8
10
|
const DB_SCHEMA_VERSION = 3;
|
|
9
11
|
|
|
12
|
+
// SQLITE_BUSY 应用层有限重试(R-08 / NFR-03):WAL 单写者模型,并发写第二者在
|
|
13
|
+
// busy_timeout=5000(init PRAGMA)后抛 SQLITE_BUSY。busy_timeout 已在引擎层处理大部分等待;
|
|
14
|
+
// 此处常量给 transaction() 做超时后的应用层兜底重试。
|
|
15
|
+
// 仅 SQLITE_BUSY 重试(其它异常直接上抛);MAX_BUSY_RETRIES 有限次防死循环,达上限 fail-loud
|
|
16
|
+
// 不静默吞错。BUSY_BACKOFF_MS 递增退避(50→100→200ms),总等待上限 350ms。
|
|
17
|
+
// busy_timeout=5000 保持不变(design §7):引擎层 5s 等待已覆盖大部分锁竞争,应用层 3 次退避
|
|
18
|
+
// 足够消化极端并发超时尖刺,无需抬高 busy_timeout(过长会让单次写阻塞拖慢整体吞吐)。
|
|
19
|
+
const MAX_BUSY_RETRIES = 3;
|
|
20
|
+
const BUSY_BACKOFF_MS = [50, 100, 200];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 同步阻塞 sleep(better-sqlite3 同步 API,BUSY 退避不能用 await)。
|
|
24
|
+
* Atomics.wait 阻塞当前线程不占 CPU;SharedArrayBuffer 不可用的环境(受限 CSP / 旧 Node)
|
|
25
|
+
* 退化为 busy-wait(退避时长 50-200ms 短,CPU 占用可接受)。仅在 transaction() 的 BUSY
|
|
26
|
+
* 重试路径调用。
|
|
27
|
+
*/
|
|
28
|
+
function _sleepSync(ms) {
|
|
29
|
+
try {
|
|
30
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
31
|
+
} catch {
|
|
32
|
+
const end = Date.now() + ms;
|
|
33
|
+
while (Date.now() < end) { /* busy-wait fallback */ }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
10
37
|
export class DB {
|
|
11
38
|
constructor(dbPath) {
|
|
12
39
|
this.dbPath = dbPath;
|
|
13
40
|
this.db = null;
|
|
14
41
|
}
|
|
15
42
|
|
|
16
|
-
|
|
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 落地;
|
|
47
|
+
* sql.js 是纯内存库,WAL 对它无意义),busy_timeout=5000 让写锁竞争时自动等待,
|
|
48
|
+
* foreign_keys=ON 启用外键级联,synchronous=NORMAL 在 WAL 下兼顾安全与性能
|
|
49
|
+
* - schema 戳检查:内容匹配 DB_SCHEMA_VERSION 则跳过 _createSchema,省建表开销
|
|
50
|
+
* - better-sqlite3 打开即持久化,DDL/事务提交直接落盘,不需要 sql.js 时代的 _save()
|
|
51
|
+
*
|
|
52
|
+
* .bak 损坏回退由 _openWithFallback() 承担(task-04):主库 → .bak → 全新/报错 逐级回退,
|
|
53
|
+
* 语义对齐原 sql.js 的 _loadDatabase。
|
|
54
|
+
*/
|
|
55
|
+
init() {
|
|
17
56
|
// 1. 确保父目录存在
|
|
18
57
|
const dir = dirname(this.dbPath);
|
|
19
58
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
20
59
|
|
|
21
|
-
// 2.
|
|
22
|
-
|
|
60
|
+
// 2. 同步打开/创建数据库(主库 → .bak → 全新/报错 逐级回退,归 task-04)
|
|
61
|
+
this.db = this._openWithFallback();
|
|
23
62
|
|
|
24
|
-
// 3.
|
|
25
|
-
this.db =
|
|
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');
|
|
26
68
|
|
|
27
|
-
// 4.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
this.db.run('PRAGMA synchronous = NORMAL');
|
|
32
|
-
|
|
33
|
-
// 5. 创建表结构(仅当 schema 版本戳不匹配时——W4-H):
|
|
34
|
-
// schema 已最新则只 load 不写,省掉 _createSchema + _save 的整库 export + 三段原子写。
|
|
35
|
-
// 戳在但 db 缺表属"手动改 db"边角(SillySpec 不支持),_loadDatabase 的损坏检测兜底正常路径。
|
|
69
|
+
// 4. 创建表结构(仅当 schema 版本戳不匹配时——W4-H):
|
|
70
|
+
// schema 已最新则跳过建表,省 DDL 开销。better-sqlite3 打开即持久化,
|
|
71
|
+
// _createSchema 内 DDL 直接落盘,无需 _save。
|
|
72
|
+
// 戳在但 db 缺表属"手动改 db"边角(SillySpec 不支持),不在本层兜底。
|
|
36
73
|
const versionPath = `${this.dbPath}.schema-version`;
|
|
37
74
|
let schemaCurrent = false;
|
|
38
75
|
try {
|
|
@@ -42,161 +79,124 @@ export class DB {
|
|
|
42
79
|
|
|
43
80
|
if (!schemaCurrent) {
|
|
44
81
|
this._createSchema();
|
|
45
|
-
this._save();
|
|
46
82
|
try { writeFileSync(versionPath, String(DB_SCHEMA_VERSION)); } catch { /* 戳写入失败不阻断,下次重跑 */ }
|
|
47
83
|
}
|
|
48
84
|
}
|
|
49
85
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
86
|
+
/**
|
|
87
|
+
* 打开数据库(主库 → .bak → 全新/报错 逐级回退)。
|
|
88
|
+
* 语义对齐原 sql.js 的 _loadDatabase,但 better-sqlite3 绑定文件路径无 _save,
|
|
89
|
+
* .bak 回退时 copyFileSync 把 .bak 内容恢复到主库路径,统一用 dbPath。
|
|
90
|
+
* - 主库存在且有内容且可打开:直接用。
|
|
91
|
+
* - 主库缺失/为空/损坏:尝试 .bak;成功则 warn 原因后 copy 回主库使用。
|
|
92
|
+
* - 都不可用:主库不存在→建全新空库;主库曾存在(空/损坏)→fail-loud 抛错(不静默建空库吞进度)。
|
|
93
|
+
* better-sqlite3 打开 0 字节文件不抛错:主库须 statSync 显式判空(截断信号→走回退,防静默吞进度);
|
|
94
|
+
* .bak 作为恢复源只要存在且能打开即用(含 0 字节空库——better-sqlite3 视其为合法空库,
|
|
95
|
+
* 生产环境 .bak 由原子写改名上一完整主库得来,永不为 0 字节,0 字节态仅出现在测试构造)。
|
|
96
|
+
*/
|
|
97
|
+
_openWithFallback() {
|
|
98
|
+
const bakPath = `${this.dbPath}.bak`;
|
|
99
|
+
// readValid: null=文件不存在;'empty'=存在但 0 字节;'ok'=有内容
|
|
100
|
+
const readValid = (p) => {
|
|
101
|
+
if (!existsSync(p)) return null;
|
|
102
|
+
return statSync(p).size === 0 ? 'empty' : 'ok';
|
|
103
|
+
};
|
|
104
|
+
// tryOpen: new Database(path) + prepare 探测双保险(防打开成功但内容非 SQLite)
|
|
105
|
+
const tryOpen = (p) => {
|
|
106
|
+
let db;
|
|
107
|
+
try { db = new Database(p); }
|
|
108
|
+
catch { return null; }
|
|
109
|
+
try { db.prepare('SELECT count(*) FROM sqlite_master').get(); return db; }
|
|
110
|
+
catch { try { db.close(); } catch { /* 关闭失败忽略 */ } return null; }
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// 1. 主库(0 字节=截断信号,必须走回退,故门禁要求 'ok' 有内容)
|
|
114
|
+
if (readValid(this.dbPath) === 'ok') {
|
|
115
|
+
const primaryDb = tryOpen(this.dbPath);
|
|
116
|
+
if (primaryDb) return primaryDb;
|
|
55
117
|
}
|
|
56
|
-
}
|
|
57
118
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
119
|
+
// 2. .bak 回退(.bak 是恢复源:存在且能打开即用,含 0 字节空库;门禁只挡"不存在",
|
|
120
|
+
// 避免 tryOpen 对不存在的路径触发 new Database 的副作用——会凭空创建文件)
|
|
121
|
+
if (readValid(bakPath) !== null) {
|
|
122
|
+
const bakDb = tryOpen(bakPath);
|
|
123
|
+
if (bakDb) {
|
|
124
|
+
const primaryRaw = readValid(this.dbPath);
|
|
125
|
+
const reason = primaryRaw === 'empty'
|
|
126
|
+
? 'sillyspec.db 为空(可能被截断)'
|
|
127
|
+
: primaryRaw === null
|
|
128
|
+
? 'sillyspec.db 不存在'
|
|
129
|
+
: 'sillyspec.db 损坏';
|
|
130
|
+
console.warn(`⚠️ ${reason},已从 .bak 备份恢复。`);
|
|
131
|
+
bakDb.close();
|
|
132
|
+
// better-sqlite3 绑定路径,无 _save:把 .bak 内容 copy 回主库,统一用 dbPath
|
|
133
|
+
copyFileSync(bakPath, this.dbPath);
|
|
134
|
+
return new Database(this.dbPath);
|
|
135
|
+
}
|
|
69
136
|
}
|
|
70
|
-
// COMMIT 成功后才持久化。_save 失败时抛原生错(磁盘满/EPERM),不再被 catch 里
|
|
71
|
-
// 无效的 ROLLBACK 二级异常("no transaction active")掩盖——内存库已 commit,
|
|
72
|
-
// 下次 _save / close() 会落盘,调用方拿到的是准确的持久化错误而非被替换的误报。
|
|
73
|
-
this._save();
|
|
74
|
-
return result;
|
|
75
|
-
}
|
|
76
137
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
138
|
+
// 3. 都不可用
|
|
139
|
+
if (readValid(this.dbPath) === null) {
|
|
140
|
+
// 主库与备份都不存在 → 全新项目,建空库
|
|
141
|
+
return new Database(this.dbPath);
|
|
142
|
+
}
|
|
143
|
+
// 主库曾存在(空/损坏)但无法恢复 → 数据丢失,必须 fail-loud(不静默建空库吞进度)
|
|
144
|
+
throw new Error('sillyspec.db 损坏且 .bak 备份不可用,无法恢复。请从版本控制或其他备份恢复 .sillyspec/.runtime/ 后重试。');
|
|
80
145
|
}
|
|
81
146
|
|
|
82
147
|
/**
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
* 不会出现写入中途崩溃导致的截断态。sql.js 是纯内存库,
|
|
86
|
-
* PRAGMA journal_mode=WAL 对它无意义——真正的持久化原子性在这里保证。
|
|
148
|
+
* 关闭数据库。better-sqlite3 的 close() 自动做 WAL checkpoint(把 -wal/-shm 日志合并回主库),
|
|
149
|
+
* 不需要 sql.js 时代的 _save() 整库 export 落盘。
|
|
87
150
|
*/
|
|
88
|
-
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
// sql.js 的 export() 会重置 PRAGMA 状态,需要重新设置
|
|
94
|
-
this.db.run('PRAGMA journal_mode = WAL');
|
|
95
|
-
this.db.run('PRAGMA busy_timeout = 5000');
|
|
96
|
-
this.db.run('PRAGMA foreign_keys = ON');
|
|
97
|
-
this.db.run('PRAGMA synchronous = NORMAL');
|
|
151
|
+
close() {
|
|
152
|
+
if (this.db) {
|
|
153
|
+
this.db.close();
|
|
154
|
+
this.db = null;
|
|
155
|
+
}
|
|
98
156
|
}
|
|
99
157
|
|
|
100
158
|
/**
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
159
|
+
* 原生事务 + SQLITE_BUSY 应用层有限重试(R-08 / NFR-03)。
|
|
160
|
+
*
|
|
161
|
+
* better-sqlite3 的 db.transaction(fn) 返回一个包装函数,调用时自动
|
|
162
|
+
* BEGIN/COMMIT/ROLLBACK(fn 抛错自动 ROLLBACK 且不吞错),嵌套调用自动用 SAVEPOINT。
|
|
163
|
+
* 无需手动 BEGIN/COMMIT/ROLLBACK,也无需 _save(better-sqlite3 提交即持久化)。
|
|
164
|
+
*
|
|
165
|
+
* SQLITE_BUSY 重试:WAL 单写者模型,并发写第二者在 busy_timeout=5000 后抛 BUSY。此处对
|
|
166
|
+
* BUSY 加 MAX_BUSY_RETRIES 次递增退避(BUSY_BACKOFF_MS);仅 BUSY 重试(其它异常直接上抛),
|
|
167
|
+
* 达上限 fail-loud 抛错不静默吞(防死循环)。BUSY 重试收敛在 DB 层 transaction 封装,
|
|
168
|
+
* 所有写者(progress.js _write / initChange / setStage 等)统一受益,不散落调用点。
|
|
104
169
|
*/
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
writeFileSync(tmpPath, buffer);
|
|
111
|
-
// 2. 旧主库 → .bak(保留上一版;不存在则跳过;失败不阻断主流程)
|
|
112
|
-
if (existsSync(filePath)) {
|
|
113
|
-
try { this._renameSyncRetry(filePath, bakPath); }
|
|
114
|
-
catch { /* 备份失败最坏只是少一个 .bak,主库仍会被新内容替换 */ }
|
|
115
|
-
}
|
|
116
|
-
// 3. tmp → 主库(成功后主库即新版完整态)
|
|
117
|
-
this._renameSyncRetry(tmpPath, filePath);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/** rename 带退避重试,覆盖 Windows 偶发的 EPERM/EBUSY/EACCES/ENOTEMPTY。 */
|
|
121
|
-
_renameSyncRetry(from, to, retries = 5) {
|
|
122
|
-
let lastErr;
|
|
123
|
-
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
170
|
+
transaction(fn) {
|
|
171
|
+
if (!this.db) throw new Error('DB not initialized');
|
|
172
|
+
const tx = this.db.transaction(fn);
|
|
173
|
+
let attempt = 0;
|
|
174
|
+
while (true) {
|
|
124
175
|
try {
|
|
125
|
-
|
|
126
|
-
return;
|
|
176
|
+
return tx();
|
|
127
177
|
} catch (err) {
|
|
128
|
-
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
if (
|
|
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
|
+
|| (typeof err.message === 'string' && /database is locked|SQLITE_BUSY/i.test(err.message)));
|
|
181
|
+
if (!isBusy) throw err;
|
|
182
|
+
if (attempt >= MAX_BUSY_RETRIES) throw err; // 达上限 fail-loud(不静默吞,防死循环)
|
|
183
|
+
_sleepSync(BUSY_BACKOFF_MS[attempt]);
|
|
184
|
+
attempt++;
|
|
132
185
|
}
|
|
133
186
|
}
|
|
134
|
-
throw lastErr;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/** 同步退避:优先 Atomics.wait,不可用时退化为忙等(仅在 rename 冲突的极端情况触发)。 */
|
|
138
|
-
_sleepSync(ms) {
|
|
139
|
-
try {
|
|
140
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
141
|
-
} catch {
|
|
142
|
-
const end = Date.now() + ms;
|
|
143
|
-
while (Date.now() < end) { /* busy wait */ }
|
|
144
|
-
}
|
|
145
187
|
}
|
|
146
188
|
|
|
147
|
-
/**
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
* - 主库缺失/为空/损坏:尝试 .bak;成功则 warn 后用 .bak。
|
|
151
|
-
* - 主库与 .bak 都不可用:
|
|
152
|
-
* · 全新项目(两者都不存在):创建空库;
|
|
153
|
-
* · 主库曾存在却无法恢复(截断/损坏且无可用备份):fail-loud 抛错,
|
|
154
|
-
* 不静默建空库——那会无声吞掉用户所有进度。
|
|
155
|
-
*/
|
|
156
|
-
_loadDatabase(SQL) {
|
|
157
|
-
const bakPath = `${this.dbPath}.bak`;
|
|
158
|
-
// readValid: null=文件不存在;'empty'=存在但 0 字节;Buffer=有内容
|
|
159
|
-
const readValid = (p) => {
|
|
160
|
-
if (!existsSync(p)) return null;
|
|
161
|
-
const b = readFileSync(p);
|
|
162
|
-
return (b && b.length > 0) ? b : 'empty';
|
|
163
|
-
};
|
|
164
|
-
const tryOpen = (raw) => {
|
|
165
|
-
if (!raw || raw === 'empty') return null;
|
|
166
|
-
let instance;
|
|
167
|
-
try { instance = new SQL.Database(raw); }
|
|
168
|
-
catch { return null; }
|
|
169
|
-
// 双保险:损坏 buffer 偶尔被 sql.js 静默打开为空库,用一次查询确认它真能用
|
|
170
|
-
try { instance.exec('SELECT count(*) FROM sqlite_master'); return instance; }
|
|
171
|
-
catch { try { instance.close(); } catch {} return null; }
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
const primaryRaw = readValid(this.dbPath);
|
|
175
|
-
const primaryDb = tryOpen(primaryRaw);
|
|
176
|
-
if (primaryDb) return primaryDb;
|
|
177
|
-
|
|
178
|
-
const bakDb = tryOpen(readValid(bakPath));
|
|
179
|
-
if (bakDb) {
|
|
180
|
-
const reason = primaryRaw === 'empty'
|
|
181
|
-
? 'sillyspec.db 为空(可能被截断)'
|
|
182
|
-
: primaryRaw === null
|
|
183
|
-
? 'sillyspec.db 不存在'
|
|
184
|
-
: 'sillyspec.db 损坏';
|
|
185
|
-
console.warn(`⚠️ ${reason},已从 .bak 备份恢复。`);
|
|
186
|
-
return bakDb;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
if (primaryRaw === null) {
|
|
190
|
-
// 主库与备份都不存在 → 全新项目
|
|
191
|
-
return new SQL.Database();
|
|
192
|
-
}
|
|
193
|
-
// 主库曾存在(空/损坏)但无法恢复 → 数据丢失,必须 fail-loud
|
|
194
|
-
throw new Error('sillyspec.db 损坏且 .bak 备份不可用,无法恢复。请从版本控制或其他备份恢复 .sillyspec/.runtime/ 后重试。');
|
|
189
|
+
/** 获取底层 db 对象(better-sqlite3 Database 实例,供 progress.js 直接使用) */
|
|
190
|
+
getDb() {
|
|
191
|
+
return this.db;
|
|
195
192
|
}
|
|
196
193
|
|
|
197
194
|
_createSchema() {
|
|
195
|
+
// better-sqlite3:exec() 执行多条 SQL/DDL(无返回);prepare().run/get/all 用于 DML。
|
|
196
|
+
// sql.js 的 .run(sql) 在此换为 .exec(sql)。
|
|
197
|
+
|
|
198
198
|
// project 表
|
|
199
|
-
this.db.
|
|
199
|
+
this.db.exec(`
|
|
200
200
|
CREATE TABLE IF NOT EXISTS project (
|
|
201
201
|
id INTEGER PRIMARY KEY DEFAULT 1,
|
|
202
202
|
name TEXT NOT NULL,
|
|
@@ -207,7 +207,7 @@ export class DB {
|
|
|
207
207
|
`);
|
|
208
208
|
|
|
209
209
|
// changes 表
|
|
210
|
-
this.db.
|
|
210
|
+
this.db.exec(`
|
|
211
211
|
CREATE TABLE IF NOT EXISTS changes (
|
|
212
212
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
213
213
|
name TEXT UNIQUE NOT NULL,
|
|
@@ -224,7 +224,7 @@ export class DB {
|
|
|
224
224
|
`);
|
|
225
225
|
|
|
226
226
|
// stages 表
|
|
227
|
-
this.db.
|
|
227
|
+
this.db.exec(`
|
|
228
228
|
CREATE TABLE IF NOT EXISTS stages (
|
|
229
229
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
230
230
|
change_id INTEGER NOT NULL REFERENCES changes(id) ON DELETE CASCADE,
|
|
@@ -237,7 +237,7 @@ export class DB {
|
|
|
237
237
|
`);
|
|
238
238
|
|
|
239
239
|
// steps 表
|
|
240
|
-
this.db.
|
|
240
|
+
this.db.exec(`
|
|
241
241
|
CREATE TABLE IF NOT EXISTS steps (
|
|
242
242
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
243
243
|
stage_id INTEGER NOT NULL REFERENCES stages(id) ON DELETE CASCADE,
|
|
@@ -250,7 +250,7 @@ export class DB {
|
|
|
250
250
|
`);
|
|
251
251
|
|
|
252
252
|
// batch_progress 表
|
|
253
|
-
this.db.
|
|
253
|
+
this.db.exec(`
|
|
254
254
|
CREATE TABLE IF NOT EXISTS batch_progress (
|
|
255
255
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
256
256
|
change_id INTEGER NOT NULL REFERENCES changes(id) ON DELETE CASCADE,
|
|
@@ -263,7 +263,7 @@ export class DB {
|
|
|
263
263
|
`);
|
|
264
264
|
|
|
265
265
|
// approvals 表
|
|
266
|
-
this.db.
|
|
266
|
+
this.db.exec(`
|
|
267
267
|
CREATE TABLE IF NOT EXISTS approvals (
|
|
268
268
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
269
269
|
change_id INTEGER NOT NULL REFERENCES changes(id) ON DELETE CASCADE,
|
|
@@ -277,10 +277,10 @@ export class DB {
|
|
|
277
277
|
`);
|
|
278
278
|
|
|
279
279
|
// 索引
|
|
280
|
-
this.db.
|
|
281
|
-
this.db.
|
|
282
|
-
this.db.
|
|
283
|
-
this.db.
|
|
280
|
+
this.db.exec('CREATE INDEX IF NOT EXISTS idx_changes_current_stage ON changes(current_stage)');
|
|
281
|
+
this.db.exec('CREATE INDEX IF NOT EXISTS idx_changes_status ON changes(status)');
|
|
282
|
+
this.db.exec('CREATE INDEX IF NOT EXISTS idx_stages_change ON stages(change_id)');
|
|
283
|
+
this.db.exec('CREATE INDEX IF NOT EXISTS idx_steps_stage ON steps(stage_id)');
|
|
284
284
|
|
|
285
285
|
// Migration: add isolation columns to changes table (idempotent)
|
|
286
286
|
this._migrateAddColumn('changes', 'isolation_status', 'TEXT');
|
|
@@ -305,12 +305,13 @@ export class DB {
|
|
|
305
305
|
}
|
|
306
306
|
|
|
307
307
|
/**
|
|
308
|
-
*
|
|
308
|
+
* 幂等地给表添加列(列已存在则跳过)。
|
|
309
|
+
* better-sqlite3:exec() 执行 ALTER TABLE DDL;列已存在抛错被 catch 静默跳过。
|
|
309
310
|
* @private
|
|
310
311
|
*/
|
|
311
312
|
_migrateAddColumn(table, column, type) {
|
|
312
313
|
try {
|
|
313
|
-
this.db.
|
|
314
|
+
this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${type}`);
|
|
314
315
|
} catch {
|
|
315
316
|
// 列已存在,静默跳过
|
|
316
317
|
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local 后端派发指令模板(task-dispatcher 抽象层)— task-03
|
|
3
|
+
*
|
|
4
|
+
* 定位(D-007):dispatcher **不是 JS 执行体**。本机 Agent tool(Claude Code Task 工具)
|
|
5
|
+
* 只有 agent 能调,CLI(Node)进程调不了。所以本文件 = **派发指令模板生成器**:
|
|
6
|
+
* 生成注入 execute prompt 的「派发指令文本」,告诉 agent 如何用本机 Agent tool 启动
|
|
7
|
+
* 子代理;实际 Agent tool 调用由 agent 执行,本模块不调任何 tool。
|
|
8
|
+
*
|
|
9
|
+
* Local 后端 = **默认与降级路径**(D-005),保留现有 buildWavePrompt(src/stages/execute.js)
|
|
10
|
+
* 的本机 Agent tool 派发行为:每任务一个独立子代理、workdir 强制 = worktreePath、
|
|
11
|
+
* worker 终态后 SillySpec 对 worktree 工作区 git diff 写 review.json(复用既有
|
|
12
|
+
* task-review 契约)。SillyHub MCP 不可用 / 未配置 / 路径 A 未落地时全程走此路径,零回归。
|
|
13
|
+
*
|
|
14
|
+
* Local 后端**忽略** contract 中的 modelHint / agentProfileHint:本机 Agent tool 不做
|
|
15
|
+
* 异构模型 / agent 分配,这两字段仅 SillyHub 后端消费。
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Local 后端结果回收约定(worker 终态 → review.json)。
|
|
20
|
+
*
|
|
21
|
+
* 抽成独立常量供 strategy.js 在拼接「兜底 / 降级 / SillyHub 回收」段时复用,避免 Local
|
|
22
|
+
* 与 SillyHub 两份回收文案重复维护(R-07:回收统一走 review.json,屏蔽后端差异)。
|
|
23
|
+
*
|
|
24
|
+
* 含 `<runId>` 占位符,使用方按实际 contract.runId 替换(见 renderLocalInstruction)。
|
|
25
|
+
*/
|
|
26
|
+
export const LOCAL_RECYCLE_RULE = `### 回收约定(worker 终态 → review.json)
|
|
27
|
+
|
|
28
|
+
worker 子代理结束后,**回收不依赖 worker 自报结果,也不依赖 worker git commit**:
|
|
29
|
+
SillySpec 主体(你,调度 agent)对 worktree **工作区**做 git diff,按既有 task-review
|
|
30
|
+
契约写 review.json。
|
|
31
|
+
|
|
32
|
+
1. 读取该 task 在 worktree 工作区的 git diff(task 开始 → 完成的变更)
|
|
33
|
+
2. 对照 plan.md 与 tasks/task-XX.md 检查实现(不信任 implementer 自报,只看当前 task 的 diff)
|
|
34
|
+
3. 写入 review.json:\`.runtime/execute-runs/<runId>/tasks/task-XX/review.json\`
|
|
35
|
+
4. review.json 写入成功后,该 task 的派发回收完成
|
|
36
|
+
|
|
37
|
+
review.json 复用既有 task-review 契约(必填字段:schemaVersion / task / base / head /
|
|
38
|
+
changedFiles / specVerdict / qualityVerdict / reviewerNotes / requiredEvidence),不发明
|
|
39
|
+
新契约。回收屏蔽后端差异:无论 Local 还是 SillyHub worker,SillySpec 都自己 diff worktree
|
|
40
|
+
工作区写 review.json。`
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @typedef {object} DispatchContract
|
|
44
|
+
* @property {string} brief - 任务目标 + 蓝图路径(注入子代理 prompt)
|
|
45
|
+
* @property {string} worktreePath - SillySpec worktree 绝对路径(子代理 cwd)
|
|
46
|
+
* @property {string} branch - worktree 当前分支
|
|
47
|
+
* @property {string[]} allowedPaths - task 边界(子代理 prompt 里声明,SillySpec 侧 assess/apply 校验)
|
|
48
|
+
* @property {boolean} readOnly - execute 写模式=false;true=只读 QA(仅读/diff/写 review.json)
|
|
49
|
+
* @property {string} [modelHint] - tasks.md [model:xxx](Local 忽略,仅 SillyHub 消费)
|
|
50
|
+
* @property {string} [agentProfileHint] - agent profile(Local 忽略,仅 SillyHub 消费)
|
|
51
|
+
* @property {string} runId - SillySpec execute-run id(关联 review.json 路径)
|
|
52
|
+
* @property {string} [missionId] - Wave 级 mission id(D-008,SillyHub 专属,Local 忽略)
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 渲染 Local 后端派发指令文本。
|
|
57
|
+
*
|
|
58
|
+
* 纯模板生成器:不校验 contract 完整性、不调任何 tool,只把 contract 字段拼成可注入
|
|
59
|
+
* execute prompt 的中文指令字符串。contract 缺字段时回退到醒目占位符(`<未提供 ...>`),
|
|
60
|
+
* 让 agent 在 prompt 里一眼看到派发前必须补齐的参数,而非静默产出错位指令。
|
|
61
|
+
*
|
|
62
|
+
* @param {DispatchContract} [contract] - 派发契约(见 design.md DispatchContract 接口定义)
|
|
63
|
+
* @returns {string} 注入 execute prompt 的 Local 后端派发指令文本
|
|
64
|
+
*/
|
|
65
|
+
export function renderLocalInstruction(contract) {
|
|
66
|
+
const c = contract || {}
|
|
67
|
+
const worktreePath = c.worktreePath
|
|
68
|
+
const branch = c.branch
|
|
69
|
+
const allowedPaths = Array.isArray(c.allowedPaths) ? c.allowedPaths : null
|
|
70
|
+
const readOnly = !!c.readOnly
|
|
71
|
+
const brief = c.brief
|
|
72
|
+
const runId = c.runId
|
|
73
|
+
|
|
74
|
+
// 回收约定:替换 <runId> 占位符;runId 缺失保留占位符让 agent 警觉
|
|
75
|
+
const recycleRule = LOCAL_RECYCLE_RULE.replaceAll(
|
|
76
|
+
'<runId>',
|
|
77
|
+
runId ? String(runId) : '<未提供 runId>'
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
const workdirDisplay = worktreePath
|
|
81
|
+
? String(worktreePath)
|
|
82
|
+
: '<未提供 contract.worktreePath — 派发前必须补齐,否则破坏 worktree 隔离>'
|
|
83
|
+
|
|
84
|
+
const branchLine = branch
|
|
85
|
+
? `- worktree 当前分支:\`${branch}\``
|
|
86
|
+
: '- worktree 当前分支:未提供(沿用 worktree 检出时的既有分支)'
|
|
87
|
+
|
|
88
|
+
const allowedLine = allowedPaths && allowedPaths.length
|
|
89
|
+
? `- 本次任务边界(allowedPaths):${allowedPaths.map(p => `\`${p}\``).join(' / ')}`
|
|
90
|
+
: '- 本次任务边界(allowedPaths):未提供,按蓝图 tasks/task-XX.md 的 allowed_paths 为准'
|
|
91
|
+
|
|
92
|
+
const briefLine = brief
|
|
93
|
+
? String(brief)
|
|
94
|
+
: '<未提供 contract.brief — 派发前必须补齐任务目标与蓝图路径>'
|
|
95
|
+
|
|
96
|
+
const modeLine = readOnly
|
|
97
|
+
? '5. **只读模式(readOnly=true)**:本子代理只读 / 审查 / 写 review.json,**不改源码**'
|
|
98
|
+
: '5. **写模式(readOnly=false)**:按蓝图实现源码(TDD:先读后写 → 写测试 → 写实现)'
|
|
99
|
+
|
|
100
|
+
return `### 派发后端:Local(本机 Agent tool)— 默认与降级路径
|
|
101
|
+
|
|
102
|
+
本指令用**本机 Agent tool**(Claude Code Task 工具)启动子代理执行任务。这是默认与降级
|
|
103
|
+
路径:SillyHub MCP 不可用 / 未配置 / 路径 A 未落地时全程走此路径,派发行为与既有 execute
|
|
104
|
+
波次派发等价(零回归)。
|
|
105
|
+
|
|
106
|
+
**你的角色是调度者 + 审查者,不要自己写代码。** 每个任务由一个独立子代理执行,同 Wave 内
|
|
107
|
+
可并行启动多个子代理。
|
|
108
|
+
|
|
109
|
+
### 启动子代理(必须严格遵守)
|
|
110
|
+
|
|
111
|
+
调用本机 Agent tool(Task 工具)为该 task 启动一个独立子代理,参数结构范例:
|
|
112
|
+
|
|
113
|
+
\`\`\`json
|
|
114
|
+
{
|
|
115
|
+
"subagent_type": "general",
|
|
116
|
+
"workdir": "${workdirDisplay}",
|
|
117
|
+
"prompt": "在此编写任务描述:任务目标 + 蓝图路径 + 编码铁律(见下方要点)"
|
|
118
|
+
}
|
|
119
|
+
\`\`\`
|
|
120
|
+
|
|
121
|
+
### workdir 强制必传 = contract.worktreePath(关键)
|
|
122
|
+
|
|
123
|
+
**workdir 参数是强制必传的,值必须为 \`${workdirDisplay}\`(contract.worktreePath)。**
|
|
124
|
+
|
|
125
|
+
- **不传 workdir,子代理会在主工作区(主仓库根)而非 worktree 里读写文件**,把改动写到
|
|
126
|
+
主工作区、**破坏 worktree 隔离**,后续 assess / apply / review.json 全部错位,并可能
|
|
127
|
+
撞其他并行 agent 的改动。
|
|
128
|
+
${branchLine}
|
|
129
|
+
- 蓝图文件(tasks.md / design.md / tasks/task-XX.md)在主工作区 \`.sillyspec/changes/<change>/\`
|
|
130
|
+
下,可能不在 worktree 中——**读蓝图用主工作区绝对路径,不要拼接到 worktree 路径下**;
|
|
131
|
+
写代码必须落在 workdir(worktree)内。
|
|
132
|
+
|
|
133
|
+
### 子代理 prompt 要点
|
|
134
|
+
|
|
135
|
+
在子代理 prompt 里写明:
|
|
136
|
+
1. 任务目标(brief):${briefLine}
|
|
137
|
+
2. 蓝图文件路径(让子代理按需读取详情,不要内联整份蓝图污染上下文)
|
|
138
|
+
3. 编码铁律:先读后写、TDD、不编造方法、只做蓝图里写的事、遵守边界处理规则、不超出 allowedPaths
|
|
139
|
+
4. 如存在模块文档(\`.sillyspec/docs/*/modules/\`),按需读取涉及模块的 <module>.md 参考接口约定与数据流
|
|
140
|
+
${modeLine}
|
|
141
|
+
${allowedLine}
|
|
142
|
+
|
|
143
|
+
### Local 后端忽略的字段(不消费)
|
|
144
|
+
|
|
145
|
+
本机 Agent tool 不做异构模型 / agent 分配,故 Local 后端**忽略** contract 中的以下字段
|
|
146
|
+
(仅 SillyHub 后端消费,此处列出仅为避免 agent 误把它们塞进 Agent tool 参数):
|
|
147
|
+
- \`modelHint\` → SillyHub model 映射,Local 不传给本机 Agent tool
|
|
148
|
+
- \`agentProfileHint\` → SillyHub agent_profile_id 映射,Local 不传给本机 Agent tool
|
|
149
|
+
- \`missionId\` → SillyHub「一 Wave 一 mission」概念(D-008),Local 无 mission
|
|
150
|
+
|
|
151
|
+
${recycleRule}`
|
|
152
|
+
}
|