sillyspec 3.23.0 → 3.23.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/index.js +6 -2
- package/src/run.js +2 -2
- package/src/stages/execute.js +2 -2
- package/src/stages/plan-postcheck.js +9 -3
- package/src/stages/plan.js +5 -2
- package/src/task-review.js +6 -3
- package/src/worktree-apply.js +54 -2
- package/src/worktree.js +5 -1
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -632,12 +632,13 @@ SillySpec worktree — git worktree 隔离管理
|
|
|
632
632
|
}
|
|
633
633
|
case 'apply': {
|
|
634
634
|
if (!wtName) {
|
|
635
|
-
console.error('❌ 用法: sillyspec worktree apply <change-name> [--check-only]');
|
|
635
|
+
console.error('❌ 用法: sillyspec worktree apply <change-name> [--check-only] [--merge]');
|
|
636
636
|
process.exit(1);
|
|
637
637
|
}
|
|
638
638
|
const checkOnly = args.includes('--check-only');
|
|
639
|
+
const merge = args.includes('--merge');
|
|
639
640
|
const { applyWorktree } = await import('./worktree-apply.js');
|
|
640
|
-
const result = applyWorktree(wtName, { cwd: dir, checkOnly });
|
|
641
|
+
const result = applyWorktree(wtName, { cwd: dir, checkOnly, merge });
|
|
641
642
|
|
|
642
643
|
if (result.errors.length > 0) {
|
|
643
644
|
console.error(`❌ 校验失败:`);
|
|
@@ -657,6 +658,8 @@ SillySpec worktree — git worktree 隔离管理
|
|
|
657
658
|
for (const f of result.changedFiles) {
|
|
658
659
|
console.log(` ${f}`);
|
|
659
660
|
}
|
|
661
|
+
} else if (result.merged) {
|
|
662
|
+
console.log(`✅ 已通过 git merge 应用变更(baseline 漂移降级,引入合并提交)${result.mergeSummary ? ':' + result.mergeSummary : ''}`);
|
|
660
663
|
} else {
|
|
661
664
|
console.log(`✅ 已应用 ${result.changedFiles.length} 个文件变更`);
|
|
662
665
|
}
|
|
@@ -710,6 +713,7 @@ SillySpec worktree — git worktree 隔离管理
|
|
|
710
713
|
console.log('Action: blocked');
|
|
711
714
|
console.log(' → 检查变更: sillyspec worktree diff ' + wtName);
|
|
712
715
|
console.log(' → 丢弃变更: sillyspec worktree cleanup ' + wtName);
|
|
716
|
+
console.log(' → baseline 漂移降级: sillyspec worktree apply ' + wtName + ' --merge');
|
|
713
717
|
}
|
|
714
718
|
break;
|
|
715
719
|
}
|
package/src/run.js
CHANGED
|
@@ -3321,7 +3321,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
3321
3321
|
} catch {}
|
|
3322
3322
|
|
|
3323
3323
|
const reviewResult = validateTaskReviews({ planContent, runtimeRoot, executeRunId, changeDir: planFile, gitDir: reviewGitDir })
|
|
3324
|
-
printReviewResult(reviewResult)
|
|
3324
|
+
printReviewResult(reviewResult, { runtimeRoot, executeRunId })
|
|
3325
3325
|
|
|
3326
3326
|
if (!reviewResult.ok) {
|
|
3327
3327
|
// Task review 校验失败,阻断 execute 完成
|
|
@@ -3329,7 +3329,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
3329
3329
|
const uncheckedTasks = reviewResult.errors.filter(e => e.includes('缺少 review.json'))
|
|
3330
3330
|
if (uncheckedTasks.length > 0) {
|
|
3331
3331
|
console.error('\n⚠️ 部分任务已在 plan.md 中勾选,但 review.json 不存在。')
|
|
3332
|
-
console.error(
|
|
3332
|
+
console.error(` 请取消勾选这些任务的 checkbox,或补充对应的 review.json(execute run ID: ${executeRunId})。`)
|
|
3333
3333
|
}
|
|
3334
3334
|
rollbackStageCompletion(stageData, steps, currentIdx)
|
|
3335
3335
|
progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
|
package/src/stages/execute.js
CHANGED
|
@@ -620,7 +620,7 @@ ${taskList}
|
|
|
620
620
|
|
|
621
621
|
**review.json 路径:**
|
|
622
622
|
|
|
623
|
-
task-XX
|
|
623
|
+
task-XX 对应:{SPEC_ROOT}/.runtime/execute-runs/{EXECUTE_RUN_ID}/tasks/task-XX/review.json
|
|
624
624
|
|
|
625
625
|
本 execute run 的固定 ID 是:{EXECUTE_RUN_ID}
|
|
626
626
|
**所有 task 的 review.json 必须使用这个 ID,不要自行创建新目录。**
|
|
@@ -641,7 +641,7 @@ task-XX 对应:.sillyspec/.runtime/execute-runs/{EXECUTE_RUN_ID}/tasks/task-XX
|
|
|
641
641
|
### 完成后
|
|
642
642
|
1. 为每个后端 router task,扫描变更文件提取 API 端点 artifact:
|
|
643
643
|
- 在变更文件中搜索所有 router 注册路径(@router.get/post/put/delete)
|
|
644
|
-
- 将端点清单写入
|
|
644
|
+
- 将端点清单写入 {SPEC_ROOT}/.runtime/contract-artifacts/<task-name>/endpoints.json
|
|
645
645
|
- 格式: { "task": "task-XX", "type": "backend_endpoints", "endpoints": [{ "method": "GET", "path": "/api/ppm/xxx" }] }
|
|
646
646
|
2. 运行 sillyspec run execute --done --input "用户原始反馈" --output "Wave ${waveIndex} 结果摘要"`
|
|
647
647
|
}
|
|
@@ -80,6 +80,10 @@ function parseAllowedPaths(content) {
|
|
|
80
80
|
* @returns {boolean}
|
|
81
81
|
*/
|
|
82
82
|
function hasAcceptanceCriteria(content) {
|
|
83
|
+
// 新模板把 goal/implementation/acceptance/verify/constraints 放在 frontmatter
|
|
84
|
+
// (acceptance: 作为 fm 字段),老格式或人工写的卡片可能用 body 章节。
|
|
85
|
+
// 两种都认,避免「模板格式」与「postcheck 判定」不一致导致卡片过不了校验。
|
|
86
|
+
if (/^acceptance:/m.test(content)) return true
|
|
83
87
|
return /##\s*验收标准/.test(content) || /##\s*Acceptance/.test(content)
|
|
84
88
|
}
|
|
85
89
|
|
|
@@ -89,6 +93,8 @@ function hasAcceptanceCriteria(content) {
|
|
|
89
93
|
* @returns {boolean}
|
|
90
94
|
*/
|
|
91
95
|
function hasTddOrVerify(content) {
|
|
96
|
+
// 同 hasAcceptanceCriteria:新模板用 frontmatter 的 verify: 字段,老格式用 body 章节。
|
|
97
|
+
if (/^verify:/m.test(content)) return true
|
|
92
98
|
return /##\s*TDD/.test(content) || /##\s*验证/.test(content) || /##\s*Verify/.test(content)
|
|
93
99
|
}
|
|
94
100
|
|
|
@@ -230,13 +236,13 @@ export function validateBlueprintConsistency(changeDir) {
|
|
|
230
236
|
taskInfo.set(taskId, { dependsOn, allowedPaths, hasAcceptance, hasTdd, file })
|
|
231
237
|
|
|
232
238
|
if (allowedPaths.length === 0) {
|
|
233
|
-
errors.push(`${taskId} (${file}): 缺少 allowed_paths
|
|
239
|
+
errors.push(`${taskId} (${file}): frontmatter 缺少 allowed_paths(需非空数组,列出本 task 真实改动的源文件;回归类 task 无源码改动时填被验证的关键入口文件)`)
|
|
234
240
|
}
|
|
235
241
|
if (!hasAcceptance) {
|
|
236
|
-
errors.push(`${taskId} (${file}):
|
|
242
|
+
errors.push(`${taskId} (${file}): 缺少验收标准——frontmatter 需有 acceptance: 列表字段,或 body 需有「## 验收标准」/「## Acceptance」章节`)
|
|
237
243
|
}
|
|
238
244
|
if (!hasTdd) {
|
|
239
|
-
warnings.push(`${taskId} (${file}):
|
|
245
|
+
warnings.push(`${taskId} (${file}): 缺少验证步骤——frontmatter 需有 verify: 字段,或 body 需有「## TDD」/「## 验证」/「## Verify」章节`)
|
|
240
246
|
}
|
|
241
247
|
|
|
242
248
|
for (const p of allowedPaths) {
|
package/src/stages/plan.js
CHANGED
|
@@ -447,6 +447,10 @@ TaskCard 格式规则(必须严格遵守):
|
|
|
447
447
|
- 填写后 plan-postcheck 会做硬对账:consumer 的每个 expects_from[provider].needs 字段必须在对应 provider 的 provides.fields 里,否则 plan 阶段阻断(不进入 execute)
|
|
448
448
|
- 不要把内部实现字段塞进 provides;只暴露给其他 task 的对外契约形状
|
|
449
449
|
- 如果存在 decisions.md,无法覆盖的 D-xxx@vN 在 constraints 中标注
|
|
450
|
+
- **保存前格式自检**(plan-postcheck 会硬校验,不通过到 Step 4 会批量报错,先在这里逐条自查):
|
|
451
|
+
- frontmatter 字段齐全:id、title、title_zh、author、created_at、priority、depends_on、blocks、allowed_paths、goal、implementation、acceptance、verify、constraints
|
|
452
|
+
- allowed_paths 非空(至少一个真实源文件路径;回归类 task 无源码改动时填被验证的关键入口文件)
|
|
453
|
+
- acceptance 与 verify 都在 frontmatter 内(漏写会被 plan-postcheck 报「缺少验收标准」/「缺少验证步骤」)
|
|
450
454
|
- 写完后用 Write tool 保存到文件
|
|
451
455
|
\`\`\``
|
|
452
456
|
}).join('\n\n')
|
|
@@ -478,8 +482,7 @@ ${subagentPrompts}
|
|
|
478
482
|
|
|
479
483
|
## 验收(生成后自查,不另开步骤)
|
|
480
484
|
- 每个 task-N.md 文件存在且非空
|
|
481
|
-
- frontmatter 包含:id、title、author、created_at、priority、depends_on、blocks、allowed_paths
|
|
482
|
-
- body 包含:goal、implementation、acceptance、verify、constraints
|
|
485
|
+
- frontmatter 包含:id、title、author、created_at、priority、depends_on、blocks、allowed_paths、goal、implementation、acceptance、verify、constraints
|
|
483
486
|
- 每个 task 总长度 20~40 行
|
|
484
487
|
- **一致性自查**:
|
|
485
488
|
- allowed_paths 有无冲突
|
package/src/task-review.js
CHANGED
|
@@ -179,7 +179,7 @@ export function validateTaskReviews(opts) {
|
|
|
179
179
|
if (isTaskLowRisk(changeDir, taskId)) {
|
|
180
180
|
warnings.push(`${taskId}: 缺少 review.json — task 声明 low_risk: true,已豁免评审(type-only/机械迁移)`)
|
|
181
181
|
} else {
|
|
182
|
-
errors.push(`${taskId}: 缺少 review.json — task
|
|
182
|
+
errors.push(`${taskId}: 缺少 review.json — task 未经过评审(期望路径:${reviewPath},execute run ID: ${executeRunId})`)
|
|
183
183
|
}
|
|
184
184
|
}
|
|
185
185
|
continue
|
|
@@ -447,7 +447,7 @@ export function ensureTaskReviewDir(runtimeRoot, executeRunId, taskId) {
|
|
|
447
447
|
* 打印校验结果
|
|
448
448
|
* @param {{ ok: boolean, errors: string[], warnings: string[], requiredEvidence: Array }} result
|
|
449
449
|
*/
|
|
450
|
-
export function printReviewResult(result) {
|
|
450
|
+
export function printReviewResult(result, context = {}) {
|
|
451
451
|
if (result.ok && result.warnings.length === 0) {
|
|
452
452
|
console.log('\n✅ Task Review Gate — 所有任务评审通过')
|
|
453
453
|
return
|
|
@@ -458,7 +458,10 @@ export function printReviewResult(result) {
|
|
|
458
458
|
for (const err of result.errors) {
|
|
459
459
|
console.error(` - ${err}`)
|
|
460
460
|
}
|
|
461
|
-
|
|
461
|
+
const hint = context.runtimeRoot && context.executeRunId
|
|
462
|
+
? `期望路径:${context.runtimeRoot}/execute-runs/${context.executeRunId}/tasks/<task-XX>/review.json`
|
|
463
|
+
: '为缺失/失败的任务补充 review.json'
|
|
464
|
+
console.error(`\n 提示:${hint},然后重新 --done`)
|
|
462
465
|
}
|
|
463
466
|
|
|
464
467
|
if (result.warnings.length > 0) {
|
package/src/worktree-apply.js
CHANGED
|
@@ -73,7 +73,7 @@ function getFileBlobHash(cwd, treeish, filePath) {
|
|
|
73
73
|
* errors: string[]
|
|
74
74
|
* }}
|
|
75
75
|
*/
|
|
76
|
-
export function applyWorktree(changeName, { cwd, checkOnly = false } = {}) {
|
|
76
|
+
export function applyWorktree(changeName, { cwd, checkOnly = false, merge = false } = {}) {
|
|
77
77
|
const projectRoot = cwd || process.cwd();
|
|
78
78
|
const wm = new WorktreeManager({ cwd: projectRoot });
|
|
79
79
|
const meta = wm.getMeta(changeName);
|
|
@@ -84,6 +84,7 @@ export function applyWorktree(changeName, { cwd, checkOnly = false } = {}) {
|
|
|
84
84
|
hashMismatchFiles: [],
|
|
85
85
|
patchPath: null,
|
|
86
86
|
errors: [],
|
|
87
|
+
merged: false,
|
|
87
88
|
};
|
|
88
89
|
|
|
89
90
|
// --- 1. 校验 worktree 存在 + meta.json 有效 ---
|
|
@@ -172,9 +173,13 @@ export function applyWorktree(changeName, { cwd, checkOnly = false } = {}) {
|
|
|
172
173
|
const raw = `staged:${staged}\nunstaged:${unstaged}\nuntracked:${untracked}`;
|
|
173
174
|
const currentHash = createHash('sha256').update(raw).digest('hex').slice(0, 16);
|
|
174
175
|
if (currentHash !== meta.baselineHash) {
|
|
176
|
+
if (merge && !checkOnly) {
|
|
177
|
+
// baseline 漂移 + --merge 降级(D-001):跳过步骤 5-7(patch 路径),走 git merge
|
|
178
|
+
return applyByMerge(result, changeName, projectRoot, wm);
|
|
179
|
+
}
|
|
175
180
|
result.errors.push(
|
|
176
181
|
`主工作区 baseline 已变化(execute 前后不一致),不能直接 apply task.patch。\n` +
|
|
177
|
-
`建议:重新创建 worktree
|
|
182
|
+
`建议:重新创建 worktree,或用 --merge 降级(git merge sillyspec/${changeName} 替代 patch,会引入合并提交)。\n` +
|
|
178
183
|
`execute 前 baseline: ${meta.baselineHash}\n` +
|
|
179
184
|
`当前 baseline: ${currentHash}`
|
|
180
185
|
);
|
|
@@ -320,6 +325,53 @@ export function applyWorktree(changeName, { cwd, checkOnly = false } = {}) {
|
|
|
320
325
|
return result;
|
|
321
326
|
}
|
|
322
327
|
|
|
328
|
+
/**
|
|
329
|
+
* baseline 漂移时的 merge 降级路径(D-001)。
|
|
330
|
+
*
|
|
331
|
+
* 当主工作区 baseline 在 execute 期间漂移、patch 无法干净应用时,用
|
|
332
|
+
* `git merge sillyspec/<change>` 替代 patch apply(BRANCH_PREFIX='sillyspec/',worktree.js:18)。
|
|
333
|
+
* git merge 比 patch 鲁棒,能处理 baseline 漂移 + 潜在冲突。会引入合并提交
|
|
334
|
+
* (D-002:与 worktree.md:84「patch 而非 merge 保持线性历史」架构决策的张力——
|
|
335
|
+
* 仅作 --merge 显式 opt-in,不改变默认 patch 行为)。
|
|
336
|
+
*
|
|
337
|
+
* merge 冲突时不自动解决:git merge --abort 回滚到合并前状态 + 报冲突文件列表。
|
|
338
|
+
*
|
|
339
|
+
* @param {object} result - applyWorktree 的 result 对象(mutate 后返回)
|
|
340
|
+
* @param {string} changeName
|
|
341
|
+
* @param {string} projectRoot - 主仓库根
|
|
342
|
+
* @param {object} wm - WorktreeManager 实例
|
|
343
|
+
* @returns {object} result(merged=true 表示走了 merge 降级)
|
|
344
|
+
*/
|
|
345
|
+
function applyByMerge(result, changeName, projectRoot, wm) {
|
|
346
|
+
try {
|
|
347
|
+
git(projectRoot, `merge --no-ff sillyspec/${changeName}`);
|
|
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
|
+
}
|
|
358
|
+
} catch (e) {
|
|
359
|
+
// merge 冲突:先取冲突文件列表,再 abort 回滚(避免半成品合并)
|
|
360
|
+
let conflictFiles = [];
|
|
361
|
+
try {
|
|
362
|
+
const cf = gitQuiet(projectRoot, `diff --name-only --diff-filter=U`);
|
|
363
|
+
conflictFiles = cf ? cf.split('\n').filter(Boolean) : [];
|
|
364
|
+
} catch {}
|
|
365
|
+
try { gitQuiet(projectRoot, `merge --abort`); } catch {}
|
|
366
|
+
result.errors.push(
|
|
367
|
+
`git merge sillyspec/${changeName} 冲突,请手动解决。冲突文件:\n` +
|
|
368
|
+
(conflictFiles.length ? ` ${conflictFiles.join('\n ')}\n` : ` (未能获取冲突文件列表)\n`) +
|
|
369
|
+
`已执行 git merge --abort 回滚到合并前状态。`
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
return result;
|
|
373
|
+
}
|
|
374
|
+
|
|
323
375
|
/**
|
|
324
376
|
* 风险审计:评估 worktree 变更是否可以安全自动 apply
|
|
325
377
|
*
|
package/src/worktree.js
CHANGED
|
@@ -1078,8 +1078,12 @@ export class WorktreeManager {
|
|
|
1078
1078
|
if (!status) {
|
|
1079
1079
|
return gitQuiet(worktreePath, 'rev-parse HEAD');
|
|
1080
1080
|
}
|
|
1081
|
+
// --no-verify:baseline 是锚点不是交付物,只是把主仓库 dirty 文件快照到 worktree
|
|
1082
|
+
// 分支上以便区分「前置 baseline」与「子代理新增改动」。它不该触发项目 pre-commit
|
|
1083
|
+
// hook(如 ruff format),否则主仓库 dirty 文件中任一不达标的会被 hook reformat
|
|
1084
|
+
// 致 commit 失败 → worktree 创建失败 → execute 无法启动。
|
|
1081
1085
|
execSync(
|
|
1082
|
-
`git commit -m "sillyspec: baseline checkpoint for ${changeName}"`,
|
|
1086
|
+
`git commit --no-verify -m "sillyspec: baseline checkpoint for ${changeName}"`,
|
|
1083
1087
|
{ cwd: worktreePath, encoding: 'utf8', stdio: ['pipe','pipe','pipe'], env }
|
|
1084
1088
|
);
|
|
1085
1089
|
const hash = git(worktreePath, 'rev-parse HEAD');
|