sillyspec 3.28.3 → 3.28.4

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 (56) hide show
  1. package/.claude/skills/sillyspec-auto/SKILL.md +54 -54
  2. package/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -114
  3. package/.claude/skills/sillyspec-continue/SKILL.md +17 -17
  4. package/.claude/skills/sillyspec-execute/SKILL.md +222 -222
  5. package/.claude/skills/sillyspec-knowledge/SKILL.md +40 -40
  6. package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
  7. package/.claude/skills/sillyspec-propose/SKILL.md +16 -16
  8. package/.claude/skills/sillyspec-resume/SKILL.md +21 -21
  9. package/.claude/skills/sillyspec-state/SKILL.md +25 -25
  10. package/.claude/skills/sillyspec-workspace/SKILL.md +33 -33
  11. package/AGENTS.md +24 -0
  12. package/INSTRUCTIONS.md +12 -0
  13. package/package.json +1 -1
  14. package/src/archive-delta.js +82 -0
  15. package/src/change-delete.js +2 -1
  16. package/src/change-list.js +7 -2
  17. package/src/change-risk-profile.js +77 -6
  18. package/src/config-cat.js +12 -1
  19. package/src/constants.js +3 -1
  20. package/src/docs-check.js +34 -0
  21. package/src/doctor-diagnostics.js +441 -5
  22. package/src/foreign-declared.js +53 -2
  23. package/src/index.js +181 -6
  24. package/src/module-impact.js +77 -12
  25. package/src/plan-adopt-waves.js +13 -5
  26. package/src/progress/change-registry.js +16 -0
  27. package/src/progress.js +62 -1
  28. package/src/quick-recommend.js +6 -1
  29. package/src/quicklog.js +10 -2
  30. package/src/run/command.js +30 -9
  31. package/src/run/complete-handlers.js +152 -6
  32. package/src/run/complete.js +45 -0
  33. package/src/run/gates.js +94 -6
  34. package/src/run/prompt.js +96 -5
  35. package/src/run/shared.js +74 -6
  36. package/src/run/stage.js +35 -0
  37. package/src/runtime-hygiene.js +84 -0
  38. package/src/spec-sync.js +63 -8
  39. package/src/stage-contract.js +83 -3
  40. package/src/stage-review.js +3 -0
  41. package/src/stages/archive.js +4 -4
  42. package/src/stages/brainstorm.js +2 -8
  43. package/src/stages/doctor.js +41 -515
  44. package/src/stages/plan-postcheck.js +191 -7
  45. package/src/stages/plan.js +5 -26
  46. package/src/stages/quick.js +11 -11
  47. package/src/stages/verify.js +10 -6
  48. package/src/sync-noise.js +158 -0
  49. package/src/sync.js +42 -17
  50. package/src/task-review.js +25 -1
  51. package/src/verify-facts-schema.js +141 -0
  52. package/src/verify-postcheck.js +257 -19
  53. package/src/verify-probes.js +142 -13
  54. package/src/workflow.js +6 -0
  55. package/src/worktree-apply.js +51 -13
  56. package/src/worktree.js +5 -1
package/src/sync.js CHANGED
@@ -17,6 +17,7 @@ import { safeGit } from './git-helper.js';
17
17
  import { openDatabase } from './db-engine.js';
18
18
  import { PLATFORM_MANAGED_FILENAME, QUICK_SID_RE } from './run/shared.js';
19
19
  import { syncSpecTree } from './spec-sync.js';
20
+ import { bindSyncNoiseFromCwd, syncConnectionWarn, isConnectionClassStatus, noteSyncSuccess } from './sync-noise.js';
20
21
 
21
22
  // sync 是 best-effort(网络失败只 warn):平台指针失效时不抛,跳过平台、回退本地。
