sillyspec 3.26.1 → 3.26.3

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.
@@ -118,6 +118,44 @@ execute 还有**第二道**独立的 stage 级审查:除逐 task review.json
118
118
 
119
119
  可选:`sillyspec dispatch probe` 查看 SillyHub 是否可用。
120
120
 
121
+ ## 跨仓 task(一个 change 改多个仓库)
122
+
123
+ 单个 change 的 task 可以分散到主仓 + 多个跨仓仓实现(典型场景:dogfood 自指、monorepo 多包仓、共享库 + 调用方联合改造)。**单仓 change 不需要任何跨仓配置**(所有 task 不写 `repo:` 即走原流程,零回归)。
124
+
125
+ ### 跨仓 task 配置(plan 阶段产出,execute 阶段消费)
126
+
127
+ - **task 卡片 `repo:` 字段**:跨仓 task 在 `tasks/task-NN.md` frontmatter 写 `repo: <key>`(缺省='main'=主仓 task,不写即主仓)。
128
+ - **local.yaml `repos:` 段**:在 `.sillyspec/local.yaml` 注册跨仓仓路径(`main` 不用注册,隐式=当前项目):
129
+ ```yaml
130
+ repos:
131
+ sillyspec: C:/Users/qinyi/IdeaProjects/sillyspec
132
+ shared-lib: ../shared-lib
133
+ ```
134
+ task 卡 `repo:` 引用的 key 必须在此注册,否则 execute 启动 fail-closed 阻断(跨仓 apply 走错仓=数据所有权事故,配置错误不降级)。
135
+ - **跨仓 task 的 `allowed_paths`**:指**相对跨仓仓根**的路径(非主仓根)。
136
+
137
+ ### workdir 切换(execute 派发子代理时)
138
+
139
+ - **主仓 task**:子代理 workdir = 主仓 worktree 路径(CLI 自动创建的隔离 worktree)。
140
+ - **跨仓 task**:子代理 workdir = 跨仓仓根目录(**直接在跨仓仓主干工作区改+commit,不经主仓 worktree、不建分支**)。commit 到跨仓仓主干即落盘。
141
+ - 同一个 Wave 内允许混合主仓 + 跨仓 task(每个 task 独立子代理,各传各的 workdir)。Wave prompt 会注入 per-task workdir 表,按表选。
142
+
143
+ ### 跨仓 task 的双锡点(CLI 写入,子代理不改)
144
+
145
+ - `base_commit`:CLI 派发跨仓 task 前实时 `git -C <跨仓仓根> rev-parse HEAD` 落盘到 task 卡 frontmatter(锁 base,防同 Wave 多 task 改同跨仓仓时 HEAD 推进致 diff 漂移)。
146
+ - `head_commit`:跨仓 task 子代理 commit 完成后、写 review.json / 勾选 checkbox **之前**,由你(主 agent)运行 `git -C <跨仓仓根> rev-parse HEAD` 把结果写入该 task 卡 frontmatter `head_commit:` 字段。
147
+ - review.json 的 `base`/`head` 取这两个锡点(非瞬时 HEAD)。
148
+
149
+ ### 跨仓 task 的 review.json
150
+
151
+ - 路径仍写主仓 `.sillyspec/.runtime/execute-runs/<run-id>/tasks/task-XX/review.json`(review 统一存主仓)。
152
+ - 加 `repo: <key>` 字段标该 task 所属仓(缺省='main')。
153
+ - `base`/`head` 是**跨仓仓的 commit**(取 task 卡锡点),CLI 据此在跨仓仓根跑 git 校验。
154
+
155
+ ### 跨仓 task apply = no-op
156
+
157
+ 跨仓 task 的代码由子代理直接 commit 到跨仓仓主干(commit 即落地),主仓 `worktree apply` 对跨仓 task **不打 patch、不 cleanup**——只校验 `review.head` 是跨仓仓真实 commit。主仓 task 走原 apply 路径不变。
158
+
121
159
  ## worktree 子命令(execute 相关)
122
160
 
123
161
  ```bash
@@ -81,6 +81,54 @@ plan 的步骤是动态的:`generate_plan`(生成分级计划)→ `review_
81
81
 
