sillyspec 3.28.5 → 3.28.7
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/attachments/2ab2fd87cfa568ca6acadbb934a651d293a0ad8f3227c66a70b56dcf53a0ea3d.pptx +0 -0
- package/attachments/dc4c2b314ee0c295e1fe5243073eb5c7df7e429074ad7c14424523fbe86e6193.pptx +0 -0
- package/meta.json +11 -0
- package/package.json +1 -1
- package/sa-rename-probe/src/new.js +4 -0
- package/src/config-schema.js +26 -0
- package/src/decision-distill.js +14 -4
- package/src/dispatch/probe.js +8 -1
- package/src/docs-check.js +9 -5
- package/src/friction-tally.js +221 -0
- package/src/index.js +4 -1
- package/src/knowledge-match.js +90 -6
- package/src/progress/change-registry.js +28 -0
- package/src/progress.js +6 -0
- package/src/review-dispatch.js +7 -1
- package/src/run/command.js +17 -1
- package/src/run/complete-handlers.js +105 -2
- package/src/run/complete.js +53 -10
- package/src/run/gate-snapshot.js +73 -2
- package/src/run/gates.js +53 -16
- package/src/run/prompt.js +53 -1
- package/src/run/quick-audit.js +95 -5
- package/src/run/shared.js +29 -1
- package/src/run/stage.js +14 -0
- package/src/run/verify-quality-scan.js +52 -5
- package/src/scope-audit.js +308 -27
- package/src/semantic-guard.js +207 -0
- package/src/sillyhub-mcp/client.js +14 -0
- package/src/stages/brainstorm.js +3 -3
package/src/progress.js
CHANGED
|
@@ -908,6 +908,9 @@ export class ProgressManager {
|
|
|
908
908
|
// verifyStartAt 基准只读(2026-09-08-ir-verify-facts):execute 行 completed_at;无行 null 走 R-05 fallback
|
|
909
909
|
getStageCompletedAt(cwd, changeName, stage) { return this._changeRegistry.getStageCompletedAt(cwd, changeName, stage); }
|
|
910
910
|
|
|
911
|
+
// 最近进度活动时刻只读(D-002@v1 复潮):quick --done 自动归档闸的时近性信号
|
|
912
|
+
getLatestActivityAt(cwd, changeName) { return this._changeRegistry.getLatestActivityAt(cwd, changeName); }
|
|
913
|
+
|
|
911
914
|
// evidence mtime 窗口锚点(2026-09-10 用户反馈②):execute 行 started_at——证据合法产自 execute
|
|
912
915
|
// 或 verify 两窗口,锚「完成时刻」会把 execute 期间产的证据判旧(时序两难);无行 null 走 completed_at → R-05
|
|
913
916
|
getStageStartedAt(cwd, changeName, stage) { return this._changeRegistry.getStageStartedAt(cwd, changeName, stage); }
|
|
@@ -992,6 +995,9 @@ export class ProgressManager {
|
|
|
992
995
|
}
|
|
993
996
|
// quick 会话 id(quick-<uuid8>,见 run.js QUICK_SID_RE)只作 progress 的跨进程 session key,
|
|
994
997
|
// 进度存 SQL 不需要实体 change 目录——跳过避免 changes/quick-<uuid>/ 空目录残留。
|
|
998
|
+
// 注:日期前缀门禁(assertDatedChangeName)只在 CLI 边界强制(run --change / change-rename,
|
|
999
|
+
// 见 run/command.js + index.js)——initChange 是库函数,测试/平台工具合法用任意名建 fixture,
|
|
1000
|
+
// 不在此设门(2026-09-11 决策,避免 ~30 处测试 fixture 非日期名被拦)。
|
|
995
1001
|
if (!/^quick-[0-9a-f]{8}$/.test(changeName)) {
|
|
996
1002
|
this._ensureChangeDir(cwd, changeName);
|
|
997
1003
|
}
|
package/src/review-dispatch.js
CHANGED
|
@@ -262,7 +262,13 @@ export async function runReviewDispatch(opts = {}) {
|
|
|
262
262
|
const probeFn = probe || (await import('./dispatch/probe.js')).probeSillyHub
|
|
263
263
|
const pr = await probeFn({ client, worktreePath: null, cwd })
|
|
264
264
|
if (!pr || pr.available !== true) {
|
|
265
|
-
|
|
265
|
+
const why = pr && pr.reason ? pr.reason : 'probe 异常'
|
|
266
|
+
// token 失效给「重连再配对」出口(2026-09-11 用户实测:gateway/daemon 都活着,
|
|
267
|
+
// 死的只是凭据——一把 platform connect 就能救活平台通道,比直接降级更对症)。
|
|
268
|
+
const tokenHint = why === 'mcp-token-invalid'
|
|
269
|
+
? `mcp token 已失效(吊销/过期)——重跑 \`sillyspec platform connect ${client && client._url ? client._url : '<平台url>'} --token <user级token>\` 重连(connect 会成对签发 read+dispatch 新 token 自动覆盖写 mcp 段),然后重试平台通道。\n若暂不重连,按通道优先序降级:\n`
|
|
270
|
+
: ''
|
|
271
|
+
return { ok: false, reason: `平台通道不可用(${why})`, guidance: tokenHint + renderFallbackGuidance(cwd) }
|
|
266
272
|
}
|
|
267
273
|
// reviewRunId:优先复用 marker(与 prompt/gate 同链,勿手算)
|
|
268
274
|
let runId = reviewRunId
|
package/src/run/command.js
CHANGED
|
@@ -23,7 +23,7 @@ import { basename, join, resolve, dirname } from 'node:path'
|
|
|
23
23
|
import { existsSync, readdirSync, mkdirSync, writeFileSync, readFileSync, rmSync, unlinkSync } from 'node:fs'
|
|
24
24
|
import { randomBytes, randomUUID } from 'node:crypto'
|
|
25
25
|
import { writeAtomicSync } from '../fs-atomic.js'
|
|
26
|
-
import { resolveSpecDir, countAncestorSpecDirs, ancestorSpecDirs, resolveAncestorCeiling, resolveChangeDir, triggerSync, getStageSteps, formatWaitOptions, checkApproval, warnApprovalUnknown, didYouMean, assertSafeChangeName, detectQuickSessionDrift, detectWorktreeSpecDrift, resolveRuntimeRoot, resolveQuickSessionsDir, writePlatformPointer, checkPlatformManaged, isSelfReferentialSpecRoot, isTempResidueSpecRoot, PLATFORM_MANAGED_FILENAME, warnSelfRefPointerOnce } from './shared.js'
|
|
26
|
+
import { resolveSpecDir, countAncestorSpecDirs, ancestorSpecDirs, resolveAncestorCeiling, resolveChangeDir, triggerSync, getStageSteps, formatWaitOptions, checkApproval, warnApprovalUnknown, didYouMean, assertSafeChangeName, assertDatedChangeName, detectQuickSessionDrift, detectWorktreeSpecDrift, resolveRuntimeRoot, resolveQuickSessionsDir, writePlatformPointer, checkPlatformManaged, isSelfReferentialSpecRoot, isTempResidueSpecRoot, PLATFORM_MANAGED_FILENAME, warnSelfRefPointerOnce } from './shared.js'
|
|
27
27
|
import { resolveQuickLinkedChanges } from './quick-audit.js'
|
|
28
28
|
import { outputStep, collectStageWaitHistory } from './prompt.js'
|
|
29
29
|
import { completeStep, skipStep, waitStep, continueStep, synthesizeStepOutput } from './complete.js'
|
|
@@ -1046,6 +1046,22 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
1046
1046
|
process.exit(2) // 用法/环境错(目标变更不在当前库)→ exit 2
|
|
1047
1047
|
}
|
|
1048
1048
|
}
|
|
1049
|
+
// ── 变更名日期前缀门禁(brainstorm step6 规则 CLI 化,2026-09-11 实证 friction-signal-hint)──
|
|
1050
|
+
// 格式 YYYY-MM-DD-<简短描述> 此前只是 prompt 约束,agent 自拟名/重命名丢前缀时 CLI 照单
|
|
1051
|
+
// 全收静默物化。只拦「净新建」:DB 无行(!progress 已证)+ changes/(含 archive/)无目录;
|
|
1052
|
+
// 已物化存量(含归档无前缀旧名 auto-flow-optimization 等)不追诉,照常自愈初始化。
|
|
1053
|
+
// quick 阶段豁免:其 changeName 是会话 key(quick-<8hex>),非变更名。
|
|
1054
|
+
if (changeName && stageName !== 'quick' && !/^quick-[0-9a-f]{8}$/.test(changeName)
|
|
1055
|
+
&& !existsSync(join(specBase, 'changes', changeName))
|
|
1056
|
+
&& !existsSync(join(specBase, 'changes', 'archive', changeName))) {
|
|
1057
|
+
try {
|
|
1058
|
+
assertDatedChangeName(changeName)
|
|
1059
|
+
} catch (e) {
|
|
1060
|
+
console.error(`❌ ${e.message}`)
|
|
1061
|
+
console.error(` 重试:sillyspec run ${stageName} --change <YYYY-MM-DD-简短描述>`)
|
|
1062
|
+
process.exit(2) // 用法错(新变更名格式非法)→ exit 2
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1049
1065
|
// 如果指定了变更名或有变更目录,自动初始化变更的 progress
|
|
1050
1066
|
const autoChange = changeName || resolveChangeNameAuto(cwd, specRoot)
|
|
1051
1067
|
if (autoChange) {
|
|
@@ -22,8 +22,9 @@
|
|
|
22
22
|
* archiveChangeDirectory 的 6 处 process.exit 全 exit(1):5 个顶层 guard 直接终止;L1325 在 catch 内主动 exit
|
|
23
23
|
* (非被外层吞);process.exit 不可被 try 捕获 → 搬迁行为完全等价。
|
|
24
24
|
*/
|
|
25
|
-
import { basename, join, resolve, relative, isAbsolute } from 'node:path'
|
|
25
|
+
import { basename, dirname, join, resolve, relative, isAbsolute } from 'node:path'
|
|
26
26
|
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, rmSync } from 'node:fs'
|
|
27
|
+
import { createHash } from 'node:crypto'
|
|
27
28
|
import { renameSyncRetry, writeAtomicSync } from '../fs-atomic.js'
|
|
28
29
|
import { gitQuiet } from '../git-helper.js'
|
|
29
30
|
import { resolveChangeDir, resolveQuickSessionsDir, safeGit, auditQuickCompletion, triggerSync, isQuickMetadata, isQuicklogFileLineNoise, resolveRuntimeRoot, collectOtherQuickSessionDeclarations, mergeQuickBoundaryFiles } from './shared.js'
|
|
@@ -35,6 +36,7 @@ import { validateQuickResult, allocateQuicklogEntry, appendQuicklogEntryWithId,
|
|
|
35
36
|
import { getRule } from '../stage-contract-spec.js'
|
|
36
37
|
import { archiveDestDirName } from '../stage-contract.js'
|
|
37
38
|
import { collectNumstatByPath } from '../scope-audit.js'
|
|
39
|
+
import { recordFrictionEvent, consumeFrictionHint } from '../friction-tally.js'
|
|
38
40
|
|
|
39
41
|
/**
|
|
40
42
|
* 清洗项目名:只保留 ASCII 字母/数字/横线/下划线/点,过滤中文和特殊字符。
|
|
@@ -170,6 +172,13 @@ export function pruneArchivedChangeRuntime(runtimeRoot, changeName) {
|
|
|
170
172
|
if (existsSync(pathspec)) gone(pathspec)
|
|
171
173
|
} catch {}
|
|
172
174
|
|
|
175
|
+
// friction tally(friction-signal-hint FR-05):摩擦计数随变更终态一并回收——真实变更的
|
|
176
|
+
// friction-tally-<change>.json 落 runtimeRoot,变更归档/删除后无读者,残留即孤儿文件。
|
|
177
|
+
try {
|
|
178
|
+
const fp = join(runtimeRoot, `friction-tally-${changeName}.json`)
|
|
179
|
+
if (existsSync(fp)) gone(fp)
|
|
180
|
+
} catch {}
|
|
181
|
+
|
|
173
182
|
try {
|
|
174
183
|
const execDir = join(runtimeRoot, 'execute-runs')
|
|
175
184
|
if (existsSync(execDir)) {
|
|
@@ -1056,6 +1065,9 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
|
|
|
1056
1065
|
steps[currentIdx].status = 'pending'
|
|
1057
1066
|
steps[currentIdx].completedAt = null
|
|
1058
1067
|
if (outputText) steps[currentIdx].output = null
|
|
1068
|
+
// 摩擦计数(friction-signal-hint task-04):quick 审计被拦也是摩擦信号——exit 前记
|
|
1069
|
+
// 一笔(changeName=quick 会话 id,tally 落 session 目录,成功收尾 consume 后随目录清理)。
|
|
1070
|
+
recordFrictionEvent({ cwd, changeName, platformOpts, type: 'gate_rollback', detail: 'quick-audit' })
|
|
1059
1071
|
process.exit(1)
|
|
1060
1072
|
}
|
|
1061
1073
|
// P0-2(2026-09-02 跨 agent 工单):触及 src/test 的 quick --done 内置 test+lint
|
|
@@ -1076,6 +1088,9 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
|
|
|
1076
1088
|
steps[currentIdx].status = 'pending'
|
|
1077
1089
|
steps[currentIdx].completedAt = null
|
|
1078
1090
|
if (outputText) steps[currentIdx].output = null
|
|
1091
|
+
// 摩擦计数(friction-signal-hint task-04):test/lint 实测门禁拦下同记摩擦——exit 前
|
|
1092
|
+
// 落 session tally,与 quick-audit 同款静默降级。
|
|
1093
|
+
recordFrictionEvent({ cwd, changeName, platformOpts, type: 'gate_rollback', detail: 'quick-test-lint' })
|
|
1079
1094
|
process.exit(1)
|
|
1080
1095
|
}
|
|
1081
1096
|
progress.lastQuickReview = review
|
|
@@ -1223,6 +1238,10 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
|
|
|
1223
1238
|
}
|
|
1224
1239
|
if (review?.docsCheckHint && review.docsCheckHint.invalid > 0) {
|
|
1225
1240
|
auditNotes.push(`📎 文档引用失效:${review.docsCheckHint.invalid}/${review.docsCheckHint.total} 处 file:line 失效(sillyspec docs check 可复现)`)
|
|
1241
|
+
// 逐条指名(2026-09-11 同 quick-audit 侧:门禁输出直接带文件:行号,封顶 5 保持注记紧凑)
|
|
1242
|
+
for (const i of (review.docsCheckHint.invalidRefs || []).slice(0, 5)) {
|
|
1243
|
+
auditNotes.push(` ❌ [${i.doc}:${i.docLine}] ${i.ref} → ${i.reason}`)
|
|
1244
|
+
}
|
|
1226
1245
|
// 行号漂移自动重锚(2026-09-08 用户反馈②:活文档 file:line 硬编码随任何插入失效,
|
|
1227
1246
|
// 每回手跑 docs check --fix 是机械活):autoReanchorDocRefs = --fix 主链路编程化封装
|
|
1228
1247
|
// (fixable 唯一/优选命中才改 + 定点替换 + 同口径回执),只作用于本次改动的文档。
|
|
@@ -1266,6 +1285,59 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
|
|
|
1266
1285
|
softFiles,
|
|
1267
1286
|
})
|
|
1268
1287
|
console.log(`📝 QUICKLOG 条目 ${qlId} 已标记完成`)
|
|
1288
|
+
// 范围快照/patch 审计级落盘(quick-359a48f1):quicklog/patches/<qlId>.json+.patch——按
|
|
1289
|
+
// ql-ID 对齐 QUICKLOG 条目(平台按条目抓取),json 冗余 sessionId 供 guard 清理后记录态
|
|
1290
|
+
// 反查(computeQuickAudit → findQuickPatchRecord 同链)。patch 为 --done 时点全量冻结
|
|
1291
|
+
// (HEAD 未提交窗口,untracked 自拼 hunk)。fail-soft:失败只提示,不阻断收尾。
|
|
1292
|
+
try {
|
|
1293
|
+
const { computeChangeScopeAudit, collectNumstatByPath, buildFrozenPatch } = await import('../scope-audit.js')
|
|
1294
|
+
// changeName == sessionId(handleQuickStageCompletion 作用域内,§4.6 注释同款约定)
|
|
1295
|
+
const snapResult = await computeChangeScopeAudit({ cwd, specBase, changeName, platformOpts, collectPatch: true })
|
|
1296
|
+
let quickRows = snapResult && snapResult.mode === 'quick' && Array.isArray(snapResult.rows)
|
|
1297
|
+
? snapResult.rows.filter(r => r && r.attribution !== 'undeclared')
|
|
1298
|
+
: null
|
|
1299
|
+
let frozenPatch = snapResult ? snapResult.frozenPatch : undefined
|
|
1300
|
+
// 窗口空兜底(baseline 会话常态):--files 声明文件是会话启动时预存脏(audit baseline
|
|
1301
|
+
// 剔除后窗口空,quickRows 空),但声明即归属——改用声明文件集构造 rows + patch(行数按
|
|
1302
|
+
// HEAD 未提交窗口采集,同口径)。
|
|
1303
|
+
if ((!quickRows || quickRows.length === 0) && Array.isArray(guard?.allowedFiles) && guard.allowedFiles.length > 0) {
|
|
1304
|
+
const sessionRoot = dirname(specBase)
|
|
1305
|
+
const declaredPosix = guard.allowedFiles.map(f => String(f).replace(/\\/g, '/')).filter(Boolean)
|
|
1306
|
+
const stats = collectNumstatByPath(sessionRoot, declaredPosix, { baseRef: 'HEAD' })
|
|
1307
|
+
quickRows = declaredPosix.filter(f => stats.has(f) || existsSync(join(sessionRoot, f)))
|
|
1308
|
+
.map(f => {
|
|
1309
|
+
const st = stats.get(f) || { additions: null, deletions: null, kind: existsSync(join(sessionRoot, f)) ? 'modified' : 'deleted' }
|
|
1310
|
+
return { path: f, declared: true, additions: st.additions, deletions: st.deletions, kind: st.kind, attribution: 'declared' }
|
|
1311
|
+
})
|
|
1312
|
+
frozenPatch = buildFrozenPatch(sessionRoot, quickRows.map(r => r.path), { baseRef: 'HEAD' })
|
|
1313
|
+
}
|
|
1314
|
+
if (quickRows && quickRows.length > 0) {
|
|
1315
|
+
const patchesDir = join(specBase, 'quicklog', 'patches')
|
|
1316
|
+
mkdirSync(patchesDir, { recursive: true })
|
|
1317
|
+
const { frozenPatch: _fp, ...snapRest } = snapResult || {}
|
|
1318
|
+
const snapObj = {
|
|
1319
|
+
...snapRest,
|
|
1320
|
+
rows: quickRows,
|
|
1321
|
+
totals: { files: quickRows.length, additions: quickRows.reduce((n, r) => n + (Number.isFinite(r.additions) ? r.additions : 0), 0), deletions: quickRows.reduce((n, r) => n + (Number.isFinite(r.deletions) ? r.deletions : 0), 0) },
|
|
1322
|
+
qlId,
|
|
1323
|
+
sessionId: changeName,
|
|
1324
|
+
savedAt: new Date().toISOString(),
|
|
1325
|
+
}
|
|
1326
|
+
snapObj.note = 'quick --done 时点冻结(本文件落盘时采集)'
|
|
1327
|
+
if (typeof frozenPatch === 'string' && frozenPatch) {
|
|
1328
|
+
const patchText = frozenPatch.endsWith('\n') ? frozenPatch : frozenPatch + '\n'
|
|
1329
|
+
writeFileSync(join(patchesDir, `${qlId}.patch`), patchText)
|
|
1330
|
+
snapObj.patchSha256 = createHash('sha256').update(patchText, 'utf8').digest('hex')
|
|
1331
|
+
snapObj.patchStatus = 'ok'
|
|
1332
|
+
} else if (frozenPatch === null) {
|
|
1333
|
+
snapObj.patchStatus = 'failed'
|
|
1334
|
+
}
|
|
1335
|
+
writeFileSync(join(patchesDir, `${qlId}.json`), JSON.stringify(snapObj, null, 2) + '\n')
|
|
1336
|
+
console.log(`📦 范围快照已落 quicklog/patches/${qlId}.json${snapObj.patchStatus === 'ok' ? ' + .patch(--done 时点冻结,sha256 已锚)' : snapObj.patchStatus === 'failed' ? '——patch 采集失败已留痕' : ''}`)
|
|
1337
|
+
}
|
|
1338
|
+
} catch (e) {
|
|
1339
|
+
console.warn(` ⚠️ quick 范围快照落盘失败(不阻断收尾):${e && e.message ? String(e.message).split('\n')[0] : e}`)
|
|
1340
|
+
}
|
|
1269
1341
|
// 刷新 DB title:从 step3「需求:」提取(agent 可改 title 的途径),覆盖启动时的兜底快照。
|
|
1270
1342
|
// 与 QUICKLOG 标题刷新(flipEntryInContent 内 extractTitleFromResult)同源,保持 DB↔QUICKLOG 一致。
|
|
1271
1343
|
try {
|
|
@@ -1291,6 +1363,14 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
|
|
|
1291
1363
|
console.warn(`⚠️ 关联变更轻量归档失败(不阻断 quick 完成): ${e.message}`)
|
|
1292
1364
|
}
|
|
1293
1365
|
|
|
1366
|
+
// 摩擦信号消费(friction-signal-hint task-04):必须在 session 目录清理**前**——清理后
|
|
1367
|
+
// tally 随目录删除,晚于此就读不到。QUICKLOG 完成打印之后 consume,提示里的 postmortem
|
|
1368
|
+
// 建议有落点可循;全零/读失败零输出,摩擦提示失败不影响收尾。
|
|
1369
|
+
try {
|
|
1370
|
+
const fr = consumeFrictionHint({ cwd, changeName, platformOpts })
|
|
1371
|
+
if (fr && fr.hint) console.log(`\n${fr.hint}`)
|
|
1372
|
+
} catch { /* 摩擦提示失败不影响收尾 */ }
|
|
1373
|
+
|
|
1294
1374
|
// 清理 session 目录(rmSync/unlinkSync 容忍不存在)。路径与写入对齐(Q4 resolveQuickSessionsDir)。
|
|
1295
1375
|
try {
|
|
1296
1376
|
if (changeName) {
|
|
@@ -1392,6 +1472,10 @@ async function closeSingleQuickLinkedChange({ pm, cwd, specBase, changeName, pla
|
|
|
1392
1472
|
*/
|
|
1393
1473
|
const QUICK_CLOSE_ALLOWED_STAGES = new Set(['', 'scan', 'brainstorm'])
|
|
1394
1474
|
|
|
1475
|
+
/** 时近性闸窗口(缺陷②复潮实现):最近进度活动 60 分钟内的变更不自动归档——活跃会话
|
|
1476
|
+
* 步进间隔为分钟级(brainstorm 8 步通常 <1h),僵尸的最后活动以小时/天计。 */
|
|
1477
|
+
const QUICK_CLOSE_ACTIVITY_WINDOW_MS = 60 * 60 * 1000
|
|
1478
|
+
|
|
1395
1479
|
/**
|
|
1396
1480
|
* quick --done 完成后,自动关闭任务已全部完成的关联真实变更。
|
|
1397
1481
|
* quick-<hex> sessionId 自身不在此处理(由调用方单独注销)。
|
|
@@ -1441,6 +1525,25 @@ export async function closeQuickLinkedChanges({ pm, cwd, specBase, linkedChanges
|
|
|
1441
1525
|
})
|
|
1442
1526
|
continue
|
|
1443
1527
|
}
|
|
1528
|
+
// 时近性闸(quick-done-autoarchive-misfire 缺陷②,D-002@v1 复潮,2026-09-11
|
|
1529
|
+
// cross-change-decision-guard 实证):阶段态区分不了「活跃在途」与「启动后弃单」——
|
|
1530
|
+
// brainstorm in_progress + tasks.md 仅含他者 quick 行时「全勾」是空洞真值(完整流程
|
|
1531
|
+
// plan 前无自有任务行),关联 quick --done 勾掉 ql 行即触发在途变更被轻量归档。
|
|
1532
|
+
// 信号取时近性:最近一次 stage/step 完成时刻在窗口内 = 会话分钟级在推进(在途),
|
|
1533
|
+
// 不自动归档;僵尸的最后活动必然陈旧(或无完成步 → null),逃生通道语义不变
|
|
1534
|
+
//(既有「真·僵尸 in_progress→closed」行为保留)。pm 无此方法(旧 mock/旧进度库)
|
|
1535
|
+
// 按无近期活动放行——与 getChangeStage 缺失 skip 的 fail-safe 不同向:本闸误放行的
|
|
1536
|
+
// 最坏后果是回到缺陷②现状(有 D-002 防护兜底),误拦截则僵尸永不清——权衡取放行。
|
|
1537
|
+
if (typeof pm.getLatestActivityAt === 'function') {
|
|
1538
|
+
const latest = pm.getLatestActivityAt(cwd, changeName)
|
|
1539
|
+
if (latest && Date.now() - new Date(latest).getTime() < QUICK_CLOSE_ACTIVITY_WINDOW_MS) {
|
|
1540
|
+
skipped.push({
|
|
1541
|
+
name: changeName,
|
|
1542
|
+
reason: `变更 ${Math.round((Date.now() - new Date(latest).getTime()) / 60000)} 分钟内仍有进度活动(在途会话,非僵尸),不自动归档——确已弃单请走原流程收尾或窗口过后重试`,
|
|
1543
|
+
})
|
|
1544
|
+
continue
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1444
1547
|
if (!isChangeTasksComplete(specBase, changeName)) {
|
|
1445
1548
|
skipped.push({ name: changeName, reason: 'tasks.md 未全勾选或不存在' })
|
|
1446
1549
|
continue
|
|
@@ -1851,7 +1954,7 @@ export async function handleScanStageCompleted({ stageName, currentIdx, cwd, pro
|
|
|
1851
1954
|
// untracked 新文件 wc-l 记全 + 行、binary 显 BIN)。纯展示列(advisory,D-006):采集失败/行数
|
|
1852
1955
|
// 不可得 → 括注省略不出伪数据;不改 auditQuickCompletion 判定、runQuickTestLintGate 与任何门禁。
|
|
1853
1956
|
// 放文件尾(函数声明提升)而不内联进 handleQuickStageCompletion:docs/platform-interface-map
|
|
1854
|
-
// 行号锚在上方(complete-handlers.js:
|
|
1957
|
+
// 行号锚在上方(complete-handlers.js:1695 handleScanStageCompleted),内联大块会把 doc-ref 锚
|
|
1855
1958
|
// 推出关键词窗口。
|
|
1856
1959
|
//
|
|
1857
1960
|
// 消费分工(调用方 quick 收尾):
|
package/src/run/complete.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { join } from 'node:path'
|
|
20
20
|
import { existsSync, readFileSync, mkdirSync, writeFileSync, appendFileSync } from 'node:fs'
|
|
21
|
+
import { createHash } from 'node:crypto'
|
|
21
22
|
import { writeAtomicSync } from '../fs-atomic.js'
|
|
22
23
|
import { gitQuiet } from '../git-helper.js'
|
|
23
24
|
import { withFileLock } from '../quicklog.js'
|
|
@@ -666,6 +667,17 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
666
667
|
// verify 完成出一行漂移确认。advisory fail-soft(D-006)——helper 内部全兜,绝不阻断完成。
|
|
667
668
|
await printStageCompletionScopeAudit({ stageName, cwd, changeName, specBase, platformOpts })
|
|
668
669
|
|
|
670
|
+
// 摩擦信号消费(friction-signal-hint task-03):verify 收尾读一次 friction tally,非零打
|
|
671
|
+
// 一行 advisory 引导补 postmortem(consume 内部提示后清零);全零/读失败零输出——
|
|
672
|
+
// 摩擦提示失败不影响收尾。
|
|
673
|
+
if (stageName === 'verify') {
|
|
674
|
+
try {
|
|
675
|
+
const { consumeFrictionHint } = await import('../friction-tally.js')
|
|
676
|
+
const r = consumeFrictionHint({ cwd, changeName, platformOpts })
|
|
677
|
+
if (r && r.hint) console.log(`\n${r.hint}`)
|
|
678
|
+
} catch { /* 摩擦提示失败不影响收尾 */ }
|
|
679
|
+
}
|
|
680
|
+
|
|
669
681
|
if (stageName === 'execute') {
|
|
670
682
|
// execute run summary:展示真实可得的结构化信息
|
|
671
683
|
try {
|
|
@@ -804,24 +816,41 @@ function scopeAuditPathSet(result) {
|
|
|
804
816
|
return set
|
|
805
817
|
}
|
|
806
818
|
|
|
807
|
-
/** execute 完成路径:全表 +
|
|
819
|
+
/** execute 完成路径:全表 + 审计级快照/patch 落盘(quick-359a48f1:changes/<变更名>/ 随归档
|
|
820
|
+
* 入库;patch 为收尾时点全量冻结——--file 真·当时内容比对的数据源。写失败只提示,verify
|
|
821
|
+
* 对比按无快照降级。审查修复:freshActual 绕过 settled 快照捷径防陈旧回写(execute 重跑
|
|
822
|
+
* 场景 C-F01);落盘 note 换冻结措辞防「自述漂移」污染(C-F06);patchSha256/patchStatus
|
|
823
|
+
* 留防篡改锚与失败痕(A-F01/C-F07) */
|
|
808
824
|
async function printExecuteScopeAudit({ cwd, changeName, specBase, platformOpts, computeChangeScopeAudit, renderScopeAuditTable }) {
|
|
809
|
-
const result = await computeChangeScopeAudit({ cwd, specBase, changeName, platformOpts })
|
|
825
|
+
const result = await computeChangeScopeAudit({ cwd, specBase, changeName, platformOpts, collectPatch: true, freshActual: true })
|
|
810
826
|
console.log(`\n${renderScopeAuditTable(result, { maxRows: 60 })}`)
|
|
811
827
|
try {
|
|
812
|
-
|
|
813
|
-
const
|
|
814
|
-
|
|
815
|
-
|
|
828
|
+
// 归档竞态(审查 C-F10):目录已被并行归档移动 → 写归档侧,不在活跃区重建假目录
|
|
829
|
+
const activeDir = join(specBase, 'changes', changeName)
|
|
830
|
+
const changeDir = existsSync(activeDir) ? activeDir : join(specBase, 'changes', 'archive', changeName)
|
|
831
|
+
mkdirSync(changeDir, { recursive: true })
|
|
832
|
+
const { frozenPatch, ...snap } = result
|
|
833
|
+
snap.note = 'execute --done 时点冻结(本文件落盘时采集)'
|
|
834
|
+
if (typeof frozenPatch === 'string') {
|
|
835
|
+
const patchText = frozenPatch.endsWith('\n') ? frozenPatch : frozenPatch + '\n'
|
|
836
|
+
writeFileSync(join(changeDir, 'scope-audit.patch'), patchText)
|
|
837
|
+
snap.patchSha256 = createHash('sha256').update(patchText, 'utf8').digest('hex')
|
|
838
|
+
snap.patchStatus = 'ok'
|
|
839
|
+
} else if (frozenPatch === null) {
|
|
840
|
+
snap.patchStatus = 'failed'
|
|
841
|
+
}
|
|
842
|
+
writeFileSync(join(changeDir, 'scope-audit.json'), JSON.stringify({ ...snap, savedAt: new Date().toISOString() }, null, 2) + '\n')
|
|
843
|
+
console.log(` 📦 范围快照已落变更目录(scope-audit.json${snap.patchStatus === 'ok' ? ' + scope-audit.patch(收尾时点冻结,sha256 已锚)' : snap.patchStatus === 'failed' ? '——patch 采集失败已留痕' : ''})`)
|
|
816
844
|
} catch (e) {
|
|
817
845
|
console.warn(` ⚠️ 范围对账快照写入失败(不阻断,verify 漂移对比将按无快照降级):${e && e.message ? String(e.message).split('\n')[0] : e}`)
|
|
818
846
|
}
|
|
819
847
|
}
|
|
820
848
|
|
|
821
849
|
/** verify 完成路径:读 execute 时点快照对比文件集差集,一行结论(行数不比——execute 形态 A 锚
|
|
822
|
-
* worktree meta、verify 形态 B 锚 merge-base
|
|
850
|
+
* worktree meta、verify 形态 B 锚 merge-base,基点不同行数天然抖动,比行数必假阳)。
|
|
851
|
+
* 审查 C-F01:current 侧 freshActual——settled 快照捷径会让本函数变成快照比快照恒「一致」 */
|
|
823
852
|
async function printVerifyScopeDrift({ cwd, changeName, specBase, platformOpts, computeChangeScopeAudit }) {
|
|
824
|
-
const result = await computeChangeScopeAudit({ cwd, specBase, changeName, platformOpts })
|
|
853
|
+
const result = await computeChangeScopeAudit({ cwd, specBase, changeName, platformOpts, freshActual: true })
|
|
825
854
|
const totals = result?.totals && typeof result.totals === 'object' ? result.totals : {}
|
|
826
855
|
const scopeLine = `变更范围:${totals.files ?? (Array.isArray(result?.rows) ? result.rows.length : 0)} 文件 +${totals.additions ?? 0}/-${totals.deletions ?? 0}`
|
|
827
856
|
if (result?.ok !== true) {
|
|
@@ -829,9 +858,14 @@ async function printVerifyScopeDrift({ cwd, changeName, specBase, platformOpts,
|
|
|
829
858
|
return
|
|
830
859
|
}
|
|
831
860
|
let snapshot = null
|
|
861
|
+
// 快照读取链(quick-359a48f1):变更目录(审计级,execute --done 落)→ .runtime(存量兼容)
|
|
832
862
|
try {
|
|
833
|
-
snapshot = JSON.parse(readFileSync(join(
|
|
834
|
-
} catch {
|
|
863
|
+
snapshot = JSON.parse(readFileSync(join(specBase, 'changes', changeName, 'scope-audit.json'), 'utf8'))
|
|
864
|
+
} catch {
|
|
865
|
+
try {
|
|
866
|
+
snapshot = JSON.parse(readFileSync(join(resolveRuntimeRoot(platformOpts, specBase), `scope-audit-${changeName}.json`), 'utf8'))
|
|
867
|
+
} catch { /* 快照缺失/损坏 → 跳过对比只打当前 totals */ }
|
|
868
|
+
}
|
|
835
869
|
if (!snapshot || snapshot.ok !== true) {
|
|
836
870
|
console.log(` ${scopeLine}(无 execute 时点快照可对比——完整表跑 sillyspec scope-audit --change ${changeName})`)
|
|
837
871
|
return
|
|
@@ -1474,6 +1508,15 @@ export async function continueStep(pm, progress, stageName, cwd, answer, options
|
|
|
1474
1508
|
// task-04:wait 解除完成路径与 completeStep 完成分支同源注入(Grill G-2:本路径不漏,
|
|
1475
1509
|
// 否则 execute 快照缺失 → verify 假报「无快照可对比」)。
|
|
1476
1510
|
await printStageCompletionScopeAudit({ stageName, cwd, changeName, specBase, platformOpts })
|
|
1511
|
+
// 摩擦信号消费(friction-signal-hint task-03):与 completeStep 完成分支同款(Grill CC-03:
|
|
1512
|
+
// 漏此则 --continue 收尾丢提示丢清零)。全零/读失败零输出,摩擦提示失败不影响收尾。
|
|
1513
|
+
if (stageName === 'verify') {
|
|
1514
|
+
try {
|
|
1515
|
+
const { consumeFrictionHint } = await import('../friction-tally.js')
|
|
1516
|
+
const r = consumeFrictionHint({ cwd, changeName, platformOpts })
|
|
1517
|
+
if (r && r.hint) console.log(`\n${r.hint}`)
|
|
1518
|
+
} catch { /* 摩擦提示失败不影响收尾 */ }
|
|
1519
|
+
}
|
|
1477
1520
|
// 阶段完成后明确下一步(agent 常卡:stageData completed 但不知要 run <下一阶段> 推进 currentStage)
|
|
1478
1521
|
const nextStageHint = { brainstorm: 'plan', plan: 'execute', execute: 'verify', verify: 'archive' }[stageName]
|
|
1479
1522
|
if (nextStageHint) {
|
package/src/run/gate-snapshot.js
CHANGED
|
@@ -28,7 +28,7 @@ function git(cwd, args) {
|
|
|
28
28
|
* @param {{ cwd: string, files: string[] }} opts cwd=主仓根;files=本会话变更文件(仓库根相对 POSIX)
|
|
29
29
|
* @returns {{ snapshotRoot: string, cleanup: () => void, reason?: string }|null} 失败返回 null(调用方回退主仓)
|
|
30
30
|
*/
|
|
31
|
-
export function createGateSnapshot({ cwd, files }) {
|
|
31
|
+
export function createGateSnapshot({ cwd, files, sourceRoot = null }) {
|
|
32
32
|
let snapshotRoot = null
|
|
33
33
|
try {
|
|
34
34
|
// 前置:主仓须是 git 仓且有 HEAD(无 git 环境回退主仓现行为)
|
|
@@ -38,16 +38,19 @@ export function createGateSnapshot({ cwd, files }) {
|
|
|
38
38
|
git(cwd, ['worktree', 'add', '--detach', '--quiet', snapshotRoot, 'HEAD'])
|
|
39
39
|
|
|
40
40
|
// 会话文件 overlay:主仓工作区版本覆盖进快照(本会话的最新态;文件不存在=已删,跳过)。
|
|
41
|
+
// sourceRoot(verify 门定向跑用,2026-09-10 驾驭小结第八批):overlay 源切换为本变更
|
|
42
|
+
// worktree 根——「--worktree 定向跑本变更分支内容」的快照实现(缺省仍是主仓 cwd)。
|
|
41
43
|
// .sillyspec/ 跳过面收窄(P2-d 落地实证的快照盲区):原一刀切跳过整个 .sillyspec/ ——
|
|
42
44
|
// 会话声明的 tracked docs(module-map 补录/知识库/模块卡)进不了快照,门禁读到 HEAD
|
|
43
45
|
// 旧版恒误报(src 未录 module-map 的修复在主仓生效、快照里仍然失败)。真正需要隔离的
|
|
44
46
|
// 是共享运行时面:.runtime/(sqlite 锁/在途 marker)与 quicklog/(跨会话共享账本);
|
|
45
47
|
// local.yaml 由下方 cfg 段显式复制(单一来源,不经 overlay)。
|
|
48
|
+
const overlayRoot = sourceRoot || cwd
|
|
46
49
|
let overlaid = 0
|
|
47
50
|
for (const f of files) {
|
|
48
51
|
const isSillyspecRuntime = typeof f === 'string' && (f.startsWith('.sillyspec/.runtime/') || f.startsWith('.sillyspec/quicklog/') || f === '.sillyspec/local.yaml' || f === '.sillyspec/.sillyspec-platform.json')
|
|
49
52
|
if (typeof f !== 'string' || f.includes('..') || f.startsWith('/') || isSillyspecRuntime) continue
|
|
50
|
-
const src = join(
|
|
53
|
+
const src = join(overlayRoot, f)
|
|
51
54
|
if (!existsSync(src)) continue
|
|
52
55
|
const dst = join(snapshotRoot, f)
|
|
53
56
|
mkdirSync(dirname(dst), { recursive: true })
|
|
@@ -86,3 +89,71 @@ export function createGateSnapshot({ cwd, files }) {
|
|
|
86
89
|
return null
|
|
87
90
|
}
|
|
88
91
|
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* verify 门专用快照(2026-09-10 驾驭小结第八批,用户第二次真实阻塞:verify 实测门跑
|
|
95
|
+
* main 工作区,多会话并发任何人的 WIP 都能弄红别人的门)。等价「--worktree 定向跑」:
|
|
96
|
+
* overlay 文件集 = resolveVerifyChangedFiles(worktree-aware:含 merge-base 补齐与
|
|
97
|
+
* working-tree),overlay 源 = 本变更 worktree 根(meta 感知;in-place 退主仓 cwd)——
|
|
98
|
+
* 并行会话的在途文件物理不进快照。变更文档(changes/<name>/**,test_strategy 的
|
|
99
|
+
* module-impact.md 等消费)从主仓 specBase 随快照复制。
|
|
100
|
+
* @param {{ cwd: string, changeName: string, specBase: string, platformOpts?: object }} opts
|
|
101
|
+
* @returns {Promise<{snapshotRoot: string, cleanup: () => void, overlaid: number, changeFileCount: number}|null>}
|
|
102
|
+
*/
|
|
103
|
+
export async function createVerifyGateSnapshot({ cwd, changeName, specBase, platformOpts = {} }) {
|
|
104
|
+
try {
|
|
105
|
+
const { resolveVerifyChangedFiles } = await import('../verify-postcheck.js')
|
|
106
|
+
const { resolveRuntimeRoot } = await import('./shared.js')
|
|
107
|
+
const { splitOwnVsForeignDiffFiles } = await import('../foreign-declared.js')
|
|
108
|
+
let changeFiles = resolveVerifyChangedFiles(cwd, changeName, null, {
|
|
109
|
+
specBase, includeWorkingTree: true,
|
|
110
|
+
}) || []
|
|
111
|
+
if (changeFiles.length === 0) return null // 无变更文件集 → 无从定向,回退主仓
|
|
112
|
+
// 他者声明归属过滤(坑 verify-reconcile-foreign-wip 同款):in-place 模式 working-tree
|
|
113
|
+
// 并入后主仓全部在途文件进场——他者活跃变更显式声明的文件(quick --files / 他者 design
|
|
114
|
+
// 清单)剔除,只留本变更相关面(无主文件保留,fail-closed 口径与 probe6 一致)。
|
|
115
|
+
try {
|
|
116
|
+
const runtimeRoot = resolveRuntimeRoot(platformOpts, specBase)
|
|
117
|
+
const { foreign } = splitOwnVsForeignDiffFiles(cwd, changeName, changeFiles, { specBase, runtimeRoot })
|
|
118
|
+
if (foreign.length > 0) {
|
|
119
|
+
const foreignSet = new Set(foreign.map(x => x.file))
|
|
120
|
+
changeFiles = changeFiles.filter(f => !foreignSet.has(f))
|
|
121
|
+
if (changeFiles.length === 0) return null
|
|
122
|
+
}
|
|
123
|
+
} catch { /* 过滤失败退全量(fail-closed:宁可多 overlay 不漏本变更文件) */ }
|
|
124
|
+
|
|
125
|
+
// overlay 源:worktree meta 感知(native worktree 在则从 worktree 根取「本变更分支内容」)
|
|
126
|
+
let sourceRoot = null
|
|
127
|
+
try {
|
|
128
|
+
const { WorktreeManager } = await import('../worktree.js')
|
|
129
|
+
const wm = new WorktreeManager({ cwd })
|
|
130
|
+
const meta = wm.getMeta(changeName)
|
|
131
|
+
if (meta && meta.worktreePath && meta.mode !== 'in-place-fallback' && existsSync(meta.worktreePath)) {
|
|
132
|
+
sourceRoot = meta.worktreePath
|
|
133
|
+
}
|
|
134
|
+
} catch { /* meta 读取失败 → 主仓 cwd 源(files 已含本变更 working-tree 改动) */ }
|
|
135
|
+
|
|
136
|
+
const snap = createGateSnapshot({ cwd, files: changeFiles, sourceRoot })
|
|
137
|
+
if (!snap) return null
|
|
138
|
+
|
|
139
|
+
// 变更文档随快照:module-impact.md(test_strategy=evidence-auto 消费)/tasks.md 等
|
|
140
|
+
// 在 .sillyspec(gitignore)不进 HEAD,从主仓 specBase 复制 changes/<name>/**
|
|
141
|
+
try {
|
|
142
|
+
const changeDir = join(specBase, 'changes', changeName)
|
|
143
|
+
if (existsSync(changeDir)) {
|
|
144
|
+
const copyDir = (srcDir, dstDir) => {
|
|
145
|
+
for (const e of readdirSync(srcDir, { withFileTypes: true })) {
|
|
146
|
+
const s = join(srcDir, e.name); const d = join(dstDir, e.name)
|
|
147
|
+
if (e.isDirectory()) { mkdirSync(d, { recursive: true }); copyDir(s, d) }
|
|
148
|
+
else { mkdirSync(dirname(d), { recursive: true }); copyFileSync(s, d) }
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
copyDir(changeDir, join(snap.snapshotRoot, '.sillyspec', 'changes', changeName))
|
|
152
|
+
}
|
|
153
|
+
} catch { /* 文档复制失败不连坐快照(test_strategy 降级 module 集仍可跑) */ }
|
|
154
|
+
|
|
155
|
+
return { ...snap, changeFileCount: changeFiles.length, sourceRoot }
|
|
156
|
+
} catch {
|
|
157
|
+
return null // 任一链路异常 → 回退主仓现行为
|
|
158
|
+
}
|
|
159
|
+
}
|