intentdna 1.6.4 → 1.7.0
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 +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/cli/commands/compile.js +2 -47
- package/dist/cli/commands/context.d.ts +8 -0
- package/dist/cli/commands/context.js +63 -0
- package/dist/cli/commands/feedback.d.ts +1 -0
- package/dist/cli/commands/feedback.js +11 -0
- package/dist/cli/commands/generate.js +5 -33
- package/dist/cli/commands/init.js +11 -63
- package/dist/cli/commands/run.js +5 -4
- package/dist/cli/commands/show.js +2 -38
- package/dist/cli/commands/sync.d.ts +9 -6
- package/dist/cli/commands/sync.js +209 -190
- package/dist/cli/commands/templates.d.ts +10 -1
- package/dist/cli/commands/templates.js +50 -1
- package/dist/cli/commands/validate.js +15 -9
- package/dist/cli/commands/verify.js +97 -25
- package/dist/cli/index.js +76 -11
- package/dist/compiler/cascade.d.ts +3 -1
- package/dist/compiler/cascade.js +51 -0
- package/dist/compiler/compile.js +37 -0
- package/dist/compiler/diagnostics.d.ts +17 -0
- package/dist/compiler/diagnostics.js +30 -0
- package/dist/compiler/index.d.ts +3 -0
- package/dist/compiler/index.js +8 -11
- package/dist/compiler/input-resolver.d.ts +25 -0
- package/dist/compiler/input-resolver.js +175 -0
- package/dist/hooks/cli.d.ts +10 -1
- package/dist/hooks/cli.js +39 -26
- package/dist/hooks/event-registry.d.ts +17 -0
- package/dist/hooks/event-registry.js +89 -0
- package/dist/hooks/protocol.d.ts +1 -1
- package/dist/hooks/schema.js +5 -1
- package/dist/hooks/state.d.ts +2 -0
- package/dist/hooks/state.js +23 -2
- package/dist/mcp/index.js +2 -0
- package/dist/mcp/tools-compile.js +18 -49
- package/dist/mcp/tools-context.d.ts +2 -0
- package/dist/mcp/tools-context.js +85 -0
- package/dist/mcp/tools-enforce.d.ts +2 -2
- package/dist/mcp/tools-enforce.js +21 -53
- package/dist/mcp/tools-observability.js +24 -0
- package/dist/report/kernel-signals.js +3 -0
- package/dist/report/report-package.d.ts +56 -0
- package/dist/report/report-package.js +85 -0
- package/dist/runtime/agent-md.d.ts +1 -0
- package/dist/runtime/agent-md.js +21 -3
- package/dist/runtime/context-sources.d.ts +14 -0
- package/dist/runtime/context-sources.js +60 -0
- package/dist/runtime/markdown-target-registry.d.ts +17 -0
- package/dist/runtime/markdown-target-registry.js +37 -0
- package/dist/runtime/markdown.d.ts +2 -1
- package/dist/runtime/markdown.js +9 -12
- package/dist/runtime/output-artifact-registry.d.ts +11 -0
- package/dist/runtime/output-artifact-registry.js +30 -0
- package/dist/runtime/settings-adapter.d.ts +2 -1
- package/dist/runtime/settings-adapter.js +4 -12
- package/dist/runtime/skill-adapter.d.ts +32 -4
- package/dist/runtime/skill-adapter.js +187 -10
- package/dist/runtime/workflow-runner.d.ts +1 -1
- package/dist/runtime/workflow-runner.js +1 -1
- package/dist/schema/controller-registry.d.ts +29 -0
- package/dist/schema/controller-registry.js +35 -0
- package/dist/schema/types.d.ts +33 -0
- package/dist/schema/validate.js +157 -138
- package/dist/schema/validators/controllers.d.ts +3 -0
- package/dist/schema/validators/controllers.js +156 -0
- package/dist/signals/index.d.ts +10 -0
- package/dist/signals/index.js +90 -5
- package/dist/templates/catalog.d.ts +19 -0
- package/dist/templates/catalog.js +57 -0
- package/dist/templates/flutter-rewrite.dna.yaml +2 -2
- package/dist/templates/metadata.d.ts +6 -0
- package/dist/templates/metadata.js +32 -0
- package/package.json +1 -1
- package/spec/foundation-hardening.md +2 -1
package/dist/hooks/state.js
CHANGED
|
@@ -330,13 +330,34 @@ export async function appendSessionRead(projectDir, filePath, sessionId) {
|
|
|
330
330
|
}
|
|
331
331
|
}
|
|
332
332
|
const VERIFIER_RESULTS_FILE = "workflow/verifier-results.json";
|
|
333
|
+
export function verifierResultId(result) {
|
|
334
|
+
return "vr_" + createHash("sha256")
|
|
335
|
+
.update(JSON.stringify({
|
|
336
|
+
verifier_id: result.verifier_id,
|
|
337
|
+
when: result.when,
|
|
338
|
+
severity: result.severity,
|
|
339
|
+
kind: result.kind,
|
|
340
|
+
workflow: result.workflow ?? "",
|
|
341
|
+
step_id: result.step_id ?? "",
|
|
342
|
+
status: result.status,
|
|
343
|
+
target: result.target ?? "",
|
|
344
|
+
evidence: result.evidence ?? "",
|
|
345
|
+
artifact: result.artifact ?? "",
|
|
346
|
+
timestamp: result.timestamp,
|
|
347
|
+
}))
|
|
348
|
+
.digest("hex")
|
|
349
|
+
.slice(0, 16);
|
|
350
|
+
}
|
|
351
|
+
function normalizeVerifierResult(result) {
|
|
352
|
+
return result.result_id ? result : { ...result, result_id: verifierResultId(result) };
|
|
353
|
+
}
|
|
333
354
|
async function readVerifierResultsFile(projectDir, sessionId) {
|
|
334
355
|
const stateDir = resolveStateDir(projectDir, sessionId);
|
|
335
356
|
const filePath = join(stateDir, VERIFIER_RESULTS_FILE);
|
|
336
357
|
try {
|
|
337
358
|
const raw = await readFile(filePath, "utf-8");
|
|
338
359
|
const parsed = JSON.parse(raw);
|
|
339
|
-
return Array.isArray(parsed) ? parsed : [];
|
|
360
|
+
return Array.isArray(parsed) ? parsed.map(normalizeVerifierResult) : [];
|
|
340
361
|
}
|
|
341
362
|
catch {
|
|
342
363
|
return [];
|
|
@@ -381,7 +402,7 @@ export async function writeVerifierResults(projectDir, results, sessionId) {
|
|
|
381
402
|
}
|
|
382
403
|
export async function appendVerifierResult(projectDir, result, sessionId) {
|
|
383
404
|
const results = await readVerifierResultsFile(projectDir, sessionId);
|
|
384
|
-
results.push(result);
|
|
405
|
+
results.push(normalizeVerifierResult(result));
|
|
385
406
|
await writeVerifierResults(projectDir, results, sessionId);
|
|
386
407
|
}
|
|
387
408
|
const TRACE_DIR = "trace";
|
package/dist/mcp/index.js
CHANGED
|
@@ -15,6 +15,7 @@ import { createStateTools } from "./tools-state.js";
|
|
|
15
15
|
import { createCompileTools } from "./tools-compile.js";
|
|
16
16
|
import { createEnforceTools } from "./tools-enforce.js";
|
|
17
17
|
import { createObservabilityTools } from "./tools-observability.js";
|
|
18
|
+
import { createContextTools } from "./tools-context.js";
|
|
18
19
|
// Parse args
|
|
19
20
|
const args = process.argv.slice(2);
|
|
20
21
|
let projectDir = process.cwd();
|
|
@@ -36,6 +37,7 @@ catch { /* use default */ }
|
|
|
36
37
|
const tools = [
|
|
37
38
|
...createStateTools(projectDir),
|
|
38
39
|
...createCompileTools(projectDir),
|
|
40
|
+
...createContextTools(projectDir),
|
|
39
41
|
...createEnforceTools(projectDir),
|
|
40
42
|
...createObservabilityTools(projectDir),
|
|
41
43
|
];
|
|
@@ -7,45 +7,8 @@
|
|
|
7
7
|
* - dna_sync: Run full sync pipeline (compile + inject + hooks)
|
|
8
8
|
*/
|
|
9
9
|
import { resolve } from "node:path";
|
|
10
|
-
import {
|
|
11
|
-
import { loadDNA, compileFromFiles } from "../compiler/index.js";
|
|
10
|
+
import { compileFromFiles, detectDNAConfigs, expandDNAInputFiles, formatDiagnostic, loadDNAWithDiagnostics } from "../compiler/index.js";
|
|
12
11
|
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
12
|
export function createCompileTools(projectDir) {
|
|
50
13
|
return [
|
|
51
14
|
// ── dna_validate ─────────────────────────────────────
|
|
@@ -62,20 +25,23 @@ export function createCompileTools(projectDir) {
|
|
|
62
25
|
const configPath = typeof args.config_path === "string"
|
|
63
26
|
? resolve(projectDir, args.config_path)
|
|
64
27
|
: undefined;
|
|
65
|
-
const files = configPath ? [configPath] : await
|
|
28
|
+
const files = configPath ? [configPath] : await detectDNAConfigs(projectDir);
|
|
66
29
|
if (files.length === 0)
|
|
67
30
|
return errorResult("No DNA config files found.");
|
|
68
31
|
const results = [];
|
|
69
32
|
let allValid = true;
|
|
70
33
|
for (const file of files) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
34
|
+
const result = await loadDNAWithDiagnostics(file);
|
|
35
|
+
const errors = result.diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
36
|
+
if (errors.length > 0 || !result.dna) {
|
|
37
|
+
allValid = false;
|
|
38
|
+
results.push(`${file}: INVALID`);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
74
41
|
results.push(`${file}: valid`);
|
|
75
42
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
results.push(`${file}: INVALID — ${err instanceof Error ? err.message : String(err)}`);
|
|
43
|
+
for (const diagnostic of result.diagnostics) {
|
|
44
|
+
results.push(` ${formatDiagnostic(diagnostic)}`);
|
|
79
45
|
}
|
|
80
46
|
}
|
|
81
47
|
results.unshift(allValid ? "All configs valid." : "Validation errors found.");
|
|
@@ -98,11 +64,12 @@ export function createCompileTools(projectDir) {
|
|
|
98
64
|
const configPath = typeof args.config_path === "string"
|
|
99
65
|
? resolve(projectDir, args.config_path)
|
|
100
66
|
: undefined;
|
|
101
|
-
const files = configPath ? [configPath] : await
|
|
67
|
+
const files = configPath ? [configPath] : await detectDNAConfigs(projectDir);
|
|
102
68
|
if (files.length === 0)
|
|
103
69
|
return errorResult("No DNA config files found.");
|
|
104
70
|
try {
|
|
105
|
-
const
|
|
71
|
+
const expandedFiles = await expandDNAInputFiles(files);
|
|
72
|
+
const ir = await compileFromFiles(expandedFiles, {
|
|
106
73
|
context: typeof args.context === "string" ? args.context : undefined,
|
|
107
74
|
role: typeof args.role === "string" ? args.role : undefined,
|
|
108
75
|
});
|
|
@@ -142,12 +109,13 @@ export function createCompileTools(projectDir) {
|
|
|
142
109
|
evolve: { type: "boolean", description: "Run evolution engine before compiling (default false)" },
|
|
143
110
|
context: { type: "string", description: "Active context name" },
|
|
144
111
|
role: { type: "string", description: "Active role name" },
|
|
112
|
+
mode: { type: "string", enum: ["auto", "plugin", "bin"], description: "Sync mode (default auto)" },
|
|
145
113
|
},
|
|
146
114
|
},
|
|
147
115
|
handler: async (args) => {
|
|
148
116
|
// Import sync command dynamically to avoid circular deps
|
|
149
|
-
const { runSync
|
|
150
|
-
const files = await
|
|
117
|
+
const { runSync } = await import("../cli/commands/sync.js");
|
|
118
|
+
const files = await detectDNAConfigs(projectDir);
|
|
151
119
|
if (files.length === 0)
|
|
152
120
|
return errorResult("No DNA config files found.");
|
|
153
121
|
const exitCode = await runSync({
|
|
@@ -160,6 +128,7 @@ export function createCompileTools(projectDir) {
|
|
|
160
128
|
skillsDir: resolve(projectDir, ".claude", "skills"),
|
|
161
129
|
settingsPath: resolve(projectDir, ".claude", "settings.json"),
|
|
162
130
|
remove: false,
|
|
131
|
+
mode: typeof args.mode === "string" && ["auto", "plugin", "bin"].includes(args.mode) ? args.mode : "auto",
|
|
163
132
|
});
|
|
164
133
|
if (exitCode === 0) {
|
|
165
134
|
return textResult("Sync completed successfully.");
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { findContextSource, formatContextSource, loadContextSourceIndex, readContextSourceDocument, searchContextSources, } from "../runtime/context-sources.js";
|
|
3
|
+
import { errorResult, textResult } from "./server.js";
|
|
4
|
+
function filesArg(args, projectDir) {
|
|
5
|
+
if (typeof args.config_path === "string")
|
|
6
|
+
return [resolve(projectDir, args.config_path)];
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
export function createContextTools(projectDir) {
|
|
10
|
+
return [
|
|
11
|
+
{
|
|
12
|
+
name: "dna_context_sources",
|
|
13
|
+
description: "List explicitly declared planning-only advisory context sources",
|
|
14
|
+
inputSchema: {
|
|
15
|
+
type: "object",
|
|
16
|
+
properties: {
|
|
17
|
+
config_path: { type: "string", description: "Path to DNA config (auto-detected if omitted)" },
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
handler: async (args) => {
|
|
21
|
+
const index = await loadContextSourceIndex(projectDir, filesArg(args, projectDir));
|
|
22
|
+
if (index.sources.length === 0)
|
|
23
|
+
return textResult("No advisory context sources declared.");
|
|
24
|
+
const lines = index.sources.map(formatContextSource);
|
|
25
|
+
if (index.planning_instructions.length > 0) {
|
|
26
|
+
lines.push("", "Planning instructions:");
|
|
27
|
+
for (const instruction of index.planning_instructions)
|
|
28
|
+
lines.push(`- ${instruction}`);
|
|
29
|
+
}
|
|
30
|
+
return textResult(lines.join("\n"));
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: "dna_context_search",
|
|
35
|
+
description: "Search advisory context source metadata. This does not search repository files.",
|
|
36
|
+
inputSchema: {
|
|
37
|
+
type: "object",
|
|
38
|
+
properties: {
|
|
39
|
+
query: { type: "string", description: "Metadata query" },
|
|
40
|
+
config_path: { type: "string", description: "Path to DNA config (auto-detected if omitted)" },
|
|
41
|
+
},
|
|
42
|
+
required: ["query"],
|
|
43
|
+
},
|
|
44
|
+
handler: async (args) => {
|
|
45
|
+
const query = typeof args.query === "string" ? args.query : "";
|
|
46
|
+
const index = await loadContextSourceIndex(projectDir, filesArg(args, projectDir));
|
|
47
|
+
const matches = searchContextSources(index, query);
|
|
48
|
+
if (matches.length === 0)
|
|
49
|
+
return errorResult(`No advisory context sources matched '${query}'.`);
|
|
50
|
+
return textResult(matches.map(formatContextSource).join("\n"));
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: "dna_get_context",
|
|
55
|
+
description: "Read a declared local advisory context document by source id",
|
|
56
|
+
inputSchema: {
|
|
57
|
+
type: "object",
|
|
58
|
+
properties: {
|
|
59
|
+
id: { type: "string", description: "Context source id" },
|
|
60
|
+
config_path: { type: "string", description: "Path to DNA config (auto-detected if omitted)" },
|
|
61
|
+
},
|
|
62
|
+
required: ["id"],
|
|
63
|
+
},
|
|
64
|
+
handler: async (args) => {
|
|
65
|
+
const id = typeof args.id === "string" ? args.id : "";
|
|
66
|
+
const index = await loadContextSourceIndex(projectDir, filesArg(args, projectDir));
|
|
67
|
+
const source = findContextSource(index, id);
|
|
68
|
+
if (!source)
|
|
69
|
+
return errorResult(`Unknown advisory context source '${id}'.`);
|
|
70
|
+
const doc = await readContextSourceDocument(projectDir, source);
|
|
71
|
+
const lines = [
|
|
72
|
+
formatContextSource(source),
|
|
73
|
+
"Planning-only advisory context; does not satisfy enforcement, handoff, or ArtifactManifest requirements.",
|
|
74
|
+
];
|
|
75
|
+
if (doc.content !== undefined) {
|
|
76
|
+
lines.push("", "---", doc.content);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
lines.push("No local document body is available for this source type.");
|
|
80
|
+
}
|
|
81
|
+
return textResult(lines.join("\n"));
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Intent DNA — MCP Enforce Tools (U2)
|
|
3
3
|
*
|
|
4
|
-
* Provides enforcement via MCP server with in-memory IR cache.
|
|
5
|
-
*
|
|
4
|
+
* Provides enforcement via a persistent MCP server with in-memory IR cache.
|
|
5
|
+
* MCP calls reuse the full hook runtime without reloading IR on every request.
|
|
6
6
|
*
|
|
7
7
|
* Tools:
|
|
8
8
|
* - dna_enforce: Run enforcement for a hook event (PreToolUse, PostToolUse, etc.)
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Intent DNA — MCP Enforce Tools (U2)
|
|
3
3
|
*
|
|
4
|
-
* Provides enforcement via MCP server with in-memory IR cache.
|
|
5
|
-
*
|
|
4
|
+
* Provides enforcement via a persistent MCP server with in-memory IR cache.
|
|
5
|
+
* MCP calls reuse the full hook runtime without reloading IR on every request.
|
|
6
6
|
*
|
|
7
7
|
* Tools:
|
|
8
8
|
* - dna_enforce: Run enforcement for a hook event (PreToolUse, PostToolUse, etc.)
|
|
9
9
|
*/
|
|
10
10
|
import { resolve } from "node:path";
|
|
11
11
|
import { stat, readFile } from "node:fs/promises";
|
|
12
|
-
import {
|
|
12
|
+
import { silentOutput } from "../hooks/protocol.js";
|
|
13
|
+
import { hookEventsForSurface } from "../hooks/event-registry.js";
|
|
14
|
+
import { validateHookInput } from "../hooks/schema.js";
|
|
15
|
+
import { runHookEvent } from "../hooks/cli.js";
|
|
13
16
|
import { textResult, errorResult } from "./server.js";
|
|
14
17
|
// ── IR Cache ────────────────────────────────────────────────
|
|
15
18
|
/** In-memory IR cache with mtime-based invalidation. */
|
|
@@ -61,53 +64,18 @@ export class IRCache {
|
|
|
61
64
|
this.lastMtime = 0;
|
|
62
65
|
}
|
|
63
66
|
}
|
|
64
|
-
const VALID_EVENTS = new Set(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
tool_name: String(input.tool_name ?? ""),
|
|
77
|
-
tool_input: (input.tool_input ?? {}),
|
|
78
|
-
agent_type: typeof input.agent_type === "string" ? input.agent_type : undefined,
|
|
79
|
-
cwd: typeof input.cwd === "string" ? input.cwd : undefined,
|
|
80
|
-
}, undefined, roles);
|
|
81
|
-
case "PostToolUse":
|
|
82
|
-
return enforcePostToolUse(ir, {
|
|
83
|
-
tool_name: String(input.tool_name ?? ""),
|
|
84
|
-
tool_input: (input.tool_input ?? {}),
|
|
85
|
-
tool_output: typeof input.tool_output === "string" ? input.tool_output : undefined,
|
|
86
|
-
agent_type: typeof input.agent_type === "string" ? input.agent_type : undefined,
|
|
87
|
-
});
|
|
88
|
-
case "UserPromptSubmit":
|
|
89
|
-
return enforceUserPromptSubmit(ir, {
|
|
90
|
-
prompt: typeof input.prompt === "string" ? input.prompt : undefined,
|
|
91
|
-
});
|
|
92
|
-
case "SubagentStop":
|
|
93
|
-
return enforceSubagentStop(ir, {
|
|
94
|
-
agent_name: typeof input.agent_name === "string" ? input.agent_name : undefined,
|
|
95
|
-
agent_type: typeof input.agent_type === "string" ? input.agent_type : undefined,
|
|
96
|
-
});
|
|
97
|
-
case "PreCompact":
|
|
98
|
-
return enforcePreCompact(ir);
|
|
99
|
-
case "Notification":
|
|
100
|
-
return enforceNotification(ir, {
|
|
101
|
-
title: typeof input.title === "string" ? input.title : undefined,
|
|
102
|
-
message: typeof input.message === "string" ? input.message : undefined,
|
|
103
|
-
});
|
|
104
|
-
case "SessionStart":
|
|
105
|
-
return enforceSessionStart(ir, {
|
|
106
|
-
cwd: typeof input.cwd === "string" ? input.cwd : undefined,
|
|
107
|
-
sessionId: typeof input.session_id === "string" ? input.session_id : undefined,
|
|
108
|
-
});
|
|
109
|
-
default:
|
|
110
|
-
return null;
|
|
67
|
+
const VALID_EVENTS = new Set(hookEventsForSurface("mcp"));
|
|
68
|
+
async function dispatchEnforce(event, ir, input, projectDir, roles) {
|
|
69
|
+
try {
|
|
70
|
+
const validation = validateHookInput(event, input);
|
|
71
|
+
if (!validation.valid)
|
|
72
|
+
return silentOutput();
|
|
73
|
+
const rawInput = validation.normalized;
|
|
74
|
+
const sessionId = typeof rawInput.sessionId === "string" ? rawInput.sessionId : undefined;
|
|
75
|
+
return await runHookEvent({ event, ir, rawInput, projectDir, sessionId, roles });
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return silentOutput();
|
|
111
79
|
}
|
|
112
80
|
}
|
|
113
81
|
// ── Tool Registration ───────────────────────────────────────
|
|
@@ -116,8 +84,8 @@ export function createEnforceTools(projectDir) {
|
|
|
116
84
|
return [
|
|
117
85
|
{
|
|
118
86
|
name: "dna_enforce",
|
|
119
|
-
description: "Run DNA enforcement for a hook event
|
|
120
|
-
"
|
|
87
|
+
description: "Run DNA enforcement for a hook event inside the persistent MCP server. " +
|
|
88
|
+
"Uses cached IR and the same runtime path as the dna-hook binary.",
|
|
121
89
|
inputSchema: {
|
|
122
90
|
type: "object",
|
|
123
91
|
properties: {
|
|
@@ -143,7 +111,7 @@ export function createEnforceTools(projectDir) {
|
|
|
143
111
|
return errorResult("No compiled IR found. Run `dna sync` to compile your DNA config.");
|
|
144
112
|
}
|
|
145
113
|
const input = (args.input ?? {});
|
|
146
|
-
const result = dispatchEnforce(event, cached.ir, input, cached.roles);
|
|
114
|
+
const result = await dispatchEnforce(event, cached.ir, input, projectDir, cached.roles);
|
|
147
115
|
// Format as human-readable text
|
|
148
116
|
const lines = [];
|
|
149
117
|
lines.push(`Decision: ${result.continue ? "allow" : "BLOCK"}`);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { textResult } from "./server.js";
|
|
2
2
|
import { buildKernelReport, formatKernelReport } from "../report/kernel-report.js";
|
|
3
|
+
import { buildReportPackage } from "../report/report-package.js";
|
|
3
4
|
export function createObservabilityTools(projectDir) {
|
|
4
5
|
return [
|
|
5
6
|
{
|
|
@@ -30,5 +31,28 @@ export function createObservabilityTools(projectDir) {
|
|
|
30
31
|
"\nMCP observability only: this tool does not execute verifiers, does not enforce hook parity, and does not mutate state or markers.\n");
|
|
31
32
|
},
|
|
32
33
|
},
|
|
34
|
+
{
|
|
35
|
+
name: "dna_report_package",
|
|
36
|
+
description: "Read-only local evidence package with traces, verifier results, handoffs, diagnostics, deferrals, and fact-backed critical decisions.",
|
|
37
|
+
inputSchema: {
|
|
38
|
+
type: "object",
|
|
39
|
+
properties: {
|
|
40
|
+
days: { type: "number", description: "Days to look back (default 7)" },
|
|
41
|
+
session_id: { type: "string", description: "Session ID filter" },
|
|
42
|
+
dna_id: { type: "string", description: "DNA ID for marker preview; derived from compiled IR if omitted and unambiguous" },
|
|
43
|
+
include_marker_preview: { type: "boolean", description: "Include preview-only marker changes (default false)" },
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
handler: async (args) => {
|
|
47
|
+
const reportPackage = await buildReportPackage({
|
|
48
|
+
projectDir,
|
|
49
|
+
days: typeof args.days === "number" ? args.days : 7,
|
|
50
|
+
sessionId: typeof args.session_id === "string" ? args.session_id : undefined,
|
|
51
|
+
dnaId: typeof args.dna_id === "string" ? args.dna_id : undefined,
|
|
52
|
+
includeMarkerPreview: typeof args.include_marker_preview === "boolean" ? args.include_marker_preview : false,
|
|
53
|
+
});
|
|
54
|
+
return textResult(JSON.stringify(reportPackage, null, 2));
|
|
55
|
+
},
|
|
56
|
+
},
|
|
33
57
|
];
|
|
34
58
|
}
|
|
@@ -11,6 +11,8 @@ function extractGeneFromReason(reason) {
|
|
|
11
11
|
return sourceMatch?.[1];
|
|
12
12
|
}
|
|
13
13
|
function verifierSourceRef(result) {
|
|
14
|
+
if (result.result_id)
|
|
15
|
+
return `verifier:${result.result_id}`;
|
|
14
16
|
return [
|
|
15
17
|
"verifier",
|
|
16
18
|
result.workflow ?? "unknown-workflow",
|
|
@@ -21,6 +23,7 @@ function verifierSourceRef(result) {
|
|
|
21
23
|
}
|
|
22
24
|
function verifierEvidencePayload(result) {
|
|
23
25
|
const payload = {
|
|
26
|
+
result_id: result.result_id,
|
|
24
27
|
verifier_id: result.verifier_id,
|
|
25
28
|
when: result.when,
|
|
26
29
|
severity: result.severity,
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { DNAWorkflowState, TraceEntry, VerifierResultEntry } from "../hooks/state.js";
|
|
2
|
+
import type { CompletedArtifactEntry } from "../schema/types.js";
|
|
3
|
+
import { type KernelReport, type KernelReportOptions } from "./kernel-report.js";
|
|
4
|
+
export type EvidenceRef = {
|
|
5
|
+
type: "trace";
|
|
6
|
+
trace_id: string;
|
|
7
|
+
} | {
|
|
8
|
+
type: "verifier_result";
|
|
9
|
+
result_id: string;
|
|
10
|
+
} | {
|
|
11
|
+
type: "artifact";
|
|
12
|
+
artifact_id: string;
|
|
13
|
+
path?: string;
|
|
14
|
+
};
|
|
15
|
+
export interface CriticalDecision {
|
|
16
|
+
decision_id: string;
|
|
17
|
+
kind: "block" | "warn" | "verifier_failure" | "handoff_gate";
|
|
18
|
+
summary: string;
|
|
19
|
+
refs: EvidenceRef[];
|
|
20
|
+
}
|
|
21
|
+
export interface ReportPackage {
|
|
22
|
+
schema_version: "intentdna.report_package.v1";
|
|
23
|
+
generated_at: string;
|
|
24
|
+
source: KernelReport["source"];
|
|
25
|
+
kernel_report: KernelReport;
|
|
26
|
+
traces: TraceEntry[];
|
|
27
|
+
verifier_results: VerifierResultEntry[];
|
|
28
|
+
handoffs: {
|
|
29
|
+
workflow_state?: DNAWorkflowState;
|
|
30
|
+
completed_artifacts: CompletedArtifactEntry[];
|
|
31
|
+
artifact_refs: Array<{
|
|
32
|
+
artifact_id: string;
|
|
33
|
+
path: string;
|
|
34
|
+
metadata?: Record<string, unknown>;
|
|
35
|
+
}>;
|
|
36
|
+
};
|
|
37
|
+
diagnostics: Array<{
|
|
38
|
+
severity: "error" | "warning" | "info";
|
|
39
|
+
message: string;
|
|
40
|
+
code?: string;
|
|
41
|
+
}>;
|
|
42
|
+
deferrals: Array<{
|
|
43
|
+
id: string;
|
|
44
|
+
reason: string;
|
|
45
|
+
refs: EvidenceRef[];
|
|
46
|
+
}>;
|
|
47
|
+
critical_decisions: CriticalDecision[];
|
|
48
|
+
mutation: {
|
|
49
|
+
command_class: "read-only" | "preview-only";
|
|
50
|
+
wrote_state: false;
|
|
51
|
+
wrote_evolution: false;
|
|
52
|
+
persisted_outcomes: false;
|
|
53
|
+
mutated_markers: false;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export declare function buildReportPackage(options: KernelReportOptions): Promise<ReportPackage>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { readTraces, readVerifierResults, readWorkflowState } from "../hooks/state.js";
|
|
2
|
+
import { buildKernelReport } from "./kernel-report.js";
|
|
3
|
+
function withinDays(timestamp, days) {
|
|
4
|
+
const parsed = Date.parse(timestamp);
|
|
5
|
+
if (Number.isNaN(parsed))
|
|
6
|
+
return true;
|
|
7
|
+
return parsed >= Date.now() - days * 24 * 60 * 60 * 1000;
|
|
8
|
+
}
|
|
9
|
+
function artifactRefs(completed) {
|
|
10
|
+
const refs = [];
|
|
11
|
+
for (const entry of completed) {
|
|
12
|
+
for (const artifact of entry.artifacts) {
|
|
13
|
+
refs.push({
|
|
14
|
+
artifact_id: artifact.artifact_id ?? `${entry.step_id}:${artifact.type}:${artifact.path}`,
|
|
15
|
+
path: artifact.path,
|
|
16
|
+
metadata: artifact.metadata,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return refs;
|
|
21
|
+
}
|
|
22
|
+
function buildCriticalDecisions(traces, verifierResults, completed) {
|
|
23
|
+
const decisions = [];
|
|
24
|
+
for (const trace of traces) {
|
|
25
|
+
if (trace.decision !== "block" && trace.decision !== "warn")
|
|
26
|
+
continue;
|
|
27
|
+
decisions.push({
|
|
28
|
+
decision_id: `${trace.decision}:${trace.trace_id}`,
|
|
29
|
+
kind: trace.decision,
|
|
30
|
+
summary: trace.reason?.split("\n")[0] ?? `${trace.decision} decision`,
|
|
31
|
+
refs: [{ type: "trace", trace_id: trace.trace_id }],
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
for (const result of verifierResults) {
|
|
35
|
+
if (result.status !== "fail")
|
|
36
|
+
continue;
|
|
37
|
+
decisions.push({
|
|
38
|
+
decision_id: `verifier_failure:${result.result_id ?? result.verifier_id}`,
|
|
39
|
+
kind: "verifier_failure",
|
|
40
|
+
summary: result.message ?? `Verifier failed: ${result.verifier_id}`,
|
|
41
|
+
refs: [{ type: "verifier_result", result_id: result.result_id ?? result.verifier_id }],
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
for (const ref of artifactRefs(completed)) {
|
|
45
|
+
decisions.push({
|
|
46
|
+
decision_id: `handoff_gate:${ref.artifact_id}`,
|
|
47
|
+
kind: "handoff_gate",
|
|
48
|
+
summary: `Completed handoff artifact ${ref.artifact_id}`,
|
|
49
|
+
refs: [{ type: "artifact", artifact_id: ref.artifact_id, path: ref.path }],
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return decisions;
|
|
53
|
+
}
|
|
54
|
+
export async function buildReportPackage(options) {
|
|
55
|
+
const days = options.days ?? 7;
|
|
56
|
+
const kernelReport = await buildKernelReport(options);
|
|
57
|
+
const traces = await readTraces(options.projectDir, days, options.sessionId);
|
|
58
|
+
const verifierResults = (await readVerifierResults(options.projectDir, options.sessionId))
|
|
59
|
+
.filter((result) => withinDays(result.timestamp, days));
|
|
60
|
+
const workflowState = await readWorkflowState(options.projectDir, options.sessionId, 0) ?? undefined;
|
|
61
|
+
const completedArtifacts = workflowState?.completed_artifacts ?? [];
|
|
62
|
+
return {
|
|
63
|
+
schema_version: "intentdna.report_package.v1",
|
|
64
|
+
generated_at: new Date().toISOString(),
|
|
65
|
+
source: kernelReport.source,
|
|
66
|
+
kernel_report: kernelReport,
|
|
67
|
+
traces,
|
|
68
|
+
verifier_results: verifierResults,
|
|
69
|
+
handoffs: {
|
|
70
|
+
workflow_state: workflowState,
|
|
71
|
+
completed_artifacts: completedArtifacts,
|
|
72
|
+
artifact_refs: artifactRefs(completedArtifacts),
|
|
73
|
+
},
|
|
74
|
+
diagnostics: [],
|
|
75
|
+
deferrals: [],
|
|
76
|
+
critical_decisions: buildCriticalDecisions(traces, verifierResults, completedArtifacts),
|
|
77
|
+
mutation: {
|
|
78
|
+
command_class: options.includeMarkerPreview ? "preview-only" : "read-only",
|
|
79
|
+
wrote_state: false,
|
|
80
|
+
wrote_evolution: false,
|
|
81
|
+
persisted_outcomes: false,
|
|
82
|
+
mutated_markers: false,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -31,6 +31,7 @@ export interface AgentMDResult {
|
|
|
31
31
|
* Convert camelCase or snake_case to kebab-case.
|
|
32
32
|
*/
|
|
33
33
|
export declare function toKebabCase(name: string): string;
|
|
34
|
+
export declare function assertSafeGeneratedName(name: string, label?: string): void;
|
|
34
35
|
/**
|
|
35
36
|
* Compile a single RoleDef into an agent MD file.
|
|
36
37
|
* Pure function, no side effects.
|
package/dist/runtime/agent-md.js
CHANGED
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
* Output directory: .claude/agents/<name>.md
|
|
10
10
|
*/
|
|
11
11
|
import { mkdir, writeFile, readFile, readdir, rm } from "node:fs/promises";
|
|
12
|
-
import { join } from "node:path";
|
|
12
|
+
import { join, resolve, sep } from "node:path";
|
|
13
13
|
const SENTINEL_COMMENT = "<!-- intentdna:managed — do not edit manually -->";
|
|
14
|
+
const SAFE_GENERATED_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
14
15
|
// ── Helpers ────────────────────────────────────────────────
|
|
15
16
|
/**
|
|
16
17
|
* Convert camelCase or snake_case to kebab-case.
|
|
@@ -23,12 +24,26 @@ export function toKebabCase(name) {
|
|
|
23
24
|
.replace(/[_\s]+/g, "-")
|
|
24
25
|
.toLowerCase();
|
|
25
26
|
}
|
|
27
|
+
export function assertSafeGeneratedName(name, label = "generated name") {
|
|
28
|
+
if (!SAFE_GENERATED_NAME.test(name)) {
|
|
29
|
+
throw new Error(`${label} '${name}' must contain only lowercase letters, digits, and single hyphens`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function resolveContainedPath(baseDir, ...segments) {
|
|
33
|
+
const base = resolve(baseDir);
|
|
34
|
+
const target = resolve(baseDir, ...segments);
|
|
35
|
+
if (target !== base && !target.startsWith(`${base}${sep}`)) {
|
|
36
|
+
throw new Error(`generated path escapes output directory: ${target}`);
|
|
37
|
+
}
|
|
38
|
+
return target;
|
|
39
|
+
}
|
|
26
40
|
/**
|
|
27
41
|
* Render YAML frontmatter string (without --- delimiters).
|
|
28
42
|
*/
|
|
29
43
|
function renderFrontmatter(name, role, options) {
|
|
30
44
|
const prefix = options?.namePrefix ?? "dna-";
|
|
31
45
|
const fullName = `${prefix}${toKebabCase(name)}`;
|
|
46
|
+
assertSafeGeneratedName(fullName, "agent name");
|
|
32
47
|
const lines = [];
|
|
33
48
|
lines.push(`name: ${fullName}`);
|
|
34
49
|
lines.push(`description: "${escapeYamlString(role.description)}"`);
|
|
@@ -65,6 +80,7 @@ function renderConstraints(ir) {
|
|
|
65
80
|
function renderBody(name, role, ir, options) {
|
|
66
81
|
const prefix = options?.namePrefix ?? "dna-";
|
|
67
82
|
const fullName = `${prefix}${toKebabCase(name)}`;
|
|
83
|
+
assertSafeGeneratedName(fullName, "agent name");
|
|
68
84
|
const compiledAt = new Date().toISOString();
|
|
69
85
|
const includeDNA = options?.includeDNAGuidelines ?? true;
|
|
70
86
|
const sections = [];
|
|
@@ -184,7 +200,9 @@ function renderBody(name, role, ir, options) {
|
|
|
184
200
|
export function compileRoleToAgentMD(roleName, role, ir, options) {
|
|
185
201
|
const prefix = options?.namePrefix ?? "dna-";
|
|
186
202
|
const kebabName = toKebabCase(roleName);
|
|
187
|
-
const
|
|
203
|
+
const fullName = `${prefix}${kebabName}`;
|
|
204
|
+
assertSafeGeneratedName(fullName, "agent name");
|
|
205
|
+
const fileName = `${fullName}.md`;
|
|
188
206
|
const frontmatter = renderFrontmatter(roleName, role, options);
|
|
189
207
|
const body = renderBody(roleName, role, ir, options);
|
|
190
208
|
const content = `---\n${frontmatter}\n---\n\n${body}`;
|
|
@@ -207,7 +225,7 @@ export async function writeAgentMDFiles(results, outputDir) {
|
|
|
207
225
|
await mkdir(outputDir, { recursive: true });
|
|
208
226
|
const written = [];
|
|
209
227
|
for (const result of results) {
|
|
210
|
-
const path =
|
|
228
|
+
const path = resolveContainedPath(outputDir, result.fileName);
|
|
211
229
|
await writeFile(path, result.content, "utf-8");
|
|
212
230
|
written.push(path);
|
|
213
231
|
}
|