sillyspec 3.26.9 → 3.26.10

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.
@@ -147,7 +147,7 @@ execute 还有**第二道**独立的 stage 级审查:除逐 task review.json
147
147
 
148
148
  - **主仓 task**:子代理 workdir = 主仓 worktree 路径(CLI 自动创建的隔离 worktree)。
149
149
  - **跨仓 task**:子代理 workdir = 跨仓仓根目录(**直接在跨仓仓主干工作区改+commit,不经主仓 worktree、不建分支**)。commit 到跨仓仓主干即落盘。
150
- - 同一个 Wave 内允许混合主仓 + 跨仓 task(每个 task 独立子代理,各传各的 workdir)。Wave prompt 会注入 per-task workdir 表,按表选。
150
+ - 同一个 Wave 内允许混合主仓 + 跨仓 task。默认每 task 独立子代理(各传各的 workdir,Wave prompt 注入 per-task workdir 表,按表选);同 Wave 内满足**文件正交 / 无契约链 / 组 ≤3** 三个条件的 task 可合并为一个 batch 子代理串行实现——batch 只合并实现,task 审查、review.json 产出与 checkbox 勾选仍归主 agent(子代理返回后逐 task 进行)。
151
151
 
152
152
  ### 跨仓 task 的双锡点(CLI 写入,子代理不改)
153
153
 
@@ -59,7 +59,7 @@ sillyspec run quick --done --change quick-<hash> --output "…" # 完成该会
59
59
  | 参数 | 说明 |
60
60
  |---|---|
61
61
  | `--linked-changes none\|a,b` | **显式关联变更(取代 `--change`,推荐)**。none=不关联,a,b=关联列表 |
62
- | `--files a.js,b.js` | 显式声明本次允许修改的文件(边界保护) |
62
+ | `--files a.js,b.js` | 显式声明本次允许修改的文件(边界保护 + 声明即归属:多 agent 并发仓防他者窗口文件混入 QUICKLOG 文件行,未声明窗口文件进「审计:」行追溯) |
63
63
  | `--file-notes "p::注 \|\| p::注"` | quick `--done` 用:QUICKLOG「文件:」行落盘为多行带括注 bullet(省事后手改文件行)。格式 `path::括注`,`\|\|` 分隔多条;**只随 step3 --done 同命令传**(CLI 短进程,step1/step2 传无效,不带到 step3) |
64
64
  | `--allow-new` | 允许新增文件(默认禁止,防意外创建) |
65
65
  | `--allow-delete` | 允许删除文件(默认 fail-closed,删除是破坏性操作;确认删除带此 flag 显式解锁) |
@@ -103,8 +103,8 @@ sillyspec run quick --files src/phone.ts,src/phone.test.ts
103
103
  - 完成后立即 `--done`,不跳过
104
104
  - QUICKLOG 记录的**骨架由 CLI 接管**:启动时 CLI 自动分配 ql-ID 并在 `.sillyspec/quicklog/QUICKLOG-<user>.md` 写「进行中」条目(含关联变更 tasks.md),`--done` 时 CLI 自动翻「已完成」+ 勾选 task + 回填文件路径。ql-ID 分配/状态/task 你无需手写,只需用注入的 `<quicklog-id>` 在模块文档变更索引引用
105
105
  - **QUICKLOG 落盘已结构化(`--done` 后按需核对,多数无需手改)**:CLI 已落盘结构化条目——标题从 `--output` 的「需求:」自动提取、正文四字段自动分行、文件行用 `--file-notes` 时为多行带括注。`--done` 后只需核对:标题弱才改(禁留 `(quick 任务)` 占位);没用 `--file-notes` 时文件行是单行、可事后补括注(参照同文件早期丰富条目);正文 `需求:`/`根因:`/`方案:`/`结果:` 四段按需充实(禁只留一段「结果:」)。一条 quick = 一条独立 ql,不追加到旧条目
