sillyspec 3.26.7 → 3.26.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-archive/SKILL.md +1 -2
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +1 -2
- package/.claude/skills/sillyspec-doctor/SKILL.md +2 -0
- package/.claude/skills/sillyspec-execute/SKILL.md +10 -2
- package/.claude/skills/sillyspec-explore/SKILL.md +0 -1
- package/.claude/skills/sillyspec-plan/SKILL.md +1 -2
- package/.claude/skills/sillyspec-quick/SKILL.md +1 -2
- package/.claude/skills/sillyspec-scan/SKILL.md +2 -2
- package/.claude/skills/sillyspec-status/SKILL.md +0 -1
- package/.claude/skills/sillyspec-verify/SKILL.md +1 -2
- package/.husky/pre-push +9 -2
- package/README.md +5 -9
- package/bin/sillyspec.js +1 -1
- package/package.json +2 -2
- package/packages/dashboard/server/index.js +14 -8
- package/src/change-risk-profile.js +5 -207
- package/src/config-schema.js +310 -287
- package/src/constants.js +97 -106
- package/src/db-engine.js +1 -1
- package/src/docs-check.js +362 -0
- package/src/docs-debt.js +226 -0
- package/src/docs-gate.js +106 -0
- package/src/doctor-diagnostics.js +23 -2
- package/src/git-helper.js +2 -2
- package/src/hooks/worktree-guard.js +1 -1
- package/src/index.js +1963 -1787
- package/src/init.js +95 -17
- package/src/machine-interface.js +7 -4
- package/src/modules.js +490 -486
- package/src/progress/stage-machine.js +3 -0
- package/src/progress.js +37 -13
- package/src/quicklog.js +182 -1
- package/src/run/command.js +154 -26
- package/src/run/complete-handlers.js +1254 -1128
- package/src/run/complete.js +9 -33
- package/src/run/gates.js +85 -1
- package/src/run/prompt.js +82 -1
- package/src/run/quick-audit.js +23 -0
- package/src/run/shared.js +273 -5
- package/src/run/stage.js +17 -1
- package/src/scan-diff.js +388 -0
- package/src/scan-staleness.js +95 -0
- package/src/stage-contract-spec.js +16 -1
- package/src/stage-contract.js +8 -5
- package/src/stage-review.js +1 -1
- package/src/stages/brainstorm-auto.js +0 -1
- package/src/stages/brainstorm.js +3 -3
- package/src/stages/execute.js +35 -11
- package/src/stages/plan-postcheck.js +93 -36
- package/src/stages/plan.js +22 -7
- package/src/stages/quick.js +1 -1
- package/src/stages/scan.js +2 -2
- package/src/stages/verify.js +11 -12
- package/src/sync.js +39 -17
- package/src/task-review.js +14 -5
- package/src/workflow.js +8 -185
- package/src/worktree-apply.js +128 -3
- package/src/worktree.js +2 -2
- package/templates/claude-instruction.md +1 -1
- package/templates/prompts/taskcard-rules.md +1 -0
- package/templates/prompts/verify-probes.md +2 -2
- package/templates/skills/sillyspec-onboard/SKILL.md +0 -92
package/src/run/shared.js
CHANGED
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
* 本模块仅 re-export,run/ 层现有调用方路径与行为不变。
|
|
12
12
|
*/
|
|
13
13
|
import { basename, join, resolve, dirname, sep } from 'node:path'
|
|
14
|
-
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
|
|
14
|
+
import { existsSync, readdirSync, readFileSync, statSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
15
15
|
import { fileURLToPath } from 'node:url'
|
|
16
|
+
import os from 'node:os'
|
|
16
17
|
|
|
17
18
|
// safeGit 收口至 src/git-helper.js:import 建本地词法绑定(本模块内部 L128/130/431 调用)+
|
|
18
19
|
// re-export 供 run/ 层现有调用方继续从 shared.js 引用(pure re-export 不建本地绑定,会致内部 ReferenceError)。
|
|
@@ -94,6 +95,9 @@ export function resolvePromptIncludes(text) {
|
|
|
94
95
|
|
|
95
96
|
/**
|
|
96
97
|
* 找 .sillyspec 祖先目录:用户指定 specDir 优先,否则从 cwd 向上找。
|
|
98
|
+
* home 拒绝守卫:home 下的 .sillyspec 恒不命中——历史污染源(smoke 测试在 home 下临时目录
|
|
99
|
+
* 跑 CLI,_ensureDB 读路径即建库,~/.sillyspec 长出一整套平行进度库后,任何 home 子目录
|
|
100
|
+
* 跑命令都会向上撞它,污染自我延续)。撞 home 即停,回退 cwd/.sillyspec(子目录自建)。
|
|
97
101
|
* @param {string} cwd
|
|
98
102
|
* @param {object} [opts]
|
|
99
103
|
* @param {string} [opts.specDir] - 用户指定的 specDir(通过 --spec-dir 或 --spec-root)
|
|
@@ -101,10 +105,15 @@ export function resolvePromptIncludes(text) {
|
|
|
101
105
|
*/
|
|
102
106
|
export function resolveSpecDir(cwd, opts = {}) {
|
|
103
107
|
if (opts.specDir) return resolve(opts.specDir)
|
|
108
|
+
const home = os.homedir()
|
|
104
109
|
let dir = resolve(cwd)
|
|
105
110
|
while (true) {
|
|
106
|
-
|
|
107
|
-
|
|
111
|
+
// home 拒绝守卫:cwd 本身就在 home 下(含恰好在 home 跑命令)时,home 层不匹配 .sillyspec,
|
|
112
|
+
// 继续向上只会离项目更远(home 的父目录不可能有真项目 .sillyspec),直接回退 cwd/.sillyspec。
|
|
113
|
+
if (dir !== home) {
|
|
114
|
+
const candidate = join(dir, '.sillyspec')
|
|
115
|
+
if (existsSync(candidate)) return candidate
|
|
116
|
+
}
|
|
108
117
|
const parent = dirname(dir)
|
|
109
118
|
if (parent === dir) break
|
|
110
119
|
dir = parent
|
|
@@ -301,6 +310,84 @@ export function resolveQuickSessionsDir(platformOpts, localSpecBase) {
|
|
|
301
310
|
return join(resolveRuntimeRoot(platformOpts, localSpecBase), 'quick-sessions')
|
|
302
311
|
}
|
|
303
312
|
|
|
313
|
+
/**
|
|
314
|
+
* 平台指针三写(单一数据源,runCommand scan 与 cmdInit 平台模式共用)。
|
|
315
|
+
*
|
|
316
|
+
* 写三处:
|
|
317
|
+
* 1. <specRoot>/.runtime/platform-scan.json — 主持久化文件(后续 run/--done 恢复)
|
|
318
|
+
* 2. <cwd>/.sillyspec-platform.json — 轻量恢复指针(供裸调——不带 --spec-root——找回 specDir)
|
|
319
|
+
* 3. <cwd>/.sillyspec-platform-managed — 平台接管声明(无过期,独立于指针生命周期;
|
|
320
|
+
* 指针被 cleanup/STALE 清理后它仍在,resolvePlatformSpecDir/runCommand 恢复链靠它
|
|
321
|
+
* fail-closed 防静默落本地库。唯一删除路径 = platform disconnect)
|
|
322
|
+
*
|
|
323
|
+
* status 字段语义(POINTER_STATUS):
|
|
324
|
+
* - init 落盘时传 status: 'active'(已接入未 scan,24h STALE 清理不作用于它)
|
|
325
|
+
* - scan 完成时 complete-handlers 升级为 'scan_completed'
|
|
326
|
+
* - 不传 status 则省略(与 runCommand 历史行为一致)
|
|
327
|
+
*
|
|
328
|
+
* 写失败静默(不阻断主流程),返回 true/false 供调用方决定是否提示。
|
|
329
|
+
*
|
|
330
|
+
* @param {string} cwd - 恢复指针落盘目录(= 调用方命令的 cwd,通常为项目根)
|
|
331
|
+
* @param {object} platformOpts - { specRoot, runtimeRoot?, workspaceId?, scanRunId? }
|
|
332
|
+
* @param {object} [extra] - 追加字段(如 { status: 'active' },只进指针/主文件不进声明)
|
|
333
|
+
* @returns {boolean} 是否全部写入成功
|
|
334
|
+
*/
|
|
335
|
+
export const PLATFORM_MANAGED_FILENAME = '.sillyspec-platform-managed'
|
|
336
|
+
|
|
337
|
+
export function writePlatformPointer(cwd, platformOpts, extra = {}) {
|
|
338
|
+
if (!platformOpts || (!platformOpts.specRoot && !platformOpts.runtimeRoot)) return false
|
|
339
|
+
const payload = {
|
|
340
|
+
specRoot: platformOpts.specRoot || null,
|
|
341
|
+
runtimeRoot: platformOpts.runtimeRoot || null,
|
|
342
|
+
workspaceId: platformOpts.workspaceId || null,
|
|
343
|
+
scanRunId: platformOpts.scanRunId || null,
|
|
344
|
+
savedAt: new Date().toISOString(),
|
|
345
|
+
...extra,
|
|
346
|
+
}
|
|
347
|
+
// 声明文件字段集独立于指针 payload(D-E@v2 四字段:无 status/savedAt/scanRunId——
|
|
348
|
+
// 声明语义是"项目归平台管"这一持久事实,非 scan 会话状态)
|
|
349
|
+
const declaration = {
|
|
350
|
+
managed: true,
|
|
351
|
+
specRoot: platformOpts.specRoot || null,
|
|
352
|
+
workspaceId: platformOpts.workspaceId || null,
|
|
353
|
+
declaredAt: new Date().toISOString(),
|
|
354
|
+
}
|
|
355
|
+
try {
|
|
356
|
+
mkdirSync(join(platformOpts.specRoot, '.runtime'), { recursive: true })
|
|
357
|
+
writeFileSync(join(platformOpts.specRoot, '.runtime', 'platform-scan.json'), JSON.stringify(payload, null, 2) + '\n')
|
|
358
|
+
writeFileSync(join(cwd, '.sillyspec-platform.json'), JSON.stringify(payload, null, 2) + '\n')
|
|
359
|
+
writeFileSync(join(cwd, PLATFORM_MANAGED_FILENAME), JSON.stringify(declaration, null, 2) + '\n')
|
|
360
|
+
return true
|
|
361
|
+
} catch {
|
|
362
|
+
return false
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* 读平台接管声明(读侧宽容:任何异常情况返回 null = 视同无声明,不抛错)。
|
|
368
|
+
* 供 resolvePlatformSpecDir / runCommand 恢复链 / doctor 诊断三处消费。
|
|
369
|
+
*
|
|
370
|
+
* @param {string} cwd - 项目根(声明查找目录)
|
|
371
|
+
* @returns {{managed:true, specRoot, workspaceId, declaredAt}|null}
|
|
372
|
+
* 文件不存在 / JSON 损坏 / managed 非 true → null
|
|
373
|
+
*/
|
|
374
|
+
export function checkPlatformManaged(cwd) {
|
|
375
|
+
const p = join(resolve(cwd), PLATFORM_MANAGED_FILENAME)
|
|
376
|
+
if (!existsSync(p)) return null
|
|
377
|
+
try {
|
|
378
|
+
const decl = JSON.parse(readFileSync(p, 'utf8'))
|
|
379
|
+
if (!decl || decl.managed !== true) return null
|
|
380
|
+
return {
|
|
381
|
+
managed: true,
|
|
382
|
+
specRoot: decl.specRoot || null,
|
|
383
|
+
workspaceId: decl.workspaceId || null,
|
|
384
|
+
declaredAt: decl.declaredAt || null,
|
|
385
|
+
}
|
|
386
|
+
} catch {
|
|
387
|
+
return null
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
304
391
|
/**
|
|
305
392
|
* 统一查找变更目录(与 progress.js 的变更检测逻辑一致)。
|
|
306
393
|
*/
|
|
@@ -494,8 +581,90 @@ export function isQuickMetadata(p, linkedChanges = []) {
|
|
|
494
581
|
* quick 完成审计:对比 baseline 与实际变更。
|
|
495
582
|
* @returns {{ status: 'safe'|'warning'|'blocked', reasons: string[], changedFiles: string[], newFiles: string[], deletedFiles: string[], baselineHit: string[], stagedTotal: number }}
|
|
496
583
|
*/
|
|
584
|
+
/**
|
|
585
|
+
* D-8 O-1 模块归属(2026-08-15 docs-signals-o12):quick 欠账 hint 从"改了 N 文件"升级为
|
|
586
|
+
* "涉及哪些模块卡"。只做归属(matchFilesToModules 纯函数零 git),不算 behind——对账是
|
|
587
|
+
* execute Wave [docs-debt] 的职责(design D-001 轻量边界)。map 缺失/解析空/异常 → 空数组
|
|
588
|
+
* 降级(hint 仍打,modules 省略),不报错不阻断。
|
|
589
|
+
* 动态 import docs-debt.js/modules.js:prompt.js 已静态 import shared(本模块),静态互引成 ESM 环。
|
|
590
|
+
*/
|
|
591
|
+
async function matchQuickModules(srcChanged, specBase, projectName) {
|
|
592
|
+
if (!specBase || !projectName || srcChanged.length === 0) return []
|
|
593
|
+
try {
|
|
594
|
+
const { join } = await import('node:path')
|
|
595
|
+
const { existsSync: ex, readFileSync: rf } = await import('node:fs')
|
|
596
|
+
const mapPath = join(specBase, 'docs', projectName, 'modules', '_module-map.yaml')
|
|
597
|
+
if (!ex(mapPath)) return []
|
|
598
|
+
const { parseModuleMapSimple } = await import('../modules.js')
|
|
599
|
+
const idx = parseModuleMapSimple(rf(mapPath, 'utf8'))
|
|
600
|
+
if (!idx || Object.keys(idx).length === 0) return []
|
|
601
|
+
const { matchFilesToModules } = await import('../docs-debt.js')
|
|
602
|
+
const cardsDir = join(specBase, 'docs', projectName, 'modules')
|
|
603
|
+
const { byModule } = matchFilesToModules(srcChanged, idx, { cardsDir })
|
|
604
|
+
return [...byModule.entries()].map(([id, e]) => ({ id, doc: e.doc }))
|
|
605
|
+
} catch {
|
|
606
|
+
return []
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// ── task-03(2026-08-16-state-split-fixes): 活文档漂移提示辅助 ──
|
|
611
|
+
// 背景:platform-interface-map.md 等活文档持续维护 src 主入口的 file:line 映射;并行会话改
|
|
612
|
+
// src/ 后引用行号静默失效,docs gate 只能事后拦到别人的流程上。审计(auditQuickCompletion)
|
|
613
|
+
// 当场检测「本次改动的 src 文件是否被活文档引用」并提示(advisory 不阻断,docs gate 语义不变)。
|
|
614
|
+
|
|
615
|
+
/** 缺省活文档:src 主入口 ↔ 文档接口映射(file:line 锚点密度最高的持续维护文档)。 */
|
|
616
|
+
export const DEFAULT_LIVING_DOC = 'docs/sillyspec/platform-interface-map.md'
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* 解析活文档集合:local.yaml `docs-check.living-docs` 列表【只追加不覆盖】缺省集合。
|
|
620
|
+
* 与 docs-check.paths 的覆盖语义刻意不同——paths 是扫描范围(配了即整体替换缺省),
|
|
621
|
+
* living-docs 是监控点登记(配了是加哨兵,不该把缺省监控点挤掉)。读配置走本模块现成的
|
|
622
|
+
* readLocalYamlRaw + js-yaml 动态 import(readDocsCheckConfig 不回 living-docs 键;动态
|
|
623
|
+
* import 与上方 docs-check 的隔离约定一致);坏 YAML / 非数组 / 读失败 → 仅缺省。
|
|
624
|
+
* @param {string} cwd 仓库根(local.yaml 在 <cwd>/.sillyspec/local.yaml)
|
|
625
|
+
* @returns {Promise<string[]>} 去重后的活文档相对路径(仓库根相对 POSIX)
|
|
626
|
+
*/
|
|
627
|
+
export async function resolveLivingDocs(cwd) {
|
|
628
|
+
let configured = []
|
|
629
|
+
const raw = readLocalYamlRaw(cwd)
|
|
630
|
+
if (raw) {
|
|
631
|
+
try {
|
|
632
|
+
const mod = await import('js-yaml')
|
|
633
|
+
const yamlLoad = mod.load || mod.default?.load
|
|
634
|
+
const doc = yamlLoad(raw)
|
|
635
|
+
const dc = doc && typeof doc === 'object' ? doc['docs-check'] : null
|
|
636
|
+
const ld = dc && typeof dc === 'object' ? dc['living-docs'] : null
|
|
637
|
+
if (Array.isArray(ld)) configured = ld.filter((s) => typeof s === 'string' && s.trim() !== '')
|
|
638
|
+
} catch { /* 坏 YAML → 忽略配置,仅缺省 */ }
|
|
639
|
+
}
|
|
640
|
+
return [...new Set([DEFAULT_LIVING_DOC, ...configured])]
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* 纯函数:活文档引用的源码文件与本次改动的 src/ 文件求交。
|
|
645
|
+
* 匹配口径对齐 docs-check resolveCandidates 的三形态(纯路径比对,不查盘):
|
|
646
|
+
* ①仓库根相对:changed === ref(如 src/a.js)
|
|
647
|
+
* ②src 内部相对:changed === 'src/' + ref(如 run/b.js → src/run/b.js)
|
|
648
|
+
* ③裸名/中缀后缀:changed.endsWith('/' + ref)(如 b.js;前导斜杠防 aa.js 被引用 a.js 误吃)
|
|
649
|
+
* changedFiles 口径 = git status porcelain 归一后的仓库根相对 POSIX 路径(parsePorcelainPath 产物)。
|
|
650
|
+
* 同名多文件时后缀形态可能多报——与 docs-check 多候选宽容同向,advisory 仅提示可接受。
|
|
651
|
+
* @param {string[]} srcChangedFiles changedFiles 中 src/ 前缀的文件
|
|
652
|
+
* @param {Array<{file: string}>} refs collectDocRefs(md) 产物(只用 .file)
|
|
653
|
+
* @returns {string[]} 命中的 srcChangedFiles 子集(去重保序)
|
|
654
|
+
*/
|
|
655
|
+
export function matchLivingDocRefs(srcChangedFiles, refs) {
|
|
656
|
+
const hit = new Set()
|
|
657
|
+
for (const c of srcChangedFiles) {
|
|
658
|
+
if (refs.some((r) => r && typeof r.file === 'string' &&
|
|
659
|
+
(c === r.file || c === 'src/' + r.file || c.endsWith('/' + r.file)))) {
|
|
660
|
+
hit.add(c)
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
return [...hit]
|
|
664
|
+
}
|
|
665
|
+
|
|
497
666
|
export async function auditQuickCompletion(cwd, guard, options = {}) {
|
|
498
|
-
const { baselineFiles, allowedFiles = [], allowNew = false, forceBaseline = false, allowDelete = false } = guard
|
|
667
|
+
const { baselineFiles, allowedFiles = [], allowNew = false, forceBaseline = false, allowDelete = false, specBase = null, projectName = null } = guard
|
|
499
668
|
const { isConfirm } = options
|
|
500
669
|
// stagedTotal:当前所有非 quick 元数据的未提交条目(含前序 baseline 残留)。
|
|
501
670
|
// 与 changedFiles(扣 baseline 后的本轮新增)区分,供审计文案同时展示「本轮新增 vs 累计暂存」,
|
|
@@ -644,6 +813,70 @@ export async function auditQuickCompletion(cwd, guard, options = {}) {
|
|
|
644
813
|
}
|
|
645
814
|
} catch {}
|
|
646
815
|
|
|
816
|
+
// D-8 文档欠账显性化(advisory):本次改动含源码文件但不含任何文档文件 → 打标记 warn。
|
|
817
|
+
// quick 是低摩擦通道(不强制文档同步),但欠账要显性化——72% 流量 86% 不动文档是系统性欠账
|
|
818
|
+
// 主通道(doc-consistency-debt D-8)。不阻断不改 status,只 warn + 记 reasons 供 QUICKLOG 追溯。
|
|
819
|
+
const isDocFile = (f) =>
|
|
820
|
+
f.endsWith('.md') || f.endsWith('.yaml') || f.endsWith('.yml') ||
|
|
821
|
+
f.startsWith('docs/') || f.startsWith('.sillyspec/docs/')
|
|
822
|
+
const srcChanged = result.changedFiles.filter(f => !isDocFile(f) && !isQuickMetadata(f, guard.linkedChanges))
|
|
823
|
+
const docChanged = result.changedFiles.filter(isDocFile)
|
|
824
|
+
if (srcChanged.length > 0 && docChanged.length === 0) {
|
|
825
|
+
result.reasons.push(`本次未同步模块文档(${srcChanged.length} 个源码文件改动,无文档文件)`)
|
|
826
|
+
result.docSyncHint = { touchedSource: srcChanged.length, docFiles: [], modules: await matchQuickModules(srcChanged, specBase, projectName) }
|
|
827
|
+
} else if (srcChanged.length > 0 && docChanged.length > 0) {
|
|
828
|
+
result.docSyncHint = { touchedSource: srcChanged.length, docFiles: docChanged }
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// docs check advisory(doc-consistency-debt D-6 后续):本次改动的 .md 文档跑 file:line
|
|
832
|
+
// 引用校验,失效即 warn + reasons——检测能力落地后欠账只涨不跌的最后一公里。只查本次
|
|
833
|
+
// changedFiles 的文档(归因到本次改动),不扫全仓历史欠账(那是存量问题,quick 不背锅)。
|
|
834
|
+
// 排除删除文件(troubleshooting #9):删除的 .md 不在盘,runDocsCheck 只会报「文档不存在」
|
|
835
|
+
// 假失效——删除语义归 --allow-delete 管,无引用校验可言(并行会话的删除尤其不该归属本会话)。
|
|
836
|
+
// docs-check 是 IO 面(walkGlob/existsSync),动态 import 隔离;任何异常 fail-open 只跳过。
|
|
837
|
+
const deletedSet = new Set(result.deletedFiles)
|
|
838
|
+
const mdChanged = result.changedFiles.filter((f) => f.endsWith('.md') && !deletedSet.has(f) && !isQuickMetadata(f, guard.linkedChanges))
|
|
839
|
+
if (mdChanged.length > 0) {
|
|
840
|
+
try {
|
|
841
|
+
const { runDocsCheck } = await import('../docs-check.js')
|
|
842
|
+
const dc = runDocsCheck({ projectRoot: cwd, docs: mdChanged })
|
|
843
|
+
if (!dc.ok) {
|
|
844
|
+
result.reasons.push(`本次改动文档含 ${dc.invalid.length} 处失效 file:line 引用(sillyspec docs check 可复现)`)
|
|
845
|
+
result.docsCheckHint = { invalid: dc.invalid.length, total: dc.total }
|
|
846
|
+
if (result.status === 'safe') result.status = 'warning'
|
|
847
|
+
}
|
|
848
|
+
} catch { /* docs-check 不可用(老版本包等)→ 静默跳过,不阻断 quick */ }
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// task-03: 活文档漂移提示(advisory,不阻断不改 status,docs gate 语义不变)。
|
|
852
|
+
// 方向与上方 docsCheckHint 相反:那是「本次改的文档引用失效吗」,这是「本次改的 src 被活文档
|
|
853
|
+
// 引用吗」——并行会话改 src/ 主入口后 platform-interface-map.md 的 file:line 行号静默失效,
|
|
854
|
+
// gate 事后拦到别人流程上;审计当场命中交集即提示顺手跑 docs check。fail-open:活文档
|
|
855
|
+
// 缺失/读失败/collectDocRefs 不可用 → 静默跳过(不误报不阻断)。
|
|
856
|
+
const srcChangedFiles = result.changedFiles.filter((f) => f.startsWith('src/') && !deletedSet.has(f))
|
|
857
|
+
if (srcChangedFiles.length > 0) {
|
|
858
|
+
try {
|
|
859
|
+
const { collectDocRefs } = await import('../docs-check.js')
|
|
860
|
+
const livingDocs = await resolveLivingDocs(cwd)
|
|
861
|
+
const files = []
|
|
862
|
+
const docs = []
|
|
863
|
+
for (const docRel of livingDocs) {
|
|
864
|
+
let md
|
|
865
|
+
try { md = readFileSync(join(cwd, docRel), 'utf8') } catch { continue } // 活文档缺失/读失败 → 跳过
|
|
866
|
+
const hit = matchLivingDocRefs(srcChangedFiles, collectDocRefs(md))
|
|
867
|
+
if (hit.length > 0) {
|
|
868
|
+
files.push(...hit)
|
|
869
|
+
docs.push(docRel)
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
if (files.length > 0) {
|
|
873
|
+
const uniqFiles = [...new Set(files)]
|
|
874
|
+
result.docsCheckHint = { ...(result.docsCheckHint || {}), livingDocDrift: { files: uniqFiles, docs, total: srcChangedFiles.length } }
|
|
875
|
+
result.reasons.push(`活文档引用漂移: 改动 ${uniqFiles.length} 个文件被活文档引用(${docs.join(', ')})`)
|
|
876
|
+
}
|
|
877
|
+
} catch { /* collectDocRefs 不可用 → 静默跳过 */ }
|
|
878
|
+
}
|
|
879
|
+
|
|
647
880
|
// task-02: 同文件并发检测——allowedFile 在 baseline(他者改过)且当前 hash ≠ step1 hash(我也改了)
|
|
648
881
|
// → commit 整文件 pathspec 会夹带他者 hunk,warn 给分离指引(advisory,不阻断,D-002)
|
|
649
882
|
if (allowedFiles.length > 0 && guard.allowedFilesHash) {
|
|
@@ -784,6 +1017,35 @@ export function aggregateDeclaredRepos(planContent) {
|
|
|
784
1017
|
return Array.from(seen)
|
|
785
1018
|
}
|
|
786
1019
|
|
|
1020
|
+
/**
|
|
1021
|
+
* 坑7 补扫:读 tasks/task-NN.md 独立卡片,拼成可被 aggregateDeclaredRepos 的 frontmatter
|
|
1022
|
+
* 正则识别的文本(每张卡内容原样拼接,卡内容自带的 ---\n…\n--- frontmatter 会被同一正则命中)。
|
|
1023
|
+
*
|
|
1024
|
+
* plan 阶段标准产出形态是 plan.md 内联卡片块 + tasks/ 独立卡片双写,但实际执行中 plan.md
|
|
1025
|
+
* 可能只留 checkbox 行(卡片全在 tasks/)——此时 plan.md 聚合不到跨仓 repo,跨仓 review
|
|
1026
|
+
* 误报伪造。本函数读 tasks/ 目录兜底,任一文件读失败跳过(fail-open,兜底源不引入新阻断)。
|
|
1027
|
+
*
|
|
1028
|
+
* @param {string} specDir - spec 根(change 目录的父级)
|
|
1029
|
+
* @param {string} changeName
|
|
1030
|
+
* @returns {string} 拼接文本(无 tasks/ 或全空 → '',plan.md 原行为零回归)
|
|
1031
|
+
*/
|
|
1032
|
+
function collectTaskCardReposFallback(specDir, changeName) {
|
|
1033
|
+
const tasksDir = join(specDir, 'changes', changeName, 'tasks')
|
|
1034
|
+
if (!existsSync(tasksDir)) return ''
|
|
1035
|
+
let merged = ''
|
|
1036
|
+
for (const f of readdirSync(tasksDir)) {
|
|
1037
|
+
if (!/^task-\d+\.md$/i.test(f)) continue
|
|
1038
|
+
const p = join(tasksDir, f)
|
|
1039
|
+
// CRLF 卡片:\r?\n 容差在 aggregateDeclaredRepos 正则里,这里只管拼接不归一
|
|
1040
|
+
try {
|
|
1041
|
+
merged += '\n' + readFileSync(p, 'utf8') + '\n'
|
|
1042
|
+
} catch {
|
|
1043
|
+
// 单文件读失败跳过——兜底源缺失不该阻断主聚合
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
return merged
|
|
1047
|
+
}
|
|
1048
|
+
|
|
787
1049
|
/**
|
|
788
1050
|
* 构造或取进程级 MultiRepoContext 单例(D-013 G2)。
|
|
789
1051
|
*
|
|
@@ -835,7 +1097,13 @@ export async function getOrCreateMultiRepoContext({ cwd, changeName, platformOpt
|
|
|
835
1097
|
}
|
|
836
1098
|
|
|
837
1099
|
// 聚合 declaredRepos(扫 plan.md task 卡片 repo:)+ 读 local.yaml repos: 段
|
|
838
|
-
|
|
1100
|
+
// 坑7 兼扫 tasks/task-NN.md:plan 阶段把卡片写到 tasks/、plan.md 只留 checkbox 行时,
|
|
1101
|
+
// plan.md 内联 frontmatter 聚合不到跨仓 repo → ctx 不含跨仓 entry → 跨仓 review 退回主仓
|
|
1102
|
+
// gitDir 校验,误报「base 不是真实 commit — 疑似伪造」(真实 commit 在跨仓仓可达)。
|
|
1103
|
+
// 两源并入同一聚合:plan.md 内联块(原行为,零回归)+ tasks/ 独立卡片(坑7 补扫)。
|
|
1104
|
+
const declaredRepos = aggregateDeclaredRepos(
|
|
1105
|
+
planContent + collectTaskCardReposFallback(specDir, changeName)
|
|
1106
|
+
)
|
|
839
1107
|
const repoRegistry = parseRepoRegistry(readLocalYamlRaw(cwd))
|
|
840
1108
|
|
|
841
1109
|
// 构造 WorktreeManager(主仓 meta 读取;与 getStageSteps 同源范式)
|
package/src/run/stage.js
CHANGED
|
@@ -25,6 +25,7 @@ import { outputStep } from './prompt.js'
|
|
|
25
25
|
import { allocateQuicklogEntry, deriveTitleFromLinkedChange, sanitizeDesc } from '../quicklog.js'
|
|
26
26
|
import { createHash } from 'node:crypto'
|
|
27
27
|
import { checkTransition } from '../stage-contract.js'
|
|
28
|
+
import { AUXILIARY_STAGES } from '../constants.js'
|
|
28
29
|
import { completeStageGates } from './gates.js'
|
|
29
30
|
|
|
30
31
|
export async function runStage(pm, progress, stageName, cwd, changeName, skipApproval = false, platformOpts = {}, quickOpts = {}) {
|
|
@@ -107,6 +108,15 @@ export async function runStage(pm, progress, stageName, cwd, changeName, skipApp
|
|
|
107
108
|
} catch {}
|
|
108
109
|
if (!currentExecuteRunId) {
|
|
109
110
|
currentExecuteRunId = generateExecuteRunId()
|
|
111
|
+
// D-001#1 主写入点:mkdir execute-runs/<runId>/tasks 先于 marker(不变量:marker 在则目录在,
|
|
112
|
+
// archive 完成度扫描/漂移兜底不再落到「有 marker 无目录」的空 run)。失败直接 throw——execute
|
|
113
|
+
// 启动即失败优于事后 review 错配(调用方 runCommand 冒到 CLI 顶层 exit 1,给出修复指引)。
|
|
114
|
+
try {
|
|
115
|
+
mkdirSync(join(runtimeRoot, 'execute-runs', currentExecuteRunId, 'tasks'), { recursive: true })
|
|
116
|
+
} catch (e) {
|
|
117
|
+
throw new Error(`execute run 目录创建失败(${join(runtimeRoot, 'execute-runs', currentExecuteRunId)}): ${e.message};` +
|
|
118
|
+
`请检查该路径是否为普通文件/只读,清理后重跑(sillyspec run execute --change ${changeName} --skip-approval)`)
|
|
119
|
+
}
|
|
110
120
|
writeFileSync(runIdFile, currentExecuteRunId + '\n')
|
|
111
121
|
}
|
|
112
122
|
}
|
|
@@ -125,8 +135,12 @@ export async function runStage(pm, progress, stageName, cwd, changeName, skipApp
|
|
|
125
135
|
}
|
|
126
136
|
|
|
127
137
|
// 用户显式调用 sillyspec run <stage>:把它标记为当前阶段
|
|
138
|
+
// (D-003@v1:auxiliary 阶段不写 currentStage,避免 scan/quick/explore/archive/status/doctor
|
|
139
|
+
// 执行后污染主流程当前阶段;lastActive 心跳与 pm._write/triggerSync 照常)
|
|
128
140
|
if (progress.currentStage !== stageName) {
|
|
129
|
-
|
|
141
|
+
if (!AUXILIARY_STAGES.includes(stageName)) {
|
|
142
|
+
progress.currentStage = stageName
|
|
143
|
+
}
|
|
130
144
|
progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
131
145
|
pm._write(cwd, progress, changeName)
|
|
132
146
|
triggerSync(cwd, changeName, platformOpts)
|
|
@@ -375,6 +389,8 @@ export async function runStage(pm, progress, stageName, cwd, changeName, skipApp
|
|
|
375
389
|
// 阶段完成收尾共享管线(noAI 末步核心修复 S1:plan postcheck independent-tier review verdict=fail /
|
|
376
390
|
// 平台 scan manifest 此前被绕过)。gate 失败已 rollback 为 in-progress,early-return(不 fall through 到末尾 return)。
|
|
377
391
|
const _stageGatesResult = await completeStageGates({ stageName, cwd, changeName, platformOpts, specBase, progress, pm, stageData, steps, currentIdx, outputText: null })
|
|
392
|
+
// task-04 / A5:gate 失败(stageCompleted===false)设进程退出码 1(与 completeStep/continueStep 同语义)。
|
|
393
|
+
if (_stageGatesResult?.stageCompleted === false) process.exitCode = 1
|
|
378
394
|
if (_stageGatesResult) return _stageGatesResult
|
|
379
395
|
// gate 全过:persist completed(task-02 移后;此处无 triggerSync,与同文件 noAI 末步语义一致)。
|
|
380
396
|
pm._write(cwd, progress, changeName)
|