sillyspec 3.25.2 → 3.25.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.
@@ -87,7 +87,8 @@ sillyspec run quick --files src/phone.ts,src/phone.test.ts
87
87
  - **必须用 exec 工具(shell)执行 CLI,不要自己编造流程**
88
88
  - quick 直接在主工作区改代码(不创建 worktree),范围必须小且明确
89
89
  - 完成后立即 `--done`,不跳过
90
- - QUICKLOG 记录由 **CLI 接管**:启动时 CLI 自动分配 ql-ID 并在 `.sillyspec/quicklog/QUICKLOG-<user>.md` 写「进行中」条目(含关联变更 tasks.md),完成时 CLI 自动翻「已完成」并勾选 task。**你无需手写 QUICKLOG / tasks.md**,只需用注入的 `<quicklog-id>` 在模块文档变更索引引用
90
+ - QUICKLOG 记录的**骨架由 CLI 接管**:启动时 CLI 自动分配 ql-ID 并在 `.sillyspec/quicklog/QUICKLOG-<user>.md` 写「进行中」条目(含关联变更 tasks.md),`--done` 时 CLI 自动翻「已完成」+ 勾选 task + 回填文件路径。ql-ID 分配/状态/task 你无需手写,只需用注入的 `<quicklog-id>` 在模块文档变更索引引用
91
+ - **QUICKLOG 正文精修(`--done` 后必做,不可跳过)**:CLI 落盘的是简版骨架(标题可能是 `(quick 任务)`、文件是单行逗号无括注),**不合格**。`--done` 成功后必须立即编辑本次 `<quicklog-id>` 条目,手动精修成丰富格式:① 标题改真实需求摘要(禁 `(quick 任务)`);② 文件改多行 bullet、每行带关键改动括注(禁 `(见实际改动)` 或无括注纯路径);③ 正文 `需求:`/`根因:`/`方案:`/`结果:` 四段逐项充实(禁只留一段「结果:」)。参照同文件早期丰富条目。一条 quick = 一条独立 ql,不追加到旧条目
91
92
  - **最后一步 `--done --output` 必须按结构化结果模板给全四字段**(逐项一句话):`需求:… 根因:… 方案:… 结果:…`。这是 QUICKLOG「结果:」归档的唯一来源;CLI 校验缺字段会拒绝 `--done`(exit 1),补全后重跑即可。前两个 step 的 `--output` 是中间摘要,不用此模板
92
93
  - **禁止**在没有运行 CLI 的情况下自行决定流程
93
94
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sillyspec",
3
- "version": "3.25.2",
3
+ "version": "3.25.4",
4
4
  "description": "SillySpec CLI — 流程状态机,让 AI 严格按步骤来",
5
5
  "icon": "logo.jpg",
6
6
  "homepage": "https://sillyspec.ppdmq.top/",
