intentdna 1.5.9 → 1.5.10

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.
@@ -9,7 +9,7 @@
9
9
  {
10
10
  "name": "intentdna",
11
11
  "description": "DNA template compilation + runtime enforcement",
12
- "version": "1.5.9",
12
+ "version": "1.5.10",
13
13
  "source": "./"
14
14
  }
15
15
  ]
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "intentdna",
3
- "version": "1.5.9",
3
+ "version": "1.5.10",
4
4
  "description": "Declarative policy layer for AI agent governance"
5
5
  }
@@ -226,7 +226,7 @@ function resolveRunIf(runIf) {
226
226
  * Map WorkflowStepDef to compiled WorkflowStep.
227
227
  */
228
228
  function toWorkflowStep(def) {
229
- return {
229
+ const step = {
230
230
  id: def.id,
231
231
  role: def.role,
232
232
  description: def.description ?? def.id,
@@ -238,6 +238,19 @@ function toWorkflowStep(def) {
238
238
  checkpoints: def.checkpoints && def.checkpoints.length > 0 ? [...def.checkpoints] : null,
239
239
  handoff: def.handoff ?? null,
240
240
  };
241
+ if (def.max_attempts !== undefined)
242
+ step.max_attempts = def.max_attempts;
243
+ if (def.on_fail !== undefined)
244
+ step.on_fail = def.on_fail;
245
+ if (def.handoff_to !== undefined)
246
+ step.handoff_to = def.handoff_to;
247
+ if (def.max_handoffs !== undefined)
248
+ step.max_handoffs = def.max_handoffs;
249
+ if (def.on_handoff_exhausted !== undefined)
250
+ step.on_handoff_exhausted = def.on_handoff_exhausted;
251
+ if (def.blocked_items_path !== undefined)
252
+ step.blocked_items_path = def.blocked_items_path;
253
+ return step;
241
254
  }
242
255
  /**
243
256
  * Generate a Mermaid flowchart diagram from a WorkflowPlan.
package/dist/hooks/cli.js CHANGED
@@ -15,11 +15,11 @@
15
15
  * Fail-open: all errors → { continue: true, suppressOutput: true }
16
16
  */
17
17
  import { readFile, stat } from "node:fs/promises";
18
- import { resolve } from "node:path";
18
+ import { join, resolve } from "node:path";
19
19
  import { randomUUID } from "node:crypto";
20
20
  import { readStdin, writeOutput, silentOutput, allowOutput } from "./protocol.js";
21
- import { enforcePreToolUse, enforcePostToolUse, enforceUserPromptSubmit, enforceSubagentStop, enforcePreCompact, enforceNotification, enforceSessionStart, enforceStop, extractBashWritePaths, } from "./enforce.js";
22
- import { appendAudit, readWorkflowState, appendTrace, rotateTraces, readTraces, cleanStaleState } from "./state.js";
21
+ import { enforcePreToolUse, enforcePostToolUse, enforceUserPromptSubmit, enforceSubagentStop, enforcePreCompact, enforceNotification, enforceSessionStart, enforceStop, extractBashWritePaths, checkReflectionLimit, } from "./enforce.js";
22
+ import { appendAudit, readWorkflowState, appendTrace, rotateTraces, readTraces, cleanStaleState, readSurgeonAttempts, writeSurgeonAttempts } from "./state.js";
23
23
  // ── Constants ──────────────────────────────────────────────
24
24
  const DEFAULT_IR_PATH = ".dna/compiled/ir.json";
25
25
  const VALID_EVENTS = new Set([
@@ -160,6 +160,22 @@ async function main() {
160
160
  const start = Date.now();
161
161
  let output = dispatch(event, ir, rawInput, state);
162
162
  const durationMs = Date.now() - start;
163
+ // Surgeon reflection gate (PostToolUse only, async)
164
+ if (event === "PostToolUse") {
165
+ try {
166
+ const reflectionOutput = await handleSurgeonReflection(ir, rawInput, projectDir, sessionId);
167
+ if (reflectionOutput) {
168
+ // Merge: keep original output but append reflection warning
169
+ if (output.suppressOutput) {
170
+ output = reflectionOutput;
171
+ }
172
+ else if (output.reason) {
173
+ output = { ...output, reason: output.reason + "\n" + (reflectionOutput.reason ?? "") };
174
+ }
175
+ }
176
+ }
177
+ catch { /* fail-open */ }
178
+ }
163
179
  // Extract target file path for trace + pattern detection
164
180
  const toolInput = typeof rawInput.tool_input === "object" && rawInput.tool_input !== null
165
181
  ? rawInput.tool_input : undefined;
@@ -335,6 +351,125 @@ async function scanConsumedArtifactPaths(projectDir, ir, workflowName, currentSt
335
351
  }
336
352
  return paths;
337
353
  }
354
+ // ── Surgeon Reflection Gate ──────────────────────────────
355
+ /** Regex to parse test green count from vitest/flutter test output */
356
+ const TEST_PASSED_RE = /(\d+)\s+(?:tests?\s+)?passed/i;
357
+ /**
358
+ * Handle surgeon reflection gate for PostToolUse events.
359
+ * Tracks edit/test progress and warns when no progress is detected.
360
+ * Returns modified HookOutput with warning injected, or null to use original.
361
+ */
362
+ async function handleSurgeonReflection(ir, input, projectDir, sessionId) {
363
+ const agentType = typeof input.agent_type === "string" ? input.agent_type : undefined;
364
+ if (!agentType)
365
+ return null;
366
+ const toolName = String(input.tool_name ?? "");
367
+ const toolOutput = typeof input.tool_output === "string" ? input.tool_output : undefined;
368
+ // Find if any workflow step with max_attempts matches this agent_type
369
+ const workflows = ir.workflows_ir;
370
+ if (!workflows || workflows.length === 0)
371
+ return null;
372
+ let maxAttempts;
373
+ let handoffTo;
374
+ let maxHandoffs;
375
+ let blockedItemsPath;
376
+ // Search all workflows for a step whose role matches agent_type and has max_attempts
377
+ // We need the original DNA step defs, which are encoded in the IR's step_enforce_rules
378
+ // But the original step config (max_attempts etc) isn't in the IR yet.
379
+ // Instead, scan workflow state to find the current step, then check if the IR source has the fields.
380
+ const wfState = await readWorkflowState(projectDir, sessionId);
381
+ if (!wfState || !wfState.active)
382
+ return null;
383
+ // Match agent_type to current role in workflow
384
+ const expectedAgentType = `dna-${wfState.current_role.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[_\s]+/g, "-").toLowerCase()}`;
385
+ if (agentType !== expectedAgentType)
386
+ return null;
387
+ // Look up step config from the compiled IR's workflow
388
+ // We need the original step definitions — they are not directly in IR.
389
+ // For now, we encode max_attempts info in the IR via step_enforce_rules extension.
390
+ // Fallback: check if workflow state has reflection config.
391
+ // Since the IR doesn't carry max_attempts yet, we use a convention:
392
+ // The plugin-adapter will encode reflection fields into step_enforce_rules.
393
+ // For Phase 1, we read from a sidecar file: .dna/compiled/reflection-config.json
394
+ const reflectionConfig = await loadReflectionConfig(projectDir, wfState.workflow, wfState.current_step);
395
+ if (!reflectionConfig)
396
+ return null;
397
+ maxAttempts = reflectionConfig.max_attempts;
398
+ handoffTo = reflectionConfig.handoff_to;
399
+ maxHandoffs = reflectionConfig.max_handoffs ?? 3;
400
+ blockedItemsPath = reflectionConfig.blocked_items_path;
401
+ if (!maxAttempts)
402
+ return null;
403
+ // Read current surgeon state
404
+ const surgeonState = await readSurgeonAttempts(projectDir, sessionId);
405
+ // Track edits
406
+ if (toolName === "Edit") {
407
+ surgeonState.edit_count++;
408
+ await writeSurgeonAttempts(projectDir, surgeonState, sessionId);
409
+ return null;
410
+ }
411
+ // Track test results from Bash
412
+ if (toolName === "Bash" && toolOutput) {
413
+ const match = toolOutput.match(TEST_PASSED_RE);
414
+ if (!match)
415
+ return null; // Can't parse — fail-open
416
+ const greenCount = parseInt(match[1], 10);
417
+ surgeonState.bash_test_count++;
418
+ surgeonState.current_green_count = greenCount;
419
+ if (!surgeonState.baseline_established) {
420
+ surgeonState.baseline_established = true;
421
+ surgeonState.last_green_count = greenCount;
422
+ await writeSurgeonAttempts(projectDir, surgeonState, sessionId);
423
+ return null;
424
+ }
425
+ // Check progress: green count should increase after edits
426
+ if (surgeonState.edit_count >= 2 && greenCount <= surgeonState.last_green_count) {
427
+ surgeonState.fail_count++;
428
+ const result = checkReflectionLimit(surgeonState.fail_count, maxAttempts, surgeonState.handoff_count, maxHandoffs);
429
+ // Update last_green_count for next comparison
430
+ surgeonState.last_green_count = greenCount;
431
+ surgeonState.edit_count = 0;
432
+ await writeSurgeonAttempts(projectDir, surgeonState, sessionId);
433
+ if (result.action === "handoff" || result.action === "warn") {
434
+ const target = handoffTo ? ` Hand off to '${handoffTo}' for re-analysis.` : "";
435
+ return allowOutput(`WARN [Intent DNA] Reflection Gate: ${result.reason}${target}\n` +
436
+ `Review experience_chain in .dna/state/workflow/surgeon-attempts.json before next attempt.`);
437
+ }
438
+ if (result.action === "skip") {
439
+ const path = blockedItemsPath ? ` Record blocked item to ${blockedItemsPath}.` : "";
440
+ return allowOutput(`WARN [Intent DNA] Reflection Gate: ${result.reason}${path}\n` +
441
+ `SKIP this item and move to the next.`);
442
+ }
443
+ }
444
+ else {
445
+ // Progress made — reset counters
446
+ surgeonState.last_green_count = greenCount;
447
+ surgeonState.edit_count = 0;
448
+ if (greenCount > surgeonState.last_green_count) {
449
+ surgeonState.fail_count = 0;
450
+ }
451
+ }
452
+ await writeSurgeonAttempts(projectDir, surgeonState, sessionId);
453
+ }
454
+ return null;
455
+ }
456
+ /**
457
+ * Load reflection config for a specific workflow step.
458
+ * Reads from .dna/compiled/ir.json — scans workflows_ir for step metadata.
459
+ * Falls back to sidecar .dna/compiled/reflection-config.json for Phase 1.
460
+ */
461
+ async function loadReflectionConfig(projectDir, _workflowName, _stepId) {
462
+ // Phase 1: read sidecar config
463
+ const configPath = join(projectDir, ".dna", "compiled", "reflection-config.json");
464
+ try {
465
+ const raw = await readFile(configPath, "utf-8");
466
+ const config = JSON.parse(raw);
467
+ return config[_workflowName]?.[_stepId] ?? null;
468
+ }
469
+ catch {
470
+ return null;
471
+ }
472
+ }
338
473
  // ── Entry Point ────────────────────────────────────────────
339
474
  main().catch(() => {
340
475
  // Fail-open: never block Claude Code on unexpected errors
@@ -139,3 +139,12 @@ export declare function resolveToolTarget(target?: string): string[] | null;
139
139
  * Returns deduplicated list of target paths.
140
140
  */
141
141
  export declare function extractBashWritePaths(command: string): string[];
142
+ export interface ReflectionResult {
143
+ action: "continue" | "warn" | "handoff" | "skip";
144
+ reason: string;
145
+ }
146
+ /**
147
+ * Determine the reflection gate action based on fail/handoff counts.
148
+ * Pure function — no I/O.
149
+ */
150
+ export declare function checkReflectionLimit(failCount: number, maxAttempts: number, handoffCount: number, maxHandoffs: number): ReflectionResult;
@@ -690,3 +690,16 @@ function toKebabCase(s) {
690
690
  .replace(/[_\s]+/g, "-")
691
691
  .toLowerCase();
692
692
  }
693
+ /**
694
+ * Determine the reflection gate action based on fail/handoff counts.
695
+ * Pure function — no I/O.
696
+ */
697
+ export function checkReflectionLimit(failCount, maxAttempts, handoffCount, maxHandoffs) {
698
+ if (failCount < maxAttempts) {
699
+ return { action: "continue", reason: `Attempt ${failCount + 1}/${maxAttempts}` };
700
+ }
701
+ if (handoffCount < maxHandoffs) {
702
+ return { action: "handoff", reason: `No progress after ${maxAttempts} attempts, handing off for re-analysis (handoff ${handoffCount + 1}/${maxHandoffs})` };
703
+ }
704
+ return { action: "skip", reason: `No progress after ${maxAttempts} attempts and ${maxHandoffs} handoffs exhausted — skipping` };
705
+ }
@@ -30,6 +30,24 @@ export interface AuditEntry {
30
30
  message: string;
31
31
  session_id?: string;
32
32
  }
33
+ /** Surgeon attempt state for reflection gate tracking */
34
+ export interface SurgeonAttemptState {
35
+ round: number;
36
+ edit_count: number;
37
+ bash_test_count: number;
38
+ last_green_count: number;
39
+ current_green_count: number;
40
+ fail_count: number;
41
+ handoff_count: number;
42
+ baseline_established: boolean;
43
+ experience_chain: Array<{
44
+ round: number;
45
+ analysis_summary: string;
46
+ attempt: string;
47
+ result: string;
48
+ lesson: string;
49
+ }>;
50
+ }
33
51
  /**
34
52
  * Resolve state directory path.
35
53
  * With session isolation: `.dna/state/sessions/<sessionId>/`
@@ -64,6 +82,14 @@ export declare function appendCompletedArtifact(projectDir: string, stepId: stri
64
82
  }, sessionId?: string): Promise<void>;
65
83
  /** Atomic write: write to temp file then rename. */
66
84
  export declare function atomicWrite(filePath: string, data: string): Promise<void>;
85
+ /**
86
+ * Read surgeon attempt state. Returns default state if not found.
87
+ */
88
+ export declare function readSurgeonAttempts(projectDir: string, sessionId?: string): Promise<SurgeonAttemptState>;
89
+ /**
90
+ * Write surgeon attempt state atomically.
91
+ */
92
+ export declare function writeSurgeonAttempts(projectDir: string, state: SurgeonAttemptState, sessionId?: string): Promise<void>;
67
93
  /** Trace entry for hook call observability */
68
94
  export interface TraceEntry {
69
95
  trace_id: string;
@@ -136,6 +136,43 @@ export async function atomicWrite(filePath, data) {
136
136
  await writeFile(tmpPath, data, "utf-8");
137
137
  await rename(tmpPath, filePath);
138
138
  }
139
+ // ── Surgeon Attempt State ────────────────────────────────
140
+ const SURGEON_ATTEMPTS_FILE = "workflow/surgeon-attempts.json";
141
+ function defaultSurgeonState() {
142
+ return {
143
+ round: 0,
144
+ edit_count: 0,
145
+ bash_test_count: 0,
146
+ last_green_count: 0,
147
+ current_green_count: 0,
148
+ fail_count: 0,
149
+ handoff_count: 0,
150
+ baseline_established: false,
151
+ experience_chain: [],
152
+ };
153
+ }
154
+ /**
155
+ * Read surgeon attempt state. Returns default state if not found.
156
+ */
157
+ export async function readSurgeonAttempts(projectDir, sessionId) {
158
+ const stateDir = resolveStateDir(projectDir, sessionId);
159
+ const filePath = join(stateDir, SURGEON_ATTEMPTS_FILE);
160
+ try {
161
+ const raw = await readFile(filePath, "utf-8");
162
+ return JSON.parse(raw);
163
+ }
164
+ catch {
165
+ return defaultSurgeonState();
166
+ }
167
+ }
168
+ /**
169
+ * Write surgeon attempt state atomically.
170
+ */
171
+ export async function writeSurgeonAttempts(projectDir, state, sessionId) {
172
+ const stateDir = resolveStateDir(projectDir, sessionId);
173
+ const filePath = join(stateDir, SURGEON_ATTEMPTS_FILE);
174
+ await atomicWrite(filePath, JSON.stringify(state, null, 2));
175
+ }
139
176
  const TRACE_DIR = "trace";
140
177
  const MAX_TRACE_SIZE = 10 * 1024 * 1024; // 10MB
141
178
  const TRACE_RETENTION_DAYS = 7;
@@ -44,6 +44,12 @@ export function compileWorkflowToSkill(plan, roles, ir, variables) {
44
44
  }
45
45
  // Steps — each step MUST be executed via Agent() tool call
46
46
  const usedRoles = new Set(plan.steps.map((s) => s.role));
47
+ // Pre-compute handoff targets: steps that are pointed to by another step's handoff_to
48
+ const handoffTargets = new Set();
49
+ for (const step of plan.steps) {
50
+ if (step.handoff_to)
51
+ handoffTargets.add(step.handoff_to);
52
+ }
47
53
  lines.push("<Steps>");
48
54
  let stepNumber = 0;
49
55
  for (let i = 0; i < plan.parallel_groups.length; i++) {
@@ -58,8 +64,24 @@ export function compileWorkflowToSkill(plan, roles, ir, variables) {
58
64
  const promptParts = [
59
65
  "Do NOT spawn sub-agents.",
60
66
  `You are the ${agentType} agent.`,
61
- step.prompt || step.description,
62
67
  ];
68
+ // Reflection gate: inject Previous_Attempts for handoff target steps
69
+ if (handoffTargets.has(step.id)) {
70
+ promptParts.push("<Previous_Attempts>\\n" +
71
+ "If this is not the first round, the workflow state contains an experience_chain " +
72
+ "with previous analysis, what surgeon tried, results, and lessons learned. " +
73
+ "Read the experience chain from workflow state before analyzing. " +
74
+ "DO NOT repeat approaches that already failed.\\n" +
75
+ "</Previous_Attempts>");
76
+ }
77
+ // Reflection gate: inject Failed_Approaches for steps with max_attempts + handoff_to
78
+ if (step.max_attempts && step.handoff_to) {
79
+ promptParts.push("<Failed_Approaches>\\n" +
80
+ "If previous attempts exist in the experience chain, review what was tried " +
81
+ "and why it failed. You MUST use a DIFFERENT approach.\\n" +
82
+ "</Failed_Approaches>");
83
+ }
84
+ promptParts.push(step.prompt || step.description);
63
85
  const agentPrompt = escapePrompt(promptParts.join(" "));
64
86
  lines.push(`${stepNumber}. **${step.id}**${optional}`);
65
87
  lines.push(` ${step.description}${runIf}`);
@@ -77,6 +99,18 @@ export function compileWorkflowToSkill(plan, roles, ir, variables) {
77
99
  lines.push(` Verify produced artifacts: ${artifacts}`);
78
100
  }
79
101
  lines.push("");
102
+ // Reflection gate: inject Reflection_Gate after steps with max_attempts
103
+ if (step.max_attempts) {
104
+ const handoffStep = step.handoff_to ?? "previous step";
105
+ const maxH = step.max_handoffs ?? 3;
106
+ const blockedPath = step.blocked_items_path ?? "blocked_items.md";
107
+ lines.push(`<Reflection_Gate>`);
108
+ lines.push(`max_attempts=${step.max_attempts}, handoff_to=${handoffStep}, max_handoffs=${maxH}`);
109
+ lines.push(`No test progress after ${step.max_attempts} attempts → handoff to ${handoffStep} for re-analysis.`);
110
+ lines.push(`After ${maxH} handoffs with no progress → SKIP and record to ${blockedPath}.`);
111
+ lines.push(`</Reflection_Gate>`);
112
+ lines.push("");
113
+ }
80
114
  }
81
115
  }
82
116
  lines.push("</Steps>");
@@ -184,6 +218,12 @@ export function compileWorkflowToSkill(plan, roles, ir, variables) {
184
218
  lines.push("</Variables>");
185
219
  lines.push("");
186
220
  }
221
+ // Workflow Boundary — prevent cross-workflow execution
222
+ lines.push("<Workflow_Boundary>");
223
+ lines.push("This workflow is COMPLETE. Do NOT proceed to any other workflow.");
224
+ lines.push("Report your results and STOP. The user will decide the next step.");
225
+ lines.push("</Workflow_Boundary>");
226
+ lines.push("");
187
227
  let content = lines.join("\n") + "\n";
188
228
  // Substitute project variables: {{var_name}} → value
189
229
  if (variables) {
@@ -146,6 +146,12 @@ export interface WorkflowStepDef {
146
146
  relax_after_iteration?: number;
147
147
  additional_write_paths?: string[];
148
148
  };
149
+ max_attempts?: number;
150
+ on_fail?: "retry_with_feedback" | "handoff" | "skip";
151
+ handoff_to?: string;
152
+ max_handoffs?: number;
153
+ on_handoff_exhausted?: "skip" | "stop";
154
+ blocked_items_path?: string;
149
155
  }
150
156
  /** Top-level workflow definition */
151
157
  export interface WorkflowDef {
@@ -345,6 +351,12 @@ export interface WorkflowStep {
345
351
  completion: CompletionCheck[] | null;
346
352
  checkpoints: StepCheckpoint[] | null;
347
353
  handoff: StepHandoff | null;
354
+ max_attempts?: number;
355
+ on_fail?: "retry_with_feedback" | "handoff" | "skip";
356
+ handoff_to?: string;
357
+ max_handoffs?: number;
358
+ on_handoff_exhausted?: "skip" | "stop";
359
+ blocked_items_path?: string;
348
360
  }
349
361
  /** A group of steps that can execute in parallel */
350
362
  export interface ParallelGroup {
@@ -235,6 +235,20 @@ function validateWorkflow(workflow, roleNames, pathPrefix) {
235
235
  if (step.prompt !== undefined && (typeof step.prompt !== "string" || !step.prompt)) {
236
236
  errors.push({ path: `${stepPath}.prompt`, message: "prompt must be a non-empty string" });
237
237
  }
238
+ // on_fail: must be a valid enum value
239
+ if (step.on_fail !== undefined) {
240
+ const validOnFail = ["retry_with_feedback", "handoff", "skip"];
241
+ if (!validOnFail.includes(step.on_fail)) {
242
+ errors.push({ path: `${stepPath}.on_fail`, message: `must be one of: ${validOnFail.join(", ")}` });
243
+ }
244
+ }
245
+ // on_handoff_exhausted: must be a valid enum value
246
+ if (step.on_handoff_exhausted !== undefined) {
247
+ const validExhausted = ["skip", "stop"];
248
+ if (!validExhausted.includes(step.on_handoff_exhausted)) {
249
+ errors.push({ path: `${stepPath}.on_handoff_exhausted`, message: `must be one of: ${validExhausted.join(", ")}` });
250
+ }
251
+ }
238
252
  // isolation: must be a valid enum value
239
253
  if (step.isolation !== undefined) {
240
254
  const validIsolation = ["none", "worktree", "auto"];
@@ -292,6 +306,18 @@ function validateWorkflow(workflow, roleNames, pathPrefix) {
292
306
  });
293
307
  }
294
308
  }
309
+ // handoff_to: must reference an existing step in this workflow
310
+ if (step.handoff_to !== undefined) {
311
+ if (typeof step.handoff_to !== "string" || !step.handoff_to) {
312
+ errors.push({ path: `${stepPath}.handoff_to`, message: "handoff_to must be a non-empty string" });
313
+ }
314
+ else if (!stepIds.has(step.handoff_to)) {
315
+ errors.push({
316
+ path: `${stepPath}.handoff_to`,
317
+ message: `step '${step.handoff_to}' not found in workflow`,
318
+ });
319
+ }
320
+ }
295
321
  }
296
322
  // Validate default_isolation
297
323
  if (workflow.default_isolation !== undefined) {
@@ -316,6 +316,12 @@ workflows:
316
316
  role: surgeon
317
317
  depends_on: [investigate]
318
318
  description: "Fix identified issues. Max 5 files per round."
319
+ max_attempts: 3
320
+ on_fail: handoff
321
+ handoff_to: investigate
322
+ max_handoffs: 3
323
+ on_handoff_exhausted: skip
324
+ blocked_items_path: "docs/behavior/blocked_items.md"
319
325
  checkpoints:
320
326
  - assert: clean_working_tree
321
327
  message: "Commit all changes before proceeding"
@@ -337,9 +343,26 @@ workflows:
337
343
  produces:
338
344
  - type: git_commit
339
345
  description: "Rescue round commit"
340
- - id: review
346
+ - id: progress_check
341
347
  role: investigator
342
348
  depends_on: [fix]
349
+ description: "Check test progress after surgeon's fix."
350
+ prompt: |
351
+ Run tests. Compare green count with previous round.
352
+ If green count increased: PROGRESS — proceed to review.
353
+ If green count unchanged or decreased: NO_PROGRESS — record what surgeon tried
354
+ and why it didn't work, for the experience chain.
355
+ handoff:
356
+ consumes:
357
+ - type: git_commit
358
+ from: fix
359
+ description: "Fix commit from surgeon"
360
+ produces:
361
+ - type: summary
362
+ description: "Progress check result (PROGRESS or NO_PROGRESS)"
363
+ - id: review
364
+ role: investigator
365
+ depends_on: [progress_check]
343
366
  description: "Read-only review of surgeon's changes."
344
367
  prompt: |
345
368
  Review surgeon's git diff (read-only, do NOT modify any files):
@@ -352,6 +375,9 @@ workflows:
352
375
  Verdict: REQUEST_CHANGES → describe specific problems. Next round's investigate step will include this feedback.
353
376
  handoff:
354
377
  consumes:
378
+ - type: summary
379
+ from: progress_check
380
+ description: "Progress check result"
355
381
  - type: git_commit
356
382
  from: fix
357
383
  description: "Committed fix from surgeon"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "intentdna",
3
- "version": "1.5.9",
3
+ "version": "1.5.10",
4
4
  "description": "Intent DNA — Declarative policy layer for AI agent behavior",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",