sillyspec 3.28.8 → 3.28.9
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-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-risk-profile.js +20 -0
- package/src/config-schema.js +32 -0
- package/src/db.js +11 -3
- package/src/decision-distill.js +15 -3
- package/src/design-facts.js +33 -3
- 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 +188 -17
- package/src/knowledge-classify.js +363 -0
- package/src/knowledge-hits.js +74 -0
- package/src/knowledge-stats.js +180 -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 +155 -14
- package/src/review-dispatch.js +2 -1
- package/src/run/command.js +107 -12
- package/src/run/complete-handlers.js +511 -21
- package/src/run/complete.js +54 -16
- package/src/run/concurrent-detect.js +98 -0
- package/src/run/gates.js +50 -9
- package/src/run/prompt.js +134 -5
- package/src/run/quick-audit.js +111 -13
- package/src/run/shared.js +76 -10
- 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 +365 -35
- package/src/stage-contract.js +185 -2
- package/src/stage-review.js +23 -7
- package/src/stages/brainstorm.js +6 -0
- package/src/stages/execute.js +85 -3
- package/src/stages/knowledge.js +52 -3
- package/src/stages/quick.js +1 -1
- package/src/stages/verify.js +1 -0
- package/src/sync-noise.js +114 -6
- package/src/sync.js +135 -7
- package/src/task-review.js +292 -21
- package/src/verify-postcheck.js +20 -0
- package/src/verify-probes.js +340 -4
- package/src/worktree-apply.js +538 -44
- package/src/worktree-deps.js +100 -2
- package/src/worktree.js +117 -5
- 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
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* - handleArchiveConfirmStep:archive「确认归档」步骤 --confirm 门控 + 推荐文档校验
|
|
6
6
|
* - handlePlanGeneratePlanStep:plan「generate_plan」完成后动态插入 coordinator + postcheck 步骤
|
|
7
7
|
* - handleScanProjectListStep:scan step 2 完成后按项目展开 perProject 步骤(用 sanitizeProjectName/validateParsedProjects)
|
|
8
|
-
* - archiveChangeDirectory
|
|
8
|
+
* - archiveChangeDirectory:归档移动变更目录(移动前所有权硬校验 + 未 apply 交付面门(--skip-apply 留痕)+ 6 处 process.exit(1) + worktree 清理;handleArchiveConfirmStep 内部调用)
|
|
9
9
|
* srcDir 缺失时走 findAlreadyArchivedDir 幂等自愈(issue archive-stage-physical-tracking-desync);
|
|
10
10
|
* archiveChangeDirectory + findAlreadyArchivedDir 已 export 供 test 直接 import
|
|
11
11
|
* - sanitizeProjectName / validateParsedProjects:项目名清洗 + 列表校验纯函数(handleScanProjectListStep 专用)
|
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
* 安全锚:run.js 始终 barrel。3 handler 由 run.js import 回来;sanitizeProjectName + validateParsedProjects
|
|
14
14
|
* 被 test 直接 import(run-sanitize-project-name / run-scan-project-parse),run.js barrel re-export 契约保留。
|
|
15
15
|
* 4 目标 handler 无 test 直接 import;archiveChangeDirectory + findAlreadyArchivedDir 供自愈 test 直接 import,无需 barrel re-export。completeStep(Step7 搬)将把 import 行带走。
|
|
16
|
+
* - 知识闭环收尾渲染(2026-09-14-knowledge-loop-close task-03):归类提议(quick --done 进程内
|
|
17
|
+
* outputText 根因 × matchKnowledge)+ knowledge-baseline 棘轮(quick/archive 双宿主)+ archive
|
|
18
|
+
* 抽审清单(近 7 天 classify 审计);countUncategorizedEntries / checkKnowledgeBaselineRatchet /
|
|
19
|
+
* extractQuickCauseField export 供 test/knowledge-baseline.test.mjs 直接 import(实现收拢文件尾)。
|
|
16
20
|
*
|
|
17
21
|
* 路径修正(相对 src/run/):
|
|
18
22
|
* - resolveChangeDir 从 './shared.js';renameSyncRetry 从 '../fs-atomic.js';stageRegistry 从 '../stages/index.js'
|
|
@@ -31,12 +35,18 @@ import { resolveChangeDir, resolveQuickSessionsDir, safeGit, auditQuickCompletio
|
|
|
31
35
|
import { detectConcurrentChanges, formatConcurrentWarning, resolveConcurrentAnchor } from './concurrent-detect.js'
|
|
32
36
|
import { stageRegistry } from '../stages/index.js'
|
|
33
37
|
import { SCAN_STATUS, POINTER_STATUS } from '../constants.js'
|
|
34
|
-
import { printQuickAuditReview, runQuickTestLintGate, printQuickTestLintGate } from './quick-audit.js'
|
|
35
|
-
import { validateQuickResult, allocateQuicklogEntry, appendQuicklogEntryWithId, findQuicklogEntry, completeQuicklogEntry, extractTitleFromResult, parseFileNotes, getQuickFileNotes } from '../quicklog.js'
|
|
38
|
+
import { printQuickAuditReview, runQuickTestLintGate, printQuickTestLintGate, buildGateAuditNote } from './quick-audit.js'
|
|
39
|
+
import { validateQuickResult, allocateQuicklogEntry, appendQuicklogEntryWithId, findQuicklogEntry, completeQuicklogEntry, extractTitleFromResult, parseFileNotes, getQuickFileNotes, countQuicklogEntries, collectGuardReservedQuicklogIds } from '../quicklog.js'
|
|
36
40
|
import { getRule } from '../stage-contract-spec.js'
|
|
37
41
|
import { archiveDestDirName } from '../stage-contract.js'
|
|
38
42
|
import { collectNumstatByPath } from '../scope-audit.js'
|
|
39
43
|
import { recordFrictionEvent, consumeFrictionHint } from '../friction-tally.js'
|
|
44
|
+
import { mergeFrictionEntrySync } from '../friction-ledger.js'
|
|
45
|
+
import { resolveSessionIdentity } from '../progress.js'
|
|
46
|
+
// ql-20260915-001 修复④:chunkPaths(argv 分批)供归档窄化 add / minePaths 精确补暂存用。
|
|
47
|
+
// 无环:worktree-apply 静态闭包(worktree/task-review/quicklog 等)不引本文件;既有
|
|
48
|
+
// withMainRepoLock 走动态 import 是归档链防环的历史形态,chunkPaths 纯函数无此约束。
|
|
49
|
+
import { chunkPaths } from '../worktree-apply.js'
|
|
40
50
|
|
|
41
51
|
/**
|
|
42
52
|
* 清洗项目名:只保留 ASCII 字母/数字/横线/下划线/点,过滤中文和特殊字符。
|
|
@@ -152,13 +162,21 @@ function ownerFromReviewedFiles(reviewedFiles) {
|
|
|
152
162
|
*
|
|
153
163
|
* fail-open:卫生动作失败不抛,返回 { ok:true }。
|
|
154
164
|
*
|
|
165
|
+
* 台账兜底滚动(2026-09-15-tax-governance FR-02):删 friction tally 前读残余
|
|
166
|
+
* events[type].count(X-10:tally 结构 events:{type:{count,lastAt}},无 counts 字段)merge
|
|
167
|
+
* 进 <runtimeRoot>/friction-ledger.json 同 change 条目并落 archivedAt——verify 收尾 consume
|
|
168
|
+
* 已滚主账,此处只兜残余(tally 缺失/全零/坏 JSON 跳过,归档照常)。走 mergeFrictionEntrySync
|
|
169
|
+
* (同步契约:既有 {ok,removed} 消费方与测试直调,拿不了 async 锁;无锁读改写并发丢失按
|
|
170
|
+
* design R-02 P3 容忍)。台账写失败 fail-soft 只反映在 ledgerAppend:false,不抛不阻断归档。
|
|
171
|
+
*
|
|
155
172
|
* @param {string} runtimeRoot
|
|
156
173
|
* @param {string} changeName
|
|
157
|
-
* @returns {{ ok: true, removed: number }}
|
|
174
|
+
* @returns {{ ok: true, removed: number, ledgerAppend: boolean }}
|
|
158
175
|
*/
|
|
159
176
|
export function pruneArchivedChangeRuntime(runtimeRoot, changeName) {
|
|
160
|
-
if (!runtimeRoot || !changeName || !existsSync(runtimeRoot)) return { ok: true, removed: 0 }
|
|
177
|
+
if (!runtimeRoot || !changeName || !existsSync(runtimeRoot)) return { ok: true, removed: 0, ledgerAppend: false }
|
|
161
178
|
let removed = 0
|
|
179
|
+
let ledgerAppend = false
|
|
162
180
|
const gone = (p, recursive = false) => {
|
|
163
181
|
try {
|
|
164
182
|
if (recursive) rmSync(p, { recursive: true, force: true })
|
|
@@ -174,9 +192,18 @@ export function pruneArchivedChangeRuntime(runtimeRoot, changeName) {
|
|
|
174
192
|
|
|
175
193
|
// friction tally(friction-signal-hint FR-05):摩擦计数随变更终态一并回收——真实变更的
|
|
176
194
|
// friction-tally-<change>.json 落 runtimeRoot,变更归档/删除后无读者,残留即孤儿文件。
|
|
195
|
+
// 删前残余先滚台账(tax-governance FR-02 兜底滚动点,见函数头注释)。
|
|
177
196
|
try {
|
|
178
197
|
const fp = join(runtimeRoot, `friction-tally-${changeName}.json`)
|
|
179
|
-
if (existsSync(fp))
|
|
198
|
+
if (existsSync(fp)) {
|
|
199
|
+
const merged = mergeFrictionEntrySync(runtimeRoot, {
|
|
200
|
+
change: changeName,
|
|
201
|
+
counts: residualFrictionCounts(fp),
|
|
202
|
+
archivedAt: new Date().toISOString(),
|
|
203
|
+
})
|
|
204
|
+
ledgerAppend = !!(merged && merged.ok && !merged.skipped)
|
|
205
|
+
gone(fp)
|
|
206
|
+
}
|
|
180
207
|
} catch {}
|
|
181
208
|
|
|
182
209
|
try {
|
|
@@ -231,7 +258,28 @@ export function pruneArchivedChangeRuntime(runtimeRoot, changeName) {
|
|
|
231
258
|
}
|
|
232
259
|
} catch {}
|
|
233
260
|
|
|
234
|
-
return { ok: true, removed }
|
|
261
|
+
return { ok: true, removed, ledgerAppend }
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* 残余摩擦计数读取(tax-governance FR-02 prune 兜底用):tally 的 events 结构是
|
|
266
|
+
* { type: { count, lastAt } }(X-10——无 counts 字段),转成 mergeFrictionEntry 认的
|
|
267
|
+
* counts 平面映射(只收三枚举类型且 count>0,与 consumeFrictionHint 的 counts 形态一致)。
|
|
268
|
+
* 文件缺失/坏 JSON/脏型 → {}(跳过入账,归档照常)。
|
|
269
|
+
*/
|
|
270
|
+
function residualFrictionCounts(tallyPath) {
|
|
271
|
+
try {
|
|
272
|
+
const raw = JSON.parse(readFileSync(tallyPath, 'utf8'))
|
|
273
|
+
const events = raw && typeof raw === 'object' && raw.events && typeof raw.events === 'object' && !Array.isArray(raw.events) ? raw.events : {}
|
|
274
|
+
const counts = {}
|
|
275
|
+
for (const type of ['gate_rollback', 'verify_run_failed', 'review_rejected']) {
|
|
276
|
+
const n = Number(events[type] && events[type].count)
|
|
277
|
+
if (Number.isFinite(n) && n > 0) counts[type] = n
|
|
278
|
+
}
|
|
279
|
+
return counts
|
|
280
|
+
} catch {
|
|
281
|
+
return {}
|
|
282
|
+
}
|
|
235
283
|
}
|
|
236
284
|
|
|
237
285
|
/**
|
|
@@ -393,7 +441,121 @@ export function extractDoneDocTargets(content) {
|
|
|
393
441
|
return [...targets]
|
|
394
442
|
}
|
|
395
443
|
|
|
396
|
-
|
|
444
|
+
/**
|
|
445
|
+
* 归档 docs 侧窄化 add 的目标解析:module-impact「更新结果」done 行 token → 仓相对 POSIX 路径
|
|
446
|
+
* (ql-20260915-001 修复④,坑 archive-git-add-sweeps-parallel-docs:2026-09-14 实证归档自动
|
|
447
|
+
* git add 用目录级 pathspec .sillyspec/docs/ 把并行会话同目录未提交文件夹带进共享暂存区)。
|
|
448
|
+
*
|
|
449
|
+
* 解析规则(与 extractDoneDocTargets 的 token 形态对齐,保守宁漏勿误扫):
|
|
450
|
+
* - `.sillyspec/docs/...` 全路径 token → 相对 cwd 解析,存在才收
|
|
451
|
+
* - `modules/<id>.md` 相对写法(module-impact 骨架的规范形态)→ 扫 <specBase>/docs/ 下各
|
|
452
|
+
* project 目录的 modules/<id>.md——project 归属不猜单值,多项目同名命中各自收(仍是
|
|
453
|
+
* 确定性文件级,不回退目录级)
|
|
454
|
+
* - 其他形态 / 仓库外(relative 出 ..) / 不存在 → 跳过
|
|
455
|
+
*
|
|
456
|
+
* 纯函数(只读 fs),export 供 test 直接 import。
|
|
457
|
+
* @param {string} cwd 仓库根
|
|
458
|
+
* @param {string} specBase .sillyspec 绝对路径
|
|
459
|
+
* @param {string[]} tokens extractDoneDocTargets 产物
|
|
460
|
+
* @returns {string[]} 仓相对 POSIX 路径(去重)
|
|
461
|
+
*/
|
|
462
|
+
export function resolveArchiveDocAddPaths(cwd, specBase, tokens) {
|
|
463
|
+
const out = new Set()
|
|
464
|
+
const pushIfExists = (abs) => {
|
|
465
|
+
try {
|
|
466
|
+
if (abs && existsSync(abs)) {
|
|
467
|
+
const rel = relative(cwd, abs).replace(/\\/g, '/')
|
|
468
|
+
if (rel && !rel.startsWith('..')) out.add(rel)
|
|
469
|
+
}
|
|
470
|
+
} catch { /* 单 token 解析失败跳过(不收即不误 add) */ }
|
|
471
|
+
}
|
|
472
|
+
for (const t of (Array.isArray(tokens) ? tokens : [])) {
|
|
473
|
+
if (!t || typeof t !== 'string') continue
|
|
474
|
+
const norm = String(t).replace(/\\/g, '/')
|
|
475
|
+
if (norm.startsWith('.sillyspec/docs/')) {
|
|
476
|
+
pushIfExists(resolve(cwd, norm))
|
|
477
|
+
continue
|
|
478
|
+
}
|
|
479
|
+
const m = /^modules\/(.+\.md)$/.exec(norm)
|
|
480
|
+
if (m) {
|
|
481
|
+
const docsDir = join(specBase, 'docs')
|
|
482
|
+
if (!existsSync(docsDir)) continue
|
|
483
|
+
let projEntries
|
|
484
|
+
try { projEntries = readdirSync(docsDir, { withFileTypes: true }) } catch { continue }
|
|
485
|
+
for (const proj of projEntries) {
|
|
486
|
+
if (!proj.isDirectory()) continue
|
|
487
|
+
pushIfExists(join(docsDir, proj.name, 'modules', m[1]))
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return [...out]
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* 归档收尾的窄化 git add(ql-20260915-001 修复④,坑 archive-git-add-sweeps-parallel-docs)。
|
|
496
|
+
*
|
|
497
|
+
* changes 侧:本变更归档目录精确 pathspec(closeSingleQuickLinkedChange 的 :1641 先例)——
|
|
498
|
+
* 目录级 .sillyspec/changes/archive/ 会把并行会话停在 archive/ 下的未提交归档包一并扫进
|
|
499
|
+
* 共享暂存区。docs 侧:从(已移动到归档目录的)module-impact.md「## 更新结果」done 行提取
|
|
500
|
+
* 目标文档逐文件 add;提取失败(无表 / 读取异常)回退目录级 add,但前置 warning 列出将被
|
|
501
|
+
* 扫入的未提交 docs 文件(git status --porcelain -- .sillyspec/docs/ 差集——回退态无本变更
|
|
502
|
+
* 声明面,差集即全部未提交项),提示 agent 提交时改用精确 pathspec。表在且解析成功(含
|
|
503
|
+
* 空集 = 无声明文档,docs 面零 add)不回退。幂等:git add 重复执行无有害副作用。
|
|
504
|
+
*
|
|
505
|
+
* 独立函数(非内联)+ export 供 test 直接构造双变更目录场景驱动(不依赖 process.exit 的
|
|
506
|
+
* archiveChangeDirectory 主链)。
|
|
507
|
+
*
|
|
508
|
+
* @param {{ cwd: string, specBase: string, destDir: string, destName: string }} opts
|
|
509
|
+
* @returns {{ docsAdded: string[]|null, fallbackDocs: boolean }} docsAdded=null 表示走了目录级回退
|
|
510
|
+
*/
|
|
511
|
+
export function archiveNarrowedGitAdd({ cwd, specBase, destDir, destName }) {
|
|
512
|
+
// changes 侧:本变更归档目录精确收窄(quick 轻量归档 closeSingleQuickLinkedChange 同款)
|
|
513
|
+
safeGit(cwd, ['add', '--', `.sillyspec/changes/archive/${destName}/`])
|
|
514
|
+
|
|
515
|
+
// docs 侧:module-impact「更新结果」done 行 → 精确文件集。
|
|
516
|
+
// 三态(对齐修复④要求):
|
|
517
|
+
// - module-impact.md 无「## 更新结果」段 / 文件缺失 / 读取异常 → docPaths=null → 回退目录级
|
|
518
|
+
// - 表在但无 done 行(全 skipped / 只有表头)→ 精确空集 → docs 面零 add(不回退——
|
|
519
|
+
// 本变更声明了不同步,目录级扫入他者文件才是错)
|
|
520
|
+
// - 表在有 done 行 → 逐文件精确 add
|
|
521
|
+
let docPaths = null
|
|
522
|
+
try {
|
|
523
|
+
const impactPath = join(destDir, 'module-impact.md')
|
|
524
|
+
if (existsSync(impactPath)) {
|
|
525
|
+
const content = readFileSync(impactPath, 'utf8')
|
|
526
|
+
// 段存在性判定与 extractDoneDocTargets/extractPendingDocSyncRows 同一标题正则口径
|
|
527
|
+
if (/^#{2,3}\s*更新结果\s*$/m.test(content.replace(/\r\n/g, '\n'))) {
|
|
528
|
+
docPaths = resolveArchiveDocAddPaths(cwd, specBase, extractDoneDocTargets(content))
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
} catch (e) {
|
|
532
|
+
docPaths = null
|
|
533
|
+
console.warn(`⚠️ 归档 docs 精确 add 目标提取失败(${e && e.message ? e.message : e})——回退目录级 .sillyspec/docs/ add`)
|
|
534
|
+
}
|
|
535
|
+
if (Array.isArray(docPaths)) {
|
|
536
|
+
for (const batch of chunkPaths(docPaths)) {
|
|
537
|
+
safeGit(cwd, ['add', '--', ...batch])
|
|
538
|
+
}
|
|
539
|
+
return { docsAdded: docPaths, fallbackDocs: false }
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// 回退:目录级(旧行为),前置 warning 列将被扫入的未提交 docs 文件(unstaged/untracked 面)
|
|
543
|
+
try {
|
|
544
|
+
const raw = gitQuiet(cwd, ['status', '--porcelain', '--', '.sillyspec/docs/'], { trim: false })
|
|
545
|
+
const swept = (raw ? String(raw).split('\n') : []).filter(Boolean)
|
|
546
|
+
.filter(l => l.length > 3 && (l.slice(0, 2).includes('?') || l[1] !== ' '))
|
|
547
|
+
.map(l => l.slice(3).trim().replace(/^"|"$/g, ''))
|
|
548
|
+
if (swept.length > 0) {
|
|
549
|
+
console.warn(`⚠️ 归档 git add 回退目录级 .sillyspec/docs/(module-impact.md 无「## 更新结果」表或读取失败)——以下 ${swept.length} 个未提交 docs 文件将被扫入共享暂存区(可能含并行会话在途文件),提交时请用精确 pathspec:`)
|
|
550
|
+
for (const p of swept.slice(0, 10)) console.warn(` - ${p}`)
|
|
551
|
+
if (swept.length > 10) console.warn(` … 共 ${swept.length} 个(git status --porcelain -- .sillyspec/docs/ 查看全量)`)
|
|
552
|
+
}
|
|
553
|
+
} catch { /* 差集探测失败不阻断 add 本身(advisory) */ }
|
|
554
|
+
safeGit(cwd, ['add', '--', '.sillyspec/docs/'])
|
|
555
|
+
return { docsAdded: null, fallbackDocs: true }
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
export async function archiveChangeDirectory(pm, cwd, progress, specBase, platformOpts = {}, gateOpts = {}) {
|
|
397
559
|
const archiveChangeName = progress.currentChange
|
|
398
560
|
if (!archiveChangeName) {
|
|
399
561
|
console.error('❌ 归档失败:未找到当前变更名(currentChange)')
|
|
@@ -426,6 +588,68 @@ export async function archiveChangeDirectory(pm, cwd, progress, specBase, platfo
|
|
|
426
588
|
console.error(` 且 changes/archive/ 下未找到该变更的归档目录。若已手动归档请核对目录名;否则先补全变更产物。`)
|
|
427
589
|
process.exit(1)
|
|
428
590
|
}
|
|
591
|
+
// ── 所有权护栏(2026-09-14-change-ownership-guards task-03 / FR-01 / D-001@v1 接线点)──
|
|
592
|
+
// 归档=接管类操作(注销 change + 移目录 + 清 worktree),他人活跃 change 硬拒。判定在
|
|
593
|
+
// withMainRepoLock 锁内(handleArchiveConfirmStep 调用点),判定-执行无 TOCTOU;放行分支
|
|
594
|
+
// no-owner/takeover-stale 重写 owner=本会话(setChangeOwner 同事务刷新 last_active,心跳从
|
|
595
|
+
// 接管时刻起算)。会话标识三级解析:--session > env SILLYSPEC_SESSION_ID > anon@host(降级
|
|
596
|
+
// 时 resolveSessionIdentity 自带教学 warning)。消费 task-02 契约(assertChangeOwnership
|
|
597
|
+
// 纯读不写库),不重复实现判定语义。
|
|
598
|
+
{
|
|
599
|
+
const { session } = resolveSessionIdentity({ flagSession: gateOpts.sessionFlag || null, cwd })
|
|
600
|
+
const check = pm.assertChangeOwnership(cwd, archiveChangeName, { selfSession: session, nowMs: Date.now() })
|
|
601
|
+
if (!check.allowed) {
|
|
602
|
+
console.error(`🚫 归档失败:变更 ${archiveChangeName} 正被其他会话持有(owner: ${check.owner},最后活跃: ${check.lastActive || '未知'}),活跃窗口 ${Math.round(check.heartbeatMs / 60000)} 分钟内拒绝归档`)
|
|
603
|
+
console.error(` 等对方会话收尾后重试(窗口过后自动接管);确认对方已放弃且本会话应接管:先经 sillyspec worktree apply/cleanup ${archiveChangeName} --takeover 显式接管(重写 owner 留痕)再归档`)
|
|
604
|
+
process.exit(1)
|
|
605
|
+
}
|
|
606
|
+
if (check.action !== 'self') pm.setChangeOwner(cwd, archiveChangeName, session)
|
|
607
|
+
}
|
|
608
|
+
// ── 归档收口(2026-09-14-change-ownership-guards task-03 / FR-02 / D-002@v1)──
|
|
609
|
+
// worktree 有未 apply 交付面 → 归档阻断:归档会注销 change + 清理 worktree(目录/分支/meta),
|
|
610
|
+
// 交付物失去进主仓通道,形成「归档完成但交付物未进主仓」悬空态(troubleshooting §65 事件①
|
|
611
|
+
// 的代劳窗口放大器)。只拦不代跑(design 非目标:脏重叠场景人确认更稳)。--skip-apply 显式
|
|
612
|
+
// 跳过并留痕(控制台输出 + skip-apply.record.json 随归档包留存,可审计)。无 worktree
|
|
613
|
+
// (meta 缺失/in-place/native)或零交付面 → 零行为变化。探测用 applyWorktree checkOnly
|
|
614
|
+
// (只读零写盘,Gate 全收集不落盘、不短路)。
|
|
615
|
+
try {
|
|
616
|
+
const { WorktreeManager } = await import('../worktree.js')
|
|
617
|
+
const wmGate = new WorktreeManager({ cwd })
|
|
618
|
+
const gateMeta = wmGate.getMeta(archiveChangeName)
|
|
619
|
+
if (gateMeta
|
|
620
|
+
&& gateMeta.mode !== 'in-place-fallback'
|
|
621
|
+
&& gateMeta.mode !== 'native-worktree'
|
|
622
|
+
&& gateMeta.worktreePath
|
|
623
|
+
&& existsSync(gateMeta.worktreePath)) {
|
|
624
|
+
const { applyWorktree } = await import('../worktree-apply.js')
|
|
625
|
+
const checkApply = applyWorktree(archiveChangeName, { cwd, checkOnly: true })
|
|
626
|
+
const face = (checkApply && Array.isArray(checkApply.changedFiles)) ? checkApply.changedFiles : []
|
|
627
|
+
if (face.length > 0) {
|
|
628
|
+
const facePreview = `${face.slice(0, 5).join('、')}${face.length > 5 ? ' 等' : ''}`
|
|
629
|
+
if (!gateOpts.skipApply) {
|
|
630
|
+
console.error(`❌ 归档失败:worktree 存在未 apply 的交付面(${face.length} 个文件:${facePreview})`)
|
|
631
|
+
console.error(` 归档会清理 worktree(目录+分支+meta),交付物将失去进主仓的通道,形成「归档完成但交付物未进主仓」悬空态。`)
|
|
632
|
+
console.error(` 先 apply 再归档: sillyspec worktree apply ${archiveChangeName}`)
|
|
633
|
+
console.error(` 确认无需 apply(交付面已由其他会话落地/纯探索性变更等): sillyspec run archive --done --confirm --skip-apply(显式跳过并留痕归档记录)`)
|
|
634
|
+
process.exit(1)
|
|
635
|
+
}
|
|
636
|
+
console.warn(`⚠️ 已按 --skip-apply 跳过未 apply 交付面检查(${face.length} 个文件未确认落地主仓:${facePreview})——留痕归档记录`)
|
|
637
|
+
writeFileSync(join(srcDir, 'skip-apply.record.json'), JSON.stringify({
|
|
638
|
+
schemaVersion: 1,
|
|
639
|
+
change: archiveChangeName,
|
|
640
|
+
flag: '--skip-apply',
|
|
641
|
+
skippedAt: new Date().toISOString(),
|
|
642
|
+
deliverableCount: face.length,
|
|
643
|
+
deliverableFiles: face,
|
|
644
|
+
note: '归档时显式跳过未 apply 交付面检查(--skip-apply):交付物未确认进主仓,本记录随归档包留存供审计',
|
|
645
|
+
}, null, 2) + '\n')
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
} catch (e) {
|
|
649
|
+
// 探测自身异常 fail-open 留痕:归档主流程既有失败面(rename/git/清理)不叠加;正常路径
|
|
650
|
+
// 的阻断语义不受影响(无异常时 face 判定照常生效)。
|
|
651
|
+
console.warn(`⚠️ 归档前未 apply 交付面探测失败(不阻断归档): ${e && e.message ? e.message : e}`)
|
|
652
|
+
}
|
|
429
653
|
// 移动前硬校验:变更包必须含 plan.md,否则不该归档。
|
|
430
654
|
// 在移动前阻断(而非移动后),目录尚未动,用户可直接修复后重试。
|
|
431
655
|
if (!existsSync(join(srcDir, 'plan.md'))) {
|
|
@@ -474,9 +698,12 @@ export async function archiveChangeDirectory(pm, cwd, progress, specBase, platfo
|
|
|
474
698
|
// step5 prompt 的 git add 保留作幂等兜底;POSIX 路径跨平台(git 接受正斜杠)。
|
|
475
699
|
// safeGit 内部已 try-catch(返回 {value,error} 不抛),外层 try 兜底防御;失败不阻断归档
|
|
476
700
|
// (目录已移动 + change 已注销),由 step5 prompt git add + agent git status 核对兜底。
|
|
701
|
+
// ql-20260915-001 修复④(坑 archive-git-add-sweeps-parallel-docs,2026-09-14 实证:目录级
|
|
702
|
+
// pathspec 把并行会话同目录未提交文件夹带进共享暂存区):改为窄化——changes 侧只 add 本变更
|
|
703
|
+
// 归档目录 archive/<destName>/,docs 侧按 module-impact「更新结果」done 行精确文件集(提取
|
|
704
|
+
// 失败回退目录级 + 前置 warning,见 archiveNarrowedGitAdd)。
|
|
477
705
|
try {
|
|
478
|
-
|
|
479
|
-
safeGit(cwd, ['add', '--', '.sillyspec/docs/'])
|
|
706
|
+
archiveNarrowedGitAdd({ cwd, specBase, destDir, destName })
|
|
480
707
|
} catch {}
|
|
481
708
|
|
|
482
709
|
// ── 他者半归档残留探测(坑 archive-other-residual-rename,2026-08-21 实证)──
|
|
@@ -514,7 +741,13 @@ export async function archiveChangeDirectory(pm, cwd, progress, specBase, platfo
|
|
|
514
741
|
}
|
|
515
742
|
}
|
|
516
743
|
if (minePaths.length > 0) {
|
|
517
|
-
|
|
744
|
+
// ql-20260915-001 修复④:补暂存源侧移动改精确 pathspec(minePaths 已按本变更目录名
|
|
745
|
+
// 过滤,chunkPaths 分批防 Windows argv 上限)——原 add -A -- .sillyspec/changes/ 目录级
|
|
746
|
+
// 会顺带扫入并行会话在 changes/ 下的未提交文件(坑 archive-git-add-sweeps-parallel-docs
|
|
747
|
+
// 同坑不同点;git add -- <已删路径> 即暂存删除,语义等价 -A 限本变更面)
|
|
748
|
+
for (const batch of chunkPaths(minePaths)) {
|
|
749
|
+
safeGit(cwd, ['add', '--', ...batch])
|
|
750
|
+
}
|
|
518
751
|
console.log(`🧾 已补暂存本变更归档的源侧移动(${minePaths.length} 项,归档成单次原子提交)`)
|
|
519
752
|
}
|
|
520
753
|
if (othersResidual.length > 0) {
|
|
@@ -542,7 +775,7 @@ export async function archiveChangeDirectory(pm, cwd, progress, specBase, platfo
|
|
|
542
775
|
*
|
|
543
776
|
* @returns {{stageCompleted:false,currentIdx,nextPendingIdx:number}|null}
|
|
544
777
|
*/
|
|
545
|
-
export async function handleArchiveConfirmStep({ stageName, steps, currentIdx, confirm, outputText, pm, cwd, progress, changeName, specBase, platformOpts = {} }) {
|
|
778
|
+
export async function handleArchiveConfirmStep({ stageName, steps, currentIdx, confirm, outputText, pm, cwd, progress, changeName, specBase, platformOpts = {}, isSkipApply = false, sessionFlag = null }) {
|
|
546
779
|
if (stageName !== 'archive' || steps[currentIdx]?.name !== '确认归档') return null
|
|
547
780
|
if (!confirm) {
|
|
548
781
|
steps[currentIdx].status = 'pending'
|
|
@@ -584,7 +817,9 @@ export async function handleArchiveConfirmStep({ stageName, steps, currentIdx, c
|
|
|
584
817
|
// rename + 共享 index 的 git add + marker 删除 + worktree 清理),与并行会话的 apply/cleanup
|
|
585
818
|
// 互踩。exit 钩子兜底:其内部 guard 失败 exit(1) 时锁也会被清(见 withMainRepoLock)。
|
|
586
819
|
const { withMainRepoLock } = await import('../worktree-apply.js')
|
|
587
|
-
|
|
820
|
+
// gateOpts(task-03):--skip-apply(归档收口跳过留痕)+ --session(所有权会话标识)随链
|
|
821
|
+
// 透传——所有权硬校验与未 apply 交付面门都在 archiveChangeDirectory 内(即锁内)执行。
|
|
822
|
+
const archivedDir = await withMainRepoLock(cwd, changeName, 'archive-finalize', () => archiveChangeDirectory(pm, cwd, progress, specBase, platformOpts, { skipApply: isSkipApply, sessionFlag }))
|
|
588
823
|
if (archivedDir && existsSync(archivedDir)) {
|
|
589
824
|
// 内存快照同步(坑 archive-progress-show-stale,2026-08-21 实证):archiveChangeDirectory 内
|
|
590
825
|
// unregisterChange 已在 DB 写 current_stage='archive'(终态一致化),但本进程 progress 是命令
|
|
@@ -620,6 +855,22 @@ export async function handleArchiveConfirmStep({ stageName, steps, currentIdx, c
|
|
|
620
855
|
}
|
|
621
856
|
}
|
|
622
857
|
}
|
|
858
|
+
// ── 知识闭环收尾渲染(2026-09-14-knowledge-loop-close task-03,X-006 定锚)──
|
|
859
|
+
// ① knowledge-baseline 棘轮(FR-03/R-05):与 quick --done 收尾同款三态(超线软警告不阻断 /
|
|
860
|
+
// 降线自动收紧 / 基线缺失跳过),archive 时刻是归类清欠的另一自然卡点。
|
|
861
|
+
renderKnowledgeBaselineRatchet(specBase)
|
|
862
|
+
// ② 自动归类抽审清单:近 7 天 hits.jsonl 里 type:classify 的记录数——归类是机器提案经
|
|
863
|
+
// classify 落地,人闸收敛到归档时抽审(R-01:错误归类 revert 的发现面)。读实现为
|
|
864
|
+
// task-01 交付的 src/knowledge-hits.js(readKnowledgeHits 坏行容忍);runtimeRoot 解析
|
|
865
|
+
// 与本文件既有 delta sidecar 同源(resolveRuntimeRoot,平台模式对齐)。无记录/读失败零输出。
|
|
866
|
+
try {
|
|
867
|
+
const { readKnowledgeHits } = await import('../knowledge-hits.js')
|
|
868
|
+
const classifyCount = readKnowledgeHits(resolveRuntimeRoot(platformOpts, specBase), { sinceDays: 7 })
|
|
869
|
+
.filter((h) => h && h.type === 'classify').length
|
|
870
|
+
if (classifyCount > 0) {
|
|
871
|
+
console.log(`\n🧪 本周期自动归类 ${classifyCount} 条(近 7 天 knowledge classify 审计),建议抽审:sillyspec knowledge stats`)
|
|
872
|
+
}
|
|
873
|
+
} catch { /* 抽审清单读取失败不影响归档(advisory fail-open) */ }
|
|
623
874
|
return null
|
|
624
875
|
}
|
|
625
876
|
/**
|
|
@@ -980,7 +1231,7 @@ export async function handleWorkflowPostCheck({ stageName, steps, currentIdx, cw
|
|
|
980
1231
|
* isForceBaseline/isAllowNew/platformOpts。辅助函数直接 import(safeGit/auditQuickCompletion ← shared,
|
|
981
1232
|
* printQuickAuditReview ← quick-audit,4 个 quicklog fns ← quicklog,unlinkSync/rmSync ← fs 静态)。
|
|
982
1233
|
*/
|
|
983
|
-
export async function handleQuickStageCompletion({ stageName, steps, currentIdx, cwd, progress, changeName, specBase, outputText, confirm, isForceBaseline, isAllowNew, isAllowDelete, platformOpts, pm, quickFiles = [] }) {
|
|
1234
|
+
export async function handleQuickStageCompletion({ stageName, steps, currentIdx, cwd, progress, changeName, specBase, outputText, confirm, isForceBaseline, isAllowNew, isAllowDelete, isNoDocs, sessionFlag = null, platformOpts, pm, quickFiles = [] }) {
|
|
984
1235
|
// quick 收尾:强校验 QUICKLOG 条目 + 翻状态 + 勾 tasks.md(CLI 接管)
|
|
985
1236
|
if (stageName === 'quick') {
|
|
986
1237
|
// §4.6 从 session guard.json 读 guard(不依赖 progress.quickGuard)。
|
|
@@ -1004,6 +1255,9 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
|
|
|
1004
1255
|
const gitUser = safeGit(cwd, ['config', 'user.name']).value || 'unknown'
|
|
1005
1256
|
let qlId = guard?.quicklogId || null
|
|
1006
1257
|
const linkedChanges = Array.isArray(guard?.linkedChanges) ? guard.linkedChanges : []
|
|
1258
|
+
// 机器自动关联溯源(坑 quick-single-change-auto-link):单候选信号门控命中自动关联的
|
|
1259
|
+
// 子集——归档闸消费(这类变更不触发轻量归档),显式 --linked-changes 关联不在此列
|
|
1260
|
+
const linkedChangesAuto = Array.isArray(guard?.linkedChangesAuto) ? guard.linkedChangesAuto : []
|
|
1007
1261
|
// 审计结果(仅 guard 存在时填充)。提到 if(guard) 外声明,供下方回填 QUICKLOG 文件行复用
|
|
1008
1262
|
// review.changedFiles;brownfield 无 guard 时保持 null → 文件行不回填(降级,不报错)。
|
|
1009
1263
|
let review = null
|
|
@@ -1059,7 +1313,13 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
|
|
|
1059
1313
|
// → 回到无豁免现状。
|
|
1060
1314
|
otherSessionsDeclared: collectOtherQuickSessionDeclarations(platformOpts, specBase, changeName),
|
|
1061
1315
|
}
|
|
1062
|
-
review = await auditQuickCompletion(cwd, mergedGuard, {
|
|
1316
|
+
review = await auditQuickCompletion(cwd, mergedGuard, {
|
|
1317
|
+
isConfirm: confirm,
|
|
1318
|
+
// task-02(FR-03):--no-docs 豁免 + --file-notes 覆盖率透传进门禁画像(per-process 值与
|
|
1319
|
+
// 上方边界并入、下方 completeQuicklogEntry 消费同源;不传 → 纯默认画像)
|
|
1320
|
+
noDocs: isNoDocs === true,
|
|
1321
|
+
fileNotes: parseFileNotes(getQuickFileNotes()),
|
|
1322
|
+
})
|
|
1063
1323
|
printQuickAuditReview(review)
|
|
1064
1324
|
if (review.status === 'blocked') {
|
|
1065
1325
|
steps[currentIdx].status = 'pending'
|
|
@@ -1187,6 +1447,8 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
|
|
|
1187
1447
|
description: guard?.taskDescription || '(补分配)',
|
|
1188
1448
|
linkedChanges,
|
|
1189
1449
|
allowedFiles: Array.isArray(guard?.allowedFiles) ? guard.allowedFiles : [],
|
|
1450
|
+
// 他者 guard 预留让位(坑 ql-id-double-occupancy):与启动分配同源
|
|
1451
|
+
sessionsDir: resolveQuickSessionsDir(platformOpts, specBase),
|
|
1190
1452
|
})
|
|
1191
1453
|
qlId = alloc.qlId
|
|
1192
1454
|
console.log(`📝 QUICKLOG 兜底补写: ${qlId}(guard 缺失/brownfield 会话)`)
|
|
@@ -1198,14 +1460,69 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
|
|
|
1198
1460
|
process.exit(1)
|
|
1199
1461
|
}
|
|
1200
1462
|
}
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1463
|
+
// 占用校验(坑 ql-id-double-occupancy,2026-09-13 实证 007-1351 双占用):启动预留 ql-ID
|
|
1464
|
+
// 写入 guard.json 后,QUICKLOG 条目丢失/分裂窗口内并行会话可分得同一 ID。--done 落最终
|
|
1465
|
+
// ID 前两级校验:
|
|
1466
|
+
// ① 盘上同 ID 条目 ≥2 → 记录已损坏,fail-closed 硬拦(不猜哪条属于本会话,手工去重后重跑)
|
|
1467
|
+
// ② 他者活跃会话 guard 仍预留同 ID → 本会话换新号完成(原 ID 让位他者,双方记录不混写)
|
|
1468
|
+
const sessionsDirForClaims = resolveQuickSessionsDir(platformOpts, specBase)
|
|
1469
|
+
const occupancy = countQuicklogEntries(specBase, qlId)
|
|
1470
|
+
if (occupancy.count >= 2) {
|
|
1471
|
+
console.error(`\n❌ QUICKLOG 条目 ${qlId} 双占用:${occupancy.count} 处命中(${occupancy.files.join('、')})——分配竞态残留已损坏记录。`)
|
|
1472
|
+
console.error(` 手工去重:编辑上述文件,同 ID 只保留属于本会话的一条(对照时间戳/任务描述),删除其余后重跑 --done。`)
|
|
1204
1473
|
steps[currentIdx].status = 'pending'
|
|
1205
1474
|
steps[currentIdx].completedAt = null
|
|
1206
1475
|
if (outputText) steps[currentIdx].output = null
|
|
1207
1476
|
process.exit(1)
|
|
1208
1477
|
}
|
|
1478
|
+
const claimants = (collectGuardReservedQuicklogIds(specBase, sessionsDirForClaims).get(qlId) || []).filter(s => s !== changeName)
|
|
1479
|
+
if (claimants.length > 0) {
|
|
1480
|
+
const oldId = qlId
|
|
1481
|
+
try {
|
|
1482
|
+
const alloc = await allocateQuicklogEntry(specBase, gitUser, {
|
|
1483
|
+
description: guard?.taskDescription || '(占用换号补分配)',
|
|
1484
|
+
linkedChanges,
|
|
1485
|
+
allowedFiles: Array.isArray(guard?.allowedFiles) ? guard.allowedFiles : [],
|
|
1486
|
+
sessionsDir: sessionsDirForClaims,
|
|
1487
|
+
})
|
|
1488
|
+
qlId = alloc.qlId
|
|
1489
|
+
console.warn(`⚠️ 预留 ql-ID ${oldId} 被并行会话占用(${claimants.join('、')})——分配竞态残留(坑 ql-id-double-occupancy)。`)
|
|
1490
|
+
console.warn(` 本会话已换新号完成:${qlId};原 ID 让位该会话(其 --done 正常落原条目),双方 QUICKLOG 记录不混写。`)
|
|
1491
|
+
} catch (e) {
|
|
1492
|
+
console.error(`\n❌ 占用换号失败: ${e.message}`)
|
|
1493
|
+
steps[currentIdx].status = 'pending'
|
|
1494
|
+
steps[currentIdx].completedAt = null
|
|
1495
|
+
if (outputText) steps[currentIdx].output = null
|
|
1496
|
+
process.exit(1)
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
// 最终 ID 回写 guard(坑 ql-id-double-occupancy 建议项):换号/兜底分配路径下预留 ID ≠
|
|
1500
|
+
// 最终 ID 时对齐 guard 记录,会话期内 hook/审计读到的不说谎。
|
|
1501
|
+
if (guard && guard.quicklogId !== qlId) {
|
|
1502
|
+
guard.quicklogId = qlId
|
|
1503
|
+
try { writeAtomicSync(sessionGuardFile, JSON.stringify(guard, null, 2)) } catch { /* 回写失败降级:本进程内已对齐 */ }
|
|
1504
|
+
}
|
|
1505
|
+
// 条目缺失自愈(原硬拦改补建,坑 ql-id-double-occupancy 同族):条目在会话期间丢失
|
|
1506
|
+
// (并行 git 操作回滚未提交 QUICKLOG 等)时,用最终 ID 补建「进行中」骨架再走完成翻态
|
|
1507
|
+
// ——与 guard 缺失分支同 cure(坑 platform-takeover-phantom-progress-db 分裂形态),
|
|
1508
|
+
// 消「检查后重跑」的人工一轮。占用校验已在前:补建不会压到他者条目/预留。
|
|
1509
|
+
if (!findQuicklogEntry(specBase, gitUser, qlId)) {
|
|
1510
|
+
try {
|
|
1511
|
+
await appendQuicklogEntryWithId(specBase, gitUser, qlId, {
|
|
1512
|
+
description: guard?.taskDescription || '(条目丢失补建)',
|
|
1513
|
+
linkedChanges,
|
|
1514
|
+
allowedFiles: Array.isArray(guard?.allowedFiles) ? guard.allowedFiles : [],
|
|
1515
|
+
})
|
|
1516
|
+
console.warn(`⚠️ QUICKLOG 条目 ${qlId} 会话期间丢失,已按原 ID 补建骨架(并行 git 操作回滚未提交 QUICKLOG 所致,详见坑 ql-id-double-occupancy)。`)
|
|
1517
|
+
} catch (e) {
|
|
1518
|
+
console.error(`\n❌ quick 阶段完成校验失败:QUICKLOG 条目 ${qlId} 不存在且补建失败(${e.message})。`)
|
|
1519
|
+
console.error(` 请检查 .sillyspec/quicklog/ 后重跑 --done。`)
|
|
1520
|
+
steps[currentIdx].status = 'pending'
|
|
1521
|
+
steps[currentIdx].completedAt = null
|
|
1522
|
+
if (outputText) steps[currentIdx].output = null
|
|
1523
|
+
process.exit(1)
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1209
1526
|
// 翻状态进行中→已完成 + 追加结果 + 勾选关联 tasks.md
|
|
1210
1527
|
// resultText 不再截断:结构化结果块(需求/根因/方案/结果)完整落盘,多行写成字段化块。
|
|
1211
1528
|
try {
|
|
@@ -1262,6 +1579,13 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
|
|
|
1262
1579
|
}
|
|
1263
1580
|
} catch { /* 自动重锚失败退化为纯 advisory(上方 auditNote 已落盘) */ }
|
|
1264
1581
|
}
|
|
1582
|
+
// ── [gate] 分级门禁落账(FR-03,2026-09-14-quick-exit-tiered-gates task-02):L1/L2 画像
|
|
1583
|
+
// 经 buildGateAuditNote 组装单行 [gate] 注记随既有 auditNotes 通道落 QUICKLOG(L1 每文件
|
|
1584
|
+
// 注记+测试增量;L2 模块文档认领+风险命中;--no-docs 豁免同通道留痕);L0/无画像 → null
|
|
1585
|
+
// 零落账。纯 advisory:不阻断完成、不改 review.status 语义(D-003);未声明脏文件维持上方
|
|
1586
|
+
// 归属切分注(⚖️/🔍)不并入文档认领判定(D-005)。
|
|
1587
|
+
const gateAuditNote = buildGateAuditNote(review?.gateProfile)
|
|
1588
|
+
if (gateAuditNote) auditNotes.push(gateAuditNote)
|
|
1265
1589
|
// 归属切分注(2026-08-18 误归属修复):窗口内未声明脏文件不进「文件:」行,但必须落盘可追溯
|
|
1266
1590
|
// (多 agent 并发仓他者窗口改动 / 本会话漏声明均可能),防真实改动被静默挤走。
|
|
1267
1591
|
// 软归属拆分(2026-09-10):softFiles 中的同模块测试文件已补入文件行,不再占 ⚖️「未计入」
|
|
@@ -1327,7 +1651,7 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
|
|
|
1327
1651
|
if (typeof frozenPatch === 'string' && frozenPatch) {
|
|
1328
1652
|
const patchText = frozenPatch.endsWith('\n') ? frozenPatch : frozenPatch + '\n'
|
|
1329
1653
|
writeFileSync(join(patchesDir, `${qlId}.patch`), patchText)
|
|
1330
|
-
snapObj.patchSha256 = createHash('sha256').update(patchText, 'utf8').digest('hex')
|
|
1654
|
+
snapObj.patchSha256 = createHash('sha256').update(patchText.replace(/\r\n/g, '\n'), 'utf8').digest('hex')
|
|
1331
1655
|
snapObj.patchStatus = 'ok'
|
|
1332
1656
|
} else if (frozenPatch === null) {
|
|
1333
1657
|
snapObj.patchStatus = 'failed'
|
|
@@ -1348,9 +1672,31 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
|
|
|
1348
1672
|
console.warn(`⚠️ QUICKLOG 完成态写入失败: ${e.message}`)
|
|
1349
1673
|
}
|
|
1350
1674
|
|
|
1675
|
+
// ── 归类提议(2026-09-14-knowledge-loop-close task-03,FR-01/D-001@v1)──
|
|
1676
|
+
// QUICKLOG 已落盘,用进程内已过四字段校验的 outputText(免读回盘)提取「根因:」拼查询串跑
|
|
1677
|
+
// matchKnowledge(复用 knowledge-match 既有引擎;INDEX.md 缺失/未命中 matched:false 全链路
|
|
1678
|
+
// no-op)。只渲染不自动执行:确认归类须 agent 显式跑 knowledge classify(R-01,错误归类靠
|
|
1679
|
+
// archive/doctor 抽审 revert)。根因为「无,纯新增/纯样式」形态(无坑可归)或未命中时不渲染
|
|
1680
|
+
// 任何提议(拿不准不写,零输出变化)。实现与口径见文件尾(函数声明提升 + B1 断言窗口约束)。
|
|
1681
|
+
try {
|
|
1682
|
+
const causeText = extractQuickCauseField(outputText)
|
|
1683
|
+
if (causeText && !PURE_NEW_CAUSE_RE.test(causeText)) {
|
|
1684
|
+
const { matchKnowledge } = await import('../knowledge-match.js')
|
|
1685
|
+
const km = matchKnowledge(join(specBase, 'knowledge'), causeText)
|
|
1686
|
+
if (km && km.matched && Array.isArray(km.entries) && km.entries.length > 0) {
|
|
1687
|
+
const hit = km.entries[0]
|
|
1688
|
+
const target = hit.anchor ? `${hit.file}#${hit.anchor}` : hit.file
|
|
1689
|
+
console.log(`\n📚 待归类提议:${qlId} 根因疑似命中 ${target}`)
|
|
1690
|
+
console.log(` 确认归类跑:sillyspec knowledge classify --ql ${qlId} --file ${hit.file}`)
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
} catch { /* 提议渲染失败零副作用(advisory fail-open,不阻断 --done 收尾) */ }
|
|
1694
|
+
|
|
1351
1695
|
// 轻量归档任务已全勾选的关联真实变更
|
|
1352
1696
|
try {
|
|
1353
|
-
|
|
1697
|
+
// quickSessionName/sessionFlag(task-03 / FR-01):quick 轻量归档链所有权校验入参——
|
|
1698
|
+
// quick 会话标识(changeName=quick-<hex>,三级解析第三层)+ --session 显式标识。
|
|
1699
|
+
const closeResult = await closeQuickLinkedChanges({ pm, cwd, specBase, linkedChanges, linkedChangesAuto, platformOpts, quickSessionName: changeName, sessionFlag })
|
|
1354
1700
|
if (closeResult.closed.length > 0) {
|
|
1355
1701
|
console.log(`📦 已自动归档 ${closeResult.closed.length} 个关联变更:${closeResult.closed.join(', ')}`)
|
|
1356
1702
|
}
|
|
@@ -1392,6 +1738,11 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
|
|
|
1392
1738
|
console.warn(`⚠️ 注销 quick 会话 changes 行失败(不阻断完成): ${e.message}`)
|
|
1393
1739
|
}
|
|
1394
1740
|
}
|
|
1741
|
+
|
|
1742
|
+
// ── knowledge-baseline 棘轮(2026-09-14-knowledge-loop-close task-03,FR-03/R-05)──
|
|
1743
|
+
// uncategorized 待归类条数对比基线:超线 ⚠️ 软警告(不阻断不抛错)、降线自动收紧写回、
|
|
1744
|
+
// 基线文件缺失 = 未启用直接跳过(存量仓零迁移)。实现收拢文件尾(B1 断言窗口约束同上)。
|
|
1745
|
+
renderKnowledgeBaselineRatchet(specBase)
|
|
1395
1746
|
}
|
|
1396
1747
|
|
|
1397
1748
|
return null
|
|
@@ -1491,15 +1842,27 @@ const QUICK_CLOSE_ACTIVITY_WINDOW_MS = 60 * 60 * 1000
|
|
|
1491
1842
|
* @param {string} opts.specBase
|
|
1492
1843
|
* @param {string[]} [opts.linkedChanges]
|
|
1493
1844
|
* @param {Object} [opts.platformOpts]
|
|
1845
|
+
* @param {string|null} [opts.quickSessionName] quick 会话名(quick-<hex>,所有权三级标识第三层)
|
|
1846
|
+
* @param {string|null} [opts.sessionFlag] --session 显式会话标识(所有权三级标识最高层)
|
|
1494
1847
|
* @returns {Promise<{closed:string[], skipped:{name:string,reason:string}[]}>}
|
|
1495
1848
|
*/
|
|
1496
|
-
export async function closeQuickLinkedChanges({ pm, cwd, specBase, linkedChanges = [], platformOpts = {} }) {
|
|
1849
|
+
export async function closeQuickLinkedChanges({ pm, cwd, specBase, linkedChanges = [], linkedChangesAuto = [], platformOpts = {}, quickSessionName = null, sessionFlag = null }) {
|
|
1497
1850
|
const closed = []
|
|
1498
1851
|
const skipped = []
|
|
1499
1852
|
// 只处理真实变更,跳过 quick 会话 sessionId
|
|
1500
1853
|
const realChanges = linkedChanges.filter((name) => !/^quick-[0-9a-f]{8}$/.test(name))
|
|
1501
1854
|
for (const changeName of realChanges) {
|
|
1502
1855
|
try {
|
|
1856
|
+
// 止血(坑 quick-single-change-auto-link 配套):仅被机器自动关联(单候选信号门控命中
|
|
1857
|
+
// 的猜测,非 --linked-changes 显式协作声明)的变更不触发轻量归档——归档是破坏性动作,
|
|
1858
|
+
// 机器猜的关联声明不够格;显式关联的僵尸清理通道(D-002@v1/v2 契约)不受影响。
|
|
1859
|
+
if (linkedChangesAuto.includes(changeName)) {
|
|
1860
|
+
skipped.push({
|
|
1861
|
+
name: changeName,
|
|
1862
|
+
reason: '自动关联(信号命中机器猜测,非显式协作声明),不触发自动归档——确要归档请走原流程收尾(sillyspec progress show)',
|
|
1863
|
+
})
|
|
1864
|
+
continue
|
|
1865
|
+
}
|
|
1503
1866
|
if (typeof pm.getChangeStage !== 'function') {
|
|
1504
1867
|
skipped.push({ name: changeName, reason: '进度库接口缺失(getChangeStage),无法判定流程阶段,不自动归档' })
|
|
1505
1868
|
continue
|
|
@@ -1548,6 +1911,39 @@ export async function closeQuickLinkedChanges({ pm, cwd, specBase, linkedChanges
|
|
|
1548
1911
|
skipped.push({ name: changeName, reason: 'tasks.md 未全勾选或不存在' })
|
|
1549
1912
|
continue
|
|
1550
1913
|
}
|
|
1914
|
+
// ── 所有权护栏(2026-09-14-change-ownership-guards task-03 / FR-01 接线点)──
|
|
1915
|
+
// closeSingleQuickLinkedChange 归档移动前对 linked full-flow change 校验(消费 task-02
|
|
1916
|
+
// 契约,不重复实现):他人活跃 → 跳过该 linked 归档 + warning(不炸 quick --done);无主/
|
|
1917
|
+
// 窗口外/本会话自有 → 放行,接管分支重写 owner。quick 会话标识=quick-<hex>(三级解析
|
|
1918
|
+
// 第三层,quickChangeName 传入);quick 面互斥恒 self(design 非目标声明),本闸只拦
|
|
1919
|
+
// full-flow change 的「他人活跃」态。pm 缺接口(旧 mock/旧进度库)fail-open 放行留痕
|
|
1920
|
+
//(同 command.js claim fail-open 先例:护栏退回 no-owner 语义,不废僵尸逃生通道);
|
|
1921
|
+
// 校验自身异常则跳过该归档(fail-closed 不误归档)。
|
|
1922
|
+
if (typeof pm.assertChangeOwnership !== 'function') {
|
|
1923
|
+
console.warn(`⚠️ 进度库接口缺失(assertChangeOwnership),关联变更 ${changeName} 跳过所有权校验(fail-open 放行)`)
|
|
1924
|
+
} else {
|
|
1925
|
+
try {
|
|
1926
|
+
const { session } = resolveSessionIdentity({
|
|
1927
|
+
flagSession: sessionFlag || null,
|
|
1928
|
+
quickChangeName: quickSessionName || null,
|
|
1929
|
+
cwd,
|
|
1930
|
+
})
|
|
1931
|
+
const ownCheck = pm.assertChangeOwnership(cwd, changeName, { selfSession: session, nowMs: Date.now() })
|
|
1932
|
+
if (!ownCheck.allowed) {
|
|
1933
|
+
skipped.push({
|
|
1934
|
+
name: changeName,
|
|
1935
|
+
reason: `变更正被其他会话持有(owner: ${ownCheck.owner},最后活跃: ${ownCheck.lastActive || '未知'}),跳过自动归档——由其会话自行收尾(sillyspec progress show 查看进度)`,
|
|
1936
|
+
})
|
|
1937
|
+
continue
|
|
1938
|
+
}
|
|
1939
|
+
if (ownCheck.action !== 'self') {
|
|
1940
|
+
try { pm.setChangeOwner(cwd, changeName, session) } catch { /* 接管写库失败不阻断放行语义 */ }
|
|
1941
|
+
}
|
|
1942
|
+
} catch (e) {
|
|
1943
|
+
skipped.push({ name: changeName, reason: `所有权校验异常(${e && e.message ? e.message : e}),跳过自动归档` })
|
|
1944
|
+
continue
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1551
1947
|
const result = await closeSingleQuickLinkedChange({ pm, cwd, specBase, changeName, platformOpts })
|
|
1552
1948
|
if (result.closed) {
|
|
1553
1949
|
closed.push(changeName)
|
|
@@ -1991,3 +2387,97 @@ function attachQuickLineCounts({ cwd, realFiles, softFiles, review, auditFiles,
|
|
|
1991
2387
|
return { annotatedRealFiles, annotatedSoftFiles, fmtLineCounts }
|
|
1992
2388
|
}
|
|
1993
2389
|
|
|
2390
|
+
// ── 知识闭环收尾渲染(2026-09-14-knowledge-loop-close task-03,FR-01/FR-03)──
|
|
2391
|
+
// 放文件尾(函数声明提升,同 attachQuickLineCounts 先例):归类提议/棘轮渲染挂在 quick --done 与
|
|
2392
|
+
// archive 收尾两处,插码位置受 concurrent-preflight-hooks.test.mjs B1 源码文本级断言窗口约束
|
|
2393
|
+
// (detectConcurrentChanges ±[200,600] 字符内禁 process.exit、须保 try/catch)——实现收拢在此,
|
|
2394
|
+
// 调用点只留小段调用,避免大段内联把断言锚推出窗口。全段 fail-open:任何异常只吞不阻断收尾。
|
|
2395
|
+
|
|
2396
|
+
/**
|
|
2397
|
+
* 数 knowledge/uncategorized.md 待归类条目数。计数正则与 `sillyspec knowledge` validate 同款
|
|
2398
|
+
* /^#{2,3}\s+\S/gm(X-010 口径对齐,防双数字打架);文件缺失算 0(未启用知识库的仓不警告)。
|
|
2399
|
+
* 已 export 供 test/knowledge-baseline.test.mjs 直接断言计数口径。
|
|
2400
|
+
*/
|
|
2401
|
+
export function countUncategorizedEntries(knowledgeDir) {
|
|
2402
|
+
const uncPath = join(knowledgeDir, 'uncategorized.md')
|
|
2403
|
+
if (!existsSync(uncPath)) return 0
|
|
2404
|
+
try {
|
|
2405
|
+
return (readFileSync(uncPath, 'utf8').match(/^#{2,3}\s+\S/gm) || []).length
|
|
2406
|
+
} catch {
|
|
2407
|
+
return 0
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
/**
|
|
2412
|
+
* 读 .sillyspec/knowledge-baseline 单整数(uncategorized 条数上限,语义/读写形态对齐
|
|
2413
|
+
* docs-check-baseline:trim 后 parseInt,非负整数才有效)。缺失/不可解析 → null(未启用)。
|
|
2414
|
+
*/
|
|
2415
|
+
function readKnowledgeBaseline(specBase) {
|
|
2416
|
+
try {
|
|
2417
|
+
const baselinePath = join(specBase, 'knowledge-baseline')
|
|
2418
|
+
if (!existsSync(baselinePath)) return null
|
|
2419
|
+
const n = parseInt(readFileSync(baselinePath, 'utf8').trim(), 10)
|
|
2420
|
+
return Number.isInteger(n) && n >= 0 ? n : null
|
|
2421
|
+
} catch {
|
|
2422
|
+
return null
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
/**
|
|
2427
|
+
* knowledge-baseline 棘轮三态对比(FR-03/R-05):
|
|
2428
|
+
* - 基线缺失 → { status:'disabled' }(未启用,调用方零输出零写盘)
|
|
2429
|
+
* - count > baseline → { status:'over' }(软警告,不阻断——棘轮起步只警告)
|
|
2430
|
+
* - count < baseline → 自动收紧:基线文件改写为当前值(writeAtomicSync,Windows 兼容),
|
|
2431
|
+
* → { status:'tightened' };写盘失败降级 steady(下次收尾再试,不谎报收紧)
|
|
2432
|
+
* - count == baseline → { status:'steady' }
|
|
2433
|
+
* 已 export 供 test/knowledge-baseline.test.mjs 断言三态与收紧副作用。
|
|
2434
|
+
*/
|
|
2435
|
+
export function checkKnowledgeBaselineRatchet(specBase) {
|
|
2436
|
+
const baseline = readKnowledgeBaseline(specBase)
|
|
2437
|
+
const count = countUncategorizedEntries(join(specBase, 'knowledge'))
|
|
2438
|
+
if (baseline === null) return { status: 'disabled', count, baseline: null }
|
|
2439
|
+
if (count > baseline) return { status: 'over', count, baseline }
|
|
2440
|
+
if (count < baseline) {
|
|
2441
|
+
try {
|
|
2442
|
+
writeAtomicSync(join(specBase, 'knowledge-baseline'), `${count}\n`)
|
|
2443
|
+
return { status: 'tightened', count, baseline }
|
|
2444
|
+
} catch {
|
|
2445
|
+
return { status: 'steady', count, baseline }
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
return { status: 'steady', count, baseline }
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
/**
|
|
2452
|
+
* 棘轮对比 + console 渲染(quick --done 与 archive 收尾共用):over → ⚠️ 软警告(条数 +
|
|
2453
|
+
* 建议 knowledge classify 清单,不阻断不抛错);tightened → 📉 单行留痕;disabled/steady
|
|
2454
|
+
* 零输出。自身 try/catch fail-open(R-05:棘轮误伤的兜底就是什么都不做)。
|
|
2455
|
+
*/
|
|
2456
|
+
function renderKnowledgeBaselineRatchet(specBase) {
|
|
2457
|
+
try {
|
|
2458
|
+
const r = checkKnowledgeBaselineRatchet(specBase)
|
|
2459
|
+
if (r && r.status === 'over') {
|
|
2460
|
+
console.warn(`\n⚠️ knowledge/uncategorized.md 待归类 ${r.count} 条,超 knowledge-baseline 基线 ${r.baseline}(软警告不阻断)`)
|
|
2461
|
+
console.warn(` 建议跑 sillyspec knowledge classify 逐条归类(待归类清单见 knowledge/uncategorized.md),条数降后基线自动收紧。`)
|
|
2462
|
+
} else if (r && r.status === 'tightened') {
|
|
2463
|
+
console.log(`📉 knowledge-baseline 已自动收紧:${r.baseline} → ${r.count}(uncategorized 待归类条数下降)`)
|
|
2464
|
+
}
|
|
2465
|
+
} catch { /* 棘轮渲染失败零副作用(fail-open,R-05) */ }
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
/** 根因「无,纯新增/纯样式」形态:无坑可归,归类提议不渲染(拿不准不写)。 */
|
|
2469
|
+
const PURE_NEW_CAUSE_RE = /^无(?:[,,].*)?$/
|
|
2470
|
+
|
|
2471
|
+
/**
|
|
2472
|
+
* 从 quick --done 四字段 outputText 提取「根因:」字段值(归类提议的查询串来源;进程内已过
|
|
2473
|
+
* validateQuickResult 校验,免读回盘)。多行字段块与单行压缩形态都支持;根因块内嵌套子字段
|
|
2474
|
+
* 列表行(- 现象:/- 根因: 等 D-004@v1 合法形态)随正文保留——只是匹配查询串,多文本无害。
|
|
2475
|
+
* 提取失败/无根因字段返回 ''。已 export 供测试断言提取口径。
|
|
2476
|
+
*/
|
|
2477
|
+
export function extractQuickCauseField(outputText) {
|
|
2478
|
+
const text = String(outputText || '')
|
|
2479
|
+
const m = text.match(/根因\s*[::]\s*([\s\S]*?)(?=(?:^|\n|\s)(?:方案|结果)\s*[::]|$)/)
|
|
2480
|
+
if (!m) return ''
|
|
2481
|
+
return m[1].replace(/\s+/g, ' ').trim()
|
|
2482
|
+
}
|
|
2483
|
+
|