sillyspec 3.29.5 → 3.30.0

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 (110) hide show
  1. package/.claude/skills/sillyspec-auto/SKILL.md +54 -54
  2. package/.claude/skills/sillyspec-brainstorm/SKILL.md +2 -2
  3. package/.claude/skills/sillyspec-commit/SKILL.md +1 -1
  4. package/.claude/skills/sillyspec-flow/SKILL.md +57 -0
  5. package/.claude/skills/sillyspec-quick/SKILL.md +18 -116
  6. package/AGENTS.md +38 -20
  7. package/SKILL.md +9 -3
  8. package/assets/command-cards/flow.md +32 -0
  9. package/assets/command-cards/run-brainstorm.md +1 -1
  10. package/assets/command-cards/run-quick.md +8 -25
  11. package/package.json +2 -1
  12. package/packages/dashboard/server/executor.js +2 -1
  13. package/packages/dashboard/server/parser.js +7 -7
  14. package/r7-test-env/README-R7-TEST.md +93 -0
  15. package/r7-test-env/demo-project/lib/calc.js +6 -0
  16. package/r7-test-env/demo-project/package.json +1 -0
  17. package/r7-test-env/demo-project/test.js +10 -0
  18. package/r7-test-env/prompt-R7-SMOKE.md +41 -0
  19. package/r7-test-env/sillyspec-r7.cmd +3 -0
  20. package/round4/account.mjs +18 -5
  21. package/round4/b1-analyze-task08.mjs +40 -0
  22. package/round4/b1-b1-target-validation.md +32 -0
  23. package/round4/b3a-prompt-audit-result.md +66 -0
  24. package/round4/b3a-prompt-audit.mjs +89 -0
  25. package/round4/optimization-plan.md +165 -0
  26. package/round4/r4-final-report.html +347 -0
  27. package/round4/r5-scenario-matrix.md +46 -0
  28. package/src/change-list.js +9 -0
  29. package/src/classify-change.js +6 -0
  30. package/src/command-cards.js +2 -1
  31. package/src/commit-guard.js +2 -0
  32. package/src/config-schema.js +64 -12
  33. package/src/db.js +35 -4
  34. package/src/decision-distill.js +9 -3
  35. package/src/docs-check.js +1 -1
  36. package/src/doctor-diagnostics.js +44 -2
  37. package/src/flow-draft.js +748 -0
  38. package/src/flow-parity.js +236 -0
  39. package/src/flow-review.js +184 -0
  40. package/src/flow.js +1492 -0
  41. package/src/foreign-declared.js +17 -1
  42. package/src/fr-index.js +396 -22
  43. package/src/fs-atomic.js +5 -1
  44. package/src/git-helper.js +2 -2
  45. package/src/greenfield-bootstrap.js +98 -0
  46. package/src/handoff.js +120 -7
  47. package/src/hunk-attribution.js +191 -0
  48. package/src/index.js +570 -16
  49. package/src/knowledge-classify.js +47 -1
  50. package/src/knowledge-digest.js +162 -0
  51. package/src/knowledge-quicklog.js +246 -0
  52. package/src/machine-draft.js +128 -0
  53. package/src/machine-interface.js +107 -5
  54. package/src/mcp-server.js +1 -0
  55. package/src/module-resolve.js +3 -1
  56. package/src/preview-progress.js +160 -0
  57. package/src/progress/change-registry.js +11 -3
  58. package/src/progress/shared.js +3 -2
  59. package/src/progress.js +56 -13
  60. package/src/quicklog.js +44 -6
  61. package/src/redomain.js +147 -0
  62. package/src/review-material-pack.js +204 -1
  63. package/src/run/archive-distill.js +4 -2
  64. package/src/run/bg-sync.js +67 -0
  65. package/src/run/command.js +123 -10
  66. package/src/run/complete-handlers.js +327 -72
  67. package/src/run/complete.js +350 -71
  68. package/src/run/gate-snapshot-ledger.js +213 -0
  69. package/src/run/gate-snapshot.js +250 -34
  70. package/src/run/gates.js +370 -233
  71. package/src/run/green-cache.js +123 -0
  72. package/src/run/junction-rm.js +70 -0
  73. package/src/run/next.js +14 -0
  74. package/src/run/prompt.js +185 -55
  75. package/src/run/quick-audit.js +185 -12
  76. package/src/run/shared.js +120 -3
  77. package/src/run/stage.js +182 -216
  78. package/src/run/test-ledger.js +264 -0
  79. package/src/run/verify-quality-scan.js +84 -7
  80. package/src/scope-audit.js +6 -1
  81. package/src/sentinel-assertions.js +114 -0
  82. package/src/setup.js +3 -3
  83. package/src/spec-sync.js +41 -1
  84. package/src/stage-contract.js +26 -8
  85. package/src/stage-review-checklist.js +2 -0
  86. package/src/stage-review.js +7 -2
  87. package/src/stage-templates.js +157 -0
  88. package/src/stages/archive.js +2 -0
  89. package/src/stages/brainstorm.js +24 -106
  90. package/src/stages/execute.js +221 -84
  91. package/src/stages/knowledge.js +74 -1
  92. package/src/stages/plan-postcheck.js +352 -1
  93. package/src/stages/plan.js +23 -1
  94. package/src/stages/quick.js +10 -2
  95. package/src/stages/verify.js +4 -2
  96. package/src/sync.js +81 -23
  97. package/src/task-done.js +181 -0
  98. package/src/task-review.js +8 -55
  99. package/src/test-bindings.js +549 -0
  100. package/src/timeline.js +218 -0
  101. package/src/ui-visual.js +175 -0
  102. package/src/verify-draft.js +214 -0
  103. package/src/verify-postcheck.js +860 -158
  104. package/src/verify-probes.js +59 -3
  105. package/src/watcher.js +1031 -0
  106. package/src/worktree-apply.js +44 -4
  107. package/src/worktree-deps.js +22 -8
  108. package/src/worktree-policy.js +42 -0
  109. package/src/worktree.js +102 -15
  110. package/templates/agents-instruction.md +37 -18
package/src/run/prompt.js CHANGED
@@ -18,14 +18,39 @@
18
18
  * - 删除 outputStep 内死代码 `const { execSync } = await import('child_process')`(execSync 解构未用,实际走 safeGit)
