sillyspec 3.27.9 → 3.27.12
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 +214 -214
- package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
- package/.claude/skills/sillyspec-quick/SKILL.md +116 -116
- package/README.md +3 -2
- package/package.json +1 -1
- package/src/change-delete.js +181 -0
- package/src/commit-suggest.js +22 -0
- package/src/contract-matrix.js +20 -4
- package/src/decision-distill.js +33 -14
- package/src/docs-check.js +138 -2
- package/src/endpoint-extractor.js +7 -4
- package/src/facade-hint.js +170 -0
- package/src/index.js +259 -22
- package/src/init.js +104 -27
- package/src/module-changelog.js +116 -0
- package/src/progress/change-registry.js +28 -0
- package/src/progress/shared.js +6 -0
- package/src/progress/stage-machine.js +85 -8
- package/src/progress.js +3 -0
- package/src/run/command.js +60 -0
- package/src/run/complete-handlers.js +25 -1
- package/src/run/gates.js +62 -5
- package/src/run/prompt.js +12 -1
- package/src/run/shared.js +81 -23
- package/src/run/stage.js +21 -1
- package/src/spec-sync.js +88 -1
- package/src/stages/archive.js +5 -0
- package/src/stages/execute.js +3 -1
- package/src/sync.js +453 -17
- package/src/verify-postcheck.js +47 -6
- package/src/workflow.js +4 -0
- package/src/worktree-apply.js +62 -4
- package/src/worktree.js +6 -4
- package/templates/{claude-instruction.md → agents-instruction.md} +2 -2
- package/templates/prompts/verify-probes.md +2 -1
package/src/run/gates.js
CHANGED
|
@@ -363,6 +363,23 @@ export async function enforceDepsGate(stageName, cwd, changeName, step, steps, c
|
|
|
363
363
|
try {
|
|
364
364
|
worktreeGone = !!(wm && changeName) && !existsSync(wm.getWorktreePath(changeName))
|
|
365
365
|
} catch {}
|
|
366
|
+
// ── 正当收尾放行(坑 deps-gate-cleanup-order,2026-08-28 实证:apply 后 / execute 空变更
|
|
367
|
+
// cleanup 后重试 --done,被本门拦「worktree 不可用」逼 doctor --align-execute-progress 手工
|
|
368
|
+
// 对齐——顺序矛盾:cleanup 是流程自己做的收尾,门却当意外丢失拦)。两凭据二选一:
|
|
369
|
+
// - apply-pathspec-<change>.txt:applyWorktree 成功后落盘(apply 后自动 cleanup 删 meta);
|
|
370
|
+
// - execute-cleanup-<change>.json:handleExecuteWorktreeCleanup 正当清理回执。
|
|
371
|
+
// 有凭据 = 隔离期已正当结束(交付已落主仓或本无交付),deps 能力要求不再适用 → 放行。
|
|
372
|
+
if (worktreeGone && changeName) {
|
|
373
|
+
try {
|
|
374
|
+
const runtimeRoot = resolveRuntimeRoot(platformOpts, specBase)
|
|
375
|
+
const legitimatelyEnded = existsSync(join(runtimeRoot, `apply-pathspec-${changeName}.txt`))
|
|
376
|
+
|| existsSync(join(runtimeRoot, `execute-cleanup-${changeName}.json`))
|
|
377
|
+
if (legitimatelyEnded) {
|
|
378
|
+
console.log('ℹ️ worktree 已在 apply/execute 收尾时正当清理(凭据在 .runtime),deps 门放行')
|
|
379
|
+
return true
|
|
380
|
+
}
|
|
381
|
+
} catch { /* 凭据读取失败 → 维持原判定 */ }
|
|
382
|
+
}
|
|
366
383
|
// ── fail-loud 块(Phase 3,D-005@v1:仅改拒绝侧 stderr)──
|
|
367
384
|
console.error('❌ ── deps 门控阻断(本次 --done 未完成,进度未推进)──')
|
|
368
385
|
if (worktreeGone) {
|
|
@@ -559,6 +576,19 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
559
576
|
}
|
|
560
577
|
}
|
|
561
578
|
|
|
579
|
+
// plan 门 facade 候选预检(坑 plan-facade-files-manual-backfill,2026-08-28 用户实证 ×2:
|
|
580
|
+
// daemon/service.py 类「透传必经文件」不在 allowed_paths → 执行期 apply Gate1 拦截 →
|
|
581
|
+
// 手工回补两轮)。allowed 集与 Gate1 同源(resolveApplyAllowSet 主仓切片),
|
|
582
|
+
// 静态引用/聚合扫描亮候选,advisory 不阻断。
|
|
583
|
+
if (stageName === 'plan' && changeName) {
|
|
584
|
+
try {
|
|
585
|
+
const { resolveApplyAllowSet } = await import('../worktree-apply.js')
|
|
586
|
+
const { warnFacadeCandidateFiles } = await import('../facade-hint.js')
|
|
587
|
+
const allowMap = resolveApplyAllowSet(cwd, changeName)
|
|
588
|
+
warnFacadeCandidateFiles({ cwd, changeName, allowSet: allowMap.get('main') })
|
|
589
|
+
} catch { /* 预检失败不阻断 plan 完成 */ }
|
|
590
|
+
}
|
|
591
|
+
|
|
562
592
|
// verify 产物校验通过 + 结论非 FAIL(否则上面已阻断)。
|
|
563
593
|
// 再由 CLI 亲自执行 local.yaml 的测试命令,与 verify-result.md 的自报告对账:
|
|
564
594
|
// 自报告 PASS 但实测失败 → 阻断(防止"文案通过"绕过验证)。
|
|
@@ -576,16 +606,23 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
576
606
|
const { collectForeignDeclaredFiles } = await import('../verify-postcheck.js')
|
|
577
607
|
const { gitQuiet } = await import('../git-helper.js')
|
|
578
608
|
const foreignMap = collectForeignDeclaredFiles(cwd, changeName, { specBase, runtimeRoot: resolveRuntimeRoot(platformOpts, specBase) })
|
|
609
|
+
// dirty 集提前算好:归因提示①(他者在途)与②(生成产物旧基线)共用
|
|
610
|
+
const dirty = (gitQuiet(cwd, ['diff', '--name-only', 'HEAD']) || '')
|
|
611
|
+
.split('\n').filter(Boolean).map(f => f.replace(/\\/g, '/'))
|
|
612
|
+
const dirtySet = new Set(dirty)
|
|
579
613
|
if (foreignMap.size > 0) {
|
|
580
|
-
const dirty = (gitQuiet(cwd, ['diff', '--name-only', 'HEAD']) || '')
|
|
581
|
-
.split('\n').filter(Boolean).map(f => f.replace(/\\/g, '/'))
|
|
582
614
|
const hitForeign = dirty.filter(f => foreignMap.has(f))
|
|
583
615
|
if (hitForeign.length > 0) {
|
|
584
616
|
console.error(` ℹ️ 归因提示:主仓检出 ${hitForeign.length} 个并行会话声明的在途文件(${hitForeign.slice(0, 5).join(', ')}${hitForeign.length > 5 ? ' 等' : ''})——实测失败可能混入他者 WIP 而非本变更问题。待其提交/收尾后复验,仍失败才是本变更的。`)
|
|
585
617
|
}
|
|
586
618
|
}
|
|
587
619
|
// 第二判据(坑 derived-artifact-stale-baseline):本变更 apply 的文件(apply-pathspec 落盘)
|
|
588
|
-
//
|
|
620
|
+
// 与主仓近期提交重叠——本变更旧基线的生成产物可能覆盖了已合入内容,先在新基线重跑生成命令再复验。
|
|
621
|
+
// 坑 verify-attr2-foreign-wip-misdirection(2026-08-28 实证):并行会话在途/近期收尾文件
|
|
622
|
+
// 让重叠集虚高(51 文件),每轮误导 agent 重跑 gen:types,实际失败源是他者 WIP。两道过滤:
|
|
623
|
+
// - 必须 dirty:apply 落盘的旧内容此刻物理在场(verify gate 时本变更未提交,旧基线覆盖
|
|
624
|
+
// 必然表现为对 HEAD 的修改)——内容与 HEAD 一致时重跑生成命令改变不了本轮实测;
|
|
625
|
+
// - 剔除他者声明集:那是归因提示①的领地(待其收尾后复验),不是生成产物过期。
|
|
589
626
|
try {
|
|
590
627
|
const pathspecFile = join(resolveRuntimeRoot(platformOpts, specBase), `apply-pathspec-${changeName}.txt`)
|
|
591
628
|
if (changeName && existsSync(pathspecFile)) {
|
|
@@ -594,8 +631,9 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
594
631
|
const recent = (gitQuiet(cwd, ['log', '-n', '10', '--name-only', '--pretty=format:']) || '')
|
|
595
632
|
.split('\n').map(l => l.trim().replace(/\\/g, '/')).filter(Boolean)
|
|
596
633
|
const overlap = [...new Set(recent)].filter(f => ownFiles.has(f))
|
|
597
|
-
|
|
598
|
-
|
|
634
|
+
const actionable = filterStaleBaselineOverlap(overlap, dirtySet, foreignMap)
|
|
635
|
+
if (actionable.length > 0) {
|
|
636
|
+
console.error(` ℹ️ 归因提示②:本变更有 ${actionable.length} 个文件与主仓最近 10 条提交重叠且旧内容仍在工作区(${actionable.slice(0, 5).join(', ')}${actionable.length > 5 ? ' 等' : ''})——若为生成产物(api-types 等),本变更可能在旧基线生成并覆盖了已合入内容;先在新基线重跑生成命令(如 gen:types)再复验。(重叠共 ${overlap.length} 个,其余未在工作区改动或属他者会话在途文件,已归提示①/无需处理)`)
|
|
599
637
|
}
|
|
600
638
|
}
|
|
601
639
|
}
|
|
@@ -1112,6 +1150,25 @@ export async function completeStageGates({ stageName, cwd, changeName, platformO
|
|
|
1112
1150
|
}
|
|
1113
1151
|
|
|
1114
1152
|
|
|
1153
|
+
/**
|
|
1154
|
+
* 归因提示②(生成产物旧基线覆盖)的重叠集过滤(坑 verify-attr2-foreign-wip-misdirection)。
|
|
1155
|
+
*
|
|
1156
|
+
* 原始重叠 = 本变更 apply-pathspec ∩ 主仓最近提交。并行会话场景下该集合被在途/刚收尾的
|
|
1157
|
+
* 他者文件虚高,逐轮误导 agent 重跑生成命令。只保留两个必要条件都成立的文件:
|
|
1158
|
+
* - dirty(此刻对 HEAD 有改动):verify gate 时本变更未提交,旧基线覆盖必然物理在场;
|
|
1159
|
+
* 内容与 HEAD 一致时重跑生成命令改变不了本轮实测。
|
|
1160
|
+
* - 非他者声明集(collectForeignDeclaredFiles):他者在途文件归归因提示①(待其收尾
|
|
1161
|
+
* 复验),不是生成产物过期。
|
|
1162
|
+
*
|
|
1163
|
+
* @param {string[]} overlap 原始重叠文件(POSIX 路径)
|
|
1164
|
+
* @param {Set<string>} dirtySet 主仓当前对 HEAD 的改动集
|
|
1165
|
+
* @param {Set<string>|Map} foreignDeclared 他者会话声明集(Map 亦可用 has 判定)
|
|
1166
|
+
* @returns {string[]} 可行动子集(空 = 不出提示②,避免误导)
|
|
1167
|
+
*/
|
|
1168
|
+
export function filterStaleBaselineOverlap(overlap, dirtySet, foreignDeclared) {
|
|
1169
|
+
return (overlap || []).filter(f => dirtySet?.has(f) && !(foreignDeclared?.has(f)))
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1115
1172
|
/**
|
|
1116
1173
|
* verify 服务进程回收(verify --done 收尾调用)。
|
|
1117
1174
|
*
|
package/src/run/prompt.js
CHANGED
|
@@ -26,6 +26,7 @@ import { resolvePromptIncludes, resolveRuntimeRoot, safeGit, WAIT_MARKER_RE, QUI
|
|
|
26
26
|
import { renderStageContract } from '../stage-contract-spec.js'
|
|
27
27
|
import { nowWallClock } from '../datetime.js'
|
|
28
28
|
import { parseModuleMapSimple } from '../modules.js'
|
|
29
|
+
import { readModuleRecentChanges } from '../module-changelog.js'
|
|
29
30
|
import { REVIEW_SCHEMA_VERSION, isValidExecuteRunId } from '../task-review.js'
|
|
30
31
|
|
|
31
32
|
/**
|
|
@@ -111,6 +112,12 @@ function buildModuleContextInjection(taskDescription, moduleIndex, specBase, pro
|
|
|
111
112
|
if (deps.length > 0) injection += `- **依赖**: ${deps.join(', ')}\n`
|
|
112
113
|
const usedBy = data.used_by || []
|
|
113
114
|
if (usedBy.length > 0) injection += `- **被引用**: ${usedBy.join(', ')}\n`
|
|
115
|
+
// 最近变更(模块卡「变更索引」惯例解析,2026-08-31 变更关联审计):三阶段免扫描即知
|
|
116
|
+
// 该模块最近被谁动过。懒读命中模块的卡 + sidecar(matched 通常 ≤3 个,不做全目录扫描)
|
|
117
|
+
const recentChanges = readModuleRecentChanges(join(specBase, 'docs', projectName, 'modules'), moduleId)
|
|
118
|
+
if (recentChanges.length > 0) {
|
|
119
|
+
injection += `- **最近变更**: ${recentChanges.slice(0, 3).map(e => e.name).join('、')}\n`
|
|
120
|
+
}
|
|
114
121
|
injection += '\n'
|
|
115
122
|
}
|
|
116
123
|
|
|
@@ -868,6 +875,10 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
868
875
|
const mayNeedWait = WAIT_MARKER_RE.test(stepPrompt) || requiresWait || conditionalWait
|
|
869
876
|
|
|
870
877
|
console.log(`\n### 完成后执行`)
|
|
878
|
+
// requiresConfirm 步骤(如 archive「确认归档」)的完成命令必须带 --confirm——坑
|
|
879
|
+
// archive-batch-31-tool-notes ②:通用 --done 模板不带该 flag,agent 照抄执行撞
|
|
880
|
+
// 「请添加 --confirm」确认门,误以为参数没带。
|
|
881
|
+
const confirmFlag = step.requiresConfirm === true ? ' --confirm' : ''
|
|
871
882
|
if (requiresWait) {
|
|
872
883
|
console.log(`本步骤必须等待用户输入,不能直接 --done:`)
|
|
873
884
|
console.log(`sillyspec run ${stageName} --wait --reason "${step.waitReason || '等待用户输入'}" --options "${(step.waitOptions || ['确认']).join(',')}"${changeFlag} --output "你的问题/方案摘要"`)
|
|
@@ -882,7 +893,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
882
893
|
console.log(``)
|
|
883
894
|
console.log(`如果不需要用户决策,正常完成:`)
|
|
884
895
|
}
|
|
885
|
-
console.log(`sillyspec run ${stageName} --done${changeFlag} --input "用户原始需求/反馈" --output "你的摘要"`)
|
|
896
|
+
console.log(`sillyspec run ${stageName} --done${confirmFlag}${changeFlag} --input "用户原始需求/反馈" --output "你的摘要"`)
|
|
886
897
|
}
|
|
887
898
|
/**
|
|
888
899
|
* 替换 prompt 文本中的路径根占位符 {SPEC_ROOT}/{DOCS_ROOT}/{PROJECTS_ROOT}/
|
package/src/run/shared.js
CHANGED
|
@@ -616,6 +616,38 @@ export async function triggerSync(cwd, changeName, platformOpts = {}, opts = {})
|
|
|
616
616
|
}
|
|
617
617
|
}
|
|
618
618
|
|
|
619
|
+
/**
|
|
620
|
+
* 触发步骤开始上报(X3 / design §8.2,变更 2026-08-29-change-delete-closure-and-spec-pull task-13)。
|
|
621
|
+
*
|
|
622
|
+
* 步骤启动时同端点同结构补推一次 progress:CLI 状态机的步骤行 DB 侧种子是 pending,
|
|
623
|
+
* 本推送在载荷层把 current_stage 的第一个未完成步投影为 in-progress(sync.js
|
|
624
|
+
* _projectStepStart,不写 DB,--done 推送路径不受影响)。真实效果 = 平台
|
|
625
|
+
* last_pushed_at 以步骤起点刷新(投影 current_step_status 对下一个 pending 步本就
|
|
626
|
+
* 推导为 active,X3 不改变其值,只让「最后信号」时间可信,停滞判定不再失真)。
|
|
627
|
+
* 与 triggerSync 同款契约:quick 会话降级跳过(无 progress 实体)、未连接平台在
|
|
628
|
+
* sync() 内部静默跳过、8s 总熔断(raceWithAbort + abort 在飞请求)、失败只 warn 不抛。
|
|
629
|
+
*
|
|
630
|
+
* 接线说明(本变更 allowed_paths 约束):步骤 prompt 渲染点(run/stage.js outputStep
|
|
631
|
+
* 前段 / run/prompt.js)不在本变更可改文件集内——本导出即「步骤开始钩子」本体,
|
|
632
|
+
* 渲染侧一行 `triggerStepStartSync(cwd, changeName, platformOpts)` 的接线由后续
|
|
633
|
+
* 变更补上;execute 阶段的任务粒度信号已由 X4(stages/execute.js Wave prompt
|
|
634
|
+
* 每任务上报指引)覆盖。
|
|
635
|
+
*/
|
|
636
|
+
export async function triggerStepStartSync(cwd, changeName, platformOpts = {}, opts = {}) {
|
|
637
|
+
try {
|
|
638
|
+
const tsSpecBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
|
|
639
|
+
if (changeName && QUICK_SID_RE.test(changeName) && !existsSync(join(tsSpecBase, 'changes', changeName))) {
|
|
640
|
+
return // quick 会话无 progress 实体(与 triggerSync 同判),无步骤可报
|
|
641
|
+
}
|
|
642
|
+
// shared.js 在 src/run/,sync.js 在 src/ → 退一层
|
|
643
|
+
const syncMod = await import('../sync.js')
|
|
644
|
+
await raceWithAbort((sig) => syncMod.sync(changeName, cwd, { ...sig, stepStart: true }), opts.timeoutMs)
|
|
645
|
+
} catch (e) {
|
|
646
|
+
// Best Effort:步骤开始上报失败不影响主流程(下次 --done 的 triggerSync 兜底)
|
|
647
|
+
console.warn('⚠️ 步骤开始上报失败:', e.message)
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
619
651
|
/**
|
|
620
652
|
* 自动 pull 节流(体检 PERF-02):triggerPull/triggerPullActiveChange 在每条 stage 命令
|
|
621
653
|
* 启动时注入,agent 命令往往是秒级连发——每次都发 HTTP GET(daemon 挂/慢时单命令最多
|
|
@@ -813,6 +845,54 @@ export function isQuickMetadata(p, linkedChanges = []) {
|
|
|
813
845
|
return false
|
|
814
846
|
}
|
|
815
847
|
|
|
848
|
+
// 危险文件清单(auditQuickCompletion 危险门与 predictProtectedQuickFiles 预告共用单一真相源)。
|
|
849
|
+
// SillySpec 核心流程代码。W6 重构后 src/run.js(23 行 barrel)/ src/progress.js(facade)
|
|
850
|
+
// 把真正逻辑下沉到 src/run/、src/progress/ 子目录——旧的精确文件名匹配
|
|
851
|
+
// (file === 'src/run.js')命中不到 src/run/command.js(. 与 / 不可混同),致危险文件门
|
|
852
|
+
// 静默失效。改用「目录前缀(带尾斜杠,startsWith 不会误伤 src/runtime-* 等同名前缀)+
|
|
853
|
+
// barrel/facade 本体精确名」双重覆盖。重构 src/ 模块时须同步本清单(multi-agent-review Q5)。
|
|
854
|
+
const QUICK_DANGEROUS_PATTERNS = [
|
|
855
|
+
'package.json',
|
|
856
|
+
'package-lock.json',
|
|
857
|
+
'yarn.lock',
|
|
858
|
+
'pnpm-lock.yaml',
|
|
859
|
+
'.eslintrc',
|
|
860
|
+
'tsconfig.json',
|
|
861
|
+
'src/run.js',
|
|
862
|
+
'src/run/',
|
|
863
|
+
'src/progress.js',
|
|
864
|
+
'src/progress/',
|
|
865
|
+
'src/db.js',
|
|
866
|
+
'src/stage-contract.js',
|
|
867
|
+
'src/stage-contract-spec.js',
|
|
868
|
+
'src/worktree.js',
|
|
869
|
+
'src/worktree-apply.js',
|
|
870
|
+
'src/hooks/',
|
|
871
|
+
]
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* 预告:--files 声明中哪些文件会在 --done 审计被危险/基线门拦截(坑 quick-protected-late-hint,
|
|
875
|
+
* 2026-08-28 用户实证:scan 类文档 ARCHITECTURE/CONCERNS 属受保护基线,--files 声明了照样拦、
|
|
876
|
+
* 必须 --force-baseline——设计合理但提示太晚,要等审计轮才发现,白跑一轮往返)。
|
|
877
|
+
* 判定与 auditQuickCompletion 危险门逐字同口径(isQuickMetadata 豁免 + 关联变更目录退栈 +
|
|
878
|
+
* DANGEROUS_PATTERNS 前缀/精确匹配),forceBaseline 已带 → 无预告。
|
|
879
|
+
* @param {string[]} files 声明文件(--files)
|
|
880
|
+
* @param {{ linkedChanges?: string[], forceBaseline?: boolean }} opts
|
|
881
|
+
* @returns {string[]} 将被拦截的声明文件子集
|
|
882
|
+
*/
|
|
883
|
+
export function predictProtectedQuickFiles(files, { linkedChanges = [], forceBaseline = false } = {}) {
|
|
884
|
+
if (forceBaseline) return []
|
|
885
|
+
const linked = (Array.isArray(linkedChanges) ? linkedChanges : []).map(c => String(c).replace(/\\/g, '/'))
|
|
886
|
+
const isUnderLinkedChange = (f) => linked.some(lc => f.startsWith(`.sillyspec/changes/${lc}/`))
|
|
887
|
+
return (files || [])
|
|
888
|
+
.filter(Boolean)
|
|
889
|
+
.map(f => String(f).replace(/\\/g, '/'))
|
|
890
|
+
.filter(f => {
|
|
891
|
+
if (f.startsWith('.sillyspec/') && !isQuickMetadata(f, linked) && !isUnderLinkedChange(f)) return true
|
|
892
|
+
return QUICK_DANGEROUS_PATTERNS.some(p => p === f || f.startsWith(p))
|
|
893
|
+
})
|
|
894
|
+
}
|
|
895
|
+
|
|
816
896
|
/**
|
|
817
897
|
* quick 完成审计:对比 baseline 与实际变更。
|
|
818
898
|
* @returns {{ status: 'safe'|'warning'|'blocked', reasons: string[], changedFiles: string[], newFiles: string[], deletedFiles: string[], baselineHit: string[], stagedTotal: number, attributedFiles: string[], undeclaredFiles: string[], foreignSessionDeclared: Array<{file: string, sessions: string[]}> }}
|
|
@@ -990,29 +1070,7 @@ export async function auditQuickCompletion(cwd, guard, options = {}) {
|
|
|
990
1070
|
}
|
|
991
1071
|
return foreignDeclaredIndex.get(f) || null
|
|
992
1072
|
}
|
|
993
|
-
const DANGEROUS_PATTERNS =
|
|
994
|
-
'package.json',
|
|
995
|
-
'package-lock.json',
|
|
996
|
-
'yarn.lock',
|
|
997
|
-
'pnpm-lock.yaml',
|
|
998
|
-
'.eslintrc',
|
|
999
|
-
'tsconfig.json',
|
|
1000
|
-
// SillySpec 核心流程代码。W6 重构后 src/run.js(23 行 barrel)/ src/progress.js(facade)
|
|
1001
|
-
// 把真正逻辑下沉到 src/run/、src/progress/ 子目录——旧的精确文件名匹配
|
|
1002
|
-
// (file === 'src/run.js')命中不到 src/run/command.js(. 与 / 不可混同),致危险文件门
|
|
1003
|
-
// 静默失效。改用「目录前缀(带尾斜杠,startsWith 不会误伤 src/runtime-* 等同名前缀)+
|
|
1004
|
-
// barrel/facade 本体精确名」双重覆盖。重构 src/ 模块时须同步本清单(multi-agent-review Q5)。
|
|
1005
|
-
'src/run.js',
|
|
1006
|
-
'src/run/',
|
|
1007
|
-
'src/progress.js',
|
|
1008
|
-
'src/progress/',
|
|
1009
|
-
'src/db.js',
|
|
1010
|
-
'src/stage-contract.js',
|
|
1011
|
-
'src/stage-contract-spec.js',
|
|
1012
|
-
'src/worktree.js',
|
|
1013
|
-
'src/worktree-apply.js',
|
|
1014
|
-
'src/hooks/',
|
|
1015
|
-
]
|
|
1073
|
+
const DANGEROUS_PATTERNS = QUICK_DANGEROUS_PATTERNS
|
|
1016
1074
|
|
|
1017
1075
|
for (const entry of currentEntries) {
|
|
1018
1076
|
// porcelain 两字符状态码(X=暂存区 Y=工作区)不能先 trim:' D'.trim()==='D' 使第二分支恒假、
|
package/src/run/stage.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
import { join, dirname } from 'node:path'
|
|
20
20
|
import { existsSync, readdirSync, readFileSync, mkdirSync } from 'node:fs'
|
|
21
21
|
import { writeAtomicSync } from '../fs-atomic.js'
|
|
22
|
-
import { resolveSpecDir, resolveChangeDir, resolveRuntimeRoot, resolveQuickSessionsDir, triggerSync, safeGit, parsePorcelainPath, formatWaitOptions, checkApproval, getStageSteps, warnApprovalUnknown } from './shared.js'
|
|
22
|
+
import { resolveSpecDir, resolveChangeDir, resolveRuntimeRoot, resolveQuickSessionsDir, triggerSync, safeGit, parsePorcelainPath, formatWaitOptions, checkApproval, getStageSteps, warnApprovalUnknown, predictProtectedQuickFiles } from './shared.js'
|
|
23
23
|
import { computeScanProfile, applyScanProfileSteps, executeScanPreflight, executeScanPostcheck } from './scan-profile.js'
|
|
24
24
|
import { outputStep, collectStageWaitHistory } from './prompt.js'
|
|
25
25
|
import { allocateQuicklogEntry, deriveTitleFromLinkedChange, sanitizeDesc } from '../quicklog.js'
|
|
@@ -328,6 +328,15 @@ export async function runStage(pm, progress, stageName, cwd, changeName, skipApp
|
|
|
328
328
|
// (D-003@v1:顶层 quickGuard 不跨进程持久化;agent 在 step 间用 `run quick` 取下一步
|
|
329
329
|
// prompt 时每个新进程都进此块,按文件判幂等才不会重复分配 ql-ID / 重复写条目)。
|
|
330
330
|
if (stageName === 'quick') {
|
|
331
|
+
// 受保护文件预告打印(坑 quick-protected-late-hint,2026-08-28 用户实证:scan 类文档
|
|
332
|
+
// ARCHITECTURE/CONCERNS 属受保护基线,--files 声明了照样拦、必须 --force-baseline——
|
|
333
|
+
// 设计合理但提示太晚。step1 即预告哪些声明文件会触发拦截,省一轮跑到 --done 才发现的往返)
|
|
334
|
+
const warnProtectedQuickFiles = (protectedFiles) => {
|
|
335
|
+
console.warn(`⚠️ 声明文件中 ${protectedFiles.length} 个属受保护/危险范围,--done 审计将拦截(--files 只声明归属,不解锁拦截):`)
|
|
336
|
+
for (const f of protectedFiles) console.warn(` - ${f}`)
|
|
337
|
+
console.warn(` 确认要改这类文件:本会话收尾带 --force-baseline(sillyspec run quick --done --force-baseline ...);`)
|
|
338
|
+
console.warn(` 属误判或不该由本会话改:换出 --files 声明,或走完整流程(execute 有 review 把关)。`)
|
|
339
|
+
}
|
|
331
340
|
// quick-sessions 目录经 resolveQuickSessionsDir 单一解析(multi-agent-review Q4):
|
|
332
341
|
// 平台模式 runtimeRoot 与 specBase/.runtime 不同时,写/读须对齐,否则 guard 写一处、收尾读另一处。
|
|
333
342
|
const sessionGuardDir = join(resolveQuickSessionsDir(platformOpts, specBase), changeName)
|
|
@@ -348,6 +357,13 @@ export async function runStage(pm, progress, stageName, cwd, changeName, skipApp
|
|
|
348
357
|
const known = new Set(progress.quickGuard.allowedFiles || [])
|
|
349
358
|
const added = resumeFiles.filter(f => !known.has(f))
|
|
350
359
|
if (added.length > 0) {
|
|
360
|
+
// 受保护文件预告(坑 quick-protected-late-hint):追加声明若含审计将拦的文件,
|
|
361
|
+
// 追加时即点破(省一轮跑到 --done 才发现)。--files 只声明归属,不解锁拦截。
|
|
362
|
+
const protectedPreview = predictProtectedQuickFiles(added, {
|
|
363
|
+
linkedChanges: progress.quickGuard.linkedChanges || [],
|
|
364
|
+
forceBaseline: quickOpts?.isForceBaseline || false,
|
|
365
|
+
})
|
|
366
|
+
if (protectedPreview.length > 0) warnProtectedQuickFiles(protectedPreview)
|
|
351
367
|
progress.quickGuard.allowedFiles = [...(progress.quickGuard.allowedFiles || []), ...added]
|
|
352
368
|
progress.quickGuard.allowedFilesHash = { ...(progress.quickGuard.allowedFilesHash || {}) }
|
|
353
369
|
for (const f of added) {
|
|
@@ -385,6 +401,10 @@ export async function runStage(pm, progress, stageName, cwd, changeName, skipApp
|
|
|
385
401
|
const allowDelete = quickOpts?.isAllowDelete || false
|
|
386
402
|
const forceBaseline = quickOpts?.isForceBaseline || false
|
|
387
403
|
const linkedChanges = Array.isArray(quickOpts?.linkedChanges) ? quickOpts.linkedChanges : []
|
|
404
|
+
// 受保护文件预告(坑 quick-protected-late-hint):会话起步(step1)即点破,
|
|
405
|
+
// 判定与 --done 审计危险门同口径(predictProtectedQuickFiles)
|
|
406
|
+
const protectedPreview = predictProtectedQuickFiles(allowedFiles, { linkedChanges, forceBaseline })
|
|
407
|
+
if (protectedPreview.length > 0) warnProtectedQuickFiles(protectedPreview)
|
|
388
408
|
// CLI 接管:分配 ql-ID + 写 QUICKLOG「进行中」条目 + 关联 tasks.md(持锁、当天唯一)
|
|
389
409
|
const gitUser = safeGit(cwd, ['config', 'user.name']).value || 'unknown'
|
|
390
410
|
// 标题回退:启动 quick 不带 --input 时,从关联变更的 proposal/design 提取语义标题,
|
package/src/spec-sync.js
CHANGED
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
*
|
|
9
9
|
* 复用 daemon 已验证的排除口径(UPLOAD_EXCLUDE_TOP_BASE / UPLOAD_PRUNE_NAMES_BASE),
|
|
10
10
|
* 与 daemon 共用同一套清单语义(base_version 乐观锁、conflict 提示但不阻塞)。
|
|
11
|
+
*
|
|
12
|
+
* 并行会话 fail-closed 护栏(computeSpecOps 空树/changes 整删守卫 + filterStaleUpdates
|
|
13
|
+
* 旧副本回推守卫):共享同一平台工作空间的多会话/多机场景下,错锚或滞后本地的破坏性
|
|
14
|
+
* ops(整删 changes/、旧版内容回推)在发出前拦下——服务器侧一旦被破坏会经同步链路
|
|
15
|
+
* 落地回各端本地(2026-08-26/27 两次实证)。
|
|
11
16
|
*/
|
|
12
17
|
|
|
13
18
|
import { createHash } from 'crypto';
|
|
@@ -140,6 +145,20 @@ export function computeSpecOps(serverManifest, localFiles) {
|
|
|
140
145
|
return [];
|
|
141
146
|
}
|
|
142
147
|
|
|
148
|
+
// 并行会话护栏(坑 spec-sync-parallel-changes-wipe,2026-08-26/27 两次实证:平台同步
|
|
149
|
+
// 删整个 changes 目录):本地树非空但 changes/ 子树一个文件都没有、服务器却有——
|
|
150
|
+
// 几乎必然是错锚/新机未先落地,而非用户真删光了所有变更(archive 是移动不是清空)。
|
|
151
|
+
// 放行即服务器 changes/ 整树清空,再经 daemon/对端同步落地回本地(破坏本地)。
|
|
152
|
+
// 逐个变更目录的合法删除不受影响(本地仍有其他 changes/ 文件时不触发本护栏)。
|
|
153
|
+
const serverChanges = serverPaths.filter((p) => p.startsWith('changes/'));
|
|
154
|
+
const localChanges = localFiles.filter((f) => f.path.startsWith('changes/'));
|
|
155
|
+
if (localFiles.length > 0 && serverChanges.length > 0 && localChanges.length === 0) {
|
|
156
|
+
console.warn(
|
|
157
|
+
`[spec-sync] 本地 spec 树无 changes/ 文件但服务器有 ${serverChanges.length} 个,跳过同步(防并行会话/错锚整删 changes/;请检查 spec 目录锚点,新环境先完成平台侧文件落地)`
|
|
158
|
+
);
|
|
159
|
+
return [];
|
|
160
|
+
}
|
|
161
|
+
|
|
143
162
|
// rename 检测:旧路径(服务器有、本地无)↔ 新路径(本地有、服务器无)hash 相同
|
|
144
163
|
const renames = [];
|
|
145
164
|
const consumedNew = new Set();
|
|
@@ -248,6 +267,62 @@ function collectChangeDir(dirs, p) {
|
|
|
248
267
|
}
|
|
249
268
|
}
|
|
250
269
|
|
|
270
|
+
/**
|
|
271
|
+
* 过滤「旧副本回推」型 update op(坑 spec-sync-parallel-stale-overwrite,2026-08-26/27
|
|
272
|
+
* 两次实证:平台同步把本地文件覆盖回旧版)。
|
|
273
|
+
*
|
|
274
|
+
* 判据:本地文件 mtime 早于上次成功同步(mtime + 1s 缓冲)= 该文件自上次同步后本地从未
|
|
275
|
+
* 改动——此刻服务器 hash 却不同,只可能是他者会话/他机推进了服务器内容。本地这份是旧
|
|
276
|
+
* 副本,照推 update 就是把服务器内容回退到旧版(再经 daemon/对端落地破坏各端本地)。
|
|
277
|
+
* 本地改过的文件(mtime 晚于上次同步)不受影响,正常 last-writer-wins。
|
|
278
|
+
*
|
|
279
|
+
* 被滤掉的文件如确需以本地为准覆盖服务器:本地重存(touch/再编辑)后重推。
|
|
280
|
+
*
|
|
281
|
+
* @param {Array} ops computeSpecOps 产物
|
|
282
|
+
* @param {Array} localFiles hashFiles 产物(含 mtime,Unix 秒)
|
|
283
|
+
* @param {number|null} lastSyncTs 上次成功同步完成时刻(ms);null/0 → 不过滤
|
|
284
|
+
* @returns {{ ops: Array, stale: string[] }} 过滤后 ops + 被拦下的路径
|
|
285
|
+
*/
|
|
286
|
+
export function filterStaleUpdates(ops, localFiles, lastSyncTs) {
|
|
287
|
+
if (!lastSyncTs || !Array.isArray(ops) || ops.length === 0) {
|
|
288
|
+
return { ops, stale: [] };
|
|
289
|
+
}
|
|
290
|
+
const mtimeByPath = new Map(localFiles.map((f) => [f.path, f.mtime]));
|
|
291
|
+
const stale = [];
|
|
292
|
+
const kept = [];
|
|
293
|
+
for (const op of ops) {
|
|
294
|
+
if (op.op === 'update') {
|
|
295
|
+
const mtime = mtimeByPath.get(op.path);
|
|
296
|
+
if (mtime !== undefined && mtime * 1000 + 1000 <= lastSyncTs) {
|
|
297
|
+
stale.push(op.path);
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
kept.push(op);
|
|
302
|
+
}
|
|
303
|
+
return { ops: kept, stale };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// 上次成功同步标记(本地 .runtime 下,walk 排除不上传):filterStaleUpdates 的时间锚。
|
|
307
|
+
const LAST_SUCCESS_MARKER = 'spec-sync-last-success.json';
|
|
308
|
+
|
|
309
|
+
function readLastSyncTs(specRoot) {
|
|
310
|
+
try {
|
|
311
|
+
const raw = readFileSync(join(specRoot, '.runtime', LAST_SUCCESS_MARKER), 'utf8');
|
|
312
|
+
const ts = JSON.parse(raw).ts;
|
|
313
|
+
return Number.isFinite(ts) ? ts : null;
|
|
314
|
+
} catch {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function writeLastSyncTs(specRoot) {
|
|
320
|
+
try {
|
|
321
|
+
mkdirSync(join(specRoot, '.runtime'), { recursive: true });
|
|
322
|
+
writeFileSync(join(specRoot, '.runtime', LAST_SUCCESS_MARKER), JSON.stringify({ ts: Date.now() }) + '\n', 'utf8');
|
|
323
|
+
} catch { /* 标记失败只损失下次的防回推过滤,不影响本次同步结果 */ }
|
|
324
|
+
}
|
|
325
|
+
|
|
251
326
|
/**
|
|
252
327
|
* 同步本地 .sillyspec 树到平台(CLI 直跑增量同步入口)。
|
|
253
328
|
*
|
|
@@ -292,7 +367,17 @@ export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
|
|
|
292
367
|
// 2. walk/hash/diff
|
|
293
368
|
const entries = walkSpecTree(specRoot);
|
|
294
369
|
const localFiles = hashFiles(entries);
|
|
295
|
-
|
|
370
|
+
let ops = computeSpecOps(serverManifest, localFiles);
|
|
371
|
+
|
|
372
|
+
// 旧副本回推防护(并行会话一致性):上次同步后本地从未改动的文件不回推覆盖服务器新内容
|
|
373
|
+
const lastSyncTs = readLastSyncTs(specRoot);
|
|
374
|
+
const filtered = filterStaleUpdates(ops, localFiles, lastSyncTs);
|
|
375
|
+
if (filtered.stale.length > 0) {
|
|
376
|
+
console.warn(
|
|
377
|
+
`[spec-sync] 拦下 ${filtered.stale.length} 个旧副本回推(本地自上次同步未改动而服务器已前进,如确需以本地为准:重存后重推): ${filtered.stale.slice(0, 5).join(', ')}${filtered.stale.length > 5 ? ' 等' : ''}`
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
ops = filtered.ops;
|
|
296
381
|
|
|
297
382
|
if (ops.length === 0) {
|
|
298
383
|
debugLog(`[spec-sync] 无差异,跳过同步: ${changeName}`);
|
|
@@ -342,6 +427,7 @@ export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
|
|
|
342
427
|
return { synced: 0, conflict: true, serverVersions, conflictPath };
|
|
343
428
|
}
|
|
344
429
|
console.log(`[spec-sync] 已同步 ${ops.length} 个文件变更: ${changeName}`);
|
|
430
|
+
writeLastSyncTs(specRoot);
|
|
345
431
|
return { synced: ops.length };
|
|
346
432
|
} catch (err) {
|
|
347
433
|
console.warn(`[spec-sync] 同步异常(文件树本次未同步,下次自动重试): ${changeName}: ${err.message}`);
|
|
@@ -354,5 +440,6 @@ export default {
|
|
|
354
440
|
hashFiles,
|
|
355
441
|
computeSpecOps,
|
|
356
442
|
extractChangeDirs,
|
|
443
|
+
filterStaleUpdates,
|
|
357
444
|
syncSpecTree,
|
|
358
445
|
};
|
package/src/stages/archive.js
CHANGED
|
@@ -177,6 +177,11 @@ created_at: <now-datetime>
|
|
|
177
177
|
### 输出
|
|
178
178
|
归档完成 + archive 目录路径`,
|
|
179
179
|
outputHint: '归档确认',
|
|
180
|
+
// 坑 archive-batch-31-tool-notes ②:完成本步必须带 --confirm(complete-handlers 门控),
|
|
181
|
+
// 但 outputStep 机器生成的「完成后执行」提示原是通用 --done 模板不带 --confirm——agent
|
|
182
|
+
// 照抄执行就撞「请添加 --confirm」。requiresConfirm 让提示行带上该 flag(数据驱动,其他
|
|
183
|
+
// 阶段未来有确认步同样声明即可)。
|
|
184
|
+
requiresConfirm: true,
|
|
180
185
|
optional: false
|
|
181
186
|
},
|
|
182
187
|
{
|
package/src/stages/execute.js
CHANGED
|
@@ -1214,6 +1214,7 @@ ${taskList}
|
|
|
1214
1214
|
4. 每个任务完成后:
|
|
1215
1215
|
- **在 worktree 内 git add -A && git commit -m "<task-NN 摘要>"**(坑 subagent-uncommitted-newfile-apply3way,2026-08-22 实证:纯新增文件不 commit 时 apply 的 git apply --3way 报 "does not exist in index" 直接炸——未 commit 的新文件不在 base commit 也不在 index,patch 生成取不到。commit 后 base..HEAD diff 完整、apply 顺畅,review.json 的 head 也有真实锚点)
|
|
1216
1216
|
- **先写 review.json 再勾选 checkbox**(见下方 Task Review Gate)
|
|
1217
|
+
- **任务边界上报(每任务一次,主仓根目录)**:勾选 checkbox 后跑一次 \`sillyspec platform sync --change <change-name>\`——以任务粒度把「最后信号」(last_pushed_at)与 tasks.md 勾选状态推上平台(变更中心「进行中」可见性);未连接平台时该命令静默跳过,无需先检查连接状态
|
|
1217
1218
|
- **既跑 lint check 也跑 formatter**:凡变更涉及的源码跑项目的 lint 检查 **和** 格式化(如 \`ruff format\` / \`prettier --write\`),不要只跑 check——只 check 不 format 会把格式问题留到 commit 时被 pre-commit hook 拦截(worktree 内二进制可能缺失,先 \`which <bin>\` 确认,缺则 \`uv tool install\` / \`uv sync\`)
|
|
1218
1219
|
- 记录改动文件和测试结果
|
|
1219
1220
|
5. 遇到 BLOCKED → 记录原因,选择:重试/跳过/停止
|
|
@@ -1265,10 +1266,11 @@ task-XX 对应:{SPEC_ROOT}/.runtime/execute-runs/{EXECUTE_RUN_ID}/tasks/task-X
|
|
|
1265
1266
|
|
|
1266
1267
|
### 完成后
|
|
1267
1268
|
1. 为每个后端 router task 生成 API 端点 artifact——**用 CLI 命令,勿手扫装饰器手写(易漏 endpoint)**:
|
|
1268
|
-
-
|
|
1269
|
+
- 聚合一次跑全:sillyspec endpoints extract --change <change> --all-tasks(逐 task 卡 allowed_paths 各自提取落盘,<change> = 当前变更名;与 verify 探针 5 聚合对账口径一致)
|
|
1269
1270
|
- CLI 静态扫描变更文件(FastAPI @router.* / Express router.* / Spring @*Mapping)生成
|
|
1270
1271
|
{SPEC_ROOT}/.runtime/contract-artifacts/<task-name>/endpoints.json
|
|
1271
1272
|
- 格式: { "task": "task-XX", "type": "backend_endpoints", "endpoints": [{ "method": "GET", "path": "/api/ppm/xxx" }] }(CLI 已按此格式写好,verify 探针 5 直接消费)
|
|
1273
|
+
- 只提单 task(--task task-NN)会让探针 5 用局部端点集对账放大假 missing——多 task 变更必用 --all-tasks
|
|
1272
1274
|
`
|
|
1273
1275
|
}
|
|
1274
1276
|
|