106
- - **收尾顺序(模块文档在 `--done` 前,QUICKLOG 在 `--done` 后,别记混)**:① 命中模块→改模块文档→`git add`;② `sillyspec run quick --done --change <id> --output "四字段" [--file-notes "..."]`(CLI 自动翻完成 + 勾 task + 落盘 QUICKLOG 标题/文件/正文);③ 核对 QUICKLOG 标题(CLI 已从「需求:」提取,仅弱标题才改)→若改了再 `git add`
107
- - **最后一步 `--done --output` 必须按结构化结果模板给全四字段**(逐项一句话):`需求:… 根因:… 方案:… 结果:…`。这是 QUICKLOG「结果:」归档的唯一来源;CLI 校验缺字段会拒绝 `--done`(exit 1),补全后重跑即可。正文内避免嵌套全角冒号(如「方案:(说明 xxx):」这样标签后紧接嵌套冒号会被拆分判定缺字段),直接让标签接正文。前两个 step 的 `--output` 是中间摘要,不用此模板
106
+ - **收尾顺序(模块文档在 `--done` 前,QUICKLOG 在 `--done` 后,别记混)**:① 命中模块→改模块文档→`git add`;② `sillyspec run quick --done --change <id> --output "四字段" [--file-notes "..."]`(CLI 自动翻完成 + 勾 task + 落盘 QUICKLOG 标题/文件/正文;若 `--linked-changes` 关联的真实变更其 tasks.md 已全部勾选,CLI 会自动将其归档到 `changes/archive/`,无需再跑 archive 阶段);③ 核对 QUICKLOG 标题(CLI 已从「需求:」提取并截断,写成短标题则无需改)→若改了再 `git add`
107
+ - **最后一步 `--done --output` 必须按结构化结果模板给全四字段**(逐项一句话):`需求:… 根因:… 方案:… 结果:…`。这是 QUICKLOG「结果:」归档的唯一来源;CLI 校验缺字段会拒绝 `--done`(exit 1),补全后重跑即可。**「需求:」写一句语义化短标题**(写「改了什么」,如「登录限流修复——INCR 计数误清」)——CLI 把它提取为 QUICKLOG 条目标题,截到首个标点、超 80 字截断;写完整需求长句会被截成语义不完整的状语前半段,需求背景放「根因:/方案:」。正文内避免嵌套全角冒号(如「方案:(说明 xxx):」这样标签后紧接嵌套冒号会被拆分判定缺字段),直接让标签接正文。前两个 step 的 `--output` 是中间摘要,不用此模板
108
108
  - **禁止**在没有运行 CLI 的情况下自行决定流程
109
109
 
110
110
  ## 用户指令
@@ -38,7 +38,9 @@ sillyspec run verify --reopen --from-step N # 重新打开已完成阶段修
38
38
 
39
39
  ## verify 特有:完成门控(重要)
40
40
 
41
- verify 是只读阶段(**禁止改代码/改 git 状态**,只检查 + 写报告)。完成时有硬校验:
41
+ verify 是只读阶段(**禁止改代码/改 git 状态**,只检查 + 写报告)。
42
+
43
+ **diff 对账基点**:本变更走 worktree 时,「加载规范并锚定」步骤会注入 worktree 基线锚点(分支名 + `git merge-base` 真实基点)——对照设计做 diff 对账时以它为基点,**不要拿主仓当前 HEAD 当基点**(主仓在 execute 期间可能被并行推进,用错基点会把别人的演进误判为本变更越权改动)。task review 的 base/head 引用分支上的 commit,逐 task 核验用 `git diff <base>..<head>`。完成时有硬校验:
42
44
 
43
45
  - **必须产出 `verify-result.md`**——不存在则阻断完成(不能跳过报告直接 `--done`)
44
46
  - **结论为 `FAIL` 则阻断完成**——不能带着 FAIL 标记 verify 完成
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sillyspec",
3
- "version": "3.26.9",
3
+ "version": "3.26.10",
4
4
  "description": "SillySpec CLI — 流程状态机,让 AI 严格按步骤来",
5
5
  "icon": "logo.jpg",
6
6
  "homepage": "https://sillyspec.ppdmq.top/",
