sillyspec 3.28.10 → 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-risk-profile.js +51 -3
- package/src/config-schema.js +21 -0
- package/src/design-facts.js +46 -9
- 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 +129 -1
- package/src/machine-interface.js +26 -0
- package/src/probe7-anchor-check.js +20 -2
- package/src/progress/stage-machine.js +34 -5
- package/src/progress.js +1 -1
- package/src/quicklog.js +231 -1
- package/src/run/complete-handlers.js +172 -1
- package/src/run/complete.js +73 -3
- package/src/run/gate-snapshot.js +262 -0
- package/src/run/gates.js +41 -7
- package/src/run/prompt.js +79 -1
- package/src/run/shared.js +10 -0
- 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/execute.js +62 -8
- package/src/stages/plan-postcheck.js +42 -5
- package/src/stages/plan.js +8 -1
- package/src/stages/quick.js +5 -5
- package/src/stages/verify.js +3 -2
- package/src/sync.js +69 -0
- package/src/task-review.js +9 -3
- package/src/verify-facts-schema.js +78 -9
- package/src/verify-postcheck.js +260 -26
- package/src/verify-probes.js +780 -44
- package/src/worktree-apply.js +175 -3
- package/src/write-audit.js +55 -0
- package/.claude/full.json +0 -1
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* 条目为纯文本,不含序号/checkbox/缩进等结构性前缀(渲染层负责前缀形态):
|
|
10
10
|
* - brainstorm:前 3 条 =「交叉审查模型」三层检查(prompt 内渲染为「N. 」序号行),
|
|
11
|
-
* 后
|
|
11
|
+
* 后 8 条 =「交叉点抽取」(渲染为「- 」列表行);
|
|
12
12
|
* - plan:渲染为「- [ ] 」checkbox 行(审查清单段);
|
|
13
13
|
* - execute:渲染为 2 空格缩进的「N. 」序号行(QA 段「以下三项始终必查」)。
|
|
14
14
|
*/
|
|
@@ -23,6 +23,8 @@ export const REVIEW_CHECKLISTS = {
|
|
|
23
23
|
'tasks.md 的执行范围与 design.md 的非目标',
|
|
24
24
|
'decisions.md 的 D-xxx@vN 与 design.md 当前说法',
|
|
25
25
|
'scan/module docs 或源码中的真实约束与 design.md 假设',
|
|
26
|
+
'涉及角色/权限/字典类数据时,实证不能只看静态定义:须以生产查询口径可解析到目标结果——用真实租户/库下可执行的查询(如按 enname+tenant 查询)确认返回非空,仅 enname 存在不算通过',
|
|
27
|
+
'涉及新页面/前端路由时,用户入口 × 菜单/注册 DML 对账:入口路由与菜单表、角色-权限注册 DML 逐一对账——能点到的入口必有对应菜单与授权注册行,菜单/注册 SQL 必须出现在交付清单(design.md 文件变更清单或 db/ 脚本)',
|
|
26
28
|
],
|
|
27
29
|
plan: [
|
|
28
30
|
'task 编号与 Wave checkbox 格式正确,execute 依赖此格式解析任务',
|
package/src/stage-review.js
CHANGED
|
@@ -422,6 +422,111 @@ export function getLatestStageReviewRunId(runtimeRoot, stage, changeName) {
|
|
|
422
422
|
}
|
|
423
423
|
}
|
|
424
424
|
|
|
425
|
+
/**
|
|
426
|
+
* 同阶段上一轮审查结论采集(ql-20260916-021,Superpowers scoped re-review 采纳①)。
|
|
427
|
+
*
|
|
428
|
+
* 复审派发 prompt 原本只有跨阶段 pass 结论注入(prompt.js {PRIOR_REVIEW_FACTS} 的前序阶段段),
|
|
429
|
+
* 同 stage 上一轮 FAIL 的 findings 与已实证 pass 面不回灌——独立复审子代理全量重读并重复报告
|
|
430
|
+
* 已修问题。本函数机械提取「该 stage 本变更最近一轮有效 review」的两份清单供回灌:
|
|
431
|
+
* - openFindings:checklist result=fail/gap 的未决项(verdict=fail 但 checklist 无明细时以
|
|
432
|
+
* reviewerNotes 合成一条,防 fail 轮零线索);
|
|
433
|
+
* - passItems:checklist result=pass 的已实证面(复审勿重复报告/重验,除非修复改动触及该面)。
|
|
434
|
+
*
|
|
435
|
+
* 采集规则:
|
|
436
|
+
* - runId 字典序 = 时间序(generateStageReviewRunId 时间戳格式),取最新有效轮——含 marker
|
|
437
|
+
* 当前指向 run 的已写 review(同目录覆盖式复审路径下它就是最新轮,无需调用方区分);
|
|
438
|
+
* - 跨变更过滤:reviewedFiles[0] 不含 `changes/<changeName>/` 的轮不采(与
|
|
439
|
+
* getLatestStageReviewRunId 的 cross-change 防护同口径);
|
|
440
|
+
* - 骨架轮跳过(register-stage-review 生成的双 cannot_verify 待审骨架);
|
|
441
|
+
* - 坏 JSON 轮跳过不抛(best-effort);两者皆空(无未决也无 pass 面)→ null。
|
|
442
|
+
*
|
|
443
|
+
* @param {string} runtimeRoot - .runtime 绝对路径
|
|
444
|
+
* @param {string} stage - brainstorm|plan|execute
|
|
445
|
+
* @param {string} changeName - 变更名(跨变更过滤锚)
|
|
446
|
+
* @returns {{ priorRunId: string, verdicts: string, openFindings: string[], passItems: string[], notesPreview: string }|null}
|
|
447
|
+
*/
|
|
448
|
+
export function collectSameStagePriorReview(runtimeRoot, stage, changeName) {
|
|
449
|
+
if (!runtimeRoot || !stage || !changeName) return null
|
|
450
|
+
const dir = join(runtimeRoot, 'stage-reviews')
|
|
451
|
+
if (!existsSync(dir)) return null
|
|
452
|
+
let entries
|
|
453
|
+
try {
|
|
454
|
+
entries = readdirSync(dir, { withFileTypes: true })
|
|
455
|
+
.filter(e => e.isDirectory() && e.name.startsWith(`${stage}-review-`))
|
|
456
|
+
.map(e => e.name)
|
|
457
|
+
} catch { return null }
|
|
458
|
+
if (entries.length === 0) return null
|
|
459
|
+
|
|
460
|
+
const candidates = []
|
|
461
|
+
for (const name of entries) {
|
|
462
|
+
const rp = join(dir, name, 'review.json')
|
|
463
|
+
if (!existsSync(rp)) continue
|
|
464
|
+
try {
|
|
465
|
+
const review = JSON.parse(readFileSync(rp, 'utf8'))
|
|
466
|
+
const r0 = Array.isArray(review.reviewedFiles) ? String(review.reviewedFiles[0]) : ''
|
|
467
|
+
if (!r0.includes(`changes/${changeName}/`)) continue
|
|
468
|
+
// 骨架占位跳过(register-stage-review 待审骨架:双 cannot_verify + 骨架 notes 标记)
|
|
469
|
+
const isSkeleton = review.specVerdict === 'cannot_verify' && review.qualityVerdict === 'cannot_verify'
|
|
470
|
+
&& String(review.reviewerNotes || '').includes('骨架由 register-stage-review')
|
|
471
|
+
if (isSkeleton) continue
|
|
472
|
+
candidates.push({ runId: name.slice(stage.length + 1), review })
|
|
473
|
+
} catch { continue }
|
|
474
|
+
}
|
|
475
|
+
if (candidates.length === 0) return null
|
|
476
|
+
|
|
477
|
+
candidates.sort((a, b) => a.runId.localeCompare(b.runId))
|
|
478
|
+
const latest = candidates[candidates.length - 1]
|
|
479
|
+
const openFindings = []
|
|
480
|
+
const passItems = []
|
|
481
|
+
for (const c of (Array.isArray(latest.review.checklist) ? latest.review.checklist : [])) {
|
|
482
|
+
if (!c || typeof c !== 'object' || !c.item) continue
|
|
483
|
+
const text = String(c.item).replace(/\s+/g, ' ').slice(0, 120)
|
|
484
|
+
if (c.result === 'fail' || c.result === 'gap') openFindings.push(`[${c.result}] ${text}`)
|
|
485
|
+
else if (c.result === 'pass') passItems.push(text)
|
|
486
|
+
}
|
|
487
|
+
// verdict=fail 但 checklist 无明细:fail 结论不丢线索——notes 合成一条(复审者据此核验)
|
|
488
|
+
if (openFindings.length === 0 && (latest.review.specVerdict === 'fail' || latest.review.qualityVerdict === 'fail')) {
|
|
489
|
+
const notes = String(latest.review.reviewerNotes || '').replace(/\s+/g, ' ').slice(0, 160)
|
|
490
|
+
openFindings.push(`[verdict-fail] verdict=fail 但 checklist 无明细——以 reviewerNotes 为准:${notes || '(无 notes)'}`)
|
|
491
|
+
}
|
|
492
|
+
if (openFindings.length === 0 && passItems.length === 0) return null
|
|
493
|
+
return {
|
|
494
|
+
priorRunId: latest.runId,
|
|
495
|
+
verdicts: `spec=${latest.review.specVerdict}, quality=${latest.review.qualityVerdict}`,
|
|
496
|
+
openFindings,
|
|
497
|
+
passItems,
|
|
498
|
+
notesPreview: String(latest.review.reviewerNotes || '').replace(/\s+/g, ' ').slice(0, 160),
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* renderPriorRoundFindingsMd——把 collectSameStagePriorReview 产物渲染为复审派发 prompt 的
|
|
504
|
+
* 回灌块(prompt.js 注入 {PRIOR_REVIEW_FACTS},与前序阶段 pass 结论段同串拼接)。
|
|
505
|
+
*
|
|
506
|
+
* 语义双向:未决项**必须**逐项核验(已修给证据、未解决如实再次 fail——漏放行=假通过);
|
|
507
|
+
* pass 面**勿重复报告/重验**(除非修复改动明确触及该面)——一堵一疏,复审以增量为主。
|
|
508
|
+
*
|
|
509
|
+
* @param {{ priorRunId: string, verdicts: string, openFindings: string[], passItems: string[] }|null} collected
|
|
510
|
+
* @param {number} [cap=15] 每段展示封顶(防 prompt 灌爆,对齐前序实证段的 15 条惯例)
|
|
511
|
+
* @returns {string} markdown 块;collected 空 → 空串
|
|
512
|
+
*/
|
|
513
|
+
export function renderPriorRoundFindingsMd(collected, cap = 15) {
|
|
514
|
+
if (!collected) return ''
|
|
515
|
+
const L = []
|
|
516
|
+
L.push(`**同阶段上一轮审查结论(run ${collected.priorRunId},verdict ${collected.verdicts})——本次为复审,以增量为主,不重演全量审查**:`)
|
|
517
|
+
if (collected.openFindings.length > 0) {
|
|
518
|
+
const shown = collected.openFindings.slice(0, cap)
|
|
519
|
+
L.push(`- **上一轮未决问题(逐项核验修复状态:已修给证据、未解决必须如实再次 fail——漏放行=假通过)**(${shown.length}/${collected.openFindings.length} 条):`)
|
|
520
|
+
for (const f of shown) L.push(` - ${f}`)
|
|
521
|
+
}
|
|
522
|
+
if (collected.passItems.length > 0) {
|
|
523
|
+
const shown = collected.passItems.slice(0, cap)
|
|
524
|
+
L.push(`- **上一轮已实证 pass 面(勿重复报告/重验——除非本次修复改动明确触及该面才定向复查)**(${shown.length}/${collected.passItems.length} 条):`)
|
|
525
|
+
for (const p of shown) L.push(` - ${p}`)
|
|
526
|
+
}
|
|
527
|
+
return `\n${L.join('\n')}\n`
|
|
528
|
+
}
|
|
529
|
+
|
|
425
530
|
/**
|
|
426
531
|
* 降级自审检测(2026-09-10 用户反馈①:PI agent 等宿主环境无 Agent tool,tier=independent
|
|
427
532
|
* 硬要求子代理时只能降级自审——stage prompts 降级条款约定 reviewerNotes **首行**记
|
|
@@ -437,6 +542,19 @@ export function isDegradedSelfReview(review) {
|
|
|
437
542
|
return /^降级[::]/.test(firstLine)
|
|
438
543
|
}
|
|
439
544
|
|
|
545
|
+
/**
|
|
546
|
+
* 代落盘披露识别(C4,坑 delegated-write-audit-blind,2026-09-15 wp EHS 会话实证:平台写通道
|
|
547
|
+
* 故障下主流程 4 次代子代理落盘 review.json/交付物——独立性全靠 agent 自觉 + notes 留痕,gate
|
|
548
|
+
* 无任何可见标记。审查派发 prompt 已要求代落盘时 reviewerNotes 首行记「代落盘:…」,本识别让
|
|
549
|
+
* gate 侧补 ⚠️ 审计行(与 isDegradedSelfReview 同哲学:可见的折损优于静默的伪装——首行约定
|
|
550
|
+
* 识别,不承诺全文语义判定)。
|
|
551
|
+
*/
|
|
552
|
+
export function hasDelegatedWriteDisclosure(review) {
|
|
553
|
+
const notes = review && typeof review.reviewerNotes === 'string' ? review.reviewerNotes : ''
|
|
554
|
+
const firstLine = notes.split(/\r?\n/, 1)[0].trim()
|
|
555
|
+
return /^代落盘[::]/.test(firstLine)
|
|
556
|
+
}
|
|
557
|
+
|
|
440
558
|
/**
|
|
441
559
|
* stage-level review 总校验(brainstorm/plan/execute-acceptance 的 done gate)
|
|
442
560
|
*
|
|
@@ -572,10 +690,20 @@ export function validateStageReviewWithAutoRefresh(opts) {
|
|
|
572
690
|
if (!docHash) return { result, autoRefreshed: false }
|
|
573
691
|
|
|
574
692
|
const refreshedAt = new Date().toISOString().slice(0, 19).replace('T', ' ')
|
|
693
|
+
// C3 声明化(坑 dochash-autorefresh-silent-drift,2026-09-15 wp EHS 会话实证:审查通过后
|
|
694
|
+
// 主文档多次改版——NEW: 前缀批注、需求调整改名、Reverse Sync——gate 每次都静默机械重算放行,
|
|
695
|
+
// verdict 原样保留,「审查后的变更不再见审查」零痕迹可读。旧内容不可复原(只有 hash),改用
|
|
696
|
+
// 可实现且更贴风险的两个信号做声明:① hash 漂移(旧→新短前缀)② 刷新序数——同一 review 被
|
|
697
|
+
// 反复刷新 = 主文档在审查后反复变更,序数 ≥2 时 gate 侧升级提醒人工确认结论续用。
|
|
698
|
+
const priorNotes = String(result.review.reviewerNotes || '')
|
|
699
|
+
const refreshOrdinal = (priorNotes.match(/docHash auto-refreshed at /g) || []).length + 1
|
|
700
|
+
const oldHashShort = String(result.review.docHash || '').slice(0, 8)
|
|
701
|
+
const newHashShort = String(docHash).slice(0, 8)
|
|
702
|
+
const auditLine = `docHash auto-refreshed at ${refreshedAt}(第 ${refreshOrdinal} 次;hash ${oldHashShort}→${newHashShort};gate 检测主文档改版,机械重算;verdict 保留,结论是否仍适用于新文档需人工确认${refreshOrdinal >= 2 ? '——已多次刷新:主文档在审查后反复变更,建议人工复核结论续用或重审' : ''})`
|
|
575
703
|
const refreshed = {
|
|
576
704
|
...result.review,
|
|
577
705
|
docHash,
|
|
578
|
-
reviewerNotes: `${
|
|
706
|
+
reviewerNotes: `${priorNotes}\n${auditLine}`.trim(),
|
|
579
707
|
}
|
|
580
708
|
try {
|
|
581
709
|
writeAtomicSync(reviewPath, JSON.stringify(refreshed, null, 2) + '\n')
|
|
@@ -583,7 +711,7 @@ export function validateStageReviewWithAutoRefresh(opts) {
|
|
|
583
711
|
return { result, autoRefreshed: false } // 写失败回落原失败结果,不静默放行
|
|
584
712
|
}
|
|
585
713
|
const recheck = validateStageReview(opts)
|
|
586
|
-
return { result: recheck, autoRefreshed: recheck.ok, refreshedPath: reviewPath, previousErrors: result.errors }
|
|
714
|
+
return { result: recheck, autoRefreshed: recheck.ok, refreshedPath: reviewPath, previousErrors: result.errors, refreshOrdinal, hashDrift: `${oldHashShort}→${newHashShort}` }
|
|
587
715
|
}
|
|
588
716
|
|
|
589
717
|
/**
|
|
@@ -708,6 +836,13 @@ export function registerStageReview({ changeName, stage, fromFile, cwd, platform
|
|
|
708
836
|
} catch (e) {
|
|
709
837
|
throw new Error(`register-stage-review: 既有 review.json 解析失败 ${existingPath}: ${e.message}`)
|
|
710
838
|
}
|
|
839
|
+
// 幂等跳过(2026-09-17 用户反馈⑥-②,ql-20260917-005):docHash 已一致时 refresh 仍整文件
|
|
840
|
+
// 重写 + 追加审计行——会话内多次 refresh 每次都 bump mtime/改内容,agent 的 Read→Edit 窗口
|
|
841
|
+
// 被「file modified since read」打断(本会话两次实证)。hash 未变 = 无可刷新内容,直接
|
|
842
|
+
// no-op 返回(不写盘不 bump mtime);行为差异只在「少一次无意义重写」。
|
|
843
|
+
if (existing.docHash === docHash && JSON.stringify(existing.reviewedFiles) === JSON.stringify(reviewedFiles)) {
|
|
844
|
+
return { ok: true, reviewRunId: existingRunId, reviewPath: existingPath, markerPath: stageReviewMarkerPath(runtimeRoot, stage, changeName), mode: 'noop-unchanged', mainDoc, review: existing }
|
|
845
|
+
}
|
|
711
846
|
const refreshedAt = new Date().toISOString().slice(0, 19).replace('T', ' ')
|
|
712
847
|
const review = {
|
|
713
848
|
...existing,
|
package/src/stages/archive.js
CHANGED
|
@@ -75,13 +75,17 @@ export const definition = {
|
|
|
75
75
|
|
|
76
76
|
{SCOPE_AUDIT_TABLE}
|
|
77
77
|
|
|
78
|
-
3.
|
|
78
|
+
3. 展示移交项清单(verify 阶段 facts.handover——CLI 机械注入如下,勿手改勿手算;blocking 置顶 + severity 标注,超封顶截断;确认前逐项核对 blocking 移交项已有处置安排(db-script 类须先在 verify-result.md 补执行声明——归档门会拦未声明/互斥态,env-blocked 确认复跑条件已登记,manual-acceptance 确认验收安排),advisory 项记录备查;无移交项时本区为空,注入异常时置换单行降级指引,全量见 changes/<变更名>/verify-facts.json):
|
|
79
|
+
|
|
80
|
+
{HANDOVER_SUMMARY}
|
|
81
|
+
|
|
82
|
+
4. 确保任务清单(tasks.md)所有 task checkbox 都已勾选;对照下方完成度快照做归档前最后注视(未完成/存疑 → 停下勿确认):
|
|
79
83
|
{TASK_COMPLETION_REPORT}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
84
|
+
5. 若上一步机械决策提炼输出「⚠️ rejected 决策缺否决理由/复潮条件(needsWait)」:先按其指引处理——补录 decisions.md 缺失字段后重跑提炼命令(幂等),或经用户裁决跳过该条;处理结果写进本步 --output
|
|
85
|
+
6. **git 暂存已由 CLI 自动完成**(归档完成时自动 add:\`.sillyspec/changes/archive/\`、本次同步的模块文档、\`.sillyspec/knowledge/decisions/\`——未提交,由用户通过统一提交工具处理)——你不需要也不应该手动 git add
|
|
86
|
+
7. 让用户确认后,用 \`--confirm\` 完成本步骤:
|
|
83
87
|
\`sillyspec run archive --done --confirm --output "确认归档"\`
|
|
84
|
-
|
|
88
|
+
8. CLI 会创建 \`.sillyspec/changes/archive/\`,并将变更目录移动到 \`.sillyspec/changes/archive/<原变更名>/\`
|
|
85
89
|
|
|
86
90
|
### 输出
|
|
87
91
|
归档完成 + archive 目录路径`,
|
package/src/stages/brainstorm.js
CHANGED
|
@@ -286,10 +286,11 @@ design.md 第一行标题必须用中文:# 设计文档(Design)— <变更
|
|
|
286
286
|
|
|
287
287
|
| 操作 | 文件路径 | 说明 |
|
|
288
288
|
|---|---|---|
|
|
289
|
-
| 新增 | src/xxx/NewFile.java | ... |
|
|
289
|
+
| 新增 | NEW:src/xxx/NewFile.java | ... |
|
|
290
290
|
| 修改 | src/xxx/ExistingFile.java | 新增 xx 方法 |
|
|
291
291
|
| 删除 | src/xxx/OldFile.java | 已被 xx 替代 |
|
|
292
292
|
|
|
293
|
+
**路径存在性核验(NEW: 前缀铁律,2026-09-17 用户反馈⑥-①前移)**:清单条目在「生成规范文件」步 \`--done\` 门禁会逐一核验仓内存在性——**计划新建的文件必须在路径前加 \`NEW:\` 前缀**(冒号后不加空格,\`NEW:src/foo.js\` 合法、\`NEW: src/foo.js\` 属书写错误);未加前缀的新建路径会以 \`design_file_ref_invalid\`(幻觉路径/书写错误)在末步被拦,返工一轮。已有文件写仓根相对正斜杠路径,禁绝对路径/glob。
|
|
293
294
|
**字段数据流标注**(避免新增字段到 execute 才发现没透传):当清单含「新增/修改对外字段、接口、DTO、响应体、事件 payload、配置键」时,对应行「说明」列必须交代 producer→consumer 数据流——产出方(producer)→ 每跳流转/归一化点(序列化/反序列化/字段映射/默认值兜底)→ 消费方(consumer)。漏标 = 字段在某跳 dormant(声明了却没透传),到 execute/verify 才暴露,属设计层缺口。
|
|
294
295
|
- 示例:「修改 daemon.ts:新增 budget_tokens,producer=backend/api-types.ts → ws_hub envelope 透传 → daemon.ts normalizeUsage(snake→camel)→ consumer=runtime/lease.ts 写 lease」
|
|
295
296
|
- 仅改内部实现、无对外字段变动时,说明列照常写「新增 xx 方法」即可,无需数据流。
|
|
@@ -385,8 +386,12 @@ tier: {REVIEW_TIER}({REVIEW_TIER_REASON})
|
|
|
385
386
|
- tier=self:当前 agent 直接执行下方交叉审查(小变更)
|
|
386
387
|
- tier=independent:必须用 Agent tool 启动一个独立的设计审查子代理(独立上下文,不共享你的分析与倾向),子代理按下方"交叉审查模型"审查 design.md 并输出 review.json。review.json 产物契约(CLI Stage Review Gate 将硬校验,schema + 完整示例 + docHash 算法如下,照抄改值):
|
|
387
388
|
宿主环境无 Agent tool 可用(调用报 Unknown agent / Available agents: none)→ 不卡死:主代理切换为审查者角色自审替代,reviewerNotes 首行记录「降级:环境无子代理可用」,逐条结论附源码锚点(file:line 或 grep/read 证据)补偿独立性。
|
|
389
|
+
{PRIOR_REVIEW_FACTS}
|
|
388
390
|
{REVIEW_JSON_CONTRACT}
|
|
389
391
|
子代理只产出 review + Unresolved Blockers,**是否调用 sillyspec run brainstorm --wait 仍由你(主 agent)根据其 verdict 决定**(子代理不直接操作 CLI 状态机)。
|
|
392
|
+
**子代理执行纪律(坑 review-subagent-stall,2026-09-15 wp EHS 会话实证:独立审查 94 分钟无收敛被用户三催、写通道故障空转 35 分钟)**:
|
|
393
|
+
- 时间盒收敛:审查子代理的调研是收敛动作不是发散——必读材料读完即逐条出结论并落盘 review.json;仅个别结论存疑时定向补证,禁止循环扩大核验面(连续读文件 10+ 次仍零结论 = 立即停止扩展、基于已读材料收敛)。
|
|
394
|
+
- 写通道降级:子代理写操作持续被平台拒绝(session not in running turn 类故障)→ 重试 ≤3 次即停,把完整审查结论(含 review.json 全文)作为最终文本回传主代理,由主代理代为落盘并在 reviewerNotes 首行留痕「代落盘:子代理写通道故障」;禁止长时间空转重试。
|
|
390
395
|
|
|
391
396
|
### 默认行为
|
|
392
397
|
1. 默认必须执行一次交叉审查;不要让用户凭主观判断决定"要不要 Grill"。
|
|
@@ -413,6 +418,7 @@ ${REVIEW_CHECKLISTS.brainstorm.slice(0, 3).map((item, index) => (index + 1) + '.
|
|
|
413
418
|
### 交叉点抽取
|
|
414
419
|
重点找这些交叉点:
|
|
415
420
|
${REVIEW_CHECKLISTS.brainstorm.slice(3).map((item) => '- ' + item).join('\n')}
|
|
421
|
+
后两条为命中条件注入项:变更涉及角色/权限/字典类数据或新页面/前端路由时必查。
|
|
416
422
|
|
|
417
423
|
### 问答处理
|
|
418
424
|
1. 先自动交叉审查,不要一上来问用户。
|
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
|
- 写了大量代码后需要验证语法正确性
|
|
@@ -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
|
}
|
|
@@ -1263,6 +1291,15 @@ export function validatePlanFeasibility(changeDir, projectRoot = null) {
|
|
|
1263
1291
|
const fm = fmMatch[1]
|
|
1264
1292
|
const body = content.slice(fmMatch[0].length)
|
|
1265
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
|
+
|
|
1266
1303
|
// 1. 必要字段检查
|
|
1267
1304
|
const taskId = (fm.match(/^id:\s*(.+)/m)?.[1] || '').trim()
|
|
1268
1305
|
const title = (fm.match(/^title:\s*(.+)/m)?.[1] || '').trim()
|
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
|
|