19
19
  * - loadModuleContextIndex/buildModuleContextInjection 内 require('fs'/'path') 改顶部静态 import
20
20
  */
21
- import { basename, join } from 'node:path'
22
- import { existsSync, readFileSync, mkdirSync, readdirSync } from 'node:fs'
21
+ import { basename, dirname, join } from 'node:path'
22
+ import { existsSync, readFileSync, mkdirSync, readdirSync, writeFileSync } from 'node:fs'
23
23
  import { createHash } from 'node:crypto'
24
24
  import jsYaml from 'js-yaml'
25
25
  import { writeAtomicSync } from '../fs-atomic.js'
26
26
  import { stageRegistry } from '../stages/index.js'
27
27
  import { resolvePromptIncludes, resolveRuntimeRoot, safeGit, parsePorcelainPath, WAIT_MARKER_RE, QUICK_SID_RE, triggerStepStartSync } from './shared.js'
28
+ import { materializeStageTemplates } from '../stage-templates.js'
28
29
  import { renderSemanticGuardBlock, readSemanticGuardEnabled } from '../semantic-guard.js'
30
+
31
+ // ── M1 步骤指引静态段指纹(2026-09-21-r5-efficiency-batch2 task-01 / D-001@v1)──
32
+ // 易变占位符全集(值因运行而异):指纹掩码 + 复入附录照常渲染。新增易变占位符时同步此表
33
+ // (漏登会令指纹误变——退化后果只是多一次全量重印,不产生错误指引;多登同理只多印)。
34
+ const VOLATILE_PLACEHOLDER_RES = [
35
+ /<now-datetime>/g, /<now-timestamp>/g, /<now-date>/g, /<now-iso-datetime>/g, /<git-head-short>/g, /<quicklog-id>/g,
36
+ /{SCAN_STALENESS}/g, /{SCAN_FACTS}/g, /{QUICK_CONTEXT_DIGEST}/g, /{DECISION_HITS}/g,
37
+ /{KNOWLEDGE_HIT_REPORT}/g, /{DOCS_DEBT}/g, /{MODULE_RESOLVE_TABLE}/g, /{REVIEW_MATERIALS}/g,
38
+ /{EXECUTE_RUN_ID}/g, /{STAGE_REVIEW_RUN_ID}/g, /{PROGRESS_SNAPSHOT}/g, /{TASK_COMPLETION_REPORT}/g,
39
+ /{WORKTREE_META}/g, /{WORKTREE_BASELINE_INFO}/g, /{TASKS_CHECKBOX}/g, /{GIT_DIRTY}/g,
40
+ /{SCOPE_AUDIT_TABLE}/g, /{FR_INDEX_DIGEST}/g, /{HANDOVER_SUMMARY}/g, /{PREFLIGHT_FAILURES}/g,
41
+ /{PRIOR_REVIEW_FACTS}/g, /{ARCHIVE_IMPACT_AUDIT}/g, /{EVIDENCE_AUTO_RECOMMENDATION}/g,
42
+ ]
43
+ const VOLATILE_MASK = '⟦dyn⟧'
44
+ function maskVolatileForGuide(text) {
45
+ let out = text
46
+ for (const re of VOLATILE_PLACEHOLDER_RES) out = out.replace(re, VOLATILE_MASK)
47
+ return out
48
+ }
49
+ /** M1:步骤指引静态段指纹——模板原文(include 解析后、占位符替换前)掩码易变占位符后的 sha256。
50
+ * 确定性:同模板同指纹;模板文本/静态占位符变更即变;易变占位符的值不参与(值在替换后,不在此面)。 */
51
+ export function computeStepGuideFingerprint(template) {
52
+ return createHash('sha256').update(maskVolatileForGuide(String(template))).digest('hex')
53
+ }
29
54
  import { renderStageContract } from '../stage-contract-spec.js'
30
55
  import { nowWallClock } from '../datetime.js'
31
56
  import { parseModuleMapSimple } from '../modules.js'
@@ -35,6 +60,7 @@ import { REVIEW_SCHEMA_VERSION, isValidExecuteRunId } from '../task-review.js'
35
60
  // 静态 import 不引入环(stages/* 反向引用本文件会撞 index.js 顶层 stageRegistry TDZ——见
36
61
  // buildKnowledgeInjection docstring)。
37
62
  import { matchKnowledge } from '../knowledge-match.js'
63
+ import { matchQuicklogContext } from '../knowledge-quicklog.js'
38
64
  import { appendKnowledgeHit } from '../knowledge-hits.js'
39
65
  import { readActiveFrDigest } from '../fr-index.js'
40
66
  // 前置失败清单 validator 面(2026-09-18-preflight-slimming task-01):design-facts /
