sillyspec 3.25.4 → 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 +27 -1
- package/.claude/skills/sillyspec-execute/SKILL.md +56 -0
- package/.claude/skills/sillyspec-plan/SKILL.md +26 -0
- package/.claude/skills/sillyspec-propose/SKILL.md +26 -0
- 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 +114 -12
- package/src/change-risk-profile.js +36 -1
- package/src/constants.js +11 -0
- package/src/index.js +4 -1
- package/src/machine-interface.js +5 -1
- package/src/progress/stage-machine.js +17 -14
- package/src/run/command.js +58 -3
- package/src/run/complete-handlers.js +9 -3
- package/src/run/complete.js +130 -33
- package/src/run/gates.js +37 -0
- package/src/run/prompt.js +20 -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 +39 -8
- package/src/stage-review.js +16 -5
- package/src/stages/archive.js +13 -9
- package/src/stages/brainstorm-auto.js +3 -2
- package/src/stages/brainstorm.js +13 -16
- package/src/stages/execute.js +4 -3
- package/src/stages/plan-postcheck.js +1 -1
- package/src/stages/plan.js +13 -0
- package/src/stages/propose.js +3 -2
- package/src/stages/quick.js +31 -31
- package/src/stages/scan.js +2 -1
- package/src/stages/status.js +2 -2
- package/src/stages/verify.js +19 -8
- package/src/task-review.js +308 -3
- package/src/verify-postcheck.js +353 -38
- package/src/worktree-apply.js +134 -67
- package/src/worktree.js +106 -32
- 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
|
|
@@ -75,6 +75,32 @@ brainstorm 完成时按 design.md frontmatter 的 `scale` 分叉:
|
|
|
75
75
|
|
|
76
76
|
> 规模由 AI 在 brainstorm 最后一步评估并写入 design.md frontmatter。判错可手动改 `scale` 后再跑相应阶段。
|
|
77
77
|
|
|
78
|
+
### Stage Review Gate(brainstorm 末尾的 design review.json)
|
|
79
|
+
|
|
80
|
+
brainstorm 在 `tier=independent` 规模下除 design.md 等四件套外,还需产出一个 stage 级 `review.json`,CLI `Stage Review Gate` 硬校验其 schema 与 `docHash` 真实性。
|
|
81
|
+
|
|
82
|
+
- 路径:`.sillyspec/.runtime/stage-reviews/brainstorm-review-<stage-review-run-id>/review.json`(目录可能不存在需手建;run-id 由该步 `--done` prompt 输出指定)。marker 文件 `.runtime/current-stage-review-run-id-brainstorm-<变更名>`。
|
|
83
|
+
- 字段(`schemaVersion:1`,`reviewType=design` —— 区别于 execute 的 `acceptance`):
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{
|
|
87
|
+
"schemaVersion": 1,
|
|
88
|
+
"reviewType": "design",
|
|
89
|
+
"reviewedFiles": ["changes/<变更名>/design.md"], // [0] 为主文档
|
|
90
|
+
"docHash": "<sha256(reviewedFiles[0] 文件内容,hex)>",
|
|
91
|
+
"specVerdict": "pass", // pass | fail | cannot_verify
|
|
92
|
+
"qualityVerdict": "pass", // pass | fail | cannot_verify
|
|
93
|
+
"checklist": [ // 扁平数组,逐条对照 design 章节核验
|
|
94
|
+
{ "item": "背景与目标", "result": "pass", "note": "..." } // result: pass | gap | fail
|
|
95
|
+
],
|
|
96
|
+
"requiredEvidence": [], // cannot_verify 时必填非空
|
|
97
|
+
"reviewerNotes": "..."
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
- `docHash` = `sha256(主审查文档内容)`(hex)—— brainstorm 主文档是 `design.md`(即 `reviewedFiles[0]`)。CLI 重算 sha256 比对,不符判伪造(fail-closed)。改 design.md 后须重算 docHash。`tier=self`(≤3 文件)降级为当前 agent 自审,不强制独立子代理。
|
|
102
|
+
- 运行时 CLI 通过 `{REVIEW_JSON_CONTRACT}` 注入精确 schema+示例,以注入版为权威逐字模板。
|
|
103
|
+
|
|
78
104
|
## 铁律
|
|
79
105
|
|
|
80
106
|
- **必须用 exec 工具(shell)执行 CLI,不要自己编造流程**
|
|
@@ -60,6 +60,58 @@ sillyspec worktree doctor --fix --change <变更名>
|
|
|
60
60
|
|
|
61
61
|
execute 完成时,每个 task 必须有 `review.json` 且 verdict 通过,否则阻断完成。**例外**:task 在 `tasks/task-XX.md` frontmatter 声明 `low_risk: true`(type-only / 机械迁移等低逻辑风险)时,缺 review.json 只发 warning 不阻断。`cannot_verify` 的 task 会写入 `verify-required-evidence.json`,由 verify 阶段消费。
|
|
62
62
|
|
|
63
|
+
#### task 级 review.json 契约(CLI 硬校验)
|
|
64
|
+
|
|
65
|
+
- 路径:`.sillyspec/.runtime/execute-runs/<execute-run-id>/tasks/task-XX/review.json`(目录不存在需先建)。
|
|
66
|
+
- `execute-run-id` 取自**第一次** `--done` prompt 输出(形如 `exec-2026-07-28-112833`),也写在 marker 文件 `.runtime/current-execute-run-id-<变更名>`。多个 run 时 gate 读最新 marker。
|
|
67
|
+
- 字段(`schemaVersion:1`):
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"schemaVersion": 1,
|
|
72
|
+
"task": "task-01",
|
|
73
|
+
"base": "<git-base-commit-hash>",
|
|
74
|
+
"head": "<git-head-commit-hash>",
|
|
75
|
+
"changedFiles": ["src/..."],
|
|
76
|
+
"specVerdict": "pass", // pass | fail | cannot_verify
|
|
77
|
+
"qualityVerdict": "pass", // pass | fail | cannot_verify
|
|
78
|
+
"reviewerNotes": "...",
|
|
79
|
+
"requiredEvidence": [] // cannot_verify 时必须非空
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
- `base` / `head` 必须是**真实 git commit**(`git rev-parse --verify`),否则判伪造并阻断。`changedFiles` 与实际 `git diff base..head` 完全不相交也判伪造。base..head 空 commit diff 但 working-tree 有未提交改动 → 视为有效改动(warning 不阻断)。
|
|
84
|
+
- 后端 router task 另需在 `.sillyspec/.runtime/contract-artifacts/<task-name>/endpoints.json` 写 API 端点清单(扫 `@router.get/post/...`)。
|
|
85
|
+
|
|
86
|
+
### Stage Review Gate(execute 末尾的 acceptance review.json)
|
|
87
|
+
|
|
88
|
+
execute 还有**第二道**独立的 stage 级审查:除逐 task review.json 外,整个 execute 阶段完成还需一个 stage 级 `review.json`(在 "完成确认"/acceptance 步骤产出)。CLI `Stage Review Gate` 硬校验其 schema 与 `docHash` 真实性。
|
|
89
|
+
|
|
90
|
+
- 路径:`.sillyspec/.runtime/stage-reviews/execute-review-<stage-review-run-id>/review.json`(目录可能不存在需手建;run-id 由该步 `--done` prompt 输出指定)。marker 文件 `.runtime/current-stage-review-run-id-execute-<变更名>`。
|
|
91
|
+
- 字段(`schemaVersion:1`,`reviewType=acceptance` —— 区别于 brainstorm/plan/propose 的 `"design"`):
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"schemaVersion": 1,
|
|
96
|
+
"reviewType": "acceptance",
|
|
97
|
+
"reviewedFiles": ["changes/<变更名>/design.md", "<可选追加 git diff 涉及的源码>"],
|
|
98
|
+
"docHash": "<sha256(reviewedFiles[0] 文件内容,hex)>",
|
|
99
|
+
"specVerdict": "pass", // pass | fail | cannot_verify
|
|
100
|
+
"qualityVerdict": "pass", // pass | fail | cannot_verify
|
|
101
|
+
"checklist": [ // 扁平数组(非按层嵌套!),逐条对照 design 章节 + FR/NFR/决策核验代码
|
|
102
|
+
{ "item": "FR-01 排序", "result": "pass", "note": "..." } // result: pass | gap | fail
|
|
103
|
+
],
|
|
104
|
+
"requiredEvidence": [], // cannot_verify 时必填非空
|
|
105
|
+
"reviewerNotes": "..."
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
- `docHash` = `sha256(主审查文档内容)`(hex)—— execute 主审查文档是 `design.md`,即 `reviewedFiles[0]`。CLI 会重算 sha256 比对,不符判伪造(fail-closed);找不到主文档也 fail。**改 design.md 后须重算 docHash 再写**,可用 `sillyspec run execute --done` 触发的 prompt 注入版契约(`{REVIEW_JSON_CONTRACT}`)逐字改值。
|
|
110
|
+
- `tier=independent` 时必须由独立 QA 子代理产出该 review.json(独立上下文,不共享实现者分析);`tier=self`(变更 ≤3 文件)降级为当前 agent 自审。
|
|
111
|
+
- 该 acceptance review 同时覆盖"代码审查"视角,后续代码审查步骤仅需轻量复审。
|
|
112
|
+
|
|
113
|
+
> 运行时 CLI 会把精确 schema 表 + 完整 JSON 示例 + docHash 算法通过 `{REVIEW_JSON_CONTRACT}` 占位符注入到该步 prompt(源码 `src/stage-review.js: renderReviewJsonContract()`)。本段为常驻摘要;以运行时注入的契约为权威逐字模板。
|
|
114
|
+
|
|
63
115
|
## worktree 子命令(execute 相关)
|
|
64
116
|
|
|
65
117
|
```bash
|
|
@@ -80,6 +132,10 @@ plan → execute → verify
|
|
|
80
132
|
|
|
81
133
|
execute 完成后(所有 Wave/task 完成 + Task Review Gate 通过),运行 `sillyspec run verify --change <变更名>` 验证。
|
|
82
134
|
|
|
135
|
+
### 批量完成(一次 --done 收尾)
|
|
136
|
+
|
|
137
|
+
当 `plan.md` 所有 task checkbox 已勾(人工勾或基于各 task `review.json` pass 由 CLI 自动勾)且代码客观核验通过(`checkExecuteCodeEvidence` 非"零变更")时,任一 execute `--done` 会**一次性补完所有剩余 step** 直达阶段完成,不必逐次 +1 推进。日志会打印 `🚀 execute 批量完成:plan 全勾 + 代码核验通过,一次性补完 N 个剩余 step`。条件不满足(plan 未全勾 / 代码零变更)时仍按单步推进,要求补 review 后重跑 `--done` 直至满足。
|
|
138
|
+
|
|
83
139
|
## 铁律
|
|
84
140
|
|
|
85
141
|
- **必须用 exec 工具(shell)执行 CLI,不要自己编造流程**
|
|
@@ -46,6 +46,32 @@ sillyspec run plan --done --answer "..." --output "..." # 一步完成 wait+d
|
|
|
46
46
|
|
|
47
47
|
plan 的步骤是动态的:`generate_plan`(生成分级计划)→ `review_plan`(审查计划,按规模分级:tier=self 当前 agent 自审 / tier=independent 启动独立审查子代理产出 stage review.json,避免生成+自审同一次输出的偏差)→ CLI 从 `plan.md` 解析出 task 自动插入"任务蓝图协调器"步骤(per-task)。这是正常行为,不要手动添加。
|
|
48
48
|
|
|
49
|
+
### Stage Review Gate(plan 的 design review.json)
|
|
50
|
+
|
|
51
|
+
`review_plan` 步骤在 `tier=independent` 时产出一个 stage 级 `review.json`,CLI `Stage Review Gate` 硬校验其 schema 与 `docHash` 真实性。
|
|
52
|
+
|
|
53
|
+
- 路径:`.sillyspec/.runtime/stage-reviews/plan-review-<stage-review-run-id>/review.json`(目录可能不存在需手建;run-id 由该步 `--done` prompt 输出指定)。marker 文件 `.runtime/current-stage-review-run-id-plan-<变更名>`。
|
|
54
|
+
- 字段(`schemaVersion:1`,`reviewType=plan` —— plan 阶段主审查文档是 `plan.md`;execute 阶段才是 `acceptance`):
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"schemaVersion": 1,
|
|
59
|
+
"reviewType": "plan",
|
|
60
|
+
"reviewedFiles": ["changes/<变更名>/plan.md"], // [0] 为主文档
|
|
61
|
+
"docHash": "<sha256(reviewedFiles[0] 文件内容,hex)>",
|
|
62
|
+
"specVerdict": "pass", // pass | fail | cannot_verify
|
|
63
|
+
"qualityVerdict": "pass", // pass | fail | cannot_verify
|
|
64
|
+
"checklist": [ // 扁平数组,逐条对照 plan 章节/Wave 结构/task 完整性核验
|
|
65
|
+
{ "item": "Wave 分级合理", "result": "pass", "note": "..." } // result: pass | gap | fail
|
|
66
|
+
],
|
|
67
|
+
"requiredEvidence": [], // cannot_verify 时必填非空
|
|
68
|
+
"reviewerNotes": "..."
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
- `docHash` = `sha256(主审查文档内容)`(hex)—— plan 主文档是 `plan.md`(`reviewedFiles[0]`)。CLI 重算 sha256 比对,不符判伪造(fail-closed)。改 plan.md 后须重算 docHash。`tier=self`(≤3 文件)降级为当前 agent 自审。
|
|
73
|
+
- 运行时 CLI 通过 `{REVIEW_JSON_CONTRACT}` 注入精确 schema+示例,以注入版为权威逐字模板。
|
|
74
|
+
|
|
49
75
|
### 契约门控(阻断完成)
|
|
50
76
|
|
|
51
77
|
- **plan 启动前**:CLI 校验 `design.md` 是否满足 plan 契约(缺文件变更清单/风险登记/自审章节会阻断)。若失败需先 `sillyspec run brainstorm --reopen --from-step N` 修订设计。
|
|
@@ -42,6 +42,32 @@ sillyspec run propose --reopen --from-step N # 重新打开已完成阶段修
|
|
|
42
42
|
|
|
43
43
|
propose 含自检步骤(按规模分级:tier=self 当前 agent 自审 / tier=independent 启动独立审查子代理产出 stage review.json),完成时校验四件套文档齐全 + 内容章节(如 proposal 的 Non-Goals、design 的文件变更清单/风险登记/自审)。缺失会阻断完成。
|
|
44
44
|
|
|
45
|
+
### Stage Review Gate(propose 的 proposal review.json)
|
|
46
|
+
|
|
47
|
+
propose 在 `tier=independent` 时产出一个 stage 级 `review.json`,CLI `Stage Review Gate` 硬校验其 schema 与 `docHash` 真实性。
|
|
48
|
+
|
|
49
|
+
- 路径:`.sillyspec/.runtime/stage-reviews/propose-review-<stage-review-run-id>/review.json`(目录可能不存在需手建;run-id 由该步 `--done` prompt 输出指定)。marker 文件 `.runtime/current-stage-review-run-id-propose-<变更名>`。
|
|
50
|
+
- 字段(`schemaVersion:1`,`reviewType=proposal` —— propose 阶段主审查文档是 `proposal.md`):
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"schemaVersion": 1,
|
|
55
|
+
"reviewType": "proposal",
|
|
56
|
+
"reviewedFiles": ["changes/<变更名>/proposal.md"], // [0] 为主文档
|
|
57
|
+
"docHash": "<sha256(reviewedFiles[0] 文件内容,hex)>",
|
|
58
|
+
"specVerdict": "pass", // pass | fail | cannot_verify
|
|
59
|
+
"qualityVerdict": "pass", // pass | fail | cannot_verify
|
|
60
|
+
"checklist": [ // 扁平数组,逐条对照 proposal 章节/Non-Goals 核验
|
|
61
|
+
{ "item": "Non-Goals 明确", "result": "pass", "note": "..." } // result: pass | gap | fail
|
|
62
|
+
],
|
|
63
|
+
"requiredEvidence": [], // cannot_verify 时必填非空
|
|
64
|
+
"reviewerNotes": "..."
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
- `docHash` = `sha256(主审查文档内容)`(hex)—— propose 主文档是 `proposal.md`(`reviewedFiles[0]`)。CLI 重算 sha256 比对,不符判伪造(fail-closed)。改 proposal.md 后须重算 docHash。`tier=self`(≤3 文件)降级为当前 agent 自审。
|
|
69
|
+
- 运行时 CLI 通过 `{REVIEW_JSON_CONTRACT}` 注入精确 schema+示例,以注入版为权威逐字模板。
|
|
70
|
+
|
|
45
71
|
## 阶段流转
|
|
46
72
|
|
|
47
73
|
```
|
|
@@ -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
|
}
|
|
@@ -91,23 +128,45 @@ function isPathHeaderCell(c) {
|
|
|
91
128
|
}
|
|
92
129
|
|
|
93
130
|
/**
|
|
94
|
-
*
|
|
131
|
+
* 路径合理性兜底:合法路径必含目录分隔 `/` 或扩展名点;
|
|
132
|
+
* 两者皆无时仅保留纯 ASCII 单词 token(Dockerfile / Makefile / LICENSE 这类无扩展名文件名),
|
|
133
|
+
* 丢弃含空格/中文的脏描述(如 design 表格第二列误写的「frontend 组件测试」「后端逻辑」)。
|
|
134
|
+
* 防止把自由文本当路径计入 fileCount,虚高 review-tier 分级(误推 independent)。
|
|
135
|
+
*/
|
|
136
|
+
function looksLikePath(p) {
|
|
137
|
+
if (!p) return false
|
|
138
|
+
if (p.includes('/') || p.includes('.')) return true
|
|
139
|
+
return /^[\w@-]+$/.test(p)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
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 标记)。兼容两种真实写法:
|
|
95
152
|
* ① 表格:`| 操作 | 文件路径 | 说明 |`(brainstorm 模板默认)
|
|
96
153
|
* ② 分类列表:`### 新增文件` / `### 修改文件` / `### 不修改文件` 下的 `- path`
|
|
97
154
|
* 表头列顺序自适应(定位「文件/路径/file/path」列,列顺序写反时不会把操作名当路径);
|
|
98
155
|
* 忽略 `.sillyspec/` 与占位符(`—`/`-`/`N/A`/`无`);「不修改/暂缓」子段下的路径会被排除;
|
|
99
|
-
* CRLF 容错。
|
|
156
|
+
* CRLF 容错。incidental 嗅探:表格非路径列(说明列)+ 路径 cell 原始值(剥注释前的括号内容)+ 列表项原文。
|
|
157
|
+
*
|
|
158
|
+
* 内核函数:parseFileChangeList(Set 包装,向后兼容)与 parseFileChangeListDetailed 共用,
|
|
159
|
+
* 单一真相源,避免两处各自重写清单解析漂移。
|
|
100
160
|
* @param {string} designMdPath - design.md 文件路径
|
|
101
|
-
* @returns {
|
|
161
|
+
* @returns {Array<{ path: string, operation: string|null, incidental: boolean }>}(顺序按首次出现,exclude 子段移除)
|
|
102
162
|
*/
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (!designMdPath || !existsSync(designMdPath)) return result
|
|
163
|
+
function _parseFileListDetailed(designMdPath) {
|
|
164
|
+
if (!designMdPath || !existsSync(designMdPath)) return []
|
|
106
165
|
|
|
107
166
|
const content = readFileSync(designMdPath, 'utf8').replace(/\r\n/g, '\n')
|
|
108
167
|
|
|
109
168
|
const sectionMatch = content.match(FILE_LIST_SECTION_RE)
|
|
110
|
-
if (!sectionMatch) return
|
|
169
|
+
if (!sectionMatch) return []
|
|
111
170
|
|
|
112
171
|
// 从标题后开始,截取到下一个 ## 标题或文件末尾
|
|
113
172
|
const afterSection = content.slice(sectionMatch.index + sectionMatch[0].length)
|
|
@@ -119,13 +178,24 @@ export function parseFileChangeList(designMdPath) {
|
|
|
119
178
|
const lines = relevantContent.split('\n')
|
|
120
179
|
let headerSkipped = false
|
|
121
180
|
let pathColIdx = 1 // 默认第 2 列;解析表头后可重定位
|
|
181
|
+
let opColIdx = -1 // 操作列下标(表头扫描后定位;-1 = 无操作列 → operation=null)
|
|
122
182
|
let listMode = 'include' // include | exclude(分类列表子段)
|
|
183
|
+
let currentOp = null // 分类列表子标题推导的 operation(表格模式不用)
|
|
184
|
+
const entries = new Map() // path -> { path, operation, incidental }(exclude 子段 delete)
|
|
123
185
|
|
|
124
186
|
for (const line of lines) {
|
|
125
187
|
// 分类列表子标题:### 新增文件 / ### 修改文件 / ### 不修改文件
|
|
126
188
|
const subHeader = line.match(/^###\s+(.+?)\s*$/)
|
|
127
189
|
if (subHeader) {
|
|
128
|
-
|
|
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
|
+
}
|
|
129
199
|
continue
|
|
130
200
|
}
|
|
131
201
|
|
|
@@ -140,6 +210,9 @@ export function parseFileChangeList(designMdPath) {
|
|
|
140
210
|
// 表头列定位:找「文件/路径/file/path」列,找不到保持默认第 2 列
|
|
141
211
|
const idx = cells.findIndex(isPathHeaderCell)
|
|
142
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
|
|
143
216
|
continue
|
|
144
217
|
}
|
|
145
218
|
|
|
@@ -147,7 +220,14 @@ export function parseFileChangeList(designMdPath) {
|
|
|
147
220
|
// 列定位兜底:取到纯操作词(表头未命中且列顺序异常)→ 跳过,避免把「修改」当路径
|
|
148
221
|
if (/^(新增|修改|删除|重命名|new|modify|update|delete|create|rename)$/i.test(filePath)) continue
|
|
149
222
|
if (isPlaceholder(filePath) || filePath.startsWith('.sillyspec/')) continue
|
|
150
|
-
|
|
223
|
+
if (!looksLikePath(filePath)) continue // 脏描述兜底:丢弃非路径自由文本(防虚高 fileCount)
|
|
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 })
|
|
151
231
|
continue
|
|
152
232
|
}
|
|
153
233
|
|
|
@@ -156,10 +236,32 @@ export function parseFileChangeList(designMdPath) {
|
|
|
156
236
|
if (listItem) {
|
|
157
237
|
const filePath = normalizePath(listItem[1])
|
|
158
238
|
if (isPlaceholder(filePath) || filePath.startsWith('.sillyspec/')) continue
|
|
159
|
-
if (
|
|
160
|
-
|
|
239
|
+
if (!looksLikePath(filePath)) continue // 脏描述兜底
|
|
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 })
|
|
161
243
|
}
|
|
162
244
|
}
|
|
163
245
|
|
|
164
|
-
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)
|
|
165
267
|
}
|
|
@@ -290,14 +290,49 @@ export const VERIFICATION_NEEDS = {
|
|
|
290
290
|
export const RISK_LEVEL_CAUSES = {
|
|
291
291
|
'deployment-critical':
|
|
292
292
|
'design.md / plan.md 命中启动入口关键词(cli.ts / main.ts / server.(js|ts) / bootstrap / entrypoint)。' +
|
|
293
|
-
'注意:这是按 design/plan 里的措辞判定的,不一定代表你真改了启动入口;若属误判可在 design
|
|
293
|
+
'注意:这是按 design/plan 里的措辞判定的,不一定代表你真改了启动入口;若属误判可在 design.md frontmatter 用 risk_level 显式声明真实等级(如 unit-sufficient)覆盖——不要靠删措辞绕门控,危险链路该有真实启动证据。',
|
|
294
294
|
'integration-critical':
|
|
295
295
|
'design.md / plan.md 命中跨进程/状态机关键词(daemon / backend / session / lease / lifecycle / heartbeat 等)。',
|
|
296
296
|
'contract-required': 'design.md / plan.md 命中 API contract 关键词(api / client / contract / dto)。',
|
|
297
|
+
'explicit': 'design.md frontmatter 的 risk_level 显式声明(覆盖关键词判级)。',
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** design.md frontmatter 可显式声明的合法 risk_level 值(与 detectChangeRisk 判级结果同集合) */
|
|
301
|
+
export const RISK_LEVELS = ['doc-only', 'unit-sufficient', 'contract-required', 'integration-critical', 'deployment-critical']
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* 从 design.md 顶部 frontmatter 提取显式 risk_level 声明。
|
|
305
|
+
* 只认文档最开头 `---\n...\n---` 块内的 `risk_level: <level>` 单行(与 plan.md 的 plan_level 同款解析),
|
|
306
|
+
* 不扫正文——避免正文里讨论 risk_level 措辞时被误当声明。返回合法 level 或 null。
|
|
307
|
+
*/
|
|
308
|
+
export function extractExplicitRiskLevel(designContent = '') {
|
|
309
|
+
const fm = designContent.match(/^---\r?\n([\s\S]*?)\r?\n---/)
|
|
310
|
+
if (!fm) return null
|
|
311
|
+
const line = fm[1].split('\n').map(l => l.trim()).find(l => l.startsWith('risk_level:'))
|
|
312
|
+
if (!line) return null
|
|
313
|
+
const value = line.slice('risk_level:'.length).trim().replace(/^["']|["']$/g, '').toLowerCase()
|
|
314
|
+
return RISK_LEVELS.includes(value) ? value : null
|
|
297
315
|
}
|
|
298
316
|
|
|
299
317
|
export function detectChangeRisk({ designContent = '', planContent = '', changedFiles = [] } = {}) {
|
|
300
318
|
const triggers = []
|
|
319
|
+
|
|
320
|
+
// ── 显式豁免优先:design.md frontmatter 声明 risk_level → 以声明为准,跳过关键词判级 ──
|
|
321
|
+
// 历史教训:detectChangeRisk 是机械字面匹配,不认否定语境——design 写「本次不改动 daemon/session」
|
|
322
|
+
// 仍命中关键词被误判 integration/deployment-critical,强制要全套集成证据,对无状态后端变更误伤。
|
|
323
|
+
// 与其在正则层做脆弱的否定识别,不如给一条显式、诚实、可审计(落在 design frontmatter + verify-result)
|
|
324
|
+
// 的覆盖通道。声明仍走门控(结论 FAIL 仍拦),但豁免级(unit-sufficient 等)不再强制集成证据。
|
|
325
|
+
const explicit = extractExplicitRiskLevel(designContent)
|
|
326
|
+
if (explicit) {
|
|
327
|
+
const requiredVerification =
|
|
328
|
+
explicit === 'deployment-critical' ? ['unit_tests', 'contract_tests', 'real_daemon_backend_integration', 'runtime_log_evidence', 'real_startup_once'] :
|
|
329
|
+
explicit === 'integration-critical' ? ['unit_tests', 'contract_tests', 'real_daemon_backend_integration', 'runtime_log_evidence', 'terminal_state_assertion'] :
|
|
330
|
+
explicit === 'contract-required' ? ['unit_tests', 'contract_tests'] :
|
|
331
|
+
explicit === 'doc-only' ? ['static_check'] :
|
|
332
|
+
['unit_tests']
|
|
333
|
+
return { level: explicit, triggers: ['risk_level (explicit)'], requiredVerification, explicit: true }
|
|
334
|
+
}
|
|
335
|
+
|
|
301
336
|
const combined = [designContent, planContent].join('\n')
|
|
302
337
|
|
|
303
338
|
for (const pattern of INTEGRATION_CRITICAL_PATTERNS) {
|
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 <模式> 显式指定流程模式
|
|
@@ -1226,6 +1226,9 @@ SillySpec modules — 模块文档管理
|
|
|
1226
1226
|
lines.push('# module_paths:');
|
|
1227
1227
|
lines.push('# user-service: "user/"');
|
|
1228
1228
|
lines.push('# order-service: "order/"');
|
|
1229
|
+
lines.push('# 已知预存失败(可选):verify 实测时这些失败被豁免、不阻断归档;fail-safe 仅豁免能检测到的失败行');
|
|
1230
|
+
lines.push('# known_failures:');
|
|
1231
|
+
lines.push('# - "tests/test_legacy.py::test_old"');
|
|
1229
1232
|
const yamlText = lines.join('\n') + '\n';
|
|
1230
1233
|
|
|
1231
1234
|
// 原子写(tmp + rename + Windows EPERM 重试),避免半截 local.yaml
|
package/src/machine-interface.js
CHANGED
|
@@ -225,7 +225,11 @@ export async function runGate(stage, changeName, { cwd, specBase, runtimeRoot }
|
|
|
225
225
|
const vt = runVerifyTestCheck({ cwd, specBase: specRoot, changeName });
|
|
226
226
|
const vtWarnings = [];
|
|
227
227
|
if (vt.status === 'skipped') {
|
|
228
|
-
vtWarnings.push(
|
|
228
|
+
vtWarnings.push(`⚠️ verify-test SKIPPED — gate 未核验测试(${vt.reason || 'local.yaml 未配置 commands.test 或显式无测试'})。本次 gate 结论不含测试客观核验,driver 不应据 exit 0 判定测试通过;integration-critical 变更应在 verify 阶段降级 FAIL`);
|
|
229
|
+
}
|
|
230
|
+
// known_failures 豁免披露:本次 PASS 含豁免,driver 应提示人工复核清单是否过宽
|
|
231
|
+
if (vt.exemptedCount > 0) {
|
|
232
|
+
vtWarnings.push(`⚠️ verify-test PASS 含 ${vt.exemptedCount} 个 known_failures 豁免(${vt.reason || ''});driver 应提示人工复核豁免清单是否过宽(避免误豁免本变更引入的真实失败)`);
|
|
229
233
|
}
|
|
230
234
|
// 全量 fallback 明示:跑了全量 commands.test 但非变更范围子集,失败可能含未变更
|
|
231
235
|
// 模块的预存错误——driver 不应据 exit 0 判定本次变更范围已客观测试(见 3.24 verify 坑1)。
|
|
@@ -287,22 +287,25 @@ export class StageMachine {
|
|
|
287
287
|
}
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
-
//
|
|
290
|
+
// 找主流程里第一个「未完成且上游已就绪」的阶段,推荐它作为下一步。
|
|
291
|
+
// 修两个缺陷(曾导致 brainstorm 完成后误推 archive):
|
|
292
|
+
// ① 原 steps.length>0 要求:plan/execute/verify 首次 run 前惰性未初始化(steps 空)被跳过;
|
|
293
|
+
// ② 原 upstream 把 pending 当就绪:archive 的上游 plan/execute/verify 即便 pending 也算 ok。
|
|
294
|
+
// 叠加 → 循环跳过未初始化的中间阶段,漏到 archive(steps 非空)误推,违反流程顺序。
|
|
295
|
+
// 改为:不要求 steps(空=该开始了);upstream 必须全 completed(pending 不算就绪)——
|
|
296
|
+
// archive 只在 scan/brainstorm/plan/execute/verify 全完成后才可能被推荐。
|
|
291
297
|
for (const s of STAGE_ORDER) {
|
|
292
298
|
const sd = data.stages[s];
|
|
293
|
-
if (sd
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
command: `sillyspec run ${s}`,
|
|
304
|
-
};
|
|
305
|
-
}
|
|
299
|
+
if (!sd) continue;
|
|
300
|
+
if (sd.status === 'completed' || sd.status === 'skipped') continue;
|
|
301
|
+
const idx = STAGE_ORDER.indexOf(s);
|
|
302
|
+
const upstream = STAGE_ORDER.slice(0, idx);
|
|
303
|
+
const upstreamOk = upstream.every(us => data.stages[us]?.status === 'completed');
|
|
304
|
+
if (upstreamOk) {
|
|
305
|
+
return {
|
|
306
|
+
text: `可以开始 ${STAGE_LABELS[s] || s}。`,
|
|
307
|
+
command: `sillyspec run ${s}`,
|
|
308
|
+
};
|
|
306
309
|
}
|
|
307
310
|
}
|
|
308
311
|
|