sillyspec 3.17.5 → 3.17.6
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/package.json +1 -1
- package/src/db.js +6 -0
- package/src/progress.js +25 -8
- package/src/run.js +290 -29
- package/src/stages/archive.js +6 -3
- package/src/stages/brainstorm.js +59 -31
- package/src/stages/execute.js +7 -9
- package/src/stages/plan.js +4 -1
- package/src/stages/scan.js +7 -4
package/package.json
CHANGED
package/src/db.js
CHANGED
|
@@ -169,6 +169,12 @@ export class DB {
|
|
|
169
169
|
this._migrateAddColumn('changes', 'isolation_status', 'TEXT');
|
|
170
170
|
this._migrateAddColumn('changes', 'isolation_mode', 'TEXT');
|
|
171
171
|
this._migrateAddColumn('changes', 'isolation_reason', 'TEXT');
|
|
172
|
+
|
|
173
|
+
// Migration: add waiting support columns to steps table (idempotent)
|
|
174
|
+
this._migrateAddColumn('steps', 'wait_reason', 'TEXT');
|
|
175
|
+
this._migrateAddColumn('steps', 'wait_options', 'TEXT');
|
|
176
|
+
this._migrateAddColumn('steps', 'wait_answer', 'TEXT');
|
|
177
|
+
this._migrateAddColumn('steps', 'waited_at', 'TEXT');
|
|
172
178
|
}
|
|
173
179
|
|
|
174
180
|
/**
|
package/src/progress.js
CHANGED
|
@@ -22,7 +22,7 @@ const CHANGES_SUBDIR = 'changes';
|
|
|
22
22
|
const GLOBAL_FILE = 'global.json';
|
|
23
23
|
const CURRENT_VERSION = 3;
|
|
24
24
|
const VALID_STAGES = ['scan', 'brainstorm', 'propose', 'plan', 'execute', 'verify', 'archive', 'quick', 'explore'];
|
|
25
|
-
const VALID_STATUSES = ['pending', 'in-progress', 'completed', 'failed', 'blocked'];
|
|
25
|
+
const VALID_STATUSES = ['pending', 'in-progress', 'completed', 'failed', 'blocked', 'waiting'];
|
|
26
26
|
|
|
27
27
|
const STAGE_LABELS = {
|
|
28
28
|
brainstorm: '🧠 需求探索',
|
|
@@ -207,16 +207,22 @@ export class ProgressManager {
|
|
|
207
207
|
if (stageIds.length > 0) {
|
|
208
208
|
const placeholders = stageIds.map(() => '?').join(',');
|
|
209
209
|
stepRows = sqlDb.exec(
|
|
210
|
-
`SELECT stage_id, name, status, output, completed_at, ordering FROM steps WHERE stage_id IN (${placeholders}) ORDER BY stage_id, ordering`,
|
|
210
|
+
`SELECT stage_id, name, status, output, completed_at, ordering, wait_reason, wait_options, wait_answer, waited_at FROM steps WHERE stage_id IN (${placeholders}) ORDER BY stage_id, ordering`,
|
|
211
211
|
stageIds
|
|
212
212
|
);
|
|
213
213
|
}
|
|
214
214
|
// 按阶段分组步骤
|
|
215
215
|
const stepsByStage = {};
|
|
216
216
|
if (stepRows && stepRows.length > 0) {
|
|
217
|
-
for (const [stageId, name, status, output, completedAt, ordering] of stepRows[0].values) {
|
|
217
|
+
for (const [stageId, name, status, output, completedAt, ordering, waitReason, waitOptions, waitAnswer, waitedAt] of stepRows[0].values) {
|
|
218
218
|
if (!stepsByStage[stageId]) stepsByStage[stageId] = [];
|
|
219
|
-
stepsByStage[stageId].push({
|
|
219
|
+
stepsByStage[stageId].push({
|
|
220
|
+
name, status, output, completedAt,
|
|
221
|
+
...(waitReason ? { waitReason } : {}),
|
|
222
|
+
...(waitOptions ? { waitOptions } : {}),
|
|
223
|
+
...(waitAnswer ? { waitAnswer } : {}),
|
|
224
|
+
...(waitedAt ? { waitedAt } : {}),
|
|
225
|
+
});
|
|
220
226
|
}
|
|
221
227
|
}
|
|
222
228
|
|
|
@@ -247,6 +253,10 @@ export class ProgressManager {
|
|
|
247
253
|
status: s.status,
|
|
248
254
|
output: s.output,
|
|
249
255
|
completedAt: s.completedAt,
|
|
256
|
+
...(s.waitReason ? { waitReason: s.waitReason } : {}),
|
|
257
|
+
...(s.waitOptions ? { waitOptions: s.waitOptions } : {}),
|
|
258
|
+
...(s.waitAnswer ? { waitAnswer: s.waitAnswer } : {}),
|
|
259
|
+
...(s.waitedAt ? { waitedAt: s.waitedAt } : {}),
|
|
250
260
|
}));
|
|
251
261
|
stages[stage] = {
|
|
252
262
|
status: info.status,
|
|
@@ -330,8 +340,9 @@ export class ProgressManager {
|
|
|
330
340
|
// UPSERT 步骤(先删再插,steps 表无 UNIQUE 约束)
|
|
331
341
|
sqlDb.run('DELETE FROM steps WHERE stage_id = ? AND name = ?', [stageId, step.name]);
|
|
332
342
|
sqlDb.run(
|
|
333
|
-
'INSERT INTO steps (stage_id, name, status, output, completed_at, ordering) VALUES (?, ?, ?, ?, ?, ?)',
|
|
334
|
-
[stageId, step.name, step.status || 'pending', step.output || null, step.completedAt || null, i
|
|
343
|
+
'INSERT INTO steps (stage_id, name, status, output, completed_at, ordering, wait_reason, wait_options, wait_answer, waited_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
|
344
|
+
[stageId, step.name, step.status || 'pending', step.output || null, step.completedAt || null, i,
|
|
345
|
+
step.waitReason || null, step.waitOptions || null, step.waitAnswer || null, step.waitedAt || null]
|
|
335
346
|
);
|
|
336
347
|
}
|
|
337
348
|
}
|
|
@@ -917,7 +928,7 @@ export class ProgressManager {
|
|
|
917
928
|
console.log(' ═══════════════════════════════════════');
|
|
918
929
|
console.log('');
|
|
919
930
|
|
|
920
|
-
const statusIcons = { pending: '⬜', 'in-progress': '🔵', completed: '✅', failed: '❌', blocked: '🚫' };
|
|
931
|
+
const statusIcons = { pending: '⬜', 'in-progress': '🔵', completed: '✅', failed: '❌', blocked: '🚫', waiting: '⏸️' };
|
|
921
932
|
|
|
922
933
|
for (const stage of VALID_STAGES) {
|
|
923
934
|
const stageData = data.stages[stage] || emptyStage();
|
|
@@ -931,7 +942,13 @@ export class ProgressManager {
|
|
|
931
942
|
for (const step of stageData.steps) {
|
|
932
943
|
const si = statusIcons[step.status] || '○';
|
|
933
944
|
const out = step.output ? ` — ${step.output.slice(0, 60)}` : '';
|
|
934
|
-
|
|
945
|
+
const waitingTag = step.status === 'waiting' ? ' [WAITING]' : ''
|
|
946
|
+
console.log(` ${si} ${step.name}${out}${waitingTag}`);
|
|
947
|
+
if (step.status === 'waiting') {
|
|
948
|
+
if (step.waitReason) console.log(` 原因:${step.waitReason}`);
|
|
949
|
+
if (step.waitOptions) console.log(` 选项:${(() => { try { const p = JSON.parse(step.waitOptions); return Array.isArray(p) ? p.join(', ') : step.waitOptions; } catch { return step.waitOptions; }})()}`);
|
|
950
|
+
if (step.waitedAt) console.log(` 等待时间:${step.waitedAt}`);
|
|
951
|
+
}
|
|
935
952
|
}
|
|
936
953
|
}
|
|
937
954
|
|
package/src/run.js
CHANGED
|
@@ -10,6 +10,25 @@ import { createRequire } from 'module'
|
|
|
10
10
|
const require = createRequire(import.meta.url)
|
|
11
11
|
import { ProgressManager } from './progress.js'
|
|
12
12
|
|
|
13
|
+
// ── Wait State Constants ──
|
|
14
|
+
// 正则匹配:只识别独立一行的标记,避免误伤文档正文引用
|
|
15
|
+
const WAIT_MARKER_RE = /^\s*\[(WAIT_FOR_USER|NEEDS_CONFIRM|NEEDS_DECISION)\]\s*$/m
|
|
16
|
+
const WAIT_MARKERS = ['[WAIT_FOR_USER]', '[NEEDS_CONFIRM]', '[NEEDS_DECISION]']
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 格式化 waitOptions 为人类可读字符串
|
|
20
|
+
*/
|
|
21
|
+
function formatWaitOptions(raw) {
|
|
22
|
+
if (!raw) return null
|
|
23
|
+
try {
|
|
24
|
+
const parsed = JSON.parse(raw)
|
|
25
|
+
if (Array.isArray(parsed)) return parsed.join(', ')
|
|
26
|
+
return raw
|
|
27
|
+
} catch {
|
|
28
|
+
return raw
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
13
32
|
/**
|
|
14
33
|
* 解析规范目录路径
|
|
15
34
|
* @param {string} cwd - 项目根目录
|
|
@@ -277,7 +296,7 @@ async function ensureStageSteps(progress, stageName, cwd, specDir = null) {
|
|
|
277
296
|
/**
|
|
278
297
|
* 输出当前步骤的 prompt
|
|
279
298
|
*/
|
|
280
|
-
async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjectName, platformOpts = {}) {
|
|
299
|
+
async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjectName, platformOpts = {}, prevStepAnswer = null) {
|
|
281
300
|
const step = steps[stepIndex]
|
|
282
301
|
const total = steps.length
|
|
283
302
|
const projectName = dbProjectName || basename(cwd)
|
|
@@ -423,6 +442,11 @@ async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjec
|
|
|
423
442
|
}
|
|
424
443
|
}
|
|
425
444
|
|
|
445
|
+
if (prevStepAnswer) {
|
|
446
|
+
console.log(`\n### 📩 上一步用户回答`)
|
|
447
|
+
console.log(prevStepAnswer)
|
|
448
|
+
}
|
|
449
|
+
|
|
426
450
|
console.log(promptText)
|
|
427
451
|
console.log(`\n### ⚠️ 铁律`)
|
|
428
452
|
console.log('- **文档是核心资产,代码是文档的产物。** 没有文档就没有代码——文档是 AI 的记忆,是团队协作的基础,是后续维护的唯一依据。任何代码产出必须先有对应的设计/规范文档支撑。')
|
|
@@ -448,7 +472,16 @@ async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjec
|
|
|
448
472
|
console.log(`- **文件路径规则:所有变更文件必须写入 \`${changeDir}/\` 目录下。不要自己拼接路径,直接使用 changeDir 值。示例:\`${changeDir}/proposal.md\`**`)
|
|
449
473
|
}
|
|
450
474
|
const changeFlag = changeName ? ` --change ${changeName}` : ''
|
|
475
|
+
// 检测当前 step prompt 是否包含 WAIT 指令(即可能需要等待用户)
|
|
476
|
+
const stepPrompt = promptText || ''
|
|
477
|
+
const mayNeedWait = WAIT_MARKER_RE.test(stepPrompt)
|
|
451
478
|
console.log(`\n### 完成后执行`)
|
|
479
|
+
if (mayNeedWait) {
|
|
480
|
+
console.log(`如果需要用户决策(选择方案/确认设计等):`)
|
|
481
|
+
console.log(`sillyspec run ${stageName} --wait --reason "等待原因" --options "选项1,选项2"${changeFlag} --output "你的摘要"`)
|
|
482
|
+
console.log(``)
|
|
483
|
+
console.log(`如果不需要用户决策,正常完成:`)
|
|
484
|
+
}
|
|
452
485
|
console.log(`sillyspec run ${stageName} --done${changeFlag} --input "用户原始需求/反馈" --output "你的摘要"`)
|
|
453
486
|
}
|
|
454
487
|
|
|
@@ -664,20 +697,27 @@ export async function runCommand(args, cwd, specDir = null) {
|
|
|
664
697
|
process.exit(1)
|
|
665
698
|
}
|
|
666
699
|
|
|
667
|
-
const isDone = flags.includes('--done')
|
|
668
|
-
const isSkip = flags.includes('--skip')
|
|
669
|
-
const isStatus = flags.includes('--status')
|
|
670
|
-
const isReset = flags.includes('--reset')
|
|
671
|
-
const isConfirm = flags.includes('--confirm')
|
|
672
|
-
const isSkipApproval = flags.includes('--skip-approval')
|
|
673
|
-
|
|
674
700
|
// 平台模式参数(供 SillyHub 等平台调用)
|
|
675
701
|
// --spec-dir 是统一参数名,--spec-root 保留为向后兼容别名
|
|
676
702
|
const getFlagValue = (name) => {
|
|
677
703
|
const idx = flags.indexOf(name)
|
|
678
704
|
return idx !== -1 && flags[idx + 1] ? flags[idx + 1] : null
|
|
679
705
|
}
|
|
680
|
-
const
|
|
706
|
+
const isDone = flags.includes('--done')
|
|
707
|
+
const isSkip = flags.includes('--skip')
|
|
708
|
+
const isStatus = flags.includes('--status')
|
|
709
|
+
const isReset = flags.includes('--reset')
|
|
710
|
+
const isConfirm = flags.includes('--confirm')
|
|
711
|
+
const isSkipApproval = flags.includes('--skip-approval')
|
|
712
|
+
const isWait = flags.includes('--wait')
|
|
713
|
+
const isContinue = flags.includes('--continue')
|
|
714
|
+
const isNonInteractive = flags.includes('--non-interactive')
|
|
715
|
+
const isInteractive = flags.includes('--interactive')
|
|
716
|
+
const waitReason = getFlagValue('--reason')
|
|
717
|
+
const waitOptions = getFlagValue('--options')
|
|
718
|
+
const continueAnswer = getFlagValue('--answer')
|
|
719
|
+
const confirmMode = getFlagValue('--confirm-mode')
|
|
720
|
+
const resolvedSpecDir = specDir || getFlagValue('--spec-dir') || getFlagValue('--spec-root')
|
|
681
721
|
const platformOpts = {
|
|
682
722
|
specRoot: resolvedSpecDir ? resolve(resolvedSpecDir) : null,
|
|
683
723
|
runtimeRoot: getFlagValue('--runtime-root') ? resolve(getFlagValue('--runtime-root')) : null,
|
|
@@ -797,6 +837,8 @@ export async function runCommand(args, cwd, specDir = null) {
|
|
|
797
837
|
// 未知参数 fail-fast
|
|
798
838
|
const knownFlags = new Set([
|
|
799
839
|
'--done', '--skip', '--status', '--reset', '--confirm', '--skip-approval',
|
|
840
|
+
'--wait', '--continue', '--non-interactive', '--interactive',
|
|
841
|
+
'--reason', '--options', '--answer', '--confirm-mode',
|
|
800
842
|
'--output', '--input', '--change',
|
|
801
843
|
'--spec-dir', '--spec-root', '--runtime-root', '--workspace-id', '--scan-run-id',
|
|
802
844
|
'--files', '--allow-new', '--force-baseline',
|
|
@@ -891,9 +933,19 @@ export async function runCommand(args, cwd, specDir = null) {
|
|
|
891
933
|
return await skipStep(pm, progress, stageName, cwd, effectiveChange)
|
|
892
934
|
}
|
|
893
935
|
|
|
936
|
+
// --wait: 将 step 设为 waiting(独立于 --done)
|
|
937
|
+
if (isWait) {
|
|
938
|
+
return await waitStep(pm, progress, stageName, cwd, outputText, waitReason, waitOptions, { changeName: effectiveChange, nonInteractive: isNonInteractive && !isInteractive, platformOpts })
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// --continue: 从 waiting 恢复
|
|
942
|
+
if (isContinue) {
|
|
943
|
+
return await continueStep(pm, progress, stageName, cwd, continueAnswer, { changeName: effectiveChange, nonInteractive: isNonInteractive && !isInteractive, platformOpts })
|
|
944
|
+
}
|
|
945
|
+
|
|
894
946
|
// --done
|
|
895
947
|
if (isDone) {
|
|
896
|
-
return await completeStep(pm, progress, stageName, cwd, outputText, inputText, { confirm: isConfirm, changeName: effectiveChange, platformOpts })
|
|
948
|
+
return await completeStep(pm, progress, stageName, cwd, outputText, inputText, { confirm: isConfirm, changeName: effectiveChange, nonInteractive: isNonInteractive && !isInteractive, platformOpts, confirmMode })
|
|
897
949
|
}
|
|
898
950
|
|
|
899
951
|
// 默认:输出当前步骤
|
|
@@ -962,6 +1014,18 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
|
|
|
962
1014
|
}
|
|
963
1015
|
|
|
964
1016
|
const steps = stageData.steps
|
|
1017
|
+
// ── 检查是否有 waiting step 需要先处理 ──
|
|
1018
|
+
const waitingIdx = steps.findIndex(s => s.status === 'waiting')
|
|
1019
|
+
if (waitingIdx !== -1) {
|
|
1020
|
+
const ws = steps[waitingIdx]
|
|
1021
|
+
console.error(`\n⏸️ Step ${waitingIdx + 1}/${steps.length} 正在等待用户输入:${ws.name}`)
|
|
1022
|
+
if (ws.waitReason) console.error(` 原因:${ws.waitReason}`)
|
|
1023
|
+
if (ws.waitOptions) console.error(` 选项:${formatWaitOptions(ws.waitOptions)}`)
|
|
1024
|
+
console.error(`\n 普通运行无法跳过等待中的步骤。请先处理:`)
|
|
1025
|
+
console.error(` sillyspec run ${stageName} --continue --answer "你的选择"${changeName ? ` --change ${changeName}` : ''}`)
|
|
1026
|
+
process.exit(1)
|
|
1027
|
+
}
|
|
1028
|
+
|
|
965
1029
|
let currentIdx = steps.findIndex(s => s.status !== 'completed' && s.status !== 'skipped')
|
|
966
1030
|
|
|
967
1031
|
// ── scanProfile: 根据 project 规模动态裁剪步骤 ──
|
|
@@ -1053,7 +1117,7 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
|
|
|
1053
1117
|
stageData.steps[currentIdx].completedAt = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
1054
1118
|
await pm._write(cwd, progress, changeName)
|
|
1055
1119
|
// 自动前进到下一步
|
|
1056
|
-
const nextIdx = stageData.steps.findIndex(s => s.status === 'pending')
|
|
1120
|
+
const nextIdx = stageData.steps.findIndex(s => s.status === 'pending' || s.status === 'in-progress')
|
|
1057
1121
|
if (nextIdx !== -1 && defSteps[nextIdx]) {
|
|
1058
1122
|
console.log('')
|
|
1059
1123
|
await outputStep(stageName, nextIdx, defSteps, cwd, changeName, progress.project || null, platformOpts)
|
|
@@ -1181,12 +1245,176 @@ async function archiveChangeDirectory(pm, cwd, progress) {
|
|
|
1181
1245
|
console.log(`📦 已归档:${archiveChangeName} → archive/${date}-${archiveChangeName}/`)
|
|
1182
1246
|
}
|
|
1183
1247
|
|
|
1248
|
+
// ── Wait Step ──
|
|
1249
|
+
|
|
1250
|
+
async function waitStep(pm, progress, stageName, cwd, outputText, waitReason, waitOptions, options = {}) {
|
|
1251
|
+
const { changeName, nonInteractive = false, platformOpts = {} } = options
|
|
1252
|
+
const specBase = platformOpts.specRoot || join(cwd, '.sillyspec')
|
|
1253
|
+
const stageData = progress.stages[stageName]
|
|
1254
|
+
|
|
1255
|
+
if (!stageData || !stageData.steps) {
|
|
1256
|
+
console.error(`❌ 阶段 ${stageName} 未初始化`)
|
|
1257
|
+
process.exit(1)
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
// 查找下一个 pending 或 in-progress 的步骤
|
|
1261
|
+
const currentIdx = stageData.steps.findIndex(s => s.status === 'pending' || s.status === 'in-progress')
|
|
1262
|
+
if (currentIdx === -1) {
|
|
1263
|
+
console.error('没有可以等待的步骤')
|
|
1264
|
+
process.exit(1)
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
// 非交互模式下拒绝等待
|
|
1268
|
+
if (nonInteractive) {
|
|
1269
|
+
console.error(`❌ Human decision required in non-interactive mode.`)
|
|
1270
|
+
console.error(` Reason: ${waitReason || '(unknown)'}`)
|
|
1271
|
+
if (waitOptions) console.error(` Options: ${formatWaitOptions(waitOptions)}`)
|
|
1272
|
+
console.error(` Fix: rerun with --interactive or provide decision via sillyspec run ${stageName} --continue --answer "..."`)
|
|
1273
|
+
process.exit(2)
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
const now = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
1277
|
+
stageData.steps[currentIdx].status = 'waiting'
|
|
1278
|
+
stageData.steps[currentIdx].waitedAt = now
|
|
1279
|
+
if (outputText) {
|
|
1280
|
+
const MAX_OUTPUT = 200
|
|
1281
|
+
stageData.steps[currentIdx].output = outputText.length > MAX_OUTPUT
|
|
1282
|
+
? outputText.slice(0, MAX_OUTPUT) + '…' : outputText
|
|
1283
|
+
}
|
|
1284
|
+
if (waitReason) stageData.steps[currentIdx].waitReason = waitReason
|
|
1285
|
+
if (waitOptions) {
|
|
1286
|
+
// 统一存为 JSON 数组
|
|
1287
|
+
try {
|
|
1288
|
+
const parsed = JSON.parse(waitOptions)
|
|
1289
|
+
if (Array.isArray(parsed)) {
|
|
1290
|
+
stageData.steps[currentIdx].waitOptions = JSON.stringify(parsed)
|
|
1291
|
+
} else {
|
|
1292
|
+
stageData.steps[currentIdx].waitOptions = JSON.stringify(waitOptions.split(',').map(o => o.trim()))
|
|
1293
|
+
}
|
|
1294
|
+
} catch {
|
|
1295
|
+
stageData.steps[currentIdx].waitOptions = JSON.stringify(waitOptions.split(',').map(o => o.trim()))
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
progress.lastActive = now
|
|
1300
|
+
await pm._write(cwd, progress, changeName)
|
|
1301
|
+
triggerSync(cwd, changeName, platformOpts)
|
|
1302
|
+
|
|
1303
|
+
console.log(`⏸️ Step ${currentIdx + 1}/${stageData.steps.length} 已暂停等待:${stageData.steps[currentIdx].name}`)
|
|
1304
|
+
if (waitReason) console.log(` 原因:${waitReason}`)
|
|
1305
|
+
if (waitOptions) console.log(` 选项:${formatWaitOptions(waitOptions)}`)
|
|
1306
|
+
console.log(` 继续时执行:sillyspec run ${stageName} --continue --answer "你的选择"${changeName ? ` --change ${changeName}` : ''}`)
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
// ── Continue Step ──
|
|
1310
|
+
|
|
1311
|
+
async function continueStep(pm, progress, stageName, cwd, answer, options = {}) {
|
|
1312
|
+
const { changeName, platformOpts = {} } = options
|
|
1313
|
+
const specBase = platformOpts.specRoot || join(cwd, '.sillyspec')
|
|
1314
|
+
const stageData = progress.stages[stageName]
|
|
1315
|
+
|
|
1316
|
+
if (!stageData || !stageData.steps) {
|
|
1317
|
+
console.error(`❌ 阶段 ${stageName} 未初始化`)
|
|
1318
|
+
process.exit(1)
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
if (!answer) {
|
|
1322
|
+
console.error('❌ --continue 需要 --answer 参数')
|
|
1323
|
+
process.exit(1)
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
// 查找 waiting 的步骤
|
|
1327
|
+
const waitingSteps = stageData.steps.map((s, i) => ({ ...s, idx: i })).filter(s => s.status === 'waiting')
|
|
1328
|
+
if (waitingSteps.length === 0) {
|
|
1329
|
+
console.error('没有处于等待状态的步骤')
|
|
1330
|
+
process.exit(1)
|
|
1331
|
+
}
|
|
1332
|
+
if (waitingSteps.length > 1) {
|
|
1333
|
+
console.error(`❌ 检测到 ${waitingSteps.length} 个等待中的步骤,无法确定恢复目标:`)
|
|
1334
|
+
for (const ws of waitingSteps) {
|
|
1335
|
+
console.error(` Step ${ws.idx + 1}: ${ws.name}${ws.waitReason ? `(${ws.waitReason})` : ''}`)
|
|
1336
|
+
}
|
|
1337
|
+
console.error(` 请使用 --reset 重置,或手动修复 DB`)
|
|
1338
|
+
process.exit(1)
|
|
1339
|
+
}
|
|
1340
|
+
const currentIdx = waitingSteps[0].idx
|
|
1341
|
+
|
|
1342
|
+
const now = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
1343
|
+
stageData.steps[currentIdx].status = 'completed'
|
|
1344
|
+
stageData.steps[currentIdx].completedAt = now
|
|
1345
|
+
stageData.steps[currentIdx].waitAnswer = answer
|
|
1346
|
+
|
|
1347
|
+
// 合并 waiting 信息到 output
|
|
1348
|
+
const prevOutput = stageData.steps[currentIdx].output || ''
|
|
1349
|
+
const waitInfo = stageData.steps[currentIdx].waitReason || ''
|
|
1350
|
+
if (waitInfo) {
|
|
1351
|
+
stageData.steps[currentIdx].output = prevOutput
|
|
1352
|
+
? `${prevOutput} | 用户选择:${answer}`
|
|
1353
|
+
: `用户选择:${answer}`
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// 清除等待状态
|
|
1357
|
+
delete stageData.steps[currentIdx].waitReason
|
|
1358
|
+
delete stageData.steps[currentIdx].waitOptions
|
|
1359
|
+
delete stageData.steps[currentIdx].waitedAt
|
|
1360
|
+
|
|
1361
|
+
progress.lastActive = now
|
|
1362
|
+
await pm._write(cwd, progress, changeName)
|
|
1363
|
+
triggerSync(cwd, changeName, platformOpts)
|
|
1364
|
+
|
|
1365
|
+
console.log(`✅ Step ${currentIdx + 1}/${stageData.steps.length} 已继续:${stageData.steps[currentIdx].name}`)
|
|
1366
|
+
console.log(` 回答:${answer}`)
|
|
1367
|
+
|
|
1368
|
+
// Append to user-inputs.md
|
|
1369
|
+
const inputsPath = join(specBase, '.runtime', 'user-inputs.md')
|
|
1370
|
+
const entry = `\n## ${now} | ${changeName || '?'} | ${stageName}: ${stageData.steps[currentIdx].name} [CONTINUED]\n- 回答:${answer}\n`
|
|
1371
|
+
appendFileSync(inputsPath, entry)
|
|
1372
|
+
|
|
1373
|
+
// 检查阶段是否全部完成
|
|
1374
|
+
const nextPendingIdx = stageData.steps.findIndex(s => s.status === 'pending')
|
|
1375
|
+
const nextWaitingIdx = stageData.steps.findIndex(s => s.status === 'waiting')
|
|
1376
|
+
if (nextPendingIdx === -1 && nextWaitingIdx === -1) {
|
|
1377
|
+
stageData.status = 'completed'
|
|
1378
|
+
stageData.completedAt = now
|
|
1379
|
+
await pm._write(cwd, progress, changeName)
|
|
1380
|
+
console.log(`\n✅ ${stageName} 阶段已完成(${stageData.steps.length}/${stageData.steps.length} 步)`)
|
|
1381
|
+
return { stageCompleted: true, currentIdx, nextPendingIdx: -1 }
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
// 输出下一步
|
|
1385
|
+
const defSteps = await getStageSteps(stageName, cwd, progress)
|
|
1386
|
+
if (nextPendingIdx !== -1 && defSteps) {
|
|
1387
|
+
console.log('')
|
|
1388
|
+
await outputStep(stageName, nextPendingIdx, defSteps, cwd, changeName, progress.project || null, platformOpts, answer)
|
|
1389
|
+
} else if (nextWaitingIdx !== -1 && defSteps) {
|
|
1390
|
+
// 下一个步骤也在等待状态
|
|
1391
|
+
const ws = stageData.steps[nextWaitingIdx]
|
|
1392
|
+
console.log(`\n⏸️ Step ${nextWaitingIdx + 1}/${stageData.steps.length} 仍在等待:${ws.name}`)
|
|
1393
|
+
if (ws.waitReason) console.log(` 原因:${ws.waitReason}`)
|
|
1394
|
+
console.log(` 继续:sillyspec run ${stageName} --continue --answer "..."${changeName ? ` --change ${changeName}` : ''}`)
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
return { stageCompleted: false, currentIdx, nextPendingIdx: nextPendingIdx }
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1184
1400
|
async function completeStep(pm, progress, stageName, cwd, outputText, inputText = null, options = {}) {
|
|
1185
|
-
const { printNext = true, confirm = false, changeName, platformOpts = {} } = options
|
|
1401
|
+
const { printNext = true, confirm = false, changeName, platformOpts = {}, nonInteractive = false, confirmMode = null } = options
|
|
1186
1402
|
const specBase = platformOpts.specRoot || join(cwd, '.sillyspec')
|
|
1187
1403
|
const stageData = progress.stages[stageName]
|
|
1188
1404
|
const scanProfile = stageData?.scanProfile || null
|
|
1189
1405
|
|
|
1406
|
+
// ── WAIT MARKER 硬校验 ──
|
|
1407
|
+
// 如果 output 包含等待标记,拒绝 --done 推进
|
|
1408
|
+
if (outputText) {
|
|
1409
|
+
const match = WAIT_MARKER_RE.exec(outputText)
|
|
1410
|
+
if (match) {
|
|
1411
|
+
console.error(`❌ Refused: step output contains ${match[1]} — human input required.`)
|
|
1412
|
+
console.error(` 使用 --wait 替代 --done,例如:`)
|
|
1413
|
+
console.error(` sillyspec run ${stageName} --wait --reason "等待用户决策" --output "你的摘要"${changeName ? ` --change ${changeName}` : ''}`)
|
|
1414
|
+
process.exit(1)
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1190
1418
|
// scanProfile 非 deep 模式:截断 outputText 减少 token 传递
|
|
1191
1419
|
let effectiveOutput = outputText
|
|
1192
1420
|
if (scanProfile && scanProfile.mode !== 'deep' && outputText && outputText.length > 1000) {
|
|
@@ -1198,12 +1426,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1198
1426
|
}
|
|
1199
1427
|
|
|
1200
1428
|
const steps = stageData.steps
|
|
1201
|
-
const currentIdx = steps.findIndex(s => s.status === 'pending')
|
|
1202
|
-
|
|
1203
|
-
if (currentIdx === -1) {
|
|
1204
|
-
console.error('没有待完成的步骤')
|
|
1205
|
-
process.exit(1)
|
|
1206
|
-
}
|
|
1429
|
+
const currentIdx = steps.findIndex(s => s.status === 'pending' || s.status === 'in-progress')
|
|
1207
1430
|
|
|
1208
1431
|
steps[currentIdx].status = 'completed'
|
|
1209
1432
|
steps[currentIdx].completedAt = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
@@ -1407,9 +1630,20 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
1407
1630
|
}
|
|
1408
1631
|
}
|
|
1409
1632
|
|
|
1410
|
-
const nextPendingIdx = steps.findIndex(s => s.status === 'pending')
|
|
1633
|
+
const nextPendingIdx = steps.findIndex(s => s.status === 'pending' || s.status === 'in-progress')
|
|
1411
1634
|
|
|
1412
1635
|
if (nextPendingIdx === -1) {
|
|
1636
|
+
// 也检查是否有 waiting 的步骤
|
|
1637
|
+
const hasWaiting = steps.some(s => s.status === 'waiting')
|
|
1638
|
+
if (hasWaiting) {
|
|
1639
|
+
// 有等待中的步骤,阶段未完成
|
|
1640
|
+
progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
1641
|
+
await pm._write(cwd, progress, changeName)
|
|
1642
|
+
const wsIdx = steps.findIndex(s => s.status === 'waiting')
|
|
1643
|
+
console.log(`\n⏸️ 阶段暂停:Step ${wsIdx + 1} 等待用户输入`)
|
|
1644
|
+
if (steps[wsIdx].waitReason) console.log(` 原因:${steps[wsIdx].waitReason}`)
|
|
1645
|
+
return { stageCompleted: false, currentIdx, nextPendingIdx: -1 }
|
|
1646
|
+
}
|
|
1413
1647
|
stageData.status = 'completed'
|
|
1414
1648
|
stageData.completedAt = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
1415
1649
|
progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
@@ -1700,10 +1934,16 @@ async function skipStep(pm, progress, stageName, cwd, changeName) {
|
|
|
1700
1934
|
}
|
|
1701
1935
|
|
|
1702
1936
|
const steps = stageData.steps
|
|
1703
|
-
const currentIdx = steps.findIndex(s => s.status === 'pending')
|
|
1937
|
+
const currentIdx = steps.findIndex(s => s.status === 'pending' || s.status === 'in-progress')
|
|
1704
1938
|
|
|
1705
1939
|
if (currentIdx === -1) {
|
|
1706
|
-
|
|
1940
|
+
const wsIdx = steps.findIndex(s => s.status === 'waiting')
|
|
1941
|
+
if (wsIdx !== -1) {
|
|
1942
|
+
console.error(`⏸️ Step ${wsIdx + 1} 正在等待用户输入,不能跳过。`)
|
|
1943
|
+
console.error(` 请先使用 --continue --answer "..." 继续,或用 --reset 重置。`)
|
|
1944
|
+
} else {
|
|
1945
|
+
console.error('没有待跳过的步骤')
|
|
1946
|
+
}
|
|
1707
1947
|
process.exit(1)
|
|
1708
1948
|
}
|
|
1709
1949
|
|
|
@@ -1722,10 +1962,17 @@ async function skipStep(pm, progress, stageName, cwd, changeName) {
|
|
|
1722
1962
|
|
|
1723
1963
|
console.log(`⏭️ Step ${currentIdx + 1}/${steps.length} 已跳过:${steps[currentIdx].name}`)
|
|
1724
1964
|
|
|
1725
|
-
const nextPendingIdx = steps.findIndex(s => s.status === 'pending')
|
|
1965
|
+
const nextPendingIdx = steps.findIndex(s => s.status === 'pending' || s.status === 'in-progress')
|
|
1726
1966
|
if (nextPendingIdx !== -1 && defSteps) {
|
|
1727
1967
|
console.log('')
|
|
1728
1968
|
await outputStep(stageName, nextPendingIdx, defSteps, cwd, changeName, progress.project || null, platformOpts)
|
|
1969
|
+
} else {
|
|
1970
|
+
const wsIdx = steps.findIndex(s => s.status === 'waiting')
|
|
1971
|
+
if (wsIdx !== -1) {
|
|
1972
|
+
console.log(`\n⏸️ Step ${wsIdx + 1}/${steps.length} 正在等待:${steps[wsIdx].name}`)
|
|
1973
|
+
if (steps[wsIdx].waitReason) console.log(` 原因:${steps[wsIdx].waitReason}`)
|
|
1974
|
+
console.log(` 继续:sillyspec run ${stageName} --continue --answer "..."${changeName ? ` --change ${changeName}` : ''}`)
|
|
1975
|
+
}
|
|
1729
1976
|
}
|
|
1730
1977
|
}
|
|
1731
1978
|
|
|
@@ -1746,7 +1993,7 @@ function showStatus(progress, stageName) {
|
|
|
1746
1993
|
console.log(`阶段:${stageName}(${stageDef.title})`)
|
|
1747
1994
|
console.log(`进度:[${bar}] ${completed}/${steps.length}\n`)
|
|
1748
1995
|
|
|
1749
|
-
const firstPending = steps.findIndex(s => s.status === 'pending')
|
|
1996
|
+
const firstPending = steps.findIndex(s => s.status === 'pending' || s.status === 'in-progress')
|
|
1750
1997
|
|
|
1751
1998
|
if (progress.batchProgress) {
|
|
1752
1999
|
const bp = progress.batchProgress
|
|
@@ -1765,9 +2012,15 @@ function showStatus(progress, stageName) {
|
|
|
1765
2012
|
}
|
|
1766
2013
|
|
|
1767
2014
|
steps.forEach((step, i) => {
|
|
1768
|
-
const icon = step.status === 'completed' ? '✅' : step.status === 'skipped' ? '⏭️' : '⬜'
|
|
1769
|
-
const isCurrent = step.status === 'pending' && i === firstPending
|
|
1770
|
-
|
|
2015
|
+
const icon = step.status === 'completed' ? '✅' : step.status === 'skipped' ? '⏭️' : step.status === 'waiting' ? '⏸️' : '⬜'
|
|
2016
|
+
const isCurrent = (step.status === 'pending' || step.status === 'in-progress') && i === firstPending
|
|
2017
|
+
const isWaiting = step.status === 'waiting'
|
|
2018
|
+
console.log(`${icon} Step ${i + 1}: ${step.name}${isCurrent ? ' ← 当前' : ''}${isWaiting ? ' [WAITING]' : ''}`)
|
|
2019
|
+
if (isWaiting) {
|
|
2020
|
+
if (step.waitReason) console.log(` 原因:${step.waitReason}`)
|
|
2021
|
+
if (step.waitOptions) console.log(` 选项:${formatWaitOptions(step.waitOptions)}`)
|
|
2022
|
+
if (step.waitedAt) console.log(` 等待时间:${step.waitedAt}`)
|
|
2023
|
+
}
|
|
1771
2024
|
})
|
|
1772
2025
|
}
|
|
1773
2026
|
|
|
@@ -1849,8 +2102,16 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
|
|
|
1849
2102
|
console.log('')
|
|
1850
2103
|
|
|
1851
2104
|
const defSteps = await getStageSteps(currentStage, cwd, progress)
|
|
1852
|
-
const pendingIdx = progress.stages[currentStage]?.steps?.findIndex(step => step.status === 'pending') ?? -1
|
|
2105
|
+
const pendingIdx = progress.stages[currentStage]?.steps?.findIndex(step => step.status === 'pending' || step.status === 'in-progress') ?? -1
|
|
1853
2106
|
if (pendingIdx === -1) {
|
|
2107
|
+
const wsIdx = progress.stages[currentStage]?.steps?.findIndex(step => step.status === 'waiting') ?? -1
|
|
2108
|
+
if (wsIdx !== -1) {
|
|
2109
|
+
const ws = progress.stages[currentStage].steps[wsIdx]
|
|
2110
|
+
console.log(`⏸️ Step ${wsIdx + 1} 等待用户输入:${ws.name}`)
|
|
2111
|
+
if (ws.waitReason) console.log(` 原因:${ws.waitReason}`)
|
|
2112
|
+
console.log(` 继续:sillyspec run auto --continue --answer "..."`)
|
|
2113
|
+
return
|
|
2114
|
+
}
|
|
1854
2115
|
const next = nextInFlow(currentStage)
|
|
1855
2116
|
if (next) console.log(`${currentStage} is complete. Run: sillyspec run auto --done --output "${currentStage} complete"`)
|
|
1856
2117
|
else console.log('All auto flow stages are complete.')
|
|
@@ -1883,7 +2144,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
|
|
|
1883
2144
|
if (!result) return
|
|
1884
2145
|
progress = await pm.read(cwd, changeName)
|
|
1885
2146
|
|
|
1886
|
-
const nextPendingIdx = progress.stages[currentStage]?.steps?.findIndex(step => step.status === 'pending') ?? -1
|
|
2147
|
+
const nextPendingIdx = progress.stages[currentStage]?.steps?.findIndex(step => step.status === 'pending' || step.status === 'in-progress') ?? -1
|
|
1887
2148
|
if (nextPendingIdx !== -1) {
|
|
1888
2149
|
const defSteps = await getStageSteps(currentStage, cwd, progress)
|
|
1889
2150
|
// execute 阶段启动前检查审批
|
|
@@ -1926,7 +2187,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
|
|
|
1926
2187
|
|
|
1927
2188
|
console.log(`\n${currentStage} complete. Auto advanced to ${next}.`)
|
|
1928
2189
|
const nextSteps = await getStageSteps(next, cwd, progress)
|
|
1929
|
-
const firstPending = progress.stages[next]?.steps?.findIndex(step => step.status === 'pending') ?? -1
|
|
2190
|
+
const firstPending = progress.stages[next]?.steps?.findIndex(step => step.status === 'pending' || step.status === 'in-progress') ?? -1
|
|
1930
2191
|
if (firstPending !== -1) {
|
|
1931
2192
|
// execute 阶段启动前检查审批
|
|
1932
2193
|
if (next === 'execute' && !skipApproval) {
|
package/src/stages/archive.js
CHANGED
|
@@ -11,7 +11,8 @@ export const definition = {
|
|
|
11
11
|
1. 读取 \`.sillyspec/changes/<change-name>/plan.md\`
|
|
12
12
|
2. 检查所有 \`- [x]\` checkbox 是否已勾选
|
|
13
13
|
3. 如果 plan.md 不存在,回退读取 tasks.md 作为备选
|
|
14
|
-
4. 如有遗漏 →
|
|
14
|
+
4. 如有遗漏 → **必须暂停等待用户决定**,不要自行判断"可以归档"
|
|
15
|
+
- 调用:\`sillyspec run archive --wait --reason "存在未完成任务,是否继续归档" --options "继续归档,回到execute完成剩余任务" --output "未完成任务列表"\`
|
|
15
16
|
|
|
16
17
|
### 输出
|
|
17
18
|
完成度报告(已勾选/总数 + 未完成任务列表)`,
|
|
@@ -110,8 +111,10 @@ module_id: <module-id>
|
|
|
110
111
|
<!-- MANUAL_NOTES_END -->
|
|
111
112
|
\`\`\`
|
|
112
113
|
|
|
113
|
-
4. 展示所有更新内容(diff
|
|
114
|
-
|
|
114
|
+
4. 展示所有更新内容(diff 摘要),**必须暂停等待用户确认**
|
|
115
|
+
- 调用:\`sillyspec run archive --wait --reason "等待用户确认模块文档同步" --options "确认写入,跳过同步" --output "diff 摘要"\`
|
|
116
|
+
5. **只有用户通过 --continue --answer "确认写入" 后才写入文件**
|
|
117
|
+
- 写入 _module-map.yaml 和受影响的模块卡片
|
|
115
118
|
6. 用户拒绝时,不写入,但提示"module-impact.md 已保留,可稍后手动同步"
|
|
116
119
|
7. 回填 module-impact.md 的"更新结果"表格,区分目标:
|
|
117
120
|
- 目标列写 "\`_module-map.yaml: <module-id>\`" 或 "\`modules/<module-id>.md\`"
|
package/src/stages/brainstorm.js
CHANGED
|
@@ -12,7 +12,7 @@ export const definition = {
|
|
|
12
12
|
2. 确认 currentStage 为 "brainstorm"
|
|
13
13
|
3. 如果有进行中的 brainstorm,提示选择继续或重新开始
|
|
14
14
|
4. 如果未初始化,提示先运行 sillyspec init
|
|
15
|
-
5. **检查变更名称是否有意义**:如果当前变更名是自动生成的(如 \`2026-06-02-new-change
|
|
15
|
+
5. **检查变更名称是否有意义**:如果当前变更名是自动生成的(如 \`2026-06-02-new-change\`),询问用户确认实际变更名,然后运行 \`sillyspec change-rename <旧名> <新名>\` 重命名
|
|
16
16
|
|
|
17
17
|
### 输出
|
|
18
18
|
当前状态摘要(1-2 句话)
|
|
@@ -32,13 +32,12 @@ export const definition = {
|
|
|
32
32
|
1. 读取 CODEBASE-OVERVIEW.md + 共享规范 + 子项目上下文
|
|
33
33
|
2. 加载项目信息:\`cat .sillyspec/projects/*.yaml 2>/dev/null\`
|
|
34
34
|
3. 加载本地配置:\`cat .sillyspec/local.yaml 2>/dev/null\`
|
|
35
|
-
4.
|
|
36
|
-
5.
|
|
37
|
-
|
|
38
|
-
- 这一步是高频操作,_module-map.yaml 回答“哪个文件属于哪个模块、模块之间怎么依赖”
|
|
35
|
+
4. 棕地项目:读取 .sillyspec/docs/<project>/scan/ 下的 STRUCTURE.md、CONVENTIONS.md、ARCHITECTURE.md
|
|
36
|
+
5. **加载模块索引**:读取 \`.sillyspec/docs/<project>/modules/_module-map.yaml\`(如存在)
|
|
37
|
+
- 这一步是高频操作,_module-map.yaml 回答"哪个文件属于哪个模块、模块之间怎么依赖"
|
|
39
38
|
- 用 tags/aliases 字段做需求关键词→模块的粗匹配
|
|
40
39
|
- 用 entrypoints 字段快速了解模块对外能力
|
|
41
|
-
|
|
40
|
+
6. 查看进行中的变更:\`ls .sillyspec/changes/ | grep -v archive\`
|
|
42
41
|
|
|
43
42
|
### 模块匹配方法
|
|
44
43
|
读取 _module-map.yaml 后,根据用户描述的需求关键词,匹配相关模块:
|
|
@@ -46,11 +45,15 @@ export const definition = {
|
|
|
46
45
|
- 需求中提到特定文件路径 → 匹配 paths 字段
|
|
47
46
|
- 匹配结果用于后续 design.md 的文件变更清单
|
|
48
47
|
|
|
48
|
+
### 子项目判定
|
|
49
|
+
- 单项目:直接确认,不需要等待
|
|
50
|
+
- 多项目且用户已指定:直接确认,不需要等待
|
|
51
|
+
- 多项目且用户未指定:列出项目列表,需要用户确认本次需求属于哪个子项目
|
|
52
|
+
|
|
49
53
|
### 输出
|
|
50
|
-
项目现状理解摘要(3-5 句话,关键约定和架构决策)+ 可能涉及的模块列表
|
|
54
|
+
项目现状理解摘要(3-5 句话,关键约定和架构决策)+ 可能涉及的模块列表 + 本次需求所属子项目
|
|
51
55
|
|
|
52
56
|
### 注意
|
|
53
|
-
- 始终询问本次需求属于哪个子项目
|
|
54
57
|
- 棕地项目必须读取数据模型章节
|
|
55
58
|
- 模块匹配只是粗筛,后续步骤会细化`,
|
|
56
59
|
outputHint: '上下文摘要',
|
|
@@ -128,13 +131,16 @@ export const definition = {
|
|
|
128
131
|
- 建议用「继承 + override」配置解决特殊任务,配置解决不了的才写定制代码
|
|
129
132
|
- 架构设计时预留扩展点(hooks/overrides),让特殊任务能"挂上去"而不是"另起炉灶"
|
|
130
133
|
|
|
134
|
+
### 铁律 — 何时需要等待用户
|
|
135
|
+
- **需要拆分或批量模式时**:列出方案并暂停等待用户确认
|
|
136
|
+
- 调用:\`sillyspec run brainstorm --wait --reason "等待用户确认拆分方案" --options "同意拆分,不需要拆分" --output "拆分方案摘要"\`
|
|
137
|
+
- **不需要拆分也不需要批量模式时**:正常完成即可
|
|
138
|
+
|
|
131
139
|
### 输出
|
|
132
140
|
拆分方案 / 批量模式确认 / "无需拆分"确认
|
|
133
141
|
|
|
134
142
|
### 注意
|
|
135
|
-
- 简单 CRUD
|
|
136
|
-
- 拆分方案需用户确认
|
|
137
|
-
- 批量模式需用户确认`,
|
|
143
|
+
- 简单 CRUD 不拆`,
|
|
138
144
|
outputHint: '拆分方案或无需拆分确认',
|
|
139
145
|
optional: true
|
|
140
146
|
},
|
|
@@ -144,18 +150,25 @@ export const definition = {
|
|
|
144
150
|
|
|
145
151
|
### 操作
|
|
146
152
|
1. 从最核心的一个问题开始(用户到底想要什么?)
|
|
147
|
-
2.
|
|
148
|
-
3.
|
|
153
|
+
2. **提出问题后必须暂停等待用户回答**,不要替用户回答
|
|
154
|
+
3. 根据用户回答判断:信息够了 → 正常完成 / 需要追问 → 暂停等待下一个回答
|
|
149
155
|
4. 探索顺序(按需):目的 → 约束 → 边界 → 成功标准
|
|
150
156
|
|
|
157
|
+
### 铁律 — 不要自问自答
|
|
158
|
+
- **这是人机协作步骤,你必须暂停等待用户输入。**
|
|
159
|
+
- 每次提出问题后,调用:
|
|
160
|
+
\`sillyspec run brainstorm --wait --reason "等待用户回答需求问题" --options "回答见--answer" --output "你的问题"\`
|
|
161
|
+
- **绝对禁止**:在自己输出中模拟用户的回答,然后说"需求已明确"
|
|
162
|
+
- 2-3 轮问答就应进入方案讨论
|
|
163
|
+
- 多选题优于开放式问题
|
|
164
|
+
- YAGNI — 砍掉不需要的功能
|
|
165
|
+
|
|
151
166
|
### 输出
|
|
152
167
|
需求理解摘要(用户确认的需求点列表)
|
|
153
168
|
|
|
154
|
-
###
|
|
155
|
-
-
|
|
156
|
-
-
|
|
157
|
-
- 多选题优于开放式问题
|
|
158
|
-
- YAGNI — 砍掉不需要的功能`,
|
|
169
|
+
### 注意
|
|
170
|
+
- 第一次进入此步骤时,提出第一个问题并暂停
|
|
171
|
+
- 用户通过 \`--continue --answer "回答"\` 回答后,本步骤会再次执行,此时检查是否需要追问或可以结束`,
|
|
159
172
|
outputHint: '需求理解摘要',
|
|
160
173
|
optional: false
|
|
161
174
|
},
|
|
@@ -166,7 +179,12 @@ export const definition = {
|
|
|
166
179
|
### 操作
|
|
167
180
|
1. 每种方案列出:核心思路、优势、劣势
|
|
168
181
|
2. 给出推荐方案和理由
|
|
169
|
-
|
|
182
|
+
|
|
183
|
+
### 铁律 — 必须等待用户选择方案
|
|
184
|
+
- **不要替用户选择方案。** 列出方案对比表和推荐后,必须暂停等待用户选择。
|
|
185
|
+
- 列出方案后,调用:
|
|
186
|
+
\`sillyspec run brainstorm --wait --reason "等待用户选择方案" --options "方案A,方案B,方案C" --output "方案对比摘要"\`
|
|
187
|
+
- **绝对禁止**:在输出中自己说"推荐方案 A"然后当用户选了
|
|
170
188
|
|
|
171
189
|
### 输出
|
|
172
190
|
方案对比表 + 推荐方案
|
|
@@ -179,21 +197,26 @@ export const definition = {
|
|
|
179
197
|
},
|
|
180
198
|
{
|
|
181
199
|
name: '分段展示设计',
|
|
182
|
-
prompt:
|
|
200
|
+
prompt: `展示完整设计方案供用户确认。
|
|
183
201
|
|
|
184
202
|
### 操作
|
|
185
203
|
1. 简单项目:几句话整体描述
|
|
186
|
-
2.
|
|
187
|
-
3.
|
|
188
|
-
4.
|
|
189
|
-
5.
|
|
204
|
+
2. 复杂项目:按模块/Phase 分段展示,每段 200-300 字
|
|
205
|
+
3. 展示完整设计方案(不要逐段停顿,一次性展示)
|
|
206
|
+
4. 确认变更名(格式:\`YYYY-MM-DD-<简短描述>\`,例如 \`2026-05-13-user-auth\`)
|
|
207
|
+
5. 暂停等待用户确认或修改意见
|
|
208
|
+
|
|
209
|
+
### 铁律 — 必须等待用户确认设计
|
|
210
|
+
- **不要替用户确认设计。** 展示完整设计方案后,必须暂停等待用户确认。
|
|
211
|
+
- 列出设计后,调用:
|
|
212
|
+
\`sillyspec run brainstorm --wait --reason "等待用户确认设计方案" --options "确认,需要修改,推翻重来" --output "设计方案摘要"\`
|
|
213
|
+
- **绝对禁止**:在输出中自己说"设计已充分确认"然后推进
|
|
190
214
|
|
|
191
215
|
### 输出
|
|
192
|
-
|
|
216
|
+
完整设计方案 + 变更名
|
|
193
217
|
|
|
194
218
|
### 注意
|
|
195
|
-
-
|
|
196
|
-
- 逐段确认,确保用户跟上
|
|
219
|
+
- 不要一次输出大段文字,按模块/Phase 分段
|
|
197
220
|
- 变更名必须以当天日期开头(YYYY-MM-DD-),后跟英文短横线分隔的简短描述`,
|
|
198
221
|
outputHint: '用户确认的设计方案',
|
|
199
222
|
optional: false
|
|
@@ -257,7 +280,7 @@ HTML 原型文件路径(或"跳过"如果不适合)`,
|
|
|
257
280
|
- 变更名格式必须为 \`YYYY-MM-DD-<简短描述>\`(如 \`2026-05-13-user-auth\`)
|
|
258
281
|
2. 将确认的设计写入 \`.sillyspec/changes/<change-name>/design.md\`
|
|
259
282
|
3. 自审检查:
|
|
260
|
-
-
|
|
283
|
+
- 需求覆盖:是否完整覆盖对话式探索中确认的需求
|
|
261
284
|
- 约束一致性:是否与 CONVENTIONS.md、ARCHITECTURE.md 一致
|
|
262
285
|
- 真实性:表名/字段名/类名/方法名来自真实代码或标注"新增"
|
|
263
286
|
- YAGNI:是否包含不必要功能
|
|
@@ -283,7 +306,7 @@ design.md 文件路径 + 自审结果
|
|
|
283
306
|
|
|
284
307
|
### 操作
|
|
285
308
|
1. 展示 design.md 摘要给用户
|
|
286
|
-
2.
|
|
309
|
+
2. 暂停等待用户选择:✅ 确认 / ✏️ 修改 / ❌ 推翻重来
|
|
287
310
|
3. 确认后,在 \`.sillyspec/changes/<change-name>/\` 下生成所有规范文件:
|
|
288
311
|
- **design.md**:架构决策、文件变更清单、数据模型、API 设计、兼容策略、风险登记、自审
|
|
289
312
|
- **proposal.md**:动机、关键问题(为什么现有方案不够)、变更范围、不在范围内(显式清单)、成功标准(可验证条件)
|
|
@@ -358,14 +381,19 @@ Then 期望结果
|
|
|
358
381
|
4. \`git add .sillyspec/\` — 暂存所有新增文件(不要 commit)
|
|
359
382
|
5. 后续变更包的骨架文件同样必须包含 \`author: <git-user>\` 和 \`created_at: <now-datetime>\`
|
|
360
383
|
|
|
384
|
+
### 铁律 — 必须等待用户最终确认
|
|
385
|
+
- **展示 design.md 摘要后,暂停等待用户确认。** 不要替用户确认。
|
|
386
|
+
- 调用:\`sillyspec run brainstorm --wait --reason "等待用户确认设计方案" --options "确认,需要修改,推翻重来" --output "design.md 摘要"\`
|
|
387
|
+
- **绝对禁止**:在输出中自己说"用户已确认"然后生成文件
|
|
388
|
+
- **只有用户通过 --continue --answer "确认" 后才生成规范文件**
|
|
389
|
+
|
|
361
390
|
### 输出
|
|
362
391
|
所有规范文件路径(含后续变更包目录列表)
|
|
363
392
|
|
|
364
393
|
### 注意
|
|
365
|
-
- 必须等待用户明确确认
|
|
366
394
|
- 禁止在确认前推进到后续阶段
|
|
367
395
|
- 禁止自动 commit
|
|
368
|
-
- 推翻重来回到 Step 6
|
|
396
|
+
- 推翻重来回到 Step 6(对话式探索)
|
|
369
397
|
- 表名/字段名/类名必须来自真实代码或标注"新增"
|
|
370
398
|
- tasks.md 只列任务名,细节在 plan 阶段展开`,
|
|
371
399
|
|
package/src/stages/execute.js
CHANGED
|
@@ -87,17 +87,15 @@ sillyspec run execute --done --output "worktree 路径 + 分支名"`,
|
|
|
87
87
|
- 简单修改 → 快速模型
|
|
88
88
|
- 文档/写作 → 写作模型
|
|
89
89
|
3. 用户在 tasks.md 中的 [model:xxx] 标签优先
|
|
90
|
-
4.
|
|
91
|
-
-
|
|
92
|
-
-
|
|
93
|
-
-
|
|
90
|
+
4. 读取 \`--confirm-mode\` 参数(由 CLI 传入,不需要询问用户):
|
|
91
|
+
- wave — 每个 Wave 完成后展示结果(默认)
|
|
92
|
+
- task — 每个 Task 完成后展示结果
|
|
93
|
+
- auto — 全部自动执行
|
|
94
94
|
5. 查询知识库:读取 \`.sillyspec/knowledge/INDEX.md\`,根据 Task 关键词匹配
|
|
95
95
|
|
|
96
|
-
###
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
### 注意
|
|
100
|
-
- 默认推荐"每个 Wave 确认"`,
|
|
96
|
+
### 铁律
|
|
97
|
+
- **不要询问用户确认频率**,确认模式由 CLI \`--confirm-mode\` 参数决定
|
|
98
|
+
- 如果未检测到 \`--confirm-mode\`,默认使用 wave 模式`,
|
|
101
99
|
outputHint: 'Wave 分组 + 模型分配',
|
|
102
100
|
optional: false
|
|
103
101
|
}
|
package/src/stages/plan.js
CHANGED
|
@@ -207,7 +207,10 @@ export const fixedSuffix = [
|
|
|
207
207
|
- 依赖关系和 plan.md 的 Wave 分组是否一致
|
|
208
208
|
- 验收标准和 plan.md 的全局标准是否矛盾
|
|
209
209
|
- 接口定义是否自洽
|
|
210
|
-
3. 发现问题 →
|
|
210
|
+
3. 发现问题 → 列出问题清单,暂停等待用户决定
|
|
211
|
+
- 调用:\`sillyspec run plan --wait --reason "审查发现一致性问题" --options "自动修复,手动修复,忽略并继续" --output "问题清单"\`
|
|
212
|
+
- **绝对禁止**:自己决定修复方向然后自动修复
|
|
213
|
+
4. 无问题 → 正常完成
|
|
211
214
|
|
|
212
215
|
### 输出
|
|
213
216
|
一致性审查结果`,
|
package/src/stages/scan.js
CHANGED
|
@@ -51,12 +51,14 @@ export const definition = {
|
|
|
51
51
|
2. dashboard(子项目)— scan 文档:0/7 已存在
|
|
52
52
|
\`\`\`
|
|
53
53
|
|
|
54
|
-
4.
|
|
54
|
+
4. **如果存在歧义(多项目且无法自动判定),必须暂停等待用户**:
|
|
55
55
|
- 选择要扫描的项目(默认全部)
|
|
56
56
|
- 每个项目的扫描策略:**重新扫描(全部覆盖)** / **只补扫描缺失的文档** / **跳过**
|
|
57
|
+
- 调用:\`sillyspec run scan --wait --reason "等待用户确认扫描项目列表" --options "全部重新扫描,只补缺失,跳过" --output "项目列表和策略建议"\`
|
|
58
|
+
5. **如果只有单一项目且已明确,正常完成即可,不需要等待。**
|
|
57
59
|
|
|
58
60
|
### ⛔ 重要
|
|
59
|
-
-
|
|
61
|
+
- **不要自行决定跳过**,有歧义时暂停等用户选择后再继续
|
|
60
62
|
- 最终确定的项目列表将用于后续所有步骤
|
|
61
63
|
- **后续每个需要生成文档的步骤,都必须对列表中的每个项目分别执行**
|
|
62
64
|
|
|
@@ -308,10 +310,11 @@ _module-map.yaml 生成结果(已存在/已生成/模块列表)`,
|
|
|
308
310
|
1. 读取 \`{DOCS_ROOT}/modules/_module-map.yaml\`,获取模块列表和路径
|
|
309
311
|
2. 检查 \`{DOCS_ROOT}/modules/\` 下已有的模块文档(<module>.md)
|
|
310
312
|
3. 列出每个模块的状态:已有文档 / 缺失
|
|
311
|
-
4.
|
|
313
|
+
4. **如果有覆盖风险(已有模块文档会被覆盖),必须暂停等待用户**:
|
|
312
314
|
- 展示模块列表及现有文档状态
|
|
313
315
|
- 明确提供选项:**为缺失模块生成初始文档** / **全部重新生成(覆盖已有)** / **跳过**
|
|
314
|
-
|
|
316
|
+
- 调用:\`sillyspec run scan --wait --reason "检测到已有模块文档覆盖风险" --options "只生成缺失,全部重新生成,跳过" --output "受影响模块列表"\`
|
|
317
|
+
5. **如果没有覆盖风险(全部都是缺失模块),正常完成即可,不需要等待。**
|
|
315
318
|
|
|
316
319
|
### 生成方法(子代理并行,只针对用户选中的模块)
|
|
317
320
|
**你必须为每个模块启动独立子代理执行,不要自己写文档。**
|