sillyspec 3.25.0 → 3.25.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/package.json +1 -1
- package/src/db.js +28 -8
- package/src/hooks/worktree-guard.js +9 -2
- package/src/index.js +5 -5
- package/src/machine-interface.js +13 -7
- package/src/progress.js +22 -6
- package/src/run/command.js +1011 -0
- package/src/run/complete-handlers.js +873 -0
- package/src/run/complete.js +695 -0
- package/src/run/gates.js +321 -0
- package/src/run/prompt.js +604 -0
- package/src/run/quick-audit.js +102 -0
- package/src/run/scan-profile.js +238 -0
- package/src/run/shared.js +375 -0
- package/src/run/stage.js +414 -0
- package/src/run.js +23 -4379
- package/src/stage-contract.js +4 -2
- package/src/worktree.js +17 -8
package/package.json
CHANGED
package/src/db.js
CHANGED
|
@@ -2,6 +2,11 @@ import initSqlJs from 'sql.js';
|
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'fs';
|
|
3
3
|
import { basename, dirname, join } from 'path';
|
|
4
4
|
|
|
5
|
+
// DB schema 版本(与 project.schema_version DEFAULT 对齐)。_createSchema 改动(加表/列/migration)时 bump,
|
|
6
|
+
// 触发 .schema-version 戳失效 → 下次 init 重跑 _createSchema + _save(W4-H:高频读路径 gate/derive/status
|
|
7
|
+
// 每次 new ProgressManager 都过 init,原无条件 _createSchema+_save 致每次整库 export+三段原子写)。
|
|
8
|
+
const DB_SCHEMA_VERSION = 3;
|
|
9
|
+
|
|
5
10
|
export class DB {
|
|
6
11
|
constructor(dbPath) {
|
|
7
12
|
this.dbPath = dbPath;
|
|
@@ -25,11 +30,21 @@ export class DB {
|
|
|
25
30
|
this.db.run('PRAGMA foreign_keys = ON');
|
|
26
31
|
this.db.run('PRAGMA synchronous = NORMAL');
|
|
27
32
|
|
|
28
|
-
// 5.
|
|
29
|
-
|
|
33
|
+
// 5. 创建表结构(仅当 schema 版本戳不匹配时——W4-H):
|
|
34
|
+
// schema 已最新则只 load 不写,省掉 _createSchema + _save 的整库 export + 三段原子写。
|
|
35
|
+
// 戳在但 db 缺表属"手动改 db"边角(SillySpec 不支持),_loadDatabase 的损坏检测兜底正常路径。
|
|
36
|
+
const versionPath = `${this.dbPath}.schema-version`;
|
|
37
|
+
let schemaCurrent = false;
|
|
38
|
+
try {
|
|
39
|
+
schemaCurrent = existsSync(versionPath)
|
|
40
|
+
&& readFileSync(versionPath, 'utf8').trim() === String(DB_SCHEMA_VERSION);
|
|
41
|
+
} catch { /* 戳读取失败保守重跑 schema */ }
|
|
30
42
|
|
|
31
|
-
|
|
32
|
-
|
|
43
|
+
if (!schemaCurrent) {
|
|
44
|
+
this._createSchema();
|
|
45
|
+
this._save();
|
|
46
|
+
try { writeFileSync(versionPath, String(DB_SCHEMA_VERSION)); } catch { /* 戳写入失败不阻断,下次重跑 */ }
|
|
47
|
+
}
|
|
33
48
|
}
|
|
34
49
|
|
|
35
50
|
close() {
|
|
@@ -43,15 +58,20 @@ export class DB {
|
|
|
43
58
|
transaction(fn) {
|
|
44
59
|
if (!this.db) throw new Error('DB not initialized');
|
|
45
60
|
this.db.run('BEGIN');
|
|
61
|
+
let result;
|
|
46
62
|
try {
|
|
47
|
-
|
|
63
|
+
result = fn(this.db);
|
|
48
64
|
this.db.run('COMMIT');
|
|
49
|
-
this._save();
|
|
50
|
-
return result;
|
|
51
65
|
} catch (err) {
|
|
52
|
-
|
|
66
|
+
// fn 抛错或 COMMIT 失败:事务级回滚(此时事务确实活跃,ROLLBACK 有效)
|
|
67
|
+
try { this.db.run('ROLLBACK'); } catch { /* 事务可能已被 fn 内的错自动回滚 */ }
|
|
53
68
|
throw err;
|
|
54
69
|
}
|
|
70
|
+
// COMMIT 成功后才持久化。_save 失败时抛原生错(磁盘满/EPERM),不再被 catch 里
|
|
71
|
+
// 无效的 ROLLBACK 二级异常("no transaction active")掩盖——内存库已 commit,
|
|
72
|
+
// 下次 _save / close() 会落盘,调用方拿到的是准确的持久化错误而非被替换的误报。
|
|
73
|
+
this._save();
|
|
74
|
+
return result;
|
|
55
75
|
}
|
|
56
76
|
|
|
57
77
|
/** 获取底层 db 对象(供 progress.js 直接使用) */
|
|
@@ -258,12 +258,19 @@ function queryDbFirstCell(cwd, sql) {
|
|
|
258
258
|
"if(!fs.existsSync(dbPath))process.exit(0);" +
|
|
259
259
|
"import(lib).then(async m=>{const SQL=await m.default();" +
|
|
260
260
|
"const db=new SQL.Database(fs.readFileSync(dbPath));const r=db.exec(sql);" +
|
|
261
|
-
"if(r.length&&r[0].values.length)process.stdout.write(String(r[0].values[0][0]??''));db.close()}).catch(()
|
|
261
|
+
"if(r.length&&r[0].values.length)process.stdout.write(String(r[0].values[0][0]??''));db.close()}).catch(e=>{process.stderr.write('hook db query failed: '+String(e&&e.message||e));process.exit(1)});"
|
|
262
262
|
try {
|
|
263
263
|
return execFileSync(process.execPath, ['-e', script], {
|
|
264
264
|
encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'pipe'],
|
|
265
265
|
}).trim() || null
|
|
266
|
-
} catch {
|
|
266
|
+
} catch (e) {
|
|
267
|
+
// DB 查询失败(db 损坏 / sql.js resolve 失败 / 子进程超时):降级返回 null,调用方
|
|
268
|
+
//(readCurrentStage 等)对 null 走 fail-closed。补 warn 让 doctor/agent 能看到根因,
|
|
269
|
+
// 而非完全静默(子进程把诊断写到了 stderr,execFileSync 抛错时挂在 e.stderr 上)。
|
|
270
|
+
const detail = (e && e.stderr ? String(e.stderr).trim() : '') || (e && e.message) || 'timeout/crash'
|
|
271
|
+
console.warn('⚠️ sillyspec.db 查询失败,hook 降级 fail-closed: ' + detail)
|
|
272
|
+
return null
|
|
273
|
+
}
|
|
267
274
|
}
|
|
268
275
|
|
|
269
276
|
export { queryDbFirstCell as _queryDbFirstCellForTest };
|
package/src/index.js
CHANGED
|
@@ -340,8 +340,8 @@ async function main() {
|
|
|
340
340
|
for (const e of (c.errors || [])) console.log(` ✗ ${e}`);
|
|
341
341
|
for (const w of (c.warnings || [])) console.log(` ⚠ ${w}`);
|
|
342
342
|
}
|
|
343
|
-
|
|
344
|
-
|
|
343
|
+
// 顶层 errors/warnings 是各 check 的聚合,已在上面按 check 分组显示;不再重复打印——
|
|
344
|
+
// 非 json 是 agent 默认消费路径,重复文本纯耗 context(W1-J)。
|
|
345
345
|
}
|
|
346
346
|
process.exitCode = exitCode;
|
|
347
347
|
break;
|
|
@@ -402,7 +402,7 @@ async function main() {
|
|
|
402
402
|
const { runCommand } = await import('./run.js')
|
|
403
403
|
// 平台模式(--spec-dir 已指定)时,--dir 是明确的 source_root,不应被 resolveEffectiveDir 纠正
|
|
404
404
|
const effectiveDir = specDir ? dir : resolveEffectiveDir(dir)
|
|
405
|
-
await runCommand(filteredArgs.slice(1), effectiveDir, specDir)
|
|
405
|
+
await runCommand(filteredArgs.slice(1), effectiveDir, specDir, { json })
|
|
406
406
|
break
|
|
407
407
|
}
|
|
408
408
|
// 顶层命令别名:转发 runCommand,与 case 'run': 路径行为一致。
|
|
@@ -513,7 +513,7 @@ async function main() {
|
|
|
513
513
|
}
|
|
514
514
|
// 否则:保持原有 prompt 驱动的 bash 自检流程
|
|
515
515
|
const { runCommand } = await import('./run.js');
|
|
516
|
-
await runCommand([command, ...filteredArgs.slice(1)], doctorEffectiveDir, specDir);
|
|
516
|
+
await runCommand([command, ...filteredArgs.slice(1)], doctorEffectiveDir, specDir, { json });
|
|
517
517
|
break;
|
|
518
518
|
}
|
|
519
519
|
case 'scan':
|
|
@@ -528,7 +528,7 @@ async function main() {
|
|
|
528
528
|
const { runCommand } = await import('./run.js')
|
|
529
529
|
const stageArgs = [command, ...filteredArgs.slice(1)]
|
|
530
530
|
const effectiveDir = specDir ? dir : resolveEffectiveDir(dir)
|
|
531
|
-
await runCommand(stageArgs, effectiveDir, specDir)
|
|
531
|
+
await runCommand(stageArgs, effectiveDir, specDir, { json })
|
|
532
532
|
break
|
|
533
533
|
}
|
|
534
534
|
case 'knowledge': {
|
package/src/machine-interface.js
CHANGED
|
@@ -125,10 +125,16 @@ export async function runGate(stage, changeName, { cwd, specBase, runtimeRoot }
|
|
|
125
125
|
const currentStage = progress.currentStage || '';
|
|
126
126
|
const checks = [];
|
|
127
127
|
|
|
128
|
+
// W4-G (D-008):execute 阶段预计算 code evidence 一次,供 artifacts check(经 runValidators
|
|
129
|
+
// → validateExecuteOutputs)与下方 execute-evidence check 共享——避免各调一次 checkExecuteCodeEvidence
|
|
130
|
+
// 各 spawn 2 个 git 进程(gate execute 一次省 ≈ 60-200ms on Windows)。
|
|
131
|
+
const sharedEvidence = stage === 'execute' ? checkExecuteCodeEvidence(cwd, changeName) : undefined;
|
|
132
|
+
|
|
128
133
|
// ── a. artifacts:阶段产物校验(全部阶段)──
|
|
129
134
|
const r = runValidators(stage, cwd, changeName, {
|
|
130
135
|
projectName: progress.project,
|
|
131
136
|
specRoot,
|
|
137
|
+
...(sharedEvidence ? { evidence: sharedEvidence } : {}),
|
|
132
138
|
});
|
|
133
139
|
checks.push({
|
|
134
140
|
id: 'artifacts',
|
|
@@ -137,24 +143,24 @@ export async function runGate(stage, changeName, { cwd, specBase, runtimeRoot }
|
|
|
137
143
|
warnings: r.warnings || [],
|
|
138
144
|
});
|
|
139
145
|
|
|
140
|
-
// ── b. transition
|
|
141
|
-
// 与 run.js
|
|
142
|
-
// transition
|
|
146
|
+
// ── b. transition:状态转换合法性(参与综合 ok,与 completeStep 硬阻断一致)──
|
|
147
|
+
// 与 run.js runStage 同源:传 fromStageData 触发 failed_post_check 门控。
|
|
148
|
+
// transition 必须参与 ok:否则 gate 返回 ok=true、exit 0,Agent 据此 --done 却被
|
|
149
|
+
// runStage 的 checkTransition 硬阻断(exit 1),gate/run 判定分裂(design §8 漂移)。
|
|
143
150
|
const fromStageData = (progress.stages && currentStage && progress.stages[currentStage]) || undefined;
|
|
144
151
|
const t = checkTransition(currentStage, stage, fromStageData ? { fromStageData } : {});
|
|
145
152
|
checks.push({
|
|
146
153
|
id: 'transition',
|
|
147
154
|
ok: t.allowed,
|
|
148
|
-
informational: true,
|
|
149
155
|
errors: t.allowed ? [] : [t.reason].filter(Boolean),
|
|
150
156
|
warnings: [],
|
|
151
157
|
});
|
|
152
158
|
|
|
153
159
|
// ── c. execute 阶段追加 execute-evidence + task-reviews ──
|
|
154
|
-
// D-008
|
|
155
|
-
//
|
|
160
|
+
// D-008 + W4-G:checkExecuteCodeEvidence 整个 runGate 只调一次(顶部 sharedEvidence 预计算),
|
|
161
|
+
// 这里复用——真正落实去重(原注释声称"只调一次"但实现里 execute-evidence check 仍重复调了第二次)。
|
|
156
162
|
if (stage === 'execute') {
|
|
157
|
-
const ev =
|
|
163
|
+
const ev = sharedEvidence;
|
|
158
164
|
checks.push({
|
|
159
165
|
id: 'execute-evidence',
|
|
160
166
|
ok: ev.status !== 'unchanged',
|
package/src/progress.js
CHANGED
|
@@ -645,8 +645,12 @@ export class ProgressManager {
|
|
|
645
645
|
db.transaction((sqlDb) => {
|
|
646
646
|
sqlDb.run(`UPDATE changes SET name = ?, last_active = ? WHERE name = ?`, [oldName, now, newName]);
|
|
647
647
|
});
|
|
648
|
-
|
|
649
|
-
|
|
648
|
+
console.error(`❌ 重命名失败:移动目录出错(${e.message}),已回滚数据库`);
|
|
649
|
+
} catch (rollbackErr) {
|
|
650
|
+
// 回滚本身也失败:DB 是 newName、目录是 oldName,两端分裂且无自动恢复。
|
|
651
|
+
// 不能再撒谎"已回滚"——必须显式报错让用户跑 doctor 修复。
|
|
652
|
+
console.error(`❌ 重命名失败且数据库回滚也失败(状态分裂,需手动修复):原错=${e.message} 回滚错=${rollbackErr.message}。请跑 sillyspec doctor --json`);
|
|
653
|
+
}
|
|
650
654
|
}
|
|
651
655
|
} else {
|
|
652
656
|
mkdirSync(newDir, { recursive: true });
|
|
@@ -1815,9 +1819,16 @@ export class ProgressManager {
|
|
|
1815
1819
|
const gatePath = this._runtimePath(cwd, 'gate-status.json');
|
|
1816
1820
|
|
|
1817
1821
|
if (!rows || rows.length === 0 || rows[0].values.length === 0) {
|
|
1818
|
-
// 无 execute/quick
|
|
1822
|
+
// 无 execute/quick 阶段的活跃变更:写墓碑而非 unlink。unlink 失败(Windows 杀毒/索引占用)
|
|
1823
|
+
// 会被空 catch 吞掉,陈旧 gate-status 残留 → hook 把 brainstorm 误当 execute 放行(fail-open)。
|
|
1824
|
+
// 墓碑 {stage:null} 让 readCurrentStage 的 `gateStatus.stage` 为假 → 自动 fallback DB,语义等价删除但原子。
|
|
1819
1825
|
if (existsSync(gatePath)) {
|
|
1820
|
-
try {
|
|
1826
|
+
try {
|
|
1827
|
+
this._ensureRuntimeDir(cwd);
|
|
1828
|
+
writeAtomicSync(gatePath, JSON.stringify({ stage: null, tombstone: true, updatedAt: new Date().toISOString() }) + '\n');
|
|
1829
|
+
} catch (e) {
|
|
1830
|
+
console.warn(`⚠️ gate-status 墓碑写入失败(hook 可能读到陈旧缓存): ${e.message}`);
|
|
1831
|
+
}
|
|
1821
1832
|
}
|
|
1822
1833
|
return;
|
|
1823
1834
|
}
|
|
@@ -1837,9 +1848,14 @@ export class ProgressManager {
|
|
|
1837
1848
|
}
|
|
1838
1849
|
|
|
1839
1850
|
if (!gateStage) {
|
|
1840
|
-
// current_stage 为 NULL 的边界情况,等同于无 execute/quick
|
|
1851
|
+
// current_stage 为 NULL 的边界情况,等同于无 execute/quick:同样写墓碑(见上 rationale)
|
|
1841
1852
|
if (existsSync(gatePath)) {
|
|
1842
|
-
try {
|
|
1853
|
+
try {
|
|
1854
|
+
this._ensureRuntimeDir(cwd);
|
|
1855
|
+
writeAtomicSync(gatePath, JSON.stringify({ stage: null, tombstone: true, updatedAt: new Date().toISOString() }) + '\n');
|
|
1856
|
+
} catch (e) {
|
|
1857
|
+
console.warn(`⚠️ gate-status 墓碑写入失败(hook 可能读到陈旧缓存): ${e.message}`);
|
|
1858
|
+
}
|
|
1843
1859
|
}
|
|
1844
1860
|
return;
|
|
1845
1861
|
}
|