sillyspec 3.32.9 → 3.32.10
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 +2 -2
- package/.claude/skills/sillyspec-execute/SKILL.md +2 -2
- package/.claude/skills/sillyspec-flow/SKILL.md +1 -1
- package/.claude/skills/sillyspec-plan/SKILL.md +2 -2
- package/AGENTS.md +1 -1
- package/assets/command-cards/flow.md +1 -1
- package/package.json +1 -1
- package/src/commit-suggest.js +45 -4
- package/src/config-schema.js +14 -0
- package/src/decision-distill.js +77 -21
- package/src/decision-renumber.js +157 -0
- package/src/docs-check.js +30 -15
- package/src/docs-debt.js +3 -2
- package/src/doctor-diagnostics.js +3 -1
- package/src/flow-parity.js +18 -2
- package/src/flow.js +75 -12
- package/src/fr-index.js +15 -1
- package/src/hooks/worktree-guard.js +3 -3
- package/src/index.js +7 -5
- package/src/knowledge-digest.js +23 -6
- package/src/knowledge-graph.js +92 -14
- 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/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 +93 -89
- package/src/run/prompt.js +3 -3
- package/src/run/stage.js +1 -1
- package/src/sentinel-assertions.js +43 -1
- package/src/stage-contract.js +26 -3
- package/src/stage-review.js +44 -11
- package/src/stage-templates.js +2 -0
- package/src/stages/brainstorm.js +5 -1
- package/src/stages/knowledge.js +56 -5
- package/src/sync.js +28 -2
- package/src/task-tick.js +12 -0
- package/src/timeline.js +60 -19
- package/src/verify-postcheck.js +49 -1
- package/src/verify-probes.js +61 -18
- package/templates/agents-instruction.md +1 -1
package/src/knowledge-match.js
CHANGED
|
@@ -61,7 +61,7 @@ function isDecisionRoute(entry) {
|
|
|
61
61
|
return /^decisions\//.test(String(entry.file || '').replace(/\\/g, '/'))
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
const DECISION_HEADER_RE = /^##\s+(D-\d+@v\d+)\s*(.*)$/
|
|
64
|
+
const DECISION_HEADER_RE = /^##\s+(D-(?:[A-Za-z0-9_\-\u4e00-\u9fff]+-)?\d+@v\d+)\s*(.*)$/
|
|
65
65
|
const DECISION_FIELD_RE = /^(状态|文件|理由|否决理由|复潮条件|supersedes)\s*[::]\s*(.*)$/
|
|
66
66
|
// 「锚点:」单独标签精确匹配读入(仿 docs-check.js parseDecisionEntries 先例)——绝不并入
|
|
67
67
|
// DECISION_FIELD_RE:其 else-if 链末尾的 reason 回填(else if (!cur.reason))会把锚点值
|
|
@@ -69,6 +69,12 @@ const DECISION_FIELD_RE = /^(状态|文件|理由|否决理由|复潮条件|supe
|
|
|
69
69
|
const DECISION_ANCHOR_RE = /^锚点\s*[::]\s*(.*)$/
|
|
70
70
|
// 变更:单独标签(变更名限定——D-xxx 是变更内局部序号,跨变更同号互不相干,去重键依赖它)
|
|
71
71
|
const DECISION_CHANGE_RE = /^变更\s*[::]\s*(.*)$/
|
|
72
|
+
// 来源:兜底标签(2026-10-14-decision-id-p2-hardening):2026-08-24 首修形态残留(后被「变更:」
|
|
73
|
+
// 取代,现存 20 行)——变更行无条件覆盖在前,来源仅补缺(任意行序下变更优先)
|
|
74
|
+
const DECISION_ORIGIN_RE = /^来源\s*[::]\s*(.*)$/
|
|
75
|
+
// 原编号:溯源标签(2026-10-14-decision-global-id-epoch):epoch 形态条目的局部号——graph
|
|
76
|
+
// supersedes 经它把「supersedes:D-011@v1」解析到同变更全局号段
|
|
77
|
+
const DECISION_ORIGINID_RE = /^原编号\s*[::]\s*(.*)$/
|
|
72
78
|
|
|
73
79
|
/** 「文件:」值切分 + 逐项反斜杠归一 POSIX(与 decision-distill 的 entry.files 契约对齐:
|
|
74
80
|
* split /[,,、\s]+/ + replace(/\\/g,'/'))。 */
|
|
@@ -133,7 +139,7 @@ function parseDecisionFile(filePath, file) {
|
|
|
133
139
|
const h = line.match(DECISION_HEADER_RE)
|
|
134
140
|
if (h) {
|
|
135
141
|
flush()
|
|
136
|
-
cur = { file, id: h[1], title: h[2].trim(), status: '', reason: '', revisitWhen: '', files: [], anchor: '', change: '', supersedesText: '' }
|
|
142
|
+
cur = { file, id: h[1], title: h[2].trim(), status: '', reason: '', revisitWhen: '', files: [], anchor: '', change: '', supersedesText: '', originId: '' }
|
|
137
143
|
continue
|
|
138
144
|
}
|
|
139
145
|
if (!cur) continue
|
|
@@ -144,6 +150,10 @@ function parseDecisionFile(filePath, file) {
|
|
|
144
150
|
// 域文件跨变更同号条目常见(D-xxx 是变更内局部序号),去重键须含变更名否则先到条目遮蔽后来者)
|
|
145
151
|
const c = line.match(DECISION_CHANGE_RE)
|
|
146
152
|
if (c) { cur.change = c[1].trim(); continue }
|
|
153
|
+
const o = line.match(DECISION_ORIGIN_RE)
|
|
154
|
+
if (o) { if (!cur.change) cur.change = o[1].trim(); continue }
|
|
155
|
+
const oi = line.match(DECISION_ORIGINID_RE)
|
|
156
|
+
if (oi) { cur.originId = oi[1].trim(); continue }
|
|
147
157
|
const f = line.match(DECISION_FIELD_RE)
|
|
148
158
|
if (!f) continue
|
|
149
159
|
const value = f[2].trim()
|
|
@@ -170,20 +180,25 @@ function parseDecisionFile(filePath, file) {
|
|
|
170
180
|
export function parseDecisionEntries(indexDir, indexEntries = null) {
|
|
171
181
|
const routes = (indexEntries || parseKnowledgeIndex(indexDir)).filter(isDecisionRoute)
|
|
172
182
|
const hits = []
|
|
183
|
+
// 路由行按文件去重单解析(2026-10-14-decision-id-p2-hardening):多条路由行指向同一域文件只解析
|
|
184
|
+
// 一次——此前靠条目级折叠兜完全重复,legacy 不折叠后从源头消除重复
|
|
185
|
+
const parsed = new Set()
|
|
173
186
|
for (const route of routes) {
|
|
187
|
+
const fileKey = String(route.file || '').replace(/\\/g, '/')
|
|
188
|
+
if (parsed.has(fileKey)) continue
|
|
189
|
+
parsed.add(fileKey)
|
|
174
190
|
const filePath = join(indexDir, route.file)
|
|
175
191
|
if (!existsSync(filePath)) continue // 路由行失效(域文件已删)→ 跳过不阻断
|
|
176
192
|
hits.push(...parseDecisionFile(filePath, route.file))
|
|
177
193
|
}
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
// 注入同样被遮蔽)。无「变更:」行的 legacy 条目 change='' 与同号 legacy 条目互折叠
|
|
183
|
-
// (维持旧行为,不放大历史条目数)。
|
|
194
|
+
// 带 change 条目按 file+id+change 去重(坑 decision-cross-change-id-shadow,2026-09-11 dogfood 实证:
|
|
195
|
+
// 域文件跨变更同号条目常见,键须含变更名否则先到遮蔽——runtime.md 七个 D-001@v1 分属七变更)。
|
|
196
|
+
// 无「变更:/来源:」行的 legacy 条目不再互折叠(2026-10-14-decision-id-p2-hardening:旧互折叠致
|
|
197
|
+
// 同号 legacy 互相遮蔽——实测 2 条 implemented 条目永久隐身;路由单解析后完全重复已从源头消除)。
|
|
184
198
|
const seen = new Set()
|
|
185
199
|
return hits.filter(h => {
|
|
186
|
-
|
|
200
|
+
if (!h.change) return true
|
|
201
|
+
const key = `${h.file}#${h.id}#${h.change}`
|
|
187
202
|
if (seen.has(key)) return false
|
|
188
203
|
seen.add(key)
|
|
189
204
|
return true
|
|
@@ -221,6 +236,16 @@ export function matchDecisionsByFiles(indexDir, files) {
|
|
|
221
236
|
return result
|
|
222
237
|
}
|
|
223
238
|
|
|
239
|
+
/** 决策展示行变更名限定词(2026-10-14-supersedes-mislink-and-change-display):D-xxx 是变更内
|
|
240
|
+
* 局部序号,跨变更同号条目常见(知识库 D-001@v1 ×296、其中 192 条同题)——四处展示面
|
|
241
|
+
* ({DECISION_HITS}/轻量道防复潮/knowledge-gate/decision-touch)经本函数统一渲染限定词消歧;
|
|
242
|
+
* change 缺失(legacy 无「变更:」行)固定降级「变更未记录」——不编造、不省略。
|
|
243
|
+
* 单一源防四处文案漂移(与 deathPathNote 同址同规约)。 */
|
|
244
|
+
export function decisionChangeQualifier(change) {
|
|
245
|
+
const c = String(change || '').trim()
|
|
246
|
+
return c ? `变更 ${c}` : '变更未记录'
|
|
247
|
+
}
|
|
248
|
+
|
|
224
249
|
/** 死路注记短句提取(消费方渲染用):理由里「死路:…」到首个句读为止;无标记时截前 60 字。
|
|
225
250
|
* 单一源供 flow 知识注入段与 complete.js knowledge-gate 回显共用(2026-09-28-knowledge-inject-ranking)。 */
|
|
226
251
|
export function deathPathNote(reason) {
|
package/src/knowledge-stats.js
CHANGED
|
@@ -99,6 +99,10 @@ export function buildHitMatrix(knowledgeDir, runtimeDir, { sinceDays = 30 } = {}
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
// ── FR 索引实验聚合(2026-09-18-fr-index-l1 后续钩子·L3 裁决仪表盘)──
|
|
102
|
+
// L3 已裁决冻结(2026-10-15-fr-chain-l3-adjudication D-001@v1):证伪三条件全中(承接引用率
|
|
103
|
+
// 1/452≈0% / fr-supersede 窗口 2 次 / fr-duplicate-warning 24 次零候选),条款明文合法结局——
|
|
104
|
+
// 活规格树不建,fr 索引降级保留检索/注入面;本区指标转历史观察,候选视图退役为冻结注记。
|
|
105
|
+
const FR_L3_ADJUDICATED_NOTE = '已裁决(2026-10-15 D-001@v1):L3 冻结不建,fr 索引保留检索/注入面——本区指标转历史观察(复潮条件见裁决条目)'
|
|
102
106
|
|
|
103
107
|
/**
|
|
104
108
|
* 聚合 L1 四类 fr-* 遥测事件 + knowledge/fr/ 索引面统计——L3 裁决(design 实验证伪条款)
|
|
@@ -314,7 +318,7 @@ export async function cmdKnowledgeStats(dir, args, opts = {}) {
|
|
|
314
318
|
if (frIndex.present) {
|
|
315
319
|
const e = frIndex.events
|
|
316
320
|
const i = frIndex.index
|
|
317
|
-
lines.push(`🔬 FR 索引实验(窗口内事件 / 索引面全量——L3 裁决指标,证伪条款见 2026-09-18-fr-index-l1 design
|
|
321
|
+
lines.push(`🔬 FR 索引实验(窗口内事件 / 索引面全量——L3 裁决指标,证伪条款见 2026-09-18-fr-index-l1 design)|${FR_L3_ADJUDICATED_NOTE}`)
|
|
318
322
|
lines.push(` 注入 fr-inject ${e.frInject} 次(${e.frInjectChanges} 变更) | 取代链 fr-supersede ${e.frSupersede} 次(${e.frSupersedeChanges} 变更) | 重复拦截 fr-duplicate-warning ${e.frDuplicateWarning} 次(${e.frDuplicateWarningCandidates || e.frDuplicateCandidates || 0} 候选)`)
|
|
319
323
|
lines.push(` 腐烂 suspect fr-rot-suspect ${e.frRotSuspect || 0} 次(${e.frRotSuspectChanges || 0} 变更${e.frRotSuspectByDomain && e.frRotSuspectByDomain.length > 0 ? `,按域 ${e.frRotSuspectByDomain.map(d => `${d.domain}×${d.count}`).join(',')}` : ''})`)
|
|
320
324
|
if (e.frUnreferenced.length > 0) {
|
|
@@ -325,10 +329,8 @@ export async function cmdKnowledgeStats(dir, args, opts = {}) {
|
|
|
325
329
|
lines.push(` 承接引用率:${frIndex.supersedeRate === null ? '—(索引空)' : `${(frIndex.supersedeRate * 100).toFixed(0)}%(${e.frSupersedeChanges}/${i.sourceChanges},分子窗口内/分母全量——裁决用大窗口)`}`)
|
|
326
330
|
const cand = frIndex.adjudicationCandidates || []
|
|
327
331
|
if (cand.length > 0) {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
lines.push(` - ${c.id} suspect×${c.suspect} unref×${c.unreferenced}${c.change ? `(来源 ${c.change})` : ''}`)
|
|
331
|
-
}
|
|
332
|
+
// L3 冻结后候选视图退役(数据面 adjudicationCandidates 保留 JSON 可查)——显示冻结注记
|
|
333
|
+
lines.push(` 裁决候选视图已退役|${FR_L3_ADJUDICATED_NOTE}`)
|
|
332
334
|
}
|
|
333
335
|
} else {
|
|
334
336
|
lines.push('(FR 索引面不存在——knowledge/fr/ 目录缺失或无条目;归档完整流程变更后此处可查)')
|
|
@@ -375,12 +377,12 @@ export async function cmdKnowledgeStats(dir, args, opts = {}) {
|
|
|
375
377
|
|
|
376
378
|
lines.push(`遥测计数(窗口内):注入 ${result.totalInjects} 次 | 归类 ${result.totalClassifies} 次${lastEventAt ? ` | 数据截至 ${lastEventAt.slice(0, 10)}` : ''}`)
|
|
377
379
|
|
|
378
|
-
// FR 索引实验(L3
|
|
380
|
+
// FR 索引实验(L3 裁决仪表盘——证伪条款出数面;L3 已裁决冻结 2026-10-15,指标转历史观察)
|
|
379
381
|
if (frIndex.present) {
|
|
380
382
|
const e = frIndex.events
|
|
381
383
|
const i = frIndex.index
|
|
382
384
|
lines.push('')
|
|
383
|
-
lines.push(`🔬 FR 索引实验(窗口内事件 / 索引面全量——L3 裁决指标,证伪条款见 2026-09-18-fr-index-l1 design
|
|
385
|
+
lines.push(`🔬 FR 索引实验(窗口内事件 / 索引面全量——L3 裁决指标,证伪条款见 2026-09-18-fr-index-l1 design)|${FR_L3_ADJUDICATED_NOTE}`)
|
|
384
386
|
lines.push(` 注入 fr-inject ${e.frInject} 次(${e.frInjectChanges} 变更) | 取代链 fr-supersede ${e.frSupersede} 次(${e.frSupersedeChanges} 变更) | 重复拦截 fr-duplicate-warning ${e.frDuplicateWarning} 次(${e.frDuplicateCandidates} 候选)`)
|
|
385
387
|
if (e.frUnreferenced.length > 0) {
|
|
386
388
|
const top = e.frUnreferenced.slice(0, 5).map((u) => `${u.domain}×${u.count}`).join(',')
|
|
@@ -388,14 +390,11 @@ export async function cmdKnowledgeStats(dir, args, opts = {}) {
|
|
|
388
390
|
}
|
|
389
391
|
lines.push(` 索引面:${i.entries} 条(active ${i.active} / superseded ${i.superseded})| 来源变更 ${i.sourceChanges} 个 | 域 ${i.domains.join(', ') || '—'}`)
|
|
390
392
|
lines.push(` 承接引用率:${frIndex.supersedeRate === null ? '—(索引空)' : `${(frIndex.supersedeRate * 100).toFixed(0)}%(${e.frSupersedeChanges}/${i.sourceChanges},分子窗口内/分母全量——裁决用大窗口)`}`)
|
|
391
|
-
//
|
|
392
|
-
//
|
|
393
|
+
// 裁决候选视图退役(L3 冻结,2026-10-15-fr-chain-l3-adjudication D-001@v1)——数据面
|
|
394
|
+
// adjudicationCandidates 保留 JSON 可查,显示面只留冻结注记。
|
|
393
395
|
const cand = frIndex.adjudicationCandidates || []
|
|
394
396
|
if (cand.length > 0) {
|
|
395
|
-
lines.push(
|
|
396
|
-
for (const c of cand.slice(0, 10)) {
|
|
397
|
-
lines.push(` - ${c.id} suspect×${c.suspect} unref×${c.unreferenced}${c.change ? `(来源 ${c.change})` : ''}`)
|
|
398
|
-
}
|
|
397
|
+
lines.push(` 裁决候选视图已退役|${FR_L3_ADJUDICATED_NOTE}`)
|
|
399
398
|
}
|
|
400
399
|
}
|
|
401
400
|
|
package/src/knowledge-vector.js
CHANGED
|
@@ -18,20 +18,65 @@
|
|
|
18
18
|
* "change": "2026-09-26-thin-agent-tasks", "score": 0.83 } ] }
|
|
19
19
|
* anchor = 决策条目 id(## D-xxx@vN);change = 条目「变更:」字段——同号条目跨变更常见
|
|
20
20
|
* (runtime.md 七个 D-001@v1 先例),change 在场时消歧;缺省时同号条目全量带回(候选诚实)。
|
|
21
|
+
* epoch 注记(2026-10-14-decision-global-id-epoch):新蒸馏条目 anchor 为全局号形态
|
|
22
|
+
* D-<域>-NNN@vN——等值匹配语义不变;平台重建索引窗口内向量召回对新形态条目降级
|
|
23
|
+
* (本地词面回退不受影响),重索引后恢复。
|
|
21
24
|
*
|
|
22
25
|
* 降级纪律:任何失败(未连接/开关关闭/网络/404/5xx/超时/响应形态不符)静默降级本地层
|
|
23
26
|
* ——检索是 advisory 面且高频(每方案步 --done),失败刷屏即狼来了;debug 排障可设
|
|
24
27
|
* SILLYSPEC_KNOWLEDGE_VECTOR_DEBUG=1 打单行stderr。
|
|
28
|
+
*
|
|
29
|
+
* 开关契约(2026-10-15-vector-capability-autodetect 终态;前身 vector-gate-tighten 缺省关是
|
|
30
|
+
* 「端点不存在+无发现机制」时代的正确补丁,平台端点落地后 auto 成为诚实缺省):
|
|
31
|
+
* 缺省(未写键/auto)跟随平台能力自动探测——能力缓存 .runtime/vector-capability.json
|
|
32
|
+
* (404→unavailable 重试窗 1h 内跳过;成功→available;按平台 URL 匹配;跨进程持久),
|
|
33
|
+
* 平台部署端点后最迟 1h 全仓自动启用,消费仓零配置。local.yaml on=强制开跳过探测、
|
|
34
|
+
* off=强制关(用户逃生阀);env SILLYSPEC_KNOWLEDGE_VECTOR 同语义强制。
|
|
35
|
+
* 404 另有进程级死信短路(端点死信不再空转探测)。
|
|
25
36
|
*/
|
|
26
|
-
import { readFileSync } from 'fs'
|
|
37
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
|
|
27
38
|
import { join, dirname } from 'path'
|
|
28
39
|
import { readPlatformConfig } from './sync.js'
|
|
29
40
|
import { matchByRouting, fallbackByQueryShingles, parseDecisionEntries } from './knowledge-match.js'
|
|
30
41
|
|
|
31
42
|
const VECTOR_TIMEOUT_MS = 3000
|
|
32
43
|
const ENDPOINT_PATH = '/api/spec/knowledge/vector-search'
|
|
44
|
+
/** 能力缓存重试窗:unavailable 记录 1h 内不再探测(平台部署后最迟 1h 自动启用)。 */
|
|
45
|
+
const CAPABILITY_RETRY_MS = 60 * 60 * 1000
|
|
46
|
+
|
|
47
|
+
// ── 平台能力探测缓存(2026-10-15-vector-capability-autodetect)────────────────
|
|
48
|
+
// .runtime/ 已 gitignored 且被 daemon spec 上传排除(UPLOAD_EXCLUDE_TOP_BASE),是仓内
|
|
49
|
+
// 机器态的标准落点(knowledge-hits 同款)。写盘全 fail-soft;按平台 URL 匹配(换平台即失效)。
|
|
50
|
+
|
|
51
|
+
function capabilityCachePath(cwd) {
|
|
52
|
+
return join(cwd, '.sillyspec', '.runtime', 'vector-capability.json')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function readCapabilityCache(cwd) {
|
|
56
|
+
try {
|
|
57
|
+
const obj = JSON.parse(readFileSync(capabilityCachePath(cwd), 'utf-8'))
|
|
58
|
+
const cfg = readPlatformConfig(cwd)
|
|
59
|
+
if (!cfg || !cfg.url) return null
|
|
60
|
+
if (obj.url !== String(cfg.url).replace(/\/+$/, '')) return null // 换平台 → 缓存失效
|
|
61
|
+
if (typeof obj.available !== 'boolean' || typeof obj.probedAt !== 'number') return null
|
|
62
|
+
return { available: obj.available, probedAt: obj.probedAt }
|
|
63
|
+
} catch { /* 无缓存/坏 JSON = 未探测 */ }
|
|
64
|
+
return null
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function writeCapabilityCache(cwd, platformUrl, available) {
|
|
68
|
+
try {
|
|
69
|
+
const p = capabilityCachePath(cwd)
|
|
70
|
+
const cur = readCapabilityCache(cwd)
|
|
71
|
+
if (cur && cur.available === available && Date.now() - cur.probedAt < CAPABILITY_RETRY_MS) return // 值未变且未过期:零写盘
|
|
72
|
+
const dir = join(p, '..')
|
|
73
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
|
|
74
|
+
writeFileSync(p, JSON.stringify({ url: platformUrl, available, probedAt: Date.now() }), 'utf-8')
|
|
75
|
+
} catch { /* 缓存写失败不影响检索本体(fail-soft) */ }
|
|
76
|
+
}
|
|
33
77
|
|
|
34
|
-
/** local.yaml knowledge.vector_search 开关(行级扫描 knowledge: 段下 vector_search
|
|
78
|
+
/** local.yaml knowledge.vector_search 开关(行级扫描 knowledge: 段下 vector_search 值;
|
|
79
|
+
* 未写键/无 yaml → null=未表态——2026-10-15-vector-gate-tighten 起缺省关,显式落盘才启用)。 */
|
|
35
80
|
function vectorSwitchFromLocalYaml(cwd) {
|
|
36
81
|
try {
|
|
37
82
|
const lines = readFileSync(join(cwd, '.sillyspec', 'local.yaml'), 'utf-8').split(/\r?\n/)
|
|
@@ -43,16 +88,55 @@ function vectorSwitchFromLocalYaml(cwd) {
|
|
|
43
88
|
const kv = line.match(/^\s+vector_search\s*:\s*(\S+)/)
|
|
44
89
|
if (kv) return kv[1].toLowerCase()
|
|
45
90
|
}
|
|
46
|
-
} catch { /* 无 local.yaml =
|
|
47
|
-
return
|
|
91
|
+
} catch { /* 无 local.yaml = 未表态 */ }
|
|
92
|
+
return null
|
|
48
93
|
}
|
|
49
94
|
|
|
95
|
+
/**
|
|
96
|
+
* 向量层开关(2026-10-15-vector-capability-autodetect 终态契约;前身 2026-10-15-vector-gate-tighten
|
|
97
|
+
* 缺省关是「端点不存在+无发现机制」时代的正确补丁,端点落地后 auto 成为诚实缺省):
|
|
98
|
+
* env SILLYSPEC_KNOWLEDGE_VECTOR=on|auto → 强制启用;off(其他值保守视为 off)→ 强制关;
|
|
99
|
+
* local.yaml knowledge.vector_search: on → 强制启用;off → 强制关(用户显式逃生阀);
|
|
100
|
+
* auto/未写键 → **跟随平台能力自动探测**:读能力缓存(.runtime/vector-capability.json,
|
|
101
|
+
* {url,available,probedAt})——unavailable 未过重试窗(1h)=关,否则乐观试(探测结果由
|
|
102
|
+
* platformVectorRecall 落缓存,404→unavailable / 成功→available;跨进程持久)。
|
|
103
|
+
* 「端点部署本身就是开关」——平台部署后最迟 1h 内全仓自动启用,消费仓零配置。
|
|
104
|
+
*/
|
|
50
105
|
function vectorEnabled(cwd) {
|
|
51
106
|
const env = String(process.env.SILLYSPEC_KNOWLEDGE_VECTOR || '').toLowerCase()
|
|
52
|
-
if (env === '
|
|
53
|
-
|
|
107
|
+
if (env === 'on' || env === 'auto') return true
|
|
108
|
+
if (env) return false
|
|
109
|
+
const sw = vectorSwitchFromLocalYaml(cwd)
|
|
110
|
+
if (sw === 'off') return false
|
|
111
|
+
if (sw === 'on') return true
|
|
112
|
+
// absent/auto → 能力缓存判定(无平台配置=没得探测=关)
|
|
113
|
+
if (!readPlatformConfig(cwd)) return false
|
|
114
|
+
const cap = readCapabilityCache(cwd)
|
|
115
|
+
if (cap && !cap.available && Date.now() - cap.probedAt < CAPABILITY_RETRY_MS) return false
|
|
116
|
+
return true
|
|
54
117
|
}
|
|
55
118
|
|
|
119
|
+
/**
|
|
120
|
+
* 向量能力提示(2026-10-15-vector-capability-autodetect 语义随迁;前身 vector-switch-discoverability
|
|
121
|
+
* 的「未开请配置」提示随缺省 auto 退役——auto 模式下没有可执行配置,提示转为信息性):
|
|
122
|
+
* 仅当 auto 模式(无显式键/键=auto)且能力缓存记 unavailable 时给一行信息——「平台部署后
|
|
123
|
+
* 自动启用,无需本仓配置」。强制关(yaml off/env off=用户显式选择)与已启用 → ''(零噪音)。
|
|
124
|
+
* 平台未连接 → ''。
|
|
125
|
+
*/
|
|
126
|
+
export function vectorSwitchHint(cwd) {
|
|
127
|
+
if (!readPlatformConfig(cwd)) return ''
|
|
128
|
+
if (vectorEnabled(cwd)) return ''
|
|
129
|
+
const cap = readCapabilityCache(cwd)
|
|
130
|
+
if (cap && !cap.available) {
|
|
131
|
+
return `ℹ️ 平台向量语义检索未就绪(自动探测缓存生效中)——平台部署 vector-search 端点后最迟 1 小时内本仓自动启用,无需配置`
|
|
132
|
+
}
|
|
133
|
+
return ''
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** 404 死信端点(进程级,按平台 URL 记):端点未实现=今天真实平台状态,标记后本进程不再
|
|
137
|
+
* 对同 URL 空转探测(CLI 短进程生命周期天然重置;测试各用独立端口互不污染)。 */
|
|
138
|
+
const vectorDeadEndpoints = new Set()
|
|
139
|
+
|
|
56
140
|
/**
|
|
57
141
|
* 平台向量召回(纯 HTTP 层)。未连接/失败/形态不符 → null(调用方降级)。
|
|
58
142
|
* @returns {Promise<{results: Array<{spec_path: string, anchor: string, score: number}>}|null>}
|
|
@@ -60,19 +144,26 @@ function vectorEnabled(cwd) {
|
|
|
60
144
|
export async function platformVectorRecall({ cwd, query, limit = 10, timeoutMs = VECTOR_TIMEOUT_MS }) {
|
|
61
145
|
const cfg = readPlatformConfig(cwd)
|
|
62
146
|
if (!cfg || !cfg.url || !cfg.token) return null
|
|
147
|
+
const base = String(cfg.url).replace(/\/+$/, '')
|
|
148
|
+
if (vectorDeadEndpoints.has(base)) return null
|
|
63
149
|
const q = String(query || '').trim().slice(0, 4000)
|
|
64
150
|
if (!q) return null
|
|
65
151
|
try {
|
|
66
|
-
const res = await fetch(`${
|
|
152
|
+
const res = await fetch(`${base}${ENDPOINT_PATH}`, {
|
|
67
153
|
method: 'POST',
|
|
68
154
|
headers: { Authorization: `Bearer ${cfg.token}`, 'Content-Type': 'application/json' },
|
|
69
155
|
body: JSON.stringify({ query: q, limit }),
|
|
70
156
|
signal: AbortSignal.timeout(timeoutMs),
|
|
71
157
|
})
|
|
72
158
|
if (!res.ok) {
|
|
159
|
+
if (res.status === 404) {
|
|
160
|
+
vectorDeadEndpoints.add(base) // 端点未实现——本进程短路后续探测
|
|
161
|
+
writeCapabilityCache(cwd, base, false) // 跨进程持久:重试窗内不再探测(平台部署后自动启用)
|
|
162
|
+
}
|
|
73
163
|
if (process.env.SILLYSPEC_KNOWLEDGE_VECTOR_DEBUG) console.error(`[knowledge-vector] ${res.status} ${ENDPOINT_PATH}——降级本地`)
|
|
74
164
|
return null
|
|
75
165
|
}
|
|
166
|
+
writeCapabilityCache(cwd, base, true) // 端点在场——缓存 available(后续进程零探测直用)
|
|
76
167
|
const body = await res.json().catch(() => null)
|
|
77
168
|
const results = body && Array.isArray(body.results)
|
|
78
169
|
? body.results.filter((r) => r && typeof r.spec_path === 'string' && typeof r.anchor === 'string')
|
package/src/machine-interface.js
CHANGED
|
@@ -187,10 +187,25 @@ export async function runGate(stage, changeName, { cwd, specBase, runtimeRoot, s
|
|
|
187
187
|
// 各 spawn 2 个 git 进程(gate execute 一次省 ≈ 60-200ms on Windows)。
|
|
188
188
|
const sharedEvidence = stage === 'execute' ? checkExecuteCodeEvidence(cwd, changeName) : undefined;
|
|
189
189
|
|
|
190
|
+
// ── gate 预检实况注入(2026-10-14-gate-verify-apiface-preflight FR-01~FR-03):facts.apiFace
|
|
191
|
+
// 唯一生产者是 run verify --done 收尾前置 backfill(run/gates.js),gate 路径不回填且 --init
|
|
192
|
+
// 刻意不落键 → 锚点核对 parseSet 空/旧 → design接口表# 锚点恒空指(平台 6 轮 gate 实证)。
|
|
193
|
+
// 此处以同一解析器实况解析 design.md 经 context 注入 validator(与 --done backfill 产物同形
|
|
194
|
+
// 同源,零落盘守 D-002 只读契约);design.md 缺失/不可读 helper 返 null、解析异常 fail-soft
|
|
195
|
+
// ——两种态都不注入,退回 facts 口径(现状行为;评审 P1:空面注入会覆写有效 facts 反致误阻断)。──
|
|
196
|
+
let apiFaceLive = null;
|
|
197
|
+
if (stage === 'verify') {
|
|
198
|
+
try {
|
|
199
|
+
const { computeApiFaceFromDesign } = await import('./verify-probes.js');
|
|
200
|
+
apiFaceLive = computeApiFaceFromDesign(join(specRoot, 'changes', changeName, 'design.md'));
|
|
201
|
+
} catch { /* fail-soft:不注入退回 facts 口径 */ }
|
|
202
|
+
}
|
|
203
|
+
|
|
190
204
|
// ── a. artifacts:阶段产物校验(全部阶段)──
|
|
191
205
|
const r = runValidators(stage, cwd, changeName, {
|
|
192
206
|
projectName: progress.project,
|
|
193
207
|
specRoot,
|
|
208
|
+
...(apiFaceLive ? { apiFaceLive } : {}),
|
|
194
209
|
...(sharedEvidence ? { evidence: sharedEvidence } : {}),
|
|
195
210
|
});
|
|
196
211
|
checks.push({
|
|
@@ -752,10 +767,21 @@ export async function runDerive(facet, changeName, { cwd, specBase, runtimeRoot,
|
|
|
752
767
|
|
|
753
768
|
// ── d. artifacts:当前阶段产物校验 ──
|
|
754
769
|
// runValidators 需要 stage:用 progress.currentStage(若为空串则校验器内部处理)。
|
|
770
|
+
// verify 阶段同款实况注入(2026-10-14-gate-verify-apiface-preflight):与 runGate 同因——
|
|
771
|
+
// facts.apiFace 唯一生产者是 --done backfill,derive 不回填 → 锚点核对恒空指;零落盘 +
|
|
772
|
+
// fail-soft(缺失返 null/异常两态都不注入退回 facts 口径),守 D-002 只读契约。
|
|
755
773
|
case 'artifacts': {
|
|
774
|
+
let apiFaceLive = null;
|
|
775
|
+
if (currentStage === 'verify') {
|
|
776
|
+
try {
|
|
777
|
+
const { computeApiFaceFromDesign } = await import('./verify-probes.js');
|
|
778
|
+
apiFaceLive = computeApiFaceFromDesign(join(specRoot, 'changes', changeName, 'design.md'));
|
|
779
|
+
} catch { /* fail-soft:不注入退回 facts 口径 */ }
|
|
780
|
+
}
|
|
756
781
|
const r = runValidators(currentStage, cwd, changeName, {
|
|
757
782
|
projectName: progress.project,
|
|
758
783
|
specRoot,
|
|
784
|
+
...(apiFaceLive ? { apiFaceLive } : {}),
|
|
759
785
|
});
|
|
760
786
|
data = { ok: r.ok, errors: r.errors, warnings: r.warnings };
|
|
761
787
|
ok = r.ok;
|
package/src/quicklog.js
CHANGED
|
@@ -124,13 +124,18 @@ export function sanitizeDesc(description) {
|
|
|
124
124
|
* 「需求:/动机:」类标签前缀,空白折叠单行,超长截断加 …;空输入返回 ''(调用方兜底
|
|
125
125
|
* change 名)。agent 在 --input 首行写一句中文概括即成标题(flow start 横幅有指引),
|
|
126
126
|
* --title 显式覆盖一切推导。
|
|
127
|
+
* 段头行防御(2026-10-15-title-label-skip):过门格式示例曾被读成段头形态——agent 把
|
|
128
|
+
* 「动机与背景:」独立成行照抄,白名单无复合词致整行成标题(进度库 6 变更实证)。两道
|
|
129
|
+
* 防御:白名单补复合词(动机与背景/背景与动机);任意短段头行(≤12 字非冒号内容+行尾
|
|
130
|
+
* 冒号,如「变更背景:」「详细说明:」)整行跳过取次非空行——冒号后无内容不成标题。
|
|
127
131
|
*/
|
|
128
132
|
export function deriveChangeTitle(input, max = 50) {
|
|
129
133
|
for (const l of String(input || '').replace(/\r\n/g, '\n').split('\n')) {
|
|
130
134
|
const t = l.trim()
|
|
131
135
|
if (!t) continue
|
|
132
136
|
if (/^成功标准\s*[::]/.test(t)) break
|
|
133
|
-
|
|
137
|
+
if (/^[^::]{1,12}[::]$/.test(t)) continue
|
|
138
|
+
const stripped = t.replace(/^(?:需求|动机|背景|任务|标题|动机与背景|背景与动机)\s*[::]\s*/, '').trim()
|
|
134
139
|
if (!stripped) continue
|
|
135
140
|
const one = stripped.replace(/\s+/g, ' ')
|
|
136
141
|
return one.length > max ? one.slice(0, max) + '…' : one
|
|
@@ -39,6 +39,9 @@ export async function executeArchiveDistill({ cwd, specBase, changeName }) {
|
|
|
39
39
|
console.warn(`\n⚠️ rejected 决策缺否决理由/复潮条件(needsWait)——这些条目未写盘,其余条目照常提炼。`)
|
|
40
40
|
console.warn(` 裁决指引(确认归档 --confirm 前处理):`)
|
|
41
41
|
console.warn(` ① 补录:把缺失字段补进 ${join(changeDir, 'decisions.md')} 后重跑提炼(幂等)——`)
|
|
42
|
+
console.warn(` 可复制补丁样例(在对应 D-xxx@vN 条目末尾追加两行,字段名与值之间用冒号+空格):`)
|
|
43
|
+
console.warn(` - 否决理由: <一句话核心劣势>`)
|
|
44
|
+
console.warn(` - 复潮条件: <何种信号下可重议本方案>`)
|
|
42
45
|
console.warn(` node --input-type=module -e "import { distillIntoKnowledge } from '<sillyspec 源码路径>/src/decision-distill.js'; console.log(JSON.stringify(distillIntoKnowledge(${JSON.stringify(changeDir)}, ${JSON.stringify(knowledgeRoot)}, ${JSON.stringify(headHash)}), null, 2))"`)
|
|
43
46
|
console.warn(` ② 跳过:经用户裁决不入库该条,并在「确认归档」--output 注记跳过原因。`)
|
|
44
47
|
}
|
package/src/run/command.js
CHANGED
|
@@ -232,7 +232,7 @@ export async function ensureStageSteps(progress, stageName, cwd, specDir = null)
|
|
|
232
232
|
function emitReadonlyEmptyGuide() {
|
|
233
233
|
console.log('ℹ️ 未找到进度数据(只读查询不建变更)')
|
|
234
234
|
console.log(' → 可开始轻量变更(--input 引号内换行合法,下例可照抄):')
|
|
235
|
-
console.log(' sillyspec flow start --change <YYYY-MM-DD-名> --input "
|
|
235
|
+
console.log(' sillyspec flow start --change <YYYY-MM-DD-名> --input "<一句中文概括(首行即变更标题);随后动机与背景')
|
|
236
236
|
console.log('')
|
|
237
237
|
console.log(' 成功标准:')
|
|
238
238
|
console.log(' - <可验证标准>"')
|
|
@@ -858,7 +858,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
858
858
|
quickFallbackUsed = true
|
|
859
859
|
} else {
|
|
860
860
|
console.log(' 本仓无近期 quick 会话记录(current marker 与 quick-sessions 均空)——新工作请走轻量变更(--input 引号内换行合法,可照抄):')
|
|
861
|
-
console.log(' sillyspec flow start --change <YYYY-MM-DD-名> --input "
|
|
861
|
+
console.log(' sillyspec flow start --change <YYYY-MM-DD-名> --input "<一句中文概括(首行即变更标题);随后动机与背景')
|
|
862
862
|
console.log('')
|
|
863
863
|
console.log(' 成功标准:')
|
|
864
864
|
console.log(' - <可验证标准>"')
|
|
@@ -2076,7 +2076,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
|
|
|
2076
2076
|
if (classification.mode === 'quick') {
|
|
2077
2077
|
console.log(`📊 auto 模式分类:${classification.reason} 判为小变更`)
|
|
2078
2078
|
console.log(` 小变更请走轻量变更(quick 通道已退役,v3.30.0 起拒绝新会话;--input 引号内换行合法,可照抄):`)
|
|
2079
|
-
console.log(' sillyspec flow start --change <YYYY-MM-DD-名> --input "
|
|
2079
|
+
console.log(' sillyspec flow start --change <YYYY-MM-DD-名> --input "<一句中文概括(首行即变更标题);随后动机与背景')
|
|
2080
2080
|
console.log('')
|
|
2081
2081
|
console.log(' 成功标准:')
|
|
2082
2082
|
console.log(' - <可验证标准>"')
|
|
@@ -816,9 +816,14 @@ export async function bakeArchiveTimeline({ cwd, specBase, changeName, destDir,
|
|
|
816
816
|
const { readWatcherEvents, watcherEventsPath } = await import('../watcher.js')
|
|
817
817
|
const res = readWatcherEvents({ runtimeRoot, change: changeName })
|
|
818
818
|
if (!res.exists) return { ok: true, skipped: true, reason: `无事件流(${watcherEventsPath(runtimeRoot, changeName)})` }
|
|
819
|
-
|
|
819
|
+
// 侧车透传(2026-10-14-timeline-ticktime-anchor-scope):destDir 内 task-ticks.jsonl 已随归档包
|
|
820
|
+
// ——烤制面读包内侧车(与 tasks.md 同源同路径形态),精确勾选时刻跨机可读;锚收敛 changeName
|
|
821
|
+
// 进解析(消息不含变更名的提交不锚任务行)。
|
|
822
|
+
const { parseTaskLines, renderBakedTimeline, resolveCommitAnchors, parseTickLog, readBirthTs } = await import('../timeline.js')
|
|
820
823
|
let tasks = null
|
|
821
824
|
try { tasks = parseTaskLines(readRawImpl(join(destDir, 'tasks.md'), 'utf8')) } catch { /* 任务面缺失照烤(渲染层有降级段) */ }
|
|
825
|
+
let tickLog = []
|
|
826
|
+
try { tickLog = parseTickLog(readRawImpl(join(destDir, 'task-ticks.jsonl'), 'utf8')) } catch { /* 侧车缺失照烤(时刻退推断面) */ }
|
|
822
827
|
let tier = null
|
|
823
828
|
try { const m = /^tier:\s*(\S+)/m.exec(readRawImpl(join(destDir, 'flow-state.yaml'), 'utf8')); if (m) tier = m[1] } catch { /* 头脑风暴预段等无 flow-state 按未知 */ }
|
|
824
829
|
const look = gitLook || ((hash) => {
|
|
@@ -829,12 +834,12 @@ export async function bakeArchiveTimeline({ cwd, specBase, changeName, destDir,
|
|
|
829
834
|
const sep2 = rest.indexOf('\x1f')
|
|
830
835
|
return { hash: out.slice(0, sep), dateISO: rest.slice(0, sep2), message: rest.slice(sep2 + 1) }
|
|
831
836
|
})
|
|
832
|
-
const anchors = resolveCommitAnchors(res.events, look)
|
|
837
|
+
const anchors = resolveCommitAnchors(res.events, look, changeName)
|
|
833
838
|
let eventsRaw = null
|
|
834
839
|
try { eventsRaw = readRawImpl(watcherEventsPath(runtimeRoot, changeName), 'utf8') } catch { /* 副本缺源只烤渲染面 */ }
|
|
835
840
|
const oversize = eventsRaw != null && Buffer.byteLength(eventsRaw, 'utf8') > eventsCopyMaxBytes
|
|
836
841
|
// 评审 P3-1 清偿:副本未落盘(读源失败或超帽)时头注记必须显式「未随包」,不虚报在场
|
|
837
|
-
const timelineMd = renderBakedTimeline({ change: changeName, events: res.events, tasks, anchors, birthTs: readBirthTs(destDir), tier, bakedAtIso: nowIso || new Date().toISOString(), eventsCopySkipped: eventsRaw == null || oversize })
|
|
842
|
+
const timelineMd = renderBakedTimeline({ change: changeName, events: res.events, tasks, anchors, birthTs: readBirthTs(destDir), tier, bakedAtIso: nowIso || new Date().toISOString(), eventsCopySkipped: eventsRaw == null || oversize, tickLog })
|
|
838
843
|
writeImpl(join(destDir, 'timeline.md'), timelineMd)
|
|
839
844
|
const files = ['timeline.md']
|
|
840
845
|
if (eventsRaw != null && !oversize) {
|