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,157 @@
1
+ export function getMovaObserveScriptV0() {
2
+ return [
3
+ "#!/usr/bin/env node",
4
+ "import fs from \"node:fs/promises\";",
5
+ "import path from \"node:path\";",
6
+ "import crypto from \"node:crypto\";",
7
+ "const KEY_RE = /(api[_-]?key|token|secret|password|authorization|bearer)/i;",
8
+ "const INLINE_SECRET_RE = /(sk-[a-zA-Z0-9]{8,})/g;",
9
+ "const PLACEHOLDER_RE = /^\\$\\{[A-Z0-9_]+(?::-?[^}]+)?\\}$/;",
10
+ "const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();",
11
+ "const args = process.argv.slice(2);",
12
+ "const readArg = (name) => {",
13
+ " const idx = args.indexOf(name);",
14
+ " if (idx === -1) return undefined;",
15
+ " return args[idx + 1];",
16
+ "};",
17
+ "const eventType = readArg(\"--event\") || process.env.CLAUDE_HOOK_EVENT || \"post_tool_use\";",
18
+ "const stdoutTailBytes = Number(readArg(\"--stdout-tail-bytes\") || process.env.MOVA_OBS_STDOUT_TAIL_BYTES || 4000);",
19
+ "const stderrTailBytes = Number(readArg(\"--stderr-tail-bytes\") || process.env.MOVA_OBS_STDERR_TAIL_BYTES || 4000);",
20
+ "const maxEventBytes = Number(readArg(\"--max-event-bytes\") || process.env.MOVA_OBS_MAX_EVENT_BYTES || 20000);",
21
+ "const tailLines = Number(readArg(\"--tail-lines\") || process.env.MOVA_OBS_TAIL_LINES || 50);",
22
+ "const outputDir = readArg(\"--output-dir\") || process.env.MOVA_OBS_OUTPUT_DIR || \".mova/episodes\";",
23
+ "const now = new Date();",
24
+ "const iso = now.toISOString();",
25
+ "const env = process.env;",
26
+ "const sessionId = env.CLAUDE_SESSION_ID || env.CLAUDE_RUN_ID;",
27
+ "async function ensureRunId(root) {",
28
+ " if (sessionId) return sessionId;",
29
+ " const currentPath = path.join(root, \".current_run_id\");",
30
+ " try {",
31
+ " const raw = await fs.readFile(currentPath, \"utf8\");",
32
+ " if (raw.trim()) return raw.trim();",
33
+ " } catch {}",
34
+ " const runId = `run_${Date.now()}_${crypto.randomBytes(4).toString(\"hex\")}`;",
35
+ " await fs.mkdir(root, { recursive: true });",
36
+ " await fs.writeFile(currentPath, runId, \"utf8\");",
37
+ " return runId;",
38
+ "}",
39
+ "function tailLinesFn(text, maxLines) {",
40
+ " const lines = text.split(/\\r?\\n/);",
41
+ " if (lines.length <= maxLines) return text;",
42
+ " return lines.slice(-maxLines).join(\"\\n\");",
43
+ "}",
44
+ "function tailBytesFn(text, maxBytes) {",
45
+ " if (!maxBytes || maxBytes <= 0) return \"\";",
46
+ " const buf = Buffer.from(text, \"utf8\");",
47
+ " if (buf.length <= maxBytes) return text;",
48
+ " return buf.slice(buf.length - maxBytes).toString(\"utf8\");",
49
+ "}",
50
+ "function trimText(text, maxBytes, maxLines) {",
51
+ " return tailBytesFn(tailLinesFn(text, maxLines), maxBytes);",
52
+ "}",
53
+ "function redactText(input) {",
54
+ " let out = input;",
55
+ " out = out.replace(INLINE_SECRET_RE, () => \"[REDACTED_TOKEN]\");",
56
+ " out = out.replace(/^([A-Z0-9_]{3,80})\\s*=\\s*(.+)$/gmi, (line, k, v) => {",
57
+ " if (!KEY_RE.test(k)) return line;",
58
+ " if (PLACEHOLDER_RE.test(String(v).trim())) return line;",
59
+ " return `${k}=[REDACTED_VALUE_LEN_${String(v).length}]`;",
60
+ " });",
61
+ " return out;",
62
+ "}",
63
+ "function stableStringify(value) {",
64
+ " if (Array.isArray(value)) return `[${value.map(stableStringify).join(\",\")}]`;",
65
+ " if (value && typeof value === \"object\") {",
66
+ " const keys = Object.keys(value).sort();",
67
+ " const parts = keys.map((k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`);",
68
+ " return `{${parts.join(\",\")}}`;",
69
+ " }",
70
+ " return JSON.stringify(value);",
71
+ "}",
72
+ "function sha(text) {",
73
+ " return crypto.createHash(\"sha256\").update(text, \"utf8\").digest(\"hex\");",
74
+ "}",
75
+ "async function run() {",
76
+ " const root = path.join(projectDir, outputDir);",
77
+ " const runId = await ensureRunId(root);",
78
+ " const runDir = path.join(root, runId);",
79
+ " await fs.mkdir(runDir, { recursive: true });",
80
+ " const event = {",
81
+ " ts: iso,",
82
+ " event_type: String(eventType),",
83
+ " tool_name: env.CLAUDE_TOOL_NAME || null,",
84
+ " ok: env.CLAUDE_TOOL_STATUS ? env.CLAUDE_TOOL_STATUS === \"0\" : null,",
85
+ " durations: env.CLAUDE_TOOL_DURATION_MS ? { tool_ms: Number(env.CLAUDE_TOOL_DURATION_MS) } : null,",
86
+ " paths: {",
87
+ " input: env.CLAUDE_TOOL_INPUT_FILE_PATH || null,",
88
+ " output: env.CLAUDE_TOOL_OUTPUT_FILE_PATH || null",
89
+ " },",
90
+ " stdout_tail: null,",
91
+ " stderr_tail: null,",
92
+ " mcp: { server_name: env.CLAUDE_MCP_SERVER || null },",
93
+ " hashes: { input_hash: null, output_hash: null }",
94
+ " };",
95
+ " const stdoutRaw = env.CLAUDE_TOOL_STDOUT || env.CLAUDE_TOOL_OUTPUT || \"\";",
96
+ " const stderrRaw = env.CLAUDE_TOOL_STDERR || \"\";",
97
+ " let outputHashPayload = \"\";",
98
+ " if (stdoutRaw) {",
99
+ " const trimmed = trimText(stdoutRaw, stdoutTailBytes, tailLines);",
100
+ " const redacted = redactText(trimmed);",
101
+ " event.stdout_tail = redacted;",
102
+ " outputHashPayload += redacted;",
103
+ " }",
104
+ " if (stderrRaw) {",
105
+ " const trimmed = trimText(stderrRaw, stderrTailBytes, tailLines);",
106
+ " const redacted = redactText(trimmed);",
107
+ " event.stderr_tail = redacted;",
108
+ " outputHashPayload += `\\n${redacted}`;",
109
+ " }",
110
+ " if (outputHashPayload) {",
111
+ " event.hashes.output_hash = sha(outputHashPayload);",
112
+ " }",
113
+ " const inputRaw = env.CLAUDE_TOOL_INPUT || \"\";",
114
+ " if (inputRaw) {",
115
+ " const trimmed = trimText(inputRaw, stdoutTailBytes, tailLines);",
116
+ " const redacted = redactText(trimmed);",
117
+ " event.hashes.input_hash = sha(redacted);",
118
+ " }",
119
+ " let line = stableStringify(event);",
120
+ " if (line.length > maxEventBytes) {",
121
+ " event.stdout_tail = null;",
122
+ " event.stderr_tail = null;",
123
+ " line = stableStringify(event);",
124
+ " }",
125
+ " await fs.appendFile(path.join(runDir, \"events.jsonl\"), line + \"\\n\", \"utf8\");",
126
+ " const summaryPath = path.join(runDir, \"summary.json\");",
127
+ " let summary = {",
128
+ " run_id: runId,",
129
+ " started_at: iso,",
130
+ " last_event_at: iso,",
131
+ " counts: {},",
132
+ " tools: {}",
133
+ " };",
134
+ " try {",
135
+ " const raw = await fs.readFile(summaryPath, \"utf8\");",
136
+ " summary = JSON.parse(raw);",
137
+ " } catch {}",
138
+ " summary.last_event_at = iso;",
139
+ " summary.counts[event.event_type] = (summary.counts[event.event_type] || 0) + 1;",
140
+ " if (event.tool_name) {",
141
+ " summary.tools[event.tool_name] = (summary.tools[event.tool_name] || 0) + 1;",
142
+ " }",
143
+ " await fs.writeFile(summaryPath, stableStringify(summary) + \"\\n\", \"utf8\");",
144
+ " await fs.appendFile(path.join(root, \"index.jsonl\"), stableStringify({ ts: iso, run_id: runId, event_type: event.event_type }) + \"\\n\", \"utf8\");",
145
+ " if (String(event.event_type).toLowerCase() === \"stop\") {",
146
+ " const currentPath = path.join(root, \".current_run_id\");",
147
+ " try { await fs.rm(currentPath); } catch {}",
148
+ " }",
149
+ "}",
150
+ "run().catch((err) => {",
151
+ " const msg = String(err?.message || err || \"failed\");",
152
+ " process.stdout.write(JSON.stringify({ feedback: `MOVA observe: ${msg}`, suppressOutput: true }));",
153
+ " process.exit(0);",
154
+ "});",
155
+ "",
156
+ ].join("\n");
157
+ }
@@ -0,0 +1,8 @@
1
+ export declare function listObservabilityRuns(projectDir: string): Promise<{
2
+ run_id: string;
3
+ last_event_at: string | null;
4
+ started_at: string | null;
5
+ events: number;
6
+ }[]>;
7
+ export declare function tailObservabilityEvents(projectDir: string, runId: string, limit?: number): Promise<string[]>;
8
+ export declare function readObservabilitySummary(projectDir: string, runId: string): Promise<any>;
@@ -0,0 +1,57 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ async function exists(p) {
4
+ try {
5
+ await fs.stat(p);
6
+ return true;
7
+ }
8
+ catch {
9
+ return false;
10
+ }
11
+ }
12
+ async function readJson(p) {
13
+ const raw = await fs.readFile(p, "utf8");
14
+ return JSON.parse(raw);
15
+ }
16
+ function sumCounts(summary) {
17
+ if (!summary?.counts)
18
+ return 0;
19
+ return Object.values(summary.counts).reduce((acc, value) => acc + Number(value || 0), 0);
20
+ }
21
+ export async function listObservabilityRuns(projectDir) {
22
+ const root = path.join(projectDir, ".mova", "episodes");
23
+ if (!(await exists(root)))
24
+ return [];
25
+ const entries = await fs.readdir(root, { withFileTypes: true });
26
+ const runs = [];
27
+ for (const entry of entries) {
28
+ if (!entry.isDirectory())
29
+ continue;
30
+ const runId = entry.name;
31
+ const summaryPath = path.join(root, runId, "summary.json");
32
+ const summary = (await exists(summaryPath)) ? (await readJson(summaryPath)) : null;
33
+ const lastEventAt = summary?.last_event_at ?? null;
34
+ runs.push({
35
+ run_id: runId,
36
+ last_event_at: lastEventAt,
37
+ started_at: summary?.started_at ?? null,
38
+ events: sumCounts(summary),
39
+ });
40
+ }
41
+ runs.sort((a, b) => String(b.last_event_at ?? "").localeCompare(String(a.last_event_at ?? "")));
42
+ return runs;
43
+ }
44
+ export async function tailObservabilityEvents(projectDir, runId, limit = 20) {
45
+ const eventsPath = path.join(projectDir, ".mova", "episodes", runId, "events.jsonl");
46
+ if (!(await exists(eventsPath)))
47
+ return [];
48
+ const raw = await fs.readFile(eventsPath, "utf8");
49
+ const lines = raw.trim().split(/\r?\n/).filter(Boolean);
50
+ return lines.slice(-limit);
51
+ }
52
+ export async function readObservabilitySummary(projectDir, runId) {
53
+ const summaryPath = path.join(projectDir, ".mova", "episodes", runId, "summary.json");
54
+ if (!(await exists(summaryPath)))
55
+ return null;
56
+ return await readJson(summaryPath);
57
+ }
@@ -0,0 +1,11 @@
1
+ type PresetInfo = {
2
+ name: string;
3
+ root: string;
4
+ control_path: string;
5
+ assets_root: string;
6
+ };
7
+ export declare function getPresetsRoot(): string;
8
+ export declare function listPresets(): Promise<string[]>;
9
+ export declare function resolvePreset(name: string): Promise<PresetInfo | null>;
10
+ export declare function readPresetControlRaw(name: string): Promise<string | null>;
11
+ export {};
@@ -0,0 +1,49 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ async function exists(p) {
4
+ try {
5
+ await fs.stat(p);
6
+ return true;
7
+ }
8
+ catch {
9
+ return false;
10
+ }
11
+ }
12
+ export function getPresetsRoot() {
13
+ return path.join(process.cwd(), "presets");
14
+ }
15
+ export async function listPresets() {
16
+ const root = getPresetsRoot();
17
+ if (!(await exists(root)))
18
+ return [];
19
+ const entries = await fs.readdir(root, { withFileTypes: true });
20
+ const names = [];
21
+ for (const entry of entries) {
22
+ if (!entry.isDirectory())
23
+ continue;
24
+ const controlPath = path.join(root, entry.name, "control_v0.json");
25
+ if (await exists(controlPath))
26
+ names.push(entry.name);
27
+ }
28
+ return names.sort();
29
+ }
30
+ export async function resolvePreset(name) {
31
+ const root = getPresetsRoot();
32
+ const presetRoot = path.join(root, name);
33
+ const controlPath = path.join(presetRoot, "control_v0.json");
34
+ const assetsRoot = path.join(presetRoot, "assets");
35
+ if (!(await exists(controlPath)))
36
+ return null;
37
+ return {
38
+ name,
39
+ root: presetRoot,
40
+ control_path: controlPath,
41
+ assets_root: assetsRoot,
42
+ };
43
+ }
44
+ export async function readPresetControlRaw(name) {
45
+ const preset = await resolvePreset(name);
46
+ if (!preset)
47
+ return null;
48
+ return await fs.readFile(preset.control_path, "utf8");
49
+ }
package/dist/redaction.js CHANGED
@@ -1,6 +1,10 @@
1
1
  import crypto from "node:crypto";
