intentdna 1.2.3 → 1.4.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/hooks/hooks.json +12 -0
- package/.claude-plugin/marketplace.json +12 -0
- package/.claude-plugin/plugin.json +5 -0
- package/dist/cli/commands/setup.d.ts +21 -0
- package/dist/cli/commands/setup.js +192 -0
- package/dist/cli/commands/sync.d.ts +12 -0
- package/dist/cli/commands/sync.js +115 -15
- package/dist/cli/commands/verify.d.ts +1 -0
- package/dist/cli/commands/verify.js +69 -0
- package/dist/cli/index.js +26 -0
- package/dist/compiler/compile.js +40 -0
- package/dist/compiler/workflow.js +1 -0
- package/dist/hooks/cli.js +70 -4
- package/dist/hooks/enforce.d.ts +56 -1
- package/dist/hooks/enforce.js +160 -0
- package/dist/hooks/index.d.ts +2 -2
- package/dist/hooks/index.js +1 -1
- package/dist/hooks/protocol.d.ts +4 -1
- package/dist/hooks/state.d.ts +40 -1
- package/dist/hooks/state.js +137 -2
- package/dist/runtime/skill-adapter.js +21 -0
- package/dist/schema/types.d.ts +54 -0
- package/dist/schema/validate.js +29 -0
- package/dist/templates/code-review-pipeline.dna.yaml +12 -0
- package/dist/templates/flutter-refactoring-rescue.dna.yaml +20 -0
- package/dist/templates/flutter-rewrite.dna.yaml +39 -0
- package/dist/templates/full-pipeline.dna.yaml +29 -0
- package/package.json +2 -1
package/dist/hooks/cli.js
CHANGED
|
@@ -16,9 +16,10 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { readFile } from "node:fs/promises";
|
|
18
18
|
import { resolve } from "node:path";
|
|
19
|
+
import { randomUUID } from "node:crypto";
|
|
19
20
|
import { readStdin, writeOutput, silentOutput } from "./protocol.js";
|
|
20
|
-
import { enforcePreToolUse, enforcePostToolUse, enforceUserPromptSubmit, enforceSubagentStop, enforcePreCompact, enforceNotification, } from "./enforce.js";
|
|
21
|
-
import { appendAudit } from "./state.js";
|
|
21
|
+
import { enforcePreToolUse, enforcePostToolUse, enforceUserPromptSubmit, enforceSubagentStop, enforcePreCompact, enforceNotification, enforceSessionStart, enforceStop, } from "./enforce.js";
|
|
22
|
+
import { appendAudit, readWorkflowState, appendTrace, rotateTraces } from "./state.js";
|
|
22
23
|
// ── Constants ──────────────────────────────────────────────
|
|
23
24
|
const DEFAULT_IR_PATH = ".dna/compiled/ir.json";
|
|
24
25
|
const VALID_EVENTS = new Set([
|
|
@@ -52,9 +53,70 @@ async function main() {
|
|
|
52
53
|
return;
|
|
53
54
|
}
|
|
54
55
|
const state = {};
|
|
55
|
-
//
|
|
56
|
+
// Load workflow state for events that need handoff context
|
|
57
|
+
if (event === "PreToolUse") {
|
|
58
|
+
const wfState = await readWorkflowState(projectDir, sessionId);
|
|
59
|
+
if (wfState && wfState.active) {
|
|
60
|
+
state.workflowState = {
|
|
61
|
+
current_step: wfState.current_step,
|
|
62
|
+
workflow: wfState.workflow,
|
|
63
|
+
completed_artifacts: wfState.completed_artifacts,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Special handling for Stop — needs async workflow state read
|
|
68
|
+
if (event === "Stop") {
|
|
69
|
+
const wfState = await readWorkflowState(projectDir, sessionId);
|
|
70
|
+
const stopContext = wfState ? {
|
|
71
|
+
active: wfState.active,
|
|
72
|
+
workflow: wfState.workflow,
|
|
73
|
+
current_step: wfState.current_step,
|
|
74
|
+
current_role: wfState.current_role,
|
|
75
|
+
started_at: wfState.started_at,
|
|
76
|
+
completed_artifacts: wfState.completed_artifacts,
|
|
77
|
+
} : null;
|
|
78
|
+
const stopOutput = enforceStop(ir, {
|
|
79
|
+
cwd: typeof rawInput.cwd === "string" ? rawInput.cwd : undefined,
|
|
80
|
+
session_id: sessionId,
|
|
81
|
+
stop_reason: typeof rawInput.stop_reason === "string" ? rawInput.stop_reason : undefined,
|
|
82
|
+
}, stopContext);
|
|
83
|
+
writeOutput(stopOutput);
|
|
84
|
+
// Trace for Stop
|
|
85
|
+
appendTrace(projectDir, {
|
|
86
|
+
trace_id: randomUUID(),
|
|
87
|
+
event: "Stop",
|
|
88
|
+
workflow: wfState?.workflow,
|
|
89
|
+
step: wfState?.current_step,
|
|
90
|
+
decision: stopOutput.continue === false ? "block" : "allow",
|
|
91
|
+
duration_ms: Date.now() - Date.now(), // minimal
|
|
92
|
+
timestamp: new Date().toISOString(),
|
|
93
|
+
}).catch(() => { });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
// Dispatch to enforcement engine with timing
|
|
97
|
+
const start = Date.now();
|
|
56
98
|
const output = dispatch(event, ir, rawInput, state);
|
|
99
|
+
const durationMs = Date.now() - start;
|
|
57
100
|
writeOutput(output);
|
|
101
|
+
// Trace logging (async, fail-open)
|
|
102
|
+
const decision = output.continue === false ? "block"
|
|
103
|
+
: output.hookSpecificOutput?.additionalContext?.startsWith("WARN") ? "warn"
|
|
104
|
+
: "allow";
|
|
105
|
+
appendTrace(projectDir, {
|
|
106
|
+
trace_id: randomUUID(),
|
|
107
|
+
event,
|
|
108
|
+
tool_name: typeof rawInput.tool_name === "string" ? rawInput.tool_name : undefined,
|
|
109
|
+
agent_type: typeof rawInput.agent_type === "string" ? rawInput.agent_type : undefined,
|
|
110
|
+
workflow: state.workflowState?.workflow,
|
|
111
|
+
step: state.workflowState?.current_step,
|
|
112
|
+
decision: decision,
|
|
113
|
+
duration_ms: durationMs,
|
|
114
|
+
timestamp: new Date().toISOString(),
|
|
115
|
+
}).catch(() => { }); // Fail-open
|
|
116
|
+
// Side effect: rotate traces on SessionStart
|
|
117
|
+
if (event === "SessionStart") {
|
|
118
|
+
rotateTraces(projectDir).catch(() => { });
|
|
119
|
+
}
|
|
58
120
|
// Side effect: audit log for notifications
|
|
59
121
|
if (event === "Notification" && output.hookSpecificOutput?.additionalContext?.includes("violation")) {
|
|
60
122
|
const message = typeof rawInput.message === "string" ? rawInput.message : "";
|
|
@@ -103,7 +165,11 @@ function dispatch(event, ir, input, state) {
|
|
|
103
165
|
case "Stop":
|
|
104
166
|
return silentOutput(); // DNA doesn't block stops
|
|
105
167
|
case "SessionStart":
|
|
106
|
-
return
|
|
168
|
+
return enforceSessionStart(ir, {
|
|
169
|
+
cwd: typeof input.cwd === "string" ? input.cwd : undefined,
|
|
170
|
+
session_id: typeof input.sessionId === "string" ? input.sessionId : undefined,
|
|
171
|
+
trigger: typeof input.trigger === "string" ? input.trigger : undefined,
|
|
172
|
+
});
|
|
107
173
|
default:
|
|
108
174
|
return silentOutput();
|
|
109
175
|
}
|
package/dist/hooks/enforce.d.ts
CHANGED
|
@@ -13,9 +13,21 @@
|
|
|
13
13
|
*
|
|
14
14
|
* Step checkpoints are handled by the CLI, not here.
|
|
15
15
|
*/
|
|
16
|
-
import type { ConstraintIR, RoleDef } from "../schema/types.js";
|
|
16
|
+
import type { ConstraintIR, RoleDef, CompletedArtifactEntry } from "../schema/types.js";
|
|
17
17
|
import type { PreToolUseInput, PostToolUseInput, UserPromptSubmitInput, SubagentStopInput, NotificationInput, HookOutput } from "./protocol.js";
|
|
18
|
+
/** SessionStart input fields */
|
|
19
|
+
export interface SessionStartInput {
|
|
20
|
+
cwd?: string;
|
|
21
|
+
session_id?: string;
|
|
22
|
+
trigger?: string;
|
|
23
|
+
}
|
|
18
24
|
export interface EnforceState {
|
|
25
|
+
/** Workflow handoff context — passed from CLI after reading workflow state */
|
|
26
|
+
workflowState?: {
|
|
27
|
+
current_step: string;
|
|
28
|
+
workflow: string;
|
|
29
|
+
completed_artifacts?: CompletedArtifactEntry[];
|
|
30
|
+
};
|
|
19
31
|
}
|
|
20
32
|
/**
|
|
21
33
|
* Enforce PreToolUse constraints.
|
|
@@ -47,6 +59,49 @@ export declare function enforcePreCompact(ir: ConstraintIR): HookOutput;
|
|
|
47
59
|
* The CLI handles actual audit file writes.
|
|
48
60
|
*/
|
|
49
61
|
export declare function enforceNotification(ir: ConstraintIR, input: NotificationInput): HookOutput;
|
|
62
|
+
/**
|
|
63
|
+
* Initialize DNA state and inject policy summary on session start.
|
|
64
|
+
* Returns silent if no DNA policies are active.
|
|
65
|
+
*/
|
|
66
|
+
export declare function enforceSessionStart(ir: ConstraintIR, input: SessionStartInput): HookOutput;
|
|
67
|
+
/** Stop enforcement input — includes workflow awareness */
|
|
68
|
+
export interface StopEnforceInput {
|
|
69
|
+
cwd?: string;
|
|
70
|
+
session_id?: string;
|
|
71
|
+
stop_reason?: string;
|
|
72
|
+
}
|
|
73
|
+
/** Workflow state context for stop enforcement */
|
|
74
|
+
export interface StopWorkflowContext {
|
|
75
|
+
active: boolean;
|
|
76
|
+
workflow: string;
|
|
77
|
+
current_step: string;
|
|
78
|
+
current_role: string;
|
|
79
|
+
started_at: string;
|
|
80
|
+
completed_artifacts?: CompletedArtifactEntry[];
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Enforce Stop hook — verify workflow checkpoint completion.
|
|
84
|
+
*
|
|
85
|
+
* Safety valves (NEVER block):
|
|
86
|
+
* - context_limit / context_window stops
|
|
87
|
+
* - Stale workflow state (>2h)
|
|
88
|
+
* - No active workflow
|
|
89
|
+
* - No checkpoints defined
|
|
90
|
+
*
|
|
91
|
+
* Block when:
|
|
92
|
+
* - Workflow active + unmet checkpoints for current step
|
|
93
|
+
*/
|
|
94
|
+
export declare function enforceStop(ir: ConstraintIR, input: StopEnforceInput, workflowState: StopWorkflowContext | null): HookOutput;
|
|
95
|
+
/**
|
|
96
|
+
* Enforce handoff produces — verify that the current step has produced
|
|
97
|
+
* its declared artifacts. Called at stop/checkpoint time.
|
|
98
|
+
* Returns block output if a required artifact is missing, null if all produced.
|
|
99
|
+
*/
|
|
100
|
+
export declare function enforceHandoffProduces(ir: ConstraintIR, wfState: {
|
|
101
|
+
current_step: string;
|
|
102
|
+
workflow: string;
|
|
103
|
+
completed_artifacts?: CompletedArtifactEntry[];
|
|
104
|
+
}): HookOutput | null;
|
|
50
105
|
/** Check if a file path is allowed by a list of write globs (prefix matching). */
|
|
51
106
|
export declare function checkWriteAllowed(filePath: string, allowedGlobs: string[]): boolean;
|
|
52
107
|
/**
|
package/dist/hooks/enforce.js
CHANGED
|
@@ -46,6 +46,12 @@ export function enforcePreToolUse(ir, input, state, roles) {
|
|
|
46
46
|
if (result)
|
|
47
47
|
return result;
|
|
48
48
|
}
|
|
49
|
+
// Layer 6: Handoff — check consumed artifacts are available
|
|
50
|
+
if (state?.workflowState && ir.workflows_ir) {
|
|
51
|
+
const result = enforceHandoffConsumes(ir, state.workflowState);
|
|
52
|
+
if (result)
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
49
55
|
return silentOutput();
|
|
50
56
|
}
|
|
51
57
|
// ── PostToolUse Enforcement ────────────────────────────────
|
|
@@ -145,7 +151,161 @@ export function enforceNotification(ir, input) {
|
|
|
145
151
|
}
|
|
146
152
|
return silentOutput();
|
|
147
153
|
}
|
|
154
|
+
// ── SessionStart Enforcement ───────────────────────────────
|
|
155
|
+
/**
|
|
156
|
+
* Initialize DNA state and inject policy summary on session start.
|
|
157
|
+
* Returns silent if no DNA policies are active.
|
|
158
|
+
*/
|
|
159
|
+
export function enforceSessionStart(ir, input) {
|
|
160
|
+
// Check if there's anything meaningful to report
|
|
161
|
+
const directiveCount = ir.prompt_directives.length;
|
|
162
|
+
const gateCount = ir.pre_execution_gates.length;
|
|
163
|
+
const filterCount = ir.tool_filters.length;
|
|
164
|
+
const roleCount = ir.roles_scope_map?.length ?? 0;
|
|
165
|
+
const workflowCount = ir.workflows_ir?.length ?? 0;
|
|
166
|
+
if (directiveCount === 0 && gateCount === 0 && filterCount === 0 && roleCount === 0) {
|
|
167
|
+
return silentOutput();
|
|
168
|
+
}
|
|
169
|
+
// Build policy summary
|
|
170
|
+
const lines = [
|
|
171
|
+
"[Intent DNA] Session initialized — active governance:",
|
|
172
|
+
];
|
|
173
|
+
if (ir.source_dna_ids.length > 0) {
|
|
174
|
+
lines.push(` Templates: ${ir.source_dna_ids.join(", ")}`);
|
|
175
|
+
}
|
|
176
|
+
const stats = [];
|
|
177
|
+
if (directiveCount > 0)
|
|
178
|
+
stats.push(`${directiveCount} directives`);
|
|
179
|
+
if (gateCount > 0)
|
|
180
|
+
stats.push(`${gateCount} gates`);
|
|
181
|
+
if (filterCount > 0)
|
|
182
|
+
stats.push(`${filterCount} filters`);
|
|
183
|
+
if (roleCount > 0)
|
|
184
|
+
stats.push(`${roleCount} roles`);
|
|
185
|
+
if (workflowCount > 0)
|
|
186
|
+
stats.push(`${workflowCount} workflows`);
|
|
187
|
+
if (stats.length > 0) {
|
|
188
|
+
lines.push(` Enforcement: ${stats.join(", ")}`);
|
|
189
|
+
}
|
|
190
|
+
return allowOutput(lines.join("\n"));
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Enforce Stop hook — verify workflow checkpoint completion.
|
|
194
|
+
*
|
|
195
|
+
* Safety valves (NEVER block):
|
|
196
|
+
* - context_limit / context_window stops
|
|
197
|
+
* - Stale workflow state (>2h)
|
|
198
|
+
* - No active workflow
|
|
199
|
+
* - No checkpoints defined
|
|
200
|
+
*
|
|
201
|
+
* Block when:
|
|
202
|
+
* - Workflow active + unmet checkpoints for current step
|
|
203
|
+
*/
|
|
204
|
+
export function enforceStop(ir, input, workflowState) {
|
|
205
|
+
// Safety valve 1: no workflow state → silent
|
|
206
|
+
if (!workflowState || !workflowState.active) {
|
|
207
|
+
return silentOutput();
|
|
208
|
+
}
|
|
209
|
+
// Safety valve 2: context_limit stops → NEVER block
|
|
210
|
+
const reason = input.stop_reason ?? "";
|
|
211
|
+
if (reason.includes("context_limit") || reason.includes("context_window")) {
|
|
212
|
+
return silentOutput();
|
|
213
|
+
}
|
|
214
|
+
// Safety valve 3: user abort → don't block
|
|
215
|
+
if (reason === "user_abort" || reason === "sigint") {
|
|
216
|
+
return silentOutput();
|
|
217
|
+
}
|
|
218
|
+
// Safety valve 4: stale state (>2h)
|
|
219
|
+
const STALE_MS = 2 * 60 * 60 * 1000;
|
|
220
|
+
if (workflowState.started_at) {
|
|
221
|
+
const age = Date.now() - new Date(workflowState.started_at).getTime();
|
|
222
|
+
if (age > STALE_MS) {
|
|
223
|
+
return silentOutput();
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// Check for unmet checkpoints in current step
|
|
227
|
+
const checkpoints = ir.step_checkpoints ?? [];
|
|
228
|
+
const currentStepCheckpoints = checkpoints.filter(cp => cp.step_id === workflowState.current_step);
|
|
229
|
+
// Also check workflows_ir for the active workflow's checkpoints
|
|
230
|
+
let workflowCheckpoints = currentStepCheckpoints;
|
|
231
|
+
if (ir.workflows_ir && ir.workflows_ir.length > 0) {
|
|
232
|
+
const activeWf = ir.workflows_ir.find(w => w.workflow_name === workflowState.workflow);
|
|
233
|
+
if (activeWf) {
|
|
234
|
+
const wfStepCps = activeWf.step_checkpoints.filter(cp => cp.step_id === workflowState.current_step);
|
|
235
|
+
if (wfStepCps.length > 0) {
|
|
236
|
+
workflowCheckpoints = wfStepCps;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (workflowCheckpoints.length === 0) {
|
|
241
|
+
return silentOutput(); // No checkpoints for current step
|
|
242
|
+
}
|
|
243
|
+
// Build list of blocking checkpoints
|
|
244
|
+
const blocking = workflowCheckpoints.flatMap(cp => cp.checkpoints.filter(c => (c.action ?? "block") === "block"));
|
|
245
|
+
if (blocking.length === 0) {
|
|
246
|
+
// No checkpoint blocking — check handoff produces
|
|
247
|
+
if (workflowState.workflow) {
|
|
248
|
+
const handoffResult = enforceHandoffProduces(ir, {
|
|
249
|
+
current_step: workflowState.current_step,
|
|
250
|
+
workflow: workflowState.workflow,
|
|
251
|
+
completed_artifacts: workflowState.completed_artifacts,
|
|
252
|
+
});
|
|
253
|
+
if (handoffResult && !handoffResult.continue)
|
|
254
|
+
return handoffResult;
|
|
255
|
+
}
|
|
256
|
+
return silentOutput();
|
|
257
|
+
}
|
|
258
|
+
// Block: workflow active with unmet checkpoints
|
|
259
|
+
const messages = blocking.map(c => c.message);
|
|
260
|
+
return blockOutput(`[Intent DNA] Workflow '${workflowState.workflow}' has unmet checkpoints at step '${workflowState.current_step}':\n` +
|
|
261
|
+
messages.map(m => ` - ${m}`).join("\n"));
|
|
262
|
+
}
|
|
148
263
|
// ── Internal: Layer Enforcement ────────────────────────────
|
|
264
|
+
/**
|
|
265
|
+
* Enforce handoff consumes — verify that the current step's consumed
|
|
266
|
+
* artifacts have been produced by a preceding step.
|
|
267
|
+
* Returns block output if a required artifact is missing, null if all satisfied.
|
|
268
|
+
*/
|
|
269
|
+
function enforceHandoffConsumes(ir, wfState) {
|
|
270
|
+
const activeWf = ir.workflows_ir?.find(w => w.workflow_name === wfState.workflow);
|
|
271
|
+
if (!activeWf || activeWf.handoff_chain.length === 0)
|
|
272
|
+
return null;
|
|
273
|
+
const currentEntry = activeWf.handoff_chain.find(h => h.step_id === wfState.current_step);
|
|
274
|
+
if (!currentEntry?.consumes || currentEntry.consumes.length === 0)
|
|
275
|
+
return null;
|
|
276
|
+
const completedStepIds = new Set((wfState.completed_artifacts ?? []).map(a => a.step_id));
|
|
277
|
+
// For each consumed artifact, find the producing step and check it completed
|
|
278
|
+
for (const consumed of currentEntry.consumes) {
|
|
279
|
+
// Find which step produces this artifact
|
|
280
|
+
const producer = activeWf.handoff_chain.find(h => h.produces?.some(p => p.type === consumed.type && ((p.path && consumed.path && p.path === consumed.path) ||
|
|
281
|
+
(consumed.from && h.step_id === consumed.from))));
|
|
282
|
+
if (producer && !completedStepIds.has(producer.step_id)) {
|
|
283
|
+
return blockOutput(`[Intent DNA] Step '${wfState.current_step}' requires artifact from step '${producer.step_id}' (${consumed.description}). Run step '${producer.step_id}' first.`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Enforce handoff produces — verify that the current step has produced
|
|
290
|
+
* its declared artifacts. Called at stop/checkpoint time.
|
|
291
|
+
* Returns block output if a required artifact is missing, null if all produced.
|
|
292
|
+
*/
|
|
293
|
+
export function enforceHandoffProduces(ir, wfState) {
|
|
294
|
+
const activeWf = ir.workflows_ir?.find(w => w.workflow_name === wfState.workflow);
|
|
295
|
+
if (!activeWf || activeWf.handoff_chain.length === 0)
|
|
296
|
+
return null;
|
|
297
|
+
const currentEntry = activeWf.handoff_chain.find(h => h.step_id === wfState.current_step);
|
|
298
|
+
if (!currentEntry?.produces || currentEntry.produces.length === 0)
|
|
299
|
+
return null;
|
|
300
|
+
const currentArtifacts = (wfState.completed_artifacts ?? []).find(a => a.step_id === wfState.current_step);
|
|
301
|
+
// Check that each produced artifact with a path has been recorded
|
|
302
|
+
for (const produced of currentEntry.produces) {
|
|
303
|
+
if (produced.path && (!currentArtifacts || !currentArtifacts.artifacts.some(a => a.path === produced.path))) {
|
|
304
|
+
return blockOutput(`[Intent DNA] Step '${wfState.current_step}' must produce artifact '${produced.description}' (path: ${produced.path}) before proceeding.`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
149
309
|
function enforceRoleScope(rolesScopeMap, input) {
|
|
150
310
|
if (!input.agent_type)
|
|
151
311
|
return null;
|
package/dist/hooks/index.d.ts
CHANGED
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
* Public exports for the hook enforcement engine.
|
|
5
5
|
* Used by the `dna-hook` CLI and importable for programmatic use.
|
|
6
6
|
*/
|
|
7
|
-
export { type HookEvent, type HookInput, type HookOutput, type HookInputBase, type PreToolUseInput, type PostToolUseInput, type UserPromptSubmitInput, type SubagentStopInput, type PreCompactInput, type NotificationInput, type StopInput, readStdin, writeOutput, allowOutput, blockOutput, escalateOutput, silentOutput, } from "./protocol.js";
|
|
8
|
-
export { type EnforceState, enforcePreToolUse, enforcePostToolUse, enforceUserPromptSubmit, enforceSubagentStop, enforcePreCompact, enforceNotification, checkWriteAllowed, globToPrefix, evaluateGateCondition, resolveToolTarget, } from "./enforce.js";
|
|
7
|
+
export { type HookEvent, type HookInput, type HookOutput, type HookInputBase, type PreToolUseInput, type PostToolUseInput, type UserPromptSubmitInput, type SubagentStopInput, type PreCompactInput, type NotificationInput, type StopInput, type SessionStartInput as SessionStartHookInput, readStdin, writeOutput, allowOutput, blockOutput, escalateOutput, silentOutput, } from "./protocol.js";
|
|
8
|
+
export { type EnforceState, type SessionStartInput, type StopEnforceInput, type StopWorkflowContext, enforcePreToolUse, enforcePostToolUse, enforceUserPromptSubmit, enforceSubagentStop, enforcePreCompact, enforceNotification, enforceSessionStart, enforceStop, checkWriteAllowed, globToPrefix, evaluateGateCondition, resolveToolTarget, } from "./enforce.js";
|
|
9
9
|
export { type DNAWorkflowState, type AuditEntry, resolveStateDir, readWorkflowState, writeWorkflowState, clearWorkflowState, appendAudit, } from "./state.js";
|
package/dist/hooks/index.js
CHANGED
|
@@ -7,6 +7,6 @@
|
|
|
7
7
|
// Protocol types and I/O
|
|
8
8
|
export { readStdin, writeOutput, allowOutput, blockOutput, escalateOutput, silentOutput, } from "./protocol.js";
|
|
9
9
|
// Enforcement engine
|
|
10
|
-
export { enforcePreToolUse, enforcePostToolUse, enforceUserPromptSubmit, enforceSubagentStop, enforcePreCompact, enforceNotification, checkWriteAllowed, globToPrefix, evaluateGateCondition, resolveToolTarget, } from "./enforce.js";
|
|
10
|
+
export { enforcePreToolUse, enforcePostToolUse, enforceUserPromptSubmit, enforceSubagentStop, enforcePreCompact, enforceNotification, enforceSessionStart, enforceStop, checkWriteAllowed, globToPrefix, evaluateGateCondition, resolveToolTarget, } from "./enforce.js";
|
|
11
11
|
// State management
|
|
12
12
|
export { resolveStateDir, readWorkflowState, writeWorkflowState, clearWorkflowState, appendAudit, } from "./state.js";
|
package/dist/hooks/protocol.d.ts
CHANGED
|
@@ -39,7 +39,10 @@ export interface NotificationInput extends HookInputBase {
|
|
|
39
39
|
export interface StopInput extends HookInputBase {
|
|
40
40
|
stop_reason?: string;
|
|
41
41
|
}
|
|
42
|
-
export
|
|
42
|
+
export interface SessionStartInput extends HookInputBase {
|
|
43
|
+
trigger?: string;
|
|
44
|
+
}
|
|
45
|
+
export type HookInput = PreToolUseInput | PostToolUseInput | UserPromptSubmitInput | SubagentStopInput | PreCompactInput | NotificationInput | StopInput | SessionStartInput;
|
|
43
46
|
export interface HookOutput {
|
|
44
47
|
continue: boolean;
|
|
45
48
|
decision?: "block" | "allow";
|
package/dist/hooks/state.d.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* All read operations are fail-safe (return null on error, never throw).
|
|
10
10
|
* All write operations use atomic temp+rename pattern.
|
|
11
11
|
*/
|
|
12
|
+
import type { CompletedArtifactEntry } from "../schema/types.js";
|
|
12
13
|
/** Workflow state written by runtime and read by hooks */
|
|
13
14
|
export interface DNAWorkflowState {
|
|
14
15
|
active: boolean;
|
|
@@ -18,6 +19,7 @@ export interface DNAWorkflowState {
|
|
|
18
19
|
iteration: number;
|
|
19
20
|
session_id: string;
|
|
20
21
|
started_at: string;
|
|
22
|
+
completed_artifacts?: CompletedArtifactEntry[];
|
|
21
23
|
}
|
|
22
24
|
/** Audit log entry (one per line in JSON Lines format) */
|
|
23
25
|
export interface AuditEntry {
|
|
@@ -47,7 +49,44 @@ export declare function writeWorkflowState(projectDir: string, state: DNAWorkflo
|
|
|
47
49
|
*/
|
|
48
50
|
export declare function clearWorkflowState(projectDir: string, sessionId?: string): Promise<void>;
|
|
49
51
|
/**
|
|
50
|
-
* Append an entry to the audit log.
|
|
52
|
+
* Append an entry to the audit log with dedup protection.
|
|
53
|
+
* Dedup key: event + tool_name + timestamp (second-level).
|
|
51
54
|
* Log file: `.dna/audit/violations-YYYY-MM-DD.log` (JSON Lines format)
|
|
52
55
|
*/
|
|
53
56
|
export declare function appendAudit(projectDir: string, entry: AuditEntry): Promise<void>;
|
|
57
|
+
/**
|
|
58
|
+
* Record a completed artifact for a workflow step.
|
|
59
|
+
* Reads current workflow state, appends the artifact, writes back atomically.
|
|
60
|
+
*/
|
|
61
|
+
export declare function appendCompletedArtifact(projectDir: string, stepId: string, artifact: {
|
|
62
|
+
type: string;
|
|
63
|
+
path: string;
|
|
64
|
+
}, sessionId?: string): Promise<void>;
|
|
65
|
+
/** Trace entry for hook call observability */
|
|
66
|
+
export interface TraceEntry {
|
|
67
|
+
trace_id: string;
|
|
68
|
+
event: string;
|
|
69
|
+
tool_name?: string;
|
|
70
|
+
agent_type?: string;
|
|
71
|
+
workflow?: string;
|
|
72
|
+
step?: string;
|
|
73
|
+
decision: "allow" | "block" | "warn";
|
|
74
|
+
duration_ms: number;
|
|
75
|
+
timestamp: string;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Append a trace entry to the daily trace file.
|
|
79
|
+
* File: `.dna/state/trace/trace-YYYY-MM-DD.jsonl`
|
|
80
|
+
* Fail-open: never throws.
|
|
81
|
+
*/
|
|
82
|
+
export declare function appendTrace(projectDir: string, entry: TraceEntry): Promise<void>;
|
|
83
|
+
/**
|
|
84
|
+
* Read trace entries from the last N days.
|
|
85
|
+
* Returns parsed entries sorted by timestamp.
|
|
86
|
+
*/
|
|
87
|
+
export declare function readTraces(projectDir: string, days?: number): Promise<TraceEntry[]>;
|
|
88
|
+
/**
|
|
89
|
+
* Clean up trace files older than retention period.
|
|
90
|
+
* Removes `.dna/state/trace/trace-*.jsonl` files older than TRACE_RETENTION_DAYS.
|
|
91
|
+
*/
|
|
92
|
+
export declare function rotateTraces(projectDir: string): Promise<number>;
|
package/dist/hooks/state.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* All read operations are fail-safe (return null on error, never throw).
|
|
10
10
|
* All write operations use atomic temp+rename pattern.
|
|
11
11
|
*/
|
|
12
|
-
import { readFile, writeFile, rename, mkdir, appendFile, unlink } from "node:fs/promises";
|
|
12
|
+
import { readFile, writeFile, rename, mkdir, appendFile, unlink, readdir, stat } from "node:fs/promises";
|
|
13
13
|
import { join, dirname } from "node:path";
|
|
14
14
|
// ── Constants ──────────────────────────────────────────────
|
|
15
15
|
const WORKFLOW_FILE = "workflow.json";
|
|
@@ -73,7 +73,8 @@ export async function clearWorkflowState(projectDir, sessionId) {
|
|
|
73
73
|
}
|
|
74
74
|
// ── Audit Log ──────────────────────────────────────────────
|
|
75
75
|
/**
|
|
76
|
-
* Append an entry to the audit log.
|
|
76
|
+
* Append an entry to the audit log with dedup protection.
|
|
77
|
+
* Dedup key: event + tool_name + timestamp (second-level).
|
|
77
78
|
* Log file: `.dna/audit/violations-YYYY-MM-DD.log` (JSON Lines format)
|
|
78
79
|
*/
|
|
79
80
|
export async function appendAudit(projectDir, entry) {
|
|
@@ -81,8 +82,52 @@ export async function appendAudit(projectDir, entry) {
|
|
|
81
82
|
await mkdir(auditDir, { recursive: true });
|
|
82
83
|
const date = entry.timestamp.slice(0, 10); // YYYY-MM-DD
|
|
83
84
|
const logPath = join(auditDir, `violations-${date}.log`);
|
|
85
|
+
// Dedup: check if identical entry (same event+tool+second) already exists
|
|
86
|
+
const dedupKey = `${entry.event}|${entry.tool_name ?? ""}|${entry.timestamp.slice(0, 19)}`;
|
|
87
|
+
try {
|
|
88
|
+
const existing = await readFile(logPath, "utf-8");
|
|
89
|
+
const lines = existing.trimEnd().split("\n");
|
|
90
|
+
// Check last 10 lines for dedup (avoid scanning entire file)
|
|
91
|
+
const recentLines = lines.slice(-10);
|
|
92
|
+
for (const line of recentLines) {
|
|
93
|
+
try {
|
|
94
|
+
const prev = JSON.parse(line);
|
|
95
|
+
const prevKey = `${prev.event}|${prev.tool_name ?? ""}|${prev.timestamp.slice(0, 19)}`;
|
|
96
|
+
if (prevKey === dedupKey)
|
|
97
|
+
return; // Already logged
|
|
98
|
+
}
|
|
99
|
+
catch { /* skip malformed lines */ }
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch { /* file doesn't exist yet */ }
|
|
84
103
|
await appendFile(logPath, JSON.stringify(entry) + "\n", "utf-8");
|
|
85
104
|
}
|
|
105
|
+
// ── Completed Artifacts ───────────────────────────────────
|
|
106
|
+
/**
|
|
107
|
+
* Record a completed artifact for a workflow step.
|
|
108
|
+
* Reads current workflow state, appends the artifact, writes back atomically.
|
|
109
|
+
*/
|
|
110
|
+
export async function appendCompletedArtifact(projectDir, stepId, artifact, sessionId) {
|
|
111
|
+
const state = await readWorkflowState(projectDir, sessionId, 0);
|
|
112
|
+
if (!state)
|
|
113
|
+
return;
|
|
114
|
+
const artifacts = state.completed_artifacts ?? [];
|
|
115
|
+
let stepEntry = artifacts.find(a => a.step_id === stepId);
|
|
116
|
+
if (!stepEntry) {
|
|
117
|
+
stepEntry = { step_id: stepId, artifacts: [] };
|
|
118
|
+
artifacts.push(stepEntry);
|
|
119
|
+
}
|
|
120
|
+
// Dedup: don't add if same path already recorded
|
|
121
|
+
if (stepEntry.artifacts.some(a => a.path === artifact.path))
|
|
122
|
+
return;
|
|
123
|
+
stepEntry.artifacts.push({
|
|
124
|
+
type: artifact.type,
|
|
125
|
+
path: artifact.path,
|
|
126
|
+
verified_at: new Date().toISOString(),
|
|
127
|
+
});
|
|
128
|
+
state.completed_artifacts = artifacts;
|
|
129
|
+
await writeWorkflowState(projectDir, state, sessionId);
|
|
130
|
+
}
|
|
86
131
|
// ── Internal ───────────────────────────────────────────────
|
|
87
132
|
/** Atomic write: write to temp file then rename. */
|
|
88
133
|
async function atomicWrite(filePath, data) {
|
|
@@ -91,3 +136,93 @@ async function atomicWrite(filePath, data) {
|
|
|
91
136
|
await writeFile(tmpPath, data, "utf-8");
|
|
92
137
|
await rename(tmpPath, filePath);
|
|
93
138
|
}
|
|
139
|
+
const TRACE_DIR = "trace";
|
|
140
|
+
const MAX_TRACE_SIZE = 10 * 1024 * 1024; // 10MB
|
|
141
|
+
const TRACE_RETENTION_DAYS = 7;
|
|
142
|
+
/**
|
|
143
|
+
* Append a trace entry to the daily trace file.
|
|
144
|
+
* File: `.dna/state/trace/trace-YYYY-MM-DD.jsonl`
|
|
145
|
+
* Fail-open: never throws.
|
|
146
|
+
*/
|
|
147
|
+
export async function appendTrace(projectDir, entry) {
|
|
148
|
+
try {
|
|
149
|
+
const traceDir = join(projectDir, ".dna", "state", TRACE_DIR);
|
|
150
|
+
await mkdir(traceDir, { recursive: true });
|
|
151
|
+
const date = entry.timestamp.slice(0, 10);
|
|
152
|
+
const tracePath = join(traceDir, `trace-${date}.jsonl`);
|
|
153
|
+
// Check file size — rotate if over limit
|
|
154
|
+
try {
|
|
155
|
+
const stats = await stat(tracePath);
|
|
156
|
+
if (stats.size >= MAX_TRACE_SIZE)
|
|
157
|
+
return; // silently skip if file too large
|
|
158
|
+
}
|
|
159
|
+
catch { /* file doesn't exist yet */ }
|
|
160
|
+
await appendFile(tracePath, JSON.stringify(entry) + "\n", "utf-8");
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
// Fail-open: trace write failure never affects hook execution
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Read trace entries from the last N days.
|
|
168
|
+
* Returns parsed entries sorted by timestamp.
|
|
169
|
+
*/
|
|
170
|
+
export async function readTraces(projectDir, days = 1) {
|
|
171
|
+
const traceDir = join(projectDir, ".dna", "state", TRACE_DIR);
|
|
172
|
+
const entries = [];
|
|
173
|
+
const cutoff = new Date();
|
|
174
|
+
cutoff.setDate(cutoff.getDate() - days);
|
|
175
|
+
const cutoffDate = cutoff.toISOString().slice(0, 10);
|
|
176
|
+
try {
|
|
177
|
+
const files = await readdir(traceDir);
|
|
178
|
+
const traceFiles = files
|
|
179
|
+
.filter(f => f.startsWith("trace-") && f.endsWith(".jsonl"))
|
|
180
|
+
.filter(f => {
|
|
181
|
+
const fileDate = f.slice(6, 16); // "trace-YYYY-MM-DD.jsonl" → "YYYY-MM-DD"
|
|
182
|
+
return fileDate >= cutoffDate;
|
|
183
|
+
})
|
|
184
|
+
.sort();
|
|
185
|
+
for (const file of traceFiles) {
|
|
186
|
+
const content = await readFile(join(traceDir, file), "utf-8");
|
|
187
|
+
for (const line of content.trim().split("\n")) {
|
|
188
|
+
if (!line)
|
|
189
|
+
continue;
|
|
190
|
+
try {
|
|
191
|
+
entries.push(JSON.parse(line));
|
|
192
|
+
}
|
|
193
|
+
catch { /* skip malformed */ }
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
// No trace dir yet
|
|
199
|
+
}
|
|
200
|
+
return entries;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Clean up trace files older than retention period.
|
|
204
|
+
* Removes `.dna/state/trace/trace-*.jsonl` files older than TRACE_RETENTION_DAYS.
|
|
205
|
+
*/
|
|
206
|
+
export async function rotateTraces(projectDir) {
|
|
207
|
+
const traceDir = join(projectDir, ".dna", "state", TRACE_DIR);
|
|
208
|
+
let removed = 0;
|
|
209
|
+
const cutoff = new Date();
|
|
210
|
+
cutoff.setDate(cutoff.getDate() - TRACE_RETENTION_DAYS);
|
|
211
|
+
const cutoffDate = cutoff.toISOString().slice(0, 10);
|
|
212
|
+
try {
|
|
213
|
+
const files = await readdir(traceDir);
|
|
214
|
+
for (const file of files) {
|
|
215
|
+
if (!file.startsWith("trace-") || !file.endsWith(".jsonl"))
|
|
216
|
+
continue;
|
|
217
|
+
const fileDate = file.slice(6, 16);
|
|
218
|
+
if (fileDate < cutoffDate) {
|
|
219
|
+
await unlink(join(traceDir, file)).catch(() => { });
|
|
220
|
+
removed++;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
// No trace dir
|
|
226
|
+
}
|
|
227
|
+
return removed;
|
|
228
|
+
}
|
|
@@ -124,6 +124,27 @@ export function compileWorkflowToSkill(plan, roles, ir, variables) {
|
|
|
124
124
|
lines.push("</Checkpoints>");
|
|
125
125
|
lines.push("");
|
|
126
126
|
}
|
|
127
|
+
// Handoff — artifact flow between steps
|
|
128
|
+
const stepsWithHandoff = plan.steps.filter((s) => s.handoff && (s.handoff.consumes?.length || s.handoff.produces?.length));
|
|
129
|
+
if (stepsWithHandoff.length > 0) {
|
|
130
|
+
lines.push("<Handoff>");
|
|
131
|
+
for (const step of stepsWithHandoff) {
|
|
132
|
+
const h = step.handoff;
|
|
133
|
+
if (h.produces && h.produces.length > 0) {
|
|
134
|
+
for (const p of h.produces) {
|
|
135
|
+
lines.push(` Step ${step.id} produces: ${p.description} (${p.type}${p.path ? `, ${p.path}` : ""})`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (h.consumes && h.consumes.length > 0) {
|
|
139
|
+
for (const c of h.consumes) {
|
|
140
|
+
const fromNote = c.from ? ` from ${c.from}` : "";
|
|
141
|
+
lines.push(` Step ${step.id} consumes: ${c.description}${fromNote} (${c.type}${c.path ? `, ${c.path}` : ""})`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
lines.push("</Handoff>");
|
|
146
|
+
lines.push("");
|
|
147
|
+
}
|
|
127
148
|
// Variables — collect {{var_name}} references from the whole plan
|
|
128
149
|
const varRefs = collectVariableRefs(plan);
|
|
129
150
|
if (varRefs.size > 0) {
|