sillyspec 3.32.8 → 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 +5 -4
- package/assets/command-cards/flow.md +9 -4
- package/package.json +1 -1
- package/src/change-list.js +13 -0
- package/src/commit-guard.js +70 -2
- package/src/commit-suggest.js +45 -4
- package/src/config-schema.js +14 -0
- package/src/cross-repo-reconcile.js +54 -1
- package/src/decision-distill.js +77 -21
- package/src/decision-renumber.js +157 -0
- package/src/deleted-ledger.js +75 -0
- package/src/design-facts.js +5 -3
- package/src/dispatch/backends/local-agent.js +1 -1
- package/src/dispatch/backends/sillyhub-mcp.js +1 -1
- package/src/docs-check.js +30 -15
- package/src/docs-debt.js +3 -2
- package/src/doctor-diagnostics.js +3 -1
- package/src/flow-draft.js +24 -2
- package/src/flow-parity.js +32 -2
- package/src/flow.js +427 -69
- package/src/fr-index.js +15 -1
- package/src/hooks/worktree-guard.js +3 -3
- package/src/index.js +22 -10
- 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/progress/change-registry.js +7 -0
- package/src/progress.js +67 -0
- package/src/quicklog.js +6 -1
- package/src/redomain.js +27 -0
- package/src/route-hindsight.js +34 -2
- package/src/run/archive-distill.js +3 -0
- package/src/run/command.js +40 -44
- package/src/run/complete-handlers.js +8 -3
- package/src/run/complete.js +129 -63
- package/src/run/gates.js +94 -90
- package/src/run/prompt.js +13 -3
- package/src/run/shared.js +19 -0
- package/src/run/stage.js +1 -1
- package/src/scope-audit.js +2 -2
- 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 +24 -12
- package/src/stages/execute.js +1 -1
- package/src/stages/explore.js +3 -5
- package/src/stages/knowledge.js +56 -5
- package/src/sync.js +98 -2
- package/src/task-tick.js +12 -0
- package/src/timeline.js +60 -19
- package/src/verify-postcheck.js +83 -6
- package/src/verify-probes.js +61 -18
- package/templates/agents-instruction.md +5 -4
- package/templates/prompts/testcase-design.md +5 -0
- package/src/stages/brainstorm-auto.js +0 -286
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;
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// pm._changePath(persistence-core 留 facade 本体)。无共享常量依赖。
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, renameSync } from 'fs';
|
|
5
5
|
import { join } from 'path';
|
|
6
|
+
import { appendDeletedLedger } from '../deleted-ledger.js';
|
|
6
7
|
|
|
7
8
|
// 所有权活跃心跳窗缺省(2026-09-14-change-ownership-guards task-02 / FR-01):owner 非本会话
|
|
8
9
|
// 且 now-last_active < 窗口 → 拒绝接管;窗口外 → 自动接管。local.yaml change-ownership.
|
|
@@ -601,6 +602,9 @@ export class ChangeRegistry {
|
|
|
601
602
|
// 本地脏度(D-013 / task-04):删除也是本地状态推进(triggerSync 据此推终态/墓碑上行)
|
|
602
603
|
this.pm._touchLocalModified(cwd, changeName, now);
|
|
603
604
|
});
|
|
605
|
+
// 删除台账(2026-10-13-sync-ledger-tombstone):DB 行被外部物理清除后墓碑链的唯一
|
|
606
|
+
// 终态凭证。事务提交后追加、fail-open(见 deleted-ledger.js 头注释)。
|
|
607
|
+
appendDeletedLedger(this.pm._getSpecDir(cwd), changeName, 'deleted');
|
|
604
608
|
}
|
|
605
609
|
|
|
606
610
|
/**
|
|
@@ -669,6 +673,9 @@ export class ChangeRegistry {
|
|
|
669
673
|
// 本地脏度(D-013 / task-04):归档也是本地状态推进
|
|
670
674
|
this.pm._touchLocalModified(cwd, changeName, now);
|
|
671
675
|
});
|
|
676
|
+
// 删除台账(2026-10-13-sync-ledger-tombstone):归档终态同记(archived)——行被外部
|
|
677
|
+
// 清除后 sync 台账分支据此补推 archived 墓碑(平台 location='archive')。fail-open。
|
|
678
|
+
appendDeletedLedger(this.pm._getSpecDir(cwd), changeName, 'archived');
|
|
672
679
|
}
|
|
673
680
|
}
|
|
674
681
|
|
package/src/progress.js
CHANGED
|
@@ -1217,6 +1217,73 @@ export class ProgressManager {
|
|
|
1217
1217
|
return this.read(cwd, changeName);
|
|
1218
1218
|
}
|
|
1219
1219
|
|
|
1220
|
+
/**
|
|
1221
|
+
* 原子抢占式建变更(2026-10-13-autocreate-atomic-claim,事故 2026-10-13-new-change-e5a27813)。
|
|
1222
|
+
*
|
|
1223
|
+
* 背景:run brainstorm / run auto 无 --change 的自动建名此前是「listChanges 查零活跃 →
|
|
1224
|
+
* initChange 插行」两步——检查与插入非原子,并行进程都能通过检查各建一行(当日
|
|
1225
|
+
* daemon-log-config-view 与 new-change-e5a27813 同秒落地实证)。本方法把「零活跃检查 +
|
|
1226
|
+
* 建行」收敛为单条 INSERT..SELECT(NOT EXISTS active / NOT EXISTS 同名 双守卫)——
|
|
1227
|
+
* SQLite 单语句原子;跨进程 WAL 快照过期(BUSY_SNAPSHOT)由 db.js transaction 的
|
|
1228
|
+
* BUSY 整体重跑收敛:重跑后守卫见到先行者已提交的 active 行即拒绝。
|
|
1229
|
+
*
|
|
1230
|
+
* 与 initChange 的差异(调用方仅 run/command.js 自动建名两处;显式 --change / 平台
|
|
1231
|
+
* 工具 / 测试 fixture 继续用 initChange,语义不动):
|
|
1232
|
+
* - 检查+插行原子化(败者零写入);
|
|
1233
|
+
* - 目录创建移到抢占成功之后(败者不留 changes/<自动名>/ 空目录);
|
|
1234
|
+
* - 失败返回原因而非静默创建。
|
|
1235
|
+
*
|
|
1236
|
+
* @param {string} cwd 项目根
|
|
1237
|
+
* @param {string} changeName 变更名(自动名恒为日期前缀名;quick 会话名与 default 不走本方法)
|
|
1238
|
+
* @param {{title?: string|null, quicklogId?: string|null}} [meta]
|
|
1239
|
+
* @returns {{created: true, progress: object} | {created: false, reason: 'active_exists'|'name_exists', activeChanges?: string[]}}
|
|
1240
|
+
*/
|
|
1241
|
+
initChangeIfNoActive(cwd, changeName, meta = {}) {
|
|
1242
|
+
if (!changeName) {
|
|
1243
|
+
console.warn('⚠️ initChangeIfNoActive: changeName 为空,跳过');
|
|
1244
|
+
return { created: false, reason: 'name_exists' };
|
|
1245
|
+
}
|
|
1246
|
+
const db = this._ensureDB(cwd);
|
|
1247
|
+
let claimed = false;
|
|
1248
|
+
db.transaction(() => {
|
|
1249
|
+
const sqlDb = db.getDb();
|
|
1250
|
+
const now = new Date().toISOString();
|
|
1251
|
+
// 单语句原子抢占:守卫与插入同一语句,无跨语句竞态窗口
|
|
1252
|
+
const claim = sqlDb.prepare(
|
|
1253
|
+
`INSERT INTO changes (name, current_stage, status, created_at, last_active, title, quicklog_id)
|
|
1254
|
+
SELECT ?, 'brainstorm', 'active', ?, ?, ?, ?
|
|
1255
|
+
WHERE NOT EXISTS (SELECT 1 FROM changes WHERE status = 'active')
|
|
1256
|
+
AND NOT EXISTS (SELECT 1 FROM changes WHERE name = ?)`
|
|
1257
|
+
).run(changeName, now, now, meta.title || null, meta.quicklogId || null, changeName);
|
|
1258
|
+
claimed = claim.changes === 1;
|
|
1259
|
+
if (!claimed) return; // 败者:事务内零后续写入,提交为空事务
|
|
1260
|
+
// 抢到后与 initChange 同款收尾(同事务):种全部合法阶段 + 本地脏度戳
|
|
1261
|
+
const changeRow = sqlDb.prepare('SELECT id FROM changes WHERE name = ?').get(changeName);
|
|
1262
|
+
for (const stage of VALID_STAGES) {
|
|
1263
|
+
sqlDb.prepare(
|
|
1264
|
+
`INSERT OR IGNORE INTO stages (change_id, stage, status)
|
|
1265
|
+
VALUES (?, ?, 'pending')`
|
|
1266
|
+
).run(changeRow.id, stage);
|
|
1267
|
+
}
|
|
1268
|
+
this._touchLocalModified(cwd, changeName, now);
|
|
1269
|
+
});
|
|
1270
|
+
if (!claimed) {
|
|
1271
|
+
const sqlDb = db.getDb();
|
|
1272
|
+
const activeChanges = sqlDb.prepare("SELECT name FROM changes WHERE status = 'active'").all().map((r) => r.name);
|
|
1273
|
+
const sameName = sqlDb.prepare('SELECT id FROM changes WHERE name = ?').get(changeName);
|
|
1274
|
+
// 判序:同名行存在(任意 status)优先报 name_exists(重名与自动名随机碰撞兜底);
|
|
1275
|
+
// 否则按 active_exists 附清单(调用方走多活跃引导)
|
|
1276
|
+
return sameName && !activeChanges.includes(changeName)
|
|
1277
|
+
? { created: false, reason: 'name_exists' }
|
|
1278
|
+
: { created: false, reason: 'active_exists', activeChanges };
|
|
1279
|
+
}
|
|
1280
|
+
// 抢占成功后才建目录(败者零残留);quick/default 豁免与 initChange 同款(防辅助阶段空目录物化)
|
|
1281
|
+
if (!/^quick-[0-9a-f]{8}$/.test(changeName) && changeName !== 'default') {
|
|
1282
|
+
this._ensureChangeDir(cwd, changeName);
|
|
1283
|
+
}
|
|
1284
|
+
return { created: true, progress: this.read(cwd, changeName) };
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1220
1287
|
setStage(cwd, stage, changeName = null) { return this._stepStore.setStage(cwd, stage, changeName); }
|
|
1221
1288
|
|
|
1222
1289
|
addStep(cwd, stage, stepName, changeName = null) { return this._stepStore.addStep(cwd, stage, stepName, changeName); }
|
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
|
package/src/redomain.js
CHANGED
|
@@ -21,6 +21,27 @@ import { writeAtomicSync } from './fs-atomic.js'
|
|
|
21
21
|
/** 条目段头(## FR-xxx-NNN / ## FR-NNN)判定与 ID 提取 */
|
|
22
22
|
const FR_HEADER_RE = /^## (FR-[\w.-]+-\d+|FR-\d+)\s*(.*)$/
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* 域名入参归一化(2026-10-14-redomain-domain-arg-normalize)。
|
|
26
|
+
*
|
|
27
|
+
* flow done 伪域告警提示语(fr-index.js)给出的是 `fr/auto-backend` 形态、手工
|
|
28
|
+
* 输入还可能带 `.md` 后缀,而本模块按裸域名拼 `fr/${from}.md`——两种口径曾打架
|
|
29
|
+
* 产生 `fr/fr/` 双前缀报错。本函数把入参统一收敛为裸域名:trim → 剥前导 `fr/`
|
|
30
|
+
* (不区分大小写)→ 剥尾随 `.md`;裸名原样返回,幂等。可能返回空串(如只传
|
|
31
|
+
* `fr/`)——空值拒绝由调用方负责(报错文案需含「fr/ 前缀」用法提示)。
|
|
32
|
+
* 域文件物理上都在 fr/ 目录内、裸名不含 `/`,`fr/xxx` 只可能是前缀形态,无歧义。
|
|
33
|
+
*/
|
|
34
|
+
export function normalizeDomainArg(v) {
|
|
35
|
+
return String(v ?? '').trim().replace(/^fr\//i, '').replace(/\.md$/i, '').trim()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 归一化后空值拒绝(统一文案,调用方在一切 fs IO 之前调用) */
|
|
39
|
+
function assertDomainArgs({ from, to }) {
|
|
40
|
+
if (!from || !to) {
|
|
41
|
+
throw new Error('源域/目标域为空:域名可带 fr/ 前缀与 .md 后缀(fr/auto-backend ≍ auto-backend.md ≍ auto-backend)')
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
24
45
|
function frEntryId(headerLine) {
|
|
25
46
|
const m = FR_HEADER_RE.exec(headerLine)
|
|
26
47
|
return m ? m[1] : null
|
|
@@ -64,6 +85,9 @@ function entrySourceChange(sectionLines) {
|
|
|
64
85
|
* @throws 源域缺席 / 无匹配条目 / anchor 未命中
|
|
65
86
|
*/
|
|
66
87
|
export function redomainFrEntries({ knowledgeRoot, from, to, anchors = null, byChange = null }) {
|
|
88
|
+
from = normalizeDomainArg(from)
|
|
89
|
+
to = normalizeDomainArg(to)
|
|
90
|
+
assertDomainArgs({ from, to })
|
|
67
91
|
const frDir = join(knowledgeRoot, 'fr')
|
|
68
92
|
const fromPath = join(frDir, `${from}.md`)
|
|
69
93
|
const toPath = join(frDir, `${to}.md`)
|
|
@@ -134,6 +158,9 @@ export function redomainFrEntries({ knowledgeRoot, from, to, anchors = null, byC
|
|
|
134
158
|
|
|
135
159
|
/** 干跑预览:列出将迁移条目,不落盘(byChange 同执行侧过滤) */
|
|
136
160
|
export function planRedomain({ knowledgeRoot, from, to, anchors = null, byChange = null }) {
|
|
161
|
+
from = normalizeDomainArg(from)
|
|
162
|
+
to = normalizeDomainArg(to)
|
|
163
|
+
assertDomainArgs({ from, to })
|
|
137
164
|
const frDir = join(knowledgeRoot, 'fr')
|
|
138
165
|
const fromPath = join(frDir, `${from}.md`)
|
|
139
166
|
if (!existsSync(fromPath)) throw new Error(`源域文件不存在:fr/${from}.md`)
|
package/src/route-hindsight.js
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
* flow-state substeps 无失败计数面(执行期裁决 D-006:数据源从卡面所述「flow-state
|
|
33
33
|
* substeps」改读同为既有记录面的 verify-runs 时间线,封闭面语义不变)。
|
|
34
34
|
*/
|
|
35
|
-
import { existsSync, readFileSync, readdirSync, mkdirSync } from 'node:fs'
|
|
35
|
+
import { existsSync, readFileSync, readdirSync, mkdirSync, rmSync } from 'node:fs'
|
|
36
36
|
import { createHash } from 'node:crypto'
|
|
37
37
|
import { join, basename, dirname } from 'node:path'
|
|
38
38
|
import { writeAtomicSync } from './fs-atomic.js'
|
|
@@ -273,4 +273,36 @@ export function readHindsightHint({ specBase }) {
|
|
|
273
273
|
}
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
-
|
|
276
|
+
/**
|
|
277
|
+
* 标记结构化读取(2026-10-14-prestage-consent-gate):同意门需要 change/reasons 组文案,
|
|
278
|
+
* 提示行渲染(resume 路径)仍走 readHindsightHint。无文件/损坏 → null(读不到=无门)。
|
|
279
|
+
* @returns {{ change: string, marked_at?: string, metrics?: object, reasons?: string[] } | null}
|
|
280
|
+
*/
|
|
281
|
+
export function readHindsightMark({ specBase }) {
|
|
282
|
+
try {
|
|
283
|
+
const raw = readFileSync(join(specBase, '.runtime', MARK_FILE), 'utf8')
|
|
284
|
+
const j = JSON.parse(raw)
|
|
285
|
+
if (!j || typeof j !== 'object' || !j.change) return null
|
|
286
|
+
return j
|
|
287
|
+
} catch {
|
|
288
|
+
return null
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* 消费标记(声明放行 --no-prestage / adopt 头脑风暴收编时由 flow.js 调用):删除标记文件。
|
|
294
|
+
* 一次性反馈语义——声明只豁免下一次 start,不豁免之后所有(之后再被标记会重新落盘重新拦)。
|
|
295
|
+
* @returns {boolean} 是否真删(无标记/删除失败 = false,幂等零副作用)
|
|
296
|
+
*/
|
|
297
|
+
export function clearHindsightMark({ specBase }) {
|
|
298
|
+
try {
|
|
299
|
+
const p = join(specBase, '.runtime', MARK_FILE)
|
|
300
|
+
if (!existsSync(p)) return false
|
|
301
|
+
rmSync(p)
|
|
302
|
+
return true
|
|
303
|
+
} catch {
|
|
304
|
+
return false
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export default { HINDSIGHT_THRESHOLDS, snapshotBaseline, computeHindsightMetrics, markHindsight, readHindsightHint, readHindsightMark, clearHindsightMark }
|
|
@@ -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
|
@@ -24,7 +24,7 @@ import { existsSync, readdirSync, mkdirSync, writeFileSync, readFileSync, rmSync
|
|
|
24
24
|
import { randomBytes, randomUUID } from 'node:crypto'
|
|
25
25
|
import { fileURLToPath } from 'node:url'
|
|
26
26
|
import { writeAtomicSync } from '../fs-atomic.js'
|
|
27
|
-
import { resolveSpecDir, countAncestorSpecDirs, ancestorSpecDirs, resolveAncestorCeiling, resolveChangeDir, triggerSync, getStageSteps, formatWaitOptions, checkApproval, warnApprovalUnknown, didYouMean, assertSafeChangeName, assertDatedChangeName, detectQuickSessionDrift, detectWorktreeSpecDrift, resolveRuntimeRoot, resolveQuickSessionsDir, writePlatformPointer, checkPlatformManaged, isSelfReferentialSpecRoot, isTempResidueSpecRoot, PLATFORM_MANAGED_FILENAME, warnSelfRefPointerOnce, readStageBurst, STAGE_BURST_STAGES } from './shared.js'
|
|
27
|
+
import { resolveSpecDir, countAncestorSpecDirs, ancestorSpecDirs, resolveAncestorCeiling, resolveChangeDir, triggerSync, getStageSteps, formatWaitOptions, checkApproval, warnApprovalUnknown, didYouMean, assertSafeChangeName, assertDatedChangeName, detectQuickSessionDrift, detectWorktreeSpecDrift, resolveRuntimeRoot, resolveQuickSessionsDir, writePlatformPointer, checkPlatformManaged, isSelfReferentialSpecRoot, isTempResidueSpecRoot, PLATFORM_MANAGED_FILENAME, warnSelfRefPointerOnce, readStageBurst, STAGE_BURST_STAGES, isRetiredAutoBrainstormTable, printRetiredAutoBrainstormRefusal } from './shared.js'
|
|
28
28
|
import { resolveQuickLinkedChanges } from './quick-audit.js'
|
|
29
29
|
import { outputStep, collectStageWaitHistory } from './prompt.js'
|
|
30
30
|
import { completeStep, completeStepBurst, skipStep, waitStep, continueStep, synthesizeStepOutput } from './complete.js'
|
|
@@ -34,7 +34,7 @@ import { ProgressManager, resolveSessionIdentity } from '../progress.js'
|
|
|
34
34
|
import { validateChangeExists, checkTransition } from '../stage-contract.js'
|
|
35
35
|
import { READONLY_AUXILIARY_STAGES } from '../constants.js'
|
|
36
36
|
import { stageRegistry, auxiliaryStages } from '../stages/index.js'
|
|
37
|
-
|
|
37
|
+
|
|
38
38
|
import { setQuickFileNotes } from '../quicklog.js'
|
|
39
39
|
import { checkQuickSessionOwner, writeQuickSessionOwner } from '../quick-session-owner.js'
|
|
40
40
|
import { hasDecisionId } from '../decisions-io.js'
|
|
@@ -171,6 +171,14 @@ function resolveChangeName(cwd, progress, specDir = null) {
|
|
|
171
171
|
export async function ensureStageSteps(progress, stageName, cwd, specDir = null) {
|
|
172
172
|
if (!progress.stages) progress.stages = {}
|
|
173
173
|
|
|
174
|
+
// 退役自动道守门(2026-10-14-brainstorm-auto-retire):存量 brainstorm 4 步 auto 表在此明确
|
|
175
|
+
// 拒绝——否则下方「步骤表与当前定义漂移」分支会把 auto 表静默重种为主模式 8 步表并清零进度。
|
|
176
|
+
// --reset/--reopen 走 resetStage 内联重种(不经此函数)= 指引中的恢复出路。
|
|
177
|
+
if (stageName === 'brainstorm' && isRetiredAutoBrainstormTable(progress.stages.brainstorm?.steps)) {
|
|
178
|
+
printRetiredAutoBrainstormRefusal(progress?.change || null)
|
|
179
|
+
process.exit(2)
|
|
180
|
+
}
|
|
181
|
+
|
|
174
182
|
const steps = await getStageSteps(stageName, cwd, progress, specDir)
|
|
175
183
|
if (!steps) return false
|
|
176
184
|
|
|
@@ -224,7 +232,7 @@ export async function ensureStageSteps(progress, stageName, cwd, specDir = null)
|
|
|
224
232
|
function emitReadonlyEmptyGuide() {
|
|
225
233
|
console.log('ℹ️ 未找到进度数据(只读查询不建变更)')
|
|
226
234
|
console.log(' → 可开始轻量变更(--input 引号内换行合法,下例可照抄):')
|
|
227
|
-
console.log(' sillyspec flow start --change <YYYY-MM-DD-名> --input "
|
|
235
|
+
console.log(' sillyspec flow start --change <YYYY-MM-DD-名> --input "<一句中文概括(首行即变更标题);随后动机与背景')
|
|
228
236
|
console.log('')
|
|
229
237
|
console.log(' 成功标准:')
|
|
230
238
|
console.log(' - <可验证标准>"')
|
|
@@ -850,7 +858,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
850
858
|
quickFallbackUsed = true
|
|
851
859
|
} else {
|
|
852
860
|
console.log(' 本仓无近期 quick 会话记录(current marker 与 quick-sessions 均空)——新工作请走轻量变更(--input 引号内换行合法,可照抄):')
|
|
853
|
-
console.log(' sillyspec flow start --change <YYYY-MM-DD-名> --input "
|
|
861
|
+
console.log(' sillyspec flow start --change <YYYY-MM-DD-名> --input "<一句中文概括(首行即变更标题);随后动机与背景')
|
|
854
862
|
console.log('')
|
|
855
863
|
console.log(' 成功标准:')
|
|
856
864
|
console.log(' - <可验证标准>"')
|
|
@@ -1340,39 +1348,45 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
1340
1348
|
process.exit(2) // 用法错(--done 找不到变更,未传 --change)→ exit 2
|
|
1341
1349
|
}
|
|
1342
1350
|
// task-03 (D-006@v1 / FR-05): 无 --change 仅当无已存在活跃变更时 auto-create;
|
|
1343
|
-
// 多活跃变更仓强制 --change(exit 2 引导),防 B8
|
|
1344
|
-
//
|
|
1345
|
-
|
|
1346
|
-
|
|
1351
|
+
// 多活跃变更仓强制 --change(exit 2 引导),防 B8 幽灵变更。
|
|
1352
|
+
// 2026-10-13-autocreate-atomic-claim:listChanges 检查 + initChange 两步非原子
|
|
1353
|
+
// (并行进程同过检查各建一行,2026-10-13-new-change-e5a27813 事故实证)——改
|
|
1354
|
+
// initChangeIfNoActive 单语句原子抢占,败者走同一引导。
|
|
1355
|
+
const date = new Date().toISOString().slice(0, 10)
|
|
1356
|
+
const autoName = `${date}-new-change-${randomBytes(4).toString('hex')}`
|
|
1357
|
+
const claim = pm.initChangeIfNoActive(cwd, autoName, { title: inputText ? (deriveChangeTitle(inputText) || sanitizeDesc(inputText)) : autoName })
|
|
1358
|
+
if (!claim.created) {
|
|
1359
|
+
const activeChanges = claim.activeChanges || []
|
|
1347
1360
|
console.error(`❌ 已存在 ${activeChanges.length} 个活跃变更,run brainstorm 无 --change 不会自动创建新变更。`)
|
|
1348
1361
|
console.error(` 活跃变更:${activeChanges.join('、')}`)
|
|
1349
1362
|
console.error(' 请用 --change <变更名> 指定要操作的变更,')
|
|
1350
1363
|
console.error(' 或先归档/收尾现有活跃变更后重试。')
|
|
1351
1364
|
process.exit(2) // 用法错(多活跃变更未指定 --change)→ exit 2
|
|
1352
1365
|
}
|
|
1353
|
-
const date = new Date().toISOString().slice(0, 10)
|
|
1354
|
-
const autoName = `${date}-new-change-${randomBytes(4).toString('hex')}`
|
|
1355
1366
|
console.log(`🔄 自动创建变更:${autoName}`)
|
|
1356
1367
|
console.log(` 提示:可以用 --change <名称> 指定自定义变更名`)
|
|
1357
1368
|
console.log(` 或事后重命名:sillyspec change-rename ${autoName} <新名称>`)
|
|
1358
|
-
|
|
1359
|
-
progress = pm.initChange(cwd, autoName, { title: inputText ? (deriveChangeTitle(inputText) || sanitizeDesc(inputText)) : autoName })
|
|
1369
|
+
progress = claim.progress
|
|
1360
1370
|
changeName = autoName
|
|
1361
1371
|
} else if (stageName === 'auto') {
|
|
1362
1372
|
// auto 零活跃建变更(2026-09-08-auto-driver D-003@v1,Grill P1-②):SKILL 启动命令即
|
|
1363
1373
|
// run auto --input——此前此处 exit 2 是断头路。复用 brainstorm 命名(date-new-change-hex)。
|
|
1364
|
-
// pm.read 对零/多活跃都返 null
|
|
1365
|
-
|
|
1366
|
-
|
|
1374
|
+
// pm.read 对零/多活跃都返 null——多活跃报错列候选(不隐式选择),零活跃才建。
|
|
1375
|
+
// 2026-10-13-autocreate-atomic-claim:同 brainstorm 改原子抢占;顺带收紧「恰好 1 个
|
|
1376
|
+
// 活跃也建第二个」的旧宽限(旧 >1 检查与其「零活跃才建」注释自相矛盾,且计数宽限
|
|
1377
|
+
// 正是并发双建的温床)——1 活跃现与多活跃同走 exit 2 引导。
|
|
1378
|
+
const date = new Date().toISOString().slice(0, 10)
|
|
1379
|
+
const autoName = `${date}-new-change-${randomBytes(4).toString('hex')}`
|
|
1380
|
+
const claim = pm.initChangeIfNoActive(cwd, autoName, { title: inputText ? (deriveChangeTitle(inputText) || sanitizeDesc(inputText)) : autoName })
|
|
1381
|
+
if (!claim.created) {
|
|
1382
|
+
const activeCount = claim.activeChanges || []
|
|
1367
1383
|
console.error(`❌ 已存在 ${activeCount.length} 个活跃变更,run auto 无法自动定位目标。`)
|
|
1368
1384
|
console.error(' 活跃变更:' + activeCount.join('、'))
|
|
1369
1385
|
console.error(' 请用 --change <变更名> 显式指定,或先归档/收尾现有变更。')
|
|
1370
1386
|
process.exit(2)
|
|
1371
1387
|
}
|
|
1372
|
-
const date = new Date().toISOString().slice(0, 10)
|
|
1373
|
-
const autoName = `${date}-new-change-${randomBytes(4).toString('hex')}`
|
|
1374
1388
|
console.log(`🔄 auto 模式自动创建变更:${autoName}(可用 --change 指定名称,或事后 change-rename)`)
|
|
1375
|
-
progress =
|
|
1389
|
+
progress = claim.progress
|
|
1376
1390
|
changeName = autoName
|
|
1377
1391
|
} else {
|
|
1378
1392
|
console.error('❌ 未找到进度数据,请先运行 sillyspec init 或指定 --change <变更名>')
|
|
@@ -2021,11 +2035,9 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
|
|
|
2021
2035
|
}
|
|
2022
2036
|
}
|
|
2023
2037
|
|
|
2024
|
-
// Helper: 在 auto
|
|
2038
|
+
// Helper: 在 auto 模式下获取步骤定义(brainstorm-auto 已删除 2026-10-14-brainstorm-auto-retire:
|
|
2039
|
+
// brainstorm 一律主模式定义,wait 步由 auto 驱动方按 SS-META requiresUser 自行推进)
|
|
2025
2040
|
const getAutoSteps = async (stage) => {
|
|
2026
|
-
if (stage === 'brainstorm') {
|
|
2027
|
-
return brainstormAutoDef.steps
|
|
2028
|
-
}
|
|
2029
2041
|
return getStageSteps(stage, cwd, progress, platformOpts?.specRoot || null)
|
|
2030
2042
|
}
|
|
2031
2043
|
|
|
@@ -2037,27 +2049,11 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
|
|
|
2037
2049
|
const ensureAutoStage = async (stage) => {
|
|
2038
2050
|
const stageChanged = progress.currentStage !== stage
|
|
2039
2051
|
progress.currentStage = stage
|
|
2040
|
-
//
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
// auto 表存在时掉进下方通用 ensureStageSteps——主模式 8 步定义判 4 步 auto 表「漂移」
|
|
2046
|
-
// 重种主模式表,与 getAutoSteps 渲染双轨互踩,进度永远回 step1)
|
|
2047
|
-
if (isAutoModeSteps) return progress
|
|
2048
|
-
if (!isAutoModeSteps) {
|
|
2049
|
-
if (!progress.stages) progress.stages = {}
|
|
2050
|
-
progress.stages.brainstorm = {
|
|
2051
|
-
status: 'in-progress',
|
|
2052
|
-
startedAt: new Date().toLocaleString('zh-CN', { hour12: false }),
|
|
2053
|
-
completedAt: null,
|
|
2054
|
-
steps: brainstormAutoDef.steps.map(s => ({ name: s.name, status: 'pending' }))
|
|
2055
|
-
}
|
|
2056
|
-
pm._write(cwd, progress, changeName)
|
|
2057
|
-
triggerSync(cwd, changeName, platformOpts)
|
|
2058
|
-
progress = pm.read(cwd, changeName)
|
|
2059
|
-
return progress
|
|
2060
|
-
}
|
|
2052
|
+
// 退役自动道守门(2026-10-14-brainstorm-auto-retire):存量 4 步 auto 表明确拒绝(下方通用
|
|
2053
|
+
// ensureStageSteps 同款拒绝兜底,此处先行给带变更名的文案)
|
|
2054
|
+
if (stage === 'brainstorm' && isRetiredAutoBrainstormTable(progress.stages?.brainstorm?.steps)) {
|
|
2055
|
+
printRetiredAutoBrainstormRefusal(changeName)
|
|
2056
|
+
process.exit(2)
|
|
2061
2057
|
}
|
|
2062
2058
|
// specRoot 必传(坑 auto-mode-specroot-miss):平台模式 specRoot 是外部目录,漏传时
|
|
2063
2059
|
// getStageSteps 从 cwd 推导 miss 掉 plan/execute 动态步骤(buildPlanSteps(null) 只剩
|
|
@@ -2080,7 +2076,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
|
|
|
2080
2076
|
if (classification.mode === 'quick') {
|
|
2081
2077
|
console.log(`📊 auto 模式分类:${classification.reason} 判为小变更`)
|
|
2082
2078
|
console.log(` 小变更请走轻量变更(quick 通道已退役,v3.30.0 起拒绝新会话;--input 引号内换行合法,可照抄):`)
|
|
2083
|
-
console.log(' sillyspec flow start --change <YYYY-MM-DD-名> --input "
|
|
2079
|
+
console.log(' sillyspec flow start --change <YYYY-MM-DD-名> --input "<一句中文概括(首行即变更标题);随后动机与背景')
|
|
2084
2080
|
console.log('')
|
|
2085
2081
|
console.log(' 成功标准:')
|
|
2086
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) {
|