sillyspec 3.33.0 → 3.33.2

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 (72) hide show
  1. package/.claude/skills/sillyspec-flow/SKILL.md +18 -10
  2. package/.github/workflows/ci.yml +11 -3
  3. package/AGENTS.md +3 -3
  4. package/README.md +2 -2
  5. package/assets/command-cards/flow.md +9 -5
  6. package/package.json +1 -1
  7. package/src/archive-delta.js +4 -3
  8. package/src/change-delete.js +6 -5
  9. package/src/contract-matrix.js +2 -1
  10. package/src/cross-placement.js +5 -1
  11. package/src/cross-repo-reconcile.js +4 -1
  12. package/src/db.js +8 -2
  13. package/src/decision-distill.js +22 -8
  14. package/src/decision-values.js +34 -0
  15. package/src/decisions-io.js +5 -2
  16. package/src/deleted-ledger.js +2 -1
  17. package/src/doctor-diagnostics.js +8 -3
  18. package/src/flow-draft.js +17 -10
  19. package/src/flow-parity.js +93 -5
  20. package/src/flow-review-pack.js +183 -0
  21. package/src/flow-review.js +45 -3
  22. package/src/flow.js +365 -109
  23. package/src/friction-ledger.js +2 -1
  24. package/src/friction-tally.js +8 -4
  25. package/src/handoff.js +2 -1
  26. package/src/hooks/worktree-guard.js +5 -5
  27. package/src/index.js +62 -21
  28. package/src/init.js +7 -30
  29. package/src/knowledge-hits.js +2 -1
  30. package/src/knowledge-vector.js +2 -1
  31. package/src/progress/consistency-doctor.js +5 -2
  32. package/src/progress/stage-machine.js +6 -2
  33. package/src/progress.js +19 -8
  34. package/src/quicklog.js +194 -9
  35. package/src/review-dispatch.js +4 -3
  36. package/src/review-tier.js +2 -1
  37. package/src/route-hindsight.js +4 -3
  38. package/src/run/bg-sync.js +10 -3
  39. package/src/run/command.js +14 -12
  40. package/src/run/complete-handlers.js +40 -37
  41. package/src/run/complete.js +12 -8
  42. package/src/run/gate-snapshot-ledger.js +2 -1
  43. package/src/run/gate-snapshot.js +102 -23
  44. package/src/run/gates.js +32 -8
  45. package/src/run/prompt.js +6 -5
  46. package/src/run/scan-profile.js +2 -1
  47. package/src/run/shared.js +109 -5
  48. package/src/run/stage.js +5 -4
  49. package/src/run/test-ledger.js +2 -1
  50. package/src/run/verify-quality-scan.js +11 -5
  51. package/src/runtime-paths.js +189 -0
  52. package/src/scan-refresh.js +4 -3
  53. package/src/scope-audit.js +13 -7
  54. package/src/sentinel-assertions.js +37 -0
  55. package/src/spec-sync.js +38 -22
  56. package/src/stage-contract.js +24 -2
  57. package/src/stage-review.js +3 -2
  58. package/src/stage-templates.js +3 -2
  59. package/src/stages/brainstorm.js +6 -5
  60. package/src/sync-noise.js +3 -2
  61. package/src/sync.js +71 -15
  62. package/src/task-done.js +2 -1
  63. package/src/task-review.js +7 -5
  64. package/src/task-tick.js +45 -1
  65. package/src/validate-artifacts.js +2 -1
  66. package/src/verify-draft.js +2 -1
  67. package/src/verify-postcheck.js +11 -10
  68. package/src/verify-probes.js +157 -52
  69. package/src/watcher.js +15 -9
  70. package/src/worktree-apply.js +2 -1
  71. package/src/write-audit.js +2 -1
  72. package/templates/agents-instruction.md +3 -3
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  import { existsSync, readFileSync, mkdirSync, writeFileSync, readdirSync, statSync } from 'fs'
12
+ import { runtimeStatePath } from './runtime-paths.js'
12
13
  import { join, resolve, basename, dirname } from 'path'
13
14
  import { execFileSync } from 'child_process'
14
15
  import { git, unquoteGitPath } from './git-helper.js'
