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.
Files changed (62) hide show
  1. package/.claude/skills/sillyspec-archive/SKILL.md +1 -1
  2. package/.claude/skills/sillyspec-execute/SKILL.md +1 -1
  3. package/.claude/skills/sillyspec-knowledge/SKILL.md +1 -1
  4. package/.claude/skills/sillyspec-plan/SKILL.md +5 -5
  5. package/.claude/skills/sillyspec-quick/SKILL.md +5 -1
  6. package/README.md +7 -5
  7. package/SKILL.md +5 -4
  8. package/package.json +1 -1
  9. package/packages/dashboard/server/executor.js +10 -1
  10. package/packages/dashboard/server/index.js +108 -46
  11. package/src/config-schema.js +3 -0
  12. package/src/contract-matrix.js +22 -13
  13. package/src/db.js +24 -4
  14. package/src/dispatch/probe.js +99 -63
  15. package/src/docs-check.js +58 -13
  16. package/src/docs-debt.js +6 -2
  17. package/src/docs-gate.js +1 -0
  18. package/src/doctor-diagnostics.js +208 -21
  19. package/src/fs-atomic.js +3 -1
  20. package/src/git-helper.js +7 -4
  21. package/src/hooks/worktree-guard.js +8 -2
  22. package/src/index.js +200 -27
  23. package/src/init.js +3 -2
  24. package/src/local-detect.js +9 -1
  25. package/src/modules.js +7 -5
  26. package/src/progress/stage-machine.js +589 -578
  27. package/src/progress/step-store.js +1 -1
  28. package/src/progress.js +187 -30
  29. package/src/quicklog.js +47 -16
  30. package/src/run/command.js +88 -46
  31. package/src/run/complete-handlers.js +27 -15
  32. package/src/run/complete.js +1186 -1121
  33. package/src/run/concurrent-detect.js +3 -1
  34. package/src/run/gates.js +172 -25
  35. package/src/run/prompt.js +17 -9
  36. package/src/run/scan-profile.js +35 -23
  37. package/src/run/shared.js +164 -63
  38. package/src/run/stage.js +41 -1
  39. package/src/setup.js +8 -10
  40. package/src/sillyhub-mcp/client.js +74 -6
  41. package/src/spec-dir-typo.js +3 -24
  42. package/src/spec-sync.js +43 -8
  43. package/src/stage-contract.js +12 -38
  44. package/src/stage-review.js +40 -1
  45. package/src/stages/archive.js +4 -3
  46. package/src/stages/brainstorm-auto.js +1 -1
  47. package/src/stages/brainstorm.js +27 -13
  48. package/src/stages/doctor.js +29 -16
  49. package/src/stages/execute.js +170 -154
  50. package/src/stages/knowledge.js +18 -3
  51. package/src/stages/plan-postcheck.js +72 -37
  52. package/src/stages/plan.js +70 -48
  53. package/src/stages/quick.js +3 -1
  54. package/src/stages/verify.js +1 -1
  55. package/src/sync.js +339 -225
  56. package/src/task-review.js +281 -64
  57. package/src/taskcard.js +178 -0
  58. package/src/verify-postcheck.js +48 -35
  59. package/src/worktree-apply.js +58 -34
  60. package/src/worktree-deps.js +120 -20
  61. package/src/worktree.js +55 -9
  62. 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: AbortSignal.timeout(10000),
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: AbortSignal.timeout(30000),
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
- console.warn(
305
- `[spec-sync] 检测到冲突,请人工拍板。服务器版本: ${JSON.stringify(body.server_versions || {})}`
306
- );
307
- return { synced: 0, conflict: true, serverVersions: body.server_versions };
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 };
@@ -7,7 +7,7 @@
7
7
 
8
8
  import { existsSync, readdirSync, readFileSync } from 'fs'
9
9
  import { join, basename } from 'path'
