sillyspec 3.28.0 → 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 +462 -0
- package/src/constants.js +6 -0
- package/src/design-facts.js +418 -0
- package/src/dispatch/backends/sillyhub-mcp.js +17 -5
- package/src/docs-check.js +197 -34
- package/src/docs-migrate.js +124 -0
- package/src/doctor-diagnostics.js +11 -2
- package/src/endpoint-baseline.js +109 -0
- package/src/git-helper.js +31 -0
- package/src/index.js +381 -22
- package/src/modules.js +41 -12
- package/src/progress/change-registry.js +39 -0
- package/src/progress/consistency-doctor.js +124 -5
- package/src/progress/stage-machine.js +13 -2
- package/src/progress.js +5 -0
- package/src/quicklog.js +51 -0
- package/src/run/command.js +199 -5
- package/src/run/complete-handlers.js +60 -4
- package/src/run/complete.js +105 -6
- package/src/run/gates.js +353 -2
- package/src/run/progress-confirm.js +66 -0
- package/src/run/prompt.js +193 -15
- package/src/run/quick-audit.js +12 -5
- package/src/run/scan-profile.js +117 -7
- package/src/run/shared.js +22 -6
- package/src/run/stage.js +14 -1
- package/src/scan-diff.js +54 -1
- package/src/scan-facts.js +173 -0
- package/src/scan-postcheck.js +114 -3
- package/src/spec-sync.js +19 -2
- package/src/stage-review.js +62 -3
- package/src/stages/archive.js +5 -5
- package/src/stages/brainstorm-auto.js +2 -1
- package/src/stages/brainstorm.js +12 -20
- package/src/stages/execute.js +19 -21
- package/src/stages/plan-postcheck.js +194 -1
- package/src/stages/plan.js +8 -3
- package/src/stages/quick.js +4 -2
- package/src/stages/scan.js +44 -102
- package/src/stages/verify.js +18 -18
- package/src/task-review.js +200 -0
- package/src/taskcard.js +8 -1
- package/src/verify-postcheck.js +771 -29
- package/src/verify-probes.js +89 -10
- package/src/workflow.js +30 -0
- package/templates/agents-instruction.md +1 -1
- package/templates/prompts/taskcard-rules.md +5 -0
- package/templates/workflows/scan-docs.yaml +6 -0
- package/.claude/skills/verify-per-user/SKILL.md +0 -6
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 {
|
|
@@ -477,6 +502,109 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
477
502
|
}
|
|
478
503
|
}
|
|
479
504
|
|
|
505
|
+
// 机械事实底稿注入(IR P3c task-03,D-003@v1 三件套之三):brainstorm「加载项目上下文」Step2
|
|
506
|
+
// 的 {SCAN_FACTS} → docs/<project>/scan/_facts.md 全文(scan facts CLI 确定性抽取的端点/依赖/
|
|
507
|
+
// 规模底稿)+ 红线一句(措辞对齐 scan 子代理先例 stages/scan.js——禁止重新 grep 发现底稿覆盖
|
|
508
|
+
// 的机械事实,冲突以底稿为准),探索期直接消费省 token。>15KB 按 UTF-8 字节截断并提示跑
|
|
509
|
+
// `sillyspec scan facts` 刷新(design R-04);文件不存在/读取失败空注入(fail-soft,不留残留
|
|
510
|
+
// 占位符)。只挂 brainstorm:双重条件(stageName 判定 + 占位符仅存在于 brainstorm Step2
|
|
511
|
+
// prompt),与下方 buildModuleContextInjection(横跨 brainstorm/plan/execute 三阶段)相互独立。
|
|
512
|
+
if (stageName === 'brainstorm' && promptText.includes('{SCAN_FACTS}')) {
|
|
513
|
+
let factsInjected = ''
|
|
514
|
+
try {
|
|
515
|
+
const factsSpecBase = resolvePromptSpecBase(platformOpts, cwd)
|
|
516
|
+
const factsProjectName = dbProjectName || basename(cwd)
|
|
517
|
+
const factsPath = join(factsSpecBase, 'docs', factsProjectName, 'scan', '_facts.md')
|
|
518
|
+
if (existsSync(factsPath)) {
|
|
519
|
+
const factsBuf = readFileSync(factsPath) // Buffer:按 UTF-8 字节判 15KB 阈值
|
|
520
|
+
const FACTS_LIMIT = 15 * 1024
|
|
521
|
+
const factsBody = factsBuf.length > FACTS_LIMIT
|
|
522
|
+
? factsBuf.subarray(0, FACTS_LIMIT).toString('utf8') +
|
|
523
|
+
`\n\n(⚠️ 底稿超过 15KB 已截断——完整内容可读 \`${factsPath}\`;底稿已过期时可跑 \`sillyspec scan facts\` 刷新)`
|
|
524
|
+
: factsBuf.toString('utf8')
|
|
525
|
+
factsInjected =
|
|
526
|
+
'\n\n### 🧾 机械事实底稿(scan facts CLI 抽取,docs/' + factsProjectName + '/scan/_facts.md 全文注入)\n\n' +
|
|
527
|
+
factsBody +
|
|
528
|
+
'\n\n⛔ 红线(同 scan 子代理先例):禁止重新 grep 发现底稿覆盖的机械事实(端点/依赖/规模),冲突以底稿为准。\n'
|
|
529
|
+
}
|
|
530
|
+
} catch { /* fail-soft:读取失败空注入,不阻断 prompt 输出 */ }
|
|
531
|
+
promptText = promptText.replace(/\{SCAN_FACTS\}/g, factsInjected)
|
|
532
|
+
}
|
|
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
|
+
|
|
480
608
|
// 决策防复潮注入(W1.1 第 3 点,task-04,FR-05):brainstorm「加载项目上下文」Step2 的
|
|
481
609
|
// {DECISION_HITS} → matchKnowledge decisionHits——命中 rejected 条目时渲染「否决决策提示」段
|
|
482
610
|
// (ID/标题/否决理由/复潮条件),无命中替换为空串零输出(不留残留占位符);注入结果同步落
|
|
@@ -683,6 +811,35 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
683
811
|
}
|
|
684
812
|
}
|
|
685
813
|
|
|
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
|
+
// 无消费者,注入分支删除)。
|
|
818
|
+
|
|
819
|
+
// execute Step「确认 worktree 路径」meta 注入(2026-09-05 全流程审计):worktree meta 由 CLI
|
|
820
|
+
// 读出直接注入,agent 省一趟 worktree meta 工具往返;工具链可用性检查(真实工作)仍归 agent。
|
|
821
|
+
if (stageName === 'execute' && promptText.includes('{WORKTREE_META}')) {
|
|
822
|
+
let metaInfo
|
|
823
|
+
try {
|
|
824
|
+
const { WorktreeManager } = await import('../worktree.js')
|
|
825
|
+
const wm = new WorktreeManager({ cwd })
|
|
826
|
+
const meta = changeName ? wm.getMeta(changeName) : null
|
|
827
|
+
if (meta && meta.worktreePath) {
|
|
828
|
+
const dirOk = existsSync(meta.worktreePath)
|
|
829
|
+
metaInfo =
|
|
830
|
+
`- worktreePath:${meta.worktreePath}\n` +
|
|
831
|
+
`- branch:${meta.branch || '(meta 未记录)'}\n` +
|
|
832
|
+
`- mode:${meta.mode || 'worktree'}\n` +
|
|
833
|
+
`- worktree 目录存在:${dirOk ? '是' : '否——in-place 模式属正常;worktree/native-worktree 模式缺失则停止并报错'}`
|
|
834
|
+
} else {
|
|
835
|
+
metaInfo = '(worktree meta 不可读——meta.json 不存在说明创建失败,停止并报错;可运行 sillyspec worktree meta ' + (changeName || '<change-name>') + ' 自查)'
|
|
836
|
+
}
|
|
837
|
+
} catch (e) {
|
|
838
|
+
metaInfo = '(注入失败:' + e.message + '——运行 sillyspec worktree meta 自查,meta.json 不存在则停止并报错)'
|
|
839
|
+
}
|
|
840
|
+
promptText = promptText.split('{WORKTREE_META}').join(metaInfo)
|
|
841
|
+
}
|
|
842
|
+
|
|
686
843
|
// archive Step1「任务完成度检查」客观真相源注入:以 review.json verdict 算完成度,
|
|
687
844
|
// 替代「机械数 plan.md checkbox」(checkbox 依赖 autoCheckPlanFromReviews 回填,runId marker /
|
|
688
845
|
// review 缺失时回填静默 no-op,会停在未勾态导致完成度失真 → archive 误判「全未完成」)。
|
|
@@ -818,9 +975,9 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
818
975
|
}
|
|
819
976
|
|
|
820
977
|
// 完成契约(事前预知):该 stage 的机械校验通过条件,从 stage-contract-spec.js manifest 渲染,
|
|
821
|
-
// 与 CLI 完成校验严格同源(事前给的 == 事后查的)
|
|
822
|
-
//
|
|
823
|
-
if (stepIndex ===
|
|
978
|
+
// 与 CLI 完成校验严格同源(事前给的 == 事后查的)。仅首个 agent 可见步注入(与 persona/guardrails
|
|
979
|
+
// 同模式,一次建立即可;后续步靠 context 保留)。
|
|
980
|
+
if (stepIndex === firstRenderableIdx) {
|
|
824
981
|
const stageContract = renderStageContract(stageName)
|
|
825
982
|
if (stageContract) {
|
|
826
983
|
promptText = `${promptText}\n\n${stageContract}`
|
|
@@ -828,10 +985,10 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
828
985
|
}
|
|
829
986
|
|
|
830
987
|
console.log(promptText)
|
|
831
|
-
// 铁律拆分(W3 token
|
|
988
|
+
// 铁律拆分(W3 token 效率):通用流程纪律(文档优先/不跳步/不编造命令)只在首个 agent 可见步注入——
|
|
832
989
|
// 每步重复 ~800B 纯耗 context。但【平台写入规则 + 路径规则】是安全关键(防写错目录/绕过 Write),
|
|
833
990
|
// 且依赖 changeName/platformOpts,必须【每步注入】(context 压缩丢失会让 agent 越界写源码)。
|
|
834
|
-
if (stepIndex ===
|
|
991
|
+
if (stepIndex === firstRenderableIdx) {
|
|
835
992
|
console.log(`\n### ⚠️ 铁律`)
|
|
836
993
|
console.log('- 文档优先:代码产出必须先有对应的设计/规范文档支撑。')
|
|
837
994
|
console.log('- 聚焦本步骤:只执行本步骤描述的操作并完整做完;自行扩展或跳步会破坏状态机推进,后续步骤再做后续事。')
|
|
@@ -879,21 +1036,42 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
879
1036
|
// archive-batch-31-tool-notes ②:通用 --done 模板不带该 flag,agent 照抄执行撞
|
|
880
1037
|
// 「请添加 --confirm」确认门,误以为参数没带。
|
|
881
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 "你的摘要"`
|
|
882
1043
|
if (requiresWait) {
|
|
883
1044
|
console.log(`本步骤必须等待用户输入,不能直接 --done:`)
|
|
884
|
-
console.log(`sillyspec run ${
|
|
1045
|
+
console.log(`sillyspec run ${cmdStage} --wait --reason "${step.waitReason || '等待用户输入'}" --options "${(step.waitOptions || ['确认']).join(',')}"${changeFlag} --output "你的问题/方案摘要"`)
|
|
885
1046
|
console.log(``)
|
|
886
1047
|
console.log(`用户回答后执行:`)
|
|
887
|
-
console.log(`sillyspec run ${
|
|
1048
|
+
console.log(`sillyspec run ${cmdStage} --continue --answer "用户回答"${changeFlag}`)
|
|
888
1049
|
console.log(``)
|
|
889
1050
|
console.log(`收到回答并完成本步骤总结后,再执行:`)
|
|
890
1051
|
} else if (mayNeedWait) {
|
|
891
1052
|
console.log(`如果需要用户决策(选择方案/确认设计等):`)
|
|
892
|
-
console.log(`sillyspec run ${
|
|
1053
|
+
console.log(`sillyspec run ${cmdStage} --wait --reason "${step.waitReason || '等待原因'}" --options "${(step.waitOptions || ['选项1', '选项2']).join(',')}"${changeFlag} --output "你的摘要"`)
|
|
893
1054
|
console.log(``)
|
|
894
1055
|
console.log(`如果不需要用户决策,正常完成:`)
|
|
895
1056
|
}
|
|
896
|
-
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
|
+
}
|
|
897
1075
|
}
|
|
898
1076
|
/**
|
|
899
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,9 @@
|
|
|
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 } 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
|
+
import { SCAN_REQUIRED_DOCS } from '../constants.js'
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* 估算源码规模(文件数 + 字节数)—— 跨平台 Node 遍历,替代 Unix `find`(原生 Windows 无 find,
|
|
@@ -130,19 +131,21 @@ export function buildQuickScanSteps() {
|
|
|
130
131
|
当前为 quick profile(小项目自动判定 或 用户显式 --quick),一次性生成核心文档用于快速接入。
|
|
131
132
|
|
|
132
133
|
### 操作
|
|
133
|
-
1.
|
|
134
|
-
2.
|
|
134
|
+
1. 先跑 \`sillyspec scan facts\`(CLI 确定性抽取端点/依赖/规模,产物 {DOCS_ROOT}/scan/_facts.md,零 token)——PROJECT/ARCHITECTURE 的技术栈与集成事实以底稿为准,不重复 grep 发现
|
|
135
|
+
2. 读取项目结构和关键文件(package.json / pyproject.toml / README / 入口文件)
|
|
136
|
+
3. 生成以下 4 份文档并写入 \`{DOCS_ROOT}/scan/\`:
|
|
135
137
|
- **PROJECT.md** — 项目简介、技术栈、模块划分
|
|
136
138
|
- **ARCHITECTURE.md** — 架构概览、模块关系、技术决策
|
|
137
139
|
- **CONVENTIONS.md** — 代码风格、框架隐形规则
|
|
138
140
|
- **STRUCTURE.md** — 目录树 + 模块说明
|
|
139
|
-
|
|
141
|
+
4. 如发现子项目,注册到 \`{PROJECTS_ROOT}/\` 下
|
|
140
142
|
|
|
141
|
-
|
|
143
|
+
每份文档正文第 1 行直接写中文标题;frontmatter(author / created_at / source_commit / updated_at / generator / scan_depth: quick——标记快速接入的浅层版本,后续深度扫描 --deep 会识别此标记并覆盖升级)**由 CLI 在终检时自动注入,你不需要写 frontmatter、也不要跑 git 取值**。
|
|
142
144
|
|
|
143
145
|
### ⛔ 硬约束
|
|
144
146
|
- **严禁使用子代理(Agent/Task 工具)。** 所有文档在一个 turn 内完成。
|
|
145
147
|
- 不要搜索 .sillyspec/ .claude/ .git/ node_modules/ dist/ build/
|
|
148
|
+
- 关键事实(模块职责/技术栈组件/外部集成)须带反引号 path:line 引用(如 \`src/db.js:42\`,真实可核验——CLI postcheck 逐条核验);不确定就不写行号,**禁止编造**
|
|
146
149
|
- --output 只需要列出生成的文件名,不要写长篇总结
|
|
147
150
|
|
|
148
151
|
### 输出
|
|
@@ -219,8 +222,11 @@ export async function executeScanPreflight(cwd, platformOpts, scanProfile) {
|
|
|
219
222
|
*/
|
|
220
223
|
export async function executeScanPostcheck(cwd, platformOpts, scanProfile) {
|
|
221
224
|
// scan-postcheck.js 在 src/,本模块在 src/run/ → 退一层(真环依赖,保留动态 import)
|
|
222
|
-
const { runScanPostCheck, printScanPostCheckResult } = await import('../scan-postcheck.js')
|
|
225
|
+
const { runScanPostCheck, printScanPostCheckResult, stampScanDocHeaders } = await import('../scan-postcheck.js')
|
|
223
226
|
const specDir = platformOpts?.specRoot || null
|
|
227
|
+
// ② frontmatter CLI 盖章(幂等,只补缺不覆盖):quick 档注入含 scan_depth: quick
|
|
228
|
+
const stamped = stampScanDocHeaders({ cwd, specDir, mode: scanProfile?.mode })
|
|
229
|
+
if (stamped.fixed.length > 0) console.log(` 📝 frontmatter 已由 CLI 注入 ${stamped.fixed.length} 份文档`)
|
|
224
230
|
const result = runScanPostCheck({
|
|
225
231
|
cwd,
|
|
226
232
|
specDir,
|
|
@@ -268,3 +274,107 @@ export async function executeScanPostcheck(cwd, platformOpts, scanProfile) {
|
|
|
268
274
|
}
|
|
269
275
|
}
|
|
270
276
|
}
|
|
277
|
+
|
|
278
|
+
// ─── noAI 化步骤动作(2026-09-05 ①:ls 级操作不值整轮 agent 往返) ─────────────
|
|
279
|
+
|
|
280
|
+
/** 已注册项目清单(projects/*.yaml 文件名去后缀);无注册时回退 [仓库根 basename](单项目口径) */
|
|
281
|
+
function listRegisteredProjects(specBase, cwd) {
|
|
282
|
+
const projectsRoot = join(specBase, 'projects')
|
|
283
|
+
try {
|
|
284
|
+
const names = readdirSync(projectsRoot).filter(f => f.endsWith('.yaml')).map(f => f.replace(/\.yaml$/, ''))
|
|
285
|
+
if (names.length > 0) return names
|
|
286
|
+
} catch { /* 无 projects 目录 → 回退单项目 */ }
|
|
287
|
+
return [basename(cwd)]
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* CLI-only: 子项目探测(scan 步骤 1 的 noAI 化)。
|
|
292
|
+
* 顶层目录 × 构建文件存在性 → 打印建议清单;**只建议不注册**(原红线语义不变,注册仍由用户确认)。
|
|
293
|
+
*/
|
|
294
|
+
export async function executeScanDetectProjects(cwd, platformOpts) {
|
|
295
|
+
const specBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
|
|
296
|
+
const projectsRoot = join(specBase, 'projects')
|
|
297
|
+
const buildFiles = [
|
|
298
|
+
['package.json', 'Node.js'], ['pom.xml', 'Java/Maven'], ['build.gradle', 'Java/Gradle'],
|
|
299
|
+
['pyproject.toml', 'Python'], ['requirements.txt', 'Python'], ['go.mod', 'Go'], ['Cargo.toml', 'Rust'],
|
|
300
|
+
]
|
|
301
|
+
const skip = new Set(['node_modules', '.git', '.sillyspec', 'dist', 'build', 'out', 'target', 'coverage', '.claude'])
|
|
302
|
+
console.log(' 📁 子项目探测(CLI,只建议不注册——注册需用户在后续步骤确认)')
|
|
303
|
+
let found = 0
|
|
304
|
+
try {
|
|
305
|
+
for (const e of readdirSync(cwd, { withFileTypes: true })) {
|
|
306
|
+
if (!e.isDirectory() || skip.has(e.name) || e.name.startsWith('.')) continue
|
|
307
|
+
const hit = buildFiles.find(([f]) => existsSync(join(cwd, e.name, f)))
|
|
308
|
+
if (!hit) continue
|
|
309
|
+
found++
|
|
310
|
+
const registered = existsSync(join(projectsRoot, `${e.name}.yaml`))
|
|
311
|
+
console.log(` - ${e.name}:${hit[1]} · ${registered ? '已注册' : '未注册(是否注册由用户确认)'}`)
|
|
312
|
+
}
|
|
313
|
+
} catch { /* 顶层目录读取失败 → 空清单 */ }
|
|
314
|
+
if (found === 0) console.log(' (未发现独立子项目,按单项目扫描)')
|
|
315
|
+
console.log('')
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* CLI-only: 断点续扫检测(scan 步骤 4 的 noAI 化)。existsSync 级检查,一次遍历全部已注册项目。
|
|
320
|
+
*/
|
|
321
|
+
export async function executeScanResumeCheck(cwd, platformOpts) {
|
|
322
|
+
const specBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
|
|
323
|
+
for (const p of listRegisteredProjects(specBase, cwd)) {
|
|
324
|
+
const scanDir = join(specBase, 'docs', p, 'scan')
|
|
325
|
+
const states = SCAN_REQUIRED_DOCS.map(d => existsSync(join(scanDir, d)) ? `✅${d.replace('.md', '')}` : `⬜${d.replace('.md', '')}`)
|
|
326
|
+
const facts = existsSync(join(scanDir, '_facts.md')) ? '· 底稿✅' : '· 底稿⬜'
|
|
327
|
+
console.log(` 📂 ${p}: ${states.join(' ')} ${facts}`)
|
|
328
|
+
}
|
|
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 缺席零影响) */ }
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* CLI-only: scan 终检(步骤 11「自检和提交」的 noAI 化,含 ② frontmatter 盖章)。
|
|
352
|
+
* 顺序:frontmatter 注入(幂等)→ 清理 _env-detect.md → runScanPostCheck(本地轻量/平台严格)
|
|
353
|
+
* → failed_post_check 时 throw(step 保持 pending)→ 非平台 git add 扫描产物(不 commit)。
|
|
354
|
+
*/
|
|
355
|
+
export async function executeScanFinalize(cwd, platformOpts) {
|
|
356
|
+
const specDir = platformOpts?.specRoot || null
|
|
357
|
+
const specBase = specDir || join(cwd, '.sillyspec')
|
|
358
|
+
const { stampScanDocHeaders, runScanPostCheck, printScanPostCheckResult } = await import('../scan-postcheck.js')
|
|
359
|
+
|
|
360
|
+
const stamped = stampScanDocHeaders({ cwd, specDir })
|
|
361
|
+
if (stamped.fixed.length > 0) console.log(` 📝 frontmatter 已由 CLI 注入 ${stamped.fixed.length} 份文档(${stamped.stampedKeys} 个键,只补缺不覆盖)`)
|
|
362
|
+
|
|
363
|
+
for (const p of listRegisteredProjects(specBase, cwd)) {
|
|
364
|
+
const tmp = join(specBase, 'docs', p, 'scan', '_env-detect.md')
|
|
365
|
+
try { if (existsSync(tmp)) { unlinkSync(tmp); console.log(` 🧹 已清理 ${p}/scan/_env-detect.md`) } } catch { /* 清理失败不阻断 */ }
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const result = runScanPostCheck({ cwd, specDir })
|
|
369
|
+
printScanPostCheckResult(result)
|
|
370
|
+
// 不 throw:平台模式的 manifest/指针/exit(1) 由 handleScanStageCompleted 在阶段完成时统一落
|
|
371
|
+
//(throw 会拦在完成前 → manifest.json 永不写入,破坏 SillyHub 消费契约,实测
|
|
372
|
+
// run-complete-step-scan-platform Case 1);本地模式的告警打印/postcheck-result.json 同理
|
|
373
|
+
// 由 complete-handlers 非平台收尾分支负责。本动作只负责:盖章 + 清理 + 即时报告 + 暂存。
|
|
374
|
+
|
|
375
|
+
if (!specDir) {
|
|
376
|
+
const r = safeGit(cwd, ['add', '.sillyspec/docs', '.sillyspec/knowledge'])
|
|
377
|
+
if (r && r.error) console.warn(` ⚠️ git add 扫描产物失败(可手动处理)`)
|
|
378
|
+
else console.log(' 📦 已暂存扫描产物 .sillyspec/docs 与 .sillyspec/knowledge(未提交,由统一提交工具处理)')
|
|
379
|
+
}
|
|
380
|
+
}
|
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
|
/**
|
|
@@ -614,13 +619,24 @@ export function resolveChangeDir(cwd, progress, specDir = null) {
|
|
|
614
619
|
// 仍在跑,平台可能已接受推送而客户端当作超时放弃(spec 树推送无 base_ts 自愈兜底)。
|
|
615
620
|
const SYNC_TOTAL_TIMEOUT_MS = 8_000
|
|
616
621
|
|
|
622
|
+
// 熔断总预算 env 开关(坑 spec-sync-aborted-looks-exception / 债 E22b 场景实证,2026-09-07):
|
|
623
|
+
// 真实平台执行环境后端偶发 >8s 响应,8s 熔断间歇触发(warn 文案见 spec-sync.js
|
|
624
|
+
// describeSyncError)。SILLYSPEC_SYNC_TIMEOUT_MS 允许平台环境放宽总预算;代价是 --done
|
|
625
|
+
// 最坏等待同比例变长(e2c9f90 治的体感 hang 需自行权衡)。整数毫秒,[1000, 120000],
|
|
626
|
+
// 非法/越界一律回退默认 8s。raceWithAbort 默认参数逐次求值——进程内改 env 即时生效。
|
|
627
|
+
export function resolveSyncTotalTimeoutMs() {
|
|
628
|
+
const raw = Number(process.env.SILLYSPEC_SYNC_TIMEOUT_MS)
|
|
629
|
+
if (Number.isInteger(raw) && raw >= 1_000 && raw <= 120_000) return raw
|
|
630
|
+
return SYNC_TOTAL_TIMEOUT_MS
|
|
631
|
+
}
|
|
632
|
+
|
|
617
633
|
/**
|
|
618
634
|
* 熔断 race + 确定性取消:超时触发 controller.abort() 并 resolve,在飞 fetch 收到
|
|
619
635
|
* AbortError 后自然失败(各调用方 catch 返回降级值,进程不挂起)。
|
|
620
636
|
* @param {() => Promise} op 接收 { signal } 的异步操作
|
|
621
637
|
* @param {number} timeoutMs 熔断时长(测试可缩短)
|
|
622
638
|
*/
|
|
623
|
-
async function raceWithAbort(op, timeoutMs =
|
|
639
|
+
async function raceWithAbort(op, timeoutMs = resolveSyncTotalTimeoutMs()) {
|
|
624
640
|
const controller = new AbortController()
|
|
625
641
|
let timer
|
|
626
642
|
try {
|
package/src/run/stage.js
CHANGED
|
@@ -20,7 +20,8 @@ import { join, dirname } from 'node:path'
|
|
|
20
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, predictProtectedQuickFiles, mergeQuickBoundaryFiles, detectEmptyShellQuickSessions } from './shared.js'
|
|
23
|
-
import { computeScanProfile, applyScanProfileSteps, executeScanPreflight, executeScanPostcheck } from './scan-profile.js'
|
|
23
|
+
import { computeScanProfile, applyScanProfileSteps, executeScanPreflight, executeScanPostcheck, executeScanDetectProjects, executeScanResumeCheck, executeScanFinalize } from './scan-profile.js'
|
|
24
|
+
import { executeProgressConfirm } from './progress-confirm.js'
|
|
24
25
|
import { outputStep, collectStageWaitHistory } from './prompt.js'
|
|
25
26
|
import { allocateQuicklogEntry, deriveTitleFromLinkedChange, sanitizeDesc } from '../quicklog.js'
|
|
26
27
|
import { createHash } from 'node:crypto'
|
|
@@ -529,8 +530,18 @@ export async function runStage(pm, progress, stageName, cwd, changeName, skipApp
|
|
|
529
530
|
await executeScanPreflight(cwd, platformOpts, scanProfile)
|
|
530
531
|
} else if (cliAction === 'scanPostcheck') {
|
|
531
532
|
await executeScanPostcheck(cwd, platformOpts, scanProfile)
|
|
533
|
+
} else if (cliAction === 'scanDetectProjects') {
|
|
534
|
+
await executeScanDetectProjects(cwd, platformOpts)
|
|
535
|
+
} else if (cliAction === 'scanResumeCheck') {
|
|
536
|
+
await executeScanResumeCheck(cwd, platformOpts)
|
|
537
|
+
} else if (cliAction === 'scanFinalize') {
|
|
538
|
+
await executeScanFinalize(cwd, platformOpts)
|
|
532
539
|
} else if (cliAction === 'planPostcheck') {
|
|
533
540
|
await executePlanPostcheck(cwd, platformOpts, progress)
|
|
541
|
+
} else if (cliAction === 'progressConfirm') {
|
|
542
|
+
// 主流程 step1「进度确认」noAI 化(2026-09-07):brainstorm/execute/verify——
|
|
543
|
+
// 快照本就 CLI 注入、阶段路由已由 run 落定,省一轮「复述摘要→--done」往返
|
|
544
|
+
executeProgressConfirm({ stageName, cwd, stageData, changeName, pm })
|
|
534
545
|
} else {
|
|
535
546
|
throw new Error(`noAI 步骤 ${stepName} 的未知 _cliAction: ${cliAction}——请在 stage.js 注册对应分支`)
|
|
536
547
|
}
|
|
@@ -546,6 +557,8 @@ export async function runStage(pm, progress, stageName, cwd, changeName, skipApp
|
|
|
546
557
|
// 所有步骤完成
|
|
547
558
|
stageData.status = 'completed'
|
|
548
559
|
stageData.completedAt = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
560
|
+
// 主阶段完成钉 currentStage(troubleshooting #56 根因修复,2026-09-08;与 complete.js 两处同语义)
|
|
561
|
+
if (!AUXILIARY_STAGES.includes(stageName)) progress.currentStage = stageName
|
|
549
562
|
// persist _write 移到 completeStageGates 成功之后(task-02 / review-2026-08-09 #2):gate 异常/失败 → rollback 回 in-progress 落盘,此处未到 _write,DB 不留假 completed。
|
|
550
563
|
// 阶段完成收尾共享管线(noAI 末步核心修复 S1:plan postcheck independent-tier review verdict=fail /
|
|
551
564
|
// 平台 scan manifest 此前被绕过)。gate 失败已 rollback 为 in-progress,early-return(不 fall through 到末尾 return)。
|