@@ -163,9 +189,10 @@ export function loadModuleContextIndex(specBase, projectName) {
163
189
  * @param {object} moduleIndex - loadModuleContextIndex 返回值
164
190
  * @param {string} specBase - 规范目录
165
191
  * @param {string} projectName - 项目名
192
+ * @param {object} [opts] - { change }:变更名 / quick sessionId(docs-inject 遥测 change 字段;缺省 '')
166
193
  * @returns {{text: string, frModules: Array<{id: string, count: number}>}} 上下文注入文本(空=无匹配模块)+ 有活需求的模块清单(供遥测)
167
194
  */
168
- export function buildModuleContextInjection(taskDescription, moduleIndex, specBase, projectName) { if (!moduleIndex || !taskDescription) return { text: '', frModules: [] }
195
+ export function buildModuleContextInjection(taskDescription, moduleIndex, specBase, projectName, opts = {}) { if (!moduleIndex || !taskDescription) return { text: '', frModules: [] }
169
196
 
170
197
  const taskLower = taskDescription.toLowerCase()
171
198
  const matched = []
@@ -237,6 +264,21 @@ export function buildModuleContextInjection(taskDescription, moduleIndex, specBa
237
264
  injection += '\n'
238
265
  }
239
266
 
267
+ // docs-inject 遥测(2026-09-21-scan-docs-ops-panel task-04,FR-06 / D-003@v1 Wave 0):模块
268
+ // 上下文注入命中(≥1 模块、注入段已渲染)经 task-01 底座 appendKnowledgeHit 落一行
269
+ // type:docs-inject,供平台 scan-docs 注入频次聚合消费(knowledge-stats 未知 type 前向兼容
270
+ // 静默跳过,不污染知识 stats 口径)。matchedFiles 取命中模块 doc 字段原值(modules/<x>.md,
271
+ // _module-map.yaml 实际形态)。未命中零行;fail-soft:遥测写失败不影响注入本体(对齐上方
272
+ // buildKnowledgeInjection 遥测分离先例)。
273
+ try {
274
+ appendKnowledgeHit(join(specBase, '.runtime'), {
275
+ type: 'docs-inject',
276
+ change: String((opts && opts.change) || ''),
277
+ query: String(taskDescription || ''),
278
+ matchedFiles: matched.map(m => m.data && m.data.doc).filter(Boolean),
279
+ })
280
+ } catch { /* 遥测 fail-soft(R-04):hits 落盘失败不影响注入本体 */ }
281
+
240
282
  return { text: injection, frModules }
241
283
  }
242
284
 
@@ -409,19 +451,28 @@ function renderKnowledgeInjectSection(knowledgeResult, { knowledgeDir, maxFiles,
409
451
  */
410
452
  export function buildKnowledgeInjection({ knowledgeDir, runtimeDir, change, query, maxFiles = KNOWLEDGE_INJECT_MAX_FILES, maxLines = KNOWLEDGE_INJECT_MAX_LINES } = {}) {
411
453
  const knowledgeResult = matchKnowledge(knowledgeDir, String(query || ''))
412
- if (!knowledgeResult.matched) {
454
+ // quicklog 检索面(知识可见性导线):与 INDEX 知识面独立——INDEX 未命中时 quicklog 照常
455
+ // 注入(两 retrieval 面互为补充);fail-open(无 quicklog/异常 → 空 hits 零注入)。
456
+ // specBase 由 knowledgeDir 上溯(两个注入点传的都是 <specBase>/knowledge)。
457
+ const quick = knowledgeDir
458
+ ? matchQuicklogContext(dirname(knowledgeDir), String(query || ''))
459
+ : { hits: [], report: '' }
460
+ if (!knowledgeResult.matched && quick.hits.length === 0) {
413
461
  return { matched: false, section: '', report: knowledgeResult.report, json: knowledgeResult.json }
414
462
  }
415
- const section = renderKnowledgeInjectSection(knowledgeResult, { knowledgeDir, maxFiles, maxLines })
463
+ const kSection = knowledgeResult.matched ? renderKnowledgeInjectSection(knowledgeResult, { knowledgeDir, maxFiles, maxLines }) : ''
464
+ const qSection = quick.report || ''
465
+ const section = [kSection, qSection].filter(Boolean).join('\n\n')
416
466
  try {
417
467
  appendKnowledgeHit(runtimeDir, {
418
468
  type: 'inject',
419
469
  change: change || '',
420
470
  query: String(query || ''),
421
471
  matchedFiles: knowledgeResult.entries.map(e => (e.anchor ? `${e.file}#${e.anchor}` : e.file)),
472
+ quicklogIds: quick.hits.map(h => h.qlId),
422
473
  })
423
474
  } catch { /* 遥测 fail-soft(R-04):hits 落盘失败不阻断注入本体 */ }
424
- return { matched: true, section, report: knowledgeResult.report, json: knowledgeResult.json }
475
+ return { matched: true, section, report: knowledgeResult.report, json: knowledgeResult.json, quickHits: quick.hits }
425
476
  }
426
477
 
427
478
  // parseModuleMapSimple 复用 modules.js 的 canonical 实现(合并历史 copy-paste 副本,2026-08-07;
@@ -775,6 +826,13 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
775
826
  // POST 落地后进程才退出。
776
827
  triggerStepStartSync(cwd, changeName, platformOpts)
777
828
 
829
+ // ── 阶段产物模板落盘(R16 减负批次 2026-09-24):模板出上下文——prompt 只留指针,模板
830
+ // 幂等落盘 <specBase>/.runtime/templates/ 供 agent 写产物前 Read 一次。best-effort:
831
+ // 落盘失败零输出不阻断渲染(模板文件缺失时 --done 门禁会兜底点名)。──
832
+ try {
833
+ materializeStageTemplates(platformOpts?.specRoot || platformOpts?.specDriftAnchor || join(cwd, '.sillyspec'))
834
+ } catch { /* fail-soft */ }
835
+
778
836
  // ── 首个 agent 可见步索引(「首步一次建立」类注入的锚点)──
779
837
  // step0 为 noAI(brainstorm/execute/verify 的 step1「进度确认」,2026-09-07)时不渲染 prompt,
780
838
  // persona/护栏/完成契约/铁律/平台路径约束等 step0 专属注入须顺延到首个渲染步,否则整段丢失
@@ -852,17 +910,36 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
852
910
  // 先解析 {{include: name}}(把外部模板片段拉进 prompt),再做下方占位符替换,
853
911
  // 保证模板内容里的 {SPEC_ROOT}/<change-name> 等也能被替换
854
912
  let promptText = resolvePromptIncludes(step.prompt)
913
+ // ── M1 步骤指引静态段指纹增量(2026-09-21-r5-efficiency-batch2 task-01 / FR-01 / D-001@v1)──
914
+ // P8 实证(round5):73 次 CLI 调用注入 252KB、同步骤复入全量重印。契约:静态段按渲染指纹落盘
915
+ // .runtime/step-guides/,同指纹复入静态部分 ≤10 行(指纹+路径+提示),动态注入段(值因运行而异)
916
+ // 每次渲染永不缓存(附录照常输出)。--json(console.log 劫持形态,src/index.js withJsonOutput)与
917
+ // SILLYSPEC_STEP_GUIDE=0 走全量——机器消费方与逃生门不读盘。
918
+ const guideTemplate = promptText // 替换管线入口的模板原文(指纹基面)
919
+ const dynSegments = [] // 动态注入段值收集(复入附录素材,顺序即渲染顺序)
920
+ const substitute = (re, value, isStaticToken = false) => {
921
+ const v = String(value)
922
+ if (!isStaticToken) dynSegments.push(v) // 非白名单占位符默认按动态收集(错收只多不少,漏收才是坑)
923
+ promptText = promptText.replace(re, v)
924
+ return promptText
925
+ }
926
+ const substituteSplitJoin = (token, value) => {
927
+ const v = String(value)
928
+ dynSegments.push(v)
929
+ promptText = promptText.split(token).join(v)
930
+ return promptText
931
+ }
855
932
  // 替换 prompt 中的占位符
856
933
  if (projectName && promptText.includes('<project>')) {
857
- promptText = promptText.replace(/<project>/g, projectName)
934
+ substitute(/<project>/g, projectName, true)
858
935
  }
859
936
  // 替换 <git-user> 占位符
860
937
  if (promptText.includes('<git-user>')) {
861
938
  try {
862
939
  const gitUser = safeGit(cwd, ['config', 'user.name']).value || 'unknown'
863
- promptText = promptText.replace(/<git-user>/g, gitUser)
940
+ substitute(/<git-user>/g, gitUser, true)
864
941
  } catch {
865
- promptText = promptText.replace(/<git-user>/g, 'unknown')
942
+ substitute(/<git-user>/g, 'unknown', true)
866
943
  }
867
944
  }
868
945
  // 替换时间戳占位符(本地墙钟统一走 nowWallClock——坑 taskcard-created-at-utc:人读字段用
@@ -871,33 +948,33 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
871
948
  const nowDatetime = nowWallClock(now)
872
949
  const nowTimestamp = now.getFullYear() + String(now.getMonth()+1).padStart(2,'0') + String(now.getDate()).padStart(2,'0') + '-' + String(now.getHours()).padStart(2,'0') + String(now.getMinutes()).padStart(2,'0') + String(now.getSeconds()).padStart(2,'0')
873
950
  const nowDate = now.getFullYear() + '-' + String(now.getMonth()+1).padStart(2,'0') + '-' + String(now.getDate()).padStart(2,'0')
874
- promptText = promptText.replace(/<now-datetime>/g, nowDatetime)
875
- promptText = promptText.replace(/<now-timestamp>/g, nowTimestamp)
876
- promptText = promptText.replace(/<now-date>/g, nowDate)
951
+ substitute(/<now-datetime>/g, nowDatetime)
952
+ substitute(/<now-timestamp>/g, nowTimestamp)
953
+ substitute(/<now-date>/g, nowDate)
877
954
  // <now-iso-datetime>(scan frontmatter updated_at 用;与 scan-fix-headers 同款秒级格式)。
878
955
  // 占位符名带 iso 是历史遗留——值已统一为本地墙钟(同 <now-datetime>),改名会破坏 stages 引用。
879
- promptText = promptText.replace(/<now-iso-datetime>/g, nowWallClock(now))
956
+ substitute(/<now-iso-datetime>/g, nowWallClock(now))
880
957
  // <git-head-short>(scan frontmatter source_commit 用;CLI 代查,agent 勿自跑 rev-parse)
881
958
  if (promptText.includes('<git-head-short>')) {
882
959
  let headShort = 'unknown'
883
960
  try {
884
961
  headShort = safeGit(cwd, ['rev-parse', '--short', 'HEAD']).value || 'unknown'
885
962
  } catch { /* git 不可用 → unknown,scan-postcheck 失配时再核 */ }
886
- promptText = promptText.replace(/<git-head-short>/g, headShort)
963
+ substitute(/<git-head-short>/g, headShort)
887
964
  }
888
965
  // 替换 {REVIEW_SCHEMA_VERSION} 占位符(task review 示例模板用,值=CLI 当前 REVIEW_SCHEMA_VERSION 常量,
889
966
  // 避免 agent 照抄 design 目标版本与 CLI 写侧常量漂移;与 stage review 契约 renderReviewJsonContract 动态注入同源)
890
967
  if (promptText.includes('{REVIEW_SCHEMA_VERSION}')) {
891
- promptText = promptText.replace(/\{REVIEW_SCHEMA_VERSION\}/g, String(REVIEW_SCHEMA_VERSION))
968
+ substitute(/\{REVIEW_SCHEMA_VERSION\}/g, String(REVIEW_SCHEMA_VERSION))
892
969
  }
893
970
  // 替换 <change-name> 占位符
894
971
  if (changeName && promptText.includes('<change-name>')) {
895
- promptText = promptText.replace(/<change-name>/g, changeName)
972
+ substitute(/<change-name>/g, changeName, true)
896
973
  }
897
974
  // 替换 <quick-session-id> 占位符(quick 阶段专用:sessionId == changeName == quick-<uuid8>,
898
975
  // 见 runStage 参数解析 quickSessionId 生成。告知 agent 本会话 id + --done 需带 --change)
899
976
  if (changeName && promptText.includes('<quick-session-id>')) {
900
- promptText = promptText.replace(/<quick-session-id>/g, changeName)
977
+ substitute(/<quick-session-id>/g, changeName, true)
901
978
  }
902
979
  // 替换 <quicklog-id> 占位符(quick 阶段:从 session guard.json 读 CLI 分配的 ql-ID,
903
980
  // 供 agent 在模块文档变更索引等处引用)
@@ -912,7 +989,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
912
989
  } catch (err) {
913
990
  console.warn(`⚠️ quicklog-id 占位符读取 guard.json 失败: ${err && err.message ? err.message : err}`);
914
991
  }
915
- promptText = promptText.replace(/<quicklog-id>/g, qlIdVal || '(未分配)')
992
+ substitute(/<quicklog-id>/g, qlIdVal || '(未分配)')
916
993
  }
917
994
  // 替换 <linked-changes> 占位符(quick 阶段:从 .runtime/quick-sessions/<sessionId>/guard.json 读关联变更)
918
995
  if (promptText.includes('<linked-changes>')) {
@@ -928,7 +1005,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
928
1005
  if (guard) linkedChanges = Array.isArray(guard.linkedChanges) ? guard.linkedChanges : []
929
1006
  } catch {}
930
1007
  const display = linkedChanges.length > 0 ? linkedChanges.join(', ') : '(无)'
931
- promptText = promptText.replace(/<linked-changes>/g, display)
1008
+ substitute(/<linked-changes>/g, display, true)
932
1009
  }
933
1010
  // 平台模式:注入路径覆盖指令
934
1011
  if (platformOpts?.specRoot || platformOpts?.runtimeRoot) {
@@ -1058,9 +1135,9 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
1058
1135
  const injected = fact
1059
1136
  ? `[docs-debt] scan 基线漂移:${fact.message}`
1060
1137
  : '[docs-debt] scan 基线漂移:无 scan 文档(绿地项目),跳过判定'
1061
- promptText = promptText.replace(/\{SCAN_STALENESS\}/g, injected)
1138
+ substitute(/\{SCAN_STALENESS\}/g, injected)
1062
1139
  } catch (e) {
1063
- promptText = promptText.replace(/\{SCAN_STALENESS\}/g, `[docs-debt] scan 漂移检测异常(${e.message}),跳过判定`)
1140
+ substitute(/\{SCAN_STALENESS\}/g, `[docs-debt] scan 漂移检测异常(${e.message}),跳过判定`)
1064
1141
  }
1065
1142
  }
1066
1143
 
@@ -1090,7 +1167,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
1090
1167
  '\n\n⛔ 红线(同 scan 子代理先例):禁止重新 grep 发现底稿覆盖的机械事实(端点/依赖/规模),冲突以底稿为准。\n'
1091
1168
  }