82
82
  plan 完成校验会检查:design 提到入口文件(cli.ts/main.ts/server.ts 等)但 task 的 allowed_paths 不含该文件 → 报 error。若确实不需要改入口,在 design.md 明示理由。
83
83
 
84
+ ## 跨仓 task 卡片协议(一个 change 改多个仓库)
85
+
86
+ 单个 change 的 task 可以分散到主仓 + 多个跨仓仓实现(典型场景:dogfood 自指、monorepo 多包仓、共享库 + 调用方联合改造)。**单仓 change 不需要任何跨仓配置**(所有 task 不写 `repo:` 即走原流程,零回归)。
87
+
88
+ ### 跨仓 task 卡片字段
89
+
90
+ 在 `tasks/task-NN.md` frontmatter 加:
91
+
92
+ - `repo: <key>`(可选,缺省='main'=主仓 task):task 所属仓的 key。key 必须在 `.sillyspec/local.yaml` `repos:` 段注册(`main` 隐式=主仓不用注册)。跨仓 change 缺注册 → execute 启动 fail-closed 阻断。
93
+ - `allowed_paths`:跨仓 task 时指**相对跨仓仓根**的路径(非主仓根)。例如跨仓 task 改 `sillyspec` 仓的 `src/foo.js`,allowed_paths 写 `src/foo.js`(相对 sillyspec 仓根),不是 `../sillyspec/src/foo.js`。
94
+ - `base_commit` / `head_commit`:**不要手写**——CLI 在 execute 派发/回收时自动落盘双锡点(锁 base/head 防 diff 漂移),plan 阶段不写。
95
+
96
+ ### local.yaml `repos:` 段(跨仓注册表)
97
+
98
+ 跨仓 change 必须在 `.sillyspec/local.yaml` 注册所有跨仓仓路径:
99
+
100
+ ```yaml
101
+ repos:
102
+ sillyspec: C:/Users/qinyi/IdeaProjects/sillyspec
103
+ shared-lib: ../shared-lib
104
+ ```
105
+
106
+ key = task 卡 `repo:` 引用名,value = 跨仓仓绝对路径(或相对主仓根的路径)。`main` 不用注册。
107
+
108
+ ### design.md 文件变更清单按仓分段
109
+
110
+ 跨仓 change 的 `## 文件变更清单` 段须按仓分段,段头格式 `## <repo> 仓变更`(主仓段头可省略或写 `## main 仓变更`):
111
+
112
+ ```markdown
113
+ ## 文件变更清单
114
+
115
+ ### sillyspec 仓变更
116
+ | 操作 | 文件路径 | 说明 |
117
+ |---|---|---|
118
+ | 修改 | src/task-review.js | 跨仓 task 改的文件 |
119
+
120
+ ### main 仓变更
121
+ | 操作 | 文件路径 | 说明 |
122
+ |---|---|---|
123
+ | 修改 | src/index.js | 主仓 task 改的文件 |
124
+ ```
125
+
126
+ plan 完成校验会按仓分段对账:跨仓仓路径相对跨仓仓根校验 task allowed_paths 覆盖,主仓路径相对主仓根校验。跨仓 task 与主仓 task 同名物理路径分属不同 repo 不判冲突。
127
+
128
+ ### Wave 划分
129
+
130
+ 同 Wave 内允许混合主仓 + 跨仓 task(execute 按 per-task workdir 派发,不强制同 Wave 同 repo)。共享文件的 task 仍须分到不同 Wave(同 Wave 共享文件会被强制并行,子代理互相覆盖)。
131
+
84
132
  ## 阶段流转
85
133
 
86
134
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sillyspec",
3
- "version": "3.26.1",
3
+ "version": "3.26.3",
4
4
  "description": "SillySpec CLI — 流程状态机,让 AI 严格按步骤来",
5
5
  "icon": "logo.jpg",
6
6
  "homepage": "https://sillyspec.ppdmq.top/",
@@ -5,6 +5,10 @@
5
5
  * 供 auto 模式决定内部流程深度。
6
6
  */
7
7
 
8
+ import { existsSync, readFileSync } from 'fs'
9
+ import { join } from 'path'
10
+ import jsYaml from 'js-yaml'
11
+
8
12
  /**
9
13
  * 用户显式指定的关键词 → 强制模式
10
14
  */
