sillyspec 3.28.7 → 3.28.9
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-quick/SKILL.md +2 -0
- package/.husky/pre-push +3 -1
- package/AGENTS.md +3 -2
- package/README.md +35 -11
- package/SKILL.md +24 -10
- package/SillySpec-/350/203/275/345/212/233/344/272/256/347/202/271/345/205/250/346/231/257-2026-09-14.pptx +0 -0
- package/package.json +1 -1
- package/src/agent-session-log.js +446 -35
- package/src/change-risk-profile.js +20 -0
- package/src/config-cat.js +6 -3
- package/src/config-schema.js +32 -0
- package/src/constants.js +3 -0
- package/src/db.js +23 -6
- package/src/decision-distill.js +15 -3
- package/src/design-facts.js +33 -3
- package/src/docs-check.js +137 -12
- package/src/docs-gate.js +108 -6
- package/src/doctor-diagnostics.js +243 -9
- package/src/friction-ledger.js +171 -0
- package/src/friction-tally.js +7 -2
- package/src/git-helper.js +6 -4
- package/src/hooks/worktree-guard.js +72 -30
- package/src/index.js +356 -81
- package/src/knowledge-classify.js +363 -0
- package/src/knowledge-hits.js +74 -0
- package/src/knowledge-stats.js +180 -0
- package/src/local-detect.js +30 -15
- package/src/local-register.js +13 -4
- package/src/mcp-server.js +38 -8
- package/src/progress/change-registry.js +224 -43
- package/src/progress/consistency-doctor.js +11 -4
- package/src/progress/shared.js +3 -2
- package/src/progress/stage-machine.js +3 -1
- package/src/progress/step-store.js +30 -17
- package/src/progress.js +123 -8
- package/src/quick-gate-profile.js +251 -0
- package/src/quicklog.js +199 -32
- package/src/review-dispatch.js +2 -1
- package/src/run/command.js +124 -15
- package/src/run/complete-handlers.js +514 -24
- package/src/run/complete.js +82 -25
- package/src/run/concurrent-detect.js +99 -1
- package/src/run/gate-snapshot.js +115 -5
- package/src/run/gates.js +85 -14
- package/src/run/prompt.js +143 -6
- package/src/run/quick-audit.js +136 -15
- package/src/run/shared.js +174 -20
- package/src/run/stage.js +6 -0
- package/src/run/verify-quality-scan.js +14 -3
- package/src/scan-diff.js +61 -30
- package/src/scan-postcheck.js +74 -0
- package/src/scan-refresh.js +477 -0
- package/src/scan-staleness.js +25 -13
- package/src/scope-audit.js +365 -35
- package/src/spec-sync.js +19 -14
- package/src/stage-contract-spec.js +4 -3
- package/src/stage-contract.js +185 -2
- package/src/stage-review.js +29 -12
- package/src/stages/brainstorm.js +7 -1
- package/src/stages/execute.js +91 -4
- package/src/stages/knowledge.js +52 -3
- package/src/stages/plan-postcheck.js +57 -12
- package/src/stages/quick.js +1 -1
- package/src/stages/verify.js +1 -0
- package/src/sync-noise.js +114 -6
- package/src/sync.js +201 -11
- package/src/task-review.js +333 -27
- package/src/verify-facts-schema.js +10 -3
- package/src/verify-postcheck.js +41 -6
- package/src/verify-probes.js +348 -7
- package/src/worktree-apply.js +594 -62
- package/src/worktree-cross.js +12 -18
- package/src/worktree-deps.js +100 -2
- package/src/worktree.js +255 -30
- package/src/wt-commit.js +128 -0
- package/templates/agents-instruction.md +2 -2
- package/templates/prompts/testcase-design.md +2 -1
- package/templates/prompts/verify-probes.md +4 -1
- package/sa-rename-probe/src/new.js +0 -4
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import { existsSync, readFileSync, readdirSync } from 'fs'
|
|
13
13
|
import path from 'path'
|
|
14
|
-
import {
|
|
14
|
+
import { createRequire } from 'node:module'
|
|
15
15
|
|
|
16
16
|
// ── 常量 ──
|
|
17
17
|
|
|
@@ -29,8 +29,9 @@ const READONLY_COMMANDS = new Set([
|
|
|
29
29
|
'node', 'npm', 'npx', // 只允许 --version 等只读子命令,在 matchReadonlyWhitelist 中处理
|
|
30
30
|
])
|
|
31
31
|
|
|
32
|
-
/** 只读 git 子命令(stash
|
|
33
|
-
|
|
32
|
+
/** 只读 git 子命令(stash/branch/worktree 不在此列:branch 的 -d/-D/-m/-M 破坏性、worktree
|
|
33
|
+
* 的 remove --force 可删未提交工作区——各自在下方按参数细化放行) */
|
|
34
|
+
const READONLY_GIT_SUBS = new Set(['diff', 'status', 'log', 'show'])
|
|
34
35
|
|
|
35
36
|
/** 危险 git 子命令 */
|
|
36
37
|
const DANGER_GIT_SUBS = new Set([
|
|
@@ -38,6 +39,14 @@ const DANGER_GIT_SUBS = new Set([
|
|
|
38
39
|
'mv', 'rm',
|
|
39
40
|
])
|
|
40
41
|
|
|
42
|
+
/** git branch 的破坏性 flag(-d/-D 删分支、-m/-M 改名、-f 强建重指;含长形同义)——
|
|
43
|
+
* 删/改分支会让 task review 的 base/head ref 引用悬空(gc 后真丢),2026-09-11 审查 P1 收口 */
|
|
44
|
+
const BRANCH_DESTRUCTIVE_RE = /^(?:-[dDmMf]|--delete|--force|--move|--edit-description)(?:=|$)/
|
|
45
|
+
|
|
46
|
+
function hasDestructiveBranchFlag(parts) {
|
|
47
|
+
return parts.slice(2).some(tok => BRANCH_DESTRUCTIVE_RE.test(tok))
|
|
48
|
+
}
|
|
49
|
+
|
|
41
50
|
/** 危险 git stash 操作 */
|
|
42
51
|
const DANGER_STASH_ACTIONS = new Set(['drop', 'clear', 'pop'])
|
|
43
52
|
|
|
@@ -194,6 +203,14 @@ function shouldBlockScanDocOverwrite(filePath, projectRoot) {
|
|
|
194
203
|
const guard = readScanGuard(scanDocInfo, projectRoot)
|
|
195
204
|
if (!guard || guard.forceRescan) return { blocked: false }
|
|
196
205
|
|
|
206
|
+
// 增量刷新会话态前置分支(2026-09-14-scan-incremental-refresh D-007@v1):refresh ①拍写入
|
|
207
|
+
// mode='scan-refresh' + refreshDocs 白名单——白名单内=本次刷新工单面,agent 手术编辑放行;
|
|
208
|
+
// 白名单外的 scan 文档继续走原保护(非本次刷新面不放松)。存量 guard(无 mode 字段)不受影响。
|
|
209
|
+
if (guard.mode === 'scan-refresh' && Array.isArray(guard.refreshDocs)) {
|
|
210
|
+
const refreshRel = toPosixPath(path.relative(scanDocInfo.specRoot, filePath))
|
|
211
|
+
if (guard.refreshDocs.includes(refreshRel)) return { blocked: false }
|
|
212
|
+
}
|
|
213
|
+
|
|
197
214
|
let frontmatter = {}
|
|
198
215
|
try {
|
|
199
216
|
frontmatter = parseFrontmatter(readFileSync(filePath, 'utf8'))
|
|
@@ -202,7 +219,11 @@ function shouldBlockScanDocOverwrite(filePath, projectRoot) {
|
|
|
202
219
|
}
|
|
203
220
|
|
|
204
221
|
const relPath = toPosixPath(path.relative(projectRoot, filePath))
|
|
205
|
-
|
|
222
|
+
// 归一化比对(D-007@v1 顺带修复):guard 写入方用 40 位全哈希(run/stage.js rev-parse HEAD)、
|
|
223
|
+
// frontmatter 盖章用 7 位短哈希(scan-postcheck.js --short)——旧精确比对恒不等,guard 存在
|
|
224
|
+
// 即恒拦。两侧截 7 位后恢复「同基线放行 / 异基线拦截」设计本意。
|
|
225
|
+
if (frontmatter.source_commit && guard.sourceCommit
|
|
226
|
+
&& String(frontmatter.source_commit).slice(0, 7) !== String(guard.sourceCommit).slice(0, 7)) {
|
|
206
227
|
return {
|
|
207
228
|
blocked: true,
|
|
208
229
|
reason: [
|
|
@@ -235,9 +256,9 @@ function isInsideWorktreeStorage(filePath, cwd) {
|
|
|
235
256
|
/**
|
|
236
257
|
* 同步只读查询 sillyspec.db 第一行第一列(不依赖外部 sqlite3 CLI)。
|
|
237
258
|
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
259
|
+
* 进程内 createRequire 同步加载 node:sqlite 的 DatabaseSync(2026-09-11 审查性能包②:旧实现
|
|
260
|
+
* 每次查询 spawn 一个 node 子进程,Windows 100-300ms/次,hook 每次 Write/Edit/Bash 触发 1-2 次;
|
|
261
|
+
* hook 自身就是同一 node 二进制,子进程无隔离收益只有延迟):
|
|
241
262
|
* - readOnly 打开只读连接(替代旧方案 readonly/fileMustExist 语义):绝不写库;WAL 并发读不阻塞主进程写,
|
|
242
263
|
* 且只读连接可看到已 commit 但未 checkpoint 的过渡态(修复旧 WASM 内存库裸读 .db
|
|
243
264
|
* 看不见 WAL 的过渡态可见性窗口)。db 缺失/损坏由 open 异常自然触发 fail-closed(外层 catch)。
|
|
@@ -245,7 +266,7 @@ function isInsideWorktreeStorage(filePath, cwd) {
|
|
|
245
266
|
* 环境假设(R-09):WAL 模式下只读连接仍需在 .runtime/ 目录建/更新 -shm 索引文件,故 .runtime
|
|
246
267
|
* 必须可写(与主进程一致)——“只读”指不改 DB 数据,不等于不对 -shm 加索引。
|
|
247
268
|
*
|
|
248
|
-
* fail-closed:db 文件不存在 /
|
|
269
|
+
* fail-closed:db 文件不存在 / 打开、查询异常 / node:sqlite 不可用——一律 console.warn
|
|
249
270
|
* (含 e.stderr 详情)并返回 null,调用方(readCurrentStage/isNoWorktreeMode)对 null 走
|
|
250
271
|
* fail-closed,禁止 fail-open。
|
|
251
272
|
*/
|
|
@@ -257,26 +278,26 @@ function queryDbFirstCell(cwd, sql) {
|
|
|
257
278
|
console.warn('⚠️ sillyspec.db 不存在,hook 降级 fail-closed: ' + dbPath)
|
|
258
279
|
return null
|
|
259
280
|
}
|
|
260
|
-
//
|
|
261
|
-
//
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
"dbPath=" + JSON.stringify(dbPath) + ",sql=" + JSON.stringify(sql) + ";" +
|
|
265
|
-
"let db;" +
|
|
266
|
-
"try{db=new DatabaseSync(dbPath,{readOnly:true});" +
|
|
267
|
-
"const row=db.prepare(sql).get();" +
|
|
268
|
-
"const v=row===undefined?undefined:Object.values(row)[0];" +
|
|
269
|
-
"if(v!==undefined)process.stdout.write(String(v??''));db.close()}" +
|
|
270
|
-
"catch(e){process.stderr.write('hook db query failed: '+String(e&&e.message||e));process.exit(1)};"
|
|
281
|
+
// 进程内同步 require node:sqlite(DatabaseSync 同步 API;2026-09-11 审查性能包②:旧实现每次
|
|
282
|
+
// hook 触发 spawn 一个 node 子进程查库,Windows 100-300ms/次×每 Write/Edit/Bash 1-2 次——
|
|
283
|
+
// hook 自身就是同一 node 二进制,子进程无隔离收益只有延迟)。异常路径与旧子进程版同语义:
|
|
284
|
+
// 捕获后 warn + 返回 null,调用方(readCurrentStage 等)对 null 走 fail-closed。
|
|
271
285
|
try {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
286
|
+
const req = createRequire(import.meta.url)
|
|
287
|
+
const { DatabaseSync } = req('node:sqlite')
|
|
288
|
+
let db
|
|
289
|
+
try {
|
|
290
|
+
db = new DatabaseSync(dbPath, { readOnly: true })
|
|
291
|
+
const row = db.prepare(sql).get()
|
|
292
|
+
const v = row === undefined ? undefined : Object.values(row)[0]
|
|
293
|
+
return v === undefined ? null : (String(v ?? '') || null)
|
|
294
|
+
} finally {
|
|
295
|
+
if (db) db.close()
|
|
296
|
+
}
|
|
275
297
|
} catch (e) {
|
|
276
|
-
// DB 查询失败(db 损坏 / readOnly 打开失败 /
|
|
277
|
-
//(readCurrentStage 等)对 null 走 fail-closed。补 warn 让 doctor/agent
|
|
278
|
-
|
|
279
|
-
const detail = (e && e.stderr ? String(e.stderr).trim() : '') || (e && e.message) || 'timeout/crash'
|
|
298
|
+
// DB 查询失败(db 损坏 / readOnly 打开失败 / node:sqlite 不可用):降级返回 null,调用方
|
|
299
|
+
//(readCurrentStage 等)对 null 走 fail-closed。补 warn 让 doctor/agent 能看到根因。
|
|
300
|
+
const detail = (e && e.message) ? e.message : String(e)
|
|
280
301
|
console.warn('⚠️ sillyspec.db 查询失败,hook 降级 fail-closed: ' + detail)
|
|
281
302
|
return null
|
|
282
303
|
}
|
|
@@ -532,10 +553,13 @@ function isSingleCommandReadonly(cmd, extraReadonlyCommands = []) {
|
|
|
532
553
|
if (cmdName === 'git') {
|
|
533
554
|
const sub = parts[1] || ''
|
|
534
555
|
if (READONLY_GIT_SUBS.has(sub)) return true
|
|
556
|
+
// git branch:裸查询/建分支放行;-d/-D/-m/-M/-f(含长形)破坏性不放行(按参数细化,不再整类放行)
|
|
557
|
+
if (sub === 'branch' && !hasDestructiveBranchFlag(parts)) return true
|
|
535
558
|
// git stash list
|
|
536
559
|
if (sub === 'stash' && (parts[2] === 'list' || parts.length === 2)) return true
|
|
537
|
-
// git worktree
|
|
538
|
-
|
|
560
|
+
// git worktree:仅 list(裸 worktree 同 list)。remove --force 可删含未提交改动的工作区、
|
|
561
|
+
// 且 junction 会穿透删主仓 node_modules——add/remove/prune 一律走 sillyspec worktree 命令(内建护栏)
|
|
562
|
+
if (sub === 'worktree' && (parts.length === 2 || parts[2] === 'list')) return true
|
|
539
563
|
return false
|
|
540
564
|
}
|
|
541
565
|
|
|
@@ -563,6 +587,10 @@ function isSingleCommandDangerous(cmd) {
|
|
|
563
587
|
if (cmdName === 'git') {
|
|
564
588
|
const sub = parts[1] || ''
|
|
565
589
|
if (DANGER_GIT_SUBS.has(sub)) return true
|
|
590
|
+
// git branch -d/-D/-m/-M/-f(含长形):删/改分支让 task review 的 base/head 引用悬空
|
|
591
|
+
if (sub === 'branch' && hasDestructiveBranchFlag(parts)) return true
|
|
592
|
+
// git worktree remove/prune:删工作区(可能含未提交改动)——清理走 sillyspec worktree cleanup
|
|
593
|
+
if (sub === 'worktree' && ['remove', 'prune'].includes(parts[2] || '')) return true
|
|
566
594
|
// git stash drop/clear/pop
|
|
567
595
|
if (sub === 'stash' && DANGER_STASH_ACTIONS.has(parts[2] || '')) return true
|
|
568
596
|
}
|
|
@@ -603,6 +631,20 @@ function matchReadonlyWhitelist(command, extraReadonlyCommands = []) {
|
|
|
603
631
|
* @param {string} command
|
|
604
632
|
* @returns {boolean}
|
|
605
633
|
*/
|
|
634
|
+
/**
|
|
635
|
+
* 危险拦截原因的定向指引(branch/worktree 的破坏形态有官方替代路径,提示同步到拦截消息)
|
|
636
|
+
*/
|
|
637
|
+
function dangerBlockHint(command) {
|
|
638
|
+
const parts = String(command).trim().toLowerCase().split(/\s+/)
|
|
639
|
+
if (parts[0] === 'git' && parts[1] === 'branch') {
|
|
640
|
+
return '——分支删除/改名/强建会让 task review 的 base/head 引用悬空;分支管理走 sillyspec worktree 流程'
|
|
641
|
+
}
|
|
642
|
+
if (parts[0] === 'git' && parts[1] === 'worktree') {
|
|
643
|
+
return '——worktree 清理走 sillyspec worktree cleanup(内建未落仓护栏),勿裸删工作区'
|
|
644
|
+
}
|
|
645
|
+
return ''
|
|
646
|
+
}
|
|
647
|
+
|
|
606
648
|
function matchDangerBlacklist(command) {
|
|
607
649
|
const parts = splitCommandParts(command)
|
|
608
650
|
return parts.some(p => isSingleCommandDangerous(p))
|
|
@@ -825,7 +867,7 @@ function shouldBlockBash(command, cwd) {
|
|
|
825
867
|
if (stage === 'quick') {
|
|
826
868
|
// 危险黑名单仍然拦截
|
|
827
869
|
if (matchDangerBlacklist(command)) {
|
|
828
|
-
return { blocked: true, reason: `dangerous command blocked: ${command.trim()}` }
|
|
870
|
+
return { blocked: true, reason: `dangerous command blocked: ${command.trim()}${dangerBlockHint(command)}` }
|
|
829
871
|
}
|
|
830
872
|
// 检查命令是否会覆盖 baseline files(并集 = 保护所有 session 的 baseline)
|
|
831
873
|
const { baselineFiles } = readAllQuickGuards(projectRoot)
|
|
@@ -843,7 +885,7 @@ function shouldBlockBash(command, cwd) {
|
|
|
843
885
|
|
|
844
886
|
// 危险黑名单
|
|
845
887
|
if (matchDangerBlacklist(command)) {
|
|
846
|
-
return { blocked: true, reason: `dangerous command blocked: ${command.trim()}` }
|
|
888
|
+
return { blocked: true, reason: `dangerous command blocked: ${command.trim()}${dangerBlockHint(command)}` }
|
|
847
889
|
}
|
|
848
890
|
|
|
849
891
|
// 只读白名单放行
|