spec-wave 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +327 -0
  3. package/README.zh-CN.md +327 -0
  4. package/RELEASING.md +52 -0
  5. package/assets/README.md +30 -0
  6. package/assets/ci/samples/README.md +76 -0
  7. package/assets/ci/samples/hgm-ingest.yml.example +42 -0
  8. package/assets/ci/samples/lint-wiki-delta.pin.yml.example +47 -0
  9. package/assets/ci/samples/lint-wiki-delta.yml.example +49 -0
  10. package/assets/ci/samples/pytest.yml.example +49 -0
  11. package/assets/ci/samples/quality.yml.example +56 -0
  12. package/assets/ci/samples/skills-validate.yml.example +38 -0
  13. package/assets/ci/samples/tech-graph.yml.example +55 -0
  14. package/assets/coding_wiki/templates/README.md +115 -0
  15. package/assets/coding_wiki/templates/_index.md +19 -0
  16. package/assets/coding_wiki/templates/context.md +18 -0
  17. package/assets/coding_wiki/templates/stable.md +26 -0
  18. package/assets/coding_wiki/templates/topics/wiki_layout.md +13 -0
  19. package/assets/coding_wiki/templates/topics/wikilinks_export.md +13 -0
  20. package/assets/coding_wiki/templates/volatile.md +25 -0
  21. package/assets/docs/POINTER_ONBOARDING.md +9 -0
  22. package/assets/docs/POINTER_RUNBOOK_wiki_delta.md +18 -0
  23. package/assets/docs/POINTER_SDD_HAT_FLOW.md +8 -0
  24. package/assets/docs/POINTER_USER_GUIDE.md +8 -0
  25. package/assets/graph/stubs/README.md +6 -0
  26. package/assets/graph/templates/00_main.graph.yaml +72 -0
  27. package/assets/graph/templates/00_main.md +85 -0
  28. package/assets/graph/templates/01_struct.md +38 -0
  29. package/assets/graph/templates/02_version.md +7 -0
  30. package/assets/graph/templates/10_flow_MAIN.graph.yaml +105 -0
  31. package/assets/graph/templates/10_flow_MAIN.md +103 -0
  32. package/assets/graph/templates/99_mermaid_protocol.md +156 -0
  33. package/assets/graph/templates/README.md +66 -0
  34. package/assets/harness/README.md +18 -0
  35. package/assets/harness/discipline-coverage.yaml +320 -0
  36. package/assets/harness/invokes/README.md +22 -0
  37. package/assets/harness/invokes/TEMPLATE_invoke.md +61 -0
  38. package/assets/harness/lifecycle.yaml +127 -0
  39. package/assets/harness/prompts/00-orchestrator.md +91 -0
  40. package/assets/harness/prompts/10-spec-requirements.md +63 -0
  41. package/assets/harness/prompts/10-task-requirements.md +70 -0
  42. package/assets/harness/prompts/20-spec-audit.md +50 -0
  43. package/assets/harness/prompts/20-task-audit.md +90 -0
  44. package/assets/harness/prompts/30-execute-code.md +64 -0
  45. package/assets/harness/prompts/40-self-check.md +65 -0
  46. package/assets/harness/prompts/FRAGMENT_00_delegate_only_v1_zh.md +19 -0
  47. package/assets/harness/prompts/FRAGMENT_30_gate_verify_v1_zh.md +34 -0
  48. package/assets/harness/prompts/FRAGMENT_30_invoke_block_v1_zh.md +28 -0
  49. package/assets/harness/prompts/FRAGMENT_hat_reanchor_v1_zh.md +19 -0
  50. package/assets/harness/prompts/README.md +67 -0
  51. package/assets/harness/prompts/TEMPLATE_30_gate_stop.md +36 -0
  52. package/assets/harness/templates/FRAGMENT_task_domain_infer_v1_zh.md +16 -0
  53. package/assets/harness/templates/ONTOLOGY_consumer_slice_v1.md +41 -0
  54. package/assets/harness/templates/QUICKREF_v1_zh.md +35 -0
  55. package/assets/harness/templates/README.md +50 -0
  56. package/assets/harness/templates/TASK_TEMPLATE.md +133 -0
  57. package/assets/harness/templates/TASK_done_README.md +76 -0
  58. package/assets/harness/templates/TASK_epic.md +103 -0
  59. package/assets/harness/templates/TASK_graph_bootstrap.md +115 -0
  60. package/assets/harness/templates/VIEW_done_by_domain.md +66 -0
  61. package/assets/harness/templates/VIEW_done_thin_pointer.md +16 -0
  62. package/assets/ide/adapters/AGENTS.md.fragment.example +33 -0
  63. package/assets/ide/adapters/CLAUDE.md.fragment.example +32 -0
  64. package/assets/ide/adapters/README.md +57 -0
  65. package/assets/ide/adapters/cursor-harness-starter.mdc.example +39 -0
  66. package/assets/ide/commands/claude/kit/apply-standards.md +10 -0
  67. package/assets/ide/commands/claude/kit/gate-status.md +10 -0
  68. package/assets/ide/commands/claude/kit/graph-check.md +8 -0
  69. package/assets/ide/commands/claude/kit/hat-00-delegate.md +9 -0
  70. package/assets/ide/commands/claude/kit/hat-10-spec.md +9 -0
  71. package/assets/ide/commands/claude/kit/hat-10-task.md +9 -0
  72. package/assets/ide/commands/claude/kit/hat-20-spec-audit.md +9 -0
  73. package/assets/ide/commands/claude/kit/hat-20-task-audit.md +9 -0
  74. package/assets/ide/commands/claude/kit/hat-reanchor.md +8 -0
  75. package/assets/ide/commands/claude/kit/init-guide.md +16 -0
  76. package/assets/ide/commands/claude/kit/sync-prompts-guide.md +9 -0
  77. package/assets/ide/commands/claude/kit/verify.md +16 -0
  78. package/assets/ide/commands/cursor/kit-apply-standards.md +12 -0
  79. package/assets/ide/commands/cursor/kit-gate-status.md +12 -0
  80. package/assets/ide/commands/cursor/kit-graph-check.md +10 -0
  81. package/assets/ide/commands/cursor/kit-hat-00-delegate.md +11 -0
  82. package/assets/ide/commands/cursor/kit-hat-10-spec.md +11 -0
  83. package/assets/ide/commands/cursor/kit-hat-10-task.md +11 -0
  84. package/assets/ide/commands/cursor/kit-hat-20-spec-audit.md +11 -0
  85. package/assets/ide/commands/cursor/kit-hat-20-task-audit.md +11 -0
  86. package/assets/ide/commands/cursor/kit-hat-reanchor.md +10 -0
  87. package/assets/ide/commands/cursor/kit-init-guide.md +18 -0
  88. package/assets/ide/commands/cursor/kit-sync-prompts-guide.md +11 -0
  89. package/assets/ide/commands/cursor/kit-verify.md +18 -0
  90. package/assets/ide/host-adapt/README.md +113 -0
  91. package/assets/ide/host-adapt/examples/mvp-hosts.yaml +78 -0
  92. package/assets/ide/host-adapt/host-adapt.schema.json +97 -0
  93. package/assets/ide/skills-orch/README.md +16 -0
  94. package/assets/ide/skills-orch/kit-apply-standards/SKILL.md +17 -0
  95. package/assets/ide/skills-orch/kit-gate-status/SKILL.md +17 -0
  96. package/assets/ide/skills-orch/kit-hat-reanchor/SKILL.md +15 -0
  97. package/assets/ide/skills-orch/kit-init-guide/SKILL.md +23 -0
  98. package/assets/ide/skills-orch/kit-verify/SKILL.md +23 -0
  99. package/assets/ontology.yaml +113 -0
  100. package/assets/skills/README.md +37 -0
  101. package/assets/skills/harness-00-delegate-only/SKILL.md +19 -0
  102. package/assets/skills/harness-10-spec/SKILL.md +63 -0
  103. package/assets/skills/harness-10-task/SKILL.md +70 -0
  104. package/assets/skills/harness-20-spec-audit/SKILL.md +50 -0
  105. package/assets/skills/harness-20-task-audit/SKILL.md +90 -0
  106. package/assets/skills/harness-20-task-audit/references/TEMPLATE_30_gate_stop.md +36 -0
  107. package/assets/skills/harness-hat-reanchor/SKILL.md +19 -0
  108. package/assets/standards/POINTER_workspace_truth_v1_zh.md +42 -0
  109. package/assets/standards/README.md +35 -0
  110. package/assets/standards/SOURCES_v1_zh.md +49 -0
  111. package/assets/standards/TEMPLATE_CODING_BASELINE_L1_v1_zh.md +88 -0
  112. package/assets/standards/TEMPLATE_CODING_BASELINE_L2_backend_v1_zh.md +75 -0
  113. package/assets/standards/TEMPLATE_CODING_BASELINE_L2_frontend_v1_zh.md +76 -0
  114. package/bin/dsh-coding-kit.js +7 -0
  115. package/bin/specgate.js +7 -0
  116. package/bin/validate_template_compliance.py +470 -0
  117. package/cordis.patch.yml +5 -0
  118. package/lib/cli-checks.d.ts +68 -0
  119. package/lib/cli-checks.js +812 -0
  120. package/lib/cli-checks.js.map +1 -0
  121. package/lib/cli-graph-hgm.d.ts +74 -0
  122. package/lib/cli-graph-hgm.js +416 -0
  123. package/lib/cli-graph-hgm.js.map +1 -0
  124. package/lib/cli-graph-yaml.d.ts +79 -0
  125. package/lib/cli-graph-yaml.js +602 -0
  126. package/lib/cli-graph-yaml.js.map +1 -0
  127. package/lib/cli-graph.d.ts +1 -0
  128. package/lib/cli-graph.js +199 -0
  129. package/lib/cli-graph.js.map +1 -0
  130. package/lib/cli-host.d.ts +24 -0
  131. package/lib/cli-host.js +1197 -0
  132. package/lib/cli-host.js.map +1 -0
  133. package/lib/cli-lifecycle.d.ts +2 -0
  134. package/lib/cli-lifecycle.js +401 -0
  135. package/lib/cli-lifecycle.js.map +1 -0
  136. package/lib/cli-refresh-ide-blocks.d.ts +66 -0
  137. package/lib/cli-refresh-ide-blocks.js +484 -0
  138. package/lib/cli-refresh-ide-blocks.js.map +1 -0
  139. package/lib/cli-shared.d.ts +74 -0
  140. package/lib/cli-shared.js +281 -0
  141. package/lib/cli-shared.js.map +1 -0
  142. package/lib/cli-skills.d.ts +52 -0
  143. package/lib/cli-skills.js +363 -0
  144. package/lib/cli-skills.js.map +1 -0
  145. package/lib/cli-status.d.ts +2 -0
  146. package/lib/cli-status.js +251 -0
  147. package/lib/cli-status.js.map +1 -0
  148. package/lib/cli-sync-prompts.d.ts +25 -0
  149. package/lib/cli-sync-prompts.js +158 -0
  150. package/lib/cli-sync-prompts.js.map +1 -0
  151. package/lib/cli-sync.d.ts +2 -0
  152. package/lib/cli-sync.js +124 -0
  153. package/lib/cli-sync.js.map +1 -0
  154. package/lib/cli-task-extra.d.ts +53 -0
  155. package/lib/cli-task-extra.js +408 -0
  156. package/lib/cli-task-extra.js.map +1 -0
  157. package/lib/cli-timeline.d.ts +28 -0
  158. package/lib/cli-timeline.js +68 -0
  159. package/lib/cli-timeline.js.map +1 -0
  160. package/lib/cli-wiki.d.ts +21 -0
  161. package/lib/cli-wiki.js +189 -0
  162. package/lib/cli-wiki.js.map +1 -0
  163. package/lib/cli.d.ts +22 -0
  164. package/lib/cli.js +1034 -0
  165. package/lib/cli.js.map +1 -0
  166. package/lib/host-contract.d.ts +23 -0
  167. package/lib/host-contract.js +143 -0
  168. package/lib/host-contract.js.map +1 -0
  169. package/lib/index.d.ts +18 -0
  170. package/lib/index.js +290 -0
  171. package/lib/index.js.map +1 -0
  172. package/lib/yaml.d.ts +4 -0
  173. package/lib/yaml.js +10 -0
  174. package/lib/yaml.js.map +1 -0
  175. package/package.json +87 -0
