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
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export class DNADiagnosticsError extends Error {
|
|
2
|
+
diagnostics;
|
|
3
|
+
constructor(message, diagnostics) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "DNADiagnosticsError";
|
|
6
|
+
this.diagnostics = diagnostics;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export function hasDiagnosticErrors(diagnostics) {
|
|
10
|
+
return diagnostics.some((diagnostic) => diagnostic.severity === "error");
|
|
11
|
+
}
|
|
12
|
+
export function formatDiagnostic(diagnostic) {
|
|
13
|
+
const location = [diagnostic.file, diagnostic.path].filter(Boolean).join(":");
|
|
14
|
+
const prefix = location ? `${location}: ` : "";
|
|
15
|
+
const hint = diagnostic.hint ? ` (${diagnostic.hint})` : "";
|
|
16
|
+
return `${diagnostic.severity.toUpperCase()} ${diagnostic.code}: ${prefix}${diagnostic.message}${hint}`;
|
|
17
|
+
}
|
|
18
|
+
export function formatDiagnostics(diagnostics) {
|
|
19
|
+
return diagnostics.map(formatDiagnostic).join("\n");
|
|
20
|
+
}
|
|
21
|
+
export function diagnosticsFromError(error, file) {
|
|
22
|
+
if (error instanceof DNADiagnosticsError)
|
|
23
|
+
return error.diagnostics;
|
|
24
|
+
return [{
|
|
25
|
+
severity: "error",
|
|
26
|
+
code: "unexpected_error",
|
|
27
|
+
message: error instanceof Error ? error.message : String(error),
|
|
28
|
+
file,
|
|
29
|
+
}];
|
|
30
|
+
}
|
package/dist/compiler/index.d.ts
CHANGED
|
@@ -10,6 +10,9 @@ export { activateDNA } from "./activate.js";
|
|
|
10
10
|
export { compileDNA } from "./compile.js";
|
|
11
11
|
export { compileWorkflow } from "./workflow.js";
|
|
12
12
|
export type { CompileWorkflowOptions, CompileWorkflowResult, CompileWorkflowError, } from "./workflow.js";
|
|
13
|
+
export type { Diagnostic, DiagnosticSeverity } from "./diagnostics.js";
|
|
14
|
+
export { DNADiagnosticsError, formatDiagnostic, formatDiagnostics } from "./diagnostics.js";
|
|
15
|
+
export { assertNoNamespaceCollisions, checkNamespaceCollisions, detectDNAConfigs, expandDNAInputFiles, loadDNAWithDiagnostics, resolveDNAInputs, resolveSpeciesReference, } from "./input-resolver.js";
|
|
13
16
|
/**
|
|
14
17
|
* Load and parse a DNA file from disk.
|
|
15
18
|
*/
|
package/dist/compiler/index.js
CHANGED
|
@@ -4,29 +4,26 @@
|
|
|
4
4
|
* Full compilation pipeline:
|
|
5
5
|
* DNA files → parse → cascade → activate (+ epigenetic) → compile → IR
|
|
6
6
|
*/
|
|
7
|
-
import { readFile } from "node:fs/promises";
|
|
8
|
-
import { validateDNA } from "../schema/validate.js";
|
|
9
|
-
import { parseYAML } from "../schema/yaml-parser.js";
|
|
10
7
|
import { cascadeDNA } from "./cascade.js";
|
|
11
8
|
import { activateDNA } from "./activate.js";
|
|
12
9
|
import { compileDNA } from "./compile.js";
|
|
10
|
+
import { DNADiagnosticsError, formatDiagnostics } from "./diagnostics.js";
|
|
11
|
+
import { loadDNAWithDiagnostics } from "./input-resolver.js";
|
|
13
12
|
export { cascadeDNA } from "./cascade.js";
|
|
14
13
|
export { activateDNA } from "./activate.js";
|
|
15
14
|
export { compileDNA } from "./compile.js";
|
|
16
15
|
export { compileWorkflow } from "./workflow.js";
|
|
16
|
+
export { DNADiagnosticsError, formatDiagnostic, formatDiagnostics } from "./diagnostics.js";
|
|
17
|
+
export { assertNoNamespaceCollisions, checkNamespaceCollisions, detectDNAConfigs, expandDNAInputFiles, loadDNAWithDiagnostics, resolveDNAInputs, resolveSpeciesReference, } from "./input-resolver.js";
|
|
17
18
|
/**
|
|
18
19
|
* Load and parse a DNA file from disk.
|
|
19
20
|
*/
|
|
20
21
|
export async function loadDNA(filePath) {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const result = validateDNA(dna);
|
|
25
|
-
if (!result.valid) {
|
|
26
|
-
const messages = result.errors.map((e) => ` ${e.path}: ${e.message}`).join("\n");
|
|
27
|
-
throw new Error(`Invalid DNA file ${filePath}:\n${messages}`);
|
|
22
|
+
const result = await loadDNAWithDiagnostics(filePath);
|
|
23
|
+
if (!result.dna) {
|
|
24
|
+
throw new DNADiagnosticsError(`Invalid DNA file ${filePath}:\n${formatDiagnostics(result.diagnostics)}`, result.diagnostics);
|
|
28
25
|
}
|
|
29
|
-
return dna;
|
|
26
|
+
return result.dna;
|
|
30
27
|
}
|
|
31
28
|
/**
|
|
32
29
|
* Full compilation pipeline: load DNA files, cascade, activate context, compile to IR.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { IntentDNA } from "../schema/types.js";
|
|
2
|
+
import type { Diagnostic } from "./diagnostics.js";
|
|
3
|
+
export interface ResolveDNAInputOptions {
|
|
4
|
+
cwd?: string;
|
|
5
|
+
includeDiagnostics?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface ResolvedDNAInputs {
|
|
8
|
+
files: string[];
|
|
9
|
+
dnas: IntentDNA[];
|
|
10
|
+
diagnostics: Diagnostic[];
|
|
11
|
+
}
|
|
12
|
+
export declare function parseDNAFile(filePath: string): Promise<IntentDNA>;
|
|
13
|
+
export declare function resolveSpeciesReference(ref: string): string | null;
|
|
14
|
+
export declare function detectDNAConfigs(projectDir?: string): Promise<string[]>;
|
|
15
|
+
export interface ExpandDNAInputOptions {
|
|
16
|
+
cwd?: string;
|
|
17
|
+
}
|
|
18
|
+
export declare function expandDNAInputFiles(files: string[], options?: ExpandDNAInputOptions): Promise<string[]>;
|
|
19
|
+
export declare function checkNamespaceCollisions(configPaths: string[]): Promise<Diagnostic[]>;
|
|
20
|
+
export declare function assertNoNamespaceCollisions(configPaths: string[]): Promise<void>;
|
|
21
|
+
export declare function loadDNAWithDiagnostics(filePath: string): Promise<{
|
|
22
|
+
dna?: IntentDNA;
|
|
23
|
+
diagnostics: Diagnostic[];
|
|
24
|
+
}>;
|
|
25
|
+
export declare function resolveDNAInputs(files: string[], options?: ResolveDNAInputOptions): Promise<ResolvedDNAInputs>;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { validateDNA } from "../schema/validate.js";
|
|
5
|
+
import { parseYAML } from "../schema/yaml-parser.js";
|
|
6
|
+
import { DNADiagnosticsError, hasDiagnosticErrors } from "./diagnostics.js";
|
|
7
|
+
const DNA_CONFIG_CANDIDATES = [
|
|
8
|
+
".dna/config.yaml",
|
|
9
|
+
".dna/config.yml",
|
|
10
|
+
".dna/config.json",
|
|
11
|
+
".dna.yaml",
|
|
12
|
+
".dna.yml",
|
|
13
|
+
".dna.json",
|
|
14
|
+
];
|
|
15
|
+
async function fileExists(path) {
|
|
16
|
+
try {
|
|
17
|
+
await stat(path);
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function parseDNAContent(filePath, raw) {
|
|
25
|
+
const isYAML = filePath.endsWith(".yaml") || filePath.endsWith(".yml");
|
|
26
|
+
return (isYAML ? parseYAML(raw) : JSON.parse(raw));
|
|
27
|
+
}
|
|
28
|
+
export async function parseDNAFile(filePath) {
|
|
29
|
+
const raw = await readFile(filePath, "utf-8");
|
|
30
|
+
return parseDNAContent(filePath, raw);
|
|
31
|
+
}
|
|
32
|
+
export function resolveSpeciesReference(ref) {
|
|
33
|
+
if (!ref.startsWith("species:"))
|
|
34
|
+
return null;
|
|
35
|
+
const name = ref.slice("species:".length);
|
|
36
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
37
|
+
return resolve(thisDir, "..", "species", `${name}.dna.json`);
|
|
38
|
+
}
|
|
39
|
+
export async function detectDNAConfigs(projectDir = process.cwd()) {
|
|
40
|
+
const configsDir = resolve(projectDir, ".dna", "configs");
|
|
41
|
+
try {
|
|
42
|
+
const files = await readdir(configsDir);
|
|
43
|
+
const configs = files
|
|
44
|
+
.filter((file) => file.endsWith(".yaml") || file.endsWith(".yml"))
|
|
45
|
+
.sort()
|
|
46
|
+
.map((file) => resolve(configsDir, file));
|
|
47
|
+
if (configs.length > 0)
|
|
48
|
+
return configs;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// Directory does not exist.
|
|
52
|
+
}
|
|
53
|
+
for (const candidate of DNA_CONFIG_CANDIDATES) {
|
|
54
|
+
const path = resolve(projectDir, candidate);
|
|
55
|
+
if (await fileExists(path))
|
|
56
|
+
return [path];
|
|
57
|
+
}
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
export async function expandDNAInputFiles(files, options) {
|
|
61
|
+
const cwd = options?.cwd ?? process.cwd();
|
|
62
|
+
const expanded = [];
|
|
63
|
+
const seen = new Set();
|
|
64
|
+
const add = (path, position) => {
|
|
65
|
+
if (seen.has(path))
|
|
66
|
+
return;
|
|
67
|
+
seen.add(path);
|
|
68
|
+
if (position === "front")
|
|
69
|
+
expanded.unshift(path);
|
|
70
|
+
else
|
|
71
|
+
expanded.push(path);
|
|
72
|
+
};
|
|
73
|
+
for (const file of files) {
|
|
74
|
+
const abs = resolve(cwd, file);
|
|
75
|
+
add(abs, "back");
|
|
76
|
+
try {
|
|
77
|
+
const dna = await parseDNAFile(abs);
|
|
78
|
+
for (const ref of dna.cascade?.inherits ?? []) {
|
|
79
|
+
const speciesPath = resolveSpeciesReference(ref);
|
|
80
|
+
if (speciesPath)
|
|
81
|
+
add(speciesPath, "front");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// Validation/loading reports the concrete parse error later.
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return expanded;
|
|
89
|
+
}
|
|
90
|
+
export async function checkNamespaceCollisions(configPaths) {
|
|
91
|
+
const diagnostics = [];
|
|
92
|
+
const namespaces = new Map();
|
|
93
|
+
for (const configPath of configPaths) {
|
|
94
|
+
try {
|
|
95
|
+
const data = await parseDNAFile(configPath);
|
|
96
|
+
const namespace = typeof data.namespace === "string" ? data.namespace : undefined;
|
|
97
|
+
if (!namespace)
|
|
98
|
+
continue;
|
|
99
|
+
const previous = namespaces.get(namespace);
|
|
100
|
+
if (previous) {
|
|
101
|
+
diagnostics.push({
|
|
102
|
+
severity: "error",
|
|
103
|
+
code: "namespace_conflict",
|
|
104
|
+
message: `Namespace "${namespace}" conflict: ${previous} and ${configPath}. Each template must have a unique namespace.`,
|
|
105
|
+
file: configPath,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
namespaces.set(namespace, configPath);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// Parse/validation errors are reported by the load step.
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return diagnostics;
|
|
117
|
+
}
|
|
118
|
+
export async function assertNoNamespaceCollisions(configPaths) {
|
|
119
|
+
const diagnostics = await checkNamespaceCollisions(configPaths);
|
|
120
|
+
if (diagnostics.length > 0) {
|
|
121
|
+
throw new DNADiagnosticsError(diagnostics[0].message, diagnostics);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
export async function loadDNAWithDiagnostics(filePath) {
|
|
125
|
+
const diagnostics = [];
|
|
126
|
+
try {
|
|
127
|
+
const dna = await parseDNAFile(filePath);
|
|
128
|
+
const result = validateDNA(dna);
|
|
129
|
+
for (const warning of result.warnings ?? []) {
|
|
130
|
+
diagnostics.push({
|
|
131
|
+
severity: "warning",
|
|
132
|
+
code: "schema_warning",
|
|
133
|
+
message: warning.message,
|
|
134
|
+
file: filePath,
|
|
135
|
+
path: warning.path,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
for (const error of result.errors) {
|
|
139
|
+
diagnostics.push({
|
|
140
|
+
severity: "error",
|
|
141
|
+
code: "schema_error",
|
|
142
|
+
message: error.message,
|
|
143
|
+
file: filePath,
|
|
144
|
+
path: error.path,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return { dna: result.valid ? dna : undefined, diagnostics };
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
diagnostics.push({
|
|
151
|
+
severity: "error",
|
|
152
|
+
code: "parse_error",
|
|
153
|
+
message: error instanceof Error ? error.message : String(error),
|
|
154
|
+
file: filePath,
|
|
155
|
+
});
|
|
156
|
+
return { diagnostics };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
export async function resolveDNAInputs(files, options) {
|
|
160
|
+
const cwd = options?.cwd ?? process.cwd();
|
|
161
|
+
const inputFiles = files.length > 0 ? files : await detectDNAConfigs(cwd);
|
|
162
|
+
const expandedFiles = await expandDNAInputFiles(inputFiles, { cwd });
|
|
163
|
+
const diagnostics = await checkNamespaceCollisions(expandedFiles);
|
|
164
|
+
const dnas = [];
|
|
165
|
+
for (const file of expandedFiles) {
|
|
166
|
+
const result = await loadDNAWithDiagnostics(file);
|
|
167
|
+
diagnostics.push(...result.diagnostics);
|
|
168
|
+
if (result.dna)
|
|
169
|
+
dnas.push(result.dna);
|
|
170
|
+
}
|
|
171
|
+
if (hasDiagnosticErrors(diagnostics)) {
|
|
172
|
+
throw new DNADiagnosticsError("DNA input resolution failed", diagnostics);
|
|
173
|
+
}
|
|
174
|
+
return { files: expandedFiles, dnas, diagnostics };
|
|
175
|
+
}
|
package/dist/hooks/cli.d.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*
|
|
15
15
|
* Fail-open: all errors → { continue: true, suppressOutput: true }
|
|
16
16
|
*/
|
|
17
|
-
import type { ArtifactFact, ConstraintIR, VerifierSpec } from "../schema/types.js";
|
|
17
|
+
import type { ArtifactFact, ConstraintIR, RoleDef, VerifierSpec } from "../schema/types.js";
|
|
18
18
|
import type { HookEvent, HookOutput } from "./protocol.js";
|
|
19
19
|
import { blockOutput } from "./protocol.js";
|
|
20
20
|
import { readWorkflowState } from "./state.js";
|
|
@@ -39,6 +39,15 @@ export declare function computeSummary(traces: Array<{
|
|
|
39
39
|
reason?: string;
|
|
40
40
|
}>): SessionSummary;
|
|
41
41
|
export declare function formatSummary(s: SessionSummary): string | null;
|
|
42
|
+
export interface RunHookEventOptions {
|
|
43
|
+
event: HookEvent;
|
|
44
|
+
ir: ConstraintIR;
|
|
45
|
+
rawInput: Record<string, unknown>;
|
|
46
|
+
projectDir: string;
|
|
47
|
+
sessionId?: string;
|
|
48
|
+
roles?: Record<string, RoleDef>;
|
|
49
|
+
}
|
|
50
|
+
export declare function runHookEvent(options: RunHookEventOptions): Promise<HookOutput>;
|
|
42
51
|
declare function recordProducedArtifacts(projectDir: string, ir: ConstraintIR, wfState: {
|
|
43
52
|
workflow: string;
|
|
44
53
|
current_step: string;
|
package/dist/hooks/cli.js
CHANGED
|
@@ -15,20 +15,20 @@
|
|
|
15
15
|
* Fail-open: all errors → { continue: true, suppressOutput: true }
|
|
16
16
|
*/
|
|
17
17
|
import { mkdir, readFile, realpath, stat, unlink, writeFile } from "node:fs/promises";
|
|
18
|
+
import { realpathSync } from "node:fs";
|
|
18
19
|
import { spawn } from "node:child_process";
|
|
19
20
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
20
21
|
import { randomUUID } from "node:crypto";
|
|
22
|
+
import { fileURLToPath } from "node:url";
|
|
21
23
|
import { readStdin, writeOutput, silentOutput, allowOutput, blockOutput, stopOutput } from "./protocol.js";
|
|
22
24
|
import { validateHookInput } from "./schema.js";
|
|
23
25
|
import { enforcePreToolUse, enforcePostToolUse, enforceUserPromptSubmit, enforceSubagentStop, enforcePreCompact, enforceNotification, enforceSessionStart, enforceStop, extractBashWritePaths, checkReflectionLimit, checkContextReadiness, checkWorkflowBoundary, } from "./enforce.js";
|
|
24
26
|
import { appendAudit, readWorkflowState, appendTrace, rotateTraces, readTraces, cleanStaleState, readSurgeonAttempts, writeSurgeonAttempts, appendSessionRead, readSessionReads, appendVerifierResult, appendCompletedArtifact, artifactIdentity, buildArtifactKey, readArtifactManifest, resolveArtifactTemplate, safePathComponent, writeArtifactManifest } from "./state.js";
|
|
27
|
+
import { hookEventsForSurface } from "./event-registry.js";
|
|
25
28
|
import { writeAuditEvent } from "../audit/index.js";
|
|
26
29
|
// ── Constants ──────────────────────────────────────────────
|
|
27
30
|
const DEFAULT_IR_PATH = ".dna/compiled/ir.json";
|
|
28
|
-
const VALID_EVENTS = new Set(
|
|
29
|
-
"PreToolUse", "PostToolUse", "UserPromptSubmit",
|
|
30
|
-
"SubagentStop", "PreCompact", "Notification", "Stop", "SessionStart",
|
|
31
|
-
]);
|
|
31
|
+
const VALID_EVENTS = new Set(hookEventsForSurface("cli"));
|
|
32
32
|
export function computeSummary(traces) {
|
|
33
33
|
let blocks = 0, warns = 0;
|
|
34
34
|
const toolCounts = new Map();
|
|
@@ -103,14 +103,18 @@ async function main() {
|
|
|
103
103
|
writeOutput(silentOutput());
|
|
104
104
|
return;
|
|
105
105
|
}
|
|
106
|
+
const output = await runHookEvent({ event, ir, rawInput, projectDir, sessionId });
|
|
107
|
+
writeOutput(output);
|
|
108
|
+
}
|
|
109
|
+
export async function runHookEvent(options) {
|
|
110
|
+
const { event, ir, rawInput, projectDir, sessionId, roles } = options;
|
|
106
111
|
const state = {};
|
|
107
112
|
let wfStateRaw = null;
|
|
108
113
|
// Load workflow state for events that need it (handoff context + PreCompact preservation)
|
|
109
114
|
if (event === "PreToolUse" || event === "PostToolUse" || event === "PreCompact") {
|
|
110
115
|
const workflowState = await readWorkflowStateForHook(projectDir, event, sessionId);
|
|
111
116
|
if ("output" in workflowState) {
|
|
112
|
-
|
|
113
|
-
return;
|
|
117
|
+
return workflowState.output;
|
|
114
118
|
}
|
|
115
119
|
wfStateRaw = workflowState.state;
|
|
116
120
|
if (wfStateRaw && wfStateRaw.active) {
|
|
@@ -123,9 +127,8 @@ async function main() {
|
|
|
123
127
|
};
|
|
124
128
|
const artifactFacts = await resolveWorkflowArtifactFactsForHook(projectDir, ir, wfStateRaw, event, sessionId);
|
|
125
129
|
if ("output" in artifactFacts) {
|
|
126
|
-
writeOutput(artifactFacts.output);
|
|
127
130
|
appendArtifactResolverTrace(projectDir, event, wfStateRaw, artifactFacts.output, sessionId);
|
|
128
|
-
return;
|
|
131
|
+
return artifactFacts.output;
|
|
129
132
|
}
|
|
130
133
|
state.artifactFacts = artifactFacts.facts;
|
|
131
134
|
}
|
|
@@ -141,7 +144,6 @@ async function main() {
|
|
|
141
144
|
if (event === "PreToolUse") {
|
|
142
145
|
const gateResult = await handlePreToolGates(ir, rawInput, wfStateRaw, projectDir, sessionId);
|
|
143
146
|
if (gateResult) {
|
|
144
|
-
writeOutput(gateResult.output);
|
|
145
147
|
appendTrace(projectDir, {
|
|
146
148
|
trace_id: randomUUID(),
|
|
147
149
|
event,
|
|
@@ -154,24 +156,22 @@ async function main() {
|
|
|
154
156
|
duration_ms: 0,
|
|
155
157
|
timestamp: new Date().toISOString(),
|
|
156
158
|
}, sessionId).catch(() => { });
|
|
157
|
-
return;
|
|
159
|
+
return gateResult.output;
|
|
158
160
|
}
|
|
159
161
|
}
|
|
160
162
|
// Special handling for Stop — needs async workflow state read + session summary
|
|
161
163
|
if (event === "Stop") {
|
|
162
164
|
const workflowState = await readWorkflowStateForHook(projectDir, event, sessionId);
|
|
163
165
|
if ("output" in workflowState) {
|
|
164
|
-
|
|
165
|
-
return;
|
|
166
|
+
return workflowState.output;
|
|
166
167
|
}
|
|
167
168
|
const wfState = workflowState.state;
|
|
168
169
|
let stopArtifactFacts = [];
|
|
169
170
|
if (wfState?.active) {
|
|
170
171
|
const artifactFacts = await finalizeAndResolveArtifactsForHook(projectDir, ir, wfState, event, sessionId);
|
|
171
172
|
if ("output" in artifactFacts) {
|
|
172
|
-
writeOutput(artifactFacts.output);
|
|
173
173
|
appendArtifactResolverTrace(projectDir, event, wfState, artifactFacts.output, sessionId);
|
|
174
|
-
return;
|
|
174
|
+
return artifactFacts.output;
|
|
175
175
|
}
|
|
176
176
|
stopArtifactFacts = artifactFacts.facts;
|
|
177
177
|
}
|
|
@@ -219,7 +219,6 @@ async function main() {
|
|
|
219
219
|
}
|
|
220
220
|
}
|
|
221
221
|
catch { /* fail-open */ }
|
|
222
|
-
writeOutput(stopOutput);
|
|
223
222
|
// Trace for Stop
|
|
224
223
|
appendTrace(projectDir, {
|
|
225
224
|
trace_id: randomUUID(),
|
|
@@ -230,11 +229,11 @@ async function main() {
|
|
|
230
229
|
duration_ms: 0,
|
|
231
230
|
timestamp: new Date().toISOString(),
|
|
232
231
|
}, sessionId).catch(() => { });
|
|
233
|
-
return;
|
|
232
|
+
return stopOutput;
|
|
234
233
|
}
|
|
235
234
|
// Dispatch to enforcement engine with timing
|
|
236
235
|
const start = Date.now();
|
|
237
|
-
let result = dispatch(event, ir, rawInput, state);
|
|
236
|
+
let result = dispatch(event, ir, rawInput, state, roles);
|
|
238
237
|
let output = result?.output ?? silentOutput();
|
|
239
238
|
const durationMs = Date.now() - start;
|
|
240
239
|
// PostToolUse side effects: session read tracking + surgeon reflection gate.
|
|
@@ -306,7 +305,6 @@ async function main() {
|
|
|
306
305
|
}
|
|
307
306
|
catch { /* fail-open: pattern detection never blocks */ }
|
|
308
307
|
}
|
|
309
|
-
writeOutput(output);
|
|
310
308
|
// Trace logging (async, fail-open)
|
|
311
309
|
const decision = output.continue === false ? "block"
|
|
312
310
|
: output.hookSpecificOutput?.additionalContext?.startsWith("WARN") ? "warn"
|
|
@@ -341,9 +339,10 @@ async function main() {
|
|
|
341
339
|
session_id: sessionId,
|
|
342
340
|
}).catch(() => { }); // Fail-open
|
|
343
341
|
}
|
|
342
|
+
return output;
|
|
344
343
|
}
|
|
345
344
|
// ── Dispatch ───────────────────────────────────────────────
|
|
346
|
-
function dispatch(event, ir, input, state) {
|
|
345
|
+
function dispatch(event, ir, input, state, roles) {
|
|
347
346
|
switch (event) {
|
|
348
347
|
case "PreToolUse":
|
|
349
348
|
return enforcePreToolUse(ir, {
|
|
@@ -353,7 +352,7 @@ function dispatch(event, ir, input, state) {
|
|
|
353
352
|
cwd: typeof input.cwd === "string" ? input.cwd : undefined,
|
|
354
353
|
sessionId: typeof input.session_id === "string" ? input.session_id
|
|
355
354
|
: typeof input.sessionId === "string" ? input.sessionId : undefined,
|
|
356
|
-
}, state);
|
|
355
|
+
}, state, roles);
|
|
357
356
|
case "PostToolUse":
|
|
358
357
|
return enforcePostToolUse(ir, {
|
|
359
358
|
tool_name: String(input.tool_name ?? ""),
|
|
@@ -576,7 +575,7 @@ async function finalizeAndResolveArtifacts(projectDir, ir, wfState, sessionId) {
|
|
|
576
575
|
}
|
|
577
576
|
function artifactResolverErrorOutput(event, error) {
|
|
578
577
|
const detail = error instanceof Error ? error.message : String(error);
|
|
579
|
-
return blockOutput(`[Intent DNA] ${event} artifact resolver failed: ${detail}
|
|
578
|
+
return blockOutput(`[Intent DNA] ${event} artifact resolver failed: ${detail}. Fix the workflow/session identifiers in DNA state, then rerun the hook or \`dna sync\`.`);
|
|
580
579
|
}
|
|
581
580
|
async function readWorkflowStateForHook(projectDir, event, sessionId) {
|
|
582
581
|
try {
|
|
@@ -691,7 +690,8 @@ export function appendStopVerifierWarnings(output, verifierResults, currentStep)
|
|
|
691
690
|
if (warningVerifierFailures.length === 0)
|
|
692
691
|
return output;
|
|
693
692
|
const warningText = `[Intent DNA] Verifier warnings at step '${currentStep ?? "unknown"}':\n` +
|
|
694
|
-
warningVerifierFailures.map((result) => ` - ${result.message ?? result.verifier_id}`).join("\n")
|
|
693
|
+
warningVerifierFailures.map((result) => ` - ${result.message ?? result.verifier_id}`).join("\n") +
|
|
694
|
+
"\nFix the warning above before relying on this step, or rerun the verifier after repair.";
|
|
695
695
|
return appendOutputText(output, warningText, "Stop");
|
|
696
696
|
}
|
|
697
697
|
function trimEvidence(raw) {
|
|
@@ -1523,7 +1523,20 @@ function toKebabCase(s) {
|
|
|
1523
1523
|
.toLowerCase();
|
|
1524
1524
|
}
|
|
1525
1525
|
// ── Entry Point ────────────────────────────────────────────
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1526
|
+
function isDirectEntryPoint() {
|
|
1527
|
+
const entry = process.argv[1];
|
|
1528
|
+
if (!entry)
|
|
1529
|
+
return false;
|
|
1530
|
+
try {
|
|
1531
|
+
return realpathSync(entry) === realpathSync(fileURLToPath(import.meta.url));
|
|
1532
|
+
}
|
|
1533
|
+
catch {
|
|
1534
|
+
return resolve(entry) === resolve(fileURLToPath(import.meta.url));
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
if (isDirectEntryPoint()) {
|
|
1538
|
+
main().catch(() => {
|
|
1539
|
+
// Fail-open: never block Claude Code on unexpected errors
|
|
1540
|
+
writeOutput(silentOutput());
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare const HOOK_EVENT_NAMES: readonly ["PreToolUse", "PostToolUse", "UserPromptSubmit", "SubagentStop", "PreCompact", "Notification", "Stop", "SessionStart"];
|
|
2
|
+
export type HookEvent = typeof HOOK_EVENT_NAMES[number];
|
|
3
|
+
export declare const SETTINGS_HOOK_EVENT_KEYS: readonly ["preToolUse", "postToolUse", "userPromptSubmit", "subagentStop", "preCompact", "notification", "stop"];
|
|
4
|
+
export type SettingsHookEventKey = typeof SETTINGS_HOOK_EVENT_KEYS[number];
|
|
5
|
+
export type HookSurface = "protocol" | "cli" | "settings" | "manifest" | "mcp";
|
|
6
|
+
export interface HookEventMetadata {
|
|
7
|
+
event: HookEvent;
|
|
8
|
+
settingsKey?: SettingsHookEventKey;
|
|
9
|
+
manifestTimeout: number;
|
|
10
|
+
surfaces: Record<HookSurface, boolean>;
|
|
11
|
+
}
|
|
12
|
+
export declare const HOOK_EVENT_REGISTRY: readonly HookEventMetadata[];
|
|
13
|
+
export declare function getHookEventMetadata(event: unknown): HookEventMetadata | undefined;
|
|
14
|
+
export declare function getHookEventBySettingsKey(key: unknown): HookEventMetadata | undefined;
|
|
15
|
+
export declare function hookEventsForSurface(surface: HookSurface): HookEvent[];
|
|
16
|
+
export declare function settingsHookEventKeys(): SettingsHookEventKey[];
|
|
17
|
+
export declare function isHookEventSupportedBySurface(event: unknown, surface: HookSurface): event is HookEvent;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
export const HOOK_EVENT_NAMES = [
|
|
2
|
+
"PreToolUse",
|
|
3
|
+
"PostToolUse",
|
|
4
|
+
"UserPromptSubmit",
|
|
5
|
+
"SubagentStop",
|
|
6
|
+
"PreCompact",
|
|
7
|
+
"Notification",
|
|
8
|
+
"Stop",
|
|
9
|
+
"SessionStart",
|
|
10
|
+
];
|
|
11
|
+
export const SETTINGS_HOOK_EVENT_KEYS = [
|
|
12
|
+
"preToolUse",
|
|
13
|
+
"postToolUse",
|
|
14
|
+
"userPromptSubmit",
|
|
15
|
+
"subagentStop",
|
|
16
|
+
"preCompact",
|
|
17
|
+
"notification",
|
|
18
|
+
"stop",
|
|
19
|
+
];
|
|
20
|
+
export const HOOK_EVENT_REGISTRY = [
|
|
21
|
+
{
|
|
22
|
+
event: "PreToolUse",
|
|
23
|
+
settingsKey: "preToolUse",
|
|
24
|
+
manifestTimeout: 5,
|
|
25
|
+
surfaces: { protocol: true, cli: true, settings: true, manifest: true, mcp: true },
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
event: "PostToolUse",
|
|
29
|
+
settingsKey: "postToolUse",
|
|
30
|
+
manifestTimeout: 3,
|
|
31
|
+
surfaces: { protocol: true, cli: true, settings: true, manifest: true, mcp: true },
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
event: "UserPromptSubmit",
|
|
35
|
+
settingsKey: "userPromptSubmit",
|
|
36
|
+
manifestTimeout: 5,
|
|
37
|
+
surfaces: { protocol: true, cli: true, settings: true, manifest: true, mcp: true },
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
event: "SubagentStop",
|
|
41
|
+
settingsKey: "subagentStop",
|
|
42
|
+
manifestTimeout: 3,
|
|
43
|
+
surfaces: { protocol: true, cli: true, settings: true, manifest: true, mcp: true },
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
event: "PreCompact",
|
|
47
|
+
settingsKey: "preCompact",
|
|
48
|
+
manifestTimeout: 3,
|
|
49
|
+
surfaces: { protocol: true, cli: true, settings: true, manifest: true, mcp: true },
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
event: "Notification",
|
|
53
|
+
settingsKey: "notification",
|
|
54
|
+
manifestTimeout: 3,
|
|
55
|
+
surfaces: { protocol: true, cli: true, settings: true, manifest: true, mcp: true },
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
event: "Stop",
|
|
59
|
+
settingsKey: "stop",
|
|
60
|
+
manifestTimeout: 45,
|
|
61
|
+
surfaces: { protocol: true, cli: true, settings: true, manifest: true, mcp: false },
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
event: "SessionStart",
|
|
65
|
+
manifestTimeout: 5,
|
|
66
|
+
surfaces: { protocol: true, cli: true, settings: false, manifest: true, mcp: true },
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
const EVENT_BY_NAME = new Map(HOOK_EVENT_REGISTRY.map((entry) => [entry.event, entry]));
|
|
70
|
+
const EVENT_BY_SETTINGS_KEY = new Map(HOOK_EVENT_REGISTRY.flatMap((entry) => entry.settingsKey ? [[entry.settingsKey, entry]] : []));
|
|
71
|
+
export function getHookEventMetadata(event) {
|
|
72
|
+
return typeof event === "string" ? EVENT_BY_NAME.get(event) : undefined;
|
|
73
|
+
}
|
|
74
|
+
export function getHookEventBySettingsKey(key) {
|
|
75
|
+
return typeof key === "string" ? EVENT_BY_SETTINGS_KEY.get(key) : undefined;
|
|
76
|
+
}
|
|
77
|
+
export function hookEventsForSurface(surface) {
|
|
78
|
+
return HOOK_EVENT_REGISTRY
|
|
79
|
+
.filter((entry) => entry.surfaces[surface])
|
|
80
|
+
.map((entry) => entry.event);
|
|
81
|
+
}
|
|
82
|
+
export function settingsHookEventKeys() {
|
|
83
|
+
return HOOK_EVENT_REGISTRY
|
|
84
|
+
.filter((entry) => entry.surfaces.settings && entry.settingsKey)
|
|
85
|
+
.map((entry) => entry.settingsKey);
|
|
86
|
+
}
|
|
87
|
+
export function isHookEventSupportedBySurface(event, surface) {
|
|
88
|
+
return getHookEventMetadata(event)?.surfaces[surface] === true;
|
|
89
|
+
}
|
package/dist/hooks/protocol.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Protocol reference: docs/references/cc-source-analysis.md §2
|
|
8
8
|
*/
|
|
9
|
-
export type HookEvent
|
|
9
|
+
export type { HookEvent } from "./event-registry.js";
|
|
10
10
|
/** Base fields present in all hook inputs */
|
|
11
11
|
export interface HookInputBase {
|
|
12
12
|
cwd?: string;
|
package/dist/hooks/schema.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Zero external dependencies. Hand-written validators.
|
|
4
4
|
* Fail-open: invalid input → log warning + return { valid: false, ... }
|
|
5
5
|
*/
|
|
6
|
+
import { isHookEventSupportedBySurface } from "./event-registry.js";
|
|
6
7
|
/**
|
|
7
8
|
* Validate hook input against expected schema for each event type.
|
|
8
9
|
* Returns normalized input (camelCase) on success.
|
|
@@ -20,6 +21,9 @@ export function validateHookInput(event, raw) {
|
|
|
20
21
|
normalized.sessionId = sessionId;
|
|
21
22
|
delete normalized.session_id;
|
|
22
23
|
}
|
|
24
|
+
if (!isHookEventSupportedBySurface(event, "protocol")) {
|
|
25
|
+
return { valid: false, errors: [`Unknown event type: ${event}`] };
|
|
26
|
+
}
|
|
23
27
|
switch (event) {
|
|
24
28
|
case "PreToolUse":
|
|
25
29
|
if (typeof input.tool_name !== "string") {
|
|
@@ -58,7 +62,7 @@ export function validateHookInput(event, raw) {
|
|
|
58
62
|
// Minimal validation — these events have no required fields beyond base
|
|
59
63
|
break;
|
|
60
64
|
default:
|
|
61
|
-
|
|
65
|
+
break;
|
|
62
66
|
}
|
|
63
67
|
return {
|
|
64
68
|
valid: errors.length === 0,
|
package/dist/hooks/state.d.ts
CHANGED
|
@@ -136,6 +136,7 @@ export declare function writeSessionReads(projectDir: string, state: SessionRead
|
|
|
136
136
|
*/
|
|
137
137
|
export declare function appendSessionRead(projectDir: string, filePath: string, sessionId?: string): Promise<void>;
|
|
138
138
|
export interface VerifierResultEntry {
|
|
139
|
+
result_id?: string;
|
|
139
140
|
verifier_id: string;
|
|
140
141
|
when: VerifierWhen;
|
|
141
142
|
severity: VerifierSeverity;
|
|
@@ -151,6 +152,7 @@ export interface VerifierResultEntry {
|
|
|
151
152
|
message?: string;
|
|
152
153
|
timestamp: string;
|
|
153
154
|
}
|
|
155
|
+
export declare function verifierResultId(result: Omit<VerifierResultEntry, "result_id"> | VerifierResultEntry): string;
|
|
154
156
|
export declare function readVerifierResults(projectDir: string, sessionId?: string): Promise<VerifierResultEntry[]>;
|
|
155
157
|
export declare function writeVerifierResults(projectDir: string, results: VerifierResultEntry[], sessionId?: string): Promise<void>;
|
|
156
158
|
export declare function appendVerifierResult(projectDir: string, result: VerifierResultEntry, sessionId?: string): Promise<void>;
|