1092
1169
  } catch { /* fail-soft:读取失败空注入,不阻断 prompt 输出 */ }
1093
- promptText = promptText.replace(/\{SCAN_FACTS\}/g, factsInjected)
1170
+ substitute(/\{SCAN_FACTS\}/g, factsInjected)
1094
1171
  }
1095
1172
 
1096
1173
  // ── 机械事实注入三件(2026-09-07 注入缺口批次):{LOCAL_COMMANDS} / {GIT_DIRTY} / {TASKS_CHECKBOX} ──
@@ -1125,7 +1202,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
1125
1202
  } catch (e) {
1126
1203
  localBlock = `(构建命令注入失败:${e.message}——可自行读 local.yaml)`
1127
1204
  }
1128
- promptText = promptText.replace(/\{LOCAL_COMMANDS\}/g, localBlock)
1205
+ substitute(/\{LOCAL_COMMANDS\}/g, localBlock)
1129
1206
  }
1130
1207
 
1131
1208
  // ①b {QUICK_CONTEXT_DIGEST}(刀①,2026-09-08):quick step1 的项目/约定上下文 CLI 代读注入
@@ -1133,7 +1210,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
1133
1210
  // fail-soft 同三件套;无 quick 占位符自然零输出。
1134
1211
  if (stageName === 'quick' && promptText.includes('{QUICK_CONTEXT_DIGEST}')) {
1135
1212
  const digestSpecBase = resolvePromptSpecBase(platformOpts, cwd)
1136
- promptText = promptText.replace(/\{QUICK_CONTEXT_DIGEST}/g, buildQuickContextDigest(digestSpecBase, projectName))
1213
+ substitute(/\{QUICK_CONTEXT_DIGEST}/g, buildQuickContextDigest(digestSpecBase, projectName))
1137
1214
  }
