sillyspec 3.26.15 → 3.27.1
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.
- package/.claude/skills/sillyspec-explore/SKILL.md +10 -3
- package/.claude/skills/sillyspec-verify/SKILL.md +2 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/packages/dashboard/server/index.js +22 -6
- package/src/change-risk-profile.js +21 -6
- package/src/db.js +14 -3
- package/src/docs-check.js +598 -570
- package/src/doctor-diagnostics.js +53 -1
- package/src/endpoint-extractor.js +21 -2
- package/src/fs-atomic.js +4 -2
- package/src/git-helper.js +73 -5
- package/src/hooks/claude-pre-tool-use.cjs +10 -5
- package/src/hooks/worktree-guard.js +22 -3
- package/src/index.js +49 -6
- package/src/module-impact.js +21 -1
- package/src/module-resolve.js +211 -0
- package/src/modules.js +82 -2
- package/src/progress/step-store.js +12 -8
- package/src/progress.js +6 -2
- package/src/run/complete-handlers.js +17 -2
- package/src/run/complete.js +43 -7
- package/src/run/gates.js +83 -14
- package/src/run/prompt.js +16 -2
- package/src/run/quick-audit.js +5 -2
- package/src/run/shared.js +46 -18
- package/src/run/stage.js +37 -3
- package/src/stage-contract.js +14 -1
- package/src/stages/archive.js +3 -1
- package/src/stages/execute.js +170 -18
- package/src/stages/explore.js +11 -2
- package/src/stages/quick.js +4 -3
- package/src/stages/verify.js +9 -2
- package/src/sync.js +44 -3
- package/src/task-review.js +55 -7
- package/src/taskcard.js +52 -22
- package/src/verify-postcheck.js +223 -6
- package/src/worktree-apply.js +28 -10
- package/src/worktree-deps.js +59 -5
- package/src/worktree.js +70 -6
package/src/run/gates.js
CHANGED
|
@@ -19,8 +19,10 @@
|
|
|
19
19
|
*
|
|
20
20
|
* 不搬:ensureDepsFreshness(execute 入口 deps 自检,调用方 runStage 非 completeStep,归属未来 execute-handler)
|
|
21
21
|
*/
|
|
22
|
-
import { basename, join } from 'node:path'
|
|
23
|
-
import {
|
|
22
|
+
import { basename, join, relative, dirname } from 'node:path'
|
|
23
|
+
import { createHash } from 'node:crypto'
|
|
24
|
+
import { existsSync, readFileSync, readdirSync, statSync, mkdirSync, unlinkSync, writeFileSync } from 'node:fs'
|
|
25
|
+
import { writeAtomicSync } from '../fs-atomic.js'
|
|
24
26
|
import { triggerSync, resolveChangeDir, resolveRuntimeRoot } from './shared.js'
|
|
25
27
|
import { runValidators } from '../stage-contract.js'
|
|
26
28
|
import { handleScanStageCompleted, handleExecuteWorktreeCleanup } from './complete-handlers.js'
|
|
@@ -89,9 +91,17 @@ export function validateSymbolImpactReport({ reportContent, planContent }) {
|
|
|
89
91
|
export function generateSymbolImpactSkeleton(tasksContent) {
|
|
90
92
|
const taskIds = extractTaskIdsFromRegistry(tasksContent)
|
|
91
93
|
if (taskIds.length === 0) return null
|
|
94
|
+
// tasks.md 内容指纹(token 成本优化 P2a):中断续跑/阶段重开时若指纹一致且结论完整,
|
|
95
|
+
// 「加载上下文」步按 prompt 指引直接沿用既有报告,不重做调用点扫描(team-session-unify
|
|
96
|
+
// 实测该步 3h06min,重入重做是纯浪费)。指纹只作提示锚,gate 校验逻辑不变。
|
|
97
|
+
let fingerprint = ''
|
|
98
|
+
try {
|
|
99
|
+
fingerprint = createHash('sha256').update(String(tasksContent)).digest('hex').slice(0, 16)
|
|
100
|
+
} catch { /* 指纹失败不阻断骨架生成 */ }
|
|
92
101
|
const lines = [
|
|
93
102
|
'# 符号影响面报告',
|
|
94
103
|
'',
|
|
104
|
+
`> tasks.md 内容指纹(生成时): ${fingerprint}——重入本步时若与当前 tasks.md 指纹一致且结论已填全,直接沿用不重做扫描。`,
|
|
95
105
|
'> 骨架由 CLI 生成(`sillyspec symbol-impact --change <变更名>`,gate 失败时也会自动落一份)。',
|
|
96
106
|
'> 逐行把 `<!--TODO-->` 替换为真实结论:涉及签名级变更(构造函数参数/接口/DTO/方法签名增删改)',
|
|
97
107
|
'> 写变更类型 + 受影响调用点 + 是否在任务范围内;无签名级变更也要显式写「无签名级变更」。',
|
|
@@ -104,7 +114,8 @@ export function generateSymbolImpactSkeleton(tasksContent) {
|
|
|
104
114
|
|
|
105
115
|
/**
|
|
106
116
|
* 符号影响面报告硬门:execute「加载上下文」步 --done 时校验 symbol-impact.md 落盘 + task 覆盖度。
|
|
107
|
-
* fail-closed:不通过 →
|
|
117
|
+
* fail-closed:不通过 → exit 1(进度不推进;步骤状态保持 pending,无 steps 句柄不标 blocked)。
|
|
118
|
+
* 非目标步骤/阶段直接放行。
|
|
108
119
|
*/
|
|
109
120
|
export async function enforceSymbolImpactGate(stageName, changeName, currentStepName, specBase) {
|
|
110
121
|
if (stageName !== 'execute') return
|
|
@@ -129,7 +140,7 @@ export async function enforceSymbolImpactGate(stageName, changeName, currentStep
|
|
|
129
140
|
const skeleton = generateSymbolImpactSkeleton(tasksContent)
|
|
130
141
|
if (skeleton) {
|
|
131
142
|
try {
|
|
132
|
-
|
|
143
|
+
writeAtomicSync(reportPath, skeleton)
|
|
133
144
|
skeletonNote = `\n 📄 已代生成逐 task 骨架:${reportPath}(逐行替换 <!--TODO--> 为结论,无签名级变更也显式写「无」)`
|
|
134
145
|
} catch { /* 写失败只影响提示,不掩盖原始错误 */ }
|
|
135
146
|
}
|
|
@@ -286,9 +297,10 @@ export async function enforceAlignExecuteReviewGate({ cwd, changeName, specBase,
|
|
|
286
297
|
/**
|
|
287
298
|
* execute deps 验证硬门(change 2026-06-28-worktree-deps-provision / D-001@v1, D-003@v1, D-006@v2)。
|
|
288
299
|
* depsStatus 不达标且非 wave 级 opt-out 时阻断 --done:置 step=blocked + exit(1),与 requiresWait 同范式。
|
|
289
|
-
* 放行返回 true
|
|
300
|
+
* 放行返回 true;阻断时经 persist 回调落盘 blocked 后 exit(1)(此前只改内存不落库,
|
|
301
|
+
* progress show/doctor 看到的仍是 pending,与 docstring 承诺不符、误导诊断)。
|
|
290
302
|
*/
|
|
291
|
-
export async function enforceDepsGate(stageName, cwd, changeName, step, steps, currentIdx, specBase, platformOpts) {
|
|
303
|
+
export async function enforceDepsGate(stageName, cwd, changeName, step, steps, currentIdx, specBase, platformOpts, persist) {
|
|
292
304
|
if (stageName !== 'execute') return true
|
|
293
305
|
let meta = null
|
|
294
306
|
let wm = null
|
|
@@ -324,6 +336,7 @@ export async function enforceDepsGate(stageName, cwd, changeName, step, steps, c
|
|
|
324
336
|
console.error(' 或在 worktree 内手动安装依赖后重试。')
|
|
325
337
|
if (meta?.depsError) console.error(` 上次供给错误:${meta.depsError}`)
|
|
326
338
|
}
|
|
339
|
+
if (persist) { try { await persist() } catch { /* 落盘失败不吞阻断语义 */ } }
|
|
327
340
|
process.exit(1)
|
|
328
341
|
}
|
|
329
342
|
|
|
@@ -333,10 +346,11 @@ export async function enforceDepsGate(stageName, cwd, changeName, step, steps, c
|
|
|
333
346
|
* Task Review Gate(validateTaskReviews)只在 execute 整阶段完成时跑(complete.js 阶段完成分支的
|
|
334
347
|
* actualCompleted===actualTotal 守卫),单 task --done 不校验 → 子代理勾 checkbox 却漏写/漏字段
|
|
335
348
|
* review.json,要到收尾才暴露,用户被迫事后批量补。本门提前到每次 --done:校验 plan 里所有已勾
|
|
336
|
-
* task 的 review.json,缺字段/不存在/JSON 坏 → 置 step=blocked + exit(1),与 enforceDepsGate
|
|
349
|
+
* task 的 review.json,缺字段/不存在/JSON 坏 → 置 step=blocked + exit(1),与 enforceDepsGate 同范式
|
|
350
|
+
* (阻断前经 persist 回调落盘 blocked)。
|
|
337
351
|
* 未勾 task 不校验(还没做)。平台模式/无 marker/无 plan 时放行(下游 Task Review Gate 兜底)。
|
|
338
352
|
*/
|
|
339
|
-
export async function enforceReviewJsonGate(stageName, cwd, changeName, step, steps, currentIdx, specBase, platformOpts) {
|
|
353
|
+
export async function enforceReviewJsonGate(stageName, cwd, changeName, step, steps, currentIdx, specBase, platformOpts, persist) {
|
|
340
354
|
if (stageName !== 'execute' || !changeName) return true
|
|
341
355
|
// head 锡点自动落盘(2026-08-21 审计项③,D-010 补对称):跨仓 task base_commit 已在派发时
|
|
342
356
|
// CLI 落盘,head_commit 此前靠主 agent 按 prompt 手跑 rev-parse 手写(漏抄/抄错炸 review gate)。
|
|
@@ -399,6 +413,7 @@ export async function enforceReviewJsonGate(stageName, cwd, changeName, step, st
|
|
|
399
413
|
for (const e of f.errors) console.error(` - ${e}`)
|
|
400
414
|
}
|
|
401
415
|
console.error(' 修复:mechanics 字段(base/head/changedFiles/schemaVersion)缺错可跑 sillyspec backfill-reviews --change ' + changeName + ' --adopt 一键代填(verdict 保留);verdict/证据缺失需人工补全后重跑 execute --done。')
|
|
416
|
+
if (persist) { try { await persist() } catch { /* 落盘失败不吞阻断语义 */ } }
|
|
402
417
|
process.exit(1)
|
|
403
418
|
}
|
|
404
419
|
|
|
@@ -486,6 +501,14 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
486
501
|
console.error(' 请修复失败的测试并更新 verify-result.md 后重新完成此步骤。')
|
|
487
502
|
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
488
503
|
}
|
|
504
|
+
// lint 对账(2026-08-21 审查 CLI-1):test 侧"自报告 PASS 但实测失败→阻断"已闭环,
|
|
505
|
+
// lint 侧此前纯口头——CLI 亲自执行 commands.lint,advisory 起步(失败打印不阻断,观察期后升级)
|
|
506
|
+
const { runVerifyLintCheck, printVerifyLintCheck } = await import('../verify-postcheck.js')
|
|
507
|
+
const lintCheck = runVerifyLintCheck({ cwd, specBase })
|
|
508
|
+
if (lintCheck.status !== 'skipped') {
|
|
509
|
+
console.log(`\n⏳ Verify lint 对账:CLI 亲自执行 local.yaml 的 commands.lint…`)
|
|
510
|
+
}
|
|
511
|
+
printVerifyLintCheck(lintCheck)
|
|
489
512
|
// 契约 parity 对账:扫前端 API 调用 vs execute 提取的 provider endpoint artifact。
|
|
490
513
|
// 接线自 contract-matrix pipeline(verifyApiParity 的 CLI 入口)。
|
|
491
514
|
const { runVerifyParityCheck, printVerifyParityCheck } = await import('../verify-postcheck.js')
|
|
@@ -520,6 +543,31 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
520
543
|
}
|
|
521
544
|
}
|
|
522
545
|
}
|
|
546
|
+
// ── verify 服务进程回收(坑 verify-service-process-leak,2026-08-21 实证:真实启动验证
|
|
547
|
+
// 起的 uvicorn 漏挂一天多)。verify prompt 要求长驻服务 PID 登记 .runtime/verify-services.pids,
|
|
548
|
+
// 此处收尾逐个 kill + 清文件。best-effort:ESRCH(已退出)静默,kill 失败 warn 不阻断 verify。
|
|
549
|
+
try {
|
|
550
|
+
const servicesPath = join(resolveRuntimeRoot(platformOpts, specBase), 'verify-services.pids')
|
|
551
|
+
if (existsSync(servicesPath)) {
|
|
552
|
+
const pids = readFileSync(servicesPath, 'utf8').split('\n').map(l => l.trim()).filter(l => /^\d+$/.test(l))
|
|
553
|
+
const reaped = []; const failed = []
|
|
554
|
+
for (const pid of pids) {
|
|
555
|
+
try { process.kill(parseInt(pid, 10), 'SIGTERM'); reaped.push(pid) }
|
|
556
|
+
catch (e) { if (e.code !== 'ESRCH') failed.push(`${pid}(${e.code || e.message})`) }
|
|
557
|
+
}
|
|
558
|
+
try { unlinkSync(servicesPath) } catch {}
|
|
559
|
+
if (reaped.length > 0) console.log(`🧹 verify 服务进程已回收 ${reaped.length} 个(PID: ${reaped.join(', ')})`)
|
|
560
|
+
// CLI 回执(坑 verify-literal-evidence-mismatch):回收即真实启动的结构化证据——
|
|
561
|
+
// checkIntegrationEvidence(stage-contract)读回执注入匹配,agent 措辞差异不再误拦
|
|
562
|
+
try {
|
|
563
|
+
writeFileSync(join(dirname(servicesPath), 'verify-services.receipt.json'), JSON.stringify({
|
|
564
|
+
change: changeName || null, reapedPidCount: reaped.length, reapedAt: new Date().toISOString(),
|
|
565
|
+
}, null, 2) + '\n')
|
|
566
|
+
} catch {}
|
|
567
|
+
if (failed.length > 0) console.warn(`⚠️ 服务进程回收失败 ${failed.length} 个:${failed.join(', ')} — 手动处理:taskkill /PID <pid> /F(Windows)/ kill -9 <pid>`)
|
|
568
|
+
}
|
|
569
|
+
} catch (e) { console.warn(`⚠️ verify 服务进程回收异常(不阻断): ${e.message}`) }
|
|
570
|
+
|
|
523
571
|
console.log('\n✅ 验证通过,下一步:sillyspec run archive')
|
|
524
572
|
}
|
|
525
573
|
|
|
@@ -581,7 +629,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
581
629
|
reviewRunId = generateStageReviewRunId()
|
|
582
630
|
try {
|
|
583
631
|
mkdirSync(runtimeRoot, { recursive: true })
|
|
584
|
-
|
|
632
|
+
writeAtomicSync(stageReviewMarkerPath(runtimeRoot, stageName, changeName), reviewRunId + '\n')
|
|
585
633
|
} catch {}
|
|
586
634
|
}
|
|
587
635
|
const reviewType = stageName === 'brainstorm' ? 'design'
|
|
@@ -601,6 +649,26 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
601
649
|
}
|
|
602
650
|
}
|
|
603
651
|
|
|
652
|
+
// ── module-impact pending 死信 execute 收尾提示(坑 module-impact-debt-late-warn,
|
|
653
|
+
// 2026-08-22 实证:死信门在 verify 才硬拦,文档债拖到 verify 末尾才暴露、上次也拦)。
|
|
654
|
+
// execute --done 即 advisory 提示(不阻断 execute——execute 期间可能仍在迭代模块文档;
|
|
655
|
+
// verify 的硬拦不变),让 agent 当场清债而非攒到 verify 撞门。
|
|
656
|
+
if (stageName === 'execute') {
|
|
657
|
+
try {
|
|
658
|
+
const execChangeDir = resolveChangeDir(cwd, progress, platformOpts?.specRoot)
|
|
659
|
+
const execImpactPath = execChangeDir ? join(execChangeDir, 'module-impact.md') : null
|
|
660
|
+
if (execImpactPath && existsSync(execImpactPath)) {
|
|
661
|
+
const { extractPendingDocSyncRows } = await import('./complete-handlers.js')
|
|
662
|
+
const execPendingRows = extractPendingDocSyncRows(readFileSync(execImpactPath, 'utf8'))
|
|
663
|
+
if (execPendingRows.length > 0) {
|
|
664
|
+
console.warn(`\n⚠️ module-impact.md「更新结果」表有 ${execPendingRows.length} 个 pending/待办项(文档同步债)——verify 阶段会硬拦:`)
|
|
665
|
+
for (const row of execPendingRows) console.warn(` - ${row}`)
|
|
666
|
+
console.warn(' 建议 execute 收尾就完成模块文档同步并回填 done/skipped(说明原因),别把债拖到 verify 末尾。')
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
} catch { /* 提示失败不阻断 execute */ }
|
|
670
|
+
}
|
|
671
|
+
|
|
604
672
|
// ── Execute Task Review Gate:所有 task 必须有 review.json 且 verdict 通过 ──
|
|
605
673
|
if (stageName === 'execute') {
|
|
606
674
|
try {
|
|
@@ -645,7 +713,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
645
713
|
// marker 在则目录在)。不 try/catch——异常直穿外层 catch 走 fail-closed 阻断
|
|
646
714
|
//(gate 自身写 run 目录失败不能静默放行完成)。
|
|
647
715
|
mkdirSync(join(runtimeRoot, 'execute-runs', executeRunId, 'tasks'), { recursive: true })
|
|
648
|
-
|
|
716
|
+
writeAtomicSync(runIdFile, executeRunId + '\n')
|
|
649
717
|
stampExecuteRunChange(runtimeRoot, executeRunId, changeName)
|
|
650
718
|
}
|
|
651
719
|
}
|
|
@@ -753,7 +821,7 @@ export function validateMetadata(cwd, stageName, specBase) {
|
|
|
753
821
|
const unique = [...new Set(missing)]
|
|
754
822
|
if (unique.length > 0) {
|
|
755
823
|
console.log(`\n⚠️ 以下文件缺少 author 或 created_at 元数据:`)
|
|
756
|
-
unique.forEach(f => console.log(` - ${
|
|
824
|
+
unique.forEach(f => console.log(` - ${relative(cwd, f) || f}`))
|
|
757
825
|
console.log('请在文件头部添加 author(git 用户名)和 created_at(精确到秒)')
|
|
758
826
|
}
|
|
759
827
|
}
|
|
@@ -794,13 +862,14 @@ export function validateFileLocations(cwd, stageName, progress, changeName, spec
|
|
|
794
862
|
|
|
795
863
|
if (missing.length > 0) {
|
|
796
864
|
console.log(`\n⚠️ 文件位置验证:以下文件未在变更目录中找到`)
|
|
797
|
-
|
|
865
|
+
// relative(cwd, x):Windows 下 join() 产物是反斜杠绝对路径,正斜杠拼接的 cwd 前缀裁剪永不命中
|
|
866
|
+
console.log(` 变更目录:${relative(cwd, changeDir) || changeDir}/`)
|
|
798
867
|
for (const f of missing) {
|
|
799
868
|
// 检查是否写到了错误的位置
|
|
800
869
|
const wrongPath = join(specBase, 'changes', 'change', effectiveChange, f)
|
|
801
870
|
if (existsSync(wrongPath)) {
|
|
802
|
-
console.log(` ❌ ${f} — 不存在,但发现了错误路径:${
|
|
803
|
-
console.log(` 提示:应该写入 ${
|
|
871
|
+
console.log(` ❌ ${f} — 不存在,但发现了错误路径:${relative(cwd, wrongPath) || wrongPath}`)
|
|
872
|
+
console.log(` 提示:应该写入 ${relative(cwd, changeDir) || changeDir}/${f}`)
|
|
804
873
|
} else {
|
|
805
874
|
console.log(` ⬜ ${f} — 未找到(该阶段可能未产出此文件)`)
|
|
806
875
|
}
|
package/src/run/prompt.js
CHANGED
|
@@ -510,6 +510,21 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
510
510
|
}
|
|
511
511
|
}
|
|
512
512
|
|
|
513
|
+
// 模块卡分级表注入(token 成本优化 P0a,2026-08-22-token-cost-optimization):execute
|
|
514
|
+
// 「加载上下文」步 {MODULE_RESOLVE_TABLE} → resolveChangeModuleCards(tasks 卡 allowed_paths
|
|
515
|
+
// × 全部 _module-map.yaml 跨层最长前缀匹配,子项目细卡优先于根层大卡)。全降级不抛
|
|
516
|
+
// (异常 → 单行说明),无 map 由渲染函数给出跳过提示——占位符恒被替换。
|
|
517
|
+
if (stageName === 'execute' && promptText.includes('{MODULE_RESOLVE_TABLE}')) {
|
|
518
|
+
try {
|
|
519
|
+
const { renderModuleResolveTable } = await import('../module-resolve.js')
|
|
520
|
+
const mrSpecBase = resolvePromptSpecBase(platformOpts, cwd)
|
|
521
|
+
const table = renderModuleResolveTable({ cwd, specBase: mrSpecBase, changeName })
|
|
522
|
+
promptText = promptText.replace(/\{MODULE_RESOLVE_TABLE\}/g, table)
|
|
523
|
+
} catch (e) {
|
|
524
|
+
promptText = promptText.replace(/\{MODULE_RESOLVE_TABLE\}/g, `[modules-resolve] 模块卡分级解析异常(${e.message}),跳过——可手跑 sillyspec modules resolve --change <变更名>`)
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
513
528
|
// Execute: 注入 currentExecuteRunId(从变更专属标记文件读取)
|
|
514
529
|
if (stageName === 'execute' && promptText.includes('{EXECUTE_RUN_ID}')) {
|
|
515
530
|
let runId = ''
|
|
@@ -782,6 +797,5 @@ export function applyRootPlaceholders(text, roots) {
|
|
|
782
797
|
.replaceAll('{DOCS_ROOT}', roots.docsRoot)
|
|
783
798
|
.replaceAll('{PROJECTS_ROOT}', roots.projectsRoot)
|
|
784
799
|
.replaceAll('{WORKFLOWS_ROOT}', roots.workflowsRoot)
|
|
785
|
-
.replaceAll('{KNOWLEDGE_ROOT}', roots.knowledgeRoot)
|
|
786
|
-
}
|
|
800
|
+
.replaceAll('{KNOWLEDGE_ROOT}', roots.knowledgeRoot)}
|
|
787
801
|
|
package/src/run/quick-audit.js
CHANGED
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
* 路径修正(相对 src/run/):
|
|
8
8
|
* - resolveQuickLinkedChanges 的 quick-recommend.js 动态 import './quick-recommend.js' → '../quick-recommend.js'
|
|
9
9
|
* - 脏文件信号改用 safeGit(带 safe.directory,Q3);不再 import execSync
|
|
10
|
-
* - parsePorcelainPath 从 ./shared.js 重 import;checkbox
|
|
10
|
+
* - parsePorcelainPath 从 ./shared.js 重 import;checkbox 改交互分支内动态 import——
|
|
11
|
+
* 本模块被 command.js 静态 import,顶部静态拉 @inquirer/prompts 会进**每条** run 命令
|
|
12
|
+
* 启动路径(实测冷加载 100-150ms),而 checkbox 仅"≥2 活跃变更 + TTY"分支用到
|
|
11
13
|
*/
|
|
12
|
-
import { checkbox } from '@inquirer/prompts'
|
|
13
14
|
import { parsePorcelainPath, safeGit } from './shared.js'
|
|
14
15
|
|
|
15
16
|
/**
|
|
@@ -150,6 +151,8 @@ export async function resolveQuickLinkedChanges({ pm, cwd, specDir, quickFiles,
|
|
|
150
151
|
if (recommendedSet.size > 0) {
|
|
151
152
|
console.log(' ⭐ = 基于脏文件/任务描述推荐,已默认勾选')
|
|
152
153
|
}
|
|
154
|
+
// 动态 import:@inquirer/prompts 只在本交互分支加载,不进 run 命令通用启动路径
|
|
155
|
+
const { checkbox } = await import('@inquirer/prompts')
|
|
153
156
|
const selected = await checkbox({ message: '关联变更(空格切换,回车确认)', choices })
|
|
154
157
|
return selected
|
|
155
158
|
}
|
package/src/run/shared.js
CHANGED
|
@@ -17,9 +17,10 @@ import os from 'node:os'
|
|
|
17
17
|
|
|
18
18
|
// safeGit 收口至 src/git-helper.js:import 建本地词法绑定(本模块内部 L128/130/431 调用)+
|
|
19
19
|
// re-export 供 run/ 层现有调用方继续从 shared.js 引用(pure re-export 不建本地绑定,会致内部 ReferenceError)。
|
|
20
|
-
|
|
20
|
+
// unquoteGitPath 同理:parsePorcelainPath 内部消费 + re-export。
|
|
21
|
+
import { safeGit, unquoteGitPath } from '../git-helper.js'
|
|
21
22
|
import { createHash } from 'node:crypto'
|
|
22
|
-
export { safeGit }
|
|
23
|
+
export { safeGit, unquoteGitPath }
|
|
23
24
|
|
|
24
25
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
25
26
|
|
|
@@ -133,23 +134,41 @@ export function resolveSpecDir(cwd, opts = {}) {
|
|
|
133
134
|
* 被 countAncestorSpecDirs(漂移提醒 warn)与 locateQuickSessionGuard(quick 漂移 fail-fast
|
|
134
135
|
* 守卫,坑 quick-cwd-drift-splits-specdir)共用 —— 单一真相源,避免两处各自重写祖先枚举漂移。
|
|
135
136
|
*/
|
|
137
|
+
// 祖先链 ceiling(git 主仓根)按 cwd 进程级缓存:每条 stage 命令必经(countAncestorSpecDirs
|
|
138
|
+
// 漂移提醒 + quick 漂移守卫两条路径),此前固定 2 次串行 git spawn(Windows 60-200ms)。
|
|
139
|
+
// 只缓存"git root 已找到"的结果——非 git 仓库不缓存,防测试流程中途 git init 后读到陈旧 null。
|
|
140
|
+
const _ancestorCeilingCache = new Map()
|
|
141
|
+
|
|
142
|
+
function resolveAncestorCeiling(resolved) {
|
|
143
|
+
if (_ancestorCeilingCache.has(resolved)) return _ancestorCeilingCache.get(resolved)
|
|
144
|
+
// 一次 spawn 拿两值(rev-parse 按参数序输出,每值一行)
|
|
145
|
+
const out = safeGit(resolved, ['rev-parse', '--show-toplevel', '--git-common-dir']).value
|
|
146
|
+
let ceiling = null
|
|
147
|
+
if (out) {
|
|
148
|
+
const lines = out.split('\n')
|
|
149
|
+
const topLevel = (lines[0] || '').trim()
|
|
150
|
+
if (topLevel) ceiling = resolve(topLevel)
|
|
151
|
+
const commonDir = (lines[1] || '').trim()
|
|
152
|
+
if (commonDir) {
|
|
153
|
+
const absCommonDir = resolve(resolved, commonDir)
|
|
154
|
+
if (existsSync(absCommonDir) && statSync(absCommonDir).isDirectory()) {
|
|
155
|
+
const mainRoot = dirname(absCommonDir)
|
|
156
|
+
// 主仓根是 worktree 根的祖先时取它(更靠上);monorepo/单仓两者相等不替换,行为不变
|
|
157
|
+
if (!ceiling || resolve(ceiling).startsWith(resolve(mainRoot) + sep)) ceiling = mainRoot
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (ceiling) _ancestorCeilingCache.set(resolved, ceiling)
|
|
162
|
+
return ceiling
|
|
163
|
+
}
|
|
164
|
+
|
|
136
165
|
export function ancestorSpecDirs(cwd) {
|
|
137
166
|
const resolved = resolve(cwd)
|
|
138
167
|
// 上界 = git root。但 linked worktree 内 --show-toplevel 返回 worktree 根(非主仓根),
|
|
139
168
|
// 祖先链到不了主仓 .sillyspec → 漂移提醒/quick 守卫全哑(坑 worktree-execute-spec-drift)。
|
|
140
169
|
// 复刻 worktree.js _resolveMainRepoRoot:--git-common-dir 的 dirname 才是主仓根,取更靠上者作 ceiling。
|
|
141
170
|
// (git 可能返回相对路径,须 resolve(cwd, commonDir) 绝对化,否则相对 process.cwd 误解析。)
|
|
142
|
-
const
|
|
143
|
-
let ceiling = topLevel ? resolve(topLevel) : null
|
|
144
|
-
const commonDir = safeGit(resolved, ['rev-parse', '--git-common-dir']).value
|
|
145
|
-
if (commonDir) {
|
|
146
|
-
const absCommonDir = resolve(resolved, commonDir)
|
|
147
|
-
if (existsSync(absCommonDir) && statSync(absCommonDir).isDirectory()) {
|
|
148
|
-
const mainRoot = dirname(absCommonDir)
|
|
149
|
-
// 主仓根是 worktree 根的祖先时取它(更靠上);monorepo/单仓两者相等不替换,行为不变
|
|
150
|
-
if (!ceiling || resolve(ceiling).startsWith(resolve(mainRoot) + sep)) ceiling = mainRoot
|
|
151
|
-
}
|
|
152
|
-
}
|
|
171
|
+
const ceiling = resolveAncestorCeiling(resolved)
|
|
153
172
|
const dirs = []
|
|
154
173
|
let dir = resolved
|
|
155
174
|
while (true) {
|
|
@@ -499,6 +518,12 @@ export async function triggerSync(cwd, changeName, platformOpts = {}, opts = {})
|
|
|
499
518
|
*/
|
|
500
519
|
const AUTO_PULL_THROTTLE_MS = 10_000
|
|
501
520
|
|
|
521
|
+
// 自动 pull 专属熔断(2026-08-21 性能审查 PERF-04):自动注入点(stage 命令分发前 / approve 前)
|
|
522
|
+
// 在打印任何输出前 await,daemon 慢/挂时每 10s 窗口首条命令硬等上限即体感 hang——降为 2s。
|
|
523
|
+
// 手动 `platform pull` 不经此路径(用户显式发起,等待 8s 合理)。超时语义与 8s 版一致:
|
|
524
|
+
// abort 在飞请求(HUB-09),错过窗口的平台更新由下一条 >10s 命令或 push 409 自愈兜底。
|
|
525
|
+
const AUTO_PULL_TIMEOUT_MS = 2_000
|
|
526
|
+
|
|
502
527
|
function _autoPullThrottlePath(cwd) {
|
|
503
528
|
return join(cwd, '.sillyspec', '.runtime', 'auto-pull-throttle.json')
|
|
504
529
|
}
|
|
@@ -521,7 +546,7 @@ function _stampAutoPull(cwd) {
|
|
|
521
546
|
|
|
522
547
|
/**
|
|
523
548
|
* 触发 pull(下行同步,task-10 / D-009 / FR-04 / FR-06)。
|
|
524
|
-
*
|
|
549
|
+
* 熔断走 AUTO_PULL_TIMEOUT_MS(2s,见常量注释)与 Best Effort 语义;未连接平台静默跳过(与现状一致)。
|
|
525
550
|
* 注入时机:stage 命令启动(顶层别名 + case 'run',ql-20260818-008 补齐后者)+ 关键决策点
|
|
526
551
|
* (approve 前)+ 手动 platform pull。不在每步 pull(避免高频写入与网络压力),仅低频边界点。
|
|
527
552
|
* 自动注入走 skipIfLocalDirty 保守守卫:本地有未同步改动时 pull 内部跳过 import,
|
|
@@ -541,7 +566,7 @@ export async function triggerPull(cwd, changeName, platformOpts = {}, opts = {})
|
|
|
541
566
|
if (!sm._getPlatform()) return
|
|
542
567
|
if (_autoPullRecently(cwd)) return
|
|
543
568
|
_stampAutoPull(cwd)
|
|
544
|
-
await raceWithAbort((sig) => sm.pull(changeName, { skipIfLocalDirty: true, signal: sig.signal }), opts.timeoutMs)
|
|
569
|
+
await raceWithAbort((sig) => sm.pull(changeName, { skipIfLocalDirty: true, signal: sig.signal }), opts.timeoutMs ?? AUTO_PULL_TIMEOUT_MS)
|
|
545
570
|
} catch (e) {
|
|
546
571
|
// pull 失败静默跳过(Best Effort,失败不影响正确性)
|
|
547
572
|
console.warn('⚠️ 拉取失败:', e.message)
|
|
@@ -578,10 +603,10 @@ export async function triggerPullActiveChange(cwd, platformOpts = {}) {
|
|
|
578
603
|
// progress 不可达则跳过(Best Effort)
|
|
579
604
|
}
|
|
580
605
|
if (!cn) return
|
|
581
|
-
// 已确认连接 + 单活跃变更,调 pull
|
|
606
|
+
// 已确认连接 + 单活跃变更,调 pull(自动注入 2s 熔断 + abort(HUB-09);skipIfLocalDirty 保守守卫
|
|
582
607
|
// 同 triggerPull——本地脏时跳过 import,防平台旧快照覆盖本地领先进度,ql-20260818-008)
|
|
583
608
|
try {
|
|
584
|
-
await raceWithAbort((sig) => sm.pull(cn, { skipIfLocalDirty: true, signal: sig.signal }))
|
|
609
|
+
await raceWithAbort((sig) => sm.pull(cn, { skipIfLocalDirty: true, signal: sig.signal }), AUTO_PULL_TIMEOUT_MS)
|
|
585
610
|
} catch (e) {
|
|
586
611
|
console.warn('⚠️ 拉取失败:', e.message)
|
|
587
612
|
}
|
|
@@ -639,7 +664,10 @@ export function parsePorcelainPath(line) {
|
|
|
639
664
|
if (!line) return ''
|
|
640
665
|
let path = line.slice(3).trim()
|
|
641
666
|
if (path.length >= 2 && path.startsWith('"') && path.endsWith('"')) {
|
|
642
|
-
|
|
667
|
+
// unquoteGitPath:\\ \" \n \t 与 \NNN 八进制字节聚合 UTF-8 解码——原 \\(.) 逐字符
|
|
668
|
+
// 解引会把 "\346\226\207.md" 拆成 "346226207.md"。git-helper 已统一 core.quotepath=false
|
|
669
|
+
// (非 ASCII 路径裸输出),此处兜底仍会被引号转义的路径(含控制字符等)。
|
|
670
|
+
path = unquoteGitPath(path.slice(1, -1))
|
|
643
671
|
}
|
|
644
672
|
const arrow = path.indexOf(' -> ')
|
|
645
673
|
if (arrow !== -1) path = path.slice(arrow + 4)
|
package/src/run/stage.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* - 'child_process'(execSync)裸模块名不变
|
|
18
18
|
*/
|
|
19
19
|
import { join, dirname } from 'node:path'
|
|
20
|
-
import { existsSync, readdirSync, readFileSync,
|
|
20
|
+
import { existsSync, readdirSync, readFileSync, mkdirSync } from 'node:fs'
|
|
21
21
|
import { writeAtomicSync } from '../fs-atomic.js'
|
|
22
22
|
import { resolveSpecDir, resolveChangeDir, resolveRuntimeRoot, resolveQuickSessionsDir, triggerSync, safeGit, parsePorcelainPath, formatWaitOptions, checkApproval, getStageSteps, warnApprovalUnknown } from './shared.js'
|
|
23
23
|
import { computeScanProfile, applyScanProfileSteps, executeScanPreflight, executeScanPostcheck } from './scan-profile.js'
|
|
@@ -122,7 +122,7 @@ export async function runStage(pm, progress, stageName, cwd, changeName, skipApp
|
|
|
122
122
|
throw new Error(`execute run 目录创建失败(${join(runtimeRoot, 'execute-runs', currentExecuteRunId)}): ${e.message};` +
|
|
123
123
|
`请检查该路径是否为普通文件/只读,清理后重跑(sillyspec run execute --change ${changeName} --skip-approval)`)
|
|
124
124
|
}
|
|
125
|
-
|
|
125
|
+
writeAtomicSync(runIdFile, currentExecuteRunId + '\n')
|
|
126
126
|
// change 归属戳(坑 worktree-cleanup-marker-chain 根治):run 目录自带变更身份,
|
|
127
127
|
// marker 断裂(worktree cleanup / 归档清理 / 并行误删)后按戳归属,不再错配其他变更的 run
|
|
128
128
|
const { stampExecuteRunChange } = await import('../task-review.js')
|
|
@@ -130,6 +130,38 @@ export async function runStage(pm, progress, stageName, cwd, changeName, skipApp
|
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
// ── 跨变更文件冲突预警(坑 cross-change-conflict-no-warning,2026-08-22 实证:并行变更改
|
|
134
|
+
// 同一文件靠人工发现——f578a08e 的「锚点更新」是好实践但工具无预警)。execute 启动时把本变更
|
|
135
|
+
// 的 diff 文件集与其他活跃变更的 diff 文件集求交集,advisory warn 交集与对端变更名 + 锚点
|
|
136
|
+
// 更新提示。best-effort:任何异常静默跳过(预警不阻断 execute 启动)。
|
|
137
|
+
if (stageName === 'execute' && changeName) {
|
|
138
|
+
try {
|
|
139
|
+
const { resolveVerifyChangedFiles } = await import('../verify-postcheck.js')
|
|
140
|
+
const activeChanges = (pm.listChanges(cwd) || []).filter(c => c && c !== changeName)
|
|
141
|
+
if (activeChanges.length > 0) {
|
|
142
|
+
const myFiles = resolveVerifyChangedFiles(cwd, changeName, null, { includeWorkingTree: true }) || []
|
|
143
|
+
const mySet = new Set(myFiles.map(f => f.replace(/\\/g, '/')).filter(f => !f.startsWith('.sillyspec/')))
|
|
144
|
+
if (mySet.size > 0) {
|
|
145
|
+
const conflicts = []
|
|
146
|
+
for (const other of activeChanges) {
|
|
147
|
+
const otherFiles = resolveVerifyChangedFiles(cwd, other, null, { includeWorkingTree: true }) || []
|
|
148
|
+
const overlap = [...new Set(otherFiles.map(f => f.replace(/\\/g, '/')))]
|
|
149
|
+
.filter(f => mySet.has(f))
|
|
150
|
+
if (overlap.length > 0) conflicts.push({ other, overlap })
|
|
151
|
+
}
|
|
152
|
+
if (conflicts.length > 0) {
|
|
153
|
+
console.warn(`⚠️ 跨变更文件冲突预警:本变更(${changeName}})与其他活跃变更改动重叠——`)
|
|
154
|
+
for (const c of conflicts) {
|
|
155
|
+
console.warn(` 与 ${c.other} 重叠 ${c.overlap.length} 个文件:${c.overlap.slice(0, 5).join(', ')}${c.overlap.length > 5 ? ` …(共 ${c.overlap.length})` : ''}`)
|
|
156
|
+
}
|
|
157
|
+
console.warn(` 并行 apply/归档时重叠文件会互相覆盖或冲突。实践:后完成方在其 verify/design 里记录「锚点更新」`)
|
|
158
|
+
console.warn(` (说明基于对方已合入的版本重新锚定),并考虑串行化这两个变更的 apply。`)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
} catch { /* 预警失败不阻断 execute */ }
|
|
163
|
+
}
|
|
164
|
+
|
|
133
165
|
// 自动探测 currentChange
|
|
134
166
|
if (autoDetectChange(progress, cwd)) {
|
|
135
167
|
progress.lastActive = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
@@ -207,7 +239,9 @@ export async function runStage(pm, progress, stageName, cwd, changeName, skipApp
|
|
|
207
239
|
}
|
|
208
240
|
const guardFile = join(specBase, '.runtime', 'scan-guard.json')
|
|
209
241
|
mkdirSync(dirname(guardFile), { recursive: true })
|
|
210
|
-
|
|
242
|
+
// 原子写:hook 进程(worktree-guard 的 readScanGuard)并发读,裸 truncate→write 的
|
|
243
|
+
// 半截窗口会被 JSON.parse 吞成 null → 覆盖保护静默失效(fail-open)
|
|
244
|
+
writeAtomicSync(guardFile, JSON.stringify(scanGuard, null, 2) + '\n')
|
|
211
245
|
if (scanGuard.forceRescan) {
|
|
212
246
|
console.log('🛡️ scan 覆盖保护已记录: --force-rescan 已开启')
|
|
213
247
|
} else {
|
package/src/stage-contract.js
CHANGED
|
@@ -480,7 +480,20 @@ function validateVerifyOutputs(cwd, changeName, context = {}) {
|
|
|
480
480
|
warnings.push(`[${changeRiskProfile.level}] 结论 PASS WITH NOTES:design frontmatter 显式声明 risk_level=${changeRiskProfile.level},残留项须为真实集成证据缺口,并在 verify-result.md 如实说明`)
|
|
481
481
|
}
|
|
482
482
|
if (requiresEvidence) {
|
|
483
|
-
|
|
483
|
+
// CLI 回执注入(坑 verify-literal-evidence-mismatch,2026-08-22 实证:证据第一轮就齐
|
|
484
|
+
// 但自然措辞不含字面词被误拦三轮):verify 服务回收器落的回执 = 真实启动+PID 登记
|
|
485
|
+
// 的结构化证据,作为附加匹配文本——有回执不依赖 agent 措辞;无回执维持原字面匹配
|
|
486
|
+
let receiptText = ''
|
|
487
|
+
try {
|
|
488
|
+
const receiptPath = join(cwd, '.sillyspec', '.runtime', 'verify-services.receipt.json')
|
|
489
|
+
if (existsSync(receiptPath)) {
|
|
490
|
+
const receipt = JSON.parse(readFileSync(receiptPath, 'utf8'))
|
|
491
|
+
if (receipt && (receipt.reapedPidCount || 0) > 0 && (!receipt.change || receipt.change === changeName)) {
|
|
492
|
+
receiptText = `CLI 回执:verify 服务进程已回收 ${receipt.reapedPidCount} 个(PID 已登记,真实启动,运行时证据 reapedAt=${receipt.reapedAt})`
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
} catch { /* 回执损坏按无回执处理(fail 回原字面匹配) */ }
|
|
496
|
+
const evidenceCheck = checkIntegrationEvidence(verify, changeRiskProfile.requiredVerification, { extraEvidenceText: receiptText })
|
|
484
497
|
if (!evidenceCheck.ok) {
|
|
485
498
|
// A: 报错说人话 —— 把「缺哪一项、要写/做什么、判级原因」逐条列出,
|
|
486
499
|
// 让 agent 不必靠改结论文案撞墙。detail 指明真实启动须是本变更实际改动的
|
package/src/stages/archive.js
CHANGED
|
@@ -82,12 +82,14 @@ export const definition = {
|
|
|
82
82
|
3. 如果标记缺失或重复 → 在 _module-map.yaml 中标记 needs_review: true
|
|
83
83
|
|
|
84
84
|
#### 新建模块卡片模板
|
|
85
|
-
一级标题格式 \`# <中文名>(<module-id>)\`——中文名从模块职责提炼 2-8 字,括号内保留 module-id 原样(sillyhub 平台解析识别用,与 scan 生成约定一致)。
|
|
85
|
+
一级标题格式 \`# <中文名>(<module-id>)\`——中文名从模块职责提炼 2-8 字,括号内保留 module-id 原样(sillyhub 平台解析识别用,与 scan 生成约定一致)。frontmatter 已带 author/created_at 真值(<git-user>/<now-datetime> 占位符由 CLI 每步替换),照抄即过元数据校验、勿删。
|
|
86
86
|
\`\`\`markdown
|
|
87
87
|
---
|
|
88
88
|
schema_version: 1
|
|
89
89
|
doc_type: module-card
|
|
90
90
|
module_id: <module-id>
|
|
91
|
+
author: <git-user>
|
|
92
|
+
created_at: <now-datetime>
|
|
91
93
|
---
|
|
92
94
|
|
|
93
95
|
# <中文名>(<module-id>)
|