sillyspec 3.32.9 → 3.33.0
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-brainstorm/SKILL.md +114 -114
- package/.claude/skills/sillyspec-execute/SKILL.md +222 -222
- package/.claude/skills/sillyspec-explore/SKILL.md +107 -107
- package/.claude/skills/sillyspec-flow/SKILL.md +1 -1
- package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
- package/.github/workflows/ci.yml +58 -0
- package/.husky/pre-push +7 -6
- package/AGENTS.md +1 -1
- package/assets/command-cards/flow.md +1 -1
- package/eval/eval.config.json +3 -3
- package/eval/lib/config.mjs +6 -3
- package/eval/lib/util.mjs +16 -3
- package/eval/lib/workdir.mjs +4 -1
- package/eval/results/eval.db +0 -0
- package/eval/results/logs/2026-10-15T12-37-22-986Z-004-phase-stats-armA/prompt.txt +10 -0
- package/eval/results/logs/2026-10-15T12-37-22-986Z-004-phase-stats-armA/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T12-37-22-986Z-004-phase-stats-armA/stdout.txt +1 -0
- package/eval/results/logs/2026-10-15T12-45-37-124Z-004-phase-stats-armB/prompt.txt +10 -0
- package/eval/results/logs/2026-10-15T12-45-37-124Z-004-phase-stats-armB/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T12-45-37-124Z-004-phase-stats-armB/stdout.txt +1 -0
- package/eval/results/logs/2026-10-15T12-56-19-259Z-004-phase-stats-armA/prompt.txt +10 -0
- package/eval/results/logs/2026-10-15T12-56-19-259Z-004-phase-stats-armA/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T12-56-19-259Z-004-phase-stats-armA/stdout.txt +1 -0
- package/eval/results/logs/2026-10-15T13-00-05-625Z-004-phase-stats-armB/prompt.txt +10 -0
- package/eval/results/logs/2026-10-15T13-04-51-003Z-004-phase-stats-armA/prompt.txt +10 -0
- package/eval/results/logs/2026-10-15T13-04-51-003Z-004-phase-stats-armA/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T13-04-51-003Z-004-phase-stats-armA/stdout.txt +1 -0
- package/eval/results/logs/2026-10-15T13-06-25-271Z-004-phase-stats-armB/prompt.txt +10 -0
- package/eval/results/logs/2026-10-15T13-06-25-271Z-004-phase-stats-armB/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T13-06-25-271Z-004-phase-stats-armB/stdout.txt +1 -0
- package/eval/results/logs/2026-10-15T13-24-38-308Z-004-phase-stats-armB/prompt.txt +10 -0
- package/eval/results/logs/2026-10-15T13-24-38-308Z-004-phase-stats-armB/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T13-24-38-308Z-004-phase-stats-armB/stdout.txt +1 -0
- package/eval/results/logs/2026-10-15T14-50-01-950Z-005-orders-export-armA/prompt.txt +8 -0
- package/eval/results/logs/2026-10-15T14-50-01-950Z-005-orders-export-armA/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T14-50-01-950Z-005-orders-export-armA/stdout.txt +1 -0
- package/eval/results/logs/2026-10-15T14-51-56-316Z-005-orders-export-armB/prompt.txt +8 -0
- package/eval/results/logs/2026-10-15T14-51-56-316Z-005-orders-export-armB/stderr.txt +0 -0
- package/eval/results/logs/2026-10-15T14-51-56-316Z-005-orders-export-armB/stdout.txt +1 -0
- package/eval/results/workdirs/001-slugify__armA/package.json +6 -0
- package/eval/results/workdirs/001-slugify__armA/src/slugify.mjs +26 -0
- package/eval/results/workdirs/001-slugify__armB/package.json +6 -0
- package/eval/results/workdirs/001-slugify__armB/src/slugify.mjs +26 -0
- package/eval/results/workdirs/002-each-day__armA/package.json +6 -0
- package/eval/results/workdirs/002-each-day__armA/src/dates.mjs +30 -0
- package/eval/results/workdirs/002-each-day__armB/DRY_RUN_WRONG_ANSWER.mjs +1 -0
- package/eval/results/workdirs/002-each-day__armB/package.json +6 -0
- package/eval/results/workdirs/002-each-day__armB/src/dates.mjs +4 -0
- package/eval/results/workdirs/003-word-report__armA/bin/report.mjs +36 -0
- package/eval/results/workdirs/003-word-report__armA/data/notes.txt +5 -0
- package/eval/results/workdirs/003-word-report__armA/package.json +6 -0
- package/eval/results/workdirs/003-word-report__armB/bin/report.mjs +36 -0
- package/eval/results/workdirs/003-word-report__armB/data/notes.txt +5 -0
- package/eval/results/workdirs/003-word-report__armB/package.json +6 -0
- package/eval/results/workdirs/004-phase-stats__armA/data/events.log +16 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/flow.md +44 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-archive.md +27 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-brainstorm.md +34 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-execute.md +30 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-plan.md +28 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/run-verify.md +30 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/commands/sillyspec/status.md +28 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-archive/SKILL.md +87 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-auto/SKILL.md +54 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-commit/SKILL.md +67 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-continue/SKILL.md +17 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-doctor/SKILL.md +76 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-execute/SKILL.md +222 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-explore/SKILL.md +107 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-export/SKILL.md +57 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-flow/SKILL.md +62 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-init/SKILL.md +177 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-knowledge/SKILL.md +40 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-plan/SKILL.md +156 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-propose/SKILL.md +16 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-quick/SKILL.md +116 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-resume/SKILL.md +21 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-scan/SKILL.md +79 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-state/SKILL.md +25 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-status/SKILL.md +54 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-verify/SKILL.md +87 -0
- package/eval/results/workdirs/004-phase-stats__armB/.claude/skills/sillyspec-workspace/SKILL.md +33 -0
- package/eval/results/workdirs/004-phase-stats__armB/AGENTS.md +45 -0
- package/eval/results/workdirs/004-phase-stats__armB/CLAUDE.md +3 -0
- package/eval/results/workdirs/004-phase-stats__armB/data/events.log +16 -0
- package/eval/results/workdirs/run-001-slugify__armA/package.json +6 -0
- package/eval/results/workdirs/run-001-slugify__armA/src/slugify.mjs +26 -0
- package/eval/results/workdirs/run-001-slugify__armB/package.json +6 -0
- package/eval/results/workdirs/run-001-slugify__armB/src/slugify.mjs +26 -0
- package/eval/results/workdirs/run-002-each-day__armA/package.json +6 -0
- package/eval/results/workdirs/run-002-each-day__armA/src/dates.mjs +30 -0
- package/eval/results/workdirs/run-002-each-day__armB/DRY_RUN_WRONG_ANSWER.mjs +1 -0
- package/eval/results/workdirs/run-002-each-day__armB/package.json +6 -0
- package/eval/results/workdirs/run-002-each-day__armB/src/dates.mjs +4 -0
- package/eval/results/workdirs/run-003-word-report__armA/bin/report.mjs +36 -0
- package/eval/results/workdirs/run-003-word-report__armA/data/notes.txt +5 -0
- package/eval/results/workdirs/run-003-word-report__armA/package.json +6 -0
- package/eval/results/workdirs/run-003-word-report__armB/bin/report.mjs +36 -0
- package/eval/results/workdirs/run-003-word-report__armB/data/notes.txt +5 -0
- package/eval/results/workdirs/run-003-word-report__armB/package.json +6 -0
- package/eval/results/workdirs/run-004-phase-stats__armA/bin/phase-stats.mjs +37 -0
- package/eval/results/workdirs/run-004-phase-stats__armA/data/events.log +16 -0
- package/eval/results/workdirs/run-004-phase-stats__armB/DRY_RUN_WRONG_ANSWER.mjs +1 -0
- package/eval/results/workdirs/run-004-phase-stats__armB/data/events.log +16 -0
- package/eval/results/workdirs/run-005-orders-export__armA/bin/export-orders.mjs +57 -0
- package/eval/results/workdirs/run-005-orders-export__armA/data/.gitattributes +1 -0
- package/eval/results/workdirs/run-005-orders-export__armA/data/orders.csv +9 -0
- package/eval/results/workdirs/run-005-orders-export__armA/out/summary.json +1 -0
- package/eval/results/workdirs/run-005-orders-export__armB/bin/export-orders.mjs +57 -0
- package/eval/results/workdirs/run-005-orders-export__armB/data/.gitattributes +1 -0
- package/eval/results/workdirs/run-005-orders-export__armB/data/orders.csv +9 -0
- package/eval/results/workdirs/run-005-orders-export__armB/out/summary.json +1 -0
- package/eval/tasks/004-phase-stats/files/data/events.log +16 -0
- package/eval/tasks/004-phase-stats/solution/bin/phase-stats.mjs +37 -0
- package/eval/tasks/004-phase-stats/task.json +6 -0
- package/eval/tasks/004-phase-stats/verify.mjs +38 -0
- package/eval/tasks/005-orders-export/files/data/.gitattributes +1 -0
- package/eval/tasks/005-orders-export/files/data/orders.csv +9 -0
- package/eval/tasks/005-orders-export/solution/bin/export-orders.mjs +57 -0
- package/eval/tasks/005-orders-export/task.json +6 -0
- package/eval/tasks/005-orders-export/verify.mjs +28 -0
- package/package.json +1 -1
- package/quicklog/tmp-fix-review.mjs +21 -0
- package/src/ceremony-tier.js +2 -1
- package/src/commit-suggest.js +45 -4
- package/src/config-schema.js +14 -0
- package/src/decision-distill.js +106 -24
- package/src/decision-redomain.js +148 -0
- package/src/decision-renumber.js +157 -0
- package/src/design-facts.js +3 -3
- package/src/docs-check.js +30 -15
- package/src/docs-debt.js +6 -5
- package/src/doctor-diagnostics.js +3 -1
- package/src/flow-parity.js +28 -4
- package/src/flow.js +76 -13
- package/src/forensics.js +236 -0
- package/src/fr-index.js +17 -2
- package/src/hooks/worktree-guard.js +3 -3
- package/src/index.js +41 -6
- package/src/knowledge-digest.js +23 -6
- package/src/knowledge-graph.js +95 -18
- package/src/knowledge-match.js +34 -9
- package/src/knowledge-stats.js +12 -13
- package/src/knowledge-vector.js +98 -7
- package/src/machine-interface.js +26 -0
- package/src/module-impact.js +2 -5
- package/src/modules.js +31 -0
- package/src/quicklog.js +6 -1
- package/src/run/archive-distill.js +3 -0
- package/src/run/command.js +3 -3
- package/src/run/complete-handlers.js +8 -3
- package/src/run/complete.js +113 -44
- package/src/run/gates.js +100 -103
- package/src/run/lint-ownership.js +37 -0
- package/src/run/prompt.js +43 -28
- package/src/run/stage.js +1 -1
- package/src/run/verify-quality-scan.js +10 -1
- package/src/sentinel-assertions.js +43 -1
- package/src/spec-sync.js +81 -2
- package/src/stage-contract.js +26 -3
- package/src/stage-review.js +46 -12
- package/src/stage-templates.js +2 -0
- package/src/stages/brainstorm.js +76 -21
- package/src/stages/knowledge.js +56 -5
- package/src/stages/plan-postcheck.js +13 -2
- package/src/stages/plan.js +19 -0
- package/src/sync.js +28 -2
- package/src/task-tick.js +12 -0
- package/src/test-bindings.js +32 -3
- package/src/timeline.js +60 -19
- package/src/ui-visual.js +119 -4
- package/src/verify-postcheck.js +166 -18
- package/src/verify-probes.js +61 -18
- package/src/worktree-apply.js +26 -2
- package/templates/agents-instruction.md +1 -1
package/src/run/prompt.js
CHANGED
|
@@ -68,6 +68,7 @@ const VOLATILE_PLACEHOLDER_RES = [
|
|
|
68
68
|
/<now-datetime>/g, /<now-timestamp>/g, /<now-date>/g, /<now-iso-datetime>/g, /<git-head-short>/g, /<quicklog-id>/g,
|
|
69
69
|
/{SCAN_STALENESS}/g, /{SCAN_FACTS}/g, /{QUICK_CONTEXT_DIGEST}/g, /{DECISION_HITS}/g, /{UI_VISUAL_GUIDANCE}/g,
|
|
70
70
|
/{KNOWLEDGE_HIT_REPORT}/g, /{DOCS_DEBT}/g, /{MODULE_RESOLVE_TABLE}/g, /{REVIEW_MATERIALS}/g,
|
|
71
|
+
/{REVIEW_TIER}/g, /{REVIEW_TIER_REASON}/g, /{REVIEW_JSON_CONTRACT}/g,
|
|
71
72
|
/{EXECUTE_RUN_ID}/g, /{STAGE_REVIEW_RUN_ID}/g, /{PROGRESS_SNAPSHOT}/g, /{TASK_COMPLETION_REPORT}/g,
|
|
72
73
|
/{WORKTREE_META}/g, /{WORKTREE_BASELINE_INFO}/g, /{TASKS_CHECKBOX}/g, /{GIT_DIRTY}/g,
|
|
73
74
|
/{SCOPE_AUDIT_TABLE}/g, /{FR_INDEX_DIGEST}/g, /{HANDOVER_SUMMARY}/g, /{PREFLIGHT_FAILURES}/g,
|
|
@@ -1044,6 +1045,18 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
1044
1045
|
promptText = promptText.split(token).join(v)
|
|
1045
1046
|
return promptText
|
|
1046
1047
|
}
|
|
1048
|
+
// 裸 split/join 注入点的收集通道(2026-10-16-step-guide-dyn-completeness):语义与
|
|
1049
|
+
// substituteSplitJoin 相同,供不以占位符正则批量替换的站点使用。
|
|
1050
|
+
// ⚠️ 纪律:凡向 promptText 注入运行期值的站点必须走本组三个助手之一——裸 `.split().join()`
|
|
1051
|
+
// 会让值漏进复入短输出的「动态注入段」附录(值不落盘、guide 掩码后无回读通道)。
|
|
1052
|
+
// 2026-10-15 消费仓实证:评审六值(tier/材料包/契约等)漏收,主代理被迫读引擎源码逆推档位、
|
|
1053
|
+
// 手建材料包、手拼 review.json 路径写错一层前缀再纠正——收集器注释「漏收才是坑」的兑现。
|
|
1054
|
+
const substituteRaw = (token, value) => {
|
|
1055
|
+
const v = String(value)
|
|
1056
|
+
dynSegments.push(v)
|
|
1057
|
+
promptText = promptText.split(token).join(v)
|
|
1058
|
+
return promptText
|
|
1059
|
+
}
|
|
1047
1060
|
// 替换 prompt 中的占位符
|
|
1048
1061
|
if (projectName && promptText.includes('<project>')) {
|
|
1049
1062
|
substitute(/<project>/g, projectName, true)
|
|
@@ -1390,7 +1403,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
1390
1403
|
if (stageName === 'brainstorm' && promptText.includes('{DECISION_HITS}')) {
|
|
1391
1404
|
try {
|
|
1392
1405
|
const { matchKnowledgeHybrid } = await import('../knowledge-vector.js')
|
|
1393
|
-
const { deathPathNote } = await import('../knowledge-match.js')
|
|
1406
|
+
const { deathPathNote, decisionChangeQualifier } = await import('../knowledge-match.js')
|
|
1394
1407
|
const decSpecBase = resolvePromptSpecBase(platformOpts, cwd)
|
|
1395
1408
|
const decKnowledgeDir = join(decSpecBase, 'knowledge')
|
|
1396
1409
|
// taskContext:changeName(brainstorm Step2 时 tasks.md 尚未生成,变更名是唯一稳定任务
|
|
@@ -1402,10 +1415,10 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
1402
1415
|
let decInjected = ''
|
|
1403
1416
|
if (rejectedHits.length > 0) {
|
|
1404
1417
|
const lines = [
|
|
1405
|
-
'⚠️ 否决决策/死路注记提示(历史已否决或已记死路,防复潮)——下列决策此前已被否决或已记死路。看到理由后,除非复潮条件明确满足,不要在本次方案中重新提出;若认为复潮条件已满足,须在本变更 decisions.md 记录新版本条目(D-xxx@vN+1
|
|
1418
|
+
'⚠️ 否决决策/死路注记提示(历史已否决或已记死路,防复潮)——下列决策此前已被否决或已记死路。看到理由后,除非复潮条件明确满足,不要在本次方案中重新提出;若认为复潮条件已满足,须在本变更 decisions.md 记录新版本条目(D-xxx@vN+1)说明依据;跨变更引用他条目须带变更名限定(变更名#D-xxx@vN——同号不同变更互不相干):'
|
|
1406
1419
|
]
|
|
1407
1420
|
for (const h of rejectedHits.slice(0, 5)) {
|
|
1408
|
-
lines.push(`- ${h.id} ${h.title}(${h.file})${h.status === 'rejected' ? '' : ' ⚰️死路注记'}`)
|
|
1421
|
+
lines.push(`- ${h.id} ${h.title}(${h.file}|${decisionChangeQualifier(h.change)})${h.status === 'rejected' ? '' : ' ⚰️死路注记'}`)
|
|
1409
1422
|
lines.push(` - ${h.status === 'rejected' ? '否决理由' : '死路'}:${h.status === 'rejected' ? (h.reason || '(未记录)') : deathPathNote(h.reason)}`)
|
|
1410
1423
|
lines.push(` - 复潮条件:${h.revisitWhen || '(未记录)'}`)
|
|
1411
1424
|
}
|
|
@@ -1427,7 +1440,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
1427
1440
|
// 替换空串零输出;fail-soft 异常单行说明,不留残留占位符。
|
|
1428
1441
|
if (stageName === 'brainstorm' && promptText.includes('{UI_VISUAL_GUIDANCE}')) {
|
|
1429
1442
|
try {
|
|
1430
|
-
const { detectUiTouch, buildUiGuidanceLines } = await import('../ui-visual.js')
|
|
1443
|
+
const { detectUiTouch, buildUiGuidanceLines, extractQuotedPaths } = await import('../ui-visual.js')
|
|
1431
1444
|
const uiSpecBase = resolvePromptSpecBase(platformOpts, cwd)
|
|
1432
1445
|
let corpus = changeName || ''
|
|
1433
1446
|
for (const name of ['proposal.md', 'requirements.md']) {
|
|
@@ -1435,7 +1448,10 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
1435
1448
|
corpus += '\n' + readFileSync(join(uiSpecBase, 'changes', changeName, name), 'utf8')
|
|
1436
1449
|
} catch { /* 缺席跳过 */ }
|
|
1437
1450
|
}
|
|
1438
|
-
|
|
1451
|
+
// ui_prototype 形态注入(2026-10-15-ui-proto-form-config):语料路径参与子仓 override
|
|
1452
|
+
// 匹配(proposal/requirements 反引号路径粗提);无路径=仓级兜底。
|
|
1453
|
+
const guidancePaths = extractQuotedPaths(corpus)
|
|
1454
|
+
substitute(/\{UI_VISUAL_GUIDANCE\}/g, detectUiTouch(corpus) ? '\n' + buildUiGuidanceLines(uiSpecBase, guidancePaths).join('\n') + '\n' : '')
|
|
1439
1455
|
} catch (e) {
|
|
1440
1456
|
substitute(/\{UI_VISUAL_GUIDANCE\}/g, `[ui-guidance] UI 须知注入异常(${e.message}),跳过`)
|
|
1441
1457
|
}
|
|
@@ -1676,22 +1692,21 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
1676
1692
|
reviewMaterialsMd = await assembleStageReviewMaterials({ stage: matStage, cwd, changeName, specBase: tierSpecBase })
|
|
1677
1693
|
} catch { /* 组装 best-effort:失败零注入 */ }
|
|
1678
1694
|
}
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1695
|
+
// 全部走 substituteRaw(收集进 dynSegments——复入附录含评审六值;2026-10-16-step-guide-dyn-completeness)
|
|
1696
|
+
substituteRaw('{REVIEW_TIER}', ceremonyInjection.tierValue + ceremonyInjection.menuMd)
|
|
1697
|
+
substituteRaw('{REVIEW_TIER_REASON}', tier.reason)
|
|
1698
|
+
substituteRaw('{STAGE_REVIEW_RUN_ID}', reviewRunId)
|
|
1699
|
+
substituteRaw('{REVIEW_JSON_CONTRACT}', reviewContractMd)
|
|
1700
|
+
substituteRaw('{PRIOR_REVIEW_FACTS}', priorFactsMd)
|
|
1701
|
+
substituteRaw('{REVIEW_MATERIALS}', reviewMaterialsMd) // 三阶段材料包槽(CLI 组装注入,Gap 1 收口——此前恒空串靠主代理手组,机械保证断一半;主代理点名半边由模板补位指引接手)
|
|
1686
1702
|
} catch (e) {
|
|
1687
|
-
// 降级 self,避免 prompt
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
.split('{REVIEW_MATERIALS}').join('') // 降级分支同步 join(plan 审查提示:防占位符字面量残留)
|
|
1703
|
+
// 降级 self,避免 prompt 残留占位符(降级值同样收编进 dynSegments——附录如实呈现降级态)
|
|
1704
|
+
substituteRaw('{REVIEW_TIER}', 'self')
|
|
1705
|
+
substituteRaw('{REVIEW_TIER_REASON}', '分级异常降级 self: ' + e.message)
|
|
1706
|
+
substituteRaw('{STAGE_REVIEW_RUN_ID}', 'review-unknown')
|
|
1707
|
+
substituteRaw('{REVIEW_JSON_CONTRACT}', '(review 契约注入失败,按 schemaVersion=1 + reviewType + verdicts∈pass/fail/cannot_verify + reviewedFiles + docHash=主文档 sha256 产出)')
|
|
1708
|
+
substituteRaw('{PRIOR_REVIEW_FACTS}', '')
|
|
1709
|
+
substituteRaw('{REVIEW_MATERIALS}', '') // 降级分支同步 join(plan 审查提示:防占位符字面量残留)
|
|
1695
1710
|
}
|
|
1696
1711
|
}
|
|
1697
1712
|
|
|
@@ -1721,7 +1736,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
1721
1736
|
} catch (e) {
|
|
1722
1737
|
metaInfo = '(注入失败:' + e.message + '——运行 sillyspec worktree meta 自查,meta.json 不存在则停止并报错)'
|
|
1723
1738
|
}
|
|
1724
|
-
|
|
1739
|
+
substituteRaw('{WORKTREE_META}', metaInfo)
|
|
1725
1740
|
}
|
|
1726
1741
|
|
|
1727
1742
|
// archive Step1「任务完成度检查」客观真相源注入:以 review.json verdict 算完成度,
|
|
@@ -1738,9 +1753,9 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
1738
1753
|
const summary = tcrChangeDir
|
|
1739
1754
|
? summarizeTaskCompletion({ changeDir: tcrChangeDir, runtimeRoot: tcrRuntimeRoot, changeName })
|
|
1740
1755
|
: null
|
|
1741
|
-
|
|
1756
|
+
substituteRaw('{TASK_COMPLETION_REPORT}', summary ? summary.report : '(无法计算完成度:变更目录缺失)')
|
|
1742
1757
|
} catch (e) {
|
|
1743
|
-
|
|
1758
|
+
substituteRaw('{TASK_COMPLETION_REPORT}', '(完成度计算异常:' + e.message + ',请手动核对 plan.md 与 .runtime/execute-runs/*/tasks/task-NN/review.json)')
|
|
1744
1759
|
}
|
|
1745
1760
|
}
|
|
1746
1761
|
|
|
@@ -1781,7 +1796,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
1781
1796
|
} catch (e) {
|
|
1782
1797
|
baselineInfo = '(worktree 基线注入失败:' + e.message + '——diff 对账前先 git branch -a 查 sillyspec/<change> 分支并用 git merge-base <main> <branch> 自查基点,勿用主仓 HEAD 当基点)'
|
|
1783
1798
|
}
|
|
1784
|
-
|
|
1799
|
+
substituteRaw('{WORKTREE_BASELINE_INFO}', baselineInfo)
|
|
1785
1800
|
}
|
|
1786
1801
|
|
|
1787
1802
|
// verify「运行测试和质量扫描」evidence-auto 推荐注入(task-12,FR-11 / D-005@v2):
|
|
@@ -1822,7 +1837,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
1822
1837
|
} catch (e) {
|
|
1823
1838
|
eaInjected = `(evidence-auto 推荐注入失败:${e.message}——请读 .sillyspec/local.yaml 确认 test_strategy;--done 时 CLI 仍会按 resolveTestStrategy 实测对账,推荐不可用时可显式设 test_strategy: full/module)`
|
|
1824
1839
|
}
|
|
1825
|
-
|
|
1840
|
+
substituteRaw('{EVIDENCE_AUTO_RECOMMENDATION}', eaInjected)
|
|
1826
1841
|
}
|
|
1827
1842
|
|
|
1828
1843
|
// {ARCHIVE_IMPACT_AUDIT}(2026-09-09 ql-20260909-004):archive extract-module-impact 步的
|
|
@@ -1842,7 +1857,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
1842
1857
|
} catch (e) {
|
|
1843
1858
|
auditBlock = '(三重核对机械预填失败:' + (e && e.message ? e.message : e) + '——回退手跑 git diff --name-only 比对)'
|
|
1844
1859
|
}
|
|
1845
|
-
|
|
1860
|
+
substituteRaw('{ARCHIVE_IMPACT_AUDIT}', auditBlock)
|
|
1846
1861
|
}
|
|
1847
1862
|
|
|
1848
1863
|
// {SCOPE_AUDIT_TABLE}(2026-09-10-change-scope-audit task-05,FR-03):archive「确认归档」步的
|
|
@@ -1865,7 +1880,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
1865
1880
|
} catch (e) {
|
|
1866
1881
|
scopeTable = '(变更范围对账表注入失败:' + (e && e.message ? e.message : e) + '——回退手跑 sillyspec scope-audit --change <name> 查看后再确认归档,注入失败不阻断归档)'
|
|
1867
1882
|
}
|
|
1868
|
-
|
|
1883
|
+
substituteRaw('{SCOPE_AUDIT_TABLE}', scopeTable)
|
|
1869
1884
|
}
|
|
1870
1885
|
|
|
1871
1886
|
// {HANDOVER_SUMMARY}(2026-09-17-pass-cap-semantics task-04,FR-06 注入态):archive「确认
|
|
@@ -1901,7 +1916,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
1901
1916
|
} catch (e) {
|
|
1902
1917
|
handoverBlock = '(移交项清单注入失败:' + (e && e.message ? e.message : e) + '——回退读 changes/<变更名>/verify-facts.json 的 handover 段核对后再确认归档,注入失败不阻断归档)'
|
|
1903
1918
|
}
|
|
1904
|
-
|
|
1919
|
+
substituteRaw('{HANDOVER_SUMMARY}', handoverBlock)
|
|
1905
1920
|
}
|
|
1906
1921
|
|
|
1907
1922
|
// 注入模块上下文(brainstorm/plan/execute 阶段 + quick 首步「理解任务」——刀①:quick
|
package/src/run/stage.js
CHANGED
|
@@ -417,7 +417,7 @@ export async function runStage(pm, progress, stageName, cwd, changeName, skipApp
|
|
|
417
417
|
// 进程内直调 runCommand 的入口(测试/编程调用)与 --change 指向不存在会话的形态。
|
|
418
418
|
console.error(`❌ quick 通道已退役,无法启动 quick 会话(若带 --change 则该会话不存在或已清理)。`)
|
|
419
419
|
console.error(` 新工作请走轻量变更(--input 引号内换行合法,可照抄):`)
|
|
420
|
-
console.error(' sillyspec flow start --change <YYYY-MM-DD-名> --input "
|
|
420
|
+
console.error(' sillyspec flow start --change <YYYY-MM-DD-名> --input "<一句中文概括(首行即变更标题);随后动机与背景')
|
|
421
421
|
console.error('')
|
|
422
422
|
console.error(' 成功标准:')
|
|
423
423
|
console.error(' - <可验证标准>"')
|
|
@@ -798,13 +798,22 @@ export async function executeVerifyQualityScan({ cwd, specBase, changeName, plat
|
|
|
798
798
|
storeQualityScan({ specBase, cwd, changeName, testResult: testCheck, lintResult: lintCheck, smokeResult, coverageCheck, usedSnapshot: Boolean(snap), missReason: _missReasonParts.join(';') || null, scopeDecision: { planned: !process.env.SILLYSPEC_VERIFY_GATE_SNAPSHOT_OFF, actual: Boolean(snap) } })
|
|
799
799
|
const testFailed = testCheck.status === 'failed'
|
|
800
800
|
const lintBlocked = shouldBlockVerifyLint(lintCheck)
|
|
801
|
+
// 2026-10-15-lint-triage-path-unify:lint 归属鉴定降档接线(与 gates --done 路径同源共享
|
|
802
|
+
// helper)——零交集存量债降档 advisory 不 throw;owned/unattributable/鉴定异常维持硬拦
|
|
803
|
+
// (保守)。仅在「lint 单独失败」时降档(testFailed 优先 throw——测试失败不作 lint 降档
|
|
804
|
+
// 陪葬判断)。原两路径分叉实证见消费仓坑档案 verify-lint-triage-path-divergence.md。
|
|
805
|
+
let lintAdvisoryDowngraded = false
|
|
806
|
+
if (lintBlocked && !testFailed) {
|
|
807
|
+
const { lintOwnershipDowngrade } = await import('./lint-ownership.js')
|
|
808
|
+
lintAdvisoryDowngraded = await lintOwnershipDowngrade({ lintCheck, cwd, changeName, specBase })
|
|
809
|
+
}
|
|
801
810
|
// 摩擦计数(friction-signal-hint task-03):记录条件与 throw 条件**刻意解耦**——advisory 档
|
|
802
811
|
// lint 失败(SILLYSPEC_VERIFY_LINT_GATE=advisory 时不 throw)同样是摩擦信号,计数不应随
|
|
803
812
|
// 逃生阀漂移。recordFrictionEvent 自带静默降级,不影响本步推进/throw 语义。
|
|
804
813
|
if (testFailed || lintCheck.status === 'failed') {
|
|
805
814
|
await recordFrictionEvent({ cwd, changeName, platformOpts, type: 'verify_run_failed', detail: testFailed ? 'test' : 'lint' })
|
|
806
815
|
}
|
|
807
|
-
if (testFailed || lintBlocked) {
|
|
816
|
+
if (testFailed || (lintBlocked && !lintAdvisoryDowngraded)) {
|
|
808
817
|
if (snap) {
|
|
809
818
|
try { const { printSnapshotFailureHint } = await import('./gate-snapshot.js')
|
|
810
819
|
printSnapshotFailureHint(snap) } catch { /* 提示失败不影响阻断 */ }
|
|
@@ -165,6 +165,48 @@ export function detectBatchCheckCadence(events) {
|
|
|
165
165
|
return worst;
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
+
/**
|
|
169
|
+
* 侧车勾选突发检测(2026-10-14-tick-burst-sidecar-gate,纯函数):task-ticks.jsonl 的毫秒级
|
|
170
|
+
* 时戳按相邻间隔窗口分簇——CLI 逐格快连点(收口一把点的官方路径形态)在事件流里是单格精确跳,
|
|
171
|
+
* 10-07 采样去重后对 detectBatchCheckCadence 不可见(近期实证:6 格 2 秒内/4 格 1 秒内零检出);
|
|
172
|
+
* 侧车是 CLI tick 的事实源(随归档进 git、不受观测盲窗影响),收口节奏门回退消费本函数——
|
|
173
|
+
* CLI 与直接 Edit 两条路径同判。
|
|
174
|
+
* 语义:tickLog 同任务取最早(幂等重勾防御)+ 坏行跳过(task 形态/时戳有限性)+ 排序后按
|
|
175
|
+
* gap<=windowMs(缺省 60s,opts.windowMs 覆写)分簇;最大簇 size>=2 → 返回与事件流同形的
|
|
176
|
+
* batchTick 记录。from/to 是 CLI 勾序计数(Edit 翻格不记侧车,与事件流 checked N→M 计数空间
|
|
177
|
+
* 不同源)——resolveBatchTickAction 只消费跳幅(to-from)与 ts,语义等价;detail 带「侧车突发」
|
|
178
|
+
* 标注防口径混淆。fail-open:不足两格/空输入/全坏行 → null。
|
|
179
|
+
*/
|
|
180
|
+
export function detectTickBurst(tickLog, opts = {}) {
|
|
181
|
+
const windowMs = Number.isFinite(opts.windowMs) ? opts.windowMs : 60_000;
|
|
182
|
+
const byTask = new Map();
|
|
183
|
+
for (const r of Array.isArray(tickLog) ? tickLog : []) {
|
|
184
|
+
const task = String((r && r.task) || '').trim();
|
|
185
|
+
const ts = Number(r && r.ts);
|
|
186
|
+
if (!/^task-\d+$/.test(task) || !Number.isFinite(ts)) continue;
|
|
187
|
+
const prev = byTask.get(task);
|
|
188
|
+
if (prev === undefined || ts < prev) byTask.set(task, ts);
|
|
189
|
+
}
|
|
190
|
+
const ticks = [...byTask.values()].sort((a, b) => a - b);
|
|
191
|
+
if (ticks.length < 2) return null;
|
|
192
|
+
let worst = null;
|
|
193
|
+
let start = 0;
|
|
194
|
+
for (let i = 1; i <= ticks.length; i++) {
|
|
195
|
+
if (i < ticks.length && ticks[i] - ticks[i - 1] <= windowMs) continue;
|
|
196
|
+
const size = i - start;
|
|
197
|
+
if (size >= 2 && (!worst || size > worst.size)) {
|
|
198
|
+
worst = { size, from: start, to: i, ts: ticks[start], spanMs: ticks[i - 1] - ticks[start] };
|
|
199
|
+
}
|
|
200
|
+
start = i;
|
|
201
|
+
}
|
|
202
|
+
if (!worst) return null;
|
|
203
|
+
return {
|
|
204
|
+
from: worst.from, to: worst.to, size: worst.size, spanMs: worst.spanMs, ts: worst.ts,
|
|
205
|
+
source: 'tick-burst',
|
|
206
|
+
detail: `checked ${worst.from}→${worst.to}(task tick 侧车突发 ${worst.size} 格)`,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
168
210
|
/**
|
|
169
211
|
* 单拍勾选门决策(2026-09-29-batch-tick-gate,纯函数):检出单拍跳(batchTick 非 null)
|
|
170
212
|
* 时的收口动作裁决。镜像豁免分支退役(2026-10-07-thin-tasks-v3——任务面统一为 agent
|
|
@@ -193,4 +235,4 @@ export function resolveBatchTickAction({ batchTick, nonMirrorCount = null, allow
|
|
|
193
235
|
return { action: 'reject', reason: 'agent-batch-tick' };
|
|
194
236
|
}
|
|
195
237
|
|
|
196
|
-
export default { detectFakeCheckCompletion, detectBatchCheckCadence };
|
|
238
|
+
export default { detectFakeCheckCompletion, detectBatchCheckCadence, detectTickBurst };
|
package/src/spec-sync.js
CHANGED
|
@@ -19,6 +19,7 @@ import { createHash } from 'crypto';
|
|
|
19
19
|
import { existsSync, readdirSync, readFileSync, statSync, mkdirSync, writeFileSync, unlinkSync } from 'fs';
|
|
20
20
|
import { join, relative, sep } from 'path';
|
|
21
21
|
import { bindSyncNoiseRoot, syncConnectionWarn, isConnectionClassStatus, noteSyncSuccess } from './sync-noise.js';
|
|
22
|
+
import { writeAtomicSync } from './fs-atomic.js';
|
|
22
23
|
|
|
23
24
|
// 与 sillyhub-daemon/src/spec-sync.ts 共用排除口径(task-07 / FR-06 / D-008@v2):
|
|
24
25
|
// .runtime(有点)/ runtime(无点)/ projects(本地环境文件)在顶层排除;
|
|
@@ -63,6 +64,61 @@ export function describeSyncError(err) {
|
|
|
63
64
|
return err && err.message ? err.message : String(err);
|
|
64
65
|
}
|
|
65
66
|
|
|
67
|
+
// ── 同步失败连续计数与响应体摘要(2026-10-16 生产索引损坏事故可观测性加固)──
|
|
68
|
+
// 事故:平台 422 业务错误只打状态码(真因 HTTP_422_SPEC_BUNDLE_INVALID+路径要手动重放请求
|
|
69
|
+
// 才拿到),且失败走「下次自动重试」无限静默循环——归档半态(本地已归档、平台仍「进行中」)
|
|
70
|
+
// 挂数小时无人知晓。两道护栏:①非 2xx 响应体解析进告警行;②按 label 连续失败计数持久化,
|
|
71
|
+
// 连续 ≥3 次升级 🚨 显式告警,任一次成功清零。
|
|
72
|
+
const FAIL_STREAK_FILE = join('.runtime', 'spec-sync-fail-streak.json');
|
|
73
|
+
const FAIL_STREAK_ESCALATE_AT = 3;
|
|
74
|
+
|
|
75
|
+
export function describeErrorBody(status, rawBody) {
|
|
76
|
+
if (!rawBody) return '';
|
|
77
|
+
try {
|
|
78
|
+
const j = JSON.parse(rawBody);
|
|
79
|
+
const code = j.code || `HTTP_${status}`;
|
|
80
|
+
const p = j.details && j.details.path ? ` path=${j.details.path}` : '';
|
|
81
|
+
return j.message ? `${code}${p}: ${j.message}` : rawBody.slice(0, 200);
|
|
82
|
+
} catch {
|
|
83
|
+
return rawBody.slice(0, 200);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function readFailStreak(specRoot) {
|
|
88
|
+
try {
|
|
89
|
+
return JSON.parse(readFileSync(join(specRoot, FAIL_STREAK_FILE), 'utf8')) || {};
|
|
90
|
+
} catch {
|
|
91
|
+
return {};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function bumpFailStreak(specRoot, label, detail) {
|
|
96
|
+
const all = readFailStreak(specRoot);
|
|
97
|
+
const cur = all[label] || { count: 0 };
|
|
98
|
+
cur.count += 1;
|
|
99
|
+
cur.lastAt = new Date().toISOString();
|
|
100
|
+
if (detail) cur.lastError = String(detail).slice(0, 300);
|
|
101
|
+
all[label] = cur;
|
|
102
|
+
try {
|
|
103
|
+
mkdirSync(join(specRoot, '.runtime'), { recursive: true });
|
|
104
|
+
writeAtomicSync(join(specRoot, FAIL_STREAK_FILE), JSON.stringify(all, null, 2) + '\n');
|
|
105
|
+
} catch {
|
|
106
|
+
// fail-soft:计数落盘失败只影响告警升级,不阻断同步主流程
|
|
107
|
+
}
|
|
108
|
+
return cur.count;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function clearFailStreak(specRoot, label) {
|
|
112
|
+
const all = readFailStreak(specRoot);
|
|
113
|
+
if (!all[label]) return;
|
|
114
|
+
delete all[label];
|
|
115
|
+
try {
|
|
116
|
+
writeAtomicSync(join(specRoot, FAIL_STREAK_FILE), JSON.stringify(all, null, 2) + '\n');
|
|
117
|
+
} catch {
|
|
118
|
+
// fail-soft:残留计数只是少清一次,下次成功再清
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
66
122
|
function _hashBuffer(buf) {
|
|
67
123
|
return createHash('sha256').update(buf).digest('hex');
|
|
68
124
|
}
|
|
@@ -640,16 +696,26 @@ export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
|
|
|
640
696
|
// 8 分钟才被后续步骤的同步补上)。失败可见、成功不打扰。
|
|
641
697
|
// 连接类失败(404=端点未就绪 / 5xx)走噪音闸(sync-noise.js):平台未就绪期每步
|
|
642
698
|
// 自动同步重刷同一行,淹没门禁输出;其余状态码(4xx 业务态)每轮该看见。
|
|
643
|
-
const
|
|
699
|
+
const raw = await res.text().catch(() => '');
|
|
700
|
+
const detail = describeErrorBody(res.status, raw);
|
|
701
|
+
const failCount = bumpFailStreak(specRoot, logLabel, detail);
|
|
702
|
+
const line = `[spec-sync] 拉取清单失败 HTTP ${res.status}(文件树本次未同步,下次自动重试): ${logLabel}${detail ? ` —— ${detail}` : ''}`;
|
|
644
703
|
if (isConnectionClassStatus(res.status)) syncConnectionWarn(line);
|
|
645
704
|
else console.warn(line);
|
|
705
|
+
if (failCount >= FAIL_STREAK_ESCALATE_AT) {
|
|
706
|
+
console.warn(`🚨 [spec-sync] 已连续 ${failCount} 次同步失败——平台文件树可能持续落后(变更中心或仍显示「进行中」/旧内容): ${logLabel}`);
|
|
707
|
+
}
|
|
646
708
|
return { synced: 0 };
|
|
647
709
|
}
|
|
648
710
|
const body = await res.json().catch(() => ({}));
|
|
649
711
|
serverManifest = body.files || {};
|
|
650
712
|
} catch (err) {
|
|
713
|
+
const failCount = bumpFailStreak(specRoot, logLabel, describeSyncError(err));
|
|
651
714
|
const line = `[spec-sync] 拉取清单异常(文件树本次未同步,下次自动重试): ${logLabel}: ${describeSyncError(err)}`;
|
|
652
715
|
syncConnectionWarn(line); // 网络/超时/熔断均连接类
|
|
716
|
+
if (failCount >= FAIL_STREAK_ESCALATE_AT) {
|
|
717
|
+
console.warn(`🚨 [spec-sync] 已连续 ${failCount} 次同步失败——平台文件树可能持续落后(变更中心或仍显示「进行中」/旧内容): ${logLabel}`);
|
|
718
|
+
}
|
|
653
719
|
return { synced: 0 };
|
|
654
720
|
}
|
|
655
721
|
|
|
@@ -691,6 +757,7 @@ export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
|
|
|
691
757
|
if (ops.length === 0) {
|
|
692
758
|
debugLog(`[spec-sync] 无差异,跳过同步: ${logLabel}`);
|
|
693
759
|
clearStaleTombstoneMarkers(specRoot);
|
|
760
|
+
clearFailStreak(specRoot, logLabel);
|
|
694
761
|
return { synced: 0 };
|
|
695
762
|
}
|
|
696
763
|
|
|
@@ -706,9 +773,15 @@ export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
|
|
|
706
773
|
signal: withSignal(30000),
|
|
707
774
|
});
|
|
708
775
|
if (!res.ok) {
|
|
709
|
-
const
|
|
776
|
+
const raw = await res.text().catch(() => '');
|
|
777
|
+
const detail = describeErrorBody(res.status, raw);
|
|
778
|
+
const failCount = bumpFailStreak(specRoot, logLabel, detail);
|
|
779
|
+
const line = `[spec-sync] 同步请求失败 HTTP ${res.status}(文件树本次未同步,下次自动重试): ${logLabel}${detail ? ` —— ${detail}` : ''}`;
|
|
710
780
|
if (isConnectionClassStatus(res.status)) syncConnectionWarn(line);
|
|
711
781
|
else console.warn(line);
|
|
782
|
+
if (failCount >= FAIL_STREAK_ESCALATE_AT) {
|
|
783
|
+
console.warn(`🚨 [spec-sync] 已连续 ${failCount} 次同步失败——平台文件树可能持续落后(变更中心或仍显示「进行中」/旧内容): ${logLabel}`);
|
|
784
|
+
}
|
|
712
785
|
return { synced: 0 };
|
|
713
786
|
}
|
|
714
787
|
const body = await res.json().catch(() => ({}));
|
|
@@ -780,6 +853,7 @@ export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
|
|
|
780
853
|
writeBaseSnapshot(specRoot, localFiles);
|
|
781
854
|
clearSpecConflictMarker(specRoot, conflictLabel);
|
|
782
855
|
noteSyncSuccess();
|
|
856
|
+
clearFailStreak(specRoot, logLabel);
|
|
783
857
|
console.log(`[spec-sync] 冲突已自动消解(0 个需人工裁决;本会话改动 ${ops.length - followers.length > 0 ? '已同步' : '无差异'}): ${conflictLabel}`);
|
|
784
858
|
return { synced: Math.max(ops.length - followers.length, 0), conflict: false, autoResolved: followers.length };
|
|
785
859
|
}
|
|
@@ -811,6 +885,7 @@ export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
|
|
|
811
885
|
return { synced: 0, conflict: true, serverVersions: real, autoResolved: followers.length, conflictPath };
|
|
812
886
|
}
|
|
813
887
|
noteSyncSuccess();
|
|
888
|
+
clearFailStreak(specRoot, logLabel);
|
|
814
889
|
console.log(`[spec-sync] 已同步 ${ops.length} 个文件变更: ${logLabel}`);
|
|
815
890
|
// 本轮无冲突 = 此前的未决 spec 树冲突已不存在,清残留标记(否则 status 永久红标、
|
|
816
891
|
// 横幅去重单行提示不消——坑 spec-sync-conflict-banner-spam)。全树入口
|
|
@@ -828,8 +903,12 @@ export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
|
|
|
828
903
|
writeBaseSnapshot(specRoot, localFiles);
|
|
829
904
|
return { synced: ops.length };
|
|
830
905
|
} catch (err) {
|
|
906
|
+
const failCount = bumpFailStreak(specRoot, logLabel, describeSyncError(err));
|
|
831
907
|
const line = `[spec-sync] 同步异常(文件树本次未同步,下次自动重试): ${logLabel}: ${describeSyncError(err)}`;
|
|
832
908
|
syncConnectionWarn(line); // 网络/超时/熔断均连接类
|
|
909
|
+
if (failCount >= FAIL_STREAK_ESCALATE_AT) {
|
|
910
|
+
console.warn(`🚨 [spec-sync] 已连续 ${failCount} 次同步失败——平台文件树可能持续落后(变更中心或仍显示「进行中」/旧内容): ${logLabel}`);
|
|
911
|
+
}
|
|
833
912
|
return { synced: 0 };
|
|
834
913
|
}
|
|
835
914
|
}
|
package/src/stage-contract.js
CHANGED
|
@@ -1059,8 +1059,12 @@ const API_COVERAGE_HEADING_RE = /^## 接口验证覆盖矩阵/m
|
|
|
1059
1059
|
const API_ENDPOINT_CELL_RE = /(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(\/[^\s|]+)/
|
|
1060
1060
|
/** 声明占位行(D-005 零解析降级侧,骨架「本变更接口面:N 端点(agent 声明)」字面同源) */
|
|
1061
1061
|
const API_DECLARED_ROW_RE = /本变更接口面[::]\s*(\d+)\s*端点/
|
|
1062
|
-
/** design接口表# 锚点提取(解析级校验用——防空指,Grill #10:非物理行号,METHOD /path
|
|
1063
|
-
|
|
1062
|
+
/** design接口表# 锚点提取(解析级校验用——防空指,Grill #10:非物理行号,METHOD /path 形态)。
|
|
1063
|
+
* 字符类左右括号对称排除(坑 verify-close-gate-staggered-exposure,2026-10-15 消费仓 node 直调
|
|
1064
|
+
* 实证:原只排右括号)不排左括号(——`GET /api/x(注记)` 的路径吞成 `/api/x(注记`,与解析面
|
|
1065
|
+
* 永不匹配 → 恒空指,报错文案却指向「design.md 未命中」误导排查方向)。中文(英文、全/半角
|
|
1066
|
+
* 四形与既有分隔符一并终止捕获。 */
|
|
1067
|
+
const API_ANCHOR_DESIGN_API_RE = /design接口表#\s*(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(\/[^\s|,。;;()()]+)/g
|
|
1064
1068
|
/** 探索行标记(骨架文法注释字面同源:证据列含 [探索]) */
|
|
1065
1069
|
const API_EXPLORATION_MARK_RE = /\[探索\]/
|
|
1066
1070
|
|
|
@@ -1261,9 +1265,16 @@ export function judgeApiCoverageMatrix(args) {
|
|
|
1261
1265
|
}
|
|
1262
1266
|
}
|
|
1263
1267
|
if (anchorViolations.length > 0) {
|
|
1268
|
+
// 空指诊断(坑 verify-close-gate-staggered-exposure 建议③,2026-10-15 实证:报错只说「未命中
|
|
1269
|
+
// 解析面」不展示解析面实际内容——「锚点吞字」与「design 缺表」分不清,agent 被引去改 design
|
|
1270
|
+
// 文档白做功。附上 parseSet 实际端点集一眼对齐)。
|
|
1271
|
+
const parseFaceDesc = parseSet.size > 0
|
|
1272
|
+
? [...parseSet].join(';')
|
|
1273
|
+
: '(解析面为空——design.md 接口段未解析到端点,先表格化接口段再跑 verify-probes --init)'
|
|
1264
1274
|
errors.push(
|
|
1265
1275
|
`接口验证覆盖矩阵有 ${anchorViolations.length} 项证据锚点缺失/空指` +
|
|
1266
1276
|
`(covered/partial 证据须含锚点五形态之一,可复制样例:design接口表#POST /api/xx(# 后必须 METHOD /path 且命中 design 接口段解析面,仅表名/行号不计)/ 权限矩阵[admin×读] / 契约表@任务卡字段清单 / DDL@users.id / 载荷@e2e_body.json;covered-service 证据须含测试锚点三形态之一:\`.test.\` 文件名 / file:line / 反引号包裹的测试名):${anchorViolations.join(';')}。` +
|
|
1277
|
+
`design 接口段实际解析面端点集:${parseFaceDesc}——design接口表# 锚点路径须与之逐字一致。` +
|
|
1267
1278
|
`修复:在证据列补真实锚点(design接口表# 锚点须与 design.md 接口段端点一致,防编造端点)。`
|
|
1268
1279
|
)
|
|
1269
1280
|
}
|
|
@@ -1419,6 +1430,9 @@ function extractPermissionMatrixText(designMd) {
|
|
|
1419
1430
|
* - tasks/ 不存在(quick 会话/旧变更)或 verify-result.md 未落盘(中间步骤)→ no-op
|
|
1420
1431
|
* - factsExpected=false(存量未跑管线,resolveFactsExpected 同源口径)→ no-op 零行为
|
|
1421
1432
|
* - verify-facts.json(apiFace/consumerHints/handover,缺失容 null——fail-closed 归纯函数)
|
|
1433
|
+
* - context.apiFaceLive(可选,2026-10-14-gate-verify-apiface-preflight):gate/derive 预检侧
|
|
1434
|
+
* 以同一解析器实况解析 design.md 注入(与 --done backfill 产物同形同源,零落盘)——在场时
|
|
1435
|
+
* 覆写 facts.apiFace 作解析产出集(过期 facts 不得赢),未注入行为与现状完全一致
|
|
1422
1436
|
* - 矩阵段 MD 槽解析(X-05 防篡改锚点:锚定当前文档实态)
|
|
1423
1437
|
* - resolveChangeRisk 声明面判级(design 文件清单 × blast 声明 + explicit,task-03)
|
|
1424
1438
|
* - design.md 权限矩阵段提取(fs 只读,壳非纯函数)
|
|
@@ -1441,9 +1455,18 @@ export function validateApiCoverageMatrix(cwd, changeName, context = {}) {
|
|
|
1441
1455
|
// 2026-09-19-ceremony-pricing-five-cuts task-03:声明面判级(design 文件清单 × blast 声明 +
|
|
1442
1456
|
// explicit;level 为五级词兼容字段——evidenceRequired → integration-critical)
|
|
1443
1457
|
const riskProfile = resolveChangeRiskForChangeDir(changeDir, context.specRoot || specRoot || join(cwd, '.sillyspec'))
|
|
1458
|
+
// gate/derive 预检注入(2026-10-14-gate-verify-apiface-preflight FR-01/FR-02):facts.apiFace
|
|
1459
|
+
// 唯一生产者是 run verify --done 收尾前置 backfill(run/gates.js),gate 路径不回填且 --init
|
|
1460
|
+
// 刻意不落键 → 预检 parseSet 空/旧 → design接口表# 锚点恒空指(平台 6 轮 gate 实证)。调用方
|
|
1461
|
+
// (machine-interface runGate/derive)以同一解析器实况解析 design.md 经 context.apiFaceLive
|
|
1462
|
+
// 传入(本模块零 import verify-probes 纪律:注入不做函数调用)——与 --done backfill 产物同形
|
|
1463
|
+
// 同源,在场时必须覆写 facts(过期 facts 不得赢),未注入时行为与现状完全一致。
|
|
1464
|
+
const apiFace = context.apiFaceLive && typeof context.apiFaceLive === 'object'
|
|
1465
|
+
? context.apiFaceLive
|
|
1466
|
+
: (facts && facts.apiFace && typeof facts.apiFace === 'object' ? facts.apiFace : null)
|
|
1444
1467
|
return judgeApiCoverageMatrix({
|
|
1445
1468
|
matrix: extractApiCoverageMatrixSlots(readFileSync(verifyResultPath, 'utf8')),
|
|
1446
|
-
apiFace
|
|
1469
|
+
apiFace,
|
|
1447
1470
|
consumerHints: facts && facts.consumerHints && typeof facts.consumerHints === 'object' ? facts.consumerHints : null,
|
|
1448
1471
|
facts,
|
|
1449
1472
|
factsExpected: true,
|
package/src/stage-review.js
CHANGED
|
@@ -139,7 +139,8 @@ export function renderReviewJsonContract({ stage, changeDir, reviewRunId, tier,
|
|
|
139
139
|
'',
|
|
140
140
|
'> 路径:`{SPEC_ROOT}/.runtime/stage-reviews/' + stageLbl + '-' + runIdLbl + '/review.json`(本次 reviewType=' + reviewType + ',主审查文档=' + mainDocRel + ')',
|
|
141
141
|
...(changeDir
|
|
142
|
-
? ['>
|
|
142
|
+
? ['> **本次完整绝对路径(直接复制给审查子代理——勿手拼、勿增删任何前缀)**: `' + join(dirname(dirname(changeDir)), '.runtime', 'stage-reviews', stageLbl + '-' + runIdLbl, 'review.json') + '`',
|
|
143
|
+
'> ⚠️ 坑 stage-review-path-prefix 双向实测:漏「<stage>-」前缀即错位(2026-10-10:marker 只有裸 runId,子代理按 runId 直写);**多加一层「review-」前缀同样错位**(2026-10-15 复发:手拼成 `<stage>-review-review-…`,空目录残渣+一轮纠正)。唯一可靠做法:整条路径直接粘贴。']
|
|
143
144
|
: []),
|
|
144
145
|
'',
|
|
145
146
|
'### 必填字段(validateStageReviewSchema 硬校验,缺任一即 schema 失败阻断)',
|
|
@@ -366,6 +367,31 @@ export function stageReviewMarkerPath(runtimeRoot, stage, changeName) {
|
|
|
366
367
|
return join(runtimeRoot, `current-stage-review-run-id-${suffix}`)
|
|
367
368
|
}
|
|
368
369
|
|
|
370
|
+
/**
|
|
371
|
+
* stage review 目录读侧归一(坑 stage-review-dir-naming-mismatch,2026-10-15 消费仓三阶段
|
|
372
|
+
* 连环实证):规范目录 = `<stage>-<runId>`(runId 自带 review- 前缀,拼出来恰一次 review 字样)。
|
|
373
|
+
* skill 文档字面 `<stage>-review-<run-id>` + 「run-id 必须 review- 前缀」诱导出两类错位目录:
|
|
374
|
+
* ① 裸 runId 目录(agent 拿 marker 值当目录名):<runId>/
|
|
375
|
+
* ② 双前缀目录(字面拼装):<stage>-review-<runId>/
|
|
376
|
+
* 本助手按 规范 → ②双前缀 → ①裸 runId 顺序探测 review.json 在场性,返回首个命中目录;全不在
|
|
377
|
+
* 则返回规范目录(保持既有「期望路径」报错文案语义)。只读不搬文件——写入侧恒规范路径,读侧
|
|
378
|
+
* 容错让 gate / refresh-hash / autoRefresh 对错位文件天然可用。
|
|
379
|
+
* @param {string} runtimeRoot - .runtime 绝对路径
|
|
380
|
+
* @param {string} stage - brainstorm|plan|execute
|
|
381
|
+
* @param {string} reviewRunId - run id(review-<ts>)
|
|
382
|
+
* @returns {string} review 目录绝对路径(容错命中或规范缺省)
|
|
383
|
+
*/
|
|
384
|
+
export function resolveStageReviewDir(runtimeRoot, stage, reviewRunId) {
|
|
385
|
+
const base = join(runtimeRoot, 'stage-reviews')
|
|
386
|
+
const canonical = join(base, `${stage}-${reviewRunId}`)
|
|
387
|
+
const doublePrefix = join(base, `${stage}-review-${reviewRunId}`)
|
|
388
|
+
const bare = join(base, reviewRunId)
|
|
389
|
+
for (const dir of [canonical, doublePrefix, bare]) {
|
|
390
|
+
if (existsSync(join(dir, 'review.json'))) return dir
|
|
391
|
+
}
|
|
392
|
+
return canonical
|
|
393
|
+
}
|
|
394
|
+
|
|
369
395
|
/**
|
|
370
396
|
* 获取最新的 stage review run id
|
|
371
397
|
*
|
|
@@ -395,33 +421,37 @@ export function getLatestStageReviewRunId(runtimeRoot, stage, changeName) {
|
|
|
395
421
|
}
|
|
396
422
|
} catch {}
|
|
397
423
|
|
|
398
|
-
// fallback:扫描 stage-reviews
|
|
424
|
+
// fallback:扫描 stage-reviews/ 目录取字典序最新(向后兼容无 marker 旧数据)。候选两形:
|
|
425
|
+
// 规范/双前缀 `<stage>-review-*` + 裸 runId `review-*`(坑 stage-review-dir-naming-mismatch
|
|
426
|
+
// 错位目录——裸目录不编码 stage,须 reviewType 归属校验防跨段误认)
|
|
399
427
|
const dir = join(runtimeRoot, 'stage-reviews')
|
|
400
428
|
if (!existsSync(dir)) return null
|
|
401
429
|
try {
|
|
402
430
|
const entries = readdirSync(dir, { withFileTypes: true })
|
|
403
|
-
.filter(e => e.isDirectory() && e.name.startsWith(`${stage}-review-`))
|
|
404
|
-
.map(e => e.name)
|
|
405
|
-
.sort()
|
|
431
|
+
.filter(e => e.isDirectory() && (e.name.startsWith(`${stage}-review-`) || e.name.startsWith('review-')))
|
|
432
|
+
.map(e => ({ name: e.name, runId: e.name.startsWith(`${stage}-`) ? e.name.slice(stage.length + 1) : e.name }))
|
|
433
|
+
.sort((a, b) => (a.name < b.name ? -1 : 1))
|
|
406
434
|
.reverse()
|
|
407
435
|
if (!entries[0]) return null
|
|
408
436
|
// cross-change 防护(execute 复盘 b1):changeName 提供时按 review.json 的 reviewedFiles[0] 归属过滤。
|
|
409
437
|
// 契约 reviewedFiles[0] = `changes/<change>/<mainDoc>`(renderReviewJsonContract),据此排除他变更的 review,
|
|
410
438
|
// 否则批量完成 marker 缺失时可能读到 proxy 等其他变更的 acceptance review 报错误导。
|
|
439
|
+
// 裸目录另加 reviewType 校验(STAGE_REVIEW_TYPE 段专属,防他 stage 的错位文件串台)。
|
|
411
440
|
if (changeName) {
|
|
412
441
|
const matching = entries.find(e => {
|
|
413
442
|
try {
|
|
414
|
-
const rj = JSON.parse(readFileSync(join(dir, e, 'review.json'), 'utf8'))
|
|
443
|
+
const rj = JSON.parse(readFileSync(join(dir, e.name, 'review.json'), 'utf8'))
|
|
444
|
+
if (!e.name.startsWith(`${stage}-`) && rj.reviewType && rj.reviewType !== STAGE_REVIEW_TYPE[stage]) return false
|
|
415
445
|
const r0 = Array.isArray(rj.reviewedFiles) ? String(rj.reviewedFiles[0]) : ''
|
|
416
446
|
return r0.includes(`changes/${changeName}/`)
|
|
417
447
|
} catch { return false }
|
|
418
448
|
})
|
|
419
|
-
if (matching) return matching.
|
|
449
|
+
if (matching) return matching.runId
|
|
420
450
|
console.warn(`⚠️ 无 ${stage} 的 review marker,且 stage-reviews/ 下无归属变更 ${changeName} 的 review(reviewedFiles 应含 changes/${changeName}/);为避免跨变更串台不再取最新,返回 null(fail-closed,由调用方报缺 review.json)`)
|
|
421
451
|
return null
|
|
422
452
|
}
|
|
423
|
-
//
|
|
424
|
-
return entries[0].
|
|
453
|
+
// 规范形 "plan-review-2026-..." 剥 `${stage}-` 前缀得 runId;裸形目录名即 runId
|
|
454
|
+
return entries[0].runId
|
|
425
455
|
} catch {
|
|
426
456
|
return null
|
|
427
457
|
}
|
|
@@ -613,7 +643,8 @@ export function validateStageReview(opts) {
|
|
|
613
643
|
const errors = []
|
|
614
644
|
const warnings = []
|
|
615
645
|
|
|
616
|
-
|
|
646
|
+
// 读侧目录归一(坑 stage-review-dir-naming-mismatch):容错命中裸 runId / 双前缀错位目录
|
|
647
|
+
const reviewDir = resolveStageReviewDir(runtimeRoot, stage, reviewRunId)
|
|
617
648
|
const reviewPath = join(reviewDir, 'review.json')
|
|
618
649
|
|
|
619
650
|
if (!reviewRunId || !existsSync(reviewPath)) {
|
|
@@ -705,7 +736,7 @@ export function validateStageReviewWithAutoRefresh(opts) {
|
|
|
705
736
|
&& result.errors.every(e => String(e).includes('docHash 与主审查文档'))
|
|
706
737
|
if (!mismatchOnly) return { result, autoRefreshed: false }
|
|
707
738
|
|
|
708
|
-
const reviewDir =
|
|
739
|
+
const reviewDir = resolveStageReviewDir(opts.runtimeRoot, opts.stage, opts.reviewRunId)
|
|
709
740
|
const reviewPath = join(reviewDir, 'review.json')
|
|
710
741
|
// 定位主文档并重算 hash(与 verifyStageReviewDocHash 同法:reviewedFiles[0] × searchDirs)
|
|
711
742
|
const primaryRel = Array.isArray(result.review.reviewedFiles) ? result.review.reviewedFiles[0] : null
|
|
@@ -922,8 +953,11 @@ export function registerStageReview({ changeName, stage, fromFile, cwd, platform
|
|
|
922
953
|
if (refreshHash) {
|
|
923
954
|
if (fromFile) throw new Error('register-stage-review: --refresh-hash 与 --from 互斥(refresh 就地更新既有 review,adopt 引入新文件)')
|
|
924
955
|
const existingRunId = getLatestStageReviewRunId(runtimeRoot, stage, changeName)
|
|
956
|
+
// 读侧目录归一(坑 stage-review-dir-naming-mismatch):--refresh-hash 按 marker 取 runId 后
|
|
957
|
+
// 经 resolveStageReviewDir 容错定位——review.json 落在裸 runId / 双前缀错位目录也能刷新,
|
|
958
|
+
// 不再恒报「无对应 run」。
|
|
925
959
|
const existingPath = existingRunId
|
|
926
|
-
? join(runtimeRoot,
|
|
960
|
+
? join(resolveStageReviewDir(runtimeRoot, stage, existingRunId), 'review.json')
|
|
927
961
|
: null
|
|
928
962
|
if (!existingPath || !existsSync(existingPath)) {
|
|
929
963
|
throw new Error(`register-stage-review: --refresh-hash 找不到 ${stage} 的既有 review.json(marker ${existingRunId || '无'} 无对应 run)——先去掉 --refresh-hash 生成骨架,或派审查子代理产出 review`)
|
package/src/stage-templates.js
CHANGED
|
@@ -120,6 +120,8 @@ created_at: <now-datetime>
|
|
|
120
120
|
- normalized_requirement: 可测试的约束
|
|
121
121
|
- impacts: [FR-01, task-01, verify-01]
|
|
122
122
|
- evidence: 用户回答轮次或代码/文档路径
|
|
123
|
+
- 否决理由: <一句话核心劣势>(status=rejected 时必填——distill 入库前置要求,缺失则该条目不进知识库防复潮通道)
|
|
124
|
+
- 复潮条件: <何种信号下可重议本方案>(status=rejected 时必填)
|
|
123
125
|
\`\`\`
|
|
124
126
|
|
|
125
127
|
> 可选字段(按需另加,旧格式决策缺这些字段不受影响):**锚点**(决策落点主文件,\`<src 路径>:<行号或符号>\`;status=confirmed 时必填);**模块域**(决策涉及的模块 ID,可多个逗号分隔——合法 id 只取**当前变更所属项目**的 \`{SPEC_ROOT}/docs/<project>/modules/_module-map.yaml\`,多项目仓勿读其他子项目的 map(核验不认,报错会点名归属);规划中的新模块用 \`NEW:<名>\` 前缀声明,冒号后不加空格:\`NEW:foo\` 合法、\`NEW: foo\` 属书写错误。「生成规范文件」步 --done 的模块域核验对不认识的 id 直接阻断);**否决理由**(status=rejected 时必填);**复潮条件**(status=rejected 时必填);**故障面**(本决策引入的新失败模式——新机制落地时留痕它引入什么失败模式;type=architecture 时建议填写,可选);**退役判据**(出现什么信号时简化或删除本机制——信号出现就该简化它;type=architecture 时建议填写,可选)。
|