1138
1215
 
1139
1216
  // ①c {FR_INDEX_DIGEST}(2026-09-18-fr-index-l1 L1,D-004):brainstorm step8 的触达域现行 FR 注入
@@ -1149,12 +1226,12 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
1149
1226
  const domains = resolveTouchedDomains(frChangeDir, discoverModuleIndex(knowledgeRoot))
1150
1227
  const entries = readActiveFrDigest(knowledgeRoot, domains)
1151
1228
  if (entries.length === 0) {
1152
- promptText = promptText.replace(/\{FR_INDEX_DIGEST\}/g, '(触达域暂无 active FR 索引条目——本变更大概率是这些域的首批需求,照常写作)')
1229
+ substitute(/\{FR_INDEX_DIGEST\}/g, '(触达域暂无 active FR 索引条目——本变更大概率是这些域的首批需求,照常写作)')
1153
1230
  } else {
1154
1231
  const lines = entries.map((e) => `- ${e.id} ${e.title}(来源 ${e.change}${e.scenarios.length ? ';场景:' + e.scenarios.slice(0, 3).join(',') : ''}${(e.decisions || []).length ? ';依据:' + e.decisions.slice(0, 3).join('、') : ''})${e.needsReview ? ` ⚠️ 待复核(${e.needsReview})——该 FR 覆盖的代码近期被 quick 触达,行为可能已变;本变更若触及同域先核对现状再决定承接/新写` : ''}`)
1155
1232
  lines.push('')
1156
1233
  lines.push('> 域解析自本变更 design.md 文件清单(漏域先核对清单)。改写/取代已有行为 → 对应 FR 块加承接行;新行为 → 新 FR 块。superseded 条目默认不列(历史回溯自行读 knowledge/fr/)。依据决策(L2)= 当年取舍锚——翻案须先读 knowledge/decisions/<域>.md 的否决理由,满足复潮条件走 D-xxx@vN+1,不得静默改行为。承接行可带退役理由:`承接: FR-<域>-NNN(退役理由:一句话)`——归档时写进被取代条目(理由内禁逗号);条目正文是截断摘要,全文锚(全文:<归档路径>#FR-NN)由 CLI 自动落。')
1157
- promptText = promptText.replace(/\{FR_INDEX_DIGEST\}/g, lines.join('\n'))
1234
+ substitute(/\{FR_INDEX_DIGEST\}/g, lines.join('\n'))
1158
1235
  }
1159
1236
  try {
1160
1237
  const { appendKnowledgeHit } = await import('../knowledge-hits.js')
@@ -1163,7 +1240,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
1163
1240
  })
1164
1241
  } catch { /* 遥测 fail-soft */ }
1165
1242
  } catch (e) {
1166
- promptText = promptText.replace(/\{FR_INDEX_DIGEST\}/g, `(FR 索引注入失败:${e && e.message ? e.message : e}——可自行读 {SPEC_ROOT}/knowledge/fr/)`)
1243
+ substitute(/\{FR_INDEX_DIGEST\}/g, `(FR 索引注入失败:${e && e.message ? e.message : e}——可自行读 {SPEC_ROOT}/knowledge/fr/)`)
1167
1244
  }
1168
1245
  }
1169
1246
 
@@ -1180,7 +1257,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
1180
1257
  } catch (e) {
1181
1258
  dirtyBlock = `(脏文件注入失败:${e.message}——可自行跑 git status --porcelain)`
1182
1259
  }
