intentdna 1.5.14 → 1.5.16
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 +20 -7
- package/.claude-plugin/plugin.json +16 -2
- 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 +18 -17
- package/package.json +4 -2
- package/spec/hooks-infra-harness-hardening.md +72 -26
- package/LICENSE +0 -69
- /package/{.claude-plugin/hooks → hooks}/hooks.json +0 -0
|
@@ -1,16 +1,29 @@
|
|
|
1
1
|
{
|
|
2
|
+
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
|
|
2
3
|
"name": "intentdna",
|
|
3
|
-
"description": "
|
|
4
|
+
"description": "Intent DNA plugin marketplace for Claude Code",
|
|
4
5
|
"owner": {
|
|
5
|
-
"name": "Samuel"
|
|
6
|
-
"email": "wanglushan3344@gmail.com"
|
|
6
|
+
"name": "Samuel"
|
|
7
7
|
},
|
|
8
8
|
"plugins": [
|
|
9
9
|
{
|
|
10
10
|
"name": "intentdna",
|
|
11
|
-
"description": "
|
|
12
|
-
"version": "1.5.
|
|
13
|
-
"
|
|
11
|
+
"description": "Declarative policy layer for AI agent behavior with plugin-managed hook runtime for Claude Code.",
|
|
12
|
+
"version": "1.5.16",
|
|
13
|
+
"author": {
|
|
14
|
+
"name": "Samuel"
|
|
15
|
+
},
|
|
16
|
+
"source": "./",
|
|
17
|
+
"category": "development",
|
|
18
|
+
"homepage": "https://github.com/lushan1314/intentdna-repo",
|
|
19
|
+
"tags": [
|
|
20
|
+
"agent",
|
|
21
|
+
"policy",
|
|
22
|
+
"guardrails",
|
|
23
|
+
"hooks",
|
|
24
|
+
"development"
|
|
25
|
+
]
|
|
14
26
|
}
|
|
15
|
-
]
|
|
27
|
+
],
|
|
28
|
+
"version": "1.5.16"
|
|
16
29
|
}
|
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "intentdna",
|
|
3
|
-
"version": "1.5.
|
|
4
|
-
"description": "Declarative policy layer for AI agent
|
|
3
|
+
"version": "1.5.16",
|
|
4
|
+
"description": "Declarative policy layer for AI agent behavior",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Samuel"
|
|
7
|
+
},
|
|
8
|
+
"repository": "https://github.com/lushan1314/intentdna-repo",
|
|
9
|
+
"homepage": "https://github.com/lushan1314/intentdna-repo",
|
|
10
|
+
"license": "Apache-2.0",
|
|
11
|
+
"keywords": [
|
|
12
|
+
"claude-code",
|
|
13
|
+
"plugin",
|
|
14
|
+
"agent",
|
|
15
|
+
"policy",
|
|
16
|
+
"dna",
|
|
17
|
+
"guardrails"
|
|
18
|
+
]
|
|
5
19
|
}
|
package/dist/hooks/cli.d.ts
CHANGED
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
*
|
|
15
15
|
* Fail-open: all errors → { continue: true, suppressOutput: true }
|
|
16
16
|
*/
|
|
17
|
+
import type { ConstraintIR } from "../schema/types.js";
|
|
18
|
+
import { blockOutput } from "./protocol.js";
|
|
19
|
+
import { readWorkflowState } from "./state.js";
|
|
17
20
|
export interface SessionSummary {
|
|
18
21
|
total: number;
|
|
19
22
|
blocks: number;
|
|
@@ -34,3 +37,30 @@ export declare function computeSummary(traces: Array<{
|
|
|
34
37
|
reason?: string;
|
|
35
38
|
}>): SessionSummary;
|
|
36
39
|
export declare function formatSummary(s: SessionSummary): string | null;
|
|
40
|
+
/**
|
|
41
|
+
* Evaluate PreToolUse gates in priority order:
|
|
42
|
+
* 1. Workflow Boundary — block Skill() after workflow completed
|
|
43
|
+
* 2. Context Gate — block Edit/Write/Bash when required context files unread
|
|
44
|
+
*
|
|
45
|
+
* Returns the first triggered block, or null if all gates pass.
|
|
46
|
+
* Fail-open on any exception.
|
|
47
|
+
*/
|
|
48
|
+
export declare function handlePreToolGates(ir: ConstraintIR, rawInput: Record<string, unknown>, wfState: Awaited<ReturnType<typeof readWorkflowState>>, projectDir: string, sessionId?: string): Promise<{
|
|
49
|
+
output: ReturnType<typeof blockOutput>;
|
|
50
|
+
matched_rule: "workflow_boundary" | "context_gate";
|
|
51
|
+
} | null>;
|
|
52
|
+
/**
|
|
53
|
+
* Build next-step guidance for Stop hook.
|
|
54
|
+
* Returns a string to append to the Stop output's reason, or null if
|
|
55
|
+
* no explicit guidance applies (normal flow).
|
|
56
|
+
*
|
|
57
|
+
* Guidance cases:
|
|
58
|
+
* 1. Surgeon stalled — fail_count has reached the step's max_attempts
|
|
59
|
+
* for a step that defines handoff_to. Emit Agent() dispatch to the
|
|
60
|
+
* handoff role with experience chain context.
|
|
61
|
+
* 2. Workflow complete — wfState exists but active=false. Emit STOP
|
|
62
|
+
* directive so the assistant doesn't auto-start another workflow.
|
|
63
|
+
*
|
|
64
|
+
* Fail-open: any exception returns null.
|
|
65
|
+
*/
|
|
66
|
+
export declare function buildWorkflowGuidance(projectDir: string, wfState: Awaited<ReturnType<typeof readWorkflowState>>, sessionId?: string): Promise<string | null>;
|
package/dist/hooks/cli.js
CHANGED
|
@@ -17,8 +17,9 @@
|
|
|
17
17
|
import { readFile, stat } from "node:fs/promises";
|
|
18
18
|
import { join, resolve } from "node:path";
|
|
19
19
|
import { randomUUID } from "node:crypto";
|
|
20
|
-
import { readStdin, writeOutput, silentOutput, allowOutput } from "./protocol.js";
|
|
21
|
-
import {
|
|
20
|
+
import { readStdin, writeOutput, silentOutput, allowOutput, blockOutput } from "./protocol.js";
|
|
21
|
+
import { validateHookInput } from "./schema.js";
|
|
22
|
+
import { enforcePreToolUse, enforcePostToolUse, enforceUserPromptSubmit, enforceSubagentStop, enforcePreCompact, enforceNotification, enforceSessionStart, enforceStop, extractBashWritePaths, checkReflectionLimit, checkContextReadiness, checkWorkflowBoundary, } from "./enforce.js";
|
|
22
23
|
import { appendAudit, readWorkflowState, appendTrace, rotateTraces, readTraces, cleanStaleState, readSurgeonAttempts, writeSurgeonAttempts, appendSessionRead, readSessionReads } from "./state.js";
|
|
23
24
|
// ── Constants ──────────────────────────────────────────────
|
|
24
25
|
const DEFAULT_IR_PATH = ".dna/compiled/ir.json";
|
|
@@ -80,12 +81,19 @@ async function main() {
|
|
|
80
81
|
irPath = args[irFlagIdx + 1];
|
|
81
82
|
}
|
|
82
83
|
// Read stdin
|
|
83
|
-
const
|
|
84
|
+
const rawStdin = await readStdin(5000);
|
|
85
|
+
// Validate + normalize input against CC hook protocol schema.
|
|
86
|
+
// Fail-open: invalid input → write stderr warning + silent exit.
|
|
87
|
+
const validation = validateHookInput(event, rawStdin);
|
|
88
|
+
if (!validation.valid) {
|
|
89
|
+
process.stderr.write(`\n Intent DNA: invalid ${event} input — ${validation.errors.join("; ")}\n`);
|
|
90
|
+
writeOutput(silentOutput());
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const rawInput = validation.normalized;
|
|
84
94
|
// Resolve project directory from input or cwd
|
|
85
95
|
const projectDir = typeof rawInput.cwd === "string" ? rawInput.cwd : process.cwd();
|
|
86
|
-
const sessionId = typeof rawInput.
|
|
87
|
-
: typeof rawInput.sessionId === "string" ? rawInput.sessionId
|
|
88
|
-
: undefined;
|
|
96
|
+
const sessionId = typeof rawInput.sessionId === "string" ? rawInput.sessionId : undefined;
|
|
89
97
|
// Load compiled IR
|
|
90
98
|
const resolvedIRPath = resolve(projectDir, irPath);
|
|
91
99
|
const ir = await loadIR(resolvedIRPath);
|
|
@@ -94,24 +102,46 @@ async function main() {
|
|
|
94
102
|
return;
|
|
95
103
|
}
|
|
96
104
|
const state = {};
|
|
105
|
+
let wfStateRaw = null;
|
|
97
106
|
// Load workflow state for events that need it (handoff context + PreCompact preservation)
|
|
98
107
|
if (event === "PreToolUse" || event === "PreCompact") {
|
|
99
|
-
|
|
100
|
-
if (
|
|
108
|
+
wfStateRaw = await readWorkflowState(projectDir, sessionId);
|
|
109
|
+
if (wfStateRaw && wfStateRaw.active) {
|
|
101
110
|
state.workflowState = {
|
|
102
|
-
current_step:
|
|
103
|
-
workflow:
|
|
104
|
-
current_role:
|
|
105
|
-
completed_artifacts:
|
|
106
|
-
iteration:
|
|
111
|
+
current_step: wfStateRaw.current_step,
|
|
112
|
+
workflow: wfStateRaw.workflow,
|
|
113
|
+
current_role: wfStateRaw.current_role,
|
|
114
|
+
completed_artifacts: wfStateRaw.completed_artifacts,
|
|
115
|
+
iteration: wfStateRaw.iteration, // G4: pass iteration for state-driven rules
|
|
107
116
|
};
|
|
108
117
|
// Re-run fallback: scan consumed artifact paths on disk so
|
|
109
118
|
// enforceHandoffConsumes can skip blocks for files that already exist.
|
|
110
119
|
if (event === "PreToolUse") {
|
|
111
|
-
state.existingArtifactPaths = await scanConsumedArtifactPaths(projectDir, ir,
|
|
120
|
+
state.existingArtifactPaths = await scanConsumedArtifactPaths(projectDir, ir, wfStateRaw.workflow, wfStateRaw.current_step);
|
|
112
121
|
}
|
|
113
122
|
}
|
|
114
123
|
}
|
|
124
|
+
// Phase 2B: PreToolUse gates (workflow boundary + context gate).
|
|
125
|
+
// Run before dispatch so the block shortcircuits the rest of the pipeline.
|
|
126
|
+
if (event === "PreToolUse") {
|
|
127
|
+
const gateResult = await handlePreToolGates(ir, rawInput, wfStateRaw, projectDir, sessionId);
|
|
128
|
+
if (gateResult) {
|
|
129
|
+
writeOutput(gateResult.output);
|
|
130
|
+
appendTrace(projectDir, {
|
|
131
|
+
trace_id: randomUUID(),
|
|
132
|
+
event,
|
|
133
|
+
tool_name: typeof rawInput.tool_name === "string" ? rawInput.tool_name : undefined,
|
|
134
|
+
agent_type: typeof rawInput.agent_type === "string" ? rawInput.agent_type : undefined,
|
|
135
|
+
workflow: wfStateRaw?.workflow,
|
|
136
|
+
step: wfStateRaw?.current_step,
|
|
137
|
+
decision: "block",
|
|
138
|
+
reason: gateResult.output.reason,
|
|
139
|
+
duration_ms: 0,
|
|
140
|
+
timestamp: new Date().toISOString(),
|
|
141
|
+
}, sessionId).catch(() => { });
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
115
145
|
// Special handling for Stop — needs async workflow state read + session summary
|
|
116
146
|
if (event === "Stop") {
|
|
117
147
|
const wfState = await readWorkflowState(projectDir, sessionId);
|
|
@@ -123,11 +153,12 @@ async function main() {
|
|
|
123
153
|
started_at: wfState.started_at,
|
|
124
154
|
completed_artifacts: wfState.completed_artifacts,
|
|
125
155
|
} : null;
|
|
126
|
-
let
|
|
156
|
+
let stopResult = enforceStop(ir, {
|
|
127
157
|
cwd: typeof rawInput.cwd === "string" ? rawInput.cwd : undefined,
|
|
128
|
-
|
|
158
|
+
sessionId: sessionId,
|
|
129
159
|
stop_reason: typeof rawInput.stop_reason === "string" ? rawInput.stop_reason : undefined,
|
|
130
160
|
}, stopContext);
|
|
161
|
+
let stopOutput = stopResult?.output ?? silentOutput();
|
|
131
162
|
// Session summary: aggregate block/warn stats from trace
|
|
132
163
|
try {
|
|
133
164
|
const traces = await readTraces(projectDir, 1, sessionId);
|
|
@@ -135,16 +166,29 @@ async function main() {
|
|
|
135
166
|
const summaryText = formatSummary(summary);
|
|
136
167
|
if (summaryText) {
|
|
137
168
|
if (stopOutput.suppressOutput) {
|
|
138
|
-
// Was silent → upgrade to allow with summary
|
|
139
169
|
stopOutput = allowOutput(summaryText, "Stop");
|
|
140
170
|
}
|
|
141
171
|
else if (stopOutput.reason) {
|
|
142
|
-
// Was a block → append summary to reason
|
|
143
172
|
stopOutput.reason += "\n\n" + summaryText;
|
|
144
173
|
}
|
|
145
174
|
}
|
|
146
175
|
}
|
|
147
176
|
catch { /* fail-open: summary failure never blocks */ }
|
|
177
|
+
// Phase 3: Smart orchestration guidance.
|
|
178
|
+
// Appended to Stop output so the next assistant turn sees explicit
|
|
179
|
+
// next-step instructions (Agent() call + workflow boundary).
|
|
180
|
+
try {
|
|
181
|
+
const guidance = await buildWorkflowGuidance(projectDir, wfState, sessionId);
|
|
182
|
+
if (guidance) {
|
|
183
|
+
if (stopOutput.suppressOutput) {
|
|
184
|
+
stopOutput = allowOutput(guidance, "Stop");
|
|
185
|
+
}
|
|
186
|
+
else if (stopOutput.reason) {
|
|
187
|
+
stopOutput.reason += "\n\n" + guidance;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
catch { /* fail-open */ }
|
|
148
192
|
writeOutput(stopOutput);
|
|
149
193
|
// Trace for Stop
|
|
150
194
|
appendTrace(projectDir, {
|
|
@@ -160,11 +204,12 @@ async function main() {
|
|
|
160
204
|
}
|
|
161
205
|
// Dispatch to enforcement engine with timing
|
|
162
206
|
const start = Date.now();
|
|
163
|
-
let
|
|
207
|
+
let result = dispatch(event, ir, rawInput, state);
|
|
208
|
+
let output = result?.output ?? silentOutput();
|
|
164
209
|
const durationMs = Date.now() - start;
|
|
165
|
-
//
|
|
210
|
+
// PostToolUse side effects: session read tracking + surgeon reflection gate.
|
|
211
|
+
// Context gate moved to PreToolUse (block mode) in Phase 2B.
|
|
166
212
|
if (event === "PostToolUse") {
|
|
167
|
-
// 1. Track session reads
|
|
168
213
|
try {
|
|
169
214
|
const toolName = String(rawInput.tool_name ?? "");
|
|
170
215
|
if (toolName === "Read") {
|
|
@@ -174,22 +219,6 @@ async function main() {
|
|
|
174
219
|
await appendSessionRead(projectDir, readPath, sessionId);
|
|
175
220
|
}
|
|
176
221
|
}
|
|
177
|
-
// 2. Context gate (Phase 1: warn mode)
|
|
178
|
-
const writeTools = new Set(["Edit", "Write", "Bash"]);
|
|
179
|
-
if (writeTools.has(toolName)) {
|
|
180
|
-
const wfStateForCtx = await readWorkflowState(projectDir, sessionId);
|
|
181
|
-
if (wfStateForCtx?.active && ir.source_dna_ids.length > 0) {
|
|
182
|
-
const contextGateOutput = await handleContextGate(ir, wfStateForCtx, projectDir, sessionId);
|
|
183
|
-
if (contextGateOutput) {
|
|
184
|
-
if (output.suppressOutput) {
|
|
185
|
-
output = contextGateOutput;
|
|
186
|
-
}
|
|
187
|
-
else if (output.reason) {
|
|
188
|
-
output = { ...output, reason: output.reason + "\n" + (contextGateOutput.reason ?? "") };
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
222
|
}
|
|
194
223
|
catch { /* fail-open */ }
|
|
195
224
|
try {
|
|
@@ -306,16 +335,16 @@ function dispatch(event, ir, input, state) {
|
|
|
306
335
|
message: typeof input.message === "string" ? input.message : undefined,
|
|
307
336
|
});
|
|
308
337
|
case "Stop":
|
|
309
|
-
return
|
|
338
|
+
return null;
|
|
310
339
|
case "SessionStart":
|
|
311
340
|
return enforceSessionStart(ir, {
|
|
312
341
|
cwd: typeof input.cwd === "string" ? input.cwd : undefined,
|
|
313
|
-
|
|
342
|
+
sessionId: typeof input.session_id === "string" ? input.session_id
|
|
314
343
|
: typeof input.sessionId === "string" ? input.sessionId : undefined,
|
|
315
344
|
trigger: typeof input.trigger === "string" ? input.trigger : undefined,
|
|
316
345
|
});
|
|
317
346
|
default:
|
|
318
|
-
return
|
|
347
|
+
return null;
|
|
319
348
|
}
|
|
320
349
|
}
|
|
321
350
|
// ── IR Loading ─────────────────────────────────────────────
|
|
@@ -504,32 +533,112 @@ async function loadReflectionConfig(projectDir, _workflowName, _stepId) {
|
|
|
504
533
|
return null;
|
|
505
534
|
}
|
|
506
535
|
}
|
|
507
|
-
// ──
|
|
536
|
+
// ── PreToolUse Gates (Phase 2B) ──────────────────────────
|
|
537
|
+
/**
|
|
538
|
+
* Evaluate PreToolUse gates in priority order:
|
|
539
|
+
* 1. Workflow Boundary — block Skill() after workflow completed
|
|
540
|
+
* 2. Context Gate — block Edit/Write/Bash when required context files unread
|
|
541
|
+
*
|
|
542
|
+
* Returns the first triggered block, or null if all gates pass.
|
|
543
|
+
* Fail-open on any exception.
|
|
544
|
+
*/
|
|
545
|
+
export async function handlePreToolGates(ir, rawInput, wfState, projectDir, sessionId) {
|
|
546
|
+
try {
|
|
547
|
+
const toolName = typeof rawInput.tool_name === "string" ? rawInput.tool_name : "";
|
|
548
|
+
const toolInput = typeof rawInput.tool_input === "object" && rawInput.tool_input !== null
|
|
549
|
+
? rawInput.tool_input : {};
|
|
550
|
+
// Gate 1: Workflow Boundary — Skill invoked after workflow finished.
|
|
551
|
+
// Interpret "completed" as a state record existing with active === false.
|
|
552
|
+
if (toolName === "Skill" && wfState && wfState.active === false) {
|
|
553
|
+
const skillName = typeof toolInput.skill === "string"
|
|
554
|
+
? toolInput.skill
|
|
555
|
+
: typeof toolInput.name === "string" ? toolInput.name : "unknown";
|
|
556
|
+
const result = checkWorkflowBoundary(true, skillName);
|
|
557
|
+
if (result.output.continue === false) {
|
|
558
|
+
return { output: result.output, matched_rule: "workflow_boundary" };
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
// Gate 2: Context Gate — block write tools when required context files unread.
|
|
562
|
+
const writeTools = new Set(["Edit", "Write", "Bash"]);
|
|
563
|
+
if (writeTools.has(toolName) && wfState?.active && ir.context_files) {
|
|
564
|
+
const required = [];
|
|
565
|
+
if (ir.context_files.mandatory)
|
|
566
|
+
required.push(...ir.context_files.mandatory);
|
|
567
|
+
if (ir.context_files.per_role?.[wfState.current_role]) {
|
|
568
|
+
required.push(...ir.context_files.per_role[wfState.current_role]);
|
|
569
|
+
}
|
|
570
|
+
if (ir.context_files.per_workflow?.[wfState.workflow]) {
|
|
571
|
+
required.push(...ir.context_files.per_workflow[wfState.workflow]);
|
|
572
|
+
}
|
|
573
|
+
if (required.length > 0) {
|
|
574
|
+
const sessionReadsState = await readSessionReads(projectDir, sessionId);
|
|
575
|
+
const ready = checkContextReadiness(sessionReadsState.read_files, required);
|
|
576
|
+
if (!ready.ready) {
|
|
577
|
+
return {
|
|
578
|
+
output: blockOutput(`[Intent DNA] Context Gate: required context files not yet read — ${ready.missing.join(", ")}. Read them before attempting ${toolName}.`),
|
|
579
|
+
matched_rule: "context_gate",
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
catch {
|
|
586
|
+
// Fail-open: gate failures never block execution
|
|
587
|
+
}
|
|
588
|
+
return null;
|
|
589
|
+
}
|
|
590
|
+
// ── Stop guidance (Phase 3) ───────────────────────────────
|
|
508
591
|
/**
|
|
509
|
-
*
|
|
510
|
-
*
|
|
511
|
-
*
|
|
592
|
+
* Build next-step guidance for Stop hook.
|
|
593
|
+
* Returns a string to append to the Stop output's reason, or null if
|
|
594
|
+
* no explicit guidance applies (normal flow).
|
|
595
|
+
*
|
|
596
|
+
* Guidance cases:
|
|
597
|
+
* 1. Surgeon stalled — fail_count has reached the step's max_attempts
|
|
598
|
+
* for a step that defines handoff_to. Emit Agent() dispatch to the
|
|
599
|
+
* handoff role with experience chain context.
|
|
600
|
+
* 2. Workflow complete — wfState exists but active=false. Emit STOP
|
|
601
|
+
* directive so the assistant doesn't auto-start another workflow.
|
|
602
|
+
*
|
|
603
|
+
* Fail-open: any exception returns null.
|
|
512
604
|
*/
|
|
513
|
-
async function
|
|
514
|
-
if (!
|
|
605
|
+
export async function buildWorkflowGuidance(projectDir, wfState, sessionId) {
|
|
606
|
+
if (!wfState)
|
|
515
607
|
return null;
|
|
516
|
-
|
|
517
|
-
if (
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
required.push(...ir.context_files.per_role[wfState.current_role]);
|
|
608
|
+
// Case 2: workflow marked completed (inactive state record still present).
|
|
609
|
+
if (wfState.active === false) {
|
|
610
|
+
return `[DNA WORKFLOW] Workflow "${wfState.workflow}" is complete. ` +
|
|
611
|
+
`Output the final report and STOP. Do NOT start another workflow.`;
|
|
521
612
|
}
|
|
522
|
-
|
|
523
|
-
|
|
613
|
+
// Case 1: surgeon stalled — load reflection config sidecar + current attempts.
|
|
614
|
+
try {
|
|
615
|
+
const reflectionConfig = await loadReflectionConfig(projectDir, wfState.workflow, wfState.current_step);
|
|
616
|
+
if (!reflectionConfig?.max_attempts)
|
|
617
|
+
return null;
|
|
618
|
+
const attempts = await readSurgeonAttempts(projectDir, sessionId);
|
|
619
|
+
if (attempts.fail_count < reflectionConfig.max_attempts)
|
|
620
|
+
return null;
|
|
621
|
+
const handoffTo = reflectionConfig.handoff_to ?? "handoff role";
|
|
622
|
+
const agentType = `dna-${toKebabCase(handoffTo)}`;
|
|
623
|
+
const expCount = attempts.experience_chain?.length ?? 0;
|
|
624
|
+
return `[DNA WORKFLOW] Surgeon stalled — ${attempts.fail_count} consecutive no-progress attempts ` +
|
|
625
|
+
`(limit: ${reflectionConfig.max_attempts}).\n` +
|
|
626
|
+
`Next step: re-analyze via handoff. Dispatch:\n` +
|
|
627
|
+
`Agent(\n` +
|
|
628
|
+
` subagent_type="${agentType}",\n` +
|
|
629
|
+
` prompt="Re-analyze the current step. Read .dna/state/workflow/experience.md ` +
|
|
630
|
+
`for ${expCount} previous failure notes — do NOT repeat those approaches."\n` +
|
|
631
|
+
`)`;
|
|
524
632
|
}
|
|
525
|
-
|
|
633
|
+
catch {
|
|
526
634
|
return null;
|
|
527
|
-
const sessionReadsState = await readSessionReads(projectDir, sessionId);
|
|
528
|
-
const result = checkContextReadiness(sessionReadsState.read_files, required);
|
|
529
|
-
if (!result.ready) {
|
|
530
|
-
return allowOutput(`WARN [Intent DNA] Context Gate: Required files not yet read: ${result.missing.join(", ")}. Read them before continuing.`);
|
|
531
635
|
}
|
|
532
|
-
|
|
636
|
+
}
|
|
637
|
+
function toKebabCase(s) {
|
|
638
|
+
return s
|
|
639
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
640
|
+
.replace(/[_\s]+/g, "-")
|
|
641
|
+
.toLowerCase();
|
|
533
642
|
}
|
|
534
643
|
// ── Entry Point ────────────────────────────────────────────
|
|
535
644
|
main().catch(() => {
|
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;
|