sillyspec 3.28.8 → 3.28.10
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/full.json +1 -0
- package/.claude/skills/sillyspec-execute/SKILL.md +222 -224
- package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
- package/.claude/skills/sillyspec-quick/SKILL.md +118 -117
- package/AGENTS.md +3 -2
- package/README.md +35 -11
- package/SKILL.md +24 -10
- package/SillySpec-/350/203/275/345/212/233/344/272/256/347/202/271/345/205/250/346/231/257-2026-09-14.pptx +0 -0
- package/package.json +1 -1
- package/src/change-list.js +73 -20
- package/src/change-risk-profile.js +20 -0
- package/src/config-schema.js +62 -0
- package/src/contract-matrix.js +84 -5
- package/src/cross-repo-reconcile.js +134 -0
- package/src/db.js +11 -3
- package/src/decision-distill.js +15 -3
- package/src/design-facts.js +89 -20
- package/src/docs-gate.js +105 -6
- package/src/doctor-diagnostics.js +224 -2
- package/src/friction-ledger.js +171 -0
- package/src/hooks/worktree-guard.js +13 -1
- package/src/index.js +206 -18
- package/src/knowledge-classify.js +382 -0
- package/src/knowledge-hits.js +74 -0
- package/src/knowledge-stats.js +180 -0
- package/src/probe7-anchor-check.js +69 -0
- package/src/progress/change-registry.js +183 -8
- package/src/progress/shared.js +3 -2
- package/src/progress.js +100 -8
- package/src/quick-gate-profile.js +251 -0
- package/src/quicklog.js +164 -18
- package/src/review-dispatch.js +2 -1
- package/src/run/command.js +131 -13
- package/src/run/complete-handlers.js +511 -21
- package/src/run/complete.js +123 -23
- package/src/run/concurrent-detect.js +98 -0
- package/src/run/gate-snapshot.js +110 -2
- package/src/run/gates.js +141 -16
- package/src/run/prompt.js +221 -11
- package/src/run/quick-audit.js +111 -13
- package/src/run/shared.js +147 -11
- package/src/run/stage.js +6 -0
- package/src/scan-diff.js +61 -30
- package/src/scan-postcheck.js +74 -0
- package/src/scan-refresh.js +477 -0
- package/src/scan-staleness.js +25 -13
- package/src/scope-audit.js +456 -56
- package/src/stage-contract.js +185 -2
- package/src/stage-review.js +23 -7
- package/src/stages/brainstorm.js +6 -0
- package/src/stages/cmd-existence.js +9 -3
- package/src/stages/execute.js +85 -3
- package/src/stages/knowledge.js +76 -6
- package/src/stages/plan-postcheck.js +32 -5
- package/src/stages/quick.js +1 -1
- package/src/stages/verify.js +1 -0
- package/src/sync-noise.js +170 -6
- package/src/sync.js +141 -9
- package/src/task-review.js +366 -46
- package/src/verify-postcheck.js +212 -55
- package/src/verify-probes.js +727 -16
- package/src/worktree-apply.js +685 -48
- package/src/worktree-deps.js +124 -3
- package/src/worktree.js +391 -15
- package/templates/agents-instruction.md +2 -2
- package/templates/prompts/testcase-design.md +2 -1
- package/templates/prompts/verify-probes.md +4 -1
- package/.patch16-tmp.mjs +0 -169
- package/sa-rename-probe/src/new.js +0 -4
package/src/task-review.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { existsSync, readFileSync, mkdirSync, writeFileSync, readdirSync, statSync } from 'fs'
|
|
12
12
|
import { join, resolve, basename, dirname } from 'path'
|
|
13
|
+
import { execFileSync } from 'child_process'
|
|
13
14
|
import { git, unquoteGitPath } from './git-helper.js'
|
|
14
15
|
import { pathMatches } from './change-list.js'
|
|
15
16
|
import { parseAllowedPaths, parseRepo, parseBaseCommit, parseHeadCommit } from './stages/plan-postcheck.js'
|
|
@@ -17,6 +18,7 @@ import { resolveVerifyChangedFiles } from './verify-postcheck.js'
|
|
|
17
18
|
import { splitOwnVsForeignDiffFiles } from './foreign-declared.js'
|
|
18
19
|
import { WorktreeManager } from './worktree.js'
|
|
19
20
|
import { resolveRuntimeRoot } from './run/shared.js'
|
|
21
|
+
import { DB } from './db.js'
|
|
20
22
|
|
|
21
23
|
// ── review.json schema version ──
|
|
22
24
|
//
|
|
@@ -334,6 +336,26 @@ export function readReview(reviewPath) {
|
|
|
334
336
|
return { ok: true, review: parsed, errors: [] }
|
|
335
337
|
}
|
|
336
338
|
|
|
339
|
+
/**
|
|
340
|
+
* 判定 review 是否经显式 CLI 命令式写入(`sillyspec review write` → writeTaskReview)。
|
|
341
|
+
* (ql-20260915-001 修复②,坑 review-write-draft-marker-stuck:execute 期间反复
|
|
342
|
+
* 「草稿 review changedFiles 为空,跳过自动勾选」,主仓文档面任务自动勾选断链。)
|
|
343
|
+
*
|
|
344
|
+
* 判据:reviewProvenanceStamp 落的 writtenBy 以 'writeTaskReview' 开头(含 ':force' 变体)。
|
|
345
|
+
* 显式写入带 agent 供给的 verdict(非草稿兜底通道),即使 reviewerNotes 字面残留
|
|
346
|
+
* 'auto-generated draft'(agent 升级草稿时 --notes 引用/复述草稿文案、或 adopt 通道合并
|
|
347
|
+
* 保留旧 notes 的变体)也一律按非草稿处理——verdict 判定不再受草稿零 diff 守卫限制。
|
|
348
|
+
*
|
|
349
|
+
* 无 writtenBy 的存量 review(provenance 机制引入前手写)按非显式处理,走原 notes
|
|
350
|
+
* 启发式判定(向后兼容,行为不回退)。
|
|
351
|
+
*
|
|
352
|
+
* @param {object|null} review readReview 返回的 review 对象
|
|
353
|
+
* @returns {boolean}
|
|
354
|
+
*/
|
|
355
|
+
export function isExplicitReviewWrite(review) {
|
|
356
|
+
return typeof review?.writtenBy === 'string' && review.writtenBy.startsWith('writeTaskReview')
|
|
357
|
+
}
|
|
358
|
+
|
|
337
359
|
/**
|
|
338
360
|
* 校验 plan.md 里所有「已勾选 [x]」task 的 review.json 是否 schema 完整(坑 review-json-field-gap)。
|
|
339
361
|
*
|
|
@@ -1103,6 +1125,194 @@ export function resolveExecuteRunForChange({ runtimeRoot, changeName, taskIds =
|
|
|
1103
1125
|
}
|
|
1104
1126
|
}
|
|
1105
1127
|
|
|
1128
|
+
// ── 归因模式分流(2026-09-14-change-ownership-guards task-03 / D-004@v1 / FR-03)──
|
|
1129
|
+
// 草稿/changedFiles 归因的取数源路由:判定源=changes.isolation_mode 列(DB 读,meta 缺失也可判,
|
|
1130
|
+
// 与 change-registry.readChangeIsolation 同列;此处直连 db.js 读——task-review 是叶子模块,
|
|
1131
|
+
// 静态 import progress.js 会成环 progress→consistency-doctor→task-review)。进程内按
|
|
1132
|
+
// dbPath+change 缓存(CLI 短进程生命周期内 isolation_mode 不变,避免重复 open/init)。
|
|
1133
|
+
const _isolationModeCache = new Map()
|
|
1134
|
+
|
|
1135
|
+
/**
|
|
1136
|
+
* 读 changes.isolation_mode(只读;无库/无行/读失败 → null)。DB 路径候选序:
|
|
1137
|
+
* platformOpts.specRoot > specDriftAnchor > specBase > cwd/.sillyspec(首个存在 sillyspec.db 的根)。
|
|
1138
|
+
* 模块内函数(非 export——check-syntax 22e-b 死码门禁:无跨文件消费不导出)。
|
|
1139
|
+
* @param {{ cwd: string, specBase: string, platformOpts?: object, changeName: string }} opts
|
|
1140
|
+
* @returns {string|null} isolation_mode('worktree'|'native-worktree'|'in-place-fallback')或 null
|
|
1141
|
+
*/
|
|
1142
|
+
function readChangeIsolationMode({ cwd, specBase, platformOpts = {}, changeName }) {
|
|
1143
|
+
if (!changeName) return null
|
|
1144
|
+
const roots = []
|
|
1145
|
+
const addRoot = (r) => { if (r && !roots.includes(r)) roots.push(r) }
|
|
1146
|
+
addRoot(platformOpts && platformOpts.specRoot)
|
|
1147
|
+
addRoot(platformOpts && platformOpts.specDriftAnchor)
|
|
1148
|
+
addRoot(specBase)
|
|
1149
|
+
addRoot(join(cwd, '.sillyspec'))
|
|
1150
|
+
for (const root of roots) {
|
|
1151
|
+
const dbPath = join(root, '.runtime', 'sillyspec.db')
|
|
1152
|
+
if (!existsSync(dbPath)) continue
|
|
1153
|
+
const key = dbPath + '::' + changeName
|
|
1154
|
+
if (_isolationModeCache.has(key)) return _isolationModeCache.get(key)
|
|
1155
|
+
let mode = null
|
|
1156
|
+
try {
|
|
1157
|
+
const db = new DB(dbPath)
|
|
1158
|
+
db.init()
|
|
1159
|
+
const row = db.getDb().prepare('SELECT isolation_mode FROM changes WHERE name = ?').get(changeName)
|
|
1160
|
+
mode = (row && typeof row.isolation_mode === 'string' && row.isolation_mode) || null
|
|
1161
|
+
} catch { mode = null }
|
|
1162
|
+
_isolationModeCache.set(key, mode)
|
|
1163
|
+
return mode
|
|
1164
|
+
}
|
|
1165
|
+
return null
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
/**
|
|
1169
|
+
* 归因取数源路由(单一入口,草稿生成 generateTaskReviewDrafts 与 mechanics 代算 writeTaskReview
|
|
1170
|
+
* 两处共用)。判定优先级(D-004):
|
|
1171
|
+
* 1. DB isolation_mode ∈ {worktree, native-worktree} → worktree 归因:worktree 目录活则原口径
|
|
1172
|
+
* (resolveVerifyChangedFiles meta 路径 + porcelain 并入,零回归);meta 缺失/in-place meta/
|
|
1173
|
+
* 目录已删 → 分支 ref 口径(merge-base(主仓 HEAD, 分支 tip)..tip 的 commit diff + worktree
|
|
1174
|
+
* 目录在则 porcelain 并入,与 verify reconcile 同口径)。分支 ref 也已删(cleanup 后态)→
|
|
1175
|
+
* fail-closed 空集 +「不可归因(worktree 已清理)」注记,绝不回退主仓窗口(复审残留①)。
|
|
1176
|
+
* 2. DB in-place-fallback 但 meta 判 worktree 且目录活 → 仍取 worktree(fail-safe:不因 DB 行
|
|
1177
|
+
* 陈旧切向主仓共享脏窗口——归因源只会更隔离不会更共享),注记不一致。
|
|
1178
|
+
* 3. 其余(DB in-place / NULL)→ 主仓窗口原行为零回归;NULL 且 meta 缺失加存量路由注记
|
|
1179
|
+
* (存量态不追求完美只保安全,D-004 残留②/R-04)。
|
|
1180
|
+
* meta 在时交叉校验不一致以 DB 为准(方向 1 的 worktree 覆盖);返回 notes 逐条说明路由依据。
|
|
1181
|
+
*
|
|
1182
|
+
* @param {{ cwd: string, changeName: string, specBase: string, platformOpts?: object }} opts
|
|
1183
|
+
* @returns {{ mode: 'worktree'|'worktree-branch'|'worktree-cleaned'|'main-window', diffFiles: string[]|null, notes: string[], meta: object|null, mergeGitDir: string|null, branch?: string, branchBase?: string, branchHead?: string }}
|
|
1184
|
+
*/
|
|
1185
|
+
function resolveAttributionDiffFiles({ cwd, changeName, specBase, platformOpts = {} }) {
|
|
1186
|
+
const notes = []
|
|
1187
|
+
const wm = new WorktreeManager({ cwd })
|
|
1188
|
+
let meta = null
|
|
1189
|
+
try { meta = wm.getMeta(changeName) } catch { meta = null }
|
|
1190
|
+
const iso = readChangeIsolationMode({ cwd, specBase, platformOpts, changeName })
|
|
1191
|
+
const isoWorktree = iso === 'worktree' || iso === 'native-worktree'
|
|
1192
|
+
|
|
1193
|
+
if (isoWorktree) {
|
|
1194
|
+
const wtGitDir = (meta && meta.worktreePath && meta.mode !== 'in-place-fallback' && existsSync(meta.worktreePath))
|
|
1195
|
+
? meta.worktreePath
|
|
1196
|
+
: null
|
|
1197
|
+
if (wtGitDir) {
|
|
1198
|
+
if (meta.mode !== iso) notes.push(`meta.mode=${meta.mode} 与 DB isolation_mode=${iso} 不一致——以 DB 为准(同属 worktree 族,归因源不变)`)
|
|
1199
|
+
return { mode: 'worktree', diffFiles: resolveVerifyChangedFiles(cwd, changeName, null, { specBase }), notes, meta, mergeGitDir: wtGitDir }
|
|
1200
|
+
}
|
|
1201
|
+
// meta 缺失 / in-place meta / worktree 目录已删:分支 ref 判定(不落主仓窗口)
|
|
1202
|
+
const branch = (meta && meta.branch) || `sillyspec/${changeName}`
|
|
1203
|
+
let branchTip = null
|
|
1204
|
+
try { branchTip = runGit(cwd, ['rev-parse', '--verify', '--quiet', `refs/heads/${branch}`]) } catch { branchTip = null }
|
|
1205
|
+
if (!branchTip) {
|
|
1206
|
+
notes.push(`不可归因(worktree 已清理)——DB 判 worktree(isolation_mode=${iso})但分支 ref ${branch} 不存在(cleanup 后态),fail-closed 空集,不回退主仓窗口`)
|
|
1207
|
+
return { mode: 'worktree-cleaned', diffFiles: [], notes, meta, mergeGitDir: null }
|
|
1208
|
+
}
|
|
1209
|
+
let branchBase = null
|
|
1210
|
+
let files = []
|
|
1211
|
+
try {
|
|
1212
|
+
const mainHead = runGit(cwd, ['rev-parse', 'HEAD'])
|
|
1213
|
+
const mb = mainHead ? runGit(cwd, ['merge-base', branchTip, mainHead]) : null
|
|
1214
|
+
branchBase = mb || mainHead || null
|
|
1215
|
+
if (branchBase) {
|
|
1216
|
+
files = String(runGit(cwd, ['diff', '--name-only', branchBase, branchTip]) || '')
|
|
1217
|
+
.split('\n').map(p => p.replace(/^"|"$/g, '').replace(/\\/g, '/').trim())
|
|
1218
|
+
.filter(p => p && p !== '.sillyspec' && !p.startsWith('.sillyspec/'))
|
|
1219
|
+
}
|
|
1220
|
+
} catch { /* 已提交口径 git 失败 → 空集 fail-closed(不回退主仓窗口) */ }
|
|
1221
|
+
// 未提交并入:约定路径 worktree 目录仍在(meta 失联孤儿)→ porcelain(与 verify reconcile 同口径)
|
|
1222
|
+
const wtDirGuess = join(wm.worktreeBase, changeName)
|
|
1223
|
+
if (existsSync(wtDirGuess)) {
|
|
1224
|
+
try {
|
|
1225
|
+
const st = runGit(wtDirGuess, ['status', '--porcelain', '--untracked-files=all'], { trim: false })
|
|
1226
|
+
const wtFiles = parsePorcelainFiles(st)
|
|
1227
|
+
.map(p => String(p).replace(/\\/g, '/'))
|
|
1228
|
+
.filter(p => p !== '.sillyspec' && !p.startsWith('.sillyspec/'))
|
|
1229
|
+
files = [...new Set([...files, ...wtFiles])]
|
|
1230
|
+
} catch { /* porcelain 失败退回 commit diff 口径 */ }
|
|
1231
|
+
}
|
|
1232
|
+
notes.push(`meta 缺失/失效,DB isolation_mode=${iso} 判 worktree——归因源=worktree 分支 ${branch} diff(base..HEAD+porcelain,与 verify reconcile 同口径),不落主仓共享脏窗口`)
|
|
1233
|
+
return { mode: 'worktree-branch', diffFiles: files, notes, meta, mergeGitDir: null, branch, branchBase, branchHead: branchTip }
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
if (iso === 'in-place-fallback' && meta && meta.mode !== 'in-place-fallback'
|
|
1237
|
+
&& meta.worktreePath && existsSync(meta.worktreePath)) {
|
|
1238
|
+
notes.push(`DB isolation_mode=in-place-fallback 但 meta.mode=${meta.mode}——worktree 分支仍是隔离归因源(fail-safe:不因 DB 行陈旧切向主仓共享窗口),按 meta 取 worktree 归因`)
|
|
1239
|
+
return { mode: 'worktree', diffFiles: resolveVerifyChangedFiles(cwd, changeName, null, { specBase }), notes, meta, mergeGitDir: meta.worktreePath }
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
// in-place(DB 或 meta)与 NULL 存量:主仓窗口(原行为零回归)
|
|
1243
|
+
if (!meta && iso === null) {
|
|
1244
|
+
notes.push('isolation_mode 未登记(存量变更/无 DB 行)且 worktree meta 缺失——按主仓窗口归因(存量兼容路由,D-004 残留②:不追求完美只保安全)')
|
|
1245
|
+
}
|
|
1246
|
+
const mergeGitDir = meta
|
|
1247
|
+
? ((meta.worktreePath && meta.mode !== 'in-place-fallback' && existsSync(meta.worktreePath)) ? meta.worktreePath : cwd)
|
|
1248
|
+
: null
|
|
1249
|
+
return { mode: 'main-window', diffFiles: resolveVerifyChangedFiles(cwd, changeName, null, { specBase }), notes, meta, mergeGitDir }
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
/**
|
|
1253
|
+
* worktree/in-place 改动文件集:porcelain 未提交 ∪ committed merge-base 补齐——口径单一化。
|
|
1254
|
+
* 口径出处:D-004@v1(worktree 归因唯一事实源 = worktree 分支 diff + porcelain,决策库
|
|
1255
|
+
* decisions/worktree.md)+ 变更 2026-09-15-worktree-dual-truth-gates §4 / FR-04;坑④
|
|
1256
|
+
* worktree-tick-guard-commit-only(2026-09-15 实证):勾选守卫 prefetchDiffFileSet
|
|
1257
|
+
* (run/complete.js)只算 base..head(已提交),子代理默认不 commit 时守卫恒空跳过勾选——
|
|
1258
|
+
* 本 helper 是「草稿归属并入段(generateTaskReviewDrafts)」与「勾选守卫(prefetchDiffFileSet)」
|
|
1259
|
+
* 两处的单一真相。已知残留 R-09:resolveVerifyChangedFiles(verify-postcheck.js)内另有同型
|
|
1260
|
+
* 补齐段(form A 专属:滤网保 .sillyspec/docs/** 交付物,与本 helper 的整前缀滤网不同),
|
|
1261
|
+
* 本次不动,待该函数下次触碰时统一。
|
|
1262
|
+
*
|
|
1263
|
+
* 行为契约:
|
|
1264
|
+
* - meta 缺省时内部 WorktreeManager.getMeta 自取(try/catch → null)。
|
|
1265
|
+
* - gitDir = meta.worktreePath(mode 非 in-place-fallback 且目录存在)否则 cwd——
|
|
1266
|
+
* in-place 退化取 cwd(保住现状 in-place 主仓 porcelain 并入,不丢,R-08);
|
|
1267
|
+
* meta 完全缺失时同样取 cwd(与被并入段 mergeGitDir 语义一致)。
|
|
1268
|
+
* - porcelain:status --porcelain --untracked-files=all(parsePorcelainFiles 解析),
|
|
1269
|
+
* 滤 .sillyspec 前缀(与被替换的草稿并入段同口径)。
|
|
1270
|
+
* - committed 补齐(仅 gitDir !== cwd,防双并):merge-base(主仓 HEAD, wtHEAD)..wtHEAD
|
|
1271
|
+
* 的 diff --name-only——worktree 内已 commit 的改动对主仓 base..head 与 status 双不可见
|
|
1272
|
+
* (「先提交则 diff 空」时序两难);git 失败 fail-open 忽略。
|
|
1273
|
+
* - 返回正斜杠归一去重数组;任何 git 异常 → 已收集部分或 [](fail-open,不阻断调用方)。
|
|
1274
|
+
*
|
|
1275
|
+
* @param {string} cwd 主仓根
|
|
1276
|
+
* @param {string} changeName 变更名
|
|
1277
|
+
* @param {object|null} [meta] 预取的 worktree meta(缺省内部 getMeta 自取)
|
|
1278
|
+
* @param {object} [opts] 预留(对齐 design 接口签名)
|
|
1279
|
+
* @returns {string[]} 正斜杠归一文件集(git 失败 → 已收集部分或 [],fail-open)
|
|
1280
|
+
*/
|
|
1281
|
+
export function collectWorktreeChangedFiles(cwd, changeName, meta = null, opts = {}) {
|
|
1282
|
+
const files = new Set()
|
|
1283
|
+
if (!cwd || !changeName) return []
|
|
1284
|
+
let m = meta
|
|
1285
|
+
if (m == null) {
|
|
1286
|
+
try { m = new WorktreeManager({ cwd }).getMeta(changeName) } catch { m = null }
|
|
1287
|
+
}
|
|
1288
|
+
const gitDir = (m?.worktreePath && m.mode !== 'in-place-fallback' && existsSync(m.worktreePath))
|
|
1289
|
+
? m.worktreePath
|
|
1290
|
+
: cwd
|
|
1291
|
+
const keep = (p) => p && p !== '.sillyspec' && !p.startsWith('.sillyspec/')
|
|
1292
|
+
// 未提交:worktree(in-place 退化=主仓)porcelain 全量(含 untracked)
|
|
1293
|
+
try {
|
|
1294
|
+
const st = runGit(gitDir, ['status', '--porcelain', '--untracked-files=all'], { trim: false })
|
|
1295
|
+
for (const p of parsePorcelainFiles(st)) if (keep(p)) files.add(p)
|
|
1296
|
+
} catch { /* porcelain 失败 → 返回已收集部分(fail-open) */ }
|
|
1297
|
+
// 已提交补齐:merge-base(主仓 HEAD, wtHEAD)(= 创建锚点)..wtHEAD 的 commit diff。
|
|
1298
|
+
// in-place(gitDir===cwd)时已提交改动在主仓 diff 内,跳过防双并;失败 fail-open 忽略。
|
|
1299
|
+
if (gitDir !== cwd) {
|
|
1300
|
+
try {
|
|
1301
|
+
const mainHead = runGit(cwd, ['rev-parse', 'HEAD'])
|
|
1302
|
+
const wtHead = runGit(gitDir, ['rev-parse', 'HEAD'])
|
|
1303
|
+
const mb = (mainHead && wtHead) ? runGit(gitDir, ['merge-base', wtHead, mainHead]) : null
|
|
1304
|
+
if (mb) {
|
|
1305
|
+
const out = String(runGit(gitDir, ['diff', '--name-only', mb, wtHead]) || '')
|
|
1306
|
+
for (let p of out.split('\n')) {
|
|
1307
|
+
p = p.replace(/^"|"$/g, '').replace(/\\/g, '/').trim()
|
|
1308
|
+
if (keep(p)) files.add(p)
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
} catch { /* 已提交补齐失败忽略(fail-open,不拖垮未提交并入) */ }
|
|
1312
|
+
}
|
|
1313
|
+
return [...files]
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1106
1316
|
/**
|
|
1107
1317
|
* worktree execute「主 agent 直接实现」模式收尾兜底:per-task review.json 缺失时,
|
|
1108
1318
|
* 据 git diff base..head 按 task allowed_paths 归属,自动落盘 cannot_verify 草稿。
|
|
@@ -1170,53 +1380,54 @@ export async function generateTaskReviewDrafts({ changeName, cwd, platformOpts =
|
|
|
1170
1380
|
}
|
|
1171
1381
|
}
|
|
1172
1382
|
|
|
1383
|
+
// ── 归因模式分流(task-03 / D-004@v1):判定源=changes.isolation_mode(DB 读)──
|
|
1384
|
+
// worktree 模式(含 meta 缺失回退态)归因唯一源=worktree 分支 diff(绝不落主仓共享脏窗口,
|
|
1385
|
+
// troubleshooting §65 事件②根因);in-place 维持主仓窗口;NULL 存量按 meta 在场路由。
|
|
1386
|
+
// 分流自身异常 → 退回原 resolveVerifyChangedFiles 口径(fail-open,不比修复前差)。
|
|
1387
|
+
let attribution
|
|
1388
|
+
try {
|
|
1389
|
+
attribution = resolveAttributionDiffFiles({ cwd, changeName, specBase, platformOpts })
|
|
1390
|
+
} catch (e) {
|
|
1391
|
+
attribution = { mode: 'main-window', diffFiles: resolveVerifyChangedFiles(cwd, changeName, null, { specBase }), notes: [`归因分流解析异常(${e && e.message ? e.message : e})——退回原口径`], meta: null, mergeGitDir: null }
|
|
1392
|
+
}
|
|
1393
|
+
for (const note of attribution.notes) console.log(`[sillyspec] 归因注记:${note}`)
|
|
1173
1394
|
// base..head diff 文件集(worktree-aware;null=git 不可用,[]=无 commit diff)
|
|
1174
|
-
let diffFiles =
|
|
1395
|
+
let diffFiles = attribution.diffFiles
|
|
1175
1396
|
// ⚠️ 并入 worktree 未提交改动(坑 draft-attribution-uncommitted-worktree,2026-08-21 实证):
|
|
1176
1397
|
// 子代理默认不 commit(execute 复盘 a 同源事实),真实改动全在 worktree working-tree——只看
|
|
1177
1398
|
// base..HEAD commit diff 时归属恒空,9/9 草稿全成「无归属」靠主代理手写升级。与
|
|
1178
1399
|
// verifyReviewGitEvidence 的 working-tree 并入口径同源:status --porcelain 文件(排除
|
|
1179
1400
|
// .sillyspec/ 运行时产物)并入归属集,按 allowed_paths 正常路径归属;review.head 仍为 HEAD
|
|
1180
1401
|
// commit——evidence 校验对「commit diff 空 + working-tree 有改动」明示不判伪造,语义一致。
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
// in-place 退化(wtGitDir===cwd)时已提交改动在主仓 diff 内,跳过防双并;git 失败 fail-open。
|
|
1197
|
-
let committedFiles = []
|
|
1198
|
-
if (wtGitDir !== cwd) {
|
|
1199
|
-
try {
|
|
1200
|
-
const mainHead = runGit(cwd, ['rev-parse', 'HEAD'])
|
|
1201
|
-
const wtHead = runGit(wtGitDir, ['rev-parse', 'HEAD'])
|
|
1202
|
-
const mb = (mainHead && wtHead) ? runGit(wtGitDir, ['merge-base', wtHead, mainHead]) : null
|
|
1203
|
-
if (mb) {
|
|
1204
|
-
committedFiles = String(runGit(wtGitDir, ['diff', '--name-only', mb, wtHead]) || '')
|
|
1205
|
-
.split('\n').map(p => p.replace(/^"|"$/g, '').replace(/\\/g, '/').trim())
|
|
1206
|
-
.filter(p => p && p !== '.sillyspec' && !p.startsWith('.sillyspec/'))
|
|
1207
|
-
}
|
|
1208
|
-
} catch { /* 已提交补齐失败退回 status-only(fail-open,不拖垮未提交并入) */ }
|
|
1209
|
-
}
|
|
1210
|
-
if (wtFiles.length > 0 || committedFiles.length > 0) {
|
|
1211
|
-
diffFiles = [...new Set([...(Array.isArray(diffFiles) ? diffFiles : []), ...wtFiles, ...committedFiles])]
|
|
1212
|
-
console.log(`[sillyspec] 草稿归属并入 worktree 改动 ${wtFiles.length + committedFiles.length} 个(未提交 ${wtFiles.length} + 已提交 ${committedFiles.length},按 allowed_paths 归属)`)
|
|
1402
|
+
// D-004 后仅在归因源指向活跃 worktree 目录 / in-place meta(mergeGitDir)时执行——
|
|
1403
|
+
// worktree-branch/cleaned 归因的未提交并入已在 resolveAttributionDiffFiles 内按同口径完成。
|
|
1404
|
+
if (attribution.mergeGitDir) {
|
|
1405
|
+
try {
|
|
1406
|
+
// D-004@v1 口径单一化(坑④ worktree-tick-guard-commit-only,2026-09-15 实证):原手写
|
|
1407
|
+
// 并入段(porcelain 未提交 + committed merge-base 补齐两段)收口为
|
|
1408
|
+
// collectWorktreeChangedFiles 单一真相——勾选守卫 prefetchDiffFileSet(run/complete.js)
|
|
1409
|
+
// 与本草稿归属自此消费同一口径(原「两处口径须同步改」注记就此作废,单一真相在 helper)。
|
|
1410
|
+
// in-place(mergeGitDir===cwd)时 helper 同取 cwd porcelain(现状主仓并入不丢)。
|
|
1411
|
+
// resolveVerifyChangedFiles(verify-postcheck.js)内另有同型补齐段(form A 专属,滤网保
|
|
1412
|
+
// .sillyspec/docs/** 交付物),已知残留 R-09,本次不动。
|
|
1413
|
+
const mergedFiles = collectWorktreeChangedFiles(cwd, changeName, attribution.meta)
|
|
1414
|
+
if (mergedFiles.length > 0) {
|
|
1415
|
+
diffFiles = [...new Set([...(Array.isArray(diffFiles) ? diffFiles : []), ...mergedFiles])]
|
|
1416
|
+
console.log(`[sillyspec] 草稿归属并入 worktree 改动 ${mergedFiles.length} 个(未提交 porcelain + 已提交 merge-base 补齐,按 allowed_paths 归属)`)
|
|
1213
1417
|
}
|
|
1214
|
-
}
|
|
1215
|
-
}
|
|
1418
|
+
} catch { /* working-tree 并入失败退回 commit diff 口径(fail-open,不阻断草稿) */ }
|
|
1419
|
+
}
|
|
1216
1420
|
// 单仓模式(无 ctx):主仓无 diff 即无任何 task 可生成 → 提前返回(原逻辑零回归)。
|
|
1217
1421
|
// 有 ctx:主仓无 diff 不阻断——跨仓 task 的 diff 在跨仓仓根独立取(per-task),主仓 task 自然跳过(空 changedFiles)。
|
|
1422
|
+
// 终态空源(D-004):worktree-cleaned(DB 判 worktree 但分支 ref 已删)空集的 reason 显式
|
|
1423
|
+
// 注记「不可归因」,与普通「无 diff」区分——绝不回退主仓窗口补救。
|
|
1218
1424
|
if (!ctx && (!diffFiles || diffFiles.length === 0)) {
|
|
1219
|
-
return {
|
|
1425
|
+
return {
|
|
1426
|
+
generated: 0, skipped: 0, unattributed: [], executeRunId,
|
|
1427
|
+
reason: attribution.mode === 'worktree-cleaned'
|
|
1428
|
+
? '不可归因(worktree 已清理)——DB 判 worktree 但分支 ref 已删,fail-closed 空集(不回退主仓窗口)'
|
|
1429
|
+
: 'base..head 无代码 diff(改动未 commit?)',
|
|
1430
|
+
}
|
|
1220
1431
|
}
|
|
1221
1432
|
|
|
1222
1433
|
// base/head + gitDir:与 gates.js reviewGitDir 同源(worktree 优先,in-place 回退 cwd)
|
|
@@ -1248,6 +1459,15 @@ export async function generateTaskReviewDrafts({ changeName, cwd, platformOpts =
|
|
|
1248
1459
|
// 有 ctx:主仓 HEAD 失败不阻断跨仓 task;head 留 null,主仓 task 后续按空 changedFiles 跳过
|
|
1249
1460
|
}
|
|
1250
1461
|
}
|
|
1462
|
+
// worktree-branch 归因的 base/head 锚定(task-03 / D-004):meta 缺失但分支 ref 活着时用
|
|
1463
|
+
// merge-base(主仓 HEAD, 分支 tip)=创建锚点..分支 tip 锚定——DB 判 worktree 的回退态仍能
|
|
1464
|
+
// 生成带真实 git 锚点的草稿(否则上方「无 meta.baseHash」早退白费分支归因成果)。分支 ref
|
|
1465
|
+
// 在主仓共享 .git 内,reviewGitDir=cwd 可解析,evidence 校验的 base..head diff 与归因同源。
|
|
1466
|
+
if (!base && attribution.branchBase && attribution.branchHead) {
|
|
1467
|
+
base = attribution.branchBase
|
|
1468
|
+
head = attribution.branchHead
|
|
1469
|
+
reviewGitDir = cwd
|
|
1470
|
+
}
|
|
1251
1471
|
|
|
1252
1472
|
const taskFiles = readdirSync(tasksDir).filter(f => /^task-\d+\.md$/.test(f)).sort()
|
|
1253
1473
|
let generated = 0
|
|
@@ -1652,6 +1872,89 @@ export async function adoptTaskReviewMechanics({ changeName, cwd, platformOpts =
|
|
|
1652
1872
|
* @param {object} [opts.platformOpts]
|
|
1653
1873
|
* @returns {Promise<{ok: boolean, reviewPath?: string, executeRunId?: string, errors: string[], warnings: string[]}>}
|
|
1654
1874
|
*/
|
|
1875
|
+
// ── ql-20260915-003 修复③(坑 interrupted-residue-detect,2026-09-15 用户实证:中断的
|
|
1876
|
+
// execute 子代理在工作区留下半成品 import 语法坏文件,无自动检测靠人工 diff 发现)──
|
|
1877
|
+
//
|
|
1878
|
+
// 对 changedFiles 中的 .js/.mjs/.cjs 逐个 `node --check`(子进程数组形式不经 shell,单文件
|
|
1879
|
+
// 5s 超时;总数 >60 抽样前 60 并提示——大变更全量查的耗时不可接受)。
|
|
1880
|
+
//
|
|
1881
|
+
// 判定分层(Node v24 实测:.js 的 file-check 受最近 package.json type 影响——无 type 时
|
|
1882
|
+
// ESM 半成品可能静默通过、合法 ESM 又会报 import 语句误错):
|
|
1883
|
+
// - .mjs/.cjs:file-check 权威(对应 module/commonjs 全量解析)
|
|
1884
|
+
// - .js:file-check **与** stdin `--input-type=module` check 双跑,两者都报 SyntaxError
|
|
1885
|
+
// 才判残留(合法 ESM / 合法 CJS(含 sloppy-mode)必有一路通过,零假阳;真断档半成品
|
|
1886
|
+
// 两路都挂)。stdin check 不可用/无 SyntaxError 字样 → fail-open 跳过该文件。
|
|
1887
|
+
//
|
|
1888
|
+
// 语法失败 → 返回「疑似中断残留」warning(advisory:只提醒,不改 verdict / 勾选语义 /
|
|
1889
|
+
// 落盘行为——quick --done 的 test/lint 实测硬门才是卡点,这里补「中断半成品」的可见性)。
|
|
1890
|
+
// 文件在候选根下不存在(diff 里的删除态文件等)直接跳过;超时(e.killed)无法定论不报警。
|
|
1891
|
+
// 仅 review write 时点执行一次,非热路径。
|
|
1892
|
+
const SYNTAX_CHECK_EXT_RE = /\.(?:js|mjs|cjs)$/
|
|
1893
|
+
const SYNTAX_CHECK_MAX_FILES = 60
|
|
1894
|
+
// 严格 .js(.mjs/.cjs 也 endsWith('.js'),须排除——否则 sloppy-mode .cjs 会误入 stdin ESM
|
|
1895
|
+
// 复核路径产生假阳)
|
|
1896
|
+
const isPlainJs = (f) => f.endsWith('.js') && !f.endsWith('.mjs') && !f.endsWith('.cjs')
|
|
1897
|
+
function _syntaxErrLine(e) {
|
|
1898
|
+
const raw = String((e.stderr && e.stderr.toString()) || e.message || '')
|
|
1899
|
+
const line = raw.split('\n').find(l => l.includes('SyntaxError'))
|
|
1900
|
+
return line ? line.trim() : null
|
|
1901
|
+
}
|
|
1902
|
+
function detectInterruptedSyntaxResidue(changedFiles, candidateRoots) {
|
|
1903
|
+
const warnings = []
|
|
1904
|
+
const jsFiles = (Array.isArray(changedFiles) ? changedFiles : [])
|
|
1905
|
+
.filter(f => typeof f === 'string' && SYNTAX_CHECK_EXT_RE.test(f))
|
|
1906
|
+
if (jsFiles.length === 0) return warnings
|
|
1907
|
+
const overCap = jsFiles.length > SYNTAX_CHECK_MAX_FILES
|
|
1908
|
+
for (const f of overCap ? jsFiles.slice(0, SYNTAX_CHECK_MAX_FILES) : jsFiles) {
|
|
1909
|
+
let abs = null
|
|
1910
|
+
for (const root of candidateRoots) {
|
|
1911
|
+
if (!root) continue
|
|
1912
|
+
const p = join(root, f)
|
|
1913
|
+
if (existsSync(p)) { abs = p; break }
|
|
1914
|
+
}
|
|
1915
|
+
if (!abs) continue
|
|
1916
|
+
try {
|
|
1917
|
+
execFileSync(process.execPath, ['--check', abs], { timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] })
|
|
1918
|
+
if (isPlainJs(f)) {
|
|
1919
|
+
// 无 type:module 上下文的 .js:file-check 通过不代表语法完整(Node v24 实测假阴),
|
|
1920
|
+
// 再跑 stdin ESM 全量解析兜底——两路都挂才判残留
|
|
1921
|
+
let src = ''
|
|
1922
|
+
try { src = readFileSync(abs, 'utf8') } catch { continue }
|
|
1923
|
+
try {
|
|
1924
|
+
execFileSync(process.execPath, ['--input-type=module', '--check'], { input: src, timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] })
|
|
1925
|
+
} catch (e2) {
|
|
1926
|
+
if (e2 && e2.killed) continue
|
|
1927
|
+
const line = _syntaxErrLine(e2)
|
|
1928
|
+
if (!line) continue // 非 SyntaxError(如版本不支持 stdin check)→ fail-open
|
|
1929
|
+
warnings.push(`⚠️ 疑似中断残留:${f} 语法错误(${line})——子代理异常退出可能留下半成品,请 diff 该文件确认还原或补完`)
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
} catch (e) {
|
|
1933
|
+
if (e && e.killed) continue // 单文件超时:无法定论,不产假阳 warning
|
|
1934
|
+
if (!isPlainJs(f)) {
|
|
1935
|
+
const line = _syntaxErrLine(e)
|
|
1936
|
+
if (line) warnings.push(`⚠️ 疑似中断残留:${f} 语法错误(${line})——子代理异常退出可能留下半成品,请 diff 该文件确认还原或补完`)
|
|
1937
|
+
continue
|
|
1938
|
+
}
|
|
1939
|
+
// .js file-check 挂可能是模块口径误错(合法 ESM 在无 type 上下文报 import 语句)——
|
|
1940
|
+
// stdin ESM 解析仲裁:也挂才算真残留
|
|
1941
|
+
let src = ''
|
|
1942
|
+
try { src = readFileSync(abs, 'utf8') } catch { continue }
|
|
1943
|
+
try {
|
|
1944
|
+
execFileSync(process.execPath, ['--input-type=module', '--check'], { input: src, timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] })
|
|
1945
|
+
} catch (e2) {
|
|
1946
|
+
if (e2 && e2.killed) continue
|
|
1947
|
+
const line = _syntaxErrLine(e2) || _syntaxErrLine(e)
|
|
1948
|
+
if (line) warnings.push(`⚠️ 疑似中断残留:${f} 语法错误(${line})——子代理异常退出可能留下半成品,请 diff 该文件确认还原或补完`)
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
if (overCap) {
|
|
1953
|
+
warnings.push(`⚠️ 中断残留检测抽样:changedFiles 含 ${jsFiles.length} 个 .js 系文件,仅语法检查前 ${SYNTAX_CHECK_MAX_FILES} 个(interrupted-residue-detect)`)
|
|
1954
|
+
}
|
|
1955
|
+
return warnings
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1655
1958
|
export async function writeTaskReview({
|
|
1656
1959
|
changeName, cwd, taskId, specVerdict, qualityVerdict,
|
|
1657
1960
|
reviewerNotes = '', requiredEvidence = [],
|
|
@@ -1758,22 +2061,30 @@ export async function writeTaskReview({
|
|
|
1758
2061
|
changedFiles = changedFilesOverride.map(p => String(p).replace(/\\/g, '/'))
|
|
1759
2062
|
warnings.push(`changedFiles 采用显式覆盖(${changedFiles.length} 个)——evidence 校验仍会与 git diff 相交比对`)
|
|
1760
2063
|
} else {
|
|
1761
|
-
|
|
1762
|
-
//
|
|
2064
|
+
// ── 归因模式分流(task-03 / D-004@v1):与草稿生成同源路由(判定源=changes.isolation_mode)──
|
|
2065
|
+
// worktree 模式(含 meta 缺失回退态)取 worktree 分支 diff,绝不落主仓共享脏窗口;
|
|
2066
|
+
// in-place/NULL 存量维持主仓窗口原口径。分流异常退回原 resolveVerifyChangedFiles(fail-open)。
|
|
2067
|
+
let attribution
|
|
1763
2068
|
try {
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
2069
|
+
attribution = resolveAttributionDiffFiles({ cwd, changeName, specBase, platformOpts })
|
|
2070
|
+
} catch (e) {
|
|
2071
|
+
attribution = { mode: 'main-window', diffFiles: resolveVerifyChangedFiles(cwd, changeName, null, { specBase }), notes: [`归因分流解析异常(${e && e.message ? e.message : e})——退回原口径`], meta: null, mergeGitDir: null }
|
|
2072
|
+
}
|
|
2073
|
+
for (const note of attribution.notes) warnings.push(`归因注记:${note}`)
|
|
2074
|
+
let diffFiles = attribution.diffFiles
|
|
2075
|
+
// 并入 worktree 未提交改动(与草稿生成同源口径:子代理默认不 commit)。D-004 后仅在
|
|
2076
|
+
// 归因源指向活跃 worktree 目录 / in-place meta(mergeGitDir)时执行——worktree-branch/
|
|
2077
|
+
// cleaned 归因的未提交并入已在 resolveAttributionDiffFiles 内按同口径完成。
|
|
2078
|
+
if (attribution.mergeGitDir) {
|
|
2079
|
+
try {
|
|
2080
|
+
const wtGitDir = attribution.mergeGitDir
|
|
1770
2081
|
const wtStatus = runGit(wtGitDir, ['status', '--porcelain', '--untracked-files=all'], { trim: false })
|
|
1771
2082
|
const wtFiles = parsePorcelainFiles(wtStatus)
|
|
1772
2083
|
.map(p => String(p).replace(/\\/g, '/'))
|
|
1773
2084
|
.filter(p => p !== '.sillyspec' && !p.startsWith('.sillyspec/'))
|
|
1774
2085
|
if (wtFiles.length > 0) diffFiles = [...new Set([...(Array.isArray(diffFiles) ? diffFiles : []), ...wtFiles])]
|
|
1775
|
-
}
|
|
1776
|
-
}
|
|
2086
|
+
} catch { /* 退回 commit diff 口径 */ }
|
|
2087
|
+
}
|
|
1777
2088
|
const mainDiff = Array.isArray(diffFiles) ? diffFiles : []
|
|
1778
2089
|
changedFiles = allowedPaths.length > 0
|
|
1779
2090
|
? mainDiff.filter(f => allowedPaths.some(p => pathMatches(f, p)))
|
|
@@ -1791,6 +2102,15 @@ export async function writeTaskReview({
|
|
|
1791
2102
|
}
|
|
1792
2103
|
}
|
|
1793
2104
|
|
|
2105
|
+
// ── ql-20260915-003 修复③(坑 interrupted-residue-detect):changedFiles 定稿后的
|
|
2106
|
+
// 语法检查(advisory warning,见 detectInterruptedSyntaxResidue 注释)。候选根依次试
|
|
2107
|
+
// reviewGitDir(meta 活跃时的 worktree 根)、cwd、worktree-branch 归因的孤儿目录兜底。
|
|
2108
|
+
for (const w of detectInterruptedSyntaxResidue(changedFiles, [
|
|
2109
|
+
reviewGitDir,
|
|
2110
|
+
cwd,
|
|
2111
|
+
join(cwd, '.sillyspec', '.runtime', 'worktrees', changeName),
|
|
2112
|
+
])) warnings.push(w)
|
|
2113
|
+
|
|
1794
2114
|
// ── 组装 + schema 自检 + 落盘 ──
|
|
1795
2115
|
const review = {
|
|
1796
2116
|
schemaVersion: REVIEW_SCHEMA_VERSION,
|