sillyspec 3.28.2 → 3.28.3
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-auto/SKILL.md +54 -93
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -114
- package/.claude/skills/sillyspec-continue/SKILL.md +17 -47
- package/.claude/skills/sillyspec-execute/SKILL.md +222 -214
- package/.claude/skills/sillyspec-knowledge/SKILL.md +40 -270
- package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
- package/.claude/skills/sillyspec-propose/SKILL.md +16 -87
- package/.claude/skills/sillyspec-resume/SKILL.md +21 -70
- package/.claude/skills/sillyspec-state/SKILL.md +25 -64
- package/.claude/skills/sillyspec-workspace/SKILL.md +33 -175
- package/package.json +1 -1
- package/src/archive-delta.js +45 -2
- package/src/constants.js +6 -0
- package/src/design-facts.js +64 -0
- package/src/dispatch/backends/sillyhub-mcp.js +17 -5
- package/src/docs-check.js +119 -27
- package/src/docs-migrate.js +124 -0
- package/src/index.js +172 -22
- package/src/progress/change-registry.js +19 -0
- package/src/progress.js +2 -0
- package/src/run/command.js +129 -4
- package/src/run/complete-handlers.js +10 -3
- package/src/run/complete.js +59 -5
- package/src/run/gates.js +22 -2
- package/src/run/progress-confirm.js +66 -0
- package/src/run/prompt.js +139 -31
- package/src/run/quick-audit.js +12 -5
- package/src/run/scan-profile.js +20 -2
- package/src/run/shared.js +10 -5
- package/src/run/stage.js +7 -0
- package/src/scan-postcheck.js +5 -1
- package/src/stages/brainstorm-auto.js +2 -1
- package/src/stages/brainstorm.js +9 -17
- package/src/stages/execute.js +10 -14
- package/src/stages/plan.js +2 -1
- package/src/stages/quick.js +4 -2
- package/src/stages/verify.js +13 -15
- package/src/task-review.js +200 -0
- package/src/verify-postcheck.js +164 -30
- package/templates/agents-instruction.md +1 -1
- package/.claude/skills/verify-per-user/SKILL.md +0 -6
package/src/run/complete.js
CHANGED
|
@@ -21,7 +21,25 @@ import { existsSync, readFileSync, mkdirSync, writeFileSync, appendFileSync } fr
|
|
|
21
21
|
import { writeAtomicSync } from '../fs-atomic.js'
|
|
22
22
|
import { withFileLock } from '../quicklog.js'
|
|
23
23
|
import { triggerSync, WAIT_MARKER_RE, getStageSteps, formatWaitOptions, resolveRuntimeRoot, getOrCreateMultiRepoContext, resolveChangeDir } from './shared.js'
|
|
24
|
+
|
|
25
|
+
// auto 模式 brainstorm 步骤表感知解析(2026-09-08 E2E 实证 bug:--done/--wait 推进主模式 8 步表,
|
|
26
|
+
// 与 runAutoMode getAutoSteps 的 4 步 auto 表双轨互踩——ensureAutoStage 判非 auto 表重种清零,
|
|
27
|
+
// 进度永远回 step1)。DB 现存 brainstorm 表为 auto 形态(4 步+首名「进度确认与上下文加载」)时,
|
|
28
|
+
// complete 路径与渲染路径同源用 brainstormAutoDef.steps。
|
|
29
|
+
async function getStageStepsAutoAware(stageName, cwd, progress, specDir) {
|
|
30
|
+
const steps = await getStageSteps(stageName, cwd, progress, specDir)
|
|
31
|
+
if (stageName !== 'brainstorm') return steps
|
|
32
|
+
const existing = progress?.stages?.brainstorm?.steps
|
|
33
|
+
const isAutoTable = Array.isArray(existing) && existing.length === 4
|
|
34
|
+
&& existing[0]?.name === '进度确认与上下文加载'
|
|
35
|
+
if (!isAutoTable) return steps
|
|
36
|
+
const { definition: brainstormAutoDef } = await import('../stages/brainstorm-auto.js')
|
|
37
|
+
return brainstormAutoDef.steps
|
|
38
|
+
}
|
|
39
|
+
|
|
24
40
|
import { executeScanPreflight, executeScanPostcheck, computeScanProfile, executeScanDetectProjects, executeScanResumeCheck, executeScanFinalize } from './scan-profile.js'
|
|
41
|
+
import { executeProgressConfirm } from './progress-confirm.js'
|
|
42
|
+
import { AUXILIARY_STAGES } from '../constants.js'
|
|
25
43
|
import { executePlanPostcheck as runPlanPostcheckLib } from '../stages/plan-postcheck.js'
|
|
26
44
|
import { outputStep, collectStageWaitHistory } from './prompt.js'
|
|
27
45
|
import { enforceDepsGate, enforceReviewJsonGate, enforceSymbolImpactGate, warnMissingUiPrototype, completeStageGates, readDesignScale } from './gates.js'
|
|
@@ -220,7 +238,7 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
220
238
|
}
|
|
221
239
|
|
|
222
240
|
// ── requiresWait 硬门控 ──
|
|
223
|
-
const defStepsForCurrent = await
|
|
241
|
+
const defStepsForCurrent = await getStageStepsAutoAware(stageName, cwd, progress, platformOpts?.specRoot || null)
|
|
224
242
|
// def↔DB 一致性守卫(坑 execute-step-table-drift,2026-08-20 实证):runCommand 入口已按名
|
|
225
243
|
// 重播种(ensureStageSteps),但同一命令进程内 plan.md 若再次被改(或绕过 runCommand 直调
|
|
226
244
|
// completeStep 的路径),def 与 DB 步数错位会让 currentIdx 的门控/prompt 施加到错误的步骤上
|
|
@@ -312,6 +330,29 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
312
330
|
}
|
|
313
331
|
}
|
|
314
332
|
|
|
333
|
+
// ── design.md 文件清单行级核验(2026-09-07-ir-hardening D-004@v1,FR-02)──
|
|
334
|
+
// 幻觉路径 ERROR(design_file_ref_invalid,与 plan 侧 validateTargetFiles 同语义先例);
|
|
335
|
+
// NEW: 前缀豁免 / glob 跳过 / 清单缺失 WARNING。挂点与决策模块域核验同点位(brainstorm 末步)。
|
|
336
|
+
if (stageName === 'brainstorm' && steps[currentIdx]?.name?.includes('生成规范文件') && changeName) {
|
|
337
|
+
try {
|
|
338
|
+
const { validateDesignFileList } = await import('../design-facts.js')
|
|
339
|
+
const _dflResult = validateDesignFileList({
|
|
340
|
+
changeDir: join(specBase, 'changes', changeName),
|
|
341
|
+
cwd,
|
|
342
|
+
})
|
|
343
|
+
if ((_dflResult.errors || []).length > 0) {
|
|
344
|
+
console.error(`❌ ── design 文件清单核验阻断(本次 --done 未完成,进度未推进)──`)
|
|
345
|
+
for (const _dflErr of _dflResult.errors) console.error(` • ${_dflErr.message}`)
|
|
346
|
+
console.error(` 修复:更正 design.md「文件变更清单」对应行路径,或计划新建文件加 NEW: 前缀后重跑:`)
|
|
347
|
+
console.error(` sillyspec run brainstorm --done${changeName ? ` --change ${changeName}` : ''} --output "修复说明"`)
|
|
348
|
+
process.exit(1)
|
|
349
|
+
}
|
|
350
|
+
for (const _dflWarn of (_dflResult.warnings || [])) console.warn(` ⚠️ ${_dflWarn}`)
|
|
351
|
+
} catch (_dflEx) {
|
|
352
|
+
console.warn(` ⚠️ design 清单核验自身异常,fail-open 放行(不误拦本次完成):${_dflEx && _dflEx.message ? _dflEx.message : _dflEx}`)
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
315
356
|
// ── noAI 步骤硬门(坑 noai-done-bypass):noAI 步骤的确定性校验不可被 --done 绕过 ──
|
|
316
357
|
// 正常路径 agent 跑 `run <stage>` 推进到 noAI step 时,runStage 自动执行 _cliAction
|
|
317
358
|
// (stage.js noAI 分支,不写 step output);若 agent 对 noAI step 直接 --done,此前
|
|
@@ -336,6 +377,9 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
336
377
|
await executeScanFinalize(cwd, platformOpts)
|
|
337
378
|
} else if (_cliAction === 'planPostcheck') {
|
|
338
379
|
await runPlanPostcheckLib({ cwd, specRoot: platformOpts?.specRoot, resolveChangeDir, progress })
|
|
380
|
+
} else if (_cliAction === 'progressConfirm') {
|
|
381
|
+
// 与 stage.js noAI 分支同语义(brainstorm/execute/verify step1 进度确认)
|
|
382
|
+
executeProgressConfirm({ stageName, cwd, stageData, changeName, pm })
|
|
339
383
|
} else {
|
|
340
384
|
throw new Error(`noAI 步骤 ${steps[currentIdx].name} 的未知 _cliAction: ${_cliAction}——请在 complete.js 注册对应分支`)
|
|
341
385
|
}
|
|
@@ -483,6 +527,11 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
483
527
|
}
|
|
484
528
|
}
|
|
485
529
|
|
|
530
|
+
// 主阶段完成钉 currentStage(troubleshooting #56 根因修复,2026-09-08):
|
|
531
|
+
// `run <stage> --done` 直达完成不经过裸 run 入口(唯一写点 stage.js:227),
|
|
532
|
+
// currentStage 停留旧阶段 → 下游 checkTransition 用 stale prevStage 误拦
|
|
533
|
+
// (实证:verify 全 --done 完成后归档报 execute→archive)。与入口写点同语义:辅助阶段不写。
|
|
534
|
+
if (!AUXILIARY_STAGES.includes(stageName)) progress.currentStage = stageName
|
|
486
535
|
stageData.status = 'completed'
|
|
487
536
|
stageData.completedAt = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
488
537
|
progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
@@ -606,7 +655,7 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
606
655
|
}
|
|
607
656
|
}
|
|
608
657
|
|
|
609
|
-
const defSteps = await
|
|
658
|
+
const defSteps = await getStageStepsAutoAware(stageName, cwd, progress, platformOpts?.specRoot || null)
|
|
610
659
|
console.log(`✅ Step ${currentIdx + 1}/${steps.length} 完成:${steps[currentIdx].name}\n`)
|
|
611
660
|
|
|
612
661
|
// Workflow post_check(W6 Step6 抽至 complete-handlers.js handleWorkflowPostCheck)
|
|
@@ -1007,7 +1056,7 @@ export async function waitStep(pm, progress, stageName, cwd, outputText, waitRea
|
|
|
1007
1056
|
|
|
1008
1057
|
// maxWaitRounds 硬上限:达到后拒绝继续 --wait
|
|
1009
1058
|
const currentStep = stageData.steps[currentIdx]
|
|
1010
|
-
const defSteps = await
|
|
1059
|
+
const defSteps = await getStageStepsAutoAware(stageName, cwd, progress, platformOpts?.specRoot || null)
|
|
1011
1060
|
const stepDef = defSteps?.[currentIdx] || {}
|
|
1012
1061
|
const maxWaitRounds = currentStep.maxWaitRounds ?? stepDef.maxWaitRounds
|
|
1013
1062
|
const currentWaitRound = currentStep.waitRound || 0
|
|
@@ -1121,7 +1170,7 @@ export async function continueStep(pm, progress, stageName, cwd, answer, options
|
|
|
1121
1170
|
} else {
|
|
1122
1171
|
currentIdx = waitingSteps[0].idx
|
|
1123
1172
|
}
|
|
1124
|
-
const defSteps = await
|
|
1173
|
+
const defSteps = await getStageStepsAutoAware(stageName, cwd, progress, platformOpts?.specRoot || null)
|
|
1125
1174
|
const currentStepDef = defSteps?.[currentIdx] || {}
|
|
1126
1175
|
const currentStep = stageData.steps[currentIdx]
|
|
1127
1176
|
const isRepeatableWait = currentStepDef.repeatableWait === true || currentStep.repeatableWait === true
|
|
@@ -1219,6 +1268,11 @@ export async function continueStep(pm, progress, stageName, cwd, answer, options
|
|
|
1219
1268
|
if (nextPendingIdx === -1 && nextWaitingIdx === -1) {
|
|
1220
1269
|
stageData.status = 'completed'
|
|
1221
1270
|
stageData.completedAt = now
|
|
1271
|
+
// 主阶段完成钉 currentStage(troubleshooting #56 根因修复,2026-09-08):
|
|
1272
|
+
// `run <stage> --done` 直达完成不经过裸 run 入口(唯一写点 stage.js:227),
|
|
1273
|
+
// currentStage 停留旧阶段 → 下游 checkTransition 用 stale prevStage 误拦
|
|
1274
|
+
// (实证:verify 全 --done 完成后归档报 execute→archive)。与入口写点同语义:辅助阶段不写。
|
|
1275
|
+
if (!AUXILIARY_STAGES.includes(stageName)) progress.currentStage = stageName
|
|
1222
1276
|
// persist _write 移到 completeStageGates 成功之后(task-03 / review-2026-08-09 #2):gate 异常/失败 → rollback 回 in-progress 落盘,此处未到 _write,DB 不留假 completed。
|
|
1223
1277
|
// 阶段完成收尾共享管线(含 execute worktree cleanup),消除 continueStep 完成分支绕过 gate 的 S2(task-01 抽出)。
|
|
1224
1278
|
// gate 失败已 rollback,early-return 跳过下方"阶段已完成/下一步"提示(与 completeStep 同语义)。
|
|
@@ -1303,7 +1357,7 @@ export async function skipStep(pm, progress, stageName, cwd, changeName, platfor
|
|
|
1303
1357
|
process.exit(1)
|
|
1304
1358
|
}
|
|
1305
1359
|
|
|
1306
|
-
const defSteps = await
|
|
1360
|
+
const defSteps = await getStageStepsAutoAware(stageName, cwd, progress, platformOpts?.specRoot || null)
|
|
1307
1361
|
const stepDef = defSteps ? defSteps[currentIdx] : null
|
|
1308
1362
|
if (stepDef && !stepDef.optional) {
|
|
1309
1363
|
console.error(`❌ 步骤 "${steps[currentIdx].name}" 不可跳过`)
|
package/src/run/gates.js
CHANGED
|
@@ -674,13 +674,15 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
674
674
|
// 声明=scope creep)启发式噪音面大(并行 WIP 剔除后仍可能有工具产物),WARNING 放行留审计;
|
|
675
675
|
// skipped/degraded(无 task 卡 / 全无声明 / git 不可用)降级不误红(存量变更零红门禁)。
|
|
676
676
|
// 阻断形态照 runVerifyTestCheck 先例:逐条列 task + path + 修复提示 + rollbackCompletionAndReturn。
|
|
677
|
-
const { reconcileTargetFiles } = await import('../verify-postcheck.js')
|
|
677
|
+
const { reconcileTargetFiles, isStrictChange } = await import('../verify-postcheck.js')
|
|
678
678
|
const reconcileRuntimeRoot = resolveRuntimeRoot(platformOpts, specBase)
|
|
679
679
|
const reconcileCheck = reconcileTargetFiles({
|
|
680
680
|
cwd, specBase, changeName,
|
|
681
681
|
// runtimeRoot 口径与上方 parity 对账同源(B3 apply-pathspec 兜底源在此根下;平台模式下
|
|
682
682
|
// specBase 与 .runtime 分离,不传会读不到兜底清单 → 假降级)
|
|
683
683
|
runtimeRoot: reconcileRuntimeRoot,
|
|
684
|
+
// IR 严格档(2026-09-07-ir-hardening D-003@v1):主仓卡全零声明 → strictViolation → ERROR
|
|
685
|
+
strictMode: isStrictChange({ pm, cwd, changeName }),
|
|
684
686
|
})
|
|
685
687
|
const reconcileEnvelope = buildReconcileTargetFilesEnvelope(reconcileCheck)
|
|
686
688
|
const reconcileBlocked = printReconcileTargetFilesCheck(reconcileCheck, reconcileEnvelope)
|
|
@@ -705,6 +707,9 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
705
707
|
// 的锚点产物实际都在 changeDir,此参为统一传参签名对齐预留,口径一处定义不二算
|
|
706
708
|
cwd, specBase, changeName,
|
|
707
709
|
runtimeRoot: reconcileRuntimeRoot,
|
|
710
|
+
// IR 严格档(2026-09-07-ir-hardening D-002@v1):created_at ≥ IR_STRICT_SINCE 的变更
|
|
711
|
+
// 探针子节全缺 = ERROR(存量 skip 语义零变化);fail-open(读不到 → false 落豁免)
|
|
712
|
+
strictMode: isStrictChange({ pm, cwd, changeName }),
|
|
708
713
|
})
|
|
709
714
|
const probeEnvelope = buildProbeConsistencyEnvelope(probeCheck)
|
|
710
715
|
const probeBlocked = printProbeConsistencyCheck(probeCheck, probeEnvelope)
|
|
@@ -1357,6 +1362,14 @@ export function printReconcileTargetFilesCheck(r, envelope = buildReconcileTarge
|
|
|
1357
1362
|
console.warn(` 对账口径 actual=${(r.sources || []).join(' + ') || 'n/a'}(③类误报先核对 sources 与并行会话剔除提示)。`)
|
|
1358
1363
|
return false
|
|
1359
1364
|
}
|
|
1365
|
+
// IR 严格档(2026-09-07-ir-hardening D-003):strictViolation → ERROR 阻断(与②类同处置)
|
|
1366
|
+
if (r.status === 'skipped' && r.strictViolation) {
|
|
1367
|
+
console.error(`
|
|
1368
|
+
❌ target_files 对账阻断:${r.skipReason}`)
|
|
1369
|
+
console.error(` ${r.strictViolation.message}`)
|
|
1370
|
+
console.error(` 信封 code: ${r.strictViolation.code}`)
|
|
1371
|
+
return true
|
|
1372
|
+
}
|
|
1360
1373
|
// 降级(skipped/degraded):WARNING 放行,skipReason 一句话(存量卡/git 不可用,不误红)
|
|
1361
1374
|
if (r.status === 'skipped' || r.status === 'degraded') {
|
|
1362
1375
|
console.warn(`\n⚠️ target_files 对账${r.status === 'degraded' ? '降级' : '跳过'}(不阻断):${r.skipReason}`)
|
|
@@ -1385,6 +1398,12 @@ export function printReconcileTargetFilesCheck(r, envelope = buildReconcileTarge
|
|
|
1385
1398
|
export function buildProbeConsistencyEnvelope(r) {
|
|
1386
1399
|
const blocked = r.status === 'mismatch' && r.severity === 'error'
|
|
1387
1400
|
const drifted = r.status === 'mismatch' && !blocked
|
|
1401
|
+
// IR 严格档独立 code(2026-09-07-ir-hardening D-002):首条 mismatch 携带
|
|
1402
|
+
// probe_prefill_missing_strict 时路由专用 code(平台/上游按 code 分支可区分
|
|
1403
|
+
// 「篡改预填段」与「从未生成预填段」两类 ERROR)
|
|
1404
|
+
const strictMissing = blocked
|
|
1405
|
+
&& Array.isArray(r.mismatches) && r.mismatches.length > 0
|
|
1406
|
+
&& r.mismatches[0] && r.mismatches[0].code === 'probe_prefill_missing_strict'
|
|
1388
1407
|
const supportedFixes = []
|
|
1389
1408
|
if (blocked) {
|
|
1390
1409
|
supportedFixes.push('如实重生成:跑 `sillyspec verify-probes --change <变更名> --init` 重新落盘机械预填段,再基于新预填段如实补写正文结论(删改预填段过门是 Step7 防篡改纪律红线)')
|
|
@@ -1396,7 +1415,8 @@ export function buildProbeConsistencyEnvelope(r) {
|
|
|
1396
1415
|
}
|
|
1397
1416
|
return {
|
|
1398
1417
|
// code 机器路由键(task-02 JSDoc):四状态→四 code,供平台/上游按 code 分支
|
|
1399
|
-
code:
|
|
1418
|
+
code: strictMissing ? 'probe_prefill_missing_strict'
|
|
1419
|
+
: blocked ? 'probe_consistency_mismatch'
|
|
1400
1420
|
: drifted ? 'probe_consistency_drift'
|
|
1401
1421
|
: (r.status === 'ok' ? 'probe_consistency_ok' : 'probe_consistency_skipped'),
|
|
1402
1422
|
name: 'probe_consistency',
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* progressConfirm — 主流程 Step1「进度确认」的 noAI 动作(brainstorm / execute / verify)
|
|
3
|
+
* (2026-09-07 noAI 主流程首批;机制先例:scan 系 noAI 步 + planPostcheck,见 run/stage.js noAI 分支)
|
|
4
|
+
*
|
|
5
|
+
* noAI 化依据(step1 职责在历次加固后已全部机械化):
|
|
6
|
+
* - 阶段路由:`run <stage>` 显式调用即写 currentStage(run/stage.js D-003@v1),
|
|
7
|
+
* 阶段顺序违规由 checkTransition 拦截——原 prompt 的「阶段不符则停止」检查已残留化
|
|
8
|
+
* ({PROGRESS_SNAPSHOT} 注入文案本就写着「CLI 已路由到本阶段」);
|
|
9
|
+
* - 进度摘要:快照三行(变更/阶段/步骤位置)由 CLI 注入,agent 的「输出当前进度摘要」
|
|
10
|
+
* 只是把注入内容复述一遍再 --done——整轮往返零判断增量;
|
|
11
|
+
* - brainstorm 变更名检查:自动名在创建时 CLI 已打印 rename 提示(run/command.js),
|
|
12
|
+
* 此处机械复检一次模式命中;命中只打印 ⚠️ 建议(noAI 步的 console 输出仍在 CLI 工具
|
|
13
|
+
* 结果里,agent 可见、可在后续步骤与用户交互改名)——不 throw、不阻塞推进。
|
|
14
|
+
* - execute 执行范围:$ARGUMENTS 是会话层信息,CLI 不掺入;本步只补 tasks 勾选计数
|
|
15
|
+
* (readPlanCheckboxStatus 既有机器事实),范围以用户在会话中给出的为准。
|
|
16
|
+
* - verify 测试提示:原 prompt 首段的「CLI 在 --done 统一跑测试」要点改为 console 提示。
|
|
17
|
+
*
|
|
18
|
+
* 输出纪律:只 console 打印(noAI 步不写 step output,与 scan 系动作一致);
|
|
19
|
+
* 纯确认步无失败语义,不 throw。
|
|
20
|
+
*/
|
|
21
|
+
import { join } from 'node:path'
|
|
22
|
+
|
|
23
|
+
/** brainstorm 自动生成变更名的模式(run/command.js:1083 `${date}-new-change-${hex}`) */
|
|
24
|
+
const AUTO_CHANGE_NAME_RE = /^\d{4}-\d{2}-\d{2}-new-change-[a-f0-9]+$/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 执行「进度确认」noAI 动作:打印进度快照 + 阶段专属机械提示。
|
|
28
|
+
*
|
|
29
|
+
* @param {object} opts
|
|
30
|
+
* @param {string} opts.stageName - brainstorm / execute / verify
|
|
31
|
+
* @param {string} opts.cwd
|
|
32
|
+
* @param {object} opts.stageData - progress.stages[stageName](steps 数组)
|
|
33
|
+
* @param {string|null} opts.changeName
|
|
34
|
+
* @param {object|null} [opts.pm] - ProgressManager(execute 档 tasks 勾选计数用;缺省跳过该段)
|
|
35
|
+
*/
|
|
36
|
+
export function executeProgressConfirm({ stageName, cwd, stageData, changeName, pm = null }) {
|
|
37
|
+
const steps = Array.isArray(stageData?.steps) ? stageData.steps : []
|
|
38
|
+
const done = steps.filter(s => s.status === 'completed').length
|
|
39
|
+
console.log('📊 进度快照(CLI 注入,无需跑 progress show):')
|
|
40
|
+
console.log(` - 变更:${changeName || '(未知)'}`)
|
|
41
|
+
console.log(` - 阶段:${stageName}(CLI 已路由到本阶段)`)
|
|
42
|
+
console.log(` - 步骤:${done}/${steps.length} 已完成;本步已由 CLI 自动确认`)
|
|
43
|
+
|
|
44
|
+
if (stageName === 'brainstorm' && changeName && AUTO_CHANGE_NAME_RE.test(changeName)) {
|
|
45
|
+
console.log('')
|
|
46
|
+
console.log(`⚠️ 变更名「${changeName}」疑似自动生成——建议与用户确认语义化名称后执行:`)
|
|
47
|
+
console.log(` sillyspec change-rename ${changeName} <新名称>(勿用 mv,会漏改进度库引用)`)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (stageName === 'execute' && pm && changeName) {
|
|
51
|
+
try {
|
|
52
|
+
const changeDir = join(pm._getSpecDir(cwd), 'changes', changeName)
|
|
53
|
+
const plan = pm.readPlanCheckboxStatus(changeDir)
|
|
54
|
+
if (plan && plan.total > 0) {
|
|
55
|
+
console.log(` - 任务勾选:tasks ${plan.checked}/${plan.total}(执行范围以用户会话指定为准,未指定则全部 Wave)`)
|
|
56
|
+
}
|
|
57
|
+
} catch { /* 勾选计数 fail-soft:plan.md 缺失/解析失败不阻塞确认步 */ }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (stageName === 'verify') {
|
|
61
|
+
console.log('')
|
|
62
|
+
console.log('💡 `run verify` 只下发指令不跑测试——全量测试由 CLI 在最后 --done 统一执行')
|
|
63
|
+
console.log(' (local.yaml 的 commands.test,同步对账可能耗时较长);后续「运行测试和质量扫描」')
|
|
64
|
+
console.log(' 步骤无需手动跑全量,只做 lint/静态检查 + 可选快速冒烟。')
|
|
65
|
+
}
|
|
66
|
+
}
|
package/src/run/prompt.js
CHANGED
|
@@ -185,7 +185,20 @@ export function collectStageWaitHistory(progress, stageName) {
|
|
|
185
185
|
/**
|
|
186
186
|
* 输出当前步骤的 prompt
|
|
187
187
|
*/
|
|
188
|
-
|
|
188
|
+
/**
|
|
189
|
+
* requiresUser —— auto driver 元数据(change: 2026-09-08-auto-driver,D-002@v1)的四源判定纯函数:
|
|
190
|
+
* step.requiresWait / conditionalWait / requiresConfirm 三键(step 定义权威信号)+ WAIT_MARKER_RE 正文扫描
|
|
191
|
+
* (execute/verify 的 step 定义无三键,纯三源会漏真 wait 步)。全缺兜底 **false**——auto 默认语义是
|
|
192
|
+
* 继续驱动(Grill P1-①:保守 true 会让 execute/verify 逐步人工确认,推翻 FR-01)。
|
|
193
|
+
* 与 outputStep 内既有 mayNeedWait 计算同源(:1019 附近),仅多 requiresConfirm 一源。
|
|
194
|
+
*/
|
|
195
|
+
export function requiresUser(step, promptText) {
|
|
196
|
+
if (!step) return false
|
|
197
|
+
if (step.requiresWait === true || step.conditionalWait === true || step.requiresConfirm === true) return true
|
|
198
|
+
return WAIT_MARKER_RE.test(String(promptText || ''))
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjectName, platformOpts = {}, prevStepAnswer = null, waitHistory = null, autoMeta = null) {
|
|
189
202
|
const step = steps[stepIndex]
|
|
190
203
|
const total = steps.length
|
|
191
204
|
// ── 越界防御 ──
|
|
@@ -199,6 +212,16 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
199
212
|
console.error(` 请检查 --change 指定的变更目录是否存在、specRoot 是否正确,然后重试。`)
|
|
200
213
|
return false
|
|
201
214
|
}
|
|
215
|
+
|
|
216
|
+
// ── 首个 agent 可见步索引(「首步一次建立」类注入的锚点)──
|
|
217
|
+
// step0 为 noAI(brainstorm/execute/verify 的 step1「进度确认」,2026-09-07)时不渲染 prompt,
|
|
218
|
+
// persona/护栏/完成契约/铁律/平台路径约束等 step0 专属注入须顺延到首个渲染步,否则整段丢失
|
|
219
|
+
//(spec-dir.test.mjs Test4 平台 specDir 断言即此坑的行为探针)。全 noAI 防御性回退 0。
|
|
220
|
+
const firstRenderableIdx = (() => {
|
|
221
|
+
const i = steps.findIndex(s => s?.noAI !== true)
|
|
222
|
+
return i === -1 ? 0 : i
|
|
223
|
+
})()
|
|
224
|
+
|
|
202
225
|
const projectName = dbProjectName || basename(cwd)
|
|
203
226
|
|
|
204
227
|
// ── Revision context injection ──
|
|
@@ -244,8 +267,8 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
244
267
|
console.log(`changeDir: ${changeDir}`)
|
|
245
268
|
}
|
|
246
269
|
console.log(`---\n`)
|
|
247
|
-
// persona
|
|
248
|
-
if (personas[stageName] && stepIndex ===
|
|
270
|
+
// persona 只在首个 agent 可见步注入——角色设定一次即可,后续 step 重复注入纯属 token 浪费
|
|
271
|
+
if (personas[stageName] && stepIndex === firstRenderableIdx) {
|
|
249
272
|
console.log(personas[stageName])
|
|
250
273
|
console.log('')
|
|
251
274
|
}
|
|
@@ -255,7 +278,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
255
278
|
|
|
256
279
|
console.log(`## Step ${stepIndex + 1}/${total}: ${step.name}\n`)
|
|
257
280
|
if (guardrails) {
|
|
258
|
-
if (stepIndex ===
|
|
281
|
+
if (stepIndex === firstRenderableIdx) {
|
|
259
282
|
console.log(guardrails.trim())
|
|
260
283
|
console.log('')
|
|
261
284
|
} else {
|
|
@@ -398,13 +421,15 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
398
421
|
platformDirectives.push(`workspace_id: ${platformOpts.workspaceId}`)
|
|
399
422
|
}
|
|
400
423
|
// 平台 directives 前置条件:
|
|
401
|
-
// -
|
|
424
|
+
// - 首个 agent 可见步(对齐 persona 仅 step0 策略):路径列表/Write 规则/占位符映射
|
|
425
|
+
// 一次建立即可——step0 为 noAI(brainstorm/execute/verify 的 step1「进度确认」,
|
|
426
|
+
// 2026-09-07)时首个可见步顺延,按 defSteps 计算(坑同 scan quick 档 noAI preflight);
|
|
402
427
|
// - scan 阶段所有 step:scan 的 profileDirectives 本就每步注入(见下方 L917),平台路径
|
|
403
428
|
// 约束也需每步可见——否则 quick profile 的 step0 是 noAI preflight(自动执行),run scan
|
|
404
429
|
// 输出的首个可见 step 是 step1,会漏掉 specDir 路径,导致平台模式写入约束丢失。
|
|
405
430
|
// 后续 step 靠 changeDir header + footer 平台规则(L1077+)维持,platformDirectives
|
|
406
431
|
// 每步重复约 500 tokens 仅在 scan(步数有限)可接受。
|
|
407
|
-
if (stepIndex ===
|
|
432
|
+
if (stepIndex === firstRenderableIdx || stageName === 'scan') {
|
|
408
433
|
promptText = platformDirectives.join('\n') + '\n\n' + promptText
|
|
409
434
|
}
|
|
410
435
|
} else {
|
|
@@ -506,6 +531,80 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
506
531
|
promptText = promptText.replace(/\{SCAN_FACTS\}/g, factsInjected)
|
|
507
532
|
}
|
|
508
533
|
|
|
534
|
+
// ── 机械事实注入三件(2026-09-07 注入缺口批次):{LOCAL_COMMANDS} / {GIT_DIRTY} / {TASKS_CHECKBOX} ──
|
|
535
|
+
// CLI 渲染 prompt 时已能确定性拿到的机械事实,替代 agent 各自跑一趟 cat local.yaml /
|
|
536
|
+
// git status / 手数勾选(全流程 7+ 处步骤的高频机械读)。全部 fail-soft:异常注入单行
|
|
537
|
+
// 说明,不留残留占位符;替换后占位符即消失(不存在的档位自然零输出)。
|
|
538
|
+
|
|
539
|
+
// ① {LOCAL_COMMANDS}:local.yaml commands 段原文(brainstorm/plan/execute/verify/quick 多步消费)
|
|
540
|
+
if (promptText.includes('{LOCAL_COMMANDS}')) {
|
|
541
|
+
let localBlock = ''
|
|
542
|
+
try {
|
|
543
|
+
const lcSpecBase = resolvePromptSpecBase(platformOpts, cwd)
|
|
544
|
+
const localPath = join(lcSpecBase, 'local.yaml')
|
|
545
|
+
if (existsSync(localPath)) {
|
|
546
|
+
const lcLines = String(readFileSync(localPath, 'utf8')).replace(/\r\n?/g, '\n').split('\n')
|
|
547
|
+
const start = lcLines.findIndex(l => /^commands:\s*(#.*)?$/.test(l))
|
|
548
|
+
if (start !== -1) {
|
|
549
|
+
const body = []
|
|
550
|
+
for (let i = start + 1; i < lcLines.length && !/^\S/.test(lcLines[i]); i++) body.push(lcLines[i])
|
|
551
|
+
// unavailable 标记的条目按未配置语义剔除行(与 extractTestCommand 同口径)
|
|
552
|
+
const avail = body.filter(l => !/^\s*[\w.-]+:\s*['"]?unavailable['"]?\s*(#.*)?$/i.test(l))
|
|
553
|
+
const trimmed = avail.join('\n').trim()
|
|
554
|
+
localBlock = trimmed
|
|
555
|
+
? 'commands:\n' + trimmed
|
|
556
|
+
: '(local.yaml 存在但 commands 段为空——可跑 `sillyspec local detect` 补默认骨架)'
|
|
557
|
+
} else {
|
|
558
|
+
localBlock = '(local.yaml 存在但无 commands 段——可跑 `sillyspec local detect` 补默认骨架)'
|
|
559
|
+
}
|
|
560
|
+
} else {
|
|
561
|
+
localBlock = '(未配置 local.yaml——先跑 `sillyspec local detect` 生成骨架,以命令输出为准)'
|
|
562
|
+
}
|
|
563
|
+
} catch (e) {
|
|
564
|
+
localBlock = `(构建命令注入失败:${e.message}——可自行读 local.yaml)`
|
|
565
|
+
}
|
|
566
|
+
promptText = promptText.replace(/\{LOCAL_COMMANDS\}/g, localBlock)
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// ② {GIT_DIRTY}:工作区脏文件清单(git status --porcelain,quick 收尾步消费)
|
|
570
|
+
if (promptText.includes('{GIT_DIRTY}')) {
|
|
571
|
+
let dirtyBlock = ''
|
|
572
|
+
try {
|
|
573
|
+
const { gitQuiet } = await import('../git-helper.js')
|
|
574
|
+
// gitQuiet 返回裸 string(失败 null)——非 safeGit 的 {value,error} 形状
|
|
575
|
+
const lines = String(gitQuiet(cwd, ['status', '--porcelain']) || '').split('\n').filter(Boolean)
|
|
576
|
+
dirtyBlock = lines.length === 0
|
|
577
|
+
? '(工作区干净,无脏文件)'
|
|
578
|
+
: lines.slice(0, 40).join('\n') + (lines.length > 40 ? `\n(…共 ${lines.length} 行,超出 40 行截断——完整清单可自行跑 git status --porcelain)` : '')
|
|
579
|
+
} catch (e) {
|
|
580
|
+
dirtyBlock = `(脏文件注入失败:${e.message}——可自行跑 git status --porcelain)`
|
|
581
|
+
}
|
|
582
|
+
promptText = promptText.replace(/\{GIT_DIRTY\}/g, dirtyBlock)
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// ③ {TASKS_CHECKBOX}:tasks.md 任务勾选状态投影(verify 逐项检查步消费——省 agent 手数)
|
|
586
|
+
if (promptText.includes('{TASKS_CHECKBOX}')) {
|
|
587
|
+
let tasksBlock = ''
|
|
588
|
+
try {
|
|
589
|
+
const tcSpecBase = resolvePromptSpecBase(platformOpts, cwd)
|
|
590
|
+
const tasksPath = changeName ? join(tcSpecBase, 'changes', changeName, 'tasks.md') : null
|
|
591
|
+
if (tasksPath && existsSync(tasksPath)) {
|
|
592
|
+
const re = /^(\s*[-*]\s+\[([ xX])\]\s+task-\d+.*)$/
|
|
593
|
+
const rows = String(readFileSync(tasksPath, 'utf8')).replace(/\r\n?/g, '\n').split('\n')
|
|
594
|
+
.map(l => l.match(re)).filter(Boolean).map(m => m[1].trim())
|
|
595
|
+
const checked = rows.filter(r => /\[x\]/i.test(r)).length
|
|
596
|
+
tasksBlock = rows.length === 0
|
|
597
|
+
? '(tasks.md 无 task-NN checkbox 行)'
|
|
598
|
+
: `已勾 ${checked}/${rows.length}:\n` + rows.slice(0, 60).join('\n') + (rows.length > 60 ? `\n(…共 ${rows.length} 条,超出 60 条截断)` : '')
|
|
599
|
+
} else {
|
|
600
|
+
tasksBlock = '(tasks.md 不存在——跳过勾选对照)'
|
|
601
|
+
}
|
|
602
|
+
} catch (e) {
|
|
603
|
+
tasksBlock = `(勾选状态注入失败:${e.message}——可自行读 tasks.md)`
|
|
604
|
+
}
|
|
605
|
+
promptText = promptText.replace(/\{TASKS_CHECKBOX\}/g, tasksBlock)
|
|
606
|
+
}
|
|
607
|
+
|
|
509
608
|
// 决策防复潮注入(W1.1 第 3 点,task-04,FR-05):brainstorm「加载项目上下文」Step2 的
|
|
510
609
|
// {DECISION_HITS} → matchKnowledge decisionHits——命中 rejected 条目时渲染「否决决策提示」段
|
|
511
610
|
// (ID/标题/否决理由/复潮条件),无命中替换为空串零输出(不留残留占位符);注入结果同步落
|
|
@@ -712,22 +811,10 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
712
811
|
}
|
|
713
812
|
}
|
|
714
813
|
|
|
715
|
-
// 三主阶段 Step1「进度确认」快照注入(2026-09-05
|
|
716
|
-
//
|
|
717
|
-
//
|
|
718
|
-
|
|
719
|
-
try {
|
|
720
|
-
const done = steps.filter(s => s.status === 'completed').length
|
|
721
|
-
const waiting = steps.filter(s => s.status === 'waiting').length
|
|
722
|
-
const snapshot =
|
|
723
|
-
`- 变更:${changeName || '(未知)'}\n` +
|
|
724
|
-
`- 阶段:${stageName}(CLI 已路由到本阶段,无需再跑 progress show 确认)\n` +
|
|
725
|
-
`- 步骤:${done}/${steps.length} 已完成${waiting > 0 ? `,${waiting} 个等待中` : ''};当前推进到第 ${stepIndex + 1} 步`
|
|
726
|
-
promptText = promptText.split('{PROGRESS_SNAPSHOT}').join(snapshot)
|
|
727
|
-
} catch (e) {
|
|
728
|
-
promptText = promptText.split('{PROGRESS_SNAPSHOT}').join('(快照注入失败:' + e.message + '——可运行 sillyspec progress show 自查)')
|
|
729
|
-
}
|
|
730
|
-
}
|
|
814
|
+
// 三主阶段 Step1「进度确认」快照注入(2026-09-05 全流程审计)已随 step1 整体 noAI 化退役
|
|
815
|
+
// (2026-09-07):brainstorm/execute/verify 的 step1 不再渲染 prompt,快照改由
|
|
816
|
+
// run/progress-confirm.js 的 progressConfirm 动作 console 直出(占位符 {PROGRESS_SNAPSHOT}
|
|
817
|
+
// 无消费者,注入分支删除)。
|
|
731
818
|
|
|
732
819
|
// execute Step「确认 worktree 路径」meta 注入(2026-09-05 全流程审计):worktree meta 由 CLI
|
|
733
820
|
// 读出直接注入,agent 省一趟 worktree meta 工具往返;工具链可用性检查(真实工作)仍归 agent。
|
|
@@ -888,9 +975,9 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
888
975
|
}
|
|
889
976
|
|
|
890
977
|
// 完成契约(事前预知):该 stage 的机械校验通过条件,从 stage-contract-spec.js manifest 渲染,
|
|
891
|
-
// 与 CLI 完成校验严格同源(事前给的 == 事后查的)
|
|
892
|
-
//
|
|
893
|
-
if (stepIndex ===
|
|
978
|
+
// 与 CLI 完成校验严格同源(事前给的 == 事后查的)。仅首个 agent 可见步注入(与 persona/guardrails
|
|
979
|
+
// 同模式,一次建立即可;后续步靠 context 保留)。
|
|
980
|
+
if (stepIndex === firstRenderableIdx) {
|
|
894
981
|
const stageContract = renderStageContract(stageName)
|
|
895
982
|
if (stageContract) {
|
|
896
983
|
promptText = `${promptText}\n\n${stageContract}`
|
|
@@ -898,10 +985,10 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
898
985
|
}
|
|
899
986
|
|
|
900
987
|
console.log(promptText)
|
|
901
|
-
// 铁律拆分(W3 token
|
|
988
|
+
// 铁律拆分(W3 token 效率):通用流程纪律(文档优先/不跳步/不编造命令)只在首个 agent 可见步注入——
|
|
902
989
|
// 每步重复 ~800B 纯耗 context。但【平台写入规则 + 路径规则】是安全关键(防写错目录/绕过 Write),
|
|
903
990
|
// 且依赖 changeName/platformOpts,必须【每步注入】(context 压缩丢失会让 agent 越界写源码)。
|
|
904
|
-
if (stepIndex ===
|
|
991
|
+
if (stepIndex === firstRenderableIdx) {
|
|
905
992
|
console.log(`\n### ⚠️ 铁律`)
|
|
906
993
|
console.log('- 文档优先:代码产出必须先有对应的设计/规范文档支撑。')
|
|
907
994
|
console.log('- 聚焦本步骤:只执行本步骤描述的操作并完整做完;自行扩展或跳步会破坏状态机推进,后续步骤再做后续事。')
|
|
@@ -949,21 +1036,42 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
949
1036
|
// archive-batch-31-tool-notes ②:通用 --done 模板不带该 flag,agent 照抄执行撞
|
|
950
1037
|
// 「请添加 --confirm」确认门,误以为参数没带。
|
|
951
1038
|
const confirmFlag = step.requiresConfirm === true ? ' --confirm' : ''
|
|
1039
|
+
// auto driver(2026-09-08-auto-driver D-002/P1-③):auto 模式下命令一律 run auto 形态——
|
|
1040
|
+
// 与 SS-META 的 doneCommand 同源(同一 cmdStage 变量),消灭「正文 run <stage> / 元数据 run auto」双命令
|
|
1041
|
+
const cmdStage = autoMeta ? 'auto' : stageName
|
|
1042
|
+
const doneCommand = `sillyspec run ${cmdStage} --done${confirmFlag}${changeFlag} --output "你的摘要"`
|
|
952
1043
|
if (requiresWait) {
|
|
953
1044
|
console.log(`本步骤必须等待用户输入,不能直接 --done:`)
|
|
954
|
-
console.log(`sillyspec run ${
|
|
1045
|
+
console.log(`sillyspec run ${cmdStage} --wait --reason "${step.waitReason || '等待用户输入'}" --options "${(step.waitOptions || ['确认']).join(',')}"${changeFlag} --output "你的问题/方案摘要"`)
|
|
955
1046
|
console.log(``)
|
|
956
1047
|
console.log(`用户回答后执行:`)
|
|
957
|
-
console.log(`sillyspec run ${
|
|
1048
|
+
console.log(`sillyspec run ${cmdStage} --continue --answer "用户回答"${changeFlag}`)
|
|
958
1049
|
console.log(``)
|
|
959
1050
|
console.log(`收到回答并完成本步骤总结后,再执行:`)
|
|
960
1051
|
} else if (mayNeedWait) {
|
|
961
1052
|
console.log(`如果需要用户决策(选择方案/确认设计等):`)
|
|
962
|
-
console.log(`sillyspec run ${
|
|
1053
|
+
console.log(`sillyspec run ${cmdStage} --wait --reason "${step.waitReason || '等待原因'}" --options "${(step.waitOptions || ['选项1', '选项2']).join(',')}"${changeFlag} --output "你的摘要"`)
|
|
963
1054
|
console.log(``)
|
|
964
1055
|
console.log(`如果不需要用户决策,正常完成:`)
|
|
965
1056
|
}
|
|
966
|
-
console.log(
|
|
1057
|
+
console.log(doneCommand + (autoMeta ? '' : ' --input "用户原始需求/反馈"'))
|
|
1058
|
+
|
|
1059
|
+
// ── SS-META 机器可读元数据块(change: 2026-09-08-auto-driver,D-002@v1,FR-01)──
|
|
1060
|
+
// 仅 auto 模式(autoMeta 非空)渲染:单行 HTML 注释内 JSON,agent 低噪可读、脚本一行正则
|
|
1061
|
+
// 可提取(<!--SS-META:(.*)-->)。requiresUser 四源见上方纯函数;doneCommand 与正文同源。
|
|
1062
|
+
if (autoMeta) {
|
|
1063
|
+
const meta = {
|
|
1064
|
+
stage: stageName,
|
|
1065
|
+
stepIndex: stepIndex + 1,
|
|
1066
|
+
stepName: step.name,
|
|
1067
|
+
requiresUser: requiresWait || mayNeedWait || step.requiresConfirm === true,
|
|
1068
|
+
doneCommand,
|
|
1069
|
+
waitHint: (requiresWait || mayNeedWait)
|
|
1070
|
+
? 'requiresUser=true:先与用户交互(wait/continue 或 --wait-interactive 直通),再 done'
|
|
1071
|
+
: '直接执行任务后逐字跑 doneCommand',
|
|
1072
|
+
}
|
|
1073
|
+
console.log('<!--SS-META:' + JSON.stringify(meta) + '-->')
|
|
1074
|
+
}
|
|
967
1075
|
}
|
|
968
1076
|
/**
|
|
969
1077
|
* 替换 prompt 文本中的路径根占位符 {SPEC_ROOT}/{DOCS_ROOT}/{PROJECTS_ROOT}/
|
package/src/run/quick-audit.js
CHANGED
|
@@ -209,12 +209,15 @@ export async function resolveQuickLinkedChanges({ pm, cwd, specDir, quickFiles,
|
|
|
209
209
|
* npm test + npm run lint」从 agent 自律下沉为 CLI 卡点。
|
|
210
210
|
*
|
|
211
211
|
* 语义(对齐规则 8 + verify-postcheck 既有降级哲学):
|
|
212
|
-
* -
|
|
212
|
+
* - 变更文件未触及 src/ 或 test/(纯 doc/配置改动)→ skip(不跑不阻断)
|
|
213
213
|
* - 触及 → 亲自执行 local.yaml 的 commands.test / commands.lint(复用 verify 阶段
|
|
214
214
|
* 对账引擎 runVerifyTestCheck/runVerifyLintCheck,含 test_strategy/known_failures/
|
|
215
215
|
* 超时语义);未配置命令 → 内部 skipped,不阻断(兼容无测试项目)
|
|
216
216
|
* - 任一实测 failed → fail(调用方阻断 --done,step 回 pending)
|
|
217
217
|
* - 逃生门:SILLYSPEC_QUICK_TEST_GATE=skip 显式跳过(CI/特殊场景,审计留痕)
|
|
218
|
+
* - 倒推 B 模式兜底(2026-09-07,quick-f9138c2f 实证):代码先行会话的文件全部早于
|
|
219
|
+
* 启动时间窗 → 审计 changedFiles 为空 → 此前静默 skip;--files 声明边界
|
|
220
|
+
* (declaredFiles = guard.allowedFiles)明确触及 src/test 时实测仍应跑。
|
|
218
221
|
*
|
|
219
222
|
* verify-postcheck 顶层 import 链较重(contract-matrix 等),且本函数仅 quick --done
|
|
220
223
|
* 收尾路径调用——用动态 import,不进 run 命令通用启动路径(与上方 @inquirer/prompts 同款纪律)。
|
|
@@ -222,18 +225,22 @@ export async function resolveQuickLinkedChanges({ pm, cwd, specDir, quickFiles,
|
|
|
222
225
|
* @param {object} opts
|
|
223
226
|
* @param {string} opts.cwd - 仓库根(命令执行 cwd)
|
|
224
227
|
* @param {string} opts.specBase - .sillyspec 目录(local.yaml 读取源,信任边界见 runVerifyTestCheck SEC-02 注释)
|
|
225
|
-
* @param {string[]} [opts.changedFiles] - 审计口径的本轮变更文件(仓库根相对 POSIX 路径;空/null →
|
|
228
|
+
* @param {string[]} [opts.changedFiles] - 审计口径的本轮变更文件(仓库根相对 POSIX 路径;空/null → 回退 declaredFiles)
|
|
229
|
+
* @param {string[]} [opts.declaredFiles] - 会话声明边界(guard.allowedFiles;changedFiles 为空时的兜底判定源)
|
|
226
230
|
* @param {string} [opts.changeName] - quick 会话名(evidence-auto 策略解析用)
|
|
227
231
|
* @returns {Promise<{action:'pass'|'fail'|'skip', failed:string[], reason:string, test:object|null, lint:object|null}>}
|
|
228
232
|
*/
|
|
229
|
-
export async function runQuickTestLintGate({ cwd, specBase, changedFiles = [], changeName = null }) {
|
|
233
|
+
export async function runQuickTestLintGate({ cwd, specBase, changedFiles = [], declaredFiles = [], changeName = null }) {
|
|
230
234
|
if (process.env.SILLYSPEC_QUICK_TEST_GATE === 'skip') {
|
|
231
235
|
return { action: 'skip', failed: [], reason: 'SILLYSPEC_QUICK_TEST_GATE=skip 显式跳过(审计留痕)', test: null, lint: null }
|
|
232
236
|
}
|
|
233
|
-
const
|
|
237
|
+
const audited = Array.isArray(changedFiles) ? changedFiles : []
|
|
238
|
+
// 倒推 B 模式兜底:审计口径为空时回退声明边界(文件早于会话启动被基线吸收的场景)
|
|
239
|
+
const files = audited.length > 0 ? audited : (Array.isArray(declaredFiles) ? declaredFiles : [])
|
|
240
|
+
const fileSource = audited.length > 0 ? '审计' : (files.length > 0 ? '声明边界兜底(倒推 B:文件早于会话启动被基线吸收)' : '无')
|
|
234
241
|
const codeFiles = files.filter(f => typeof f === 'string' && (f.startsWith('src/') || f.startsWith('test/')))
|
|
235
242
|
if (files.length === 0) {
|
|
236
|
-
return { action: 'skip', failed: [], reason:
|
|
243
|
+
return { action: 'skip', failed: [], reason: `无变更文件清单(${fileSource}口径均空——brownfield 无 guard 或空审计),跳过`, test: null, lint: null }
|
|
237
244
|
}
|
|
238
245
|
if (codeFiles.length === 0) {
|
|
239
246
|
return { action: 'skip', failed: [], reason: `纯 doc/配置改动(${files.length} 个文件均未触及 src/test,规则 8 语义跳过 test+lint)`, test: null, lint: null }
|
package/src/run/scan-profile.js
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* - executeScanPostcheck 原冗余动态 import('fs'/'path'/'child_process') 删除,改顶部静态(execSync 实际未直接用,纯遗留)
|
|
10
10
|
*/
|
|
11
11
|
import { join, basename, extname } from 'node:path'
|
|
12
|
-
import { existsSync, readdirSync, statSync, mkdirSync, writeFileSync, unlinkSync } from 'node:fs'
|
|
13
|
-
import { safeGit } from './shared.js'
|
|
12
|
+
import { existsSync, readdirSync, statSync, mkdirSync, writeFileSync, unlinkSync, readFileSync } from 'node:fs'
|
|
13
|
+
import { safeGit, resolveRuntimeRoot } from './shared.js'
|
|
14
14
|
import { SCAN_REQUIRED_DOCS } from '../constants.js'
|
|
15
15
|
|
|
16
16
|
/**
|
|
@@ -327,6 +327,24 @@ export async function executeScanResumeCheck(cwd, platformOpts) {
|
|
|
327
327
|
console.log(` 📂 ${p}: ${states.join(' ')} ${facts}`)
|
|
328
328
|
}
|
|
329
329
|
console.log('')
|
|
330
|
+
|
|
331
|
+
// IR 增量回灌 advisory(change: 2026-09-07-ir-hardening,D-006@v1,FR-03):读
|
|
332
|
+
// .runtime/last-delta.json(归档/delta 补跑写入的最近变更快照),14 天窗口内提示本轮
|
|
333
|
+
// scan 优先核对其涉及模块——advisory 不阻断不改变步骤结构;缺失/过期/解析失败静默。
|
|
334
|
+
try {
|
|
335
|
+
const sidecarPath = join(resolveRuntimeRoot(platformOpts, specBase), 'last-delta.json')
|
|
336
|
+
if (existsSync(sidecarPath)) {
|
|
337
|
+
const sidecar = JSON.parse(readFileSync(sidecarPath, 'utf8'))
|
|
338
|
+
const ageMs = Date.now() - Date.parse(sidecar.updatedAt || '')
|
|
339
|
+
const FOURTEEN_DAYS = 14 * 24 * 60 * 60 * 1000
|
|
340
|
+
if (Number.isFinite(ageMs) && ageMs >= 0 && ageMs <= FOURTEEN_DAYS
|
|
341
|
+
&& Array.isArray(sidecar.affectedModules) && sidecar.affectedModules.length > 0) {
|
|
342
|
+
console.log(`🔁 增量回灌提示(上次归档变更 ${sidecar.change},${new Date(sidecar.updatedAt).toISOString().slice(0, 10)}):`)
|
|
343
|
+
console.log(` 其涉及模块 ${sidecar.affectedModules.join('、')} 的文档与 staleRefs 本轮 scan 优先核对(advisory,不据此跳过其他模块)`)
|
|
344
|
+
console.log('')
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
} catch { /* sidecar 读取/解析失败静默跳过(advisory 缺席零影响) */ }
|
|
330
348
|
}
|
|
331
349
|
|
|
332
350
|
/**
|
package/src/run/shared.js
CHANGED
|
@@ -107,19 +107,24 @@ export function resolvePromptIncludes(text) {
|
|
|
107
107
|
export function resolveSpecDir(cwd, opts = {}) {
|
|
108
108
|
if (opts.specDir) return resolve(opts.specDir)
|
|
109
109
|
const home = os.homedir()
|
|
110
|
-
|
|
110
|
+
const origin = resolve(cwd)
|
|
111
|
+
const originBelowHome = origin.startsWith(home + sep) // 严格子目录,不含 home 自身
|
|
112
|
+
let dir = origin
|
|
113
|
+
let passedHome = false
|
|
111
114
|
while (true) {
|
|
112
|
-
// home
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
+
// home 拒绝守卫:起点在 home 子树内时,home 层不检查 .sillyspec,
|
|
116
|
+
// 防止 home 子目录遍历经过 home 误命中 ~/.sillyspec;
|
|
117
|
+
// passedHome:经过 home 后不再检查(覆盖遍历到真实 home 的场景)。
|
|
118
|
+
if (!(passedHome || (dir === home && originBelowHome))) {
|
|
115
119
|
const candidate = join(dir, '.sillyspec')
|
|
116
120
|
if (existsSync(candidate)) return candidate
|
|
117
121
|
}
|
|
122
|
+
if (dir === home) passedHome = true
|
|
118
123
|
const parent = dirname(dir)
|
|
119
124
|
if (parent === dir) break
|
|
120
125
|
dir = parent
|
|
121
126
|
}
|
|
122
|
-
return join(
|
|
127
|
+
return join(origin, '.sillyspec')
|
|
123
128
|
}
|
|
124
129
|
|
|
125
130
|
/**
|