@@ -0,0 +1,1197 @@
1
+ /**
2
+ * host 子命令(2.x W1–W4):validate + apply + update。
3
+ * apply:always_on + commands(core|expanded) + skills(跳过 30/40)。
4
+ * update:刷新产品 commands/skills;conflict 默认不覆盖(`--force` 显式)。
5
+ * U-01:契约嗅探不匹配 → exit 2 零写入。
6
+ * 2.1.1 W1:`.coding-kit/host-tools.json` 粘性;`--tools all`;成功 --yes 写粘性。
7
+ * 2.1.1 W2:`host update` 解析序 A — CLI `--tools` → 粘性 → 否则 exit 1。
8
+ * 2.1.1 W3:`init --tools` 同进程可调用 `cmdHost(['apply', …])`(勿 shell 自调)。
9
+ * 本波禁止:bump / publish / 默认分发 30/40 / onboard / kit-30 slash。
10
+ */
11
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
12
+ import path from 'node:path';
13
+ import { assertNotS2Abs, fail, isS2AbsPath, isS2RelPath, kitLayoutJoin, normalizeSlashPath, packageRoot, resolveTarget, takeOption, toRel, } from "./cli-shared.js";
14
+ import { isExecuteHatSkipped } from "./cli-skills.js";
15
+ import { evaluateHostContract, } from "./host-contract.js";
16
+ import { yamlLoad } from "./yaml.js";
17
+ export { sniffHostContract } from "./host-contract.js";
18
+ const HOST_USAGE = 'host validate [--file PATH] [--json]\n host apply --tools LIST|all [--profile core|expanded] [--target PATH] [--file PATH] [--json] [--dry-run|--yes]\n host update [--tools LIST|all] [--profile core|expanded] [--target PATH] [--file PATH] [--json] [--dry-run|--yes] [--force]';
19
+ const APPLY_USAGE = 'host apply --tools cursor,claude|all [--profile core|expanded] [--target PATH] [--file PATH] [--json] [--dry-run|--yes]';
20
+ const UPDATE_USAGE = 'host update [--tools LIST|all] [--profile core|expanded] [--target PATH] [--file PATH] [--json] [--dry-run|--yes] [--force]';
21
+ const DEFAULT_EXAMPLE_REL = path.join('assets', 'ide', 'host-adapt', 'examples', 'mvp-hosts.yaml');
22
+ /** 粘性文件(相对 `.coding-kit/`) */
23
+ const HOST_TOOLS_STICKY_REL = 'host-tools.json';
24
+ function hostToolsStickyAbs(target) {
25
+ return kitLayoutJoin(target, HOST_TOOLS_STICKY_REL);
26
+ }
27
+ function kitPackageSemver() {
28
+ try {
29
+ const raw = readFileSync(path.join(packageRoot(), 'package.json'), 'utf8');
30
+ const pkg = JSON.parse(raw);
31
+ return typeof pkg.version === 'string' && pkg.version.trim().length > 0
32
+ ? pkg.version.trim()
33
+ : undefined;
34
+ }
35
+ catch {
36
+ return undefined;
37
+ }
38
+ }
39
+ /** 解析并校验粘性 JSON;损坏 → exit 2 */
40
+ export function parseHostToolsSticky(raw) {
41
+ let data;
42
+ try {
43
+ data = JSON.parse(raw);
44
+ }
45
+ catch {
46
+ fail('粘性文件 JSON 损坏: .coding-kit/host-tools.json(请删除后重新 host apply / update)', 2);
47
+ }
48
+ if (!isPlainObject(data)) {
49
+ fail('粘性文件 schema 无效: .coding-kit/host-tools.json(根须为对象;请删除后重建)', 2);
50
+ }
51
+ if (data.version !== 1) {
52
+ fail(`粘性文件 schema 无效: version 须为 1(收到: ${String(data.version)});请删除后重建`, 2);
53
+ }
54
+ if (!Array.isArray(data.host_ids) ||
55
+ data.host_ids.length < 1 ||
56
+ !data.host_ids.every((id) => typeof id === 'string' && id.trim().length > 0)) {
57
+ fail('粘性文件 schema 无效: host_ids 须为非空字符串数组;请删除后重建', 2);
58
+ }
59
+ if (typeof data.profile !== 'string' || data.profile.trim().length < 1) {
60
+ fail('粘性文件 schema 无效: profile 须为非空字符串;请删除后重建', 2);
61
+ }
62
+ if (typeof data.updated_at !== 'string' || data.updated_at.trim().length < 1) {
63
+ fail('粘性文件 schema 无效: updated_at 须为非空字符串;请删除后重建', 2);
64
+ }
65
+ if (data.kit_semver !== undefined && typeof data.kit_semver !== 'string') {
66
+ fail('粘性文件 schema 无效: kit_semver 须为字符串;请删除后重建', 2);
67
+ }
68
+ const sticky = {
69
+ version: 1,
70
+ host_ids: uniqueKeepOrder(data.host_ids.map((s) => s.trim()).filter((s) => s.length > 0)),
71
+ profile: data.profile.trim(),
72
+ updated_at: data.updated_at.trim(),
73
+ };
74
+ if (typeof data.kit_semver === 'string' && data.kit_semver.trim().length > 0) {
75
+ sticky.kit_semver = data.kit_semver.trim();
76
+ }
77
+ return sticky;
78
+ }
79
+ /** 读取粘性;缺失 → null;损坏 → exit 2 */
80
+ export function loadHostToolsSticky(target) {
81
+ const abs = hostToolsStickyAbs(target);
82
+ if (!existsSync(abs))
83
+ return null;
84
+ let raw;
85
+ try {
86
+ raw = readFileSync(abs, 'utf8');
87
+ }
88
+ catch {
89
+ fail('粘性文件无法读取: .coding-kit/host-tools.json(请删除后重建)', 2);
90
+ }
91
+ return parseHostToolsSticky(raw);
92
+ }
93
+ /** apply/update --yes 成功写盘后写入/更新粘性(dry-run 不调用) */
94
+ export function writeHostToolsSticky(target, hostIds, profile) {
95
+ const abs = hostToolsStickyAbs(target);
96
+ const body = {
97
+ version: 1,
98
+ host_ids: uniqueKeepOrder(hostIds),
99
+ profile,
100
+ updated_at: new Date().toISOString(),
101
+ };
102
+ const semver = kitPackageSemver();
103
+ if (semver)
104
+ body.kit_semver = semver;
105
+ mkdirSync(path.dirname(abs), { recursive: true });
106
+ atomicWrite(abs, `${JSON.stringify(body, null, 2)}\n`);
107
+ }
108
+ /**
109
+ * 解析 --tools LIST|all(`none` 仅 init,apply/update 拒)。
110
+ * `all` = 适配表全部 host_id(表顺序)。
111
+ */
112
+ function resolveToolsList(toolsArg, knownIds, cmd, usage) {
113
+ const tokens = uniqueKeepOrder(toolsArg
114
+ .split(',')
115
+ .map((s) => s.trim())
116
+ .filter((s) => s.length > 0));
117
+ if (tokens.length < 1) {
118
+ fail(`${cmd} 须 --tools LIST|all(逗号分隔 host_id)\n用法: ${usage}`);
119
+ }
120
+ if (tokens.includes('none')) {
121
+ fail(`${cmd} 不支持 --tools none(仅 init)\n用法: ${usage}`);
122
+ }
123
+ if (tokens.includes('all')) {
124
+ if (tokens.length !== 1) {
125
+ fail(`${cmd} --tools all 不可与其它 host_id 混用\n用法: ${usage}`);
126
+ }
127
+ return uniqueKeepOrder(knownIds);
128
+ }
129
+ return tokens;
130
+ }
131
+ /** core 五 verb:Cursor 扁平 kit-<verb>.md;Claude 子目录 <verb>.md */
132
+ const CORE_COMMAND_VERBS = [
133
+ 'verify',
134
+ 'gate-status',
135
+ 'init-guide',
136
+ 'apply-standards',
137
+ 'hat-reanchor',
138
+ ];
139
+ /**
140
+ * expanded stems:Cursor = kit-<stem>.md;Claude kit/ = <stem>.md → /kit:<stem>
141
+ * 至少五条 hat;另含 graph-check / sync-prompts-guide。禁 kit-30 / kit-publish。
142
+ */
143
+ const EXPANDED_COMMAND_STEMS = [
144
+ 'hat-00-delegate',
145
+ 'hat-10-spec',
146
+ 'hat-10-task',
147
+ 'hat-20-spec-audit',
148
+ 'hat-20-task-audit',
149
+ 'graph-check',
150
+ 'sync-prompts-guide',
151
+ ];
152
+ function parseCoreCommandBasename(base) {
153
+ for (const verb of CORE_COMMAND_VERBS) {
154
+ if (base === `kit-${verb}.md` || base === `${verb}.md`)
155
+ return verb;
156
+ }
157
+ return null;
158
+ }
159
+ function parseExpandedCommandBasename(base) {
160
+ for (const stem of EXPANDED_COMMAND_STEMS) {
161
+ if (base === `kit-${stem}.md` || base === `${stem}.md`)
162
+ return stem;
163
+ }
164
+ return null;
165
+ }
166
+ /** expanded ⊇ core:请求 expanded 时仍物化 profile:core 条目 */
167
+ function commandEntryApplies(entryProfile, requested) {
168
+ if (!entryProfile)
169
+ return true;
170
+ if (entryProfile === requested)
171
+ return true;
172
+ if (requested === 'expanded' && entryProfile === 'core')
173
+ return true;
174
+ return false;
175
+ }
176
+ function assertHostProfile(profile, cmd, usage) {
177
+ if (profile !== 'core' && profile !== 'expanded') {
178
+ fail(`${cmd} 仅支持 --profile core|expanded(收到: ${profile})\n用法: ${usage}`);
179
+ }
180
+ }
181
+ /** 旧 Claude 扁平落点(2.0):.claude/commands/kit-<verb>.md */
182
+ function legacyClaudeFlatCommandRels() {
183
+ return CORE_COMMAND_VERBS.map((v) => `.claude/commands/kit-${v}.md`);
184
+ }
185
+ function findLegacyClaudeFlatCommands(target) {
186
+ return legacyClaudeFlatCommandRels().filter((rel) => existsSync(path.join(target, rel)));
187
+ }
188
+ const PRODUCT_BEGIN = '<!-- cyning-harness:begin -->';
189
+ const PRODUCT_END = '<!-- cyning-harness:end -->';
190
+ const LOCAL_BEGIN = '<!-- cyning-harness-local:begin -->';
191
+ const LOCAL_END = '<!-- cyning-harness-local:end -->';
192
+ const BACKUP_KEEP = 5;
193
+ function isPlainObject(v) {
194
+ return v !== null && typeof v === 'object' && !Array.isArray(v);
195
+ }
196
+ function requireString(obj, key, base, issues) {
197
+ const v = obj[key];
198
+ if (typeof v !== 'string' || v.trim().length < 1) {
199
+ issues.push({ path: `${base}.${key}`, code: 'schema', message: `须为非空字符串` });
200
+ return null;
201
+ }
202
+ return v;
203
+ }
204
+ function checkS2Field(rel, fieldPath, issues) {
205
+ if (isS2RelPath(rel)) {
206
+ issues.push({
207
+ path: fieldPath,
208
+ code: 's2',
209
+ message: `路径命中 S2 过程域(拒): ${rel}`,
210
+ });
211
+ }
212
+ }
213
+ function validateAlwaysOn(entries, base, issues) {
214
+ if (!Array.isArray(entries)) {
215
+ issues.push({ path: base, code: 'schema', message: '须为数组' });
216
+ return;
217
+ }
218
+ entries.forEach((item, i) => {
219
+ const p = `${base}[${i}]`;
220
+ if (!isPlainObject(item)) {
221
+ issues.push({ path: p, code: 'schema', message: '须为对象' });
222
+ return;
223
+ }
224
+ const extra = Object.keys(item).filter((k) => k !== 'target' && k !== 'source');
225
+ if (extra.length > 0) {
226
+ issues.push({ path: p, code: 'schema', message: `未知字段: ${extra.join(', ')}` });
227
+ }
228
+ const target = requireString(item, 'target', p, issues);
229
+ requireString(item, 'source', p, issues);
230
+ if (target)
231
+ checkS2Field(target, `${p}.target`, issues);
232
+ });
233
+ }
234
+ function validateDirFrom(entries, base, issues, opts = {}) {
235
+ if (!Array.isArray(entries)) {
236
+ issues.push({ path: base, code: 'schema', message: '须为数组' });
237
+ return;
238
+ }
239
+ const allowed = opts.allowProfile
240
+ ? new Set(['target_dir', 'from', 'profile'])
241
+ : new Set(['target_dir', 'from']);
242
+ entries.forEach((item, i) => {
243
+ const p = `${base}[${i}]`;
244
+ if (!isPlainObject(item)) {
245
+ issues.push({ path: p, code: 'schema', message: '须为对象' });
246
+ return;
247
+ }
248
+ const extra = Object.keys(item).filter((k) => !allowed.has(k));
249
+ if (extra.length > 0) {
250
+ issues.push({ path: p, code: 'schema', message: `未知字段: ${extra.join(', ')}` });
251
+ }
252
+ const targetDir = requireString(item, 'target_dir', p, issues);
253
+ requireString(item, 'from', p, issues);
254
+ if (opts.allowProfile && item.profile !== undefined) {
255
+ const prof = item.profile;
256
+ if (typeof prof !== 'string' || !['core', 'expanded', 'custom'].includes(prof)) {
257
+ issues.push({
258
+ path: `${p}.profile`,
259
+ code: 'schema',
260
+ message: `profile 须为 core|expanded|custom(收到: ${String(prof)})`,
261
+ });
262
+ }
263
+ }
264
+ if (targetDir)
265
+ checkS2Field(targetDir, `${p}.target_dir`, issues);
266
+ });
267
+ }
268
+ function validateVerify(v, base, issues) {
269
+ if (v === undefined)
270
+ return;
271
+ if (!isPlainObject(v)) {
272
+ issues.push({ path: base, code: 'schema', message: '须为对象' });
273
+ return;
274
+ }
275
+ const extra = Object.keys(v).filter((k) => !['kind', 'bin', 'failClosed'].includes(k));
276
+ if (extra.length > 0) {
277
+ issues.push({ path: base, code: 'schema', message: `未知字段: ${extra.join(', ')}` });
278
+ }
279
+ if (v.kind !== 'cli') {
280
+ issues.push({ path: `${base}.kind`, code: 'schema', message: `须为 "cli"` });
281
+ }
282
+ requireString(v, 'bin', base, issues);
283
+ if (v.failClosed !== undefined && typeof v.failClosed !== 'boolean') {
284
+ issues.push({ path: `${base}.failClosed`, code: 'schema', message: '须为 boolean' });
285
+ }
286
+ }
287
+ /** 对照 host-adapt.schema.json 的手写校验 + S2 扫描 */
288
+ export function validateHostAdaptDoc(data) {
289
+ const issues = [];
290
+ if (!isPlainObject(data)) {
291
+ issues.push({ path: '$', code: 'schema', message: '根须为对象' });
292
+ return issues;
293
+ }
294
+ const extraRoot = Object.keys(data).filter((k) => k !== 'version' && k !== 'hosts');
295
+ if (extraRoot.length > 0) {
296
+ issues.push({ path: '$', code: 'schema', message: `未知字段: ${extraRoot.join(', ')}` });
297
+ }
298
+ requireString(data, 'version', '$', issues);
299
+ if (!Array.isArray(data.hosts) || data.hosts.length < 1) {
300
+ issues.push({ path: '$.hosts', code: 'schema', message: '须为非空数组' });
301
+ return issues;
302
+ }
303
+ data.hosts.forEach((row, i) => {
304
+ const p = `$.hosts[${i}]`;
305
+ if (!isPlainObject(row)) {
306
+ issues.push({ path: p, code: 'schema', message: '须为对象' });
307
+ return;
308
+ }
309
+ const extra = Object.keys(row).filter((k) => k !== 'host_id' && k !== 'surfaces');
310
+ if (extra.length > 0) {
311
+ issues.push({ path: p, code: 'schema', message: `未知字段: ${extra.join(', ')}` });
312
+ }
313
+ requireString(row, 'host_id', p, issues);
314
+ const surfaces = row.surfaces;
315
+ if (!isPlainObject(surfaces)) {
316
+ issues.push({ path: `${p}.surfaces`, code: 'schema', message: '须为对象' });
317
+ return;
318
+ }
319
+ const extraSurf = Object.keys(surfaces).filter((k) => !['always_on', 'skills', 'commands', 'verify'].includes(k));
320
+ if (extraSurf.length > 0) {
321
+ issues.push({
322
+ path: `${p}.surfaces`,
323
+ code: 'schema',
324
+ message: `未知字段: ${extraSurf.join(', ')}`,
325
+ });
326
+ }
327
+ for (const req of ['always_on', 'skills', 'commands']) {
328
+ if (!(req in surfaces)) {
329
+ issues.push({ path: `${p}.surfaces.${req}`, code: 'schema', message: '必填' });
330
+ }
331
+ }
332
+ validateAlwaysOn(surfaces.always_on, `${p}.surfaces.always_on`, issues);
333
+ validateDirFrom(surfaces.skills, `${p}.surfaces.skills`, issues);
334
+ validateDirFrom(surfaces.commands, `${p}.surfaces.commands`, issues, { allowProfile: true });
335
+ validateVerify(surfaces.verify, `${p}.surfaces.verify`, issues);
336
+ });
337
+ return issues;
338
+ }
339
+ function resolveValidateFile(fileArg) {
340
+ if (fileArg)
341
+ return path.resolve(process.cwd(), fileArg);
342
+ return path.join(packageRoot(), DEFAULT_EXAMPLE_REL);
343
+ }
344
+ /** 适配表 host_id 列表(表顺序);供 init 询问 / `--tools` 校验复用 */
345
+ export function listKnownHostIds(fileArg) {
346
+ const fileAbs = resolveValidateFile(fileArg);
347
+ if (!existsSync(fileAbs)) {
348
+ fail(`host 适配表不存在: ${fileAbs}`);
349
+ }
350
+ let data;
351
+ try {
352
+ data = yamlLoad(readFileSync(fileAbs, 'utf8'));
353
+ }
354
+ catch (err) {
355
+ fail(`host 适配表 YAML 解析失败: ${err.message}`);
356
+ }
357
+ const issues = validateHostAdaptDoc(data);
358
+ if (issues.length > 0) {
359
+ fail(`host 适配表无效:\n${issues.map((e) => ` - [${e.code}] ${e.path}: ${e.message}`).join('\n')}`);
360
+ }
361
+ return asHostRows(data).map((r) => r.host_id);
362
+ }
363
+ async function cmdHostValidate(args) {
364
+ if (args.includes('--help') || args.includes('-h')) {
365
+ console.log(`用法: npx spec-wave ${HOST_USAGE}`);
366
+ return;
367
+ }
368
+ const json = args.includes('--json');
369
+ let rest = args.filter((a) => a !== '--json');
370
+ // --file 出现但无值 → 用法错误 exit 1
371
+ const fileIdx = rest.indexOf('--file');
372
+ if (fileIdx !== -1 && (fileIdx + 1 >= rest.length || rest[fileIdx + 1].startsWith('-'))) {
373
+ fail(`host validate 须 --file PATH\n用法: ${HOST_USAGE}`);
374
+ }
375
+ const { value: fileArg, rest: r1 } = takeOption(rest, '--file');
376
+ rest = r1;
377
+ if (rest.length > 0)
378
+ fail(`host validate 未知参数: ${rest.join(' ')}\n用法: ${HOST_USAGE}`);
379
+ const abs = resolveValidateFile(fileArg);
380
+ if (!existsSync(abs)) {
381
+ fail(`host validate 文件不存在: ${abs}`);
382
+ }
383
+ let data;
384
+ try {
385
+ data = yamlLoad(readFileSync(abs, 'utf8'));
386
+ }
387
+ catch (err) {
388
+ const msg = `YAML 解析失败: ${err.message}`;
389
+ if (json) {
390
+ console.log(JSON.stringify({
391
+ command: 'host validate',
392
+ file: abs,
393
+ ok: false,
394
+ verdict: 'FAIL',
395
+ errors: [{ path: '$', code: 'parse', message: msg }],
396
+ }, null, 2));
397
+ }
398
+ else {
399
+ console.error(msg);
400
+ console.log('HOST VALIDATE: FAIL');
401
+ }
402
+ fail('', 2);
403
+ }
404
+ const issues = validateHostAdaptDoc(data);
405
+ if (issues.length > 0) {
406
+ if (json) {
407
+ console.log(JSON.stringify({
408
+ command: 'host validate',
409
+ file: abs,
410
+ ok: false,
411
+ verdict: 'FAIL',
412
+ errors: issues,
413
+ }, null, 2));
414
+ }
415
+ else {
416
+ for (const e of issues) {
417
+ console.error(` - [${e.code}] ${e.path}: ${e.message}`);
418
+ }
419
+ console.log('HOST VALIDATE: FAIL');
420
+ }
421
+ fail('', 2);
422
+ }
423
+ if (json) {
424
+ console.log(JSON.stringify({
425
+ command: 'host validate',
426
+ file: abs,
427
+ ok: true,
428
+ verdict: 'PASS',
429
+ }, null, 2));
430
+ return;
431
+ }
432
+ console.log(`file: ${abs}`);
433
+ console.log('HOST VALIDATE: PASS');
434
+ }
435
+ function takeOptionalFlag(args, name, usage, cmd = 'host apply') {
436
+ const idx = args.indexOf(name);
437
+ if (idx === -1)
438
+ return { value: undefined, rest: args };
439
+ if (idx + 1 >= args.length || args[idx + 1].startsWith('-')) {
440
+ fail(`${cmd} ${name} 须跟值\n用法: ${usage}`);
441
+ }
442
+ return takeOption(args, name);
443
+ }
444
+ function uniqueKeepOrder(ids) {
445
+ const seen = new Set();
446
+ const out = [];
447
+ for (const id of ids) {
448
+ if (seen.has(id))
449
+ continue;
450
+ seen.add(id);
451
+ out.push(id);
452
+ }
453
+ return out;
454
+ }
455
+ function asHostRows(data) {
456
+ const root = data;
457
+ return root.hosts;
458
+ }
459
+ function isMarkdownMergeTarget(destRel) {
460
+ const base = path.basename(destRel);
461
+ return base === 'CLAUDE.md' || base === 'AGENTS.md';
462
+ }
463
+ function isKitManagedContent(content) {
464
+ if (content.includes(PRODUCT_BEGIN) || content.includes(PRODUCT_END))
465
+ return true;
466
+ if (content.includes('# Harness Starter(业务仓'))
467
+ return true;
468
+ if (content.includes('Harness Starter — 读序、task、人工闸与 Verify 纪律'))
469
+ return true;
470
+ return false;
471
+ }
472
+ function extractProductInner(source) {
473
+ const text = source.replace(/\r\n/g, '\n');
474
+ const b = text.indexOf(PRODUCT_BEGIN);
475
+ const e = text.indexOf(PRODUCT_END);
476
+ if (b !== -1 && e !== -1 && e > b) {
477
+ return text.slice(b + PRODUCT_BEGIN.length, e).replace(/^\n/, '').replace(/\n$/, '');
478
+ }
479
+ return text.replace(/\s+$/, '');
480
+ }
481
+ function wrapProductBlock(inner) {
482
+ const body = inner.replace(/\s+$/, '');
483
+ return `${PRODUCT_BEGIN}\n${body}\n${PRODUCT_END}\n`;
484
+ }
485
+ function productSpanHasLocal(text) {
486
+ const b = text.indexOf(PRODUCT_BEGIN);
487
+ const e = text.indexOf(PRODUCT_END);
488
+ if (b === -1 || e === -1 || e < b)
489
+ return false;
490
+ const span = text.slice(b, e + PRODUCT_END.length);
491
+ return span.includes(LOCAL_BEGIN) || span.includes(LOCAL_END);
492
+ }
493
+ function mergeMarkdownAlwaysOn(existing, sourceBody) {
494
+ const block = wrapProductBlock(extractProductInner(sourceBody));
495
+ if (existing === null)
496
+ return block;
497
+ const text = existing.replace(/\r\n/g, '\n');
498
+ const b = text.indexOf(PRODUCT_BEGIN);
499
+ const e = text.indexOf(PRODUCT_END);
500
+ if (b === -1 || e === -1 || e < b) {
501
+ const prefix = text.endsWith('\n') ? text : `${text}\n`;
502
+ return `${prefix}\n${block}`;
503
+ }
504
+ let rest = text.slice(e + PRODUCT_END.length);
505
+ if (rest.startsWith('\n'))
506
+ rest = rest.slice(1);
507
+ return text.slice(0, b) + block + rest;
508
+ }
509
+ function expandFromGlob(fromPat, root) {
510
+ const n = normalizeSlashPath(fromPat).replace(/^\.\//, '');
511
+ if (n.endsWith('/*')) {
512
+ const dirRel = n.slice(0, -2);
513
+ const dirAbs = path.join(root, dirRel);
514
+ if (!existsSync(dirAbs) || !statSync(dirAbs).isDirectory())
515
+ return [];
516
+ return readdirSync(dirAbs)
517
+ .filter((f) => statSync(path.join(dirAbs, f)).isFile())
518
+ .map((f) => normalizeSlashPath(path.join(dirRel, f)))
519
+ .sort();
520
+ }
521
+ const abs = path.join(root, n);
522
+ if (existsSync(abs) && statSync(abs).isFile())
523
+ return [n];
524
+ return [];
525
+ }
526
+ function expandSkillSources(fromPat, root) {
527
+ const n = normalizeSlashPath(fromPat).replace(/^\.\//, '');
528
+ const out = [];
529
+ const walkFiles = (absDir, sourceDirRel, innerPrefix) => {
530
+ if (!existsSync(absDir) || !statSync(absDir).isDirectory())
531
+ return;
532
+ for (const name of readdirSync(absDir).sort()) {
533
+ const abs = path.join(absDir, name);
534
+ const st = statSync(abs);
535
+ const sourceRel = normalizeSlashPath(path.join(sourceDirRel, name));
536
+ const innerRel = normalizeSlashPath(path.join(innerPrefix, name));
537
+ if (st.isDirectory()) {
538
+ if (isExecuteHatSkipped(absDir, name, false))
539
+ continue;
540
+ walkFiles(abs, sourceRel, innerRel);
541
+ }
542
+ else if (st.isFile()) {
543
+ out.push({ sourceRel, innerRel });
544
+ }
545
+ }
546
+ };
547
+ if (n.endsWith('/*')) {
548
+ const parentRel = n.slice(0, -2);
549
+ const parentAbs = path.join(root, parentRel);
550
+ if (!existsSync(parentAbs) || !statSync(parentAbs).isDirectory()) {
551
+ fail(`host 缺 skills 源目录: ${parentRel}`, 2);
552
+ }
553
+ for (const name of readdirSync(parentAbs).sort()) {
554
+ const abs = path.join(parentAbs, name);
555
+ if (!statSync(abs).isDirectory())
556
+ continue;
557
+ if (isExecuteHatSkipped(parentAbs, name, false))
558
+ continue;
559
+ walkFiles(abs, normalizeSlashPath(path.join(parentRel, name)), name);
560
+ }
561
+ return out.sort((a, b) => a.sourceRel.localeCompare(b.sourceRel));
562
+ }
563
+ const abs = path.join(root, n);
564
+ if (existsSync(abs) && statSync(abs).isDirectory()) {
565
+ if (!isExecuteHatSkipped(path.dirname(abs), path.basename(n), false)) {
566
+ walkFiles(abs, n, path.basename(n));
567
+ }
568
+ }
569
+ return out.sort((a, b) => a.sourceRel.localeCompare(b.sourceRel));
570
+ }
571
+ function backupsRoot(target, family) {
572
+ return kitLayoutJoin(target, 'backups', family);
573
+ }
574
+ function backupFile(target, genDir, rel) {
575
+ const src = path.join(target, rel);
576
+ const dest = path.join(genDir, rel);
577
+ mkdirSync(path.dirname(dest), { recursive: true });
578
+ copyFileSync(src, dest);
579
+ return dest;
580
+ }
581
+ function atomicWrite(abs, body) {
582
+ mkdirSync(path.dirname(abs), { recursive: true });
583
+ const tmp = `${abs}.tmp-host-apply-${process.pid}`;
584
+ writeFileSync(tmp, body, 'utf8');
585
+ renameSync(tmp, abs);
586
+ }
587
+ function pruneBackups(target, family) {
588
+ const root = backupsRoot(target, family);
589
+ if (!existsSync(root))
590
+ return;
591
+ const gens = readdirSync(root)
592
+ .filter((n) => statSync(path.join(root, n)).isDirectory())
593
+ .sort();
594
+ for (const old of gens.slice(0, Math.max(0, gens.length - BACKUP_KEEP))) {
595
+ rmSync(path.join(root, old), { recursive: true, force: true });
596
+ }
597
+ }
598
+ function hostBanner(command) {
599
+ return command === 'host update' ? 'HOST UPDATE' : 'HOST APPLY';
600
+ }
601
+ function printHostHuman(report) {
602
+ const banner = hostBanner(report.command);
603
+ console.log(`${banner}: ${report.mode}`);
604
+ console.log(`hosts: ${report.hosts.join(', ')}`);
605
+ const sections = [
606
+ ['planned', report.planned],
607
+ ['written', report.written],
608
+ ['skipped', report.skipped],
609
+ ['conflict', report.conflict],
610
+ ['removed', report.removed],
611
+ ];
612
+ for (const [label, items] of sections) {
613
+ console.log(`${label} (${items.length}):`);
614
+ if (items.length === 0)
615
+ console.log(' (无)');
616
+ else
617
+ for (const p of items)
618
+ console.log(` ${p}`);
619
+ }
620
+ if (report.backup)
621
+ console.log(`backup: ${report.backup}`);
622
+ console.log(`${banner}: ${report.verdict}`);
623
+ }
624
+ function emitHostFail(json, command, payload, extraLines) {
625
+ if (json) {
626
+ console.log(JSON.stringify({
627
+ ...payload,
628
+ ok: false,
629
+ verdict: 'FAIL',
630
+ }, null, 2));
631
+ }
632
+ else {
633
+ for (const line of extraLines)
634
+ console.error(line);
635
+ console.log(`${hostBanner(command)}: FAIL`);
636
+ }
637
+ fail('', 2);
638
+ }
639
+ function tableVersionOf(data) {
640
+ if (isPlainObject(data) && typeof data.version === 'string')
641
+ return data.version;
642
+ return '';
643
+ }
644
+ function emitU01Degraded(json, command, base, contract) {
645
+ const extraLines = [
646
+ 'U-01: 宿主契约不匹配(degraded),拒绝写盘',
647
+ ...contract.reasons.map((r) => ` - ${r}`),
648
+ ];
649
+ emitHostFail(json, command, {
650
+ ...base,
651
+ planned: [],
652
+ written: [],
653
+ skipped: [],
654
+ conflict: [],
655
+ removed: [],
656
+ backup: null,
657
+ contract,
658
+ }, extraLines);
659
+ }
660
+ function ensureBackupGen(target, family, genDir) {
661
+ if (genDir)
662
+ return { genDir, backup: toRel(target, genDir) };
663
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
664
+ const next = path.join(backupsRoot(target, family), ts);
665
+ mkdirSync(next, { recursive: true });
666
+ return { genDir: next, backup: toRel(target, next) };
667
+ }
668
+ function commitPlannedWrites(target, items, family, legacyRemoveRels = []) {
669
+ const toWrite = items.filter((i) => i.op === 'write' || i.op === 'merge');
670
+ for (const item of toWrite)
671
+ assertNotS2Abs(item.destAbs);
672
+ const needBackup = toWrite.some((i) => existsSync(i.destAbs)) || legacyRemoveRels.length > 0;
673
+ let backup = null;
674
+ let genDir = null;
675
+ if (needBackup) {
676
+ const ensured = ensureBackupGen(target, family, null);
677
+ genDir = ensured.genDir;
678
+ backup = ensured.backup;
679
+ }
680
+ const written = [];
681
+ for (const item of toWrite) {
682
+ if (genDir && existsSync(item.destAbs))
683
+ backupFile(target, genDir, item.destRel);
684
+ atomicWrite(item.destAbs, item.nextText);
685
+ written.push(item.destRel);
686
+ }
687
+ const removed = [];
688
+ for (const rel of legacyRemoveRels) {
689
+ const abs = path.join(target, rel);
690
+ if (!existsSync(abs))
691
+ continue;
692
+ if (!genDir) {
693
+ const ensured = ensureBackupGen(target, family, genDir);
694
+ genDir = ensured.genDir;
695
+ backup = ensured.backup;
696
+ }
697
+ backupFile(target, genDir, rel);
698
+ unlinkSync(abs);
699
+ removed.push(rel);
700
+ }
701
+ if (needBackup || removed.length > 0)
702
+ pruneBackups(target, family);
703
+ return { written, removed, backup };
704
+ }
705
+ function planApply(opts) {
706
+ const items = [];
707
+ const s2 = [];
708
+ const rowById = new Map(opts.rows.map((r) => [r.host_id, r]));
709
+ const pushDest = (destRelRaw, destAbs) => {
710
+ const destRel = normalizeSlashPath(destRelRaw);
711
+ if (isS2RelPath(destRel) || isS2AbsPath(destAbs))
712
+ s2.push(destRel);
713
+ return destRel;
714
+ };
715
+ for (const hostId of opts.toolIds) {
716
+ const row = rowById.get(hostId);
717
+ if (!row)
718
+ continue;
719
+ for (const entry of row.surfaces.always_on) {
720
+ const sourceRel = normalizeSlashPath(entry.source);
721
+ const sourceAbs = path.join(opts.pkgRoot, sourceRel);
722
+ const destAbs = path.resolve(opts.target, entry.target);
723
+ const destRel = pushDest(toRel(opts.target, destAbs), destAbs);
724
+ if (!existsSync(sourceAbs)) {
725
+ fail(`host apply 缺 always_on 源文件: ${sourceRel}`, 2);
726
+ }
727
+ const sourceBody = readFileSync(sourceAbs, 'utf8');
728
+ const exists = existsSync(destAbs);
729
+ const existing = exists ? readFileSync(destAbs, 'utf8') : null;
730
+ if (isMarkdownMergeTarget(destRel)) {
731
+ if (existing && productSpanHasLocal(existing)) {
732
+ items.push({
733
+ hostId,
734
+ kind: 'always_on',
735
+ destRel,
736
+ destAbs,
737
+ sourceRel,
738
+ sourceAbs,
739
+ op: 'conflict',
740
+ nextText: existing,
741
+ });
742
+ continue;
743
+ }
744
+ const nextText = mergeMarkdownAlwaysOn(existing, sourceBody);
745
+ let op = exists ? 'merge' : 'write';
746
+ if (existing !== null && existing === nextText)
747
+ op = 'skip_identical';
748
+ items.push({
749
+ hostId,
750
+ kind: 'always_on',
751
+ destRel,
752
+ destAbs,
753
+ sourceRel,
754
+ sourceAbs,
755
+ op,
756
+ nextText,
757
+ });
758
+ continue;
759
+ }
760
+ // .mdc 及其他 always_on:缺失则写;已存在不同内容:kit 管理则覆写,否则 conflict
761
+ if (!exists) {
762
+ items.push({
763
+ hostId,
764
+ kind: 'always_on',
765
+ destRel,
766
+ destAbs,
767
+ sourceRel,
768
+ sourceAbs,
769
+ op: 'write',
770
+ nextText: sourceBody,
771
+ });
772
+ continue;
773
+ }
774
+ if (existing === sourceBody) {
775
+ items.push({
776
+ hostId,
777
+ kind: 'always_on',
778
+ destRel,
779
+ destAbs,
780
+ sourceRel,
781
+ sourceAbs,
782
+ op: 'skip_identical',
783
+ nextText: sourceBody,
784
+ });
785
+ continue;
786
+ }
787
+ if (isKitManagedContent(existing ?? '')) {
788
+ items.push({
789
+ hostId,
790
+ kind: 'always_on',
791
+ destRel,
792
+ destAbs,
793
+ sourceRel,
794
+ sourceAbs,
795
+ op: 'write',
796
+ nextText: sourceBody,
797
+ });
798
+ }
799
+ else {
800
+ items.push({
801
+ hostId,
802
+ kind: 'always_on',
803
+ destRel,
804
+ destAbs,
805
+ sourceRel,
806
+ sourceAbs,
807
+ op: 'conflict',
808
+ nextText: sourceBody,
809
+ });
810
+ }
811
+ }
812
+ // skills:与表一致物化;跳过 30/40(isExecuteHatSkipped)
813
+ for (const entry of row.surfaces.skills ?? []) {
814
+ const sources = expandSkillSources(entry.from, opts.pkgRoot);
815
+ for (const src of sources) {
816
+ const sourceAbs = path.join(opts.pkgRoot, src.sourceRel);
817
+ const destAbs = path.resolve(opts.target, entry.target_dir, src.innerRel);
818
+ const destRel = pushDest(toRel(opts.target, destAbs), destAbs);
819
+ const sourceBody = readFileSync(sourceAbs, 'utf8');
820
+ const exists = existsSync(destAbs);
821
+ const existing = exists ? readFileSync(destAbs, 'utf8') : null;
822
+ let op = 'write';
823
+ if (exists && existing === sourceBody)
824
+ op = 'skip_identical';
825
+ items.push({
826
+ hostId,
827
+ kind: 'skill',
828
+ destRel,
829
+ destAbs,
830
+ sourceRel: src.sourceRel,
831
+ sourceAbs,
832
+ op,
833
+ nextText: sourceBody,
834
+ });
835
+ }
836
+ }
837
+ for (const entry of row.surfaces.commands) {
838
+ if (!commandEntryApplies(entry.profile, opts.profile))
839
+ continue;
840
+ const band = entry.profile === 'expanded' ? 'expanded' : 'core';
841
+ const matched = expandFromGlob(entry.from, opts.pkgRoot).filter((rel) => {
842
+ const base = path.basename(rel);
843
+ return band === 'core'
844
+ ? parseCoreCommandBasename(base) !== null
845
+ : parseExpandedCommandBasename(base) !== null;
846
+ });
847
+ if (band === 'core') {
848
+ const have = new Set(matched
849
+ .map((rel) => parseCoreCommandBasename(path.basename(rel)))
850
+ .filter((v) => v !== null));
851
+ const missing = CORE_COMMAND_VERBS.filter((v) => !have.has(v));
852
+ if (missing.length > 0) {
853
+ fail(`host apply 缺 core 命令资产(from=${entry.from}): ${missing.map((v) => `${v}.md|kit-${v}.md`).join(', ')}`, 2);
854
+ }
855
+ }
856
+ else {
857
+ const have = new Set(matched
858
+ .map((rel) => parseExpandedCommandBasename(path.basename(rel)))
859
+ .filter((v) => v !== null));
860
+ const missing = EXPANDED_COMMAND_STEMS.filter((v) => !have.has(v));
861
+ if (missing.length > 0) {
862
+ fail(`host apply 缺 expanded 命令资产(from=${entry.from}): ${missing.map((v) => `${v}.md|kit-${v}.md`).join(', ')}`, 2);
863
+ }
864
+ }
865
+ for (const sourceRelRaw of matched) {
866
+ const sourceRel = normalizeSlashPath(sourceRelRaw);
867
+ const sourceAbs = path.join(opts.pkgRoot, sourceRel);
868
+ const destAbs = path.resolve(opts.target, entry.target_dir, path.basename(sourceRel));
869
+ const destRel = pushDest(toRel(opts.target, destAbs), destAbs);
870
+ const sourceBody = readFileSync(sourceAbs, 'utf8');
871
+ const exists = existsSync(destAbs);
872
+ const existing = exists ? readFileSync(destAbs, 'utf8') : null;
873
+ let op = 'write';
874
+ if (exists && existing === sourceBody)
875
+ op = 'skip_identical';
876
+ items.push({
877
+ hostId,
878
+ kind: 'command',
879
+ destRel,
880
+ destAbs,
881
+ sourceRel,
882
+ sourceAbs,
883
+ op,
884
+ nextText: sourceBody,
885
+ });
886
+ }
887
+ }
888
+ }
889
+ return { items, s2 };
890
+ }
891
+ function remapUpdateConflicts(items, force) {
892
+ for (const item of items) {
893
+ if ((item.kind === 'command' || item.kind === 'skill') && item.op === 'write' && existsSync(item.destAbs)) {
894
+ if (!force)
895
+ item.op = 'conflict';
896
+ }
897
+ if (item.kind === 'always_on' && item.op === 'conflict' && force) {
898
+ const existing = existsSync(item.destAbs) ? readFileSync(item.destAbs, 'utf8') : '';
899
+ if (!productSpanHasLocal(existing))
900
+ item.op = 'write';
901
+ }
902
+ }
903
+ }
904
+ async function cmdHostApply(args) {
905
+ if (args.includes('--help') || args.includes('-h')) {
906
+ console.log(`用法: npx spec-wave ${APPLY_USAGE}`);
907
+ return;
908
+ }
909
+ const yes = args.includes('--yes');
910
+ const dryRunFlag = args.includes('--dry-run');
911
+ const json = args.includes('--json');
912
+ if (yes && dryRunFlag) {
913
+ fail(`host apply: --yes 与 --dry-run 不可同现\n用法: ${APPLY_USAGE}`);
914
+ }
915
+ let rest = args.filter((a) => a !== '--yes' && a !== '--dry-run' && a !== '--json');
916
+ if (!rest.includes('--tools')) {
917
+ fail(`host apply 须 --tools LIST|all(逗号分隔 host_id)\n用法: ${APPLY_USAGE}`);
918
+ }
919
+ const toolsIdx = rest.indexOf('--tools');
920
+ if (toolsIdx + 1 >= rest.length || rest[toolsIdx + 1].startsWith('-')) {
921
+ fail(`host apply 须 --tools LIST|all\n用法: ${APPLY_USAGE}`);
922
+ }
923
+ const { value: toolsArg, rest: rTools } = takeOption(rest, '--tools');
924
+ rest = rTools;
925
+ const { value: profileArg, rest: rProfile } = takeOptionalFlag(rest, '--profile', APPLY_USAGE);
926
+ rest = rProfile;
927
+ const { value: targetArg, rest: rTarget } = takeOptionalFlag(rest, '--target', APPLY_USAGE);
928
+ rest = rTarget;
929
+ const { value: fileArg, rest: rFile } = takeOptionalFlag(rest, '--file', APPLY_USAGE);
930
+ rest = rFile;
931
+ if (rest.length > 0)
932
+ fail(`host apply 未知参数: ${rest.join(' ')}\n用法: ${APPLY_USAGE}`);
933
+ const profile = profileArg ?? 'core';
934
+ assertHostProfile(profile, 'host apply', APPLY_USAGE);
935
+ const target = resolveTarget(process.cwd(), targetArg);
936
+ if (!existsSync(target) || !statSync(target).isDirectory()) {
937
+ fail(`host apply target 不存在或不是目录: ${target}`);
938
+ }
939
+ const fileAbs = resolveValidateFile(fileArg);
940
+ if (!existsSync(fileAbs))
941
+ fail(`host apply 文件不存在: ${fileAbs}`);
942
+ const mode = yes ? 'apply' : 'dry-run';
943
+ const baseReport = {
944
+ command: 'host apply',
945
+ mode,
946
+ hosts: [],
947
+ planned: [],
948
+ written: [],
949
+ skipped: [],
950
+ conflict: [],
951
+ removed: [],
952
+ backup: null,
953
+ };
954
+ let data;
955
+ try {
956
+ data = yamlLoad(readFileSync(fileAbs, 'utf8'));
957
+ }
958
+ catch (err) {
959
+ const msg = `YAML 解析失败: ${err.message}`;
960
+ emitHostFail(json, 'host apply', { ...baseReport, errors: [{ path: '$', code: 'parse', message: msg }] }, [msg]);
961
+ }
962
+ const issues = validateHostAdaptDoc(data);
963
+ if (issues.length > 0) {
964
+ emitHostFail(json, 'host apply', { ...baseReport, errors: issues }, issues.map((e) => ` - [${e.code}] ${e.path}: ${e.message}`));
965
+ }
966
+ const rows = asHostRows(data);
967
+ const knownIds = rows.map((r) => r.host_id);
968
+ const known = new Set(knownIds);
969
+ const toolIds = resolveToolsList(toolsArg ?? '', knownIds, 'host apply', APPLY_USAGE);
970
+ baseReport.hosts = toolIds;
971
+ const unknown = toolIds.filter((id) => !known.has(id));
972
+ if (unknown.length > 0) {
973
+ fail(`host apply 未知 host_id: ${unknown.join(', ')}\n用法: ${APPLY_USAGE}`);
974
+ }
975
+ const contract = evaluateHostContract(tableVersionOf(data));
976
+ if (contract.status === 'degraded') {
977
+ emitU01Degraded(json, 'host apply', baseReport, contract);
978
+ }
979
+ const { items, s2 } = planApply({
980
+ target,
981
+ rows,
982
+ toolIds,
983
+ profile,
984
+ pkgRoot: packageRoot(),
985
+ });
986
+ if (s2.length > 0) {
987
+ const uniq = uniqueKeepOrder(s2);
988
+ emitHostFail(json, 'host apply', {
989
+ ...baseReport,
990
+ errors: uniq.map((p) => ({
991
+ path: p,
992
+ code: 's2',
993
+ message: `路径命中 S2 过程域(拒): ${p}`,
994
+ })),
995
+ }, uniq.map((p) => ` - [s2] ${p}`));
996
+ }
997
+ const planned = items.filter((i) => i.op === 'write' || i.op === 'merge').map((i) => i.destRel);
998
+ const skipped = items.filter((i) => i.op === 'skip_identical').map((i) => i.destRel);
999
+ const conflict = items.filter((i) => i.op === 'conflict').map((i) => i.destRel);
1000
+ // Claude 在工具列表时:清除旧扁平 kit-*.md,禁止与 kit/<verb>.md 双份并存
1001
+ const legacyRemove = toolIds.includes('claude') ? findLegacyClaudeFlatCommands(target) : [];
1002
+ let written = [];
1003
+ let removed = [];
1004
+ let backup = null;
1005
+ if (yes) {
1006
+ const committed = commitPlannedWrites(target, items, 'host-apply', legacyRemove);
1007
+ written = committed.written;
1008
+ removed = committed.removed;
1009
+ backup = committed.backup;
1010
+ writeHostToolsSticky(target, toolIds, profile);
1011
+ }
1012
+ const report = {
1013
+ command: 'host apply',
1014
+ mode,
1015
+ hosts: toolIds,
1016
+ planned,
1017
+ written: yes ? written : [],
1018
+ skipped,
1019
+ conflict,
1020
+ removed: yes ? removed : legacyRemove,
1021
+ backup,
1022
+ contract,
1023
+ ok: true,
1024
+ verdict: 'PASS',
1025
+ };
1026
+ if (json)
1027
+ console.log(JSON.stringify(report, null, 2));
1028
+ else
1029
+ printHostHuman(report);
1030
+ }
1031
+ async function cmdHostUpdate(args) {
1032
+ if (args.includes('--help') || args.includes('-h')) {
1033
+ console.log(`用法: npx spec-wave ${UPDATE_USAGE}`);
1034
+ return;
1035
+ }
1036
+ const yes = args.includes('--yes');
1037
+ const dryRunFlag = args.includes('--dry-run');
1038
+ const json = args.includes('--json');
1039
+ const force = args.includes('--force');
1040
+ if (yes && dryRunFlag) {
1041
+ fail(`host update: --yes 与 --dry-run 不可同现\n用法: ${UPDATE_USAGE}`);
1042
+ }
1043
+ let rest = args.filter((a) => a !== '--yes' && a !== '--dry-run' && a !== '--json' && a !== '--force');
1044
+ let toolsArg;
1045
+ if (rest.includes('--tools')) {
1046
+ const toolsIdx = rest.indexOf('--tools');
1047
+ if (toolsIdx + 1 >= rest.length || rest[toolsIdx + 1].startsWith('-')) {
1048
+ fail(`host update 须 --tools LIST\n用法: ${UPDATE_USAGE}`);
1049
+ }
1050
+ const taken = takeOption(rest, '--tools');
1051
+ toolsArg = taken.value;
1052
+ rest = taken.rest;
1053
+ }
1054
+ const { value: profileArg, rest: rProfile } = takeOptionalFlag(rest, '--profile', UPDATE_USAGE, 'host update');
1055
+ rest = rProfile;
1056
+ const { value: targetArg, rest: rTarget } = takeOptionalFlag(rest, '--target', UPDATE_USAGE, 'host update');
1057
+ rest = rTarget;
1058
+ const { value: fileArg, rest: rFile } = takeOptionalFlag(rest, '--file', UPDATE_USAGE, 'host update');
1059
+ rest = rFile;
1060
+ if (rest.length > 0)
1061
+ fail(`host update 未知参数: ${rest.join(' ')}\n用法: ${UPDATE_USAGE}`);
1062
+ const profile = profileArg ?? 'core';
1063
+ assertHostProfile(profile, 'host update', UPDATE_USAGE);
1064
+ const target = resolveTarget(process.cwd(), targetArg);
1065
+ if (!existsSync(target) || !statSync(target).isDirectory()) {
1066
+ fail(`host update target 不存在或不是目录: ${target}`);
1067
+ }
1068
+ const fileAbs = resolveValidateFile(fileArg);
1069
+ if (!existsSync(fileAbs))
1070
+ fail(`host update 文件不存在: ${fileAbs}`);
1071
+ const mode = yes ? 'update' : 'dry-run';
1072
+ const baseReport = {
1073
+ command: 'host update',
1074
+ mode,
1075
+ hosts: [],
1076
+ planned: [],
1077
+ written: [],
1078
+ skipped: [],
1079
+ conflict: [],
1080
+ removed: [],
1081
+ backup: null,
1082
+ };
1083
+ let data;
1084
+ try {
1085
+ data = yamlLoad(readFileSync(fileAbs, 'utf8'));
1086
+ }
1087
+ catch (err) {
1088
+ const msg = `YAML 解析失败: ${err.message}`;
1089
+ emitHostFail(json, 'host update', { ...baseReport, errors: [{ path: '$', code: 'parse', message: msg }] }, [
1090
+ msg,
1091
+ ]);
1092
+ }
1093
+ const issues = validateHostAdaptDoc(data);
1094
+ if (issues.length > 0) {
1095
+ emitHostFail(json, 'host update', { ...baseReport, errors: issues }, issues.map((e) => ` - [${e.code}] ${e.path}: ${e.message}`));
1096
+ }
1097
+ const rows = asHostRows(data);
1098
+ const knownIds = rows.map((r) => r.host_id);
1099
+ const known = new Set(knownIds);
1100
+ // W2 方案 A:CLI `--tools` → 粘性 host_ids → 否则 exit 1(相对 2.1.0 全表为 BREAKING 小)
1101
+ let toolIds;
1102
+ if (toolsArg !== undefined) {
1103
+ toolIds = resolveToolsList(toolsArg, knownIds, 'host update', UPDATE_USAGE);
1104
+ }
1105
+ else {
1106
+ const sticky = loadHostToolsSticky(target);
1107
+ if (sticky !== null && sticky.host_ids.length >= 1) {
1108
+ toolIds = uniqueKeepOrder(sticky.host_ids);
1109
+ }
1110
+ else {
1111
+ fail(`host update 无粘性且未传 --tools:请先 host apply / init,或传 --tools LIST / --tools all\n用法: ${UPDATE_USAGE}`);
1112
+ }
1113
+ }
1114
+ const unknown = toolIds.filter((id) => !known.has(id));
1115
+ if (unknown.length > 0) {
1116
+ fail(`host update 未知 host_id: ${unknown.join(', ')}\n用法: ${UPDATE_USAGE}`);
1117
+ }
1118
+ baseReport.hosts = toolIds;
1119
+ const contract = evaluateHostContract(tableVersionOf(data));
1120
+ if (contract.status === 'degraded') {
1121
+ emitU01Degraded(json, 'host update', baseReport, contract);
1122
+ }
1123
+ const { items, s2 } = planApply({
1124
+ target,
1125
+ rows,
1126
+ toolIds,
1127
+ profile,
1128
+ pkgRoot: packageRoot(),
1129
+ });
1130
+ remapUpdateConflicts(items, force);
1131
+ if (s2.length > 0) {
1132
+ const uniq = uniqueKeepOrder(s2);
1133
+ emitHostFail(json, 'host update', {
1134
+ ...baseReport,
1135
+ errors: uniq.map((p) => ({
1136
+ path: p,
1137
+ code: 's2',
1138
+ message: `路径命中 S2 过程域(拒): ${p}`,
1139
+ })),
1140
+ }, uniq.map((p) => ` - [s2] ${p}`));
1141
+ }
1142
+ const planned = items.filter((i) => i.op === 'write' || i.op === 'merge').map((i) => i.destRel);
1143
+ const skipped = items.filter((i) => i.op === 'skip_identical').map((i) => i.destRel);
1144
+ const conflict = items.filter((i) => i.op === 'conflict').map((i) => i.destRel);
1145
+ const legacyRemove = toolIds.includes('claude') ? findLegacyClaudeFlatCommands(target) : [];
1146
+ let written = [];
1147
+ let removed = [];
1148
+ let backup = null;
1149
+ if (yes) {
1150
+ const committed = commitPlannedWrites(target, items, 'host-update', legacyRemove);
1151
+ written = committed.written;
1152
+ removed = committed.removed;
1153
+ backup = committed.backup;
1154
+ writeHostToolsSticky(target, toolIds, profile);
1155
+ }
1156
+ const report = {
1157
+ command: 'host update',
1158
+ mode,
1159
+ hosts: toolIds,
1160
+ planned,
1161
+ written: yes ? written : [],
1162
+ skipped,
1163
+ conflict,
1164
+ removed: yes ? removed : legacyRemove,
1165
+ backup,
1166
+ contract,
1167
+ ok: true,
1168
+ verdict: 'PASS',
1169
+ };
1170
+ if (json)
1171
+ console.log(JSON.stringify(report, null, 2));
1172
+ else
1173
+ printHostHuman(report);
1174
+ }
1175
+ export async function cmdHost(args) {
1176
+ const [sub, ...rest] = args;
1177
+ if (sub === '--help' || sub === '-h') {
1178
+ console.log(`用法: npx spec-wave ${HOST_USAGE}`);
1179
+ return;
1180
+ }
1181
+ if (!sub)
1182
+ fail(`host 子命令未知: (空)\n用法: ${HOST_USAGE}`);
1183
+ if (sub === 'validate') {
1184
+ await cmdHostValidate(rest);
1185
+ return;
1186
+ }
1187
+ if (sub === 'apply') {
1188
+ await cmdHostApply(rest);
1189
+ return;
1190
+ }
1191
+ if (sub === 'update') {
1192
+ await cmdHostUpdate(rest);
1193
+ return;
1194
+ }
1195
+ fail(`host 子命令未知: ${sub}\n用法: ${HOST_USAGE}`);
1196
+ }
1197
+ //# sourceMappingURL=cli-host.js.map