sillyspec 3.30.0 → 3.32.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-auto/SKILL.md +54 -54
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +2 -2
- package/.claude/skills/sillyspec-commit/SKILL.md +1 -1
- package/.claude/skills/sillyspec-flow/SKILL.md +10 -6
- package/.claude/skills/sillyspec-quick/SKILL.md +116 -18
- package/.zcodeignore +66 -0
- package/AGENTS.md +15 -15
- package/assets/command-cards/flow.md +9 -3
- package/assets/command-cards/run-quick.md +4 -1
- package/package.json +2 -2
- package/src/agent-session-log.js +27 -5
- package/src/config-schema.js +21 -0
- package/src/datetime.js +51 -0
- package/src/db.js +10 -3
- package/src/decision-distill.js +21 -12
- package/src/design-facts.js +46 -6
- package/src/flow-draft.js +468 -95
- package/src/flow-parity.js +62 -4
- package/src/flow-review.js +136 -6
- package/src/flow.js +729 -125
- package/src/foreign-declared.js +71 -3
- package/src/fr-index.js +148 -162
- package/src/git-helper.js +16 -3
- package/src/hooks/worktree-guard.js +16 -5
- package/src/hunk-attribution.js +28 -5
- package/src/index.js +184 -17
- package/src/knowledge-classify.js +3 -1
- package/src/knowledge-digest.js +55 -26
- package/src/knowledge-match.js +135 -7
- package/src/knowledge-stats.js +66 -4
- package/src/knowledge-vector.js +160 -0
- package/src/machine-interface.js +90 -2
- package/src/module-impact.js +22 -1
- package/src/modules.js +9 -0
- package/src/progress/change-registry.js +20 -0
- package/src/progress/stage-machine.js +4 -7
- package/src/progress.js +8 -1
- package/src/quicklog.js +20 -0
- package/src/redomain.js +23 -7
- package/src/route-hindsight.js +276 -0
- package/src/run/archive-distill.js +3 -1
- package/src/run/command.js +65 -12
- package/src/run/complete-handlers.js +219 -5
- package/src/run/complete.js +77 -11
- package/src/run/gate-snapshot.js +15 -0
- package/src/run/gates.js +60 -4
- package/src/run/prompt.js +159 -31
- package/src/run/quick-audit.js +66 -7
- package/src/run/shared.js +6 -11
- package/src/run/stage.js +5 -1
- package/src/run/verify-quality-scan.js +84 -0
- package/src/scan-facts.js +2 -1
- package/src/scope-audit.js +9 -2
- package/src/sentinel-assertions.js +67 -16
- package/src/stage-contract-engine.js +4 -1
- package/src/stage-contract-spec.js +103 -4
- package/src/stage-contract.js +55 -7
- package/src/stages/brainstorm.js +27 -14
- package/src/stages/knowledge.js +4 -3
- package/src/stages/plan-postcheck.js +98 -3
- package/src/stages/plan.js +22 -2
- package/src/stages/verify.js +2 -1
- package/src/task-review.js +1 -1
- package/src/task-tick.js +121 -0
- package/src/taskcard-frontmatter.js +31 -0
- package/src/ui-visual.js +43 -8
- package/src/verify-postcheck.js +509 -37
- package/src/verify-probes.js +216 -5
- package/src/watcher.js +1067 -1031
- package/src/worktree-apply.js +9 -1
- package/src/worktree.js +21 -6
- package/templates/agents-instruction.md +15 -15
- package/templates/prompts/taskcard-rules.md +1 -0
package/src/modules.js
CHANGED
|
@@ -452,6 +452,8 @@ export function parseModuleMapSimple(content) {
|
|
|
452
452
|
let currentArray = null;
|
|
453
453
|
|
|
454
454
|
for (const line of content.split('\n')) {
|
|
455
|
+
// 空行/注释行:保状态(列表块内允许空行与注释穿插——结构守卫前的免死金牌)
|
|
456
|
+
if (line.trim() === '' || line.trim().startsWith('#')) continue;
|
|
455
457
|
// 模块 id 字符集含点(db.engine 等,与 module-impact.js classifyFile 同口径):
|
|
456
458
|
// 不含点时带点 id 的条目被静默丢弃,上下文注入/scan-diff 侧与 module-impact 侧给相反结论
|
|
457
459
|
const moduleMatch = line.match(/^ ([a-zA-Z0-9_.\-]+):$/);
|
|
@@ -501,6 +503,13 @@ export function parseModuleMapSimple(content) {
|
|
|
501
503
|
currentArray.push(itemMatch[1].trim());
|
|
502
504
|
continue;
|
|
503
505
|
}
|
|
506
|
+
|
|
507
|
+
// 结构守卫(坑 module-map-list-leak,2026-10-06-wallclock-entry 实测发现):list 收集只在
|
|
508
|
+
// 「6 缩进项行 / 空行 / 注释」上存活,其余任何行——未知字段头(字段名开放集,不枚举;
|
|
509
|
+
// 枚举白名单外的字段头此前不重置收集,其列表项漏进上一个 list 字段如 paths)、顶层段
|
|
510
|
+
// (span_risk:/blast:)、异缩进项——终结收集并落盘已收集项。字段名识别仍按白名单(语义面),
|
|
511
|
+
// 收集边界按行结构(开放世界安全)。
|
|
512
|
+
if (currentArray && currentKey) { modules[currentModule][currentKey] = currentArray; currentArray = null; currentKey = null; }
|
|
504
513
|
}
|
|
505
514
|
|
|
506
515
|
// Flush last
|
|
@@ -222,6 +222,26 @@ export class ChangeRegistry {
|
|
|
222
222
|
}
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
+
/**
|
|
226
|
+
* 读变更标题(2026-10-06-flow-status-title:flow status 显示面消费——flow start/--title 写入侧
|
|
227
|
+
* 的对读)。只读不抛(与 getChangeOwner 同族容错):DB 文件不在场/行缺失/title 空/读取失败 → null。
|
|
228
|
+
* 前置 existsSync 判 DB 在场——只读查询路径不得经 _ensureDB 的建库副作用新建 sillyspec.db。
|
|
229
|
+
* @param {string} cwd
|
|
230
|
+
* @param {string} changeName
|
|
231
|
+
* @returns {string|null} 变更标题,无值返回 null
|
|
232
|
+
*/
|
|
233
|
+
getChangeTitle(cwd, changeName) {
|
|
234
|
+
if (!changeName) return null;
|
|
235
|
+
try {
|
|
236
|
+
if (!existsSync(this.pm._runtimePath(cwd, 'sillyspec.db'))) return null;
|
|
237
|
+
const db = this.pm._ensureDB(cwd);
|
|
238
|
+
const row = db.getDb().prepare('SELECT title FROM changes WHERE name = ?').get(changeName);
|
|
239
|
+
return (row && typeof row.title === 'string' && row.title) || null;
|
|
240
|
+
} catch {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
225
245
|
/**
|
|
226
246
|
* 认领变更所有权(2026-09-14-change-ownership-guards task-01 数据载体):
|
|
227
247
|
* - 行不存在 → 创建并写 owner=session(首建者获得);
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { mkdirSync, existsSync, readdirSync, readFileSync } from 'fs';
|
|
7
7
|
import { join, resolve, basename } from 'path';
|
|
8
8
|
import { writeAtomicSync } from '../fs-atomic.js';
|
|
9
|
+
import { timeAgo } from '../datetime.js';
|
|
9
10
|
import { runValidators } from '../stage-contract.js';
|
|
10
11
|
import { QUICK_SID_RE } from '../run/shared.js';
|
|
11
12
|
import { VALID_STAGES, STAGE_LABELS, STAGE_ORDER, MAIN_FLOW_ORDER, SPEC_DIR_NAME, CURRENT_VERSION, STALL_WARN_DAYS, emptyStage } from './shared.js';
|
|
@@ -831,13 +832,9 @@ export class StageMachine {
|
|
|
831
832
|
_timeAgo(dateStr) {
|
|
832
833
|
const ts = _parseFlexibleTs(dateStr);
|
|
833
834
|
if (ts === null) return dateStr || '未知';
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
if (minutes < 60) return `${minutes} 分钟前`;
|
|
838
|
-
const hours = Math.floor(minutes / 60);
|
|
839
|
-
if (hours < 24) return `${hours} 小时前`;
|
|
840
|
-
return `${Math.floor(hours / 24)} 天前`;
|
|
835
|
+
// 档位换算委托 datetime.timeAgo(单源);zh-CN 旧格式容错解析与失败回退留在调用方
|
|
836
|
+
// ——是对本模块存量 lastActive 数据的展示职责(2026-10-06-datetime-timeago 收敛)。
|
|
837
|
+
return timeAgo(ts);
|
|
841
838
|
}
|
|
842
839
|
}
|
|
843
840
|
|
package/src/progress.js
CHANGED
|
@@ -582,7 +582,7 @@ export class ProgressManager {
|
|
|
582
582
|
|
|
583
583
|
// 1. changes 行(投影流程进度列 + name 标识 + 所有权列 owner_session)
|
|
584
584
|
const changeRow = sqlDb.prepare(
|
|
585
|
-
`SELECT id, name, current_stage, status, last_active, last_synced_platform_ts, last_local_modified_ts, owner_session
|
|
585
|
+
`SELECT id, name, current_stage, status, last_active, last_synced_platform_ts, last_local_modified_ts, owner_session, title
|
|
586
586
|
FROM changes WHERE name = ?`
|
|
587
587
|
).get(changeName);
|
|
588
588
|
if (changeRow === undefined) return null;
|
|
@@ -595,6 +595,10 @@ export class ProgressManager {
|
|
|
595
595
|
last_synced_platform_ts: changeRow.last_synced_platform_ts ?? null,
|
|
596
596
|
last_local_modified_ts: changeRow.last_local_modified_ts ?? null,
|
|
597
597
|
owner_session: changeRow.owner_session ?? null,
|
|
598
|
+
// title 一并上行(用户需求 2026-09-29:变更标题为中文概括 ≤50 字)——平台
|
|
599
|
+
// _ensure_change_row 占位与既有行收养均消费 changes[].title;此前 SELECT 漏列
|
|
600
|
+
// → 上行恒无标题,平台只能落 change_key(「英文 key」病灶的同步链一侧)。
|
|
601
|
+
title: changeRow.title ?? null,
|
|
598
602
|
};
|
|
599
603
|
|
|
600
604
|
// 2. project 全局单行(只投影稳定字段 name/schema_version;created_at/updated_at 是本地库元数据,
|
|
@@ -1084,6 +1088,9 @@ export class ProgressManager {
|
|
|
1084
1088
|
/** 读变更所有者会话标识;无行/未登记/无主 → null */
|
|
1085
1089
|
getChangeOwner(cwd, changeName) { return this._changeRegistry.getChangeOwner(cwd, changeName); }
|
|
1086
1090
|
|
|
1091
|
+
/** 读变更标题(2026-10-06-flow-status-title):纯读不抛、无 DB 不建库,缺值 → null */
|
|
1092
|
+
getChangeTitle(cwd, changeName) { return this._changeRegistry.getChangeTitle(cwd, changeName); }
|
|
1093
|
+
|
|
1087
1094
|
/** 认领所有权:首建写 own、已有值不覆盖;返回实际 owner(string|null) */
|
|
1088
1095
|
claimChangeOwner(cwd, changeName, session) { return this._changeRegistry.claimChangeOwner(cwd, changeName, session); }
|
|
1089
1096
|
|
package/src/quicklog.js
CHANGED
|
@@ -118,6 +118,26 @@ export function sanitizeDesc(description) {
|
|
|
118
118
|
return s.length > 120 ? s.slice(0, 120) + '…' : s
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
/**
|
|
122
|
+
* 变更标题推导(用户需求 2026-09-29:changes.title 应为中文描述,≤50 字、建议 ~20 字)。
|
|
123
|
+
* 取 --input 首个非空行(「成功标准:」引导行即止——那是验收面不是标题素材),剥
|
|
124
|
+
* 「需求:/动机:」类标签前缀,空白折叠单行,超长截断加 …;空输入返回 ''(调用方兜底
|
|
125
|
+
* change 名)。agent 在 --input 首行写一句中文概括即成标题(flow start 横幅有指引),
|
|
126
|
+
* --title 显式覆盖一切推导。
|
|
127
|
+
*/
|
|
128
|
+
export function deriveChangeTitle(input, max = 50) {
|
|
129
|
+
for (const l of String(input || '').replace(/\r\n/g, '\n').split('\n')) {
|
|
130
|
+
const t = l.trim()
|
|
131
|
+
if (!t) continue
|
|
132
|
+
if (/^成功标准\s*[::]/.test(t)) break
|
|
133
|
+
const stripped = t.replace(/^(?:需求|动机|背景|任务|标题)\s*[::]\s*/, '').trim()
|
|
134
|
+
if (!stripped) continue
|
|
135
|
+
const one = stripped.replace(/\s+/g, ' ')
|
|
136
|
+
return one.length > max ? one.slice(0, max) + '…' : one
|
|
137
|
+
}
|
|
138
|
+
return ''
|
|
139
|
+
}
|
|
140
|
+
|
|
121
141
|
|
|
122
142
|
// ── 平台推送(best-effort,2026-08-16-change-center-quick-tab task-06 / D-003)──
|
|
123
143
|
|
package/src/redomain.js
CHANGED
|
@@ -47,13 +47,23 @@ function newDomainPreamble(domain) {
|
|
|
47
47
|
]
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/** 条目段「变更:<名>」行提取(--by-change 分批迁移的过滤键,坑 fr-domain-suggest 缺陷2) */
|
|
51
|
+
function entrySourceChange(sectionLines) {
|
|
52
|
+
for (const l of sectionLines) {
|
|
53
|
+
const m = /^变更:(.+)$/m.exec(String(l || '').trim())
|
|
54
|
+
if (m) return m[1].trim()
|
|
55
|
+
}
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
58
|
+
|
|
50
59
|
/**
|
|
51
60
|
* 执行迁移(纯函数式落盘)。
|
|
52
|
-
* @param {{knowledgeRoot: string, from: string, to: string, anchors?: string[]}} opts
|
|
61
|
+
* @param {{knowledgeRoot: string, from: string, to: string, anchors?: string[], byChange?: string}} opts
|
|
62
|
+
* byChange:仅迁「变更:<名>」字段匹配的条目(unmapped 等混合池按来源变更分批治理)。
|
|
53
63
|
* @returns {{moved: Array<{id,title}>, sourceDeleted: boolean, targetCreated: boolean, indexLineAdded: boolean}}
|
|
54
64
|
* @throws 源域缺席 / 无匹配条目 / anchor 未命中
|
|
55
65
|
*/
|
|
56
|
-
export function redomainFrEntries({ knowledgeRoot, from, to, anchors = null }) {
|
|
66
|
+
export function redomainFrEntries({ knowledgeRoot, from, to, anchors = null, byChange = null }) {
|
|
57
67
|
const frDir = join(knowledgeRoot, 'fr')
|
|
58
68
|
const fromPath = join(frDir, `${from}.md`)
|
|
59
69
|
const toPath = join(frDir, `${to}.md`)
|
|
@@ -75,10 +85,13 @@ export function redomainFrEntries({ knowledgeRoot, from, to, anchors = null }) {
|
|
|
75
85
|
const id = frEntryId(s.lines[0] || '')
|
|
76
86
|
if (!id) { staying.push(s); continue } // 非条目段(说明/杂项)留守
|
|
77
87
|
if (anchorSet && !anchorSet.has(id)) { staying.push(s); continue }
|
|
88
|
+
if (byChange && entrySourceChange(s.lines) !== byChange) { staying.push(s); continue }
|
|
78
89
|
moving.push({ id, title: (FR_HEADER_RE.exec(s.lines[0])?.[2] || '').trim(), section: s })
|
|
79
90
|
}
|
|
80
91
|
if (moving.length === 0) {
|
|
81
|
-
throw new Error(anchorSet
|
|
92
|
+
throw new Error(anchorSet
|
|
93
|
+
? `anchor 未命中(${[...anchorSet].join('、')} 不在 fr/${from}.md)`
|
|
94
|
+
: byChange ? `「变更:${byChange}」条目在 fr/${from}.md 无命中` : `fr/${from}.md 无 FR 条目段`)
|
|
82
95
|
}
|
|
83
96
|
if (anchorSet && moving.length !== anchorSet.size) {
|
|
84
97
|
const got = new Set(moving.map((m) => m.id))
|
|
@@ -119,8 +132,8 @@ export function redomainFrEntries({ knowledgeRoot, from, to, anchors = null }) {
|
|
|
119
132
|
}
|
|
120
133
|
}
|
|
121
134
|
|
|
122
|
-
/**
|
|
123
|
-
export function planRedomain({ knowledgeRoot, from, to, anchors = null }) {
|
|
135
|
+
/** 干跑预览:列出将迁移条目,不落盘(byChange 同执行侧过滤) */
|
|
136
|
+
export function planRedomain({ knowledgeRoot, from, to, anchors = null, byChange = null }) {
|
|
124
137
|
const frDir = join(knowledgeRoot, 'fr')
|
|
125
138
|
const fromPath = join(frDir, `${from}.md`)
|
|
126
139
|
if (!existsSync(fromPath)) throw new Error(`源域文件不存在:fr/${from}.md`)
|
|
@@ -132,10 +145,13 @@ export function planRedomain({ knowledgeRoot, from, to, anchors = null }) {
|
|
|
132
145
|
const id = frEntryId(s.lines[0] || '')
|
|
133
146
|
if (!id) continue
|
|
134
147
|
if (anchorSet && !anchorSet.has(id)) continue
|
|
135
|
-
|
|
148
|
+
if (byChange && entrySourceChange(s.lines) !== byChange) continue
|
|
149
|
+
candidates.push({ id, title: (FR_HEADER_RE.exec(s.lines[0])?.[2] || '').trim(), sourceChange: entrySourceChange(s.lines) })
|
|
136
150
|
}
|
|
137
151
|
if (candidates.length === 0) {
|
|
138
|
-
throw new Error(anchorSet
|
|
152
|
+
throw new Error(anchorSet
|
|
153
|
+
? `anchor 未命中(${[...anchorSet].join('、')})`
|
|
154
|
+
: byChange ? `「变更:${byChange}」条目在 fr/${from}.md 无命中` : `fr/${from}.md 无 FR 条目段`)
|
|
139
155
|
}
|
|
140
156
|
return {
|
|
141
157
|
from,
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* route-hindsight.js — 轻量道「疑似该走预段未走」事后闭环(2026-09-28-unclear-req-to-brainstorm FR-02)。
|
|
3
|
+
*
|
|
4
|
+
* 三层分工中的后门层:前门盘问(flow.js 渲染)管入口弱信号,本模块管收口事后指标——
|
|
5
|
+
* flow done 收口时计算过程形态指标,任一超阈即标记落库,下次 flow start 点名提示
|
|
6
|
+
* (「疑似」非定罪、可无视),形成 per-repo 自校准回路。
|
|
7
|
+
*
|
|
8
|
+
* D-003 禁区(rejected 决策,逐字遵守):全部指标为封闭面——行级 diff 比例与记录计数,
|
|
9
|
+
* 零关键词/词表语义判定。需求清不清晰的判定归 agent,机器只产过程形态信号(D-005 弱信号)。
|
|
10
|
+
*
|
|
11
|
+
* 数据面(均 .runtime 下,跨平台 join + 原子写):
|
|
12
|
+
* - 首版快照 route-hindsight-baseline-<change>.json:start/adopt 机器稿首次落盘后由
|
|
13
|
+
* flow.js 调 snapshotBaseline 存 design.md/tasks.md 全文副本;首写者胜(幂等,R-04:
|
|
14
|
+
* 快照锚定起草时点,重入不覆盖)。
|
|
15
|
+
* - 标记文件 route-hindsight.json:per-repo 单条,每次标记整文件覆盖——提示语义即
|
|
16
|
+
* 「上个轻量变更」。
|
|
17
|
+
*
|
|
18
|
+
* 指标四元组(口径):
|
|
19
|
+
* designRewriteRatio / tasksRewriteRatio:终稿 vs 首版快照的行级改写比——复用
|
|
20
|
+
* flow-draft.computeEditRatio 同款 LCS 口径(改写行/首版行数,纯增行不计入;CRLF/LF
|
|
21
|
+
* 归一后比对)。diff 面先做结构归一:剔 frontmatter 块、HTML 注释标记行(<!--)、空行
|
|
22
|
+
* ——惰性脚手架行(机器稿里约占半数)会把真实换血稀释到阈值以下(实测整写机器段仅得
|
|
23
|
+
* 0.175-0.275,0.5 阈值永不可达=闭环失效面);归一是纯结构过滤零语义判定(D-003)。
|
|
24
|
+
* tasks 面另做勾选框状态归一(normalizeTaskCheckbox:[x]/[X]→[ ]——v2 镜像任务行
|
|
25
|
+
* 翻格是进度簿记非内容改写,2026-10-05-hindsight-checkbox-noise 修复前全勾即 N/(N+1)
|
|
26
|
+
* 恒超阈);design 面不归一。无快照(旧变更/起草失败)= 0(零信号,不是「实测 0% 改写」)。
|
|
27
|
+
* blindDims:收口评审 review.json 的 dimensionNotes 中值为 'finding' 的维数——枚举值
|
|
28
|
+
* 计数(评审员已按任务书 schema 判定的结构化面),非机器语义判定;无 review.json
|
|
29
|
+
* (评审豁免)= 0。
|
|
30
|
+
* testFailures:.runtime/verify-runs/<ts>/test-result.json 记录面中 change 归属本变更且
|
|
31
|
+
* status='failed' 的实测失败次数——每次失败收口尝试各落一条(writeRunResult 既有面)。
|
|
32
|
+
* flow-state substeps 无失败计数面(执行期裁决 D-006:数据源从卡面所述「flow-state
|
|
33
|
+
* substeps」改读同为既有记录面的 verify-runs 时间线,封闭面语义不变)。
|
|
34
|
+
*/
|
|
35
|
+
import { existsSync, readFileSync, readdirSync, mkdirSync } from 'node:fs'
|
|
36
|
+
import { createHash } from 'node:crypto'
|
|
37
|
+
import { join, basename, dirname } from 'node:path'
|
|
38
|
+
import { writeAtomicSync } from './fs-atomic.js'
|
|
39
|
+
import { computeEditRatio } from './flow-draft.js'
|
|
40
|
+
|
|
41
|
+
/** 超阈判定常量(可调;口径:比例两路为严格大于——恰等于阈值不算超,保守防误标 R-01;
|
|
42
|
+
* 计数两路为大于等于——2 维/2 次即系统性信号)。 */
|
|
43
|
+
export const HINDSIGHT_THRESHOLDS = {
|
|
44
|
+
designRewriteRatio: 0.5, // design 机器稿改写比 > 0.5(50% 首版行被改写)
|
|
45
|
+
tasksRewriteRatio: 0.6, // tasks 机器预填稿改写率 > 0.6
|
|
46
|
+
blindDims: 2, // 评审盲维命中(dimensionNotes='finding')≥ 2 维
|
|
47
|
+
testFailures: 2, // 收口实测失败记录 ≥ 2 次
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const BASELINE_PREFIX = 'route-hindsight-baseline-'
|
|
51
|
+
const MARK_FILE = 'route-hindsight.json'
|
|
52
|
+
|
|
53
|
+
/** CRLF/LF 归一(Windows/Linux/macOS 换行兼容——未归一的行比对会把换行差异计成改写)。 */
|
|
54
|
+
function norm(text) {
|
|
55
|
+
return String(text || '').replace(/\r\n/g, '\n')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** diff 面结构归一:剔 frontmatter 块、HTML 注释标记行(<!-- 机器段/槽标记)、空行、
|
|
59
|
+
* 引导行(> 起头——v2 纯 markdown 起草的头注指引是惰性脚手架,与 v1 由 MACHINE-DRAFT
|
|
60
|
+
* 段剔除覆盖同职责;不剔会把改写比稀释到阈下,2026-10-04-thin-docs-v2 实证)——
|
|
61
|
+
* 只留正文行(惰性脚手架行不稀释改写比;纯结构过滤,零语义判定)。 */
|
|
62
|
+
function contentSurface(text) {
|
|
63
|
+
const lines = norm(text).split('\n')
|
|
64
|
+
const out = []
|
|
65
|
+
let fmOpen = false, fmDone = false
|
|
66
|
+
for (let i = 0; i < lines.length; i++) {
|
|
67
|
+
const l = lines[i]
|
|
68
|
+
if (!fmDone) {
|
|
69
|
+
if (i === 0 && l === '---') { fmOpen = true; continue }
|
|
70
|
+
if (fmOpen) { if (l === '---') { fmOpen = false; fmDone = true } continue }
|
|
71
|
+
fmDone = true // 首行非 ---:无 frontmatter 的裸文本,全行入面
|
|
72
|
+
}
|
|
73
|
+
if (l.startsWith('<!--')) continue
|
|
74
|
+
if (l.startsWith('>')) continue
|
|
75
|
+
if (l.trim() === '') continue
|
|
76
|
+
out.push(l)
|
|
77
|
+
}
|
|
78
|
+
return out.join('\n')
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** tasks 勾选框状态归一:行首 `- [x]`/`- [X]`/`- [ ]` → `- [ ]`(仅 tasks 比对面消费——
|
|
82
|
+
* v2 镜像任务行翻格(勾选)是进度簿记不是内容改写,2026-10-05 四变更实测全勾即 N/(N+1)
|
|
83
|
+
* 恒超 0.6 阈误标「疑似该走预段」;纯结构 token 替换,D-003 封闭面零语义判定)。 */
|
|
84
|
+
function normalizeTaskCheckbox(text) {
|
|
85
|
+
return norm(text).replace(/^(\s*[-*]\s+)\[[ xX]\]/gm, '$1[ ]')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function readTextSafe(p) {
|
|
89
|
+
try { return readFileSync(p, 'utf8') } catch { return null }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function round4(x) {
|
|
93
|
+
return Math.round(x * 10000) / 10000
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function num(v) {
|
|
97
|
+
const n = Number(v)
|
|
98
|
+
return Number.isFinite(n) ? n : 0
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 首版快照:机器稿起草时点(start 的 draftAll / adopt·resume 的 redraftMissingArtifacts
|
|
103
|
+
* 首次落盘后)由 flow.js 调用。首写者胜——已存在不覆盖(重入/续跑取到的都是首版,R-04)。
|
|
104
|
+
* 缺件(起草失败的文件)存 null,指标面按零信号处理。
|
|
105
|
+
* @returns {{ captured: boolean, path?: string, reason?: string }}
|
|
106
|
+
*/
|
|
107
|
+
export function snapshotBaseline({ specBase, change, changeDir }) {
|
|
108
|
+
try {
|
|
109
|
+
const runtimeRoot = join(specBase, '.runtime')
|
|
110
|
+
const p = join(runtimeRoot, `${BASELINE_PREFIX}${change}.json`)
|
|
111
|
+
if (existsSync(p)) return { captured: false, reason: 'already-exists(首写者胜)' }
|
|
112
|
+
const payload = {
|
|
113
|
+
schemaVersion: 1,
|
|
114
|
+
change,
|
|
115
|
+
capturedAt: new Date().toISOString(),
|
|
116
|
+
design: readTextSafe(join(changeDir, 'design.md')),
|
|
117
|
+
tasks: readTextSafe(join(changeDir, 'tasks.md')),
|
|
118
|
+
}
|
|
119
|
+
mkdirSync(runtimeRoot, { recursive: true })
|
|
120
|
+
writeAtomicSync(p, JSON.stringify(payload, null, 2) + '\n')
|
|
121
|
+
return { captured: true, path: p }
|
|
122
|
+
} catch (e) {
|
|
123
|
+
return { captured: false, reason: (e && e.message) || String(e) }
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 基线文件 sha256(2026-09-28-sentinel-waiver-hardening:flow-state 锚定用——start/adopt 快照
|
|
129
|
+
* 时点记哈希,哨兵消费时校验,防 .runtime 明文基线被事后改写伪装镜像)。缺文件 → null。
|
|
130
|
+
*/
|
|
131
|
+
export function baselineSha256({ specBase, change }) {
|
|
132
|
+
try {
|
|
133
|
+
const p = join(specBase, '.runtime', `${BASELINE_PREFIX}${change}.json`)
|
|
134
|
+
if (!existsSync(p)) return null
|
|
135
|
+
return createHash('sha256').update(readTextSafe(p)).digest('hex')
|
|
136
|
+
} catch {
|
|
137
|
+
return null
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 机器稿基线读取面退役(2026-10-07-thin-tasks-v3):哨兵镜像豁免与收口代勾下线后,
|
|
143
|
+
* readBaselineTasks/readBaselineTasksVerified(哨兵侧判别源)零消费删除——基线快照本体
|
|
144
|
+
* (snapshotBaseline/baselineSha256)保留,供 hindsight 改写比指标(computeHindsightMetrics
|
|
145
|
+
* 直读快照 JSON)与 flow-state 锚定。
|
|
146
|
+
*/
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* 收口指标计算(封闭面:只读文件与既有记录,无语义判定)。
|
|
150
|
+
* @param {{ changeDir: string, reviewJson: object|null, flowState: object|null }} p
|
|
151
|
+
* changeDir 布局=<specBase>/changes/<change>(specBase 与 change 名据此派生);
|
|
152
|
+
* reviewJson=flow done 评审子步已校验的 review.json 解析对象(豁免时 null);
|
|
153
|
+
* flowState=flow-state 对象(仅随 raw 记录,不参与判定)。
|
|
154
|
+
* @returns {{ designRewriteRatio: number, tasksRewriteRatio: number, blindDims: number, testFailures: number, raw: object }}
|
|
155
|
+
*/
|
|
156
|
+
export function computeHindsightMetrics({ changeDir, reviewJson, flowState }) {
|
|
157
|
+
const raw = { baselinePresent: false }
|
|
158
|
+
const change = basename(changeDir)
|
|
159
|
+
const specBase = dirname(dirname(changeDir))
|
|
160
|
+
|
|
161
|
+
// ① 首版快照 → 终稿行级改写比
|
|
162
|
+
let designRewriteRatio = 0
|
|
163
|
+
let tasksRewriteRatio = 0
|
|
164
|
+
const baselineRaw = readTextSafe(join(specBase, '.runtime', `${BASELINE_PREFIX}${change}.json`))
|
|
165
|
+
let baseline = null
|
|
166
|
+
if (baselineRaw != null) { try { const j = JSON.parse(baselineRaw); baseline = j && typeof j === 'object' ? j : null } catch { baseline = null } }
|
|
167
|
+
if (baseline) {
|
|
168
|
+
raw.baselinePresent = true
|
|
169
|
+
const pairs = [['design', 'designRewriteRatio'], ['tasks', 'tasksRewriteRatio']]
|
|
170
|
+
for (const [key, outKey] of pairs) {
|
|
171
|
+
// tasks 面比对前先勾选框归一(翻格=簿记非改写);design 面不归一(无此形态,保原口径)
|
|
172
|
+
const pre = key === 'tasks' ? normalizeTaskCheckbox : (t) => t
|
|
173
|
+
const first = typeof baseline[key] === 'string' ? contentSurface(pre(baseline[key])) : null
|
|
174
|
+
const finalText = readTextSafe(join(changeDir, `${key}.md`))
|
|
175
|
+
if (first && finalText != null) {
|
|
176
|
+
const surface = contentSurface(pre(finalText))
|
|
177
|
+
const ratio = round4(computeEditRatio(first, surface))
|
|
178
|
+
if (outKey === 'designRewriteRatio') designRewriteRatio = ratio
|
|
179
|
+
else tasksRewriteRatio = ratio
|
|
180
|
+
raw[`${key}BaselineLines`] = first.split('\n').length
|
|
181
|
+
raw[`${key}FinalLines`] = surface.split('\n').length
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ② 评审盲维命中:dimensionNotes 枚举值计数('finding'——评审员结构化判定面)
|
|
187
|
+
let blindDims = 0
|
|
188
|
+
const dims = reviewJson && typeof reviewJson === 'object' && reviewJson.dimensionNotes
|
|
189
|
+
&& typeof reviewJson.dimensionNotes === 'object' && !Array.isArray(reviewJson.dimensionNotes)
|
|
190
|
+
? reviewJson.dimensionNotes : null
|
|
191
|
+
if (dims) {
|
|
192
|
+
for (const v of Object.values(dims)) if (v === 'finding') blindDims++
|
|
193
|
+
raw.dimensionNotesKeys = Object.keys(dims).length
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ③ 实测失败次数:verify-runs 记录面按 change 归属计数(status='failed' 枚举)
|
|
197
|
+
let testFailures = 0
|
|
198
|
+
const runsDir = join(specBase, '.runtime', 'verify-runs')
|
|
199
|
+
try {
|
|
200
|
+
for (const e of readdirSync(runsDir, { withFileTypes: true })) {
|
|
201
|
+
if (!e.isDirectory()) continue
|
|
202
|
+
const tr = readTextSafe(join(runsDir, e.name, 'test-result.json'))
|
|
203
|
+
if (tr == null) continue
|
|
204
|
+
try {
|
|
205
|
+
const j = JSON.parse(tr)
|
|
206
|
+
if (j && typeof j === 'object' && j.change === change && j.status === 'failed') testFailures++
|
|
207
|
+
} catch { /* 损坏记录不计 */ }
|
|
208
|
+
}
|
|
209
|
+
} catch { /* 无 verify-runs 目录 = 0 */ }
|
|
210
|
+
raw.failedRunCount = testFailures
|
|
211
|
+
|
|
212
|
+
// flowState 仅随 raw 留档(不参与判定——判定面恒为上方三个封闭面)
|
|
213
|
+
raw.flowTier = flowState && flowState.tier ? String(flowState.tier) : null
|
|
214
|
+
raw.adopted = Boolean(flowState && flowState.adopted_from)
|
|
215
|
+
|
|
216
|
+
return { designRewriteRatio, tasksRewriteRatio, blindDims, testFailures, raw }
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* 超阈判定+标记落库(.sillyspec/.runtime/route-hindsight.json,per-repo 单条整文件覆盖)。
|
|
221
|
+
* @param {{ cwd: string, specBase: string, change: string, metrics: object }} p
|
|
222
|
+
* @returns {{ marked: boolean, reasons: string[] }}
|
|
223
|
+
*/
|
|
224
|
+
export function markHindsight({ cwd, specBase, change, metrics }) {
|
|
225
|
+
const base = specBase || join(cwd || '.', '.sillyspec')
|
|
226
|
+
const m = metrics || {}
|
|
227
|
+
const reasons = []
|
|
228
|
+
if (num(m.designRewriteRatio) > HINDSIGHT_THRESHOLDS.designRewriteRatio) {
|
|
229
|
+
reasons.push(`design 机器稿改写比 ${num(m.designRewriteRatio)} > ${HINDSIGHT_THRESHOLDS.designRewriteRatio}`)
|
|
230
|
+
}
|
|
231
|
+
if (num(m.tasksRewriteRatio) > HINDSIGHT_THRESHOLDS.tasksRewriteRatio) {
|
|
232
|
+
reasons.push(`tasks 机器预填稿改写率 ${num(m.tasksRewriteRatio)} > ${HINDSIGHT_THRESHOLDS.tasksRewriteRatio}`)
|
|
233
|
+
}
|
|
234
|
+
if (num(m.blindDims) >= HINDSIGHT_THRESHOLDS.blindDims) {
|
|
235
|
+
reasons.push(`评审盲维命中 ${num(m.blindDims)} 维 ≥ ${HINDSIGHT_THRESHOLDS.blindDims}`)
|
|
236
|
+
}
|
|
237
|
+
if (num(m.testFailures) >= HINDSIGHT_THRESHOLDS.testFailures) {
|
|
238
|
+
reasons.push(`实测失败 ${num(m.testFailures)} 次 ≥ ${HINDSIGHT_THRESHOLDS.testFailures}`)
|
|
239
|
+
}
|
|
240
|
+
if (reasons.length === 0) return { marked: false, reasons: [] }
|
|
241
|
+
const runtimeRoot = join(base, '.runtime')
|
|
242
|
+
mkdirSync(runtimeRoot, { recursive: true })
|
|
243
|
+
writeAtomicSync(join(runtimeRoot, MARK_FILE), JSON.stringify({
|
|
244
|
+
change,
|
|
245
|
+
marked_at: new Date().toISOString(),
|
|
246
|
+
metrics: {
|
|
247
|
+
designRewriteRatio: num(m.designRewriteRatio),
|
|
248
|
+
tasksRewriteRatio: num(m.tasksRewriteRatio),
|
|
249
|
+
blindDims: num(m.blindDims),
|
|
250
|
+
testFailures: num(m.testFailures),
|
|
251
|
+
},
|
|
252
|
+
reasons,
|
|
253
|
+
}, null, 2) + '\n')
|
|
254
|
+
return { marked: true, reasons }
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* 下次 flow start 的点名提示读取。无文件/无标记/损坏 → null(新装零影响——无 hindsight
|
|
259
|
+
* 文件的仓 flow start 输出与现状完全一致)。
|
|
260
|
+
* @returns { string | null }
|
|
261
|
+
*/
|
|
262
|
+
export function readHindsightHint({ specBase }) {
|
|
263
|
+
try {
|
|
264
|
+
const raw = readFileSync(join(specBase, '.runtime', MARK_FILE), 'utf8')
|
|
265
|
+
const j = JSON.parse(raw)
|
|
266
|
+
if (!j || typeof j !== 'object' || !j.change) return null
|
|
267
|
+
const m = j.metrics || {}
|
|
268
|
+
const pct = (v) => (Number.isFinite(Number(v)) ? `${Math.round(Number(v) * 100)}%` : '?')
|
|
269
|
+
const why = Array.isArray(j.reasons) && j.reasons.length > 0 ? j.reasons.join(';') : '过程形态超阈'
|
|
270
|
+
return `🕰️ route-hindsight:上个轻量变更「${j.change}」疑似该走头脑风暴预段未走(${why};形态计数——design 改写比 ${pct(m.designRewriteRatio)}、tasks 改写率 ${pct(m.tasksRewriteRatio)}、评审盲维命中 ${m.blindDims ?? '?'}、实测失败 ${m.testFailures ?? '?'} 次)。「疑似」是过程形态信号非定罪——本次需求确实已含决策可无视;若拿不准,先 run brainstorm --change <名> 再回来收编。`
|
|
271
|
+
} catch {
|
|
272
|
+
return null
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export default { HINDSIGHT_THRESHOLDS, snapshotBaseline, computeHindsightMetrics, markHindsight, readHindsightHint }
|
|
@@ -70,7 +70,9 @@ export async function executeArchiveDistill({ cwd, specBase, changeName }) {
|
|
|
70
70
|
}
|
|
71
71
|
// 删除缺口探针(D-008 护栏③):观察信号,不算 L3 门禁——L1 无删除声明义务,只采趋势数据
|
|
72
72
|
for (const u of (fr && fr.unreferenced) || []) {
|
|
73
|
-
|
|
73
|
+
// ids(2026-10-03-fr-governance-telemetry):条目级信号透传(帽 20 已在探针端裁)——
|
|
74
|
+
// stats 裁决候选视图聚合键;缺席(异常形态)省字段不伪造。
|
|
75
|
+
appendKnowledgeHit(runtimeRoot, { type: 'fr-unreferenced', change: changeName, domain: u.domain, count: u.count, ...(Array.isArray(u.ids) && u.ids.length > 0 ? { ids: u.ids } : {}) })
|
|
74
76
|
console.log(` ℹ️ [观察信号·不算 L3 门禁] 触达域 ${u.domain} 有 ${u.count} 条 active FR 未被本次承接引用(fr-unreferenced 遥测,L3 裁决趋势数据)`)
|
|
75
77
|
}
|
|
76
78
|
} catch (e) {
|
package/src/run/command.js
CHANGED
|
@@ -29,7 +29,7 @@ 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'
|
|
31
31
|
import { runStage } from './stage.js'
|
|
32
|
-
import { sanitizeDesc } from '../quicklog.js'
|
|
32
|
+
import { sanitizeDesc, deriveChangeTitle } from '../quicklog.js'
|
|
33
33
|
import { ProgressManager, resolveSessionIdentity } from '../progress.js'
|
|
34
34
|
import { validateChangeExists, checkTransition } from '../stage-contract.js'
|
|
35
35
|
import { READONLY_AUXILIARY_STAGES } from '../constants.js'
|
|
@@ -215,6 +215,22 @@ export async function ensureStageSteps(progress, stageName, cwd, specDir = null)
|
|
|
215
215
|
* run <stage> --help/-h 的用法帮助(runCommand 内 --help 短路调用)。
|
|
216
216
|
* 只打印不落盘:帮助查询不该有副作用(建会话/写 QUICKLOG)。
|
|
217
217
|
*/
|
|
218
|
+
/**
|
|
219
|
+
* 只读空态引导文案(2026-10-05-status-empty-guide 首创;2026-10-06-status-multi-active-list
|
|
220
|
+
* 抽出单一源——库不在场前置门与 read 后空分支共用一份,防两处文案漂移)。静态文案——引用
|
|
221
|
+
* 通用命令形态而非本仓状态(空态定义即无数据可读)。可照抄实例(2026-10-05-input-teach-copyable):
|
|
222
|
+
* 引号内换行合法,extractSuccessCriteria 行级 trim 容忍缩进,带缩进照抄亦过门。
|
|
223
|
+
*/
|
|
224
|
+
function emitReadonlyEmptyGuide() {
|
|
225
|
+
console.log('ℹ️ 未找到进度数据(只读查询不建变更)')
|
|
226
|
+
console.log(' → 可开始轻量变更(--input 引号内换行合法,下例可照抄):')
|
|
227
|
+
console.log(' sillyspec flow start --change <YYYY-MM-DD-名> --input "<动机与背景>')
|
|
228
|
+
console.log('')
|
|
229
|
+
console.log(' 成功标准:')
|
|
230
|
+
console.log(' - <可验证标准>"')
|
|
231
|
+
console.log(' → 需求不明可 sillyspec run brainstorm --change <名> 先探索')
|
|
232
|
+
}
|
|
233
|
+
|
|
218
234
|
function printStageUsage(stageName) {
|
|
219
235
|
const stage = stageRegistry[stageName]
|
|
220
236
|
console.log(`
|
|
@@ -833,7 +849,11 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
833
849
|
quickSessionId = recent
|
|
834
850
|
quickFallbackUsed = true
|
|
835
851
|
} else {
|
|
836
|
-
console.log(' 本仓无近期 quick 会话记录(current marker 与 quick-sessions
|
|
852
|
+
console.log(' 本仓无近期 quick 会话记录(current marker 与 quick-sessions 均空)——新工作请走轻量变更(--input 引号内换行合法,可照抄):')
|
|
853
|
+
console.log(' sillyspec flow start --change <YYYY-MM-DD-名> --input "<动机与背景>')
|
|
854
|
+
console.log('')
|
|
855
|
+
console.log(' 成功标准:')
|
|
856
|
+
console.log(' - <可验证标准>"')
|
|
837
857
|
process.exit(0)
|
|
838
858
|
}
|
|
839
859
|
} else {
|
|
@@ -1111,9 +1131,23 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
1111
1131
|
}
|
|
1112
1132
|
}
|
|
1113
1133
|
|
|
1134
|
+
// 只读辅助阶段的写意图 flag 清单(下方两处只读短路共用:库不在场前置门 + read 后短路)
|
|
1135
|
+
const READONLY_WRITE_ACTIONS = ['--cleanup-remnant', '--align-execute-progress', '--done', '--skip', '--reset', '--reopen', '--wait', '--continue']
|
|
1136
|
+
|
|
1137
|
+
// 只读前置门(2026-10-06-status-multi-active-list):库文件不在场时不得进入 pm.read()——
|
|
1138
|
+
// read() 无显式 --change 先走 listChanges→_ensureDB,会凭空新建空库(只读承诺破防,实测);
|
|
1139
|
+
// 空库撞残留 schema-version 戳无表可查,首个 SELECT 直接崩(no such table: changes)。
|
|
1140
|
+
// 库不在场=字面意义的零进度:与零活跃同款空态引导 + exit 0,零落盘。
|
|
1141
|
+
if (READONLY_AUXILIARY_STAGES.includes(stageName) && !flags.some(f => READONLY_WRITE_ACTIONS.includes(f))
|
|
1142
|
+
&& !existsSync(pm._runtimePath(cwd, 'sillyspec.db'))) {
|
|
1143
|
+
emitReadonlyEmptyGuide()
|
|
1144
|
+
process.exit(0)
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1114
1147
|
let progress = pm.read(cwd, changeName)
|
|
1115
1148
|
|
|
1116
|
-
// task-03 (D-005@v2 / FR-04): READONLY_AUXILIARY_STAGES(status
|
|
1149
|
+
// task-03 (D-005@v2 / FR-04): READONLY_AUXILIARY_STAGES(status;doctor 已于 2026-09-09-doctor-noai
|
|
1150
|
+
// 移出——阶段形态走状态机,顶层 doctor 只读语义由 index.js case 头部分流保真)查询只读短路。
|
|
1117
1151
|
// 无显式写 flag 时在 registerChange / ensureStageSteps 之前拦截,零副作用:
|
|
1118
1152
|
// - 目标 progress 不存在 → 提示「只读查询不建变更」exit 0(不 initChange、不建 default 行,治 8b)
|
|
1119
1153
|
// - 目标 progress 存在 → 只读展示,不 seed steps、不刷新 lastActive、不 registerChange
|
|
@@ -1121,10 +1155,25 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
1121
1155
|
// 显式写意图不短路:doctor 写操作 flag(--cleanup-remnant / --align-execute-progress)与
|
|
1122
1156
|
// 步骤动作 flag(--done/--skip/--reset/--reopen/--wait/--continue)走原写路径;--status 本身
|
|
1123
1157
|
// 是只读查询,纳入短路。--fix 是 worktree doctor 的 flag 不在此列(design.md Phase 4)。
|
|
1124
|
-
const READONLY_WRITE_ACTIONS = ['--cleanup-remnant', '--align-execute-progress', '--done', '--skip', '--reset', '--reopen', '--wait', '--continue']
|
|
1125
1158
|
if (READONLY_AUXILIARY_STAGES.includes(stageName) && !flags.some(f => READONLY_WRITE_ACTIONS.includes(f))) {
|
|
1126
1159
|
if (!progress) {
|
|
1127
|
-
|
|
1160
|
+
// 多活跃分流(2026-10-06-status-multi-active-list):read() 无显式 --change 在 ≥2 活跃
|
|
1161
|
+
// 时同样返回 null——「有数据但需要点名」被空态文案一律误报成「无数据」(本仓实测
|
|
1162
|
+
// 10 行 active、2 个目录待收口却被告知未找到)。前置门保证走到这库必在场,listChanges
|
|
1163
|
+
// 纯读;显式 --change 未命中仍走空态引导(点名失败与未点名歧义是两件事)。
|
|
1164
|
+
let activeChanges = []
|
|
1165
|
+
try { activeChanges = pm.listChanges(cwd) || [] } catch { activeChanges = [] }
|
|
1166
|
+
if (!changeName && activeChanges.length >= 2) {
|
|
1167
|
+
console.log(`ℹ️ 进度数据存在,但有 ${activeChanges.length} 个活跃变更(未指定 --change,无法确定展示哪个):`)
|
|
1168
|
+
for (const name of activeChanges) {
|
|
1169
|
+
// DB 行与目录是两个真相层:目录缺失的幽灵行如实标注(清幽灵走既有 doctor --cleanup-remnant)
|
|
1170
|
+
console.log(` - ${name}${existsSync(pm._changePath(cwd, name)) ? '' : '(本地无目录)'}`)
|
|
1171
|
+
}
|
|
1172
|
+
console.log(` → 指定查看其一:sillyspec status --change ${activeChanges[0]}`)
|
|
1173
|
+
console.log(' → 恢复面:sillyspec flow status --change <名>')
|
|
1174
|
+
process.exit(0)
|
|
1175
|
+
}
|
|
1176
|
+
emitReadonlyEmptyGuide()
|
|
1128
1177
|
process.exit(0)
|
|
1129
1178
|
}
|
|
1130
1179
|
// 只读展示路径:复用阶段定义渲染当前步骤 prompt(outputStep 对 status/doctor 纯只读),
|
|
@@ -1225,10 +1274,11 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
1225
1274
|
// 如果指定了变更名或有变更目录,自动初始化变更的 progress
|
|
1226
1275
|
const autoChange = changeName || resolveChangeNameAuto(cwd, specRoot)
|
|
1227
1276
|
if (autoChange) {
|
|
1228
|
-
// 创建时即写 title(与 name/KEY 同时落 db):--input
|
|
1277
|
+
// 创建时即写 title(与 name/KEY 同时落 db):--input 首行推导 deriveChangeTitle(≤50 字,
|
|
1278
|
+
// 用户需求 2026-09-29)优先,长输入兜底 sanitizeDesc 压行,无则用 name 兜底。
|
|
1229
1279
|
// proposal 还没写、无权威标题来源,这是临时值;brainstorm/plan 完成 proposal/design 落盘后由
|
|
1230
1280
|
// complete.js 通用完成路径 deriveTitleFromLinkedChange 刷新为真实 # 标题(与 quick 启动 title 同源)。
|
|
1231
|
-
progress = pm.initChange(cwd, autoChange, { title: inputText ? sanitizeDesc(inputText) : autoChange })
|
|
1281
|
+
progress = pm.initChange(cwd, autoChange, { title: inputText ? (deriveChangeTitle(inputText) || sanitizeDesc(inputText)) : autoChange })
|
|
1232
1282
|
} else if (isAuxiliary) {
|
|
1233
1283
|
let autoName = changeName || resolveChangeNameAuto(cwd, specRoot) || 'default'
|
|
1234
1284
|
// archive 特例:归档后变更从活跃列表排除(listChanges WHERE status='active'),
|
|
@@ -1255,7 +1305,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
1255
1305
|
changeName = autoName
|
|
1256
1306
|
if (!progress) {
|
|
1257
1307
|
// auxiliary 创建也写 title(--input 优先 / name 兜底),同上完整流程语义。
|
|
1258
|
-
progress = pm.initChange(cwd, autoName, { title: inputText ? sanitizeDesc(inputText) : autoName })
|
|
1308
|
+
progress = pm.initChange(cwd, autoName, { title: inputText ? (deriveChangeTitle(inputText) || sanitizeDesc(inputText)) : autoName })
|
|
1259
1309
|
// initChange 可能因 project 表为空返回 null
|
|
1260
1310
|
if (!progress) {
|
|
1261
1311
|
progress = { currentStage: stageName, stages: {}, lastActive: new Date().toLocaleString('zh-CN', { hour12: false }), project: '' }
|
|
@@ -1287,7 +1337,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
1287
1337
|
console.log(` 提示:可以用 --change <名称> 指定自定义变更名`)
|
|
1288
1338
|
console.log(` 或事后重命名:sillyspec change-rename ${autoName} <新名称>`)
|
|
1289
1339
|
// brainstorm 自动创建变更也写 title(--input 需求描述优先 / 自动名兜底),proposal 落盘后刷新。
|
|
1290
|
-
progress = pm.initChange(cwd, autoName, { title: inputText ? sanitizeDesc(inputText) : autoName })
|
|
1340
|
+
progress = pm.initChange(cwd, autoName, { title: inputText ? (deriveChangeTitle(inputText) || sanitizeDesc(inputText)) : autoName })
|
|
1291
1341
|
changeName = autoName
|
|
1292
1342
|
} else if (stageName === 'auto') {
|
|
1293
1343
|
// auto 零活跃建变更(2026-09-08-auto-driver D-003@v1,Grill P1-②):SKILL 启动命令即
|
|
@@ -1303,7 +1353,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
1303
1353
|
const date = new Date().toISOString().slice(0, 10)
|
|
1304
1354
|
const autoName = `${date}-new-change-${randomBytes(4).toString('hex')}`
|
|
1305
1355
|
console.log(`🔄 auto 模式自动创建变更:${autoName}(可用 --change 指定名称,或事后 change-rename)`)
|
|
1306
|
-
progress = pm.initChange(cwd, autoName, { title: inputText ? sanitizeDesc(inputText) : autoName })
|
|
1356
|
+
progress = pm.initChange(cwd, autoName, { title: inputText ? (deriveChangeTitle(inputText) || sanitizeDesc(inputText)) : autoName })
|
|
1307
1357
|
changeName = autoName
|
|
1308
1358
|
} else {
|
|
1309
1359
|
console.error('❌ 未找到进度数据,请先运行 sillyspec init 或指定 --change <变更名>')
|
|
@@ -2010,8 +2060,11 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
|
|
|
2010
2060
|
const classification = classifyChange({ description: inputText || '', explicitMode, localConfig })
|
|
2011
2061
|
if (classification.mode === 'quick') {
|
|
2012
2062
|
console.log(`📊 auto 模式分类:${classification.reason} 判为小变更`)
|
|
2013
|
-
console.log(` 小变更请走轻量变更(quick 通道已退役,v3.30.0
|
|
2014
|
-
console.log(
|
|
2063
|
+
console.log(` 小变更请走轻量变更(quick 通道已退役,v3.30.0 起拒绝新会话;--input 引号内换行合法,可照抄):`)
|
|
2064
|
+
console.log(' sillyspec flow start --change <YYYY-MM-DD-名> --input "<动机与背景>')
|
|
2065
|
+
console.log('')
|
|
2066
|
+
console.log(' 成功标准:')
|
|
2067
|
+
console.log(' - <可验证标准>"')
|
|
2015
2068
|
return
|
|
2016
2069
|
}
|
|
2017
2070
|
console.log(`📊 auto 模式分类:${classification.mode}(${classification.reason})`)
|