@@ -959,7 +960,7 @@ export function resolveLatestExecuteRunIdWithTasks({ runtimeRoot, changeName = n
959
960
  }
960
961
 
961
962
  export function resolveLatestExecuteRunId({ runtimeRoot, changeName }) {
962
- const marker = join(runtimeRoot, `current-execute-run-id-${changeName}`)
963
+ const marker = runtimeStatePath(runtimeRoot, `current-execute-run-id-${changeName}`)
963
964
  try {
964
965
  if (existsSync(marker)) {
965
966
  const c = readFileSync(marker, 'utf8').trim()
@@ -1077,7 +1078,7 @@ export function resolveExecuteRunForChange({ runtimeRoot, changeName, taskIds =
1077
1078
  // 1. marker
1078
1079
  let markerRunId = null
1079
1080
  try {
1080
- const marker = join(runtimeRoot, `current-execute-run-id-${changeName}`)
1081
+ const marker = runtimeStatePath(runtimeRoot, `current-execute-run-id-${changeName}`)
1081
1082
  if (existsSync(marker)) {
1082
1083
  const c = readFileSync(marker, 'utf8').trim()
1083
1084
  if (c && !isValidExecuteRunId(c)) console.warn(`[sillyspec] execute run marker 内容非法(期望 exec-YYYY-MM-DD-HHMMSS,实得 ${JSON.stringify(c.slice(0, 60))}),忽略并回退归属扫描`)
@@ -1341,7 +1342,7 @@ export async function generateTaskReviewDrafts({ changeName, cwd, platformOpts =
1341
1342
  }
1342
1343
 
1343
1344
  // exec-id:与 Task Review Gate(gates.js:269)/ autoCheckPlanFromReviews 同源
1344
- const runIdFile = join(runtimeRoot, 'current-execute-run-id-' + changeName)
1345
+ const runIdFile = runtimeStatePath(runtimeRoot, 'current-execute-run-id-' + changeName)
1345
1346
  let executeRunId = ''
1346
1347
  try {
1347
1348
  if (existsSync(runIdFile)) {
@@ -1637,7 +1638,7 @@ export async function adoptTaskReviewMechanics({ changeName, cwd, platformOpts =
1637
1638
  // executeRunId:与草稿/gate 同源 marker。缺 marker 不新建 run——adopt 修的是当前 run 里
1638
1639
  // 已存在的 review,marker 丢失说明无「当前 run」语义,无对象可修(gate 对 marker 缺失放行,
1639
1640
  // 下游 Task Review Gate 兜底,这里同口径不造 run)。
1640
- const runIdFile = join(runtimeRoot, 'current-execute-run-id-' + changeName)
1641
+ const runIdFile = runtimeStatePath(runtimeRoot, 'current-execute-run-id-' + changeName)
1641
1642
  let executeRunId = ''
1642
1643
  try {
1643
1644
  if (existsSync(runIdFile)) {
@@ -1969,7 +1970,7 @@ export async function writeTaskReview({
1969
1970
  if (!existsSync(taskCardPath)) {
1970
1971
  return { ok: false, errors: [`task 卡不存在:${taskCardPath}(先由 plan 阶段生成或手建)`], warnings }
1971
1972
  }
1972
- const markerFile = join(runtimeRoot, 'current-execute-run-id-' + changeName)
1973
+ const markerFile = runtimeStatePath(runtimeRoot, 'current-execute-run-id-' + changeName)
1973
1974
  let executeRunId = ''
1974
1975
  try {
1975
1976
  if (existsSync(markerFile)) {
@@ -1986,6 +1987,7 @@ export async function writeTaskReview({
1986
1987
  // 排他认领(坑 exec-run-id-same-second-collision)+ 幂等 mkdir,认领碰撞换后缀
1987
1988
  executeRunId = claimExecuteRunId(runtimeRoot, executeRunId)
1988
1989
  mkdirSync(join(runtimeRoot, 'execute-runs', executeRunId, 'tasks'), { recursive: true })
1990
+ try { mkdirSync(dirname(markerFile), { recursive: true }); } catch { /* fail-open */ }
1989
1991
  writeFileSync(markerFile, executeRunId + '\n')
1990
1992
  stampExecuteRunChange(runtimeRoot, executeRunId, changeName)
1991
1993
  warnings.push(`无 execute run marker——已生成新 runId ${executeRunId} 并建目录(正常应先跑过 execute Wave 步)`)
package/src/task-tick.js CHANGED
@@ -19,6 +19,7 @@
19
19
  * - 归档件只读:变更已归档时拒收(exit 2)。
20
20
  */
21
21
  import { existsSync, readFileSync, appendFileSync } from 'node:fs'
22
+ import { runtimeStatePath } from './runtime-paths.js'
22
23
  import { join } from 'node:path'
23
24
 
24
25
  /** 任务行(宽容缩进/星号有序标记,与哨兵判集行锚同口径 + 缩进容差)。 */
@@ -137,7 +138,7 @@ export async function runTaskTick({ changeName, cwd, taskId, specBase, evidence
137
138
  try {
138
139
  const { resolveRuntimeRoot } = await import('./run/shared.js')
139
140
  const runtimeRoot = resolveRuntimeRoot({}, specBase)
140
- const eventsPath = join(runtimeRoot, `watcher-events-${changeName}.jsonl`)
141
+ const eventsPath = runtimeStatePath(runtimeRoot, `watcher-events-${changeName}.jsonl`)
141
142
  const from = Math.max(0, r.checked - 1)
142
143
  appendFileSync(eventsPath, JSON.stringify({
143
144
  ts: Date.now(), kind: 'task-done', stage: 'tasks',
@@ -168,6 +169,49 @@ export async function runTaskTick({ changeName, cwd, taskId, specBase, evidence
168
169
  ? `|下一任务:${r.next.id} ${r.next.desc.slice(0, 40)}${r.next.desc.length > 40 ? '…' : ''}`
169
170
  : `|🎉 ${r.total}/${r.total} 全勾——可收口:sillyspec flow done --change ${changeName}`
170
171
  console.log(`${r.kind === 'ticked' ? '✅ 已勾' : 'ℹ️ 已是勾选态(幂等跳过写盘)'}(${r.checked}/${r.total})${nextTip}`)
172
+ if (r.kind === 'ticked') {
173
+ // 勾选随交付入库教学(2026-10-17-tick-timing-source,坑 tick-timing-squash-blindspot 根因 1/3
174
+ // 断链处):翻格后给现成提交命令——pathspec 含 tasks.md 与侧车(勾选修改与任务交付同一提交
175
+ // 入库,消息占位带 task-NN token),把纪律变成可抄的命令;并当场提醒「此前勾选仍未进
176
+ // git」(已勾 ≥2 格且 tasks.md 零提交)——反馈不再等到 flow done 终点。best-effort:
177
+ // git 不可用(非 git 目录/失败返回 null)只给命令不给提醒,失败不影响翻格与 exit code。
178
+ try {
179
+ const { relative } = await import('node:path')
180
+ const rel = (p) => relative(cwd, p).split('\\').join('/')
181
+ const sidecar = join(changesRoot, changeName, 'task-ticks.jsonl')
182
+ const paths = `${rel(tasksPath)}${existsSync(sidecar) ? ` ${rel(sidecar)}` : ''}`
183
+ console.log(`📦 勾选随交付入库(add 与 commit 同一清单——勾选修改随任务交付同一提交):`)
184
+ console.log(` git add -- <交付文件…> ${paths}`)
185
+ console.log(` git commit -m "<本任务交付概括> (${normalizeTaskId(taskId) || taskId})" -- <交付文件…> ${paths}`)
186
+ const { gitQuiet } = await import('./git-helper.js')
187
+ const touched = gitQuiet(cwd, ['log', '-n', '1', '--format=%h', '--', tasksPath])
188
+ if (r.checked >= 2 && touched === '') {
189
+ console.log(`ℹ️ tasks.md 尚无任何提交(此前勾选未随交付入库)——上面两行命令已把它带进清单,照抄即闭环`)
190
+ }
191
+ } catch { /* 教学面 best-effort:失败不影响翻格 */ }
192
+ }
193
+ // 顺序正典现场提醒(2026-10-19-approve-gate-tick-nudge FR-04,advisory):勾选是「正在干活」
194
+ // 的最高频时刻——spec 面未齐或未过断点① 时给一行警示(不阻断、不改既有行;读取失败静默,
195
+ // 回显退化为现状)。判定与 flow status 同源(designSectionsFilled 单一源)。
196
+ try {
197
+ const dText = readFileSync(join(changesRoot, changeName, 'design.md'), 'utf8')
198
+ const { designSectionsFilled } = await import('./flow-review.js')
199
+ const unanswered = !designSectionsFilled(dText)
200
+ const fsText = readFileSync(join(changesRoot, changeName, 'flow-state.yaml'), 'utf8')
201
+ const unapproved = !/^spec_approved:\s*true/m.test(fsText) && !/^autopilot:\s*true/m.test(fsText)
202
+ if (unanswered || unapproved) {
203
+ const parts = []
204
+ if (unanswered) parts.push('design 四节未答')
205
+ if (unapproved) parts.push('未过断点①(flow approve)')
206
+ console.log(`⚠️ spec 面未就位(${parts.join('、')})——顺序正典:先答 spec 面并过批准,再继续勾任务`)
207
+ }
208
+ } catch { /* 提醒 best-effort:读取失败静默,不影响勾选回显与 exit code */ }
209
+ // 执行期用户决策落盘提示(2026-10-20-thin-user-decision-record,advisory):tick 是执行期
210
+ // 对 agent 最高频触点——decisions.md 尚未落盘时提一句(用户中途给了实现影响方向可当场
211
+ // decisions add 记录;缺席合法,纯提示不阻断;存在性判定即时读盘,在场分支零新增行)。
212
+ if (!existsSync(join(changesRoot, changeName, 'decisions.md'))) {
213
+ console.log(`💡 本变更 decisions.md 尚未落盘——执行期用户给过实现影响方向的话,当场 sillyspec decisions add --change ${changeName} --id D-001 --title "…" 记录(收口随蒸馏进知识防复潮;纯提示不阻断)`)
214
+ }
171
215
  return r
172
216
  }
173
217
 
@@ -11,6 +11,7 @@
11
11
  * 无既有 validator 的 required-evidence 与 endpoints(容忍缺省,出场才算)。
12
12
  */
13
13
  import { existsSync, readFileSync, readdirSync } from 'node:fs'
14
+ import { runtimeStatePath } from './runtime-paths.js'
14
15
  import { join } from 'node:path'
15
16
 
16
17
  function checkJson(path, fn) {
@@ -66,7 +67,7 @@ export async function validateChangeArtifacts({ cwd, specBase, changeName, runti
66
67
 
67
68
  // ③ task review.json(最新 execute run 的逐 task 产物——既有 validateReviewSchema)
68
69
  {
69
- const markerPath = join(rt, `current-execute-run-id-${changeName}`)
70
+ const markerPath = runtimeStatePath(rt, `current-execute-run-id-${changeName}`)
70
71
  const runId = existsSync(markerPath) ? readFileSync(markerPath, 'utf8').trim() : null
71
72
  if (!runId) {
72
73
  checks.push({ artifact: 'task reviews', status: 'skip', reason: '无 execute run marker(未经 execute 或已清理)' })
@@ -21,6 +21,7 @@
21
21
  * 纯 --init(无 --draft)路径零变化;无 sidecar → 门禁 not-applicable(存量变更零红)。
22
22
  */
23
23
  import { existsSync, readFileSync } from 'node:fs'
24
+ import { runtimeStatePath } from './runtime-paths.js'
24
25
  import { join } from 'node:path'
25
26
  import { writeAtomicSync } from './fs-atomic.js'
26
27
  // 三件套原语消费方化(R7 切片三 task-04):标记格式/指纹/三态判定/重锚的机械半边在本模块,
@@ -33,7 +34,7 @@ const AMEND_CMD = 'sillyspec verify-probes --change <变更名> --amend-draft'
33
34
 
34
35
  /** sidecar 路径(.runtime 下,与质量扫描记录/探针回执同族) */
35
36
  export function verifyDraftSidecarPath(runtimeRoot, changeName) {
36
- return join(runtimeRoot, `verify-draft-${changeName}.json`)
37
+ return runtimeStatePath(runtimeRoot, `verify-draft-${changeName}.json`)
37
38
  }
38
39
 
39
40
  /**
@@ -23,6 +23,7 @@
23
23
  */
24
24
 
25
25
  import { execSync, spawnSync } from 'child_process'
26
+ import { runtimeStatePath } from './runtime-paths.js'
26
27
  import { createHash } from 'crypto'
27
28
  import { IR_STRICT_SINCE } from './constants.js'
28
29
  import { gitQuiet } from './git-helper.js'
@@ -236,7 +237,7 @@ function recordVerifyLintTally({ specBase, result }) {
236
237
  try {
237
238
  const runtimeDir = join(specBase, '.runtime')
238
239
  mkdirSync(runtimeDir, { recursive: true })
239
- const tallyPath = join(runtimeDir, 'verify-lint-tally.json')
240
+ const tallyPath = runtimeStatePath(runtimeDir, 'verify-lint-tally.json')
240
241
  let tally = { totalRuns: 0, failedRuns: 0, history: [] }
241
242
  try {
242
243
  tally = { ...tally, ...JSON.parse(readFileSync(tallyPath, 'utf8')) }
@@ -1424,7 +1425,7 @@ function readTestRerunConfig(specBase) {
1424
1425
  }
1425
1426
 
1426
1427
  function testRerunLedgerPath(specBase, changeName) {
1427
- return join(specBase, '.runtime', `test-rerun-${changeName || 'change'}.json`)
1428
+ return runtimeStatePath(join(specBase, '.runtime'), `test-rerun-${changeName || 'change'}.json`)
1428
1429
  }
1429
1430
 
1430
1431
  /**
@@ -1861,7 +1862,7 @@ export function resolveVerifyChangedFiles(cwd, changeName, ctx = null, opts = {}
1861
1862
  if (!ctx || typeof ctx.repos !== 'object' || ctx.repos === null) {
1862
1863
  try {
1863
1864
  const rtRoot = resolveRuntimeRoot({ runtimeRoot: opts.runtimeRoot, specDriftAnchor: opts.specDriftAnchor }, opts.specBase || join(cwd, '.sillyspec'))
1864
- const pathspecFile = join(rtRoot, `apply-pathspec-${changeName}.txt`)
1865
+ const pathspecFile = runtimeStatePath(rtRoot, `apply-pathspec-${changeName}.txt`)
1865
1866
  if (existsSync(pathspecFile)) {
1866
1867
  const declared = readFileSync(pathspecFile, 'utf8').split('\n').map(l => l.trim()).filter(Boolean)
1867
1868
  .map(p => p.replace(/\\/g, '/'))
@@ -3138,7 +3139,7 @@ export function resolveTraceResidual({ specBase, changeName, cwd }) {
3138
3139
  const repoKey = r.repo && r.repo !== 'main' ? r.repo : null
3139
3140
  for (const t of (r.tests || [])) {
3140
3141
  const f = testAnchorFile(t)
3141
- const key = (repoKey ? repoKey + '' : '') + f
3142
+ const key = (repoKey ? repoKey + '\u0000' : '') + f
3142
3143
  if (!seen.has(key)) { seen.add(key); perFile.push({ repo: repoKey, file: f }) }
3143
3144
  }
3144
3145
  }
@@ -3230,7 +3231,7 @@ export function applyTraceResidual({ mainResult, action, cwd, specBase, changeNa
3230
3231
  const _rk = x.repo || null
3231
3232
  // worktree 感知(同悬空判定口径):主仓行按 resolvedRoot(主仓或变更 worktree 根)取执行根。
3232
3233
  const _root = (_rk && _repoRoots.get(_rk)) || x.resolvedRoot || cwd
3233
- const _gk = _rk + '' + _root
3234
+ const _gk = _rk + '\u0000' + _root
3234
3235
  if (!_gmap.has(_gk)) _gmap.set(_gk, { repo: _rk, root: _root, files: [] })
3235
3236
  _gmap.get(_gk).files.push(x.file)
3236
3237
  }
@@ -3780,7 +3781,7 @@ export function trackVerifyResultRegression(specBase, changeName, absPath) {
3780
3781
  if (!absPath || !existsSync(absPath)) return { regressed: false, prevMtime: null }
3781
3782
  const st = statSync(absPath)
3782
3783
  const hash = createHash('sha256').update(readFileSync(absPath)).digest('hex')
3783
- const hwmPath = join(specBase, '.runtime', `verify-result-hwm-${changeName}.json`)
3784
+ const hwmPath = runtimeStatePath(join(specBase, '.runtime'), `verify-result-hwm-${changeName}.json`)
3784
3785
  let prev = null
3785
3786
  try { prev = JSON.parse(readFileSync(hwmPath, 'utf8')) } catch { prev = null }
3786
3787
  const regressed = !!(prev && typeof prev.hash === 'string' &&
@@ -3790,7 +3791,7 @@ export function trackVerifyResultRegression(specBase, changeName, absPath) {
3790
3791
  if (!prev || st.mtimeMs >= (prev.mtimeMs || 0)) {
3791
3792
  try {
3792
3793
  mkdirSync(join(specBase, '.runtime'), { recursive: true })
3793
- writeFileSync(hwmPath, JSON.stringify({ hash, mtimeMs: st.mtimeMs, ts: Date.now() }) + '\n')
3794
+ writeFileSync(hwmPath, JSON.stringify({ hash, mtimeMs: Math.round(st.mtimeMs), ts: Date.now() }) + '\n')
3794
3795
  } catch { /* hwm 落盘失败 → 下次当首次(fail-open) */ }
3795
3796
  }
3796
3797
  return { regressed, prevMtime: prev && prev.mtimeMs ? prev.mtimeMs : null }
@@ -4307,7 +4308,7 @@ export function resolveReconcileActualFiles({ cwd, specBase, runtimeRoot, change
4307
4308
  }
4308
4309
  } catch { /* 窗口源失败静默省略 */ }
4309
4310
  // B3 apply-pathspec 兜底(存在则并入,读取失败静默忽略——前两源不受影响)
4310
- const pathspecFile = join(runtimeRoot, `apply-pathspec-${changeName}.txt`)
4311
+ const pathspecFile = runtimeStatePath(runtimeRoot, `apply-pathspec-${changeName}.txt`)
4311
4312
  if (existsSync(pathspecFile)) {
4312
4313
  try {
4313
4314
  const lines = readFileSync(pathspecFile, 'utf8').split('\n').map(l => l.trim()).filter(Boolean).map(normalizeReconcilePath)
@@ -4704,7 +4705,7 @@ export async function runCeremonyDualRunCheck({ cwd, specBase = null, changeName
4704
4705
  const rt = runtimeRoot || join(sb, '.runtime')
4705
4706
 
4706
4707
  // —— 声明侧:开跑定价落档(task-03 产物,tier/components/reasons/transitions)——
4707
- const tierPath = join(rt, `ceremony-tier-${changeName}.json`)
4708
+ const tierPath = runtimeStatePath(rt, `ceremony-tier-${changeName}.json`)
4708
4709
  let tierFile = null
4709
4710
  try { tierFile = JSON.parse(readFileSync(tierPath, 'utf8')) } catch { /* 缺失/坏 JSON → 下方跳过 */ }
4710
4711
  if (tierFile == null || typeof tierFile !== 'object' || Array.isArray(tierFile)) {
@@ -4801,7 +4802,7 @@ export async function runCeremonyDualRunCheck({ cwd, specBase = null, changeName
4801
4802
  recomputedReasons: [`双跑低报自动追赶(事实面 tier=${verdict.factTier},证据面 seed ${actual.files.length} 文件)`],
4802
4803
  frictionCounts: {},
4803
4804
  })
4804
- const tierPathForReprice = join(rt, `ceremony-tier-${changeName}.json`)
4805
+ const tierPathForReprice = runtimeStatePath(rt, `ceremony-tier-${changeName}.json`)
4805
4806
  const { writeAtomicSync } = await import('./fs-atomic.js')
4806
4807
  writeAtomicSync(tierPathForReprice, JSON.stringify(caught, null, 2) + '\n')
4807
4808
  result.violation.message += `;档位文件已自动按事实面追赶重定价(${String(declaredTier)} → ${caught.tier})——修复低报根因后重跑 --done 即对账通过,勿手动改档(ql-013 死锁修复)`