2
2
  const KEY_RE = /(api[_-]?key|token|secret|password|authorization|bearer)/i;
3
3
  const INLINE_SECRET_RE = /(sk-[a-zA-Z0-9]{8,})/g; // best‑effort
4
+ const PLACEHOLDER_RE = /^\$\{[A-Z0-9_]+(?::-?[^}]+)?\}$/;
5
+ function isPlaceholderValue(value) {
6
+ return PLACEHOLDER_RE.test(value.trim());
7
+ }
4
8
  export function redactText(input) {
5
9
  const hits = [];
6
10
  let out = input;
@@ -33,7 +37,7 @@ export function redactJson(obj) {
33
37
  if (typeof x === "object") {
34
38
  const out = {};
35
39
  for (const [k, v] of Object.entries(x)) {
36
- if (KEY_RE.test(k) && typeof v === "string") {
40
+ if (KEY_RE.test(k) && typeof v === "string" && !isPlaceholderValue(v)) {
37
41
  hits.push({ rule_id: "json_secret_field", key: k, len: v.length });
38
42
  out[k] = `[REDACTED_VALUE_LEN_${v.length}]`;
39
43
  }
@@ -10,11 +10,13 @@ import { getAnthropicProfileV0Files } from "./anthropic_profile_v0.js";
10
10
  import { lintV0 } from "./lint_v0.js";
11
11
  import { stableStringify } from "./stable_json.js";
12
12
  import { createExportZipV0 } from "./export_zip_v0.js";
13
- import { buildMovaOverlayV0, buildMovaControlEntryV0, MOVA_CONTROL_ENTRY_MARKER } from "./mova_overlay_v0.js";
13
+ import { buildMovaOverlayV0, buildMovaControlEntryV0 } from "./mova_overlay_v0.js";
14
14
  import { scanInputPolicyV0 } from "./input_policy_v0.js";
15
15
  import { EvidenceWriter } from "@leryk1981/mova-core-engine";
16
16
  import { MOVA_SPEC_BINDINGS_V0 } from "./mova_spec_bindings_v0.js";
17
17
  import { writeCleanClaudeProfileScaffoldV0 } from "./claude_profile_scaffold_v0.js";
18
+ import { controlFromSettingsV0, controlToMcpJson, controlToSettingsV0, normalizeControlV0, } from "./control_v0.js";
19
+ import { getMovaObserveScriptV0 } from "./observability_writer_v0.js";
18
20
  async function exists(p) {
19
21
  try {
20
22
  await fs.stat(p);
@@ -24,6 +26,9 @@ async function exists(p) {
24
26
  return false;
25
27
  }
26
28
  }
29
+ function isObject(value) {
30
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
31
+ }
27
32
  async function sha256File(p) {
28
33
  const buf = await fs.readFile(p);
29
34
  return crypto.createHash("sha256").update(buf).digest("hex");
@@ -59,6 +64,12 @@ async function scanProject(opts) {
59
64
  const mcpJson = path.join(projectDir, ".mcp.json");
60
65
  if (await exists(mcpJson))
61
66
  found.mcpJsonPath = mcpJson;
67
+ const settingsJson = path.join(projectDir, ".claude", "settings.json");
68
+ if (await exists(settingsJson))
69
+ found.settingsPath = settingsJson;
70
+ const controlJson = path.join(projectDir, "mova", "control_v0.json");
71
+ if (await exists(controlJson))
72
+ found.controlPath = controlJson;
62
73
  const skillsRoot = path.join(projectDir, ".claude", "skills");
63
74
  if (await exists(skillsRoot)) {
64
75
  const stack = [skillsRoot];
@@ -112,11 +123,15 @@ async function loadAndRedactJson(p) {
112
123
  const { redacted, hits } = redactJson(parsed);
113
124
  return { raw, parsed, redacted, hits };
114
125
  }
115
- async function ensureClaudeControlEntry(claudePath, block, marker) {
116
- const content = await fs.readFile(claudePath, "utf8");
117
- if (content.includes(marker))
118
- return;
119
- await fs.writeFile(claudePath, `${block}\n${content}`, "utf8");
126
+ function updateClaudeBlock(content, marker, block) {
127
+ if (content.includes(marker)) {
128
+ const idx = content.indexOf(marker);
129
+ const after = content.slice(idx);
130
+ const split = after.split("\n\n");
131
+ split[0] = block.trimEnd();
132
+ return content.slice(0, idx) + split.join("\n\n");
133
+ }
134
+ return `${block}\n${content}`;
120
135
  }
121
136
  function orderedObject(obj) {
122
137
  return JSON.parse(stableStringify(obj));
@@ -156,24 +171,67 @@ export async function runImport(opts) {
156
171
  redactionHits.push(...hits);
157
172
  return redacted;
158
173
  }
174
+ /** Process a JSON file – redact and record */
175
+ async function processJsonFile(rel, absPath) {
176
+ const { parsed, hits } = await loadAndRedactJson(absPath);
177
+ inputs.push({ rel, sha256: await sha256File(absPath) });
178
+ redactionHits.push(...hits);
179
+ return parsed;
180
+ }
181
+ const controlRel = "mova/control_v0.json";
182
+ let controlOutput = null;
183
+ let controlDefaults = [];
184
+ let controlSource = "migrated";
185
+ let controlMigrationReport = null;
186
+ if (found.controlPath) {
187
+ const controlParsed = await processJsonFile(controlRel, found.controlPath);
188
+ const normalized = normalizeControlV0(controlParsed);
189
+ controlOutput = normalized.control;
190
+ controlDefaults = normalized.defaults;
191
+ controlSource = "control_file";
192
+ }
193
+ else {
194
+ let settingsParsed;
195
+ let mcpParsed;
196
+ let settingsFound = false;
197
+ let mcpFound = false;
198
+ if (found.settingsPath) {
199
+ settingsParsed = await processJsonFile(".claude/settings.json", found.settingsPath);
200
+ settingsFound = true;
201
+ }
202
+ if (found.mcpJsonPath) {
203
+ mcpParsed = await processJsonFile(".mcp.json", found.mcpJsonPath);
204
+ mcpFound = true;
205
+ }
206
+ const migrated = controlFromSettingsV0(settingsParsed, mcpParsed);
207
+ controlOutput = migrated.control;
208
+ controlDefaults = migrated.defaults;
209
+ controlSource = "migrated";
210
+ controlMigrationReport = {
211
+ profile_version: "v0",
212
+ control_version: "control_v0",
213
+ project_dir: projectDir,
214
+ control_path: controlRel,
215
+ found: {
216
+ settings_json: settingsFound,
217
+ mcp_json: mcpFound,
218
+ },
219
+ sources: {
220
+ claude_md: "default",
221
+ overlay: "default",
222
+ policy: settingsFound ? "settings_json" : "default",
223
+ mcp: mcpFound ? "mcp_json" : "default",
224
+ },
225
+ defaults_used: controlDefaults,
226
+ };
227
+ }
228
+ const control = controlOutput ?? normalizeControlV0({}).control;
229
+ const mcpJsonParsed = controlToMcpJson(control);
159
230
  // CLAUDE.md
160
231
  let claudeMdRedacted = "";
161
232
  if (found.claudeMdPath) {
162
233
  claudeMdRedacted = await processTextFile("CLAUDE.md", found.claudeMdPath);
163
234
  }
164
- // .mcp.json
165
- let mcpJsonRedacted = "";
166
- let mcpJsonParsed;
167
- if (found.mcpJsonPath) {
168
- const { parsed, redacted, hits } = await loadAndRedactJson(found.mcpJsonPath);
169
- mcpJsonParsed = parsed;
170
- mcpJsonRedacted = stableStringify(redacted);
171
- redactionHits.push(...hits);
172
- inputs.push({
173
- rel: ".mcp.json",
174
- sha256: await sha256File(found.mcpJsonPath),
175
- });
176
- }
177
235
  // skill files
178
236
  const skillRedactedMap = {};
179
237
  for (const f of found.skillFiles) {
@@ -214,6 +272,9 @@ export async function runImport(opts) {
214
272
  };
215
273
  await writeEvidenceArtifact(evidenceWriter, movaBase, "VERSION.json", versionInfo);
216
274
  await writeEvidenceArtifact(evidenceWriter, movaBase, "input_policy_report_v0.json", inputPolicy);
275
+ if (controlMigrationReport) {
276
+ await writeEvidenceArtifact(evidenceWriter, movaBase, "control_migration_report_v0.json", controlMigrationReport);
277
+ }
217
278
  }
218
279
  if (opts.strict && !inputPolicy.ok) {
219
280
  const deniedRunId = computeRunId(inputPolicy.denied.map((d) => `${d.path}:${d.kind}:${d.reason}`).sort());
@@ -321,11 +382,16 @@ export async function runImport(opts) {
321
382
  }
322
383
  if (!opts.dryRun && opts.emitProfile) {
323
384
  await writeCleanClaudeProfileScaffoldV0(outRoot);
324
- if (opts.emitOverlay) {
385
+ const overlayEnabled = opts.emitOverlay && control.overlay.enable;
386
+ if (overlayEnabled) {
325
387
  const controlEntry = buildMovaControlEntryV0(overlayParams);
326
388
  const claudePath = path.join(outRoot, "CLAUDE.md");
327
389
  if (await exists(claudePath)) {
328
- await ensureClaudeControlEntry(claudePath, controlEntry, MOVA_CONTROL_ENTRY_MARKER);
390
+ if (control.claude_md.inject_control_entry) {
391
+ const raw = await fs.readFile(claudePath, "utf8");
392
+ const updated = updateClaudeBlock(raw, control.claude_md.marker, controlEntry);
393
+ await fs.writeFile(claudePath, updated, "utf8");
394
+ }
329
395
  }
330
396
  const overlayFiles = buildMovaOverlayV0(overlayParams);
331
397
  for (const [rel, content] of Object.entries(overlayFiles)) {
@@ -338,8 +404,13 @@ export async function runImport(opts) {
338
404
  continue;
339
405
  await writeTextFile(path.join(outRoot, rel), content);
340
406
  }
341
- if (mcpJsonParsed) {
342
- await writeJsonFile(path.join(outRoot, ".mcp.json"), mcpJsonParsed);
407
+ await writeJsonFile(path.join(outRoot, ".mcp.json"), mcpJsonParsed);
408
+ await writeJsonFile(path.join(outRoot, ".claude", "settings.json"), controlToSettingsV0(control));
409
+ await writeJsonFile(path.join(outRoot, controlRel), control);
410
+ if (control.observability.enable && control.observability.writer?.script_path) {
411
+ const scriptPath = path.join(outRoot, control.observability.writer.script_path);
412
+ await fs.mkdir(path.dirname(scriptPath), { recursive: true });
413
+ await fs.writeFile(scriptPath, getMovaObserveScriptV0(), "utf8");
343
414
  }
344
415
  for (const skill of normalizedSkills) {
345
416
  const outRel = path.join(".claude", "skills", skill.normDir, "SKILL.md");
@@ -364,9 +435,22 @@ export async function runImport(opts) {
364
435
  skill_md: skill.body,
365
436
  })),
366
437
  };
438
+ const mcpServersRaw = isObject(mcpJsonParsed?.mcpServers)
439
+ ? mcpJsonParsed?.mcpServers
440
+ : mcpJsonParsed?.servers;
441
+ const mcpServersList = Array.isArray(mcpServersRaw)
442
+ ? mcpServersRaw
443
+ : isObject(mcpServersRaw)
444
+ ? Object.entries(mcpServersRaw).map(([name, server]) => ({
445
+ name,
446
+ command: typeof server?.command === "string" ? server.command : "unknown",
447
+ args: Array.isArray(server?.args) ? server.args : [],
448
+ env_keys: isObject(server?.env) ? Object.keys(server.env).sort() : [],
449
+ }))
450
+ : [];
367
451
  const mcpServers = {
368
452
  profile_version: "v0",
369
- servers: Array.isArray(mcpJsonParsed?.servers) ? mcpJsonParsed?.servers : [],
453
+ servers: mcpServersList,
370
454
  };
371
455
  if (!opts.dryRun) {
372
456
  await writeJsonFile(path.join(movaBase, "contracts", "instruction_profile_v0.json"), instructionProfile);
@@ -407,6 +491,7 @@ export async function runImport(opts) {
407
491
  mcp_servers: validateMcp.errors,
408
492
  },
409
493
  };
494
+ const mcpSourcePresent = controlSource === "control_file" || Boolean(found.mcpJsonPath);
410
495
  const manifest = {
411
496
  tool: "mova-claude-import",
412
497
  version: "v0",
@@ -416,7 +501,7 @@ export async function runImport(opts) {
416
501
  inputs: inputs.sort((a, b) => a.rel.localeCompare(b.rel)),
417
502
  imported: {
418
503
  claude_md: Boolean(found.claudeMdPath),
419
- mcp_json: Boolean(found.mcpJsonPath),
504
+ mcp_json: mcpSourcePresent,
420
505
  skills_count: normalizedSkills.length,
421
506
  },
422
507
  skipped: found.skipped,
@@ -453,7 +538,7 @@ export async function runImport(opts) {
453
538
  lintReport = await lintV0({
454
539
  outRoot,
455
540
  emitProfile: opts.emitProfile,
456
- mcpExpected: Boolean(found.mcpJsonPath),
541
+ mcpExpected: mcpSourcePresent,
457
542
  });
458
543
  await writeEvidenceArtifact(evidenceWriter, movaBase, "lint_report_v0.json", lintReport);
459
544
  }
@@ -0,0 +1,78 @@
1
+ # Claude Control Surface Map v0
2
+
3
+ ## Как было в Claude Code
4
+
5
+ Основные файлы контроля:
6
+
7
+ - `CLAUDE.md` — project memory и правила.
8
+ - `.claude/settings.json` — hooks, permissions, plugins, MCP toggles.
9
+ - `.mcp.json` — MCP servers и env placeholders.
10
+ - `.claude/skills/` — навыки.
11
+ - `.claude/agents/` — агенты.
12
+ - `.claude/commands/` — команды.
13
+ - `.claude/rules/` — правила.
14
+ - `.claude/hooks/` — hook scripts и skill rules.
15
+ - `.github/workflows/` — CI сценарии.
16
+
17
+ ## Как стало с MOVA control
18
+
19
+ Один источник истины: `mova/control_v0.json`.
20
+
21
+ Из него генерируются:
22
+
23
+ - `CLAUDE.md` (MOVA Control Entry block по маркеру)
24
+ - `.claude/settings.json`
25
+ - `.mcp.json`
26
+ - раскладка assets (skills/agents/commands/hooks/rules/workflows/docs/dotfiles/schemas)
27
+
28
+ ## Карта: control_v0 -> поверхность
29
+
30
+ | control_v0 поле | Выход/эффект |
31
+ | --- | --- |
32
+ | `claude_md.inject_control_entry` + `marker` | блок MOVA в `CLAUDE.md` |
33
+ | `settings.*` | `.claude/settings.json` |
34
+ | `mcp.*` | `.mcp.json` |
35
+ | `policy.hooks.*` | hooks в `.claude/settings.json` |
36
+ | `policy.permissions.*` | permissions в `.claude/settings.json` |
37
+ | `policy.plugins.*` | plugins в `.claude/settings.json` |
38
+ | `lsp.*` | `.claude/settings.json` + опциональный lsp-файл |
39
+ | `assets.skills` | `.claude/skills/**` |
40
+ | `assets.agents` | `.claude/agents/**` |
41
+ | `assets.commands` | `.claude/commands/**` |
42
+ | `assets.rules` | `.claude/rules/**` |
43
+ | `assets.hooks` | `.claude/hooks/**` |
44
+ | `assets.docs` | `.claude/settings.md`, `.claude/skills/README.md` |
45
+ | `assets.dotfiles` | `.claude/.gitignore` |
46
+ | `assets.schemas` | `.claude/hooks/*.schema.json` |
47
+ | `assets.workflows` | `.github/workflows/**` |
48
+ | `observability.*` | hooks в `.claude/settings.json` + артефакты в `.mova/episodes/**` |
49
+
50
+ ## Что реально блокирует, а что наблюдает
51
+
52
+ - Блокировка: `hooks.PreToolUse` с явным `block: true` и exit 2.
53
+ - Наблюдение: `PostToolUse`, `UserPromptSubmit`, `Stop` — без hard-stop, по умолчанию `report_only`.
54
+ - Наблюдаемость MOVA пишет события в `.mova/episodes/**` и не блокирует работу.
55
+
56
+ ## Managed vs unmanaged LSP
57
+
58
+ - `lsp.managed: true` — `control apply` пишет файл по `lsp.config_path`.
59
+ - `lsp.managed: false` — файл считается внешним и не трогается.
60
+
61
+ ## Drift и как чинить
62
+
63
+ Если `control check` сообщает drift:
64
+
65
+ 1) `control prefill` (если нужно переснять)
66
+ 2) Правка `mova/control_v0.json`
67
+ 3) `control apply --mode apply`
68
+ 4) `control check` до зелёного статуса
69
+
70
+ ## Исключения поверхности
71
+
72
+ Если есть сознательно неуправляемые файлы, они перечисляются в `control_surface_exclusions_v0.json` с причиной.
73
+
74
+ ## Как включать/выключать строгость
75
+
76
+ - `policy.mode` по умолчанию `report_only`.
77
+ - Для жёсткого режима включайте блокирующие PreToolUse и задавайте строгую политику в `permissions` и `hooks`.
78
+ - Детали CI/strict поведения — в `docs/OPERATOR_GUIDE_v0.md`.
@@ -12,6 +12,15 @@
12
12
  - `.claude/output-styles/`
13
13
  - `.claude/hooks/`
14
14
  - `.mcp.json`
15
+ - `mova/control_v0.json` (единый источник правды для rebuild/import)
16
+
17
+ ## Единый контрольный файл
18
+
19
+ `mova/control_v0.json` — источник истины для rebuild/import. Он синхронизирует:
20
+
21
+ - блок MOVA в `CLAUDE.md` по маркеру
22
+ - `.claude/settings.json`
23
+ - `.mcp.json`
15
24
 
16
25
  ## Как MOVA слой добавляет наблюдаемость
17
26
 
@@ -41,3 +50,5 @@
41
50
  - `0` — успех или preview‑план построен.
42
51
  - `1` — неожиданный runtime‑сбой.
43
52
  - `2` — автоматизация остановлена политикой контроля (strict/CI).
53
+ - `3` — control check обнаружил drift между control и проектом.
54
+ - `4` — control check обнаружил отсутствующие обязательные файлы.