package/src/quicklog.js CHANGED
@@ -153,7 +153,7 @@ function buildPushPayloadFromRaw(rawBlock, { ql_id, author_raw, status, linked_c
153
153
  payload.timestamp = parts[1] || null
154
154
  payload.title = parts.slice(2).join('|').trim() || payload.title
155
155
  }
156
- const labelRe = /^(状态|关联变更|文件|需求|根因|方案|结果)\s*[::]\s*(.*)$/
156
+ const labelRe = /^(状态|关联变更|文件|审计|需求|根因|方案|结果)\s*[::]\s*(.*)$/
157
157
  const bulletRe = /^-\s+(.*)$/
158
158
  let inFiles = false
159
159
  let inLinked = false
@@ -168,6 +168,7 @@ function buildPushPayloadFromRaw(rawBlock, { ql_id, author_raw, status, linked_c
168
168
  if (label === '状态') lastStatus = value
169
169
  else if (label === '关联变更') { payload.linked_changes = value.split(/[,,、+;;]/).map(s => s.trim()).filter(Boolean); inLinked = true; inFiles = false }
170
170
  else if (label === '文件') { if (value) payload.files.push(...value.split(/[,,、+;;]/).map(s => s.trim()).filter(Boolean).map(p => ({ path: p, note: null }))); inFiles = true; inLinked = false }
171
+ else if (label === '审计') { /* D-8 advisory 行:只进 raw_block 不进结构化段(body_sections 不扩 schema),并阻断续行误挂 */ lastLabel = null; inFiles = false; inLinked = false }
171
172
  else { payload.body_sections[label] = value; lastLabel = label }
172
173
  continue
173
174
  }
@@ -477,7 +478,7 @@ function splitSingleLineFields(body) {
477
478
  return scanFields(body, findBoundaryLabel) ?? scanFields(body, (b, l, f) => b.indexOf(l, f))
478
479
  }
479
480
 
480
- function flipEntryInContent(content, qlId, result, changedFiles = [], fileNotes = []) {
481
+ function flipEntryInContent(content, qlId, result, changedFiles = [], fileNotes = [], auditNotes = []) {
481
482
  const lines = content.split('\n')
482
483
  const startIdx = lines.findIndex(l => l.startsWith(`## ${qlId} |`))
483
484
  if (startIdx === -1) return null
@@ -543,6 +544,22 @@ function flipEntryInContent(content, qlId, result, changedFiles = [], fileNotes
543
544
  lines.splice(after, 0, '')
544
545
  }
545
546
  }
547
+ // D-8 落盘(2026-08-18 修):advisory 审计行(文档欠账 / 引用失效)写入条目尾部——修复
548
+ // quick-audit「欠账已记录(QUICKLOG reasons)」的不实承诺(原先纯 console 打印,事后不可审计;
549
+ // 两周实测需要分母:信号触发次数必须可追溯)。幂等:条目内已有 审计: 行则不重复写(--done 重跑安全)。
550
+ // 位置:结果块之后 / 条目尾部(重算边界,不吃上方 splice 的陈旧索引)。
551
+ if (auditNotes.length > 0) {
552
+ let entryEnd = -1
553
+ for (let i = startIdx + 1; i < lines.length; i++) {
554
+ if (lines[i].startsWith('## ')) { entryEnd = i; break }
555
+ }
556
+ if (entryEnd === -1) entryEnd = lines.length
557
+ if (!lines.slice(startIdx, entryEnd).some((l) => l.startsWith('审计:'))) {
558
+ let insertAt = entryEnd
559
+ while (insertAt - 1 > startIdx && lines[insertAt - 1].trim() === '') insertAt--
560
+ lines.splice(insertAt, 0, ...auditNotes.map((n) => `审计:${n}`))
561
+ }
562
+ }
546
563
  return lines.join('\n')
547
564
  }
548
565
 
@@ -614,7 +631,7 @@ export async function allocateQuicklogEntry(specBase, gitUser, { description, li
614
631
  /**
615
632
  * 翻某 qlId 条目为「已完成」+ 追加结果 + 勾选关联 tasks.md。持锁。
616
633
  */
617
- export async function completeQuicklogEntry(specBase, gitUser, qlId, { resultText = '', linkedChanges = [], changedFiles = [] } = {}) {
634
+ export async function completeQuicklogEntry(specBase, gitUser, qlId, { resultText = '', linkedChanges = [], changedFiles = [], auditNotes = [] } = {}) {
618
635
  const quicklogDir = join(specBase, 'quicklog')
619
636
  // 与 allocateQuicklogEntry 同源消毒:锁文件路径含 user(防穿越写,两入口必须一致否则锁不上同一文件)
620
637
  const user = sanitizeQuicklogUser(gitUser) || 'unknown'
@@ -636,7 +653,8 @@ export async function completeQuicklogEntry(specBase, gitUser, qlId, { resultTex
636
653
  const filePath = join(quicklogDir, f)
637
654
  let content = ''
638
655
  try { content = readFileSync(filePath, 'utf8') } catch { continue }
639
- const updated = flipEntryInContent(content, qlId, result, realFiles, fileNotes)
656
+ const updated = flipEntryInContent(content, qlId, result, realFiles, fileNotes,
657
+ Array.isArray(auditNotes) ? auditNotes.filter((n) => typeof n === 'string' && n.trim() !== '') : [])
640
658
  if (updated !== null) {
641
659
  await writeAtomic(filePath, updated) // 命中处原子落盘(只改含目标条目的那一个文件)
642
660
  updatedContent = updated