sillyspec 3.26.11 → 3.26.14
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-archive/SKILL.md +1 -1
- 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 +5 -5
- package/.claude/skills/sillyspec-quick/SKILL.md +5 -1
- package/README.md +7 -5
- package/SKILL.md +5 -4
- package/package.json +1 -1
- package/packages/dashboard/server/executor.js +10 -1
- package/packages/dashboard/server/index.js +108 -46
- package/src/config-schema.js +3 -0
- package/src/contract-matrix.js +22 -13
- package/src/db.js +24 -4
- package/src/dispatch/probe.js +99 -63
- package/src/docs-check.js +58 -13
- package/src/docs-debt.js +6 -2
- package/src/docs-gate.js +1 -0
- package/src/doctor-diagnostics.js +208 -21
- package/src/fs-atomic.js +3 -1
- package/src/git-helper.js +7 -4
- package/src/hooks/worktree-guard.js +8 -2
- package/src/index.js +200 -27
- package/src/init.js +3 -2
- package/src/local-detect.js +9 -1
- package/src/modules.js +7 -5
- package/src/progress/stage-machine.js +589 -578
- package/src/progress/step-store.js +1 -1
- package/src/progress.js +187 -30
- package/src/quicklog.js +47 -16
- package/src/run/command.js +88 -46
- package/src/run/complete-handlers.js +27 -15
- package/src/run/complete.js +1186 -1121
- package/src/run/concurrent-detect.js +3 -1
- package/src/run/gates.js +172 -25
- package/src/run/prompt.js +17 -9
- package/src/run/scan-profile.js +35 -23
- package/src/run/shared.js +164 -63
- package/src/run/stage.js +41 -1
- package/src/setup.js +8 -10
- package/src/sillyhub-mcp/client.js +74 -6
- package/src/spec-dir-typo.js +3 -24
- package/src/spec-sync.js +43 -8
- package/src/stage-contract.js +12 -38
- package/src/stage-review.js +40 -1
- package/src/stages/archive.js +4 -3
- package/src/stages/brainstorm-auto.js +1 -1
- package/src/stages/brainstorm.js +27 -13
- package/src/stages/doctor.js +29 -16
- package/src/stages/execute.js +170 -154
- package/src/stages/knowledge.js +18 -3
- package/src/stages/plan-postcheck.js +72 -37
- package/src/stages/plan.js +70 -48
- package/src/stages/quick.js +3 -1
- package/src/stages/verify.js +1 -1
- package/src/sync.js +339 -225
- package/src/task-review.js +281 -64
- package/src/taskcard.js +178 -0
- package/src/verify-postcheck.js +48 -35
- package/src/worktree-apply.js +58 -34
- package/src/worktree-deps.js +120 -20
- package/src/worktree.js +55 -9
- package/.npmrc-publish +0 -2
package/src/spec-sync.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { createHash } from 'crypto';
|
|
14
|
-
import { existsSync, readdirSync, readFileSync, statSync } from 'fs';
|
|
14
|
+
import { existsSync, readdirSync, readFileSync, statSync, mkdirSync, writeFileSync } from 'fs';
|
|
15
15
|
import { join, relative, sep } from 'path';
|
|
16
16
|
|
|
17
17
|
// 与 sillyhub-daemon/src/spec-sync.ts 共用排除口径(task-07 / FR-06 / D-008@v2):
|
|
@@ -127,6 +127,19 @@ export function computeSpecOps(serverManifest, localFiles) {
|
|
|
127
127
|
const serverPaths = Object.keys(server).filter((p) => server[p]?.exists !== false);
|
|
128
128
|
const serverSet = new Set(serverPaths);
|
|
129
129
|
|
|
130
|
+
// fail-closed 护栏(2026-08-20 全量体检 BUG-01):本地树为空而服务器非空时,
|
|
131
|
+
// 全量 delete 几乎必然是「锚错了目录」(如平台模式 cwd/.sillyspec 只剩 local.yaml
|
|
132
|
+
// 被 walk 排除),而不是用户真的删光了所有文件——一旦发出就是服务器整树清空。
|
|
133
|
+
// 唯一放行:服务器只剩 local.yaml 存量(顶层单文件),保留「delete 清 token 存量」
|
|
134
|
+
// 的既定语义(见 UPLOAD_EXCLUDE_FILENAMES 注释)。真实清空需求需本地树非空逐轮收敛。
|
|
135
|
+
const nonTrivialServer = serverPaths.filter((p) => p !== 'local.yaml');
|
|
136
|
+
if (localFiles.length === 0 && nonTrivialServer.length > 0) {
|
|
137
|
+
console.warn(
|
|
138
|
+
`[spec-sync] 本地 spec 树为空但服务器有 ${serverPaths.length} 个文件,跳过同步(防误清空;请检查 spec 目录锚点)`
|
|
139
|
+
);
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
|
|
130
143
|
// rename 检测:旧路径(服务器有、本地无)↔ 新路径(本地有、服务器无)hash 相同
|
|
131
144
|
const renames = [];
|
|
132
145
|
const consumedNew = new Set();
|
|
@@ -243,11 +256,13 @@ function collectChangeDir(dirs, p) {
|
|
|
243
256
|
* @param {string} changeName - 当前变更名
|
|
244
257
|
* @returns {Promise<{synced: number, conflict?: boolean, serverVersions?: object}>}
|
|
245
258
|
*/
|
|
246
|
-
export async function syncSpecTree(specRoot, platform, changeName) {
|
|
259
|
+
export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
|
|
247
260
|
if (!platform || !platform.url || !platform.token) {
|
|
248
261
|
debugLog('[spec-sync] 未连接平台(本地合法状态);跳过 spec 树增量同步');
|
|
249
262
|
return { synced: 0 };
|
|
250
263
|
}
|
|
264
|
+
// HUB-09:单请求超时与外部熔断 signal 合并——trigger* 熔断时在飞请求被真实取消
|
|
265
|
+
const withSignal = (timeoutMs) => (opts.signal ? AbortSignal.any([AbortSignal.timeout(timeoutMs), opts.signal]) : AbortSignal.timeout(timeoutMs));
|
|
251
266
|
|
|
252
267
|
const manifestUrl = `${platform.url.replace(/\/$/, '')}/api/changes/-/spec-manifest`;
|
|
253
268
|
const syncUrl = `${platform.url.replace(/\/$/, '')}/api/changes/-/spec-sync`;
|
|
@@ -257,7 +272,7 @@ export async function syncSpecTree(specRoot, platform, changeName) {
|
|
|
257
272
|
try {
|
|
258
273
|
const res = await fetch(manifestUrl, {
|
|
259
274
|
headers: { Authorization: `Bearer ${platform.token}` },
|
|
260
|
-
signal:
|
|
275
|
+
signal: withSignal(10000),
|
|
261
276
|
});
|
|
262
277
|
if (!res.ok) {
|
|
263
278
|
// ql-20260818-008:debugLog → console.warn。树同步失败原本完全静默(要
|
|
@@ -293,7 +308,7 @@ export async function syncSpecTree(specRoot, platform, changeName) {
|
|
|
293
308
|
Authorization: `Bearer ${platform.token}`,
|
|
294
309
|
},
|
|
295
310
|
body: JSON.stringify({ ops }),
|
|
296
|
-
signal:
|
|
311
|
+
signal: withSignal(30000),
|
|
297
312
|
});
|
|
298
313
|
if (!res.ok) {
|
|
299
314
|
console.warn(`[spec-sync] 同步请求失败 HTTP ${res.status}(文件树本次未同步,下次自动重试): ${changeName}`);
|
|
@@ -301,10 +316,30 @@ export async function syncSpecTree(specRoot, platform, changeName) {
|
|
|
301
316
|
}
|
|
302
317
|
const body = await res.json().catch(() => ({}));
|
|
303
318
|
if (body.conflict) {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
319
|
+
// HUB-08 冲突闭环:落 spec-sync-conflict-<change>.json(与进度 sync-conflict-* 同目录),
|
|
320
|
+
// 供 platform status 列出 + platform resolve 三态处置。此前只 warn 返回,下次 sync 用
|
|
321
|
+
// 同一 base_version 继续冲突循环,无人工裁决入口。
|
|
322
|
+
const serverVersions = body.server_versions || {};
|
|
323
|
+
let conflictPath = null;
|
|
324
|
+
try {
|
|
325
|
+
const runtimeDir = join(specRoot, '.runtime');
|
|
326
|
+
mkdirSync(runtimeDir, { recursive: true });
|
|
327
|
+
conflictPath = join(runtimeDir, `spec-sync-conflict-${changeName}.json`);
|
|
328
|
+
writeFileSync(conflictPath, JSON.stringify({
|
|
329
|
+
change: changeName,
|
|
330
|
+
kind: 'spec-tree',
|
|
331
|
+
created_at: new Date().toISOString(),
|
|
332
|
+
server_versions: serverVersions,
|
|
333
|
+
conflicting_paths: Object.keys(serverVersions),
|
|
334
|
+
note: 'spec 树文件冲突(服务器版本领先于本地 diff 基线)。resolve --keep-local 以本地为准重推;--take-platform 暂不支持(平台无文件下载端点)',
|
|
335
|
+
}, null, 2) + '\n', 'utf8');
|
|
336
|
+
} catch (e) {
|
|
337
|
+
console.warn(`[spec-sync] 冲突文件写入失败(冲突信息仅打印): ${e.message}`);
|
|
338
|
+
}
|
|
339
|
+
console.warn('');
|
|
340
|
+
console.warn(`⚠️ [spec-sync] 检测到 spec 树冲突(${Object.keys(serverVersions).length} 个文件,服务器版本: ${JSON.stringify(serverVersions)})`);
|
|
341
|
+
console.warn(`⚠️ 处置:sillyspec platform resolve ${changeName} --keep-local | --abort(冲突详情: ${conflictPath || '(写入失败)'})`);
|
|
342
|
+
return { synced: 0, conflict: true, serverVersions, conflictPath };
|
|
308
343
|
}
|
|
309
344
|
console.log(`[spec-sync] 已同步 ${ops.length} 个文件变更: ${changeName}`);
|
|
310
345
|
return { synced: ops.length };
|
package/src/stage-contract.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { existsSync, readdirSync, readFileSync } from 'fs'
|
|
9
9
|
import { join, basename } from 'path'
|
|
10
|
-
import {
|
|
10
|
+
import { safeGit } from './git-helper.js'
|
|
11
11
|
import { detectChangeRisk, checkIntegrationEvidence, VERIFICATION_NEEDS, RISK_LEVEL_CAUSES } from './change-risk-profile.js'
|
|
12
12
|
import { SCAN_REQUIRED_DOCS, AUXILIARY_STAGES } from './constants.js'
|
|
13
13
|
import { evaluateRules } from './stage-contract-engine.js'
|
|
@@ -61,22 +61,12 @@ export function validateChangeExists(specBase, stageName, changeName) {
|
|
|
61
61
|
if (/^quick-[0-9a-f]{8}$/.test(changeName)) return null
|
|
62
62
|
if (existsSync(join(specBase, 'changes', changeName))) return null
|
|
63
63
|
|
|
64
|
-
// archive 特例:step4 --confirm 已把变更从 changes/<name>/ 移到 changes/archive/<
|
|
64
|
+
// archive 特例:step4 --confirm 已把变更从 changes/<name>/ 移到 changes/archive/<name>/。
|
|
65
65
|
// step5(更新路线图/提交)--change <name> 不应因 changes/<name>/ 已移走而被前置校验误拦。
|
|
66
|
-
// 精确匹配归档目录名(YYYY-MM-DD-<changeName>),避免后缀子串误匹配(auth ≠ deep-auth)。
|
|
67
66
|
if (stageName === 'archive') {
|
|
68
67
|
const archiveRoot = join(specBase, 'changes', 'archive')
|
|
69
|
-
if (existsSync(archiveRoot)) {
|
|
70
|
-
|
|
71
|
-
// 校验侧同样 strip changeName 前导日期再比 —— 否则「写 strip、读拿完整源名」语义不一致,
|
|
72
|
-
// 会让带日期前缀的源名(brainstorm 约定,如 2026-07-29-sidebar-menu-restructure)在 step5
|
|
73
|
-
// 被前置校验误拦(坑 archive-step5-leading-date-change-name)。
|
|
74
|
-
const stripped = String(changeName).replace(/^\d{4}-\d{2}-\d{2}-/, '')
|
|
75
|
-
const archived = readdirSync(archiveRoot).find(d => {
|
|
76
|
-
const m = d.match(/^\d{4}-\d{2}-\d{2}-(.+)$/)
|
|
77
|
-
return m && m[1] === stripped
|
|
78
|
-
})
|
|
79
|
-
if (archived) return null
|
|
68
|
+
if (existsSync(archiveRoot) && existsSync(join(archiveRoot, changeName))) {
|
|
69
|
+
return null
|
|
80
70
|
}
|
|
81
71
|
}
|
|
82
72
|
return {
|
|
@@ -527,19 +517,10 @@ function validateVerifyOutputs(cwd, changeName, context = {}) {
|
|
|
527
517
|
}
|
|
528
518
|
|
|
529
519
|
/**
|
|
530
|
-
*
|
|
531
|
-
*
|
|
532
|
-
* 变更名创建时常已带 YYYY-MM-DD- 前缀(brainstorm 强制约定 + CLI 自动占位名),
|
|
533
|
-
* 若归档时再无条件拼一次 date- 前缀,会产生 2026-07-28-2026-07-28-<desc> 双日期。
|
|
534
|
-
* 这里先剥离源名前导日期,再统一拼归档当天日期 → 归档目录名恒为 <归档日期>-<纯描述>。
|
|
535
|
-
* 不带日期的源名(如 quick-<hash>)strip 无效,正常补日期,行为不变。
|
|
536
|
-
*
|
|
537
|
-
* 写(archiveChangeDirectory)与校验(validateArchiveOutputs)共用此函数,
|
|
538
|
-
* 避免「写和校验各自硬编码同公式」的漂移(曾因此导致双日期 bug 写校验都自洽通过)。
|
|
520
|
+
* 计算归档目标目录名:保持原变更名不变,直接移入 archive/。
|
|
539
521
|
*/
|
|
540
522
|
export function archiveDestDirName(date, changeName) {
|
|
541
|
-
|
|
542
|
-
return `${date}-${stripped}`
|
|
523
|
+
return String(changeName)
|
|
543
524
|
}
|
|
544
525
|
|
|
545
526
|
/**
|
|
@@ -547,8 +528,7 @@ export function archiveDestDirName(date, changeName) {
|
|
|
547
528
|
*/
|
|
548
529
|
function validateArchiveOutputs(cwd, changeName) {
|
|
549
530
|
const archiveDir = join(cwd, '.sillyspec', 'changes', 'archive')
|
|
550
|
-
const
|
|
551
|
-
const destDir = join(archiveDir, archiveDestDirName(date, changeName))
|
|
531
|
+
const destDir = join(archiveDir, changeName)
|
|
552
532
|
|
|
553
533
|
// 归档目录不存在 early return(引擎在存在时才跑)
|
|
554
534
|
if (!existsSync(destDir)) {
|
|
@@ -583,18 +563,12 @@ function validateChangeClosed(cwd, changeName) {
|
|
|
583
563
|
|
|
584
564
|
// ============ Execute 代码变更客观核验 ============
|
|
585
565
|
|
|
566
|
+
// QUAL-01 收口:本地 execFileSync 裸调(无 safe.directory)→ git-helper safeGit 统一入口,
|
|
567
|
+
// {ok,out,error} 适配语义与 timeout 15000 不变
|
|
586
568
|
function gitTry(dir, args) {
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
encoding: 'utf8',
|
|
591
|
-
timeout: 15000,
|
|
592
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
593
|
-
}).trim()
|
|
594
|
-
return { ok: true, out }
|
|
595
|
-
} catch (e) {
|
|
596
|
-
return { ok: false, out: '', error: e.message?.split('\n')[0] || String(e) }
|
|
597
|
-
}
|
|
569
|
+
const r = safeGit(dir, args, { timeout: 15000 })
|
|
570
|
+
if (r.value !== null) return { ok: true, out: r.value }
|
|
571
|
+
return { ok: false, out: '', error: r.error || 'git 执行失败' }
|
|
598
572
|
}
|
|
599
573
|
|
|
600
574
|
/**
|
package/src/stage-review.js
CHANGED
|
@@ -465,7 +465,7 @@ export function printStageReviewResult(result, context = {}) {
|
|
|
465
465
|
* @returns {{ ok: boolean, reviewRunId: string, reviewPath: string, markerPath: string, mode: 'skeleton'|'adopted', mainDoc: string, review: object }}
|
|
466
466
|
* @throws {Error} changeName 空 / stage 非法 / 主文档缺失 / --from 不存在或 schema 不过
|
|
467
467
|
*/
|
|
468
|
-
export function registerStageReview({ changeName, stage, fromFile, cwd, platformOpts = {} }) {
|
|
468
|
+
export function registerStageReview({ changeName, stage, fromFile, cwd, platformOpts = {}, refreshHash = false }) {
|
|
469
469
|
if (!changeName) throw new Error('register-stage-review: changeName 不能为空')
|
|
470
470
|
if (!stage || !STAGE_MAIN_DOC[stage]) {
|
|
471
471
|
throw new Error(`register-stage-review: stage 无效 "${stage}"(应为 brainstorm|plan|execute)`)
|
|
@@ -486,6 +486,45 @@ export function registerStageReview({ changeName, stage, fromFile, cwd, platform
|
|
|
486
486
|
}
|
|
487
487
|
const reviewedFiles = [`changes/${changeName}/${mainDoc}`]
|
|
488
488
|
|
|
489
|
+
// ── refresh-hash 模式(坑 stage-review-dochash-manual-resync,2026-08-21 实证):改一版 design
|
|
490
|
+
// 要重算 2-3 个 stage review 的 docHash(brainstorm/execute 都锚 design.md),手工易漏、gate
|
|
491
|
+
// 报错才补。就地更新 marker 指向的已有 review.json——保留 verdict/checklist/requiredEvidence
|
|
492
|
+
// (结论是否仍适用由人确认),仅重算 docHash + reviewerNotes 追加刷新记录;不换 run 目录
|
|
493
|
+
// (换新 run 会把 marker 重定向到骨架,丢已审结论)。无既有 review → 报错(该用骨架模式生成)。
|
|
494
|
+
if (refreshHash) {
|
|
495
|
+
if (fromFile) throw new Error('register-stage-review: --refresh-hash 与 --from 互斥(refresh 就地更新既有 review,adopt 引入新文件)')
|
|
496
|
+
const existingRunId = getLatestStageReviewRunId(runtimeRoot, stage, changeName)
|
|
497
|
+
const existingPath = existingRunId
|
|
498
|
+
? join(runtimeRoot, 'stage-reviews', `${stage}-${existingRunId}`, 'review.json')
|
|
499
|
+
: null
|
|
500
|
+
if (!existingPath || !existsSync(existingPath)) {
|
|
501
|
+
throw new Error(`register-stage-review: --refresh-hash 找不到 ${stage} 的既有 review.json(marker ${existingRunId || '无'} 无对应 run)——先去掉 --refresh-hash 生成骨架,或派审查子代理产出 review`)
|
|
502
|
+
}
|
|
503
|
+
let existing
|
|
504
|
+
try {
|
|
505
|
+
existing = JSON.parse(readFileSync(existingPath, 'utf8'))
|
|
506
|
+
} catch (e) {
|
|
507
|
+
throw new Error(`register-stage-review: 既有 review.json 解析失败 ${existingPath}: ${e.message}`)
|
|
508
|
+
}
|
|
509
|
+
const refreshedAt = new Date().toISOString().slice(0, 19).replace('T', ' ')
|
|
510
|
+
const review = {
|
|
511
|
+
...existing,
|
|
512
|
+
docHash,
|
|
513
|
+
reviewedFiles,
|
|
514
|
+
reviewerNotes: `${existing.reviewerNotes || ''}\ndocHash refreshed at ${refreshedAt}(主文档改版后机械重算;verdict 结论是否仍适用需人工确认)`.trim(),
|
|
515
|
+
}
|
|
516
|
+
writeFileSync(existingPath, JSON.stringify(review, null, 2) + '\n')
|
|
517
|
+
const schemaRecheck = validateStageReviewSchema(review)
|
|
518
|
+
if (!schemaRecheck.ok) {
|
|
519
|
+
throw new Error(`register-stage-review: refresh 后 schema 自检失败 — ${schemaRecheck.errors.join('; ')}(原 review 字段异常,先修原文件)`)
|
|
520
|
+
}
|
|
521
|
+
const hashRecheck = verifyStageReviewDocHash(review, [specBase, changeDir, cwd])
|
|
522
|
+
if (!hashRecheck.ok) {
|
|
523
|
+
throw new Error(`register-stage-review: refresh 后 docHash 自检失败(不应发生)— ${hashRecheck.errors.join('; ')}`)
|
|
524
|
+
}
|
|
525
|
+
return { ok: true, reviewRunId: existingRunId, reviewPath: existingPath, markerPath: stageReviewMarkerPath(runtimeRoot, stage, changeName), mode: 'refreshed', mainDoc, review }
|
|
526
|
+
}
|
|
527
|
+
|
|
489
528
|
let review
|
|
490
529
|
let mode
|
|
491
530
|
if (fromFile) {
|
package/src/stages/archive.js
CHANGED
|
@@ -12,8 +12,9 @@ export const definition = {
|
|
|
12
12
|
|
|
13
13
|
### 判定规则
|
|
14
14
|
- **真相源 = review.json verdict**:上方报告由 CLI 从 .runtime/execute-runs/<runId>/tasks/task-NN/review.json 的 specVerdict + qualityVerdict 算出。
|
|
15
|
-
-
|
|
15
|
+
- tasks.md 的 - [x] checkbox **仅作显示态参考**:它依赖自动回填,runId marker / review 缺失时会停在未勾态,与客观 verdict 不一致时**以本报告为准**,不要被未勾 checkbox 误导。
|
|
16
16
|
- 报告「未通过 / 缺失」= 0 且 source 为 review.json → 完成度合格,进入下一步。
|
|
17
|
+
- 报告含「cannot_verify 草稿(未真正复核)」计数 > 0 → 这些 task 只有 cannot_verify 草稿兜底、未被真正复核:先确认 verify 阶段已兑现其 requiredEvidence(verify-result.md 有对应证据),缺证据的派独立子代理对照 task brief + git diff 补真实复核(升级为 pass/fail),勿静默放行。
|
|
17
18
|
- 报告 source 为 plan-checkbox-fallback(客观源不可用)→ 完成度无法客观确认,**必须暂停**让用户交叉核对,不要直接放行。
|
|
18
19
|
- 存在未完成任务 → **必须暂停等待用户决定**,不要自行判断"可以归档"。
|
|
19
20
|
- 调用:sillyspec run archive --wait --reason "存在未完成任务,是否继续归档" --options "继续归档,回到execute完成剩余任务" --output "未完成任务列表"
|
|
@@ -126,10 +127,10 @@ module_id: <module-id>
|
|
|
126
127
|
|
|
127
128
|
### 操作
|
|
128
129
|
1. 展示:变更目录名、包含的文件列表(含 module-impact.md)、生成总结
|
|
129
|
-
2.
|
|
130
|
+
2. 确保任务清单(tasks.md)所有 task checkbox 都已勾选
|
|
130
131
|
3. 让用户确认后,用 \`--confirm\` 完成本步骤:
|
|
131
132
|
\`sillyspec run archive --done --confirm --output "确认归档"\`
|
|
132
|
-
4. CLI 会创建 \`.sillyspec/changes/archive/\`,并将变更目录移动到 \`.sillyspec/changes/archive
|
|
133
|
+
4. CLI 会创建 \`.sillyspec/changes/archive/\`,并将变更目录移动到 \`.sillyspec/changes/archive/<原变更名>/\`
|
|
133
134
|
|
|
134
135
|
### 输出
|
|
135
136
|
归档完成 + archive 目录路径`,
|
|
@@ -217,7 +217,7 @@ AC-011: 不涉及业务规则/产品范围/默认行为/用户可见行为变更
|
|
|
217
217
|
1. 基于变更根目录下的设计产物(design.md / decisions.md / gaps.md / assumptions.md / next-action.json),生成规范文件(直接写入变更根目录):
|
|
218
218
|
- **proposal.md**:动机、关键问题、变更范围、不在范围内、成功标准
|
|
219
219
|
- **requirements.md**:角色表 + FR 编号需求 + Given/When/Then + 非功能需求
|
|
220
|
-
- **tasks.md
|
|
220
|
+
- **tasks.md**:任务列表骨架(只列名称;任务清单唯一真相,plan 阶段展开细节并写回本文件——execute 勾选与 verify 对照都在本文件)
|
|
221
221
|
2. 所有规范文件头部包含 YAML frontmatter
|
|
222
222
|
3. \`git add {SPEC_ROOT}/changes/<change-name>/\` — 暂存本变更的规范文件(勿用 .sillyspec/ 整目录——会裹挟其他活跃变更;不要 commit)
|
|
223
223
|
|
package/src/stages/brainstorm.js
CHANGED
|
@@ -218,32 +218,44 @@ export const definition = {
|
|
|
218
218
|
migratedFrom: ['HTML 原型生成'],
|
|
219
219
|
requiresWait: true,
|
|
220
220
|
waitReason: '等待用户确认设计方案',
|
|
221
|
-
waitOptions: ['确认', '需要修改', '推翻重来'],
|
|
222
|
-
prompt:
|
|
221
|
+
waitOptions: ['确认', '需要修改', '补原型', '推翻重来'],
|
|
222
|
+
prompt: `展示完整设计方案供用户确认;涉及 UI 改动时按下方分级决定是否生成 HTML 原型——原型动作必须在暂停之前落定,不能"等确认后再补"。
|
|
223
223
|
|
|
224
224
|
### 操作
|
|
225
225
|
1. 简单项目:几句话整体描述
|
|
226
226
|
2. 复杂项目:按模块/Phase 分段展示,每段 200-300 字
|
|
227
227
|
3. 展示完整设计方案(不要逐段停顿,一次性展示)
|
|
228
228
|
4. 确认变更名(格式:\`YYYY-MM-DD-<简短描述>\`,例如 \`2026-05-13-user-auth\`)
|
|
229
|
-
5.
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
-
|
|
235
|
-
|
|
236
|
-
|
|
229
|
+
5. 按「HTML 原型分级」生成原型或写出跳过原因(二选一必填,见下)
|
|
230
|
+
6. 暂停等待用户确认或修改意见
|
|
231
|
+
|
|
232
|
+
### HTML 原型分级(按改动程度,不是"是否碰前端文件")
|
|
233
|
+
判定对象是**改造后的界面相对现状的变化程度**:
|
|
234
|
+
- **必须生成**(布局/结构/流程级变化):
|
|
235
|
+
- 新增页面、视图、路由
|
|
236
|
+
- 页面布局变化:区域增删、骨架重排、信息层级调整(如单栏改多栏、列表改看板)
|
|
237
|
+
- 新交互流程:多步流程、跨页流转、状态驱动的界面变化
|
|
238
|
+
- 用户提供了截图/设计稿要求按其改造(原型即对照基准)
|
|
239
|
+
- **建议生成**(组件级变化、页面骨架不变):
|
|
240
|
+
- 表单重构、列表列调整、弹窗/抽屉流程等局部组件变化
|
|
241
|
+
- 拿不准时默认生成——宁多勿漏,原型在 execute 阶段可直接对照复用
|
|
242
|
+
- **跳过**(无需生成,但必须声明原因):
|
|
243
|
+
- 文案、颜色、间距、字号、图标等样式微调
|
|
244
|
+
- 纯后端逻辑/CLI/配置/文档,无任何界面变化
|
|
245
|
+
|
|
246
|
+
### 生成要求
|
|
247
|
+
生成一个独立的 HTML 文件(内联 CSS + JS),保存到:
|
|
248
|
+
\`{SPEC_ROOT}/changes/<change-name>/prototype-<名称>.html\`(目录不存在则先创建)
|
|
237
249
|
- 单文件,浏览器直接打开
|
|
238
250
|
- 高保真:布局/组件/交互流程按真实效果呈现,后续 execute 阶段可直接对照复用
|
|
239
251
|
- 与项目代码风格一致:优先复用项目现有技术栈/组件库/设计 token(先查 scan 文档与现有前端代码),不另起风格
|
|
240
|
-
展示给用户确认设计方向。不适合则跳过。
|
|
241
252
|
|
|
242
253
|
### 铁律 — 必须等待用户确认设计
|
|
243
254
|
- 展示完整设计方案后必须暂停等用户确认,**不要自己说"设计已充分确认"然后推进**(命令由 CLI 在下方注入)
|
|
255
|
+
- 原型生成或跳过声明必须在暂停**之前**完成——暂停是本步骤最后动作,用户确认后直接进入写 design.md,不会回头补
|
|
244
256
|
|
|
245
257
|
### 输出
|
|
246
|
-
完整设计方案 +
|
|
258
|
+
完整设计方案 + 变更名 + 原型文件路径 或 跳过原因(二选一必填;跳过时写明分级依据,用户可当场否决要求补)
|
|
247
259
|
|
|
248
260
|
### 注意
|
|
249
261
|
- 不要一次输出大段文字,按模块/Phase 分段
|
|
@@ -333,6 +345,7 @@ design.md 第一行标题必须用中文:# 设计文档(Design)— <变更
|
|
|
333
345
|
- design.md 含「自审」字面命中章节(自审 / Self-Review / Self-review)——「生成规范文件」步完成契约 brainstorm.design.self-review 硬要求,写在本步自审段即可
|
|
334
346
|
- 如存在 decisions.md,design.md 是否引用所有当前版本 D-xxx@vN
|
|
335
347
|
- 涉及 session/lease/agent_run/daemon/lifecycle 等关键词时,是否含「生命周期契约表」
|
|
348
|
+
- UI 原型核对:文件清单含前端文件(.vue/.tsx/.jsx/.svelte/.html/.css 等)且改动达到「必须生成/建议生成」原型级别(分级见「分段展示设计」步)时,变更目录应已有 \`prototype-*.html\`——没有则本步补生成;确不生成时把跳过原因记入 design.md 风险登记,不得静默缺位
|
|
336
349
|
5. 缺章节 → 补齐后重检;章节齐全 → 进入下一步(Design Grill 做语义层交叉审查)
|
|
337
350
|
|
|
338
351
|
### 输出
|
|
@@ -537,7 +550,7 @@ Then 期望结果
|
|
|
537
550
|
| D-001@v1 | FR-01 | ... |
|
|
538
551
|
\`\`\`
|
|
539
552
|
|
|
540
|
-
### tasks.md 格式要求(scale=large
|
|
553
|
+
### tasks.md 格式要求(scale=large 时生成骨架;任务清单唯一真相——plan 阶段展开细节并写回本文件)
|
|
541
554
|
\`\`\`markdown
|
|
542
555
|
---
|
|
543
556
|
author: <git-user>
|
|
@@ -548,6 +561,7 @@ created_at: <now-datetime>
|
|
|
548
561
|
- [ ] task-01: <任务名>
|
|
549
562
|
- [ ] task-02: <任务名>
|
|
550
563
|
\`\`\`
|
|
564
|
+
> 骨架只列任务名。plan 阶段会把展开后的清单**写回本文件**(checkbox 行带一句话名,可附 [model:xxx]/(depends_on: …) 标注;保留 frontmatter/标题/ql-xxx 等非 task-XX 行);execute 勾选与 verify 对照都在本文件。
|
|
551
565
|
|
|
552
566
|
### decisions.md 格式要求(仅在有 Grill/重大决策时生成)
|
|
553
567
|
\`\`\`markdown
|
package/src/stages/doctor.js
CHANGED
|
@@ -12,7 +12,7 @@ export const definition = {
|
|
|
12
12
|
name: 'SillySpec 内部检查',
|
|
13
13
|
prompt: `运行 SillySpec 内部检查。逐项执行以下命令并汇总结果:
|
|
14
14
|
|
|
15
|
-
>
|
|
15
|
+
> ✅ Windows 兼容(SS-1,2026-08-20):db 查询已全部改走 node:sqlite(node≥22.13 内置),不再依赖外部 sqlite3 CLI。若 node 脚本失败,改用 sillyspec 自身命令替代——sillyspec progress show 查看当前变更与各阶段状态,sillyspec doctor --json 查看 db 健康与活跃变更列表。
|
|
16
16
|
|
|
17
17
|
### 1. 目录结构完整性
|
|
18
18
|
\`\`\`bash
|
|
@@ -44,16 +44,22 @@ done
|
|
|
44
44
|
|
|
45
45
|
### 3. 进度数据一致性
|
|
46
46
|
\`\`\`bash
|
|
47
|
-
# 从 sillyspec.db
|
|
47
|
+
# 从 sillyspec.db 读取活跃变更并检查目录存在性(node:sqlite 主路径,Windows 无 sqlite3 CLI)
|
|
48
48
|
DB_FILE='.sillyspec/.runtime/sillyspec.db'
|
|
49
49
|
if [ ! -f "$DB_FILE" ]; then echo "⚠️ sillyspec.db 不存在"; exit 0; fi
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
50
|
+
node --input-type=module -e "
|
|
51
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
52
|
+
import { existsSync } from 'node:fs';
|
|
53
|
+
const db = new DatabaseSync(process.argv[1], { readOnly: true });
|
|
54
|
+
const row = db.prepare(\\"SELECT name FROM changes WHERE status='active' ORDER BY last_active DESC LIMIT 1\\").get();
|
|
55
|
+
if (!row) { console.log('ℹ️ 无当前变更'); process.exit(0); }
|
|
56
|
+
console.log('当前变更: ' + row.name);
|
|
57
|
+
const dir = '.sillyspec/changes/' + row.name;
|
|
58
|
+
console.log(existsSync(dir) ? '✅ 变更目录存在: ' + row.name : '❌ 变更目录不存在: ' + row.name);
|
|
59
|
+
const stages = db.prepare('SELECT s.stage AS stage, s.status AS status FROM stages s JOIN changes c ON s.change_id=c.id WHERE c.name=? ORDER BY s.stage').all(row.name);
|
|
60
|
+
if (stages.length === 0) console.log('⚠️ 无阶段数据'); else for (const s of stages) console.log(' ' + s.stage + ': ' + s.status);
|
|
61
|
+
db.close();
|
|
62
|
+
" "$DB_FILE" 2>/dev/null || echo "⚠️ 无法查询阶段数据"
|
|
57
63
|
\`\`\`
|
|
58
64
|
|
|
59
65
|
### 4. 孤儿文件检查
|
|
@@ -62,18 +68,19 @@ DB_FILE='.sillyspec/.runtime/sillyspec.db'
|
|
|
62
68
|
if [ ! -f "$DB_FILE" ]; then echo "⚠️ sillyspec.db 不存在"; exit 0; fi
|
|
63
69
|
node -e "
|
|
64
70
|
const fs = require('fs');
|
|
71
|
+
const { DatabaseSync } = require('node:sqlite');
|
|
65
72
|
const dir = '.sillyspec/changes';
|
|
66
|
-
const subs = fs.existsSync(dir) ? fs.readdirSync(dir).filter(f => { try { return fs.statSync(dir+'/'+f).isDirectory(); } catch { return false; } }) : [];
|
|
67
|
-
const { execSync } = require('child_process');
|
|
73
|
+
const subs = fs.existsSync(dir) ? fs.readdirSync(dir).filter(f => { try { return fs.statSync(dir+'/'+f).isDirectory() && f !== 'archive'; } catch { return false; } }) : [];
|
|
68
74
|
let known;
|
|
69
|
-
try { const
|
|
70
|
-
|
|
71
|
-
|
|
75
|
+
try { const db = new DatabaseSync('.sillyspec/.runtime/sillyspec.db', { readOnly: true }); known = new Set(db.prepare(\"SELECT name FROM changes WHERE status='active'\").all().map(r => r.name)); db.close(); } catch (e) { console.log('⚠️ db 读取失败: ' + e.message + '(跳过孤儿判定,不做任何清理建议)'); known = null; }
|
|
76
|
+
// known 为 null(db 不可读)时只列目录不判定孤儿,后续僵尸检查同样跳过——不守卫会 TypeError 半途崩
|
|
77
|
+
if (known) subs.forEach(s => {
|
|
78
|
+
console.log(known.has(s) ? '✅ ' + s + ' — 已关联' : '⚠️ ' + s + ' — 孤儿目录(先 dump 取证再手工归位,勿直接 rm)');
|
|
72
79
|
});
|
|
73
80
|
// 反向检查(multi-agent-review Q1):DB active 且匹配 quick-<hex> 但无目录 → 僵尸 quick 会话
|
|
74
81
|
// (quick 收尾未注销,active 行累积污染 listChanges/doctor)。新代码已修收尾,此项清理历史残留。
|
|
75
82
|
const QUICK_RE = /^quick-[0-9a-f]{8}$/;
|
|
76
|
-
const zombieQuick = [...known].filter(n => QUICK_RE.test(n) && !subs.includes(n));
|
|
83
|
+
const zombieQuick = known ? [...known].filter(n => QUICK_RE.test(n) && !subs.includes(n)) : [];
|
|
77
84
|
if (zombieQuick.length > 0) {
|
|
78
85
|
console.log('⚠️ 检测到 ' + zombieQuick.length + ' 个僵尸 quick 会话(DB active 但无目录):');
|
|
79
86
|
zombieQuick.forEach(n => console.log(' - ' + n));
|
|
@@ -132,7 +139,13 @@ DB_FILE='.sillyspec/.runtime/sillyspec.db'
|
|
|
132
139
|
if [ -f "$DB_FILE" ]; then
|
|
133
140
|
echo ""
|
|
134
141
|
echo "isolation 状态(来自 sillyspec.db):"
|
|
135
|
-
|
|
142
|
+
node -e "
|
|
143
|
+
const { DatabaseSync } = require('node:sqlite');
|
|
144
|
+
const db = new DatabaseSync(process.argv[1], { readOnly: true });
|
|
145
|
+
const rows = db.prepare(\"SELECT name, isolation_status AS status, isolation_mode AS mode, isolation_reason AS reason FROM changes WHERE status='active'\").all();
|
|
146
|
+
for (const r of rows) console.log(' ' + r.name + ' | ' + r.status + ' | ' + r.mode + ' | ' + (r.reason || ''));
|
|
147
|
+
db.close();
|
|
148
|
+
" "$DB_FILE" 2>/dev/null || echo "⚠️ 查询 isolation 失败"
|
|
136
149
|
else
|
|
137
150
|
echo ""
|
|
138
151
|
echo "ℹ️ sillyspec.db 不存在(尚未初始化)"
|