intentdna 1.5.20 → 1.6.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/README.md +5 -2
- package/dist/cli/commands/sync.js +10 -8
- package/dist/compiler/cascade.d.ts +2 -1
- package/dist/compiler/cascade.js +25 -0
- package/dist/hooks/cli.d.ts +16 -1
- package/dist/hooks/cli.js +242 -33
- package/dist/hooks/enforce.d.ts +6 -6
- package/dist/hooks/enforce.js +103 -42
- package/dist/hooks/state.d.ts +21 -1
- package/dist/hooks/state.js +119 -4
- package/dist/mcp/tools-state.js +8 -0
- package/dist/runtime/settings-adapter.js +2 -2
- package/dist/runtime/skill-adapter.d.ts +2 -1
- package/dist/runtime/skill-adapter.js +170 -0
- package/dist/schema/types.d.ts +64 -5
- package/dist/schema/validate.js +111 -0
- package/dist/templates/code-review-pipeline.dna.yaml +1 -0
- package/dist/templates/flutter-refactoring-rescue.dna.yaml +3 -0
- package/dist/templates/flutter-rewrite.dna.yaml +132 -23
- package/dist/templates/full-pipeline.dna.yaml +3 -0
- package/package.json +1 -1
- package/spec/schema-spec.md +3 -0
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
{
|
|
10
10
|
"name": "intentdna",
|
|
11
11
|
"description": "Declarative policy layer for AI agent behavior with plugin-managed hook runtime for Claude Code.",
|
|
12
|
-
"version": "1.
|
|
12
|
+
"version": "1.6.0",
|
|
13
13
|
"author": {
|
|
14
14
|
"name": "Samuel"
|
|
15
15
|
},
|
|
@@ -25,5 +25,5 @@
|
|
|
25
25
|
]
|
|
26
26
|
}
|
|
27
27
|
],
|
|
28
|
-
"version": "1.
|
|
28
|
+
"version": "1.6.0"
|
|
29
29
|
}
|
package/README.md
CHANGED
|
@@ -8,10 +8,11 @@ Intent DNA turns intent, policy, and accumulated experience into enforceable age
|
|
|
8
8
|
|
|
9
9
|
## What It Is
|
|
10
10
|
|
|
11
|
-
Intent DNA is a compiler and runtime toolkit for managing agent behavior across
|
|
11
|
+
Intent DNA is a compiler and runtime toolkit for managing agent behavior across four layers:
|
|
12
12
|
|
|
13
13
|
- **Behavioral guidance** via generated prompt/context files
|
|
14
14
|
- **Deterministic enforcement** via hooks and runtime checks
|
|
15
|
+
- **Artifact handoff** via session-scoped manifests and resolver facts
|
|
15
16
|
- **Feedback loops** via trace, audit, and epigenetic evolution
|
|
16
17
|
|
|
17
18
|
It governs the agents operating on your systems rather than re-implementing the systems themselves.
|
|
@@ -81,11 +82,12 @@ Typical outputs include:
|
|
|
81
82
|
|--------|---------|
|
|
82
83
|
| `CLAUDE.md` | Behavioral guidance |
|
|
83
84
|
| `.dna/compiled/ir.json` | Runtime-readable compiled constraints |
|
|
85
|
+
| `.dna/state/sessions/*/handoffs/*.json` | Local artifact manifests for workflow handoff facts |
|
|
84
86
|
| `.claude/agents/*.md` | Role definitions |
|
|
85
87
|
| `.claude/skills/*/SKILL.md` | Workflow entrypoints |
|
|
86
88
|
| `.claude/settings.json` or plugin runtime | Hook wiring |
|
|
87
89
|
|
|
88
|
-
**Key idea:** prompts can drift, but runtime constraints and feedback loops make behavior governable.
|
|
90
|
+
**Key idea:** prompts can drift, but runtime constraints, resolver-backed handoffs, and feedback loops make behavior governable.
|
|
89
91
|
|
|
90
92
|
---
|
|
91
93
|
|
|
@@ -97,6 +99,7 @@ Intent DNA currently focuses on:
|
|
|
97
99
|
- Role-based agent workflows
|
|
98
100
|
- Template-driven project bootstrap and upgrades
|
|
99
101
|
- Deterministic guardrails and stateful enforcement
|
|
102
|
+
- Resolver-backed artifact handoff between workflow steps
|
|
100
103
|
- Dogfood-driven template iteration
|
|
101
104
|
|
|
102
105
|
Adapter and ecosystem direction are tracked in [ROADMAP.md](ROADMAP.md), not duplicated here.
|
|
@@ -20,7 +20,7 @@ import { parseYAML } from "../../schema/yaml-parser.js";
|
|
|
20
20
|
import { compileToMarkdown, injectIntoFile, removeFromFile } from "../../runtime/markdown.js";
|
|
21
21
|
import { compileAllRolesToAgentMD, writeAgentMDFiles, removeAgentMDFiles } from "../../runtime/agent-md.js";
|
|
22
22
|
import { removeWorkflowScripts } from "../../runtime/workflow-runner.js";
|
|
23
|
-
import { compileWorkflowToSkill, writeSkillFiles } from "../../runtime/skill-adapter.js";
|
|
23
|
+
import { compileWorkflowToSkill, compileControllerToSkill, writeSkillFiles } from "../../runtime/skill-adapter.js";
|
|
24
24
|
import { compilePluginSettings, detectEnabledEvents, mergeSettingsFile, recommendHookTimeouts } from "../../runtime/settings-adapter.js";
|
|
25
25
|
import { createCompiledIR, writeCompiledIR } from "../../runtime/plugin-adapter.js";
|
|
26
26
|
import { cascadeDNA } from "../../compiler/cascade.js";
|
|
@@ -127,11 +127,9 @@ export async function cleanStaleDNAFiles(dir, type, activeNamespaces) {
|
|
|
127
127
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
128
128
|
for (const entry of entries) {
|
|
129
129
|
const fullPath = resolve(dir, entry.name);
|
|
130
|
-
//
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
// Skip if namespace doesn't match active namespaces
|
|
134
|
-
if (!fileNamespace || !activeNamespaces.includes(fileNamespace)) {
|
|
130
|
+
// Match active namespace prefixes directly so multi-hyphen names like
|
|
131
|
+
// dna-frw-fix-loop still resolve to namespace frw.
|
|
132
|
+
if (!activeNamespaces.some(ns => entry.name.startsWith(`dna-${ns}-`))) {
|
|
135
133
|
continue;
|
|
136
134
|
}
|
|
137
135
|
if (type === "agents" && entry.isFile() && entry.name.endsWith(".md")) {
|
|
@@ -653,6 +651,7 @@ export async function runSync(opts) {
|
|
|
653
651
|
const dnas = loadedDNAs;
|
|
654
652
|
const cascadedForSkills = cascadeDNA(dnas);
|
|
655
653
|
const workflows = cascadedForSkills.workflows;
|
|
654
|
+
const controllers = cascadedForSkills.controllers;
|
|
656
655
|
const roles = cascadedForSkills.roles;
|
|
657
656
|
// Collect and resolve variables from all DNA files
|
|
658
657
|
const rawVars = {};
|
|
@@ -676,8 +675,8 @@ export async function runSync(opts) {
|
|
|
676
675
|
process.stderr.write(`MCP: wrote ${Object.keys(mcpDeps).length} server(s) to ${mcpJsonPath}\n`);
|
|
677
676
|
}
|
|
678
677
|
}
|
|
679
|
-
if (Object.keys(workflows).length === 0) {
|
|
680
|
-
process.stderr.write("No workflow defined — no skills to generate\n");
|
|
678
|
+
if (Object.keys(workflows).length === 0 && Object.keys(controllers).length === 0) {
|
|
679
|
+
process.stderr.write("No workflow or controller defined — no skills to generate\n");
|
|
681
680
|
}
|
|
682
681
|
else {
|
|
683
682
|
const { compileWorkflow } = await import("../../compiler/workflow.js");
|
|
@@ -688,6 +687,9 @@ export async function runSync(opts) {
|
|
|
688
687
|
skillResults.push(compileWorkflowToSkill(result.plan, roles, ir, variables));
|
|
689
688
|
}
|
|
690
689
|
}
|
|
690
|
+
for (const [name, controllerDef] of Object.entries(controllers)) {
|
|
691
|
+
skillResults.push(compileControllerToSkill(name, controllerDef, variables, workflows));
|
|
692
|
+
}
|
|
691
693
|
if (skillResults.length > 0) {
|
|
692
694
|
const written = await writeSkillFiles(skillResults, opts.skillsDir);
|
|
693
695
|
process.stderr.write(`Generated ${written.length} skill file(s) in ${opts.skillsDir}\n`);
|
|
@@ -10,12 +10,13 @@
|
|
|
10
10
|
* - attract/repel: same target accumulates, different targets independent
|
|
11
11
|
* - amplify/suppress: factors multiply (1.5x × 2.0x = 3.0x)
|
|
12
12
|
*/
|
|
13
|
-
import type { IntentDNA, Gene, ContextRegion, EpigeneticMarker, RoleDef, WorkflowDef, LegibilityAssetMap, VerifierCommandPolicy } from "../schema/types.js";
|
|
13
|
+
import type { IntentDNA, Gene, ContextRegion, EpigeneticMarker, RoleDef, WorkflowDef, LegibilityAssetMap, VerifierCommandPolicy, ControllerDef } from "../schema/types.js";
|
|
14
14
|
export interface CascadedDNA {
|
|
15
15
|
genes: Record<string, Gene>;
|
|
16
16
|
contexts: Record<string, ContextRegion>;
|
|
17
17
|
roles: Record<string, RoleDef>;
|
|
18
18
|
workflows: Record<string, WorkflowDef>;
|
|
19
|
+
controllers: Record<string, ControllerDef>;
|
|
19
20
|
epigenetic_markers: EpigeneticMarker[];
|
|
20
21
|
source_ids: string[];
|
|
21
22
|
/** Warnings from cascade process (e.g., cross-namespace gene collisions) */
|
package/dist/compiler/cascade.js
CHANGED
|
@@ -91,6 +91,20 @@ function prefixRecordKeys(record, ns) {
|
|
|
91
91
|
return record;
|
|
92
92
|
return Object.fromEntries(Object.entries(record).map(([key, value]) => [`${ns}_${key}`, value]));
|
|
93
93
|
}
|
|
94
|
+
function prefixController(controller, ns) {
|
|
95
|
+
return {
|
|
96
|
+
...controller,
|
|
97
|
+
diagnosis_workflow: `${ns}_${controller.diagnosis_workflow}`,
|
|
98
|
+
fix_workflow: `${ns}_${controller.fix_workflow}`,
|
|
99
|
+
roles: {
|
|
100
|
+
analyzer: `${ns}_${controller.roles.analyzer}`,
|
|
101
|
+
analysis_reviewer: `${ns}_${controller.roles.analysis_reviewer}`,
|
|
102
|
+
surgeon: `${ns}_${controller.roles.surgeon}`,
|
|
103
|
+
fix_reviewer: `${ns}_${controller.roles.fix_reviewer}`,
|
|
104
|
+
test_runner: `${ns}_${controller.roles.test_runner}`,
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
94
108
|
function prefixLegibilityAssets(map, ns) {
|
|
95
109
|
if (!map)
|
|
96
110
|
return undefined;
|
|
@@ -176,6 +190,7 @@ export function cascadeDNA(layers) {
|
|
|
176
190
|
const mergedContexts = {};
|
|
177
191
|
const mergedRoles = {};
|
|
178
192
|
const mergedWorkflows = {};
|
|
193
|
+
const mergedControllers = {};
|
|
179
194
|
const allMarkers = [];
|
|
180
195
|
const sourceIds = [];
|
|
181
196
|
const warnings = [];
|
|
@@ -225,6 +240,15 @@ export function cascadeDNA(layers) {
|
|
|
225
240
|
mergedWorkflows[name] = wf;
|
|
226
241
|
}
|
|
227
242
|
}
|
|
243
|
+
// Merge controllers: namespace-prefixed keys + workflow/role references
|
|
244
|
+
for (const [name, controller] of Object.entries(dna.controllers ?? {})) {
|
|
245
|
+
if (ns) {
|
|
246
|
+
mergedControllers[`${ns}_${name}`] = prefixController(controller, ns);
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
mergedControllers[name] = controller;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
228
252
|
// Collect epigenetic markers
|
|
229
253
|
if (dna.epigenetic?.markers) {
|
|
230
254
|
allMarkers.push(...dna.epigenetic.markers);
|
|
@@ -250,6 +274,7 @@ export function cascadeDNA(layers) {
|
|
|
250
274
|
contexts: mergedContexts,
|
|
251
275
|
roles: mergedRoles,
|
|
252
276
|
workflows: mergedWorkflows,
|
|
277
|
+
controllers: mergedControllers,
|
|
253
278
|
epigenetic_markers: allMarkers,
|
|
254
279
|
source_ids: sourceIds,
|
|
255
280
|
warnings: warnings.length > 0 ? warnings : undefined,
|
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 { ConstraintIR, VerifierSpec } from "../schema/types.js";
|
|
17
|
+
import type { ArtifactFact, ConstraintIR, VerifierSpec } from "../schema/types.js";
|
|
18
18
|
import type { HookOutput } from "./protocol.js";
|
|
19
19
|
import { blockOutput } from "./protocol.js";
|
|
20
20
|
import { readWorkflowState } from "./state.js";
|
|
@@ -39,6 +39,20 @@ export declare function computeSummary(traces: Array<{
|
|
|
39
39
|
reason?: string;
|
|
40
40
|
}>): SessionSummary;
|
|
41
41
|
export declare function formatSummary(s: SessionSummary): string | null;
|
|
42
|
+
declare function recordProducedArtifacts(projectDir: string, ir: ConstraintIR, wfState: {
|
|
43
|
+
workflow: string;
|
|
44
|
+
current_step: string;
|
|
45
|
+
inputs?: Record<string, string>;
|
|
46
|
+
resolved_variables?: Record<string, string>;
|
|
47
|
+
}, sessionId?: string): Promise<ArtifactFact[]>;
|
|
48
|
+
declare function resolveWorkflowArtifactFacts(projectDir: string, ir: ConstraintIR, wfState: {
|
|
49
|
+
workflow: string;
|
|
50
|
+
current_step: string;
|
|
51
|
+
inputs?: Record<string, string>;
|
|
52
|
+
resolved_variables?: Record<string, string>;
|
|
53
|
+
}, sessionId?: string): Promise<ArtifactFact[]>;
|
|
54
|
+
export declare const recordProducedArtifactsForTest: typeof recordProducedArtifacts;
|
|
55
|
+
export declare const resolveWorkflowArtifactFactsForTest: typeof resolveWorkflowArtifactFacts;
|
|
42
56
|
export declare function appendStopVerifierWarnings(output: HookOutput, verifierResults: VerifierResultEntry[], currentStep?: string): HookOutput;
|
|
43
57
|
export declare function runVerifiersForTest(projectDir: string, ir: ConstraintIR, workflowState: {
|
|
44
58
|
workflow: string;
|
|
@@ -81,3 +95,4 @@ export declare function handlePreToolGates(ir: ConstraintIR, rawInput: Record<st
|
|
|
81
95
|
* Fail-open: any exception returns null.
|
|
82
96
|
*/
|
|
83
97
|
export declare function buildWorkflowGuidance(projectDir: string, wfState: Awaited<ReturnType<typeof readWorkflowState>>, sessionId?: string): Promise<string | null>;
|
|
98
|
+
export {};
|
package/dist/hooks/cli.js
CHANGED
|
@@ -14,14 +14,14 @@
|
|
|
14
14
|
*
|
|
15
15
|
* Fail-open: all errors → { continue: true, suppressOutput: true }
|
|
16
16
|
*/
|
|
17
|
-
import { readFile, realpath, stat } from "node:fs/promises";
|
|
17
|
+
import { mkdir, readFile, realpath, stat, unlink, writeFile } from "node:fs/promises";
|
|
18
18
|
import { spawn } from "node:child_process";
|
|
19
19
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
20
20
|
import { randomUUID } from "node:crypto";
|
|
21
21
|
import { readStdin, writeOutput, silentOutput, allowOutput, blockOutput, stopOutput } from "./protocol.js";
|
|
22
22
|
import { validateHookInput } from "./schema.js";
|
|
23
23
|
import { enforcePreToolUse, enforcePostToolUse, enforceUserPromptSubmit, enforceSubagentStop, enforcePreCompact, enforceNotification, enforceSessionStart, enforceStop, extractBashWritePaths, checkReflectionLimit, checkContextReadiness, checkWorkflowBoundary, } from "./enforce.js";
|
|
24
|
-
import { appendAudit, readWorkflowState, appendTrace, rotateTraces, readTraces, cleanStaleState, readSurgeonAttempts, writeSurgeonAttempts, appendSessionRead, readSessionReads, appendVerifierResult } from "./state.js";
|
|
24
|
+
import { appendAudit, readWorkflowState, appendTrace, rotateTraces, readTraces, cleanStaleState, readSurgeonAttempts, writeSurgeonAttempts, appendSessionRead, readSessionReads, appendVerifierResult, appendCompletedArtifact, artifactIdentity, buildArtifactKey, readArtifactManifest, resolveArtifactTemplate, safePathComponent, writeArtifactManifest } from "./state.js";
|
|
25
25
|
import { writeAuditEvent } from "../audit/index.js";
|
|
26
26
|
// ── Constants ──────────────────────────────────────────────
|
|
27
27
|
const DEFAULT_IR_PATH = ".dna/compiled/ir.json";
|
|
@@ -116,13 +116,15 @@ async function main() {
|
|
|
116
116
|
completed_artifacts: wfStateRaw.completed_artifacts,
|
|
117
117
|
iteration: wfStateRaw.iteration, // G4: pass iteration for state-driven rules
|
|
118
118
|
};
|
|
119
|
-
|
|
120
|
-
// enforceHandoffConsumes can skip blocks for files that already exist.
|
|
121
|
-
if (event === "PreToolUse") {
|
|
122
|
-
state.existingArtifactPaths = await scanConsumedArtifactPaths(projectDir, ir, wfStateRaw.workflow, wfStateRaw.current_step);
|
|
123
|
-
}
|
|
119
|
+
state.artifactFacts = await resolveWorkflowArtifactFacts(projectDir, ir, wfStateRaw, sessionId);
|
|
124
120
|
}
|
|
125
121
|
}
|
|
122
|
+
if (event === "PreToolUse" && wfStateRaw?.active) {
|
|
123
|
+
try {
|
|
124
|
+
await captureGitCommitBefore(projectDir, ir, wfStateRaw, rawInput, sessionId);
|
|
125
|
+
}
|
|
126
|
+
catch { /* fail-open */ }
|
|
127
|
+
}
|
|
126
128
|
// Phase 2B: PreToolUse gates (workflow boundary + context gate).
|
|
127
129
|
// Run before dispatch so the block shortcircuits the rest of the pipeline.
|
|
128
130
|
if (event === "PreToolUse") {
|
|
@@ -154,6 +156,7 @@ async function main() {
|
|
|
154
156
|
current_role: wfState.current_role,
|
|
155
157
|
started_at: wfState.started_at,
|
|
156
158
|
completed_artifacts: wfState.completed_artifacts,
|
|
159
|
+
artifact_facts: wfState.active ? await finalizeAndResolveArtifacts(projectDir, ir, wfState, sessionId) : [],
|
|
157
160
|
} : null;
|
|
158
161
|
const verifierResults = stopContext?.active
|
|
159
162
|
? await runStopVerifiersForTest(projectDir, ir, stopContext, sessionId)
|
|
@@ -245,6 +248,13 @@ async function main() {
|
|
|
245
248
|
}
|
|
246
249
|
}
|
|
247
250
|
catch { /* fail-open */ }
|
|
251
|
+
try {
|
|
252
|
+
if (wfStateRaw?.active) {
|
|
253
|
+
await recordGitCommitHandoff(projectDir, ir, wfStateRaw, rawInput, sessionId);
|
|
254
|
+
await recordProducedArtifacts(projectDir, ir, wfStateRaw, sessionId);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
catch { /* fail-open */ }
|
|
248
258
|
}
|
|
249
259
|
// Extract target file path for trace + pattern detection
|
|
250
260
|
const toolInput = typeof rawInput.tool_input === "object" && rawInput.tool_input !== null
|
|
@@ -395,36 +405,151 @@ async function loadIR(irPath) {
|
|
|
395
405
|
return null;
|
|
396
406
|
}
|
|
397
407
|
}
|
|
398
|
-
// ── Artifact
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
408
|
+
// ── Artifact Manifest Resolution ─────────────────────────
|
|
409
|
+
function workflowEntry(ir, workflowName, stepId) {
|
|
410
|
+
return ir.workflows_ir
|
|
411
|
+
?.find(w => w.workflow_name === workflowName)
|
|
412
|
+
?.handoff_chain.find(h => h.step_id === stepId) ?? null;
|
|
413
|
+
}
|
|
414
|
+
function findProducerStep(ir, workflowName, consumed) {
|
|
415
|
+
if (consumed.from)
|
|
416
|
+
return consumed.from;
|
|
417
|
+
const workflow = ir.workflows_ir?.find(w => w.workflow_name === workflowName);
|
|
418
|
+
const producer = workflow?.handoff_chain.find(h => h.produces?.some(p => p.type === consumed.type && ((p.path && consumed.path && p.path === consumed.path) ||
|
|
419
|
+
(p.name && consumed.name && p.name === consumed.name) ||
|
|
420
|
+
(p.artifact_id && consumed.artifact_id && p.artifact_id === consumed.artifact_id))));
|
|
421
|
+
return producer?.step_id ?? "external";
|
|
422
|
+
}
|
|
423
|
+
async function projectRelativeExistingPath(projectDir, path) {
|
|
424
|
+
const fullPath = resolve(projectDir, path);
|
|
425
|
+
let realProject;
|
|
426
|
+
let realTarget;
|
|
427
|
+
try {
|
|
428
|
+
realProject = await realpath(projectDir);
|
|
429
|
+
realTarget = await realpath(fullPath);
|
|
430
|
+
}
|
|
431
|
+
catch {
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
const rel = relative(realProject, realTarget);
|
|
435
|
+
if (rel === "" || rel.startsWith("..") || isAbsolute(rel))
|
|
436
|
+
return null;
|
|
437
|
+
await stat(realTarget);
|
|
438
|
+
return rel;
|
|
439
|
+
}
|
|
440
|
+
function resolvedArtifactIdentity(artifact, wfState) {
|
|
441
|
+
const resolvedPath = artifact.path ? resolveArtifactTemplate(artifact.path, wfState) : undefined;
|
|
442
|
+
if (artifact.path && !resolvedPath)
|
|
443
|
+
return null;
|
|
444
|
+
const identity = artifactIdentity(artifact, resolvedPath ?? undefined);
|
|
445
|
+
return identity ? { identity, resolvedPath: resolvedPath ?? undefined } : null;
|
|
446
|
+
}
|
|
447
|
+
async function writeManifestForArtifact(projectDir, workflowName, sessionId, producerStep, artifact, wfState, metadata) {
|
|
448
|
+
const resolved = resolvedArtifactIdentity(artifact, wfState);
|
|
449
|
+
if (!resolved)
|
|
450
|
+
return null;
|
|
451
|
+
const key = buildArtifactKey({ workflow: workflowName, sessionId, producerStep, type: artifact.type, identity: resolved.identity });
|
|
452
|
+
const fact = await writeArtifactManifest(projectDir, {
|
|
453
|
+
key,
|
|
454
|
+
type: artifact.type,
|
|
455
|
+
workflow: workflowName,
|
|
456
|
+
session_id: sessionId,
|
|
457
|
+
producer_step: producerStep,
|
|
458
|
+
identity: resolved.identity,
|
|
459
|
+
path: resolved.resolvedPath,
|
|
460
|
+
contract_path: artifact.path,
|
|
461
|
+
name: artifact.name,
|
|
462
|
+
artifact_id: artifact.artifact_id,
|
|
463
|
+
metadata,
|
|
464
|
+
});
|
|
465
|
+
await appendCompletedArtifact(projectDir, producerStep, {
|
|
466
|
+
type: artifact.type,
|
|
467
|
+
path: fact.manifest_path,
|
|
468
|
+
artifact_id: fact.id,
|
|
469
|
+
metadata,
|
|
470
|
+
}, sessionId);
|
|
471
|
+
return fact;
|
|
472
|
+
}
|
|
473
|
+
async function recordProducedArtifacts(projectDir, ir, wfState, sessionId) {
|
|
474
|
+
const sid = sessionId ?? "";
|
|
475
|
+
if (!sid)
|
|
476
|
+
return [];
|
|
477
|
+
const entry = workflowEntry(ir, wfState.workflow, wfState.current_step);
|
|
478
|
+
const facts = [];
|
|
479
|
+
for (const produced of entry?.produces ?? []) {
|
|
480
|
+
if (produced.type === "git_commit")
|
|
416
481
|
continue;
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
482
|
+
const resolved = resolvedArtifactIdentity(produced, wfState);
|
|
483
|
+
if (!resolved)
|
|
484
|
+
continue;
|
|
485
|
+
let metadata;
|
|
486
|
+
if (resolved.resolvedPath) {
|
|
487
|
+
const rel = await projectRelativeExistingPath(projectDir, resolved.resolvedPath);
|
|
488
|
+
if (!rel)
|
|
489
|
+
continue;
|
|
490
|
+
metadata = { source_path: rel };
|
|
421
491
|
}
|
|
422
|
-
|
|
423
|
-
|
|
492
|
+
else if (produced.type !== "summary" && produced.type !== "state" && produced.type !== "test_result") {
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
const fact = await writeManifestForArtifact(projectDir, wfState.workflow, sid, wfState.current_step, produced, wfState, metadata);
|
|
496
|
+
if (fact && await verifyArtifactFact(projectDir, fact))
|
|
497
|
+
facts.push(fact);
|
|
498
|
+
}
|
|
499
|
+
return facts;
|
|
500
|
+
}
|
|
501
|
+
async function verifyArtifactFact(projectDir, fact) {
|
|
502
|
+
if (fact.type === "file" || fact.type === "directory") {
|
|
503
|
+
if (!fact.path)
|
|
504
|
+
return false;
|
|
505
|
+
return (await projectRelativeExistingPath(projectDir, fact.path)) !== null;
|
|
506
|
+
}
|
|
507
|
+
if (fact.type === "test_result") {
|
|
508
|
+
if (!fact.path)
|
|
509
|
+
return true;
|
|
510
|
+
return (await projectRelativeExistingPath(projectDir, fact.path)) !== null;
|
|
511
|
+
}
|
|
512
|
+
if (fact.type === "git_commit") {
|
|
513
|
+
const commit = typeof fact.metadata?.commit === "string" ? fact.metadata.commit : undefined;
|
|
514
|
+
if (!commit)
|
|
515
|
+
return false;
|
|
516
|
+
return (await runGit(projectDir, ["cat-file", "-e", `${commit}^{commit}`])) !== null;
|
|
517
|
+
}
|
|
518
|
+
return true;
|
|
519
|
+
}
|
|
520
|
+
async function resolveWorkflowArtifactFacts(projectDir, ir, wfState, sessionId) {
|
|
521
|
+
const sid = sessionId ?? "";
|
|
522
|
+
if (!sid)
|
|
523
|
+
return [];
|
|
524
|
+
const facts = [];
|
|
525
|
+
const entry = workflowEntry(ir, wfState.workflow, wfState.current_step);
|
|
526
|
+
const contracts = [...(entry?.consumes ?? []), ...(entry?.produces ?? [])];
|
|
527
|
+
for (const artifact of contracts) {
|
|
528
|
+
if (artifact.required === false)
|
|
529
|
+
continue;
|
|
530
|
+
const producerStep = entry?.produces?.includes(artifact) ? wfState.current_step : findProducerStep(ir, wfState.workflow, artifact);
|
|
531
|
+
const resolved = resolvedArtifactIdentity(artifact, wfState);
|
|
532
|
+
if (!resolved)
|
|
533
|
+
continue;
|
|
534
|
+
const key = buildArtifactKey({ workflow: wfState.workflow, sessionId: sid, producerStep, type: artifact.type, identity: resolved.identity });
|
|
535
|
+
let fact = await readArtifactManifest(projectDir, key);
|
|
536
|
+
if (!fact && producerStep === "external" && artifact.path && resolved.resolvedPath) {
|
|
537
|
+
const rel = await projectRelativeExistingPath(projectDir, resolved.resolvedPath);
|
|
538
|
+
if (rel) {
|
|
539
|
+
fact = await writeManifestForArtifact(projectDir, wfState.workflow, sid, "external", artifact, wfState, { source_path: rel, external_input: true });
|
|
540
|
+
}
|
|
424
541
|
}
|
|
542
|
+
if (fact && await verifyArtifactFact(projectDir, fact))
|
|
543
|
+
facts.push(fact);
|
|
425
544
|
}
|
|
426
|
-
return
|
|
545
|
+
return facts;
|
|
546
|
+
}
|
|
547
|
+
async function finalizeAndResolveArtifacts(projectDir, ir, wfState, sessionId) {
|
|
548
|
+
await recordProducedArtifacts(projectDir, ir, wfState, sessionId);
|
|
549
|
+
return resolveWorkflowArtifactFacts(projectDir, ir, wfState, sessionId);
|
|
427
550
|
}
|
|
551
|
+
export const recordProducedArtifactsForTest = recordProducedArtifacts;
|
|
552
|
+
export const resolveWorkflowArtifactFactsForTest = resolveWorkflowArtifactFacts;
|
|
428
553
|
function clearBlockingVerifierCheckpoints(ir, workflowState) {
|
|
429
554
|
if (!workflowState || !ir.verifier_specs || ir.verifier_specs.length === 0)
|
|
430
555
|
return ir;
|
|
@@ -1002,6 +1127,89 @@ export async function runStopVerifiersForTest(projectDir, ir, workflowState, ses
|
|
|
1002
1127
|
results.push(...await runVerifiersForTest(projectDir, ir, workflowState, "stop", sessionId, options));
|
|
1003
1128
|
return results;
|
|
1004
1129
|
}
|
|
1130
|
+
// ── Handoff Artifact Recording ───────────────────────────
|
|
1131
|
+
function stepProducesGitCommit(ir, workflow, stepId) {
|
|
1132
|
+
const activeWf = ir.workflows_ir?.find(w => w.workflow_name === workflow);
|
|
1133
|
+
const currentEntry = activeWf?.handoff_chain.find(h => h.step_id === stepId);
|
|
1134
|
+
return Boolean(currentEntry?.produces?.some(p => p.type === "git_commit"));
|
|
1135
|
+
}
|
|
1136
|
+
async function runGit(projectDir, args) {
|
|
1137
|
+
return new Promise((resolveResult) => {
|
|
1138
|
+
const child = spawn("git", args, { cwd: projectDir, stdio: ["ignore", "pipe", "ignore"] });
|
|
1139
|
+
const chunks = [];
|
|
1140
|
+
child.stdout.on("data", (chunk) => chunks.push(chunk));
|
|
1141
|
+
child.on("close", (code) => {
|
|
1142
|
+
if (code !== 0) {
|
|
1143
|
+
resolveResult(null);
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
resolveResult(Buffer.concat(chunks).toString("utf-8").trim());
|
|
1147
|
+
});
|
|
1148
|
+
child.on("error", () => resolveResult(null));
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
function gitCommitCapturePath(projectDir, workflow, stepId, sessionId) {
|
|
1152
|
+
const safeWorkflow = safePathComponent(workflow, "workflow");
|
|
1153
|
+
const safeStepId = safePathComponent(stepId, "step_id");
|
|
1154
|
+
if (!sessionId)
|
|
1155
|
+
return join(projectDir, ".dna", "state", "git-commit-before", safeWorkflow, `${safeStepId}.json`);
|
|
1156
|
+
return join(projectDir, ".dna", "state", "sessions", safePathComponent(sessionId, "session_id"), "handoffs", safeWorkflow, safeStepId, "git-commit-before.json");
|
|
1157
|
+
}
|
|
1158
|
+
async function captureGitCommitBefore(projectDir, ir, workflowState, input, sessionId) {
|
|
1159
|
+
if (!sessionId)
|
|
1160
|
+
return;
|
|
1161
|
+
if (!stepProducesGitCommit(ir, workflowState.workflow, workflowState.current_step))
|
|
1162
|
+
return;
|
|
1163
|
+
if (input.tool_name !== "Bash")
|
|
1164
|
+
return;
|
|
1165
|
+
const command = typeof input.tool_input?.command === "string"
|
|
1166
|
+
? input.tool_input.command
|
|
1167
|
+
: "";
|
|
1168
|
+
if (!/\bgit\s+commit\b/.test(command))
|
|
1169
|
+
return;
|
|
1170
|
+
const before = await runGit(projectDir, ["rev-parse", "HEAD"]);
|
|
1171
|
+
if (!before)
|
|
1172
|
+
return;
|
|
1173
|
+
const capturePath = gitCommitCapturePath(projectDir, workflowState.workflow, workflowState.current_step, sessionId);
|
|
1174
|
+
await mkdir(dirname(capturePath), { recursive: true });
|
|
1175
|
+
await writeFile(capturePath, JSON.stringify({ before, captured_at: new Date().toISOString() }, null, 2), "utf-8");
|
|
1176
|
+
}
|
|
1177
|
+
async function recordGitCommitHandoff(projectDir, ir, workflowState, input, sessionId) {
|
|
1178
|
+
if (!sessionId)
|
|
1179
|
+
return;
|
|
1180
|
+
if (!stepProducesGitCommit(ir, workflowState.workflow, workflowState.current_step))
|
|
1181
|
+
return;
|
|
1182
|
+
if (input.tool_name !== "Bash")
|
|
1183
|
+
return;
|
|
1184
|
+
const command = typeof input.tool_input?.command === "string"
|
|
1185
|
+
? input.tool_input.command
|
|
1186
|
+
: "";
|
|
1187
|
+
if (!/\bgit\s+commit\b/.test(command))
|
|
1188
|
+
return;
|
|
1189
|
+
const capturePath = gitCommitCapturePath(projectDir, workflowState.workflow, workflowState.current_step, sessionId);
|
|
1190
|
+
let base;
|
|
1191
|
+
try {
|
|
1192
|
+
const capture = JSON.parse(await readFile(capturePath, "utf-8"));
|
|
1193
|
+
base = capture.before;
|
|
1194
|
+
await unlink(capturePath).catch(() => { });
|
|
1195
|
+
}
|
|
1196
|
+
catch {
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
if (!base)
|
|
1200
|
+
return;
|
|
1201
|
+
const commit = await runGit(projectDir, ["rev-parse", "HEAD"]);
|
|
1202
|
+
if (!commit || commit === base)
|
|
1203
|
+
return;
|
|
1204
|
+
const subject = await runGit(projectDir, ["log", "-1", "--format=%s", commit]);
|
|
1205
|
+
const changedPaths = base
|
|
1206
|
+
? (await runGit(projectDir, ["diff", "--name-only", base, commit]))?.split("\n").filter(Boolean) ?? []
|
|
1207
|
+
: [];
|
|
1208
|
+
const dirtyAfter = (await runGit(projectDir, ["status", "--porcelain"]))?.split("\n").filter(Boolean) ?? [];
|
|
1209
|
+
const entry = workflowEntry(ir, workflowState.workflow, workflowState.current_step);
|
|
1210
|
+
const contract = entry?.produces?.find(p => p.type === "git_commit") ?? { type: "git_commit", description: "Git commit" };
|
|
1211
|
+
await writeManifestForArtifact(projectDir, workflowState.workflow, sessionId ?? "", workflowState.current_step, contract, workflowState, { commit, base, subject, changed_paths: changedPaths, dirty_after: dirtyAfter });
|
|
1212
|
+
}
|
|
1005
1213
|
// ── Surgeon Reflection Gate ──────────────────────────────
|
|
1006
1214
|
/** Regex to parse test green count from vitest/flutter test output */
|
|
1007
1215
|
const TEST_PASSED_RE = /(\d+)\s+(?:tests?\s+)?passed/i;
|
|
@@ -1176,7 +1384,8 @@ export async function handlePreToolGates(ir, rawInput, wfState, projectDir, sess
|
|
|
1176
1384
|
}
|
|
1177
1385
|
if (required.length > 0) {
|
|
1178
1386
|
const sessionReadsState = await readSessionReads(projectDir, sessionId);
|
|
1179
|
-
const
|
|
1387
|
+
const resolvedRequired = required.map(path => resolveArtifactTemplate(path, wfState) ?? path);
|
|
1388
|
+
const ready = checkContextReadiness(sessionReadsState.read_files, resolvedRequired);
|
|
1180
1389
|
if (!ready.ready) {
|
|
1181
1390
|
return {
|
|
1182
1391
|
output: blockOutput(`[Intent DNA] Context Gate: required context files not yet read — ${ready.missing.join(", ")}. Read them before attempting ${toolName}.`),
|
package/dist/hooks/enforce.d.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*
|
|
14
14
|
* Step checkpoints are handled by the CLI, not here.
|
|
15
15
|
*/
|
|
16
|
-
import type { ConstraintIR, RoleDef, CompletedArtifactEntry } from "../schema/types.js";
|
|
16
|
+
import type { ConstraintIR, RoleDef, ArtifactFact, CompletedArtifactEntry } from "../schema/types.js";
|
|
17
17
|
import type { PreToolUseInput, PostToolUseInput, UserPromptSubmitInput, SubagentStopInput, NotificationInput, EnforceResult } from "./protocol.js";
|
|
18
18
|
/** SessionStart input fields */
|
|
19
19
|
export interface SessionStartInput {
|
|
@@ -31,9 +31,8 @@ export interface EnforceState {
|
|
|
31
31
|
/** G4: current workflow iteration (for relax_after_iteration rules) */
|
|
32
32
|
iteration?: number;
|
|
33
33
|
};
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
-
existingArtifactPaths?: Set<string>;
|
|
34
|
+
/** Resolved artifact facts loaded by CLI/state before pure enforcement. */
|
|
35
|
+
artifactFacts?: ArtifactFact[];
|
|
37
36
|
}
|
|
38
37
|
/**
|
|
39
38
|
* Enforce PreToolUse constraints.
|
|
@@ -90,6 +89,7 @@ export interface StopWorkflowContext {
|
|
|
90
89
|
current_role: string;
|
|
91
90
|
started_at: string;
|
|
92
91
|
completed_artifacts?: CompletedArtifactEntry[];
|
|
92
|
+
artifact_facts?: ArtifactFact[];
|
|
93
93
|
}
|
|
94
94
|
/**
|
|
95
95
|
* Enforce Stop hook — verify workflow checkpoint completion.
|
|
@@ -112,9 +112,9 @@ export declare function enforceStop(ir: ConstraintIR, input: StopEnforceInput, w
|
|
|
112
112
|
export declare function enforceHandoffProduces(ir: ConstraintIR, wfState: {
|
|
113
113
|
current_step: string;
|
|
114
114
|
workflow: string;
|
|
115
|
-
|
|
115
|
+
artifact_facts?: ArtifactFact[];
|
|
116
116
|
}): EnforceResult | null;
|
|
117
|
-
/** Check if a file path is allowed by a list of write globs
|
|
117
|
+
/** Check if a file path is allowed by a list of write globs. */
|
|
118
118
|
export declare function checkWriteAllowed(filePath: string, allowedGlobs: string[]): boolean;
|
|
119
119
|
/**
|
|
120
120
|
* Extract directory prefix from a glob pattern.
|