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/run/prompt.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* - loadModuleContextIndex/buildModuleContextInjection 内 require('fs'/'path') 改顶部静态 import
|
|
20
20
|
*/
|
|
21
21
|
import { basename, join } from 'node:path'
|
|
22
|
-
import { existsSync, readFileSync, mkdirSync } from 'node:fs'
|
|
22
|
+
import { existsSync, readFileSync, mkdirSync, readdirSync } from 'node:fs'
|
|
23
23
|
import { writeAtomicSync } from '../fs-atomic.js'
|
|
24
24
|
import { stageRegistry } from '../stages/index.js'
|
|
25
25
|
import { resolvePromptIncludes, resolveRuntimeRoot, safeGit, WAIT_MARKER_RE, QUICK_SID_RE } from './shared.js'
|
|
@@ -124,6 +124,63 @@ function buildModuleContextInjection(taskDescription, moduleIndex, specBase, pro
|
|
|
124
124
|
return injection
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
/**
|
|
128
|
+
* 读 quick session guard 的任务描述(刀①):启动 --input 由 stage.js 落 guard.taskDescription,
|
|
129
|
+
* 渲染 step1 模块上下文注入时作匹配源(changeName 是 quick-<hash> 无语义)。
|
|
130
|
+
* 与 <quicklog-id>/<linked-changes> 占位符同源同容错(session guard 优先,legacy 单文件回退)。
|
|
131
|
+
*/
|
|
132
|
+
export function readQuickGuardField(changeName, specBase, field) {
|
|
133
|
+
try {
|
|
134
|
+
const sessionGuardFile = join(specBase, '.runtime', 'quick-sessions', changeName, 'guard.json')
|
|
135
|
+
const legacyGuardFile = join(specBase, '.runtime', 'quick-guard.json')
|
|
136
|
+
const guard = existsSync(sessionGuardFile)
|
|
137
|
+
? JSON.parse(readFileSync(sessionGuardFile, 'utf8'))
|
|
138
|
+
: (existsSync(legacyGuardFile) ? JSON.parse(readFileSync(legacyGuardFile, 'utf8')) : null)
|
|
139
|
+
return guard ? (guard[field] || '') : ''
|
|
140
|
+
} catch {
|
|
141
|
+
return ''
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* quick step1 项目上下文摘要(刀①):CLI 代读 projects/*.yaml 与 CONVENTIONS.md 开头注入
|
|
147
|
+
* prompt——替代 agent 两轮 cat(quick 是最高频路径,省 token + 省轮次)。fail-soft:
|
|
148
|
+
* 读不到落单行说明,绝不阻断 prompt 输出。
|
|
149
|
+
*/
|
|
150
|
+
export function buildQuickContextDigest(specBase, projectName) {
|
|
151
|
+
try {
|
|
152
|
+
const sections = []
|
|
153
|
+
// 项目登记摘要(name/path/status 一行一个——技术栈等扩展字段按行透传 top-level 短行)
|
|
154
|
+
const projectsDir = join(specBase, 'projects')
|
|
155
|
+
if (existsSync(projectsDir)) {
|
|
156
|
+
const lines = []
|
|
157
|
+
for (const f of readdirSync(projectsDir).filter(f => f.endsWith('.yaml'))) {
|
|
158
|
+
const text = readFileSync(join(projectsDir, f), 'utf8')
|
|
159
|
+
const name = (text.match(/^name:\s*(.+)$/m) || [])[1]
|
|
160
|
+
const path = (text.match(/^path:\s*(.+)$/m) || [])[1]
|
|
161
|
+
const status = (text.match(/^status:\s*(.+)$/m) || [])[1]
|
|
162
|
+
lines.push(`- ${name ? name.trim() : f.replace(/\.yaml$/, '')}(${path ? path.trim() : '?'})${status ? ` — ${status.trim()}` : ''}`)
|
|
163
|
+
}
|
|
164
|
+
if (lines.length > 0) sections.push('### 项目登记(projects/*.yaml 摘要)\n' + lines.join('\n'))
|
|
165
|
+
}
|
|
166
|
+
// CONVENTIONS.md 开头(截断保底——全文可能很长,注入是省读取税不是塞全文)
|
|
167
|
+
const convPath = join(specBase, 'docs', projectName, 'scan', 'CONVENTIONS.md')
|
|
168
|
+
if (existsSync(convPath)) {
|
|
169
|
+
const text = readFileSync(convPath, 'utf8')
|
|
170
|
+
const HEAD_CHARS = 1200
|
|
171
|
+
const head = text.length > HEAD_CHARS
|
|
172
|
+
? text.slice(0, HEAD_CHARS) + `\n…(截断——全文 ${convPath},需要时再读)`
|
|
173
|
+
: text
|
|
174
|
+
sections.push('### 项目约定(CONVENTIONS.md 开头,CLI 代读)\n' + head)
|
|
175
|
+
}
|
|
176
|
+
return sections.length > 0
|
|
177
|
+
? sections.join('\n\n')
|
|
178
|
+
: '(无项目登记/约定文档——需要时按 prompt 内路径自查)'
|
|
179
|
+
} catch (e) {
|
|
180
|
+
return `(项目上下文注入失败:${e && e.message ? e.message : e}——需要时再 cat {SPEC_ROOT}/projects/*.yaml 与 CONVENTIONS.md)`
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
127
184
|
// parseModuleMapSimple 复用 modules.js 的 canonical 实现(合并历史 copy-paste 副本,2026-08-07;
|
|
128
185
|
// 无循环依赖:modules.js 仅 import fs/path/db.js,prompt.js → modules.js 单向)。
|
|
129
186
|
/**
|
|
@@ -185,7 +242,20 @@ export function collectStageWaitHistory(progress, stageName) {
|
|
|
185
242
|
/**
|
|
186
243
|
* 输出当前步骤的 prompt
|
|
187
244
|
*/
|
|
188
|
-
|
|
245
|
+
/**
|
|
246
|
+
* requiresUser —— auto driver 元数据(change: 2026-09-08-auto-driver,D-002@v1)的四源判定纯函数:
|
|
247
|
+
* step.requiresWait / conditionalWait / requiresConfirm 三键(step 定义权威信号)+ WAIT_MARKER_RE 正文扫描
|
|
248
|
+
* (execute/verify 的 step 定义无三键,纯三源会漏真 wait 步)。全缺兜底 **false**——auto 默认语义是
|
|
249
|
+
* 继续驱动(Grill P1-①:保守 true 会让 execute/verify 逐步人工确认,推翻 FR-01)。
|
|
250
|
+
* 与 outputStep 内既有 mayNeedWait 计算同源(:1019 附近),仅多 requiresConfirm 一源。
|
|
251
|
+
*/
|
|
252
|
+
export function requiresUser(step, promptText) {
|
|
253
|
+
if (!step) return false
|
|
254
|
+
if (step.requiresWait === true || step.conditionalWait === true || step.requiresConfirm === true) return true
|
|
255
|
+
return WAIT_MARKER_RE.test(String(promptText || ''))
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjectName, platformOpts = {}, prevStepAnswer = null, waitHistory = null, autoMeta = null) {
|
|
189
259
|
const step = steps[stepIndex]
|
|
190
260
|
const total = steps.length
|
|
191
261
|
// ── 越界防御 ──
|
|
@@ -199,6 +269,16 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
199
269
|
console.error(` 请检查 --change 指定的变更目录是否存在、specRoot 是否正确,然后重试。`)
|
|
200
270
|
return false
|
|
201
271
|
}
|
|
272
|
+
|
|
273
|
+
// ── 首个 agent 可见步索引(「首步一次建立」类注入的锚点)──
|
|
274
|
+
// step0 为 noAI(brainstorm/execute/verify 的 step1「进度确认」,2026-09-07)时不渲染 prompt,
|
|
275
|
+
// persona/护栏/完成契约/铁律/平台路径约束等 step0 专属注入须顺延到首个渲染步,否则整段丢失
|
|
276
|
+
//(spec-dir.test.mjs Test4 平台 specDir 断言即此坑的行为探针)。全 noAI 防御性回退 0。
|
|
277
|
+
const firstRenderableIdx = (() => {
|
|
278
|
+
const i = steps.findIndex(s => s?.noAI !== true)
|
|
279
|
+
return i === -1 ? 0 : i
|
|
280
|
+
})()
|
|
281
|
+
|
|
202
282
|
const projectName = dbProjectName || basename(cwd)
|
|
203
283
|
|
|
204
284
|
// ── Revision context injection ──
|
|
@@ -244,8 +324,8 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
244
324
|
console.log(`changeDir: ${changeDir}`)
|
|
245
325
|
}
|
|
246
326
|
console.log(`---\n`)
|
|
247
|
-
// persona
|
|
248
|
-
if (personas[stageName] && stepIndex ===
|
|
327
|
+
// persona 只在首个 agent 可见步注入——角色设定一次即可,后续 step 重复注入纯属 token 浪费
|
|
328
|
+
if (personas[stageName] && stepIndex === firstRenderableIdx) {
|
|
249
329
|
console.log(personas[stageName])
|
|
250
330
|
console.log('')
|
|
251
331
|
}
|
|
@@ -255,7 +335,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
255
335
|
|
|
256
336
|
console.log(`## Step ${stepIndex + 1}/${total}: ${step.name}\n`)
|
|
257
337
|
if (guardrails) {
|
|
258
|
-
if (stepIndex ===
|
|
338
|
+
if (stepIndex === firstRenderableIdx) {
|
|
259
339
|
console.log(guardrails.trim())
|
|
260
340
|
console.log('')
|
|
261
341
|
} else {
|
|
@@ -398,13 +478,15 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
398
478
|
platformDirectives.push(`workspace_id: ${platformOpts.workspaceId}`)
|
|
399
479
|
}
|
|
400
480
|
// 平台 directives 前置条件:
|
|
401
|
-
// -
|
|
481
|
+
// - 首个 agent 可见步(对齐 persona 仅 step0 策略):路径列表/Write 规则/占位符映射
|
|
482
|
+
// 一次建立即可——step0 为 noAI(brainstorm/execute/verify 的 step1「进度确认」,
|
|
483
|
+
// 2026-09-07)时首个可见步顺延,按 defSteps 计算(坑同 scan quick 档 noAI preflight);
|
|
402
484
|
// - scan 阶段所有 step:scan 的 profileDirectives 本就每步注入(见下方 L917),平台路径
|
|
403
485
|
// 约束也需每步可见——否则 quick profile 的 step0 是 noAI preflight(自动执行),run scan
|
|
404
486
|
// 输出的首个可见 step 是 step1,会漏掉 specDir 路径,导致平台模式写入约束丢失。
|
|
405
487
|
// 后续 step 靠 changeDir header + footer 平台规则(L1077+)维持,platformDirectives
|
|
406
488
|
// 每步重复约 500 tokens 仅在 scan(步数有限)可接受。
|
|
407
|
-
if (stepIndex ===
|
|
489
|
+
if (stepIndex === firstRenderableIdx || stageName === 'scan') {
|
|
408
490
|
promptText = platformDirectives.join('\n') + '\n\n' + promptText
|
|
409
491
|
}
|
|
410
492
|
} else {
|
|
@@ -506,6 +588,88 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
506
588
|
promptText = promptText.replace(/\{SCAN_FACTS\}/g, factsInjected)
|
|
507
589
|
}
|
|
508
590
|
|
|
591
|
+
// ── 机械事实注入三件(2026-09-07 注入缺口批次):{LOCAL_COMMANDS} / {GIT_DIRTY} / {TASKS_CHECKBOX} ──
|
|
592
|
+
// CLI 渲染 prompt 时已能确定性拿到的机械事实,替代 agent 各自跑一趟 cat local.yaml /
|
|
593
|
+
// git status / 手数勾选(全流程 7+ 处步骤的高频机械读)。全部 fail-soft:异常注入单行
|
|
594
|
+
// 说明,不留残留占位符;替换后占位符即消失(不存在的档位自然零输出)。
|
|
595
|
+
|
|
596
|
+
// ① {LOCAL_COMMANDS}:local.yaml commands 段原文(brainstorm/plan/execute/verify/quick 多步消费)
|
|
597
|
+
if (promptText.includes('{LOCAL_COMMANDS}')) {
|
|
598
|
+
let localBlock = ''
|
|
599
|
+
try {
|
|
600
|
+
const lcSpecBase = resolvePromptSpecBase(platformOpts, cwd)
|
|
601
|
+
const localPath = join(lcSpecBase, 'local.yaml')
|
|
602
|
+
if (existsSync(localPath)) {
|
|
603
|
+
const lcLines = String(readFileSync(localPath, 'utf8')).replace(/\r\n?/g, '\n').split('\n')
|
|
604
|
+
const start = lcLines.findIndex(l => /^commands:\s*(#.*)?$/.test(l))
|
|
605
|
+
if (start !== -1) {
|
|
606
|
+
const body = []
|
|
607
|
+
for (let i = start + 1; i < lcLines.length && !/^\S/.test(lcLines[i]); i++) body.push(lcLines[i])
|
|
608
|
+
// unavailable 标记的条目按未配置语义剔除行(与 extractTestCommand 同口径)
|
|
609
|
+
const avail = body.filter(l => !/^\s*[\w.-]+:\s*['"]?unavailable['"]?\s*(#.*)?$/i.test(l))
|
|
610
|
+
const trimmed = avail.join('\n').trim()
|
|
611
|
+
localBlock = trimmed
|
|
612
|
+
? 'commands:\n' + trimmed
|
|
613
|
+
: '(local.yaml 存在但 commands 段为空——可跑 `sillyspec local detect` 补默认骨架)'
|
|
614
|
+
} else {
|
|
615
|
+
localBlock = '(local.yaml 存在但无 commands 段——可跑 `sillyspec local detect` 补默认骨架)'
|
|
616
|
+
}
|
|
617
|
+
} else {
|
|
618
|
+
localBlock = '(未配置 local.yaml——先跑 `sillyspec local detect` 生成骨架,以命令输出为准)'
|
|
619
|
+
}
|
|
620
|
+
} catch (e) {
|
|
621
|
+
localBlock = `(构建命令注入失败:${e.message}——可自行读 local.yaml)`
|
|
622
|
+
}
|
|
623
|
+
promptText = promptText.replace(/\{LOCAL_COMMANDS\}/g, localBlock)
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// ①b {QUICK_CONTEXT_DIGEST}(刀①,2026-09-08):quick step1 的项目/约定上下文 CLI 代读注入
|
|
627
|
+
// ——替代 agent cat projects/*.yaml 与 CONVENTIONS.md 两轮文件读取(quick 是最高频路径)。
|
|
628
|
+
// fail-soft 同三件套;无 quick 占位符自然零输出。
|
|
629
|
+
if (stageName === 'quick' && promptText.includes('{QUICK_CONTEXT_DIGEST}')) {
|
|
630
|
+
const digestSpecBase = resolvePromptSpecBase(platformOpts, cwd)
|
|
631
|
+
promptText = promptText.replace(/\{QUICK_CONTEXT_DIGEST\}/g, buildQuickContextDigest(digestSpecBase, projectName))
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// ② {GIT_DIRTY}:工作区脏文件清单(git status --porcelain,quick 收尾步消费)
|
|
635
|
+
if (promptText.includes('{GIT_DIRTY}')) {
|
|
636
|
+
let dirtyBlock = ''
|
|
637
|
+
try {
|
|
638
|
+
const { gitQuiet } = await import('../git-helper.js')
|
|
639
|
+
// gitQuiet 返回裸 string(失败 null)——非 safeGit 的 {value,error} 形状
|
|
640
|
+
const lines = String(gitQuiet(cwd, ['status', '--porcelain']) || '').split('\n').filter(Boolean)
|
|
641
|
+
dirtyBlock = lines.length === 0
|
|
642
|
+
? '(工作区干净,无脏文件)'
|
|
643
|
+
: lines.slice(0, 40).join('\n') + (lines.length > 40 ? `\n(…共 ${lines.length} 行,超出 40 行截断——完整清单可自行跑 git status --porcelain)` : '')
|
|
644
|
+
} catch (e) {
|
|
645
|
+
dirtyBlock = `(脏文件注入失败:${e.message}——可自行跑 git status --porcelain)`
|
|
646
|
+
}
|
|
647
|
+
promptText = promptText.replace(/\{GIT_DIRTY\}/g, dirtyBlock)
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// ③ {TASKS_CHECKBOX}:tasks.md 任务勾选状态投影(verify 逐项检查步消费——省 agent 手数)
|
|
651
|
+
if (promptText.includes('{TASKS_CHECKBOX}')) {
|
|
652
|
+
let tasksBlock = ''
|
|
653
|
+
try {
|
|
654
|
+
const tcSpecBase = resolvePromptSpecBase(platformOpts, cwd)
|
|
655
|
+
const tasksPath = changeName ? join(tcSpecBase, 'changes', changeName, 'tasks.md') : null
|
|
656
|
+
if (tasksPath && existsSync(tasksPath)) {
|
|
657
|
+
const re = /^(\s*[-*]\s+\[([ xX])\]\s+task-\d+.*)$/
|
|
658
|
+
const rows = String(readFileSync(tasksPath, 'utf8')).replace(/\r\n?/g, '\n').split('\n')
|
|
659
|
+
.map(l => l.match(re)).filter(Boolean).map(m => m[1].trim())
|
|
660
|
+
const checked = rows.filter(r => /\[x\]/i.test(r)).length
|
|
661
|
+
tasksBlock = rows.length === 0
|
|
662
|
+
? '(tasks.md 无 task-NN checkbox 行)'
|
|
663
|
+
: `已勾 ${checked}/${rows.length}:\n` + rows.slice(0, 60).join('\n') + (rows.length > 60 ? `\n(…共 ${rows.length} 条,超出 60 条截断)` : '')
|
|
664
|
+
} else {
|
|
665
|
+
tasksBlock = '(tasks.md 不存在——跳过勾选对照)'
|
|
666
|
+
}
|
|
667
|
+
} catch (e) {
|
|
668
|
+
tasksBlock = `(勾选状态注入失败:${e.message}——可自行读 tasks.md)`
|
|
669
|
+
}
|
|
670
|
+
promptText = promptText.replace(/\{TASKS_CHECKBOX\}/g, tasksBlock)
|
|
671
|
+
}
|
|
672
|
+
|
|
509
673
|
// 决策防复潮注入(W1.1 第 3 点,task-04,FR-05):brainstorm「加载项目上下文」Step2 的
|
|
510
674
|
// {DECISION_HITS} → matchKnowledge decisionHits——命中 rejected 条目时渲染「否决决策提示」段
|
|
511
675
|
// (ID/标题/否决理由/复潮条件),无命中替换为空串零输出(不留残留占位符);注入结果同步落
|
|
@@ -712,22 +876,10 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
712
876
|
}
|
|
713
877
|
}
|
|
714
878
|
|
|
715
|
-
// 三主阶段 Step1「进度确认」快照注入(2026-09-05
|
|
716
|
-
//
|
|
717
|
-
//
|
|
718
|
-
|
|
719
|
-
try {
|
|
720
|
-
const done = steps.filter(s => s.status === 'completed').length
|
|
721
|
-
const waiting = steps.filter(s => s.status === 'waiting').length
|
|
722
|
-
const snapshot =
|
|
723
|
-
`- 变更:${changeName || '(未知)'}\n` +
|
|
724
|
-
`- 阶段:${stageName}(CLI 已路由到本阶段,无需再跑 progress show 确认)\n` +
|
|
725
|
-
`- 步骤:${done}/${steps.length} 已完成${waiting > 0 ? `,${waiting} 个等待中` : ''};当前推进到第 ${stepIndex + 1} 步`
|
|
726
|
-
promptText = promptText.split('{PROGRESS_SNAPSHOT}').join(snapshot)
|
|
727
|
-
} catch (e) {
|
|
728
|
-
promptText = promptText.split('{PROGRESS_SNAPSHOT}').join('(快照注入失败:' + e.message + '——可运行 sillyspec progress show 自查)')
|
|
729
|
-
}
|
|
730
|
-
}
|
|
879
|
+
// 三主阶段 Step1「进度确认」快照注入(2026-09-05 全流程审计)已随 step1 整体 noAI 化退役
|
|
880
|
+
// (2026-09-07):brainstorm/execute/verify 的 step1 不再渲染 prompt,快照改由
|
|
881
|
+
// run/progress-confirm.js 的 progressConfirm 动作 console 直出(占位符 {PROGRESS_SNAPSHOT}
|
|
882
|
+
// 无消费者,注入分支删除)。
|
|
731
883
|
|
|
732
884
|
// execute Step「确认 worktree 路径」meta 注入(2026-09-05 全流程审计):worktree meta 由 CLI
|
|
733
885
|
// 读出直接注入,agent 省一趟 worktree meta 工具往返;工具链可用性检查(真实工作)仍归 agent。
|
|
@@ -840,13 +992,39 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
840
992
|
promptText = promptText.split('{EVIDENCE_AUTO_RECOMMENDATION}').join(eaInjected)
|
|
841
993
|
}
|
|
842
994
|
|
|
843
|
-
//
|
|
844
|
-
|
|
995
|
+
// {ARCHIVE_IMPACT_AUDIT}(2026-09-09 ql-20260909-004):archive extract-module-impact 步的
|
|
996
|
+
// 三重核对机械预填——auditModuleImpactAgainstDiff 代算(module-impact × diff × map 归属),
|
|
997
|
+
// agent 只裁决不一致项。fail-soft:审计异常降级单行指引(回退手跑 git diff)。
|
|
998
|
+
if (stageName === 'archive' && promptText.includes('{ARCHIVE_IMPACT_AUDIT}')) {
|
|
999
|
+
let auditBlock = ''
|
|
1000
|
+
try {
|
|
1001
|
+
const { auditModuleImpactAgainstDiff } = await import('../archive-delta.js')
|
|
1002
|
+
const auditSpecBase = resolvePromptSpecBase(platformOpts, cwd)
|
|
1003
|
+
const r = auditModuleImpactAgainstDiff({ cwd, changeName, specDir: auditSpecBase })
|
|
1004
|
+
auditBlock = [
|
|
1005
|
+
'【三重核对报告(CLI 机械预填)】',
|
|
1006
|
+
r.summary,
|
|
1007
|
+
...(r.mismatches.length > 0 ? ['不一致项:', ...r.mismatches.map(m => ' - ' + m), '→ 逐项裁决并修正 module-impact.md;其余(未列出的 diff 文件为 .sillyspec 产物不参与核对)无需处理。'] : ['一致 ✓——第 3 步修正可跳过。']),
|
|
1008
|
+
].join(String.fromCharCode(10))
|
|
1009
|
+
} catch (e) {
|
|
1010
|
+
auditBlock = '(三重核对机械预填失败:' + (e && e.message ? e.message : e) + '——回退手跑 git diff --name-only 比对)'
|
|
1011
|
+
}
|
|
1012
|
+
promptText = promptText.split('{ARCHIVE_IMPACT_AUDIT}').join(auditBlock)
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// 注入模块上下文(brainstorm/plan/execute 阶段全步 + quick 首步「理解任务」——刀①:quick
|
|
1016
|
+
// step1 原让 agent cat module-map 再挑模块卡读,改为按任务描述匹配后注入,基于 Module Context Index)
|
|
1017
|
+
const quickFirstStep = stageName === 'quick' && step && step.name === '理解任务'
|
|
1018
|
+
if ((['brainstorm', 'plan', 'execute'].includes(stageName) || quickFirstStep) && projectName) {
|
|
845
1019
|
const effectiveSpecBase = resolvePromptSpecBase(platformOpts, cwd)
|
|
846
1020
|
const moduleIndex = loadModuleContextIndex(effectiveSpecBase, projectName)
|
|
847
1021
|
if (moduleIndex && Object.keys(moduleIndex).length > 0) {
|
|
848
|
-
// 尝试从 step prompt / changeName
|
|
849
|
-
|
|
1022
|
+
// 尝试从 step prompt / changeName 匹配模块;quick 用 guard.taskDescription(启动 --input)——
|
|
1023
|
+
// changeName 是 quick-<hash> 无语义、step.prompt 全文噪音大(刀①)
|
|
1024
|
+
let taskDesc = step.prompt || changeName || ''
|
|
1025
|
+
if (stageName === 'quick') {
|
|
1026
|
+
taskDesc = readQuickGuardField(changeName, effectiveSpecBase, 'taskDescription') || taskDesc
|
|
1027
|
+
}
|
|
850
1028
|
const injection = buildModuleContextInjection(taskDesc, moduleIndex, effectiveSpecBase, projectName)
|
|
851
1029
|
if (injection) {
|
|
852
1030
|
promptText = injection + '\n' + promptText
|
|
@@ -888,9 +1066,9 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
888
1066
|
}
|
|
889
1067
|
|
|
890
1068
|
// 完成契约(事前预知):该 stage 的机械校验通过条件,从 stage-contract-spec.js manifest 渲染,
|
|
891
|
-
// 与 CLI 完成校验严格同源(事前给的 == 事后查的)
|
|
892
|
-
//
|
|
893
|
-
if (stepIndex ===
|
|
1069
|
+
// 与 CLI 完成校验严格同源(事前给的 == 事后查的)。仅首个 agent 可见步注入(与 persona/guardrails
|
|
1070
|
+
// 同模式,一次建立即可;后续步靠 context 保留)。
|
|
1071
|
+
if (stepIndex === firstRenderableIdx) {
|
|
894
1072
|
const stageContract = renderStageContract(stageName)
|
|
895
1073
|
if (stageContract) {
|
|
896
1074
|
promptText = `${promptText}\n\n${stageContract}`
|
|
@@ -898,10 +1076,10 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
898
1076
|
}
|
|
899
1077
|
|
|
900
1078
|
console.log(promptText)
|
|
901
|
-
// 铁律拆分(W3 token
|
|
1079
|
+
// 铁律拆分(W3 token 效率):通用流程纪律(文档优先/不跳步/不编造命令)只在首个 agent 可见步注入——
|
|
902
1080
|
// 每步重复 ~800B 纯耗 context。但【平台写入规则 + 路径规则】是安全关键(防写错目录/绕过 Write),
|
|
903
1081
|
// 且依赖 changeName/platformOpts,必须【每步注入】(context 压缩丢失会让 agent 越界写源码)。
|
|
904
|
-
if (stepIndex ===
|
|
1082
|
+
if (stepIndex === firstRenderableIdx) {
|
|
905
1083
|
console.log(`\n### ⚠️ 铁律`)
|
|
906
1084
|
console.log('- 文档优先:代码产出必须先有对应的设计/规范文档支撑。')
|
|
907
1085
|
console.log('- 聚焦本步骤:只执行本步骤描述的操作并完整做完;自行扩展或跳步会破坏状态机推进,后续步骤再做后续事。')
|
|
@@ -949,21 +1127,42 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
949
1127
|
// archive-batch-31-tool-notes ②:通用 --done 模板不带该 flag,agent 照抄执行撞
|
|
950
1128
|
// 「请添加 --confirm」确认门,误以为参数没带。
|
|
951
1129
|
const confirmFlag = step.requiresConfirm === true ? ' --confirm' : ''
|
|
1130
|
+
// auto driver(2026-09-08-auto-driver D-002/P1-③):auto 模式下命令一律 run auto 形态——
|
|
1131
|
+
// 与 SS-META 的 doneCommand 同源(同一 cmdStage 变量),消灭「正文 run <stage> / 元数据 run auto」双命令
|
|
1132
|
+
const cmdStage = autoMeta ? 'auto' : stageName
|
|
1133
|
+
const doneCommand = `sillyspec run ${cmdStage} --done${confirmFlag}${changeFlag} --output "你的摘要"`
|
|
952
1134
|
if (requiresWait) {
|
|
953
1135
|
console.log(`本步骤必须等待用户输入,不能直接 --done:`)
|
|
954
|
-
console.log(`sillyspec run ${
|
|
1136
|
+
console.log(`sillyspec run ${cmdStage} --wait --reason "${step.waitReason || '等待用户输入'}" --options "${(step.waitOptions || ['确认']).join(',')}"${changeFlag} --output "你的问题/方案摘要"`)
|
|
955
1137
|
console.log(``)
|
|
956
1138
|
console.log(`用户回答后执行:`)
|
|
957
|
-
console.log(`sillyspec run ${
|
|
1139
|
+
console.log(`sillyspec run ${cmdStage} --continue --answer "用户回答"${changeFlag}`)
|
|
958
1140
|
console.log(``)
|
|
959
1141
|
console.log(`收到回答并完成本步骤总结后,再执行:`)
|
|
960
1142
|
} else if (mayNeedWait) {
|
|
961
1143
|
console.log(`如果需要用户决策(选择方案/确认设计等):`)
|
|
962
|
-
console.log(`sillyspec run ${
|
|
1144
|
+
console.log(`sillyspec run ${cmdStage} --wait --reason "${step.waitReason || '等待原因'}" --options "${(step.waitOptions || ['选项1', '选项2']).join(',')}"${changeFlag} --output "你的摘要"`)
|
|
963
1145
|
console.log(``)
|
|
964
1146
|
console.log(`如果不需要用户决策,正常完成:`)
|
|
965
1147
|
}
|
|
966
|
-
console.log(
|
|
1148
|
+
console.log(doneCommand + (autoMeta ? '' : ' --input "用户原始需求/反馈"'))
|
|
1149
|
+
|
|
1150
|
+
// ── SS-META 机器可读元数据块(change: 2026-09-08-auto-driver,D-002@v1,FR-01)──
|
|
1151
|
+
// 仅 auto 模式(autoMeta 非空)渲染:单行 HTML 注释内 JSON,agent 低噪可读、脚本一行正则
|
|
1152
|
+
// 可提取(<!--SS-META:(.*)-->)。requiresUser 四源见上方纯函数;doneCommand 与正文同源。
|
|
1153
|
+
if (autoMeta) {
|
|
1154
|
+
const meta = {
|
|
1155
|
+
stage: stageName,
|
|
1156
|
+
stepIndex: stepIndex + 1,
|
|
1157
|
+
stepName: step.name,
|
|
1158
|
+
requiresUser: requiresWait || mayNeedWait || step.requiresConfirm === true,
|
|
1159
|
+
doneCommand,
|
|
1160
|
+
waitHint: (requiresWait || mayNeedWait)
|
|
1161
|
+
? 'requiresUser=true:先与用户交互(wait/continue 或 --wait-interactive 直通),再 done'
|
|
1162
|
+
: '直接执行任务后逐字跑 doneCommand',
|
|
1163
|
+
}
|
|
1164
|
+
console.log('<!--SS-META:' + JSON.stringify(meta) + '-->')
|
|
1165
|
+
}
|
|
967
1166
|
}
|
|
968
1167
|
/**
|
|
969
1168
|
* 替换 prompt 文本中的路径根占位符 {SPEC_ROOT}/{DOCS_ROOT}/{PROJECTS_ROOT}/
|
package/src/run/quick-audit.js
CHANGED
|
@@ -209,12 +209,15 @@ export async function resolveQuickLinkedChanges({ pm, cwd, specDir, quickFiles,
|
|
|
209
209
|
* npm test + npm run lint」从 agent 自律下沉为 CLI 卡点。
|
|
210
210
|
*
|
|
211
211
|
* 语义(对齐规则 8 + verify-postcheck 既有降级哲学):
|
|
212
|
-
* -
|
|
212
|
+
* - 变更文件未触及 src/ 或 test/(纯 doc/配置改动)→ skip(不跑不阻断)
|
|
213
213
|
* - 触及 → 亲自执行 local.yaml 的 commands.test / commands.lint(复用 verify 阶段
|
|
214
214
|
* 对账引擎 runVerifyTestCheck/runVerifyLintCheck,含 test_strategy/known_failures/
|
|
215
215
|
* 超时语义);未配置命令 → 内部 skipped,不阻断(兼容无测试项目)
|
|
216
216
|
* - 任一实测 failed → fail(调用方阻断 --done,step 回 pending)
|
|
217
217
|
* - 逃生门:SILLYSPEC_QUICK_TEST_GATE=skip 显式跳过(CI/特殊场景,审计留痕)
|
|
218
|
+
* - 倒推 B 模式兜底(2026-09-07,quick-f9138c2f 实证):代码先行会话的文件全部早于
|
|
219
|
+
* 启动时间窗 → 审计 changedFiles 为空 → 此前静默 skip;--files 声明边界
|
|
220
|
+
* (declaredFiles = guard.allowedFiles)明确触及 src/test 时实测仍应跑。
|
|
218
221
|
*
|
|
219
222
|
* verify-postcheck 顶层 import 链较重(contract-matrix 等),且本函数仅 quick --done
|
|
220
223
|
* 收尾路径调用——用动态 import,不进 run 命令通用启动路径(与上方 @inquirer/prompts 同款纪律)。
|
|
@@ -222,18 +225,22 @@ export async function resolveQuickLinkedChanges({ pm, cwd, specDir, quickFiles,
|
|
|
222
225
|
* @param {object} opts
|
|
223
226
|
* @param {string} opts.cwd - 仓库根(命令执行 cwd)
|
|
224
227
|
* @param {string} opts.specBase - .sillyspec 目录(local.yaml 读取源,信任边界见 runVerifyTestCheck SEC-02 注释)
|
|
225
|
-
* @param {string[]} [opts.changedFiles] - 审计口径的本轮变更文件(仓库根相对 POSIX 路径;空/null →
|
|
228
|
+
* @param {string[]} [opts.changedFiles] - 审计口径的本轮变更文件(仓库根相对 POSIX 路径;空/null → 回退 declaredFiles)
|
|
229
|
+
* @param {string[]} [opts.declaredFiles] - 会话声明边界(guard.allowedFiles;changedFiles 为空时的兜底判定源)
|
|
226
230
|
* @param {string} [opts.changeName] - quick 会话名(evidence-auto 策略解析用)
|
|
227
231
|
* @returns {Promise<{action:'pass'|'fail'|'skip', failed:string[], reason:string, test:object|null, lint:object|null}>}
|
|
228
232
|
*/
|
|
229
|
-
export async function runQuickTestLintGate({ cwd, specBase, changedFiles = [], changeName = null }) {
|
|
233
|
+
export async function runQuickTestLintGate({ cwd, specBase, changedFiles = [], declaredFiles = [], changeName = null }) {
|
|
230
234
|
if (process.env.SILLYSPEC_QUICK_TEST_GATE === 'skip') {
|
|
231
235
|
return { action: 'skip', failed: [], reason: 'SILLYSPEC_QUICK_TEST_GATE=skip 显式跳过(审计留痕)', test: null, lint: null }
|
|
232
236
|
}
|
|
233
|
-
const
|
|
237
|
+
const audited = Array.isArray(changedFiles) ? changedFiles : []
|
|
238
|
+
// 倒推 B 模式兜底:审计口径为空时回退声明边界(文件早于会话启动被基线吸收的场景)
|
|
239
|
+
const files = audited.length > 0 ? audited : (Array.isArray(declaredFiles) ? declaredFiles : [])
|
|
240
|
+
const fileSource = audited.length > 0 ? '审计' : (files.length > 0 ? '声明边界兜底(倒推 B:文件早于会话启动被基线吸收)' : '无')
|
|
234
241
|
const codeFiles = files.filter(f => typeof f === 'string' && (f.startsWith('src/') || f.startsWith('test/')))
|
|
235
242
|
if (files.length === 0) {
|
|
236
|
-
return { action: 'skip', failed: [], reason:
|
|
243
|
+
return { action: 'skip', failed: [], reason: `无变更文件清单(${fileSource}口径均空——brownfield 无 guard 或空审计),跳过`, test: null, lint: null }
|
|
237
244
|
}
|
|
238
245
|
if (codeFiles.length === 0) {
|
|
239
246
|
return { action: 'skip', failed: [], reason: `纯 doc/配置改动(${files.length} 个文件均未触及 src/test,规则 8 语义跳过 test+lint)`, test: null, lint: null }
|
package/src/run/scan-profile.js
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* - executeScanPostcheck 原冗余动态 import('fs'/'path'/'child_process') 删除,改顶部静态(execSync 实际未直接用,纯遗留)
|
|
10
10
|
*/
|
|
11
11
|
import { join, basename, extname } from 'node:path'
|
|
12
|
-
import { existsSync, readdirSync, statSync, mkdirSync, writeFileSync, unlinkSync } from 'node:fs'
|
|
13
|
-
import { safeGit } from './shared.js'
|
|
12
|
+
import { existsSync, readdirSync, statSync, mkdirSync, writeFileSync, unlinkSync, readFileSync } from 'node:fs'
|
|
13
|
+
import { safeGit, resolveRuntimeRoot } from './shared.js'
|
|
14
14
|
import { SCAN_REQUIRED_DOCS } from '../constants.js'
|
|
15
15
|
|
|
16
16
|
/**
|
|
@@ -327,6 +327,24 @@ export async function executeScanResumeCheck(cwd, platformOpts) {
|
|
|
327
327
|
console.log(` 📂 ${p}: ${states.join(' ')} ${facts}`)
|
|
328
328
|
}
|
|
329
329
|
console.log('')
|
|
330
|
+
|
|
331
|
+
// IR 增量回灌 advisory(change: 2026-09-07-ir-hardening,D-006@v1,FR-03):读
|
|
332
|
+
// .runtime/last-delta.json(归档/delta 补跑写入的最近变更快照),14 天窗口内提示本轮
|
|
333
|
+
// scan 优先核对其涉及模块——advisory 不阻断不改变步骤结构;缺失/过期/解析失败静默。
|
|
334
|
+
try {
|
|
335
|
+
const sidecarPath = join(resolveRuntimeRoot(platformOpts, specBase), 'last-delta.json')
|
|
336
|
+
if (existsSync(sidecarPath)) {
|
|
337
|
+
const sidecar = JSON.parse(readFileSync(sidecarPath, 'utf8'))
|
|
338
|
+
const ageMs = Date.now() - Date.parse(sidecar.updatedAt || '')
|
|
339
|
+
const FOURTEEN_DAYS = 14 * 24 * 60 * 60 * 1000
|
|
340
|
+
if (Number.isFinite(ageMs) && ageMs >= 0 && ageMs <= FOURTEEN_DAYS
|
|
341
|
+
&& Array.isArray(sidecar.affectedModules) && sidecar.affectedModules.length > 0) {
|
|
342
|
+
console.log(`🔁 增量回灌提示(上次归档变更 ${sidecar.change},${new Date(sidecar.updatedAt).toISOString().slice(0, 10)}):`)
|
|
343
|
+
console.log(` 其涉及模块 ${sidecar.affectedModules.join('、')} 的文档与 staleRefs 本轮 scan 优先核对(advisory,不据此跳过其他模块)`)
|
|
344
|
+
console.log('')
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
} catch { /* sidecar 读取/解析失败静默跳过(advisory 缺席零影响) */ }
|
|
330
348
|
}
|
|
331
349
|
|
|
332
350
|
/**
|
package/src/run/shared.js
CHANGED
|
@@ -107,19 +107,24 @@ export function resolvePromptIncludes(text) {
|
|
|
107
107
|
export function resolveSpecDir(cwd, opts = {}) {
|
|
108
108
|
if (opts.specDir) return resolve(opts.specDir)
|
|
109
109
|
const home = os.homedir()
|
|
110
|
-
|
|
110
|
+
const origin = resolve(cwd)
|
|
111
|
+
const originBelowHome = origin.startsWith(home + sep) // 严格子目录,不含 home 自身
|
|
112
|
+
let dir = origin
|
|
113
|
+
let passedHome = false
|
|
111
114
|
while (true) {
|
|
112
|
-
// home
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
+
// home 拒绝守卫:起点在 home 子树内时,home 层不检查 .sillyspec,
|
|
116
|
+
// 防止 home 子目录遍历经过 home 误命中 ~/.sillyspec;
|
|
117
|
+
// passedHome:经过 home 后不再检查(覆盖遍历到真实 home 的场景)。
|
|
118
|
+
if (!(passedHome || (dir === home && originBelowHome))) {
|
|
115
119
|
const candidate = join(dir, '.sillyspec')
|
|
116
120
|
if (existsSync(candidate)) return candidate
|
|
117
121
|
}
|
|
122
|
+
if (dir === home) passedHome = true
|
|
118
123
|
const parent = dirname(dir)
|
|
119
124
|
if (parent === dir) break
|
|
120
125
|
dir = parent
|
|
121
126
|
}
|
|
122
|
-
return join(
|
|
127
|
+
return join(origin, '.sillyspec')
|
|
123
128
|
}
|
|
124
129
|
|
|
125
130
|
/**
|
|
@@ -333,6 +338,8 @@ export function resolveQuickSessionsDir(platformOpts, localSpecBase) {
|
|
|
333
338
|
// GHOST_EMPTY_DIR_STALE_MS 同口径(7 天)——正常并发会话间隔在分钟/小时级,7 天未收尾的
|
|
334
339
|
// 会话按僵尸处理,防崩溃残留的 guard 永久「认领」文件侵蚀危险门。
|
|
335
340
|
const FOREIGN_SESSION_STALE_MS = 7 * 24 * 60 * 60 * 1000
|
|
341
|
+
// 空壳会话点名宽限期:零步骤完成但启动未满此时长的不列(进行中会话的正常起步态)
|
|
342
|
+
const EMPTY_SHELL_GRACE_MS = 10 * 60 * 1000
|
|
336
343
|
|
|
337
344
|
/**
|
|
338
345
|
* 枚举其他 active quick 会话的显式文件声明(坑 foreign-session-declared-false-block)。
|
|
@@ -419,6 +426,9 @@ export function detectEmptyShellQuickSessions(platformOpts, localSpecBase, curre
|
|
|
419
426
|
if (!guard || !guard.startedAt) continue
|
|
420
427
|
const startedAtMs = Date.parse(guard.startedAt)
|
|
421
428
|
if (!Number.isFinite(startedAtMs) || nowMs - startedAtMs > FOREIGN_SESSION_STALE_MS) continue
|
|
429
|
+
// 宽限期(2026-09-09 §7-4,会话实证:2 分钟前刚启动的进行中会话被点名疑似空壳):
|
|
430
|
+
// 零步骤完成对刚起步的会话是正常态,不满 10 分钟不列(宁可漏报不误报的同款取向)。
|
|
431
|
+
if (nowMs - startedAtMs < EMPTY_SHELL_GRACE_MS) continue
|
|
422
432
|
try {
|
|
423
433
|
const p = pm.read(cwd, sessionName)
|
|
424
434
|
if (!p || !p.stages) continue
|
|
@@ -467,6 +477,16 @@ export function writePlatformPointer(cwd, platformOpts, extra = {}) {
|
|
|
467
477
|
console.warn(`⚠️ 检测到自指平台指针(repo-native junction 回环,specRoot 指回本地 .sillyspec),已跳过指针写入并按本地模式运行: ${platformOpts.specRoot}`)
|
|
468
478
|
return false
|
|
469
479
|
}
|
|
480
|
+
// temp 残留写侧守卫(2026-09-08 temp 投毒治理,防患于未然):specRoot 在系统 temp 且
|
|
481
|
+
// 项目 cwd 不在 temp → 拒绝三写。真实平台 spec 根永不在 temp;此类组合只可能来自
|
|
482
|
+
// 测试/联调以临时 specRoot 在真实项目根跑平台模式命令——指针/声明一旦落盘,指针被
|
|
483
|
+
// 清理后残留声明会让全命令 fail-closed(读侧 isTempResidueSpecRoot 降级兜底已存在
|
|
484
|
+
// 的污染,此处从源头断新增)。cwd 同在 temp(套件隔离正常形态)不拦,平台模式
|
|
485
|
+
// 测试不受影响。
|
|
486
|
+
if (isTempResidueSpecRoot(cwd, platformOpts.specRoot)) {
|
|
487
|
+
console.warn(`⚠️ 拒绝写入平台指针:specRoot 指向系统 temp 目录(${platformOpts.specRoot})而项目不在 temp 下——测试/联调残留特征,已跳过三写并按本地模式运行`)
|
|
488
|
+
return false
|
|
489
|
+
}
|
|
470
490
|
// HUB-05:合并保留既有生命周期字段。scan 完成时指针被写入 status/completedAt/scanStatus
|
|
471
491
|
//(complete-handlers),但下一次任何平台模式 run(含只读 --status)都会重写指针——
|
|
472
492
|
// 恢复链只回填 specRoot 等四字段,不保留 status → 指针永远回 active、isPointerStale
|
|
@@ -564,6 +584,38 @@ export function isSelfReferentialSpecRoot(cwd, specRoot) {
|
|
|
564
584
|
}
|
|
565
585
|
}
|
|
566
586
|
|
|
587
|
+
/**
|
|
588
|
+
* temp 残留 specRoot 判定(temp 投毒治理,2026-09-08:multi-agent-platform 工作台
|
|
589
|
+
* 「总览不可用」排障实证——Git Bash mktemp 临时 specRoot 的平台模式命令在真实项目根
|
|
590
|
+
* 写下指针+声明,指针随后被 cleanup/STALE 清理,声明按设计存活(唯一删除路径 =
|
|
591
|
+
* platform disconnect)→ 全命令 fail-closed,且 disconnect 会连带清 local.yaml
|
|
592
|
+
* platform 段误伤真实平台连接)。
|
|
593
|
+
*
|
|
594
|
+
* 判定:specRoot 位于系统 temp 目录 **且** cwd 不在 temp 下 → true。两个条件缺一不可:
|
|
595
|
+
* 真实平台的 spec 根永不在 temp(daemon 用 ~/.sillyhub/daemon/specs/{ws},服务器用
|
|
596
|
+
* /data/spec-workspaces);测试/联调的 temp specRoot 只允许配 temp 项目 cwd(套件隔离
|
|
597
|
+
* 正常形态,见 test/run-tests.mjs childEnv 与 platform-managed-declaration 夹具——
|
|
598
|
+
* cwd 与 spec 同在 temp 下时本函数返回 false,既有 fail-closed 测试语义不变)。
|
|
599
|
+
*
|
|
600
|
+
* 与 isSelfReferentialSpecRoot 并列为声明的两类「可证伪残留」:自指回环(repo-native
|
|
601
|
+
* junction 投毒)已有降级先例(run/command.js:429),本函数补 temp 残留这一类。
|
|
602
|
+
* 口径对齐 progress.js 指针路径的 temp 警告(resolve() 前缀比较,不 realpath——temp
|
|
603
|
+
* 判定是残留启发式,无需穿透 junction 的强一致)。
|
|
604
|
+
*
|
|
605
|
+
* @param {string} cwd - 项目根(与 isSelfReferentialSpecRoot 同基准)
|
|
606
|
+
* @param {string|null} [specRoot] - 平台 specRoot(可 null/undefined/空串)
|
|
607
|
+
* @returns {boolean} 是否 temp 残留(写侧应拒绝三写,读侧应降级本地并清理声明)
|
|
608
|
+
*/
|
|
609
|
+
export function isTempResidueSpecRoot(cwd, specRoot) {
|
|
610
|
+
if (!specRoot || typeof specRoot !== 'string') return false
|
|
611
|
+
const tmp = resolve(os.tmpdir())
|
|
612
|
+
const sr = resolve(specRoot)
|
|
613
|
+
if (sr !== tmp && !sr.startsWith(tmp + sep)) return false
|
|
614
|
+
const c = resolve(cwd)
|
|
615
|
+
if (c === tmp || c.startsWith(tmp + sep)) return false
|
|
616
|
+
return true
|
|
617
|
+
}
|
|
618
|
+
|
|
567
619
|
/**
|
|
568
620
|
* 平台模式判定单一数据源(自指免疫,变更 2026-08-23-repo-native-spec-backfill)。
|
|
569
621
|
*
|
|
@@ -769,7 +821,8 @@ export async function triggerPull(cwd, changeName, platformOpts = {}, opts = {})
|
|
|
769
821
|
if (!sm._getPlatform()) return
|
|
770
822
|
if (_autoPullRecently(cwd)) return
|
|
771
823
|
_stampAutoPull(cwd)
|
|
772
|
-
|
|
824
|
+
// autoPull:噪音闸标识(sync.js pull 的失败 warn 走跨进程静默窗口,见 sync-noise.js)
|
|
825
|
+
await raceWithAbort((sig) => sm.pull(changeName, { skipIfLocalDirty: true, signal: sig.signal, autoPull: true }), opts.timeoutMs ?? AUTO_PULL_TIMEOUT_MS)
|
|
773
826
|
} catch (e) {
|
|
774
827
|
// pull 失败静默跳过(Best Effort,失败不影响正确性)
|
|
775
828
|
console.warn('⚠️ 拉取失败:', e.message)
|
|
@@ -808,9 +861,10 @@ export async function triggerPullActiveChange(cwd, platformOpts = {}) {
|
|
|
808
861
|
}
|
|
809
862
|
if (!cn) return
|
|
810
863
|
// 已确认连接 + 单活跃变更,调 pull(自动注入 2s 熔断 + abort(HUB-09);skipIfLocalDirty 保守守卫
|
|
811
|
-
// 同 triggerPull——本地脏时跳过 import,防平台旧快照覆盖本地领先进度,ql-20260818-008
|
|
864
|
+
// 同 triggerPull——本地脏时跳过 import,防平台旧快照覆盖本地领先进度,ql-20260818-008)。
|
|
865
|
+
// autoPull:噪音闸标识(sync.js pull 的失败 warn 走跨进程静默窗口,见 sync-noise.js)
|
|
812
866
|
try {
|
|
813
|
-
await raceWithAbort((sig) => sm.pull(cn, { skipIfLocalDirty: true, signal: sig.signal }), AUTO_PULL_TIMEOUT_MS)
|
|
867
|
+
await raceWithAbort((sig) => sm.pull(cn, { skipIfLocalDirty: true, signal: sig.signal, autoPull: true }), AUTO_PULL_TIMEOUT_MS)
|
|
814
868
|
} catch (e) {
|
|
815
869
|
console.warn('⚠️ 拉取失败:', e.message)
|
|
816
870
|
}
|
|
@@ -879,8 +933,9 @@ export function parsePorcelainPath(line) {
|
|
|
879
933
|
}
|
|
880
934
|
|
|
881
935
|
/**
|
|
882
|
-
* quick 自身写入的 .sillyspec/ 元数据判定(auditQuickCompletion
|
|
883
|
-
*
|
|
936
|
+
* quick 自身写入的 .sillyspec/ 元数据判定(auditQuickCompletion 审计豁免的单一真相源)。
|
|
937
|
+
* 注意:QUICKLOG「文件:」行回填**不再直接复用**本谓词——记录面与审计豁免面已分叉
|
|
938
|
+
* (模块卡 + changelog sidecar 审计照旧放行、但要进文件行),见 isQuicklogFileLineNoise。
|
|
884
939
|
*
|
|
885
940
|
* 归类口径:
|
|
886
941
|
* - quicklog/.runtime/modules/_module-map/knowledge-uncategorized 等 quick 自身产物 → 元数据
|
|
@@ -889,7 +944,7 @@ export function parsePorcelainPath(line) {
|
|
|
889
944
|
*
|
|
890
945
|
* 「并发工作 vs 偷建变更」的意图软判定留给 sillyhub,确定性校验只做路径归类。
|
|
891
946
|
*
|
|
892
|
-
* @param {string} p 文件路径(容错
|
|
947
|
+
* @param {string} p 文件路径(容错 \ / 混用)
|
|
893
948
|
* @param {string[]} [linkedChanges] 关联变更名列表(影响 changes/ 归类)
|
|
894
949
|
* @returns {boolean} true=quick 元数据,应从业务文件列表过滤掉
|
|
895
950
|
*/
|
|
@@ -910,6 +965,24 @@ export function isQuickMetadata(p, linkedChanges = []) {
|
|
|
910
965
|
return false
|
|
911
966
|
}
|
|
912
967
|
|
|
968
|
+
/**
|
|
969
|
+
* QUICKLOG「文件:」行回填过滤——isQuickMetadata 的**记录面**变体(2026-09-08 用户反馈①:
|
|
970
|
+
* 审计按 .sillyspec/ 放行规则整体豁免没问题,但模块卡 + changelog sidecar 恰是收尾必改项,
|
|
971
|
+
* 被同一谓词从文件行滤掉后每回都得手工补录)。审计豁免面(isQuickMetadata)与记录面在此
|
|
972
|
+
* 分叉:quick 自身元数据(quicklog/.runtime/知识库未归类)照旧不入文件行,但本 quick 实际
|
|
973
|
+
* 改动的模块卡(`modules/<id>.md`)与 sidecar(`modules/<id>.changelog.md`,同被上方
|
|
974
|
+
* `[^/]+\.md$` 覆盖)保留——它们是真实收尾工作的一部分。`_module-map.yaml` 仍滤:CLI
|
|
975
|
+
* (module-resolve)维护的索引,非 agent 手工改动。
|
|
976
|
+
* @param {string} p 文件路径(容错 \ / 混用)
|
|
977
|
+
* @param {string[]} [linkedChanges] 关联变更名列表(透传 isQuickMetadata)
|
|
978
|
+
* @returns {boolean} true=元数据噪音,应从「文件:」行过滤掉
|
|
979
|
+
*/
|
|
980
|
+
export function isQuicklogFileLineNoise(p, linkedChanges = []) {
|
|
981
|
+
const file = String(p).replace(/\\/g, '/')
|
|
982
|
+
if (/^\.sillyspec\/docs\/[^/]+\/modules\/[^/]+\.md$/.test(file)) return false
|
|
983
|
+
return isQuickMetadata(p, linkedChanges)
|
|
984
|
+
}
|
|
985
|
+
|
|
913
986
|
// 危险文件清单(auditQuickCompletion 危险门与 predictProtectedQuickFiles 预告共用单一真相源)。
|
|
914
987
|
// SillySpec 核心流程代码。W6 重构后 src/run.js(23 行 barrel)/ src/progress.js(facade)
|
|
915
988
|
// 把真正逻辑下沉到 src/run/、src/progress/ 子目录——旧的精确文件名匹配
|