sillyspec 3.29.4 → 3.31.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 (148) hide show
  1. package/.claude/skills/sillyspec-auto/SKILL.md +1 -1
  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 +7 -105
  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-archive.md +26 -0
  10. package/assets/command-cards/run-brainstorm.md +33 -0
  11. package/assets/command-cards/run-execute.md +29 -0
  12. package/assets/command-cards/run-plan.md +27 -0
  13. package/assets/command-cards/run-quick.md +17 -0
  14. package/assets/command-cards/run-verify.md +29 -0
  15. package/assets/command-cards/status.md +27 -0
  16. package/package.json +2 -1
  17. package/packages/dashboard/server/executor.js +2 -1
  18. package/packages/dashboard/server/parser.js +7 -7
  19. package/r7-test-env/README-R7-TEST.md +93 -0
  20. package/r7-test-env/demo-project/lib/calc.js +6 -0
  21. package/r7-test-env/demo-project/package.json +1 -0
  22. package/r7-test-env/demo-project/test.js +10 -0
  23. package/r7-test-env/prompt-R7-SMOKE.md +41 -0
  24. package/r7-test-env/sillyspec-r7.cmd +3 -0
  25. package/round4/account.mjs +98 -0
  26. package/round4/b1-analyze-task08.mjs +40 -0
  27. package/round4/b1-b1-target-validation.md +32 -0
  28. package/round4/b3a-prompt-audit-result.md +66 -0
  29. package/round4/b3a-prompt-audit.mjs +89 -0
  30. package/round4/brief-autocompact.md +34 -0
  31. package/round4/brief-session-replay.md +50 -0
  32. package/round4/deep-read-synthesis.md +38 -0
  33. package/round4/extract-full.mjs +38 -0
  34. package/round4/extract-timeline.mjs +43 -0
  35. package/round4/find-sessions.mjs +13 -0
  36. package/round4/full-r4-l.md +4707 -0
  37. package/round4/full-r4-o-l.md +4300 -0
  38. package/round4/full-r4-os.md +1681 -0
  39. package/round4/full-r4-sf.md +4255 -0
  40. package/round4/full-r4-sq.md +1734 -0
  41. package/round4/gate-audit-relay-receipt.md +30 -0
  42. package/round4/gate-value-audit.md +52 -0
  43. package/round4/optimization-plan.md +165 -0
  44. package/round4/prereg.md +67 -0
  45. package/round4/prompt-R4-L.md +63 -0
  46. package/round4/prompt-R4-O-L.md +59 -0
  47. package/round4/prompt-R4-O-S.md +56 -0
  48. package/round4/prompt-R4-S-F.md +60 -0
  49. package/round4/prompt-R4-S-Q.md +66 -0
  50. package/round4/r4-final-report.html +347 -0
  51. package/round4/r4-os-timeline.txt +297 -0
  52. package/round4/r4-sf-timeline.txt +738 -0
  53. package/round4/r4-sq-timeline.txt +322 -0
  54. package/round4/r4l-timeline.txt +793 -0
  55. package/round4/r4ol-timeline.txt +729 -0
  56. package/round4/r4sf-timeline.txt +852 -0
  57. package/round4/r5-scenario-matrix.md +46 -0
  58. package/round4/result-S.md +171 -0
  59. package/round4/result-final.md +46 -0
  60. package/round4/result-quality.md +63 -0
  61. package/src/change-list.js +9 -0
  62. package/src/classify-change.js +6 -0
  63. package/src/command-cards.js +74 -0
  64. package/src/commit-guard.js +2 -0
  65. package/src/config-schema.js +64 -12
  66. package/src/contract-matrix.js +26 -5
  67. package/src/db.js +35 -4
  68. package/src/decision-distill.js +9 -3
  69. package/src/diagnostic-codes.js +2 -1
  70. package/src/docs-check.js +1 -1
  71. package/src/doctor-diagnostics.js +44 -2
  72. package/src/flow-draft.js +748 -0
  73. package/src/flow-parity.js +236 -0
  74. package/src/flow-review.js +184 -0
  75. package/src/flow.js +1492 -0
  76. package/src/foreign-declared.js +17 -1
  77. package/src/fr-index.js +396 -22
  78. package/src/fs-atomic.js +5 -1
  79. package/src/git-helper.js +2 -2
  80. package/src/greenfield-bootstrap.js +98 -0
  81. package/src/handoff.js +120 -7
  82. package/src/hunk-attribution.js +191 -0
  83. package/src/index.js +602 -21
  84. package/src/init.js +25 -8
  85. package/src/knowledge-classify.js +47 -1
  86. package/src/knowledge-digest.js +162 -0
  87. package/src/knowledge-quicklog.js +246 -0
  88. package/src/machine-draft.js +128 -0
  89. package/src/machine-interface.js +194 -7
  90. package/src/mcp-server.js +1 -0
  91. package/src/module-impact.js +51 -22
  92. package/src/module-resolve.js +3 -1
  93. package/src/preview-progress.js +160 -0
  94. package/src/progress/change-registry.js +11 -3
  95. package/src/progress/shared.js +3 -2
  96. package/src/progress.js +56 -13
  97. package/src/quicklog.js +44 -6
  98. package/src/redomain.js +147 -0
  99. package/src/review-material-pack.js +204 -1
  100. package/src/run/archive-distill.js +4 -2
  101. package/src/run/bg-sync.js +67 -0
  102. package/src/run/command.js +127 -10
  103. package/src/run/complete-handlers.js +363 -73
  104. package/src/run/complete.js +356 -74
  105. package/src/run/gate-snapshot-ledger.js +213 -0
  106. package/src/run/gate-snapshot.js +260 -38
  107. package/src/run/gates.js +482 -268
  108. package/src/run/green-cache.js +123 -0
  109. package/src/run/junction-rm.js +70 -0
  110. package/src/run/next.js +14 -0
  111. package/src/run/prompt.js +185 -55
  112. package/src/run/quick-audit.js +213 -14
  113. package/src/run/shared.js +123 -4
  114. package/src/run/stage.js +182 -216
  115. package/src/run/test-ledger.js +264 -0
  116. package/src/run/verify-quality-scan.js +84 -7
  117. package/src/scope-audit.js +6 -1
  118. package/src/sentinel-assertions.js +114 -0
  119. package/src/setup.js +3 -3
  120. package/src/spec-sync.js +41 -1
  121. package/src/stage-contract.js +26 -8
  122. package/src/stage-review-checklist.js +2 -0
  123. package/src/stage-review.js +7 -2
  124. package/src/stage-templates.js +157 -0
  125. package/src/stages/archive.js +2 -0
  126. package/src/stages/brainstorm.js +24 -106
  127. package/src/stages/execute.js +222 -85
  128. package/src/stages/knowledge.js +74 -1
  129. package/src/stages/plan-postcheck.js +385 -20
  130. package/src/stages/plan.js +23 -1
  131. package/src/stages/quick.js +10 -2
  132. package/src/stages/verify.js +4 -2
  133. package/src/sync.js +81 -23
  134. package/src/task-done.js +181 -0
  135. package/src/task-review.js +8 -55
  136. package/src/taskcard-frontmatter.js +57 -0
  137. package/src/test-bindings.js +549 -0
  138. package/src/timeline.js +218 -0
  139. package/src/ui-visual.js +175 -0
  140. package/src/verify-draft.js +214 -0
  141. package/src/verify-postcheck.js +919 -159
  142. package/src/verify-probes.js +103 -25
  143. package/src/watcher.js +1031 -0
  144. package/src/worktree-apply.js +44 -4
  145. package/src/worktree-deps.js +22 -8
  146. package/src/worktree-policy.js +42 -0
  147. package/src/worktree.js +102 -15
  148. package/templates/agents-instruction.md +37 -18
