sillyspec 3.15.2 → 3.16.0
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/.husky/pre-push +13 -0
- package/docs/sillyspec/file-lifecycle.md +140 -10
- package/docs/worktree-isolation.md +57 -2
- package/package.json +5 -1
- package/src/db.js +17 -0
- package/src/index.js +44 -3
- package/src/progress.js +42 -0
- package/src/run.js +169 -15
- package/src/stages/doctor.js +42 -0
- package/src/stages/execute.js +32 -5
- package/src/stages/quick.js +3 -3
- package/src/stages/scan.js +15 -15
- package/src/workflow.js +6 -2
- package/src/worktree-apply.js +14 -0
- package/src/worktree.js +201 -11
- package/test/scan-paths.test.mjs +68 -0
package/src/run.js
CHANGED
|
@@ -147,7 +147,7 @@ async function ensureStageSteps(progress, stageName, cwd) {
|
|
|
147
147
|
/**
|
|
148
148
|
* 输出当前步骤的 prompt
|
|
149
149
|
*/
|
|
150
|
-
async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjectName) {
|
|
150
|
+
async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjectName, platformOpts = {}) {
|
|
151
151
|
const step = steps[stepIndex]
|
|
152
152
|
const total = steps.length
|
|
153
153
|
const projectName = dbProjectName || basename(cwd)
|
|
@@ -218,6 +218,43 @@ async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjec
|
|
|
218
218
|
if (changeName && promptText.includes('<change-name>')) {
|
|
219
219
|
promptText = promptText.replace(/<change-name>/g, changeName)
|
|
220
220
|
}
|
|
221
|
+
// 平台模式:注入路径覆盖指令(仅 scan 阶段)
|
|
222
|
+
if (stageName === 'scan') {
|
|
223
|
+
const projectName = dbProjectName || basename(cwd)
|
|
224
|
+
const specSillyspec = platformOpts?.specRoot
|
|
225
|
+
? join(platformOpts.specRoot, '.sillyspec')
|
|
226
|
+
: join(cwd, '.sillyspec')
|
|
227
|
+
const docsRoot = join(specSillyspec, 'docs', projectName)
|
|
228
|
+
const projectsRoot = join(specSillyspec, 'projects')
|
|
229
|
+
|
|
230
|
+
promptText = promptText.replace(/\{DOCS_ROOT\}/g, docsRoot)
|
|
231
|
+
promptText = promptText.replace(/\{PROJECTS_ROOT\}/g, projectsRoot)
|
|
232
|
+
|
|
233
|
+
// 平台模式附加指令
|
|
234
|
+
if (platformOpts?.specRoot || platformOpts?.runtimeRoot) {
|
|
235
|
+
const platformDirectives = []
|
|
236
|
+
if (platformOpts.specRoot) {
|
|
237
|
+
platformDirectives.push(
|
|
238
|
+
`## ⚠️ 平台模式\n` +
|
|
239
|
+
`文档路径已参数化:\n` +
|
|
240
|
+
`- 文档根目录: \`${docsRoot}/\`\n` +
|
|
241
|
+
`- 项目注册表: \`${projectsRoot}/\`\n` +
|
|
242
|
+
`创建目录: \`mkdir -p ${docsRoot}/{scan,modules,flows} ${projectsRoot}\`\n`
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
if (platformOpts.runtimeRoot) {
|
|
246
|
+
const scanRunId = platformOpts.scanRunId || 'scan-' + new Date().toISOString().slice(0, 19).replace(/[T:]/g, '-')
|
|
247
|
+
platformDirectives.push(
|
|
248
|
+
`运行时产物写入: \`${platformOpts.runtimeRoot}/scan-runs/${scanRunId}/\`\n`
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
if (platformOpts.workspaceId) {
|
|
252
|
+
platformDirectives.push(`workspace_id: ${platformOpts.workspaceId}`)
|
|
253
|
+
}
|
|
254
|
+
promptText = platformDirectives.join('\n') + '\n\n' + promptText
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
221
258
|
console.log(promptText)
|
|
222
259
|
console.log(`\n### ⚠️ 铁律`)
|
|
223
260
|
console.log('- **文档是核心资产,代码是文档的产物。** 没有文档就没有代码——文档是 AI 的记忆,是团队协作的基础,是后续维护的唯一依据。任何代码产出必须先有对应的设计/规范文档支撑。')
|
|
@@ -265,6 +302,50 @@ export async function runCommand(args, cwd) {
|
|
|
265
302
|
const isConfirm = flags.includes('--confirm')
|
|
266
303
|
const isSkipApproval = flags.includes('--skip-approval')
|
|
267
304
|
|
|
305
|
+
// 平台模式参数(供 SillyHub 等平台调用)
|
|
306
|
+
const getFlagValue = (name) => {
|
|
307
|
+
const idx = flags.indexOf(name)
|
|
308
|
+
return idx !== -1 && flags[idx + 1] ? flags[idx + 1] : null
|
|
309
|
+
}
|
|
310
|
+
const platformOpts = {
|
|
311
|
+
specRoot: getFlagValue('--spec-root'),
|
|
312
|
+
runtimeRoot: getFlagValue('--runtime-root'),
|
|
313
|
+
workspaceId: getFlagValue('--workspace-id'),
|
|
314
|
+
scanRunId: getFlagValue('--scan-run-id'),
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// 跨 --done 生命周期:优先从 metadata 文件恢复 platformOpts
|
|
318
|
+
// 首次 scan 时写入,后续 --done 读取
|
|
319
|
+
const platformOptsFile = join(cwd, '.sillyspec', '.runtime', 'platform-scan.json')
|
|
320
|
+
if (isDone || isSkip) {
|
|
321
|
+
// --done/--skip 阶段:从文件恢复
|
|
322
|
+
try {
|
|
323
|
+
const { readFileSync } = await import('fs')
|
|
324
|
+
const saved = JSON.parse(readFileSync(platformOptsFile, 'utf8'))
|
|
325
|
+
if (saved.specRoot) platformOpts.specRoot = saved.specRoot
|
|
326
|
+
if (saved.runtimeRoot) platformOpts.runtimeRoot = saved.runtimeRoot
|
|
327
|
+
if (saved.workspaceId) platformOpts.workspaceId = saved.workspaceId
|
|
328
|
+
if (saved.scanRunId) platformOpts.scanRunId = saved.scanRunId
|
|
329
|
+
} catch {
|
|
330
|
+
// 文件不存在,说明不是平台模式,跳过
|
|
331
|
+
}
|
|
332
|
+
} else if (platformOpts.specRoot || platformOpts.runtimeRoot) {
|
|
333
|
+
// 首次 scan:持久化 platformOpts
|
|
334
|
+
try {
|
|
335
|
+
const { mkdirSync, writeFileSync } = await import('fs')
|
|
336
|
+
mkdirSync(join(cwd, '.sillyspec', '.runtime'), { recursive: true })
|
|
337
|
+
writeFileSync(platformOptsFile, JSON.stringify({
|
|
338
|
+
specRoot: platformOpts.specRoot,
|
|
339
|
+
runtimeRoot: platformOpts.runtimeRoot,
|
|
340
|
+
workspaceId: platformOpts.workspaceId,
|
|
341
|
+
scanRunId: platformOpts.scanRunId,
|
|
342
|
+
savedAt: new Date().toISOString(),
|
|
343
|
+
}, null, 2) + '\n')
|
|
344
|
+
} catch {
|
|
345
|
+
// 静默失败,不影响主流程
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
268
349
|
// 解析 --output
|
|
269
350
|
let outputText = null
|
|
270
351
|
const outputIdx = flags.indexOf('--output')
|
|
@@ -286,6 +367,26 @@ export async function runCommand(args, cwd) {
|
|
|
286
367
|
changeName = flags[changeIdx + 1]
|
|
287
368
|
}
|
|
288
369
|
|
|
370
|
+
// 未知参数 fail-fast
|
|
371
|
+
const knownFlags = new Set([
|
|
372
|
+
'--done', '--skip', '--status', '--reset', '--confirm', '--skip-approval',
|
|
373
|
+
'--output', '--input', '--change',
|
|
374
|
+
'--spec-root', '--runtime-root', '--workspace-id', '--scan-run-id',
|
|
375
|
+
'--json', '--dir', '--help',
|
|
376
|
+
])
|
|
377
|
+
for (let i = 0; i < flags.length; i++) {
|
|
378
|
+
const f = flags[i]
|
|
379
|
+
if (f.startsWith('--')) {
|
|
380
|
+
if (!knownFlags.has(f)) {
|
|
381
|
+
console.error(`❌ 未知参数: ${f}`)
|
|
382
|
+
console.error(`已知参数: ${[...knownFlags].sort().join(', ')}`)
|
|
383
|
+
process.exit(1)
|
|
384
|
+
}
|
|
385
|
+
// 跳过 value 参数
|
|
386
|
+
i++
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
289
390
|
const isAuxiliary = auxiliaryStages.includes(stageName)
|
|
290
391
|
|
|
291
392
|
const pm = new ProgressManager()
|
|
@@ -361,11 +462,11 @@ export async function runCommand(args, cwd) {
|
|
|
361
462
|
|
|
362
463
|
// --done
|
|
363
464
|
if (isDone) {
|
|
364
|
-
return await completeStep(pm, progress, stageName, cwd, outputText, inputText, { confirm: isConfirm, changeName: effectiveChange })
|
|
465
|
+
return await completeStep(pm, progress, stageName, cwd, outputText, inputText, { confirm: isConfirm, changeName: effectiveChange, platformOpts })
|
|
365
466
|
}
|
|
366
467
|
|
|
367
468
|
// 默认:输出当前步骤
|
|
368
|
-
return await runStage(pm, progress, stageName, cwd, effectiveChange, isSkipApproval)
|
|
469
|
+
return await runStage(pm, progress, stageName, cwd, effectiveChange, isSkipApproval, platformOpts)
|
|
369
470
|
}
|
|
370
471
|
|
|
371
472
|
/**
|
|
@@ -380,7 +481,7 @@ function resolveChangeNameAuto(cwd) {
|
|
|
380
481
|
return null
|
|
381
482
|
}
|
|
382
483
|
|
|
383
|
-
async function runStage(pm, progress, stageName, cwd, changeName, skipApproval = false) {
|
|
484
|
+
async function runStage(pm, progress, stageName, cwd, changeName, skipApproval = false, platformOpts = {}) {
|
|
384
485
|
// execute 阶段启动前检查审批
|
|
385
486
|
if (stageName === 'execute' && !skipApproval) {
|
|
386
487
|
const approval = await checkApproval(cwd, changeName)
|
|
@@ -444,7 +545,7 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
|
|
|
444
545
|
|
|
445
546
|
const defSteps = await getStageSteps(stageName, cwd, progress)
|
|
446
547
|
if (defSteps && defSteps[currentIdx]) {
|
|
447
|
-
await outputStep(stageName, currentIdx, defSteps, cwd, changeName, progress.project || null)
|
|
548
|
+
await outputStep(stageName, currentIdx, defSteps, cwd, changeName, progress.project || null, platformOpts)
|
|
448
549
|
}
|
|
449
550
|
}
|
|
450
551
|
|
|
@@ -527,7 +628,7 @@ function validateFileLocations(cwd, stageName, progress, changeName) {
|
|
|
527
628
|
}
|
|
528
629
|
|
|
529
630
|
async function completeStep(pm, progress, stageName, cwd, outputText, inputText = null, options = {}) {
|
|
530
|
-
const { printNext = true, confirm = false, changeName } = options
|
|
631
|
+
const { printNext = true, confirm = false, changeName, platformOpts = {} } = options
|
|
531
632
|
const stageData = progress.stages[stageName]
|
|
532
633
|
if (!stageData || !stageData.steps) {
|
|
533
634
|
console.error(`❌ 阶段 ${stageName} 未初始化`)
|
|
@@ -548,10 +649,13 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
548
649
|
const MAX_OUTPUT = 200
|
|
549
650
|
if (outputText.length > MAX_OUTPUT) {
|
|
550
651
|
steps[currentIdx].output = outputText.slice(0, MAX_OUTPUT) + '…'
|
|
551
|
-
|
|
552
|
-
|
|
652
|
+
// 平台模式:artifact 写入 runtime-root,否则写 .sillyspec/.runtime/artifacts
|
|
653
|
+
const artifactBase = platformOpts?.runtimeRoot
|
|
654
|
+
? join(platformOpts.runtimeRoot, 'scan-runs', platformOpts.scanRunId || 'unknown')
|
|
655
|
+
: join(cwd, '.sillyspec', '.runtime', 'artifacts')
|
|
656
|
+
mkdirSync(artifactBase, { recursive: true })
|
|
553
657
|
const ts = new Date().toISOString().slice(0,19).replace(/[-T:]/g, '')
|
|
554
|
-
writeFileSync(join(
|
|
658
|
+
writeFileSync(join(artifactBase, `${changeName || 'unknown'}-${stageName}-step${currentIdx + 1}-${ts}.txt`), outputText)
|
|
555
659
|
} else {
|
|
556
660
|
steps[currentIdx].output = outputText
|
|
557
661
|
}
|
|
@@ -683,6 +787,56 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
683
787
|
appendFileSync(inputsPath, entry)
|
|
684
788
|
}
|
|
685
789
|
|
|
790
|
+
// 平台模式:scan 完成后生成 manifest.json
|
|
791
|
+
if (stageName === 'scan' && platformOpts.specRoot) {
|
|
792
|
+
try {
|
|
793
|
+
const { mkdirSync, writeFileSync } = await import('fs')
|
|
794
|
+
const { join } = await import('path')
|
|
795
|
+
const { execSync } = await import('child_process')
|
|
796
|
+
const manifestDir = join(platformOpts.specRoot, '.sillyspec')
|
|
797
|
+
mkdirSync(manifestDir, { recursive: true })
|
|
798
|
+
let sourceCommit = null
|
|
799
|
+
try {
|
|
800
|
+
sourceCommit = execSync('git rev-parse HEAD', { cwd, encoding: 'utf8', timeout: 5000 }).trim()
|
|
801
|
+
} catch {}
|
|
802
|
+
const manifest = {
|
|
803
|
+
workspace_id: platformOpts.workspaceId || null,
|
|
804
|
+
scan_run_id: platformOpts.scanRunId || null,
|
|
805
|
+
source_commit: sourceCommit,
|
|
806
|
+
generated_at: new Date().toISOString(),
|
|
807
|
+
schema_version: 1,
|
|
808
|
+
}
|
|
809
|
+
const manifestPath = join(manifestDir, 'manifest.json')
|
|
810
|
+
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n')
|
|
811
|
+
console.log(`📄 manifest.json 已写入: ${manifestPath}`)
|
|
812
|
+
if (!sourceCommit) {
|
|
813
|
+
console.log(`⚠️ source_commit 无法获取(可能非 git 目录),已设为 null`)
|
|
814
|
+
}
|
|
815
|
+
// 清理平台参数临时文件
|
|
816
|
+
const { unlinkSync } = await import('fs')
|
|
817
|
+
const platformOptsFile = join(cwd, '.sillyspec', '.runtime', 'platform-scan.json')
|
|
818
|
+
try { unlinkSync(platformOptsFile) } catch {}
|
|
819
|
+
|
|
820
|
+
// 平台模式后置校验:检查 source_root 是否被污染
|
|
821
|
+
if (platformOpts.specRoot) {
|
|
822
|
+
const { readdirSync } = await import('fs')
|
|
823
|
+
const localDocsDir = join(cwd, '.sillyspec', 'docs')
|
|
824
|
+
try {
|
|
825
|
+
if (existsSync(localDocsDir)) {
|
|
826
|
+
const entries = readdirSync(localDocsDir, { recursive: true }).filter(e => e.endsWith('.md'))
|
|
827
|
+
if (entries.length > 0) {
|
|
828
|
+
console.warn(`⚠️ 平台模式后置校验:source_root 下存在 ${entries.length} 个文档文件:`)
|
|
829
|
+
console.warn(` 路径:${localDocsDir}/`)
|
|
830
|
+
console.warn(` 可能原因:agent 未遵守路径覆盖,将文档写入到了 cwd 而非 spec-root`)
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
} catch {}
|
|
834
|
+
}
|
|
835
|
+
} catch (e) {
|
|
836
|
+
console.warn(`⚠️ manifest.json 写入失败: ${e.message}`)
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
686
840
|
validateMetadata(cwd, stageName)
|
|
687
841
|
|
|
688
842
|
// 验证关键文件是否在正确的变更目录下
|
|
@@ -862,7 +1016,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
862
1016
|
}
|
|
863
1017
|
|
|
864
1018
|
if (printNext) {
|
|
865
|
-
await outputStep(stageName, nextPendingIdx, defSteps, cwd, changeName, progress.project || null)
|
|
1019
|
+
await outputStep(stageName, nextPendingIdx, defSteps, cwd, changeName, progress.project || null, platformOpts)
|
|
866
1020
|
}
|
|
867
1021
|
return { stageCompleted: false, currentIdx, nextPendingIdx }
|
|
868
1022
|
}
|
|
@@ -900,7 +1054,7 @@ async function skipStep(pm, progress, stageName, cwd, changeName) {
|
|
|
900
1054
|
const nextPendingIdx = steps.findIndex(s => s.status === 'pending')
|
|
901
1055
|
if (nextPendingIdx !== -1 && defSteps) {
|
|
902
1056
|
console.log('')
|
|
903
|
-
await outputStep(stageName, nextPendingIdx, defSteps, cwd, changeName, progress.project || null)
|
|
1057
|
+
await outputStep(stageName, nextPendingIdx, defSteps, cwd, changeName, progress.project || null, platformOpts)
|
|
904
1058
|
}
|
|
905
1059
|
}
|
|
906
1060
|
|
|
@@ -1045,7 +1199,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
|
|
|
1045
1199
|
}
|
|
1046
1200
|
}
|
|
1047
1201
|
}
|
|
1048
|
-
await outputStep(currentStage, pendingIdx, defSteps, cwd, changeName, progress.project || null)
|
|
1202
|
+
await outputStep(currentStage, pendingIdx, defSteps, cwd, changeName, progress.project || null, platformOpts)
|
|
1049
1203
|
return
|
|
1050
1204
|
}
|
|
1051
1205
|
|
|
@@ -1054,7 +1208,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
|
|
|
1054
1208
|
process.exit(1)
|
|
1055
1209
|
}
|
|
1056
1210
|
|
|
1057
|
-
const result = await completeStep(pm, progress, currentStage, cwd, outputText, inputText, { printNext: false, changeName })
|
|
1211
|
+
const result = await completeStep(pm, progress, currentStage, cwd, outputText, inputText, { printNext: false, changeName, platformOpts })
|
|
1058
1212
|
if (!result) return
|
|
1059
1213
|
progress = await pm.read(cwd, changeName)
|
|
1060
1214
|
|
|
@@ -1075,7 +1229,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
|
|
|
1075
1229
|
}
|
|
1076
1230
|
}
|
|
1077
1231
|
}
|
|
1078
|
-
await outputStep(currentStage, nextPendingIdx, defSteps, cwd, changeName, progress.project || null)
|
|
1232
|
+
await outputStep(currentStage, nextPendingIdx, defSteps, cwd, changeName, progress.project || null, platformOpts)
|
|
1079
1233
|
return
|
|
1080
1234
|
}
|
|
1081
1235
|
|
|
@@ -1117,6 +1271,6 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
|
|
|
1117
1271
|
}
|
|
1118
1272
|
}
|
|
1119
1273
|
}
|
|
1120
|
-
await outputStep(next, firstPending, nextSteps, cwd, changeName, progress.project || null)
|
|
1274
|
+
await outputStep(next, firstPending, nextSteps, cwd, changeName, progress.project || null, platformOpts)
|
|
1121
1275
|
}
|
|
1122
1276
|
}
|
package/src/stages/doctor.js
CHANGED
|
@@ -91,6 +91,48 @@ for f in .sillyspec/projects/*.yaml; do
|
|
|
91
91
|
done
|
|
92
92
|
\`\`\`
|
|
93
93
|
|
|
94
|
+
### 6. Worktree 隔离环境检查
|
|
95
|
+
\`\`\`bash
|
|
96
|
+
# 检测当前目录是否在 submodule 中
|
|
97
|
+
SUPERPROJECT=$(git rev-parse --show-superproject-working-tree 2>/dev/null)
|
|
98
|
+
if [ -n "$SUPERPROJECT" ]; then
|
|
99
|
+
echo "⚠️ 当前目录在 git submodule 内,worktree 隔离不可用"
|
|
100
|
+
else
|
|
101
|
+
echo "✅ 不在 submodule 中"
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
# 检测是否已在 linked worktree 中
|
|
105
|
+
GIT_DIR=$(cd "$(git rev-parse --git-dir)" 2>/dev/null && pwd -P)
|
|
106
|
+
GIT_COMMON=$(cd "$(git rev-parse --git-common-dir)" 2>/dev/null && pwd -P)
|
|
107
|
+
if [ "$GIT_DIR" != "$GIT_COMMON" ] && [ -z "$SUPERPROJECT" ]; then
|
|
108
|
+
echo "✅ 已在 linked worktree 中"
|
|
109
|
+
else
|
|
110
|
+
echo "ℹ️ 在主仓库中(非 worktree)"
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
# 检查 worktree 存储目录是否被 .gitignore 忽略
|
|
114
|
+
WT_DIR='.sillyspec/.runtime/worktrees'
|
|
115
|
+
if git check-ignore -q "$WT_DIR" 2>/dev/null; then
|
|
116
|
+
echo "✅ worktree 目录已被 .gitignore 忽略 ($WT_DIR)"
|
|
117
|
+
else
|
|
118
|
+
echo "❌ worktree 目录未被 .gitignore 忽略 ($WT_DIR) — worktree 创建将被阻断"
|
|
119
|
+
echo " 修复: 在 .gitignore 中添加 $WT_DIR/"
|
|
120
|
+
fi
|
|
121
|
+
|
|
122
|
+
# 检查 DB 中的 isolation 状态
|
|
123
|
+
DB_FILE='.sillyspec/.runtime/sillyspec.db'
|
|
124
|
+
if [ -f "$DB_FILE" ]; then
|
|
125
|
+
echo ""
|
|
126
|
+
echo "isolation 状态(来自 sillyspec.db):"
|
|
127
|
+
sqlite3 -header -column "$DB_FILE" "SELECT name, isolation_status AS status, isolation_mode AS mode, isolation_reason AS reason FROM changes WHERE status='active'" 2>/dev/null || echo "⚠️ 查询 isolation 失败"
|
|
128
|
+
else
|
|
129
|
+
echo ""
|
|
130
|
+
echo "ℹ️ sillyspec.db 不存在(尚未初始化)"
|
|
131
|
+
fi
|
|
132
|
+
else
|
|
133
|
+
echo "ℹ️ gate-status.json 不存在(尚未进入 execute 阶段)"
|
|
134
|
+
fi
|
|
135
|
+
\`\`\`\n
|
|
94
136
|
### 输出
|
|
95
137
|
汇总所有检查结果,按以下格式:
|
|
96
138
|
\`\`\`
|
package/src/stages/execute.js
CHANGED
|
@@ -61,8 +61,14 @@ const fixedPrefix = [
|
|
|
61
61
|
3. 后续所有子代理的 cwd 设为该 worktree 路径
|
|
62
62
|
4. 如果创建失败 → 报错并停止(不要在无隔离状态下继续)
|
|
63
63
|
|
|
64
|
+
### 降级模式
|
|
65
|
+
CLI 可能自动降级(sandbox 限制、已在 linked worktree 中):
|
|
66
|
+
- \`mode: native-worktree\` — 已在 linked worktree,直接复用
|
|
67
|
+
- \`mode: in-place-fallback\` — git worktree add 失败,降级为 in-place + baseline protection
|
|
68
|
+
- 这两种模式都会输出 worktree 路径和分支名,正常继续即可
|
|
69
|
+
|
|
64
70
|
### 输出
|
|
65
|
-
worktree 路径 + 分支名
|
|
71
|
+
worktree 路径 + 分支名 + 模式(如果有)
|
|
66
72
|
|
|
67
73
|
### 完成后执行
|
|
68
74
|
sillyspec run execute --done --output "worktree 路径 + 分支名"`,
|
|
@@ -184,19 +190,40 @@ const fixedSuffix = [
|
|
|
184
190
|
name: '完成确认',
|
|
185
191
|
prompt: `所有任务完成后的收尾。
|
|
186
192
|
|
|
187
|
-
|
|
193
|
+
先检查当前 worktree 的隔离模式:
|
|
194
|
+
\`\`\`bash
|
|
195
|
+
node -e "import('./src/worktree.js').then(w => { const wm = new w.WorktreeManager(); const m = wm.getMeta('<change-name>'); console.log(m ? JSON.stringify({mode: m.mode, path: m.worktreePath}) : 'no meta'); })"
|
|
196
|
+
# 或从 DB 读取:
|
|
197
|
+
sqlite3 -json .sillyspec/.runtime/sillyspec.db "SELECT isolation_status, isolation_mode, isolation_reason FROM changes WHERE name='<change-name>'" 2>/dev/null
|
|
198
|
+
\`\`\`
|
|
199
|
+
|
|
200
|
+
### 操作(mode = worktree,SillySpec 创建的隔离 worktree)
|
|
188
201
|
1. 运行 \`sillyspec worktree apply --check-only <change-name>\`
|
|
189
202
|
2. 展示 diff 摘要(文件列表 + 变更统计)
|
|
190
203
|
3. 检查结果说明(是否通过文件清单校验)
|
|
191
204
|
4. 用户确认后运行 \`sillyspec worktree apply <change-name>\`
|
|
192
|
-
5. apply 成功 →
|
|
205
|
+
5. apply 成功 → 运行 \`sillyspec worktree cleanup <change-name>\` → 输出 Worktree: cleaned
|
|
193
206
|
6. apply 失败 → 展示错误详情,用户选择重试或手动处理
|
|
194
207
|
7. 如果用户不想 apply → 运行 \`sillyspec worktree cleanup <change-name>\` 丢弃
|
|
195
208
|
8. 建议下一步:\`sillyspec run verify\`
|
|
196
209
|
|
|
210
|
+
### 操作(mode = native-worktree,用户已有的 linked worktree)
|
|
211
|
+
1. 运行 \`sillyspec worktree apply --check-only <change-name>\`
|
|
212
|
+
2. 展示 diff 摘要
|
|
213
|
+
3. 用户确认后运行 \`sillyspec worktree apply <change-name>\`
|
|
214
|
+
4. **不要运行 cleanup** — 这是用户自己的 worktree,SillySpec 不能删除
|
|
215
|
+
5. 输出 Worktree: kept(SillySpec 未创建此 worktree,保留不动)
|
|
216
|
+
6. 建议下一步:\`sillyspec run verify\`
|
|
217
|
+
|
|
218
|
+
### 操作(mode = in-place-fallback,降级模式无隔离目录)
|
|
219
|
+
1. 展示本次执行摘要(\`git diff\` 查看变更)
|
|
220
|
+
2. 跳过 apply 和 cleanup(没有隔离 worktree)
|
|
221
|
+
3. 输出 Worktree: none(降级为 in-place,无隔离目录需要清理)
|
|
222
|
+
4. 建议下一步:\`sillyspec run verify\`
|
|
223
|
+
|
|
197
224
|
### 操作(无 worktree / --no-worktree 模式)
|
|
198
|
-
1.
|
|
199
|
-
2.
|
|
225
|
+
1. 展示本次执行摘要
|
|
226
|
+
2. 输出 Worktree: none
|
|
200
227
|
3. 提示用户直接使用 \`git diff\` 查看变更
|
|
201
228
|
4. 建议下一步:\`sillyspec run verify\`
|
|
202
229
|
|
package/src/stages/quick.js
CHANGED
|
@@ -27,12 +27,12 @@ export const definition = {
|
|
|
27
27
|
1. 使用预注入的 git 用户名:\`<git-user>\`
|
|
28
28
|
2. 无 \`--change\`:创建 .sillyspec/quicklog/QUICKLOG-\`<git-user>\`.md\`(已存在则追加),写入:
|
|
29
29
|
\`\`\`
|
|
30
|
-
## ql-<YYYYMMDD>-<NNN>-<
|
|
30
|
+
## ql-<YYYYMMDD>-<NNN>-<XXXX> | <now-datetime> | <一句话任务描述>
|
|
31
31
|
状态:进行中
|
|
32
32
|
文件:<预估要改的文件>
|
|
33
33
|
\`\`\`
|
|
34
|
-
- ID 格式:\`ql-YYYYMMDD-NNN-XXXX
|
|
35
|
-
- \`XXXX\` 是 4
|
|
34
|
+
- ID 格式:\`ql-YYYYMMDD-NNN-XXXX\`
|
|
35
|
+
- \`XXXX\` 是 4 位随机十六进制字符(如 a3f2、b7c1、00ef),**不是描述词缩写**
|
|
36
36
|
- 追加前扫描文件中已有的 \`ql-<当天日期>-\` 前缀的最大序号,+1 作为新序号
|
|
37
37
|
- 每天从 001 开始,跨日重新计数
|
|
38
38
|
- 此 ID 可被 design.md / plan.md / archive / module 变更索引引用
|
package/src/stages/scan.js
CHANGED
|
@@ -12,7 +12,7 @@ export const definition = {
|
|
|
12
12
|
1. 列出项目顶层目录:\`ls -d */ 2>/dev/null | grep -v node_modules | grep -v '.git' | grep -v '.sillyspec'\`
|
|
13
13
|
2. 对每个顶层目录,快速判断是否为独立项目(检查 package.json / pom.xml / build.gradle / pyproject.toml / go.mod 等构建文件)
|
|
14
14
|
3. 对每个疑似独立项目,检测技术栈:\`cat <dir>/package.json 2>/dev/null | head -5\` 或类似
|
|
15
|
-
4. 对比
|
|
15
|
+
4. 对比 \`{PROJECTS_ROOT}/\` 已有配置,找出未注册的子项目
|
|
16
16
|
|
|
17
17
|
### 判断标准(满足任一即为子项目)
|
|
18
18
|
- 有独立的构建文件(package.json, pom.xml, build.gradle, pyproject.toml 等)
|
|
@@ -41,8 +41,8 @@ export const definition = {
|
|
|
41
41
|
prompt: `确定本次要扫描的项目列表。
|
|
42
42
|
|
|
43
43
|
### 操作
|
|
44
|
-
1. \`ls
|
|
45
|
-
2. 对每个项目,检查已有的 scan 文档状态:\`ls
|
|
44
|
+
1. \`ls {PROJECTS_ROOT}/*.yaml 2>/dev/null\` — 列出所有已注册项目
|
|
45
|
+
2. 对每个项目,检查已有的 scan 文档状态:\`ls {DOCS_ROOT}/scan/*.md 2>/dev/null\`
|
|
46
46
|
3. 按以下格式展示:
|
|
47
47
|
|
|
48
48
|
\`\`\`
|
|
@@ -75,7 +75,7 @@ export const definition = {
|
|
|
75
75
|
1. 进入项目目录(子项目用其 path,如 \`packages/dashboard/\`)
|
|
76
76
|
2. \`cat package.json pom.xml build.gradle go.mod Cargo.toml requirements.txt pyproject.toml Gemfile composer.json 2>/dev/null\`
|
|
77
77
|
3. \`find <project-dir> -maxdepth 2 -name "*.config.*" -not -path "*/node_modules/*" -not -path "*/.git/*" | head -20 | xargs cat 2>/dev/null\`
|
|
78
|
-
4. 结果保存到
|
|
78
|
+
4. 结果保存到 \`{DOCS_ROOT}/scan/_env-detect.md\`(临时文件,扫描完删除)
|
|
79
79
|
|
|
80
80
|
### 输出
|
|
81
81
|
每个项目的环境探测结果摘要`,
|
|
@@ -90,7 +90,7 @@ export const definition = {
|
|
|
90
90
|
### 操作
|
|
91
91
|
对扫描列表中的每个项目分别执行:
|
|
92
92
|
1. 检查 7 份文档是否存在:ARCHITECTURE、STRUCTURE、CONVENTIONS、INTEGRATIONS、TESTING、CONCERNS、PROJECT
|
|
93
|
-
|
|
93
|
+
路径:\`{DOCS_ROOT}/scan/<DOC>.md\`
|
|
94
94
|
2. 列出已有 ✅ 和缺失 ⬜
|
|
95
95
|
|
|
96
96
|
### 输出
|
|
@@ -187,7 +187,7 @@ local.yaml 生成结果(已存在/已生成)`,
|
|
|
187
187
|
|
|
188
188
|
### 操作
|
|
189
189
|
对扫描列表中的每个项目分别执行:
|
|
190
|
-
1. 检查
|
|
190
|
+
1. 检查 \`{DOCS_ROOT}/modules/_module-map.yaml\` 是否已存在,已存在则跳过
|
|
191
191
|
2. 分析项目源码目录结构,识别模块划分:
|
|
192
192
|
- 用 \`find . -maxdepth 3 -type d -not -path "*/node_modules/*" -not -path "*/.git/*"\` 查看目录结构
|
|
193
193
|
- 每个有明确职责的独立目录识别为一个模块
|
|
@@ -200,7 +200,7 @@ local.yaml 生成结果(已存在/已生成)`,
|
|
|
200
200
|
4. 分析跨模块依赖关系:
|
|
201
201
|
- 用 grep import/require 分析模块间的引用链
|
|
202
202
|
- 填充 depends_on(本模块依赖谁)和 used_by(谁依赖本模块)
|
|
203
|
-
5. 生成
|
|
203
|
+
5. 生成 \`{DOCS_ROOT}/modules/_module-map.yaml\`
|
|
204
204
|
6. 如果 modules/ 目录不存在,先创建
|
|
205
205
|
7. 原子写入(先写 tmp 文件再 rename)
|
|
206
206
|
|
|
@@ -305,8 +305,8 @@ _module-map.yaml 生成结果(已存在/已生成/模块列表)`,
|
|
|
305
305
|
|
|
306
306
|
### 操作
|
|
307
307
|
对扫描列表中的每个项目分别执行:
|
|
308
|
-
1. 读取
|
|
309
|
-
2. 检查
|
|
308
|
+
1. 读取 \`{DOCS_ROOT}/modules/_module-map.yaml\`,获取模块列表和路径
|
|
309
|
+
2. 检查 \`{DOCS_ROOT}/modules/\` 下已有的模块文档(<module>.md)
|
|
310
310
|
3. 列出每个模块的状态:已有文档 / 缺失
|
|
311
311
|
4. **必须停下来问用户**:
|
|
312
312
|
- 展示模块列表及现有文档状态
|
|
@@ -323,7 +323,7 @@ _module-map.yaml 生成结果(已存在/已生成/模块列表)`,
|
|
|
323
323
|
\`\`\`
|
|
324
324
|
模块名:<module-id>
|
|
325
325
|
模块路径:<glob patterns>
|
|
326
|
-
|
|
326
|
+
目标文件:{DOCS_ROOT}/modules/<module-id>.md
|
|
327
327
|
|
|
328
328
|
操作:
|
|
329
329
|
1. 用 grep/rg 搜索模块路径范围内的源码(禁止读源码全文)
|
|
@@ -380,7 +380,7 @@ module_id: <module-id>
|
|
|
380
380
|
⚠️ 这一步是可选的。如果项目模块简单、流程不明显,可以跳过。
|
|
381
381
|
|
|
382
382
|
### flows/ 目录
|
|
383
|
-
|
|
383
|
+
目标目录:\`{DOCS_ROOT}/flows/\`
|
|
384
384
|
|
|
385
385
|
根据 _module-map.yaml 中的模块依赖关系,识别跨模块业务流程:
|
|
386
386
|
1. 读取 \`_module-map.yaml\`,分析 used_by 链条
|
|
@@ -410,7 +410,7 @@ step1 → step2 → step3
|
|
|
410
410
|
\`\`\`
|
|
411
411
|
|
|
412
412
|
### glossary.md
|
|
413
|
-
|
|
413
|
+
目标文件:\`{DOCS_ROOT}/glossary.md\`
|
|
414
414
|
|
|
415
415
|
提取项目专有术语:
|
|
416
416
|
1. 用 grep 搜索 TODO/FIXME 注释中的术语定义
|
|
@@ -446,11 +446,11 @@ step1 → step2 → step3
|
|
|
446
446
|
|
|
447
447
|
### 操作
|
|
448
448
|
对扫描列表中的每个项目分别执行:
|
|
449
|
-
1. 检查 7 份 scan
|
|
450
|
-
2.
|
|
449
|
+
1. 检查 7 份 scan 文档是否全部生成(\`{DOCS_ROOT}/scan/\`)
|
|
450
|
+
2. 检查模块文档状态(\`{DOCS_ROOT}/modules/\`)
|
|
451
451
|
3. 自检门控:ARCHITECTURE(技术栈+Schema摘要)、CONVENTIONS(隐形规则+代码风格)、STRUCTURE(目录结构)、INTEGRATIONS(外部依赖)、TESTING(测试现状)、CONCERNS(技术债务)、PROJECT(项目概览)
|
|
452
452
|
4. 检查 flows/ 和 glossary.md 是否已生成(如有)
|
|
453
|
-
5. 清理:\`rm -f
|
|
453
|
+
5. 清理:\`rm -f {DOCS_ROOT}/scan/_env-detect.md\`
|
|
454
454
|
6. \`git add .sillyspec/\` — 暂存扫描结果(不要 commit,由用户通过统一提交工具处理)
|
|
455
455
|
|
|
456
456
|
### 输出
|
package/src/workflow.js
CHANGED
|
@@ -630,8 +630,12 @@ export function generateAllRolePrompts(wf, projectName, context = {}) {
|
|
|
630
630
|
* @returns {string|null} 保存路径,失败返回 null
|
|
631
631
|
*/
|
|
632
632
|
export function saveWorkflowRun(result, options = {}) {
|
|
633
|
-
const { cwd = '.', source = 'unknown', stage, step } = options
|
|
634
|
-
|
|
633
|
+
const { cwd = '.', source = 'unknown', stage, step, runtimeRoot, scanRunId } = options
|
|
634
|
+
// 平台模式:写入 runtime-root/scan-runs/<scan-run-id>/workflow-runs/
|
|
635
|
+
// 本地模式:写入 cwd/.sillyspec/.runtime/workflow-runs/
|
|
636
|
+
const runDir = runtimeRoot
|
|
637
|
+
? join(runtimeRoot, 'scan-runs', scanRunId || 'unknown', 'workflow-runs')
|
|
638
|
+
: join(cwd, '.sillyspec', '.runtime', 'workflow-runs')
|
|
635
639
|
try {
|
|
636
640
|
mkdirSync(runDir, { recursive: true })
|
|
637
641
|
} catch (e) {
|
package/src/worktree-apply.js
CHANGED
|
@@ -341,10 +341,24 @@ export function formatExecuteSummary({ changeName, stepsCompleted, stepsTotal, a
|
|
|
341
341
|
? `dirty (${baselineCount} baseline file${baselineCount === 1 ? '' : 's'} protected)`
|
|
342
342
|
: 'clean';
|
|
343
343
|
|
|
344
|
+
// Worktree 最终状态
|
|
345
|
+
const mode = meta.mode || 'worktree';
|
|
346
|
+
let worktreeStatus;
|
|
347
|
+
if (mode === 'native-worktree') {
|
|
348
|
+
worktreeStatus = 'kept (external worktree)';
|
|
349
|
+
} else if (mode === 'in-place-fallback') {
|
|
350
|
+
worktreeStatus = 'none (in-place)';
|
|
351
|
+
} else if (!wtExists) {
|
|
352
|
+
worktreeStatus = 'cleaned';
|
|
353
|
+
} else {
|
|
354
|
+
worktreeStatus = 'exists';
|
|
355
|
+
}
|
|
356
|
+
|
|
344
357
|
lines.push(`Status: COMPLETED`);
|
|
345
358
|
lines.push(`Steps: ${stepsCompleted} / ${stepsTotal}`);
|
|
346
359
|
lines.push(`Baseline: ${baselineStatus}`);
|
|
347
360
|
lines.push(`Apply: ${applyStatus}`);
|
|
361
|
+
lines.push(`Worktree: ${worktreeStatus}`);
|
|
348
362
|
}
|
|
349
363
|
|
|
350
364
|
// --- Changed files ---
|