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.
- package/README.md +68 -4
- package/control_surface_exclusions_v0.json +8 -0
- package/dist/anthropic_profile_v0.d.ts +1 -1
- package/dist/anthropic_profile_v0.js +1 -0
- package/dist/cli.js +206 -23
- package/dist/control_apply_v0.d.ts +5 -1
- package/dist/control_apply_v0.js +125 -8
- package/dist/control_check_v0.d.ts +1 -0
- package/dist/control_check_v0.js +194 -23
- package/dist/control_prefill_v0.js +128 -9
- package/dist/control_surface_coverage_v0.d.ts +22 -0
- package/dist/control_surface_coverage_v0.js +128 -0
- package/dist/control_v0.d.ts +149 -0
- package/dist/control_v0.js +360 -0
- package/dist/control_v0_schema.d.ts +6 -0
- package/dist/control_v0_schema.js +19 -0
- package/dist/init_v0.d.ts +6 -1
- package/dist/init_v0.js +41 -1
- package/dist/observability_writer_v0.d.ts +1 -0
- package/dist/observability_writer_v0.js +157 -0
- package/dist/observe_v0.d.ts +8 -0
- package/dist/observe_v0.js +57 -0
- package/dist/presets_v0.d.ts +11 -0
- package/dist/presets_v0.js +49 -0
- package/dist/redaction.js +5 -1
- package/dist/run_import.js +111 -26
- package/docs/CLAUDE_CONTROL_SURFACE_MAP_v0.md +78 -0
- package/docs/OPERATOR_GUIDE_v0.md +11 -0
- package/fixtures/pos/basic/mova/control_v0.json +93 -0
- package/fixtures/pos/claude_code_demo_full/.claude/agents/code-reviewer.md +13 -0
- package/fixtures/pos/claude_code_demo_full/.claude/agents/github-workflow.md +10 -0
- package/fixtures/pos/claude_code_demo_full/.claude/commands/code-quality.md +8 -0
- package/fixtures/pos/claude_code_demo_full/.claude/commands/docs-sync.md +8 -0
- package/fixtures/pos/claude_code_demo_full/.claude/commands/onboard.md +8 -0
- package/fixtures/pos/claude_code_demo_full/.claude/commands/pr-review.md +10 -0
- package/fixtures/pos/claude_code_demo_full/.claude/commands/pr-summary.md +8 -0
- package/fixtures/pos/claude_code_demo_full/.claude/commands/ticket.md +10 -0
- package/fixtures/pos/claude_code_demo_full/.claude/hooks/skill-eval.js +13 -0
- package/fixtures/pos/claude_code_demo_full/.claude/hooks/skill-eval.sh +15 -0
- package/fixtures/pos/claude_code_demo_full/.claude/hooks/skill-rules.json +21 -0
- package/fixtures/pos/claude_code_demo_full/.claude/hooks/skill-rules.schema.json +24 -0
- package/fixtures/pos/claude_code_demo_full/.claude/rules/code-style.md +5 -0
- package/fixtures/pos/claude_code_demo_full/.claude/rules/security.md +5 -0
- package/fixtures/pos/claude_code_demo_full/.claude/settings.json +102 -0
- package/fixtures/pos/claude_code_demo_full/.claude/settings.md +6 -0
- package/fixtures/pos/claude_code_demo_full/.claude/skills/core-components/SKILL.md +9 -0
- package/fixtures/pos/claude_code_demo_full/.claude/skills/formik-patterns/SKILL.md +9 -0
- package/fixtures/pos/claude_code_demo_full/.claude/skills/graphql-schema/SKILL.md +10 -0
- package/fixtures/pos/claude_code_demo_full/.claude/skills/react-ui-patterns/SKILL.md +10 -0
- package/fixtures/pos/claude_code_demo_full/.claude/skills/systematic-debugging/SKILL.md +9 -0
- package/fixtures/pos/claude_code_demo_full/.claude/skills/testing-patterns/SKILL.md +10 -0
- package/fixtures/pos/claude_code_demo_full/.github/workflows/pr-claude-code-review.yml +14 -0
- package/fixtures/pos/claude_code_demo_full/.github/workflows/scheduled-claude-code-dependency-audit.yml +14 -0
- package/fixtures/pos/claude_code_demo_full/.github/workflows/scheduled-claude-code-docs-sync.yml +14 -0
- package/fixtures/pos/claude_code_demo_full/.github/workflows/scheduled-claude-code-quality.yml +14 -0
- package/fixtures/pos/claude_code_demo_full/.mcp.json +44 -0
- package/fixtures/pos/claude_code_demo_full/CLAUDE.md +28 -0
- package/fixtures/pos/control_basic_project/mova/control_v0.json +93 -0
- package/fixtures/pos/observability_basic/CLAUDE.md +3 -0
- package/{.tmp_test_zip/out1 → fixtures/pos/preset_safe_observable_v0}/.mcp.json +3 -3
- package/fixtures/pos/preset_safe_observable_v0/CLAUDE.md +3 -0
- package/package.json +2 -1
- package/presets/safe_observable_v0/assets/.claude/agents/code-reviewer.md +9 -0
- package/presets/safe_observable_v0/assets/.claude/commands/finish.md +9 -0
- package/presets/safe_observable_v0/assets/.claude/commands/start.md +9 -0
- package/presets/safe_observable_v0/assets/.claude/hooks/skill-eval.js +15 -0
- package/presets/safe_observable_v0/assets/.claude/hooks/skill-eval.sh +17 -0
- package/presets/safe_observable_v0/assets/.claude/hooks/skill-rules.json +26 -0
- package/presets/safe_observable_v0/assets/.claude/rules/code-style.md +6 -0
- package/presets/safe_observable_v0/assets/.claude/rules/security.md +6 -0
- package/presets/safe_observable_v0/assets/.claude/skills/git-workflow/SKILL.md +9 -0
- package/presets/safe_observable_v0/assets/.claude/skills/security-basics/SKILL.md +9 -0
- package/presets/safe_observable_v0/assets/.claude/skills/systematic-debugging/SKILL.md +9 -0
- package/presets/safe_observable_v0/assets/.claude/skills/testing-patterns/SKILL.md +9 -0
- package/presets/safe_observable_v0/control_v0.json +214 -0
- package/schemas/mova.control_v0.schema.json +252 -0
- package/src/anthropic_profile_v0.ts +1 -0
- package/src/cli.ts +194 -23
- package/src/control_apply_v0.ts +131 -8
- package/src/control_check_v0.ts +203 -23
- package/src/control_prefill_v0.ts +136 -8
- package/src/control_surface_coverage_v0.ts +164 -0
- package/src/control_v0.ts +808 -0
- package/src/control_v0_schema.ts +26 -0
- package/src/init_v0.ts +48 -1
- package/src/observability_writer_v0.ts +157 -0
- package/src/observe_v0.ts +64 -0
- package/src/presets_v0.ts +55 -0
- package/src/redaction.ts +6 -1
- package/src/run_import.ts +132 -26
- package/test/control_demo_full_roundtrip.test.js +92 -0
- package/test/control_surface_coverage_v0.test.js +36 -0
- package/test/control_v0_schema_validation.test.js +69 -0
- package/test/init_v0.test.js +9 -0
- package/test/observability_writer_v0.test.js +59 -0
- package/test/preset_safe_observable_v0.test.js +55 -0
- package/test/profile_v0_output.test.js +1 -0
- package/test/scaffold_v0_output.test.js +1 -0
- package/tools/control_surface_coverage_v0.mjs +27 -0
- package/tools/smoke_v0.mjs +33 -0
- package/.tmp_test_control_apply/proj/.claude/agents/example_agent.md +0 -3
- package/.tmp_test_control_apply/proj/.claude/commands/example_command.md +0 -3
- package/.tmp_test_control_apply/proj/.claude/hooks/example_hook.sh +0 -2
- package/.tmp_test_control_apply/proj/.claude/output-styles/example_style.md +0 -3
- package/.tmp_test_control_apply/proj/.claude/settings.json +0 -30
- package/.tmp_test_control_apply/proj/.claude/settings.local.example.json +0 -3
- package/.tmp_test_control_apply/proj/.mcp.json +0 -3
- package/.tmp_test_control_apply/proj/CLAUDE.md +0 -13
- package/.tmp_test_control_apply/proj/MOVA.md +0 -3
- package/.tmp_test_control_check/proj/.mcp.json +0 -1
- package/.tmp_test_control_check/proj/CLAUDE.md +0 -1
- package/.tmp_test_control_prefill/out1/claude_control_profile_v0.json +0 -114
- package/.tmp_test_control_prefill/out1/prefill_report_v0.json +0 -13
- package/.tmp_test_control_prefill/out2/claude_control_profile_v0.json +0 -114
- package/.tmp_test_control_prefill/out2/prefill_report_v0.json +0 -13
- package/.tmp_test_overlay/proj/.claude/skills/a.md +0 -1
- package/.tmp_test_overlay/proj/.mcp.json +0 -1
- package/.tmp_test_overlay/proj/CLAUDE.md +0 -1
- package/.tmp_test_profile/proj/.claude/skills/a.md +0 -1
- package/.tmp_test_profile/proj/.mcp.json +0 -1
- package/.tmp_test_profile/proj/CLAUDE.md +0 -1
- package/.tmp_test_scaffold_apply/proj/.claude/agents/example_agent.md +0 -3
- package/.tmp_test_scaffold_apply/proj/.claude/commands/example_command.md +0 -3
- package/.tmp_test_scaffold_apply/proj/.claude/hooks/example_hook.sh +0 -2
- package/.tmp_test_scaffold_apply/proj/.claude/output-styles/example_style.md +0 -3
- package/.tmp_test_scaffold_apply/proj/.claude/settings.json +0 -30
- package/.tmp_test_scaffold_apply/proj/.claude/settings.local.example.json +0 -3
- package/.tmp_test_scaffold_apply/proj/.mcp.json +0 -3
- package/.tmp_test_scaffold_apply/proj/CLAUDE.md +0 -13
- package/.tmp_test_scaffold_apply/proj/MOVA.md +0 -3
- package/.tmp_test_strict/mova/claude_import/v0/VERSION.json +0 -10
- package/.tmp_test_strict/mova/claude_import/v0/episode_import_run.json +0 -20
- package/.tmp_test_strict/mova/claude_import/v0/import_manifest.json +0 -20
- package/.tmp_test_strict/mova/claude_import/v0/input_policy_report_v0.json +0 -32
- package/.tmp_test_zip/out1/.claude/agents/example_agent.md +0 -3
- package/.tmp_test_zip/out1/.claude/commands/example_command.md +0 -3
- package/.tmp_test_zip/out1/.claude/commands/mova_context.md +0 -4
- package/.tmp_test_zip/out1/.claude/commands/mova_lint.md +0 -4
- package/.tmp_test_zip/out1/.claude/commands/mova_proof.md +0 -6
- package/.tmp_test_zip/out1/.claude/hooks/example_hook.sh +0 -2
- package/.tmp_test_zip/out1/.claude/output-styles/example_style.md +0 -3
- package/.tmp_test_zip/out1/.claude/settings.json +0 -30
- package/.tmp_test_zip/out1/.claude/settings.local.example.json +0 -3
- package/.tmp_test_zip/out1/.claude/skills/a/SKILL.md +0 -1
- package/.tmp_test_zip/out1/.claude/skills/mova-control-v0/SKILL.md +0 -11
- package/.tmp_test_zip/out1/.claude/skills/mova-layer-v0/SKILL.md +0 -8
- package/.tmp_test_zip/out1/CLAUDE.md +0 -4
- package/.tmp_test_zip/out1/MOVA.md +0 -10
- package/.tmp_test_zip/out1/export.zip +0 -0
- package/.tmp_test_zip/out1/mova/claude_import/v0/VERSION.json +0 -10
- package/.tmp_test_zip/out1/mova/claude_import/v0/contracts/instruction_profile_v0.json +0 -8
- package/.tmp_test_zip/out1/mova/claude_import/v0/contracts/mcp_servers_v0.json +0 -4
- package/.tmp_test_zip/out1/mova/claude_import/v0/contracts/skills_catalog_v0.json +0 -11
- package/.tmp_test_zip/out1/mova/claude_import/v0/episode_import_run.json +0 -80
- package/.tmp_test_zip/out1/mova/claude_import/v0/export_manifest_v0.json +0 -32
- package/.tmp_test_zip/out1/mova/claude_import/v0/import_manifest.json +0 -33
- package/.tmp_test_zip/out1/mova/claude_import/v0/input_policy_report_v0.json +0 -38
- package/.tmp_test_zip/out1/mova/claude_import/v0/lint_report_v0.json +0 -6
- package/.tmp_test_zip/out1/mova/claude_import/v0/redaction_report.json +0 -4
- package/.tmp_test_zip/out2/.claude/agents/example_agent.md +0 -3
- package/.tmp_test_zip/out2/.claude/commands/example_command.md +0 -3
- package/.tmp_test_zip/out2/.claude/commands/mova_context.md +0 -4
- package/.tmp_test_zip/out2/.claude/commands/mova_lint.md +0 -4
- package/.tmp_test_zip/out2/.claude/commands/mova_proof.md +0 -6
- package/.tmp_test_zip/out2/.claude/hooks/example_hook.sh +0 -2
- package/.tmp_test_zip/out2/.claude/output-styles/example_style.md +0 -3
- package/.tmp_test_zip/out2/.claude/settings.json +0 -30
- package/.tmp_test_zip/out2/.claude/settings.local.example.json +0 -3
- package/.tmp_test_zip/out2/.claude/skills/a/SKILL.md +0 -1
- package/.tmp_test_zip/out2/.claude/skills/mova-control-v0/SKILL.md +0 -11
- package/.tmp_test_zip/out2/.claude/skills/mova-layer-v0/SKILL.md +0 -8
- package/.tmp_test_zip/out2/.mcp.json +0 -3
- package/.tmp_test_zip/out2/CLAUDE.md +0 -4
- package/.tmp_test_zip/out2/MOVA.md +0 -10
- package/.tmp_test_zip/out2/export.zip +0 -0
- package/.tmp_test_zip/out2/mova/claude_import/v0/VERSION.json +0 -10
- package/.tmp_test_zip/out2/mova/claude_import/v0/contracts/instruction_profile_v0.json +0 -8
- package/.tmp_test_zip/out2/mova/claude_import/v0/contracts/mcp_servers_v0.json +0 -4
- package/.tmp_test_zip/out2/mova/claude_import/v0/contracts/skills_catalog_v0.json +0 -11
- package/.tmp_test_zip/out2/mova/claude_import/v0/episode_import_run.json +0 -80
- package/.tmp_test_zip/out2/mova/claude_import/v0/export_manifest_v0.json +0 -32
- package/.tmp_test_zip/out2/mova/claude_import/v0/import_manifest.json +0 -33
- package/.tmp_test_zip/out2/mova/claude_import/v0/input_policy_report_v0.json +0 -38
- package/.tmp_test_zip/out2/mova/claude_import/v0/lint_report_v0.json +0 -6
- package/.tmp_test_zip/out2/mova/claude_import/v0/redaction_report.json +0 -4
- package/.tmp_test_zip/proj/.claude/skills/a.md +0 -1
- package/.tmp_test_zip/proj/.mcp.json +0 -1
- 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
|
}
|
package/dist/run_import.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
342
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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 обнаружил отсутствующие обязательные файлы.
|