sillyspec 3.15.0 → 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 +185 -16
- 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 +119 -6
- package/src/worktree.js +209 -17
- package/test/scan-paths.test.mjs +68 -0
package/src/run.js
CHANGED
|
@@ -10,6 +10,7 @@ import { ProgressManager } from './progress.js'
|
|
|
10
10
|
import { stageRegistry, auxiliaryStages } from './stages/index.js'
|
|
11
11
|
import { buildExecuteSteps } from './stages/execute.js'
|
|
12
12
|
import { buildPlanSteps } from './stages/plan.js'
|
|
13
|
+
import { formatExecuteSummary } from './worktree-apply.js'
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* 同步触发辅助函数:_write 后 best-effort 同步到平台
|
|
@@ -146,7 +147,7 @@ async function ensureStageSteps(progress, stageName, cwd) {
|
|
|
146
147
|
/**
|
|
147
148
|
* 输出当前步骤的 prompt
|
|
148
149
|
*/
|
|
149
|
-
async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjectName) {
|
|
150
|
+
async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjectName, platformOpts = {}) {
|
|
150
151
|
const step = steps[stepIndex]
|
|
151
152
|
const total = steps.length
|
|
152
153
|
const projectName = dbProjectName || basename(cwd)
|
|
@@ -217,6 +218,43 @@ async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjec
|
|
|
217
218
|
if (changeName && promptText.includes('<change-name>')) {
|
|
218
219
|
promptText = promptText.replace(/<change-name>/g, changeName)
|
|
219
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
|
+
|
|
220
258
|
console.log(promptText)
|
|
221
259
|
console.log(`\n### ⚠️ 铁律`)
|
|
222
260
|
console.log('- **文档是核心资产,代码是文档的产物。** 没有文档就没有代码——文档是 AI 的记忆,是团队协作的基础,是后续维护的唯一依据。任何代码产出必须先有对应的设计/规范文档支撑。')
|
|
@@ -264,6 +302,50 @@ export async function runCommand(args, cwd) {
|
|
|
264
302
|
const isConfirm = flags.includes('--confirm')
|
|
265
303
|
const isSkipApproval = flags.includes('--skip-approval')
|
|
266
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
|
+
|
|
267
349
|
// 解析 --output
|
|
268
350
|
let outputText = null
|
|
269
351
|
const outputIdx = flags.indexOf('--output')
|
|
@@ -285,6 +367,26 @@ export async function runCommand(args, cwd) {
|
|
|
285
367
|
changeName = flags[changeIdx + 1]
|
|
286
368
|
}
|
|
287
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
|
+
|
|
288
390
|
const isAuxiliary = auxiliaryStages.includes(stageName)
|
|
289
391
|
|
|
290
392
|
const pm = new ProgressManager()
|
|
@@ -360,11 +462,11 @@ export async function runCommand(args, cwd) {
|
|
|
360
462
|
|
|
361
463
|
// --done
|
|
362
464
|
if (isDone) {
|
|
363
|
-
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 })
|
|
364
466
|
}
|
|
365
467
|
|
|
366
468
|
// 默认:输出当前步骤
|
|
367
|
-
return await runStage(pm, progress, stageName, cwd, effectiveChange, isSkipApproval)
|
|
469
|
+
return await runStage(pm, progress, stageName, cwd, effectiveChange, isSkipApproval, platformOpts)
|
|
368
470
|
}
|
|
369
471
|
|
|
370
472
|
/**
|
|
@@ -379,7 +481,7 @@ function resolveChangeNameAuto(cwd) {
|
|
|
379
481
|
return null
|
|
380
482
|
}
|
|
381
483
|
|
|
382
|
-
async function runStage(pm, progress, stageName, cwd, changeName, skipApproval = false) {
|
|
484
|
+
async function runStage(pm, progress, stageName, cwd, changeName, skipApproval = false, platformOpts = {}) {
|
|
383
485
|
// execute 阶段启动前检查审批
|
|
384
486
|
if (stageName === 'execute' && !skipApproval) {
|
|
385
487
|
const approval = await checkApproval(cwd, changeName)
|
|
@@ -443,7 +545,7 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
|
|
|
443
545
|
|
|
444
546
|
const defSteps = await getStageSteps(stageName, cwd, progress)
|
|
445
547
|
if (defSteps && defSteps[currentIdx]) {
|
|
446
|
-
await outputStep(stageName, currentIdx, defSteps, cwd, changeName, progress.project || null)
|
|
548
|
+
await outputStep(stageName, currentIdx, defSteps, cwd, changeName, progress.project || null, platformOpts)
|
|
447
549
|
}
|
|
448
550
|
}
|
|
449
551
|
|
|
@@ -526,7 +628,7 @@ function validateFileLocations(cwd, stageName, progress, changeName) {
|
|
|
526
628
|
}
|
|
527
629
|
|
|
528
630
|
async function completeStep(pm, progress, stageName, cwd, outputText, inputText = null, options = {}) {
|
|
529
|
-
const { printNext = true, confirm = false, changeName } = options
|
|
631
|
+
const { printNext = true, confirm = false, changeName, platformOpts = {} } = options
|
|
530
632
|
const stageData = progress.stages[stageName]
|
|
531
633
|
if (!stageData || !stageData.steps) {
|
|
532
634
|
console.error(`❌ 阶段 ${stageName} 未初始化`)
|
|
@@ -547,10 +649,13 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
547
649
|
const MAX_OUTPUT = 200
|
|
548
650
|
if (outputText.length > MAX_OUTPUT) {
|
|
549
651
|
steps[currentIdx].output = outputText.slice(0, MAX_OUTPUT) + '…'
|
|
550
|
-
|
|
551
|
-
|
|
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 })
|
|
552
657
|
const ts = new Date().toISOString().slice(0,19).replace(/[-T:]/g, '')
|
|
553
|
-
writeFileSync(join(
|
|
658
|
+
writeFileSync(join(artifactBase, `${changeName || 'unknown'}-${stageName}-step${currentIdx + 1}-${ts}.txt`), outputText)
|
|
554
659
|
} else {
|
|
555
660
|
steps[currentIdx].output = outputText
|
|
556
661
|
}
|
|
@@ -682,6 +787,56 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
682
787
|
appendFileSync(inputsPath, entry)
|
|
683
788
|
}
|
|
684
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
|
+
|
|
685
840
|
validateMetadata(cwd, stageName)
|
|
686
841
|
|
|
687
842
|
// 验证关键文件是否在正确的变更目录下
|
|
@@ -745,7 +900,21 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
745
900
|
console.log(`✅ ${stageName} 阶段已完成(${total}/${total} 步)`)
|
|
746
901
|
|
|
747
902
|
if (stageName === 'execute') {
|
|
748
|
-
|
|
903
|
+
// execute run summary:展示真实可得的结构化信息
|
|
904
|
+
try {
|
|
905
|
+
const lastOutput = steps[steps.length - 1]?.output || ''
|
|
906
|
+
const summary = formatExecuteSummary({
|
|
907
|
+
changeName,
|
|
908
|
+
stepsCompleted: total,
|
|
909
|
+
stepsTotal: total,
|
|
910
|
+
agentSummary: lastOutput,
|
|
911
|
+
cwd,
|
|
912
|
+
})
|
|
913
|
+
console.log(`\n${summary}`)
|
|
914
|
+
} catch (e) {
|
|
915
|
+
// summary 失败不影响主流程
|
|
916
|
+
console.log('\n👉 下一步:sillyspec run verify(验证通过后才能归档)')
|
|
917
|
+
}
|
|
749
918
|
} else if (stageName === 'verify') {
|
|
750
919
|
console.log('\n👉 下一步:sillyspec run archive(验证通过,可以归档了)')
|
|
751
920
|
} else if (stageName === 'archive') {
|
|
@@ -847,7 +1016,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
847
1016
|
}
|
|
848
1017
|
|
|
849
1018
|
if (printNext) {
|
|
850
|
-
await outputStep(stageName, nextPendingIdx, defSteps, cwd, changeName, progress.project || null)
|
|
1019
|
+
await outputStep(stageName, nextPendingIdx, defSteps, cwd, changeName, progress.project || null, platformOpts)
|
|
851
1020
|
}
|
|
852
1021
|
return { stageCompleted: false, currentIdx, nextPendingIdx }
|
|
853
1022
|
}
|
|
@@ -885,7 +1054,7 @@ async function skipStep(pm, progress, stageName, cwd, changeName) {
|
|
|
885
1054
|
const nextPendingIdx = steps.findIndex(s => s.status === 'pending')
|
|
886
1055
|
if (nextPendingIdx !== -1 && defSteps) {
|
|
887
1056
|
console.log('')
|
|
888
|
-
await outputStep(stageName, nextPendingIdx, defSteps, cwd, changeName, progress.project || null)
|
|
1057
|
+
await outputStep(stageName, nextPendingIdx, defSteps, cwd, changeName, progress.project || null, platformOpts)
|
|
889
1058
|
}
|
|
890
1059
|
}
|
|
891
1060
|
|
|
@@ -1030,7 +1199,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
|
|
|
1030
1199
|
}
|
|
1031
1200
|
}
|
|
1032
1201
|
}
|
|
1033
|
-
await outputStep(currentStage, pendingIdx, defSteps, cwd, changeName, progress.project || null)
|
|
1202
|
+
await outputStep(currentStage, pendingIdx, defSteps, cwd, changeName, progress.project || null, platformOpts)
|
|
1034
1203
|
return
|
|
1035
1204
|
}
|
|
1036
1205
|
|
|
@@ -1039,7 +1208,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
|
|
|
1039
1208
|
process.exit(1)
|
|
1040
1209
|
}
|
|
1041
1210
|
|
|
1042
|
-
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 })
|
|
1043
1212
|
if (!result) return
|
|
1044
1213
|
progress = await pm.read(cwd, changeName)
|
|
1045
1214
|
|
|
@@ -1060,7 +1229,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
|
|
|
1060
1229
|
}
|
|
1061
1230
|
}
|
|
1062
1231
|
}
|
|
1063
|
-
await outputStep(currentStage, nextPendingIdx, defSteps, cwd, changeName, progress.project || null)
|
|
1232
|
+
await outputStep(currentStage, nextPendingIdx, defSteps, cwd, changeName, progress.project || null, platformOpts)
|
|
1064
1233
|
return
|
|
1065
1234
|
}
|
|
1066
1235
|
|
|
@@ -1102,6 +1271,6 @@ async function runAutoMode(pm, progress, cwd, flags, changeName) {
|
|
|
1102
1271
|
}
|
|
1103
1272
|
}
|
|
1104
1273
|
}
|
|
1105
|
-
await outputStep(next, firstPending, nextSteps, cwd, changeName, progress.project || null)
|
|
1274
|
+
await outputStep(next, firstPending, nextSteps, cwd, changeName, progress.project || null, platformOpts)
|
|
1106
1275
|
}
|
|
1107
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) {
|