sillyspec 3.26.6 → 3.26.8
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-doctor/SKILL.md +2 -0
- package/.claude/skills/sillyspec-execute/SKILL.md +1 -1
- package/.claude/skills/sillyspec-knowledge/SKILL.md +1 -1
- package/.claude/skills/sillyspec-plan/SKILL.md +1 -1
- package/.husky/pre-push +9 -2
- package/bin/sillyspec.js +1 -1
- package/package.json +3 -3
- package/packages/dashboard/server/index.js +8 -1
- package/src/config-schema.js +305 -287
- package/src/docs-check.js +333 -0
- package/src/docs-debt.js +226 -0
- package/src/docs-gate.js +106 -0
- package/src/doctor-diagnostics.js +23 -2
- package/src/index.js +1880 -1750
- package/src/init.js +81 -14
- package/src/modules.js +490 -486
- package/src/progress.js +37 -13
- package/src/quicklog.js +19 -4
- package/src/run/command.js +54 -24
- package/src/run/complete-handlers.js +1254 -1128
- package/src/run/complete.js +8 -34
- package/src/run/gates.js +32 -3
- package/src/run/prompt.js +713 -633
- package/src/run/quick-audit.js +137 -123
- package/src/run/shared.js +1109 -929
- package/src/run/stage.js +8 -3
- package/src/scan-staleness.js +90 -0
- package/src/setup.js +27 -9
- package/src/sillyhub-mcp/client.js +149 -14
- package/src/stage-contract-spec.js +15 -0
- package/src/stage-contract.js +8 -5
- package/src/stages/brainstorm-auto.js +0 -1
- package/src/stages/brainstorm.js +1 -1
- package/src/stages/execute.js +985 -967
- package/src/stages/plan-postcheck.js +93 -36
- package/src/stages/verify.js +1 -1
- package/src/sync.js +60 -32
- package/src/task-review.js +38 -6
- package/src/verify-postcheck.js +22 -5
- package/src/worktree-apply.js +6 -3
- package/src/worktree-deps.js +5 -3
- package/src/worktree.js +4 -2
package/src/progress.js
CHANGED
|
@@ -20,26 +20,21 @@ import { ChangeRegistry } from './progress/change-registry.js';
|
|
|
20
20
|
import { StepStore } from './progress/step-store.js';
|
|
21
21
|
import { StageMachine } from './progress/stage-machine.js';
|
|
22
22
|
import { STAGE_ORDER, MAIN_FLOW_ORDER, VALID_STAGES, STAGE_LABELS, SPEC_DIR_NAME, CURRENT_VERSION, emptyStage } from './progress/shared.js';
|
|
23
|
+
// resolveSpecDir 单一真相源在 src/run/shared.js(含 home 拒绝守卫,坑 cwd-correction-home-collision
|
|
24
|
+
// 根治:home 下 .sillyspec 恒不命中,防 smoke/临时目录污染自我延续)。此处 re-export 保持
|
|
25
|
+
// 既有 import 路径兼容;run/shared.js 对 progress.js 只有动态 import,无静态循环。
|
|
26
|
+
import { resolveSpecDir } from './run/shared.js';
|
|
27
|
+
import { checkPlatformManaged, PLATFORM_MANAGED_FILENAME } from './run/shared.js';
|
|
28
|
+
export { resolveSpecDir };
|
|
23
29
|
|
|
24
30
|
// 默认规范目录名(相对于 cwd)
|
|
25
31
|
// SPEC_DIR_NAME → ./progress/shared.js(W6 Step9d)
|
|
26
32
|
const RUNTIME_SUBDIR = '.runtime';
|
|
27
33
|
|
|
28
34
|
/**
|
|
29
|
-
* 向上查找含 .sillyspec
|
|
30
|
-
*
|
|
35
|
+
* 向上查找含 .sillyspec 目录的祖先目录——实现已收敛至 src/run/shared.js(单一真相源,
|
|
36
|
+
* 含 home 拒绝守卫),此处 re-export。见顶部 import 说明。
|
|
31
37
|
*/
|
|
32
|
-
export function resolveSpecDir(startDir) {
|
|
33
|
-
let dir = resolve(startDir);
|
|
34
|
-
while (true) {
|
|
35
|
-
const candidate = join(dir, SPEC_DIR_NAME);
|
|
36
|
-
if (existsSync(candidate)) return candidate;
|
|
37
|
-
const parent = dirname(dir);
|
|
38
|
-
if (parent === dir) break; // 到达根目录
|
|
39
|
-
dir = parent;
|
|
40
|
-
}
|
|
41
|
-
return join(resolve(startDir), SPEC_DIR_NAME);
|
|
42
|
-
}
|
|
43
38
|
|
|
44
39
|
/**
|
|
45
40
|
* 平台指针不可达错误。pointer 存在但失效时抛出,阻止静默回退到本地孤儿 db。
|
|
@@ -59,6 +54,26 @@ export class PointerUnreachableError extends Error {
|
|
|
59
54
|
}
|
|
60
55
|
}
|
|
61
56
|
|
|
57
|
+
/**
|
|
58
|
+
* 平台接管声明生效错误(指针缺失但声明存在)。fail-closed 入口一:
|
|
59
|
+
* 防指针被 cleanup/STALE 清理或项目挪动后裸调静默建本地进度库(状态分裂根因)。
|
|
60
|
+
*
|
|
61
|
+
* ⚠️ 不覆写 this.name(保持父类的 'PointerUnreachableError')——CLI 顶层 catch 按
|
|
62
|
+
* err?.name === 'PointerUnreachableError' 严格字符串匹配(index.js),子类改名会
|
|
63
|
+
* 落通用错误分支打 stack noise。靠 message 首行"平台接管声明生效"区分场景。
|
|
64
|
+
*/
|
|
65
|
+
export class PlatformManagedError extends PointerUnreachableError {
|
|
66
|
+
constructor({ declarationPath, specRoot }) {
|
|
67
|
+
super({
|
|
68
|
+
pointerPath: declarationPath,
|
|
69
|
+
specRoot,
|
|
70
|
+
reason: `平台接管声明生效——本项目已由平台托管,但恢复指针缺失(可能被 platform pointer --cleanup 清理或项目目录被移动),拒绝静默回退本地模式`,
|
|
71
|
+
hint: `① 重跑平台 scan/init(带 --spec-root)重建指针;② 确认不再使用平台:sillyspec platform disconnect(删除接管声明);③ 显式 --spec-dir <路径> 临时指定目录`,
|
|
72
|
+
});
|
|
73
|
+
this.declarationPath = declarationPath;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
62
77
|
/**
|
|
63
78
|
* 平台感知的 specDir 解析(fail-closed,所有 CLI 子命令统一入口)。
|
|
64
79
|
*
|
|
@@ -75,6 +90,15 @@ export function resolvePlatformSpecDir(cwd, explicitSpecDir = null) {
|
|
|
75
90
|
if (explicitSpecDir) return resolve(explicitSpecDir);
|
|
76
91
|
const pointerPath = join(resolve(cwd), '.sillyspec-platform.json');
|
|
77
92
|
if (!existsSync(pointerPath)) {
|
|
93
|
+
// 指针缺失:查平台接管声明(fail-closed 入口一)。声明存在 = 项目进过平台模式,
|
|
94
|
+
// 指针丢失不该静默当纯本地项目处理(会建本地进度库 → 状态分裂)。
|
|
95
|
+
const decl = checkPlatformManaged(cwd);
|
|
96
|
+
if (decl) {
|
|
97
|
+
throw new PlatformManagedError({
|
|
98
|
+
declarationPath: join(resolve(cwd), PLATFORM_MANAGED_FILENAME),
|
|
99
|
+
specRoot: decl.specRoot,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
78
102
|
return resolveSpecDir(cwd);
|
|
79
103
|
}
|
|
80
104
|
// pointer 存在 = 进过平台模式,严格校验,不静默回退
|
package/src/quicklog.js
CHANGED
|
@@ -81,6 +81,14 @@ export function sanitizeDesc(description) {
|
|
|
81
81
|
return s.length > 120 ? s.slice(0, 120) + '…' : s
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
// git user.name → 文件名安全形式:白名单保留字母数字._- 与中文等 Unicode 字,
|
|
85
|
+
// 剥路径元字符(/ \ ..)与控制字符。防 QUICKLOG-<user>.md / 锁文件 / 轮转归档穿越写。
|
|
86
|
+
export function sanitizeQuicklogUser(user) {
|
|
87
|
+
const s = String(user || '').replace(/[\\/\r\n\0]/g, '').replace(/\.{2,}/g, '.').trim()
|
|
88
|
+
if (!s) return ''
|
|
89
|
+
return s.length > 64 ? s.slice(0, 64) : s
|
|
90
|
+
}
|
|
91
|
+
|
|
84
92
|
// 从关联变更的 proposal/design 提取首个 # 标题(去「提案书(Proposal)— / 设计文档(Design)—」固定前缀)。
|
|
85
93
|
// 让 quick --linked-changes 启动时(用户常不带 --input)也能拿到语义标题,而非 (quick 任务) 占位。
|
|
86
94
|
// 读不到任何标题返回 ''(调用方再回退占位符,保持向后兼容)。
|
|
@@ -134,6 +142,9 @@ export function getQuickFileNotes() { return _pendingFileNotes }
|
|
|
134
142
|
*/
|
|
135
143
|
export function parseFileNotes(raw) {
|
|
136
144
|
if (!raw) return []
|
|
145
|
+
// note 单行化 + 限长:bullet 是「数组元素内嵌 \n」落盘(flipEntryInContent),note 带换行会伪造
|
|
146
|
+
// QUICKLOG 条目结构(\n## ql-… 可伪造后续条目头)。与 sanitizeDesc 同风格。
|
|
147
|
+
const sanitizeNote = (n) => n.replace(/[\r\n]+/g, ' ').trim().slice(0, 200)
|
|
137
148
|
return String(raw)
|
|
138
149
|
.split('||')
|
|
139
150
|
.map((seg) => {
|
|
@@ -141,7 +152,7 @@ export function parseFileNotes(raw) {
|
|
|
141
152
|
if (!s) return null
|
|
142
153
|
const idx = s.indexOf('::')
|
|
143
154
|
const path = (idx === -1 ? s : s.slice(0, idx)).replace(/\\/g, '/').trim()
|
|
144
|
-
const note = idx === -1 ? '' : s.slice(idx + 2)
|
|
155
|
+
const note = idx === -1 ? '' : sanitizeNote(s.slice(idx + 2))
|
|
145
156
|
return path ? { path, note } : null
|
|
146
157
|
})
|
|
147
158
|
.filter(Boolean)
|
|
@@ -234,7 +245,7 @@ async function rotateIfNeeded(userFile, gitUser) {
|
|
|
234
245
|
const dateStr = lastDate
|
|
235
246
|
? `${lastDate.slice(0, 4)}-${lastDate.slice(4, 6)}-${lastDate.slice(6, 8)}`
|
|
236
247
|
: nowDatetime().slice(0, 10)
|
|
237
|
-
const archiveFile = join(dirname(userFile), `QUICKLOG-${gitUser}-${dateStr}.md`)
|
|
248
|
+
const archiveFile = join(dirname(userFile), `QUICKLOG-${sanitizeQuicklogUser(gitUser) || 'unknown'}-${dateStr}.md`)
|
|
238
249
|
// 同日已轮转过:两步各自原子化(append+clear 无法单原子,reader 容忍中间态——见 writeAtomic 注释)
|
|
239
250
|
if (existsSync(archiveFile)) {
|
|
240
251
|
const existing = readFileSync(archiveFile, 'utf8')
|
|
@@ -394,7 +405,9 @@ function flipEntryInContent(content, qlId, result, changedFiles = [], fileNotes
|
|
|
394
405
|
export async function allocateQuicklogEntry(specBase, gitUser, { description, linkedChanges = [], allowedFiles = [] } = {}) {
|
|
395
406
|
const quicklogDir = join(specBase, 'quicklog')
|
|
396
407
|
mkdirSync(quicklogDir, { recursive: true })
|
|
397
|
-
|
|
408
|
+
// git user.name 无字符限制,可含 / \ .. 等路径元字符(git config 或 .git/config 可控)——
|
|
409
|
+
// 白名单消毒防穿越写(QUICKLOG-<user>.md / 锁文件 / 轮转归档三处拼接),与 assertSafeChangeName 同风格
|
|
410
|
+
const user = sanitizeQuicklogUser(gitUser) || 'unknown'
|
|
398
411
|
const userFile = join(quicklogDir, `QUICKLOG-${user}.md`)
|
|
399
412
|
const lockPath = join(quicklogDir, `.QUICKLOG-${user}.md.lock`)
|
|
400
413
|
const desc = sanitizeDesc(description)
|
|
@@ -437,7 +450,9 @@ export async function allocateQuicklogEntry(specBase, gitUser, { description, li
|
|
|
437
450
|
*/
|
|
438
451
|
export async function completeQuicklogEntry(specBase, gitUser, qlId, { resultText = '', linkedChanges = [], changedFiles = [] } = {}) {
|
|
439
452
|
const quicklogDir = join(specBase, 'quicklog')
|
|
440
|
-
|
|
453
|
+
// 与 allocateQuicklogEntry 同源消毒:锁文件路径含 user(防穿越写,两入口必须一致否则锁不上同一文件)
|
|
454
|
+
const user = sanitizeQuicklogUser(gitUser) || 'unknown'
|
|
455
|
+
const lockPath = join(quicklogDir, `.QUICKLOG-${user}.md.lock`)
|
|
441
456
|
const result = sanitizeResult(resultText)
|
|
442
457
|
const linked = Array.isArray(linkedChanges) ? linkedChanges : []
|
|
443
458
|
// 实际改动文件(调用方 complete-handlers 已用 isQuickMetadata 过滤 quick 自身元数据)。空 → 不动文件行。
|
package/src/run/command.js
CHANGED
|
@@ -23,7 +23,7 @@ import { join, resolve, dirname } from 'node:path'
|
|
|
23
23
|
import { existsSync, readdirSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'node:fs'
|
|
24
24
|
import { randomBytes, randomUUID } from 'node:crypto'
|
|
25
25
|
import { writeAtomicSync } from '../fs-atomic.js'
|
|
26
|
-
import { resolveSpecDir, countAncestorSpecDirs, resolveChangeDir, triggerSync, getStageSteps, formatWaitOptions, checkApproval, didYouMean, assertSafeChangeName, detectQuickSessionDrift, detectWorktreeSpecDrift, resolveRuntimeRoot } from './shared.js'
|
|
26
|
+
import { resolveSpecDir, countAncestorSpecDirs, resolveChangeDir, triggerSync, getStageSteps, formatWaitOptions, checkApproval, didYouMean, assertSafeChangeName, detectQuickSessionDrift, detectWorktreeSpecDrift, resolveRuntimeRoot, writePlatformPointer, checkPlatformManaged, PLATFORM_MANAGED_FILENAME } from './shared.js'
|
|
27
27
|
import { resolveQuickLinkedChanges } from './quick-audit.js'
|
|
28
28
|
import { outputStep } from './prompt.js'
|
|
29
29
|
import { completeStep, skipStep, waitStep, continueStep } from './complete.js'
|
|
@@ -125,6 +125,31 @@ export async function ensureStageSteps(progress, stageName, cwd, specDir = null)
|
|
|
125
125
|
return false
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
/**
|
|
129
|
+
* run <stage> --help/-h 的用法帮助(runCommand 内 --help 短路调用)。
|
|
130
|
+
* 只打印不落盘:帮助查询不该有副作用(建会话/写 QUICKLOG)。
|
|
131
|
+
*/
|
|
132
|
+
function printStageUsage(stageName) {
|
|
133
|
+
const stage = stageRegistry[stageName]
|
|
134
|
+
console.log(`
|
|
135
|
+
sillyspec run ${stageName}${stage && stage.title ? ` — ${stage.title}` : ''}${stage && stage.description ? `\n ${stage.description}` : ''}
|
|
136
|
+
|
|
137
|
+
用法:
|
|
138
|
+
sillyspec run ${stageName} 开始/继续该阶段(输出下一步 prompt)
|
|
139
|
+
sillyspec run ${stageName} --status 查看阶段进度
|
|
140
|
+
sillyspec run ${stageName} --done --output "摘要" [--input "用户原话"] 完成当前步骤
|
|
141
|
+
sillyspec run ${stageName} --change <名> 指定变更(多活跃变更必填)
|
|
142
|
+
|
|
143
|
+
通用参数: --done --output --input --status --skip --reset --reopen --from-step
|
|
144
|
+
--wait --continue --answer --change --spec-dir --non-interactive
|
|
145
|
+
--interactive --skip-approval --json(不支持)
|
|
146
|
+
quick 专属: --linked-changes none|a,b --files a.js,b.js --allow-new
|
|
147
|
+
--allow-delete --force-baseline --confirm --file-notes
|
|
148
|
+
scan 专属: --quick --standard --deep --force-rescan
|
|
149
|
+
archive专属: --confirm
|
|
150
|
+
`)
|
|
151
|
+
}
|
|
152
|
+
|
|
128
153
|
/**
|
|
129
154
|
* sillyspec run <stage> 主命令
|
|
130
155
|
*/
|
|
@@ -289,31 +314,27 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
289
314
|
}
|
|
290
315
|
}
|
|
291
316
|
}
|
|
317
|
+
// ── 平台接管声明 fail-closed(入口二,D-B@v2)──
|
|
318
|
+
// runCommand 有独立指针恢复链(不经 resolvePlatformSpecDir),指针缺失时若不在此
|
|
319
|
+
// 封堵,run/quick/scan 等全部 stage 命令会静默落本地库(与入口一行为分裂)。
|
|
320
|
+
// 触发条件:命令行没传平台参数 + 指针与 platform-scan.json 皆未命中 + 接管声明存在。
|
|
321
|
+
// 显式 --spec-dir/--spec-root 传参时 platformOpts.specRoot 已赋值,天然不进此分支(逃生口)。
|
|
322
|
+
if (!platformOpts.specRoot && !platformOpts.runtimeRoot && !platformFileExists) {
|
|
323
|
+
const decl = checkPlatformManaged(cwd)
|
|
324
|
+
if (decl) {
|
|
325
|
+
console.error(`❌ 平台接管声明生效:本项目已由平台托管(原 specRoot: ${decl.specRoot || '(未记录)'}),但恢复指针缺失,拒绝静默回退本地模式。`)
|
|
326
|
+
console.error(` 声明文件: ${join(cwd, PLATFORM_MANAGED_FILENAME)}`)
|
|
327
|
+
console.error(' 恢复:① 重跑平台 scan/init(带 --spec-root)重建指针;② 确认不再使用平台:sillyspec platform disconnect(删除接管声明);③ 显式 --spec-dir <路径> 临时指定目录。')
|
|
328
|
+
// exit(1) 而非邻近环境错的 exit(2):这是"状态保护阻断"(对齐 PointerUnreachableError
|
|
329
|
+
// 顶层 catch 语义),非"用法/环境错"——见 design.md §5.3 v2 复审观察 (a)
|
|
330
|
+
process.exit(1)
|
|
331
|
+
}
|
|
332
|
+
}
|
|
292
333
|
// 持久化 platformOpts
|
|
293
|
-
// 在 specRoot/.runtime/ 写主文件,同时在 cwd
|
|
334
|
+
// 在 specRoot/.runtime/ 写主文件,同时在 cwd 下写恢复指针
|
|
335
|
+
// (writePlatformPointer 收敛双写逻辑,cmdInit 平台模式共用,防两处字段漂移)
|
|
294
336
|
if (platformOpts.specRoot || platformOpts.runtimeRoot) {
|
|
295
|
-
|
|
296
|
-
const { mkdirSync, writeFileSync } = await import('fs')
|
|
297
|
-
mkdirSync(join(specRoot, '.runtime'), { recursive: true })
|
|
298
|
-
writeFileSync(platformOptsFile, JSON.stringify({
|
|
299
|
-
specRoot: platformOpts.specRoot,
|
|
300
|
-
runtimeRoot: platformOpts.runtimeRoot,
|
|
301
|
-
workspaceId: platformOpts.workspaceId,
|
|
302
|
-
scanRunId: platformOpts.scanRunId,
|
|
303
|
-
savedAt: new Date().toISOString(),
|
|
304
|
-
}, null, 2) + '\n')
|
|
305
|
-
// 恢复指针:在 cwd 下写 .sillyspec-platform.json(不在 .sillyspec 内,不污染源码结构)
|
|
306
|
-
// 供后续 --done(不带 --spec-root)找到 specDir
|
|
307
|
-
writeFileSync(join(cwd, '.sillyspec-platform.json'), JSON.stringify({
|
|
308
|
-
specRoot: platformOpts.specRoot,
|
|
309
|
-
runtimeRoot: platformOpts.runtimeRoot,
|
|
310
|
-
workspaceId: platformOpts.workspaceId,
|
|
311
|
-
scanRunId: platformOpts.scanRunId,
|
|
312
|
-
savedAt: new Date().toISOString(),
|
|
313
|
-
}, null, 2) + '\n')
|
|
314
|
-
} catch {
|
|
315
|
-
// 静默失败,不影响主流程
|
|
316
|
-
}
|
|
337
|
+
writePlatformPointer(cwd, platformOpts)
|
|
317
338
|
}
|
|
318
339
|
|
|
319
340
|
// 统一规范基路径:平台模式用 specRoot,本地模式用 cwd/.sillyspec
|
|
@@ -513,6 +534,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
513
534
|
'--json', '--dir', '--help',
|
|
514
535
|
'--reopen', '--from-step', '--mode',
|
|
515
536
|
'--deep', '--quick', '--standard', // scan profile 三档显式选择(scan-profile.js 从 argv 读;互斥见下方 PROFILE_FLAGS 检测)
|
|
537
|
+
'-h',
|
|
516
538
|
])
|
|
517
539
|
for (let i = 0; i < flags.length; i++) {
|
|
518
540
|
const f = flags[i]
|
|
@@ -531,6 +553,14 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
531
553
|
}
|
|
532
554
|
}
|
|
533
555
|
|
|
556
|
+
// ── --help/-h 短路:flag 校验通过后、任何副作用(cwd 纠正/会话创建/QUICKLOG 落盘)之前 ──
|
|
557
|
+
// --help 此前在 knownFlags 白名单里被静默吞掉,run quick --help 会误开 quick 会话+写骨架
|
|
558
|
+
// 条目(查询意图不该有副作用);-h 更是被当未知参数 exit 2。帮助查询 = 用法展示 = 退出 0。
|
|
559
|
+
if (flags.includes('--help') || flags.includes('-h')) {
|
|
560
|
+
printStageUsage(stageName)
|
|
561
|
+
process.exit(0)
|
|
562
|
+
}
|
|
563
|
+
|
|
534
564
|
const isAuxiliary = auxiliaryStages.includes(stageName)
|
|
535
565
|
// scan 元数据追踪(存储在 stageData.scanMeta 中,completeStep 通过 progress 访问)
|
|
536
566
|
|