sillyspec 3.17.15 → 3.18.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/docs/platform-scan-protocol.md +298 -0
- package/package.json +1 -1
- package/src/constants.js +70 -0
- package/src/db.js +4 -0
- package/src/hooks/worktree-guard.js +97 -4
- package/src/index.js +56 -1
- package/src/progress.js +41 -14
- package/src/run.js +315 -83
- package/src/scan-postcheck.js +17 -16
- package/src/stage-contract.js +244 -12
- package/src/stages/brainstorm.js +228 -8
- package/src/stages/index.js +0 -2
- package/src/stages/plan.js +237 -52
- package/src/stages/propose.js +30 -4
- package/src/stages/quick.js +13 -10
- package/src/stages/scan.js +12 -0
- package/src/stages/verify.js +31 -13
- package/src/workflow.js +1 -0
- package/test/platform-artifacts.test.mjs +190 -0
- package/test/platform-failure-samples.test.mjs +195 -0
- package/test/platform-recovery-chain.test.mjs +179 -0
- package/test/platform-recovery.test.mjs +14 -6
- package/test/platform-scan-p0.test.mjs +5 -2
- package/test/run-tests.mjs +31 -3
- package/test/scan-paths.test.mjs +1 -1
- package/test/scan-postcheck.test.mjs +4 -3
- package/test/spec-dir.test.mjs +3 -2
- package/test/stage-contract.test.mjs +120 -7
- package/test/stage-definitions.test.mjs +2 -6
- package/test/wait-gates.test.mjs +501 -0
- package/test/worktree-guard.test.mjs +58 -0
package/src/run.js
CHANGED
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
* CLI 成为流程引擎,AI 变成步骤执行器。
|
|
5
5
|
* 支持多变更并行:每个变更状态存储在 sillyspec.db 中。
|
|
6
6
|
*/
|
|
7
|
-
import { basename, join, resolve } from 'path'
|
|
7
|
+
import { basename, join, resolve, dirname } from 'path'
|
|
8
8
|
import { existsSync, readdirSync, mkdirSync, writeFileSync, appendFileSync, readFileSync, rmSync, statSync } from 'fs'
|
|
9
9
|
import { createRequire } from 'module'
|
|
10
10
|
const require = createRequire(import.meta.url)
|
|
11
11
|
import { ProgressManager } from './progress.js'
|
|
12
|
+
import { SCAN_STATUS, POINTER_STATUS, isPointerCorrupted } from './constants.js'
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* 在容器/Docker 环境下,git 可能因目录所有权不匹配报 dubious ownership。
|
|
@@ -47,16 +48,49 @@ function formatWaitOptions(raw) {
|
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
/**
|
|
52
|
+
* 格式化 repeatableWait 步骤的历史用户回答,注入到重新输出的 step prompt 前。
|
|
53
|
+
* @param {object} step - progress 中的 step 对象(含 waitAnswers 数组)
|
|
54
|
+
* @returns {string|null} 格式化的历史文本,或 null(无历史)
|
|
55
|
+
*/
|
|
56
|
+
function formatWaitHistory(step) {
|
|
57
|
+
const answers = Array.isArray(step.waitAnswers) ? step.waitAnswers : []
|
|
58
|
+
if (answers.length === 0) return null
|
|
59
|
+
let text = `本步骤历史用户回答(共 ${answers.length} 轮):\n`
|
|
60
|
+
for (const item of answers) {
|
|
61
|
+
text += `\n${item.round}. ${item.answer}`
|
|
62
|
+
if (item.question) {
|
|
63
|
+
text += `\n 对应问题/摘要:${item.question}`
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const maxRounds = step.maxWaitRounds || null
|
|
67
|
+
if (maxRounds && answers.length >= maxRounds) {
|
|
68
|
+
text += `\n\n已达到 maxWaitRounds=${maxRounds}。请基于以上回答总结需求;除非仍有阻塞问题,否则完成本步骤并进入方案讨论。`
|
|
69
|
+
} else {
|
|
70
|
+
text += `\n\n请判断信息是否足够:如果足够,完成本步骤;如果仍缺关键约束,再提出一个问题并 --wait。`
|
|
71
|
+
}
|
|
72
|
+
return text
|
|
73
|
+
}
|
|
74
|
+
|
|
50
75
|
/**
|
|
51
76
|
* 解析规范目录路径
|
|
52
|
-
*
|
|
77
|
+
* 向上查找含 .sillyspec 的祖先目录,类似 git 找 .git 的逻辑。
|
|
78
|
+
* @param {string} cwd - 项目根目录(或子目录)
|
|
53
79
|
* @param {object} [opts]
|
|
54
80
|
* @param {string} [opts.specDir] - 用户指定的 specDir(通过 --spec-dir 或 --spec-root)
|
|
55
81
|
* @returns {string} 规范目录的绝对路径
|
|
56
82
|
*/
|
|
57
83
|
function resolveSpecDir(cwd, opts = {}) {
|
|
58
84
|
if (opts.specDir) return resolve(opts.specDir)
|
|
59
|
-
|
|
85
|
+
let dir = resolve(cwd)
|
|
86
|
+
while (true) {
|
|
87
|
+
const candidate = join(dir, '.sillyspec')
|
|
88
|
+
if (existsSync(candidate)) return candidate
|
|
89
|
+
const parent = dirname(dir)
|
|
90
|
+
if (parent === dir) break
|
|
91
|
+
dir = parent
|
|
92
|
+
}
|
|
93
|
+
return join(resolve(cwd), '.sillyspec')
|
|
60
94
|
}
|
|
61
95
|
import { stageRegistry, auxiliaryStages } from './stages/index.js'
|
|
62
96
|
import { checkTransition, runValidators } from './stage-contract.js'
|
|
@@ -223,19 +257,34 @@ async function auditQuickCompletion(cwd, guard, options = {}) {
|
|
|
223
257
|
const gitStatus = execSync('git status --porcelain', { cwd, encoding: 'utf8', timeout: 10000 })
|
|
224
258
|
const currentEntries = gitStatus.trim().split('\n').filter(Boolean)
|
|
225
259
|
|
|
260
|
+
const normalizeGitPath = (p) => p.replace(/\\/g, '/')
|
|
261
|
+
const isQuickMetadata = (p) => {
|
|
262
|
+
const file = normalizeGitPath(p)
|
|
263
|
+
return file.startsWith('.sillyspec/quicklog/')
|
|
264
|
+
|| file.startsWith('.sillyspec/.runtime/')
|
|
265
|
+
|| file === '.sillyspec/knowledge/uncategorized.md'
|
|
266
|
+
|| (/^\.sillyspec\/docs\/[^/]+\/modules\/[^/]+\.md$/.test(file))
|
|
267
|
+
|| (/^\.sillyspec\/docs\/[^/]+\/modules\/_module-map\.yaml$/.test(file))
|
|
268
|
+
}
|
|
226
269
|
const DANGEROUS_PATTERNS = [
|
|
227
|
-
'.sillyspec/',
|
|
228
270
|
'package.json',
|
|
229
271
|
'package-lock.json',
|
|
230
272
|
'yarn.lock',
|
|
231
273
|
'pnpm-lock.yaml',
|
|
232
274
|
'.eslintrc',
|
|
233
275
|
'tsconfig.json',
|
|
276
|
+
'src/db.js',
|
|
277
|
+
'src/progress.js',
|
|
278
|
+
'src/run.js',
|
|
279
|
+
'src/stage-contract.js',
|
|
280
|
+
'src/worktree.js',
|
|
281
|
+
'src/worktree-apply.js',
|
|
282
|
+
'src/hooks/',
|
|
234
283
|
]
|
|
235
284
|
|
|
236
285
|
for (const entry of currentEntries) {
|
|
237
286
|
const status = entry.slice(0, 2).trim()
|
|
238
|
-
const file = entry.slice(3).trim()
|
|
287
|
+
const file = normalizeGitPath(entry.slice(3).trim())
|
|
239
288
|
if (!file || file.startsWith('??. ')) continue
|
|
240
289
|
|
|
241
290
|
result.changedFiles.push(file)
|
|
@@ -248,7 +297,11 @@ async function auditQuickCompletion(cwd, guard, options = {}) {
|
|
|
248
297
|
}
|
|
249
298
|
|
|
250
299
|
// 检查危险文件(除非 force-baseline)
|
|
251
|
-
if (
|
|
300
|
+
if (file.startsWith('.sillyspec/') && !isQuickMetadata(file) && !forceBaseline) {
|
|
301
|
+
result.reasons.push(`危险文件变更: ${file}`)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (DANGEROUS_PATTERNS.some(p => file === p || file.startsWith(p)) && !forceBaseline) {
|
|
252
305
|
result.reasons.push(`危险文件变更: ${file}`)
|
|
253
306
|
}
|
|
254
307
|
}
|
|
@@ -268,7 +321,7 @@ async function auditQuickCompletion(cwd, guard, options = {}) {
|
|
|
268
321
|
// 检查 new files(除非 allow-new)
|
|
269
322
|
if (!allowNew) {
|
|
270
323
|
for (const f of result.newFiles) {
|
|
271
|
-
if (!
|
|
324
|
+
if (!isQuickMetadata(f)) {
|
|
272
325
|
result.reasons.push(`新增文件(需 --allow-new): ${f}`)
|
|
273
326
|
}
|
|
274
327
|
}
|
|
@@ -277,7 +330,7 @@ async function auditQuickCompletion(cwd, guard, options = {}) {
|
|
|
277
330
|
// 检查 allowedFiles 范围
|
|
278
331
|
if (allowedFiles.length > 0) {
|
|
279
332
|
for (const f of result.changedFiles) {
|
|
280
|
-
if (!allowedFiles.includes(f) && !f
|
|
333
|
+
if (!allowedFiles.includes(f) && !isQuickMetadata(f)) {
|
|
281
334
|
result.reasons.push(`超出 allowedFiles: ${f}`)
|
|
282
335
|
}
|
|
283
336
|
}
|
|
@@ -336,6 +389,23 @@ async function auditQuickCompletion(cwd, guard, options = {}) {
|
|
|
336
389
|
return result
|
|
337
390
|
}
|
|
338
391
|
|
|
392
|
+
function printQuickAuditReview(review) {
|
|
393
|
+
if (review.status === 'blocked') {
|
|
394
|
+
console.error(`\n🚫 quick 变更边界审计 — BLOCKED:`)
|
|
395
|
+
for (const r of review.reasons) {
|
|
396
|
+
console.error(` - ${r}`)
|
|
397
|
+
}
|
|
398
|
+
console.error(`\n quick 已停止:请恢复/拆分这些变更,或重新运行 quick 并显式声明范围。`)
|
|
399
|
+
} else if (review.status === 'warning') {
|
|
400
|
+
console.warn(`\n⚠️ quick 变更边界审计 — WARNING:`)
|
|
401
|
+
for (const r of review.reasons) {
|
|
402
|
+
console.warn(` - ${r}`)
|
|
403
|
+
}
|
|
404
|
+
} else {
|
|
405
|
+
console.log(`\n✅ quick 变更边界审计 — SAFE (变更 ${review.changedFiles.length} 个文件)`)
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
339
409
|
async function triggerSync(cwd, changeName, platformOpts = {}) {
|
|
340
410
|
// 平台模式(SillyHub)走自己的回传链路,不走 CLI 内置 sync
|
|
341
411
|
if (platformOpts?.specRoot || platformOpts?.runtimeRoot) return
|
|
@@ -613,21 +683,29 @@ async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjec
|
|
|
613
683
|
}
|
|
614
684
|
profileDirectives.push(`--output 只需要列出文件名,不要写长篇总结。`)
|
|
615
685
|
promptText = profileDirectives.join('\n') + '\n\n' + promptText
|
|
686
|
+
|
|
687
|
+
// scanProfile 分支也要替换占位符(非 platform 模式也会走到这里)
|
|
688
|
+
const _pName = dbProjectName || basename(cwd)
|
|
689
|
+
const _specSS = platformOpts?.specRoot || join(cwd, '.sillyspec')
|
|
690
|
+
const _docsRoot = join(_specSS, 'docs', _pName)
|
|
691
|
+
promptText = promptText.replace(/\{DOCS_ROOT\}/g, _docsRoot)
|
|
692
|
+
promptText = promptText.replace(/\{PROJECTS_ROOT\}/g, join(_specSS, 'projects'))
|
|
693
|
+
promptText = promptText.replace(/\{WORKFLOWS_ROOT\}/g, join(_specSS, 'workflows'))
|
|
694
|
+
promptText = promptText.replace(/\{KNOWLEDGE_ROOT\}/g, join(_specSS, 'knowledge'))
|
|
695
|
+
promptText = promptText.replace(/\{SPEC_ROOT\}/g, _specSS)
|
|
616
696
|
} else {
|
|
617
697
|
// 非 platform 模式也要替换占位符
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
promptText = promptText.replace(/\{SPEC_ROOT\}/g, specSillyspec)
|
|
630
|
-
}
|
|
698
|
+
const projectName = dbProjectName || basename(cwd)
|
|
699
|
+
const specSillyspec = join(cwd, '.sillyspec')
|
|
700
|
+
const docsRoot = join(specSillyspec, 'docs', projectName)
|
|
701
|
+
const projectsRoot = join(specSillyspec, 'projects')
|
|
702
|
+
const workflowsRoot = join(specSillyspec, 'workflows')
|
|
703
|
+
const knowledgeRoot = join(specSillyspec, 'knowledge')
|
|
704
|
+
promptText = promptText.replace(/\{DOCS_ROOT\}/g, docsRoot)
|
|
705
|
+
promptText = promptText.replace(/\{PROJECTS_ROOT\}/g, projectsRoot)
|
|
706
|
+
promptText = promptText.replace(/\{WORKFLOWS_ROOT\}/g, workflowsRoot)
|
|
707
|
+
promptText = promptText.replace(/\{KNOWLEDGE_ROOT\}/g, knowledgeRoot)
|
|
708
|
+
promptText = promptText.replace(/\{SPEC_ROOT\}/g, specSillyspec)
|
|
631
709
|
}
|
|
632
710
|
|
|
633
711
|
// 注入模块上下文(brainstorm/plan/execute 阶段,基于 Module Context Index)
|
|
@@ -688,11 +766,22 @@ async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjec
|
|
|
688
766
|
const changeFlag = changeName ? ` --change ${changeName}` : ''
|
|
689
767
|
// 检测当前 step prompt 是否包含 WAIT 指令(即可能需要等待用户)
|
|
690
768
|
const stepPrompt = promptText || ''
|
|
691
|
-
const
|
|
769
|
+
const requiresWait = step.requiresWait === true
|
|
770
|
+
const conditionalWait = step.conditionalWait === true
|
|
771
|
+
const mayNeedWait = WAIT_MARKER_RE.test(stepPrompt) || requiresWait || conditionalWait
|
|
772
|
+
|
|
692
773
|
console.log(`\n### 完成后执行`)
|
|
693
|
-
if (
|
|
774
|
+
if (requiresWait) {
|
|
775
|
+
console.log(`本步骤必须等待用户输入,不能直接 --done:`)
|
|
776
|
+
console.log(`sillyspec run ${stageName} --wait --reason "${step.waitReason || '等待用户输入'}" --options "${(step.waitOptions || ['确认']).join(',')}"${changeFlag} --output "你的问题/方案摘要"`)
|
|
777
|
+
console.log(``)
|
|
778
|
+
console.log(`用户回答后执行:`)
|
|
779
|
+
console.log(`sillyspec run ${stageName} --continue --answer "用户回答"${changeFlag}`)
|
|
780
|
+
console.log(``)
|
|
781
|
+
console.log(`收到回答并完成本步骤总结后,再执行:`)
|
|
782
|
+
} else if (mayNeedWait) {
|
|
694
783
|
console.log(`如果需要用户决策(选择方案/确认设计等):`)
|
|
695
|
-
console.log(`sillyspec run ${stageName} --wait --reason "等待原因" --options "选项1
|
|
784
|
+
console.log(`sillyspec run ${stageName} --wait --reason "${step.waitReason || '等待原因'}" --options "${(step.waitOptions || ['选项1', '选项2']).join(',')}"${changeFlag} --output "你的摘要"`)
|
|
696
785
|
console.log(``)
|
|
697
786
|
console.log(`如果不需要用户决策,正常完成:`)
|
|
698
787
|
}
|
|
@@ -864,9 +953,14 @@ async function executeScanPostcheck(cwd, platformOpts, scanProfile) {
|
|
|
864
953
|
const { execSync } = await import('child_process')
|
|
865
954
|
const manifestDir = platformOpts.specRoot
|
|
866
955
|
let sourceCommit = null
|
|
956
|
+
let sourceCommitError = null
|
|
867
957
|
try {
|
|
868
|
-
const
|
|
869
|
-
|
|
958
|
+
const gitResult = safeGit(cwd, ['rev-parse', 'HEAD'])
|
|
959
|
+
sourceCommit = gitResult.value
|
|
960
|
+
sourceCommitError = gitResult.error
|
|
961
|
+
} catch (e) {
|
|
962
|
+
sourceCommitError = e.message
|
|
963
|
+
}
|
|
870
964
|
mkdirSync(manifestDir, { recursive: true })
|
|
871
965
|
const manifest = {
|
|
872
966
|
scan_profile: {
|
|
@@ -879,7 +973,7 @@ async function executeScanPostcheck(cwd, platformOpts, scanProfile) {
|
|
|
879
973
|
workspace_id: platformOpts.workspaceId || null,
|
|
880
974
|
scan_run_id: platformOpts.scanRunId || null,
|
|
881
975
|
source_commit: sourceCommit,
|
|
882
|
-
source_commit_error: sourceCommit === null ? (
|
|
976
|
+
source_commit_error: sourceCommit === null ? (sourceCommitError || 'unknown') : undefined,
|
|
883
977
|
generated_at: new Date().toISOString(),
|
|
884
978
|
schema_version: 2,
|
|
885
979
|
}
|
|
@@ -1048,6 +1142,7 @@ export async function runCommand(args, cwd, specDir = null) {
|
|
|
1048
1142
|
|
|
1049
1143
|
const isAllowNew = flags.includes('--allow-new')
|
|
1050
1144
|
const isForceBaseline = flags.includes('--force-baseline')
|
|
1145
|
+
const isForceRescan = flags.includes('--force-rescan')
|
|
1051
1146
|
|
|
1052
1147
|
// 未知参数 fail-fast
|
|
1053
1148
|
const knownFlags = new Set([
|
|
@@ -1056,7 +1151,7 @@ export async function runCommand(args, cwd, specDir = null) {
|
|
|
1056
1151
|
'--reason', '--options', '--answer', '--confirm-mode',
|
|
1057
1152
|
'--output', '--input', '--change',
|
|
1058
1153
|
'--spec-dir', '--spec-root', '--runtime-root', '--workspace-id', '--scan-run-id',
|
|
1059
|
-
'--files', '--allow-new', '--force-baseline',
|
|
1154
|
+
'--files', '--allow-new', '--force-baseline', '--force-rescan',
|
|
1060
1155
|
'--json', '--dir', '--help',
|
|
1061
1156
|
])
|
|
1062
1157
|
for (let i = 0; i < flags.length; i++) {
|
|
@@ -1093,8 +1188,8 @@ export async function runCommand(args, cwd, specDir = null) {
|
|
|
1093
1188
|
progress = { currentStage: stageName, stages: {}, lastActive: new Date().toLocaleString('zh-CN', { hour12: false }), project: '' }
|
|
1094
1189
|
}
|
|
1095
1190
|
} else {
|
|
1096
|
-
// brainstorm
|
|
1097
|
-
if (stageName === 'brainstorm'
|
|
1191
|
+
// brainstorm 作为流程入口,自动生成变更名并初始化
|
|
1192
|
+
if (stageName === 'brainstorm') {
|
|
1098
1193
|
const date = new Date().toISOString().slice(0, 10)
|
|
1099
1194
|
const autoName = `${date}-new-change`
|
|
1100
1195
|
console.log(`🔄 自动创建变更:${autoName}`)
|
|
@@ -1162,7 +1257,7 @@ export async function runCommand(args, cwd, specDir = null) {
|
|
|
1162
1257
|
}
|
|
1163
1258
|
|
|
1164
1259
|
// 默认:输出当前步骤
|
|
1165
|
-
return await runStage(pm, progress, stageName, cwd, effectiveChange, isSkipApproval, platformOpts, { quickFiles, isAllowNew, isForceBaseline })
|
|
1260
|
+
return await runStage(pm, progress, stageName, cwd, effectiveChange, isSkipApproval, platformOpts, { quickFiles, isAllowNew, isForceBaseline, isForceRescan })
|
|
1166
1261
|
}
|
|
1167
1262
|
|
|
1168
1263
|
/**
|
|
@@ -1178,6 +1273,7 @@ function resolveChangeNameAuto(cwd, specDir = null) {
|
|
|
1178
1273
|
}
|
|
1179
1274
|
|
|
1180
1275
|
async function runStage(pm, progress, stageName, cwd, changeName, skipApproval = false, platformOpts = {}, quickOpts = {}) {
|
|
1276
|
+
const specBase = platformOpts.specRoot || join(cwd, '.sillyspec')
|
|
1181
1277
|
// 状态转换校验
|
|
1182
1278
|
const prevStage = progress.currentStage || ''
|
|
1183
1279
|
const transition = checkTransition(prevStage, stageName)
|
|
@@ -1259,6 +1355,28 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
|
|
|
1259
1355
|
}
|
|
1260
1356
|
if (scanProfile) platformOpts.scanProfile = scanProfile
|
|
1261
1357
|
|
|
1358
|
+
if (stageName === 'scan') {
|
|
1359
|
+
try {
|
|
1360
|
+
const gitResult = safeGit(cwd, ['rev-parse', 'HEAD'])
|
|
1361
|
+
const scanGuard = {
|
|
1362
|
+
sourceCommit: gitResult.value,
|
|
1363
|
+
sourceCommitError: gitResult.error,
|
|
1364
|
+
startedAt: new Date().toISOString(),
|
|
1365
|
+
forceRescan: quickOpts?.isForceRescan || false,
|
|
1366
|
+
}
|
|
1367
|
+
const guardFile = join(specBase, '.runtime', 'scan-guard.json')
|
|
1368
|
+
mkdirSync(dirname(guardFile), { recursive: true })
|
|
1369
|
+
writeFileSync(guardFile, JSON.stringify(scanGuard, null, 2) + '\n')
|
|
1370
|
+
if (scanGuard.forceRescan) {
|
|
1371
|
+
console.log('🛡️ scan 覆盖保护已记录: --force-rescan 已开启')
|
|
1372
|
+
} else {
|
|
1373
|
+
console.log('🛡️ scan 覆盖保护已记录: existing scan docs require current source_commit/updated_at')
|
|
1374
|
+
}
|
|
1375
|
+
} catch (e) {
|
|
1376
|
+
console.warn(`⚠️ scan 覆盖保护记录失败: ${e.message}`)
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1262
1380
|
if (currentIdx === -1) {
|
|
1263
1381
|
// 已完成 → 自动重置,重新开始
|
|
1264
1382
|
const freshSteps = await getStageSteps(stageName, cwd, progress)
|
|
@@ -1391,9 +1509,8 @@ function validateFileLocations(cwd, stageName, progress, changeName, specBase) {
|
|
|
1391
1509
|
|
|
1392
1510
|
// 每个阶段完成后预期存在的文件
|
|
1393
1511
|
const expectedFiles = {
|
|
1394
|
-
|
|
1512
|
+
brainstorm: ['design.md', 'proposal.md', 'requirements.md', 'tasks.md'],
|
|
1395
1513
|
plan: ['plan.md'],
|
|
1396
|
-
verify: ['verify-result.md'],
|
|
1397
1514
|
archive: ['module-impact.md'],
|
|
1398
1515
|
}
|
|
1399
1516
|
|
|
@@ -1425,7 +1542,7 @@ function validateFileLocations(cwd, stageName, progress, changeName, specBase) {
|
|
|
1425
1542
|
}
|
|
1426
1543
|
}
|
|
1427
1544
|
|
|
1428
|
-
async function archiveChangeDirectory(pm, cwd, progress) {
|
|
1545
|
+
async function archiveChangeDirectory(pm, cwd, progress, specBase) {
|
|
1429
1546
|
const { renameSync } = await import('fs')
|
|
1430
1547
|
const archiveChangeName = progress.currentChange
|
|
1431
1548
|
if (!archiveChangeName) {
|
|
@@ -1477,6 +1594,28 @@ async function waitStep(pm, progress, stageName, cwd, outputText, waitReason, wa
|
|
|
1477
1594
|
process.exit(1)
|
|
1478
1595
|
}
|
|
1479
1596
|
|
|
1597
|
+
// 前置检查:不允许已有 waiting 步骤时再 --wait
|
|
1598
|
+
const existingWaitingIdx = stageData.steps.findIndex(s => s.status === 'waiting')
|
|
1599
|
+
if (existingWaitingIdx !== -1) {
|
|
1600
|
+
const ws = stageData.steps[existingWaitingIdx]
|
|
1601
|
+
console.error(`❌ 已有步骤处于等待状态:Step ${existingWaitingIdx + 1} "${ws.name}"`)
|
|
1602
|
+
console.error(` 请先 --continue 或 --reset 该步骤,再开始新的 --wait`)
|
|
1603
|
+
process.exit(1)
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
// maxWaitRounds 硬上限:达到后拒绝继续 --wait
|
|
1607
|
+
const currentStep = stageData.steps[currentIdx]
|
|
1608
|
+
const defSteps = await getStageSteps(stageName, cwd, progress, platformOpts?.specRoot || null)
|
|
1609
|
+
const stepDef = defSteps?.[currentIdx] || {}
|
|
1610
|
+
const maxWaitRounds = currentStep.maxWaitRounds ?? stepDef.maxWaitRounds
|
|
1611
|
+
const currentWaitRound = currentStep.waitRound || 0
|
|
1612
|
+
if (maxWaitRounds && currentWaitRound >= maxWaitRounds) {
|
|
1613
|
+
console.error(`❌ Step "${currentStep.name}" 已达到最大等待轮次(maxWaitRounds=${maxWaitRounds})`)
|
|
1614
|
+
console.error(` 请基于已有回答完成本步骤:`)
|
|
1615
|
+
console.error(` sillyspec run ${stageName} --done${changeName ? ` --change ${changeName}` : ''} --output "需求理解摘要"`)
|
|
1616
|
+
process.exit(1)
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1480
1619
|
// 非交互模式下拒绝等待
|
|
1481
1620
|
if (nonInteractive) {
|
|
1482
1621
|
console.error(`❌ Human decision required in non-interactive mode.`)
|
|
@@ -1551,38 +1690,82 @@ async function continueStep(pm, progress, stageName, cwd, answer, options = {})
|
|
|
1551
1690
|
process.exit(1)
|
|
1552
1691
|
}
|
|
1553
1692
|
const currentIdx = waitingSteps[0].idx
|
|
1693
|
+
const defSteps = await getStageSteps(stageName, cwd, progress, platformOpts?.specRoot || null)
|
|
1694
|
+
const currentStepDef = defSteps?.[currentIdx] || {}
|
|
1695
|
+
const currentStep = stageData.steps[currentIdx]
|
|
1696
|
+
const isRepeatableWait = currentStepDef.repeatableWait === true || currentStep.repeatableWait === true
|
|
1697
|
+
const requiresWait = currentStepDef.requiresWait === true || currentStep.requiresWait === true
|
|
1698
|
+
const shouldReturnToCurrentStep = isRepeatableWait || requiresWait
|
|
1554
1699
|
|
|
1555
1700
|
const now = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1701
|
+
const prevOutput = currentStep.output || ''
|
|
1702
|
+
const waitRound = (currentStep.waitRound || 0) + 1
|
|
1703
|
+
currentStep.waitRound = waitRound
|
|
1704
|
+
currentStep.waitAnswer = answer
|
|
1705
|
+
currentStep.waitAnswers = Array.isArray(currentStep.waitAnswers) ? currentStep.waitAnswers : []
|
|
1706
|
+
currentStep.waitAnswers.push({
|
|
1707
|
+
round: waitRound,
|
|
1708
|
+
answer,
|
|
1709
|
+
question: prevOutput || null,
|
|
1710
|
+
answeredAt: now,
|
|
1711
|
+
})
|
|
1712
|
+
currentStep.maxWaitRounds = currentStepDef.maxWaitRounds ?? currentStep.maxWaitRounds
|
|
1559
1713
|
|
|
1560
1714
|
// 合并 waiting 信息到 output
|
|
1561
|
-
const
|
|
1562
|
-
const waitInfo = stageData.steps[currentIdx].waitReason || ''
|
|
1715
|
+
const waitInfo = currentStep.waitReason || ''
|
|
1563
1716
|
if (waitInfo) {
|
|
1564
|
-
|
|
1565
|
-
? `${prevOutput} |
|
|
1566
|
-
:
|
|
1717
|
+
currentStep.output = prevOutput
|
|
1718
|
+
? `${prevOutput} | 用户回答#${waitRound}:${answer}`
|
|
1719
|
+
: `用户回答#${waitRound}:${answer}`
|
|
1567
1720
|
}
|
|
1568
1721
|
|
|
1569
1722
|
// 清除等待状态
|
|
1570
|
-
delete
|
|
1571
|
-
delete
|
|
1572
|
-
delete
|
|
1723
|
+
delete currentStep.waitReason
|
|
1724
|
+
delete currentStep.waitOptions
|
|
1725
|
+
delete currentStep.waitedAt
|
|
1726
|
+
|
|
1727
|
+
if (shouldReturnToCurrentStep) {
|
|
1728
|
+
currentStep.status = 'pending'
|
|
1729
|
+
currentStep.completedAt = null
|
|
1730
|
+
} else {
|
|
1731
|
+
currentStep.status = 'completed'
|
|
1732
|
+
currentStep.completedAt = now
|
|
1733
|
+
}
|
|
1573
1734
|
|
|
1574
1735
|
progress.lastActive = now
|
|
1575
1736
|
await pm._write(cwd, progress, changeName)
|
|
1576
1737
|
triggerSync(cwd, changeName, platformOpts)
|
|
1577
1738
|
|
|
1578
|
-
console.log(`✅ Step ${currentIdx + 1}/${stageData.steps.length} 已继续:${
|
|
1739
|
+
console.log(`✅ Step ${currentIdx + 1}/${stageData.steps.length} 已继续:${currentStep.name}`)
|
|
1579
1740
|
console.log(` 回答:${answer}`)
|
|
1580
1741
|
|
|
1581
1742
|
// Append to user-inputs.md
|
|
1582
1743
|
const inputsPath = join(specBase, '.runtime', 'user-inputs.md')
|
|
1583
|
-
const entry = `\n## ${now} | ${changeName || '?'} | ${stageName}: ${
|
|
1744
|
+
const entry = `\n## ${now} | ${changeName || '?'} | ${stageName}: ${currentStep.name} [CONTINUED]\n- 回答:${answer}\n`
|
|
1584
1745
|
appendFileSync(inputsPath, entry)
|
|
1585
1746
|
|
|
1747
|
+
// shouldReturnToCurrentStep: 回到当前步骤继续执行(repeatable=多轮探索,requiresWait=确认后执行动作)
|
|
1748
|
+
if (shouldReturnToCurrentStep) {
|
|
1749
|
+
console.log(`\n🔁 Step ${currentIdx + 1}/${stageData.steps.length} 已收到用户输入,回到当前步骤继续执行。`)
|
|
1750
|
+
if (isRepeatableWait) {
|
|
1751
|
+
console.log(` 已收集回答轮次:${waitRound}${currentStep.maxWaitRounds ? `/${currentStep.maxWaitRounds}` : ''}`)
|
|
1752
|
+
}
|
|
1753
|
+
if (defSteps && defSteps[currentIdx]) {
|
|
1754
|
+
console.log('')
|
|
1755
|
+
await outputStep(
|
|
1756
|
+
stageName,
|
|
1757
|
+
currentIdx,
|
|
1758
|
+
defSteps,
|
|
1759
|
+
cwd,
|
|
1760
|
+
changeName,
|
|
1761
|
+
progress.project || null,
|
|
1762
|
+
platformOpts,
|
|
1763
|
+
formatWaitHistory(currentStep)
|
|
1764
|
+
)
|
|
1765
|
+
}
|
|
1766
|
+
return { stageCompleted: false, currentIdx, nextPendingIdx: currentIdx }
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1586
1769
|
// 检查阶段是否全部完成
|
|
1587
1770
|
const nextPendingIdx = stageData.steps.findIndex(s => s.status === 'pending')
|
|
1588
1771
|
const nextWaitingIdx = stageData.steps.findIndex(s => s.status === 'waiting')
|
|
@@ -1595,7 +1778,6 @@ async function continueStep(pm, progress, stageName, cwd, answer, options = {})
|
|
|
1595
1778
|
}
|
|
1596
1779
|
|
|
1597
1780
|
// 输出下一步
|
|
1598
|
-
const defSteps = await getStageSteps(stageName, cwd, progress)
|
|
1599
1781
|
if (nextPendingIdx !== -1 && defSteps) {
|
|
1600
1782
|
console.log('')
|
|
1601
1783
|
await outputStep(stageName, nextPendingIdx, defSteps, cwd, changeName, progress.project || null, platformOpts, answer)
|
|
@@ -1640,6 +1822,25 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1640
1822
|
|
|
1641
1823
|
const steps = stageData.steps
|
|
1642
1824
|
const currentIdx = steps.findIndex(s => s.status === 'pending' || s.status === 'in-progress')
|
|
1825
|
+
if (currentIdx === -1) {
|
|
1826
|
+
console.error('没有待完成的步骤')
|
|
1827
|
+
process.exit(1)
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
// ── requiresWait 硬门控 ──
|
|
1831
|
+
const defStepsForCurrent = await getStageSteps(stageName, cwd, progress, platformOpts?.specRoot || null)
|
|
1832
|
+
const currentStepDef = defStepsForCurrent?.[currentIdx] || {}
|
|
1833
|
+
const currentStep = steps[currentIdx]
|
|
1834
|
+
if (currentStepDef.requiresWait === true && !currentStep.waitAnswer) {
|
|
1835
|
+
console.error(`❌ Step "${currentStep.name}" 必须先等待用户输入,不能直接 --done。`)
|
|
1836
|
+
console.error(` 原因:${currentStepDef.waitReason || '该步骤需要人工确认/回答'}`)
|
|
1837
|
+
if (currentStepDef.waitOptions) {
|
|
1838
|
+
console.error(` 选项:${currentStepDef.waitOptions.join(', ')}`)
|
|
1839
|
+
}
|
|
1840
|
+
console.error(` 请先执行:`)
|
|
1841
|
+
console.error(` sillyspec run ${stageName} --wait --reason "${currentStepDef.waitReason || '等待用户输入'}" --options "${(currentStepDef.waitOptions || ['确认']).join(',')}"${changeName ? ` --change ${changeName}` : ''} --output "你的问题/方案摘要"`)
|
|
1842
|
+
process.exit(1)
|
|
1843
|
+
}
|
|
1643
1844
|
|
|
1644
1845
|
steps[currentIdx].status = 'completed'
|
|
1645
1846
|
steps[currentIdx].completedAt = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
@@ -1670,7 +1871,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1670
1871
|
console.log('⚠️ 请添加 --confirm 确认归档,例如:sillyspec run archive --done --confirm --output "确认归档"')
|
|
1671
1872
|
return { stageCompleted: false, currentIdx, nextPendingIdx: currentIdx }
|
|
1672
1873
|
}
|
|
1673
|
-
await archiveChangeDirectory(pm, cwd, progress)
|
|
1874
|
+
await archiveChangeDirectory(pm, cwd, progress, specBase)
|
|
1674
1875
|
}
|
|
1675
1876
|
|
|
1676
1877
|
// archive "确认归档" 步骤完成后,校验归档完整性
|
|
@@ -1872,6 +2073,25 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1872
2073
|
console.error(` 请先创建 quicklog 记录再 --done,或使用 --skip-approval 跳过此校验。`)
|
|
1873
2074
|
return { stageCompleted: false, currentIdx, nextPendingIdx: -1 }
|
|
1874
2075
|
}
|
|
2076
|
+
if (progress.quickGuard) {
|
|
2077
|
+
const review = await auditQuickCompletion(cwd, progress.quickGuard, { isConfirm })
|
|
2078
|
+
progress.quickGuard.review = review
|
|
2079
|
+
progress.quickGuard.completedAt = new Date().toISOString()
|
|
2080
|
+
printQuickAuditReview(review)
|
|
2081
|
+
if (review.status === 'blocked') {
|
|
2082
|
+
steps[currentIdx].status = 'pending'
|
|
2083
|
+
steps[currentIdx].completedAt = null
|
|
2084
|
+
if (outputText) steps[currentIdx].output = null
|
|
2085
|
+
process.exit(1)
|
|
2086
|
+
}
|
|
2087
|
+
try {
|
|
2088
|
+
const { unlinkSync } = await import('fs')
|
|
2089
|
+
const guardFile = join(specBase, '.runtime', 'quick-guard.json')
|
|
2090
|
+
unlinkSync(guardFile)
|
|
2091
|
+
} catch {}
|
|
2092
|
+
progress.lastQuickReview = review
|
|
2093
|
+
delete progress.quickGuard
|
|
2094
|
+
}
|
|
1875
2095
|
}
|
|
1876
2096
|
|
|
1877
2097
|
stageData.status = 'completed'
|
|
@@ -1891,22 +2111,36 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1891
2111
|
if (stageName === 'scan' && (platformOpts.specRoot || platformOpts.runtimeRoot)) {
|
|
1892
2112
|
try {
|
|
1893
2113
|
stageData.scanMeta = stageData.scanMeta || {}; stageData.scanMeta.manifestWritten = false; // 默认失败
|
|
1894
|
-
const { mkdirSync, writeFileSync } = await import('fs')
|
|
2114
|
+
const { mkdirSync, writeFileSync, readFileSync: _readFileSync } = await import('fs')
|
|
1895
2115
|
const { join } = await import('path')
|
|
1896
2116
|
const { execSync } = await import('child_process')
|
|
1897
2117
|
const manifestDir = platformOpts.specRoot
|
|
1898
2118
|
mkdirSync(manifestDir, { recursive: true })
|
|
1899
2119
|
let sourceCommit = null
|
|
2120
|
+
let sourceCommitError = null
|
|
1900
2121
|
try {
|
|
1901
|
-
const
|
|
1902
|
-
|
|
2122
|
+
const gitResult = safeGit(cwd, ['rev-parse', 'HEAD'])
|
|
2123
|
+
sourceCommit = gitResult.value
|
|
2124
|
+
sourceCommitError = gitResult.error
|
|
2125
|
+
} catch (e) {
|
|
2126
|
+
sourceCommitError = e.message
|
|
2127
|
+
}
|
|
1903
2128
|
const manifest = {
|
|
1904
2129
|
workspace_id: platformOpts.workspaceId || null,
|
|
1905
2130
|
scan_run_id: platformOpts.scanRunId || null,
|
|
2131
|
+
source_root: cwd,
|
|
2132
|
+
spec_root: platformOpts.specRoot || null,
|
|
2133
|
+
runtime_root: platformOpts.runtimeRoot || null,
|
|
1906
2134
|
source_commit: sourceCommit,
|
|
1907
|
-
source_commit_error: sourceCommit === null ? (
|
|
2135
|
+
source_commit_error: sourceCommit === null ? (sourceCommitError || 'unknown') : undefined,
|
|
1908
2136
|
generated_at: new Date().toISOString(),
|
|
1909
2137
|
schema_version: 1,
|
|
2138
|
+
postcheck_result_path: null,
|
|
2139
|
+
workflow_runs_dir: platformOpts.runtimeRoot
|
|
2140
|
+
? join(platformOpts.runtimeRoot, 'scan-runs', platformOpts.scanRunId || 'unknown', 'workflow-runs')
|
|
2141
|
+
: null,
|
|
2142
|
+
platform_pointer_path: join(cwd, '.sillyspec-platform.json'),
|
|
2143
|
+
platform_pointer_status: POINTER_STATUS.ACTIVE,
|
|
1910
2144
|
}
|
|
1911
2145
|
const manifestPath = join(manifestDir, 'manifest.json')
|
|
1912
2146
|
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n')
|
|
@@ -1947,6 +2181,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1947
2181
|
})
|
|
1948
2182
|
if (postcheckJsonPath) {
|
|
1949
2183
|
console.log(`📄 postcheck-result.json 已写入: ${postcheckJsonPath}`)
|
|
2184
|
+
manifest.postcheck_result_path = postcheckJsonPath
|
|
1950
2185
|
}
|
|
1951
2186
|
|
|
1952
2187
|
// 将 post-check 结果写入 manifest
|
|
@@ -1958,9 +2193,19 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1958
2193
|
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n')
|
|
1959
2194
|
console.log(`📄 manifest.json 已更新(含 post-check 结果)`)
|
|
1960
2195
|
|
|
2196
|
+
// 更新平台指针状态为 scan_completed
|
|
2197
|
+
const pointerPath = join(cwd, '.sillyspec-platform.json')
|
|
2198
|
+
try {
|
|
2199
|
+
const pointer = JSON.parse(_readFileSync(pointerPath, 'utf8'))
|
|
2200
|
+
pointer.status = POINTER_STATUS.SCAN_COMPLETED
|
|
2201
|
+
pointer.completedAt = new Date().toISOString()
|
|
2202
|
+
pointer.scanStatus = postResult.status
|
|
2203
|
+
writeFileSync(pointerPath, JSON.stringify(pointer, null, 2) + '\n')
|
|
2204
|
+
} catch {}
|
|
2205
|
+
|
|
1961
2206
|
// failed_post_check 时强制阻止 clean success
|
|
1962
2207
|
if (postResult.status === 'failed_post_check') {
|
|
1963
|
-
stageData.status =
|
|
2208
|
+
stageData.status = SCAN_STATUS.FAILED_POST_CHECK
|
|
1964
2209
|
stageData.completedAt = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
1965
2210
|
await pm._write(cwd, progress, changeName)
|
|
1966
2211
|
console.error(`\n❌ scan post-check 失败,状态设为 failed_post_check。不允许 clean success。`)
|
|
@@ -2056,33 +2301,6 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
2056
2301
|
}
|
|
2057
2302
|
}
|
|
2058
2303
|
|
|
2059
|
-
// quick 阶段完成审计
|
|
2060
|
-
if (stageName === 'quick' && progress.quickGuard) {
|
|
2061
|
-
const review = await auditQuickCompletion(cwd, progress.quickGuard, { isConfirm })
|
|
2062
|
-
progress.quickGuard.review = review
|
|
2063
|
-
progress.quickGuard.completedAt = new Date().toISOString()
|
|
2064
|
-
// 清理 quick-guard.json
|
|
2065
|
-
try {
|
|
2066
|
-
const { unlinkSync } = await import('fs')
|
|
2067
|
-
const guardFile = join(specBase, '.runtime', 'quick-guard.json')
|
|
2068
|
-
unlinkSync(guardFile)
|
|
2069
|
-
} catch {}
|
|
2070
|
-
if (review.status === 'blocked') {
|
|
2071
|
-
console.error(`\n🚫 quick 变更边界审计 — BLOCKED:`)
|
|
2072
|
-
for (const r of review.reasons) {
|
|
2073
|
-
console.error(` - ${r}`)
|
|
2074
|
-
}
|
|
2075
|
-
console.error(`\n 这些文件是 baseline 保护的,不应被修改。`)
|
|
2076
|
-
} else if (review.status === 'warning') {
|
|
2077
|
-
console.warn(`\n⚠️ quick 变更边界审计 — WARNING:`)
|
|
2078
|
-
for (const r of review.reasons) {
|
|
2079
|
-
console.warn(` - ${r}`)
|
|
2080
|
-
}
|
|
2081
|
-
} else {
|
|
2082
|
-
console.log(`\n✅ quick 变更边界审计 — SAFE (变更 ${review.changedFiles.length} 个文件)`)
|
|
2083
|
-
}
|
|
2084
|
-
}
|
|
2085
|
-
|
|
2086
2304
|
progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
2087
2305
|
await pm._write(cwd, progress, changeName)
|
|
2088
2306
|
triggerSync(cwd, changeName, platformOpts)
|
|
@@ -2135,7 +2353,14 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
2135
2353
|
console.log(rp.prompt)
|
|
2136
2354
|
}
|
|
2137
2355
|
}
|
|
2138
|
-
const saved = saveWorkflowRun(result, {
|
|
2356
|
+
const saved = saveWorkflowRun(result, {
|
|
2357
|
+
cwd,
|
|
2358
|
+
source: 'run.js',
|
|
2359
|
+
stage: 'scan',
|
|
2360
|
+
step: steps[currentIdx]?.name,
|
|
2361
|
+
...(platformOpts.runtimeRoot ? { runtimeRoot: platformOpts.runtimeRoot } : {}),
|
|
2362
|
+
...(platformOpts.scanRunId ? { scanRunId: platformOpts.scanRunId } : {})
|
|
2363
|
+
})
|
|
2139
2364
|
if (saved) console.log(`📁 结果已归档:${saved}`)
|
|
2140
2365
|
}
|
|
2141
2366
|
if (anyFailed) {
|
|
@@ -2165,7 +2390,14 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
2165
2390
|
console.log(` └─ ${f}`)
|
|
2166
2391
|
}
|
|
2167
2392
|
}
|
|
2168
|
-
const saved = saveWorkflowRun(result, {
|
|
2393
|
+
const saved = saveWorkflowRun(result, {
|
|
2394
|
+
cwd,
|
|
2395
|
+
source: 'run.js',
|
|
2396
|
+
stage: 'archive',
|
|
2397
|
+
step: steps[currentIdx]?.name,
|
|
2398
|
+
...(platformOpts.runtimeRoot ? { runtimeRoot: platformOpts.runtimeRoot } : {}),
|
|
2399
|
+
...(platformOpts.scanRunId ? { scanRunId: platformOpts.scanRunId } : {})
|
|
2400
|
+
})
|
|
2169
2401
|
if (saved) console.log(`📁 结果已归档:${saved}`)
|
|
2170
2402
|
}
|
|
2171
2403
|
} catch (e) {
|