1183
- promptText = promptText.replace(/\{GIT_DIRTY\}/g, dirtyBlock)
1260
+ substitute(/\{GIT_DIRTY\}/g, dirtyBlock)
1184
1261
  }
1185
1262
 
1186
1263
  // ③ {TASKS_CHECKBOX}:tasks.md 任务勾选状态投影(verify 逐项检查步消费——省 agent 手数)
@@ -1203,7 +1280,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
1203
1280
  } catch (e) {
1204
1281
  tasksBlock = `(勾选状态注入失败:${e.message}——可自行读 tasks.md)`
1205
1282
  }
1206
- promptText = promptText.replace(/\{TASKS_CHECKBOX\}/g, tasksBlock)
1283
+ substitute(/\{TASKS_CHECKBOX\}/g, tasksBlock)
1207
1284
  }
1208
1285
 
1209
1286
  // 决策防复潮注入(W1.1 第 3 点,task-04,FR-05):brainstorm「加载项目上下文」Step2 的
@@ -1232,13 +1309,13 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
1232
1309
  }
1233
1310
  decInjected = '\n\n' + lines.join('\n')
1234
1311
  }
1235
- promptText = promptText.replace(/\{DECISION_HITS\}/g, decInjected)
1312
+ substitute(/\{DECISION_HITS\}/g, decInjected)
1236
1313
  // 注入结果同步落 runtime JSON(与既有 KNOWLEDGE_HIT_REPORT 落盘口径一致)
1237
1314
  const decRuntimeDir = join(decSpecBase, '.runtime')
1238
1315
  mkdirSync(decRuntimeDir, { recursive: true })
1239
1316
  writeAtomicSync(join(decRuntimeDir, 'decision-hits.json'), JSON.stringify({ matched: decResult.matched, decisionHits: decResult.decisionHits || [] }, null, 2) + '\n')
1240
1317
  } catch (e) {
1241
- promptText = promptText.replace(/\{DECISION_HITS\}/g, `[decisions] 决策命中检测异常(${e.message}),跳过`)
1318
+ substitute(/\{DECISION_HITS\}/g, `[decisions] 决策命中检测异常(${e.message}),跳过`)
1242
1319
  }
1243
1320
  }
1244
1321
 
@@ -1274,7 +1351,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
1274
1351
  change: changeName || '',
1275
1352
  query: taskContext,
1276
1353
  })
1277
- promptText = promptText.replace(
1354
+ substitute(
1278
1355
  /\{KNOWLEDGE_HIT_REPORT\}/g,
1279
1356
  knowledgeInjection.matched ? knowledgeInjection.section : knowledgeInjection.report
1280
1357
  )
@@ -1282,7 +1359,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
1282
1359
  mkdirSync(runtimeDir, { recursive: true })
1283
1360
  writeAtomicSync(join(runtimeDir, 'knowledge-hit-report.json'), JSON.stringify(knowledgeInjection.json, null, 2) + '\n')
1284
1361
  } catch (e) {
1285
- promptText = promptText.replace(/\{KNOWLEDGE_HIT_REPORT\}/g, 'Status: no matches (error: ' + e.message + ')')
1362
+ substitute(/\{KNOWLEDGE_HIT_REPORT\}/g, 'Status: no matches (error: ' + e.message + ')')
1286
1363
  }
1287
1364
  }
1288
1365
 
@@ -1311,9 +1388,9 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
1311
1388
  try {
1312
1389
  debtTouchFacts = renderDecisionTouchFacts(computeDecisionTouches(changedFiles, join(debtSpecBase, 'knowledge')).touches)
1313
1390
  } catch { /* 触碰计算降级为空——不影响欠账 facts 注入 */ }
1314
- promptText = promptText.replace(/\{DOCS_DEBT\}/g, [result.facts, debtTouchFacts].filter(Boolean).join('\n'))
1391
+ substitute(/\{DOCS_DEBT\}/g, [result.facts, debtTouchFacts].filter(Boolean).join('\n'))
1315
1392
  } catch (e) {
1316
- promptText = promptText.replace(/\{DOCS_DEBT\}/g, `[docs-debt] 欠账计算异常(${e.message}),跳过`)
1393
+ substitute(/\{DOCS_DEBT\}/g, `[docs-debt] 欠账计算异常(${e.message}),跳过`)
1317
1394
  }
1318
1395
  }
1319
1396
 
@@ -1326,9 +1403,9 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
1326
1403
  const { renderModuleResolveTable } = await import('../module-resolve.js')
1327
1404
  const mrSpecBase = resolvePromptSpecBase(platformOpts, cwd)
1328
1405
  const table = renderModuleResolveTable({ cwd, specBase: mrSpecBase, changeName })
1329
- promptText = promptText.replace(/\{MODULE_RESOLVE_TABLE\}/g, table)
1406
+ substitute(/\{MODULE_RESOLVE_TABLE\}/g, table)
1330
1407
  } catch (e) {
1331
- promptText = promptText.replace(/\{MODULE_RESOLVE_TABLE\}/g, `[modules-resolve] 模块卡分级解析异常(${e.message}),跳过——可手跑 sillyspec modules resolve --change <变更名>`)
1408
+ substitute(/\{MODULE_RESOLVE_TABLE\}/g, `[modules-resolve] 模块卡分级解析异常(${e.message}),跳过——可手跑 sillyspec modules resolve --change <变更名>`)
1332
1409
  }
1333
1410
  }
1334
1411
 
@@ -1366,7 +1443,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
1366
1443
  console.error(`[sillyspec] execute run marker/目录写入失败(降级继续,ID ${runId} 仍注入 prompt): ${e.message}`)
1367
1444
  }
1368
1445
  }
1369
- promptText = promptText.replace(/\{EXECUTE_RUN_ID\}/g, runId)
1446
+ substitute(/\{EXECUTE_RUN_ID\}/g, runId)
1370
1447
  }
1371
1448
 
1372
1449
  // Stage Review Tier:brainstorm/plan/execute 阶段注入审查分级占位符
@@ -1743,7 +1820,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
1743
1820
  if (stageName === 'quick') {
1744
1821
  taskDesc = readQuickGuardField(changeName, effectiveSpecBase, 'taskDescription') || taskDesc
1745
1822
  }
