sillyspec 3.17.4 → 3.17.5
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/run.js +265 -0
package/package.json
CHANGED
package/src/run.js
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { basename, join, resolve } from 'path'
|
|
8
8
|
import { existsSync, readdirSync, mkdirSync, writeFileSync, appendFileSync, readFileSync, rmSync, statSync } from 'fs'
|
|
9
|
+
import { createRequire } from 'module'
|
|
10
|
+
const require = createRequire(import.meta.url)
|
|
9
11
|
import { ProgressManager } from './progress.js'
|
|
10
12
|
|
|
11
13
|
/**
|
|
@@ -392,6 +394,23 @@ async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjec
|
|
|
392
394
|
platformDirectives.push(`workspace_id: ${platformOpts.workspaceId}`)
|
|
393
395
|
}
|
|
394
396
|
promptText = platformDirectives.join('\n') + '\n\n' + promptText
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// 注入 scanProfile 硬约束指令
|
|
400
|
+
if (stageName === 'scan' && platformOpts?.scanProfile) {
|
|
401
|
+
const sp = platformOpts.scanProfile
|
|
402
|
+
const profileDirectives = []
|
|
403
|
+
profileDirectives.push(`## 📊 Scan Profile: ${sp.mode} (${sp.reason})`)
|
|
404
|
+
if (sp.maxAgentCalls === 0) {
|
|
405
|
+
profileDirectives.push(`**⛔ 严禁使用子代理(Agent/Task 工具)。** 必须在本 turn 内完成所有工作。`)
|
|
406
|
+
} else if (sp.maxAgentCalls > 0) {
|
|
407
|
+
profileDirectives.push(`**子代理上限:${sp.maxAgentCalls} 个。** 不要超出。`)
|
|
408
|
+
}
|
|
409
|
+
if (sp.maxDocs < 99) {
|
|
410
|
+
profileDirectives.push(`**文档上限:${sp.maxDocs} 份。** 只生成核心文档,不要额外生成 flows/glossary/module-card。`)
|
|
411
|
+
}
|
|
412
|
+
profileDirectives.push(`--output 只需要列出文件名,不要写长篇总结。`)
|
|
413
|
+
promptText = profileDirectives.join('\n') + '\n\n' + promptText
|
|
395
414
|
} else {
|
|
396
415
|
// 非 platform 模式也要替换占位符
|
|
397
416
|
if (stageName === 'scan') {
|
|
@@ -433,6 +452,198 @@ async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjec
|
|
|
433
452
|
console.log(`sillyspec run ${stageName} --done${changeFlag} --input "用户原始需求/反馈" --output "你的摘要"`)
|
|
434
453
|
}
|
|
435
454
|
|
|
455
|
+
/**
|
|
456
|
+
* 根据 project 规模计算 scan profile
|
|
457
|
+
* quick: fileCount≤30 && sourceBytes≤80KB && projectCount≤3 → 3 步,0 子代理,5 份文档
|
|
458
|
+
* standard: fileCount≤200 && sourceBytes≤800KB → 压缩步骤,最多 1 子代理
|
|
459
|
+
* deep: 大项目或 --deep → 完整流程
|
|
460
|
+
*/
|
|
461
|
+
function computeScanProfile(cwd, platformOpts) {
|
|
462
|
+
// --deep 标志强制 deep
|
|
463
|
+
const flags = process.argv.slice(2)
|
|
464
|
+
if (flags.includes('--deep')) {
|
|
465
|
+
return { mode: 'deep', reason: '用户指定 --deep', maxAgentCalls: 4, maxDocs: 99 }
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const specDir = platformOpts?.specRoot || join(cwd, '.sillyspec')
|
|
469
|
+
const projectsDir = join(specDir, 'projects')
|
|
470
|
+
let projectCount = 1
|
|
471
|
+
try {
|
|
472
|
+
if (existsSync(projectsDir)) {
|
|
473
|
+
projectCount = readdirSync(projectsDir).filter(f => f.endsWith('.yaml')).length
|
|
474
|
+
}
|
|
475
|
+
} catch {}
|
|
476
|
+
|
|
477
|
+
// 快速估算源码规模
|
|
478
|
+
let fileCount = 0
|
|
479
|
+
let sourceBytes = 0
|
|
480
|
+
try {
|
|
481
|
+
const { execSync } = require('child_process')
|
|
482
|
+
const findCmd = `find "${cwd}" -type f \\( -name '*.js' -o -name '*.ts' -o -name '*.tsx' -o -name '*.py' -o -name '*.java' -o -name '*.go' -o -name '*.rs' -o -name '*.rb' -o -name '*.php' -o -name '*.c' -o -name '*.cpp' -o -name '*.h' -o -name '*.jsx' -o -name '*.vue' -o -name '*.svelte' \\) -not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/dist/*' -not -path '*/build/*' -not -path '*/__pycache__/*' -not -path '*/.sillyspec/*' -not -path '*/.claude/*' 2>/dev/null`
|
|
483
|
+
const files = execSync(findCmd, { encoding: 'utf8', timeout: 10000 }).trim().split('\n').filter(Boolean)
|
|
484
|
+
fileCount = files.length
|
|
485
|
+
for (const f of files) {
|
|
486
|
+
try { sourceBytes += statSync(f).size } catch {}
|
|
487
|
+
}
|
|
488
|
+
} catch {
|
|
489
|
+
// find 失败时假设中等规模
|
|
490
|
+
return { mode: 'standard', reason: '无法估算项目规模', maxAgentCalls: 1, maxDocs: 8 }
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (fileCount <= 30 && sourceBytes <= 80_000 && projectCount <= 3) {
|
|
494
|
+
return { mode: 'quick', reason: `${fileCount} 源文件, ${Math.round(sourceBytes / 1024)}KB`, maxAgentCalls: 0, maxDocs: 5, _fileCount: fileCount, _sourceBytes: sourceBytes, _projectCount: projectCount }
|
|
495
|
+
}
|
|
496
|
+
if (fileCount <= 200 && sourceBytes <= 800_000) {
|
|
497
|
+
return { mode: 'standard', reason: `${fileCount} 源文件, ${Math.round(sourceBytes / 1024)}KB`, maxAgentCalls: 1, maxDocs: 8, _fileCount: fileCount, _sourceBytes: sourceBytes, _projectCount: projectCount }
|
|
498
|
+
}
|
|
499
|
+
return { mode: 'deep', reason: `${fileCount} 源文件, ${Math.round(sourceBytes / 1024)}KB`, maxAgentCalls: 4, maxDocs: 99, _fileCount: fileCount, _sourceBytes: sourceBytes, _projectCount: projectCount }
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* 根据 scanProfile 裁剪步骤
|
|
504
|
+
* quick: 3 步 — CLI preflight / AI generate / CLI postcheck
|
|
505
|
+
* standard: 跳过续扫检测(4), 跳过可选步骤(9)
|
|
506
|
+
*/
|
|
507
|
+
function applyScanProfileSteps(stageData, profile, cwd, platformOpts) {
|
|
508
|
+
const steps = stageData.steps
|
|
509
|
+
const mode = profile.mode
|
|
510
|
+
|
|
511
|
+
if (mode === 'quick') {
|
|
512
|
+
const specBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
|
|
513
|
+
const projectName = basename(cwd)
|
|
514
|
+
const docsRoot = join(specBase, 'docs', projectName)
|
|
515
|
+
|
|
516
|
+
// Step 1: CLI preflight(不调 AI,自动完成)
|
|
517
|
+
const step1 = {
|
|
518
|
+
name: '项目概览(自动探测)',
|
|
519
|
+
status: 'pending',
|
|
520
|
+
noAI: true,
|
|
521
|
+
_cliAction: 'scanPreflight',
|
|
522
|
+
prompt: '',
|
|
523
|
+
outputHint: 'preflight 结果',
|
|
524
|
+
optional: false
|
|
525
|
+
}
|
|
526
|
+
// Step 2: AI 生成核心文档(唯一 AI roundtrip)
|
|
527
|
+
const step2 = {
|
|
528
|
+
name: '生成核心文档',
|
|
529
|
+
status: 'pending',
|
|
530
|
+
prompt: `## Quick Scan — 核心文档生成
|
|
531
|
+
|
|
532
|
+
项目规模较小(quick profile),请一次性生成所有核心文档。
|
|
533
|
+
|
|
534
|
+
### 操作
|
|
535
|
+
1. 读取项目结构和关键文件(package.json / pyproject.toml / README / 入口文件)
|
|
536
|
+
2. 生成以下 4 份文档并写入 \`{DOCS_ROOT}/scan/\`:
|
|
537
|
+
- **PROJECT.md** — 项目简介、技术栈、模块划分
|
|
538
|
+
- **ARCHITECTURE.md** — 架构概览、模块关系、技术决策
|
|
539
|
+
- **CONVENTIONS.md** — 代码风格、框架隐形规则
|
|
540
|
+
- **STRUCTURE.md** — 目录树 + 模块说明
|
|
541
|
+
3. 如发现子项目,注册到 \`{PROJECTS_ROOT}/\` 下
|
|
542
|
+
|
|
543
|
+
每份文档头必须包含 frontmatter:\`author\` 和 \`created_at\`。
|
|
544
|
+
|
|
545
|
+
### ⛔ 硬约束
|
|
546
|
+
- **严禁使用子代理(Agent/Task 工具)。** 所有文档在一个 turn 内完成。
|
|
547
|
+
- 不要搜索 .sillyspec/ .claude/ .git/ node_modules/ dist/ build/
|
|
548
|
+
- --output 只需要列出生成的文件名,不要写长篇总结
|
|
549
|
+
|
|
550
|
+
### 输出
|
|
551
|
+
生成的文件列表`,
|
|
552
|
+
outputHint: '文件列表',
|
|
553
|
+
optional: false
|
|
554
|
+
}
|
|
555
|
+
// Step 3: CLI postcheck(不调 AI,自动完成)
|
|
556
|
+
const selfCheck = steps.find(s => s.name === '自检和提交') || {
|
|
557
|
+
name: '自检和提交', status: 'pending', prompt: '', outputHint: '结果', optional: false
|
|
558
|
+
}
|
|
559
|
+
const step3 = { ...selfCheck, status: 'pending', noAI: true, _cliAction: 'scanPostcheck', prompt: '' }
|
|
560
|
+
stageData.steps = [step1, step2, step3]
|
|
561
|
+
return
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (mode === 'standard') {
|
|
565
|
+
// 跳过 Step 4(断点续扫检测),跳过 Step 9(flows+glossary,可选)
|
|
566
|
+
const skipNames = ['断点续扫检测', '生成业务流程和术语表(可选)']
|
|
567
|
+
for (const step of stageData.steps) {
|
|
568
|
+
if (skipNames.includes(step.name) && step.status === 'pending') {
|
|
569
|
+
step.status = 'skipped'
|
|
570
|
+
step.skippedAt = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* CLI-only: quick scan preflight
|
|
578
|
+
* 收集项目快照,打印 summary,不调 AI
|
|
579
|
+
*/
|
|
580
|
+
async function executeScanPreflight(cwd, platformOpts, scanProfile) {
|
|
581
|
+
const specBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
|
|
582
|
+
const projectName = basename(cwd)
|
|
583
|
+
console.log(` 📁 项目: ${projectName}`)
|
|
584
|
+
console.log(` 📊 Profile: ${scanProfile.mode} (${scanProfile.reason})`)
|
|
585
|
+
// 快速列出顶层结构
|
|
586
|
+
try {
|
|
587
|
+
const { execSync } = await import('child_process')
|
|
588
|
+
const dirs = execSync(`ls -d */ 2>/dev/null | grep -v node_modules | grep -v '.git' | grep -v '.sillyspec' | grep -v '.claude' | head -20`, { cwd, encoding: 'utf8' }).trim()
|
|
589
|
+
if (dirs) {
|
|
590
|
+
console.log(` 📂 目录: ${dirs.split('\n').map(d => d.replace(/\/$/, '')).join(', ')}`)
|
|
591
|
+
}
|
|
592
|
+
} catch {}
|
|
593
|
+
console.log(` ✅ Preflight 完成,准备生成核心文档\n`)
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* CLI-only: quick scan postcheck
|
|
598
|
+
* 执行文件存在性 + manifest 检查,不调 AI
|
|
599
|
+
*/
|
|
600
|
+
async function executeScanPostcheck(cwd, platformOpts, scanProfile) {
|
|
601
|
+
const { runScanPostCheck, printScanPostCheckResult } = await import('./scan-postcheck.js')
|
|
602
|
+
const specDir = platformOpts?.specRoot || null
|
|
603
|
+
const result = runScanPostCheck({
|
|
604
|
+
cwd,
|
|
605
|
+
specDir,
|
|
606
|
+
scanMeta: {
|
|
607
|
+
projectListParsed: true,
|
|
608
|
+
manifestWritten: undefined,
|
|
609
|
+
},
|
|
610
|
+
})
|
|
611
|
+
printScanPostCheckResult(result)
|
|
612
|
+
// 写 manifest(如果还没写)
|
|
613
|
+
if (platformOpts?.specRoot) {
|
|
614
|
+
try {
|
|
615
|
+
const { mkdirSync, writeFileSync } = await import('fs')
|
|
616
|
+
const { join: pJoin, basename: pBasename } = await import('path')
|
|
617
|
+
const { execSync } = await import('child_process')
|
|
618
|
+
const manifestDir = platformOpts.specRoot
|
|
619
|
+
let sourceCommit = null
|
|
620
|
+
try {
|
|
621
|
+
sourceCommit = execSync('git rev-parse HEAD', { cwd, encoding: 'utf8', timeout: 5000 }).trim()
|
|
622
|
+
} catch {}
|
|
623
|
+
mkdirSync(manifestDir, { recursive: true })
|
|
624
|
+
const manifest = {
|
|
625
|
+
scan_profile: {
|
|
626
|
+
mode: scanProfile.mode,
|
|
627
|
+
file_count: scanProfile._fileCount || 0,
|
|
628
|
+
source_bytes: scanProfile._sourceBytes || 0,
|
|
629
|
+
project_count: scanProfile._projectCount || 0,
|
|
630
|
+
reason: scanProfile.reason,
|
|
631
|
+
},
|
|
632
|
+
workspace_id: platformOpts.workspaceId || null,
|
|
633
|
+
scan_run_id: platformOpts.scanRunId || null,
|
|
634
|
+
source_commit: sourceCommit,
|
|
635
|
+
generated_at: new Date().toISOString(),
|
|
636
|
+
schema_version: 2,
|
|
637
|
+
}
|
|
638
|
+
const manifestPath = pJoin(manifestDir, 'manifest.json')
|
|
639
|
+
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n')
|
|
640
|
+
console.log(` 📄 manifest.json 已写入: ${manifestPath}`)
|
|
641
|
+
} catch (e) {
|
|
642
|
+
console.warn(` ⚠️ manifest 写入失败: ${e.message}`)
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
436
647
|
/**
|
|
437
648
|
* sillyspec run <stage> 主命令
|
|
438
649
|
*/
|
|
@@ -753,6 +964,24 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
|
|
|
753
964
|
const steps = stageData.steps
|
|
754
965
|
let currentIdx = steps.findIndex(s => s.status !== 'completed' && s.status !== 'skipped')
|
|
755
966
|
|
|
967
|
+
// ── scanProfile: 根据 project 规模动态裁剪步骤 ──
|
|
968
|
+
let scanProfile = null
|
|
969
|
+
if (stageName === 'scan' && steps.length > 0 && currentIdx === 0) {
|
|
970
|
+
scanProfile = computeScanProfile(cwd, platformOpts)
|
|
971
|
+
console.log(`\n📊 Scan Profile: ${scanProfile.mode} (原因: ${scanProfile.reason})`)
|
|
972
|
+
if (scanProfile.mode !== 'deep') {
|
|
973
|
+
applyScanProfileSteps(stageData, scanProfile, cwd, platformOpts)
|
|
974
|
+
// 步骤被裁剪后 currentIdx 需要重新计算
|
|
975
|
+
currentIdx = 0
|
|
976
|
+
}
|
|
977
|
+
// 保存 profile 供后续 postcheck 使用
|
|
978
|
+
stageData.scanProfile = scanProfile
|
|
979
|
+
await pm._write(cwd, progress, changeName)
|
|
980
|
+
} else if (stageName === 'scan' && stageData.scanProfile) {
|
|
981
|
+
scanProfile = stageData.scanProfile
|
|
982
|
+
}
|
|
983
|
+
if (scanProfile) platformOpts.scanProfile = scanProfile
|
|
984
|
+
|
|
756
985
|
if (currentIdx === -1) {
|
|
757
986
|
// 已完成 → 自动重置,重新开始
|
|
758
987
|
const freshSteps = await getStageSteps(stageName, cwd, progress)
|
|
@@ -810,6 +1039,33 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
|
|
|
810
1039
|
|
|
811
1040
|
const defSteps = await getStageSteps(stageName, cwd, progress)
|
|
812
1041
|
if (defSteps && defSteps[currentIdx]) {
|
|
1042
|
+
// noAI 步骤自动完成(CLI-only,不需要 Agent 参与)
|
|
1043
|
+
if (defSteps[currentIdx].noAI || stageData.steps[currentIdx]?.noAI) {
|
|
1044
|
+
const stepName = defSteps[currentIdx].name
|
|
1045
|
+
const cliAction = defSteps[currentIdx]._cliAction || stageData.steps[currentIdx]?._cliAction
|
|
1046
|
+
console.log(`⚙️ Step ${currentIdx + 1}/${stageData.steps.length}: ${stepName}(CLI 自动执行,无需 Agent)`)
|
|
1047
|
+
if (cliAction === 'scanPreflight') {
|
|
1048
|
+
await executeScanPreflight(cwd, platformOpts, scanProfile)
|
|
1049
|
+
} else if (cliAction === 'scanPostcheck') {
|
|
1050
|
+
await executeScanPostcheck(cwd, platformOpts, scanProfile)
|
|
1051
|
+
}
|
|
1052
|
+
stageData.steps[currentIdx].status = 'completed'
|
|
1053
|
+
stageData.steps[currentIdx].completedAt = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
1054
|
+
await pm._write(cwd, progress, changeName)
|
|
1055
|
+
// 自动前进到下一步
|
|
1056
|
+
const nextIdx = stageData.steps.findIndex(s => s.status === 'pending')
|
|
1057
|
+
if (nextIdx !== -1 && defSteps[nextIdx]) {
|
|
1058
|
+
console.log('')
|
|
1059
|
+
await outputStep(stageName, nextIdx, defSteps, cwd, changeName, progress.project || null, platformOpts)
|
|
1060
|
+
} else {
|
|
1061
|
+
// 所有步骤完成
|
|
1062
|
+
stageData.status = 'completed'
|
|
1063
|
+
stageData.completedAt = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
1064
|
+
await pm._write(cwd, progress, changeName)
|
|
1065
|
+
console.log(`\n✅ ${stageName} 阶段全部完成。`)
|
|
1066
|
+
}
|
|
1067
|
+
return
|
|
1068
|
+
}
|
|
813
1069
|
await outputStep(stageName, currentIdx, defSteps, cwd, changeName, progress.project || null, platformOpts)
|
|
814
1070
|
}
|
|
815
1071
|
}
|
|
@@ -929,6 +1185,13 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
929
1185
|
const { printNext = true, confirm = false, changeName, platformOpts = {} } = options
|
|
930
1186
|
const specBase = platformOpts.specRoot || join(cwd, '.sillyspec')
|
|
931
1187
|
const stageData = progress.stages[stageName]
|
|
1188
|
+
const scanProfile = stageData?.scanProfile || null
|
|
1189
|
+
|
|
1190
|
+
// scanProfile 非 deep 模式:截断 outputText 减少 token 传递
|
|
1191
|
+
let effectiveOutput = outputText
|
|
1192
|
+
if (scanProfile && scanProfile.mode !== 'deep' && outputText && outputText.length > 1000) {
|
|
1193
|
+
effectiveOutput = outputText.slice(0, 1000) + '\n\n…[输出已截断,完整内容见 artifact]'
|
|
1194
|
+
}
|
|
932
1195
|
if (!stageData || !stageData.steps) {
|
|
933
1196
|
console.error(`❌ 阶段 ${stageName} 未初始化`)
|
|
934
1197
|
process.exit(1)
|
|
@@ -948,6 +1211,8 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
948
1211
|
const MAX_OUTPUT = 200
|
|
949
1212
|
if (outputText.length > MAX_OUTPUT) {
|
|
950
1213
|
steps[currentIdx].output = outputText.slice(0, MAX_OUTPUT) + '…'
|
|
1214
|
+
steps[currentIdx].output_truncated = true
|
|
1215
|
+
steps[currentIdx].output_original_length = outputText.length
|
|
951
1216
|
// 平台模式:artifact 写入 runtime-root,否则写 .sillyspec/.runtime/artifacts
|
|
952
1217
|
const artifactBase = platformOpts?.runtimeRoot
|
|
953
1218
|
? join(platformOpts.runtimeRoot, 'scan-runs', platformOpts.scanRunId || 'unknown')
|