@@ -0,0 +1,264 @@
1
+ /**
2
+ * test-ledger.js — 测试结果三键指纹账本(P2 / FR-02 / D-001@v1,2026-09-21-r5-efficiency-batch3)
3
+ *
4
+ * 背景(batch2 归档实证):全量测试 ~100s/次,一个收尾事务跑 7 次(必要 2-3 次);agent 因怕被
5
+ * --done 拦而恐惧性预跑。本模块按「三键全等才复用」提供 fail-closed 结果账本,消费点=gate
6
+ * verify 的 verify-test / quick --done 实测门 / gate --full(P1)。
7
+ *
8
+ * **fail-closed 三层(红线,D-001@v1)**:
9
+ * ① 键分量不可得(git 失败/测试面读不到/配置身份不可解析)→ 不记不复用,永远真跑;
10
+ * ② 失败结果永不缓存——只有通过的结果才落账本(失败走真跑修复路径,缓存失败=假绿源);
11
+ * ③ 严格全等——key 为三键分量的 sha256 串比对,无模糊匹配、无时间窗放宽。
12
+ *
13
+ * **三键定义**:
14
+ * - codeFingerprint:{ head, dirtyDigest }——HEAD commit + porcelain 未提交摘要(路径+内容
15
+ * hash 归一)。与 verify-quality-scan 的代码指纹同思想(P0-1 先例),本模块独立实现不耦合
16
+ * (quality-scan 是 noAI 步→verify 门单向,本账本多消费点)。
17
+ * - testSet(**v2**,P0 修正——fr-test-binding 方案 §3.5 立项前置):{ runPlan, configDigest,
18
+ * testFaceDigest }——runPlan=[{ runner, files }](runner=实解命令,resolveTestSetIdentity
19
+ * 经 extractTestCommand 单一解析源解析,调用方不再传字面量;files=段级选中集合,residual
20
+ * adapter 落地后入键,现一律 null=整目录摘要兜底);configDigest=local.yaml 原文摘要
21
+ * (local.yaml 是 gitignore 文件——不进 HEAD 也不进 porcelain,v1 键只含调用方硬编码
22
+ * 'npm test' 字面量,改 commands.test / test_strategy / modules / known_failures 均**不换键**
23
+ * =两个不同实际执行映射同一键、跨命令误复用旧绿;v2 起配置任何字节变化都换键——宁可
24
+ * 多跑一次,不复用错绿);testFaceDigest=测试面(整目录或选中集合)内容摘要。测试文件
25
+ * 本身也是代码,改测试=改被测面,必须换键。
26
+ * - envProfile:结构化探针(布尔/枚举,**禁裸路径入键**)。探针清单不是拍脑袋列举——
27
+ * cwdInsideWorktree 用 detectCwdInsideWorktree 同源判定(batch2 实证:13 个 worktree-cwd
28
+ * 环境族测试在主 worktree 全挂、在隔离快照全过,其判定信号就是「cwd 路径含
29
+ * .sillyspec/.runtime/worktrees/<change> 段」——探针必须捕获该信号,worktree 与主仓天然
30
+ * 分键);另收 node 版本/平台名/行为开关 env 集(SILLYSPEC_STEP_GUIDE 等,P4 翻默认后
31
+ * 直接影响输出形态族)/本地 spec 与平台指针存在性(resolveRuntimeRoot 分流信号)。
32
+ *
33
+ * 账本形态:.runtime/test-ledger-<changeName>.json 单条覆盖写(最近一次通过结果;
34
+ * schemaVersion: 2——v1 旧账 consult 即失效【自然失效,不迁移】,首次通过后按 v2 重建)——
35
+ * quick 会话名=quick-<hex> 天然 per-change 隔离,无并发互写面;写入走 writeAtomicSync
36
+ * (meta.json 并发读先例同款约束)。
37
+ */
38
+ import { createHash } from 'crypto'
39
+ import { existsSync, readFileSync, readdirSync, statSync } from 'fs'
40
+ import { join, relative, isAbsolute } from 'path'
41
+ import { gitQuiet } from '../git-helper.js'
42
+ import { detectCwdInsideWorktree } from './shared.js'
43
+ import { writeAtomicSync } from '../fs-atomic.js'
44
+ import { extractTestCommand } from '../verify-postcheck.js'
45
+
46
+ const sha256 = (s) => createHash('sha256').update(s).digest('hex')
47
+
48
+ /** 行为开关 env 集(影响测试结果的 SILLYSPEC_* 键——P4 翻默认后 STEP_GUIDE 直接改 stdout 形态) */
49
+ const BEHAVIOR_ENV_KEYS = [
50
+ 'SILLYSPEC_STEP_GUIDE',
51
+ 'SILLYSPEC_VERIFY_GATE_SNAPSHOT_OFF',
52
+ 'SILLYSPEC_QUICK_TEST_GATE',
53
+ 'SILLYSPEC_VERIFY_QUALITY_SCAN_RERUN',
54
+ ]
55
+
56
+ /**
57
+ * 环境探针(键分量③)。任何探针异常 → 整体 null(fail-closed 层①)。
58
+ * @returns {object|null} 结构化探针(JSON 稳定序参与 key)
59
+ */
60
+ export function computeEnvProfile({ cwd = process.cwd(), env = process.env } = {}) {
61
+ try {
62
+ const profile = {
63
+ cwdInsideWorktree: detectCwdInsideWorktree(cwd) !== null, // 13 环境族测试的真实判定信号(同源)
64
+ hasLocalSpec: existsSync(join(cwd, '.sillyspec')),
65
+ hasPlatformPointer: existsSync(join(cwd, '.sillyspec-platform.json')),
66
+ nodeVersion: process.version,
67
+ platform: process.platform,
68
+ behaviorEnv: BEHAVIOR_ENV_KEYS.reduce((acc, k) => {
69
+ acc[k] = env[k] === undefined ? null : String(env[k])
70
+ return acc
71
+ }, {}),
72
+ }
73
+ return profile
74
+ } catch { return null }
75
+ }
76
+
77
+ /**
78
+ * 代码指纹(键分量①):{ head, dirtyDigest }。git 不可达/HEAD 解析失败 → null(fail-closed)。
79
+ * dirtyDigest = porcelain(含 untracked)行集的排序摘要——任何未提交变化换键。
80
+ * ignoreFiles:账本自身路径(runtimeRoot 在仓内时)——**排除自污染**:记账写文件会改
81
+ * porcelain,若不排除,落账瞬间键即漂移、下次查询永 mismatch(自毁键,测试 L1 实证)。
82
+ * @returns {{ head: string, dirtyDigest: string }|null}
83
+ */
84
+ export function computeCodeFingerprint(projectRoot, { ignoreFiles = [] } = {}) {
85
+ try {
86
+ const head = gitQuiet(projectRoot, ['rev-parse', 'HEAD'])
87
+ if (!head) return null
88
+ const status = gitQuiet(projectRoot, ['status', '--porcelain'])
89
+ if (status === null) return null
90
+ const ignore = new Set(ignoreFiles)
91
+ const lines = status.split('\n').filter(Boolean)
92
+ .filter(l => {
93
+ const p = l.slice(3).split(' -> ').pop().replace(/^"|"$/g, '')
94
+ // 两种命中:精确路径(展开的 untracked/已跟踪文件)与目录前缀(git 折叠整棵未跟踪
95
+ // 目录成 `?? dir/` 一行——排除须向下匹配;账本自污染实测形态,测试 L1)
96
+ if (ignore.has(p)) return false
97
+ for (const f of ignore) { if (p.endsWith('/') && f.startsWith(p)) return false }
98
+ return true
99
+ })
100
+ .sort()
101
+ const dirtyDigest = sha256(lines.join('\n') || '(clean)')
102
+ return { head: head.trim(), dirtyDigest }
103
+ } catch { return null }
104
+ }
105
+
106
+ /** runtimeRoot 相对 projectRoot 的账本路径(仓外 runtimeRoot → null,无需排除) */
107
+ function ledgerIgnorePath(runtimeRoot, changeName, projectRoot) {
108
+ try {
109
+ const abs = testLedgerPath(runtimeRoot, changeName)
110
+ const rel = relative(projectRoot, abs)
111
+ return rel && !rel.startsWith('..') && !isAbsolute(rel) ? rel.split('\\').join('/') : null
112
+ } catch { return null }
113
+ }
114
+
115
+ /**
116
+ * 测试面摘要(键分量②的输入):test/ 目录(含一级子目录)文件清单+每文件内容摘要。
117
+ * 深度帽 2 层、文件数帽 2000(run-tests.mjs 同量级),超帽/读失败 → null(fail-closed——
118
+ * 测试面不可全知时复用无意义)。
119
+ * @returns {string|null}
120
+ */
121
+ export function computeTestFaceDigest(testRoot, depth = 0) {
122
+ try {
123
+ if (!existsSync(testRoot) || depth > 2) return null
124
+ const names = readdirSync(testRoot).filter(n => !n.startsWith('.')).sort()
125
+ const parts = []
126
+ let count = 0
127
+ for (const n of names) {
128
+ const p = join(testRoot, n)
129
+ const st = statSync(p)
130
+ if (st.isDirectory()) {
131
+ const sub = computeTestFaceDigest(p, depth + 1)
132
+ if (sub === null) return null
133
+ parts.push(n + '/:' + sub)
134
+ } else if (/\.(mjs|js|cjs|json)$/.test(n)) {
135
+ if (++count > 2000) return null
136
+ parts.push(n + ':' + sha256(readFileSync(p)))
137
+ }
138
+ }
139
+ return sha256(parts.join('|'))
140
+ } catch { return null }
141
+ }
142
+
143
+ /**
144
+ * 键②身份解析(v2,P0 修正——fr-test-binding 方案 §3.5 立项前置):runPlan.runner =
145
+ * 实解命令(extractTestCommand 单一解析源,未配置/unavailable 回退 'npm test'——与
146
+ * runVerifyTestCheck 主路径同口径);configDigest = local.yaml 原文摘要(换行归一,EOL-only
147
+ * 重写不换键)。specBase 未提供/读取异常 → null(fail-closed 层①:身份不可知则键不可得,
148
+ * 调用方不得回退传字面量命令)。
149
+ * @returns {{ runPlan: Array<{runner: string, files: string[]|null}>, configDigest: string }|null}
150
+ */
151
+ export function resolveTestSetIdentity({ specBase } = {}) {
152
+ try {
153
+ if (!specBase) return null
154
+ const yamlPath = join(specBase, 'local.yaml')
155
+ const text = existsSync(yamlPath) ? readFileSync(yamlPath, 'utf8') : null
156
+ const runner = extractTestCommand(text) || 'npm test'
157
+ return {
158
+ // files 段级选中集合:residual adapter 落地后入键;现一律 null = 整目录摘要兜底(保守向)
159
+ runPlan: [{ runner, files: null }],
160
+ configDigest: sha256((text ?? '(no-local.yaml)').replace(/\r\n?/g, '\n')),
161
+ }
162
+ } catch { return null }
163
+ }
164
+
165
+ /**
166
+ * 选中集合摘要(v2 结构位,P0 第三行):files 非空 → 仅摘要选中文件内容(未选中测试文件
167
+ * 改动不再整目录连坐换键);任一文件缺失/读失败 → null(fail-closed——选中面不可全知时
168
+ * 复用无意义)。files 空/非数组 → null(调用方走整目录 computeTestFaceDigest 兜底)。
169
+ * @param {string} testRoot
170
+ * @param {string[]|null} files 相对 testRoot 的路径
171
+ * @returns {string|null}
172
+ */
173
+ export function computeSelectedTestFaceDigest(testRoot, files) {
174
+ try {
175
+ if (!Array.isArray(files) || files.length === 0) return null
176
+ const parts = []
177
+ for (const f of [...files].sort()) {
178
+ parts.push(f + ':' + sha256(readFileSync(join(testRoot, f))))
179
+ }
180
+ return sha256(parts.join('|'))
181
+ } catch { return null }
182
+ }
183
+
184
+ /**
185
+ * 三键合成(v2):全部分量就绪才出键;任一 null → null。
186
+ * testSet = { runPlan, configDigest, testFaceDigest }(resolveTestSetIdentity + 面摘要合成)。
187
+ * @returns {string|null} sha256 key
188
+ */
189
+ export function computeTestLedgerKey({ projectRoot, testSet, cwd, env, ignoreFiles = [] }) {
190
+ const code = computeCodeFingerprint(projectRoot, { ignoreFiles })
191
+ const profile = computeEnvProfile({ cwd, env })
192
+ if (!code || !testSet || !testSet.testFaceDigest || !profile) return null
193
+ return sha256(JSON.stringify({ code, testSet, env: profile }))
194
+ }
195
+
196
+ /** consult/record 共用键计算(同源防两端口径漂移) */
197
+ function ledgerKeyFor({ runtimeRoot, changeName, projectRoot, testRoot, specBase, cwd, env }) {
198
+ const selfIgnore = ledgerIgnorePath(runtimeRoot, changeName, projectRoot)
199
+ const identity = resolveTestSetIdentity({ specBase })
200
+ if (!identity) return { identity: null, key: null }
201
+ const face = computeSelectedTestFaceDigest(testRoot, identity.runPlan[0]?.files) || computeTestFaceDigest(testRoot)
202
+ const key = computeTestLedgerKey({
203
+ projectRoot,
204
+ testSet: { runPlan: identity.runPlan, configDigest: identity.configDigest, testFaceDigest: face },
205
+ cwd, env, ignoreFiles: selfIgnore ? [selfIgnore] : [],
206
+ })
207
+ return { identity, key }
208
+ }
209
+
210
+ /** 账本路径(per-change 单文件) */
211
+ export function testLedgerPath(runtimeRoot, changeName) {
212
+ return join(runtimeRoot, `test-ledger-${changeName}.json`)
213
+ }
214
+
215
+ /**
216
+ * 查询:三键全等且有通过记录 → 复用。任一分量不可得/键不等/账本缺失损坏/版本不符 → 不复用
217
+ * (fail-closed)。runPlan 随命中回放(供消费方如实展示实际命令)。
218
+ * @returns {{ reuse: boolean, result?: object, runPlan?: Array<object>|null, reason: string, key?: string }}
219
+ */
220
+ export function consultTestLedger({ runtimeRoot, changeName, projectRoot, testRoot, specBase, cwd, env }) {
221
+ const { key } = ledgerKeyFor({ runtimeRoot, changeName, projectRoot, testRoot, specBase, cwd, env })
222
+ if (!key) return { reuse: false, reason: 'fail-closed: 键分量不可得(git/测试面/环境探针/配置身份任一失败)' }
223
+ try {
224
+ const p = testLedgerPath(runtimeRoot, changeName)
225
+ if (!existsSync(p)) return { reuse: false, reason: 'no-ledger', key }
226
+ const j = JSON.parse(readFileSync(p, 'utf8'))
227
+ if (!j || j.schemaVersion !== 2 || !j.key || !j.result || j.result.pass !== true) {
228
+ return { reuse: false, reason: 'ledger-invalid-or-failed-entry', key }
229
+ }
230
+ if (j.key !== key) return { reuse: false, reason: 'key-mismatch', key }
231
+ return { reuse: true, result: j.result, runPlan: Array.isArray(j.runPlan) ? j.runPlan : null, reason: 'match', key }
232
+ } catch {
233
+ return { reuse: false, reason: 'ledger-read-error', key }
234
+ }
235
+ }
236
+
237
+ /**
238
+ * 记账:只有通过结果落账(fail-closed 层②——失败永不缓存)。键不可得 → 不记(下次仍真跑)。
239
+ * 返回是否落账。
240
+ */
241
+ export function recordTestLedger({ runtimeRoot, changeName, projectRoot, testRoot, specBase, cwd, env, result }) {
242
+ if (!result || result.pass !== true) return false
243
+ const { identity, key } = ledgerKeyFor({ runtimeRoot, changeName, projectRoot, testRoot, specBase, cwd, env })
244
+ if (!identity || !key) return false
245
+ try {
246
+ writeAtomicSync(testLedgerPath(runtimeRoot, changeName), JSON.stringify({
247
+ schemaVersion: 2,
248
+ change: changeName,
249
+ key,
250
+ // runPlan 回放(审计面:账本声称复用的实际执行身份;不参与键——键已含其摘要)
251
+ runPlan: identity.runPlan,
252
+ result: {
253
+ pass: true,
254
+ total: result.total ?? null,
255
+ failed: result.failed ?? [],
256
+ failedFiles: result.failedFiles ?? [],
257
+ durationMs: result.durationMs ?? null,
258
+ strategy: result.strategy ?? null,
259
+ ranAt: new Date().toISOString(),
260
+ },
261
+ }, null, 2) + '\n')
262
+ return true
263
+ } catch { return false }
264
+ }
@@ -33,6 +33,7 @@ import { join, dirname } from 'node:path'
33
33
  import { writeAtomicSync } from '../fs-atomic.js'
