sillyspec 3.28.2 → 3.28.4
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 +16 -55
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +1 -1
- package/.claude/skills/sillyspec-continue/SKILL.md +8 -38
- package/.claude/skills/sillyspec-execute/SKILL.md +10 -2
- package/.claude/skills/sillyspec-knowledge/SKILL.md +19 -249
- package/.claude/skills/sillyspec-plan/SKILL.md +1 -1
- package/.claude/skills/sillyspec-propose/SKILL.md +6 -77
- package/.claude/skills/sillyspec-resume/SKILL.md +8 -57
- package/.claude/skills/sillyspec-state/SKILL.md +7 -46
- package/.claude/skills/sillyspec-workspace/SKILL.md +14 -156
- package/AGENTS.md +24 -0
- package/INSTRUCTIONS.md +12 -0
- package/package.json +1 -1
- package/src/archive-delta.js +127 -2
- package/src/change-delete.js +2 -1
- package/src/change-list.js +7 -2
- package/src/change-risk-profile.js +77 -6
- package/src/config-cat.js +12 -1
- package/src/constants.js +9 -1
- package/src/design-facts.js +64 -0
- package/src/dispatch/backends/sillyhub-mcp.js +17 -5
- package/src/docs-check.js +153 -27
- package/src/docs-migrate.js +124 -0
- package/src/doctor-diagnostics.js +441 -5
- package/src/foreign-declared.js +53 -2
- package/src/index.js +353 -28
- package/src/module-impact.js +77 -12
- package/src/plan-adopt-waves.js +13 -5
- package/src/progress/change-registry.js +35 -0
- package/src/progress.js +64 -1
- package/src/quick-recommend.js +6 -1
- package/src/quicklog.js +10 -2
- package/src/run/command.js +159 -13
- package/src/run/complete-handlers.js +162 -9
- package/src/run/complete.js +104 -5
- package/src/run/gates.js +116 -8
- package/src/run/progress-confirm.js +66 -0
- package/src/run/prompt.js +235 -36
- package/src/run/quick-audit.js +12 -5
- package/src/run/scan-profile.js +20 -2
- package/src/run/shared.js +84 -11
- package/src/run/stage.js +42 -0
- package/src/runtime-hygiene.js +84 -0
- package/src/scan-postcheck.js +5 -1
- package/src/spec-sync.js +63 -8
- package/src/stage-contract.js +83 -3
- package/src/stage-review.js +3 -0
- package/src/stages/archive.js +4 -4
- package/src/stages/brainstorm-auto.js +2 -1
- package/src/stages/brainstorm.js +11 -25
- package/src/stages/doctor.js +41 -515
- package/src/stages/execute.js +10 -14
- package/src/stages/plan-postcheck.js +191 -7
- package/src/stages/plan.js +7 -27
- package/src/stages/quick.js +15 -13
- package/src/stages/verify.js +23 -21
- package/src/sync-noise.js +158 -0
- package/src/sync.js +42 -17
- package/src/task-review.js +225 -1
- package/src/verify-facts-schema.js +141 -0
- package/src/verify-postcheck.js +421 -49
- package/src/verify-probes.js +142 -13
- package/src/workflow.js +6 -0
- package/src/worktree-apply.js +51 -13
- package/src/worktree.js +5 -1
- package/templates/agents-instruction.md +1 -1
- package/.claude/skills/verify-per-user/SKILL.md +0 -6
package/src/module-impact.js
CHANGED
|
@@ -10,8 +10,10 @@
|
|
|
10
10
|
* diff 源:resolveVerifyChangedFiles(worktree-aware,与 task-review 同源);
|
|
11
11
|
* module-map:.sillyspec/docs/<p>/modules/_module-map.yaml 的 modules.<id>.paths
|
|
12
12
|
* (目录条目以 / 结尾按前缀匹配,与模块上下文索引读侧口径一致)。
|
|
13
|
+
* 声明源(plan --done 首版,2026-09-08 刀②):design 文件变更清单——代码未写、无 diff,
|
|
14
|
+
* 归属分类同一套 module-map 前缀匹配,仅输入来源不同。
|
|
13
15
|
*/
|
|
14
|
-
import { existsSync, readFileSync, readdirSync } from 'fs'
|
|
16
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'fs'
|
|
15
17
|
import { join } from 'path'
|
|
16
18
|
import { resolveVerifyChangedFiles } from './verify-postcheck.js'
|
|
17
19
|
import { resolveSpecDir } from './run/shared.js'
|
|
@@ -68,11 +70,67 @@ function classifyFile(posixPath, modulePaths) {
|
|
|
68
70
|
|
|
69
71
|
/**
|
|
70
72
|
* 生成 module-impact.md 骨架。
|
|
71
|
-
* @param {{ cwd: string, changeName: string, specDir?: string|null }} opts
|
|
73
|
+
* @param {{ cwd: string, changeName: string, specDir?: string|null, sourceFiles?: string[]|null, origin?: string|null }} opts
|
|
74
|
+
* sourceFiles:声明来源(plan --done 首版用 design 文件变更清单——此时代码未写、无 diff 可取);
|
|
75
|
+
* 缺省走 diff 来源(resolveVerifyChangedFiles,archive/execute 后的真实改动)。
|
|
76
|
+
* origin:骨架首行的生成方说明(区分 plan 首版与 CLI 命令生成,追溯来源用)。
|
|
72
77
|
* @returns {{ markdown: string, matchedCount: number, unmatchedCount: number }|null}
|
|
73
|
-
* 无 module-map /
|
|
78
|
+
* 无 module-map / 无可归类文件 → null(无可归类输入,agent 全手写)
|
|
74
79
|
*/
|
|
75
|
-
|
|
80
|
+
/**
|
|
81
|
+
* 模块文档 sidecar 同步命令化(2026-09-09 刀批,轮次经济学 §3.1 模块文档同步项):
|
|
82
|
+
* quick/archive 两处 prompt 让 agent 手写 sidecar 追加行——机械部分(变更归属模块、
|
|
83
|
+
* sidecar 追加、卡 updated_at 戳)CLI 代算,agent 只留卡片正文语义更新。
|
|
84
|
+
* 幂等:sidecar 已含 `- <change> |` 行则跳过该模块;卡不存在只建 sidecar 不建卡(卡内容是语义)。
|
|
85
|
+
* @returns {{ synced: string[], unmatched: string[], skipped: string[] }}
|
|
86
|
+
*/
|
|
87
|
+
export function syncModuleDocSidecars({ cwd, changeName, note, specDir = null }) {
|
|
88
|
+
const specBase = resolveSpecDir(cwd, { specDir })
|
|
89
|
+
// 复用 generateModuleImpactSkeleton 的 map 发现 + 归类(diff 来源)
|
|
90
|
+
const skel = generateModuleImpactSkeleton({ cwd, changeName, specDir })
|
|
91
|
+
if (!skel) return { synced: [], unmatched: [], skipped: [], reason: '无 module-map 或无 diff 可归类' }
|
|
92
|
+
const modulesDir = findModulesDir(specBase)
|
|
93
|
+
if (!modulesDir) return { synced: [], unmatched: [], skipped: [], reason: 'modules 目录不存在' }
|
|
94
|
+
// 从骨架 markdown 反解归类结果(命中模块集合 + 未匹配清单)
|
|
95
|
+
const hitModules = [...skel.markdown.matchAll(/^\| (\S+) \| `([^`]+)`/gm)].map(m => m[1])
|
|
96
|
+
const unmatched = [...skel.markdown.matchAll(/^- `([^`]+)` /gm)].map(m => m[1])
|
|
97
|
+
const synced = []
|
|
98
|
+
const skipped = []
|
|
99
|
+
for (const mod of [...new Set(hitModules)]) {
|
|
100
|
+
const sidecarPath = join(modulesDir, `${mod}.changelog.md`)
|
|
101
|
+
let sidecar = ''
|
|
102
|
+
try { sidecar = readFileSync(sidecarPath, 'utf8') } catch { sidecar = '' }
|
|
103
|
+
if (sidecar.includes(`- ${changeName} |`)) { skipped.push(mod); continue }
|
|
104
|
+
const NL = String.fromCharCode(10)
|
|
105
|
+
const line = `- ${changeName} | ${note || '(见该变更 quicklog/归档)'}`
|
|
106
|
+
const base = sidecar
|
|
107
|
+
? sidecar.replace(/\n*$/, NL)
|
|
108
|
+
: `# ${mod} 变更索引` + NL + NL
|
|
109
|
+
writeFileSync(sidecarPath, base + line + NL)
|
|
110
|
+
// 卡 updated_at 戳(存在才戳)
|
|
111
|
+
const cardPath = join(modulesDir, `${mod}.md`)
|
|
112
|
+
try {
|
|
113
|
+
const card = readFileSync(cardPath, 'utf8')
|
|
114
|
+
const stamped = card.replace(/^updated_at:.*$/m, `updated_at: ${new Date().toISOString().slice(0, 19) + '+08:00'}`)
|
|
115
|
+
if (stamped !== card) writeFileSync(cardPath, stamped)
|
|
116
|
+
} catch { /* 卡不存在——sidecar 照写,卡内容留给 agent */ }
|
|
117
|
+
synced.push(mod)
|
|
118
|
+
}
|
|
119
|
+
return { synced, unmatched, skipped }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function findModulesDir(specBase) {
|
|
123
|
+
try {
|
|
124
|
+
for (const p of readdirSync(join(specBase, 'docs'), { withFileTypes: true })) {
|
|
125
|
+
if (!p.isDirectory()) continue
|
|
126
|
+
const cand = join(specBase, 'docs', p.name, 'modules')
|
|
127
|
+
if (existsSync(join(cand, '_module-map.yaml'))) return cand
|
|
128
|
+
}
|
|
129
|
+
} catch { /* 无 docs/ */ }
|
|
130
|
+
return null
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function generateModuleImpactSkeleton({ cwd, changeName, specDir = null, sourceFiles = null, origin = null }) {
|
|
76
134
|
const specBase = resolveSpecDir(cwd, { specDir })
|
|
77
135
|
|
|
78
136
|
// 找 _module-map.yaml(docs/<p>/modules/;多项目取首个含 modules 索引的)
|
|
@@ -92,15 +150,20 @@ export function generateModuleImpactSkeleton({ cwd, changeName, specDir = null }
|
|
|
92
150
|
const modulePaths = parseModuleMapPaths(readFileSync(moduleMapPath, 'utf8'))
|
|
93
151
|
if (modulePaths.size === 0) return null
|
|
94
152
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
153
|
+
let sourceFileList
|
|
154
|
+
if (sourceFiles) {
|
|
155
|
+
sourceFileList = sourceFiles.map(f => f.split('\\').join('/')).filter(f => !f.startsWith('.sillyspec/'))
|
|
156
|
+
} else {
|
|
157
|
+
// includeWorkingTree 同 verify module 子集(坑 module-subset-zero-hit-uncommitted):
|
|
158
|
+
// worktree 未提交改动也计入模块影响面(module-impact 按实际 diff 归属模块)
|
|
159
|
+
const diffFiles = resolveVerifyChangedFiles(cwd, changeName, null, { includeWorkingTree: true, specBase }) || []
|
|
160
|
+
sourceFileList = diffFiles.map(f => f.split('\\').join('/')).filter(f => !f.startsWith('.sillyspec/'))
|
|
161
|
+
}
|
|
162
|
+
if (sourceFileList.length === 0) return null
|
|
100
163
|
|
|
101
164
|
const byModule = new Map()
|
|
102
165
|
const unmatched = []
|
|
103
|
-
for (const f of
|
|
166
|
+
for (const f of sourceFileList) {
|
|
104
167
|
const mod = classifyFile(f, modulePaths)
|
|
105
168
|
if (mod) {
|
|
106
169
|
if (!byModule.has(mod)) byModule.set(mod, [])
|
|
@@ -111,7 +174,9 @@ export function generateModuleImpactSkeleton({ cwd, changeName, specDir = null }
|
|
|
111
174
|
}
|
|
112
175
|
|
|
113
176
|
const L = [
|
|
114
|
-
|
|
177
|
+
origin
|
|
178
|
+
? `# 模块影响分析(骨架由 ${origin} 生成)`
|
|
179
|
+
: '# 模块影响分析(骨架由 `sillyspec module-impact --change <变更名>` 生成)',
|
|
115
180
|
'',
|
|
116
181
|
'> 文件×模块归属由 CLI 按 _module-map.yaml paths 前缀匹配预填;',
|
|
117
182
|
'> **影响类型**(逻辑变更/数据结构变更/接口变更/调用关系变更/配置变更/新增)与 review 标记是语义判断,',
|
|
@@ -162,5 +227,5 @@ export function generateModuleImpactSkeleton({ cwd, changeName, specDir = null }
|
|
|
162
227
|
L.push('规则:execute/verify 完成文档同步后把对应行回填 done;确定不同步的行改 skipped 并在操作列写明原因。')
|
|
163
228
|
L.push('')
|
|
164
229
|
|
|
165
|
-
return { markdown: L.join('\n'), matchedCount:
|
|
230
|
+
return { markdown: L.join('\n'), matchedCount: sourceFileList.length - unmatched.length, unmatchedCount: unmatched.length }
|
|
166
231
|
}
|
package/src/plan-adopt-waves.js
CHANGED
|
@@ -60,12 +60,17 @@ function updateSummaryWaveColumn(lines, waveOf) {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
|
-
* @param {{ changeDir: string, dryRun?: boolean }} opts
|
|
63
|
+
* @param {{ changeDir: string, dryRun?: boolean, mode?: 'write'|'proposal' }} opts
|
|
64
|
+
* mode='proposal'(2026-09-09-plan-derived):只读产拓扑布局提案——渲染 Wave 段草稿与
|
|
65
|
+
* W 列同步结果但不落盘,供 postcheck「提案-验证-落盘」自动修复(FR-01)验证后决断写否。
|
|
64
66
|
* @returns {{ ok: boolean, error?: string, waves?: string[][], planPath: string, dryRun: boolean,
|
|
65
67
|
* waveBlock?: string[], tableRowsUpdated?: number, tableRowsSkipped?: number,
|
|
68
|
+
* planMdDraft?: string, rewritten?: boolean,
|
|
66
69
|
* postcheck?: { errors: string[], warnings: string[] } }}
|
|
70
|
+
* planMdDraft:proposal 档下的完整 plan.md 草稿文本;rewritten:与原文比较是否有实际变更
|
|
71
|
+
* (幂等重排零变更 → false,调用方静默);write 档沿用既有 postcheck 字段。
|
|
67
72
|
*/
|
|
68
|
-
export function adoptPlanWaves({ changeDir, dryRun = false }) {
|
|
73
|
+
export function adoptPlanWaves({ changeDir, dryRun = false, mode = 'write' }) {
|
|
69
74
|
const planPath = join(changeDir, 'plan.md')
|
|
70
75
|
if (!existsSync(planPath)) return { ok: false, error: `plan.md 不存在: ${planPath}`, planPath, dryRun }
|
|
71
76
|
const tasksDir = join(changeDir, 'tasks')
|
|
@@ -108,9 +113,11 @@ export function adoptPlanWaves({ changeDir, dryRun = false }) {
|
|
|
108
113
|
|
|
109
114
|
const { updated, skipped } = updateSummaryWaveColumn(lines, waveOf)
|
|
110
115
|
const content = lines.join('\n').replace(/\n*$/, '\n')
|
|
116
|
+
const rewritten = content !== original.replace(/\r\n/g, '\n').replace(/\n*$/, '\n')
|
|
111
117
|
|
|
112
|
-
if (dryRun) {
|
|
113
|
-
return { ok: true, waves, planPath, dryRun: true, waveBlock, tableRowsUpdated: updated, tableRowsSkipped: skipped
|
|
118
|
+
if (dryRun || mode === 'proposal') {
|
|
119
|
+
return { ok: true, waves, planPath, dryRun: true, waveBlock, tableRowsUpdated: updated, tableRowsSkipped: skipped,
|
|
120
|
+
planMdDraft: content, rewritten }
|
|
114
121
|
}
|
|
115
122
|
|
|
116
123
|
writeFileSync(planPath, content, 'utf8')
|
|
@@ -122,5 +129,6 @@ export function adoptPlanWaves({ changeDir, dryRun = false }) {
|
|
|
122
129
|
} catch (e) {
|
|
123
130
|
postcheck = { errors: [`复跑校验异常: ${e?.message || e}`], warnings: [] }
|
|
124
131
|
}
|
|
125
|
-
return { ok: true, waves, planPath, dryRun: false, waveBlock, tableRowsUpdated: updated, tableRowsSkipped: skipped,
|
|
132
|
+
return { ok: true, waves, planPath, dryRun: false, waveBlock, tableRowsUpdated: updated, tableRowsSkipped: skipped,
|
|
133
|
+
rewritten, postcheck }
|
|
126
134
|
}
|
|
@@ -19,6 +19,25 @@ export class ChangeRegistry {
|
|
|
19
19
|
return rows.map(r => r.name);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* 读变更创建时间(IR 严格模式闸门判别源,change: 2026-09-07-ir-hardening D-001@v1)。
|
|
24
|
+
* 只读不抛(与 listChanges 同款 DB 访问):变更不存在 / db 异常 → null——调用方
|
|
25
|
+
* (isStrictChange)按 fail-open 落存量豁免,不误伤。
|
|
26
|
+
* @param {string} cwd
|
|
27
|
+
* @param {string} changeName
|
|
28
|
+
* @returns {string|null} ISO 时间字符串或 null
|
|
29
|
+
*/
|
|
30
|
+
getChangeCreatedAt(cwd, changeName) {
|
|
31
|
+
if (!changeName || typeof changeName !== 'string') return null;
|
|
32
|
+
try {
|
|
33
|
+
const db = this.pm._ensureDB(cwd);
|
|
34
|
+
const row = db.getDb().prepare('SELECT created_at FROM changes WHERE name = ?').get(changeName);
|
|
35
|
+
return row && typeof row.created_at === 'string' ? row.created_at : null;
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
22
41
|
/**
|
|
23
42
|
* 查询单个变更的流程阶段与状态(quick 轻量归档阶段闸用)。
|
|
24
43
|
* 与 readChangeIsolation 不同(展示读,catch → null 宽容):本方法服务权限判定,读失败
|
|
@@ -27,6 +46,22 @@ export class ChangeRegistry {
|
|
|
27
46
|
* @param {string} changeName - 变更名
|
|
28
47
|
* @returns {{ current_stage: string, status: string }|null} 无该行返回 null(未注册目录桩)
|
|
29
48
|
*/
|
|
49
|
+
/**
|
|
50
|
+
* 读某变更某阶段的 completed_at(2026-09-08-ir-verify-facts FR-02:verifyStartAt 基准——
|
|
51
|
+
* evidence 分类核验的 mtime 窗口下界,取 execute 行 completed_at)。只读不抛:无行/读失败
|
|
52
|
+
* 返回 null(调用方走 R-05 fallback,与 getChangeCreatedAt 同容错风格)。
|
|
53
|
+
*/
|
|
54
|
+
getStageCompletedAt(cwd, changeName, stage) {
|
|
55
|
+
try {
|
|
56
|
+
const db = this.pm._ensureDB(cwd);
|
|
57
|
+
const row = db.getDb().prepare(
|
|
58
|
+
`SELECT s.completed_at FROM stages s JOIN changes c ON s.change_id = c.id
|
|
59
|
+
WHERE c.name = ? AND s.stage = ? ORDER BY s.completed_at DESC LIMIT 1`
|
|
60
|
+
).get(changeName, stage);
|
|
61
|
+
return row && row.completed_at ? row.completed_at : null;
|
|
62
|
+
} catch { return null; }
|
|
63
|
+
}
|
|
64
|
+
|
|
30
65
|
getChangeStage(cwd, changeName) {
|
|
31
66
|
const db = this.pm._ensureDB(cwd);
|
|
32
67
|
const sqlDb = db.getDb();
|
package/src/progress.js
CHANGED
|
@@ -25,13 +25,19 @@ import { STAGE_ORDER, MAIN_FLOW_ORDER, VALID_STAGES, STAGE_LABELS, SPEC_DIR_NAME
|
|
|
25
25
|
// resolveSpecDir 单一真相源在 src/run/shared.js(含 home 拒绝守卫,坑 cwd-correction-home-collision
|
|
26
26
|
// 根治:home 下 .sillyspec 恒不命中,防 smoke/临时目录污染自我延续)。此处 re-export 保持
|
|
27
27
|
// 既有 import 路径兼容;run/shared.js 对 progress.js 只有动态 import,无静态循环。
|
|
28
|
-
import { resolveSpecDir, checkPlatformManaged, PLATFORM_MANAGED_FILENAME, isSelfReferentialSpecRoot } from './run/shared.js';
|
|
28
|
+
import { resolveSpecDir, checkPlatformManaged, PLATFORM_MANAGED_FILENAME, isSelfReferentialSpecRoot, isTempResidueSpecRoot } from './run/shared.js';
|
|
29
29
|
export { resolveSpecDir };
|
|
30
30
|
|
|
31
31
|
// 默认规范目录名(相对于 cwd)
|
|
32
32
|
// SPEC_DIR_NAME → ./progress/shared.js(W6 Step9d)
|
|
33
33
|
const RUNTIME_SUBDIR = '.runtime';
|
|
34
34
|
|
|
35
|
+
// import() 的 pre-import 快照保留份数。这类快照只服务「刚 pull 完发现平台盖错、要退回
|
|
36
|
+
// pull 前」这一个场景,最新一份即可覆盖;越旧的快照 schema 可能已 bump,恢复价值随时间衰减。
|
|
37
|
+
// 可用 SILLYSPEC_PREIMPORT_BAK_KEEP 覆盖(灾难排查期想多留几份时)。
|
|
38
|
+
const PRE_IMPORT_BAK_KEEP_DEFAULT = 1;
|
|
39
|
+
const PRE_IMPORT_BAK_RE = /^sillyspec\.db\.pre-import-.*\.bak$/;
|
|
40
|
+
|
|
35
41
|
/**
|
|
36
42
|
* 向上查找含 .sillyspec 目录的祖先目录——实现已收敛至 src/run/shared.js(单一真相源,
|
|
37
43
|
* 含 home 拒绝守卫),此处 re-export。见顶部 import 说明。
|
|
@@ -95,6 +101,18 @@ export function resolvePlatformSpecDir(cwd, explicitSpecDir = null) {
|
|
|
95
101
|
// 指针丢失不该静默当纯本地项目处理(会建本地进度库 → 状态分裂)。
|
|
96
102
|
const decl = checkPlatformManaged(cwd);
|
|
97
103
|
if (decl) {
|
|
104
|
+
// temp 残留降级(2026-09-08 temp 投毒治理):声明 specRoot 在系统 temp 且 cwd 不在
|
|
105
|
+
// temp → 可证伪的测试/联调残留(真实平台 spec 根永不在 temp),与指针路径的 temp
|
|
106
|
+
// 警告(下方 resolvedTmp 分支)及自指声明降级(run/command.js 恢复链)同类。warn +
|
|
107
|
+
// 自动清理声明后按本地模式继续——不清理则每次命令重复 warn,且 disconnect 会连带
|
|
108
|
+
// 清 local.yaml platform 段误伤真实平台连接,不适合作为残留清理手段。
|
|
109
|
+
// 删除失败仍按本地继续(best effort;下次命令重复 warn 提示手动清理)。
|
|
110
|
+
if (isTempResidueSpecRoot(cwd, decl.specRoot)) {
|
|
111
|
+
const declarationPath = join(resolve(cwd), PLATFORM_MANAGED_FILENAME);
|
|
112
|
+
console.warn(`⚠️ 检测到 temp 残留平台接管声明(specRoot 指向系统 temp 目录:${decl.specRoot},测试/联调投毒特征),已自动清理并按本地模式运行: ${declarationPath}`);
|
|
113
|
+
try { unlinkSync(declarationPath) } catch { /* best effort */ }
|
|
114
|
+
return resolveSpecDir(cwd);
|
|
115
|
+
}
|
|
98
116
|
throw new PlatformManagedError({
|
|
99
117
|
declarationPath: join(resolve(cwd), PLATFORM_MANAGED_FILENAME),
|
|
100
118
|
specRoot: decl.specRoot,
|
|
@@ -613,6 +631,14 @@ export class ProgressManager {
|
|
|
613
631
|
if (existsSync(walSidecar)) {
|
|
614
632
|
try { copyFileSync(walSidecar, `${bakPath}-wal`); } catch { /* 侧车消失=已 checkpoint,无碍 */ }
|
|
615
633
|
}
|
|
634
|
+
// 写完即回收旧快照:每次 pull / resolve --take-platform 都新增一份全库快照,无上限时
|
|
635
|
+
// 按同步次数无限累积(实证单仓 292 份 / 317MB)。裁剪放在写入侧而非独立 GC 命令——
|
|
636
|
+
// 靠人工记得跑 GC 等于没有回收。fail-open:卫生动作不能阻断 import 主流程。
|
|
637
|
+
try {
|
|
638
|
+
this._pruneImportBaks(cwd, bakPath);
|
|
639
|
+
} catch (e) {
|
|
640
|
+
console.warn(`⚠️ pre-import 快照裁剪失败(不阻断 import): ${e.message}`);
|
|
641
|
+
}
|
|
616
642
|
|
|
617
643
|
const pushedAt = progressObj.pushed_at ?? null;
|
|
618
644
|
const now = new Date().toISOString();
|
|
@@ -707,6 +733,38 @@ export class ProgressManager {
|
|
|
707
733
|
return { ok: true, imported: cn, reason: undefined, bakPath };
|
|
708
734
|
}
|
|
709
735
|
|
|
736
|
+
/**
|
|
737
|
+
* 滚动裁剪 .runtime/ 下的 pre-import 快照,只留最新 N 份(含各自 -wal 侧车)。
|
|
738
|
+
*
|
|
739
|
+
* 排序依据:文件名内嵌 ISO 时间戳(冒号/点已替换为 '-',见 import() 的 bakTs),
|
|
740
|
+
* 定宽且无进位歧义,字典序 == 时间序,无需 statSync(mtime 会被 copy/同步工具改写)。
|
|
741
|
+
*
|
|
742
|
+
* @param {string} cwd
|
|
743
|
+
* @param {string|null} keepPath - 本次刚写的快照路径,绝不裁(并发 import 互删护栏)
|
|
744
|
+
* @returns {number} 实际删除的快照份数
|
|
745
|
+
*/
|
|
746
|
+
_pruneImportBaks(cwd, keepPath) {
|
|
747
|
+
let keep = Number(process.env.SILLYSPEC_PREIMPORT_BAK_KEEP);
|
|
748
|
+
if (!Number.isInteger(keep) || keep < 1) keep = PRE_IMPORT_BAK_KEEP_DEFAULT;
|
|
749
|
+
|
|
750
|
+
const runtimeDir = this._runtimePath(cwd);
|
|
751
|
+
const baks = readdirSync(runtimeDir).filter(f => PRE_IMPORT_BAK_RE.test(f)).sort();
|
|
752
|
+
const keepName = keepPath ? basename(keepPath) : null;
|
|
753
|
+
const doomed = baks.slice(0, Math.max(0, baks.length - keep)).filter(f => f !== keepName);
|
|
754
|
+
|
|
755
|
+
let removed = 0;
|
|
756
|
+
for (const f of doomed) {
|
|
757
|
+
// 单份删除失败(他进程持句柄 / 权限)不连坐其余份:跳过即可,下次 import 再收。
|
|
758
|
+
try { unlinkSync(join(runtimeDir, f)); } catch { continue; }
|
|
759
|
+
removed++;
|
|
760
|
+
const wal = join(runtimeDir, `${f}-wal`);
|
|
761
|
+
if (existsSync(wal)) {
|
|
762
|
+
try { unlinkSync(wal); } catch { /* 侧车残留无害:不参与恢复链,下次裁剪再收 */ }
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
return removed;
|
|
766
|
+
}
|
|
767
|
+
|
|
710
768
|
/**
|
|
711
769
|
* 本地脏度标记(D-013 / task-04):写入路径末尾更新 changes.last_local_modified_ts。
|
|
712
770
|
* 在写方法的事务内调用则属于该事务(原子,失败整体回滚);import 是例外不调本方法
|
|
@@ -833,10 +891,15 @@ export class ProgressManager {
|
|
|
833
891
|
// ── 变更管理 ──
|
|
834
892
|
|
|
835
893
|
listChanges(cwd) { return this._changeRegistry.listChanges(cwd); }
|
|
894
|
+
/** 读变更创建时间(IR 严格模式闸门判别源,2026-09-07-ir-hardening D-001)——只读不抛,缺行/异常 → null */
|
|
895
|
+
getChangeCreatedAt(cwd, changeName) { return this._changeRegistry.getChangeCreatedAt(cwd, changeName); }
|
|
836
896
|
|
|
837
897
|
// 单变更阶段查询(quick 轻量归档阶段闸):无行 null,读失败抛(调用方 fail-closed)
|
|
838
898
|
getChangeStage(cwd, changeName) { return this._changeRegistry.getChangeStage(cwd, changeName); }
|
|
839
899
|
|
|
900
|
+
// verifyStartAt 基准只读(2026-09-08-ir-verify-facts):execute 行 completed_at;无行 null 走 R-05 fallback
|
|
901
|
+
getStageCompletedAt(cwd, changeName, stage) { return this._changeRegistry.getStageCompletedAt(cwd, changeName, stage); }
|
|
902
|
+
|
|
840
903
|
registerChange(cwd, changeName) { return this._changeRegistry.registerChange(cwd, changeName); }
|
|
841
904
|
|
|
842
905
|
updateChangeIsolation(cwd, changeName, isolation) { return this._changeRegistry.updateChangeIsolation(cwd, changeName, isolation); }
|
package/src/quick-recommend.js
CHANGED
|
@@ -84,12 +84,17 @@ export function recommendChanges({
|
|
|
84
84
|
taskDescription = '',
|
|
85
85
|
}) {
|
|
86
86
|
if (!activeChanges || activeChanges.length === 0) return []
|
|
87
|
+
// 活跃 quick 会话过滤(2026-09-09 §7-3):quick-<hex8> 是会话不是完整流程变更——无
|
|
88
|
+
// design/proposal 语义可关联(冒烟实证:新 quick 被自动挂上另一活跃 quick 当关联变更)。
|
|
89
|
+
// 形态单源 QUICK_SID_RE(run/shared.js);此处不 import 防把 fs 纯函数拖进 shared 依赖链。
|
|
90
|
+
const QUICK_SESSION_ID_RE = /^quick-[0-9a-f]{8}$/
|
|
91
|
+
const candidateChanges = activeChanges.filter((name) => !QUICK_SESSION_ID_RE.test(name))
|
|
87
92
|
|
|
88
93
|
const dirtyFiles = [...new Set([...baselineFiles, ...quickFiles])]
|
|
89
94
|
.filter(f => f && !f.startsWith('.sillyspec/'))
|
|
90
95
|
const descTokens = [...new Set(tokenizeDescription(taskDescription))]
|
|
91
96
|
|
|
92
|
-
const results =
|
|
97
|
+
const results = candidateChanges.map(name => {
|
|
93
98
|
const reasons = []
|
|
94
99
|
|
|
95
100
|
// 信号 1:脏文件命中 design.md 文件变更清单
|
package/src/quicklog.js
CHANGED
|
@@ -294,9 +294,17 @@ export function extractTitleFromResult(result) {
|
|
|
294
294
|
if (!result) return ''
|
|
295
295
|
const m = String(result).match(/需求:([^\n\r]*?)(?:\s+根因:|$)/)
|
|
296
296
|
if (!m) return ''
|
|
297
|
-
|
|
297
|
+
// 不再截到首个标点(2026-09-08 用户反馈②):旧口径迫使 agent 刻意避开标点写标题,
|
|
298
|
+
// 「A,B」被截成「A」反而降低可读性。--req 的模板指引本就要求一句话短标题,
|
|
299
|
+
// 标点由写者自主掌控;CLI 只兜底超长截断。
|
|
300
|
+
let t = m[1].trim()
|
|
298
301
|
if (!t) return ''
|
|
299
|
-
if (t.length > 80)
|
|
302
|
+
if (t.length > 80) {
|
|
303
|
+
// 超长截断优先在标点/空格处断(标题残句可读),找不到就近边界才硬截。
|
|
304
|
+
const cut = t.slice(0, 80)
|
|
305
|
+
const m2 = cut.match(/^[\s\S]*[,。;:,;:]\s*/)
|
|
306
|
+
t = (m2 && m2[0].length >= 40 ? m2[0] : cut).replace(/[,。;:,;:\s]+$/, '') + '…'
|
|
307
|
+
}
|
|
300
308
|
return t
|
|
301
309
|
}
|
|
302
310
|
|
package/src/run/command.js
CHANGED
|
@@ -20,10 +20,10 @@
|
|
|
20
20
|
* './run/stage.js' → './shared.js' 等同级
|
|
21
21
|
*/
|
|
22
22
|
import { basename, join, resolve, dirname } from 'node:path'
|
|
23
|
-
import { existsSync, readdirSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'node:fs'
|
|
23
|
+
import { existsSync, readdirSync, mkdirSync, writeFileSync, readFileSync, rmSync, unlinkSync } from 'node:fs'
|
|
24
24
|
import { randomBytes, randomUUID } from 'node:crypto'
|
|
25
25
|
import { writeAtomicSync } from '../fs-atomic.js'
|
|
26
|
-
import { resolveSpecDir, countAncestorSpecDirs, ancestorSpecDirs, resolveAncestorCeiling, resolveChangeDir, triggerSync, getStageSteps, formatWaitOptions, checkApproval, warnApprovalUnknown, didYouMean, assertSafeChangeName, detectQuickSessionDrift, detectWorktreeSpecDrift, resolveRuntimeRoot, resolveQuickSessionsDir, writePlatformPointer, checkPlatformManaged, isSelfReferentialSpecRoot, PLATFORM_MANAGED_FILENAME } from './shared.js'
|
|
26
|
+
import { resolveSpecDir, countAncestorSpecDirs, ancestorSpecDirs, resolveAncestorCeiling, resolveChangeDir, triggerSync, getStageSteps, formatWaitOptions, checkApproval, warnApprovalUnknown, didYouMean, assertSafeChangeName, detectQuickSessionDrift, detectWorktreeSpecDrift, resolveRuntimeRoot, resolveQuickSessionsDir, writePlatformPointer, checkPlatformManaged, isSelfReferentialSpecRoot, isTempResidueSpecRoot, PLATFORM_MANAGED_FILENAME } from './shared.js'
|
|
27
27
|
import { resolveQuickLinkedChanges } from './quick-audit.js'
|
|
28
28
|
import { outputStep, collectStageWaitHistory } from './prompt.js'
|
|
29
29
|
import { completeStep, skipStep, waitStep, continueStep } from './complete.js'
|
|
@@ -100,11 +100,18 @@ export function looksLikeMsysMangledPath(v) {
|
|
|
100
100
|
/**
|
|
101
101
|
* MSYS 污染告警出口:命中嗅探时向 stderr 打 flag 名 + 值前缀 + 修复指引(不阻断)。
|
|
102
102
|
*/
|
|
103
|
-
function warnMsysMangledFlag(flag, value) {
|
|
103
|
+
function warnMsysMangledFlag(flag, value, { block = false } = {}) {
|
|
104
104
|
if (!looksLikeMsysMangledPath(value)) return
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
const head = `⚠️ ${flag} 的值疑似被 Git Bash(MSYS) 路径转换污染:「${value.slice(0, 60)}${value.length > 60 ? '…' : ''}」`
|
|
106
|
+
const fix = ' 以 / 开头的文案在 Git Bash 下会被展开成 <Git 安装目录>/… 绝对路径后才传入 CLI。\n 非本意 → 去掉前导 / 或改写表述后重发本命令;确需原样 → 命令前加 MSYS_NO_PATHCONV=1。'
|
|
107
|
+
// 2026-09-09 ql-20260909-003(轮次经济学 §3.4):--output/--input 携带落盘语义(标题/正文
|
|
108
|
+
// 会写进 QUICKLOG),warn 会被忽略继续落脏标题——升 exit 2 阻断;其余调用点维持 warn。
|
|
109
|
+
if (block) {
|
|
110
|
+
console.error(head + '\n' + fix + '\n 已阻断:修复后重发本命令(进度未推进,无重跑代价)。')
|
|
111
|
+
process.exit(2)
|
|
112
|
+
}
|
|
113
|
+
console.error(head)
|
|
114
|
+
console.error(fix)
|
|
108
115
|
}
|
|
109
116
|
|
|
110
117
|
/**
|
|
@@ -427,6 +434,15 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
427
434
|
// .sillyspec,无跨库状态分裂风险;非自指声明维持 fail-closed 原样(状态保护阻断)。
|
|
428
435
|
if (isSelfReferentialSpecRoot(cwd, decl.specRoot)) {
|
|
429
436
|
console.warn(`⚠️ 检测到陈旧的自指平台接管声明(repo-native junction 回环,原 specRoot 指回本地 .sillyspec),已降级并按本地模式运行;可 sillyspec platform disconnect 清理残留声明: ${join(cwd, PLATFORM_MANAGED_FILENAME)}`)
|
|
437
|
+
} else if (isTempResidueSpecRoot(cwd, decl.specRoot)) {
|
|
438
|
+
// temp 残留降级(2026-09-08 temp 投毒治理,与入口一 resolvePlatformSpecDir 同源
|
|
439
|
+
// 判定):声明 specRoot 在系统 temp 且 cwd 不在 temp → 可证伪的测试/联调残留。
|
|
440
|
+
// 与自指降级不同点:直接自动清理声明(自指场景 specRoot 指回本地 .sillyspec 有
|
|
441
|
+
// 真实数据面,留待 disconnect 是保守正确;temp 场景声明描述的 specRoot 物理上
|
|
442
|
+
// 就是临时产物,清理无损失,且不清理则每次命令重复 warn)。不用 disconnect——
|
|
443
|
+
// 它会连带清 local.yaml platform 段,误伤真实平台连接。删除失败仍按本地继续。
|
|
444
|
+
console.warn(`⚠️ 检测到 temp 残留平台接管声明(specRoot 指向系统 temp 目录:${decl.specRoot},测试/联调投毒特征),已自动清理并按本地模式运行: ${join(cwd, PLATFORM_MANAGED_FILENAME)}`)
|
|
445
|
+
try { unlinkSync(join(cwd, PLATFORM_MANAGED_FILENAME)) } catch { /* best effort */ }
|
|
430
446
|
} else {
|
|
431
447
|
console.error(`❌ 平台接管声明生效:本项目已由平台托管(原 specRoot: ${decl.specRoot || '(未记录)'}),但恢复指针缺失,拒绝静默回退本地模式。`)
|
|
432
448
|
console.error(` 声明文件: ${join(cwd, PLATFORM_MANAGED_FILENAME)}`)
|
|
@@ -537,7 +553,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
537
553
|
let outputText = null
|
|
538
554
|
const outputValue = getFlagValue('--output')
|
|
539
555
|
if (outputValue !== null) outputText = outputValue
|
|
540
|
-
warnMsysMangledFlag('--output', outputValue)
|
|
556
|
+
warnMsysMangledFlag('--output', outputValue, { block: true })
|
|
541
557
|
|
|
542
558
|
// 解析 quick 末步四字段参数(2026-08-21 agent-手工产出审计第二批 F6):--req/--cause/
|
|
543
559
|
// --solution/--result 各传一项,CLI 合成单行四字段 outputText(quicklog 落盘侧
|
|
@@ -580,7 +596,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
580
596
|
break
|
|
581
597
|
}
|
|
582
598
|
}
|
|
583
|
-
warnMsysMangledFlag('--input', inputText)
|
|
599
|
+
warnMsysMangledFlag('--input', inputText, { block: true })
|
|
584
600
|
|
|
585
601
|
// 解析 --linked-changes <a,b|none>(quick 专用:显式声明关联变更,CI/脚本友好)
|
|
586
602
|
// 与 --change 解耦:--linked-changes 语义清晰(关联变更),不与「指定变更名」混淆。
|
|
@@ -1087,6 +1103,22 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
1087
1103
|
// brainstorm 自动创建变更也写 title(--input 需求描述优先 / 自动名兜底),proposal 落盘后刷新。
|
|
1088
1104
|
progress = pm.initChange(cwd, autoName, { title: inputText ? sanitizeDesc(inputText) : autoName })
|
|
1089
1105
|
changeName = autoName
|
|
1106
|
+
} else if (stageName === 'auto') {
|
|
1107
|
+
// auto 零活跃建变更(2026-09-08-auto-driver D-003@v1,Grill P1-②):SKILL 启动命令即
|
|
1108
|
+
// run auto --input——此前此处 exit 2 是断头路。复用 brainstorm 命名(date-new-change-hex)。
|
|
1109
|
+
// pm.read 对零/多活跃都返 null——先分流:多活跃报错列候选(不隐式选择),零活跃才建。
|
|
1110
|
+
const activeCount = pm.listChanges(cwd)
|
|
1111
|
+
if (activeCount.length > 1) {
|
|
1112
|
+
console.error(`❌ 已存在 ${activeCount.length} 个活跃变更,run auto 无法自动定位目标。`)
|
|
1113
|
+
console.error(' 活跃变更:' + activeCount.join('、'))
|
|
1114
|
+
console.error(' 请用 --change <变更名> 显式指定,或先归档/收尾现有变更。')
|
|
1115
|
+
process.exit(2)
|
|
1116
|
+
}
|
|
1117
|
+
const date = new Date().toISOString().slice(0, 10)
|
|
1118
|
+
const autoName = `${date}-new-change-${randomBytes(4).toString('hex')}`
|
|
1119
|
+
console.log(`🔄 auto 模式自动创建变更:${autoName}(可用 --change 指定名称,或事后 change-rename)`)
|
|
1120
|
+
progress = pm.initChange(cwd, autoName, { title: inputText ? sanitizeDesc(inputText) : autoName })
|
|
1121
|
+
changeName = autoName
|
|
1090
1122
|
} else {
|
|
1091
1123
|
console.error('❌ 未找到进度数据,请先运行 sillyspec init 或指定 --change <变更名>')
|
|
1092
1124
|
// 自愈引导(坑 suggestion-command-missing-change 同族):多活跃变更仓不带 --change 时
|
|
@@ -1234,7 +1266,12 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
1234
1266
|
if (!qlId) {
|
|
1235
1267
|
const guardPath = join(specBase, '.runtime', 'quick-sessions', cancelSessionRaw, 'guard.json')
|
|
1236
1268
|
try {
|
|
1237
|
-
if (existsSync(guardPath))
|
|
1269
|
+
if (existsSync(guardPath)) {
|
|
1270
|
+
// 字段名 bug 修复(2026-09-09 §7-5,会话实证:guard 在场仍报「缺失/损坏」):
|
|
1271
|
+
// guard 写侧字段是 quicklogId(run/stage.js quickGuard),旧读侧读 .qlId 恒 undefined
|
|
1272
|
+
const g = JSON.parse(readFileSync(guardPath, 'utf8'))
|
|
1273
|
+
qlId = g.quicklogId || g.qlId
|
|
1274
|
+
}
|
|
1238
1275
|
} catch { /* guard 损坏 → 下面按找不到报错 */ }
|
|
1239
1276
|
}
|
|
1240
1277
|
if (!qlId) {
|
|
@@ -1546,8 +1583,15 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
|
|
|
1546
1583
|
const inputText = autoFlagValue('--input')
|
|
1547
1584
|
const skipApproval = flags.includes('--skip-approval')
|
|
1548
1585
|
const explicitMode = autoFlagValue('--mode')
|
|
1586
|
+
// --wait-interactive(2026-09-08-auto-driver D-004@v1,FR-03):TTY 下 requiresUser 步 readline 直收
|
|
1587
|
+
const waitInteractive = autoFlagValue('--wait-interactive') === 'true'
|
|
1588
|
+
globalThis.__ssWaitInteractive = waitInteractive // maybeWaitInteractive 消费(同进程直通)
|
|
1549
1589
|
const specBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
|
|
1550
1590
|
|
|
1591
|
+
// 单活跃自动选中为既有行为(入口 pm.read(cwd,null))——此处仅回显(D-003@v1;
|
|
1592
|
+
// 零活跃建变更已上移入口守卫层,多活跃 exit 2 由入口守卫处理)。
|
|
1593
|
+
if (changeName) console.log(`🎯 auto 目标变更(单活跃自动选中或显式指定):${changeName}`)
|
|
1594
|
+
|
|
1551
1595
|
// Helper: 在 auto 模式下获取步骤定义
|
|
1552
1596
|
const getAutoSteps = async (stage) => {
|
|
1553
1597
|
if (stage === 'brainstorm') {
|
|
@@ -1568,6 +1612,10 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
|
|
|
1568
1612
|
if (stage === 'brainstorm') {
|
|
1569
1613
|
const existingSteps = progress.stages?.brainstorm?.steps
|
|
1570
1614
|
const isAutoModeSteps = existingSteps?.length === 4 && existingSteps?.[0]?.name === '进度确认与上下文加载'
|
|
1615
|
+
// auto 表在位即早退(2026-09-08 E2E 实证 bug:此前仅 !isAutoModeSteps 重种后 return,
|
|
1616
|
+
// auto 表存在时掉进下方通用 ensureStageSteps——主模式 8 步定义判 4 步 auto 表「漂移」
|
|
1617
|
+
// 重种主模式表,与 getAutoSteps 渲染双轨互踩,进度永远回 step1)
|
|
1618
|
+
if (isAutoModeSteps) return progress
|
|
1571
1619
|
if (!isAutoModeSteps) {
|
|
1572
1620
|
if (!progress.stages) progress.stages = {}
|
|
1573
1621
|
progress.stages.brainstorm = {
|
|
@@ -1613,6 +1661,8 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
|
|
|
1613
1661
|
currentStage = firstOpenStage()
|
|
1614
1662
|
}
|
|
1615
1663
|
if (!currentStage) {
|
|
1664
|
+
// 重进入且已全完成:复用收尾总结 helper(FR-04)
|
|
1665
|
+
if (changeName) printAutoCompletionSummary(pm, cwd, changeName, flowStages, specBase)
|
|
1616
1666
|
console.log('All auto flow stages are complete.')
|
|
1617
1667
|
return
|
|
1618
1668
|
}
|
|
@@ -1656,7 +1706,11 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
|
|
|
1656
1706
|
}
|
|
1657
1707
|
const next = nextInFlow(currentStage)
|
|
1658
1708
|
if (next) console.log(`${currentStage} is complete. Run: sillyspec run auto --done --output "${currentStage} complete"`)
|
|
1659
|
-
else
|
|
1709
|
+
else {
|
|
1710
|
+
console.log('All auto flow stages are complete.')
|
|
1711
|
+
// 展示路径全完成(阶段已关但用户重进)——复用收尾总结(FR-04)
|
|
1712
|
+
printAutoCompletionSummary(pm, cwd, changeName, flowStages, specBase)
|
|
1713
|
+
}
|
|
1660
1714
|
return
|
|
1661
1715
|
}
|
|
1662
1716
|
// execute 阶段启动前检查审批
|
|
@@ -1677,7 +1731,9 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
|
|
|
1677
1731
|
}
|
|
1678
1732
|
}
|
|
1679
1733
|
}
|
|
1680
|
-
await outputStep(currentStage, pendingIdx, defSteps, cwd, changeName, progress.project || null, platformOpts, null, collectStageWaitHistory(progress, currentStage))
|
|
1734
|
+
await outputStep(currentStage, pendingIdx, defSteps, cwd, changeName, progress.project || null, platformOpts, null, collectStageWaitHistory(progress, currentStage), { changeName })
|
|
1735
|
+
// wait 直通(FR-03):requiresUser 且 TTY 且旗标开 → readline 直收,等价 --continue --answer
|
|
1736
|
+
await maybeWaitInteractive(defSteps[pendingIdx], changeName, currentStage)
|
|
1681
1737
|
return
|
|
1682
1738
|
}
|
|
1683
1739
|
|
|
@@ -1717,13 +1773,15 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
|
|
|
1717
1773
|
}
|
|
1718
1774
|
}
|
|
1719
1775
|
}
|
|
1720
|
-
await outputStep(currentStage, nextPendingIdx, defSteps, cwd, changeName, progress.project || null, platformOpts, null, collectStageWaitHistory(progress, currentStage))
|
|
1776
|
+
await outputStep(currentStage, nextPendingIdx, defSteps, cwd, changeName, progress.project || null, platformOpts, null, collectStageWaitHistory(progress, currentStage), { changeName })
|
|
1721
1777
|
return
|
|
1722
1778
|
}
|
|
1723
1779
|
|
|
1724
1780
|
const next = nextInFlow(currentStage)
|
|
1725
1781
|
if (!next) {
|
|
1726
1782
|
console.log('\nAll auto flow stages are complete.')
|
|
1783
|
+
// 自然收尾点(--done 后 next==null,FR-04):打印收尾总结
|
|
1784
|
+
printAutoCompletionSummary(pm, cwd, changeName, flowStages, specBase)
|
|
1727
1785
|
return
|
|
1728
1786
|
}
|
|
1729
1787
|
|
|
@@ -1795,7 +1853,95 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
|
|
|
1795
1853
|
}
|
|
1796
1854
|
}
|
|
1797
1855
|
}
|
|
1798
|
-
await outputStep(next, firstPending, nextSteps, cwd, changeName, progress.project || null, platformOpts, null, collectStageWaitHistory(progress, next))
|
|
1856
|
+
await outputStep(next, firstPending, nextSteps, cwd, changeName, progress.project || null, platformOpts, null, collectStageWaitHistory(progress, next), { changeName })
|
|
1799
1857
|
}
|
|
1800
1858
|
}
|
|
1801
1859
|
|
|
1860
|
+
|
|
1861
|
+
/**
|
|
1862
|
+
* maybeWaitInteractive —— auto 模式 wait 用户输入直通(change: 2026-09-08-auto-driver,D-004@v1,FR-03)。
|
|
1863
|
+
*
|
|
1864
|
+
* 时序:outputStep 渲染返回后调用。条件全满足(旗标开 + requiresUser 四源 + stdin TTY)→
|
|
1865
|
+
* readline 直收一行,等价自动 `--continue --answer <输入>`(复用 completeStep wait→continue 状态机,
|
|
1866
|
+
* 无新状态);任一不满足/异常 → 静默或提示回三段式(fail-open,Grill P2-①时序)。
|
|
1867
|
+
* 仅 runAutoMode 的首渲染点消费(--done 推进路径不重收)。
|
|
1868
|
+
*/
|
|
1869
|
+
export async function maybeWaitInteractive(stepDef, changeName, stageName, { rlFactory = null } = {}) {
|
|
1870
|
+
try {
|
|
1871
|
+
if (!globalThis.__ssWaitInteractive) return
|
|
1872
|
+
if (!stepDef) return
|
|
1873
|
+
const { requiresUser } = await import('./prompt.js')
|
|
1874
|
+
// prompt 正文在此不可得——四源三键优先(WAIT_MARKER_RE 源由 SS-META 渲染时已并入,直通用三键近似:
|
|
1875
|
+
// 真需 wait 的步必有标记,纯正文标记步三段式照旧)
|
|
1876
|
+
const needsUser = stepDef.requiresWait === true || stepDef.conditionalWait === true || stepDef.requiresConfirm === true
|
|
1877
|
+
if (!needsUser) return
|
|
1878
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
1879
|
+
console.log('ℹ️ --wait-interactive 需要 TTY(当前非交互环境)——按 prompt 的 wait 指令走三段式')
|
|
1880
|
+
return
|
|
1881
|
+
}
|
|
1882
|
+
// rlFactory 为测试缝(注入伪 readline);生产路径用 node:readline/promises 真实 TTY
|
|
1883
|
+
const rl = rlFactory
|
|
1884
|
+
? rlFactory()
|
|
1885
|
+
: (await import('node:readline/promises')).createInterface({ input: process.stdin, output: process.stdout })
|
|
1886
|
+
console.log('')
|
|
1887
|
+
const answer = await rl.question('⌨️ 本步需要你的输入(直通模式,回答后将自动 --continue --answer):')
|
|
1888
|
+
rl.close()
|
|
1889
|
+
if (answer && answer.trim()) {
|
|
1890
|
+
console.log(`📩 已接收,自动续行:sillyspec run auto --continue --answer "${answer.trim().slice(0, 60)}${answer.trim().length > 60 ? '…' : ''}" ...`)
|
|
1891
|
+
// 复用既有 continue 路径:与 runCommand 的 --continue 同源(子进程自调避免递归锁)
|
|
1892
|
+
const { execFileSync } = await import('node:child_process')
|
|
1893
|
+
const binSelf = new URL('../bin/sillyspec.js', import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, '$1')
|
|
1894
|
+
try {
|
|
1895
|
+
execFileSync(process.execPath, [binSelf, 'run', 'auto', '--continue', '--answer', answer.trim(), '--change', changeName], { stdio: 'inherit', cwd: process.cwd() })
|
|
1896
|
+
} catch { /* continue 失败(非 waiting 态等)静默——回三段式由 agent 走 */ }
|
|
1897
|
+
}
|
|
1898
|
+
} catch (e) {
|
|
1899
|
+
console.log(`ℹ️ wait 直通异常回退三段式:${e && e.message ? e.message : e}`)
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
|
|
1904
|
+
/**
|
|
1905
|
+
* printAutoCompletionSummary —— auto 流程收尾总结(change: 2026-09-08-auto-driver,D-005@v1,FR-04)。
|
|
1906
|
+
* 挂点:--done 后 next==null 的自然收尾(:1726 区)+ :1616 重进入分支复用。纯 console 无新文件:
|
|
1907
|
+
* 变更名/各阶段状态/tasks 勾选(readPlanCheckboxStatus)/产物存在清单/last-delta 模块清单(缺失跳过)。
|
|
1908
|
+
*/
|
|
1909
|
+
function printAutoCompletionSummary(pm, cwd, changeName, flowStages, specBase) {
|
|
1910
|
+
try {
|
|
1911
|
+
if (!changeName) return
|
|
1912
|
+
const progress = pm.read(cwd, changeName)
|
|
1913
|
+
console.log('')
|
|
1914
|
+
console.log('═'.repeat(56))
|
|
1915
|
+
console.log(`🏁 auto 流程收尾总结 — ${changeName}`)
|
|
1916
|
+
for (const s of flowStages) {
|
|
1917
|
+
const st = progress.stages?.[s]?.status || '未开始'
|
|
1918
|
+
const icon = st === 'completed' ? '✅' : st === 'in-progress' ? '🔄' : '⬜'
|
|
1919
|
+
console.log(` ${icon} ${s}: ${st}`)
|
|
1920
|
+
}
|
|
1921
|
+
// tasks 勾选
|
|
1922
|
+
try {
|
|
1923
|
+
const changeDirCk = join((specBase || join(cwd, '.sillyspec')), 'changes', changeName)
|
|
1924
|
+
const plan = pm.readPlanCheckboxStatus(changeDirCk)
|
|
1925
|
+
if (plan && plan.total > 0) console.log(` 📋 任务勾选:${plan.checked}/${plan.total}`)
|
|
1926
|
+
} catch { /* 勾选读取 fail-soft */ }
|
|
1927
|
+
// 产物存在清单
|
|
1928
|
+
const changeDirArt = join((specBase || join(cwd, '.sillyspec')), 'changes', changeName)
|
|
1929
|
+
const artifacts = ['proposal.md', 'design.md', 'tasks.md', 'plan.md', 'verify-result.md', 'delta.md']
|
|
1930
|
+
const found = artifacts.filter(a => existsSync(join(changeDirArt, a)))
|
|
1931
|
+
if (found.length > 0) console.log(` 📁 产物:${found.join('、')}`)
|
|
1932
|
+
// last-delta 模块清单(sidecar 缺失跳过)
|
|
1933
|
+
try {
|
|
1934
|
+
const runtimeRoot = join((specBase || join(cwd, '.sillyspec')), '.runtime')
|
|
1935
|
+
const sidecarPath = join(runtimeRoot, 'last-delta.json')
|
|
1936
|
+
if (existsSync(sidecarPath)) {
|
|
1937
|
+
const sidecar = JSON.parse(readFileSync(sidecarPath, 'utf8'))
|
|
1938
|
+
if (Array.isArray(sidecar.affectedModules) && sidecar.affectedModules.length > 0) {
|
|
1939
|
+
console.log(` 🔁 涉及模块(最近 delta):${sidecar.affectedModules.join('、')}`)
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
} catch { /* sidecar 读取 fail-soft */ }
|
|
1943
|
+
console.log('═'.repeat(56))
|
|
1944
|
+
} catch (e) {
|
|
1945
|
+
console.log(`ℹ️ 收尾总结渲染降级:${e && e.message ? e.message : e}`)
|
|
1946
|
+
}
|
|
1947
|
+
}
|