1746
- const injRes = buildModuleContextInjection(taskDesc, moduleIndex, effectiveSpecBase, projectName) || { text: '', frModules: [] }
1823
+ const injRes = buildModuleContextInjection(taskDesc, moduleIndex, effectiveSpecBase, projectName, { change: changeName || '' }) || { text: '', frModules: [] }
1747
1824
  const injection = injRes.text
1748
1825
  if (injection) {
1749
1826
  promptText = injection + '\n' + promptText
@@ -1875,12 +1952,58 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
1875
1952
  })
1876
1953
  } catch { preflightFailuresMd = '' }
1877
1954
  if (promptText.includes('{PREFLIGHT_FAILURES}')) {
1878
- promptText = promptText.split('{PREFLIGHT_FAILURES}').join(preflightFailuresMd)
1955
+ promptText = substituteSplitJoin('{PREFLIGHT_FAILURES}', preflightFailuresMd)
1879
1956
  } else if (preflightFailuresMd) {
1880
1957
  promptText = `${promptText}\n\n${preflightFailuresMd}`
1881
1958
  }
1882
1959
 
1883
- console.log(promptText)
1960
+ // ── M1 打印分流:同指纹复入短输出 + 动态附录;首见全量 + guide 落盘 ──
1961
+ const jsonHijacked = String(console.log).includes('stderr.write') // --json 劫持形态探测(withJsonOutput 同款函数体)
1962
+ // P4 缺省开(D-004@batch3,2026-09-21-r5-efficiency-batch3 task-02):未设=开(短输出+落盘),
1963
+ // '=0' 显式关(逃生门保留,stdout 确定性测试族 fixture 用它锁定全量形态)。v1 默认关的动因
1964
+ // (测试族冲突)已由 task-02 迁移消解——逐例迁移注记见各测试文件。
1965
+ const guideEnabled = process.env.SILLYSPEC_STEP_GUIDE !== '0'
1966
+ const fingerprint = computeStepGuideFingerprint(guideTemplate)
1967
+ const runtimeRoot = platformOpts?.specRoot || join(cwd, '.sillyspec')
1968
+ const guideRoot = join(runtimeRoot, '.runtime', 'step-guides')
1969
+ const guideFile = join(guideRoot, `${stageName}-step${stepIndex}-${fingerprint.slice(0, 8)}.md`)
1970
+ // 复入状态=变更级(change+stage+step → 指纹):「复入」锚定同一变更同一步的重渲染(跨 CLI 进程
1971
+ // 持续生效——实验里每次 run 都是独立进程,进程内缓存无意义);他变更渲染同模板不误短路
1972
+ // (knowledge-inject 两路径字节一致测试的机制依赖此界——模板级寻址会短路其第二次全量渲染)。
1973
+ const stateRoot = join(runtimeRoot, '.runtime', 'step-guide-state')
1974
+ const stateFile = join(stateRoot, `${stageName}-step${stepIndex}-${String(changeName || '_nochange').replace(/[^A-Za-z0-9._-]/g, '_')}.json`)
1975
+ let prevState = null
1976
+ try { prevState = JSON.parse(readFileSync(stateFile, 'utf8')) } catch { /* 无状态=首见 */ }
1977
+ const reentryHit = guideEnabled && !jsonHijacked && prevState != null && prevState.fingerprint === fingerprint && existsSync(prevState.guidePath)
1978
+ if (reentryHit) {
1979
+ // 复入:静态部分 ≤10 行;动态段照常渲染为附录
1980
+ console.log(`📄 步骤指引未变(fingerprint=${fingerprint.slice(0, 8)})——静态全文不再重印。需要全文时 Read:`)
1981
+ console.log(` ${guideFile}`)
1982
+ console.log(` (首见渲染已在上下文中;上下文被压缩丢失时才需要 Read)`)
1983
+ console.log(`⛔ 本步骤安全铁律仍在生效(破坏性操作禁令见首见渲染/guide 全文)`)
1984
+ if (dynSegments.filter(x => x.trim() !== '').length > 0) {
1985
+ console.log(`## 动态注入段(本次运行——永不缓存,每次照常渲染)`)
1986
+ for (const seg of dynSegments) if (seg.trim() !== '') console.log(seg)
1987
+ }
1988
+ } else {
1989
+ console.log(promptText)
1990
+ try {
1991
+ // 特性开启且有 spec 根才落盘:默认关时不写(stdout 确定性);无 spec 根不凭空造目录
1992
+ //(spec-dir 向上命中类测试会被新建的 test/.sillyspec 污染——本批实证)。
1993
+ if (guideEnabled && (platformOpts?.specRoot || existsSync(join(cwd, '.sillyspec')))) {
1994
+ mkdirSync(guideRoot, { recursive: true })
1995
+ writeFileSync(guideFile, `<!--fp=${fingerprint}-->
1996
+ # ${stageName} step ${stepIndex + 1} 指引静态全文(${step.name})
1997
+
1998
+ > 动态占位符(时间/材料包/知识命中/欠账等)以当次渲染为准,本文件只存静态模板面。
1999
+
2000
+ ${maskVolatileForGuide(guideTemplate)}
2001
+ `, 'utf8')
2002
+ mkdirSync(stateRoot, { recursive: true })
2003
+ writeFileSync(stateFile, JSON.stringify({ fingerprint, guidePath: guideFile, changeName: changeName || null, stage: stageName, stepIndex, at: new Date().toISOString() }, null, 1) + '\n', 'utf8')
2004
+ }
2005
+ } catch { /* guide/state 落盘 best-effort:失败仅退化回全量重印,不阻断步骤输出 */ }
2006
+ }
1884
2007
  // 铁律拆分(W3 token 效率):通用流程纪律(文档优先/不跳步/不编造命令)只在首个 agent 可见步注入——
1885
2008
  // 每步重复 ~800B 纯耗 context。但【平台写入规则 + 路径规则】是安全关键(防写错目录/绕过 Write),
1886
2009
  // 且依赖 changeName/platformOpts,必须【每步注入】(context 压缩丢失会让 agent 越界写源码)。
@@ -1894,6 +2017,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
1894
2017
  console.log('- 变更目录改名用 `sillyspec change-rename <旧名> <新名>`:mv/rename 会漏改进度库引用,导致变更失联。')
