sillyspec 3.26.7 → 3.26.9
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/.claude/skills/sillyspec-archive/SKILL.md +1 -2
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +1 -2
- package/.claude/skills/sillyspec-doctor/SKILL.md +2 -0
- package/.claude/skills/sillyspec-execute/SKILL.md +10 -2
- package/.claude/skills/sillyspec-explore/SKILL.md +0 -1
- package/.claude/skills/sillyspec-plan/SKILL.md +1 -2
- package/.claude/skills/sillyspec-quick/SKILL.md +1 -2
- package/.claude/skills/sillyspec-scan/SKILL.md +2 -2
- package/.claude/skills/sillyspec-status/SKILL.md +0 -1
- package/.claude/skills/sillyspec-verify/SKILL.md +1 -2
- package/.husky/pre-push +9 -2
- package/README.md +5 -9
- package/bin/sillyspec.js +1 -1
- package/package.json +2 -2
- package/packages/dashboard/server/index.js +14 -8
- package/src/change-risk-profile.js +5 -207
- package/src/config-schema.js +310 -287
- package/src/constants.js +97 -106
- package/src/db-engine.js +1 -1
- package/src/docs-check.js +362 -0
- package/src/docs-debt.js +226 -0
- package/src/docs-gate.js +106 -0
- package/src/doctor-diagnostics.js +23 -2
- package/src/git-helper.js +2 -2
- package/src/hooks/worktree-guard.js +1 -1
- package/src/index.js +1963 -1787
- package/src/init.js +95 -17
- package/src/machine-interface.js +7 -4
- package/src/modules.js +490 -486
- package/src/progress/stage-machine.js +3 -0
- package/src/progress.js +37 -13
- package/src/quicklog.js +182 -1
- package/src/run/command.js +154 -26
- package/src/run/complete-handlers.js +1254 -1128
- package/src/run/complete.js +9 -33
- package/src/run/gates.js +85 -1
- package/src/run/prompt.js +82 -1
- package/src/run/quick-audit.js +23 -0
- package/src/run/shared.js +273 -5
- package/src/run/stage.js +17 -1
- package/src/scan-diff.js +388 -0
- package/src/scan-staleness.js +95 -0
- package/src/stage-contract-spec.js +16 -1
- package/src/stage-contract.js +8 -5
- package/src/stage-review.js +1 -1
- package/src/stages/brainstorm-auto.js +0 -1
- package/src/stages/brainstorm.js +3 -3
- package/src/stages/execute.js +35 -11
- package/src/stages/plan-postcheck.js +93 -36
- package/src/stages/plan.js +22 -7
- package/src/stages/quick.js +1 -1
- package/src/stages/scan.js +2 -2
- package/src/stages/verify.js +11 -12
- package/src/sync.js +39 -17
- package/src/task-review.js +14 -5
- package/src/workflow.js +8 -185
- package/src/worktree-apply.js +128 -3
- package/src/worktree.js +2 -2
- package/templates/claude-instruction.md +1 -1
- package/templates/prompts/taskcard-rules.md +1 -0
- package/templates/prompts/verify-probes.md +2 -2
- package/templates/skills/sillyspec-onboard/SKILL.md +0 -92
package/src/task-review.js
CHANGED
|
@@ -30,9 +30,9 @@ import { resolveRuntimeRoot } from './run/shared.js'
|
|
|
30
30
|
// 跨仓 task 写 review.json 可用 v2 带 repo 字段,单仓/旧 change 仍用 v1。后续可拆分 stage-review 专用
|
|
31
31
|
// 常量,本期遵循 allowed_paths 约束不扩散到 stage-review.js。
|
|
32
32
|
export const REVIEW_SCHEMA_VERSION = 1
|
|
33
|
-
|
|
33
|
+
const REVIEW_SCHEMA_VERSIONS_ACCEPTED = [1, 2]
|
|
34
34
|
// repo 缺省值(review.repo / task 卡 repo 缺省时统一按 'main' 处理)。design §7.2 / §7.4。
|
|
35
|
-
|
|
35
|
+
const DEFAULT_REPO_KEY = 'main'
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* 规范化 review.repo / task 卡 repo 字段为有效 repoKey。
|
|
@@ -53,7 +53,7 @@ export const VALID_VERDICTS = ['pass', 'fail', 'cannot_verify']
|
|
|
53
53
|
* @param {string} planContent - plan.md 文件内容
|
|
54
54
|
* @returns {string[]} task id 列表,如 ['task-01', 'task-02']
|
|
55
55
|
*/
|
|
56
|
-
|
|
56
|
+
function parseTaskIdsFromPlan(planContent) {
|
|
57
57
|
if (!planContent) return []
|
|
58
58
|
const ids = new Set()
|
|
59
59
|
const re = /^\s*[-*]\s*\[[ x]\]\s*task-(\d+)/gim
|
|
@@ -442,7 +442,8 @@ export function validateTaskReviews(opts) {
|
|
|
442
442
|
} else if (repoKey !== 'main') {
|
|
443
443
|
// 跨仓 repo 在 ctx 中未注册 → 不应发生(MultiRepoContext 构造已 fail-closed 拦截未注册),
|
|
444
444
|
// 防御性降级为 warning 提示,不阻断(review.json schema 已过,仓库解析交给 ctx 上游负责)。
|
|
445
|
-
|
|
445
|
+
// 坑7 教训:此分支发生时后续主仓校验必报「疑似伪造」,文案须直接指向真实排查方向。
|
|
446
|
+
warnings.push(`${taskId}: review.repo="${repoKey}" 在 MultiRepoContext 未解析到 entry,退回主仓 gitDir 校验——下方若报 commit 不真实/疑似伪造,先查 repo 声明(plan.md 内联块或 tasks/task-NN.md 的 repo:)与 local.yaml repos: 注册,非 review 伪造`)
|
|
446
447
|
}
|
|
447
448
|
}
|
|
448
449
|
if (reviewGitDir) {
|
|
@@ -791,7 +792,15 @@ export async function generateTaskReviewDrafts({ changeName, cwd, platformOpts =
|
|
|
791
792
|
} catch {}
|
|
792
793
|
if (!executeRunId) {
|
|
793
794
|
executeRunId = generateExecuteRunId()
|
|
794
|
-
|
|
795
|
+
// D-001#1 fallback 写入点:mkdir execute-runs/<runId>/tasks 先于 marker(不变量:marker 在则目录在)。
|
|
796
|
+
// 保留 fail-open 契约(调用方 catch 降级)——只去静默:失败 console.error 留痕,不 throw
|
|
797
|
+
//(草稿兜底缺了也只是退回 gate 报缺 review.json,不比修复前差)。
|
|
798
|
+
try {
|
|
799
|
+
mkdirSync(join(runtimeRoot, 'execute-runs', executeRunId, 'tasks'), { recursive: true })
|
|
800
|
+
writeFileSync(runIdFile, executeRunId + '\n')
|
|
801
|
+
} catch (e) {
|
|
802
|
+
console.error(`[sillyspec] execute run marker/目录写入失败(降级继续,草稿落盘仍尝试): ${e.message}`)
|
|
803
|
+
}
|
|
795
804
|
}
|
|
796
805
|
|
|
797
806
|
// base..head diff 文件集(worktree-aware;null=git 不可用,[]=无 commit diff)
|
package/src/workflow.js
CHANGED
|
@@ -48,7 +48,7 @@ export function loadWorkflow(cwd, name, validate = true) {
|
|
|
48
48
|
* @param {object} wf - workflow 定义
|
|
49
49
|
* @returns {string[]} 错误列表,空数组表示通过
|
|
50
50
|
*/
|
|
51
|
-
|
|
51
|
+
function validateWorkflow(wf) {
|
|
52
52
|
const errors = []
|
|
53
53
|
const roles = wf.roles || []
|
|
54
54
|
const roleIds = new Set(roles.map(r => r.id))
|
|
@@ -256,8 +256,13 @@ export function runPostCheck(wf, cwd, projectName, placeholders = {}, specBase)
|
|
|
256
256
|
let json = JSON.stringify(resolved)
|
|
257
257
|
for (const [key, value] of Object.entries(allPlaceholders)) {
|
|
258
258
|
// 支持 {key} 和 <key> 两种占位符语法
|
|
259
|
-
|
|
260
|
-
|
|
259
|
+
// 字符串值先做 JSON 转义再嵌入:Windows 路径等含反斜杠/引号的值裸替换进 JSON 字面量会
|
|
260
|
+
// 破坏 parse(实测报「Bad escaped character」→ scan 深度扫描质量门静默 fail-open)。
|
|
261
|
+
// JSON.stringify(v).slice(1,-1) 产出合法的 JSON 字符串内容(反斜杠→\\、引号→\");
|
|
262
|
+
// 非字符串值保持 String(v) 原样(数字占位符不被引号包裹,语义不变)。
|
|
263
|
+
const embedValue = (v) => (typeof v === 'string' ? JSON.stringify(v).slice(1, -1) : String(v))
|
|
264
|
+
json = json.replace(new RegExp(`\{${key}\}`, 'g'), embedValue(value))
|
|
265
|
+
json = json.replace(new RegExp(`<${key}>`, 'g'), embedValue(value))
|
|
261
266
|
}
|
|
262
267
|
resolved = JSON.parse(json)
|
|
263
268
|
|
|
@@ -537,188 +542,6 @@ export function formatCheckReport(result) {
|
|
|
537
542
|
return lines.join('\n')
|
|
538
543
|
}
|
|
539
544
|
|
|
540
|
-
// ─── 兼容适配层 ───
|
|
541
|
-
|
|
542
|
-
/**
|
|
543
|
-
* 兼容旧接口:generateRetryPrompt
|
|
544
|
-
* @deprecated 直接用 runPostCheck 返回的 retry_prompts
|
|
545
|
-
*/
|
|
546
|
-
export function generateRetryPrompt(wf, checkResult, projectName) {
|
|
547
|
-
const resolved = replaceProjectPlaceholder(wf, projectName)
|
|
548
|
-
const lines = []
|
|
549
|
-
lines.push('上一次 workflow 执行存在失败项,请重试。\n')
|
|
550
|
-
|
|
551
|
-
const roles = resolved.roles || []
|
|
552
|
-
const roleResults = checkResult.roles || []
|
|
553
|
-
for (const r of roleResults) {
|
|
554
|
-
if (r.status === 'pass') continue
|
|
555
|
-
const role = roles.find(rl => rl.id === r.id)
|
|
556
|
-
if (!role) continue
|
|
557
|
-
|
|
558
|
-
lines.push(`### 失败角色:${r.name} (${r.id})`)
|
|
559
|
-
lines.push(`失败原因:`)
|
|
560
|
-
const roleFailures = (checkResult.failures || []).filter(f => f.role_id === r.id)
|
|
561
|
-
for (const f of roleFailures) {
|
|
562
|
-
lines.push(`- ${f.message}`)
|
|
563
|
-
}
|
|
564
|
-
lines.push('')
|
|
565
|
-
|
|
566
|
-
for (const output of (role.outputs || [])) {
|
|
567
|
-
lines.push(`目标文件:\`${output.path}\``)
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
if (role.constraints && role.constraints.length > 0) {
|
|
571
|
-
lines.push('约束:')
|
|
572
|
-
for (const c of role.constraints) {
|
|
573
|
-
lines.push(`- ${c}`)
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
lines.push('')
|
|
578
|
-
lines.push('⚠️ 你必须确保文件写入指定路径。不要只报告完成,请用 write 工具实际写入。')
|
|
579
|
-
lines.push('')
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
return lines.join('\n')
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
// ─── Role Prompt 生成(Level 2)───
|
|
586
|
-
|
|
587
|
-
/**
|
|
588
|
-
* 根据 workflow role 定义生成子代理 prompt
|
|
589
|
-
* @param {object} wf - workflow 定义
|
|
590
|
-
* @param {string} roleId - 角色ID
|
|
591
|
-
* @param {string} projectName - 项目名
|
|
592
|
-
* @param {object} context - 额外上下文(envSummary, missingDocs 等)
|
|
593
|
-
* @returns {string|null} 生成的 prompt,或 null(角色不存在)
|
|
594
|
-
*/
|
|
595
|
-
export function generateRolePrompt(wf, roleId, projectName, context = {}) {
|
|
596
|
-
const resolved = replaceProjectPlaceholder(wf, projectName)
|
|
597
|
-
const role = (resolved.roles || []).find(r => r.id === roleId)
|
|
598
|
-
if (!role) return null
|
|
599
|
-
|
|
600
|
-
const lines = []
|
|
601
|
-
lines.push(`## 子代理任务:${role.name} (${roleId})`)
|
|
602
|
-
lines.push('')
|
|
603
|
-
lines.push(`项目:${projectName}`)
|
|
604
|
-
|
|
605
|
-
// 任务描述
|
|
606
|
-
if (role.task) {
|
|
607
|
-
lines.push(`任务:${role.task}`)
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
// 依赖角色的输出(depends_on + from_role)
|
|
611
|
-
const deps = role.depends_on || []
|
|
612
|
-
if (deps.length > 0) {
|
|
613
|
-
lines.push('')
|
|
614
|
-
lines.push('前置依赖(已完成角色的输出):')
|
|
615
|
-
const inputs = role.inputs || {}
|
|
616
|
-
for (const depId of deps) {
|
|
617
|
-
const depRole = (resolved.roles || []).find(r => r.id === depId)
|
|
618
|
-
if (!depRole) continue
|
|
619
|
-
if (inputs.from_role === depId) {
|
|
620
|
-
lines.push(`- ${depRole.name}(${depId}):${inputs.output_description || ''}`)
|
|
621
|
-
if (inputs.output) {
|
|
622
|
-
const depOutput = (depRole.outputs || []).find(o => {
|
|
623
|
-
const outputName = o.name || o.path?.split('/').pop()?.replace(/\.md$/, '') || ''
|
|
624
|
-
return outputName === inputs.output
|
|
625
|
-
})
|
|
626
|
-
if (depOutput) {
|
|
627
|
-
lines.push(` 输出文件:${depOutput.path}`)
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
} else {
|
|
631
|
-
lines.push(`- ${depRole.name}(${depId})`)
|
|
632
|
-
for (const o of (depRole.outputs || [])) {
|
|
633
|
-
lines.push(` 输出:${o.path}`)
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
// 输入提示
|
|
640
|
-
const inputs = role.inputs || {}
|
|
641
|
-
const inputPaths = inputs.paths || []
|
|
642
|
-
const inputHints = inputs.hints || {}
|
|
643
|
-
if (inputPaths.length > 0) {
|
|
644
|
-
lines.push('')
|
|
645
|
-
lines.push('搜索范围:')
|
|
646
|
-
for (const p of inputPaths) {
|
|
647
|
-
lines.push(`- ${p}`)
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
if (inputHints.grep_patterns && inputHints.grep_patterns.length > 0) {
|
|
651
|
-
lines.push('')
|
|
652
|
-
lines.push('搜索关键词:')
|
|
653
|
-
lines.push(`- ${inputHints.grep_patterns.join(', ')}`)
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
// 额外上下文
|
|
657
|
-
if (context.envSummary) {
|
|
658
|
-
lines.push('')
|
|
659
|
-
lines.push('环境探测结果:')
|
|
660
|
-
lines.push(context.envSummary)
|
|
661
|
-
}
|
|
662
|
-
if (context.missingDocs) {
|
|
663
|
-
lines.push('')
|
|
664
|
-
lines.push('缺失文档列表:')
|
|
665
|
-
lines.push(context.missingDocs)
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
// 输出目标
|
|
669
|
-
const outputs = role.outputs || []
|
|
670
|
-
lines.push('')
|
|
671
|
-
lines.push('目标文件:')
|
|
672
|
-
for (const o of outputs) {
|
|
673
|
-
lines.push(`- \`${o.path}\``)
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
// 约束
|
|
677
|
-
if (role.constraints && role.constraints.length > 0) {
|
|
678
|
-
lines.push('')
|
|
679
|
-
lines.push('约束:')
|
|
680
|
-
for (const c of role.constraints) {
|
|
681
|
-
lines.push(`- ${c}`)
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
// 检查要求(告诉子代理需要满足什么)
|
|
686
|
-
for (const o of outputs) {
|
|
687
|
-
const checks = o.checks || []
|
|
688
|
-
for (const check of checks) {
|
|
689
|
-
if (check.type === 'contains_sections' && check.sections) {
|
|
690
|
-
lines.push('')
|
|
691
|
-
lines.push(`必须包含章节:${check.sections.map(s => `"## ${s}"`).join(', ')}`)
|
|
692
|
-
}
|
|
693
|
-
if (check.type === 'min_lines') {
|
|
694
|
-
lines.push(`文件长度要求:至少 ${check.min} 行`)
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
lines.push('')
|
|
700
|
-
lines.push('⚠️ 必须用 write 工具将文件写入磁盘!写完后用 read 工具确认文件存在!')
|
|
701
|
-
|
|
702
|
-
return lines.join('\n')
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
/**
|
|
706
|
-
* 为 workflow 的所有角色生成 role prompts
|
|
707
|
-
* @param {object} wf - workflow 定义
|
|
708
|
-
* @param {string} projectName - 项目名
|
|
709
|
-
* @param {object} context - 额外上下文
|
|
710
|
-
* @returns {Array<{ roleId: string, roleName: string, prompt: string }>}
|
|
711
|
-
*/
|
|
712
|
-
export function generateAllRolePrompts(wf, projectName, context = {}) {
|
|
713
|
-
const resolved = replaceProjectPlaceholder(wf, projectName)
|
|
714
|
-
const roles = resolved.roles || []
|
|
715
|
-
return roles.map(role => ({
|
|
716
|
-
roleId: role.id,
|
|
717
|
-
roleName: role.name || role.id,
|
|
718
|
-
prompt: generateRolePrompt(resolved, role.id, projectName, context)
|
|
719
|
-
}))
|
|
720
|
-
}
|
|
721
|
-
|
|
722
545
|
// ─── Workflow Run 归档 ───
|
|
723
546
|
|
|
724
547
|
|
package/src/worktree-apply.js
CHANGED
|
@@ -697,6 +697,124 @@ function rollbackApply(projectRoot, trackedFiles, newFiles) {
|
|
|
697
697
|
return { conflicts, error };
|
|
698
698
|
}
|
|
699
699
|
|
|
700
|
+
/**
|
|
701
|
+
* merge 前预对齐 baseline 并行文件到 main 版(D-002@v1 / task-02)。
|
|
702
|
+
*
|
|
703
|
+
* 背景:baseline checkpoint 把 execute 启动时主仓 dirty 的并行会话文件快照进了 worktree 分支
|
|
704
|
+
* (worktree.js _createBaselineCheckpoint)。apply --merge 时主仓常已把这批文件推进(并行会话
|
|
705
|
+
* 落了 commit),分支侧旧快照与 main 新版在 merge-base 两侧同改同一文件 → merge 冲突。预对齐在
|
|
706
|
+
* merge 前把这批文件 checkout 成 main 版并提交到分支,使三方合并对该文件 ours == theirs,
|
|
707
|
+
* 消除冲突主因。
|
|
708
|
+
*
|
|
709
|
+
* 过滤集(四条件全满足才对齐;任一不满足跳过该文件):
|
|
710
|
+
* a. baseline checkpoint 文件集(已提交口径):git diff --name-only <baseHash>..<baselineCommit>
|
|
711
|
+
* ——不用 result.changedFiles(工作区口径且已过滤基础设施文件)
|
|
712
|
+
* b. ∩ main 已推进集:git diff --name-only <merge-base>..<main HEAD>
|
|
713
|
+
* (merge-base = 分支与 main HEAD 的最近公共祖先)
|
|
714
|
+
* c. ∖ 分支已变更集:git diff --name-only <baselineCommit>..<branch> 含该文件(本变更交付,不动)
|
|
715
|
+
* d. ∖ worktree 工作区 dirty 集:git status --porcelain -- <file> 非空(未提交改动会被 checkout 覆盖)
|
|
716
|
+
* 另排除「main HEAD 与分支 tip 内容已一致」的文件(对齐无意义,且防 nothing-to-commit 空提交)。
|
|
717
|
+
*
|
|
718
|
+
* 执行 cwd:checkout/commit 在 worktreePath(改的是 worktree 分支的 index+工作区);mainHead /
|
|
719
|
+
* merge-base 锚点在 projectRoot 读取。merge 本身仍在 projectRoot(applyByMerge 原逻辑不动)。
|
|
720
|
+
*
|
|
721
|
+
* 降级(不阻断):不适用场景(无 baseline checkpoint / 非 worktree 模式(in-place 的 worktreePath
|
|
722
|
+
* 就是主工作区,在其上 commit 等于绕过验收污染 main;native-worktree 是外部隔离环境)/ worktree
|
|
723
|
+
* 目录丢失 / worktree HEAD ≠ 分支 tip(checkout+commit 会落错分支,merge 的却是 meta.branch))
|
|
724
|
+
* 静默跳过;任一 git 步骤失败 → best-effort 回滚已 checkout 文件 + console.warn + result.warnings
|
|
725
|
+
* → 调用方走原 merge 路径(降级不阻断,D-002@v1 约束③)。
|
|
726
|
+
*
|
|
727
|
+
* @param {object} p
|
|
728
|
+
* @param {object} p.meta - worktree meta(baseHash / baselineCommit / worktreePath / mode)
|
|
729
|
+
* @param {string} p.branch - 将被 merge 的分支名(meta.branch)
|
|
730
|
+
* @param {string} p.projectRoot - 主仓库根(merge 的执行侧)
|
|
731
|
+
* @param {object} p.result - applyWorktree 的 result(预对齐信息写入 result.warnings,可追溯,约束④)
|
|
732
|
+
* @returns {{ aligned: string[], commit: string|null }} aligned=本次对齐的文件(空数组=未对齐)
|
|
733
|
+
*/
|
|
734
|
+
function preAlignBaselineToMain({ meta, branch, projectRoot, result }) {
|
|
735
|
+
const pushWarning = (msg) => {
|
|
736
|
+
result.warnings = result.warnings || [];
|
|
737
|
+
result.warnings.push(msg);
|
|
738
|
+
};
|
|
739
|
+
|
|
740
|
+
// 不适用场景(非失败,静默跳过)
|
|
741
|
+
if (!meta || !meta.baseHash || !meta.baselineCommit) return { aligned: [], commit: null };
|
|
742
|
+
if (meta.mode && meta.mode !== 'worktree') return { aligned: [], commit: null };
|
|
743
|
+
const worktreePath = meta.worktreePath;
|
|
744
|
+
if (!worktreePath || !existsSync(worktreePath)) return { aligned: [], commit: null };
|
|
745
|
+
const wtHead = gitQuiet(worktreePath, ['rev-parse', 'HEAD']);
|
|
746
|
+
const branchTip = gitQuiet(projectRoot, ['rev-parse', branch]);
|
|
747
|
+
if (!wtHead || !branchTip || wtHead !== branchTip) return { aligned: [], commit: null };
|
|
748
|
+
|
|
749
|
+
const { baseHash, baselineCommit } = meta;
|
|
750
|
+
const done = []; // 已 checkout 的文件(失败回滚用)
|
|
751
|
+
try {
|
|
752
|
+
// (a) baseline checkpoint 文件集(已提交口径;--no-renames:rename 退化为 D+A,两侧路径都进集)
|
|
753
|
+
const baselineSet = new Set(
|
|
754
|
+
(git(worktreePath, ['diff', '--no-renames', '--name-only', baseHash, baselineCommit]) || '')
|
|
755
|
+
.split('\n').filter(Boolean)
|
|
756
|
+
);
|
|
757
|
+
if (baselineSet.size === 0) return { aligned: [], commit: null };
|
|
758
|
+
|
|
759
|
+
// (b) main 已推进集(merge-base 后 main HEAD 的树差异;锚点拿不到 → 交原 merge,不算失败)
|
|
760
|
+
const mainHead = git(projectRoot, ['rev-parse', 'HEAD']);
|
|
761
|
+
const mergeBase = git(projectRoot, ['merge-base', branch, 'HEAD']);
|
|
762
|
+
if (!mainHead || !mergeBase) return { aligned: [], commit: null };
|
|
763
|
+
const mainAdvanced = new Set(
|
|
764
|
+
(git(projectRoot, ['diff', '--no-renames', '--name-only', mergeBase, mainHead]) || '')
|
|
765
|
+
.split('\n').filter(Boolean)
|
|
766
|
+
);
|
|
767
|
+
|
|
768
|
+
// (c) 分支已变更集(baselineCommit → 分支 tip 的已提交交付,不动)
|
|
769
|
+
const branchChanged = new Set(
|
|
770
|
+
(git(worktreePath, ['diff', '--no-renames', '--name-only', baselineCommit, branchTip]) || '')
|
|
771
|
+
.split('\n').filter(Boolean)
|
|
772
|
+
);
|
|
773
|
+
|
|
774
|
+
// (d) worktree 工作区 dirty + 两侧 HEAD 同内容排除 → 最终对齐集
|
|
775
|
+
const candidates = [];
|
|
776
|
+
for (const f of baselineSet) {
|
|
777
|
+
if (!mainAdvanced.has(f) || branchChanged.has(f)) continue;
|
|
778
|
+
// dirty(checkout 会覆盖 worktree 未提交改动)→ 跳过该文件
|
|
779
|
+
if ((gitQuiet(worktreePath, ['status', '--porcelain', '--', f]) || '') !== '') continue;
|
|
780
|
+
// main HEAD 与分支 tip 内容已一致 → 对齐无意义 + 防空提交
|
|
781
|
+
if ((gitQuiet(worktreePath, ['diff', '--name-only', mainHead, branchTip, '--', f]) || '') === '') continue;
|
|
782
|
+
candidates.push(f);
|
|
783
|
+
}
|
|
784
|
+
if (candidates.length === 0) return { aligned: [], commit: null };
|
|
785
|
+
|
|
786
|
+
// 对齐:逐文件 checkout main 版(worktree 内执行,同时更新 worktree 分支的 index+工作区)
|
|
787
|
+
for (const f of candidates) {
|
|
788
|
+
git(worktreePath, ['checkout', mainHead, '--', f]);
|
|
789
|
+
done.push(f);
|
|
790
|
+
}
|
|
791
|
+
// 显式 pathspec 提交:只提交对齐文件,不扫入 worktree 内其他 staged 改动(坑 git-commit-sweeps-prestaged)。
|
|
792
|
+
// --no-verify:对齐 commit 与 baseline checkpoint 同性质(锚点非交付物,worktree.js _createBaselineCheckpoint
|
|
793
|
+
// 同款处理)——项目 pre-commit hook(如 husky lint+全量测试)会把预对齐拖成分钟级甚至失败降级。
|
|
794
|
+
git(worktreePath, [
|
|
795
|
+
'commit', '--no-verify', '-m',
|
|
796
|
+
`sillyspec: align baseline files to main (pre-merge, ${done.length} files)`,
|
|
797
|
+
'--', ...done,
|
|
798
|
+
]);
|
|
799
|
+
const shortHash = gitQuiet(worktreePath, ['rev-parse', '--short', 'HEAD']);
|
|
800
|
+
pushWarning(
|
|
801
|
+
`merge 前预对齐 ${done.length} 个 baseline 并行文件到 main 版` +
|
|
802
|
+
`${shortHash ? `(align commit ${shortHash})` : ''}:${done.join(', ')}`
|
|
803
|
+
);
|
|
804
|
+
return { aligned: done, commit: shortHash };
|
|
805
|
+
} catch (e) {
|
|
806
|
+
// 降级:任一步失败 → best-effort 回滚已 checkout 文件(checkout <branchTip> -- f 还原 index+工作区)
|
|
807
|
+
for (const f of done) {
|
|
808
|
+
gitQuiet(worktreePath, ['checkout', branchTip, '--', f]);
|
|
809
|
+
}
|
|
810
|
+
const msg = `baseline 预对齐失败,已跳过(降级走原 merge 路径):${(e.message || '').split('\n')[0]}` +
|
|
811
|
+
(done.length > 0 ? `;已回滚 ${done.length} 个已 checkout 文件` : '');
|
|
812
|
+
console.warn(`⚠️ ${msg}`);
|
|
813
|
+
pushWarning(msg);
|
|
814
|
+
return { aligned: [], commit: null };
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
700
818
|
/**
|
|
701
819
|
* baseline 漂移时的 merge 降级路径(D-001)。
|
|
702
820
|
*
|
|
@@ -705,6 +823,7 @@ function rollbackApply(projectRoot, trackedFiles, newFiles) {
|
|
|
705
823
|
* git merge 比 patch 鲁棒,能处理 baseline 漂移 + 潜在冲突。会引入合并提交
|
|
706
824
|
* (D-002:与 worktree.md:84「patch 而非 merge 保持线性历史」架构决策的张力——
|
|
707
825
|
* 仅作 --merge 显式 opt-in,不改变默认 patch 行为)。
|
|
826
|
+
* merge 前先经 preAlignBaselineToMain 预对齐 baseline 并行文件到 main 版(D-002@v1 / task-02,失败降级走原 merge)。
|
|
708
827
|
*
|
|
709
828
|
* merge 冲突时不自动解决:git merge --abort 回滚到合并前状态 + 报冲突文件列表。
|
|
710
829
|
*
|
|
@@ -720,6 +839,9 @@ function applyByMerge(result, changeName, projectRoot, wm) {
|
|
|
720
839
|
// 用 meta.branch(native-worktree 模式分支名可能不是 sillyspec/<change>),不硬编码。
|
|
721
840
|
const branch = meta.branch || `sillyspec/${changeName}`;
|
|
722
841
|
|
|
842
|
+
// --- merge 前预对齐(D-002@v1 / task-02):baseline 并行旧文件对齐 main 版,消除 merge 冲突主因 ---
|
|
843
|
+
preAlignBaselineToMain({ meta, branch, projectRoot, result });
|
|
844
|
+
|
|
723
845
|
try {
|
|
724
846
|
git(projectRoot, ['merge', '--no-ff', branch], { timeout: 30000 });
|
|
725
847
|
} catch (e) {
|
|
@@ -877,7 +999,8 @@ export function assessApplyRisk(changeName, { cwd } = {}) {
|
|
|
877
999
|
}
|
|
878
1000
|
}
|
|
879
1001
|
|
|
880
|
-
// 检查 6: diff
|
|
1002
|
+
// 检查 6: diff 规模异常(两档:>5000 行 BLOCKED;2000~5000 行 WARNING——正常规模 change
|
|
1003
|
+
// 常超 2000 行(如 +2368),单档硬拦把正常变更逼进 rescue cp 手动路径,见 ql-20260815-001 复盘)
|
|
881
1004
|
const wtPath = meta?.worktreePath;
|
|
882
1005
|
const diffBase = meta?.baselineCommit || meta?.baseHash;
|
|
883
1006
|
let additions = 0, deletions = 0;
|
|
@@ -888,8 +1011,10 @@ export function assessApplyRisk(changeName, { cwd } = {}) {
|
|
|
888
1011
|
const delMatch = shortstat?.match(/(\d+) deletion/);
|
|
889
1012
|
additions = insMatch ? parseInt(insMatch[1]) : 0;
|
|
890
1013
|
deletions = delMatch ? parseInt(delMatch[1]) : 0;
|
|
891
|
-
if (additions + deletions >
|
|
892
|
-
reasons.push(`diff 规模异常(${additions} additions + ${deletions} deletions = ${additions + deletions}
|
|
1014
|
+
if (additions + deletions > 5000) {
|
|
1015
|
+
reasons.push(`diff 规模异常(${additions} additions + ${deletions} deletions = ${additions + deletions} 行,超过 5000 硬上限)`);
|
|
1016
|
+
} else if (additions + deletions > 2000) {
|
|
1017
|
+
warnings.push(`diff 规模偏大(${additions} additions + ${deletions} deletions = ${additions + deletions} 行,2000~5000 区间放行为 WARNING,请确认属正常变更规模)`);
|
|
893
1018
|
}
|
|
894
1019
|
} catch {}
|
|
895
1020
|
}
|
package/src/worktree.js
CHANGED
|
@@ -58,7 +58,7 @@ export function detectIsolation(cwd = process.cwd()) {
|
|
|
58
58
|
* @param {string} cwd - 项目根目录
|
|
59
59
|
* @returns {{ ignored: boolean, path: string }}
|
|
60
60
|
*/
|
|
61
|
-
|
|
61
|
+
function checkWorktreeDirIgnored(cwd = process.cwd()) {
|
|
62
62
|
const relPath = WORKTREES_REL;
|
|
63
63
|
try {
|
|
64
64
|
git(cwd, ['check-ignore', '-q', relPath]);
|
|
@@ -195,7 +195,7 @@ function validateChangeName(changeName) {
|
|
|
195
195
|
* @param {string} cwd
|
|
196
196
|
* @returns {{ supported: boolean, version: string|null, reason?: string }}
|
|
197
197
|
*/
|
|
198
|
-
|
|
198
|
+
function isGitWorktreeSupported(cwd = process.cwd()) {
|
|
199
199
|
try {
|
|
200
200
|
const raw = git(cwd, ['--version']);
|
|
201
201
|
const match = raw.match(/git version (\d+)\.(\d+)/);
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
6. **判规模选档**:≤3 文件、范围明确走 `quick`;多阶段 / 架构级走完整流程。
|
|
13
13
|
7. **代码先行不补流程(倒推 B 模式)**:代码若已先写好,**不回头补 brainstorm/plan 装样子**——用 `quick --done` 收尾 + 补 quicklog 条目,把已落盘改动如实登记进进度库。
|
|
14
14
|
8. **实证核验再 `--done`**:`quick --done` 前,触及 `src`/`test` 的改动先 `npm test` + `npm run lint`;纯 doc/配置改动可跳过(lint 不扫 docs/,空转无信息),以落盘文件与测试结果为准,不信口头"已完成"。
|
|
15
|
-
9.
|
|
15
|
+
9. **中途停下不靠额外命令存进度**——进度已由上一次 `--done` 自动落盘;恢复时用 `sillyspec progress show` 查看进度,再用 `sillyspec run <stage>` 续跑,不直接 commit 半成品。
|
|
16
16
|
10. **实现完成后对照文档验收**(design.md / 模块文档),并检查是否影响已有测试。
|
|
17
17
|
11. **非测试逻辑本身有误时,禁止改测试来"通过"**——修逻辑,不修测试。
|
|
18
18
|
12. **hook 拦截提交时禁止跳过**(`.husky/pre-push`),修复问题后再提交。
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
TaskCard 格式规则(必须严格遵守):
|
|
2
2
|
- 总长度 20~40 行,不要写成长文档
|
|
3
3
|
- frontmatter 只含必要字段,不加 estimated_hours
|
|
4
|
+
- title 用英文短语(kebab 风格动词短语,如 "Add readonly auxiliary constant"),title_zh 用对应中文标题——两字段是同一任务的双语表示,非不同信息;归档产物与 sillyhub 平台解析各取所需(title_zh 为中文展示首选)
|
|
4
5
|
- goal: 一句话,用 > 多行字符串
|
|
5
6
|
- implementation: 列表,每条一个具体步骤
|
|
6
7
|
- acceptance: 列表,每条可独立验证(不是表格)
|
|
@@ -46,11 +46,11 @@ grep -rl "<关键词>" <源码目录>/ --include="*.java" --include="*.js" --inc
|
|
|
46
46
|
3. Diff 对账:
|
|
47
47
|
- 前端调用路径在 backend 端点清单中找不到 → **❌ Missing backend endpoint**(advisory:CLI 复核后仅 warn,不硬阻断归档;但 contract gap 是真实集成缺陷,应诚实标 FAIL 并回 execute 补端点)
|
|
48
48
|
- backend 端点在前端无调用 → ⚠️ Unused backend endpoint(warning,不阻断)
|
|
49
|
-
4.
|
|
49
|
+
4. 输出对账结果表格(示例用通用占位路径,实际填本仓真实文件):
|
|
50
50
|
|
|
51
51
|
| 状态 | 前端调用 | 后端端点 | 文件 |
|
|
52
52
|
|---|---|---|---|
|
|
53
|
-
| ❌ missing | GET /api
|
|
53
|
+
| ❌ missing | GET /api/<域>/<资源>/<param> | — | <前端调用所在文件,如 src/lib/<模块>.ts> |
|
|
54
54
|
|
|
55
55
|
如果发现 Missing backend endpoint,在验证报告中标记为 ❌ contract gap(CLI 仅 advisory 提示、不硬阻断归档;但缺口真实存在——诚实判 FAIL 并回 execute 补齐端点,勿因 CLI 不拦而放行)。
|
|
56
56
|
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: sillyspec-onboard
|
|
3
|
-
description: 为未安装 SillySpec 的全新项目接入规范驱动开发工作流——引导安装 CLI、初始化接入、建立项目库,并把「用 SillySpec 走流程」确立为项目默认工作方式。适合用户说"给这个项目接上 sillyspec"或新项目开工前确认流程。
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
## 交互规范
|
|
7
|
-
|
|
8
|
-
**当需要用户从多个选项中做出选择时,必须使用 Claude Code 内置的 AskUserQuestion 工具,将选项以参数传入。** 涉及是否全局安装 CLI / 是否建立项目库等决策,让用户亲手拍板,不替用户做决定。
|
|
9
|
-
|
|
10
|
-
## 何时使用
|
|
11
|
-
|
|
12
|
-
- 目标项目**没有安装/未接入** SillySpec(判断:项目根无 `.sillyspec/` 目录;或 `sillyspec --version` 命令不可用)
|
|
13
|
-
- 用户想让 SillySpec 成为该项目的开发流程控制器
|
|
14
|
-
|
|
15
|
-
## 铁律
|
|
16
|
-
|
|
17
|
-
- **必须用 exec 工具(shell)执行 CLI,不编造命令**;命中的命令固定为下方公开 CLI,不自行发明子命令
|
|
18
|
-
- 本 skill 只做**接入动作**(安装/初始化/验证),不写业务代码、不改项目源码
|
|
19
|
-
- 全程在主工作区根执行 CLI,不进入 worktree
|
|
20
|
-
- 每个决策点用 AskUserQuestion 让用户拍板,不静默代选
|
|
21
|
-
|
|
22
|
-
## 核心流程
|
|
23
|
-
|
|
24
|
-
### Step 0: 判定接入状态
|
|
25
|
-
|
|
26
|
-
```bash
|
|
27
|
-
# 项目根是否已有 .sillyspec/(已接入)
|
|
28
|
-
ls -d .sillyspec 2>/dev/null
|
|
29
|
-
# CLI 是否可用
|
|
30
|
-
sillyspec --version 2>/dev/null
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
- 已接入(`.sillyspec/` 存在)→ 不重复接入,提示用户项目已用 SillySpec 管理,退出。
|
|
34
|
-
- 未接入但 CLI 已装 → 跳到 Step 2。
|
|
35
|
-
- 都无 → 继续 Step 1。
|
|
36
|
-
|
|
37
|
-
### Step 1: 引导安装 CLI(全局)
|
|
38
|
-
|
|
39
|
-
```bash
|
|
40
|
-
npm install -g sillyspec
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
- 若当前环境沙箱/权限导致 `npm -g` 失败,提示用户用 `! npm install -g sillyspec` 手动执行(输出直达会话)。
|
|
44
|
-
- 装完验证:`sillyspec --version` 可输出版本号。
|
|
45
|
-
|
|
46
|
-
### Step 2: 初始化接入项目
|
|
47
|
-
|
|
48
|
-
```bash
|
|
49
|
-
sillyspec init
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
`init` 会自动:创建 `.sillyspec/`(项目注册、文档中心、知识库、运行时进度库)、检测项目中的 AI 工具(Claude Code 等)、给 `CLAUDE.md` 注入 SillySpec 受管段(若项目已有文件)或生成完整指引模板(若没有)、安装对应命令模板/MCP 工具。
|
|
53
|
-
|
|
54
|
-
- 新项目为空目录时建议走 `sillyspec run scan`(见 Step 3)补齐项目库;已有代码但未接入的存量项目,先确认项目状态再决定是否 scan。
|
|
55
|
-
- 验证产物:
|
|
56
|
-
```bash
|
|
57
|
-
ls .sillyspec/projects/ # 项目注册表
|
|
58
|
-
grep -c "SillySpec" CLAUDE.md # 受管段/指引已注入(>0)
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
### Step 3: 建立项目库(知识基石)
|
|
62
|
-
|
|
63
|
-
```bash
|
|
64
|
-
sillyspec run scan
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
把项目的代码风格(CONVENTIONS.md)、架构(ARCHITECTURE.md)沉淀为后续 agent 的检索知识。这是「让后续 agent 不瞎猜」的关键一步,新项目建议立即做。
|
|
68
|
-
|
|
69
|
-
### Step 4: 价值认知锚 —— 让后续 agent 愿意用
|
|
70
|
-
|
|
71
|
-
接入完成后,**让"用 SillySpec 走流程"成为项目默认工作方式**靠的是已落盘的载体,不是本 skill(skill 是一次性的):
|
|
72
|
-
|
|
73
|
-
1. 确认价值锚已就位:
|
|
74
|
-
- `CLAUDE.md` 含 SillySpec 段(Step 2 已验证)——agent 每次会话读取项目说明书时都会看到「禁止绕过流程 / 新功能走 brainstorm→plan→execute→verify→archive / 小修复走 quick」。
|
|
75
|
-
- `.claude/skills/` 含 SillySpec 持续 skill(init 已复制 plan/quick/brainstorm 等)——agent 在开发任务中 `/plan`、`/quick` 即走流程。
|
|
76
|
-
- `.sillyspec/` 进度库已建——进度自动落盘,多 agent 并发不互相覆盖。
|
|
77
|
-
|
|
78
|
-
2. 向用户/后续 agent 说明价值(一句话口径,复用 init 注入的陈述,不另造文案):
|
|
79
|
-
> SillySpec 是给 Agent 调用的 CLI 流程控制器:你通过 CLI 告诉它"我在哪",它告诉你"下一步做什么";你执行步骤,它校验产出、推进状态,人类只在关键决策点介入审批。多 agent 同时操作代码时进度统一落盘、不丢不回滚。
|
|
80
|
-
|
|
81
|
-
3. 给后续 agent 一个「开工首查」示例:
|
|
82
|
-
```bash
|
|
83
|
-
sillyspec progress show # 看项目当前阶段/进行中变更
|
|
84
|
-
sillyspec run <stage> # 续跑当前阶段(brainstorm/plan/execute/verify/archive)
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
## 分发与安装(本 skill 是引导型)
|
|
88
|
-
|
|
89
|
-
本 skill 是「接入引导」,只在新项目接入时用一次,**不应随项目复制进 `.claude/skills/` 长期留存**。使用方式二选一:
|
|
90
|
-
|
|
91
|
-
- **项目级一次性**:复制本 `SKILL.md` 到目标项目 `.claude/skills/sillyspec-onboard/SKILL.md`,接入完成后删除。
|
|
92
|
-
- **全局可用**:复制到 `~/.claude/skills/sillyspec-onboard/SKILL.md`(或 `~/.config/claude/skills/`,随 Claude Code 版本),任何新项目都能呼起 `/sillyspec-onboard`。
|