mova-claude-import 0.1.1 → 0.1.2

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 (188) hide show
  1. package/README.md +68 -4
  2. package/control_surface_exclusions_v0.json +8 -0
  3. package/dist/anthropic_profile_v0.d.ts +1 -1
  4. package/dist/anthropic_profile_v0.js +1 -0
  5. package/dist/cli.js +206 -23
  6. package/dist/control_apply_v0.d.ts +5 -1
  7. package/dist/control_apply_v0.js +125 -8
  8. package/dist/control_check_v0.d.ts +1 -0
  9. package/dist/control_check_v0.js +194 -23
  10. package/dist/control_prefill_v0.js +128 -9
  11. package/dist/control_surface_coverage_v0.d.ts +22 -0
  12. package/dist/control_surface_coverage_v0.js +128 -0
  13. package/dist/control_v0.d.ts +149 -0
  14. package/dist/control_v0.js +360 -0
  15. package/dist/control_v0_schema.d.ts +6 -0
  16. package/dist/control_v0_schema.js +19 -0
  17. package/dist/init_v0.d.ts +6 -1
  18. package/dist/init_v0.js +41 -1
  19. package/dist/observability_writer_v0.d.ts +1 -0
  20. package/dist/observability_writer_v0.js +157 -0
  21. package/dist/observe_v0.d.ts +8 -0
  22. package/dist/observe_v0.js +57 -0
  23. package/dist/presets_v0.d.ts +11 -0
  24. package/dist/presets_v0.js +49 -0
  25. package/dist/redaction.js +5 -1
  26. package/dist/run_import.js +111 -26
  27. package/docs/CLAUDE_CONTROL_SURFACE_MAP_v0.md +78 -0
  28. package/docs/OPERATOR_GUIDE_v0.md +11 -0
  29. package/fixtures/pos/basic/mova/control_v0.json +93 -0
  30. package/fixtures/pos/claude_code_demo_full/.claude/agents/code-reviewer.md +13 -0
  31. package/fixtures/pos/claude_code_demo_full/.claude/agents/github-workflow.md +10 -0
  32. package/fixtures/pos/claude_code_demo_full/.claude/commands/code-quality.md +8 -0
  33. package/fixtures/pos/claude_code_demo_full/.claude/commands/docs-sync.md +8 -0
  34. package/fixtures/pos/claude_code_demo_full/.claude/commands/onboard.md +8 -0
  35. package/fixtures/pos/claude_code_demo_full/.claude/commands/pr-review.md +10 -0
  36. package/fixtures/pos/claude_code_demo_full/.claude/commands/pr-summary.md +8 -0
  37. package/fixtures/pos/claude_code_demo_full/.claude/commands/ticket.md +10 -0
  38. package/fixtures/pos/claude_code_demo_full/.claude/hooks/skill-eval.js +13 -0
  39. package/fixtures/pos/claude_code_demo_full/.claude/hooks/skill-eval.sh +15 -0
  40. package/fixtures/pos/claude_code_demo_full/.claude/hooks/skill-rules.json +21 -0
  41. package/fixtures/pos/claude_code_demo_full/.claude/hooks/skill-rules.schema.json +24 -0
  42. package/fixtures/pos/claude_code_demo_full/.claude/rules/code-style.md +5 -0
  43. package/fixtures/pos/claude_code_demo_full/.claude/rules/security.md +5 -0
  44. package/fixtures/pos/claude_code_demo_full/.claude/settings.json +102 -0
  45. package/fixtures/pos/claude_code_demo_full/.claude/settings.md +6 -0
  46. package/fixtures/pos/claude_code_demo_full/.claude/skills/core-components/SKILL.md +9 -0
  47. package/fixtures/pos/claude_code_demo_full/.claude/skills/formik-patterns/SKILL.md +9 -0
  48. package/fixtures/pos/claude_code_demo_full/.claude/skills/graphql-schema/SKILL.md +10 -0
  49. package/fixtures/pos/claude_code_demo_full/.claude/skills/react-ui-patterns/SKILL.md +10 -0
  50. package/fixtures/pos/claude_code_demo_full/.claude/skills/systematic-debugging/SKILL.md +9 -0
  51. package/fixtures/pos/claude_code_demo_full/.claude/skills/testing-patterns/SKILL.md +10 -0
  52. package/fixtures/pos/claude_code_demo_full/.github/workflows/pr-claude-code-review.yml +14 -0
  53. package/fixtures/pos/claude_code_demo_full/.github/workflows/scheduled-claude-code-dependency-audit.yml +14 -0
  54. package/fixtures/pos/claude_code_demo_full/.github/workflows/scheduled-claude-code-docs-sync.yml +14 -0
  55. package/fixtures/pos/claude_code_demo_full/.github/workflows/scheduled-claude-code-quality.yml +14 -0
  56. package/fixtures/pos/claude_code_demo_full/.mcp.json +44 -0
  57. package/fixtures/pos/claude_code_demo_full/CLAUDE.md +28 -0
  58. package/fixtures/pos/control_basic_project/mova/control_v0.json +93 -0
  59. package/fixtures/pos/observability_basic/CLAUDE.md +3 -0
  60. package/{.tmp_test_zip/out1 → fixtures/pos/preset_safe_observable_v0}/.mcp.json +3 -3
  61. package/fixtures/pos/preset_safe_observable_v0/CLAUDE.md +3 -0
  62. package/package.json +2 -1
  63. package/presets/safe_observable_v0/assets/.claude/agents/code-reviewer.md +9 -0
  64. package/presets/safe_observable_v0/assets/.claude/commands/finish.md +9 -0
  65. package/presets/safe_observable_v0/assets/.claude/commands/start.md +9 -0
  66. package/presets/safe_observable_v0/assets/.claude/hooks/skill-eval.js +15 -0
  67. package/presets/safe_observable_v0/assets/.claude/hooks/skill-eval.sh +17 -0
  68. package/presets/safe_observable_v0/assets/.claude/hooks/skill-rules.json +26 -0
  69. package/presets/safe_observable_v0/assets/.claude/rules/code-style.md +6 -0
  70. package/presets/safe_observable_v0/assets/.claude/rules/security.md +6 -0
  71. package/presets/safe_observable_v0/assets/.claude/skills/git-workflow/SKILL.md +9 -0
  72. package/presets/safe_observable_v0/assets/.claude/skills/security-basics/SKILL.md +9 -0
  73. package/presets/safe_observable_v0/assets/.claude/skills/systematic-debugging/SKILL.md +9 -0
  74. package/presets/safe_observable_v0/assets/.claude/skills/testing-patterns/SKILL.md +9 -0
  75. package/presets/safe_observable_v0/control_v0.json +214 -0
  76. package/schemas/mova.control_v0.schema.json +252 -0
  77. package/src/anthropic_profile_v0.ts +1 -0
  78. package/src/cli.ts +194 -23
  79. package/src/control_apply_v0.ts +131 -8
  80. package/src/control_check_v0.ts +203 -23
  81. package/src/control_prefill_v0.ts +136 -8
  82. package/src/control_surface_coverage_v0.ts +164 -0
  83. package/src/control_v0.ts +808 -0
  84. package/src/control_v0_schema.ts +26 -0
  85. package/src/init_v0.ts +48 -1
  86. package/src/observability_writer_v0.ts +157 -0
  87. package/src/observe_v0.ts +64 -0
  88. package/src/presets_v0.ts +55 -0
  89. package/src/redaction.ts +6 -1
  90. package/src/run_import.ts +132 -26
  91. package/test/control_demo_full_roundtrip.test.js +92 -0
  92. package/test/control_surface_coverage_v0.test.js +36 -0
  93. package/test/control_v0_schema_validation.test.js +69 -0
  94. package/test/init_v0.test.js +9 -0
  95. package/test/observability_writer_v0.test.js +59 -0
  96. package/test/preset_safe_observable_v0.test.js +55 -0
  97. package/test/profile_v0_output.test.js +1 -0
  98. package/test/scaffold_v0_output.test.js +1 -0
  99. package/tools/control_surface_coverage_v0.mjs +27 -0
  100. package/tools/smoke_v0.mjs +33 -0
  101. package/.tmp_test_control_apply/proj/.claude/agents/example_agent.md +0 -3
  102. package/.tmp_test_control_apply/proj/.claude/commands/example_command.md +0 -3
  103. package/.tmp_test_control_apply/proj/.claude/hooks/example_hook.sh +0 -2
  104. package/.tmp_test_control_apply/proj/.claude/output-styles/example_style.md +0 -3
  105. package/.tmp_test_control_apply/proj/.claude/settings.json +0 -30
  106. package/.tmp_test_control_apply/proj/.claude/settings.local.example.json +0 -3
  107. package/.tmp_test_control_apply/proj/.mcp.json +0 -3
  108. package/.tmp_test_control_apply/proj/CLAUDE.md +0 -13
  109. package/.tmp_test_control_apply/proj/MOVA.md +0 -3
  110. package/.tmp_test_control_check/proj/.mcp.json +0 -1
  111. package/.tmp_test_control_check/proj/CLAUDE.md +0 -1
  112. package/.tmp_test_control_prefill/out1/claude_control_profile_v0.json +0 -114
  113. package/.tmp_test_control_prefill/out1/prefill_report_v0.json +0 -13
  114. package/.tmp_test_control_prefill/out2/claude_control_profile_v0.json +0 -114
  115. package/.tmp_test_control_prefill/out2/prefill_report_v0.json +0 -13
  116. package/.tmp_test_overlay/proj/.claude/skills/a.md +0 -1
  117. package/.tmp_test_overlay/proj/.mcp.json +0 -1
  118. package/.tmp_test_overlay/proj/CLAUDE.md +0 -1
  119. package/.tmp_test_profile/proj/.claude/skills/a.md +0 -1
  120. package/.tmp_test_profile/proj/.mcp.json +0 -1
  121. package/.tmp_test_profile/proj/CLAUDE.md +0 -1
  122. package/.tmp_test_scaffold_apply/proj/.claude/agents/example_agent.md +0 -3
  123. package/.tmp_test_scaffold_apply/proj/.claude/commands/example_command.md +0 -3
  124. package/.tmp_test_scaffold_apply/proj/.claude/hooks/example_hook.sh +0 -2
  125. package/.tmp_test_scaffold_apply/proj/.claude/output-styles/example_style.md +0 -3
  126. package/.tmp_test_scaffold_apply/proj/.claude/settings.json +0 -30
  127. package/.tmp_test_scaffold_apply/proj/.claude/settings.local.example.json +0 -3
  128. package/.tmp_test_scaffold_apply/proj/.mcp.json +0 -3
  129. package/.tmp_test_scaffold_apply/proj/CLAUDE.md +0 -13
  130. package/.tmp_test_scaffold_apply/proj/MOVA.md +0 -3
  131. package/.tmp_test_strict/mova/claude_import/v0/VERSION.json +0 -10
  132. package/.tmp_test_strict/mova/claude_import/v0/episode_import_run.json +0 -20
  133. package/.tmp_test_strict/mova/claude_import/v0/import_manifest.json +0 -20
  134. package/.tmp_test_strict/mova/claude_import/v0/input_policy_report_v0.json +0 -32
  135. package/.tmp_test_zip/out1/.claude/agents/example_agent.md +0 -3
  136. package/.tmp_test_zip/out1/.claude/commands/example_command.md +0 -3
  137. package/.tmp_test_zip/out1/.claude/commands/mova_context.md +0 -4
  138. package/.tmp_test_zip/out1/.claude/commands/mova_lint.md +0 -4
  139. package/.tmp_test_zip/out1/.claude/commands/mova_proof.md +0 -6
  140. package/.tmp_test_zip/out1/.claude/hooks/example_hook.sh +0 -2
  141. package/.tmp_test_zip/out1/.claude/output-styles/example_style.md +0 -3
  142. package/.tmp_test_zip/out1/.claude/settings.json +0 -30
  143. package/.tmp_test_zip/out1/.claude/settings.local.example.json +0 -3
  144. package/.tmp_test_zip/out1/.claude/skills/a/SKILL.md +0 -1
  145. package/.tmp_test_zip/out1/.claude/skills/mova-control-v0/SKILL.md +0 -11
  146. package/.tmp_test_zip/out1/.claude/skills/mova-layer-v0/SKILL.md +0 -8
  147. package/.tmp_test_zip/out1/CLAUDE.md +0 -4
  148. package/.tmp_test_zip/out1/MOVA.md +0 -10
  149. package/.tmp_test_zip/out1/export.zip +0 -0
  150. package/.tmp_test_zip/out1/mova/claude_import/v0/VERSION.json +0 -10
  151. package/.tmp_test_zip/out1/mova/claude_import/v0/contracts/instruction_profile_v0.json +0 -8
  152. package/.tmp_test_zip/out1/mova/claude_import/v0/contracts/mcp_servers_v0.json +0 -4
  153. package/.tmp_test_zip/out1/mova/claude_import/v0/contracts/skills_catalog_v0.json +0 -11
  154. package/.tmp_test_zip/out1/mova/claude_import/v0/episode_import_run.json +0 -80
  155. package/.tmp_test_zip/out1/mova/claude_import/v0/export_manifest_v0.json +0 -32
  156. package/.tmp_test_zip/out1/mova/claude_import/v0/import_manifest.json +0 -33
  157. package/.tmp_test_zip/out1/mova/claude_import/v0/input_policy_report_v0.json +0 -38
  158. package/.tmp_test_zip/out1/mova/claude_import/v0/lint_report_v0.json +0 -6
  159. package/.tmp_test_zip/out1/mova/claude_import/v0/redaction_report.json +0 -4
  160. package/.tmp_test_zip/out2/.claude/agents/example_agent.md +0 -3
  161. package/.tmp_test_zip/out2/.claude/commands/example_command.md +0 -3
  162. package/.tmp_test_zip/out2/.claude/commands/mova_context.md +0 -4
  163. package/.tmp_test_zip/out2/.claude/commands/mova_lint.md +0 -4
  164. package/.tmp_test_zip/out2/.claude/commands/mova_proof.md +0 -6
  165. package/.tmp_test_zip/out2/.claude/hooks/example_hook.sh +0 -2
  166. package/.tmp_test_zip/out2/.claude/output-styles/example_style.md +0 -3
  167. package/.tmp_test_zip/out2/.claude/settings.json +0 -30
  168. package/.tmp_test_zip/out2/.claude/settings.local.example.json +0 -3
  169. package/.tmp_test_zip/out2/.claude/skills/a/SKILL.md +0 -1
  170. package/.tmp_test_zip/out2/.claude/skills/mova-control-v0/SKILL.md +0 -11
  171. package/.tmp_test_zip/out2/.claude/skills/mova-layer-v0/SKILL.md +0 -8
  172. package/.tmp_test_zip/out2/.mcp.json +0 -3
  173. package/.tmp_test_zip/out2/CLAUDE.md +0 -4
  174. package/.tmp_test_zip/out2/MOVA.md +0 -10
  175. package/.tmp_test_zip/out2/export.zip +0 -0
  176. package/.tmp_test_zip/out2/mova/claude_import/v0/VERSION.json +0 -10
  177. package/.tmp_test_zip/out2/mova/claude_import/v0/contracts/instruction_profile_v0.json +0 -8
  178. package/.tmp_test_zip/out2/mova/claude_import/v0/contracts/mcp_servers_v0.json +0 -4
  179. package/.tmp_test_zip/out2/mova/claude_import/v0/contracts/skills_catalog_v0.json +0 -11
  180. package/.tmp_test_zip/out2/mova/claude_import/v0/episode_import_run.json +0 -80
  181. package/.tmp_test_zip/out2/mova/claude_import/v0/export_manifest_v0.json +0 -32
  182. package/.tmp_test_zip/out2/mova/claude_import/v0/import_manifest.json +0 -33
  183. package/.tmp_test_zip/out2/mova/claude_import/v0/input_policy_report_v0.json +0 -38
  184. package/.tmp_test_zip/out2/mova/claude_import/v0/lint_report_v0.json +0 -6
  185. package/.tmp_test_zip/out2/mova/claude_import/v0/redaction_report.json +0 -4
  186. package/.tmp_test_zip/proj/.claude/skills/a.md +0 -1
  187. package/.tmp_test_zip/proj/.mcp.json +0 -1
  188. package/.tmp_test_zip/proj/CLAUDE.md +0 -1
