sillyspec 3.25.5 → 3.25.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +1 -1
- package/.claude/skills/sillyspec-scan/SKILL.md +12 -7
- package/.claude/skills/sillyspec-verify/SKILL.md +2 -2
- package/package.json +1 -1
- package/src/change-list.js +100 -12
- package/src/constants.js +11 -0
- package/src/index.js +1 -1
- package/src/run/command.js +57 -2
- package/src/run/complete-handlers.js +5 -1
- package/src/run/complete.js +20 -1
- package/src/run/gates.js +37 -0
- package/src/run/quick-audit.js +8 -1
- package/src/run/scan-profile.js +29 -10
- package/src/run/shared.js +161 -4
- package/src/run/stage.js +4 -0
- package/src/scan-postcheck.js +25 -11
- package/src/stage-contract.js +6 -1
- package/src/stage-review.js +10 -4
- package/src/stages/brainstorm.js +9 -14
- package/src/stages/plan-postcheck.js +1 -1
- package/src/stages/plan.js +6 -0
- package/src/stages/quick.js +31 -31
- package/src/stages/scan.js +2 -1
- package/src/stages/verify.js +6 -3
- package/src/task-review.js +159 -0
- package/src/verify-postcheck.js +148 -3
- package/src/worktree-apply.js +34 -29
- package/templates/prompts/verify-probes.md +13 -2
|
@@ -48,7 +48,7 @@ sillyspec run brainstorm --done --answer "..." --output "..." # 一步完成
|
|
|
48
48
|
|
|
49
49
|
某些步骤(如"对话式探索与需求澄清")需要用户输入。两种方式:
|
|
50
50
|
|
|
51
|
-
> 自动去重:若前置 step 已对同一问题(waitReason
|
|
51
|
+
> 自动去重:若前置 step 已对同一问题(waitReason 归一化后相同)确认过,后续重复 wait 会自动跳过,无需再 `--wait`。
|
|
52
52
|
|
|
53
53
|
- **方式一(推荐)**:AI 自行与用户交互后,一步完成:
|
|
54
54
|
```bash
|
|
@@ -38,22 +38,27 @@ sillyspec run scan --reset # 重置阶段(从头开始)
|
|
|
38
38
|
|
|
39
39
|
| 参数 | 说明 |
|
|
40
40
|
|---|---|
|
|
41
|
-
| `--
|
|
41
|
+
| `--quick` | 强制 quick profile(快速接入,仅 4 份核心文档,0 子代理) |
|
|
42
|
+
| `--standard` | 强制 standard profile(压缩步骤,最多 1 子代理) |
|
|
43
|
+
| `--deep` | 强制 deep profile(完整流程,不按规模裁剪) |
|
|
42
44
|
| `--force-rescan` | 覆盖已有 scan 文档的保护(默认覆盖需 source_commit/updated_at 匹配) |
|
|
43
45
|
|
|
44
|
-
### scanProfile
|
|
46
|
+
### scanProfile(显式选择优先,否则按规模自动裁剪)
|
|
45
47
|
|
|
46
|
-
|
|
48
|
+
可用 `--quick` / `--standard` / `--deep` **显式指定 profile**(三档互斥,优先于自动判定)。
|
|
49
|
+
不带 flag 时 CLI 按源码规模自动选择:
|
|
47
50
|
|
|
48
51
|
| profile | 触发条件 | 行为 |
|
|
49
52
|
|---|---|---|
|
|
50
|
-
| quick | ≤30 文件 且 ≤80KB 且 ≤3 项目 | 3 步,0 子代理,
|
|
51
|
-
| standard | ≤200 文件 且 ≤800KB | 压缩步骤,最多 1 子代理 |
|
|
52
|
-
| deep |
|
|
53
|
+
| quick | `--quick` 或 ≤30 文件 且 ≤80KB 且 ≤3 项目 | 3 步,0 子代理,4 份核心文档 |
|
|
54
|
+
| standard | `--standard` 或 ≤200 文件 且 ≤800KB | 压缩步骤,最多 1 子代理 |
|
|
55
|
+
| deep | `--deep` 或 大项目 | 完整流程 |
|
|
56
|
+
|
|
57
|
+
> **平台快速接入**:对大项目用 `sillyspec run scan --quick`(含平台参数 `--spec-root`/`--workspace-id`/`--scan-run-id`)只生成 4 份核心文档完成接入,后续 `sillyspec run scan --deep` 覆盖升级为完整 7 份。quick 文档 frontmatter 标 `scan_depth: quick`,深度扫描识别后允许覆盖。
|
|
53
58
|
|
|
54
59
|
### post-check
|
|
55
60
|
|
|
56
|
-
scan 完成时 CLI
|
|
61
|
+
scan 完成时 CLI **按 profile 校验文档齐全**:quick 模式只要求 4 份核心文档(PROJECT/ARCHITECTURE/CONVENTIONS/STRUCTURE),standard/deep 要求完整 7 份。缺失会设状态为 `failed_post_check`,阻断进入主流程下游(brainstorm/plan 等),需修复后重跑 scan。quick 模式带一条 informational warning(`quick_profile_notice`),状态落 `completed_with_warnings`,不阻断完成。
|
|
57
62
|
|
|
58
63
|
## 阶段流转
|
|
59
64
|
|
|
@@ -43,7 +43,7 @@ verify 是只读阶段(**禁止改代码/改 git 状态**,只检查 + 写报
|
|
|
43
43
|
|
|
44
44
|
- **必须产出 `verify-result.md`**——不存在则阻断完成(不能跳过报告直接 `--done`)
|
|
45
45
|
- **结论为 `FAIL` 则阻断完成**——不能带着 FAIL 标记 verify 完成
|
|
46
|
-
- **`integration-critical` / `deployment-critical` 变更**(design/plan 含 daemon/session/lease/lifecycle 等关键词):结论 PASS WITH NOTES 降级为 FAIL,必须有真实集成证据(Runtime Evidence section
|
|
46
|
+
- **`integration-critical` / `deployment-critical` 变更**(design/plan 含 daemon/session/lease/lifecycle 等关键词):结论 PASS WITH NOTES 降级为 FAIL,必须有真实集成证据(Runtime Evidence section)——该证据为 Agent 自报告,CLI 仅校验其字面存在、不独立运行时核验,须真实执行过
|
|
47
47
|
- `verify-required-evidence.json`(execute 写入)中每条 missing evidence → 阻断
|
|
48
48
|
|
|
49
49
|
被阻断时 CLI 打印 ❌ 校验失败,不会提示"验证通过"。修复 `verify-result.md` 后重新 `--done`。
|
|
@@ -59,7 +59,7 @@ PASS / PASS WITH NOTES / FAIL ← 必须有结论章节(标题含"结论/
|
|
|
59
59
|
## 探针结果
|
|
60
60
|
## 测试结果
|
|
61
61
|
## 变更风险等级
|
|
62
|
-
## Runtime Evidence(integration/deployment-critical
|
|
62
|
+
## Runtime Evidence(integration/deployment-critical 必填;自报告,CLI 仅字面校验、不独立核验)
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
## 阶段流转
|
package/package.json
CHANGED
package/src/change-list.js
CHANGED
|
@@ -81,6 +81,43 @@ const FILE_LIST_SECTION_RE = /^#{2,3}\s*(?:\d+[.)]\s*)?(文件变更清单|变
|
|
|
81
81
|
*/
|
|
82
82
|
const EXCLUDE_SUBSECTION_RE = /不修改|不变|保留|无变更|未变更|不改动|暂不|暂缓|暂定|待定|后续|未改|无需|不涉及/
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* operation 词→归一化标签。design 清单「操作」列 cell 与分类列表子标题共用。
|
|
86
|
+
* 用途:verify 删除探针对账——声明「新增/修改」却整文件删除 = 高风险信号。
|
|
87
|
+
*/
|
|
88
|
+
const OP_KEYWORDS = [
|
|
89
|
+
{ re: /^(新增|新建|创建|添加|add|new|create)$/i, op: '新增' },
|
|
90
|
+
{ re: /^(修改|更新|调整|改动|变更|重写|modify|update|change)$/i, op: '修改' },
|
|
91
|
+
{ re: /^(删除|移除|去掉|remove|delete|drop)$/i, op: '删除' },
|
|
92
|
+
{ re: /^(重命名|改名|rename|move)$/i, op: '重命名' },
|
|
93
|
+
]
|
|
94
|
+
function classifyOperation(raw) {
|
|
95
|
+
if (!raw) return null
|
|
96
|
+
const hit = OP_KEYWORDS.find(k => k.re.test(raw.trim()))
|
|
97
|
+
return hit ? hit.op : null
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 分类列表子标题 → operation(`### 删除文件` → '删除')。不命中返回 null。
|
|
102
|
+
* ⚠️ 调用方必须先判 EXCLUDE_SUBSECTION_RE:「### 不修改文件」含「修改」二字,
|
|
103
|
+
* 会被此处误匹配为「修改」——exclude 永远优先,命中 exclude 不跑 OP 映射。
|
|
104
|
+
*/
|
|
105
|
+
const OP_SUBSECTION_RE = [
|
|
106
|
+
{ re: /新增|新建|创建/, op: '新增' },
|
|
107
|
+
{ re: /修改|更新|改动|重写/, op: '修改' },
|
|
108
|
+
{ re: /删除|移除|去掉/, op: '删除' },
|
|
109
|
+
{ re: /重命名|改名/, op: '重命名' },
|
|
110
|
+
]
|
|
111
|
+
function classifySubsectionOp(title) {
|
|
112
|
+
const hit = OP_SUBSECTION_RE.find(k => k.re.test(title))
|
|
113
|
+
return hit ? hit.op : null
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** 表头列定位:cell 整体匹配操作类列名(与 isPathHeaderCell 对偶,用于定位 operation 列) */
|
|
117
|
+
function isOperationHeaderCell(c) {
|
|
118
|
+
return /^(操作|类型|变更类型|改动类型|变更|operation|type|action|op)$/i.test(c)
|
|
119
|
+
}
|
|
120
|
+
|
|
84
121
|
function isPlaceholder(p) {
|
|
85
122
|
return !p || p === '—' || p === '-' || /^(n\/?a|无|none|-+)$/i.test(p)
|
|
86
123
|
}
|
|
@@ -103,23 +140,33 @@ function looksLikePath(p) {
|
|
|
103
140
|
}
|
|
104
141
|
|
|
105
142
|
/**
|
|
106
|
-
*
|
|
143
|
+
* 「顺带修复」标记:design §6 清单里标注的、不属本次变更 task 边界但合规修的预存债文件
|
|
144
|
+
* (CLAUDE.md 规则20 鼓励)。assess 的 allowed_paths 校验对 incidental 文件豁免(坑
|
|
145
|
+
* worktree-execute-apply-friction 坑1/4)。标记载体:表格「说明」列写「顺带修复:xxx」,
|
|
146
|
+
* 或路径 cell / 列表项的括号注释「(顺带修复)」。
|
|
147
|
+
*/
|
|
148
|
+
const INCIDENTAL_RE = /顺带修复|附带修复|顺带|drive-?by|incidental/i
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 从 design.md 解析文件变更清单(含 incidental 标记)。兼容两种真实写法:
|
|
107
152
|
* ① 表格:`| 操作 | 文件路径 | 说明 |`(brainstorm 模板默认)
|
|
108
153
|
* ② 分类列表:`### 新增文件` / `### 修改文件` / `### 不修改文件` 下的 `- path`
|
|
109
154
|
* 表头列顺序自适应(定位「文件/路径/file/path」列,列顺序写反时不会把操作名当路径);
|
|
110
155
|
* 忽略 `.sillyspec/` 与占位符(`—`/`-`/`N/A`/`无`);「不修改/暂缓」子段下的路径会被排除;
|
|
111
|
-
* CRLF 容错。
|
|
156
|
+
* CRLF 容错。incidental 嗅探:表格非路径列(说明列)+ 路径 cell 原始值(剥注释前的括号内容)+ 列表项原文。
|
|
157
|
+
*
|
|
158
|
+
* 内核函数:parseFileChangeList(Set 包装,向后兼容)与 parseFileChangeListDetailed 共用,
|
|
159
|
+
* 单一真相源,避免两处各自重写清单解析漂移。
|
|
112
160
|
* @param {string} designMdPath - design.md 文件路径
|
|
113
|
-
* @returns {
|
|
161
|
+
* @returns {Array<{ path: string, operation: string|null, incidental: boolean }>}(顺序按首次出现,exclude 子段移除)
|
|
114
162
|
*/
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (!designMdPath || !existsSync(designMdPath)) return result
|
|
163
|
+
function _parseFileListDetailed(designMdPath) {
|
|
164
|
+
if (!designMdPath || !existsSync(designMdPath)) return []
|
|
118
165
|
|
|
119
166
|
const content = readFileSync(designMdPath, 'utf8').replace(/\r\n/g, '\n')
|
|
120
167
|
|
|
121
168
|
const sectionMatch = content.match(FILE_LIST_SECTION_RE)
|
|
122
|
-
if (!sectionMatch) return
|
|
169
|
+
if (!sectionMatch) return []
|
|
123
170
|
|
|
124
171
|
// 从标题后开始,截取到下一个 ## 标题或文件末尾
|
|
125
172
|
const afterSection = content.slice(sectionMatch.index + sectionMatch[0].length)
|
|
@@ -131,13 +178,24 @@ export function parseFileChangeList(designMdPath) {
|
|
|
131
178
|
const lines = relevantContent.split('\n')
|
|
132
179
|
let headerSkipped = false
|
|
133
180
|
let pathColIdx = 1 // 默认第 2 列;解析表头后可重定位
|
|
181
|
+
let opColIdx = -1 // 操作列下标(表头扫描后定位;-1 = 无操作列 → operation=null)
|
|
134
182
|
let listMode = 'include' // include | exclude(分类列表子段)
|
|
183
|
+
let currentOp = null // 分类列表子标题推导的 operation(表格模式不用)
|
|
184
|
+
const entries = new Map() // path -> { path, operation, incidental }(exclude 子段 delete)
|
|
135
185
|
|
|
136
186
|
for (const line of lines) {
|
|
137
187
|
// 分类列表子标题:### 新增文件 / ### 修改文件 / ### 不修改文件
|
|
138
188
|
const subHeader = line.match(/^###\s+(.+?)\s*$/)
|
|
139
189
|
if (subHeader) {
|
|
140
|
-
|
|
190
|
+
// ⚠️ EXCLUDE 优先:「### 不修改文件」含「修改」二字,会被 classifySubsectionOp 误匹配。
|
|
191
|
+
// 先判 exclude 词集,命中则 exclude 且 currentOp=null,绝不跑 OP 映射。
|
|
192
|
+
if (EXCLUDE_SUBSECTION_RE.test(subHeader[1])) {
|
|
193
|
+
listMode = 'exclude'
|
|
194
|
+
currentOp = null
|
|
195
|
+
} else {
|
|
196
|
+
listMode = 'include'
|
|
197
|
+
currentOp = classifySubsectionOp(subHeader[1])
|
|
198
|
+
}
|
|
141
199
|
continue
|
|
142
200
|
}
|
|
143
201
|
|
|
@@ -152,6 +210,9 @@ export function parseFileChangeList(designMdPath) {
|
|
|
152
210
|
// 表头列定位:找「文件/路径/file/path」列,找不到保持默认第 2 列
|
|
153
211
|
const idx = cells.findIndex(isPathHeaderCell)
|
|
154
212
|
if (idx >= 0) pathColIdx = idx
|
|
213
|
+
// 操作列定位:非路径列里找操作类列名(「操作/类型/operation」),找不到 opColIdx 保持 -1
|
|
214
|
+
const opIdx = cells.findIndex((c, i) => i !== pathColIdx && isOperationHeaderCell(c))
|
|
215
|
+
if (opIdx >= 0) opColIdx = opIdx
|
|
155
216
|
continue
|
|
156
217
|
}
|
|
157
218
|
|
|
@@ -160,7 +221,13 @@ export function parseFileChangeList(designMdPath) {
|
|
|
160
221
|
if (/^(新增|修改|删除|重命名|new|modify|update|delete|create|rename)$/i.test(filePath)) continue
|
|
161
222
|
if (isPlaceholder(filePath) || filePath.startsWith('.sillyspec/')) continue
|
|
162
223
|
if (!looksLikePath(filePath)) continue // 脏描述兜底:丢弃非路径自由文本(防虚高 fileCount)
|
|
163
|
-
|
|
224
|
+
// operation:操作列 cell 分类(无操作列 → null)
|
|
225
|
+
const operation = opColIdx >= 0 ? classifyOperation(cells[opColIdx] || '') : null
|
|
226
|
+
// incidental:说明列(非路径列的所有 cell)+ 路径 cell 原始值(剥注释前的括号内容)
|
|
227
|
+
const incidental = cells.some((c, i) => i !== pathColIdx && INCIDENTAL_RE.test(c))
|
|
228
|
+
|| INCIDENTAL_RE.test(cells[pathColIdx] || '')
|
|
229
|
+
if (listMode === 'exclude') { entries.delete(filePath); continue }
|
|
230
|
+
entries.set(filePath, { path: filePath, operation, incidental })
|
|
164
231
|
continue
|
|
165
232
|
}
|
|
166
233
|
|
|
@@ -170,10 +237,31 @@ export function parseFileChangeList(designMdPath) {
|
|
|
170
237
|
const filePath = normalizePath(listItem[1])
|
|
171
238
|
if (isPlaceholder(filePath) || filePath.startsWith('.sillyspec/')) continue
|
|
172
239
|
if (!looksLikePath(filePath)) continue // 脏描述兜底
|
|
173
|
-
|
|
174
|
-
|
|
240
|
+
const incidental = INCIDENTAL_RE.test(listItem[1])
|
|
241
|
+
if (listMode === 'exclude') { entries.delete(filePath); continue }
|
|
242
|
+
entries.set(filePath, { path: filePath, operation: currentOp, incidental })
|
|
175
243
|
}
|
|
176
244
|
}
|
|
177
245
|
|
|
178
|
-
return
|
|
246
|
+
return [...entries.values()]
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* 从 design.md 解析文件变更清单(路径集合,向后兼容)。
|
|
251
|
+
* @param {string} designMdPath - design.md 文件路径
|
|
252
|
+
* @returns {Set<string>} 文件路径集合(相对路径,如 "src/worktree.js")
|
|
253
|
+
*/
|
|
254
|
+
export function parseFileChangeList(designMdPath) {
|
|
255
|
+
return new Set(_parseFileListDetailed(designMdPath).map(e => e.path))
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* 从 design.md 解析文件变更清单(含 operation + incidental 标记)。
|
|
260
|
+
* operation 供 verify 删除探针对账(声明「新增/修改」却整文件删除 = 高风险);
|
|
261
|
+
* incidental 供 assess allowed_paths 豁免。
|
|
262
|
+
* @param {string} designMdPath - design.md 文件路径
|
|
263
|
+
* @returns {Array<{ path: string, operation: string|null, incidental: boolean }>}
|
|
264
|
+
*/
|
|
265
|
+
export function parseFileChangeListDetailed(designMdPath) {
|
|
266
|
+
return _parseFileListDetailed(designMdPath)
|
|
179
267
|
}
|
package/src/constants.js
CHANGED
|
@@ -81,6 +81,17 @@ export const SCAN_REQUIRED_DOCS = Object.freeze([
|
|
|
81
81
|
'PROJECT.md',
|
|
82
82
|
])
|
|
83
83
|
|
|
84
|
+
// ── scan quick profile 必需文档子集(4 份核心文档)──
|
|
85
|
+
// quick 档(--quick 显式 或 小项目自动判定)只生成这 4 份;scan-postcheck 按 profile 取清单:
|
|
86
|
+
// mode==='quick' → SCAN_REQUIRED_DOCS_QUICK,否则 → SCAN_REQUIRED_DOCS(7 份)。
|
|
87
|
+
// 由 scan-postcheck.js + complete-handlers.js 透传的 scanProfile.mode 决定取哪份。
|
|
88
|
+
export const SCAN_REQUIRED_DOCS_QUICK = Object.freeze([
|
|
89
|
+
'PROJECT.md',
|
|
90
|
+
'ARCHITECTURE.md',
|
|
91
|
+
'CONVENTIONS.md',
|
|
92
|
+
'STRUCTURE.md',
|
|
93
|
+
])
|
|
94
|
+
|
|
84
95
|
// ── 辅助 stage 清单(stage-contract 转移规则 与 stages/index 注册表 共用单一真相源)──
|
|
85
96
|
// 曾在 stage-contract.js 与 stages/index.js 逐字重复定义,新增/调整辅助 stage 需同步改两处
|
|
86
97
|
// 易分叉。现收敛一处。(注意:主流程序 STAGE_ORDER/mainFlowStages/flowStages 语义不同、
|
package/src/index.js
CHANGED
|
@@ -54,7 +54,7 @@ SillySpec CLI — 规范驱动开发工具包
|
|
|
54
54
|
--allow-new 允许新增文件(默认禁止)
|
|
55
55
|
--force-baseline 允许覆盖 baseline 受保护文件
|
|
56
56
|
--confirm 完成时确认接受变更审计
|
|
57
|
-
scan: --
|
|
57
|
+
scan: --quick | --standard | --deep 显式选择 profile(优先于规模自动判定;三档互斥)
|
|
58
58
|
--force-rescan 覆盖已有 scan 文档保护
|
|
59
59
|
archive: --confirm 归档确认(必须)
|
|
60
60
|
auto: --mode <模式> 显式指定流程模式
|
package/src/run/command.js
CHANGED
|
@@ -23,7 +23,7 @@ import { join, resolve, dirname } from 'node:path'
|
|
|
23
23
|
import { existsSync, readdirSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'node:fs'
|
|
24
24
|
import { randomBytes, randomUUID } from 'node:crypto'
|
|
25
25
|
import { writeAtomicSync } from '../fs-atomic.js'
|
|
26
|
-
import { resolveSpecDir, resolveChangeDir, triggerSync, getStageSteps, formatWaitOptions, checkApproval, didYouMean, assertSafeChangeName } from './shared.js'
|
|
26
|
+
import { resolveSpecDir, countAncestorSpecDirs, resolveChangeDir, triggerSync, getStageSteps, formatWaitOptions, checkApproval, didYouMean, assertSafeChangeName, detectQuickSessionDrift, detectWorktreeSpecDrift } from './shared.js'
|
|
27
27
|
import { resolveQuickLinkedChanges } from './quick-audit.js'
|
|
28
28
|
import { outputStep } from './prompt.js'
|
|
29
29
|
import { completeStep, skipStep, waitStep, continueStep } from './complete.js'
|
|
@@ -195,6 +195,20 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
195
195
|
console.error(' 这些动作互斥,请只保留其中一个再重试。')
|
|
196
196
|
process.exit(2)
|
|
197
197
|
}
|
|
198
|
+
// scan profile flag 互斥:--quick/--standard/--deep 同时给≥2 个语义矛盾(computeScanProfile
|
|
199
|
+
// 只取首个命中、其余静默忽略 → agent 以为生效的其实没生效,与 STEP_ACTIONS 同类风险)。
|
|
200
|
+
const PROFILE_FLAGS = [
|
|
201
|
+
['--quick', flags.includes('--quick'), 'quick profile(快速接入,4 份核心文档)'],
|
|
202
|
+
['--standard', flags.includes('--standard'), 'standard profile(压缩步骤)'],
|
|
203
|
+
['--deep', flags.includes('--deep'), 'deep profile(完整扫描)'],
|
|
204
|
+
]
|
|
205
|
+
const activeProfiles = PROFILE_FLAGS.filter(([, v]) => v)
|
|
206
|
+
if (activeProfiles.length >= 2) {
|
|
207
|
+
console.error('❌ scan profile 参数冲突(同时只能指定一个):')
|
|
208
|
+
for (const [name, , desc] of activeProfiles) console.error(` ${name} — ${desc}`)
|
|
209
|
+
console.error(' 这三档互斥,请只保留其中一个再重试。')
|
|
210
|
+
process.exit(2)
|
|
211
|
+
}
|
|
198
212
|
// 注:--non-interactive 与 --interactive 的冲突在 index.js 检测(--interactive 被 index.js
|
|
199
213
|
// 作为全局 flag 吞掉、不透传到此处,故 isInteractive 在此恒为 false)。
|
|
200
214
|
const waitReason = getFlagValue('--reason')
|
|
@@ -279,6 +293,17 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
279
293
|
// runCommand 后续所有 .sillyspec/ 操作必须用 specBase
|
|
280
294
|
const specBase = platformOpts.specRoot || join(cwd, '.sillyspec')
|
|
281
295
|
|
|
296
|
+
// 漂移提醒:cwd 祖先链 ≥2 个 .sillyspec = monorepo 多实例,当前命中的「最近」实例
|
|
297
|
+
// 可能不是用户意图的项目(如 cd 进被独立 scan 的子项目跑测试后忘回根)。
|
|
298
|
+
// 平台模式 / 显式 --spec-dir 跳过(已明确指定,无歧义)。仅提醒不阻断。
|
|
299
|
+
if (!platformOpts.specRoot && !specDir) {
|
|
300
|
+
const ancestorCount = countAncestorSpecDirs(cwd)
|
|
301
|
+
if (ancestorCount >= 2) {
|
|
302
|
+
console.warn(`⚠️ 检测到祖先链有 ${ancestorCount} 个 .sillyspec 实例(monorepo 多实例),当前使用: ${specBase}`)
|
|
303
|
+
console.warn(` 若意图是另一个项目:cd 回该项目根,或用 --spec-dir <根>/.sillyspec 显式指定。`)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
282
307
|
// 平台模式:首次接入时清理旧版本残留的 cwd/.sillyspec/(防止源码污染)。
|
|
283
308
|
// ⚠️ 同 init.js:必须保护真实资产(changes/、projects/、sillyspec.db)。
|
|
284
309
|
// 只在「首次」执行一次——用 cwd 下的 .sillyspec-platform-cleaned 标记文件记录已处理,
|
|
@@ -437,7 +462,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
437
462
|
'--files', '--allow-new', '--force-baseline', '--force-rescan',
|
|
438
463
|
'--json', '--dir', '--help',
|
|
439
464
|
'--reopen', '--from-step', '--mode',
|
|
440
|
-
'--deep', //
|
|
465
|
+
'--deep', '--quick', '--standard', // scan profile 三档显式选择(scan-profile.js 从 argv 读;互斥见下方 PROFILE_FLAGS 检测)
|
|
441
466
|
])
|
|
442
467
|
for (let i = 0; i < flags.length; i++) {
|
|
443
468
|
const f = flags[i]
|
|
@@ -497,6 +522,21 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
497
522
|
process.exit(2) // 用法错(execute 必须显式 --change)→ exit 2
|
|
498
523
|
}
|
|
499
524
|
|
|
525
|
+
// worktree 副本漂移硬守卫(坑 worktree-execute-spec-drift):specBase 命中 worktree 内 .sillyspec
|
|
526
|
+
// checkout 副本 → 进度/产出会写分裂库。必须在 validateChangeExists 之前——副本里 change 目录真实
|
|
527
|
+
// 存在,存在性校验会被骗放行。覆盖 plan/execute/verify/archive(validateChangeExists 同 Set):
|
|
528
|
+
// 都要求 change 已存在、都被副本骗、都写 .sillyspec 资产(archive 尤甚:在副本里移动 change+commit)。
|
|
529
|
+
// 平台模式/显式 --spec-dir 跳过(已明确指定,与 line~285 warn / ~529 quick 守卫条件对齐)。
|
|
530
|
+
if (!platformOpts.specRoot && !specDir
|
|
531
|
+
&& ['plan', 'execute', 'verify', 'archive'].includes(stageName)) {
|
|
532
|
+
const wt = detectWorktreeSpecDrift(specBase)
|
|
533
|
+
if (wt) {
|
|
534
|
+
console.error(`❌ ${wt.message}`)
|
|
535
|
+
console.error(` 当前 cwd=${cwd},命中的 spec=${specBase}。`)
|
|
536
|
+
process.exit(2) // 环境错(cwd/spec 漂移)→ exit 2
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
500
540
|
// --change 变更名存在性校验(治 cwd 漂移误匹配,缺陷 execute-in-place-windows-pitfalls 坑5):
|
|
501
541
|
// 必须在 pm.read/initChange 之前——initChange 会为 --change 指定的新名字创建 changes/ 目录,
|
|
502
542
|
// 校验若在其后则目录已存在、永远通过。用原始 changeName(--change 参数),覆盖 plan/execute/
|
|
@@ -509,6 +549,21 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
509
549
|
process.exit(2) // 环境错(cwd/spec 漂移)→ exit 2
|
|
510
550
|
}
|
|
511
551
|
|
|
552
|
+
// quick 专用 cwd 漂移 fail-fast 守卫(坑 quick-cwd-drift-splits-specdir):
|
|
553
|
+
// quick 被 validateChangeExists 的 sessionId 豁免(quick-<8hex> 不在 changes/ 下),漂移时除上方
|
|
554
|
+
// countAncestorSpecDirs 的 warn 外无硬守卫 → 无声分裂(progress/artifact/QUICKLOG 落子项目、根
|
|
555
|
+
// 会话停滞)。这里补:当前 specBase 无本 session guard、但祖先链别处有 = 跨 specDir 漂移 → fail-fast。
|
|
556
|
+
// 平台模式/显式 --spec-dir 跳过(specBase 已明确,与 line~285 warn 条件对齐)。
|
|
557
|
+
if (stageName === 'quick' && changeName && /^quick-[0-9a-f]{8}$/.test(changeName)
|
|
558
|
+
&& !platformOpts.specRoot && !specDir) {
|
|
559
|
+
const drift = detectQuickSessionDrift(cwd, specBase, changeName)
|
|
560
|
+
if (drift) {
|
|
561
|
+
console.error(`❌ ${drift.message}`)
|
|
562
|
+
console.error(` 当前 cwd=${cwd},命中的 spec=${specBase}。`)
|
|
563
|
+
process.exit(2) // 环境错(cwd/spec 漂移)→ exit 2
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
512
567
|
let progress = await pm.read(cwd, changeName)
|
|
513
568
|
|
|
514
569
|
if (!progress) {
|
|
@@ -769,6 +769,9 @@ export async function handleScanStageCompleted({ stageName, currentIdx, cwd, pro
|
|
|
769
769
|
source_commit_error: sourceCommit === null ? (sourceCommitError || 'unknown') : undefined,
|
|
770
770
|
generated_at: new Date().toISOString(),
|
|
771
771
|
schema_version: 1,
|
|
772
|
+
scan_profile: stageData.scanProfile
|
|
773
|
+
? { mode: stageData.scanProfile.mode, reason: stageData.scanProfile.reason }
|
|
774
|
+
: null,
|
|
772
775
|
postcheck_result_path: null,
|
|
773
776
|
workflow_runs_dir: platformOpts.runtimeRoot
|
|
774
777
|
? join(platformOpts.runtimeRoot, 'scan-runs', platformOpts.scanRunId || 'unknown', 'workflow-runs')
|
|
@@ -797,6 +800,7 @@ export async function handleScanStageCompleted({ stageName, currentIdx, cwd, pro
|
|
|
797
800
|
projectListParsed: stageData.scanMeta?.projectListParsed ?? null,
|
|
798
801
|
manifestWritten: stageData.scanMeta?.manifestWritten ?? null,
|
|
799
802
|
},
|
|
803
|
+
scanProfile: stageData.scanProfile || null,
|
|
800
804
|
})
|
|
801
805
|
printScanPostCheckResult(postResult)
|
|
802
806
|
|
|
@@ -871,7 +875,7 @@ export async function handleScanStageCompleted({ stageName, currentIdx, cwd, pro
|
|
|
871
875
|
// 非 platform 模式 scan 也做轻量 post-check + 结构化输出
|
|
872
876
|
if (stageName === 'scan' && !platformOpts.specRoot && !platformOpts.runtimeRoot) {
|
|
873
877
|
const { runScanPostCheck, printScanPostCheckResult, formatStructuredResult, writeStructuredResult } = await import('../scan-postcheck.js')
|
|
874
|
-
const postResult = runScanPostCheck({ cwd, specDir: null, outputText })
|
|
878
|
+
const postResult = runScanPostCheck({ cwd, specDir: null, outputText, scanProfile: stageData.scanProfile || null })
|
|
875
879
|
printScanPostCheckResult(postResult)
|
|
876
880
|
// 结构化结果写入 .sillyspec/.runtime/
|
|
877
881
|
const structured = formatStructuredResult(postResult, { source_root: cwd })
|
package/src/run/complete.js
CHANGED
|
@@ -18,7 +18,7 @@ import { join } from 'node:path'
|
|
|
18
18
|
import { existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync, appendFileSync, statSync } from 'node:fs'
|
|
19
19
|
import { triggerSync, WAIT_MARKER_RE, getStageSteps, formatWaitOptions } from './shared.js'
|
|
20
20
|
import { outputStep } from './prompt.js'
|
|
21
|
-
import { enforceDepsGate, runStageCompletionGates } from './gates.js'
|
|
21
|
+
import { enforceDepsGate, enforceReviewJsonGate, runStageCompletionGates } from './gates.js'
|
|
22
22
|
import { handleArchiveConfirmStep, handlePlanGeneratePlanStep, handleScanProjectListStep, handleWorkflowPostCheck, handleQuickStageCompletion, handleExecuteWaveArtifact, handleExecuteWorktreeCleanup, handleScanStageCompleted } from './complete-handlers.js'
|
|
23
23
|
import { stageRegistry } from '../stages/index.js'
|
|
24
24
|
import { formatExecuteSummary } from '../worktree-apply.js'
|
|
@@ -174,6 +174,9 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
174
174
|
|
|
175
175
|
// ── execute deps 验证硬门(change 2026-06-28-worktree-deps-provision)──
|
|
176
176
|
await enforceDepsGate(stageName, cwd, changeName, steps[currentIdx], steps, currentIdx, specBase, platformOpts)
|
|
177
|
+
// review.json 字段硬门(坑 review-json-field-gap):已勾 [x] task 的 review.json 必须 schema 完整,
|
|
178
|
+
// 提前到每次 --done 校验(而非等 Task Review Gate 在整阶段收尾才暴露,迫使用户事后批量补)。
|
|
179
|
+
await enforceReviewJsonGate(stageName, cwd, changeName, steps[currentIdx], steps, currentIdx, specBase, platformOpts)
|
|
177
180
|
|
|
178
181
|
steps[currentIdx].status = 'completed'
|
|
179
182
|
steps[currentIdx].completedAt = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
@@ -226,6 +229,22 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
226
229
|
if (_bf.batched && _bf.aligned > 0) {
|
|
227
230
|
console.log(`\n🚀 execute 批量完成:plan 全勾 + 代码核验通过,一次性补完 ${_bf.aligned} 个剩余 step → 进入阶段完成分支`)
|
|
228
231
|
}
|
|
232
|
+
|
|
233
|
+
// per-task review 草稿兜底(坑 worktree-execute-apply-friction 坑2):主 agent 直接实现模式
|
|
234
|
+
// 无子代理 review 落盘 → Task Review Gate 报缺 review.json 阻断。每次 execute --done 跑(幂等,
|
|
235
|
+
// 已存在跳过),据 git diff base..head 按 allowed_paths 归属生成 cannot_verify 草稿,进 gate 前就位。
|
|
236
|
+
try {
|
|
237
|
+
const { generateTaskReviewDrafts } = await import('../task-review.js')
|
|
238
|
+
const _drafts = await generateTaskReviewDrafts({ changeName, cwd, platformOpts })
|
|
239
|
+
if (_drafts.generated > 0) {
|
|
240
|
+
console.log(' 📄 自动补写 ' + _drafts.generated + ' 个 per-task review.json 草稿(cannot_verify,主 agent 实现模式兜底,需 agent 复核后升级为 pass/fail)')
|
|
241
|
+
if (_drafts.unattributed && _drafts.unattributed.length > 0) {
|
|
242
|
+
console.warn(' ⚠️ ' + _drafts.unattributed.length + ' 个变更文件未归属任何 task(顺带修复/非源码),草稿未覆盖:' + _drafts.unattributed.join(', '))
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
} catch (e) {
|
|
246
|
+
console.warn(' ⚠️ per-task review 草稿生成失败(不阻断 execute 完成):' + (e && e.message ? e.message : e))
|
|
247
|
+
}
|
|
229
248
|
}
|
|
230
249
|
|
|
231
250
|
const nextPendingIdx = steps.findIndex(s => s.status === 'pending' || s.status === 'in-progress')
|
package/src/run/gates.js
CHANGED
|
@@ -96,6 +96,39 @@ export async function enforceDepsGate(stageName, cwd, changeName, step, steps, c
|
|
|
96
96
|
}
|
|
97
97
|
process.exit(1)
|
|
98
98
|
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* execute --done 硬门:已勾 [x] task 的 review.json 必须 schema 完整(坑 review-json-field-gap)。
|
|
102
|
+
*
|
|
103
|
+
* Task Review Gate(validateTaskReviews)只在 execute 整阶段完成时跑(complete.js 阶段完成分支的
|
|
104
|
+
* actualCompleted===actualTotal 守卫),单 task --done 不校验 → 子代理勾 checkbox 却漏写/漏字段
|
|
105
|
+
* review.json,要到收尾才暴露,用户被迫事后批量补。本门提前到每次 --done:校验 plan 里所有已勾
|
|
106
|
+
* task 的 review.json,缺字段/不存在/JSON 坏 → 置 step=blocked + exit(1),与 enforceDepsGate 同范式。
|
|
107
|
+
* 未勾 task 不校验(还没做)。平台模式/无 marker/无 plan 时放行(下游 Task Review Gate 兜底)。
|
|
108
|
+
*/
|
|
109
|
+
export async function enforceReviewJsonGate(stageName, cwd, changeName, step, steps, currentIdx, specBase, platformOpts) {
|
|
110
|
+
if (stageName !== 'execute' || !changeName) return true
|
|
111
|
+
const runtimeRoot = platformOpts?.runtimeRoot || join(specBase, '.runtime')
|
|
112
|
+
const runIdFile = join(runtimeRoot, `current-execute-run-id-${changeName}`)
|
|
113
|
+
const planPath = join(specBase, 'changes', changeName, 'plan.md')
|
|
114
|
+
if (!existsSync(runIdFile) || !existsSync(planPath)) return true
|
|
115
|
+
const executeRunId = readFileSync(runIdFile, 'utf8').trim()
|
|
116
|
+
const planContent = readFileSync(planPath, 'utf8')
|
|
117
|
+
const { validateCheckedTaskReviews } = await import('../task-review.js')
|
|
118
|
+
const result = validateCheckedTaskReviews({ planContent, runtimeRoot, executeRunId })
|
|
119
|
+
if (result.ok) return true
|
|
120
|
+
if (steps && steps[currentIdx]) steps[currentIdx].status = 'blocked'
|
|
121
|
+
console.error('❌ ── review.json 字段校验阻断(本次 --done 未完成,进度未推进)──')
|
|
122
|
+
console.error(' 已勾选 [x] 的 task review.json 不完整(铁律:勾 checkbox 前必须先写完整 review.json):')
|
|
123
|
+
for (const f of result.failures) {
|
|
124
|
+
const kindLabel = f.kind === 'missing' ? 'review.json 不存在' : (f.kind === 'parseError' ? 'JSON 解析失败' : '字段缺失')
|
|
125
|
+
console.error(` • ${f.taskId}(${kindLabel}): ${f.reviewPath}`)
|
|
126
|
+
for (const e of f.errors) console.error(` - ${e}`)
|
|
127
|
+
}
|
|
128
|
+
console.error(' 修复:补全上述 review.json 字段后重跑 execute --done。')
|
|
129
|
+
process.exit(1)
|
|
130
|
+
}
|
|
131
|
+
|
|
99
132
|
/**
|
|
100
133
|
* 阶段完成校验失败时回滚状态。
|
|
101
134
|
*
|
|
@@ -183,6 +216,10 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
183
216
|
const { runVerifyParityCheck, printVerifyParityCheck } = await import('../verify-postcheck.js')
|
|
184
217
|
const parityCheck = runVerifyParityCheck({ cwd, specBase, changeName, runtimeRoot: platformOpts?.runtimeRoot })
|
|
185
218
|
printVerifyParityCheck(parityCheck)
|
|
219
|
+
// ── 删除探针(advisory,不阻断):切斯特顿栅栏护栏,用 git 事实对账静默删除代码 ──
|
|
220
|
+
const { runVerifyDeletionCheck, printVerifyDeletionCheck } = await import('../verify-postcheck.js')
|
|
221
|
+
const deletionCheck = runVerifyDeletionCheck({ cwd, specBase, changeName })
|
|
222
|
+
printVerifyDeletionCheck(deletionCheck)
|
|
186
223
|
console.log('\n✅ 验证通过,下一步:sillyspec run archive')
|
|
187
224
|
}
|
|
188
225
|
|
package/src/run/quick-audit.js
CHANGED
|
@@ -34,7 +34,14 @@ export function printQuickAuditReview(review) {
|
|
|
34
34
|
console.warn(` - ${r}`)
|
|
35
35
|
}
|
|
36
36
|
} else {
|
|
37
|
-
|
|
37
|
+
// 区分「本轮新增」(changedFiles,已扣前序 baseline)vs「累计暂存」(stagedTotal,全部未提交)。
|
|
38
|
+
// 仅当存在前序 baseline 残留(累计 > 本轮新增)时才追加括注,避免普通场景冗余。
|
|
39
|
+
const stagedTotal = typeof review.stagedTotal === 'number' ? review.stagedTotal : review.changedFiles.length
|
|
40
|
+
if (stagedTotal > review.changedFiles.length) {
|
|
41
|
+
console.log(`\n✅ quick 变更边界审计 — SAFE (本轮新增变更 ${review.changedFiles.length} 个文件;累计暂存 ${stagedTotal} 个,含前序 baseline ${stagedTotal - review.changedFiles.length} 个未计入本轮)`)
|
|
42
|
+
} else {
|
|
43
|
+
console.log(`\n✅ quick 变更边界审计 — SAFE (本轮新增变更 ${review.changedFiles.length} 个文件)`)
|
|
44
|
+
}
|
|
38
45
|
}
|
|
39
46
|
}
|
|
40
47
|
|
package/src/run/scan-profile.js
CHANGED
|
@@ -15,23 +15,36 @@ import { safeGit } from './shared.js'
|
|
|
15
15
|
/**
|
|
16
16
|
* 估算源码规模(文件数 + 字节数)—— 跨平台 Node 遍历,替代 Unix `find`(原生 Windows 无 find,
|
|
17
17
|
* 原 find 失效会让所有 Windows 项目永远 fallback 到 standard profile)。
|
|
18
|
-
*
|
|
19
|
-
* (超限时 fileCount 已远超 quick/standard 阈值,不影响 profile
|
|
18
|
+
* 扩展名限定源码后缀;skipDirs 排除依赖/构建产物/覆盖率等非源码目录;maxFiles 封顶避免超大仓库遍历过久
|
|
19
|
+
* (超限时 fileCount 已远超 quick/standard 阈值,不影响 profile 判定);maxDepth 兜底防极深嵌套产物。
|
|
20
20
|
*/
|
|
21
|
-
export function estimateSourceSize(cwd, maxFiles = 5000) {
|
|
21
|
+
export function estimateSourceSize(cwd, maxFiles = 5000, maxDepth = 6) {
|
|
22
22
|
const sourceExts = new Set(['.js','.ts','.tsx','.py','.java','.go','.rs','.rb','.php','.c','.cpp','.h','.jsx','.vue','.svelte'])
|
|
23
|
-
|
|
23
|
+
// skipDirs:排除依赖、版本控制、构建产物、覆盖率、临时目录等非源码目录。
|
|
24
|
+
// 原清单只排了 node_modules/.git/dist/build 等,漏了 .next/.nuxt/coverage/.svelte-kit 等构建产物,
|
|
25
|
+
// 导致带构建产物的小项目被产物里的海量 .js 拉高 fileCount/sourceBytes → 误判 deep。
|
|
26
|
+
const skipDirs = new Set([
|
|
27
|
+
'node_modules', '.git', 'dist', 'build', 'out', 'target',
|
|
28
|
+
'__pycache__', '.venv', 'venv',
|
|
29
|
+
'.next', '.nuxt', '.output', '.svelte-kit', '.astro', '.turbo', '.parcel-cache',
|
|
30
|
+
'coverage', '.nyc_output', '.vitest', '.cache',
|
|
31
|
+
'vendor', 'bower_components',
|
|
32
|
+
'dist-types',
|
|
33
|
+
'.sillyspec', '.claude',
|
|
34
|
+
'tmp', 'temp', '.tmp', 'logs',
|
|
35
|
+
])
|
|
24
36
|
let fileCount = 0
|
|
25
37
|
let sourceBytes = 0
|
|
26
|
-
|
|
38
|
+
// stack 存 [dir, depth]:maxDepth 兜底,防极深嵌套的产物/生成目录在 skipDirs 漏网时被全遍历。
|
|
39
|
+
const stack = [[cwd, 0]]
|
|
27
40
|
while (stack.length) {
|
|
28
|
-
const dir = stack.pop()
|
|
41
|
+
const [dir, depth] = stack.pop()
|
|
29
42
|
let entries
|
|
30
43
|
try { entries = readdirSync(dir, { withFileTypes: true }) }
|
|
31
44
|
catch { continue }
|
|
32
45
|
for (const e of entries) {
|
|
33
46
|
if (e.isDirectory()) {
|
|
34
|
-
if (!skipDirs.has(e.name)) stack.push(join(dir, e.name))
|
|
47
|
+
if (!skipDirs.has(e.name) && depth < maxDepth) stack.push([join(dir, e.name), depth + 1])
|
|
35
48
|
} else if (e.isFile() && sourceExts.has(extname(e.name))) {
|
|
36
49
|
fileCount++
|
|
37
50
|
try { sourceBytes += statSync(join(dir, e.name)).size } catch {}
|
|
@@ -49,8 +62,14 @@ export function estimateSourceSize(cwd, maxFiles = 5000) {
|
|
|
49
62
|
* deep: 大项目或 --deep → 完整流程
|
|
50
63
|
*/
|
|
51
64
|
export function computeScanProfile(cwd, platformOpts) {
|
|
52
|
-
//
|
|
65
|
+
// 显式 profile flag 优先于自动判定(三档互斥由 command.js 检测;此处取首个命中即可)
|
|
53
66
|
const flags = process.argv.slice(2)
|
|
67
|
+
if (flags.includes('--quick')) {
|
|
68
|
+
return { mode: 'quick', reason: '用户指定 --quick', maxAgentCalls: 0, maxDocs: 5 }
|
|
69
|
+
}
|
|
70
|
+
if (flags.includes('--standard')) {
|
|
71
|
+
return { mode: 'standard', reason: '用户指定 --standard', maxAgentCalls: 1, maxDocs: 8 }
|
|
72
|
+
}
|
|
54
73
|
if (flags.includes('--deep')) {
|
|
55
74
|
return { mode: 'deep', reason: '用户指定 --deep', maxAgentCalls: 4, maxDocs: 99 }
|
|
56
75
|
}
|
|
@@ -115,7 +134,7 @@ export function applyScanProfileSteps(stageData, profile, cwd, platformOpts) {
|
|
|
115
134
|
status: 'pending',
|
|
116
135
|
prompt: `## Quick Scan — 核心文档生成
|
|
117
136
|
|
|
118
|
-
|
|
137
|
+
当前为 quick profile(小项目自动判定 或 用户显式 --quick),一次性生成核心文档用于快速接入。
|
|
119
138
|
|
|
120
139
|
### 操作
|
|
121
140
|
1. 读取项目结构和关键文件(package.json / pyproject.toml / README / 入口文件)
|
|
@@ -126,7 +145,7 @@ export function applyScanProfileSteps(stageData, profile, cwd, platformOpts) {
|
|
|
126
145
|
- **STRUCTURE.md** — 目录树 + 模块说明
|
|
127
146
|
3. 如发现子项目,注册到 \`{PROJECTS_ROOT}/\` 下
|
|
128
147
|
|
|
129
|
-
|
|
148
|
+
每份文档 frontmatter 必须包含:\`author\`、\`created_at\`、\`scan_depth: quick\`(标记快速接入的浅层版本;后续深度扫描 --deep 会识别此标记并覆盖升级为完整文档)。
|
|
130
149
|
|
|
131
150
|
### ⛔ 硬约束
|
|
132
151
|
- **严禁使用子代理(Agent/Task 工具)。** 所有文档在一个 turn 内完成。
|