@@ -24,12 +28,36 @@ const FORCE_QUICK_PATTERNS = [
24
28
  /修复.*\s*\bbug\b.*\bfix\b/i,
25
29
  ]
26
30
 
31
+ /**
32
+ * 读 local.yaml 的 auto_mode 段(best-effort,绝不抛)。
33
+ * 供 runCommand 接线:传给 classifyChange 的 localConfig,让 force_*_patterns 真生效。
34
+ * @param {string} cwd - 主仓库根(local.yaml 在 <cwd>/.sillyspec/local.yaml)
35
+ * @returns {{ force_full_patterns?: string[], force_quick_patterns?: string[] } | null}
36
+ * auto_mode 段含至少一个 string pattern → 归一化数组;文件缺/解析失败/无段/空 → null(降级默认关键词)
37
+ */
38
+ export function readAutoModeFromLocalYaml(cwd) {
39
+ try {
40
+ const p = join(cwd, '.sillyspec', 'local.yaml')
41
+ if (!existsSync(p)) return null
42
+ const doc = jsYaml.load(readFileSync(p, 'utf8'))
43
+ if (!doc || typeof doc !== 'object') return null
44
+ const am = doc.auto_mode
45
+ if (!am || typeof am !== 'object') return null
46
+ const ffp = Array.isArray(am.force_full_patterns) ? am.force_full_patterns.filter(s => typeof s === 'string') : []
47
+ const fqp = Array.isArray(am.force_quick_patterns) ? am.force_quick_patterns.filter(s => typeof s === 'string') : []
48
+ if (ffp.length === 0 && fqp.length === 0) return null
49
+ return { force_full_patterns: ffp, force_quick_patterns: fqp }
50
+ } catch {
51
+ return null
52
+ }
53
+ }
54
+
27
55
  /**
28
56
  * 分类变更规模
29
57
  * @param {object} opts
30
58
  * @param {string} opts.description - 用户需求描述
31
59
  * @param {string} [opts.explicitMode] - 用户显式指定的模式(auto/quick/full)
32
- * @param {object} [opts.localConfig] - local.yaml 中的 auto_mode 配置
60
+ * @param {object} [opts.localConfig] - local.yaml auto_mode 段(由 readAutoModeFromLocalYaml 读出)
33
61
  * @returns {{ mode: 'quick'|'auto'|'full', reason: string }}
34
62
  */
