sillyspec 3.26.11 → 3.26.14
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 +1 -1
- package/.claude/skills/sillyspec-execute/SKILL.md +1 -1
- package/.claude/skills/sillyspec-knowledge/SKILL.md +1 -1
- package/.claude/skills/sillyspec-plan/SKILL.md +5 -5
- package/.claude/skills/sillyspec-quick/SKILL.md +5 -1
- package/README.md +7 -5
- package/SKILL.md +5 -4
- package/package.json +1 -1
- package/packages/dashboard/server/executor.js +10 -1
- package/packages/dashboard/server/index.js +108 -46
- package/src/config-schema.js +3 -0
- package/src/contract-matrix.js +22 -13
- package/src/db.js +24 -4
- package/src/dispatch/probe.js +99 -63
- package/src/docs-check.js +58 -13
- package/src/docs-debt.js +6 -2
- package/src/docs-gate.js +1 -0
- package/src/doctor-diagnostics.js +208 -21
- package/src/fs-atomic.js +3 -1
- package/src/git-helper.js +7 -4
- package/src/hooks/worktree-guard.js +8 -2
- package/src/index.js +200 -27
- package/src/init.js +3 -2
- package/src/local-detect.js +9 -1
- package/src/modules.js +7 -5
- package/src/progress/stage-machine.js +589 -578
- package/src/progress/step-store.js +1 -1
- package/src/progress.js +187 -30
- package/src/quicklog.js +47 -16
- package/src/run/command.js +88 -46
- package/src/run/complete-handlers.js +27 -15
- package/src/run/complete.js +1186 -1121
- package/src/run/concurrent-detect.js +3 -1
- package/src/run/gates.js +172 -25
- package/src/run/prompt.js +17 -9
- package/src/run/scan-profile.js +35 -23
- package/src/run/shared.js +164 -63
- package/src/run/stage.js +41 -1
- package/src/setup.js +8 -10
- package/src/sillyhub-mcp/client.js +74 -6
- package/src/spec-dir-typo.js +3 -24
- package/src/spec-sync.js +43 -8
- package/src/stage-contract.js +12 -38
- package/src/stage-review.js +40 -1
- package/src/stages/archive.js +4 -3
- package/src/stages/brainstorm-auto.js +1 -1
- package/src/stages/brainstorm.js +27 -13
- package/src/stages/doctor.js +29 -16
- package/src/stages/execute.js +170 -154
- package/src/stages/knowledge.js +18 -3
- package/src/stages/plan-postcheck.js +72 -37
- package/src/stages/plan.js +70 -48
- package/src/stages/quick.js +3 -1
- package/src/stages/verify.js +1 -1
- package/src/sync.js +339 -225
- package/src/task-review.js +281 -64
- package/src/taskcard.js +178 -0
- package/src/verify-postcheck.js +48 -35
- package/src/worktree-apply.js +58 -34
- package/src/worktree-deps.js +120 -20
- package/src/worktree.js +55 -9
- package/.npmrc-publish +0 -2
package/src/stages/execute.js
CHANGED
|
@@ -9,24 +9,67 @@ import { gitQuiet } from '../git-helper.js'
|
|
|
9
9
|
import { parseRepo } from './plan-postcheck.js'
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
12
|
+
* 任务注册表解析(2026-08-20-task-truth-unify D-001@v1:tasks.md 唯一任务真相)。
|
|
13
|
+
* 行契约:`- [ ] task-01: 名称 [model:xxx] (depends_on: task-01,02)`——标注均可选;
|
|
14
|
+
* ql-xxx 等 task-XX 前缀之外的行(quick 挂载条目)与注册表正交,不收。
|
|
15
|
+
* @param {string} tasksContent - tasks.md 文件内容
|
|
16
|
+
* @returns {Array<{index:number, id:string, name:string, done:boolean, model:string|null, dependsOn:string[], file:string, steps:string, reference:string}>}
|
|
17
|
+
*/
|
|
18
|
+
export function parseTaskRegistry(tasksContent) {
|
|
19
|
+
const tasks = []
|
|
20
|
+
if (!tasksContent) return tasks
|
|
21
|
+
for (const line of String(tasksContent).split('\n')) {
|
|
22
|
+
const m = line.match(/^[-*]\s*\[([ xX])\]\s*task-(\d+)\b[::]?\s*(.*)/)
|
|
23
|
+
if (!m) continue
|
|
24
|
+
let name = (m[3] || '').trim()
|
|
25
|
+
let model = null
|
|
26
|
+
const modelMatch = name.match(/\[model:([^\]]+)\]/i)
|
|
27
|
+
if (modelMatch) { model = modelMatch[1].trim(); name = name.replace(modelMatch[0], '').trim() }
|
|
28
|
+
let dependsOn = []
|
|
29
|
+
const depMatch = name.match(/\(depends_on:\s*([^)]+)\)/i)
|
|
30
|
+
if (depMatch) {
|
|
31
|
+
dependsOn = depMatch[1].split(/[,,]/).map(s => s.trim().replace(/^task-/i, '')).filter(Boolean).map(n => `task-${n.padStart(2, '0')}`)
|
|
32
|
+
name = name.replace(depMatch[0], '').trim()
|
|
33
|
+
}
|
|
34
|
+
// 兼容行尾 (文件路径) 尾注(旧 plan 内联习惯在 tasks.md 沿用时同样收容)
|
|
35
|
+
let file = ''
|
|
36
|
+
const fileMatch = name.match(/\(([^()]+)\)$/)
|
|
37
|
+
if (fileMatch) { file = fileMatch[1].trim(); name = name.replace(/\([^()]+\)$/, '').trim() }
|
|
38
|
+
tasks.push({
|
|
39
|
+
index: parseInt(m[2], 10),
|
|
40
|
+
id: `task-${m[2].padStart(2, '0')}`,
|
|
41
|
+
name,
|
|
42
|
+
done: (m[1] === 'x' || m[1] === 'X'),
|
|
43
|
+
model,
|
|
44
|
+
dependsOn,
|
|
45
|
+
file,
|
|
46
|
+
steps: '',
|
|
47
|
+
reference: ''
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
return tasks
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 校验 tasks.md(任务注册表)× plan.md(Wave ID 引用)是否满足 execute 执行契约。
|
|
55
|
+
* 新契约(D-001@v1):任务清单唯一真相在 tasks.md;plan.md Wave 段下为纯 ID 引用行
|
|
56
|
+
* (`- task-01`,不重抄任务名),交叉校验存在性/覆盖恰一次/编号连续。
|
|
57
|
+
* @param {string} tasksContent - tasks.md 文件内容
|
|
13
58
|
* @param {string} planContent - plan.md 文件内容
|
|
14
59
|
* @returns {{ ok: boolean, errors: string[], warnings: string[], tasks: object[], waves: object[] }}
|
|
15
60
|
*/
|
|
16
|
-
export function validatePlanForExecute(planContent) {
|
|
61
|
+
export function validatePlanForExecute(tasksContent, planContent) {
|
|
17
62
|
const errors = []
|
|
18
63
|
const warnings = []
|
|
64
|
+
const plan = String(planContent || '')
|
|
65
|
+
const registry = parseTaskRegistry(tasksContent)
|
|
19
66
|
|
|
20
|
-
if (!
|
|
21
|
-
return { ok: false, errors: ['plan.md
|
|
67
|
+
if (!plan.trim() && !String(tasksContent || '').trim()) {
|
|
68
|
+
return { ok: false, errors: ['plan.md 与 tasks.md 内容均为空'], warnings, tasks: [], waves: [] }
|
|
22
69
|
}
|
|
23
70
|
|
|
24
|
-
const waves = parseWavesFromPlan(planContent)
|
|
25
|
-
|
|
26
71
|
// 检查 0: Wave 编号带字母后缀(如 "## Wave 2b")→ 显式报错(坑 plan-wave-letter-suffix)
|
|
27
|
-
|
|
28
|
-
// 与显式 Wave 2 合并为同一 Wave 强制并行——串行意图静默失效且无任何提示,排查到源码才明白。
|
|
29
|
-
const letterSuffixWaves = [...planContent.matchAll(/^#+\s*Wave\s+(\d+)([a-z])(?![a-z0-9])/gim)]
|
|
72
|
+
const letterSuffixWaves = [...plan.matchAll(/^#+\s*Wave\s+(\d+)([a-z])(?![a-z0-9])/gim)]
|
|
30
73
|
for (const m of letterSuffixWaves) {
|
|
31
74
|
errors.push(
|
|
32
75
|
`Wave 编号带字母后缀:${m[0].trim()}("${m[1]}${m[2]}")不被支持。` +
|
|
@@ -35,70 +78,89 @@ export function validatePlanForExecute(planContent) {
|
|
|
35
78
|
)
|
|
36
79
|
}
|
|
37
80
|
|
|
38
|
-
//
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
81
|
+
// 检查 0.5: 旧格式残留——plan.md 内出现任务名级 checkbox 行(任务清单旧家)→ 指路迁移
|
|
82
|
+
const legacyCheckbox = [...plan.matchAll(/^[-*]\s*\[[ xX]\]\s*task-\d+\b[::]/gim)]
|
|
83
|
+
if (legacyCheckbox.length > 0) {
|
|
84
|
+
errors.push(
|
|
85
|
+
`plan.md 含 ${legacyCheckbox.length} 处旧格式任务 checkbox 行("- [ ] task-XX: 名称")。` +
|
|
86
|
+
`新契约(2026-08-20-task-truth-unify):任务清单唯一真相在 tasks.md,plan.md Wave 段下只写纯 ID 引用行。` +
|
|
87
|
+
`迁移:把任务 checkbox 行移入 tasks.md(保留 [model:xxx]/(depends_on: …) 行内标注),plan.md Wave 段改为 "- task-XX" 引用行`
|
|
88
|
+
)
|
|
44
89
|
}
|
|
45
90
|
|
|
46
|
-
|
|
91
|
+
const waves = parseWavesFromPlan(plan, registry)
|
|
92
|
+
const allTasks = registry
|
|
93
|
+
|
|
94
|
+
// 检查 0.8: Wave 标题格式不对(W1/Wave1/波次1 等)→ 引用行不被收容,静默退化隐式单 Wave
|
|
95
|
+
// 全并行——串行意图失效且无提示(原「标题格式不对」诊断的新契约承接)。仅在确有疑似标题
|
|
96
|
+
// 且无任何显式 Wave 被解析时报,正常 Wave 10/带括号标题不误伤。
|
|
97
|
+
const hasExplicitWave = waves.some(w => !w.implicit)
|
|
98
|
+
const waveLikeHeading = /^#+\s*(?:wave\s*\d+|w\d+|波次\s*\d+)/im
|
|
99
|
+
if (!hasExplicitWave && waveLikeHeading.test(plan)) {
|
|
100
|
+
errors.push(
|
|
101
|
+
`Wave 标题格式不对:必须字面 "## Wave N"(Wave + 空格 + 数字),"## W1" / "## Wave1" / "## 波次1" 都不被识别。` +
|
|
102
|
+
`其下的 "- task-XX" 引用行不会被收容——任务将退化为单个隐式 Wave 全并行,串行意图失效`
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 检查 1: 注册表非空(tasks.md 无 task-XX checkbox → 三类根因诊断)
|
|
47
107
|
if (allTasks.length === 0) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const diags = diagnoseNoTaskRootCause(planContent)
|
|
51
|
-
errors.push('plan.md 中没有找到 checkbox task(格式: "- [ ] task-XX: 任务名")')
|
|
108
|
+
const diags = diagnoseNoTaskRegistry(tasksContent, plan)
|
|
109
|
+
errors.push('tasks.md 中没有找到 task-XX checkbox(任务注册表为空,格式: "- [ ] task-XX: 任务名")')
|
|
52
110
|
for (const d of diags) errors.push(` 诊断:${d}`)
|
|
53
111
|
return { ok: false, errors, warnings, tasks: allTasks, waves }
|
|
54
112
|
}
|
|
55
113
|
|
|
56
|
-
// 检查 2: task id
|
|
114
|
+
// 检查 2: task id 唯一性(tasks.md 内重复行)
|
|
57
115
|
const idCounts = {}
|
|
58
|
-
for (const task of allTasks)
|
|
59
|
-
if (task.index != null) {
|
|
60
|
-
const key = `task-${task.index}`
|
|
61
|
-
idCounts[key] = (idCounts[key] || 0) + 1
|
|
62
|
-
}
|
|
63
|
-
}
|
|
116
|
+
for (const task of allTasks) idCounts[task.id] = (idCounts[task.id] || 0) + 1
|
|
64
117
|
for (const [id, count] of Object.entries(idCounts)) {
|
|
65
|
-
if (count > 1) {
|
|
66
|
-
errors.push(`task id 重复: ${id} 出现 ${count} 次`)
|
|
67
|
-
}
|
|
118
|
+
if (count > 1) errors.push(`task id 重复: ${id} 在 tasks.md 出现 ${count} 次`)
|
|
68
119
|
}
|
|
69
120
|
|
|
70
|
-
// 检查 3: task id 连续性(从 1
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (ids[0] === 1) {
|
|
79
|
-
for (let i = 0; i < ids.length; i++) {
|
|
80
|
-
if (ids[i] !== i + 1) {
|
|
81
|
-
errors.push(getRule('plan.task-id-continuity').failMessage.replaceAll('${expected}', String(i + 1).padStart(2, '0')).replaceAll('${actual}', String(ids[i]).padStart(2, '0')))
|
|
82
|
-
break
|
|
83
|
-
}
|
|
121
|
+
// 检查 3: task id 连续性(从 1 开始;ids[0]!==1 时整体跳过 = 兼容旧变更编号不从 1 起,
|
|
122
|
+
// 契约由 test/plan-execute-contract.test.mjs Case 10 钉死,勿改)
|
|
123
|
+
const ids = allTasks.map(t => t.index).sort((a, b) => a - b)
|
|
124
|
+
if (ids.length > 0 && ids[0] === 1) {
|
|
125
|
+
for (let i = 0; i < ids.length; i++) {
|
|
126
|
+
if (ids[i] !== i + 1) {
|
|
127
|
+
errors.push(getRule('plan.task-id-continuity').failMessage.replaceAll('${expected}', String(i + 1).padStart(2, '0')).replaceAll('${actual}', String(ids[i]).padStart(2, '0')))
|
|
128
|
+
break
|
|
84
129
|
}
|
|
85
130
|
}
|
|
86
131
|
}
|
|
87
132
|
|
|
88
|
-
// 检查 4: task name
|
|
133
|
+
// 检查 4: task name 非空(注册表行)
|
|
89
134
|
for (const task of allTasks) {
|
|
90
135
|
if (!task.name || !task.name.trim()) {
|
|
91
|
-
errors.push(
|
|
136
|
+
errors.push(`${task.id}: 任务名为空(tasks.md 行格式 "- [ ] ${task.id}: 一句话任务名")`)
|
|
92
137
|
}
|
|
93
138
|
}
|
|
94
139
|
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
140
|
+
// 交叉校验:plan.md Wave 引用 × tasks.md 注册表(仅显式 Wave 结构参与;隐式 Wave 由
|
|
141
|
+
// parseWavesFromPlan 从注册表整体合成,天然全覆盖)
|
|
142
|
+
const explicitWaves = waves.filter(w => !w.implicit)
|
|
143
|
+
if (explicitWaves.length > 0) {
|
|
144
|
+
const refCount = {}
|
|
145
|
+
for (const wave of explicitWaves) {
|
|
146
|
+
for (const task of wave.tasks) {
|
|
147
|
+
refCount[task.id] = (refCount[task.id] || 0) + 1
|
|
148
|
+
if (task.dangling) {
|
|
149
|
+
errors.push(`plan.md Wave ${wave.index} 引用的 ${task.id} 不存在于 tasks.md 注册表(悬空引用)`)
|
|
150
|
+
}
|
|
100
151
|
}
|
|
101
152
|
}
|
|
153
|
+
// 覆盖恰一次:注册表每个 id 必须被引用(缺失)且只被一个 Wave 引用(重复)
|
|
154
|
+
for (const task of allTasks) {
|
|
155
|
+
const n = refCount[task.id] || 0
|
|
156
|
+
if (n === 0) errors.push(`${task.id} 未被任何 Wave 引用(plan.md Wave 段缺 "- ${task.id}" 引用行——未引用的任务不进 execute 步骤)`)
|
|
157
|
+
if (n > 1) errors.push(`${task.id} 被多个 Wave 重复引用 ${n} 次(一个任务只属一个 Wave;跨 Wave 依赖用 (depends_on: …) 声明)`)
|
|
158
|
+
}
|
|
159
|
+
// Wave 段空引用诊断
|
|
160
|
+
const emptyWave = explicitWaves.find(w => w.tasks.length === 0)
|
|
161
|
+
if (emptyWave) {
|
|
162
|
+
errors.push(`plan.md Wave ${emptyWave.index} 段下没有任何 "- task-XX" 引用行(新契约 Wave 段只收 ID 引用行)`)
|
|
163
|
+
}
|
|
102
164
|
}
|
|
103
165
|
|
|
104
166
|
return { ok: errors.length === 0, errors, warnings, tasks: allTasks, waves }
|
|
@@ -134,7 +196,7 @@ const fixedPrefix = [
|
|
|
134
196
|
prompt: `加载计划、设计和代码库上下文。
|
|
135
197
|
|
|
136
198
|
### 操作
|
|
137
|
-
1. 读取 tasks.md
|
|
199
|
+
1. 读取 tasks.md(任务注册表与勾选唯一真相;plan.md 只提供 Wave 分组/依赖结构——Wave 段下为纯 ID 引用行)
|
|
138
200
|
2. 读取 design.md(技术方案)
|
|
139
201
|
3. 读取 CONVENTIONS.md、ARCHITECTURE.md
|
|
140
202
|
4. 读取 local.yaml(构建命令);若 local.yaml 不存在,先 \`sillyspec local detect\` 生成骨架再读取
|
|
@@ -386,123 +448,71 @@ Apply Decision + 下一步建议
|
|
|
386
448
|
]
|
|
387
449
|
|
|
388
450
|
/**
|
|
389
|
-
*
|
|
390
|
-
*
|
|
391
|
-
*
|
|
451
|
+
* 诊断任务注册表为空的根因(坑 plan-md-format-contract-hidden 延续:笼统报错逼 agent 试错)。
|
|
452
|
+
* 三类根因:tasks.md 无 task-XX checkbox / plan.md Wave 段无 ID 引用 / 旧格式(任务 checkbox 还在 plan.md)。
|
|
453
|
+
* @param {string} tasksContent
|
|
392
454
|
* @param {string} planContent
|
|
393
455
|
* @returns {string[]}
|
|
394
456
|
*/
|
|
395
|
-
function
|
|
457
|
+
function diagnoseNoTaskRegistry(tasksContent, planContent) {
|
|
396
458
|
const diags = []
|
|
397
|
-
const
|
|
398
|
-
const
|
|
399
|
-
const
|
|
400
|
-
const
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
diags.push('task 用了 "### task-XX:" 标题(非 checkbox):标题会打断 Wave 段导致后续 checkbox 全被跳过,请改用 "- [ ] task-XX: 名称"(英文冒号)')
|
|
459
|
+
const tc = String(tasksContent || '')
|
|
460
|
+
const pc = String(planContent || '')
|
|
461
|
+
const hasTaskCheckboxInTasks = /^[-*]\s*\[[ xX]\]\s*task-\d+/im.test(tc)
|
|
462
|
+
const hasQlLines = /^[-*]\s*\[[ xX]\]\s*ql-/im.test(tc)
|
|
463
|
+
const hasLegacyInPlan = /^[-*]\s*\[[ xX]\]\s*task-\d+\b[::]/im.test(pc)
|
|
464
|
+
const hasWaveHeading = /^#+\s*Wave\s+\d+/im.test(pc)
|
|
465
|
+
const hasRefLine = /^[-*]\s+task-\d+\s*$/im.test(pc)
|
|
466
|
+
|
|
467
|
+
if (hasLegacyInPlan) {
|
|
468
|
+
diags.push('任务 checkbox 还在 plan.md(旧格式):请移入 tasks.md(行格式 "- [ ] task-XX: 一句话任务名",[model:xxx]/(depends_on: …) 标注随行),plan.md Wave 段改为 "- task-XX" 纯 ID 引用行')
|
|
469
|
+
} else if (!tc.trim()) {
|
|
470
|
+
diags.push('tasks.md 内容为空或缺失:plan 阶段应把展开后的任务清单写回 tasks.md(brainstorm 骨架只是名字级占位)')
|
|
471
|
+
} else if (!hasTaskCheckboxInTasks && hasQlLines) {
|
|
472
|
+
diags.push('tasks.md 只有 ql-xxx 行(quick 挂载条目),没有 task-XX 任务行——完整流程变更的任务清单尚未写入')
|
|
473
|
+
} else if (!hasTaskCheckboxInTasks) {
|
|
474
|
+
diags.push('tasks.md 无 "- [ ] task-XX:" 格式的任务行:检查行首格式(- 空格 [ ] 空格 task-XX: 英文冒号)')
|
|
414
475
|
}
|
|
415
|
-
if (
|
|
416
|
-
diags.push('有 Wave
|
|
476
|
+
if (hasTaskCheckboxInTasks && hasWaveHeading && !hasRefLine) {
|
|
477
|
+
diags.push('plan.md 有 Wave 段但没有 "- task-XX" 引用行:新契约 Wave 段只收纯 ID 引用行(如 "- task-01"),未引用的任务不进 execute')
|
|
417
478
|
}
|
|
418
479
|
return diags
|
|
419
480
|
}
|
|
420
481
|
|
|
421
482
|
/**
|
|
422
|
-
* 从 plan
|
|
483
|
+
* 从 plan 解析 Wave 分组(新契约:Wave 段下纯 ID 引用行 `- task-XX`,任务详情在 tasks.md 注册表)。
|
|
484
|
+
* @param {string} planContent
|
|
485
|
+
* @param {Array<object>} registry - parseTaskRegistry 产物(id → 任务详情富化源)
|
|
423
486
|
*/
|
|
424
|
-
function parseWavesFromPlan(planContent) {
|
|
487
|
+
function parseWavesFromPlan(planContent, registry = []) {
|
|
425
488
|
const waves = []
|
|
426
|
-
const
|
|
489
|
+
const regById = new Map(registry.map(t => [t.id, t]))
|
|
427
490
|
let currentWave = null
|
|
428
|
-
let currentTask = null
|
|
429
|
-
// light/none plan.md 用 `## Tasks`(无 `## Wave N`)包任务,需识别为隐式任务区,
|
|
430
|
-
// 让其中的 task checkbox 能被收进惰性创建的隐式 Wave(见下方 taskMatch 分支)。
|
|
431
|
-
let inImplicitTaskSection = false
|
|
432
491
|
|
|
433
|
-
for (const line of
|
|
492
|
+
for (const line of String(planContent || '').split('\n')) {
|
|
434
493
|
const waveMatch = line.match(/^#+\s*Wave\s+(\d+)/i)
|
|
435
494
|
if (waveMatch) {
|
|
436
495
|
currentWave = { index: parseInt(waveMatch[1]), tasks: [] }
|
|
437
|
-
currentTask = null
|
|
438
|
-
inImplicitTaskSection = false
|
|
439
496
|
waves.push(currentWave)
|
|
440
497
|
continue
|
|
441
498
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
const headingText = headingMatch[1].trim().toLowerCase()
|
|
452
|
-
inImplicitTaskSection = /^(tasks?|任务)$/.test(headingText)
|
|
453
|
-
continue
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
const taskMatch = line.match(/^[-*]\s*\[[ x]\]\s*(.+)/)
|
|
457
|
-
if (taskMatch) {
|
|
458
|
-
const taskNoMatch = taskMatch[1].match(/\btask-(\d+)\b/i)
|
|
459
|
-
// full plan.md:task 必须在显式 Wave 段内(currentWave 非 null),正常收容。
|
|
460
|
-
// light/none plan.md(无 Wave 标题,任务在 `## Tasks` 下):在隐式任务区内,遇含
|
|
461
|
-
// task-XX 编号的 checkbox 时惰性创建隐式 Wave 收容,否则 validatePlanForExecute 会报
|
|
462
|
-
// "没有找到 checkbox task"。详见 docs/sillyspec/plan-light-needs-wave-heading.md
|
|
463
|
-
// 不收的情况:非任务区(## 自检/## 验收 等)的 checkbox,或任务区内无 task-XX 编号的 checkbox。
|
|
464
|
-
if (!currentWave) {
|
|
465
|
-
if (!inImplicitTaskSection || !taskNoMatch) continue
|
|
466
|
-
const nextIndex = waves.length === 0 ? 1 : (waves[waves.length - 1].index || waves.length) + 1
|
|
467
|
-
currentWave = { index: nextIndex, tasks: [], implicit: true }
|
|
468
|
-
currentTask = null
|
|
469
|
-
waves.push(currentWave)
|
|
470
|
-
}
|
|
471
|
-
currentTask = {
|
|
472
|
-
index: taskNoMatch ? parseInt(taskNoMatch[1], 10) : null,
|
|
473
|
-
name: taskMatch[1].trim(),
|
|
474
|
-
file: '',
|
|
475
|
-
steps: '',
|
|
476
|
-
reference: ''
|
|
477
|
-
}
|
|
478
|
-
// 兼容旧格式:任务名后跟 (文件路径)
|
|
479
|
-
const fileMatch = taskMatch[1].match(/\(([^)]+)\)$/)
|
|
480
|
-
if (fileMatch) {
|
|
481
|
-
currentTask.file = fileMatch[1]
|
|
482
|
-
currentTask.name = taskMatch[1].replace(/\([^)]+\)$/, '').trim()
|
|
483
|
-
}
|
|
484
|
-
currentWave.tasks.push(currentTask)
|
|
485
|
-
continue
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
// 解析子行信息(修改/参考/步骤)
|
|
489
|
-
if (currentTask) {
|
|
490
|
-
const modMatch = line.match(/^\s+-\s*修改:\s*(.+)/)
|
|
491
|
-
if (modMatch) { currentTask.file = modMatch[1].trim(); continue }
|
|
492
|
-
|
|
493
|
-
const refMatch = line.match(/^\s+-\s*参考:\s*(.+)/)
|
|
494
|
-
if (refMatch) { currentTask.reference = refMatch[1].trim(); continue }
|
|
495
|
-
|
|
496
|
-
const stepMatch = line.match(/^\s+-\s*步骤:/)
|
|
497
|
-
if (stepMatch) { currentTask.steps = line.replace(/^\s+-\s*步骤:\s*/, '').trim(); continue }
|
|
498
|
-
|
|
499
|
-
// 步骤续行(数字开头的子步骤)
|
|
500
|
-
if (currentTask.steps && line.match(/^\s+\d+\./)) {
|
|
501
|
-
currentTask.steps += '\n' + line.trim()
|
|
502
|
-
}
|
|
499
|
+
// 任何非 Wave 的标题行退出当前 Wave 段(「## 自检」段的行不收,与旧解析同守卫)
|
|
500
|
+
if (/^#{1,6}\s+/.test(line)) { currentWave = null; continue }
|
|
501
|
+
const refMatch = line.match(/^[-*]\s+task-(\d+)\s*$/i)
|
|
502
|
+
if (refMatch && currentWave) {
|
|
503
|
+
const id = `task-${refMatch[1].padStart(2, '0')}`
|
|
504
|
+
const reg = regById.get(id)
|
|
505
|
+
currentWave.tasks.push(reg
|
|
506
|
+
? { ...reg }
|
|
507
|
+
: { index: parseInt(refMatch[1], 10), id, name: '', done: false, model: null, dependsOn: [], file: '', steps: '', reference: '', dangling: true })
|
|
503
508
|
}
|
|
504
509
|
}
|
|
505
510
|
|
|
511
|
+
// 无显式 Wave 结构但注册表非空(light 级:任务全在 tasks.md、plan.md 只留策略)→
|
|
512
|
+
// 合成单隐式 Wave 收容全部任务(与旧 light `## Tasks` 隐式收容语义对齐,单 Wave 串行执行)
|
|
513
|
+
if (waves.length === 0 && registry.length > 0) {
|
|
514
|
+
waves.push({ index: 1, implicit: true, tasks: registry.map(t => ({ ...t })) })
|
|
515
|
+
}
|
|
506
516
|
return waves
|
|
507
517
|
}
|
|
508
518
|
|
|
@@ -568,6 +578,10 @@ function writeBaseCommitToTaskCard(taskFilePath, baseCommit) {
|
|
|
568
578
|
} catch {
|
|
569
579
|
return false
|
|
570
580
|
}
|
|
581
|
+
// CRLF 归一(坑 base-commit-crlf-frontmatter,plan-postcheck.js 同款):跨仓 task 卡被
|
|
582
|
+
// 编辑器/子代理写成 CRLF 后 `^---\n` 匹配失败 → base_commit 锚点静默不落盘,而 Wave
|
|
583
|
+
// prompt 仍声称「CLI 已落盘」。归一后按 LF 写回(taskcard 生成侧本就强制 LF)。
|
|
584
|
+
content = content.replace(/\r\n?/g, '\n')
|
|
571
585
|
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/)
|
|
572
586
|
if (!fmMatch) return false
|
|
573
587
|
const fm = fmMatch[1]
|
|
@@ -855,7 +869,7 @@ ${workdirLines}
|
|
|
855
869
|
你的角色是调度者 + 审查者(batch 只合并实现、不合并审查):
|
|
856
870
|
1. 为每个任务启动一个子代理(Agent tool),或按上述三条件把多个任务合并为一个 batch 子代理,同 Wave 内可并行
|
|
857
871
|
2. 子代理完成后审查结果——batch 子代理只做实现与自验,task 审查、review.json 产出与 checkbox 勾选仍归你(主 agent),在子代理返回后逐 task 进行;审查 batch 报告时逐 task 对照 allowed_paths 检查改动文件清单有无越权
|
|
858
|
-
3. 勾选
|
|
872
|
+
3. 勾选 tasks.md 中对应任务的 checkbox
|
|
859
873
|
4. 记录改动文件和测试结果
|
|
860
874
|
|
|
861
875
|
${worktreeSection}${crossRepoCommitSection}${dispatchSection}
|
|
@@ -874,7 +888,7 @@ ${taskSummary}
|
|
|
874
888
|
5. 任务含测试代码时,把下方「测试用例设计」整段复制进子代理 prompt,要求子代理按此设计测试用例
|
|
875
889
|
6. **增量落盘与中断接手指引**:每完成一个可见产出(代码/测试/文档),立即写盘并执行一次最小验证(如语法检查、单跑相关测试)。工作过程中如被 429/API 配额/会话中断,应在最终回复里输出「已完成清单」(含文件路径、测试命令、当前卡点),不要只输出结论——主代理会依据磁盘产物和该清单判断哪些部分已完成,哪些需接手补做,避免重做已落盘的工作
|
|
876
890
|
7. **任务边界铁律**:严格只实现本 task 的 \`allowed_paths\` 内文件;若 design.md/plan.md 明确指定了接口/回调/钩子接入位置,必须逐字遵守;不允许顺手实现其他 task 的内容(如 task-01 不要把 task-02 的接入也做了)。如发现必须改其他 task 文件才能继续,先回到主代理由主代理决定是否重分 Wave 或调整 plan,禁止子代理私自越界
|
|
877
|
-
8. **batch 子代理协议**(仅当按「执行方式」节条件合并 batch 时附加进该子代理 prompt):按 batch 内 task 顺序逐个完成实现闭环——读取 tasks/task-N.md → 实现 → 跑该 task 的 verify 命令 → 记录该 task 报告(改动文件清单 / verify 结果 / 卡点)→ 才开始下一个 task;最终回复输出逐 task 报告清单。禁止写 review.json、禁止勾选
|
|
891
|
+
8. **batch 子代理协议**(仅当按「执行方式」节条件合并 batch 时附加进该子代理 prompt):按 batch 内 task 顺序逐个完成实现闭环——读取 tasks/task-N.md → 实现 → 跑该 task 的 verify 命令 → 记录该 task 报告(改动文件清单 / verify 结果 / 卡点)→ 才开始下一个 task;最终回复输出逐 task 报告清单。禁止写 review.json、禁止勾选 tasks.md checkbox——task 审查与勾选归主 agent,在子代理返回后逐 task 进行。越权即停:发现必须改 batch 内其他 task 或任何 batch 外 task 的 allowed_paths 文件 → 立即停止本 task 及后续,报告冲突文件与卡点,回主 agent 裁决(重分 Wave / 调整 plan / 回退独立子代理)。第 7 条任务边界铁律在 batch 语境下的「本 task」= 当前正在实现的 task
|
|
878
892
|
|
|
879
893
|
{{include: testcase-design}}
|
|
880
894
|
|
|
@@ -889,8 +903,8 @@ ${taskSummary}
|
|
|
889
903
|
- ❄️ 冷上下文:其他变更的 design.md、历史 plan.md(不要主动加载,除非明确需要)
|
|
890
904
|
|
|
891
905
|
### 中断续跑(如曾中断恢复)
|
|
892
|
-
execute 按 Wave 持久化进度,task 级进度靠
|
|
893
|
-
-
|
|
906
|
+
execute 按 Wave 持久化进度,task 级进度靠 tasks.md checkbox 勾选。若本 Wave 曾因 429/API 配额/崩溃中断:
|
|
907
|
+
- tasks.md 中**已勾选 \`- [x]\` 的 task 已完成,跳过不重跑**(子代理也可能在完成前中断,重跑前先确认该 task 产出文件是否完整)
|
|
894
908
|
- 用 \`sillyspec status\` 查当前进度,重新 \`sillyspec run execute\` 会回到当前 Wave step 继续,**不要从零重置或重跑已完成 Wave**
|
|
895
909
|
- 本 Wave 已完成但不完整(产出缺文件)的 task 补做,不牵连其他 task
|
|
896
910
|
|
|
@@ -919,7 +933,7 @@ ${taskList}
|
|
|
919
933
|
1. 读取当前 task 的 git diff(从 task 开始到完成的变更)
|
|
920
934
|
2. 对照 plan.md 中该 task 的描述和 tasks/task-XX.md(如果存在)检查实现是否符合要求
|
|
921
935
|
3. 写入 review.json 文件
|
|
922
|
-
4. **只有 review.json 写入成功后,才允许勾选
|
|
936
|
+
4. **只有 review.json 写入成功后,才允许勾选 tasks.md 中对应任务的 checkbox**(勾选唯一落点;CLI 的 autoCheckPlanFromReviews 机器勾选器同样写 tasks.md,文件锁 .tasks.md.lock 串行化双路勾选)
|
|
923
937
|
|
|
924
938
|
**review.json 路径:**
|
|
925
939
|
|
|
@@ -970,9 +984,11 @@ export function buildExecuteSteps(planFilePath = null, options = {}) {
|
|
|
970
984
|
|
|
971
985
|
if (planFilePath && existsSync(planFilePath)) {
|
|
972
986
|
const planContent = readFileSync(planFilePath, 'utf8')
|
|
973
|
-
//
|
|
974
|
-
|
|
975
|
-
|
|
987
|
+
// 新契约(D-001@v1):任务注册表在 tasks.md(与 plan.md 同目录),Wave 结构在 plan.md
|
|
988
|
+
const tasksPath = path.join(path.dirname(planFilePath), 'tasks.md')
|
|
989
|
+
const tasksContent = existsSync(tasksPath) ? readFileSync(tasksPath, 'utf8') : ''
|
|
990
|
+
const registry = parseTaskRegistry(tasksContent)
|
|
991
|
+
waves = parseWavesFromPlan(planContent, registry)
|
|
976
992
|
changeDir = path.dirname(planFilePath)
|
|
977
993
|
}
|
|
978
994
|
|
package/src/stages/knowledge.js
CHANGED
|
@@ -276,10 +276,25 @@ export async function cmdRefresh(dir, args, opts = {}) {
|
|
|
276
276
|
// refresh 只写 generated/ 区
|
|
277
277
|
mkdirSync(generatedDir, { recursive: true })
|
|
278
278
|
|
|
279
|
-
// 扫描 scan
|
|
279
|
+
// 扫描 scan 文档。项目名用 DB 注册名优先(坑 knowledge-basename-project-mismatch):
|
|
280
|
+
// scan 写侧 docs/<project> 的 project 来自 dbProjectName || basename(cwd),注册名 ≠ 目录名
|
|
281
|
+
// (projects yaml name/path 分离、平台模式 specRoot≠sourceRoot)时,用 basename(dir) 拼
|
|
282
|
+
// scanDir 永远找不到 → refresh 恒报 scan_docs_missing。DB 读失败回退 basename(与写侧
|
|
283
|
+
// default 同口径)。查不到注册名对应目录时再回退 basename 试一次(双形态兼容)。
|
|
280
284
|
const base = opts.specDir || join(dir, '.sillyspec')
|
|
281
|
-
|
|
282
|
-
|
|
285
|
+
let projectName = null
|
|
286
|
+
try {
|
|
287
|
+
const { DatabaseSync } = await import('node:sqlite')
|
|
288
|
+
const db = new DatabaseSync(join(base, '.runtime', 'sillyspec.db'), { readOnly: true })
|
|
289
|
+
const row = db.prepare('SELECT name FROM project LIMIT 1').get()
|
|
290
|
+
db.close()
|
|
291
|
+
if (row && row.name) projectName = row.name
|
|
292
|
+
} catch { /* db 不可读 → 走 basename 兜底 */ }
|
|
293
|
+
if (!projectName) projectName = basename(dir)
|
|
294
|
+
let scanDir = join(base, 'docs', projectName, 'scan')
|
|
295
|
+
if (!existsSync(scanDir) && projectName !== basename(dir)) {
|
|
296
|
+
scanDir = join(base, 'docs', basename(dir), 'scan')
|
|
297
|
+
}
|
|
283
298
|
|
|
284
299
|
if (!existsSync(scanDir)) {
|
|
285
300
|
output(false, {}, { code: 'scan_docs_missing', path: scanDir })
|