10
- import { execFileSync } from 'child_process'
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/<date>-<name>/。
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
- // 归档目录名恒为 <归档日>-<纯描述>(archiveDestDirName 写入时已 strip 源名前导日期)。
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
- * 计算归档目标目录名:保证 archive/ 下目录有且仅有一个日期前缀。
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
- const stripped = String(changeName).replace(/^\d{4}-\d{2}-\d{2}-/, '')
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 date = new Date().toISOString().slice(0, 10)
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
- try {
588
- const out = execFileSync('git', args, {
589
- cwd: dir,
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
  /**
@@ -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) {
@@ -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
- - plan.md 的 - [x] checkbox **仅作显示态参考**:它依赖自动回填,runId marker / review 缺失时会停在未勾态,与客观 verdict 不一致时**以本报告为准**,不要被未勾 checkbox 误导。
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. 确保所有 checkbox 都已勾选
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**:任务列表(只列名称,细节在 plan 阶段展开)
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
 
@@ -218,32 +218,44 @@ export const definition = {
218
218
  migratedFrom: ['HTML 原型生成'],
219
219
  requiresWait: true,
220
220
  waitReason: '等待用户确认设计方案',
221
- waitOptions: ['确认', '需要修改', '推翻重来'],
222
- prompt: `展示完整设计方案供用户确认;如适合可视化,顺带生成 HTML 原型。
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
- ### HTML 原型生成(适合可视化时)
232
- 判断本次设计是否适合生成 HTML 原型:
233
- - 适合:有 UI 组件/布局/交互流程/状态转换/架构图
234
- - 不适合:纯后端逻辑/配置修改/无可视化意义
235
- 如果适合,生成一个独立的 HTML 文件(内联 CSS + JS),保存到:
236
- \`{SPEC_ROOT}/changes/<change-name>/prototype-<名称>.html\`
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 时生成骨架,plan 阶段展开)
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
@@ -12,7 +12,7 @@ export const definition = {
12
12
  name: 'SillySpec 内部检查',
13
13
  prompt: `运行 SillySpec 内部检查。逐项执行以下命令并汇总结果:
14
14
 
15
- > ⚠️ Windows 兼容:下列 sqlite3 查询依赖外部 sqlite3 CLI(Windows 默认没有)。若某条 sqlite3 失败,改用 sillyspec 自身命令替代——sillyspec progress show 查看当前变更与各阶段状态(等价于查 changes/stages 表),sillyspec doctor --json 查看 db 健康与活跃变更列表。
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
- ACTIVE_CHANGE=$(sqlite3 "$DB_FILE" "SELECT name FROM changes WHERE status='active' ORDER BY last_active DESC LIMIT 1" 2>/dev/null)
51
- if [ -z "$ACTIVE_CHANGE" ]; then echo "ℹ️ 无当前变更"; exit 0; fi
52
- echo "当前变更: $ACTIVE_CHANGE"
53
- DIR=".sillyspec/changes/$ACTIVE_CHANGE"
54
- [ -d "$DIR" ] && echo "✅ 变更目录存在: $ACTIVE_CHANGE" || echo "❌ 变更目录不存在: $ACTIVE_CHANGE"
55
- # 检查各阶段状态
56
- sqlite3 -header -column "$DB_FILE" "SELECT stage, status FROM stages s JOIN changes c ON s.change_id=c.id WHERE c.name='$ACTIVE_CHANGE' ORDER BY s.stage" 2>/dev/null || echo "⚠️ 无法查询阶段数据"
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 rows = execSync('sqlite3 -json \".sillyspec/.runtime/sillyspec.db\" \"SELECT name FROM changes WHERE status=\\\"active\\\"\"', {encoding:'utf8'}).trim(); known = new Set(JSON.parse(rows).map(r => r.name)); } catch { known = new Set(); }
70
- subs.forEach(s => {
71
- console.log(known.has(s) ? '✅ ' + s + ' — 已关联' : '⚠️ ' + s + ' — 孤儿目录(可清理)');
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
- sqlite3 -header -column "$DB_FILE" "SELECT name, isolation_status AS status, isolation_mode AS mode, isolation_reason AS reason FROM changes WHERE status='active'" 2>/dev/null || echo "⚠️ 查询 isolation 失败"
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 不存在(尚未初始化)"