sillyspec 3.18.4 → 3.18.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-auto/SKILL.md +1 -1
- package/.claude/skills/sillyspec-doctor/SKILL.md +1 -1
- package/.claude/skills/sillyspec-execute/SKILL.md +2 -0
- package/.claude/skills/sillyspec-resume/SKILL.md +5 -5
- package/.claude/skills/sillyspec-state/SKILL.md +8 -8
- package/docs/sillyspec/file-lifecycle/storage-and-state.md +1 -1
- package/package.json +1 -1
- package/packages/dashboard/server/index.js +4 -3
- package/packages/dashboard/server/parser.js +30 -70
- package/src/index.js +36 -5
- package/src/run.js +58 -12
- package/src/stage-contract.js +14 -1
- package/src/stages/execute.js +1 -0
- package/src/workflow.js +12 -8
- package/src/worktree-apply.js +5 -4
- package/templates/workflows/scan-docs.yaml +8 -8
- package/test/cli-top-level-aliases.test.mjs +174 -0
- package/test/run-sanitize-project-name.test.mjs +51 -0
- package/test/run-scan-postcheck-fail.test.mjs +64 -0
- package/test/run-scan-project-parse.test.mjs +77 -0
- package/test/scan-docs-yaml-placeholders.test.mjs +84 -0
- package/test/scan-postcheck-project-priority.test.mjs +85 -0
- package/test/scan-workflow-anyfailed-block.test.mjs +52 -0
- package/test/stage-contract-failed-post-check.test.mjs +102 -0
- package/test/workflow-spec-base.test.mjs +142 -0
|
@@ -23,6 +23,8 @@ description: 用于按 plan 执行代码实现。适合用户说"开始写代码
|
|
|
23
23
|
- Worktree 路径在 Step 3(确认 worktree 路径)中输出,后续子代理的 cwd 必须设为该路径
|
|
24
24
|
- **禁止跳过 worktree 或在主仓库直接写代码**
|
|
25
25
|
- 如果 worktree 创建失败,CLI 会报错并退出,需要排查后再重试
|
|
26
|
+
- **未提交的文件、dirty 状态等不影响 worktree 创建和进入,直接按 CLI 输出的 worktree 路径操作即可**
|
|
27
|
+
- 不要自行检查 git 状态来判断是否可以进入 worktree,CLI 会自动处理
|
|
26
28
|
|
|
27
29
|
## 用户指令
|
|
28
30
|
$ARGUMENTS
|
|
@@ -22,15 +22,15 @@ description: 恢复工作 — 从中断处继续
|
|
|
22
22
|
sillyspec progress show
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
### 2.
|
|
25
|
+
### 2. 如果有活跃变更
|
|
26
26
|
|
|
27
|
-
从
|
|
27
|
+
从 `sillyspec progress show` 输出中提取并展示当前状态。
|
|
28
28
|
|
|
29
29
|
然后问用户:
|
|
30
30
|
1. 直接继续执行下一步
|
|
31
31
|
2. 查看更多细节
|
|
32
32
|
|
|
33
|
-
### 3.
|
|
33
|
+
### 3. 如果没有活跃变更
|
|
34
34
|
|
|
35
35
|
自动探测项目状态:
|
|
36
36
|
|
|
@@ -64,5 +64,5 @@ cat .sillyspec/ROADMAP.md 2>/dev/null
|
|
|
64
64
|
|
|
65
65
|
### 4. 关键原则
|
|
66
66
|
|
|
67
|
-
-
|
|
68
|
-
-
|
|
67
|
+
- 进度数据存储在 SQLite 数据库中(`.sillyspec/.runtime/sillyspec.db`),通过 `sillyspec progress show` 命令查看
|
|
68
|
+
- 进度随 `sillyspec run <stage> --done` 自动更新,不需要手动保存
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: sillyspec:state
|
|
3
|
-
description: 查看当前工作状态 — 显示
|
|
3
|
+
description: 查看当前工作状态 — 显示 SillySpec 进度
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
你现在是 SillySpec 的状态查看器。
|
|
7
7
|
|
|
8
8
|
## 流程
|
|
9
9
|
|
|
10
|
-
### 1.
|
|
10
|
+
### 1. 读取进度
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
|
-
sillyspec
|
|
13
|
+
sillyspec progress show
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
### 2.
|
|
16
|
+
### 2. 如果有活跃变更
|
|
17
17
|
|
|
18
18
|
格式化展示当前状态:
|
|
19
19
|
|
|
@@ -33,9 +33,9 @@ sillyspec run state --status 2>/dev/null
|
|
|
33
33
|
> **阻塞项**:
|
|
34
34
|
> - xxx(如无则省略)
|
|
35
35
|
|
|
36
|
-
### 3.
|
|
36
|
+
### 3. 如果没有活跃变更
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
提示用户项目还没有开始:
|
|
39
39
|
|
|
40
40
|
> 📊 还没有工作记录。
|
|
41
41
|
>
|
|
@@ -44,11 +44,11 @@ sillyspec run state --status 2>/dev/null
|
|
|
44
44
|
> - 已有项目:`/sillyspec:scan`
|
|
45
45
|
> - 恢复中断的工作:`/sillyspec:resume`
|
|
46
46
|
>
|
|
47
|
-
>
|
|
47
|
+
> 进度数据会在 `sillyspec init` 时自动创建到 SQLite 数据库中。
|
|
48
48
|
|
|
49
49
|
### 注意
|
|
50
50
|
|
|
51
51
|
- 这是只读命令,**不修改任何文件**
|
|
52
52
|
- `/sillyspec:status` 查看项目整体进度(change 文件级别)
|
|
53
|
-
- `/sillyspec:state`
|
|
53
|
+
- `/sillyspec:state` 查看当前工作状态(阶段/步骤级别)
|
|
54
54
|
- 两者互补:status 看"有什么",state 看"在做什么"
|
|
@@ -43,7 +43,7 @@ created_at: 2026-06-04 16:25:42
|
|
|
43
43
|
| `batch_progress` | 批量任务统计 |
|
|
44
44
|
| `approvals` | 平台审批状态 |
|
|
45
45
|
|
|
46
|
-
`progress.js` 通过 SQL 读写这些表,并组装成兼容旧 progress
|
|
46
|
+
`progress.js` 通过 SQL 读写这些表,并组装成兼容旧 progress 格式的 JS 对象。进度数据仅存储在 SQLite 数据库中,不再使用 progress.json 文件。
|
|
47
47
|
|
|
48
48
|
注意:`db.js` 的 `project.schema_version` DDL 默认值是 `4`,但 `progress.js` 的 `CURRENT_VERSION` 是 `3`,并在初始化/写入时使用 `3`。文档不要把这里写成稳定的 v4 schema 事实。
|
|
49
49
|
|
package/package.json
CHANGED
|
@@ -48,12 +48,13 @@ function startProgressWatch(projectPath) {
|
|
|
48
48
|
progressWatchers.get(projectPath).refCount++
|
|
49
49
|
return
|
|
50
50
|
}
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
// Watch the SQLite database file for changes (replaces old progress.json watch)
|
|
52
|
+
const dbFile = join(projectPath, '.sillyspec', '.runtime', 'sillyspec.db')
|
|
53
|
+
if (!existsSync(dbFile)) return
|
|
53
54
|
|
|
54
55
|
let timer = null
|
|
55
56
|
try {
|
|
56
|
-
const watcher = watch(
|
|
57
|
+
const watcher = watch(dbFile, (eventType) => {
|
|
57
58
|
if (timer) clearTimeout(timer)
|
|
58
59
|
timer = setTimeout(() => {
|
|
59
60
|
timer = null
|
|
@@ -77,18 +77,13 @@ export function parseProjectOverview(projectPath) {
|
|
|
77
77
|
|
|
78
78
|
// --- Last active ---
|
|
79
79
|
const sillyspecDir = join(projectPath, '.sillyspec')
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
// Progress is stored in SQLite (.sillyspec/.runtime/sillyspec.db), not progress.json
|
|
81
|
+
// Use mtime of the DB file as a fallback for lastActive
|
|
82
|
+
const dbPath = join(sillyspecDir, '.runtime', 'sillyspec.db')
|
|
83
|
+
if (existsSync(dbPath)) {
|
|
82
84
|
try {
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
for (const stageData of Object.values(progress.stages)) {
|
|
86
|
-
if (stageData.lastActive && (!result.lastActive || new Date(stageData.lastActive) > new Date(result.lastActive))) {
|
|
87
|
-
result.lastActive = stageData.lastActive
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
if (progress.lastActive) result.lastActive = progress.lastActive
|
|
85
|
+
const s = statSync(dbPath)
|
|
86
|
+
result.lastActive = s.mtime.toISOString()
|
|
92
87
|
} catch {}
|
|
93
88
|
}
|
|
94
89
|
if (!result.lastActive) {
|
|
@@ -432,81 +427,46 @@ export function parseSillyspecDocsTree(projectPath) {
|
|
|
432
427
|
}
|
|
433
428
|
|
|
434
429
|
/**
|
|
435
|
-
* Parse project state from .sillyspec
|
|
430
|
+
* Parse project state from .sillyspec SQLite database via CLI.
|
|
431
|
+
* Progress data is stored in SQLite (.sillyspec/.runtime/sillyspec.db),
|
|
432
|
+
* accessed through `sillyspec progress show`.
|
|
436
433
|
* @param {string} projectPath - Path to the project directory
|
|
437
|
-
* @returns {object} Project state with currentStage,
|
|
434
|
+
* @returns {object|null} Project state with currentStage, stages, lastActive
|
|
438
435
|
*/
|
|
439
436
|
export function parseProjectState(projectPath) {
|
|
440
437
|
const sillyspecDir = join(projectPath, '.sillyspec')
|
|
438
|
+
const dbPath = join(sillyspecDir, '.runtime', 'sillyspec.db')
|
|
441
439
|
|
|
442
|
-
if (!existsSync(sillyspecDir)) {
|
|
440
|
+
if (!existsSync(sillyspecDir) || !existsSync(dbPath)) {
|
|
443
441
|
return null
|
|
444
442
|
}
|
|
445
443
|
|
|
446
444
|
let currentStage = ''
|
|
447
|
-
let nextStep = null
|
|
448
|
-
let progress = { stages: {} }
|
|
449
|
-
let stages = []
|
|
450
|
-
let specs = []
|
|
451
445
|
let lastActive = null
|
|
452
446
|
|
|
453
|
-
//
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
progress = progressData
|
|
459
|
-
currentStage = progressData.currentStage || ''
|
|
460
|
-
stages = Object.keys(progressData.stages || {})
|
|
461
|
-
|
|
462
|
-
// Find last active
|
|
463
|
-
if (progressData.lastActive) lastActive = progressData.lastActive
|
|
464
|
-
if (progressData.stages) {
|
|
465
|
-
for (const [stageName, stageData] of Object.entries(progressData.stages)) {
|
|
466
|
-
if (stageData.lastActive || stageData.startedAt) {
|
|
467
|
-
const t = stageData.lastActive || stageData.startedAt
|
|
468
|
-
if (!lastActive || new Date(t) > new Date(lastActive)) lastActive = t
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
} catch (err) {
|
|
473
|
-
// Progress file exists but couldn't be parsed
|
|
474
|
-
}
|
|
475
|
-
}
|
|
447
|
+
// Use DB file mtime as lastActive indicator
|
|
448
|
+
try {
|
|
449
|
+
const s = statSync(dbPath)
|
|
450
|
+
lastActive = s.mtime.toISOString()
|
|
451
|
+
} catch {}
|
|
476
452
|
|
|
477
|
-
//
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
const specPath = join(specsDir, f)
|
|
487
|
-
try {
|
|
488
|
-
const content = readFileSync(specPath, 'utf-8')
|
|
489
|
-
const titleMatch = content.match(/^#\s+(.+)$/m)
|
|
490
|
-
return {
|
|
491
|
-
name: f,
|
|
492
|
-
title: titleMatch ? titleMatch[1] : f,
|
|
493
|
-
path: specPath
|
|
494
|
-
}
|
|
495
|
-
} catch {
|
|
496
|
-
return { name: f, title: f, path: specPath }
|
|
497
|
-
}
|
|
498
|
-
})
|
|
499
|
-
} catch (err) {
|
|
500
|
-
// Specs directory couldn't be read
|
|
501
|
-
}
|
|
453
|
+
// Use CLI to read current stage from SQLite
|
|
454
|
+
try {
|
|
455
|
+
const output = execSync('sillyspec progress show 2>/dev/null', {
|
|
456
|
+
cwd: projectPath, encoding: 'utf-8', timeout: 5000
|
|
457
|
+
})
|
|
458
|
+
const stageMatch = output.match(/当前阶段:\s*(\S+)/)
|
|
459
|
+
if (stageMatch) currentStage = stageMatch[1]
|
|
460
|
+
} catch {
|
|
461
|
+
// CLI unavailable or no active change
|
|
502
462
|
}
|
|
503
463
|
|
|
504
464
|
return {
|
|
505
465
|
currentStage,
|
|
506
|
-
nextStep,
|
|
507
|
-
progress,
|
|
508
|
-
stages,
|
|
509
|
-
specs,
|
|
466
|
+
nextStep: null,
|
|
467
|
+
progress: { stages: {} },
|
|
468
|
+
stages: [],
|
|
469
|
+
specs: [],
|
|
510
470
|
lastActive
|
|
511
471
|
}
|
|
512
472
|
}
|
package/src/index.js
CHANGED
|
@@ -145,6 +145,21 @@ async function main() {
|
|
|
145
145
|
targetDir = resolve(filteredArgs[1]);
|
|
146
146
|
filteredArgs.splice(1, 1);
|
|
147
147
|
}
|
|
148
|
+
// ── 自动纠正 cwd ──
|
|
149
|
+
// 当 agent 在 worktree 内跑 pnpm 等工具后 shell cwd 可能被改变,
|
|
150
|
+
// 导致 sillyspec 命令找不到 .sillyspec。此函数尝试从 git root 解析。
|
|
151
|
+
function resolveEffectiveDir(baseDir) {
|
|
152
|
+
if (existsSync(join(baseDir, '.sillyspec'))) return baseDir
|
|
153
|
+
try {
|
|
154
|
+
const { execSync } = require('child_process')
|
|
155
|
+
const gitRoot = execSync('git rev-parse --show-toplevel', {
|
|
156
|
+
cwd: baseDir, encoding: 'utf8', timeout: 5000
|
|
157
|
+
}).trim()
|
|
158
|
+
if (gitRoot && existsSync(join(gitRoot, '.sillyspec'))) return gitRoot
|
|
159
|
+
} catch {}
|
|
160
|
+
return baseDir
|
|
161
|
+
}
|
|
162
|
+
|
|
148
163
|
const dir = targetDir;
|
|
149
164
|
|
|
150
165
|
if (command === 'init' && !existsSync(dir)) {
|
|
@@ -167,6 +182,7 @@ async function main() {
|
|
|
167
182
|
break;
|
|
168
183
|
case 'progress': {
|
|
169
184
|
const pm = new ProgressManager();
|
|
185
|
+
const progDir = resolveEffectiveDir(dir);
|
|
170
186
|
const subCommand = filteredArgs[1];
|
|
171
187
|
const stageIdx = filteredArgs.indexOf('--stage');
|
|
172
188
|
const stage = stageIdx >= 0 && filteredArgs[stageIdx + 1] ? filteredArgs[stageIdx + 1] : null;
|
|
@@ -176,18 +192,18 @@ async function main() {
|
|
|
176
192
|
|
|
177
193
|
switch (subCommand) {
|
|
178
194
|
case 'init':
|
|
179
|
-
pm.init(
|
|
195
|
+
pm.init(progDir);
|
|
180
196
|
break;
|
|
181
197
|
case 'status':
|
|
182
198
|
case 'show':
|
|
183
|
-
pm.show(
|
|
199
|
+
pm.show(progDir, progChangeName);
|
|
184
200
|
break;
|
|
185
201
|
case 'check':
|
|
186
|
-
await pm.checkConsistency(
|
|
202
|
+
await pm.checkConsistency(progDir, progChangeName);
|
|
187
203
|
break;
|
|
188
204
|
case 'repair': {
|
|
189
205
|
const repairApply = filteredArgs.includes('--apply');
|
|
190
|
-
await pm.repairConsistency(
|
|
206
|
+
await pm.repairConsistency(progDir, { apply: repairApply, changeName: progChangeName });
|
|
191
207
|
break;
|
|
192
208
|
}
|
|
193
209
|
case 'validate':
|
|
@@ -270,7 +286,22 @@ async function main() {
|
|
|
270
286
|
}
|
|
271
287
|
case 'run': {
|
|
272
288
|
const { runCommand } = await import('./run.js')
|
|
273
|
-
await runCommand(filteredArgs.slice(1), dir, specDir)
|
|
289
|
+
await runCommand(filteredArgs.slice(1), resolveEffectiveDir(dir), specDir)
|
|
290
|
+
break
|
|
291
|
+
}
|
|
292
|
+
// task-10: 顶层命令别名,转发 runCommand,与 case 'run': 路径行为一致
|
|
293
|
+
// help 文本(:44-46)已宣称这些 stage 可直接使用,这里补齐路由避免落 default 分支。
|
|
294
|
+
// 注意:filteredArgs[0] === command,直接透传 filteredArgs 即可让 runCommand
|
|
295
|
+
// 从 args[0] 取到 stage 名(run.js:1036)。与 case 'run': 的 filteredArgs.slice(1)
|
|
296
|
+
// 区别只在于 slice(1) 去掉的是 'run' 字面量,这里 command 本身就是 stage 名不能丢。
|
|
297
|
+
case 'doctor':
|
|
298
|
+
case 'scan':
|
|
299
|
+
case 'status':
|
|
300
|
+
case 'quick':
|
|
301
|
+
case 'explore': {
|
|
302
|
+
const { runCommand } = await import('./run.js')
|
|
303
|
+
const stageArgs = [command, ...filteredArgs.slice(1)]
|
|
304
|
+
await runCommand(stageArgs, resolveEffectiveDir(dir), specDir)
|
|
274
305
|
break
|
|
275
306
|
}
|
|
276
307
|
case 'dashboard': {
|
package/src/run.js
CHANGED
|
@@ -11,6 +11,22 @@ const require = createRequire(import.meta.url)
|
|
|
11
11
|
import { ProgressManager } from './progress.js'
|
|
12
12
|
import { SCAN_STATUS, POINTER_STATUS, isPointerCorrupted } from './constants.js'
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* 清洗项目名:只保留 ASCII 字母/数字/横线/下划线/点,过滤中文和特殊字符。
|
|
16
|
+
* - 必须含至少一个字母(拒绝纯数字 "0"/"7"/"07",避免 scan-projects.json 脏数据)
|
|
17
|
+
* - 长度必须 ≥ 2(拒绝单字符 "a"/"0")
|
|
18
|
+
* @param {string} name - 原始项目名候选
|
|
19
|
+
* @returns {string | null} 合法项目名或 null(拒绝)
|
|
20
|
+
*/
|
|
21
|
+
export function sanitizeProjectName(name) {
|
|
22
|
+
if (!name) return null
|
|
23
|
+
const clean = String(name).replace(/[^a-zA-Z0-9_\-.]/g, '').trim()
|
|
24
|
+
if (!clean) return null
|
|
25
|
+
if (!/[a-zA-Z]/.test(clean)) return null // 纯数字/符号拒绝("0"/"7"/"07")
|
|
26
|
+
if (clean.length < 2) return null // 单字符拒绝("a"/"0")
|
|
27
|
+
return clean
|
|
28
|
+
}
|
|
29
|
+
|
|
14
30
|
/**
|
|
15
31
|
* 在容器/Docker 环境下,git 可能因目录所有权不匹配报 dubious ownership。
|
|
16
32
|
* 使用 -c safe.directory= 临时参数,不污染全局 git config。
|
|
@@ -666,6 +682,12 @@ async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjec
|
|
|
666
682
|
`2. **不允许**用 cat >、tee、heredoc 等 Bash 方式绕过 Write 工具。\n` +
|
|
667
683
|
`3. 如果 Write 和 Read 均失败,记录失败并停止当前 step。\n` +
|
|
668
684
|
`\n` +
|
|
685
|
+
`### 📍 Workflow YAML 占位符映射(task-05)\n` +
|
|
686
|
+
`读取 \`{WORKFLOWS_ROOT}/scan-docs.yaml\` 时,yaml 内的占位符按以下映射替换为绝对路径:\n` +
|
|
687
|
+
`- \`{SPEC_ROOT}\` → \`${specSillyspec}\`(规范目录根)\n` +
|
|
688
|
+
`- \`<project>\` → 当前项目名(见下方 step 提示,等于 \`${projectName}\`)\n` +
|
|
689
|
+
`- 例:\`{SPEC_ROOT}/docs/<project>/scan/ARCHITECTURE.md\` → \`${docsRoot}/scan/ARCHITECTURE.md\`\n` +
|
|
690
|
+
`\n` +
|
|
669
691
|
`创建目录: \`mkdir -p ${docsRoot}/{scan,modules,flows} ${projectsRoot} ${changesRoot}\`\n`
|
|
670
692
|
)
|
|
671
693
|
if (platformOpts.runtimeRoot) {
|
|
@@ -1416,7 +1438,9 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
|
|
|
1416
1438
|
const specBase = platformOpts.specRoot || join(cwd, '.sillyspec')
|
|
1417
1439
|
// 状态转换校验
|
|
1418
1440
|
const prevStage = progress.currentStage || ''
|
|
1419
|
-
|
|
1441
|
+
// task-07: 提取 prevStage 的 stageData,传给 checkTransition 检测 failed_post_check 门控
|
|
1442
|
+
const fromStageData = (progress.stages && prevStage && progress.stages[prevStage]) || undefined
|
|
1443
|
+
const transition = checkTransition(prevStage, stageName, fromStageData ? { fromStageData } : {})
|
|
1420
1444
|
if (!transition.allowed) {
|
|
1421
1445
|
console.error(`❌ 阶段转换不允许: ${prevStage || '(起始)'} → ${stageName}`)
|
|
1422
1446
|
console.error(` 原因: ${transition.reason}`)
|
|
@@ -2150,16 +2174,15 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
2150
2174
|
if (stageName === 'scan' && steps[currentIdx]?.name === '构建扫描项目列表') {
|
|
2151
2175
|
// 解析项目列表:从 step 2 输出提取,或回退读取 projects/*.yaml
|
|
2152
2176
|
let projectNames = []
|
|
2153
|
-
//
|
|
2154
|
-
const sanitizeProjectName = (name) => {
|
|
2155
|
-
const clean = name.replace(/[^a-zA-Z0-9_\-.]/g, '').trim()
|
|
2156
|
-
return clean || null
|
|
2157
|
-
}
|
|
2177
|
+
// sanitizeProjectName 已提取到模块顶层(含字母校验 + 长度≥2)
|
|
2158
2178
|
if (outputText) {
|
|
2159
2179
|
// 匹配方式 1: "1. project-name" 编号列表
|
|
2160
|
-
|
|
2180
|
+
// 正则收紧:token 必须以字母开头,避免误捕获纯数字 "0"/"7" 和步骤说明中英文行
|
|
2181
|
+
const numbered = outputText.match(/^\s*\d+\.\s+([a-zA-Z][\w\-.]*)/gm)
|
|
2161
2182
|
if (numbered) {
|
|
2162
|
-
|
|
2183
|
+
// task-05 B2 延伸修正:原 /[—\-:].*$/ 会把 ASCII 连字符当后缀分隔符,
|
|
2184
|
+
// 把 order-service 切成 order。现只针对中文长破折号 `—`(LLM 输出列表时常作分隔符)。
|
|
2185
|
+
const raw = numbered.map(m => m.replace(/^\s*\d+\.\s+/, '').replace(/—.*$/, '').trim())
|
|
2163
2186
|
projectNames = raw.map(sanitizeProjectName).filter(Boolean)
|
|
2164
2187
|
if (projectNames.length > 0) { stageData.scanMeta = stageData.scanMeta || {}; stageData.scanMeta.projectListParsed = true; }
|
|
2165
2188
|
}
|
|
@@ -2434,8 +2457,18 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
2434
2457
|
stageData.status = SCAN_STATUS.FAILED_POST_CHECK
|
|
2435
2458
|
stageData.completedAt = new Date().toLocaleString('zh-CN',{hour12:false})
|
|
2436
2459
|
await pm._write(cwd, progress, changeName)
|
|
2460
|
+
triggerSync(cwd, changeName, platformOpts)
|
|
2437
2461
|
console.error(`\n❌ scan post-check 失败,状态设为 failed_post_check。不允许 clean success。`)
|
|
2438
2462
|
console.error(` 请检查上方错误信息并修复后重新 scan。`)
|
|
2463
|
+
// 平台模式:exit(1) 让 daemon/SillyHub 感知非 0 退出码(manifest.json 已落盘,不会被撤销)
|
|
2464
|
+
if (platformOpts.specRoot || platformOpts.runtimeRoot) {
|
|
2465
|
+
console.error(' 平台模式:CLI 将以 exit code 1 退出,通知 SillyHub scan 失败。')
|
|
2466
|
+
process.exit(1)
|
|
2467
|
+
}
|
|
2468
|
+
// 接口与 plan contract (run.js:2551 附近 plan 失败分支) 对齐:
|
|
2469
|
+
// 返回 { stageCompleted:false, currentIdx, nextPendingIdx: currentIdx }
|
|
2470
|
+
// 让上层 runStage 走"完成但不推进"分支,--done 被拒
|
|
2471
|
+
return { stageCompleted: false, currentIdx, nextPendingIdx: currentIdx }
|
|
2439
2472
|
} else if (postResult.status === 'completed_with_warnings') {
|
|
2440
2473
|
// 警告不阻止完成,但记录
|
|
2441
2474
|
stageData.status = 'completed'
|
|
@@ -2623,8 +2656,17 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
2623
2656
|
const { loadWorkflow, runPostCheck, formatCheckReport, saveWorkflowRun } = await import('./workflow.js')
|
|
2624
2657
|
const wf = loadWorkflow(cwd, 'scan-docs')
|
|
2625
2658
|
if (wf) {
|
|
2626
|
-
//
|
|
2627
|
-
|
|
2659
|
+
// 确定当前项目(优先级链):
|
|
2660
|
+
// progress.project (dbProjectName,平台模式真实项目名,与 outputStep 占位符渲染对齐)
|
|
2661
|
+
// > change?.project (变更对象的项目字段,平台模式 change 创建时传入)
|
|
2662
|
+
// > steps[idx].project (perProject 展开标记,兼容旧模式)
|
|
2663
|
+
// > steps[idx].name 正则提取 [xxx] 后缀
|
|
2664
|
+
// > null(回退检查所有项目)
|
|
2665
|
+
// task-05 修复:日志显示项目名变 frontend 是 perProject 误展开 bug,
|
|
2666
|
+
// 用 progress.project(与 outputStep 占位符渲染路径一致)修正 myaaa/frontend 分裂。
|
|
2667
|
+
const currentProjectName = progress.project
|
|
2668
|
+
|| (typeof change !== 'undefined' && change ? change.project : null)
|
|
2669
|
+
|| steps[currentIdx].project
|
|
2628
2670
|
|| (steps[currentIdx].name.match(/\[([^\]]+)\]\s*$/) || [])[1]
|
|
2629
2671
|
|| null
|
|
2630
2672
|
|
|
@@ -2644,7 +2686,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
2644
2686
|
|
|
2645
2687
|
let anyFailed = false
|
|
2646
2688
|
for (const pName of projectsToCheck) {
|
|
2647
|
-
const result = runPostCheck(wf, cwd, pName)
|
|
2689
|
+
const result = runPostCheck(wf, cwd, pName, {}, specBase)
|
|
2648
2690
|
const report = formatCheckReport(result)
|
|
2649
2691
|
console.log(report)
|
|
2650
2692
|
if (result.status === 'fail') {
|
|
@@ -2667,6 +2709,10 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
2667
2709
|
}
|
|
2668
2710
|
if (anyFailed) {
|
|
2669
2711
|
console.log(`\n⚠️ 存在检查失败项,请按上面的重试提示修复后再继续。`)
|
|
2712
|
+
// task-07: 阻断推进(与 task-06 平台模式 scan-postcheck 失败分支 return 结构对齐)
|
|
2713
|
+
// scan 深度扫描产物校验未通过时,不允许 clean success / 进入下一 step,
|
|
2714
|
+
// 让上层走"完成但不推进"分支,--done 被拒。
|
|
2715
|
+
return { stageCompleted: false, currentIdx, nextPendingIdx: currentIdx }
|
|
2670
2716
|
}
|
|
2671
2717
|
}
|
|
2672
2718
|
} catch (e) {
|
|
@@ -2682,7 +2728,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
|
|
|
2682
2728
|
if (wf && changeName) {
|
|
2683
2729
|
const raw = JSON.stringify(wf)
|
|
2684
2730
|
const resolved = JSON.parse(raw.replace(/<change-name>/g, changeName))
|
|
2685
|
-
const result = runPostCheck(resolved, cwd, 'sillyspec')
|
|
2731
|
+
const result = runPostCheck(resolved, cwd, 'sillyspec', {}, specBase)
|
|
2686
2732
|
// 只报告 impact-analyzer 的结果(doc-syncer 是后续步骤)
|
|
2687
2733
|
const impactResult = (result.roles || []).find(r => r.id === 'impact-analyzer')
|
|
2688
2734
|
if (impactResult) {
|
package/src/stage-contract.js
CHANGED
|
@@ -587,9 +587,11 @@ export function getContract(stageName) {
|
|
|
587
587
|
* 校验状态转换是否允许
|
|
588
588
|
* @param {string} fromStage - 当前阶段(空字符串表示变更起始)
|
|
589
589
|
* @param {string} toStage - 目标阶段
|
|
590
|
+
* @param {{ fromStageData?: { status?: string } | undefined }} [options] - 可选,从 progress.stages[prevStage] 提取
|
|
590
591
|
* @returns {{ allowed: boolean, reason?: string }}
|
|
591
592
|
*/
|
|
592
|
-
export function checkTransition(fromStage, toStage) {
|
|
593
|
+
export function checkTransition(fromStage, toStage, options = {}) {
|
|
594
|
+
const { fromStageData } = options // { status?: string } | undefined
|
|
593
595
|
const contract = contracts[toStage]
|
|
594
596
|
if (!contract) {
|
|
595
597
|
return { allowed: false, reason: `未知阶段: ${toStage}` }
|
|
@@ -605,6 +607,17 @@ export function checkTransition(fromStage, toStage) {
|
|
|
605
607
|
return { allowed: true }
|
|
606
608
|
}
|
|
607
609
|
|
|
610
|
+
// task-07: failed_post_check 门控
|
|
611
|
+
// scan post-check 未通过时,禁止进入主流程的下游阶段(brainstorm/plan/execute/verify/archive)
|
|
612
|
+
// 必须先重跑 scan 修复。toStage === 'scan' 的重跑路径已被上方 fromStage === toStage 放行。
|
|
613
|
+
// fromStageData.status 缺失(旧数据)时门控不触发(向后兼容)。
|
|
614
|
+
if (fromStage === 'scan' && fromStageData?.status === 'failed_post_check' && toStage !== 'scan') {
|
|
615
|
+
return {
|
|
616
|
+
allowed: false,
|
|
617
|
+
reason: 'scan post-check 未通过(failed_post_check),需修复后重跑 scan 再进入 ' + toStage,
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
608
621
|
// archive 特殊处理:从 verify 来的允许,从其他主流程阶段来的需要校验
|
|
609
622
|
if (toStage === 'archive') {
|
|
610
623
|
if (fromStage === 'verify') {
|
package/src/stages/execute.js
CHANGED
|
@@ -165,6 +165,7 @@ const fixedPrefix = [
|
|
|
165
165
|
- **worktree 已由 CLI 在 execute 阶段启动时自动创建,不要自行创建或跳过**
|
|
166
166
|
- **后续所有子代理的 cwd 必须设为该 worktree 路径**
|
|
167
167
|
- 如果 meta.json 不存在(说明创建失败),停止并报错
|
|
168
|
+
- **不要自行检查 git dirty/uncommitted 状态来判断是否可以进入 worktree,CLI 已自动处理**
|
|
168
169
|
|
|
169
170
|
### 输出
|
|
170
171
|
worktree 路径 + 分支名 + 模式
|
package/src/workflow.js
CHANGED
|
@@ -149,10 +149,13 @@ function replaceProjectPlaceholder(wf, projectName) {
|
|
|
149
149
|
* @param {string} cwd - 项目根目录
|
|
150
150
|
* @returns {CheckResult}
|
|
151
151
|
*/
|
|
152
|
-
function checkOutput(outputDef, projectName, cwd) {
|
|
152
|
+
function checkOutput(outputDef, projectName, cwd, specBase) {
|
|
153
|
+
// specBase 优先(平台模式 platformOpts.specRoot,已含或不含 .sillyspec 语义);
|
|
154
|
+
// 未传时回退 join(cwd, '.sillyspec'),等价于旧行为 resolve(cwd, '.sillyspec/...')
|
|
155
|
+
const effectiveBase = specBase || join(cwd, '.sillyspec')
|
|
153
156
|
// 将 <project> 替换为实际项目名
|
|
154
157
|
const rawPath = (outputDef.path || '').replace(/<project>/g, projectName)
|
|
155
|
-
const fullPath = resolve(
|
|
158
|
+
const fullPath = resolve(effectiveBase, rawPath)
|
|
156
159
|
const checks = outputDef.checks || []
|
|
157
160
|
const results = []
|
|
158
161
|
|
|
@@ -241,7 +244,7 @@ function checkOutput(outputDef, projectName, cwd) {
|
|
|
241
244
|
* retry_prompts: [{ role_id, role_name, prompt }]
|
|
242
245
|
* }
|
|
243
246
|
*/
|
|
244
|
-
export function runPostCheck(wf, cwd, projectName, placeholders = {}) {
|
|
247
|
+
export function runPostCheck(wf, cwd, projectName, placeholders = {}, specBase) {
|
|
245
248
|
let resolved = replaceProjectPlaceholder(wf, projectName)
|
|
246
249
|
if (Object.keys(placeholders).length > 0) {
|
|
247
250
|
let json = JSON.stringify(resolved)
|
|
@@ -250,10 +253,11 @@ export function runPostCheck(wf, cwd, projectName, placeholders = {}) {
|
|
|
250
253
|
}
|
|
251
254
|
resolved = JSON.parse(json)
|
|
252
255
|
}
|
|
253
|
-
return _checkWorkflow(resolved, cwd, projectName)
|
|
256
|
+
return _checkWorkflow(resolved, cwd, projectName, specBase)
|
|
254
257
|
}
|
|
255
258
|
|
|
256
|
-
function _checkWorkflow(wf, cwd, projectName) {
|
|
259
|
+
function _checkWorkflow(wf, cwd, projectName, specBase) {
|
|
260
|
+
const effectiveBase = specBase || join(cwd, '.sillyspec')
|
|
257
261
|
const workflowName = wf.name || 'unknown'
|
|
258
262
|
const specVersion = wf.spec_version || wf.version || 0
|
|
259
263
|
const workflowChecks = wf.checks?.workflow_level || []
|
|
@@ -270,7 +274,7 @@ function _checkWorkflow(wf, cwd, projectName) {
|
|
|
270
274
|
|
|
271
275
|
for (const outputDef of outputDefs) {
|
|
272
276
|
const rawPath = (outputDef.path || '').replace(/<project>/g, projectName)
|
|
273
|
-
const checkResults = checkOutput(outputDef, projectName, cwd)
|
|
277
|
+
const checkResults = checkOutput(outputDef, projectName, cwd, effectiveBase)
|
|
274
278
|
const outputPassed = checkResults.every(c => c.passed)
|
|
275
279
|
|
|
276
280
|
outputs.push({
|
|
@@ -309,7 +313,7 @@ function _checkWorkflow(wf, cwd, projectName) {
|
|
|
309
313
|
for (const check of workflowChecks) {
|
|
310
314
|
switch (check.type) {
|
|
311
315
|
case 'file_count': {
|
|
312
|
-
const scanDir = join(
|
|
316
|
+
const scanDir = join(effectiveBase, 'docs', projectName, check.path || 'scan/')
|
|
313
317
|
if (existsSync(scanDir)) {
|
|
314
318
|
const files = readdirSync(scanDir).filter(f => f.endsWith('.md'))
|
|
315
319
|
const min = check.min || 0
|
|
@@ -328,7 +332,7 @@ function _checkWorkflow(wf, cwd, projectName) {
|
|
|
328
332
|
break
|
|
329
333
|
}
|
|
330
334
|
case 'no_empty_files': {
|
|
331
|
-
const scanDir = join(
|
|
335
|
+
const scanDir = join(effectiveBase, 'docs', projectName, check.path || 'scan/')
|
|
332
336
|
if (existsSync(scanDir)) {
|
|
333
337
|
const files = readdirSync(scanDir).filter(f => f.endsWith('.md'))
|
|
334
338
|
let anyEmpty = false
|
package/src/worktree-apply.js
CHANGED
|
@@ -214,7 +214,7 @@ export function applyWorktree(changeName, { cwd, checkOnly = false } = {}) {
|
|
|
214
214
|
const patchFiles = hasAllowList
|
|
215
215
|
? [...allowSet].filter(f => changedFiles.includes(f))
|
|
216
216
|
: changedFiles;
|
|
217
|
-
const fileArgs = patchFiles.
|
|
217
|
+
const fileArgs = patchFiles.length > 0 ? `-- ${patchFiles.join(' ')}` : '';
|
|
218
218
|
|
|
219
219
|
// 创建临时文件
|
|
220
220
|
const tmpDir = mkdtempSync(join(tmpdir(), 'sillyspec-patch-'));
|
|
@@ -236,7 +236,7 @@ export function applyWorktree(changeName, { cwd, checkOnly = false } = {}) {
|
|
|
236
236
|
|
|
237
237
|
// tracked 文件:git diff baseHash
|
|
238
238
|
if (trackedFiles.length > 0) {
|
|
239
|
-
const trackedArgs = trackedFiles.
|
|
239
|
+
const trackedArgs = trackedFiles.length > 0 ? `-- ${trackedFiles.join(' ')}` : '';
|
|
240
240
|
patchContent += execSync(
|
|
241
241
|
`git diff --binary ${diffBase} ${trackedArgs}`,
|
|
242
242
|
{ cwd: worktreePath, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
@@ -245,11 +245,12 @@ export function applyWorktree(changeName, { cwd, checkOnly = false } = {}) {
|
|
|
245
245
|
|
|
246
246
|
// untracked 新文件:git add 到 index,git diff --cached,然后 reset
|
|
247
247
|
if (untrackedPatchFiles.length > 0) {
|
|
248
|
-
const addArgs = untrackedPatchFiles.
|
|
248
|
+
const addArgs = untrackedPatchFiles.length > 0 ? `-- ${untrackedPatchFiles.join(' ')}` : '';
|
|
249
249
|
git(worktreePath, `add ${addArgs}`);
|
|
250
250
|
try {
|
|
251
|
+
const diffCachedArgs = untrackedPatchFiles.length > 0 ? `-- ${untrackedPatchFiles.join(' ')}` : '';
|
|
251
252
|
patchContent += execSync(
|
|
252
|
-
`git diff --binary --cached ${
|
|
253
|
+
`git diff --binary --cached ${diffCachedArgs}`,
|
|
253
254
|
{ cwd: worktreePath, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
254
255
|
);
|
|
255
256
|
} finally {
|