sillyspec 3.31.0 → 3.32.1
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-auto/SKILL.md +1 -1
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +2 -2
- package/.claude/skills/sillyspec-commit/SKILL.md +1 -1
- package/.claude/skills/sillyspec-flow/SKILL.md +10 -6
- package/.claude/skills/sillyspec-quick/SKILL.md +105 -7
- package/.zcodeignore +66 -0
- package/AGENTS.md +16 -16
- package/assets/command-cards/flow.md +9 -3
- package/assets/command-cards/run-quick.md +4 -1
- package/package.json +2 -2
- package/src/agent-session-log.js +27 -5
- package/src/config-schema.js +21 -0
- package/src/datetime.js +51 -0
- package/src/db.js +10 -3
- package/src/decision-distill.js +21 -12
- package/src/design-facts.js +46 -6
- package/src/flow-draft.js +468 -95
- package/src/flow-parity.js +199 -5
- package/src/flow-review.js +136 -6
- package/src/flow.js +786 -145
- package/src/foreign-declared.js +75 -3
- package/src/fr-index.js +148 -162
- package/src/git-helper.js +16 -3
- package/src/hooks/worktree-guard.js +16 -5
- package/src/hunk-attribution.js +28 -5
- package/src/index.js +186 -19
- package/src/knowledge-classify.js +3 -1
- package/src/knowledge-digest.js +55 -26
- package/src/knowledge-match.js +135 -7
- package/src/knowledge-stats.js +105 -4
- package/src/knowledge-vector.js +160 -0
- package/src/machine-interface.js +90 -2
- package/src/module-impact.js +22 -1
- package/src/modules.js +9 -0
- package/src/progress/change-registry.js +20 -0
- package/src/progress/stage-machine.js +4 -7
- package/src/progress.js +8 -1
- package/src/quicklog.js +20 -0
- package/src/redomain.js +23 -7
- package/src/route-hindsight.js +276 -0
- package/src/run/archive-distill.js +3 -1
- package/src/run/command.js +65 -12
- package/src/run/complete-handlers.js +219 -5
- package/src/run/complete.js +99 -22
- package/src/run/gate-snapshot.js +15 -0
- package/src/run/gates.js +60 -4
- package/src/run/prompt.js +159 -31
- package/src/run/quick-audit.js +66 -7
- package/src/run/shared.js +6 -11
- package/src/run/stage.js +5 -1
- package/src/run/verify-quality-scan.js +84 -0
- package/src/scan-facts.js +2 -1
- package/src/scope-audit.js +138 -5
- package/src/sentinel-assertions.js +67 -16
- package/src/stage-contract-engine.js +4 -1
- package/src/stage-contract-spec.js +103 -4
- package/src/stage-contract.js +55 -7
- package/src/stages/brainstorm.js +27 -14
- package/src/stages/knowledge.js +4 -3
- package/src/stages/plan-postcheck.js +98 -3
- package/src/stages/plan.js +22 -2
- package/src/stages/quick.js +1 -1
- package/src/stages/verify.js +2 -1
- package/src/task-review.js +1 -1
- package/src/task-tick.js +121 -0
- package/src/taskcard-frontmatter.js +31 -0
- package/src/ui-visual.js +43 -8
- package/src/verify-postcheck.js +518 -38
- package/src/verify-probes.js +216 -5
- package/src/watcher.js +1067 -1031
- package/src/worktree-apply.js +9 -1
- package/src/worktree.js +21 -6
- package/templates/agents-instruction.md +15 -15
- package/templates/prompts/taskcard-rules.md +1 -0
package/src/run/command.js
CHANGED
|
@@ -29,7 +29,7 @@ import { resolveQuickLinkedChanges } from './quick-audit.js'
|
|
|
29
29
|
import { outputStep, collectStageWaitHistory } from './prompt.js'
|
|
30
30
|
import { completeStep, completeStepBurst, skipStep, waitStep, continueStep, synthesizeStepOutput } from './complete.js'
|
|
31
31
|
import { runStage } from './stage.js'
|
|
32
|
-
import { sanitizeDesc } from '../quicklog.js'
|
|
32
|
+
import { sanitizeDesc, deriveChangeTitle } from '../quicklog.js'
|
|
33
33
|
import { ProgressManager, resolveSessionIdentity } from '../progress.js'
|
|
34
34
|
import { validateChangeExists, checkTransition } from '../stage-contract.js'
|
|
35
35
|
import { READONLY_AUXILIARY_STAGES } from '../constants.js'
|
|
@@ -215,6 +215,22 @@ export async function ensureStageSteps(progress, stageName, cwd, specDir = null)
|
|
|
215
215
|
* run <stage> --help/-h 的用法帮助(runCommand 内 --help 短路调用)。
|
|
216
216
|
* 只打印不落盘:帮助查询不该有副作用(建会话/写 QUICKLOG)。
|
|
217
217
|
*/
|
|
218
|
+
/**
|
|
219
|
+
* 只读空态引导文案(2026-10-05-status-empty-guide 首创;2026-10-06-status-multi-active-list
|
|
220
|
+
* 抽出单一源——库不在场前置门与 read 后空分支共用一份,防两处文案漂移)。静态文案——引用
|
|
221
|
+
* 通用命令形态而非本仓状态(空态定义即无数据可读)。可照抄实例(2026-10-05-input-teach-copyable):
|
|
222
|
+
* 引号内换行合法,extractSuccessCriteria 行级 trim 容忍缩进,带缩进照抄亦过门。
|
|
223
|
+
*/
|
|
224
|
+
function emitReadonlyEmptyGuide() {
|
|
225
|
+
console.log('ℹ️ 未找到进度数据(只读查询不建变更)')
|
|
226
|
+
console.log(' → 可开始轻量变更(--input 引号内换行合法,下例可照抄):')
|
|
227
|
+
console.log(' sillyspec flow start --change <YYYY-MM-DD-名> --input "<动机与背景>')
|
|
228
|
+
console.log('')
|
|
229
|
+
console.log(' 成功标准:')
|
|
230
|
+
console.log(' - <可验证标准>"')
|
|
231
|
+
console.log(' → 需求不明可 sillyspec run brainstorm --change <名> 先探索')
|
|
232
|
+
}
|
|
233
|
+
|
|
218
234
|
function printStageUsage(stageName) {
|
|
219
235
|
const stage = stageRegistry[stageName]
|
|
220
236
|
console.log(`
|
|
@@ -833,7 +849,11 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
833
849
|
quickSessionId = recent
|
|
834
850
|
quickFallbackUsed = true
|
|
835
851
|
} else {
|
|
836
|
-
console.log(' 本仓无近期 quick 会话记录(current marker 与 quick-sessions
|
|
852
|
+
console.log(' 本仓无近期 quick 会话记录(current marker 与 quick-sessions 均空)——新工作请走轻量变更(--input 引号内换行合法,可照抄):')
|
|
853
|
+
console.log(' sillyspec flow start --change <YYYY-MM-DD-名> --input "<动机与背景>')
|
|
854
|
+
console.log('')
|
|
855
|
+
console.log(' 成功标准:')
|
|
856
|
+
console.log(' - <可验证标准>"')
|
|
837
857
|
process.exit(0)
|
|
838
858
|
}
|
|
839
859
|
} else {
|
|
@@ -1111,9 +1131,23 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
1111
1131
|
}
|
|
1112
1132
|
}
|
|
1113
1133
|
|
|
1134
|
+
// 只读辅助阶段的写意图 flag 清单(下方两处只读短路共用:库不在场前置门 + read 后短路)
|
|
1135
|
+
const READONLY_WRITE_ACTIONS = ['--cleanup-remnant', '--align-execute-progress', '--done', '--skip', '--reset', '--reopen', '--wait', '--continue']
|
|
1136
|
+
|
|
1137
|
+
// 只读前置门(2026-10-06-status-multi-active-list):库文件不在场时不得进入 pm.read()——
|
|
1138
|
+
// read() 无显式 --change 先走 listChanges→_ensureDB,会凭空新建空库(只读承诺破防,实测);
|
|
1139
|
+
// 空库撞残留 schema-version 戳无表可查,首个 SELECT 直接崩(no such table: changes)。
|
|
1140
|
+
// 库不在场=字面意义的零进度:与零活跃同款空态引导 + exit 0,零落盘。
|
|
1141
|
+
if (READONLY_AUXILIARY_STAGES.includes(stageName) && !flags.some(f => READONLY_WRITE_ACTIONS.includes(f))
|
|
1142
|
+
&& !existsSync(pm._runtimePath(cwd, 'sillyspec.db'))) {
|
|
1143
|
+
emitReadonlyEmptyGuide()
|
|
1144
|
+
process.exit(0)
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1114
1147
|
let progress = pm.read(cwd, changeName)
|
|
1115
1148
|
|
|
1116
|
-
// task-03 (D-005@v2 / FR-04): READONLY_AUXILIARY_STAGES(status
|
|
1149
|
+
// task-03 (D-005@v2 / FR-04): READONLY_AUXILIARY_STAGES(status;doctor 已于 2026-09-09-doctor-noai
|
|
1150
|
+
// 移出——阶段形态走状态机,顶层 doctor 只读语义由 index.js case 头部分流保真)查询只读短路。
|
|
1117
1151
|
// 无显式写 flag 时在 registerChange / ensureStageSteps 之前拦截,零副作用:
|
|
1118
1152
|
// - 目标 progress 不存在 → 提示「只读查询不建变更」exit 0(不 initChange、不建 default 行,治 8b)
|
|
1119
1153
|
// - 目标 progress 存在 → 只读展示,不 seed steps、不刷新 lastActive、不 registerChange
|
|
@@ -1121,10 +1155,25 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
1121
1155
|
// 显式写意图不短路:doctor 写操作 flag(--cleanup-remnant / --align-execute-progress)与
|
|
1122
1156
|
// 步骤动作 flag(--done/--skip/--reset/--reopen/--wait/--continue)走原写路径;--status 本身
|
|
1123
1157
|
// 是只读查询,纳入短路。--fix 是 worktree doctor 的 flag 不在此列(design.md Phase 4)。
|
|
1124
|
-
const READONLY_WRITE_ACTIONS = ['--cleanup-remnant', '--align-execute-progress', '--done', '--skip', '--reset', '--reopen', '--wait', '--continue']
|
|
1125
1158
|
if (READONLY_AUXILIARY_STAGES.includes(stageName) && !flags.some(f => READONLY_WRITE_ACTIONS.includes(f))) {
|
|
1126
1159
|
if (!progress) {
|
|
1127
|
-
|
|
1160
|
+
// 多活跃分流(2026-10-06-status-multi-active-list):read() 无显式 --change 在 ≥2 活跃
|
|
1161
|
+
// 时同样返回 null——「有数据但需要点名」被空态文案一律误报成「无数据」(本仓实测
|
|
1162
|
+
// 10 行 active、2 个目录待收口却被告知未找到)。前置门保证走到这库必在场,listChanges
|
|
1163
|
+
// 纯读;显式 --change 未命中仍走空态引导(点名失败与未点名歧义是两件事)。
|
|
1164
|
+
let activeChanges = []
|
|
1165
|
+
try { activeChanges = pm.listChanges(cwd) || [] } catch { activeChanges = [] }
|
|
1166
|
+
if (!changeName && activeChanges.length >= 2) {
|
|
1167
|
+
console.log(`ℹ️ 进度数据存在,但有 ${activeChanges.length} 个活跃变更(未指定 --change,无法确定展示哪个):`)
|
|
1168
|
+
for (const name of activeChanges) {
|
|
1169
|
+
// DB 行与目录是两个真相层:目录缺失的幽灵行如实标注(清幽灵走既有 doctor --cleanup-remnant)
|
|
1170
|
+
console.log(` - ${name}${existsSync(pm._changePath(cwd, name)) ? '' : '(本地无目录)'}`)
|
|
1171
|
+
}
|
|
1172
|
+
console.log(` → 指定查看其一:sillyspec status --change ${activeChanges[0]}`)
|
|
1173
|
+
console.log(' → 恢复面:sillyspec flow status --change <名>')
|
|
1174
|
+
process.exit(0)
|
|
1175
|
+
}
|
|
1176
|
+
emitReadonlyEmptyGuide()
|
|
1128
1177
|
process.exit(0)
|
|
1129
1178
|
}
|
|
1130
1179
|
// 只读展示路径:复用阶段定义渲染当前步骤 prompt(outputStep 对 status/doctor 纯只读),
|
|
@@ -1225,10 +1274,11 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
1225
1274
|
// 如果指定了变更名或有变更目录,自动初始化变更的 progress
|
|
1226
1275
|
const autoChange = changeName || resolveChangeNameAuto(cwd, specRoot)
|
|
1227
1276
|
if (autoChange) {
|
|
1228
|
-
// 创建时即写 title(与 name/KEY 同时落 db):--input
|
|
1277
|
+
// 创建时即写 title(与 name/KEY 同时落 db):--input 首行推导 deriveChangeTitle(≤50 字,
|
|
1278
|
+
// 用户需求 2026-09-29)优先,长输入兜底 sanitizeDesc 压行,无则用 name 兜底。
|
|
1229
1279
|
// proposal 还没写、无权威标题来源,这是临时值;brainstorm/plan 完成 proposal/design 落盘后由
|
|
1230
1280
|
// complete.js 通用完成路径 deriveTitleFromLinkedChange 刷新为真实 # 标题(与 quick 启动 title 同源)。
|
|
1231
|
-
progress = pm.initChange(cwd, autoChange, { title: inputText ? sanitizeDesc(inputText) : autoChange })
|
|
1281
|
+
progress = pm.initChange(cwd, autoChange, { title: inputText ? (deriveChangeTitle(inputText) || sanitizeDesc(inputText)) : autoChange })
|
|
1232
1282
|
} else if (isAuxiliary) {
|
|
1233
1283
|
let autoName = changeName || resolveChangeNameAuto(cwd, specRoot) || 'default'
|
|
1234
1284
|
// archive 特例:归档后变更从活跃列表排除(listChanges WHERE status='active'),
|
|
@@ -1255,7 +1305,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
1255
1305
|
changeName = autoName
|
|
1256
1306
|
if (!progress) {
|
|
1257
1307
|
// auxiliary 创建也写 title(--input 优先 / name 兜底),同上完整流程语义。
|
|
1258
|
-
progress = pm.initChange(cwd, autoName, { title: inputText ? sanitizeDesc(inputText) : autoName })
|
|
1308
|
+
progress = pm.initChange(cwd, autoName, { title: inputText ? (deriveChangeTitle(inputText) || sanitizeDesc(inputText)) : autoName })
|
|
1259
1309
|
// initChange 可能因 project 表为空返回 null
|
|
1260
1310
|
if (!progress) {
|
|
1261
1311
|
progress = { currentStage: stageName, stages: {}, lastActive: new Date().toLocaleString('zh-CN', { hour12: false }), project: '' }
|
|
@@ -1287,7 +1337,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
1287
1337
|
console.log(` 提示:可以用 --change <名称> 指定自定义变更名`)
|
|
1288
1338
|
console.log(` 或事后重命名:sillyspec change-rename ${autoName} <新名称>`)
|
|
1289
1339
|
// brainstorm 自动创建变更也写 title(--input 需求描述优先 / 自动名兜底),proposal 落盘后刷新。
|
|
1290
|
-
progress = pm.initChange(cwd, autoName, { title: inputText ? sanitizeDesc(inputText) : autoName })
|
|
1340
|
+
progress = pm.initChange(cwd, autoName, { title: inputText ? (deriveChangeTitle(inputText) || sanitizeDesc(inputText)) : autoName })
|
|
1291
1341
|
changeName = autoName
|
|
1292
1342
|
} else if (stageName === 'auto') {
|
|
1293
1343
|
// auto 零活跃建变更(2026-09-08-auto-driver D-003@v1,Grill P1-②):SKILL 启动命令即
|
|
@@ -1303,7 +1353,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
1303
1353
|
const date = new Date().toISOString().slice(0, 10)
|
|
1304
1354
|
const autoName = `${date}-new-change-${randomBytes(4).toString('hex')}`
|
|
1305
1355
|
console.log(`🔄 auto 模式自动创建变更:${autoName}(可用 --change 指定名称,或事后 change-rename)`)
|
|
1306
|
-
progress = pm.initChange(cwd, autoName, { title: inputText ? sanitizeDesc(inputText) : autoName })
|
|
1356
|
+
progress = pm.initChange(cwd, autoName, { title: inputText ? (deriveChangeTitle(inputText) || sanitizeDesc(inputText)) : autoName })
|
|
1307
1357
|
changeName = autoName
|
|
1308
1358
|
} else {
|
|
1309
1359
|
console.error('❌ 未找到进度数据,请先运行 sillyspec init 或指定 --change <变更名>')
|
|
@@ -2010,8 +2060,11 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
|
|
|
2010
2060
|
const classification = classifyChange({ description: inputText || '', explicitMode, localConfig })
|
|
2011
2061
|
if (classification.mode === 'quick') {
|
|
2012
2062
|
console.log(`📊 auto 模式分类:${classification.reason} 判为小变更`)
|
|
2013
|
-
console.log(` 小变更请走轻量变更(quick 通道已退役,v3.
|
|
2014
|
-
console.log(
|
|
2063
|
+
console.log(` 小变更请走轻量变更(quick 通道已退役,v3.30.0 起拒绝新会话;--input 引号内换行合法,可照抄):`)
|
|
2064
|
+
console.log(' sillyspec flow start --change <YYYY-MM-DD-名> --input "<动机与背景>')
|
|
2065
|
+
console.log('')
|
|
2066
|
+
console.log(' 成功标准:')
|
|
2067
|
+
console.log(' - <可验证标准>"')
|
|
2015
2068
|
return
|
|
2016
2069
|
}
|
|
2017
2070
|
console.log(`📊 auto 模式分类:${classification.mode}(${classification.reason})`)
|
|
@@ -410,6 +410,60 @@ export function extractPendingDocSyncRows(content) {
|
|
|
410
410
|
return rows
|
|
411
411
|
}
|
|
412
412
|
|
|
413
|
+
/**
|
|
414
|
+
* module-impact.md pending 死信一键回填(2026-10-07-flow-friction-batch3 task-04):verify 硬拦
|
|
415
|
+
* 「更新结果」表 pending/待办行后,agent 实测写一次性 node 脚本批量回填——CLI 化。语义对齐
|
|
416
|
+
* 文件内规则行「确定不同步的行改 skipped 并在操作列写明原因」:状态列 pending/待办→skipped,
|
|
417
|
+
* --reason 追加进操作列(`——skipped:<reason>`);done/其余状态行与表外内容逐字不动;幂等
|
|
418
|
+
* (无 pending 行时零写盘)。与 extractPendingDocSyncRows 同一节段解析口径(不二算规则集)。
|
|
419
|
+
* @param {string} mdPath module-impact.md 路径
|
|
420
|
+
* @param {{ reason?: string }} opts reason 追加进操作列的一句话原因(缺省只翻状态)
|
|
421
|
+
* @returns {{ filled: number, ok: boolean, error?: string }} filled=回填行数;ok=false 时 error 带原因
|
|
422
|
+
*/
|
|
423
|
+
export function fillModuleImpactSkipped(mdPath, { reason = '' } = {}) {
|
|
424
|
+
let content
|
|
425
|
+
try { content = readFileSync(mdPath, 'utf8') } catch (e) {
|
|
426
|
+
return { filled: 0, ok: false, error: `module-impact.md 不可读:${mdPath}(${e && e.message ? e.message : e})` }
|
|
427
|
+
}
|
|
428
|
+
const normalized = content.replace(/\r\n/g, '\n')
|
|
429
|
+
const sectionStart = normalized.search(/^#{2,3}\s*更新结果\s*$/m)
|
|
430
|
+
if (sectionStart === -1) return { filled: 0, ok: false, error: 'module-impact.md 无「更新结果」小节——无可回填行(先核对文件是否为骨架未填形态)' }
|
|
431
|
+
const titleMatch = normalized.slice(sectionStart).match(/^#{2,3}\s*更新结果\s*$/m)
|
|
432
|
+
const bodyStart = sectionStart + titleMatch[0].length
|
|
433
|
+
const afterTitle = normalized.slice(bodyStart)
|
|
434
|
+
const nextSection = afterTitle.search(/^#{1,3}\s/m)
|
|
435
|
+
const section = nextSection === -1 ? afterTitle : afterTitle.slice(0, nextSection)
|
|
436
|
+
const sectionEnd = nextSection === -1 ? normalized.length : bodyStart + nextSection
|
|
437
|
+
const PENDING_STATUSES = new Set(['pending', '待办', '未同步', 'not-done', 'todo'])
|
|
438
|
+
let filled = 0
|
|
439
|
+
const newSection = section.split('\n').map((line) => {
|
|
440
|
+
const trimmed = line.trim()
|
|
441
|
+
if (!trimmed.startsWith('|')) return line
|
|
442
|
+
const cells = trimmed.split('|').map(c => c.trim())
|
|
443
|
+
const lastCell = cells[cells.length - 1]
|
|
444
|
+
if (lastCell === '' && cells.length >= 2) cells.pop()
|
|
445
|
+
const statusCell = cells[cells.length - 1]
|
|
446
|
+
if (!statusCell || !PENDING_STATUSES.has(statusCell.toLowerCase())) return line
|
|
447
|
+
filled++
|
|
448
|
+
// 结构 | 目标 | 操作 | 状态 | → cells ['', 目标, 操作, 状态];操作列 = 倒数第二格
|
|
449
|
+
if (cells.length >= 3 && reason) {
|
|
450
|
+
cells[cells.length - 2] = `${cells[cells.length - 2]}——skipped:${reason}`
|
|
451
|
+
}
|
|
452
|
+
cells[cells.length - 1] = 'skipped'
|
|
453
|
+
// 重拼保留「| a | b | c |」空格形态(cells[0] 是行首 | 产生的空串);行首缩进对齐原行
|
|
454
|
+
const indent = line.slice(0, line.indexOf('|'))
|
|
455
|
+
return `${indent}| ${cells.slice(1).join(' | ')} |`
|
|
456
|
+
}).join('\n')
|
|
457
|
+
if (filled === 0) return { filled: 0, ok: true }
|
|
458
|
+
const next = normalized.slice(0, bodyStart) + newSection + normalized.slice(sectionEnd)
|
|
459
|
+
try {
|
|
460
|
+
writeFileSync(mdPath, next, 'utf8')
|
|
461
|
+
return { filled, ok: true }
|
|
462
|
+
} catch (e) {
|
|
463
|
+
return { filled: 0, ok: false, error: `回填写盘失败:${e && e.message ? e.message : e}` }
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
413
467
|
/**
|
|
414
468
|
* 提取 module-impact.md「更新结果」表中的已完成(done)行声明的目标文档路径(债单 D-4 窄口径)。
|
|
415
469
|
*
|
|
@@ -508,6 +562,47 @@ export function resolveArchiveDocAddPaths(cwd, specBase, tokens) {
|
|
|
508
562
|
return [...out]
|
|
509
563
|
}
|
|
510
564
|
|
|
565
|
+
/**
|
|
566
|
+
* 归档源侧移动补暂存(坑 archive-stage-claim,2026-10-06-module-map-list-leak 收口实测发现)。
|
|
567
|
+
*
|
|
568
|
+
* safeGit 契约是返回 {value, error} 不抛错——此前调用方忽略返回值无条件打成功提示,add 失败
|
|
569
|
+
* (index.lock 竞态等)时「已补暂存 N 项」假成功、暂存面实际缺项(实测声称 1 项暂存面为空,
|
|
570
|
+
* 靠核对暂存面纪律兜住后手工补提交)。本助手逐批 add 并校验 error,失败明细返回给调用方
|
|
571
|
+
* 择路打印(fail-soft:不抛错不阻断归档链,幂等 add 重跑同值)。
|
|
572
|
+
*
|
|
573
|
+
* @param {{ cwd: string, paths: string[] }} opts paths 已按本变更目录名过滤的精确 pathspec
|
|
574
|
+
* @returns {{ ok: boolean, count: number, failures: Array<{ path: string, error: string }> }}
|
|
575
|
+
*/
|
|
576
|
+
export function stageArchiveSourceSideMoves({ cwd, paths }) {
|
|
577
|
+
const failures = []
|
|
578
|
+
let count = 0
|
|
579
|
+
for (const batch of chunkPaths(paths)) {
|
|
580
|
+
const r = safeGit(cwd, ['add', '--', ...batch])
|
|
581
|
+
if (r && r.error) {
|
|
582
|
+
for (const p of batch) failures.push({ path: p, error: r.error })
|
|
583
|
+
} else {
|
|
584
|
+
count += batch.length
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
return { ok: failures.length === 0, count, failures }
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* 补暂存结果文案(成功=既有提示逐字不变;失败=⚠️ 告警含失败路径与 error 首行 + 手工兜底指引;
|
|
592
|
+
* 空清单零文案)。独立导出供测试断言两半(返回值与文案),打印决策归调用方。
|
|
593
|
+
* @param {{ ok: boolean, count: number, failures: Array<{ path: string, error: string }> }} r
|
|
594
|
+
* @returns {string}
|
|
595
|
+
*/
|
|
596
|
+
export function renderStageSourceSideMovesMessage(r) {
|
|
597
|
+
if (!r || r.count === 0 && (!r.failures || r.failures.length === 0)) return ''
|
|
598
|
+
if (r.ok) return `🧾 已补暂存本变更归档的源侧移动(${r.count} 项,归档成单次原子提交)`
|
|
599
|
+
const lines = [`⚠️ 补暂存本变更归档的源侧移动失败(${r.failures.length} 个路径,成功 ${r.count} 项)——暂存面可能缺项,归档提交前请核对 git diff --cached --name-only:`]
|
|
600
|
+
for (const f of r.failures.slice(0, 10)) lines.push(` - ${f.path}(${f.error})`)
|
|
601
|
+
if (r.failures.length > 10) lines.push(` … 共 ${r.failures.length} 个`)
|
|
602
|
+
lines.push(` 手工兜底:git add -- <上述路径> 后重跑收口命令(归档子步幂等续)`)
|
|
603
|
+
return lines.join('\n')
|
|
604
|
+
}
|
|
605
|
+
|
|
511
606
|
/**
|
|
512
607
|
* 归档收尾的窄化 git add(ql-20260915-001 修复④,坑 archive-git-add-sweeps-parallel-docs)。
|
|
513
608
|
*
|
|
@@ -731,6 +826,60 @@ export async function bakeArchiveTimeline({ cwd, specBase, changeName, destDir,
|
|
|
731
826
|
*
|
|
732
827
|
* @returns {Promise<void>} 失败面沿链内既有 process.exit 语义(纯搬运不改)。
|
|
733
828
|
*/
|
|
829
|
+
/**
|
|
830
|
+
* 归档提交 pathspec 构成(2026-10-06-archive-cmd-race-and-brief:共享暂存区竞态加固)。
|
|
831
|
+
*
|
|
832
|
+
* 背景:旧实现照抄 `git diff --cached --name-status` 单元格——多 agent 共享仓下,兄弟会话/
|
|
833
|
+
* 后台同步可在「打印→执行」窗口瞬改暂存区(实测 flow-status-json 收尾:建议命令含转瞬即逝
|
|
834
|
+
* 的源侧 A 条目,照抄执行报 pathspec did not match;单会话镜像复盘命令干净=竞态即现即逝)。
|
|
835
|
+
*
|
|
836
|
+
* 构成按稳定性分级:
|
|
837
|
+
* - src 侧(changes/<名>/ 非归档):仅取 HEAD 树在册路径——HEAD 不可变,不受暂存区竞态
|
|
838
|
+
* 影响;rename src / 删除 D 恒在册,转瞬即逝的源侧 A 条目恒不在册(天然滤幽灵)。
|
|
839
|
+
* - dst 侧:本变更专属归档目录单条 pathspec(archive/<名>/ 与窄化 add 同一专属前提),
|
|
840
|
+
* 目录在场即匹配,条目级抖动无感。
|
|
841
|
+
* - knowledge/docs 共享面:HEAD 在册(M 形态)或工作区在场(蒸馏新产 A 形态)才入列。
|
|
842
|
+
* - 其余丢弃并回报(dropped),由调用方显式提示。
|
|
843
|
+
*
|
|
844
|
+
* 纯函数(git 读数由调用方喂入),单测直接钉构成口径。
|
|
845
|
+
* @param {{changeName:string, stagedNameStatus:string, headTreeFiles:string, existsFn?:(p:string)=>boolean}} p
|
|
846
|
+
* @returns {{pathspecs:string[], dropped:string[]}}
|
|
847
|
+
*/
|
|
848
|
+
export function resolveArchiveCommitPathspecs({ changeName, stagedNameStatus, headTreeFiles, existsFn }) {
|
|
849
|
+
const changesPrefix = '.sillyspec/changes/'
|
|
850
|
+
// 与旧实现同源的单元格收集(--name-status:R 行 src<TAB>dst 两格,A/M 单格)
|
|
851
|
+
const mineCells = new Set()
|
|
852
|
+
for (const line of String(stagedNameStatus || '').split('\n')) {
|
|
853
|
+
if (!line.trim()) continue
|
|
854
|
+
for (const cell of line.split('\t').slice(1)) {
|
|
855
|
+
const p = cell.trim().replace(/^"|"$/g, '')
|
|
856
|
+
if (!p) continue
|
|
857
|
+
if (p.startsWith(changesPrefix + changeName + '/')
|
|
858
|
+
|| p.startsWith(changesPrefix + 'archive/' + changeName + '/')
|
|
859
|
+
|| p.startsWith('.sillyspec/knowledge/')
|
|
860
|
+
|| p.startsWith('.sillyspec/docs/')) mineCells.add(p)
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
const headSet = new Set(String(headTreeFiles || '').split('\n').map((s) => s.trim()).filter(Boolean))
|
|
864
|
+
const srcPaths = []
|
|
865
|
+
let archiveHit = false
|
|
866
|
+
const sharedPaths = []
|
|
867
|
+
const dropped = []
|
|
868
|
+
for (const p of mineCells) {
|
|
869
|
+
if (p.startsWith(changesPrefix + 'archive/' + changeName + '/')) {
|
|
870
|
+
archiveHit = true
|
|
871
|
+
} else if (p.startsWith(changesPrefix + changeName + '/')) {
|
|
872
|
+
if (headSet.has(p)) srcPaths.push(p)
|
|
873
|
+
else dropped.push(p)
|
|
874
|
+
} else {
|
|
875
|
+
if (headSet.has(p) || (typeof existsFn === 'function' && existsFn(p))) sharedPaths.push(p)
|
|
876
|
+
else dropped.push(p)
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
const pathspecs = [...(archiveHit ? [`${changesPrefix}archive/${changeName}/`] : []), ...srcPaths, ...sharedPaths]
|
|
880
|
+
return { pathspecs, dropped }
|
|
881
|
+
}
|
|
882
|
+
|
|
734
883
|
export async function runArchiveChain({ pm, cwd, specBase, changeName, srcDir, destDir, skipApply = false, skipPlanCheck = false, deliverableFiles = null, docsIncrement = null } = {}) {
|
|
735
884
|
const archiveDir = dirname(destDir) // 抽取补:原闭包变量(changes/archive),从 destDir 推导等价
|
|
736
885
|
// ── 模块文档认领 advisory(资产三小件③,2026-09-23)──
|
|
@@ -858,9 +1007,16 @@ export async function runArchiveChain({ pm, cwd, specBase, changeName, srcDir, d
|
|
|
858
1007
|
// pathspec 把并行会话同目录未提交文件夹带进共享暂存区):改为窄化——changes 侧只 add 本变更
|
|
859
1008
|
// 归档目录 archive/<destName>/,docs 侧按 module-impact「更新结果」done 行精确文件集(提取
|
|
860
1009
|
// 失败回退目录级 + 前置 warning,见 archiveNarrowedGitAdd)。
|
|
1010
|
+
// destName 推导(2026-10-05-flow-tail-polish 根因修复):本链签名无 destName——此前下方
|
|
1011
|
+
// 调用引用未声明标识符抛 ReferenceError 被空 catch 静默吞,archiveNarrowedGitAdd 在 flow
|
|
1012
|
+
// done 路径从未生效(归档目录/knowledge/docs 自动暂存全失效,靠 agent 手工兜底——实测
|
|
1013
|
+
// porcelain 实证)。basename(destDir) 与调用方 destDir 恒一致(flow.js 与 run archive 同构)。
|
|
1014
|
+
const destName = basename(destDir)
|
|
861
1015
|
try {
|
|
862
1016
|
archiveNarrowedGitAdd({ cwd, specBase, destDir, destName })
|
|
863
|
-
} catch {
|
|
1017
|
+
} catch (e) {
|
|
1018
|
+
console.warn(`⚠️ 归档窄化 git add 异常(不阻断归档,交付面靠下方探测块兜底/agent git status 核对): ${(e && e.message) || e}`)
|
|
1019
|
+
}
|
|
864
1020
|
|
|
865
1021
|
// ── 他者半归档残留探测(坑 archive-other-residual-rename,2026-08-21 实证)──
|
|
866
1022
|
// 并行变更的手动归档把 R 残留(源目录 rename 目标行)留在暂存区;本变更归档提交时
|
|
@@ -875,6 +1031,10 @@ export async function runArchiveChain({ pm, cwd, specBase, changeName, srcDir, d
|
|
|
875
1031
|
const changesPrefix = '.sillyspec/changes/'
|
|
876
1032
|
const minePaths = []
|
|
877
1033
|
const othersResidual = []
|
|
1034
|
+
// 兜底信号(2026-10-05-flow-tail-polish):正常路径 archiveNarrowedGitAdd 已暂存归档目录
|
|
1035
|
+
// 与 knowledge——这两个信号只在窄化 add 失败降级时非空/置位
|
|
1036
|
+
let untrackedArchiveHit = false
|
|
1037
|
+
const knowledgePending = []
|
|
878
1038
|
for (const line of raw.split('\n')) {
|
|
879
1039
|
if (!line || line.length < 4) continue
|
|
880
1040
|
const x = line[0], y = line[1]
|
|
@@ -894,18 +1054,72 @@ export async function runArchiveChain({ pm, cwd, specBase, changeName, srcDir, d
|
|
|
894
1054
|
|| p.startsWith(changesPrefix + 'archive/' + changeName + '/'))) {
|
|
895
1055
|
minePaths.push(p)
|
|
896
1056
|
}
|
|
1057
|
+
} else if (x === '?' && y === '?') {
|
|
1058
|
+
// 未跟踪兜底:归档新目录(目录聚合/逐文件两形态都收,archive/<me>/ 专属本变更零共享)
|
|
1059
|
+
if (dst === `${changesPrefix}archive/${changeName}/` || dst.startsWith(`${changesPrefix}archive/${changeName}/`)) {
|
|
1060
|
+
untrackedArchiveHit = true
|
|
1061
|
+
} else if (dst.startsWith('.sillyspec/knowledge/')) {
|
|
1062
|
+
knowledgePending.push(dst)
|
|
1063
|
+
}
|
|
1064
|
+
} else if (x === ' ' && y === 'M' && dst.startsWith('.sillyspec/knowledge/')) {
|
|
1065
|
+
// knowledge 未暂存修改(同兜底语义)
|
|
1066
|
+
knowledgePending.push(dst)
|
|
897
1067
|
}
|
|
898
1068
|
}
|
|
899
1069
|
if (minePaths.length > 0) {
|
|
900
1070
|
// ql-20260915-001 修复④:补暂存源侧移动改精确 pathspec(minePaths 已按本变更目录名
|
|
901
1071
|
// 过滤,chunkPaths 分批防 Windows argv 上限)——原 add -A -- .sillyspec/changes/ 目录级
|
|
902
1072
|
// 会顺带扫入并行会话在 changes/ 下的未提交文件(坑 archive-git-add-sweeps-parallel-docs
|
|
903
|
-
// 同坑不同点;git add -- <已删路径> 即暂存删除,语义等价 -A
|
|
904
|
-
|
|
905
|
-
|
|
1073
|
+
// 同坑不同点;git add -- <已删路径> 即暂存删除,语义等价 -A 限本变更面)。
|
|
1074
|
+
// 坑 archive-stage-claim(2026-10-06-module-map-list-leak 收口实测):safeGit 不抛错返回
|
|
1075
|
+
// {value,error}——此前忽略返回值无条件打成功提示,add 失败(index.lock 竞态等)时假成功。
|
|
1076
|
+
const staged = stageArchiveSourceSideMoves({ cwd, paths: minePaths })
|
|
1077
|
+
const msg = renderStageSourceSideMovesMessage(staged)
|
|
1078
|
+
if (msg) (staged.ok ? console.log : console.warn)(msg)
|
|
1079
|
+
}
|
|
1080
|
+
if (untrackedArchiveHit) {
|
|
1081
|
+
// 文件级枚举补暂存(守 AGENTS.md 规则 11 不用目录级 add;实际仅窄化 add 降级时可达)
|
|
1082
|
+
try {
|
|
1083
|
+
const archFiles = []
|
|
1084
|
+
const walk = (d) => { for (const e of readdirSync(d, { withFileTypes: true })) { const p = join(d, e.name); if (e.isDirectory()) walk(p); else archFiles.push(p) } }
|
|
1085
|
+
walk(destDir)
|
|
1086
|
+
const rel = archFiles.map((p) => relative(cwd, p).replace(/\\/g, '/'))
|
|
1087
|
+
for (const batch of chunkPaths(rel)) safeGit(cwd, ['add', '--', ...batch])
|
|
1088
|
+
console.log(`🧾 已补暂存未跟踪的归档目录文件(${rel.length} 项,archive/${changeName}/ 专属本变更——窄化 add 降级兜底)`)
|
|
1089
|
+
} catch (e) {
|
|
1090
|
+
console.warn(`⚠️ 归档目录兜底补暂存失败(请手工 git add -- .sillyspec/changes/archive/${changeName}/ 下文件): ${(e && e.message) || e}`)
|
|
906
1091
|
}
|
|
907
|
-
console.log(`🧾 已补暂存本变更归档的源侧移动(${minePaths.length} 项,归档成单次原子提交)`)
|
|
908
1092
|
}
|
|
1093
|
+
if (knowledgePending.length > 0) {
|
|
1094
|
+
console.log(`📎 归档提交待办(knowledge 蒸馏产物未暂存——核对归属后随归档提交一并带上):`)
|
|
1095
|
+
console.log(` git add -- ${knowledgePending.join(' ')}`)
|
|
1096
|
+
}
|
|
1097
|
+
// 归档提交一笔到位提示(2026-10-06-litest-p1-fixes 实测 b112d738/2c8fd25c:git commit
|
|
1098
|
+
// -- <pathspec> 是部分提交——rename 条目 pathspec 只匹配 archive/ 侧时提交树里源文件
|
|
1099
|
+
// 不会被删(D 留暂存区,源侧得再来一笔)。补暂存只保证暂存面完整,提交 pathspec 仍由
|
|
1100
|
+
// agent 掌握)。聚合本变更归档面全量 staged 路径打印可直接执行的一笔到位 commit 命令。
|
|
1101
|
+
// 2026-10-06-archive-cmd-race-and-brief 竞态加固:真仓实测(flow-status-json 收尾)照抄执行报
|
|
1102
|
+
// pathspec did not match——共享仓多会话/后台同步可在「打印→执行」窗口瞬改暂存区,单会话镜像
|
|
1103
|
+
// 复盘命令干净(竞态即现即逝)。照抄暂存面不可靠,pathspec 构成按稳定性分级(见
|
|
1104
|
+
// resolveArchiveCommitPathspecs JSDoc);无 staged 面不打印(幂等重入)。
|
|
1105
|
+
try {
|
|
1106
|
+
const stagedRaw = gitQuiet(cwd, ['diff', '--cached', '--name-status'])
|
|
1107
|
+
const headTreeRaw = gitQuiet(cwd, ['ls-tree', '-r', '--name-only', 'HEAD'])
|
|
1108
|
+
const { pathspecs, dropped } = resolveArchiveCommitPathspecs({
|
|
1109
|
+
changeName,
|
|
1110
|
+
stagedNameStatus: String(stagedRaw || ''),
|
|
1111
|
+
headTreeFiles: String(headTreeRaw || ''),
|
|
1112
|
+
existsFn: (p) => { try { return existsSync(join(cwd, p)) } catch { return false } },
|
|
1113
|
+
})
|
|
1114
|
+
if (pathspecs.length > 0) {
|
|
1115
|
+
console.log(`🧾 归档提交一笔到位(src 锚 HEAD 树/归档侧目录 pathspec/共享面按在场——共享暂存区竞态加固):`)
|
|
1116
|
+
console.log(` git commit -m "chore(archive): ${changeName} 归档留档" -- ${pathspecs.join(' ')}`)
|
|
1117
|
+
if (dropped.length > 0) {
|
|
1118
|
+
console.log(` ⚠️ 丢弃 ${dropped.length} 个瞬时暂存条目(HEAD 不在册且工作区不在场——并行会话动了共享暂存区,未入命令):${dropped.join('、')}`)
|
|
1119
|
+
}
|
|
1120
|
+
console.log(` (若执行报 pathspec 不匹配:共享仓暂存区在打印→执行间又被并行会话改动——git diff --cached --name-only 全量重核,以实际面替换 pathspec 后再提交)`)
|
|
1121
|
+
}
|
|
1122
|
+
} catch { /* 提示失败不阻断归档(advisory) */ }
|
|
909
1123
|
if (othersResidual.length > 0) {
|
|
910
1124
|
const owners = [...new Set(othersResidual)].filter(Boolean)
|
|
911
1125
|
console.warn(`⚠️ 检测到「他者半归档」残留(暂存区存在其他变更的 rename 记录):${owners.join('、')}`)
|
package/src/run/complete.js
CHANGED
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { join, isAbsolute, resolve } from 'node:path'
|
|
20
20
|
import { existsSync, readFileSync, mkdirSync, writeFileSync, appendFileSync } from 'node:fs'
|
|
21
|
-
import { createHash } from 'node:crypto'
|
|
22
21
|
import { writeAtomicSync } from '../fs-atomic.js'
|
|
23
22
|
import { gitQuiet } from '../git-helper.js'
|
|
23
|
+
import { writeCloseTraceArtifacts, projectTraceFaceRows } from '../flow-parity.js'
|
|
24
24
|
import { withFileLock } from '../quicklog.js'
|
|
25
25
|
import { triggerSync, WAIT_MARKER_RE, getStageSteps, formatWaitOptions, resolveRuntimeRoot, getOrCreateMultiRepoContext, resolveChangeDir, writePlatformDocsPointer } from './shared.js'
|
|
26
26
|
import { isExplicitReviewWrite, collectWorktreeChangedFiles } from '../task-review.js'
|
|
@@ -451,6 +451,70 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
451
451
|
}
|
|
452
452
|
}
|
|
453
453
|
|
|
454
|
+
// ── 方案步 --done 知识检索命中回显(FR-03/D-004,2026-09-28-unclear-req-to-brainstorm)──
|
|
455
|
+
// 设计时点此前零检索面(自动注入只锚变更入口)——方案步 --done 对 --output 与 decisions.md
|
|
456
|
+
// 当前条目(D-xxx 标题+question 拼串;执行期裁决 D-007:条目无时间戳,「自上次 --done 新增」
|
|
457
|
+
// 无机械锚点,取全部条目为超集)跑既有检索匹配器 matchKnowledge(复用不另写);命中 → warn
|
|
458
|
+
// 回显(decisionHits rejected 优先排序为匹配器既有行为)+ evidence 回应提示;v1 只 warn 不
|
|
459
|
+
// 阻断(D-004 故障面自留退路)。逃生阀 local.yaml commands.knowledge-gate: false/off 关回显
|
|
460
|
+
// (缺省开,config-schema 已登记);无命中输出与现状一致。fail-open:检索异常静默放行。
|
|
461
|
+
if (stageName === 'brainstorm' && steps[currentIdx]?.name?.includes('提出 2-3 种方案') && changeName) {
|
|
462
|
+
try {
|
|
463
|
+
let _kgOn = true
|
|
464
|
+
try {
|
|
465
|
+
const _ly = readFileSync(join(specBase, 'local.yaml'), 'utf8')
|
|
466
|
+
const _kgm = _ly.match(/^\s*knowledge-gate\s*:\s*(true|false|on|off)\s*$/m)
|
|
467
|
+
if (_kgm && (_kgm[1] === 'false' || _kgm[1] === 'off')) _kgOn = false
|
|
468
|
+
} catch { /* 无 local.yaml = 缺省开 */ }
|
|
469
|
+
if (_kgOn) {
|
|
470
|
+
let _kgQuery = String(outputText || '')
|
|
471
|
+
let _dm = ''
|
|
472
|
+
try {
|
|
473
|
+
_dm = readFileSync(join(specBase, 'changes', changeName, 'decisions.md'), 'utf8')
|
|
474
|
+
const _parts = []
|
|
475
|
+
for (const m of _dm.matchAll(/^##\s+(D-\d+@v\d+):\s*(.+)$/gm)) _parts.push(`${m[1]} ${m[2]}`)
|
|
476
|
+
for (const m of _dm.matchAll(/^- question:\s*(.+)$/gm)) _parts.push(m[1])
|
|
477
|
+
if (_parts.length > 0) _kgQuery += '\n' + _parts.join('\n')
|
|
478
|
+
} catch { /* 无 decisions.md = 仅 --output */ }
|
|
479
|
+
_kgQuery = _kgQuery.slice(0, 4000)
|
|
480
|
+
if (_kgQuery.trim()) {
|
|
481
|
+
const { matchKnowledgeHybrid } = await import('../knowledge-vector.js')
|
|
482
|
+
const km = await matchKnowledgeHybrid(join(specBase, 'knowledge'), _kgQuery, { cwd })
|
|
483
|
+
// 零分不弹(2026-09-28-knowledge-gate-denoise):score 零且非死路的 rejected 与查询零主题
|
|
484
|
+
// 重叠——空标题条目靠状态蹭进回显是行为实测三例的噪音源;死路条目不受限(防复潮先验)。
|
|
485
|
+
const _hits = (km.decisionHits || []).filter((h) => h.deathPath || (h.status === 'rejected' && h.score > 0))
|
|
486
|
+
// 已回应不重弹:decisions.md 正文已含「命中 id+域文件名」共现(小白鼠回应形态
|
|
487
|
+
// 「unmapped.md D-001@v1」)→ 视为已甄别过,静默;同轮查询串仍含其词面属预期。
|
|
488
|
+
const _answered = _dm
|
|
489
|
+
? (h) => {
|
|
490
|
+
const _dom = String(h.file || '').replace(/^decisions\//, '').replace(/\.md$/, '')
|
|
491
|
+
return _dm.includes(h.id) && _dm.includes(_dom)
|
|
492
|
+
}
|
|
493
|
+
: () => false
|
|
494
|
+
const _echoHits = _hits.filter((h) => !_answered(h))
|
|
495
|
+
const _entries = km.matched ? (km.entries || []) : []
|
|
496
|
+
if (_echoHits.length > 0 || _entries.length > 0) {
|
|
497
|
+
const { deathPathNote } = await import('../knowledge-match.js')
|
|
498
|
+
console.warn(`\n⚠️ [knowledge-gate] 方案/决策知识命中(设计时点防复潮提示,warn 不阻断):`)
|
|
499
|
+
for (const h of _echoHits.slice(0, 5)) {
|
|
500
|
+
const _note = h.status === 'rejected'
|
|
501
|
+
? ` 否决理由:${h.reason || '(未记录)'}`
|
|
502
|
+
: h.deathPath ? ` ⚰️死路注记:${deathPathNote(h.reason)}` : ''
|
|
503
|
+
console.warn(` - ${h.id} ${h.title}(${h.file})status=${h.status || '?'}${_note}`)
|
|
504
|
+
}
|
|
505
|
+
for (const e of _entries.slice(0, 3)) {
|
|
506
|
+
const _base = e.anchor ? `${e.file}#${e.anchor}` : e.file
|
|
507
|
+
console.warn(` - 知识条目 ${_base}${e.display ? `(${e.display})` : ''}`)
|
|
508
|
+
}
|
|
509
|
+
console.warn(` 须在对应决策的 evidence 回应或说明不复潮(v1 提示级;local.yaml commands.knowledge-gate: false 可关本回显)`)
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
} catch (_kgEx) {
|
|
514
|
+
console.warn(` ⚠️ knowledge-gate 检索自身异常,fail-open 放行(不误拦本次完成):${_kgEx && _kgEx.message ? _kgEx.message : _kgEx}`)
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
454
518
|
// ── noAI 步骤硬门(坑 noai-done-bypass):noAI 步骤的确定性校验不可被 --done 绕过 ──
|
|
455
519
|
// 正常路径 agent 跑 `run <stage>` 推进到 noAI step 时,runStage 自动执行 _cliAction
|
|
456
520
|
// (stage.js noAI 分支,不写 step output);若 agent 对 noAI step 直接 --done,此前
|
|
@@ -898,15 +962,17 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
898
962
|
// verify 的"验证通过"提示延后到下方 validator 通过后才打印,
|
|
899
963
|
// 避免校验失败(FAIL / 缺 verify-result.md)时仍声称"验证通过可以归档"。
|
|
900
964
|
} else if (stageName === 'brainstorm') {
|
|
901
|
-
// brainstorm 下一步按 design.md 的 scale
|
|
965
|
+
// brainstorm 下一步按 design.md 的 scale 分叉(2026-09-29-brainstorm-exit-thin-default:large→plan;small/未标→flow start 收编),
|
|
902
966
|
// 不走下方 _getNextSuggestion —— 后者按全局状态机「第一个未完成且上游就绪的阶段」推荐,
|
|
903
967
|
// 当 scan 未完成时会误推 scan(回头路),与 brainstorm 已完成、应进入 plan/quick 矛盾。
|
|
904
968
|
// 历史教训:曾因此让 agent 在 brainstorm 完成后被误导去跑 scan。
|
|
905
969
|
const _bscale = readDesignScale(specBase, changeName)
|
|
906
|
-
if (_bscale === '
|
|
907
|
-
console.log(`\n👉 brainstorm 已完成(
|
|
970
|
+
if (_bscale === 'large') {
|
|
971
|
+
console.log(`\n👉 brainstorm 已完成(large)。下一步:sillyspec run plan${changeName ? ` --change ${changeName}` : ''}(完整五阶段)`)
|
|
908
972
|
} else {
|
|
909
|
-
|
|
973
|
+
// 未标/small 默认收编轻量道(2026-09-29-brainstorm-exit-thin-default:此前「未标走完整 plan」
|
|
974
|
+
// 是缺省即厚;翻转与 thin-default-flip 同哲学——升厚留运行时证据+用户决策)
|
|
975
|
+
console.log(`\n👉 brainstorm 已完成(scale=${_bscale || '未标,默认 small'})。下一步:sillyspec flow start --change ${changeName}(轻量变更收编头脑风暴产物,2 调用收口;实测失败自动升厚 / --upgrade-thick 用户决策)`)
|
|
910
976
|
}
|
|
911
977
|
} else if (stageName === 'quick') {
|
|
912
978
|
// quick 是收尾阶段(辅助流程,不走主链 scan→archive),完成后该提交,而非推 scan。
|
|
@@ -1043,16 +1109,27 @@ async function printExecuteScopeAudit({ cwd, changeName, specBase, platformOpts,
|
|
|
1043
1109
|
mkdirSync(changeDir, { recursive: true })
|
|
1044
1110
|
const { frozenPatch, ...snap } = result
|
|
1045
1111
|
snap.note = 'execute --done 时点冻结(本文件落盘时采集)'
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1112
|
+
snap.closedBy = 'execute --done'
|
|
1113
|
+
// 沉淀资产面投影(2026-10-07-unify-close-trace):files/totals = 主仓实改行(planned+
|
|
1114
|
+
// unplanned;untouched 0/0 是声明不是改动、crossRepo 行 patch 不在主仓——均不入)。
|
|
1115
|
+
// 经共用 writeCloseTraceArtifacts 与 thin flow done 同一处写四件(sha256 双套同锚);
|
|
1116
|
+
// 投影口径抽 flow-parity.projectTraceFaceRows 纯函数(排除路径有单测钉住)。
|
|
1117
|
+
const face = projectTraceFaceRows(snap.rows)
|
|
1118
|
+
const closeTrace = writeCloseTraceArtifacts({
|
|
1119
|
+
changeDir,
|
|
1120
|
+
change: changeName,
|
|
1121
|
+
baseline: typeof snap.baseAnchor === 'string' ? snap.baseAnchor : null,
|
|
1122
|
+
head: String(gitQuiet(cwd, ['rev-parse', 'HEAD']) || '').trim() || null,
|
|
1123
|
+
files: face.files,
|
|
1124
|
+
metaTotals: face.totals,
|
|
1125
|
+
patchText: typeof frozenPatch === 'string' && frozenPatch ? frozenPatch : null,
|
|
1126
|
+
savedAt: new Date().toISOString(),
|
|
1127
|
+
snapObj: snap,
|
|
1128
|
+
meta: {
|
|
1129
|
+
note: 'execute --done 时点冻结(沉淀资产面:主仓实改行投影——thin flow done 对等;治理工件目录与其后 verify/archive 演进不在内)',
|
|
1130
|
+
},
|
|
1131
|
+
})
|
|
1132
|
+
console.log(` 📦 范围快照 + 沉淀资产双轨落盘(scope-audit.json${closeTrace.patchStatus === 'ok' ? ' + scope-audit.patch' : ''} + change-patch.json${closeTrace.patchStatus === 'ok' ? ' + change.patch' : ''}${closeTrace.patchStatus === 'ok' ? '——收尾时点冻结,sha256 同锚' : '——patch 采集失败已留痕'})`)
|
|
1056
1133
|
} catch (e) {
|
|
1057
1134
|
console.warn(` ⚠️ 范围对账快照写入失败(不阻断,verify 漂移对比将按无快照降级):${e && e.message ? String(e.message).split('\n')[0] : e}`)
|
|
1058
1135
|
}
|
|
@@ -2086,16 +2163,16 @@ export async function continueStep(pm, progress, stageName, cwd, answer, options
|
|
|
2086
2163
|
// 阶段完成后明确下一步(agent 常卡:stageData completed 但不知要 run <下一阶段> 推进 currentStage)
|
|
2087
2164
|
const nextStageHint = { brainstorm: 'plan', plan: 'execute', execute: 'verify', verify: 'archive' }[stageName]
|
|
2088
2165
|
if (nextStageHint) {
|
|
2089
|
-
// brainstorm 按 design.md frontmatter 的 scale
|
|
2090
|
-
//
|
|
2091
|
-
//
|
|
2166
|
+
// brainstorm 按 design.md frontmatter 的 scale 分叉(2026-09-29-brainstorm-exit-thin-default
|
|
2167
|
+
// 与 completeStep 主路径同口径):large → run plan(完整五阶段);small/未标 → flow start
|
|
2168
|
+
// 收编轻量道(此前 small→quick --linked-changes 是旧收编通道、未标→plan 是缺省即厚,均已退役)。
|
|
2092
2169
|
let hintStage = nextStageHint
|
|
2093
2170
|
let hintChangeFlag = changeName ? ` --change ${changeName}` : ''
|
|
2094
|
-
if (stageName === 'brainstorm' && changeName && readDesignScale(specBase, changeName)
|
|
2095
|
-
|
|
2096
|
-
|
|
2171
|
+
if (stageName === 'brainstorm' && changeName && readDesignScale(specBase, changeName) !== 'large') {
|
|
2172
|
+
console.log(`\n👉 ${stageName} 已完成。下一步:sillyspec flow start --change ${changeName}(轻量变更收编头脑风暴产物,2 调用收口)`)
|
|
2173
|
+
} else {
|
|
2174
|
+
console.log(`\n👉 ${stageName} 已完成。下一步:sillyspec run ${hintStage}${hintChangeFlag}`)
|
|
2097
2175
|
}
|
|
2098
|
-
console.log(`\n👉 ${stageName} 已完成。下一步:sillyspec run ${hintStage}${hintChangeFlag}`)
|
|
2099
2176
|
if (stageName === 'execute') {
|
|
2100
2177
|
console.log(` ⚠️ 若 worktree 改动还没 apply 到主工作区,先:sillyspec worktree apply ${changeName}`)
|
|
2101
2178
|
console.log(` (apply 不需要先 commit,支持 working tree 未提交改动)`)
|
package/src/run/gate-snapshot.js
CHANGED
|
@@ -722,6 +722,21 @@ export function detectSymlinkStoreLayout(cwd) {
|
|
|
722
722
|
if (existsSync(join(cwd, 'pnpm-lock.yaml'))) return 'pnpm'
|
|
723
723
|
if (existsSync(join(cwd, 'bun.lockb')) || existsSync(join(cwd, 'bun.lock'))) return 'bun'
|
|
724
724
|
if (existsSync(join(cwd, 'lerna.json'))) return 'lerna'
|
|
725
|
+
// ── 子目录 lockfile(apps 型 monorepo——2026-09-30-snapshot-symlink-store-subdir)──
|
|
726
|
+
// 实证:multi-agent-platform(frontend/ + sillyhub-daemon/ 各自 pnpm-lock.yaml,根目录无
|
|
727
|
+
// lockfile)漏检 → 快照照建,verify 收敛循环每轮 ~160s 纯烧快照构建/junction。packages/*
|
|
728
|
+
// workspace 型不扫(该形态 lockfile 在根,根判据已覆盖)。子目录命中=该 app 的
|
|
729
|
+
// node_modules 符号链接网同样跨根失效——跳快照回主仓是既有保守裁决(宁主仓口径)。
|
|
730
|
+
try {
|
|
731
|
+
for (const e of readdirSync(cwd, { withFileTypes: true })) {
|
|
732
|
+
if (!e.isDirectory()) continue
|
|
733
|
+
if (e.name === 'node_modules' || e.name.startsWith('.')) continue
|
|
734
|
+
const d = join(cwd, e.name)
|
|
735
|
+
if (existsSync(join(d, 'pnpm-lock.yaml'))) return `pnpm(subdir:${e.name})`
|
|
736
|
+
if (existsSync(join(d, 'bun.lockb')) || existsSync(join(d, 'bun.lock'))) return `bun(subdir:${e.name})`
|
|
737
|
+
if (existsSync(join(d, 'lerna.json'))) return `lerna(subdir:${e.name})`
|
|
738
|
+
}
|
|
739
|
+
} catch { /* 根不可读(与非仓目录同语义)→ 落到 packageManager 判据 */ }
|
|
725
740
|
try {
|
|
726
741
|
const pkg = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf8'))
|
|
727
742
|
const pm = pkg && pkg.packageManager ? String(pkg.packageManager) : ''
|