1895
2018
  console.log('- 变更产物文档一律用 CLI 骨架命令生成(fourpiece-init / design-init / taskcard / module-impact / verify-probes --init)——骨架已预填 author/created_at/generated_by 元数据,勿删勿手拼 frontmatter;仅从零手写的补充文档才需手填 author(git 用户名)与 created_at(精确到秒)')
1896
2019
  console.log('- 执行构建/测试前必须先读 local.yaml,优先使用其中配置的命令、路径和环境变量;未配置时才使用默认值')
2020
+ console.log('- **输出纪律(上下文卫生;R12 对撞实测均轮 134K vs 其他轮 181-234K)**:预计输出超过 ~50 行的命令(pytest / vitest / npm test / uv sync / 全量 JSON 等)一律重定向落盘 `.sillyspec/.runtime/logs/<名>.log 2>&1`,只回看 `tail -n 50` 判断结果;需要细节时 grep 该日志。禁止把命令全量输出读进上下文——长输出逐轮回放是 token 膨胀主源(CLI noAI 步的输出截断同理,勿整段复述)。')
1897
2021
  }
1898
2022
  // 平台模式 + 路径规则(安全关键,每步注入;step1+ 起带精简标题,不复述通用铁律)
1899
2023
  if (platformOpts?.specRoot || platformOpts?.runtimeRoot || changeName) {
@@ -1948,21 +2072,27 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
1948
2072
  // 提示格式,且静态步骤名稳定无漂移风险)。
1949
2073
  const stepAssertFlag = (stageName === 'execute' && step?.name) ? ` --step "${step.name}"` : ''
1950
2074
  const doneCommand = `sillyspec run ${cmdStage} --done${confirmFlag}${stepAssertFlag}${changeFlag} --output "你的摘要"`
2075
+ // 合并式完成命令(R16 减负批次 2026-09-24):--done --answer 一步吞 wait+done——
2076
+ // 未挂起时自动补全 waitAnswer(complete.js requiresWait 门),已挂起时 resolveWaitingStepWithAnswer
2077
+ // 拉回收口。R15-SF 实证 brainstorm 8 次 CLI 调用中 4 次是三段式(wait/continue/done)补轮——
2078
+ // 合并式主路径把每个 wait 步省 2 次调用;--wait 降级为「用户暂不可达需挂起」的备选。
2079
+ const answerDoneCommand = `sillyspec run ${cmdStage} --done --answer "用户回答"${confirmFlag}${stepAssertFlag}${changeFlag} --output "你的摘要"`
1951
2080
  if (requiresWait) {
1952
- console.log(`本步骤必须等待用户输入,不能直接 --done:`)
1953
- console.log(`sillyspec run ${cmdStage} --wait --reason "${step.waitReason || '等待用户输入'}" --options "${(step.waitOptions || ['确认']).join(',')}"${changeFlag} --output "你的问题/方案摘要"`)
2081
+ console.log(`本步骤需要用户决策——主路径一步合并完成(--answer 自动补全 wait 状态,省 2 次 CLI 往返):`)
2082
+ console.log(answerDoneCommand + (autoMeta ? '' : ' --input "用户原始需求/反馈"'))
1954
2083
  console.log(``)
1955
- console.log(`用户回答后执行:`)
1956
- console.log(`sillyspec run ${cmdStage} --continue --answer "用户回答"${changeFlag}`)
1957
- console.log(``)
1958
- console.log(`收到回答并完成本步骤总结后,再执行:`)
1959
- } else if (mayNeedWait) {
1960
- console.log(`如果需要用户决策(选择方案/确认设计等):`)
1961
- console.log(`sillyspec run ${cmdStage} --wait --reason "${step.waitReason || '等待原因'}" --options "${(step.waitOptions || ['选项1', '选项2']).join(',')}"${changeFlag} --output "你的摘要"`)
1962
- console.log(``)
1963
- console.log(`如果不需要用户决策,正常完成:`)
2084
+ console.log(`若用户暂不可达需先挂起:`)
2085
+ console.log(`sillyspec run ${cmdStage} --wait --reason "${step.waitReason || '等待用户输入'}" --options "${(step.waitOptions || ['确认']).join(',')}"${changeFlag} --output "你的问题/方案摘要"`)
2086
+ console.log(`(挂起后拿到回答,仍用上方一步合并命令收口——无需 --continue 单独解等待)`)
2087
+ } else {
2088
+ if (mayNeedWait) {
2089
+ console.log(`如果需要用户决策(选择方案/确认设计等),拿到回答后一步完成(--answer 自动补全 wait 状态):`)
2090
+ console.log(answerDoneCommand + (autoMeta ? '' : ' --input "用户原始需求/反馈"'))
2091
+ console.log(``)
2092
+ console.log(`如果不需要用户决策,正常完成:`)
2093
+ }
2094
+ console.log(doneCommand + (autoMeta ? '' : ' --input "用户原始需求/反馈"'))
1964
2095
  }
1965
- console.log(doneCommand + (autoMeta ? '' : ' --input "用户原始需求/反馈"'))
1966
2096
  // P0-2(noai-ir-roadmap §3):--output 可省略提示(auto 模式除外——auto driver 的
1967
2097
  // SS-META doneCommand 仍带 --output,照抄路径零变化;省略路径由 command.js 同源合成)。
1968
2098
  if (!autoMeta) {
@@ -1982,9 +2112,9 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
1982
2112
  stepName: step.name,
1983
2113
  change: changeName || null,
1984
2114
  requiresUser: requiresWait || mayNeedWait || step.requiresConfirm === true,
1985
- doneCommand,
2115
+ doneCommand: requiresWait ? answerDoneCommand : doneCommand,
1986
2116
  waitHint: (requiresWait || mayNeedWait)
1987
- ? 'requiresUser=true:先与用户交互(wait/continue 或 --wait-interactive 直通),再 done'
2117
+ ? 'requiresUser=true:与用户交互拿到回答后,逐字跑 doneCommand(--done --answer 一步合并,自动补全 wait 状态);用户暂不可达才先 --wait 挂起'
1988
2118
  : '直接执行任务后逐字跑 doneCommand',
1989
2119
  }
1990
2120
  console.log('<!--SS-META:' + JSON.stringify(meta) + '-->')