sillyspec 3.32.3 → 3.32.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.
@@ -51,4 +51,4 @@ sillyspec run auto --input "<用户需求>" [--mode <模式>]
51
51
  ### 边界
52
52
 
53
53
  - 中断恢复:直接重跑 `sillyspec run auto`(进度已落盘,从断点续)。
54
- - 分类为 quick 的小变更:CLI 会提示改跑 `sillyspec run quick`,照提示执行。
54
+ - 分类为小变更时 CLI 会提示改走轻量变更 `sillyspec flow start`,照提示执行。
@@ -67,12 +67,12 @@ sillyspec run brainstorm --done --answer "..." --output "..." # 一步完成
67
67
  ```
68
68
  ┌─ scale=large → plan(四件套齐)
69
69
  scan → brainstorm ┤
70
- └─ scale=small → quick --linked-changes(仅 design.md)
70
+ └─ scale=small → flow start 收编(仅 design.md)
71
71
  ```
72
72
 
73
73
  brainstorm 完成时按 design.md frontmatter 的 `scale` 分叉:
74
74
  - **large**(多文件/跨模块/有状态机或 schema 变更):四件套齐 + Design Grill 审查通过(tier=independent 时由独立审查子代理产出 stage review.json)→ `sillyspec run plan --change <变更名>`
75
- - **small**(≤2 文件、单模块、无跨模块依赖):仅生成 design.md → `sillyspec run quick --linked-changes <变更名>`
75
+ - **small**(≤2 文件、单模块、无跨模块依赖):仅生成 design.md → `sillyspec flow start --change <变更名>` 收编续跑(design 以头脑风暴版为准)
76
76
 
77
77
  > 规模由 AI 在 brainstorm 最后一步评估并写入 design.md frontmatter。判错可手动改 `scale` 后再跑相应阶段。
78
78
 
@@ -108,6 +108,7 @@ brainstorm 在 `tier=independent` 规模下除 design.md 等四件套外,还
108
108
  - **必须用 exec 工具(shell)执行 CLI,不要自己编造流程**
109
109
  - 只做当前步骤 prompt 描述的操作,不跳过、不自行扩展
110
110
  - 产物写入 CLI 输出的 `changeDir` 目录(如 `<changeDir>/design.md`),不要自己拼路径
111
+ - **变更标题(面板显示用):总结一句中文概括,≤50 字、建议 ~20 字**——CLI 从 proposal.md / design.md 首行 H1 提取简述写入 changes.title(剥「提案书(Proposal)— / 设计文档(Design)—」固定前缀后取段),design.md 固定格式 `# 设计文档(Design)— <简述>` 中的简述即面板标题,按此口径写
111
112
  - 完成后立即 `--done`,不跳过
112
113
 
113
114
  ## 用户指令
@@ -79,8 +79,6 @@ sillyspec run explore --status # 查看阶段状态
79
79
 
80
80
  **暴露风险**:识别可能出错的地方、发现理解空白
81
81
 
82
- ## OpenSpec 上下文感知
83
-
84
82
  ### 检查已有上下文
85
83
 
86
84
  ```bash
package/README.md CHANGED
@@ -85,7 +85,7 @@ SillySpec 进度库使用 SQLite 持久化(基于 Node.js 内置的 `node:sqli
85
85
 
86
86
  **也可以跳过完整流程:**
87
87
 
88
- - 小改动:`/sillyspec:quick "修复 xxx"`
88
+ - 小改动:`/sillyspec:flow "修复 xxx"`
89
89
  - 不确定做什么:`/sillyspec:explore`
90
90
  - 中断恢复:`/sillyspec:resume`
91
91
  - 不知道下一步:`/sillyspec:continue`