@@ -0,0 +1,808 @@
1
+ import { MOVA_CONTROL_ENTRY_MARKER } from "./mova_overlay_v0.js";
2
+
3
+ export const CONTROL_V0_SCHEMA_ID = "https://mova.dev/schemas/mova.control_v0.schema.json";
4
+
5
+ export type AssetMode = "copy_through" | "managed";
6
+
7
+ export type AssetItem = {
8
+ path: string;
9
+ mode: AssetMode;
10
+ source_path?: string;
11
+ };
12
+
13
+ export type ControlV0 = {
14
+ $schema?: string;
15
+ version: "control_v0";
16
+ claude_md: {
17
+ inject_control_entry: boolean;
18
+ marker: string;
19
+ priority_sources: string[];
20
+ };
21
+ claude_memory: {
22
+ enable: boolean;
23
+ sources: string[];
24
+ };
25
+ overlay: {
26
+ enable: boolean;
27
+ };
28
+ settings: {
29
+ include_co_authored_by: boolean;
30
+ env: Record<string, string>;
31
+ };
32
+ mcp: {
33
+ servers: any;
34
+ enable_all_project_mcp_servers: boolean;
35
+ enabled_mcpjson_servers: string[];
36
+ env_substitutions: {
37
+ format: string;
38
+ default_format: string;
39
+ };
40
+ };
41
+ policy: {
42
+ mode: string;
43
+ hooks: {
44
+ enable: boolean;
45
+ on_invalid_hook: string;
46
+ definitions: any[];
47
+ events: Record<string, any>;
48
+ };
49
+ permissions: {
50
+ allow: any[];
51
+ deny: any[];
52
+ on_conflict: string;
53
+ on_unknown: string;
54
+ };
55
+ plugins: {
56
+ enable: boolean;
57
+ allowed_plugin_ids: any[];
58
+ denied_plugin_ids: any[];
59
+ on_unknown: string;
60
+ enabled_plugins: Record<string, boolean>;
61
+ };
62
+ };
63
+ lsp: {
64
+ enabled_plugins: string[];
65
+ config_path: string;
66
+ managed: boolean;
67
+ };
68
+ skill_eval: {
69
+ enable: boolean;
70
+ hooks: {
71
+ shell: string;
72
+ node: string;
73
+ };
74
+ rules_path: string;
75
+ scoring: {
76
+ threshold: number;
77
+ };
78
+ };
79
+ observability: {
80
+ enable: boolean;
81
+ mode: string;
82
+ writer: {
83
+ type: "node";
84
+ script_path: string;
85
+ };
86
+ output_dir: string;
87
+ stdout_tail_bytes: number;
88
+ stderr_tail_bytes: number;
89
+ max_event_bytes: number;
90
+ tail_lines: number;
91
+ include_tools: string[];
92
+ include_events: string[];
93
+ };
94
+ assets: {
95
+ skills: AssetItem[];
96
+ agents: AssetItem[];
97
+ commands: AssetItem[];
98
+ rules: AssetItem[];
99
+ hooks: AssetItem[];
100
+ workflows: AssetItem[];
101
+ docs: AssetItem[];
102
+ dotfiles: AssetItem[];
103
+ schemas: AssetItem[];
104
+ };
105
+ };
106
+
107
+ function isObject(value: any): value is Record<string, any> {
108
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
109
+ }
110
+
111
+ export function defaultControlV0(): ControlV0 {
112
+ return {
113
+ $schema: CONTROL_V0_SCHEMA_ID,
114
+ version: "control_v0",
115
+ claude_md: {
116
+ inject_control_entry: true,
117
+ marker: MOVA_CONTROL_ENTRY_MARKER,
118
+ priority_sources: ["CLAUDE.md", ".claude/CLAUDE.md", "~/.claude/CLAUDE.md"],
119
+ },
120
+ claude_memory: {
121
+ enable: true,
122
+ sources: ["CLAUDE.md"],
123
+ },
124
+ overlay: {
125
+ enable: true,
126
+ },
127
+ settings: {
128
+ include_co_authored_by: true,
129
+ env: {},
130
+ },
131
+ mcp: {
132
+ servers: {},
133
+ enable_all_project_mcp_servers: false,
134
+ enabled_mcpjson_servers: [],
135
+ env_substitutions: {
136
+ format: "${VAR}",
137
+ default_format: "${VAR:-default}",
138
+ },
139
+ },
140
+ policy: {
141
+ mode: "report_only",
142
+ hooks: {
143
+ enable: true,
144
+ on_invalid_hook: "report_only",
145
+ definitions: [],
146
+ events: {},
147
+ },
148
+ permissions: {
149
+ allow: [],
150
+ deny: [],
151
+ on_conflict: "deny_wins",
152
+ on_unknown: "report_only",
153
+ },
154
+ plugins: {
155
+ enable: true,
156
+ allowed_plugin_ids: [],
157
+ denied_plugin_ids: [],
158
+ on_unknown: "report_only",
159
+ enabled_plugins: {},
160
+ },
161
+ },
162
+ lsp: {
163
+ enabled_plugins: [],
164
+ config_path: ".claude/lsp.json",
165
+ managed: false,
166
+ },
167
+ skill_eval: {
168
+ enable: false,
169
+ hooks: {
170
+ shell: ".claude/hooks/skill-eval.sh",
171
+ node: ".claude/hooks/skill-eval.js",
172
+ },
173
+ rules_path: ".claude/hooks/skill-rules.json",
174
+ scoring: {
175
+ threshold: 0.6,
176
+ },
177
+ },
178
+ observability: {
179
+ enable: true,
180
+ mode: "report_only",
181
+ writer: {
182
+ type: "node",
183
+ script_path: ".claude/hooks/mova-observe.js",
184
+ },
185
+ output_dir: ".mova/episodes",
186
+ stdout_tail_bytes: 4000,
187
+ stderr_tail_bytes: 4000,
188
+ max_event_bytes: 20000,
189
+ tail_lines: 50,
190
+ include_tools: ["Bash", "Read", "Edit", "MultiEdit", "Write"],
191
+ include_events: ["PostToolUse", "UserPromptSubmit", "Stop"],
192
+ },
193
+ assets: {
194
+ skills: [],
195
+ agents: [],
196
+ commands: [],
197
+ rules: [],
198
+ hooks: [],
199
+ workflows: [],
200
+ docs: [],
201
+ dotfiles: [],
202
+ schemas: [],
203
+ },
204
+ };
205
+ }
206
+
207
+ function coerceBoolean(value: any, fallback: boolean, defaults: string[], path: string): boolean {
208
+ if (typeof value === "boolean") return value;
209
+ defaults.push(path);
210
+ return fallback;
211
+ }
212
+
213
+ function coerceString(value: any, fallback: string, defaults: string[], path: string): string {
214
+ if (typeof value === "string") return value;
215
+ defaults.push(path);
216
+ return fallback;
217
+ }
218
+
219
+ function coerceNumber(value: any, fallback: number, defaults: string[], path: string): number {
220
+ if (typeof value === "number" && Number.isFinite(value)) return value;
221
+ defaults.push(path);
222
+ return fallback;
223
+ }
224
+
225
+ function coerceArray(value: any, fallback: any[], defaults: string[], path: string): any[] {
226
+ if (Array.isArray(value)) return value;
227
+ defaults.push(path);
228
+ return fallback;
229
+ }
230
+
231
+ function coerceRecord(value: any, fallback: Record<string, any>, defaults: string[], path: string): Record<string, any> {
232
+ if (isObject(value)) return value;
233
+ defaults.push(path);
234
+ return fallback;
235
+ }
236
+
237
+ function coerceServers(value: any, fallback: any, defaults: string[], path: string): any {
238
+ if (Array.isArray(value)) return value;
239
+ if (isObject(value)) return value;
240
+ defaults.push(path);
241
+ return fallback;
242
+ }
243
+
244
+ function normalizeAssets(
245
+ value: any,
246
+ fallback: AssetItem[],
247
+ defaults: string[],
248
+ path: string
249
+ ): AssetItem[] {
250
+ if (!Array.isArray(value)) {
251
+ defaults.push(path);
252
+ return fallback;
253
+ }
254
+ const normalized: AssetItem[] = [];
255
+ for (const item of value) {
256
+ if (!isObject(item) || typeof item.path !== "string") continue;
257
+ const mode = item.mode === "managed" ? "managed" : "copy_through";
258
+ const entry: AssetItem = { path: item.path, mode };
259
+ if (typeof item.source_path === "string") entry.source_path = item.source_path;
260
+ normalized.push(entry);
261
+ }
262
+ return normalized.sort((a, b) => a.path.localeCompare(b.path));
263
+ }
264
+
265
+ export function normalizeControlV0(input: any): { control: ControlV0; defaults: string[] } {
266
+ const defaults: string[] = [];
267
+ const base = defaultControlV0();
268
+ const src = isObject(input) ? input : {};
269
+
270
+ base.$schema = CONTROL_V0_SCHEMA_ID;
271
+ base.version = "control_v0";
272
+
273
+ base.claude_md.inject_control_entry = coerceBoolean(
274
+ src?.claude_md?.inject_control_entry,
275
+ base.claude_md.inject_control_entry,
276
+ defaults,
277
+ "claude_md.inject_control_entry"
278
+ );
279
+ base.claude_md.marker = coerceString(
280
+ src?.claude_md?.marker,
281
+ base.claude_md.marker,
282
+ defaults,
283
+ "claude_md.marker"
284
+ );
285
+ base.claude_md.priority_sources = coerceArray(
286
+ src?.claude_md?.priority_sources,
287
+ base.claude_md.priority_sources,
288
+ defaults,
289
+ "claude_md.priority_sources"
290
+ );
291
+
292
+ base.claude_memory.enable = coerceBoolean(
293
+ src?.claude_memory?.enable,
294
+ base.claude_memory.enable,
295
+ defaults,
296
+ "claude_memory.enable"
297
+ );
298
+ base.claude_memory.sources = coerceArray(
299
+ src?.claude_memory?.sources,
300
+ base.claude_memory.sources,
301
+ defaults,
302
+ "claude_memory.sources"
303
+ );
304
+
305
+ base.overlay.enable = coerceBoolean(src?.overlay?.enable, base.overlay.enable, defaults, "overlay.enable");
306
+
307
+ base.settings.include_co_authored_by = coerceBoolean(
308
+ src?.settings?.include_co_authored_by,
309
+ base.settings.include_co_authored_by,
310
+ defaults,
311
+ "settings.include_co_authored_by"
312
+ );
313
+ base.settings.env = coerceRecord(src?.settings?.env, base.settings.env, defaults, "settings.env");
314
+
315
+ base.mcp.servers = coerceServers(src?.mcp?.servers, base.mcp.servers, defaults, "mcp.servers");
316
+ base.mcp.enable_all_project_mcp_servers = coerceBoolean(
317
+ src?.mcp?.enable_all_project_mcp_servers,
318
+ base.mcp.enable_all_project_mcp_servers,
319
+ defaults,
320
+ "mcp.enable_all_project_mcp_servers"
321
+ );
322
+ base.mcp.enabled_mcpjson_servers = coerceArray(
323
+ src?.mcp?.enabled_mcpjson_servers,
324
+ base.mcp.enabled_mcpjson_servers,
325
+ defaults,
326
+ "mcp.enabled_mcpjson_servers"
327
+ );
328
+ base.mcp.env_substitutions = coerceRecord(
329
+ src?.mcp?.env_substitutions,
330
+ base.mcp.env_substitutions,
331
+ defaults,
332
+ "mcp.env_substitutions"
333
+ ) as ControlV0["mcp"]["env_substitutions"];
334
+ base.mcp.env_substitutions.format = coerceString(
335
+ base.mcp.env_substitutions.format,
336
+ "${VAR}",
337
+ defaults,
338
+ "mcp.env_substitutions.format"
339
+ );
340
+ base.mcp.env_substitutions.default_format = coerceString(
341
+ base.mcp.env_substitutions.default_format,
342
+ "${VAR:-default}",
343
+ defaults,
344
+ "mcp.env_substitutions.default_format"
345
+ );
346
+
347
+ base.policy.mode = coerceString(src?.policy?.mode, base.policy.mode, defaults, "policy.mode");
348
+ base.policy.hooks.enable = coerceBoolean(
349
+ src?.policy?.hooks?.enable,
350
+ base.policy.hooks.enable,
351
+ defaults,
352
+ "policy.hooks.enable"
353
+ );
354
+ base.policy.hooks.on_invalid_hook = coerceString(
355
+ src?.policy?.hooks?.on_invalid_hook,
356
+ base.policy.hooks.on_invalid_hook,
357
+ defaults,
358
+ "policy.hooks.on_invalid_hook"
359
+ );
360
+ base.policy.hooks.definitions = coerceArray(
361
+ src?.policy?.hooks?.definitions,
362
+ base.policy.hooks.definitions,
363
+ defaults,
364
+ "policy.hooks.definitions"
365
+ );
366
+ base.policy.hooks.events = coerceRecord(
367
+ src?.policy?.hooks?.events,
368
+ base.policy.hooks.events,
369
+ defaults,
370
+ "policy.hooks.events"
371
+ );
372
+
373
+ base.policy.permissions.allow = coerceArray(
374
+ src?.policy?.permissions?.allow,
375
+ base.policy.permissions.allow,
376
+ defaults,
377
+ "policy.permissions.allow"
378
+ );
379
+ base.policy.permissions.deny = coerceArray(
380
+ src?.policy?.permissions?.deny,
381
+ base.policy.permissions.deny,
382
+ defaults,
383
+ "policy.permissions.deny"
384
+ );
385
+ base.policy.permissions.on_conflict = coerceString(
386
+ src?.policy?.permissions?.on_conflict,
387
+ base.policy.permissions.on_conflict,
388
+ defaults,
389
+ "policy.permissions.on_conflict"
390
+ );
391
+ base.policy.permissions.on_unknown = coerceString(
392
+ src?.policy?.permissions?.on_unknown,
393
+ base.policy.permissions.on_unknown,
394
+ defaults,
395
+ "policy.permissions.on_unknown"
396
+ );
397
+
398
+ base.policy.plugins.enable = coerceBoolean(
399
+ src?.policy?.plugins?.enable,
400
+ base.policy.plugins.enable,
401
+ defaults,
402
+ "policy.plugins.enable"
403
+ );
404
+ base.policy.plugins.allowed_plugin_ids = coerceArray(
405
+ src?.policy?.plugins?.allowed_plugin_ids,
406
+ base.policy.plugins.allowed_plugin_ids,
407
+ defaults,
408
+ "policy.plugins.allowed_plugin_ids"
409
+ );
410
+ base.policy.plugins.denied_plugin_ids = coerceArray(
411
+ src?.policy?.plugins?.denied_plugin_ids,
412
+ base.policy.plugins.denied_plugin_ids,
413
+ defaults,
414
+ "policy.plugins.denied_plugin_ids"
415
+ );
416
+ base.policy.plugins.on_unknown = coerceString(
417
+ src?.policy?.plugins?.on_unknown,
418
+ base.policy.plugins.on_unknown,
419
+ defaults,
420
+ "policy.plugins.on_unknown"
421
+ );
422
+ base.policy.plugins.enabled_plugins = coerceRecord(
423
+ src?.policy?.plugins?.enabled_plugins,
424
+ base.policy.plugins.enabled_plugins,
425
+ defaults,
426
+ "policy.plugins.enabled_plugins"
427
+ );
428
+
429
+ base.lsp.enabled_plugins = coerceArray(
430
+ src?.lsp?.enabled_plugins,
431
+ base.lsp.enabled_plugins,
432
+ defaults,
433
+ "lsp.enabled_plugins"
434
+ );
435
+ base.lsp.config_path = coerceString(
436
+ src?.lsp?.config_path,
437
+ base.lsp.config_path,
438
+ defaults,
439
+ "lsp.config_path"
440
+ );
441
+ base.lsp.managed = coerceBoolean(src?.lsp?.managed, base.lsp.managed, defaults, "lsp.managed");
442
+
443
+ base.skill_eval.enable = coerceBoolean(
444
+ src?.skill_eval?.enable,
445
+ base.skill_eval.enable,
446
+ defaults,
447
+ "skill_eval.enable"
448
+ );
449
+ base.skill_eval.hooks = coerceRecord(
450
+ src?.skill_eval?.hooks,
451
+ base.skill_eval.hooks,
452
+ defaults,
453
+ "skill_eval.hooks"
454
+ ) as ControlV0["skill_eval"]["hooks"];
455
+ base.skill_eval.hooks.shell = coerceString(
456
+ base.skill_eval.hooks.shell,
457
+ defaultControlV0().skill_eval.hooks.shell,
458
+ defaults,
459
+ "skill_eval.hooks.shell"
460
+ );
461
+ base.skill_eval.hooks.node = coerceString(
462
+ base.skill_eval.hooks.node,
463
+ defaultControlV0().skill_eval.hooks.node,
464
+ defaults,
465
+ "skill_eval.hooks.node"
466
+ );
467
+ base.skill_eval.rules_path = coerceString(
468
+ src?.skill_eval?.rules_path,
469
+ base.skill_eval.rules_path,
470
+ defaults,
471
+ "skill_eval.rules_path"
472
+ );
473
+ base.skill_eval.scoring = coerceRecord(
474
+ src?.skill_eval?.scoring,
475
+ base.skill_eval.scoring,
476
+ defaults,
477
+ "skill_eval.scoring"
478
+ ) as ControlV0["skill_eval"]["scoring"];
479
+ base.skill_eval.scoring.threshold = coerceNumber(
480
+ base.skill_eval.scoring.threshold,
481
+ defaultControlV0().skill_eval.scoring.threshold,
482
+ defaults,
483
+ "skill_eval.scoring.threshold"
484
+ );
485
+
486
+ base.observability.enable = coerceBoolean(
487
+ src?.observability?.enable,
488
+ base.observability.enable,
489
+ defaults,
490
+ "observability.enable"
491
+ );
492
+ base.observability.mode = coerceString(
493
+ src?.observability?.mode,
494
+ base.observability.mode,
495
+ defaults,
496
+ "observability.mode"
497
+ );
498
+ base.observability.writer = coerceRecord(
499
+ src?.observability?.writer,
500
+ base.observability.writer,
501
+ defaults,
502
+ "observability.writer"
503
+ ) as ControlV0["observability"]["writer"];
504
+ base.observability.writer.type = "node";
505
+ base.observability.writer.script_path = coerceString(
506
+ base.observability.writer.script_path,
507
+ defaultControlV0().observability.writer.script_path,
508
+ defaults,
509
+ "observability.writer.script_path"
510
+ );
511
+ base.observability.output_dir = coerceString(
512
+ src?.observability?.output_dir,
513
+ base.observability.output_dir,
514
+ defaults,
515
+ "observability.output_dir"
516
+ );
517
+ base.observability.stdout_tail_bytes = coerceNumber(
518
+ src?.observability?.stdout_tail_bytes,
519
+ base.observability.stdout_tail_bytes,
520
+ defaults,
521
+ "observability.stdout_tail_bytes"
522
+ );
523
+ base.observability.stderr_tail_bytes = coerceNumber(
524
+ src?.observability?.stderr_tail_bytes,
525
+ base.observability.stderr_tail_bytes,
526
+ defaults,
527
+ "observability.stderr_tail_bytes"
528
+ );
529
+ base.observability.max_event_bytes = coerceNumber(
530
+ src?.observability?.max_event_bytes,
531
+ base.observability.max_event_bytes,
532
+ defaults,
533
+ "observability.max_event_bytes"
534
+ );
535
+ base.observability.tail_lines = coerceNumber(
536
+ src?.observability?.tail_lines,
537
+ base.observability.tail_lines,
538
+ defaults,
539
+ "observability.tail_lines"
540
+ );
541
+ base.observability.include_tools = coerceArray(
542
+ src?.observability?.include_tools,
543
+ base.observability.include_tools,
544
+ defaults,
545
+ "observability.include_tools"
546
+ );
547
+ base.observability.include_events = coerceArray(
548
+ src?.observability?.include_events,
549
+ base.observability.include_events,
550
+ defaults,
551
+ "observability.include_events"
552
+ );
553
+
554
+ base.assets.skills = normalizeAssets(src?.assets?.skills, base.assets.skills, defaults, "assets.skills");
555
+ base.assets.agents = normalizeAssets(src?.assets?.agents, base.assets.agents, defaults, "assets.agents");
556
+ base.assets.commands = normalizeAssets(src?.assets?.commands, base.assets.commands, defaults, "assets.commands");
557
+ base.assets.rules = normalizeAssets(src?.assets?.rules, base.assets.rules, defaults, "assets.rules");
558
+ base.assets.hooks = normalizeAssets(src?.assets?.hooks, base.assets.hooks, defaults, "assets.hooks");
559
+ base.assets.workflows = normalizeAssets(src?.assets?.workflows, base.assets.workflows, defaults, "assets.workflows");
560
+ base.assets.docs = normalizeAssets(src?.assets?.docs, base.assets.docs, defaults, "assets.docs");
561
+ base.assets.dotfiles = normalizeAssets(src?.assets?.dotfiles, base.assets.dotfiles, defaults, "assets.dotfiles");
562
+ base.assets.schemas = normalizeAssets(src?.assets?.schemas, base.assets.schemas, defaults, "assets.schemas");
563
+
564
+ return { control: base, defaults };
565
+ }
566
+
567
+ export function controlFromSettingsV0(
568
+ settings: any | undefined,
569
+ mcp: any | undefined
570
+ ): { control: ControlV0; defaults: string[] } {
571
+ const defaults: string[] = [];
572
+ const base = defaultControlV0();
573
+
574
+ if (!settings) {
575
+ defaults.push("settings", "policy", "claude_md");
576
+ } else {
577
+ base.settings.include_co_authored_by = coerceBoolean(
578
+ settings?.includeCoAuthoredBy,
579
+ base.settings.include_co_authored_by,
580
+ defaults,
581
+ "settings.include_co_authored_by"
582
+ );
583
+ base.settings.env = coerceRecord(settings?.env, base.settings.env, defaults, "settings.env");
584
+
585
+ base.policy.hooks.enable = coerceBoolean(
586
+ settings?.hooks?.enable ?? true,
587
+ base.policy.hooks.enable,
588
+ defaults,
589
+ "policy.hooks.enable"
590
+ );
591
+ base.policy.hooks.on_invalid_hook = coerceString(
592
+ settings?.hooks?.behavior?.on_invalid_hook,
593
+ base.policy.hooks.on_invalid_hook,
594
+ defaults,
595
+ "policy.hooks.on_invalid_hook"
596
+ );
597
+ base.policy.hooks.definitions = coerceArray(
598
+ settings?.hooks?.definitions,
599
+ base.policy.hooks.definitions,
600
+ defaults,
601
+ "policy.hooks.definitions"
602
+ );
603
+ const events: Record<string, any> = {};
604
+ if (isObject(settings?.hooks)) {
605
+ for (const [key, value] of Object.entries(settings.hooks)) {
606
+ if (key === "enable" || key === "behavior" || key === "definitions") continue;
607
+ events[key] = value;
608
+ }
609
+ }
610
+ base.policy.hooks.events = events;
611
+
612
+ base.policy.permissions.allow = coerceArray(
613
+ settings?.permissions?.allow,
614
+ base.policy.permissions.allow,
615
+ defaults,
616
+ "policy.permissions.allow"
617
+ );
618
+ base.policy.permissions.deny = coerceArray(
619
+ settings?.permissions?.deny,
620
+ base.policy.permissions.deny,
621
+ defaults,
622
+ "policy.permissions.deny"
623
+ );
624
+ base.policy.permissions.on_conflict = coerceString(
625
+ settings?.permissions?.behavior?.on_conflict,
626
+ base.policy.permissions.on_conflict,
627
+ defaults,
628
+ "policy.permissions.on_conflict"
629
+ );
630
+ base.policy.permissions.on_unknown = coerceString(
631
+ settings?.permissions?.behavior?.on_unknown,
632
+ base.policy.permissions.on_unknown,
633
+ defaults,
634
+ "policy.permissions.on_unknown"
635
+ );
636
+
637
+ base.policy.plugins.enable = coerceBoolean(
638
+ settings?.plugins?.enable,
639
+ base.policy.plugins.enable,
640
+ defaults,
641
+ "policy.plugins.enable"
642
+ );
643
+ base.policy.plugins.allowed_plugin_ids = coerceArray(
644
+ settings?.plugins?.allowed_plugin_ids,
645
+ base.policy.plugins.allowed_plugin_ids,
646
+ defaults,
647
+ "policy.plugins.allowed_plugin_ids"
648
+ );
649
+ base.policy.plugins.denied_plugin_ids = coerceArray(
650
+ settings?.plugins?.denied_plugin_ids,
651
+ base.policy.plugins.denied_plugin_ids,
652
+ defaults,
653
+ "policy.plugins.denied_plugin_ids"
654
+ );
655
+ base.policy.plugins.on_unknown = coerceString(
656
+ settings?.plugins?.behavior?.on_unknown,
657
+ base.policy.plugins.on_unknown,
658
+ defaults,
659
+ "policy.plugins.on_unknown"
660
+ );
661
+ base.policy.plugins.enabled_plugins = coerceRecord(
662
+ settings?.plugins?.enabledPlugins,
663
+ base.policy.plugins.enabled_plugins,
664
+ defaults,
665
+ "policy.plugins.enabled_plugins"
666
+ );
667
+
668
+ base.claude_md.inject_control_entry = coerceBoolean(
669
+ settings?.claude_md?.inject_control_entry,
670
+ base.claude_md.inject_control_entry,
671
+ defaults,
672
+ "claude_md.inject_control_entry"
673
+ );
674
+ base.claude_md.marker = coerceString(
675
+ settings?.claude_md?.marker,
676
+ base.claude_md.marker,
677
+ defaults,
678
+ "claude_md.marker"
679
+ );
680
+
681
+ base.mcp.enable_all_project_mcp_servers = coerceBoolean(
682
+ settings?.mcp?.enableAllProjectMcpServers,
683
+ base.mcp.enable_all_project_mcp_servers,
684
+ defaults,
685
+ "mcp.enable_all_project_mcp_servers"
686
+ );
687
+ base.mcp.enabled_mcpjson_servers = coerceArray(
688
+ settings?.mcp?.enabledMcpjsonServers,
689
+ base.mcp.enabled_mcpjson_servers,
690
+ defaults,
691
+ "mcp.enabled_mcpjson_servers"
692
+ );
693
+
694
+ base.lsp.enabled_plugins = coerceArray(
695
+ settings?.lsp?.enabledPlugins,
696
+ base.lsp.enabled_plugins,
697
+ defaults,
698
+ "lsp.enabled_plugins"
699
+ );
700
+ }
701
+
702
+ if (!mcp) {
703
+ defaults.push("mcp.servers");
704
+ } else if (isObject(mcp) && isObject(mcp.mcpServers)) {
705
+ base.mcp.servers = mcp.mcpServers;
706
+ } else {
707
+ base.mcp.servers = coerceServers(mcp?.servers, base.mcp.servers, defaults, "mcp.servers");
708
+ }
709
+
710
+ return { control: base, defaults };
711
+ }
712
+
713
+ export function controlToSettingsV0(control: ControlV0) {
714
+ const hooks = {
715
+ enable: control.policy.hooks.enable,
716
+ definitions: control.policy.hooks.definitions,
717
+ behavior: {
718
+ on_invalid_hook: control.policy.hooks.on_invalid_hook,
719
+ },
720
+ } as Record<string, any>;
721
+
722
+ for (const [event, value] of Object.entries(control.policy.hooks.events)) {
723
+ hooks[event] = value;
724
+ }
725
+
726
+ if (control.observability.enable && Array.isArray(control.observability.include_events)) {
727
+ const scriptRel = control.observability.writer.script_path.replace(/^[\\/]+/, "");
728
+ const baseCommand = `node \"$CLAUDE_PROJECT_DIR/${scriptRel}\"`;
729
+ const commonArgs = [
730
+ `--stdout-tail-bytes ${control.observability.stdout_tail_bytes}`,
731
+ `--stderr-tail-bytes ${control.observability.stderr_tail_bytes}`,
732
+ `--max-event-bytes ${control.observability.max_event_bytes}`,
733
+ `--tail-lines ${control.observability.tail_lines}`,
734
+ `--output-dir ${control.observability.output_dir}`,
735
+ ].join(" ");
736
+ const postMatcher = control.observability.include_tools.length
737
+ ? control.observability.include_tools.join("|")
738
+ : undefined;
739
+
740
+ const ensureEventHook = (event: string, matcher?: string) => {
741
+ const entry: any = {
742
+ hooks: [
743
+ {
744
+ type: "command",
745
+ command: `${baseCommand} --event ${event} ${commonArgs}`.trim(),
746
+ timeout: 5,
747
+ },
748
+ ],
749
+ };
750
+ if (matcher) entry.matcher = matcher;
751
+ hooks[event] = Array.isArray(hooks[event]) ? [...hooks[event], entry] : [entry];
752
+ };
753
+
754
+ if (control.observability.include_events.includes("PostToolUse")) {
755
+ ensureEventHook("PostToolUse", postMatcher);
756
+ }
757
+ if (control.observability.include_events.includes("UserPromptSubmit")) {
758
+ ensureEventHook("UserPromptSubmit");
759
+ }
760
+ if (control.observability.include_events.includes("Stop")) {
761
+ ensureEventHook("Stop");
762
+ }
763
+ }
764
+
765
+ return {
766
+ includeCoAuthoredBy: control.settings.include_co_authored_by,
767
+ env: control.settings.env,
768
+ mcp: {
769
+ enableAllProjectMcpServers: control.mcp.enable_all_project_mcp_servers,
770
+ enabledMcpjsonServers: control.mcp.enabled_mcpjson_servers,
771
+ },
772
+ permissions: {
773
+ allow: control.policy.permissions.allow,
774
+ deny: control.policy.permissions.deny,
775
+ behavior: {
776
+ on_conflict: control.policy.permissions.on_conflict,
777
+ on_unknown: control.policy.permissions.on_unknown,
778
+ },
779
+ },
780
+ plugins: {
781
+ enable: control.policy.plugins.enable,
782
+ allowed_plugin_ids: control.policy.plugins.allowed_plugin_ids,
783
+ denied_plugin_ids: control.policy.plugins.denied_plugin_ids,
784
+ behavior: {
785
+ on_unknown: control.policy.plugins.on_unknown,
786
+ },
787
+ enabledPlugins: control.policy.plugins.enabled_plugins,
788
+ },
789
+ lsp: {
790
+ enabledPlugins: control.lsp.enabled_plugins,
791
+ },
792
+ hooks,
793
+ claude_md: {
794
+ inject_control_entry: control.claude_md.inject_control_entry,
795
+ marker: control.claude_md.marker,
796
+ },
797
+ };
798
+ }
799
+
800
+ export function controlToMcpJson(control: ControlV0) {
801
+ if (Array.isArray(control.mcp.servers)) {
802
+ return { servers: control.mcp.servers };
803
+ }
804
+ if (isObject(control.mcp.servers)) {
805
+ return { mcpServers: control.mcp.servers };
806
+ }
807
+ return { servers: [] };
808
+ }