35
63
  export function classifyChange({ description = '', explicitMode, localConfig } = {}) {
@@ -44,13 +72,21 @@ export function classifyChange({ description = '', explicitMode, localConfig } =
44
72
  const forceQuickPatterns = localConfig.force_quick_patterns || []
45
73
 
46
74
  for (const pattern of forceFullPatterns) {
47
- if (new RegExp(pattern, 'i').test(description)) {
48
- return { mode: 'full', reason: `local.yaml force_full_pattern 匹配: ${pattern}` }
75
+ try {
76
+ if (new RegExp(pattern, 'i').test(description)) {
77
+ return { mode: 'full', reason: `local.yaml force_full_pattern 匹配: ${pattern}` }
78
+ }
79
+ } catch {
80
+ // 非法正则跳过,不崩 auto 分类(review-2026-08-09 #30)
49
81
  }
50
82
  }
51
83
  for (const pattern of forceQuickPatterns) {
52
- if (new RegExp(pattern, 'i').test(description)) {
53
- return { mode: 'quick', reason: `local.yaml force_quick_pattern 匹配: ${pattern}` }
84
+ try {
85
+ if (new RegExp(pattern, 'i').test(description)) {
86
+ return { mode: 'quick', reason: `local.yaml force_quick_pattern 匹配: ${pattern}` }
87
+ }
88
+ } catch {
89
+ // 非法正则跳过
54
90
  }
55
91
  }
56
92
  }
@@ -0,0 +1,287 @@
1
+ /**
2
+ * config-schema.js — local.yaml 配置键的单一数据源。
3
+ *
4
+ * 定位:`.sillyspec/local.yaml` 是 gitignored 的机器+人混写配置,键散落在 ~10 个 reader
5
+ * 里(detect / verify-postcheck / sync / dispatch / worktree-guard / classify-change …),
6
+ * 外部项目的 agent 无从得知有哪些键、哪些生效。本模块把「全部已知键 + 生效状态 + 读取点」
7
+ * 集中成一张表,供:
8
+ * - `sillyspec config schema`(人类可读树 / --json 机读)打印;
9
+ * - `sillyspec init` 调 renderExample() 落盘脱敏 local.yaml.example 给人/外部 agent 看。
10
+ *
11
+ * 单一真相 = LOCAL_YAML_SCHEMA。renderExample() 是策展模板(YAML 形态各异:嵌套 / inline
12
+ * flow / 顶层标量,自动生成不如策展可读),由测试「每个 live 键路径必出现于 example 文本」
13
+ * 耦合保证不漂移——加键忘 example 则测试红。
14
+ *
15
+ * reader 引用一律用符号名(readMcpConfig / extractModules),**不用行号**——行号随重构漂移,
16
+ * 符号名稳定可 grep。
17
+ *
18
+ * status 语义:
19
+ * - live 真有 reader 从 local.yaml 读并起效(配了即生效)。
20
+ * - declared 代码/JSDoc/prompt 声明过,但无 reader 真读 local.yaml(或调用方未传),配了不生效。
21
+ * 诚实标出,避免 agent 白配 + 暴露待接线债。
22
+ */
23
+
24
+ /**
25
+ * @typedef {Object} SchemaKey
26
+ * @property {string} path 点分路径(mcp.url / worktree-hook.readonlyCommands)
27
+ * @property {string} type string|enum|integer|object|array|scalar
28
+ * @property {string[]} [values] enum 取值
29
+ * @property {boolean} [required] 凭据类必填(两键齐全才视为有效源)
30
+ * @property {boolean} [secret] 敏感凭据,example 用占位符
31
+ * @property {boolean} [optional] 可选键
32
+ * @property {string} status live|declared
33
+ * @property {string[]} readers 读取点符号名(稳定可 grep),declared 键留空数组
34
+ * @property {string} desc 一句话说明
35
+ * @property {string} example 脱敏示例值(secret 键用占位符)
36
+ *
37
+ * @typedef {Object} SchemaSection
38
+ * @property {string} id
39
+ * @property {string} title
40
+ * @property {string} note 段说明(写入来源 / 注意事项)
41
+ * @property {SchemaKey[]} keys
42
+ */
43
+
44
+ /** @type {{ file: string, note: string, sections: SchemaSection[] }} */
45
+ export const LOCAL_YAML_SCHEMA = {
46
+ file: '.sillyspec/local.yaml',
47
+ note: 'gitignored(含敏感凭据勿提交);脱敏示例 local.yaml.example 由 sillyspec init 生成(可提交)。',
48
+ sections: [
49
+ {
50
+ id: 'project',
51
+ title: '项目类型',
52
+ note: '由 sillyspec local detect 纯 fs 嗅探写入,一般无需手填。',
53
+ keys: [
54
+ {
55
+ path: 'project.type',
56
+ type: 'enum',
57
+ values: ['nodejs', 'maven', 'gradle', 'make', 'generic'],
58
+ status: 'live',
59
+ readers: ['detectLocalYaml (src/local-detect.js)', 'extractProjectType (src/worktree-deps.js)'],
60
+ desc: '项目类型,决定默认 commands 嗅探与 worktree 依赖策略。',
61
+ example: 'nodejs',
62
+ },
63
+ ],
64
+ },
65
+ {
66
+ id: 'commands',
67
+ title: '构建/测试/lint 命令',
68
+ note: 'detect 核验 package.json(或对应构建文件) scripts 存在性后才写对应键;缺失则不写。agent 可改。',
69
+ keys: [
70
+ { path: 'commands.build', type: 'string', optional: true, status: 'live', readers: ['detectLocalYaml (src/local-detect.js)', 'validateCommands (src/scan-postcheck.js)'], desc: '构建命令。', example: 'npm run build' },
71
+ { path: 'commands.test', type: 'string', optional: true, status: 'live', readers: ['extractTestCommand (src/verify-postcheck.js)', 'validateCommands (src/scan-postcheck.js)'], desc: '测试命令——verify 阶段 CLI 亲自执行此命令与 verify-result.md 对账,实测失败即阻断。', example: 'npm test' },
72
+ { path: 'commands.lint', type: 'string', optional: true, status: 'live', readers: ['detectLocalYaml (src/local-detect.js)', 'validateCommands (src/scan-postcheck.js)'], desc: 'lint 命令。', example: 'npm run lint' },
73
+ { path: 'commands.install', type: 'string', optional: true, status: 'live', readers: ['extractInstallCommand (src/worktree-deps.js)'], desc: '依赖安装命令(worktree 依赖 provisioning 用)。', example: 'npm install' },
74
+ ],
75
+ },
76
+ {
77
+ id: 'mcp',
78
+ title: 'SillyHub MCP 客户端凭据(派发用)',
79
+ note: 'agent 手填,或 sillyspec platform connect 在 mcp 段缺失时同源自动填(§7.4)。两键齐全才生效,否则回退 env SILLYHUB_MCP_URL/TOKEN。',
80
+ keys: [
81
+ { path: 'mcp.url', type: 'string', required: true, secret: true, status: 'live', readers: ['readMcpConfig (src/sillyhub-mcp/config.js)'], desc: 'SillyHub MCP 服务地址(尾斜杠自动归一)。', example: 'https://your-sillyhub.example.com' },
82
+ { path: 'mcp.token', type: 'string', required: true, secret: true, status: 'live', readers: ['readMcpConfig (src/sillyhub-mcp/config.js)'], desc: 'SillyHub MCP 认证 token。', example: '<your-mcp-token>' },
83
+ ],
84
+ },
85
+ {
86
+ id: 'platform',
87
+ title: 'SillyHub 平台同步',
88
+ note: 'sillyspec platform connect 写入(含 ping 验活);手填亦可。user/last_connected 可空或自动写。',
89
+ keys: [
90
+ { path: 'platform.url', type: 'string', required: true, secret: true, status: 'live', readers: ['SyncManager._getPlatform (src/sync.js)'], desc: 'SillyHub 平台地址。', example: 'https://your-sillyhub.example.com' },
91
+ { path: 'platform.token', type: 'string', required: true, secret: true, status: 'live', readers: ['SyncManager._getPlatform (src/sync.js)'], desc: '平台认证 token(Bearer)。', example: '<your-platform-token>' },
92
+ { path: 'platform.user', type: 'string', optional: true, status: 'live', readers: ['resolvePlatformUser / SyncManager.connect (src/sync.js)'], desc: '推送者身份;缺则回退 git user.name / env。推送时进 X-SillySpec-User header。', example: 'alice' },
93
+ { path: 'platform.last_connected', type: 'string', optional: true, status: 'live', readers: ['SyncManager.connect (src/sync.js)'], desc: 'connect 成功时间戳(ISO),自动写,勿手填。', example: '2026-08-11T00:00:00.000Z' },
94
+ ],
95
+ },
96
+ {
97
+ id: 'dispatch',
98
+ title: '派发调参',
99
+ note: '仅在确知调优值时填,否则留默认。',
100
+ keys: [
101
+ { path: 'dispatch.probe_ttl_ms', type: 'integer', optional: true, status: 'live', readers: ['readProbeTtlFromLocalYaml (src/dispatch/probe.js)'], desc: '派发探测负面缓存 TTL(毫秒,默认 60000)。daemon 抖动期避免反复探测。', example: '60000' },
102
+ { path: 'dispatch.poll_interval_ms', type: 'integer', optional: true, status: 'declared', readers: [], desc: '【路径A 预留·未落地】renderSillyHubInstruction 注入的轮询文本提及,但 isPathASupported()=false 该指令当前不注入;路径A 落地后接线,配了暂不生效。', example: '15000' },
103
+ { path: 'dispatch.worker_timeout_ms', type: 'integer', optional: true, status: 'declared', readers: [], desc: '【路径A 预留·未落地】同 poll_interval_ms,路径A 落地后接线,配了暂不生效。', example: '60000' },
104
+ ],
105
+ },
106
+ {
107
+ id: 'modules',
108
+ title: 'monorepo 子模块映射',
109
+ note: 'test_strategy: module 时,按 git diff 命中的模块子集收窄测试。只支持 inline flow 形态(extractModules 不解析嵌套展开式)。',
110
+ keys: [
111
+ { path: 'modules.<name>.path', type: 'string', optional: true, status: 'live', readers: ['extractModules (src/verify-postcheck.js)', 'parseLocalYamlModules (src/plan-postcheck.js)', 'extractModulePaths (src/worktree-deps.js)'], desc: '子模块目录路径(相对仓库根)。', example: 'frontend/' },
112
+ { path: 'modules.<name>.test', type: 'string', optional: true, status: 'live', readers: ['extractModules (src/verify-postcheck.js)'], desc: '该子模块的测试命令。', example: 'cd frontend && pnpm test' },
113
+ ],
114
+ },
115
+ {
116
+ id: 'test_strategy',
117
+ title: '测试策略',
118
+ note: 'verify 阶段 CLI 对账的收窄策略。',
119
+ keys: [
120
+ { path: 'test_strategy', type: 'enum', values: ['full', 'module'], optional: true, status: 'live', readers: ['extractTestStrategy (src/verify-postcheck.js)'], desc: 'full=全量 commands.test;module=按 git diff 命中 modules 子集收窄(需配 modules)。缺省=全量。', example: 'full' },
121
+ ],
122
+ },
123
+ {
124
+ id: 'known_failures',
125
+ title: '预存失败豁免清单',
126
+ note: '变更前就失败的测试行,豁免后不计入 verify 阻断。支持块列表或 inline 数组。务必定期复核,避免误豁免本变更引入的真实失败。',
127
+ keys: [
128
+ { path: 'known_failures', type: 'array', optional: true, status: 'live', readers: ['extractKnownFailures (src/verify-postcheck.js)'], desc: '失败行模式列表(匹配测试输出失败行)。', example: 'tests/some-old-test' },
129
+ ],
130
+ },
131
+ {
132
+ id: 'worktree-hook',
133
+ title: 'worktree-guard 扩展',
134
+ note: 'execute worktree 隔离期内放行的额外只读命令。键名用 camelCase(parseSimpleYaml 原样保留)。',
135
+ keys: [
136
+ { path: 'worktree-hook.readonlyCommands', type: 'array', optional: true, status: 'live', readers: ['loadLocalConfig (src/hooks/worktree-guard.js)'], desc: '额外放行的只读命令名列表(如 rg/fd),绕过 worktree-guard 写操作拦截。', example: 'rg' },
137
+ ],
138
+ },
139
+ {
140
+ id: 'auto_mode',
141
+ title: '变更规模自动分类',
142
+ note: 'sillyspec run auto 时,readAutoModeFromLocalYaml 读本段传 classifyChange 的 localConfig,force_*_patterns 匹配需求描述则强制对应模式。',
143
+ keys: [
144
+ { path: 'auto_mode.force_full_patterns', type: 'array', optional: true, status: 'live', readers: ['readAutoModeFromLocalYaml + classifyChange (src/classify-change.js)', 'runCommand (src/run/command.js)'], desc: '需求描述匹配任一正则(i 大小写无关)→ 强制 full 流程。非法正则跳过不崩。', example: '数据库|migration' },
145
+ { path: 'auto_mode.force_quick_patterns', type: 'array', optional: true, status: 'live', readers: ['readAutoModeFromLocalYaml + classifyChange (src/classify-change.js)', 'runCommand (src/run/command.js)'], desc: '需求描述匹配任一正则 → 强制 quick。非法正则跳过不崩。', example: 'fix typo' },
146
+ ],
147
+ },
148
+ ],
149
+ };
150
+
151
+ /** 全部键(拍平),便于 JSON 输出与 example 耦合测试。 */
152
+ export function flatKeys() {
153
+ return LOCAL_YAML_SCHEMA.sections.flatMap((s) => s.keys.map((k) => ({ section: s.id, ...k })));
154
+ }
155
+
156
+ /** 人类可读树形 schema 文本。live 在前、declared 在后,段内逐键列 path/type/约束/读取点/说明。 */
157
+ export function renderSchemaHuman() {
158
+ const L = [];
159
+ L.push('SillySpec local.yaml 配置清单');
160
+ L.push(`文件:${LOCAL_YAML_SCHEMA.file} — ${LOCAL_YAML_SCHEMA.note}`);
161
+ L.push('数据源:src/config-schema.js(本表唯一真相;reader 见各键「读取点」,符号名可 grep)');
162
+ L.push('');
163
+
164
+ const all = flatKeys();
165
+ const live = all.filter((k) => k.status === 'live');
166
+ const declared = all.filter((k) => k.status === 'declared');
167
+
168
+ L.push(`━━━ 生效(${live.length} 键,配了即生效) ━━━`);
169
+ L.push('');
170
+ for (const s of LOCAL_YAML_SCHEMA.sections) {
171
+ const keys = s.keys.filter((k) => k.status === 'live');
172
+ if (keys.length === 0) continue;
173
+ L.push(`[${s.id}] ${s.title}`);
174
+ L.push(` ${s.note}`);
175
+ for (const k of keys) {
176
+ L.push(` ${k.path}${formatConstraint(k)} ${k.desc}`);
177
+ if (k.readers.length) L.push(` 读取点:${k.readers.join(';')}`);
178
+ }
179
+ L.push('');
180
+ }
181
+
182
+ L.push(`━━━ 声明但未接线(${declared.length} 键,配了不生效 / 待接线债) ━━━`);
183
+ L.push('');
184
+ for (const s of LOCAL_YAML_SCHEMA.sections) {
185
+ const keys = s.keys.filter((k) => k.status === 'declared');
186
+ if (keys.length === 0) continue;
187
+ L.push(`[${s.id}] ${s.title}`);
188
+ L.push(` ${s.note}`);
189
+ for (const k of keys) {
190
+ L.push(` ${k.path}${formatConstraint(k)} ${k.desc}`);
191
+ }
192
+ L.push('');
193
+ }
194
+ return L.join('\n');
195
+ }
196
+
197
+ function formatConstraint(k) {
198
+ const tags = [];
199
+ if (k.required) tags.push('必填');
200
+ if (k.optional) tags.push('可选');
201
+ if (k.secret) tags.push('凭据');
202
+ if (k.type === 'enum' && k.values) tags.push(k.values.join('|'));
203
+ else tags.push(k.type);
204
+ return tags.length ? ` [${tags.join(' · ')}]` : '';
205
+ }
206
+
207
+ /** 机读 JSON(程序化消费)。结构 = LOCAL_YAML_SCHEMA 本体 + flatKeys 视图。 */
208
+ export function renderSchemaJson() {
209
+ return JSON.stringify({
210
+ file: LOCAL_YAML_SCHEMA.file,
211
+ note: LOCAL_YAML_SCHEMA.note,
212
+ sections: LOCAL_YAML_SCHEMA.sections,
213
+ keys: flatKeys().map(({ section, path, type, values, required, optional, secret, status, readers, desc }) => ({
214
+ section, path, type, values, required, optional, secret, status, readers, desc,
215
+ })),
216
+ }, null, 2);
217
+ }
218
+
219
+ /**
220
+ * 脱敏 local.yaml 示例文本(sillyspec init 落盘用)。
221
+ * 策展模板(非自动生成):YAML 形态各异(嵌套/inline flow/顶层标量),策展可读性远胜自动拼。
222
+ * 与 SCHEMA 的耦合由测试保证:每个 live 键路径必出现于本文本。
223
+ */
224
+ export function renderExample() {
225
+ return `# SillySpec local.yaml — 脱敏示例(由 sillyspec init 生成,可提交)
226
+ # 真实配置文件 .sillyspec/local.yaml 是 gitignored(含敏感凭据)。
227
+ # 用法:复制本文件为 .sillyspec/local.yaml,填真实值;或直接 sillyspec platform connect 自动写凭据段。
228
+ # 全部键与生效状态:sillyspec config schema
229
+
230
+ # ── 项目类型(sillyspec local detect 自动写入,一般无需手填)──
231
+ project:
232
+ type: nodejs # nodejs | maven | gradle | make | generic
233
+
234
+ # ── 构建/测试/lint 命令(detect 核验 scripts 存在性后才写;缺失键省略)──
235
+ commands:
236
+ build: npm run build
237
+ test: npm test
238
+ lint: npm run lint
239
+ # install: npm install # worktree 依赖安装命令
240
+
241
+ # ── SillyHub MCP 客户端凭据(派发到 worker 用)──
242
+ # agent 手填,或 sillyspec platform connect 在 mcp 段缺失时同源自动填。
243
+ # 两键齐全才生效;缺则回退环境变量 SILLYHUB_MCP_URL / SILLYHUB_MCP_TOKEN。
244
+ mcp:
245
+ url: https://your-sillyhub.example.com
246
+ token: <your-mcp-token>
247
+
248
+ # ── SillyHub 平台同步(sillyspec platform connect 写入,含 ping 验活)──
249
+ platform:
250
+ url: https://your-sillyhub.example.com
251
+ token: <your-platform-token>
252
+ user: alice # 推送者身份(可空,回退 git user.name / env)
253
+ # last_connected: 2026-08-11T00:00:00.000Z # connect 自动写,勿手填
254
+
255
+ # ── 派发调参(仅确知调优值时填,否则留默认)──
256
+ dispatch:
257
+ probe_ttl_ms: 60000 # 派发探测负面缓存 TTL(默认 60000)
258
+ # poll_interval_ms: 15000 # ⚠ 路径A 预留·未落地,配了暂不生效
259
+ # worker_timeout_ms: 60000 # ⚠ 路径A 预留·未落地,配了暂不生效
260
+
261
+ # ── monorepo 子模块映射(test_strategy: module 时按 git diff 命中模块收窄测试)──
262
+ # 只支持 inline flow 形态(嵌套展开式解析不出):
263
+ modules:
264
+ frontend: { path: "frontend/", test: "cd frontend && pnpm test" }
265
+ backend: { path: "backend/", test: "cd backend && npm test" }
266
+
267
+ # ── 测试策略:full=全量 commands.test | module=按命中模块收窄(需配 modules)──
268
+ test_strategy: full
269
+
270
+ # ── 预存失败豁免清单(变更前就失败的测试行;务必定期复核,防误豁免真实失败)──
271
+ known_failures:
272
+ - tests/some-old-stable-failure
273
+
274
+ # ── worktree-guard 只读命令扩展(execute worktree 隔离期内放行额外只读命令)──
275
+ worktree-hook:
276
+ readonlyCommands:
277
+ - rg
278
+ - fd
279
+
280
+ # ── 变更规模自动分类(sillyspec run auto 时按需求描述强制 quick/full;正则数组,i 大小写无关)──
281
+ auto_mode:
282
+ force_full_patterns:
283
+ - 数据库|migration
284
+ force_quick_patterns:
285
+ - fix typo
286
+ `;
287
+ }
package/src/db.js CHANGED
@@ -202,7 +202,7 @@ export class DB {
202
202
  CREATE TABLE IF NOT EXISTS project (
203
203
  id INTEGER PRIMARY KEY DEFAULT 1,
204
204
  name TEXT NOT NULL,
205
- schema_version INTEGER DEFAULT 4,
205
+ schema_version INTEGER DEFAULT 5,
206
206
  created_at TEXT NOT NULL,
207
207
  updated_at TEXT NOT NULL
208
208
  )
@@ -356,9 +356,10 @@ function matchFileWhitelist(filePath) {
356
356
  }
357
357
 
358
358
  /**
359
- * 读取 local.yaml 中的扩展白名单配置(如果存在)
359
+ * 读取并解析 local.yaml(轻量手写解析,返回顶层对象)。
360
+ * 消费方读嵌套段 worktree-hook.readonlyCommands(见 isSingleCommandReadonly 的 extraReadonlyCommands)。
360
361
  * @param {string} cwd
361
- * @returns {{ fileWhitelist?: string[], readonlyCommands?: string[] }}
362
+ * @returns {Record<string, any>} 解析后的 local.yaml 顶层对象(文件缺/解析失败 → {})
362
363
  */
363
364
  function loadLocalConfig(cwd) {
364
365
  const candidates = [