34
34
  import { gitQuiet } from '../git-helper.js'
35
35
  import { resolveRuntimeRoot, parsePorcelainPath } from './shared.js'
36
+ import { computeEnvProfile, computeTestFaceDigest } from './test-ledger.js'
36
37
  import { resolveChangeRisk, extractExplicitRiskLevel } from '../change-risk-profile.js'
37
38
  import { loadBlastDeclarationsAllProjects } from '../blast-surface.js'
38
39
  import { parseFileChangeListDetailed } from '../change-list.js'
@@ -103,6 +104,7 @@ export function storeQualityScan({ specBase, cwd, changeName, testResult, lintRe
103
104
  const path = qualityScanRecordPath(specBase, changeName)
104
105
  if (!path) return null
105
106
  const fingerprint = computeQualityScanFingerprint({ cwd, specBase })
107
+ const dedupKey = computeQualityScanDedupKey({ cwd, specBase, fingerprint })
106
108
  try {
107
109
  mkdirSync(dirname(path), { recursive: true })
108
110
  writeAtomicSync(path, JSON.stringify({
@@ -112,7 +114,10 @@ export function storeQualityScan({ specBase, cwd, changeName, testResult, lintRe
112
114
  fingerprint,
113
115
  // 失败签名去重面(ql-20260920-010 修复一 d):additive 字段,存量记录无这两键 →
114
116
  // shouldReuseLastFailedScan 判 no-dedup-key 保守重跑一次,之后新记录即携带
115
- dedupKey: computeQualityScanDedupKey({ cwd, specBase, fingerprint }),
117
+ dedupKey,
118
+ // RERUN 失败签名(r5l 方案 4):dedupKey × 测试面 × 环境探针;同样 additive——存量
119
+ // 记录无此键 → RERUN 签名闸判无签名放行一次(同 no-dedup-key 先例口径)
120
+ rerunSignature: computeRerunSignature({ cwd, specBase, dedupKey, env: process.env }),
116
121
  usedSnapshot: Boolean(usedSnapshot),
117
122
  ranAt: new Date().toISOString(),
118
123
  testResult,
@@ -173,6 +178,36 @@ export function computeQualityScanDedupKey({ cwd, specBase, fingerprint = null }
173
178
  return createHash('sha256').update(fp + '\n##known-failures##\n' + kf).digest('hex')
174
179
  }
175
180
 
181
+ /**
182
+ * RERUN 失败签名(r5l-forensic-verdict.md 方案 4 / 评审护栏#6):dedupKey(代码指纹 ×
183
+ * known_failures 豁免面)× 测试面 × 环境探针 三路合成,供 SILLYSPEC_VERIFY_QUALITY_SCAN_RERUN=1
184
+ * 生效前对账——签名未变则拒绝重跑(重跑结果注定相同,R5-L 两次强制重扫各烧 ~8M+10m 的根治)。
185
+ *
186
+ * 环境必须入键(护栏#6 五态之「环境变放行」):环境真变(探针换值)签名才失配,RERUN=1 才
187
+ * 不会被假拒漏真拦。探针复用 test-ledger computeEnvProfile 同源实现,但剔除
188
+ * SILLYSPEC_VERIFY_QUALITY_SCAN_RERUN 自身——被测旋钮不是环境(原样入键 = RERUN=1 与历史
189
+ * 记录恒失配,闸门永不触发)。测试面同理复用 computeTestFaceDigest(test/ 读不到按
190
+ * 'unavailable' 入键——代码指纹的 HEAD+非文档脏集仍覆盖已提交/未提交的测试文件变更)。
191
+ *
192
+ * 任一 fail 分量不可得 → null:签名闸 fail-open(拒跑是危险方向——算不出签名就放行重跑,
193
+ * 宁可多跑一次不可漏真拦;与复用判定的 fail-closed 方向刻意相反)。
194
+ */
195
+ export function computeRerunSignature({ cwd, specBase, dedupKey = null, env = process.env }) {
196
+ const dedup = dedupKey ?? computeQualityScanDedupKey({ cwd, specBase })
197
+ if (!dedup) return null
198
+ let testFace = 'unavailable'
199
+ try {
200
+ const face = computeTestFaceDigest(join(cwd, 'test'))
201
+ if (face !== null) testFace = face
202
+ } catch { /* 测试面读不到 → unavailable 兜底入键 */ }
203
+ const profile = computeEnvProfile({ cwd, env })
204
+ if (!profile) return null
205
+ if (profile.behaviorEnv) delete profile.behaviorEnv.SILLYSPEC_VERIFY_QUALITY_SCAN_RERUN
206
+ return createHash('sha256')
207
+ .update(dedup + '\n##test-face##\n' + testFace + '\n##env-probe##\n' + JSON.stringify(profile))
208
+ .digest('hex')
209
+ }
210
+
176
211
  /**
177
212
  * 读回最近一条扫描记录(不论成败)——失败去重重放用。loadReusableQualityScan 拒绝
178
213
  * failed 记录(复用是优化不是正确性依赖),去重恰恰要消费 failed 态;schema/source/
@@ -297,7 +332,7 @@ export async function runCoverageExistenceCheck({ cwd, specBase, changeName }) {
297
332
  const artifactRel = (yamlText.match(/^\s*coverage_artifact:\s*["']?([^"'\n#]+?)["']?\s*(?:#.*)?$/m) || [])[1] || 'coverage/lcov.info'
298
333
  let runNote = ''
299
334
  try {
300
- execSync(covCmd.trim(), { cwd, encoding: 'utf8', timeout: 10 * 60 * 1000, maxBuffer: 32 * 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'] })
335
+ execSync(covCmd.trim(), { cwd, encoding: 'utf8', timeout: 10 * 60 * 1000, maxBuffer: 32 * 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true })
301
336
  runNote = '命令退出码 0'
302
337
  } catch (e) {
303
338
  runNote = `命令退出非 0(${e.status ?? '?'})——产物若已生成仍按产物判定`
@@ -407,7 +442,7 @@ function runSmokeCheck({ gateCwd, mainCwd, specBase, changeName, inSnapshot }) {
407
442
  let r = null
408
443
  let spawnError = null
409
444
  try {
410
- r = spawnSync(command, { shell: true, cwd: gateCwd, timeout: SMOKE_TIMEOUT_MS, encoding: 'utf8', maxBuffer: 32 * 1024 * 1024 })
445
+ r = spawnSync(command, { shell: true, cwd: gateCwd, timeout: SMOKE_TIMEOUT_MS, encoding: 'utf8', maxBuffer: 32 * 1024 * 1024, windowsHide: true })
411
446
  } catch (e) { spawnError = e }
412
447
  let timedOut = isSyncTimeout(r)
413
448
  let fallbackMainRepo = false
@@ -417,7 +452,7 @@ function runSmokeCheck({ gateCwd, mainCwd, specBase, changeName, inSnapshot }) {
417
452
  fallbackMainRepo = true
418
453
  console.warn('⚠️ 快照 smoke 超时(node_modules junction I/O 慢)→ 主仓复跑 smoke(并行噪声可能混入——失败先做归属鉴定)')
419
454
  try {
420
- r = spawnSync(command, { shell: true, cwd: mainCwd, timeout: SMOKE_TIMEOUT_MS, encoding: 'utf8', maxBuffer: 32 * 1024 * 1024 })
455
+ r = spawnSync(command, { shell: true, cwd: mainCwd, timeout: SMOKE_TIMEOUT_MS, encoding: 'utf8', maxBuffer: 32 * 1024 * 1024, windowsHide: true })
421
456
  spawnError = null
422
457
  ranCwd = mainCwd
423
458
  timedOut = isSyncTimeout(r)
@@ -493,12 +528,43 @@ function warnIfMainRepoDirtyForGate(cwd) {
493
528
  */
494
529
  export async function executeVerifyQualityScan({ cwd, specBase, changeName, platformOpts }) {
495
530
  const { runVerifyTestCheck, printVerifyTestCheck, runVerifyLintCheck, printVerifyLintCheck, shouldBlockVerifyLint, classifyTestFailureArtifact } = await import('../verify-postcheck.js')
531
+ // ── RERUN 失败签名闸(r5l-forensic-verdict.md 方案 4 / 评审护栏#6 五态)──
532
+ // R5-L 实证:RERUN=1 两次强制重扫各烧 ~8M 重发 + 10m/6.8m 等待,而失败签名未变——重跑
533
+ // 结果注定相同。故 RERUN=1 生效前先对账失败签名(dedupKey 已含代码指纹×豁免面,再叠测试
534
+ // 面×环境探针):未变 → 拒绝重跑(throw=退出码非 0);环境真变(探针入键)/代码/豁免/测试
535
+ // 面变化 → 签名失配自动放行;RERUN=force 无条件放行(真·逃生阀,旧 RERUN=1 的裸语义移到
536
+ // 此档)。RERUN 未设时本闸不参与——下方既有 dedup 复用判定与 fail-closed 语义一行不动。
537
+ const rerunMode = process.env.SILLYSPEC_VERIFY_QUALITY_SCAN_RERUN
538
+ if (rerunMode === 'force') {
539
+ console.log('ℹ️ SILLYSPEC_VERIFY_QUALITY_SCAN_RERUN=force——无条件强制重跑(RERUN 签名闸与失败签名去重均旁路)')
540
+ } else if (rerunMode === '1') {
541
+ const lastRecord = loadLastQualityScanRecord({ specBase, changeName })
542
+ const lastSig = lastRecord ? lastRecord.rerunSignature : null
543
+ const lastFailed = lastRecord && lastRecord.testResult && lastRecord.testResult.status === 'failed'
544
+ if (lastFailed && lastSig) {
545
+ const sig = computeRerunSignature({ cwd, specBase })
546
+ if (sig && sig === lastSig) {
547
+ const t = lastRecord.testResult
548
+ console.error(`\n🛑 RERUN 签名闸:上次质量扫描失败且失败签名未变(代码指纹 / known_failures 豁免面 / 测试面 / 环境探针均未变${lastRecord.ranAt ? ',上次运行 ' + lastRecord.ranAt : ''})——重跑结果注定相同,拒绝重跑:`)
549
+ console.error(` 上次实测:\`${t.command}\` — ${(t.reason || '').split('\n')[0]}`)
550
+ if (t.outputTail) {
551
+ for (const line of t.outputTail.split('\n').slice(-8)) console.error(` | ${line}`)
552
+ }
553
+ console.error(' 出路(按序):① 修代码(指纹即失配,下次自动重测)② 补 known_failures 豁免(豁免面即失配)③ 换快照口径(设/删 SILLYSPEC_VERIFY_GATE_SNAPSHOT_OFF,口径变化即失配)④ 环境确已修好后强制重跑:SILLYSPEC_VERIFY_QUALITY_SCAN_RERUN=1(环境探针已入失败签名——环境真变即自动放行;仍被拒=探针未见变化,确要无条件重跑用 SILLYSPEC_VERIFY_QUALITY_SCAN_RERUN=force)')
554
+ throw new Error(`RERUN 签名闸拒绝重跑(失败签名未变:代码指纹×测试面×环境探针全等)——${(t.reason || '测试失败').split('\n')[0]}。按上方出路处置;确要无条件重跑设 SILLYSPEC_VERIFY_QUALITY_SCAN_RERUN=force。`)
555
+ }
556
+ } else if (lastFailed) {
557
+ console.log('ℹ️ RERUN=1:上次失败记录无 rerunSignature(存量记录)——签名闸放行一次,本次重跑后新记录即携带签名')
558
+ } else {
559
+ console.log('ℹ️ SILLYSPEC_VERIFY_QUALITY_SCAN_RERUN=1——强制重跑(上次无失败记录在案,签名闸无事可对)')
560
+ }
561
+ }
496
562
  // ── 失败签名去重(ql-20260920-010 修复一 d)──
497
563
  // 复入本步前先对账上次失败:代码指纹 + known_failures 豁免面 + 快照口径都没变 → 失败
498
564
  // 必然原样复现,重跑是纯等待(对撞三轮三连 8~10 分钟假红重试的根治)。复用只消费
499
565
  // failed 态记录(passed 态 --done 复用归 loadReusableQualityScan),逃生阀
500
- // SILLYSPEC_VERIFY_QUALITY_SCAN_RERUN=1。
501
- if (process.env.SILLYSPEC_VERIFY_QUALITY_SCAN_RERUN !== '1') {
566
+ // SILLYSPEC_VERIFY_QUALITY_SCAN_RERUN=1(闸门见上方签名闸)与 =force(无条件旁路)。
567
+ if (rerunMode !== '1' && rerunMode !== 'force') {
502
568
  const lastRecord = loadLastQualityScanRecord({ specBase, changeName })
503
569
  const verdict = shouldReuseLastFailedScan({
504
570
  lastRecord,
@@ -519,7 +585,7 @@ export async function executeVerifyQualityScan({ cwd, specBase, changeName, plat
519
585
  console.error(` 🔬 失败签名命中伪影形态(${replayTriage.kind}):${replayTriage.advice}`)
520
586
  console.error(' (上次运行早于分诊机制——本次重跑会自动做主树口径对照;如需立即重跑设 SILLYSPEC_VERIFY_QUALITY_SCAN_RERUN=1)')
521
587
  }
522
- console.error(' 出路(按序):① 修代码(指纹即失配,下次自动重测)② 补 known_failures 豁免(豁免面即失配)③ 换快照口径(设/删 SILLYSPEC_VERIFY_GATE_SNAPSHOT_OFF,口径变化即失配)④ 环境确已修好后强制重跑:SILLYSPEC_VERIFY_QUALITY_SCAN_RERUN=1')
588
+ console.error(' 出路(按序):① 修代码(指纹即失配,下次自动重测)② 补 known_failures 豁免(豁免面即失配)③ 换快照口径(设/删 SILLYSPEC_VERIFY_GATE_SNAPSHOT_OFF,口径变化即失配)④ 环境确已修好后强制重跑:SILLYSPEC_VERIFY_QUALITY_SCAN_RERUN=1(环境探针已入失败签名——环境真变即放行;仍被拒用 SILLYSPEC_VERIFY_QUALITY_SCAN_RERUN=force)')
523
589
  throw new Error(`noAI 质量扫描复用上次失败结果(失败签名未变,跳过重跑)——${(t.reason || '测试失败').split('\n')[0]}。修复后重跑 sillyspec run verify${changeName ? ` --change ${changeName}` : ''},或按上方出路处置。`)
524
590
  }
525
591
  }
@@ -530,6 +596,16 @@ export async function executeVerifyQualityScan({ cwd, specBase, changeName, plat
530
596
  // ⚠️ 口径可见。指纹(storeQualityScan)仍按主仓 cwd 算——--done 对账复用的指纹匹配口径不变。
531
597
  let snap = null
532
598
  if (!process.env.SILLYSPEC_VERIFY_GATE_SNAPSHOT_OFF) {
599
+ // worktree 会话跳快照(2026-09-23 R9/R10 根治):worktree 已是会话独占隔离——快照只剩
600
+ // venv junction 冻结/慢 I/O 纯成本(R9 3MB/0CPU 冻结、R10 15min 慢跑实证),直接工作树实测。
601
+ let skipForWorktree = false
602
+ try {
603
+ const { shouldSkipGateSnapshotForWorktree } = await import('./gate-snapshot.js')
604
+ skipForWorktree = shouldSkipGateSnapshotForWorktree(cwd)
605
+ } catch { /* 判定异常按不跳走原路 */ }
606
+ if (skipForWorktree) {
607
+ console.log('🔀 cwd 是会话专属 worktree——隔离快照冗余(并行会话不在场;快照 venv junction 冻结/慢 I/O 面 R9/R10 双实证),直接在 worktree 实测')
608
+ } else {
533
609
  try {
534
610
  const { createVerifyGateSnapshot } = await import('./gate-snapshot.js')
535
611
  snap = await createVerifyGateSnapshot({ cwd, changeName, specBase, platformOpts })
@@ -537,6 +613,7 @@ export async function executeVerifyQualityScan({ cwd, specBase, changeName, plat
537
613
  console.log(`🧪 noAI 扫描隔离快照(根:${snap.snapshotRoot}):HEAD + 本变更 ${snap.changeFileCount} 个文件${snap.sourceRoot ? '(overlay 自 worktree——本变更分支内容定向跑)' : ''}(并行会话脏文件不参与判定)`)
538
614
  }
539
615
  } catch { /* 快照链路异常 → 主仓现行为 */ }
616
+ }
540
617
  }
541
618
  const gateCwd = snap ? snap.snapshotRoot : cwd
542
619
  const gateSpecBase = snap ? join(snap.snapshotRoot, '.sillyspec') : specBase
@@ -321,7 +321,12 @@ export function buildFrozenPatch(root, files, { baseRef } = {}) {
321
321
  const parts = []
322
322
  try {
323
323
  const r = safeGit(root, ['diff', '--no-color', baseRef], { timeout: 60 * 1000 })
324
- const filtered = filterPatchForFiles(r.error ? null : r.value, wanted)
324
+ // diff 采集失败 fail-closed(2026-09-27-thin-module-scope-persist 评审 P1 清偿:此前失败静默
325
+ // 跳过 tracked 段,untracked 自拼 hunk 仍拼出非空 patch → patchStatus ok 的伪完整件——实证
326
+ // core.bare 被误写 true 时 `git diff <ref>` 报「must be run in a work tree」,change.patch
327
+ // 只剩 proposal.md 一个 hunk 却标 ok。失败即 null(jsdoc 契约:调用方 fail-soft 不落伪 patch))
328
+ if (r.error) return null
329
+ const filtered = filterPatchForFiles(r.value, wanted)
325
330
  if (filtered) parts.push(filtered)
326
331
  const trackedOut = safeGit(root, ['ls-files'], { timeout: 30 * 1000 })
327
332
  const trackedSet = new Set(String(trackedOut.value || '').split('\n').map(toPosix).filter(Boolean))
@@ -0,0 +1,114 @@
1
+ /**
2
+ * sentinel-assertions.js — L0 收口侧纯断言(2026-09-23-sentinel-rules / FR-07 / D-002@v1)。
3
+ *
4
+ * 与 watcher 侧哨兵规则(src/watcher.js R1 假勾选)同判据的**硬门版**纯函数:--done 收口
5
+ * 侧调用拒收「全勾但零完成证据」的假完成主张(L0 升级通道——watcher 恒 advisory 人判,
6
+ * 收口是唯一拒绝点,符合「守卫必须是验收侧的机制,永远不是生成侧的劝说」设计原则)。
7
+ *
8
+ * ⚠️ 本批(task-03)只交付函数+单测,**无任何仓内调用方**——收口接线留下批(避免与并行
9
+ * 会话改同文件);测试文件是当前唯一消费方。
10
+ *
11
+ * 纯度口径:无副作用(零写盘/零 db),输出由参数+只读盘面(review.json 在场探测)决定;
12
+ * 盘面探测可经 opts.listReviewsImpl 注入替身(单测零真 fs)。证据判据与 R1 同口径:
13
+ * commit 消息(标题或正文——2026-09-25 起收口侧取 %B 整条消息)含完整 token task-NN
14
+ * (负向前瞻,task-01 不证 task-010)或对应 review.json 在场(.runtime/execute-runs
15
+ * 任意 run 的 tasks 目录下对应任务子目录)。
16
+ */
17
+ import { join, dirname } from 'path';
18
+ import { existsSync, readdirSync } from 'fs';
19
+
20
+ /** 判集行锚:`- [x] task-01: …`(id 行才是可验证主张;无 id 勾选行不入判——不可验证不拒收)。 */
21
+ const CHECKED_TASK_LINE_RE = /^[-*] \[( |x|X)\] (task-\d+)/;
22
+
23
+ /** task id 完整 token 匹配(词边界,与 watcher R1 同口径)。 */
24
+ function taskTokenRe(id) {
25
+ return new RegExp(`${id}(?!\\d)`);
26
+ }
27
+
28
+ /**
29
+ * review.json 在场证据清单(execute-runs 两级遍历;异常/缺失 → [],commit 证据照判)。
30
+ */
31
+ function listReviewEvidence(changeDir, opts) {
32
+ if (typeof opts.listReviewsImpl === 'function') {
33
+ try { return opts.listReviewsImpl(changeDir) || []; } catch { return []; }
34
+ }
35
+ if (!changeDir) return [];
36
+ try {
37
+ const runsRoot = join(dirname(changeDir), '.runtime', 'execute-runs');
38
+ const out = [];
39
+ for (const run of readdirSync(runsRoot)) {
40
+ let names = [];
41
+ try { names = readdirSync(join(runsRoot, run, 'tasks')); } catch { continue; }
42
+ for (const t of names) {
43
+ if (existsSync(join(runsRoot, run, 'tasks', t, 'review.json'))) {
44
+ out.push(`execute-runs/${run}/tasks/${t}/review.json`);
45
+ }
46
+ }
47
+ }
48
+ return out;
49
+ } catch {
50
+ return [];
51
+ }
52
+ }
53
+
54
+ /**
55
+ * 假勾选三态判定(L0 收口拒收支点,下批接线)。
56
+ *
57
+ * @param {object} args
58
+ * @param {string|null} args.changeDir 变更目录(推导 specBase 定位 execute-runs;null/探测
59
+ * 失败按无 review 证据,commit 证据照判)
60
+ * @param {string|null} args.tasksMd tasks.md 全文(判集=行首 checkbox+task-NN id 行)
61
+ * @param {Array<string|{message:string}|{subject:string}>} args.commits 提交组(消息含
62
+ * task-NN 即该任务证据)
63
+ * @param {{listReviewsImpl?: Function}} [args.opts] 注入面(测试替身)
64
+ * @returns {{status:'complete'|'fake'|'none', claimTotal:number, checked:number, missing:string[]}}
65
+ * none=无完成主张(判集空或未全勾);complete=全勾且零 missing;fake=全勾且有零证据
66
+ * 任务(missing 列其 id,收口侧拒收依据)。
67
+ */
68
+ export function detectFakeCheckCompletion({ changeDir, tasksMd, commits, opts = {} } = {}) {
69
+ const entries = [];
70
+ for (const line of String(tasksMd || '').split(/\r?\n/)) {
71
+ const m = line.match(CHECKED_TASK_LINE_RE);
72
+ if (m) entries.push({ id: m[2], checked: m[1].toLowerCase() === 'x' });
73
+ }
74
+ const claimTotal = entries.length;
75
+ const checked = entries.filter((e) => e.checked).length;
76
+ if (claimTotal === 0 || checked < claimTotal) {
77
+ return { status: 'none', claimTotal, checked, missing: [] };
78
+ }
79
+ const messages = (Array.isArray(commits) ? commits : [])
80
+ .map((c) => (typeof c === 'string' ? c : String((c && (c.message ?? c.subject)) || '')));
81
+ const reviews = listReviewEvidence(changeDir, opts);
82
+ const missing = entries
83
+ .filter((e) => {
84
+ const re = taskTokenRe(e.id);
85
+ const byCommit = messages.some((msg) => re.test(msg));
86
+ const byReview = reviews.some((p) => p.includes(`/tasks/${e.id}/`));
87
+ return !(byCommit || byReview);
88
+ })
89
+ .map((e) => e.id);
90
+ return { status: missing.length === 0 ? 'complete' : 'fake', claimTotal, checked, missing };
91
+ }
92
+
93
+ /**
94
+ * 勾选节奏检测(2026-09-26-thin-check-cadence):watcher 事件流里 task-done 单拍跳 ≥2 格 =
95
+ * 一把全勾(未按工作单元逐个勾)——thin-agent-tasks 纪律的收口侧 advisory 判定面(warn 不
96
+ * 阻断,接线在 flow done ledger 子步;节奏是习惯问题非造假主张,L0 硬门另有其人)。
97
+ * 纯函数:事件清单→最大跳格记录;无多格跳返回 null。detail 与 watcher inferEvents 生成
98
+ * 格式成对(`checked N→M`);解析失配(格式漂移/坏行/非 task-done)按无证据静默——fail-open。
99
+ */
100
+ export function detectBatchCheckCadence(events) {
101
+ let worst = null;
102
+ for (const e of events || []) {
103
+ if (!e || e.kind !== 'task-done') continue;
104
+ const m = /^checked (\d+)→(\d+)$/.exec(String(e.detail || ''));
105
+ if (!m) continue;
106
+ const from = Number(m[1]);
107
+ const to = Number(m[2]);
108
+ if (to - from < 2) continue;
109
+ if (!worst || to - from > worst.to - worst.from) worst = { from, to, detail: e.detail, ts: e.ts };
110
+ }
111
+ return worst;
112
+ }
113
+
114
+ export default { detectFakeCheckCompletion, detectBatchCheckCadence };
package/src/setup.js CHANGED
@@ -171,7 +171,7 @@ export async function cmdSetup(dir, options = {}) {
171
171
  results['全局工具'] = {};
172
172
  for (const g of GLOBAL_TOOLS) {
173
173
  try {
174
- execSync(g.checkCommand, { stdio: 'pipe', encoding: 'utf8' });
174
+ execSync(g.checkCommand, { stdio: 'pipe', encoding: 'utf8', windowsHide: true });
175
175
  results['全局工具'][g.id] = true;
176
176
  } catch {
177
177
  results['全局工具'][g.id] = false;
@@ -252,7 +252,7 @@ export async function cmdSetup(dir, options = {}) {
252
252
  const globalInstalled = new Set();
253
253
  for (const tool of GLOBAL_TOOLS) {
254
254
  try {
255
- execSync(tool.checkCommand, { stdio: 'pipe', encoding: 'utf8' });
255
+ execSync(tool.checkCommand, { stdio: 'pipe', encoding: 'utf8', windowsHide: true });
256
256
  globalInstalled.add(tool.id);
257
257
  } catch {}
258
258
  }
@@ -379,7 +379,7 @@ export async function cmdSetup(dir, options = {}) {
379
379
  for (const tool of selectedGlobal) {
380
380
  const spinner = ora(`安装 ${tool.name}...`).start();
381
381
  try {
382
- execSync(tool.installCommand, { stdio: 'pipe', encoding: 'utf8', timeout: 120000 });
382
+ execSync(tool.installCommand, { stdio: 'pipe', encoding: 'utf8', timeout: 120000, windowsHide: true });
383
383
  spinner.succeed(`${tool.name} 安装完成`);
384
384
  } catch (err) {
385
385
  spinner.fail(`${tool.name} 安装失败: ${err.message}`);