sillyspec 3.28.2 → 3.28.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/sillyspec-auto/SKILL.md +16 -55
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +1 -1
- package/.claude/skills/sillyspec-continue/SKILL.md +8 -38
- package/.claude/skills/sillyspec-execute/SKILL.md +10 -2
- package/.claude/skills/sillyspec-knowledge/SKILL.md +19 -249
- package/.claude/skills/sillyspec-plan/SKILL.md +1 -1
- package/.claude/skills/sillyspec-propose/SKILL.md +6 -77
- package/.claude/skills/sillyspec-resume/SKILL.md +8 -57
- package/.claude/skills/sillyspec-state/SKILL.md +7 -46
- package/.claude/skills/sillyspec-workspace/SKILL.md +14 -156
- package/AGENTS.md +24 -0
- package/INSTRUCTIONS.md +12 -0
- package/package.json +1 -1
- package/src/archive-delta.js +127 -2
- package/src/change-delete.js +2 -1
- package/src/change-list.js +7 -2
- package/src/change-risk-profile.js +77 -6
- package/src/config-cat.js +12 -1
- package/src/constants.js +9 -1
- package/src/design-facts.js +64 -0
- package/src/dispatch/backends/sillyhub-mcp.js +17 -5
- package/src/docs-check.js +153 -27
- package/src/docs-migrate.js +124 -0
- package/src/doctor-diagnostics.js +441 -5
- package/src/foreign-declared.js +53 -2
- package/src/index.js +353 -28
- package/src/module-impact.js +77 -12
- package/src/plan-adopt-waves.js +13 -5
- package/src/progress/change-registry.js +35 -0
- package/src/progress.js +64 -1
- package/src/quick-recommend.js +6 -1
- package/src/quicklog.js +10 -2
- package/src/run/command.js +159 -13
- package/src/run/complete-handlers.js +162 -9
- package/src/run/complete.js +104 -5
- package/src/run/gates.js +116 -8
- package/src/run/progress-confirm.js +66 -0
- package/src/run/prompt.js +235 -36
- package/src/run/quick-audit.js +12 -5
- package/src/run/scan-profile.js +20 -2
- package/src/run/shared.js +84 -11
- package/src/run/stage.js +42 -0
- package/src/runtime-hygiene.js +84 -0
- package/src/scan-postcheck.js +5 -1
- package/src/spec-sync.js +63 -8
- package/src/stage-contract.js +83 -3
- package/src/stage-review.js +3 -0
- package/src/stages/archive.js +4 -4
- package/src/stages/brainstorm-auto.js +2 -1
- package/src/stages/brainstorm.js +11 -25
- package/src/stages/doctor.js +41 -515
- package/src/stages/execute.js +10 -14
- package/src/stages/plan-postcheck.js +191 -7
- package/src/stages/plan.js +7 -27
- package/src/stages/quick.js +15 -13
- package/src/stages/verify.js +23 -21
- package/src/sync-noise.js +158 -0
- package/src/sync.js +42 -17
- package/src/task-review.js +225 -1
- package/src/verify-facts-schema.js +141 -0
- package/src/verify-postcheck.js +421 -49
- package/src/verify-probes.js +142 -13
- package/src/workflow.js +6 -0
- package/src/worktree-apply.js +51 -13
- package/src/worktree.js +5 -1
- package/templates/agents-instruction.md +1 -1
- package/.claude/skills/verify-per-user/SKILL.md +0 -6
package/src/verify-probes.js
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
* 探针2(关键词提取半语义)/探针3.4 集成盲区/3.5 断言抽查/探针4(决策追踪语义)留 agent。
|
|
12
12
|
*
|
|
13
13
|
* verify-result.md 骨架:七章节固定结构 + 探针结果机械预填 + 其余章节 <!--TODO--> 占位。
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* 结论走「结论枚举:」固定槽行(刀③)——占位符不含枚举词,槽未填 extractVerifyConclusionSlot
|
|
15
|
+
* 返回 '' 即判不过,骨架不能直接过门(与 symbol-impact 骨架同款防偷懒语义)。
|
|
16
16
|
* P3b 增量:①章节标题行末尾 claims 层标注(可复跑探针/确定性检查/人工判断,纯后缀不新增行);
|
|
17
17
|
* ②--init 同步落盘 verify-facts.json 机器底稿(探针命令行 + 首跑关键指标 + 时间戳,CLI 全权写,
|
|
18
18
|
* 供事后独立复跑审计;重复 --init 覆盖为最近一次 init 快照)。
|
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'fs'
|
|
21
21
|
import { join, dirname, basename } from 'path'
|
|
22
22
|
import { gitQuiet } from './git-helper.js'
|
|
23
|
+
import {
|
|
24
|
+
FACTS_SCHEMA_VERSION, EVIDENCE_SLOT_HEADING, RECEIPT_SLOT_HEADING, parseEvidenceSlots,
|
|
25
|
+
} from './verify-facts-schema.js'
|
|
23
26
|
import { parseFileChangeListDetailed } from './change-list.js'
|
|
24
27
|
import { parseAllowedPaths } from './stages/plan-postcheck.js'
|
|
25
28
|
import { verifyApiParity, _readWorktreeMeta } from './contract-matrix.js'
|
|
@@ -310,8 +313,11 @@ export function buildVerifyFacts(result, { changeName, now } = {}) {
|
|
|
310
313
|
const p3 = (result && result.probe3) || {}
|
|
311
314
|
const p5 = (result && result.probe5) || {}
|
|
312
315
|
const p6 = (result && result.probe6) || {}
|
|
316
|
+
// v2(2026-09-08-ir-verify-facts):probes 机器段原样;conclusion/tests/requiredEvidence/
|
|
317
|
+
// runtimeEvidence/factsConsistency 五段是 slot-backfill/实测回填段(D-001@v2),--init 快照
|
|
318
|
+
// 不落键(writeVerifyFacts 分段合并时保留既有固化段),由 backfillFactsFromMdAndTests 填。
|
|
313
319
|
return {
|
|
314
|
-
schemaVersion:
|
|
320
|
+
schemaVersion: 2,
|
|
315
321
|
change: changeName,
|
|
316
322
|
generatedAt: now || new Date().toISOString(),
|
|
317
323
|
probes: {
|
|
@@ -350,37 +356,160 @@ export function buildVerifyFacts(result, { changeName, now } = {}) {
|
|
|
350
356
|
}
|
|
351
357
|
|
|
352
358
|
/**
|
|
353
|
-
* verify-probes --init 落盘 verify-facts.json(CLI
|
|
354
|
-
*
|
|
355
|
-
*
|
|
359
|
+
* verify-probes --init 落盘 verify-facts.json(CLI 全权写)。v2(2026-09-08-ir-verify-facts)改
|
|
360
|
+
* 分段合并:re-init 刷新机器段(probes/generatedAt),保留既有 slot-backfill/实测固化段
|
|
361
|
+
* (conclusion/tests/requiredEvidence/runtimeEvidence)——旧「无条件覆盖」会抹掉 --done 回填数据
|
|
362
|
+
* (设计审查 P1-5)。agent 勿手改:复跑审计的对照快照,防篡改基准是 verify-result.md 正文。
|
|
356
363
|
* @param {string} changeDir 变更目录(spec 根下 changes/<name>)
|
|
357
364
|
* @param {object} result runVerifyProbes 返回值
|
|
358
365
|
* @param {string} changeName 变更名(统一命令行呈现)
|
|
359
366
|
* @returns {{ facts: object, path: string }}
|
|
360
367
|
*/
|
|
361
368
|
export function writeVerifyFacts(changeDir, result, changeName) {
|
|
362
|
-
const
|
|
369
|
+
const fresh = buildVerifyFacts(result, { changeName })
|
|
363
370
|
const factsPath = join(changeDir, 'verify-facts.json')
|
|
371
|
+
let facts = fresh
|
|
372
|
+
try {
|
|
373
|
+
const prev = JSON.parse(readFileSync(factsPath, 'utf8'))
|
|
374
|
+
if (prev && prev.schemaVersion === FACTS_SCHEMA_VERSION) {
|
|
375
|
+
facts = {
|
|
376
|
+
...fresh,
|
|
377
|
+
conclusion: prev.conclusion,
|
|
378
|
+
tests: prev.tests,
|
|
379
|
+
requiredEvidence: prev.requiredEvidence,
|
|
380
|
+
runtimeEvidence: prev.runtimeEvidence,
|
|
381
|
+
factsConsistency: prev.factsConsistency, // 保留至上次对比结论(下次 --done 重算覆盖)
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
} catch { /* 首次落盘/v1/损坏 → 全新快照(v1 无固化段,直接升 v2) */ }
|
|
364
385
|
writeFileSync(factsPath, JSON.stringify(facts, null, 2) + '\n')
|
|
365
|
-
console.log(`📝 已刷新 verify-facts.json 机器底稿: ${factsPath}(CLI 全权写,勿手改)`)
|
|
386
|
+
console.log(`📝 已刷新 verify-facts.json 机器底稿: ${factsPath}(v2 分段合并,固化段保留;CLI 全权写,勿手改)`)
|
|
366
387
|
return { facts, path: factsPath }
|
|
367
388
|
}
|
|
368
389
|
|
|
390
|
+
/**
|
|
391
|
+
* --done 回填(D-001@v2 slot-backfill):结论枚举槽 + 证据/回执槽解析固化进 facts;
|
|
392
|
+
* testCheckResult 提供时回填 tests 段(实测在 runValidators 之后 → gates 二次回填,FR-03 次序)。
|
|
393
|
+
* 机器段(probes/factsConsistency)不经本函数(writeVerifyFacts / checkProbeConsistency 所有)。
|
|
394
|
+
* fail-soft:facts 缺失时原地升 v2(probes 保留或空);落盘失败 warn 不抛。
|
|
395
|
+
* @param {string} factsPath verify-facts.json 路径
|
|
396
|
+
* @param {{ verifyMd: string, testCheckResult?: object|null }} opts
|
|
397
|
+
* @returns {{ facts: object, conclusion: string|null, evidenceCount: number, receiptCount: number, testsBackfilled: boolean }}
|
|
398
|
+
*/
|
|
399
|
+
export function backfillFactsFromMdAndTests(factsPath, { verifyMd, testCheckResult = null, conclusion = null }) {
|
|
400
|
+
let facts = null
|
|
401
|
+
try { facts = JSON.parse(readFileSync(factsPath, 'utf8')) } catch { /* 缺失见下 */ }
|
|
402
|
+
// 只固化既有底稿,不无中生有(2026-09-08-ir-verify-facts 接线实证):无 facts 的存量变更
|
|
403
|
+
// (未跑过 --init)若被回填凭空建出 facts.json,checkProbeConsistency 的「有 facts 无探针
|
|
404
|
+
// 子节 → error」判别会把存量兼容 skip 误升 error(run-complete-step-verify e2e 抓出)。
|
|
405
|
+
// 底稿创建唯一入口是 verify-probes --init。
|
|
406
|
+
if (!facts) {
|
|
407
|
+
return { facts: null, skipped: true, conclusion: null, evidenceCount: 0, receiptCount: 0, testsBackfilled: false, reason: '无 verify-facts.json(未跑 --init),回填跳过——底稿创建唯一入口是 --init' }
|
|
408
|
+
}
|
|
409
|
+
if (facts.schemaVersion !== FACTS_SCHEMA_VERSION) {
|
|
410
|
+
facts = { ...facts, schemaVersion: FACTS_SCHEMA_VERSION, probes: (facts && facts.probes) || {} }
|
|
411
|
+
}
|
|
412
|
+
const slots = parseEvidenceSlots(verifyMd || '')
|
|
413
|
+
// conclusion 由调用方传 stage-contract.extractVerifyConclusionSlot(verifyMd) 的结果
|
|
414
|
+
// (避免 verify-probes → stage-contract 静态 import 的潜在环,分层单向)
|
|
415
|
+
const conclusionSlot = conclusion || null
|
|
416
|
+
if (conclusionSlot) facts.conclusion = conclusionSlot
|
|
417
|
+
if (slots.hasEvidenceSlot && slots.requiredEvidence.length > 0) {
|
|
418
|
+
facts.requiredEvidence = slots.requiredEvidence.map(e => ({
|
|
419
|
+
task: e.task, status: e.status, verifiedFiles: e.verifiedFiles,
|
|
420
|
+
...(e.exempt ? { exempt: true, exemptionReason: e.exemptionReason } : {}),
|
|
421
|
+
}))
|
|
422
|
+
}
|
|
423
|
+
if (slots.hasReceiptSlot && slots.runtimeEvidence.length > 0) {
|
|
424
|
+
facts.runtimeEvidence = slots.runtimeEvidence
|
|
425
|
+
}
|
|
426
|
+
let testsBackfilled = false
|
|
427
|
+
if (testCheckResult && testCheckResult.status && testCheckResult.status !== 'skipped') {
|
|
428
|
+
facts.tests = {
|
|
429
|
+
command: testCheckResult.command || null,
|
|
430
|
+
exitCode: typeof testCheckResult.exitCode === 'number' ? testCheckResult.exitCode : null,
|
|
431
|
+
strategy: testCheckResult.mode || testCheckResult.strategy || null,
|
|
432
|
+
failedRemaining: testCheckResult.failureNames || testCheckResult.failedTests || [],
|
|
433
|
+
passedAt: new Date().toISOString(),
|
|
434
|
+
}
|
|
435
|
+
testsBackfilled = true
|
|
436
|
+
}
|
|
437
|
+
try {
|
|
438
|
+
writeFileSync(factsPath, JSON.stringify(facts, null, 2) + '\n')
|
|
439
|
+
} catch (e) {
|
|
440
|
+
console.warn(`⚠️ facts 回填落盘失败(fail-soft,不阻断): ${e && e.message ? e.message : e}`)
|
|
441
|
+
}
|
|
442
|
+
return { facts, conclusion: conclusionSlot, evidenceCount: slots.requiredEvidence.length, receiptCount: slots.runtimeEvidence.length, testsBackfilled }
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* md 槽段缺失补齐(--init 段落级补齐,2026-09-08-ir-verify-facts R-02):已存在的
|
|
447
|
+
* verify-result.md 缺「证据账/集成验证回执」槽段时仅追加骨架(不触碰既有正文),幂等二跑零改动。
|
|
448
|
+
* @param {string} mdPath verify-result.md 路径
|
|
449
|
+
* @param {Array<{task:string,evidence:string[]}>} [requiredEvidenceItems] verify-required-evidence.json items(预填 task 行)
|
|
450
|
+
* @returns {{ added: string[] }} 追加的槽段标题列表
|
|
451
|
+
*/
|
|
452
|
+
export function backfillMissingEvidenceSlots(mdPath, requiredEvidenceItems = []) {
|
|
453
|
+
let text = ''
|
|
454
|
+
try { text = readFileSync(mdPath, 'utf8') } catch { return { added: [] } }
|
|
455
|
+
const normalized = text.replace(/\r\n/g, '\n')
|
|
456
|
+
const added = []
|
|
457
|
+
const blocks = []
|
|
458
|
+
if (!normalized.includes(EVIDENCE_SLOT_HEADING)) {
|
|
459
|
+
const lines = [EVIDENCE_SLOT_HEADING, '[层:人工判断——CLI 核验]', '', '<!-- 无 cannot_verify 任务时本节写「无」 -->']
|
|
460
|
+
if (requiredEvidenceItems.length > 0) {
|
|
461
|
+
for (const it of requiredEvidenceItems) {
|
|
462
|
+
lines.push(`- ${it.task}: <待填:三选一> | verifiedFiles: <精确路径,逗号分隔>(satisfied 必填;豁免时填 missing 并加(豁免:<一句话理由>)后缀)`)
|
|
463
|
+
}
|
|
464
|
+
} else {
|
|
465
|
+
lines.push('无(本次变更无 cannot_verify 任务)')
|
|
466
|
+
}
|
|
467
|
+
lines.push('')
|
|
468
|
+
blocks.push(lines.join('\n'))
|
|
469
|
+
added.push(EVIDENCE_SLOT_HEADING)
|
|
470
|
+
}
|
|
471
|
+
if (!normalized.includes(RECEIPT_SLOT_HEADING)) {
|
|
472
|
+
blocks.push([
|
|
473
|
+
RECEIPT_SLOT_HEADING, '[层:自述声明——CLI 一致性校验]', '',
|
|
474
|
+
'<!-- integration-critical/deployment-critical 变更必填;其余写「无」 -->',
|
|
475
|
+
'- claim: <待填:一句话> | command: <待填:命令> | exit: <待填:0 或非 0> | log: <待填:日志路径>',
|
|
476
|
+
'',
|
|
477
|
+
].join('\n'))
|
|
478
|
+
added.push(RECEIPT_SLOT_HEADING)
|
|
479
|
+
}
|
|
480
|
+
if (added.length > 0) {
|
|
481
|
+
writeFileSync(mdPath, normalized.replace(/\n?$/, '\n') + blocks.join('\n') + '\n')
|
|
482
|
+
}
|
|
483
|
+
return { added }
|
|
484
|
+
}
|
|
485
|
+
|
|
369
486
|
/**
|
|
370
487
|
* 生成 verify-result.md 骨架(七章节;探针结果机械预填,语义章节 <!--TODO--> 占位)。
|
|
371
|
-
*
|
|
488
|
+
* 结论走「结论枚举:」固定槽行(刀③,2026-09-08)——占位符不含枚举词(<待填:三选一>),
|
|
489
|
+
* 槽未填 → extractVerifyConclusionSlot 返回 '' → gate 判不过(fail-closed;旧占位符
|
|
490
|
+
* `<待填:PASS 或 FAIL>` 含 PASS 字样会被窗口正则误读成已填 PASS,已修)。
|
|
372
491
|
* 章节标题行末尾带 claims 层标注(P3b):探针结果=可复跑探针 / 测试结果=确定性检查 / 其余=
|
|
373
|
-
*
|
|
492
|
+
* 人工判断——纯渲染层后缀,不新增行;结论解析槽优先于标题关键词窗口(存量兼容)。
|
|
374
493
|
* @returns {string|null} 骨架全文;无 design.md/tasks.md(非完整流程变更)→ null
|
|
375
494
|
*/
|
|
376
495
|
export function generateVerifyResultSkeleton(result) {
|
|
377
496
|
const L = [
|
|
378
497
|
'# 验证报告(骨架由 `sillyspec verify-probes --change <变更名> --init` 生成)',
|
|
379
498
|
'',
|
|
380
|
-
'> 探针结果已机械预填;其余章节把 `<!--TODO-->`
|
|
381
|
-
'>
|
|
499
|
+
'> 探针结果已机械预填;其余章节把 `<!--TODO-->` 替换为真实内容。**结论只认「结论枚举:」槽行**——',
|
|
500
|
+
'> 槽行留「<待填:三选一>」会被 gate 判不过(fail-closed),正文其他位置的 PASS/FAIL 字样不参与判定。',
|
|
501
|
+
'',
|
|
502
|
+
'## 结论 [层:人工判断]',
|
|
503
|
+
'',
|
|
504
|
+
'结论枚举:`<待填:三选一>`(把尖括号占位整体替换为 PASS / PASS WITH NOTES / FAIL 之一;一句话理由写在枚举后同行或下一行)',
|
|
505
|
+
'',
|
|
506
|
+
'## 证据账(cannot_verify 任务) [层:人工判断——CLI 核验]',
|
|
507
|
+
'<!-- 无 cannot_verify 任务时本节写「无」;有则逐 task 一行 -->',
|
|
508
|
+
'- task-NN: <待填:三选一> | verifiedFiles: <精确路径,逗号分隔>(satisfied 必填;豁免时填 missing 并加(豁免:<一句话理由>)后缀)',
|
|
382
509
|
'',
|
|
383
|
-
'##
|
|
510
|
+
'## 集成验证回执 [层:自述声明——CLI 一致性校验]',
|
|
511
|
+
'<!-- integration-critical/deployment-critical 变更必填;其余写「无」 -->',
|
|
512
|
+
'- claim: <待填:一句话> | command: <待填:命令> | exit: <待填:0 或非 0> | log: <待填:日志路径>',
|
|
384
513
|
'',
|
|
385
514
|
'## 任务完成度 [层:人工判断]',
|
|
386
515
|
'<!--TODO: 逐 task 对照 tasks.md 勾选与验收标准,完成/未完成/存疑三态-->',
|
package/src/workflow.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { readFileSync, existsSync, readdirSync, writeFileSync, mkdirSync } from 'fs'
|
|
13
13
|
import { join, resolve, basename } from 'path'
|
|
14
14
|
import jsYaml from 'js-yaml'
|
|
15
|
+
import { pruneTimestampedEntries } from './runtime-hygiene.js'
|
|
15
16
|
import { WORKFLOW_STATUS } from './constants.js'
|
|
16
17
|
import { contentNonEmpty, lineCount, missingSections, placeholderLineMatches } from './check-primitives.js'
|
|
17
18
|
import { collectInvalidDocRefs } from './docs-check.js'
|
|
@@ -626,6 +627,11 @@ export function saveWorkflowRun(result, options = {}) {
|
|
|
626
627
|
|
|
627
628
|
try {
|
|
628
629
|
writeFileSync(filepath, JSON.stringify(record, null, 2), 'utf8')
|
|
630
|
+
// 写入侧滚动裁剪(runtime-hygiene.js,2026-09-08 系统性回收):workflow-runs/ 只写不收,
|
|
631
|
+
// 实证本仓 21 份累积。文件名以零填充时间戳开头,name 字典序 == 时间序。fail-open 静默。
|
|
632
|
+
try {
|
|
633
|
+
pruneTimestampedEntries({ dir: runDir, keep: 30 })
|
|
634
|
+
} catch { /* 卫生动作 fail-open */ }
|
|
629
635
|
return filepath
|
|
630
636
|
} catch (e) {
|
|
631
637
|
console.warn('⚠️ 保存 workflow run 失败:', e.message)
|
package/src/worktree-apply.js
CHANGED
|
@@ -217,23 +217,31 @@ function getBlobHashMap(cwd, treeish, files) {
|
|
|
217
217
|
* 跨仓文件不在主仓 worktree 故永不进 diff);跨仓 task 走 no-op(D-009),其 allowed_paths 仅
|
|
218
218
|
* 作 Map 切片返回,供上游(assess / 跨仓 review 校验)按 repo 取用。
|
|
219
219
|
*
|
|
220
|
+
* **specBase 解析(2026-09-08 用户反馈:平台模式 apply allowlist 读本地空目录整批 BLOCKED)**:
|
|
221
|
+
* 优先级 opts.specBase(调用方传 platformOpts.specRoot)> 平台指针(.sillyspec-platform.json
|
|
222
|
+
* 静默读,不触发 resolvePlatformSpecDir 的守卫 warn)> 本地 projectRoot/.sillyspec。平台模式下
|
|
223
|
+
* changes/ 实体在 specRoot,本地目录常为空——旧硬编码 CHANGES_REL 恒读空 → allow 集恒空 →
|
|
224
|
+
* Gate1 整批误拦(sync.js BUG-01 同族)。
|
|
225
|
+
*
|
|
220
226
|
* @param {string} projectRoot - 主仓库根
|
|
221
227
|
* @param {string} changeName - 变更名
|
|
228
|
+
* @param {{ specBase?: string }} [opts] specBase 显式传入(平台模式必传 specRoot)
|
|
222
229
|
* @returns {Map<string, Set<string>>} repoKey → 并集清单 Set(无 design 清单且无 task 卡片时为空 Map)
|
|
223
230
|
*/
|
|
224
|
-
export function resolveApplyAllowSet(projectRoot, changeName) {
|
|
231
|
+
export function resolveApplyAllowSet(projectRoot, changeName, opts = {}) {
|
|
225
232
|
const repoMap = new Map();
|
|
226
233
|
const getOrCreate = (key) => {
|
|
227
234
|
if (!repoMap.has(key)) repoMap.set(key, new Set());
|
|
228
235
|
return repoMap.get(key);
|
|
229
236
|
};
|
|
230
|
-
|
|
237
|
+
const specBase = opts.specBase || _silentPointerSpecRoot(projectRoot) || join(projectRoot, '.sillyspec');
|
|
238
|
+
// design §6 清单归属 main(清单路径相对主仓根;跨仓 task 的 allowed_paths 由 task 卡 repo 切片)
|
|
231
239
|
const mainSet = getOrCreate('main');
|
|
232
240
|
parseFileChangeList(
|
|
233
|
-
join(
|
|
241
|
+
join(specBase, 'changes', changeName, 'design.md'),
|
|
234
242
|
{ keepSillyspecDocs: true }
|
|
235
243
|
).forEach(p => mainSet.add(p));
|
|
236
|
-
const tasksDir = join(
|
|
244
|
+
const tasksDir = join(specBase, 'changes', changeName, 'tasks');
|
|
237
245
|
if (existsSync(tasksDir)) {
|
|
238
246
|
for (const tf of readdirSync(tasksDir).filter(f => /^task-\d+\.md$/.test(f))) {
|
|
239
247
|
const content = readFileSync(join(tasksDir, tf), 'utf8');
|
|
@@ -246,6 +254,18 @@ export function resolveApplyAllowSet(projectRoot, changeName) {
|
|
|
246
254
|
return repoMap;
|
|
247
255
|
}
|
|
248
256
|
|
|
257
|
+
// 平台指针静默读(worktree-apply 内部用):只读 <projectRoot>/.sillyspec-platform.json 的
|
|
258
|
+
// specRoot 字段,不做存在性/自指/temp 守卫、不打 warn——allowlist 只需一个「读得到就用」的
|
|
259
|
+
// 基准目录,坏指针/缺字段落回本地(与 sync-noise.js bindSyncNoiseFromCwd 同款零副作用风格)。
|
|
260
|
+
function _silentPointerSpecRoot(projectRoot) {
|
|
261
|
+
try {
|
|
262
|
+
const p = join(projectRoot, '.sillyspec-platform.json');
|
|
263
|
+
if (!existsSync(p)) return null;
|
|
264
|
+
const s = JSON.parse(readFileSync(p, 'utf8').replace(/^\uFEFF/, ''));
|
|
265
|
+
return s && typeof s.specRoot === 'string' && s.specRoot ? s.specRoot : null;
|
|
266
|
+
} catch { return null; }
|
|
267
|
+
}
|
|
268
|
+
|
|
249
269
|
/**
|
|
250
270
|
* 收集最新 execute run 各 task review.json 的 changedFiles 声明 → Map<repoKey, string[]>。
|
|
251
271
|
*
|
|
@@ -257,14 +277,23 @@ export function resolveApplyAllowSet(projectRoot, changeName) {
|
|
|
257
277
|
* collectExecuteChangedFiles 一致:change 戳归属 run、.sillyspec//meta.json 过滤、
|
|
258
278
|
* review.repo 切片(跨仓声明不进 main 集)。读不到 run/review → 空 Map(fail-closed 回退旧行为)。
|
|
259
279
|
*
|
|
280
|
+
* runtimeRoot 解析(2026-09-08 平台模式修复,与 resolveApplyAllowSet 的 specBase 同族):
|
|
281
|
+
* 优先级 opts.runtimeRoot(调用方传 resolveRuntimeRoot(platformOpts, specBase) 产物)>
|
|
282
|
+
* 指针 specRoot/.runtime(静默读)> 本地 projectRoot/.sillyspec/.runtime。平台模式
|
|
283
|
+
* execute-runs 在平台 runtime root,旧硬编码本地恒空 → 第三源恒缺失。
|
|
284
|
+
*
|
|
260
285
|
* @param {string} projectRoot - 主仓库根
|
|
261
286
|
* @param {string} changeName - 变更名
|
|
287
|
+
* @param {{ runtimeRoot?: string }} [opts]
|
|
262
288
|
* @returns {Map<string, string[]>}
|
|
263
289
|
*/
|
|
264
|
-
export function collectReviewDeclaredFiles(projectRoot, changeName) {
|
|
290
|
+
export function collectReviewDeclaredFiles(projectRoot, changeName, opts = {}) {
|
|
265
291
|
const byRepo = new Map();
|
|
266
292
|
try {
|
|
267
|
-
const
|
|
293
|
+
const ptrSpec = _silentPointerSpecRoot(projectRoot);
|
|
294
|
+
const runtimeRoot = opts.runtimeRoot
|
|
295
|
+
|| (ptrSpec ? join(ptrSpec, '.runtime') : null)
|
|
296
|
+
|| join(projectRoot, '.sillyspec', '.runtime');
|
|
268
297
|
const runId = resolveLatestExecuteRunIdWithTasks({ runtimeRoot, changeName });
|
|
269
298
|
if (!runId) return byRepo;
|
|
270
299
|
const runTasksDir = join(runtimeRoot, 'execute-runs', runId, 'tasks');
|
|
@@ -504,10 +533,11 @@ function applyCrossRepoWorktrees(changeName, projectRoot, ctx, { checkOnly = fal
|
|
|
504
533
|
continue;
|
|
505
534
|
}
|
|
506
535
|
|
|
507
|
-
// 清单校验(per-repo 切片 + review 声明切片并入,与主仓 Gate1/3b
|
|
508
|
-
|
|
536
|
+
// 清单校验(per-repo 切片 + review 声明切片并入,与主仓 Gate1/3b 同语义)。
|
|
537
|
+
// specBase 显式传(平台模式 changes/ 在 specRoot;本地模式与旧硬编码同值零回归)。
|
|
538
|
+
const allowMap = resolveApplyAllowSet(projectRoot, changeName, { specBase });
|
|
509
539
|
let allowSet = allowMap.get(repoKey) || new Set();
|
|
510
|
-
const reviewDeclared = collectReviewDeclaredFiles(projectRoot, changeName).get(repoKey);
|
|
540
|
+
const reviewDeclared = collectReviewDeclaredFiles(projectRoot, changeName, { runtimeRoot: join(specBase, '.runtime') }).get(repoKey);
|
|
511
541
|
if (reviewDeclared) {
|
|
512
542
|
const merged = new Set([...allowSet]);
|
|
513
543
|
for (const f of reviewDeclared) {
|
|
@@ -754,15 +784,21 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
|
|
|
754
784
|
}
|
|
755
785
|
|
|
756
786
|
// --- 3. 解析 apply 文件清单(design §6 清单 ∪ plan TaskCard allowed_paths,execute 复盘 c) ---
|
|
787
|
+
// allowlist 基准目录(2026-09-08 平台模式修复):ctx.platformOpts.specRoot 优先(execute
|
|
788
|
+
// ctx 透传),否则 resolveApplyAllowSet 内部静默读平台指针兜底,最后本地——旧硬编码在
|
|
789
|
+
// 平台模式读本地空 changes/ → allow 集恒空 → Gate1 整批误拦。
|
|
790
|
+
const allowSpecBase = (ctx && ctx.platformOpts && ctx.platformOpts.specRoot) || undefined;
|
|
791
|
+
const allowRuntimeRoot = (ctx && ctx.platformOpts && ctx.platformOpts.runtimeRoot)
|
|
792
|
+
|| (allowSpecBase ? join(allowSpecBase, '.runtime') : undefined);
|
|
757
793
|
// resolveApplyAllowSet 返回 Map<repo, Set>(task-05 跨仓切片);主仓 apply 只消费 main 仓 Set
|
|
758
794
|
// (主仓 worktree diff 只含主仓文件,跨仓文件在跨仓仓不进主仓 worktree diff)。跨仓 allowed_paths
|
|
759
795
|
// 仅作 Map 切片返回供上游用,主流程不消费(跨仓 apply=no-op,D-009)。
|
|
760
|
-
const allowMap = resolveApplyAllowSet(projectRoot, changeName);
|
|
796
|
+
const allowMap = resolveApplyAllowSet(projectRoot, changeName, { specBase: allowSpecBase });
|
|
761
797
|
// --- 3b. review.json 声明偏差文件并入(坑 apply-undeclared-deviation-block)---
|
|
762
798
|
// 第三源:reviewer 声明的 changedFiles(已过 Task Review Gate git 证据交叉校验)——执行期
|
|
763
799
|
// 有据越界(facade 转发/名单测试)不再逼回改 design.md。各 repo 切片各自并入;仅靠 review
|
|
764
800
|
// 放行的文件记审计 warning(result.reviewAdmittedFiles),完全越界文件仍拦(Gate1 在扩展后判)。
|
|
765
|
-
const reviewDeclaredByRepo = collectReviewDeclaredFiles(projectRoot, changeName);
|
|
801
|
+
const reviewDeclaredByRepo = collectReviewDeclaredFiles(projectRoot, changeName, { runtimeRoot: allowRuntimeRoot });
|
|
766
802
|
const reviewAdmittedFiles = [];
|
|
767
803
|
for (const [repoKey, files] of reviewDeclaredByRepo) {
|
|
768
804
|
const repoSet = allowMap.get(repoKey);
|
|
@@ -1693,13 +1729,15 @@ export function assessApplyRisk(changeName, { cwd } = {}) {
|
|
|
1693
1729
|
|
|
1694
1730
|
// design §6 标记为「顺带修复」的文件(坑 worktree-execute-apply-friction 坑1):合规修预存债,
|
|
1695
1731
|
// 不属任何 task 边界,assess 豁免 allowed_paths 严格校验(降级 warning),避免被迫 cherry-pick 绕过。
|
|
1696
|
-
|
|
1732
|
+
// specBase 走指针/本地解析(2026-09-08 平台模式修复,与 resolveApplyAllowSet 同族)。
|
|
1733
|
+
const assessSpecBase = _silentPointerSpecRoot(projectRoot) || join(projectRoot, '.sillyspec');
|
|
1734
|
+
const designPath = join(assessSpecBase, 'changes', changeName, 'design.md');
|
|
1697
1735
|
const incidentalSet = new Set(
|
|
1698
1736
|
parseFileChangeListDetailed(designPath).filter(e => e.incidental).map(e => e.path)
|
|
1699
1737
|
);
|
|
1700
1738
|
// review.json 声明偏差文件(坑 apply-undeclared-deviation-block):与顺带修复同等待遇——
|
|
1701
1739
|
// reviewer 声明(已过 git 证据校验)的执行期偏差豁免 allowed_paths 严格校验,降 warning 注明来源。
|
|
1702
|
-
const reviewDeclaredSet = new Set(collectReviewDeclaredFiles(projectRoot, changeName).get('main') || []);
|
|
1740
|
+
const reviewDeclaredSet = new Set(collectReviewDeclaredFiles(projectRoot, changeName, { runtimeRoot: join(assessSpecBase, '.runtime') }).get('main') || []);
|
|
1703
1741
|
|
|
1704
1742
|
// 检查 2: 变更在 allowed_paths 内(仅在 TaskCard 存在时);顺带修复/review 声明文件豁免。
|
|
1705
1743
|
// 匹配换 pathMatches(与 Gate1/plan-postcheck 同语义容差),消除原字面前缀弱匹配漂移。
|
package/src/worktree.js
CHANGED
|
@@ -203,7 +203,11 @@ function printSyncReport(diag, baseHash, changeName) {
|
|
|
203
203
|
}
|
|
204
204
|
|
|
205
205
|
function parseJSON(raw) {
|
|
206
|
-
|
|
206
|
+
// BOM 容错(2026-09-08 用户反馈:meta.json 带 UTF-8 BOM 被当损坏 → apply 报
|
|
207
|
+
// 「meta.json 不存在或已损坏」、幽灵判定误清):Windows 编辑器(记事本/部分 PowerShell
|
|
208
|
+
// 重定向)写 JSON 常带 \uFEFF 前缀,JSON.parse 对首字符 BOM 直接抛。剥掉再 parse。
|
|
209
|
+
const s = typeof raw === 'string' ? raw.replace(/^\uFEFF/, '') : raw
|
|
210
|
+
try { return JSON.parse(s); } catch { return null; }
|
|
207
211
|
}
|
|
208
212
|
|
|
209
213
|
/**
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
5. **执行顺序**:文档 → 读代码 → 写测试 → 写实现 → 跑测试 → 验收 → 更新文档。
|
|
12
12
|
6. **判规模选档**:≤3 文件、范围明确走 `quick`;多阶段 / 架构级走完整流程。
|
|
13
13
|
7. **代码先行不补流程(倒推 B 模式)**:代码若已先写好,**不回头补 brainstorm/plan 装样子**——用 `quick --done` 收尾 + 补 quicklog 条目,把已落盘改动如实登记进进度库。
|
|
14
|
-
8. **实证核验再 `--done
|
|
14
|
+
8. **实证核验再 `--done`**:触及 `src`/`test` 的改动,CLI 会在 `quick --done` 时**亲自实测** `.sillyspec/local.yaml` 的 `commands.test` / `commands.lint`(门禁:实测失败阻断 --done 回 pending;纯 doc/配置与未配置命令自动跳过;倒推 B 模式按 --files 声明边界兜底判定)。agent 侧预跑**可选**——想省「--done 被拦→修复→重跑」一轮时才预跑;纯 doc/配置改动无需跑。以落盘文件与测试结果为准,不信口头"已完成"。
|
|
15
15
|
9. **中途停下不靠额外命令存进度**——进度已由上一次 `--done` 自动落盘;恢复时用 `sillyspec progress show` 查看进度,再用 `sillyspec run <stage>` 续跑,不直接 commit 半成品。
|
|
16
16
|
10. **实现完成后对照文档验收**(design.md / 模块文档),并检查是否影响已有测试。
|
|
17
17
|
11. **非测试逻辑本身有误时,禁止改测试来"通过"**——修逻辑,不修测试。
|