package/src/quicklog.js CHANGED
@@ -215,7 +215,7 @@ async function checkTaskCheckbox(specBase, change, qlId) {
215
215
  // 可能是 CRLF,split('\n') 后每行带 \r。原代码 `lines[i] === '状态:进行中'` 精确匹配
216
216
  // 恒失败 → 走 splice「兜底插入」→ 条目内同时出现「状态:已完成」+「状态:进行中」。
217
217
  // 状态行匹配与「已完成」判断统一用行首前缀匹配,容忍行尾 \r;写入保持 CRLF 不扩大改动。
218
- function flipEntryInContent(content, qlId, result) {
218
+ function flipEntryInContent(content, qlId, result, changedFiles = []) {
219
219
  const lines = content.split('\n')
220
220
  const startIdx = lines.findIndex(l => l.startsWith(`## ${qlId} |`))
221
221
  if (startIdx === -1) return null
@@ -228,6 +228,12 @@ function flipEntryInContent(content, qlId, result) {
228
228
  for (let i = startIdx + 1; i < endIdx; i++) {
229
229
  if (/^状态:进行中\r?$/.test(lines[i])) { lines[i] = '状态:已完成'; flipped = true }
230
230
  if (lines[i].startsWith('结果:')) hasResult = true
231
+ // 回填实际改动文件(调用方已用 isQuickMetadata 过滤 quick 自身元数据,只留业务文件)。
232
+ // 原地替换 lines[i],不改数组长度 → 与下方状态/结果 splice 的索引完全解耦。
233
+ // changedFiles 空(brownfield 无 guard / 调用方未传)→ 不动文件行,保持「(见实际改动)」。
234
+ if (changedFiles.length > 0 && lines[i].startsWith('文件:')) {
235
+ lines[i] = `文件:${changedFiles.join(', ')}`
236
+ }
231
237
  }
232
238
  if (!flipped && !lines.slice(startIdx, endIdx).some(l => /^状态:已完成\r?$/.test(l))) {
233
239
  // 无状态行(异常),插一条已完成
@@ -297,11 +303,13 @@ export async function allocateQuicklogEntry(specBase, gitUser, { description, li
297
303
  /**
298
304
  * 翻某 qlId 条目为「已完成」+ 追加结果 + 勾选关联 tasks.md。持锁。
299
305
  */
300
- export async function completeQuicklogEntry(specBase, gitUser, qlId, { resultText = '', linkedChanges = [] } = {}) {
306
+ export async function completeQuicklogEntry(specBase, gitUser, qlId, { resultText = '', linkedChanges = [], changedFiles = [] } = {}) {
301
307
  const quicklogDir = join(specBase, 'quicklog')
302
308
  const lockPath = join(quicklogDir, `.QUICKLOG-${(gitUser || 'unknown')}.md.lock`)
303
309
  const result = sanitizeResult(resultText)
304
310
  const linked = Array.isArray(linkedChanges) ? linkedChanges : []
311
+ // 实际改动文件(调用方 complete-handlers 已用 isQuickMetadata 过滤 quick 自身元数据)。空 → 不动文件行。
312
+ const realFiles = Array.isArray(changedFiles) ? changedFiles.filter(Boolean) : []
305
313
 
306
314
  await withFileLock(lockPath, async () => {
307
315
  // 条目可能在主文件或轮转归档中
@@ -309,7 +317,7 @@ export async function completeQuicklogEntry(specBase, gitUser, qlId, { resultTex
309
317
  const filePath = join(quicklogDir, f)
310
318
  let content = ''
311
319
  try { content = readFileSync(filePath, 'utf8') } catch { continue }
312
- const updated = flipEntryInContent(content, qlId, result)
320
+ const updated = flipEntryInContent(content, qlId, result, realFiles)
313
321
  if (updated !== null) {
314
322
  await writeAtomic(filePath, updated) // 命中处原子落盘(只改含目标条目的那一个文件)
315
323
  break
@@ -23,7 +23,7 @@
23
23
  import { basename, join, resolve, relative, isAbsolute } from 'node:path'
24
24
  import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, rmSync } from 'node:fs'
25
25
  import { renameSyncRetry, writeAtomicSync } from '../fs-atomic.js'
26
- import { resolveChangeDir, safeGit, auditQuickCompletion, triggerSync } from './shared.js'
26
+ import { resolveChangeDir, safeGit, auditQuickCompletion, triggerSync, isQuickMetadata } from './shared.js'
27
27
  import { stageRegistry } from '../stages/index.js'
28
28
  import { SCAN_STATUS, POINTER_STATUS } from '../constants.js'
29
29
  import { printQuickAuditReview } from './quick-audit.js'
@@ -559,6 +559,9 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
559
559
  const gitUser = safeGit(cwd, ['config', 'user.name']).value || 'unknown'
560
560
  let qlId = guard?.quicklogId || null
561
561
  const linkedChanges = Array.isArray(guard?.linkedChanges) ? guard.linkedChanges : []
562
+ // 审计结果(仅 guard 存在时填充)。提到 if(guard) 外声明,供下方回填 QUICKLOG 文件行复用
563
+ // review.changedFiles;brownfield 无 guard 时保持 null → 文件行不回填(降级,不报错)。
564
+ let review = null
562
565
 
563
566
  // 审计:仅在有 guard 时跑(brownfield 无 guard 跳过,兼容 D-003 brownfield 行为)。
564
567
  if (guard) {
@@ -570,7 +573,7 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
570
573
  forceBaseline: guard.forceBaseline || isForceBaseline,
571
574
  allowNew: guard.allowNew || isAllowNew,
572
575
  }
573
- const review = await auditQuickCompletion(cwd, mergedGuard, { isConfirm: confirm })
576
+ review = await auditQuickCompletion(cwd, mergedGuard, { isConfirm: confirm })
574
577
  printQuickAuditReview(review)
575
578
  if (review.status === 'blocked') {
576
579
  steps[currentIdx].status = 'pending'
@@ -632,9 +635,15 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
632
635
  // 翻状态进行中→已完成 + 追加结果 + 勾选关联 tasks.md
633
636
  // resultText 不再截断:结构化结果块(需求/根因/方案/结果)完整落盘,多行写成字段化块。
634
637
  try {
638
+ // 回填实际改动文件:review.changedFiles 含 quick 自身元数据(quicklog/.runtime/modules 等),
639
+ // 用 isQuickMetadata 过滤掉,只留真实业务文件。brownfield 无 review → 空数组,文件行不动。
640
+ const realFiles = Array.isArray(review?.changedFiles)
641
+ ? review.changedFiles.filter(f => !isQuickMetadata(f, linkedChanges))
642
+ : []
635
643
  await completeQuicklogEntry(specBase, gitUser, qlId, {
636
644
  resultText: outputText || '',
637
645
  linkedChanges,
646
+ changedFiles: realFiles,
638
647
  })
639
648
  console.log(`📝 QUICKLOG 条目 ${qlId} 已标记完成`)
640
649
  } catch (e) {
package/src/run/gates.js CHANGED
@@ -234,7 +234,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
234
234
  if (tier.tier === 'self') {
235
235
  console.log('\nℹ️ Stage Review: ' + stageName + ' tier=self(' + tier.reason + '),已降级为当前 agent 自审,不强制独立子代理。')
236
236
  } else {
237
- const reviewRunId = getLatestStageReviewRunId(runtimeRoot, stageName)
237
+ const reviewRunId = getLatestStageReviewRunId(runtimeRoot, stageName, changeName)
238
238
  const reviewType = stageName === 'brainstorm' ? 'design'
239
239
  : stageName === 'plan' ? 'plan'
240
240
  : stageName === 'propose' ? 'proposal'
package/src/run/prompt.js CHANGED
@@ -472,7 +472,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
472
472
  if (['brainstorm', 'plan', 'propose', 'execute'].includes(stageName) && promptText.includes('{REVIEW_TIER}')) {
473
473
  try {
474
474
  const { classifyReviewTier } = await import('../review-tier.js')
475
- const { generateStageReviewRunId, renderReviewJsonContract } = await import('../stage-review.js')
475
+ const { generateStageReviewRunId, renderReviewJsonContract, stageReviewMarkerPath } = await import('../stage-review.js')
476
476
  const tierSpecBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
477
477
  const tierChangeDir = changeName ? join(tierSpecBase, 'changes', changeName) : null
478
478
  const designPath = tierChangeDir ? join(tierChangeDir, 'design.md') : null
@@ -485,7 +485,21 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
485
485
  }
486
486
  }
487
487
  const tier = classifyReviewTier({ planLevel, designPath })
488
- const reviewRunId = generateStageReviewRunId()
488
+ // reviewRunId:优先读 marker 复用(保证多次渲染 prompt 注入同一 ID == gate 读取的 ID,
489
+ // 修复「prompt 多次渲染 / 多次 review 时 gate 取错 ID 读错 review.json」),marker 缺失才
490
+ // generate + 落盘。对齐 execute {EXECUTE_RUN_ID} 段(prompt.js:449-467)。
491
+ const tierRuntimeRoot = platformOpts?.runtimeRoot || join(tierSpecBase, '.runtime')
492
+ const reviewRunIdMarker = stageReviewMarkerPath(tierRuntimeRoot, stageName, changeName)
493
+ let reviewRunId = ''
494
+ try {
495
+ if (existsSync(reviewRunIdMarker)) {
496
+ reviewRunId = readFileSync(reviewRunIdMarker, 'utf8').trim()
497
+ }
498
+ } catch {}
499
+ if (!reviewRunId) {
500
+ reviewRunId = generateStageReviewRunId()
501
+ try { mkdirSync(tierRuntimeRoot, { recursive: true }); writeFileSync(reviewRunIdMarker, reviewRunId + '\n') } catch {}
502
+ }
489
503
  // review.json 产物契约(schema + 示例 + docHash 算法 + 重算提示)事前注入,与 validateStageReviewSchema 同源
490
504
  const reviewContractMd = renderReviewJsonContract({ stage: stageName, changeDir: tierChangeDir, reviewRunId, tier: tier.tier })
491
505
  promptText = promptText
package/src/run/shared.js CHANGED
@@ -188,6 +188,38 @@ export function parsePorcelainPath(line) {
188
188
  return path.replace(/\\/g, '/')
189
189
  }
190
190
 
191
+ /**
192
+ * quick 自身写入的 .sillyspec/ 元数据判定(auditQuickCompletion 审计 与 QUICKLOG「文件:」行回填
193
+ * 单源)。回填文件行时复用:review.changedFiles 含这些元数据时过滤掉,只留真实业务文件。
194
+ *
195
+ * 归类口径:
196
+ * - quicklog/.runtime/modules/_module-map/knowledge-uncategorized 等 quick 自身产物 → 元数据
197
+ * - .sillyspec/changes/:quick 自己没有该目录,其下文件要么是关联变更(reverse-sync),
198
+ * 要么是并发他者会话的工作。非关联变更目录整体视为元数据放行;关联变更文件不算(属本 quick 真实改动)
199
+ *
200
+ * 「并发工作 vs 偷建变更」的意图软判定留给 sillyhub,确定性校验只做路径归类。
201
+ *
202
+ * @param {string} p 文件路径(容错 \\ / 混用)
203
+ * @param {string[]} [linkedChanges] 关联变更名列表(影响 changes/ 归类)
204
+ * @returns {boolean} true=quick 元数据,应从业务文件列表过滤掉
205
+ */
206
+ export function isQuickMetadata(p, linkedChanges = []) {
207
+ const normalizeGitPath = (x) => String(x).replace(/\\/g, '/')
208
+ const file = normalizeGitPath(p)
209
+ if (file.startsWith('.sillyspec/quicklog/')
210
+ || file.startsWith('.sillyspec/.runtime/')
211
+ || file === '.sillyspec/knowledge/uncategorized.md'
212
+ || (/^\.sillyspec\/docs\/[^/]+\/modules\/[^/]+\.md$/.test(file))
213
+ || (/^\.sillyspec\/docs\/[^/]+\/modules\/_module-map\.yaml$/.test(file))) return true
214
+ if (file.startsWith('.sillyspec/changes/')) {
215
+ const linkedChangeNames = new Set((Array.isArray(linkedChanges) ? linkedChanges : []).map(c => normalizeGitPath(c)))
216
+ const m = file.match(/^\.sillyspec\/changes\/([^/]+)(\/|$)/)
217
+ // bare 折叠 token(git 把全新未跟踪 changes/ 折叠成 `changes/`)或具体但非关联的变更 → 放行
218
+ if (!m || !linkedChangeNames.has(m[1])) return true
219
+ }
220
+ return false
221
+ }
222
+
191
223
  /**
192
224
  * quick 完成审计:对比 baseline 与实际变更。
193
225
  * @returns {{ status: 'safe'|'warning'|'blocked', reasons: string[], changedFiles: string[], newFiles: string[], deletedFiles: string[], baselineHit: string[] }}
@@ -215,26 +247,6 @@ export async function auditQuickCompletion(cwd, guard, options = {}) {
215
247
  const f = normalizeGitPath(p)
216
248
  return baselineExact.has(f) || baselineDirs.some(d => f.startsWith(d))
217
249
  }
218
- // quick 自己没有 .sillyspec/changes/ 目录——该路径下任何文件要么是关联变更(reverse-sync),
219
- // 要么是并发其他会话的变更工作。多会话并行时别人的 changes/ 不应被本 quick 审计拦截
220
- // (确定性校验无法区分「并发工作」与「本 quick 偷建变更」,后者这类意图软判定留给 sillyhub)。
221
- // 故:非关联变更目录整体放行;关联变更的文件仍走正常审计(reverse-sync 可见,需 --force-baseline)。
222
- const linkedChangeNames = new Set((Array.isArray(guard.linkedChanges) ? guard.linkedChanges : []).map(c => normalizeGitPath(c)))
223
- const isQuickMetadata = (p) => {
224
- const file = normalizeGitPath(p)
225
- if (file.startsWith('.sillyspec/quicklog/')
226
- || file.startsWith('.sillyspec/.runtime/')
227
- || file === '.sillyspec/knowledge/uncategorized.md'
228
- || (/^\.sillyspec\/docs\/[^/]+\/modules\/[^/]+\.md$/.test(file))
229
- || (/^\.sillyspec\/docs\/[^/]+\/modules\/_module-map\.yaml$/.test(file))) return true
230
- // .sillyspec/changes/ 下:bare 折叠 token(git 把全新未跟踪 changes/ 折叠成 `changes/`)
231
- // 或具体但【非关联】的变更 → 都属并发其他会话工作,放行;关联变更文件仍走审计。
232
- if (file.startsWith('.sillyspec/changes/')) {
233
- const m = file.match(/^\.sillyspec\/changes\/([^/]+)(\/|$)/)
234
- if (!m || !linkedChangeNames.has(m[1])) return true
235
- }
236
- return false
237
- }
238
250
  const DANGEROUS_PATTERNS = [
239
251
  'package.json',
240
252
  'package-lock.json',
@@ -269,7 +281,7 @@ export async function auditQuickCompletion(cwd, guard, options = {}) {
269
281
  }
270
282
 
271
283
  // 检查危险文件(除非 force-baseline)
272
- if (file.startsWith('.sillyspec/') && !isQuickMetadata(file) && !forceBaseline) {
284
+ if (file.startsWith('.sillyspec/') && !isQuickMetadata(file, guard.linkedChanges) && !forceBaseline) {
273
285
  result.reasons.push(`危险文件变更: ${file}`)
274
286
  }
275
287
 
@@ -293,7 +305,7 @@ export async function auditQuickCompletion(cwd, guard, options = {}) {
293
305
  // 检查 new files(除非 allow-new)
294
306
  if (!allowNew) {
295
307
  for (const f of result.newFiles) {
296
- if (!isQuickMetadata(f)) {
308
+ if (!isQuickMetadata(f, guard.linkedChanges)) {
297
309
  result.reasons.push(`新增文件(需 --allow-new): ${f}`)
298
310
  }
299
311
  }
@@ -302,7 +314,7 @@ export async function auditQuickCompletion(cwd, guard, options = {}) {
302
314
  // 检查 allowedFiles 范围
303
315
  if (allowedFiles.length > 0) {
304
316
  for (const f of result.changedFiles) {
305
- if (!allowedFiles.includes(f) && !isQuickMetadata(f)) {
317
+ if (!allowedFiles.includes(f) && !isQuickMetadata(f, guard.linkedChanges)) {
306
318
  result.reasons.push(`超出 allowedFiles: ${f}`)
307
319
  }
308
320
  }
package/src/run.js CHANGED
@@ -12,8 +12,8 @@
12
12
  */
13
13
  // W6 Step8c: 主命令分发(runCommand)+ auto 模式 + ensureStageSteps 搬至 ./run/command.js
14
14
  export { runCommand, ensureStageSteps } from './run/command.js'
15
- // barrel re-export: parsePorcelainPath + auditQuickCompletion 被 test 直接 import(契约保留)
16
- export { parsePorcelainPath, auditQuickCompletion } from './run/shared.js'
15
+ // barrel re-export: parsePorcelainPath + auditQuickCompletion + isQuickMetadata 被 test 直接 import(契约保留)
16
+ export { parsePorcelainPath, auditQuickCompletion, isQuickMetadata } from './run/shared.js'
17
17
  // barrel re-export: applyRootPlaceholders 被 test/prompt-placeholders.test.mjs 直接 import(契约保留)
18
18
  export { applyRootPlaceholders } from './run/prompt.js'
19
19
  // barrel re-export: sanitizeProjectName + validateParsedProjects 被 test 直接 import(随 handleScan 搬走,契约保留)
@@ -175,8 +175,9 @@ export function validateStageReviewSchema(review) {
175
175
  * 对应 task-review.verifyReviewGitEvidence 的"防伪造"职责——agent 不能凭空编一份
176
176
  * review.json 贴个假 hash。约定 docHash 对应 reviewedFiles[0](主审查对象)。
177
177
  *
178
- * searchDirs 依次尝试解析 reviewedFiles 的相对路径(specBase / changeDir / cwd 都可能)。
179
- * 主文档在所有基准下都找不到时降级 warning,不阻断(不因路径基准差异误杀)。
178
+ * searchDirs 依次尝试解析 reviewedFiles[0] 的相对路径(effectiveSpecBase / changeDir / cwd)。
179
+ * 主文档在所有基准下都找不到时 fail-closed 阻断——连主审查文档都定位不到,要么 reviewedFiles[0]
180
+ * 是伪造路径(防 agent 填假路径跳过校验),要么路径基准严重错位,两者都不该静默放行。
180
181
  *
181
182
  * @param {object} review - 已通过 schema 校验
182
183
  * @param {string[]} searchDirs - 解析相对路径的候选基准目录(绝对路径)
@@ -204,9 +205,13 @@ export function verifyStageReviewDocHash(review, searchDirs) {
204
205
  return { ok: true, errors, warnings }
205
206
  }
206
207
 
207
- // 所有基准都找不到主文档:路径基准差异,降级 warning
208
- warnings.push(`主审查文档 ${primaryRel} 在候选目录下均不存在,跳过 docHash 校验(可能路径基准不同)`)
209
- return { ok: true, errors, warnings }
208
+ // 所有基准都找不到主文档 → fail-closed。连主审查文档都定位不到,说明 reviewedFiles[0] 要么
209
+ // 是伪造路径(恶意 agent 填假路径跳过 docHash 校验),要么路径基准严重错位——两者都该阻断
210
+ // 而非静默放行(历史降级 warning 是可被利用的伪造通道)。searchDirs =
211
+ // [effectiveSpecBase(.sillyspec), reviewChangeDir(changes/<change>), cwd],契约规定的
212
+ // reviewedFiles[0](changes/<change>/<mainDoc> 或 <mainDoc>)在这三个基准下必命中其一。
213
+ errors.push('主审查文档 ' + primaryRel + ' 在 ' + (searchDirs || []).length + ' 个候选基准目录下均不存在 — 无法做 docHash 校验(reviewedFiles[0] 路径伪造或基准错位)')
214
+ return { ok: false, errors, warnings }
210
215
  }
211
216
 
212
217
  /**
@@ -220,13 +225,45 @@ export function generateStageReviewRunId() {
220
225
  }
221
226
 
222
227
  /**
223
- * 获取最新的 stage review run id(扫描 stage-reviews/<stage>-review-* 目录取最新)
228
+ * stage review marker 文件路径(prompt 渲染时写 / getLatestStageReviewRunId 读,同源)。
229
+ *
230
+ * 对齐 execute 的 current-execute-run-id-<change> 语义:含 change 防多 change 串台
231
+ * (同一 stage 不同变更的 review 各自独立 marker);changeName 缺失时退化为按 stage 分。
232
+ *
233
+ * @param {string} runtimeRoot - .runtime 绝对路径
234
+ * @param {string} stage - brainstorm|plan|propose|execute
235
+ * @param {string} [changeName] - 变更名(防多 change 串台)
236
+ * @returns {string} marker 文件绝对路径
237
+ */
238
+ export function stageReviewMarkerPath(runtimeRoot, stage, changeName) {
239
+ const suffix = changeName ? `${stage}-${changeName}` : stage
240
+ return join(runtimeRoot, `current-stage-review-run-id-${suffix}`)
241
+ }
242
+
243
+ /**
244
+ * 获取最新的 stage review run id
245
+ *
246
+ * 优先读 marker 文件(prompt 渲染 {REVIEW_TIER} 时写入,保证 gate 取的 ID == 注入给 agent
247
+ * 的 ID —— 修复「prompt 多次渲染 / 多次 review 时 gate 取错 ID 读错 review.json」),
248
+ * 对齐 getLatestExecuteRunId(task-review.js:420-428)。marker 缺失时 fallback 扫描
249
+ * stage-reviews/<stage>-review-* 目录取字典序最新(向后兼容无 marker 旧数据)。
224
250
  *
225
251
  * @param {string} runtimeRoot - .runtime 绝对路径
226
252
  * @param {string} stage - brainstorm|plan|propose|execute
253
+ * @param {string} [changeName] - 变更名(读对应 marker,防多 change 串台)
227
254
  * @returns {string|null} run id(如 'review-2026-07-16-143000'),无则 null
228
255
  */
229
- export function getLatestStageReviewRunId(runtimeRoot, stage) {
256
+ export function getLatestStageReviewRunId(runtimeRoot, stage, changeName) {
257
+ // 优先读 marker(prompt 写 / gate 读同源,对齐 execute current-execute-run-id 机制)
258
+ try {
259
+ const markerPath = stageReviewMarkerPath(runtimeRoot, stage, changeName)
260
+ if (existsSync(markerPath)) {
261
+ const content = readFileSync(markerPath, 'utf8').trim()
262
+ if (content) return content
263
+ }
264
+ } catch {}
265
+
266
+ // fallback:扫描 stage-reviews/<stage>-review-* 目录取字典序最新(向后兼容无 marker 旧数据)
230
267
  const dir = join(runtimeRoot, 'stage-reviews')
231
268
  if (!existsSync(dir)) return null
232
269
  try {
@@ -202,6 +202,34 @@ export function topoSortWaves(depMap) {
202
202
  return { waves, error: null }
203
203
  }
204
204
 
205
+ /**
206
+ * 解析 plan.md 显式 Wave 分组 → Map<taskId, waveIndex>。
207
+ *
208
+ * Wave 口径与 execute.js parseWavesFromPlan 一致(execute 的真实并行单元,非 topoSort 建议值):
209
+ * - `## Wave N` 标题下的 task-XX checkbox 归入 Wave N;
210
+ * - 非 Wave 标题行(## 自检 等)退出当前 Wave 段,避免其 checkbox 混入。
211
+ * 返回 null = plan.md 不存在或无显式 Wave 标题 → execute 会把所有 task 放进单个隐式 Wave
212
+ * 全并行(execute.js:402-408),调用方据此判「同文件即冲突」。
213
+ *
214
+ * @param {string} planPath - plan.md 绝对路径
215
+ * @returns {Map<string, number>|null} taskId → waveIndex;null = 无显式 Wave
216
+ */
217
+ function parseTaskWavesFromPlan(planPath) {
218
+ if (!existsSync(planPath)) return null
219
+ const content = readFileSync(planPath, 'utf8')
220
+ if (!/^#+\s*Wave\s+\d+/im.test(content)) return null
221
+ const map = new Map()
222
+ let currentWave = null
223
+ for (const line of content.split('\n')) {
224
+ const wm = line.match(/^#+\s*Wave\s+(\d+)/i)
225
+ if (wm) { currentWave = parseInt(wm[1], 10); continue }
226
+ if (/^#+\s/.test(line)) { currentWave = null; continue } // 非 Wave 标题退出当前 Wave 段
227
+ const tm = line.match(/^[-*]\s*\[[ x]\]\s*(task-\d+)/i)
228
+ if (tm && currentWave !== null) map.set(tm[1], currentWave)
229
+ }
230
+ return map
231
+ }
232
+
205
233
  /**
206
234
  * 本地一致性校验器
207
235
  * @param {string} changeDir - 变更目录
@@ -258,10 +286,40 @@ export function validateBlueprintConsistency(changeDir) {
258
286
  }
259
287
  }
260
288
 
261
- // 路径冲突
289
+ // 路径冲突(Wave 感知):同 Wave 内 >1 task 共享 allowed_path → execute 强制并行(execute.js:603)
290
+ // 子代理会互相覆盖该文件 → error。跨 Wave 同文件 → 串行执行安全 → warning。
291
+ // Wave 口径 = plan.md 显式 `## Wave N`(parseTaskWavesFromPlan,与 execute 同源,非 topoSort 建议值);
292
+ // plan.md 无显式 Wave → execute 全并行(隐式单 Wave,execute.js:402-408)→ 同文件即冲突。
293
+ const waveOfTask = parseTaskWavesFromPlan(pJoin(changeDir, 'plan.md'))
262
294
  for (const [p, owners] of pathOwners) {
263
- if (owners.length > 1) {
264
- warnings.push(`路径 ${p} 被 ${owners.length} 个 task 修改: ${owners.join(', ')}(确认是否为有意共享)`)
295
+ if (owners.length < 2) continue
296
+ if (waveOfTask === null) {
297
+ errors.push(
298
+ `路径 ${p} 被 ${owners.length} 个 task 修改: ${owners.join(', ')} — plan.md 无显式 Wave 划分,` +
299
+ `execute 会把它们放进同一隐式 Wave 全并行(execute.js:402-408),子代理互相覆盖。` +
300
+ `解法:拆到不同 Wave(串行),或合并为单个 task。`
301
+ )
302
+ continue
303
+ }
304
+ // 显式 Wave:按 wave 分组 owners,找同 Wave 冲突
305
+ const byWave = new Map()
306
+ for (const t of owners) {
307
+ const w = waveOfTask.get(t)
308
+ if (w === undefined) continue // 未列入任何 Wave → execute 不执行,不参与冲突判定
309
+ if (!byWave.has(w)) byWave.set(w, [])
310
+ byWave.get(w).push(t)
311
+ }
312
+ for (const [w, ts] of byWave) {
313
+ if (ts.length > 1) {
314
+ errors.push(
315
+ `路径 ${p} 被 Wave ${w} 内 ${ts.length} 个 task 修改: ${ts.join(', ')} — 同 Wave 任务 execute 强制并行(execute.js:603),` +
316
+ `子代理会互相覆盖该文件。解法:把它们拆到不同 Wave(串行)。`
317
+ )
318
+ }
319
+ }
320
+ // 跨 Wave 共享:串行安全,仅提示
321
+ if (byWave.size > 1) {
322
+ warnings.push(`路径 ${p} 跨 Wave 被修改: ${owners.join(', ')}(不同 Wave 串行执行,安全;确认是否有意共享)`)
265
323
  }
266
324
  }
267
325
 
@@ -468,8 +526,9 @@ export function validatePlanFeasibility(changeDir, projectRoot = null) {
468
526
  // 1. 必要字段检查
469
527
  const taskId = (fm.match(/^id:\s*(.+)/m)?.[1] || '').trim()
470
528
  const title = (fm.match(/^title:\s*(.+)/m)?.[1] || '').trim()
471
- const allowedPathsRaw = fm.match(/allowed_paths:\s*\n((?:\s+-\s+.+\n?)+)/)?.[1] || ''
472
- const allowedPaths = allowedPathsRaw.match(/-\s+(.+)/g)?.map(s => s.replace(/^-\s+/, '').trim()) || []
529
+ // 复用 parseAllowedPaths(认 inline [x] + 块式两种写法),与 blueprint consistency / design coverage
530
+ // 三处统一,消除「feasibility 只认块式 → inline 写法被误报 allowed_paths 为空」的漂移(transcript 卡 3 轮根因)。
531
+ const allowedPaths = parseAllowedPaths(content)
473
532
 
474
533
  if (!taskId) errors.push(fsRule.data.messageId.replaceAll('${id}', file))
475
534
  if (!title) errors.push(fsRule.data.messageTitle.replaceAll('${id}', file))
@@ -31,7 +31,9 @@ export const definition = {
31
31
  - 在 \`{SPEC_ROOT}/quicklog/QUICKLOG-<git-user>.md\` 写入「进行中」条目(含关联变更与预估文件)
32
32
  - 对每个关联变更 \`<c>\`:在 \`{SPEC_ROOT}/changes/<c>/tasks.md\` 追加未勾选 task \`- [ ] <quicklog-id> <任务描述>\`
33
33
 
34
- **你不要创建或修改任何 QUICKLOG / tasks.md 记录**——完成本任务时 CLI 会自动将条目翻为「已完成」并勾选 task(含轮转)。\`<quicklog-id>\` 可用于 design.md / plan.md / archive / 模块变更索引引用。
34
+ **你不要创建或修改任何 QUICKLOG 的条目骨架 / tasks.md 记录**——ql-ID 分配、「进行中」条目、状态翻转、task 勾选、轮转全由 CLI 接管,你无需手写。<quicklog-id> 可用于 design.md / plan.md / archive / 模块变更索引引用。
35
+
36
+ > ⚠️ 例外:step 3 --done 之后,CLI 落盘的 QUICKLOG 只是简版骨架,**你必须手动精修正文成丰富格式**(标题语义化 + 文件多行带括注 + 四段充实),详见 step 3「QUICKLOG 正文精修」。这是 quick 交付质量的一部分,不可跳过。
35
37
 
36
38
  ### 输出
37
39
  任务理解 + 上下文摘要`,
@@ -85,7 +87,7 @@ export const definition = {
85
87
  2. 使用 \`git add -- <file...>\` 暂存本次 quick 实际修改的文件(不要 commit,由用户通过统一提交工具处理)
86
88
  - 禁止使用 \`git add -A\`
87
89
  - 不要暂存 quick 开始前就已存在的无关改动
88
- 3. QUICKLOG / tasks.md 记录由 CLI 在本 step 完成时自动收尾(翻「已完成」+ 勾选 task + 轮转),你无需手动更新
90
+ 3. QUICKLOG / tasks.md 记录由 CLI 在本 step 完成时自动收尾(翻「已完成」+ 勾选 task + 轮转),你无需手动建/翻骨架——但**正文精修必做**(见下方「QUICKLOG 正文精修」)
89
91
  4. 如果发现项目特有的坑,追加到 \`{SPEC_ROOT}/knowledge/uncategorized.md\`
90
92
  5. 任务比预期复杂 → 建议用完整流程
91
93
 
@@ -100,8 +102,20 @@ export const definition = {
100
102
  - 使用 \`git add -- <module-doc>\` 暂存更新的模块文件
101
103
  9. 未命中任何模块 → 跳过,不做额外操作
102
104
 
103
- ### 输出
104
- 暂存确认 + 记录路径 + 模块文档同步结果(如有)`,
105
+ ### 输出
106
+ 暂存确认 + 记录路径 + 模块文档同步结果(如有)
107
+
108
+ ### 📝 QUICKLOG 正文精修(--done 之后必做,不可跳过)
109
+ sillyspec run quick --done 成功后,CLI 已在 {SPEC_ROOT}/quicklog/QUICKLOG-<git-user>.md 落盘本次 <quicklog-id> 条目的简版骨架:状态翻「已完成」、「文件:」是单行逗号路径、正文是 --output 的四字段。骨架是机械产物不达标,你必须立即编辑该条目手动精修成丰富格式,三项必改:
110
+
111
+ 1. 标题:把 ## <quicklog-id> | <时间> | <标题> 中的 <标题> 改为真实需求摘要(如「登录加 IP 限流(5 次/分)+ 失败 3 次滑块验证」)。禁止保留 CLI 兜底的 (quick 任务) 或一句话笼统描述。
112
+ 2. 文件:把 CLI 写的单行「文件:a.js, b.js」改写为多行 bullet,每个文件带关键改动括注;禁止「(见实际改动)」或无括注纯路径单行。示例:
113
+ 文件:
114
+ - backend/app/modules/auth/router.py(登录端点串 check_rate_limit→assert_captcha_if_needed)
115
+ - backend/app/modules/auth/captcha_service.py(新建:限流 INCR + 失败计数 + Pillow 滑块生成/校验)
116
+ 3. 正文:核对 需求: 根因: 方案: 结果: 四段逐项充实(--output 已写、CLI 原样插入,按需扩写),不允许只留一段「结果:」。
117
+
118
+ 一条 quick = 一条独立 ql 条目;精修只动本次 <quicklog-id> 条目,不追加到旧条目。QUICKLOG 在 .sillyspec/(gitignore),精修不影响 --done 已通过的边界审计。`,
105
119
  outputHint: '暂存和记录确认',
106
120
  optional: false
107
121
  }
package/src/sync.js CHANGED
@@ -26,6 +26,13 @@ const LOCAL_YAML = '.sillyspec/local.yaml';
26
26
  const CHANGES_DIR = '.sillyspec/changes';
27
27
  const REQUEST_TIMEOUT_MS = 10_000;
28
28
 
29
+ // 未连接平台是本地独立用户的合法默认状态。sync / syncDocuments / checkApproval 由 run 流程
30
+ // 在后台 best-effort 触发(每步完成、execute 阶段启动),未连接时默认静默跳过——不每步催连
31
+ // 平台制造噪音(本地用户根本不需要平台)。需要排查同步行为时设 SILLYSPEC_DEBUG_SYNC=1。
32
+ function debugLog(msg) {
33
+ if (process.env.SILLYSPEC_DEBUG_SYNC) console.warn(msg)
34
+ }
35
+
29
36
  /** 四件套文档文件名 */
30
37
  const DOCUMENT_FILES = ['proposal.md', 'design.md', 'requirements.md', 'tasks.md'];
31
38
 
@@ -193,7 +200,7 @@ export class SyncManager {
193
200
  async sync(changeName) {
194
201
  const platform = this._getPlatform();
195
202
  if (!platform) {
196
- console.warn('[sync] 未连接平台,请先 sillyspec platform connect');
203
+ debugLog('[sync] 未连接平台(本地合法状态);如需平台同步:sillyspec platform connect');
197
204
  return { synced: 0, errors: ['未连接平台'] };
198
205
  }
199
206
 
@@ -255,7 +262,7 @@ export class SyncManager {
255
262
  async syncDocuments(changeName) {
256
263
  const platform = this._getPlatform();
257
264
  if (!platform) {
258
- console.warn('[sync] 未连接平台,请先 sillyspec platform connect');
265
+ debugLog('[sync] 未连接平台(本地合法状态);如需平台同步:sillyspec platform connect');
259
266
  return { synced: 0, errors: ['未连接平台'] };
260
267
  }
261
268
 
@@ -317,7 +324,7 @@ export class SyncManager {
317
324
  async checkApproval(changeName) {
318
325
  const platform = this._getPlatform();
319
326
  if (!platform) {
320
- console.warn('[sync] 未连接平台,请先 sillyspec platform connect');
327
+ debugLog('[sync] 未连接平台(本地合法状态);如需平台同步:sillyspec platform connect');
321
328
  return { status: 'pending', reason: '未连接平台' };
322
329
  }
323
330
 
@@ -185,11 +185,16 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
185
185
 
186
186
  // --- 4.5 校验:主工作区 baseline 是否变化(防 execute 期间主工作区被修改)---
187
187
  // 注意:必须和 computeBaselineHash (worktree.js) 使用相同的排除规则。
188
- // 排除 execute 流程自身会改的元数据(否则每个 execute 收尾都因自身改动 baseline 漂移 BLOCKED):
188
+ // 排除非交付物的元数据/文档 churn(execute 自身改的 + 多操作者常改的 agent 指引/文档),
189
+ // 否则别人改 CLAUDE.md/docs/.claude → 整树 hash 变 → apply 误阻断(多操作者仓库高频踩坑)。
190
+ // 安全:放过它们不丢真冲突——下方 step5a(未提交∩changedFiles)+step5b(HEAD blob) 对交付文件
191
+ // 做精确 per-file 校验,不读这里的 exclude。
189
192
  // - .sillyspec/:plan/design 蓝图 + runtime 产物
190
- // - docs/sillyspec/:工具坑文件(CLAUDE.md 规则要求 execute 期间记录)
193
+ // - .claude/:agent 配置/skills/CLAUDE.md
194
+ // - docs/:文档(deliverable 文档冲突由 step5a/5b 兜底)
195
+ // - CLAUDE.md:根 agent 指引
191
196
  if (meta.baselineHash) {
192
- const exclude = '-- . ":(exclude).sillyspec/" ":(exclude)docs/sillyspec/"';
197
+ const exclude = '-- . ":(exclude).sillyspec/" ":(exclude).claude/" ":(exclude)docs/" ":(exclude)CLAUDE.md"';
193
198
  const staged = gitQuiet(projectRoot, `diff --cached ${exclude}`) || '';
194
199
  const unstaged = gitQuiet(projectRoot, `diff ${exclude}`) || '';
195
200
  const untracked = gitQuiet(projectRoot, `ls-files --others --exclude-standard ${exclude}`) || '';
package/src/worktree.js CHANGED
@@ -168,11 +168,16 @@ function sleepMs(ms) {
168
168
  }
169
169
 
170
170
  export function computeBaselineHash(cwd) {
171
- // 排除 execute 流程自身会改的元数据,避免漂移误报(否则每个 execute 收尾都因自身改动 BLOCKED):
171
+ // 排除非交付物的元数据/文档 churn,避免多操作者仓库里别人改这些 → 整树 hash 变 → apply 误阻断
172
+ // (execute 自身也会改 .sillyspec/docs.sillyspec,否则每个 execute 收尾都因自身改动 BLOCKED)。
173
+ // 安全:coarse hash 放过这些不丢真冲突——applyWorktree step5a(未提交∩changedFiles)+step5b(HEAD blob)
174
+ // 对交付文件做精确 per-file 校验,不读这里的 exclude。
172
175
  // - .sillyspec/:brainstorm/plan 蓝图 + runtime 产物
173
- // - docs/sillyspec/:工具坑文件(CLAUDE.md 规则要求 execute 期间记录)
176
+ // - .claude/:agent 配置/skills/CLAUDE.md(多操作者 agent 指引 churn)
177
+ // - docs/:文档(非代码交付物;deliverable 文档冲突由 step5a/5b 兜底)
178
+ // - CLAUDE.md:根 agent 指引(多操作者常改)
174
179
  // 必须和 applyWorktree step 4.5 (worktree-apply.js) 使用相同的排除规则。
175
- const exclude = '-- . ":(exclude).sillyspec/" ":(exclude)docs/sillyspec/"';
180
+ const exclude = '-- . ":(exclude).sillyspec/" ":(exclude).claude/" ":(exclude)docs/" ":(exclude)CLAUDE.md"';
176
181
  const staged = gitQuiet(cwd, `diff --cached ${exclude}`) || '';
177
182
  const unstaged = gitQuiet(cwd, `diff ${exclude}`) || '';
178
183
  const untracked = gitQuiet(cwd, `ls-files --others --exclude-standard ${exclude}`) || '';
@@ -1 +0,0 @@
1
- [ 809ms] [ERROR] Failed to load resource: the server responded with a status of 404 (File not found) @ http://localhost:8848/favicon.ico:0
@@ -1,144 +0,0 @@
1
- - generic [active] [ref=e1]:
2
- - banner [ref=e2]:
3
- - generic [ref=e3]:
4
- - generic [ref=e4]:
5
- - text: Silly
6
- - emphasis [ref=e5]: Spec
7
- - generic [ref=e6]: Prompt Dossier
8
- - generic [ref=e7]:
9
- - text: ⌕
10
- - textbox "搜索步骤名 / prompt 内容 / 占位符…" [ref=e8]
11
- - generic [ref=e9]:
12
- - generic [ref=e10]: 12 stages · 71 steps · 87k
13
- - button "◐" [ref=e11] [cursor=pointer]
14
- - generic [ref=e87]:
15
- - navigation [ref=e88]:
16
- - generic [ref=e89]:
17
- - generic [ref=e90]:
18
- - text: 主流程
19
- - generic [ref=e91]: "4"
20
- - generic [ref=e92] [cursor=pointer]:
21
- - generic [ref=e93]: "01"
22
- - generic [ref=e94]: 头脑风暴
23
- - generic [ref=e95]: "8"
24
- - generic [ref=e96] [cursor=pointer]:
25
- - generic [ref=e97]: "02"
26
- - generic [ref=e98]: 实现计划
27
- - generic [ref=e99]: "5"
28
- - generic [ref=e100] [cursor=pointer]:
29
- - generic [ref=e101]: "03"
30
- - generic [ref=e102]: 波次执行
31
- - generic [ref=e103]: "12"
32
- - generic [ref=e104] [cursor=pointer]:
33
- - generic [ref=e105]: "04"
34
- - generic [ref=e106]: 验证
35
- - generic [ref=e107]: "7"
36
- - generic [ref=e108]:
37
- - generic [ref=e109]:
38
- - text: 辅助
39
- - generic [ref=e110]: "6"
40
- - generic [ref=e111] [cursor=pointer]:
41
- - generic [ref=e112]: "05"
42
- - generic [ref=e113]: 项目扫描
43
- - generic [ref=e114]: "11"
44
- - generic [ref=e115] [cursor=pointer]:
45
- - generic [ref=e116]: "06"
46
- - generic [ref=e117]: 快速任务
47
- - generic [ref=e118]: "3"
48
- - generic [ref=e119] [cursor=pointer]:
49
- - generic [ref=e120]: "07"
50
- - generic [ref=e121]: 自由探索
51
- - generic [ref=e122]: "1"
52
- - generic [ref=e123] [cursor=pointer]:
53
- - generic [ref=e124]: "08"
54
- - generic [ref=e125]: 归档
55
- - generic [ref=e126]: "5"
56
- - generic [ref=e127] [cursor=pointer]:
57
- - generic [ref=e128]: "09"
58
- - generic [ref=e129]: 状态
59
- - generic [ref=e130]: "3"
60
- - generic [ref=e131] [cursor=pointer]:
61
- - generic [ref=e132]: "10"
62
- - generic [ref=e133]: 自检
63
- - generic [ref=e134]: "5"
64
- - generic [ref=e135]:
65
- - generic [ref=e136]:
66
- - text: 变体
67
- - generic [ref=e137]: "1"
68
- - generic [ref=e138] [cursor=pointer]:
69
- - generic [ref=e139]: "11"
70
- - generic [ref=e140]: 自动模式头脑风暴
71
- - generic [ref=e141]: "4"
72
- - generic [ref=e142]:
73
- - generic [ref=e143]:
74
- - text: 已废弃
75
- - generic [ref=e144]: "1"
76
- - generic [ref=e145] [cursor=pointer]:
77
- - generic [ref=e146]: "12"
78
- - generic [ref=e147]: propose · 已废弃
79
- - generic [ref=e148]: "7"
80
- - main [ref=e149]:
81
- - generic [ref=e150]: 主流程 · STAGE 01
82
- - heading "头脑风暴" [level=1] [ref=e151]
83
- - paragraph [ref=e152]: 探索需求、分析技术方案、识别风险
84
- - generic [ref=e153]:
85
- - generic [ref=e154]: sourcesrc/stages/brainstorm.js
86
- - generic [ref=e155]: persona资深架构师
87
- - generic [ref=e156]: steps8
88
- - generic [ref=e157]:
89
- - button "全部展开" [ref=e158] [cursor=pointer]
90
- - button "全部折叠" [ref=e159] [cursor=pointer]
91
- - generic [ref=e160]: 8 STEPS
92
- - generic [ref=e161]:
93
- - article [ref=e162]:
94
- - generic [ref=e163] [cursor=pointer]:
95
- - generic [ref=e164]: 01 / 08
96
- - generic [ref=e165]: 状态检查
97
- - generic [ref=e166]: ▸
98
- - article [ref=e167]:
99
- - generic [ref=e168] [cursor=pointer]:
100
- - generic [ref=e169]: 02 / 08
101
- - generic [ref=e170]: 加载项目上下文
102
- - generic [ref=e171]: ▸
103
- - article [ref=e172]:
104
- - generic [ref=e173] [cursor=pointer]:
105
- - generic [ref=e174]: 03 / 08
106
- - generic [ref=e175]: 对话式探索与需求澄清
107
- - generic [ref=e176]:
108
- - generic [ref=e177]: conditionalWait
109
- - generic [ref=e178]: repeatable×8
110
- - generic [ref=e179]: ▸
111
- - article [ref=e180]:
112
- - generic [ref=e181] [cursor=pointer]:
113
- - generic [ref=e182]: 04 / 08
114
- - generic [ref=e183]: 提出 2-3 种方案
115
- - generic [ref=e184]: requiresWait
116
- - generic [ref=e186]: ▸
117
- - article [ref=e187]:
118
- - generic [ref=e188] [cursor=pointer]:
119
- - generic [ref=e189]: 05 / 08
120
- - generic [ref=e190]: 分段展示设计
121
- - generic [ref=e191]: requiresWait
122
- - generic [ref=e193]: ▸
123
- - article [ref=e194]:
124
- - generic [ref=e195] [cursor=pointer]:
125
- - generic [ref=e196]: 06 / 08
126
- - generic [ref=e197]: 写设计文档并自审
127
- - generic [ref=e198]: ▸
128
- - article [ref=e199]:
129
- - generic [ref=e200] [cursor=pointer]:
130
- - generic [ref=e201]: 07 / 08
131
- - generic [ref=e202]: Design Grill 交叉审查
132
- - generic [ref=e203]: conditionalWait
133
- - generic [ref=e205]: ▸
134
- - article [ref=e206]:
135
- - generic [ref=e207] [cursor=pointer]:
136
- - generic [ref=e208]: 08 / 08
137
- - generic [ref=e209]: 用户确认并生成规范文件
138
- - generic [ref=e210]: requiresWait
139
- - generic [ref=e212]: ▸
140
- - contentinfo [ref=e213]:
141
- - text: 数据机械提取自
142
- - code [ref=e214]: src/stages/*.js
143
- - text: · 单文件自包含 · file:// 可直接打开 · 生成命令:
144
- - code [ref=e215]: node docs/prompt/_build-site.mjs
Binary file
Binary file