sillyspec 3.26.9 → 3.26.11
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 +85 -85
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -114
- package/.claude/skills/sillyspec-commit/SKILL.md +109 -109
- package/.claude/skills/sillyspec-continue/SKILL.md +45 -45
- package/.claude/skills/sillyspec-doctor/SKILL.md +76 -76
- package/.claude/skills/sillyspec-execute/SKILL.md +200 -200
- package/.claude/skills/sillyspec-explore/SKILL.md +95 -95
- package/.claude/skills/sillyspec-knowledge/SKILL.md +269 -269
- package/.claude/skills/sillyspec-plan/SKILL.md +148 -148
- package/.claude/skills/sillyspec-quick/SKILL.md +111 -111
- package/.claude/skills/sillyspec-resume/SKILL.md +68 -68
- package/.claude/skills/sillyspec-scan/SKILL.md +79 -79
- package/.claude/skills/sillyspec-state/SKILL.md +64 -64
- package/.claude/skills/sillyspec-status/SKILL.md +54 -54
- package/.claude/skills/sillyspec-verify/SKILL.md +83 -81
- package/.claude/skills/sillyspec-workspace/SKILL.md +169 -169
- package/.gitattributes +2 -0
- package/.husky/pre-push +20 -20
- package/.npmrc-publish +2 -0
- package/README.md +201 -201
- package/SKILL.md +90 -90
- package/package.json +39 -39
- package/packages/dashboard/dist/assets/index-Bq_Z2hne.js +7446 -7446
- package/packages/dashboard/dist/assets/index-O2W5RV4z.css +1 -1
- package/packages/dashboard/dist/index.html +16 -16
- package/packages/dashboard/package.json +25 -25
- package/packages/dashboard/server/executor.js +86 -86
- package/packages/dashboard/server/index.js +601 -601
- package/packages/dashboard/server/parser.js +526 -526
- package/src/change-risk-profile.js +254 -254
- package/src/classify-change.js +109 -109
- package/src/config-schema.js +310 -310
- package/src/contract-matrix.js +379 -379
- package/src/db.js +350 -350
- package/src/dispatch/backends/local-agent.js +155 -155
- package/src/dispatch/backends/sillyhub-mcp.js +245 -245
- package/src/dispatch/probe.js +210 -210
- package/src/dispatch/strategy.js +121 -121
- package/src/docs-check.js +131 -4
- package/src/doctor-diagnostics.js +711 -711
- package/src/endpoint-extractor.js +401 -401
- package/src/fs-atomic.js +69 -69
- package/src/git-helper.js +79 -79
- package/src/hooks/claude-pre-tool-use.cjs +125 -125
- package/src/hooks/worktree-guard.js +848 -848
- package/src/index.js +90 -8
- package/src/init.js +623 -623
- package/src/knowledge-match.js +130 -130
- package/src/local-detect.js +120 -120
- package/src/machine-interface.js +543 -543
- package/src/migrate.js +127 -127
- package/src/progress/change-registry.js +297 -260
- package/src/progress/consistency-doctor.js +355 -355
- package/src/progress/shared.js +36 -36
- package/src/progress/stage-machine.js +29 -1
- package/src/progress/step-store.js +261 -261
- package/src/progress.js +1131 -1127
- package/src/quicklog.js +693 -675
- package/src/review-tier.js +77 -77
- package/src/run/command.js +1385 -1364
- package/src/run/complete-handlers.js +198 -9
- package/src/run/complete.js +225 -24
- package/src/run/concurrent-detect.js +22 -0
- package/src/run/gates.js +755 -755
- package/src/run/prompt.js +761 -721
- package/src/run/quick-audit.js +155 -146
- package/src/run/shared.js +1268 -1197
- package/src/run/stage.js +487 -479
- package/src/scan-postcheck.js +371 -371
- package/src/setup.js +416 -416
- package/src/sillyhub-mcp/client.js +529 -529
- package/src/spec-sync.js +323 -0
- package/src/stage-contract.js +897 -897
- package/src/stage-review.js +549 -549
- package/src/stages/archive.js +155 -154
- package/src/stages/brainstorm-auto.js +236 -236
- package/src/stages/brainstorm.js +596 -597
- package/src/stages/cmd-existence.js +145 -145
- package/src/stages/doctor.js +371 -371
- package/src/stages/execute.js +1003 -991
- package/src/stages/explore.js +34 -34
- package/src/stages/index.js +29 -29
- package/src/stages/knowledge.js +498 -498
- package/src/stages/plan-postcheck.js +1302 -1281
- package/src/stages/plan.js +625 -604
- package/src/stages/quick.js +151 -143
- package/src/stages/scan.js +606 -605
- package/src/stages/status.js +64 -64
- package/src/stages/verify.js +264 -261
- package/src/sync.js +1223 -1125
- package/src/task-review.js +1019 -1019
- package/src/verify-postcheck.js +1186 -1186
- package/src/workflow.js +600 -600
- package/src/worktree-apply.js +1213 -1136
- package/src/worktree-deps.js +353 -353
- package/src/worktree.js +1481 -1437
- package/templates/workflows/archive-impact.yaml +79 -79
- package/templates/workflows/scan-docs.yaml +132 -132
|
@@ -26,7 +26,7 @@ import { basename, join, resolve, relative, isAbsolute } from 'node:path'
|
|
|
26
26
|
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, rmSync } from 'node:fs'
|
|
27
27
|
import { renameSyncRetry, writeAtomicSync } from '../fs-atomic.js'
|
|
28
28
|
import { resolveChangeDir, resolveQuickSessionsDir, safeGit, auditQuickCompletion, triggerSync, isQuickMetadata, resolveRuntimeRoot } from './shared.js'
|
|
29
|
-
import { detectConcurrentChanges, formatConcurrentWarning } from './concurrent-detect.js'
|
|
29
|
+
import { detectConcurrentChanges, formatConcurrentWarning, resolveConcurrentAnchor } from './concurrent-detect.js'
|
|
30
30
|
import { stageRegistry } from '../stages/index.js'
|
|
31
31
|
import { SCAN_STATUS, POINTER_STATUS } from '../constants.js'
|
|
32
32
|
import { printQuickAuditReview } from './quick-audit.js'
|
|
@@ -719,7 +719,9 @@ export async function handleWorkflowPostCheck({ stageName, steps, currentIdx, cw
|
|
|
719
719
|
const icon = impactResult.status === 'pass' ? '✅' : '❌'
|
|
720
720
|
console.log(`${icon} module-impact.md 检查${impactResult.status === 'pass' ? '通过' : '失败'}`)
|
|
721
721
|
for (const f of (result.failures || []).filter(f => f.role_id === 'impact-analyzer')) {
|
|
722
|
-
|
|
722
|
+
// f 是 {level,role_id,output,check,message} 对象——直接 ${f} 打印裸 [object Object](ql-20260819-006);
|
|
723
|
+
// message 才是人类可读 detail(如「缺少章节: …」),缺字段时 stringify 兜底防再退化成不可诊断输出
|
|
724
|
+
console.log(` └─ ${f.message ?? JSON.stringify(f)}`)
|
|
723
725
|
}
|
|
724
726
|
}
|
|
725
727
|
const saved = saveWorkflowRun(result, {
|
|
@@ -812,10 +814,15 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
|
|
|
812
814
|
// D-003),用 ?. + ?? [] 兜底防 spread undefined 抛 TypeError(B-005)。整钩子 try/catch 隔离
|
|
813
815
|
// ——detect 本就 fail-open,保守再包一层,任何异常只吞不 bubble,主完成流程不受影响。
|
|
814
816
|
try {
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
817
|
+
// ownFiles 锚点(2026-08-18 误归属修复):声明会话 = baselineFiles ∪ allowedFiles(声明即归属),
|
|
818
|
+
// 不再用 changedFiles 全量——他者窗口文件已被污染进 changedFiles,旧口径自吞致预检对
|
|
819
|
+
// ql-20260818-003 形态结构性失明。未声明会话维持旧口径(changed ∪ baseline):无声明无归属
|
|
820
|
+
// 信息,收窄锚点会把自身未声明改动全误报他者。resolveConcurrentAnchor 纯函数(concurrent-detect)。
|
|
821
|
+
const ownFiles = resolveConcurrentAnchor({
|
|
822
|
+
changedFiles: review?.changedFiles ?? [],
|
|
823
|
+
baselineFiles: mergedGuard?.baselineFiles ?? [],
|
|
824
|
+
allowedFiles: mergedGuard?.allowedFiles ?? [],
|
|
825
|
+
})
|
|
819
826
|
const detected = detectConcurrentChanges(cwd, {
|
|
820
827
|
changeName,
|
|
821
828
|
linkedChanges: mergedGuard?.linkedChanges ?? [],
|
|
@@ -892,13 +899,37 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
|
|
|
892
899
|
try {
|
|
893
900
|
// 回填实际改动文件:review.changedFiles 含 quick 自身元数据(quicklog/.runtime/modules 等),
|
|
894
901
|
// 用 isQuickMetadata 过滤掉,只留真实业务文件。brownfield 无 review → 空数组,文件行不动。
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
902
|
+
// 归属口径(2026-08-18 误归属修复):声明会话优先 attributedFiles(窗口∩声明∪同文件并发命中),
|
|
903
|
+
// 他者窗口文件不进文件行;未声明会话/旧 review 无 attributedFiles → 兜底 changedFiles 全量。
|
|
904
|
+
const auditFiles = Array.isArray(review?.attributedFiles)
|
|
905
|
+
? review.attributedFiles
|
|
906
|
+
: (Array.isArray(review?.changedFiles) ? review.changedFiles : [])
|
|
907
|
+
const realFiles = auditFiles.filter(f => !isQuickMetadata(f, linkedChanges))
|
|
908
|
+
// D-8 落盘(2026-08-18 修):advisory 欠账信号从「纯打印」升级为「随条目落盘」——修复
|
|
909
|
+
// 「欠账已记录(QUICKLOG reasons)」的不实承诺(交叉审查实证 reasons 纯 stdout,事后不可审计)。
|
|
910
|
+
// 两周实测(2026-08-31 裁决,doc-consistency-debt §七)需要分母:信号触发次数必须可追溯。
|
|
911
|
+
const auditNotes = []
|
|
912
|
+
if (review?.docSyncHint && review.docSyncHint.touchedSource > 0 && review.docSyncHint.docFiles.length === 0) {
|
|
913
|
+
const mods = Array.isArray(review.docSyncHint.modules) && review.docSyncHint.modules.length > 0
|
|
914
|
+
? `(涉及模块:${review.docSyncHint.modules.map((m) => m.id).join(' · ')})` : ''
|
|
915
|
+
auditNotes.push(`📝 文档欠账(D-8):${review.docSyncHint.touchedSource} 个源码文件改动未同步任何模块文档${mods}`)
|
|
916
|
+
}
|
|
917
|
+
if (review?.docsCheckHint && review.docsCheckHint.invalid > 0) {
|
|
918
|
+
auditNotes.push(`📎 文档引用失效:${review.docsCheckHint.invalid}/${review.docsCheckHint.total} 处 file:line 失效(sillyspec docs check 可复现)`)
|
|
919
|
+
}
|
|
920
|
+
// 归属切分注(2026-08-18 误归属修复):窗口内未声明脏文件不进「文件:」行,但必须落盘可追溯
|
|
921
|
+
// (多 agent 并发仓他者窗口改动 / 本会话漏声明均可能),防真实改动被静默挤走。
|
|
922
|
+
if (Array.isArray(review?.undeclaredFiles) && review.undeclaredFiles.length > 0) {
|
|
923
|
+
const undeclared = review.undeclaredFiles.filter(f => !isQuickMetadata(f, linkedChanges))
|
|
924
|
+
if (undeclared.length > 0) {
|
|
925
|
+
auditNotes.push(`⚖️ 归属切分:${undeclared.length} 个窗口内未声明脏文件未计入文件行(并行会话改动或本会话漏声明):${undeclared.join(', ')}`)
|
|
926
|
+
}
|
|
927
|
+
}
|
|
898
928
|
await completeQuicklogEntry(specBase, gitUser, qlId, {
|
|
899
929
|
resultText: outputText || '',
|
|
900
930
|
linkedChanges,
|
|
901
931
|
changedFiles: realFiles,
|
|
932
|
+
auditNotes,
|
|
902
933
|
})
|
|
903
934
|
console.log(`📝 QUICKLOG 条目 ${qlId} 已标记完成`)
|
|
904
935
|
// 刷新 DB title:从 step3「需求:」提取(agent 可改 title 的途径),覆盖启动时的兜底快照。
|
|
@@ -911,6 +942,21 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
|
|
|
911
942
|
console.warn(`⚠️ QUICKLOG 完成态写入失败: ${e.message}`)
|
|
912
943
|
}
|
|
913
944
|
|
|
945
|
+
// 轻量归档任务已全勾选的关联真实变更
|
|
946
|
+
try {
|
|
947
|
+
const closeResult = await closeQuickLinkedChanges({ pm, cwd, specBase, linkedChanges, platformOpts })
|
|
948
|
+
if (closeResult.closed.length > 0) {
|
|
949
|
+
console.log(`📦 已自动归档 ${closeResult.closed.length} 个关联变更:${closeResult.closed.join(', ')}`)
|
|
950
|
+
}
|
|
951
|
+
if (closeResult.skipped.length > 0) {
|
|
952
|
+
for (const s of closeResult.skipped) {
|
|
953
|
+
console.log(`⏭️ 关联变更 ${s.name} 未归档:${s.reason}`)
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
} catch (e) {
|
|
957
|
+
console.warn(`⚠️ 关联变更轻量归档失败(不阻断 quick 完成): ${e.message}`)
|
|
958
|
+
}
|
|
959
|
+
|
|
914
960
|
// 清理 session 目录(rmSync/unlinkSync 容忍不存在)。路径与写入对齐(Q4 resolveQuickSessionsDir)。
|
|
915
961
|
try {
|
|
916
962
|
if (changeName) {
|
|
@@ -933,8 +979,151 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
|
|
|
933
979
|
}
|
|
934
980
|
}
|
|
935
981
|
}
|
|
982
|
+
|
|
936
983
|
return null
|
|
937
984
|
}
|
|
985
|
+
|
|
986
|
+
/**
|
|
987
|
+
* 判定变更 tasks.md 是否全部勾选完成。
|
|
988
|
+
* 无 tasks.md 或有未勾选项均返回 false(保守策略)。
|
|
989
|
+
*
|
|
990
|
+
* @param {string} specBase - .sillyspec 根目录
|
|
991
|
+
* @param {string} changeName - 变更名
|
|
992
|
+
* @returns {boolean}
|
|
993
|
+
*/
|
|
994
|
+
function isChangeTasksComplete(specBase, changeName) {
|
|
995
|
+
const tasksPath = join(specBase, 'changes', changeName, 'tasks.md')
|
|
996
|
+
if (!existsSync(tasksPath)) return false
|
|
997
|
+
try {
|
|
998
|
+
const content = readFileSync(tasksPath, 'utf8').replace(/\r\n/g, '\n')
|
|
999
|
+
return !/^-\s*\[\s*\]\s+/m.test(content)
|
|
1000
|
+
} catch {
|
|
1001
|
+
return false
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
/**
|
|
1006
|
+
* 对单个已完成的关联真实变更执行轻量归档(不校验 plan.md/module-impact.md)。
|
|
1007
|
+
*
|
|
1008
|
+
* @param {Object} opts
|
|
1009
|
+
* @param {ProgressManager} opts.pm
|
|
1010
|
+
* @param {string} opts.cwd
|
|
1011
|
+
* @param {string} opts.specBase
|
|
1012
|
+
* @param {string} opts.changeName
|
|
1013
|
+
* @param {Object} [opts.platformOpts]
|
|
1014
|
+
* @returns {Promise<{closed:boolean, destDir?:string, reason?:string}>}
|
|
1015
|
+
*/
|
|
1016
|
+
async function closeSingleQuickLinkedChange({ pm, cwd, specBase, changeName, platformOpts = {} }) {
|
|
1017
|
+
const srcDir = join(specBase, 'changes', changeName)
|
|
1018
|
+
if (!existsSync(srcDir)) {
|
|
1019
|
+
return { closed: false, reason: '源目录不存在' }
|
|
1020
|
+
}
|
|
1021
|
+
const changesDir = join(specBase, 'changes')
|
|
1022
|
+
const archiveDir = join(changesDir, 'archive')
|
|
1023
|
+
const date = new Date().toISOString().slice(0, 10)
|
|
1024
|
+
const destName = archiveDestDirName(date, changeName)
|
|
1025
|
+
const destDir = join(archiveDir, destName)
|
|
1026
|
+
|
|
1027
|
+
// 幂等:目标目录已存在或已在 archive/ 中
|
|
1028
|
+
if (existsSync(destDir) || findAlreadyArchivedDir(archiveDir, changeName)) {
|
|
1029
|
+
return { closed: false, reason: `目标目录已存在(${destName})` }
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
mkdirSync(archiveDir, { recursive: true })
|
|
1033
|
+
try {
|
|
1034
|
+
renameSyncRetry(srcDir, destDir)
|
|
1035
|
+
} catch (e) {
|
|
1036
|
+
return { closed: false, reason: `移动目录失败:${e.message}` }
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
pm.unregisterChange(cwd, changeName)
|
|
1040
|
+
await archiveWorktreeCleanup(cwd, changeName, specBase, platformOpts)
|
|
1041
|
+
safeGit(cwd, ['add', '--', `.sillyspec/changes/archive/${destName}/`])
|
|
1042
|
+
|
|
1043
|
+
console.log(`📦 关联变更已自动归档:${changeName} → archive/${destName}/`)
|
|
1044
|
+
return { closed: true, destDir }
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
/**
|
|
1048
|
+
* quick 轻量归档的阶段闸允许集:仅「从未进入完整流程」的变更可被 quick --done 自动归档
|
|
1049
|
+
* (d192f89 原始场景:评估 small 转 quick、停在 brainstorm 的僵尸变更)。execute 完成后
|
|
1050
|
+
* tasks.md 必然全勾选而流程未收尾——不看 current_stage 会把 verify/中途的完整流程变更
|
|
1051
|
+
* 绕过 verify/archive 校验直接归档注销(quick-close-midflight 缺陷)。
|
|
1052
|
+
*
|
|
1053
|
+
* 阶段名允许集之外还有「阶段完成态」闸(ql-20260819-010,quick-done-autoarchive-misfire
|
|
1054
|
+
* 缺陷①):current_stage=brainstorm 但该阶段 status=completed(brainstorm 收尾到 plan
|
|
1055
|
+
* 开始之间的空窗)≠ 僵尸变更——此时关联 quick --done 会把即将进 plan 的进行中变更误
|
|
1056
|
+
* 轻量归档。stage_status=completed 一律走原流程收尾。
|
|
1057
|
+
*/
|
|
1058
|
+
const QUICK_CLOSE_ALLOWED_STAGES = new Set(['', 'scan', 'brainstorm'])
|
|
1059
|
+
|
|
1060
|
+
/**
|
|
1061
|
+
* quick --done 完成后,自动关闭任务已全部完成的关联真实变更。
|
|
1062
|
+
* quick-<hex> sessionId 自身不在此处理(由调用方单独注销)。
|
|
1063
|
+
* 单个归档失败 catch warn,不阻断 quick 完成。
|
|
1064
|
+
*
|
|
1065
|
+
* 阶段闸(fail-closed):先查 changes.current_stage——无 DB 记录(未注册目录桩)或停在
|
|
1066
|
+
* scan/brainstorm 才继续 tasks 判定;plan/execute/verify/archive 一律 skip 走原流程收尾。
|
|
1067
|
+
* pm 缺 getChangeStage 接口或查询抛错同样 skip,不静默放行。
|
|
1068
|
+
*
|
|
1069
|
+
* @param {Object} opts
|
|
1070
|
+
* @param {ProgressManager} opts.pm
|
|
1071
|
+
* @param {string} opts.cwd
|
|
1072
|
+
* @param {string} opts.specBase
|
|
1073
|
+
* @param {string[]} [opts.linkedChanges]
|
|
1074
|
+
* @param {Object} [opts.platformOpts]
|
|
1075
|
+
* @returns {Promise<{closed:string[], skipped:{name:string,reason:string}[]}>}
|
|
1076
|
+
*/
|
|
1077
|
+
export async function closeQuickLinkedChanges({ pm, cwd, specBase, linkedChanges = [], platformOpts = {} }) {
|
|
1078
|
+
const closed = []
|
|
1079
|
+
const skipped = []
|
|
1080
|
+
// 只处理真实变更,跳过 quick 会话 sessionId
|
|
1081
|
+
const realChanges = linkedChanges.filter((name) => !/^quick-[0-9a-f]{8}$/.test(name))
|
|
1082
|
+
for (const changeName of realChanges) {
|
|
1083
|
+
try {
|
|
1084
|
+
if (typeof pm.getChangeStage !== 'function') {
|
|
1085
|
+
skipped.push({ name: changeName, reason: '进度库接口缺失(getChangeStage),无法判定流程阶段,不自动归档' })
|
|
1086
|
+
continue
|
|
1087
|
+
}
|
|
1088
|
+
const stageInfo = pm.getChangeStage(cwd, changeName)
|
|
1089
|
+
if (stageInfo !== null && !QUICK_CLOSE_ALLOWED_STAGES.has(stageInfo.current_stage || '')) {
|
|
1090
|
+
skipped.push({
|
|
1091
|
+
name: changeName,
|
|
1092
|
+
reason: `变更处于完整流程「${stageInfo.current_stage}」阶段(tasks.md 全勾不等于流程收尾),不自动归档——请走原流程收尾(sillyspec progress show 查看进度)`,
|
|
1093
|
+
})
|
|
1094
|
+
continue
|
|
1095
|
+
}
|
|
1096
|
+
// ql-20260819-010(quick-done-autoarchive-misfire 缺陷①):当前阶段已完成 ≠
|
|
1097
|
+
// 从未进入完整流程的僵尸。brainstorm 收尾到 plan 开始之间的空窗里 current_stage
|
|
1098
|
+
// 仍读 brainstorm,只看阶段名会把即将进 plan 的变更误轻量归档(propose 骨架
|
|
1099
|
+
// tasks.md 除 quick 追加的 ql 行外没有任务行,「无未勾选框=全勾」恒真)。
|
|
1100
|
+
// stage_status=completed 一律原流程收尾;null(无阶段行/brownfield)与旧调用方
|
|
1101
|
+
// mock 缺字段(undefined)均按未完成放行,僵尸逃生通道行为不变。
|
|
1102
|
+
if (stageInfo !== null && stageInfo.stage_status === 'completed') {
|
|
1103
|
+
skipped.push({
|
|
1104
|
+
name: changeName,
|
|
1105
|
+
reason: `变更当前阶段「${stageInfo.current_stage || '(空)'}」已完成(推进/收尾中,非僵尸变更),不自动归档——请走原流程收尾(sillyspec progress show 查看进度)`,
|
|
1106
|
+
})
|
|
1107
|
+
continue
|
|
1108
|
+
}
|
|
1109
|
+
if (!isChangeTasksComplete(specBase, changeName)) {
|
|
1110
|
+
skipped.push({ name: changeName, reason: 'tasks.md 未全勾选或不存在' })
|
|
1111
|
+
continue
|
|
1112
|
+
}
|
|
1113
|
+
const result = await closeSingleQuickLinkedChange({ pm, cwd, specBase, changeName, platformOpts })
|
|
1114
|
+
if (result.closed) {
|
|
1115
|
+
closed.push(changeName)
|
|
1116
|
+
} else {
|
|
1117
|
+
skipped.push({ name: changeName, reason: result.reason || '未知原因' })
|
|
1118
|
+
}
|
|
1119
|
+
} catch (e) {
|
|
1120
|
+
console.warn(`⚠️ 关联变更 ${changeName} 轻量归档失败(不阻断 quick 完成): ${e.message}`)
|
|
1121
|
+
skipped.push({ name: changeName, reason: e.message })
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
return { closed, skipped }
|
|
1125
|
+
}
|
|
1126
|
+
|
|
938
1127
|
/**
|
|
939
1128
|
* execute「Wave N 执行」步骤完成后扫 worktree 提取 provider endpoint artifact(W6 Step6c 从
|
|
940
1129
|
* completeStep 内联块抽出)。供 verify 阶段 parity 对账 + consumer task 上游契约注入。
|
package/src/run/complete.js
CHANGED
|
@@ -29,11 +29,25 @@ import { handleArchiveConfirmStep, handlePlanGeneratePlanStep, handleScanProject
|
|
|
29
29
|
import { formatExecuteSummary } from '../worktree-apply.js'
|
|
30
30
|
import { isEndToEndTaskText } from '../change-risk-profile.js'
|
|
31
31
|
import { deriveTitleFromLinkedChange } from '../quicklog.js'
|
|
32
|
+
import { gitQuiet } from '../git-helper.js'
|
|
32
33
|
|
|
33
34
|
// validateMetadata / readDesignScale / validateFileLocations 已迁至 ./gates.js(completeStageGates
|
|
34
35
|
// 共享收尾管线,消除 noAI 末步 / continueStep 完成分支绕过 gate 的 S1/S2/S3 不对称)。
|
|
35
36
|
// completeStep 阶段完成分支(task-02 接入 completeStageGates)+ brainstorm 下一步提示改 import 自 gates.js。
|
|
36
37
|
|
|
38
|
+
// 完整流程 change title 刷新:从 proposal/design 首个 # 标题提取中文描述写 changes.title(人类可读
|
|
39
|
+
// 展示元信息)。proposal/design 落盘后的**任意步骤持久化点**都调用——单步 --done(brainstorm
|
|
40
|
+
// step6 design.md 已落盘但阶段未完)与阶段完成同样刷新,治「brainstorm 全程 title 存英文 autoName
|
|
41
|
+
// 兜底」。quick-<hex> 无 proposal/design 目录,deriveTitleFromLinkedChange 返回 '' 不刷新
|
|
42
|
+
// (quick 走 handleQuickStageCompletion 的 extractTitleFromResult)。失败静默(不阻断流程)。
|
|
43
|
+
function refreshChangeTitleFromArtifacts(pm, cwd, specBase, changeName) {
|
|
44
|
+
if (!changeName || /^quick-[0-9a-f]{8}$/.test(changeName)) return
|
|
45
|
+
try {
|
|
46
|
+
const refinedTitle = deriveTitleFromLinkedChange(specBase, changeName)
|
|
47
|
+
if (refinedTitle) pm.updateChangeMeta(cwd, changeName, { title: refinedTitle })
|
|
48
|
+
} catch { /* title 刷新失败不阻断 */ }
|
|
49
|
+
}
|
|
50
|
+
|
|
37
51
|
/**
|
|
38
52
|
* 坑1:用 --done --answer 解掉「已 waiting 的步骤」。
|
|
39
53
|
*
|
|
@@ -114,8 +128,28 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
114
128
|
console.log(`⚠️ Step "${steps[_resolvedWaitIdx].name}" 此前处于 waiting,--done --answer 已补回答并拉回待完成。`)
|
|
115
129
|
}
|
|
116
130
|
if (currentIdx === -1) {
|
|
117
|
-
|
|
118
|
-
|
|
131
|
+
// ── reopen stale 收尾路径(坑 reopen-done-escape-hatch-unreachable)──
|
|
132
|
+
// 全 completed+stale(无 pending/in-progress)时若只有 exit(1),--done --confirm 逃生门
|
|
133
|
+
// 永远到不了下方回填门控。修复:有 stale 时——无 confirm 报拦截指引(与门控同文案);
|
|
134
|
+
// 带 confirm 把首个 stale 拉回 pending 走完整完成管线,末步后的回填门控 confirm 分支
|
|
135
|
+
// 收尾其余 stale(--confirm 语义 = 全部 stale 一并确认)。
|
|
136
|
+
const _staleIdx = steps.findIndex(s => s.status === 'stale')
|
|
137
|
+
if (_staleIdx !== -1) {
|
|
138
|
+
const _staleNames = steps.filter(s => s.status === 'stale').map(s => s.name)
|
|
139
|
+
if (!confirm) {
|
|
140
|
+
console.error(`\n⏸️ 检测到 ${_staleNames.length} 个 stale 步骤(reopen 后未执行):${_staleNames.join('、')}`)
|
|
141
|
+
console.error(` 两条出路:`)
|
|
142
|
+
console.error(` ① sillyspec run ${stageName}${changeName ? ` --change ${changeName}` : ''} 逐个真实执行(stale 会被转为 pending)`)
|
|
143
|
+
console.error(` ② 确认方案未变,一次性回填收尾:sillyspec run ${stageName} --done --confirm${changeName ? ` --change ${changeName}` : ''} --output "..."`)
|
|
144
|
+
process.exit(1)
|
|
145
|
+
}
|
|
146
|
+
steps[_staleIdx].status = 'pending'
|
|
147
|
+
currentIdx = _staleIdx
|
|
148
|
+
console.log(`⚠️ --confirm 收尾:把 stale 步骤 "${steps[_staleIdx].name}" 拉回当前步骤走完成管线,其余 stale 由回填门控一并确认。`)
|
|
149
|
+
} else {
|
|
150
|
+
console.error(`没有待完成的步骤(阶段 ${stageName} 已无 pending/in-progress 步骤)。当前阶段状态:${stageData?.status ?? '未知'}。用 \`sillyspec run ${stageName} --status\` 查看进度,或 \`sillyspec progress show\` 看全局下一步。`)
|
|
151
|
+
process.exit(1)
|
|
152
|
+
}
|
|
119
153
|
}
|
|
120
154
|
|
|
121
155
|
// ── requiresWait 硬门控 ──
|
|
@@ -285,15 +319,40 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
285
319
|
// completed,否则进度数字与实际步骤明细矛盾(阶段已完成 + 6/8)。
|
|
286
320
|
// 安全边界:仅进入阶段完成分支时才回填(stale 本就被跳过、不回填也照常标 completed),
|
|
287
321
|
// 不改变单步推进行为;stale 若确需重跑,运行流(stage.js:141-148)会先于本分支转 pending 执行。
|
|
322
|
+
// FR-01 门控:无 --confirm 时不回填,阻断并给两条出路;带 --confirm 时回填并审计。
|
|
288
323
|
const staleSteps = steps.filter(s => s.status === 'stale')
|
|
289
324
|
if (staleSteps.length > 0) {
|
|
325
|
+
if (!confirm) {
|
|
326
|
+
// 无 confirm:不回填,阶段不完成,返回 staleBlocked 标记
|
|
327
|
+
const staleNames = staleSteps.map(s => `Step ${steps.indexOf(s) + 1}(${s.name})`).join(', ')
|
|
328
|
+
console.error(`⏸️ 检测到 ${staleSteps.length} 个 stale 步骤(reopen 后未执行)——两条出路:`)
|
|
329
|
+
console.error(` ① sillyspec run ${stageName} 逐个真实执行(首个 stale 自动转 pending)`)
|
|
330
|
+
console.error(` ② 确认方案未变用 --done --confirm 一次性回填收尾`)
|
|
331
|
+
console.error(` stale 步骤:${staleNames}`)
|
|
332
|
+
progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
333
|
+
pm._write(cwd, progress, changeName)
|
|
334
|
+
return { stageCompleted: false, currentIdx, nextPendingIdx: -1, staleBlocked: true }
|
|
335
|
+
}
|
|
336
|
+
// 带 confirm:按现行回填逻辑 + 审计记录
|
|
290
337
|
const nowStr = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
291
338
|
for (const st of staleSteps) {
|
|
292
339
|
st.status = 'completed'
|
|
293
340
|
st.completedAt = st.completedAt || nowStr
|
|
294
341
|
}
|
|
295
342
|
pm._write(cwd, progress, changeName)
|
|
296
|
-
console.log(` ⚠️ 同步回填 ${staleSteps.length} 个 stale 步骤为 completed(reopen --from-step N 后 --done,方案未变)`)
|
|
343
|
+
console.log(` ⚠️ 同步回填 ${staleSteps.length} 个 stale 步骤为 completed(reopen --from-step N 后 --done --confirm,方案未变)`)
|
|
344
|
+
// 审计记录 reopen-stale-backfill
|
|
345
|
+
try {
|
|
346
|
+
pm._appendAuditLog(cwd, {
|
|
347
|
+
action: 'reopen-stale-backfill',
|
|
348
|
+
stage: stageName,
|
|
349
|
+
change: changeName,
|
|
350
|
+
steps: staleSteps.map(s => s.name),
|
|
351
|
+
confirmedAt: nowStr
|
|
352
|
+
})
|
|
353
|
+
} catch (e) {
|
|
354
|
+
console.warn('⚠️ 回填审计日志写入失败(不阻断):', e.message)
|
|
355
|
+
}
|
|
297
356
|
}
|
|
298
357
|
|
|
299
358
|
stageData.status = 'completed'
|
|
@@ -331,15 +390,8 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
331
390
|
if (_stageGatesResult?.stageCompleted === false) process.exitCode = 1
|
|
332
391
|
if (_stageGatesResult) return _stageGatesResult
|
|
333
392
|
|
|
334
|
-
// 完整流程 change title
|
|
335
|
-
|
|
336
|
-
// deriveTitleFromLinkedChange 返回 '' 不刷新(quick 走 handleQuickStageCompletion 的 extractTitleFromResult)。
|
|
337
|
-
if (changeName && !/^quick-[0-9a-f]{8}$/.test(changeName)) {
|
|
338
|
-
try {
|
|
339
|
-
const refinedTitle = deriveTitleFromLinkedChange(specBase, changeName)
|
|
340
|
-
if (refinedTitle) pm.updateChangeMeta(cwd, changeName, { title: refinedTitle })
|
|
341
|
-
} catch { /* title 刷新失败不阻断完成 */ }
|
|
342
|
-
}
|
|
393
|
+
// 完整流程 change title 刷新(阶段完成时;proposal/design 已落盘,机制见 refreshChangeTitleFromArtifacts)
|
|
394
|
+
refreshChangeTitleFromArtifacts(pm, cwd, specBase, changeName)
|
|
343
395
|
|
|
344
396
|
// gate 全过:persist completed(task-01 移后)。此处到 _write 之间若崩,DB 仍 in-progress(内存已 completed 但未落盘),下次进 CLI 读 DB 即 in-progress,不产生"假 completed"。
|
|
345
397
|
pm._write(cwd, progress, changeName)
|
|
@@ -404,6 +456,9 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
404
456
|
progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
405
457
|
pm._write(cwd, progress, changeName)
|
|
406
458
|
triggerSync(cwd, changeName, platformOpts)
|
|
459
|
+
// 单步完成也刷新 change title:design.md 在 brainstorm step6 落盘,此后每次 --done 都该让
|
|
460
|
+
// changes.title 反映中文描述,不等阶段收尾(机制见 refreshChangeTitleFromArtifacts)。
|
|
461
|
+
refreshChangeTitleFromArtifacts(pm, cwd, specBase, changeName)
|
|
407
462
|
|
|
408
463
|
// Append to user-inputs.md
|
|
409
464
|
if (outputText) {
|
|
@@ -453,21 +508,90 @@ function readTaskCardText(changeDir, taskNum) {
|
|
|
453
508
|
try { return readFileSync(join(changeDir, 'tasks', `task-${taskNum}.md`), 'utf8') } catch { return '' }
|
|
454
509
|
}
|
|
455
510
|
|
|
511
|
+
/**
|
|
512
|
+
* W2 task-04/05 共用 helper:构造草稿零 diff 校验用的 ctx(与 task-review.js generateTaskReviewDrafts 同源)。
|
|
513
|
+
* 用于 autoCheckPlanFromReviews(勾选层)和 detectExecuteBatchFinish(批量层)。
|
|
514
|
+
* 失败返回 null(调用方降级:勾选层不勾、批量层不过滤——向后兼容)。
|
|
515
|
+
*
|
|
516
|
+
* @returns {Promise<{gitDir:string, base:string, head:string}|null>}
|
|
517
|
+
*/
|
|
518
|
+
async function buildDraftContext(cwd, changeName) {
|
|
519
|
+
try {
|
|
520
|
+
const { WorktreeManager } = await import('../worktree.js')
|
|
521
|
+
const meta = new WorktreeManager({ cwd }).getMeta(changeName)
|
|
522
|
+
const base = meta?.baselineCommit || meta?.baseHash || null
|
|
523
|
+
let gitDir = cwd
|
|
524
|
+
let head = null
|
|
525
|
+
if (meta?.worktreePath && meta.mode !== 'in-place-fallback' && existsSync(meta.worktreePath)) {
|
|
526
|
+
gitDir = meta.worktreePath
|
|
527
|
+
}
|
|
528
|
+
if (base) {
|
|
529
|
+
try {
|
|
530
|
+
head = gitQuiet(gitDir, ['rev-parse', 'HEAD'])
|
|
531
|
+
} catch (e) {
|
|
532
|
+
console.warn(`⚠️ ctx 构造失败(git rev-parse HEAD 失败:${e && e.message ? e.message : e}),跳过草稿 diff 校验`)
|
|
533
|
+
return null
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
if (base && head && gitDir) {
|
|
537
|
+
return { gitDir, base, head }
|
|
538
|
+
}
|
|
539
|
+
return null
|
|
540
|
+
} catch (e) {
|
|
541
|
+
console.warn(`⚠️ ctx 构造失败(${e && e.message ? e.message : e}),跳过草稿 diff 校验`)
|
|
542
|
+
return null
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
456
546
|
/**
|
|
457
547
|
* 判定 task 是否该被 autoCheckPlanFromReviews 自动勾选。
|
|
458
548
|
* 端到端/deployment-critical task 要求 review spec+quality 双 pass(cannot_verify 不算,防批量完成
|
|
459
549
|
* 放行未真验的端到端 task);普通 task 非 fail 即可(保主 agent 直接实现模式体验,其 cannot_verify
|
|
460
550
|
* 草稿仍可批量收尾)。坑 execute-batch-complete-endtoend-checkbox。
|
|
461
|
-
*
|
|
551
|
+
*
|
|
552
|
+
* W2 task-04 FR-03:草稿勾选层零 diff 守卫——review 为自动草稿且 ctx 给定时,额外要求
|
|
553
|
+
* changedFiles 非空且 git diff 实测非空。防 allowed_paths 误归属他人 diff 或陈旧 review.json 导致
|
|
554
|
+
* 空任务被自动勾选/批量放行。ctx 缺省时保持现行判定(向后兼容)。
|
|
555
|
+
*
|
|
556
|
+
* @param {{ok?:boolean, review?:{specVerdict?:string, qualityVerdict?:string, reviewerNotes?:string, changedFiles?:string[]}}} r readReview 结果
|
|
462
557
|
* @param {boolean} endToEnd 是否端到端/deployment-critical task
|
|
558
|
+
* @param {{gitDir?:string, base?:string, head?:string}|null} [ctx] 实测 diff 上下文;缺省保持现行判定
|
|
463
559
|
* @returns {boolean}
|
|
464
560
|
*/
|
|
465
|
-
export function shouldAutoCheckTask(r, endToEnd) {
|
|
561
|
+
export function shouldAutoCheckTask(r, endToEnd, ctx = null) {
|
|
466
562
|
if (!r?.ok) return false
|
|
467
563
|
const spec = r.review?.specVerdict
|
|
468
564
|
const quality = r.review?.qualityVerdict
|
|
469
565
|
if (spec === 'fail' || quality === 'fail') return false
|
|
470
566
|
if (endToEnd) return spec === 'pass' && quality === 'pass'
|
|
567
|
+
|
|
568
|
+
// W2 task-04 草稿零 diff 守卫(FR-03)
|
|
569
|
+
// ctx 给定且 review 为自动草稿时,要求 changedFiles 非空且实测 diff 非空
|
|
570
|
+
if (ctx && r.review?.reviewerNotes && r.review.reviewerNotes.includes('auto-generated draft')) {
|
|
571
|
+
const changedFiles = r.review.changedFiles || []
|
|
572
|
+
if (changedFiles.length === 0) {
|
|
573
|
+
console.warn(`⚠️ 草稿 review changedFiles 为空,跳过自动勾选`)
|
|
574
|
+
return false
|
|
575
|
+
}
|
|
576
|
+
if (!ctx.gitDir || !ctx.base || !ctx.head) {
|
|
577
|
+
console.warn(`⚠️ ctx 信息不完整(gitDir=${ctx.gitDir}, base=${ctx.base}, head=${ctx.head}),跳过草稿 diff 校验,保守不勾`)
|
|
578
|
+
return false
|
|
579
|
+
}
|
|
580
|
+
try {
|
|
581
|
+
// 实测 diff:git diff --name-only <base>..<head> -- <changedFiles>
|
|
582
|
+
// 用数组参数避免 shell 拆词,复用 git-helper 安全模式
|
|
583
|
+
const diffResult = gitQuiet(ctx.gitDir, ['diff', '--name-only', `${ctx.base}..${ctx.head}`, '--', ...changedFiles], { trim: true })
|
|
584
|
+
if (!diffResult || diffResult.trim() === '') {
|
|
585
|
+
console.warn(`⚠️ 草稿 review 实测 diff 为空(base=${ctx.base.slice(0,8)}, head=${ctx.head.slice(0,8)}, files=${changedFiles.length}),跳过自动勾选`)
|
|
586
|
+
return false
|
|
587
|
+
}
|
|
588
|
+
console.log(` ✓ 草稿 diff 校验通过(${diffResult.trim().split('\n').length} 个文件有改动)`)
|
|
589
|
+
} catch (e) {
|
|
590
|
+
console.warn(`⚠️ 草稿 diff 校验失败(${e && e.message ? e.message : e}),保守不勾`)
|
|
591
|
+
return false
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
471
595
|
return true
|
|
472
596
|
}
|
|
473
597
|
async function autoCheckPlanFromReviews({ stageName, changeName, cwd, platformOpts }) {
|
|
@@ -491,6 +615,10 @@ async function autoCheckPlanFromReviews({ stageName, changeName, cwd, platformOp
|
|
|
491
615
|
return { autoChecked: false, checkedCount: 0, skippedCount: 0 }
|
|
492
616
|
}
|
|
493
617
|
const executeRunId = c
|
|
618
|
+
|
|
619
|
+
// W2 task-04 FR-03:构造 ctx 供草稿零 diff 守卫用(与 task-review.js generateTaskReviewDrafts 同源)
|
|
620
|
+
const ctx = await buildDraftContext(cwd, changeName)
|
|
621
|
+
|
|
494
622
|
// plan.md 是 agent 与 CLI 都会写的共享文件(agent 勾 checkbox、此处 autoCheck 也勾选)。
|
|
495
623
|
// 读-改-写必须整体持锁(withFileLock 串行化多进程),否则并发 execute --done / 手动勾选互相覆盖
|
|
496
624
|
// (后到者覆盖先到者);写入用 writeAtomicSync(tmp+rename 原子),防 Windows 整文件覆盖被读半截
|
|
@@ -506,7 +634,7 @@ async function autoCheckPlanFromReviews({ stageName, changeName, cwd, platformOp
|
|
|
506
634
|
const r = readReview(reviewPath)
|
|
507
635
|
// 端到端 task:必须 pass(cannot_verify 不算);普通 task:非 fail 即可(坑 execute-batch-complete-endtoend-checkbox)
|
|
508
636
|
const endToEnd = isEndToEndTaskText(match + ' ' + readTaskCardText(changeDir, taskNum))
|
|
509
|
-
if (shouldAutoCheckTask(r, endToEnd)) {
|
|
637
|
+
if (shouldAutoCheckTask(r, endToEnd, ctx)) {
|
|
510
638
|
checkedCount++
|
|
511
639
|
return `${p1}x${p2}` // 勾选
|
|
512
640
|
}
|
|
@@ -530,8 +658,11 @@ async function autoCheckPlanFromReviews({ stageName, changeName, cwd, platformOp
|
|
|
530
658
|
* 复用现成能力:readPlanCheckboxStatus(plan 勾选状态)+ checkExecuteCodeEvidence(代码客观核验,
|
|
531
659
|
* 与 doctor --align-execute-progress 同源,D-002/D-004)。仅在 stageName==='execute' && changeName 触发。
|
|
532
660
|
*
|
|
661
|
+
* W2 task-05 FR-04:plan 全勾后逐 task 复核——review 缺失或草稿零 diff → 阻断批量,
|
|
662
|
+
* 返回 blockedTasks 列表(task id 数组),调用方打印 reason。
|
|
663
|
+
*
|
|
533
664
|
* 安全门:plan 零 checkbox / 未全勾 / 代码零变更(unchanged)均不批量——信任 plan 声明但用代码核验兜底。
|
|
534
|
-
* @returns {Promise<{batched:boolean, aligned:number, reason?:string}>}
|
|
665
|
+
* @returns {Promise<{batched:boolean, aligned:number, reason?:string, blockedTasks?:string[]}>}
|
|
535
666
|
*/
|
|
536
667
|
async function detectExecuteBatchFinish({ pm, stageName, changeName, cwd, specBase, steps }) {
|
|
537
668
|
if (stageName !== 'execute' || !changeName) return { batched: false, aligned: 0 }
|
|
@@ -548,6 +679,78 @@ async function detectExecuteBatchFinish({ pm, stageName, changeName, cwd, specBa
|
|
|
548
679
|
if (evidence.status === 'unchanged') {
|
|
549
680
|
return { batched: false, aligned: 0, reason: `代码零变更(${evidence.detail})` }
|
|
550
681
|
}
|
|
682
|
+
|
|
683
|
+
// W2 task-05 FR-04:逐 task 复核(草稿零 diff 守卫——批量层)
|
|
684
|
+
// 读取 plan.md 提取已勾 task id 列表(正则 `- [x] task-NN`)
|
|
685
|
+
const planPath = join(changeDir, 'plan.md')
|
|
686
|
+
const planContent = readFileSync(planPath, 'utf8')
|
|
687
|
+
const taskCheckboxRegex = /-\s\[x\]\s+task-(\d+)/gi
|
|
688
|
+
const taskMatches = [...planContent.matchAll(taskCheckboxRegex)]
|
|
689
|
+
const taskIds = taskMatches.map(m => `task-${m[1].padStart(2, '0')}`)
|
|
690
|
+
|
|
691
|
+
if (taskIds.length === 0) {
|
|
692
|
+
return { batched: false, aligned: 0, reason: 'plan.md 无已勾 task checkbox' }
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// 构造 ctx 供草稿零 diff 校验用(与 task-04 同源)
|
|
696
|
+
const ctx = await buildDraftContext(cwd, changeName)
|
|
697
|
+
const runtimeRoot = resolveRuntimeRoot({ specRoot: specBase }, specBase)
|
|
698
|
+
const runIdFile = join(runtimeRoot, `current-execute-run-id-${changeName}`)
|
|
699
|
+
const blockedTasks = []
|
|
700
|
+
|
|
701
|
+
for (const taskId of taskIds) {
|
|
702
|
+
let review = null
|
|
703
|
+
try {
|
|
704
|
+
const { readReview, isValidExecuteRunId } = await import('../task-review.js')
|
|
705
|
+
const c = readFileSync(runIdFile, 'utf8').trim()
|
|
706
|
+
if (!c || !isValidExecuteRunId(c)) {
|
|
707
|
+
blockedTasks.push(taskId)
|
|
708
|
+
continue
|
|
709
|
+
}
|
|
710
|
+
const executeRunId = c
|
|
711
|
+
const reviewPath = join(runtimeRoot, 'execute-runs', executeRunId, 'tasks', taskId, 'review.json')
|
|
712
|
+
const r = readReview(reviewPath)
|
|
713
|
+
if (!r?.ok) {
|
|
714
|
+
blockedTasks.push(taskId)
|
|
715
|
+
continue
|
|
716
|
+
}
|
|
717
|
+
review = r.review
|
|
718
|
+
} catch {
|
|
719
|
+
blockedTasks.push(taskId)
|
|
720
|
+
continue
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// review 存在:判定是否为自动草稿且零 diff
|
|
724
|
+
if (review?.reviewerNotes && review.reviewerNotes.includes('auto-generated draft')) {
|
|
725
|
+
const changedFiles = review.changedFiles || []
|
|
726
|
+
// 草稿且 changedFiles 为空 → 阻断
|
|
727
|
+
if (changedFiles.length === 0) {
|
|
728
|
+
blockedTasks.push(taskId)
|
|
729
|
+
continue
|
|
730
|
+
}
|
|
731
|
+
// 草稿且有 changedFiles → 实测 diff 校验(ctx 与 task-04 同源)
|
|
732
|
+
if (ctx && ctx.gitDir && ctx.base && ctx.head) {
|
|
733
|
+
try {
|
|
734
|
+
const diffResult = gitQuiet(ctx.gitDir, ['diff', '--name-only', `${ctx.base}..${ctx.head}`, '--', ...changedFiles], { trim: true })
|
|
735
|
+
if (!diffResult || diffResult.trim() === '') {
|
|
736
|
+
blockedTasks.push(taskId)
|
|
737
|
+
}
|
|
738
|
+
} catch (e) {
|
|
739
|
+
// diff 校验失败保守处理:阻断(避免误批量)
|
|
740
|
+
blockedTasks.push(taskId)
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// blockedTasks 非空 → 阻断批量
|
|
747
|
+
if (blockedTasks.length > 0) {
|
|
748
|
+
const reason = `以下 task 复核未过(review 缺失或草稿零 diff):${blockedTasks.join(', ')}`
|
|
749
|
+
console.warn(`⚠️ 批量完成被阻断:${reason}`)
|
|
750
|
+
return { batched: false, aligned: 0, reason, blockedTasks }
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// 全部复核通过 → 批量标 completed
|
|
551
754
|
const now = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
552
755
|
let aligned = 0
|
|
553
756
|
for (const step of steps) {
|
|
@@ -558,8 +761,9 @@ async function detectExecuteBatchFinish({ pm, stageName, changeName, cwd, specBa
|
|
|
558
761
|
}
|
|
559
762
|
}
|
|
560
763
|
return { batched: true, aligned }
|
|
561
|
-
} catch {
|
|
562
|
-
|
|
764
|
+
} catch (e) {
|
|
765
|
+
// 异常 fail-open:不批量,reason 带错误信息
|
|
766
|
+
return { batched: false, aligned: 0, reason: `批量完成检测异常:${e && e.message ? e.message : e}` }
|
|
563
767
|
}
|
|
564
768
|
}
|
|
565
769
|
|
|
@@ -759,6 +963,8 @@ export async function continueStep(pm, progress, stageName, cwd, answer, options
|
|
|
759
963
|
progress.lastActive = now
|
|
760
964
|
pm._write(cwd, progress, changeName)
|
|
761
965
|
triggerSync(cwd, changeName, platformOpts)
|
|
966
|
+
// wait 解除持久化点(含 repeatable 多轮)同样刷新 change title——多轮确认期间 design.md 已存在
|
|
967
|
+
refreshChangeTitleFromArtifacts(pm, cwd, specBase, changeName)
|
|
762
968
|
|
|
763
969
|
console.log(`✅ Step ${currentIdx + 1}/${stageData.steps.length} 已继续:${currentStep.name}`)
|
|
764
970
|
console.log(` 回答:${answer}`)
|
|
@@ -815,12 +1021,7 @@ export async function continueStep(pm, progress, stageName, cwd, answer, options
|
|
|
815
1021
|
if (_stageGatesResult?.stageCompleted === false) process.exitCode = 1
|
|
816
1022
|
if (_stageGatesResult) return _stageGatesResult
|
|
817
1023
|
// 完整流程 change title 刷新(同 completeStep 完成分支,wait 解除后阶段完成也刷新)。
|
|
818
|
-
|
|
819
|
-
try {
|
|
820
|
-
const refinedTitle = deriveTitleFromLinkedChange(specBase, changeName)
|
|
821
|
-
if (refinedTitle) pm.updateChangeMeta(cwd, changeName, { title: refinedTitle })
|
|
822
|
-
} catch { /* title 刷新失败不阻断完成 */ }
|
|
823
|
-
}
|
|
1024
|
+
refreshChangeTitleFromArtifacts(pm, cwd, specBase, changeName)
|
|
824
1025
|
// gate 全过:persist completed(task-03 移后;此处无 triggerSync)。
|
|
825
1026
|
pm._write(cwd, progress, changeName)
|
|
826
1027
|
console.log(`\n✅ ${stageName} 阶段已完成(${stageData.steps.length}/${stageData.steps.length} 步)`)
|
|
@@ -30,6 +30,28 @@ function normalizeGitPath(p) {
|
|
|
30
30
|
return String(p).replace(/\\/g, '/')
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* 并发预检 ownFiles 锚点(2026-08-18 误归属修复)。
|
|
35
|
+
*
|
|
36
|
+
* 未声明会话(allowedFiles 空)维持旧口径 changedFiles ∪ baselineFiles——无声明即无归属信息,
|
|
37
|
+
* 收窄锚点会把本会话每次未声明的自身改动全误报他者。声明会话锚点 = baselineFiles ∪ allowedFiles
|
|
38
|
+
* (声明即归属):窗口 diff(changedFiles)在多 agent 并发仓库可能含他者污染(ql-20260818-003
|
|
39
|
+
* 实证:并行会话窗口内改的文件被算进本会话 changedFiles 再被 ownFiles 自吞),超出声明集的
|
|
40
|
+
* 窗口新脏文件才作为 foreign 信号报出(= 他者或漏声明,歧义由 advisory 文案兜住)。
|
|
41
|
+
*
|
|
42
|
+
* 纯函数:不查盘不调 git,调用点(complete-handlers task-02 钩子)传入三个列表。
|
|
43
|
+
* @param {{ changedFiles?: string[], baselineFiles?: string[], allowedFiles?: string[] }} opts
|
|
44
|
+
* @returns {string[]} ownFiles 锚点集合(去重)
|
|
45
|
+
*/
|
|
46
|
+
export function resolveConcurrentAnchor({ changedFiles = [], baselineFiles = [], allowedFiles = [] } = {}) {
|
|
47
|
+
const arr = (x) => (Array.isArray(x) ? x : [])
|
|
48
|
+
const declared = arr(allowedFiles).map(normalizeGitPath).filter(Boolean)
|
|
49
|
+
if (declared.length === 0) {
|
|
50
|
+
return [...arr(changedFiles).map(normalizeGitPath), ...arr(baselineFiles).map(normalizeGitPath)]
|
|
51
|
+
}
|
|
52
|
+
return [...new Set([...arr(baselineFiles).map(normalizeGitPath), ...declared])]
|
|
53
|
+
}
|
|
54
|
+
|
|
33
55
|
/**
|
|
34
56
|
* 检测工作树里的并发他者改动(非阻塞 advisory 用)。
|
|
35
57
|
*
|