intentdna 1.5.2 → 1.5.4
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/dist/cli/commands/sync.d.ts +23 -0
- package/dist/cli/commands/sync.js +76 -5
- package/dist/cli/commands/verify.d.ts +11 -0
- package/dist/cli/commands/verify.js +190 -0
- package/dist/cli/index.js +3 -0
- package/dist/compiler/compile.js +11 -0
- package/dist/governance/index.d.ts +7 -0
- package/dist/governance/index.js +7 -0
- package/dist/governance/types.d.ts +135 -0
- package/dist/governance/types.js +12 -0
- package/dist/hooks/cli.js +16 -2
- package/dist/hooks/enforce.d.ts +2 -0
- package/dist/hooks/enforce.js +71 -10
- package/dist/hooks/state.d.ts +18 -6
- package/dist/hooks/state.js +105 -34
- package/dist/mcp/index.d.ts +13 -0
- package/dist/mcp/index.js +40 -0
- package/dist/mcp/server.d.ts +39 -0
- package/dist/mcp/server.js +84 -0
- package/dist/mcp/tools-compile.d.ts +10 -0
- package/dist/mcp/tools-compile.js +214 -0
- package/dist/mcp/tools-enforce.d.ts +27 -0
- package/dist/mcp/tools-enforce.js +178 -0
- package/dist/mcp/tools-state.d.ts +11 -0
- package/dist/mcp/tools-state.js +160 -0
- package/dist/mcp/transport.d.ts +36 -0
- package/dist/mcp/transport.js +48 -0
- package/dist/schema/types.d.ts +16 -0
- package/package.json +3 -2
- package/spec/foundation-hardening.md +8 -8
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intent DNA — MCP Compile Tools (G3)
|
|
3
|
+
*
|
|
4
|
+
* Tools for compiling and managing DNA via MCP:
|
|
5
|
+
* - dna_compile: Compile DNA config to Constraint IR
|
|
6
|
+
* - dna_validate: Validate a DNA config file
|
|
7
|
+
* - dna_sync: Run full sync pipeline (compile + inject + hooks)
|
|
8
|
+
*/
|
|
9
|
+
import { resolve } from "node:path";
|
|
10
|
+
import { stat } from "node:fs/promises";
|
|
11
|
+
import { loadDNA, compileFromFiles } from "../compiler/index.js";
|
|
12
|
+
import { textResult, errorResult } from "./server.js";
|
|
13
|
+
async function fileExists(path) {
|
|
14
|
+
try {
|
|
15
|
+
await stat(path);
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** Auto-detect DNA config files in the project directory. */
|
|
23
|
+
async function detectConfigs(projectDir) {
|
|
24
|
+
const { readdir } = await import("node:fs/promises");
|
|
25
|
+
// Multi-config: .dna/configs/*.yaml
|
|
26
|
+
const configsDir = resolve(projectDir, ".dna", "configs");
|
|
27
|
+
try {
|
|
28
|
+
const files = await readdir(configsDir);
|
|
29
|
+
const yamls = files
|
|
30
|
+
.filter(f => f.endsWith(".yaml") || f.endsWith(".yml"))
|
|
31
|
+
.sort()
|
|
32
|
+
.map(f => resolve(configsDir, f));
|
|
33
|
+
if (yamls.length > 0)
|
|
34
|
+
return yamls;
|
|
35
|
+
}
|
|
36
|
+
catch { /* dir doesn't exist */ }
|
|
37
|
+
// Legacy single-config
|
|
38
|
+
const candidates = [
|
|
39
|
+
".dna/config.yaml", ".dna/config.yml", ".dna/config.json",
|
|
40
|
+
".dna.yaml", ".dna.yml", ".dna.json",
|
|
41
|
+
];
|
|
42
|
+
for (const name of candidates) {
|
|
43
|
+
const path = resolve(projectDir, name);
|
|
44
|
+
if (await fileExists(path))
|
|
45
|
+
return [path];
|
|
46
|
+
}
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
export function createCompileTools(projectDir) {
|
|
50
|
+
return [
|
|
51
|
+
// ── dna_validate ─────────────────────────────────────
|
|
52
|
+
{
|
|
53
|
+
name: "dna_validate",
|
|
54
|
+
description: "Validate a DNA config file for schema errors without compiling",
|
|
55
|
+
inputSchema: {
|
|
56
|
+
type: "object",
|
|
57
|
+
properties: {
|
|
58
|
+
config_path: { type: "string", description: "Path to DNA config (auto-detected if omitted)" },
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
handler: async (args) => {
|
|
62
|
+
const configPath = typeof args.config_path === "string"
|
|
63
|
+
? resolve(projectDir, args.config_path)
|
|
64
|
+
: undefined;
|
|
65
|
+
const files = configPath ? [configPath] : await detectConfigs(projectDir);
|
|
66
|
+
if (files.length === 0)
|
|
67
|
+
return errorResult("No DNA config files found.");
|
|
68
|
+
const results = [];
|
|
69
|
+
let allValid = true;
|
|
70
|
+
for (const file of files) {
|
|
71
|
+
try {
|
|
72
|
+
// loadDNA calls validateDNA internally — throws on invalid
|
|
73
|
+
await loadDNA(file);
|
|
74
|
+
results.push(`${file}: valid`);
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
allValid = false;
|
|
78
|
+
results.push(`${file}: INVALID — ${err instanceof Error ? err.message : String(err)}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
results.unshift(allValid ? "All configs valid." : "Validation errors found.");
|
|
82
|
+
return textResult(results.join("\n"));
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
// ── dna_compile ──────────────────────────────────────
|
|
86
|
+
{
|
|
87
|
+
name: "dna_compile",
|
|
88
|
+
description: "Compile DNA config(s) to Constraint IR and return a summary of the compiled constraints",
|
|
89
|
+
inputSchema: {
|
|
90
|
+
type: "object",
|
|
91
|
+
properties: {
|
|
92
|
+
config_path: { type: "string", description: "Path to DNA config (auto-detected if omitted)" },
|
|
93
|
+
context: { type: "string", description: "Active context name (e.g., 'work', 'personal')" },
|
|
94
|
+
role: { type: "string", description: "Active role name" },
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
handler: async (args) => {
|
|
98
|
+
const configPath = typeof args.config_path === "string"
|
|
99
|
+
? resolve(projectDir, args.config_path)
|
|
100
|
+
: undefined;
|
|
101
|
+
const files = configPath ? [configPath] : await detectConfigs(projectDir);
|
|
102
|
+
if (files.length === 0)
|
|
103
|
+
return errorResult("No DNA config files found.");
|
|
104
|
+
try {
|
|
105
|
+
const ir = await compileFromFiles(files, {
|
|
106
|
+
context: typeof args.context === "string" ? args.context : undefined,
|
|
107
|
+
role: typeof args.role === "string" ? args.role : undefined,
|
|
108
|
+
});
|
|
109
|
+
const lines = [
|
|
110
|
+
`Compiled from: ${ir.source_dna_ids.join(", ")}`,
|
|
111
|
+
` Directives: ${ir.prompt_directives.length}`,
|
|
112
|
+
` Gates: ${ir.pre_execution_gates.length}`,
|
|
113
|
+
` Filters: ${ir.tool_filters.length}`,
|
|
114
|
+
` Validators: ${ir.post_execution_validators.length}`,
|
|
115
|
+
];
|
|
116
|
+
if (ir.roles_scope_map?.length) {
|
|
117
|
+
lines.push(` Roles: ${ir.roles_scope_map.map(r => r.role_name).join(", ")}`);
|
|
118
|
+
}
|
|
119
|
+
if (ir.workflows_ir?.length) {
|
|
120
|
+
lines.push(` Workflows: ${ir.workflows_ir.map(w => w.workflow_name).join(", ")}`);
|
|
121
|
+
for (const wf of ir.workflows_ir) {
|
|
122
|
+
lines.push(` ${wf.workflow_name}: ${wf.active_roles.length} roles, ${wf.step_checkpoints.length} checkpoints`);
|
|
123
|
+
if (wf.step_enforce_rules?.length) {
|
|
124
|
+
lines.push(` Enforce rules: ${wf.step_enforce_rules.length}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return textResult(lines.join("\n"));
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
return errorResult(`Compilation failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
// ── dna_sync ─────────────────────────────────────────
|
|
136
|
+
{
|
|
137
|
+
name: "dna_sync",
|
|
138
|
+
description: "Run full DNA sync pipeline: compile + inject CLAUDE.md + generate agents/skills/hooks. Equivalent to CLI `dna sync`.",
|
|
139
|
+
inputSchema: {
|
|
140
|
+
type: "object",
|
|
141
|
+
properties: {
|
|
142
|
+
evolve: { type: "boolean", description: "Run evolution engine before compiling (default false)" },
|
|
143
|
+
context: { type: "string", description: "Active context name" },
|
|
144
|
+
role: { type: "string", description: "Active role name" },
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
handler: async (args) => {
|
|
148
|
+
// Import sync command dynamically to avoid circular deps
|
|
149
|
+
const { runSync, autoDetectConfigs: detectAll } = await import("../cli/commands/sync.js");
|
|
150
|
+
const files = await detectAll();
|
|
151
|
+
if (files.length === 0)
|
|
152
|
+
return errorResult("No DNA config files found.");
|
|
153
|
+
const exitCode = await runSync({
|
|
154
|
+
files,
|
|
155
|
+
target: "claude-md",
|
|
156
|
+
inject: resolve(projectDir, "CLAUDE.md"),
|
|
157
|
+
evolve: typeof args.evolve === "boolean" ? args.evolve : false,
|
|
158
|
+
context: typeof args.context === "string" ? args.context : undefined,
|
|
159
|
+
agentsDir: resolve(projectDir, ".claude", "agents"),
|
|
160
|
+
skillsDir: resolve(projectDir, ".claude", "skills"),
|
|
161
|
+
settingsPath: resolve(projectDir, ".claude", "settings.json"),
|
|
162
|
+
remove: false,
|
|
163
|
+
});
|
|
164
|
+
if (exitCode === 0) {
|
|
165
|
+
return textResult("Sync completed successfully.");
|
|
166
|
+
}
|
|
167
|
+
return errorResult(`Sync failed with exit code ${exitCode}.`);
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
// ── dna_generate ─────────────────────────────────────
|
|
171
|
+
{
|
|
172
|
+
name: "dna_generate",
|
|
173
|
+
description: "Generate a DNA config from a natural-language description. Lists matching templates or provides guidance for custom configs.",
|
|
174
|
+
inputSchema: {
|
|
175
|
+
type: "object",
|
|
176
|
+
properties: {
|
|
177
|
+
description: { type: "string", description: "Natural-language description of the desired DNA config" },
|
|
178
|
+
},
|
|
179
|
+
required: ["description"],
|
|
180
|
+
},
|
|
181
|
+
handler: async (args) => {
|
|
182
|
+
const description = String(args.description);
|
|
183
|
+
// List available templates
|
|
184
|
+
const { readdir } = await import("node:fs/promises");
|
|
185
|
+
const { resolve: resolvePath, dirname } = await import("node:path");
|
|
186
|
+
const { fileURLToPath } = await import("node:url");
|
|
187
|
+
let templates = [];
|
|
188
|
+
try {
|
|
189
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
190
|
+
const templatesDir = resolvePath(thisDir, "..", "templates");
|
|
191
|
+
const files = await readdir(templatesDir);
|
|
192
|
+
templates = files.filter(f => f.endsWith(".yaml")).map(f => f.replace(".yaml", ""));
|
|
193
|
+
}
|
|
194
|
+
catch { /* templates dir not found */ }
|
|
195
|
+
const lines = [
|
|
196
|
+
`Generate DNA for: "${description}"`,
|
|
197
|
+
"",
|
|
198
|
+
"Options:",
|
|
199
|
+
` 1. Use a template: dna init --template <name>`,
|
|
200
|
+
` 2. Generate prompt: dna generate "${description}" --output .dna/prompt.md`,
|
|
201
|
+
` 3. Create manually: write .dna/config.yaml following the DNA schema`,
|
|
202
|
+
"",
|
|
203
|
+
];
|
|
204
|
+
if (templates.length > 0) {
|
|
205
|
+
lines.push(`Available templates (${templates.length}):`);
|
|
206
|
+
for (const t of templates) {
|
|
207
|
+
lines.push(` - ${t}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return textResult(lines.join("\n"));
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
];
|
|
214
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intent DNA — MCP Enforce Tools (U2)
|
|
3
|
+
*
|
|
4
|
+
* Provides enforcement via MCP server with in-memory IR cache.
|
|
5
|
+
* Zero cold-start: IR is loaded once and cached with mtime-based invalidation.
|
|
6
|
+
*
|
|
7
|
+
* Tools:
|
|
8
|
+
* - dna_enforce: Run enforcement for a hook event (PreToolUse, PostToolUse, etc.)
|
|
9
|
+
*/
|
|
10
|
+
import type { ConstraintIR, RoleDef } from "../schema/types.js";
|
|
11
|
+
import type { ToolDef } from "./server.js";
|
|
12
|
+
/** In-memory IR cache with mtime-based invalidation. */
|
|
13
|
+
export declare class IRCache {
|
|
14
|
+
private ir;
|
|
15
|
+
private roles;
|
|
16
|
+
private lastMtime;
|
|
17
|
+
private irPath;
|
|
18
|
+
constructor(projectDir: string);
|
|
19
|
+
/** Get cached IR, reloading from disk only if file changed. */
|
|
20
|
+
get(): Promise<{
|
|
21
|
+
ir: ConstraintIR;
|
|
22
|
+
roles?: Record<string, RoleDef>;
|
|
23
|
+
} | null>;
|
|
24
|
+
/** Invalidate the cache (e.g., after dna sync). */
|
|
25
|
+
invalidate(): void;
|
|
26
|
+
}
|
|
27
|
+
export declare function createEnforceTools(projectDir: string): ToolDef[];
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intent DNA — MCP Enforce Tools (U2)
|
|
3
|
+
*
|
|
4
|
+
* Provides enforcement via MCP server with in-memory IR cache.
|
|
5
|
+
* Zero cold-start: IR is loaded once and cached with mtime-based invalidation.
|
|
6
|
+
*
|
|
7
|
+
* Tools:
|
|
8
|
+
* - dna_enforce: Run enforcement for a hook event (PreToolUse, PostToolUse, etc.)
|
|
9
|
+
*/
|
|
10
|
+
import { resolve } from "node:path";
|
|
11
|
+
import { stat, readFile } from "node:fs/promises";
|
|
12
|
+
import { enforcePreToolUse, enforcePostToolUse, enforceUserPromptSubmit, enforceSubagentStop, enforcePreCompact, enforceNotification, enforceSessionStart, } from "../hooks/enforce.js";
|
|
13
|
+
import { textResult, errorResult } from "./server.js";
|
|
14
|
+
// ── IR Cache ────────────────────────────────────────────────
|
|
15
|
+
/** In-memory IR cache with mtime-based invalidation. */
|
|
16
|
+
export class IRCache {
|
|
17
|
+
ir = null;
|
|
18
|
+
roles = undefined;
|
|
19
|
+
lastMtime = 0;
|
|
20
|
+
irPath;
|
|
21
|
+
constructor(projectDir) {
|
|
22
|
+
this.irPath = resolve(projectDir, ".dna", "compiled", "ir.json");
|
|
23
|
+
}
|
|
24
|
+
/** Get cached IR, reloading from disk only if file changed. */
|
|
25
|
+
async get() {
|
|
26
|
+
try {
|
|
27
|
+
const fileStat = await stat(this.irPath);
|
|
28
|
+
const mtime = fileStat.mtimeMs;
|
|
29
|
+
const cachedIR = this.ir;
|
|
30
|
+
if (cachedIR && mtime === this.lastMtime) {
|
|
31
|
+
return { ir: cachedIR, roles: this.roles };
|
|
32
|
+
}
|
|
33
|
+
// File changed or first load — read from disk
|
|
34
|
+
const raw = await readFile(this.irPath, "utf-8");
|
|
35
|
+
const data = JSON.parse(raw);
|
|
36
|
+
if (data.ir_version && data.ir) {
|
|
37
|
+
const ir = data.ir;
|
|
38
|
+
this.ir = ir;
|
|
39
|
+
this.roles = data.roles;
|
|
40
|
+
this.lastMtime = mtime;
|
|
41
|
+
return { ir, roles: this.roles };
|
|
42
|
+
}
|
|
43
|
+
// Direct ConstraintIR format (backward compat)
|
|
44
|
+
if (data.compiled_at && data.source_dna_ids) {
|
|
45
|
+
const ir = data;
|
|
46
|
+
this.ir = ir;
|
|
47
|
+
this.roles = undefined;
|
|
48
|
+
this.lastMtime = mtime;
|
|
49
|
+
return { ir };
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** Invalidate the cache (e.g., after dna sync). */
|
|
58
|
+
invalidate() {
|
|
59
|
+
this.ir = null;
|
|
60
|
+
this.roles = undefined;
|
|
61
|
+
this.lastMtime = 0;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const VALID_EVENTS = new Set([
|
|
65
|
+
"PreToolUse", "PostToolUse", "UserPromptSubmit",
|
|
66
|
+
"SubagentStop", "PreCompact", "Notification", "SessionStart",
|
|
67
|
+
]);
|
|
68
|
+
function dispatchEnforce(event, ir, input, roles) {
|
|
69
|
+
switch (event) {
|
|
70
|
+
case "PreToolUse":
|
|
71
|
+
return enforcePreToolUse(ir, {
|
|
72
|
+
tool_name: String(input.tool_name ?? ""),
|
|
73
|
+
tool_input: (input.tool_input ?? {}),
|
|
74
|
+
agent_type: typeof input.agent_type === "string" ? input.agent_type : undefined,
|
|
75
|
+
cwd: typeof input.cwd === "string" ? input.cwd : undefined,
|
|
76
|
+
}, undefined, roles);
|
|
77
|
+
case "PostToolUse":
|
|
78
|
+
return enforcePostToolUse(ir, {
|
|
79
|
+
tool_name: String(input.tool_name ?? ""),
|
|
80
|
+
tool_input: (input.tool_input ?? {}),
|
|
81
|
+
tool_output: typeof input.tool_output === "string" ? input.tool_output : undefined,
|
|
82
|
+
agent_type: typeof input.agent_type === "string" ? input.agent_type : undefined,
|
|
83
|
+
});
|
|
84
|
+
case "UserPromptSubmit":
|
|
85
|
+
return enforceUserPromptSubmit(ir, {
|
|
86
|
+
prompt: typeof input.prompt === "string" ? input.prompt : undefined,
|
|
87
|
+
});
|
|
88
|
+
case "SubagentStop":
|
|
89
|
+
return enforceSubagentStop(ir, {
|
|
90
|
+
agent_name: typeof input.agent_name === "string" ? input.agent_name : undefined,
|
|
91
|
+
agent_type: typeof input.agent_type === "string" ? input.agent_type : undefined,
|
|
92
|
+
});
|
|
93
|
+
case "PreCompact":
|
|
94
|
+
return enforcePreCompact(ir);
|
|
95
|
+
case "Notification":
|
|
96
|
+
return enforceNotification(ir, {
|
|
97
|
+
title: typeof input.title === "string" ? input.title : undefined,
|
|
98
|
+
message: typeof input.message === "string" ? input.message : undefined,
|
|
99
|
+
});
|
|
100
|
+
case "SessionStart":
|
|
101
|
+
return enforceSessionStart(ir, {
|
|
102
|
+
cwd: typeof input.cwd === "string" ? input.cwd : undefined,
|
|
103
|
+
session_id: typeof input.session_id === "string" ? input.session_id : undefined,
|
|
104
|
+
});
|
|
105
|
+
default:
|
|
106
|
+
return { continue: true, suppressOutput: true };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// ── Tool Registration ───────────────────────────────────────
|
|
110
|
+
export function createEnforceTools(projectDir) {
|
|
111
|
+
const cache = new IRCache(projectDir);
|
|
112
|
+
return [
|
|
113
|
+
{
|
|
114
|
+
name: "dna_enforce",
|
|
115
|
+
description: "Run DNA enforcement for a hook event. Uses in-memory cached IR for zero cold-start. " +
|
|
116
|
+
"Returns the same result as the dna-hook binary but without spawning a new process.",
|
|
117
|
+
inputSchema: {
|
|
118
|
+
type: "object",
|
|
119
|
+
properties: {
|
|
120
|
+
event: {
|
|
121
|
+
type: "string",
|
|
122
|
+
description: "Hook event type",
|
|
123
|
+
enum: [...VALID_EVENTS],
|
|
124
|
+
},
|
|
125
|
+
input: {
|
|
126
|
+
type: "object",
|
|
127
|
+
description: "Hook input (same fields as Claude Code passes to hooks via stdin)",
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
required: ["event", "input"],
|
|
131
|
+
},
|
|
132
|
+
handler: async (args) => {
|
|
133
|
+
const event = String(args.event);
|
|
134
|
+
if (!VALID_EVENTS.has(event)) {
|
|
135
|
+
return errorResult(`Invalid event: ${event}. Valid: ${[...VALID_EVENTS].join(", ")}`);
|
|
136
|
+
}
|
|
137
|
+
const cached = await cache.get();
|
|
138
|
+
if (!cached) {
|
|
139
|
+
return errorResult("No compiled IR found. Run `dna sync` to compile your DNA config.");
|
|
140
|
+
}
|
|
141
|
+
const input = (args.input ?? {});
|
|
142
|
+
const result = dispatchEnforce(event, cached.ir, input, cached.roles);
|
|
143
|
+
// Format as human-readable text
|
|
144
|
+
const lines = [];
|
|
145
|
+
lines.push(`Decision: ${result.continue ? "allow" : "BLOCK"}`);
|
|
146
|
+
if (result.reason)
|
|
147
|
+
lines.push(`Reason: ${result.reason}`);
|
|
148
|
+
if (result.hookSpecificOutput?.additionalContext) {
|
|
149
|
+
lines.push(`Context: ${result.hookSpecificOutput.additionalContext}`);
|
|
150
|
+
}
|
|
151
|
+
return textResult(lines.join("\n"));
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: "dna_enforce_cache_status",
|
|
156
|
+
description: "Check IR cache status — whether IR is loaded, file path, and last load time",
|
|
157
|
+
inputSchema: { type: "object", properties: {} },
|
|
158
|
+
handler: async () => {
|
|
159
|
+
const cached = await cache.get();
|
|
160
|
+
if (!cached) {
|
|
161
|
+
return textResult("IR cache: empty (no compiled IR found at .dna/compiled/ir.json)");
|
|
162
|
+
}
|
|
163
|
+
const lines = [
|
|
164
|
+
"IR cache: loaded",
|
|
165
|
+
` Templates: ${cached.ir.source_dna_ids.join(", ")}`,
|
|
166
|
+
` Compiled: ${cached.ir.compiled_at}`,
|
|
167
|
+
` Directives: ${cached.ir.prompt_directives.length}`,
|
|
168
|
+
` Gates: ${cached.ir.pre_execution_gates.length}`,
|
|
169
|
+
` Filters: ${cached.ir.tool_filters.length}`,
|
|
170
|
+
];
|
|
171
|
+
if (cached.roles) {
|
|
172
|
+
lines.push(` Roles: ${Object.keys(cached.roles).join(", ")}`);
|
|
173
|
+
}
|
|
174
|
+
return textResult(lines.join("\n"));
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
];
|
|
178
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intent DNA — MCP State Management Tools (G2)
|
|
3
|
+
*
|
|
4
|
+
* Tools for reading/writing DNA governance state via MCP:
|
|
5
|
+
* - dna_status: Overview of DNA state
|
|
6
|
+
* - dna_workflow_read: Read current workflow state
|
|
7
|
+
* - dna_workflow_write: Update workflow state
|
|
8
|
+
* - dna_trace_query: Query trace data with filters
|
|
9
|
+
*/
|
|
10
|
+
import type { ToolDef } from "./server.js";
|
|
11
|
+
export declare function createStateTools(projectDir: string): ToolDef[];
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intent DNA — MCP State Management Tools (G2)
|
|
3
|
+
*
|
|
4
|
+
* Tools for reading/writing DNA governance state via MCP:
|
|
5
|
+
* - dna_status: Overview of DNA state
|
|
6
|
+
* - dna_workflow_read: Read current workflow state
|
|
7
|
+
* - dna_workflow_write: Update workflow state
|
|
8
|
+
* - dna_trace_query: Query trace data with filters
|
|
9
|
+
*/
|
|
10
|
+
import { resolve } from "node:path";
|
|
11
|
+
import { readWorkflowState, writeWorkflowState, readTraces, listSessions } from "../hooks/state.js";
|
|
12
|
+
import { loadCompiledIR } from "../runtime/plugin-adapter.js";
|
|
13
|
+
import { textResult } from "./server.js";
|
|
14
|
+
export function createStateTools(projectDir) {
|
|
15
|
+
return [
|
|
16
|
+
// ── dna_status ────────────────────────────────────────
|
|
17
|
+
{
|
|
18
|
+
name: "dna_status",
|
|
19
|
+
description: "Overview of DNA governance state: compiled IR info, active workflow, session list, and recent enforcement stats",
|
|
20
|
+
inputSchema: { type: "object", properties: {} },
|
|
21
|
+
handler: async () => {
|
|
22
|
+
const lines = [];
|
|
23
|
+
// IR info
|
|
24
|
+
const irPath = resolve(projectDir, ".dna", "compiled", "ir.json");
|
|
25
|
+
const compiled = await loadCompiledIR(irPath);
|
|
26
|
+
if (compiled) {
|
|
27
|
+
lines.push(`IR: v${compiled.ir_version}, compiled ${compiled.compiled_at}`);
|
|
28
|
+
lines.push(` Templates: ${compiled.source_dna_ids.join(", ")}`);
|
|
29
|
+
const ir = compiled.ir;
|
|
30
|
+
lines.push(` Directives: ${ir.prompt_directives.length}, Gates: ${ir.pre_execution_gates.length}, Filters: ${ir.tool_filters.length}`);
|
|
31
|
+
if (ir.roles_scope_map?.length) {
|
|
32
|
+
lines.push(` Roles: ${ir.roles_scope_map.map(r => r.role_name).join(", ")}`);
|
|
33
|
+
}
|
|
34
|
+
if (ir.workflows_ir?.length) {
|
|
35
|
+
lines.push(` Workflows: ${ir.workflows_ir.map(w => w.workflow_name).join(", ")}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
lines.push("IR: not found (run `dna sync` to compile)");
|
|
40
|
+
}
|
|
41
|
+
// Active workflow
|
|
42
|
+
const wf = await readWorkflowState(projectDir);
|
|
43
|
+
if (wf) {
|
|
44
|
+
lines.push(`\nWorkflow: ${wf.workflow} (active)`);
|
|
45
|
+
lines.push(` Step: ${wf.current_step}, Role: ${wf.current_role}, Iteration: ${wf.iteration}`);
|
|
46
|
+
lines.push(` Started: ${wf.started_at}`);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
lines.push("\nWorkflow: none active");
|
|
50
|
+
}
|
|
51
|
+
// Sessions
|
|
52
|
+
const sessions = await listSessions(projectDir);
|
|
53
|
+
if (sessions.length > 0) {
|
|
54
|
+
const preview = sessions.slice(0, 5).join(", ");
|
|
55
|
+
lines.push(`\nSessions: ${sessions.length} (${preview}${sessions.length > 5 ? "..." : ""})`);
|
|
56
|
+
}
|
|
57
|
+
// Recent trace stats
|
|
58
|
+
const traces = await readTraces(projectDir, 1);
|
|
59
|
+
if (traces.length > 0) {
|
|
60
|
+
const blocks = traces.filter(t => t.decision === "block").length;
|
|
61
|
+
const warns = traces.filter(t => t.decision === "warn").length;
|
|
62
|
+
lines.push(`\nTrace (24h): ${traces.length} events, ${blocks} blocks, ${warns} warns`);
|
|
63
|
+
}
|
|
64
|
+
return textResult(lines.join("\n"));
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
// ── dna_workflow_read ─────────────────────────────────
|
|
68
|
+
{
|
|
69
|
+
name: "dna_workflow_read",
|
|
70
|
+
description: "Read current workflow state (step, role, iteration, artifacts)",
|
|
71
|
+
inputSchema: {
|
|
72
|
+
type: "object",
|
|
73
|
+
properties: {
|
|
74
|
+
session_id: { type: "string", description: "Session ID (reads root state if omitted)" },
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
handler: async (args) => {
|
|
78
|
+
const sessionId = typeof args.session_id === "string" ? args.session_id : undefined;
|
|
79
|
+
const wf = await readWorkflowState(projectDir, sessionId, 0);
|
|
80
|
+
if (!wf)
|
|
81
|
+
return textResult("No active workflow state found.");
|
|
82
|
+
return textResult(JSON.stringify(wf, null, 2));
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
// ── dna_workflow_write ────────────────────────────────
|
|
86
|
+
{
|
|
87
|
+
name: "dna_workflow_write",
|
|
88
|
+
description: "Update workflow state (set current step, role, iteration, or deactivate)",
|
|
89
|
+
inputSchema: {
|
|
90
|
+
type: "object",
|
|
91
|
+
properties: {
|
|
92
|
+
workflow: { type: "string", description: "Workflow name" },
|
|
93
|
+
current_step: { type: "string", description: "Current step ID" },
|
|
94
|
+
current_role: { type: "string", description: "Current role name" },
|
|
95
|
+
iteration: { type: "number", description: "Current iteration number" },
|
|
96
|
+
active: { type: "boolean", description: "Whether workflow is active (false to deactivate)" },
|
|
97
|
+
session_id: { type: "string", description: "Session ID for isolation" },
|
|
98
|
+
},
|
|
99
|
+
required: ["workflow", "current_step", "current_role"],
|
|
100
|
+
},
|
|
101
|
+
handler: async (args) => {
|
|
102
|
+
const sessionId = typeof args.session_id === "string" ? args.session_id : undefined;
|
|
103
|
+
const existing = await readWorkflowState(projectDir, sessionId, 0);
|
|
104
|
+
const state = {
|
|
105
|
+
active: typeof args.active === "boolean" ? args.active : true,
|
|
106
|
+
workflow: String(args.workflow),
|
|
107
|
+
current_step: String(args.current_step),
|
|
108
|
+
current_role: String(args.current_role),
|
|
109
|
+
iteration: typeof args.iteration === "number" ? args.iteration : (existing?.iteration ?? 1),
|
|
110
|
+
session_id: sessionId ?? existing?.session_id ?? "",
|
|
111
|
+
started_at: existing?.started_at ?? new Date().toISOString(),
|
|
112
|
+
completed_artifacts: existing?.completed_artifacts,
|
|
113
|
+
};
|
|
114
|
+
await writeWorkflowState(projectDir, state, sessionId);
|
|
115
|
+
return textResult(`Workflow state updated: ${state.workflow} step=${state.current_step} role=${state.current_role} iteration=${state.iteration}`);
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
// ── dna_trace_query ──────────────────────────────────
|
|
119
|
+
{
|
|
120
|
+
name: "dna_trace_query",
|
|
121
|
+
description: "Query enforcement trace data with filters (blocks, warns, by tool, by path)",
|
|
122
|
+
inputSchema: {
|
|
123
|
+
type: "object",
|
|
124
|
+
properties: {
|
|
125
|
+
days: { type: "number", description: "Days to look back (default 1)" },
|
|
126
|
+
session_id: { type: "string", description: "Filter by session ID" },
|
|
127
|
+
decision: { type: "string", enum: ["block", "warn", "allow"], description: "Filter by decision type" },
|
|
128
|
+
tool_name: { type: "string", description: "Filter by tool name" },
|
|
129
|
+
limit: { type: "number", description: "Max entries to return (default 50)" },
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
handler: async (args) => {
|
|
133
|
+
const days = typeof args.days === "number" ? args.days : 1;
|
|
134
|
+
const sessionId = typeof args.session_id === "string" ? args.session_id : undefined;
|
|
135
|
+
const decision = typeof args.decision === "string" ? args.decision : undefined;
|
|
136
|
+
const toolName = typeof args.tool_name === "string" ? args.tool_name : undefined;
|
|
137
|
+
const limit = typeof args.limit === "number" ? args.limit : 50;
|
|
138
|
+
let traces = await readTraces(projectDir, days, sessionId);
|
|
139
|
+
if (decision)
|
|
140
|
+
traces = traces.filter(t => t.decision === decision);
|
|
141
|
+
if (toolName)
|
|
142
|
+
traces = traces.filter(t => t.tool_name === toolName);
|
|
143
|
+
const total = traces.length;
|
|
144
|
+
traces = traces.slice(-limit);
|
|
145
|
+
const lines = [`Trace: ${total} matching entries (showing last ${traces.length})`];
|
|
146
|
+
for (const t of traces) {
|
|
147
|
+
const parts = [t.timestamp.slice(11, 19), t.event, t.decision];
|
|
148
|
+
if (t.tool_name)
|
|
149
|
+
parts.push(t.tool_name);
|
|
150
|
+
if (t.target_path)
|
|
151
|
+
parts.push(t.target_path);
|
|
152
|
+
if (t.reason)
|
|
153
|
+
parts.push(`"${t.reason.slice(0, 80)}"`);
|
|
154
|
+
lines.push(" " + parts.join(" | "));
|
|
155
|
+
}
|
|
156
|
+
return textResult(lines.join("\n"));
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
];
|
|
160
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intent DNA — MCP Transport (stdio)
|
|
3
|
+
*
|
|
4
|
+
* Minimal JSON-RPC 2.0 over stdio transport for MCP protocol.
|
|
5
|
+
* Zero external dependencies — implements the protocol directly.
|
|
6
|
+
*
|
|
7
|
+
* Messages are newline-delimited JSON (one JSON object per line).
|
|
8
|
+
*/
|
|
9
|
+
export interface JsonRpcRequest {
|
|
10
|
+
jsonrpc: "2.0";
|
|
11
|
+
id?: number | string;
|
|
12
|
+
method: string;
|
|
13
|
+
params?: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
export interface JsonRpcResponse {
|
|
16
|
+
jsonrpc: "2.0";
|
|
17
|
+
id: number | string | null;
|
|
18
|
+
result?: unknown;
|
|
19
|
+
error?: {
|
|
20
|
+
code: number;
|
|
21
|
+
message: string;
|
|
22
|
+
data?: unknown;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** Send a JSON-RPC response to stdout. */
|
|
26
|
+
export declare function sendResponse(response: JsonRpcResponse): void;
|
|
27
|
+
/** Send a JSON-RPC success response. */
|
|
28
|
+
export declare function sendResult(id: number | string | null, result: unknown): void;
|
|
29
|
+
/** Send a JSON-RPC error response. */
|
|
30
|
+
export declare function sendError(id: number | string | null, code: number, message: string): void;
|
|
31
|
+
/**
|
|
32
|
+
* Start reading JSON-RPC messages from stdin.
|
|
33
|
+
* Calls handler for each valid message.
|
|
34
|
+
* Exits cleanly when stdin closes.
|
|
35
|
+
*/
|
|
36
|
+
export declare function startTransport(handler: (msg: JsonRpcRequest) => Promise<void>): void;
|