sillyspec 3.24.0 → 3.24.2
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 +39 -8
- package/src/stage-contract.js +14 -0
- package/src/stage-review.js +1 -1
- package/src/stages/brainstorm.js +23 -21
- package/src/stages/plan.js +14 -13
- package/src/task-review.js +15 -2
- package/src/worktree-apply.js +33 -13
package/package.json
CHANGED
package/src/run.js
CHANGED
|
@@ -607,7 +607,7 @@ async function getStageSteps(stageName, cwd, progress, specDir = null) {
|
|
|
607
607
|
/**
|
|
608
608
|
* 确保阶段的 steps 已初始化到 progress
|
|
609
609
|
*/
|
|
610
|
-
async function ensureStageSteps(progress, stageName, cwd, specDir = null) {
|
|
610
|
+
export async function ensureStageSteps(progress, stageName, cwd, specDir = null) {
|
|
611
611
|
if (!progress.stages) progress.stages = {}
|
|
612
612
|
|
|
613
613
|
const steps = await getStageSteps(stageName, cwd, progress, specDir)
|
|
@@ -628,7 +628,15 @@ async function ensureStageSteps(progress, stageName, cwd, specDir = null) {
|
|
|
628
628
|
const oldSteps = progress.stages[stageName].steps
|
|
629
629
|
progress.stages[stageName].steps = steps.map(s => {
|
|
630
630
|
const old = oldSteps.find(step => step.name === s.name)
|
|
631
|
-
if (old) return old
|
|
631
|
+
if (old) return old // 精确名命中 → 保留旧状态
|
|
632
|
+
// migratedFrom:新步骤是合并/重命名来的(如 brainstorm 13→8 折叠)。
|
|
633
|
+
// 吸收的旧步骤全部 completed → 新步骤标 completed,避免 completed 丢失导致 currentIdx 回跳。
|
|
634
|
+
if (Array.isArray(s.migratedFrom) && s.migratedFrom.length > 0) {
|
|
635
|
+
const migrated = oldSteps.filter(step => s.migratedFrom.includes(step.name))
|
|
636
|
+
if (migrated.length > 0 && migrated.every(step => step.status === 'completed')) {
|
|
637
|
+
return { name: s.name, status: 'completed', migratedFrom: true }
|
|
638
|
+
}
|
|
639
|
+
}
|
|
632
640
|
return { name: s.name, status: 'pending' }
|
|
633
641
|
})
|
|
634
642
|
return true
|
|
@@ -916,7 +924,8 @@ async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjec
|
|
|
916
924
|
if (stageName === 'execute' && promptText.includes('{EXECUTE_RUN_ID}')) {
|
|
917
925
|
let runId = ''
|
|
918
926
|
const execSpecBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
|
|
919
|
-
const
|
|
927
|
+
const runtimeRoot = platformOpts?.runtimeRoot || join(execSpecBase, '.runtime')
|
|
928
|
+
const runIdFile = join(runtimeRoot, `current-execute-run-id-${changeName}`)
|
|
920
929
|
try {
|
|
921
930
|
if (existsSync(runIdFile)) {
|
|
922
931
|
runId = readFileSync(runIdFile, 'utf8').trim()
|
|
@@ -925,6 +934,8 @@ async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjec
|
|
|
925
934
|
if (!runId) {
|
|
926
935
|
const { generateExecuteRunId } = await import('./task-review.js')
|
|
927
936
|
runId = generateExecuteRunId()
|
|
937
|
+
// 落盘(与启动站点一致),保证 agent 收到的 ID == gate/checkbox 读取的 ID
|
|
938
|
+
try { mkdirSync(runtimeRoot, { recursive: true }); writeFileSync(runIdFile, runId + '\n') } catch {}
|
|
928
939
|
}
|
|
929
940
|
promptText = promptText.replace(/\{EXECUTE_RUN_ID\}/g, runId)
|
|
930
941
|
}
|
|
@@ -1609,13 +1620,31 @@ export async function runCommand(args, cwd, specDir = null) {
|
|
|
1609
1620
|
if (autoChange) {
|
|
1610
1621
|
progress = await pm.initChange(cwd, autoChange)
|
|
1611
1622
|
} else if (isAuxiliary) {
|
|
1612
|
-
|
|
1613
|
-
|
|
1623
|
+
let autoName = changeName || resolveChangeNameAuto(cwd, specRoot) || 'default'
|
|
1624
|
+
// archive 特例:归档后变更从活跃列表排除(listChanges WHERE status='active'),
|
|
1625
|
+
// 不带 --change 回退 default 会读错变更。无活跃变更时取最新归档变更,读其现有 progress。
|
|
1626
|
+
if (stageName === 'archive' && autoName === 'default') {
|
|
1627
|
+
try {
|
|
1628
|
+
const archiveDir = join(specBase, 'changes', 'archive')
|
|
1629
|
+
if (existsSync(archiveDir)) {
|
|
1630
|
+
const latest = readdirSync(archiveDir)
|
|
1631
|
+
.filter(d => /^\d{4}-\d{2}-\d{2}-.+/.test(d))
|
|
1632
|
+
.sort()
|
|
1633
|
+
.pop()
|
|
1634
|
+
if (latest) {
|
|
1635
|
+
autoName = latest.replace(/^\d{4}-\d{2}-\d{2}-/, '')
|
|
1636
|
+
progress = await pm.read(cwd, autoName)
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
} catch {}
|
|
1640
|
+
}
|
|
1614
1641
|
changeName = autoName
|
|
1615
|
-
progress = await pm.initChange(cwd, autoName)
|
|
1616
|
-
// initChange 可能因 project 表为空返回 null
|
|
1617
1642
|
if (!progress) {
|
|
1618
|
-
progress =
|
|
1643
|
+
progress = await pm.initChange(cwd, autoName)
|
|
1644
|
+
// initChange 可能因 project 表为空返回 null
|
|
1645
|
+
if (!progress) {
|
|
1646
|
+
progress = { currentStage: stageName, stages: {}, lastActive: new Date().toLocaleString('zh-CN', { hour12: false }), project: '' }
|
|
1647
|
+
}
|
|
1619
1648
|
}
|
|
1620
1649
|
} else {
|
|
1621
1650
|
// brainstorm 作为流程入口,自动生成变更名并初始化
|
|
@@ -3547,6 +3576,8 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
3547
3576
|
if (!executeRunId) {
|
|
3548
3577
|
const { generateExecuteRunId } = await import('./task-review.js')
|
|
3549
3578
|
executeRunId = generateExecuteRunId()
|
|
3579
|
+
// 落盘(marker 缺失时 fallback 生成后写盘,保证后续 checkbox/gate 读到同一 ID)
|
|
3580
|
+
try { mkdirSync(runtimeRoot, { recursive: true }); writeFileSync(runIdFile, executeRunId + '\n') } catch {}
|
|
3550
3581
|
}
|
|
3551
3582
|
|
|
3552
3583
|
// git 真实性校验目录:worktree 存在则用 worktree(base/head commit 在其中),否则主仓库
|
package/src/stage-contract.js
CHANGED
|
@@ -56,6 +56,20 @@ export function validateChangeExists(specBase, stageName, changeName) {
|
|
|
56
56
|
if (!changeName || !STAGES_REQUIRE_EXISTING_CHANGE.has(stageName)) return null
|
|
57
57
|
if (/^quick-[0-9a-f]{8}$/.test(changeName)) return null
|
|
58
58
|
if (existsSync(join(specBase, 'changes', changeName))) return null
|
|
59
|
+
|
|
60
|
+
// archive 特例:step4 --confirm 已把变更从 changes/<name>/ 移到 changes/archive/<date>-<name>/。
|
|
61
|
+
// step5(更新路线图/提交)--change <name> 不应因 changes/<name>/ 已移走而被前置校验误拦。
|
|
62
|
+
// 精确匹配归档目录名(YYYY-MM-DD-<changeName>),避免后缀子串误匹配(auth ≠ deep-auth)。
|
|
63
|
+
if (stageName === 'archive') {
|
|
64
|
+
const archiveRoot = join(specBase, 'changes', 'archive')
|
|
65
|
+
if (existsSync(archiveRoot)) {
|
|
66
|
+
const archived = readdirSync(archiveRoot).find(d => {
|
|
67
|
+
const m = d.match(/^\d{4}-\d{2}-\d{2}-(.+)$/)
|
|
68
|
+
return m && m[1] === changeName
|
|
69
|
+
})
|
|
70
|
+
if (archived) return null
|
|
71
|
+
}
|
|
72
|
+
}
|
|
59
73
|
return {
|
|
60
74
|
changeName,
|
|
61
75
|
specBase,
|
package/src/stage-review.js
CHANGED
|
@@ -134,7 +134,7 @@ export function verifyStageReviewDocHash(review, searchDirs) {
|
|
|
134
134
|
const actual = computeDocHash(abs)
|
|
135
135
|
if (actual === null) continue
|
|
136
136
|
// 找到主文档,比对 hash
|
|
137
|
-
if (review.docHash !== actual) {
|
|
137
|
+
if (review.docHash.toLowerCase() !== actual.toLowerCase()) {
|
|
138
138
|
errors.push(`docHash 与主审查文档 ${primaryRel} 的实际内容不匹配 — review.json 疑似伪造(未真正读取文档)`)
|
|
139
139
|
return { ok: false, errors, warnings }
|
|
140
140
|
}
|
package/src/stages/brainstorm.js
CHANGED
|
@@ -30,14 +30,14 @@ export const definition = {
|
|
|
30
30
|
|
|
31
31
|
### 操作
|
|
32
32
|
1. 读取 CODEBASE-OVERVIEW.md + 共享规范 + 子项目上下文
|
|
33
|
-
2. 加载项目信息:\`cat
|
|
34
|
-
3. 加载本地配置:\`cat
|
|
35
|
-
4. 棕地项目:读取
|
|
36
|
-
5. **加载模块索引**:读取
|
|
33
|
+
2. 加载项目信息:\`cat {SPEC_ROOT}/projects/*.yaml 2>/dev/null\`
|
|
34
|
+
3. 加载本地配置:\`cat {SPEC_ROOT}/local.yaml 2>/dev/null\`
|
|
35
|
+
4. 棕地项目:读取 {SPEC_ROOT}/docs/<project>/scan/ 下的 STRUCTURE.md、CONVENTIONS.md、ARCHITECTURE.md
|
|
36
|
+
5. **加载模块索引**:读取 \`{SPEC_ROOT}/docs/<project>/modules/_module-map.yaml\`(如存在)
|
|
37
37
|
- 这一步是高频操作,_module-map.yaml 回答"哪个文件属于哪个模块、模块之间怎么依赖"
|
|
38
38
|
- 用 tags/aliases 字段做需求关键词→模块的粗匹配
|
|
39
39
|
- 用 entrypoints 字段快速了解模块对外能力
|
|
40
|
-
6. 查看进行中的变更:\`ls
|
|
40
|
+
6. 查看进行中的变更:\`ls {SPEC_ROOT}/changes/ | grep -v archive\`
|
|
41
41
|
- 有相关同名变更 → 提示用户,避免重复
|
|
42
42
|
7. 检查全局模板:\`ls ~/.sillyspec/templates/\`
|
|
43
43
|
- 有匹配模板 → 询问是否基于模板
|
|
@@ -71,6 +71,7 @@ export const definition = {
|
|
|
71
71
|
},
|
|
72
72
|
{
|
|
73
73
|
name: '对话式探索与需求澄清',
|
|
74
|
+
migratedFrom: ['协作与复用检查', '原型/设计图分析', '需求范围评估', '对话式探索', '需求澄清 Grill'],
|
|
74
75
|
conditionalWait: true,
|
|
75
76
|
repeatableWait: true,
|
|
76
77
|
maxWaitRounds: 8,
|
|
@@ -132,9 +133,9 @@ export const definition = {
|
|
|
132
133
|
- 代码冲突:用户描述与现有代码/scan/module 文档不一致
|
|
133
134
|
|
|
134
135
|
**能通过代码或文档确认的不要问用户,先读取**:
|
|
135
|
-
-
|
|
136
|
-
-
|
|
137
|
-
-
|
|
136
|
+
- \`{SPEC_ROOT}/docs/<project>/scan/ARCHITECTURE.md\`
|
|
137
|
+
- \`{SPEC_ROOT}/docs/<project>/scan/CONVENTIONS.md\`
|
|
138
|
+
- \`{SPEC_ROOT}/docs/<project>/modules/_module-map.yaml\`
|
|
138
139
|
- 相关源码文件
|
|
139
140
|
|
|
140
141
|
**给每个未解决歧义分级**:
|
|
@@ -211,6 +212,7 @@ export const definition = {
|
|
|
211
212
|
},
|
|
212
213
|
{
|
|
213
214
|
name: '分段展示设计',
|
|
215
|
+
migratedFrom: ['HTML 原型生成'],
|
|
214
216
|
requiresWait: true,
|
|
215
217
|
waitReason: '等待用户确认设计方案',
|
|
216
218
|
waitOptions: ['确认', '需要修改', '推翻重来'],
|
|
@@ -228,7 +230,7 @@ export const definition = {
|
|
|
228
230
|
- 适合:有 UI 组件/布局/交互流程/状态转换/架构图
|
|
229
231
|
- 不适合:纯后端逻辑/配置修改/无可视化意义
|
|
230
232
|
如果适合,生成一个独立的 HTML 文件(内联 CSS + JS),保存到:
|
|
231
|
-
|
|
233
|
+
\`{SPEC_ROOT}/changes/<change-name>/prototype-<名称>.html\`
|
|
232
234
|
- 单文件,浏览器直接打开
|
|
233
235
|
- 展示关键布局结构和交互流程
|
|
234
236
|
- 不需要完整功能,重点是让用户确认设计方向
|
|
@@ -309,16 +311,16 @@ design.md 第一行标题必须用中文:# 设计文档(Design)— <变更
|
|
|
309
311
|
12. **自审**(AI 对自身设计的校验)
|
|
310
312
|
|
|
311
313
|
### 操作
|
|
312
|
-
1. 确认变更目录存在:\`mkdir -p
|
|
314
|
+
1. 确认变更目录存在:\`mkdir -p {SPEC_ROOT}/changes/<change-name>\`(Windows 用 \`mkdir {SPEC_ROOT}/changes\\<变更名>\` 或 PowerShell \`New-Item -ItemType Directory -Force -Path {SPEC_ROOT}/changes/<change-name>\`)
|
|
313
315
|
- 变更名格式必须为 \`YYYY-MM-DD-<简短描述>\`(如 \`2026-05-13-user-auth\`)
|
|
314
|
-
2. 将确认的设计写入
|
|
315
|
-
3. 如果对话探索或方案讨论产生了实现相关决策,写入
|
|
316
|
+
2. 将确认的设计写入 \`{SPEC_ROOT}/changes/<change-name>/design.md\`
|
|
317
|
+
3. 如果对话探索或方案讨论产生了实现相关决策,写入 \`{SPEC_ROOT}/changes/<change-name>/decisions.md\`:
|
|
316
318
|
- decisions.md 是本次变更的决策台账,不是长期术语表
|
|
317
319
|
- 只记录有实现/验收影响的决策,闲聊和低风险偏好不记录
|
|
318
320
|
- 每条记录必须有稳定版本 ID:D-001@v1、D-002@v1 ...
|
|
319
321
|
- 若后续 Design Grill 修正该决策,新记录使用 D-001@v2,并写明 supersedes: D-001@v1
|
|
320
322
|
- 每条记录必须包含:type、status、source、question、answer、normalized_requirement、impacts、evidence、priority
|
|
321
|
-
- 长期术语只在 archive/scan 时再提升到
|
|
323
|
+
- 长期术语只在 archive/scan 时再提升到 \`{SPEC_ROOT}/docs/<project>/glossary.md\`
|
|
322
324
|
4. 格式自检(只查章节齐全;语义一致性/可行性/YAGNI 不在本步查,交给下一步 Design Grill 独立审查):
|
|
323
325
|
- design.md 含全部必填章节(背景/设计目标/非目标/总体方案/文件变更清单/接口定义/风险登记)
|
|
324
326
|
- 如存在 decisions.md,design.md 是否引用所有当前版本 D-xxx@vN
|
|
@@ -358,12 +360,12 @@ tier: {REVIEW_TIER}({REVIEW_TIER_REASON})
|
|
|
358
360
|
3. 即使跳过,也要输出"Design Grill skipped"和原因,不能静默跳过。
|
|
359
361
|
|
|
360
362
|
### 输入材料
|
|
361
|
-
1. 必须读取完整
|
|
363
|
+
1. 必须读取完整 \`{SPEC_ROOT}/changes/<change-name>/design.md\`
|
|
362
364
|
2. 读取 proposal.md、requirements.md、tasks.md、decisions.md(如存在)
|
|
363
365
|
3. 读取 scan/module docs:
|
|
364
|
-
-
|
|
365
|
-
-
|
|
366
|
-
-
|
|
366
|
+
- \`{SPEC_ROOT}/docs/<project>/scan/ARCHITECTURE.md\`
|
|
367
|
+
- \`{SPEC_ROOT}/docs/<project>/scan/CONVENTIONS.md\`
|
|
368
|
+
- \`{SPEC_ROOT}/docs/<project>/modules/_module-map.yaml\`
|
|
367
369
|
- 命中的模块文档
|
|
368
370
|
4. 按 design.md 文件变更清单读取相关源码、测试、配置、schema 或样例数据;矛盾经常藏在设计与外部约束交叉处,素材宁可多读,不要只读摘要。
|
|
369
371
|
|
|
@@ -453,9 +455,9 @@ status: passed | needs-user-input | blocked | skipped
|
|
|
453
455
|
1. 展示 design.md 摘要 + **规模评估结果(small/large + 一句依据)** 给用户
|
|
454
456
|
2. 暂停等待用户选择:✅ 确认 / ✏️ 修改 / ❌ 推翻重来
|
|
455
457
|
3. 确认后,**按规模生成规范文件**:
|
|
456
|
-
- **scale=large**:在
|
|
458
|
+
- **scale=large**:在 \`{SPEC_ROOT}/changes/<change-name>/\` 下生成完整四件套(design.md / decisions.md 可选 / proposal.md / requirements.md / tasks.md),实现路径 → \`sillyspec run plan --change <变更名>\`
|
|
457
459
|
- **scale=small**:只生成/补全 design.md(proposal/requirements/tasks 对 quick 无用,不生成),实现路径 → \`sillyspec run quick --linked-changes <变更名>\`
|
|
458
|
-
- 两种规模都执行 \`git add .sillyspec/\` — 暂存规范文件(不要 commit
|
|
460
|
+
- 两种规模都执行 \`git add .sillyspec/\` — 暂存规范文件(不要 commit,由用户通过统一提交工具处理)。**平台模式跳过 git add**(specRoot 不在 sourceRoot 的 git repo 内)
|
|
459
461
|
|
|
460
462
|
所有规范文件头部必须包含 YAML frontmatter:
|
|
461
463
|
\`\`\`yaml
|
|
@@ -538,13 +540,13 @@ Then 期望结果
|
|
|
538
540
|
### 后续变更包处理
|
|
539
541
|
如果 MASTER.md 中规划了后续变更包(拆分后的子阶段),**必须同时为每个后续包创建独立变更目录**:
|
|
540
542
|
1. 读取 MASTER.md 中的变更包列表(包名 + 边界描述)
|
|
541
|
-
2. 为每个后续包创建目录:\`mkdir -p
|
|
543
|
+
2. 为每个后续包创建目录:\`mkdir -p {SPEC_ROOT}/changes/<后续包名>\`
|
|
542
544
|
3. 每个目录生成骨架文件:
|
|
543
545
|
- \`proposal.md\`:从 MASTER.md 中提取该包的动机和边界
|
|
544
546
|
- \`design.md\`:从 MASTER.md 中提取该包的职责描述(标记为「待设计 - 本包 design 在该包进入 brainstorm 时完善」)
|
|
545
547
|
- \`requirements.md\`:从 MASTER.md 中提取该包的需求范围(标记为「待完善」)
|
|
546
548
|
- \`tasks.md\`:创建空任务列表,标记为「待 plan 阶段展开」
|
|
547
|
-
4. \`git add .sillyspec/\` — 暂存所有新增文件(不要 commit
|
|
549
|
+
4. \`git add .sillyspec/\` — 暂存所有新增文件(不要 commit,由用户通过统一提交工具处理)。**平台模式跳过 git add**(specRoot 不在 sourceRoot 的 git repo 内)
|
|
548
550
|
5. 后续变更包的骨架文件同样必须包含 \`author: <git-user>\` 和 \`created_at: <now-datetime>\`
|
|
549
551
|
|
|
550
552
|
### 铁律 — 必须等待用户最终确认
|
package/src/stages/plan.js
CHANGED
|
@@ -94,7 +94,7 @@ const stepClassify = {
|
|
|
94
94
|
- 如果发现 superseded 决策,只引用最新版本,不引用旧版本
|
|
95
95
|
5. 读取 CONVENTIONS.md、ARCHITECTURE.md、STACK.md
|
|
96
96
|
6. 读取 local.yaml 获取构建/测试命令
|
|
97
|
-
7. 读取
|
|
97
|
+
7. 读取 \`{SPEC_ROOT}/docs/<project>/modules/_module-map.yaml\`(不存在则跳过)
|
|
98
98
|
- 根据 design.md 的文件变更清单匹配模块
|
|
99
99
|
- 读取匹配到的模块文档
|
|
100
100
|
- 利用模块依赖关系辅助分析(depends_on / used_by)
|
|
@@ -314,7 +314,7 @@ full 计划的约束:
|
|
|
314
314
|
2. 读取 design.md 获取文件变更清单
|
|
315
315
|
3. 读取上一步的 plan_level 分类结果
|
|
316
316
|
4. 按对应级别模板生成内容
|
|
317
|
-
5. 保存到变更目录下的 plan.md
|
|
317
|
+
5. 保存到变更目录下的 plan.md(路径格式:\`{SPEC_ROOT}/changes/<change-name>/plan.md\`,其中 <change-name> 是变更目录名,直接使用,不加子目录。正确路径示例:\`{SPEC_ROOT}/changes/2026-05-28-agent-log-streaming/plan.md\`)
|
|
318
318
|
**plan_level 为 none 时生成最小 plan.md(占位),不生成完整蓝图。**
|
|
319
319
|
|
|
320
320
|
---
|
|
@@ -384,16 +384,16 @@ tier: {REVIEW_TIER}({REVIEW_TIER_REASON})
|
|
|
384
384
|
* 每个 task 生成 20~40 行紧凑可执行卡片
|
|
385
385
|
*/
|
|
386
386
|
export function buildCoordinatorStep(changeDir, taskNames) {
|
|
387
|
-
const taskList = taskNames.map(
|
|
388
|
-
const num =
|
|
389
|
-
return `- task-${num}: ${name}`
|
|
387
|
+
const taskList = taskNames.map(t => {
|
|
388
|
+
const num = t.num
|
|
389
|
+
return `- task-${num}: ${t.name}`
|
|
390
390
|
}).join('\n')
|
|
391
391
|
|
|
392
|
-
const subagentPrompts = taskNames.map(
|
|
393
|
-
const num =
|
|
392
|
+
const subagentPrompts = taskNames.map(t => {
|
|
393
|
+
const num = t.num
|
|
394
394
|
return `\`\`\`
|
|
395
395
|
任务编号:task-${num}
|
|
396
|
-
任务名称:${name}
|
|
396
|
+
任务名称:${t.name}
|
|
397
397
|
文件路径:${changeDir}/tasks/task-${num}.md
|
|
398
398
|
当前时间:<now-datetime>(frontmatter 的 created_at 使用此值)
|
|
399
399
|
当前用户:<git-user>(frontmatter 的 author 使用此值)
|
|
@@ -405,7 +405,7 @@ export function buildCoordinatorStep(changeDir, taskNames) {
|
|
|
405
405
|
|
|
406
406
|
---
|
|
407
407
|
id: task-${num}
|
|
408
|
-
title: ${name}
|
|
408
|
+
title: ${t.name}
|
|
409
409
|
title_zh: <任务中文标题>
|
|
410
410
|
author: <git-user>
|
|
411
411
|
created_at: <now-datetime>
|
|
@@ -551,13 +551,14 @@ function parseTaskCount(planContent) {
|
|
|
551
551
|
* 从 plan.md 解析任务名列表
|
|
552
552
|
*/
|
|
553
553
|
function parseTaskNames(planContent) {
|
|
554
|
-
|
|
554
|
+
// 返回 [{name, num}]:保留 plan.md 原始编号,避免 buildCoordinatorStep 按 i+1 重编号与 execute 原始编号错位
|
|
555
|
+
const tasks = []
|
|
555
556
|
const lines = planContent.split('\n')
|
|
556
557
|
for (const line of lines) {
|
|
557
|
-
const m = line.match(/^[-*]\s*\[[ x]\]\s*task
|
|
558
|
-
if (m)
|
|
558
|
+
const m = line.match(/^[-*]\s*\[[ x]\]\s*task-(\d+):\s*(.+)/i)
|
|
559
|
+
if (m) tasks.push({ num: String(m[1]).padStart(2, '0'), name: m[2].trim() })
|
|
559
560
|
}
|
|
560
|
-
return
|
|
561
|
+
return tasks
|
|
561
562
|
}
|
|
562
563
|
|
|
563
564
|
/**
|
package/src/task-review.js
CHANGED
|
@@ -337,9 +337,22 @@ export function verifyReviewGitEvidence(review, gitDir) {
|
|
|
337
337
|
}
|
|
338
338
|
|
|
339
339
|
const emptyDiff = diffFiles.length === 0
|
|
340
|
+
// emptyDiff 回退:子代理可能未 commit,base..head 无 commit diff 但 working-tree 有改动。
|
|
341
|
+
// 对齐 checkExecuteCodeEvidence(stage-contract.js 同时查 working-tree)语义,避免误判零改动伪造。
|
|
342
|
+
let workingTreeChanged = false
|
|
343
|
+
if (emptyDiff) {
|
|
344
|
+
try {
|
|
345
|
+
const wtStatus = runGit(gitDir, ['status', '--porcelain'])
|
|
346
|
+
workingTreeChanged = !!wtStatus && wtStatus.trim().length > 0
|
|
347
|
+
} catch {}
|
|
348
|
+
if (workingTreeChanged) {
|
|
349
|
+
warnings.push(`base..head 无 commit diff(${review.base.slice(0, 8)}..${review.head.slice(0, 8)}),但 working-tree 有未提交改动 —— 按有效改动处理,不判零改动伪造`)
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
const effectiveEmptyDiff = emptyDiff && !workingTreeChanged
|
|
340
353
|
|
|
341
354
|
// changedFiles 交叉比对:完全不相交 = review 描述的改动与实际 diff 无关
|
|
342
|
-
if (!
|
|
355
|
+
if (!effectiveEmptyDiff && Array.isArray(review.changedFiles) && review.changedFiles.length > 0) {
|
|
343
356
|
const normalize = (p) => String(p).replace(/^\.\//, '')
|
|
344
357
|
const diffSet = new Set(diffFiles.map(normalize))
|
|
345
358
|
const hasOverlap = review.changedFiles.some(f => {
|
|
@@ -352,7 +365,7 @@ export function verifyReviewGitEvidence(review, gitDir) {
|
|
|
352
365
|
}
|
|
353
366
|
}
|
|
354
367
|
|
|
355
|
-
return { ok: errors.length === 0, emptyDiff, errors, warnings, unavailable: false }
|
|
368
|
+
return { ok: errors.length === 0, emptyDiff: effectiveEmptyDiff, errors, warnings, unavailable: false }
|
|
356
369
|
}
|
|
357
370
|
|
|
358
371
|
/**
|
package/src/worktree-apply.js
CHANGED
|
@@ -343,20 +343,15 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
|
|
|
343
343
|
* @returns {object} result(merged=true 表示走了 merge 降级)
|
|
344
344
|
*/
|
|
345
345
|
function applyByMerge(result, changeName, projectRoot, wm) {
|
|
346
|
+
const meta = wm.getMeta(changeName);
|
|
347
|
+
const changedFiles = result.changedFiles || [];
|
|
348
|
+
// 用 meta.branch(native-worktree 模式分支名可能不是 sillyspec/<change>),不硬编码。
|
|
349
|
+
const branch = meta.branch || `sillyspec/${changeName}`;
|
|
350
|
+
|
|
346
351
|
try {
|
|
347
|
-
git(projectRoot, `merge --no-ff
|
|
348
|
-
result.merged = true;
|
|
349
|
-
result.ok = true;
|
|
350
|
-
try { result.mergeSummary = git(projectRoot, `log --oneline -1`); } catch {}
|
|
351
|
-
// 成功后自动 cleanup(与 patch 路径步骤 8 一致;分支已合并,删除安全)
|
|
352
|
-
try {
|
|
353
|
-
wm.cleanup(changeName);
|
|
354
|
-
} catch (cleanupErr) {
|
|
355
|
-
result.warnings = result.warnings || [];
|
|
356
|
-
result.warnings.push(`cleanup 失败(不影响 merge 结果): ${cleanupErr.message}`);
|
|
357
|
-
}
|
|
352
|
+
git(projectRoot, `merge --no-ff ${branch}`);
|
|
358
353
|
} catch (e) {
|
|
359
|
-
// merge
|
|
354
|
+
// merge 冲突:取冲突文件列表 + abort 回滚(不 cleanup,保留 worktree)
|
|
360
355
|
let conflictFiles = [];
|
|
361
356
|
try {
|
|
362
357
|
const cf = gitQuiet(projectRoot, `diff --name-only --diff-filter=U`);
|
|
@@ -364,10 +359,35 @@ function applyByMerge(result, changeName, projectRoot, wm) {
|
|
|
364
359
|
} catch {}
|
|
365
360
|
try { gitQuiet(projectRoot, `merge --abort`); } catch {}
|
|
366
361
|
result.errors.push(
|
|
367
|
-
`git merge
|
|
362
|
+
`git merge ${branch} 冲突,请手动解决。冲突文件:\n` +
|
|
368
363
|
(conflictFiles.length ? ` ${conflictFiles.join('\n ')}\n` : ` (未能获取冲突文件列表)\n`) +
|
|
369
364
|
`已执行 git merge --abort 回滚到合并前状态。`
|
|
370
365
|
);
|
|
366
|
+
return result;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// 落地校验:merge 报成功(exit 0)不代表交付物真进 main——
|
|
370
|
+
// 分支可能只含 baseline checkpoint(子代理改动未 commit),merge 产生空内容合并,文件零落地。
|
|
371
|
+
// 逐个确认 changedFiles 在 main HEAD 存在。任一缺失 → 不 cleanup(fail-open:保留 worktree 唯一副本)。
|
|
372
|
+
result.merged = true;
|
|
373
|
+
const notLanded = changedFiles.filter(f => gitQuiet(projectRoot, `cat-file -e HEAD:${f}`) === null);
|
|
374
|
+
if (notLanded.length > 0) {
|
|
375
|
+
result.ok = false;
|
|
376
|
+
result.errors.push(
|
|
377
|
+
`git merge ${branch} 报成功,但 ${notLanded.length}/${changedFiles.length} 个交付文件未出现在 main HEAD` +
|
|
378
|
+
`(分支可能只含 baseline checkpoint,子代理改动未 commit)。worktree 已保留(未 cleanup),` +
|
|
379
|
+
`请用 git cherry-pick 或手动 cp 落地:\n ${notLanded.join('\n ')}`
|
|
380
|
+
);
|
|
381
|
+
return result;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
result.ok = true;
|
|
385
|
+
try { result.mergeSummary = git(projectRoot, `log --oneline -1`); } catch {}
|
|
386
|
+
try {
|
|
387
|
+
wm.cleanup(changeName);
|
|
388
|
+
} catch (cleanupErr) {
|
|
389
|
+
result.warnings = result.warnings || [];
|
|
390
|
+
result.warnings.push(`cleanup 失败(不影响 merge 结果): ${cleanupErr.message}`);
|
|
371
391
|
}
|
|
372
392
|
return result;
|
|
373
393
|
}
|