@@ -112,7 +112,7 @@ SillySpec 进度库使用 SQLite 持久化(基于 Node.js 内置的 `node:sqli
112
112
  | `/sillyspec:status` · `/sillyspec:state` | 查看进度 / 当前工作状态 |
113
113
  | `/sillyspec:continue` | 自动判断并执行下一步 |
114
114
  | `/sillyspec:explore` | 自由思考:画图、讨论、调研、HTML 原型 |
115
- | `/sillyspec:quick` | 快速模式:跳过完整流程 |
115
+ | `/sillyspec:flow` | 轻量变更(默认快道):需求明确的小改动两步收口 |
116
116
  | `/sillyspec:resume` | 恢复工作(支持大模块阶段进度) |
117
117
  | `/sillyspec:doctor` | 项目自检与状态修复 |
118
118
  | `/sillyspec:commit` | 智能提交 |
@@ -122,7 +122,7 @@ SillySpec 进度库使用 SQLite 持久化(基于 Node.js 内置的 `node:sqli
122
122
  ## CLI 命令
123
123
 
124
124
  ```bash
125
- sillyspec run <stage> 执行阶段(auto/brainstorm/plan/execute/verify/archive/scan/quick/explore)
125
+ sillyspec run <stage> 执行阶段(auto/brainstorm/plan/execute/verify/archive/scan/explore)
126
126
  sillyspec run <stage> --done 完成当前步骤并推进到下一步
127
127
  sillyspec run <stage> --status 查看阶段进度
128
128
  sillyspec progress show 显示当前项目状态
@@ -140,7 +140,7 @@ sillyspec change-delete <变更名> 删除变更(DB status=deleted 与归
140
140
  **第一层:护城河(外部状态,模型能力不可替代)**
141
141
 
142
142
  - **多 agent 并发控制** — 会话/变更/worktree 三级隔离;文件锁 ID 分配 + 双占用 fail-closed 硬拦;`--files` 边界声明切分"这是谁改的";`--done` 前并发写预检告警他者未提交改动;平台同步乐观锁(base_ts + 身份 + 服务器权威钟)+ 冲突血统归属(自回声不误报)
143
- - **实测验收门禁** — verify / quick 收尾时 CLI **亲自执行** test / lint 与 agent 自报对账,不符即阻断回滚;docHash 重算比对 + git 提交证据交叉对账防伪造;门禁跑在隔离快照,不受并行会话脏文件污染
143
+ - **实测验收门禁** — verify 收尾时 CLI **亲自执行** test / lint 与 agent 自报对账,不符即阻断回滚;docHash 重算比对 + git 提交证据交叉对账防伪造;门禁跑在隔离快照,不受并行会话脏文件污染
144
144
  - **持久账本 + 断点恢复** — SQLite(node:sqlite)单一进度源,进度/决策/审批答复全落库,跨会话跨天续跑;quicklog 四字段结构化台账可检索
145
145
  - **跨仓变更管理** — task 卡声明 `repo:` 字段即可管理跨仓任务:同构 worktree 隔离、base 锚快照、apply 永不覆盖用户在途改动
146
146
 
package/SKILL.md CHANGED
@@ -51,7 +51,6 @@ Claude Code / Cursor / Codex / OpenCode / OpenClaw / Gemini 通用。
51
51
  | `/sillyspec:continue` | 自动判断并执行下一步 |
52
52
  | `/sillyspec:explore` | 自由思考模式 |
53
53
  | `/sillyspec:flow` | 轻量变更(默认快道):flow start → 干活 → flow done |
54
- | `/sillyspec:quick` | (已退役,重定向到 flow)仅存量在途会话收尾 |
55
54
  | `/sillyspec:resume` | 恢复工作 |
56
55
  | `/sillyspec:doctor` | 项目自检 |
57
56
  | `/sillyspec:commit` | 智能提交 |
@@ -70,7 +69,6 @@ sillyspec run execute 执行开发阶段(子代理并行 + worktree
70
69
  sillyspec run verify 执行验证阶段
71
70
  sillyspec run archive 执行归档阶段
72
71
  sillyspec flow start/done/status 轻量变更(默认快道,2 次调用收口)
73
- sillyspec run quick (已退役,仅存量在途会话 --done/--cancel 收尾)
74
72
  sillyspec run explore 自由探索
75
73
  sillyspec progress show 显示当前项目状态
76
74
  sillyspec setup 安装推荐 MCP 工具
@@ -81,12 +79,12 @@ sillyspec init 初始化(零交互,自动检测工具)
81
79
 
82
80
  **护城河层(外部状态,模型能力不可替代)**
83
81
  - **多 agent 并发控制** — 会话/变更/worktree 三级隔离;文件锁 ql-ID 分配 + 双占用 fail-closed 硬拦;边界声明归属切分(窗口∩声明);并发写预检告警;平台乐观锁(base_ts + 身份头 + 服务器权威钟)
84
- - **实测验收门禁** — verify/quick 收尾时 CLI 亲自执行 test/lint 与自报对账,不符即阻断回滚;docHash 重算 + git 证据交叉对账防伪造;门禁跑在隔离快照里不受并行会话脏文件污染
82
+ - **实测验收门禁** — verify 收尾时 CLI 亲自执行 test/lint 与自报对账,不符即阻断回滚;docHash 重算 + git 证据交叉对账防伪造;门禁跑在隔离快照里不受并行会话脏文件污染
85
83
  - **持久账本 + 断点恢复** — SQLite 单一进度源,进度/决策/审批答复全落库,跨会话跨天续跑;quicklog 四字段结构化台账
86
84
  - **跨仓变更管理** — task 卡声明 `repo:` 字段,跨仓同构 worktree 隔离,base 锚快照,apply 不覆盖用户在途改动
87
85
 
88
86
  **确定性对账层(机械核验,零模型轮次)**
89
- - **范围对账** — 计划改动 × 实际改动三态对账(planned/unplanned/untouched),execute/verify/archive/quick 四处收尾注入
87
+ - **范围对账** — 计划改动 × 实际改动三态对账(planned/unplanned/untouched),execute/verify/archive 三处收尾注入
90
88
  - **API 契约矩阵** — provider/consumer 识别、endpoint 工件提取、前后端 API parity 对账
91
89
  - **文档一致性棘轮** — 源码引用真实性校验(HEAD 模式),失败数只降不升
92
90
  - **noAI 下沉 / IR 事实层** — 机械轮次下沉 CLI(骨架预生成、注入、代算对账),判断层留给 agent;同一契约清单事前渲染预览、事后引擎核验
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sillyspec",
3
- "version": "3.32.3",
3
+ "version": "3.32.4",
4
4
  "description": "SillySpec CLI — 流程状态机,让 AI 严格按步骤来",
5
5
  "icon": "logo.jpg",
6
6
  "homepage": "https://sillyspec.ppdmq.top/",
@@ -10,7 +10,7 @@ import { writeAtomicSync } from './fs-atomic.js'
10
10
 
11
11
  export const COMMAND_CARD_TARGETS = { zcode: '.zcode/commands/sillyspec', claude: '.claude/commands/sillyspec' }
12
12
  // flow=轻量变更默认道入口卡(2026-09-25-quick-channel-retire 配套:quick 退役后快道首次有卡)
13
- export const COMMAND_CARD_NAMES = ['flow', 'run-brainstorm', 'run-plan', 'run-execute', 'run-verify', 'run-archive', 'run-quick', 'status']
13
+ export const COMMAND_CARD_NAMES = ['flow', 'run-brainstorm', 'run-plan', 'run-execute', 'run-verify', 'run-archive', 'status']
14
14
 
15
15
  const ANCHOR_TAIL_RE = /\n?<!-- sillyspec-card: v(\S+) sha256=([0-9a-f]{64}) -->\s*$/
16
16
  const sha256 = (s) => createHash('sha256').update(s, 'utf8').digest('hex')
@@ -221,7 +221,7 @@ export function filterCommittedFace(committedRaw, ownPrefix, opts = {}) {
221
221
 
222
222
  /**
223
223
  * patch 冻结面漂移检测(2026-10-05-disposition-refreeze-drift,主清单 P1):处置重入审计
224
- * 错位——评审发现处置涉及代码修改后重跑 flow done,patch 子步幂等跳过让 change.patch/
224
+ * 时点错位——评审发现处置涉及代码修改后重跑 flow done,patch 子步幂等跳过让 change.patch/
225
225
  * review.json 停在处置前时点(2026-10-05-review-promise-negation 先例 905397ef;
226
226
  * 2026-10-05-flowdone-lintfail-output 收编活体复现:评审 P1 处置提交后不带 --refreeze 重跑,
227
227
  * 归档件缺处置面)。
@@ -230,23 +230,41 @@ export function filterCommittedFace(committedRaw, ownPrefix, opts = {}) {
230
230
  * 漂移——按提交 message 变更名归属(parseChangeNamesFromSubject,与 filterCommittedFace
231
231
  * 同口径的既成事实切分,非声明抢文件)。仅他侧后缀/裸提交不触发(他侧面归属切分另有防线,
232
232
  * 裸提交保守不动作)。检测自身失败按无漂移(退现状幂等跳过行为,fail-safe 不新造阻断面)。
233
+ *
234
+ * 文件面三字段(2026-10-10-drift-review-governance-keep,坑 drift-review-governance-loop):
235
+ * ownCommits 只答「有没有漂移」,不答「评审结论该不该作废」——调用方(flow.js 漂移分支)
236
+ * 据文件面区分治理面等价(评审保留)与交付/承诺面(隔离重评):
237
+ * ownFiles 窗口内本变更提交触及的文件集(--name-only 采集,正斜杠归一;merge 提交
238
+ * 无 -m 展开不计文件——漏采只可能让 governanceOnly 偏 false,回落隔离
239
+ * 重评,fail-safe 宁可多评)
240
+ * governanceOnly own 提交全在 .sillyspec/** 下且文件集非空(治理文件等价漂移)
241
+ * touchedPromiseFace 含本变更 requirements.md / design.md(承诺面也是评审对象,破豁免)
233
242
  * @param {{ cwd: string, change: string, freezeHead: string|null }} opts
234
- * @returns {{ drifted: boolean, ownCommits: string[], head: string|null }} ownCommits=本变更后缀提交的 subject 列表(截 80 字)
243
+ * @returns {{ drifted: boolean, ownCommits: string[], head: string|null, ownFiles: string[], governanceOnly: boolean, touchedPromiseFace: boolean }} ownCommits=本变更后缀提交的 subject 列表(截 80 字)
235
244
  */
236
245
  export function detectPatchDrift({ cwd, change, freezeHead }) {
237
246
  try {
238
- if (!freezeHead) return { drifted: false, ownCommits: [], head: null }
247
+ if (!freezeHead) return { drifted: false, ownCommits: [], head: null, ownFiles: [], governanceOnly: false, touchedPromiseFace: false }
239
248
  const head = execFileSync('git', ['rev-parse', 'HEAD'], { cwd, encoding: 'utf8', timeout: 30000, windowsHide: true }).trim()
240
- if (!head || head === freezeHead) return { drifted: false, ownCommits: [], head }
241
- const log = execFileSync('git', ['log', '--format=%x00%s', `${freezeHead}..HEAD`], { cwd, encoding: 'utf8', timeout: 30000, windowsHide: true })
249
+ if (!head || head === freezeHead) return { drifted: false, ownCommits: [], head, ownFiles: [], governanceOnly: false, touchedPromiseFace: false }
250
+ const log = execFileSync('git', ['log', '--name-only', '--format=%x00%s', `${freezeHead}..HEAD`], { cwd, encoding: 'utf8', timeout: 30000, windowsHide: true })
242
251
  const own = []
252
+ const ownFileSet = new Set()
243
253
  for (const block of String(log).split('\x00')) {
244
- const subject = block.split('\n').map((l) => l.trim()).filter(Boolean)[0]
254
+ const lines = block.split('\n').map((l) => l.trim()).filter(Boolean)
255
+ const subject = lines[0]
245
256
  if (!subject) continue
246
- if (parseChangeNamesFromSubject(subject).includes(change)) own.push(subject.slice(0, 80))
257
+ if (parseChangeNamesFromSubject(subject).includes(change)) {
258
+ own.push(subject.slice(0, 80))
259
+ for (const f of lines.slice(1)) ownFileSet.add(f.replace(/\\/g, '/'))
260
+ }
247
261
  }
248
- return { drifted: own.length > 0, ownCommits: own, head }
249
- } catch { return { drifted: false, ownCommits: [], head: null } }
262
+ const ownFiles = [...ownFileSet]
263
+ const promiseFace = [`.sillyspec/changes/${change}/requirements.md`, `.sillyspec/changes/${change}/design.md`]
264
+ const governanceOnly = own.length > 0 && ownFiles.length > 0 && ownFiles.every((f) => f.startsWith('.sillyspec/'))
265
+ const touchedPromiseFace = ownFiles.some((f) => promiseFace.includes(f))
266
+ return { drifted: own.length > 0, ownCommits: own, head, ownFiles, governanceOnly, touchedPromiseFace }
267
+ } catch { return { drifted: false, ownCommits: [], head: null, ownFiles: [], governanceOnly: false, touchedPromiseFace: false } }
250
268
  }
251
269
 
252
270
  /**
@@ -16,7 +16,7 @@
16
16
  * 豁免 ≠ 免责:全信号不命中而豁免的变更按 1/4 定额抽查采样(确定性哈希分桶),采样命中照评
17
17
  * 并进遥测——误豁免率可测量可校准(先例:review-dispatch shadow 影子期 N=10 转正判据)。
18
18
  */
19
- import { existsSync, readFileSync } from 'node:fs'
19
+ import { existsSync, readFileSync, readdirSync } from 'node:fs'
20
20
  import { join } from 'node:path'
21
21
  import { DESIGN_QUESTIONS as DESIGN_QUESTIONS_FALLBACK } from './flow-draft.js'
22
22
 
@@ -203,7 +203,17 @@ export function renderReviewerTaskbook({ change, changeDir, head = null }) {
203
203
  // (首评任务书与现状逐字一致)。
204
204
  let priorFindingsMd = ''
205
205
  try {
206
- const prev = JSON.parse(readFileSync(join(changeDir, 'review.json'), 'utf8'))
206
+ // 前轮产物读取(2026-10-10-drift-review-governance-keep,坑 drift-review-governance-loop ②):
207
+ // review.json 在场优先;漂移隔离后已改名 review.json.superseded-<ts>——缺席时回退字典序最新
208
+ // 隔离件(时间戳后缀同格式可排序),否则处置重评读不到前轮 findings,「已报项只验修复+
209
+ // 回归」复审语义丢失,退化为全量首评整轮重报(2026-10-10 用户实证)。changeDir 不存在/
210
+ // 无隔离件 → readFileSync/readdirSync 抛错走外层 catch,首评形态零注入不变。
211
+ let prevPath = join(changeDir, 'review.json')
212
+ if (!existsSync(prevPath)) {
213
+ const supers = readdirSync(changeDir).filter((f) => /^review\.json\.superseded-/.test(f)).sort()
214
+ if (supers.length > 0) prevPath = join(changeDir, supers[supers.length - 1])
215
+ }
216
+ const prev = JSON.parse(readFileSync(prevPath, 'utf8'))
207
217
  if (Array.isArray(prev.findings) && prev.findings.length > 0) {
208
218
  priorFindingsMd = [
209
219
  ``,
package/src/flow.js CHANGED
@@ -1272,6 +1272,14 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1272
1272
  } catch { /* 锚定判定失败按未锚定处理 */ }
1273
1273
  if (anchoredToHead) {
1274
1274
  console.log(` review.json 已锚定当前 HEAD(reviewedAgainst 命中 ${String(drift.head || '').slice(0, 8)})——评审对象即最新交付面,结论保留不隔离`)
1275
+ } else if (drift.governanceOnly && !drift.touchedPromiseFace) {
1276
+ // 治理面等价保留(2026-10-10-drift-review-governance-keep,坑 drift-review-governance-loop
1277
+ // 用户实证):评审结论的对象是交付面——窗口内本变更提交只碰 .sillyspec/** 治理文件且
1278
+ // 未触本变更 requirements/design(承诺面也是评审对象)时,结论对最新交付面逐字节等价,
1279
+ // 保留不隔离不重置标记;change.patch 仍走下方 driftRefreeze 自动重冻结吸收治理面增量
1280
+ // (审计件覆盖最新提交面不变量不破)。此前任何后缀提交一律隔离重评——评审后修一个
1281
+ // P2 文档措辞也付整轮子代理评审费(循环,被迫手工重排执行顺序自赎)。
1282
+ console.log(` review.json 保留不隔离——窗口内本变更提交仅触及治理文件(${drift.ownFiles.length} 个,未触 requirements/design),评审对象(交付面)未变,结论对最新面等价;change.patch 自动重冻结吸收治理面增量`)
1275
1283
  } else {
1276
1284
  const ts = new Date().toISOString().replace(/[-:]/g, '').replace(/\..+$/, '')
1277
1285
  try {
@@ -1598,6 +1606,11 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1598
1606
  if (others.length > 0) {
1599
1607
  console.warn(`⚠️ 评审非阻断发现 ${others.length} 项(P2/P3,随归档留档):`)
1600
1608
  for (const f of others) console.warn(` [${f.severity}] ${f.title} — ${f.evidence || ''}`)
1609
+ // 处置提示(2026-10-10-drift-review-governance-keep,坑 drift-review-governance-loop):
1610
+ // P2/P3 非阻断可留债归档;要修则修复面决定是否重评——只碰治理文件不动本变更
1611
+ // requirements/design 的后缀提交触发治理面等价保留(评审保留+自动重冻结,零重评);
1612
+ // 触及交付面或承诺面才隔离重评(任务书带前轮 findings,复审只验修复+新增)。
1613
+ console.warn(` 💡 处置参考:P2/P3 非阻断,可留债归档;要修——只碰治理文件(.sillyspec/**,不动本变更 requirements/design)的提交不触发重评;触及交付面或 requirements/design 需重评(复审只验修复+新增,非全量首评)`)
1601
1614
  }
1602
1615
  console.log(`✅ 独立评审通过(reviewer=${v.review.reviewer}${tier.sampled ? ',豁免抽查采样命中' : ''})`)
1603
1616
  reviewOutcome = { required: true, sampled: tier.sampled, verdict: 'PASS', findingsP1: 0 }
package/src/index.js CHANGED
@@ -246,7 +246,7 @@ async function main() {
246
246
 
247
247
  // E22:重路径统一加载(轻路径 --version/help 已早退,未付此税)。
248
248
  const { ProgressManager, resolvePlatformSpecDir, resolvePlatformOpts } = await import('./progress.js');
249
- const { didYouMean, assertSafeChangeName, assertDatedChangeName, resolveSpecDir, detectWorktreeSpecDrift, detectCwdInsideWorktree, ancestorSpecDirs } = await import('./run/shared.js');
249
+ const { didYouMean, assertSafeChangeName, assertDatedChangeName, resolveSpecDir, detectWorktreeSpecDrift, detectCwdInsideWorktree, ancestorSpecDirs, resolveUninitCwdGate } = await import('./run/shared.js');
250
250
 
251
251
  // 解析全局选项
252
252
  let json = false;
@@ -354,7 +354,9 @@ async function main() {
354
354
  return baseDir
355
355
  }
356
356
 
357
- const dir = targetDir;
357
+ // let:下方未初始化目录硬拦命中祖先链时重锚定到 spec 根(治裸 join(dir,'.sillyspec')
358
+ // 调用点如 review status rsSpecBase 的子目录漂移;resolveEffectiveDir 系本就上溯,变为无操作)
359
+ let dir = targetDir;
358
360
 
359
361
  if (command === 'init' && !existsSync(dir)) {
360
362
  const { mkdirSync } = await import('fs');
@@ -388,6 +390,33 @@ async function main() {
388
390
  }
389
391
  }
390
392
 
393
+ // ── 未初始化目录硬拦(坑:agent 在未初始化/错误目录跑非 init 命令,fail-soft 散点报错不说
394
+ // 正确 cwd——实测 status 在未初始化目录提示可开变更、flow start 到 local.yaml 才报错、
395
+ // knowledge 只报参数错,agent 多轮试错)──
396
+ // 判定与豁免清单收敛在 run/shared.js resolveUninitCwdGate(单一真相源,测试直测);
397
+ // 平台 pointer/接管声明/显式 --spec-dir/平台 flag 由其判 skip(平台项目不受本门误拦)。
398
+ // 未初始化目录无 ground truth——文案只给锚点(git root)与三条出路,不断言唯一正确 cwd。
399
+ const uninitGate = resolveUninitCwdGate(command, {
400
+ dir,
401
+ specDir,
402
+ // --spec-root 同为平台显式意图(平台模式首扫 run scan --spec-root 在无 pointer 的新目录
403
+ // 合法运行,spec 恒在仓外)——与 --workspace-id/--runtime-root 一并越过本门
404
+ platformFlags: platformWorkspaceId || platformRuntimeRoot || filteredArgs.includes('--spec-root'),
405
+ });
406
+ if (uninitGate.verdict === 'block') {
407
+ console.error(`\n❌ 当前目录未初始化(${dir}):${uninitGate.gitRoot
408
+ ? `已上溯祖先链至 git root(${uninitGate.gitRoot})`
409
+ : '已上溯祖先链(当前不在 git 仓内)'}未找到 .sillyspec,平台指针亦无。`);
410
+ console.error(' 非 init/scan 类命令需要已初始化的项目根。');
411
+ console.error(' 修复:① cd 回项目根(含 .sillyspec 的目录)再跑;② 新项目先 sillyspec init(棕地项目 sillyspec scan);③ 或 --spec-dir <路径> 显式指定 .sillyspec 位置。');
412
+ console.error(' 查询/诊断类命令(status/doctor/progress/next/knowledge 等)不受此拦。');
413
+ process.exit(2);
414
+ }
415
+ if (uninitGate.verdict === 'pass' && uninitGate.anchor && resolve(dir) !== uninitGate.anchor) {
416
+ console.log(`ℹ️ cwd 在子目录(${dir}),已锚定到项目根:${uninitGate.anchor}(spec 解析与产物落点以项目根为准)`);
417
+ dir = uninitGate.anchor;
418
+ }
419
+
391
420
  switch (command) {
392
421
  case 'init':
393
422
  await (await import('./init.js')).cmdInit(dir, { tool, tools: toolValues.length > 0 ? toolValues : null, interactive, specDir, noSkills, forceCards, platformOpts: (platformWorkspaceId || platformRuntimeRoot) ? { workspaceId: platformWorkspaceId, runtimeRoot: platformRuntimeRoot } : null });
package/src/init.js CHANGED
@@ -89,9 +89,9 @@ const TOOL_LABELS = {
89
89
  opencode: 'OpenCode (通过 INSTRUCTIONS.md)',
90
90
  };
91
91
 
92
- // 小段追加工具(gemini/opencode)。codex 不在此列——AGENTS.md 是跨工具通用的标准内容源,
93
- // codex 与 claude 共用完整模板注入(injectAgentsInstructions);gemini/opencode 改 @AGENTS.md
94
- // 指针需先验证两家对 @ 导入语法的支持,留待后续变更。
92
+ // 指令文件工具(gemini/opencode)。codex/claude/zcode 共用 AGENTS.md(跨工具通用标准内容源);
93
+ // gemini/opencode 写各自指令文件,内容与 AGENTS.md 同源同全文(改 @AGENTS.md 指针需先验证
94
+ // 两家对 @ 导入语法的支持,留待后续变更)。
95
95
  const INSTRUCTION_TOOLS = ['gemini', 'opencode'];
96
96
 
97
97
  const INSTRUCTION_FILE_MAP = {
@@ -99,7 +99,13 @@ const INSTRUCTION_FILE_MAP = {
99
99
  opencode: 'INSTRUCTIONS.md',
100
100
  };
101
101
 
102
- const INJECTION_CONTENT = `## SillySpec — 规范驱动开发
102
+ // ── 注入指令文件(全量方案,2026-10-10-init-full-injection:完整模板是唯一注入源)──
103
+
104
+ const AGENTS_TEMPLATE_PATH = join(__dirname, '..', 'templates', 'agents-instruction.md');
105
+
106
+ // 旧版小段(v≤3.32.3:AGENTS.md 追加态受管块与 injectInstructions 的注入源)。仅作老安装
107
+ // 迁移的精确匹配文本,不再是任何注入源——所有状态一律写完整模板。
108
+ const LEGACY_SMALL_BLOCK = `## SillySpec — 规范驱动开发
103
109
 
104
110
  在执行开发任务时,遵循以下规范:
105
111
 
@@ -113,41 +119,23 @@ const INJECTION_CONTENT = `## SillySpec — 规范驱动开发
113
119
  - 各阶段产出文件位于 \`.sillyspec/changes/<变更名>/\` 下
114
120
  `;
115
121
 
116
- // ── 注入指令文件 ──
117
-
118
- function injectInstructions(tool, projectDir) {
119
- const fileName = INSTRUCTION_FILE_MAP[tool];
120
- if (!fileName) return;
121
- const filePath = join(projectDir, fileName);
122
-
123
- // 文件不存在则创建
124
- if (!existsSync(filePath)) {
125
- writeFileSync(filePath, INJECTION_CONTENT);
126
- return;
122
+ function readInstructionTemplate() {
123
+ try {
124
+ return readFileSync(AGENTS_TEMPLATE_PATH, 'utf8');
125
+ } catch {
126
+ console.error(`❌ [sillyspec] 未找到 Agent 指引模板:${AGENTS_TEMPLATE_PATH}`);
127
+ return null;
127
128
  }
128
-
129
- // 已存在 SillySpec 标记则跳过
130
- const content = readFileSync(filePath, 'utf8');
131
- if (content.includes('## SillySpec')) return;
132
-
133
- // 追加到末尾
134
- writeFileSync(filePath, content.trimEnd() + '\n\n' + INJECTION_CONTENT);
135
129
  }
136
130
 
137
- // ── 注入 AGENTS.md / CLAUDE.md(claude + codex,版本感知幂等)──
138
- // AGENTS.md 是唯一承载完整指引的内容源(跨工具通用标准);CLAUDE.md 仅为 @AGENTS.md
139
- // 导入指针(Claude Code 记忆导入语法)。取代 design: 2026-08-02-init-claude-md 的
140
- // CLAUDE.md 单文件方案——其 D-004 预留的「marker 方案迁移」即本次变更。
141
- const AGENTS_TEMPLATE_PATH = join(__dirname, '..', 'templates', 'agents-instruction.md');
142
-
143
131
  /**
144
- * 移除 codex 老安装的旧版小段(injectInstructions 方案:`## SillySpec` 段,追加在 EOF)。
132
+ * 移除老安装的旧版小段(v≤3.32.3 方案:`## SillySpec` 段,追加在 EOF 或整文件即小段)。
145
133
  * 优先精确匹配旧文本;用户编辑过 / CRLF 漂移导致匹配失败时回退按标题截到 EOF。
146
- * @param {string} content - AGENTS.md 原文
134
+ * @param {string} content - 指引文件原文
147
135
  * @returns {string} 截除旧段后的内容(已 trimEnd)
148
136
  */
149
- function stripLegacyAgentsBlock(content) {
150
- const exact = INJECTION_CONTENT.trimEnd();
137
+ function stripLegacySmallBlock(content) {
138
+ const exact = LEGACY_SMALL_BLOCK.trimEnd();
151
139
  const idx = content.indexOf(exact);
152
140
  if (idx >= 0) return content.slice(0, idx).trimEnd();
153
141
  const headingIdx = content.indexOf('## SillySpec');
@@ -156,50 +144,49 @@ function stripLegacyAgentsBlock(content) {
156
144
  }
157
145
 
158
146
  /**
159
- * 为 AGENTS.md 注入完整指引(版本感知幂等,三态四分支 + 旧标记迁移)。
160
- * - 不存在:写完整模板(templates/agents-instruction.md)+ 顶部版本注释
161
- * - 存在无标记:追加受管段(INJECTION_CONTENT)+ 版本块标记;
162
- * 含旧 `## SillySpec` 段(codex 老安装)先截除再追加(marker 迁移,防双段)
147
+ * 向指引文件注入完整模板(版本感知幂等,三态四分支 + 旧标记迁移)。
148
+ * AGENTS.md / GEMINI.md / INSTRUCTIONS.md 共用;完整模板(templates/agents-instruction.md)
149
+ * 是唯一注入源。取代 design: 2026-08-02-init-claude-md 的 CLAUDE.md 单文件方案与
150
+ * v≤3.32.3 的追加态小段方案(其 D-004 预留的「marker 方案迁移」即彼方案)。
151
+ * - 不存在:写完整模板 + 顶部版本注释
152
+ * - 存在无标记:追加受管段(完整模板全文)+ 版本块标记;
153
+ * 含旧 `## SillySpec` 小段(v≤3.32.3 安装)先截除再追加(marker 迁移,防双段)
163
154
  * - 存在同版本标记:跳过(不写文件)
164
155
  * - 存在异版本标记:追加态刷新块 / 完整态仅 stderr 提示(保留用户改动)
165
- * @param {string} projectDir - 源码项目根
156
+ * @param {string} filePath - 指引文件绝对路径
166
157
  */
167
- export function injectAgentsInstructions(projectDir) {
168
- const filePath = join(projectDir, 'AGENTS.md');
158
+ function injectFullInstructions(filePath) {
169
159
  const version = getVersion();
160
+ const template = readInstructionTemplate();
161
+ if (template === null) return;
162
+ const fileName = basename(filePath);
170
163
  // 完整态顶部注释(新文件首行,轻量、不限制后续编辑)
171
164
  const fullHeader = `<!-- SillySpec v${version} — 由 sillyspec init 生成,可自由编辑;重跑 init 同版本不更新 -->`;
172
- // 追加态受管段(包版本块标记,明确"勿手动编辑此段")
173
- const appendBlock =
165
+ // 追加态受管段(包版本块标记,明确"勿手动编辑此段"),内容=完整模板全文
166
+ const managedBlock =
174
167
  `<!-- SillySpec v${version} START — 由 sillyspec init 注入,勿手动编辑此段 -->\n` +
175
- INJECTION_CONTENT.trimEnd() +
168
+ template.trimEnd() +
176
169
  `\n<!-- SillySpec END -->`;
177
170
 
178
171
  // 状态 1:文件不存在 → 写完整模板 + 顶部版本注释
179
172
  if (!existsSync(filePath)) {
180
- let template = '';
181
- try {
182
- template = readFileSync(AGENTS_TEMPLATE_PATH, 'utf8');
183
- } catch {
184
- console.error(`❌ [sillyspec] 未找到 Agent 指引模板:${AGENTS_TEMPLATE_PATH}`);
185
- return;
186
- }
187
173
  writeFileSync(filePath, fullHeader + '\n' + template);
188
- console.log(chalk.green(` ✓ AGENTS.md 已生成(SillySpec v${version},完整指引)`));
174
+ console.log(chalk.green(` ✓ ${fileName} 已生成(SillySpec v${version},完整指引)`));
189
175
  return;
190
176
  }
191
177
 
192
178
  let content = readFileSync(filePath, 'utf8');
193
179
 
194
- // 无任何 `<!-- SillySpec v` 标记 → 状态 2:追加受管段(原文字节保留)
180
+ // 无任何 `<!-- SillySpec v` 标记 → 状态 2:追加完整受管段(原文字节保留)
195
181
  const markMatch = content.match(/<!-- SillySpec v(\S+)/);
196
182
  if (!markMatch) {
197
183
  if (content.includes('## SillySpec')) {
198
- content = stripLegacyAgentsBlock(content);
199
- console.log(chalk.green(' ✓ AGENTS.md 旧版 SillySpec 小段已迁移为新受管段'));
184
+ content = stripLegacySmallBlock(content);
185
+ console.log(chalk.green(' ✓ 旧版 SillySpec 小段已迁移为完整受管段'));
200
186
  }
201
- writeFileSync(filePath, content + '\n\n' + appendBlock + '\n');
202
- console.log(chalk.green(` ✓ AGENTS.md 已追加 SillySpec 受管段(v${version})`));
187
+ const body = content.trimEnd() === '' ? managedBlock : content.trimEnd() + '\n\n' + managedBlock;
188
+ writeFileSync(filePath, body + '\n');
189
+ console.log(chalk.green(` ✓ ${fileName} 已追加 SillySpec 完整指引受管段(v${version})`));
203
190
  return;
204
191
  }
205
192
 
@@ -210,15 +197,25 @@ export function injectAgentsInstructions(projectDir) {
210
197
  // 状态 4:异版本(升级)。区分追加态(有 START...END 块)/ 完整态(仅顶部注释)。
211
198
  const startBlockRe = /<!-- SillySpec v\S+\s+START[\s\S]*?<!-- SillySpec END -->/;
212
199
  if (startBlockRe.test(content)) {
213
- // 4a:追加态 → 用当前版本受管段替换该块(块外用户内容字节保留)
214
- writeFileSync(filePath, content.replace(startBlockRe, appendBlock));
215
- console.log(chalk.green(` ✓ AGENTS.md 受管段已升级(v${existingVersion} → v${version})`));
200
+ // 4a:追加态 → 用当前版本完整受管段替换该块(块外用户内容字节保留)
201
+ writeFileSync(filePath, content.replace(startBlockRe, managedBlock));
202
+ console.log(chalk.green(` ✓ ${fileName} 完整指引受管段已升级(v${existingVersion} → v${version})`));
216
203
  } else {
217
204
  // 4b:完整态 → 不覆盖(保留用户改动),仅 stderr 打印升级提示
218
- console.error(`⚠️ [sillyspec] SillySpec 升级 v${existingVersion}→v${version},AGENTS.md 未自动更新(保留你的改动)。如需采用新模板:备份后删除 AGENTS.md 再跑 sillyspec init。`);
205
+ console.error(`⚠️ [sillyspec] SillySpec 升级 v${existingVersion}→v${version},${fileName} 未自动更新(保留你的改动)。如需采用新模板:备份后删除 ${fileName} 再跑 sillyspec init。`);
219
206
  }
220
207
  }
221
208
 
209
+ export function injectAgentsInstructions(projectDir) {
210
+ injectFullInstructions(join(projectDir, 'AGENTS.md'));
211
+ }
212
+
213
+ export function injectInstructions(tool, projectDir) {
214
+ const fileName = INSTRUCTION_FILE_MAP[tool];
215
+ if (!fileName) return;
216
+ injectFullInstructions(join(projectDir, fileName));
217
+ }
218
+
222
219
  /**
223
220
  * 为 Claude Code 写 CLAUDE.md 指针(@AGENTS.md 导入,薄文件)。
224
221
  * - 不存在:写指针文件(版本头注释 + @AGENTS.md)
@@ -441,7 +438,7 @@ async function doInstall(projectDir, tools, subprojects = [], specDir = null, op
441
438
  }
442
439
  }
443
440
 
444
- // 注入指令文件(gemini/opencode 小段追加,不进 AGENTS.md 注入器)
441
+ // 注入指令文件(gemini/opencode:与 AGENTS.md 同源同全文,共用全量注入器)
445
442
  for (let i = 0; i < tools.length; i++) {
446
443
  const toolName = tools[i];
447
444
  if (INSTRUCTION_TOOLS.includes(toolName)) {
@@ -449,7 +446,7 @@ async function doInstall(projectDir, tools, subprojects = [], specDir = null, op
449
446
  }
450
447
  }
451
448
 
452
- // 注入 AGENTS.md 完整指引(claude/codex/zcode 共用:版本感知幂等三态四分支 + 旧 ## SillySpec 段迁移;
449
+ // 注入 AGENTS.md 完整指引(claude/codex/zcode 共用:版本感知幂等三态四分支 + 旧 ## SillySpec 小段迁移;
453
450
  // zcode 2026-09-21-flow-command-cards FR-05 入面——AGENTS.md 是跨工具通用标准内容源)
454
451
  if (tools.includes('claude') || tools.includes('codex') || tools.includes('zcode')) {
455
452
  injectAgentsInstructions(projectDir);
package/src/run/shared.js CHANGED
@@ -367,6 +367,81 @@ export function detectCwdInsideWorktree(dir) {
367
367
  return null
368
368
  }
369
369
 
370
+ // ── 未初始化目录硬拦(坑:agent 在未初始化/错误目录跑非 init 命令,fail-soft 散点报错不说
371
+ // 正确 cwd,agent 多轮试错)──
372
+ // 豁免面 = 按设计可在未初始化目录运行的命令。每项豁免依据(勿凭感觉增删):
373
+ // init/scan 初始化器与棕地入口,未初始化目录是本职(scan 亦是 monorepo 子项目独立建实例的合法入口)
374
+ // doctor/status/progress/next/workspace 未初始化是它们的设计输出态(run/next.js
375
+ // '未初始化(无 .sillyspec 产物)'、workspace.js '未初始化(无 .sillyspec)')
376
+ // setup/config 自带候选链 fail-closed 报错('请先运行 sillyspec init')
377
+ // knowledge 知识目录缺失走友好空态(stages/knowledge.js)
378
+ // local 纯 fs 嗅探生成 local.yaml,设计上 init 前可用
379
+ // mcp/dashboard/platform 服务/平台设施,自有路径解析
380
+ // wt-commit 子代理 workdir=worktree 内合法(worktree cwd 硬拦同款豁免)
381
+ // agent-log --detect 现场探测不落盘,自有解析链
382
+ export const UNINIT_CWD_GATE_EXEMPT = new Set([
383
+ 'init', 'scan', 'doctor', 'status', 'progress', 'next', 'workspace',
384
+ 'setup', 'knowledge', 'local', 'config', 'mcp', 'dashboard', 'platform',
385
+ 'wt-commit', 'agent-log',
386
+ ])
387
+
388
+ /**
389
+ * CLI 入口「未初始化目录」判定(与 detectCwdInsideWorktree 分工:那边判「在隔离 worktree
390
+ * 内」,文案更具体故先拦;这边判「任何形式的 spec 都解析不到」——覆盖未初始化目录与非
391
+ * sillspec 项目的误跑两类)。
392
+ *
393
+ * 判定复用 resolveSpecDir 全套守卫(home 拒绝 / tmp 边界 / .runtime 回环防护)——不重写
394
+ * 遍历,否则已修复的坑(2026-09-27-spec-sync-413 .runtime 回环等)会回归。
395
+ *
396
+ * 返回(风格仿 detectCwdInsideWorktree):
397
+ * { verdict: 'exempt' } 豁免命令 / 无命令(usage 已早退,保险不拦),不拦不锚
398
+ * { verdict: 'skip' } 显式意图(--spec-dir、平台 flag、平台 pointer/接管声明在
399
+ * dir)——spec 解析与错误面归命令自身的 fail-closed 文案
400
+ * (PointerUnreachableError 等),本门不重复判、更不能拿
401
+ * 「dir 里没有 .sillyspec」误拦平台项目
402
+ * { verdict: 'pass', anchor } 命中已初始化上下文。anchor=spec 根父目录(供调用方重锚定,
403
+ * 修裸 join(dir,'.sillyspec') 调用点如 review status rsSpecBase
404
+ * 的子目录漂移);linked worktree rescued 场景 anchor=null——
405
+ * 主仓 spec 不在 cwd 祖先链上,锚定交命令层自有逻辑
406
+ * (如 endpoints baseline 的主仓锚定),本门不越俎
407
+ * { verdict: 'block', gitRoot } 全链未命中 → 调用方硬报错 exit 2;gitRoot 供文案指路
408
+ * (未初始化目录无 ground truth,只能给锚点不能断言唯一 cwd)
409
+ *
410
+ * @param {string} command 顶层命令名
411
+ * @param {{ dir?: string, specDir?: string, platformFlags?: unknown }} opts
412
+ * dir=目标目录(cwd 或 --dir);specDir=显式 --spec-dir;platformFlags=--workspace-id/
413
+ * --runtime-root/--spec-root 任一显式给出即为真值(平台模式显式意图)
414
+ */
415
+ export function resolveUninitCwdGate(command, { dir, specDir, platformFlags } = {}) {
416
+ if (!command || UNINIT_CWD_GATE_EXEMPT.has(command)) return { verdict: 'exempt' }
417
+ if (specDir || platformFlags) return { verdict: 'skip' }
418
+ const resolvedDir = resolve(dir || process.cwd())
419
+ if (existsSync(join(resolvedDir, '.sillyspec-platform.json')) || existsSync(join(resolvedDir, PLATFORM_MANAGED_FILENAME))) {
420
+ return { verdict: 'skip' }
421
+ }
422
+ const spec = resolveSpecDir(resolvedDir)
423
+ if (existsSync(spec)) return { verdict: 'pass', anchor: dirname(spec) }
424
+ // linked worktree 兜底(判据同 resolveEffectiveDir P1-1 / worktree.js _resolveMainRepoRoot:
425
+ // --git-dir ≠ --git-common-dir 且非 submodule):主仓 .sillyspec 不在 cwd 祖先链上(如
426
+ // `git worktree add` 到兄弟路径的副本),resolveSpecDir 必 miss 但项目实为已初始化——
427
+ // 误拦会打断 endpoints baseline 等命令层自有的主仓锚定逻辑。放行不重锚:锚定归命令层。
428
+ const gd = safeGit(resolvedDir, ['rev-parse', '--git-dir'])
429
+ const gcd = safeGit(resolvedDir, ['rev-parse', '--git-common-dir'])
430
+ const superProj = safeGit(resolvedDir, ['rev-parse', '--show-superproject-working-tree'])
431
+ if (gd.value && gcd.value && gd.value !== gcd.value && !superProj.value) {
432
+ // common-dir 可能是相对路径,须相对 resolvedDir 绝对化(与 worktree.js 同坑同解)
433
+ const absCommonDir = resolve(resolvedDir, gcd.value)
434
+ if (existsSync(absCommonDir)) {
435
+ const mainRoot = dirname(absCommonDir)
436
+ if (mainRoot !== resolvedDir && existsSync(join(mainRoot, '.sillyspec'))) {
437
+ return { verdict: 'pass', anchor: null }
438
+ }
439
+ }
440
+ }
441
+ const gitRoot = safeGit(resolvedDir, ['rev-parse', '--show-toplevel']).value || null
442
+ return { verdict: 'block', gitRoot }
443
+ }
444
+
370
445
  /**
371
446
  * 统一解析 .runtime 根目录(坑 execute-runs-isolation)。
372
447
  *
package/src/worktree.js CHANGED
@@ -1326,9 +1326,17 @@ export class WorktreeManager {
1326
1326
  * 写进 worktree 内的 .sillyspec——而 apply 的 filterDeliverableFiles 把 .sillyspec/changes/ 排除在
1327
1327
  * 交付外(spec 文档不进代码 patch),cleanup 删除 worktree 这些产物即蒸发、主仓流程看不到。
1328
1328
  * 删除前扫 worktree 内两棵子树做最后打捞:
1329
- * changes/<name>/** → 主仓缺失则 copy 回(verify-result.md 等核心场景);同名不同内容仅列
1330
- * 清单 warn 不覆盖(主仓版本可能是更新的,覆盖会倒退)
1331
- * docs/** → 主仓缺失则 copy 回(模块文档卡片;同名差异不提示——tracked checkout 副本常态)
1329
+ * changes/<name>/** → 三态判定(坑 archive-resurrect,2026-10-10 下游实证
1330
+ * 2026-10-09-attachment-inline-reference 残留 13 文件:归档把 changes/<name>/
1331
+ * 整体 rename 进 changes/archive/ 后,原路径缺失被误判 worktree 独有,旧快照
1332
+ * 整批复制回原路径复活已归档目录):
1333
+ * · 原路径同名存在 → 内容不同仅列清单 warn 不覆盖(主仓版本可能是更新的)
1334
+ * · 原路径缺、changes/archive/<name>/ 有副本 → 判归档搬运,跳过不复制
1335
+ * (不复活原路径、不覆盖 archive 权威副本)
1336
+ * · 原路径与 archive 均缺 → 真 worktree 独有照捞:归档态捞进
1337
+ * changes/archive/<name>/(原路径已注销禁止重建),未归档态捞回原路径
1338
+ * docs/** → 主仓缺失则 copy 回(模块文档卡片;同名差异不提示——tracked checkout 副本常态;
1339
+ * docs 不参与归档搬运,无三态)
1332
1340
  * 只扫本变更子树(其它 change 的副本文件不越权搬运)。异常只 warn 不阻断清理。
1333
1341
  * @private
1334
1342
  */
@@ -1340,7 +1348,11 @@ export class WorktreeManager {
1340
1348
  if (resolve(wtSpec) === resolve(mainSpec)) return; // in-place:同一目录,无需打捞
1341
1349
  const salvaged = [];
1342
1350
  const conflicts = [];
1343
- const salvageTree = (subDir, reportConflict) => {
1351
+ const archivedSkipped = [];
1352
+ const salvagedIntoArchive = [];
1353
+ const archivedBase = join('changes', 'archive', name);
1354
+ const archivedBaseExists = existsSync(join(mainSpec, archivedBase));
1355
+ const salvageTree = (subDir, reportConflict, trackArchive) => {
1344
1356
  const srcRoot = join(wtSpec, subDir);
1345
1357
  if (!existsSync(srcRoot)) return;
1346
1358
  const walk = (dir) => {
@@ -1349,28 +1361,50 @@ export class WorktreeManager {
1349
1361
  if (entry.isDirectory()) { walk(src); continue; }
1350
1362
  if (!entry.isFile()) continue;
1351
1363
  const rel = relative(wtSpec, src);
1364
+ const rest = relative(srcRoot, src);
1352
1365
  const dst = join(mainSpec, rel);
1366
+ const archivedDst = trackArchive ? join(mainSpec, archivedBase, rest) : null;
1353
1367
  try {
1354
- if (!existsSync(dst)) {
1368
+ if (existsSync(dst)) {
1369
+ if (reportConflict && readFileSync(src, 'utf8') !== readFileSync(dst, 'utf8')) {
1370
+ conflicts.push(rel);
1371
+ }
1372
+ } else if (archivedDst && existsSync(archivedDst)) {
1373
+ archivedSkipped.push(rest);
1374
+ } else if (archivedDst && archivedBaseExists) {
1375
+ mkdirSync(dirname(archivedDst), { recursive: true });
1376
+ copyFileSync(src, archivedDst);
1377
+ salvagedIntoArchive.push(rest);
1378
+ } else {
1355
1379
  mkdirSync(dirname(dst), { recursive: true });
1356
1380
  copyFileSync(src, dst);
1357
1381
  salvaged.push(rel);
1358
- } else if (reportConflict && readFileSync(src, 'utf8') !== readFileSync(dst, 'utf8')) {
1359
- conflicts.push(rel);
1360
1382
  }
1361
1383
  } catch { /* 单文件失败不拖垮整体打捞 */ }
1362
1384
  }
1363
1385
  };
1364
1386
  walk(srcRoot);
1365
1387
  };
1366
- salvageTree(join('changes', name), true);
1367
- salvageTree('docs', false);
1388
+ salvageTree(join('changes', name), true, true);
1389
+ salvageTree('docs', false, false);
1368
1390
  if (salvaged.length > 0) {
1369
1391
  console.warn(`⚠️ worktree 清理前打捞:${salvaged.length} 个 spec 流程产物只存在于 worktree 副本,已复制回主仓(apply 不搬运 .sillyspec/changes,不捞即随清理蒸发):`);
1370
1392
  for (const f of salvaged.slice(0, 20)) console.warn(` ${join(mainSpec, f)}`);
1371
1393
  if (salvaged.length > 20) console.warn(` …等共 ${salvaged.length} 个`);
1372
1394
  details.push(`salvaged ${salvaged.length} spec artifacts to main repo`);
1373
1395
  }
1396
+ if (salvagedIntoArchive.length > 0) {
1397
+ console.warn(`⚠️ worktree 清理前打捞:${salvagedIntoArchive.length} 个真独有产物(原路径与归档副本均缺)已捞进归档副本(变更已归档,原路径不重建):`);
1398
+ for (const f of salvagedIntoArchive.slice(0, 20)) console.warn(` ${join(mainSpec, archivedBase, f)}`);
1399
+ if (salvagedIntoArchive.length > 20) console.warn(` …等共 ${salvagedIntoArchive.length} 个`);
1400
+ details.push(`salvaged ${salvagedIntoArchive.length} spec artifacts into archive copy`);
1401
+ }
1402
+ if (archivedSkipped.length > 0) {
1403
+ console.warn(`⏭️ worktree 清理前打捞:${archivedSkipped.length} 个流程产物判定为归档搬运(主仓 changes/${name}/ 原路径不存在、changes/archive/${name}/ 已有副本),跳过不复制——防复活已归档目录:`);
1404
+ for (const f of archivedSkipped.slice(0, 20)) console.warn(` ${join(mainSpec, archivedBase, f)}`);
1405
+ if (archivedSkipped.length > 20) console.warn(` …等共 ${archivedSkipped.length} 个`);
1406
+ details.push(`skipped ${archivedSkipped.length} archived-moved spec artifacts (resurrect guard: changes/${name}/ not rebuilt)`);
1407
+ }
1374
1408
  if (conflicts.length > 0) {
1375
1409
  console.warn(`⚠️ worktree 与主仓存在同名但内容不同的流程产物(不覆盖,请人工确认是否需要合并):`);
1376
1410
  for (const f of conflicts.slice(0, 20)) console.warn(` ${join(mainSpec, f)}`);
@@ -1,116 +0,0 @@
1
- ---
2
- name: sillyspec:quick
3
- description: 用于明确、低风险、范围很小的直接任务。适合用户说"直接改、快速修、顺手调整、改个文案、修个小 bug、更新一个文件、不要完整流程"。跳过 brainstorm/plan,但仍按 sillyspec quick 流程执行。
4
- ---
5
-
6
- ## 何时使用
7
-
8
- - 明确、低风险、范围小的直接修改:改文案、修小 bug、更新单个文件
9
- - 用户说"直接改、快速修、顺手调整、不要完整流程"
10
- - brainstorm 判定 `scale=small` 的变更:带 design.md 进 quick(`sillyspec run quick --linked-changes <变更名>`),design 当背景
11
- - 跳过 brainstorm/plan,但仍走 quick 的 3 步流程(理解任务 → 实现 → 自检提交)
12
-
13
- ## 多变更说明(quick 特殊,务必注意)
14
-
15
- quick 阶段的 `--change` 语义是「关联变更」**且会触发步骤重置**,**不要用 `--change` 来指定关联变更**。多活跃变更时改用:
16
-
17
- - `--linked-changes none`:不关联,仅记 QUICKLOG(CLI 启动时写入)
18
- - `--linked-changes a,b`:显式关联到变更 a、b
19
- - `--non-interactive`:CI/脚本环境,默认不关联(避免交互 prompt 崩溃)
20
-
21
- 首次 `sillyspec run quick` 选定的关联会持久化到 `.runtime/quick-sessions/<quick-session-id>/guard.json`(每个 quick 会话独立,多会话并发不互覆盖),后续 `--done` 自动复用,**不会重复弹交互 prompt**。
22
-
23
- ## 步骤生命周期(所有阶段通用)
24
-
25
- > `sillyspec quick` 是 `sillyspec run quick` 的顶层别名,两者等价。
26
-
27
- ```bash
28
- sillyspec run quick # 输出当前步骤 prompt(首次会记录 baseline)
29
- sillyspec run quick --done --output "摘要" # 完成当前步骤
30
- sillyspec run quick --status # 查看阶段进度
31
- sillyspec run quick --skip # 跳过可选步骤
32
- sillyspec run quick --reset # 重置阶段(从头开始)
33
- sillyspec run quick --reopen --from-step N # 重新打开已完成阶段修订(N=序号或名称)
34
- ```
35
-
36
- ### 已有进行中会话的恢复(sessionId)
37
-
38
- `sillyspec run quick` / `run quick --done` / `--status` **不带 `--change` 时,是启动一个新 quick 会话**(CLI 分配新 `quick-<hash>` + 新 ql-ID,QUICKLOG 追加「进行中」条目),不是恢复正在进行的旧会话。多会话并发或中断恢复时,必须带上启动时 CLI 打印的 sessionId:
39
-
40
- ```bash
41
- sillyspec run quick --change quick-<hash> # 恢复查看该会话当前 step prompt
42
- sillyspec run quick --done --change quick-<hash> --output "…" # 完成该会话步骤
43
- ```
44
-
45
- - 这里的 `--change quick-<hash>` 是「恢复到该 CLI 生成的 session」,与下方 `--linked-changes` 的关联业务变更语义不同:想关联真实业务变更才用 `--linked-changes`。
46
- - 误启动的空壳会话:`sillyspec run quick --cancel --change quick-<hash>` 一键取消(QUICKLOG 条目翻「已取消」+ tasks.md 挂载行移除 + 会话目录清理 + db 行注销;已完成/已勾选的拒绝取消)。
47
-
48
- ## 通用参数(所有阶段适用)
49
-
50
- | 参数 | 说明 |
51
- |---|---|
52
- | `--spec-dir <path>` | 指定规范目录(默认 `<项目>/.sillyspec`) |
53
- | `--input "<一句话任务描述>"` | 通用参数,**quick 启动时强烈建议带**:作为 QUICKLOG 条目标题,条目从第一分钟即语义可读。不带则落「(quick 任务)」占位标题——平台「快速修复」列表默认隐藏进行中的占位条目,语义标题要到最终 `--done` 才回填(关联变更有 proposal/design 标题时可自动提取,免传) |
54
- | `--non-interactive` | CI/脚本下禁用交互式 prompt |
55
- | `--interactive` | 强制交互(即便 stdin 非 TTY) |
56
- | `--skip-approval` | 跳过阶段转换/审批检查(不能跳产物校验 gate——review.json/文档产物硬校验仍在) |
57
-
58
- ## quick 特有参数
59
-
60
- | 参数 | 说明 |
61
- |---|---|
62
- | `--linked-changes none\|a,b` | **显式关联变更(取代 `--change`,推荐)**。none=不关联,a,b=关联列表 |
63
- | `--files a.js,b.js` | 显式声明本次允许修改的文件(边界保护 + 声明即归属:多 agent 并发仓防他者窗口文件混入 QUICKLOG 文件行,未声明窗口文件进「审计:」行追溯;**他向亦然**——其他 active 会话已声明的文件在你的 `--done` 审计中自动剔除归该会话,不再误拦、无需 `--force-baseline`)。**中途可追加**:发现要改声明外文件(如新写的测试)时,带新增文件恢复会话即并入边界(追加不替换)——`sillyspec run quick --files <新增> --change <sessionId>`;不带 `--files` 恢复则边界原样保留 |
64
- | `--file-notes "p::注 \|\| p::注"` | quick `--done` 用:QUICKLOG「文件:」行落盘为多行带括注 bullet(省事后手改文件行)。格式 `path::括注`,`\|\|` 分隔多条;**只随 step3 --done 同命令传**(CLI 短进程,step1/step2 传无效,不带到 step3) |
65
- | `--allow-new` | 允许新增文件(默认禁止,防意外创建) |
66
- | `--allow-delete` | 允许删除文件(默认 fail-closed,删除是破坏性操作;确认删除带此 flag 显式解锁) |
67
- | `--force-baseline` | 允许覆盖 baseline 受保护文件 / 压制 `.sillyspec/` 危险判定(危险,慎用) |
68
- | `--confirm` | ⚠️ 仅打印变更概览,**不解锁 blocked**(blocked 仍 exit 1)。真正解锁用 `--force-baseline`/`--allow-new`/`--allow-delete` |
69
-
70
- ## 审计与并发变更(`--done` 边界审计)
71
-
72
- `--done` 收尾时对比 step1 baseline 与当前 `git status`,拦「危险文件 / 新增文件 / 覆盖 baseline」。要点:
73
-
74
- - **并发其他会话的 `.sillyspec/changes/<非关联变更>/` 不再被本 quick 拦截**。quick 自己没有 `changes/` 目录,该路径下非关联内容视为并发会话的工作,整体放行(确定性审计无法区分「并发工作」与「本 quick 偷建变更」,后者这类意图软判定留给 sillyhub)。
75
- - **其他 active 会话已声明的文件自动豁免(`--files` 的他向语义)**。多 agent 并发时,并行 quick 会话 `--files` 显式声明的文件(如它的 `daemon/router.py`)出现在你的窗口内(它在你启动后改的、不在你的 baseline)——`--done` 审计自动剔除归该会话边界管,不再判危险 BLOCK、无需 `--force-baseline`。放行可见:输出 🔗 软警告逐文件列归属会话;若确系你的改动,恢复会话时 `--files` 追加声明;超 7 天未收尾的僵尸会话声明自动失效(`sillyspec run quick --cancel --change <他者会话ID>` 清理)。豁免只认他者**显式声明**——未声明的他者文件照旧走各门判定(漏声明的补 `--files`,危险文件仍需 `--force-baseline`)。
76
- - **关联变更的文件仍走审计**:reverse-sync 改自己关联变更的 `design.md` 会被拦,需 `--force-baseline` 显式确认。
77
- - **baseline 折叠目录前缀匹配**:step1 启动时整片 `changes/` 未跟踪会被 git 折叠成 `?? .sillyspec/changes/`(带尾斜杠 token);审计时若该目录下文件被并发会话跟踪而展开成文件级路径,按尾斜杠 token 前缀放行其下所有文件,不误判。
78
- - **同文件并发 warn(advisory,不阻断)**:step1 启动时若你的某个 `allowedFile` 已在他者脏文件列表里(他者也改了这文件),--done 时 CLI 会比对当前内容 hash 与启动时录入的 hash——不一致(你也改了它)即判「同文件并发」并 warn:整文件 pathspec 提交会夹带他者 hunk。warn 给出分离指引(`git add -p <file>` 交互选你自己的 hunk,或 `git diff <file> > mine.patch` 编辑后 `git apply --cached mine.patch` 再 commit)。这**不阻断** --done(你可能有意整文件提交),看 warn 后自行决定是否分离。
79
- - **`--confirm` ≠ 解锁**:它只打印概览。blocked 时真正解锁的是 `--force-baseline`(压制 `.sillyspec/` 危险判定 + baseline 覆盖)/`--allow-new`(放行新增)/`--allow-delete`(放行删除,删除是破坏性操作默认 fail-closed,须显式 opt-in),三者可在 step1 或 `--done` 传,都生效。
80
-
81
- ## 典型用法
82
-
83
- ```bash
84
- # 推荐启动:带一句话语义标题(QUICKLOG/平台快速修复列表进行中即可见可读)
85
- sillyspec run quick --input "修复登录限流 INCR 计数误清" --linked-changes none --files src/auth.ts
86
-
87
- # 单变更项目,直接开始
88
- sillyspec run quick
89
-
90
- # 多变更项目,显式不关联
91
- sillyspec run quick --linked-changes none
92
-
93
- # 多变更项目,关联到指定变更
94
- sillyspec run quick --linked-changes 2026-07-03-add-login
95
-
96
- # CI/脚本(非交互,避免 prompt 崩溃)
97
- sillyspec run quick --non-interactive
98
- sillyspec run quick --done --linked-changes none --output "修复手机号校验"
99
-
100
- # 限定修改文件范围
101
- sillyspec run quick --files src/phone.ts,src/phone.test.ts
102
- ```
103
-
104
- ## 铁律
105
-
106
- - **必须用 exec 工具(shell)执行 CLI,不要自己编造流程**
107
- - quick 直接在主工作区改代码(不创建 worktree),范围必须小且明确
108
- - 完成后立即 `--done`,不跳过
109
- - QUICKLOG 记录的**骨架由 CLI 接管**:启动时 CLI 自动分配 ql-ID 并在 `.sillyspec/quicklog/QUICKLOG-<user>.md` 写「进行中」条目(含关联变更 tasks.md),`--done` 时 CLI 自动翻「已完成」+ 勾选 task + 回填文件路径。ql-ID 分配/状态/task 你无需手写,只需用注入的 `<quicklog-id>` 在模块文档变更索引引用
110
- - **QUICKLOG 落盘已结构化(`--done` 后按需核对,多数无需手改)**:CLI 已落盘结构化条目——标题从 `--output` 的「需求:」自动提取、正文四字段自动分行、文件行用 `--file-notes` 时为多行带括注。`--done` 后只需核对:标题弱才改(禁留 `(quick 任务)` 占位);没用 `--file-notes` 时文件行是单行、可事后补括注(参照同文件早期丰富条目);正文 `需求:`/`根因:`/`方案:`/`结果:` 四段按需充实(禁只留一段「结果:」)。一条 quick = 一条独立 ql,不追加到旧条目
111
- - **收尾顺序(模块文档在 `--done` 前,QUICKLOG 在 `--done` 后,别记混)**:① 命中模块→改模块文档→`git add`;② `sillyspec run quick --done --change <id> --output "四字段" [--file-notes "..."]`(CLI 自动翻完成 + 勾 task + 落盘 QUICKLOG 标题/文件/正文;若 `--linked-changes` 关联的真实变更其 tasks.md 已全部勾选**且该变更未进入完整流程**(进度库阶段停在 brainstorm 及之前),CLI 会自动将其归档到 `changes/archive/`;已走到 plan/execute/verify/archive 的变更不自动归档(tasks.md 全勾不等于流程收尾),须走原流程收尾);③ 核对 QUICKLOG 标题(CLI 已从「需求:」提取并截断,写成短标题则无需改)→若改了再 `git add`
112
- - **最后一步 `--done` 必须给全四字段**(逐项一句话):🔧 **推荐四参数形式**(CLI 自动合成结构化模板,无格式事故面):`--done --change <id> --req "一句语义化短标题" --cause "为什么改" --solution "怎么改的" --result "验证结果"`。兼容旧形式 `--output "需求:… 根因:… 方案:… 结果:…"`(手拼须懂截断规则、禁嵌套全角冒号,缺字段被拒 exit 1)。这是 QUICKLOG「结果:」归档的唯一来源。**`--req` 写一句语义化短标题**(写「改了什么」,如「登录限流修复——INCR 计数误清」)——CLI 把它提取为 QUICKLOG 条目标题,截到首个标点、超 80 字截断;写完整需求长句会被截成语义不完整的状语前半段,需求背景放 `--cause`/`--solution`。前两个 step 的 `--output` 是中间摘要,不用此模板
113
- - **禁止**在没有运行 CLI 的情况下自行决定流程
114
-
115
- ## 用户指令
116
- $ARGUMENTS
@@ -1,20 +0,0 @@
1
- ---
2
- name: sillyspec:run-quick
3
- description: 已退役重定向 — quick 通道已拒绝新会话(v3.31.0 起),小修补请用 /sillyspec:flow
4
- ---
5
- # 通道已退役
6
-
7
- quick 通道已于 v3.31.0 直接退役:`sillyspec run quick` 对新会话一律拒绝(exit 1 并指路 flow start)。本卡只保留触发词重定向,勿执行任何 quick 新会话命令。
8
-
9
- 范围明确的小修补请走**轻量变更**(/sillyspec:flow):
10
-
11
- ```bash
12
- sillyspec flow start --change <变更名> --input "<动机与背景>
13
-
14
- 成功标准:
15
- - <可验证标准>"
16
- # → 直接干活(改代码写测试,治理工件 CLI 机器起草)
17
- sillyspec flow done --change <变更名>
18
- ```
19
-
20
- 升级前遗留的在途 quick 会话仍可收尾:`sillyspec run quick --change <会话ID>` 续跑 / `--done` 收口 / `--cancel` 取消(以 CLI 拒绝文案里的指引为准)。