sillyspec 3.28.9 → 3.28.11
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 +223 -222
- package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
- package/.claude/skills/sillyspec-quick/SKILL.md +8 -10
- package/.claude/skills/sillyspec-verify/SKILL.md +91 -87
- package/package.json +1 -1
- package/src/change-list.js +73 -20
- package/src/change-risk-profile.js +51 -3
- package/src/config-schema.js +51 -0
- package/src/contract-matrix.js +84 -5
- package/src/cross-repo-reconcile.js +134 -0
- package/src/design-facts.js +94 -18
- package/src/docs-check.js +14 -10
- package/src/docs-debt.js +7 -2
- package/src/docs-gate.js +42 -15
- package/src/endpoint-extractor.js +50 -2
- package/src/foreign-declared.js +25 -16
- package/src/hooks/worktree-guard.js +75 -2
- package/src/index.js +147 -2
- package/src/knowledge-classify.js +20 -1
- package/src/machine-interface.js +26 -0
- package/src/probe7-anchor-check.js +87 -0
- package/src/progress/stage-machine.js +34 -5
- package/src/progress.js +1 -1
- package/src/quicklog.js +240 -5
- package/src/run/command.js +25 -2
- package/src/run/complete-handlers.js +172 -1
- package/src/run/complete.js +143 -11
- package/src/run/gate-snapshot.js +372 -2
- package/src/run/gates.js +130 -12
- package/src/run/prompt.js +166 -7
- package/src/run/shared.js +81 -1
- package/src/scope-audit.js +101 -31
- package/src/stage-contract-spec.js +14 -0
- package/src/stage-contract.js +314 -8
- package/src/stage-review-checklist.js +3 -1
- package/src/stage-review.js +137 -2
- package/src/stages/archive.js +9 -5
- package/src/stages/brainstorm.js +7 -1
- package/src/stages/cmd-existence.js +9 -3
- package/src/stages/execute.js +62 -8
- package/src/stages/knowledge.js +24 -3
- package/src/stages/plan-postcheck.js +74 -10
- package/src/stages/plan.js +8 -1
- package/src/stages/quick.js +5 -5
- package/src/stages/verify.js +3 -2
- package/src/sync-noise.js +57 -1
- package/src/sync.js +77 -4
- package/src/task-review.js +84 -29
- package/src/verify-facts-schema.js +78 -9
- package/src/verify-postcheck.js +439 -68
- package/src/verify-probes.js +1156 -45
- package/src/worktree-apply.js +323 -8
- package/src/worktree-deps.js +24 -1
- package/src/worktree.js +274 -10
- package/src/write-audit.js +55 -0
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { existsSync, readFileSync } from 'fs'
|
|
16
|
-
import { join } from 'path'
|
|
16
|
+
import { join, isAbsolute } from 'path'
|
|
17
17
|
|
|
18
18
|
// 提取 `npm|pnpm|yarn run <script>` 命令。
|
|
19
19
|
// 对齐 scan-postcheck:136 的 `/npm run (\S+)/g`,扩展包管理器 + 容忍多空白。
|
|
@@ -50,7 +50,12 @@ function readScripts(pkgPath) {
|
|
|
50
50
|
* 为单条命令计算候选 package.json 路径列表。
|
|
51
51
|
*
|
|
52
52
|
* 规则(blueprint task-02 + design §5 H2):
|
|
53
|
-
* - 有 `cd <subdir> &&` 前缀 → 仅查 `<
|
|
53
|
+
* - 有 `cd <subdir> &&` 前缀 → 仅查 `<subdir>/package.json`(cd 锁定子目录,不回退根)。
|
|
54
|
+
* subdir 为绝对路径(win 盘符 / posix 根)时不再拼 projectRoot——坑
|
|
55
|
+
* verify-cmd-absolute-cd-path(2026-09-15 wp 会话实证):跨仓 task 卡 verify 写
|
|
56
|
+
* `cd E:/PZwangge/sub-grid-security && npm run lint`,join(主仓根, 'E:/...') 拼出
|
|
57
|
+
* 病态路径 → package.json 恒 missing → 真实命令被误判死命令,agent 只能改写成
|
|
58
|
+
* ../ 相对路径绕过
|
|
54
59
|
* - 无前缀 + modules 块提供 → 查每个 module.path 的子包 + 根 package.json(多候选任一命中即视为存在)
|
|
55
60
|
* - 无前缀 + 无 modules → 仅查 `<projectRoot>/package.json`
|
|
56
61
|
*
|
|
@@ -65,7 +70,8 @@ function resolveCandidates(text, matchIndex, projectRoot, modules) {
|
|
|
65
70
|
const cdMatch = before.match(CD_PREFIX_RE)
|
|
66
71
|
if (cdMatch) {
|
|
67
72
|
const subdir = cdMatch[1].replace(/^["']|["']$/g, '')
|
|
68
|
-
|
|
73
|
+
const base = isAbsolute(subdir) ? subdir : join(projectRoot, subdir)
|
|
74
|
+
return [join(base, 'package.json')]
|
|
69
75
|
}
|
|
70
76
|
if (modules && typeof modules === 'object') {
|
|
71
77
|
const candidates = []
|
package/src/stages/execute.js
CHANGED
|
@@ -197,14 +197,14 @@ export function validatePlanForExecute(tasksContent, planContent) {
|
|
|
197
197
|
}
|
|
198
198
|
|
|
199
199
|
// 检查 0.8: Wave 标题格式不对(W1/波次1 等)→ 引用行不被收容,静默退化隐式单 Wave
|
|
200
|
-
//
|
|
200
|
+
// 串行执行——Wave 分组意图丢失且无提示(原「标题格式不对」诊断的新契约承接)。仅在确有疑似标题
|
|
201
201
|
// 且无任何显式 Wave 被解析时报,正常 Wave 10/带括号标题不误伤。
|
|
202
202
|
const hasExplicitWave = waves.some(w => !w.implicit)
|
|
203
203
|
const waveLikeHeading = /^#+\s*(?:wave\s*\d+|w\d+|波次\s*\d+)/im
|
|
204
204
|
if (!hasExplicitWave && waveLikeHeading.test(plan)) {
|
|
205
205
|
errors.push(
|
|
206
206
|
`Wave 标题格式不对:必须字面 "## Wave N"(Wave + 空格 + 数字),"## W1" / "## Wave1" / "## 波次1" 都不被识别。` +
|
|
207
|
-
`其下的 "- task-XX"
|
|
207
|
+
`其下的 "- task-XX" 引用行不会被收容——所有任务将退化为单个隐式 Wave 串行执行(Wave 分组意图丢失,无法按组并行/验收)`
|
|
208
208
|
)
|
|
209
209
|
}
|
|
210
210
|
// 检查 0.9(坑 wave-heading-undercount,2026-08-21 实证):部分 Wave 被识别 + 部分
|
|
@@ -404,6 +404,12 @@ worktree 路径 + 分支名 + 模式
|
|
|
404
404
|
欠账处理:Wave 收尾时顺手同步对应模块卡,不必为此停下。变更索引类条目追加到卡同目录 \`<module>.changelog.md\` sidecar(无则创建;可先跑 \`sillyspec modules split-changelog\` 迁出历史段)——勿把历史条目堆回模块卡正文(卡是子代理的读取税)。
|
|
405
405
|
⚠️ **铁律:模块卡 / sidecar 一律写主仓绝对路径 \`{SPEC_ROOT}/docs/<project>/modules/\`(CLI 已替换为绝对路径)**——在 worktree 内执行时**绝不**写 worktree checkout 出来的 \`.sillyspec\` 副本(该副本随 worktree cleanup 整目录蒸发,主仓永远读不到;2026-09-10 实证 task-13 子代理写副本、归档阶段被迫 checkout 补救)。
|
|
406
406
|
|
|
407
|
+
### 歧义裁决权限(非破坏性自行裁决+记录,破坏性停人)
|
|
408
|
+
执行中撞到 plan/design 歧义或冲突(任务描述与代码现实不符、两任务契约打架、design 未覆盖的边界情况)时按破坏性分级处理,不要一律停下等用户、也不要静默猜测(外部实测:可控歧义一律停人导致执行会话停摆 9 小时):
|
|
409
|
+
- **非破坏性歧义**(可逆、局部、不改既有 D-xxx 决策语义、不越 allowed_paths 边界、不删不改他人模块):自行裁决继续干。裁决落盘 \`{SPEC_ROOT}/changes/<变更名>/decisions.md\` 新条目(接续既有最大编号,格式 \`## D-xxx@v1: <短标题>\`,正文含歧义描述/选择/理由/影响 task),并在该 Wave 完成输出中披露「执行期裁决 N 条:D-xxx…」——review/verify 据此对账。⚠️ decisions.md 一律写**主仓** \`{SPEC_ROOT}\` 绝对路径(CLI 已替换),勿写 worktree checkout 的 \`.sillyspec\` 副本(随 cleanup 蒸发,同模块卡铁律)。
|
|
410
|
+
- **破坏性/不可逆动作**(删数据/重写他人模块、改共享配置或门禁、跳过测试、扩大 allowed_paths 边界、推翻既有 D-xxx 决策、跨仓联动):禁止自行裁决,\`sillyspec run execute --wait --reason "<冲突描述>" --options "<裁决A>,<裁决B>,需要更多上下文"\` 停人。
|
|
411
|
+
- 拿不准是否破坏性 → 按破坏性处理(fail-closed);同一歧义重复出现 → 引用既有 D-xxx,不重复裁决。
|
|
412
|
+
|
|
407
413
|
### 铁律
|
|
408
414
|
- **不要询问用户确认频率**,默认 wave 模式;用户已明确口头指定时遵从其指定`,
|
|
409
415
|
outputHint: 'Wave 分组 + 模型分配',
|
|
@@ -422,6 +428,7 @@ const acceptanceSteps = [
|
|
|
422
428
|
tier: {REVIEW_TIER}({REVIEW_TIER_REASON})
|
|
423
429
|
- tier=self:当前 agent 汇总执行(对照 design.md 逐项检查 + 偏差说明)
|
|
424
430
|
- tier=independent:必须用 Agent tool 启动一个独立的 QA 子代理(独立上下文,不共享实现者的分析),子代理对照 design.md 逐项检查实现一致性并输出 review.json。review.json 产物契约(CLI Stage Review Gate 将硬校验,schema + 完整示例 + docHash 算法如下,照抄改值;reviewedFiles 除主文档 design.md 外可追加 git diff 涉及的源码文件):
|
|
431
|
+
{PRIOR_REVIEW_FACTS}
|
|
425
432
|
宿主环境无 Agent tool 可用(调用报 Unknown agent / Available agents: none)→ 不卡死:主代理切换为审查者角色自审替代,reviewerNotes 首行记录「降级:环境无子代理可用」,逐条结论附源码锚点(file:line 或 grep/read 证据)补偿独立性。
|
|
426
433
|
{REVIEW_JSON_CONTRACT}
|
|
427
434
|
该 acceptance review 同时覆盖"代码审查"视角(风格/bug/安全/冗余),后续代码审查步骤仅需轻量复审。
|
|
@@ -430,6 +437,7 @@ tier: {REVIEW_TIER}({REVIEW_TIER_REASON})
|
|
|
430
437
|
${REVIEW_CHECKLISTS.execute.map((item, index) => ' ' + (index + 1) + '. ' + item).join('\n')}
|
|
431
438
|
|
|
432
439
|
**产物唯一化(省重复消耗)**:本步逐项对照结论**只落盘一份**——直接写进 review.json 的 \`checklist\` 数组(item=设计要点/FR/决策,note=实现状态 ✅/⚠️/❌ + 偏差说明 + commit 锚点),reviewerNotes 写汇总。**不要**另写独立的 design-check.md 长文(同一份 design×diff 二次消费;2026-08-22 实测该重复一遍 ≈8 分钟全量重读)。
|
|
440
|
+
**执行纪律(坑 review-subagent-stall,2026-09-15 wp EHS 会话实证)**:brainstorm/plan 阶段已实证的结论(既往 stage-review checklist pass 项、file:line 锚点)直接引用勿重验;必读材料读完即逐条出结论落盘,仅结论存疑时定向补证,禁止循环扩大核验面(连续读文件 10+ 次仍零结论 = 基于已读材料立即收敛)。QA 子代理写操作持续被平台拒绝(session not in running turn 类)→ 重试 ≤3 次即停,完整结论(含 review.json 全文)回传主代理代落盘,reviewerNotes 首行留痕「代落盘:子代理写通道故障」;禁止长时间空转重试。
|
|
433
441
|
**gate 重试修复**:review.json 落盘后若 design.md 又有改版,gate 会**自动机械重算 docHash 放行**(verdict/checklist 保留,结论是否仍适用于新文档需人工确认)——**不要重做审查**(重做=同一材料第三遍);主文档路径错/缺失不会被自动修复,按 gate 报错修正 reviewedFiles[0],或跑 \`sillyspec register-stage-review --change <变更名> --stage execute --refresh-hash\`。
|
|
434
442
|
|
|
435
443
|
### 操作
|
|
@@ -652,7 +660,8 @@ function parseWavesFromPlan(planContent, registry = []) {
|
|
|
652
660
|
} catch { /* 告警失败不阻断解析 */ }
|
|
653
661
|
|
|
654
662
|
// 无显式 Wave 结构但注册表非空(light 级:任务全在 tasks.md、plan.md 只留策略)→
|
|
655
|
-
// 合成单隐式 Wave 收容全部任务(与旧 light `## Tasks` 隐式收容语义对齐,单 Wave
|
|
663
|
+
// 合成单隐式 Wave 收容全部任务(与旧 light `## Tasks` 隐式收容语义对齐,单 Wave 串行
|
|
664
|
+
// 执行——现为真实行为:buildWavePrompt 对 implicit Wave 下发串行调度指令,2026-09-17-feedback-hardening D-003@v1)
|
|
656
665
|
if (waves.length === 0 && registry.length > 0) {
|
|
657
666
|
waves.push({ index: 1, implicit: true, tasks: registry.map(t => ({ ...t })) })
|
|
658
667
|
}
|
|
@@ -1013,6 +1022,30 @@ ${indexLines}
|
|
|
1013
1022
|
}
|
|
1014
1023
|
} catch { /* 热区提取 best-effort:失败不注入 */ }
|
|
1015
1024
|
|
|
1025
|
+
// ── 全局硬约束段(ql-20260917-002,Superpowers v6.0 writing-plans 采纳④):plan.md
|
|
1026
|
+
// 「## 全局硬约束」段机械提取,作为子代理 prompt 要点第 9 条逐字下发——实现者子代理
|
|
1027
|
+
// 只读自己的任务卡 + 本段,不重读 design 全文;designHotzone 只达 Wave 协调者层,本段
|
|
1028
|
+
// 补齐子代理触达。advisory:段缺省/空体/提取失败零注入零阻断(对齐 hotzone 模式);
|
|
1029
|
+
// 超 2400 字截断(全文让子代理读 plan.md 该段)。
|
|
1030
|
+
let globalConstraintsBlock = ''
|
|
1031
|
+
try {
|
|
1032
|
+
if (changeDir) {
|
|
1033
|
+
const planLines = memo('planLines', () => {
|
|
1034
|
+
const pp = path.join(changeDir, 'plan.md')
|
|
1035
|
+
return existsSync(pp) ? readFileSync(pp, 'utf8').replace(/\r\n/g, '\n').split('\n') : []
|
|
1036
|
+
})
|
|
1037
|
+
const gcStart = planLines.findIndex(l => /^##\s*全局硬约束/.test(l))
|
|
1038
|
+
if (gcStart >= 0) {
|
|
1039
|
+
const gcEnd = planLines.findIndex((l, i) => i > gcStart && /^##\s+/.test(l))
|
|
1040
|
+
const gcBody = planLines.slice(gcStart + 1, gcEnd < 0 ? planLines.length : gcEnd).join('\n').trim()
|
|
1041
|
+
if (gcBody) {
|
|
1042
|
+
const capped = gcBody.length > 2400 ? gcBody.slice(0, 2400) + '\n…(超 2400 字截断,全文读 plan.md 该段)' : gcBody
|
|
1043
|
+
globalConstraintsBlock = `\n9. **全局硬约束(plan.md 逐字下发,每个子代理必须遵守——与本 task 蓝图冲突时以本段为准并上报主代理)**:\n\n${capped}\n`
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
} catch { /* 全局硬约束提取 best-effort:失败不注入 */ }
|
|
1048
|
+
|
|
1016
1049
|
// ── 决策锚点触碰事实(W-A 主渲染点,D-003):changedFiles 口径与 run/prompt.js {DOCS_DEBT}
|
|
1017
1050
|
// 现算同源——collectExecuteChangedFiles(porcelain 未提交 ∪ baseline..HEAD)唯一实现,勿双写。
|
|
1018
1051
|
// 单过流程前缀第 4 步渲染 {DOCS_DEBT} 时 changedFiles 恒空,Wave 步(重入/续跑时已含前置
|
|
@@ -1231,7 +1264,25 @@ ${workdirLines}
|
|
|
1231
1264
|
}
|
|
1232
1265
|
// dispatchMode === 'local'(无配置)或 worktreePath 为空 → dispatchSection = '' → 输出与改前字节一致(零回归)
|
|
1233
1266
|
|
|
1234
|
-
|
|
1267
|
+
// ── 隐式 Wave 串行化(2026-09-17-feedback-hardening D-003@v1):plan.md 无显式 Wave 划分时
|
|
1268
|
+
// parseWavesFromPlan 合成的 implicit Wave 未做过文件正交/契约链核查,并行不安全——prompt 层
|
|
1269
|
+
// 下发串行调度指令(头标注 + 角色清单 + 调度要求铁律 + batch 条件 2 括注收敛,防同 prompt
|
|
1270
|
+
// 自相矛盾)。显式 Wave 走原文条件变量,输出与改前逐字节一致(零回归)。
|
|
1271
|
+
const implicit = wave.implicit === true
|
|
1272
|
+
const waveHeader = implicit
|
|
1273
|
+
? `## Wave ${waveIndex}(隐式合成——plan.md 无显式 Wave 划分,串行执行): 执行以下任务`
|
|
1274
|
+
: `## Wave ${waveIndex}: 执行以下任务`
|
|
1275
|
+
const batchCond2 = implicit
|
|
1276
|
+
? `- 组内任意两个 task 之间无 provides / expects_from 契约链(契约 task 禁止同批——串行实现会读到半成品,契约 task 由独立子代理逐个(串行)处理或落在不同批次)`
|
|
1277
|
+
: `- 组内任意两个 task 之间无 provides / expects_from 契约链(契约 task 禁止同批——串行实现会读到半成品,契约 task 由独立子代理并行处理或落在不同 Wave)`
|
|
1278
|
+
const roleItem1 = implicit
|
|
1279
|
+
? `1. 为每个任务启动一个子代理(Agent tool),或按上述三条件把多个任务合并为一个 batch 子代理,逐个完成(串行——启动一个、等它完成并审查后再启动下一个,见「调度要求」串行铁律)`
|
|
1280
|
+
: `1. 为每个任务启动一个子代理(Agent tool),或按上述三条件把多个任务合并为一个 batch 子代理,同 Wave 内可并行`
|
|
1281
|
+
const scheduleItem1 = implicit
|
|
1282
|
+
? `1. **隐式 Wave 串行铁律**:本 Wave 由 plan.md 无显式 Wave 划分时合成——任务一律逐个完成(单子代理串行逐个实现,或逐个启动子代理并等待其完成再启动下一个),**禁止并行启动**(未做过文件正交/契约链核查,并行不安全;需要并行收益请在 plan.md 显式划分 Wave)。`
|
|
1283
|
+
: `1. **同一 Wave 的多个子代理(独立或 batch)必须并行启动,batch 内部串行**(batch 分组仅按文件正交 / 无契约链判定,不改变 Wave 依赖语义——Wave 定义=无依赖可并行;有依赖应在 plan.md 的不同 Wave 中)。`
|
|
1284
|
+
|
|
1285
|
+
return `${waveHeader}
|
|
1235
1286
|
|
|
1236
1287
|
## 执行方式
|
|
1237
1288
|
|
|
@@ -1239,13 +1290,13 @@ ${workdirLines}
|
|
|
1239
1290
|
|
|
1240
1291
|
可选 batch(合并实现):同一 Wave 内,若一组任务同时满足以下三个条件,可把它们合并为一个 batch(最多 3 个 task),交给单个子代理串行逐个完成实现:
|
|
1241
1292
|
- 组内任意两个 task 的 allowed_paths 无交集(文件正交)
|
|
1242
|
-
|
|
1293
|
+
${batchCond2}
|
|
1243
1294
|
- 组大小不超过 3 个 task
|
|
1244
1295
|
|
|
1245
1296
|
任一条件不满足,该 task 走独立子代理(默认形态);拿不准就不合并。无论独立还是 batch,实现一律由子代理完成,你不要自己写代码。
|
|
1246
1297
|
|
|
1247
1298
|
你的角色是调度者 + 审查者(batch 只合并实现、不合并审查):
|
|
1248
|
-
|
|
1299
|
+
${roleItem1}
|
|
1249
1300
|
2. 子代理完成后审查结果——batch 子代理只做实现与自验,task 审查、review.json 产出与 checkbox 勾选仍归你(主 agent),在子代理返回后逐 task 进行;审查 batch 报告时逐 task 对照 allowed_paths 检查改动文件清单有无越权
|
|
1250
1301
|
3. checkbox 由 CLI 自动勾选(review write 落盘即按 verdict 勾选 tasks.md;勿手动勾选)
|
|
1251
1302
|
4. 记录改动文件和测试结果
|
|
@@ -1267,7 +1318,7 @@ ${taskSummary}
|
|
|
1267
1318
|
6. **增量落盘与中断接手指引**:每完成一个可见产出(代码/测试/文档),立即写盘并执行一次最小验证(如语法检查、单跑相关测试)。工作过程中如被 429/API 配额/会话中断,应在最终回复里输出「已完成清单」(含文件路径、测试命令、当前卡点),不要只输出结论——主代理会依据磁盘产物和该清单判断哪些部分已完成,哪些需接手补做,避免重做已落盘的工作
|
|
1268
1319
|
7. **任务边界铁律**:严格只实现本 task 的 \`allowed_paths\` 内文件;若 design.md/plan.md 明确指定了接口/回调/钩子接入位置,必须逐字遵守;不允许顺手实现其他 task 的内容(如 task-01 不要把 task-02 的接入也做了)。如发现必须改其他 task 文件才能继续,先回到主代理由主代理决定是否重分 Wave 或调整 plan,禁止子代理私自越界
|
|
1269
1320
|
8. **batch 子代理协议**(仅当按「执行方式」节条件合并 batch 时附加进该子代理 prompt):按 batch 内 task 顺序逐个完成实现闭环——读取 tasks/task-N.md → 实现 → 跑该 task 的 verify 命令 → 记录该 task 报告(改动文件清单 / verify 结果 / 卡点)→ 才开始下一个 task;最终回复输出逐 task 报告清单。禁止写 review.json、禁止勾选 tasks.md checkbox——task 审查与勾选归主 agent,在子代理返回后逐 task 进行。越权即停:发现必须改 batch 内其他 task 或任何 batch 外 task 的 allowed_paths 文件 → 立即停止本 task 及后续,报告冲突文件与卡点,回主 agent 裁决(重分 Wave / 调整 plan / 回退独立子代理)。第 7 条任务边界铁律在 batch 语境下的「本 task」= 当前正在实现的 task
|
|
1270
|
-
|
|
1321
|
+
${globalConstraintsBlock}
|
|
1271
1322
|
{{include: testcase-design}}
|
|
1272
1323
|
|
|
1273
1324
|
${designHotzone}${decisionTouchSection}${waveKnowledgeSection}
|
|
@@ -1281,6 +1332,9 @@ ${waveStartItem1}
|
|
|
1281
1332
|
- 🌡️ 温上下文:CONVENTIONS.md + ARCHITECTURE.md(需要时加载)
|
|
1282
1333
|
- ❄️ 冷上下文:其他变更的 design.md、历史 plan.md(不要主动加载,除非明确需要)
|
|
1283
1334
|
|
|
1335
|
+
### 歧义裁决(撞到 plan/design 歧义或冲突时)
|
|
1336
|
+
非破坏性歧义(可逆、局部、不越界、不推翻 D-xxx)→ 主代理自行裁决继续:裁决写主仓 \`{SPEC_ROOT}/changes/<变更名>/decisions.md\` 新条目(\`## D-xxx@v1: <短标题>\` 接续编号,正文=歧义/选择/理由/影响 task),本 Wave 完成输出披露「执行期裁决 N 条」;review/verify 据此对账。破坏性/不可逆(删数据/重写他人模块、改共享配置或门禁、跳过测试、扩 allowed_paths、推翻既有决策、跨仓联动)→ \`sillyspec run execute --wait\` 停人。拿不准 → 按破坏性(fail-closed);重复歧义引用既有 D-xxx。子代理报告的冲突(越界类)回主代理裁决,**子代理不自行记 decisions**。
|
|
1337
|
+
|
|
1284
1338
|
### 中断续跑(如曾中断恢复)
|
|
1285
1339
|
execute 按 Wave 持久化进度,task 级进度靠 tasks.md checkbox 勾选。若本 Wave 曾因 429/API 配额/崩溃中断:
|
|
1286
1340
|
- tasks.md 中**已勾选 \`- [x]\` 的 task 已完成,跳过不重跑**(子代理也可能在完成前中断,重跑前先确认该 task 产出文件是否完整)
|
|
@@ -1292,7 +1346,7 @@ ${contractInjection}${prototypeInjection}
|
|
|
1292
1346
|
${taskList}
|
|
1293
1347
|
|
|
1294
1348
|
### 调度要求
|
|
1295
|
-
|
|
1349
|
+
${scheduleItem1}
|
|
1296
1350
|
2. **Reverse Sync**:子代理报告实现与 design.md 不一致时,先检查是代码错了还是文档有遗漏
|
|
1297
1351
|
3. **不要频繁编译!** 编译很慢,只在以下情况运行:
|
|
1298
1352
|
- 写了大量代码后需要验证语法正确性
|
package/src/stages/knowledge.js
CHANGED
|
@@ -76,10 +76,15 @@ function extractTitle(content) {
|
|
|
76
76
|
|
|
77
77
|
export async function cmdSearch(dir, args, opts = {}) {
|
|
78
78
|
const knowledgeDir = resolveKnowledgeDir(dir, opts.specDir)
|
|
79
|
+
// flag-as-value 防护(坑 knowledge-flag-as-value):值以 '--' 开头视为漏传(只读命令,
|
|
80
|
+
// 按缺参处理落入既有 required 校验/默认值,不静默拿 flag 字面量当查询词)
|
|
79
81
|
const queryIdx = args.indexOf('--query')
|
|
80
|
-
const
|
|
82
|
+
const queryRaw = queryIdx >= 0 ? args[queryIdx + 1] : ''
|
|
83
|
+
const query = queryRaw && !queryRaw.startsWith('--') ? queryRaw : ''
|
|
81
84
|
const limitIdx = args.indexOf('--limit')
|
|
82
|
-
const
|
|
85
|
+
const limitRaw = limitIdx >= 0 ? args[limitIdx + 1] : ''
|
|
86
|
+
const parsedLimit = limitRaw && !limitRaw.startsWith('--') ? parseInt(limitRaw, 10) : NaN
|
|
87
|
+
const limit = Number.isInteger(parsedLimit) && parsedLimit > 0 ? parsedLimit : 10
|
|
83
88
|
|
|
84
89
|
if (!query) {
|
|
85
90
|
output(false, {}, '--query is required')
|
|
@@ -130,8 +135,10 @@ export async function cmdSearch(dir, args, opts = {}) {
|
|
|
130
135
|
|
|
131
136
|
export async function cmdInspect(dir, args, opts = {}) {
|
|
132
137
|
const knowledgeDir = resolveKnowledgeDir(dir, opts.specDir)
|
|
138
|
+
// flag-as-value 防护同 cmdSearch(坑 knowledge-flag-as-value)
|
|
133
139
|
const idIdx = args.indexOf('--id')
|
|
134
|
-
const
|
|
140
|
+
const idRaw = idIdx >= 0 ? args[idIdx + 1] : ''
|
|
141
|
+
const id = idRaw && !idRaw.startsWith('--') ? idRaw : ''
|
|
135
142
|
|
|
136
143
|
if (!id) {
|
|
137
144
|
output(false, {}, '--id is required')
|
|
@@ -215,6 +222,20 @@ export async function cmdValidate(dir, args, opts = {}) {
|
|
|
215
222
|
})
|
|
216
223
|
}
|
|
217
224
|
}
|
|
225
|
+
|
|
226
|
+
// 2b. flag 形态关键词(坑 knowledge-flag-as-value):classify 曾把 flag-as-value 的
|
|
227
|
+
// 字面量 '--file' 写进路由行——目标文件存在、行格式合法,既有检查放行,此处补侦测
|
|
228
|
+
for (const entry of entries) {
|
|
229
|
+
const flagKw = entry.keywords.find(kw => kw.startsWith('--'))
|
|
230
|
+
if (flagKw) {
|
|
231
|
+
warnings.push({
|
|
232
|
+
code: 'flag_like_keyword',
|
|
233
|
+
path: 'knowledge/INDEX.md',
|
|
234
|
+
display: entry.display,
|
|
235
|
+
keyword: flagKw,
|
|
236
|
+
})
|
|
237
|
+
}
|
|
238
|
+
}
|
|
218
239
|
}
|
|
219
240
|
|
|
220
241
|
// 3. 扫描所有 .md 文件,检查是否在 INDEX 中注册
|
|
@@ -216,6 +216,32 @@ export function parseHeadCommit(content) {
|
|
|
216
216
|
return parseFrontmatterScalar(content, 'head_commit')
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
+
/**
|
|
220
|
+
* frontmatter 顶层键重复检测(纯函数,无 IO,2026-09-16-friction5-hardening FR-02 / D-004@v1)。
|
|
221
|
+
*
|
|
222
|
+
* 坑锚定(用户 2026-09-16 驾驭小结②):taskcard 骨架双来源反填 depends_on(tasks.md 行内
|
|
223
|
+
* 注解 + plan.md Wave 兜底)后 agent 又重复手填同键——js-yaml 4 对重复映射键 throw,本文件
|
|
224
|
+
* 四处 jsYaml.load 的 catch 全部静默降级(blueprint 契约空 / repo=null / 命令不校验 /
|
|
225
|
+
* Wave proposal best-effort 跳过 = 吞字段),parseDependsOn 正则又只取首个命中——重复键在
|
|
226
|
+
* 下游全链路静默失真,只能在 feasibility 入口单点拦截。行首 ^key: 顶格锚定:块列表项
|
|
227
|
+
* (- x)与缩进子键天然不匹配;goal: > 折叠块的缩进续行不匹配(顶格续行属罕见畸形,
|
|
228
|
+
* 宁报错不漏报——fail-closed)。
|
|
229
|
+
* @param {string} fmText frontmatter 文本(--- 之间的内容;CRLF 入口归一,同 parseDependsOn 惯例)
|
|
230
|
+
* @returns {Array<{key: string, lines: number[]}>} 出现 ≥2 次的顶层键及其行号(1-based,相对 fmText)
|
|
231
|
+
*/
|
|
232
|
+
export function detectDuplicateTopKeys(fmText) {
|
|
233
|
+
const keyLines = new Map()
|
|
234
|
+
String(fmText || '').replace(/\r\n/g, '\n').split('\n').forEach((line, idx) => {
|
|
235
|
+
const m = line.match(/^([A-Za-z_][\w-]*):/)
|
|
236
|
+
if (!m) return
|
|
237
|
+
if (!keyLines.has(m[1])) keyLines.set(m[1], [])
|
|
238
|
+
keyLines.get(m[1]).push(idx + 1)
|
|
239
|
+
})
|
|
240
|
+
return [...keyLines.entries()]
|
|
241
|
+
.filter(([, lines]) => lines.length >= 2)
|
|
242
|
+
.map(([key, lines]) => ({ key, lines }))
|
|
243
|
+
}
|
|
244
|
+
|
|
219
245
|
/**
|
|
220
246
|
* 从 local.yaml 文本解析 repos: 段(跨仓 workspace 注册表)。
|
|
221
247
|
* 与 parseLocalYamlModules 同风格(轻量行扫描,不引 yaml 依赖),结构 Map<repoKey, absolutePath>。
|
|
@@ -397,7 +423,8 @@ export function topoSortWaves(depMap) {
|
|
|
397
423
|
* - `## Wave N` 标题下的 task-XX checkbox 归入 Wave N;
|
|
398
424
|
* - 非 Wave 标题行(## 自检 等)退出当前 Wave 段,避免其 checkbox 混入。
|
|
399
425
|
* 返回 null = plan.md 不存在或无显式 Wave 标题 → execute 会把所有 task 放进单个隐式 Wave
|
|
400
|
-
*
|
|
426
|
+
* 串行执行(共享文件安全,2026-09-17-feedback-hardening D-003@v1),调用方据此降为 warning
|
|
427
|
+
* (显式划分 Wave 才有按组并行收益;同 Wave 内共享文件仍 error)。
|
|
401
428
|
*
|
|
402
429
|
* @param {string} planPath - plan.md 绝对路径
|
|
403
430
|
* @returns {Map<string, number>|null} taskId → waveIndex;null = 无显式 Wave
|
|
@@ -562,15 +589,16 @@ export function validateBlueprintConsistency(changeDir, opts = {}) {
|
|
|
562
589
|
// 子代理会互相覆盖该文件 → error。跨 Wave 同文件 → 串行执行安全 → warning。
|
|
563
590
|
// 按 (repo, path) 二元组判冲突(约束③):跨仓 task 与主仓 task 同名路径不误判(不同 repo → 不同键)。
|
|
564
591
|
// Wave 口径 = plan.md 显式 `## Wave N`(parseTaskWavesFromPlan,与 execute 同源,非 topoSort 建议值);
|
|
565
|
-
// plan.md 无显式 Wave → execute
|
|
592
|
+
// plan.md 无显式 Wave → execute 按单个隐式 Wave 串行执行(2026-09-17-feedback-hardening
|
|
593
|
+
// D-003@v1,共享文件安全)→ warning(显式划分 Wave 可获并行收益;同 Wave 内共享仍 error)。
|
|
566
594
|
const waveOfTask = parseTaskWavesFromPlan(pJoin(changeDir, 'plan.md'))
|
|
567
595
|
for (const [, { repo, path: p, owners }] of pathOwners) {
|
|
568
596
|
if (owners.length < 2) continue
|
|
569
597
|
if (waveOfTask === null) {
|
|
570
|
-
|
|
598
|
+
warnings.push(
|
|
571
599
|
`路径 ${p} 被 ${owners.length} 个 task 修改: ${owners.join(', ')} — plan.md 无显式 Wave 划分,` +
|
|
572
|
-
`execute
|
|
573
|
-
|
|
600
|
+
`execute 将按单个隐式 Wave 串行执行(共享文件安全)。` +
|
|
601
|
+
`显式划分 Wave 可获并行收益(同 Wave 内仍禁止共享文件);或合并为单个 task`
|
|
574
602
|
)
|
|
575
603
|
continue
|
|
576
604
|
}
|
|
@@ -785,12 +813,19 @@ export function parseLocalYamlModules(yamlText) {
|
|
|
785
813
|
* modules 从 local.yaml 提取(monorepo 子包感知):无 modules 块时仅查根 package.json
|
|
786
814
|
* (与 scan-postcheck 历史行为一致);有 modules 块时多候选子包 package.json 任一命中即通过。
|
|
787
815
|
*
|
|
816
|
+
* 跨仓卡基准根切换(坑 verify-cmd-cross-repo-root,2026-09-15 wp 会话实证):卡片声明
|
|
817
|
+
* repo: <key> 时其 verify/implementation 里的路径相对**该仓**根——此前恒用主仓 projectRoot,
|
|
818
|
+
* 跨仓卡的 `cd src && npm run lint`(src 在跨仓里)解析到主仓 → 误报死命令。repoRegistry
|
|
819
|
+
* 命中该 key 时以跨仓根为该卡的 projectRoot(modules 是主仓 local.yaml 的相对子包路径,
|
|
820
|
+
* 跨仓卡不适用,置 null);repo 未注册时由检查 1 的注册校验报错,此处维持主仓根不重复报。
|
|
821
|
+
*
|
|
788
822
|
* @param {string} changeDir - 变更目录
|
|
789
823
|
* @param {string} projectRoot - 项目根目录(package.json 查找基准)
|
|
790
824
|
* @param {object|null} modules - local.yaml modules 块({ name: { path } }),可选
|
|
825
|
+
* @param {Map<string,string>|null} [repoRegistry=null] - local.yaml repos: 段(跨仓卡基准根解析)
|
|
791
826
|
* @returns {{ ok: boolean, errors: string[], warnings: string[] }}
|
|
792
827
|
*/
|
|
793
|
-
export function validateTaskCommands(changeDir, projectRoot, modules = null) {
|
|
828
|
+
export function validateTaskCommands(changeDir, projectRoot, modules = null, repoRegistry = null) {
|
|
794
829
|
const errors = []
|
|
795
830
|
const warnings = []
|
|
796
831
|
|
|
@@ -826,7 +861,16 @@ export function validateTaskCommands(changeDir, projectRoot, modules = null) {
|
|
|
826
861
|
const text = `${verifyText}\n${implText}`
|
|
827
862
|
if (!text.trim()) continue
|
|
828
863
|
|
|
829
|
-
|
|
864
|
+
// 跨仓卡基准根切换(坑 verify-cmd-cross-repo-root):repo: 命中注册表 → 该卡命令以跨仓
|
|
865
|
+
// 根解析;modules(主仓相对子包路径)对跨仓卡不适用置 null
|
|
866
|
+
const cardRepo = parseRepo(content)
|
|
867
|
+
let cardRoot = projectRoot
|
|
868
|
+
let cardModules = modules
|
|
869
|
+
if (cardRepo && cardRepo !== 'main' && repoRegistry && repoRegistry instanceof Map && repoRegistry.has(cardRepo)) {
|
|
870
|
+
cardRoot = repoRegistry.get(cardRepo)
|
|
871
|
+
cardModules = null
|
|
872
|
+
}
|
|
873
|
+
const { invalid } = validateScriptCommands(text, { projectRoot: cardRoot, modules: cardModules })
|
|
830
874
|
for (const inv of invalid) {
|
|
831
875
|
errors.push(`${taskId}: ${inv.cmd} 命令不存在(${inv.reason})`)
|
|
832
876
|
}
|
|
@@ -880,7 +924,7 @@ const FILE_LIST_SECTION_RE = /^#{2,3}\s*(?:\d+[.)]\s*)?(文件变更清单|变
|
|
|
880
924
|
* @param {string} designPath - design.md 绝对路径
|
|
881
925
|
* @returns {{ byRepo: Map<string, Set<string>>, hasSegmentHeader: boolean, allFiles: string[], malformedHeaders: string[] }}
|
|
882
926
|
*/
|
|
883
|
-
function parseDesignCoverageByRepo(designPath) {
|
|
927
|
+
export function parseDesignCoverageByRepo(designPath) {
|
|
884
928
|
const byRepo = new Map()
|
|
885
929
|
const allFiles = []
|
|
886
930
|
const malformedHeaders = []
|
|
@@ -1247,6 +1291,15 @@ export function validatePlanFeasibility(changeDir, projectRoot = null) {
|
|
|
1247
1291
|
const fm = fmMatch[1]
|
|
1248
1292
|
const body = content.slice(fmMatch[0].length)
|
|
1249
1293
|
|
|
1294
|
+
// 0. 顶层键重复检测(FR-02 / D-004@v1,坑见 detectDuplicateTopKeys jsdoc:骨架反填
|
|
1295
|
+
// depends_on 后 agent 重复手填 → js-yaml throw 被四处 catch 静默吞字段,只能在入口拦截)。
|
|
1296
|
+
// 先于必要字段检查——重复键会令下游 jsYaml.load 全链路失真,属最高优先级形态错误。
|
|
1297
|
+
const dupTaskId = (fm.match(/^id:\s*(.+)/m)?.[1] || '').trim()
|
|
1298
|
+
const dupKeys = detectDuplicateTopKeys(fm)
|
|
1299
|
+
for (const dup of dupKeys) {
|
|
1300
|
+
errors.push(`${dupTaskId || file}: frontmatter 顶层键 ${dup.key} 重复出现 ${dup.lines.length} 次(L${dup.lines.join('、L')})——骨架已自动反填的键(如 depends_on)勿重复手填,保留正确一处删除其余`)
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1250
1303
|
// 1. 必要字段检查
|
|
1251
1304
|
const taskId = (fm.match(/^id:\s*(.+)/m)?.[1] || '').trim()
|
|
1252
1305
|
const title = (fm.match(/^title:\s*(.+)/m)?.[1] || '').trim()
|
|
@@ -1619,10 +1672,21 @@ export async function executePlanPostcheck(context) {
|
|
|
1619
1672
|
let taskModules = null
|
|
1620
1673
|
let repoRegistry = null
|
|
1621
1674
|
if (existsSync(localYamlPath)) {
|
|
1622
|
-
const localYamlText =
|
|
1675
|
+
const localYamlText = _readFileSync(localYamlPath, 'utf8')
|
|
1623
1676
|
taskModules = parseLocalYamlModules(localYamlText)
|
|
1624
1677
|
repoRegistry = parseRepoRegistry(localYamlText)
|
|
1625
1678
|
}
|
|
1679
|
+
// 坑 register-repo-specbase-split-brain(读侧对齐):平台模式下 specDir=平台 spec 根,此处
|
|
1680
|
+
// 读 spec 根 local.yaml 的 repos: 而 execute MultiRepoContext 恒读 <cwd>/.sillyspec/local.yaml
|
|
1681
|
+
// ——plan gate 看得到注册、execute 启动却 fail-closed(2026-09-15 wp 会话实证)。项目侧
|
|
1682
|
+
// local.yaml 存在时以它为 repos: 权威(register-repo 写侧已对齐项目侧);不存在(纯平台
|
|
1683
|
+
// 项目从未建过项目侧文件)维持 spec 根回退,零回归。
|
|
1684
|
+
const projectLocalYamlPath = pJoin(cwd, '.sillyspec', 'local.yaml')
|
|
1685
|
+
if (existsSync(projectLocalYamlPath)) {
|
|
1686
|
+
try {
|
|
1687
|
+
repoRegistry = parseRepoRegistry(_readFileSync(projectLocalYamlPath, 'utf8'))
|
|
1688
|
+
} catch { /* 项目侧读失败维持 spec 根结果 */ }
|
|
1689
|
+
}
|
|
1626
1690
|
|
|
1627
1691
|
// ── 1. 一致性校验 ──
|
|
1628
1692
|
const consistency = validateBlueprintConsistency(changeDir, { repoRegistry })
|
|
@@ -1656,7 +1720,7 @@ export async function executePlanPostcheck(context) {
|
|
|
1656
1720
|
// invalid → error 硬阻断,避免 execute 子代理跑死命令(design D-04 / 问题 3)。
|
|
1657
1721
|
// modules 块可选:无块时仅查根 package.json(与 scan-postcheck 历史行为一致)。
|
|
1658
1722
|
// local.yaml 已在检查 1 前预读(taskModules),此处直接消费。
|
|
1659
|
-
const taskCmds = validateTaskCommands(changeDir, context.cwd, taskModules)
|
|
1723
|
+
const taskCmds = validateTaskCommands(changeDir, context.cwd, taskModules, repoRegistry)
|
|
1660
1724
|
if (taskCmds.errors.length > 0) {
|
|
1661
1725
|
failures.push({
|
|
1662
1726
|
name: 'TaskCard 命令存在性校验(verify/implementation 的 npm/pnpm/yarn run <script> 不存在)',
|
package/src/stages/plan.js
CHANGED
|
@@ -245,6 +245,10 @@ plan_level: full
|
|
|
245
245
|
## 关键路径
|
|
246
246
|
task-01 → task-03(最长路径,决定最短交付周期)
|
|
247
247
|
|
|
248
|
+
## 全局硬约束(从 design.md 逐字抄录,绑定所有 task)
|
|
249
|
+
- (逐字抄录 design.md 中绑定**每个** task 的硬约束:版本底线 / 依赖限制 / 命名与文案 / 精确值 / 兼容策略 / 非目标边界——执行期子代理只读自己的任务卡 + 本段、不重读 design 全文,抄录失真 = 执行期偏差源头;无跨 task 硬约束时可省略本段)
|
|
250
|
+
- 示例:Node ≥ 20;禁止新增运行时依赖;错误文案一律中文
|
|
251
|
+
|
|
248
252
|
## 全局验收标准
|
|
249
253
|
1. 所有单元测试通过
|
|
250
254
|
2. 集成敏感 task(路由/layout/跨进程装配)建议加集成冒烟验收——组件单测全绿 ≠ 集成正确
|
|
@@ -263,7 +267,7 @@ full 计划的约束:
|
|
|
263
267
|
- **禁止**泛泛风险分析("需要充分测试"类废话转为具体验收条目)
|
|
264
268
|
- Mermaid 依赖关系图**仅当依赖关系非平凡时生成**(线性依赖或全并行时不生成)
|
|
265
269
|
- **Wave 段 ID 引用行必须保留**(\`- task-XX\` 纯 ID、无 checkbox 无任务名——机器解析依赖;任务名唯一真相在 tasks.md,plan.md 重抄即双写漂移)
|
|
266
|
-
- plan.md 包含 Wave 分组 + 任务总表 + 关键路径 + 全局验收标准,**不放实现细节**
|
|
270
|
+
- plan.md 包含 Wave 分组 + 任务总表 + 关键路径 + 全局硬约束(design 有跨 task 硬约束时逐字抄录)+ 全局验收标准,**不放实现细节**
|
|
267
271
|
- 如果存在 decisions.md,plan.md 建议包含当前版本 D-xxx@vN/FR-xxx 覆盖矩阵(CLI 只校验 D-xxx@vN/FR-xxx ID 字面出现在 plan.md,warning 不阻断;矩阵的 D→FR→task 映射完整性供人类追溯,CLI 不校验——勿以为画了矩阵就被结构校验)
|
|
268
272
|
- 如果存在 P0/P1 unresolved blocker,不生成 plan.md,输出阻塞清单
|
|
269
273
|
- 实现细节写到后续的 tasks/task-NN.md 中
|
|
@@ -338,12 +342,15 @@ tier: {REVIEW_TIER}({REVIEW_TIER_REASON})
|
|
|
338
342
|
${REVIEW_CHECKLISTS.plan.map((item) => '- [ ] ' + item).join('\n')}
|
|
339
343
|
|
|
340
344
|
### tier=independent 时:启动 plan-review 子代理
|
|
345
|
+
{PRIOR_REVIEW_FACTS}
|
|
341
346
|
用 Agent tool 启动子代理(subagent_type: general),prompt 要点:
|
|
342
347
|
1. 独立读取 {SPEC_ROOT}/changes/<change>/plan.md + design.md + tasks/*.md(不要让生成者喂结论给你,自己读原始文件)
|
|
343
348
|
2. 执行上方审查清单,每条给 pass/gap/fail + 证据
|
|
344
349
|
3. 输出 review.json(CLI Stage Review Gate 将硬校验,契约如下 —— schema + 完整示例 + docHash 算法,照抄改值):
|
|
345
350
|
{REVIEW_JSON_CONTRACT}
|
|
346
351
|
4. verdict=fail 时在 reviewerNotes 写明阻断项
|
|
352
|
+
5. 执行纪律(坑 review-subagent-stall,2026-09-15 wp EHS 会话实证:plan 审查重核三仓源码 94 分钟不收敛——brainstorm 阶段已两轮实证的结论被全量重验):brainstorm 阶段 design review 已实证的结论(checklist pass 项、file:line 锚点)**直接引用勿重验**,本阶段只审 plan 特有面(任务拆分/依赖/契约/规模映射);必读材料读完即逐条出结论落盘,仅结论存疑时定向补证,禁止循环扩大核验面(连续读文件 10+ 次仍零结论 = 基于已读材料立即收敛)。
|
|
353
|
+
6. 写通道降级:写操作持续被平台拒绝(session not in running turn 类)→ 重试 ≤3 次即停,完整结论(含 review.json 全文)作为最终文本回传主代理代落盘,reviewerNotes 首行留痕「代落盘:子代理写通道故障」;禁止长时间空转重试。
|
|
347
354
|
|
|
348
355
|
### module-impact.md 首版(scale≠small 时:CLI 自动生成,审查步勿手写)
|
|
349
356
|
module-impact.md 首版**由 CLI 在本阶段 --done 时自动生成**——文件×模块归属按 _module-map.yaml 前缀匹配机械预填,章节含「## 模块影响矩阵」「## 未匹配文件」「## 更新结果」表骨架(每受影响模块一行 pending),影响类型列留 <!--TODO--> 由 execute/verify 按实际 diff 回填。已存在不覆盖。手写整份首版是历史返工根源(章节标题变体会被 archive contains_sections 硬拦),**本步不要手写**。
|
package/src/stages/quick.js
CHANGED
|
@@ -114,7 +114,7 @@ sillyspec run quick --done --change <id> --req "…" --cause "…" --solution "
|
|
|
114
114
|
|
|
115
115
|
格式:\`path::括注\` 一条,\`||\` 分隔多条(括注可省略只留 \`path\`)。不传则「文件:」行回填审计到的实际改动文件单行(仍可事后手改)。
|
|
116
116
|
|
|
117
|
-
> ⚠️ --file-notes 只随 step3 --done 同一命令传才生效。CLI 是短进程,run 与 done 是独立进程——step1/step2 的 sillyspec run quick
|
|
117
|
+
> ⚠️ --file-notes 只随 step3 --done 同一命令传才生效。CLI 是短进程,run 与 done 是独立进程——step1/step2 的 sillyspec run quick(仅拿提示词)带 --file-notes 会被直接拒绝执行(去掉该参数重跑本条命令即可,进度不丢)。请在 step3 收尾时连同四字段一起传。
|
|
118
118
|
|
|
119
119
|
### 收尾推荐顺序(模块文档在 --done 前,QUICKLOG 在 --done 后,别记混)
|
|
120
120
|
1. 【--done 前】命中模块 → 改模块文档 → \`git add -- <模块文档>\`(见下方「模块文档同步」)
|
|
@@ -153,13 +153,13 @@ sillyspec run quick --done --change <id> --req "…" --cause "…" --solution "
|
|
|
153
153
|
CLI 落盘的条目已是结构化产物(非简版骨架):标题从「需求:」自动提取、正文四字段自动分行、文件行用 --file-notes 时为多行括注。--done 后只需核对,多数情况无需手改:
|
|
154
154
|
|
|
155
155
|
1. 标题:CLI 已从「需求:」提取(截到首个标点、超 80 字截断);按 step3 模板写成一句语义化短标题则无需手改,写成长句被截断时才手改。
|
|
156
|
-
2. 文件:用了 --file-notes
|
|
156
|
+
2. 文件:用了 --file-notes 已是多行括注;没传则回填审计到的实际文件单行,可事后补括注(格式见下)。
|
|
157
157
|
3. 正文:四字段已分行落盘,按需扩写充实(不允许只留一段「结果:」)。
|
|
158
158
|
|
|
159
|
-
|
|
159
|
+
补括注格式(**虚构占位示例,非任何会话的真实条目**——多会话并行时勿把下面两行误认成本 quick 的改动文件,核对对象永远是 QUICKLOG 中本会话 <quicklog-id> 条目的「文件:」行;仅当没用 --file-notes、想事后补时参照此格式):
|
|
160
160
|
文件:
|
|
161
|
-
-
|
|
162
|
-
-
|
|
161
|
+
- <path/to/改动文件>(一句话括注:该文件改了什么,可含串入的调用链)
|
|
162
|
+
- <path/to/新建文件>(新建:职责一句话)
|
|
163
163
|
|
|
164
164
|
一条 quick = 一条独立 ql 条目;核对只动本次 <quicklog-id> 条目,不追加到旧条目。QUICKLOG 在 .sillyspec/quicklog/(git 跟踪,--done 后需提交),核对不影响 --done 已通过的边界审计。`,
|
|
165
165
|
outputHint: '暂存和记录确认',
|
package/src/stages/verify.js
CHANGED
|
@@ -160,7 +160,7 @@ export const definition = {
|
|
|
160
160
|
2. 如果存在:
|
|
161
161
|
- 逐个读取 tasks/task-NN.md,对照 frontmatter 的 \`acceptance:\` 列表逐条核验(TaskCard 协议的验收标准在 frontmatter YAML,正文无 checkbox)
|
|
162
162
|
- 每条 acceptance 对照实际实现/测试结果判定满足与否,未满足的项列为不通过
|
|
163
|
-
- 覆盖对账走探针 7 矩阵:verify-result.md「探针 7:验收×测试覆盖矩阵」段已由 CLI 预填归属测试文件与关键词提示(骨架未生成时先跑 \`sillyspec verify-probes --change <change-name> --init\`,幂等不覆盖已有正文);逐行填判定(covered/partial/uncovered/non-testable 四选一)与证据——covered/partial
|
|
163
|
+
- 覆盖对账走探针 7 矩阵:verify-result.md「探针 7:验收×测试覆盖矩阵」段已由 CLI 预填归属测试文件与关键词提示(骨架未生成时先跑 \`sillyspec verify-probes --change <change-name> --init\`,幂等不覆盖已有正文);逐行填判定(covered/partial/uncovered/non-testable 四选一)与证据——covered/partial 附测试锚点(file:line / \`.test.\` 测试文件名 / 反引号包裹的路径或测试名,行号可省),non-testable 写一句理由;判定/证据未填会被 verify \`--done\` 门禁阻断(fail-closed)
|
|
164
164
|
3. 如果不存在:跳过此步骤
|
|
165
165
|
|
|
166
166
|
### 输出
|
|
@@ -200,13 +200,14 @@ export const definition = {
|
|
|
200
200
|
4. **预填探针段不可篡改或删除**:verify \`--done\` gate 会重跑探针对比 verify-result.md 正文,不符即拦(ERROR);对探针结果有异议只能在预填段旁追加说明,不得改写原文
|
|
201
201
|
5. **verify-facts.json 为机器底稿,勿手改**:防篡改对比的基准是正文,改底稿无意义
|
|
202
202
|
6. 给出结论:PASS / PASS WITH NOTES / FAIL(受风险门控约束)——**结论只认骨架「结论枚举:」槽行(行首锚定,把 <待填:三选一> 整体替换为枚举值);槽留待填会被 gate 判不过,正文其他位置的 PASS/FAIL 字样不参与判定**
|
|
203
|
+
- **结论想写 PASS 前自查四事实条件(PASS 封顶语义,--done 门禁按 verify-facts.json 实拦)**:①集成实测未跑 ②「## 移交项(结构化)」含 blocking 级行 ③db/**/*.sql 未声明执行(verify-result.md 无「已对目标库执行」声明、回执 command 亦不含该文件)④验收×测试覆盖矩阵含 partial/uncovered 行且移交项零有效行——任一成立即不得写 PASS:改写 PASS WITH NOTES 并在「## 移交项(结构化)」表格如实分行(类型枚举 env-blocked/manual-acceptance/db-script/other)。severity 口径:db-script/env-blocked 恒 blocking;manual-acceptance/other 默认 advisory、须显式标 blocking 才计入封顶;确需把 blocking 降为 advisory 必须附「(降级:<理由>,依据 <file:line 或 D-xxx>)」
|
|
203
204
|
7. **核对 module-impact.md**(若 \`{SPEC_ROOT}/changes/<change>/module-impact.md\` 存在):对照本次实际代码变更(git diff)与 module-impact.md 的模块影响矩阵,发现不一致(漏标受影响模块 / 影响类型错误 / 实际未触碰的模块被误标)则在 verify-result.md 标注。module-impact 由 plan 首版生成、execute 各 Wave 更新,verify 是最后一次核对机会(archive 仅终审不再生成)。这是 advisory 核对(不阻断 verify 完成),但 module-impact 与实际严重背离应记为风险。
|
|
204
205
|
|
|
205
206
|
### verify-result.md 章节结构(骨架已含,占位替换即可)
|
|
206
207
|
|
|
207
208
|
结论(PASS / PASS WITH NOTES / FAIL)→ 证据账(cannot_verify 任务,槽段)→ 集成验证回执(槽段)→ 任务完成度 → 设计一致性 → 探针结果(已预填)→ 测试结果 → 决策追踪矩阵(\`| 决策 ID | FR | Task | Evidence | 状态 |\`,存在 decisions.md 才留)→ 技术债务 → 变更风险等级 → Runtime Evidence → 代码审查。
|
|
208
209
|
|
|
209
|
-
**「## 集成验证回执」槽行结构**(integration/deployment-critical
|
|
210
|
+
**「## 集成验证回执」槽行结构**(integration/deployment-critical 变更必填;其余写「无」)双形态二选一(多行 YAML 形态推荐,字段序无关):\n- claim: <一句话>\n command: <命令>\n exit: <0 或非 0>\n log: <日志路径>\n亦认单行管道形态:\`- claim: <一句话> | command: <命令> | exit: <0 或非 0> | log: <日志路径>\`——CLI 一致性校验四条件:log 存在 × mtime 在 verify 窗口内 × 日志尾无失败签名(error/exception/traceback/fatal 行首,剔除「0 errors」类良性行)× exit 0;全绿才算在场证据,字面措辞不再参与判定(v2 起 literals 仅存量回退)。CLI 不代跑集成进程——回执必须来自你真实执行过的命令。
|
|
210
211
|
|
|
211
212
|
**Runtime Evidence 行结构**(integration/deployment-critical 必填;按实际触碰的运行时组件写,未涉及的行写「不涉及」勿堆关键词):长驻进程启动命令 / 触碰的服务端点 / 触发核心路径的请求(附关键响应)/ 进程日志关键片段(证明走了新路径)/ 生命周期终态断言(初始态→运行态→终态)/ 失败模式排除(逐条说明为何未触发)。
|
|
212
213
|
|
package/src/sync-noise.js
CHANGED
|
@@ -243,6 +243,60 @@ export function noteChangeDeletedResolved(changeName) {
|
|
|
243
243
|
}
|
|
244
244
|
}
|
|
245
245
|
|
|
246
|
+
// ── push 409 自愈回执噪音闸(2026-09-15 复盘反馈:自愈重试不阻断但每步刷屏)──
|
|
247
|
+
// 与 change_deleted 闸同构,按 `${changeName}\u0000${kind}` 键控(kind=自竞态/内容一致/自回声
|
|
248
|
+
// 三类自愈)。语义边界:自愈是成功收敛路径不是故障——base_ts 409 每步自动同步都可能重撞、
|
|
249
|
+
// ts 逐次不同但零新信息;首报完整可见(保留原文 ⚠️ 口吻),10 分钟窗口内同变更同类型重复
|
|
250
|
+
// 自愈静默,窗口过期重报保活。推送成功不自愈清窗(下次 409 首报照常可见——竞态是否复发
|
|
251
|
+
// 本身是信号);SILLYSPEC_DEBUG_SYNC=1 与手动 platform sync(noMute)旁路。
|
|
252
|
+
const SELF_HEAL_MARKER = 'sync-noise-self-heal.json'
|
|
253
|
+
let _openedSelfHealByThisProcess = new Set()
|
|
254
|
+
const _seenSelfHealInProcess = new Set()
|
|
255
|
+
|
|
256
|
+
function _readSelfHealWindows() {
|
|
257
|
+
const p = _runtimeDir ? join(_runtimeDir, SELF_HEAL_MARKER) : null
|
|
258
|
+
if (!p) return null
|
|
259
|
+
try {
|
|
260
|
+
const s = JSON.parse(readFileSync(p, 'utf8'))
|
|
261
|
+
return s && typeof s === 'object' && !Array.isArray(s) ? s : null
|
|
262
|
+
} catch { return null }
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function _selfHealLineVisible(changeName, kind, noMute) {
|
|
266
|
+
if (!changeName || noMute || process.env.SILLYSPEC_DEBUG_SYNC) return true
|
|
267
|
+
const key = changeName + '\u0000' + kind
|
|
268
|
+
if (_seenSelfHealInProcess.has(key)) return false
|
|
269
|
+
const now = Date.now()
|
|
270
|
+
const windows = _readSelfHealWindows()
|
|
271
|
+
const until = windows ? windows[key] : undefined
|
|
272
|
+
if (typeof until === 'number' && now < until) {
|
|
273
|
+
_seenSelfHealInProcess.add(key)
|
|
274
|
+
// 开窗进程内同类自愈可见(诊断完整性),先前进程留的窗口静默
|
|
275
|
+
return _openedSelfHealByThisProcess.has(key)
|
|
276
|
+
}
|
|
277
|
+
_seenSelfHealInProcess.add(key)
|
|
278
|
+
const next = windows || {}
|
|
279
|
+
next[key] = now + MUTE_WINDOW_MS
|
|
280
|
+
try {
|
|
281
|
+
mkdirSync(_runtimeDir, { recursive: true })
|
|
282
|
+
writeFileSync(join(_runtimeDir, SELF_HEAL_MARKER), JSON.stringify(next) + '\n', 'utf8')
|
|
283
|
+
} catch { /* marker 写失败 = 闸门失效退化为直通,仅损失降噪 */ }
|
|
284
|
+
_openedSelfHealByThisProcess.add(key)
|
|
285
|
+
return true
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* push 409 自愈回执(warn 口吻:首报保留原文 ⚠️,窗口内同变更同类型静默)。
|
|
290
|
+
* @returns {boolean} true=已输出
|
|
291
|
+
*/
|
|
292
|
+
export function syncSelfHealWarn(changeName, kind, msg, { noMute = false } = {}) {
|
|
293
|
+
if (_selfHealLineVisible(changeName, kind, noMute)) {
|
|
294
|
+
console.warn(msg)
|
|
295
|
+
return true
|
|
296
|
+
}
|
|
297
|
+
return false
|
|
298
|
+
}
|
|
299
|
+
|
|
246
300
|
/**
|
|
247
301
|
* 任意一次同步成功:清 marker。此前处于闸内(marker 存在)时打一行恢复提示——
|
|
248
302
|
* 平台恢复的第一个信号应该可见,之后的成功照常静默(成功不打扰)。
|
|
@@ -256,11 +310,13 @@ export function noteSyncSuccess() {
|
|
|
256
310
|
console.log('[sync] 平台连接已恢复(此前静默的同步失败已停止)')
|
|
257
311
|
}
|
|
258
312
|
|
|
259
|
-
/** 测试复位:清进程内绑定与开窗状态(连接类闸 + change_deleted
|
|
313
|
+
/** 测试复位:清进程内绑定与开窗状态(连接类闸 + change_deleted 变更级闸 + 409 自愈闸)。 */
|
|
260
314
|
export function _resetSyncNoiseForTest() {
|
|
261
315
|
_runtimeDir = null
|
|
262
316
|
_openedByThisProcess = false
|
|
263
317
|
_seenInProcess.clear()
|
|
264
318
|
_openedDeletedByThisProcess = new Set()
|
|
265
319
|
_seenDeletedInProcess.clear()
|
|
320
|
+
_openedSelfHealByThisProcess = new Set()
|
|
321
|
+
_seenSelfHealInProcess.clear()
|
|
266
322
|
}
|