sillyspec 3.26.14 → 3.27.1
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-archive/SKILL.md +2 -0
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +1 -1
- package/.claude/skills/sillyspec-commit/SKILL.md +18 -60
- package/.claude/skills/sillyspec-continue/SKILL.md +2 -0
- package/.claude/skills/sillyspec-execute/SKILL.md +10 -4
- package/.claude/skills/sillyspec-explore/SKILL.md +10 -3
- package/.claude/skills/sillyspec-plan/SKILL.md +10 -2
- package/.claude/skills/sillyspec-quick/SKILL.md +2 -2
- package/.claude/skills/sillyspec-resume/SKILL.md +2 -0
- package/.claude/skills/sillyspec-verify/SKILL.md +4 -0
- package/.claude/skills/sillyspec-workspace/SKILL.md +7 -1
- package/README.md +1 -1
- package/package.json +1 -1
- package/packages/dashboard/server/index.js +22 -6
- package/src/change-risk-profile.js +21 -6
- package/src/commit-suggest.js +202 -0
- package/src/config-cat.js +88 -0
- package/src/db.js +14 -3
- package/src/docs-check.js +598 -534
- package/src/doctor-diagnostics.js +102 -1
- package/src/endpoint-extractor.js +21 -2
- package/src/fs-atomic.js +4 -2
- package/src/git-helper.js +73 -5
- package/src/hooks/claude-pre-tool-use.cjs +10 -5
- package/src/hooks/worktree-guard.js +42 -4
- package/src/index.js +557 -12
- package/src/init.js +1 -1
- package/src/local-register.js +98 -0
- package/src/module-impact.js +166 -0
- package/src/module-resolve.js +211 -0
- package/src/modules.js +82 -2
- package/src/progress/change-registry.js +43 -4
- package/src/progress/stage-machine.js +10 -5
- package/src/progress/step-store.js +12 -8
- package/src/progress.js +19 -3
- package/src/quicklog.js +106 -1
- package/src/run/command.js +95 -1
- package/src/run/complete-handlers.js +73 -5
- package/src/run/complete.js +47 -14
- package/src/run/gates.js +144 -17
- package/src/run/multi-repo-context.js +3 -1
- package/src/run/next.js +128 -0
- package/src/run/prompt.js +36 -4
- package/src/run/quick-audit.js +5 -2
- package/src/run/shared.js +48 -20
- package/src/run/stage.js +37 -3
- package/src/scan-postcheck.js +142 -2
- package/src/stage-contract-spec.js +1 -1
- package/src/stage-contract.js +14 -1
- package/src/stage-review.js +15 -6
- package/src/stages/archive.js +4 -2
- package/src/stages/brainstorm.js +1 -0
- package/src/stages/execute.js +275 -44
- package/src/stages/explore.js +11 -2
- package/src/stages/knowledge.js +3 -2
- package/src/stages/quick.js +18 -9
- package/src/stages/scan.js +1 -0
- package/src/stages/verify.js +16 -51
- package/src/sync.js +44 -3
- package/src/task-review.js +273 -11
- package/src/taskcard.js +52 -22
- package/src/verify-postcheck.js +223 -6
- package/src/verify-probes.js +264 -0
- package/src/workspace.js +125 -0
- package/src/worktree-apply.js +43 -11
- package/src/worktree-deps.js +59 -5
- package/src/worktree.js +128 -14
- package/templates/claude-instruction.md +1 -1
- package/templates/prompts/taskcard-rules.md +1 -1
- package/templates/prompts/verify-probes.md +13 -47
package/src/stages/execute.js
CHANGED
|
@@ -7,6 +7,10 @@ import { isPathASupported } from '../dispatch/backends/sillyhub-mcp.js'
|
|
|
7
7
|
import { readMcpConfig } from '../sillyhub-mcp/config.js'
|
|
8
8
|
import { gitQuiet } from '../git-helper.js'
|
|
9
9
|
import { parseRepo } from './plan-postcheck.js'
|
|
10
|
+
// 模块卡分级解析(token 成本优化 P0a)。注意:module-resolve.js 反向 import 本文件的
|
|
11
|
+
// parseTaskRegistry(注册表同口径)——双向引用均为函数声明 + 调用时解引(无模块初始化期
|
|
12
|
+
// 交叉求值),ESM live binding 下安全;两边都是纯读函数,无副作用序问题。
|
|
13
|
+
import { resolveChangeModuleCards } from '../module-resolve.js'
|
|
10
14
|
|
|
11
15
|
/**
|
|
12
16
|
* 任务注册表解析(2026-08-20-task-truth-unify D-001@v1:tasks.md 唯一任务真相)。
|
|
@@ -91,7 +95,26 @@ export function validatePlanForExecute(tasksContent, planContent) {
|
|
|
91
95
|
const waves = parseWavesFromPlan(plan, registry)
|
|
92
96
|
const allTasks = registry
|
|
93
97
|
|
|
94
|
-
// 检查 0.
|
|
98
|
+
// 检查 0.6: plan_level 锚点缺失(2026-08-21 审查 CLI-5):plan_level 是 review tier 的
|
|
99
|
+
// 判定主锚(classifyReviewTier)——漏写时静默退文件数启发式,full 级大变更会被降级成
|
|
100
|
+
// self 自审且无人报错(降级方向比 fail 更危险)。与 gates 读侧同口径:任意行 plan_level:。
|
|
101
|
+
const fmLine = plan.split('\n').find(l => l.trim().startsWith('plan_level:'))
|
|
102
|
+
if (!fmLine) {
|
|
103
|
+
errors.push(
|
|
104
|
+
`plan.md 缺 plan_level 锚点。brainstorm 的规模判定落盘(frontmatter 写 plan_level: none|light|full)是 review 分级的主锚,` +
|
|
105
|
+
`漏写会静默退文件数启发式——full 级变更可能被降级自审。修复:在 plan.md frontmatter 补一行,如 plan_level: full`
|
|
106
|
+
)
|
|
107
|
+
} else {
|
|
108
|
+
const planLevelValue = fmLine.split(':')[1].trim()
|
|
109
|
+
if (!['none', 'light', 'full'].includes(planLevelValue)) {
|
|
110
|
+
errors.push(
|
|
111
|
+
`plan_level 值非法:${JSON.stringify(planLevelValue)}(合法值 none|light|full)。` +
|
|
112
|
+
`非法值在 review 分级中被当作无锚点处理,同样会静默退文件数启发式`
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 检查 0.8: Wave 标题格式不对(W1/波次1 等)→ 引用行不被收容,静默退化隐式单 Wave
|
|
95
118
|
// 全并行——串行意图失效且无提示(原「标题格式不对」诊断的新契约承接)。仅在确有疑似标题
|
|
96
119
|
// 且无任何显式 Wave 被解析时报,正常 Wave 10/带括号标题不误伤。
|
|
97
120
|
const hasExplicitWave = waves.some(w => !w.implicit)
|
|
@@ -102,6 +125,22 @@ export function validatePlanForExecute(tasksContent, planContent) {
|
|
|
102
125
|
`其下的 "- task-XX" 引用行不会被收容——任务将退化为单个隐式 Wave 全并行,串行意图失效`
|
|
103
126
|
)
|
|
104
127
|
}
|
|
128
|
+
// 检查 0.9(坑 wave-heading-undercount,2026-08-21 实证):部分 Wave 被识别 + 部分
|
|
129
|
+
// wave-like 标题漏识别(6 标题解析 5 个)→ 漏掉 Wave 的任务静默落入后续步骤,收尾靠
|
|
130
|
+
// 批量完成兜住才没出事。逐个点名漏网标题,plan --done 即阻断修复。
|
|
131
|
+
if (hasExplicitWave) {
|
|
132
|
+
const strictRe = /^#+\s*Wave\s*\d+/i
|
|
133
|
+
const missed = plan.split('\n')
|
|
134
|
+
.map(l => l.replace(/\r$/, ''))
|
|
135
|
+
.filter(l => waveLikeHeading.test(l) && !strictRe.test(l))
|
|
136
|
+
if (missed.length > 0) {
|
|
137
|
+
errors.push(
|
|
138
|
+
`存在 ${missed.length} 个「疑似 Wave 标题但未被识别」的行(其余 Wave 已正常解析,这 ${missed.length} 个段的任务会漏进后续步骤):` +
|
|
139
|
+
missed.slice(0, 5).map(l => `"${l.trim().slice(0, 50)}"`).join('、') +
|
|
140
|
+
`。必须字面 "## Wave N" 格式(Wave+空格+数字),修正后重跑 plan --done`
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
105
144
|
|
|
106
145
|
// 检查 1: 注册表非空(tasks.md 无 task-XX checkbox → 三类根因诊断)
|
|
107
146
|
if (allTasks.length === 0) {
|
|
@@ -202,18 +241,20 @@ const fixedPrefix = [
|
|
|
202
241
|
4. 读取 local.yaml(构建命令);若 local.yaml 不存在,先 \`sillyspec local detect\` 生成骨架再读取
|
|
203
242
|
5. 加载项目总览 \`.sillyspec/docs/<project>/scan/PROJECT.md\`(如存在)
|
|
204
243
|
|
|
205
|
-
###
|
|
206
|
-
6.
|
|
207
|
-
7.
|
|
208
|
-
8.
|
|
209
|
-
9.
|
|
210
|
-
10. **利用模块索引快速定位源码**:
|
|
244
|
+
### 模块文档加载(细粒度卡优先——CLI 已按 tasks 卡 allowed_paths 级联匹配)
|
|
245
|
+
6. 读取下方注入的 per-task 模块卡表({MODULE_RESOLVE_TABLE})——跨全部 _module-map.yaml 最长前缀匹配,子项目细粒度卡优先于根层大卡
|
|
246
|
+
7. 主代理与子代理**只读表中命中的卡**:细粒度卡整卡可读(体量小、语义近);仅粗粒度大卡命中时**按节读**——只读「契约摘要」「注意事项」「定位」,跳过「变更索引」「人工备注」等历史累积段
|
|
247
|
+
8. 实现代码时遵循模块卡中描述的接口约定、数据流和依赖关系
|
|
248
|
+
9. **利用模块索引快速定位源码**:
|
|
211
249
|
- 用 entrypoints 字段直接找到模块对外 API 的源码位置
|
|
212
250
|
- 用 main_symbols 字段找到核心类/函数的定义位置
|
|
213
251
|
- 子代理优先读模块卡片理解语义,再读 entrypoints/main_symbols 对应的源码
|
|
252
|
+
10. 表查询命令同源可复跑:\`sillyspec modules resolve --change <change-name>\`(任务卡改动后重跑刷新表)
|
|
214
253
|
|
|
215
254
|
### 符号影响面扩展检查
|
|
216
255
|
11. **符号影响面扫描**(Critical — execute 前必做):
|
|
256
|
+
- **重入沿用(省重复消耗)**:若 symbol-impact.md 已存在、结论完整且其文件头「tasks.md 内容指纹」与当前 tasks.md 一致(中断续跑/阶段重开场景),直接复核沿用既有结论,**不重做调用点扫描**
|
|
257
|
+
- **报告骨架勿手写**:先跑 \`sillyspec symbol-impact --change <change-name>\`——CLI 从 tasks.md 生成逐 task \`<!--TODO-->\` 骨架(gate 拦截时也会自动落一份);把每行占位替换为真实结论(**未替换的 TODO 占位会被 gate 拒绝**,骨架不能直接过门)
|
|
217
258
|
- 读取所有 tasks/task-NN.md,提取每个任务涉及的修改文件
|
|
218
259
|
- 对每个修改文件,检查是否涉及以下变更类型:
|
|
219
260
|
- class 构造函数参数变更(新增/删除/修改参数)
|
|
@@ -221,7 +262,7 @@ const fixedPrefix = [
|
|
|
221
262
|
- DTO / 类型定义变更
|
|
222
263
|
- API client 方法签名变更
|
|
223
264
|
- 函数/方法签名变更(参数增删改)
|
|
224
|
-
-
|
|
265
|
+
- 如果涉及上述变更类型,执行调用点搜索(用于补骨架外的调用点证据,不是生成报告的方式):
|
|
225
266
|
\`\`\`bash
|
|
226
267
|
rg "new ClassName\(" src/
|
|
227
268
|
rg "ClassName\(" src/
|
|
@@ -230,7 +271,7 @@ const fixedPrefix = [
|
|
|
230
271
|
\`\`\`
|
|
231
272
|
- 将搜索到的调用点与 plan.md 和 tasks/task-NN.md 的 allowed_paths 对比
|
|
232
273
|
- **发现调用点不在任何 task 的 allowed_paths 中 → 直接阻断 execute**
|
|
233
|
-
- **报告落盘(CLI
|
|
274
|
+
- **报告落盘(CLI 硬校验)**:结论写入 \`{SPEC_ROOT}/changes/<change-name>/symbol-impact.md\`,每个 task 一行结论(task-XX: 变更类型 + 受影响调用点 + 是否在范围内;无签名级变更也要显式写「无签名级变更」)。\`--done\` 时 CLI 校验该文件存在且覆盖 plan.md 全部 task,缺失或不覆盖会阻断完成
|
|
234
275
|
- 如果调用点不在范围内但任务明确写了"不改原因",记录但不阻断
|
|
235
276
|
|
|
236
277
|
### 输出
|
|
@@ -281,7 +322,7 @@ worktree 路径 + 分支名 + 模式
|
|
|
281
322
|
{DOCS_DEBT}
|
|
282
323
|
|
|
283
324
|
如上为 CLI 用 git 事实算出的本变更触及模块文档欠账(无输出=无欠账或归属数据缺失)。
|
|
284
|
-
欠账处理:Wave
|
|
325
|
+
欠账处理:Wave 收尾时顺手同步对应模块卡,不必为此停下。变更索引类条目追加到卡同目录 \`<module>.changelog.md\` sidecar(无则创建;可先跑 \`sillyspec modules split-changelog\` 迁出历史段)——勿把历史条目堆回模块卡正文(卡是子代理的读取税)。
|
|
285
326
|
|
|
286
327
|
### 铁律
|
|
287
328
|
- **不要询问用户确认频率**,默认 wave 模式;用户已明确口头指定时遵从其指定`,
|
|
@@ -309,14 +350,17 @@ tier: {REVIEW_TIER}({REVIEW_TIER_REASON})
|
|
|
309
350
|
2. design.md 整体对照(最终实现拼起来是否仍符合设计意图,而非仅各 task 局部合规)
|
|
310
351
|
3. 组装行为(全量测试/构建/启动通过——单 task 测试全绿 ≠ 组装正确)
|
|
311
352
|
|
|
353
|
+
**产物唯一化(省重复消耗)**:本步逐项对照结论**只落盘一份**——直接写进 review.json 的 \`checklist\` 数组(item=设计要点/FR/决策,note=实现状态 ✅/⚠️/❌ + 偏差说明 + commit 锚点),reviewerNotes 写汇总。**不要**另写独立的 design-check.md 长文(同一份 design×diff 二次消费;2026-08-22 实测该重复一遍 ≈8 分钟全量重读)。
|
|
354
|
+
**gate 重试修复**:review.json 落盘后若后续 gate 报 docHash 失配 / 路径错,跑 \`sillyspec register-stage-review --change <变更名> --stage execute --refresh-hash\` 一键重算修复——**不要重做审查**(重做=同一材料第三遍)。
|
|
355
|
+
|
|
312
356
|
### 操作
|
|
313
|
-
1. 读取 design.md
|
|
357
|
+
1. 读取 design.md(技术方案)——按章节精准读(design-check 对照表可借 tasks 卡 §锚点定位),避免反复整读全文
|
|
314
358
|
2. 逐一对照 design.md 中的设计要点与实际代码实现
|
|
315
359
|
3. 检查接口签名、数据结构、模块划分是否一致
|
|
316
360
|
4. 记录偏差项(偏差 ≠ 错误,可能是合理的实现调整)
|
|
317
361
|
|
|
318
362
|
### 输出
|
|
319
|
-
|
|
363
|
+
review.json 已落盘(checklist=逐项核验表)+ checklist 摘要与偏差说明;无独立长文产物`,
|
|
320
364
|
outputHint: '设计对照检查清单',
|
|
321
365
|
optional: false
|
|
322
366
|
},
|
|
@@ -488,9 +532,14 @@ function parseWavesFromPlan(planContent, registry = []) {
|
|
|
488
532
|
const waves = []
|
|
489
533
|
const regById = new Map(registry.map(t => [t.id, t]))
|
|
490
534
|
let currentWave = null
|
|
535
|
+
// Wave 标题正则(坑 wave-heading-undercount,2026-08-21 实证:plan 6 个 Wave 只解析出 5 个,
|
|
536
|
+
// 末 Wave 任务静默落入「运行测试」验收步靠批量完成兜住)。空格可选("Wave6"/"Wave 6")、
|
|
537
|
+
// 编号后缀任意("## Wave 6(测试)")——解析侧宁可多收(字母后缀重复编号的硬拦仍在
|
|
538
|
+
// validatePlanForExecute),不可静默丢。
|
|
539
|
+
const WAVE_HEADING_RE = /^#+\s*Wave\s*(\d+)/i
|
|
491
540
|
|
|
492
541
|
for (const line of String(planContent || '').split('\n')) {
|
|
493
|
-
const waveMatch = line.match(
|
|
542
|
+
const waveMatch = line.match(WAVE_HEADING_RE)
|
|
494
543
|
if (waveMatch) {
|
|
495
544
|
currentWave = { index: parseInt(waveMatch[1]), tasks: [] }
|
|
496
545
|
waves.push(currentWave)
|
|
@@ -508,6 +557,20 @@ function parseWavesFromPlan(planContent, registry = []) {
|
|
|
508
557
|
}
|
|
509
558
|
}
|
|
510
559
|
|
|
560
|
+
// wave-like 漂移告警(坑 wave-heading-undercount 运行时防线):比解析正则更宽的「像 Wave
|
|
561
|
+
// 标题」形态(波次/W+数字等)若未被收容,说明该 Wave 段的任务会被静默漏进后续步骤——
|
|
562
|
+
// execute 启动即 warn(agent 当场修 plan),而非收尾才发现步骤表缺 Wave。
|
|
563
|
+
try {
|
|
564
|
+
const likeRe = /^#{1,6}\s*(?:wave|w|波次)\s*\d+/i
|
|
565
|
+
const likeHeadings = String(planContent || '').split('\n')
|
|
566
|
+
.map(l => l.replace(/\r$/, ''))
|
|
567
|
+
.filter(l => likeRe.test(l) && !WAVE_HEADING_RE.test(l))
|
|
568
|
+
if (likeHeadings.length > 0) {
|
|
569
|
+
console.warn(`⚠️ plan 存在 ${likeHeadings.length} 个「疑似 Wave 标题但未被识别」的行(其任务会漏进后续步骤):`)
|
|
570
|
+
for (const h of likeHeadings.slice(0, 5)) console.warn(` ${h.trim().slice(0, 60)}(须字面 "## Wave N" 格式)`)
|
|
571
|
+
}
|
|
572
|
+
} catch { /* 告警失败不阻断解析 */ }
|
|
573
|
+
|
|
511
574
|
// 无显式 Wave 结构但注册表非空(light 级:任务全在 tasks.md、plan.md 只留策略)→
|
|
512
575
|
// 合成单隐式 Wave 收容全部任务(与旧 light `## Tasks` 隐式收容语义对齐,单 Wave 串行执行)
|
|
513
576
|
if (waves.length === 0 && registry.length > 0) {
|
|
@@ -558,19 +621,20 @@ function resolveTaskRepo(task, changeDir) {
|
|
|
558
621
|
}
|
|
559
622
|
|
|
560
623
|
/**
|
|
561
|
-
* 把 base_commit
|
|
562
|
-
*
|
|
563
|
-
* 防止同 Wave 多 task 改同一跨仓仓时 HEAD 推进致 diff 范围漂移(design §5.3 约束① / R-01)。
|
|
624
|
+
* 把 commit 锡点(base_commit/head_commit)写入 task 卡 frontmatter(W3 task-08,D-010;
|
|
625
|
+
* head 侧 2026-08-21 审计项③ 补对称)。
|
|
564
626
|
*
|
|
565
|
-
* 策略:幂等写——frontmatter
|
|
566
|
-
*
|
|
627
|
+
* 策略:幂等写——frontmatter 有该字段行则就地替换,无则按 D-010 协议顺序插入:
|
|
628
|
+
* head_commit 跟在 base_commit 行后;base_commit 跟在 repo 行后;均无则插 frontmatter
|
|
629
|
+
* 首部。仅改 frontmatter 不动正文。已存在相同值不重复写。
|
|
567
630
|
*
|
|
568
631
|
* @param {string} taskFilePath - task-NN.md 绝对路径
|
|
569
|
-
* @param {
|
|
632
|
+
* @param {'base_commit'|'head_commit'} field - 锚点字段名
|
|
633
|
+
* @param {string} commit - commit sha
|
|
570
634
|
* @returns {boolean} 是否实际写入(false = 文件不存在 / 写失败 / 值未变)
|
|
571
635
|
*/
|
|
572
|
-
function
|
|
573
|
-
if (!taskFilePath || !
|
|
636
|
+
export function writeCommitAnchorToTaskCard(taskFilePath, field, commit) {
|
|
637
|
+
if (!taskFilePath || !commit || (field !== 'base_commit' && field !== 'head_commit')) return false
|
|
574
638
|
if (!existsSync(taskFilePath)) return false
|
|
575
639
|
let content
|
|
576
640
|
try {
|
|
@@ -579,27 +643,28 @@ function writeBaseCommitToTaskCard(taskFilePath, baseCommit) {
|
|
|
579
643
|
return false
|
|
580
644
|
}
|
|
581
645
|
// CRLF 归一(坑 base-commit-crlf-frontmatter,plan-postcheck.js 同款):跨仓 task 卡被
|
|
582
|
-
// 编辑器/子代理写成 CRLF 后 `^---\n` 匹配失败 →
|
|
646
|
+
// 编辑器/子代理写成 CRLF 后 `^---\n` 匹配失败 → 锚点静默不落盘,而 Wave
|
|
583
647
|
// prompt 仍声称「CLI 已落盘」。归一后按 LF 写回(taskcard 生成侧本就强制 LF)。
|
|
584
648
|
content = content.replace(/\r\n?/g, '\n')
|
|
585
649
|
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/)
|
|
586
650
|
if (!fmMatch) return false
|
|
587
651
|
const fm = fmMatch[1]
|
|
588
|
-
const
|
|
652
|
+
const lineRe = new RegExp(`^${field}:\\s.*$`, 'm')
|
|
589
653
|
let newFm
|
|
590
|
-
if (
|
|
591
|
-
const existing = (fm.match(
|
|
592
|
-
if (existing ===
|
|
593
|
-
newFm = fm.replace(
|
|
654
|
+
if (lineRe.test(fm)) {
|
|
655
|
+
const existing = (fm.match(lineRe) || [''])[0]
|
|
656
|
+
if (existing === `${field}: ${commit}`) return false // 值未变,幂等跳过
|
|
657
|
+
newFm = fm.replace(lineRe, `${field}: ${commit}`)
|
|
594
658
|
} else {
|
|
595
|
-
//
|
|
596
|
-
|
|
597
|
-
const anchor =
|
|
598
|
-
|
|
599
|
-
|
|
659
|
+
// 插入锚点(D-010 协议顺序 repo→base_commit→head_commit):head 跟 base 后,
|
|
660
|
+
// base 跟 repo 后,都无则 frontmatter 首字段
|
|
661
|
+
const anchor = `${field}: ${commit}`
|
|
662
|
+
const prevField = field === 'head_commit' ? 'base_commit' : 'repo'
|
|
663
|
+
const prevRe = new RegExp(`^${prevField}:\\s.*$`, 'm')
|
|
664
|
+
if (prevRe.test(fm)) {
|
|
665
|
+
newFm = fm.replace(prevRe, m => m + '\n' + anchor)
|
|
600
666
|
} else {
|
|
601
|
-
|
|
602
|
-
newFm = anchor.trimEnd() + '\n' + fm
|
|
667
|
+
newFm = anchor + '\n' + fm
|
|
603
668
|
}
|
|
604
669
|
}
|
|
605
670
|
const newContent = content.replace(/^---\n[\s\S]*?\n---/, `---\n${newFm}\n---`)
|
|
@@ -612,6 +677,75 @@ function writeBaseCommitToTaskCard(taskFilePath, baseCommit) {
|
|
|
612
677
|
}
|
|
613
678
|
}
|
|
614
679
|
|
|
680
|
+
function writeBaseCommitToTaskCard(taskFilePath, baseCommit) {
|
|
681
|
+
return writeCommitAnchorToTaskCard(taskFilePath, 'base_commit', baseCommit)
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* 跨仓 task head_commit 锡点自动落盘(2026-08-21 agent-手工产出审计项③,D-010 补对称)。
|
|
686
|
+
*
|
|
687
|
+
* base_commit 已在派发时 CLI 落盘,head_commit 此前靠主 agent 按 Wave prompt 指引手跑
|
|
688
|
+
* `rev-parse` 手写(漏抄/抄错直接炸 Task Review Gate 真实性校验)。本函数在 execute
|
|
689
|
+
* --done 时机补齐另一半:扫描 task 卡,跨仓 task(repo≠main)缺 head_commit 的,实时
|
|
690
|
+
* `git -C <跨仓仓根> rev-parse HEAD` 幂等写入(**已存在不覆盖**——agent 手写的精确锚点优先)。
|
|
691
|
+
*
|
|
692
|
+
* best-effort:MultiRepoContext 构造失败 / git 不可达 → 跳过该 task 记 reason,绝不抛。
|
|
693
|
+
*
|
|
694
|
+
* @param {{ changeName: string, cwd: string, specBase?: string, platformOpts?: object }} opts
|
|
695
|
+
* @returns {Promise<{ stamped: number, stampedTasks: string[], skipped: number, reasons: string[] }>}
|
|
696
|
+
*/
|
|
697
|
+
export async function stampCrossRepoHeadCommits({ changeName, cwd, specBase, platformOpts = {} }) {
|
|
698
|
+
const base = specBase || platformOpts.specRoot || path.join(cwd, '.sillyspec')
|
|
699
|
+
const reasons = []
|
|
700
|
+
const stampedTasks = []
|
|
701
|
+
if (!changeName) {
|
|
702
|
+
return { stamped: 0, stampedTasks, skipped: 0, reasons: ['无 changeName'] }
|
|
703
|
+
}
|
|
704
|
+
const tasksDir = path.join(base, 'changes', changeName, 'tasks')
|
|
705
|
+
if (!existsSync(tasksDir)) {
|
|
706
|
+
return { stamped: 0, stampedTasks, skipped: 0, reasons: ['无 tasks/ 目录(单 task 变更,无跨仓锡点)'] }
|
|
707
|
+
}
|
|
708
|
+
let ctx = null
|
|
709
|
+
try {
|
|
710
|
+
const { getOrCreateMultiRepoContext } = await import('../run/shared.js')
|
|
711
|
+
ctx = await getOrCreateMultiRepoContext({ cwd, changeName, platformOpts })
|
|
712
|
+
} catch (e) {
|
|
713
|
+
return { stamped: 0, stampedTasks, skipped: 0, reasons: [`MultiRepoContext 构造失败: ${e.message}`] }
|
|
714
|
+
}
|
|
715
|
+
let stamped = 0
|
|
716
|
+
let skipped = 0
|
|
717
|
+
for (const f of readdirSync(tasksDir).filter(n => /^task-\d+\.md$/.test(n)).sort()) {
|
|
718
|
+
const taskFile = path.join(tasksDir, f)
|
|
719
|
+
let content
|
|
720
|
+
try {
|
|
721
|
+
content = readFileSync(taskFile, 'utf8')
|
|
722
|
+
} catch {
|
|
723
|
+
skipped++
|
|
724
|
+
continue
|
|
725
|
+
}
|
|
726
|
+
if (/^head_commit:/m.test(content)) continue // 已有(手写或前次落盘)不覆盖
|
|
727
|
+
const repoKey = parseRepo(content)
|
|
728
|
+
if (!repoKey || repoKey === 'main') continue // 主仓 task 锚 meta,无 head_commit 字段
|
|
729
|
+
const entry = ctx && ctx.resolve ? ctx.resolve(repoKey) : null
|
|
730
|
+
if (!entry || !entry.gitDir) {
|
|
731
|
+
skipped++
|
|
732
|
+
reasons.push(`${f}: repo "${repoKey}" 无法解析 gitDir(local.yaml repos 未注册?)`)
|
|
733
|
+
continue
|
|
734
|
+
}
|
|
735
|
+
const head = gitQuiet(entry.gitDir, ['rev-parse', 'HEAD'])
|
|
736
|
+
if (!head) {
|
|
737
|
+
skipped++
|
|
738
|
+
reasons.push(`${f}: git rev-parse HEAD 失败(${entry.gitDir})`)
|
|
739
|
+
continue
|
|
740
|
+
}
|
|
741
|
+
if (writeCommitAnchorToTaskCard(taskFile, 'head_commit', head)) {
|
|
742
|
+
stamped++
|
|
743
|
+
stampedTasks.push(f.replace(/\.md$/, ''))
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
return { stamped, stampedTasks, skipped, reasons }
|
|
747
|
+
}
|
|
748
|
+
|
|
615
749
|
/**
|
|
616
750
|
* 为 Wave 生成 prompt(强制子代理执行)
|
|
617
751
|
*
|
|
@@ -700,6 +834,94 @@ ${prototypes.map(p => `- \`${path.join(protoRelDir, p)}\``).join('\n')}
|
|
|
700
834
|
} catch {}
|
|
701
835
|
}
|
|
702
836
|
|
|
837
|
+
// ── 模块卡分级(P0a):本 Wave per-task 最优卡表,细卡优先——治「根层大卡被每个子代理
|
|
838
|
+
// 整读」(multi-agent-platform backend.md 58KB 实测单卡一项 20 万+ tokens/全流程)。
|
|
839
|
+
// best-effort:解析失败/无 map 不注入(零回归),可经 `sillyspec modules resolve` 手查。
|
|
840
|
+
let moduleSection = ''
|
|
841
|
+
try {
|
|
842
|
+
if (changeDir) {
|
|
843
|
+
const specBase = path.dirname(path.dirname(changeDir))
|
|
844
|
+
// repo root 推导:默认 spec 布局(<repo>/.sillyspec/changes/<change>)从 specBase 反推,
|
|
845
|
+
// 平台模式(specRoot 异位)回退 process.cwd()——子项目目录前缀判定用,误差只降级匹配粒度
|
|
846
|
+
const repoRoot = path.basename(specBase) === '.sillyspec' ? path.dirname(specBase) : process.cwd()
|
|
847
|
+
const waveTaskIds = new Set(wave.tasks.map((t, ti) => `task-${String(t.index || (ti + 1)).padStart(2, '0')}`))
|
|
848
|
+
const { hasMaps, rows } = resolveChangeModuleCards({
|
|
849
|
+
cwd: repoRoot,
|
|
850
|
+
specBase,
|
|
851
|
+
changeName: path.basename(changeDir),
|
|
852
|
+
})
|
|
853
|
+
const hits = rows.filter(r => waveTaskIds.has(r.taskId) && r.moduleId)
|
|
854
|
+
if (hasMaps && hits.length > 0) {
|
|
855
|
+
const kb = n => (n > 0 ? `${(n / 1024).toFixed(1)}KB` : '?')
|
|
856
|
+
const cardLines = hits.map(r =>
|
|
857
|
+
`- ${r.taskId} → \`${r.cardPath}\`(${r.moduleId},${r.granularity === 'fine' ? '细卡' : '粗卡'} ${kb(r.cardBytes)}:${r.advice})`
|
|
858
|
+
).join('\n')
|
|
859
|
+
moduleSection = `
|
|
860
|
+
### 模块卡分级(本 Wave 子代理按表引用,勿按目录漫游 / 整读根层大卡)
|
|
861
|
+
|
|
862
|
+
${cardLines}
|
|
863
|
+
|
|
864
|
+
细粒度卡整卡读;粗粒度大卡只读「契约摘要/注意事项/定位」节(跳过变更索引/人工备注历史段)。为子代理写 prompt 时把对应卡路径直接写入。
|
|
865
|
+
`
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
} catch { /* 模块卡分级是 best-effort:失败不注入不阻断 */ }
|
|
869
|
+
|
|
870
|
+
// ── design.md 热区(P1a):Wave 前置只需「非目标/兼容策略」两节,CLI 提取直供——免每
|
|
871
|
+
// Wave 整读全文(28KB 级 design × 8 Wave 重复读)。无 design.md / 无对应节 → 不注入零回归。
|
|
872
|
+
let designHotzone = ''
|
|
873
|
+
let hasDesignHotzone = false
|
|
874
|
+
try {
|
|
875
|
+
if (changeDir) {
|
|
876
|
+
const designPath = path.join(changeDir, 'design.md')
|
|
877
|
+
if (existsSync(designPath)) {
|
|
878
|
+
const SECTION_CHAR_LIMIT = 2400
|
|
879
|
+
const dLines = readFileSync(designPath, 'utf8').replace(/\r\n/g, '\n').split('\n')
|
|
880
|
+
const sections = []
|
|
881
|
+
let cur = null
|
|
882
|
+
dLines.forEach((l, i) => {
|
|
883
|
+
const m = l.match(/^##\s+(.+)$/)
|
|
884
|
+
if (m) {
|
|
885
|
+
if (cur) cur.end = i
|
|
886
|
+
cur = { title: m[1].trim(), start: i, end: dLines.length }
|
|
887
|
+
sections.push(cur)
|
|
888
|
+
}
|
|
889
|
+
})
|
|
890
|
+
const grab = kwRe => {
|
|
891
|
+
const s = sections.find(s => kwRe.test(s.title))
|
|
892
|
+
if (!s) return null
|
|
893
|
+
const body = dLines.slice(s.start + 1, s.end).join('\n').trim()
|
|
894
|
+
if (!body) return null
|
|
895
|
+
return { title: s.title, body: body.length > SECTION_CHAR_LIMIT ? body.slice(0, SECTION_CHAR_LIMIT) + '\n…(超长截断,原文见 design.md)' : body }
|
|
896
|
+
}
|
|
897
|
+
const nonGoals = grab(/非目标/)
|
|
898
|
+
const compat = grab(/兼容/)
|
|
899
|
+
if (nonGoals || compat) {
|
|
900
|
+
hasDesignHotzone = true
|
|
901
|
+
const fmt = s => (s ? `#### ${s.title}\n${s.body}\n` : '')
|
|
902
|
+
const indexLines = sections.map(s => `- L${s.start + 1} ## ${s.title}`).join('\n')
|
|
903
|
+
designHotzone = `
|
|
904
|
+
### design.md 热区(CLI 提取——Wave 前置只读这两节,勿整读全文)
|
|
905
|
+
|
|
906
|
+
${fmt(nonGoals)}${fmt(compat)}
|
|
907
|
+
### design.md 章节行号索引(需要其余章节时按行号精准读,Read offset/limit)
|
|
908
|
+
|
|
909
|
+
${indexLines}
|
|
910
|
+
`
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
} catch { /* 热区提取 best-effort:失败不注入 */ }
|
|
915
|
+
|
|
916
|
+
// 子代理要点 4 / Wave 开始前第 1 条:模块表 / 热区存在时换成「按注入内容执行」版文案,
|
|
917
|
+
// 否则保留原文(零回归——无 map / 无 design 的项目 prompt 不变)
|
|
918
|
+
const moduleDocPoint = moduleSection
|
|
919
|
+
? '4. **模块卡分级**:把上方「模块卡分级」表中该 task 命中的卡路径写进子代理 prompt(细卡整读;粗大卡只读「契约摘要/注意事项/定位」节)——勿让子代理按目录漫游或整读根层大卡'
|
|
920
|
+
: '4. 如存在模块文档(.sillyspec/docs/*/modules/),按需读取涉及模块的 <module>.md 参考接口约定和数据流'
|
|
921
|
+
const waveStartItem1 = hasDesignHotzone
|
|
922
|
+
? '1. 「非目标」「兼容策略」两节已由 CLI 提取附在上方「design.md 热区」——**不要再整读 design.md**;需要其余章节时按热区下方行号索引精准读取(Read offset/limit)。确保子代理不超范围、不破坏旧逻辑'
|
|
923
|
+
: '1. 读取 design.md 的「非目标」与「兼容策略」章节(如存在),确保子代理不超范围、不破坏旧逻辑'
|
|
924
|
+
|
|
703
925
|
// 构建任务摘要(不再内联完整蓝图,减少上下文污染)
|
|
704
926
|
const taskSummary = wave.tasks.map((t, ti) => {
|
|
705
927
|
const taskNum = String(t.index || (ti + 1)).padStart(2, '0')
|
|
@@ -768,9 +990,9 @@ ${crossLines}
|
|
|
768
990
|
**派发跨仓 task 前(base 锡点,CLI 已在 prompt 构造时落盘):**
|
|
769
991
|
- 跨仓 task 卡 frontmatter 的 \`base_commit\` 已由 CLI 实时 \`git -C <跨仓仓根> rev-parse HEAD\` 锁定(base 锡点,约束①)。子代理在此 HEAD 上改+commit,不受同 Wave 其他 task 推进 HEAD 影响。
|
|
770
992
|
|
|
771
|
-
**回收跨仓 task
|
|
772
|
-
-
|
|
773
|
-
- review.json 的
|
|
993
|
+
**回收跨仓 task(head 锡点,CLI 自动落盘,勿手写):**
|
|
994
|
+
- 子代理完成 commit 后,正常写 review.json(verdict/notes)并勾选 checkbox 即可。execute \`--done\` 时 CLI 自动 \`git -C <跨仓仓根> rev-parse HEAD\` 写入该 task 卡 \`head_commit:\`(幂等,已存在不覆盖——你若手写了精确锚点则以你的为准)。
|
|
995
|
+
- review.json 的 mechanics 字段(\`base\`/\`head\`/\`changedFiles\`)无需手算:\`base\` 取 task 卡 \`base_commit\`、\`head\` 取 task 卡 \`head_commit\`,写完跑 \`sillyspec backfill-reviews --change <变更名> --adopt\` 一键代填(verdict 保留)。
|
|
774
996
|
|
|
775
997
|
**跨仓 task 子代理 prompt 必须注入:**
|
|
776
998
|
> 该 task 改的是 \`<repo>\` 仓,workdir=\`<跨仓仓根>\`。**直接在该仓主干工作区改+commit(git add + git commit 到该仓主干),不经主仓 worktree、不建分支。** commit 到该仓主干即落盘,apply 阶段对跨仓 task 为 no-op(design §5.4 G1)。
|
|
@@ -874,7 +1096,7 @@ ${workdirLines}
|
|
|
874
1096
|
|
|
875
1097
|
${worktreeSection}${crossRepoCommitSection}${dispatchSection}
|
|
876
1098
|
**SillyHub 派发互斥**:SillyHub 派发模式下按派发段执行(一 Wave 一 mission),不按 batch 分组;batch 分组指导仅适用于本地 Agent tool 派发。
|
|
877
|
-
|
|
1099
|
+
${moduleSection}
|
|
878
1100
|
### 任务摘要(按需读取完整蓝图)
|
|
879
1101
|
为每个任务启动子代理时,**只需告知任务目标和蓝图文件路径,让子代理按需读取**:
|
|
880
1102
|
|
|
@@ -884,7 +1106,7 @@ ${taskSummary}
|
|
|
884
1106
|
1. 任务目标(简短描述)
|
|
885
1107
|
2. 蓝图文件路径(让子代理自行读取详情)
|
|
886
1108
|
3. 编码铁律:先读后写、TDD、不编造方法、只做蓝图里写的事、遵守边界处理规则、不超出 allowed_paths
|
|
887
|
-
4.
|
|
1109
|
+
4. ${moduleDocPoint}
|
|
888
1110
|
5. 任务含测试代码时,把下方「测试用例设计」整段复制进子代理 prompt,要求子代理按此设计测试用例
|
|
889
1111
|
6. **增量落盘与中断接手指引**:每完成一个可见产出(代码/测试/文档),立即写盘并执行一次最小验证(如语法检查、单跑相关测试)。工作过程中如被 429/API 配额/会话中断,应在最终回复里输出「已完成清单」(含文件路径、测试命令、当前卡点),不要只输出结论——主代理会依据磁盘产物和该清单判断哪些部分已完成,哪些需接手补做,避免重做已落盘的工作
|
|
890
1112
|
7. **任务边界铁律**:严格只实现本 task 的 \`allowed_paths\` 内文件;若 design.md/plan.md 明确指定了接口/回调/钩子接入位置,必须逐字遵守;不允许顺手实现其他 task 的内容(如 task-01 不要把 task-02 的接入也做了)。如发现必须改其他 task 文件才能继续,先回到主代理由主代理决定是否重分 Wave 或调整 plan,禁止子代理私自越界
|
|
@@ -892,8 +1114,9 @@ ${taskSummary}
|
|
|
892
1114
|
|
|
893
1115
|
{{include: testcase-design}}
|
|
894
1116
|
|
|
1117
|
+
${designHotzone}
|
|
895
1118
|
### Wave 开始前
|
|
896
|
-
|
|
1119
|
+
${waveStartItem1}
|
|
897
1120
|
2. 读取 plan.md 了解全局任务划分和依赖关系
|
|
898
1121
|
3. 确认本 Wave 的输入/输出契约(前置 Wave 产出了什么,本 Wave 需要消费什么)
|
|
899
1122
|
4. 检查前置 Wave 的产出是否完整(文件是否存在、测试是否通过)
|
|
@@ -963,11 +1186,19 @@ task-XX 对应:{SPEC_ROOT}/.runtime/execute-runs/{EXECUTE_RUN_ID}/tasks/task-X
|
|
|
963
1186
|
- 无 _module-map.yaml 时跳过模块匹配,仅按文件清单更新 unmapped 部分
|
|
964
1187
|
这是可选更新(不阻断 execute 完成),但保持 module-impact 与实际变更一致利于 verify 核对与 archive 终审。
|
|
965
1188
|
|
|
1189
|
+
### 知识沉淀(主代理在本 Wave 收尾,与 module-impact.md 同批)
|
|
1190
|
+
本 Wave 各 task 子代理报告里若提到**真正的项目特有坑**(跨变更可复用、未来 agent 可能再次踩到、且不是本任务专属细节),由你在 Wave 收尾统一追加到 \`{SPEC_ROOT}/knowledge/uncategorized.md\`:
|
|
1191
|
+
- 条目格式:\`## <一句话标题>\` + 一段说明(坑的来龙去脉 + 规避/解法),末尾标注来源(task-XX)
|
|
1192
|
+
- 不由各 task 子代理分别写(并行改同一文件会互相覆盖),只由主代理在 Wave 收尾统一追加——与 module-impact.md 同一收尾节奏
|
|
1193
|
+
- 不要为了完成任务而硬凑条目;纯新增/纯样式/单点 bug 修复/临时绕过不写;拿不准时不写,宁缺毋滥
|
|
1194
|
+
- execute 末步「知识库审阅」会检查待确认条目并提示用户归类
|
|
1195
|
+
|
|
966
1196
|
### 完成后
|
|
967
|
-
1. 为每个后端 router task
|
|
968
|
-
-
|
|
969
|
-
-
|
|
970
|
-
|
|
1197
|
+
1. 为每个后端 router task 生成 API 端点 artifact——**用 CLI 命令,勿手扫装饰器手写(易漏 endpoint)**:
|
|
1198
|
+
- 逐 task 运行:sillyspec endpoints extract --change <change> --task <task-NN>(<change> = 当前变更名)
|
|
1199
|
+
- CLI 静态扫描变更文件(FastAPI @router.* / Express router.* / Spring @*Mapping)生成
|
|
1200
|
+
{SPEC_ROOT}/.runtime/contract-artifacts/<task-name>/endpoints.json
|
|
1201
|
+
- 格式: { "task": "task-XX", "type": "backend_endpoints", "endpoints": [{ "method": "GET", "path": "/api/ppm/xxx" }] }(CLI 已按此格式写好,verify 探针 5 直接消费)
|
|
971
1202
|
`
|
|
972
1203
|
}
|
|
973
1204
|
|
package/src/stages/explore.js
CHANGED
|
@@ -18,14 +18,23 @@ export const definition = {
|
|
|
18
18
|
3. 可以用 \`rg\` / \`ls\` / \`cat\` 调查已有结构和集成点
|
|
19
19
|
4. 输出 2-3 个有价值方向、关键风险和下一步建议
|
|
20
20
|
5. 如果用户要求保存结论,先明确保存位置,再写入对应文档
|
|
21
|
+
6. 讨论 UI/交互方案且用户明确要求原型时,按下方「HTML 原型生成」执行
|
|
22
|
+
|
|
23
|
+
### HTML 原型生成(仅用户明确要求时)
|
|
24
|
+
探索保持只读姿态:不主动写原型文件;UI 讨论成形时可以提议("要不要出个原型看效果"),用户同意后再生成。生成要求与 brainstorm 阶段的原型对齐:
|
|
25
|
+
- 单文件 HTML(内联 CSS + JS),浏览器直接打开
|
|
26
|
+
- 高保真呈现讨论中的布局/组件/交互流程——用户看到的是"这个方案做出来长什么样",不是示意图
|
|
27
|
+
- 与项目现有界面风格一致:先读 scan 文档与现有前端代码,复用现有组件库/设计 token 的观感,不另起风格
|
|
28
|
+
- 保存到 \`{SPEC_ROOT}/explore/prototype-<名称>.html\`(目录不存在则先创建;用户指定了其他位置则从其指定)
|
|
29
|
+
- 原型是探索讨论工具、不是交付物,也不是实现代码——不要顺手把原型接进项目源码或构建;话题后续进入 brainstorm 时,把原型文件复制到 \`{SPEC_ROOT}/changes/<变更名>/\` 可直接复用为该变更的原型
|
|
21
30
|
|
|
22
31
|
### 输出
|
|
23
|
-
探索结论、选项对比、风险清单或 ASCII
|
|
32
|
+
探索结论、选项对比、风险清单或 ASCII 图;如生成了原型,附原型文件路径
|
|
24
33
|
|
|
25
34
|
### 铁律
|
|
26
35
|
- 不写实现代码
|
|
27
36
|
- 不安装依赖
|
|
28
|
-
-
|
|
37
|
+
- 不修改文件,除非用户明确要求保存探索结论或生成原型
|
|
29
38
|
- 不强行推进到 brainstorm/plan/execute`,
|
|
30
39
|
outputHint: '探索结论',
|
|
31
40
|
optional: false
|
package/src/stages/knowledge.js
CHANGED
|
@@ -232,11 +232,12 @@ export async function cmdValidate(dir, args, opts = {}) {
|
|
|
232
232
|
}
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
-
// 4. uncategorized.md
|
|
235
|
+
// 4. uncategorized.md 条目数(多了提示需清理)。条目行契约是 `## <qlId> | <标题>`
|
|
236
|
+
//(quick 自动候选与手工条目同格式),h3 形态一并兼容;原 `^###` 只数 h3 恒为 0
|
|
236
237
|
const uncategorizedPath = join(knowledgeDir, 'uncategorized.md')
|
|
237
238
|
if (existsSync(uncategorizedPath)) {
|
|
238
239
|
const content = readFileSync(uncategorizedPath, 'utf8')
|
|
239
|
-
const entryCount = (content.match(
|
|
240
|
+
const entryCount = (content.match(/^#{2,3}\s+\S/gm) || []).length
|
|
240
241
|
if (entryCount >= 10) {
|
|
241
242
|
warnings.push({
|
|
242
243
|
code: 'too_many_uncategorized',
|
package/src/stages/quick.js
CHANGED
|
@@ -81,7 +81,14 @@ step 3 --done 审计会把 src 核心文件(CLI / 状态机 / 注入框架等
|
|
|
81
81
|
- 本步骤是最后一步,完成后 quick 会话即结束(--change 传递见首步 sessionId 说明)
|
|
82
82
|
|
|
83
83
|
### ⚠️ 结果摘要模板(必填,CLI 会校验结构)
|
|
84
|
-
|
|
84
|
+
|
|
85
|
+
🔧 **推荐四参数形式**(CLI 自动合成结构化模板,杜绝嵌套冒号/缺字段/截断规则踩坑):
|
|
86
|
+
|
|
87
|
+
\`\`\`
|
|
88
|
+
sillyspec run quick --done --change <id> --req "一句话语义化短标题(写「改了什么」)" --cause "为什么这样改(纯新增/配置/样式写「无,纯新增/纯样式」)" --solution "怎么改的" --result "验证情况(测试数 / lint / typecheck / 部署状态)"
|
|
89
|
+
\`\`\`
|
|
90
|
+
|
|
91
|
+
兼容旧形式 \`--output "需求:… 根因:… 方案:… 结果:…"\`(**必须按此结构给全四项**,逐项一句话,不可用「见前述」替代;缺任一项 \`--done\` 会被拒并提示缺失字段,补全后重跑即可不丢进度):
|
|
85
92
|
|
|
86
93
|
\`\`\`
|
|
87
94
|
需求:一句话语义化短标题(写「改了什么」)
|
|
@@ -90,15 +97,13 @@ step 3 --done 审计会把 src 核心文件(CLI / 状态机 / 注入框架等
|
|
|
90
97
|
结果:验证情况(测试数 / lint / typecheck / 部署状态)
|
|
91
98
|
\`\`\`
|
|
92
99
|
|
|
93
|
-
⚠️
|
|
94
|
-
|
|
95
|
-
缺任一项,\`--done\` 会被拒并提示缺失字段,补全后重跑即可(不丢进度)。
|
|
100
|
+
⚠️ **「需求:」(= --req)就是 QUICKLOG 条目标题**——CLI 提取该字段、截到首个标点(,。;,;)、超 80 字再截断。写成完整需求长句会被截成语义不完整的状语前半段(如「在 X 时,…」标题只剩「在 X 时」),事后还得手改。一句话说清改了什么(如「登录限流修复——INCR 计数误清」),需求背景放「根因:/方案:」字段。旧形式正文内避免嵌套全角冒号(标签后紧接嵌套冒号会被拆分判定缺字段)——这正是四参数形式消灭的事故面。
|
|
96
101
|
|
|
97
102
|
### 📎 文件括注 --file-notes(可选,省事后手改文件行)
|
|
98
103
|
若希望 QUICKLOG「文件:」行直接落盘为多行带括注(而非单行逗号路径),\`--done\` 时加 \`--file-notes\`,CLI 落盘为多行 \`- path(括注)\`,免去事后手改:
|
|
99
104
|
|
|
100
105
|
\`\`\`
|
|
101
|
-
sillyspec run quick --done --change <id> --
|
|
106
|
+
sillyspec run quick --done --change <id> --req "…" --cause "…" --solution "…" --result "…" --file-notes "src/a.js::登录端点串限流 || src/b.js::新建 INCR 计数"
|
|
102
107
|
\`\`\`
|
|
103
108
|
|
|
104
109
|
格式:\`path::括注\` 一条,\`||\` 分隔多条(括注可省略只留 \`path\`)。不传则「文件:」行回填审计到的实际改动文件单行(仍可事后手改)。
|
|
@@ -116,7 +121,10 @@ sillyspec run quick --done --change <id> --output "需求:… 根因:… 方
|
|
|
116
121
|
- 禁止使用 \`git add -A\`
|
|
117
122
|
- 不要暂存 quick 开始前就已存在的无关改动
|
|
118
123
|
3. QUICKLOG / tasks.md 记录由 CLI 在本 step 完成时自动收尾(翻「已完成」+ 勾选 task + 轮转),你无需手动建/翻骨架——**QUICKLOG 核对按需**(见下方「QUICKLOG 正文核对」;文件行可用 --file-notes 免手改)
|
|
119
|
-
4.
|
|
124
|
+
4. 只有遇到**真正的项目特有坑**(跨变更可复用、未来 agent 可能再次踩到、且不是本任务专属细节)时,才追加到 \`{SPEC_ROOT}/knowledge/uncategorized.md\`:
|
|
125
|
+
- 格式:\`## <一句话标题>\` + 一段说明(坑的来龙去脉 + 规避/解法),末尾标注来源 \`(<quicklog-id>)\`
|
|
126
|
+
- 不要为了完成任务而硬凑条目;纯新增/纯样式/单点 bug 修复/临时绕过不写
|
|
127
|
+
- 拿不准时不写,宁缺毋滥
|
|
120
128
|
5. 任务比预期复杂 → 建议用完整流程
|
|
121
129
|
|
|
122
130
|
### 模块文档同步
|
|
@@ -124,10 +132,11 @@ sillyspec run quick --done --change <id> --output "需求:… 根因:… 方
|
|
|
124
132
|
7. 对比本次修改的文件(\`git diff --name-only HEAD\`)与模块映射
|
|
125
133
|
8. 如果命中模块 → 直接同步模块文档:
|
|
126
134
|
- 读取对应的 \`{SPEC_ROOT}/docs/<project>/modules/<module>.md\`(如不存在则新建)
|
|
127
|
-
-
|
|
135
|
+
- 根据本次改动内容更新模块文档(正文描述当前状态;**勿把历史条目堆进正文——卡是子代理的读取税**)
|
|
136
|
+
- 变更索引条目追加到卡同目录 \`<module>.changelog.md\` sidecar(无则创建;历史段可跑 \`sillyspec modules split-changelog\` 迁出)
|
|
128
137
|
- 变更索引格式:\`- <quicklog-id> | <一句话描述>\`
|
|
129
|
-
-
|
|
130
|
-
- 使用 \`git add -- <module-doc>\`
|
|
138
|
+
- 写入模块文档与 sidecar
|
|
139
|
+
- 使用 \`git add -- <module-doc>\` 暂存更新的模块文件(sidecar 一并暂存)
|
|
131
140
|
9. 未命中任何模块 → 跳过,不做额外操作
|
|
132
141
|
|
|
133
142
|
### 输出
|
package/src/stages/scan.js
CHANGED
|
@@ -154,6 +154,7 @@ export const definition = {
|
|
|
154
154
|
generator: sillyspec-scan
|
|
155
155
|
---
|
|
156
156
|
\`\`\`
|
|
157
|
+
(\`<git-head-short>\` / \`<now-iso-datetime>\` 两占位符由 CLI 注入 prompt 时自动替换为当前 HEAD 短哈希与时间——照 prompt 里已替换的真实值抄写,勿自跑 git rev-parse;漏 header 事后 \`sillyspec scan-fix-headers\` 一键补 author/created_at)
|
|
157
158
|
- 覆盖已有 scan 文档前先读取旧 frontmatter;如果旧文档的 \`source_commit\` 与当前 HEAD 不一致,或旧文档 \`updated_at\` 晚于本次 scan 开始时间,不要覆盖。
|
|
158
159
|
- 如果用户明确传入 \`--force-rescan\`,允许覆盖,但仍需写入新的 \`source_commit\` 和 \`updated_at\`。
|
|
159
160
|
|