sillyspec 3.26.14 → 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-archive/SKILL.md +2 -0
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +1 -1
- package/.claude/skills/sillyspec-commit/SKILL.md +18 -60
- package/.claude/skills/sillyspec-continue/SKILL.md +2 -0
- package/.claude/skills/sillyspec-execute/SKILL.md +10 -4
- package/.claude/skills/sillyspec-explore/SKILL.md +10 -3
- package/.claude/skills/sillyspec-plan/SKILL.md +10 -2
- package/.claude/skills/sillyspec-quick/SKILL.md +2 -2
- package/.claude/skills/sillyspec-resume/SKILL.md +2 -0
- package/.claude/skills/sillyspec-verify/SKILL.md +4 -0
- package/.claude/skills/sillyspec-workspace/SKILL.md +7 -1
- 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/commit-suggest.js +202 -0
- package/src/config-cat.js +88 -0
- package/src/db.js +14 -3
- package/src/docs-check.js +598 -534
- package/src/doctor-diagnostics.js +102 -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 +42 -4
- package/src/index.js +557 -12
- package/src/init.js +1 -1
- package/src/local-register.js +98 -0
- package/src/module-impact.js +166 -0
- package/src/module-resolve.js +211 -0
- package/src/modules.js +82 -2
- package/src/progress/change-registry.js +43 -4
- package/src/progress/stage-machine.js +10 -5
- package/src/progress/step-store.js +12 -8
- package/src/progress.js +19 -3
- package/src/quicklog.js +106 -1
- package/src/run/command.js +95 -1
- package/src/run/complete-handlers.js +73 -5
- package/src/run/complete.js +47 -14
- package/src/run/gates.js +144 -17
- package/src/run/multi-repo-context.js +3 -1
- package/src/run/next.js +128 -0
- package/src/run/prompt.js +36 -4
- package/src/run/quick-audit.js +5 -2
- package/src/run/shared.js +48 -20
- package/src/run/stage.js +37 -3
- package/src/scan-postcheck.js +142 -2
- package/src/stage-contract-spec.js +1 -1
- package/src/stage-contract.js +14 -1
- package/src/stage-review.js +15 -6
- package/src/stages/archive.js +4 -2
- package/src/stages/brainstorm.js +1 -0
- package/src/stages/execute.js +275 -44
- package/src/stages/explore.js +11 -2
- package/src/stages/knowledge.js +3 -2
- package/src/stages/quick.js +18 -9
- package/src/stages/scan.js +1 -0
- package/src/stages/verify.js +16 -51
- package/src/sync.js +44 -3
- package/src/task-review.js +273 -11
- package/src/taskcard.js +52 -22
- package/src/verify-postcheck.js +223 -6
- package/src/verify-probes.js +264 -0
- package/src/workspace.js +125 -0
- package/src/worktree-apply.js +43 -11
- package/src/worktree-deps.js +59 -5
- package/src/worktree.js +128 -14
- package/templates/claude-instruction.md +1 -1
- package/templates/prompts/taskcard-rules.md +1 -1
- package/templates/prompts/verify-probes.md +13 -47
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'
|
|
@@ -65,12 +67,55 @@ export function validateSymbolImpactReport({ reportContent, planContent }) {
|
|
|
65
67
|
for (const id of missing) {
|
|
66
68
|
errors.push(`${id} 未在 symbol-impact.md 中出现——每个 task 都要有一行结论(含「无签名级变更」的显式声明)`)
|
|
67
69
|
}
|
|
70
|
+
// 防骨架直接过门(2026-08-21 agent-手工产出审计项⑤):CLI 会代生成逐 task TODO 骨架
|
|
71
|
+
//(generateSymbolImpactSkeleton),占位未替换即拦——骨架消灭的是「从零手写/漏 task」错误面,
|
|
72
|
+
// 不是把「逐 task 结论」本身变成走过场。
|
|
73
|
+
const report = String(reportContent)
|
|
74
|
+
const todoPending = taskIds.filter(id => new RegExp(`^[-*]\\s*${id}[^\\n]*<!--TODO-->`, 'm').test(report))
|
|
75
|
+
for (const id of todoPending) {
|
|
76
|
+
errors.push(`${id} 的结论仍是骨架 <!--TODO--> 占位——替换为真实结论(无签名级变更也显式写「无」)`)
|
|
77
|
+
}
|
|
68
78
|
return { ok: errors.length === 0, errors }
|
|
69
79
|
}
|
|
70
80
|
|
|
81
|
+
/**
|
|
82
|
+
* 生成 symbol-impact.md 逐 task TODO 骨架(2026-08-21 审计项⑤「报错即生成」)。
|
|
83
|
+
*
|
|
84
|
+
* gate 此前只报错不代填 → agent 从零手写整份报告,忘一个 task 就被拦、再补再撞。
|
|
85
|
+
* 骨架从 tasks.md 注册表生成逐 task 占位行,agent 只需逐行填结论;占位 <!--TODO-->
|
|
86
|
+
* 由 validateSymbolImpactReport 拒绝,骨架不能直接过门(防偷懒)。
|
|
87
|
+
*
|
|
88
|
+
* @param {string} tasksContent - 任务注册表(tasks.md,回退 plan.md)内容
|
|
89
|
+
* @returns {string|null} 骨架全文(LF);注册表无 task 行 → null
|
|
90
|
+
*/
|
|
91
|
+
export function generateSymbolImpactSkeleton(tasksContent) {
|
|
92
|
+
const taskIds = extractTaskIdsFromRegistry(tasksContent)
|
|
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 { /* 指纹失败不阻断骨架生成 */ }
|
|
101
|
+
const lines = [
|
|
102
|
+
'# 符号影响面报告',
|
|
103
|
+
'',
|
|
104
|
+
`> tasks.md 内容指纹(生成时): ${fingerprint}——重入本步时若与当前 tasks.md 指纹一致且结论已填全,直接沿用不重做扫描。`,
|
|
105
|
+
'> 骨架由 CLI 生成(`sillyspec symbol-impact --change <变更名>`,gate 失败时也会自动落一份)。',
|
|
106
|
+
'> 逐行把 `<!--TODO-->` 替换为真实结论:涉及签名级变更(构造函数参数/接口/DTO/方法签名增删改)',
|
|
107
|
+
'> 写变更类型 + 受影响调用点 + 是否在任务范围内;无签名级变更也要显式写「无签名级变更」。',
|
|
108
|
+
'> **gate 拒绝仍含 <!--TODO--> 的行**——骨架不能直接过门。',
|
|
109
|
+
'',
|
|
110
|
+
]
|
|
111
|
+
for (const id of taskIds) lines.push(`- ${id}: <!--TODO-->`)
|
|
112
|
+
return lines.join('\n') + '\n'
|
|
113
|
+
}
|
|
114
|
+
|
|
71
115
|
/**
|
|
72
116
|
* 符号影响面报告硬门:execute「加载上下文」步 --done 时校验 symbol-impact.md 落盘 + task 覆盖度。
|
|
73
|
-
* fail-closed:不通过 →
|
|
117
|
+
* fail-closed:不通过 → exit 1(进度不推进;步骤状态保持 pending,无 steps 句柄不标 blocked)。
|
|
118
|
+
* 非目标步骤/阶段直接放行。
|
|
74
119
|
*/
|
|
75
120
|
export async function enforceSymbolImpactGate(stageName, changeName, currentStepName, specBase) {
|
|
76
121
|
if (stageName !== 'execute') return
|
|
@@ -88,7 +133,19 @@ export async function enforceSymbolImpactGate(stageName, changeName, currentStep
|
|
|
88
133
|
console.error(` 「加载上下文」步的符号影响面检查报告未落盘或不完整:`)
|
|
89
134
|
console.error(` 期望路径:${reportPath}`)
|
|
90
135
|
for (const e of result.errors) console.error(` • ${e}`)
|
|
91
|
-
|
|
136
|
+
// 报错即生成(2026-08-21 审计项⑤):报告缺失时自动落一份逐 task TODO 骨架,agent 从
|
|
137
|
+
// 「从零手写整份」变「逐行填结论」;TODO 占位由 validate 拒绝,骨架不能直接过门。
|
|
138
|
+
let skeletonNote = ''
|
|
139
|
+
if (!existsSync(reportPath)) {
|
|
140
|
+
const skeleton = generateSymbolImpactSkeleton(tasksContent)
|
|
141
|
+
if (skeleton) {
|
|
142
|
+
try {
|
|
143
|
+
writeAtomicSync(reportPath, skeleton)
|
|
144
|
+
skeletonNote = `\n 📄 已代生成逐 task 骨架:${reportPath}(逐行替换 <!--TODO--> 为结论,无签名级变更也显式写「无」)`
|
|
145
|
+
} catch { /* 写失败只影响提示,不掩盖原始错误 */ }
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
console.error(` 修复:按「加载上下文」步操作 11 完成符号影响面扫描,逐 task 写结论后重跑 execute --done。${skeletonNote}`)
|
|
92
149
|
process.exit(1)
|
|
93
150
|
}
|
|
94
151
|
|
|
@@ -195,7 +252,7 @@ export async function enforceAlignExecuteReviewGate({ cwd, changeName, specBase,
|
|
|
195
252
|
}
|
|
196
253
|
const searchDirs = [effectiveSpecBase, reviewChangeDir, cwd].filter(Boolean)
|
|
197
254
|
const reviewResult = validateStageReview({ stage: 'execute', reviewType: 'acceptance', runtimeRoot, reviewRunId, searchDirs })
|
|
198
|
-
printStageReviewResult(reviewResult, { stage: 'execute', reviewRunId, runtimeRoot })
|
|
255
|
+
printStageReviewResult(reviewResult, { stage: 'execute', reviewRunId, runtimeRoot, changeName })
|
|
199
256
|
if (!reviewResult.ok) return true // printStageReviewResult 已给明细,不再重复列
|
|
200
257
|
}
|
|
201
258
|
} catch (e) {
|
|
@@ -240,9 +297,10 @@ export async function enforceAlignExecuteReviewGate({ cwd, changeName, specBase,
|
|
|
240
297
|
/**
|
|
241
298
|
* execute deps 验证硬门(change 2026-06-28-worktree-deps-provision / D-001@v1, D-003@v1, D-006@v2)。
|
|
242
299
|
* depsStatus 不达标且非 wave 级 opt-out 时阻断 --done:置 step=blocked + exit(1),与 requiresWait 同范式。
|
|
243
|
-
* 放行返回 true
|
|
300
|
+
* 放行返回 true;阻断时经 persist 回调落盘 blocked 后 exit(1)(此前只改内存不落库,
|
|
301
|
+
* progress show/doctor 看到的仍是 pending,与 docstring 承诺不符、误导诊断)。
|
|
244
302
|
*/
|
|
245
|
-
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) {
|
|
246
304
|
if (stageName !== 'execute') return true
|
|
247
305
|
let meta = null
|
|
248
306
|
let wm = null
|
|
@@ -278,6 +336,7 @@ export async function enforceDepsGate(stageName, cwd, changeName, step, steps, c
|
|
|
278
336
|
console.error(' 或在 worktree 内手动安装依赖后重试。')
|
|
279
337
|
if (meta?.depsError) console.error(` 上次供给错误:${meta.depsError}`)
|
|
280
338
|
}
|
|
339
|
+
if (persist) { try { await persist() } catch { /* 落盘失败不吞阻断语义 */ } }
|
|
281
340
|
process.exit(1)
|
|
282
341
|
}
|
|
283
342
|
|
|
@@ -287,11 +346,24 @@ export async function enforceDepsGate(stageName, cwd, changeName, step, steps, c
|
|
|
287
346
|
* Task Review Gate(validateTaskReviews)只在 execute 整阶段完成时跑(complete.js 阶段完成分支的
|
|
288
347
|
* actualCompleted===actualTotal 守卫),单 task --done 不校验 → 子代理勾 checkbox 却漏写/漏字段
|
|
289
348
|
* review.json,要到收尾才暴露,用户被迫事后批量补。本门提前到每次 --done:校验 plan 里所有已勾
|
|
290
|
-
* task 的 review.json,缺字段/不存在/JSON 坏 → 置 step=blocked + exit(1),与 enforceDepsGate
|
|
349
|
+
* task 的 review.json,缺字段/不存在/JSON 坏 → 置 step=blocked + exit(1),与 enforceDepsGate 同范式
|
|
350
|
+
* (阻断前经 persist 回调落盘 blocked)。
|
|
291
351
|
* 未勾 task 不校验(还没做)。平台模式/无 marker/无 plan 时放行(下游 Task Review Gate 兜底)。
|
|
292
352
|
*/
|
|
293
|
-
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) {
|
|
294
354
|
if (stageName !== 'execute' || !changeName) return true
|
|
355
|
+
// head 锡点自动落盘(2026-08-21 审计项③,D-010 补对称):跨仓 task base_commit 已在派发时
|
|
356
|
+
// CLI 落盘,head_commit 此前靠主 agent 按 prompt 手跑 rev-parse 手写(漏抄/抄错炸 review gate)。
|
|
357
|
+
// 每次 --done 时机幂等补齐(已存在不覆盖)。best-effort:失败只 warn,不阻断 --done 主流程。
|
|
358
|
+
try {
|
|
359
|
+
const { stampCrossRepoHeadCommits } = await import('../stages/execute.js')
|
|
360
|
+
const stamped = await stampCrossRepoHeadCommits({ changeName, cwd, specBase, platformOpts })
|
|
361
|
+
if (stamped && stamped.stamped > 0) {
|
|
362
|
+
console.log(`📌 已自动落盘跨仓 task head_commit 锡点:${stamped.stampedTasks.join(', ')}(幂等,此后 --done 不再重复写)`)
|
|
363
|
+
}
|
|
364
|
+
} catch (e) {
|
|
365
|
+
console.warn(`⚠️ head_commit 锡点自动落盘失败(降级留给 agent 手写): ${e.message}`)
|
|
366
|
+
}
|
|
295
367
|
const runtimeRoot = resolveRuntimeRoot(platformOpts, specBase)
|
|
296
368
|
const runIdFile = join(runtimeRoot, `current-execute-run-id-${changeName}`)
|
|
297
369
|
const planPath = join(specBase, 'changes', changeName, 'plan.md')
|
|
@@ -340,7 +412,8 @@ export async function enforceReviewJsonGate(stageName, cwd, changeName, step, st
|
|
|
340
412
|
console.error(` • ${f.taskId}(${kindLabel}): ${f.reviewPath}`)
|
|
341
413
|
for (const e of f.errors) console.error(` - ${e}`)
|
|
342
414
|
}
|
|
343
|
-
console.error('
|
|
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 { /* 落盘失败不吞阻断语义 */ } }
|
|
344
417
|
process.exit(1)
|
|
345
418
|
}
|
|
346
419
|
|
|
@@ -428,6 +501,14 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
428
501
|
console.error(' 请修复失败的测试并更新 verify-result.md 后重新完成此步骤。')
|
|
429
502
|
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
430
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)
|
|
431
512
|
// 契约 parity 对账:扫前端 API 调用 vs execute 提取的 provider endpoint artifact。
|
|
432
513
|
// 接线自 contract-matrix pipeline(verifyApiParity 的 CLI 入口)。
|
|
433
514
|
const { runVerifyParityCheck, printVerifyParityCheck } = await import('../verify-postcheck.js')
|
|
@@ -462,6 +543,31 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
462
543
|
}
|
|
463
544
|
}
|
|
464
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
|
+
|
|
465
571
|
console.log('\n✅ 验证通过,下一步:sillyspec run archive')
|
|
466
572
|
}
|
|
467
573
|
|
|
@@ -523,7 +629,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
523
629
|
reviewRunId = generateStageReviewRunId()
|
|
524
630
|
try {
|
|
525
631
|
mkdirSync(runtimeRoot, { recursive: true })
|
|
526
|
-
|
|
632
|
+
writeAtomicSync(stageReviewMarkerPath(runtimeRoot, stageName, changeName), reviewRunId + '\n')
|
|
527
633
|
} catch {}
|
|
528
634
|
}
|
|
529
635
|
const reviewType = stageName === 'brainstorm' ? 'design'
|
|
@@ -531,7 +637,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
531
637
|
: 'acceptance'
|
|
532
638
|
const searchDirs = [effectiveSpecBase, reviewChangeDir, cwd].filter(Boolean)
|
|
533
639
|
const reviewResult = validateStageReview({ stage: stageName, reviewType, runtimeRoot, reviewRunId, searchDirs })
|
|
534
|
-
printStageReviewResult(reviewResult, { stage: stageName, reviewRunId, runtimeRoot })
|
|
640
|
+
printStageReviewResult(reviewResult, { stage: stageName, reviewRunId, runtimeRoot, changeName })
|
|
535
641
|
if (!reviewResult.ok) {
|
|
536
642
|
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
537
643
|
}
|
|
@@ -543,6 +649,26 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
543
649
|
}
|
|
544
650
|
}
|
|
545
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
|
+
|
|
546
672
|
// ── Execute Task Review Gate:所有 task 必须有 review.json 且 verdict 通过 ──
|
|
547
673
|
if (stageName === 'execute') {
|
|
548
674
|
try {
|
|
@@ -587,7 +713,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
587
713
|
// marker 在则目录在)。不 try/catch——异常直穿外层 catch 走 fail-closed 阻断
|
|
588
714
|
//(gate 自身写 run 目录失败不能静默放行完成)。
|
|
589
715
|
mkdirSync(join(runtimeRoot, 'execute-runs', executeRunId, 'tasks'), { recursive: true })
|
|
590
|
-
|
|
716
|
+
writeAtomicSync(runIdFile, executeRunId + '\n')
|
|
591
717
|
stampExecuteRunChange(runtimeRoot, executeRunId, changeName)
|
|
592
718
|
}
|
|
593
719
|
}
|
|
@@ -695,7 +821,7 @@ export function validateMetadata(cwd, stageName, specBase) {
|
|
|
695
821
|
const unique = [...new Set(missing)]
|
|
696
822
|
if (unique.length > 0) {
|
|
697
823
|
console.log(`\n⚠️ 以下文件缺少 author 或 created_at 元数据:`)
|
|
698
|
-
unique.forEach(f => console.log(` - ${
|
|
824
|
+
unique.forEach(f => console.log(` - ${relative(cwd, f) || f}`))
|
|
699
825
|
console.log('请在文件头部添加 author(git 用户名)和 created_at(精确到秒)')
|
|
700
826
|
}
|
|
701
827
|
}
|
|
@@ -736,13 +862,14 @@ export function validateFileLocations(cwd, stageName, progress, changeName, spec
|
|
|
736
862
|
|
|
737
863
|
if (missing.length > 0) {
|
|
738
864
|
console.log(`\n⚠️ 文件位置验证:以下文件未在变更目录中找到`)
|
|
739
|
-
|
|
865
|
+
// relative(cwd, x):Windows 下 join() 产物是反斜杠绝对路径,正斜杠拼接的 cwd 前缀裁剪永不命中
|
|
866
|
+
console.log(` 变更目录:${relative(cwd, changeDir) || changeDir}/`)
|
|
740
867
|
for (const f of missing) {
|
|
741
868
|
// 检查是否写到了错误的位置
|
|
742
869
|
const wrongPath = join(specBase, 'changes', 'change', effectiveChange, f)
|
|
743
870
|
if (existsSync(wrongPath)) {
|
|
744
|
-
console.log(` ❌ ${f} — 不存在,但发现了错误路径:${
|
|
745
|
-
console.log(` 提示:应该写入 ${
|
|
871
|
+
console.log(` ❌ ${f} — 不存在,但发现了错误路径:${relative(cwd, wrongPath) || wrongPath}`)
|
|
872
|
+
console.log(` 提示:应该写入 ${relative(cwd, changeDir) || changeDir}/${f}`)
|
|
746
873
|
} else {
|
|
747
874
|
console.log(` ⬜ ${f} — 未找到(该阶段可能未产出此文件)`)
|
|
748
875
|
}
|
|
@@ -79,7 +79,9 @@ export class MultiRepoContext {
|
|
|
79
79
|
throw new Error(
|
|
80
80
|
`MultiRepoContext: 以下 repo 未在 local.yaml repos: 段注册:[${unregistered.join(', ')}]。` +
|
|
81
81
|
`当前已注册 repo:[${registered.join(', ')}]。` +
|
|
82
|
-
|
|
82
|
+
`一键注册(每个缺的 key 各跑一次,勿手编 YAML):` +
|
|
83
|
+
unregistered.map(k => `sillyspec local register-repo ${k} <${k} 仓根路径>`).join(';') +
|
|
84
|
+
`。补注册后再启动 execute(约束② fail-closed,跨仓 apply 走错仓=数据所有权事故)。`
|
|
83
85
|
);
|
|
84
86
|
}
|
|
85
87
|
|
package/src/run/next.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* next.js — `sillyspec next` 项目状态探测(吸收 continue/resume 两个 skill 的手工探测表,
|
|
3
|
+
* 2026-08-21 agent-手工产出审计第二批 G7/G8)。
|
|
4
|
+
*
|
|
5
|
+
* 两个 skill 各自维护一套「文件存在性状态机」推断表且内容重复,agent 每次手工跑
|
|
6
|
+
* ls/cat 探测再查表——纯机械探测是 CLI 的本职。本模块输出「状态描述 + 下一步命令 +
|
|
7
|
+
* 依据文件」,agent 按输出执行即可;skill 保留作兜底(CLI 不可用/输出异常时)。
|
|
8
|
+
*
|
|
9
|
+
* 纯 fs 探测(零 DB 依赖、零 token、快):进度库的权威状态归 `sillyspec progress show`,
|
|
10
|
+
* 本命令管「没有活跃进度时从产物文件反推该干什么」。决策表(两个 skill 合并):
|
|
11
|
+
*
|
|
12
|
+
* 1. HANDOFF.json 存在 → 交接恢复
|
|
13
|
+
* 2. 活跃变更(changes/ 非 archive)逐个推断:
|
|
14
|
+
* 无文件→补 proposal / 无 design.md→brainstorm / 无 tasks.md→brainstorm 末步 /
|
|
15
|
+
* 无 plan.md→plan / tasks.md 有未勾→execute / 全勾无 verify-result.md→verify /
|
|
16
|
+
* 有 verify-result.md→archive
|
|
17
|
+
* 3. 有 docs/<p>/scan/ 无活跃变更 → brainstorm(已扫描待开变更)
|
|
18
|
+
* 4. 有 REQUIREMENTS.md/ROADMAP.md 无变更 → brainstorm(绿地已有需求)
|
|
19
|
+
* 5. 什么都没有 → init(新项目)或 scan(棕地)
|
|
20
|
+
*/
|
|
21
|
+
import { existsSync, readdirSync, readFileSync } from 'fs'
|
|
22
|
+
import { join } from 'path'
|
|
23
|
+
import { resolveSpecDir } from './shared.js'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 数 tasks.md checkbox:返回 { checked, total }(无文件 → {checked:0,total:0})
|
|
27
|
+
*/
|
|
28
|
+
function countTasks(tasksPath) {
|
|
29
|
+
if (!existsSync(tasksPath)) return { checked: 0, total: 0 }
|
|
30
|
+
let checked = 0
|
|
31
|
+
let total = 0
|
|
32
|
+
for (const line of readFileSync(tasksPath, 'utf8').split('\n')) {
|
|
33
|
+
const m = line.match(/^[-*]\s*\[([ xX])\]\s*task-\d+\b/)
|
|
34
|
+
if (!m) continue
|
|
35
|
+
total++
|
|
36
|
+
if (m[1] !== ' ') checked++
|
|
37
|
+
}
|
|
38
|
+
return { checked, total }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 单个活跃变更的产物推断:返回 { state, next, evidence }
|
|
43
|
+
*/
|
|
44
|
+
function inferChange(changeDir, name) {
|
|
45
|
+
const has = (f) => existsSync(join(changeDir, f))
|
|
46
|
+
const ev = (f) => join('.sillyspec', 'changes', name, f)
|
|
47
|
+
const t = countTasks(join(changeDir, 'tasks.md'))
|
|
48
|
+
|
|
49
|
+
if (!has('proposal.md') && !has('design.md') && !has('tasks.md') && !has('plan.md')) {
|
|
50
|
+
return { state: `${name}:变更目录为空`, next: 'sillyspec run brainstorm(完善 proposal)或清理该空目录', evidence: [ev('(无产物)')] }
|
|
51
|
+
}
|
|
52
|
+
if (!has('design.md')) {
|
|
53
|
+
return { state: `${name}:有 proposal 缺 design.md`, next: `sillyspec run brainstorm --change ${name}(推进到设计产物)`, evidence: [ev('proposal.md')] }
|
|
54
|
+
}
|
|
55
|
+
if (!has('tasks.md')) {
|
|
56
|
+
return { state: `${name}:有 design 缺 tasks.md`, next: `sillyspec run brainstorm --change ${name}(末步生成规范文件)`, evidence: [ev('design.md')] }
|
|
57
|
+
}
|
|
58
|
+
if (!has('plan.md')) {
|
|
59
|
+
return { state: `${name}:有 tasks.md 缺 plan.md`, next: `sillyspec run plan --change ${name}`, evidence: [ev('tasks.md')] }
|
|
60
|
+
}
|
|
61
|
+
if (t.total > 0 && t.checked < t.total) {
|
|
62
|
+
return {
|
|
63
|
+
state: `${name}:执行中(task ${t.checked}/${t.total})`,
|
|
64
|
+
next: `sillyspec run execute --change ${name}`,
|
|
65
|
+
evidence: [ev('plan.md'), ev('tasks.md')],
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (!has('verify-result.md')) {
|
|
69
|
+
return { state: `${name}:task 全勾,待验证`, next: `sillyspec run verify --change ${name}`, evidence: [ev('tasks.md'), ev('plan.md')] }
|
|
70
|
+
}
|
|
71
|
+
return { state: `${name}:已验证,待归档`, next: `sillyspec run archive --change ${name}`, evidence: [ev('verify-result.md')] }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 探测当前项目状态与下一步命令。
|
|
76
|
+
* @param {{ cwd: string, specDir?: string|null }} opts
|
|
77
|
+
* @returns {{ state: string, next: string, evidence: string[], activeChanges: Array<{name,checked,total}> }}
|
|
78
|
+
* activeChanges 附带各变更 task 勾选进度(供 --json 消费);evidence 为相对 spec 根的路径
|
|
79
|
+
*/
|
|
80
|
+
export function detectNextStep({ cwd, specDir = null }) {
|
|
81
|
+
const specBase = resolveSpecDir(cwd, { specDir })
|
|
82
|
+
const changesRoot = join(specBase, 'changes')
|
|
83
|
+
|
|
84
|
+
// 1. HANDOFF 交接
|
|
85
|
+
for (const handoff of [join(cwd, 'HANDOFF.json'), join(specBase, 'HANDOFF.json')]) {
|
|
86
|
+
if (existsSync(handoff)) {
|
|
87
|
+
return { state: '存在 HANDOFF.json(上次会话交接)', next: '读 HANDOFF.json 后从交接点继续(sillyspec progress show 看活跃进度)', evidence: [handoff], activeChanges: [] }
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 2. 活跃变更(排除 archive)
|
|
92
|
+
let active = []
|
|
93
|
+
try {
|
|
94
|
+
active = readdirSync(changesRoot, { withFileTypes: true })
|
|
95
|
+
.filter(e => e.isDirectory() && e.name !== 'archive')
|
|
96
|
+
.map(e => e.name)
|
|
97
|
+
.sort()
|
|
98
|
+
} catch { /* 无 changes/ 目录 */ }
|
|
99
|
+
if (active.length > 0) {
|
|
100
|
+
// 多活跃时逐个推断全报告,next 取第一个(字典序最前)的;调用方输出含全部状态行
|
|
101
|
+
const inferences = active.map(name => inferChange(join(changesRoot, name), name))
|
|
102
|
+
const progress = active.map((name, i) => {
|
|
103
|
+
const t = countTasks(join(changesRoot, name, 'tasks.md'))
|
|
104
|
+
return { name, checked: t.checked, total: t.total, state: inferences[i].state }
|
|
105
|
+
})
|
|
106
|
+
return { state: inferences.map(i => i.state).join(';'), next: inferences[0].next, evidence: inferences[0].evidence, activeChanges: progress }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 3. 已扫描无变更
|
|
110
|
+
const docsDir = join(specBase, 'docs')
|
|
111
|
+
try {
|
|
112
|
+
const scanned = readdirSync(docsDir, { withFileTypes: true })
|
|
113
|
+
.filter(e => e.isDirectory() && existsSync(join(docsDir, e.name, 'scan')))
|
|
114
|
+
if (scanned.length > 0) {
|
|
115
|
+
return { state: `已扫描(${scanned.length} 个项目文档),无活跃变更`, next: 'sillyspec run brainstorm "你的想法"', evidence: [join('.sillyspec', 'docs', scanned[0].name, 'scan')], activeChanges: [] }
|
|
116
|
+
}
|
|
117
|
+
} catch { /* 无 docs/ */ }
|
|
118
|
+
|
|
119
|
+
// 4. 绿地已有需求
|
|
120
|
+
for (const f of ['REQUIREMENTS.md', 'ROADMAP.md']) {
|
|
121
|
+
if (existsSync(join(specBase, f))) {
|
|
122
|
+
return { state: `绿地项目已有 ${f},无活跃变更`, next: 'sillyspec run brainstorm "你的想法"', evidence: [join('.sillyspec', f)], activeChanges: [] }
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 5. 什么都没有
|
|
127
|
+
return { state: '未初始化(无 .sillyspec 产物)', next: 'sillyspec init(新项目)或 sillyspec scan(棕地项目)', evidence: [], activeChanges: [] }
|
|
128
|
+
}
|
package/src/run/prompt.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
import { basename, join } from 'node:path'
|
|
22
22
|
import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
23
23
|
import { stageRegistry } from '../stages/index.js'
|
|
24
|
-
import { resolvePromptIncludes, resolveRuntimeRoot, safeGit, WAIT_MARKER_RE } from './shared.js'
|
|
24
|
+
import { resolvePromptIncludes, resolveRuntimeRoot, safeGit, WAIT_MARKER_RE, QUICK_SID_RE } from './shared.js'
|
|
25
25
|
import { renderStageContract } from '../stage-contract-spec.js'
|
|
26
26
|
import { parseModuleMapSimple } from '../modules.js'
|
|
27
27
|
import { REVIEW_SCHEMA_VERSION, isValidExecuteRunId } from '../task-review.js'
|
|
@@ -236,6 +236,16 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
236
236
|
promptText = promptText.replace(/<now-datetime>/g, nowDatetime)
|
|
237
237
|
promptText = promptText.replace(/<now-timestamp>/g, nowTimestamp)
|
|
238
238
|
promptText = promptText.replace(/<now-date>/g, nowDate)
|
|
239
|
+
// <now-iso-datetime>(scan frontmatter updated_at 用;与 scan-fix-headers 同款秒级格式)
|
|
240
|
+
promptText = promptText.replace(/<now-iso-datetime>/g, now.toISOString().slice(0, 19).replace('T', ' '))
|
|
241
|
+
// <git-head-short>(scan frontmatter source_commit 用;CLI 代查,agent 勿自跑 rev-parse)
|
|
242
|
+
if (promptText.includes('<git-head-short>')) {
|
|
243
|
+
let headShort = 'unknown'
|
|
244
|
+
try {
|
|
245
|
+
headShort = safeGit(cwd, ['rev-parse', '--short', 'HEAD']).value || 'unknown'
|
|
246
|
+
} catch { /* git 不可用 → unknown,scan-postcheck 失配时再核 */ }
|
|
247
|
+
promptText = promptText.replace(/<git-head-short>/g, headShort)
|
|
248
|
+
}
|
|
239
249
|
// 替换 {REVIEW_SCHEMA_VERSION} 占位符(task review 示例模板用,值=CLI 当前 REVIEW_SCHEMA_VERSION 常量,
|
|
240
250
|
// 避免 agent 照抄 design 目标版本与 CLI 写侧常量漂移;与 stage review 契约 renderReviewJsonContract 动态注入同源)
|
|
241
251
|
if (promptText.includes('{REVIEW_SCHEMA_VERSION}')) {
|
|
@@ -500,6 +510,21 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
500
510
|
}
|
|
501
511
|
}
|
|
502
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
|
+
|
|
503
528
|
// Execute: 注入 currentExecuteRunId(从变更专属标记文件读取)
|
|
504
529
|
if (stageName === 'execute' && promptText.includes('{EXECUTE_RUN_ID}')) {
|
|
505
530
|
let runId = ''
|
|
@@ -722,7 +747,15 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
722
747
|
// 普通本地保持相对 '.sillyspec' 展示;runtimeRoot-only 组合不再 join(null) 崩溃
|
|
723
748
|
const changeDirBase = platformOpts?.specRoot || platformOpts?.specDriftAnchor || '.sillyspec'
|
|
724
749
|
const changeDir = join(changeDirBase, 'changes', changeName)
|
|
725
|
-
|
|
750
|
+
// quick 会话(quick-<hex8>)按设计无实体变更目录(progress.js initChange 同款跳过),
|
|
751
|
+
// 纯代码 quick 根本不产 spec 文档——「所有变更文件必须写入 changes/<change>/」的硬规则
|
|
752
|
+
// 对它是误导(ql-20260821-011 实证:CLI 反复提示一个用不到的目录)。改述条件化:
|
|
753
|
+
// 仅「若本 quick 决定补文档」才落该目录,代码改动照常写源码目录。
|
|
754
|
+
if (QUICK_SID_RE.test(changeName)) {
|
|
755
|
+
console.log(`- **文件路径规则(quick 会话):纯代码改动直接写源码目录,无目录限制。仅当本 quick 需要落 spec 文档(design/plan 等,复杂任务建议升级完整流程)时才写 \`${changeDir}/\`;QUICKLOG/tasks.md 记录由 CLI 接管,不要手建。**`)
|
|
756
|
+
} else {
|
|
757
|
+
console.log(`- **文件路径规则:所有变更文件必须写入 \`${changeDir}/\` 目录下。不要自己拼接路径,直接使用 changeDir 值。示例:\`${changeDir}/proposal.md\`**`)
|
|
758
|
+
}
|
|
726
759
|
}
|
|
727
760
|
}
|
|
728
761
|
const changeFlag = changeName ? ` --change ${changeName}` : ''
|
|
@@ -764,6 +797,5 @@ export function applyRootPlaceholders(text, roots) {
|
|
|
764
797
|
.replaceAll('{DOCS_ROOT}', roots.docsRoot)
|
|
765
798
|
.replaceAll('{PROJECTS_ROOT}', roots.projectsRoot)
|
|
766
799
|
.replaceAll('{WORKFLOWS_ROOT}', roots.workflowsRoot)
|
|
767
|
-
.replaceAll('{KNOWLEDGE_ROOT}', roots.knowledgeRoot)
|
|
768
|
-
}
|
|
800
|
+
.replaceAll('{KNOWLEDGE_ROOT}', roots.knowledgeRoot)}
|
|
769
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
|
}
|