intentdna 1.5.13 → 1.5.15
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/dist/cli/commands/init.d.ts +4 -0
- package/dist/cli/commands/init.js +24 -11
- package/dist/cli/commands/sync.d.ts +9 -4
- package/dist/cli/commands/sync.js +61 -23
- package/dist/hooks/cli.d.ts +30 -0
- package/dist/hooks/cli.js +168 -59
- package/dist/hooks/enforce.d.ts +13 -12
- package/dist/hooks/enforce.js +61 -46
- package/dist/hooks/protocol.d.ts +11 -0
- package/dist/hooks/schema.d.ts +22 -0
- package/dist/hooks/schema.js +86 -0
- package/dist/hooks/state-manager.d.ts +48 -0
- package/dist/hooks/state-manager.js +135 -0
- package/dist/hooks/state.d.ts +8 -0
- package/dist/hooks/state.js +8 -0
- package/dist/mcp/tools-enforce.js +6 -2
- package/dist/templates/flutter-rewrite.dna.yaml +22 -17
- package/package.json +3 -2
- package/spec/hooks-infra-harness-hardening.md +72 -26
- package/spec/template-version-namespace-fix.md +653 -0
- package/.claude-plugin/hooks/hooks.json +0 -12
- package/.claude-plugin/marketplace.json +0 -16
- package/.claude-plugin/plugin.json +0 -5
- package/LICENSE +0 -69
package/dist/hooks/enforce.d.ts
CHANGED
|
@@ -14,11 +14,11 @@
|
|
|
14
14
|
* Step checkpoints are handled by the CLI, not here.
|
|
15
15
|
*/
|
|
16
16
|
import type { ConstraintIR, RoleDef, CompletedArtifactEntry } from "../schema/types.js";
|
|
17
|
-
import type { PreToolUseInput, PostToolUseInput, UserPromptSubmitInput, SubagentStopInput, NotificationInput,
|
|
17
|
+
import type { PreToolUseInput, PostToolUseInput, UserPromptSubmitInput, SubagentStopInput, NotificationInput, EnforceResult } from "./protocol.js";
|
|
18
18
|
/** SessionStart input fields */
|
|
19
19
|
export interface SessionStartInput {
|
|
20
20
|
cwd?: string;
|
|
21
|
-
|
|
21
|
+
sessionId?: string;
|
|
22
22
|
trigger?: string;
|
|
23
23
|
}
|
|
24
24
|
export interface EnforceState {
|
|
@@ -41,22 +41,22 @@ export interface EnforceState {
|
|
|
41
41
|
*
|
|
42
42
|
* Optional `roles` parameter provides output_schema enforcement.
|
|
43
43
|
*/
|
|
44
|
-
export declare function enforcePreToolUse(ir: ConstraintIR, input: PreToolUseInput, state?: EnforceState, roles?: Record<string, RoleDef>):
|
|
44
|
+
export declare function enforcePreToolUse(ir: ConstraintIR, input: PreToolUseInput, state?: EnforceState, roles?: Record<string, RoleDef>): EnforceResult | null;
|
|
45
45
|
/**
|
|
46
46
|
* Enforce PostToolUse validators.
|
|
47
47
|
* 1. Runs post-execution validators (audit)
|
|
48
48
|
* 2. Verifies Bash write paths were within scope (defense-in-depth)
|
|
49
49
|
*/
|
|
50
|
-
export declare function enforcePostToolUse(ir: ConstraintIR, input: PostToolUseInput):
|
|
50
|
+
export declare function enforcePostToolUse(ir: ConstraintIR, input: PostToolUseInput): EnforceResult | null;
|
|
51
51
|
/**
|
|
52
52
|
* Inject DNA policy reminders on each user prompt.
|
|
53
53
|
* Only injects when there are high-priority directives.
|
|
54
54
|
*/
|
|
55
|
-
export declare function enforceUserPromptSubmit(ir: ConstraintIR, _input: UserPromptSubmitInput):
|
|
55
|
+
export declare function enforceUserPromptSubmit(ir: ConstraintIR, _input: UserPromptSubmitInput): EnforceResult | null;
|
|
56
56
|
/**
|
|
57
57
|
* Audit role scope compliance when a subagent finishes.
|
|
58
58
|
*/
|
|
59
|
-
export declare function enforceSubagentStop(ir: ConstraintIR, input: SubagentStopInput):
|
|
59
|
+
export declare function enforceSubagentStop(ir: ConstraintIR, input: SubagentStopInput): EnforceResult | null;
|
|
60
60
|
/**
|
|
61
61
|
* Preserve DNA directive summary before context compaction.
|
|
62
62
|
* Includes: high-priority directives, role scope map, active workflow state.
|
|
@@ -65,21 +65,21 @@ export declare function enforcePreCompact(ir: ConstraintIR, workflowContext?: {
|
|
|
65
65
|
workflow: string;
|
|
66
66
|
current_step: string;
|
|
67
67
|
current_role: string;
|
|
68
|
-
} | null):
|
|
68
|
+
} | null): EnforceResult | null;
|
|
69
69
|
/**
|
|
70
70
|
* Check if a notification is a DNA violation and return audit info.
|
|
71
71
|
* The CLI handles actual audit file writes.
|
|
72
72
|
*/
|
|
73
|
-
export declare function enforceNotification(ir: ConstraintIR, input: NotificationInput):
|
|
73
|
+
export declare function enforceNotification(ir: ConstraintIR, input: NotificationInput): EnforceResult | null;
|
|
74
74
|
/**
|
|
75
75
|
* Initialize DNA state and inject policy summary on session start.
|
|
76
76
|
* Returns silent if no DNA policies are active.
|
|
77
77
|
*/
|
|
78
|
-
export declare function enforceSessionStart(ir: ConstraintIR, input: SessionStartInput):
|
|
78
|
+
export declare function enforceSessionStart(ir: ConstraintIR, input: SessionStartInput): EnforceResult | null;
|
|
79
79
|
/** Stop enforcement input — includes workflow awareness */
|
|
80
80
|
export interface StopEnforceInput {
|
|
81
81
|
cwd?: string;
|
|
82
|
-
|
|
82
|
+
sessionId?: string;
|
|
83
83
|
stop_reason?: string;
|
|
84
84
|
}
|
|
85
85
|
/** Workflow state context for stop enforcement */
|
|
@@ -103,7 +103,7 @@ export interface StopWorkflowContext {
|
|
|
103
103
|
* Block when:
|
|
104
104
|
* - Workflow active + unmet checkpoints for current step
|
|
105
105
|
*/
|
|
106
|
-
export declare function enforceStop(ir: ConstraintIR, input: StopEnforceInput, workflowState: StopWorkflowContext | null):
|
|
106
|
+
export declare function enforceStop(ir: ConstraintIR, input: StopEnforceInput, workflowState: StopWorkflowContext | null): EnforceResult | null;
|
|
107
107
|
/**
|
|
108
108
|
* Enforce handoff produces — verify that the current step has produced
|
|
109
109
|
* its declared artifacts. Called at stop/checkpoint time.
|
|
@@ -113,7 +113,7 @@ export declare function enforceHandoffProduces(ir: ConstraintIR, wfState: {
|
|
|
113
113
|
current_step: string;
|
|
114
114
|
workflow: string;
|
|
115
115
|
completed_artifacts?: CompletedArtifactEntry[];
|
|
116
|
-
}):
|
|
116
|
+
}): EnforceResult | null;
|
|
117
117
|
/** Check if a file path is allowed by a list of write globs (prefix matching). */
|
|
118
118
|
export declare function checkWriteAllowed(filePath: string, allowedGlobs: string[]): boolean;
|
|
119
119
|
/**
|
|
@@ -157,3 +157,4 @@ export interface ContextReadinessResult {
|
|
|
157
157
|
* Pure function — no I/O.
|
|
158
158
|
*/
|
|
159
159
|
export declare function checkContextReadiness(sessionReads: string[], required: string[]): ContextReadinessResult;
|
|
160
|
+
export declare function checkWorkflowBoundary(workflowCompleted: boolean, targetSkill: string): EnforceResult;
|
package/dist/hooks/enforce.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*
|
|
14
14
|
* Step checkpoints are handled by the CLI, not here.
|
|
15
15
|
*/
|
|
16
|
-
import { allowOutput, blockOutput, escalateOutput
|
|
16
|
+
import { allowOutput, blockOutput, escalateOutput } from "./protocol.js";
|
|
17
17
|
import { relative, normalize } from "node:path";
|
|
18
18
|
// ── PreToolUse Enforcement ─────────────────────────────────
|
|
19
19
|
/**
|
|
@@ -27,43 +27,42 @@ export function enforcePreToolUse(ir, input, state, roles) {
|
|
|
27
27
|
if (state?.workflowState && ir.workflows_ir) {
|
|
28
28
|
const stepRuleResult = enforceStepRules(ir, input, state);
|
|
29
29
|
if (stepRuleResult !== null)
|
|
30
|
-
return stepRuleResult;
|
|
30
|
+
return { output: stepRuleResult, trace: { matched_rule: "step_rule", step_id: state.workflowState.current_step } };
|
|
31
31
|
}
|
|
32
32
|
// Layer 2: Role scope
|
|
33
33
|
if (input.agent_type && ir.roles_scope_map && ir.roles_scope_map.length > 0) {
|
|
34
|
-
// G4: Check if scope is relaxed by iteration-based rule
|
|
35
34
|
const relaxed = isStepScopeRelaxed(ir, state);
|
|
36
35
|
if (!relaxed) {
|
|
37
36
|
const result = enforceRoleScope(ir.roles_scope_map, input, getStepAdditionalPaths(ir, state));
|
|
38
37
|
if (result)
|
|
39
|
-
return result;
|
|
38
|
+
return { output: result, trace: { matched_rule: "scope", role: input.agent_type } };
|
|
40
39
|
}
|
|
41
40
|
}
|
|
42
41
|
// Layer 3: Tool filters
|
|
43
42
|
if (ir.tool_filters.length > 0) {
|
|
44
43
|
const result = enforceToolFilters(ir.tool_filters, input);
|
|
45
44
|
if (result)
|
|
46
|
-
return result;
|
|
45
|
+
return { output: result, trace: { matched_rule: "filter" } };
|
|
47
46
|
}
|
|
48
47
|
// Layer 4: Pre-execution gates
|
|
49
48
|
if (ir.pre_execution_gates.length > 0) {
|
|
50
49
|
const result = enforceGates(ir.pre_execution_gates, input);
|
|
51
50
|
if (result)
|
|
52
|
-
return result;
|
|
51
|
+
return { output: result, trace: { matched_rule: "gate" } };
|
|
53
52
|
}
|
|
54
53
|
// Layer 5: Output schema
|
|
55
54
|
if (roles && input.tool_name === "Write") {
|
|
56
55
|
const result = enforceOutputSchema(roles, input);
|
|
57
56
|
if (result)
|
|
58
|
-
return result;
|
|
57
|
+
return { output: result, trace: { matched_rule: "output_schema" } };
|
|
59
58
|
}
|
|
60
59
|
// Layer 6: Handoff — check consumed artifacts are available
|
|
61
60
|
if (state?.workflowState && ir.workflows_ir) {
|
|
62
61
|
const result = enforceHandoffConsumes(ir, state.workflowState, state.existingArtifactPaths);
|
|
63
62
|
if (result)
|
|
64
|
-
return result;
|
|
63
|
+
return { output: result, trace: { matched_rule: "handoff", step_id: state.workflowState.current_step } };
|
|
65
64
|
}
|
|
66
|
-
return
|
|
65
|
+
return null;
|
|
67
66
|
}
|
|
68
67
|
// ── PostToolUse Enforcement ────────────────────────────────
|
|
69
68
|
/**
|
|
@@ -92,8 +91,13 @@ export function enforcePostToolUse(ir, input) {
|
|
|
92
91
|
}
|
|
93
92
|
}
|
|
94
93
|
if (lines.length === 0)
|
|
95
|
-
return
|
|
96
|
-
|
|
94
|
+
return null;
|
|
95
|
+
// Audit-only path: validator entries emit audit lines, plus bash scope WARN lines.
|
|
96
|
+
// If any bash scope violation was found use "scope" (real path match); otherwise
|
|
97
|
+
// this is validator-only output — use "validator" for accurate trace semantics.
|
|
98
|
+
const hasScopeViolation = lines.some(l => l.startsWith("WARN"));
|
|
99
|
+
const matchedRule = hasScopeViolation ? "scope" : "validator";
|
|
100
|
+
return { output: allowOutput(lines.join("\n")), trace: { matched_rule: matchedRule } };
|
|
97
101
|
}
|
|
98
102
|
// ── UserPromptSubmit Enforcement ───────────────────────────
|
|
99
103
|
/**
|
|
@@ -103,7 +107,7 @@ export function enforcePostToolUse(ir, input) {
|
|
|
103
107
|
export function enforceUserPromptSubmit(ir, _input) {
|
|
104
108
|
const highPriority = ir.prompt_directives.filter(d => d.priority === "high");
|
|
105
109
|
if (highPriority.length === 0)
|
|
106
|
-
return
|
|
110
|
+
return null;
|
|
107
111
|
const lines = [
|
|
108
112
|
"<system-reminder>[Intent DNA] Active high-priority policies:",
|
|
109
113
|
];
|
|
@@ -111,7 +115,7 @@ export function enforceUserPromptSubmit(ir, _input) {
|
|
|
111
115
|
lines.push(` - [${d.source_gene}] ${d.text}`);
|
|
112
116
|
}
|
|
113
117
|
lines.push("</system-reminder>");
|
|
114
|
-
return allowOutput(lines.join("\n"));
|
|
118
|
+
return { output: allowOutput(lines.join("\n")) };
|
|
115
119
|
}
|
|
116
120
|
// ── SubagentStop Enforcement ───────────────────────────────
|
|
117
121
|
/**
|
|
@@ -120,9 +124,9 @@ export function enforceUserPromptSubmit(ir, _input) {
|
|
|
120
124
|
export function enforceSubagentStop(ir, input) {
|
|
121
125
|
const rolesScopeMap = ir.roles_scope_map;
|
|
122
126
|
if (!rolesScopeMap || rolesScopeMap.length === 0)
|
|
123
|
-
return
|
|
127
|
+
return null;
|
|
124
128
|
if (!input.agent_type)
|
|
125
|
-
return
|
|
129
|
+
return null;
|
|
126
130
|
for (const entry of rolesScopeMap) {
|
|
127
131
|
const agentTypeName = `dna-${toKebabCase(entry.role_name)}`;
|
|
128
132
|
if (input.agent_type === agentTypeName) {
|
|
@@ -130,10 +134,15 @@ export function enforceSubagentStop(ir, input) {
|
|
|
130
134
|
const scopeDesc = writeGlobs.length > 0
|
|
131
135
|
? `write:[${writeGlobs.join(",")}]`
|
|
132
136
|
: "write:[]";
|
|
133
|
-
return
|
|
137
|
+
return {
|
|
138
|
+
output: allowOutput(`[Intent DNA] SubagentStop audit: role=${entry.role_name} agent=${input.agent_name ?? "unknown"} scope=${scopeDesc}`),
|
|
139
|
+
trace: { matched_rule: "scope", role: entry.role_name },
|
|
140
|
+
};
|
|
134
141
|
}
|
|
135
142
|
}
|
|
136
|
-
return
|
|
143
|
+
return {
|
|
144
|
+
output: allowOutput(`[Intent DNA] SubagentStop: unknown role for agent_type=${input.agent_type}`),
|
|
145
|
+
};
|
|
137
146
|
}
|
|
138
147
|
// ── PreCompact Enforcement ─────────────────────────────────
|
|
139
148
|
/**
|
|
@@ -145,7 +154,7 @@ export function enforcePreCompact(ir, workflowContext) {
|
|
|
145
154
|
const hasRoles = ir.roles_scope_map && ir.roles_scope_map.length > 0;
|
|
146
155
|
const hasWorkflow = !!workflowContext;
|
|
147
156
|
if (!hasDirectives && !hasRoles && !hasWorkflow)
|
|
148
|
-
return
|
|
157
|
+
return null;
|
|
149
158
|
const critical = ir.prompt_directives.filter(d => d.priority === "high");
|
|
150
159
|
const standard = ir.prompt_directives.filter(d => d.priority === "medium");
|
|
151
160
|
const lines = [
|
|
@@ -176,7 +185,7 @@ export function enforcePreCompact(ir, workflowContext) {
|
|
|
176
185
|
if (hasWorkflow) {
|
|
177
186
|
lines.push(` Active workflow: ${workflowContext.workflow} (step: ${workflowContext.current_step}, role: ${workflowContext.current_role})`);
|
|
178
187
|
}
|
|
179
|
-
return allowOutput(lines.join("\n"));
|
|
188
|
+
return { output: allowOutput(lines.join("\n")) };
|
|
180
189
|
}
|
|
181
190
|
// ── Notification Enforcement ───────────────────────────────
|
|
182
191
|
/**
|
|
@@ -187,12 +196,14 @@ export function enforceNotification(ir, input) {
|
|
|
187
196
|
const blockGates = ir.pre_execution_gates.filter(g => g.action === "block" || g.action === "escalate");
|
|
188
197
|
const blockFilters = ir.tool_filters.filter(f => f.action === "remove");
|
|
189
198
|
if (blockGates.length === 0 && blockFilters.length === 0)
|
|
190
|
-
return
|
|
199
|
+
return null;
|
|
191
200
|
const message = input.message ?? "";
|
|
192
201
|
if (message.includes("Intent DNA")) {
|
|
193
|
-
return
|
|
202
|
+
return {
|
|
203
|
+
output: allowOutput(`[Intent DNA] Notification: violation detected — ${input.title ?? "untitled"}: ${message}`),
|
|
204
|
+
};
|
|
194
205
|
}
|
|
195
|
-
return
|
|
206
|
+
return null;
|
|
196
207
|
}
|
|
197
208
|
// ── SessionStart Enforcement ───────────────────────────────
|
|
198
209
|
/**
|
|
@@ -200,16 +211,14 @@ export function enforceNotification(ir, input) {
|
|
|
200
211
|
* Returns silent if no DNA policies are active.
|
|
201
212
|
*/
|
|
202
213
|
export function enforceSessionStart(ir, input) {
|
|
203
|
-
// Check if there's anything meaningful to report
|
|
204
214
|
const directiveCount = ir.prompt_directives.length;
|
|
205
215
|
const gateCount = ir.pre_execution_gates.length;
|
|
206
216
|
const filterCount = ir.tool_filters.length;
|
|
207
217
|
const roleCount = ir.roles_scope_map?.length ?? 0;
|
|
208
218
|
const workflowCount = ir.workflows_ir?.length ?? 0;
|
|
209
219
|
if (directiveCount === 0 && gateCount === 0 && filterCount === 0 && roleCount === 0) {
|
|
210
|
-
return
|
|
220
|
+
return null;
|
|
211
221
|
}
|
|
212
|
-
// Build policy summary
|
|
213
222
|
const lines = [
|
|
214
223
|
"[Intent DNA] Session initialized — active governance:",
|
|
215
224
|
];
|
|
@@ -230,7 +239,7 @@ export function enforceSessionStart(ir, input) {
|
|
|
230
239
|
if (stats.length > 0) {
|
|
231
240
|
lines.push(` Enforcement: ${stats.join(", ")}`);
|
|
232
241
|
}
|
|
233
|
-
return allowOutput(lines.join("\n"), "SessionStart");
|
|
242
|
+
return { output: allowOutput(lines.join("\n"), "SessionStart") };
|
|
234
243
|
}
|
|
235
244
|
/**
|
|
236
245
|
* Enforce Stop hook — verify workflow checkpoint completion.
|
|
@@ -245,31 +254,25 @@ export function enforceSessionStart(ir, input) {
|
|
|
245
254
|
* - Workflow active + unmet checkpoints for current step
|
|
246
255
|
*/
|
|
247
256
|
export function enforceStop(ir, input, workflowState) {
|
|
248
|
-
// Safety valve 1: no workflow state → silent
|
|
249
257
|
if (!workflowState || !workflowState.active) {
|
|
250
|
-
return
|
|
258
|
+
return null;
|
|
251
259
|
}
|
|
252
|
-
// Safety valve 2: context_limit stops → NEVER block
|
|
253
260
|
const reason = input.stop_reason ?? "";
|
|
254
261
|
if (reason.includes("context_limit") || reason.includes("context_window")) {
|
|
255
|
-
return
|
|
262
|
+
return null;
|
|
256
263
|
}
|
|
257
|
-
// Safety valve 3: user abort → don't block
|
|
258
264
|
if (reason === "user_abort" || reason === "sigint") {
|
|
259
|
-
return
|
|
265
|
+
return null;
|
|
260
266
|
}
|
|
261
|
-
// Safety valve 4: stale state (>2h)
|
|
262
267
|
const STALE_MS = 2 * 60 * 60 * 1000;
|
|
263
268
|
if (workflowState.started_at) {
|
|
264
269
|
const age = Date.now() - new Date(workflowState.started_at).getTime();
|
|
265
270
|
if (age > STALE_MS) {
|
|
266
|
-
return
|
|
271
|
+
return null;
|
|
267
272
|
}
|
|
268
273
|
}
|
|
269
|
-
// Check for unmet checkpoints in current step
|
|
270
274
|
const checkpoints = ir.step_checkpoints ?? [];
|
|
271
275
|
const currentStepCheckpoints = checkpoints.filter(cp => cp.step_id === workflowState.current_step);
|
|
272
|
-
// Also check workflows_ir for the active workflow's checkpoints
|
|
273
276
|
let workflowCheckpoints = currentStepCheckpoints;
|
|
274
277
|
if (ir.workflows_ir && ir.workflows_ir.length > 0) {
|
|
275
278
|
const activeWf = ir.workflows_ir.find(w => w.workflow_name === workflowState.workflow);
|
|
@@ -281,27 +284,27 @@ export function enforceStop(ir, input, workflowState) {
|
|
|
281
284
|
}
|
|
282
285
|
}
|
|
283
286
|
if (workflowCheckpoints.length === 0) {
|
|
284
|
-
return
|
|
287
|
+
return null;
|
|
285
288
|
}
|
|
286
|
-
// Build list of blocking checkpoints
|
|
287
289
|
const blocking = workflowCheckpoints.flatMap(cp => cp.checkpoints.filter(c => (c.action ?? "block") === "block"));
|
|
288
290
|
if (blocking.length === 0) {
|
|
289
|
-
// No checkpoint blocking — check handoff produces
|
|
290
291
|
if (workflowState.workflow) {
|
|
291
292
|
const handoffResult = enforceHandoffProduces(ir, {
|
|
292
293
|
current_step: workflowState.current_step,
|
|
293
294
|
workflow: workflowState.workflow,
|
|
294
295
|
completed_artifacts: workflowState.completed_artifacts,
|
|
295
296
|
});
|
|
296
|
-
if (handoffResult
|
|
297
|
+
if (handoffResult)
|
|
297
298
|
return handoffResult;
|
|
298
299
|
}
|
|
299
|
-
return
|
|
300
|
+
return null;
|
|
300
301
|
}
|
|
301
|
-
// Block: workflow active with unmet checkpoints
|
|
302
302
|
const messages = blocking.map(c => c.message);
|
|
303
|
-
return
|
|
304
|
-
|
|
303
|
+
return {
|
|
304
|
+
output: blockOutput(`[Intent DNA] Workflow '${workflowState.workflow}' has unmet checkpoints at step '${workflowState.current_step}':\n` +
|
|
305
|
+
messages.map(m => ` - ${m}`).join("\n")),
|
|
306
|
+
trace: { matched_rule: "handoff", step_id: workflowState.current_step },
|
|
307
|
+
};
|
|
305
308
|
}
|
|
306
309
|
// ── G4: State-driven Step Enforcement ─────────────────────
|
|
307
310
|
/** Write tools that step read_only rule should block */
|
|
@@ -393,10 +396,12 @@ export function enforceHandoffProduces(ir, wfState) {
|
|
|
393
396
|
if (!currentEntry?.produces || currentEntry.produces.length === 0)
|
|
394
397
|
return null;
|
|
395
398
|
const currentArtifacts = (wfState.completed_artifacts ?? []).find(a => a.step_id === wfState.current_step);
|
|
396
|
-
// Check that each produced artifact with a path has been recorded
|
|
397
399
|
for (const produced of currentEntry.produces) {
|
|
398
400
|
if (produced.path && (!currentArtifacts || !currentArtifacts.artifacts.some(a => a.path === produced.path))) {
|
|
399
|
-
return
|
|
401
|
+
return {
|
|
402
|
+
output: blockOutput(`[Intent DNA] Step '${wfState.current_step}' must produce artifact '${produced.description}' (path: ${produced.path}) before proceeding.`),
|
|
403
|
+
trace: { matched_rule: "handoff", step_id: wfState.current_step },
|
|
404
|
+
};
|
|
400
405
|
}
|
|
401
406
|
}
|
|
402
407
|
return null;
|
|
@@ -712,3 +717,13 @@ export function checkContextReadiness(sessionReads, required) {
|
|
|
712
717
|
const missing = required.filter(f => !readSet.has(f));
|
|
713
718
|
return { ready: missing.length === 0, missing };
|
|
714
719
|
}
|
|
720
|
+
// ── Workflow Boundary Gate ────────────────────────────────────
|
|
721
|
+
export function checkWorkflowBoundary(workflowCompleted, targetSkill) {
|
|
722
|
+
if (workflowCompleted) {
|
|
723
|
+
return {
|
|
724
|
+
output: blockOutput(`[Intent DNA] Workflow completed. Cannot start skill '${targetSkill}'.`),
|
|
725
|
+
trace: { matched_rule: "workflow_boundary" },
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
return { output: allowOutput() };
|
|
729
|
+
}
|
package/dist/hooks/protocol.d.ts
CHANGED
|
@@ -54,6 +54,17 @@ export interface HookOutput {
|
|
|
54
54
|
permissionDecision?: "ask" | "allow" | "deny";
|
|
55
55
|
};
|
|
56
56
|
}
|
|
57
|
+
export type MatchedRule = "scope" | "filter" | "gate" | "schema" | "validator" | "handoff" | "step_rule" | "output_schema" | "context_gate" | "reflection_gate" | "workflow_boundary";
|
|
58
|
+
export interface TraceMetadata {
|
|
59
|
+
matched_rule: MatchedRule;
|
|
60
|
+
gene_id?: string;
|
|
61
|
+
step_id?: string;
|
|
62
|
+
role?: string;
|
|
63
|
+
}
|
|
64
|
+
export interface EnforceResult {
|
|
65
|
+
output: HookOutput;
|
|
66
|
+
trace?: TraceMetadata;
|
|
67
|
+
}
|
|
57
68
|
/** Create an allow output, optionally injecting text into the conversation. */
|
|
58
69
|
export declare function allowOutput(additionalContext?: string, hookEventName?: string): HookOutput;
|
|
59
70
|
/** Create a block output with reason. */
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook input validation — CC protocol boundary.
|
|
3
|
+
* Zero external dependencies. Hand-written validators.
|
|
4
|
+
* Fail-open: invalid input → log warning + return { valid: false, ... }
|
|
5
|
+
*/
|
|
6
|
+
export interface ValidationResult {
|
|
7
|
+
valid: boolean;
|
|
8
|
+
errors: string[];
|
|
9
|
+
normalized?: Record<string, unknown>;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Validate hook input against expected schema for each event type.
|
|
13
|
+
* Returns normalized input (camelCase) on success.
|
|
14
|
+
*/
|
|
15
|
+
export declare function validateHookInput(event: string, raw: unknown): ValidationResult;
|
|
16
|
+
/**
|
|
17
|
+
* Normalize session_id field from various formats:
|
|
18
|
+
* - session_id (snake_case from CC)
|
|
19
|
+
* - sessionId (camelCase internal)
|
|
20
|
+
* - CLAUDE_SESSION_ID (env var fallback)
|
|
21
|
+
*/
|
|
22
|
+
export declare function normalizeSessionId(input: Record<string, unknown>): string | undefined;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook input validation — CC protocol boundary.
|
|
3
|
+
* Zero external dependencies. Hand-written validators.
|
|
4
|
+
* Fail-open: invalid input → log warning + return { valid: false, ... }
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Validate hook input against expected schema for each event type.
|
|
8
|
+
* Returns normalized input (camelCase) on success.
|
|
9
|
+
*/
|
|
10
|
+
export function validateHookInput(event, raw) {
|
|
11
|
+
if (typeof raw !== "object" || raw === null) {
|
|
12
|
+
return { valid: false, errors: ["Input must be an object"] };
|
|
13
|
+
}
|
|
14
|
+
const input = raw;
|
|
15
|
+
const errors = [];
|
|
16
|
+
const normalized = { ...input };
|
|
17
|
+
// Normalize session_id to sessionId
|
|
18
|
+
const sessionId = normalizeSessionId(input);
|
|
19
|
+
if (sessionId !== undefined) {
|
|
20
|
+
normalized.sessionId = sessionId;
|
|
21
|
+
delete normalized.session_id;
|
|
22
|
+
}
|
|
23
|
+
switch (event) {
|
|
24
|
+
case "PreToolUse":
|
|
25
|
+
if (typeof input.tool_name !== "string") {
|
|
26
|
+
errors.push("PreToolUse: tool_name must be a string");
|
|
27
|
+
}
|
|
28
|
+
if (typeof input.tool_input !== "object" || input.tool_input === null) {
|
|
29
|
+
errors.push("PreToolUse: tool_input must be an object");
|
|
30
|
+
}
|
|
31
|
+
break;
|
|
32
|
+
case "PostToolUse":
|
|
33
|
+
if (typeof input.tool_name !== "string") {
|
|
34
|
+
errors.push("PostToolUse: tool_name must be a string");
|
|
35
|
+
}
|
|
36
|
+
if (typeof input.tool_input !== "object" || input.tool_input === null) {
|
|
37
|
+
errors.push("PostToolUse: tool_input must be an object");
|
|
38
|
+
}
|
|
39
|
+
// tool_output is optional
|
|
40
|
+
break;
|
|
41
|
+
case "SessionStart":
|
|
42
|
+
// session_id is optional but should be normalized if present
|
|
43
|
+
break;
|
|
44
|
+
case "SubagentStop":
|
|
45
|
+
// agent_type or agent_name should be present but not strictly required
|
|
46
|
+
break;
|
|
47
|
+
case "Notification":
|
|
48
|
+
if (typeof input.title !== "string" && input.title !== undefined) {
|
|
49
|
+
errors.push("Notification: title must be a string if present");
|
|
50
|
+
}
|
|
51
|
+
if (typeof input.message !== "string" && input.message !== undefined) {
|
|
52
|
+
errors.push("Notification: message must be a string if present");
|
|
53
|
+
}
|
|
54
|
+
break;
|
|
55
|
+
case "UserPromptSubmit":
|
|
56
|
+
case "PreCompact":
|
|
57
|
+
case "Stop":
|
|
58
|
+
// Minimal validation — these events have no required fields beyond base
|
|
59
|
+
break;
|
|
60
|
+
default:
|
|
61
|
+
errors.push(`Unknown event type: ${event}`);
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
valid: errors.length === 0,
|
|
65
|
+
errors,
|
|
66
|
+
normalized: errors.length === 0 ? normalized : undefined,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Normalize session_id field from various formats:
|
|
71
|
+
* - session_id (snake_case from CC)
|
|
72
|
+
* - sessionId (camelCase internal)
|
|
73
|
+
* - CLAUDE_SESSION_ID (env var fallback)
|
|
74
|
+
*/
|
|
75
|
+
export function normalizeSessionId(input) {
|
|
76
|
+
if (typeof input.sessionId === "string") {
|
|
77
|
+
return input.sessionId;
|
|
78
|
+
}
|
|
79
|
+
if (typeof input.session_id === "string") {
|
|
80
|
+
return input.session_id;
|
|
81
|
+
}
|
|
82
|
+
if (typeof input.CLAUDE_SESSION_ID === "string") {
|
|
83
|
+
return input.CLAUDE_SESSION_ID;
|
|
84
|
+
}
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intent DNA — DNAStateManager
|
|
3
|
+
*
|
|
4
|
+
* Class-based facade over state.ts module functions. Bundles projectDir +
|
|
5
|
+
* sessionId into a single object so call sites don't repeat those args.
|
|
6
|
+
* Adds experience chain persistence (markdown + structured JSON) and
|
|
7
|
+
* lifecycle operations (init / cleanup / isStale) that state.ts lacks.
|
|
8
|
+
*
|
|
9
|
+
* Existing state.ts functions remain the canonical I/O primitives — this
|
|
10
|
+
* class delegates to them. Prefer this class in new code; state.ts exports
|
|
11
|
+
* are @deprecated for direct use.
|
|
12
|
+
*/
|
|
13
|
+
import type { DNAWorkflowState, AuditEntry, SurgeonAttemptState, SessionReadsState } from "./state.js";
|
|
14
|
+
/** Single entry in the experience chain — one per failed surgeon round. */
|
|
15
|
+
export interface ExperienceEntry {
|
|
16
|
+
round: number;
|
|
17
|
+
analysis: string;
|
|
18
|
+
attempt: string;
|
|
19
|
+
result: string;
|
|
20
|
+
lesson: string;
|
|
21
|
+
timestamp: string;
|
|
22
|
+
}
|
|
23
|
+
export declare class DNAStateManager {
|
|
24
|
+
private readonly projectDir;
|
|
25
|
+
private readonly sessionId?;
|
|
26
|
+
constructor(projectDir: string, sessionId?: string | undefined);
|
|
27
|
+
readWorkflowState(stalenessMs?: number): Promise<DNAWorkflowState | null>;
|
|
28
|
+
writeWorkflowState(state: DNAWorkflowState): Promise<void>;
|
|
29
|
+
clearWorkflowState(): Promise<void>;
|
|
30
|
+
appendAudit(entry: AuditEntry): Promise<void>;
|
|
31
|
+
readSurgeonAttempts(): Promise<SurgeonAttemptState>;
|
|
32
|
+
writeSurgeonAttempts(state: SurgeonAttemptState): Promise<void>;
|
|
33
|
+
readSessionReads(): Promise<SessionReadsState>;
|
|
34
|
+
appendSessionRead(filePath: string): Promise<void>;
|
|
35
|
+
readExperienceChain(): Promise<ExperienceEntry[]>;
|
|
36
|
+
writeExperienceChain(chain: ExperienceEntry[]): Promise<void>;
|
|
37
|
+
appendExperience(entry: ExperienceEntry): Promise<void>;
|
|
38
|
+
/** Ensure session directory structure exists. */
|
|
39
|
+
init(): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Remove this manager's state files.
|
|
42
|
+
* Callers that need trace rotation should invoke rotateTraces() from state.ts separately.
|
|
43
|
+
*/
|
|
44
|
+
cleanup(): Promise<void>;
|
|
45
|
+
/** True when workflow.json started_at exceeds the staleness threshold. */
|
|
46
|
+
isStale(stalenessMs?: number): Promise<boolean>;
|
|
47
|
+
private path;
|
|
48
|
+
}
|