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,92 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { execFile } from "node:child_process";
6
+ import { promisify } from "node:util";
7
+
8
+ const execFileP = promisify(execFile);
9
+
10
+ function sortKeys(value) {
11
+ if (Array.isArray(value)) return value.map(sortKeys);
12
+ if (value && typeof value === "object") {
13
+ const out = {};
14
+ for (const key of Object.keys(value).sort()) {
15
+ out[key] = sortKeys(value[key]);
16
+ }
17
+ return out;
18
+ }
19
+ return value;
20
+ }
21
+
22
+ async function normalizeJson(p) {
23
+ const raw = await fs.readFile(p, "utf8");
24
+ const parsed = JSON.parse(raw);
25
+ return JSON.stringify(sortKeys(parsed), null, 2);
26
+ }
27
+
28
+ test("control demo full roundtrip is deterministic", async () => {
29
+ const tmp = path.join(process.cwd(), ".tmp_test_full_roundtrip");
30
+ const fixture = path.join(process.cwd(), "fixtures", "pos", "claude_code_demo_full");
31
+ const proj = path.join(tmp, "proj");
32
+ const out1 = path.join(tmp, "out1");
33
+ const out2 = path.join(tmp, "out2");
34
+ await fs.rm(tmp, { recursive: true, force: true });
35
+ await fs.mkdir(tmp, { recursive: true });
36
+ await fs.cp(fixture, proj, { recursive: true });
37
+
38
+ await execFileP("node", ["dist/cli.js", "control", "prefill", "--project", proj, "--out", out1]);
39
+ await execFileP("node", ["dist/cli.js", "control", "prefill", "--project", proj, "--out", out2]);
40
+
41
+ const control1 = await normalizeJson(path.join(out1, "mova", "control_v0.json"));
42
+ const control2 = await normalizeJson(path.join(out2, "mova", "control_v0.json"));
43
+ assert.equal(control1, control2);
44
+
45
+ await execFileP("node", [
46
+ "dist/cli.js",
47
+ "control",
48
+ "apply",
49
+ "--project",
50
+ proj,
51
+ "--profile",
52
+ path.join(out1, "mova", "control_v0.json"),
53
+ "--mode",
54
+ "apply",
55
+ "--out",
56
+ out1,
57
+ ]);
58
+
59
+ const control = JSON.parse(await fs.readFile(path.join(out1, "mova", "control_v0.json"), "utf8"));
60
+ const { controlToSettingsV0 } = await import("../dist/control_v0.js");
61
+ const expectedSettingsPath = path.join(out1, "mova", "control_v0_settings_expected.json");
62
+ await fs.writeFile(expectedSettingsPath, JSON.stringify(controlToSettingsV0(control), null, 2));
63
+ const settingsExpected = await normalizeJson(expectedSettingsPath);
64
+ const settingsOut = await normalizeJson(path.join(proj, ".claude", "settings.json"));
65
+ assert.equal(settingsOut, settingsExpected);
66
+
67
+ const mcpOut = await normalizeJson(path.join(proj, ".mcp.json"));
68
+ const mcpExpected = await normalizeJson(path.join(fixture, ".mcp.json"));
69
+ assert.equal(mcpOut, mcpExpected);
70
+
71
+ const claude = await fs.readFile(path.join(proj, "CLAUDE.md"), "utf8");
72
+ assert.ok(claude.includes("MOVA_CONTROL_ENTRY_V0"));
73
+
74
+ await execFileP("node", [
75
+ "dist/cli.js",
76
+ "control",
77
+ "apply",
78
+ "--project",
79
+ proj,
80
+ "--profile",
81
+ path.join(out1, "mova", "control_v0.json"),
82
+ "--mode",
83
+ "apply",
84
+ "--out",
85
+ out1,
86
+ ]);
87
+
88
+ const settingsOut2 = await normalizeJson(path.join(proj, ".claude", "settings.json"));
89
+ const mcpOut2 = await normalizeJson(path.join(proj, ".mcp.json"));
90
+ assert.equal(settingsOut, settingsOut2);
91
+ assert.equal(mcpOut, mcpOut2);
92
+ });
@@ -0,0 +1,36 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { execFile } from "node:child_process";
6
+ import { promisify } from "node:util";
7
+ import { runControlSurfaceCoverageV0 } from "../dist/control_surface_coverage_v0.js";
8
+
9
+ const execFileP = promisify(execFile);
10
+
11
+ test("control surface coverage reaches 100%", async () => {
12
+ const tmp = path.join(process.cwd(), ".tmp_test_surface_coverage");
13
+ const proj = path.join(tmp, "proj");
14
+ const out = path.join(tmp, "out");
15
+ await fs.rm(tmp, { recursive: true, force: true });
16
+ await fs.mkdir(tmp, { recursive: true });
17
+
18
+ const fixture = path.join(process.cwd(), "fixtures", "pos", "claude_code_demo_full");
19
+ await fs.cp(fixture, proj, { recursive: true });
20
+
21
+ await execFileP("node", ["dist/cli.js", "control", "prefill", "--project", proj, "--out", out]);
22
+
23
+ const showcaseRoot = process.env.SHOWCASE_ROOT || fixture;
24
+ const controlPath = path.join(out, "mova", "control_v0.json");
25
+ const exclusionsPath = path.join(process.cwd(), "control_surface_exclusions_v0.json");
26
+ const reportPath = path.join(tmp, "control_surface_coverage_report_v0.json");
27
+
28
+ const report = await runControlSurfaceCoverageV0({
29
+ showcaseRoot,
30
+ controlPath,
31
+ exclusionsPath,
32
+ reportPath,
33
+ });
34
+
35
+ assert.equal(report.coverage_percent, 100);
36
+ });
@@ -0,0 +1,69 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { execFile } from "node:child_process";
6
+ import { promisify } from "node:util";
7
+
8
+ const execFileP = promisify(execFile);
9
+
10
+ test("control check validates schema and fails on invalid control", async () => {
11
+ const tmp = path.join(process.cwd(), ".tmp_test_control_schema");
12
+ const proj = path.join(tmp, "proj");
13
+ const out = path.join(tmp, "out");
14
+ await fs.rm(tmp, { recursive: true, force: true });
15
+ await fs.mkdir(tmp, { recursive: true });
16
+
17
+ const fixture = path.join(process.cwd(), "fixtures", "pos", "claude_code_demo_full");
18
+ await fs.cp(fixture, proj, { recursive: true });
19
+
20
+ await execFileP("node", ["dist/cli.js", "control", "prefill", "--project", proj, "--out", out]);
21
+ await execFileP("node", [
22
+ "dist/cli.js",
23
+ "control",
24
+ "apply",
25
+ "--project",
26
+ proj,
27
+ "--profile",
28
+ path.join(out, "mova", "control_v0.json"),
29
+ "--mode",
30
+ "apply",
31
+ "--out",
32
+ out,
33
+ ]);
34
+
35
+ await execFileP("node", [
36
+ "dist/cli.js",
37
+ "control",
38
+ "check",
39
+ "--project",
40
+ proj,
41
+ "--profile",
42
+ path.join(out, "mova", "control_v0.json"),
43
+ "--out",
44
+ out,
45
+ ]);
46
+
47
+ const invalidControlPath = path.join(tmp, "invalid_control.json");
48
+ const invalid = JSON.parse(await fs.readFile(path.join(out, "mova", "control_v0.json"), "utf8"));
49
+ invalid.policy.mode = 123;
50
+ await fs.writeFile(invalidControlPath, JSON.stringify(invalid, null, 2));
51
+
52
+ let code = 0;
53
+ try {
54
+ await execFileP("node", [
55
+ "dist/cli.js",
56
+ "control",
57
+ "check",
58
+ "--project",
59
+ proj,
60
+ "--profile",
61
+ invalidControlPath,
62
+ "--out",
63
+ out,
64
+ ]);
65
+ } catch (err) {
66
+ code = err?.code ?? 1;
67
+ }
68
+ assert.equal(code, 2);
69
+ });
@@ -26,6 +26,7 @@ test("init v0 creates profile scaffold and optional zip", async () => {
26
26
  await assertExists(path.join(out, "CLAUDE.md"));
27
27
  await assertExists(path.join(out, ".claude", "settings.json"));
28
28
  await assertExists(path.join(out, ".claude", "skills", "mova-layer-v0", "SKILL.md"));
29
+ await assertExists(path.join(out, "mova", "control_v0.json"));
29
30
 
30
31
  const initManifestPath = path.join(out, "mova", "claude_import", "v0", "init_manifest_v0.json");
31
32
  const initManifest = JSON.parse(await fs.readFile(initManifestPath, "utf8"));
@@ -33,5 +34,13 @@ test("init v0 creates profile scaffold and optional zip", async () => {
33
34
  assert.ok(initManifest.zip_sha256);
34
35
  assert.ok(initManifest.zip_rel_path);
35
36
 
37
+ const controlPath = path.join(out, "mova", "control_v0.json");
38
+ const control = JSON.parse(await fs.readFile(controlPath, "utf8"));
39
+ assert.equal(control.version, "control_v0");
40
+ assert.equal(control.policy.mode, "report_only");
41
+ assert.equal(control.policy.hooks.on_invalid_hook, "report_only");
42
+ assert.equal(control.policy.permissions.on_unknown, "report_only");
43
+ assert.equal(control.policy.plugins.on_unknown, "report_only");
44
+
36
45
  await assertExists(path.join(out, initManifest.zip_rel_path));
37
46
  });
@@ -0,0 +1,59 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { execFile } from "node:child_process";
6
+ import { promisify } from "node:util";
7
+
8
+ const execFileP = promisify(execFile);
9
+
10
+ test("observability writer logs events without secrets", async () => {
11
+ const tmp = path.join(process.cwd(), ".tmp_test_observability");
12
+ const proj = path.join(tmp, "proj");
13
+ await fs.rm(tmp, { recursive: true, force: true });
14
+ await fs.mkdir(tmp, { recursive: true });
15
+
16
+ await execFileP("node", ["dist/cli.js", "init", "--out", proj]);
17
+ await execFileP("node", [
18
+ "dist/cli.js",
19
+ "control",
20
+ "apply",
21
+ "--project",
22
+ proj,
23
+ "--profile",
24
+ path.join(proj, "mova", "control_v0.json"),
25
+ "--mode",
26
+ "apply",
27
+ "--out",
28
+ proj,
29
+ ]);
30
+
31
+ const scriptPath = path.join(proj, ".claude", "hooks", "mova-observe.js");
32
+ await fs.stat(scriptPath);
33
+
34
+ await execFileP("node", [scriptPath, "--event", "PostToolUse"], {
35
+ env: {
36
+ ...process.env,
37
+ CLAUDE_PROJECT_DIR: proj,
38
+ CLAUDE_TOOL_NAME: "Bash",
39
+ CLAUDE_TOOL_STDOUT: "TOKEN=supersecret\nPLACEHOLDER=${TOKEN}\n",
40
+ CLAUDE_TOOL_STDERR: "sk-abcdef123456\n",
41
+ },
42
+ });
43
+
44
+ const episodesRoot = path.join(proj, ".mova", "episodes");
45
+ const runDirs = await fs.readdir(episodesRoot, { withFileTypes: true });
46
+ const runDir = runDirs.find((d) => d.isDirectory())?.name;
47
+ assert.ok(runDir, "expected run directory");
48
+
49
+ const eventsPath = path.join(episodesRoot, runDir, "events.jsonl");
50
+ const summaryPath = path.join(episodesRoot, runDir, "summary.json");
51
+ const events = (await fs.readFile(eventsPath, "utf8")).trim().split(/\r?\n/);
52
+ assert.ok(events.length >= 1);
53
+ const event = JSON.parse(events[0]);
54
+ assert.equal(event.event_type, "PostToolUse");
55
+ assert.ok(!JSON.stringify(event).includes("supersecret"));
56
+
57
+ const summary = JSON.parse(await fs.readFile(summaryPath, "utf8"));
58
+ assert.equal(summary.run_id, runDir);
59
+ });
@@ -0,0 +1,55 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { execFile } from "node:child_process";
6
+ import { promisify } from "node:util";
7
+
8
+ const execFileP = promisify(execFile);
9
+
10
+ function hasHookCommand(hooks, needle) {
11
+ if (!Array.isArray(hooks)) return false;
12
+ for (const entry of hooks) {
13
+ for (const hook of entry?.hooks ?? []) {
14
+ if (typeof hook?.command === "string" && hook.command.includes(needle)) return true;
15
+ }
16
+ }
17
+ return false;
18
+ }
19
+
20
+ test("preset list includes safe_observable_v0", async () => {
21
+ const { stdout } = await execFileP("node", ["dist/cli.js", "preset", "list"]);
22
+ assert.ok(stdout.split(/\r?\n/).includes("safe_observable_v0"));
23
+ });
24
+
25
+ test("preset overlay applies assets and hooks", async () => {
26
+ const tmp = path.join(process.cwd(), ".tmp_test_preset");
27
+ const proj = path.join(tmp, "proj");
28
+ await fs.rm(tmp, { recursive: true, force: true });
29
+ await fs.mkdir(tmp, { recursive: true });
30
+
31
+ await execFileP("node", ["dist/cli.js", "init", "--out", proj]);
32
+ await execFileP("node", [
33
+ "dist/cli.js",
34
+ "control",
35
+ "apply",
36
+ "--preset",
37
+ "safe_observable_v0",
38
+ "--project",
39
+ proj,
40
+ "--mode",
41
+ "overlay",
42
+ "--out",
43
+ proj,
44
+ ]);
45
+
46
+ await fs.stat(path.join(proj, ".claude", "hooks", "skill-eval.sh"));
47
+ await fs.stat(path.join(proj, ".claude", "commands", "start.md"));
48
+ await fs.stat(path.join(proj, ".claude", "skills", "testing-patterns", "SKILL.md"));
49
+ await fs.stat(path.join(proj, ".claude", "hooks", "mova-observe.js"));
50
+
51
+ const settings = JSON.parse(await fs.readFile(path.join(proj, ".claude", "settings.json"), "utf8"));
52
+ assert.ok(hasHookCommand(settings?.hooks?.PostToolUse, "mova-observe.js"));
53
+ assert.ok(hasHookCommand(settings?.hooks?.PostToolUse, "--event PostToolUse"));
54
+ assert.ok(hasHookCommand(settings?.hooks?.UserPromptSubmit, "skill-eval.sh"));
55
+ });
@@ -35,6 +35,7 @@ test("profile v0 output includes required files", async () => {
35
35
  await assertExists(path.join(out, ".claude", "commands", "mova_lint.md"));
36
36
  await assertExists(path.join(out, ".claude", "skills", "mova-layer-v0", "SKILL.md"));
37
37
  await assertExists(path.join(out, "mova", "claude_import", "v0", "lint_report_v0.json"));
38
+ await assertExists(path.join(out, "mova", "control_v0.json"));
38
39
  const versionPath = path.join(out, "mova", "claude_import", "v0", "VERSION.json");
39
40
  await assertExists(versionPath);
40
41
  const version = JSON.parse(await fs.readFile(versionPath, "utf8"));
@@ -31,6 +31,7 @@ test("init creates full scaffold surfaces", async () => {
31
31
  await assertExists(path.join(out, ".claude", "output-styles", "example_style.md"));
32
32
  await assertExists(path.join(out, ".claude", "hooks", "example_hook.sh"));
33
33
  await assertExists(path.join(out, ".mcp.json"));
34
+ await assertExists(path.join(out, "mova", "control_v0.json"));
34
35
  });
35
36
 
36
37
  test("control apply creates missing surfaces without erasing content", async () => {
@@ -0,0 +1,27 @@
1
+ import { runControlSurfaceCoverageV0 } from "../dist/control_surface_coverage_v0.js";
2
+
3
+ const args = process.argv.slice(2);
4
+ const getArg = (name) => {
5
+ const idx = args.indexOf(name);
6
+ if (idx === -1) return undefined;
7
+ return args[idx + 1];
8
+ };
9
+
10
+ const showcaseRoot = getArg("--showcase");
11
+ const controlPath = getArg("--control");
12
+ const exclusionsPath = getArg("--exclusions");
13
+ const reportPath = getArg("--report");
14
+
15
+ if (!showcaseRoot || !controlPath || !exclusionsPath || !reportPath) {
16
+ console.error("Usage: node tools/control_surface_coverage_v0.mjs --showcase <dir> --control <file> --exclusions <file> --report <file>");
17
+ process.exit(2);
18
+ }
19
+
20
+ const report = await runControlSurfaceCoverageV0({
21
+ showcaseRoot,
22
+ controlPath,
23
+ exclusionsPath,
24
+ reportPath,
25
+ });
26
+
27
+ process.stdout.write(JSON.stringify(report, null, 2) + "\n");
@@ -0,0 +1,33 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { execFile } from "node:child_process";
4
+ import { promisify } from "node:util";
5
+
6
+ const execFileP = promisify(execFile);
7
+ const tmp = path.join(process.cwd(), ".tmp_smoke");
8
+ const out = path.join(tmp, "out");
9
+
10
+ await fs.rm(tmp, { recursive: true, force: true });
11
+ await fs.mkdir(out, { recursive: true });
12
+
13
+ await execFileP("node", ["dist/cli.js", "init", "--out", out]);
14
+ await execFileP("node", [
15
+ "dist/cli.js",
16
+ "control",
17
+ "apply",
18
+ "--preset",
19
+ "safe_observable_v0",
20
+ "--project",
21
+ out,
22
+ "--mode",
23
+ "overlay",
24
+ "--out",
25
+ out,
26
+ ]);
27
+
28
+ await fs.stat(path.join(out, ".claude", "hooks", "skill-eval.sh"));
29
+ await fs.stat(path.join(out, ".claude", "commands", "start.md"));
30
+ await fs.stat(path.join(out, ".claude", "hooks", "mova-observe.js"));
31
+
32
+ await fs.rm(tmp, { recursive: true, force: true });
33
+ process.stdout.write("smoke: ok\n");
@@ -1,3 +0,0 @@
1
- # example_agent
2
-
3
- Role: reviewer
@@ -1,3 +0,0 @@
1
- # example_command
2
-
3
- Write a brief project summary.
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env bash
2
- echo "hook: dry check"
@@ -1,3 +0,0 @@
1
- # example_style
2
-
3
- Style: concise, bullet-first.
@@ -1,30 +0,0 @@
1
- {
2
- "claude_md": {
3
- "inject_control_entry": true,
4
- "marker": "<!-- MOVA_CONTROL_ENTRY_V0 -->"
5
- },
6
- "hooks": {
7
- "behavior": {
8
- "on_invalid_hook": "report_only"
9
- },
10
- "definitions": [],
11
- "enable": true
12
- },
13
- "permissions": {
14
- "allow": [],
15
- "behavior": {
16
- "on_conflict": "deny_wins",
17
- "on_unknown": "report_only"
18
- },
19
- "deny": []
20
- },
21
- "plugins": {
22
- "allowed_plugin_ids": [],
23
- "behavior": {
24
- "on_unknown": "report_only"
25
- },
26
- "denied_plugin_ids": [],
27
- "enable": true
28
- },
29
- "profile_version": "v0"
30
- }
@@ -1,3 +0,0 @@
1
- {
2
- "note": "Local settings are not committed; use this as a template."
3
- }
@@ -1,3 +0,0 @@
1
- {
2
- "servers": {}
3
- }
@@ -1,13 +0,0 @@
1
- <!-- MOVA_CONTROL_ENTRY_V0 -->
2
- ## MOVA Control Entry (v0)
3
-
4
- Source of truth (do this first):
5
- - mova/claude_import/v0/contracts/instruction_profile_v0.json
6
- - mova/claude_import/v0/contracts/skills_catalog_v0.json
7
- - mova/claude_import/v0/contracts/mcp_servers_v0.json
8
-
9
- Proof / evidence:
10
- - mova/claude_import/v0/quality_report_v0.json
11
- - mova/claude_import/v0/export_manifest_v0.json
12
-
13
- Hello
@@ -1,3 +0,0 @@
1
- # MOVA
2
-
3
- Notes and operator instructions.
@@ -1 +0,0 @@
1
- {"servers":[]}
@@ -1 +0,0 @@
1
- Hello
@@ -1,114 +0,0 @@
1
- {
2
- "anthropic": {
3
- "claude_md": {
4
- "description": "Политика вставки управляющего блока MOVA и позиционирование его в CLAUDE.md.",
5
- "inject_control_entry": true,
6
- "marker": "<!-- MOVA_CONTROL_ENTRY_V0 -->",
7
- "paths": {
8
- "local": "CLAUDE.local.md",
9
- "project_alt": ".claude/CLAUDE.md",
10
- "project_primary": "CLAUDE.md"
11
- }
12
- },
13
- "hooks": {
14
- "behavior": {
15
- "on_invalid_hook": "report_only"
16
- },
17
- "definitions": [],
18
- "description": "Крючки (hooks) Claude Code, если используются в проекте/плагинах.",
19
- "enable": true
20
- },
21
- "mcp": {
22
- "description": "Подключения MCP серверов и правила по переменным окружения.",
23
- "env_expansion_policy": {
24
- "allow_defaults_syntax": true,
25
- "on_missing_var": "report_only",
26
- "require_all_vars": false
27
- },
28
- "servers": []
29
- },
30
- "permissions": {
31
- "allow": [],
32
- "behavior": {
33
- "on_conflict": "deny_wins",
34
- "on_unknown": "report_only"
35
- },
36
- "deny": [],
37
- "description": "Правила allow/deny (в стиле Claude Code) для действий и инструментов."
38
- },
39
- "plugins": {
40
- "allowed_plugin_ids": [],
41
- "behavior": {
42
- "on_unknown": "report_only"
43
- },
44
- "denied_plugin_ids": [],
45
- "description": "Политика использования плагинов/команд. На v0 — только реестр намерений и отчёты.",
46
- "enable": true
47
- },
48
- "scopes_enabled": {
49
- "local": true,
50
- "managed": false,
51
- "project": true,
52
- "user": true
53
- }
54
- },
55
- "apply": {
56
- "backup_before_write": true,
57
- "behavior": {
58
- "on_parse_error": "report_only",
59
- "on_unwritable_target": "report_only"
60
- },
61
- "default_apply_mode": "preview",
62
- "default_target": "project_only",
63
- "description": "Настройки операции apply (разнос из профиля в файлы Claude Code).",
64
- "write_scopes": {
65
- "local": false,
66
- "project": true,
67
- "user": false
68
- }
69
- },
70
- "id": "ds.claude_control_profile_v0",
71
- "kind": "ds",
72
- "mode": "report_only",
73
- "mova": {
74
- "export": {
75
- "description": "Экспорт zip для передачи/архивации результата.",
76
- "emit_export_manifest": true,
77
- "emit_zip": false,
78
- "zip_name": "export.zip"
79
- },
80
- "observability": {
81
- "artifact_root": "mova/claude_import/v0",
82
- "description": "Артефакты наблюдаемости и доказательности. Включены по умолчанию.",
83
- "emit_apply_report": true,
84
- "emit_manifest": true,
85
- "emit_policy_report": true,
86
- "emit_quality_report": true,
87
- "emit_version_anchor": true
88
- },
89
- "overlay": {
90
- "description": "MOVA Control Overlay — добавляет понятный блок управления и ссылки на отчёты.",
91
- "emit": true
92
- },
93
- "premium": {
94
- "description": "Премиум-блоки по умолчанию выключены. Включаются пользователем, если он подключает соответствующие пакеты/дверь.",
95
- "proofkits": {
96
- "enabled": false,
97
- "selected": []
98
- },
99
- "tool_door": {
100
- "auth_ref": "",
101
- "enabled": false,
102
- "endpoint": "",
103
- "policy_profile_id": ""
104
- }
105
- }
106
- },
107
- "notes": [
108
- "Единая точка управления контролем Claude Code + MOVA слой наблюдаемости.",
109
- "По умолчанию НЕ ломает работу: формирует отчёты и подсказки. Жёсткое применение/запреты включаются явно пользователем.",
110
- "Этот профиль можно заполнить вручную или получить prefill через импорт существующей папки Claude."
111
- ],
112
- "title": "Claude Control Profile v0",
113
- "version": "0.1.0"
114
- }
@@ -1,13 +0,0 @@
1
- {
2
- "applied": {
3
- "mcp_servers": true
4
- },
5
- "found": {
6
- "mcp_json": true,
7
- "settings_json": false,
8
- "settings_local_json": false
9
- },
10
- "profile_path": "D:\\Projects_Clean\\mova-claude-import\\.tmp_test_control_prefill\\out1\\claude_control_profile_v0.json",
11
- "profile_version": "v0",
12
- "project_dir": "D:\\Projects_Clean\\mova-claude-import\\fixtures\\pos\\control_basic_project"
13
- }