22
23
  function safePlatformSpecDir(cwd) {
@@ -218,11 +219,18 @@ function combineSignals(external, timeoutMs = REQUEST_TIMEOUT_MS) {
218
219
 
219
220
  async function fetchJson(url, options = {}) {
220
221
  const signal = combineSignals(options.signal);
222
+ // noMute 是本模块的闸门开关不是 fetch 参数,先剥掉再透传
223
+ const { noMute, ...init } = options;
221
224
  try {
222
- const res = await fetch(url, { ...options, signal });
225
+ const res = await fetch(url, { ...init, signal });
223
226
  if (!res.ok) {
224
227
  const text = await res.text().catch(() => '');
225
- console.warn(`[sync] ${options.method || 'GET'} ${url} → ${res.status} ${text.slice(0, 200)}`);
228
+ const line = `[sync] ${options.method || 'GET'} ${url} → ${res.status} ${text.slice(0, 200)}`;
229
+ // 连接类失败(404=端点未就绪 / 5xx)走跨进程噪音闸:平台未就绪期每条命令的自动
230
+ // 同步都撞同一批失败,重复刷屏淹没门禁输出(见 sync-noise.js 模块头)。409 冲突与
231
+ // 其余 4xx 业务态每轮都该看见,不走闸。用户显式命令(connect health ping)传 noMute。
232
+ if (!noMute && isConnectionClassStatus(res.status)) syncConnectionWarn(line);
233
+ else console.warn(line);
226
234
  return null;
227
235
  }
228
236
  const ct = res.headers.get('content-type') || '';
@@ -231,11 +239,11 @@ async function fetchJson(url, options = {}) {
231
239
  }
232
240
  return null;
233
241
  } catch (err) {
234
- if (err.name === 'AbortError') {
235
- console.warn(`[sync] ${url} 请求超时/中断 (${REQUEST_TIMEOUT_MS}ms 上限或外部熔断)`);
236
- } else {
237
- console.warn(`[sync] ${url} 请求失败: ${err.message}`);
238
- }
242
+ const line = err.name === 'AbortError' || err.name === 'TimeoutError'
243
+ ? `[sync] ${url} 请求超时/中断 (${REQUEST_TIMEOUT_MS}ms 上限或外部熔断)`
244
+ : `[sync] ${url} 请求失败: ${err.message}`;
245
+ if (!noMute) syncConnectionWarn(line);
246
+ else console.warn(line);
239
247
  return null;
240
248
  }
241
249
  }
@@ -247,23 +255,26 @@ async function fetchJson(url, options = {}) {
247
255
  */
248
256
  async function fetchJsonWithStatus(url, options = {}) {
249
257
  const signal = combineSignals(options.signal);
258
+ const { noMute, ...init } = options;
250
259
  try {
251
- const res = await fetch(url, { ...options, signal });
260
+ const res = await fetch(url, { ...init, signal });
252
261
  const text = await res.text().catch(() => '');
253
262
  let body = null;
254
263
  if (text) {
255
264
  try { body = JSON.parse(text); } catch { body = null; }
256
265
  }
257
266
  if (!res.ok) {
258
- console.warn(`[sync] ${options.method || 'GET'} ${url} → ${res.status} ${text.slice(0, 200)}`);
267
+ const line = `[sync] ${options.method || 'GET'} ${url} → ${res.status} ${text.slice(0, 200)}`;
268
+ if (!noMute && isConnectionClassStatus(res.status)) syncConnectionWarn(line);
269
+ else console.warn(line);
259
270
  }
260
271
  return { ok: res.ok, status: res.status, body };
261
272
  } catch (err) {
262
- if (err.name === 'AbortError') {
263
- console.warn(`[sync] ${url} 请求超时/中断 (${REQUEST_TIMEOUT_MS}ms 上限或外部熔断)`);
264
- } else {
265
- console.warn(`[sync] ${url} 请求失败: ${err.message}`);
266
- }
273
+ const line = err.name === 'AbortError' || err.name === 'TimeoutError'
274
+ ? `[sync] ${url} 请求超时/中断 (${REQUEST_TIMEOUT_MS}ms 上限或外部熔断)`
275
+ : `[sync] ${url} 请求失败: ${err.message}`;
276
+ if (!noMute) syncConnectionWarn(line);
277
+ else console.warn(line);
267
278
  return { ok: false, status: 0, body: null };
268
279
  }
269
280
  }
@@ -399,6 +410,12 @@ export class SyncManager {
399
410
  // 归档尾声静默化旗标(坑 post-archive-sync-noise):sync() 探测到已归档时置位,
400
411
  // 链内 syncDocuments 的「变更不存在」降 debug(每次 sync 调用重置)
401
412
  this._suppressDocsMissingWarn = false;
413
+ // 绑定连接类失败噪音闸的 marker 目录(sync-noise.js):本实例所有 fetch 失败 warn 的
414
+ // 跨进程静默窗口落这里。bindSyncNoiseFromCwd 零副作用(只读指针 JSON,不触发
415
+ // resolvePlatformSpecDir 的指针守卫 warn——doctor 两路字节一致契约)。
416
+ try {
417
+ bindSyncNoiseFromCwd(cwd);
418
+ } catch { /* 未绑定 = 闸门直通(多一行 warn,无正确性影响) */ }
402
419
  }
403
420
 
404
421
  /**
@@ -423,7 +440,8 @@ export class SyncManager {
423
440
  console.warn(`[sync] platform.url 非 https(${normalizedUrl.slice(0, 60)}),同步 token 将明文传输——请确认为受控内网环境`);
424
441
  }
425
442
  const healthUrl = `${normalizedUrl}/api/health`;
426
- const result = await fetchJson(healthUrl);
443
+ // noMute:connect 是用户显式命令,health ping 的失败细节不走静默闸(用户正在诊断连接)
444
+ const result = await fetchJson(healthUrl, { noMute: true });
427
445
  if (result === null) {
428
446
  console.warn(`[sync] 平台连接验证失败: ${url}`);
429
447
  return;
@@ -661,6 +679,9 @@ export class SyncManager {
661
679
  }
662
680
 
663
681
  if (res.ok) {
682
+ // 平台连接恢复信号(sync-noise.js):此前静默窗口内的失败在恢复的第一笔成功时
683
+ // 打一行提示并清 marker;之后照常「成功不打扰」。
684
+ noteSyncSuccess();
664
685
  // 更新 platform_last_sync + 推进 base_ts(ql-20260818-008:值优先平台回执 last_pushed_at,
665
686
  // 缺省用本次 pushedAt——服务器 _apply 存的就是客户端 X-SillySpec-Pushed-At 原值,回写一致。
666
687
  // 修复前该列从不写,下次 push 永不带 X-SillySpec-Base-Ts、pull 脏度检测恒 false)
@@ -1268,8 +1289,12 @@ export class SyncManager {
1268
1289
  signal: opts.signal, // HUB-09:熔断 abort 传到底层请求
1269
1290
  });
1270
1291
  if (result === null) {
1271
- // sillyhub 未就绪 / 404 / 网络失败均 fetchJson 返回 null → Best Effort 降级
1272
- console.warn(`[sync] 拉取变更进度失败: ${changeName}(sillyhub 未就绪或变更不存在)`);
1292
+ // sillyhub 未就绪 / 404 / 网络失败均 fetchJson 返回 null → Best Effort 降级。
1293
+ // 自动注入点(triggerPull,opts.autoPull)走噪音闸防刷屏;手动 `platform pull`
1294
+ // 是用户显式诊断动作,恒可见。
1295
+ const line = `[sync] 拉取变更进度失败: ${changeName}(sillyhub 未就绪或变更不存在)`;
1296
+ if (opts.autoPull) syncConnectionWarn(line);
1297
+ else console.warn(line);
1273
1298
  return { ok: false, imported: false, conflict: false, reason: '拉取变更进度失败' };
1274
1299
  }
1275
1300
 
@@ -1398,7 +1398,21 @@ export async function adoptTaskReviewMechanics({ changeName, cwd, platformOpts =
1398
1398
  ? crossDiffFiles.filter(f => allowedPaths.some(p => pathMatches(f, p)))
1399
1399
  : crossDiffFiles
1400
1400
  } else {
1401
- const mainDiff = Array.isArray(diffFiles) ? diffFiles : []
1401
+ // 坑 backfill-reviews-adopt-empties-changedfiles(2026-09-08 实证):主仓 task 同样
1402
+ // 优先卡锚点切片(base_commit/head_commit),不再共用「worktree 基线..当前 HEAD」——
1403
+ // per-task commit 模式下 base 必须是本 task 起点commit,diff 才与 allowed_paths 对齐
1404
+ // (跨仓路径本就按卡切片);卡无锚点回落 change 级切片(旧语义,兼容 wave 级提交)。
1405
+ const cardHead = parseHeadCommit(content)
1406
+ const cardBase = parseBaseCommit(content)
1407
+ let mainDiff = Array.isArray(diffFiles) ? diffFiles : []
1408
+ if (cardHead) {
1409
+ taskHead = cardHead
1410
+ taskBase = cardBase || taskBase
1411
+ try {
1412
+ const out = runGit(reviewGitDir, ['diff', '--name-only', `${taskBase}..${taskHead}`])
1413
+ mainDiff = out ? out.split('\n').filter(Boolean).map(p => p.replace(/\\/g, '/')) : []
1414
+ } catch { /* 锚点不可解析(如卡记短 hash 仓内无引用)→ 回落 change 级切片 */ }
1415
+ }
1402
1416
  taskChangedFiles = allowedPaths.length > 0
1403
1417
  ? mainDiff.filter(f => allowedPaths.some(p => pathMatches(f, p)))
1404
1418
  : []
@@ -1410,6 +1424,16 @@ export async function adoptTaskReviewMechanics({ changeName, cwd, platformOpts =
1410
1424
  continue
1411
1425
  }
1412
1426
 
1427
+ // 切片空不冲声明(2026-09-08 用户反馈:backfill-reviews --adopt 冲掉 review):task 卡
1428
+ // 无 allowed_paths、或 allowed_paths 与 change 级 diff 的路径形态失配时,上方过滤产出
1429
+ // 空 changedFiles——旧代码照样整字段覆写,reviewer 已声明且过证据校验的 changedFiles
1430
+ // 被清空,gate(verifyReviewGitEvidence)随即按空集误判。adopt 的职责是重算 mechanics,
1431
+ // 不是降级声明:计算切片为空而原声明非空时保留原声明并留 reason 可追溯。
1432
+ if (taskChangedFiles.length === 0 && Array.isArray(prev.changedFiles) && prev.changedFiles.length > 0) {
1433
+ taskChangedFiles = prev.changedFiles
1434
+ reasons.push(`${taskId}: allowed_paths 未切出任何 diff 文件(卡无声明/路径形态失配),保留原 changedFiles ${prev.changedFiles.length} 条不冲空`)
1435
+ }
1436
+
1413
1437
  const merged = { ...prev }
1414
1438
  merged.schemaVersion = REVIEW_SCHEMA_VERSION
1415
1439
  merged.task = taskId
@@ -0,0 +1,141 @@
1
+ /**
2
+ * verify-facts-schema.js — verify-facts.json v2 schema 单点(2026-09-08-ir-verify-facts task-01 / D-005@v2)。
3
+ *
4
+ * facts v2 的字段/枚举/豁免形态/路径分类/槽段解析集中于此,builder(verify-probes.buildVerifyFacts)、
5
+ * 对账引擎(verify-postcheck.runVerifyRequiredEvidenceCheck)、集成证据(change-risk-profile 经
6
+ * stage-contract)、骨架渲染(generateVerifyResultSkeleton)四方 import 同源,禁复制。
7
+ *
8
+ * 双层写入模型(D-001@v2):机器数据(probes/tests/factsConsistency)CLI 直写;判断层
9
+ * (结论枚举/证据状态/回执声明)经 verify-result.md 受控槽段录入、--done 时 CLI 解析固化
10
+ * (slot-backfill),单向渲染约束对象是机器数据。
11
+ */
12
+ export const FACTS_SCHEMA_VERSION = 2
13
+
14
+ /** 证据状态枚举单源(「## 证据账」槽行 + facts.requiredEvidence[].status 共用) */
15
+ export const EVIDENCE_STATUS = ['satisfied', 'missing', 'partial']
16
+
17
+ /** 豁免后缀形态:missing + (豁免:<理由>)——机器可解析,无括注的 missing 不算豁免(模块私有:parseEvidenceSlots 内部消费,外部读解析结果的 exempt 字段) */
18
+ const EXEMPTION_RE = /(豁免:[^)]+)/
19
+
20
+ /** 证据账槽段标题(与 generateVerifyResultSkeleton 渲染逐字一致——避开既有「## Runtime Evidence」人工判断章) */
21
+ export const EVIDENCE_SLOT_HEADING = '## 证据账(cannot_verify 任务)'
22
+
23
+ /** 集成验证回执槽段标题 */
24
+ export const RECEIPT_SLOT_HEADING = '## 集成验证回执'
25
+
26
+ /**
27
+ * verifiedFiles 路径分类:决定核验口径(design Phase 2)。
28
+ * artifact 类(.runtime/ 日志 / 文档——resolveVerifyChangedFiles 天然不覆盖)核 存在×mtime,
29
+ * diff 交集豁免;其余(code)核 存在×mtime×diff 交集。
30
+ * @param {string} path 仓根相对路径(posix)
31
+ * @returns {'code'|'artifact'}
32
+ */
33
+ export function classifyVerifiedFile(path) {
34
+ const p = String(path || '').replace(/\\/g, '/')
35
+ if (p.startsWith('.runtime/') || p.startsWith('.sillyspec/.runtime/')) return 'artifact'
36
+ if (/\.(log|txt|out)$/i.test(p)) return 'artifact'
37
+ if (p.startsWith('.sillyspec/docs/') || p.startsWith('docs/')) return 'artifact'
38
+ if (p.endsWith('.md') || p.endsWith('.mdx')) return 'artifact'
39
+ return 'code'
40
+ }
41
+
42
+ /** 证据账 task 行:`- task-NN: <status> | verifiedFiles: a, b(豁免:…)`——占位 <待填:*> 不命中(fail-closed) */
43
+ const EVIDENCE_LINE_RE = /^- (task-\d+):\s*(satisfied|missing|partial)\b([^\n]*)$/
44
+
45
+ /** 回执行:`- claim: … | command: … | exit: 0 | log: path`——四字段行首锚定,占位不命中 */
46
+ const RECEIPT_LINE_RE = /^- claim:\s*([^|]+)\|\s*command:\s*([^|]+)\|\s*exit:\s*(\d+)\s*\|\s*log:\s*([^\s|]+)/
47
+
48
+ /**
49
+ * 解析 verify-result.md 的两个受控槽段(D-001@v2 slot-backfill 录入界面)。
50
+ * 占位形态(<待填:三选一> / <待填:0 或非 0>)不含枚举词,行首锚定 + 枚举词双保险 fail-closed。
51
+ * @param {string} mdText verify-result.md 全文(LF 归一由调用方 readFileSync 层负责或此处容错)
52
+ * @returns {{ requiredEvidence: Array<{task,status,exempt:boolean,exemptionReason:string|null,verifiedFiles:string[]}>, runtimeEvidence: Array<{claim,command,exitCode,logPath}>, hasEvidenceSlot: boolean, hasReceiptSlot: boolean }}
53
+ */
54
+ export function parseEvidenceSlots(mdText) {
55
+ const text = String(mdText || '').replace(/\r\n/g, '\n')
56
+ const lines = text.split('\n')
57
+
58
+ // 槽段定位:标题行到下一个同级/更高级标题或文末
59
+ const sectionOf = (heading) => {
60
+ const start = lines.findIndex(l => l.startsWith(heading))
61
+ if (start === -1) return null
62
+ const body = []
63
+ for (let i = start + 1; i < lines.length; i++) {
64
+ if (/^#{1,2}\s/.test(lines[i])) break
65
+ body.push(lines[i])
66
+ }
67
+ return body
68
+ }
69
+
70
+ const evidenceBody = sectionOf(EVIDENCE_SLOT_HEADING)
71
+ const receiptBody = sectionOf(RECEIPT_SLOT_HEADING)
72
+
73
+ const requiredEvidence = []
74
+ if (evidenceBody) {
75
+ for (const line of evidenceBody) {
76
+ const m = line.match(EVIDENCE_LINE_RE)
77
+ if (!m) continue
78
+ const [, task, status, rest] = m
79
+ const exemptM = rest.match(EXEMPTION_RE)
80
+ const vfMatch = rest.match(/verifiedFiles:\s*([^\n(]*)/)
81
+ const verifiedFiles = (vfMatch ? vfMatch[1] : '')
82
+ .split(',')
83
+ .map(s => s.trim().replace(/^`|`$/g, ''))
84
+ .filter(s => s.length > 0 && !s.startsWith('<'))
85
+ requiredEvidence.push({
86
+ task,
87
+ status,
88
+ exempt: status === 'missing' && !!exemptM,
89
+ exemptionReason: exemptM ? exemptM[0] : null,
90
+ verifiedFiles,
91
+ })
92
+ }
93
+ }
94
+
95
+ const runtimeEvidence = []
96
+ if (receiptBody) {
97
+ for (const line of receiptBody) {
98
+ const m = line.match(RECEIPT_LINE_RE)
99
+ if (!m) continue
100
+ runtimeEvidence.push({
101
+ claim: m[1].trim(),
102
+ command: m[2].trim(),
103
+ exitCode: Number(m[3]),
104
+ logPath: m[4].trim().replace(/^`|`$/g, ''),
105
+ })
106
+ }
107
+ }
108
+
109
+ return {
110
+ requiredEvidence,
111
+ runtimeEvidence,
112
+ hasEvidenceSlot: evidenceBody !== null,
113
+ hasReceiptSlot: receiptBody !== null,
114
+ }
115
+ }
116
+
117
+ /**
118
+ * facts v2 结构校验(写入前/消费侧防线)。
119
+ * @param {object} facts
120
+ * @returns {{ ok: boolean, errors: string[] }}
121
+ */
122
+ export function validateFactsV2(facts) {
123
+ const errors = []
124
+ if (!facts || typeof facts !== 'object') return { ok: false, errors: ['facts 非对象'] }
125
+ if (facts.schemaVersion !== FACTS_SCHEMA_VERSION) {
126
+ errors.push(`schemaVersion 应为 ${FACTS_SCHEMA_VERSION},实际 ${facts.schemaVersion}`)
127
+ }
128
+ if (!Array.isArray(facts.probes ? Object.keys(facts.probes) : null)) {
129
+ errors.push('probes 段缺失或非对象')
130
+ }
131
+ if (facts.conclusion != null && !['PASS', 'PASS WITH NOTES', 'FAIL'].includes(facts.conclusion)) {
132
+ errors.push(`conclusion 非法枚举值:${facts.conclusion}`)
133
+ }
134
+ for (const item of facts.requiredEvidence || []) {
135
+ if (!EVIDENCE_STATUS.includes(item.status)) errors.push(`requiredEvidence[${item.task}] status 非法:${item.status}`)
136
+ if (item.status === 'satisfied' && (!Array.isArray(item.verifiedFiles) || item.verifiedFiles.length === 0)) {
137
+ errors.push(`requiredEvidence[${item.task}] satisfied 但 verifiedFiles 为空`)
138
+ }
139
+ }
140
+ return { ok: errors.length === 0, errors }
141
+ }